diff --git a/docker-compose.yml b/docker-compose.yml index 198df198..91ca1263 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,5 +12,9 @@ services: XRAY_VMESS_AEAD_FORCED: "false" XUI_ENABLE_FAIL2BAN: "true" tty: true - network_mode: host + # network_mode: host # Отключено, так как на Windows 11/WSL эта опция не пробрасывает порты на localhost + ports: + - "2053:2053" # Веб-панель + - "2054:2054" # Доп. порт панели/API + - "2096:2096" # Порт подписки (Subscription port) restart: unless-stopped diff --git a/subproject/Xray-core-main/.github/ISSUE_TEMPLATE/bug_report.yml b/subproject/Xray-core-main/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 00000000..3a5c04f8 --- /dev/null +++ b/subproject/Xray-core-main/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,78 @@ +name: Bug report +description: "Submit Xray-core bug" +body: + - type: checkboxes + attributes: + label: Integrity requirements + description: |- + Please check all of the following options to prove that you have read and understood the requirements, otherwise this issue will be closed. + options: + - label: I have read all the comments in the issue template and ensured that this issue meet the requirements. + required: true + - label: I confirm that I have read the documentation, understand the meaning of all the configuration items I wrote, and did not pile up seemingly useful options or default values. + required: true + - label: I provided the complete config and logs, rather than just providing the truncated parts based on my own judgment. + required: true + - label: I searched issues and did not find any similar issues. + required: true + - label: The problem can be successfully reproduced in the latest Release + required: true + - type: textarea + attributes: + label: Description + description: |- + Please provide a detailed description of the error. And the information you think valuable. + If the problem occurs after the update, please provide the **specific** version + validations: + required: true + - type: textarea + attributes: + label: Reproduction Method + description: |- + Based on the configuration you provided below, provide the method to reproduce the bug. + validations: + required: true + - type: markdown + attributes: + value: |- + ## Configuration and Log Section + + ### For config + Please provide the configuration files that can reproduce the problem, including the server and client. + Don't just paste a big exported config file here. Eliminate useless inbound/outbound, rules, options, this can help determine the problem, if you really want to get help. + After removing parts that do not affect reproduction, provide the actual running **complete** file. + meaning of complete: This config can be directly used to start the core, **not a truncated part of the config**. For fields like keys, use newly generated valid parameters that have not been actually used to fill in. + + ### For logs + Please set the log level to debug and dnsLog to true first. + Restart Xray-core, then operate according to the reproduction method, try to reduce the irrelevant part in the log. + Remember to delete parts with personal information (such as UUID and IP). + Provide the log of Xray-core, not the log output by the panel or other things. + + ### Finally + The specific content to be filled in each of the following text boxes needs to be placed between ```
``` and ```
```, like this + ``` +

+        (config)
+        
+ ``` + - type: textarea + attributes: + label: Client config + validations: + required: true + - type: textarea + attributes: + label: Server config + validations: + required: true + - type: textarea + attributes: + label: Client log + validations: + required: true + - type: textarea + attributes: + label: Server log + validations: + required: true \ No newline at end of file diff --git a/subproject/Xray-core-main/.github/ISSUE_TEMPLATE/bug_report_zh.yml b/subproject/Xray-core-main/.github/ISSUE_TEMPLATE/bug_report_zh.yml new file mode 100644 index 00000000..939f663c --- /dev/null +++ b/subproject/Xray-core-main/.github/ISSUE_TEMPLATE/bug_report_zh.yml @@ -0,0 +1,78 @@ +name: bug反馈 +description: "提交 Xray-core bug" +body: + - type: checkboxes + attributes: + label: 完整性要求 + description: |- + 请勾选以下所有选项以证明您已经阅读并理解了以下要求,否则该 issue 将被关闭。 + options: + - label: 我读完了 issue 模板中的所有注释,确保填写符合要求。 + required: true + - label: 我保证阅读了文档,了解所有我编写的配置文件项的含义,而不是大量堆砌看似有用的选项或默认值。 + required: true + - label: 我提供了完整的配置文件和日志,而不是出于自己的判断只给出截取的部分。 + required: true + - label: 我搜索了 issues, 没有发现已提出的类似问题。 + required: true + - label: 问题在 Release 最新的版本上可以成功复现 + required: true + - type: textarea + attributes: + label: 描述 + description: |- + 请提供错误的详细描述。以及你认为有价值的信息。 + 如果问题在更新后出现,请提供**具体**出现问题的版本号。 + validations: + required: true + - type: textarea + attributes: + label: 重现方式 + description: |- + 基于你下面提供的配置,提供重现BUG方法。 + validations: + required: true + - type: markdown + attributes: + value: |- + ## 配置与日志部分 + + ### 对于配置文件 + 请提供可以重现问题的配置文件,包括服务端和客户端。 + 不要直接在这里黏贴一大段导出的 config 文件。去掉无用的出入站、规则、选项,这可以帮助确定问题,如果你真的想得到帮助。 + 在去掉不影响复现的部分后,提供实际运行的**完整**文件。 + 完整的含义:可以直接使用这个配置启动核心,**不是截取的部分配置**。对于密钥等参数使用重新生成未实际使用的有效参数填充。 + + ### 对于日志 + 请先将日志等级设置为 debug, dnsLog 设置为true. + 重启 Xray-core ,再按复现方式操作,尽量减少日志中的无关部分。 + 记得删除有关个人信息(如UUID与IP)的部分。 + 提供 Xray-core 的日志,而不是面板或者别的东西输出的日志。 + + ### 最后 + 把下面的每格具体内容需要放在 ```
``` 和 ```
``` 中间,如 + ``` +

+        (config)
+        
+ ``` + - type: textarea + attributes: + label: 客户端配置 + validations: + required: true + - type: textarea + attributes: + label: 服务端配置 + validations: + required: true + - type: textarea + attributes: + label: 客户端日志 + validations: + required: true + - type: textarea + attributes: + label: 服务端日志 + validations: + required: true diff --git a/subproject/Xray-core-main/.github/ISSUE_TEMPLATE/config.yml b/subproject/Xray-core-main/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..8fe9cb8e --- /dev/null +++ b/subproject/Xray-core-main/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,4 @@ +contact_links: + - name: Community Support and Questions + url: https://github.com/XTLS/Xray-core/discussions + about: Please ask and answer questions there. The issue tracker is for issues with core. diff --git a/subproject/Xray-core-main/.github/build/friendly-filenames.json b/subproject/Xray-core-main/.github/build/friendly-filenames.json new file mode 100644 index 00000000..e6679aed --- /dev/null +++ b/subproject/Xray-core-main/.github/build/friendly-filenames.json @@ -0,0 +1,33 @@ +{ + "android-arm64": { "friendlyName": "android-arm64-v8a" }, + "darwin-amd64": { "friendlyName": "macos-64" }, + "darwin-arm64": { "friendlyName": "macos-arm64-v8a" }, + "freebsd-386": { "friendlyName": "freebsd-32" }, + "freebsd-amd64": { "friendlyName": "freebsd-64" }, + "freebsd-arm64": { "friendlyName": "freebsd-arm64-v8a" }, + "freebsd-arm7": { "friendlyName": "freebsd-arm32-v7a" }, + "linux-386": { "friendlyName": "linux-32" }, + "linux-amd64": { "friendlyName": "linux-64" }, + "linux-arm5": { "friendlyName": "linux-arm32-v5" }, + "linux-arm64": { "friendlyName": "linux-arm64-v8a" }, + "linux-arm6": { "friendlyName": "linux-arm32-v6" }, + "linux-arm7": { "friendlyName": "linux-arm32-v7a" }, + "linux-mips64le": { "friendlyName": "linux-mips64le" }, + "linux-mips64": { "friendlyName": "linux-mips64" }, + "linux-mipslesoftfloat": { "friendlyName": "linux-mips32le-softfloat" }, + "linux-mipsle": { "friendlyName": "linux-mips32le" }, + "linux-mipssoftfloat": { "friendlyName": "linux-mips32-softfloat" }, + "linux-mips": { "friendlyName": "linux-mips32" }, + "linux-ppc64le": { "friendlyName": "linux-ppc64le" }, + "linux-ppc64": { "friendlyName": "linux-ppc64" }, + "linux-riscv64": { "friendlyName": "linux-riscv64" }, + "linux-loong64": { "friendlyName": "linux-loong64" }, + "linux-s390x": { "friendlyName": "linux-s390x" }, + "openbsd-386": { "friendlyName": "openbsd-32" }, + "openbsd-amd64": { "friendlyName": "openbsd-64" }, + "openbsd-arm64": { "friendlyName": "openbsd-arm64-v8a" }, + "openbsd-arm7": { "friendlyName": "openbsd-arm32-v7a" }, + "windows-386": { "friendlyName": "windows-32" }, + "windows-amd64": { "friendlyName": "windows-64" }, + "windows-arm64": { "friendlyName": "windows-arm64-v8a" } +} diff --git a/subproject/Xray-core-main/.github/dependabot.yml b/subproject/Xray-core-main/.github/dependabot.yml new file mode 100644 index 00000000..b69c1776 --- /dev/null +++ b/subproject/Xray-core-main/.github/dependabot.yml @@ -0,0 +1,15 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "gomod" + directory: "/" # Location of package manifests + schedule: + interval: "daily" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" diff --git a/subproject/Xray-core-main/.github/docker/Dockerfile b/subproject/Xray-core-main/.github/docker/Dockerfile new file mode 100644 index 00000000..867e671a --- /dev/null +++ b/subproject/Xray-core-main/.github/docker/Dockerfile @@ -0,0 +1,62 @@ +# syntax=docker/dockerfile:latest +FROM --platform=$BUILDPLATFORM golang:latest AS build + +# Build xray-core +WORKDIR /src +COPY . . +ARG TARGETOS +ARG TARGETARCH +RUN GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 go build -o xray -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags "-s -w -buildid=" ./main + +# Download geodat into a staging directory +ADD https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/geoip.dat /tmp/geodat/geoip.dat +ADD https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/geosite.dat /tmp/geodat/geosite.dat + +RUN mkdir -p /tmp/empty + +# Create config files with empty JSON content +RUN mkdir -p /tmp/usr/local/etc/xray +RUN cat </tmp/usr/local/etc/xray/00_log.json +{ + "log": { + "error": "/var/log/xray/error.log", + "loglevel": "warning", + "access": "none", + "dnsLog": false + } +} +EOF +RUN echo '{}' >/tmp/usr/local/etc/xray/01_api.json +RUN echo '{}' >/tmp/usr/local/etc/xray/02_dns.json +RUN echo '{}' >/tmp/usr/local/etc/xray/03_routing.json +RUN echo '{}' >/tmp/usr/local/etc/xray/04_policy.json +RUN echo '{}' >/tmp/usr/local/etc/xray/05_inbounds.json +RUN echo '{}' >/tmp/usr/local/etc/xray/06_outbounds.json +RUN echo '{}' >/tmp/usr/local/etc/xray/07_transport.json +RUN echo '{}' >/tmp/usr/local/etc/xray/08_stats.json +RUN echo '{}' >/tmp/usr/local/etc/xray/09_reverse.json + +# Create log files +RUN mkdir -p /tmp/var/log/xray && touch \ + /tmp/var/log/xray/access.log \ + /tmp/var/log/xray/error.log + +# Build finally image +FROM gcr.io/distroless/static:nonroot + +COPY --from=build --chown=0:0 --chmod=755 /src/xray /usr/local/bin/xray +COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /usr/local/share/xray +COPY --from=build --chown=0:0 --chmod=644 /tmp/geodat/*.dat /usr/local/share/xray/ +COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /usr/local/etc/xray +COPY --from=build --chown=0:0 --chmod=644 /tmp/usr/local/etc/xray/*.json /usr/local/etc/xray/ +COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /var/log/xray +COPY --from=build --chown=65532:65532 --chmod=600 /tmp/var/log/xray/*.log /var/log/xray/ + +VOLUME /usr/local/etc/xray +VOLUME /var/log/xray + +ARG TZ=Etc/UTC +ENV TZ=$TZ + +ENTRYPOINT [ "/usr/local/bin/xray" ] +CMD [ "-confdir", "/usr/local/etc/xray/" ] diff --git a/subproject/Xray-core-main/.github/docker/Dockerfile.usa b/subproject/Xray-core-main/.github/docker/Dockerfile.usa new file mode 100644 index 00000000..80cc523a --- /dev/null +++ b/subproject/Xray-core-main/.github/docker/Dockerfile.usa @@ -0,0 +1,71 @@ +# syntax=docker/dockerfile:latest +FROM --platform=$BUILDPLATFORM golang:latest AS build + +# Build xray-core +WORKDIR /src +COPY . . +ARG TARGETOS +ARG TARGETARCH +RUN GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 go build -o xray -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags "-s -w -buildid=" ./main + +# Download geodat into a staging directory +ADD https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/geoip.dat /tmp/geodat/geoip.dat +ADD https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/geosite.dat /tmp/geodat/geosite.dat + +RUN mkdir -p /tmp/empty + +# Create config files with empty JSON content +RUN mkdir -p /tmp/usr/local/etc/xray +RUN cat </tmp/usr/local/etc/xray/00_log.json +{ + "log": { + "error": "/var/log/xray/error.log", + "loglevel": "warning", + "access": "none", + "dnsLog": false + } +} +EOF +RUN echo '{}' >/tmp/usr/local/etc/xray/01_api.json +RUN echo '{}' >/tmp/usr/local/etc/xray/02_dns.json +RUN echo '{}' >/tmp/usr/local/etc/xray/03_routing.json +RUN echo '{}' >/tmp/usr/local/etc/xray/04_policy.json +RUN echo '{}' >/tmp/usr/local/etc/xray/05_inbounds.json +RUN echo '{}' >/tmp/usr/local/etc/xray/06_outbounds.json +RUN echo '{}' >/tmp/usr/local/etc/xray/07_transport.json +RUN echo '{}' >/tmp/usr/local/etc/xray/08_stats.json +RUN echo '{}' >/tmp/usr/local/etc/xray/09_reverse.json + +# Create log files +RUN mkdir -p /tmp/var/log/xray && touch \ + /tmp/var/log/xray/access.log \ + /tmp/var/log/xray/error.log + +# Build finally image +# Note on Distroless Base Image and Architecture Support: +# - The official 'gcr.io/distroless/static' image provided by Google only supports a limited set of architectures for Linux: +# - linux/amd64 +# - linux/arm/v7 +# - linux/arm64/v8 +# - linux/ppc64le +# - linux/s390x +# - Upon inspection, the blob contents of the Distroless images across these architectures are nearly identical, with only minor differences in metadata (e.g., 'Architecture' field in the manifest). +# - Due to this similarity in content, it is feasible to forcibly specify a single platform (e.g., '--platform=linux/amd64') for unsupported architectures, as the core image content remains compatible with statically compiled binaries like Go applications. +FROM --platform=linux/amd64 gcr.io/distroless/static:nonroot + +COPY --from=build --chown=0:0 --chmod=755 /src/xray /usr/local/bin/xray +COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /usr/local/share/xray +COPY --from=build --chown=0:0 --chmod=644 /tmp/geodat/*.dat /usr/local/share/xray/ +COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /usr/local/etc/xray +COPY --from=build --chown=0:0 --chmod=644 /tmp/usr/local/etc/xray/*.json /usr/local/etc/xray/ +COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /var/log/xray +COPY --from=build --chown=65532:65532 --chmod=600 /tmp/var/log/xray/*.log /var/log/xray/ + +VOLUME /usr/local/etc/xray +VOLUME /var/log/xray + +ARG TZ=Etc/UTC +ENV TZ=$TZ + +ENTRYPOINT [ "/usr/local/bin/xray" ] +CMD [ "-confdir", "/usr/local/etc/xray/" ] diff --git a/subproject/Xray-core-main/.github/workflows/docker.yml b/subproject/Xray-core-main/.github/workflows/docker.yml new file mode 100644 index 00000000..38fff2be --- /dev/null +++ b/subproject/Xray-core-main/.github/workflows/docker.yml @@ -0,0 +1,133 @@ +name: Build and Push Docker Image + +on: + release: + types: + - published + - released + + workflow_dispatch: + inputs: + tag: + description: "Docker image tag:" + required: true + latest: + description: "Set to latest" + type: boolean + default: false + +jobs: + build-and-push: + if: (github.event.action != 'published') || (github.event.action == 'published' && github.event.release.prerelease == true) + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Set repository and image name to lowercase + env: + IMAGE_NAME: "${{ github.repository }}" + run: | + echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV} + echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV} + + - name: Validate and extract tag + run: | + SOURCE_TAG="${{ github.event.inputs.tag }}" + if [[ -z "$SOURCE_TAG" ]]; then + SOURCE_TAG="${{ github.ref_name }}" + fi + if [[ -z "$SOURCE_TAG" ]]; then + SOURCE_TAG="${{ github.event.release.tag_name }}" + fi + + if [[ -z "$SOURCE_TAG" ]]; then + echo "Error: Could not determine a valid tag source. Input tag and context tag (github.ref_name) are both empty." + exit 1 + fi + + if [[ "$SOURCE_TAG" =~ ^v[0-9]+\.[0-9] ]]; then + IMAGE_TAG="${SOURCE_TAG#v}" + else + IMAGE_TAG="$SOURCE_TAG" + fi + + echo "Docker image tag: '$IMAGE_TAG'." + echo "IMAGE_TAG=$IMAGE_TAG" >>${GITHUB_ENV} + + LATEST=false + if [[ "${{ github.event_name }}" == "release" && "${{ github.event.release.prerelease }}" == "false" ]] || [[ "${{ github.event_name }}" == "workflow_dispatch" && "${{ github.event.inputs.latest }}" == "true" ]]; then + LATEST=true + fi + + echo "Latest: '$LATEST'." + echo "LATEST=$LATEST" >>${GITHUB_ENV} + + - name: Checkout code + uses: actions/checkout@v6 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build Docker image (main architectures) + id: build_main_arches + uses: docker/build-push-action@v7 + with: + context: . + file: .github/docker/Dockerfile + platforms: | + linux/amd64 + linux/arm/v7 + linux/arm64/v8 + linux/ppc64le + linux/s390x + provenance: false + outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true + + - name: Build Docker image (additional architectures) + id: build_additional_arches + uses: docker/build-push-action@v7 + with: + context: . + file: .github/docker/Dockerfile.usa + platforms: | + linux/386 + linux/arm/v6 + linux/riscv64 + linux/loong64 + provenance: false + outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true + + - name: Create manifest list and push + run: | + echo "Creating multi-arch manifest with tag: '${{ env.FULL_IMAGE_NAME }}:${{ env.IMAGE_TAG }}'." + docker buildx imagetools create \ + --tag ${{ env.FULL_IMAGE_NAME }}:${{ env.IMAGE_TAG }} \ + ${{ env.FULL_IMAGE_NAME }}@${{ steps.build_main_arches.outputs.digest }} \ + ${{ env.FULL_IMAGE_NAME }}@${{ steps.build_additional_arches.outputs.digest }} + + if [[ "${{ env.LATEST }}" == "true" ]]; then + echo "Adding 'latest' tag to manifest: '${{ env.FULL_IMAGE_NAME }}:latest'." + docker buildx imagetools create \ + --tag ${{ env.FULL_IMAGE_NAME }}:latest \ + ${{ env.FULL_IMAGE_NAME }}:${{ env.IMAGE_TAG }} + fi + + - name: Inspect image + run: | + docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ env.IMAGE_TAG }} + + if [[ "${{ env.LATEST }}" == "true" ]]; then + docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:latest + fi diff --git a/subproject/Xray-core-main/.github/workflows/release-win7.yml b/subproject/Xray-core-main/.github/workflows/release-win7.yml new file mode 100644 index 00000000..9d777bc0 --- /dev/null +++ b/subproject/Xray-core-main/.github/workflows/release-win7.yml @@ -0,0 +1,180 @@ +name: Build and Release for Windows 7 + +on: + workflow_dispatch: + release: + types: [published] + push: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + check-assets: + runs-on: ubuntu-latest + steps: + - name: Restore Geodat Cache + uses: actions/cache/restore@v5 + with: + path: resources + key: xray-geodat- + + - name: Restore Wintun Cache + uses: actions/cache/restore@v5 + with: + path: resources + key: xray-wintun- + + - 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 + LIST=('amd64' 'x86') + for ARCHITECTURE in "${LIST[@]}" + do + echo -e "Checking wintun.dll for ${ARCHITECTURE}..." + if [ -s "./resources/wintun/bin/${ARCHITECTURE}/wintun.dll" ]; then + echo -e "wintun.dll for ${ARCHITECTURE} exists." + else + echo -e "wintun.dll for ${ARCHITECTURE} is missing." + 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 + + build: + needs: check-assets + permissions: + contents: write + strategy: + matrix: + include: + # BEGIN Windows 7 + - goos: windows + goarch: amd64 + assetname: win7-64 + - goos: windows + goarch: 386 + assetname: win7-32 + # END Windows 7 + fail-fast: false + + runs-on: ubuntu-latest + env: + GOOS: ${{ matrix.goos}} + GOARCH: ${{ matrix.goarch }} + CGO_ENABLED: 0 + steps: + - name: Checkout codebase + uses: actions/checkout@v6 + + - name: Show workflow information + run: | + _NAME=${{ matrix.assetname }} + echo "GOOS: ${{ matrix.goos }}, GOARCH: ${{ matrix.goarch }}, RELEASE_NAME: $_NAME" + echo "ASSET_NAME=$_NAME" >> $GITHUB_ENV + + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version-file: go.mod + check-latest: true + + - name: Setup patched builder + run: | + GOSDK=$(go env GOROOT) + 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 + rm ./go-for-win7-linux-amd64.zip + + - name: Get project dependencies + run: go mod download + + - name: Build Xray + run: | + mkdir -p build_assets + COMMID=$(git describe --always --dirty) + echo 'Building Xray for Windows 7...' + go build -o build_assets/xray.exe -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags="-X github.com/xtls/xray-core/core.build=${COMMID} -s -w -buildid=" -v ./main + # The line below is for without running conhost.exe version. Commented for not being used. Provided for reference. + # go build -o build_assets/wxray.exe -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags="-H windowsgui -X github.com/xtls/xray-core/core.build=${COMMID} -s -w -buildid=" -v ./main + + - name: Restore Geodat Cache + uses: actions/cache/restore@v5 + with: + path: resources + key: xray-geodat- + + - name: Restore Wintun Cache + uses: actions/cache/restore@v5 + with: + path: resources + key: xray-wintun- + + - name: Add additional assets into package + run: | + mv -f resources/geo* build_assets/ + if [[ ${GOOS} == 'windows' ]]; then + echo 'CreateObject("Wscript.Shell").Run "xray.exe -config config.json",0' > build_assets/xray_no_window.vbs + echo 'Start-Process -FilePath ".\xray.exe" -ArgumentList "-config .\config.json" -WindowStyle Hidden' > build_assets/xray_no_window.ps1 + if [[ ${GOARCH} == 'amd64' ]]; then + mv resources/wintun/bin/amd64/wintun.dll build_assets/ + fi + if [[ ${GOARCH} == '386' ]]; then + mv resources/wintun/bin/x86/wintun.dll build_assets/ + fi + mv resources/wintun/LICENSE.txt build_assets/LICENSE-wintun.txt + fi + + - name: Copy README.md & LICENSE + run: | + cp ${GITHUB_WORKSPACE}/README.md ./build_assets/README.md + cp ${GITHUB_WORKSPACE}/LICENSE ./build_assets/LICENSE + + - name: Create ZIP archive + if: github.event_name == 'release' + shell: bash + run: | + pushd build_assets || exit 1 + touch -mt $(date +%Y01010000) * + zip -9vr ../Xray-${{ env.ASSET_NAME }}.zip . + popd || exit 1 + FILE=./Xray-${{ env.ASSET_NAME }}.zip + DGST=$FILE.dgst + for METHOD in {"md5","sha1","sha256","sha512"} + do + openssl dgst -$METHOD $FILE | sed 's/([^)]*)//g' >>$DGST + done + + - name: Upload binaries to release + uses: svenstaro/upload-release-action@v2 + if: github.event_name == 'release' + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: ./Xray-${{ env.ASSET_NAME }}.zip* + tag: ${{ github.ref }} + file_glob: true + + - name: Upload files to Artifacts + uses: actions/upload-artifact@v7 + with: + name: Xray-${{ env.ASSET_NAME }} + path: | + ./build_assets/* diff --git a/subproject/Xray-core-main/.github/workflows/release.yml b/subproject/Xray-core-main/.github/workflows/release.yml new file mode 100644 index 00000000..33ab9c43 --- /dev/null +++ b/subproject/Xray-core-main/.github/workflows/release.yml @@ -0,0 +1,287 @@ +name: Build and Release + +on: + workflow_dispatch: + release: + types: [published] + push: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + check-assets: + runs-on: ubuntu-latest + steps: + - name: Restore Geodat Cache + uses: actions/cache/restore@v5 + with: + path: resources + key: xray-geodat- + + - name: Restore Wintun Cache + uses: actions/cache/restore@v5 + with: + path: resources + key: xray-wintun- + + - 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 + LIST=('amd64' 'x86' 'arm64') + for ARCHITECTURE in "${LIST[@]}" + do + echo -e "Checking wintun.dll for ${ARCHITECTURE}..." + if [ -s "./resources/wintun/bin/${ARCHITECTURE}/wintun.dll" ]; then + echo -e "wintun.dll for ${ARCHITECTURE} exists." + else + echo -e "wintun.dll for ${ARCHITECTURE} is missing." + 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@v8 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { owner, repo } = context.repo; + await github.rest.actions.createWorkflowDispatch({ + owner, + repo, + workflow_id: 'scheduled-assets-update.yml', + ref: context.ref + }); + console.log('Triggered scheduled-assets-update.yml due to missing assets on branch:', context.ref); + + - name: Sleep for 90 seconds if Assets Missing + if: steps.check-assets.outputs.missing == 'true' + run: sleep 90 + + build: + needs: check-assets + permissions: + contents: write + strategy: + matrix: + # Include amd64 on all platforms. + goos: [windows, freebsd, openbsd, linux, darwin] + goarch: [amd64, 386] + patch-assetname: [""] + exclude: + # Exclude i386 on darwin + - goarch: 386 + goos: darwin + include: + # BEGIN MacOS ARM64 + - goos: darwin + goarch: arm64 + # END MacOS ARM64 + # BEGIN Linux ARM 5 6 7 + - goos: linux + goarch: arm + goarm: 7 + - goos: linux + goarch: arm + goarm: 6 + - goos: linux + goarch: arm + goarm: 5 + # END Linux ARM 5 6 7 + # BEGIN Android ARM 8 + - goos: android + goarch: arm64 + # END Android ARM 8 + # BEGIN Android AMD64 + - goos: android + goarch: amd64 + patch-assetname: android-amd64 + # END Android AMD64 + # Windows ARM + - goos: windows + goarch: arm64 + # BEGIN Other architectures + # BEGIN riscv64 & ARM64 & LOONG64 + - goos: linux + goarch: arm64 + - goos: linux + goarch: riscv64 + - goos: linux + goarch: loong64 + # END riscv64 & ARM64 & LOONG64 + # BEGIN MIPS + - goos: linux + goarch: mips64 + - goos: linux + goarch: mips64le + - goos: linux + goarch: mipsle + - goos: linux + goarch: mips + # END MIPS + # BEGIN PPC + - goos: linux + goarch: ppc64 + - goos: linux + goarch: ppc64le + # END PPC + # BEGIN FreeBSD ARM + - goos: freebsd + goarch: arm64 + - goos: freebsd + goarch: arm + goarm: 7 + # END FreeBSD ARM + # BEGIN S390X + - goos: linux + goarch: s390x + # END S390X + # END Other architectures + # BEGIN OPENBSD ARM + - goos: openbsd + goarch: arm64 + - goos: openbsd + goarch: arm + goarm: 7 + # END OPENBSD ARM + fail-fast: false + + runs-on: ubuntu-latest + env: + GOOS: ${{ matrix.goos }} + GOARCH: ${{ matrix.goarch }} + GOARM: ${{ matrix.goarm }} + CGO_ENABLED: 0 + steps: + - name: Checkout codebase + uses: actions/checkout@v6 + + - 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 + run: | + _NAME=${{ matrix.patch-assetname }} + [ -n "$_NAME" ] || _NAME=$(jq ".[\"$GOOS-$GOARCH$GOARM$GOMIPS\"].friendlyName" -r < .github/build/friendly-filenames.json) + echo "GOOS: $GOOS, GOARCH: $GOARCH, GOARM: $GOARM, GOMIPS: $GOMIPS, RELEASE_NAME: $_NAME" + echo "ASSET_NAME=$_NAME" >> $GITHUB_ENV + + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version-file: go.mod + check-latest: true + + - name: Get project dependencies + run: go mod download + + - name: Build Xray + run: | + mkdir -p build_assets + COMMID=$(git describe --always --dirty) + if [[ ${GOOS} == 'windows' ]]; then + echo 'Building Xray for Windows...' + go build -o build_assets/xray.exe -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags="-X github.com/xtls/xray-core/core.build=${COMMID} -s -w -buildid=" -v ./main + # The line below is for without running conhost.exe version. Commented for not being used. Provided for reference. + # go build -o build_assets/wxray.exe -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags="-H windowsgui -X github.com/xtls/xray-core/core.build=${COMMID} -s -w -buildid=" -v ./main + else + echo 'Building Xray...' + if [[ ${GOARCH} == 'mips' || ${GOARCH} == 'mipsle' ]]; then + go build -o build_assets/xray -trimpath -buildvcs=false -gcflags="-l=4" -ldflags="-X github.com/xtls/xray-core/core.build=${COMMID} -s -w -buildid=" -v ./main + echo 'Building soft-float Xray for MIPS/MIPSLE 32-bit...' + GOMIPS=softfloat go build -o build_assets/xray_softfloat -trimpath -buildvcs=false -gcflags="-l=4" -ldflags="-X github.com/xtls/xray-core/core.build=${COMMID} -s -w -buildid=" -v ./main + else + go build -o build_assets/xray -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags="-X github.com/xtls/xray-core/core.build=${COMMID} -s -w -buildid=" -v ./main + fi + fi + + - name: Restore Geodat Cache + uses: actions/cache/restore@v5 + with: + path: resources + key: xray-geodat- + + - name: Restore Wintun Cache + if: matrix.goos == 'windows' + uses: actions/cache/restore@v5 + with: + path: resources + key: xray-wintun- + + - name: Add additional assets into package + run: | + mv -f resources/geo* build_assets/ + if [[ ${GOOS} == 'windows' ]]; then + echo 'CreateObject("Wscript.Shell").Run "xray.exe -config config.json",0' > build_assets/xray_no_window.vbs + echo 'Start-Process -FilePath ".\xray.exe" -ArgumentList "-config .\config.json" -WindowStyle Hidden' > build_assets/xray_no_window.ps1 + if [[ ${GOARCH} == 'amd64' ]]; then + mv resources/wintun/bin/amd64/wintun.dll build_assets/ + fi + if [[ ${GOARCH} == '386' ]]; then + mv resources/wintun/bin/x86/wintun.dll build_assets/ + fi + if [[ ${GOARCH} == 'arm64' ]]; then + mv resources/wintun/bin/arm64/wintun.dll build_assets/ + fi + mv resources/wintun/LICENSE.txt build_assets/LICENSE-wintun.txt + fi + + - name: Copy README.md & LICENSE + run: | + cp ${GITHUB_WORKSPACE}/README.md ./build_assets/README.md + cp ${GITHUB_WORKSPACE}/LICENSE ./build_assets/LICENSE + + - name: Create ZIP archive + if: github.event_name == 'release' + shell: bash + run: | + pushd build_assets || exit 1 + touch -mt $(date +%Y01010000) * + zip -9vr ../Xray-${{ env.ASSET_NAME }}.zip . + popd || exit 1 + FILE=./Xray-${{ env.ASSET_NAME }}.zip + DGST=$FILE.dgst + for METHOD in {"md5","sha1","sha256","sha512"} + do + openssl dgst -$METHOD $FILE | sed 's/([^)]*)//g' >>$DGST + done + + - name: Upload binaries to release + uses: svenstaro/upload-release-action@v2 + if: github.event_name == 'release' + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: ./Xray-${{ env.ASSET_NAME }}.zip* + tag: ${{ github.ref }} + file_glob: true + + - name: Upload files to Artifacts + uses: actions/upload-artifact@v7 + with: + name: Xray-${{ env.ASSET_NAME }} + path: | + ./build_assets/* diff --git a/subproject/Xray-core-main/.github/workflows/scheduled-assets-update.yml b/subproject/Xray-core-main/.github/workflows/scheduled-assets-update.yml new file mode 100644 index 00000000..44d4cb5b --- /dev/null +++ b/subproject/Xray-core-main/.github/workflows/scheduled-assets-update.yml @@ -0,0 +1,129 @@ +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) +# - Wintun (wintun.dll) + +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@v5 + with: + path: resources + key: xray-geodat- + + - name: Update Geodat + id: update + uses: nick-fields/retry@v4 + 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@v5 + if: ${{ steps.update.outputs.unhit }} + with: + path: resources + key: xray-geodat-${{ github.sha }}-${{ github.run_number }} + + wintun: + if: github.event.schedule == '30 22 * * *' || github.event_name == 'push' || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + steps: + - name: Restore Wintun Cache + uses: actions/cache/restore@v5 + with: + path: resources + key: xray-wintun- + + - name: Force downloading if run manually or on file update + if: github.event_name == 'workflow_dispatch' || github.event_name == 'push' + run: | + echo "FORCE_UPDATE=true" >> $GITHUB_ENV + + - name: Update Wintun + id: update + uses: nick-fields/retry@v4 + with: + timeout_minutes: 60 + retry_wait_seconds: 60 + max_attempts: 60 + command: | + [ -d 'resources' ] || mkdir resources + LIST=('amd64' 'x86' 'arm64') + for ARCHITECTURE in "${LIST[@]}" + do + FILE_PATH="resources/wintun/bin/${ARCHITECTURE}/wintun.dll" + echo -e "Checking if wintun.dll for ${ARCHITECTURE} exists..." + if [ -s "./resources/wintun/bin/${ARCHITECTURE}/wintun.dll" ]; then + echo -e "wintun.dll for ${ARCHITECTURE} exists" + continue + else + echo -e "wintun.dll for ${ARCHITECTURE} is missing" + missing=true + fi + done + if [ -s "./resources/wintun/LICENSE.txt" ]; then + echo -e "LICENSE for Wintun exists" + else + echo -e "LICENSE for Wintun is missing" + missing=true + fi + if [[ -v FORCE_UPDATE ]]; then + missing=true + fi + if [[ "$missing" == true ]]; then + FILENAME=wintun.zip + DOWNLOAD_FILE=wintun-0.14.1.zip + echo -e "Downloading https://www.wintun.net/builds/${DOWNLOAD_FILE}..." + curl -L "https://www.wintun.net/builds/${DOWNLOAD_FILE}" -o "${FILENAME}" + echo -e "Unpacking wintun..." + unzip -u ${FILENAME} -d resources/ + echo "unhit=true" >> $GITHUB_OUTPUT + fi + + - name: Save Wintun Cache + uses: actions/cache/save@v5 + if: ${{ steps.update.outputs.unhit }} + with: + path: resources + key: xray-wintun-${{ github.sha }}-${{ github.run_number }} diff --git a/subproject/Xray-core-main/.github/workflows/test.yml b/subproject/Xray-core-main/.github/workflows/test.yml new file mode 100644 index 00000000..c07983a4 --- /dev/null +++ b/subproject/Xray-core-main/.github/workflows/test.yml @@ -0,0 +1,77 @@ +name: Test + +on: + push: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + check-assets: + runs-on: ubuntu-latest + steps: + - name: Restore Geodat Cache + uses: actions/cache/restore@v5 + 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 + + check-proto: + runs-on: ubuntu-latest + steps: + - name: Checkout codebase + uses: actions/checkout@v6 + - name: Check Proto Version Header + run: | + head -n 4 core/config.pb.go > ref.txt + find . -name "*.pb.go" ! -name "*_grpc.pb.go" -print0 | while IFS= read -r -d '' file; do + if ! cmp -s ref.txt <(head -n 4 "$file"); then + echo "Error: Header mismatch in $file" + head -n 4 "$file" + exit 1 + fi + done + + test: + needs: check-assets + permissions: + contents: read + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [windows-latest, ubuntu-latest, macos-latest] + steps: + - name: Checkout codebase + uses: actions/checkout@v6 + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version-file: go.mod + check-latest: true + - name: Restore Geodat Cache + uses: actions/cache/restore@v5 + with: + path: resources + key: xray-geodat- + enableCrossOsArchive: true + - name: Test + run: go test -timeout 1h -v ./... diff --git a/subproject/Xray-core-main/.gitignore b/subproject/Xray-core-main/.gitignore new file mode 100644 index 00000000..f1eca817 --- /dev/null +++ b/subproject/Xray-core-main/.gitignore @@ -0,0 +1,68 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# macOS specific files +.DS_Store + +# IDE/editor specific files +.idea/ +.vscode/ +*.swp +*.swo + +# Archives and compressed files +*.zip +*.tar.gz +*.tar +*.gz +*.bz2 + +# Go build binaries +xray +xray_softfloat +mockgen +vprotogen +!infra/vprotogen/ +errorgen +!common/errors/errorgen/ +*.dat + +# Build assets +/build_assets/ + +# Output from dlv test +**/debug.* + +# Certificates and keys +*.crt +*.key + +# Dependency directories (uncomment if needed) +# vendor/ + +# Logs +*.log + +# Coverage reports +coverage.* + +# Node modules (in case of frontend assets) +node_modules/ + +# System files +Thumbs.db +ehthumbs.db + +# Other common ignores +*.bak +*.tmp diff --git a/subproject/Xray-core-main/CODE_OF_CONDUCT.md b/subproject/Xray-core-main/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..bb773213 --- /dev/null +++ b/subproject/Xray-core-main/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +https://t.me/projectXtls. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/subproject/Xray-core-main/LICENSE b/subproject/Xray-core-main/LICENSE new file mode 100644 index 00000000..a612ad98 --- /dev/null +++ b/subproject/Xray-core-main/LICENSE @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/subproject/Xray-core-main/README.md b/subproject/Xray-core-main/README.md new file mode 100644 index 00000000..f2001426 --- /dev/null +++ b/subproject/Xray-core-main/README.md @@ -0,0 +1,214 @@ +# Project X + +[Project X](https://github.com/XTLS) originates from XTLS protocol, providing a set of network tools such as [Xray-core](https://github.com/XTLS/Xray-core) and [REALITY](https://github.com/XTLS/REALITY). + +[README](https://github.com/XTLS/Xray-core#readme) is open, so feel free to submit your project [here](https://github.com/XTLS/Xray-core/pulls). + +## Sponsors + +[![Remnawave](https://github.com/user-attachments/assets/a22d34ae-01ee-441c-843a-85356748ed1e)](https://docs.rw) + +[![Happ](https://github.com/user-attachments/assets/14055dab-e8bb-48bd-89e8-962709e4098e)](https://happ.su) + +[![BlancVPN](https://github.com/user-attachments/assets/9145ea7d-5da3-446e-8143-710dba4292c3)](https://blanc.link/VMTSDqW) + +[**Sponsor Xray-core**](https://github.com/XTLS/Xray-core/issues/3668) + +## Donation & NFTs + +### [Collect a Project X NFT to support the development of Project X!](https://opensea.io/item/ethereum/0x5ee362866001613093361eb8569d59c4141b76d1/1) + +[Project X NFT](https://opensea.io/item/ethereum/0x5ee362866001613093361eb8569d59c4141b76d1/1) + +- **TRX(Tron)/USDT/USDC: `TNrDh5VSfwd4RPrwsohr6poyNTfFefNYan`** +- **TON: `UQApeV-u2gm43aC1uP76xAC1m6vCylstaN1gpfBmre_5IyTH`** +- **BTC: `1JpqcziZZuqv3QQJhZGNGBVdCBrGgkL6cT`** +- **XMR: `4ABHQZ3yJZkBnLoqiKvb3f8eqUnX4iMPb6wdant5ZLGQELctcerceSGEfJnoCk6nnyRZm73wrwSgvZ2WmjYLng6R7sR67nq`** +- **SOL/USDT/USDC: `3x5NuXHzB5APG6vRinPZcsUv5ukWUY1tBGRSJiEJWtZa`** +- **ETH/USDT/USDC: `0xDc3Fe44F0f25D13CACb1C4896CD0D321df3146Ee`** +- **Project X NFT: https://opensea.io/item/ethereum/0x5ee362866001613093361eb8569d59c4141b76d1/1** +- **VLESS NFT: https://opensea.io/collection/vless** +- **REALITY NFT: https://opensea.io/item/ethereum/0x5ee362866001613093361eb8569d59c4141b76d1/2** +- **Related links: [VLESS Post-Quantum Encryption](https://github.com/XTLS/Xray-core/pull/5067), [XHTTP: Beyond REALITY](https://github.com/XTLS/Xray-core/discussions/4113), [Announcement of NFTs by Project X](https://github.com/XTLS/Xray-core/discussions/3633)** + +## License + +[Mozilla Public License Version 2.0](https://github.com/XTLS/Xray-core/blob/main/LICENSE) + +## Documentation + +[Project X Official Website](https://xtls.github.io) + +## Telegram + +[Project X](https://t.me/projectXray) + +[Project X Channel](https://t.me/projectXtls) + +[Project VLESS](https://t.me/projectVless) (Русский) + +[Project XHTTP](https://t.me/projectXhttp) (Persian) + +## Installation + +- Linux Script + - [XTLS/Xray-install](https://github.com/XTLS/Xray-install) (**Official**) + - [tempest](https://github.com/team-cloudchaser/tempest) (supports [`systemd`](https://systemd.io) and [OpenRC](https://github.com/OpenRC/openrc); Linux-only) +- Docker + - [ghcr.io/xtls/xray-core](https://ghcr.io/xtls/xray-core) (**Official**) + - [teddysun/xray](https://hub.docker.com/r/teddysun/xray) + - [wulabing/xray_docker](https://github.com/wulabing/xray_docker) +- Web Panel + - [Remnawave](https://github.com/remnawave/panel) + - [3X-UI](https://github.com/MHSanaei/3x-ui) + - [PasarGuard](https://github.com/PasarGuard/panel) + - [Xray-UI](https://github.com/qist/xray-ui) + - [X-Panel](https://github.com/xeefei/X-Panel) + - [Marzban](https://github.com/Gozargah/Marzban) + - [Hiddify](https://github.com/hiddify/Hiddify-Manager) + - [TX-UI](https://github.com/AghayeCoder/tx-ui) + - [CELERITY](https://github.com/ClickDevTech/CELERITY-panel) +- One Click + - [Xray-REALITY](https://github.com/zxcvos/Xray-script), [xray-reality](https://github.com/sajjaddg/xray-reality), [reality-ezpz](https://github.com/aleskxyz/reality-ezpz) + - [Xray_bash_onekey](https://github.com/hello-yunshu/Xray_bash_onekey), [XTool](https://github.com/LordPenguin666/XTool), [VPainLess](https://github.com/vpainless/vpainless) + - [v2ray-agent](https://github.com/mack-a/v2ray-agent), [Xray_onekey](https://github.com/wulabing/Xray_onekey), [ProxySU](https://github.com/proxysu/ProxySU) +- Magisk + - [NetProxy-Magisk](https://github.com/Fanju6/NetProxy-Magisk) + - [Xray4Magisk](https://github.com/Asterisk4Magisk/Xray4Magisk) + - [Xray_For_Magisk](https://github.com/E7KMbb/Xray_For_Magisk) +- Homebrew + - `brew install xray` + +## Usage + +- Example + - [VLESS-XTLS-uTLS-REALITY](https://github.com/XTLS/REALITY#readme) + - [VLESS-TCP-XTLS-Vision](https://github.com/XTLS/Xray-examples/tree/main/VLESS-TCP-XTLS-Vision) + - [All-in-One-fallbacks-Nginx](https://github.com/XTLS/Xray-examples/tree/main/All-in-One-fallbacks-Nginx) +- Xray-examples + - [XTLS/Xray-examples](https://github.com/XTLS/Xray-examples) + - [chika0801/Xray-examples](https://github.com/chika0801/Xray-examples) + - [lxhao61/integrated-examples](https://github.com/lxhao61/integrated-examples) +- Tutorial + - [XTLS Vision](https://github.com/chika0801/Xray-install) + - [REALITY (English)](https://cscot.pages.dev/2023/03/02/Xray-REALITY-tutorial/) + - [XTLS-Iran-Reality (English)](https://github.com/SasukeFreestyle/XTLS-Iran-Reality) + - [Xray REALITY with 'steal oneself' (English)](https://computerscot.github.io/vless-xtls-utls-reality-steal-oneself.html) + - [Xray with WireGuard inbound (English)](https://g800.pages.dev/wireguard) + +## GUI Clients + +- OpenWrt + - [PassWall](https://github.com/Openwrt-Passwall/openwrt-passwall), [PassWall 2](https://github.com/Openwrt-Passwall/openwrt-passwall2) + - [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)) +- Asuswrt-Merlin + - [XRAYUI](https://github.com/DanielLavrushin/asuswrt-merlin-xrayui) + - [fancyss](https://github.com/hq450/fancyss) +- Windows + - [v2rayN](https://github.com/2dust/v2rayN) + - [Furious](https://github.com/LorenEteval/Furious) + - [Invisible Man - Xray](https://github.com/InvisibleManVPN/InvisibleMan-XRayClient) + - [AnyPortal](https://github.com/AnyPortal/AnyPortal) + - [GenyConnect](https://github.com/genyleap/GenyConnect) +- Android + - [v2rayNG](https://github.com/2dust/v2rayNG) + - [X-flutter](https://github.com/XTLS/X-flutter) + - [SaeedDev94/Xray](https://github.com/SaeedDev94/Xray) + - [SimpleXray](https://github.com/lhear/SimpleXray) + - [XrayFA](https://github.com/Q7DF1/XrayFA) + - [AnyPortal](https://github.com/AnyPortal/AnyPortal) + - [NetProxy-Magisk](https://github.com/Fanju6/NetProxy-Magisk) +- iOS & macOS arm64 & tvOS + - [Happ](https://apps.apple.com/app/happ-proxy-utility/id6504287215) | [Happ RU](https://apps.apple.com/ru/app/happ-proxy-utility-plus/id6746188973) | [Happ tvOS](https://apps.apple.com/us/app/happ-proxy-utility-for-tv/id6748297274) + - [Streisand](https://apps.apple.com/app/streisand/id6450534064) + - [OneXray](https://github.com/OneXray/OneXray) + - [INCY](https://apps.apple.com/en/app/incy/id6756943388) +- macOS arm64 & x64 + - [Happ](https://apps.apple.com/app/happ-proxy-utility/id6504287215) | [Happ RU](https://apps.apple.com/ru/app/happ-proxy-utility-plus/id6746188973) + - [V2rayU](https://github.com/yanue/V2rayU) + - [V2RayXS](https://github.com/tzmax/V2RayXS) + - [Furious](https://github.com/LorenEteval/Furious) + - [OneXray](https://github.com/OneXray/OneXray) + - [GoXRay](https://github.com/goxray/desktop) + - [AnyPortal](https://github.com/AnyPortal/AnyPortal) + - [v2rayN](https://github.com/2dust/v2rayN) + - [GenyConnect](https://github.com/genyleap/GenyConnect) + - [INCY](https://apps.apple.com/en/app/incy/id6756943388) +- Linux + - [v2rayA](https://github.com/v2rayA/v2rayA) + - [Furious](https://github.com/LorenEteval/Furious) + - [GorzRay](https://github.com/ketetefid/GorzRay) + - [GoXRay](https://github.com/goxray/desktop) + - [AnyPortal](https://github.com/AnyPortal/AnyPortal) + - [v2rayN](https://github.com/2dust/v2rayN) + - [GenyConnect](https://github.com/genyleap/GenyConnect) + +## Others that support VLESS, XTLS, REALITY, XUDP, PLUX... + +- iOS & macOS arm64 & tvOS + - [Shadowrocket](https://apps.apple.com/app/shadowrocket/id932747118) + - [Loon](https://apps.apple.com/us/app/loon/id1373567447) + - [Egern](https://apps.apple.com/us/app/egern/id1616105820) + - [Quantumult X](https://apps.apple.com/us/app/quantumult-x/id1443988620) +- Xray Tools + - [xray-knife](https://github.com/lilendian0x00/xray-knife) + - [xray-checker](https://github.com/kutovoys/xray-checker) +- Xray Wrapper + - [XTLS/libXray](https://github.com/XTLS/libXray) + - [xtls-sdk](https://github.com/remnawave/xtls-sdk) + - [xtlsapi](https://github.com/hiddify/xtlsapi) + - [AndroidLibXrayLite](https://github.com/2dust/AndroidLibXrayLite) + - [Xray-core-python](https://github.com/LorenEteval/Xray-core-python) + - [xray-api](https://github.com/XVGuardian/xray-api) +- [XrayR](https://github.com/XrayR-project/XrayR) + - [XrayR-release](https://github.com/XrayR-project/XrayR-release) + - [XrayR-V2Board](https://github.com/missuo/XrayR-V2Board) +- Cores + - [Amnezia VPN](https://github.com/amnezia-vpn) + - [mihomo](https://github.com/MetaCubeX/mihomo) + - [sing-box](https://github.com/SagerNet/sing-box) + +## Contributing + +[Code of Conduct](https://github.com/XTLS/Xray-core/blob/main/CODE_OF_CONDUCT.md) + +[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/XTLS/Xray-core) + +## Credits + +- [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). + +## One-line Compilation + +### Windows (PowerShell) + +```powershell +$env:CGO_ENABLED=0 +go build -o xray.exe -trimpath -buildvcs=false -ldflags="-s -w -buildid=" -v ./main +``` + +### Linux / macOS + +```bash +CGO_ENABLED=0 go build -o xray -trimpath -buildvcs=false -ldflags="-s -w -buildid=" -v ./main +``` + +### Reproducible Releases + +Make sure that you are using the same Go version, and remember to set the git commit id (7 bytes): + +```bash +CGO_ENABLED=0 go build -o xray -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags="-X github.com/xtls/xray-core/core.build=REPLACE -s -w -buildid=" -v ./main +``` + +If you are compiling a 32-bit MIPS/MIPSLE target, use this command instead: + +```bash +CGO_ENABLED=0 go build -o xray -trimpath -buildvcs=false -gcflags="-l=4" -ldflags="-X github.com/xtls/xray-core/core.build=REPLACE -s -w -buildid=" -v ./main +``` + +## Stargazers over time + +[![Stargazers over time](https://starchart.cc/XTLS/Xray-core.svg)](https://starchart.cc/XTLS/Xray-core) diff --git a/subproject/Xray-core-main/SECURITY.md b/subproject/Xray-core-main/SECURITY.md new file mode 100644 index 00000000..c83f60bf --- /dev/null +++ b/subproject/Xray-core-main/SECURITY.md @@ -0,0 +1,5 @@ +# Security Policy + +If you found an issue related to security vulnerability or protocol-identification problem, please report it to us via "[Report a vulnerability](https://github.com/XTLS/Xray-core/security/advisories/new)" privately, instead of publish it publicly before we release the fixed version. + +Thanks for your contribution to the FREE Internet! diff --git a/subproject/Xray-core-main/app/app.go b/subproject/Xray-core-main/app/app.go new file mode 100644 index 00000000..decf348a --- /dev/null +++ b/subproject/Xray-core-main/app/app.go @@ -0,0 +1,2 @@ +// Package app contains feature implementations of Xray. The features may be enabled during runtime. +package app diff --git a/subproject/Xray-core-main/app/commander/commander.go b/subproject/Xray-core-main/app/commander/commander.go new file mode 100644 index 00000000..9ea71e6e --- /dev/null +++ b/subproject/Xray-core-main/app/commander/commander.go @@ -0,0 +1,121 @@ +package commander + +import ( + "context" + "net" + "sync" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/signal/done" + core "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/features/outbound" + "google.golang.org/grpc" +) + +// Commander is a Xray feature that provides gRPC methods to external clients. +type Commander struct { + sync.Mutex + server *grpc.Server + services []Service + ohm outbound.Manager + tag string + listen string +} + +// NewCommander creates a new Commander based on the given config. +func NewCommander(ctx context.Context, config *Config) (*Commander, error) { + c := &Commander{ + tag: config.Tag, + listen: config.Listen, + } + + common.Must(core.RequireFeatures(ctx, func(om outbound.Manager) { + c.ohm = om + })) + + for _, rawConfig := range config.Service { + config, err := rawConfig.GetInstance() + if err != nil { + return nil, err + } + rawService, err := common.CreateObject(ctx, config) + if err != nil { + return nil, err + } + service, ok := rawService.(Service) + if !ok { + return nil, errors.New("not a Service.") + } + c.services = append(c.services, service) + } + + return c, nil +} + +// Type implements common.HasType. +func (c *Commander) Type() interface{} { + return (*Commander)(nil) +} + +// Start implements common.Runnable. +func (c *Commander) Start() error { + c.Lock() + c.server = grpc.NewServer() + for _, service := range c.services { + service.Register(c.server) + } + c.Unlock() + + var listen = func(listener net.Listener) { + if err := c.server.Serve(listener); err != nil { + errors.LogErrorInner(context.Background(), err, "failed to start grpc server") + } + } + + if len(c.listen) > 0 { + if l, err := net.Listen("tcp", c.listen); err != nil { + errors.LogErrorInner(context.Background(), err, "API server failed to listen on ", c.listen) + return err + } else { + errors.LogInfo(context.Background(), "API server listening on ", l.Addr()) + go listen(l) + } + return nil + } + + listener := &OutboundListener{ + buffer: make(chan net.Conn, 4), + done: done.New(), + } + + go listen(listener) + + if err := c.ohm.RemoveHandler(context.Background(), c.tag); err != nil { + errors.LogInfoInner(context.Background(), err, "failed to remove existing handler") + } + + return c.ohm.AddHandler(context.Background(), &Outbound{ + tag: c.tag, + listener: listener, + }) +} + +// Close implements common.Closable. +func (c *Commander) Close() error { + c.Lock() + defer c.Unlock() + + if c.server != nil { + c.server.Stop() + c.server = nil + } + + return nil +} + +func init() { + common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, cfg interface{}) (interface{}, error) { + return NewCommander(ctx, cfg.(*Config)) + })) +} diff --git a/subproject/Xray-core-main/app/commander/config.pb.go b/subproject/Xray-core-main/app/commander/config.pb.go new file mode 100644 index 00000000..b91de6d4 --- /dev/null +++ b/subproject/Xray-core-main/app/commander/config.pb.go @@ -0,0 +1,188 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: app/commander/config.proto + +package commander + +import ( + serial "github.com/xtls/xray-core/common/serial" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Config is the settings for Commander. +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Tag of the outbound handler that handles grpc connections. + Tag string `protobuf:"bytes,1,opt,name=tag,proto3" json:"tag,omitempty"` + // Network address of commander grpc service. + Listen string `protobuf:"bytes,3,opt,name=listen,proto3" json:"listen,omitempty"` + // Services that supported by this server. All services must implement Service + // interface. + Service []*serial.TypedMessage `protobuf:"bytes,2,rep,name=service,proto3" json:"service,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_app_commander_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_app_commander_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_app_commander_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Config) GetTag() string { + if x != nil { + return x.Tag + } + return "" +} + +func (x *Config) GetListen() string { + if x != nil { + return x.Listen + } + return "" +} + +func (x *Config) GetService() []*serial.TypedMessage { + if x != nil { + return x.Service + } + return nil +} + +// ReflectionConfig is the placeholder config for ReflectionService. +type ReflectionConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReflectionConfig) Reset() { + *x = ReflectionConfig{} + mi := &file_app_commander_config_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReflectionConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReflectionConfig) ProtoMessage() {} + +func (x *ReflectionConfig) ProtoReflect() protoreflect.Message { + mi := &file_app_commander_config_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ReflectionConfig.ProtoReflect.Descriptor instead. +func (*ReflectionConfig) Descriptor() ([]byte, []int) { + return file_app_commander_config_proto_rawDescGZIP(), []int{1} +} + +var File_app_commander_config_proto protoreflect.FileDescriptor + +const file_app_commander_config_proto_rawDesc = "" + + "\n" + + "\x1aapp/commander/config.proto\x12\x12xray.app.commander\x1a!common/serial/typed_message.proto\"n\n" + + "\x06Config\x12\x10\n" + + "\x03tag\x18\x01 \x01(\tR\x03tag\x12\x16\n" + + "\x06listen\x18\x03 \x01(\tR\x06listen\x12:\n" + + "\aservice\x18\x02 \x03(\v2 .xray.common.serial.TypedMessageR\aservice\"\x12\n" + + "\x10ReflectionConfigBX\n" + + "\x16com.xray.app.commanderP\x01Z'github.com/xtls/xray-core/app/commander\xaa\x02\x12Xray.App.Commanderb\x06proto3" + +var ( + file_app_commander_config_proto_rawDescOnce sync.Once + file_app_commander_config_proto_rawDescData []byte +) + +func file_app_commander_config_proto_rawDescGZIP() []byte { + file_app_commander_config_proto_rawDescOnce.Do(func() { + file_app_commander_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_app_commander_config_proto_rawDesc), len(file_app_commander_config_proto_rawDesc))) + }) + return file_app_commander_config_proto_rawDescData +} + +var file_app_commander_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_app_commander_config_proto_goTypes = []any{ + (*Config)(nil), // 0: xray.app.commander.Config + (*ReflectionConfig)(nil), // 1: xray.app.commander.ReflectionConfig + (*serial.TypedMessage)(nil), // 2: xray.common.serial.TypedMessage +} +var file_app_commander_config_proto_depIdxs = []int32{ + 2, // 0: xray.app.commander.Config.service:type_name -> xray.common.serial.TypedMessage + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_app_commander_config_proto_init() } +func file_app_commander_config_proto_init() { + if File_app_commander_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_app_commander_config_proto_rawDesc), len(file_app_commander_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_app_commander_config_proto_goTypes, + DependencyIndexes: file_app_commander_config_proto_depIdxs, + MessageInfos: file_app_commander_config_proto_msgTypes, + }.Build() + File_app_commander_config_proto = out.File + file_app_commander_config_proto_goTypes = nil + file_app_commander_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/app/commander/config.proto b/subproject/Xray-core-main/app/commander/config.proto new file mode 100644 index 00000000..688949d9 --- /dev/null +++ b/subproject/Xray-core-main/app/commander/config.proto @@ -0,0 +1,25 @@ +syntax = "proto3"; + +package xray.app.commander; +option csharp_namespace = "Xray.App.Commander"; +option go_package = "github.com/xtls/xray-core/app/commander"; +option java_package = "com.xray.app.commander"; +option java_multiple_files = true; + +import "common/serial/typed_message.proto"; + +// Config is the settings for Commander. +message Config { + // Tag of the outbound handler that handles grpc connections. + string tag = 1; + + // Network address of commander grpc service. + string listen = 3; + + // Services that supported by this server. All services must implement Service + // interface. + repeated xray.common.serial.TypedMessage service = 2; +} + +// ReflectionConfig is the placeholder config for ReflectionService. +message ReflectionConfig {} diff --git a/subproject/Xray-core-main/app/commander/outbound.go b/subproject/Xray-core-main/app/commander/outbound.go new file mode 100644 index 00000000..7f520d74 --- /dev/null +++ b/subproject/Xray-core-main/app/commander/outbound.go @@ -0,0 +1,121 @@ +package commander + +import ( + "context" + "sync" + + "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/net/cnc" + "github.com/xtls/xray-core/common/serial" + "github.com/xtls/xray-core/common/signal/done" + "github.com/xtls/xray-core/transport" +) + +// OutboundListener is a net.Listener for listening gRPC connections. +type OutboundListener struct { + buffer chan net.Conn + done *done.Instance +} + +func (l *OutboundListener) add(conn net.Conn) { + select { + case l.buffer <- conn: + case <-l.done.Wait(): + conn.Close() + default: + conn.Close() + } +} + +// Accept implements net.Listener. +func (l *OutboundListener) Accept() (net.Conn, error) { + select { + case <-l.done.Wait(): + return nil, errors.New("listen closed") + case c := <-l.buffer: + return c, nil + } +} + +// Close implements net.Listener. +func (l *OutboundListener) Close() error { + common.Must(l.done.Close()) +L: + for { + select { + case c := <-l.buffer: + c.Close() + default: + break L + } + } + return nil +} + +// Addr implements net.Listener. +func (l *OutboundListener) Addr() net.Addr { + return &net.TCPAddr{ + IP: net.IP{0, 0, 0, 0}, + Port: 0, + } +} + +// Outbound is a outbound.Handler that handles gRPC connections. +type Outbound struct { + tag string + listener *OutboundListener + access sync.RWMutex + closed bool +} + +// Dispatch implements outbound.Handler. +func (co *Outbound) Dispatch(ctx context.Context, link *transport.Link) { + co.access.RLock() + + if co.closed { + common.Interrupt(link.Reader) + common.Interrupt(link.Writer) + co.access.RUnlock() + return + } + + closeSignal := done.New() + c := cnc.NewConnection(cnc.ConnectionInputMulti(link.Writer), cnc.ConnectionOutputMulti(link.Reader), cnc.ConnectionOnClose(closeSignal)) + co.listener.add(c) + co.access.RUnlock() + <-closeSignal.Wait() +} + +// Tag implements outbound.Handler. +func (co *Outbound) Tag() string { + return co.tag +} + +// Start implements common.Runnable. +func (co *Outbound) Start() error { + co.access.Lock() + co.closed = false + co.access.Unlock() + return nil +} + +// Close implements common.Closable. +func (co *Outbound) Close() error { + co.access.Lock() + defer co.access.Unlock() + + co.closed = true + return co.listener.Close() +} + +// SenderSettings implements outbound.Handler. +func (co *Outbound) SenderSettings() *serial.TypedMessage { + return nil +} + +// ProxySettings implements outbound.Handler. +func (co *Outbound) ProxySettings() *serial.TypedMessage { + return nil +} diff --git a/subproject/Xray-core-main/app/commander/service.go b/subproject/Xray-core-main/app/commander/service.go new file mode 100644 index 00000000..510f321f --- /dev/null +++ b/subproject/Xray-core-main/app/commander/service.go @@ -0,0 +1,27 @@ +package commander + +import ( + "context" + + "github.com/xtls/xray-core/common" + "google.golang.org/grpc" + "google.golang.org/grpc/reflection" +) + +// Service is a Commander service. +type Service interface { + // Register registers the service itself to a gRPC server. + Register(*grpc.Server) +} + +type reflectionService struct{} + +func (r reflectionService) Register(s *grpc.Server) { + reflection.Register(s) +} + +func init() { + common.Must(common.RegisterConfig((*ReflectionConfig)(nil), func(ctx context.Context, cfg interface{}) (interface{}, error) { + return reflectionService{}, nil + })) +} diff --git a/subproject/Xray-core-main/app/dispatcher/config.pb.go b/subproject/Xray-core-main/app/dispatcher/config.pb.go new file mode 100644 index 00000000..c01aa845 --- /dev/null +++ b/subproject/Xray-core-main/app/dispatcher/config.pb.go @@ -0,0 +1,162 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: app/dispatcher/config.proto + +package dispatcher + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type SessionConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SessionConfig) Reset() { + *x = SessionConfig{} + mi := &file_app_dispatcher_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SessionConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SessionConfig) ProtoMessage() {} + +func (x *SessionConfig) ProtoReflect() protoreflect.Message { + mi := &file_app_dispatcher_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SessionConfig.ProtoReflect.Descriptor instead. +func (*SessionConfig) Descriptor() ([]byte, []int) { + return file_app_dispatcher_config_proto_rawDescGZIP(), []int{0} +} + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + Settings *SessionConfig `protobuf:"bytes,1,opt,name=settings,proto3" json:"settings,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_app_dispatcher_config_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_app_dispatcher_config_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_app_dispatcher_config_proto_rawDescGZIP(), []int{1} +} + +func (x *Config) GetSettings() *SessionConfig { + if x != nil { + return x.Settings + } + return nil +} + +var File_app_dispatcher_config_proto protoreflect.FileDescriptor + +const file_app_dispatcher_config_proto_rawDesc = "" + + "\n" + + "\x1bapp/dispatcher/config.proto\x12\x13xray.app.dispatcher\"\x15\n" + + "\rSessionConfigJ\x04\b\x01\x10\x02\"H\n" + + "\x06Config\x12>\n" + + "\bsettings\x18\x01 \x01(\v2\".xray.app.dispatcher.SessionConfigR\bsettingsB[\n" + + "\x17com.xray.app.dispatcherP\x01Z(github.com/xtls/xray-core/app/dispatcher\xaa\x02\x13Xray.App.Dispatcherb\x06proto3" + +var ( + file_app_dispatcher_config_proto_rawDescOnce sync.Once + file_app_dispatcher_config_proto_rawDescData []byte +) + +func file_app_dispatcher_config_proto_rawDescGZIP() []byte { + file_app_dispatcher_config_proto_rawDescOnce.Do(func() { + file_app_dispatcher_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_app_dispatcher_config_proto_rawDesc), len(file_app_dispatcher_config_proto_rawDesc))) + }) + return file_app_dispatcher_config_proto_rawDescData +} + +var file_app_dispatcher_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_app_dispatcher_config_proto_goTypes = []any{ + (*SessionConfig)(nil), // 0: xray.app.dispatcher.SessionConfig + (*Config)(nil), // 1: xray.app.dispatcher.Config +} +var file_app_dispatcher_config_proto_depIdxs = []int32{ + 0, // 0: xray.app.dispatcher.Config.settings:type_name -> xray.app.dispatcher.SessionConfig + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_app_dispatcher_config_proto_init() } +func file_app_dispatcher_config_proto_init() { + if File_app_dispatcher_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_app_dispatcher_config_proto_rawDesc), len(file_app_dispatcher_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_app_dispatcher_config_proto_goTypes, + DependencyIndexes: file_app_dispatcher_config_proto_depIdxs, + MessageInfos: file_app_dispatcher_config_proto_msgTypes, + }.Build() + File_app_dispatcher_config_proto = out.File + file_app_dispatcher_config_proto_goTypes = nil + file_app_dispatcher_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/app/dispatcher/config.proto b/subproject/Xray-core-main/app/dispatcher/config.proto new file mode 100644 index 00000000..e6794238 --- /dev/null +++ b/subproject/Xray-core-main/app/dispatcher/config.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; + +package xray.app.dispatcher; +option csharp_namespace = "Xray.App.Dispatcher"; +option go_package = "github.com/xtls/xray-core/app/dispatcher"; +option java_package = "com.xray.app.dispatcher"; +option java_multiple_files = true; + +message SessionConfig { + reserved 1; +} + +message Config { + SessionConfig settings = 1; +} diff --git a/subproject/Xray-core-main/app/dispatcher/default.go b/subproject/Xray-core-main/app/dispatcher/default.go new file mode 100644 index 00000000..e6f89657 --- /dev/null +++ b/subproject/Xray-core-main/app/dispatcher/default.go @@ -0,0 +1,519 @@ +package dispatcher + +import ( + "context" + "regexp" + "strings" + "sync" + "time" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/log" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/features/dns" + "github.com/xtls/xray-core/features/outbound" + "github.com/xtls/xray-core/features/policy" + "github.com/xtls/xray-core/features/routing" + routing_session "github.com/xtls/xray-core/features/routing/session" + "github.com/xtls/xray-core/features/stats" + "github.com/xtls/xray-core/transport" + "github.com/xtls/xray-core/transport/pipe" +) + +var errSniffingTimeout = errors.New("timeout on sniffing") + +type cachedReader struct { + sync.Mutex + reader buf.TimeoutReader // *pipe.Reader or *buf.TimeoutWrapperReader + cache buf.MultiBuffer +} + +func (r *cachedReader) Cache(b *buf.Buffer, deadline time.Duration) error { + mb, err := r.reader.ReadMultiBufferTimeout(deadline) + if err != nil { + return err + } + r.Lock() + if !mb.IsEmpty() { + r.cache, _ = buf.MergeMulti(r.cache, mb) + } + b.Clear() + rawBytes := b.Extend(min(r.cache.Len(), b.Cap())) + n := r.cache.Copy(rawBytes) + b.Resize(0, int32(n)) + r.Unlock() + return nil +} + +func (r *cachedReader) readInternal() buf.MultiBuffer { + r.Lock() + defer r.Unlock() + + if r.cache != nil && !r.cache.IsEmpty() { + mb := r.cache + r.cache = nil + return mb + } + + return nil +} + +func (r *cachedReader) ReadMultiBuffer() (buf.MultiBuffer, error) { + mb := r.readInternal() + if mb != nil { + return mb, nil + } + + return r.reader.ReadMultiBuffer() +} + +func (r *cachedReader) ReadMultiBufferTimeout(timeout time.Duration) (buf.MultiBuffer, error) { + mb := r.readInternal() + if mb != nil { + return mb, nil + } + + return r.reader.ReadMultiBufferTimeout(timeout) +} + +func (r *cachedReader) Interrupt() { + r.Lock() + if r.cache != nil { + r.cache = buf.ReleaseMulti(r.cache) + } + r.Unlock() + if p, ok := r.reader.(*pipe.Reader); ok { + p.Interrupt() + } +} + +// DefaultDispatcher is a default implementation of Dispatcher. +type DefaultDispatcher struct { + ohm outbound.Manager + router routing.Router + policy policy.Manager + stats stats.Manager + fdns dns.FakeDNSEngine +} + +func init() { + common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + d := new(DefaultDispatcher) + if err := core.RequireFeatures(ctx, func(om outbound.Manager, router routing.Router, pm policy.Manager, sm stats.Manager, dc dns.Client) error { + core.OptionalFeatures(ctx, func(fdns dns.FakeDNSEngine) { + d.fdns = fdns + }) + return d.Init(config.(*Config), om, router, pm, sm) + }); err != nil { + return nil, err + } + return d, nil + })) +} + +// Init initializes DefaultDispatcher. +func (d *DefaultDispatcher) Init(config *Config, om outbound.Manager, router routing.Router, pm policy.Manager, sm stats.Manager) error { + d.ohm = om + d.router = router + d.policy = pm + d.stats = sm + return nil +} + +// Type implements common.HasType. +func (*DefaultDispatcher) Type() interface{} { + return routing.DispatcherType() +} + +// Start implements common.Runnable. +func (*DefaultDispatcher) Start() error { + return nil +} + +// Close implements common.Closable. +func (*DefaultDispatcher) Close() error { return nil } + +func (d *DefaultDispatcher) getLink(ctx context.Context) (*transport.Link, *transport.Link) { + opt := pipe.OptionsFromContext(ctx) + uplinkReader, uplinkWriter := pipe.New(opt...) + downlinkReader, downlinkWriter := pipe.New(opt...) + + inboundLink := &transport.Link{ + Reader: downlinkReader, + Writer: uplinkWriter, + } + + outboundLink := &transport.Link{ + Reader: uplinkReader, + Writer: downlinkWriter, + } + + sessionInbound := session.InboundFromContext(ctx) + var user *protocol.MemoryUser + if sessionInbound != nil { + user = sessionInbound.User + } + + if user != nil && len(user.Email) > 0 { + p := d.policy.ForLevel(user.Level) + if p.Stats.UserUplink { + name := "user>>>" + user.Email + ">>>traffic>>>uplink" + if c, _ := stats.GetOrRegisterCounter(d.stats, name); c != nil { + inboundLink.Writer = &SizeStatWriter{ + Counter: c, + Writer: inboundLink.Writer, + } + } + } + if p.Stats.UserDownlink { + name := "user>>>" + user.Email + ">>>traffic>>>downlink" + if c, _ := stats.GetOrRegisterCounter(d.stats, name); c != nil { + outboundLink.Writer = &SizeStatWriter{ + Counter: c, + Writer: outboundLink.Writer, + } + } + } + + if p.Stats.UserOnline { + name := "user>>>" + user.Email + ">>>online" + if om, _ := stats.GetOrRegisterOnlineMap(d.stats, name); om != nil { + userIP := sessionInbound.Source.Address.String() + om.AddIP(userIP) + context.AfterFunc(ctx, func() { om.RemoveIP(userIP) }) + } + } + } + + return inboundLink, outboundLink +} + +func WrapLink(ctx context.Context, policyManager policy.Manager, statsManager stats.Manager, link *transport.Link) *transport.Link { + sessionInbound := session.InboundFromContext(ctx) + var user *protocol.MemoryUser + if sessionInbound != nil { + user = sessionInbound.User + } + + link.Reader = &buf.TimeoutWrapperReader{Reader: link.Reader} + + if user != nil && len(user.Email) > 0 { + p := policyManager.ForLevel(user.Level) + if p.Stats.UserUplink { + name := "user>>>" + user.Email + ">>>traffic>>>uplink" + if c, _ := stats.GetOrRegisterCounter(statsManager, name); c != nil { + link.Reader.(*buf.TimeoutWrapperReader).Counter = c + } + } + if p.Stats.UserDownlink { + name := "user>>>" + user.Email + ">>>traffic>>>downlink" + if c, _ := stats.GetOrRegisterCounter(statsManager, name); c != nil { + link.Writer = &SizeStatWriter{ + Counter: c, + Writer: link.Writer, + } + } + } + if p.Stats.UserOnline { + name := "user>>>" + user.Email + ">>>online" + if om, _ := stats.GetOrRegisterOnlineMap(statsManager, name); om != nil { + userIP := sessionInbound.Source.Address.String() + om.AddIP(userIP) + context.AfterFunc(ctx, func() { om.RemoveIP(userIP) }) + } + } + } + + return link +} + +func (d *DefaultDispatcher) shouldOverride(ctx context.Context, result SniffResult, request session.SniffingRequest, destination net.Destination) bool { + domain := result.Domain() + if domain == "" { + return false + } + for _, d := range request.ExcludeForDomain { + if strings.HasPrefix(d, "regexp:") { + pattern := d[7:] + re, err := regexp.Compile(pattern) + if err != nil { + errors.LogInfo(ctx, "Unable to compile regex") + continue + } + if re.MatchString(domain) { + return false + } + } else { + if strings.ToLower(domain) == d { + return false + } + } + } + protocolString := result.Protocol() + if resComp, ok := result.(SnifferResultComposite); ok { + protocolString = resComp.ProtocolForDomainResult() + } + for _, p := range request.OverrideDestinationForProtocol { + if strings.HasPrefix(protocolString, p) || strings.HasPrefix(p, protocolString) { + return true + } + if fkr0, ok := d.fdns.(dns.FakeDNSEngineRev0); ok && protocolString != "bittorrent" && p == "fakedns" && + fkr0.IsIPInIPPool(destination.Address) { + errors.LogInfo(ctx, "Using sniffer ", protocolString, " since the fake DNS missed") + return true + } + if resultSubset, ok := result.(SnifferIsProtoSubsetOf); ok { + if resultSubset.IsProtoSubsetOf(p) { + return true + } + } + } + + return false +} + +// Dispatch implements routing.Dispatcher. +func (d *DefaultDispatcher) Dispatch(ctx context.Context, destination net.Destination) (*transport.Link, error) { + if !destination.IsValid() { + panic("Dispatcher: Invalid destination.") + } + outbounds := session.OutboundsFromContext(ctx) + if len(outbounds) == 0 { + outbounds = []*session.Outbound{{}} + ctx = session.ContextWithOutbounds(ctx, outbounds) + } + ob := outbounds[len(outbounds)-1] + ob.OriginalTarget = destination + ob.Target = destination + content := session.ContentFromContext(ctx) + if content == nil { + content = new(session.Content) + ctx = session.ContextWithContent(ctx, content) + } + + sniffingRequest := content.SniffingRequest + inbound, outbound := d.getLink(ctx) + if !sniffingRequest.Enabled { + go d.routedDispatch(ctx, outbound, destination) + } else { + go func() { + cReader := &cachedReader{ + reader: outbound.Reader.(*pipe.Reader), + } + outbound.Reader = cReader + result, err := sniffer(ctx, cReader, sniffingRequest.MetadataOnly, destination.Network) + if err == nil { + content.Protocol = result.Protocol() + } + if err == nil && d.shouldOverride(ctx, result, sniffingRequest, destination) { + domain := result.Domain() + errors.LogInfo(ctx, "sniffed domain: ", domain) + destination.Address = net.ParseAddress(domain) + protocol := result.Protocol() + if resComp, ok := result.(SnifferResultComposite); ok { + protocol = resComp.ProtocolForDomainResult() + } + isFakeIP := false + if fkr0, ok := d.fdns.(dns.FakeDNSEngineRev0); ok && fkr0.IsIPInIPPool(ob.Target.Address) { + isFakeIP = true + } + if sniffingRequest.RouteOnly && protocol != "fakedns" && protocol != "fakedns+others" && !isFakeIP { + ob.RouteTarget = destination + } else { + ob.Target = destination + } + } + d.routedDispatch(ctx, outbound, destination) + }() + } + return inbound, nil +} + +// DispatchLink implements routing.Dispatcher. +func (d *DefaultDispatcher) DispatchLink(ctx context.Context, destination net.Destination, outbound *transport.Link) error { + if !destination.IsValid() { + return errors.New("Dispatcher: Invalid destination.") + } + outbounds := session.OutboundsFromContext(ctx) + if len(outbounds) == 0 { + outbounds = []*session.Outbound{{}} + ctx = session.ContextWithOutbounds(ctx, outbounds) + } + ob := outbounds[len(outbounds)-1] + ob.OriginalTarget = destination + ob.Target = destination + content := session.ContentFromContext(ctx) + if content == nil { + content = new(session.Content) + ctx = session.ContextWithContent(ctx, content) + } + outbound = WrapLink(ctx, d.policy, d.stats, outbound) + sniffingRequest := content.SniffingRequest + if !sniffingRequest.Enabled { + d.routedDispatch(ctx, outbound, destination) + } else { + cReader := &cachedReader{ + reader: outbound.Reader.(buf.TimeoutReader), + } + outbound.Reader = cReader + result, err := sniffer(ctx, cReader, sniffingRequest.MetadataOnly, destination.Network) + if err == nil { + content.Protocol = result.Protocol() + } + if err == nil && d.shouldOverride(ctx, result, sniffingRequest, destination) { + domain := result.Domain() + errors.LogInfo(ctx, "sniffed domain: ", domain) + destination.Address = net.ParseAddress(domain) + protocol := result.Protocol() + if resComp, ok := result.(SnifferResultComposite); ok { + protocol = resComp.ProtocolForDomainResult() + } + isFakeIP := false + if fkr0, ok := d.fdns.(dns.FakeDNSEngineRev0); ok && fkr0.IsIPInIPPool(ob.Target.Address) { + isFakeIP = true + } + if sniffingRequest.RouteOnly && protocol != "fakedns" && protocol != "fakedns+others" && !isFakeIP { + ob.RouteTarget = destination + } else { + ob.Target = destination + } + } + d.routedDispatch(ctx, outbound, destination) + } + + return nil +} + +func sniffer(ctx context.Context, cReader *cachedReader, metadataOnly bool, network net.Network) (SniffResult, error) { + payload := buf.NewWithSize(32767) + defer payload.Release() + + sniffer := NewSniffer(ctx) + + metaresult, metadataErr := sniffer.SniffMetadata(ctx) + + if metadataOnly { + return metaresult, metadataErr + } + + contentResult, contentErr := func() (SniffResult, error) { + cacheDeadline := 200 * time.Millisecond + totalAttempt := 0 + for { + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + cachingStartingTimeStamp := time.Now() + err := cReader.Cache(payload, cacheDeadline) + if err != nil { + return nil, err + } + cachingTimeElapsed := time.Since(cachingStartingTimeStamp) + cacheDeadline -= cachingTimeElapsed + + if !payload.IsEmpty() { + result, err := sniffer.Sniff(ctx, payload.Bytes(), network) + switch err { + 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 + } + } else { + totalAttempt++ + } + if totalAttempt >= 2 || cacheDeadline <= 0 { + return nil, errSniffingTimeout + } + } + } + }() + if contentErr != nil && metadataErr == nil { + return metaresult, nil + } + if contentErr == nil && metadataErr == nil { + return CompositeResult(metaresult, contentResult), nil + } + return contentResult, contentErr +} + +func (d *DefaultDispatcher) routedDispatch(ctx context.Context, link *transport.Link, destination net.Destination) { + outbounds := session.OutboundsFromContext(ctx) + ob := outbounds[len(outbounds)-1] + + var handler outbound.Handler + + routingLink := routing_session.AsRoutingContext(ctx) + inTag := routingLink.GetInboundTag() + isPickRoute := 0 + if forcedOutboundTag := session.GetForcedOutboundTagFromContext(ctx); forcedOutboundTag != "" { + ctx = session.SetForcedOutboundTagToContext(ctx, "") + if h := d.ohm.GetHandler(forcedOutboundTag); h != nil { + isPickRoute = 1 + errors.LogInfo(ctx, "taking platform initialized detour [", forcedOutboundTag, "] for [", destination, "]") + handler = h + } else { + errors.LogError(ctx, "non existing tag for platform initialized detour: ", forcedOutboundTag) + common.Close(link.Writer) + common.Interrupt(link.Reader) + return + } + } else if d.router != nil { + if route, err := d.router.PickRoute(routingLink); err == nil { + outTag := route.GetOutboundTag() + if h := d.ohm.GetHandler(outTag); h != nil { + isPickRoute = 2 + if route.GetRuleTag() == "" { + errors.LogInfo(ctx, "taking detour [", outTag, "] for [", destination, "]") + } else { + errors.LogInfo(ctx, "Hit route rule: [", route.GetRuleTag(), "] so taking detour [", outTag, "] for [", destination, "]") + } + handler = h + } else { + errors.LogWarning(ctx, "non existing outTag: ", outTag) + common.Close(link.Writer) + common.Interrupt(link.Reader) + return // DO NOT CHANGE: the traffic shouldn't be processed by default outbound if the specified outbound tag doesn't exist (yet), e.g., VLESS Reverse Proxy + } + } else { + errors.LogInfo(ctx, "default route for ", destination) + } + } + + if handler == nil { + handler = d.ohm.GetDefaultHandler() + } + + if handler == nil { + errors.LogInfo(ctx, "default outbound handler not exist") + common.Close(link.Writer) + common.Interrupt(link.Reader) + return + } + + ob.Tag = handler.Tag() + if accessMessage := log.AccessMessageFromContext(ctx); accessMessage != nil { + if tag := handler.Tag(); tag != "" { + if inTag == "" { + accessMessage.Detour = tag + } else if isPickRoute == 1 { + accessMessage.Detour = inTag + " ==> " + tag + } else if isPickRoute == 2 { + accessMessage.Detour = inTag + " -> " + tag + } else { + accessMessage.Detour = inTag + " >> " + tag + } + } + log.Record(accessMessage) + } + + handler.Dispatch(ctx, link) +} diff --git a/subproject/Xray-core-main/app/dispatcher/dispatcher.go b/subproject/Xray-core-main/app/dispatcher/dispatcher.go new file mode 100644 index 00000000..909218d5 --- /dev/null +++ b/subproject/Xray-core-main/app/dispatcher/dispatcher.go @@ -0,0 +1 @@ +package dispatcher diff --git a/subproject/Xray-core-main/app/dispatcher/fakednssniffer.go b/subproject/Xray-core-main/app/dispatcher/fakednssniffer.go new file mode 100644 index 00000000..bed90877 --- /dev/null +++ b/subproject/Xray-core-main/app/dispatcher/fakednssniffer.go @@ -0,0 +1,121 @@ +package dispatcher + +import ( + "context" + "strings" + + "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/session" + "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/features/dns" +) + +// newFakeDNSSniffer Creates a Fake DNS metadata sniffer +func newFakeDNSSniffer(ctx context.Context) (protocolSnifferWithMetadata, error) { + var fakeDNSEngine dns.FakeDNSEngine + { + fakeDNSEngineFeat := core.MustFromContext(ctx).GetFeature((*dns.FakeDNSEngine)(nil)) + if fakeDNSEngineFeat != nil { + fakeDNSEngine = fakeDNSEngineFeat.(dns.FakeDNSEngine) + } + } + + if fakeDNSEngine == nil { + errNotInit := errors.New("FakeDNSEngine is not initialized, but such a sniffer is used").AtError() + return protocolSnifferWithMetadata{}, errNotInit + } + return protocolSnifferWithMetadata{protocolSniffer: func(ctx context.Context, bytes []byte) (SniffResult, error) { + outbounds := session.OutboundsFromContext(ctx) + ob := outbounds[len(outbounds)-1] + if ob.Target.Network == net.Network_TCP || ob.Target.Network == net.Network_UDP { + domainFromFakeDNS := fakeDNSEngine.GetDomainFromFakeDNS(ob.Target.Address) + if domainFromFakeDNS != "" { + errors.LogInfo(ctx, "fake dns got domain: ", domainFromFakeDNS, " for ip: ", ob.Target.Address.String()) + return &fakeDNSSniffResult{domainName: domainFromFakeDNS}, nil + } + } + + if ipAddressInRangeValueI := ctx.Value(ipAddressInRange); ipAddressInRangeValueI != nil { + ipAddressInRangeValue := ipAddressInRangeValueI.(*ipAddressInRangeOpt) + if fkr0, ok := fakeDNSEngine.(dns.FakeDNSEngineRev0); ok { + inPool := fkr0.IsIPInIPPool(ob.Target.Address) + ipAddressInRangeValue.addressInRange = &inPool + } + } + + return nil, common.ErrNoClue + }, metadataSniffer: true}, nil +} + +type fakeDNSSniffResult struct { + domainName string +} + +func (fakeDNSSniffResult) Protocol() string { + return "fakedns" +} + +func (f fakeDNSSniffResult) Domain() string { + return f.domainName +} + +type fakeDNSExtraOpts int + +const ipAddressInRange fakeDNSExtraOpts = 1 + +type ipAddressInRangeOpt struct { + addressInRange *bool +} + +type DNSThenOthersSniffResult struct { + domainName string + protocolOriginalName string +} + +func (f DNSThenOthersSniffResult) IsProtoSubsetOf(protocolName string) bool { + return strings.HasPrefix(protocolName, f.protocolOriginalName) +} + +func (DNSThenOthersSniffResult) Protocol() string { + return "fakedns+others" +} + +func (f DNSThenOthersSniffResult) Domain() string { + return f.domainName +} + +func newFakeDNSThenOthers(ctx context.Context, fakeDNSSniffer protocolSnifferWithMetadata, others []protocolSnifferWithMetadata) ( + protocolSnifferWithMetadata, error, +) { // nolint: unparam + // ctx may be used in the future + _ = ctx + return protocolSnifferWithMetadata{ + protocolSniffer: func(ctx context.Context, bytes []byte) (SniffResult, error) { + ipAddressInRangeValue := &ipAddressInRangeOpt{} + ctx = context.WithValue(ctx, ipAddressInRange, ipAddressInRangeValue) + result, err := fakeDNSSniffer.protocolSniffer(ctx, bytes) + if err == nil { + return result, nil + } + if ipAddressInRangeValue.addressInRange != nil { + if *ipAddressInRangeValue.addressInRange { + for _, v := range others { + if v.metadataSniffer || bytes != nil { + if result, err := v.protocolSniffer(ctx, bytes); err == nil { + return DNSThenOthersSniffResult{domainName: result.Domain(), protocolOriginalName: result.Protocol()}, nil + } + } + } + return nil, common.ErrNoClue + } + errors.LogDebug(ctx, "ip address not in fake dns range, return as is") + return nil, common.ErrNoClue + } + errors.LogWarning(ctx, "fake dns sniffer did not set address in range option, assume false.") + return nil, common.ErrNoClue + }, + metadataSniffer: false, + }, nil +} diff --git a/subproject/Xray-core-main/app/dispatcher/sniffer.go b/subproject/Xray-core-main/app/dispatcher/sniffer.go new file mode 100644 index 00000000..d6acf0d9 --- /dev/null +++ b/subproject/Xray-core-main/app/dispatcher/sniffer.go @@ -0,0 +1,142 @@ +package dispatcher + +import ( + "context" + + "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/protocol" + "github.com/xtls/xray-core/common/protocol/bittorrent" + "github.com/xtls/xray-core/common/protocol/http" + "github.com/xtls/xray-core/common/protocol/quic" + "github.com/xtls/xray-core/common/protocol/tls" +) + +type SniffResult interface { + Protocol() string + Domain() string +} + +type protocolSniffer func(context.Context, []byte) (SniffResult, error) + +type protocolSnifferWithMetadata struct { + protocolSniffer protocolSniffer + // A Metadata sniffer will be invoked on connection establishment only, with nil body, + // for both TCP and UDP connections + // It will not be shown as a traffic type for routing unless there is no other successful sniffing. + metadataSniffer bool + network net.Network +} + +type Sniffer struct { + sniffer []protocolSnifferWithMetadata +} + +func NewSniffer(ctx context.Context) *Sniffer { + ret := &Sniffer{ + sniffer: []protocolSnifferWithMetadata{ + {func(c context.Context, b []byte) (SniffResult, error) { return http.SniffHTTP(b, c) }, false, net.Network_TCP}, + {func(c context.Context, b []byte) (SniffResult, error) { return tls.SniffTLS(b) }, false, net.Network_TCP}, + {func(c context.Context, b []byte) (SniffResult, error) { return bittorrent.SniffBittorrent(b) }, false, net.Network_TCP}, + {func(c context.Context, b []byte) (SniffResult, error) { return quic.SniffQUIC(b) }, false, net.Network_UDP}, + {func(c context.Context, b []byte) (SniffResult, error) { return bittorrent.SniffUTP(b) }, false, net.Network_UDP}, + }, + } + if sniffer, err := newFakeDNSSniffer(ctx); err == nil { + others := ret.sniffer + ret.sniffer = append(ret.sniffer, sniffer) + fakeDNSThenOthers, err := newFakeDNSThenOthers(ctx, sniffer, others) + if err == nil { + ret.sniffer = append([]protocolSnifferWithMetadata{fakeDNSThenOthers}, ret.sniffer...) + } + } + return ret +} + +var errUnknownContent = errors.New("unknown content") + +func (s *Sniffer) Sniff(c context.Context, payload []byte, network net.Network) (SniffResult, error) { + var pendingSniffer []protocolSnifferWithMetadata + for _, si := range s.sniffer { + protocolSniffer := si.protocolSniffer + if si.metadataSniffer || si.network != network { + continue + } + result, err := protocolSniffer(c, payload) + if err == common.ErrNoClue { + pendingSniffer = append(pendingSniffer, si) + 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 { + return result, nil + } + } + + if len(pendingSniffer) > 0 { + s.sniffer = pendingSniffer + return nil, common.ErrNoClue + } + + return nil, errUnknownContent +} + +func (s *Sniffer) SniffMetadata(c context.Context) (SniffResult, error) { + var pendingSniffer []protocolSnifferWithMetadata + for _, si := range s.sniffer { + s := si.protocolSniffer + if !si.metadataSniffer { + pendingSniffer = append(pendingSniffer, si) + continue + } + result, err := s(c, nil) + if err == common.ErrNoClue { + pendingSniffer = append(pendingSniffer, si) + continue + } + + if err == nil && result != nil { + return result, nil + } + } + + if len(pendingSniffer) > 0 { + s.sniffer = pendingSniffer + return nil, common.ErrNoClue + } + + return nil, errUnknownContent +} + +func CompositeResult(domainResult SniffResult, protocolResult SniffResult) SniffResult { + return &compositeResult{domainResult: domainResult, protocolResult: protocolResult} +} + +type compositeResult struct { + domainResult SniffResult + protocolResult SniffResult +} + +func (c compositeResult) Protocol() string { + return c.protocolResult.Protocol() +} + +func (c compositeResult) Domain() string { + return c.domainResult.Domain() +} + +func (c compositeResult) ProtocolForDomainResult() string { + return c.domainResult.Protocol() +} + +type SnifferResultComposite interface { + ProtocolForDomainResult() string +} + +type SnifferIsProtoSubsetOf interface { + IsProtoSubsetOf(protocolName string) bool +} diff --git a/subproject/Xray-core-main/app/dispatcher/stats.go b/subproject/Xray-core-main/app/dispatcher/stats.go new file mode 100644 index 00000000..8fac0193 --- /dev/null +++ b/subproject/Xray-core-main/app/dispatcher/stats.go @@ -0,0 +1,25 @@ +package dispatcher + +import ( + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/features/stats" +) + +type SizeStatWriter struct { + Counter stats.Counter + Writer buf.Writer +} + +func (w *SizeStatWriter) WriteMultiBuffer(mb buf.MultiBuffer) error { + w.Counter.Add(int64(mb.Len())) + return w.Writer.WriteMultiBuffer(mb) +} + +func (w *SizeStatWriter) Close() error { + return common.Close(w.Writer) +} + +func (w *SizeStatWriter) Interrupt() { + common.Interrupt(w.Writer) +} diff --git a/subproject/Xray-core-main/app/dispatcher/stats_test.go b/subproject/Xray-core-main/app/dispatcher/stats_test.go new file mode 100644 index 00000000..6eca32a4 --- /dev/null +++ b/subproject/Xray-core-main/app/dispatcher/stats_test.go @@ -0,0 +1,44 @@ +package dispatcher_test + +import ( + "testing" + + . "github.com/xtls/xray-core/app/dispatcher" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" +) + +type TestCounter int64 + +func (c *TestCounter) Value() int64 { + return int64(*c) +} + +func (c *TestCounter) Add(v int64) int64 { + x := int64(*c) + v + *c = TestCounter(x) + return x +} + +func (c *TestCounter) Set(v int64) int64 { + *c = TestCounter(v) + return v +} + +func TestStatsWriter(t *testing.T) { + var c TestCounter + writer := &SizeStatWriter{ + Counter: &c, + Writer: buf.Discard, + } + + mb := buf.MergeBytes(nil, []byte("abcd")) + common.Must(writer.WriteMultiBuffer(mb)) + + mb = buf.MergeBytes(nil, []byte("efg")) + common.Must(writer.WriteMultiBuffer(mb)) + + if c.Value() != 7 { + t.Fatal("unexpected counter value. want 7, but got ", c.Value()) + } +} diff --git a/subproject/Xray-core-main/app/dns/cache_controller.go b/subproject/Xray-core-main/app/dns/cache_controller.go new file mode 100644 index 00000000..a303b264 --- /dev/null +++ b/subproject/Xray-core-main/app/dns/cache_controller.go @@ -0,0 +1,339 @@ +package dns + +import ( + "context" + go_errors "errors" + "runtime" + "sync" + "time" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/errors" + "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" + "golang.org/x/sync/singleflight" +) + +const ( + minSizeForEmptyRebuild = 512 + shrinkAbsoluteThreshold = 10240 + shrinkRatioThreshold = 0.65 + migrationBatchSize = 4096 +) + +type CacheController struct { + name string + disableCache bool + serveStale bool + serveExpiredTTL int32 + + ips map[string]*record + dirtyips map[string]*record + + sync.RWMutex + pub *pubsub.Service + cacheCleanup *task.Periodic + highWatermark int + requestGroup singleflight.Group +} + +func NewCacheController(name string, disableCache bool, serveStale bool, serveExpiredTTL uint32) *CacheController { + c := &CacheController{ + name: name, + disableCache: disableCache, + serveStale: serveStale, + serveExpiredTTL: -int32(serveExpiredTTL), + ips: make(map[string]*record), + pub: pubsub.NewService(), + } + + c.cacheCleanup = &task.Periodic{ + Interval: 300 * time.Second, + Execute: c.CacheCleanup, + } + return c +} + +// CacheCleanup clears expired items from cache +func (c *CacheController) CacheCleanup() error { + expiredKeys, err := c.collectExpiredKeys() + if err != nil { + return err + } + if len(expiredKeys) == 0 { + return nil + } + c.writeAndShrink(expiredKeys) + return nil +} + +func (c *CacheController) collectExpiredKeys() ([]string, error) { + c.RLock() + defer c.RUnlock() + + if len(c.ips) == 0 { + return nil, errors.New("nothing to do. stopping...") + } + + // skip collection if a migration is in progress + if c.dirtyips != nil { + return nil, nil + } + + now := time.Now() + if c.serveStale && c.serveExpiredTTL != 0 { + now = now.Add(time.Duration(c.serveExpiredTTL) * time.Second) + } + + expiredKeys := make([]string, 0, len(c.ips)/4) // pre-allocate + + for domain, rec := range c.ips { + if (rec.A != nil && rec.A.Expire.Before(now)) || + (rec.AAAA != nil && rec.AAAA.Expire.Before(now)) { + expiredKeys = append(expiredKeys, domain) + } + } + + return expiredKeys, nil +} + +func (c *CacheController) writeAndShrink(expiredKeys []string) { + c.Lock() + defer c.Unlock() + + // double check to prevent upper call multiple cleanup tasks + if c.dirtyips != nil { + return + } + + lenBefore := len(c.ips) + if lenBefore > c.highWatermark { + c.highWatermark = lenBefore + } + + now := time.Now() + if c.serveStale && c.serveExpiredTTL != 0 { + now = now.Add(time.Duration(c.serveExpiredTTL) * time.Second) + } + + for _, domain := range expiredKeys { + rec := c.ips[domain] + if rec == nil { + continue + } + if rec.A != nil && rec.A.Expire.Before(now) { + rec.A = nil + } + if rec.AAAA != nil && rec.AAAA.Expire.Before(now) { + rec.AAAA = nil + } + if rec.A == nil && rec.AAAA == nil { + delete(c.ips, domain) + } + } + + lenAfter := len(c.ips) + + if lenAfter == 0 { + if c.highWatermark >= minSizeForEmptyRebuild { + errors.LogDebug(context.Background(), c.name, + " rebuilding empty cache map to reclaim memory.", + " size_before_cleanup=", lenBefore, + " peak_size_before_rebuild=", c.highWatermark, + ) + + c.ips = make(map[string]*record) + c.highWatermark = 0 + } + return + } + + if reductionFromPeak := c.highWatermark - lenAfter; reductionFromPeak > shrinkAbsoluteThreshold && + float64(reductionFromPeak) > float64(c.highWatermark)*shrinkRatioThreshold { + errors.LogDebug(context.Background(), c.name, + " shrinking cache map to reclaim memory.", + " new_size=", lenAfter, + " peak_size_before_shrink=", c.highWatermark, + " reduction_since_peak=", reductionFromPeak, + ) + + c.dirtyips = c.ips + c.ips = make(map[string]*record, int(float64(lenAfter)*1.1)) + c.highWatermark = lenAfter + go c.migrate() + } + +} + +type migrationEntry struct { + key string + value *record +} + +func (c *CacheController) migrate() { + defer func() { + if r := recover(); r != nil { + errors.LogError(context.Background(), c.name, " panic during cache migration: ", r) + c.Lock() + c.dirtyips = nil + // c.ips = make(map[string]*record) + // c.highWatermark = 0 + c.Unlock() + } + }() + + c.RLock() + dirtyips := c.dirtyips + c.RUnlock() + + // double check to prevent upper call multiple cleanup tasks + if dirtyips == nil { + return + } + + errors.LogDebug(context.Background(), c.name, " starting background cache migration for ", len(dirtyips), " items") + + batch := make([]migrationEntry, 0, migrationBatchSize) + for domain, recD := range dirtyips { + batch = append(batch, migrationEntry{domain, recD}) + + if len(batch) >= migrationBatchSize { + c.flush(batch) + batch = batch[:0] + runtime.Gosched() + } + } + if len(batch) > 0 { + c.flush(batch) + } + + c.Lock() + c.dirtyips = nil + c.Unlock() + + errors.LogDebug(context.Background(), c.name, " cache migration completed") +} + +func (c *CacheController) flush(batch []migrationEntry) { + c.Lock() + defer c.Unlock() + + for _, dirty := range batch { + if cur := c.ips[dirty.key]; cur != nil { + merge := &record{} + if cur.A == nil { + merge.A = dirty.value.A + } else { + merge.A = cur.A + } + if cur.AAAA == nil { + merge.AAAA = dirty.value.AAAA + } else { + merge.AAAA = cur.AAAA + } + c.ips[dirty.key] = merge + } else { + c.ips[dirty.key] = dirty.value + } + } +} + +func (c *CacheController) updateRecord(req *dnsRequest, rep *IPRecord) { + rtt := time.Since(req.start) + + switch req.reqType { + case dnsmessage.TypeA: + c.pub.Publish(req.domain+"4", rep) + case dnsmessage.TypeAAAA: + c.pub.Publish(req.domain+"6", rep) + } + + if c.disableCache { + errors.LogInfo(context.Background(), c.name, " got answer: ", req.domain, " ", req.reqType, " -> ", rep.IP, ", rtt: ", rtt) + return + } + + c.Lock() + lockWait := time.Since(req.start) - rtt + + newRec := &record{} + oldRec := c.ips[req.domain] + var dirtyRec *record + if c.dirtyips != nil { + dirtyRec = c.dirtyips[req.domain] + } + + var pubRecord *IPRecord + var pubSuffix string + + switch req.reqType { + case dnsmessage.TypeA: + newRec.A = rep + if oldRec != nil && oldRec.AAAA != nil { + newRec.AAAA = oldRec.AAAA + pubRecord = oldRec.AAAA + } else if dirtyRec != nil && dirtyRec.AAAA != nil { + pubRecord = dirtyRec.AAAA + } + pubSuffix = "6" + case dnsmessage.TypeAAAA: + newRec.AAAA = rep + if oldRec != nil && oldRec.A != nil { + newRec.A = oldRec.A + pubRecord = oldRec.A + } else if dirtyRec != nil && dirtyRec.A != nil { + pubRecord = dirtyRec.A + } + pubSuffix = "4" + } + + c.ips[req.domain] = newRec + c.Unlock() + + if pubRecord != nil { + _, ttl, err := pubRecord.getIPs() + if ttl > 0 && !go_errors.Is(err, errRecordNotFound) { + c.pub.Publish(req.domain+pubSuffix, pubRecord) + } + } + + errors.LogInfo(context.Background(), c.name, " got answer: ", req.domain, " ", req.reqType, " -> ", rep.IP, ", rtt: ", rtt, ", lock: ", lockWait) + + if !c.serveStale || c.serveExpiredTTL != 0 { + common.Must(c.cacheCleanup.Start()) + } +} + +func (c *CacheController) findRecords(domain string) *record { + c.RLock() + defer c.RUnlock() + + rec := c.ips[domain] + if rec == nil && c.dirtyips != nil { + rec = c.dirtyips[domain] + } + return rec +} + +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() + } +} diff --git a/subproject/Xray-core-main/app/dns/config.go b/subproject/Xray-core-main/app/dns/config.go new file mode 100644 index 00000000..ab547e14 --- /dev/null +++ b/subproject/Xray-core-main/app/dns/config.go @@ -0,0 +1,64 @@ +package dns + +import ( + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/strmatcher" + "github.com/xtls/xray-core/common/uuid" +) + +var typeMap = map[DomainMatchingType]strmatcher.Type{ + DomainMatchingType_Full: strmatcher.Full, + DomainMatchingType_Subdomain: strmatcher.Domain, + DomainMatchingType_Keyword: strmatcher.Substr, + DomainMatchingType_Regex: strmatcher.Regex, +} + +// References: +// https://www.iana.org/assignments/special-use-domain-names/special-use-domain-names.xhtml +// https://unix.stackexchange.com/questions/92441/whats-the-difference-between-local-home-and-lan +var localTLDsAndDotlessDomains = []*NameServer_PriorityDomain{ + {Type: DomainMatchingType_Regex, Domain: "^[^.]+$"}, // This will only match domains without any dot + {Type: DomainMatchingType_Subdomain, Domain: "local"}, + {Type: DomainMatchingType_Subdomain, Domain: "localdomain"}, + {Type: DomainMatchingType_Subdomain, Domain: "localhost"}, + {Type: DomainMatchingType_Subdomain, Domain: "lan"}, + {Type: DomainMatchingType_Subdomain, Domain: "home.arpa"}, + {Type: DomainMatchingType_Subdomain, Domain: "example"}, + {Type: DomainMatchingType_Subdomain, Domain: "invalid"}, + {Type: DomainMatchingType_Subdomain, Domain: "test"}, +} + +var localTLDsAndDotlessDomainsRule = &NameServer_OriginalRule{ + Rule: "geosite:private", + Size: uint32(len(localTLDsAndDotlessDomains)), +} + +func toStrMatcher(t DomainMatchingType, domain string) (strmatcher.Matcher, error) { + strMType, f := typeMap[t] + if !f { + return nil, errors.New("unknown mapping type", t).AtWarning() + } + matcher, err := strMType.New(domain) + if err != nil { + return nil, errors.New("failed to create str matcher").Base(err) + } + return matcher, nil +} + +func toNetIP(addrs []net.Address) ([]net.IP, error) { + ips := make([]net.IP, 0, len(addrs)) + for _, addr := range addrs { + if addr.Family().IsIP() { + ips = append(ips, addr.IP()) + } else { + return nil, errors.New("Failed to convert address", addr, "to Net IP.").AtWarning() + } + } + return ips, nil +} + +func generateRandomTag() string { + id := uuid.New() + return "xray.system." + id.String() +} diff --git a/subproject/Xray-core-main/app/dns/config.pb.go b/subproject/Xray-core-main/app/dns/config.pb.go new file mode 100644 index 00000000..fb351afb --- /dev/null +++ b/subproject/Xray-core-main/app/dns/config.pb.go @@ -0,0 +1,748 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: app/dns/config.proto + +package dns + +import ( + router "github.com/xtls/xray-core/app/router" + net "github.com/xtls/xray-core/common/net" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type DomainMatchingType int32 + +const ( + DomainMatchingType_Full DomainMatchingType = 0 + DomainMatchingType_Subdomain DomainMatchingType = 1 + DomainMatchingType_Keyword DomainMatchingType = 2 + DomainMatchingType_Regex DomainMatchingType = 3 +) + +// Enum value maps for DomainMatchingType. +var ( + DomainMatchingType_name = map[int32]string{ + 0: "Full", + 1: "Subdomain", + 2: "Keyword", + 3: "Regex", + } + DomainMatchingType_value = map[string]int32{ + "Full": 0, + "Subdomain": 1, + "Keyword": 2, + "Regex": 3, + } +) + +func (x DomainMatchingType) Enum() *DomainMatchingType { + p := new(DomainMatchingType) + *p = x + return p +} + +func (x DomainMatchingType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (DomainMatchingType) Descriptor() protoreflect.EnumDescriptor { + return file_app_dns_config_proto_enumTypes[0].Descriptor() +} + +func (DomainMatchingType) Type() protoreflect.EnumType { + return &file_app_dns_config_proto_enumTypes[0] +} + +func (x DomainMatchingType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use DomainMatchingType.Descriptor instead. +func (DomainMatchingType) EnumDescriptor() ([]byte, []int) { + return file_app_dns_config_proto_rawDescGZIP(), []int{0} +} + +type QueryStrategy int32 + +const ( + QueryStrategy_USE_IP QueryStrategy = 0 + QueryStrategy_USE_IP4 QueryStrategy = 1 + QueryStrategy_USE_IP6 QueryStrategy = 2 + QueryStrategy_USE_SYS QueryStrategy = 3 +) + +// Enum value maps for QueryStrategy. +var ( + QueryStrategy_name = map[int32]string{ + 0: "USE_IP", + 1: "USE_IP4", + 2: "USE_IP6", + 3: "USE_SYS", + } + QueryStrategy_value = map[string]int32{ + "USE_IP": 0, + "USE_IP4": 1, + "USE_IP6": 2, + "USE_SYS": 3, + } +) + +func (x QueryStrategy) Enum() *QueryStrategy { + p := new(QueryStrategy) + *p = x + return p +} + +func (x QueryStrategy) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (QueryStrategy) Descriptor() protoreflect.EnumDescriptor { + return file_app_dns_config_proto_enumTypes[1].Descriptor() +} + +func (QueryStrategy) Type() protoreflect.EnumType { + return &file_app_dns_config_proto_enumTypes[1] +} + +func (x QueryStrategy) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use QueryStrategy.Descriptor instead. +func (QueryStrategy) EnumDescriptor() ([]byte, []int) { + return file_app_dns_config_proto_rawDescGZIP(), []int{1} +} + +type NameServer struct { + state protoimpl.MessageState `protogen:"open.v1"` + 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"` + 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"` + ExpectedGeoip []*router.GeoIP `protobuf:"bytes,3,rep,name=expected_geoip,json=expectedGeoip,proto3" json:"expected_geoip,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"` + ActPrior bool `protobuf:"varint,8,opt,name=actPrior,proto3" json:"actPrior,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"` + DisableCache *bool `protobuf:"varint,11,opt,name=disableCache,proto3,oneof" json:"disableCache,omitempty"` + ServeStale *bool `protobuf:"varint,15,opt,name=serveStale,proto3,oneof" json:"serveStale,omitempty"` + ServeExpiredTTL *uint32 `protobuf:"varint,16,opt,name=serveExpiredTTL,proto3,oneof" json:"serveExpiredTTL,omitempty"` + FinalQuery bool `protobuf:"varint,12,opt,name=finalQuery,proto3" json:"finalQuery,omitempty"` + UnexpectedGeoip []*router.GeoIP `protobuf:"bytes,13,rep,name=unexpected_geoip,json=unexpectedGeoip,proto3" json:"unexpected_geoip,omitempty"` + ActUnprior bool `protobuf:"varint,14,opt,name=actUnprior,proto3" json:"actUnprior,omitempty"` + PolicyID uint32 `protobuf:"varint,17,opt,name=policyID,proto3" json:"policyID,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NameServer) Reset() { + *x = NameServer{} + mi := &file_app_dns_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NameServer) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NameServer) ProtoMessage() {} + +func (x *NameServer) ProtoReflect() protoreflect.Message { + mi := &file_app_dns_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NameServer.ProtoReflect.Descriptor instead. +func (*NameServer) Descriptor() ([]byte, []int) { + return file_app_dns_config_proto_rawDescGZIP(), []int{0} +} + +func (x *NameServer) GetAddress() *net.Endpoint { + if x != nil { + return x.Address + } + return nil +} + +func (x *NameServer) GetClientIp() []byte { + if x != nil { + return x.ClientIp + } + return nil +} + +func (x *NameServer) GetSkipFallback() bool { + if x != nil { + return x.SkipFallback + } + return false +} + +func (x *NameServer) GetPrioritizedDomain() []*NameServer_PriorityDomain { + if x != nil { + return x.PrioritizedDomain + } + return nil +} + +func (x *NameServer) GetExpectedGeoip() []*router.GeoIP { + if x != nil { + return x.ExpectedGeoip + } + return nil +} + +func (x *NameServer) GetOriginalRules() []*NameServer_OriginalRule { + if x != nil { + return x.OriginalRules + } + return nil +} + +func (x *NameServer) GetQueryStrategy() QueryStrategy { + if x != nil { + return x.QueryStrategy + } + return QueryStrategy_USE_IP +} + +func (x *NameServer) GetActPrior() bool { + if x != nil { + return x.ActPrior + } + 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 +} + +func (x *NameServer) GetDisableCache() bool { + if x != nil && x.DisableCache != nil { + return *x.DisableCache + } + return false +} + +func (x *NameServer) GetServeStale() bool { + if x != nil && x.ServeStale != nil { + return *x.ServeStale + } + return false +} + +func (x *NameServer) GetServeExpiredTTL() uint32 { + if x != nil && x.ServeExpiredTTL != nil { + return *x.ServeExpiredTTL + } + return 0 +} + +func (x *NameServer) GetFinalQuery() bool { + if x != nil { + return x.FinalQuery + } + return false +} + +func (x *NameServer) GetUnexpectedGeoip() []*router.GeoIP { + if x != nil { + return x.UnexpectedGeoip + } + return nil +} + +func (x *NameServer) GetActUnprior() bool { + if x != nil { + return x.ActUnprior + } + return false +} + +func (x *NameServer) GetPolicyID() uint32 { + if x != nil { + return x.PolicyID + } + return 0 +} + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + // NameServer list used by this DNS client. + // A special value 'localhost' as a domain address can be set to use DNS on local system. + NameServer []*NameServer `protobuf:"bytes,5,rep,name=name_server,json=nameServer,proto3" json:"name_server,omitempty"` + // Client IP for EDNS client subnet. Must be 4 bytes (IPv4) or 16 bytes + // (IPv6). + ClientIp []byte `protobuf:"bytes,3,opt,name=client_ip,json=clientIp,proto3" json:"client_ip,omitempty"` + StaticHosts []*Config_HostMapping `protobuf:"bytes,4,rep,name=static_hosts,json=staticHosts,proto3" json:"static_hosts,omitempty"` + // Tag is the inbound tag of DNS client. + Tag string `protobuf:"bytes,6,opt,name=tag,proto3" json:"tag,omitempty"` + // DisableCache disables DNS cache + DisableCache bool `protobuf:"varint,8,opt,name=disableCache,proto3" json:"disableCache,omitempty"` + ServeStale bool `protobuf:"varint,12,opt,name=serveStale,proto3" json:"serveStale,omitempty"` + ServeExpiredTTL uint32 `protobuf:"varint,13,opt,name=serveExpiredTTL,proto3" json:"serveExpiredTTL,omitempty"` + QueryStrategy QueryStrategy `protobuf:"varint,9,opt,name=query_strategy,json=queryStrategy,proto3,enum=xray.app.dns.QueryStrategy" json:"query_strategy,omitempty"` + DisableFallback bool `protobuf:"varint,10,opt,name=disableFallback,proto3" json:"disableFallback,omitempty"` + DisableFallbackIfMatch bool `protobuf:"varint,11,opt,name=disableFallbackIfMatch,proto3" json:"disableFallbackIfMatch,omitempty"` + EnableParallelQuery bool `protobuf:"varint,14,opt,name=enableParallelQuery,proto3" json:"enableParallelQuery,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_app_dns_config_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_app_dns_config_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_app_dns_config_proto_rawDescGZIP(), []int{1} +} + +func (x *Config) GetNameServer() []*NameServer { + if x != nil { + return x.NameServer + } + return nil +} + +func (x *Config) GetClientIp() []byte { + if x != nil { + return x.ClientIp + } + return nil +} + +func (x *Config) GetStaticHosts() []*Config_HostMapping { + if x != nil { + return x.StaticHosts + } + return nil +} + +func (x *Config) GetTag() string { + if x != nil { + return x.Tag + } + return "" +} + +func (x *Config) GetDisableCache() bool { + if x != nil { + return x.DisableCache + } + return false +} + +func (x *Config) GetServeStale() bool { + if x != nil { + return x.ServeStale + } + return false +} + +func (x *Config) GetServeExpiredTTL() uint32 { + if x != nil { + return x.ServeExpiredTTL + } + return 0 +} + +func (x *Config) GetQueryStrategy() QueryStrategy { + if x != nil { + return x.QueryStrategy + } + return QueryStrategy_USE_IP +} + +func (x *Config) GetDisableFallback() bool { + if x != nil { + return x.DisableFallback + } + return false +} + +func (x *Config) GetDisableFallbackIfMatch() bool { + if x != nil { + return x.DisableFallbackIfMatch + } + return false +} + +func (x *Config) GetEnableParallelQuery() bool { + if x != nil { + return x.EnableParallelQuery + } + return false +} + +type NameServer_PriorityDomain struct { + state protoimpl.MessageState `protogen:"open.v1"` + Type DomainMatchingType `protobuf:"varint,1,opt,name=type,proto3,enum=xray.app.dns.DomainMatchingType" json:"type,omitempty"` + Domain string `protobuf:"bytes,2,opt,name=domain,proto3" json:"domain,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NameServer_PriorityDomain) Reset() { + *x = NameServer_PriorityDomain{} + mi := &file_app_dns_config_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NameServer_PriorityDomain) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NameServer_PriorityDomain) ProtoMessage() {} + +func (x *NameServer_PriorityDomain) ProtoReflect() protoreflect.Message { + mi := &file_app_dns_config_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NameServer_PriorityDomain.ProtoReflect.Descriptor instead. +func (*NameServer_PriorityDomain) Descriptor() ([]byte, []int) { + return file_app_dns_config_proto_rawDescGZIP(), []int{0, 0} +} + +func (x *NameServer_PriorityDomain) GetType() DomainMatchingType { + if x != nil { + return x.Type + } + return DomainMatchingType_Full +} + +func (x *NameServer_PriorityDomain) GetDomain() string { + if x != nil { + return x.Domain + } + return "" +} + +type NameServer_OriginalRule struct { + state protoimpl.MessageState `protogen:"open.v1"` + Rule string `protobuf:"bytes,1,opt,name=rule,proto3" json:"rule,omitempty"` + Size uint32 `protobuf:"varint,2,opt,name=size,proto3" json:"size,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NameServer_OriginalRule) Reset() { + *x = NameServer_OriginalRule{} + mi := &file_app_dns_config_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NameServer_OriginalRule) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NameServer_OriginalRule) ProtoMessage() {} + +func (x *NameServer_OriginalRule) ProtoReflect() protoreflect.Message { + mi := &file_app_dns_config_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NameServer_OriginalRule.ProtoReflect.Descriptor instead. +func (*NameServer_OriginalRule) Descriptor() ([]byte, []int) { + return file_app_dns_config_proto_rawDescGZIP(), []int{0, 1} +} + +func (x *NameServer_OriginalRule) GetRule() string { + if x != nil { + return x.Rule + } + return "" +} + +func (x *NameServer_OriginalRule) GetSize() uint32 { + if x != nil { + return x.Size + } + return 0 +} + +type Config_HostMapping struct { + state protoimpl.MessageState `protogen:"open.v1"` + Type DomainMatchingType `protobuf:"varint,1,opt,name=type,proto3,enum=xray.app.dns.DomainMatchingType" json:"type,omitempty"` + Domain string `protobuf:"bytes,2,opt,name=domain,proto3" json:"domain,omitempty"` + Ip [][]byte `protobuf:"bytes,3,rep,name=ip,proto3" json:"ip,omitempty"` + // ProxiedDomain indicates the mapped domain has the same IP address on this + // domain. Xray will use this domain for IP queries. + ProxiedDomain string `protobuf:"bytes,4,opt,name=proxied_domain,json=proxiedDomain,proto3" json:"proxied_domain,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config_HostMapping) Reset() { + *x = Config_HostMapping{} + mi := &file_app_dns_config_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config_HostMapping) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config_HostMapping) ProtoMessage() {} + +func (x *Config_HostMapping) ProtoReflect() protoreflect.Message { + mi := &file_app_dns_config_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config_HostMapping.ProtoReflect.Descriptor instead. +func (*Config_HostMapping) Descriptor() ([]byte, []int) { + return file_app_dns_config_proto_rawDescGZIP(), []int{1, 0} +} + +func (x *Config_HostMapping) GetType() DomainMatchingType { + if x != nil { + return x.Type + } + return DomainMatchingType_Full +} + +func (x *Config_HostMapping) GetDomain() string { + if x != nil { + return x.Domain + } + return "" +} + +func (x *Config_HostMapping) GetIp() [][]byte { + if x != nil { + return x.Ip + } + return nil +} + +func (x *Config_HostMapping) GetProxiedDomain() string { + if x != nil { + return x.ProxiedDomain + } + return "" +} + +var File_app_dns_config_proto protoreflect.FileDescriptor + +const file_app_dns_config_proto_rawDesc = "" + + "\n" + + "\x14app/dns/config.proto\x12\fxray.app.dns\x1a\x1ccommon/net/destination.proto\x1a\x17app/router/config.proto\"\xdf\a\n" + + "\n" + + "NameServer\x123\n" + + "\aaddress\x18\x01 \x01(\v2\x19.xray.common.net.EndpointR\aaddress\x12\x1b\n" + + "\tclient_ip\x18\x05 \x01(\fR\bclientIp\x12\"\n" + + "\fskipFallback\x18\x06 \x01(\bR\fskipFallback\x12V\n" + + "\x12prioritized_domain\x18\x02 \x03(\v2'.xray.app.dns.NameServer.PriorityDomainR\x11prioritizedDomain\x12=\n" + + "\x0eexpected_geoip\x18\x03 \x03(\v2\x16.xray.app.router.GeoIPR\rexpectedGeoip\x12L\n" + + "\x0eoriginal_rules\x18\x04 \x03(\v2%.xray.app.dns.NameServer.OriginalRuleR\roriginalRules\x12B\n" + + "\x0equery_strategy\x18\a \x01(\x0e2\x1b.xray.app.dns.QueryStrategyR\rqueryStrategy\x12\x1a\n" + + "\bactPrior\x18\b \x01(\bR\bactPrior\x12\x10\n" + + "\x03tag\x18\t \x01(\tR\x03tag\x12\x1c\n" + + "\ttimeoutMs\x18\n" + + " \x01(\x04R\ttimeoutMs\x12'\n" + + "\fdisableCache\x18\v \x01(\bH\x00R\fdisableCache\x88\x01\x01\x12#\n" + + "\n" + + "serveStale\x18\x0f \x01(\bH\x01R\n" + + "serveStale\x88\x01\x01\x12-\n" + + "\x0fserveExpiredTTL\x18\x10 \x01(\rH\x02R\x0fserveExpiredTTL\x88\x01\x01\x12\x1e\n" + + "\n" + + "finalQuery\x18\f \x01(\bR\n" + + "finalQuery\x12A\n" + + "\x10unexpected_geoip\x18\r \x03(\v2\x16.xray.app.router.GeoIPR\x0funexpectedGeoip\x12\x1e\n" + + "\n" + + "actUnprior\x18\x0e \x01(\bR\n" + + "actUnprior\x12\x1a\n" + + "\bpolicyID\x18\x11 \x01(\rR\bpolicyID\x1a^\n" + + "\x0ePriorityDomain\x124\n" + + "\x04type\x18\x01 \x01(\x0e2 .xray.app.dns.DomainMatchingTypeR\x04type\x12\x16\n" + + "\x06domain\x18\x02 \x01(\tR\x06domain\x1a6\n" + + "\fOriginalRule\x12\x12\n" + + "\x04rule\x18\x01 \x01(\tR\x04rule\x12\x12\n" + + "\x04size\x18\x02 \x01(\rR\x04sizeB\x0f\n" + + "\r_disableCacheB\r\n" + + "\v_serveStaleB\x12\n" + + "\x10_serveExpiredTTL\"\x98\x05\n" + + "\x06Config\x129\n" + + "\vname_server\x18\x05 \x03(\v2\x18.xray.app.dns.NameServerR\n" + + "nameServer\x12\x1b\n" + + "\tclient_ip\x18\x03 \x01(\fR\bclientIp\x12C\n" + + "\fstatic_hosts\x18\x04 \x03(\v2 .xray.app.dns.Config.HostMappingR\vstaticHosts\x12\x10\n" + + "\x03tag\x18\x06 \x01(\tR\x03tag\x12\"\n" + + "\fdisableCache\x18\b \x01(\bR\fdisableCache\x12\x1e\n" + + "\n" + + "serveStale\x18\f \x01(\bR\n" + + "serveStale\x12(\n" + + "\x0fserveExpiredTTL\x18\r \x01(\rR\x0fserveExpiredTTL\x12B\n" + + "\x0equery_strategy\x18\t \x01(\x0e2\x1b.xray.app.dns.QueryStrategyR\rqueryStrategy\x12(\n" + + "\x0fdisableFallback\x18\n" + + " \x01(\bR\x0fdisableFallback\x126\n" + + "\x16disableFallbackIfMatch\x18\v \x01(\bR\x16disableFallbackIfMatch\x120\n" + + "\x13enableParallelQuery\x18\x0e \x01(\bR\x13enableParallelQuery\x1a\x92\x01\n" + + "\vHostMapping\x124\n" + + "\x04type\x18\x01 \x01(\x0e2 .xray.app.dns.DomainMatchingTypeR\x04type\x12\x16\n" + + "\x06domain\x18\x02 \x01(\tR\x06domain\x12\x0e\n" + + "\x02ip\x18\x03 \x03(\fR\x02ip\x12%\n" + + "\x0eproxied_domain\x18\x04 \x01(\tR\rproxiedDomainJ\x04\b\a\x10\b*E\n" + + "\x12DomainMatchingType\x12\b\n" + + "\x04Full\x10\x00\x12\r\n" + + "\tSubdomain\x10\x01\x12\v\n" + + "\aKeyword\x10\x02\x12\t\n" + + "\x05Regex\x10\x03*B\n" + + "\rQueryStrategy\x12\n" + + "\n" + + "\x06USE_IP\x10\x00\x12\v\n" + + "\aUSE_IP4\x10\x01\x12\v\n" + + "\aUSE_IP6\x10\x02\x12\v\n" + + "\aUSE_SYS\x10\x03BF\n" + + "\x10com.xray.app.dnsP\x01Z!github.com/xtls/xray-core/app/dns\xaa\x02\fXray.App.Dnsb\x06proto3" + +var ( + file_app_dns_config_proto_rawDescOnce sync.Once + file_app_dns_config_proto_rawDescData []byte +) + +func file_app_dns_config_proto_rawDescGZIP() []byte { + file_app_dns_config_proto_rawDescOnce.Do(func() { + file_app_dns_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_app_dns_config_proto_rawDesc), len(file_app_dns_config_proto_rawDesc))) + }) + return file_app_dns_config_proto_rawDescData +} + +var file_app_dns_config_proto_enumTypes = make([]protoimpl.EnumInfo, 2) +var file_app_dns_config_proto_msgTypes = make([]protoimpl.MessageInfo, 5) +var file_app_dns_config_proto_goTypes = []any{ + (DomainMatchingType)(0), // 0: xray.app.dns.DomainMatchingType + (QueryStrategy)(0), // 1: xray.app.dns.QueryStrategy + (*NameServer)(nil), // 2: xray.app.dns.NameServer + (*Config)(nil), // 3: xray.app.dns.Config + (*NameServer_PriorityDomain)(nil), // 4: xray.app.dns.NameServer.PriorityDomain + (*NameServer_OriginalRule)(nil), // 5: xray.app.dns.NameServer.OriginalRule + (*Config_HostMapping)(nil), // 6: xray.app.dns.Config.HostMapping + (*net.Endpoint)(nil), // 7: xray.common.net.Endpoint + (*router.GeoIP)(nil), // 8: xray.app.router.GeoIP +} +var file_app_dns_config_proto_depIdxs = []int32{ + 7, // 0: xray.app.dns.NameServer.address:type_name -> xray.common.net.Endpoint + 4, // 1: xray.app.dns.NameServer.prioritized_domain:type_name -> xray.app.dns.NameServer.PriorityDomain + 8, // 2: xray.app.dns.NameServer.expected_geoip:type_name -> xray.app.router.GeoIP + 5, // 3: xray.app.dns.NameServer.original_rules:type_name -> xray.app.dns.NameServer.OriginalRule + 1, // 4: xray.app.dns.NameServer.query_strategy:type_name -> xray.app.dns.QueryStrategy + 8, // 5: xray.app.dns.NameServer.unexpected_geoip:type_name -> xray.app.router.GeoIP + 2, // 6: xray.app.dns.Config.name_server:type_name -> xray.app.dns.NameServer + 6, // 7: xray.app.dns.Config.static_hosts:type_name -> xray.app.dns.Config.HostMapping + 1, // 8: xray.app.dns.Config.query_strategy:type_name -> xray.app.dns.QueryStrategy + 0, // 9: xray.app.dns.NameServer.PriorityDomain.type:type_name -> xray.app.dns.DomainMatchingType + 0, // 10: xray.app.dns.Config.HostMapping.type:type_name -> xray.app.dns.DomainMatchingType + 11, // [11:11] is the sub-list for method output_type + 11, // [11:11] is the sub-list for method input_type + 11, // [11:11] is the sub-list for extension type_name + 11, // [11:11] is the sub-list for extension extendee + 0, // [0:11] is the sub-list for field type_name +} + +func init() { file_app_dns_config_proto_init() } +func file_app_dns_config_proto_init() { + if File_app_dns_config_proto != nil { + return + } + file_app_dns_config_proto_msgTypes[0].OneofWrappers = []any{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_app_dns_config_proto_rawDesc), len(file_app_dns_config_proto_rawDesc)), + NumEnums: 2, + NumMessages: 5, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_app_dns_config_proto_goTypes, + DependencyIndexes: file_app_dns_config_proto_depIdxs, + EnumInfos: file_app_dns_config_proto_enumTypes, + MessageInfos: file_app_dns_config_proto_msgTypes, + }.Build() + File_app_dns_config_proto = out.File + file_app_dns_config_proto_goTypes = nil + file_app_dns_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/app/dns/config.proto b/subproject/Xray-core-main/app/dns/config.proto new file mode 100644 index 00000000..3ce312bc --- /dev/null +++ b/subproject/Xray-core-main/app/dns/config.proto @@ -0,0 +1,95 @@ +syntax = "proto3"; + +package xray.app.dns; +option csharp_namespace = "Xray.App.Dns"; +option go_package = "github.com/xtls/xray-core/app/dns"; +option java_package = "com.xray.app.dns"; +option java_multiple_files = true; + +import "common/net/destination.proto"; +import "app/router/config.proto"; + +message NameServer { + xray.common.net.Endpoint address = 1; + bytes client_ip = 5; + bool skipFallback = 6; + + message PriorityDomain { + DomainMatchingType type = 1; + string domain = 2; + } + + message OriginalRule { + string rule = 1; + uint32 size = 2; + } + + repeated PriorityDomain prioritized_domain = 2; + repeated xray.app.router.GeoIP expected_geoip = 3; + repeated OriginalRule original_rules = 4; + QueryStrategy query_strategy = 7; + bool actPrior = 8; + string tag = 9; + uint64 timeoutMs = 10; + optional bool disableCache = 11; + optional bool serveStale = 15; + optional uint32 serveExpiredTTL = 16; + bool finalQuery = 12; + repeated xray.app.router.GeoIP unexpected_geoip = 13; + bool actUnprior = 14; + uint32 policyID = 17; +} + +enum DomainMatchingType { + Full = 0; + Subdomain = 1; + Keyword = 2; + Regex = 3; +} + +enum QueryStrategy { + USE_IP = 0; + USE_IP4 = 1; + USE_IP6 = 2; + USE_SYS = 3; +} + +message Config { + // NameServer list used by this DNS client. + // A special value 'localhost' as a domain address can be set to use DNS on local system. + repeated NameServer name_server = 5; + + // Client IP for EDNS client subnet. Must be 4 bytes (IPv4) or 16 bytes + // (IPv6). + bytes client_ip = 3; + + message HostMapping { + DomainMatchingType type = 1; + string domain = 2; + + repeated bytes ip = 3; + + // ProxiedDomain indicates the mapped domain has the same IP address on this + // domain. Xray will use this domain for IP queries. + string proxied_domain = 4; + } + + repeated HostMapping static_hosts = 4; + + // Tag is the inbound tag of DNS client. + string tag = 6; + + reserved 7; + + // DisableCache disables DNS cache + bool disableCache = 8; + bool serveStale = 12; + uint32 serveExpiredTTL = 13; + + QueryStrategy query_strategy = 9; + + bool disableFallback = 10; + bool disableFallbackIfMatch = 11; + + bool enableParallelQuery = 14; +} diff --git a/subproject/Xray-core-main/app/dns/dns.go b/subproject/Xray-core-main/app/dns/dns.go new file mode 100644 index 00000000..c1082083 --- /dev/null +++ b/subproject/Xray-core-main/app/dns/dns.go @@ -0,0 +1,606 @@ +// Package dns is an implementation of core.DNS feature. +package dns + +import ( + "context" + go_errors "errors" + "fmt" + "os" + "runtime" + "sort" + "strings" + "sync" + "time" + + "github.com/xtls/xray-core/app/router" + "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/platform" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/common/strmatcher" + "github.com/xtls/xray-core/features/dns" +) + +// DNS is a DNS rely server. +type DNS struct { + sync.Mutex + disableFallback bool + disableFallbackIfMatch bool + enableParallelQuery bool + ipOption *dns.IPOption + hosts *StaticHosts + clients []*Client + ctx context.Context + domainMatcher strmatcher.IndexMatcher + matcherInfos []*DomainMatcherInfo + checkSystem bool +} + +// DomainMatcherInfo contains information attached to index returned by Server.domainMatcher +type DomainMatcherInfo struct { + clientIdx uint16 + domainRuleIdx uint16 +} + +// New creates a new DNS server with given configuration. +func New(ctx context.Context, config *Config) (*DNS, error) { + var clientIP net.IP + switch len(config.ClientIp) { + case 0, net.IPv4len, net.IPv6len: + clientIP = net.IP(config.ClientIp) + default: + return nil, errors.New("unexpected client IP length ", len(config.ClientIp)) + } + + var ipOption dns.IPOption + checkSystem := false + switch config.QueryStrategy { + case QueryStrategy_USE_IP: + ipOption = dns.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + FakeEnable: false, + } + case QueryStrategy_USE_SYS: + ipOption = dns.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + FakeEnable: false, + } + checkSystem = true + case QueryStrategy_USE_IP4: + ipOption = dns.IPOption{ + IPv4Enable: true, + IPv6Enable: false, + FakeEnable: false, + } + case QueryStrategy_USE_IP6: + ipOption = dns.IPOption{ + IPv4Enable: false, + IPv6Enable: true, + FakeEnable: false, + } + default: + return nil, errors.New("unexpected query strategy ", config.QueryStrategy) + } + + var hosts *StaticHosts + mphLoaded := false + domainMatcherPath := platform.NewEnvFlag(platform.MphCachePath).GetValue(func() string { return "" }) + if domainMatcherPath != "" { + if f, err := os.Open(domainMatcherPath); err == nil { + defer f.Close() + if m, err := router.LoadGeoSiteMatcher(f, "HOSTS"); err == nil { + f.Seek(0, 0) + if hostIPs, err := router.LoadGeoSiteHosts(f); err == nil { + if sh, err := NewStaticHostsFromCache(m, hostIPs); err == nil { + hosts = sh + mphLoaded = true + errors.LogDebug(ctx, "MphDomainMatcher loaded from cache for DNS hosts, size: ", sh.matchers.Size()) + } + } + } + } + } + + if !mphLoaded { + sh, err := NewStaticHosts(config.StaticHosts) + if err != nil { + return nil, errors.New("failed to create hosts").Base(err) + } + hosts = sh + } + + var clients []*Client + domainRuleCount := 0 + + var defaultTag = config.Tag + if len(config.Tag) == 0 { + defaultTag = generateRandomTag() + } + + for _, ns := range config.NameServer { + domainRuleCount += len(ns.PrioritizedDomain) + } + + // MatcherInfos is ensured to cover the maximum index domainMatcher could return, where matcher's index starts from 1 + matcherInfos := make([]*DomainMatcherInfo, domainRuleCount+1) + domainMatcher := &strmatcher.MatcherGroup{} + + for _, ns := range config.NameServer { + clientIdx := len(clients) + updateDomain := func(domainRule strmatcher.Matcher, originalRuleIdx int, matcherInfos []*DomainMatcherInfo) { + midx := domainMatcher.Add(domainRule) + matcherInfos[midx] = &DomainMatcherInfo{ + clientIdx: uint16(clientIdx), + domainRuleIdx: uint16(originalRuleIdx), + } + } + + myClientIP := clientIP + switch len(ns.ClientIp) { + case net.IPv4len, net.IPv6len: + myClientIP = net.IP(ns.ClientIp) + } + + disableCache := config.DisableCache + if ns.DisableCache != nil { + disableCache = *ns.DisableCache + } + + serveStale := config.ServeStale + if ns.ServeStale != nil { + serveStale = *ns.ServeStale + } + + serveExpiredTTL := config.ServeExpiredTTL + if ns.ServeExpiredTTL != nil { + serveExpiredTTL = *ns.ServeExpiredTTL + } + + 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, serveStale, serveExpiredTTL, tag, clientIPOption, &matcherInfos, updateDomain) + if err != nil { + return nil, errors.New("failed to create client").Base(err) + } + clients = append(clients, client) + } + + // If there is no DNS client in config, add a `localhost` DNS client + if len(clients) == 0 { + clients = append(clients, NewLocalDNSClient(ipOption)) + } + + return &DNS{ + hosts: hosts, + ipOption: &ipOption, + clients: clients, + ctx: ctx, + domainMatcher: domainMatcher, + matcherInfos: matcherInfos, + disableFallback: config.DisableFallback, + disableFallbackIfMatch: config.DisableFallbackIfMatch, + enableParallelQuery: config.EnableParallelQuery, + checkSystem: checkSystem, + }, nil +} + +// Type implements common.HasType. +func (*DNS) Type() interface{} { + return dns.ClientType() +} + +// Start implements common.Runnable. +func (s *DNS) Start() error { + return nil +} + +// Close implements common.Closable. +func (s *DNS) Close() error { + return nil +} + +// IsOwnLink implements proxy.dns.ownLinkVerifier +func (s *DNS) IsOwnLink(ctx context.Context) bool { + inbound := session.InboundFromContext(ctx) + if inbound == nil { + return false + } + for _, client := range s.clients { + if client.tag == inbound.Tag { + return true + } + } + return false +} + +// LookupIP implements dns.Client. +func (s *DNS) LookupIP(domain string, option dns.IPOption) ([]net.IP, uint32, error) { + // Normalize the FQDN form query + domain = strings.TrimSuffix(domain, ".") + if domain == "" { + return nil, 0, errors.New("empty domain name") + } + + if s.checkSystem { + supportIPv4, supportIPv6 := checkRoutes() + option.IPv4Enable = option.IPv4Enable && supportIPv4 + option.IPv6Enable = option.IPv6Enable && supportIPv6 + } else { + option.IPv4Enable = option.IPv4Enable && s.ipOption.IPv4Enable + option.IPv6Enable = option.IPv6Enable && s.ipOption.IPv6Enable + } + + if !option.IPv4Enable && !option.IPv6Enable { + return nil, 0, dns.ErrEmptyResponse + } + + // Static host lookup + switch addrs, err := s.hosts.Lookup(domain, option); { + case err != nil: + if go_errors.Is(err, dns.ErrEmptyResponse) { + return nil, 0, dns.ErrEmptyResponse + } + return nil, 0, errors.New("returning nil for domain ", domain).Base(err) + case addrs == nil: // Domain not recorded in static host + break + case len(addrs) == 0: // Domain recorded, but no valid IP returned (e.g. IPv4 address with only IPv6 enabled) + return nil, 0, dns.ErrEmptyResponse + case len(addrs) == 1 && addrs[0].Family().IsDomain(): // Domain replacement + errors.LogInfo(s.ctx, "domain replaced: ", domain, " -> ", addrs[0].Domain()) + domain = addrs[0].Domain() + default: // Successfully found ip records in static host + errors.LogInfo(s.ctx, "returning ", len(addrs), " IP(s) for domain ", domain, " -> ", addrs) + ips, err := toNetIP(addrs) + if err != nil { + return nil, 0, err + } + return ips, 10, nil // Hosts ttl is 10 + } + + // Name servers lookup + if s.enableParallelQuery { + return s.parallelQuery(domain, option) + } else { + return s.serialQuery(domain, option) + } +} + +func (s *DNS) sortClients(domain string) []*Client { + clients := make([]*Client, 0, len(s.clients)) + clientUsed := make([]bool, len(s.clients)) + clientNames := make([]string, 0, len(s.clients)) + domainRules := []string{} + + // Priority domain matching + hasMatch := false + MatchSlice := 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] + client := s.clients[info.clientIdx] + domainRule := client.domains[info.domainRuleIdx] + domainRules = append(domainRules, fmt.Sprintf("%s(DNS idx:%d)", domainRule, info.clientIdx)) + if clientUsed[info.clientIdx] { + continue + } + clientUsed[info.clientIdx] = true + clients = append(clients, client) + clientNames = append(clientNames, client.Name()) + hasMatch = true + if client.finalQuery { + return clients + } + } + + if !(s.disableFallback || s.disableFallbackIfMatch && hasMatch) { + // Default round-robin query + for idx, client := range s.clients { + if clientUsed[idx] || client.skipFallback { + continue + } + clientUsed[idx] = true + clients = append(clients, client) + clientNames = append(clientNames, client.Name()) + if client.finalQuery { + return clients + } + } + } + + if len(domainRules) > 0 { + errors.LogDebug(s.ctx, "domain ", domain, " matches following rules: ", domainRules) + } + if len(clientNames) > 0 { + errors.LogDebug(s.ctx, "domain ", domain, " will use DNS in order: ", clientNames) + } + + if len(clients) == 0 { + if len(s.clients) > 0 { + clients = append(clients, s.clients[0]) + clientNames = append(clientNames, s.clients[0].Name()) + errors.LogWarning(s.ctx, "domain ", domain, " will use the first DNS: ", clientNames) + } else { + errors.LogError(s.ctx, "no DNS clients available for domain ", domain, " and no default clients configured") + } + } + + return clients +} + +func mergeQueryErrors(domain string, errs []error) error { + if len(errs) == 0 { + return dns.ErrEmptyResponse + } + + var noRNF error + for _, err := range errs { + if go_errors.Is(err, errRecordNotFound) { + continue // server no response, ignore + } else if noRNF == nil { + noRNF = err + } else if !go_errors.Is(err, noRNF) { + return errors.New("returning nil for domain ", domain).Base(errors.Combine(errs...)) + } + } + if go_errors.Is(noRNF, dns.ErrEmptyResponse) { + return dns.ErrEmptyResponse + } + if noRNF == nil { + noRNF = errRecordNotFound + } + return errors.New("returning nil for domain ", domain).Base(noRNF) +} + +func (s *DNS) serialQuery(domain string, option dns.IPOption) ([]net.IP, uint32, error) { + var errs []error + for _, client := range s.sortClients(domain) { + if !option.FakeEnable && strings.EqualFold(client.Name(), "FakeDNS") { + errors.LogDebug(s.ctx, "skip DNS resolution for domain ", domain, " at server ", client.Name()) + continue + } + + ips, ttl, err := client.QueryIP(s.ctx, domain, option) + + if len(ips) > 0 { + return ips, ttl, nil + } + + errors.LogInfoInner(s.ctx, err, "failed to lookup ip for domain ", domain, " at server ", client.Name(), " in serial query mode") + if err == nil { + err = dns.ErrEmptyResponse + } + errs = append(errs, err) + } + return nil, 0, mergeQueryErrors(domain, errs) +} + +func (s *DNS) parallelQuery(domain string, option dns.IPOption) ([]net.IP, uint32, error) { + var errs []error + clients := s.sortClients(domain) + + resultsChan := asyncQueryAll(domain, option, clients, s.ctx) + + groups, groupOf := makeGroups( /*s.ctx,*/ clients) + results := make([]*queryResult, len(clients)) + pending := make([]int, len(groups)) + for gi, g := range groups { + pending[gi] = g.end - g.start + 1 + } + + nextGroup := 0 + for range clients { + result := <-resultsChan + results[result.index] = &result + + gi := groupOf[result.index] + pending[gi]-- + + for nextGroup < len(groups) { + g := groups[nextGroup] + + // group race, minimum rtt -> return + for j := g.start; j <= g.end; j++ { + r := results[j] + if r != nil && r.err == nil && len(r.ips) > 0 { + return r.ips, r.ttl, nil + } + } + + // current group is incomplete and no one success -> continue pending + if pending[nextGroup] > 0 { + break + } + + // all failed -> log and continue next group + for j := g.start; j <= g.end; j++ { + r := results[j] + e := r.err + if e == nil { + e = dns.ErrEmptyResponse + } + errors.LogInfoInner(s.ctx, e, "failed to lookup ip for domain ", domain, " at server ", clients[j].Name(), " in parallel query mode") + errs = append(errs, e) + } + nextGroup++ + } + } + + return nil, 0, mergeQueryErrors(domain, errs) +} + +type queryResult struct { + ips []net.IP + ttl uint32 + err error + index int +} + +func asyncQueryAll(domain string, option dns.IPOption, clients []*Client, ctx context.Context) chan queryResult { + if len(clients) == 0 { + ch := make(chan queryResult) + close(ch) + return ch + } + + ch := make(chan queryResult, len(clients)) + for i, client := range clients { + if !option.FakeEnable && strings.EqualFold(client.Name(), "FakeDNS") { + errors.LogDebug(ctx, "skip DNS resolution for domain ", domain, " at server ", client.Name()) + ch <- queryResult{err: dns.ErrEmptyResponse, index: i} + continue + } + + go func(i int, c *Client) { + qctx := ctx + if !c.server.IsDisableCache() { + nctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), c.timeoutMs*2) + qctx = nctx + defer cancel() + } + ips, ttl, err := c.QueryIP(qctx, domain, option) + ch <- queryResult{ips: ips, ttl: ttl, err: err, index: i} + }(i, client) + } + return ch +} + +type group struct{ start, end int } + +// merge only adjacent and rule-equivalent Client into a single group +func makeGroups( /*ctx context.Context,*/ clients []*Client) ([]group, []int) { + n := len(clients) + if n == 0 { + return nil, nil + } + groups := make([]group, 0, n) + groupOf := make([]int, n) + + s, e := 0, 0 + for i := 1; i < n; i++ { + if clients[i-1].policyID == clients[i].policyID { + e = i + } else { + for k := s; k <= e; k++ { + groupOf[k] = len(groups) + } + groups = append(groups, group{start: s, end: e}) + s, e = i, i + } + } + for k := s; k <= e; k++ { + groupOf[k] = len(groups) + } + groups = append(groups, group{start: s, end: e}) + + // var b strings.Builder + // b.WriteString("dns grouping: total clients=") + // b.WriteString(strconv.Itoa(n)) + // b.WriteString(", groups=") + // b.WriteString(strconv.Itoa(len(groups))) + + // for gi, g := range groups { + // b.WriteString("\n [") + // b.WriteString(strconv.Itoa(g.start)) + // b.WriteString("..") + // b.WriteString(strconv.Itoa(g.end)) + // b.WriteString("] gid=") + // b.WriteString(strconv.Itoa(gi)) + // b.WriteString(" pid=") + // b.WriteString(strconv.FormatUint(uint64(clients[g.start].policyID), 10)) + // b.WriteString(" members: ") + + // for i := g.start; i <= g.end; i++ { + // if i > g.start { + // b.WriteString(", ") + // } + // b.WriteString(strconv.Itoa(i)) + // b.WriteByte(':') + // b.WriteString(clients[i].Name()) + // } + // } + // errors.LogDebug(ctx, b.String()) + + return groups, groupOf +} + +func init() { + common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + return New(ctx, config.(*Config)) + })) +} + +func probeRoutes() (ipv4 bool, ipv6 bool) { + if conn, err := net.Dial("udp4", "192.33.4.12:53"); err == nil { + ipv4 = true + conn.Close() + } + if conn, err := net.Dial("udp6", "[2001:500:2::c]:53"); err == nil { + ipv6 = true + conn.Close() + } + return +} + +var routeCache struct { + sync.Once + sync.RWMutex + expire time.Time + ipv4, ipv6 bool +} + +func checkRoutes() (bool, bool) { + if !isGUIPlatform { + routeCache.Once.Do(func() { + routeCache.ipv4, routeCache.ipv6 = probeRoutes() + }) + return routeCache.ipv4, routeCache.ipv6 + } + + routeCache.RWMutex.RLock() + now := time.Now() + if routeCache.expire.After(now) { + routeCache.RWMutex.RUnlock() + return routeCache.ipv4, routeCache.ipv6 + } + routeCache.RWMutex.RUnlock() + + routeCache.RWMutex.Lock() + defer routeCache.RWMutex.Unlock() + + now = time.Now() + if routeCache.expire.After(now) { // double-check + return routeCache.ipv4, routeCache.ipv6 + } + routeCache.ipv4, routeCache.ipv6 = probeRoutes() // ~2ms + routeCache.expire = now.Add(100 * time.Millisecond) // ttl + return routeCache.ipv4, routeCache.ipv6 +} + +var isGUIPlatform = detectGUIPlatform() + +func detectGUIPlatform() bool { + switch runtime.GOOS { + case "android", "ios", "windows", "darwin": + return true + case "linux", "freebsd", "openbsd": + if t := os.Getenv("XDG_SESSION_TYPE"); t == "wayland" || t == "x11" { + return true + } + if os.Getenv("DISPLAY") != "" || os.Getenv("WAYLAND_DISPLAY") != "" { + return true + } + } + return false +} diff --git a/subproject/Xray-core-main/app/dns/dns_test.go b/subproject/Xray-core-main/app/dns/dns_test.go new file mode 100644 index 00000000..cb70b0b3 --- /dev/null +++ b/subproject/Xray-core-main/app/dns/dns_test.go @@ -0,0 +1,1065 @@ +package dns_test + +import ( + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/miekg/dns" + "github.com/xtls/xray-core/app/dispatcher" + . "github.com/xtls/xray-core/app/dns" + "github.com/xtls/xray-core/app/policy" + "github.com/xtls/xray-core/app/proxyman" + _ "github.com/xtls/xray-core/app/proxyman/outbound" + "github.com/xtls/xray-core/app/router" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/serial" + "github.com/xtls/xray-core/core" + feature_dns "github.com/xtls/xray-core/features/dns" + "github.com/xtls/xray-core/proxy/freedom" + "github.com/xtls/xray-core/testing/servers/udp" +) + +type staticHandler struct{} + +func (*staticHandler) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { + ans := new(dns.Msg) + ans.Id = r.Id + + var clientIP net.IP + + opt := r.IsEdns0() + if opt != nil { + for _, o := range opt.Option { + if o.Option() == dns.EDNS0SUBNET { + subnet := o.(*dns.EDNS0_SUBNET) + clientIP = subnet.Address + } + } + } + + for _, q := range r.Question { + switch { + case q.Name == "google.com." && q.Qtype == dns.TypeA: + if clientIP == nil { + rr, _ := dns.NewRR("google.com. IN A 8.8.8.8") + ans.Answer = append(ans.Answer, rr) + } else { + rr, _ := dns.NewRR("google.com. IN A 8.8.4.4") + ans.Answer = append(ans.Answer, rr) + } + + case q.Name == "api.google.com." && q.Qtype == dns.TypeA: + rr, _ := dns.NewRR("api.google.com. IN A 8.8.7.7") + ans.Answer = append(ans.Answer, rr) + + case q.Name == "v2.api.google.com." && q.Qtype == dns.TypeA: + rr, _ := dns.NewRR("v2.api.google.com. IN A 8.8.7.8") + ans.Answer = append(ans.Answer, rr) + + case q.Name == "facebook.com." && q.Qtype == dns.TypeA: + rr, _ := dns.NewRR("facebook.com. IN A 9.9.9.9") + ans.Answer = append(ans.Answer, rr) + + case q.Name == "ipv6.google.com." && q.Qtype == dns.TypeA: + rr, err := dns.NewRR("ipv6.google.com. IN A 8.8.8.7") + common.Must(err) + ans.Answer = append(ans.Answer, rr) + + case q.Name == "ipv6.google.com." && q.Qtype == dns.TypeAAAA: + rr, err := dns.NewRR("ipv6.google.com. IN AAAA 2001:4860:4860::8888") + common.Must(err) + ans.Answer = append(ans.Answer, rr) + + case q.Name == "notexist.google.com." && q.Qtype == dns.TypeAAAA: + 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: + rr, _ := dns.NewRR("hostname. IN A 127.0.0.1") + ans.Answer = append(ans.Answer, rr) + + case q.Name == "hostname.local." && q.Qtype == dns.TypeA: + rr, _ := dns.NewRR("hostname.local. IN A 127.0.0.1") + ans.Answer = append(ans.Answer, rr) + + case q.Name == "hostname.localdomain." && q.Qtype == dns.TypeA: + rr, _ := dns.NewRR("hostname.localdomain. IN A 127.0.0.1") + ans.Answer = append(ans.Answer, rr) + + case q.Name == "localhost." && q.Qtype == dns.TypeA: + rr, _ := dns.NewRR("localhost. IN A 127.0.0.2") + ans.Answer = append(ans.Answer, rr) + + case q.Name == "localhost-a." && q.Qtype == dns.TypeA: + rr, _ := dns.NewRR("localhost-a. IN A 127.0.0.3") + ans.Answer = append(ans.Answer, rr) + + case q.Name == "localhost-b." && q.Qtype == dns.TypeA: + rr, _ := dns.NewRR("localhost-b. IN A 127.0.0.4") + ans.Answer = append(ans.Answer, rr) + + case q.Name == "Mijia\\ Cloud." && q.Qtype == dns.TypeA: + rr, _ := dns.NewRR("Mijia\\ Cloud. IN A 127.0.0.1") + ans.Answer = append(ans.Answer, rr) + } + } + w.WriteMsg(ans) +} + +func TestUDPServerSubnet(t *testing.T) { + port := udp.PickPort() + + dnsServer := dns.Server{ + Addr: "127.0.0.1:" + port.String(), + Net: "udp", + Handler: &staticHandler{}, + UDPSize: 1200, + } + go dnsServer.ListenAndServe() + time.Sleep(time.Second) + + config := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&Config{ + NameServer: []*NameServer{ + { + Address: &net.Endpoint{ + Network: net.Network_UDP, + Address: &net.IPOrDomain{ + Address: &net.IPOrDomain_Ip{ + Ip: []byte{127, 0, 0, 1}, + }, + }, + Port: uint32(port), + }, + }, + }, + ClientIp: []byte{7, 8, 9, 10}, + }), + serial.ToTypedMessage(&dispatcher.Config{}), + serial.ToTypedMessage(&proxyman.OutboundConfig{}), + serial.ToTypedMessage(&policy.Config{}), + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + v, err := core.New(config) + common.Must(err) + + client := v.GetFeature(feature_dns.ClientType()).(feature_dns.Client) + + ips, _, err := client.LookupIP("google.com", feature_dns.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + FakeEnable: false, + }) + if err != nil { + t.Fatal("unexpected error: ", err) + } + + if r := cmp.Diff(ips, []net.IP{{8, 8, 4, 4}}); r != "" { + t.Fatal(r) + } +} + +func TestUDPServer(t *testing.T) { + port := udp.PickPort() + + dnsServer := dns.Server{ + Addr: "127.0.0.1:" + port.String(), + Net: "udp", + Handler: &staticHandler{}, + UDPSize: 1200, + } + + go dnsServer.ListenAndServe() + time.Sleep(time.Second) + + config := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&Config{ + NameServer: []*NameServer{ + { + Address: &net.Endpoint{ + Network: net.Network_UDP, + Address: &net.IPOrDomain{ + Address: &net.IPOrDomain_Ip{ + Ip: []byte{127, 0, 0, 1}, + }, + }, + Port: uint32(port), + }, + }, + }, + }), + serial.ToTypedMessage(&dispatcher.Config{}), + serial.ToTypedMessage(&proxyman.OutboundConfig{}), + serial.ToTypedMessage(&policy.Config{}), + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + v, err := core.New(config) + common.Must(err) + + client := v.GetFeature(feature_dns.ClientType()).(feature_dns.Client) + + { + ips, _, err := client.LookupIP("google.com", feature_dns.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + FakeEnable: false, + }) + if err != nil { + t.Fatal("unexpected error: ", err) + } + + if r := cmp.Diff(ips, []net.IP{{8, 8, 8, 8}}); r != "" { + t.Fatal(r) + } + } + + { + ips, _, err := client.LookupIP("facebook.com", feature_dns.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + FakeEnable: false, + }) + if err != nil { + t.Fatal("unexpected error: ", err) + } + + if r := cmp.Diff(ips, []net.IP{{9, 9, 9, 9}}); r != "" { + t.Fatal(r) + } + } + + { + _, _, err := client.LookupIP("notexist.google.com", feature_dns.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + FakeEnable: false, + }) + if err == nil { + t.Fatal("nil error") + } + if r := feature_dns.RCodeFromError(err); r != uint16(dns.RcodeNameError) { + t.Fatal("expected NameError, but got ", r) + } + } + + { + ips, _, err := client.LookupIP("ipv4only.google.com", feature_dns.IPOption{ + IPv4Enable: false, + IPv6Enable: true, + FakeEnable: false, + }) + if !errors.AllEqual(feature_dns.ErrEmptyResponse, errors.Cause(err)) { + t.Fatal("error: ", err) + } + if len(ips) != 0 { + t.Fatal("ips: ", ips) + } + } + + dnsServer.Shutdown() + + { + ips, _, err := client.LookupIP("google.com", feature_dns.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + FakeEnable: false, + }) + if err != nil { + t.Fatal("unexpected error: ", err) + } + + if r := cmp.Diff(ips, []net.IP{{8, 8, 8, 8}}); r != "" { + t.Fatal(r) + } + } +} + +func TestPrioritizedDomain(t *testing.T) { + port := udp.PickPort() + + dnsServer := dns.Server{ + Addr: "127.0.0.1:" + port.String(), + Net: "udp", + Handler: &staticHandler{}, + UDPSize: 1200, + } + + go dnsServer.ListenAndServe() + time.Sleep(time.Second) + + config := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&Config{ + NameServer: []*NameServer{ + { + Address: &net.Endpoint{ + Network: net.Network_UDP, + Address: &net.IPOrDomain{ + Address: &net.IPOrDomain_Ip{ + Ip: []byte{127, 0, 0, 1}, + }, + }, + Port: 9999, /* unreachable */ + }, + }, + { + Address: &net.Endpoint{ + Network: net.Network_UDP, + Address: &net.IPOrDomain{ + Address: &net.IPOrDomain_Ip{ + Ip: []byte{127, 0, 0, 1}, + }, + }, + Port: uint32(port), + }, + PrioritizedDomain: []*NameServer_PriorityDomain{ + { + Type: DomainMatchingType_Full, + Domain: "google.com", + }, + }, + }, + }, + }), + serial.ToTypedMessage(&dispatcher.Config{}), + serial.ToTypedMessage(&proxyman.OutboundConfig{}), + serial.ToTypedMessage(&policy.Config{}), + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + v, err := core.New(config) + common.Must(err) + + client := v.GetFeature(feature_dns.ClientType()).(feature_dns.Client) + + startTime := time.Now() + + { + ips, _, err := client.LookupIP("google.com", feature_dns.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + FakeEnable: false, + }) + if err != nil { + t.Fatal("unexpected error: ", err) + } + + if r := cmp.Diff(ips, []net.IP{{8, 8, 8, 8}}); r != "" { + t.Fatal(r) + } + } + + endTime := time.Now() + if startTime.After(endTime.Add(time.Second * 2)) { + t.Error("DNS query doesn't finish in 2 seconds.") + } +} + +func TestUDPServerIPv6(t *testing.T) { + port := udp.PickPort() + + dnsServer := dns.Server{ + Addr: "127.0.0.1:" + port.String(), + Net: "udp", + Handler: &staticHandler{}, + UDPSize: 1200, + } + + go dnsServer.ListenAndServe() + time.Sleep(time.Second) + + config := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&Config{ + NameServer: []*NameServer{ + { + Address: &net.Endpoint{ + Network: net.Network_UDP, + Address: &net.IPOrDomain{ + Address: &net.IPOrDomain_Ip{ + Ip: []byte{127, 0, 0, 1}, + }, + }, + Port: uint32(port), + }, + }, + }, + }), + serial.ToTypedMessage(&dispatcher.Config{}), + serial.ToTypedMessage(&proxyman.OutboundConfig{}), + serial.ToTypedMessage(&policy.Config{}), + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + v, err := core.New(config) + common.Must(err) + + client := v.GetFeature(feature_dns.ClientType()).(feature_dns.Client) + { + ips, _, err := client.LookupIP("ipv6.google.com", feature_dns.IPOption{ + IPv4Enable: false, + IPv6Enable: true, + FakeEnable: false, + }) + if err != nil { + t.Fatal("unexpected error: ", err) + } + + if r := cmp.Diff(ips, []net.IP{{32, 1, 72, 96, 72, 96, 0, 0, 0, 0, 0, 0, 0, 0, 136, 136}}); r != "" { + t.Fatal(r) + } + } +} + +func TestStaticHostDomain(t *testing.T) { + port := udp.PickPort() + + dnsServer := dns.Server{ + Addr: "127.0.0.1:" + port.String(), + Net: "udp", + Handler: &staticHandler{}, + UDPSize: 1200, + } + + go dnsServer.ListenAndServe() + time.Sleep(time.Second) + + config := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&Config{ + NameServer: []*NameServer{ + { + Address: &net.Endpoint{ + Network: net.Network_UDP, + Address: &net.IPOrDomain{ + Address: &net.IPOrDomain_Ip{ + Ip: []byte{127, 0, 0, 1}, + }, + }, + Port: uint32(port), + }, + }, + }, + StaticHosts: []*Config_HostMapping{ + { + Type: DomainMatchingType_Full, + Domain: "example.com", + ProxiedDomain: "google.com", + }, + }, + }), + serial.ToTypedMessage(&dispatcher.Config{}), + serial.ToTypedMessage(&proxyman.OutboundConfig{}), + serial.ToTypedMessage(&policy.Config{}), + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + v, err := core.New(config) + common.Must(err) + + client := v.GetFeature(feature_dns.ClientType()).(feature_dns.Client) + + { + ips, _, err := client.LookupIP("example.com", feature_dns.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + FakeEnable: false, + }) + if err != nil { + t.Fatal("unexpected error: ", err) + } + + if r := cmp.Diff(ips, []net.IP{{8, 8, 8, 8}}); r != "" { + t.Fatal(r) + } + } + + dnsServer.Shutdown() +} + +func TestIPMatch(t *testing.T) { + port := udp.PickPort() + + dnsServer := dns.Server{ + Addr: "127.0.0.1:" + port.String(), + Net: "udp", + Handler: &staticHandler{}, + UDPSize: 1200, + } + + go dnsServer.ListenAndServe() + time.Sleep(time.Second) + + config := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&Config{ + NameServer: []*NameServer{ + // private dns, not match + { + Address: &net.Endpoint{ + Network: net.Network_UDP, + Address: &net.IPOrDomain{ + Address: &net.IPOrDomain_Ip{ + Ip: []byte{127, 0, 0, 1}, + }, + }, + Port: uint32(port), + }, + ExpectedGeoip: []*router.GeoIP{ + { + CountryCode: "local", + Cidr: []*router.CIDR{ + { + // inner ip, will not match + Ip: []byte{192, 168, 11, 1}, + Prefix: 32, + }, + }, + }, + }, + }, + // second dns, match ip + { + Address: &net.Endpoint{ + Network: net.Network_UDP, + Address: &net.IPOrDomain{ + Address: &net.IPOrDomain_Ip{ + Ip: []byte{127, 0, 0, 1}, + }, + }, + Port: uint32(port), + }, + ExpectedGeoip: []*router.GeoIP{ + { + CountryCode: "test", + Cidr: []*router.CIDR{ + { + Ip: []byte{8, 8, 8, 8}, + Prefix: 32, + }, + }, + }, + { + CountryCode: "test", + Cidr: []*router.CIDR{ + { + Ip: []byte{8, 8, 8, 4}, + Prefix: 32, + }, + }, + }, + }, + }, + }, + }), + serial.ToTypedMessage(&dispatcher.Config{}), + serial.ToTypedMessage(&proxyman.OutboundConfig{}), + serial.ToTypedMessage(&policy.Config{}), + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + v, err := core.New(config) + common.Must(err) + + client := v.GetFeature(feature_dns.ClientType()).(feature_dns.Client) + + startTime := time.Now() + + { + ips, _, err := client.LookupIP("google.com", feature_dns.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + FakeEnable: false, + }) + if err != nil { + t.Fatal("unexpected error: ", err) + } + + if r := cmp.Diff(ips, []net.IP{{8, 8, 8, 8}}); r != "" { + t.Fatal(r) + } + } + + endTime := time.Now() + if startTime.After(endTime.Add(time.Second * 2)) { + t.Error("DNS query doesn't finish in 2 seconds.") + } +} + +func TestLocalDomain(t *testing.T) { + port := udp.PickPort() + + dnsServer := dns.Server{ + Addr: "127.0.0.1:" + port.String(), + Net: "udp", + Handler: &staticHandler{}, + UDPSize: 1200, + } + + go dnsServer.ListenAndServe() + time.Sleep(time.Second) + + config := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&Config{ + NameServer: []*NameServer{ + { + Address: &net.Endpoint{ + Network: net.Network_UDP, + Address: &net.IPOrDomain{ + Address: &net.IPOrDomain_Ip{ + Ip: []byte{127, 0, 0, 1}, + }, + }, + Port: 9999, /* unreachable */ + }, + }, + { + Address: &net.Endpoint{ + Network: net.Network_UDP, + Address: &net.IPOrDomain{ + Address: &net.IPOrDomain_Ip{ + Ip: []byte{127, 0, 0, 1}, + }, + }, + Port: uint32(port), + }, + PrioritizedDomain: []*NameServer_PriorityDomain{ + // Equivalent of dotless:localhost + {Type: DomainMatchingType_Regex, Domain: "^[^.]*localhost[^.]*$"}, + }, + ExpectedGeoip: []*router.GeoIP{ + { // Will match localhost, localhost-a and localhost-b, + CountryCode: "local", + Cidr: []*router.CIDR{ + {Ip: []byte{127, 0, 0, 2}, Prefix: 32}, + {Ip: []byte{127, 0, 0, 3}, Prefix: 32}, + {Ip: []byte{127, 0, 0, 4}, Prefix: 32}, + }, + }, + }, + }, + { + Address: &net.Endpoint{ + Network: net.Network_UDP, + Address: &net.IPOrDomain{ + Address: &net.IPOrDomain_Ip{ + Ip: []byte{127, 0, 0, 1}, + }, + }, + Port: uint32(port), + }, + PrioritizedDomain: []*NameServer_PriorityDomain{ + // Equivalent of dotless: and domain:local + {Type: DomainMatchingType_Regex, Domain: "^[^.]*$"}, + {Type: DomainMatchingType_Subdomain, Domain: "local"}, + {Type: DomainMatchingType_Subdomain, Domain: "localdomain"}, + }, + }, + }, + StaticHosts: []*Config_HostMapping{ + { + Type: DomainMatchingType_Full, + Domain: "hostnamestatic", + Ip: [][]byte{{127, 0, 0, 53}}, + }, + { + Type: DomainMatchingType_Full, + Domain: "hostnamealias", + ProxiedDomain: "hostname.localdomain", + }, + }, + }), + serial.ToTypedMessage(&dispatcher.Config{}), + serial.ToTypedMessage(&proxyman.OutboundConfig{}), + serial.ToTypedMessage(&policy.Config{}), + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + v, err := core.New(config) + common.Must(err) + + client := v.GetFeature(feature_dns.ClientType()).(feature_dns.Client) + + startTime := time.Now() + + { // Will match dotless: + ips, _, err := client.LookupIP("hostname", feature_dns.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + FakeEnable: false, + }) + if err != nil { + t.Fatal("unexpected error: ", err) + } + + if r := cmp.Diff(ips, []net.IP{{127, 0, 0, 1}}); r != "" { + t.Fatal(r) + } + } + + { // Will match domain:local + ips, _, err := client.LookupIP("hostname.local", feature_dns.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + FakeEnable: false, + }) + if err != nil { + t.Fatal("unexpected error: ", err) + } + + if r := cmp.Diff(ips, []net.IP{{127, 0, 0, 1}}); r != "" { + t.Fatal(r) + } + } + + { // Will match static ip + ips, _, err := client.LookupIP("hostnamestatic", feature_dns.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + FakeEnable: false, + }) + if err != nil { + t.Fatal("unexpected error: ", err) + } + + if r := cmp.Diff(ips, []net.IP{{127, 0, 0, 53}}); r != "" { + t.Fatal(r) + } + } + + { // Will match domain replacing + ips, _, err := client.LookupIP("hostnamealias", feature_dns.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + FakeEnable: false, + }) + if err != nil { + t.Fatal("unexpected error: ", err) + } + + if r := cmp.Diff(ips, []net.IP{{127, 0, 0, 1}}); r != "" { + t.Fatal(r) + } + } + + { // Will match dotless:localhost, but not expectedIPs: 127.0.0.2, 127.0.0.3, then matches at dotless: + ips, _, err := client.LookupIP("localhost", feature_dns.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + FakeEnable: false, + }) + if err != nil { + t.Fatal("unexpected error: ", err) + } + + if r := cmp.Diff(ips, []net.IP{{127, 0, 0, 2}}); r != "" { + t.Fatal(r) + } + } + + { // Will match dotless:localhost, and expectedIPs: 127.0.0.2, 127.0.0.3 + ips, _, err := client.LookupIP("localhost-a", feature_dns.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + FakeEnable: false, + }) + if err != nil { + t.Fatal("unexpected error: ", err) + } + + if r := cmp.Diff(ips, []net.IP{{127, 0, 0, 3}}); r != "" { + t.Fatal(r) + } + } + + { // Will match dotless:localhost, and expectedIPs: 127.0.0.2, 127.0.0.3 + ips, _, err := client.LookupIP("localhost-b", feature_dns.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + FakeEnable: false, + }) + if err != nil { + t.Fatal("unexpected error: ", err) + } + + if r := cmp.Diff(ips, []net.IP{{127, 0, 0, 4}}); r != "" { + t.Fatal(r) + } + } + + { // Will match dotless: + ips, _, err := client.LookupIP("Mijia Cloud", feature_dns.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + FakeEnable: false, + }) + if err != nil { + t.Fatal("unexpected error: ", err) + } + + if r := cmp.Diff(ips, []net.IP{{127, 0, 0, 1}}); r != "" { + t.Fatal(r) + } + } + + endTime := time.Now() + if startTime.After(endTime.Add(time.Second * 2)) { + t.Error("DNS query doesn't finish in 2 seconds.") + } +} + +func TestMultiMatchPrioritizedDomain(t *testing.T) { + port := udp.PickPort() + + dnsServer := dns.Server{ + Addr: "127.0.0.1:" + port.String(), + Net: "udp", + Handler: &staticHandler{}, + UDPSize: 1200, + } + + go dnsServer.ListenAndServe() + time.Sleep(time.Second) + + config := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&Config{ + NameServer: []*NameServer{ + { + Address: &net.Endpoint{ + Network: net.Network_UDP, + Address: &net.IPOrDomain{ + Address: &net.IPOrDomain_Ip{ + Ip: []byte{127, 0, 0, 1}, + }, + }, + Port: 9999, /* unreachable */ + }, + }, + { + Address: &net.Endpoint{ + Network: net.Network_UDP, + Address: &net.IPOrDomain{ + Address: &net.IPOrDomain_Ip{ + Ip: []byte{127, 0, 0, 1}, + }, + }, + Port: uint32(port), + }, + PrioritizedDomain: []*NameServer_PriorityDomain{ + { + Type: DomainMatchingType_Subdomain, + Domain: "google.com", + }, + }, + ExpectedGeoip: []*router.GeoIP{ + { // Will only match 8.8.8.8 and 8.8.4.4 + Cidr: []*router.CIDR{ + {Ip: []byte{8, 8, 8, 8}, Prefix: 32}, + {Ip: []byte{8, 8, 4, 4}, Prefix: 32}, + }, + }, + }, + }, + { + Address: &net.Endpoint{ + Network: net.Network_UDP, + Address: &net.IPOrDomain{ + Address: &net.IPOrDomain_Ip{ + Ip: []byte{127, 0, 0, 1}, + }, + }, + Port: uint32(port), + }, + PrioritizedDomain: []*NameServer_PriorityDomain{ + { + Type: DomainMatchingType_Subdomain, + Domain: "google.com", + }, + }, + ExpectedGeoip: []*router.GeoIP{ + { // Will match 8.8.8.8 and 8.8.8.7, etc + Cidr: []*router.CIDR{ + {Ip: []byte{8, 8, 8, 7}, Prefix: 24}, + }, + }, + }, + }, + { + Address: &net.Endpoint{ + Network: net.Network_UDP, + Address: &net.IPOrDomain{ + Address: &net.IPOrDomain_Ip{ + Ip: []byte{127, 0, 0, 1}, + }, + }, + Port: uint32(port), + }, + PrioritizedDomain: []*NameServer_PriorityDomain{ + { + Type: DomainMatchingType_Subdomain, + Domain: "api.google.com", + }, + }, + ExpectedGeoip: []*router.GeoIP{ + { // Will only match 8.8.7.7 (api.google.com) + Cidr: []*router.CIDR{ + {Ip: []byte{8, 8, 7, 7}, Prefix: 32}, + }, + }, + }, + }, + { + Address: &net.Endpoint{ + Network: net.Network_UDP, + Address: &net.IPOrDomain{ + Address: &net.IPOrDomain_Ip{ + Ip: []byte{127, 0, 0, 1}, + }, + }, + Port: uint32(port), + }, + PrioritizedDomain: []*NameServer_PriorityDomain{ + { + Type: DomainMatchingType_Full, + Domain: "v2.api.google.com", + }, + }, + ExpectedGeoip: []*router.GeoIP{ + { // Will only match 8.8.7.8 (v2.api.google.com) + Cidr: []*router.CIDR{ + {Ip: []byte{8, 8, 7, 8}, Prefix: 32}, + }, + }, + }, + }, + }, + }), + serial.ToTypedMessage(&dispatcher.Config{}), + serial.ToTypedMessage(&proxyman.OutboundConfig{}), + serial.ToTypedMessage(&policy.Config{}), + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + v, err := core.New(config) + common.Must(err) + + client := v.GetFeature(feature_dns.ClientType()).(feature_dns.Client) + + startTime := time.Now() + + { // Will match server 1,2 and server 1 returns expected ip + ips, _, err := client.LookupIP("google.com", feature_dns.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + FakeEnable: false, + }) + if err != nil { + t.Fatal("unexpected error: ", err) + } + + if r := cmp.Diff(ips, []net.IP{{8, 8, 8, 8}}); r != "" { + t.Fatal(r) + } + } + + { // 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{ + IPv4Enable: true, + IPv6Enable: false, + FakeEnable: false, + }) + if err != nil { + t.Fatal("unexpected error: ", err) + } + + if r := cmp.Diff(ips, []net.IP{{8, 8, 8, 7}}); r != "" { + t.Fatal(r) + } + } + + { // Will match server 3,1,2 and server 3 returns expected one + ips, _, err := client.LookupIP("api.google.com", feature_dns.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + FakeEnable: false, + }) + if err != nil { + t.Fatal("unexpected error: ", err) + } + + if r := cmp.Diff(ips, []net.IP{{8, 8, 7, 7}}); r != "" { + t.Fatal(r) + } + } + + { // Will match server 4,3,1,2 and server 4 returns expected one + ips, _, err := client.LookupIP("v2.api.google.com", feature_dns.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + FakeEnable: false, + }) + if err != nil { + t.Fatal("unexpected error: ", err) + } + + if r := cmp.Diff(ips, []net.IP{{8, 8, 7, 8}}); r != "" { + t.Fatal(r) + } + } + + endTime := time.Now() + if startTime.After(endTime.Add(time.Second * 2)) { + t.Error("DNS query doesn't finish in 2 seconds.") + } +} diff --git a/subproject/Xray-core-main/app/dns/dnscommon.go b/subproject/Xray-core-main/app/dns/dnscommon.go new file mode 100644 index 00000000..2092a2fd --- /dev/null +++ b/subproject/Xray-core-main/app/dns/dnscommon.go @@ -0,0 +1,263 @@ +package dns + +import ( + "context" + "encoding/binary" + "math" + "strings" + "time" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/log" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/core" + dns_feature "github.com/xtls/xray-core/features/dns" + + "golang.org/x/net/dns/dnsmessage" +) + +// Fqdn normalizes domain make sure it ends with '.' +// case-sensitive +func Fqdn(domain string) string { + if len(domain) > 0 && strings.HasSuffix(domain, ".") { + return domain + } + return domain + "." +} + +type record struct { + A *IPRecord + AAAA *IPRecord +} + +// IPRecord is a cacheable item for a resolved domain +type IPRecord struct { + ReqID uint16 + IP []net.IP + Expire time.Time + RCode dnsmessage.RCode + RawHeader *dnsmessage.Header +} + +func (r *IPRecord) getIPs() ([]net.IP, int32, error) { + if r == nil { + return nil, 0, errRecordNotFound + } + + untilExpire := time.Until(r.Expire).Seconds() + ttl := int32(math.Ceil(untilExpire)) + + if r.RCode != dnsmessage.RCodeSuccess { + return nil, ttl, dns_feature.RCodeError(r.RCode) + } + if len(r.IP) == 0 { + return nil, ttl, dns_feature.ErrEmptyResponse + } + + return r.IP, ttl, nil +} + +var errRecordNotFound = errors.New("record not found") + +type dnsRequest struct { + reqType dnsmessage.Type + domain string + start time.Time + expire time.Time + msg *dnsmessage.Message +} + +func genEDNS0Options(clientIP net.IP, padding int) *dnsmessage.Resource { + if len(clientIP) == 0 && padding == 0 { + return nil + } + + const EDNS0SUBNET = 0x8 + const EDNS0PADDING = 0xc + + opt := new(dnsmessage.Resource) + common.Must(opt.Header.SetEDNS0(1350, 0xfe00, true)) + body := dnsmessage.OPTResource{} + opt.Body = &body + + if len(clientIP) != 0 { + var netmask int + 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, + Data: b, + }) + } + + if padding != 0 { + body.Options = append(body.Options, + dnsmessage.Option{ + Code: EDNS0PADDING, + Data: make([]byte, padding), + }) + } + + return opt +} + +func buildReqMsgs(domain string, option dns_feature.IPOption, reqIDGen func() uint16, reqOpts *dnsmessage.Resource) []*dnsRequest { + qA := dnsmessage.Question{ + Name: dnsmessage.MustNewName(domain), + Type: dnsmessage.TypeA, + Class: dnsmessage.ClassINET, + } + + qAAAA := dnsmessage.Question{ + Name: dnsmessage.MustNewName(domain), + Type: dnsmessage.TypeAAAA, + Class: dnsmessage.ClassINET, + } + + var reqs []*dnsRequest + now := time.Now() + + if option.IPv4Enable { + msg := new(dnsmessage.Message) + msg.Header.ID = reqIDGen() + msg.Header.RecursionDesired = true + msg.Questions = []dnsmessage.Question{qA} + if reqOpts != nil { + msg.Additionals = append(msg.Additionals, *reqOpts) + } + reqs = append(reqs, &dnsRequest{ + reqType: dnsmessage.TypeA, + domain: domain, + start: now, + msg: msg, + }) + } + + if option.IPv6Enable { + msg := new(dnsmessage.Message) + msg.Header.ID = reqIDGen() + msg.Header.RecursionDesired = true + msg.Questions = []dnsmessage.Question{qAAAA} + if reqOpts != nil { + msg.Additionals = append(msg.Additionals, *reqOpts) + } + reqs = append(reqs, &dnsRequest{ + reqType: dnsmessage.TypeAAAA, + domain: domain, + start: now, + msg: msg, + }) + } + + return reqs +} + +// parseResponse parses DNS answers from the returned payload +func parseResponse(payload []byte) (*IPRecord, error) { + var parser dnsmessage.Parser + h, err := parser.Start(payload) + if err != nil { + return nil, errors.New("failed to parse DNS response").Base(err).AtWarning() + } + if err := parser.SkipAllQuestions(); err != nil { + return nil, errors.New("failed to skip questions in DNS response").Base(err).AtWarning() + } + + now := time.Now() + ipRecord := &IPRecord{ + ReqID: h.ID, + RCode: h.RCode, + Expire: now.Add(time.Second * dns_feature.DefaultTTL), + RawHeader: &h, + } + +L: + for { + ah, err := parser.AnswerHeader() + if err != nil { + if err != dnsmessage.ErrSectionDone { + errors.LogInfoInner(context.Background(), err, "failed to parse answer section for domain: ", ah.Name.String()) + } + break + } + + ttl := ah.TTL + if ttl == 0 { + ttl = 1 + } + expire := now.Add(time.Duration(ttl) * time.Second) + if ipRecord.Expire.After(expire) { + ipRecord.Expire = expire + } + + switch ah.Type { + case dnsmessage.TypeA: + ans, err := parser.AResource() + if err != nil { + errors.LogInfoInner(context.Background(), err, "failed to parse A record for domain: ", ah.Name) + break L + } + ipRecord.IP = append(ipRecord.IP, net.IPAddress(ans.A[:]).IP()) + case dnsmessage.TypeAAAA: + ans, err := parser.AAAAResource() + if err != nil { + errors.LogInfoInner(context.Background(), err, "failed to parse AAAA record for domain: ", ah.Name) + break L + } + newIP := net.IPAddress(ans.AAAA[:]).IP() + if len(newIP) == net.IPv6len { + ipRecord.IP = append(ipRecord.IP, newIP) + } + default: + if err := parser.SkipAnswer(); err != nil { + errors.LogInfoInner(context.Background(), err, "failed to skip answer") + break L + } + continue + } + } + + return ipRecord, nil +} + +// toDnsContext create a new background context with parent inbound, session and dns log +func toDnsContext(ctx context.Context, addr string) context.Context { + dnsCtx := core.ToBackgroundDetachedContext(ctx) + if inbound := session.InboundFromContext(ctx); inbound != nil { + dnsCtx = session.ContextWithInbound(dnsCtx, inbound) + } + dnsCtx = session.ContextWithContent(dnsCtx, session.ContentFromContext(ctx)) + dnsCtx = log.ContextWithAccessMessage(dnsCtx, &log.AccessMessage{ + From: "DNS", + To: addr, + Status: log.AccessAccepted, + Reason: "", + }) + return dnsCtx +} diff --git a/subproject/Xray-core-main/app/dns/dnscommon_test.go b/subproject/Xray-core-main/app/dns/dnscommon_test.go new file mode 100644 index 00000000..e77117c6 --- /dev/null +++ b/subproject/Xray-core-main/app/dns/dnscommon_test.go @@ -0,0 +1,185 @@ +package dns + +import ( + "math/rand" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/miekg/dns" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/net" + dns_feature "github.com/xtls/xray-core/features/dns" + "golang.org/x/net/dns/dnsmessage" +) + +func Test_parseResponse(t *testing.T) { + var p [][]byte + + ans := new(dns.Msg) + ans.Id = 0 + p = append(p, common.Must2(ans.Pack())) + + p = append(p, []byte{}) + + ans = new(dns.Msg) + ans.Id = 1 + ans.Answer = append(ans.Answer, + common.Must2(dns.NewRR("google.com. IN CNAME m.test.google.com")), + common.Must2(dns.NewRR("google.com. IN CNAME fake.google.com")), + common.Must2(dns.NewRR("google.com. IN A 8.8.8.8")), + common.Must2(dns.NewRR("google.com. IN A 8.8.4.4")), + ) + p = append(p, common.Must2(ans.Pack())) + + ans = new(dns.Msg) + ans.Id = 2 + ans.Answer = append(ans.Answer, + common.Must2(dns.NewRR("google.com. IN CNAME m.test.google.com")), + common.Must2(dns.NewRR("google.com. IN CNAME fake.google.com")), + common.Must2(dns.NewRR("google.com. IN CNAME m.test.google.com")), + common.Must2(dns.NewRR("google.com. IN CNAME test.google.com")), + common.Must2(dns.NewRR("google.com. IN AAAA 2001:4860:4860::8888")), + common.Must2(dns.NewRR("google.com. IN AAAA 2001:4860:4860::8844")), + ) + p = append(p, common.Must2(ans.Pack())) + + tests := []struct { + name string + want *IPRecord + wantErr bool + }{ + { + "empty", + &IPRecord{0, []net.IP(nil), time.Time{}, dnsmessage.RCodeSuccess, nil}, + false, + }, + { + "error", + nil, + true, + }, + { + "a record", + &IPRecord{ + 1, + []net.IP{net.ParseIP("8.8.8.8"), net.ParseIP("8.8.4.4")}, + time.Time{}, + dnsmessage.RCodeSuccess, + nil, + }, + false, + }, + { + "aaaa record", + &IPRecord{2, []net.IP{net.ParseIP("2001:4860:4860::8888"), net.ParseIP("2001:4860:4860::8844")}, time.Time{}, dnsmessage.RCodeSuccess, nil}, + false, + }, + } + for i, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parseResponse(p[i]) + if (err != nil) != tt.wantErr { + t.Errorf("handleResponse() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if got != nil { + // reset the time and RawHeader + got.Expire = time.Time{} + got.RawHeader = nil + } + if cmp.Diff(got, tt.want) != "" { + t.Error(cmp.Diff(got, tt.want)) + // t.Errorf("handleResponse() = %#v, want %#v", got, tt.want) + } + }) + } +} + +func Test_buildReqMsgs(t *testing.T) { + stubID := func() uint16 { + return uint16(rand.Uint32()) + } + type args struct { + domain string + option dns_feature.IPOption + reqOpts *dnsmessage.Resource + } + tests := []struct { + name string + args args + want int + }{ + {"dual stack", args{"test.com", dns_feature.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + FakeEnable: false, + }, nil}, 2}, + {"ipv4 only", args{"test.com", dns_feature.IPOption{ + IPv4Enable: true, + IPv6Enable: false, + FakeEnable: false, + }, nil}, 1}, + {"ipv6 only", args{"test.com", dns_feature.IPOption{ + IPv4Enable: false, + IPv6Enable: true, + FakeEnable: false, + }, nil}, 1}, + {"none/error", args{"test.com", dns_feature.IPOption{ + IPv4Enable: false, + IPv6Enable: false, + FakeEnable: false, + }, nil}, 0}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := buildReqMsgs(tt.args.domain, tt.args.option, stubID, tt.args.reqOpts); !(len(got) == tt.want) { + t.Errorf("buildReqMsgs() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_genEDNS0Options(t *testing.T) { + type args struct { + clientIP net.IP + } + tests := []struct { + name string + args args + want *dnsmessage.Resource + }{ + // TODO: Add test cases. + {"ipv4", args{net.ParseIP("4.3.2.1")}, nil}, + {"ipv6", args{net.ParseIP("2001::4321")}, nil}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := genEDNS0Options(tt.args.clientIP, 0); got == nil { + t.Errorf("genEDNS0Options() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestFqdn(t *testing.T) { + type args struct { + domain string + } + tests := []struct { + name string + args args + want string + }{ + {"with fqdn", args{"www.example.com."}, "www.example.com."}, + {"without fqdn", args{"www.example.com"}, "www.example.com."}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := Fqdn(tt.args.domain); got != tt.want { + t.Errorf("Fqdn() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/subproject/Xray-core-main/app/dns/fakedns/fake.go b/subproject/Xray-core-main/app/dns/fakedns/fake.go new file mode 100644 index 00000000..33bf63cc --- /dev/null +++ b/subproject/Xray-core-main/app/dns/fakedns/fake.go @@ -0,0 +1,250 @@ +package fakedns + +import ( + "context" + "math" + "math/big" + "sync" + "time" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/cache" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/features/dns" +) + +type Holder struct { + domainToIP cache.Lru + ipRange *net.IPNet + mu *sync.Mutex + + config *FakeDnsPool +} + +func (fkdns *Holder) IsIPInIPPool(ip net.Address) bool { + if ip.Family().IsDomain() { + return false + } + return fkdns.ipRange.Contains(ip.IP()) +} + +func (fkdns *Holder) GetFakeIPForDomain3(domain string, ipv4, ipv6 bool) []net.Address { + isIPv6 := fkdns.ipRange.IP.To4() == nil + if (isIPv6 && ipv6) || (!isIPv6 && ipv4) { + return fkdns.GetFakeIPForDomain(domain) + } + return []net.Address{} +} + +func (*Holder) Type() interface{} { + return (*dns.FakeDNSEngine)(nil) +} + +func (fkdns *Holder) Start() error { + if fkdns.config != nil && fkdns.config.IpPool != "" && fkdns.config.LruSize != 0 { + return fkdns.initializeFromConfig() + } + return errors.New("invalid fakeDNS setting") +} + +func (fkdns *Holder) Close() error { + fkdns.domainToIP = nil + fkdns.ipRange = nil + fkdns.mu = nil + return nil +} + +func NewFakeDNSHolder() (*Holder, error) { + var fkdns *Holder + var err error + + if fkdns, err = NewFakeDNSHolderConfigOnly(nil); err != nil { + return nil, errors.New("Unable to create Fake Dns Engine").Base(err).AtError() + } + err = fkdns.initialize(dns.FakeIPv4Pool, 65535) + if err != nil { + return nil, err + } + return fkdns, nil +} + +func NewFakeDNSHolderConfigOnly(conf *FakeDnsPool) (*Holder, error) { + return &Holder{nil, nil, nil, conf}, nil +} + +func (fkdns *Holder) initializeFromConfig() error { + return fkdns.initialize(fkdns.config.IpPool, int(fkdns.config.LruSize)) +} + +func (fkdns *Holder) initialize(ipPoolCidr string, lruSize int) error { + var ipRange *net.IPNet + var err error + + if _, ipRange, err = net.ParseCIDR(ipPoolCidr); err != nil { + return errors.New("Unable to parse CIDR for Fake DNS IP assignment").Base(err).AtError() + } + + ones, bits := ipRange.Mask.Size() + rooms := bits - ones + if math.Log2(float64(lruSize)) >= float64(rooms) { + return errors.New("LRU size is bigger than subnet size").AtError() + } + fkdns.domainToIP = cache.NewLru(lruSize) + fkdns.ipRange = ipRange + fkdns.mu = new(sync.Mutex) + return nil +} + +// GetFakeIPForDomain checks and generates a fake IP for a domain name +func (fkdns *Holder) GetFakeIPForDomain(domain string) []net.Address { + fkdns.mu.Lock() + defer fkdns.mu.Unlock() + if v, ok := fkdns.domainToIP.Get(domain); ok { + return []net.Address{v.(net.Address)} + } + currentTimeMillis := uint64(time.Now().UnixNano() / 1e6) + ones, bits := fkdns.ipRange.Mask.Size() + rooms := bits - ones + if rooms < 64 { + currentTimeMillis %= (uint64(1) << rooms) + } + bigIntIP := big.NewInt(0).SetBytes(fkdns.ipRange.IP) + bigIntIP = bigIntIP.Add(bigIntIP, new(big.Int).SetUint64(currentTimeMillis)) + var ip net.Address + for { + ip = net.IPAddress(bigIntIP.Bytes()) + + // if we run for a long time, we may go back to beginning and start seeing the IP in use + if _, ok := fkdns.domainToIP.PeekKeyFromValue(ip); !ok { + break + } + + bigIntIP = bigIntIP.Add(bigIntIP, big.NewInt(1)) + if !fkdns.ipRange.Contains(bigIntIP.Bytes()) { + bigIntIP = big.NewInt(0).SetBytes(fkdns.ipRange.IP) + } + } + fkdns.domainToIP.Put(domain, ip) + return []net.Address{ip} +} + +// GetDomainFromFakeDNS checks if an IP is a fake IP and have corresponding domain name +func (fkdns *Holder) GetDomainFromFakeDNS(ip net.Address) string { + if !ip.Family().IsIP() || !fkdns.ipRange.Contains(ip.IP()) { + return "" + } + if k, ok := fkdns.domainToIP.GetKeyFromValue(ip); ok { + return k.(string) + } + errors.LogInfo(context.Background(), "A fake ip request to ", ip, ", however there is no matching domain name in fake DNS") + return "" +} + +type HolderMulti struct { + holders []*Holder + + config *FakeDnsPoolMulti +} + +func (h *HolderMulti) IsIPInIPPool(ip net.Address) bool { + if ip.Family().IsDomain() { + return false + } + for _, v := range h.holders { + if v.IsIPInIPPool(ip) { + return true + } + } + return false +} + +func (h *HolderMulti) GetFakeIPForDomain3(domain string, ipv4, ipv6 bool) []net.Address { + var ret []net.Address + for _, v := range h.holders { + ret = append(ret, v.GetFakeIPForDomain3(domain, ipv4, ipv6)...) + } + return ret +} + +func (h *HolderMulti) GetFakeIPForDomain(domain string) []net.Address { + var ret []net.Address + for _, v := range h.holders { + ret = append(ret, v.GetFakeIPForDomain(domain)...) + } + return ret +} + +func (h *HolderMulti) GetDomainFromFakeDNS(ip net.Address) string { + for _, v := range h.holders { + if domain := v.GetDomainFromFakeDNS(ip); domain != "" { + return domain + } + } + return "" +} + +func (h *HolderMulti) Type() interface{} { + return (*dns.FakeDNSEngine)(nil) +} + +func (h *HolderMulti) Start() error { + for _, v := range h.holders { + if v.config != nil && v.config.IpPool != "" && v.config.LruSize != 0 { + if err := v.Start(); err != nil { + return errors.New("Cannot start all fake dns pools").Base(err) + } + } else { + return errors.New("invalid fakeDNS setting") + } + } + return nil +} + +func (h *HolderMulti) Close() error { + for _, v := range h.holders { + if err := v.Close(); err != nil { + return errors.New("Cannot close all fake dns pools").Base(err) + } + } + return nil +} + +func (h *HolderMulti) createHolderGroups() error { + for _, v := range h.config.Pools { + holder, err := NewFakeDNSHolderConfigOnly(v) + if err != nil { + return err + } + h.holders = append(h.holders, holder) + } + return nil +} + +func NewFakeDNSHolderMulti(conf *FakeDnsPoolMulti) (*HolderMulti, error) { + holderMulti := &HolderMulti{nil, conf} + if err := holderMulti.createHolderGroups(); err != nil { + return nil, err + } + return holderMulti, nil +} + +func init() { + common.Must(common.RegisterConfig((*FakeDnsPool)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + var f *Holder + var err error + if f, err = NewFakeDNSHolderConfigOnly(config.(*FakeDnsPool)); err != nil { + return nil, err + } + return f, nil + })) + + common.Must(common.RegisterConfig((*FakeDnsPoolMulti)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + var f *HolderMulti + var err error + if f, err = NewFakeDNSHolderMulti(config.(*FakeDnsPoolMulti)); err != nil { + return nil, err + } + return f, nil + })) +} diff --git a/subproject/Xray-core-main/app/dns/fakedns/fakedns.go b/subproject/Xray-core-main/app/dns/fakedns/fakedns.go new file mode 100644 index 00000000..05da03a5 --- /dev/null +++ b/subproject/Xray-core-main/app/dns/fakedns/fakedns.go @@ -0,0 +1 @@ +package fakedns diff --git a/subproject/Xray-core-main/app/dns/fakedns/fakedns.pb.go b/subproject/Xray-core-main/app/dns/fakedns/fakedns.pb.go new file mode 100644 index 00000000..01ba9de3 --- /dev/null +++ b/subproject/Xray-core-main/app/dns/fakedns/fakedns.pb.go @@ -0,0 +1,180 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: app/dns/fakedns/fakedns.proto + +package fakedns + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type FakeDnsPool struct { + state protoimpl.MessageState `protogen:"open.v1"` + IpPool string `protobuf:"bytes,1,opt,name=ip_pool,json=ipPool,proto3" json:"ip_pool,omitempty"` //CIDR of IP pool used as fake DNS IP + LruSize int64 `protobuf:"varint,2,opt,name=lruSize,proto3" json:"lruSize,omitempty"` //Size of Pool for remembering relationship between domain name and IP address + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FakeDnsPool) Reset() { + *x = FakeDnsPool{} + mi := &file_app_dns_fakedns_fakedns_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FakeDnsPool) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FakeDnsPool) ProtoMessage() {} + +func (x *FakeDnsPool) ProtoReflect() protoreflect.Message { + mi := &file_app_dns_fakedns_fakedns_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FakeDnsPool.ProtoReflect.Descriptor instead. +func (*FakeDnsPool) Descriptor() ([]byte, []int) { + return file_app_dns_fakedns_fakedns_proto_rawDescGZIP(), []int{0} +} + +func (x *FakeDnsPool) GetIpPool() string { + if x != nil { + return x.IpPool + } + return "" +} + +func (x *FakeDnsPool) GetLruSize() int64 { + if x != nil { + return x.LruSize + } + return 0 +} + +type FakeDnsPoolMulti struct { + state protoimpl.MessageState `protogen:"open.v1"` + Pools []*FakeDnsPool `protobuf:"bytes,1,rep,name=pools,proto3" json:"pools,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FakeDnsPoolMulti) Reset() { + *x = FakeDnsPoolMulti{} + mi := &file_app_dns_fakedns_fakedns_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FakeDnsPoolMulti) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FakeDnsPoolMulti) ProtoMessage() {} + +func (x *FakeDnsPoolMulti) ProtoReflect() protoreflect.Message { + mi := &file_app_dns_fakedns_fakedns_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FakeDnsPoolMulti.ProtoReflect.Descriptor instead. +func (*FakeDnsPoolMulti) Descriptor() ([]byte, []int) { + return file_app_dns_fakedns_fakedns_proto_rawDescGZIP(), []int{1} +} + +func (x *FakeDnsPoolMulti) GetPools() []*FakeDnsPool { + if x != nil { + return x.Pools + } + return nil +} + +var File_app_dns_fakedns_fakedns_proto protoreflect.FileDescriptor + +const file_app_dns_fakedns_fakedns_proto_rawDesc = "" + + "\n" + + "\x1dapp/dns/fakedns/fakedns.proto\x12\x14xray.app.dns.fakedns\"@\n" + + "\vFakeDnsPool\x12\x17\n" + + "\aip_pool\x18\x01 \x01(\tR\x06ipPool\x12\x18\n" + + "\alruSize\x18\x02 \x01(\x03R\alruSize\"K\n" + + "\x10FakeDnsPoolMulti\x127\n" + + "\x05pools\x18\x01 \x03(\v2!.xray.app.dns.fakedns.FakeDnsPoolR\x05poolsB^\n" + + "\x18com.xray.app.dns.fakednsP\x01Z)github.com/xtls/xray-core/app/dns/fakedns\xaa\x02\x14Xray.App.Dns.Fakednsb\x06proto3" + +var ( + file_app_dns_fakedns_fakedns_proto_rawDescOnce sync.Once + file_app_dns_fakedns_fakedns_proto_rawDescData []byte +) + +func file_app_dns_fakedns_fakedns_proto_rawDescGZIP() []byte { + file_app_dns_fakedns_fakedns_proto_rawDescOnce.Do(func() { + file_app_dns_fakedns_fakedns_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_app_dns_fakedns_fakedns_proto_rawDesc), len(file_app_dns_fakedns_fakedns_proto_rawDesc))) + }) + return file_app_dns_fakedns_fakedns_proto_rawDescData +} + +var file_app_dns_fakedns_fakedns_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_app_dns_fakedns_fakedns_proto_goTypes = []any{ + (*FakeDnsPool)(nil), // 0: xray.app.dns.fakedns.FakeDnsPool + (*FakeDnsPoolMulti)(nil), // 1: xray.app.dns.fakedns.FakeDnsPoolMulti +} +var file_app_dns_fakedns_fakedns_proto_depIdxs = []int32{ + 0, // 0: xray.app.dns.fakedns.FakeDnsPoolMulti.pools:type_name -> xray.app.dns.fakedns.FakeDnsPool + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_app_dns_fakedns_fakedns_proto_init() } +func file_app_dns_fakedns_fakedns_proto_init() { + if File_app_dns_fakedns_fakedns_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_app_dns_fakedns_fakedns_proto_rawDesc), len(file_app_dns_fakedns_fakedns_proto_rawDesc)), + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_app_dns_fakedns_fakedns_proto_goTypes, + DependencyIndexes: file_app_dns_fakedns_fakedns_proto_depIdxs, + MessageInfos: file_app_dns_fakedns_fakedns_proto_msgTypes, + }.Build() + File_app_dns_fakedns_fakedns_proto = out.File + file_app_dns_fakedns_fakedns_proto_goTypes = nil + file_app_dns_fakedns_fakedns_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/app/dns/fakedns/fakedns.proto b/subproject/Xray-core-main/app/dns/fakedns/fakedns.proto new file mode 100644 index 00000000..aa168aaf --- /dev/null +++ b/subproject/Xray-core-main/app/dns/fakedns/fakedns.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +package xray.app.dns.fakedns; +option csharp_namespace = "Xray.App.Dns.Fakedns"; +option go_package = "github.com/xtls/xray-core/app/dns/fakedns"; +option java_package = "com.xray.app.dns.fakedns"; +option java_multiple_files = true; + +message FakeDnsPool{ + string ip_pool = 1; //CIDR of IP pool used as fake DNS IP + int64 lruSize = 2; //Size of Pool for remembering relationship between domain name and IP address +} + +message FakeDnsPoolMulti{ + repeated FakeDnsPool pools = 1; +} \ No newline at end of file diff --git a/subproject/Xray-core-main/app/dns/fakedns/fakedns_test.go b/subproject/Xray-core-main/app/dns/fakedns/fakedns_test.go new file mode 100644 index 00000000..f9a8449c --- /dev/null +++ b/subproject/Xray-core-main/app/dns/fakedns/fakedns_test.go @@ -0,0 +1,205 @@ +package fakedns + +import ( + "strconv" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/uuid" + "github.com/xtls/xray-core/features/dns" + "golang.org/x/sync/errgroup" +) + +var ipPrefix = "198.1" + +func TestNewFakeDnsHolder(_ *testing.T) { + _, err := NewFakeDNSHolder() + common.Must(err) +} + +func TestFakeDnsHolderCreateMapping(t *testing.T) { + fkdns, err := NewFakeDNSHolder() + common.Must(err) + + addr := fkdns.GetFakeIPForDomain("fakednstest.example.com") + assert.Equal(t, ipPrefix, addr[0].IP().String()[0:len(ipPrefix)]) +} + +func TestFakeDnsHolderCreateMappingMany(t *testing.T) { + fkdns, err := NewFakeDNSHolder() + common.Must(err) + + addr := fkdns.GetFakeIPForDomain("fakednstest.example.com") + assert.Equal(t, ipPrefix, addr[0].IP().String()[0:len(ipPrefix)]) + + addr2 := fkdns.GetFakeIPForDomain("fakednstest2.example.com") + assert.Equal(t, ipPrefix, addr2[0].IP().String()[0:len(ipPrefix)]) + assert.NotEqual(t, addr[0].IP().String(), addr2[0].IP().String()) +} + +func TestFakeDnsHolderCreateMappingManyAndResolve(t *testing.T) { + fkdns, err := NewFakeDNSHolder() + common.Must(err) + + addr := fkdns.GetFakeIPForDomain("fakednstest.example.com") + addr2 := fkdns.GetFakeIPForDomain("fakednstest2.example.com") + + { + result := fkdns.GetDomainFromFakeDNS(addr[0]) + assert.Equal(t, "fakednstest.example.com", result) + } + + { + result := fkdns.GetDomainFromFakeDNS(addr2[0]) + assert.Equal(t, "fakednstest2.example.com", result) + } +} + +func TestFakeDnsHolderCreateMappingManySingleDomain(t *testing.T) { + fkdns, err := NewFakeDNSHolder() + common.Must(err) + + addr := fkdns.GetFakeIPForDomain("fakednstest.example.com") + addr2 := fkdns.GetFakeIPForDomain("fakednstest.example.com") + assert.Equal(t, addr[0].IP().String(), addr2[0].IP().String()) +} + +func TestGetFakeIPForDomainConcurrently(t *testing.T) { + fkdns, err := NewFakeDNSHolder() + common.Must(err) + + total := 200 + addr := make([][]net.Address, total) + var errg errgroup.Group + for i := 0; i < total; i++ { + errg.Go(testGetFakeIP(i, addr, fkdns)) + } + errg.Wait() + for i := 0; i < total; i++ { + for j := i + 1; j < total; j++ { + assert.NotEqual(t, addr[i][0].IP().String(), addr[j][0].IP().String()) + } + } +} + +func testGetFakeIP(index int, addr [][]net.Address, fkdns *Holder) func() error { + return func() error { + addr[index] = fkdns.GetFakeIPForDomain("fakednstest" + strconv.Itoa(index) + ".example.com") + return nil + } +} + +func TestFakeDnsHolderCreateMappingAndRollOver(t *testing.T) { + fkdns, err := NewFakeDNSHolderConfigOnly(&FakeDnsPool{ + IpPool: dns.FakeIPv4Pool, + LruSize: 256, + }) + common.Must(err) + + err = fkdns.Start() + + common.Must(err) + + addr := fkdns.GetFakeIPForDomain("fakednstest.example.com") + addr2 := fkdns.GetFakeIPForDomain("fakednstest2.example.com") + + for i := 0; i <= 8192; i++ { + { + result := fkdns.GetDomainFromFakeDNS(addr[0]) + assert.Equal(t, "fakednstest.example.com", result) + } + + { + result := fkdns.GetDomainFromFakeDNS(addr2[0]) + assert.Equal(t, "fakednstest2.example.com", result) + } + + { + uuid := uuid.New() + domain := uuid.String() + ".fakednstest.example.com" + tempAddr := fkdns.GetFakeIPForDomain(domain) + rsaddr := tempAddr[0].IP().String() + + result := fkdns.GetDomainFromFakeDNS(net.ParseAddress(rsaddr)) + assert.Equal(t, domain, result) + } + } +} + +func TestFakeDNSMulti(t *testing.T) { + fakeMulti, err := NewFakeDNSHolderMulti(&FakeDnsPoolMulti{ + Pools: []*FakeDnsPool{{ + IpPool: "240.0.0.0/12", + LruSize: 256, + }, { + IpPool: "fddd:c5b4:ff5f:f4f0::/64", + LruSize: 256, + }}, + }, + ) + common.Must(err) + + err = fakeMulti.Start() + + common.Must(err) + + assert.Nil(t, err, "Should not throw error") + _ = fakeMulti + + t.Run("checkInRange", func(t *testing.T) { + t.Run("ipv4", func(t *testing.T) { + inPool := fakeMulti.IsIPInIPPool(net.IPAddress([]byte{240, 0, 0, 5})) + assert.True(t, inPool) + }) + t.Run("ipv6", func(t *testing.T) { + ip, err := net.ResolveIPAddr("ip", "fddd:c5b4:ff5f:f4f0::5") + assert.Nil(t, err) + inPool := fakeMulti.IsIPInIPPool(net.IPAddress(ip.IP)) + assert.True(t, inPool) + }) + t.Run("ipv4_inverse", func(t *testing.T) { + inPool := fakeMulti.IsIPInIPPool(net.IPAddress([]byte{241, 0, 0, 5})) + assert.False(t, inPool) + }) + t.Run("ipv6_inverse", func(t *testing.T) { + ip, err := net.ResolveIPAddr("ip", "fcdd:c5b4:ff5f:f4f0::5") + assert.Nil(t, err) + inPool := fakeMulti.IsIPInIPPool(net.IPAddress(ip.IP)) + assert.False(t, inPool) + }) + }) + + t.Run("allocateTwoAddressForTwoPool", func(t *testing.T) { + address := fakeMulti.GetFakeIPForDomain("fakednstest.example.com") + assert.Len(t, address, 2, "should be 2 address one for each pool") + t.Run("eachOfThemShouldResolve:0", func(t *testing.T) { + domain := fakeMulti.GetDomainFromFakeDNS(address[0]) + assert.Equal(t, "fakednstest.example.com", domain) + }) + t.Run("eachOfThemShouldResolve:1", func(t *testing.T) { + domain := fakeMulti.GetDomainFromFakeDNS(address[1]) + assert.Equal(t, "fakednstest.example.com", domain) + }) + }) + + t.Run("understandIPTypeSelector", func(t *testing.T) { + t.Run("ipv4", func(t *testing.T) { + address := fakeMulti.GetFakeIPForDomain3("fakednstestipv4.example.com", true, false) + assert.Len(t, address, 1, "should be 1 address") + assert.True(t, address[0].Family().IsIPv4()) + }) + t.Run("ipv6", func(t *testing.T) { + address := fakeMulti.GetFakeIPForDomain3("fakednstestipv6.example.com", false, true) + assert.Len(t, address, 1, "should be 1 address") + assert.True(t, address[0].Family().IsIPv6()) + }) + t.Run("ipv46", func(t *testing.T) { + address := fakeMulti.GetFakeIPForDomain3("fakednstestipv46.example.com", true, true) + assert.Len(t, address, 2, "should be 2 address") + assert.True(t, address[0].Family().IsIPv4()) + assert.True(t, address[1].Family().IsIPv6()) + }) + }) +} diff --git a/subproject/Xray-core-main/app/dns/hosts.go b/subproject/Xray-core-main/app/dns/hosts.go new file mode 100644 index 00000000..fab08d54 --- /dev/null +++ b/subproject/Xray-core-main/app/dns/hosts.go @@ -0,0 +1,173 @@ +package dns + +import ( + "context" + "runtime" + "strconv" + + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/strmatcher" + "github.com/xtls/xray-core/features/dns" +) + +// StaticHosts represents static domain-ip mapping in DNS server. +type StaticHosts struct { + ips [][]net.Address + matchers strmatcher.IndexMatcher +} + +// NewStaticHosts creates a new StaticHosts instance. +func NewStaticHosts(hosts []*Config_HostMapping) (*StaticHosts, error) { + g := new(strmatcher.MatcherGroup) + sh := &StaticHosts{ + ips: make([][]net.Address, len(hosts)+16), + matchers: g, + } + + defer runtime.GC() + for i, mapping := range hosts { + hosts[i] = nil + matcher, err := toStrMatcher(mapping.Type, mapping.Domain) + if err != nil { + errors.LogErrorInner(context.Background(), err, "failed to create domain matcher, ignore domain rule [type: ", mapping.Type, ", domain: ", mapping.Domain, "]") + continue + } + id := g.Add(matcher) + ips := make([]net.Address, 0, len(mapping.Ip)+1) + switch { + case len(mapping.ProxiedDomain) > 0: + if mapping.ProxiedDomain[0] == '#' { + rcode, err := strconv.Atoi(mapping.ProxiedDomain[1:]) + if err != nil { + return nil, err + } + ips = append(ips, dns.RCodeError(rcode)) + } else { + ips = append(ips, net.DomainAddress(mapping.ProxiedDomain)) + } + case len(mapping.Ip) > 0: + for _, ip := range mapping.Ip { + addr := net.IPAddress(ip) + if addr == nil { + errors.LogError(context.Background(), "invalid IP address in static hosts: ", ip, ", ignore this ip for rule [type: ", mapping.Type, ", domain: ", mapping.Domain, "]") + continue + } + ips = append(ips, addr) + } + if len(ips) == 0 { + continue + } + } + + sh.ips[id] = ips + } + + return sh, nil +} + +func filterIP(ips []net.Address, option dns.IPOption) []net.Address { + filtered := make([]net.Address, 0, len(ips)) + for _, ip := range ips { + if (ip.Family().IsIPv4() && option.IPv4Enable) || (ip.Family().IsIPv6() && option.IPv6Enable) { + filtered = append(filtered, ip) + } + } + return filtered +} + +func (h *StaticHosts) lookupInternal(domain string) ([]net.Address, error) { + ips := make([]net.Address, 0) + found := false + for _, id := range h.matchers.Match(domain) { + for _, v := range h.ips[id] { + if err, ok := v.(dns.RCodeError); ok { + if uint16(err) == 0 { + return nil, dns.ErrEmptyResponse + } + return nil, err + } + } + ips = append(ips, h.ips[id]...) + found = true + } + if !found { + return nil, nil + } + return ips, nil +} + +func (h *StaticHosts) lookup(domain string, option dns.IPOption, maxDepth int) ([]net.Address, error) { + switch addrs, err := h.lookupInternal(domain); { + case err != nil: + return nil, err + case len(addrs) == 0: // Not recorded in static hosts, return nil + return addrs, nil + 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") + if maxDepth > 0 { + unwrapped, err := h.lookup(addrs[0].Domain(), option, maxDepth-1) + if err != nil { + return nil, err + } + if unwrapped != nil { + return unwrapped, nil + } + } + return addrs, nil + default: // IP record found, return a non-nil IP array + return filterIP(addrs, option), nil + } +} + +// Lookup returns IP addresses or proxied domain for the given domain, if exists in this StaticHosts. +func (h *StaticHosts) Lookup(domain string, option dns.IPOption) ([]net.Address, error) { + return h.lookup(domain, option, 5) +} +func NewStaticHostsFromCache(matcher strmatcher.IndexMatcher, hostIPs map[string][]string) (*StaticHosts, error) { + sh := &StaticHosts{ + ips: make([][]net.Address, matcher.Size()+1), + matchers: matcher, + } + + order := hostIPs["_ORDER"] + var offset uint32 + + img, ok := matcher.(*strmatcher.IndexMatcherGroup) + if !ok { + // Single matcher (e.g. only manual or only one geosite) + if len(order) > 0 { + pattern := order[0] + ips := parseIPs(hostIPs[pattern]) + for i := uint32(1); i <= matcher.Size(); i++ { + sh.ips[i] = ips + } + } + return sh, nil + } + + for i, m := range img.Matchers { + if i < len(order) { + pattern := order[i] + ips := parseIPs(hostIPs[pattern]) + for j := uint32(1); j <= m.Size(); j++ { + sh.ips[offset+j] = ips + } + offset += m.Size() + } + } + return sh, nil +} + +func parseIPs(raw []string) []net.Address { + addrs := make([]net.Address, 0, len(raw)) + for _, s := range raw { + if len(s) > 1 && s[0] == '#' { + rcode, _ := strconv.Atoi(s[1:]) + addrs = append(addrs, dns.RCodeError(rcode)) + } else { + addrs = append(addrs, net.ParseAddress(s)) + } + } + return addrs +} diff --git a/subproject/Xray-core-main/app/dns/hosts_test.go b/subproject/Xray-core-main/app/dns/hosts_test.go new file mode 100644 index 00000000..2b9c24d8 --- /dev/null +++ b/subproject/Xray-core-main/app/dns/hosts_test.go @@ -0,0 +1,188 @@ +package dns_test + +import ( + "bytes" + "testing" + + "github.com/google/go-cmp/cmp" + . "github.com/xtls/xray-core/app/dns" + "github.com/xtls/xray-core/app/router" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/features/dns" +) + +func TestStaticHosts(t *testing.T) { + pb := []*Config_HostMapping{ + { + Type: DomainMatchingType_Subdomain, + Domain: "lan", + ProxiedDomain: "#3", + }, + { + Type: DomainMatchingType_Full, + Domain: "example.com", + Ip: [][]byte{ + {1, 1, 1, 1}, + }, + }, + { + Type: DomainMatchingType_Full, + Domain: "proxy.xray.com", + Ip: [][]byte{ + {1, 2, 3, 4}, + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, + }, + ProxiedDomain: "another-proxy.xray.com", + }, + { + Type: DomainMatchingType_Full, + Domain: "proxy2.xray.com", + ProxiedDomain: "proxy.xray.com", + }, + { + Type: DomainMatchingType_Subdomain, + Domain: "example.cn", + Ip: [][]byte{ + {2, 2, 2, 2}, + }, + }, + { + Type: DomainMatchingType_Subdomain, + Domain: "baidu.com", + Ip: [][]byte{ + {127, 0, 0, 1}, + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, + }, + }, + } + + hosts, err := NewStaticHosts(pb) + common.Must(err) + + { + _, err := hosts.Lookup("example.com.lan", dns.IPOption{}) + if dns.RCodeFromError(err) != 3 { + t.Error(err) + } + } + + { + ips, _ := hosts.Lookup("example.com", dns.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + }) + if len(ips) != 1 { + t.Error("expect 1 IP, but got ", len(ips)) + } + if diff := cmp.Diff([]byte(ips[0].IP()), []byte{1, 1, 1, 1}); diff != "" { + t.Error(diff) + } + } + + { + domain, _ := hosts.Lookup("proxy.xray.com", dns.IPOption{ + IPv4Enable: true, + IPv6Enable: false, + }) + if len(domain) != 1 { + t.Error("expect 1 domain, but got ", len(domain)) + } + if diff := cmp.Diff(domain[0].Domain(), "another-proxy.xray.com"); diff != "" { + t.Error(diff) + } + } + + { + domain, _ := hosts.Lookup("proxy2.xray.com", dns.IPOption{ + IPv4Enable: true, + IPv6Enable: false, + }) + if len(domain) != 1 { + t.Error("expect 1 domain, but got ", len(domain)) + } + if diff := cmp.Diff(domain[0].Domain(), "another-proxy.xray.com"); diff != "" { + t.Error(diff) + } + } + + { + ips, _ := hosts.Lookup("www.example.cn", dns.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + }) + if len(ips) != 1 { + t.Error("expect 1 IP, but got ", len(ips)) + } + if diff := cmp.Diff([]byte(ips[0].IP()), []byte{2, 2, 2, 2}); diff != "" { + t.Error(diff) + } + } + + { + ips, _ := hosts.Lookup("baidu.com", dns.IPOption{ + IPv4Enable: false, + IPv6Enable: true, + }) + if len(ips) != 1 { + t.Error("expect 1 IP, but got ", len(ips)) + } + if diff := cmp.Diff([]byte(ips[0].IP()), []byte(net.LocalHostIPv6.IP())); diff != "" { + t.Error(diff) + } + } +} +func TestStaticHostsFromCache(t *testing.T) { + sites := []*router.GeoSite{ + { + CountryCode: "cloudflare-dns.com", + Domain: []*router.Domain{ + {Type: router.Domain_Full, Value: "example.com"}, + }, + }, + { + CountryCode: "geosite:cn", + Domain: []*router.Domain{ + {Type: router.Domain_Domain, Value: "baidu.cn"}, + }, + }, + } + deps := map[string][]string{ + "HOSTS": {"cloudflare-dns.com", "geosite:cn"}, + } + hostIPs := map[string][]string{ + "cloudflare-dns.com": {"1.1.1.1"}, + "geosite:cn": {"2.2.2.2"}, + "_ORDER": {"cloudflare-dns.com", "geosite:cn"}, + } + + var buf bytes.Buffer + err := router.SerializeGeoSiteList(sites, deps, hostIPs, &buf) + common.Must(err) + + // Load matcher + m, err := router.LoadGeoSiteMatcher(bytes.NewReader(buf.Bytes()), "HOSTS") + common.Must(err) + + // Load hostIPs + f := bytes.NewReader(buf.Bytes()) + hips, err := router.LoadGeoSiteHosts(f) + common.Must(err) + + hosts, err := NewStaticHostsFromCache(m, hips) + common.Must(err) + + { + ips, _ := hosts.Lookup("example.com", dns.IPOption{IPv4Enable: true}) + if len(ips) != 1 || ips[0].String() != "1.1.1.1" { + t.Error("failed to lookup example.com from cache") + } + } + + { + ips, _ := hosts.Lookup("baidu.cn", dns.IPOption{IPv4Enable: true}) + if len(ips) != 1 || ips[0].String() != "2.2.2.2" { + t.Error("failed to lookup baidu.cn from cache deps") + } + } +} diff --git a/subproject/Xray-core-main/app/dns/nameserver.go b/subproject/Xray-core-main/app/dns/nameserver.go new file mode 100644 index 00000000..00d435b5 --- /dev/null +++ b/subproject/Xray-core-main/app/dns/nameserver.go @@ -0,0 +1,342 @@ +package dns + +import ( + "context" + "net/url" + "runtime" + "strings" + "time" + + "github.com/xtls/xray-core/app/router" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/platform" + "github.com/xtls/xray-core/common/platform/filesystem" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/common/strmatcher" + "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/features/dns" + "github.com/xtls/xray-core/features/routing" +) + +type mphMatcherWrapper struct { + m strmatcher.IndexMatcher +} + +func (w *mphMatcherWrapper) Match(s string) bool { + return w.m.Match(s) != nil +} + +func (w *mphMatcherWrapper) String() string { + return "mph-matcher" +} + +// Server is the interface for Name Server. +type Server interface { + // Name of the Client. + Name() string + + IsDisableCache() bool + + // QueryIP sends IP queries to its configured server. + QueryIP(ctx context.Context, domain string, option dns.IPOption) ([]net.IP, uint32, error) +} + +// Client is the interface for DNS client. +type Client struct { + server Server + skipFallback bool + domains []string + expectedIPs router.GeoIPMatcher + unexpectedIPs router.GeoIPMatcher + actPrior bool + actUnprior bool + tag string + timeoutMs time.Duration + finalQuery bool + ipOption *dns.IPOption + checkSystem bool + policyID uint32 +} + +// 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, serveStale bool, serveExpiredTTL uint32, clientIP net.IP) (Server, error) { + if address := dest.Address; address.Family().IsDomain() { + u, err := url.Parse(address.Domain()) + if err != nil { + return nil, err + } + switch { + case strings.EqualFold(u.String(), "localhost"): + return NewLocalNameServer(), nil + case strings.EqualFold(u.Scheme, "https"): // DNS-over-HTTPS Remote mode + return NewDoHNameServer(u, dispatcher, false, disableCache, serveStale, serveExpiredTTL, clientIP), nil + case strings.EqualFold(u.Scheme, "h2c"): // DNS-over-HTTPS h2c Remote mode + return NewDoHNameServer(u, dispatcher, true, disableCache, serveStale, serveExpiredTTL, clientIP), nil + case strings.EqualFold(u.Scheme, "https+local"): // DNS-over-HTTPS Local mode + return NewDoHNameServer(u, nil, false, disableCache, serveStale, serveExpiredTTL, clientIP), nil + case strings.EqualFold(u.Scheme, "h2c+local"): // DNS-over-HTTPS h2c Local mode + return NewDoHNameServer(u, nil, true, disableCache, serveStale, serveExpiredTTL, clientIP), nil + case strings.EqualFold(u.Scheme, "quic+local"): // DNS-over-QUIC Local mode + return NewQUICNameServer(u, disableCache, serveStale, serveExpiredTTL, clientIP) + case strings.EqualFold(u.Scheme, "tcp"): // DNS-over-TCP Remote mode + return NewTCPNameServer(u, dispatcher, disableCache, serveStale, serveExpiredTTL, clientIP) + case strings.EqualFold(u.Scheme, "tcp+local"): // DNS-over-TCP Local mode + return NewTCPLocalNameServer(u, disableCache, serveStale, serveExpiredTTL, clientIP) + case strings.EqualFold(u.String(), "fakedns"): + var fd dns.FakeDNSEngine + err = core.RequireFeatures(ctx, func(fdns dns.FakeDNSEngine) { + fd = fdns + }) + if err != nil { + return nil, err + } + return NewFakeDNSServer(fd), nil + } + } + if dest.Network == net.Network_Unknown { + dest.Network = net.Network_UDP + } + if dest.Network == net.Network_UDP { // UDP classic DNS mode + return NewClassicNameServer(dest, dispatcher, disableCache, serveStale, serveExpiredTTL, clientIP), nil + } + return nil, errors.New("No available name server could be created from ", dest).AtWarning() +} + +// NewClient creates a DNS client managing a name server with client IP, domain rules and expected IPs. +func NewClient( + ctx context.Context, + ns *NameServer, + clientIP net.IP, + disableCache bool, serveStale bool, serveExpiredTTL uint32, + tag string, + ipOption dns.IPOption, + matcherInfos *[]*DomainMatcherInfo, + updateDomainRule func(strmatcher.Matcher, int, []*DomainMatcherInfo), +) (*Client, error) { + client := &Client{} + + err := core.RequireFeatures(ctx, func(dispatcher routing.Dispatcher) error { + // Create a new server for each client for now + server, err := NewServer(ctx, ns.Address.AsDestination(), dispatcher, disableCache, serveStale, serveExpiredTTL, clientIP) + if err != nil { + return errors.New("failed to create nameserver").Base(err).AtWarning() + } + + // Prioritize local domains with specific TLDs or those without any dot for the local DNS + if _, isLocalDNS := server.(*LocalNameServer); isLocalDNS { + ns.PrioritizedDomain = append(ns.PrioritizedDomain, localTLDsAndDotlessDomains...) + ns.OriginalRules = append(ns.OriginalRules, localTLDsAndDotlessDomainsRule) + // The following lines is a solution to avoid core panics(rule index out of range) when setting `localhost` DNS client in config. + // Because the `localhost` DNS client will append len(localTLDsAndDotlessDomains) rules into matcherInfos to match `geosite:private` default rule. + // But `matcherInfos` has no enough length to add rules, which leads to core panics (rule index out of range). + // To avoid this, the length of `matcherInfos` must be equal to the expected, so manually append it with Golang default zero value first for later modification. + // Related issues: + // https://github.com/v2fly/v2ray-core/issues/529 + // https://github.com/v2fly/v2ray-core/issues/719 + for i := 0; i < len(localTLDsAndDotlessDomains); i++ { + *matcherInfos = append(*matcherInfos, &DomainMatcherInfo{ + clientIdx: uint16(0), + domainRuleIdx: uint16(0), + }) + } + } + + // Establish domain rules + var rules []string + ruleCurr := 0 + ruleIter := 0 + + // Check if domain matcher cache is provided via environment + domainMatcherPath := platform.NewEnvFlag(platform.MphCachePath).GetValue(func() string { return "" }) + var mphLoaded bool + + if domainMatcherPath != "" && ns.Tag != "" { + f, err := filesystem.NewFileReader(domainMatcherPath) + if err == nil { + defer f.Close() + g, err := router.LoadGeoSiteMatcher(f, ns.Tag) + if err == nil { + errors.LogDebug(ctx, "MphDomainMatcher loaded from cache for ", ns.Tag, " dns tag)") + updateDomainRule(&mphMatcherWrapper{m: g}, 0, *matcherInfos) + rules = append(rules, "[MPH Cache]") + mphLoaded = true + } + } + } + + if !mphLoaded { + for i, domain := range ns.PrioritizedDomain { + ns.PrioritizedDomain[i] = nil + domainRule, err := toStrMatcher(domain.Type, domain.Domain) + if err != nil { + errors.LogErrorInner(ctx, err, "failed to create domain matcher, ignore domain rule [type: ", domain.Type, ", domain: ", domain.Domain, "]") + domainRule, _ = toStrMatcher(DomainMatchingType_Full, "hack.fix.index.for.illegal.domain.rule") + } + originalRuleIdx := ruleCurr + if ruleCurr < len(ns.OriginalRules) { + rule := ns.OriginalRules[ruleCurr] + if ruleCurr >= len(rules) { + rules = append(rules, rule.Rule) + } + ruleIter++ + if ruleIter >= int(rule.Size) { + ruleIter = 0 + ruleCurr++ + } + } else { // No original rule, generate one according to current domain matcher (majorly for compatibility with tests) + rules = append(rules, domainRule.String()) + ruleCurr++ + } + updateDomainRule(domainRule, originalRuleIdx, *matcherInfos) + } + } + ns.PrioritizedDomain = nil + runtime.GC() + + // Establish expected IPs + var expectedMatcher router.GeoIPMatcher + if len(ns.ExpectedGeoip) > 0 { + expectedMatcher, err = router.BuildOptimizedGeoIPMatcher(ns.ExpectedGeoip...) + if err != nil { + return errors.New("failed to create expected ip matcher").Base(err).AtWarning() + } + ns.ExpectedGeoip = nil + runtime.GC() + } + + // Establish unexpected IPs + var unexpectedMatcher router.GeoIPMatcher + if len(ns.UnexpectedGeoip) > 0 { + unexpectedMatcher, err = router.BuildOptimizedGeoIPMatcher(ns.UnexpectedGeoip...) + if err != nil { + return errors.New("failed to create unexpected ip matcher").Base(err).AtWarning() + } + ns.UnexpectedGeoip = nil + runtime.GC() + } + + if len(clientIP) > 0 { + switch ns.Address.Address.GetAddress().(type) { + case *net.IPOrDomain_Domain: + errors.LogInfo(ctx, "DNS: client ", ns.Address.Address.GetDomain(), " uses clientIP ", clientIP.String()) + case *net.IPOrDomain_Ip: + errors.LogInfo(ctx, "DNS: client ", net.IP(ns.Address.Address.GetIp()), " uses clientIP ", clientIP.String()) + } + } + + var timeoutMs = 4000 * time.Millisecond + if ns.TimeoutMs > 0 { + timeoutMs = time.Duration(ns.TimeoutMs) * time.Millisecond + } + + checkSystem := ns.QueryStrategy == QueryStrategy_USE_SYS + + client.server = server + client.skipFallback = ns.SkipFallback + client.domains = rules + client.expectedIPs = expectedMatcher + client.unexpectedIPs = unexpectedMatcher + client.actPrior = ns.ActPrior + client.actUnprior = ns.ActUnprior + client.tag = tag + client.timeoutMs = timeoutMs + client.finalQuery = ns.FinalQuery + client.ipOption = &ipOption + client.checkSystem = checkSystem + client.policyID = ns.PolicyID + return nil + }) + return client, err +} + +// Name returns the server name the client manages. +func (c *Client) Name() string { + return c.server.Name() +} + +// QueryIP sends DNS query to the name server with the client's IP. +func (c *Client) QueryIP(ctx context.Context, domain string, option dns.IPOption) ([]net.IP, uint32, error) { + if c.checkSystem { + supportIPv4, supportIPv6 := checkRoutes() + option.IPv4Enable = option.IPv4Enable && supportIPv4 + option.IPv6Enable = option.IPv6Enable && supportIPv6 + } else { + option.IPv4Enable = option.IPv4Enable && c.ipOption.IPv4Enable + option.IPv6Enable = option.IPv6Enable && c.ipOption.IPv6Enable + } + + 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() + + if err != nil { + return nil, 0, err + } + + if len(ips) == 0 { + return nil, 0, dns.ErrEmptyResponse + } + + if c.expectedIPs != nil && !c.actPrior { + ips, _ = c.expectedIPs.FilterIPs(ips) + errors.LogDebug(context.Background(), "domain ", domain, " expectedIPs ", ips, " matched at server ", c.Name()) + if len(ips) == 0 { + return nil, 0, dns.ErrEmptyResponse + } + } + + if c.unexpectedIPs != nil && !c.actUnprior { + _, ips = c.unexpectedIPs.FilterIPs(ips) + errors.LogDebug(context.Background(), "domain ", domain, " unexpectedIPs ", ips, " matched at server ", c.Name()) + if len(ips) == 0 { + return nil, 0, dns.ErrEmptyResponse + } + } + + if c.expectedIPs != nil && c.actPrior { + ipsNew, _ := c.expectedIPs.FilterIPs(ips) + if len(ipsNew) > 0 { + ips = ipsNew + errors.LogDebug(context.Background(), "domain ", domain, " priorIPs ", ips, " matched at server ", c.Name()) + } + } + + if c.unexpectedIPs != nil && c.actUnprior { + _, ipsNew := c.unexpectedIPs.FilterIPs(ips) + if len(ipsNew) > 0 { + ips = ipsNew + errors.LogDebug(context.Background(), "domain ", domain, " unpriorIPs ", ips, " matched at server ", c.Name()) + } + } + + return ips, ttl, nil +} + +func ResolveIpOptionOverride(queryStrategy QueryStrategy, ipOption dns.IPOption) dns.IPOption { + switch queryStrategy { + case QueryStrategy_USE_IP: + return ipOption + case QueryStrategy_USE_SYS: + return ipOption + case QueryStrategy_USE_IP4: + return dns.IPOption{ + IPv4Enable: ipOption.IPv4Enable, + IPv6Enable: false, + FakeEnable: false, + } + case QueryStrategy_USE_IP6: + return dns.IPOption{ + IPv4Enable: false, + IPv6Enable: ipOption.IPv6Enable, + FakeEnable: false, + } + default: + return ipOption + } +} diff --git a/subproject/Xray-core-main/app/dns/nameserver_cached.go b/subproject/Xray-core-main/app/dns/nameserver_cached.go new file mode 100644 index 00000000..cbd2f031 --- /dev/null +++ b/subproject/Xray-core-main/app/dns/nameserver_cached.go @@ -0,0 +1,173 @@ +package dns + +import ( + "context" + go_errors "errors" + "time" + + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/log" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/signal/pubsub" + "github.com/xtls/xray-core/features/dns" +) + +type CachedNameserver interface { + getCacheController() *CacheController + + sendQuery(ctx context.Context, noResponseErrCh chan<- error, fqdn string, option dns.IPOption) +} + +// queryIP is called from dns.Server->queryIPTimeout +func queryIP(ctx context.Context, s CachedNameserver, domain string, option dns.IPOption) ([]net.IP, uint32, error) { + fqdn := Fqdn(domain) + + cache := s.getCacheController() + if !cache.disableCache { + if rec := cache.findRecords(fqdn); rec != nil { + ips, ttl, err := merge(option, rec.A, rec.AAAA) + if !go_errors.Is(err, errRecordNotFound) { + if ttl > 0 { + errors.LogDebugInner(ctx, err, cache.name, " cache HIT ", fqdn, " -> ", ips) + log.Record(&log.DNSLog{Server: cache.name, Domain: fqdn, Result: ips, Status: log.DNSCacheHit, Elapsed: 0, Error: err}) + return ips, uint32(ttl), err + } + if cache.serveStale && (cache.serveExpiredTTL == 0 || cache.serveExpiredTTL < ttl) { + errors.LogDebugInner(ctx, err, cache.name, " cache OPTIMISTE ", fqdn, " -> ", ips) + log.Record(&log.DNSLog{Server: cache.name, Domain: fqdn, Result: ips, Status: log.DNSCacheOptimiste, Elapsed: 0, Error: err}) + go pull(ctx, s, fqdn, option) + return ips, 1, err + } + } + } + } else { + errors.LogDebug(ctx, "DNS cache is disabled. Querying IP for ", fqdn, " at ", cache.name) + } + + return fetch(ctx, s, fqdn, option) +} + +func pull(ctx context.Context, s CachedNameserver, fqdn string, option dns.IPOption) { + nctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 8*time.Second) + defer cancel() + + fetch(nctx, s, fqdn, option) +} + +func fetch(ctx context.Context, s CachedNameserver, fqdn string, option dns.IPOption) ([]net.IP, uint32, error) { + key := fqdn + switch { + case option.IPv4Enable && option.IPv6Enable: + key = key + "46" + case option.IPv4Enable: + key = key + "4" + case option.IPv6Enable: + key = key + "6" + } + + v, _, _ := s.getCacheController().requestGroup.Do(key, func() (any, error) { + return doFetch(ctx, s, fqdn, option), nil + }) + ret := v.(result) + + return ret.ips, ret.ttl, ret.error +} + +type result struct { + ips []net.IP + ttl uint32 + error +} + +func doFetch(ctx context.Context, s CachedNameserver, fqdn string, option dns.IPOption) result { + sub4, sub6 := s.getCacheController().registerSubscribers(fqdn, option) + defer closeSubscribers(sub4, sub6) + + noResponseErrCh := make(chan error, 2) + onEvent := func(sub *pubsub.Subscriber) (*IPRecord, error) { + if sub == nil { + return nil, nil + } + select { + case <-ctx.Done(): + return nil, ctx.Err() + case err := <-noResponseErrCh: + return nil, err + case msg := <-sub.Wait(): + sub.Close() + return msg.(*IPRecord), nil // should panic + } + } + + start := time.Now() + s.sendQuery(ctx, noResponseErrCh, fqdn, option) + + rec4, err4 := onEvent(sub4) + rec6, err6 := onEvent(sub6) + + var errs []error + if err4 != nil { + errs = append(errs, err4) + } + if err6 != nil { + errs = append(errs, err6) + } + + ips, ttl, err := merge(option, rec4, rec6, errs...) + var rTTL uint32 + if ttl > 0 { + rTTL = uint32(ttl) + } else if ttl == 0 && go_errors.Is(err, errRecordNotFound) { + rTTL = 0 + } else { // edge case: where a fast rep's ttl expires during the rtt of a slower, parallel query + rTTL = 1 + } + + log.Record(&log.DNSLog{Server: s.getCacheController().name, Domain: fqdn, Result: ips, Status: log.DNSQueried, Elapsed: time.Since(start), Error: err}) + return result{ips, rTTL, err} +} + +func merge(option dns.IPOption, rec4 *IPRecord, rec6 *IPRecord, errs ...error) ([]net.IP, int32, error) { + var allIPs []net.IP + var rTTL int32 = dns.DefaultTTL + + mergeReq := option.IPv4Enable && option.IPv6Enable + + if option.IPv4Enable { + ips, ttl, err := rec4.getIPs() // it's safe + 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 := rec6.getIPs() // it's safe + 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 len(errs) == 2 && go_errors.Is(errs[0], errs[1]) { + return nil, rTTL, errs[0] + } + return nil, rTTL, errors.Combine(errs...) +} diff --git a/subproject/Xray-core-main/app/dns/nameserver_doh.go b/subproject/Xray-core-main/app/dns/nameserver_doh.go new file mode 100644 index 00000000..2849dbed --- /dev/null +++ b/subproject/Xray-core-main/app/dns/nameserver_doh.go @@ -0,0 +1,239 @@ +package dns + +import ( + "bytes" + "context" + "crypto/tls" + "fmt" + "io" + "net/http" + "net/url" + "time" + + utls "github.com/refraction-networking/utls" + "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/log" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/net/cnc" + "github.com/xtls/xray-core/common/protocol/dns" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/common/utils" + dns_feature "github.com/xtls/xray-core/features/dns" + "github.com/xtls/xray-core/features/routing" + "github.com/xtls/xray-core/transport/internet" + "golang.org/x/net/http2" +) + +// DoHNameServer implemented DNS over HTTPS (RFC8484) Wire Format, +// which is compatible with traditional dns over udp(RFC1035), +// thus most of the DOH implementation is copied from udpns.go +type DoHNameServer struct { + cacheController *CacheController + httpClient *http.Client + dohURL string + clientIP net.IP +} + +// NewDoHNameServer creates DOH/DOHL client object for remote/local resolving. +func NewDoHNameServer(url *url.URL, dispatcher routing.Dispatcher, h2c bool, disableCache bool, serveStale bool, serveExpiredTTL uint32, clientIP net.IP) *DoHNameServer { + url.Scheme = "https" + mode := "DOH" + if dispatcher == nil { + mode = "DOHL" + } + errors.LogInfo(context.Background(), "DNS: created ", mode, " client for ", url.String(), ", with h2c ", h2c) + s := &DoHNameServer{ + cacheController: NewCacheController(mode+"//"+url.Host, disableCache, serveStale, serveExpiredTTL), + dohURL: url.String(), + clientIP: clientIP, + } + s.httpClient = &http.Client{ + Transport: &http2.Transport{ + IdleConnTimeout: net.ConnIdleTimeout, + ReadIdleTimeout: net.ChromeH2KeepAlivePeriod, + DialTLSContext: func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) { + dest, err := net.ParseDestination(network + ":" + addr) + if err != nil { + return nil, err + } + var conn net.Conn + if dispatcher != nil { + dnsCtx := toDnsContext(ctx, s.dohURL) + if h2c { + dnsCtx = session.ContextWithMitmAlpn11(dnsCtx, false) // for insurance + dnsCtx = session.ContextWithMitmServerName(dnsCtx, url.Hostname()) + } + link, err := dispatcher.Dispatch(dnsCtx, 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) + } + 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 + }, + }, + } + return s +} + +// Name implements Server. +func (s *DoHNameServer) Name() string { + return s.cacheController.name +} + +// IsDisableCache implements Server. +func (s *DoHNameServer) IsDisableCache() bool { + return s.cacheController.disableCache +} + +func (s *DoHNameServer) newReqID() uint16 { + return 0 +} + +// getCacheController implements CachedNameserver. +func (s *DoHNameServer) getCacheController() *CacheController { + return s.cacheController +} + +// sendQuery implements CachedNameserver. +func (s *DoHNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- error, fqdn string, option dns_feature.IPOption) { + errors.LogInfo(ctx, s.Name(), " querying: ", fqdn) + + if s.Name()+"." == "DOH//"+fqdn { + errors.LogError(ctx, s.Name(), " tries to resolve itself! Use IP or set \"hosts\" instead") + if noResponseErrCh != nil { + noResponseErrCh <- errors.New("tries to resolve itself!", s.Name()) + } + return + } + + // As we don't want our traffic pattern looks like DoH, we use Random-Length Padding instead of Block-Length Padding recommended in RFC 8467 + // Although DoH server like 1.1.1.1 will pad the response to Block-Length 468, at least it is better than no padding for response at all + reqs := buildReqMsgs(fqdn, option, s.newReqID, genEDNS0Options(s.clientIP, int(crypto.RandBetween(100, 300)))) + + var deadline time.Time + if d, ok := ctx.Deadline(); ok { + deadline = d + } else { + deadline = time.Now().Add(time.Second * 5) + } + + for _, req := range reqs { + go func(r *dnsRequest) { + // generate new context for each req, using same context + // may cause reqs all aborted if any one encounter an error + dnsCtx := ctx + + // reserve internal dns server requested Inbound + if inbound := session.InboundFromContext(ctx); inbound != nil { + dnsCtx = session.ContextWithInbound(dnsCtx, inbound) + } + + dnsCtx = session.ContextWithContent(dnsCtx, &session.Content{ + Protocol: "https", + SkipDNSResolve: true, + }) + + // forced to use mux for DOH + // dnsCtx = session.ContextWithMuxPreferred(dnsCtx, true) + + var cancel context.CancelFunc + dnsCtx, cancel = context.WithDeadline(dnsCtx, deadline) + defer cancel() + + b, err := dns.PackMessage(r.msg) + if err != nil { + errors.LogErrorInner(ctx, err, "failed to pack dns query for ", fqdn) + if noResponseErrCh != nil { + noResponseErrCh <- err + } + return + } + resp, err := s.dohHTTPSContext(dnsCtx, b.Bytes()) + if err != nil { + errors.LogErrorInner(ctx, err, "failed to retrieve response for ", fqdn) + if noResponseErrCh != nil { + noResponseErrCh <- err + } + return + } + rec, err := parseResponse(resp) + if err != nil { + errors.LogErrorInner(ctx, err, "failed to handle DOH response for ", fqdn) + if noResponseErrCh != nil { + noResponseErrCh <- err + } + return + } + s.cacheController.updateRecord(r, rec) + }(req) + } +} + +func (s *DoHNameServer) dohHTTPSContext(ctx context.Context, b []byte) ([]byte, error) { + body := bytes.NewBuffer(b) + req, err := http.NewRequest("POST", s.dohURL, body) + if err != nil { + return nil, err + } + + req.Header.Add("Accept", "application/dns-message") + req.Header.Add("Content-Type", "application/dns-message") + utils.TryDefaultHeadersWith(req.Header, "fetch") + req.Header.Set("X-Padding", utils.H2Base62Pad(crypto.RandBetween(100, 1000))) + + hc := s.httpClient + + resp, err := hc.Do(req.WithContext(ctx)) + if err != nil { + return nil, err + } + + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + io.Copy(io.Discard, resp.Body) // flush resp.Body so that the conn is reusable + return nil, fmt.Errorf("DOH server returned code %d", resp.StatusCode) + } + + return io.ReadAll(resp.Body) +} + +// QueryIP implements Server. +func (s *DoHNameServer) QueryIP(ctx context.Context, domain string, option dns_feature.IPOption) ([]net.IP, uint32, error) { + return queryIP(ctx, s, domain, option) +} diff --git a/subproject/Xray-core-main/app/dns/nameserver_doh_test.go b/subproject/Xray-core-main/app/dns/nameserver_doh_test.go new file mode 100644 index 00000000..52387661 --- /dev/null +++ b/subproject/Xray-core-main/app/dns/nameserver_doh_test.go @@ -0,0 +1,105 @@ +package dns_test + +import ( + "context" + "net/url" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + . "github.com/xtls/xray-core/app/dns" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/net" + dns_feature "github.com/xtls/xray-core/features/dns" +) + +func TestDOHNameServer(t *testing.T) { + url, err := url.Parse("https+local://1.1.1.1/dns-query") + common.Must(err) + + s := NewDoHNameServer(url, nil, false, false, false, 0, net.IP(nil)) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + ips, _, err := s.QueryIP(ctx, "google.com", dns_feature.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + }) + cancel() + common.Must(err) + if len(ips) == 0 { + t.Error("expect some ips, but got 0") + } +} + +func TestDOHNameServerWithCache(t *testing.T) { + url, err := url.Parse("https+local://1.1.1.1/dns-query") + common.Must(err) + + s := NewDoHNameServer(url, nil, false, false, false, 0, net.IP(nil)) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + ips, _, err := s.QueryIP(ctx, "google.com", dns_feature.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + }) + cancel() + common.Must(err) + if len(ips) == 0 { + t.Error("expect some ips, but got 0") + } + + ctx2, cancel := context.WithTimeout(context.Background(), time.Second*5) + ips2, _, err := s.QueryIP(ctx2, "google.com", dns_feature.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + }) + cancel() + common.Must(err) + if r := cmp.Diff(ips2, ips); r != "" { + t.Fatal(r) + } +} + +func TestDOHNameServerWithIPv4Override(t *testing.T) { + url, err := url.Parse("https+local://1.1.1.1/dns-query") + common.Must(err) + + s := NewDoHNameServer(url, nil, false, false, false, 0, net.IP(nil)) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + ips, _, err := s.QueryIP(ctx, "google.com", dns_feature.IPOption{ + IPv4Enable: true, + IPv6Enable: false, + }) + cancel() + common.Must(err) + if len(ips) == 0 { + t.Error("expect some ips, but got 0") + } + + for _, ip := range ips { + if len(ip) != net.IPv4len { + t.Error("expect only IPv4 response from DNS query") + } + } +} + +func TestDOHNameServerWithIPv6Override(t *testing.T) { + url, err := url.Parse("https+local://1.1.1.1/dns-query") + common.Must(err) + + s := NewDoHNameServer(url, nil, false, false, false, 0, net.IP(nil)) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + ips, _, err := s.QueryIP(ctx, "google.com", dns_feature.IPOption{ + IPv4Enable: false, + IPv6Enable: true, + }) + cancel() + common.Must(err) + if len(ips) == 0 { + t.Error("expect some ips, but got 0") + } + + for _, ip := range ips { + if len(ip) != net.IPv6len { + t.Error("expect only IPv6 response from DNS query") + } + } +} diff --git a/subproject/Xray-core-main/app/dns/nameserver_fakedns.go b/subproject/Xray-core-main/app/dns/nameserver_fakedns.go new file mode 100644 index 00000000..bed11bd6 --- /dev/null +++ b/subproject/Xray-core-main/app/dns/nameserver_fakedns.go @@ -0,0 +1,51 @@ +package dns + +import ( + "context" + + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/features/dns" +) + +type FakeDNSServer struct { + fakeDNSEngine dns.FakeDNSEngine +} + +func NewFakeDNSServer(fd dns.FakeDNSEngine) *FakeDNSServer { + return &FakeDNSServer{fakeDNSEngine: fd} +} + +func (FakeDNSServer) Name() string { + return "FakeDNS" +} + +// IsDisableCache implements Server. +func (s *FakeDNSServer) IsDisableCache() bool { + return true +} + +func (f *FakeDNSServer) QueryIP(ctx context.Context, domain string, opt dns.IPOption) ([]net.IP, uint32, error) { + if f.fakeDNSEngine == nil { + return nil, 0, errors.New("Unable to locate a fake DNS Engine").AtError() + } + + var ips []net.Address + if fkr0, ok := f.fakeDNSEngine.(dns.FakeDNSEngineRev0); ok { + ips = fkr0.GetFakeIPForDomain3(domain, opt.IPv4Enable, opt.IPv6Enable) + } else { + ips = f.fakeDNSEngine.GetFakeIPForDomain(domain) + } + + netIP, err := toNetIP(ips) + if err != nil { + return nil, 0, errors.New("Unable to convert IP to net ip").Base(err).AtError() + } + + errors.LogInfo(ctx, f.Name(), " got answer: ", domain, " -> ", ips) + + if len(netIP) > 0 { + return netIP, 1, nil // fakeIP ttl is 1 + } + return nil, 0, dns.ErrEmptyResponse +} diff --git a/subproject/Xray-core-main/app/dns/nameserver_local.go b/subproject/Xray-core-main/app/dns/nameserver_local.go new file mode 100644 index 00000000..576c259b --- /dev/null +++ b/subproject/Xray-core-main/app/dns/nameserver_local.go @@ -0,0 +1,54 @@ +package dns + +import ( + "context" + "time" + + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/log" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/features/dns" + "github.com/xtls/xray-core/features/dns/localdns" +) + +// LocalNameServer is an wrapper over local DNS feature. +type LocalNameServer struct { + client *localdns.Client +} + +// QueryIP implements Server. +func (s *LocalNameServer) QueryIP(ctx context.Context, domain string, option dns.IPOption) (ips []net.IP, ttl uint32, err error) { + + start := time.Now() + ips, ttl, err = s.client.LookupIP(domain, option) + + if len(ips) > 0 { + errors.LogInfo(ctx, "Localhost got answer: ", domain, " -> ", ips) + log.Record(&log.DNSLog{Server: s.Name(), Domain: domain, Result: ips, Status: log.DNSQueried, Elapsed: time.Since(start), Error: err}) + } + + return +} + +// Name implements Server. +func (s *LocalNameServer) Name() string { + return "localhost" +} + +// IsDisableCache implements Server. +func (s *LocalNameServer) IsDisableCache() bool { + return true +} + +// NewLocalNameServer creates localdns server object for directly lookup in system DNS. +func NewLocalNameServer() *LocalNameServer { + errors.LogInfo(context.Background(), "DNS: created localhost client") + return &LocalNameServer{ + client: localdns.New(), + } +} + +// NewLocalDNSClient creates localdns client object for directly lookup in system DNS. +func NewLocalDNSClient(ipOption dns.IPOption) *Client { + return &Client{server: NewLocalNameServer(), ipOption: &ipOption} +} diff --git a/subproject/Xray-core-main/app/dns/nameserver_local_test.go b/subproject/Xray-core-main/app/dns/nameserver_local_test.go new file mode 100644 index 00000000..71aa08c4 --- /dev/null +++ b/subproject/Xray-core-main/app/dns/nameserver_local_test.go @@ -0,0 +1,26 @@ +package dns_test + +import ( + "context" + "testing" + "time" + + . "github.com/xtls/xray-core/app/dns" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/features/dns" +) + +func TestLocalNameServer(t *testing.T) { + s := NewLocalNameServer() + ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) + ips, _, err := s.QueryIP(ctx, "google.com", dns.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + FakeEnable: false, + }) + cancel() + common.Must(err) + if len(ips) == 0 { + t.Error("expect some ips, but got 0") + } +} diff --git a/subproject/Xray-core-main/app/dns/nameserver_quic.go b/subproject/Xray-core-main/app/dns/nameserver_quic.go new file mode 100644 index 00000000..bd0da170 --- /dev/null +++ b/subproject/Xray-core-main/app/dns/nameserver_quic.go @@ -0,0 +1,276 @@ +package dns + +import ( + "bytes" + "context" + "encoding/binary" + "net/url" + "sync" + "time" + + "github.com/apernet/quic-go" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/log" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol/dns" + "github.com/xtls/xray-core/common/session" + dns_feature "github.com/xtls/xray-core/features/dns" + "github.com/xtls/xray-core/transport/internet/tls" + "golang.org/x/net/http2" +) + +// NextProtoDQ - During connection establishment, DNS/QUIC support is indicated +// by selecting the ALPN token "dq" in the crypto handshake. +const NextProtoDQ = "doq" + +const handshakeTimeout = time.Second * 8 + +// QUICNameServer implemented DNS over QUIC +type QUICNameServer struct { + sync.RWMutex + cacheController *CacheController + destination *net.Destination + connection *quic.Conn + clientIP net.IP +} + +// NewQUICNameServer creates DNS-over-QUIC client object for local resolving +func NewQUICNameServer(url *url.URL, disableCache bool, serveStale bool, serveExpiredTTL uint32, clientIP net.IP) (*QUICNameServer, error) { + var err error + port := net.Port(853) + if url.Port() != "" { + port, err = net.PortFromString(url.Port()) + if err != nil { + return nil, err + } + } + dest := net.UDPDestination(net.ParseAddress(url.Hostname()), port) + + s := &QUICNameServer{ + cacheController: NewCacheController(url.String(), disableCache, serveStale, serveExpiredTTL), + destination: &dest, + clientIP: clientIP, + } + + errors.LogInfo(context.Background(), "DNS: created Local DNS-over-QUIC client for ", url.String()) + return s, nil +} + +// Name implements Server. +func (s *QUICNameServer) Name() string { + return s.cacheController.name +} + +// IsDisableCache implements Server. +func (s *QUICNameServer) IsDisableCache() bool { + return s.cacheController.disableCache +} + +func (s *QUICNameServer) newReqID() uint16 { + return 0 +} + +// getCacheController implements CachedNameServer. +func (s *QUICNameServer) getCacheController() *CacheController { return s.cacheController } + +// sendQuery implements CachedNameServer. +func (s *QUICNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- error, fqdn string, option dns_feature.IPOption) { + errors.LogInfo(ctx, s.Name(), " querying: ", fqdn) + + reqs := buildReqMsgs(fqdn, option, s.newReqID, genEDNS0Options(s.clientIP, 0)) + + var deadline time.Time + if d, ok := ctx.Deadline(); ok { + deadline = d + } else { + deadline = time.Now().Add(time.Second * 5) + } + + for _, req := range reqs { + go func(r *dnsRequest) { + // generate new context for each req, using same context + // may cause reqs all aborted if any one encounter an error + dnsCtx := ctx + + // reserve internal dns server requested Inbound + if inbound := session.InboundFromContext(ctx); inbound != nil { + dnsCtx = session.ContextWithInbound(dnsCtx, inbound) + } + + dnsCtx = session.ContextWithContent(dnsCtx, &session.Content{ + Protocol: "quic", + SkipDNSResolve: true, + }) + + var cancel context.CancelFunc + dnsCtx, cancel = context.WithDeadline(dnsCtx, deadline) + defer cancel() + + b, err := dns.PackMessage(r.msg) + if err != nil { + errors.LogErrorInner(ctx, err, "failed to pack dns query") + if noResponseErrCh != nil { + noResponseErrCh <- err + } + return + } + + dnsReqBuf := buf.New() + err = binary.Write(dnsReqBuf, binary.BigEndian, uint16(b.Len())) + if err != nil { + errors.LogErrorInner(ctx, err, "binary write failed") + if noResponseErrCh != nil { + noResponseErrCh <- err + } + return + } + _, err = dnsReqBuf.Write(b.Bytes()) + if err != nil { + errors.LogErrorInner(ctx, err, "buffer write failed") + if noResponseErrCh != nil { + noResponseErrCh <- err + } + return + } + b.Release() + + conn, err := s.openStream(dnsCtx) + if err != nil { + errors.LogErrorInner(ctx, err, "failed to open quic connection") + if noResponseErrCh != nil { + noResponseErrCh <- err + } + return + } + + _, err = conn.Write(dnsReqBuf.Bytes()) + if err != nil { + errors.LogErrorInner(ctx, err, "failed to send query") + if noResponseErrCh != nil { + noResponseErrCh <- err + } + return + } + + _ = conn.Close() + + respBuf := buf.New() + defer respBuf.Release() + n, err := respBuf.ReadFullFrom(conn, 2) + if err != nil && n == 0 { + errors.LogErrorInner(ctx, err, "failed to read response length") + if noResponseErrCh != nil { + noResponseErrCh <- err + } + return + } + var length uint16 + err = binary.Read(bytes.NewReader(respBuf.Bytes()), binary.BigEndian, &length) + if err != nil { + errors.LogErrorInner(ctx, err, "failed to parse response length") + if noResponseErrCh != nil { + noResponseErrCh <- err + } + return + } + respBuf.Clear() + n, err = respBuf.ReadFullFrom(conn, int32(length)) + if err != nil && n == 0 { + errors.LogErrorInner(ctx, err, "failed to read response length") + if noResponseErrCh != nil { + noResponseErrCh <- err + } + return + } + + rec, err := parseResponse(respBuf.Bytes()) + if err != nil { + errors.LogErrorInner(ctx, err, "failed to handle response") + if noResponseErrCh != nil { + noResponseErrCh <- err + } + return + } + s.cacheController.updateRecord(r, rec) + }(req) + } +} + +// QueryIP implements Server. +func (s *QUICNameServer) QueryIP(ctx context.Context, domain string, option dns_feature.IPOption) ([]net.IP, uint32, error) { + return queryIP(ctx, s, domain, option) +} + +func isActive(s *quic.Conn) bool { + select { + case <-s.Context().Done(): + return false + default: + return true + } +} + +func (s *QUICNameServer) getConnection() (*quic.Conn, error) { + var conn *quic.Conn + s.RLock() + conn = s.connection + if conn != nil && isActive(conn) { + s.RUnlock() + return conn, nil + } + if conn != nil { + // we're recreating the connection, let's create a new one + _ = conn.CloseWithError(0, "") + } + s.RUnlock() + + s.Lock() + defer s.Unlock() + + var err error + conn, err = s.openConnection() + if err != nil { + // This does not look too nice, but QUIC (or maybe quic-go) + // doesn't seem stable enough. + // Maybe retransmissions aren't fully implemented in quic-go? + // Anyways, the simple solution is to make a second try when + // it fails to open the QUIC connection. + conn, err = s.openConnection() + if err != nil { + return nil, err + } + } + s.connection = conn + return conn, nil +} + +func (s *QUICNameServer) openConnection() (*quic.Conn, error) { + tlsConfig := tls.Config{} + quicConfig := &quic.Config{ + HandshakeIdleTimeout: handshakeTimeout, + } + tlsConfig.ServerName = s.destination.Address.String() + conn, err := quic.DialAddr(context.Background(), s.destination.NetAddr(), tlsConfig.GetTLSConfig(tls.WithNextProto("http/1.1", http2.NextProtoTLS, NextProtoDQ)), quicConfig) + log.Record(&log.AccessMessage{ + From: "DNS", + To: s.destination, + Status: log.AccessAccepted, + Detour: "local", + }) + if err != nil { + return nil, err + } + + return conn, nil +} + +func (s *QUICNameServer) openStream(ctx context.Context) (*quic.Stream, error) { + conn, err := s.getConnection() + if err != nil { + return nil, err + } + + // open a new stream + return conn.OpenStreamSync(ctx) +} diff --git a/subproject/Xray-core-main/app/dns/nameserver_quic_test.go b/subproject/Xray-core-main/app/dns/nameserver_quic_test.go new file mode 100644 index 00000000..83212dcb --- /dev/null +++ b/subproject/Xray-core-main/app/dns/nameserver_quic_test.go @@ -0,0 +1,87 @@ +package dns_test + +import ( + "context" + "net/url" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + . "github.com/xtls/xray-core/app/dns" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/features/dns" +) + +func TestQUICNameServer(t *testing.T) { + url, err := url.Parse("quic://dns.adguard-dns.com") + common.Must(err) + s, err := NewQUICNameServer(url, false, false, 0, net.IP(nil)) + common.Must(err) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) + ips, _, err := s.QueryIP(ctx, "google.com", dns.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + }) + cancel() + common.Must(err) + if len(ips) == 0 { + t.Error("expect some ips, but got 0") + } + ctx2, cancel := context.WithTimeout(context.Background(), time.Second*5) + ips2, _, err := s.QueryIP(ctx2, "google.com", dns.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + }) + cancel() + common.Must(err) + if r := cmp.Diff(ips2, ips); r != "" { + t.Fatal(r) + } +} + +func TestQUICNameServerWithIPv4Override(t *testing.T) { + url, err := url.Parse("quic://dns.adguard-dns.com") + common.Must(err) + s, err := NewQUICNameServer(url, false, false, 0, net.IP(nil)) + common.Must(err) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) + ips, _, err := s.QueryIP(ctx, "google.com", dns.IPOption{ + IPv4Enable: true, + IPv6Enable: false, + }) + cancel() + common.Must(err) + if len(ips) == 0 { + t.Error("expect some ips, but got 0") + } + + for _, ip := range ips { + if len(ip) != net.IPv4len { + t.Error("expect only IPv4 response from DNS query") + } + } +} + +func TestQUICNameServerWithIPv6Override(t *testing.T) { + url, err := url.Parse("quic://dns.adguard-dns.com") + common.Must(err) + s, err := NewQUICNameServer(url, false, false, 0, net.IP(nil)) + common.Must(err) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) + ips, _, err := s.QueryIP(ctx, "google.com", dns.IPOption{ + IPv4Enable: false, + IPv6Enable: true, + }) + cancel() + common.Must(err) + if len(ips) == 0 { + t.Error("expect some ips, but got 0") + } + + for _, ip := range ips { + if len(ip) != net.IPv6len { + t.Error("expect only IPv6 response from DNS query") + } + } +} diff --git a/subproject/Xray-core-main/app/dns/nameserver_tcp.go b/subproject/Xray-core-main/app/dns/nameserver_tcp.go new file mode 100644 index 00000000..283a42a7 --- /dev/null +++ b/subproject/Xray-core-main/app/dns/nameserver_tcp.go @@ -0,0 +1,235 @@ +package dns + +import ( + "bytes" + "context" + "encoding/binary" + "net/url" + "sync/atomic" + "time" + + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/net/cnc" + "github.com/xtls/xray-core/common/protocol/dns" + "github.com/xtls/xray-core/common/session" + dns_feature "github.com/xtls/xray-core/features/dns" + "github.com/xtls/xray-core/features/routing" + "github.com/xtls/xray-core/transport/internet" +) + +// TCPNameServer implemented DNS over TCP (RFC7766). +type TCPNameServer struct { + cacheController *CacheController + destination *net.Destination + reqID uint32 + dial func(context.Context) (net.Conn, error) + clientIP net.IP +} + +// NewTCPNameServer creates DNS over TCP server object for remote resolving. +func NewTCPNameServer( + url *url.URL, + dispatcher routing.Dispatcher, + disableCache bool, serveStale bool, serveExpiredTTL uint32, + clientIP net.IP, +) (*TCPNameServer, error) { + s, err := baseTCPNameServer(url, "TCP", disableCache, serveStale, serveExpiredTTL, clientIP) + if err != nil { + return nil, err + } + + s.dial = func(ctx context.Context) (net.Conn, error) { + link, err := dispatcher.Dispatch(toDnsContext(ctx, s.destination.String()), *s.destination) + if err != nil { + return nil, err + } + + return cnc.NewConnection( + cnc.ConnectionInputMulti(link.Writer), + cnc.ConnectionOutputMulti(link.Reader), + ), nil + } + + errors.LogInfo(context.Background(), "DNS: created TCP client initialized for ", url.String()) + return s, nil +} + +// NewTCPLocalNameServer creates DNS over TCP client object for local resolving +func NewTCPLocalNameServer(url *url.URL, disableCache bool, serveStale bool, serveExpiredTTL uint32, clientIP net.IP) (*TCPNameServer, error) { + s, err := baseTCPNameServer(url, "TCPL", disableCache, serveStale, serveExpiredTTL, clientIP) + if err != nil { + return nil, err + } + + s.dial = func(ctx context.Context) (net.Conn, error) { + return internet.DialSystem(ctx, *s.destination, nil) + } + + errors.LogInfo(context.Background(), "DNS: created Local TCP client initialized for ", url.String()) + return s, nil +} + +func baseTCPNameServer(url *url.URL, prefix string, disableCache bool, serveStale bool, serveExpiredTTL uint32, clientIP net.IP) (*TCPNameServer, error) { + port := net.Port(53) + if url.Port() != "" { + var err error + if port, err = net.PortFromString(url.Port()); err != nil { + return nil, err + } + } + dest := net.TCPDestination(net.ParseAddress(url.Hostname()), port) + + s := &TCPNameServer{ + cacheController: NewCacheController(prefix+"//"+dest.NetAddr(), disableCache, serveStale, serveExpiredTTL), + destination: &dest, + clientIP: clientIP, + } + + return s, nil +} + +// Name implements Server. +func (s *TCPNameServer) Name() string { + return s.cacheController.name +} + +// IsDisableCache implements Server. +func (s *TCPNameServer) IsDisableCache() bool { + return s.cacheController.disableCache +} + +func (s *TCPNameServer) newReqID() uint16 { + return uint16(atomic.AddUint32(&s.reqID, 1)) +} + +// getCacheController implements CachedNameserver. +func (s *TCPNameServer) getCacheController() *CacheController { + return s.cacheController +} + +// sendQuery implements CachedNameserver. +func (s *TCPNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- error, fqdn string, option dns_feature.IPOption) { + errors.LogInfo(ctx, s.Name(), " querying DNS for: ", fqdn) + + reqs := buildReqMsgs(fqdn, option, s.newReqID, genEDNS0Options(s.clientIP, 0)) + + var deadline time.Time + if d, ok := ctx.Deadline(); ok { + deadline = d + } else { + deadline = time.Now().Add(time.Second * 5) + } + + for _, req := range reqs { + go func(r *dnsRequest) { + dnsCtx := ctx + + if inbound := session.InboundFromContext(ctx); inbound != nil { + dnsCtx = session.ContextWithInbound(dnsCtx, inbound) + } + + dnsCtx = session.ContextWithContent(dnsCtx, &session.Content{ + Protocol: "dns", + SkipDNSResolve: true, + }) + + var cancel context.CancelFunc + dnsCtx, cancel = context.WithDeadline(dnsCtx, deadline) + defer cancel() + + b, err := dns.PackMessage(r.msg) + if err != nil { + errors.LogErrorInner(ctx, err, "failed to pack dns query") + if noResponseErrCh != nil { + noResponseErrCh <- err + } + return + } + + conn, err := s.dial(dnsCtx) + if err != nil { + errors.LogErrorInner(ctx, err, "failed to dial namesever") + if noResponseErrCh != nil { + noResponseErrCh <- err + } + return + } + defer conn.Close() + dnsReqBuf := buf.New() + err = binary.Write(dnsReqBuf, binary.BigEndian, uint16(b.Len())) + if err != nil { + errors.LogErrorInner(ctx, err, "binary write failed") + if noResponseErrCh != nil { + noResponseErrCh <- err + } + return + } + _, err = dnsReqBuf.Write(b.Bytes()) + if err != nil { + errors.LogErrorInner(ctx, err, "buffer write failed") + if noResponseErrCh != nil { + noResponseErrCh <- err + } + return + } + b.Release() + + _, err = conn.Write(dnsReqBuf.Bytes()) + if err != nil { + errors.LogErrorInner(ctx, err, "failed to send query") + if noResponseErrCh != nil { + noResponseErrCh <- err + } + return + } + dnsReqBuf.Release() + + respBuf := buf.New() + defer respBuf.Release() + n, err := respBuf.ReadFullFrom(conn, 2) + if err != nil && n == 0 { + errors.LogErrorInner(ctx, err, "failed to read response length") + if noResponseErrCh != nil { + noResponseErrCh <- err + } + return + } + var length uint16 + err = binary.Read(bytes.NewReader(respBuf.Bytes()), binary.BigEndian, &length) + if err != nil { + errors.LogErrorInner(ctx, err, "failed to parse response length") + if noResponseErrCh != nil { + noResponseErrCh <- err + } + return + } + respBuf.Clear() + n, err = respBuf.ReadFullFrom(conn, int32(length)) + if err != nil && n == 0 { + errors.LogErrorInner(ctx, err, "failed to read response length") + if noResponseErrCh != nil { + noResponseErrCh <- err + } + return + } + + rec, err := parseResponse(respBuf.Bytes()) + if err != nil { + errors.LogErrorInner(ctx, err, "failed to parse DNS over TCP response") + if noResponseErrCh != nil { + noResponseErrCh <- err + } + return + } + + s.cacheController.updateRecord(r, rec) + }(req) + } +} + +// QueryIP implements Server. +func (s *TCPNameServer) QueryIP(ctx context.Context, domain string, option dns_feature.IPOption) ([]net.IP, uint32, error) { + return queryIP(ctx, s, domain, option) +} diff --git a/subproject/Xray-core-main/app/dns/nameserver_tcp_test.go b/subproject/Xray-core-main/app/dns/nameserver_tcp_test.go new file mode 100644 index 00000000..048e9477 --- /dev/null +++ b/subproject/Xray-core-main/app/dns/nameserver_tcp_test.go @@ -0,0 +1,107 @@ +package dns_test + +import ( + "context" + "net/url" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + . "github.com/xtls/xray-core/app/dns" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/net" + dns_feature "github.com/xtls/xray-core/features/dns" +) + +func TestTCPLocalNameServer(t *testing.T) { + url, err := url.Parse("tcp+local://8.8.8.8") + common.Must(err) + s, err := NewTCPLocalNameServer(url, false, false, 0, net.IP(nil)) + common.Must(err) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + ips, _, err := s.QueryIP(ctx, "google.com", dns_feature.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + }) + cancel() + common.Must(err) + if len(ips) == 0 { + t.Error("expect some ips, but got 0") + } +} + +func TestTCPLocalNameServerWithCache(t *testing.T) { + url, err := url.Parse("tcp+local://8.8.8.8") + common.Must(err) + s, err := NewTCPLocalNameServer(url, false, false, 0, net.IP(nil)) + common.Must(err) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + ips, _, err := s.QueryIP(ctx, "google.com", dns_feature.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + }) + cancel() + common.Must(err) + if len(ips) == 0 { + t.Error("expect some ips, but got 0") + } + + ctx2, cancel := context.WithTimeout(context.Background(), time.Second*5) + ips2, _, err := s.QueryIP(ctx2, "google.com", dns_feature.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + }) + cancel() + common.Must(err) + if r := cmp.Diff(ips2, ips); r != "" { + t.Fatal(r) + } +} + +func TestTCPLocalNameServerWithIPv4Override(t *testing.T) { + url, err := url.Parse("tcp+local://8.8.8.8") + common.Must(err) + s, err := NewTCPLocalNameServer(url, false, false, 0, net.IP(nil)) + common.Must(err) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + ips, _, err := s.QueryIP(ctx, "google.com", dns_feature.IPOption{ + IPv4Enable: true, + IPv6Enable: false, + }) + cancel() + common.Must(err) + + if len(ips) == 0 { + t.Error("expect some ips, but got 0") + } + + for _, ip := range ips { + if len(ip) != net.IPv4len { + t.Error("expect only IPv4 response from DNS query") + } + } +} + +func TestTCPLocalNameServerWithIPv6Override(t *testing.T) { + url, err := url.Parse("tcp+local://8.8.8.8") + common.Must(err) + s, err := NewTCPLocalNameServer(url, false, false, 0, net.IP(nil)) + common.Must(err) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + ips, _, err := s.QueryIP(ctx, "google.com", dns_feature.IPOption{ + IPv4Enable: false, + IPv6Enable: true, + }) + cancel() + common.Must(err) + + if len(ips) == 0 { + t.Error("expect some ips, but got 0") + } + + for _, ip := range ips { + if len(ip) != net.IPv6len { + t.Error("expect only IPv6 response from DNS query") + } + } +} diff --git a/subproject/Xray-core-main/app/dns/nameserver_udp.go b/subproject/Xray-core-main/app/dns/nameserver_udp.go new file mode 100644 index 00000000..55b825f7 --- /dev/null +++ b/subproject/Xray-core-main/app/dns/nameserver_udp.go @@ -0,0 +1,191 @@ +package dns + +import ( + "context" + "strings" + "sync" + "sync/atomic" + "time" + + "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/protocol/dns" + udp_proto "github.com/xtls/xray-core/common/protocol/udp" + "github.com/xtls/xray-core/common/task" + dns_feature "github.com/xtls/xray-core/features/dns" + "github.com/xtls/xray-core/features/routing" + "github.com/xtls/xray-core/transport/internet/udp" + "golang.org/x/net/dns/dnsmessage" +) + +// ClassicNameServer implemented traditional UDP DNS. +type ClassicNameServer struct { + sync.RWMutex + cacheController *CacheController + address *net.Destination + requests map[uint16]*udpDnsRequest + udpServer *udp.Dispatcher + requestsCleanup *task.Periodic + reqID uint32 + clientIP net.IP +} + +type udpDnsRequest struct { + dnsRequest + ctx context.Context +} + +// NewClassicNameServer creates udp server object for remote resolving. +func NewClassicNameServer(address net.Destination, dispatcher routing.Dispatcher, disableCache bool, serveStale bool, serveExpiredTTL uint32, clientIP net.IP) *ClassicNameServer { + // default to 53 if unspecific + if address.Port == 0 { + address.Port = net.Port(53) + } + + s := &ClassicNameServer{ + cacheController: NewCacheController(strings.ToUpper(address.String()), disableCache, serveStale, serveExpiredTTL), + address: &address, + requests: make(map[uint16]*udpDnsRequest), + clientIP: clientIP, + } + s.requestsCleanup = &task.Periodic{ + Interval: time.Minute, + Execute: s.RequestsCleanup, + } + s.udpServer = udp.NewDispatcher(dispatcher, s.HandleResponse) + + errors.LogInfo(context.Background(), "DNS: created UDP client initialized for ", address.NetAddr()) + return s +} + +// Name implements Server. +func (s *ClassicNameServer) Name() string { + return s.cacheController.name +} + +// IsDisableCache implements Server. +func (s *ClassicNameServer) IsDisableCache() bool { + return s.cacheController.disableCache +} + +// RequestsCleanup clears expired items from cache +func (s *ClassicNameServer) RequestsCleanup() error { + now := time.Now() + s.Lock() + defer s.Unlock() + + if len(s.requests) == 0 { + return errors.New(s.Name(), " nothing to do. stopping...") + } + + for id, req := range s.requests { + if req.expire.Before(now) { + delete(s.requests, id) + } + } + + if len(s.requests) == 0 { + s.requests = make(map[uint16]*udpDnsRequest) + } + + return nil +} + +// HandleResponse handles udp response packet from remote DNS server. +func (s *ClassicNameServer) HandleResponse(ctx context.Context, packet *udp_proto.Packet) { + payload := packet.Payload + ipRec, err := parseResponse(payload.Bytes()) + payload.Release() + if err != nil { + errors.LogErrorInner(ctx, err, s.Name(), " fail to parse responded DNS udp") + return + } + + s.Lock() + id := ipRec.ReqID + req, ok := s.requests[id] + if ok { + // remove the pending request + delete(s.requests, id) + } + s.Unlock() + if !ok { + errors.LogErrorInner(ctx, err, s.Name(), " cannot find the pending request") + return + } + + // if truncated, retry with EDNS0 option(udp payload size: 1350) + if ipRec.RawHeader.Truncated { + // if already has EDNS0 option, no need to retry + if len(req.msg.Additionals) == 0 { + // copy necessary meta data from original request + // and add EDNS0 option + 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) + copyDest := net.UDPDestination(s.address.Address, s.address.Port) + b.UDP = ©Dest + s.udpServer.Dispatch(toDnsContext(newReq.ctx, s.address.String()), *s.address, b) + return + } + } + + s.cacheController.updateRecord(&req.dnsRequest, ipRec) +} + +func (s *ClassicNameServer) newReqID() uint16 { + return uint16(atomic.AddUint32(&s.reqID, 1)) +} + +func (s *ClassicNameServer) addPendingRequest(req *udpDnsRequest) { + s.Lock() + id := req.msg.ID + req.expire = time.Now().Add(time.Second * 8) + s.requests[id] = req + s.Unlock() + common.Must(s.requestsCleanup.Start()) +} + +// getCacheController implements CachedNameserver. +func (s *ClassicNameServer) getCacheController() *CacheController { + return s.cacheController +} + +// sendQuery implements CachedNameserver. +func (s *ClassicNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- error, fqdn string, option dns_feature.IPOption) { + errors.LogInfo(ctx, s.Name(), " querying DNS for: ", fqdn) + + reqs := buildReqMsgs(fqdn, option, s.newReqID, genEDNS0Options(s.clientIP, 0)) + + for _, req := range reqs { + udpReq := &udpDnsRequest{ + dnsRequest: *req, + ctx: ctx, + } + s.addPendingRequest(udpReq) + b, err := dns.PackMessage(req.msg) + if err != nil { + errors.LogErrorInner(ctx, err, "failed to pack dns query") + if noResponseErrCh != nil { + noResponseErrCh <- err + } + return + } + copyDest := net.UDPDestination(s.address.Address, s.address.Port) + b.UDP = ©Dest + s.udpServer.Dispatch(toDnsContext(ctx, s.address.String()), *s.address, b) + } +} + +// QueryIP implements Server. +func (s *ClassicNameServer) QueryIP(ctx context.Context, domain string, option dns_feature.IPOption) ([]net.IP, uint32, error) { + return queryIP(ctx, s, domain, option) +} diff --git a/subproject/Xray-core-main/app/log/command/command.go b/subproject/Xray-core-main/app/log/command/command.go new file mode 100644 index 00000000..2df8ce6d --- /dev/null +++ b/subproject/Xray-core-main/app/log/command/command.go @@ -0,0 +1,55 @@ +package command + +import ( + "context" + + "github.com/xtls/xray-core/app/log" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/core" + grpc "google.golang.org/grpc" +) + +type LoggerServer struct { + V *core.Instance +} + +// RestartLogger implements LoggerService. +func (s *LoggerServer) RestartLogger(ctx context.Context, request *RestartLoggerRequest) (*RestartLoggerResponse, error) { + logger := s.V.GetFeature((*log.Instance)(nil)) + if logger == nil { + return nil, errors.New("unable to get logger instance") + } + if err := logger.Close(); err != nil { + return nil, errors.New("failed to close logger").Base(err) + } + if err := logger.Start(); err != nil { + return nil, errors.New("failed to start logger").Base(err) + } + return &RestartLoggerResponse{}, nil +} + +func (s *LoggerServer) mustEmbedUnimplementedLoggerServiceServer() {} + +type service struct { + v *core.Instance +} + +func (s *service) Register(server *grpc.Server) { + ls := &LoggerServer{ + V: s.v, + } + RegisterLoggerServiceServer(server, ls) + + // For compatibility purposes + vCoreDesc := LoggerService_ServiceDesc + vCoreDesc.ServiceName = "v2ray.core.app.log.command.LoggerService" + server.RegisterService(&vCoreDesc, ls) +} + +func init() { + common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, cfg interface{}) (interface{}, error) { + s := core.MustFromContext(ctx) + return &service{v: s}, nil + })) +} diff --git a/subproject/Xray-core-main/app/log/command/command_test.go b/subproject/Xray-core-main/app/log/command/command_test.go new file mode 100644 index 00000000..bd3970a5 --- /dev/null +++ b/subproject/Xray-core-main/app/log/command/command_test.go @@ -0,0 +1,34 @@ +package command_test + +import ( + "context" + "testing" + + "github.com/xtls/xray-core/app/dispatcher" + "github.com/xtls/xray-core/app/log" + . "github.com/xtls/xray-core/app/log/command" + "github.com/xtls/xray-core/app/proxyman" + _ "github.com/xtls/xray-core/app/proxyman/inbound" + _ "github.com/xtls/xray-core/app/proxyman/outbound" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/serial" + "github.com/xtls/xray-core/core" +) + +func TestLoggerRestart(t *testing.T) { + v, err := core.New(&core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{}), + serial.ToTypedMessage(&dispatcher.Config{}), + serial.ToTypedMessage(&proxyman.InboundConfig{}), + serial.ToTypedMessage(&proxyman.OutboundConfig{}), + }, + }) + common.Must(err) + common.Must(v.Start()) + + server := &LoggerServer{ + V: v, + } + common.Must2(server.RestartLogger(context.Background(), &RestartLoggerRequest{})) +} diff --git a/subproject/Xray-core-main/app/log/command/config.pb.go b/subproject/Xray-core-main/app/log/command/config.pb.go new file mode 100644 index 00000000..f9b9d5ef --- /dev/null +++ b/subproject/Xray-core-main/app/log/command/config.pb.go @@ -0,0 +1,194 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: app/log/command/config.proto + +package command + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_app_log_command_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_app_log_command_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_app_log_command_config_proto_rawDescGZIP(), []int{0} +} + +type RestartLoggerRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RestartLoggerRequest) Reset() { + *x = RestartLoggerRequest{} + mi := &file_app_log_command_config_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RestartLoggerRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RestartLoggerRequest) ProtoMessage() {} + +func (x *RestartLoggerRequest) ProtoReflect() protoreflect.Message { + mi := &file_app_log_command_config_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RestartLoggerRequest.ProtoReflect.Descriptor instead. +func (*RestartLoggerRequest) Descriptor() ([]byte, []int) { + return file_app_log_command_config_proto_rawDescGZIP(), []int{1} +} + +type RestartLoggerResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RestartLoggerResponse) Reset() { + *x = RestartLoggerResponse{} + mi := &file_app_log_command_config_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RestartLoggerResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RestartLoggerResponse) ProtoMessage() {} + +func (x *RestartLoggerResponse) ProtoReflect() protoreflect.Message { + mi := &file_app_log_command_config_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RestartLoggerResponse.ProtoReflect.Descriptor instead. +func (*RestartLoggerResponse) Descriptor() ([]byte, []int) { + return file_app_log_command_config_proto_rawDescGZIP(), []int{2} +} + +var File_app_log_command_config_proto protoreflect.FileDescriptor + +const file_app_log_command_config_proto_rawDesc = "" + + "\n" + + "\x1capp/log/command/config.proto\x12\x14xray.app.log.command\"\b\n" + + "\x06Config\"\x16\n" + + "\x14RestartLoggerRequest\"\x17\n" + + "\x15RestartLoggerResponse2{\n" + + "\rLoggerService\x12j\n" + + "\rRestartLogger\x12*.xray.app.log.command.RestartLoggerRequest\x1a+.xray.app.log.command.RestartLoggerResponse\"\x00B^\n" + + "\x18com.xray.app.log.commandP\x01Z)github.com/xtls/xray-core/app/log/command\xaa\x02\x14Xray.App.Log.Commandb\x06proto3" + +var ( + file_app_log_command_config_proto_rawDescOnce sync.Once + file_app_log_command_config_proto_rawDescData []byte +) + +func file_app_log_command_config_proto_rawDescGZIP() []byte { + file_app_log_command_config_proto_rawDescOnce.Do(func() { + file_app_log_command_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_app_log_command_config_proto_rawDesc), len(file_app_log_command_config_proto_rawDesc))) + }) + return file_app_log_command_config_proto_rawDescData +} + +var file_app_log_command_config_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_app_log_command_config_proto_goTypes = []any{ + (*Config)(nil), // 0: xray.app.log.command.Config + (*RestartLoggerRequest)(nil), // 1: xray.app.log.command.RestartLoggerRequest + (*RestartLoggerResponse)(nil), // 2: xray.app.log.command.RestartLoggerResponse +} +var file_app_log_command_config_proto_depIdxs = []int32{ + 1, // 0: xray.app.log.command.LoggerService.RestartLogger:input_type -> xray.app.log.command.RestartLoggerRequest + 2, // 1: xray.app.log.command.LoggerService.RestartLogger:output_type -> xray.app.log.command.RestartLoggerResponse + 1, // [1:2] is the sub-list for method output_type + 0, // [0:1] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_app_log_command_config_proto_init() } +func file_app_log_command_config_proto_init() { + if File_app_log_command_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_app_log_command_config_proto_rawDesc), len(file_app_log_command_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_app_log_command_config_proto_goTypes, + DependencyIndexes: file_app_log_command_config_proto_depIdxs, + MessageInfos: file_app_log_command_config_proto_msgTypes, + }.Build() + File_app_log_command_config_proto = out.File + file_app_log_command_config_proto_goTypes = nil + file_app_log_command_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/app/log/command/config.proto b/subproject/Xray-core-main/app/log/command/config.proto new file mode 100644 index 00000000..2ebf40aa --- /dev/null +++ b/subproject/Xray-core-main/app/log/command/config.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; + +package xray.app.log.command; +option csharp_namespace = "Xray.App.Log.Command"; +option go_package = "github.com/xtls/xray-core/app/log/command"; +option java_package = "com.xray.app.log.command"; +option java_multiple_files = true; + +message Config {} + +message RestartLoggerRequest {} + +message RestartLoggerResponse {} + +service LoggerService { + rpc RestartLogger(RestartLoggerRequest) returns (RestartLoggerResponse) {} +} diff --git a/subproject/Xray-core-main/app/log/command/config_grpc.pb.go b/subproject/Xray-core-main/app/log/command/config_grpc.pb.go new file mode 100644 index 00000000..8ebd60be --- /dev/null +++ b/subproject/Xray-core-main/app/log/command/config_grpc.pb.go @@ -0,0 +1,121 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.0 +// - protoc v6.33.5 +// source: app/log/command/config.proto + +package command + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + LoggerService_RestartLogger_FullMethodName = "/xray.app.log.command.LoggerService/RestartLogger" +) + +// LoggerServiceClient is the client API for LoggerService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type LoggerServiceClient interface { + RestartLogger(ctx context.Context, in *RestartLoggerRequest, opts ...grpc.CallOption) (*RestartLoggerResponse, error) +} + +type loggerServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewLoggerServiceClient(cc grpc.ClientConnInterface) LoggerServiceClient { + return &loggerServiceClient{cc} +} + +func (c *loggerServiceClient) RestartLogger(ctx context.Context, in *RestartLoggerRequest, opts ...grpc.CallOption) (*RestartLoggerResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(RestartLoggerResponse) + err := c.cc.Invoke(ctx, LoggerService_RestartLogger_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// LoggerServiceServer is the server API for LoggerService service. +// All implementations must embed UnimplementedLoggerServiceServer +// for forward compatibility. +type LoggerServiceServer interface { + RestartLogger(context.Context, *RestartLoggerRequest) (*RestartLoggerResponse, error) + mustEmbedUnimplementedLoggerServiceServer() +} + +// UnimplementedLoggerServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedLoggerServiceServer struct{} + +func (UnimplementedLoggerServiceServer) RestartLogger(context.Context, *RestartLoggerRequest) (*RestartLoggerResponse, error) { + return nil, status.Error(codes.Unimplemented, "method RestartLogger not implemented") +} +func (UnimplementedLoggerServiceServer) mustEmbedUnimplementedLoggerServiceServer() {} +func (UnimplementedLoggerServiceServer) testEmbeddedByValue() {} + +// UnsafeLoggerServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to LoggerServiceServer will +// result in compilation errors. +type UnsafeLoggerServiceServer interface { + mustEmbedUnimplementedLoggerServiceServer() +} + +func RegisterLoggerServiceServer(s grpc.ServiceRegistrar, srv LoggerServiceServer) { + // If the following call panics, it indicates UnimplementedLoggerServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&LoggerService_ServiceDesc, srv) +} + +func _LoggerService_RestartLogger_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RestartLoggerRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(LoggerServiceServer).RestartLogger(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: LoggerService_RestartLogger_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(LoggerServiceServer).RestartLogger(ctx, req.(*RestartLoggerRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// LoggerService_ServiceDesc is the grpc.ServiceDesc for LoggerService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var LoggerService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "xray.app.log.command.LoggerService", + HandlerType: (*LoggerServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "RestartLogger", + Handler: _LoggerService_RestartLogger_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "app/log/command/config.proto", +} diff --git a/subproject/Xray-core-main/app/log/config.pb.go b/subproject/Xray-core-main/app/log/config.pb.go new file mode 100644 index 00000000..e3a8f01c --- /dev/null +++ b/subproject/Xray-core-main/app/log/config.pb.go @@ -0,0 +1,242 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: app/log/config.proto + +package log + +import ( + log "github.com/xtls/xray-core/common/log" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type LogType int32 + +const ( + LogType_None LogType = 0 + LogType_Console LogType = 1 + LogType_File LogType = 2 + LogType_Event LogType = 3 +) + +// Enum value maps for LogType. +var ( + LogType_name = map[int32]string{ + 0: "None", + 1: "Console", + 2: "File", + 3: "Event", + } + LogType_value = map[string]int32{ + "None": 0, + "Console": 1, + "File": 2, + "Event": 3, + } +) + +func (x LogType) Enum() *LogType { + p := new(LogType) + *p = x + return p +} + +func (x LogType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (LogType) Descriptor() protoreflect.EnumDescriptor { + return file_app_log_config_proto_enumTypes[0].Descriptor() +} + +func (LogType) Type() protoreflect.EnumType { + return &file_app_log_config_proto_enumTypes[0] +} + +func (x LogType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use LogType.Descriptor instead. +func (LogType) EnumDescriptor() ([]byte, []int) { + return file_app_log_config_proto_rawDescGZIP(), []int{0} +} + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + ErrorLogType LogType `protobuf:"varint,1,opt,name=error_log_type,json=errorLogType,proto3,enum=xray.app.log.LogType" json:"error_log_type,omitempty"` + ErrorLogLevel log.Severity `protobuf:"varint,2,opt,name=error_log_level,json=errorLogLevel,proto3,enum=xray.common.log.Severity" json:"error_log_level,omitempty"` + ErrorLogPath string `protobuf:"bytes,3,opt,name=error_log_path,json=errorLogPath,proto3" json:"error_log_path,omitempty"` + AccessLogType LogType `protobuf:"varint,4,opt,name=access_log_type,json=accessLogType,proto3,enum=xray.app.log.LogType" json:"access_log_type,omitempty"` + AccessLogPath string `protobuf:"bytes,5,opt,name=access_log_path,json=accessLogPath,proto3" json:"access_log_path,omitempty"` + EnableDnsLog bool `protobuf:"varint,6,opt,name=enable_dns_log,json=enableDnsLog,proto3" json:"enable_dns_log,omitempty"` + MaskAddress string `protobuf:"bytes,7,opt,name=mask_address,json=maskAddress,proto3" json:"mask_address,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_app_log_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_app_log_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_app_log_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Config) GetErrorLogType() LogType { + if x != nil { + return x.ErrorLogType + } + return LogType_None +} + +func (x *Config) GetErrorLogLevel() log.Severity { + if x != nil { + return x.ErrorLogLevel + } + return log.Severity(0) +} + +func (x *Config) GetErrorLogPath() string { + if x != nil { + return x.ErrorLogPath + } + return "" +} + +func (x *Config) GetAccessLogType() LogType { + if x != nil { + return x.AccessLogType + } + return LogType_None +} + +func (x *Config) GetAccessLogPath() string { + if x != nil { + return x.AccessLogPath + } + return "" +} + +func (x *Config) GetEnableDnsLog() bool { + if x != nil { + return x.EnableDnsLog + } + return false +} + +func (x *Config) GetMaskAddress() string { + if x != nil { + return x.MaskAddress + } + return "" +} + +var File_app_log_config_proto protoreflect.FileDescriptor + +const file_app_log_config_proto_rawDesc = "" + + "\n" + + "\x14app/log/config.proto\x12\fxray.app.log\x1a\x14common/log/log.proto\"\xde\x02\n" + + "\x06Config\x12;\n" + + "\x0eerror_log_type\x18\x01 \x01(\x0e2\x15.xray.app.log.LogTypeR\ferrorLogType\x12A\n" + + "\x0ferror_log_level\x18\x02 \x01(\x0e2\x19.xray.common.log.SeverityR\rerrorLogLevel\x12$\n" + + "\x0eerror_log_path\x18\x03 \x01(\tR\ferrorLogPath\x12=\n" + + "\x0faccess_log_type\x18\x04 \x01(\x0e2\x15.xray.app.log.LogTypeR\raccessLogType\x12&\n" + + "\x0faccess_log_path\x18\x05 \x01(\tR\raccessLogPath\x12$\n" + + "\x0eenable_dns_log\x18\x06 \x01(\bR\fenableDnsLog\x12!\n" + + "\fmask_address\x18\a \x01(\tR\vmaskAddress*5\n" + + "\aLogType\x12\b\n" + + "\x04None\x10\x00\x12\v\n" + + "\aConsole\x10\x01\x12\b\n" + + "\x04File\x10\x02\x12\t\n" + + "\x05Event\x10\x03BF\n" + + "\x10com.xray.app.logP\x01Z!github.com/xtls/xray-core/app/log\xaa\x02\fXray.App.Logb\x06proto3" + +var ( + file_app_log_config_proto_rawDescOnce sync.Once + file_app_log_config_proto_rawDescData []byte +) + +func file_app_log_config_proto_rawDescGZIP() []byte { + file_app_log_config_proto_rawDescOnce.Do(func() { + file_app_log_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_app_log_config_proto_rawDesc), len(file_app_log_config_proto_rawDesc))) + }) + return file_app_log_config_proto_rawDescData +} + +var file_app_log_config_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_app_log_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_app_log_config_proto_goTypes = []any{ + (LogType)(0), // 0: xray.app.log.LogType + (*Config)(nil), // 1: xray.app.log.Config + (log.Severity)(0), // 2: xray.common.log.Severity +} +var file_app_log_config_proto_depIdxs = []int32{ + 0, // 0: xray.app.log.Config.error_log_type:type_name -> xray.app.log.LogType + 2, // 1: xray.app.log.Config.error_log_level:type_name -> xray.common.log.Severity + 0, // 2: xray.app.log.Config.access_log_type:type_name -> xray.app.log.LogType + 3, // [3:3] is the sub-list for method output_type + 3, // [3:3] is the sub-list for method input_type + 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_log_config_proto_init() } +func file_app_log_config_proto_init() { + if File_app_log_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_app_log_config_proto_rawDesc), len(file_app_log_config_proto_rawDesc)), + NumEnums: 1, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_app_log_config_proto_goTypes, + DependencyIndexes: file_app_log_config_proto_depIdxs, + EnumInfos: file_app_log_config_proto_enumTypes, + MessageInfos: file_app_log_config_proto_msgTypes, + }.Build() + File_app_log_config_proto = out.File + file_app_log_config_proto_goTypes = nil + file_app_log_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/app/log/config.proto b/subproject/Xray-core-main/app/log/config.proto new file mode 100644 index 00000000..8dc729d0 --- /dev/null +++ b/subproject/Xray-core-main/app/log/config.proto @@ -0,0 +1,27 @@ +syntax = "proto3"; + +package xray.app.log; +option csharp_namespace = "Xray.App.Log"; +option go_package = "github.com/xtls/xray-core/app/log"; +option java_package = "com.xray.app.log"; +option java_multiple_files = true; + +import "common/log/log.proto"; + +enum LogType { + None = 0; + Console = 1; + File = 2; + Event = 3; +} + +message Config { + LogType error_log_type = 1; + xray.common.log.Severity error_log_level = 2; + string error_log_path = 3; + + LogType access_log_type = 4; + string access_log_path = 5; + bool enable_dns_log = 6; + string mask_address= 7; +} diff --git a/subproject/Xray-core-main/app/log/log.go b/subproject/Xray-core-main/app/log/log.go new file mode 100644 index 00000000..553862b1 --- /dev/null +++ b/subproject/Xray-core-main/app/log/log.go @@ -0,0 +1,256 @@ +package log + +import ( + "context" + "net" + "regexp" + "strconv" + "strings" + "sync" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/log" +) + +// Instance is a log.Handler that handles logs. +type Instance struct { + sync.RWMutex + config *Config + accessLogger log.Handler + errorLogger log.Handler + active bool + dns bool + mask4 int + mask6 int +} + +// New creates a new log.Instance based on the given config. +func New(ctx context.Context, config *Config) (*Instance, error) { + m4, m6, err := ParseMaskAddress(config.MaskAddress) + if err != nil { + return nil, err + } + + g := &Instance{ + config: config, + active: false, + dns: config.EnableDnsLog, + mask4: m4, + mask6: m6, + } + log.RegisterHandler(g) + + // start logger now, + // then other modules will be able to log during initialization + if err := g.startInternal(); err != nil { + return nil, err + } + + errors.LogDebug(ctx, "Logger started") + return g, nil +} + +func (g *Instance) initAccessLogger() error { + handler, err := createHandler(g.config.AccessLogType, HandlerCreatorOptions{ + Path: g.config.AccessLogPath, + }) + if err != nil { + return err + } + g.accessLogger = handler + return nil +} + +func (g *Instance) initErrorLogger() error { + handler, err := createHandler(g.config.ErrorLogType, HandlerCreatorOptions{ + Path: g.config.ErrorLogPath, + }) + if err != nil { + return err + } + g.errorLogger = handler + return nil +} + +// Type implements common.HasType. +func (*Instance) Type() interface{} { + return (*Instance)(nil) +} + +func (g *Instance) startInternal() error { + g.Lock() + defer g.Unlock() + + if g.active { + return nil + } + + g.active = true + + if err := g.initAccessLogger(); err != nil { + return errors.New("failed to initialize access logger").Base(err).AtWarning() + } + if err := g.initErrorLogger(); err != nil { + return errors.New("failed to initialize error logger").Base(err).AtWarning() + } + + return nil +} + +// Start implements common.Runnable.Start(). +func (g *Instance) Start() error { + return g.startInternal() +} + +// Handle implements log.Handler. +func (g *Instance) Handle(msg log.Message) { + g.RLock() + defer g.RUnlock() + + if !g.active { + return + } + + var Msg log.Message + if g.config.MaskAddress != "" { + Msg = &MaskedMsgWrapper{ + Message: msg, + Mask4: g.mask4, + Mask6: g.mask6, + } + } else { + Msg = msg + } + + switch msg := msg.(type) { + case *log.AccessMessage: + if g.accessLogger != nil { + g.accessLogger.Handle(Msg) + } + case *log.DNSLog: + if g.dns && g.accessLogger != nil { + g.accessLogger.Handle(Msg) + } + case *log.GeneralMessage: + if g.errorLogger != nil && msg.Severity <= g.config.ErrorLogLevel { + g.errorLogger.Handle(Msg) + } + default: + // Swallow + } +} + +// Close implements common.Closable.Close(). +func (g *Instance) Close() error { + errors.LogDebug(context.Background(), "Logger closing") + + g.Lock() + defer g.Unlock() + + if !g.active { + return nil + } + + g.active = false + + common.Close(g.accessLogger) + g.accessLogger = nil + + common.Close(g.errorLogger) + g.errorLogger = nil + + return nil +} + +func ParseMaskAddress(c string) (int, int, error) { + var m4, m6 int + switch c { + case "half": + m4, m6 = 16, 32 + case "quarter": + m4, m6 = 8, 16 + case "full": + m4, m6 = 0, 0 + case "": + // do nothing + default: + if parts := strings.Split(c, "+"); len(parts) > 0 { + if len(parts) >= 1 && parts[0] != "" { + i, err := strconv.Atoi(strings.TrimPrefix(parts[0], "/")) + if err != nil { + return 32, 128, err + } + m4 = i + } + if len(parts) >= 2 && parts[1] != "" { + i, err := strconv.Atoi(strings.TrimPrefix(parts[1], "/")) + if err != nil { + return 32, 128, err + } + m6 = i + } + } + } + + if m4%8 != 0 || m4 > 32 || m4 < 0 { + return 32, 128, errors.New("Log Mask: ipv4 mask must be divisible by 8 and between 0-32") + } + + return m4, m6, nil +} + +// MaskedMsgWrapper is to wrap the string() method to mask IP addresses in the log. +type MaskedMsgWrapper struct { + log.Message + Mask4 int + Mask6 int +} + +var ( + ipv4Regex = regexp.MustCompile(`(\d{1,3}\.){3}\d{1,3}`) + ipv6Regex = regexp.MustCompile(`(?:[\da-fA-F]{0,4}:[\da-fA-F]{0,4}){2,7}`) +) + +func (m *MaskedMsgWrapper) String() string { + str := m.Message.String() + + // Process ipv4 + maskedMsg := ipv4Regex.ReplaceAllStringFunc(str, func(s string) string { + if m.Mask4 == 32 { + return s + } + if m.Mask4 == 0 { + return "[Masked IPv4]" + } + + parts := strings.Split(s, ".") + for i := m.Mask4 / 8; i < 4; i++ { + parts[i] = "*" + } + return strings.Join(parts, ".") + }) + + // process ipv6 + maskedMsg = ipv6Regex.ReplaceAllStringFunc(maskedMsg, func(s string) string { + if m.Mask6 == 128 { + return s + } + if m.Mask6 == 0 { + return "Masked IPv6" + } + ip := net.ParseIP(s) + if ip == nil { + return s + } + return ip.Mask(net.CIDRMask(m.Mask6, 128)).String() + "/" + strconv.Itoa(m.Mask6) + }) + + return maskedMsg +} + +func init() { + common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + return New(ctx, config.(*Config)) + })) +} diff --git a/subproject/Xray-core-main/app/log/log_creator.go b/subproject/Xray-core-main/app/log/log_creator.go new file mode 100644 index 00000000..48287271 --- /dev/null +++ b/subproject/Xray-core-main/app/log/log_creator.go @@ -0,0 +1,60 @@ +package log + +import ( + "sync" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/log" +) + +type HandlerCreatorOptions struct { + Path string +} + +type HandlerCreator func(LogType, HandlerCreatorOptions) (log.Handler, error) + +var handlerCreatorMap = make(map[LogType]HandlerCreator) + +var handlerCreatorMapLock = &sync.RWMutex{} + +func RegisterHandlerCreator(logType LogType, f HandlerCreator) error { + if f == nil { + return errors.New("nil HandlerCreator") + } + + handlerCreatorMapLock.Lock() + defer handlerCreatorMapLock.Unlock() + + handlerCreatorMap[logType] = f + return nil +} + +func createHandler(logType LogType, options HandlerCreatorOptions) (log.Handler, error) { + handlerCreatorMapLock.RLock() + defer handlerCreatorMapLock.RUnlock() + + creator, found := handlerCreatorMap[logType] + if !found { + return nil, errors.New("unable to create log handler for ", logType) + } + return creator(logType, options) +} + +func init() { + common.Must(RegisterHandlerCreator(LogType_Console, func(lt LogType, options HandlerCreatorOptions) (log.Handler, error) { + return log.NewLogger(log.CreateStdoutLogWriter()), nil + })) + + common.Must(RegisterHandlerCreator(LogType_File, func(lt LogType, options HandlerCreatorOptions) (log.Handler, error) { + creator, err := log.CreateFileLogWriter(options.Path) + if err != nil { + return nil, err + } + return log.NewLogger(creator), nil + })) + + common.Must(RegisterHandlerCreator(LogType_None, func(lt LogType, options HandlerCreatorOptions) (log.Handler, error) { + return nil, nil + })) +} diff --git a/subproject/Xray-core-main/app/log/log_test.go b/subproject/Xray-core-main/app/log/log_test.go new file mode 100644 index 00000000..9a40e975 --- /dev/null +++ b/subproject/Xray-core-main/app/log/log_test.go @@ -0,0 +1,89 @@ +package log_test + +import ( + "context" + "net" + "testing" + + "github.com/golang/mock/gomock" + "github.com/xtls/xray-core/app/log" + "github.com/xtls/xray-core/common" + clog "github.com/xtls/xray-core/common/log" + "github.com/xtls/xray-core/testing/mocks" +) + +func TestCustomLogHandler(t *testing.T) { + mockCtl := gomock.NewController(t) + defer mockCtl.Finish() + + var loggedValue []string + + mockHandler := mocks.NewLogHandler(mockCtl) + mockHandler.EXPECT().Handle(gomock.Any()).AnyTimes().DoAndReturn(func(msg clog.Message) { + loggedValue = append(loggedValue, msg.String()) + }) + + log.RegisterHandlerCreator(log.LogType_Console, func(lt log.LogType, options log.HandlerCreatorOptions) (clog.Handler, error) { + return mockHandler, nil + }) + + logger, err := log.New(context.Background(), &log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + AccessLogType: log.LogType_None, + }) + common.Must(err) + + common.Must(logger.Start()) + + clog.Record(&clog.GeneralMessage{ + Severity: clog.Severity_Debug, + Content: "test", + }) + + if len(loggedValue) < 2 { + t.Fatal("expected 2 log messages, but actually ", loggedValue) + } + + if loggedValue[1] != "[Debug] test" { + t.Fatal("expected '[Debug] test', but actually ", loggedValue[1]) + } + + common.Must(logger.Close()) +} + +func TestMaskAddress(t *testing.T) { + m4, m6, err := log.ParseMaskAddress("half") + if err != nil { + t.Fatal(err) + } + maskedAddr := log.MaskedMsgWrapper{ + Mask4: m4, + Mask6: m6, + } + maskedAddr.Message = net.ParseIP("11.45.1.4") + if maskedAddr.String() != "11.45.*.*" { + t.Fatal("expected '11.45.*.*', but actually ", maskedAddr.String()) + } + maskedAddr.Message = net.ParseIP("11:45:14:19:19:81:0::") + if maskedAddr.String() != "11:45::/32" { + t.Fatal("expected '11:45::/32', but actually", maskedAddr.String()) + } + + m4, m6, err = log.ParseMaskAddress("/16+/64") + if err != nil { + t.Fatal(err) + } + maskedAddr = log.MaskedMsgWrapper{ + Mask4: m4, + Mask6: m6, + } + maskedAddr.Message = net.ParseIP("11.45.1.4") + if maskedAddr.String() != "11.45.*.*" { + t.Fatal("expected '11.45.*.*', but actually ", maskedAddr.String()) + } + maskedAddr.Message = net.ParseIP("11:45:14:19:19:81:0::") + if maskedAddr.String() != "11:45:14:19::/64" { + t.Fatal("expected '11:45:14:19::/64', but actually", maskedAddr.String()) + } +} diff --git a/subproject/Xray-core-main/app/metrics/config.pb.go b/subproject/Xray-core-main/app/metrics/config.pb.go new file mode 100644 index 00000000..750b35ca --- /dev/null +++ b/subproject/Xray-core-main/app/metrics/config.pb.go @@ -0,0 +1,134 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: app/metrics/config.proto + +package metrics + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Config is the settings for metrics. +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Tag of the outbound handler that handles metrics http connections. + Tag string `protobuf:"bytes,1,opt,name=tag,proto3" json:"tag,omitempty"` + Listen string `protobuf:"bytes,2,opt,name=listen,proto3" json:"listen,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_app_metrics_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_app_metrics_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_app_metrics_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Config) GetTag() string { + if x != nil { + return x.Tag + } + return "" +} + +func (x *Config) GetListen() string { + if x != nil { + return x.Listen + } + return "" +} + +var File_app_metrics_config_proto protoreflect.FileDescriptor + +const file_app_metrics_config_proto_rawDesc = "" + + "\n" + + "\x18app/metrics/config.proto\x12\x10xray.app.metrics\"2\n" + + "\x06Config\x12\x10\n" + + "\x03tag\x18\x01 \x01(\tR\x03tag\x12\x16\n" + + "\x06listen\x18\x02 \x01(\tR\x06listenBR\n" + + "\x14com.xray.app.metricsP\x01Z%github.com/xtls/xray-core/app/metrics\xaa\x02\x10Xray.App.Metricsb\x06proto3" + +var ( + file_app_metrics_config_proto_rawDescOnce sync.Once + file_app_metrics_config_proto_rawDescData []byte +) + +func file_app_metrics_config_proto_rawDescGZIP() []byte { + file_app_metrics_config_proto_rawDescOnce.Do(func() { + file_app_metrics_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_app_metrics_config_proto_rawDesc), len(file_app_metrics_config_proto_rawDesc))) + }) + return file_app_metrics_config_proto_rawDescData +} + +var file_app_metrics_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_app_metrics_config_proto_goTypes = []any{ + (*Config)(nil), // 0: xray.app.metrics.Config +} +var file_app_metrics_config_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_app_metrics_config_proto_init() } +func file_app_metrics_config_proto_init() { + if File_app_metrics_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_app_metrics_config_proto_rawDesc), len(file_app_metrics_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_app_metrics_config_proto_goTypes, + DependencyIndexes: file_app_metrics_config_proto_depIdxs, + MessageInfos: file_app_metrics_config_proto_msgTypes, + }.Build() + File_app_metrics_config_proto = out.File + file_app_metrics_config_proto_goTypes = nil + file_app_metrics_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/app/metrics/config.proto b/subproject/Xray-core-main/app/metrics/config.proto new file mode 100644 index 00000000..0441df03 --- /dev/null +++ b/subproject/Xray-core-main/app/metrics/config.proto @@ -0,0 +1,14 @@ +syntax = "proto3"; + +package xray.app.metrics; +option csharp_namespace = "Xray.App.Metrics"; +option go_package = "github.com/xtls/xray-core/app/metrics"; +option java_package = "com.xray.app.metrics"; +option java_multiple_files = true; + +// Config is the settings for metrics. +message Config { + // Tag of the outbound handler that handles metrics http connections. + string tag = 1; + string listen = 2; +} diff --git a/subproject/Xray-core-main/app/metrics/metrics.go b/subproject/Xray-core-main/app/metrics/metrics.go new file mode 100644 index 00000000..1dc5b2f5 --- /dev/null +++ b/subproject/Xray-core-main/app/metrics/metrics.go @@ -0,0 +1,139 @@ +package metrics + +import ( + "context" + "expvar" + "net/http" + _ "net/http/pprof" + "strings" + + "github.com/xtls/xray-core/app/observatory" + "github.com/xtls/xray-core/app/stats" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/signal/done" + "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/features/extension" + "github.com/xtls/xray-core/features/outbound" + feature_stats "github.com/xtls/xray-core/features/stats" +) + +type MetricsHandler struct { + ohm outbound.Manager + statsManager feature_stats.Manager + observatory extension.Observatory + tag string + listen string + tcpListener net.Listener +} + +// NewMetricsHandler creates a new MetricsHandler based on the given config. +func NewMetricsHandler(ctx context.Context, config *Config) (*MetricsHandler, error) { + c := &MetricsHandler{ + tag: config.Tag, + listen: config.Listen, + } + common.Must(core.RequireFeatures(ctx, func(om outbound.Manager, sm feature_stats.Manager) { + c.statsManager = sm + c.ohm = om + })) + expvar.Publish("stats", expvar.Func(func() interface{} { + manager, ok := c.statsManager.(*stats.Manager) + if !ok { + return nil + } + resp := map[string]map[string]map[string]int64{ + "inbound": {}, + "outbound": {}, + "user": {}, + } + manager.VisitCounters(func(name string, counter feature_stats.Counter) bool { + nameSplit := strings.Split(name, ">>>") + typeName, tagOrUser, direction := nameSplit[0], nameSplit[1], nameSplit[3] + if item, found := resp[typeName][tagOrUser]; found { + item[direction] = counter.Value() + } else { + resp[typeName][tagOrUser] = map[string]int64{ + direction: counter.Value(), + } + } + return true + }) + return resp + })) + expvar.Publish("observatory", expvar.Func(func() interface{} { + if c.observatory == nil { + common.Must(core.RequireFeatures(ctx, func(observatory extension.Observatory) error { + c.observatory = observatory + return nil + })) + if c.observatory == nil { + return nil + } + } + resp := map[string]*observatory.OutboundStatus{} + if o, err := c.observatory.GetObservation(context.Background()); err != nil { + return err + } else { + for _, x := range o.(*observatory.ObservationResult).GetStatus() { + resp[x.OutboundTag] = x + } + } + return resp + })) + return c, nil +} + +func (p *MetricsHandler) Type() interface{} { + return (*MetricsHandler)(nil) +} + +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{ + buffer: make(chan net.Conn, 4), + done: done.New(), + } + + go func() { + if err := http.Serve(listener, http.DefaultServeMux); err != nil { + errors.LogErrorInner(context.Background(), err, "failed to start metrics server") + } + }() + + if err := p.ohm.RemoveHandler(context.Background(), p.tag); err != nil { + errors.LogInfo(context.Background(), "failed to remove existing handler") + } + + return p.ohm.AddHandler(context.Background(), &Outbound{ + tag: p.tag, + listener: listener, + }) +} + +func (p *MetricsHandler) Close() error { + return nil +} + +func init() { + common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, cfg interface{}) (interface{}, error) { + return NewMetricsHandler(ctx, cfg.(*Config)) + })) +} diff --git a/subproject/Xray-core-main/app/metrics/outbound.go b/subproject/Xray-core-main/app/metrics/outbound.go new file mode 100644 index 00000000..e55b33fb --- /dev/null +++ b/subproject/Xray-core-main/app/metrics/outbound.go @@ -0,0 +1,121 @@ +package metrics + +import ( + "context" + "sync" + + "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/net/cnc" + "github.com/xtls/xray-core/common/serial" + "github.com/xtls/xray-core/common/signal/done" + "github.com/xtls/xray-core/transport" +) + +// OutboundListener is a net.Listener for listening metrics http connections. +type OutboundListener struct { + buffer chan net.Conn + done *done.Instance +} + +func (l *OutboundListener) add(conn net.Conn) { + select { + case l.buffer <- conn: + case <-l.done.Wait(): + conn.Close() + default: + conn.Close() + } +} + +// Accept implements net.Listener. +func (l *OutboundListener) Accept() (net.Conn, error) { + select { + case <-l.done.Wait(): + return nil, errors.New("listen closed") + case c := <-l.buffer: + return c, nil + } +} + +// Close implement net.Listener. +func (l *OutboundListener) Close() error { + common.Must(l.done.Close()) +L: + for { + select { + case c := <-l.buffer: + c.Close() + default: + break L + } + } + return nil +} + +// Addr implements net.Listener. +func (l *OutboundListener) Addr() net.Addr { + return &net.TCPAddr{ + IP: net.IP{0, 0, 0, 0}, + Port: 0, + } +} + +// Outbound is an outbound.Handler that handles metrics http connections. +type Outbound struct { + tag string + listener *OutboundListener + access sync.RWMutex + closed bool +} + +// Dispatch implements outbound.Handler. +func (co *Outbound) Dispatch(ctx context.Context, link *transport.Link) { + co.access.RLock() + + if co.closed { + common.Interrupt(link.Reader) + common.Interrupt(link.Writer) + co.access.RUnlock() + return + } + + closeSignal := done.New() + c := cnc.NewConnection(cnc.ConnectionInputMulti(link.Writer), cnc.ConnectionOutputMulti(link.Reader), cnc.ConnectionOnClose(closeSignal)) + co.listener.add(c) + co.access.RUnlock() + <-closeSignal.Wait() +} + +// Tag implements outbound.Handler. +func (co *Outbound) Tag() string { + return co.tag +} + +// Start implements common.Runnable. +func (co *Outbound) Start() error { + co.access.Lock() + co.closed = false + co.access.Unlock() + return nil +} + +// Close implements common.Closable. +func (co *Outbound) Close() error { + co.access.Lock() + defer co.access.Unlock() + + co.closed = true + return co.listener.Close() +} + +// SenderSettings implements outbound.Handler. +func (co *Outbound) SenderSettings() *serial.TypedMessage { + return nil +} + +// ProxySettings implements outbound.Handler. +func (co *Outbound) ProxySettings() *serial.TypedMessage { + return nil +} diff --git a/subproject/Xray-core-main/app/observatory/burst/burst.go b/subproject/Xray-core-main/app/observatory/burst/burst.go new file mode 100644 index 00000000..8658086a --- /dev/null +++ b/subproject/Xray-core-main/app/observatory/burst/burst.go @@ -0,0 +1,12 @@ +package burst + +import ( + "math" + "time" +) + +const ( + rttFailed = time.Duration(math.MaxInt64 - iota) + rttUntested + rttUnqualified +) diff --git a/subproject/Xray-core-main/app/observatory/burst/burstobserver.go b/subproject/Xray-core-main/app/observatory/burst/burstobserver.go new file mode 100644 index 00000000..9af96a1d --- /dev/null +++ b/subproject/Xray-core-main/app/observatory/burst/burstobserver.go @@ -0,0 +1,117 @@ +package burst + +import ( + "context" + + "sync" + + "github.com/xtls/xray-core/app/observatory" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/signal/done" + "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/features/extension" + "github.com/xtls/xray-core/features/outbound" + "github.com/xtls/xray-core/features/routing" + "google.golang.org/protobuf/proto" +) + +type Observer struct { + config *Config + ctx context.Context + + statusLock sync.Mutex + hp *HealthPing + + finished *done.Instance + + ohm outbound.Manager +} + +func (o *Observer) GetObservation(ctx context.Context) (proto.Message, error) { + return &observatory.ObservationResult{Status: o.createResult()}, nil +} + +func (o *Observer) Check(tag []string) { + o.hp.Check(tag) +} + +func (o *Observer) createResult() []*observatory.OutboundStatus { + var result []*observatory.OutboundStatus + o.hp.access.Lock() + defer o.hp.access.Unlock() + for name, value := range o.hp.Results { + status := observatory.OutboundStatus{ + Alive: value.getStatistics().All != value.getStatistics().Fail, + Delay: value.getStatistics().Average.Milliseconds(), + LastErrorReason: "", + OutboundTag: name, + LastSeenTime: 0, + LastTryTime: 0, + HealthPing: &observatory.HealthPingMeasurementResult{ + All: int64(value.getStatistics().All), + Fail: int64(value.getStatistics().Fail), + Deviation: int64(value.getStatistics().Deviation), + Average: int64(value.getStatistics().Average), + Max: int64(value.getStatistics().Max), + Min: int64(value.getStatistics().Min), + }, + } + result = append(result, &status) + } + return result +} + +func (o *Observer) Type() interface{} { + return extension.ObservatoryType() +} + +func (o *Observer) Start() error { + if o.config != nil && len(o.config.SubjectSelector) != 0 { + o.finished = done.New() + o.hp.StartScheduler(func() ([]string, error) { + hs, ok := o.ohm.(outbound.HandlerSelector) + if !ok { + + return nil, errors.New("outbound.Manager is not a HandlerSelector") + } + + outbounds := hs.Select(o.config.SubjectSelector) + return outbounds, nil + }) + } + return nil +} + +func (o *Observer) Close() error { + if o.finished != nil { + o.hp.StopScheduler() + return o.finished.Close() + } + return nil +} + +func New(ctx context.Context, config *Config) (*Observer, error) { + var outboundManager outbound.Manager + var dispatcher routing.Dispatcher + err := core.RequireFeatures(ctx, func(om outbound.Manager, rd routing.Dispatcher) { + outboundManager = om + dispatcher = rd + }) + if err != nil { + return nil, errors.New("Cannot get depended features").Base(err) + } + hp := NewHealthPing(ctx, dispatcher, config.PingConfig) + return &Observer{ + config: config, + ctx: ctx, + ohm: outboundManager, + hp: hp, + }, nil +} + +func init() { + common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + return New(ctx, config.(*Config)) + })) +} diff --git a/subproject/Xray-core-main/app/observatory/burst/config.pb.go b/subproject/Xray-core-main/app/observatory/burst/config.pb.go new file mode 100644 index 00000000..b698e036 --- /dev/null +++ b/subproject/Xray-core-main/app/observatory/burst/config.pb.go @@ -0,0 +1,236 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: app/observatory/burst/config.proto + +package burst + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + // @Document The selectors for outbound under observation + SubjectSelector []string `protobuf:"bytes,2,rep,name=subject_selector,json=subjectSelector,proto3" json:"subject_selector,omitempty"` + PingConfig *HealthPingConfig `protobuf:"bytes,3,opt,name=ping_config,json=pingConfig,proto3" json:"ping_config,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_app_observatory_burst_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_app_observatory_burst_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_app_observatory_burst_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Config) GetSubjectSelector() []string { + if x != nil { + return x.SubjectSelector + } + return nil +} + +func (x *Config) GetPingConfig() *HealthPingConfig { + if x != nil { + return x.PingConfig + } + return nil +} + +type HealthPingConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + // destination url, need 204 for success return + // default https://connectivitycheck.gstatic.com/generate_204 + Destination string `protobuf:"bytes,1,opt,name=destination,proto3" json:"destination,omitempty"` + // connectivity check url + Connectivity string `protobuf:"bytes,2,opt,name=connectivity,proto3" json:"connectivity,omitempty"` + // health check interval, int64 values of time.Duration + Interval int64 `protobuf:"varint,3,opt,name=interval,proto3" json:"interval,omitempty"` + // sampling count is the amount of recent ping results which are kept for calculation + SamplingCount int32 `protobuf:"varint,4,opt,name=samplingCount,proto3" json:"samplingCount,omitempty"` + // ping timeout, int64 values of time.Duration + Timeout int64 `protobuf:"varint,5,opt,name=timeout,proto3" json:"timeout,omitempty"` + // http method to make request + HttpMethod string `protobuf:"bytes,6,opt,name=httpMethod,proto3" json:"httpMethod,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HealthPingConfig) Reset() { + *x = HealthPingConfig{} + mi := &file_app_observatory_burst_config_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HealthPingConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HealthPingConfig) ProtoMessage() {} + +func (x *HealthPingConfig) ProtoReflect() protoreflect.Message { + mi := &file_app_observatory_burst_config_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HealthPingConfig.ProtoReflect.Descriptor instead. +func (*HealthPingConfig) Descriptor() ([]byte, []int) { + return file_app_observatory_burst_config_proto_rawDescGZIP(), []int{1} +} + +func (x *HealthPingConfig) GetDestination() string { + if x != nil { + return x.Destination + } + return "" +} + +func (x *HealthPingConfig) GetConnectivity() string { + if x != nil { + return x.Connectivity + } + return "" +} + +func (x *HealthPingConfig) GetInterval() int64 { + if x != nil { + return x.Interval + } + return 0 +} + +func (x *HealthPingConfig) GetSamplingCount() int32 { + if x != nil { + return x.SamplingCount + } + return 0 +} + +func (x *HealthPingConfig) GetTimeout() int64 { + if x != nil { + return x.Timeout + } + return 0 +} + +func (x *HealthPingConfig) GetHttpMethod() string { + if x != nil { + return x.HttpMethod + } + return "" +} + +var File_app_observatory_burst_config_proto protoreflect.FileDescriptor + +const file_app_observatory_burst_config_proto_rawDesc = "" + + "\n" + + "\"app/observatory/burst/config.proto\x12\x1fxray.core.app.observatory.burst\"\x87\x01\n" + + "\x06Config\x12)\n" + + "\x10subject_selector\x18\x02 \x03(\tR\x0fsubjectSelector\x12R\n" + + "\vping_config\x18\x03 \x01(\v21.xray.core.app.observatory.burst.HealthPingConfigR\n" + + "pingConfig\"\xd4\x01\n" + + "\x10HealthPingConfig\x12 \n" + + "\vdestination\x18\x01 \x01(\tR\vdestination\x12\"\n" + + "\fconnectivity\x18\x02 \x01(\tR\fconnectivity\x12\x1a\n" + + "\binterval\x18\x03 \x01(\x03R\binterval\x12$\n" + + "\rsamplingCount\x18\x04 \x01(\x05R\rsamplingCount\x12\x18\n" + + "\atimeout\x18\x05 \x01(\x03R\atimeout\x12\x1e\n" + + "\n" + + "httpMethod\x18\x06 \x01(\tR\n" + + "httpMethodBp\n" + + "\x1ecom.xray.app.observatory.burstP\x01Z/github.com/xtls/xray-core/app/observatory/burst\xaa\x02\x1aXray.App.Observatory.Burstb\x06proto3" + +var ( + file_app_observatory_burst_config_proto_rawDescOnce sync.Once + file_app_observatory_burst_config_proto_rawDescData []byte +) + +func file_app_observatory_burst_config_proto_rawDescGZIP() []byte { + file_app_observatory_burst_config_proto_rawDescOnce.Do(func() { + file_app_observatory_burst_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_app_observatory_burst_config_proto_rawDesc), len(file_app_observatory_burst_config_proto_rawDesc))) + }) + return file_app_observatory_burst_config_proto_rawDescData +} + +var file_app_observatory_burst_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_app_observatory_burst_config_proto_goTypes = []any{ + (*Config)(nil), // 0: xray.core.app.observatory.burst.Config + (*HealthPingConfig)(nil), // 1: xray.core.app.observatory.burst.HealthPingConfig +} +var file_app_observatory_burst_config_proto_depIdxs = []int32{ + 1, // 0: xray.core.app.observatory.burst.Config.ping_config:type_name -> xray.core.app.observatory.burst.HealthPingConfig + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_app_observatory_burst_config_proto_init() } +func file_app_observatory_burst_config_proto_init() { + if File_app_observatory_burst_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_app_observatory_burst_config_proto_rawDesc), len(file_app_observatory_burst_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_app_observatory_burst_config_proto_goTypes, + DependencyIndexes: file_app_observatory_burst_config_proto_depIdxs, + MessageInfos: file_app_observatory_burst_config_proto_msgTypes, + }.Build() + File_app_observatory_burst_config_proto = out.File + file_app_observatory_burst_config_proto_goTypes = nil + file_app_observatory_burst_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/app/observatory/burst/config.proto b/subproject/Xray-core-main/app/observatory/burst/config.proto new file mode 100644 index 00000000..a60978d5 --- /dev/null +++ b/subproject/Xray-core-main/app/observatory/burst/config.proto @@ -0,0 +1,32 @@ +syntax = "proto3"; + +package xray.core.app.observatory.burst; +option csharp_namespace = "Xray.App.Observatory.Burst"; +option go_package = "github.com/xtls/xray-core/app/observatory/burst"; +option java_package = "com.xray.app.observatory.burst"; +option java_multiple_files = true; + +message Config { + /* @Document The selectors for outbound under observation + */ + repeated string subject_selector = 2; + + HealthPingConfig ping_config = 3; +} + +message HealthPingConfig { + // destination url, need 204 for success return + // default https://connectivitycheck.gstatic.com/generate_204 + string destination = 1; + // connectivity check url + string connectivity = 2; + // health check interval, int64 values of time.Duration + int64 interval = 3; + // sampling count is the amount of recent ping results which are kept for calculation + int32 samplingCount = 4; + // ping timeout, int64 values of time.Duration + int64 timeout = 5; + // http method to make request + string httpMethod = 6; + +} diff --git a/subproject/Xray-core-main/app/observatory/burst/healthping.go b/subproject/Xray-core-main/app/observatory/burst/healthping.go new file mode 100644 index 00000000..cb1c3402 --- /dev/null +++ b/subproject/Xray-core-main/app/observatory/burst/healthping.go @@ -0,0 +1,268 @@ +package burst + +import ( + "context" + "fmt" + "strings" + "sync" + "time" + + "github.com/xtls/xray-core/common/dice" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/features/routing" +) + +// HealthPingSettings holds settings for health Checker +type HealthPingSettings struct { + Destination string `json:"destination"` + Connectivity string `json:"connectivity"` + Interval time.Duration `json:"interval"` + SamplingCount int `json:"sampling"` + Timeout time.Duration `json:"timeout"` + HttpMethod string `json:"httpMethod"` +} + +// HealthPing is the health checker for balancers +type HealthPing struct { + ctx context.Context + dispatcher routing.Dispatcher + access sync.Mutex + ticker *time.Ticker + tickerClose chan struct{} + + Settings *HealthPingSettings + Results map[string]*HealthPingRTTS +} + +// NewHealthPing creates a new HealthPing with settings +func NewHealthPing(ctx context.Context, dispatcher routing.Dispatcher, config *HealthPingConfig) *HealthPing { + settings := &HealthPingSettings{} + if config != nil { + + var httpMethod string + if config.HttpMethod == "" { + httpMethod = "HEAD" + } else { + httpMethod = strings.TrimSpace(config.HttpMethod) + } + + settings = &HealthPingSettings{ + Connectivity: strings.TrimSpace(config.Connectivity), + Destination: strings.TrimSpace(config.Destination), + Interval: time.Duration(config.Interval), + SamplingCount: int(config.SamplingCount), + Timeout: time.Duration(config.Timeout), + HttpMethod: httpMethod, + } + } + if settings.Destination == "" { + // Destination URL, need 204 for success return default to chromium + // https://github.com/chromium/chromium/blob/main/components/safety_check/url_constants.cc#L10 + // https://chromium.googlesource.com/chromium/src/+/refs/heads/main/components/safety_check/url_constants.cc#10 + settings.Destination = "https://connectivitycheck.gstatic.com/generate_204" + } + if settings.Interval == 0 { + settings.Interval = time.Duration(1) * time.Minute + } else if settings.Interval < 10 { + errors.LogWarning(ctx, "health check interval is too small, 10s is applied") + settings.Interval = time.Duration(10) * time.Second + } + if settings.SamplingCount <= 0 { + settings.SamplingCount = 10 + } + if settings.Timeout <= 0 { + // results are saved after all health pings finish, + // a larger timeout could possibly makes checks run longer + settings.Timeout = time.Duration(5) * time.Second + } + return &HealthPing{ + ctx: ctx, + dispatcher: dispatcher, + Settings: settings, + Results: nil, + } +} + +// StartScheduler implements the HealthChecker +func (h *HealthPing) StartScheduler(selector func() ([]string, error)) { + if h.ticker != nil { + return + } + interval := h.Settings.Interval * time.Duration(h.Settings.SamplingCount) + ticker := time.NewTicker(interval) + tickerClose := make(chan struct{}) + h.ticker = ticker + h.tickerClose = tickerClose + go func() { + tags, err := selector() + if err != nil { + errors.LogWarning(h.ctx, "error select outbounds for initial health check: ", err) + return + } + h.Check(tags) + }() + + go func() { + for { + go func() { + tags, err := selector() + if err != nil { + errors.LogWarning(h.ctx, "error select outbounds for scheduled health check: ", err) + return + } + h.doCheck(tags, interval, h.Settings.SamplingCount) + h.Cleanup(tags) + }() + select { + case <-ticker.C: + continue + case <-tickerClose: + return + } + } + }() +} + +// StopScheduler implements the HealthChecker +func (h *HealthPing) StopScheduler() { + if h.ticker == nil { + return + } + h.ticker.Stop() + h.ticker = nil + close(h.tickerClose) + h.tickerClose = nil +} + +// Check implements the HealthChecker +func (h *HealthPing) Check(tags []string) error { + if len(tags) == 0 { + return nil + } + errors.LogInfo(h.ctx, "perform one-time health check for tags ", tags) + h.doCheck(tags, 0, 1) + return nil +} + +type rtt struct { + handler string + value time.Duration +} + +// doCheck performs the 'rounds' amount checks in given 'duration'. You should make +// sure all tags are valid for current balancer +func (h *HealthPing) doCheck(tags []string, duration time.Duration, rounds int) { + count := len(tags) * rounds + if count == 0 { + return + } + ch := make(chan *rtt, count) + + for _, tag := range tags { + handler := tag + client := newPingClient( + h.ctx, + h.dispatcher, + h.Settings.Destination, + h.Settings.Timeout, + handler, + ) + for i := 0; i < rounds; i++ { + delay := time.Duration(0) + if duration > 0 { + delay = time.Duration(dice.RollInt63n(int64(duration))) + } + time.AfterFunc(delay, func() { + errors.LogDebug(h.ctx, "checking ", handler) + delay, err := client.MeasureDelay(h.Settings.HttpMethod) + if err == nil { + ch <- &rtt{ + handler: handler, + value: delay, + } + return + } + if !h.checkConnectivity() { + errors.LogWarning(h.ctx, "network is down") + ch <- &rtt{ + handler: handler, + value: 0, + } + return + } + errors.LogWarning(h.ctx, fmt.Sprintf( + "error ping %s with %s: %s", + h.Settings.Destination, + handler, + err, + )) + ch <- &rtt{ + handler: handler, + value: rttFailed, + } + }) + } + } + for i := 0; i < count; i++ { + rtt := <-ch + if rtt.value > 0 { + // should not put results when network is down + h.PutResult(rtt.handler, rtt.value) + } + } +} + +// PutResult put a ping rtt to results +func (h *HealthPing) PutResult(tag string, rtt time.Duration) { + h.access.Lock() + defer h.access.Unlock() + if h.Results == nil { + h.Results = make(map[string]*HealthPingRTTS) + } + r, ok := h.Results[tag] + if !ok { + // validity is 2 times to sampling period, since the check are + // distributed in the time line randomly, in extreme cases, + // Previous checks are distributed on the left, and later ones + // on the right + validity := h.Settings.Interval * time.Duration(h.Settings.SamplingCount) * 2 + r = NewHealthPingResult(h.Settings.SamplingCount, validity) + h.Results[tag] = r + } + r.Put(rtt) +} + +// Cleanup removes results of removed handlers, +// tags should be all valid tags of the Balancer now +func (h *HealthPing) Cleanup(tags []string) { + h.access.Lock() + defer h.access.Unlock() + for tag := range h.Results { + found := false + for _, v := range tags { + if tag == v { + found = true + break + } + } + if !found { + delete(h.Results, tag) + } + } +} + +// checkConnectivity checks the network connectivity, it returns +// true if network is good or "connectivity check url" not set +func (h *HealthPing) checkConnectivity() bool { + if h.Settings.Connectivity == "" { + return true + } + tester := newDirectPingClient( + h.Settings.Connectivity, + h.Settings.Timeout, + ) + if _, err := tester.MeasureDelay(h.Settings.HttpMethod); err != nil { + return false + } + return true +} diff --git a/subproject/Xray-core-main/app/observatory/burst/healthping_result.go b/subproject/Xray-core-main/app/observatory/burst/healthping_result.go new file mode 100644 index 00000000..f48d37b6 --- /dev/null +++ b/subproject/Xray-core-main/app/observatory/burst/healthping_result.go @@ -0,0 +1,143 @@ +package burst + +import ( + "math" + "time" +) + +// HealthPingStats is the statistics of HealthPingRTTS +type HealthPingStats struct { + All int + Fail int + Deviation time.Duration + Average time.Duration + Max time.Duration + Min time.Duration +} + +// HealthPingRTTS holds ping rtts for health Checker +type HealthPingRTTS struct { + idx int + cap int + validity time.Duration + rtts []*pingRTT + + lastUpdateAt time.Time + stats *HealthPingStats +} + +type pingRTT struct { + time time.Time + value time.Duration +} + +// NewHealthPingResult returns a *HealthPingResult with specified capacity +func NewHealthPingResult(cap int, validity time.Duration) *HealthPingRTTS { + return &HealthPingRTTS{cap: cap, validity: validity} +} + +// Get gets statistics of the HealthPingRTTS +func (h *HealthPingRTTS) Get() *HealthPingStats { + return h.getStatistics() +} + +// GetWithCache get statistics and write cache for next call +// Make sure use Mutex.Lock() before calling it, RWMutex.RLock() +// is not an option since it writes cache +func (h *HealthPingRTTS) GetWithCache() *HealthPingStats { + lastPutAt := h.rtts[h.idx].time + now := time.Now() + if h.stats == nil || h.lastUpdateAt.Before(lastPutAt) || h.findOutdated(now) >= 0 { + h.stats = h.getStatistics() + h.lastUpdateAt = now + } + return h.stats +} + +// Put puts a new rtt to the HealthPingResult +func (h *HealthPingRTTS) Put(d time.Duration) { + if h.rtts == nil { + h.rtts = make([]*pingRTT, h.cap) + for i := 0; i < h.cap; i++ { + h.rtts[i] = &pingRTT{} + } + h.idx = -1 + } + h.idx = h.calcIndex(1) + now := time.Now() + h.rtts[h.idx].time = now + h.rtts[h.idx].value = d +} + +func (h *HealthPingRTTS) calcIndex(step int) int { + idx := h.idx + idx += step + if idx >= h.cap { + idx %= h.cap + } + return idx +} + +func (h *HealthPingRTTS) getStatistics() *HealthPingStats { + stats := &HealthPingStats{} + stats.Fail = 0 + stats.Max = 0 + stats.Min = rttFailed + sum := time.Duration(0) + cnt := 0 + validRTTs := make([]time.Duration, 0) + for _, rtt := range h.rtts { + switch { + case rtt.value == 0 || time.Since(rtt.time) > h.validity: + continue + case rtt.value == rttFailed: + stats.Fail++ + continue + } + cnt++ + sum += rtt.value + validRTTs = append(validRTTs, rtt.value) + if stats.Max < rtt.value { + stats.Max = rtt.value + } + if stats.Min > rtt.value { + stats.Min = rtt.value + } + } + stats.All = cnt + stats.Fail + if cnt == 0 { + stats.Min = 0 + return stats + } + stats.Average = time.Duration(int(sum) / cnt) + var std float64 + if cnt < 2 { + // no enough data for standard deviation, we assume it's half of the average rtt + // if we don't do this, standard deviation of 1 round tested nodes is 0, will always + // selected before 2 or more rounds tested nodes + std = float64(stats.Average / 2) + } else { + variance := float64(0) + for _, rtt := range validRTTs { + variance += math.Pow(float64(rtt-stats.Average), 2) + } + std = math.Sqrt(variance / float64(cnt)) + } + stats.Deviation = time.Duration(std) + return stats +} + +func (h *HealthPingRTTS) findOutdated(now time.Time) int { + for i := h.cap - 1; i < 2*h.cap; i++ { + // from oldest to latest + idx := h.calcIndex(i) + validity := h.rtts[idx].time.Add(h.validity) + if h.lastUpdateAt.After(validity) { + return idx + } + if validity.Before(now) { + return idx + } + } + return -1 +} diff --git a/subproject/Xray-core-main/app/observatory/burst/healthping_result_test.go b/subproject/Xray-core-main/app/observatory/burst/healthping_result_test.go new file mode 100644 index 00000000..a93e22dd --- /dev/null +++ b/subproject/Xray-core-main/app/observatory/burst/healthping_result_test.go @@ -0,0 +1,106 @@ +package burst_test + +import ( + "math" + reflect "reflect" + "testing" + "time" + + "github.com/xtls/xray-core/app/observatory/burst" +) + +func TestHealthPingResults(t *testing.T) { + rtts := []int64{60, 140, 60, 140, 60, 60, 140, 60, 140} + hr := burst.NewHealthPingResult(4, time.Hour) + for _, rtt := range rtts { + hr.Put(time.Duration(rtt)) + } + rttFailed := time.Duration(math.MaxInt64) + expected := &burst.HealthPingStats{ + All: 4, + Fail: 0, + Deviation: 40, + Average: 100, + Max: 140, + Min: 60, + } + actual := hr.Get() + if !reflect.DeepEqual(expected, actual) { + t.Errorf("expected: %v, actual: %v", expected, actual) + } + hr.Put(rttFailed) + hr.Put(rttFailed) + expected.Fail = 2 + actual = hr.Get() + if !reflect.DeepEqual(expected, actual) { + t.Errorf("failed half-failures test, expected: %v, actual: %v", expected, actual) + } + hr.Put(rttFailed) + hr.Put(rttFailed) + expected = &burst.HealthPingStats{ + All: 4, + Fail: 4, + Deviation: 0, + Average: 0, + Max: 0, + Min: 0, + } + actual = hr.Get() + if !reflect.DeepEqual(expected, actual) { + t.Errorf("failed all-failures test, expected: %v, actual: %v", expected, actual) + } +} + +func TestHealthPingResultsIgnoreOutdated(t *testing.T) { + rtts := []int64{60, 140, 60, 140} + hr := burst.NewHealthPingResult(4, time.Duration(10)*time.Millisecond) + for i, rtt := range rtts { + if i == 2 { + // wait for previous 2 outdated + time.Sleep(time.Duration(10) * time.Millisecond) + } + hr.Put(time.Duration(rtt)) + } + hr.Get() + expected := &burst.HealthPingStats{ + All: 2, + Fail: 0, + Deviation: 40, + Average: 100, + Max: 140, + Min: 60, + } + actual := hr.Get() + if !reflect.DeepEqual(expected, actual) { + t.Errorf("failed 'half-outdated' test, expected: %v, actual: %v", expected, actual) + } + // wait for all outdated + time.Sleep(time.Duration(10) * time.Millisecond) + expected = &burst.HealthPingStats{ + All: 0, + Fail: 0, + Deviation: 0, + Average: 0, + Max: 0, + Min: 0, + } + actual = hr.Get() + if !reflect.DeepEqual(expected, actual) { + t.Errorf("failed 'outdated / not-tested' test, expected: %v, actual: %v", expected, actual) + } + + hr.Put(time.Duration(60)) + expected = &burst.HealthPingStats{ + All: 1, + Fail: 0, + // 1 sample, std=0.5rtt + Deviation: 30, + Average: 60, + Max: 60, + Min: 60, + } + actual = hr.Get() + if !reflect.DeepEqual(expected, actual) { + t.Errorf("expected: %v, actual: %v", expected, actual) + } +} diff --git a/subproject/Xray-core-main/app/observatory/burst/ping.go b/subproject/Xray-core-main/app/observatory/burst/ping.go new file mode 100644 index 00000000..2755ec54 --- /dev/null +++ b/subproject/Xray-core-main/app/observatory/burst/ping.go @@ -0,0 +1,81 @@ +package burst + +import ( + "context" + "io" + "net/http" + "time" + + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/utils" + "github.com/xtls/xray-core/features/routing" + "github.com/xtls/xray-core/transport/internet/tagged" +) + +type pingClient struct { + destination string + httpClient *http.Client +} + +func newPingClient(ctx context.Context, dispatcher routing.Dispatcher, destination string, timeout time.Duration, handler string) *pingClient { + return &pingClient{ + destination: destination, + httpClient: newHTTPClient(ctx, dispatcher, handler, timeout), + } +} + +func newDirectPingClient(destination string, timeout time.Duration) *pingClient { + return &pingClient{ + destination: destination, + httpClient: &http.Client{Timeout: timeout}, + } +} + +func newHTTPClient(ctxv context.Context, dispatcher routing.Dispatcher, handler string, timeout time.Duration) *http.Client { + tr := &http.Transport{ + DisableKeepAlives: true, + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + dest, err := net.ParseDestination(network + ":" + addr) + if err != nil { + return nil, err + } + return tagged.Dialer(ctxv, dispatcher, dest, handler) + }, + } + return &http.Client{ + Transport: tr, + Timeout: timeout, + // don't follow redirect + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + } +} + +// MeasureDelay returns the delay time of the request to dest +func (s *pingClient) MeasureDelay(httpMethod string) (time.Duration, error) { + if s.httpClient == nil { + panic("pingClient not initialized") + } + + req, err := http.NewRequest(httpMethod, s.destination, nil) + if err != nil { + return rttFailed, err + } + utils.TryDefaultHeadersWith(req.Header, "nav") + + start := time.Now() + resp, err := s.httpClient.Do(req) + if err != nil { + return rttFailed, err + } + if httpMethod == http.MethodGet { + _, err = io.Copy(io.Discard, resp.Body) + if err != nil { + return rttFailed, err + } + } + resp.Body.Close() + + return time.Since(start), nil +} diff --git a/subproject/Xray-core-main/app/observatory/command/command.go b/subproject/Xray-core-main/app/observatory/command/command.go new file mode 100644 index 00000000..f9bb58e3 --- /dev/null +++ b/subproject/Xray-core-main/app/observatory/command/command.go @@ -0,0 +1,47 @@ +package command + +import ( + "context" + + "github.com/xtls/xray-core/app/observatory" + "github.com/xtls/xray-core/common" + core "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/features/extension" + "google.golang.org/grpc" +) + +type service struct { + UnimplementedObservatoryServiceServer + v *core.Instance + + observatory extension.Observatory +} + +func (s *service) GetOutboundStatus(ctx context.Context, request *GetOutboundStatusRequest) (*GetOutboundStatusResponse, error) { + resp, err := s.observatory.GetObservation(ctx) + if err != nil { + return nil, err + } + retdata := resp.(*observatory.ObservationResult) + return &GetOutboundStatusResponse{ + Status: retdata, + }, nil +} + +func (s *service) Register(server *grpc.Server) { + RegisterObservatoryServiceServer(server, s) +} + +func init() { + common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, cfg interface{}) (interface{}, error) { + s := core.MustFromContext(ctx) + sv := &service{v: s} + err := s.RequireFeatures(func(Observatory extension.Observatory) { + sv.observatory = Observatory + }, false) + if err != nil { + return nil, err + } + return sv, nil + })) +} diff --git a/subproject/Xray-core-main/app/observatory/command/command.pb.go b/subproject/Xray-core-main/app/observatory/command/command.pb.go new file mode 100644 index 00000000..1be1fc8b --- /dev/null +++ b/subproject/Xray-core-main/app/observatory/command/command.pb.go @@ -0,0 +1,206 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: app/observatory/command/command.proto + +package command + +import ( + observatory "github.com/xtls/xray-core/app/observatory" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type GetOutboundStatusRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetOutboundStatusRequest) Reset() { + *x = GetOutboundStatusRequest{} + mi := &file_app_observatory_command_command_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetOutboundStatusRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetOutboundStatusRequest) ProtoMessage() {} + +func (x *GetOutboundStatusRequest) ProtoReflect() protoreflect.Message { + mi := &file_app_observatory_command_command_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetOutboundStatusRequest.ProtoReflect.Descriptor instead. +func (*GetOutboundStatusRequest) Descriptor() ([]byte, []int) { + return file_app_observatory_command_command_proto_rawDescGZIP(), []int{0} +} + +type GetOutboundStatusResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Status *observatory.ObservationResult `protobuf:"bytes,1,opt,name=status,proto3" json:"status,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetOutboundStatusResponse) Reset() { + *x = GetOutboundStatusResponse{} + mi := &file_app_observatory_command_command_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetOutboundStatusResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetOutboundStatusResponse) ProtoMessage() {} + +func (x *GetOutboundStatusResponse) ProtoReflect() protoreflect.Message { + mi := &file_app_observatory_command_command_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetOutboundStatusResponse.ProtoReflect.Descriptor instead. +func (*GetOutboundStatusResponse) Descriptor() ([]byte, []int) { + return file_app_observatory_command_command_proto_rawDescGZIP(), []int{1} +} + +func (x *GetOutboundStatusResponse) GetStatus() *observatory.ObservationResult { + if x != nil { + return x.Status + } + return nil +} + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_app_observatory_command_command_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_app_observatory_command_command_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_app_observatory_command_command_proto_rawDescGZIP(), []int{2} +} + +var File_app_observatory_command_command_proto protoreflect.FileDescriptor + +const file_app_observatory_command_command_proto_rawDesc = "" + + "\n" + + "%app/observatory/command/command.proto\x12!xray.core.app.observatory.command\x1a\x1capp/observatory/config.proto\"\x1a\n" + + "\x18GetOutboundStatusRequest\"a\n" + + "\x19GetOutboundStatusResponse\x12D\n" + + "\x06status\x18\x01 \x01(\v2,.xray.core.app.observatory.ObservationResultR\x06status\"\b\n" + + "\x06Config2\xa7\x01\n" + + "\x12ObservatoryService\x12\x90\x01\n" + + "\x11GetOutboundStatus\x12;.xray.core.app.observatory.command.GetOutboundStatusRequest\x1a<.xray.core.app.observatory.command.GetOutboundStatusResponse\"\x00B\x80\x01\n" + + "%com.xray.core.app.observatory.commandP\x01Z1github.com/xtls/xray-core/app/observatory/command\xaa\x02!Xray.Core.App.Observatory.Commandb\x06proto3" + +var ( + file_app_observatory_command_command_proto_rawDescOnce sync.Once + file_app_observatory_command_command_proto_rawDescData []byte +) + +func file_app_observatory_command_command_proto_rawDescGZIP() []byte { + file_app_observatory_command_command_proto_rawDescOnce.Do(func() { + file_app_observatory_command_command_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_app_observatory_command_command_proto_rawDesc), len(file_app_observatory_command_command_proto_rawDesc))) + }) + return file_app_observatory_command_command_proto_rawDescData +} + +var file_app_observatory_command_command_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_app_observatory_command_command_proto_goTypes = []any{ + (*GetOutboundStatusRequest)(nil), // 0: xray.core.app.observatory.command.GetOutboundStatusRequest + (*GetOutboundStatusResponse)(nil), // 1: xray.core.app.observatory.command.GetOutboundStatusResponse + (*Config)(nil), // 2: xray.core.app.observatory.command.Config + (*observatory.ObservationResult)(nil), // 3: xray.core.app.observatory.ObservationResult +} +var file_app_observatory_command_command_proto_depIdxs = []int32{ + 3, // 0: xray.core.app.observatory.command.GetOutboundStatusResponse.status:type_name -> xray.core.app.observatory.ObservationResult + 0, // 1: xray.core.app.observatory.command.ObservatoryService.GetOutboundStatus:input_type -> xray.core.app.observatory.command.GetOutboundStatusRequest + 1, // 2: xray.core.app.observatory.command.ObservatoryService.GetOutboundStatus:output_type -> xray.core.app.observatory.command.GetOutboundStatusResponse + 2, // [2:3] is the sub-list for method output_type + 1, // [1:2] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_app_observatory_command_command_proto_init() } +func file_app_observatory_command_command_proto_init() { + if File_app_observatory_command_command_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_app_observatory_command_command_proto_rawDesc), len(file_app_observatory_command_command_proto_rawDesc)), + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_app_observatory_command_command_proto_goTypes, + DependencyIndexes: file_app_observatory_command_command_proto_depIdxs, + MessageInfos: file_app_observatory_command_command_proto_msgTypes, + }.Build() + File_app_observatory_command_command_proto = out.File + file_app_observatory_command_command_proto_goTypes = nil + file_app_observatory_command_command_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/app/observatory/command/command.proto b/subproject/Xray-core-main/app/observatory/command/command.proto new file mode 100644 index 00000000..4948809a --- /dev/null +++ b/subproject/Xray-core-main/app/observatory/command/command.proto @@ -0,0 +1,24 @@ +syntax = "proto3"; + +package xray.core.app.observatory.command; +option csharp_namespace = "Xray.Core.App.Observatory.Command"; +option go_package = "github.com/xtls/xray-core/app/observatory/command"; +option java_package = "com.xray.core.app.observatory.command"; +option java_multiple_files = true; + +import "app/observatory/config.proto"; + +message GetOutboundStatusRequest { +} + +message GetOutboundStatusResponse { + xray.core.app.observatory.ObservationResult status = 1; +} + +service ObservatoryService { + rpc GetOutboundStatus(GetOutboundStatusRequest) + returns (GetOutboundStatusResponse) {} +} + + +message Config {} \ No newline at end of file diff --git a/subproject/Xray-core-main/app/observatory/command/command_grpc.pb.go b/subproject/Xray-core-main/app/observatory/command/command_grpc.pb.go new file mode 100644 index 00000000..a3d42003 --- /dev/null +++ b/subproject/Xray-core-main/app/observatory/command/command_grpc.pb.go @@ -0,0 +1,121 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.0 +// - protoc v6.33.5 +// source: app/observatory/command/command.proto + +package command + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + ObservatoryService_GetOutboundStatus_FullMethodName = "/xray.core.app.observatory.command.ObservatoryService/GetOutboundStatus" +) + +// ObservatoryServiceClient is the client API for ObservatoryService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type ObservatoryServiceClient interface { + GetOutboundStatus(ctx context.Context, in *GetOutboundStatusRequest, opts ...grpc.CallOption) (*GetOutboundStatusResponse, error) +} + +type observatoryServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewObservatoryServiceClient(cc grpc.ClientConnInterface) ObservatoryServiceClient { + return &observatoryServiceClient{cc} +} + +func (c *observatoryServiceClient) GetOutboundStatus(ctx context.Context, in *GetOutboundStatusRequest, opts ...grpc.CallOption) (*GetOutboundStatusResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetOutboundStatusResponse) + err := c.cc.Invoke(ctx, ObservatoryService_GetOutboundStatus_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// ObservatoryServiceServer is the server API for ObservatoryService service. +// All implementations must embed UnimplementedObservatoryServiceServer +// for forward compatibility. +type ObservatoryServiceServer interface { + GetOutboundStatus(context.Context, *GetOutboundStatusRequest) (*GetOutboundStatusResponse, error) + mustEmbedUnimplementedObservatoryServiceServer() +} + +// UnimplementedObservatoryServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedObservatoryServiceServer struct{} + +func (UnimplementedObservatoryServiceServer) GetOutboundStatus(context.Context, *GetOutboundStatusRequest) (*GetOutboundStatusResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetOutboundStatus not implemented") +} +func (UnimplementedObservatoryServiceServer) mustEmbedUnimplementedObservatoryServiceServer() {} +func (UnimplementedObservatoryServiceServer) testEmbeddedByValue() {} + +// UnsafeObservatoryServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to ObservatoryServiceServer will +// result in compilation errors. +type UnsafeObservatoryServiceServer interface { + mustEmbedUnimplementedObservatoryServiceServer() +} + +func RegisterObservatoryServiceServer(s grpc.ServiceRegistrar, srv ObservatoryServiceServer) { + // If the following call panics, it indicates UnimplementedObservatoryServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&ObservatoryService_ServiceDesc, srv) +} + +func _ObservatoryService_GetOutboundStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetOutboundStatusRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ObservatoryServiceServer).GetOutboundStatus(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ObservatoryService_GetOutboundStatus_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ObservatoryServiceServer).GetOutboundStatus(ctx, req.(*GetOutboundStatusRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// ObservatoryService_ServiceDesc is the grpc.ServiceDesc for ObservatoryService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var ObservatoryService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "xray.core.app.observatory.command.ObservatoryService", + HandlerType: (*ObservatoryServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "GetOutboundStatus", + Handler: _ObservatoryService_GetOutboundStatus_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "app/observatory/command/command.proto", +} diff --git a/subproject/Xray-core-main/app/observatory/config.pb.go b/subproject/Xray-core-main/app/observatory/config.pb.go new file mode 100644 index 00000000..f87440c8 --- /dev/null +++ b/subproject/Xray-core-main/app/observatory/config.pb.go @@ -0,0 +1,528 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: app/observatory/config.proto + +package observatory + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type ObservationResult struct { + state protoimpl.MessageState `protogen:"open.v1"` + Status []*OutboundStatus `protobuf:"bytes,1,rep,name=status,proto3" json:"status,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ObservationResult) Reset() { + *x = ObservationResult{} + mi := &file_app_observatory_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ObservationResult) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ObservationResult) ProtoMessage() {} + +func (x *ObservationResult) ProtoReflect() protoreflect.Message { + mi := &file_app_observatory_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ObservationResult.ProtoReflect.Descriptor instead. +func (*ObservationResult) Descriptor() ([]byte, []int) { + return file_app_observatory_config_proto_rawDescGZIP(), []int{0} +} + +func (x *ObservationResult) GetStatus() []*OutboundStatus { + if x != nil { + return x.Status + } + return nil +} + +type HealthPingMeasurementResult struct { + state protoimpl.MessageState `protogen:"open.v1"` + All int64 `protobuf:"varint,1,opt,name=all,proto3" json:"all,omitempty"` + Fail int64 `protobuf:"varint,2,opt,name=fail,proto3" json:"fail,omitempty"` + Deviation int64 `protobuf:"varint,3,opt,name=deviation,proto3" json:"deviation,omitempty"` + Average int64 `protobuf:"varint,4,opt,name=average,proto3" json:"average,omitempty"` + Max int64 `protobuf:"varint,5,opt,name=max,proto3" json:"max,omitempty"` + Min int64 `protobuf:"varint,6,opt,name=min,proto3" json:"min,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HealthPingMeasurementResult) Reset() { + *x = HealthPingMeasurementResult{} + mi := &file_app_observatory_config_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HealthPingMeasurementResult) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HealthPingMeasurementResult) ProtoMessage() {} + +func (x *HealthPingMeasurementResult) ProtoReflect() protoreflect.Message { + mi := &file_app_observatory_config_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HealthPingMeasurementResult.ProtoReflect.Descriptor instead. +func (*HealthPingMeasurementResult) Descriptor() ([]byte, []int) { + return file_app_observatory_config_proto_rawDescGZIP(), []int{1} +} + +func (x *HealthPingMeasurementResult) GetAll() int64 { + if x != nil { + return x.All + } + return 0 +} + +func (x *HealthPingMeasurementResult) GetFail() int64 { + if x != nil { + return x.Fail + } + return 0 +} + +func (x *HealthPingMeasurementResult) GetDeviation() int64 { + if x != nil { + return x.Deviation + } + return 0 +} + +func (x *HealthPingMeasurementResult) GetAverage() int64 { + if x != nil { + return x.Average + } + return 0 +} + +func (x *HealthPingMeasurementResult) GetMax() int64 { + if x != nil { + return x.Max + } + return 0 +} + +func (x *HealthPingMeasurementResult) GetMin() int64 { + if x != nil { + return x.Min + } + return 0 +} + +type OutboundStatus struct { + state protoimpl.MessageState `protogen:"open.v1"` + // @Document Whether this outbound is usable + // @Restriction ReadOnlyForUser + Alive bool `protobuf:"varint,1,opt,name=alive,proto3" json:"alive,omitempty"` + // @Document The time for probe request to finish. + // @Type time.ms + // @Restriction ReadOnlyForUser + Delay int64 `protobuf:"varint,2,opt,name=delay,proto3" json:"delay,omitempty"` + // @Document The last error caused this outbound failed to relay probe request + // @Restriction NotMachineReadable + LastErrorReason string `protobuf:"bytes,3,opt,name=last_error_reason,json=lastErrorReason,proto3" json:"last_error_reason,omitempty"` + // @Document The outbound tag for this Server + // @Type id.outboundTag + OutboundTag string `protobuf:"bytes,4,opt,name=outbound_tag,json=outboundTag,proto3" json:"outbound_tag,omitempty"` + // @Document The time this outbound is known to be alive + // @Type id.outboundTag + LastSeenTime int64 `protobuf:"varint,5,opt,name=last_seen_time,json=lastSeenTime,proto3" json:"last_seen_time,omitempty"` + // @Document The time this outbound is tried + // @Type id.outboundTag + LastTryTime int64 `protobuf:"varint,6,opt,name=last_try_time,json=lastTryTime,proto3" json:"last_try_time,omitempty"` + HealthPing *HealthPingMeasurementResult `protobuf:"bytes,7,opt,name=health_ping,json=healthPing,proto3" json:"health_ping,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OutboundStatus) Reset() { + *x = OutboundStatus{} + mi := &file_app_observatory_config_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OutboundStatus) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OutboundStatus) ProtoMessage() {} + +func (x *OutboundStatus) ProtoReflect() protoreflect.Message { + mi := &file_app_observatory_config_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OutboundStatus.ProtoReflect.Descriptor instead. +func (*OutboundStatus) Descriptor() ([]byte, []int) { + return file_app_observatory_config_proto_rawDescGZIP(), []int{2} +} + +func (x *OutboundStatus) GetAlive() bool { + if x != nil { + return x.Alive + } + return false +} + +func (x *OutboundStatus) GetDelay() int64 { + if x != nil { + return x.Delay + } + return 0 +} + +func (x *OutboundStatus) GetLastErrorReason() string { + if x != nil { + return x.LastErrorReason + } + return "" +} + +func (x *OutboundStatus) GetOutboundTag() string { + if x != nil { + return x.OutboundTag + } + return "" +} + +func (x *OutboundStatus) GetLastSeenTime() int64 { + if x != nil { + return x.LastSeenTime + } + return 0 +} + +func (x *OutboundStatus) GetLastTryTime() int64 { + if x != nil { + return x.LastTryTime + } + return 0 +} + +func (x *OutboundStatus) GetHealthPing() *HealthPingMeasurementResult { + if x != nil { + return x.HealthPing + } + return nil +} + +type ProbeResult struct { + state protoimpl.MessageState `protogen:"open.v1"` + // @Document Whether this outbound is usable + // @Restriction ReadOnlyForUser + Alive bool `protobuf:"varint,1,opt,name=alive,proto3" json:"alive,omitempty"` + // @Document The time for probe request to finish. + // @Type time.ms + // @Restriction ReadOnlyForUser + Delay int64 `protobuf:"varint,2,opt,name=delay,proto3" json:"delay,omitempty"` + // @Document The error caused this outbound failed to relay probe request + // @Restriction NotMachineReadable + LastErrorReason string `protobuf:"bytes,3,opt,name=last_error_reason,json=lastErrorReason,proto3" json:"last_error_reason,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProbeResult) Reset() { + *x = ProbeResult{} + mi := &file_app_observatory_config_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProbeResult) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProbeResult) ProtoMessage() {} + +func (x *ProbeResult) ProtoReflect() protoreflect.Message { + mi := &file_app_observatory_config_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProbeResult.ProtoReflect.Descriptor instead. +func (*ProbeResult) Descriptor() ([]byte, []int) { + return file_app_observatory_config_proto_rawDescGZIP(), []int{3} +} + +func (x *ProbeResult) GetAlive() bool { + if x != nil { + return x.Alive + } + return false +} + +func (x *ProbeResult) GetDelay() int64 { + if x != nil { + return x.Delay + } + return 0 +} + +func (x *ProbeResult) GetLastErrorReason() string { + if x != nil { + return x.LastErrorReason + } + return "" +} + +type Intensity struct { + state protoimpl.MessageState `protogen:"open.v1"` + // @Document The time interval for a probe request in ms. + // @Type time.ms + ProbeInterval uint32 `protobuf:"varint,1,opt,name=probe_interval,json=probeInterval,proto3" json:"probe_interval,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Intensity) Reset() { + *x = Intensity{} + mi := &file_app_observatory_config_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Intensity) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Intensity) ProtoMessage() {} + +func (x *Intensity) ProtoReflect() protoreflect.Message { + mi := &file_app_observatory_config_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Intensity.ProtoReflect.Descriptor instead. +func (*Intensity) Descriptor() ([]byte, []int) { + return file_app_observatory_config_proto_rawDescGZIP(), []int{4} +} + +func (x *Intensity) GetProbeInterval() uint32 { + if x != nil { + return x.ProbeInterval + } + return 0 +} + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + // @Document The selectors for outbound under observation + SubjectSelector []string `protobuf:"bytes,2,rep,name=subject_selector,json=subjectSelector,proto3" json:"subject_selector,omitempty"` + ProbeUrl string `protobuf:"bytes,3,opt,name=probe_url,json=probeUrl,proto3" json:"probe_url,omitempty"` + ProbeInterval int64 `protobuf:"varint,4,opt,name=probe_interval,json=probeInterval,proto3" json:"probe_interval,omitempty"` + EnableConcurrency bool `protobuf:"varint,5,opt,name=enable_concurrency,json=enableConcurrency,proto3" json:"enable_concurrency,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_app_observatory_config_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_app_observatory_config_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_app_observatory_config_proto_rawDescGZIP(), []int{5} +} + +func (x *Config) GetSubjectSelector() []string { + if x != nil { + return x.SubjectSelector + } + return nil +} + +func (x *Config) GetProbeUrl() string { + if x != nil { + return x.ProbeUrl + } + return "" +} + +func (x *Config) GetProbeInterval() int64 { + if x != nil { + return x.ProbeInterval + } + return 0 +} + +func (x *Config) GetEnableConcurrency() bool { + if x != nil { + return x.EnableConcurrency + } + return false +} + +var File_app_observatory_config_proto protoreflect.FileDescriptor + +const file_app_observatory_config_proto_rawDesc = "" + + "\n" + + "\x1capp/observatory/config.proto\x12\x19xray.core.app.observatory\"V\n" + + "\x11ObservationResult\x12A\n" + + "\x06status\x18\x01 \x03(\v2).xray.core.app.observatory.OutboundStatusR\x06status\"\x9f\x01\n" + + "\x1bHealthPingMeasurementResult\x12\x10\n" + + "\x03all\x18\x01 \x01(\x03R\x03all\x12\x12\n" + + "\x04fail\x18\x02 \x01(\x03R\x04fail\x12\x1c\n" + + "\tdeviation\x18\x03 \x01(\x03R\tdeviation\x12\x18\n" + + "\aaverage\x18\x04 \x01(\x03R\aaverage\x12\x10\n" + + "\x03max\x18\x05 \x01(\x03R\x03max\x12\x10\n" + + "\x03min\x18\x06 \x01(\x03R\x03min\"\xae\x02\n" + + "\x0eOutboundStatus\x12\x14\n" + + "\x05alive\x18\x01 \x01(\bR\x05alive\x12\x14\n" + + "\x05delay\x18\x02 \x01(\x03R\x05delay\x12*\n" + + "\x11last_error_reason\x18\x03 \x01(\tR\x0flastErrorReason\x12!\n" + + "\foutbound_tag\x18\x04 \x01(\tR\voutboundTag\x12$\n" + + "\x0elast_seen_time\x18\x05 \x01(\x03R\flastSeenTime\x12\"\n" + + "\rlast_try_time\x18\x06 \x01(\x03R\vlastTryTime\x12W\n" + + "\vhealth_ping\x18\a \x01(\v26.xray.core.app.observatory.HealthPingMeasurementResultR\n" + + "healthPing\"e\n" + + "\vProbeResult\x12\x14\n" + + "\x05alive\x18\x01 \x01(\bR\x05alive\x12\x14\n" + + "\x05delay\x18\x02 \x01(\x03R\x05delay\x12*\n" + + "\x11last_error_reason\x18\x03 \x01(\tR\x0flastErrorReason\"2\n" + + "\tIntensity\x12%\n" + + "\x0eprobe_interval\x18\x01 \x01(\rR\rprobeInterval\"\xa6\x01\n" + + "\x06Config\x12)\n" + + "\x10subject_selector\x18\x02 \x03(\tR\x0fsubjectSelector\x12\x1b\n" + + "\tprobe_url\x18\x03 \x01(\tR\bprobeUrl\x12%\n" + + "\x0eprobe_interval\x18\x04 \x01(\x03R\rprobeInterval\x12-\n" + + "\x12enable_concurrency\x18\x05 \x01(\bR\x11enableConcurrencyB^\n" + + "\x18com.xray.app.observatoryP\x01Z)github.com/xtls/xray-core/app/observatory\xaa\x02\x14Xray.App.Observatoryb\x06proto3" + +var ( + file_app_observatory_config_proto_rawDescOnce sync.Once + file_app_observatory_config_proto_rawDescData []byte +) + +func file_app_observatory_config_proto_rawDescGZIP() []byte { + file_app_observatory_config_proto_rawDescOnce.Do(func() { + file_app_observatory_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_app_observatory_config_proto_rawDesc), len(file_app_observatory_config_proto_rawDesc))) + }) + return file_app_observatory_config_proto_rawDescData +} + +var file_app_observatory_config_proto_msgTypes = make([]protoimpl.MessageInfo, 6) +var file_app_observatory_config_proto_goTypes = []any{ + (*ObservationResult)(nil), // 0: xray.core.app.observatory.ObservationResult + (*HealthPingMeasurementResult)(nil), // 1: xray.core.app.observatory.HealthPingMeasurementResult + (*OutboundStatus)(nil), // 2: xray.core.app.observatory.OutboundStatus + (*ProbeResult)(nil), // 3: xray.core.app.observatory.ProbeResult + (*Intensity)(nil), // 4: xray.core.app.observatory.Intensity + (*Config)(nil), // 5: xray.core.app.observatory.Config +} +var file_app_observatory_config_proto_depIdxs = []int32{ + 2, // 0: xray.core.app.observatory.ObservationResult.status:type_name -> xray.core.app.observatory.OutboundStatus + 1, // 1: xray.core.app.observatory.OutboundStatus.health_ping:type_name -> xray.core.app.observatory.HealthPingMeasurementResult + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_app_observatory_config_proto_init() } +func file_app_observatory_config_proto_init() { + if File_app_observatory_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_app_observatory_config_proto_rawDesc), len(file_app_observatory_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 6, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_app_observatory_config_proto_goTypes, + DependencyIndexes: file_app_observatory_config_proto_depIdxs, + MessageInfos: file_app_observatory_config_proto_msgTypes, + }.Build() + File_app_observatory_config_proto = out.File + file_app_observatory_config_proto_goTypes = nil + file_app_observatory_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/app/observatory/config.proto b/subproject/Xray-core-main/app/observatory/config.proto new file mode 100644 index 00000000..fbfabad6 --- /dev/null +++ b/subproject/Xray-core-main/app/observatory/config.proto @@ -0,0 +1,84 @@ +syntax = "proto3"; + +package xray.core.app.observatory; +option csharp_namespace = "Xray.App.Observatory"; +option go_package = "github.com/xtls/xray-core/app/observatory"; +option java_package = "com.xray.app.observatory"; +option java_multiple_files = true; + +message ObservationResult { + repeated OutboundStatus status = 1; +} + +message HealthPingMeasurementResult { + int64 all = 1; + int64 fail = 2; + int64 deviation = 3; + int64 average = 4; + int64 max = 5; + int64 min = 6; +} + +message OutboundStatus{ + /* @Document Whether this outbound is usable + @Restriction ReadOnlyForUser + */ + bool alive = 1; + /* @Document The time for probe request to finish. + @Type time.ms + @Restriction ReadOnlyForUser + */ + int64 delay = 2; + /* @Document The last error caused this outbound failed to relay probe request + @Restriction NotMachineReadable + */ + string last_error_reason = 3; + /* @Document The outbound tag for this Server + @Type id.outboundTag + */ + string outbound_tag = 4; + /* @Document The time this outbound is known to be alive + @Type id.outboundTag +*/ + int64 last_seen_time = 5; + /* @Document The time this outbound is tried + @Type id.outboundTag +*/ + int64 last_try_time = 6; + + HealthPingMeasurementResult health_ping = 7; +} + +message ProbeResult{ + /* @Document Whether this outbound is usable + @Restriction ReadOnlyForUser + */ + bool alive = 1; + /* @Document The time for probe request to finish. + @Type time.ms + @Restriction ReadOnlyForUser + */ + int64 delay = 2; + /* @Document The error caused this outbound failed to relay probe request + @Restriction NotMachineReadable +*/ + string last_error_reason = 3; +} + +message Intensity{ + /* @Document The time interval for a probe request in ms. + @Type time.ms + */ + uint32 probe_interval = 1; +} +message Config { + /* @Document The selectors for outbound under observation + */ + repeated string subject_selector = 2; + + string probe_url = 3; + + int64 probe_interval = 4; + + bool enable_concurrency = 5; +} \ No newline at end of file diff --git a/subproject/Xray-core-main/app/observatory/explainErrors.go b/subproject/Xray-core-main/app/observatory/explainErrors.go new file mode 100644 index 00000000..287a78ef --- /dev/null +++ b/subproject/Xray-core-main/app/observatory/explainErrors.go @@ -0,0 +1,26 @@ +package observatory + +import "github.com/xtls/xray-core/common/errors" + +type errorCollector struct { + errors *errors.Error +} + +func (e *errorCollector) SubmitError(err error) { + if e.errors == nil { + e.errors = errors.New("underlying connection error").Base(err) + return + } + e.errors = e.errors.Base(errors.New("underlying connection error").Base(err)) +} + +func newErrorCollector() *errorCollector { + return &errorCollector{} +} + +func (e *errorCollector) UnderlyingError() error { + if e.errors == nil { + return errors.New("failed to produce report") + } + return e.errors +} diff --git a/subproject/Xray-core-main/app/observatory/observatory.go b/subproject/Xray-core-main/app/observatory/observatory.go new file mode 100644 index 00000000..43e79a60 --- /dev/null +++ b/subproject/Xray-core-main/app/observatory/observatory.go @@ -0,0 +1 @@ +package observatory diff --git a/subproject/Xray-core-main/app/observatory/observer.go b/subproject/Xray-core-main/app/observatory/observer.go new file mode 100644 index 00000000..02dbaf13 --- /dev/null +++ b/subproject/Xray-core-main/app/observatory/observer.go @@ -0,0 +1,252 @@ +package observatory + +import ( + "context" + "net" + "net/http" + "net/url" + "slices" + "sort" + "sync" + "time" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/errors" + v2net "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/common/signal/done" + "github.com/xtls/xray-core/common/task" + "github.com/xtls/xray-core/common/utils" + "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/features/extension" + "github.com/xtls/xray-core/features/outbound" + "github.com/xtls/xray-core/features/routing" + "github.com/xtls/xray-core/transport/internet/tagged" + "google.golang.org/protobuf/proto" +) + +type Observer struct { + config *Config + ctx context.Context + + statusLock sync.Mutex + status []*OutboundStatus + + finished *done.Instance + + ohm outbound.Manager + dispatcher routing.Dispatcher +} + +func (o *Observer) GetObservation(ctx context.Context) (proto.Message, error) { + return &ObservationResult{Status: o.status}, nil +} + +func (o *Observer) Type() interface{} { + return extension.ObservatoryType() +} + +func (o *Observer) Start() error { + if o.config != nil && len(o.config.SubjectSelector) != 0 { + o.finished = done.New() + go o.background() + } + return nil +} + +func (o *Observer) Close() error { + if o.finished != nil { + return o.finished.Close() + } + return nil +} + +func (o *Observer) background() { + for !o.finished.Done() { + hs, ok := o.ohm.(outbound.HandlerSelector) + if !ok { + errors.LogInfo(o.ctx, "outbound.Manager is not a HandlerSelector") + return + } + + outbounds := hs.Select(o.config.SubjectSelector) + + o.clearRemovedOutbounds(outbounds) + + sleepTime := time.Second * 10 + if o.config.ProbeInterval != 0 { + sleepTime = time.Duration(o.config.ProbeInterval) + } + + if !o.config.EnableConcurrency { + sort.Strings(outbounds) + for _, v := range outbounds { + result := o.probe(v) + o.updateStatusForResult(v, &result) + if o.finished.Done() { + return + } + time.Sleep(sleepTime) + } + continue + } + + ch := make(chan struct{}, len(outbounds)) + + for _, v := range outbounds { + go func(v string) { + result := o.probe(v) + o.updateStatusForResult(v, &result) + ch <- struct{}{} + }(v) + } + + for range outbounds { + select { + case <-ch: + case <-o.finished.Wait(): + return + } + } + time.Sleep(sleepTime) + } +} + +func (o *Observer) clearRemovedOutbounds(outbounds []string) { + o.statusLock.Lock() + defer o.statusLock.Unlock() + if len(o.status) == 0 { + return + } + var pruned []*OutboundStatus + for _, status := range o.status { + if slices.Contains(outbounds, status.OutboundTag) { + pruned = append(pruned, status) + } + } + o.status = pruned +} + +func (o *Observer) probe(outbound string) ProbeResult { + errorCollectorForRequest := newErrorCollector() + + httpTransport := http.Transport{ + Proxy: func(*http.Request) (*url.URL, error) { + return nil, nil + }, + DialContext: func(ctx context.Context, network string, addr string) (net.Conn, error) { + var connection net.Conn + taskErr := task.Run(ctx, func() error { + // MUST use Xray's built in context system + dest, err := v2net.ParseDestination(network + ":" + addr) + if err != nil { + return errors.New("cannot understand address").Base(err) + } + trackedCtx := session.TrackedConnectionError(o.ctx, errorCollectorForRequest) + conn, err := tagged.Dialer(trackedCtx, o.dispatcher, dest, outbound) + if err != nil { + return errors.New("cannot dial remote address ", dest).Base(err) + } + connection = conn + return nil + }) + if taskErr != nil { + return nil, errors.New("cannot finish connection").Base(taskErr) + } + return connection, nil + }, + TLSHandshakeTimeout: time.Second * 5, + } + httpClient := &http.Client{ + Transport: &httpTransport, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + Jar: nil, + Timeout: time.Second * 5, + } + var GETTime time.Duration + err := task.Run(o.ctx, func() error { + startTime := time.Now() + probeURL := "https://www.google.com/generate_204" + if o.config.ProbeUrl != "" { + probeURL = o.config.ProbeUrl + } + req, _ := http.NewRequest(http.MethodGet, probeURL, nil) + utils.TryDefaultHeadersWith(req.Header, "nav") + response, err := httpClient.Do(req) + if err != nil { + return errors.New("outbound failed to relay connection").Base(err) + } + if response.Body != nil { + response.Body.Close() + } + endTime := time.Now() + GETTime = endTime.Sub(startTime) + return nil + }) + if err != nil { + var errorMessage = "the outbound " + outbound + " is dead: GET request failed:" + err.Error() + "with outbound handler report underlying connection failed" + errors.LogInfoInner(o.ctx, errorCollectorForRequest.UnderlyingError(), errorMessage) + return ProbeResult{Alive: false, LastErrorReason: errorMessage} + } + errors.LogInfo(o.ctx, "the outbound ", outbound, " is alive:", GETTime.Seconds()) + return ProbeResult{Alive: true, Delay: GETTime.Milliseconds()} +} + +func (o *Observer) updateStatusForResult(outbound string, result *ProbeResult) { + o.statusLock.Lock() + defer o.statusLock.Unlock() + var status *OutboundStatus + if location := o.findStatusLocationLockHolderOnly(outbound); location != -1 { + status = o.status[location] + } else { + status = &OutboundStatus{} + o.status = append(o.status, status) + } + + status.LastTryTime = time.Now().Unix() + status.OutboundTag = outbound + status.Alive = result.Alive + if result.Alive { + status.Delay = result.Delay + status.LastSeenTime = status.LastTryTime + status.LastErrorReason = "" + } else { + status.LastErrorReason = result.LastErrorReason + status.Delay = 99999999 + } +} + +func (o *Observer) findStatusLocationLockHolderOnly(outbound string) int { + for i, v := range o.status { + if v.OutboundTag == outbound { + return i + } + } + return -1 +} + +func New(ctx context.Context, config *Config) (*Observer, error) { + var outboundManager outbound.Manager + var dispatcher routing.Dispatcher + err := core.RequireFeatures(ctx, func(om outbound.Manager, rd routing.Dispatcher) { + outboundManager = om + dispatcher = rd + }) + if err != nil { + return nil, errors.New("Cannot get depended features").Base(err) + } + return &Observer{ + config: config, + ctx: ctx, + ohm: outboundManager, + dispatcher: dispatcher, + }, nil +} + +func init() { + common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + return New(ctx, config.(*Config)) + })) +} diff --git a/subproject/Xray-core-main/app/observatory/observer_test.go b/subproject/Xray-core-main/app/observatory/observer_test.go new file mode 100644 index 00000000..02d40fa0 --- /dev/null +++ b/subproject/Xray-core-main/app/observatory/observer_test.go @@ -0,0 +1,64 @@ +package observatory + +import "testing" + +func TestObserverUpdateStatusPrunesStaleOutbounds(t *testing.T) { + observer := &Observer{ + status: []*OutboundStatus{ + { + OutboundTag: "keep", + Alive: true, + Delay: 42, + LastErrorReason: "", + LastSeenTime: 111, + LastTryTime: 222, + }, + { + OutboundTag: "drop", + Alive: false, + Delay: 99999999, + LastErrorReason: "probe failed", + LastSeenTime: 333, + LastTryTime: 444, + }, + }, + } + + observer.updateStatus([]string{"keep"}) + + if len(observer.status) != 1 { + t.Fatalf("expected 1 status after pruning, got %d", len(observer.status)) + } + + got := observer.status[0] + if got.OutboundTag != "keep" { + t.Fatalf("expected remaining status for keep, got %q", got.OutboundTag) + } + if !got.Alive { + t.Fatal("expected remaining status to preserve Alive field") + } + if got.Delay != 42 { + t.Fatalf("expected remaining status to preserve Delay, got %d", got.Delay) + } + if got.LastSeenTime != 111 { + t.Fatalf("expected remaining status to preserve LastSeenTime, got %d", got.LastSeenTime) + } + if got.LastTryTime != 222 { + t.Fatalf("expected remaining status to preserve LastTryTime, got %d", got.LastTryTime) + } +} + +func TestObserverUpdateStatusClearsWhenNoOutboundsRemain(t *testing.T) { + observer := &Observer{ + status: []*OutboundStatus{ + {OutboundTag: "drop-1"}, + {OutboundTag: "drop-2"}, + }, + } + + observer.updateStatus(nil) + + if len(observer.status) != 0 { + t.Fatalf("expected all statuses to be removed, got %d", len(observer.status)) + } +} diff --git a/subproject/Xray-core-main/app/policy/config.go b/subproject/Xray-core-main/app/policy/config.go new file mode 100644 index 00000000..9e5ee1c2 --- /dev/null +++ b/subproject/Xray-core-main/app/policy/config.go @@ -0,0 +1,94 @@ +package policy + +import ( + "time" + + "github.com/xtls/xray-core/features/policy" +) + +// Duration converts Second to time.Duration. +func (s *Second) Duration() time.Duration { + if s == nil { + return 0 + } + return time.Second * time.Duration(s.Value) +} + +func defaultPolicy() *Policy { + p := policy.SessionDefault() + + return &Policy{ + Timeout: &Policy_Timeout{ + Handshake: &Second{Value: uint32(p.Timeouts.Handshake / time.Second)}, + ConnectionIdle: &Second{Value: uint32(p.Timeouts.ConnectionIdle / time.Second)}, + UplinkOnly: &Second{Value: uint32(p.Timeouts.UplinkOnly / time.Second)}, + DownlinkOnly: &Second{Value: uint32(p.Timeouts.DownlinkOnly / time.Second)}, + }, + Buffer: &Policy_Buffer{ + Connection: p.Buffer.PerConnection, + }, + } +} + +func (p *Policy_Timeout) overrideWith(another *Policy_Timeout) { + if another.Handshake != nil { + p.Handshake = &Second{Value: another.Handshake.Value} + } + if another.ConnectionIdle != nil { + p.ConnectionIdle = &Second{Value: another.ConnectionIdle.Value} + } + if another.UplinkOnly != nil { + p.UplinkOnly = &Second{Value: another.UplinkOnly.Value} + } + if another.DownlinkOnly != nil { + p.DownlinkOnly = &Second{Value: another.DownlinkOnly.Value} + } +} + +func (p *Policy) overrideWith(another *Policy) { + if another.Timeout != nil { + p.Timeout.overrideWith(another.Timeout) + } + if another.Stats != nil && p.Stats == nil { + p.Stats = &Policy_Stats{} + p.Stats = another.Stats + } + if another.Buffer != nil { + p.Buffer = &Policy_Buffer{ + Connection: another.Buffer.Connection, + } + } +} + +// ToCorePolicy converts this Policy to policy.Session. +func (p *Policy) ToCorePolicy() policy.Session { + cp := policy.SessionDefault() + + if p.Timeout != nil { + cp.Timeouts.ConnectionIdle = p.Timeout.ConnectionIdle.Duration() + cp.Timeouts.Handshake = p.Timeout.Handshake.Duration() + cp.Timeouts.DownlinkOnly = p.Timeout.DownlinkOnly.Duration() + cp.Timeouts.UplinkOnly = p.Timeout.UplinkOnly.Duration() + } + if p.Stats != nil { + cp.Stats.UserUplink = p.Stats.UserUplink + cp.Stats.UserDownlink = p.Stats.UserDownlink + cp.Stats.UserOnline = p.Stats.UserOnline + } + if p.Buffer != nil { + cp.Buffer.PerConnection = p.Buffer.Connection + } + return cp +} + +// ToCorePolicy converts this SystemPolicy to policy.System. +func (p *SystemPolicy) ToCorePolicy() policy.System { + return policy.System{ + Stats: policy.SystemStats{ + InboundUplink: p.Stats.InboundUplink, + InboundDownlink: p.Stats.InboundDownlink, + OutboundUplink: p.Stats.OutboundUplink, + OutboundDownlink: p.Stats.OutboundDownlink, + }, + } +} diff --git a/subproject/Xray-core-main/app/policy/config.pb.go b/subproject/Xray-core-main/app/policy/config.pb.go new file mode 100644 index 00000000..ce4301f1 --- /dev/null +++ b/subproject/Xray-core-main/app/policy/config.pb.go @@ -0,0 +1,574 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: app/policy/config.proto + +package policy + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Second struct { + state protoimpl.MessageState `protogen:"open.v1"` + Value uint32 `protobuf:"varint,1,opt,name=value,proto3" json:"value,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Second) Reset() { + *x = Second{} + mi := &file_app_policy_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Second) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Second) ProtoMessage() {} + +func (x *Second) ProtoReflect() protoreflect.Message { + mi := &file_app_policy_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Second.ProtoReflect.Descriptor instead. +func (*Second) Descriptor() ([]byte, []int) { + return file_app_policy_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Second) GetValue() uint32 { + if x != nil { + return x.Value + } + return 0 +} + +type Policy struct { + state protoimpl.MessageState `protogen:"open.v1"` + Timeout *Policy_Timeout `protobuf:"bytes,1,opt,name=timeout,proto3" json:"timeout,omitempty"` + Stats *Policy_Stats `protobuf:"bytes,2,opt,name=stats,proto3" json:"stats,omitempty"` + Buffer *Policy_Buffer `protobuf:"bytes,3,opt,name=buffer,proto3" json:"buffer,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Policy) Reset() { + *x = Policy{} + mi := &file_app_policy_config_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Policy) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Policy) ProtoMessage() {} + +func (x *Policy) ProtoReflect() protoreflect.Message { + mi := &file_app_policy_config_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Policy.ProtoReflect.Descriptor instead. +func (*Policy) Descriptor() ([]byte, []int) { + return file_app_policy_config_proto_rawDescGZIP(), []int{1} +} + +func (x *Policy) GetTimeout() *Policy_Timeout { + if x != nil { + return x.Timeout + } + return nil +} + +func (x *Policy) GetStats() *Policy_Stats { + if x != nil { + return x.Stats + } + return nil +} + +func (x *Policy) GetBuffer() *Policy_Buffer { + if x != nil { + return x.Buffer + } + return nil +} + +type SystemPolicy struct { + state protoimpl.MessageState `protogen:"open.v1"` + Stats *SystemPolicy_Stats `protobuf:"bytes,1,opt,name=stats,proto3" json:"stats,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SystemPolicy) Reset() { + *x = SystemPolicy{} + mi := &file_app_policy_config_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SystemPolicy) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SystemPolicy) ProtoMessage() {} + +func (x *SystemPolicy) ProtoReflect() protoreflect.Message { + mi := &file_app_policy_config_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SystemPolicy.ProtoReflect.Descriptor instead. +func (*SystemPolicy) Descriptor() ([]byte, []int) { + return file_app_policy_config_proto_rawDescGZIP(), []int{2} +} + +func (x *SystemPolicy) GetStats() *SystemPolicy_Stats { + if x != nil { + return x.Stats + } + return nil +} + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + Level map[uint32]*Policy `protobuf:"bytes,1,rep,name=level,proto3" json:"level,omitempty" protobuf_key:"varint,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + System *SystemPolicy `protobuf:"bytes,2,opt,name=system,proto3" json:"system,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_app_policy_config_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_app_policy_config_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_app_policy_config_proto_rawDescGZIP(), []int{3} +} + +func (x *Config) GetLevel() map[uint32]*Policy { + if x != nil { + return x.Level + } + return nil +} + +func (x *Config) GetSystem() *SystemPolicy { + if x != nil { + return x.System + } + return nil +} + +// Timeout is a message for timeout settings in various stages, in seconds. +type Policy_Timeout struct { + state protoimpl.MessageState `protogen:"open.v1"` + Handshake *Second `protobuf:"bytes,1,opt,name=handshake,proto3" json:"handshake,omitempty"` + ConnectionIdle *Second `protobuf:"bytes,2,opt,name=connection_idle,json=connectionIdle,proto3" json:"connection_idle,omitempty"` + UplinkOnly *Second `protobuf:"bytes,3,opt,name=uplink_only,json=uplinkOnly,proto3" json:"uplink_only,omitempty"` + DownlinkOnly *Second `protobuf:"bytes,4,opt,name=downlink_only,json=downlinkOnly,proto3" json:"downlink_only,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Policy_Timeout) Reset() { + *x = Policy_Timeout{} + mi := &file_app_policy_config_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Policy_Timeout) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Policy_Timeout) ProtoMessage() {} + +func (x *Policy_Timeout) ProtoReflect() protoreflect.Message { + mi := &file_app_policy_config_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Policy_Timeout.ProtoReflect.Descriptor instead. +func (*Policy_Timeout) Descriptor() ([]byte, []int) { + return file_app_policy_config_proto_rawDescGZIP(), []int{1, 0} +} + +func (x *Policy_Timeout) GetHandshake() *Second { + if x != nil { + return x.Handshake + } + return nil +} + +func (x *Policy_Timeout) GetConnectionIdle() *Second { + if x != nil { + return x.ConnectionIdle + } + return nil +} + +func (x *Policy_Timeout) GetUplinkOnly() *Second { + if x != nil { + return x.UplinkOnly + } + return nil +} + +func (x *Policy_Timeout) GetDownlinkOnly() *Second { + if x != nil { + return x.DownlinkOnly + } + return nil +} + +type Policy_Stats struct { + state protoimpl.MessageState `protogen:"open.v1"` + UserUplink bool `protobuf:"varint,1,opt,name=user_uplink,json=userUplink,proto3" json:"user_uplink,omitempty"` + UserDownlink bool `protobuf:"varint,2,opt,name=user_downlink,json=userDownlink,proto3" json:"user_downlink,omitempty"` + UserOnline bool `protobuf:"varint,3,opt,name=user_online,json=userOnline,proto3" json:"user_online,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Policy_Stats) Reset() { + *x = Policy_Stats{} + mi := &file_app_policy_config_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Policy_Stats) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Policy_Stats) ProtoMessage() {} + +func (x *Policy_Stats) ProtoReflect() protoreflect.Message { + mi := &file_app_policy_config_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Policy_Stats.ProtoReflect.Descriptor instead. +func (*Policy_Stats) Descriptor() ([]byte, []int) { + return file_app_policy_config_proto_rawDescGZIP(), []int{1, 1} +} + +func (x *Policy_Stats) GetUserUplink() bool { + if x != nil { + return x.UserUplink + } + return false +} + +func (x *Policy_Stats) GetUserDownlink() bool { + if x != nil { + return x.UserDownlink + } + return false +} + +func (x *Policy_Stats) GetUserOnline() bool { + if x != nil { + return x.UserOnline + } + return false +} + +type Policy_Buffer struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Buffer size per connection, in bytes. -1 for unlimited buffer. + Connection int32 `protobuf:"varint,1,opt,name=connection,proto3" json:"connection,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Policy_Buffer) Reset() { + *x = Policy_Buffer{} + mi := &file_app_policy_config_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Policy_Buffer) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Policy_Buffer) ProtoMessage() {} + +func (x *Policy_Buffer) ProtoReflect() protoreflect.Message { + mi := &file_app_policy_config_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Policy_Buffer.ProtoReflect.Descriptor instead. +func (*Policy_Buffer) Descriptor() ([]byte, []int) { + return file_app_policy_config_proto_rawDescGZIP(), []int{1, 2} +} + +func (x *Policy_Buffer) GetConnection() int32 { + if x != nil { + return x.Connection + } + return 0 +} + +type SystemPolicy_Stats struct { + state protoimpl.MessageState `protogen:"open.v1"` + InboundUplink bool `protobuf:"varint,1,opt,name=inbound_uplink,json=inboundUplink,proto3" json:"inbound_uplink,omitempty"` + InboundDownlink bool `protobuf:"varint,2,opt,name=inbound_downlink,json=inboundDownlink,proto3" json:"inbound_downlink,omitempty"` + OutboundUplink bool `protobuf:"varint,3,opt,name=outbound_uplink,json=outboundUplink,proto3" json:"outbound_uplink,omitempty"` + OutboundDownlink bool `protobuf:"varint,4,opt,name=outbound_downlink,json=outboundDownlink,proto3" json:"outbound_downlink,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SystemPolicy_Stats) Reset() { + *x = SystemPolicy_Stats{} + mi := &file_app_policy_config_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SystemPolicy_Stats) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SystemPolicy_Stats) ProtoMessage() {} + +func (x *SystemPolicy_Stats) ProtoReflect() protoreflect.Message { + mi := &file_app_policy_config_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 SystemPolicy_Stats.ProtoReflect.Descriptor instead. +func (*SystemPolicy_Stats) Descriptor() ([]byte, []int) { + return file_app_policy_config_proto_rawDescGZIP(), []int{2, 0} +} + +func (x *SystemPolicy_Stats) GetInboundUplink() bool { + if x != nil { + return x.InboundUplink + } + return false +} + +func (x *SystemPolicy_Stats) GetInboundDownlink() bool { + if x != nil { + return x.InboundDownlink + } + return false +} + +func (x *SystemPolicy_Stats) GetOutboundUplink() bool { + if x != nil { + return x.OutboundUplink + } + return false +} + +func (x *SystemPolicy_Stats) GetOutboundDownlink() bool { + if x != nil { + return x.OutboundDownlink + } + return false +} + +var File_app_policy_config_proto protoreflect.FileDescriptor + +const file_app_policy_config_proto_rawDesc = "" + + "\n" + + "\x17app/policy/config.proto\x12\x0fxray.app.policy\"\x1e\n" + + "\x06Second\x12\x14\n" + + "\x05value\x18\x01 \x01(\rR\x05value\"\xc7\x04\n" + + "\x06Policy\x129\n" + + "\atimeout\x18\x01 \x01(\v2\x1f.xray.app.policy.Policy.TimeoutR\atimeout\x123\n" + + "\x05stats\x18\x02 \x01(\v2\x1d.xray.app.policy.Policy.StatsR\x05stats\x126\n" + + "\x06buffer\x18\x03 \x01(\v2\x1e.xray.app.policy.Policy.BufferR\x06buffer\x1a\xfa\x01\n" + + "\aTimeout\x125\n" + + "\thandshake\x18\x01 \x01(\v2\x17.xray.app.policy.SecondR\thandshake\x12@\n" + + "\x0fconnection_idle\x18\x02 \x01(\v2\x17.xray.app.policy.SecondR\x0econnectionIdle\x128\n" + + "\vuplink_only\x18\x03 \x01(\v2\x17.xray.app.policy.SecondR\n" + + "uplinkOnly\x12<\n" + + "\rdownlink_only\x18\x04 \x01(\v2\x17.xray.app.policy.SecondR\fdownlinkOnly\x1an\n" + + "\x05Stats\x12\x1f\n" + + "\vuser_uplink\x18\x01 \x01(\bR\n" + + "userUplink\x12#\n" + + "\ruser_downlink\x18\x02 \x01(\bR\fuserDownlink\x12\x1f\n" + + "\vuser_online\x18\x03 \x01(\bR\n" + + "userOnline\x1a(\n" + + "\x06Buffer\x12\x1e\n" + + "\n" + + "connection\x18\x01 \x01(\x05R\n" + + "connection\"\xfb\x01\n" + + "\fSystemPolicy\x129\n" + + "\x05stats\x18\x01 \x01(\v2#.xray.app.policy.SystemPolicy.StatsR\x05stats\x1a\xaf\x01\n" + + "\x05Stats\x12%\n" + + "\x0einbound_uplink\x18\x01 \x01(\bR\rinboundUplink\x12)\n" + + "\x10inbound_downlink\x18\x02 \x01(\bR\x0finboundDownlink\x12'\n" + + "\x0foutbound_uplink\x18\x03 \x01(\bR\x0eoutboundUplink\x12+\n" + + "\x11outbound_downlink\x18\x04 \x01(\bR\x10outboundDownlink\"\xcc\x01\n" + + "\x06Config\x128\n" + + "\x05level\x18\x01 \x03(\v2\".xray.app.policy.Config.LevelEntryR\x05level\x125\n" + + "\x06system\x18\x02 \x01(\v2\x1d.xray.app.policy.SystemPolicyR\x06system\x1aQ\n" + + "\n" + + "LevelEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\rR\x03key\x12-\n" + + "\x05value\x18\x02 \x01(\v2\x17.xray.app.policy.PolicyR\x05value:\x028\x01BO\n" + + "\x13com.xray.app.policyP\x01Z$github.com/xtls/xray-core/app/policy\xaa\x02\x0fXray.App.Policyb\x06proto3" + +var ( + file_app_policy_config_proto_rawDescOnce sync.Once + file_app_policy_config_proto_rawDescData []byte +) + +func file_app_policy_config_proto_rawDescGZIP() []byte { + file_app_policy_config_proto_rawDescOnce.Do(func() { + file_app_policy_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_app_policy_config_proto_rawDesc), len(file_app_policy_config_proto_rawDesc))) + }) + return file_app_policy_config_proto_rawDescData +} + +var file_app_policy_config_proto_msgTypes = make([]protoimpl.MessageInfo, 9) +var file_app_policy_config_proto_goTypes = []any{ + (*Second)(nil), // 0: xray.app.policy.Second + (*Policy)(nil), // 1: xray.app.policy.Policy + (*SystemPolicy)(nil), // 2: xray.app.policy.SystemPolicy + (*Config)(nil), // 3: xray.app.policy.Config + (*Policy_Timeout)(nil), // 4: xray.app.policy.Policy.Timeout + (*Policy_Stats)(nil), // 5: xray.app.policy.Policy.Stats + (*Policy_Buffer)(nil), // 6: xray.app.policy.Policy.Buffer + (*SystemPolicy_Stats)(nil), // 7: xray.app.policy.SystemPolicy.Stats + nil, // 8: xray.app.policy.Config.LevelEntry +} +var file_app_policy_config_proto_depIdxs = []int32{ + 4, // 0: xray.app.policy.Policy.timeout:type_name -> xray.app.policy.Policy.Timeout + 5, // 1: xray.app.policy.Policy.stats:type_name -> xray.app.policy.Policy.Stats + 6, // 2: xray.app.policy.Policy.buffer:type_name -> xray.app.policy.Policy.Buffer + 7, // 3: xray.app.policy.SystemPolicy.stats:type_name -> xray.app.policy.SystemPolicy.Stats + 8, // 4: xray.app.policy.Config.level:type_name -> xray.app.policy.Config.LevelEntry + 2, // 5: xray.app.policy.Config.system:type_name -> xray.app.policy.SystemPolicy + 0, // 6: xray.app.policy.Policy.Timeout.handshake:type_name -> xray.app.policy.Second + 0, // 7: xray.app.policy.Policy.Timeout.connection_idle:type_name -> xray.app.policy.Second + 0, // 8: xray.app.policy.Policy.Timeout.uplink_only:type_name -> xray.app.policy.Second + 0, // 9: xray.app.policy.Policy.Timeout.downlink_only:type_name -> xray.app.policy.Second + 1, // 10: xray.app.policy.Config.LevelEntry.value:type_name -> xray.app.policy.Policy + 11, // [11:11] is the sub-list for method output_type + 11, // [11:11] is the sub-list for method input_type + 11, // [11:11] is the sub-list for extension type_name + 11, // [11:11] is the sub-list for extension extendee + 0, // [0:11] is the sub-list for field type_name +} + +func init() { file_app_policy_config_proto_init() } +func file_app_policy_config_proto_init() { + if File_app_policy_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_app_policy_config_proto_rawDesc), len(file_app_policy_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 9, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_app_policy_config_proto_goTypes, + DependencyIndexes: file_app_policy_config_proto_depIdxs, + MessageInfos: file_app_policy_config_proto_msgTypes, + }.Build() + File_app_policy_config_proto = out.File + file_app_policy_config_proto_goTypes = nil + file_app_policy_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/app/policy/config.proto b/subproject/Xray-core-main/app/policy/config.proto new file mode 100644 index 00000000..eefcc17f --- /dev/null +++ b/subproject/Xray-core-main/app/policy/config.proto @@ -0,0 +1,52 @@ +syntax = "proto3"; + +package xray.app.policy; +option csharp_namespace = "Xray.App.Policy"; +option go_package = "github.com/xtls/xray-core/app/policy"; +option java_package = "com.xray.app.policy"; +option java_multiple_files = true; + +message Second { + uint32 value = 1; +} + +message Policy { + // Timeout is a message for timeout settings in various stages, in seconds. + message Timeout { + Second handshake = 1; + Second connection_idle = 2; + Second uplink_only = 3; + Second downlink_only = 4; + } + + message Stats { + bool user_uplink = 1; + bool user_downlink = 2; + bool user_online = 3; + } + + message Buffer { + // Buffer size per connection, in bytes. -1 for unlimited buffer. + int32 connection = 1; + } + + Timeout timeout = 1; + Stats stats = 2; + Buffer buffer = 3; +} + +message SystemPolicy { + message Stats { + bool inbound_uplink = 1; + bool inbound_downlink = 2; + bool outbound_uplink = 3; + bool outbound_downlink = 4; + } + + Stats stats = 1; +} + +message Config { + map level = 1; + SystemPolicy system = 2; +} diff --git a/subproject/Xray-core-main/app/policy/manager.go b/subproject/Xray-core-main/app/policy/manager.go new file mode 100644 index 00000000..ad2c6255 --- /dev/null +++ b/subproject/Xray-core-main/app/policy/manager.go @@ -0,0 +1,68 @@ +package policy + +import ( + "context" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/features/policy" +) + +// Instance is an instance of Policy manager. +type Instance struct { + levels map[uint32]*Policy + system *SystemPolicy +} + +// New creates new Policy manager instance. +func New(ctx context.Context, config *Config) (*Instance, error) { + m := &Instance{ + levels: make(map[uint32]*Policy), + system: config.System, + } + if len(config.Level) > 0 { + for lv, p := range config.Level { + pp := defaultPolicy() + pp.overrideWith(p) + m.levels[lv] = pp + } + } + + return m, nil +} + +// Type implements common.HasType. +func (*Instance) Type() interface{} { + return policy.ManagerType() +} + +// ForLevel implements policy.Manager. +func (m *Instance) ForLevel(level uint32) policy.Session { + if p, ok := m.levels[level]; ok { + return p.ToCorePolicy() + } + return policy.SessionDefault() +} + +// ForSystem implements policy.Manager. +func (m *Instance) ForSystem() policy.System { + if m.system == nil { + return policy.System{} + } + return m.system.ToCorePolicy() +} + +// Start implements common.Runnable.Start(). +func (m *Instance) Start() error { + return nil +} + +// Close implements common.Closable.Close(). +func (m *Instance) Close() error { + return nil +} + +func init() { + common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + return New(ctx, config.(*Config)) + })) +} diff --git a/subproject/Xray-core-main/app/policy/manager_test.go b/subproject/Xray-core-main/app/policy/manager_test.go new file mode 100644 index 00000000..e738a380 --- /dev/null +++ b/subproject/Xray-core-main/app/policy/manager_test.go @@ -0,0 +1,45 @@ +package policy_test + +import ( + "context" + "testing" + "time" + + . "github.com/xtls/xray-core/app/policy" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/features/policy" +) + +func TestPolicy(t *testing.T) { + manager, err := New(context.Background(), &Config{ + Level: map[uint32]*Policy{ + 0: { + Timeout: &Policy_Timeout{ + Handshake: &Second{ + Value: 2, + }, + }, + }, + }, + }) + common.Must(err) + + pDefault := policy.SessionDefault() + + { + p := manager.ForLevel(0) + if p.Timeouts.Handshake != 2*time.Second { + t.Error("expect 2 sec timeout, but got ", p.Timeouts.Handshake) + } + if p.Timeouts.ConnectionIdle != pDefault.Timeouts.ConnectionIdle { + t.Error("expect ", pDefault.Timeouts.ConnectionIdle, " sec timeout, but got ", p.Timeouts.ConnectionIdle) + } + } + + { + p := manager.ForLevel(1) + if p.Timeouts.Handshake != pDefault.Timeouts.Handshake { + t.Error("expect ", pDefault.Timeouts.Handshake, " sec timeout, but got ", p.Timeouts.Handshake) + } + } +} diff --git a/subproject/Xray-core-main/app/policy/policy.go b/subproject/Xray-core-main/app/policy/policy.go new file mode 100644 index 00000000..60613a65 --- /dev/null +++ b/subproject/Xray-core-main/app/policy/policy.go @@ -0,0 +1,2 @@ +// Package policy is an implementation of policy.Manager feature. +package policy diff --git a/subproject/Xray-core-main/app/proxyman/command/command.go b/subproject/Xray-core-main/app/proxyman/command/command.go new file mode 100644 index 00000000..a86f0921 --- /dev/null +++ b/subproject/Xray-core-main/app/proxyman/command/command.go @@ -0,0 +1,234 @@ +package command + +import ( + "context" + + "github.com/xtls/xray-core/app/commander" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/features/inbound" + "github.com/xtls/xray-core/features/outbound" + "github.com/xtls/xray-core/proxy" + grpc "google.golang.org/grpc" +) + +// InboundOperation is the interface for operations that applies to inbound handlers. +type InboundOperation interface { + // ApplyInbound applies this operation to the given inbound handler. + ApplyInbound(context.Context, inbound.Handler) error +} + +// OutboundOperation is the interface for operations that applies to outbound handlers. +type OutboundOperation interface { + // ApplyOutbound applies this operation to the given outbound handler. + ApplyOutbound(context.Context, outbound.Handler) error +} + +func getInbound(handler inbound.Handler) (proxy.Inbound, error) { + gi, ok := handler.(proxy.GetInbound) + if !ok { + return nil, errors.New("can't get inbound proxy from handler.") + } + return gi.GetInbound(), nil +} + +// ApplyInbound implements InboundOperation. +func (op *AddUserOperation) ApplyInbound(ctx context.Context, handler inbound.Handler) error { + p, err := getInbound(handler) + if err != nil { + return err + } + um, ok := p.(proxy.UserManager) + if !ok { + return errors.New("proxy is not a UserManager") + } + mUser, err := op.User.ToMemoryUser() + if err != nil { + return errors.New("failed to parse user").Base(err) + } + return um.AddUser(ctx, mUser) +} + +// ApplyInbound implements InboundOperation. +func (op *RemoveUserOperation) ApplyInbound(ctx context.Context, handler inbound.Handler) error { + p, err := getInbound(handler) + if err != nil { + return err + } + um, ok := p.(proxy.UserManager) + if !ok { + return errors.New("proxy is not a UserManager") + } + return um.RemoveUser(ctx, op.Email) +} + +type handlerServer struct { + s *core.Instance + ihm inbound.Manager + ohm outbound.Manager +} + +func (s *handlerServer) AddInbound(ctx context.Context, request *AddInboundRequest) (*AddInboundResponse, error) { + if err := core.AddInboundHandler(s.s, request.Inbound); err != nil { + return nil, err + } + + return &AddInboundResponse{}, nil +} + +func (s *handlerServer) RemoveInbound(ctx context.Context, request *RemoveInboundRequest) (*RemoveInboundResponse, error) { + return &RemoveInboundResponse{}, s.ihm.RemoveHandler(ctx, request.Tag) +} + +func (s *handlerServer) AlterInbound(ctx context.Context, request *AlterInboundRequest) (*AlterInboundResponse, error) { + rawOperation, err := request.Operation.GetInstance() + if err != nil { + return nil, errors.New("unknown operation").Base(err) + } + operation, ok := rawOperation.(InboundOperation) + if !ok { + return nil, errors.New("not an inbound operation") + } + + handler, err := s.ihm.GetHandler(ctx, request.Tag) + if err != nil { + return nil, errors.New("failed to get handler: ", request.Tag).Base(err) + } + + return &AlterInboundResponse{}, operation.ApplyInbound(ctx, handler) +} + +func (s *handlerServer) ListInbounds(ctx context.Context, request *ListInboundsRequest) (*ListInboundsResponse, error) { + handlers := s.ihm.ListHandlers(ctx) + response := &ListInboundsResponse{} + if request.GetIsOnlyTags() { + for _, handler := range handlers { + response.Inbounds = append(response.Inbounds, &core.InboundHandlerConfig{ + Tag: handler.Tag(), + }) + } + } else { + for _, handler := range handlers { + response.Inbounds = append(response.Inbounds, &core.InboundHandlerConfig{ + Tag: handler.Tag(), + ReceiverSettings: handler.ReceiverSettings(), + ProxySettings: handler.ProxySettings(), + }) + } + } + + return response, nil +} + +func (s *handlerServer) GetInboundUsers(ctx context.Context, request *GetInboundUserRequest) (*GetInboundUserResponse, error) { + handler, err := s.ihm.GetHandler(ctx, request.Tag) + if err != nil { + return nil, errors.New("failed to get handler: ", request.Tag).Base(err) + } + p, err := getInbound(handler) + if err != nil { + return nil, err + } + um, ok := p.(proxy.UserManager) + if !ok { + return nil, errors.New("proxy is not a UserManager") + } + if len(request.Email) > 0 { + return &GetInboundUserResponse{Users: []*protocol.User{protocol.ToProtoUser(um.GetUser(ctx, request.Email))}}, nil + } + var result = make([]*protocol.User, 0, 100) + users := um.GetUsers(ctx) + for _, u := range users { + result = append(result, protocol.ToProtoUser(u)) + } + return &GetInboundUserResponse{Users: result}, nil +} + +func (s *handlerServer) GetInboundUsersCount(ctx context.Context, request *GetInboundUserRequest) (*GetInboundUsersCountResponse, error) { + handler, err := s.ihm.GetHandler(ctx, request.Tag) + if err != nil { + return nil, errors.New("failed to get handler: ", request.Tag).Base(err) + } + p, err := getInbound(handler) + if err != nil { + return nil, err + } + um, ok := p.(proxy.UserManager) + if !ok { + return nil, errors.New("proxy is not a UserManager") + } + return &GetInboundUsersCountResponse{Count: um.GetUsersCount(ctx)}, nil +} + +func (s *handlerServer) AddOutbound(ctx context.Context, request *AddOutboundRequest) (*AddOutboundResponse, error) { + if err := core.AddOutboundHandler(s.s, request.Outbound); err != nil { + return nil, err + } + return &AddOutboundResponse{}, nil +} + +func (s *handlerServer) RemoveOutbound(ctx context.Context, request *RemoveOutboundRequest) (*RemoveOutboundResponse, error) { + return &RemoveOutboundResponse{}, s.ohm.RemoveHandler(ctx, request.Tag) +} + +func (s *handlerServer) AlterOutbound(ctx context.Context, request *AlterOutboundRequest) (*AlterOutboundResponse, error) { + rawOperation, err := request.Operation.GetInstance() + if err != nil { + return nil, errors.New("unknown operation").Base(err) + } + operation, ok := rawOperation.(OutboundOperation) + if !ok { + return nil, errors.New("not an outbound operation") + } + + handler := s.ohm.GetHandler(request.Tag) + return &AlterOutboundResponse{}, operation.ApplyOutbound(ctx, handler) +} + +func (s *handlerServer) ListOutbounds(ctx context.Context, request *ListOutboundsRequest) (*ListOutboundsResponse, error) { + handlers := s.ohm.ListHandlers(ctx) + response := &ListOutboundsResponse{} + for _, handler := range handlers { + // Ignore gRPC outbound + if _, ok := handler.(*commander.Outbound); ok { + continue + } + response.Outbounds = append(response.Outbounds, &core.OutboundHandlerConfig{ + Tag: handler.Tag(), + SenderSettings: handler.SenderSettings(), + ProxySettings: handler.ProxySettings(), + }) + } + return response, nil +} + +func (s *handlerServer) mustEmbedUnimplementedHandlerServiceServer() {} + +type service struct { + v *core.Instance +} + +func (s *service) Register(server *grpc.Server) { + hs := &handlerServer{ + s: s.v, + } + common.Must(s.v.RequireFeatures(func(im inbound.Manager, om outbound.Manager) { + hs.ihm = im + hs.ohm = om + }, false)) + RegisterHandlerServiceServer(server, hs) + + // For compatibility purposes + vCoreDesc := HandlerService_ServiceDesc + vCoreDesc.ServiceName = "v2ray.core.app.proxyman.command.HandlerService" + server.RegisterService(&vCoreDesc, hs) +} + +func init() { + common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, cfg interface{}) (interface{}, error) { + s := core.MustFromContext(ctx) + return &service{v: s}, nil + })) +} diff --git a/subproject/Xray-core-main/app/proxyman/command/command.pb.go b/subproject/Xray-core-main/app/proxyman/command/command.pb.go new file mode 100644 index 00000000..28ecd167 --- /dev/null +++ b/subproject/Xray-core-main/app/proxyman/command/command.pb.go @@ -0,0 +1,1114 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: app/proxyman/command/command.proto + +package command + +import ( + protocol "github.com/xtls/xray-core/common/protocol" + serial "github.com/xtls/xray-core/common/serial" + core "github.com/xtls/xray-core/core" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type AddUserOperation struct { + state protoimpl.MessageState `protogen:"open.v1"` + User *protocol.User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AddUserOperation) Reset() { + *x = AddUserOperation{} + mi := &file_app_proxyman_command_command_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AddUserOperation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddUserOperation) ProtoMessage() {} + +func (x *AddUserOperation) ProtoReflect() protoreflect.Message { + mi := &file_app_proxyman_command_command_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AddUserOperation.ProtoReflect.Descriptor instead. +func (*AddUserOperation) Descriptor() ([]byte, []int) { + return file_app_proxyman_command_command_proto_rawDescGZIP(), []int{0} +} + +func (x *AddUserOperation) GetUser() *protocol.User { + if x != nil { + return x.User + } + return nil +} + +type RemoveUserOperation struct { + state protoimpl.MessageState `protogen:"open.v1"` + Email string `protobuf:"bytes,1,opt,name=email,proto3" json:"email,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RemoveUserOperation) Reset() { + *x = RemoveUserOperation{} + mi := &file_app_proxyman_command_command_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RemoveUserOperation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RemoveUserOperation) ProtoMessage() {} + +func (x *RemoveUserOperation) ProtoReflect() protoreflect.Message { + mi := &file_app_proxyman_command_command_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RemoveUserOperation.ProtoReflect.Descriptor instead. +func (*RemoveUserOperation) Descriptor() ([]byte, []int) { + return file_app_proxyman_command_command_proto_rawDescGZIP(), []int{1} +} + +func (x *RemoveUserOperation) GetEmail() string { + if x != nil { + return x.Email + } + return "" +} + +type AddInboundRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Inbound *core.InboundHandlerConfig `protobuf:"bytes,1,opt,name=inbound,proto3" json:"inbound,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AddInboundRequest) Reset() { + *x = AddInboundRequest{} + mi := &file_app_proxyman_command_command_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AddInboundRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddInboundRequest) ProtoMessage() {} + +func (x *AddInboundRequest) ProtoReflect() protoreflect.Message { + mi := &file_app_proxyman_command_command_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AddInboundRequest.ProtoReflect.Descriptor instead. +func (*AddInboundRequest) Descriptor() ([]byte, []int) { + return file_app_proxyman_command_command_proto_rawDescGZIP(), []int{2} +} + +func (x *AddInboundRequest) GetInbound() *core.InboundHandlerConfig { + if x != nil { + return x.Inbound + } + return nil +} + +type AddInboundResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AddInboundResponse) Reset() { + *x = AddInboundResponse{} + mi := &file_app_proxyman_command_command_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AddInboundResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddInboundResponse) ProtoMessage() {} + +func (x *AddInboundResponse) ProtoReflect() protoreflect.Message { + mi := &file_app_proxyman_command_command_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AddInboundResponse.ProtoReflect.Descriptor instead. +func (*AddInboundResponse) Descriptor() ([]byte, []int) { + return file_app_proxyman_command_command_proto_rawDescGZIP(), []int{3} +} + +type RemoveInboundRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Tag string `protobuf:"bytes,1,opt,name=tag,proto3" json:"tag,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RemoveInboundRequest) Reset() { + *x = RemoveInboundRequest{} + mi := &file_app_proxyman_command_command_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RemoveInboundRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RemoveInboundRequest) ProtoMessage() {} + +func (x *RemoveInboundRequest) ProtoReflect() protoreflect.Message { + mi := &file_app_proxyman_command_command_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RemoveInboundRequest.ProtoReflect.Descriptor instead. +func (*RemoveInboundRequest) Descriptor() ([]byte, []int) { + return file_app_proxyman_command_command_proto_rawDescGZIP(), []int{4} +} + +func (x *RemoveInboundRequest) GetTag() string { + if x != nil { + return x.Tag + } + return "" +} + +type RemoveInboundResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RemoveInboundResponse) Reset() { + *x = RemoveInboundResponse{} + mi := &file_app_proxyman_command_command_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RemoveInboundResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RemoveInboundResponse) ProtoMessage() {} + +func (x *RemoveInboundResponse) ProtoReflect() protoreflect.Message { + mi := &file_app_proxyman_command_command_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RemoveInboundResponse.ProtoReflect.Descriptor instead. +func (*RemoveInboundResponse) Descriptor() ([]byte, []int) { + return file_app_proxyman_command_command_proto_rawDescGZIP(), []int{5} +} + +type AlterInboundRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Tag string `protobuf:"bytes,1,opt,name=tag,proto3" json:"tag,omitempty"` + Operation *serial.TypedMessage `protobuf:"bytes,2,opt,name=operation,proto3" json:"operation,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AlterInboundRequest) Reset() { + *x = AlterInboundRequest{} + mi := &file_app_proxyman_command_command_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AlterInboundRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AlterInboundRequest) ProtoMessage() {} + +func (x *AlterInboundRequest) ProtoReflect() protoreflect.Message { + mi := &file_app_proxyman_command_command_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AlterInboundRequest.ProtoReflect.Descriptor instead. +func (*AlterInboundRequest) Descriptor() ([]byte, []int) { + return file_app_proxyman_command_command_proto_rawDescGZIP(), []int{6} +} + +func (x *AlterInboundRequest) GetTag() string { + if x != nil { + return x.Tag + } + return "" +} + +func (x *AlterInboundRequest) GetOperation() *serial.TypedMessage { + if x != nil { + return x.Operation + } + return nil +} + +type AlterInboundResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AlterInboundResponse) Reset() { + *x = AlterInboundResponse{} + mi := &file_app_proxyman_command_command_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AlterInboundResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AlterInboundResponse) ProtoMessage() {} + +func (x *AlterInboundResponse) ProtoReflect() protoreflect.Message { + mi := &file_app_proxyman_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 AlterInboundResponse.ProtoReflect.Descriptor instead. +func (*AlterInboundResponse) Descriptor() ([]byte, []int) { + return file_app_proxyman_command_command_proto_rawDescGZIP(), []int{7} +} + +type ListInboundsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + IsOnlyTags bool `protobuf:"varint,1,opt,name=isOnlyTags,proto3" json:"isOnlyTags,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListInboundsRequest) Reset() { + *x = ListInboundsRequest{} + mi := &file_app_proxyman_command_command_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListInboundsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListInboundsRequest) ProtoMessage() {} + +func (x *ListInboundsRequest) ProtoReflect() protoreflect.Message { + mi := &file_app_proxyman_command_command_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListInboundsRequest.ProtoReflect.Descriptor instead. +func (*ListInboundsRequest) Descriptor() ([]byte, []int) { + return file_app_proxyman_command_command_proto_rawDescGZIP(), []int{8} +} + +func (x *ListInboundsRequest) GetIsOnlyTags() bool { + if x != nil { + return x.IsOnlyTags + } + return false +} + +type ListInboundsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Inbounds []*core.InboundHandlerConfig `protobuf:"bytes,1,rep,name=inbounds,proto3" json:"inbounds,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListInboundsResponse) Reset() { + *x = ListInboundsResponse{} + mi := &file_app_proxyman_command_command_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListInboundsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListInboundsResponse) ProtoMessage() {} + +func (x *ListInboundsResponse) ProtoReflect() protoreflect.Message { + mi := &file_app_proxyman_command_command_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListInboundsResponse.ProtoReflect.Descriptor instead. +func (*ListInboundsResponse) Descriptor() ([]byte, []int) { + return file_app_proxyman_command_command_proto_rawDescGZIP(), []int{9} +} + +func (x *ListInboundsResponse) GetInbounds() []*core.InboundHandlerConfig { + if x != nil { + return x.Inbounds + } + return nil +} + +type GetInboundUserRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Tag string `protobuf:"bytes,1,opt,name=tag,proto3" json:"tag,omitempty"` + Email string `protobuf:"bytes,2,opt,name=email,proto3" json:"email,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetInboundUserRequest) Reset() { + *x = GetInboundUserRequest{} + mi := &file_app_proxyman_command_command_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetInboundUserRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetInboundUserRequest) ProtoMessage() {} + +func (x *GetInboundUserRequest) ProtoReflect() protoreflect.Message { + mi := &file_app_proxyman_command_command_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetInboundUserRequest.ProtoReflect.Descriptor instead. +func (*GetInboundUserRequest) Descriptor() ([]byte, []int) { + return file_app_proxyman_command_command_proto_rawDescGZIP(), []int{10} +} + +func (x *GetInboundUserRequest) GetTag() string { + if x != nil { + return x.Tag + } + return "" +} + +func (x *GetInboundUserRequest) GetEmail() string { + if x != nil { + return x.Email + } + return "" +} + +type GetInboundUserResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Users []*protocol.User `protobuf:"bytes,1,rep,name=users,proto3" json:"users,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetInboundUserResponse) Reset() { + *x = GetInboundUserResponse{} + mi := &file_app_proxyman_command_command_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetInboundUserResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetInboundUserResponse) ProtoMessage() {} + +func (x *GetInboundUserResponse) ProtoReflect() protoreflect.Message { + mi := &file_app_proxyman_command_command_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetInboundUserResponse.ProtoReflect.Descriptor instead. +func (*GetInboundUserResponse) Descriptor() ([]byte, []int) { + return file_app_proxyman_command_command_proto_rawDescGZIP(), []int{11} +} + +func (x *GetInboundUserResponse) GetUsers() []*protocol.User { + if x != nil { + return x.Users + } + return nil +} + +type GetInboundUsersCountResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Count int64 `protobuf:"varint,1,opt,name=count,proto3" json:"count,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetInboundUsersCountResponse) Reset() { + *x = GetInboundUsersCountResponse{} + mi := &file_app_proxyman_command_command_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetInboundUsersCountResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetInboundUsersCountResponse) ProtoMessage() {} + +func (x *GetInboundUsersCountResponse) ProtoReflect() protoreflect.Message { + mi := &file_app_proxyman_command_command_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetInboundUsersCountResponse.ProtoReflect.Descriptor instead. +func (*GetInboundUsersCountResponse) Descriptor() ([]byte, []int) { + return file_app_proxyman_command_command_proto_rawDescGZIP(), []int{12} +} + +func (x *GetInboundUsersCountResponse) GetCount() int64 { + if x != nil { + return x.Count + } + return 0 +} + +type AddOutboundRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Outbound *core.OutboundHandlerConfig `protobuf:"bytes,1,opt,name=outbound,proto3" json:"outbound,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AddOutboundRequest) Reset() { + *x = AddOutboundRequest{} + mi := &file_app_proxyman_command_command_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AddOutboundRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddOutboundRequest) ProtoMessage() {} + +func (x *AddOutboundRequest) ProtoReflect() protoreflect.Message { + mi := &file_app_proxyman_command_command_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AddOutboundRequest.ProtoReflect.Descriptor instead. +func (*AddOutboundRequest) Descriptor() ([]byte, []int) { + return file_app_proxyman_command_command_proto_rawDescGZIP(), []int{13} +} + +func (x *AddOutboundRequest) GetOutbound() *core.OutboundHandlerConfig { + if x != nil { + return x.Outbound + } + return nil +} + +type AddOutboundResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AddOutboundResponse) Reset() { + *x = AddOutboundResponse{} + mi := &file_app_proxyman_command_command_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AddOutboundResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddOutboundResponse) ProtoMessage() {} + +func (x *AddOutboundResponse) ProtoReflect() protoreflect.Message { + mi := &file_app_proxyman_command_command_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AddOutboundResponse.ProtoReflect.Descriptor instead. +func (*AddOutboundResponse) Descriptor() ([]byte, []int) { + return file_app_proxyman_command_command_proto_rawDescGZIP(), []int{14} +} + +type RemoveOutboundRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Tag string `protobuf:"bytes,1,opt,name=tag,proto3" json:"tag,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RemoveOutboundRequest) Reset() { + *x = RemoveOutboundRequest{} + mi := &file_app_proxyman_command_command_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RemoveOutboundRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RemoveOutboundRequest) ProtoMessage() {} + +func (x *RemoveOutboundRequest) ProtoReflect() protoreflect.Message { + mi := &file_app_proxyman_command_command_proto_msgTypes[15] + 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 RemoveOutboundRequest.ProtoReflect.Descriptor instead. +func (*RemoveOutboundRequest) Descriptor() ([]byte, []int) { + return file_app_proxyman_command_command_proto_rawDescGZIP(), []int{15} +} + +func (x *RemoveOutboundRequest) GetTag() string { + if x != nil { + return x.Tag + } + return "" +} + +type RemoveOutboundResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RemoveOutboundResponse) Reset() { + *x = RemoveOutboundResponse{} + mi := &file_app_proxyman_command_command_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RemoveOutboundResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RemoveOutboundResponse) ProtoMessage() {} + +func (x *RemoveOutboundResponse) ProtoReflect() protoreflect.Message { + mi := &file_app_proxyman_command_command_proto_msgTypes[16] + 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 RemoveOutboundResponse.ProtoReflect.Descriptor instead. +func (*RemoveOutboundResponse) Descriptor() ([]byte, []int) { + return file_app_proxyman_command_command_proto_rawDescGZIP(), []int{16} +} + +type AlterOutboundRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Tag string `protobuf:"bytes,1,opt,name=tag,proto3" json:"tag,omitempty"` + Operation *serial.TypedMessage `protobuf:"bytes,2,opt,name=operation,proto3" json:"operation,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AlterOutboundRequest) Reset() { + *x = AlterOutboundRequest{} + mi := &file_app_proxyman_command_command_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AlterOutboundRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AlterOutboundRequest) ProtoMessage() {} + +func (x *AlterOutboundRequest) ProtoReflect() protoreflect.Message { + mi := &file_app_proxyman_command_command_proto_msgTypes[17] + 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 AlterOutboundRequest.ProtoReflect.Descriptor instead. +func (*AlterOutboundRequest) Descriptor() ([]byte, []int) { + return file_app_proxyman_command_command_proto_rawDescGZIP(), []int{17} +} + +func (x *AlterOutboundRequest) GetTag() string { + if x != nil { + return x.Tag + } + return "" +} + +func (x *AlterOutboundRequest) GetOperation() *serial.TypedMessage { + if x != nil { + return x.Operation + } + return nil +} + +type AlterOutboundResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AlterOutboundResponse) Reset() { + *x = AlterOutboundResponse{} + mi := &file_app_proxyman_command_command_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AlterOutboundResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AlterOutboundResponse) ProtoMessage() {} + +func (x *AlterOutboundResponse) ProtoReflect() protoreflect.Message { + mi := &file_app_proxyman_command_command_proto_msgTypes[18] + 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 AlterOutboundResponse.ProtoReflect.Descriptor instead. +func (*AlterOutboundResponse) Descriptor() ([]byte, []int) { + return file_app_proxyman_command_command_proto_rawDescGZIP(), []int{18} +} + +type ListOutboundsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListOutboundsRequest) Reset() { + *x = ListOutboundsRequest{} + mi := &file_app_proxyman_command_command_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListOutboundsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListOutboundsRequest) ProtoMessage() {} + +func (x *ListOutboundsRequest) ProtoReflect() protoreflect.Message { + mi := &file_app_proxyman_command_command_proto_msgTypes[19] + 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 ListOutboundsRequest.ProtoReflect.Descriptor instead. +func (*ListOutboundsRequest) Descriptor() ([]byte, []int) { + return file_app_proxyman_command_command_proto_rawDescGZIP(), []int{19} +} + +type ListOutboundsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Outbounds []*core.OutboundHandlerConfig `protobuf:"bytes,1,rep,name=outbounds,proto3" json:"outbounds,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListOutboundsResponse) Reset() { + *x = ListOutboundsResponse{} + mi := &file_app_proxyman_command_command_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListOutboundsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListOutboundsResponse) ProtoMessage() {} + +func (x *ListOutboundsResponse) ProtoReflect() protoreflect.Message { + mi := &file_app_proxyman_command_command_proto_msgTypes[20] + 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 ListOutboundsResponse.ProtoReflect.Descriptor instead. +func (*ListOutboundsResponse) Descriptor() ([]byte, []int) { + return file_app_proxyman_command_command_proto_rawDescGZIP(), []int{20} +} + +func (x *ListOutboundsResponse) GetOutbounds() []*core.OutboundHandlerConfig { + if x != nil { + return x.Outbounds + } + return nil +} + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_app_proxyman_command_command_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_app_proxyman_command_command_proto_msgTypes[21] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_app_proxyman_command_command_proto_rawDescGZIP(), []int{21} +} + +var File_app_proxyman_command_command_proto protoreflect.FileDescriptor + +const file_app_proxyman_command_command_proto_rawDesc = "" + + "\n" + + "\"app/proxyman/command/command.proto\x12\x19xray.app.proxyman.command\x1a\x1acommon/protocol/user.proto\x1a!common/serial/typed_message.proto\x1a\x11core/config.proto\"B\n" + + "\x10AddUserOperation\x12.\n" + + "\x04user\x18\x01 \x01(\v2\x1a.xray.common.protocol.UserR\x04user\"+\n" + + "\x13RemoveUserOperation\x12\x14\n" + + "\x05email\x18\x01 \x01(\tR\x05email\"N\n" + + "\x11AddInboundRequest\x129\n" + + "\ainbound\x18\x01 \x01(\v2\x1f.xray.core.InboundHandlerConfigR\ainbound\"\x14\n" + + "\x12AddInboundResponse\"(\n" + + "\x14RemoveInboundRequest\x12\x10\n" + + "\x03tag\x18\x01 \x01(\tR\x03tag\"\x17\n" + + "\x15RemoveInboundResponse\"g\n" + + "\x13AlterInboundRequest\x12\x10\n" + + "\x03tag\x18\x01 \x01(\tR\x03tag\x12>\n" + + "\toperation\x18\x02 \x01(\v2 .xray.common.serial.TypedMessageR\toperation\"\x16\n" + + "\x14AlterInboundResponse\"5\n" + + "\x13ListInboundsRequest\x12\x1e\n" + + "\n" + + "isOnlyTags\x18\x01 \x01(\bR\n" + + "isOnlyTags\"S\n" + + "\x14ListInboundsResponse\x12;\n" + + "\binbounds\x18\x01 \x03(\v2\x1f.xray.core.InboundHandlerConfigR\binbounds\"?\n" + + "\x15GetInboundUserRequest\x12\x10\n" + + "\x03tag\x18\x01 \x01(\tR\x03tag\x12\x14\n" + + "\x05email\x18\x02 \x01(\tR\x05email\"J\n" + + "\x16GetInboundUserResponse\x120\n" + + "\x05users\x18\x01 \x03(\v2\x1a.xray.common.protocol.UserR\x05users\"4\n" + + "\x1cGetInboundUsersCountResponse\x12\x14\n" + + "\x05count\x18\x01 \x01(\x03R\x05count\"R\n" + + "\x12AddOutboundRequest\x12<\n" + + "\boutbound\x18\x01 \x01(\v2 .xray.core.OutboundHandlerConfigR\boutbound\"\x15\n" + + "\x13AddOutboundResponse\")\n" + + "\x15RemoveOutboundRequest\x12\x10\n" + + "\x03tag\x18\x01 \x01(\tR\x03tag\"\x18\n" + + "\x16RemoveOutboundResponse\"h\n" + + "\x14AlterOutboundRequest\x12\x10\n" + + "\x03tag\x18\x01 \x01(\tR\x03tag\x12>\n" + + "\toperation\x18\x02 \x01(\v2 .xray.common.serial.TypedMessageR\toperation\"\x17\n" + + "\x15AlterOutboundResponse\"\x16\n" + + "\x14ListOutboundsRequest\"W\n" + + "\x15ListOutboundsResponse\x12>\n" + + "\toutbounds\x18\x01 \x03(\v2 .xray.core.OutboundHandlerConfigR\toutbounds\"\b\n" + + "\x06Config2\xae\t\n" + + "\x0eHandlerService\x12k\n" + + "\n" + + "AddInbound\x12,.xray.app.proxyman.command.AddInboundRequest\x1a-.xray.app.proxyman.command.AddInboundResponse\"\x00\x12t\n" + + "\rRemoveInbound\x12/.xray.app.proxyman.command.RemoveInboundRequest\x1a0.xray.app.proxyman.command.RemoveInboundResponse\"\x00\x12q\n" + + "\fAlterInbound\x12..xray.app.proxyman.command.AlterInboundRequest\x1a/.xray.app.proxyman.command.AlterInboundResponse\"\x00\x12q\n" + + "\fListInbounds\x12..xray.app.proxyman.command.ListInboundsRequest\x1a/.xray.app.proxyman.command.ListInboundsResponse\"\x00\x12x\n" + + "\x0fGetInboundUsers\x120.xray.app.proxyman.command.GetInboundUserRequest\x1a1.xray.app.proxyman.command.GetInboundUserResponse\"\x00\x12\x83\x01\n" + + "\x14GetInboundUsersCount\x120.xray.app.proxyman.command.GetInboundUserRequest\x1a7.xray.app.proxyman.command.GetInboundUsersCountResponse\"\x00\x12n\n" + + "\vAddOutbound\x12-.xray.app.proxyman.command.AddOutboundRequest\x1a..xray.app.proxyman.command.AddOutboundResponse\"\x00\x12w\n" + + "\x0eRemoveOutbound\x120.xray.app.proxyman.command.RemoveOutboundRequest\x1a1.xray.app.proxyman.command.RemoveOutboundResponse\"\x00\x12t\n" + + "\rAlterOutbound\x12/.xray.app.proxyman.command.AlterOutboundRequest\x1a0.xray.app.proxyman.command.AlterOutboundResponse\"\x00\x12t\n" + + "\rListOutbounds\x12/.xray.app.proxyman.command.ListOutboundsRequest\x1a0.xray.app.proxyman.command.ListOutboundsResponse\"\x00Bm\n" + + "\x1dcom.xray.app.proxyman.commandP\x01Z.github.com/xtls/xray-core/app/proxyman/command\xaa\x02\x19Xray.App.Proxyman.Commandb\x06proto3" + +var ( + file_app_proxyman_command_command_proto_rawDescOnce sync.Once + file_app_proxyman_command_command_proto_rawDescData []byte +) + +func file_app_proxyman_command_command_proto_rawDescGZIP() []byte { + file_app_proxyman_command_command_proto_rawDescOnce.Do(func() { + file_app_proxyman_command_command_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_app_proxyman_command_command_proto_rawDesc), len(file_app_proxyman_command_command_proto_rawDesc))) + }) + return file_app_proxyman_command_command_proto_rawDescData +} + +var file_app_proxyman_command_command_proto_msgTypes = make([]protoimpl.MessageInfo, 22) +var file_app_proxyman_command_command_proto_goTypes = []any{ + (*AddUserOperation)(nil), // 0: xray.app.proxyman.command.AddUserOperation + (*RemoveUserOperation)(nil), // 1: xray.app.proxyman.command.RemoveUserOperation + (*AddInboundRequest)(nil), // 2: xray.app.proxyman.command.AddInboundRequest + (*AddInboundResponse)(nil), // 3: xray.app.proxyman.command.AddInboundResponse + (*RemoveInboundRequest)(nil), // 4: xray.app.proxyman.command.RemoveInboundRequest + (*RemoveInboundResponse)(nil), // 5: xray.app.proxyman.command.RemoveInboundResponse + (*AlterInboundRequest)(nil), // 6: xray.app.proxyman.command.AlterInboundRequest + (*AlterInboundResponse)(nil), // 7: xray.app.proxyman.command.AlterInboundResponse + (*ListInboundsRequest)(nil), // 8: xray.app.proxyman.command.ListInboundsRequest + (*ListInboundsResponse)(nil), // 9: xray.app.proxyman.command.ListInboundsResponse + (*GetInboundUserRequest)(nil), // 10: xray.app.proxyman.command.GetInboundUserRequest + (*GetInboundUserResponse)(nil), // 11: xray.app.proxyman.command.GetInboundUserResponse + (*GetInboundUsersCountResponse)(nil), // 12: xray.app.proxyman.command.GetInboundUsersCountResponse + (*AddOutboundRequest)(nil), // 13: xray.app.proxyman.command.AddOutboundRequest + (*AddOutboundResponse)(nil), // 14: xray.app.proxyman.command.AddOutboundResponse + (*RemoveOutboundRequest)(nil), // 15: xray.app.proxyman.command.RemoveOutboundRequest + (*RemoveOutboundResponse)(nil), // 16: xray.app.proxyman.command.RemoveOutboundResponse + (*AlterOutboundRequest)(nil), // 17: xray.app.proxyman.command.AlterOutboundRequest + (*AlterOutboundResponse)(nil), // 18: xray.app.proxyman.command.AlterOutboundResponse + (*ListOutboundsRequest)(nil), // 19: xray.app.proxyman.command.ListOutboundsRequest + (*ListOutboundsResponse)(nil), // 20: xray.app.proxyman.command.ListOutboundsResponse + (*Config)(nil), // 21: xray.app.proxyman.command.Config + (*protocol.User)(nil), // 22: xray.common.protocol.User + (*core.InboundHandlerConfig)(nil), // 23: xray.core.InboundHandlerConfig + (*serial.TypedMessage)(nil), // 24: xray.common.serial.TypedMessage + (*core.OutboundHandlerConfig)(nil), // 25: xray.core.OutboundHandlerConfig +} +var file_app_proxyman_command_command_proto_depIdxs = []int32{ + 22, // 0: xray.app.proxyman.command.AddUserOperation.user:type_name -> xray.common.protocol.User + 23, // 1: xray.app.proxyman.command.AddInboundRequest.inbound:type_name -> xray.core.InboundHandlerConfig + 24, // 2: xray.app.proxyman.command.AlterInboundRequest.operation:type_name -> xray.common.serial.TypedMessage + 23, // 3: xray.app.proxyman.command.ListInboundsResponse.inbounds:type_name -> xray.core.InboundHandlerConfig + 22, // 4: xray.app.proxyman.command.GetInboundUserResponse.users:type_name -> xray.common.protocol.User + 25, // 5: xray.app.proxyman.command.AddOutboundRequest.outbound:type_name -> xray.core.OutboundHandlerConfig + 24, // 6: xray.app.proxyman.command.AlterOutboundRequest.operation:type_name -> xray.common.serial.TypedMessage + 25, // 7: xray.app.proxyman.command.ListOutboundsResponse.outbounds:type_name -> xray.core.OutboundHandlerConfig + 2, // 8: xray.app.proxyman.command.HandlerService.AddInbound:input_type -> xray.app.proxyman.command.AddInboundRequest + 4, // 9: xray.app.proxyman.command.HandlerService.RemoveInbound:input_type -> xray.app.proxyman.command.RemoveInboundRequest + 6, // 10: xray.app.proxyman.command.HandlerService.AlterInbound:input_type -> xray.app.proxyman.command.AlterInboundRequest + 8, // 11: xray.app.proxyman.command.HandlerService.ListInbounds:input_type -> xray.app.proxyman.command.ListInboundsRequest + 10, // 12: xray.app.proxyman.command.HandlerService.GetInboundUsers:input_type -> xray.app.proxyman.command.GetInboundUserRequest + 10, // 13: xray.app.proxyman.command.HandlerService.GetInboundUsersCount:input_type -> xray.app.proxyman.command.GetInboundUserRequest + 13, // 14: xray.app.proxyman.command.HandlerService.AddOutbound:input_type -> xray.app.proxyman.command.AddOutboundRequest + 15, // 15: xray.app.proxyman.command.HandlerService.RemoveOutbound:input_type -> xray.app.proxyman.command.RemoveOutboundRequest + 17, // 16: xray.app.proxyman.command.HandlerService.AlterOutbound:input_type -> xray.app.proxyman.command.AlterOutboundRequest + 19, // 17: xray.app.proxyman.command.HandlerService.ListOutbounds:input_type -> xray.app.proxyman.command.ListOutboundsRequest + 3, // 18: xray.app.proxyman.command.HandlerService.AddInbound:output_type -> xray.app.proxyman.command.AddInboundResponse + 5, // 19: xray.app.proxyman.command.HandlerService.RemoveInbound:output_type -> xray.app.proxyman.command.RemoveInboundResponse + 7, // 20: xray.app.proxyman.command.HandlerService.AlterInbound:output_type -> xray.app.proxyman.command.AlterInboundResponse + 9, // 21: xray.app.proxyman.command.HandlerService.ListInbounds:output_type -> xray.app.proxyman.command.ListInboundsResponse + 11, // 22: xray.app.proxyman.command.HandlerService.GetInboundUsers:output_type -> xray.app.proxyman.command.GetInboundUserResponse + 12, // 23: xray.app.proxyman.command.HandlerService.GetInboundUsersCount:output_type -> xray.app.proxyman.command.GetInboundUsersCountResponse + 14, // 24: xray.app.proxyman.command.HandlerService.AddOutbound:output_type -> xray.app.proxyman.command.AddOutboundResponse + 16, // 25: xray.app.proxyman.command.HandlerService.RemoveOutbound:output_type -> xray.app.proxyman.command.RemoveOutboundResponse + 18, // 26: xray.app.proxyman.command.HandlerService.AlterOutbound:output_type -> xray.app.proxyman.command.AlterOutboundResponse + 20, // 27: xray.app.proxyman.command.HandlerService.ListOutbounds:output_type -> xray.app.proxyman.command.ListOutboundsResponse + 18, // [18:28] is the sub-list for method output_type + 8, // [8:18] is the sub-list for method input_type + 8, // [8:8] is the sub-list for extension type_name + 8, // [8:8] is the sub-list for extension extendee + 0, // [0:8] is the sub-list for field type_name +} + +func init() { file_app_proxyman_command_command_proto_init() } +func file_app_proxyman_command_command_proto_init() { + if File_app_proxyman_command_command_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_app_proxyman_command_command_proto_rawDesc), len(file_app_proxyman_command_command_proto_rawDesc)), + NumEnums: 0, + NumMessages: 22, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_app_proxyman_command_command_proto_goTypes, + DependencyIndexes: file_app_proxyman_command_command_proto_depIdxs, + MessageInfos: file_app_proxyman_command_command_proto_msgTypes, + }.Build() + File_app_proxyman_command_command_proto = out.File + file_app_proxyman_command_command_proto_goTypes = nil + file_app_proxyman_command_command_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/app/proxyman/command/command.proto b/subproject/Xray-core-main/app/proxyman/command/command.proto new file mode 100644 index 00000000..71f8f0dc --- /dev/null +++ b/subproject/Xray-core-main/app/proxyman/command/command.proto @@ -0,0 +1,108 @@ +syntax = "proto3"; + +package xray.app.proxyman.command; +option csharp_namespace = "Xray.App.Proxyman.Command"; +option go_package = "github.com/xtls/xray-core/app/proxyman/command"; +option java_package = "com.xray.app.proxyman.command"; +option java_multiple_files = true; + +import "common/protocol/user.proto"; +import "common/serial/typed_message.proto"; +import "core/config.proto"; + +message AddUserOperation { + xray.common.protocol.User user = 1; +} + +message RemoveUserOperation { + string email = 1; +} + +message AddInboundRequest { + core.InboundHandlerConfig inbound = 1; +} + +message AddInboundResponse {} + +message RemoveInboundRequest { + string tag = 1; +} + +message RemoveInboundResponse {} + +message AlterInboundRequest { + string tag = 1; + xray.common.serial.TypedMessage operation = 2; +} + +message AlterInboundResponse {} + +message ListInboundsRequest { + bool isOnlyTags = 1; +} + +message ListInboundsResponse { + repeated core.InboundHandlerConfig inbounds = 1; +} + +message GetInboundUserRequest { + string tag = 1; + string email = 2; +} + +message GetInboundUserResponse { + repeated xray.common.protocol.User users = 1; +} + +message GetInboundUsersCountResponse { + int64 count = 1; +} + +message AddOutboundRequest { + core.OutboundHandlerConfig outbound = 1; +} + +message AddOutboundResponse {} + +message RemoveOutboundRequest { + string tag = 1; +} + +message RemoveOutboundResponse {} + +message AlterOutboundRequest { + string tag = 1; + xray.common.serial.TypedMessage operation = 2; +} + +message AlterOutboundResponse {} + +message ListOutboundsRequest {} + +message ListOutboundsResponse { + repeated core.OutboundHandlerConfig outbounds = 1; +} + +service HandlerService { + rpc AddInbound(AddInboundRequest) returns (AddInboundResponse) {} + + rpc RemoveInbound(RemoveInboundRequest) returns (RemoveInboundResponse) {} + + rpc AlterInbound(AlterInboundRequest) returns (AlterInboundResponse) {} + + rpc ListInbounds(ListInboundsRequest) returns (ListInboundsResponse) {} + + rpc GetInboundUsers(GetInboundUserRequest) returns (GetInboundUserResponse) {} + + rpc GetInboundUsersCount(GetInboundUserRequest) returns (GetInboundUsersCountResponse) {} + + rpc AddOutbound(AddOutboundRequest) returns (AddOutboundResponse) {} + + rpc RemoveOutbound(RemoveOutboundRequest) returns (RemoveOutboundResponse) {} + + rpc AlterOutbound(AlterOutboundRequest) returns (AlterOutboundResponse) {} + + rpc ListOutbounds(ListOutboundsRequest) returns (ListOutboundsResponse) {} +} + +message Config {} diff --git a/subproject/Xray-core-main/app/proxyman/command/command_grpc.pb.go b/subproject/Xray-core-main/app/proxyman/command/command_grpc.pb.go new file mode 100644 index 00000000..3d9a9e21 --- /dev/null +++ b/subproject/Xray-core-main/app/proxyman/command/command_grpc.pb.go @@ -0,0 +1,463 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.0 +// - protoc v6.33.5 +// source: app/proxyman/command/command.proto + +package command + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + HandlerService_AddInbound_FullMethodName = "/xray.app.proxyman.command.HandlerService/AddInbound" + HandlerService_RemoveInbound_FullMethodName = "/xray.app.proxyman.command.HandlerService/RemoveInbound" + HandlerService_AlterInbound_FullMethodName = "/xray.app.proxyman.command.HandlerService/AlterInbound" + HandlerService_ListInbounds_FullMethodName = "/xray.app.proxyman.command.HandlerService/ListInbounds" + HandlerService_GetInboundUsers_FullMethodName = "/xray.app.proxyman.command.HandlerService/GetInboundUsers" + HandlerService_GetInboundUsersCount_FullMethodName = "/xray.app.proxyman.command.HandlerService/GetInboundUsersCount" + HandlerService_AddOutbound_FullMethodName = "/xray.app.proxyman.command.HandlerService/AddOutbound" + HandlerService_RemoveOutbound_FullMethodName = "/xray.app.proxyman.command.HandlerService/RemoveOutbound" + HandlerService_AlterOutbound_FullMethodName = "/xray.app.proxyman.command.HandlerService/AlterOutbound" + HandlerService_ListOutbounds_FullMethodName = "/xray.app.proxyman.command.HandlerService/ListOutbounds" +) + +// HandlerServiceClient is the client API for HandlerService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type HandlerServiceClient interface { + AddInbound(ctx context.Context, in *AddInboundRequest, opts ...grpc.CallOption) (*AddInboundResponse, error) + RemoveInbound(ctx context.Context, in *RemoveInboundRequest, opts ...grpc.CallOption) (*RemoveInboundResponse, error) + AlterInbound(ctx context.Context, in *AlterInboundRequest, opts ...grpc.CallOption) (*AlterInboundResponse, error) + ListInbounds(ctx context.Context, in *ListInboundsRequest, opts ...grpc.CallOption) (*ListInboundsResponse, error) + GetInboundUsers(ctx context.Context, in *GetInboundUserRequest, opts ...grpc.CallOption) (*GetInboundUserResponse, error) + GetInboundUsersCount(ctx context.Context, in *GetInboundUserRequest, opts ...grpc.CallOption) (*GetInboundUsersCountResponse, error) + AddOutbound(ctx context.Context, in *AddOutboundRequest, opts ...grpc.CallOption) (*AddOutboundResponse, error) + RemoveOutbound(ctx context.Context, in *RemoveOutboundRequest, opts ...grpc.CallOption) (*RemoveOutboundResponse, error) + AlterOutbound(ctx context.Context, in *AlterOutboundRequest, opts ...grpc.CallOption) (*AlterOutboundResponse, error) + ListOutbounds(ctx context.Context, in *ListOutboundsRequest, opts ...grpc.CallOption) (*ListOutboundsResponse, error) +} + +type handlerServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewHandlerServiceClient(cc grpc.ClientConnInterface) HandlerServiceClient { + return &handlerServiceClient{cc} +} + +func (c *handlerServiceClient) AddInbound(ctx context.Context, in *AddInboundRequest, opts ...grpc.CallOption) (*AddInboundResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(AddInboundResponse) + err := c.cc.Invoke(ctx, HandlerService_AddInbound_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *handlerServiceClient) RemoveInbound(ctx context.Context, in *RemoveInboundRequest, opts ...grpc.CallOption) (*RemoveInboundResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(RemoveInboundResponse) + err := c.cc.Invoke(ctx, HandlerService_RemoveInbound_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *handlerServiceClient) AlterInbound(ctx context.Context, in *AlterInboundRequest, opts ...grpc.CallOption) (*AlterInboundResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(AlterInboundResponse) + err := c.cc.Invoke(ctx, HandlerService_AlterInbound_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *handlerServiceClient) ListInbounds(ctx context.Context, in *ListInboundsRequest, opts ...grpc.CallOption) (*ListInboundsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListInboundsResponse) + err := c.cc.Invoke(ctx, HandlerService_ListInbounds_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *handlerServiceClient) GetInboundUsers(ctx context.Context, in *GetInboundUserRequest, opts ...grpc.CallOption) (*GetInboundUserResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetInboundUserResponse) + err := c.cc.Invoke(ctx, HandlerService_GetInboundUsers_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *handlerServiceClient) GetInboundUsersCount(ctx context.Context, in *GetInboundUserRequest, opts ...grpc.CallOption) (*GetInboundUsersCountResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetInboundUsersCountResponse) + err := c.cc.Invoke(ctx, HandlerService_GetInboundUsersCount_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *handlerServiceClient) AddOutbound(ctx context.Context, in *AddOutboundRequest, opts ...grpc.CallOption) (*AddOutboundResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(AddOutboundResponse) + err := c.cc.Invoke(ctx, HandlerService_AddOutbound_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *handlerServiceClient) RemoveOutbound(ctx context.Context, in *RemoveOutboundRequest, opts ...grpc.CallOption) (*RemoveOutboundResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(RemoveOutboundResponse) + err := c.cc.Invoke(ctx, HandlerService_RemoveOutbound_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *handlerServiceClient) AlterOutbound(ctx context.Context, in *AlterOutboundRequest, opts ...grpc.CallOption) (*AlterOutboundResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(AlterOutboundResponse) + err := c.cc.Invoke(ctx, HandlerService_AlterOutbound_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *handlerServiceClient) ListOutbounds(ctx context.Context, in *ListOutboundsRequest, opts ...grpc.CallOption) (*ListOutboundsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListOutboundsResponse) + err := c.cc.Invoke(ctx, HandlerService_ListOutbounds_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// HandlerServiceServer is the server API for HandlerService service. +// All implementations must embed UnimplementedHandlerServiceServer +// for forward compatibility. +type HandlerServiceServer interface { + AddInbound(context.Context, *AddInboundRequest) (*AddInboundResponse, error) + RemoveInbound(context.Context, *RemoveInboundRequest) (*RemoveInboundResponse, error) + AlterInbound(context.Context, *AlterInboundRequest) (*AlterInboundResponse, error) + ListInbounds(context.Context, *ListInboundsRequest) (*ListInboundsResponse, error) + GetInboundUsers(context.Context, *GetInboundUserRequest) (*GetInboundUserResponse, error) + GetInboundUsersCount(context.Context, *GetInboundUserRequest) (*GetInboundUsersCountResponse, error) + AddOutbound(context.Context, *AddOutboundRequest) (*AddOutboundResponse, error) + RemoveOutbound(context.Context, *RemoveOutboundRequest) (*RemoveOutboundResponse, error) + AlterOutbound(context.Context, *AlterOutboundRequest) (*AlterOutboundResponse, error) + ListOutbounds(context.Context, *ListOutboundsRequest) (*ListOutboundsResponse, error) + mustEmbedUnimplementedHandlerServiceServer() +} + +// UnimplementedHandlerServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedHandlerServiceServer struct{} + +func (UnimplementedHandlerServiceServer) AddInbound(context.Context, *AddInboundRequest) (*AddInboundResponse, error) { + return nil, status.Error(codes.Unimplemented, "method AddInbound not implemented") +} +func (UnimplementedHandlerServiceServer) RemoveInbound(context.Context, *RemoveInboundRequest) (*RemoveInboundResponse, error) { + return nil, status.Error(codes.Unimplemented, "method RemoveInbound not implemented") +} +func (UnimplementedHandlerServiceServer) AlterInbound(context.Context, *AlterInboundRequest) (*AlterInboundResponse, error) { + return nil, status.Error(codes.Unimplemented, "method AlterInbound not implemented") +} +func (UnimplementedHandlerServiceServer) ListInbounds(context.Context, *ListInboundsRequest) (*ListInboundsResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListInbounds not implemented") +} +func (UnimplementedHandlerServiceServer) GetInboundUsers(context.Context, *GetInboundUserRequest) (*GetInboundUserResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetInboundUsers not implemented") +} +func (UnimplementedHandlerServiceServer) GetInboundUsersCount(context.Context, *GetInboundUserRequest) (*GetInboundUsersCountResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetInboundUsersCount not implemented") +} +func (UnimplementedHandlerServiceServer) AddOutbound(context.Context, *AddOutboundRequest) (*AddOutboundResponse, error) { + return nil, status.Error(codes.Unimplemented, "method AddOutbound not implemented") +} +func (UnimplementedHandlerServiceServer) RemoveOutbound(context.Context, *RemoveOutboundRequest) (*RemoveOutboundResponse, error) { + return nil, status.Error(codes.Unimplemented, "method RemoveOutbound not implemented") +} +func (UnimplementedHandlerServiceServer) AlterOutbound(context.Context, *AlterOutboundRequest) (*AlterOutboundResponse, error) { + return nil, status.Error(codes.Unimplemented, "method AlterOutbound not implemented") +} +func (UnimplementedHandlerServiceServer) ListOutbounds(context.Context, *ListOutboundsRequest) (*ListOutboundsResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListOutbounds not implemented") +} +func (UnimplementedHandlerServiceServer) mustEmbedUnimplementedHandlerServiceServer() {} +func (UnimplementedHandlerServiceServer) testEmbeddedByValue() {} + +// UnsafeHandlerServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to HandlerServiceServer will +// result in compilation errors. +type UnsafeHandlerServiceServer interface { + mustEmbedUnimplementedHandlerServiceServer() +} + +func RegisterHandlerServiceServer(s grpc.ServiceRegistrar, srv HandlerServiceServer) { + // If the following call panics, it indicates UnimplementedHandlerServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&HandlerService_ServiceDesc, srv) +} + +func _HandlerService_AddInbound_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AddInboundRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(HandlerServiceServer).AddInbound(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: HandlerService_AddInbound_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(HandlerServiceServer).AddInbound(ctx, req.(*AddInboundRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _HandlerService_RemoveInbound_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RemoveInboundRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(HandlerServiceServer).RemoveInbound(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: HandlerService_RemoveInbound_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(HandlerServiceServer).RemoveInbound(ctx, req.(*RemoveInboundRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _HandlerService_AlterInbound_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AlterInboundRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(HandlerServiceServer).AlterInbound(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: HandlerService_AlterInbound_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(HandlerServiceServer).AlterInbound(ctx, req.(*AlterInboundRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _HandlerService_ListInbounds_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListInboundsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(HandlerServiceServer).ListInbounds(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: HandlerService_ListInbounds_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(HandlerServiceServer).ListInbounds(ctx, req.(*ListInboundsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _HandlerService_GetInboundUsers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetInboundUserRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(HandlerServiceServer).GetInboundUsers(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: HandlerService_GetInboundUsers_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(HandlerServiceServer).GetInboundUsers(ctx, req.(*GetInboundUserRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _HandlerService_GetInboundUsersCount_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetInboundUserRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(HandlerServiceServer).GetInboundUsersCount(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: HandlerService_GetInboundUsersCount_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(HandlerServiceServer).GetInboundUsersCount(ctx, req.(*GetInboundUserRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _HandlerService_AddOutbound_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AddOutboundRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(HandlerServiceServer).AddOutbound(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: HandlerService_AddOutbound_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(HandlerServiceServer).AddOutbound(ctx, req.(*AddOutboundRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _HandlerService_RemoveOutbound_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RemoveOutboundRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(HandlerServiceServer).RemoveOutbound(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: HandlerService_RemoveOutbound_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(HandlerServiceServer).RemoveOutbound(ctx, req.(*RemoveOutboundRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _HandlerService_AlterOutbound_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AlterOutboundRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(HandlerServiceServer).AlterOutbound(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: HandlerService_AlterOutbound_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(HandlerServiceServer).AlterOutbound(ctx, req.(*AlterOutboundRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _HandlerService_ListOutbounds_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListOutboundsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(HandlerServiceServer).ListOutbounds(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: HandlerService_ListOutbounds_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(HandlerServiceServer).ListOutbounds(ctx, req.(*ListOutboundsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// HandlerService_ServiceDesc is the grpc.ServiceDesc for HandlerService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var HandlerService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "xray.app.proxyman.command.HandlerService", + HandlerType: (*HandlerServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "AddInbound", + Handler: _HandlerService_AddInbound_Handler, + }, + { + MethodName: "RemoveInbound", + Handler: _HandlerService_RemoveInbound_Handler, + }, + { + MethodName: "AlterInbound", + Handler: _HandlerService_AlterInbound_Handler, + }, + { + MethodName: "ListInbounds", + Handler: _HandlerService_ListInbounds_Handler, + }, + { + MethodName: "GetInboundUsers", + Handler: _HandlerService_GetInboundUsers_Handler, + }, + { + MethodName: "GetInboundUsersCount", + Handler: _HandlerService_GetInboundUsersCount_Handler, + }, + { + MethodName: "AddOutbound", + Handler: _HandlerService_AddOutbound_Handler, + }, + { + MethodName: "RemoveOutbound", + Handler: _HandlerService_RemoveOutbound_Handler, + }, + { + MethodName: "AlterOutbound", + Handler: _HandlerService_AlterOutbound_Handler, + }, + { + MethodName: "ListOutbounds", + Handler: _HandlerService_ListOutbounds_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "app/proxyman/command/command.proto", +} diff --git a/subproject/Xray-core-main/app/proxyman/command/doc.go b/subproject/Xray-core-main/app/proxyman/command/doc.go new file mode 100644 index 00000000..d47dcf0d --- /dev/null +++ b/subproject/Xray-core-main/app/proxyman/command/doc.go @@ -0,0 +1 @@ +package command diff --git a/subproject/Xray-core-main/app/proxyman/config.go b/subproject/Xray-core-main/app/proxyman/config.go new file mode 100644 index 00000000..871971aa --- /dev/null +++ b/subproject/Xray-core-main/app/proxyman/config.go @@ -0,0 +1 @@ +package proxyman diff --git a/subproject/Xray-core-main/app/proxyman/config.pb.go b/subproject/Xray-core-main/app/proxyman/config.pb.go new file mode 100644 index 00000000..8b745a95 --- /dev/null +++ b/subproject/Xray-core-main/app/proxyman/config.pb.go @@ -0,0 +1,583 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: app/proxyman/config.proto + +package proxyman + +import ( + net "github.com/xtls/xray-core/common/net" + serial "github.com/xtls/xray-core/common/serial" + internet "github.com/xtls/xray-core/transport/internet" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type InboundConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *InboundConfig) Reset() { + *x = InboundConfig{} + mi := &file_app_proxyman_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *InboundConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InboundConfig) ProtoMessage() {} + +func (x *InboundConfig) ProtoReflect() protoreflect.Message { + mi := &file_app_proxyman_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use InboundConfig.ProtoReflect.Descriptor instead. +func (*InboundConfig) Descriptor() ([]byte, []int) { + return file_app_proxyman_config_proto_rawDescGZIP(), []int{0} +} + +type SniffingConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Whether or not to enable content sniffing on an inbound connection. + Enabled bool `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"` + // Override target destination if sniff'ed protocol is in the given list. + // Supported values are "http", "tls", "fakedns". + DestinationOverride []string `protobuf:"bytes,2,rep,name=destination_override,json=destinationOverride,proto3" json:"destination_override,omitempty"` + DomainsExcluded []string `protobuf:"bytes,3,rep,name=domains_excluded,json=domainsExcluded,proto3" json:"domains_excluded,omitempty"` + // Whether should only try to sniff metadata without waiting for client input. + // Can be used to support SMTP like protocol where server send the first + // message. + MetadataOnly bool `protobuf:"varint,4,opt,name=metadata_only,json=metadataOnly,proto3" json:"metadata_only,omitempty"` + RouteOnly bool `protobuf:"varint,5,opt,name=route_only,json=routeOnly,proto3" json:"route_only,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SniffingConfig) Reset() { + *x = SniffingConfig{} + mi := &file_app_proxyman_config_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SniffingConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SniffingConfig) ProtoMessage() {} + +func (x *SniffingConfig) ProtoReflect() protoreflect.Message { + mi := &file_app_proxyman_config_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SniffingConfig.ProtoReflect.Descriptor instead. +func (*SniffingConfig) Descriptor() ([]byte, []int) { + return file_app_proxyman_config_proto_rawDescGZIP(), []int{1} +} + +func (x *SniffingConfig) GetEnabled() bool { + if x != nil { + return x.Enabled + } + return false +} + +func (x *SniffingConfig) GetDestinationOverride() []string { + if x != nil { + return x.DestinationOverride + } + return nil +} + +func (x *SniffingConfig) GetDomainsExcluded() []string { + if x != nil { + return x.DomainsExcluded + } + return nil +} + +func (x *SniffingConfig) GetMetadataOnly() bool { + if x != nil { + return x.MetadataOnly + } + return false +} + +func (x *SniffingConfig) GetRouteOnly() bool { + if x != nil { + return x.RouteOnly + } + return false +} + +type ReceiverConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + // PortList specifies the ports which the Receiver should listen on. + PortList *net.PortList `protobuf:"bytes,1,opt,name=port_list,json=portList,proto3" json:"port_list,omitempty"` + // Listen specifies the IP address that the Receiver should listen on. + Listen *net.IPOrDomain `protobuf:"bytes,2,opt,name=listen,proto3" json:"listen,omitempty"` + StreamSettings *internet.StreamConfig `protobuf:"bytes,3,opt,name=stream_settings,json=streamSettings,proto3" json:"stream_settings,omitempty"` + ReceiveOriginalDestination bool `protobuf:"varint,4,opt,name=receive_original_destination,json=receiveOriginalDestination,proto3" json:"receive_original_destination,omitempty"` + SniffingSettings *SniffingConfig `protobuf:"bytes,6,opt,name=sniffing_settings,json=sniffingSettings,proto3" json:"sniffing_settings,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReceiverConfig) Reset() { + *x = ReceiverConfig{} + mi := &file_app_proxyman_config_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReceiverConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReceiverConfig) ProtoMessage() {} + +func (x *ReceiverConfig) ProtoReflect() protoreflect.Message { + mi := &file_app_proxyman_config_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ReceiverConfig.ProtoReflect.Descriptor instead. +func (*ReceiverConfig) Descriptor() ([]byte, []int) { + return file_app_proxyman_config_proto_rawDescGZIP(), []int{2} +} + +func (x *ReceiverConfig) GetPortList() *net.PortList { + if x != nil { + return x.PortList + } + return nil +} + +func (x *ReceiverConfig) GetListen() *net.IPOrDomain { + if x != nil { + return x.Listen + } + return nil +} + +func (x *ReceiverConfig) GetStreamSettings() *internet.StreamConfig { + if x != nil { + return x.StreamSettings + } + return nil +} + +func (x *ReceiverConfig) GetReceiveOriginalDestination() bool { + if x != nil { + return x.ReceiveOriginalDestination + } + return false +} + +func (x *ReceiverConfig) GetSniffingSettings() *SniffingConfig { + if x != nil { + return x.SniffingSettings + } + return nil +} + +type InboundHandlerConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + Tag string `protobuf:"bytes,1,opt,name=tag,proto3" json:"tag,omitempty"` + ReceiverSettings *serial.TypedMessage `protobuf:"bytes,2,opt,name=receiver_settings,json=receiverSettings,proto3" json:"receiver_settings,omitempty"` + ProxySettings *serial.TypedMessage `protobuf:"bytes,3,opt,name=proxy_settings,json=proxySettings,proto3" json:"proxy_settings,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *InboundHandlerConfig) Reset() { + *x = InboundHandlerConfig{} + mi := &file_app_proxyman_config_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *InboundHandlerConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InboundHandlerConfig) ProtoMessage() {} + +func (x *InboundHandlerConfig) ProtoReflect() protoreflect.Message { + mi := &file_app_proxyman_config_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use InboundHandlerConfig.ProtoReflect.Descriptor instead. +func (*InboundHandlerConfig) Descriptor() ([]byte, []int) { + return file_app_proxyman_config_proto_rawDescGZIP(), []int{3} +} + +func (x *InboundHandlerConfig) GetTag() string { + if x != nil { + return x.Tag + } + return "" +} + +func (x *InboundHandlerConfig) GetReceiverSettings() *serial.TypedMessage { + if x != nil { + return x.ReceiverSettings + } + return nil +} + +func (x *InboundHandlerConfig) GetProxySettings() *serial.TypedMessage { + if x != nil { + return x.ProxySettings + } + return nil +} + +type OutboundConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OutboundConfig) Reset() { + *x = OutboundConfig{} + mi := &file_app_proxyman_config_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OutboundConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OutboundConfig) ProtoMessage() {} + +func (x *OutboundConfig) ProtoReflect() protoreflect.Message { + mi := &file_app_proxyman_config_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OutboundConfig.ProtoReflect.Descriptor instead. +func (*OutboundConfig) Descriptor() ([]byte, []int) { + return file_app_proxyman_config_proto_rawDescGZIP(), []int{4} +} + +type SenderConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Send traffic through the given IP. Only IP is allowed. + Via *net.IPOrDomain `protobuf:"bytes,1,opt,name=via,proto3" json:"via,omitempty"` + StreamSettings *internet.StreamConfig `protobuf:"bytes,2,opt,name=stream_settings,json=streamSettings,proto3" json:"stream_settings,omitempty"` + ProxySettings *internet.ProxyConfig `protobuf:"bytes,3,opt,name=proxy_settings,json=proxySettings,proto3" json:"proxy_settings,omitempty"` + MultiplexSettings *MultiplexingConfig `protobuf:"bytes,4,opt,name=multiplex_settings,json=multiplexSettings,proto3" json:"multiplex_settings,omitempty"` + ViaCidr string `protobuf:"bytes,5,opt,name=via_cidr,json=viaCidr,proto3" json:"via_cidr,omitempty"` + TargetStrategy internet.DomainStrategy `protobuf:"varint,6,opt,name=target_strategy,json=targetStrategy,proto3,enum=xray.transport.internet.DomainStrategy" json:"target_strategy,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SenderConfig) Reset() { + *x = SenderConfig{} + mi := &file_app_proxyman_config_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SenderConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SenderConfig) ProtoMessage() {} + +func (x *SenderConfig) ProtoReflect() protoreflect.Message { + mi := &file_app_proxyman_config_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SenderConfig.ProtoReflect.Descriptor instead. +func (*SenderConfig) Descriptor() ([]byte, []int) { + return file_app_proxyman_config_proto_rawDescGZIP(), []int{5} +} + +func (x *SenderConfig) GetVia() *net.IPOrDomain { + if x != nil { + return x.Via + } + return nil +} + +func (x *SenderConfig) GetStreamSettings() *internet.StreamConfig { + if x != nil { + return x.StreamSettings + } + return nil +} + +func (x *SenderConfig) GetProxySettings() *internet.ProxyConfig { + if x != nil { + return x.ProxySettings + } + return nil +} + +func (x *SenderConfig) GetMultiplexSettings() *MultiplexingConfig { + if x != nil { + return x.MultiplexSettings + } + return nil +} + +func (x *SenderConfig) GetViaCidr() string { + if x != nil { + return x.ViaCidr + } + return "" +} + +func (x *SenderConfig) GetTargetStrategy() internet.DomainStrategy { + if x != nil { + return x.TargetStrategy + } + return internet.DomainStrategy(0) +} + +type MultiplexingConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Whether or not Mux is enabled. + Enabled bool `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"` + // Max number of concurrent connections that one Mux connection can handle. + Concurrency int32 `protobuf:"varint,2,opt,name=concurrency,proto3" json:"concurrency,omitempty"` + // Transport XUDP in another Mux. + XudpConcurrency int32 `protobuf:"varint,3,opt,name=xudpConcurrency,proto3" json:"xudpConcurrency,omitempty"` + // "reject" (default), "allow" or "skip". + XudpProxyUDP443 string `protobuf:"bytes,4,opt,name=xudpProxyUDP443,proto3" json:"xudpProxyUDP443,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MultiplexingConfig) Reset() { + *x = MultiplexingConfig{} + mi := &file_app_proxyman_config_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MultiplexingConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MultiplexingConfig) ProtoMessage() {} + +func (x *MultiplexingConfig) ProtoReflect() protoreflect.Message { + mi := &file_app_proxyman_config_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MultiplexingConfig.ProtoReflect.Descriptor instead. +func (*MultiplexingConfig) Descriptor() ([]byte, []int) { + return file_app_proxyman_config_proto_rawDescGZIP(), []int{6} +} + +func (x *MultiplexingConfig) GetEnabled() bool { + if x != nil { + return x.Enabled + } + return false +} + +func (x *MultiplexingConfig) GetConcurrency() int32 { + if x != nil { + return x.Concurrency + } + return 0 +} + +func (x *MultiplexingConfig) GetXudpConcurrency() int32 { + if x != nil { + return x.XudpConcurrency + } + return 0 +} + +func (x *MultiplexingConfig) GetXudpProxyUDP443() string { + if x != nil { + return x.XudpProxyUDP443 + } + return "" +} + +var File_app_proxyman_config_proto protoreflect.FileDescriptor + +const file_app_proxyman_config_proto_rawDesc = "" + + "\n" + + "\x19app/proxyman/config.proto\x12\x11xray.app.proxyman\x1a\x18common/net/address.proto\x1a\x15common/net/port.proto\x1a\x1ftransport/internet/config.proto\x1a!common/serial/typed_message.proto\"\x0f\n" + + "\rInboundConfig\"\xcc\x01\n" + + "\x0eSniffingConfig\x12\x18\n" + + "\aenabled\x18\x01 \x01(\bR\aenabled\x121\n" + + "\x14destination_override\x18\x02 \x03(\tR\x13destinationOverride\x12)\n" + + "\x10domains_excluded\x18\x03 \x03(\tR\x0fdomainsExcluded\x12#\n" + + "\rmetadata_only\x18\x04 \x01(\bR\fmetadataOnly\x12\x1d\n" + + "\n" + + "route_only\x18\x05 \x01(\bR\trouteOnly\"\xe5\x02\n" + + "\x0eReceiverConfig\x126\n" + + "\tport_list\x18\x01 \x01(\v2\x19.xray.common.net.PortListR\bportList\x123\n" + + "\x06listen\x18\x02 \x01(\v2\x1b.xray.common.net.IPOrDomainR\x06listen\x12N\n" + + "\x0fstream_settings\x18\x03 \x01(\v2%.xray.transport.internet.StreamConfigR\x0estreamSettings\x12@\n" + + "\x1creceive_original_destination\x18\x04 \x01(\bR\x1areceiveOriginalDestination\x12N\n" + + "\x11sniffing_settings\x18\x06 \x01(\v2!.xray.app.proxyman.SniffingConfigR\x10sniffingSettingsJ\x04\b\x05\x10\x06\"\xc0\x01\n" + + "\x14InboundHandlerConfig\x12\x10\n" + + "\x03tag\x18\x01 \x01(\tR\x03tag\x12M\n" + + "\x11receiver_settings\x18\x02 \x01(\v2 .xray.common.serial.TypedMessageR\x10receiverSettings\x12G\n" + + "\x0eproxy_settings\x18\x03 \x01(\v2 .xray.common.serial.TypedMessageR\rproxySettings\"\x10\n" + + "\x0eOutboundConfig\"\x9d\x03\n" + + "\fSenderConfig\x12-\n" + + "\x03via\x18\x01 \x01(\v2\x1b.xray.common.net.IPOrDomainR\x03via\x12N\n" + + "\x0fstream_settings\x18\x02 \x01(\v2%.xray.transport.internet.StreamConfigR\x0estreamSettings\x12K\n" + + "\x0eproxy_settings\x18\x03 \x01(\v2$.xray.transport.internet.ProxyConfigR\rproxySettings\x12T\n" + + "\x12multiplex_settings\x18\x04 \x01(\v2%.xray.app.proxyman.MultiplexingConfigR\x11multiplexSettings\x12\x19\n" + + "\bvia_cidr\x18\x05 \x01(\tR\aviaCidr\x12P\n" + + "\x0ftarget_strategy\x18\x06 \x01(\x0e2'.xray.transport.internet.DomainStrategyR\x0etargetStrategy\"\xa4\x01\n" + + "\x12MultiplexingConfig\x12\x18\n" + + "\aenabled\x18\x01 \x01(\bR\aenabled\x12 \n" + + "\vconcurrency\x18\x02 \x01(\x05R\vconcurrency\x12(\n" + + "\x0fxudpConcurrency\x18\x03 \x01(\x05R\x0fxudpConcurrency\x12(\n" + + "\x0fxudpProxyUDP443\x18\x04 \x01(\tR\x0fxudpProxyUDP443BU\n" + + "\x15com.xray.app.proxymanP\x01Z&github.com/xtls/xray-core/app/proxyman\xaa\x02\x11Xray.App.Proxymanb\x06proto3" + +var ( + file_app_proxyman_config_proto_rawDescOnce sync.Once + file_app_proxyman_config_proto_rawDescData []byte +) + +func file_app_proxyman_config_proto_rawDescGZIP() []byte { + file_app_proxyman_config_proto_rawDescOnce.Do(func() { + file_app_proxyman_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_app_proxyman_config_proto_rawDesc), len(file_app_proxyman_config_proto_rawDesc))) + }) + return file_app_proxyman_config_proto_rawDescData +} + +var file_app_proxyman_config_proto_msgTypes = make([]protoimpl.MessageInfo, 7) +var file_app_proxyman_config_proto_goTypes = []any{ + (*InboundConfig)(nil), // 0: xray.app.proxyman.InboundConfig + (*SniffingConfig)(nil), // 1: xray.app.proxyman.SniffingConfig + (*ReceiverConfig)(nil), // 2: xray.app.proxyman.ReceiverConfig + (*InboundHandlerConfig)(nil), // 3: xray.app.proxyman.InboundHandlerConfig + (*OutboundConfig)(nil), // 4: xray.app.proxyman.OutboundConfig + (*SenderConfig)(nil), // 5: xray.app.proxyman.SenderConfig + (*MultiplexingConfig)(nil), // 6: xray.app.proxyman.MultiplexingConfig + (*net.PortList)(nil), // 7: xray.common.net.PortList + (*net.IPOrDomain)(nil), // 8: xray.common.net.IPOrDomain + (*internet.StreamConfig)(nil), // 9: xray.transport.internet.StreamConfig + (*serial.TypedMessage)(nil), // 10: xray.common.serial.TypedMessage + (*internet.ProxyConfig)(nil), // 11: xray.transport.internet.ProxyConfig + (internet.DomainStrategy)(0), // 12: xray.transport.internet.DomainStrategy +} +var file_app_proxyman_config_proto_depIdxs = []int32{ + 7, // 0: xray.app.proxyman.ReceiverConfig.port_list:type_name -> xray.common.net.PortList + 8, // 1: xray.app.proxyman.ReceiverConfig.listen:type_name -> xray.common.net.IPOrDomain + 9, // 2: xray.app.proxyman.ReceiverConfig.stream_settings:type_name -> xray.transport.internet.StreamConfig + 1, // 3: xray.app.proxyman.ReceiverConfig.sniffing_settings:type_name -> xray.app.proxyman.SniffingConfig + 10, // 4: xray.app.proxyman.InboundHandlerConfig.receiver_settings:type_name -> xray.common.serial.TypedMessage + 10, // 5: xray.app.proxyman.InboundHandlerConfig.proxy_settings:type_name -> xray.common.serial.TypedMessage + 8, // 6: xray.app.proxyman.SenderConfig.via:type_name -> xray.common.net.IPOrDomain + 9, // 7: xray.app.proxyman.SenderConfig.stream_settings:type_name -> xray.transport.internet.StreamConfig + 11, // 8: xray.app.proxyman.SenderConfig.proxy_settings:type_name -> xray.transport.internet.ProxyConfig + 6, // 9: xray.app.proxyman.SenderConfig.multiplex_settings:type_name -> xray.app.proxyman.MultiplexingConfig + 12, // 10: xray.app.proxyman.SenderConfig.target_strategy:type_name -> xray.transport.internet.DomainStrategy + 11, // [11:11] is the sub-list for method output_type + 11, // [11:11] is the sub-list for method input_type + 11, // [11:11] is the sub-list for extension type_name + 11, // [11:11] is the sub-list for extension extendee + 0, // [0:11] is the sub-list for field type_name +} + +func init() { file_app_proxyman_config_proto_init() } +func file_app_proxyman_config_proto_init() { + if File_app_proxyman_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_app_proxyman_config_proto_rawDesc), len(file_app_proxyman_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 7, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_app_proxyman_config_proto_goTypes, + DependencyIndexes: file_app_proxyman_config_proto_depIdxs, + MessageInfos: file_app_proxyman_config_proto_msgTypes, + }.Build() + File_app_proxyman_config_proto = out.File + file_app_proxyman_config_proto_goTypes = nil + file_app_proxyman_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/app/proxyman/config.proto b/subproject/Xray-core-main/app/proxyman/config.proto new file mode 100644 index 00000000..4f1298b9 --- /dev/null +++ b/subproject/Xray-core-main/app/proxyman/config.proto @@ -0,0 +1,71 @@ +syntax = "proto3"; + +package xray.app.proxyman; +option csharp_namespace = "Xray.App.Proxyman"; +option go_package = "github.com/xtls/xray-core/app/proxyman"; +option java_package = "com.xray.app.proxyman"; +option java_multiple_files = true; + +import "common/net/address.proto"; +import "common/net/port.proto"; +import "transport/internet/config.proto"; +import "common/serial/typed_message.proto"; + +message InboundConfig {} + +message SniffingConfig { + // Whether or not to enable content sniffing on an inbound connection. + bool enabled = 1; + + // Override target destination if sniff'ed protocol is in the given list. + // Supported values are "http", "tls", "fakedns". + repeated string destination_override = 2; + repeated string domains_excluded = 3; + + // Whether should only try to sniff metadata without waiting for client input. + // Can be used to support SMTP like protocol where server send the first + // message. + bool metadata_only = 4; + + bool route_only = 5; +} + +message ReceiverConfig { + // PortList specifies the ports which the Receiver should listen on. + xray.common.net.PortList port_list = 1; + // Listen specifies the IP address that the Receiver should listen on. + xray.common.net.IPOrDomain listen = 2; + xray.transport.internet.StreamConfig stream_settings = 3; + bool receive_original_destination = 4; + reserved 5; + SniffingConfig sniffing_settings = 6; +} + +message InboundHandlerConfig { + string tag = 1; + xray.common.serial.TypedMessage receiver_settings = 2; + xray.common.serial.TypedMessage proxy_settings = 3; +} + +message OutboundConfig {} + +message SenderConfig { + // Send traffic through the given IP. Only IP is allowed. + xray.common.net.IPOrDomain via = 1; + xray.transport.internet.StreamConfig stream_settings = 2; + xray.transport.internet.ProxyConfig proxy_settings = 3; + MultiplexingConfig multiplex_settings = 4; + string via_cidr = 5; + xray.transport.internet.DomainStrategy target_strategy = 6; +} + +message MultiplexingConfig { + // Whether or not Mux is enabled. + bool enabled = 1; + // Max number of concurrent connections that one Mux connection can handle. + int32 concurrency = 2; + // Transport XUDP in another Mux. + int32 xudpConcurrency = 3; + // "reject" (default), "allow" or "skip". + string xudpProxyUDP443 = 4; +} diff --git a/subproject/Xray-core-main/app/proxyman/inbound/always.go b/subproject/Xray-core-main/app/proxyman/inbound/always.go new file mode 100644 index 00000000..a1cb7b7c --- /dev/null +++ b/subproject/Xray-core-main/app/proxyman/inbound/always.go @@ -0,0 +1,214 @@ +package inbound + +import ( + "context" + + "github.com/xtls/xray-core/app/proxyman" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/mux" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/serial" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/features/policy" + "github.com/xtls/xray-core/features/stats" + "github.com/xtls/xray-core/proxy" + "github.com/xtls/xray-core/transport/internet" + "google.golang.org/protobuf/proto" +) + +func getStatCounter(v *core.Instance, tag string) (stats.Counter, stats.Counter) { + var uplinkCounter stats.Counter + var downlinkCounter stats.Counter + + policy := v.GetFeature(policy.ManagerType()).(policy.Manager) + if len(tag) > 0 && policy.ForSystem().Stats.InboundUplink { + statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager) + name := "inbound>>>" + tag + ">>>traffic>>>uplink" + c, _ := stats.GetOrRegisterCounter(statsManager, name) + if c != nil { + uplinkCounter = c + } + } + if len(tag) > 0 && policy.ForSystem().Stats.InboundDownlink { + statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager) + name := "inbound>>>" + tag + ">>>traffic>>>downlink" + c, _ := stats.GetOrRegisterCounter(statsManager, name) + if c != nil { + downlinkCounter = c + } + } + + return uplinkCounter, downlinkCounter +} + +type AlwaysOnInboundHandler struct { + proxyConfig interface{} + receiverConfig *proxyman.ReceiverConfig + proxy proxy.Inbound + workers []worker + mux *mux.Server + tag string +} + +func NewAlwaysOnInboundHandler(ctx context.Context, tag string, receiverConfig *proxyman.ReceiverConfig, proxyConfig interface{}) (*AlwaysOnInboundHandler, error) { + // Set tag and sniffing config in context before creating proxy + // This allows proxies like TUN to access these settings + ctx = session.ContextWithInbound(ctx, &session.Inbound{Tag: tag}) + if receiverConfig.SniffingSettings != nil { + ctx = session.ContextWithContent(ctx, &session.Content{ + SniffingRequest: session.SniffingRequest{ + Enabled: receiverConfig.SniffingSettings.Enabled, + OverrideDestinationForProtocol: receiverConfig.SniffingSettings.DestinationOverride, + ExcludeForDomain: receiverConfig.SniffingSettings.DomainsExcluded, + MetadataOnly: receiverConfig.SniffingSettings.MetadataOnly, + RouteOnly: receiverConfig.SniffingSettings.RouteOnly, + }, + }) + } + rawProxy, err := common.CreateObject(ctx, proxyConfig) + if err != nil { + return nil, err + } + p, ok := rawProxy.(proxy.Inbound) + if !ok { + return nil, errors.New("not an inbound proxy.") + } + + h := &AlwaysOnInboundHandler{ + receiverConfig: receiverConfig, + proxyConfig: proxyConfig, + proxy: p, + mux: mux.NewServer(ctx), + tag: tag, + } + + uplinkCounter, downlinkCounter := getStatCounter(core.MustFromContext(ctx), tag) + + nl := p.Network() + pl := receiverConfig.PortList + address := receiverConfig.Listen.AsAddress() + if address == nil { + address = net.AnyIP + } + + mss, err := internet.ToMemoryStreamConfig(receiverConfig.StreamSettings) + if err != nil { + return nil, errors.New("failed to parse stream config").Base(err).AtWarning() + } + + if receiverConfig.ReceiveOriginalDestination { + if mss.SocketSettings == nil { + mss.SocketSettings = &internet.SocketConfig{} + } + if mss.SocketSettings.Tproxy == internet.SocketConfig_Off { + mss.SocketSettings.Tproxy = internet.SocketConfig_Redirect + } + mss.SocketSettings.ReceiveOriginalDestAddress = true + } + if pl == nil { + if net.HasNetwork(nl, net.Network_UNIX) { + errors.LogDebug(ctx, "creating unix domain socket worker on ", address) + + worker := &dsWorker{ + address: address, + proxy: p, + stream: mss, + tag: tag, + dispatcher: h.mux, + sniffingConfig: receiverConfig.SniffingSettings, + uplinkCounter: uplinkCounter, + downlinkCounter: downlinkCounter, + ctx: ctx, + } + h.workers = append(h.workers, worker) + } + } + if pl != nil { + for _, pr := range pl.Range { + for port := pr.From; port <= pr.To; port++ { + if net.HasNetwork(nl, net.Network_TCP) { + errors.LogDebug(ctx, "creating stream worker on ", address, ":", port) + + worker := &tcpWorker{ + address: address, + port: net.Port(port), + proxy: p, + stream: mss, + recvOrigDest: receiverConfig.ReceiveOriginalDestination, + tag: tag, + dispatcher: h.mux, + sniffingConfig: receiverConfig.SniffingSettings, + uplinkCounter: uplinkCounter, + downlinkCounter: downlinkCounter, + ctx: ctx, + } + h.workers = append(h.workers, worker) + } + + if net.HasNetwork(nl, net.Network_UDP) { + worker := &udpWorker{ + tag: tag, + proxy: p, + address: address, + port: net.Port(port), + dispatcher: h.mux, + sniffingConfig: receiverConfig.SniffingSettings, + uplinkCounter: uplinkCounter, + downlinkCounter: downlinkCounter, + stream: mss, + ctx: ctx, + } + h.workers = append(h.workers, worker) + } + } + } + } + + return h, nil +} + +// Start implements common.Runnable. +func (h *AlwaysOnInboundHandler) Start() error { + for _, worker := range h.workers { + if err := worker.Start(); err != nil { + return err + } + } + return nil +} + +// Close implements common.Closable. +func (h *AlwaysOnInboundHandler) Close() error { + var errs []error + for _, worker := range h.workers { + errs = append(errs, worker.Close()) + } + errs = append(errs, h.mux.Close()) + if err := errors.Combine(errs...); err != nil { + return errors.New("failed to close all resources").Base(err) + } + return nil +} + +func (h *AlwaysOnInboundHandler) Tag() string { + return h.tag +} + +func (h *AlwaysOnInboundHandler) GetInbound() proxy.Inbound { + return h.proxy +} + +// ReceiverSettings implements inbound.Handler. +func (h *AlwaysOnInboundHandler) ReceiverSettings() *serial.TypedMessage { + return serial.ToTypedMessage(h.receiverConfig) +} + +// ProxySettings implements inbound.Handler. +func (h *AlwaysOnInboundHandler) ProxySettings() *serial.TypedMessage { + if v, ok := h.proxyConfig.(proto.Message); ok { + return serial.ToTypedMessage(v) + } + return nil +} diff --git a/subproject/Xray-core-main/app/proxyman/inbound/inbound.go b/subproject/Xray-core-main/app/proxyman/inbound/inbound.go new file mode 100644 index 00000000..374a0428 --- /dev/null +++ b/subproject/Xray-core-main/app/proxyman/inbound/inbound.go @@ -0,0 +1,191 @@ +package inbound + +import ( + "context" + "sync" + + "github.com/xtls/xray-core/app/proxyman" + "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/serial" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/features/inbound" +) + +// Manager manages all inbound handlers. +type Manager struct { + access sync.RWMutex + untaggedHandlers []inbound.Handler + taggedHandlers map[string]inbound.Handler + running bool +} + +// New returns a new Manager for inbound handlers. +func New(ctx context.Context, config *proxyman.InboundConfig) (*Manager, error) { + m := &Manager{ + taggedHandlers: make(map[string]inbound.Handler), + } + return m, nil +} + +// Type implements common.HasType. +func (*Manager) Type() interface{} { + return inbound.ManagerType() +} + +// AddHandler implements inbound.Manager. +func (m *Manager) AddHandler(ctx context.Context, handler inbound.Handler) error { + m.access.Lock() + defer m.access.Unlock() + + tag := handler.Tag() + if len(tag) > 0 { + if _, found := m.taggedHandlers[tag]; found { + return errors.New("existing tag found: " + tag) + } + m.taggedHandlers[tag] = handler + } else { + m.untaggedHandlers = append(m.untaggedHandlers, handler) + } + + if m.running { + return handler.Start() + } + + return nil +} + +// GetHandler implements inbound.Manager. +func (m *Manager) GetHandler(ctx context.Context, tag string) (inbound.Handler, error) { + m.access.RLock() + defer m.access.RUnlock() + + handler, found := m.taggedHandlers[tag] + if !found { + return nil, errors.New("handler not found: ", tag) + } + return handler, nil +} + +// RemoveHandler implements inbound.Manager. +func (m *Manager) RemoveHandler(ctx context.Context, tag string) error { + if tag == "" { + return common.ErrNoClue + } + + m.access.Lock() + defer m.access.Unlock() + + if handler, found := m.taggedHandlers[tag]; found { + if err := handler.Close(); err != nil { + errors.LogWarningInner(ctx, err, "failed to close handler ", tag) + } + delete(m.taggedHandlers, tag) + return nil + } + + return common.ErrNoClue +} + +// ListHandlers implements inbound.Manager. +func (m *Manager) ListHandlers(ctx context.Context) []inbound.Handler { + m.access.RLock() + defer m.access.RUnlock() + + response := make([]inbound.Handler, len(m.untaggedHandlers)) + copy(response, m.untaggedHandlers) + + for _, v := range m.taggedHandlers { + response = append(response, v) + } + + return response +} + +// Start implements common.Runnable. +func (m *Manager) Start() error { + m.access.Lock() + defer m.access.Unlock() + + m.running = true + + for _, handler := range m.taggedHandlers { + if err := handler.Start(); err != nil { + return err + } + } + + for _, handler := range m.untaggedHandlers { + if err := handler.Start(); err != nil { + return err + } + } + return nil +} + +// Close implements common.Closable. +func (m *Manager) Close() error { + m.access.Lock() + defer m.access.Unlock() + + m.running = false + + var errs []interface{} + for _, handler := range m.taggedHandlers { + if err := handler.Close(); err != nil { + errs = append(errs, err) + } + } + for _, handler := range m.untaggedHandlers { + if err := handler.Close(); err != nil { + errs = append(errs, err) + } + } + + if len(errs) > 0 { + return errors.New("failed to close all handlers").Base(errors.New(serial.Concat(errs...))) + } + + return nil +} + +// NewHandler creates a new inbound.Handler based on the given config. +func NewHandler(ctx context.Context, config *core.InboundHandlerConfig) (inbound.Handler, error) { + rawReceiverSettings, err := config.ReceiverSettings.GetInstance() + if err != nil { + return nil, err + } + proxySettings, err := config.ProxySettings.GetInstance() + if err != nil { + return nil, err + } + tag := config.Tag + + receiverSettings, ok := rawReceiverSettings.(*proxyman.ReceiverConfig) + if !ok { + return nil, errors.New("not a ReceiverConfig").AtError() + } + + streamSettings := receiverSettings.StreamSettings + if streamSettings != nil && streamSettings.SocketSettings != nil { + ctx = session.ContextWithSockopt(ctx, &session.Sockopt{ + Mark: streamSettings.SocketSettings.Mark, + }) + } + if streamSettings != nil && streamSettings.ProtocolName == "splithttp" { + ctx = session.ContextWithAllowedNetwork(ctx, net.Network_UDP) + } + + return NewAlwaysOnInboundHandler(ctx, tag, receiverSettings, proxySettings) +} + +func init() { + common.Must(common.RegisterConfig((*proxyman.InboundConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + return New(ctx, config.(*proxyman.InboundConfig)) + })) + common.Must(common.RegisterConfig((*core.InboundHandlerConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + return NewHandler(ctx, config.(*core.InboundHandlerConfig)) + })) +} diff --git a/subproject/Xray-core-main/app/proxyman/inbound/worker.go b/subproject/Xray-core-main/app/proxyman/inbound/worker.go new file mode 100644 index 00000000..be671b02 --- /dev/null +++ b/subproject/Xray-core-main/app/proxyman/inbound/worker.go @@ -0,0 +1,588 @@ +package inbound + +import ( + "context" + "sync" + "sync/atomic" + "time" + + "github.com/xtls/xray-core/app/proxyman" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + c "github.com/xtls/xray-core/common/ctx" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/serial" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/common/signal/done" + "github.com/xtls/xray-core/common/task" + "github.com/xtls/xray-core/features/routing" + "github.com/xtls/xray-core/features/stats" + "github.com/xtls/xray-core/proxy" + "github.com/xtls/xray-core/proxy/hysteria/account" + hyCtx "github.com/xtls/xray-core/proxy/hysteria/ctx" + "github.com/xtls/xray-core/transport/internet" + "github.com/xtls/xray-core/transport/internet/stat" + "github.com/xtls/xray-core/transport/internet/tcp" + "github.com/xtls/xray-core/transport/internet/udp" + "github.com/xtls/xray-core/transport/pipe" +) + +type worker interface { + Start() error + Close() error + Port() net.Port + Proxy() proxy.Inbound +} + +type tcpWorker struct { + address net.Address + port net.Port + proxy proxy.Inbound + stream *internet.MemoryStreamConfig + recvOrigDest bool + tag string + dispatcher routing.Dispatcher + sniffingConfig *proxyman.SniffingConfig + uplinkCounter stats.Counter + downlinkCounter stats.Counter + + hub internet.Listener + + ctx context.Context +} + +func getTProxyType(s *internet.MemoryStreamConfig) internet.SocketConfig_TProxyMode { + if s == nil || s.SocketSettings == nil { + return internet.SocketConfig_Off + } + return s.SocketSettings.Tproxy +} + +func (w *tcpWorker) callback(conn stat.Connection) { + ctx, cancel := context.WithCancel(w.ctx) + sid := session.NewID() + ctx = c.ContextWithID(ctx, sid) + + outbounds := []*session.Outbound{{}} + if w.recvOrigDest { + var dest net.Destination + switch getTProxyType(w.stream) { + case internet.SocketConfig_Redirect: + d, err := tcp.GetOriginalDestination(conn) + if err != nil { + errors.LogInfoInner(ctx, err, "failed to get original destination") + } else { + dest = d + } + case internet.SocketConfig_TProxy: + dest = net.DestinationFromAddr(conn.LocalAddr()) + } + + if dest.IsValid() { + // Check if try to connect to this inbound itself (can cause loopback) + var isLoopBack bool + if w.address == net.AnyIP || w.address == net.AnyIPv6 { + if dest.Port.Value() == w.port.Value() && IsLocal(dest.Address.IP()) { + isLoopBack = true + } + } else { + if w.hub.Addr().String() == dest.NetAddr() { + isLoopBack = true + } + } + if isLoopBack { + cancel() + conn.Close() + errors.LogError(ctx, errors.New("loopback connection detected")) + return + } + outbounds[0].Target = dest + } + } + ctx = session.ContextWithOutbounds(ctx, outbounds) + + if w.uplinkCounter != nil || w.downlinkCounter != nil { + conn = &stat.CounterConnection{ + Connection: conn, + ReadCounter: w.uplinkCounter, + WriteCounter: w.downlinkCounter, + } + } + ctx = session.ContextWithInbound(ctx, &session.Inbound{ + Source: net.DestinationFromAddr(conn.RemoteAddr()), + Local: net.DestinationFromAddr(conn.LocalAddr()), + Gateway: net.TCPDestination(w.address, w.port), + Tag: w.tag, + Conn: conn, + }) + + content := new(session.Content) + if w.sniffingConfig != nil { + content.SniffingRequest.Enabled = w.sniffingConfig.Enabled + content.SniffingRequest.OverrideDestinationForProtocol = w.sniffingConfig.DestinationOverride + content.SniffingRequest.ExcludeForDomain = w.sniffingConfig.DomainsExcluded + content.SniffingRequest.MetadataOnly = w.sniffingConfig.MetadataOnly + content.SniffingRequest.RouteOnly = w.sniffingConfig.RouteOnly + } + ctx = session.ContextWithContent(ctx, content) + + if err := w.proxy.Process(ctx, net.Network_TCP, conn, w.dispatcher); err != nil { + errors.LogInfoInner(ctx, err, "connection ends") + } + cancel() + conn.Close() +} + +func (w *tcpWorker) Proxy() proxy.Inbound { + return w.proxy +} + +func (w *tcpWorker) Start() error { + ctx := context.Background() + + type HysteriaInboundValidator interface{ HysteriaInboundValidator() *account.Validator } + if v, ok := w.proxy.(HysteriaInboundValidator); ok { + ctx = hyCtx.ContextWithRequireDatagram(ctx, true) + ctx = hyCtx.ContextWithValidator(ctx, v.HysteriaInboundValidator()) + } + + hub, err := internet.ListenTCP(ctx, w.address, w.port, w.stream, func(conn stat.Connection) { + go w.callback(conn) + }) + if err != nil { + return errors.New("failed to listen TCP on ", w.port).AtWarning().Base(err) + } + w.hub = hub + return nil +} + +func (w *tcpWorker) Close() error { + var errs []interface{} + if w.hub != nil { + if err := common.Close(w.hub); err != nil { + errs = append(errs, err) + } + if err := common.Close(w.proxy); err != nil { + errs = append(errs, err) + } + } + if len(errs) > 0 { + return errors.New("failed to close all resources").Base(errors.New(serial.Concat(errs...))) + } + + return nil +} + +func (w *tcpWorker) Port() net.Port { + return w.port +} + +type udpConn struct { + lastActivityTime int64 // in seconds + reader buf.Reader + writer buf.Writer + output func([]byte) (int, error) + remote net.Addr + local net.Addr + done *done.Instance + uplink stats.Counter + downlink stats.Counter + inactive bool + cancel context.CancelFunc +} + +func (c *udpConn) setInactive() { + c.inactive = true +} + +func (c *udpConn) updateActivity() { + atomic.StoreInt64(&c.lastActivityTime, time.Now().Unix()) +} + +// ReadMultiBuffer implements buf.Reader +func (c *udpConn) ReadMultiBuffer() (buf.MultiBuffer, error) { + mb, err := c.reader.ReadMultiBuffer() + if err != nil { + return nil, err + } + c.updateActivity() + + if c.uplink != nil { + c.uplink.Add(int64(mb.Len())) + } + + return mb, nil +} + +func (c *udpConn) Read(buf []byte) (int, error) { + panic("not implemented") +} + +// Write implements io.Writer. +func (c *udpConn) Write(buf []byte) (int, error) { + n, err := c.output(buf) + if c.downlink != nil { + c.downlink.Add(int64(n)) + } + if err == nil { + c.updateActivity() + } + return n, err +} + +func (c *udpConn) Close() error { + if c.cancel != nil { + c.cancel() + } + common.Must(c.done.Close()) + common.Must(common.Close(c.writer)) + return nil +} + +func (c *udpConn) RemoteAddr() net.Addr { + return c.remote +} + +func (c *udpConn) LocalAddr() net.Addr { + return c.local +} + +func (*udpConn) SetDeadline(time.Time) error { + return nil +} + +func (*udpConn) SetReadDeadline(time.Time) error { + return nil +} + +func (*udpConn) SetWriteDeadline(time.Time) error { + return nil +} + +type connID struct { + src net.Destination + dest net.Destination +} + +type udpWorker struct { + sync.RWMutex + + proxy proxy.Inbound + hub *udp.Hub + address net.Address + port net.Port + tag string + stream *internet.MemoryStreamConfig + dispatcher routing.Dispatcher + sniffingConfig *proxyman.SniffingConfig + uplinkCounter stats.Counter + downlinkCounter stats.Counter + + checker *task.Periodic + activeConn map[connID]*udpConn + + ctx context.Context + cone bool +} + +func (w *udpWorker) getConnection(id connID) (*udpConn, bool) { + w.Lock() + defer w.Unlock() + + if conn, found := w.activeConn[id]; found && !conn.done.Done() { + conn.updateActivity() + return conn, true + } + + pReader, pWriter := pipe.New(pipe.DiscardOverflow(), pipe.WithSizeLimit(16*1024)) + conn := &udpConn{ + reader: pReader, + writer: pWriter, + output: func(b []byte) (int, error) { + return w.hub.WriteTo(b, id.src) + }, + remote: &net.UDPAddr{ + IP: id.src.Address.IP(), + Port: int(id.src.Port), + }, + local: &net.UDPAddr{ + IP: w.address.IP(), + Port: int(w.port), + }, + done: done.New(), + uplink: w.uplinkCounter, + downlink: w.downlinkCounter, + } + w.activeConn[id] = conn + + conn.updateActivity() + return conn, false +} + +func (w *udpWorker) callback(b *buf.Buffer, source net.Destination, originalDest net.Destination) { + id := connID{ + src: source, + } + if originalDest.IsValid() { + if !w.cone { + id.dest = originalDest + } + b.UDP = &originalDest + } + conn, existing := w.getConnection(id) + + // payload will be discarded in pipe is full. + conn.writer.WriteMultiBuffer(buf.MultiBuffer{b}) + + if !existing { + common.Must(w.checker.Start()) + + go func() { + ctx, cancel := context.WithCancel(w.ctx) + conn.cancel = cancel + sid := session.NewID() + ctx = c.ContextWithID(ctx, sid) + + outbounds := []*session.Outbound{{}} + if originalDest.IsValid() { + outbounds[0].Target = originalDest + } + ctx = session.ContextWithOutbounds(ctx, outbounds) + local := net.DestinationFromAddr(w.hub.Addr()) + if local.Address == net.AnyIP || local.Address == net.AnyIPv6 { + if source.Address.Family().IsIPv4() { + local.Address = net.AnyIP + } else if source.Address.Family().IsIPv6() { + local.Address = net.AnyIPv6 + } + } + + ctx = session.ContextWithInbound(ctx, &session.Inbound{ + Source: source, + Local: local, // Due to some limitations, in UDP connections, localIP is always equal to listen interface IP + Gateway: net.UDPDestination(w.address, w.port), + Tag: w.tag, + }) + content := new(session.Content) + if w.sniffingConfig != nil { + content.SniffingRequest.Enabled = w.sniffingConfig.Enabled + content.SniffingRequest.OverrideDestinationForProtocol = w.sniffingConfig.DestinationOverride + content.SniffingRequest.ExcludeForDomain = w.sniffingConfig.DomainsExcluded + content.SniffingRequest.MetadataOnly = w.sniffingConfig.MetadataOnly + content.SniffingRequest.RouteOnly = w.sniffingConfig.RouteOnly + } + ctx = session.ContextWithContent(ctx, content) + if err := w.proxy.Process(ctx, net.Network_UDP, conn, w.dispatcher); err != nil { + errors.LogInfoInner(ctx, err, "connection ends") + } + conn.Close() + // conn not removed by checker TODO may be lock worker here is better + if !conn.inactive { + conn.setInactive() + w.removeConn(id) + } + }() + } +} + +func (w *udpWorker) removeConn(id connID) { + w.Lock() + delete(w.activeConn, id) + w.Unlock() +} + +func (w *udpWorker) handlePackets() { + receive := w.hub.Receive() + for payload := range receive { + w.callback(payload.Payload, payload.Source, payload.Target) + } +} + +func (w *udpWorker) clean() error { + nowSec := time.Now().Unix() + w.Lock() + defer w.Unlock() + + if len(w.activeConn) == 0 { + return errors.New("no more connections. stopping...") + } + + for addr, conn := range w.activeConn { + if nowSec-atomic.LoadInt64(&conn.lastActivityTime) > 2*60 { + if !conn.inactive { + conn.setInactive() + delete(w.activeConn, addr) + } + conn.Close() + } + } + + if len(w.activeConn) == 0 { + w.activeConn = make(map[connID]*udpConn, 16) + } + + return nil +} + +func (w *udpWorker) Start() error { + w.activeConn = make(map[connID]*udpConn, 16) + ctx := context.Background() + h, err := udp.ListenUDP(ctx, w.address, w.port, w.stream, udp.HubCapacity(256)) + if err != nil { + return err + } + + w.cone = w.ctx.Value("cone").(bool) + + w.checker = &task.Periodic{ + Interval: time.Minute, + Execute: w.clean, + } + + w.hub = h + go w.handlePackets() + return nil +} + +func (w *udpWorker) Close() error { + w.Lock() + defer w.Unlock() + + var errs []interface{} + + if w.hub != nil { + if err := w.hub.Close(); err != nil { + errs = append(errs, err) + } + } + + if w.checker != nil { + if err := w.checker.Close(); err != nil { + errs = append(errs, err) + } + } + + if err := common.Close(w.proxy); err != nil { + errs = append(errs, err) + } + + if len(errs) > 0 { + return errors.New("failed to close all resources").Base(errors.New(serial.Concat(errs...))) + } + return nil +} + +func (w *udpWorker) Port() net.Port { + return w.port +} + +func (w *udpWorker) Proxy() proxy.Inbound { + return w.proxy +} + +type dsWorker struct { + address net.Address + proxy proxy.Inbound + stream *internet.MemoryStreamConfig + tag string + dispatcher routing.Dispatcher + sniffingConfig *proxyman.SniffingConfig + uplinkCounter stats.Counter + downlinkCounter stats.Counter + + hub internet.Listener + + ctx context.Context +} + +func (w *dsWorker) callback(conn stat.Connection) { + ctx, cancel := context.WithCancel(w.ctx) + sid := session.NewID() + ctx = c.ContextWithID(ctx, sid) + + if w.uplinkCounter != nil || w.downlinkCounter != nil { + conn = &stat.CounterConnection{ + Connection: conn, + ReadCounter: w.uplinkCounter, + WriteCounter: w.downlinkCounter, + } + } + ctx = session.ContextWithInbound(ctx, &session.Inbound{ + Source: net.DestinationFromAddr(conn.RemoteAddr()), + Local: net.DestinationFromAddr(conn.LocalAddr()), + Gateway: net.UnixDestination(w.address), + Tag: w.tag, + Conn: conn, + }) + + content := new(session.Content) + if w.sniffingConfig != nil { + content.SniffingRequest.Enabled = w.sniffingConfig.Enabled + content.SniffingRequest.OverrideDestinationForProtocol = w.sniffingConfig.DestinationOverride + content.SniffingRequest.ExcludeForDomain = w.sniffingConfig.DomainsExcluded + content.SniffingRequest.MetadataOnly = w.sniffingConfig.MetadataOnly + content.SniffingRequest.RouteOnly = w.sniffingConfig.RouteOnly + } + ctx = session.ContextWithContent(ctx, content) + + if err := w.proxy.Process(ctx, net.Network_UNIX, conn, w.dispatcher); err != nil { + errors.LogInfoInner(ctx, err, "connection ends") + } + cancel() + if err := conn.Close(); err != nil { + errors.LogInfoInner(ctx, err, "failed to close connection") + } +} + +func (w *dsWorker) Proxy() proxy.Inbound { + return w.proxy +} + +func (w *dsWorker) Port() net.Port { + return net.Port(0) +} + +func (w *dsWorker) Start() error { + ctx := context.Background() + hub, err := internet.ListenUnix(ctx, w.address, w.stream, func(conn stat.Connection) { + go w.callback(conn) + }) + if err != nil { + return errors.New("failed to listen Unix Domain Socket on ", w.address).AtWarning().Base(err) + } + w.hub = hub + return nil +} + +func (w *dsWorker) Close() error { + var errs []interface{} + if w.hub != nil { + if err := common.Close(w.hub); err != nil { + errs = append(errs, err) + } + if err := common.Close(w.proxy); err != nil { + errs = append(errs, err) + } + } + if len(errs) > 0 { + return errors.New("failed to close all resources").Base(errors.New(serial.Concat(errs...))) + } + + return nil +} + +func IsLocal(ip net.IP) bool { + addrs, err := net.InterfaceAddrs() + if err != nil { + return false + } + for _, addr := range addrs { + if ipnet, ok := addr.(*net.IPNet); ok { + if ipnet.IP.Equal(ip) { + return true + } + } + } + return false +} diff --git a/subproject/Xray-core-main/app/proxyman/outbound/handler.go b/subproject/Xray-core-main/app/proxyman/outbound/handler.go new file mode 100644 index 00000000..62902c60 --- /dev/null +++ b/subproject/Xray-core-main/app/proxyman/outbound/handler.go @@ -0,0 +1,415 @@ +package outbound + +import ( + "context" + "crypto/rand" + goerrors "errors" + "io" + "math/big" + "os" + + "github.com/xtls/xray-core/common/dice" + + "github.com/xtls/xray-core/app/proxyman" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/mux" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/net/cnc" + "github.com/xtls/xray-core/common/serial" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/features/outbound" + "github.com/xtls/xray-core/features/policy" + "github.com/xtls/xray-core/features/stats" + "github.com/xtls/xray-core/proxy" + "github.com/xtls/xray-core/transport" + "github.com/xtls/xray-core/transport/internet" + "github.com/xtls/xray-core/transport/internet/stat" + "github.com/xtls/xray-core/transport/internet/tls" + "github.com/xtls/xray-core/transport/pipe" + "google.golang.org/protobuf/proto" +) + +func getStatCounter(v *core.Instance, tag string) (stats.Counter, stats.Counter) { + var uplinkCounter stats.Counter + var downlinkCounter stats.Counter + + policy := v.GetFeature(policy.ManagerType()).(policy.Manager) + if len(tag) > 0 && policy.ForSystem().Stats.OutboundUplink { + statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager) + name := "outbound>>>" + tag + ">>>traffic>>>uplink" + c, _ := stats.GetOrRegisterCounter(statsManager, name) + if c != nil { + uplinkCounter = c + } + } + if len(tag) > 0 && policy.ForSystem().Stats.OutboundDownlink { + statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager) + name := "outbound>>>" + tag + ">>>traffic>>>downlink" + c, _ := stats.GetOrRegisterCounter(statsManager, name) + if c != nil { + downlinkCounter = c + } + } + + return uplinkCounter, downlinkCounter +} + +// Handler implements outbound.Handler. +type Handler struct { + tag string + senderSettings *proxyman.SenderConfig + streamSettings *internet.MemoryStreamConfig + proxyConfig proto.Message + proxy proxy.Outbound + outboundManager outbound.Manager + mux *mux.ClientManager + xudp *mux.ClientManager + udp443 string + uplinkCounter stats.Counter + downlinkCounter stats.Counter +} + +// NewHandler creates a new Handler based on the given configuration. +func NewHandler(ctx context.Context, config *core.OutboundHandlerConfig) (outbound.Handler, error) { + v := core.MustFromContext(ctx) + uplinkCounter, downlinkCounter := getStatCounter(v, config.Tag) + h := &Handler{ + tag: config.Tag, + outboundManager: v.GetFeature(outbound.ManagerType()).(outbound.Manager), + uplinkCounter: uplinkCounter, + downlinkCounter: downlinkCounter, + } + + if config.SenderSettings != nil { + senderSettings, err := config.SenderSettings.GetInstance() + if err != nil { + return nil, err + } + switch s := senderSettings.(type) { + case *proxyman.SenderConfig: + h.senderSettings = s + mss, err := internet.ToMemoryStreamConfig(s.StreamSettings) + if err != nil { + return nil, errors.New("failed to parse stream settings").Base(err).AtWarning() + } + h.streamSettings = mss + default: + return nil, errors.New("settings is not SenderConfig") + } + } + + proxyConfig, err := config.ProxySettings.GetInstance() + if err != nil { + return nil, err + } + h.proxyConfig = proxyConfig + + ctx = session.ContextWithFullHandler(ctx, h) + + rawProxyHandler, err := common.CreateObject(ctx, proxyConfig) + if err != nil { + return nil, err + } + + proxyHandler, ok := rawProxyHandler.(proxy.Outbound) + if !ok { + return nil, errors.New("not an outbound handler") + } + + if h.senderSettings != nil && h.senderSettings.MultiplexSettings != nil { + if config := h.senderSettings.MultiplexSettings; config.Enabled { + if config.Concurrency < 0 { + h.mux = &mux.ClientManager{Enabled: false} + } + if config.Concurrency == 0 { + config.Concurrency = 8 // same as before + } + if config.Concurrency > 0 { + h.mux = &mux.ClientManager{ + Enabled: true, + Picker: &mux.IncrementalWorkerPicker{ + Factory: &mux.DialingWorkerFactory{ + Proxy: proxyHandler, + Dialer: h, + Strategy: mux.ClientStrategy{ + MaxConcurrency: uint32(config.Concurrency), + MaxConnection: 128, + }, + }, + }, + } + } + if config.XudpConcurrency < 0 { + h.xudp = &mux.ClientManager{Enabled: false} + } + if config.XudpConcurrency == 0 { + h.xudp = nil // same as before + } + if config.XudpConcurrency > 0 { + h.xudp = &mux.ClientManager{ + Enabled: true, + Picker: &mux.IncrementalWorkerPicker{ + Factory: &mux.DialingWorkerFactory{ + Proxy: proxyHandler, + Dialer: h, + Strategy: mux.ClientStrategy{ + MaxConcurrency: uint32(config.XudpConcurrency), + MaxConnection: 128, + }, + }, + }, + } + } + h.udp443 = config.XudpProxyUDP443 + } + } + + h.proxy = proxyHandler + return h, nil +} + +// Tag implements outbound.Handler. +func (h *Handler) Tag() string { + return h.tag +} + +// Dispatch implements proxy.Outbound.Dispatch. +func (h *Handler) Dispatch(ctx context.Context, link *transport.Link) { + outbounds := session.OutboundsFromContext(ctx) + ob := outbounds[len(outbounds)-1] + content := session.ContentFromContext(ctx) + if h.senderSettings != nil && h.senderSettings.TargetStrategy.HasStrategy() && ob.Target.Address.Family().IsDomain() && (content == nil || !content.SkipDNSResolve) { + strategy := h.senderSettings.TargetStrategy + if ob.Target.Network == net.Network_UDP && ob.OriginalTarget.Address != nil { + strategy = strategy.GetDynamicStrategy(ob.OriginalTarget.Address.Family()) + } + ips, err := internet.LookupForIP(ob.Target.Address.Domain(), strategy, nil) + if err != nil { + errors.LogInfoInner(ctx, err, "failed to resolve ip for target ", ob.Target.Address.Domain()) + if h.senderSettings.TargetStrategy.ForceIP() { + err := errors.New("failed to resolve ip for target ", ob.Target.Address.Domain()).Base(err) + session.SubmitOutboundErrorToOriginator(ctx, err) + common.Interrupt(link.Writer) + common.Interrupt(link.Reader) + return + } + + } else { + unchangedDomain := ob.Target.Address.Domain() + ob.Target.Address = net.IPAddress(ips[dice.Roll(len(ips))]) + errors.LogInfo(ctx, "target: ", unchangedDomain, " resolved to: ", ob.Target.Address.String()) + } + } + if ob.Target.Network == net.Network_UDP && ob.OriginalTarget.Address != nil && ob.OriginalTarget.Address != ob.Target.Address { + link.Reader = &buf.EndpointOverrideReader{Reader: link.Reader, Dest: ob.Target.Address, OriginalDest: ob.OriginalTarget.Address} + link.Writer = &buf.EndpointOverrideWriter{Writer: link.Writer, Dest: ob.Target.Address, OriginalDest: ob.OriginalTarget.Address} + } + if h.mux != nil { + test := func(err error) { + if err != nil { + err := errors.New("failed to process mux outbound traffic").Base(err) + session.SubmitOutboundErrorToOriginator(ctx, err) + errors.LogInfo(ctx, err.Error()) + common.Interrupt(link.Writer) + common.Interrupt(link.Reader) + } + } + if ob.Target.Network == net.Network_UDP && ob.Target.Port == 443 { + switch h.udp443 { + case "reject": + test(errors.New("XUDP rejected UDP/443 traffic").AtInfo()) + return + case "skip": + goto out + } + } + if h.xudp != nil && ob.Target.Network == net.Network_UDP { + if !h.xudp.Enabled { + goto out + } + test(h.xudp.Dispatch(ctx, link)) + return + } + if h.mux.Enabled { + test(h.mux.Dispatch(ctx, link)) + return + } + } +out: + err := h.proxy.Process(ctx, link, h) + var errC error + if err != nil { + errC = errors.Cause(err) + if goerrors.Is(errC, io.EOF) || goerrors.Is(errC, io.ErrClosedPipe) || goerrors.Is(errC, context.Canceled) { + err = nil + } + } + if err != nil { + // Ensure outbound ray is properly closed. + err := errors.New("failed to process outbound traffic").Base(err) + session.SubmitOutboundErrorToOriginator(ctx, err) + errors.LogInfo(ctx, err.Error()) + common.Interrupt(link.Writer) + } else { + if errC != nil && goerrors.Is(errC, io.ErrClosedPipe) { + common.Interrupt(link.Writer) + } else { + common.Close(link.Writer) + } + } + common.Interrupt(link.Reader) +} + +func (h *Handler) DestIpAddress() net.IP { + return internet.DestIpAddress() +} + +// Dial implements internet.Dialer. +func (h *Handler) Dial(ctx context.Context, dest net.Destination) (stat.Connection, error) { + if h.senderSettings != nil { + + if h.senderSettings.ProxySettings.HasTag() { + + tag := h.senderSettings.ProxySettings.Tag + handler := h.outboundManager.GetHandler(tag) + if handler != nil { + errors.LogDebug(ctx, "proxying to ", tag, " for dest ", dest) + outbounds := session.OutboundsFromContext(ctx) + ctx = session.ContextWithOutbounds(ctx, append(outbounds, &session.Outbound{ + Target: dest, + Tag: tag, + })) // add another outbound in session ctx + opts := pipe.OptionsFromContext(ctx) + uplinkReader, uplinkWriter := pipe.New(opts...) + downlinkReader, downlinkWriter := pipe.New(opts...) + + go handler.Dispatch(ctx, &transport.Link{Reader: uplinkReader, Writer: downlinkWriter}) + conn := cnc.NewConnection(cnc.ConnectionInputMulti(uplinkWriter), cnc.ConnectionOutputMulti(downlinkReader)) + + if config := tls.ConfigFromStreamSettings(h.streamSettings); config != nil { + tlsConfig := config.GetTLSConfig(tls.WithDestination(dest)) + conn = tls.Client(conn, tlsConfig) + } + + return h.getStatCouterConnection(conn), nil + } + + errors.LogError(ctx, "failed to get outbound handler with tag: ", tag) + return nil, errors.New("failed to get outbound handler with tag: " + tag) + } + + if h.senderSettings.Via != nil { + outbounds := session.OutboundsFromContext(ctx) + ob := outbounds[len(outbounds)-1] + h.SetOutboundGateway(ctx, ob) + } + + } + + if conn, err := h.getUoTConnection(ctx, dest); err != os.ErrInvalid { + return conn, err + } + + conn, err := internet.Dial(ctx, dest, h.streamSettings) + conn = h.getStatCouterConnection(conn) + outbounds := session.OutboundsFromContext(ctx) + if outbounds != nil { + ob := outbounds[len(outbounds)-1] + ob.Conn = conn + } else { + // for Vision's pre-connect + } + return conn, err +} + +func (h *Handler) SetOutboundGateway(ctx context.Context, ob *session.Outbound) { + if ob.Gateway == nil && h.senderSettings != nil && h.senderSettings.Via != nil && !h.senderSettings.ProxySettings.HasTag() && (h.streamSettings.SocketSettings == nil || len(h.streamSettings.SocketSettings.DialerProxy) == 0) { + var domain string + addr := h.senderSettings.Via.AsAddress() + domain = h.senderSettings.Via.GetDomain() + switch { + case h.senderSettings.ViaCidr != "": + ob.Gateway = ParseRandomIP(addr, h.senderSettings.ViaCidr) + + case domain == "origin": + if inbound := session.InboundFromContext(ctx); inbound != nil { + if inbound.Local.IsValid() && inbound.Local.Address.Family().IsIP() { + ob.Gateway = inbound.Local.Address + errors.LogDebug(ctx, "use inbound local ip as sendthrough: ", inbound.Local.Address.String()) + } + } + case domain == "srcip": + if inbound := session.InboundFromContext(ctx); inbound != nil { + if inbound.Source.IsValid() && inbound.Source.Address.Family().IsIP() { + ob.Gateway = inbound.Source.Address + errors.LogDebug(ctx, "use inbound source ip as sendthrough: ", inbound.Source.Address.String()) + } + } + //case addr.Family().IsDomain(): + default: + ob.Gateway = addr + + } + + } +} + +func (h *Handler) getStatCouterConnection(conn stat.Connection) stat.Connection { + if h.uplinkCounter != nil || h.downlinkCounter != nil { + return &stat.CounterConnection{ + Connection: conn, + ReadCounter: h.downlinkCounter, + WriteCounter: h.uplinkCounter, + } + } + return conn +} + +// GetOutbound implements proxy.GetOutbound. +func (h *Handler) GetOutbound() proxy.Outbound { + return h.proxy +} + +// Start implements common.Runnable. +func (h *Handler) Start() error { + return nil +} + +// Close implements common.Closable. +func (h *Handler) Close() error { + common.Close(h.mux) + common.Close(h.proxy) + return nil +} + +// SenderSettings implements outbound.Handler. +func (h *Handler) SenderSettings() *serial.TypedMessage { + return serial.ToTypedMessage(h.senderSettings) +} + +// ProxySettings implements outbound.Handler. +func (h *Handler) ProxySettings() *serial.TypedMessage { + return serial.ToTypedMessage(h.proxyConfig) +} + +func ParseRandomIP(addr net.Address, prefix string) net.Address { + + _, ipnet, _ := net.ParseCIDR(addr.IP().String() + "/" + prefix) + + ones, bits := ipnet.Mask.Size() + subnetSize := new(big.Int).Lsh(big.NewInt(1), uint(bits-ones)) + + rnd, _ := rand.Int(rand.Reader, subnetSize) + + startInt := new(big.Int).SetBytes(ipnet.IP) + rndInt := new(big.Int).Add(startInt, rnd) + + rndBytes := rndInt.Bytes() + padded := make([]byte, len(ipnet.IP)) + copy(padded[len(padded)-len(rndBytes):], rndBytes) + + return net.ParseAddress(net.IP(padded).String()) +} diff --git a/subproject/Xray-core-main/app/proxyman/outbound/handler_test.go b/subproject/Xray-core-main/app/proxyman/outbound/handler_test.go new file mode 100644 index 00000000..3f7ef28e --- /dev/null +++ b/subproject/Xray-core-main/app/proxyman/outbound/handler_test.go @@ -0,0 +1,176 @@ +package outbound_test + +import ( + "context" + "fmt" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/xtls/xray-core/app/policy" + "github.com/xtls/xray-core/app/proxyman" + . "github.com/xtls/xray-core/app/proxyman/outbound" + "github.com/xtls/xray-core/app/stats" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/serial" + "github.com/xtls/xray-core/common/session" + core "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/features/outbound" + "github.com/xtls/xray-core/proxy/freedom" + "github.com/xtls/xray-core/transport/internet/stat" +) + +func TestInterfaces(t *testing.T) { + _ = (outbound.Handler)(new(Handler)) + _ = (outbound.Manager)(new(Manager)) +} + +const xrayKey core.XrayKey = 1 + +func TestOutboundWithoutStatCounter(t *testing.T) { + config := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&stats.Config{}), + serial.ToTypedMessage(&policy.Config{ + System: &policy.SystemPolicy{ + Stats: &policy.SystemPolicy_Stats{ + InboundUplink: true, + }, + }, + }), + }, + } + + v, _ := core.New(config) + v.AddFeature((outbound.Manager)(new(Manager))) + ctx := context.WithValue(context.Background(), xrayKey, v) + ctx = session.ContextWithOutbounds(ctx, []*session.Outbound{{}}) + h, _ := NewHandler(ctx, &core.OutboundHandlerConfig{ + Tag: "tag", + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }) + conn, _ := h.(*Handler).Dial(ctx, net.TCPDestination(net.DomainAddress("localhost"), 13146)) + _, ok := conn.(*stat.CounterConnection) + if ok { + t.Errorf("Expected conn to not be CounterConnection") + } +} + +func TestOutboundWithStatCounter(t *testing.T) { + config := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&stats.Config{}), + serial.ToTypedMessage(&policy.Config{ + System: &policy.SystemPolicy{ + Stats: &policy.SystemPolicy_Stats{ + OutboundUplink: true, + OutboundDownlink: true, + }, + }, + }), + }, + } + + v, _ := core.New(config) + v.AddFeature((outbound.Manager)(new(Manager))) + ctx := context.WithValue(context.Background(), xrayKey, v) + ctx = session.ContextWithOutbounds(ctx, []*session.Outbound{{}}) + h, _ := NewHandler(ctx, &core.OutboundHandlerConfig{ + Tag: "tag", + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }) + conn, _ := h.(*Handler).Dial(ctx, net.TCPDestination(net.DomainAddress("localhost"), 13146)) + _, ok := conn.(*stat.CounterConnection) + if !ok { + t.Errorf("Expected conn to be CounterConnection") + } +} + +func TestTagsCache(t *testing.T) { + + test_duration := 10 * time.Second + threads_num := 50 + delay := 10 * time.Millisecond + tags_prefix := "node" + + tags := sync.Map{} + counter := atomic.Uint64{} + + ohm, err := New(context.Background(), &proxyman.OutboundConfig{}) + if err != nil { + t.Error("failed to create outbound handler manager") + } + config := &core.Config{ + App: []*serial.TypedMessage{}, + } + v, _ := core.New(config) + v.AddFeature(ohm) + ctx := context.WithValue(context.Background(), xrayKey, v) + + stop_add_rm := false + wg_add_rm := sync.WaitGroup{} + addHandlers := func() { + defer wg_add_rm.Done() + for !stop_add_rm { + time.Sleep(delay) + idx := counter.Add(1) + tag := fmt.Sprintf("%s%d", tags_prefix, idx) + cfg := &core.OutboundHandlerConfig{ + Tag: tag, + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + } + if h, err := NewHandler(ctx, cfg); err == nil { + if err := ohm.AddHandler(ctx, h); err == nil { + // t.Log("add handler:", tag) + tags.Store(tag, nil) + } else { + t.Error("failed to add handler:", tag) + } + } else { + t.Error("failed to create handler:", tag) + } + } + } + + rmHandlers := func() { + defer wg_add_rm.Done() + for !stop_add_rm { + time.Sleep(delay) + tags.Range(func(key interface{}, value interface{}) bool { + if _, ok := tags.LoadAndDelete(key); ok { + // t.Log("remove handler:", key) + ohm.RemoveHandler(ctx, key.(string)) + return false + } + return true + }) + } + } + + selectors := []string{tags_prefix} + wg_get := sync.WaitGroup{} + stop_get := false + getTags := func() { + defer wg_get.Done() + for !stop_get { + time.Sleep(delay) + _ = ohm.Select(selectors) + // t.Logf("get tags: %v", tag) + } + } + + for i := 0; i < threads_num; i++ { + wg_add_rm.Add(2) + go rmHandlers() + go addHandlers() + wg_get.Add(1) + go getTags() + } + + time.Sleep(test_duration) + stop_add_rm = true + wg_add_rm.Wait() + stop_get = true + wg_get.Wait() +} diff --git a/subproject/Xray-core-main/app/proxyman/outbound/outbound.go b/subproject/Xray-core-main/app/proxyman/outbound/outbound.go new file mode 100644 index 00000000..244fcffe --- /dev/null +++ b/subproject/Xray-core-main/app/proxyman/outbound/outbound.go @@ -0,0 +1,198 @@ +package outbound + +import ( + "context" + "sort" + "strings" + "sync" + + "github.com/xtls/xray-core/app/proxyman" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/features/outbound" +) + +// Manager is to manage all outbound handlers. +type Manager struct { + access sync.RWMutex + defaultHandler outbound.Handler + taggedHandler map[string]outbound.Handler + untaggedHandlers []outbound.Handler + running bool + tagsCache *sync.Map +} + +// New creates a new Manager. +func New(ctx context.Context, config *proxyman.OutboundConfig) (*Manager, error) { + m := &Manager{ + taggedHandler: make(map[string]outbound.Handler), + tagsCache: &sync.Map{}, + } + return m, nil +} + +// Type implements common.HasType. +func (m *Manager) Type() interface{} { + return outbound.ManagerType() +} + +// Start implements core.Feature +func (m *Manager) Start() error { + m.access.Lock() + defer m.access.Unlock() + + m.running = true + + for _, h := range m.taggedHandler { + if err := h.Start(); err != nil { + return err + } + } + + for _, h := range m.untaggedHandlers { + if err := h.Start(); err != nil { + return err + } + } + + return nil +} + +// Close implements core.Feature +func (m *Manager) Close() error { + m.access.Lock() + defer m.access.Unlock() + + m.running = false + + var errs []error + for _, h := range m.taggedHandler { + errs = append(errs, h.Close()) + } + + for _, h := range m.untaggedHandlers { + errs = append(errs, h.Close()) + } + + return errors.Combine(errs...) +} + +// GetDefaultHandler implements outbound.Manager. +func (m *Manager) GetDefaultHandler() outbound.Handler { + m.access.RLock() + defer m.access.RUnlock() + + if m.defaultHandler == nil { + return nil + } + return m.defaultHandler +} + +// GetHandler implements outbound.Manager. +func (m *Manager) GetHandler(tag string) outbound.Handler { + m.access.RLock() + defer m.access.RUnlock() + if handler, found := m.taggedHandler[tag]; found { + return handler + } + return nil +} + +// AddHandler implements outbound.Manager. +func (m *Manager) AddHandler(ctx context.Context, handler outbound.Handler) error { + m.access.Lock() + defer m.access.Unlock() + + m.tagsCache = &sync.Map{} + + if m.defaultHandler == nil { + m.defaultHandler = handler + } + + tag := handler.Tag() + if len(tag) > 0 { + if _, found := m.taggedHandler[tag]; found { + return errors.New("existing tag found: " + tag) + } + m.taggedHandler[tag] = handler + } else { + m.untaggedHandlers = append(m.untaggedHandlers, handler) + } + + if m.running { + return handler.Start() + } + + return nil +} + +// RemoveHandler implements outbound.Manager. +func (m *Manager) RemoveHandler(ctx context.Context, tag string) error { + if tag == "" { + return common.ErrNoClue + } + m.access.Lock() + defer m.access.Unlock() + + m.tagsCache = &sync.Map{} + + delete(m.taggedHandler, tag) + if m.defaultHandler != nil && m.defaultHandler.Tag() == tag { + m.defaultHandler = nil + } + + return nil +} + +// ListHandlers implements outbound.Manager. +func (m *Manager) ListHandlers(ctx context.Context) []outbound.Handler { + m.access.RLock() + defer m.access.RUnlock() + + response := make([]outbound.Handler, len(m.untaggedHandlers)) + copy(response, m.untaggedHandlers) + + for _, v := range m.taggedHandler { + response = append(response, v) + } + + return response +} + +// Select implements outbound.HandlerSelector. +func (m *Manager) Select(selectors []string) []string { + + key := strings.Join(selectors, ",") + if cache, ok := m.tagsCache.Load(key); ok { + return cache.([]string) + } + + m.access.RLock() + defer m.access.RUnlock() + + tags := make([]string, 0, len(selectors)) + + for tag := range m.taggedHandler { + for _, selector := range selectors { + if strings.HasPrefix(tag, selector) { + tags = append(tags, tag) + break + } + } + } + + sort.Strings(tags) + m.tagsCache.Store(key, tags) + + return tags +} + +func init() { + common.Must(common.RegisterConfig((*proxyman.OutboundConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + return New(ctx, config.(*proxyman.OutboundConfig)) + })) + common.Must(common.RegisterConfig((*core.OutboundHandlerConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + return NewHandler(ctx, config.(*core.OutboundHandlerConfig)) + })) +} diff --git a/subproject/Xray-core-main/app/proxyman/outbound/uot.go b/subproject/Xray-core-main/app/proxyman/outbound/uot.go new file mode 100644 index 00000000..659f65a1 --- /dev/null +++ b/subproject/Xray-core-main/app/proxyman/outbound/uot.go @@ -0,0 +1,35 @@ +package outbound + +import ( + "context" + "os" + + "github.com/sagernet/sing/common/uot" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/transport/internet" + "github.com/xtls/xray-core/transport/internet/stat" +) + +func (h *Handler) getUoTConnection(ctx context.Context, dest net.Destination) (stat.Connection, error) { + if dest.Address == nil { + return nil, errors.New("nil destination address") + } + if !dest.Address.Family().IsDomain() { + return nil, os.ErrInvalid + } + var uotVersion int + if dest.Address.Domain() == uot.MagicAddress { + uotVersion = uot.Version + } else if dest.Address.Domain() == uot.LegacyMagicAddress { + uotVersion = uot.LegacyVersion + } else { + return nil, os.ErrInvalid + } + packetConn, err := internet.ListenSystemPacket(ctx, &net.UDPAddr{IP: net.AnyIP.IP(), Port: 0}, h.streamSettings.SocketSettings) + if err != nil { + return nil, errors.New("unable to listen socket").Base(err) + } + conn := uot.NewServerConn(packetConn, uotVersion) + return h.getStatCouterConnection(conn), nil +} diff --git a/subproject/Xray-core-main/app/reverse/bridge.go b/subproject/Xray-core-main/app/reverse/bridge.go new file mode 100644 index 00000000..f6dfec48 --- /dev/null +++ b/subproject/Xray-core-main/app/reverse/bridge.go @@ -0,0 +1,235 @@ +package reverse + +import ( + "context" + "time" + + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/mux" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/common/signal" + "github.com/xtls/xray-core/common/task" + "github.com/xtls/xray-core/features/routing" + "github.com/xtls/xray-core/transport" + "github.com/xtls/xray-core/transport/pipe" + "google.golang.org/protobuf/proto" +) + +// Bridge is a component in reverse proxy, that relays connections from Portal to local address. +type Bridge struct { + dispatcher routing.Dispatcher + tag string + domain string + workers []*BridgeWorker + monitorTask *task.Periodic +} + +// NewBridge creates a new Bridge instance. +func NewBridge(config *BridgeConfig, dispatcher routing.Dispatcher) (*Bridge, error) { + if config.Tag == "" { + return nil, errors.New("bridge tag is empty") + } + if config.Domain == "" { + return nil, errors.New("bridge domain is empty") + } + + b := &Bridge{ + dispatcher: dispatcher, + tag: config.Tag, + domain: config.Domain, + } + b.monitorTask = &task.Periodic{ + Execute: b.monitor, + Interval: time.Second * 2, + } + return b, nil +} + +func (b *Bridge) cleanup() { + var activeWorkers []*BridgeWorker + + for _, w := range b.workers { + if w.IsActive() { + activeWorkers = append(activeWorkers, w) + } + if w.Closed() { + if w.Timer != nil { + w.Timer.SetTimeout(0) + } + } + } + + if len(activeWorkers) != len(b.workers) { + b.workers = activeWorkers + } +} + +func (b *Bridge) monitor() error { + b.cleanup() + + var numConnections uint32 + var numWorker uint32 + + for _, w := range b.workers { + if w.IsActive() { + numConnections += w.Connections() + numWorker++ + } + } + + if numWorker == 0 || numConnections/numWorker > 16 { + worker, err := NewBridgeWorker(b.domain, b.tag, b.dispatcher) + if err != nil { + errors.LogWarningInner(context.Background(), err, "failed to create bridge worker") + return nil + } + b.workers = append(b.workers, worker) + } + + return nil +} + +func (b *Bridge) Start() error { + return b.monitorTask.Start() +} + +func (b *Bridge) Close() error { + return b.monitorTask.Close() +} + +type BridgeWorker struct { + Tag string + Worker *mux.ServerWorker + Dispatcher routing.Dispatcher + State Control_State + Timer *signal.ActivityTimer +} + +func NewBridgeWorker(domain string, tag string, d routing.Dispatcher) (*BridgeWorker, error) { + ctx := context.Background() + ctx = session.ContextWithInbound(ctx, &session.Inbound{ + Tag: tag, + }) + link, err := d.Dispatch(ctx, net.Destination{ + Network: net.Network_TCP, + Address: net.DomainAddress(domain), + Port: 0, + }) + if err != nil { + return nil, err + } + + w := &BridgeWorker{ + Dispatcher: d, + Tag: tag, + } + + worker, err := mux.NewServerWorker(context.Background(), w, link) + if err != nil { + return nil, err + } + w.Worker = worker + + terminate := func() { + worker.Close() + } + w.Timer = signal.CancelAfterInactivity(ctx, terminate, 60*time.Second) + return w, nil +} + +func (w *BridgeWorker) Type() interface{} { + return routing.DispatcherType() +} + +func (w *BridgeWorker) Start() error { + return nil +} + +func (w *BridgeWorker) Close() error { + return nil +} + +func (w *BridgeWorker) IsActive() bool { + return w.State == Control_ACTIVE && !w.Worker.Closed() +} + +func (w *BridgeWorker) Closed() bool { + return w.Worker.Closed() +} + +func (w *BridgeWorker) Connections() uint32 { + return w.Worker.ActiveConnections() +} + +func (w *BridgeWorker) handleInternalConn(link *transport.Link) { + reader := link.Reader + for { + mb, err := reader.ReadMultiBuffer() + if err != nil { + if w.Timer != nil { + if w.Closed() { + w.Timer.SetTimeout(0) + } else { + w.Timer.SetTimeout(24 * time.Hour) + } + } + return + } + if w.Timer != nil { + w.Timer.Update() + } + for _, b := range mb { + var ctl Control + if err := proto.Unmarshal(b.Bytes(), &ctl); err != nil { + errors.LogInfoInner(context.Background(), err, "failed to parse proto message") + if w.Timer != nil { + w.Timer.SetTimeout(0) + } + return + } + if ctl.State != w.State { + w.State = ctl.State + } + } + } +} + +func (w *BridgeWorker) Dispatch(ctx context.Context, dest net.Destination) (*transport.Link, error) { + if !isInternalDomain(dest) { + if session.InboundFromContext(ctx) == nil { + ctx = session.ContextWithInbound(ctx, &session.Inbound{ + Tag: w.Tag, + }) + } + return w.Dispatcher.Dispatch(ctx, dest) + } + + opt := []pipe.Option{pipe.WithSizeLimit(16 * 1024)} + uplinkReader, uplinkWriter := pipe.New(opt...) + downlinkReader, downlinkWriter := pipe.New(opt...) + + go w.handleInternalConn(&transport.Link{ + Reader: downlinkReader, + Writer: uplinkWriter, + }) + + return &transport.Link{ + Reader: uplinkReader, + Writer: downlinkWriter, + }, nil +} + +func (w *BridgeWorker) DispatchLink(ctx context.Context, dest net.Destination, link *transport.Link) error { + if !isInternalDomain(dest) { + if session.InboundFromContext(ctx) == nil { + ctx = session.ContextWithInbound(ctx, &session.Inbound{ + Tag: w.Tag, + }) + } + return w.Dispatcher.DispatchLink(ctx, dest, link) + } + w.handleInternalConn(link) + + return nil +} diff --git a/subproject/Xray-core-main/app/reverse/config.go b/subproject/Xray-core-main/app/reverse/config.go new file mode 100644 index 00000000..8ce38c9c --- /dev/null +++ b/subproject/Xray-core-main/app/reverse/config.go @@ -0,0 +1,15 @@ +package reverse + +import ( + "crypto/rand" + "io" + + "github.com/xtls/xray-core/common/dice" +) + +func (c *Control) FillInRandom() { + randomLength := dice.Roll(64) + randomLength++ + c.Random = make([]byte, randomLength) + io.ReadFull(rand.Reader, c.Random) +} diff --git a/subproject/Xray-core-main/app/reverse/config.pb.go b/subproject/Xray-core-main/app/reverse/config.pb.go new file mode 100644 index 00000000..d67af6f5 --- /dev/null +++ b/subproject/Xray-core-main/app/reverse/config.pb.go @@ -0,0 +1,356 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: app/reverse/config.proto + +package reverse + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Control_State int32 + +const ( + Control_ACTIVE Control_State = 0 + Control_DRAIN Control_State = 1 +) + +// Enum value maps for Control_State. +var ( + Control_State_name = map[int32]string{ + 0: "ACTIVE", + 1: "DRAIN", + } + Control_State_value = map[string]int32{ + "ACTIVE": 0, + "DRAIN": 1, + } +) + +func (x Control_State) Enum() *Control_State { + p := new(Control_State) + *p = x + return p +} + +func (x Control_State) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Control_State) Descriptor() protoreflect.EnumDescriptor { + return file_app_reverse_config_proto_enumTypes[0].Descriptor() +} + +func (Control_State) Type() protoreflect.EnumType { + return &file_app_reverse_config_proto_enumTypes[0] +} + +func (x Control_State) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Control_State.Descriptor instead. +func (Control_State) EnumDescriptor() ([]byte, []int) { + return file_app_reverse_config_proto_rawDescGZIP(), []int{0, 0} +} + +type Control struct { + state protoimpl.MessageState `protogen:"open.v1"` + State Control_State `protobuf:"varint,1,opt,name=state,proto3,enum=xray.app.reverse.Control_State" json:"state,omitempty"` + Random []byte `protobuf:"bytes,99,opt,name=random,proto3" json:"random,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Control) Reset() { + *x = Control{} + mi := &file_app_reverse_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Control) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Control) ProtoMessage() {} + +func (x *Control) ProtoReflect() protoreflect.Message { + mi := &file_app_reverse_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Control.ProtoReflect.Descriptor instead. +func (*Control) Descriptor() ([]byte, []int) { + return file_app_reverse_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Control) GetState() Control_State { + if x != nil { + return x.State + } + return Control_ACTIVE +} + +func (x *Control) GetRandom() []byte { + if x != nil { + return x.Random + } + return nil +} + +type BridgeConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + Tag string `protobuf:"bytes,1,opt,name=tag,proto3" json:"tag,omitempty"` + Domain string `protobuf:"bytes,2,opt,name=domain,proto3" json:"domain,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BridgeConfig) Reset() { + *x = BridgeConfig{} + mi := &file_app_reverse_config_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BridgeConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BridgeConfig) ProtoMessage() {} + +func (x *BridgeConfig) ProtoReflect() protoreflect.Message { + mi := &file_app_reverse_config_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BridgeConfig.ProtoReflect.Descriptor instead. +func (*BridgeConfig) Descriptor() ([]byte, []int) { + return file_app_reverse_config_proto_rawDescGZIP(), []int{1} +} + +func (x *BridgeConfig) GetTag() string { + if x != nil { + return x.Tag + } + return "" +} + +func (x *BridgeConfig) GetDomain() string { + if x != nil { + return x.Domain + } + return "" +} + +type PortalConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + Tag string `protobuf:"bytes,1,opt,name=tag,proto3" json:"tag,omitempty"` + Domain string `protobuf:"bytes,2,opt,name=domain,proto3" json:"domain,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PortalConfig) Reset() { + *x = PortalConfig{} + mi := &file_app_reverse_config_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PortalConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PortalConfig) ProtoMessage() {} + +func (x *PortalConfig) ProtoReflect() protoreflect.Message { + mi := &file_app_reverse_config_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PortalConfig.ProtoReflect.Descriptor instead. +func (*PortalConfig) Descriptor() ([]byte, []int) { + return file_app_reverse_config_proto_rawDescGZIP(), []int{2} +} + +func (x *PortalConfig) GetTag() string { + if x != nil { + return x.Tag + } + return "" +} + +func (x *PortalConfig) GetDomain() string { + if x != nil { + return x.Domain + } + return "" +} + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + BridgeConfig []*BridgeConfig `protobuf:"bytes,1,rep,name=bridge_config,json=bridgeConfig,proto3" json:"bridge_config,omitempty"` + PortalConfig []*PortalConfig `protobuf:"bytes,2,rep,name=portal_config,json=portalConfig,proto3" json:"portal_config,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_app_reverse_config_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_app_reverse_config_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_app_reverse_config_proto_rawDescGZIP(), []int{3} +} + +func (x *Config) GetBridgeConfig() []*BridgeConfig { + if x != nil { + return x.BridgeConfig + } + return nil +} + +func (x *Config) GetPortalConfig() []*PortalConfig { + if x != nil { + return x.PortalConfig + } + return nil +} + +var File_app_reverse_config_proto protoreflect.FileDescriptor + +const file_app_reverse_config_proto_rawDesc = "" + + "\n" + + "\x18app/reverse/config.proto\x12\x10xray.app.reverse\"x\n" + + "\aControl\x125\n" + + "\x05state\x18\x01 \x01(\x0e2\x1f.xray.app.reverse.Control.StateR\x05state\x12\x16\n" + + "\x06random\x18c \x01(\fR\x06random\"\x1e\n" + + "\x05State\x12\n" + + "\n" + + "\x06ACTIVE\x10\x00\x12\t\n" + + "\x05DRAIN\x10\x01\"8\n" + + "\fBridgeConfig\x12\x10\n" + + "\x03tag\x18\x01 \x01(\tR\x03tag\x12\x16\n" + + "\x06domain\x18\x02 \x01(\tR\x06domain\"8\n" + + "\fPortalConfig\x12\x10\n" + + "\x03tag\x18\x01 \x01(\tR\x03tag\x12\x16\n" + + "\x06domain\x18\x02 \x01(\tR\x06domain\"\x92\x01\n" + + "\x06Config\x12C\n" + + "\rbridge_config\x18\x01 \x03(\v2\x1e.xray.app.reverse.BridgeConfigR\fbridgeConfig\x12C\n" + + "\rportal_config\x18\x02 \x03(\v2\x1e.xray.app.reverse.PortalConfigR\fportalConfigBV\n" + + "\x16com.xray.proxy.reverseP\x01Z%github.com/xtls/xray-core/app/reverse\xaa\x02\x12Xray.Proxy.Reverseb\x06proto3" + +var ( + file_app_reverse_config_proto_rawDescOnce sync.Once + file_app_reverse_config_proto_rawDescData []byte +) + +func file_app_reverse_config_proto_rawDescGZIP() []byte { + file_app_reverse_config_proto_rawDescOnce.Do(func() { + file_app_reverse_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_app_reverse_config_proto_rawDesc), len(file_app_reverse_config_proto_rawDesc))) + }) + return file_app_reverse_config_proto_rawDescData +} + +var file_app_reverse_config_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_app_reverse_config_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_app_reverse_config_proto_goTypes = []any{ + (Control_State)(0), // 0: xray.app.reverse.Control.State + (*Control)(nil), // 1: xray.app.reverse.Control + (*BridgeConfig)(nil), // 2: xray.app.reverse.BridgeConfig + (*PortalConfig)(nil), // 3: xray.app.reverse.PortalConfig + (*Config)(nil), // 4: xray.app.reverse.Config +} +var file_app_reverse_config_proto_depIdxs = []int32{ + 0, // 0: xray.app.reverse.Control.state:type_name -> xray.app.reverse.Control.State + 2, // 1: xray.app.reverse.Config.bridge_config:type_name -> xray.app.reverse.BridgeConfig + 3, // 2: xray.app.reverse.Config.portal_config:type_name -> xray.app.reverse.PortalConfig + 3, // [3:3] is the sub-list for method output_type + 3, // [3:3] is the sub-list for method input_type + 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_reverse_config_proto_init() } +func file_app_reverse_config_proto_init() { + if File_app_reverse_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_app_reverse_config_proto_rawDesc), len(file_app_reverse_config_proto_rawDesc)), + NumEnums: 1, + NumMessages: 4, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_app_reverse_config_proto_goTypes, + DependencyIndexes: file_app_reverse_config_proto_depIdxs, + EnumInfos: file_app_reverse_config_proto_enumTypes, + MessageInfos: file_app_reverse_config_proto_msgTypes, + }.Build() + File_app_reverse_config_proto = out.File + file_app_reverse_config_proto_goTypes = nil + file_app_reverse_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/app/reverse/config.proto b/subproject/Xray-core-main/app/reverse/config.proto new file mode 100644 index 00000000..35bf74be --- /dev/null +++ b/subproject/Xray-core-main/app/reverse/config.proto @@ -0,0 +1,32 @@ +syntax = "proto3"; + +package xray.app.reverse; +option csharp_namespace = "Xray.Proxy.Reverse"; +option go_package = "github.com/xtls/xray-core/app/reverse"; +option java_package = "com.xray.proxy.reverse"; +option java_multiple_files = true; + +message Control { + enum State { + ACTIVE = 0; + DRAIN = 1; + } + + State state = 1; + bytes random = 99; +} + +message BridgeConfig { + string tag = 1; + string domain = 2; +} + +message PortalConfig { + string tag = 1; + string domain = 2; +} + +message Config { + repeated BridgeConfig bridge_config = 1; + repeated PortalConfig portal_config = 2; +} diff --git a/subproject/Xray-core-main/app/reverse/portal.go b/subproject/Xray-core-main/app/reverse/portal.go new file mode 100644 index 00000000..7e3f2caf --- /dev/null +++ b/subproject/Xray-core-main/app/reverse/portal.go @@ -0,0 +1,308 @@ +package reverse + +import ( + "context" + "sync" + "time" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/mux" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/serial" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/common/signal" + "github.com/xtls/xray-core/common/task" + "github.com/xtls/xray-core/features/outbound" + "github.com/xtls/xray-core/transport" + "github.com/xtls/xray-core/transport/pipe" + "google.golang.org/protobuf/proto" +) + +type Portal struct { + ohm outbound.Manager + tag string + domain string + picker *StaticMuxPicker + client *mux.ClientManager +} + +func NewPortal(config *PortalConfig, ohm outbound.Manager) (*Portal, error) { + if config.Tag == "" { + return nil, errors.New("portal tag is empty") + } + + if config.Domain == "" { + return nil, errors.New("portal domain is empty") + } + + picker, err := NewStaticMuxPicker() + if err != nil { + return nil, err + } + + return &Portal{ + ohm: ohm, + tag: config.Tag, + domain: config.Domain, + picker: picker, + client: &mux.ClientManager{ + Picker: picker, + }, + }, nil +} + +func (p *Portal) Start() error { + return p.ohm.AddHandler(context.Background(), &Outbound{ + portal: p, + tag: p.tag, + }) +} + +func (p *Portal) Close() error { + return p.ohm.RemoveHandler(context.Background(), p.tag) +} + +func (p *Portal) HandleConnection(ctx context.Context, link *transport.Link) error { + outbounds := session.OutboundsFromContext(ctx) + ob := outbounds[len(outbounds)-1] + if ob == nil { + return errors.New("outbound metadata not found").AtError() + } + + if isDomain(ob.Target, p.domain) { + muxClient, err := mux.NewClientWorker(*link, mux.ClientStrategy{}) + if err != nil { + return errors.New("failed to create mux client worker").Base(err).AtWarning() + } + + worker, err := NewPortalWorker(muxClient) + if err != nil { + return errors.New("failed to create portal worker").Base(err) + } + + p.picker.AddWorker(worker) + + if _, ok := link.Reader.(*pipe.Reader); !ok { + select { + case <-ctx.Done(): + case <-muxClient.WaitClosed(): + } + } + return nil + } + + if ob.Target.Network == net.Network_UDP && ob.OriginalTarget.Address != nil && ob.OriginalTarget.Address != ob.Target.Address { + link.Reader = &buf.EndpointOverrideReader{Reader: link.Reader, Dest: ob.Target.Address, OriginalDest: ob.OriginalTarget.Address} + link.Writer = &buf.EndpointOverrideWriter{Writer: link.Writer, Dest: ob.Target.Address, OriginalDest: ob.OriginalTarget.Address} + } + + return p.client.Dispatch(ctx, link) +} + +type Outbound struct { + portal *Portal + tag string +} + +func (o *Outbound) Tag() string { + return o.tag +} + +func (o *Outbound) Dispatch(ctx context.Context, link *transport.Link) { + if err := o.portal.HandleConnection(ctx, link); err != nil { + errors.LogInfoInner(ctx, err, "failed to process reverse connection") + common.Interrupt(link.Writer) + common.Interrupt(link.Reader) + } +} + +func (o *Outbound) Start() error { + return nil +} + +func (o *Outbound) Close() error { + return nil +} + +// SenderSettings implements outbound.Handler. +func (o *Outbound) SenderSettings() *serial.TypedMessage { + return nil +} + +// ProxySettings implements outbound.Handler. +func (o *Outbound) ProxySettings() *serial.TypedMessage { + return nil +} + +type StaticMuxPicker struct { + access sync.Mutex + workers []*PortalWorker + cTask *task.Periodic +} + +func NewStaticMuxPicker() (*StaticMuxPicker, error) { + p := &StaticMuxPicker{} + p.cTask = &task.Periodic{ + Execute: p.cleanup, + Interval: time.Second * 30, + } + p.cTask.Start() + return p, nil +} + +func (p *StaticMuxPicker) cleanup() error { + p.access.Lock() + defer p.access.Unlock() + + var activeWorkers []*PortalWorker + for _, w := range p.workers { + if !w.Closed() { + activeWorkers = append(activeWorkers, w) + } else { + w.timer.SetTimeout(0) + } + } + + if len(activeWorkers) != len(p.workers) { + p.workers = activeWorkers + } + + return nil +} + +func (p *StaticMuxPicker) PickAvailable() (*mux.ClientWorker, error) { + p.access.Lock() + defer p.access.Unlock() + + if len(p.workers) == 0 { + return nil, errors.New("empty worker list") + } + + var minIdx int = -1 + var minConn uint32 = 9999 + for i, w := range p.workers { + if w.draining { + continue + } + if w.IsFull() { + continue + } + if w.client.ActiveConnections() < minConn { + minConn = w.client.ActiveConnections() + minIdx = i + } + } + + if minIdx == -1 { + for i, w := range p.workers { + if w.IsFull() { + continue + } + if w.client.ActiveConnections() < minConn { + minConn = w.client.ActiveConnections() + minIdx = i + } + } + } + + if minIdx != -1 { + return p.workers[minIdx].client, nil + } + + return nil, errors.New("no mux client worker available") +} + +func (p *StaticMuxPicker) AddWorker(worker *PortalWorker) { + p.access.Lock() + defer p.access.Unlock() + + p.workers = append(p.workers, worker) +} + +type PortalWorker struct { + client *mux.ClientWorker + control *task.Periodic + writer buf.Writer + reader buf.Reader + draining bool + counter uint32 + timer *signal.ActivityTimer +} + +func NewPortalWorker(client *mux.ClientWorker) (*PortalWorker, error) { + opt := []pipe.Option{pipe.WithSizeLimit(16 * 1024)} + uplinkReader, uplinkWriter := pipe.New(opt...) + downlinkReader, downlinkWriter := pipe.New(opt...) + + ctx := context.Background() + outbounds := []*session.Outbound{{ + Target: net.UDPDestination(net.DomainAddress(internalDomain), 0), + }} + ctx = session.ContextWithOutbounds(ctx, outbounds) + f := client.Dispatch(ctx, &transport.Link{ + Reader: uplinkReader, + Writer: downlinkWriter, + }) + if !f { + return nil, errors.New("unable to dispatch control connection") + } + terminate := func() { + client.Close() + } + w := &PortalWorker{ + client: client, + reader: downlinkReader, + writer: uplinkWriter, + timer: signal.CancelAfterInactivity(ctx, terminate, 24*time.Hour), // // prevent leak + } + w.control = &task.Periodic{ + Execute: w.heartbeat, + Interval: time.Second * 2, + } + w.control.Start() + return w, nil +} + +func (w *PortalWorker) heartbeat() error { + if w.Closed() { + return errors.New("client worker stopped") + } + + if w.draining || w.writer == nil { + return errors.New("already disposed") + } + + msg := &Control{} + msg.FillInRandom() + + if w.client.TotalConnections() > 256 { + w.draining = true + msg.State = Control_DRAIN + + defer func() { + common.Close(w.writer) + common.Interrupt(w.reader) + w.writer = nil + }() + } + + w.counter = (w.counter + 1) % 5 + if w.draining || w.counter == 1 { + b, err := proto.Marshal(msg) + common.Must(err) + mb := buf.MergeBytes(nil, b) + w.timer.Update() + return w.writer.WriteMultiBuffer(mb) + } + return nil +} + +func (w *PortalWorker) IsFull() bool { + return w.client.IsFull() +} + +func (w *PortalWorker) Closed() bool { + return w.client.Closed() +} diff --git a/subproject/Xray-core-main/app/reverse/portal_test.go b/subproject/Xray-core-main/app/reverse/portal_test.go new file mode 100644 index 00000000..0d42300d --- /dev/null +++ b/subproject/Xray-core-main/app/reverse/portal_test.go @@ -0,0 +1,20 @@ +package reverse_test + +import ( + "testing" + + "github.com/xtls/xray-core/app/reverse" + "github.com/xtls/xray-core/common" +) + +func TestStaticPickerEmpty(t *testing.T) { + picker, err := reverse.NewStaticMuxPicker() + common.Must(err) + worker, err := picker.PickAvailable() + if err == nil { + t.Error("expected error, but nil") + } + if worker != nil { + t.Error("expected nil worker, but not nil") + } +} diff --git a/subproject/Xray-core-main/app/reverse/reverse.go b/subproject/Xray-core-main/app/reverse/reverse.go new file mode 100644 index 00000000..dcd24c57 --- /dev/null +++ b/subproject/Xray-core-main/app/reverse/reverse.go @@ -0,0 +1,94 @@ +package reverse + +import ( + "context" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + core "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/features/outbound" + "github.com/xtls/xray-core/features/routing" +) + +const ( + internalDomain = "reverse" +) + +func isDomain(dest net.Destination, domain string) bool { + return dest.Address.Family().IsDomain() && dest.Address.Domain() == domain +} + +func isInternalDomain(dest net.Destination) bool { + return isDomain(dest, internalDomain) +} + +func init() { + common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + r := new(Reverse) + if err := core.RequireFeatures(ctx, func(d routing.Dispatcher, om outbound.Manager) error { + return r.Init(config.(*Config), d, om) + }); err != nil { + return nil, err + } + return r, nil + })) +} + +type Reverse struct { + bridges []*Bridge + portals []*Portal +} + +func (r *Reverse) Init(config *Config, d routing.Dispatcher, ohm outbound.Manager) error { + for _, bConfig := range config.BridgeConfig { + b, err := NewBridge(bConfig, d) + if err != nil { + return err + } + r.bridges = append(r.bridges, b) + } + + for _, pConfig := range config.PortalConfig { + p, err := NewPortal(pConfig, ohm) + if err != nil { + return err + } + r.portals = append(r.portals, p) + } + + return nil +} + +func (r *Reverse) Type() interface{} { + return (*Reverse)(nil) +} + +func (r *Reverse) Start() error { + for _, b := range r.bridges { + if err := b.Start(); err != nil { + return err + } + } + + for _, p := range r.portals { + if err := p.Start(); err != nil { + return err + } + } + + return nil +} + +func (r *Reverse) Close() error { + var errs []error + for _, b := range r.bridges { + errs = append(errs, b.Close()) + } + + for _, p := range r.portals { + errs = append(errs, p.Close()) + } + + return errors.Combine(errs...) +} diff --git a/subproject/Xray-core-main/app/router/balancing.go b/subproject/Xray-core-main/app/router/balancing.go new file mode 100644 index 00000000..5f8cb1c2 --- /dev/null +++ b/subproject/Xray-core-main/app/router/balancing.go @@ -0,0 +1,167 @@ +package router + +import ( + "context" + sync "sync" + + "github.com/xtls/xray-core/app/observatory" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/features/extension" + "github.com/xtls/xray-core/features/outbound" +) + +type BalancingStrategy interface { + PickOutbound([]string) string +} + +type BalancingPrincipleTarget interface { + GetPrincipleTarget([]string) []string +} + +type RoundRobinStrategy struct { + FallbackTag string + + ctx context.Context + observatory extension.Observatory + mu sync.Mutex + index int +} + +func (s *RoundRobinStrategy) InjectContext(ctx context.Context) { + s.ctx = ctx + if len(s.FallbackTag) > 0 { + common.Must(core.RequireFeatures(s.ctx, func(observatory extension.Observatory) error { + s.observatory = observatory + return nil + })) + } +} + +func (s *RoundRobinStrategy) GetPrincipleTarget(strings []string) []string { + return strings +} + +func (s *RoundRobinStrategy) PickOutbound(tags []string) string { + if s.observatory != nil { + observeReport, err := s.observatory.GetObservation(s.ctx) + if err == nil { + aliveTags := make([]string, 0) + if result, ok := observeReport.(*observatory.ObservationResult); ok { + status := result.Status + statusMap := make(map[string]*observatory.OutboundStatus) + for _, outboundStatus := range status { + statusMap[outboundStatus.OutboundTag] = outboundStatus + } + for _, candidate := range tags { + if outboundStatus, found := statusMap[candidate]; found { + if outboundStatus.Alive { + aliveTags = append(aliveTags, candidate) + } + } else { + // unfound candidate is considered alive + aliveTags = append(aliveTags, candidate) + } + } + tags = aliveTags + } + } + } + + n := len(tags) + if n == 0 { + // goes to fallbackTag + return "" + } + + s.mu.Lock() + defer s.mu.Unlock() + tag := tags[s.index%n] + s.index = (s.index + 1) % n + return tag +} + +type Balancer struct { + selectors []string + strategy BalancingStrategy + ohm outbound.Manager + fallbackTag string + + override override +} + +// PickOutbound picks the tag of a outbound +func (b *Balancer) PickOutbound() (string, error) { + candidates, err := b.SelectOutbounds() + if err != nil { + if b.fallbackTag != "" { + errors.LogInfo(context.Background(), "fallback to [", b.fallbackTag, "], due to error: ", err) + return b.fallbackTag, nil + } + return "", err + } + var tag string + if o := b.override.Get(); o != "" { + tag = o + } else { + tag = b.strategy.PickOutbound(candidates) + } + if tag == "" { + if b.fallbackTag != "" { + errors.LogInfo(context.Background(), "fallback to [", b.fallbackTag, "], due to empty tag returned") + return b.fallbackTag, nil + } + // will use default handler + return "", errors.New("balancing strategy returns empty tag") + } + return tag, nil +} + +func (b *Balancer) InjectContext(ctx context.Context) { + if contextReceiver, ok := b.strategy.(extension.ContextReceiver); ok { + contextReceiver.InjectContext(ctx) + } +} + +// SelectOutbounds select outbounds with selectors of the Balancer +func (b *Balancer) SelectOutbounds() ([]string, error) { + hs, ok := b.ohm.(outbound.HandlerSelector) + if !ok { + return nil, errors.New("outbound.Manager is not a HandlerSelector") + } + tags := hs.Select(b.selectors) + return tags, nil +} + +// GetPrincipleTarget implements routing.BalancerPrincipleTarget +func (r *Router) GetPrincipleTarget(tag string) ([]string, error) { + if b, ok := r.balancers[tag]; ok { + if s, ok := b.strategy.(BalancingPrincipleTarget); ok { + candidates, err := b.SelectOutbounds() + if err != nil { + return nil, errors.New("unable to select outbounds").Base(err) + } + return s.GetPrincipleTarget(candidates), nil + } + return nil, errors.New("unsupported GetPrincipleTarget") + } + return nil, errors.New("cannot find tag") +} + +// SetOverrideTarget implements routing.BalancerOverrider +func (r *Router) SetOverrideTarget(tag, target string) error { + if b, ok := r.balancers[tag]; ok { + b.override.Put(target) + return nil + } + return errors.New("cannot find tag") +} + +// GetOverrideTarget implements routing.BalancerOverrider +func (r *Router) GetOverrideTarget(tag string) (string, error) { + if b, ok := r.balancers[tag]; ok { + return b.override.Get(), nil + } + return "", errors.New("cannot find tag") +} diff --git a/subproject/Xray-core-main/app/router/balancing_override.go b/subproject/Xray-core-main/app/router/balancing_override.go new file mode 100644 index 00000000..96c0aea1 --- /dev/null +++ b/subproject/Xray-core-main/app/router/balancing_override.go @@ -0,0 +1,52 @@ +package router + +import ( + sync "sync" + + "github.com/xtls/xray-core/common/errors" +) + +func (r *Router) OverrideBalancer(balancer string, target string) error { + var b *Balancer + for tag, bl := range r.balancers { + if tag == balancer { + b = bl + break + } + } + if b == nil { + return errors.New("balancer '", balancer, "' not found") + } + b.override.Put(target) + return nil +} + +type overrideSettings struct { + target string +} + +type override struct { + access sync.RWMutex + settings overrideSettings +} + +// Get gets the override settings +func (o *override) Get() string { + o.access.RLock() + defer o.access.RUnlock() + return o.settings.target +} + +// Put updates the override settings +func (o *override) Put(target string) { + o.access.Lock() + defer o.access.Unlock() + o.settings.target = target +} + +// Clear clears the override settings +func (o *override) Clear() { + o.access.Lock() + defer o.access.Unlock() + o.settings.target = "" +} diff --git a/subproject/Xray-core-main/app/router/command/command.go b/subproject/Xray-core-main/app/router/command/command.go new file mode 100644 index 00000000..c0974553 --- /dev/null +++ b/subproject/Xray-core-main/app/router/command/command.go @@ -0,0 +1,161 @@ +package command + +import ( + "context" + "time" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/features/routing" + "github.com/xtls/xray-core/features/stats" + "google.golang.org/grpc" +) + +// routingServer is an implementation of RoutingService. +type routingServer struct { + router routing.Router + routingStats stats.Channel +} + +func (s *routingServer) GetBalancerInfo(ctx context.Context, request *GetBalancerInfoRequest) (*GetBalancerInfoResponse, error) { + var ret GetBalancerInfoResponse + ret.Balancer = &BalancerMsg{} + if bo, ok := s.router.(routing.BalancerOverrider); ok { + { + res, err := bo.GetOverrideTarget(request.GetTag()) + if err != nil { + return nil, err + } + ret.Balancer.Override = &OverrideInfo{ + Target: res, + } + } + } + + if pt, ok := s.router.(routing.BalancerPrincipleTarget); ok { + { + res, err := pt.GetPrincipleTarget(request.GetTag()) + if err != nil { + errors.LogInfoInner(ctx, err, "unable to obtain principle target") + } else { + ret.Balancer.PrincipleTarget = &PrincipleTargetInfo{Tag: res} + } + } + } + return &ret, nil +} + +func (s *routingServer) OverrideBalancerTarget(ctx context.Context, request *OverrideBalancerTargetRequest) (*OverrideBalancerTargetResponse, error) { + if bo, ok := s.router.(routing.BalancerOverrider); ok { + return &OverrideBalancerTargetResponse{}, bo.SetOverrideTarget(request.BalancerTag, request.Target) + } + return nil, errors.New("unsupported router implementation") +} + +func (s *routingServer) AddRule(ctx context.Context, request *AddRuleRequest) (*AddRuleResponse, error) { + if bo, ok := s.router.(routing.Router); ok { + return &AddRuleResponse{}, bo.AddRule(request.Config, request.ShouldAppend) + } + return nil, errors.New("unsupported router implementation") + +} + +func (s *routingServer) RemoveRule(ctx context.Context, request *RemoveRuleRequest) (*RemoveRuleResponse, error) { + if bo, ok := s.router.(routing.Router); ok { + return &RemoveRuleResponse{}, bo.RemoveRule(request.RuleTag) + } + return nil, errors.New("unsupported router implementation") +} + +func (s *routingServer) ListRule(ctx context.Context, request *ListRuleRequest) (*ListRuleResponse, error) { + if bo, ok := s.router.(routing.Router); ok { + response := &ListRuleResponse{} + for _, v := range bo.ListRule() { + response.Rules = append(response.Rules, &ListRuleItem{ + Tag: v.GetOutboundTag(), + RuleTag: v.GetRuleTag(), + }) + } + return response, nil + } + return nil, errors.New("unsupported router implementation") +} + +// NewRoutingServer creates a statistics service with statistics manager. +func NewRoutingServer(router routing.Router, routingStats stats.Channel) RoutingServiceServer { + return &routingServer{ + router: router, + routingStats: routingStats, + } +} + +func (s *routingServer) TestRoute(ctx context.Context, request *TestRouteRequest) (*RoutingContext, error) { + if request.RoutingContext == nil { + return nil, errors.New("Invalid routing request.") + } + route, err := s.router.PickRoute(AsRoutingContext(request.RoutingContext)) + if err != nil { + return nil, err + } + if request.PublishResult && s.routingStats != nil { + ctx, _ := context.WithTimeout(context.Background(), 4*time.Second) + s.routingStats.Publish(ctx, route) + } + return AsProtobufMessage(request.FieldSelectors)(route), nil +} + +func (s *routingServer) SubscribeRoutingStats(request *SubscribeRoutingStatsRequest, stream RoutingService_SubscribeRoutingStatsServer) error { + if s.routingStats == nil { + return errors.New("Routing statistics not enabled.") + } + genMessage := AsProtobufMessage(request.FieldSelectors) + subscriber, err := stats.SubscribeRunnableChannel(s.routingStats) + if err != nil { + return err + } + defer stats.UnsubscribeClosableChannel(s.routingStats, subscriber) + for { + select { + case value, ok := <-subscriber: + if !ok { + return errors.New("Upstream closed the subscriber channel.") + } + route, ok := value.(routing.Route) + if !ok { + return errors.New("Upstream sent malformed statistics.") + } + err := stream.Send(genMessage(route)) + if err != nil { + return err + } + case <-stream.Context().Done(): + return stream.Context().Err() + } + } +} + +func (s *routingServer) mustEmbedUnimplementedRoutingServiceServer() {} + +type service struct { + v *core.Instance +} + +func (s *service) Register(server *grpc.Server) { + common.Must(s.v.RequireFeatures(func(router routing.Router, stats stats.Manager) { + rs := NewRoutingServer(router, nil) + RegisterRoutingServiceServer(server, rs) + + // For compatibility purposes + vCoreDesc := RoutingService_ServiceDesc + vCoreDesc.ServiceName = "v2ray.core.app.router.command.RoutingService" + server.RegisterService(&vCoreDesc, rs) + }, false)) +} + +func init() { + common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, cfg interface{}) (interface{}, error) { + s := core.MustFromContext(ctx) + return &service{v: s}, nil + })) +} diff --git a/subproject/Xray-core-main/app/router/command/command.pb.go b/subproject/Xray-core-main/app/router/command/command.pb.go new file mode 100644 index 00000000..db55b17e --- /dev/null +++ b/subproject/Xray-core-main/app/router/command/command.pb.go @@ -0,0 +1,1135 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: app/router/command/command.proto + +package command + +import ( + net "github.com/xtls/xray-core/common/net" + serial "github.com/xtls/xray-core/common/serial" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// RoutingContext is the context with information relative to routing process. +// It conforms to the structure of xray.features.routing.Context and +// xray.features.routing.Route. +type RoutingContext struct { + state protoimpl.MessageState `protogen:"open.v1"` + InboundTag string `protobuf:"bytes,1,opt,name=InboundTag,proto3" json:"InboundTag,omitempty"` + Network net.Network `protobuf:"varint,2,opt,name=Network,proto3,enum=xray.common.net.Network" json:"Network,omitempty"` + SourceIPs [][]byte `protobuf:"bytes,3,rep,name=SourceIPs,proto3" json:"SourceIPs,omitempty"` + TargetIPs [][]byte `protobuf:"bytes,4,rep,name=TargetIPs,proto3" json:"TargetIPs,omitempty"` + SourcePort uint32 `protobuf:"varint,5,opt,name=SourcePort,proto3" json:"SourcePort,omitempty"` + TargetPort uint32 `protobuf:"varint,6,opt,name=TargetPort,proto3" json:"TargetPort,omitempty"` + TargetDomain string `protobuf:"bytes,7,opt,name=TargetDomain,proto3" json:"TargetDomain,omitempty"` + Protocol string `protobuf:"bytes,8,opt,name=Protocol,proto3" json:"Protocol,omitempty"` + User string `protobuf:"bytes,9,opt,name=User,proto3" json:"User,omitempty"` + Attributes map[string]string `protobuf:"bytes,10,rep,name=Attributes,proto3" json:"Attributes,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + OutboundGroupTags []string `protobuf:"bytes,11,rep,name=OutboundGroupTags,proto3" json:"OutboundGroupTags,omitempty"` + OutboundTag string `protobuf:"bytes,12,opt,name=OutboundTag,proto3" json:"OutboundTag,omitempty"` + LocalIPs [][]byte `protobuf:"bytes,13,rep,name=LocalIPs,proto3" json:"LocalIPs,omitempty"` + LocalPort uint32 `protobuf:"varint,14,opt,name=LocalPort,proto3" json:"LocalPort,omitempty"` + VlessRoute uint32 `protobuf:"varint,15,opt,name=VlessRoute,proto3" json:"VlessRoute,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RoutingContext) Reset() { + *x = RoutingContext{} + mi := &file_app_router_command_command_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RoutingContext) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RoutingContext) ProtoMessage() {} + +func (x *RoutingContext) ProtoReflect() protoreflect.Message { + mi := &file_app_router_command_command_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RoutingContext.ProtoReflect.Descriptor instead. +func (*RoutingContext) Descriptor() ([]byte, []int) { + return file_app_router_command_command_proto_rawDescGZIP(), []int{0} +} + +func (x *RoutingContext) GetInboundTag() string { + if x != nil { + return x.InboundTag + } + return "" +} + +func (x *RoutingContext) GetNetwork() net.Network { + if x != nil { + return x.Network + } + return net.Network(0) +} + +func (x *RoutingContext) GetSourceIPs() [][]byte { + if x != nil { + return x.SourceIPs + } + return nil +} + +func (x *RoutingContext) GetTargetIPs() [][]byte { + if x != nil { + return x.TargetIPs + } + return nil +} + +func (x *RoutingContext) GetSourcePort() uint32 { + if x != nil { + return x.SourcePort + } + return 0 +} + +func (x *RoutingContext) GetTargetPort() uint32 { + if x != nil { + return x.TargetPort + } + return 0 +} + +func (x *RoutingContext) GetTargetDomain() string { + if x != nil { + return x.TargetDomain + } + return "" +} + +func (x *RoutingContext) GetProtocol() string { + if x != nil { + return x.Protocol + } + return "" +} + +func (x *RoutingContext) GetUser() string { + if x != nil { + return x.User + } + return "" +} + +func (x *RoutingContext) GetAttributes() map[string]string { + if x != nil { + return x.Attributes + } + return nil +} + +func (x *RoutingContext) GetOutboundGroupTags() []string { + if x != nil { + return x.OutboundGroupTags + } + return nil +} + +func (x *RoutingContext) GetOutboundTag() string { + if x != nil { + return x.OutboundTag + } + return "" +} + +func (x *RoutingContext) GetLocalIPs() [][]byte { + if x != nil { + return x.LocalIPs + } + return nil +} + +func (x *RoutingContext) GetLocalPort() uint32 { + if x != nil { + return x.LocalPort + } + return 0 +} + +func (x *RoutingContext) GetVlessRoute() uint32 { + if x != nil { + return x.VlessRoute + } + return 0 +} + +// SubscribeRoutingStatsRequest subscribes to routing statistics channel if +// opened by xray-core. +// * FieldSelectors selects a subset of fields in routing statistics to return. +// Valid selectors: +// - inbound: Selects connection's inbound tag. +// - network: Selects connection's network. +// - ip: Equivalent as "ip_source" and "ip_target", selects both source and +// target IP. +// - port: Equivalent as "port_source" and "port_target", selects both source +// and target port. +// - domain: Selects target domain. +// - protocol: Select connection's protocol. +// - user: Select connection's inbound user email. +// - attributes: Select connection's additional attributes. +// - outbound: Equivalent as "outbound" and "outbound_group", select both +// outbound tag and outbound group tags. +// +// * If FieldSelectors is left empty, all fields will be returned. +type SubscribeRoutingStatsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + FieldSelectors []string `protobuf:"bytes,1,rep,name=FieldSelectors,proto3" json:"FieldSelectors,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SubscribeRoutingStatsRequest) Reset() { + *x = SubscribeRoutingStatsRequest{} + mi := &file_app_router_command_command_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SubscribeRoutingStatsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SubscribeRoutingStatsRequest) ProtoMessage() {} + +func (x *SubscribeRoutingStatsRequest) ProtoReflect() protoreflect.Message { + mi := &file_app_router_command_command_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SubscribeRoutingStatsRequest.ProtoReflect.Descriptor instead. +func (*SubscribeRoutingStatsRequest) Descriptor() ([]byte, []int) { + return file_app_router_command_command_proto_rawDescGZIP(), []int{1} +} + +func (x *SubscribeRoutingStatsRequest) GetFieldSelectors() []string { + if x != nil { + return x.FieldSelectors + } + return nil +} + +// TestRouteRequest manually tests a routing result according to the routing +// context message. +// * RoutingContext is the routing message without outbound information. +// * FieldSelectors selects the fields to return in the routing result. All +// fields are returned if left empty. +// * PublishResult broadcasts the routing result to routing statistics channel +// if set true. +type TestRouteRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + RoutingContext *RoutingContext `protobuf:"bytes,1,opt,name=RoutingContext,proto3" json:"RoutingContext,omitempty"` + FieldSelectors []string `protobuf:"bytes,2,rep,name=FieldSelectors,proto3" json:"FieldSelectors,omitempty"` + PublishResult bool `protobuf:"varint,3,opt,name=PublishResult,proto3" json:"PublishResult,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TestRouteRequest) Reset() { + *x = TestRouteRequest{} + mi := &file_app_router_command_command_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TestRouteRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TestRouteRequest) ProtoMessage() {} + +func (x *TestRouteRequest) ProtoReflect() protoreflect.Message { + mi := &file_app_router_command_command_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TestRouteRequest.ProtoReflect.Descriptor instead. +func (*TestRouteRequest) Descriptor() ([]byte, []int) { + return file_app_router_command_command_proto_rawDescGZIP(), []int{2} +} + +func (x *TestRouteRequest) GetRoutingContext() *RoutingContext { + if x != nil { + return x.RoutingContext + } + return nil +} + +func (x *TestRouteRequest) GetFieldSelectors() []string { + if x != nil { + return x.FieldSelectors + } + return nil +} + +func (x *TestRouteRequest) GetPublishResult() bool { + if x != nil { + return x.PublishResult + } + return false +} + +type PrincipleTargetInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + Tag []string `protobuf:"bytes,1,rep,name=tag,proto3" json:"tag,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PrincipleTargetInfo) Reset() { + *x = PrincipleTargetInfo{} + mi := &file_app_router_command_command_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PrincipleTargetInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PrincipleTargetInfo) ProtoMessage() {} + +func (x *PrincipleTargetInfo) ProtoReflect() protoreflect.Message { + mi := &file_app_router_command_command_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PrincipleTargetInfo.ProtoReflect.Descriptor instead. +func (*PrincipleTargetInfo) Descriptor() ([]byte, []int) { + return file_app_router_command_command_proto_rawDescGZIP(), []int{3} +} + +func (x *PrincipleTargetInfo) GetTag() []string { + if x != nil { + return x.Tag + } + return nil +} + +type OverrideInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + Target string `protobuf:"bytes,2,opt,name=target,proto3" json:"target,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OverrideInfo) Reset() { + *x = OverrideInfo{} + mi := &file_app_router_command_command_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OverrideInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OverrideInfo) ProtoMessage() {} + +func (x *OverrideInfo) ProtoReflect() protoreflect.Message { + mi := &file_app_router_command_command_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OverrideInfo.ProtoReflect.Descriptor instead. +func (*OverrideInfo) Descriptor() ([]byte, []int) { + return file_app_router_command_command_proto_rawDescGZIP(), []int{4} +} + +func (x *OverrideInfo) GetTarget() string { + if x != nil { + return x.Target + } + return "" +} + +type BalancerMsg struct { + state protoimpl.MessageState `protogen:"open.v1"` + Override *OverrideInfo `protobuf:"bytes,5,opt,name=override,proto3" json:"override,omitempty"` + PrincipleTarget *PrincipleTargetInfo `protobuf:"bytes,6,opt,name=principle_target,json=principleTarget,proto3" json:"principle_target,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BalancerMsg) Reset() { + *x = BalancerMsg{} + mi := &file_app_router_command_command_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BalancerMsg) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BalancerMsg) ProtoMessage() {} + +func (x *BalancerMsg) ProtoReflect() protoreflect.Message { + mi := &file_app_router_command_command_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BalancerMsg.ProtoReflect.Descriptor instead. +func (*BalancerMsg) Descriptor() ([]byte, []int) { + return file_app_router_command_command_proto_rawDescGZIP(), []int{5} +} + +func (x *BalancerMsg) GetOverride() *OverrideInfo { + if x != nil { + return x.Override + } + return nil +} + +func (x *BalancerMsg) GetPrincipleTarget() *PrincipleTargetInfo { + if x != nil { + return x.PrincipleTarget + } + return nil +} + +type GetBalancerInfoRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Tag string `protobuf:"bytes,1,opt,name=tag,proto3" json:"tag,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetBalancerInfoRequest) Reset() { + *x = GetBalancerInfoRequest{} + mi := &file_app_router_command_command_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetBalancerInfoRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetBalancerInfoRequest) ProtoMessage() {} + +func (x *GetBalancerInfoRequest) ProtoReflect() protoreflect.Message { + mi := &file_app_router_command_command_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetBalancerInfoRequest.ProtoReflect.Descriptor instead. +func (*GetBalancerInfoRequest) Descriptor() ([]byte, []int) { + return file_app_router_command_command_proto_rawDescGZIP(), []int{6} +} + +func (x *GetBalancerInfoRequest) GetTag() string { + if x != nil { + return x.Tag + } + return "" +} + +type GetBalancerInfoResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Balancer *BalancerMsg `protobuf:"bytes,1,opt,name=balancer,proto3" json:"balancer,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetBalancerInfoResponse) Reset() { + *x = GetBalancerInfoResponse{} + mi := &file_app_router_command_command_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetBalancerInfoResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetBalancerInfoResponse) ProtoMessage() {} + +func (x *GetBalancerInfoResponse) ProtoReflect() protoreflect.Message { + mi := &file_app_router_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 GetBalancerInfoResponse.ProtoReflect.Descriptor instead. +func (*GetBalancerInfoResponse) Descriptor() ([]byte, []int) { + return file_app_router_command_command_proto_rawDescGZIP(), []int{7} +} + +func (x *GetBalancerInfoResponse) GetBalancer() *BalancerMsg { + if x != nil { + return x.Balancer + } + return nil +} + +type OverrideBalancerTargetRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + BalancerTag string `protobuf:"bytes,1,opt,name=balancerTag,proto3" json:"balancerTag,omitempty"` + Target string `protobuf:"bytes,2,opt,name=target,proto3" json:"target,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OverrideBalancerTargetRequest) Reset() { + *x = OverrideBalancerTargetRequest{} + mi := &file_app_router_command_command_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OverrideBalancerTargetRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OverrideBalancerTargetRequest) ProtoMessage() {} + +func (x *OverrideBalancerTargetRequest) ProtoReflect() protoreflect.Message { + mi := &file_app_router_command_command_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OverrideBalancerTargetRequest.ProtoReflect.Descriptor instead. +func (*OverrideBalancerTargetRequest) Descriptor() ([]byte, []int) { + return file_app_router_command_command_proto_rawDescGZIP(), []int{8} +} + +func (x *OverrideBalancerTargetRequest) GetBalancerTag() string { + if x != nil { + return x.BalancerTag + } + return "" +} + +func (x *OverrideBalancerTargetRequest) GetTarget() string { + if x != nil { + return x.Target + } + return "" +} + +type OverrideBalancerTargetResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OverrideBalancerTargetResponse) Reset() { + *x = OverrideBalancerTargetResponse{} + mi := &file_app_router_command_command_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OverrideBalancerTargetResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OverrideBalancerTargetResponse) ProtoMessage() {} + +func (x *OverrideBalancerTargetResponse) ProtoReflect() protoreflect.Message { + mi := &file_app_router_command_command_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OverrideBalancerTargetResponse.ProtoReflect.Descriptor instead. +func (*OverrideBalancerTargetResponse) Descriptor() ([]byte, []int) { + return file_app_router_command_command_proto_rawDescGZIP(), []int{9} +} + +type AddRuleRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Config *serial.TypedMessage `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"` + ShouldAppend bool `protobuf:"varint,2,opt,name=shouldAppend,proto3" json:"shouldAppend,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AddRuleRequest) Reset() { + *x = AddRuleRequest{} + mi := &file_app_router_command_command_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AddRuleRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddRuleRequest) ProtoMessage() {} + +func (x *AddRuleRequest) ProtoReflect() protoreflect.Message { + mi := &file_app_router_command_command_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AddRuleRequest.ProtoReflect.Descriptor instead. +func (*AddRuleRequest) Descriptor() ([]byte, []int) { + return file_app_router_command_command_proto_rawDescGZIP(), []int{10} +} + +func (x *AddRuleRequest) GetConfig() *serial.TypedMessage { + if x != nil { + return x.Config + } + return nil +} + +func (x *AddRuleRequest) GetShouldAppend() bool { + if x != nil { + return x.ShouldAppend + } + return false +} + +type AddRuleResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AddRuleResponse) Reset() { + *x = AddRuleResponse{} + mi := &file_app_router_command_command_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AddRuleResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddRuleResponse) ProtoMessage() {} + +func (x *AddRuleResponse) ProtoReflect() protoreflect.Message { + mi := &file_app_router_command_command_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AddRuleResponse.ProtoReflect.Descriptor instead. +func (*AddRuleResponse) Descriptor() ([]byte, []int) { + return file_app_router_command_command_proto_rawDescGZIP(), []int{11} +} + +type RemoveRuleRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + RuleTag string `protobuf:"bytes,1,opt,name=ruleTag,proto3" json:"ruleTag,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RemoveRuleRequest) Reset() { + *x = RemoveRuleRequest{} + mi := &file_app_router_command_command_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RemoveRuleRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RemoveRuleRequest) ProtoMessage() {} + +func (x *RemoveRuleRequest) ProtoReflect() protoreflect.Message { + mi := &file_app_router_command_command_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RemoveRuleRequest.ProtoReflect.Descriptor instead. +func (*RemoveRuleRequest) Descriptor() ([]byte, []int) { + return file_app_router_command_command_proto_rawDescGZIP(), []int{12} +} + +func (x *RemoveRuleRequest) GetRuleTag() string { + if x != nil { + return x.RuleTag + } + return "" +} + +type RemoveRuleResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RemoveRuleResponse) Reset() { + *x = RemoveRuleResponse{} + mi := &file_app_router_command_command_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RemoveRuleResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RemoveRuleResponse) ProtoMessage() {} + +func (x *RemoveRuleResponse) ProtoReflect() protoreflect.Message { + mi := &file_app_router_command_command_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RemoveRuleResponse.ProtoReflect.Descriptor instead. +func (*RemoveRuleResponse) Descriptor() ([]byte, []int) { + return file_app_router_command_command_proto_rawDescGZIP(), []int{13} +} + +type ListRuleRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListRuleRequest) Reset() { + *x = ListRuleRequest{} + mi := &file_app_router_command_command_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListRuleRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListRuleRequest) ProtoMessage() {} + +func (x *ListRuleRequest) ProtoReflect() protoreflect.Message { + mi := &file_app_router_command_command_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListRuleRequest.ProtoReflect.Descriptor instead. +func (*ListRuleRequest) Descriptor() ([]byte, []int) { + return file_app_router_command_command_proto_rawDescGZIP(), []int{14} +} + +type ListRuleItem struct { + state protoimpl.MessageState `protogen:"open.v1"` + Tag string `protobuf:"bytes,1,opt,name=tag,proto3" json:"tag,omitempty"` + RuleTag string `protobuf:"bytes,2,opt,name=ruleTag,proto3" json:"ruleTag,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListRuleItem) Reset() { + *x = ListRuleItem{} + mi := &file_app_router_command_command_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListRuleItem) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListRuleItem) ProtoMessage() {} + +func (x *ListRuleItem) ProtoReflect() protoreflect.Message { + mi := &file_app_router_command_command_proto_msgTypes[15] + 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 ListRuleItem.ProtoReflect.Descriptor instead. +func (*ListRuleItem) Descriptor() ([]byte, []int) { + return file_app_router_command_command_proto_rawDescGZIP(), []int{15} +} + +func (x *ListRuleItem) GetTag() string { + if x != nil { + return x.Tag + } + return "" +} + +func (x *ListRuleItem) GetRuleTag() string { + if x != nil { + return x.RuleTag + } + return "" +} + +type ListRuleResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Rules []*ListRuleItem `protobuf:"bytes,1,rep,name=rules,proto3" json:"rules,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListRuleResponse) Reset() { + *x = ListRuleResponse{} + mi := &file_app_router_command_command_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListRuleResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListRuleResponse) ProtoMessage() {} + +func (x *ListRuleResponse) ProtoReflect() protoreflect.Message { + mi := &file_app_router_command_command_proto_msgTypes[16] + 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 ListRuleResponse.ProtoReflect.Descriptor instead. +func (*ListRuleResponse) Descriptor() ([]byte, []int) { + return file_app_router_command_command_proto_rawDescGZIP(), []int{16} +} + +func (x *ListRuleResponse) GetRules() []*ListRuleItem { + if x != nil { + return x.Rules + } + return nil +} + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_app_router_command_command_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_app_router_command_command_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_app_router_command_command_proto_rawDescGZIP(), []int{17} +} + +var File_app_router_command_command_proto protoreflect.FileDescriptor + +const file_app_router_command_command_proto_rawDesc = "" + + "\n" + + " app/router/command/command.proto\x12\x17xray.app.router.command\x1a\x18common/net/network.proto\x1a!common/serial/typed_message.proto\"\xf6\x04\n" + + "\x0eRoutingContext\x12\x1e\n" + + "\n" + + "InboundTag\x18\x01 \x01(\tR\n" + + "InboundTag\x122\n" + + "\aNetwork\x18\x02 \x01(\x0e2\x18.xray.common.net.NetworkR\aNetwork\x12\x1c\n" + + "\tSourceIPs\x18\x03 \x03(\fR\tSourceIPs\x12\x1c\n" + + "\tTargetIPs\x18\x04 \x03(\fR\tTargetIPs\x12\x1e\n" + + "\n" + + "SourcePort\x18\x05 \x01(\rR\n" + + "SourcePort\x12\x1e\n" + + "\n" + + "TargetPort\x18\x06 \x01(\rR\n" + + "TargetPort\x12\"\n" + + "\fTargetDomain\x18\a \x01(\tR\fTargetDomain\x12\x1a\n" + + "\bProtocol\x18\b \x01(\tR\bProtocol\x12\x12\n" + + "\x04User\x18\t \x01(\tR\x04User\x12W\n" + + "\n" + + "Attributes\x18\n" + + " \x03(\v27.xray.app.router.command.RoutingContext.AttributesEntryR\n" + + "Attributes\x12,\n" + + "\x11OutboundGroupTags\x18\v \x03(\tR\x11OutboundGroupTags\x12 \n" + + "\vOutboundTag\x18\f \x01(\tR\vOutboundTag\x12\x1a\n" + + "\bLocalIPs\x18\r \x03(\fR\bLocalIPs\x12\x1c\n" + + "\tLocalPort\x18\x0e \x01(\rR\tLocalPort\x12\x1e\n" + + "\n" + + "VlessRoute\x18\x0f \x01(\rR\n" + + "VlessRoute\x1a=\n" + + "\x0fAttributesEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"F\n" + + "\x1cSubscribeRoutingStatsRequest\x12&\n" + + "\x0eFieldSelectors\x18\x01 \x03(\tR\x0eFieldSelectors\"\xb1\x01\n" + + "\x10TestRouteRequest\x12O\n" + + "\x0eRoutingContext\x18\x01 \x01(\v2'.xray.app.router.command.RoutingContextR\x0eRoutingContext\x12&\n" + + "\x0eFieldSelectors\x18\x02 \x03(\tR\x0eFieldSelectors\x12$\n" + + "\rPublishResult\x18\x03 \x01(\bR\rPublishResult\"'\n" + + "\x13PrincipleTargetInfo\x12\x10\n" + + "\x03tag\x18\x01 \x03(\tR\x03tag\"&\n" + + "\fOverrideInfo\x12\x16\n" + + "\x06target\x18\x02 \x01(\tR\x06target\"\xa9\x01\n" + + "\vBalancerMsg\x12A\n" + + "\boverride\x18\x05 \x01(\v2%.xray.app.router.command.OverrideInfoR\boverride\x12W\n" + + "\x10principle_target\x18\x06 \x01(\v2,.xray.app.router.command.PrincipleTargetInfoR\x0fprincipleTarget\"*\n" + + "\x16GetBalancerInfoRequest\x12\x10\n" + + "\x03tag\x18\x01 \x01(\tR\x03tag\"[\n" + + "\x17GetBalancerInfoResponse\x12@\n" + + "\bbalancer\x18\x01 \x01(\v2$.xray.app.router.command.BalancerMsgR\bbalancer\"Y\n" + + "\x1dOverrideBalancerTargetRequest\x12 \n" + + "\vbalancerTag\x18\x01 \x01(\tR\vbalancerTag\x12\x16\n" + + "\x06target\x18\x02 \x01(\tR\x06target\" \n" + + "\x1eOverrideBalancerTargetResponse\"n\n" + + "\x0eAddRuleRequest\x128\n" + + "\x06config\x18\x01 \x01(\v2 .xray.common.serial.TypedMessageR\x06config\x12\"\n" + + "\fshouldAppend\x18\x02 \x01(\bR\fshouldAppend\"\x11\n" + + "\x0fAddRuleResponse\"-\n" + + "\x11RemoveRuleRequest\x12\x18\n" + + "\aruleTag\x18\x01 \x01(\tR\aruleTag\"\x14\n" + + "\x12RemoveRuleResponse\"\x11\n" + + "\x0fListRuleRequest\":\n" + + "\fListRuleItem\x12\x10\n" + + "\x03tag\x18\x01 \x01(\tR\x03tag\x12\x18\n" + + "\aruleTag\x18\x02 \x01(\tR\aruleTag\"O\n" + + "\x10ListRuleResponse\x12;\n" + + "\x05rules\x18\x01 \x03(\v2%.xray.app.router.command.ListRuleItemR\x05rules\"\b\n" + + "\x06Config2\xa2\x06\n" + + "\x0eRoutingService\x12{\n" + + "\x15SubscribeRoutingStats\x125.xray.app.router.command.SubscribeRoutingStatsRequest\x1a'.xray.app.router.command.RoutingContext\"\x000\x01\x12a\n" + + "\tTestRoute\x12).xray.app.router.command.TestRouteRequest\x1a'.xray.app.router.command.RoutingContext\"\x00\x12v\n" + + "\x0fGetBalancerInfo\x12/.xray.app.router.command.GetBalancerInfoRequest\x1a0.xray.app.router.command.GetBalancerInfoResponse\"\x00\x12\x8b\x01\n" + + "\x16OverrideBalancerTarget\x126.xray.app.router.command.OverrideBalancerTargetRequest\x1a7.xray.app.router.command.OverrideBalancerTargetResponse\"\x00\x12^\n" + + "\aAddRule\x12'.xray.app.router.command.AddRuleRequest\x1a(.xray.app.router.command.AddRuleResponse\"\x00\x12g\n" + + "\n" + + "RemoveRule\x12*.xray.app.router.command.RemoveRuleRequest\x1a+.xray.app.router.command.RemoveRuleResponse\"\x00\x12a\n" + + "\bListRule\x12(.xray.app.router.command.ListRuleRequest\x1a).xray.app.router.command.ListRuleResponse\"\x00Bg\n" + + "\x1bcom.xray.app.router.commandP\x01Z,github.com/xtls/xray-core/app/router/command\xaa\x02\x17Xray.App.Router.Commandb\x06proto3" + +var ( + file_app_router_command_command_proto_rawDescOnce sync.Once + file_app_router_command_command_proto_rawDescData []byte +) + +func file_app_router_command_command_proto_rawDescGZIP() []byte { + file_app_router_command_command_proto_rawDescOnce.Do(func() { + file_app_router_command_command_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_app_router_command_command_proto_rawDesc), len(file_app_router_command_command_proto_rawDesc))) + }) + return file_app_router_command_command_proto_rawDescData +} + +var file_app_router_command_command_proto_msgTypes = make([]protoimpl.MessageInfo, 19) +var file_app_router_command_command_proto_goTypes = []any{ + (*RoutingContext)(nil), // 0: xray.app.router.command.RoutingContext + (*SubscribeRoutingStatsRequest)(nil), // 1: xray.app.router.command.SubscribeRoutingStatsRequest + (*TestRouteRequest)(nil), // 2: xray.app.router.command.TestRouteRequest + (*PrincipleTargetInfo)(nil), // 3: xray.app.router.command.PrincipleTargetInfo + (*OverrideInfo)(nil), // 4: xray.app.router.command.OverrideInfo + (*BalancerMsg)(nil), // 5: xray.app.router.command.BalancerMsg + (*GetBalancerInfoRequest)(nil), // 6: xray.app.router.command.GetBalancerInfoRequest + (*GetBalancerInfoResponse)(nil), // 7: xray.app.router.command.GetBalancerInfoResponse + (*OverrideBalancerTargetRequest)(nil), // 8: xray.app.router.command.OverrideBalancerTargetRequest + (*OverrideBalancerTargetResponse)(nil), // 9: xray.app.router.command.OverrideBalancerTargetResponse + (*AddRuleRequest)(nil), // 10: xray.app.router.command.AddRuleRequest + (*AddRuleResponse)(nil), // 11: xray.app.router.command.AddRuleResponse + (*RemoveRuleRequest)(nil), // 12: xray.app.router.command.RemoveRuleRequest + (*RemoveRuleResponse)(nil), // 13: xray.app.router.command.RemoveRuleResponse + (*ListRuleRequest)(nil), // 14: xray.app.router.command.ListRuleRequest + (*ListRuleItem)(nil), // 15: xray.app.router.command.ListRuleItem + (*ListRuleResponse)(nil), // 16: xray.app.router.command.ListRuleResponse + (*Config)(nil), // 17: xray.app.router.command.Config + nil, // 18: xray.app.router.command.RoutingContext.AttributesEntry + (net.Network)(0), // 19: xray.common.net.Network + (*serial.TypedMessage)(nil), // 20: xray.common.serial.TypedMessage +} +var file_app_router_command_command_proto_depIdxs = []int32{ + 19, // 0: xray.app.router.command.RoutingContext.Network:type_name -> xray.common.net.Network + 18, // 1: xray.app.router.command.RoutingContext.Attributes:type_name -> xray.app.router.command.RoutingContext.AttributesEntry + 0, // 2: xray.app.router.command.TestRouteRequest.RoutingContext:type_name -> xray.app.router.command.RoutingContext + 4, // 3: xray.app.router.command.BalancerMsg.override:type_name -> xray.app.router.command.OverrideInfo + 3, // 4: xray.app.router.command.BalancerMsg.principle_target:type_name -> xray.app.router.command.PrincipleTargetInfo + 5, // 5: xray.app.router.command.GetBalancerInfoResponse.balancer:type_name -> xray.app.router.command.BalancerMsg + 20, // 6: xray.app.router.command.AddRuleRequest.config:type_name -> xray.common.serial.TypedMessage + 15, // 7: xray.app.router.command.ListRuleResponse.rules:type_name -> xray.app.router.command.ListRuleItem + 1, // 8: xray.app.router.command.RoutingService.SubscribeRoutingStats:input_type -> xray.app.router.command.SubscribeRoutingStatsRequest + 2, // 9: xray.app.router.command.RoutingService.TestRoute:input_type -> xray.app.router.command.TestRouteRequest + 6, // 10: xray.app.router.command.RoutingService.GetBalancerInfo:input_type -> xray.app.router.command.GetBalancerInfoRequest + 8, // 11: xray.app.router.command.RoutingService.OverrideBalancerTarget:input_type -> xray.app.router.command.OverrideBalancerTargetRequest + 10, // 12: xray.app.router.command.RoutingService.AddRule:input_type -> xray.app.router.command.AddRuleRequest + 12, // 13: xray.app.router.command.RoutingService.RemoveRule:input_type -> xray.app.router.command.RemoveRuleRequest + 14, // 14: xray.app.router.command.RoutingService.ListRule:input_type -> xray.app.router.command.ListRuleRequest + 0, // 15: xray.app.router.command.RoutingService.SubscribeRoutingStats:output_type -> xray.app.router.command.RoutingContext + 0, // 16: xray.app.router.command.RoutingService.TestRoute:output_type -> xray.app.router.command.RoutingContext + 7, // 17: xray.app.router.command.RoutingService.GetBalancerInfo:output_type -> xray.app.router.command.GetBalancerInfoResponse + 9, // 18: xray.app.router.command.RoutingService.OverrideBalancerTarget:output_type -> xray.app.router.command.OverrideBalancerTargetResponse + 11, // 19: xray.app.router.command.RoutingService.AddRule:output_type -> xray.app.router.command.AddRuleResponse + 13, // 20: xray.app.router.command.RoutingService.RemoveRule:output_type -> xray.app.router.command.RemoveRuleResponse + 16, // 21: xray.app.router.command.RoutingService.ListRule:output_type -> xray.app.router.command.ListRuleResponse + 15, // [15:22] is the sub-list for method output_type + 8, // [8:15] is the sub-list for method input_type + 8, // [8:8] is the sub-list for extension type_name + 8, // [8:8] is the sub-list for extension extendee + 0, // [0:8] is the sub-list for field type_name +} + +func init() { file_app_router_command_command_proto_init() } +func file_app_router_command_command_proto_init() { + if File_app_router_command_command_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_app_router_command_command_proto_rawDesc), len(file_app_router_command_command_proto_rawDesc)), + NumEnums: 0, + NumMessages: 19, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_app_router_command_command_proto_goTypes, + DependencyIndexes: file_app_router_command_command_proto_depIdxs, + MessageInfos: file_app_router_command_command_proto_msgTypes, + }.Build() + File_app_router_command_command_proto = out.File + file_app_router_command_command_proto_goTypes = nil + file_app_router_command_command_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/app/router/command/command.proto b/subproject/Xray-core-main/app/router/command/command.proto new file mode 100644 index 00000000..22ae95b9 --- /dev/null +++ b/subproject/Xray-core-main/app/router/command/command.proto @@ -0,0 +1,132 @@ +syntax = "proto3"; + +package xray.app.router.command; +option csharp_namespace = "Xray.App.Router.Command"; +option go_package = "github.com/xtls/xray-core/app/router/command"; +option java_package = "com.xray.app.router.command"; +option java_multiple_files = true; + +import "common/net/network.proto"; +import "common/serial/typed_message.proto"; + +// RoutingContext is the context with information relative to routing process. +// It conforms to the structure of xray.features.routing.Context and +// xray.features.routing.Route. +message RoutingContext { + string InboundTag = 1; + xray.common.net.Network Network = 2; + repeated bytes SourceIPs = 3; + repeated bytes TargetIPs = 4; + uint32 SourcePort = 5; + uint32 TargetPort = 6; + string TargetDomain = 7; + string Protocol = 8; + string User = 9; + map Attributes = 10; + repeated string OutboundGroupTags = 11; + string OutboundTag = 12; + repeated bytes LocalIPs = 13; + uint32 LocalPort = 14; + uint32 VlessRoute = 15; +} + +// SubscribeRoutingStatsRequest subscribes to routing statistics channel if +// opened by xray-core. +// * FieldSelectors selects a subset of fields in routing statistics to return. +// Valid selectors: +// - inbound: Selects connection's inbound tag. +// - network: Selects connection's network. +// - ip: Equivalent as "ip_source" and "ip_target", selects both source and +// target IP. +// - port: Equivalent as "port_source" and "port_target", selects both source +// and target port. +// - domain: Selects target domain. +// - protocol: Select connection's protocol. +// - user: Select connection's inbound user email. +// - attributes: Select connection's additional attributes. +// - outbound: Equivalent as "outbound" and "outbound_group", select both +// outbound tag and outbound group tags. +// * If FieldSelectors is left empty, all fields will be returned. +message SubscribeRoutingStatsRequest { + repeated string FieldSelectors = 1; +} + +// TestRouteRequest manually tests a routing result according to the routing +// context message. +// * RoutingContext is the routing message without outbound information. +// * FieldSelectors selects the fields to return in the routing result. All +// fields are returned if left empty. +// * PublishResult broadcasts the routing result to routing statistics channel +// if set true. +message TestRouteRequest { + RoutingContext RoutingContext = 1; + repeated string FieldSelectors = 2; + bool PublishResult = 3; +} + +message PrincipleTargetInfo { + repeated string tag = 1; +} + +message OverrideInfo { + string target = 2; +} + +message BalancerMsg { + OverrideInfo override = 5; + PrincipleTargetInfo principle_target = 6; +} + +message GetBalancerInfoRequest { + string tag = 1; +} + +message GetBalancerInfoResponse { + BalancerMsg balancer = 1; +} + +message OverrideBalancerTargetRequest { + string balancerTag = 1; + string target = 2; +} + +message OverrideBalancerTargetResponse {} + +message AddRuleRequest { + xray.common.serial.TypedMessage config = 1; + bool shouldAppend = 2; +} +message AddRuleResponse {} + +message RemoveRuleRequest { + string ruleTag = 1; +} + +message RemoveRuleResponse {} + +message ListRuleRequest {} + +message ListRuleItem { + string tag = 1; + string ruleTag = 2; +} + +message ListRuleResponse{ + repeated ListRuleItem rules = 1; +} + +service RoutingService { + rpc SubscribeRoutingStats(SubscribeRoutingStatsRequest) + returns (stream RoutingContext) {} + rpc TestRoute(TestRouteRequest) returns (RoutingContext) {} + + rpc GetBalancerInfo(GetBalancerInfoRequest) returns (GetBalancerInfoResponse){} + rpc OverrideBalancerTarget(OverrideBalancerTargetRequest) returns (OverrideBalancerTargetResponse) {} + + rpc AddRule(AddRuleRequest) returns (AddRuleResponse) {} + rpc RemoveRule(RemoveRuleRequest) returns (RemoveRuleResponse) {} + + rpc ListRule(ListRuleRequest) returns (ListRuleResponse) {} +} + +message Config {} diff --git a/subproject/Xray-core-main/app/router/command/command_grpc.pb.go b/subproject/Xray-core-main/app/router/command/command_grpc.pb.go new file mode 100644 index 00000000..880ac724 --- /dev/null +++ b/subproject/Xray-core-main/app/router/command/command_grpc.pb.go @@ -0,0 +1,353 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.0 +// - protoc v6.33.5 +// source: app/router/command/command.proto + +package command + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + RoutingService_SubscribeRoutingStats_FullMethodName = "/xray.app.router.command.RoutingService/SubscribeRoutingStats" + RoutingService_TestRoute_FullMethodName = "/xray.app.router.command.RoutingService/TestRoute" + RoutingService_GetBalancerInfo_FullMethodName = "/xray.app.router.command.RoutingService/GetBalancerInfo" + RoutingService_OverrideBalancerTarget_FullMethodName = "/xray.app.router.command.RoutingService/OverrideBalancerTarget" + RoutingService_AddRule_FullMethodName = "/xray.app.router.command.RoutingService/AddRule" + RoutingService_RemoveRule_FullMethodName = "/xray.app.router.command.RoutingService/RemoveRule" + RoutingService_ListRule_FullMethodName = "/xray.app.router.command.RoutingService/ListRule" +) + +// RoutingServiceClient is the client API for RoutingService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type RoutingServiceClient interface { + SubscribeRoutingStats(ctx context.Context, in *SubscribeRoutingStatsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[RoutingContext], error) + TestRoute(ctx context.Context, in *TestRouteRequest, opts ...grpc.CallOption) (*RoutingContext, error) + GetBalancerInfo(ctx context.Context, in *GetBalancerInfoRequest, opts ...grpc.CallOption) (*GetBalancerInfoResponse, error) + OverrideBalancerTarget(ctx context.Context, in *OverrideBalancerTargetRequest, opts ...grpc.CallOption) (*OverrideBalancerTargetResponse, error) + AddRule(ctx context.Context, in *AddRuleRequest, opts ...grpc.CallOption) (*AddRuleResponse, error) + RemoveRule(ctx context.Context, in *RemoveRuleRequest, opts ...grpc.CallOption) (*RemoveRuleResponse, error) + ListRule(ctx context.Context, in *ListRuleRequest, opts ...grpc.CallOption) (*ListRuleResponse, error) +} + +type routingServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewRoutingServiceClient(cc grpc.ClientConnInterface) RoutingServiceClient { + return &routingServiceClient{cc} +} + +func (c *routingServiceClient) SubscribeRoutingStats(ctx context.Context, in *SubscribeRoutingStatsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[RoutingContext], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &RoutingService_ServiceDesc.Streams[0], RoutingService_SubscribeRoutingStats_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[SubscribeRoutingStatsRequest, RoutingContext]{ClientStream: stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type RoutingService_SubscribeRoutingStatsClient = grpc.ServerStreamingClient[RoutingContext] + +func (c *routingServiceClient) TestRoute(ctx context.Context, in *TestRouteRequest, opts ...grpc.CallOption) (*RoutingContext, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(RoutingContext) + err := c.cc.Invoke(ctx, RoutingService_TestRoute_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *routingServiceClient) GetBalancerInfo(ctx context.Context, in *GetBalancerInfoRequest, opts ...grpc.CallOption) (*GetBalancerInfoResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetBalancerInfoResponse) + err := c.cc.Invoke(ctx, RoutingService_GetBalancerInfo_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *routingServiceClient) OverrideBalancerTarget(ctx context.Context, in *OverrideBalancerTargetRequest, opts ...grpc.CallOption) (*OverrideBalancerTargetResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(OverrideBalancerTargetResponse) + err := c.cc.Invoke(ctx, RoutingService_OverrideBalancerTarget_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *routingServiceClient) AddRule(ctx context.Context, in *AddRuleRequest, opts ...grpc.CallOption) (*AddRuleResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(AddRuleResponse) + err := c.cc.Invoke(ctx, RoutingService_AddRule_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *routingServiceClient) RemoveRule(ctx context.Context, in *RemoveRuleRequest, opts ...grpc.CallOption) (*RemoveRuleResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(RemoveRuleResponse) + err := c.cc.Invoke(ctx, RoutingService_RemoveRule_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *routingServiceClient) ListRule(ctx context.Context, in *ListRuleRequest, opts ...grpc.CallOption) (*ListRuleResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListRuleResponse) + err := c.cc.Invoke(ctx, RoutingService_ListRule_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// RoutingServiceServer is the server API for RoutingService service. +// All implementations must embed UnimplementedRoutingServiceServer +// for forward compatibility. +type RoutingServiceServer interface { + SubscribeRoutingStats(*SubscribeRoutingStatsRequest, grpc.ServerStreamingServer[RoutingContext]) error + TestRoute(context.Context, *TestRouteRequest) (*RoutingContext, error) + GetBalancerInfo(context.Context, *GetBalancerInfoRequest) (*GetBalancerInfoResponse, error) + OverrideBalancerTarget(context.Context, *OverrideBalancerTargetRequest) (*OverrideBalancerTargetResponse, error) + AddRule(context.Context, *AddRuleRequest) (*AddRuleResponse, error) + RemoveRule(context.Context, *RemoveRuleRequest) (*RemoveRuleResponse, error) + ListRule(context.Context, *ListRuleRequest) (*ListRuleResponse, error) + mustEmbedUnimplementedRoutingServiceServer() +} + +// UnimplementedRoutingServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedRoutingServiceServer struct{} + +func (UnimplementedRoutingServiceServer) SubscribeRoutingStats(*SubscribeRoutingStatsRequest, grpc.ServerStreamingServer[RoutingContext]) error { + return status.Error(codes.Unimplemented, "method SubscribeRoutingStats not implemented") +} +func (UnimplementedRoutingServiceServer) TestRoute(context.Context, *TestRouteRequest) (*RoutingContext, error) { + return nil, status.Error(codes.Unimplemented, "method TestRoute not implemented") +} +func (UnimplementedRoutingServiceServer) GetBalancerInfo(context.Context, *GetBalancerInfoRequest) (*GetBalancerInfoResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetBalancerInfo not implemented") +} +func (UnimplementedRoutingServiceServer) OverrideBalancerTarget(context.Context, *OverrideBalancerTargetRequest) (*OverrideBalancerTargetResponse, error) { + return nil, status.Error(codes.Unimplemented, "method OverrideBalancerTarget not implemented") +} +func (UnimplementedRoutingServiceServer) AddRule(context.Context, *AddRuleRequest) (*AddRuleResponse, error) { + return nil, status.Error(codes.Unimplemented, "method AddRule not implemented") +} +func (UnimplementedRoutingServiceServer) RemoveRule(context.Context, *RemoveRuleRequest) (*RemoveRuleResponse, error) { + return nil, status.Error(codes.Unimplemented, "method RemoveRule not implemented") +} +func (UnimplementedRoutingServiceServer) ListRule(context.Context, *ListRuleRequest) (*ListRuleResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListRule not implemented") +} +func (UnimplementedRoutingServiceServer) mustEmbedUnimplementedRoutingServiceServer() {} +func (UnimplementedRoutingServiceServer) testEmbeddedByValue() {} + +// UnsafeRoutingServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to RoutingServiceServer will +// result in compilation errors. +type UnsafeRoutingServiceServer interface { + mustEmbedUnimplementedRoutingServiceServer() +} + +func RegisterRoutingServiceServer(s grpc.ServiceRegistrar, srv RoutingServiceServer) { + // If the following call panics, it indicates UnimplementedRoutingServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&RoutingService_ServiceDesc, srv) +} + +func _RoutingService_SubscribeRoutingStats_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(SubscribeRoutingStatsRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(RoutingServiceServer).SubscribeRoutingStats(m, &grpc.GenericServerStream[SubscribeRoutingStatsRequest, RoutingContext]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type RoutingService_SubscribeRoutingStatsServer = grpc.ServerStreamingServer[RoutingContext] + +func _RoutingService_TestRoute_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(TestRouteRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RoutingServiceServer).TestRoute(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RoutingService_TestRoute_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RoutingServiceServer).TestRoute(ctx, req.(*TestRouteRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _RoutingService_GetBalancerInfo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetBalancerInfoRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RoutingServiceServer).GetBalancerInfo(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RoutingService_GetBalancerInfo_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RoutingServiceServer).GetBalancerInfo(ctx, req.(*GetBalancerInfoRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _RoutingService_OverrideBalancerTarget_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(OverrideBalancerTargetRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RoutingServiceServer).OverrideBalancerTarget(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RoutingService_OverrideBalancerTarget_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RoutingServiceServer).OverrideBalancerTarget(ctx, req.(*OverrideBalancerTargetRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _RoutingService_AddRule_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AddRuleRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RoutingServiceServer).AddRule(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RoutingService_AddRule_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RoutingServiceServer).AddRule(ctx, req.(*AddRuleRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _RoutingService_RemoveRule_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RemoveRuleRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RoutingServiceServer).RemoveRule(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RoutingService_RemoveRule_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RoutingServiceServer).RemoveRule(ctx, req.(*RemoveRuleRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _RoutingService_ListRule_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListRuleRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RoutingServiceServer).ListRule(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RoutingService_ListRule_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RoutingServiceServer).ListRule(ctx, req.(*ListRuleRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// RoutingService_ServiceDesc is the grpc.ServiceDesc for RoutingService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var RoutingService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "xray.app.router.command.RoutingService", + HandlerType: (*RoutingServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "TestRoute", + Handler: _RoutingService_TestRoute_Handler, + }, + { + MethodName: "GetBalancerInfo", + Handler: _RoutingService_GetBalancerInfo_Handler, + }, + { + MethodName: "OverrideBalancerTarget", + Handler: _RoutingService_OverrideBalancerTarget_Handler, + }, + { + MethodName: "AddRule", + Handler: _RoutingService_AddRule_Handler, + }, + { + MethodName: "RemoveRule", + Handler: _RoutingService_RemoveRule_Handler, + }, + { + MethodName: "ListRule", + Handler: _RoutingService_ListRule_Handler, + }, + }, + Streams: []grpc.StreamDesc{ + { + StreamName: "SubscribeRoutingStats", + Handler: _RoutingService_SubscribeRoutingStats_Handler, + ServerStreams: true, + }, + }, + Metadata: "app/router/command/command.proto", +} diff --git a/subproject/Xray-core-main/app/router/command/command_test.go b/subproject/Xray-core-main/app/router/command/command_test.go new file mode 100644 index 00000000..e8329695 --- /dev/null +++ b/subproject/Xray-core-main/app/router/command/command_test.go @@ -0,0 +1,433 @@ +package command_test + +import ( + "context" + "testing" + "time" + + "github.com/golang/mock/gomock" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/xtls/xray-core/app/router" + . "github.com/xtls/xray-core/app/router/command" + "github.com/xtls/xray-core/app/stats" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/features/routing" + "github.com/xtls/xray-core/testing/mocks" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/test/bufconn" +) + +func TestServiceSubscribeRoutingStats(t *testing.T) { + c := stats.NewChannel(&stats.ChannelConfig{ + SubscriberLimit: 1, + BufferSize: 0, + Blocking: true, + }) + common.Must(c.Start()) + defer c.Close() + + lis := bufconn.Listen(1024 * 1024) + bufDialer := func(context.Context, string) (net.Conn, error) { + return lis.Dial() + } + + testCases := []*RoutingContext{ + {InboundTag: "in", OutboundTag: "out"}, + {TargetIPs: [][]byte{{1, 2, 3, 4}}, TargetPort: 8080, OutboundTag: "out"}, + {TargetDomain: "example.com", TargetPort: 443, OutboundTag: "out"}, + {SourcePort: 9999, TargetPort: 9999, OutboundTag: "out"}, + {Network: net.Network_UDP, OutboundGroupTags: []string{"outergroup", "innergroup"}, OutboundTag: "out"}, + {Protocol: "bittorrent", OutboundTag: "blocked"}, + {User: "example@example.com", OutboundTag: "out"}, + {SourceIPs: [][]byte{{127, 0, 0, 1}}, Attributes: map[string]string{"attr": "value"}, OutboundTag: "out"}, + } + errCh := make(chan error) + + // Server goroutine + go func() { + server := grpc.NewServer() + RegisterRoutingServiceServer(server, NewRoutingServer(nil, c)) + errCh <- server.Serve(lis) + }() + + // Publisher goroutine + go func() { + publishTestCases := func() error { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + for { // Wait until there's one subscriber in routing stats channel + if len(c.Subscribers()) > 0 { + break + } + if ctx.Err() != nil { + return ctx.Err() + } + } + for _, tc := range testCases { + c.Publish(context.Background(), AsRoutingRoute(tc)) + time.Sleep(time.Millisecond) + } + return nil + } + + if err := publishTestCases(); err != nil { + errCh <- err + } + }() + + // Client goroutine + go func() { + defer lis.Close() + conn, err := grpc.DialContext(context.Background(), "bufnet", grpc.WithContextDialer(bufDialer), grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + errCh <- err + return + } + defer conn.Close() + client := NewRoutingServiceClient(conn) + + // Test retrieving all fields + testRetrievingAllFields := func() error { + streamCtx, streamClose := context.WithCancel(context.Background()) + + // Test the unsubscription of stream works well + defer func() { + streamClose() + timeOutCtx, timeout := context.WithTimeout(context.Background(), time.Second) + defer timeout() + for { // Wait until there's no subscriber in routing stats channel + if len(c.Subscribers()) == 0 { + break + } + if timeOutCtx.Err() != nil { + t.Error("unexpected subscribers not decreased in channel", timeOutCtx.Err()) + } + } + }() + + stream, err := client.SubscribeRoutingStats(streamCtx, &SubscribeRoutingStatsRequest{}) + if err != nil { + return err + } + + for _, tc := range testCases { + msg, err := stream.Recv() + if err != nil { + return err + } + if r := cmp.Diff(msg, tc, cmpopts.IgnoreUnexported(RoutingContext{})); r != "" { + t.Error(r) + } + } + + // Test that double subscription will fail + errStream, err := client.SubscribeRoutingStats(context.Background(), &SubscribeRoutingStatsRequest{ + FieldSelectors: []string{"ip", "port", "domain", "outbound"}, + }) + if err != nil { + return err + } + if _, err := errStream.Recv(); err == nil { + t.Error("unexpected successful subscription") + } + + return nil + } + + if err := testRetrievingAllFields(); err != nil { + errCh <- err + } + errCh <- nil // Client passed all tests successfully + }() + + // Wait for goroutines to complete + select { + case <-time.After(2 * time.Second): + t.Fatal("Test timeout after 2s") + case err := <-errCh: + if err != nil { + t.Fatal(err) + } + } +} + +func TestServiceSubscribeSubsetOfFields(t *testing.T) { + c := stats.NewChannel(&stats.ChannelConfig{ + SubscriberLimit: 1, + BufferSize: 0, + Blocking: true, + }) + common.Must(c.Start()) + defer c.Close() + + lis := bufconn.Listen(1024 * 1024) + bufDialer := func(context.Context, string) (net.Conn, error) { + return lis.Dial() + } + + testCases := []*RoutingContext{ + {InboundTag: "in", OutboundTag: "out"}, + {TargetIPs: [][]byte{{1, 2, 3, 4}}, TargetPort: 8080, OutboundTag: "out"}, + {TargetDomain: "example.com", TargetPort: 443, OutboundTag: "out"}, + {SourcePort: 9999, TargetPort: 9999, OutboundTag: "out"}, + {Network: net.Network_UDP, OutboundGroupTags: []string{"outergroup", "innergroup"}, OutboundTag: "out"}, + {Protocol: "bittorrent", OutboundTag: "blocked"}, + {User: "example@example.com", OutboundTag: "out"}, + {SourceIPs: [][]byte{{127, 0, 0, 1}}, Attributes: map[string]string{"attr": "value"}, OutboundTag: "out"}, + } + errCh := make(chan error) + + // Server goroutine + go func() { + server := grpc.NewServer() + RegisterRoutingServiceServer(server, NewRoutingServer(nil, c)) + errCh <- server.Serve(lis) + }() + + // Publisher goroutine + go func() { + publishTestCases := func() error { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + for { // Wait until there's one subscriber in routing stats channel + if len(c.Subscribers()) > 0 { + break + } + if ctx.Err() != nil { + return ctx.Err() + } + } + for _, tc := range testCases { + c.Publish(context.Background(), AsRoutingRoute(tc)) + time.Sleep(time.Millisecond) + } + return nil + } + + if err := publishTestCases(); err != nil { + errCh <- err + } + }() + + // Client goroutine + go func() { + defer lis.Close() + conn, err := grpc.DialContext(context.Background(), "bufnet", grpc.WithContextDialer(bufDialer), grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + errCh <- err + return + } + defer conn.Close() + client := NewRoutingServiceClient(conn) + + // Test retrieving only a subset of fields + testRetrievingSubsetOfFields := func() error { + streamCtx, streamClose := context.WithCancel(context.Background()) + defer streamClose() + stream, err := client.SubscribeRoutingStats(streamCtx, &SubscribeRoutingStatsRequest{ + FieldSelectors: []string{"ip", "port", "domain", "outbound"}, + }) + if err != nil { + return err + } + + for _, tc := range testCases { + msg, err := stream.Recv() + if err != nil { + return err + } + stat := &RoutingContext{ // Only a subset of stats is retrieved + SourceIPs: tc.SourceIPs, + TargetIPs: tc.TargetIPs, + SourcePort: tc.SourcePort, + TargetPort: tc.TargetPort, + TargetDomain: tc.TargetDomain, + OutboundGroupTags: tc.OutboundGroupTags, + OutboundTag: tc.OutboundTag, + } + if r := cmp.Diff(msg, stat, cmpopts.IgnoreUnexported(RoutingContext{})); r != "" { + t.Error(r) + } + } + + return nil + } + if err := testRetrievingSubsetOfFields(); err != nil { + errCh <- err + } + errCh <- nil // Client passed all tests successfully + }() + + // Wait for goroutines to complete + select { + case <-time.After(2 * time.Second): + t.Fatal("Test timeout after 2s") + case err := <-errCh: + if err != nil { + t.Fatal(err) + } + } +} + +func TestServiceTestRoute(t *testing.T) { + c := stats.NewChannel(&stats.ChannelConfig{ + SubscriberLimit: 1, + BufferSize: 16, + Blocking: true, + }) + common.Must(c.Start()) + defer c.Close() + + r := new(router.Router) + mockCtl := gomock.NewController(t) + defer mockCtl.Finish() + common.Must(r.Init(context.TODO(), &router.Config{ + Rule: []*router.RoutingRule{ + { + InboundTag: []string{"in"}, + TargetTag: &router.RoutingRule_Tag{Tag: "out"}, + }, + { + Protocol: []string{"bittorrent"}, + TargetTag: &router.RoutingRule_Tag{Tag: "blocked"}, + }, + { + PortList: &net.PortList{Range: []*net.PortRange{{From: 8080, To: 8080}}}, + TargetTag: &router.RoutingRule_Tag{Tag: "out"}, + }, + { + SourcePortList: &net.PortList{Range: []*net.PortRange{{From: 9999, To: 9999}}}, + TargetTag: &router.RoutingRule_Tag{Tag: "out"}, + }, + { + Domain: []*router.Domain{{Type: router.Domain_Domain, Value: "com"}}, + TargetTag: &router.RoutingRule_Tag{Tag: "out"}, + }, + { + SourceGeoip: []*router.GeoIP{{CountryCode: "private", Cidr: []*router.CIDR{{Ip: []byte{127, 0, 0, 0}, Prefix: 8}}}}, + TargetTag: &router.RoutingRule_Tag{Tag: "out"}, + }, + { + UserEmail: []string{"example@example.com"}, + TargetTag: &router.RoutingRule_Tag{Tag: "out"}, + }, + { + Networks: []net.Network{net.Network_UDP, net.Network_TCP}, + TargetTag: &router.RoutingRule_Tag{Tag: "out"}, + }, + }, + }, mocks.NewDNSClient(mockCtl), mocks.NewOutboundManager(mockCtl), nil)) + + lis := bufconn.Listen(1024 * 1024) + bufDialer := func(context.Context, string) (net.Conn, error) { + return lis.Dial() + } + + errCh := make(chan error) + + // Server goroutine + go func() { + server := grpc.NewServer() + RegisterRoutingServiceServer(server, NewRoutingServer(r, c)) + errCh <- server.Serve(lis) + }() + + // Client goroutine + go func() { + defer lis.Close() + conn, err := grpc.DialContext(context.Background(), "bufnet", grpc.WithContextDialer(bufDialer), grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + errCh <- err + } + defer conn.Close() + client := NewRoutingServiceClient(conn) + + testCases := []*RoutingContext{ + {InboundTag: "in", OutboundTag: "out"}, + {TargetIPs: [][]byte{{1, 2, 3, 4}}, TargetPort: 8080, OutboundTag: "out"}, + {TargetDomain: "example.com", TargetPort: 443, OutboundTag: "out"}, + {SourcePort: 9999, TargetPort: 9999, OutboundTag: "out"}, + {Network: net.Network_UDP, Protocol: "bittorrent", OutboundTag: "blocked"}, + {User: "example@example.com", OutboundTag: "out"}, + {SourceIPs: [][]byte{{127, 0, 0, 1}}, Attributes: map[string]string{"attr": "value"}, OutboundTag: "out"}, + } + + // Test simple TestRoute + testSimple := func() error { + for _, tc := range testCases { + route, err := client.TestRoute(context.Background(), &TestRouteRequest{RoutingContext: tc}) + if err != nil { + return err + } + if r := cmp.Diff(route, tc, cmpopts.IgnoreUnexported(RoutingContext{})); r != "" { + t.Error(r) + } + } + return nil + } + + // Test TestRoute with special options + testOptions := func() error { + sub, err := c.Subscribe() + if err != nil { + return err + } + for _, tc := range testCases { + route, err := client.TestRoute(context.Background(), &TestRouteRequest{ + RoutingContext: tc, + FieldSelectors: []string{"ip", "port", "domain", "outbound"}, + PublishResult: true, + }) + if err != nil { + return err + } + stat := &RoutingContext{ // Only a subset of stats is retrieved + SourceIPs: tc.SourceIPs, + TargetIPs: tc.TargetIPs, + SourcePort: tc.SourcePort, + TargetPort: tc.TargetPort, + TargetDomain: tc.TargetDomain, + OutboundGroupTags: tc.OutboundGroupTags, + OutboundTag: tc.OutboundTag, + } + if r := cmp.Diff(route, stat, cmpopts.IgnoreUnexported(RoutingContext{})); r != "" { + t.Error(r) + } + select { // Check that routing result has been published to statistics channel + case msg, received := <-sub: + if route, ok := msg.(routing.Route); received && ok { + if r := cmp.Diff(AsProtobufMessage(nil)(route), tc, cmpopts.IgnoreUnexported(RoutingContext{})); r != "" { + t.Error(r) + } + } else { + t.Error("unexpected failure in receiving published routing result for testcase", tc) + } + case <-time.After(100 * time.Millisecond): + t.Error("unexpected failure in receiving published routing result", tc) + } + } + return nil + } + + if err := testSimple(); err != nil { + errCh <- err + } + if err := testOptions(); err != nil { + errCh <- err + } + errCh <- nil // Client passed all tests successfully + }() + + // Wait for goroutines to complete + select { + case <-time.After(2 * time.Second): + t.Fatal("Test timeout after 2s") + case err := <-errCh: + if err != nil { + t.Fatal(err) + } + } +} diff --git a/subproject/Xray-core-main/app/router/command/config.go b/subproject/Xray-core-main/app/router/command/config.go new file mode 100644 index 00000000..13a319b1 --- /dev/null +++ b/subproject/Xray-core-main/app/router/command/config.go @@ -0,0 +1,119 @@ +package command + +import ( + "strings" + + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/features/routing" +) + +// routingContext is an wrapper of protobuf RoutingContext as implementation of routing.Context and routing.Route. +type routingContext struct { + *RoutingContext +} + +func (c routingContext) GetSourceIPs() []net.IP { + return mapBytesToIPs(c.RoutingContext.GetSourceIPs()) +} + +func (c routingContext) GetSourcePort() net.Port { + return net.Port(c.RoutingContext.GetSourcePort()) +} + +func (c routingContext) GetTargetIPs() []net.IP { + return mapBytesToIPs(c.RoutingContext.GetTargetIPs()) +} + +func (c routingContext) GetTargetPort() net.Port { + return net.Port(c.RoutingContext.GetTargetPort()) +} + +func (c routingContext) GetLocalIPs() []net.IP { + return mapBytesToIPs(c.RoutingContext.GetLocalIPs()) +} + +func (c routingContext) GetLocalPort() net.Port { + return net.Port(c.RoutingContext.GetLocalPort()) +} + +func (c routingContext) GetVlessRoute() net.Port { + return net.Port(c.RoutingContext.GetVlessRoute()) +} + +func (c routingContext) GetRuleTag() string { + return "" +} + +// GetSkipDNSResolve is a mock implementation here to match the interface, +// SkipDNSResolve is set from dns module, no use if coming from a protobuf object? +// TODO: please confirm @Vigilans +func (c routingContext) GetSkipDNSResolve() bool { + return false +} + +// AsRoutingContext converts a protobuf RoutingContext into an implementation of routing.Context. +func AsRoutingContext(r *RoutingContext) routing.Context { + return routingContext{r} +} + +// AsRoutingRoute converts a protobuf RoutingContext into an implementation of routing.Route. +func AsRoutingRoute(r *RoutingContext) routing.Route { + return routingContext{r} +} + +var fieldMap = map[string]func(*RoutingContext, routing.Route){ + "inbound": func(s *RoutingContext, r routing.Route) { s.InboundTag = r.GetInboundTag() }, + "network": func(s *RoutingContext, r routing.Route) { s.Network = r.GetNetwork() }, + "ip_source": func(s *RoutingContext, r routing.Route) { s.SourceIPs = mapIPsToBytes(r.GetSourceIPs()) }, + "ip_target": func(s *RoutingContext, r routing.Route) { s.TargetIPs = mapIPsToBytes(r.GetTargetIPs()) }, + "ip_local": func(s *RoutingContext, r routing.Route) { s.LocalIPs = mapIPsToBytes(r.GetLocalIPs()) }, + "port_source": func(s *RoutingContext, r routing.Route) { s.SourcePort = uint32(r.GetSourcePort()) }, + "port_target": func(s *RoutingContext, r routing.Route) { s.TargetPort = uint32(r.GetTargetPort()) }, + "port_local": func(s *RoutingContext, r routing.Route) { s.LocalPort = uint32(r.GetLocalPort()) }, + "domain": func(s *RoutingContext, r routing.Route) { s.TargetDomain = r.GetTargetDomain() }, + "protocol": func(s *RoutingContext, r routing.Route) { s.Protocol = r.GetProtocol() }, + "user": func(s *RoutingContext, r routing.Route) { s.User = r.GetUser() }, + "attributes": func(s *RoutingContext, r routing.Route) { s.Attributes = r.GetAttributes() }, + "outbound_group": func(s *RoutingContext, r routing.Route) { s.OutboundGroupTags = r.GetOutboundGroupTags() }, + "outbound": func(s *RoutingContext, r routing.Route) { s.OutboundTag = r.GetOutboundTag() }, +} + +// AsProtobufMessage takes selectors of fields and returns a function to convert routing.Route to protobuf RoutingContext. +func AsProtobufMessage(fieldSelectors []string) func(routing.Route) *RoutingContext { + initializers := []func(*RoutingContext, routing.Route){} + for field, init := range fieldMap { + if len(fieldSelectors) == 0 { // If selectors not set, retrieve all fields + initializers = append(initializers, init) + continue + } + for _, selector := range fieldSelectors { + if strings.HasPrefix(field, selector) { + initializers = append(initializers, init) + break + } + } + } + return func(ctx routing.Route) *RoutingContext { + message := new(RoutingContext) + for _, init := range initializers { + init(message, ctx) + } + return message + } +} + +func mapBytesToIPs(bytes [][]byte) []net.IP { + var ips []net.IP + for _, rawIP := range bytes { + ips = append(ips, net.IP(rawIP)) + } + return ips +} + +func mapIPsToBytes(ips []net.IP) [][]byte { + var bytes [][]byte + for _, ip := range ips { + bytes = append(bytes, []byte(ip)) + } + return bytes +} diff --git a/subproject/Xray-core-main/app/router/condition.go b/subproject/Xray-core-main/app/router/condition.go new file mode 100644 index 00000000..54af8165 --- /dev/null +++ b/subproject/Xray-core-main/app/router/condition.go @@ -0,0 +1,432 @@ +package router + +import ( + "context" + "io" + "os" + "path/filepath" + "regexp" + "slices" + "strings" + + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/strmatcher" + "github.com/xtls/xray-core/features/routing" +) + +type Condition interface { + Apply(ctx routing.Context) bool +} + +type ConditionChan []Condition + +func NewConditionChan() *ConditionChan { + var condChan ConditionChan = make([]Condition, 0, 8) + return &condChan +} + +func (v *ConditionChan) Add(cond Condition) *ConditionChan { + *v = append(*v, cond) + return v +} + +// Apply applies all conditions registered in this chan. +func (v *ConditionChan) Apply(ctx routing.Context) bool { + for _, cond := range *v { + if !cond.Apply(ctx) { + return false + } + } + return true +} + +func (v *ConditionChan) Len() int { + return len(*v) +} + +var matcherTypeMap = map[Domain_Type]strmatcher.Type{ + Domain_Plain: strmatcher.Substr, + Domain_Regex: strmatcher.Regex, + Domain_Domain: strmatcher.Domain, + Domain_Full: strmatcher.Full, +} + +type DomainMatcher struct { + Matchers strmatcher.IndexMatcher +} + +func SerializeDomainMatcher(domains []*Domain, w io.Writer) error { + + g := strmatcher.NewMphMatcherGroup() + for _, d := range domains { + matcherType, f := matcherTypeMap[d.Type] + if !f { + continue + } + + _, err := g.AddPattern(d.Value, matcherType) + if err != nil { + return err + } + } + g.Build() + // serialize + return g.Serialize(w) +} + +func NewDomainMatcherFromBuffer(data []byte) (*strmatcher.MphMatcherGroup, error) { + matcher, err := strmatcher.NewMphMatcherGroupFromBuffer(data) + if err != nil { + return nil, err + } + return matcher, nil +} + +func NewMphMatcherGroup(domains []*Domain) (*DomainMatcher, error) { + g := strmatcher.NewMphMatcherGroup() + for i, d := range domains { + domains[i] = nil + matcherType, f := matcherTypeMap[d.Type] + if !f { + errors.LogError(context.Background(), "ignore unsupported domain type ", d.Type, " of rule ", d.Value) + continue + } + _, err := g.AddPattern(d.Value, matcherType) + if err != nil { + errors.LogErrorInner(context.Background(), err, "ignore domain rule ", d.Type, " ", d.Value) + continue + } + } + g.Build() + return &DomainMatcher{ + Matchers: g, + }, nil +} + +func (m *DomainMatcher) ApplyDomain(domain string) bool { + return len(m.Matchers.Match(strings.ToLower(domain))) > 0 +} + +// Apply implements Condition. +func (m *DomainMatcher) Apply(ctx routing.Context) bool { + domain := ctx.GetTargetDomain() + if len(domain) == 0 { + return false + } + return m.ApplyDomain(domain) +} + +type MatcherAsType byte + +const ( + MatcherAsType_Local MatcherAsType = iota + MatcherAsType_Source + MatcherAsType_Target + MatcherAsType_VlessRoute // for port +) + +type IPMatcher struct { + matcher GeoIPMatcher + asType MatcherAsType +} + +func NewIPMatcher(geoips []*GeoIP, asType MatcherAsType) (*IPMatcher, error) { + matcher, err := BuildOptimizedGeoIPMatcher(geoips...) + if err != nil { + return nil, err + } + return &IPMatcher{matcher: matcher, asType: asType}, nil +} + +// Apply implements Condition. +func (m *IPMatcher) Apply(ctx routing.Context) bool { + var ips []net.IP + + switch m.asType { + case MatcherAsType_Local: + ips = ctx.GetLocalIPs() + case MatcherAsType_Source: + ips = ctx.GetSourceIPs() + case MatcherAsType_Target: + ips = ctx.GetTargetIPs() + default: + panic("unk asType") + } + + return m.matcher.AnyMatch(ips) +} + +type PortMatcher struct { + port net.MemoryPortList + asType MatcherAsType +} + +// NewPortMatcher create a new port matcher that can match source or local or destination port +func NewPortMatcher(list *net.PortList, asType MatcherAsType) *PortMatcher { + return &PortMatcher{ + port: net.PortListFromProto(list), + asType: asType, + } +} + +// Apply implements Condition. +func (v *PortMatcher) Apply(ctx routing.Context) bool { + switch v.asType { + case MatcherAsType_Local: + return v.port.Contains(ctx.GetLocalPort()) + case MatcherAsType_Source: + return v.port.Contains(ctx.GetSourcePort()) + case MatcherAsType_Target: + return v.port.Contains(ctx.GetTargetPort()) + case MatcherAsType_VlessRoute: + return v.port.Contains(ctx.GetVlessRoute()) + default: + panic("unk asType") + } +} + +type NetworkMatcher struct { + list [8]bool +} + +func NewNetworkMatcher(network []net.Network) NetworkMatcher { + var matcher NetworkMatcher + for _, n := range network { + matcher.list[int(n)] = true + } + return matcher +} + +// Apply implements Condition. +func (v NetworkMatcher) Apply(ctx routing.Context) bool { + return v.list[int(ctx.GetNetwork())] +} + +type UserMatcher struct { + user []string + pattern []*regexp.Regexp +} + +func NewUserMatcher(users []string) *UserMatcher { + usersCopy := make([]string, 0, len(users)) + patternsCopy := make([]*regexp.Regexp, 0, len(users)) + for _, user := range users { + if len(user) > 0 { + if len(user) > 7 && strings.HasPrefix(user, "regexp:") { + if re, err := regexp.Compile(user[7:]); err == nil { + patternsCopy = append(patternsCopy, re) + } + // Items of users slice with an invalid regexp syntax are ignored. + continue + } + usersCopy = append(usersCopy, user) + } + } + return &UserMatcher{ + user: usersCopy, + pattern: patternsCopy, + } +} + +// Apply implements Condition. +func (v *UserMatcher) Apply(ctx routing.Context) bool { + user := ctx.GetUser() + if len(user) == 0 { + return false + } + for _, u := range v.user { + if u == user { + return true + } + } + for _, re := range v.pattern { + if re.MatchString(user) { + return true + } + } + return false +} + +type InboundTagMatcher struct { + tags []string +} + +func NewInboundTagMatcher(tags []string) *InboundTagMatcher { + tagsCopy := make([]string, 0, len(tags)) + for _, tag := range tags { + if len(tag) > 0 { + tagsCopy = append(tagsCopy, tag) + } + } + return &InboundTagMatcher{ + tags: tagsCopy, + } +} + +// Apply implements Condition. +func (v *InboundTagMatcher) Apply(ctx routing.Context) bool { + tag := ctx.GetInboundTag() + if len(tag) == 0 { + return false + } + for _, t := range v.tags { + if t == tag { + return true + } + } + return false +} + +type ProtocolMatcher struct { + protocols []string +} + +func NewProtocolMatcher(protocols []string) *ProtocolMatcher { + pCopy := make([]string, 0, len(protocols)) + + for _, p := range protocols { + if len(p) > 0 { + pCopy = append(pCopy, p) + } + } + + return &ProtocolMatcher{ + protocols: pCopy, + } +} + +// Apply implements Condition. +func (m *ProtocolMatcher) Apply(ctx routing.Context) bool { + protocol := ctx.GetProtocol() + if len(protocol) == 0 { + return false + } + for _, p := range m.protocols { + if strings.HasPrefix(protocol, p) { + return true + } + } + return false +} + +type AttributeMatcher struct { + configuredKeys map[string]*regexp.Regexp +} + +// Match implements attributes matching. +func (m *AttributeMatcher) Match(attrs map[string]string) bool { + // header keys are case insensitive most likely. So we do a convert + httpHeaders := make(map[string]string) + for key, value := range attrs { + httpHeaders[strings.ToLower(key)] = value + } + for key, regex := range m.configuredKeys { + if a, ok := httpHeaders[key]; !ok || !regex.MatchString(a) { + return false + } + } + return true +} + +// Apply implements Condition. +func (m *AttributeMatcher) Apply(ctx routing.Context) bool { + attributes := ctx.GetAttributes() + if attributes == nil { + return false + } + return m.Match(attributes) +} + +type ProcessNameMatcher struct { + ProcessNames []string + AbsPaths []string + Folders []string + MatchXraySelf bool +} + +func NewProcessNameMatcher(names []string) *ProcessNameMatcher { + processNames := []string{} + folders := []string{} + absPaths := []string{} + matchXraySelf := false + for _, name := range names { + if name == "self/" { + matchXraySelf = true + continue + } + // replace xray/ with self executable path + if name == "xray/" { + xrayPath, err := os.Executable() + if err != nil { + errors.LogError(context.Background(), "Failed to get xray executable path: ", err) + continue + } + name = xrayPath + } + name := filepath.ToSlash(name) + // /usr/bin/ + if strings.HasSuffix(name, "/") { + folders = append(folders, name) + continue + } + // /usr/bin/curl + if strings.Contains(name, "/") { + absPaths = append(absPaths, name) + continue + } + // curl.exe or curl + processNames = append(processNames, strings.TrimSuffix(name, ".exe")) + } + return &ProcessNameMatcher{ + ProcessNames: processNames, + AbsPaths: absPaths, + Folders: folders, + MatchXraySelf: matchXraySelf, + } +} + +func (m *ProcessNameMatcher) Apply(ctx routing.Context) bool { + if len(ctx.GetSourceIPs()) == 0 { + return false + } + srcPort := ctx.GetSourcePort().String() + srcIP := ctx.GetSourceIPs()[0].String() + var network string + switch ctx.GetNetwork() { + case net.Network_TCP: + network = "tcp" + case net.Network_UDP: + network = "udp" + default: + return false + } + src, err := net.ParseDestination(strings.Join([]string{network, srcIP, srcPort}, ":")) + if err != nil { + return false + } + pid, name, absPath, err := net.FindProcess(src) + if err != nil { + if err != net.ErrNotLocal { + errors.LogError(context.Background(), "Unables to find local process name: ", err) + } + return false + } + if m.MatchXraySelf { + if pid == os.Getpid() { + return true + } + } + if slices.Contains(m.ProcessNames, name) { + return true + } + if slices.Contains(m.AbsPaths, absPath) { + return true + } + for _, f := range m.Folders { + if strings.HasPrefix(absPath, f) { + return true + } + } + return false +} diff --git a/subproject/Xray-core-main/app/router/condition_geoip.go b/subproject/Xray-core-main/app/router/condition_geoip.go new file mode 100644 index 00000000..cdfcb9fe --- /dev/null +++ b/subproject/Xray-core-main/app/router/condition_geoip.go @@ -0,0 +1,962 @@ +package router + +import ( + "context" + "net/netip" + "sort" + "strings" + "sync" + + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + + "go4.org/netipx" +) + +type GeoIPMatcher interface { + // TODO: (PERF) all net.IP -> netipx.Addr + + // Invalid IP always return false. + Match(ip net.IP) bool + + // Returns true if *any* IP is valid and match. + AnyMatch(ips []net.IP) bool + + // Returns true only if *all* IPs are valid and match. Any invalid IP, or non-matching valid IP, causes false. + Matches(ips []net.IP) bool + + // Filters IPs. Invalid IPs are silently dropped and not included in either result. + FilterIPs(ips []net.IP) (matched []net.IP, unmatched []net.IP) + + ToggleReverse() + + SetReverse(reverse bool) +} + +type GeoIPSet struct { + ipv4, ipv6 *netipx.IPSet + max4, max6 uint8 +} + +type HeuristicGeoIPMatcher struct { + ipset *GeoIPSet + reverse bool +} + +type ipBucket struct { + rep netip.Addr + ips []net.IP +} + +// Match implements GeoIPMatcher. +func (m *HeuristicGeoIPMatcher) Match(ip net.IP) bool { + ipx, ok := netipx.FromStdIP(ip) + if !ok { + return false + } + return m.matchAddr(ipx) +} + +func (m *HeuristicGeoIPMatcher) matchAddr(ipx netip.Addr) bool { + if ipx.Is4() { + return m.ipset.ipv4.Contains(ipx) != m.reverse + } + if ipx.Is6() { + return m.ipset.ipv6.Contains(ipx) != m.reverse + } + return false +} + +// AnyMatch implements GeoIPMatcher. +func (m *HeuristicGeoIPMatcher) AnyMatch(ips []net.IP) bool { + n := len(ips) + if n == 0 { + return false + } + + if n == 1 { + return m.Match(ips[0]) + } + + heur4 := m.ipset.max4 <= 24 + heur6 := m.ipset.max6 <= 64 + if !heur4 && !heur6 { + for _, ip := range ips { + if ipx, ok := netipx.FromStdIP(ip); ok { + if m.matchAddr(ipx) { + return true + } + } + } + return false + } + + buckets := make(map[[9]byte]struct{}, n) + for _, ip := range ips { + key, ok := prefixKeyFromIP(ip) + if !ok { + continue + } + heur := (key[0] == 4 && heur4) || (key[0] == 6 && heur6) + if heur { + if _, exists := buckets[key]; exists { + continue + } + } + ipx, ok := netipx.FromStdIP(ip) + if !ok { + continue + } + if m.matchAddr(ipx) { + return true + } + if heur { + buckets[key] = struct{}{} + } + } + return false +} + +// Matches implements GeoIPMatcher. +func (m *HeuristicGeoIPMatcher) Matches(ips []net.IP) bool { + n := len(ips) + if n == 0 { + return false + } + + if n == 1 { + return m.Match(ips[0]) + } + + heur4 := m.ipset.max4 <= 24 + heur6 := m.ipset.max6 <= 64 + if !heur4 && !heur6 { + for _, ip := range ips { + ipx, ok := netipx.FromStdIP(ip) + if !ok { + return false + } + if !m.matchAddr(ipx) { + return false + } + } + return true + } + + buckets := make(map[[9]byte]netip.Addr, n) + precise := make([]netip.Addr, 0, n) + + for _, ip := range ips { + key, ok := prefixKeyFromIP(ip) + if !ok { + return false + } + + if (key[0] == 4 && heur4) || (key[0] == 6 && heur6) { + if _, exists := buckets[key]; !exists { + ipx, ok := netipx.FromStdIP(ip) + if !ok { + return false + } + buckets[key] = ipx + } + } else { + ipx, ok := netipx.FromStdIP(ip) + if !ok { + return false + } + precise = append(precise, ipx) + } + } + + for _, ipx := range buckets { + if !m.matchAddr(ipx) { + return false + } + } + for _, ipx := range precise { + if !m.matchAddr(ipx) { + return false + } + } + return true +} + +func prefixKeyFromIP(ip net.IP) (key [9]byte, ok bool) { + if ip4 := ip.To4(); ip4 != nil { + key[0] = 4 + key[1] = ip4[0] + key[2] = ip4[1] + key[3] = ip4[2] // /24 + return key, true + } + if ip16 := ip.To16(); ip16 != nil { + key[0] = 6 + key[1] = ip16[0] + key[2] = ip16[1] + key[3] = ip16[2] + key[4] = ip16[3] + key[5] = ip16[4] + key[6] = ip16[5] + key[7] = ip16[6] + key[8] = ip16[7] // /64 + return key, true + } + return key, false // illegal +} + +// FilterIPs implements GeoIPMatcher. +func (m *HeuristicGeoIPMatcher) FilterIPs(ips []net.IP) (matched []net.IP, unmatched []net.IP) { + n := len(ips) + if n == 0 { + return []net.IP{}, []net.IP{} + } + + if n == 1 { + ipx, ok := netipx.FromStdIP(ips[0]) + if !ok { + return []net.IP{}, []net.IP{} + } + if m.matchAddr(ipx) { + return ips, []net.IP{} + } + return []net.IP{}, ips + } + + heur4 := m.ipset.max4 <= 24 + heur6 := m.ipset.max6 <= 64 + if !heur4 && !heur6 { + matched = make([]net.IP, 0, n) + unmatched = make([]net.IP, 0, n) + for _, ip := range ips { + ipx, ok := netipx.FromStdIP(ip) + if !ok { + continue // illegal ip, ignore + } + if m.matchAddr(ipx) { + matched = append(matched, ip) + } else { + unmatched = append(unmatched, ip) + } + } + return + } + + buckets := make(map[[9]byte]*ipBucket, n) + precise := make([]net.IP, 0, n) + + for _, ip := range ips { + key, ok := prefixKeyFromIP(ip) + if !ok { + continue // illegal ip, ignore + } + + if (key[0] == 4 && !heur4) || (key[0] == 6 && !heur6) { + precise = append(precise, ip) + continue + } + + b, exists := buckets[key] + if !exists { + // build bucket + ipx, ok := netipx.FromStdIP(ip) + if !ok { + continue // illegal ip, ignore + } + b = &ipBucket{ + rep: ipx, + ips: make([]net.IP, 0, 4), // for dns answer + } + buckets[key] = b + } + b.ips = append(b.ips, ip) + } + + matched = make([]net.IP, 0, n) + unmatched = make([]net.IP, 0, n) + for _, b := range buckets { + if m.matchAddr(b.rep) { + matched = append(matched, b.ips...) + } else { + unmatched = append(unmatched, b.ips...) + } + } + for _, ip := range precise { + ipx, ok := netipx.FromStdIP(ip) + if !ok { + continue // illegal ip, ignore + } + if m.matchAddr(ipx) { + matched = append(matched, ip) + } else { + unmatched = append(unmatched, ip) + } + } + return +} + +// ToggleReverse implements GeoIPMatcher. +func (m *HeuristicGeoIPMatcher) ToggleReverse() { + m.reverse = !m.reverse +} + +// SetReverse implements GeoIPMatcher. +func (m *HeuristicGeoIPMatcher) SetReverse(reverse bool) { + m.reverse = reverse +} + +type GeneralMultiGeoIPMatcher struct { + matchers []GeoIPMatcher +} + +// Match implements GeoIPMatcher. +func (mm *GeneralMultiGeoIPMatcher) Match(ip net.IP) bool { + for _, m := range mm.matchers { + if m.Match(ip) { + return true + } + } + return false +} + +// AnyMatch implements GeoIPMatcher. +func (mm *GeneralMultiGeoIPMatcher) AnyMatch(ips []net.IP) bool { + for _, m := range mm.matchers { + if m.AnyMatch(ips) { + return true + } + } + return false +} + +// Matches implements GeoIPMatcher. +func (mm *GeneralMultiGeoIPMatcher) Matches(ips []net.IP) bool { + for _, m := range mm.matchers { + if m.Matches(ips) { + return true + } + } + return false +} + +// FilterIPs implements GeoIPMatcher. +func (mm *GeneralMultiGeoIPMatcher) FilterIPs(ips []net.IP) (matched []net.IP, unmatched []net.IP) { + matched = make([]net.IP, 0, len(ips)) + unmatched = ips + for _, m := range mm.matchers { + if len(unmatched) == 0 { + break + } + var mtch []net.IP + mtch, unmatched = m.FilterIPs(unmatched) + if len(mtch) > 0 { + matched = append(matched, mtch...) + } + } + return +} + +// ToggleReverse implements GeoIPMatcher. +func (mm *GeneralMultiGeoIPMatcher) ToggleReverse() { + for _, m := range mm.matchers { + m.ToggleReverse() + } +} + +// SetReverse implements GeoIPMatcher. +func (mm *GeneralMultiGeoIPMatcher) SetReverse(reverse bool) { + for _, m := range mm.matchers { + m.SetReverse(reverse) + } +} + +type HeuristicMultiGeoIPMatcher struct { + matchers []*HeuristicGeoIPMatcher +} + +// Match implements GeoIPMatcher. +func (mm *HeuristicMultiGeoIPMatcher) Match(ip net.IP) bool { + ipx, ok := netipx.FromStdIP(ip) + if !ok { + return false + } + + for _, m := range mm.matchers { + if m.matchAddr(ipx) { + return true + } + } + return false +} + +// AnyMatch implements GeoIPMatcher. +func (mm *HeuristicMultiGeoIPMatcher) AnyMatch(ips []net.IP) bool { + n := len(ips) + if n == 0 { + return false + } + + if n == 1 { + return mm.Match(ips[0]) + } + + buckets := make(map[[9]byte]struct{}, n) + for _, ip := range ips { + var ipx netip.Addr + state := uint8(0) // 0 = Not initialized, 1 = Initialized, 4 = IPv4 can be skipped, 6 = IPv6 can be skipped + for _, m := range mm.matchers { + heur4 := m.ipset.max4 <= 24 + heur6 := m.ipset.max6 <= 64 + + if state == 0 && (heur4 || heur6) { + key, ok := prefixKeyFromIP(ip) + if !ok { + break + } + if _, exists := buckets[key]; exists { + state = key[0] + } else { + buckets[key] = struct{}{} + state = 1 + } + } + if (heur4 && state == 4) || (heur6 && state == 6) { + continue + } + + if !ipx.IsValid() { + nipx, ok := netipx.FromStdIP(ip) + if !ok { + break + } + ipx = nipx + } + if m.matchAddr(ipx) { + return true + } + } + } + return false +} + +// Matches implements GeoIPMatcher. +func (mm *HeuristicMultiGeoIPMatcher) Matches(ips []net.IP) bool { + n := len(ips) + if n == 0 { + return false + } + + if n == 1 { + return mm.Match(ips[0]) + } + + var views ipViews + for _, m := range mm.matchers { + if !views.ensureForMatcher(m, ips) { + return false + } + + matched := true + if m.ipset.max4 <= 24 { + for _, ipx := range views.buckets4 { + if !m.matchAddr(ipx) { + matched = false + break + } + } + } else { + for _, ipx := range views.precise4 { + if !m.matchAddr(ipx) { + matched = false + break + } + } + } + if !matched { + continue + } + + if m.ipset.max6 <= 64 { + for _, ipx := range views.buckets6 { + if !m.matchAddr(ipx) { + matched = false + break + } + } + } else { + for _, ipx := range views.precise6 { + if !m.matchAddr(ipx) { + matched = false + break + } + } + } + if matched { + return true + } + } + return false +} + +type ipViews struct { + buckets4, buckets6 map[[9]byte]netip.Addr + precise4, precise6 []netip.Addr +} + +func (v *ipViews) ensureForMatcher(m *HeuristicGeoIPMatcher, ips []net.IP) bool { + needHeur4 := m.ipset.max4 <= 24 && v.buckets4 == nil + needHeur6 := m.ipset.max6 <= 64 && v.buckets6 == nil + needPrec4 := m.ipset.max4 > 24 && v.precise4 == nil + needPrec6 := m.ipset.max6 > 64 && v.precise6 == nil + + if !needHeur4 && !needHeur6 && !needPrec4 && !needPrec6 { + return true + } + + if needHeur4 { + v.buckets4 = make(map[[9]byte]netip.Addr, len(ips)) + } + if needHeur6 { + v.buckets6 = make(map[[9]byte]netip.Addr, len(ips)) + } + if needPrec4 { + v.precise4 = make([]netip.Addr, 0, len(ips)) + } + if needPrec6 { + v.precise6 = make([]netip.Addr, 0, len(ips)) + } + + for _, ip := range ips { + key, ok := prefixKeyFromIP(ip) + if !ok { + return false + } + + switch key[0] { + case 4: + var ipx netip.Addr + if needHeur4 { + if _, exists := v.buckets4[key]; !exists { + ipx, ok = netipx.FromStdIP(ip) + if !ok { + return false + } + v.buckets4[key] = ipx + } + } + if needPrec4 { + if !ipx.IsValid() { + ipx, ok = netipx.FromStdIP(ip) + if !ok { + return false + } + } + v.precise4 = append(v.precise4, ipx) + } + case 6: + var ipx netip.Addr + if needHeur6 { + if _, exists := v.buckets6[key]; !exists { + ipx, ok = netipx.FromStdIP(ip) + if !ok { + return false + } + v.buckets6[key] = ipx + } + } + if needPrec6 { + if !ipx.IsValid() { + ipx, ok = netipx.FromStdIP(ip) + if !ok { + return false + } + } + v.precise6 = append(v.precise6, ipx) + } + default: + return false + } + } + + return true +} + +// FilterIPs implements GeoIPMatcher. +func (mm *HeuristicMultiGeoIPMatcher) FilterIPs(ips []net.IP) (matched []net.IP, unmatched []net.IP) { + n := len(ips) + if n == 0 { + return []net.IP{}, []net.IP{} + } + + if n == 1 { + ipx, ok := netipx.FromStdIP(ips[0]) + if !ok { + return []net.IP{}, []net.IP{} + } + for _, m := range mm.matchers { + if m.matchAddr(ipx) { + return ips, []net.IP{} + } + } + return []net.IP{}, ips + } + + var views ipBucketViews + + matched = make([]net.IP, 0, n) + for _, m := range mm.matchers { + views.ensureForMatcher(m, ips) + + if m.ipset.max4 <= 24 { + for key, b := range views.buckets4 { + if b == nil { + continue + } + if m.matchAddr(b.rep) { + views.buckets4[key] = nil + matched = append(matched, b.ips...) + } + } + } else { + for ipx, ip := range views.precise4 { + if ip == nil { + continue + } + if m.matchAddr(ipx) { + views.precise4[ipx] = nil + matched = append(matched, ip) + } + } + } + + if m.ipset.max6 <= 64 { + for key, b := range views.buckets6 { + if b == nil { + continue + } + if m.matchAddr(b.rep) { + views.buckets6[key] = nil + matched = append(matched, b.ips...) + } + } + } else { + for ipx, ip := range views.precise6 { + if ip == nil { + continue + } + if m.matchAddr(ipx) { + views.precise6[ipx] = nil + matched = append(matched, ip) + } + } + } + } + + unmatched = make([]net.IP, 0, n-len(matched)) + if views.buckets4 != nil { + for _, b := range views.buckets4 { + if b == nil { + continue + } + unmatched = append(unmatched, b.ips...) + } + } + if views.precise4 != nil { + for _, ip := range views.precise4 { + if ip == nil { + continue + } + unmatched = append(unmatched, ip) + } + } + if views.buckets6 != nil { + for _, b := range views.buckets6 { + if b == nil { + continue + } + unmatched = append(unmatched, b.ips...) + } + } + if views.precise6 != nil { + for _, ip := range views.precise6 { + if ip == nil { + continue + } + unmatched = append(unmatched, ip) + } + } + + return +} + +type ipBucketViews struct { + buckets4, buckets6 map[[9]byte]*ipBucket + precise4, precise6 map[netip.Addr]net.IP +} + +func (v *ipBucketViews) ensureForMatcher(m *HeuristicGeoIPMatcher, ips []net.IP) { + needHeur4 := m.ipset.max4 <= 24 && v.buckets4 == nil + needHeur6 := m.ipset.max6 <= 64 && v.buckets6 == nil + needPrec4 := m.ipset.max4 > 24 && v.precise4 == nil + needPrec6 := m.ipset.max6 > 64 && v.precise6 == nil + + if !needHeur4 && !needHeur6 && !needPrec4 && !needPrec6 { + return + } + + if needHeur4 { + v.buckets4 = make(map[[9]byte]*ipBucket, len(ips)) + } + if needHeur6 { + v.buckets6 = make(map[[9]byte]*ipBucket, len(ips)) + } + if needPrec4 { + v.precise4 = make(map[netip.Addr]net.IP, len(ips)) + } + if needPrec6 { + v.precise6 = make(map[netip.Addr]net.IP, len(ips)) + } + + for _, ip := range ips { + key, ok := prefixKeyFromIP(ip) + if !ok { + continue // illegal ip, ignore + } + + switch key[0] { + case 4: + var ipx netip.Addr + if needHeur4 { + b, exists := v.buckets4[key] + if !exists { + // build bucket + ipx, ok = netipx.FromStdIP(ip) + if !ok { + continue // illegal ip, ignore + } + b = &ipBucket{ + rep: ipx, + ips: make([]net.IP, 0, 4), // for dns answer + } + v.buckets4[key] = b + } + b.ips = append(b.ips, ip) + } + if needPrec4 { + if !ipx.IsValid() { + ipx, ok = netipx.FromStdIP(ip) + if !ok { + continue // illegal ip, ignore + } + } + v.precise4[ipx] = ip + } + case 6: + var ipx netip.Addr + if needHeur6 { + b, exists := v.buckets6[key] + if !exists { + // build bucket + ipx, ok = netipx.FromStdIP(ip) + if !ok { + continue // illegal ip, ignore + } + b = &ipBucket{ + rep: ipx, + ips: make([]net.IP, 0, 4), // for dns answer + } + v.buckets6[key] = b + } + b.ips = append(b.ips, ip) + } + if needPrec6 { + if !ipx.IsValid() { + ipx, ok = netipx.FromStdIP(ip) + if !ok { + continue // illegal ip, ignore + } + } + v.precise6[ipx] = ip + } + } + } +} + +// ToggleReverse implements GeoIPMatcher. +func (mm *HeuristicMultiGeoIPMatcher) ToggleReverse() { + for _, m := range mm.matchers { + m.ToggleReverse() + } +} + +// SetReverse implements GeoIPMatcher. +func (mm *HeuristicMultiGeoIPMatcher) SetReverse(reverse bool) { + for _, m := range mm.matchers { + m.SetReverse(reverse) + } +} + +type GeoIPSetFactory struct { + sync.Mutex + shared map[string]*GeoIPSet // TODO: cleanup +} + +var ipsetFactory = GeoIPSetFactory{shared: make(map[string]*GeoIPSet)} + +func (f *GeoIPSetFactory) GetOrCreate(key string, cidrGroups [][]*CIDR) (*GeoIPSet, error) { + f.Lock() + defer f.Unlock() + + if ipset := f.shared[key]; ipset != nil { + return ipset, nil + } + + ipset, err := f.Create(cidrGroups...) + if err == nil { + f.shared[key] = ipset + } + return ipset, err +} + +func (f *GeoIPSetFactory) Create(cidrGroups ...[]*CIDR) (*GeoIPSet, error) { + var ipv4Builder, ipv6Builder netipx.IPSetBuilder + + for _, cidrGroup := range cidrGroups { + for i, cidrEntry := range cidrGroup { + cidrGroup[i] = nil + ipBytes := cidrEntry.GetIp() + prefixLen := int(cidrEntry.GetPrefix()) + + addr, ok := netip.AddrFromSlice(ipBytes) + if !ok { + errors.LogError(context.Background(), "ignore invalid IP byte slice: ", ipBytes) + continue + } + + prefix := netip.PrefixFrom(addr, prefixLen) + if !prefix.IsValid() { + errors.LogError(context.Background(), "ignore created invalid prefix from addr ", addr, " and length ", prefixLen) + continue + } + + if addr.Is4() { + ipv4Builder.AddPrefix(prefix) + } else if addr.Is6() { + ipv6Builder.AddPrefix(prefix) + } + } + } + + ipv4, err := ipv4Builder.IPSet() + if err != nil { + return nil, errors.New("failed to build IPv4 set").Base(err) + } + ipv6, err := ipv6Builder.IPSet() + if err != nil { + return nil, errors.New("failed to build IPv6 set").Base(err) + } + + var max4, max6 int + + for _, p := range ipv4.Prefixes() { + if b := p.Bits(); b > max4 { + max4 = b + } + } + for _, p := range ipv6.Prefixes() { + if b := p.Bits(); b > max6 { + max6 = b + } + } + + if max4 == 0 { + max4 = 0xff + } + if max6 == 0 { + max6 = 0xff + } + + return &GeoIPSet{ipv4: ipv4, ipv6: ipv6, max4: uint8(max4), max6: uint8(max6)}, nil +} + +func BuildOptimizedGeoIPMatcher(geoips ...*GeoIP) (GeoIPMatcher, error) { + n := len(geoips) + if n == 0 { + return nil, errors.New("no geoip configs provided") + } + + var subs []*HeuristicGeoIPMatcher + pos := make([]*GeoIP, 0, n) + neg := make([]*GeoIP, 0, n/2) + + for _, geoip := range geoips { + if geoip == nil { + return nil, errors.New("geoip entry is nil") + } + if geoip.CountryCode == "" { + ipset, err := ipsetFactory.Create(geoip.Cidr) + if err != nil { + return nil, err + } + subs = append(subs, &HeuristicGeoIPMatcher{ipset: ipset, reverse: geoip.ReverseMatch}) + continue + } + if !geoip.ReverseMatch { + pos = append(pos, geoip) + } else { + neg = append(neg, geoip) + } + } + + buildIPSet := func(mergeables []*GeoIP) (*GeoIPSet, error) { + n := len(mergeables) + if n == 0 { + return nil, nil + } + + sort.Slice(mergeables, func(i, j int) bool { + gi, gj := mergeables[i], mergeables[j] + return gi.CountryCode < gj.CountryCode + }) + + var sb strings.Builder + sb.Grow(n * 3) // xx, + cidrGroups := make([][]*CIDR, 0, n) + var last *GeoIP + for i, geoip := range mergeables { + if i == 0 || (geoip.CountryCode != last.CountryCode) { + last = geoip + sb.WriteString(geoip.CountryCode) + sb.WriteString(",") + cidrGroups = append(cidrGroups, geoip.Cidr) + } + } + + return ipsetFactory.GetOrCreate(sb.String(), cidrGroups) + } + + ipset, err := buildIPSet(pos) + if err != nil { + return nil, err + } + if ipset != nil { + subs = append(subs, &HeuristicGeoIPMatcher{ipset: ipset, reverse: false}) + } + + ipset, err = buildIPSet(neg) + if err != nil { + return nil, err + } + if ipset != nil { + subs = append(subs, &HeuristicGeoIPMatcher{ipset: ipset, reverse: true}) + } + + switch len(subs) { + case 0: + return nil, errors.New("no valid geoip matcher") + case 1: + return subs[0], nil + default: + return &HeuristicMultiGeoIPMatcher{matchers: subs}, nil + } +} diff --git a/subproject/Xray-core-main/app/router/condition_geoip_test.go b/subproject/Xray-core-main/app/router/condition_geoip_test.go new file mode 100644 index 00000000..b712db9e --- /dev/null +++ b/subproject/Xray-core-main/app/router/condition_geoip_test.go @@ -0,0 +1,266 @@ +package router_test + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/xtls/xray-core/app/router" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/platform" + "github.com/xtls/xray-core/common/platform/filesystem" + "google.golang.org/protobuf/proto" +) + +func getAssetPath(file string) (string, error) { + path := platform.GetAssetLocation(file) + _, err := os.Stat(path) + if os.IsNotExist(err) { + path := filepath.Join("..", "..", "resources", file) + _, err := os.Stat(path) + if os.IsNotExist(err) { + return "", fmt.Errorf("can't find %s in standard asset locations or {project_root}/resources", file) + } + if err != nil { + return "", fmt.Errorf("can't stat %s: %v", path, err) + } + return path, nil + } + if err != nil { + return "", fmt.Errorf("can't stat %s: %v", path, err) + } + + return path, nil +} + +func TestGeoIPMatcher(t *testing.T) { + cidrList := []*router.CIDR{ + {Ip: []byte{0, 0, 0, 0}, Prefix: 8}, + {Ip: []byte{10, 0, 0, 0}, Prefix: 8}, + {Ip: []byte{100, 64, 0, 0}, Prefix: 10}, + {Ip: []byte{127, 0, 0, 0}, Prefix: 8}, + {Ip: []byte{169, 254, 0, 0}, Prefix: 16}, + {Ip: []byte{172, 16, 0, 0}, Prefix: 12}, + {Ip: []byte{192, 0, 0, 0}, Prefix: 24}, + {Ip: []byte{192, 0, 2, 0}, Prefix: 24}, + {Ip: []byte{192, 168, 0, 0}, Prefix: 16}, + {Ip: []byte{192, 18, 0, 0}, Prefix: 15}, + {Ip: []byte{198, 51, 100, 0}, Prefix: 24}, + {Ip: []byte{203, 0, 113, 0}, Prefix: 24}, + {Ip: []byte{8, 8, 8, 8}, Prefix: 32}, + {Ip: []byte{91, 108, 4, 0}, Prefix: 16}, + } + + matcher, err := router.BuildOptimizedGeoIPMatcher(&router.GeoIP{ + Cidr: cidrList, + }) + common.Must(err) + + testCases := []struct { + Input string + Output bool + }{ + { + Input: "192.168.1.1", + Output: true, + }, + { + Input: "192.0.0.0", + Output: true, + }, + { + Input: "192.0.1.0", + Output: false, + }, + { + Input: "0.1.0.0", + Output: true, + }, + { + Input: "1.0.0.1", + Output: false, + }, + { + Input: "8.8.8.7", + Output: false, + }, + { + Input: "8.8.8.8", + Output: true, + }, + { + Input: "2001:cdba::3257:9652", + Output: false, + }, + { + Input: "91.108.255.254", + Output: true, + }, + } + + for _, testCase := range testCases { + ip := net.ParseAddress(testCase.Input).IP() + actual := matcher.Match(ip) + if actual != testCase.Output { + t.Error("expect input", testCase.Input, "to be", testCase.Output, ", but actually", actual) + } + } +} + +func TestGeoIPMatcherRegression(t *testing.T) { + cidrList := []*router.CIDR{ + {Ip: []byte{98, 108, 20, 0}, Prefix: 22}, + {Ip: []byte{98, 108, 20, 0}, Prefix: 23}, + } + + matcher, err := router.BuildOptimizedGeoIPMatcher(&router.GeoIP{ + Cidr: cidrList, + }) + common.Must(err) + + testCases := []struct { + Input string + Output bool + }{ + { + Input: "98.108.22.11", + Output: true, + }, + { + Input: "98.108.25.0", + Output: false, + }, + } + + for _, testCase := range testCases { + ip := net.ParseAddress(testCase.Input).IP() + actual := matcher.Match(ip) + if actual != testCase.Output { + t.Error("expect input", testCase.Input, "to be", testCase.Output, ", but actually", actual) + } + } +} + +func TestGeoIPReverseMatcher(t *testing.T) { + cidrList := []*router.CIDR{ + {Ip: []byte{8, 8, 8, 8}, Prefix: 32}, + {Ip: []byte{91, 108, 4, 0}, Prefix: 16}, + } + matcher, err := router.BuildOptimizedGeoIPMatcher(&router.GeoIP{ + Cidr: cidrList, + }) + common.Must(err) + matcher.SetReverse(true) // Reverse match + + testCases := []struct { + Input string + Output bool + }{ + { + Input: "8.8.8.8", + Output: false, + }, + { + Input: "2001:cdba::3257:9652", + Output: true, + }, + { + Input: "91.108.255.254", + Output: false, + }, + } + + for _, testCase := range testCases { + ip := net.ParseAddress(testCase.Input).IP() + actual := matcher.Match(ip) + if actual != testCase.Output { + t.Error("expect input", testCase.Input, "to be", testCase.Output, ", but actually", actual) + } + } +} + +func TestGeoIPMatcher4CN(t *testing.T) { + ips, err := loadGeoIP("CN") + common.Must(err) + + matcher, err := router.BuildOptimizedGeoIPMatcher(&router.GeoIP{ + Cidr: ips, + }) + common.Must(err) + + if matcher.Match([]byte{8, 8, 8, 8}) { + t.Error("expect CN geoip doesn't contain 8.8.8.8, but actually does") + } +} + +func TestGeoIPMatcher6US(t *testing.T) { + ips, err := loadGeoIP("US") + common.Must(err) + + matcher, err := router.BuildOptimizedGeoIPMatcher(&router.GeoIP{ + Cidr: ips, + }) + common.Must(err) + + if !matcher.Match(net.ParseAddress("2001:4860:4860::8888").IP()) { + t.Error("expect US geoip contain 2001:4860:4860::8888, but actually not") + } +} + +func loadGeoIP(country string) ([]*router.CIDR, error) { + path, err := getAssetPath("geoip.dat") + if err != nil { + return nil, err + } + geoipBytes, err := filesystem.ReadFile(path) + if err != nil { + return nil, err + } + + var geoipList router.GeoIPList + if err := proto.Unmarshal(geoipBytes, &geoipList); err != nil { + return nil, err + } + + for _, geoip := range geoipList.Entry { + if geoip.CountryCode == country { + return geoip.Cidr, nil + } + } + + panic("country not found: " + country) +} + +func BenchmarkGeoIPMatcher4CN(b *testing.B) { + ips, err := loadGeoIP("CN") + common.Must(err) + + matcher, err := router.BuildOptimizedGeoIPMatcher(&router.GeoIP{ + Cidr: ips, + }) + common.Must(err) + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + _ = matcher.Match([]byte{8, 8, 8, 8}) + } +} + +func BenchmarkGeoIPMatcher6US(b *testing.B) { + ips, err := loadGeoIP("US") + common.Must(err) + + matcher, err := router.BuildOptimizedGeoIPMatcher(&router.GeoIP{ + Cidr: ips, + }) + common.Must(err) + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + _ = matcher.Match(net.ParseAddress("2001:4860:4860::8888").IP()) + } +} diff --git a/subproject/Xray-core-main/app/router/condition_serialize_test.go b/subproject/Xray-core-main/app/router/condition_serialize_test.go new file mode 100644 index 00000000..4c6ff464 --- /dev/null +++ b/subproject/Xray-core-main/app/router/condition_serialize_test.go @@ -0,0 +1,167 @@ +package router_test + +import ( + "bytes" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + "github.com/xtls/xray-core/app/router" + "github.com/xtls/xray-core/common/platform/filesystem" +) + +func TestDomainMatcherSerialization(t *testing.T) { + + domains := []*router.Domain{ + {Type: router.Domain_Domain, Value: "google.com"}, + {Type: router.Domain_Domain, Value: "v2ray.com"}, + {Type: router.Domain_Full, Value: "full.example.com"}, + } + + var buf bytes.Buffer + if err := router.SerializeDomainMatcher(domains, &buf); err != nil { + t.Fatalf("Serialize failed: %v", err) + } + + matcher, err := router.NewDomainMatcherFromBuffer(buf.Bytes()) + if err != nil { + t.Fatalf("Deserialize failed: %v", err) + } + + dMatcher := &router.DomainMatcher{ + Matchers: matcher, + } + testCases := []struct { + Input string + Match bool + }{ + {"google.com", true}, + {"maps.google.com", true}, + {"v2ray.com", true}, + {"full.example.com", true}, + + {"example.com", false}, + } + + for _, tc := range testCases { + if res := dMatcher.ApplyDomain(tc.Input); res != tc.Match { + t.Errorf("Match(%s) = %v, want %v", tc.Input, res, tc.Match) + } + } +} + +func TestGeoSiteSerialization(t *testing.T) { + sites := []*router.GeoSite{ + { + CountryCode: "CN", + Domain: []*router.Domain{ + {Type: router.Domain_Domain, Value: "baidu.cn"}, + {Type: router.Domain_Domain, Value: "qq.com"}, + }, + }, + { + CountryCode: "US", + Domain: []*router.Domain{ + {Type: router.Domain_Domain, Value: "google.com"}, + {Type: router.Domain_Domain, Value: "facebook.com"}, + }, + }, + } + + var buf bytes.Buffer + if err := router.SerializeGeoSiteList(sites, nil, nil, &buf); err != nil { + t.Fatalf("SerializeGeoSiteList failed: %v", err) + } + + tmp := t.TempDir() + path := filepath.Join(tmp, "matcher.cache") + + f, err := os.Create(path) + require.NoError(t, err) + _, err = f.Write(buf.Bytes()) + require.NoError(t, err) + f.Close() + + f, err = os.Open(path) + require.NoError(t, err) + defer f.Close() + + require.NoError(t, err) + data, _ := filesystem.ReadFile(path) + + // cn + gp, err := router.LoadGeoSiteMatcher(bytes.NewReader(data), "CN") + if err != nil { + t.Fatalf("LoadGeoSiteMatcher(CN) failed: %v", err) + } + + cnMatcher := &router.DomainMatcher{ + Matchers: gp, + } + + if !cnMatcher.ApplyDomain("baidu.cn") { + t.Error("CN matcher should match baidu.cn") + } + if cnMatcher.ApplyDomain("google.com") { + t.Error("CN matcher should NOT match google.com") + } + + // us + gp, err = router.LoadGeoSiteMatcher(bytes.NewReader(data), "US") + if err != nil { + t.Fatalf("LoadGeoSiteMatcher(US) failed: %v", err) + } + + usMatcher := &router.DomainMatcher{ + Matchers: gp, + } + if !usMatcher.ApplyDomain("google.com") { + t.Error("US matcher should match google.com") + } + if usMatcher.ApplyDomain("baidu.cn") { + t.Error("US matcher should NOT match baidu.cn") + } + + // unknown + _, err = router.LoadGeoSiteMatcher(bytes.NewReader(data), "unknown") + if err == nil { + t.Error("LoadGeoSiteMatcher(unknown) should fail") + } +} +func TestGeoSiteSerializationWithDeps(t *testing.T) { + sites := []*router.GeoSite{ + { + CountryCode: "geosite:cn", + Domain: []*router.Domain{ + {Type: router.Domain_Domain, Value: "baidu.cn"}, + }, + }, + { + CountryCode: "geosite:google@cn", + Domain: []*router.Domain{ + {Type: router.Domain_Domain, Value: "google.cn"}, + }, + }, + { + CountryCode: "rule-1", + Domain: []*router.Domain{ + {Type: router.Domain_Domain, Value: "google.com"}, + }, + }, + } + deps := map[string][]string{ + "rule-1": {"geosite:cn", "geosite:google@cn"}, + } + + var buf bytes.Buffer + err := router.SerializeGeoSiteList(sites, deps, nil, &buf) + require.NoError(t, err) + + matcher, err := router.LoadGeoSiteMatcher(bytes.NewReader(buf.Bytes()), "rule-1") + require.NoError(t, err) + + require.True(t, matcher.Match("google.com") != nil) + require.True(t, matcher.Match("baidu.cn") != nil) + require.True(t, matcher.Match("google.cn") != nil) +} diff --git a/subproject/Xray-core-main/app/router/condition_test.go b/subproject/Xray-core-main/app/router/condition_test.go new file mode 100644 index 00000000..1272aef6 --- /dev/null +++ b/subproject/Xray-core-main/app/router/condition_test.go @@ -0,0 +1,460 @@ +package router_test + +import ( + "strconv" + "testing" + + . "github.com/xtls/xray-core/app/router" + "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/platform/filesystem" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/protocol/http" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/features/routing" + routing_session "github.com/xtls/xray-core/features/routing/session" + "google.golang.org/protobuf/proto" +) + +func withBackground() routing.Context { + return &routing_session.Context{} +} + +func withOutbound(outbound *session.Outbound) routing.Context { + return &routing_session.Context{Outbound: outbound} +} + +func withInbound(inbound *session.Inbound) routing.Context { + return &routing_session.Context{Inbound: inbound} +} + +func withContent(content *session.Content) routing.Context { + return &routing_session.Context{Content: content} +} + +func TestRoutingRule(t *testing.T) { + type ruleTest struct { + input routing.Context + output bool + } + + cases := []struct { + rule *RoutingRule + test []ruleTest + }{ + { + rule: &RoutingRule{ + Domain: []*Domain{ + { + Value: "example.com", + Type: Domain_Plain, + }, + { + Value: "google.com", + Type: Domain_Domain, + }, + { + Value: "^facebook\\.com$", + Type: Domain_Regex, + }, + }, + }, + test: []ruleTest{ + { + input: withOutbound(&session.Outbound{Target: net.TCPDestination(net.DomainAddress("example.com"), 80)}), + output: true, + }, + { + input: withOutbound(&session.Outbound{Target: net.TCPDestination(net.DomainAddress("www.example.com.www"), 80)}), + output: true, + }, + { + input: withOutbound(&session.Outbound{Target: net.TCPDestination(net.DomainAddress("example.co"), 80)}), + output: false, + }, + { + input: withOutbound(&session.Outbound{Target: net.TCPDestination(net.DomainAddress("www.google.com"), 80)}), + output: true, + }, + { + input: withOutbound(&session.Outbound{Target: net.TCPDestination(net.DomainAddress("facebook.com"), 80)}), + output: true, + }, + { + input: withOutbound(&session.Outbound{Target: net.TCPDestination(net.DomainAddress("www.facebook.com"), 80)}), + output: false, + }, + { + input: withBackground(), + output: false, + }, + }, + }, + { + rule: &RoutingRule{ + Geoip: []*GeoIP{ + { + Cidr: []*CIDR{ + { + Ip: []byte{8, 8, 8, 8}, + Prefix: 32, + }, + { + Ip: []byte{8, 8, 8, 8}, + Prefix: 32, + }, + { + Ip: net.ParseAddress("2001:0db8:85a3:0000:0000:8a2e:0370:7334").IP(), + Prefix: 128, + }, + }, + }, + }, + }, + test: []ruleTest{ + { + input: withOutbound(&session.Outbound{Target: net.TCPDestination(net.ParseAddress("8.8.8.8"), 80)}), + output: true, + }, + { + input: withOutbound(&session.Outbound{Target: net.TCPDestination(net.ParseAddress("8.8.4.4"), 80)}), + output: false, + }, + { + input: withOutbound(&session.Outbound{Target: net.TCPDestination(net.ParseAddress("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), 80)}), + output: true, + }, + { + input: withBackground(), + output: false, + }, + }, + }, + { + rule: &RoutingRule{ + SourceGeoip: []*GeoIP{ + { + Cidr: []*CIDR{ + { + Ip: []byte{192, 168, 0, 0}, + Prefix: 16, + }, + }, + }, + }, + }, + test: []ruleTest{ + { + input: withInbound(&session.Inbound{Source: net.TCPDestination(net.ParseAddress("192.168.0.1"), 80)}), + output: true, + }, + { + input: withInbound(&session.Inbound{Source: net.TCPDestination(net.ParseAddress("10.0.0.1"), 80)}), + output: false, + }, + }, + }, + { + rule: &RoutingRule{ + UserEmail: []string{ + "admin@example.com", + }, + }, + test: []ruleTest{ + { + input: withInbound(&session.Inbound{User: &protocol.MemoryUser{Email: "admin@example.com"}}), + output: true, + }, + { + input: withInbound(&session.Inbound{User: &protocol.MemoryUser{Email: "love@example.com"}}), + output: false, + }, + { + input: withBackground(), + output: false, + }, + }, + }, + { + rule: &RoutingRule{ + Protocol: []string{"http"}, + }, + test: []ruleTest{ + { + input: withContent(&session.Content{Protocol: (&http.SniffHeader{}).Protocol()}), + output: true, + }, + }, + }, + { + rule: &RoutingRule{ + InboundTag: []string{"test", "test1"}, + }, + test: []ruleTest{ + { + input: withInbound(&session.Inbound{Tag: "test"}), + output: true, + }, + { + input: withInbound(&session.Inbound{Tag: "test2"}), + output: false, + }, + }, + }, + { + rule: &RoutingRule{ + PortList: &net.PortList{ + Range: []*net.PortRange{ + {From: 443, To: 443}, + {From: 1000, To: 1100}, + }, + }, + }, + test: []ruleTest{ + { + input: withOutbound(&session.Outbound{Target: net.TCPDestination(net.LocalHostIP, 443)}), + output: true, + }, + { + input: withOutbound(&session.Outbound{Target: net.TCPDestination(net.LocalHostIP, 1100)}), + output: true, + }, + { + input: withOutbound(&session.Outbound{Target: net.TCPDestination(net.LocalHostIP, 1005)}), + output: true, + }, + { + input: withOutbound(&session.Outbound{Target: net.TCPDestination(net.LocalHostIP, 53)}), + output: false, + }, + }, + }, + { + rule: &RoutingRule{ + SourcePortList: &net.PortList{ + Range: []*net.PortRange{ + {From: 123, To: 123}, + {From: 9993, To: 9999}, + }, + }, + }, + test: []ruleTest{ + { + input: withInbound(&session.Inbound{Source: net.UDPDestination(net.LocalHostIP, 123)}), + output: true, + }, + { + input: withInbound(&session.Inbound{Source: net.UDPDestination(net.LocalHostIP, 9999)}), + output: true, + }, + { + input: withInbound(&session.Inbound{Source: net.UDPDestination(net.LocalHostIP, 9994)}), + output: true, + }, + { + input: withInbound(&session.Inbound{Source: net.UDPDestination(net.LocalHostIP, 53)}), + output: false, + }, + }, + }, + { + rule: &RoutingRule{ + Protocol: []string{"http"}, + Attributes: map[string]string{ + ":path": "/test", + }, + }, + test: []ruleTest{ + { + input: withContent(&session.Content{Protocol: "http/1.1", Attributes: map[string]string{":path": "/test/1"}}), + output: true, + }, + }, + }, + { + rule: &RoutingRule{ + Attributes: map[string]string{ + "Custom": "p([a-z]+)ch", + }, + }, + test: []ruleTest{ + { + input: withContent(&session.Content{Attributes: map[string]string{"custom": "peach"}}), + output: true, + }, + }, + }, + } + + for _, test := range cases { + cond, err := test.rule.BuildCondition() + common.Must(err) + + for _, subtest := range test.test { + actual := cond.Apply(subtest.input) + if actual != subtest.output { + t.Error("test case failed: ", subtest.input, " expected ", subtest.output, " but got ", actual) + } + } + } +} + +func loadGeoSite(country string) ([]*Domain, error) { + path, err := getAssetPath("geosite.dat") + if err != nil { + return nil, err + } + geositeBytes, err := filesystem.ReadFile(path) + if err != nil { + return nil, err + } + + var geositeList GeoSiteList + if err := proto.Unmarshal(geositeBytes, &geositeList); err != nil { + return nil, err + } + + for _, site := range geositeList.Entry { + if site.CountryCode == country { + return site.Domain, nil + } + } + + return nil, errors.New("country not found: " + country) +} + +func TestChinaSites(t *testing.T) { + domains, err := loadGeoSite("CN") + common.Must(err) + + acMatcher, err := NewMphMatcherGroup(domains) + common.Must(err) + + type TestCase struct { + Domain string + Output bool + } + testCases := []TestCase{ + { + Domain: "163.com", + Output: true, + }, + { + Domain: "163.com", + Output: true, + }, + { + Domain: "164.com", + Output: false, + }, + { + Domain: "164.com", + Output: false, + }, + } + + for i := 0; i < 1024; i++ { + testCases = append(testCases, TestCase{Domain: strconv.Itoa(i) + ".not-exists.com", Output: false}) + } + + for _, testCase := range testCases { + r := acMatcher.ApplyDomain(testCase.Domain) + if r != testCase.Output { + t.Error("ACDomainMatcher expected output ", testCase.Output, " for domain ", testCase.Domain, " but got ", r) + } + } +} + +func BenchmarkMphDomainMatcher(b *testing.B) { + domains, err := loadGeoSite("CN") + common.Must(err) + + matcher, err := NewMphMatcherGroup(domains) + common.Must(err) + + type TestCase struct { + Domain string + Output bool + } + testCases := []TestCase{ + { + Domain: "163.com", + Output: true, + }, + { + Domain: "163.com", + Output: true, + }, + { + Domain: "164.com", + Output: false, + }, + { + Domain: "164.com", + Output: false, + }, + } + + for i := 0; i < 1024; i++ { + testCases = append(testCases, TestCase{Domain: strconv.Itoa(i) + ".not-exists.com", Output: false}) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + for _, testCase := range testCases { + _ = matcher.ApplyDomain(testCase.Domain) + } + } +} + +func BenchmarkMultiGeoIPMatcher(b *testing.B) { + var geoips []*GeoIP + + { + ips, err := loadGeoIP("CN") + common.Must(err) + geoips = append(geoips, &GeoIP{ + CountryCode: "CN", + Cidr: ips, + }) + } + + { + ips, err := loadGeoIP("JP") + common.Must(err) + geoips = append(geoips, &GeoIP{ + CountryCode: "JP", + Cidr: ips, + }) + } + + { + ips, err := loadGeoIP("CA") + common.Must(err) + geoips = append(geoips, &GeoIP{ + CountryCode: "CA", + Cidr: ips, + }) + } + + { + ips, err := loadGeoIP("US") + common.Must(err) + geoips = append(geoips, &GeoIP{ + CountryCode: "US", + Cidr: ips, + }) + } + + matcher, err := NewIPMatcher(geoips, MatcherAsType_Target) + common.Must(err) + + ctx := withOutbound(&session.Outbound{Target: net.TCPDestination(net.ParseAddress("8.8.8.8"), 80)}) + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + _ = matcher.Apply(ctx) + } +} diff --git a/subproject/Xray-core-main/app/router/config.go b/subproject/Xray-core-main/app/router/config.go new file mode 100644 index 00000000..4acbaf41 --- /dev/null +++ b/subproject/Xray-core-main/app/router/config.go @@ -0,0 +1,208 @@ +package router + +import ( + "context" + "regexp" + "runtime" + "strings" + + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/platform" + "github.com/xtls/xray-core/common/platform/filesystem" + "github.com/xtls/xray-core/features/outbound" + "github.com/xtls/xray-core/features/routing" +) + +type Rule struct { + Tag string + RuleTag string + Balancer *Balancer + Condition Condition + Webhook *WebhookNotifier +} + +func (r *Rule) GetTag() (string, error) { + if r.Balancer != nil { + return r.Balancer.PickOutbound() + } + return r.Tag, nil +} + +// Apply checks rule matching of current routing context. +func (r *Rule) Apply(ctx routing.Context) bool { + return r.Condition.Apply(ctx) +} + +func (rr *RoutingRule) BuildCondition() (Condition, error) { + conds := NewConditionChan() + + if len(rr.InboundTag) > 0 { + conds.Add(NewInboundTagMatcher(rr.InboundTag)) + } + + if len(rr.Networks) > 0 { + conds.Add(NewNetworkMatcher(rr.Networks)) + } + + if len(rr.Protocol) > 0 { + conds.Add(NewProtocolMatcher(rr.Protocol)) + } + + if rr.PortList != nil { + conds.Add(NewPortMatcher(rr.PortList, MatcherAsType_Target)) + } + + if rr.SourcePortList != nil { + conds.Add(NewPortMatcher(rr.SourcePortList, MatcherAsType_Source)) + } + + if rr.LocalPortList != nil { + conds.Add(NewPortMatcher(rr.LocalPortList, MatcherAsType_Local)) + } + + if rr.VlessRouteList != nil { + conds.Add(NewPortMatcher(rr.VlessRouteList, MatcherAsType_VlessRoute)) + } + + if len(rr.UserEmail) > 0 { + conds.Add(NewUserMatcher(rr.UserEmail)) + } + + if len(rr.Attributes) > 0 { + configuredKeys := make(map[string]*regexp.Regexp) + for key, value := range rr.Attributes { + configuredKeys[strings.ToLower(key)] = regexp.MustCompile(value) + } + conds.Add(&AttributeMatcher{configuredKeys}) + } + + if len(rr.Geoip) > 0 { + cond, err := NewIPMatcher(rr.Geoip, MatcherAsType_Target) + if err != nil { + return nil, err + } + conds.Add(cond) + rr.Geoip = nil + runtime.GC() + } + + if len(rr.SourceGeoip) > 0 { + cond, err := NewIPMatcher(rr.SourceGeoip, MatcherAsType_Source) + if err != nil { + return nil, err + } + conds.Add(cond) + rr.SourceGeoip = nil + runtime.GC() + } + + if len(rr.LocalGeoip) > 0 { + cond, err := NewIPMatcher(rr.LocalGeoip, MatcherAsType_Local) + if err != nil { + return nil, err + } + conds.Add(cond) + errors.LogWarning(context.Background(), "Due to some limitations, in UDP connections, localIP is always equal to listen interface IP, so \"localIP\" rule condition does not work properly on UDP inbound connections that listen on all interfaces") + rr.LocalGeoip = nil + runtime.GC() + } + + if len(rr.Domain) > 0 { + var matcher *DomainMatcher + var err error + // Check if domain matcher cache is provided via environment + domainMatcherPath := platform.NewEnvFlag(platform.MphCachePath).GetValue(func() string { return "" }) + + if domainMatcherPath != "" { + matcher, err = GetDomainMatcherWithRuleTag(domainMatcherPath, rr.RuleTag) + if err != nil { + return nil, errors.New("failed to build domain condition from cached MphDomainMatcher").Base(err) + } + errors.LogDebug(context.Background(), "MphDomainMatcher loaded from cache for ", rr.RuleTag, " rule tag)") + + } else { + matcher, err = NewMphMatcherGroup(rr.Domain) + if err != nil { + return nil, errors.New("failed to build domain condition with MphDomainMatcher").Base(err) + } + errors.LogDebug(context.Background(), "MphDomainMatcher is enabled for ", len(rr.Domain), " domain rule(s)") + } + conds.Add(matcher) + rr.Domain = nil + runtime.GC() + } + + if len(rr.Process) > 0 { + conds.Add(NewProcessNameMatcher(rr.Process)) + } + + if conds.Len() == 0 { + return nil, errors.New("this rule has no effective fields").AtWarning() + } + + return conds, nil +} + +// Build builds the balancing rule +func (br *BalancingRule) Build(ohm outbound.Manager, dispatcher routing.Dispatcher) (*Balancer, error) { + switch strings.ToLower(br.Strategy) { + case "leastping": + return &Balancer{ + selectors: br.OutboundSelector, + strategy: &LeastPingStrategy{}, + fallbackTag: br.FallbackTag, + ohm: ohm, + }, nil + case "roundrobin": + return &Balancer{ + selectors: br.OutboundSelector, + strategy: &RoundRobinStrategy{FallbackTag: br.FallbackTag}, + fallbackTag: br.FallbackTag, + ohm: ohm, + }, nil + case "leastload": + i, err := br.StrategySettings.GetInstance() + if err != nil { + return nil, err + } + s, ok := i.(*StrategyLeastLoadConfig) + if !ok { + return nil, errors.New("not a StrategyLeastLoadConfig").AtError() + } + leastLoadStrategy := NewLeastLoadStrategy(s) + return &Balancer{ + selectors: br.OutboundSelector, + ohm: ohm, + fallbackTag: br.FallbackTag, + strategy: leastLoadStrategy, + }, nil + case "random": + fallthrough + case "": + return &Balancer{ + selectors: br.OutboundSelector, + ohm: ohm, + fallbackTag: br.FallbackTag, + strategy: &RandomStrategy{FallbackTag: br.FallbackTag}, + }, nil + default: + return nil, errors.New("unrecognized balancer type") + } +} + +func GetDomainMatcherWithRuleTag(domainMatcherPath string, ruleTag string) (*DomainMatcher, error) { + f, err := filesystem.NewFileReader(domainMatcherPath) + if err != nil { + return nil, errors.New("failed to load file: ", domainMatcherPath).Base(err) + } + defer f.Close() + + g, err := LoadGeoSiteMatcher(f, ruleTag) + if err != nil { + return nil, errors.New("failed to load file:", domainMatcherPath).Base(err) + } + return &DomainMatcher{ + Matchers: g, + }, nil + +} diff --git a/subproject/Xray-core-main/app/router/config.pb.go b/subproject/Xray-core-main/app/router/config.pb.go new file mode 100644 index 00000000..40676024 --- /dev/null +++ b/subproject/Xray-core-main/app/router/config.pb.go @@ -0,0 +1,1300 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: app/router/config.proto + +package router + +import ( + net "github.com/xtls/xray-core/common/net" + serial "github.com/xtls/xray-core/common/serial" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Type of domain value. +type Domain_Type int32 + +const ( + // The value is used as is. + Domain_Plain Domain_Type = 0 + // The value is used as a regular expression. + Domain_Regex Domain_Type = 1 + // The value is a root domain. + Domain_Domain Domain_Type = 2 + // The value is a domain. + Domain_Full Domain_Type = 3 +) + +// Enum value maps for Domain_Type. +var ( + Domain_Type_name = map[int32]string{ + 0: "Plain", + 1: "Regex", + 2: "Domain", + 3: "Full", + } + Domain_Type_value = map[string]int32{ + "Plain": 0, + "Regex": 1, + "Domain": 2, + "Full": 3, + } +) + +func (x Domain_Type) Enum() *Domain_Type { + p := new(Domain_Type) + *p = x + return p +} + +func (x Domain_Type) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Domain_Type) Descriptor() protoreflect.EnumDescriptor { + return file_app_router_config_proto_enumTypes[0].Descriptor() +} + +func (Domain_Type) Type() protoreflect.EnumType { + return &file_app_router_config_proto_enumTypes[0] +} + +func (x Domain_Type) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Domain_Type.Descriptor instead. +func (Domain_Type) EnumDescriptor() ([]byte, []int) { + return file_app_router_config_proto_rawDescGZIP(), []int{0, 0} +} + +type Config_DomainStrategy int32 + +const ( + // Use domain as is. + Config_AsIs Config_DomainStrategy = 0 + // Resolve to IP if the domain doesn't match any rules. + Config_IpIfNonMatch Config_DomainStrategy = 2 + // Resolve to IP if any rule requires IP matching. + Config_IpOnDemand Config_DomainStrategy = 3 +) + +// Enum value maps for Config_DomainStrategy. +var ( + Config_DomainStrategy_name = map[int32]string{ + 0: "AsIs", + 2: "IpIfNonMatch", + 3: "IpOnDemand", + } + Config_DomainStrategy_value = map[string]int32{ + "AsIs": 0, + "IpIfNonMatch": 2, + "IpOnDemand": 3, + } +) + +func (x Config_DomainStrategy) Enum() *Config_DomainStrategy { + p := new(Config_DomainStrategy) + *p = x + return p +} + +func (x Config_DomainStrategy) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Config_DomainStrategy) Descriptor() protoreflect.EnumDescriptor { + return file_app_router_config_proto_enumTypes[1].Descriptor() +} + +func (Config_DomainStrategy) Type() protoreflect.EnumType { + return &file_app_router_config_proto_enumTypes[1] +} + +func (x Config_DomainStrategy) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Config_DomainStrategy.Descriptor instead. +func (Config_DomainStrategy) EnumDescriptor() ([]byte, []int) { + return file_app_router_config_proto_rawDescGZIP(), []int{11, 0} +} + +// Domain for routing decision. +type Domain struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Domain matching type. + Type Domain_Type `protobuf:"varint,1,opt,name=type,proto3,enum=xray.app.router.Domain_Type" json:"type,omitempty"` + // Domain value. + Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` + // Attributes of this domain. May be used for filtering. + Attribute []*Domain_Attribute `protobuf:"bytes,3,rep,name=attribute,proto3" json:"attribute,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Domain) Reset() { + *x = Domain{} + mi := &file_app_router_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Domain) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Domain) ProtoMessage() {} + +func (x *Domain) ProtoReflect() protoreflect.Message { + mi := &file_app_router_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Domain.ProtoReflect.Descriptor instead. +func (*Domain) Descriptor() ([]byte, []int) { + return file_app_router_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Domain) GetType() Domain_Type { + if x != nil { + return x.Type + } + return Domain_Plain +} + +func (x *Domain) GetValue() string { + if x != nil { + return x.Value + } + return "" +} + +func (x *Domain) GetAttribute() []*Domain_Attribute { + if x != nil { + return x.Attribute + } + return nil +} + +// IP for routing decision, in CIDR form. +type CIDR struct { + state protoimpl.MessageState `protogen:"open.v1"` + // IP address, should be either 4 or 16 bytes. + Ip []byte `protobuf:"bytes,1,opt,name=ip,proto3" json:"ip,omitempty"` + // Number of leading ones in the network mask. + Prefix uint32 `protobuf:"varint,2,opt,name=prefix,proto3" json:"prefix,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CIDR) Reset() { + *x = CIDR{} + mi := &file_app_router_config_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CIDR) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CIDR) ProtoMessage() {} + +func (x *CIDR) ProtoReflect() protoreflect.Message { + mi := &file_app_router_config_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CIDR.ProtoReflect.Descriptor instead. +func (*CIDR) Descriptor() ([]byte, []int) { + return file_app_router_config_proto_rawDescGZIP(), []int{1} +} + +func (x *CIDR) GetIp() []byte { + if x != nil { + return x.Ip + } + return nil +} + +func (x *CIDR) GetPrefix() uint32 { + if x != nil { + return x.Prefix + } + return 0 +} + +type GeoIP struct { + state protoimpl.MessageState `protogen:"open.v1"` + CountryCode string `protobuf:"bytes,1,opt,name=country_code,json=countryCode,proto3" json:"country_code,omitempty"` + Cidr []*CIDR `protobuf:"bytes,2,rep,name=cidr,proto3" json:"cidr,omitempty"` + ReverseMatch bool `protobuf:"varint,3,opt,name=reverse_match,json=reverseMatch,proto3" json:"reverse_match,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GeoIP) Reset() { + *x = GeoIP{} + mi := &file_app_router_config_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GeoIP) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GeoIP) ProtoMessage() {} + +func (x *GeoIP) ProtoReflect() protoreflect.Message { + mi := &file_app_router_config_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GeoIP.ProtoReflect.Descriptor instead. +func (*GeoIP) Descriptor() ([]byte, []int) { + return file_app_router_config_proto_rawDescGZIP(), []int{2} +} + +func (x *GeoIP) GetCountryCode() string { + if x != nil { + return x.CountryCode + } + return "" +} + +func (x *GeoIP) GetCidr() []*CIDR { + if x != nil { + return x.Cidr + } + return nil +} + +func (x *GeoIP) GetReverseMatch() bool { + if x != nil { + return x.ReverseMatch + } + return false +} + +type GeoIPList struct { + state protoimpl.MessageState `protogen:"open.v1"` + Entry []*GeoIP `protobuf:"bytes,1,rep,name=entry,proto3" json:"entry,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GeoIPList) Reset() { + *x = GeoIPList{} + mi := &file_app_router_config_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GeoIPList) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GeoIPList) ProtoMessage() {} + +func (x *GeoIPList) ProtoReflect() protoreflect.Message { + mi := &file_app_router_config_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GeoIPList.ProtoReflect.Descriptor instead. +func (*GeoIPList) Descriptor() ([]byte, []int) { + return file_app_router_config_proto_rawDescGZIP(), []int{3} +} + +func (x *GeoIPList) GetEntry() []*GeoIP { + if x != nil { + return x.Entry + } + return nil +} + +type GeoSite struct { + state protoimpl.MessageState `protogen:"open.v1"` + CountryCode string `protobuf:"bytes,1,opt,name=country_code,json=countryCode,proto3" json:"country_code,omitempty"` + Domain []*Domain `protobuf:"bytes,2,rep,name=domain,proto3" json:"domain,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GeoSite) Reset() { + *x = GeoSite{} + mi := &file_app_router_config_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GeoSite) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GeoSite) ProtoMessage() {} + +func (x *GeoSite) ProtoReflect() protoreflect.Message { + mi := &file_app_router_config_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GeoSite.ProtoReflect.Descriptor instead. +func (*GeoSite) Descriptor() ([]byte, []int) { + return file_app_router_config_proto_rawDescGZIP(), []int{4} +} + +func (x *GeoSite) GetCountryCode() string { + if x != nil { + return x.CountryCode + } + return "" +} + +func (x *GeoSite) GetDomain() []*Domain { + if x != nil { + return x.Domain + } + return nil +} + +type GeoSiteList struct { + state protoimpl.MessageState `protogen:"open.v1"` + Entry []*GeoSite `protobuf:"bytes,1,rep,name=entry,proto3" json:"entry,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GeoSiteList) Reset() { + *x = GeoSiteList{} + mi := &file_app_router_config_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GeoSiteList) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GeoSiteList) ProtoMessage() {} + +func (x *GeoSiteList) ProtoReflect() protoreflect.Message { + mi := &file_app_router_config_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GeoSiteList.ProtoReflect.Descriptor instead. +func (*GeoSiteList) Descriptor() ([]byte, []int) { + return file_app_router_config_proto_rawDescGZIP(), []int{5} +} + +func (x *GeoSiteList) GetEntry() []*GeoSite { + if x != nil { + return x.Entry + } + return nil +} + +type RoutingRule struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to TargetTag: + // + // *RoutingRule_Tag + // *RoutingRule_BalancingTag + TargetTag isRoutingRule_TargetTag `protobuf_oneof:"target_tag"` + RuleTag string `protobuf:"bytes,19,opt,name=rule_tag,json=ruleTag,proto3" json:"rule_tag,omitempty"` + // List of domains for target domain matching. + Domain []*Domain `protobuf:"bytes,2,rep,name=domain,proto3" json:"domain,omitempty"` + // List of GeoIPs for target IP address matching. If this entry exists, the + // cidr above will have no effect. GeoIP fields with the same country code are + // supposed to contain exactly same content. They will be merged during + // runtime. For customized GeoIPs, please leave country code empty. + Geoip []*GeoIP `protobuf:"bytes,10,rep,name=geoip,proto3" json:"geoip,omitempty"` + // List of ports. + PortList *net.PortList `protobuf:"bytes,14,opt,name=port_list,json=portList,proto3" json:"port_list,omitempty"` + // List of networks for matching. + Networks []net.Network `protobuf:"varint,13,rep,packed,name=networks,proto3,enum=xray.common.net.Network" json:"networks,omitempty"` + // List of GeoIPs for source IP address matching. If this entry exists, the + // source_cidr above will have no effect. + SourceGeoip []*GeoIP `protobuf:"bytes,11,rep,name=source_geoip,json=sourceGeoip,proto3" json:"source_geoip,omitempty"` + // List of ports for source port matching. + SourcePortList *net.PortList `protobuf:"bytes,16,opt,name=source_port_list,json=sourcePortList,proto3" json:"source_port_list,omitempty"` + UserEmail []string `protobuf:"bytes,7,rep,name=user_email,json=userEmail,proto3" json:"user_email,omitempty"` + InboundTag []string `protobuf:"bytes,8,rep,name=inbound_tag,json=inboundTag,proto3" json:"inbound_tag,omitempty"` + Protocol []string `protobuf:"bytes,9,rep,name=protocol,proto3" json:"protocol,omitempty"` + Attributes map[string]string `protobuf:"bytes,15,rep,name=attributes,proto3" json:"attributes,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + LocalGeoip []*GeoIP `protobuf:"bytes,17,rep,name=local_geoip,json=localGeoip,proto3" json:"local_geoip,omitempty"` + LocalPortList *net.PortList `protobuf:"bytes,18,opt,name=local_port_list,json=localPortList,proto3" json:"local_port_list,omitempty"` + VlessRouteList *net.PortList `protobuf:"bytes,20,opt,name=vless_route_list,json=vlessRouteList,proto3" json:"vless_route_list,omitempty"` + Process []string `protobuf:"bytes,21,rep,name=process,proto3" json:"process,omitempty"` + Webhook *WebhookConfig `protobuf:"bytes,22,opt,name=webhook,proto3" json:"webhook,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RoutingRule) Reset() { + *x = RoutingRule{} + mi := &file_app_router_config_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RoutingRule) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RoutingRule) ProtoMessage() {} + +func (x *RoutingRule) ProtoReflect() protoreflect.Message { + mi := &file_app_router_config_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RoutingRule.ProtoReflect.Descriptor instead. +func (*RoutingRule) Descriptor() ([]byte, []int) { + return file_app_router_config_proto_rawDescGZIP(), []int{6} +} + +func (x *RoutingRule) GetTargetTag() isRoutingRule_TargetTag { + if x != nil { + return x.TargetTag + } + return nil +} + +func (x *RoutingRule) GetTag() string { + if x != nil { + if x, ok := x.TargetTag.(*RoutingRule_Tag); ok { + return x.Tag + } + } + return "" +} + +func (x *RoutingRule) GetBalancingTag() string { + if x != nil { + if x, ok := x.TargetTag.(*RoutingRule_BalancingTag); ok { + return x.BalancingTag + } + } + return "" +} + +func (x *RoutingRule) GetRuleTag() string { + if x != nil { + return x.RuleTag + } + return "" +} + +func (x *RoutingRule) GetDomain() []*Domain { + if x != nil { + return x.Domain + } + return nil +} + +func (x *RoutingRule) GetGeoip() []*GeoIP { + if x != nil { + return x.Geoip + } + return nil +} + +func (x *RoutingRule) GetPortList() *net.PortList { + if x != nil { + return x.PortList + } + return nil +} + +func (x *RoutingRule) GetNetworks() []net.Network { + if x != nil { + return x.Networks + } + return nil +} + +func (x *RoutingRule) GetSourceGeoip() []*GeoIP { + if x != nil { + return x.SourceGeoip + } + return nil +} + +func (x *RoutingRule) GetSourcePortList() *net.PortList { + if x != nil { + return x.SourcePortList + } + return nil +} + +func (x *RoutingRule) GetUserEmail() []string { + if x != nil { + return x.UserEmail + } + return nil +} + +func (x *RoutingRule) GetInboundTag() []string { + if x != nil { + return x.InboundTag + } + return nil +} + +func (x *RoutingRule) GetProtocol() []string { + if x != nil { + return x.Protocol + } + return nil +} + +func (x *RoutingRule) GetAttributes() map[string]string { + if x != nil { + return x.Attributes + } + return nil +} + +func (x *RoutingRule) GetLocalGeoip() []*GeoIP { + if x != nil { + return x.LocalGeoip + } + return nil +} + +func (x *RoutingRule) GetLocalPortList() *net.PortList { + if x != nil { + return x.LocalPortList + } + return nil +} + +func (x *RoutingRule) GetVlessRouteList() *net.PortList { + if x != nil { + return x.VlessRouteList + } + return nil +} + +func (x *RoutingRule) GetProcess() []string { + if x != nil { + return x.Process + } + return nil +} + +func (x *RoutingRule) GetWebhook() *WebhookConfig { + if x != nil { + return x.Webhook + } + return nil +} + +type isRoutingRule_TargetTag interface { + isRoutingRule_TargetTag() +} + +type RoutingRule_Tag struct { + // Tag of outbound that this rule is pointing to. + Tag string `protobuf:"bytes,1,opt,name=tag,proto3,oneof"` +} + +type RoutingRule_BalancingTag struct { + // Tag of routing balancer. + BalancingTag string `protobuf:"bytes,12,opt,name=balancing_tag,json=balancingTag,proto3,oneof"` +} + +func (*RoutingRule_Tag) isRoutingRule_TargetTag() {} + +func (*RoutingRule_BalancingTag) isRoutingRule_TargetTag() {} + +type WebhookConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"` + Deduplication uint32 `protobuf:"varint,2,opt,name=deduplication,proto3" json:"deduplication,omitempty"` + Headers map[string]string `protobuf:"bytes,3,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WebhookConfig) Reset() { + *x = WebhookConfig{} + mi := &file_app_router_config_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WebhookConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WebhookConfig) ProtoMessage() {} + +func (x *WebhookConfig) ProtoReflect() protoreflect.Message { + mi := &file_app_router_config_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 WebhookConfig.ProtoReflect.Descriptor instead. +func (*WebhookConfig) Descriptor() ([]byte, []int) { + return file_app_router_config_proto_rawDescGZIP(), []int{7} +} + +func (x *WebhookConfig) GetUrl() string { + if x != nil { + return x.Url + } + return "" +} + +func (x *WebhookConfig) GetDeduplication() uint32 { + if x != nil { + return x.Deduplication + } + return 0 +} + +func (x *WebhookConfig) GetHeaders() map[string]string { + if x != nil { + return x.Headers + } + return nil +} + +type BalancingRule struct { + state protoimpl.MessageState `protogen:"open.v1"` + Tag string `protobuf:"bytes,1,opt,name=tag,proto3" json:"tag,omitempty"` + OutboundSelector []string `protobuf:"bytes,2,rep,name=outbound_selector,json=outboundSelector,proto3" json:"outbound_selector,omitempty"` + Strategy string `protobuf:"bytes,3,opt,name=strategy,proto3" json:"strategy,omitempty"` + StrategySettings *serial.TypedMessage `protobuf:"bytes,4,opt,name=strategy_settings,json=strategySettings,proto3" json:"strategy_settings,omitempty"` + FallbackTag string `protobuf:"bytes,5,opt,name=fallback_tag,json=fallbackTag,proto3" json:"fallback_tag,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BalancingRule) Reset() { + *x = BalancingRule{} + mi := &file_app_router_config_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BalancingRule) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BalancingRule) ProtoMessage() {} + +func (x *BalancingRule) ProtoReflect() protoreflect.Message { + mi := &file_app_router_config_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BalancingRule.ProtoReflect.Descriptor instead. +func (*BalancingRule) Descriptor() ([]byte, []int) { + return file_app_router_config_proto_rawDescGZIP(), []int{8} +} + +func (x *BalancingRule) GetTag() string { + if x != nil { + return x.Tag + } + return "" +} + +func (x *BalancingRule) GetOutboundSelector() []string { + if x != nil { + return x.OutboundSelector + } + return nil +} + +func (x *BalancingRule) GetStrategy() string { + if x != nil { + return x.Strategy + } + return "" +} + +func (x *BalancingRule) GetStrategySettings() *serial.TypedMessage { + if x != nil { + return x.StrategySettings + } + return nil +} + +func (x *BalancingRule) GetFallbackTag() string { + if x != nil { + return x.FallbackTag + } + return "" +} + +type StrategyWeight struct { + state protoimpl.MessageState `protogen:"open.v1"` + Regexp bool `protobuf:"varint,1,opt,name=regexp,proto3" json:"regexp,omitempty"` + Match string `protobuf:"bytes,2,opt,name=match,proto3" json:"match,omitempty"` + Value float32 `protobuf:"fixed32,3,opt,name=value,proto3" json:"value,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StrategyWeight) Reset() { + *x = StrategyWeight{} + mi := &file_app_router_config_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StrategyWeight) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StrategyWeight) ProtoMessage() {} + +func (x *StrategyWeight) ProtoReflect() protoreflect.Message { + mi := &file_app_router_config_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StrategyWeight.ProtoReflect.Descriptor instead. +func (*StrategyWeight) Descriptor() ([]byte, []int) { + return file_app_router_config_proto_rawDescGZIP(), []int{9} +} + +func (x *StrategyWeight) GetRegexp() bool { + if x != nil { + return x.Regexp + } + return false +} + +func (x *StrategyWeight) GetMatch() string { + if x != nil { + return x.Match + } + return "" +} + +func (x *StrategyWeight) GetValue() float32 { + if x != nil { + return x.Value + } + return 0 +} + +type StrategyLeastLoadConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + // weight settings + Costs []*StrategyWeight `protobuf:"bytes,2,rep,name=costs,proto3" json:"costs,omitempty"` + // RTT baselines for selecting, int64 values of time.Duration + Baselines []int64 `protobuf:"varint,3,rep,packed,name=baselines,proto3" json:"baselines,omitempty"` + // expected nodes count to select + Expected int32 `protobuf:"varint,4,opt,name=expected,proto3" json:"expected,omitempty"` + // max acceptable rtt, filter away high delay nodes. default 0 + MaxRTT int64 `protobuf:"varint,5,opt,name=maxRTT,proto3" json:"maxRTT,omitempty"` + // acceptable failure rate + Tolerance float32 `protobuf:"fixed32,6,opt,name=tolerance,proto3" json:"tolerance,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StrategyLeastLoadConfig) Reset() { + *x = StrategyLeastLoadConfig{} + mi := &file_app_router_config_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StrategyLeastLoadConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StrategyLeastLoadConfig) ProtoMessage() {} + +func (x *StrategyLeastLoadConfig) ProtoReflect() protoreflect.Message { + mi := &file_app_router_config_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StrategyLeastLoadConfig.ProtoReflect.Descriptor instead. +func (*StrategyLeastLoadConfig) Descriptor() ([]byte, []int) { + return file_app_router_config_proto_rawDescGZIP(), []int{10} +} + +func (x *StrategyLeastLoadConfig) GetCosts() []*StrategyWeight { + if x != nil { + return x.Costs + } + return nil +} + +func (x *StrategyLeastLoadConfig) GetBaselines() []int64 { + if x != nil { + return x.Baselines + } + return nil +} + +func (x *StrategyLeastLoadConfig) GetExpected() int32 { + if x != nil { + return x.Expected + } + return 0 +} + +func (x *StrategyLeastLoadConfig) GetMaxRTT() int64 { + if x != nil { + return x.MaxRTT + } + return 0 +} + +func (x *StrategyLeastLoadConfig) GetTolerance() float32 { + if x != nil { + return x.Tolerance + } + return 0 +} + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + DomainStrategy Config_DomainStrategy `protobuf:"varint,1,opt,name=domain_strategy,json=domainStrategy,proto3,enum=xray.app.router.Config_DomainStrategy" json:"domain_strategy,omitempty"` + Rule []*RoutingRule `protobuf:"bytes,2,rep,name=rule,proto3" json:"rule,omitempty"` + BalancingRule []*BalancingRule `protobuf:"bytes,3,rep,name=balancing_rule,json=balancingRule,proto3" json:"balancing_rule,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_app_router_config_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_app_router_config_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_app_router_config_proto_rawDescGZIP(), []int{11} +} + +func (x *Config) GetDomainStrategy() Config_DomainStrategy { + if x != nil { + return x.DomainStrategy + } + return Config_AsIs +} + +func (x *Config) GetRule() []*RoutingRule { + if x != nil { + return x.Rule + } + return nil +} + +func (x *Config) GetBalancingRule() []*BalancingRule { + if x != nil { + return x.BalancingRule + } + return nil +} + +type Domain_Attribute struct { + state protoimpl.MessageState `protogen:"open.v1"` + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + // Types that are valid to be assigned to TypedValue: + // + // *Domain_Attribute_BoolValue + // *Domain_Attribute_IntValue + TypedValue isDomain_Attribute_TypedValue `protobuf_oneof:"typed_value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Domain_Attribute) Reset() { + *x = Domain_Attribute{} + mi := &file_app_router_config_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Domain_Attribute) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Domain_Attribute) ProtoMessage() {} + +func (x *Domain_Attribute) ProtoReflect() protoreflect.Message { + mi := &file_app_router_config_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Domain_Attribute.ProtoReflect.Descriptor instead. +func (*Domain_Attribute) Descriptor() ([]byte, []int) { + return file_app_router_config_proto_rawDescGZIP(), []int{0, 0} +} + +func (x *Domain_Attribute) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *Domain_Attribute) GetTypedValue() isDomain_Attribute_TypedValue { + if x != nil { + return x.TypedValue + } + return nil +} + +func (x *Domain_Attribute) GetBoolValue() bool { + if x != nil { + if x, ok := x.TypedValue.(*Domain_Attribute_BoolValue); ok { + return x.BoolValue + } + } + return false +} + +func (x *Domain_Attribute) GetIntValue() int64 { + if x != nil { + if x, ok := x.TypedValue.(*Domain_Attribute_IntValue); ok { + return x.IntValue + } + } + return 0 +} + +type isDomain_Attribute_TypedValue interface { + isDomain_Attribute_TypedValue() +} + +type Domain_Attribute_BoolValue struct { + BoolValue bool `protobuf:"varint,2,opt,name=bool_value,json=boolValue,proto3,oneof"` +} + +type Domain_Attribute_IntValue struct { + IntValue int64 `protobuf:"varint,3,opt,name=int_value,json=intValue,proto3,oneof"` +} + +func (*Domain_Attribute_BoolValue) isDomain_Attribute_TypedValue() {} + +func (*Domain_Attribute_IntValue) isDomain_Attribute_TypedValue() {} + +var File_app_router_config_proto protoreflect.FileDescriptor + +const file_app_router_config_proto_rawDesc = "" + + "\n" + + "\x17app/router/config.proto\x12\x0fxray.app.router\x1a!common/serial/typed_message.proto\x1a\x15common/net/port.proto\x1a\x18common/net/network.proto\"\xb3\x02\n" + + "\x06Domain\x120\n" + + "\x04type\x18\x01 \x01(\x0e2\x1c.xray.app.router.Domain.TypeR\x04type\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value\x12?\n" + + "\tattribute\x18\x03 \x03(\v2!.xray.app.router.Domain.AttributeR\tattribute\x1al\n" + + "\tAttribute\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x1f\n" + + "\n" + + "bool_value\x18\x02 \x01(\bH\x00R\tboolValue\x12\x1d\n" + + "\tint_value\x18\x03 \x01(\x03H\x00R\bintValueB\r\n" + + "\vtyped_value\"2\n" + + "\x04Type\x12\t\n" + + "\x05Plain\x10\x00\x12\t\n" + + "\x05Regex\x10\x01\x12\n" + + "\n" + + "\x06Domain\x10\x02\x12\b\n" + + "\x04Full\x10\x03\".\n" + + "\x04CIDR\x12\x0e\n" + + "\x02ip\x18\x01 \x01(\fR\x02ip\x12\x16\n" + + "\x06prefix\x18\x02 \x01(\rR\x06prefix\"z\n" + + "\x05GeoIP\x12!\n" + + "\fcountry_code\x18\x01 \x01(\tR\vcountryCode\x12)\n" + + "\x04cidr\x18\x02 \x03(\v2\x15.xray.app.router.CIDRR\x04cidr\x12#\n" + + "\rreverse_match\x18\x03 \x01(\bR\freverseMatch\"9\n" + + "\tGeoIPList\x12,\n" + + "\x05entry\x18\x01 \x03(\v2\x16.xray.app.router.GeoIPR\x05entry\"]\n" + + "\aGeoSite\x12!\n" + + "\fcountry_code\x18\x01 \x01(\tR\vcountryCode\x12/\n" + + "\x06domain\x18\x02 \x03(\v2\x17.xray.app.router.DomainR\x06domain\"=\n" + + "\vGeoSiteList\x12.\n" + + "\x05entry\x18\x01 \x03(\v2\x18.xray.app.router.GeoSiteR\x05entry\"\xbc\a\n" + + "\vRoutingRule\x12\x12\n" + + "\x03tag\x18\x01 \x01(\tH\x00R\x03tag\x12%\n" + + "\rbalancing_tag\x18\f \x01(\tH\x00R\fbalancingTag\x12\x19\n" + + "\brule_tag\x18\x13 \x01(\tR\aruleTag\x12/\n" + + "\x06domain\x18\x02 \x03(\v2\x17.xray.app.router.DomainR\x06domain\x12,\n" + + "\x05geoip\x18\n" + + " \x03(\v2\x16.xray.app.router.GeoIPR\x05geoip\x126\n" + + "\tport_list\x18\x0e \x01(\v2\x19.xray.common.net.PortListR\bportList\x124\n" + + "\bnetworks\x18\r \x03(\x0e2\x18.xray.common.net.NetworkR\bnetworks\x129\n" + + "\fsource_geoip\x18\v \x03(\v2\x16.xray.app.router.GeoIPR\vsourceGeoip\x12C\n" + + "\x10source_port_list\x18\x10 \x01(\v2\x19.xray.common.net.PortListR\x0esourcePortList\x12\x1d\n" + + "\n" + + "user_email\x18\a \x03(\tR\tuserEmail\x12\x1f\n" + + "\vinbound_tag\x18\b \x03(\tR\n" + + "inboundTag\x12\x1a\n" + + "\bprotocol\x18\t \x03(\tR\bprotocol\x12L\n" + + "\n" + + "attributes\x18\x0f \x03(\v2,.xray.app.router.RoutingRule.AttributesEntryR\n" + + "attributes\x127\n" + + "\vlocal_geoip\x18\x11 \x03(\v2\x16.xray.app.router.GeoIPR\n" + + "localGeoip\x12A\n" + + "\x0flocal_port_list\x18\x12 \x01(\v2\x19.xray.common.net.PortListR\rlocalPortList\x12C\n" + + "\x10vless_route_list\x18\x14 \x01(\v2\x19.xray.common.net.PortListR\x0evlessRouteList\x12\x18\n" + + "\aprocess\x18\x15 \x03(\tR\aprocess\x128\n" + + "\awebhook\x18\x16 \x01(\v2\x1e.xray.app.router.WebhookConfigR\awebhook\x1a=\n" + + "\x0fAttributesEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\f\n" + + "\n" + + "target_tag\"\xca\x01\n" + + "\rWebhookConfig\x12\x10\n" + + "\x03url\x18\x01 \x01(\tR\x03url\x12$\n" + + "\rdeduplication\x18\x02 \x01(\rR\rdeduplication\x12E\n" + + "\aheaders\x18\x03 \x03(\v2+.xray.app.router.WebhookConfig.HeadersEntryR\aheaders\x1a:\n" + + "\fHeadersEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xdc\x01\n" + + "\rBalancingRule\x12\x10\n" + + "\x03tag\x18\x01 \x01(\tR\x03tag\x12+\n" + + "\x11outbound_selector\x18\x02 \x03(\tR\x10outboundSelector\x12\x1a\n" + + "\bstrategy\x18\x03 \x01(\tR\bstrategy\x12M\n" + + "\x11strategy_settings\x18\x04 \x01(\v2 .xray.common.serial.TypedMessageR\x10strategySettings\x12!\n" + + "\ffallback_tag\x18\x05 \x01(\tR\vfallbackTag\"T\n" + + "\x0eStrategyWeight\x12\x16\n" + + "\x06regexp\x18\x01 \x01(\bR\x06regexp\x12\x14\n" + + "\x05match\x18\x02 \x01(\tR\x05match\x12\x14\n" + + "\x05value\x18\x03 \x01(\x02R\x05value\"\xc0\x01\n" + + "\x17StrategyLeastLoadConfig\x125\n" + + "\x05costs\x18\x02 \x03(\v2\x1f.xray.app.router.StrategyWeightR\x05costs\x12\x1c\n" + + "\tbaselines\x18\x03 \x03(\x03R\tbaselines\x12\x1a\n" + + "\bexpected\x18\x04 \x01(\x05R\bexpected\x12\x16\n" + + "\x06maxRTT\x18\x05 \x01(\x03R\x06maxRTT\x12\x1c\n" + + "\ttolerance\x18\x06 \x01(\x02R\ttolerance\"\x90\x02\n" + + "\x06Config\x12O\n" + + "\x0fdomain_strategy\x18\x01 \x01(\x0e2&.xray.app.router.Config.DomainStrategyR\x0edomainStrategy\x120\n" + + "\x04rule\x18\x02 \x03(\v2\x1c.xray.app.router.RoutingRuleR\x04rule\x12E\n" + + "\x0ebalancing_rule\x18\x03 \x03(\v2\x1e.xray.app.router.BalancingRuleR\rbalancingRule\"<\n" + + "\x0eDomainStrategy\x12\b\n" + + "\x04AsIs\x10\x00\x12\x10\n" + + "\fIpIfNonMatch\x10\x02\x12\x0e\n" + + "\n" + + "IpOnDemand\x10\x03BO\n" + + "\x13com.xray.app.routerP\x01Z$github.com/xtls/xray-core/app/router\xaa\x02\x0fXray.App.Routerb\x06proto3" + +var ( + file_app_router_config_proto_rawDescOnce sync.Once + file_app_router_config_proto_rawDescData []byte +) + +func file_app_router_config_proto_rawDescGZIP() []byte { + file_app_router_config_proto_rawDescOnce.Do(func() { + file_app_router_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_app_router_config_proto_rawDesc), len(file_app_router_config_proto_rawDesc))) + }) + return file_app_router_config_proto_rawDescData +} + +var file_app_router_config_proto_enumTypes = make([]protoimpl.EnumInfo, 2) +var file_app_router_config_proto_msgTypes = make([]protoimpl.MessageInfo, 15) +var file_app_router_config_proto_goTypes = []any{ + (Domain_Type)(0), // 0: xray.app.router.Domain.Type + (Config_DomainStrategy)(0), // 1: xray.app.router.Config.DomainStrategy + (*Domain)(nil), // 2: xray.app.router.Domain + (*CIDR)(nil), // 3: xray.app.router.CIDR + (*GeoIP)(nil), // 4: xray.app.router.GeoIP + (*GeoIPList)(nil), // 5: xray.app.router.GeoIPList + (*GeoSite)(nil), // 6: xray.app.router.GeoSite + (*GeoSiteList)(nil), // 7: xray.app.router.GeoSiteList + (*RoutingRule)(nil), // 8: xray.app.router.RoutingRule + (*WebhookConfig)(nil), // 9: xray.app.router.WebhookConfig + (*BalancingRule)(nil), // 10: xray.app.router.BalancingRule + (*StrategyWeight)(nil), // 11: xray.app.router.StrategyWeight + (*StrategyLeastLoadConfig)(nil), // 12: xray.app.router.StrategyLeastLoadConfig + (*Config)(nil), // 13: xray.app.router.Config + (*Domain_Attribute)(nil), // 14: xray.app.router.Domain.Attribute + nil, // 15: xray.app.router.RoutingRule.AttributesEntry + nil, // 16: xray.app.router.WebhookConfig.HeadersEntry + (*net.PortList)(nil), // 17: xray.common.net.PortList + (net.Network)(0), // 18: xray.common.net.Network + (*serial.TypedMessage)(nil), // 19: xray.common.serial.TypedMessage +} +var file_app_router_config_proto_depIdxs = []int32{ + 0, // 0: xray.app.router.Domain.type:type_name -> xray.app.router.Domain.Type + 14, // 1: xray.app.router.Domain.attribute:type_name -> xray.app.router.Domain.Attribute + 3, // 2: xray.app.router.GeoIP.cidr:type_name -> xray.app.router.CIDR + 4, // 3: xray.app.router.GeoIPList.entry:type_name -> xray.app.router.GeoIP + 2, // 4: xray.app.router.GeoSite.domain:type_name -> xray.app.router.Domain + 6, // 5: xray.app.router.GeoSiteList.entry:type_name -> xray.app.router.GeoSite + 2, // 6: xray.app.router.RoutingRule.domain:type_name -> xray.app.router.Domain + 4, // 7: xray.app.router.RoutingRule.geoip:type_name -> xray.app.router.GeoIP + 17, // 8: xray.app.router.RoutingRule.port_list:type_name -> xray.common.net.PortList + 18, // 9: xray.app.router.RoutingRule.networks:type_name -> xray.common.net.Network + 4, // 10: xray.app.router.RoutingRule.source_geoip:type_name -> xray.app.router.GeoIP + 17, // 11: xray.app.router.RoutingRule.source_port_list:type_name -> xray.common.net.PortList + 15, // 12: xray.app.router.RoutingRule.attributes:type_name -> xray.app.router.RoutingRule.AttributesEntry + 4, // 13: xray.app.router.RoutingRule.local_geoip:type_name -> xray.app.router.GeoIP + 17, // 14: xray.app.router.RoutingRule.local_port_list:type_name -> xray.common.net.PortList + 17, // 15: xray.app.router.RoutingRule.vless_route_list:type_name -> xray.common.net.PortList + 9, // 16: xray.app.router.RoutingRule.webhook:type_name -> xray.app.router.WebhookConfig + 16, // 17: xray.app.router.WebhookConfig.headers:type_name -> xray.app.router.WebhookConfig.HeadersEntry + 19, // 18: xray.app.router.BalancingRule.strategy_settings:type_name -> xray.common.serial.TypedMessage + 11, // 19: xray.app.router.StrategyLeastLoadConfig.costs:type_name -> xray.app.router.StrategyWeight + 1, // 20: xray.app.router.Config.domain_strategy:type_name -> xray.app.router.Config.DomainStrategy + 8, // 21: xray.app.router.Config.rule:type_name -> xray.app.router.RoutingRule + 10, // 22: xray.app.router.Config.balancing_rule:type_name -> xray.app.router.BalancingRule + 23, // [23:23] is the sub-list for method output_type + 23, // [23:23] is the sub-list for method input_type + 23, // [23:23] is the sub-list for extension type_name + 23, // [23:23] is the sub-list for extension extendee + 0, // [0:23] is the sub-list for field type_name +} + +func init() { file_app_router_config_proto_init() } +func file_app_router_config_proto_init() { + if File_app_router_config_proto != nil { + return + } + file_app_router_config_proto_msgTypes[6].OneofWrappers = []any{ + (*RoutingRule_Tag)(nil), + (*RoutingRule_BalancingTag)(nil), + } + file_app_router_config_proto_msgTypes[12].OneofWrappers = []any{ + (*Domain_Attribute_BoolValue)(nil), + (*Domain_Attribute_IntValue)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_app_router_config_proto_rawDesc), len(file_app_router_config_proto_rawDesc)), + NumEnums: 2, + NumMessages: 15, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_app_router_config_proto_goTypes, + DependencyIndexes: file_app_router_config_proto_depIdxs, + EnumInfos: file_app_router_config_proto_enumTypes, + MessageInfos: file_app_router_config_proto_msgTypes, + }.Build() + File_app_router_config_proto = out.File + file_app_router_config_proto_goTypes = nil + file_app_router_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/app/router/config.proto b/subproject/Xray-core-main/app/router/config.proto new file mode 100644 index 00000000..07fe4c51 --- /dev/null +++ b/subproject/Xray-core-main/app/router/config.proto @@ -0,0 +1,170 @@ +syntax = "proto3"; + +package xray.app.router; +option csharp_namespace = "Xray.App.Router"; +option go_package = "github.com/xtls/xray-core/app/router"; +option java_package = "com.xray.app.router"; +option java_multiple_files = true; + +import "common/serial/typed_message.proto"; +import "common/net/port.proto"; +import "common/net/network.proto"; + +// Domain for routing decision. +message Domain { + // Type of domain value. + enum Type { + // The value is used as is. + Plain = 0; + // The value is used as a regular expression. + Regex = 1; + // The value is a root domain. + Domain = 2; + // The value is a domain. + Full = 3; + } + + // Domain matching type. + Type type = 1; + + // Domain value. + string value = 2; + + message Attribute { + string key = 1; + + oneof typed_value { + bool bool_value = 2; + int64 int_value = 3; + } + } + + // Attributes of this domain. May be used for filtering. + repeated Attribute attribute = 3; +} + +// IP for routing decision, in CIDR form. +message CIDR { + // IP address, should be either 4 or 16 bytes. + bytes ip = 1; + + // Number of leading ones in the network mask. + uint32 prefix = 2; +} + +message GeoIP { + string country_code = 1; + repeated CIDR cidr = 2; + bool reverse_match = 3; +} + +message GeoIPList { + repeated GeoIP entry = 1; +} + +message GeoSite { + string country_code = 1; + repeated Domain domain = 2; +} + +message GeoSiteList { + repeated GeoSite entry = 1; +} + +message RoutingRule { + oneof target_tag { + // Tag of outbound that this rule is pointing to. + string tag = 1; + + // Tag of routing balancer. + string balancing_tag = 12; + } + string rule_tag = 19; + + // List of domains for target domain matching. + repeated Domain domain = 2; + + // List of GeoIPs for target IP address matching. If this entry exists, the + // cidr above will have no effect. GeoIP fields with the same country code are + // supposed to contain exactly same content. They will be merged during + // runtime. For customized GeoIPs, please leave country code empty. + repeated GeoIP geoip = 10; + + // List of ports. + xray.common.net.PortList port_list = 14; + + // List of networks for matching. + repeated xray.common.net.Network networks = 13; + + // List of GeoIPs for source IP address matching. If this entry exists, the + // source_cidr above will have no effect. + repeated GeoIP source_geoip = 11; + + // List of ports for source port matching. + xray.common.net.PortList source_port_list = 16; + + repeated string user_email = 7; + repeated string inbound_tag = 8; + repeated string protocol = 9; + + map attributes = 15; + + repeated GeoIP local_geoip = 17; + xray.common.net.PortList local_port_list = 18; + + xray.common.net.PortList vless_route_list = 20; + repeated string process = 21; + WebhookConfig webhook = 22; +} + +message WebhookConfig { + string url = 1; + uint32 deduplication = 2; + map headers = 3; +} + +message BalancingRule { + string tag = 1; + repeated string outbound_selector = 2; + string strategy = 3; + xray.common.serial.TypedMessage strategy_settings = 4; + string fallback_tag = 5; +} + +message StrategyWeight { + bool regexp = 1; + string match = 2; + float value =3; +} + +message StrategyLeastLoadConfig { + // weight settings + repeated StrategyWeight costs = 2; + // RTT baselines for selecting, int64 values of time.Duration + repeated int64 baselines = 3; + // expected nodes count to select + int32 expected = 4; + // max acceptable rtt, filter away high delay nodes. default 0 + int64 maxRTT = 5; + // acceptable failure rate + float tolerance = 6; +} + +message Config { + enum DomainStrategy { + // Use domain as is. + AsIs = 0; + + // [Deprecated] Always resolve IP for domains. + // UseIp = 1; + + // Resolve to IP if the domain doesn't match any rules. + IpIfNonMatch = 2; + + // Resolve to IP if any rule requires IP matching. + IpOnDemand = 3; + } + DomainStrategy domain_strategy = 1; + repeated RoutingRule rule = 2; + repeated BalancingRule balancing_rule = 3; +} diff --git a/subproject/Xray-core-main/app/router/geosite_compact.go b/subproject/Xray-core-main/app/router/geosite_compact.go new file mode 100644 index 00000000..50fee83f --- /dev/null +++ b/subproject/Xray-core-main/app/router/geosite_compact.go @@ -0,0 +1,100 @@ +package router + +import ( + "encoding/gob" + "errors" + "io" + "runtime" + + "github.com/xtls/xray-core/common/strmatcher" +) + +type geoSiteListGob struct { + Sites map[string][]byte + Deps map[string][]string + Hosts map[string][]string +} + +func SerializeGeoSiteList(sites []*GeoSite, deps map[string][]string, hosts map[string][]string, w io.Writer) error { + data := geoSiteListGob{ + Sites: make(map[string][]byte), + Deps: deps, + Hosts: hosts, + } + + for _, site := range sites { + if site == nil { + continue + } + var buf bytesWriter + if err := SerializeDomainMatcher(site.Domain, &buf); err != nil { + return err + } + data.Sites[site.CountryCode] = buf.Bytes() + } + + return gob.NewEncoder(w).Encode(data) +} + +type bytesWriter struct { + data []byte +} + +func (w *bytesWriter) Write(p []byte) (n int, err error) { + w.data = append(w.data, p...) + return len(p), nil +} + +func (w *bytesWriter) Bytes() []byte { + return w.data +} + +func LoadGeoSiteMatcher(r io.Reader, countryCode string) (strmatcher.IndexMatcher, error) { + var data geoSiteListGob + if err := gob.NewDecoder(r).Decode(&data); err != nil { + return nil, err + } + + return loadWithDeps(&data, countryCode, make(map[string]bool)) +} + +func loadWithDeps(data *geoSiteListGob, code string, visited map[string]bool) (strmatcher.IndexMatcher, error) { + if visited[code] { + return nil, errors.New("cyclic dependency") + } + visited[code] = true + + var matchers []strmatcher.IndexMatcher + + if siteData, ok := data.Sites[code]; ok { + m, err := NewDomainMatcherFromBuffer(siteData) + if err == nil { + matchers = append(matchers, m) + } + } + + if deps, ok := data.Deps[code]; ok { + for _, dep := range deps { + m, err := loadWithDeps(data, dep, visited) + if err == nil { + matchers = append(matchers, m) + } + } + } + + if len(matchers) == 0 { + return nil, errors.New("matcher not found for: " + code) + } + if len(matchers) == 1 { + return matchers[0], nil + } + runtime.GC() + return &strmatcher.IndexMatcherGroup{Matchers: matchers}, nil +} +func LoadGeoSiteHosts(r io.Reader) (map[string][]string, error) { + var data geoSiteListGob + if err := gob.NewDecoder(r).Decode(&data); err != nil { + return nil, err + } + return data.Hosts, nil +} diff --git a/subproject/Xray-core-main/app/router/router.go b/subproject/Xray-core-main/app/router/router.go new file mode 100644 index 00000000..df770a3a --- /dev/null +++ b/subproject/Xray-core-main/app/router/router.go @@ -0,0 +1,330 @@ +package router + +import ( + "context" + "sync" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/serial" + "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/features/dns" + "github.com/xtls/xray-core/features/outbound" + "github.com/xtls/xray-core/features/routing" + routing_dns "github.com/xtls/xray-core/features/routing/dns" +) + +// Router is an implementation of routing.Router. +type Router struct { + domainStrategy Config_DomainStrategy + rules []*Rule + balancers map[string]*Balancer + dns dns.Client + + ctx context.Context + ohm outbound.Manager + dispatcher routing.Dispatcher + mu sync.Mutex +} + +// Route is an implementation of routing.Route. +type Route struct { + routing.Context + outboundGroupTags []string + outboundTag string + ruleTag string +} + +// Init initializes the Router. +func (r *Router) Init(ctx context.Context, config *Config, d dns.Client, ohm outbound.Manager, dispatcher routing.Dispatcher) error { + r.domainStrategy = config.DomainStrategy + r.dns = d + r.ctx = ctx + r.ohm = ohm + r.dispatcher = dispatcher + + r.balancers = make(map[string]*Balancer, len(config.BalancingRule)) + for _, rule := range config.BalancingRule { + balancer, err := rule.Build(ohm, dispatcher) + if err != nil { + return err + } + balancer.InjectContext(ctx) + r.balancers[rule.Tag] = balancer + } + + r.rules = make([]*Rule, 0, len(config.Rule)) + for _, rule := range config.Rule { + cond, err := rule.BuildCondition() + if err != nil { + r.closeWebhooks() + return err + } + rr := &Rule{ + Condition: cond, + Tag: rule.GetTag(), + RuleTag: rule.GetRuleTag(), + } + if wh := rule.GetWebhook(); wh != nil { + notifier, err := NewWebhookNotifier(wh) + if err != nil { + r.closeWebhooks() + return err + } + rr.Webhook = notifier + } + btag := rule.GetBalancingTag() + if len(btag) > 0 { + brule, found := r.balancers[btag] + if !found { + if rr.Webhook != nil { + rr.Webhook.Close() + } + r.closeWebhooks() + return errors.New("balancer ", btag, " not found") + } + rr.Balancer = brule + } + r.rules = append(r.rules, rr) + } + + return nil +} + +// PickRoute implements routing.Router. +func (r *Router) PickRoute(ctx routing.Context) (routing.Route, error) { + originalCtx := ctx + rule, ctx, err := r.pickRouteInternal(ctx) + if err != nil { + return nil, err + } + tag, err := rule.GetTag() + if err != nil { + return nil, err + } + if rule.Webhook != nil { + rule.Webhook.Fire(originalCtx, tag) + } + return &Route{Context: ctx, outboundTag: tag, ruleTag: rule.RuleTag}, nil +} + +// AddRule implements routing.Router. +func (r *Router) AddRule(config *serial.TypedMessage, shouldAppend bool) error { + + inst, err := config.GetInstance() + if err != nil { + return err + } + if c, ok := inst.(*Config); ok { + return r.ReloadRules(c, shouldAppend) + } + return errors.New("AddRule: config type error") +} + +func (r *Router) ReloadRules(config *Config, shouldAppend bool) error { + r.mu.Lock() + defer r.mu.Unlock() + + if !shouldAppend { + for _, rule := range r.rules { + if rule.Webhook != nil { + rule.Webhook.Close() + } + } + r.balancers = make(map[string]*Balancer, len(config.BalancingRule)) + r.rules = make([]*Rule, 0, len(config.Rule)) + } + for _, rule := range config.BalancingRule { + _, found := r.balancers[rule.Tag] + if found { + return errors.New("duplicate balancer tag") + } + balancer, err := rule.Build(r.ohm, r.dispatcher) + if err != nil { + return err + } + balancer.InjectContext(r.ctx) + r.balancers[rule.Tag] = balancer + } + + startIdx := len(r.rules) + closeNewWebhooks := func() { + for i := startIdx; i < len(r.rules); i++ { + if r.rules[i].Webhook != nil { + r.rules[i].Webhook.Close() + } + } + r.rules = r.rules[:startIdx] + } + + for _, rule := range config.Rule { + if r.RuleExists(rule.GetRuleTag()) { + closeNewWebhooks() + return errors.New("duplicate ruleTag ", rule.GetRuleTag()) + } + cond, err := rule.BuildCondition() + if err != nil { + closeNewWebhooks() + return err + } + rr := &Rule{ + Condition: cond, + Tag: rule.GetTag(), + RuleTag: rule.GetRuleTag(), + } + if wh := rule.GetWebhook(); wh != nil { + notifier, err := NewWebhookNotifier(wh) + if err != nil { + closeNewWebhooks() + return err + } + rr.Webhook = notifier + } + btag := rule.GetBalancingTag() + if len(btag) > 0 { + brule, found := r.balancers[btag] + if !found { + if rr.Webhook != nil { + rr.Webhook.Close() + } + closeNewWebhooks() + return errors.New("balancer ", btag, " not found") + } + rr.Balancer = brule + } + r.rules = append(r.rules, rr) + } + + return nil +} + +func (r *Router) RuleExists(tag string) bool { + if tag != "" { + for _, rule := range r.rules { + if rule.RuleTag == tag { + return true + } + } + } + return false +} + +// RemoveRule implements routing.Router. +func (r *Router) RemoveRule(tag string) error { + r.mu.Lock() + defer r.mu.Unlock() + + newRules := []*Rule{} + if tag != "" { + for _, rule := range r.rules { + if rule.RuleTag != tag { + newRules = append(newRules, rule) + } else if rule.Webhook != nil { + rule.Webhook.Close() + } + } + r.rules = newRules + return nil + } + return errors.New("empty tag name!") + +} + +// ListRule implements routing.Router +func (r *Router) ListRule() []routing.Route { + r.mu.Lock() + defer r.mu.Unlock() + ruleList := make([]routing.Route, 0) + for _, rule := range r.rules { + ruleList = append(ruleList, &Route{ + outboundTag: rule.Tag, + ruleTag: rule.RuleTag, + }) + } + return ruleList +} + +func (r *Router) pickRouteInternal(ctx routing.Context) (*Rule, routing.Context, error) { + // SkipDNSResolve is set from DNS module. + // the DOH remote server maybe a domain name, + // this prevents cycle resolving dead loop + skipDNSResolve := ctx.GetSkipDNSResolve() + + if r.domainStrategy == Config_IpOnDemand && !skipDNSResolve { + ctx = routing_dns.ContextWithDNSClient(ctx, r.dns) + } + + for _, rule := range r.rules { + if rule.Apply(ctx) { + return rule, ctx, nil + } + } + + if r.domainStrategy != Config_IpIfNonMatch || len(ctx.GetTargetDomain()) == 0 || skipDNSResolve { + return nil, ctx, common.ErrNoClue + } + + ctx = routing_dns.ContextWithDNSClient(ctx, r.dns) + + // Try applying rules again if we have IPs. + for _, rule := range r.rules { + if rule.Apply(ctx) { + return rule, ctx, nil + } + } + + return nil, ctx, common.ErrNoClue +} + +// Start implements common.Runnable. +func (r *Router) Start() error { + return nil +} + +// closeWebhooks closes all webhook notifiers in the current rule set. +func (r *Router) closeWebhooks() { + for _, rule := range r.rules { + if rule.Webhook != nil { + rule.Webhook.Close() + } + } +} + +// Close implements common.Closable. +func (r *Router) Close() error { + r.mu.Lock() + defer r.mu.Unlock() + r.closeWebhooks() + return nil +} + +// Type implements common.HasType. +func (*Router) Type() interface{} { + return routing.RouterType() +} + +// GetOutboundGroupTags implements routing.Route. +func (r *Route) GetOutboundGroupTags() []string { + return r.outboundGroupTags +} + +// GetOutboundTag implements routing.Route. +func (r *Route) GetOutboundTag() string { + return r.outboundTag +} + +func (r *Route) GetRuleTag() string { + return r.ruleTag +} + +func init() { + common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + r := new(Router) + if err := core.RequireFeatures(ctx, func(d dns.Client, ohm outbound.Manager, dispatcher routing.Dispatcher) error { + return r.Init(ctx, config.(*Config), d, ohm, dispatcher) + }); err != nil { + return nil, err + } + return r, nil + })) +} diff --git a/subproject/Xray-core-main/app/router/router_test.go b/subproject/Xray-core-main/app/router/router_test.go new file mode 100644 index 00000000..a0516e05 --- /dev/null +++ b/subproject/Xray-core-main/app/router/router_test.go @@ -0,0 +1,278 @@ +package router_test + +import ( + "context" + "testing" + + "github.com/golang/mock/gomock" + . "github.com/xtls/xray-core/app/router" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/features/dns" + "github.com/xtls/xray-core/features/outbound" + routing_session "github.com/xtls/xray-core/features/routing/session" + "github.com/xtls/xray-core/testing/mocks" +) + +type mockOutboundManager struct { + outbound.Manager + outbound.HandlerSelector +} + +func TestSimpleRouter(t *testing.T) { + config := &Config{ + Rule: []*RoutingRule{ + { + TargetTag: &RoutingRule_Tag{ + Tag: "test", + }, + Networks: []net.Network{net.Network_TCP}, + }, + }, + } + + mockCtl := gomock.NewController(t) + defer mockCtl.Finish() + + mockDNS := mocks.NewDNSClient(mockCtl) + mockOhm := mocks.NewOutboundManager(mockCtl) + mockHs := mocks.NewOutboundHandlerSelector(mockCtl) + + r := new(Router) + common.Must(r.Init(context.TODO(), config, mockDNS, &mockOutboundManager{ + Manager: mockOhm, + HandlerSelector: mockHs, + }, nil)) + + ctx := session.ContextWithOutbounds(context.Background(), []*session.Outbound{{ + Target: net.TCPDestination(net.DomainAddress("example.com"), 80), + }}) + route, err := r.PickRoute(routing_session.AsRoutingContext(ctx)) + common.Must(err) + if tag := route.GetOutboundTag(); tag != "test" { + t.Error("expect tag 'test', bug actually ", tag) + } +} + +func TestSimpleBalancer(t *testing.T) { + config := &Config{ + Rule: []*RoutingRule{ + { + TargetTag: &RoutingRule_BalancingTag{ + BalancingTag: "balance", + }, + Networks: []net.Network{net.Network_TCP}, + }, + }, + BalancingRule: []*BalancingRule{ + { + Tag: "balance", + OutboundSelector: []string{"test-"}, + }, + }, + } + + mockCtl := gomock.NewController(t) + defer mockCtl.Finish() + + mockDNS := mocks.NewDNSClient(mockCtl) + mockOhm := mocks.NewOutboundManager(mockCtl) + mockHs := mocks.NewOutboundHandlerSelector(mockCtl) + + mockHs.EXPECT().Select(gomock.Eq([]string{"test-"})).Return([]string{"test"}) + + r := new(Router) + common.Must(r.Init(context.TODO(), config, mockDNS, &mockOutboundManager{ + Manager: mockOhm, + HandlerSelector: mockHs, + }, nil)) + + ctx := session.ContextWithOutbounds(context.Background(), []*session.Outbound{{ + Target: net.TCPDestination(net.DomainAddress("example.com"), 80), + }}) + route, err := r.PickRoute(routing_session.AsRoutingContext(ctx)) + common.Must(err) + if tag := route.GetOutboundTag(); tag != "test" { + t.Error("expect tag 'test', bug actually ", tag) + } +} + +/* + +Do not work right now: need a full client setup + +func TestLeastLoadBalancer(t *testing.T) { + config := &Config{ + Rule: []*RoutingRule{ + { + TargetTag: &RoutingRule_BalancingTag{ + BalancingTag: "balance", + }, + Networks: []net.Network{net.Network_TCP}, + }, + }, + BalancingRule: []*BalancingRule{ + { + Tag: "balance", + OutboundSelector: []string{"test-"}, + Strategy: "leastLoad", + StrategySettings: serial.ToTypedMessage(&StrategyLeastLoadConfig{ + Baselines: nil, + Expected: 1, + }), + }, + }, + } + + mockCtl := gomock.NewController(t) + defer mockCtl.Finish() + + mockDNS := mocks.NewDNSClient(mockCtl) + mockOhm := mocks.NewOutboundManager(mockCtl) + mockHs := mocks.NewOutboundHandlerSelector(mockCtl) + + mockHs.EXPECT().Select(gomock.Eq([]string{"test-"})).Return([]string{"test1"}) + + r := new(Router) + common.Must(r.Init(context.TODO(), config, mockDNS, &mockOutboundManager{ + Manager: mockOhm, + HandlerSelector: mockHs, + }, nil)) + ctx := session.ContextWithOutbound(context.Background(), &session.Outbound{Target: net.TCPDestination(net.DomainAddress("v2ray.com"), 80)}) + route, err := r.PickRoute(routing_session.AsRoutingContext(ctx)) + common.Must(err) + if tag := route.GetOutboundTag(); tag != "test1" { + t.Error("expect tag 'test1', bug actually ", tag) + } +}*/ + +func TestIPOnDemand(t *testing.T) { + config := &Config{ + DomainStrategy: Config_IpOnDemand, + Rule: []*RoutingRule{ + { + TargetTag: &RoutingRule_Tag{ + Tag: "test", + }, + Geoip: []*GeoIP{ + { + Cidr: []*CIDR{ + { + Ip: []byte{192, 168, 0, 0}, + Prefix: 16, + }, + }, + }, + }, + }, + }, + } + + mockCtl := gomock.NewController(t) + defer mockCtl.Finish() + + mockDNS := mocks.NewDNSClient(mockCtl) + mockDNS.EXPECT().LookupIP(gomock.Eq("example.com"), dns.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + FakeEnable: false, + }).Return([]net.IP{{192, 168, 0, 1}}, uint32(600), nil).AnyTimes() + + r := new(Router) + common.Must(r.Init(context.TODO(), config, mockDNS, nil, nil)) + + ctx := session.ContextWithOutbounds(context.Background(), []*session.Outbound{{ + Target: net.TCPDestination(net.DomainAddress("example.com"), 80), + }}) + route, err := r.PickRoute(routing_session.AsRoutingContext(ctx)) + common.Must(err) + if tag := route.GetOutboundTag(); tag != "test" { + t.Error("expect tag 'test', bug actually ", tag) + } +} + +func TestIPIfNonMatchDomain(t *testing.T) { + config := &Config{ + DomainStrategy: Config_IpIfNonMatch, + Rule: []*RoutingRule{ + { + TargetTag: &RoutingRule_Tag{ + Tag: "test", + }, + Geoip: []*GeoIP{ + { + Cidr: []*CIDR{ + { + Ip: []byte{192, 168, 0, 0}, + Prefix: 16, + }, + }, + }, + }, + }, + }, + } + + mockCtl := gomock.NewController(t) + defer mockCtl.Finish() + + mockDNS := mocks.NewDNSClient(mockCtl) + mockDNS.EXPECT().LookupIP(gomock.Eq("example.com"), dns.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + FakeEnable: false, + }).Return([]net.IP{{192, 168, 0, 1}}, uint32(600), nil).AnyTimes() + + r := new(Router) + common.Must(r.Init(context.TODO(), config, mockDNS, nil, nil)) + + ctx := session.ContextWithOutbounds(context.Background(), []*session.Outbound{{ + Target: net.TCPDestination(net.DomainAddress("example.com"), 80), + }}) + route, err := r.PickRoute(routing_session.AsRoutingContext(ctx)) + common.Must(err) + if tag := route.GetOutboundTag(); tag != "test" { + t.Error("expect tag 'test', bug actually ", tag) + } +} + +func TestIPIfNonMatchIP(t *testing.T) { + config := &Config{ + DomainStrategy: Config_IpIfNonMatch, + Rule: []*RoutingRule{ + { + TargetTag: &RoutingRule_Tag{ + Tag: "test", + }, + Geoip: []*GeoIP{ + { + Cidr: []*CIDR{ + { + Ip: []byte{127, 0, 0, 0}, + Prefix: 8, + }, + }, + }, + }, + }, + }, + } + + mockCtl := gomock.NewController(t) + defer mockCtl.Finish() + + mockDNS := mocks.NewDNSClient(mockCtl) + + r := new(Router) + common.Must(r.Init(context.TODO(), config, mockDNS, nil, nil)) + + ctx := session.ContextWithOutbounds(context.Background(), []*session.Outbound{{ + Target: net.TCPDestination(net.LocalHostIP, 80), + }}) + route, err := r.PickRoute(routing_session.AsRoutingContext(ctx)) + common.Must(err) + if tag := route.GetOutboundTag(); tag != "test" { + t.Error("expect tag 'test', bug actually ", tag) + } +} diff --git a/subproject/Xray-core-main/app/router/strategy_leastload.go b/subproject/Xray-core-main/app/router/strategy_leastload.go new file mode 100644 index 00000000..1bf3cbc0 --- /dev/null +++ b/subproject/Xray-core-main/app/router/strategy_leastload.go @@ -0,0 +1,203 @@ +package router + +import ( + "context" + "math" + "sort" + "time" + + "github.com/xtls/xray-core/app/observatory" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/dice" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/features/extension" +) + +// LeastLoadStrategy represents a least load balancing strategy +type LeastLoadStrategy struct { + settings *StrategyLeastLoadConfig + costs *WeightManager + + observer extension.Observatory + + ctx context.Context +} + +func (l *LeastLoadStrategy) GetPrincipleTarget(strings []string) []string { + var ret []string + nodes := l.pickOutbounds(strings) + for _, v := range nodes { + ret = append(ret, v.Tag) + } + return ret +} + +// NewLeastLoadStrategy creates a new LeastLoadStrategy with settings +func NewLeastLoadStrategy(settings *StrategyLeastLoadConfig) *LeastLoadStrategy { + return &LeastLoadStrategy{ + settings: settings, + costs: NewWeightManager( + settings.Costs, 1, + func(value, cost float64) float64 { + return value * math.Pow(cost, 0.5) + }, + ), + } +} + +// node is a minimal copy of HealthCheckResult +// we don't use HealthCheckResult directly because +// it may change by health checker during routing +type node struct { + Tag string + CountAll int + CountFail int + RTTAverage time.Duration + RTTDeviation time.Duration + RTTDeviationCost time.Duration +} + +func (s *LeastLoadStrategy) InjectContext(ctx context.Context) { + s.ctx = ctx + common.Must(core.RequireFeatures(s.ctx, func(observatory extension.Observatory) error { + s.observer = observatory + return nil + })) +} + +func (s *LeastLoadStrategy) PickOutbound(candidates []string) string { + selects := s.pickOutbounds(candidates) + count := len(selects) + if count == 0 { + // goes to fallbackTag + return "" + } + return selects[dice.Roll(count)].Tag +} + +func (s *LeastLoadStrategy) pickOutbounds(candidates []string) []*node { + qualified := s.getNodes(candidates, time.Duration(s.settings.MaxRTT)) + selects := s.selectLeastLoad(qualified) + return selects +} + +// selectLeastLoad selects nodes according to Baselines and Expected Count. +// +// The strategy always improves network response speed, not matter which mode below is configured. +// But they can still have different priorities. +// +// 1. Bandwidth priority: no Baseline + Expected Count > 0.: selects `Expected Count` of nodes. +// (one if Expected Count <= 0) +// +// 2. Bandwidth priority advanced: Baselines + Expected Count > 0. +// Select `Expected Count` amount of nodes, and also those near them according to baselines. +// In other words, it selects according to different Baselines, until one of them matches +// the Expected Count, if no Baseline matches, Expected Count applied. +// +// 3. Speed priority: Baselines + `Expected Count <= 0`. +// go through all baselines until find selects, if not, select none. Used in combination +// with 'balancer.fallbackTag', it means: selects qualified nodes or use the fallback. +func (s *LeastLoadStrategy) selectLeastLoad(nodes []*node) []*node { + if len(nodes) == 0 { + errors.LogInfo(s.ctx, "least load: no qualified outbound") + return nil + } + expected := int(s.settings.Expected) + availableCount := len(nodes) + if expected > availableCount { + return nodes + } + + if expected <= 0 { + expected = 1 + } + if len(s.settings.Baselines) == 0 { + return nodes[:expected] + } + + count := 0 + // go through all base line until find expected selects + for _, b := range s.settings.Baselines { + baseline := time.Duration(b) + for i := count; i < availableCount; i++ { + if nodes[i].RTTDeviationCost >= baseline { + break + } + count = i + 1 + } + // don't continue if find expected selects + if count >= expected { + errors.LogDebug(s.ctx, "applied baseline: ", baseline) + break + } + } + if s.settings.Expected > 0 && count < expected { + count = expected + } + return nodes[:count] +} + +func (s *LeastLoadStrategy) getNodes(candidates []string, maxRTT time.Duration) []*node { + if s.observer == nil { + errors.LogError(s.ctx, "observer is nil") + return make([]*node, 0) + } + observeResult, err := s.observer.GetObservation(s.ctx) + if err != nil { + errors.LogInfoInner(s.ctx, err, "cannot get observation") + return make([]*node, 0) + } + + results := observeResult.(*observatory.ObservationResult) + + outboundlist := outboundList(candidates) + + var ret []*node + + for _, v := range results.Status { + if v.Alive && (v.Delay < maxRTT.Milliseconds() || maxRTT == 0) && outboundlist.contains(v.OutboundTag) { + record := &node{ + Tag: v.OutboundTag, + CountAll: 1, + CountFail: 1, + RTTAverage: time.Duration(v.Delay) * time.Millisecond, + RTTDeviation: time.Duration(v.Delay) * time.Millisecond, + RTTDeviationCost: time.Duration(s.costs.Apply(v.OutboundTag, float64(time.Duration(v.Delay)*time.Millisecond))), + } + + if v.HealthPing != nil { + record.RTTAverage = time.Duration(v.HealthPing.Average) + record.RTTDeviation = time.Duration(v.HealthPing.Deviation) + record.RTTDeviationCost = time.Duration(s.costs.Apply(v.OutboundTag, float64(v.HealthPing.Deviation))) + record.CountAll = int(v.HealthPing.All) + record.CountFail = int(v.HealthPing.Fail) + + } + ret = append(ret, record) + } + } + + leastloadSort(ret) + return ret +} + +func leastloadSort(nodes []*node) { + sort.Slice(nodes, func(i, j int) bool { + left := nodes[i] + right := nodes[j] + if left.RTTDeviationCost != right.RTTDeviationCost { + return left.RTTDeviationCost < right.RTTDeviationCost + } + if left.RTTAverage != right.RTTAverage { + return left.RTTAverage < right.RTTAverage + } + if left.CountFail != right.CountFail { + return left.CountFail < right.CountFail + } + if left.CountAll != right.CountAll { + return left.CountAll > right.CountAll + } + return left.Tag < right.Tag + }) +} diff --git a/subproject/Xray-core-main/app/router/strategy_leastload_test.go b/subproject/Xray-core-main/app/router/strategy_leastload_test.go new file mode 100644 index 00000000..832e0a87 --- /dev/null +++ b/subproject/Xray-core-main/app/router/strategy_leastload_test.go @@ -0,0 +1,179 @@ +package router + +import ( + "testing" +) + +/* +Split into multiple package, need to be tested separately + + func TestSelectLeastLoad(t *testing.T) { + settings := &StrategyLeastLoadConfig{ + HealthCheck: &HealthPingConfig{ + SamplingCount: 10, + }, + Expected: 1, + MaxRTT: int64(time.Millisecond * time.Duration(800)), + } + strategy := NewLeastLoadStrategy(settings) + // std 40 + strategy.PutResult("a", time.Millisecond*time.Duration(60)) + strategy.PutResult("a", time.Millisecond*time.Duration(140)) + strategy.PutResult("a", time.Millisecond*time.Duration(60)) + strategy.PutResult("a", time.Millisecond*time.Duration(140)) + // std 60 + strategy.PutResult("b", time.Millisecond*time.Duration(40)) + strategy.PutResult("b", time.Millisecond*time.Duration(160)) + strategy.PutResult("b", time.Millisecond*time.Duration(40)) + strategy.PutResult("b", time.Millisecond*time.Duration(160)) + // std 0, but >MaxRTT + strategy.PutResult("c", time.Millisecond*time.Duration(1000)) + strategy.PutResult("c", time.Millisecond*time.Duration(1000)) + strategy.PutResult("c", time.Millisecond*time.Duration(1000)) + strategy.PutResult("c", time.Millisecond*time.Duration(1000)) + expected := "a" + actual := strategy.SelectAndPick([]string{"a", "b", "c", "untested"}) + if actual != expected { + t.Errorf("expected: %v, actual: %v", expected, actual) + } + } + + func TestSelectLeastLoadWithCost(t *testing.T) { + settings := &StrategyLeastLoadConfig{ + HealthCheck: &HealthPingConfig{ + SamplingCount: 10, + }, + Costs: []*StrategyWeight{ + {Match: "a", Value: 9}, + }, + Expected: 1, + } + strategy := NewLeastLoadStrategy(settings, nil) + // std 40, std+c 120 + strategy.PutResult("a", time.Millisecond*time.Duration(60)) + strategy.PutResult("a", time.Millisecond*time.Duration(140)) + strategy.PutResult("a", time.Millisecond*time.Duration(60)) + strategy.PutResult("a", time.Millisecond*time.Duration(140)) + // std 60 + strategy.PutResult("b", time.Millisecond*time.Duration(40)) + strategy.PutResult("b", time.Millisecond*time.Duration(160)) + strategy.PutResult("b", time.Millisecond*time.Duration(40)) + strategy.PutResult("b", time.Millisecond*time.Duration(160)) + expected := "b" + actual := strategy.SelectAndPick([]string{"a", "b", "untested"}) + if actual != expected { + t.Errorf("expected: %v, actual: %v", expected, actual) + } + } +*/ +func TestSelectLeastExpected(t *testing.T) { + strategy := &LeastLoadStrategy{ + settings: &StrategyLeastLoadConfig{ + Baselines: nil, + Expected: 3, + }, + } + nodes := []*node{ + {Tag: "a", RTTDeviationCost: 100}, + {Tag: "b", RTTDeviationCost: 200}, + {Tag: "c", RTTDeviationCost: 300}, + {Tag: "d", RTTDeviationCost: 350}, + } + expected := 3 + ns := strategy.selectLeastLoad(nodes) + if len(ns) != expected { + t.Errorf("expected: %v, actual: %v", expected, len(ns)) + } +} +func TestSelectLeastExpected2(t *testing.T) { + strategy := &LeastLoadStrategy{ + settings: &StrategyLeastLoadConfig{ + Baselines: nil, + Expected: 3, + }, + } + nodes := []*node{ + {Tag: "a", RTTDeviationCost: 100}, + {Tag: "b", RTTDeviationCost: 200}, + } + expected := 2 + ns := strategy.selectLeastLoad(nodes) + if len(ns) != expected { + t.Errorf("expected: %v, actual: %v", expected, len(ns)) + } +} +func TestSelectLeastExpectedAndBaselines(t *testing.T) { + strategy := &LeastLoadStrategy{ + settings: &StrategyLeastLoadConfig{ + Baselines: []int64{200, 300, 400}, + Expected: 3, + }, + } + nodes := []*node{ + {Tag: "a", RTTDeviationCost: 100}, + {Tag: "b", RTTDeviationCost: 200}, + {Tag: "c", RTTDeviationCost: 250}, + {Tag: "d", RTTDeviationCost: 300}, + {Tag: "e", RTTDeviationCost: 310}, + } + expected := 3 + ns := strategy.selectLeastLoad(nodes) + if len(ns) != expected { + t.Errorf("expected: %v, actual: %v", expected, len(ns)) + } +} +func TestSelectLeastExpectedAndBaselines2(t *testing.T) { + strategy := &LeastLoadStrategy{ + settings: &StrategyLeastLoadConfig{ + Baselines: []int64{200, 300, 400}, + Expected: 3, + }, + } + nodes := []*node{ + {Tag: "a", RTTDeviationCost: 500}, + {Tag: "b", RTTDeviationCost: 600}, + {Tag: "c", RTTDeviationCost: 700}, + {Tag: "d", RTTDeviationCost: 800}, + {Tag: "e", RTTDeviationCost: 900}, + } + expected := 3 + ns := strategy.selectLeastLoad(nodes) + if len(ns) != expected { + t.Errorf("expected: %v, actual: %v", expected, len(ns)) + } +} +func TestSelectLeastLoadBaselines(t *testing.T) { + strategy := &LeastLoadStrategy{ + settings: &StrategyLeastLoadConfig{ + Baselines: []int64{200, 400, 600}, + Expected: 0, + }, + } + nodes := []*node{ + {Tag: "a", RTTDeviationCost: 100}, + {Tag: "b", RTTDeviationCost: 200}, + {Tag: "c", RTTDeviationCost: 300}, + } + expected := 1 + ns := strategy.selectLeastLoad(nodes) + if len(ns) != expected { + t.Errorf("expected: %v, actual: %v", expected, len(ns)) + } +} +func TestSelectLeastLoadBaselinesNoQualified(t *testing.T) { + strategy := &LeastLoadStrategy{ + settings: &StrategyLeastLoadConfig{ + Baselines: []int64{200, 400, 600}, + Expected: 0, + }, + } + nodes := []*node{ + {Tag: "a", RTTDeviationCost: 800}, + {Tag: "b", RTTDeviationCost: 1000}, + } + expected := 0 + ns := strategy.selectLeastLoad(nodes) + if len(ns) != expected { + t.Errorf("expected: %v, actual: %v", expected, len(ns)) + } +} diff --git a/subproject/Xray-core-main/app/router/strategy_leastping.go b/subproject/Xray-core-main/app/router/strategy_leastping.go new file mode 100644 index 00000000..ada3492d --- /dev/null +++ b/subproject/Xray-core-main/app/router/strategy_leastping.go @@ -0,0 +1,67 @@ +package router + +import ( + "context" + + "github.com/xtls/xray-core/app/observatory" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/features/extension" +) + +type LeastPingStrategy struct { + ctx context.Context + observatory extension.Observatory +} + +func (l *LeastPingStrategy) GetPrincipleTarget(strings []string) []string { + return []string{l.PickOutbound(strings)} +} + +func (l *LeastPingStrategy) InjectContext(ctx context.Context) { + l.ctx = ctx + common.Must(core.RequireFeatures(l.ctx, func(observatory extension.Observatory) error { + l.observatory = observatory + return nil + })) +} + +func (l *LeastPingStrategy) PickOutbound(strings []string) string { + if l.observatory == nil { + errors.LogError(l.ctx, "observer is nil") + return "" + } + observeReport, err := l.observatory.GetObservation(l.ctx) + if err != nil { + errors.LogInfoInner(l.ctx, err, "cannot get observer report") + return "" + } + outboundsList := outboundList(strings) + if result, ok := observeReport.(*observatory.ObservationResult); ok { + status := result.Status + leastPing := int64(99999999) + selectedOutboundName := "" + for _, v := range status { + if outboundsList.contains(v.OutboundTag) && v.Alive && v.Delay < leastPing { + selectedOutboundName = v.OutboundTag + leastPing = v.Delay + } + } + return selectedOutboundName + } + + // No way to understand observeReport + return "" +} + +type outboundList []string + +func (o outboundList) contains(name string) bool { + for _, v := range o { + if v == name { + return true + } + } + return false +} diff --git a/subproject/Xray-core-main/app/router/strategy_random.go b/subproject/Xray-core-main/app/router/strategy_random.go new file mode 100644 index 00000000..ea9b7add --- /dev/null +++ b/subproject/Xray-core-main/app/router/strategy_random.go @@ -0,0 +1,67 @@ +package router + +import ( + "context" + + "github.com/xtls/xray-core/app/observatory" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/dice" + "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/features/extension" +) + +// RandomStrategy represents a random balancing strategy +type RandomStrategy struct { + FallbackTag string + + ctx context.Context + observatory extension.Observatory +} + +func (s *RandomStrategy) InjectContext(ctx context.Context) { + s.ctx = ctx + if len(s.FallbackTag) > 0 { + common.Must(core.RequireFeatures(s.ctx, func(observatory extension.Observatory) error { + s.observatory = observatory + return nil + })) + } +} + +func (s *RandomStrategy) GetPrincipleTarget(strings []string) []string { + return strings +} + +func (s *RandomStrategy) PickOutbound(candidates []string) string { + if s.observatory != nil { + observeReport, err := s.observatory.GetObservation(s.ctx) + if err == nil { + aliveTags := make([]string, 0) + if result, ok := observeReport.(*observatory.ObservationResult); ok { + status := result.Status + statusMap := make(map[string]*observatory.OutboundStatus) + for _, outboundStatus := range status { + statusMap[outboundStatus.OutboundTag] = outboundStatus + } + for _, candidate := range candidates { + if outboundStatus, found := statusMap[candidate]; found { + if outboundStatus.Alive { + aliveTags = append(aliveTags, candidate) + } + } else { + // unfound candidate is considered alive + aliveTags = append(aliveTags, candidate) + } + } + candidates = aliveTags + } + } + } + + count := len(candidates) + if count == 0 { + // goes to fallbackTag + return "" + } + return candidates[dice.Roll(count)] +} diff --git a/subproject/Xray-core-main/app/router/webhook.go b/subproject/Xray-core-main/app/router/webhook.go new file mode 100644 index 00000000..32ae2887 --- /dev/null +++ b/subproject/Xray-core-main/app/router/webhook.go @@ -0,0 +1,287 @@ +package router + +import ( + "bytes" + "context" + "encoding/json" + "io" + "net" + "net/http" + "path/filepath" + "runtime" + "strings" + "sync" + "syscall" + "time" + + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/features/routing" + routing_session "github.com/xtls/xray-core/features/routing/session" +) + +// parseURL splits a webhook URL into an HTTP URL and an optional Unix socket +// path. For regular http/https URLs the input is returned unchanged with an +// empty socketPath. For Unix sockets the format is: +// +// /path/to/socket.sock:/http/path +// @abstract:/http/path +// @@padded:/http/path +// +// The :/ separator after the socket path delimits the HTTP request path. +// If omitted, "/" is used. +func parseURL(raw string) (httpURL, socketPath string) { + if len(raw) == 0 || (!filepath.IsAbs(raw) && raw[0] != '@') { + return raw, "" + } + if idx := strings.Index(raw, ":/"); idx >= 0 { + return "http://localhost" + raw[idx+1:], raw[:idx] + } + return "http://localhost/", raw +} + +// resolveSocketPath applies platform-specific transformations to a Unix +// socket path, matching the behaviour of the listen side in +// transport/internet/system_listener.go. +// +// For abstract sockets (prefix @) on Linux/Android: +// - single @ — used as-is (lock-free abstract socket) +// - double @@ — stripped to single @ and padded to +// syscall.RawSockaddrUnix{}.Path length (HAProxy compat) +func resolveSocketPath(path string) string { + if len(path) == 0 || path[0] != '@' { + return path + } + if runtime.GOOS != "linux" && runtime.GOOS != "android" { + return path + } + if len(path) > 1 && path[1] == '@' { + fullAddr := make([]byte, len(syscall.RawSockaddrUnix{}.Path)) + copy(fullAddr, path[1:]) + return string(fullAddr) + } + return path +} + +func ptr[T any](v T) *T { return &v } + +type event struct { + Email *string `json:"email"` + Level *uint32 `json:"level"` + Protocol *string `json:"protocol"` + Network *string `json:"network"` + Source *string `json:"source"` + Destination *string `json:"destination"` + OriginalTarget *string `json:"originalTarget"` + RouteTarget *string `json:"routeTarget"` + InboundTag *string `json:"inboundTag"` + InboundName *string `json:"inboundName"` + InboundLocal *string `json:"inboundLocal"` + OutboundTag *string `json:"outboundTag"` + Timestamp int64 `json:"ts"` +} + +type WebhookNotifier struct { + url string + headers map[string]string + deduplication uint32 + client *http.Client + seen sync.Map + done chan struct{} + wg sync.WaitGroup + closeOnce sync.Once +} + +func NewWebhookNotifier(cfg *WebhookConfig) (*WebhookNotifier, error) { + if cfg == nil || cfg.Url == "" { + return nil, nil + } + + httpURL, socketPath := parseURL(cfg.Url) + h := &WebhookNotifier{ + url: httpURL, + deduplication: cfg.Deduplication, + client: &http.Client{ + Timeout: 5 * time.Second, + }, + done: make(chan struct{}), + } + + if socketPath != "" { + dialAddr := resolveSocketPath(socketPath) + h.client.Transport = &http.Transport{ + DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { + var d net.Dialer + return d.DialContext(ctx, "unix", dialAddr) + }, + } + } + + if len(cfg.Headers) > 0 { + h.headers = make(map[string]string, len(cfg.Headers)) + for k, v := range cfg.Headers { + h.headers[k] = v + } + } + + if h.deduplication > 0 { + h.wg.Add(1) + go h.cleanupLoop() + } + + return h, nil +} + +func (h *WebhookNotifier) Fire(ctx routing.Context, outboundTag string) { + ev := buildEvent(ctx, outboundTag) + + email := "" + if ev.Email != nil { + email = *ev.Email + } + if h.isDuplicate(email) { + return + } + + h.wg.Add(1) + select { + case <-h.done: + h.wg.Done() + return + default: + } + go func() { + defer h.wg.Done() + h.post(ev) + }() +} + +func buildEvent(ctx routing.Context, outboundTag string) *event { + ev := &event{ + Timestamp: time.Now().Unix(), + OutboundTag: ptr(outboundTag), + InboundTag: ptr(ctx.GetInboundTag()), + Protocol: ptr(ctx.GetProtocol()), + Network: ptr(ctx.GetNetwork().SystemString()), + } + + if user := ctx.GetUser(); user != "" { + ev.Email = ptr(user) + } + + if srcIPs := ctx.GetSourceIPs(); len(srcIPs) > 0 { + srcPort := ctx.GetSourcePort() + ev.Source = ptr(net.JoinHostPort(srcIPs[0].String(), srcPort.String())) + } + + targetPort := ctx.GetTargetPort() + if domain := ctx.GetTargetDomain(); domain != "" { + ev.Destination = ptr(net.JoinHostPort(domain, targetPort.String())) + } else if targetIPs := ctx.GetTargetIPs(); len(targetIPs) > 0 { + ev.Destination = ptr(net.JoinHostPort(targetIPs[0].String(), targetPort.String())) + } + + if localIPs := ctx.GetLocalIPs(); len(localIPs) > 0 { + localPort := ctx.GetLocalPort() + ev.InboundLocal = ptr(net.JoinHostPort(localIPs[0].String(), localPort.String())) + } + + if sctx, ok := ctx.(*routing_session.Context); ok { + enrichFromSession(ev, sctx) + } + + return ev +} + +func enrichFromSession(ev *event, sctx *routing_session.Context) { + if sctx.Inbound != nil { + ev.InboundName = ptr(sctx.Inbound.Name) + if sctx.Inbound.User != nil { + ev.Level = ptr(sctx.Inbound.User.Level) + } + } + if sctx.Outbound != nil { + if sctx.Outbound.OriginalTarget.Address != nil { + ev.OriginalTarget = ptr(sctx.Outbound.OriginalTarget.String()) + } + if sctx.Outbound.RouteTarget.Address != nil { + ev.RouteTarget = ptr(sctx.Outbound.RouteTarget.String()) + } + } +} + +func (h *WebhookNotifier) post(ev *event) { + body, err := json.Marshal(ev) + if err != nil { + errors.LogWarning(context.Background(), "webhook: marshal failed: ", err) + return + } + + req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, h.url, bytes.NewReader(body)) + if err != nil { + errors.LogWarning(context.Background(), "webhook: request build failed: ", err) + return + } + req.Header.Set("Content-Type", "application/json") + + for k, v := range h.headers { + req.Header.Set(k, v) + } + + resp, err := h.client.Do(req) + if err != nil { + errors.LogInfo(context.Background(), "webhook: POST failed: ", err) + return + } + defer func() { + io.Copy(io.Discard, resp.Body) + resp.Body.Close() + }() + if resp.StatusCode >= 400 { + errors.LogWarning(context.Background(), "webhook: POST returned status ", resp.StatusCode) + } +} + +func (h *WebhookNotifier) isDuplicate(email string) bool { + if h.deduplication == 0 || email == "" { + return false + } + ttl := time.Duration(h.deduplication) * time.Second + now := time.Now() + if v, loaded := h.seen.LoadOrStore(email, now); loaded { + if now.Sub(v.(time.Time)) < ttl { + return true + } + h.seen.Store(email, now) + } + return false +} + +func (h *WebhookNotifier) cleanupLoop() { + defer h.wg.Done() + ttl := time.Duration(h.deduplication) * time.Second + ticker := time.NewTicker(ttl) + defer ticker.Stop() + for { + select { + case <-h.done: + return + case <-ticker.C: + now := time.Now() + h.seen.Range(func(key, value any) bool { + if now.Sub(value.(time.Time)) >= ttl { + h.seen.Delete(key) + } + return true + }) + } + } +} + +func (h *WebhookNotifier) Close() error { + h.closeOnce.Do(func() { + close(h.done) + }) + h.wg.Wait() + h.client.CloseIdleConnections() + return nil +} diff --git a/subproject/Xray-core-main/app/router/weight.go b/subproject/Xray-core-main/app/router/weight.go new file mode 100644 index 00000000..1bba645e --- /dev/null +++ b/subproject/Xray-core-main/app/router/weight.go @@ -0,0 +1,92 @@ +package router + +import ( + "context" + "regexp" + "strconv" + "strings" + "sync" + + "github.com/xtls/xray-core/common/errors" +) + +type weightScaler func(value, weight float64) float64 + +var numberFinder = regexp.MustCompile(`\d+(\.\d+)?`) + +// NewWeightManager creates a new WeightManager with settings +func NewWeightManager(s []*StrategyWeight, defaultWeight float64, scaler weightScaler) *WeightManager { + return &WeightManager{ + settings: s, + cache: make(map[string]float64), + scaler: scaler, + defaultWeight: defaultWeight, + } +} + +// WeightManager manages weights for specific settings +type WeightManager struct { + settings []*StrategyWeight + cache map[string]float64 + scaler weightScaler + defaultWeight float64 + mu sync.Mutex +} + +// Get get the weight of specified tag +func (s *WeightManager) Get(tag string) float64 { + s.mu.Lock() + defer s.mu.Unlock() + weight, ok := s.cache[tag] + if ok { + return weight + } + weight = s.findValue(tag) + s.cache[tag] = weight + return weight +} + +// Apply applies weight to the value +func (s *WeightManager) Apply(tag string, value float64) float64 { + return s.scaler(value, s.Get(tag)) +} + +func (s *WeightManager) findValue(tag string) float64 { + for _, w := range s.settings { + matched := s.getMatch(tag, w.Match, w.Regexp) + if matched == "" { + continue + } + if w.Value > 0 { + return float64(w.Value) + } + // auto weight from matched + numStr := numberFinder.FindString(matched) + if numStr == "" { + return s.defaultWeight + } + weight, err := strconv.ParseFloat(numStr, 64) + if err != nil { + errors.LogError(context.Background(), "unexpected error from ParseFloat: ", err) + return s.defaultWeight + } + return weight + } + return s.defaultWeight +} + +func (s *WeightManager) getMatch(tag, find string, isRegexp bool) string { + if !isRegexp { + idx := strings.Index(tag, find) + if idx < 0 { + return "" + } + return find + } + r, err := regexp.Compile(find) + if err != nil { + errors.LogError(context.Background(), "invalid regexp: ", find, "err: ", err) + return "" + } + return r.FindString(tag) +} diff --git a/subproject/Xray-core-main/app/router/weight_test.go b/subproject/Xray-core-main/app/router/weight_test.go new file mode 100644 index 00000000..2e6a91e5 --- /dev/null +++ b/subproject/Xray-core-main/app/router/weight_test.go @@ -0,0 +1,60 @@ +package router_test + +import ( + "reflect" + "testing" + + "github.com/xtls/xray-core/app/router" +) + +func TestWeight(t *testing.T) { + manager := router.NewWeightManager( + []*router.StrategyWeight{ + { + Match: "x5", + Value: 100, + }, + { + Match: "x8", + }, + { + Regexp: true, + Match: `\bx0+(\.\d+)?\b`, + Value: 1, + }, + { + Regexp: true, + Match: `\bx\d+(\.\d+)?\b`, + }, + }, + 1, func(v, w float64) float64 { + return v * w + }, + ) + tags := []string{ + "node name, x5, and more", + "node name, x8", + "node name, x15", + "node name, x0100, and more", + "node name, x10.1", + "node name, x00.1, and more", + } + // test weight + expected := []float64{100, 8, 15, 100, 10.1, 1} + actual := make([]float64, 0) + for _, tag := range tags { + actual = append(actual, manager.Get(tag)) + } + if !reflect.DeepEqual(expected, actual) { + t.Errorf("expected: %v, actual: %v", expected, actual) + } + // test scale + expected2 := []float64{1000, 80, 150, 1000, 101, 10} + actual2 := make([]float64, 0) + for _, tag := range tags { + actual2 = append(actual2, manager.Apply(tag, 10)) + } + if !reflect.DeepEqual(expected2, actual2) { + t.Errorf("expected2: %v, actual2: %v", expected2, actual2) + } +} diff --git a/subproject/Xray-core-main/app/stats/channel.go b/subproject/Xray-core-main/app/stats/channel.go new file mode 100644 index 00000000..b32850d3 --- /dev/null +++ b/subproject/Xray-core-main/app/stats/channel.go @@ -0,0 +1,173 @@ +package stats + +import ( + "context" + "sync" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/errors" +) + +// Channel is an implementation of stats.Channel. +type Channel struct { + channel chan channelMessage + subscribers []chan interface{} + + // Synchronization components + access sync.RWMutex + closed chan struct{} + + // Channel options + blocking bool // Set blocking state if channel buffer reaches limit + bufferSize int // Set to 0 as no buffering + subsLimit int // Set to 0 as no subscriber limit +} + +// NewChannel creates an instance of Statistics Channel. +func NewChannel(config *ChannelConfig) *Channel { + return &Channel{ + channel: make(chan channelMessage, config.BufferSize), + subsLimit: int(config.SubscriberLimit), + bufferSize: int(config.BufferSize), + blocking: config.Blocking, + } +} + +// Subscribers implements stats.Channel. +func (c *Channel) Subscribers() []chan interface{} { + c.access.RLock() + defer c.access.RUnlock() + return c.subscribers +} + +// Subscribe implements stats.Channel. +func (c *Channel) Subscribe() (chan interface{}, error) { + c.access.Lock() + defer c.access.Unlock() + if c.subsLimit > 0 && len(c.subscribers) >= c.subsLimit { + return nil, errors.New("Number of subscribers has reached limit") + } + subscriber := make(chan interface{}, c.bufferSize) + c.subscribers = append(c.subscribers, subscriber) + return subscriber, nil +} + +// Unsubscribe implements stats.Channel. +func (c *Channel) Unsubscribe(subscriber chan interface{}) error { + c.access.Lock() + defer c.access.Unlock() + for i, s := range c.subscribers { + if s == subscriber { + // Copy to new memory block to prevent modifying original data + subscribers := make([]chan interface{}, len(c.subscribers)-1) + copy(subscribers[:i], c.subscribers[:i]) + copy(subscribers[i:], c.subscribers[i+1:]) + c.subscribers = subscribers + } + } + return nil +} + +// Publish implements stats.Channel. +func (c *Channel) Publish(ctx context.Context, msg interface{}) { + select { // Early exit if channel closed + case <-c.closed: + return + default: + pub := channelMessage{context: ctx, message: msg} + if c.blocking { + pub.publish(c.channel) + } else { + pub.publishNonBlocking(c.channel) + } + } +} + +// Running returns whether the channel is running. +func (c *Channel) Running() bool { + select { + case <-c.closed: // Channel closed + default: // Channel running or not initialized + if c.closed != nil { // Channel initialized + return true + } + } + return false +} + +// Start implements common.Runnable. +func (c *Channel) Start() error { + c.access.Lock() + defer c.access.Unlock() + if !c.Running() { + c.closed = make(chan struct{}) // Reset close signal + go func() { + for { + select { + case pub := <-c.channel: // Published message received + for _, sub := range c.Subscribers() { // Concurrency-safe subscribers retrievement + if c.blocking { + pub.broadcast(sub) + } else { + pub.broadcastNonBlocking(sub) + } + } + case <-c.closed: // Channel closed + for _, sub := range c.Subscribers() { // Remove all subscribers + common.Must(c.Unsubscribe(sub)) + close(sub) + } + return + } + } + }() + } + return nil +} + +// Close implements common.Closable. +func (c *Channel) Close() error { + c.access.Lock() + defer c.access.Unlock() + if c.Running() { + close(c.closed) // Send closed signal + } + return nil +} + +// channelMessage is the published message with guaranteed delivery. +// message is discarded only when the context is early cancelled. +type channelMessage struct { + context context.Context + message interface{} +} + +func (c channelMessage) publish(publisher chan channelMessage) { + select { + case publisher <- c: + case <-c.context.Done(): + } +} + +func (c channelMessage) publishNonBlocking(publisher chan channelMessage) { + select { + case publisher <- c: + default: // Create another goroutine to keep sending message + go c.publish(publisher) + } +} + +func (c channelMessage) broadcast(subscriber chan interface{}) { + select { + case subscriber <- c.message: + case <-c.context.Done(): + } +} + +func (c channelMessage) broadcastNonBlocking(subscriber chan interface{}) { + select { + case subscriber <- c.message: + default: // Create another goroutine to keep sending message + go c.broadcast(subscriber) + } +} diff --git a/subproject/Xray-core-main/app/stats/channel_test.go b/subproject/Xray-core-main/app/stats/channel_test.go new file mode 100644 index 00000000..f35b95c6 --- /dev/null +++ b/subproject/Xray-core-main/app/stats/channel_test.go @@ -0,0 +1,405 @@ +package stats_test + +import ( + "context" + "fmt" + "testing" + "time" + + . "github.com/xtls/xray-core/app/stats" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/features/stats" +) + +func TestStatsChannel(t *testing.T) { + // At most 2 subscribers could be registered + c := NewChannel(&ChannelConfig{SubscriberLimit: 2, Blocking: true}) + + a, err := stats.SubscribeRunnableChannel(c) + common.Must(err) + if !c.Running() { + t.Fatal("unexpected failure in running channel after first subscription") + } + + b, err := c.Subscribe() + common.Must(err) + + // Test that third subscriber is forbidden + _, err = c.Subscribe() + if err == nil { + t.Fatal("unexpected successful subscription") + } + t.Log("expected error: ", err) + + stopCh := make(chan struct{}) + errCh := make(chan string) + + go func() { + c.Publish(context.Background(), 1) + c.Publish(context.Background(), 2) + c.Publish(context.Background(), "3") + c.Publish(context.Background(), []int{4}) + stopCh <- struct{}{} + }() + + go func() { + if v, ok := (<-a).(int); !ok || v != 1 { + errCh <- fmt.Sprint("unexpected receiving: ", v, ", wanted ", 1) + } + if v, ok := (<-a).(int); !ok || v != 2 { + errCh <- fmt.Sprint("unexpected receiving: ", v, ", wanted ", 2) + } + if v, ok := (<-a).(string); !ok || v != "3" { + errCh <- fmt.Sprint("unexpected receiving: ", v, ", wanted ", "3") + } + if v, ok := (<-a).([]int); !ok || v[0] != 4 { + errCh <- fmt.Sprint("unexpected receiving: ", v, ", wanted ", []int{4}) + } + stopCh <- struct{}{} + }() + + go func() { + if v, ok := (<-b).(int); !ok || v != 1 { + errCh <- fmt.Sprint("unexpected receiving: ", v, ", wanted ", 1) + } + if v, ok := (<-b).(int); !ok || v != 2 { + errCh <- fmt.Sprint("unexpected receiving: ", v, ", wanted ", 2) + } + if v, ok := (<-b).(string); !ok || v != "3" { + errCh <- fmt.Sprint("unexpected receiving: ", v, ", wanted ", "3") + } + if v, ok := (<-b).([]int); !ok || v[0] != 4 { + errCh <- fmt.Sprint("unexpected receiving: ", v, ", wanted ", []int{4}) + } + stopCh <- struct{}{} + }() + + timeout := time.After(2 * time.Second) + for i := 0; i < 3; i++ { + select { + case <-timeout: + t.Fatal("Test timeout after 2s") + case e := <-errCh: + t.Fatal(e) + case <-stopCh: + } + } + + // Test the unsubscription of channel + common.Must(c.Unsubscribe(b)) + + // Test the last subscriber will close channel with `UnsubscribeClosableChannel` + common.Must(stats.UnsubscribeClosableChannel(c, a)) + if c.Running() { + t.Fatal("unexpected running channel after unsubscribing the last subscriber") + } +} + +func TestStatsChannelUnsubscribe(t *testing.T) { + c := NewChannel(&ChannelConfig{Blocking: true}) + common.Must(c.Start()) + defer c.Close() + + a, err := c.Subscribe() + common.Must(err) + defer c.Unsubscribe(a) + + b, err := c.Subscribe() + common.Must(err) + + pauseCh := make(chan struct{}) + stopCh := make(chan struct{}) + errCh := make(chan string) + + { + var aSet, bSet bool + for _, s := range c.Subscribers() { + if s == a { + aSet = true + } + if s == b { + bSet = true + } + } + if !(aSet && bSet) { + t.Fatal("unexpected subscribers: ", c.Subscribers()) + } + } + + go func() { // Blocking publish + c.Publish(context.Background(), 1) + <-pauseCh // Wait for `b` goroutine to resume sending message + c.Publish(context.Background(), 2) + }() + + go func() { + if v, ok := (<-a).(int); !ok || v != 1 { + errCh <- fmt.Sprint("unexpected receiving: ", v, ", wanted ", 1) + } + if v, ok := (<-a).(int); !ok || v != 2 { + errCh <- fmt.Sprint("unexpected receiving: ", v, ", wanted ", 2) + } + }() + + go func() { + if v, ok := (<-b).(int); !ok || v != 1 { + errCh <- fmt.Sprint("unexpected receiving: ", v, ", wanted ", 1) + } + // Unsubscribe `b` while publishing is paused + c.Unsubscribe(b) + { // Test `b` is not in subscribers + var aSet, bSet bool + for _, s := range c.Subscribers() { + if s == a { + aSet = true + } + if s == b { + bSet = true + } + } + if !(aSet && !bSet) { + errCh <- fmt.Sprint("unexpected subscribers: ", c.Subscribers()) + } + } + // Resume publishing progress + close(pauseCh) + // Test `b` is neither closed nor able to receive any data + select { + case v, ok := <-b: + if ok { + errCh <- fmt.Sprint("unexpected data received: ", v) + } else { + errCh <- fmt.Sprint("unexpected closed channel: ", b) + } + default: + } + close(stopCh) + }() + + select { + case <-time.After(2 * time.Second): + t.Fatal("Test timeout after 2s") + case e := <-errCh: + t.Fatal(e) + case <-stopCh: + } +} + +func TestStatsChannelBlocking(t *testing.T) { + // Do not use buffer so as to create blocking scenario + c := NewChannel(&ChannelConfig{BufferSize: 0, Blocking: true}) + common.Must(c.Start()) + defer c.Close() + + a, err := c.Subscribe() + common.Must(err) + defer c.Unsubscribe(a) + + pauseCh := make(chan struct{}) + stopCh := make(chan struct{}) + errCh := make(chan string) + + ctx, cancel := context.WithCancel(context.Background()) + + // Test blocking channel publishing + go func() { + // Dummy message with no subscriber receiving, will block broadcasting goroutine + c.Publish(context.Background(), nil) + + <-pauseCh + + // Publishing should be blocked here, for last message was not cleared and buffer was full + c.Publish(context.Background(), nil) + + pauseCh <- struct{}{} + + // Publishing should still be blocked here + c.Publish(ctx, nil) + + // Check publishing is done because context is canceled + select { + case <-ctx.Done(): + if ctx.Err() != context.Canceled { + errCh <- fmt.Sprint("unexpected error: ", ctx.Err()) + } + default: + errCh <- "unexpected non-blocked publishing" + } + close(stopCh) + }() + + go func() { + pauseCh <- struct{}{} + + select { + case <-pauseCh: + errCh <- "unexpected non-blocked publishing" + case <-time.After(100 * time.Millisecond): + } + + // Receive first published message + <-a + + select { + case <-pauseCh: + case <-time.After(100 * time.Millisecond): + errCh <- "unexpected blocking publishing" + } + + // Manually cancel the context to end publishing + cancel() + }() + + select { + case <-time.After(2 * time.Second): + t.Fatal("Test timeout after 2s") + case e := <-errCh: + t.Fatal(e) + case <-stopCh: + } +} + +func TestStatsChannelNonBlocking(t *testing.T) { + // Do not use buffer so as to create blocking scenario + c := NewChannel(&ChannelConfig{BufferSize: 0, Blocking: false}) + common.Must(c.Start()) + defer c.Close() + + a, err := c.Subscribe() + common.Must(err) + defer c.Unsubscribe(a) + + pauseCh := make(chan struct{}) + stopCh := make(chan struct{}) + errCh := make(chan string) + + ctx, cancel := context.WithCancel(context.Background()) + + // Test blocking channel publishing + go func() { + c.Publish(context.Background(), nil) + c.Publish(context.Background(), nil) + pauseCh <- struct{}{} + <-pauseCh + c.Publish(ctx, nil) + c.Publish(ctx, nil) + // Check publishing is done because context is canceled + select { + case <-ctx.Done(): + if ctx.Err() != context.Canceled { + errCh <- fmt.Sprint("unexpected error: ", ctx.Err()) + } + case <-time.After(100 * time.Millisecond): + errCh <- "unexpected non-cancelled publishing" + } + }() + + go func() { + // Check publishing won't block even if there is no subscriber receiving message + select { + case <-pauseCh: + case <-time.After(100 * time.Millisecond): + errCh <- "unexpected blocking publishing" + } + + // Receive first and second published message + <-a + <-a + + pauseCh <- struct{}{} + + // Manually cancel the context to end publishing + cancel() + + // Check third and forth published message is cancelled and cannot receive + <-time.After(100 * time.Millisecond) + select { + case <-a: + errCh <- "unexpected non-cancelled publishing" + default: + } + select { + case <-a: + errCh <- "unexpected non-cancelled publishing" + default: + } + close(stopCh) + }() + + select { + case <-time.After(2 * time.Second): + t.Fatal("Test timeout after 2s") + case e := <-errCh: + t.Fatal(e) + case <-stopCh: + } +} + +func TestStatsChannelConcurrency(t *testing.T) { + // Do not use buffer so as to create blocking scenario + c := NewChannel(&ChannelConfig{BufferSize: 0, Blocking: true}) + common.Must(c.Start()) + defer c.Close() + + a, err := c.Subscribe() + common.Must(err) + defer c.Unsubscribe(a) + + b, err := c.Subscribe() + common.Must(err) + defer c.Unsubscribe(b) + + stopCh := make(chan struct{}) + errCh := make(chan string) + + go func() { // Blocking publish + c.Publish(context.Background(), 1) + c.Publish(context.Background(), 2) + }() + + go func() { + if v, ok := (<-a).(int); !ok || v != 1 { + errCh <- fmt.Sprint("unexpected receiving: ", v, ", wanted ", 1) + } + if v, ok := (<-a).(int); !ok || v != 2 { + errCh <- fmt.Sprint("unexpected receiving: ", v, ", wanted ", 2) + } + }() + + go func() { + // Block `b` for a time so as to ensure source channel is trying to send message to `b`. + <-time.After(25 * time.Millisecond) + // This causes concurrency scenario: unsubscribe `b` while trying to send message to it + c.Unsubscribe(b) + // Test `b` is not closed and can still receive data 1: + // Because unsubscribe won't affect the ongoing process of sending message. + select { + case v, ok := <-b: + if v1, ok1 := v.(int); !(ok && ok1 && v1 == 1) { + errCh <- fmt.Sprint("unexpected failure in receiving data: ", 1) + } + default: + errCh <- fmt.Sprint("unexpected block from receiving data: ", 1) + } + // Test `b` is not closed but cannot receive data 2: + // Because in a new round of messaging, `b` has been unsubscribed. + select { + case v, ok := <-b: + if ok { + errCh <- fmt.Sprint("unexpected receiving: ", v) + } else { + errCh <- "unexpected closing of channel" + } + default: + } + close(stopCh) + }() + + select { + case <-time.After(2 * time.Second): + t.Fatal("Test timeout after 2s") + case e := <-errCh: + t.Fatal(e) + case <-stopCh: + } +} diff --git a/subproject/Xray-core-main/app/stats/command/command.go b/subproject/Xray-core-main/app/stats/command/command.go new file mode 100644 index 00000000..079df1ec --- /dev/null +++ b/subproject/Xray-core-main/app/stats/command/command.go @@ -0,0 +1,169 @@ +package command + +import ( + "context" + "runtime" + "time" + + "github.com/xtls/xray-core/app/stats" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/strmatcher" + "github.com/xtls/xray-core/core" + feature_stats "github.com/xtls/xray-core/features/stats" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// statsServer is an implementation of StatsService. +type statsServer struct { + stats feature_stats.Manager + startTime time.Time +} + +func NewStatsServer(manager feature_stats.Manager) StatsServiceServer { + return &statsServer{ + stats: manager, + startTime: time.Now(), + } +} + +func (s *statsServer) GetStats(ctx context.Context, request *GetStatsRequest) (*GetStatsResponse, error) { + c := s.stats.GetCounter(request.Name) + if c == nil { + return nil, status.Error(codes.NotFound, request.Name+" not found.") + } + var value int64 + if request.Reset_ { + value = c.Set(0) + } else { + value = c.Value() + } + return &GetStatsResponse{ + Stat: &Stat{ + Name: request.Name, + Value: value, + }, + }, nil +} + +func (s *statsServer) GetStatsOnline(ctx context.Context, request *GetStatsRequest) (*GetStatsResponse, error) { + c := s.stats.GetOnlineMap(request.Name) + if c == nil { + return nil, status.Error(codes.NotFound, request.Name+" not found.") + } + value := int64(c.Count()) + return &GetStatsResponse{ + Stat: &Stat{ + Name: request.Name, + Value: value, + }, + }, nil +} + +func (s *statsServer) GetStatsOnlineIpList(ctx context.Context, request *GetStatsRequest) (*GetStatsOnlineIpListResponse, error) { + c := s.stats.GetOnlineMap(request.Name) + + if c == nil { + return nil, status.Error(codes.NotFound, 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) GetAllOnlineUsers(ctx context.Context, request *GetAllOnlineUsersRequest) (*GetAllOnlineUsersResponse, error) { + return &GetAllOnlineUsersResponse{ + Users: s.stats.GetAllOnlineUsers(), + }, nil +} + +func (s *statsServer) QueryStats(ctx context.Context, request *QueryStatsRequest) (*QueryStatsResponse, error) { + matcher, err := strmatcher.Substr.New(request.Pattern) + if err != nil { + return nil, err + } + + response := &QueryStatsResponse{} + + manager, ok := s.stats.(*stats.Manager) + if !ok { + return nil, errors.New("QueryStats only works its own stats.Manager.") + } + + manager.VisitCounters(func(name string, c feature_stats.Counter) bool { + if matcher.Match(name) { + var value int64 + if request.Reset_ { + value = c.Set(0) + } else { + value = c.Value() + } + response.Stat = append(response.Stat, &Stat{ + Name: name, + Value: value, + }) + } + return true + }) + + return response, nil +} + +func (s *statsServer) GetSysStats(ctx context.Context, request *SysStatsRequest) (*SysStatsResponse, error) { + var rtm runtime.MemStats + runtime.ReadMemStats(&rtm) + + uptime := time.Since(s.startTime) + + response := &SysStatsResponse{ + Uptime: uint32(uptime.Seconds()), + NumGoroutine: uint32(runtime.NumGoroutine()), + Alloc: rtm.Alloc, + TotalAlloc: rtm.TotalAlloc, + Sys: rtm.Sys, + Mallocs: rtm.Mallocs, + Frees: rtm.Frees, + LiveObjects: rtm.Mallocs - rtm.Frees, + NumGC: rtm.NumGC, + PauseTotalNs: rtm.PauseTotalNs, + } + + return response, nil +} + +func (s *statsServer) mustEmbedUnimplementedStatsServiceServer() {} + +type service struct { + statsManager feature_stats.Manager +} + +func (s *service) Register(server *grpc.Server) { + ss := NewStatsServer(s.statsManager) + RegisterStatsServiceServer(server, ss) + + // For compatibility purposes + vCoreDesc := StatsService_ServiceDesc + vCoreDesc.ServiceName = "v2ray.core.app.stats.command.StatsService" + server.RegisterService(&vCoreDesc, ss) +} + +func init() { + common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, cfg interface{}) (interface{}, error) { + s := new(service) + + core.RequireFeatures(ctx, func(sm feature_stats.Manager) { + s.statsManager = sm + }) + + return s, nil + })) +} diff --git a/subproject/Xray-core-main/app/stats/command/command.pb.go b/subproject/Xray-core-main/app/stats/command/command.pb.go new file mode 100644 index 00000000..119cdda4 --- /dev/null +++ b/subproject/Xray-core-main/app/stats/command/command.pb.go @@ -0,0 +1,715 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: app/stats/command/command.proto + +package command + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type GetStatsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Name of the stat counter. + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Whether or not to reset the counter to fetching its value. + Reset_ bool `protobuf:"varint,2,opt,name=reset,proto3" json:"reset,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetStatsRequest) Reset() { + *x = GetStatsRequest{} + mi := &file_app_stats_command_command_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetStatsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetStatsRequest) ProtoMessage() {} + +func (x *GetStatsRequest) ProtoReflect() protoreflect.Message { + mi := &file_app_stats_command_command_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetStatsRequest.ProtoReflect.Descriptor instead. +func (*GetStatsRequest) Descriptor() ([]byte, []int) { + return file_app_stats_command_command_proto_rawDescGZIP(), []int{0} +} + +func (x *GetStatsRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *GetStatsRequest) GetReset_() bool { + if x != nil { + return x.Reset_ + } + return false +} + +type Stat struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Value int64 `protobuf:"varint,2,opt,name=value,proto3" json:"value,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Stat) Reset() { + *x = Stat{} + mi := &file_app_stats_command_command_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Stat) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Stat) ProtoMessage() {} + +func (x *Stat) ProtoReflect() protoreflect.Message { + mi := &file_app_stats_command_command_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Stat.ProtoReflect.Descriptor instead. +func (*Stat) Descriptor() ([]byte, []int) { + return file_app_stats_command_command_proto_rawDescGZIP(), []int{1} +} + +func (x *Stat) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Stat) GetValue() int64 { + if x != nil { + return x.Value + } + return 0 +} + +type GetStatsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Stat *Stat `protobuf:"bytes,1,opt,name=stat,proto3" json:"stat,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetStatsResponse) Reset() { + *x = GetStatsResponse{} + mi := &file_app_stats_command_command_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetStatsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetStatsResponse) ProtoMessage() {} + +func (x *GetStatsResponse) ProtoReflect() protoreflect.Message { + mi := &file_app_stats_command_command_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetStatsResponse.ProtoReflect.Descriptor instead. +func (*GetStatsResponse) Descriptor() ([]byte, []int) { + return file_app_stats_command_command_proto_rawDescGZIP(), []int{2} +} + +func (x *GetStatsResponse) GetStat() *Stat { + if x != nil { + return x.Stat + } + return nil +} + +type QueryStatsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Pattern string `protobuf:"bytes,1,opt,name=pattern,proto3" json:"pattern,omitempty"` + Reset_ bool `protobuf:"varint,2,opt,name=reset,proto3" json:"reset,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *QueryStatsRequest) Reset() { + *x = QueryStatsRequest{} + mi := &file_app_stats_command_command_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *QueryStatsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*QueryStatsRequest) ProtoMessage() {} + +func (x *QueryStatsRequest) ProtoReflect() protoreflect.Message { + mi := &file_app_stats_command_command_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use QueryStatsRequest.ProtoReflect.Descriptor instead. +func (*QueryStatsRequest) Descriptor() ([]byte, []int) { + return file_app_stats_command_command_proto_rawDescGZIP(), []int{3} +} + +func (x *QueryStatsRequest) GetPattern() string { + if x != nil { + return x.Pattern + } + return "" +} + +func (x *QueryStatsRequest) GetReset_() bool { + if x != nil { + return x.Reset_ + } + return false +} + +type QueryStatsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Stat []*Stat `protobuf:"bytes,1,rep,name=stat,proto3" json:"stat,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *QueryStatsResponse) Reset() { + *x = QueryStatsResponse{} + mi := &file_app_stats_command_command_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *QueryStatsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*QueryStatsResponse) ProtoMessage() {} + +func (x *QueryStatsResponse) ProtoReflect() protoreflect.Message { + mi := &file_app_stats_command_command_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use QueryStatsResponse.ProtoReflect.Descriptor instead. +func (*QueryStatsResponse) Descriptor() ([]byte, []int) { + return file_app_stats_command_command_proto_rawDescGZIP(), []int{4} +} + +func (x *QueryStatsResponse) GetStat() []*Stat { + if x != nil { + return x.Stat + } + return nil +} + +type SysStatsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SysStatsRequest) Reset() { + *x = SysStatsRequest{} + mi := &file_app_stats_command_command_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SysStatsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SysStatsRequest) ProtoMessage() {} + +func (x *SysStatsRequest) ProtoReflect() protoreflect.Message { + mi := &file_app_stats_command_command_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SysStatsRequest.ProtoReflect.Descriptor instead. +func (*SysStatsRequest) Descriptor() ([]byte, []int) { + return file_app_stats_command_command_proto_rawDescGZIP(), []int{5} +} + +type SysStatsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + NumGoroutine uint32 `protobuf:"varint,1,opt,name=NumGoroutine,proto3" json:"NumGoroutine,omitempty"` + NumGC uint32 `protobuf:"varint,2,opt,name=NumGC,proto3" json:"NumGC,omitempty"` + Alloc uint64 `protobuf:"varint,3,opt,name=Alloc,proto3" json:"Alloc,omitempty"` + TotalAlloc uint64 `protobuf:"varint,4,opt,name=TotalAlloc,proto3" json:"TotalAlloc,omitempty"` + Sys uint64 `protobuf:"varint,5,opt,name=Sys,proto3" json:"Sys,omitempty"` + Mallocs uint64 `protobuf:"varint,6,opt,name=Mallocs,proto3" json:"Mallocs,omitempty"` + Frees uint64 `protobuf:"varint,7,opt,name=Frees,proto3" json:"Frees,omitempty"` + LiveObjects uint64 `protobuf:"varint,8,opt,name=LiveObjects,proto3" json:"LiveObjects,omitempty"` + PauseTotalNs uint64 `protobuf:"varint,9,opt,name=PauseTotalNs,proto3" json:"PauseTotalNs,omitempty"` + Uptime uint32 `protobuf:"varint,10,opt,name=Uptime,proto3" json:"Uptime,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SysStatsResponse) Reset() { + *x = SysStatsResponse{} + mi := &file_app_stats_command_command_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SysStatsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SysStatsResponse) ProtoMessage() {} + +func (x *SysStatsResponse) ProtoReflect() protoreflect.Message { + mi := &file_app_stats_command_command_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SysStatsResponse.ProtoReflect.Descriptor instead. +func (*SysStatsResponse) Descriptor() ([]byte, []int) { + return file_app_stats_command_command_proto_rawDescGZIP(), []int{6} +} + +func (x *SysStatsResponse) GetNumGoroutine() uint32 { + if x != nil { + return x.NumGoroutine + } + return 0 +} + +func (x *SysStatsResponse) GetNumGC() uint32 { + if x != nil { + return x.NumGC + } + return 0 +} + +func (x *SysStatsResponse) GetAlloc() uint64 { + if x != nil { + return x.Alloc + } + return 0 +} + +func (x *SysStatsResponse) GetTotalAlloc() uint64 { + if x != nil { + return x.TotalAlloc + } + return 0 +} + +func (x *SysStatsResponse) GetSys() uint64 { + if x != nil { + return x.Sys + } + return 0 +} + +func (x *SysStatsResponse) GetMallocs() uint64 { + if x != nil { + return x.Mallocs + } + return 0 +} + +func (x *SysStatsResponse) GetFrees() uint64 { + if x != nil { + return x.Frees + } + return 0 +} + +func (x *SysStatsResponse) GetLiveObjects() uint64 { + if x != nil { + return x.LiveObjects + } + return 0 +} + +func (x *SysStatsResponse) GetPauseTotalNs() uint64 { + if x != nil { + return x.PauseTotalNs + } + return 0 +} + +func (x *SysStatsResponse) GetUptime() uint32 { + if x != nil { + return x.Uptime + } + return 0 +} + +type GetStatsOnlineIpListResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + 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" protobuf_val:"varint,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +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 GetAllOnlineUsersRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetAllOnlineUsersRequest) Reset() { + *x = GetAllOnlineUsersRequest{} + mi := &file_app_stats_command_command_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetAllOnlineUsersRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetAllOnlineUsersRequest) ProtoMessage() {} + +func (x *GetAllOnlineUsersRequest) ProtoReflect() protoreflect.Message { + mi := &file_app_stats_command_command_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetAllOnlineUsersRequest.ProtoReflect.Descriptor instead. +func (*GetAllOnlineUsersRequest) Descriptor() ([]byte, []int) { + return file_app_stats_command_command_proto_rawDescGZIP(), []int{8} +} + +type GetAllOnlineUsersResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Users []string `protobuf:"bytes,1,rep,name=users,proto3" json:"users,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetAllOnlineUsersResponse) Reset() { + *x = GetAllOnlineUsersResponse{} + mi := &file_app_stats_command_command_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetAllOnlineUsersResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetAllOnlineUsersResponse) ProtoMessage() {} + +func (x *GetAllOnlineUsersResponse) ProtoReflect() protoreflect.Message { + mi := &file_app_stats_command_command_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetAllOnlineUsersResponse.ProtoReflect.Descriptor instead. +func (*GetAllOnlineUsersResponse) Descriptor() ([]byte, []int) { + return file_app_stats_command_command_proto_rawDescGZIP(), []int{9} +} + +func (x *GetAllOnlineUsersResponse) GetUsers() []string { + if x != nil { + return x.Users + } + return nil +} + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_app_stats_command_command_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_app_stats_command_command_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_app_stats_command_command_proto_rawDescGZIP(), []int{10} +} + +var File_app_stats_command_command_proto protoreflect.FileDescriptor + +const file_app_stats_command_command_proto_rawDesc = "" + + "\n" + + "\x1fapp/stats/command/command.proto\x12\x16xray.app.stats.command\";\n" + + "\x0fGetStatsRequest\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x14\n" + + "\x05reset\x18\x02 \x01(\bR\x05reset\"0\n" + + "\x04Stat\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x14\n" + + "\x05value\x18\x02 \x01(\x03R\x05value\"D\n" + + "\x10GetStatsResponse\x120\n" + + "\x04stat\x18\x01 \x01(\v2\x1c.xray.app.stats.command.StatR\x04stat\"C\n" + + "\x11QueryStatsRequest\x12\x18\n" + + "\apattern\x18\x01 \x01(\tR\apattern\x12\x14\n" + + "\x05reset\x18\x02 \x01(\bR\x05reset\"F\n" + + "\x12QueryStatsResponse\x120\n" + + "\x04stat\x18\x01 \x03(\v2\x1c.xray.app.stats.command.StatR\x04stat\"\x11\n" + + "\x0fSysStatsRequest\"\xa2\x02\n" + + "\x10SysStatsResponse\x12\"\n" + + "\fNumGoroutine\x18\x01 \x01(\rR\fNumGoroutine\x12\x14\n" + + "\x05NumGC\x18\x02 \x01(\rR\x05NumGC\x12\x14\n" + + "\x05Alloc\x18\x03 \x01(\x04R\x05Alloc\x12\x1e\n" + + "\n" + + "TotalAlloc\x18\x04 \x01(\x04R\n" + + "TotalAlloc\x12\x10\n" + + "\x03Sys\x18\x05 \x01(\x04R\x03Sys\x12\x18\n" + + "\aMallocs\x18\x06 \x01(\x04R\aMallocs\x12\x14\n" + + "\x05Frees\x18\a \x01(\x04R\x05Frees\x12 \n" + + "\vLiveObjects\x18\b \x01(\x04R\vLiveObjects\x12\"\n" + + "\fPauseTotalNs\x18\t \x01(\x04R\fPauseTotalNs\x12\x16\n" + + "\x06Uptime\x18\n" + + " \x01(\rR\x06Uptime\"\xbb\x01\n" + + "\x1cGetStatsOnlineIpListResponse\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12O\n" + + "\x03ips\x18\x02 \x03(\v2=.xray.app.stats.command.GetStatsOnlineIpListResponse.IpsEntryR\x03ips\x1a6\n" + + "\bIpsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\x03R\x05value:\x028\x01\"\x1a\n" + + "\x18GetAllOnlineUsersRequest\"1\n" + + "\x19GetAllOnlineUsersResponse\x12\x14\n" + + "\x05users\x18\x01 \x03(\tR\x05users\"\b\n" + + "\x06Config2\x96\x05\n" + + "\fStatsService\x12_\n" + + "\bGetStats\x12'.xray.app.stats.command.GetStatsRequest\x1a(.xray.app.stats.command.GetStatsResponse\"\x00\x12e\n" + + "\x0eGetStatsOnline\x12'.xray.app.stats.command.GetStatsRequest\x1a(.xray.app.stats.command.GetStatsResponse\"\x00\x12e\n" + + "\n" + + "QueryStats\x12).xray.app.stats.command.QueryStatsRequest\x1a*.xray.app.stats.command.QueryStatsResponse\"\x00\x12b\n" + + "\vGetSysStats\x12'.xray.app.stats.command.SysStatsRequest\x1a(.xray.app.stats.command.SysStatsResponse\"\x00\x12w\n" + + "\x14GetStatsOnlineIpList\x12'.xray.app.stats.command.GetStatsRequest\x1a4.xray.app.stats.command.GetStatsOnlineIpListResponse\"\x00\x12z\n" + + "\x11GetAllOnlineUsers\x120.xray.app.stats.command.GetAllOnlineUsersRequest\x1a1.xray.app.stats.command.GetAllOnlineUsersResponse\"\x00Bd\n" + + "\x1acom.xray.app.stats.commandP\x01Z+github.com/xtls/xray-core/app/stats/command\xaa\x02\x16Xray.App.Stats.Commandb\x06proto3" + +var ( + file_app_stats_command_command_proto_rawDescOnce sync.Once + file_app_stats_command_command_proto_rawDescData []byte +) + +func file_app_stats_command_command_proto_rawDescGZIP() []byte { + file_app_stats_command_command_proto_rawDescOnce.Do(func() { + file_app_stats_command_command_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_app_stats_command_command_proto_rawDesc), len(file_app_stats_command_command_proto_rawDesc))) + }) + return file_app_stats_command_command_proto_rawDescData +} + +var file_app_stats_command_command_proto_msgTypes = make([]protoimpl.MessageInfo, 12) +var file_app_stats_command_command_proto_goTypes = []any{ + (*GetStatsRequest)(nil), // 0: xray.app.stats.command.GetStatsRequest + (*Stat)(nil), // 1: xray.app.stats.command.Stat + (*GetStatsResponse)(nil), // 2: xray.app.stats.command.GetStatsResponse + (*QueryStatsRequest)(nil), // 3: xray.app.stats.command.QueryStatsRequest + (*QueryStatsResponse)(nil), // 4: xray.app.stats.command.QueryStatsResponse + (*SysStatsRequest)(nil), // 5: xray.app.stats.command.SysStatsRequest + (*SysStatsResponse)(nil), // 6: xray.app.stats.command.SysStatsResponse + (*GetStatsOnlineIpListResponse)(nil), // 7: xray.app.stats.command.GetStatsOnlineIpListResponse + (*GetAllOnlineUsersRequest)(nil), // 8: xray.app.stats.command.GetAllOnlineUsersRequest + (*GetAllOnlineUsersResponse)(nil), // 9: xray.app.stats.command.GetAllOnlineUsersResponse + (*Config)(nil), // 10: xray.app.stats.command.Config + nil, // 11: xray.app.stats.command.GetStatsOnlineIpListResponse.IpsEntry +} +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, // 1: xray.app.stats.command.QueryStatsResponse.stat:type_name -> xray.app.stats.command.Stat + 11, // 2: xray.app.stats.command.GetStatsOnlineIpListResponse.ips:type_name -> xray.app.stats.command.GetStatsOnlineIpListResponse.IpsEntry + 0, // 3: xray.app.stats.command.StatsService.GetStats:input_type -> xray.app.stats.command.GetStatsRequest + 0, // 4: xray.app.stats.command.StatsService.GetStatsOnline:input_type -> xray.app.stats.command.GetStatsRequest + 3, // 5: xray.app.stats.command.StatsService.QueryStats:input_type -> xray.app.stats.command.QueryStatsRequest + 5, // 6: xray.app.stats.command.StatsService.GetSysStats:input_type -> xray.app.stats.command.SysStatsRequest + 0, // 7: xray.app.stats.command.StatsService.GetStatsOnlineIpList:input_type -> xray.app.stats.command.GetStatsRequest + 8, // 8: xray.app.stats.command.StatsService.GetAllOnlineUsers:input_type -> xray.app.stats.command.GetAllOnlineUsersRequest + 2, // 9: xray.app.stats.command.StatsService.GetStats:output_type -> xray.app.stats.command.GetStatsResponse + 2, // 10: xray.app.stats.command.StatsService.GetStatsOnline:output_type -> xray.app.stats.command.GetStatsResponse + 4, // 11: xray.app.stats.command.StatsService.QueryStats:output_type -> xray.app.stats.command.QueryStatsResponse + 6, // 12: xray.app.stats.command.StatsService.GetSysStats:output_type -> xray.app.stats.command.SysStatsResponse + 7, // 13: xray.app.stats.command.StatsService.GetStatsOnlineIpList:output_type -> xray.app.stats.command.GetStatsOnlineIpListResponse + 9, // 14: xray.app.stats.command.StatsService.GetAllOnlineUsers:output_type -> xray.app.stats.command.GetAllOnlineUsersResponse + 9, // [9:15] is the sub-list for method output_type + 3, // [3:9] is the sub-list for method input_type + 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 file_app_stats_command_command_proto_init() { + if File_app_stats_command_command_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_app_stats_command_command_proto_rawDesc), len(file_app_stats_command_command_proto_rawDesc)), + NumEnums: 0, + NumMessages: 12, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_app_stats_command_command_proto_goTypes, + DependencyIndexes: file_app_stats_command_command_proto_depIdxs, + MessageInfos: file_app_stats_command_command_proto_msgTypes, + }.Build() + File_app_stats_command_command_proto = out.File + file_app_stats_command_command_proto_goTypes = nil + file_app_stats_command_command_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/app/stats/command/command.proto b/subproject/Xray-core-main/app/stats/command/command.proto new file mode 100644 index 00000000..9c3f9131 --- /dev/null +++ b/subproject/Xray-core-main/app/stats/command/command.proto @@ -0,0 +1,69 @@ +syntax = "proto3"; + +package xray.app.stats.command; +option csharp_namespace = "Xray.App.Stats.Command"; +option go_package = "github.com/xtls/xray-core/app/stats/command"; +option java_package = "com.xray.app.stats.command"; +option java_multiple_files = true; + +message GetStatsRequest { + // Name of the stat counter. + string name = 1; + // Whether or not to reset the counter to fetching its value. + bool reset = 2; +} + +message Stat { + string name = 1; + int64 value = 2; +} + +message GetStatsResponse { + Stat stat = 1; +} + +message QueryStatsRequest { + string pattern = 1; + bool reset = 2; +} + +message QueryStatsResponse { + repeated Stat stat = 1; +} + +message SysStatsRequest {} + +message SysStatsResponse { + uint32 NumGoroutine = 1; + uint32 NumGC = 2; + uint64 Alloc = 3; + uint64 TotalAlloc = 4; + uint64 Sys = 5; + uint64 Mallocs = 6; + uint64 Frees = 7; + uint64 LiveObjects = 8; + uint64 PauseTotalNs = 9; + uint32 Uptime = 10; +} + +message GetStatsOnlineIpListResponse { + string name = 1; + map ips = 2; +} + +message GetAllOnlineUsersRequest {} + +message GetAllOnlineUsersResponse { + repeated string users = 1; +} + +service StatsService { + rpc GetStats(GetStatsRequest) returns (GetStatsResponse) {} + rpc GetStatsOnline(GetStatsRequest) returns (GetStatsResponse) {} + rpc QueryStats(QueryStatsRequest) returns (QueryStatsResponse) {} + rpc GetSysStats(SysStatsRequest) returns (SysStatsResponse) {} + rpc GetStatsOnlineIpList(GetStatsRequest) returns (GetStatsOnlineIpListResponse) {} + rpc GetAllOnlineUsers(GetAllOnlineUsersRequest) returns (GetAllOnlineUsersResponse) {} +} + +message Config {} diff --git a/subproject/Xray-core-main/app/stats/command/command_grpc.pb.go b/subproject/Xray-core-main/app/stats/command/command_grpc.pb.go new file mode 100644 index 00000000..1fd039b8 --- /dev/null +++ b/subproject/Xray-core-main/app/stats/command/command_grpc.pb.go @@ -0,0 +1,311 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.0 +// - protoc v6.33.5 +// source: app/stats/command/command.proto + +package command + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + StatsService_GetStats_FullMethodName = "/xray.app.stats.command.StatsService/GetStats" + StatsService_GetStatsOnline_FullMethodName = "/xray.app.stats.command.StatsService/GetStatsOnline" + StatsService_QueryStats_FullMethodName = "/xray.app.stats.command.StatsService/QueryStats" + StatsService_GetSysStats_FullMethodName = "/xray.app.stats.command.StatsService/GetSysStats" + StatsService_GetStatsOnlineIpList_FullMethodName = "/xray.app.stats.command.StatsService/GetStatsOnlineIpList" + StatsService_GetAllOnlineUsers_FullMethodName = "/xray.app.stats.command.StatsService/GetAllOnlineUsers" +) + +// StatsServiceClient is the client API for StatsService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type StatsServiceClient interface { + GetStats(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) + GetSysStats(ctx context.Context, in *SysStatsRequest, opts ...grpc.CallOption) (*SysStatsResponse, error) + GetStatsOnlineIpList(ctx context.Context, in *GetStatsRequest, opts ...grpc.CallOption) (*GetStatsOnlineIpListResponse, error) + GetAllOnlineUsers(ctx context.Context, in *GetAllOnlineUsersRequest, opts ...grpc.CallOption) (*GetAllOnlineUsersResponse, error) +} + +type statsServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewStatsServiceClient(cc grpc.ClientConnInterface) StatsServiceClient { + return &statsServiceClient{cc} +} + +func (c *statsServiceClient) GetStats(ctx context.Context, in *GetStatsRequest, opts ...grpc.CallOption) (*GetStatsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetStatsResponse) + err := c.cc.Invoke(ctx, StatsService_GetStats_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *statsServiceClient) GetStatsOnline(ctx context.Context, in *GetStatsRequest, opts ...grpc.CallOption) (*GetStatsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetStatsResponse) + err := c.cc.Invoke(ctx, StatsService_GetStatsOnline_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *statsServiceClient) QueryStats(ctx context.Context, in *QueryStatsRequest, opts ...grpc.CallOption) (*QueryStatsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(QueryStatsResponse) + err := c.cc.Invoke(ctx, StatsService_QueryStats_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *statsServiceClient) GetSysStats(ctx context.Context, in *SysStatsRequest, opts ...grpc.CallOption) (*SysStatsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SysStatsResponse) + err := c.cc.Invoke(ctx, StatsService_GetSysStats_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + 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 +} + +func (c *statsServiceClient) GetAllOnlineUsers(ctx context.Context, in *GetAllOnlineUsersRequest, opts ...grpc.CallOption) (*GetAllOnlineUsersResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetAllOnlineUsersResponse) + err := c.cc.Invoke(ctx, StatsService_GetAllOnlineUsers_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// StatsServiceServer is the server API for StatsService service. +// All implementations must embed UnimplementedStatsServiceServer +// for forward compatibility. +type StatsServiceServer interface { + GetStats(context.Context, *GetStatsRequest) (*GetStatsResponse, error) + GetStatsOnline(context.Context, *GetStatsRequest) (*GetStatsResponse, error) + QueryStats(context.Context, *QueryStatsRequest) (*QueryStatsResponse, error) + GetSysStats(context.Context, *SysStatsRequest) (*SysStatsResponse, error) + GetStatsOnlineIpList(context.Context, *GetStatsRequest) (*GetStatsOnlineIpListResponse, error) + GetAllOnlineUsers(context.Context, *GetAllOnlineUsersRequest) (*GetAllOnlineUsersResponse, error) + mustEmbedUnimplementedStatsServiceServer() +} + +// UnimplementedStatsServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedStatsServiceServer struct{} + +func (UnimplementedStatsServiceServer) GetStats(context.Context, *GetStatsRequest) (*GetStatsResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetStats not implemented") +} +func (UnimplementedStatsServiceServer) GetStatsOnline(context.Context, *GetStatsRequest) (*GetStatsResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetStatsOnline not implemented") +} +func (UnimplementedStatsServiceServer) QueryStats(context.Context, *QueryStatsRequest) (*QueryStatsResponse, error) { + return nil, status.Error(codes.Unimplemented, "method QueryStats not implemented") +} +func (UnimplementedStatsServiceServer) GetSysStats(context.Context, *SysStatsRequest) (*SysStatsResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetSysStats not implemented") +} +func (UnimplementedStatsServiceServer) GetStatsOnlineIpList(context.Context, *GetStatsRequest) (*GetStatsOnlineIpListResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetStatsOnlineIpList not implemented") +} +func (UnimplementedStatsServiceServer) GetAllOnlineUsers(context.Context, *GetAllOnlineUsersRequest) (*GetAllOnlineUsersResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetAllOnlineUsers not implemented") +} +func (UnimplementedStatsServiceServer) mustEmbedUnimplementedStatsServiceServer() {} +func (UnimplementedStatsServiceServer) testEmbeddedByValue() {} + +// UnsafeStatsServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to StatsServiceServer will +// result in compilation errors. +type UnsafeStatsServiceServer interface { + mustEmbedUnimplementedStatsServiceServer() +} + +func RegisterStatsServiceServer(s grpc.ServiceRegistrar, srv StatsServiceServer) { + // If the following call panics, it indicates UnimplementedStatsServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&StatsService_ServiceDesc, srv) +} + +func _StatsService_GetStats_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).GetStats(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StatsService_GetStats_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StatsServiceServer).GetStats(ctx, req.(*GetStatsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _StatsService_GetStatsOnline_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).GetStatsOnline(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StatsService_GetStatsOnline_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StatsServiceServer).GetStatsOnline(ctx, req.(*GetStatsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _StatsService_QueryStats_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(QueryStatsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StatsServiceServer).QueryStats(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StatsService_QueryStats_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StatsServiceServer).QueryStats(ctx, req.(*QueryStatsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _StatsService_GetSysStats_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SysStatsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StatsServiceServer).GetSysStats(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StatsService_GetSysStats_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StatsServiceServer).GetSysStats(ctx, req.(*SysStatsRequest)) + } + 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) +} + +func _StatsService_GetAllOnlineUsers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetAllOnlineUsersRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StatsServiceServer).GetAllOnlineUsers(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StatsService_GetAllOnlineUsers_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StatsServiceServer).GetAllOnlineUsers(ctx, req.(*GetAllOnlineUsersRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// StatsService_ServiceDesc is the grpc.ServiceDesc for StatsService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var StatsService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "xray.app.stats.command.StatsService", + HandlerType: (*StatsServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "GetStats", + Handler: _StatsService_GetStats_Handler, + }, + { + MethodName: "GetStatsOnline", + Handler: _StatsService_GetStatsOnline_Handler, + }, + { + MethodName: "QueryStats", + Handler: _StatsService_QueryStats_Handler, + }, + { + MethodName: "GetSysStats", + Handler: _StatsService_GetSysStats_Handler, + }, + { + MethodName: "GetStatsOnlineIpList", + Handler: _StatsService_GetStatsOnlineIpList_Handler, + }, + { + MethodName: "GetAllOnlineUsers", + Handler: _StatsService_GetAllOnlineUsers_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "app/stats/command/command.proto", +} diff --git a/subproject/Xray-core-main/app/stats/command/command_test.go b/subproject/Xray-core-main/app/stats/command/command_test.go new file mode 100644 index 00000000..3f56c7a3 --- /dev/null +++ b/subproject/Xray-core-main/app/stats/command/command_test.go @@ -0,0 +1,91 @@ +package command_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/xtls/xray-core/app/stats" + . "github.com/xtls/xray-core/app/stats/command" + "github.com/xtls/xray-core/common" +) + +func TestGetStats(t *testing.T) { + m, err := stats.NewManager(context.Background(), &stats.Config{}) + common.Must(err) + + sc, err := m.RegisterCounter("test_counter") + common.Must(err) + + sc.Set(1) + + s := NewStatsServer(m) + + testCases := []struct { + name string + reset bool + value int64 + err bool + }{ + { + name: "counterNotExist", + err: true, + }, + { + name: "test_counter", + reset: true, + value: 1, + }, + { + name: "test_counter", + value: 0, + }, + } + for _, tc := range testCases { + resp, err := s.GetStats(context.Background(), &GetStatsRequest{ + Name: tc.name, + Reset_: tc.reset, + }) + if tc.err { + if err == nil { + t.Error("nil error: ", tc.name) + } + } else { + common.Must(err) + if r := cmp.Diff(resp.Stat, &Stat{Name: tc.name, Value: tc.value}, cmpopts.IgnoreUnexported(Stat{})); r != "" { + t.Error(r) + } + } + } +} + +func TestQueryStats(t *testing.T) { + m, err := stats.NewManager(context.Background(), &stats.Config{}) + common.Must(err) + + sc1, err := m.RegisterCounter("test_counter") + common.Must(err) + sc1.Set(1) + + sc2, err := m.RegisterCounter("test_counter_2") + common.Must(err) + sc2.Set(2) + + sc3, err := m.RegisterCounter("test_counter_3") + common.Must(err) + sc3.Set(3) + + s := NewStatsServer(m) + resp, err := s.QueryStats(context.Background(), &QueryStatsRequest{ + Pattern: "counter_", + }) + common.Must(err) + if r := cmp.Diff(resp.Stat, []*Stat{ + {Name: "test_counter_2", Value: 2}, + {Name: "test_counter_3", Value: 3}, + }, cmpopts.SortSlices(func(s1, s2 *Stat) bool { return s1.Name < s2.Name }), + cmpopts.IgnoreUnexported(Stat{})); r != "" { + t.Error(r) + } +} diff --git a/subproject/Xray-core-main/app/stats/config.pb.go b/subproject/Xray-core-main/app/stats/config.pb.go new file mode 100644 index 00000000..59427bfe --- /dev/null +++ b/subproject/Xray-core-main/app/stats/config.pb.go @@ -0,0 +1,181 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: app/stats/config.proto + +package stats + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_app_stats_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_app_stats_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_app_stats_config_proto_rawDescGZIP(), []int{0} +} + +type ChannelConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + Blocking bool `protobuf:"varint,1,opt,name=Blocking,proto3" json:"Blocking,omitempty"` + SubscriberLimit int32 `protobuf:"varint,2,opt,name=SubscriberLimit,proto3" json:"SubscriberLimit,omitempty"` + BufferSize int32 `protobuf:"varint,3,opt,name=BufferSize,proto3" json:"BufferSize,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ChannelConfig) Reset() { + *x = ChannelConfig{} + mi := &file_app_stats_config_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ChannelConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ChannelConfig) ProtoMessage() {} + +func (x *ChannelConfig) ProtoReflect() protoreflect.Message { + mi := &file_app_stats_config_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ChannelConfig.ProtoReflect.Descriptor instead. +func (*ChannelConfig) Descriptor() ([]byte, []int) { + return file_app_stats_config_proto_rawDescGZIP(), []int{1} +} + +func (x *ChannelConfig) GetBlocking() bool { + if x != nil { + return x.Blocking + } + return false +} + +func (x *ChannelConfig) GetSubscriberLimit() int32 { + if x != nil { + return x.SubscriberLimit + } + return 0 +} + +func (x *ChannelConfig) GetBufferSize() int32 { + if x != nil { + return x.BufferSize + } + return 0 +} + +var File_app_stats_config_proto protoreflect.FileDescriptor + +const file_app_stats_config_proto_rawDesc = "" + + "\n" + + "\x16app/stats/config.proto\x12\x0exray.app.stats\"\b\n" + + "\x06Config\"u\n" + + "\rChannelConfig\x12\x1a\n" + + "\bBlocking\x18\x01 \x01(\bR\bBlocking\x12(\n" + + "\x0fSubscriberLimit\x18\x02 \x01(\x05R\x0fSubscriberLimit\x12\x1e\n" + + "\n" + + "BufferSize\x18\x03 \x01(\x05R\n" + + "BufferSizeBL\n" + + "\x12com.xray.app.statsP\x01Z#github.com/xtls/xray-core/app/stats\xaa\x02\x0eXray.App.Statsb\x06proto3" + +var ( + file_app_stats_config_proto_rawDescOnce sync.Once + file_app_stats_config_proto_rawDescData []byte +) + +func file_app_stats_config_proto_rawDescGZIP() []byte { + file_app_stats_config_proto_rawDescOnce.Do(func() { + file_app_stats_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_app_stats_config_proto_rawDesc), len(file_app_stats_config_proto_rawDesc))) + }) + return file_app_stats_config_proto_rawDescData +} + +var file_app_stats_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_app_stats_config_proto_goTypes = []any{ + (*Config)(nil), // 0: xray.app.stats.Config + (*ChannelConfig)(nil), // 1: xray.app.stats.ChannelConfig +} +var file_app_stats_config_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_app_stats_config_proto_init() } +func file_app_stats_config_proto_init() { + if File_app_stats_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_app_stats_config_proto_rawDesc), len(file_app_stats_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_app_stats_config_proto_goTypes, + DependencyIndexes: file_app_stats_config_proto_depIdxs, + MessageInfos: file_app_stats_config_proto_msgTypes, + }.Build() + File_app_stats_config_proto = out.File + file_app_stats_config_proto_goTypes = nil + file_app_stats_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/app/stats/config.proto b/subproject/Xray-core-main/app/stats/config.proto new file mode 100644 index 00000000..378d4ee8 --- /dev/null +++ b/subproject/Xray-core-main/app/stats/config.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; + +package xray.app.stats; +option csharp_namespace = "Xray.App.Stats"; +option go_package = "github.com/xtls/xray-core/app/stats"; +option java_package = "com.xray.app.stats"; +option java_multiple_files = true; + +message Config {} + +message ChannelConfig { + bool Blocking = 1; + int32 SubscriberLimit = 2; + int32 BufferSize = 3; +} diff --git a/subproject/Xray-core-main/app/stats/counter.go b/subproject/Xray-core-main/app/stats/counter.go new file mode 100644 index 00000000..f8373ee7 --- /dev/null +++ b/subproject/Xray-core-main/app/stats/counter.go @@ -0,0 +1,23 @@ +package stats + +import "sync/atomic" + +// Counter is an implementation of stats.Counter. +type Counter struct { + value int64 +} + +// Value implements stats.Counter. +func (c *Counter) Value() int64 { + return atomic.LoadInt64(&c.value) +} + +// Set implements stats.Counter. +func (c *Counter) Set(newValue int64) int64 { + return atomic.SwapInt64(&c.value, newValue) +} + +// Add implements stats.Counter. +func (c *Counter) Add(delta int64) int64 { + return atomic.AddInt64(&c.value, delta) +} diff --git a/subproject/Xray-core-main/app/stats/counter_test.go b/subproject/Xray-core-main/app/stats/counter_test.go new file mode 100644 index 00000000..d7086540 --- /dev/null +++ b/subproject/Xray-core-main/app/stats/counter_test.go @@ -0,0 +1,31 @@ +package stats_test + +import ( + "context" + "testing" + + . "github.com/xtls/xray-core/app/stats" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/features/stats" +) + +func TestStatsCounter(t *testing.T) { + raw, err := common.CreateObject(context.Background(), &Config{}) + common.Must(err) + + m := raw.(stats.Manager) + c, err := m.RegisterCounter("test.counter") + common.Must(err) + + if v := c.Add(1); v != 1 { + t.Fatal("unexpected Add(1) return: ", v, ", wanted ", 1) + } + + if v := c.Set(0); v != 1 { + t.Fatal("unexpected Set(0) return: ", v, ", wanted ", 1) + } + + if v := c.Value(); v != 0 { + t.Fatal("unexpected Value() return: ", v, ", wanted ", 0) + } +} diff --git a/subproject/Xray-core-main/app/stats/online_map.go b/subproject/Xray-core-main/app/stats/online_map.go new file mode 100644 index 00000000..cba62598 --- /dev/null +++ b/subproject/Xray-core-main/app/stats/online_map.go @@ -0,0 +1,99 @@ +package stats + +import ( + "sync" + "sync/atomic" + "time" +) + +const ( + localhostIPv4 = "127.0.0.1" + localhostIPv6 = "[::1]" +) + +type ipEntry struct { + refCount int + lastSeen time.Time +} + +// OnlineMap is a refcount-based implementation of stats.OnlineMap. +// IPs are tracked by reference counting: AddIP increments, RemoveIP decrements. +// An IP is removed from the map when its reference count reaches zero. +type OnlineMap struct { + entries map[string]*ipEntry + access sync.Mutex + count atomic.Int64 +} + +// NewOnlineMap creates a new OnlineMap instance. +func NewOnlineMap() *OnlineMap { + return &OnlineMap{ + entries: make(map[string]*ipEntry), + } +} + +// AddIP implements stats.OnlineMap. +func (om *OnlineMap) AddIP(ip string) { + if ip == localhostIPv4 || ip == localhostIPv6 { + return + } + + om.access.Lock() + defer om.access.Unlock() + + if e, ok := om.entries[ip]; ok { + e.refCount++ + e.lastSeen = time.Now() + } else { + om.entries[ip] = &ipEntry{ + refCount: 1, + lastSeen: time.Now(), + } + om.count.Add(1) + } +} + +// RemoveIP implements stats.OnlineMap. +func (om *OnlineMap) RemoveIP(ip string) { + om.access.Lock() + defer om.access.Unlock() + + e, ok := om.entries[ip] + if !ok { + return + } + e.refCount-- + if e.refCount <= 0 { + delete(om.entries, ip) + om.count.Add(-1) + } +} + +// Count implements stats.OnlineMap. +func (om *OnlineMap) Count() int { + return int(om.count.Load()) +} + +// List implements stats.OnlineMap. +func (om *OnlineMap) List() []string { + om.access.Lock() + defer om.access.Unlock() + + keys := make([]string, 0, len(om.entries)) + for ip := range om.entries { + keys = append(keys, ip) + } + return keys +} + +// IPTimeMap implements stats.OnlineMap. +func (om *OnlineMap) IPTimeMap() map[string]time.Time { + om.access.Lock() + defer om.access.Unlock() + + result := make(map[string]time.Time, len(om.entries)) + for ip, e := range om.entries { + result[ip] = e.lastSeen + } + return result +} diff --git a/subproject/Xray-core-main/app/stats/stats.go b/subproject/Xray-core-main/app/stats/stats.go new file mode 100644 index 00000000..fc12fa23 --- /dev/null +++ b/subproject/Xray-core-main/app/stats/stats.go @@ -0,0 +1,219 @@ +package stats + +import ( + "context" + "sync" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/features/stats" +) + +// Manager is an implementation of stats.Manager. +type Manager struct { + access sync.RWMutex + counters map[string]*Counter + onlineMap map[string]*OnlineMap + channels map[string]*Channel + running bool +} + +// NewManager creates an instance of Statistics Manager. +func NewManager(ctx context.Context, config *Config) (*Manager, error) { + m := &Manager{ + counters: make(map[string]*Counter), + onlineMap: make(map[string]*OnlineMap), + channels: make(map[string]*Channel), + } + + return m, nil +} + +// Type implements common.HasType. +func (*Manager) Type() interface{} { + return stats.ManagerType() +} + +// RegisterCounter implements stats.Manager. +func (m *Manager) RegisterCounter(name string) (stats.Counter, error) { + m.access.Lock() + defer m.access.Unlock() + + if _, found := m.counters[name]; found { + return nil, errors.New("Counter ", name, " already registered.") + } + errors.LogDebug(context.Background(), "create new counter ", name) + c := new(Counter) + m.counters[name] = c + return c, nil +} + +// UnregisterCounter implements stats.Manager. +func (m *Manager) UnregisterCounter(name string) error { + m.access.Lock() + defer m.access.Unlock() + + if _, found := m.counters[name]; found { + errors.LogDebug(context.Background(), "remove counter ", name) + delete(m.counters, name) + } + return nil +} + +// GetCounter implements stats.Manager. +func (m *Manager) GetCounter(name string) stats.Counter { + m.access.RLock() + defer m.access.RUnlock() + + if c, found := m.counters[name]; found { + return c + } + return nil +} + +// VisitCounters calls visitor function on all managed counters. +func (m *Manager) VisitCounters(visitor func(string, stats.Counter) bool) { + m.access.RLock() + defer m.access.RUnlock() + + for name, c := range m.counters { + if !visitor(name, c) { + break + } + } +} + +// RegisterOnlineMap implements stats.Manager. +func (m *Manager) RegisterOnlineMap(name string) (stats.OnlineMap, error) { + m.access.Lock() + defer m.access.Unlock() + + if _, found := m.onlineMap[name]; found { + return nil, errors.New("onlineMap ", name, " already registered.") + } + errors.LogDebug(context.Background(), "create new onlineMap ", name) + om := NewOnlineMap() + m.onlineMap[name] = om + return om, nil +} + +// UnregisterOnlineMap implements stats.Manager. +func (m *Manager) UnregisterOnlineMap(name string) error { + m.access.Lock() + defer m.access.Unlock() + + if _, found := m.onlineMap[name]; found { + errors.LogDebug(context.Background(), "remove onlineMap ", name) + delete(m.onlineMap, name) + } + return nil +} + +// GetOnlineMap implements stats.Manager. +func (m *Manager) GetOnlineMap(name string) stats.OnlineMap { + m.access.RLock() + defer m.access.RUnlock() + + if om, found := m.onlineMap[name]; found { + return om + } + return nil +} + +// RegisterChannel implements stats.Manager. +func (m *Manager) RegisterChannel(name string) (stats.Channel, error) { + m.access.Lock() + defer m.access.Unlock() + + if _, found := m.channels[name]; found { + return nil, errors.New("Channel ", name, " already registered.") + } + errors.LogDebug(context.Background(), "create new channel ", name) + c := NewChannel(&ChannelConfig{BufferSize: 64, Blocking: false}) + m.channels[name] = c + if m.running { + return c, c.Start() + } + return c, nil +} + +// UnregisterChannel implements stats.Manager. +func (m *Manager) UnregisterChannel(name string) error { + m.access.Lock() + defer m.access.Unlock() + + if c, found := m.channels[name]; found { + errors.LogDebug(context.Background(), "remove channel ", name) + delete(m.channels, name) + return c.Close() + } + return nil +} + +// GetChannel implements stats.Manager. +func (m *Manager) GetChannel(name string) stats.Channel { + m.access.RLock() + defer m.access.RUnlock() + + if c, found := m.channels[name]; found { + return c + } + return nil +} + +// GetAllOnlineUsers implements stats.Manager. +func (m *Manager) GetAllOnlineUsers() []string { + m.access.RLock() + defer m.access.RUnlock() + + usersOnline := make([]string, 0, len(m.onlineMap)) + for user, onlineMap := range m.onlineMap { + if onlineMap.Count() > 0 { + usersOnline = append(usersOnline, user) + } + } + + return usersOnline +} + +// Start implements common.Runnable. +func (m *Manager) Start() error { + m.access.Lock() + defer m.access.Unlock() + m.running = true + errs := []error{} + for _, channel := range m.channels { + if err := channel.Start(); err != nil { + errs = append(errs, err) + } + } + if len(errs) != 0 { + return errors.Combine(errs...) + } + return nil +} + +// Close implement common.Closable. +func (m *Manager) Close() error { + m.access.Lock() + defer m.access.Unlock() + m.running = false + errs := []error{} + for name, channel := range m.channels { + errors.LogDebug(context.Background(), "remove channel ", name) + delete(m.channels, name) + if err := channel.Close(); err != nil { + errs = append(errs, err) + } + } + if len(errs) != 0 { + return errors.Combine(errs...) + } + return nil +} + +func init() { + common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + return NewManager(ctx, config.(*Config)) + })) +} diff --git a/subproject/Xray-core-main/app/stats/stats_test.go b/subproject/Xray-core-main/app/stats/stats_test.go new file mode 100644 index 00000000..cdbd404e --- /dev/null +++ b/subproject/Xray-core-main/app/stats/stats_test.go @@ -0,0 +1,86 @@ +package stats_test + +import ( + "context" + "testing" + "time" + + . "github.com/xtls/xray-core/app/stats" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/features/stats" +) + +func TestInterface(t *testing.T) { + _ = (stats.Manager)(new(Manager)) +} + +func TestStatsChannelRunnable(t *testing.T) { + raw, err := common.CreateObject(context.Background(), &Config{}) + common.Must(err) + + m := raw.(stats.Manager) + + ch1, err := m.RegisterChannel("test.channel.1") + c1 := ch1.(*Channel) + common.Must(err) + + if c1.Running() { + t.Fatalf("unexpected running channel: test.channel.%d", 1) + } + + common.Must(m.Start()) + + if !c1.Running() { + t.Fatalf("unexpected non-running channel: test.channel.%d", 1) + } + + ch2, err := m.RegisterChannel("test.channel.2") + c2 := ch2.(*Channel) + common.Must(err) + + if !c2.Running() { + t.Fatalf("unexpected non-running channel: test.channel.%d", 2) + } + + s1, err := c1.Subscribe() + common.Must(err) + common.Must(c1.Close()) + + if c1.Running() { + t.Fatalf("unexpected running channel: test.channel.%d", 1) + } + + select { // Check all subscribers in closed channel are closed + case _, ok := <-s1: + if ok { + t.Fatalf("unexpected non-closed subscriber in channel: test.channel.%d", 1) + } + case <-time.After(500 * time.Millisecond): + t.Fatalf("unexpected non-closed subscriber in channel: test.channel.%d", 1) + } + + if len(c1.Subscribers()) != 0 { // Check subscribers in closed channel are emptied + t.Fatalf("unexpected non-empty subscribers in channel: test.channel.%d", 1) + } + + common.Must(m.Close()) + + if c2.Running() { + t.Fatalf("unexpected running channel: test.channel.%d", 2) + } + + ch3, err := m.RegisterChannel("test.channel.3") + c3 := ch3.(*Channel) + common.Must(err) + + if c3.Running() { + t.Fatalf("unexpected running channel: test.channel.%d", 3) + } + + common.Must(c3.Start()) + common.Must(m.UnregisterChannel("test.channel.3")) + + if c3.Running() { // Test that unregistering will close the channel. + t.Fatalf("unexpected running channel: test.channel.%d", 3) + } +} diff --git a/subproject/Xray-core-main/app/version/config.pb.go b/subproject/Xray-core-main/app/version/config.pb.go new file mode 100644 index 00000000..d294664c --- /dev/null +++ b/subproject/Xray-core-main/app/version/config.pb.go @@ -0,0 +1,143 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: app/version/config.proto + +package version + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + CoreVersion string `protobuf:"bytes,1,opt,name=core_version,json=coreVersion,proto3" json:"core_version,omitempty"` + MinVersion string `protobuf:"bytes,2,opt,name=min_version,json=minVersion,proto3" json:"min_version,omitempty"` + MaxVersion string `protobuf:"bytes,3,opt,name=max_version,json=maxVersion,proto3" json:"max_version,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_app_version_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_app_version_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_app_version_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Config) GetCoreVersion() string { + if x != nil { + return x.CoreVersion + } + return "" +} + +func (x *Config) GetMinVersion() string { + if x != nil { + return x.MinVersion + } + return "" +} + +func (x *Config) GetMaxVersion() string { + if x != nil { + return x.MaxVersion + } + return "" +} + +var File_app_version_config_proto protoreflect.FileDescriptor + +const file_app_version_config_proto_rawDesc = "" + + "\n" + + "\x18app/version/config.proto\x12\x10xray.app.version\"m\n" + + "\x06Config\x12!\n" + + "\fcore_version\x18\x01 \x01(\tR\vcoreVersion\x12\x1f\n" + + "\vmin_version\x18\x02 \x01(\tR\n" + + "minVersion\x12\x1f\n" + + "\vmax_version\x18\x03 \x01(\tR\n" + + "maxVersionBR\n" + + "\x14com.xray.app.versionP\x01Z%github.com/xtls/xray-core/app/version\xaa\x02\x10Xray.App.Versionb\x06proto3" + +var ( + file_app_version_config_proto_rawDescOnce sync.Once + file_app_version_config_proto_rawDescData []byte +) + +func file_app_version_config_proto_rawDescGZIP() []byte { + file_app_version_config_proto_rawDescOnce.Do(func() { + file_app_version_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_app_version_config_proto_rawDesc), len(file_app_version_config_proto_rawDesc))) + }) + return file_app_version_config_proto_rawDescData +} + +var file_app_version_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_app_version_config_proto_goTypes = []any{ + (*Config)(nil), // 0: xray.app.version.Config +} +var file_app_version_config_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_app_version_config_proto_init() } +func file_app_version_config_proto_init() { + if File_app_version_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_app_version_config_proto_rawDesc), len(file_app_version_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_app_version_config_proto_goTypes, + DependencyIndexes: file_app_version_config_proto_depIdxs, + MessageInfos: file_app_version_config_proto_msgTypes, + }.Build() + File_app_version_config_proto = out.File + file_app_version_config_proto_goTypes = nil + file_app_version_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/app/version/config.proto b/subproject/Xray-core-main/app/version/config.proto new file mode 100644 index 00000000..8e804838 --- /dev/null +++ b/subproject/Xray-core-main/app/version/config.proto @@ -0,0 +1,14 @@ +syntax = "proto3"; + +package xray.app.version; +option csharp_namespace = "Xray.App.Version"; +option go_package = "github.com/xtls/xray-core/app/version"; +option java_package = "com.xray.app.version"; +option java_multiple_files = true; + + +message Config { + string core_version = 1; + string min_version = 2; + string max_version = 3; +} diff --git a/subproject/Xray-core-main/app/version/version.go b/subproject/Xray-core-main/app/version/version.go new file mode 100644 index 00000000..25d7e6ff --- /dev/null +++ b/subproject/Xray-core-main/app/version/version.go @@ -0,0 +1,77 @@ +package version + +import ( + "context" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/errors" + "strconv" + "strings" +) + +type Version struct { + config *Config + ctx context.Context +} + +func New(ctx context.Context, config *Config) (*Version, error) { + if config.MinVersion != "" { + result, err := compareVersions(config.MinVersion, config.CoreVersion) + if err != nil { + return nil, err + } + if result > 0 { + return nil, errors.New("this config must be run on version ", config.MinVersion, " or higher") + } + } + if config.MaxVersion != "" { + result, err := compareVersions(config.MaxVersion, config.CoreVersion) + if err != nil { + return nil, err + } + if result < 0 { + return nil, errors.New("this config should be run on version ", config.MaxVersion, " or lower") + } + } + return &Version{config: config, ctx: ctx}, nil +} + +func compareVersions(v1, v2 string) (int, error) { + // Split version strings into components + v1Parts := strings.Split(v1, ".") + v2Parts := strings.Split(v2, ".") + + // Pad shorter versions with zeros + for len(v1Parts) < len(v2Parts) { + v1Parts = append(v1Parts, "0") + } + for len(v2Parts) < len(v1Parts) { + v2Parts = append(v2Parts, "0") + } + + // Compare each part + for i := 0; i < len(v1Parts); i++ { + // Convert parts to integers + n1, err := strconv.Atoi(v1Parts[i]) + if err != nil { + return 0, errors.New("invalid version component ", v1Parts[i], " in ", v1) + } + n2, err := strconv.Atoi(v2Parts[i]) + if err != nil { + return 0, errors.New("invalid version component ", v2Parts[i], " in ", v2) + } + + if n1 < n2 { + return -1, nil // v1 < v2 + } + if n1 > n2 { + return 1, nil // v1 > v2 + } + } + return 0, nil // v1 == v2 +} + +func init() { + common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + return New(ctx, config.(*Config)) + })) +} diff --git a/subproject/Xray-core-main/common/antireplay/antireplay_test.go b/subproject/Xray-core-main/common/antireplay/antireplay_test.go new file mode 100644 index 00000000..57e97184 --- /dev/null +++ b/subproject/Xray-core-main/common/antireplay/antireplay_test.go @@ -0,0 +1,33 @@ +package antireplay + +import ( + "bufio" + "crypto/rand" + "testing" +) + +func BenchmarkMapFilter(b *testing.B) { + filter := NewMapFilter[[16]byte](120) + var sample [16]byte + reader := bufio.NewReader(rand.Reader) + reader.Read(sample[:]) + b.ResetTimer() + for range b.N { + reader.Read(sample[:]) + filter.Check(sample) + } +} + +func TestMapFilter(t *testing.T) { + filter := NewMapFilter[[16]byte](120) + var sample [16]byte + rand.Read(sample[:]) + filter.Check(sample) + if filter.Check(sample) { + t.Error("Unexpected true negative") + } + sample[0]++ + if !filter.Check(sample) { + t.Error("Unexpected false positive") + } +} diff --git a/subproject/Xray-core-main/common/antireplay/mapfilter.go b/subproject/Xray-core-main/common/antireplay/mapfilter.go new file mode 100644 index 00000000..8cfef953 --- /dev/null +++ b/subproject/Xray-core-main/common/antireplay/mapfilter.go @@ -0,0 +1,46 @@ +package antireplay + +import ( + "sync" + "time" +) + +// ReplayFilter checks for replay attacks. +type ReplayFilter[T comparable] struct { + lock sync.Mutex + poolA map[T]struct{} + poolB map[T]struct{} + interval time.Duration + lastClean time.Time +} + +// NewMapFilter create a new filter with specifying the expiration time interval in seconds. +func NewMapFilter[T comparable](interval int64) *ReplayFilter[T] { + filter := &ReplayFilter[T]{ + poolA: make(map[T]struct{}), + poolB: make(map[T]struct{}), + interval: time.Duration(interval) * time.Second, + lastClean: time.Now(), + } + return filter +} + +// Check determines if there are duplicate records. +func (filter *ReplayFilter[T]) Check(sum T) bool { + filter.lock.Lock() + defer filter.lock.Unlock() + + now := time.Now() + if now.Sub(filter.lastClean) >= filter.interval { + filter.poolB = filter.poolA + filter.poolA = make(map[T]struct{}) + filter.lastClean = now + } + + _, existsA := filter.poolA[sum] + _, existsB := filter.poolB[sum] + if !existsA && !existsB { + filter.poolA[sum] = struct{}{} + } + return !(existsA || existsB) +} diff --git a/subproject/Xray-core-main/common/bitmask/byte.go b/subproject/Xray-core-main/common/bitmask/byte.go new file mode 100644 index 00000000..8dcc5c0c --- /dev/null +++ b/subproject/Xray-core-main/common/bitmask/byte.go @@ -0,0 +1,21 @@ +package bitmask + +// Byte is a bitmask in byte. +type Byte byte + +// Has returns true if this bitmask contains another bitmask. +func (b Byte) Has(bb Byte) bool { + return (b & bb) != 0 +} + +func (b *Byte) Set(bb Byte) { + *b |= bb +} + +func (b *Byte) Clear(bb Byte) { + *b &= ^bb +} + +func (b *Byte) Toggle(bb Byte) { + *b ^= bb +} diff --git a/subproject/Xray-core-main/common/bitmask/byte_test.go b/subproject/Xray-core-main/common/bitmask/byte_test.go new file mode 100644 index 00000000..efce1be9 --- /dev/null +++ b/subproject/Xray-core-main/common/bitmask/byte_test.go @@ -0,0 +1,36 @@ +package bitmask_test + +import ( + "testing" + + . "github.com/xtls/xray-core/common/bitmask" +) + +func TestBitmaskByte(t *testing.T) { + b := Byte(0) + b.Set(Byte(1)) + if !b.Has(1) { + t.Fatal("expected ", b, " to contain 1, but actually not") + } + + b.Set(Byte(2)) + if !b.Has(2) { + t.Fatal("expected ", b, " to contain 2, but actually not") + } + if !b.Has(1) { + t.Fatal("expected ", b, " to contain 1, but actually not") + } + + b.Clear(Byte(1)) + if !b.Has(2) { + t.Fatal("expected ", b, " to contain 2, but actually not") + } + if b.Has(1) { + t.Fatal("expected ", b, " to not contain 1, but actually did") + } + + b.Toggle(Byte(2)) + if b.Has(2) { + t.Fatal("expected ", b, " to not contain 2, but actually did") + } +} diff --git a/subproject/Xray-core-main/common/buf/buf.go b/subproject/Xray-core-main/common/buf/buf.go new file mode 100644 index 00000000..9f5c5cbe --- /dev/null +++ b/subproject/Xray-core-main/common/buf/buf.go @@ -0,0 +1,2 @@ +// Package buf provides a light-weight memory allocation mechanism. +package buf // import "github.com/xtls/xray-core/common/buf" diff --git a/subproject/Xray-core-main/common/buf/buffer.go b/subproject/Xray-core-main/common/buf/buffer.go new file mode 100644 index 00000000..6c6d69a4 --- /dev/null +++ b/subproject/Xray-core-main/common/buf/buffer.go @@ -0,0 +1,348 @@ +package buf + +import ( + "io" + + "github.com/xtls/xray-core/common/bytespool" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" +) + +const ( + // Size of a regular buffer. + Size = 8192 +) + +var ErrBufferFull = errors.New("buffer is full") + +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 +// the buffer into an internal buffer pool, in order to recreate a buffer more +// quickly. +type Buffer struct { + v []byte + start int32 + end int32 + ownership ownership + UDP *net.Destination +} + +// New creates a Buffer with 0 length and 8K capacity, managed. +func New() *Buffer { + buf := pool.Get().([]byte) + if cap(buf) >= Size { + buf = buf[:Size] + } else { + buf = make([]byte, Size) + } + + return &Buffer{ + v: buf, + } +} + +// NewExisted creates a standard size Buffer with an existed bytearray, managed. +func NewExisted(b []byte) *Buffer { + if cap(b) < Size { + panic("Invalid buffer") + } + + oLen := len(b) + if oLen < Size { + b = b[:Size] + } + + return &Buffer{ + v: b, + end: int32(oLen), + } +} + +// FromBytes creates a Buffer with an existed bytearray, unmanaged. +func FromBytes(b []byte) *Buffer { + return &Buffer{ + v: b, + end: int32(len(b)), + ownership: unmanaged, + } +} + +// StackNew creates a new Buffer object on stack, managed. +// This method is for buffers that is released in the same function. +func StackNew() Buffer { + buf := pool.Get().([]byte) + if cap(buf) >= Size { + buf = buf[:Size] + } else { + buf = make([]byte, Size) + } + + return Buffer{ + v: buf, + } +} + +// 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. +func (b *Buffer) Release() { + if b == nil || b.v == nil || b.ownership == unmanaged { + return + } + + p := b.v + b.v = nil + b.Clear() + + switch b.ownership { + case managed: + if cap(p) == Size { + pool.Put(p) + } + case bytespools: + bytespool.Free(p) + } + b.UDP = nil +} + +// Clear clears the content of the buffer, results an empty buffer with +// Len() = 0. +func (b *Buffer) Clear() { + b.start = 0 + b.end = 0 +} + +// Byte returns the bytes at index. +func (b *Buffer) Byte(index int32) byte { + return b.v[b.start+index] +} + +// SetByte sets the byte value at index. +func (b *Buffer) SetByte(index int32, value byte) { + b.v[b.start+index] = value +} + +// Bytes returns the content bytes of this Buffer. +func (b *Buffer) Bytes() []byte { + return b.v[b.start:b.end] +} + +// Extend increases the buffer size by n bytes, and returns the extended part. +// It panics if result size is larger than size of this buffer. +func (b *Buffer) Extend(n int32) []byte { + end := b.end + n + if end > int32(len(b.v)) { + panic("extending out of bound") + } + ext := b.v[b.end:end] + b.end = end + clear(ext) + return ext +} + +// BytesRange returns a slice of this buffer with given from and to boundary. +func (b *Buffer) BytesRange(from, to int32) []byte { + if from < 0 { + from += b.Len() + } + if to < 0 { + to += b.Len() + } + return b.v[b.start+from : b.start+to] +} + +// BytesFrom returns a slice of this Buffer starting from the given position. +func (b *Buffer) BytesFrom(from int32) []byte { + if from < 0 { + from += b.Len() + } + return b.v[b.start+from : b.end] +} + +// BytesTo returns a slice of this Buffer from start to the given position. +func (b *Buffer) BytesTo(to int32) []byte { + if to < 0 { + to += b.Len() + } + if to < 0 { + to = 0 + } + return b.v[b.start : b.start+to] +} + +// Check makes sure that 0 <= b.start <= b.end. +func (b *Buffer) Check() { + if b.start < 0 { + b.start = 0 + } + if b.end < 0 { + b.end = 0 + } + if b.start > b.end { + b.start = b.end + } +} + +// Resize cuts the buffer at the given position. +func (b *Buffer) Resize(from, to int32) { + oldEnd := b.end + if from < 0 { + from += b.Len() + } + if to < 0 { + to += b.Len() + } + if to < from { + panic("Invalid slice") + } + b.end = b.start + to + b.start += from + b.Check() + if b.end > oldEnd { + clear(b.v[oldEnd:b.end]) + } +} + +// Advance cuts the buffer at the given position. +func (b *Buffer) Advance(from int32) { + if from < 0 { + from += b.Len() + } + b.start += from + b.Check() +} + +// Len returns the length of the buffer content. +func (b *Buffer) Len() int32 { + if b == nil { + return 0 + } + return b.end - b.start +} + +// Cap returns the capacity of the buffer content. +func (b *Buffer) Cap() int32 { + if b == nil { + return 0 + } + return int32(len(b.v)) +} + +// Available returns the available capacity of the buffer content. +func (b *Buffer) Available() int32 { + if b == nil { + return 0 + } + return int32(len(b.v)) - b.end +} + +// IsEmpty returns true if the buffer is empty. +func (b *Buffer) IsEmpty() bool { + return b.Len() == 0 +} + +// IsFull returns true if the buffer has no more room to grow. +func (b *Buffer) IsFull() bool { + return b != nil && b.end == int32(len(b.v)) +} + +// Write implements Write method in io.Writer. +func (b *Buffer) Write(data []byte) (int, error) { + nBytes := copy(b.v[b.end:], data) + b.end += int32(nBytes) + if nBytes < len(data) { + return nBytes, ErrBufferFull + } + return nBytes, nil +} + +// WriteByte writes a single byte into the buffer. +func (b *Buffer) WriteByte(v byte) error { + if b.IsFull() { + return ErrBufferFull + } + b.v[b.end] = v + b.end++ + return nil +} + +// WriteString implements io.StringWriter. +func (b *Buffer) WriteString(s string) (int, error) { + return b.Write([]byte(s)) +} + +// ReadByte implements io.ByteReader +func (b *Buffer) ReadByte() (byte, error) { + if b.start == b.end { + return 0, io.EOF + } + + nb := b.v[b.start] + b.start++ + return nb, nil +} + +// ReadBytes implements bufio.Reader.ReadBytes +func (b *Buffer) ReadBytes(length int32) ([]byte, error) { + if b.end-b.start < length { + return nil, io.EOF + } + + nb := b.v[b.start : b.start+length] + b.start += length + return nb, nil +} + +// Read implements io.Reader.Read(). +func (b *Buffer) Read(data []byte) (int, error) { + if b.Len() == 0 { + return 0, io.EOF + } + nBytes := copy(data, b.v[b.start:b.end]) + if int32(nBytes) == b.Len() { + b.Clear() + } else { + b.start += int32(nBytes) + } + return nBytes, nil +} + +// ReadFrom implements io.ReaderFrom. +func (b *Buffer) ReadFrom(reader io.Reader) (int64, error) { + n, err := reader.Read(b.v[b.end:]) + b.end += int32(n) + return int64(n), err +} + +// ReadFullFrom reads exact size of bytes from given reader, or until error occurs. +func (b *Buffer) ReadFullFrom(reader io.Reader, size int32) (int64, error) { + end := b.end + size + if end > int32(len(b.v)) { + v := end + return 0, errors.New("out of bound: ", v) + } + n, err := io.ReadFull(reader, b.v[b.end:end]) + b.end += int32(n) + return int64(n), err +} + +// String returns the string form of this Buffer. +func (b *Buffer) String() string { + return string(b.Bytes()) +} diff --git a/subproject/Xray-core-main/common/buf/buffer_test.go b/subproject/Xray-core-main/common/buf/buffer_test.go new file mode 100644 index 00000000..596ae4e1 --- /dev/null +++ b/subproject/Xray-core-main/common/buf/buffer_test.go @@ -0,0 +1,224 @@ +package buf_test + +import ( + "bytes" + "crypto/rand" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/xtls/xray-core/common" + . "github.com/xtls/xray-core/common/buf" +) + +func TestBufferClear(t *testing.T) { + buffer := New() + defer buffer.Release() + + payload := "Bytes" + buffer.Write([]byte(payload)) + if diff := cmp.Diff(buffer.Bytes(), []byte(payload)); diff != "" { + t.Error(diff) + } + + buffer.Clear() + if buffer.Len() != 0 { + t.Error("expect 0 length, but got ", buffer.Len()) + } +} + +func TestBufferIsEmpty(t *testing.T) { + buffer := New() + defer buffer.Release() + + if buffer.IsEmpty() != true { + t.Error("expect empty buffer, but not") + } +} + +func TestBufferString(t *testing.T) { + buffer := New() + defer buffer.Release() + + const payload = "Test String" + common.Must2(buffer.WriteString(payload)) + if buffer.String() != payload { + t.Error("expect buffer content as ", payload, " but actually ", buffer.String()) + } +} + +func TestBufferByte(t *testing.T) { + { + buffer := New() + common.Must(buffer.WriteByte('m')) + if buffer.String() != "m" { + t.Error("expect buffer content as ", "m", " but actually ", buffer.String()) + } + buffer.Release() + } + { + buffer := StackNew() + common.Must(buffer.WriteByte('n')) + if buffer.String() != "n" { + t.Error("expect buffer content as ", "n", " but actually ", buffer.String()) + } + buffer.Release() + } + { + buffer := StackNew() + common.Must2(buffer.WriteString("HELLOWORLD")) + if b := buffer.Byte(5); b != 'W' { + t.Error("unexpected byte ", b) + } + + buffer.SetByte(5, 'M') + if buffer.String() != "HELLOMORLD" { + t.Error("expect buffer content as ", "n", " but actually ", buffer.String()) + } + buffer.Release() + } +} + +func TestBufferResize(t *testing.T) { + buffer := New() + defer buffer.Release() + + const payload = "Test String" + common.Must2(buffer.WriteString(payload)) + if buffer.String() != payload { + t.Error("expect buffer content as ", payload, " but actually ", buffer.String()) + } + + buffer.Resize(-6, -3) + if l := buffer.Len(); int(l) != 3 { + t.Error("len error ", l) + } + + if s := buffer.String(); s != "Str" { + t.Error("unexpect buffer ", s) + } + + buffer.Resize(int32(len(payload)), 200) + if l := buffer.Len(); int(l) != 200-len(payload) { + t.Error("len error ", l) + } +} + +func TestBufferSlice(t *testing.T) { + { + b := New() + common.Must2(b.Write([]byte("abcd"))) + bytes := b.BytesFrom(-2) + if diff := cmp.Diff(bytes, []byte{'c', 'd'}); diff != "" { + t.Error(diff) + } + } + + { + b := New() + common.Must2(b.Write([]byte("abcd"))) + bytes := b.BytesTo(-2) + if diff := cmp.Diff(bytes, []byte{'a', 'b'}); diff != "" { + t.Error(diff) + } + } + + { + b := New() + common.Must2(b.Write([]byte("abcd"))) + bytes := b.BytesRange(-3, -1) + if diff := cmp.Diff(bytes, []byte{'b', 'c'}); diff != "" { + t.Error(diff) + } + } +} + +func TestBufferReadFullFrom(t *testing.T) { + payload := make([]byte, 1024) + common.Must2(rand.Read(payload)) + + reader := bytes.NewReader(payload) + b := New() + n, err := b.ReadFullFrom(reader, 1024) + common.Must(err) + if n != 1024 { + t.Error("expect reading 1024 bytes, but actually ", n) + } + + if diff := cmp.Diff(payload, b.Bytes()); diff != "" { + t.Error(diff) + } +} + +func BenchmarkNewBuffer(b *testing.B) { + for i := 0; i < b.N; i++ { + buffer := New() + buffer.Release() + } +} + +func BenchmarkNewBufferStack(b *testing.B) { + for i := 0; i < b.N; i++ { + buffer := StackNew() + buffer.Release() + } +} + +func BenchmarkWrite2(b *testing.B) { + buffer := New() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = buffer.Write([]byte{'a', 'b'}) + buffer.Clear() + } +} + +func BenchmarkWrite8(b *testing.B) { + buffer := New() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = buffer.Write([]byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'}) + buffer.Clear() + } +} + +func BenchmarkWrite32(b *testing.B) { + buffer := New() + payload := make([]byte, 32) + rand.Read(payload) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = buffer.Write(payload) + buffer.Clear() + } +} + +func BenchmarkWriteByte2(b *testing.B) { + buffer := New() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = buffer.WriteByte('a') + _ = buffer.WriteByte('b') + buffer.Clear() + } +} + +func BenchmarkWriteByte8(b *testing.B) { + buffer := New() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = buffer.WriteByte('a') + _ = buffer.WriteByte('b') + _ = buffer.WriteByte('c') + _ = buffer.WriteByte('d') + _ = buffer.WriteByte('e') + _ = buffer.WriteByte('f') + _ = buffer.WriteByte('g') + _ = buffer.WriteByte('h') + buffer.Clear() + } +} diff --git a/subproject/Xray-core-main/common/buf/copy.go b/subproject/Xray-core-main/common/buf/copy.go new file mode 100644 index 00000000..4cc3be88 --- /dev/null +++ b/subproject/Xray-core-main/common/buf/copy.go @@ -0,0 +1,135 @@ +package buf + +import ( + "io" + "time" + + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/signal" + "github.com/xtls/xray-core/features/stats" +) + +type dataHandler func(MultiBuffer) + +type copyHandler struct { + onData []dataHandler +} + +// SizeCounter is for counting bytes copied by Copy(). +type SizeCounter struct { + Size int64 +} + +// CopyOption is an option for copying data. +type CopyOption func(*copyHandler) + +// UpdateActivity is a CopyOption to update activity on each data copy operation. +func UpdateActivity(timer signal.ActivityUpdater) CopyOption { + return func(handler *copyHandler) { + handler.onData = append(handler.onData, func(MultiBuffer) { + timer.Update() + }) + } +} + +// CountSize is a CopyOption that sums the total size of data copied into the given SizeCounter. +func CountSize(sc *SizeCounter) CopyOption { + return func(handler *copyHandler) { + handler.onData = append(handler.onData, func(b MultiBuffer) { + sc.Size += int64(b.Len()) + }) + } +} + +// AddToStatCounter a CopyOption add to stat counter +func AddToStatCounter(sc stats.Counter) CopyOption { + return func(handler *copyHandler) { + handler.onData = append(handler.onData, func(b MultiBuffer) { + if sc != nil { + sc.Add(int64(b.Len())) + } + }) + } +} + +type readError struct { + error +} + +func (e readError) Error() string { + return e.error.Error() +} + +func (e readError) Unwrap() error { + return e.error +} + +// IsReadError returns true if the error in Copy() comes from reading. +func IsReadError(err error) bool { + _, ok := err.(readError) + return ok +} + +type writeError struct { + error +} + +func (e writeError) Error() string { + return e.error.Error() +} + +func (e writeError) Unwrap() error { + return e.error +} + +// IsWriteError returns true if the error in Copy() comes from writing. +func IsWriteError(err error) bool { + _, ok := err.(writeError) + return ok +} + +func copyInternal(reader Reader, writer Writer, handler *copyHandler) error { + for { + buffer, err := reader.ReadMultiBuffer() + if !buffer.IsEmpty() { + for _, handler := range handler.onData { + handler(buffer) + } + + if werr := writer.WriteMultiBuffer(buffer); werr != nil { + return writeError{werr} + } + } + + if err != nil { + return readError{err} + } + } +} + +// Copy dumps all payload from reader to writer or stops when an error occurs. It returns nil when EOF. +func Copy(reader Reader, writer Writer, options ...CopyOption) error { + var handler copyHandler + for _, option := range options { + option(&handler) + } + err := copyInternal(reader, writer, &handler) + if err != nil && errors.Cause(err) != io.EOF { + return err + } + return nil +} + +var ErrNotTimeoutReader = errors.New("not a TimeoutReader") + +func CopyOnceTimeout(reader Reader, writer Writer, timeout time.Duration) error { + timeoutReader, ok := reader.(TimeoutReader) + if !ok { + return ErrNotTimeoutReader + } + mb, err := timeoutReader.ReadMultiBufferTimeout(timeout) + if err != nil { + return err + } + return writer.WriteMultiBuffer(mb) +} diff --git a/subproject/Xray-core-main/common/buf/copy_test.go b/subproject/Xray-core-main/common/buf/copy_test.go new file mode 100644 index 00000000..3c896801 --- /dev/null +++ b/subproject/Xray-core-main/common/buf/copy_test.go @@ -0,0 +1,70 @@ +package buf_test + +import ( + "crypto/rand" + "io" + "testing" + + "github.com/golang/mock/gomock" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/testing/mocks" +) + +func TestReadError(t *testing.T) { + mockCtl := gomock.NewController(t) + defer mockCtl.Finish() + + mockReader := mocks.NewReader(mockCtl) + mockReader.EXPECT().Read(gomock.Any()).Return(0, errors.New("error")) + + err := buf.Copy(buf.NewReader(mockReader), buf.Discard) + if err == nil { + t.Fatal("expected error, but nil") + } + + if !buf.IsReadError(err) { + t.Error("expected to be ReadError, but not") + } + + if err.Error() != "common/buf_test: error" { + t.Fatal("unexpected error message: ", err.Error()) + } +} + +func TestWriteError(t *testing.T) { + mockCtl := gomock.NewController(t) + defer mockCtl.Finish() + + mockWriter := mocks.NewWriter(mockCtl) + mockWriter.EXPECT().Write(gomock.Any()).Return(0, errors.New("error")) + + err := buf.Copy(buf.NewReader(rand.Reader), buf.NewWriter(mockWriter)) + if err == nil { + t.Fatal("expected error, but nil") + } + + if !buf.IsWriteError(err) { + t.Error("expected to be WriteError, but not") + } + + if err.Error() != "common/buf_test: error" { + t.Fatal("unexpected error message: ", err.Error()) + } +} + +type TestReader struct{} + +func (TestReader) Read(b []byte) (int, error) { + return len(b), nil +} + +func BenchmarkCopy(b *testing.B) { + reader := buf.NewReader(io.LimitReader(TestReader{}, 10240)) + writer := buf.Discard + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = buf.Copy(reader, writer) + } +} diff --git a/subproject/Xray-core-main/common/buf/io.go b/subproject/Xray-core-main/common/buf/io.go new file mode 100644 index 00000000..75565e53 --- /dev/null +++ b/subproject/Xray-core-main/common/buf/io.go @@ -0,0 +1,196 @@ +package buf + +import ( + "context" + "io" + "net" + "os" + "syscall" + "time" + + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/features/stats" + "github.com/xtls/xray-core/transport/internet/stat" +) + +// Reader extends io.Reader with MultiBuffer. +type Reader interface { + // ReadMultiBuffer reads content from underlying reader, and put it into a MultiBuffer. + ReadMultiBuffer() (MultiBuffer, error) +} + +// ErrReadTimeout is an error that happens with IO timeout. +var ErrReadTimeout = errors.New("IO timeout") + +// TimeoutReader is a reader that returns error if Read() operation takes longer than the given timeout. +type TimeoutReader interface { + Reader + ReadMultiBufferTimeout(time.Duration) (MultiBuffer, error) +} + +type TimeoutWrapperReader struct { + Reader + stats.Counter + mb MultiBuffer + err error + done chan struct{} +} + +func (r *TimeoutWrapperReader) ReadMultiBuffer() (MultiBuffer, error) { + if r.done != nil { + <-r.done + r.done = nil + if r.Counter != nil { + r.Counter.Add(int64(r.mb.Len())) + } + return r.mb, r.err + } + r.mb, r.err = r.Reader.ReadMultiBuffer() + if r.Counter != nil { + r.Counter.Add(int64(r.mb.Len())) + } + return r.mb, r.err +} + +func (r *TimeoutWrapperReader) ReadMultiBufferTimeout(duration time.Duration) (MultiBuffer, error) { + if r.done == nil { + r.done = make(chan struct{}) + go func() { + r.mb, r.err = r.Reader.ReadMultiBuffer() + close(r.done) + }() + } + timeout := make(chan struct{}) + go func() { + time.Sleep(duration) + close(timeout) + }() + select { + case <-r.done: + r.done = nil + if r.Counter != nil { + r.Counter.Add(int64(r.mb.Len())) + } + return r.mb, r.err + case <-timeout: + return nil, nil + } +} + +// Writer extends io.Writer with MultiBuffer. +type Writer interface { + // WriteMultiBuffer writes a MultiBuffer into underlying writer. + WriteMultiBuffer(MultiBuffer) error +} + +// WriteAllBytes ensures all bytes are written into the given writer. +func WriteAllBytes(writer io.Writer, payload []byte, c stats.Counter) error { + wc := 0 + defer func() { + if c != nil { + c.Add(int64(wc)) + } + }() + + for len(payload) > 0 { + n, err := writer.Write(payload) + wc += n + if err != nil { + return err + } + payload = payload[n:] + } + return nil +} + +func isPacketReader(reader io.Reader) bool { + _, ok := reader.(net.PacketConn) + return ok +} + +// NewReader creates a new Reader. +// The Reader instance doesn't take the ownership of reader. +func NewReader(reader io.Reader) Reader { + if mr, ok := reader.(Reader); ok { + return mr + } + + if isPacketReader(reader) { + return &PacketReader{ + Reader: reader, + } + } + + _, isFile := reader.(*os.File) + if !isFile && useReadv { + if sc, ok := reader.(syscall.Conn); ok { + rawConn, err := sc.SyscallConn() + if err != nil { + errors.LogInfoInner(context.Background(), err, "failed to get sysconn") + } else { + var counter stats.Counter + + if statConn, ok := reader.(*stat.CounterConnection); ok { + reader = statConn.Connection + counter = statConn.ReadCounter + } + return NewReadVReader(reader, rawConn, counter) + } + } + } + + return &SingleReader{ + Reader: reader, + } +} + +// NewPacketReader creates a new PacketReader based on the given reader. +func NewPacketReader(reader io.Reader) Reader { + if mr, ok := reader.(Reader); ok { + return mr + } + + return &PacketReader{ + Reader: reader, + } +} + +func isPacketWriter(writer io.Writer) bool { + if _, ok := writer.(net.PacketConn); ok { + return true + } + + // If the writer doesn't implement syscall.Conn, it is probably not a TCP connection. + if _, ok := writer.(syscall.Conn); !ok { + return true + } + return false +} + +// NewWriter creates a new Writer. +func NewWriter(writer io.Writer) Writer { + if mw, ok := writer.(Writer); ok { + return mw + } + + iConn := writer + if statConn, ok := writer.(*stat.CounterConnection); ok { + iConn = statConn.Connection + } + + if isPacketWriter(iConn) { + return &SequentialWriter{ + Writer: writer, + } + } + + var counter stats.Counter + + if statConn, ok := writer.(*stat.CounterConnection); ok { + counter = statConn.WriteCounter + } + return &BufferToBytesWriter{ + Writer: iConn, + counter: counter, + } +} diff --git a/subproject/Xray-core-main/common/buf/io_test.go b/subproject/Xray-core-main/common/buf/io_test.go new file mode 100644 index 00000000..d48304ea --- /dev/null +++ b/subproject/Xray-core-main/common/buf/io_test.go @@ -0,0 +1,50 @@ +package buf_test + +import ( + "crypto/tls" + "io" + "testing" + + . "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/testing/servers/tcp" +) + +func TestWriterCreation(t *testing.T) { + tcpServer := tcp.Server{} + dest, err := tcpServer.Start() + if err != nil { + t.Fatal("failed to start tcp server: ", err) + } + defer tcpServer.Close() + + conn, err := net.Dial("tcp", dest.NetAddr()) + if err != nil { + t.Fatal("failed to dial a TCP connection: ", err) + } + defer conn.Close() + + { + writer := NewWriter(conn) + if _, ok := writer.(*BufferToBytesWriter); !ok { + t.Fatal("writer is not a BufferToBytesWriter") + } + + writer2 := NewWriter(writer.(io.Writer)) + if writer2 != writer { + t.Fatal("writer is not reused") + } + } + + tlsConn := tls.Client(conn, &tls.Config{ + InsecureSkipVerify: true, + }) + defer tlsConn.Close() + + { + writer := NewWriter(tlsConn) + if _, ok := writer.(*SequentialWriter); !ok { + t.Fatal("writer is not a SequentialWriter") + } + } +} diff --git a/subproject/Xray-core-main/common/buf/multi_buffer.go b/subproject/Xray-core-main/common/buf/multi_buffer.go new file mode 100644 index 00000000..fcac84d7 --- /dev/null +++ b/subproject/Xray-core-main/common/buf/multi_buffer.go @@ -0,0 +1,310 @@ +package buf + +import ( + "io" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/serial" +) + +// ReadAllToBytes reads all content from the reader into a byte array, until EOF. +func ReadAllToBytes(reader io.Reader) ([]byte, error) { + mb, err := ReadFrom(reader) + if err != nil { + return nil, err + } + if mb.Len() == 0 { + return nil, nil + } + b := make([]byte, mb.Len()) + mb, _ = SplitBytes(mb, b) + ReleaseMulti(mb) + return b, nil +} + +// MultiBuffer is a list of Buffers. The order of Buffer matters. +type MultiBuffer []*Buffer + +// MergeMulti merges content from src to dest, and returns the new address of dest and src +func MergeMulti(dest MultiBuffer, src MultiBuffer) (MultiBuffer, MultiBuffer) { + dest = append(dest, src...) + for idx := range src { + src[idx] = nil + } + return dest, src[:0] +} + +// MergeBytes merges the given bytes into MultiBuffer and return the new address of the merged MultiBuffer. +func MergeBytes(dest MultiBuffer, src []byte) MultiBuffer { + n := len(dest) + if n > 0 && !(dest)[n-1].IsFull() { + nBytes, _ := (dest)[n-1].Write(src) + src = src[nBytes:] + } + + for len(src) > 0 { + b := New() + nBytes, _ := b.Write(src) + src = src[nBytes:] + dest = append(dest, b) + } + + return dest +} + +// ReleaseMulti releases all content of the MultiBuffer, and returns an empty MultiBuffer. +func ReleaseMulti(mb MultiBuffer) MultiBuffer { + for i := range mb { + mb[i].Release() + mb[i] = nil + } + return mb[:0] +} + +// Copy copied the beginning part of the MultiBuffer into the given byte array. +func (mb MultiBuffer) Copy(b []byte) int { + total := 0 + for _, bb := range mb { + nBytes := copy(b[total:], bb.Bytes()) + total += nBytes + if int32(nBytes) < bb.Len() { + break + } + } + return total +} + +// ReadFrom reads all content from reader until EOF. +func ReadFrom(reader io.Reader) (MultiBuffer, error) { + mb := make(MultiBuffer, 0, 16) + for { + b := New() + _, err := b.ReadFullFrom(reader, Size) + if b.IsEmpty() { + b.Release() + } else { + mb = append(mb, b) + } + if err != nil { + if errors.Cause(err) == io.EOF || errors.Cause(err) == io.ErrUnexpectedEOF { + return mb, nil + } + return mb, err + } + } +} + +// SplitBytes splits the given amount of bytes from the beginning of the MultiBuffer. +// It returns the new address of MultiBuffer leftover, and number of bytes written into the input byte slice. +func SplitBytes(mb MultiBuffer, b []byte) (MultiBuffer, int) { + totalBytes := 0 + endIndex := -1 + for i := range mb { + pBuffer := mb[i] + nBytes, _ := pBuffer.Read(b) + totalBytes += nBytes + b = b[nBytes:] + if !pBuffer.IsEmpty() { + endIndex = i + break + } + pBuffer.Release() + mb[i] = nil + } + + if endIndex == -1 { + mb = mb[:0] + } else { + mb = mb[endIndex:] + } + + return mb, totalBytes +} + +// SplitFirstBytes splits the first buffer from MultiBuffer, and then copy its content into the given slice. +func SplitFirstBytes(mb MultiBuffer, p []byte) (MultiBuffer, int) { + mb, b := SplitFirst(mb) + if b == nil { + return mb, 0 + } + n := copy(p, b.Bytes()) + b.Release() + return mb, n +} + +// Compact returns another MultiBuffer by merging all content of the given one together. +func Compact(mb MultiBuffer) MultiBuffer { + if len(mb) == 0 { + return mb + } + + mb2 := make(MultiBuffer, 0, len(mb)) + last := mb[0] + + for i := 1; i < len(mb); i++ { + curr := mb[i] + if curr.Len() > last.Available() { + mb2 = append(mb2, last) + last = curr + } else { + common.Must2(last.ReadFrom(curr)) + curr.Release() + } + } + + mb2 = append(mb2, last) + return mb2 +} + +// SplitFirst splits the first Buffer from the beginning of the MultiBuffer. +func SplitFirst(mb MultiBuffer) (MultiBuffer, *Buffer) { + if len(mb) == 0 { + return mb, nil + } + + b := mb[0] + mb[0] = nil + mb = mb[1:] + return mb, b +} + +// SplitSize splits the beginning of the MultiBuffer into another one, for at most size bytes. +func SplitSize(mb MultiBuffer, size int32) (MultiBuffer, MultiBuffer) { + if len(mb) == 0 { + return mb, nil + } + + if mb[0].Len() > size { + b := New() + copy(b.Extend(size), mb[0].BytesTo(size)) + mb[0].Advance(size) + return mb, MultiBuffer{b} + } + + totalBytes := int32(0) + var r MultiBuffer + endIndex := -1 + for i := range mb { + if totalBytes+mb[i].Len() > size { + endIndex = i + break + } + totalBytes += mb[i].Len() + r = append(r, mb[i]) + mb[i] = nil + } + if endIndex == -1 { + // To reuse mb array + mb = mb[:0] + } else { + mb = mb[endIndex:] + } + return mb, r +} + +// SplitMulti splits the beginning of the MultiBuffer into first one, the index i and after into second one +func SplitMulti(mb MultiBuffer, i int) (MultiBuffer, MultiBuffer) { + mb2 := make(MultiBuffer, 0, len(mb)) + if i < len(mb) && i >= 0 { + mb2 = append(mb2, mb[i:]...) + for j := i; j < len(mb); j++ { + mb[j] = nil + } + mb = mb[:i] + } + return mb, mb2 +} + +// WriteMultiBuffer writes all buffers from the MultiBuffer to the Writer one by one, and return error if any, with leftover MultiBuffer. +func WriteMultiBuffer(writer io.Writer, mb MultiBuffer) (MultiBuffer, error) { + for { + mb2, b := SplitFirst(mb) + mb = mb2 + if b == nil { + break + } + + _, err := writer.Write(b.Bytes()) + b.Release() + if err != nil { + return mb, err + } + } + + return nil, nil +} + +// Len returns the total number of bytes in the MultiBuffer. +func (mb MultiBuffer) Len() int32 { + if mb == nil { + return 0 + } + + size := int32(0) + for _, b := range mb { + size += b.Len() + } + return size +} + +// IsEmpty returns true if the MultiBuffer has no content. +func (mb MultiBuffer) IsEmpty() bool { + for _, b := range mb { + if !b.IsEmpty() { + return false + } + } + return true +} + +// String returns the content of the MultiBuffer in string. +func (mb MultiBuffer) String() string { + v := make([]interface{}, len(mb)) + for i, b := range mb { + v[i] = b + } + return serial.Concat(v...) +} + +// MultiBufferContainer is a ReadWriteCloser wrapper over MultiBuffer. +type MultiBufferContainer struct { + MultiBuffer +} + +// Read implements io.Reader. +func (c *MultiBufferContainer) Read(b []byte) (int, error) { + if c.MultiBuffer.IsEmpty() { + return 0, io.EOF + } + + mb, nBytes := SplitBytes(c.MultiBuffer, b) + c.MultiBuffer = mb + return nBytes, nil +} + +// ReadMultiBuffer implements Reader. +func (c *MultiBufferContainer) ReadMultiBuffer() (MultiBuffer, error) { + mb := c.MultiBuffer + c.MultiBuffer = nil + return mb, nil +} + +// Write implements io.Writer. +func (c *MultiBufferContainer) Write(b []byte) (int, error) { + c.MultiBuffer = MergeBytes(c.MultiBuffer, b) + return len(b), nil +} + +// WriteMultiBuffer implements Writer. +func (c *MultiBufferContainer) WriteMultiBuffer(b MultiBuffer) error { + mb, _ := MergeMulti(c.MultiBuffer, b) + c.MultiBuffer = mb + return nil +} + +// Close implements io.Closer. +func (c *MultiBufferContainer) Close() error { + c.MultiBuffer = ReleaseMulti(c.MultiBuffer) + return nil +} diff --git a/subproject/Xray-core-main/common/buf/multi_buffer_test.go b/subproject/Xray-core-main/common/buf/multi_buffer_test.go new file mode 100644 index 00000000..40dbdd63 --- /dev/null +++ b/subproject/Xray-core-main/common/buf/multi_buffer_test.go @@ -0,0 +1,212 @@ +package buf_test + +import ( + "bytes" + "crypto/rand" + "io" + "os" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/xtls/xray-core/common" + . "github.com/xtls/xray-core/common/buf" +) + +func TestMultiBufferRead(t *testing.T) { + b1 := New() + common.Must2(b1.WriteString("ab")) + + b2 := New() + common.Must2(b2.WriteString("cd")) + mb := MultiBuffer{b1, b2} + + bs := make([]byte, 32) + _, nBytes := SplitBytes(mb, bs) + if nBytes != 4 { + t.Error("expect 4 bytes split, but got ", nBytes) + } + if r := cmp.Diff(bs[:nBytes], []byte("abcd")); r != "" { + t.Error(r) + } +} + +func TestMultiBufferAppend(t *testing.T) { + var mb MultiBuffer + b := New() + common.Must2(b.WriteString("ab")) + mb = append(mb, b) + if mb.Len() != 2 { + t.Error("expected length 2, but got ", mb.Len()) + } +} + +func TestMultiBufferSliceBySizeLarge(t *testing.T) { + lb := make([]byte, 8*1024) + common.Must2(io.ReadFull(rand.Reader, lb)) + + mb := MergeBytes(nil, lb) + + mb, mb2 := SplitSize(mb, 1024) + if mb2.Len() != 1024 { + t.Error("expect length 1024, but got ", mb2.Len()) + } + if mb.Len() != 7*1024 { + t.Error("expect length 7*1024, but got ", mb.Len()) + } + + mb, mb3 := SplitSize(mb, 7*1024) + if mb3.Len() != 7*1024 { + t.Error("expect length 7*1024, but got", mb.Len()) + } + + if !mb.IsEmpty() { + t.Error("expect empty buffer, but got ", mb.Len()) + } +} + +func TestMultiBufferSplitFirst(t *testing.T) { + b1 := New() + b1.WriteString("b1") + + b2 := New() + b2.WriteString("b2") + + b3 := New() + b3.WriteString("b3") + + var mb MultiBuffer + mb = append(mb, b1, b2, b3) + + mb, c1 := SplitFirst(mb) + if diff := cmp.Diff(b1.String(), c1.String()); diff != "" { + t.Error(diff) + } + + mb, c2 := SplitFirst(mb) + if diff := cmp.Diff(b2.String(), c2.String()); diff != "" { + t.Error(diff) + } + + mb, c3 := SplitFirst(mb) + if diff := cmp.Diff(b3.String(), c3.String()); diff != "" { + t.Error(diff) + } + + if !mb.IsEmpty() { + t.Error("expect empty buffer, but got ", mb.String()) + } +} + +func TestMultiBufferReadAllToByte(t *testing.T) { + { + lb := make([]byte, 8*1024) + common.Must2(io.ReadFull(rand.Reader, lb)) + rd := bytes.NewBuffer(lb) + b, err := ReadAllToBytes(rd) + common.Must(err) + + if l := len(b); l != 8*1024 { + t.Error("unexpected length from ReadAllToBytes", l) + } + } + { + const dat = "data/test_MultiBufferReadAllToByte.dat" + f, err := os.Open(dat) + common.Must(err) + + buf2, err := ReadAllToBytes(f) + common.Must(err) + f.Close() + + cnt, err := os.ReadFile(dat) + common.Must(err) + + if d := cmp.Diff(buf2, cnt); d != "" { + t.Error("fail to read from file: ", d) + } + } +} + +func TestMultiBufferCopy(t *testing.T) { + lb := make([]byte, 8*1024) + common.Must2(io.ReadFull(rand.Reader, lb)) + reader := bytes.NewBuffer(lb) + + mb, err := ReadFrom(reader) + common.Must(err) + + lbdst := make([]byte, 8*1024) + mb.Copy(lbdst) + + if d := cmp.Diff(lb, lbdst); d != "" { + t.Error("unexpected different from MultiBufferCopy ", d) + } +} + +func TestSplitFirstBytes(t *testing.T) { + a := New() + common.Must2(a.WriteString("ab")) + b := New() + common.Must2(b.WriteString("bc")) + + mb := MultiBuffer{a, b} + + o := make([]byte, 2) + _, cnt := SplitFirstBytes(mb, o) + if cnt != 2 { + t.Error("unexpected cnt from SplitFirstBytes ", cnt) + } + if d := cmp.Diff(string(o), "ab"); d != "" { + t.Error("unexpected splited result from SplitFirstBytes ", d) + } +} + +func TestCompact(t *testing.T) { + a := New() + common.Must2(a.WriteString("ab")) + b := New() + common.Must2(b.WriteString("bc")) + + mb := MultiBuffer{a, b} + cmb := Compact(mb) + + if w := cmb.String(); w != "abbc" { + t.Error("unexpected Compact result ", w) + } +} + +func TestCompactWithConsumed(t *testing.T) { + // make a consumed buffer (a.Start != 0) + a := New() + for range 8192 { + common.Must2(a.WriteString("a")) + } + a.Read(make([]byte, 2)) + + b := New() + for range 2 { + common.Must2(b.WriteString("b")) + } + + mb := MultiBuffer{a, b} + cmb := Compact(mb) + mbc := &MultiBufferContainer{mb} + mbc.Read(make([]byte, 8190)) + + if w := cmb.String(); w != "bb" { + t.Error("unexpected Compact result ", w) + } +} + +func BenchmarkSplitBytes(b *testing.B) { + var mb MultiBuffer + raw := make([]byte, Size) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + buffer := StackNew() + buffer.Extend(Size) + mb = append(mb, &buffer) + mb, _ = SplitBytes(mb, raw) + } +} diff --git a/subproject/Xray-core-main/common/buf/override.go b/subproject/Xray-core-main/common/buf/override.go new file mode 100644 index 00000000..7b2f1554 --- /dev/null +++ b/subproject/Xray-core-main/common/buf/override.go @@ -0,0 +1,38 @@ +package buf + +import ( + "github.com/xtls/xray-core/common/net" +) + +type EndpointOverrideReader struct { + Reader + Dest net.Address + OriginalDest net.Address +} + +func (r *EndpointOverrideReader) ReadMultiBuffer() (MultiBuffer, error) { + mb, err := r.Reader.ReadMultiBuffer() + if err == nil { + for _, b := range mb { + if b.UDP != nil && b.UDP.Address == r.OriginalDest { + b.UDP.Address = r.Dest + } + } + } + return mb, err +} + +type EndpointOverrideWriter struct { + Writer + Dest net.Address + OriginalDest net.Address +} + +func (w *EndpointOverrideWriter) WriteMultiBuffer(mb MultiBuffer) error { + for _, b := range mb { + if b.UDP != nil && b.UDP.Address == w.Dest { + b.UDP.Address = w.OriginalDest + } + } + return w.Writer.WriteMultiBuffer(mb) +} diff --git a/subproject/Xray-core-main/common/buf/reader.go b/subproject/Xray-core-main/common/buf/reader.go new file mode 100644 index 00000000..33d362d4 --- /dev/null +++ b/subproject/Xray-core-main/common/buf/reader.go @@ -0,0 +1,174 @@ +package buf + +import ( + "io" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/errors" +) + +func readOneUDP(r io.Reader) (*Buffer, error) { + b := New() + for i := 0; i < 64; i++ { + _, err := b.ReadFrom(r) + if !b.IsEmpty() { + return b, nil + } + if err != nil { + b.Release() + return nil, err + } + } + + b.Release() + return nil, errors.New("Reader returns too many empty payloads.") +} + +// ReadBuffer reads a Buffer from the given reader. +func ReadBuffer(r io.Reader) (*Buffer, error) { + b := New() + n, err := b.ReadFrom(r) + if n > 0 { + return b, err + } + b.Release() + return nil, err +} + +// BufferedReader is a Reader that keeps its internal buffer. +type BufferedReader struct { + // Reader is the underlying reader to be read from + Reader Reader + // Buffer is the internal buffer to be read from first + Buffer MultiBuffer + // Splitter is a function to read bytes from MultiBuffer + Splitter func(MultiBuffer, []byte) (MultiBuffer, int) +} + +// BufferedBytes returns the number of bytes that is cached in this reader. +func (r *BufferedReader) BufferedBytes() int32 { + return r.Buffer.Len() +} + +// ReadByte implements io.ByteReader. +func (r *BufferedReader) ReadByte() (byte, error) { + var b [1]byte + _, err := r.Read(b[:]) + return b[0], err +} + +// Read implements io.Reader. It reads from internal buffer first (if available) and then reads from the underlying reader. +func (r *BufferedReader) Read(b []byte) (int, error) { + spliter := r.Splitter + if spliter == nil { + spliter = SplitBytes + } + + if !r.Buffer.IsEmpty() { + buffer, nBytes := spliter(r.Buffer, b) + r.Buffer = buffer + if r.Buffer.IsEmpty() { + r.Buffer = nil + } + return nBytes, nil + } + + mb, err := r.Reader.ReadMultiBuffer() + if err != nil { + return 0, err + } + + mb, nBytes := spliter(mb, b) + if !mb.IsEmpty() { + r.Buffer = mb + } + return nBytes, nil +} + +// ReadMultiBuffer implements Reader. +func (r *BufferedReader) ReadMultiBuffer() (MultiBuffer, error) { + if !r.Buffer.IsEmpty() { + mb := r.Buffer + r.Buffer = nil + return mb, nil + } + + return r.Reader.ReadMultiBuffer() +} + +// ReadAtMost returns a MultiBuffer with at most size. +func (r *BufferedReader) ReadAtMost(size int32) (MultiBuffer, error) { + if r.Buffer.IsEmpty() { + mb, err := r.Reader.ReadMultiBuffer() + if mb.IsEmpty() && err != nil { + return nil, err + } + r.Buffer = mb + } + + rb, mb := SplitSize(r.Buffer, size) + r.Buffer = rb + if r.Buffer.IsEmpty() { + r.Buffer = nil + } + return mb, nil +} + +func (r *BufferedReader) writeToInternal(writer io.Writer) (int64, error) { + mbWriter := NewWriter(writer) + var sc SizeCounter + if r.Buffer != nil { + sc.Size = int64(r.Buffer.Len()) + if err := mbWriter.WriteMultiBuffer(r.Buffer); err != nil { + return 0, err + } + r.Buffer = nil + } + + err := Copy(r.Reader, mbWriter, CountSize(&sc)) + return sc.Size, err +} + +// WriteTo implements io.WriterTo. +func (r *BufferedReader) WriteTo(writer io.Writer) (int64, error) { + nBytes, err := r.writeToInternal(writer) + if errors.Cause(err) == io.EOF { + return nBytes, nil + } + return nBytes, err +} + +// Interrupt implements common.Interruptible. +func (r *BufferedReader) Interrupt() { + common.Interrupt(r.Reader) +} + +// Close implements io.Closer. +func (r *BufferedReader) Close() error { + return common.Close(r.Reader) +} + +// SingleReader is a Reader that read one Buffer every time. +type SingleReader struct { + io.Reader +} + +// ReadMultiBuffer implements Reader. +func (r *SingleReader) ReadMultiBuffer() (MultiBuffer, error) { + b, err := ReadBuffer(r.Reader) + return MultiBuffer{b}, err +} + +// PacketReader is a Reader that read one Buffer every time. +type PacketReader struct { + io.Reader +} + +// ReadMultiBuffer implements Reader. +func (r *PacketReader) ReadMultiBuffer() (MultiBuffer, error) { + b, err := readOneUDP(r.Reader) + if err != nil { + return nil, err + } + return MultiBuffer{b}, nil +} diff --git a/subproject/Xray-core-main/common/buf/reader_test.go b/subproject/Xray-core-main/common/buf/reader_test.go new file mode 100644 index 00000000..d2e95169 --- /dev/null +++ b/subproject/Xray-core-main/common/buf/reader_test.go @@ -0,0 +1,131 @@ +package buf_test + +import ( + "bytes" + "io" + "strings" + "testing" + + "github.com/xtls/xray-core/common" + . "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/transport/pipe" +) + +func TestBytesReaderWriteTo(t *testing.T) { + pReader, pWriter := pipe.New(pipe.WithSizeLimit(1024)) + reader := &BufferedReader{Reader: pReader} + b1 := New() + b1.WriteString("abc") + b2 := New() + b2.WriteString("efg") + common.Must(pWriter.WriteMultiBuffer(MultiBuffer{b1, b2})) + pWriter.Close() + + pReader2, pWriter2 := pipe.New(pipe.WithSizeLimit(1024)) + writer := NewBufferedWriter(pWriter2) + writer.SetBuffered(false) + + nBytes, err := io.Copy(writer, reader) + common.Must(err) + if nBytes != 6 { + t.Error("copy: ", nBytes) + } + + mb, err := pReader2.ReadMultiBuffer() + common.Must(err) + if s := mb.String(); s != "abcefg" { + t.Error("content: ", s) + } +} + +func TestBytesReaderMultiBuffer(t *testing.T) { + pReader, pWriter := pipe.New(pipe.WithSizeLimit(1024)) + reader := &BufferedReader{Reader: pReader} + b1 := New() + b1.WriteString("abc") + b2 := New() + b2.WriteString("efg") + common.Must(pWriter.WriteMultiBuffer(MultiBuffer{b1, b2})) + pWriter.Close() + + mbReader := NewReader(reader) + mb, err := mbReader.ReadMultiBuffer() + common.Must(err) + if s := mb.String(); s != "abcefg" { + t.Error("content: ", s) + } +} + +func TestReadByte(t *testing.T) { + sr := strings.NewReader("abcd") + reader := &BufferedReader{ + Reader: NewReader(sr), + } + b, err := reader.ReadByte() + common.Must(err) + if b != 'a' { + t.Error("unexpected byte: ", b, " want a") + } + if reader.BufferedBytes() != 3 { // 3 bytes left in buffer + t.Error("unexpected buffered Bytes: ", reader.BufferedBytes()) + } + + nBytes, err := reader.WriteTo(DiscardBytes) + common.Must(err) + if nBytes != 3 { + t.Error("unexpect bytes written: ", nBytes) + } +} + +func TestReadBuffer(t *testing.T) { + { + sr := strings.NewReader("abcd") + buf, err := ReadBuffer(sr) + common.Must(err) + + if s := buf.String(); s != "abcd" { + t.Error("unexpected str: ", s, " want abcd") + } + buf.Release() + } +} + +func TestReadAtMost(t *testing.T) { + sr := strings.NewReader("abcd") + reader := &BufferedReader{ + Reader: NewReader(sr), + } + + mb, err := reader.ReadAtMost(3) + common.Must(err) + if s := mb.String(); s != "abc" { + t.Error("unexpected read result: ", s) + } + + nBytes, err := reader.WriteTo(DiscardBytes) + common.Must(err) + if nBytes != 1 { + t.Error("unexpect bytes written: ", nBytes) + } +} + +func TestPacketReader_ReadMultiBuffer(t *testing.T) { + const alpha = "abcefg" + buf := bytes.NewBufferString(alpha) + reader := &PacketReader{buf} + mb, err := reader.ReadMultiBuffer() + common.Must(err) + if s := mb.String(); s != alpha { + t.Error("content: ", s) + } +} + +func TestReaderInterface(t *testing.T) { + _ = (io.Reader)(new(ReadVReader)) + _ = (Reader)(new(ReadVReader)) + + _ = (Reader)(new(BufferedReader)) + _ = (io.Reader)(new(BufferedReader)) + _ = (io.ByteReader)(new(BufferedReader)) + _ = (io.WriterTo)(new(BufferedReader)) +} diff --git a/subproject/Xray-core-main/common/buf/readv_posix.go b/subproject/Xray-core-main/common/buf/readv_posix.go new file mode 100644 index 00000000..e8b3bd7f --- /dev/null +++ b/subproject/Xray-core-main/common/buf/readv_posix.go @@ -0,0 +1,46 @@ +//go:build !windows && !wasm && !illumos && !openbsd +// +build !windows,!wasm,!illumos,!openbsd + +package buf + +import ( + "syscall" + "unsafe" +) + +type posixReader struct { + iovecs []syscall.Iovec +} + +func (r *posixReader) Init(bs []*Buffer) { + iovecs := r.iovecs + if iovecs == nil { + iovecs = make([]syscall.Iovec, 0, len(bs)) + } + for idx, b := range bs { + iovecs = append(iovecs, syscall.Iovec{ + Base: &(b.v[0]), + }) + iovecs[idx].SetLen(int(Size)) + } + r.iovecs = iovecs +} + +func (r *posixReader) Read(fd uintptr) int32 { + n, _, e := syscall.Syscall(syscall.SYS_READV, fd, uintptr(unsafe.Pointer(&r.iovecs[0])), uintptr(len(r.iovecs))) + if e != 0 { + return -1 + } + return int32(n) +} + +func (r *posixReader) Clear() { + for idx := range r.iovecs { + r.iovecs[idx].Base = nil + } + r.iovecs = r.iovecs[:0] +} + +func newMultiReader() multiReader { + return &posixReader{} +} diff --git a/subproject/Xray-core-main/common/buf/readv_reader.go b/subproject/Xray-core-main/common/buf/readv_reader.go new file mode 100644 index 00000000..7d7b3ead --- /dev/null +++ b/subproject/Xray-core-main/common/buf/readv_reader.go @@ -0,0 +1,155 @@ +//go:build !wasm && !openbsd +// +build !wasm,!openbsd + +package buf + +import ( + "io" + "syscall" + + "github.com/xtls/xray-core/common/platform" + "github.com/xtls/xray-core/features/stats" +) + +type allocStrategy struct { + current uint32 +} + +func (s *allocStrategy) Current() uint32 { + return s.current +} + +func (s *allocStrategy) Adjust(n uint32) { + if n >= s.current { + s.current *= 2 + } else { + s.current = n + } + + if s.current > 8 { + s.current = 8 + } + + if s.current == 0 { + s.current = 1 + } +} + +func (s *allocStrategy) Alloc() []*Buffer { + bs := make([]*Buffer, s.current) + for i := range bs { + bs[i] = New() + } + return bs +} + +type multiReader interface { + Init([]*Buffer) + Read(fd uintptr) int32 + Clear() +} + +// ReadVReader is a Reader that uses readv(2) syscall to read data. +type ReadVReader struct { + io.Reader + rawConn syscall.RawConn + mr multiReader + alloc allocStrategy + counter stats.Counter +} + +// NewReadVReader creates a new ReadVReader. +func NewReadVReader(reader io.Reader, rawConn syscall.RawConn, counter stats.Counter) *ReadVReader { + return &ReadVReader{ + Reader: reader, + rawConn: rawConn, + alloc: allocStrategy{ + current: 1, + }, + mr: newMultiReader(), + counter: counter, + } +} + +func (r *ReadVReader) readMulti() (MultiBuffer, error) { + bs := r.alloc.Alloc() + + r.mr.Init(bs) + var nBytes int32 + err := r.rawConn.Read(func(fd uintptr) bool { + n := r.mr.Read(fd) + if n < 0 { + return false + } + + nBytes = n + return true + }) + r.mr.Clear() + + if err != nil { + ReleaseMulti(MultiBuffer(bs)) + return nil, err + } + + if nBytes == 0 { + ReleaseMulti(MultiBuffer(bs)) + return nil, io.EOF + } + + nBuf := 0 + for nBuf < len(bs) { + if nBytes <= 0 { + break + } + end := nBytes + if end > Size { + end = Size + } + bs[nBuf].end = end + nBytes -= end + nBuf++ + } + + for i := nBuf; i < len(bs); i++ { + bs[i].Release() + bs[i] = nil + } + + return MultiBuffer(bs[:nBuf]), nil +} + +// ReadMultiBuffer implements Reader. +func (r *ReadVReader) ReadMultiBuffer() (MultiBuffer, error) { + if r.alloc.Current() == 1 { + b, err := ReadBuffer(r.Reader) + if b.IsFull() { + r.alloc.Adjust(1) + } + if r.counter != nil && b != nil { + r.counter.Add(int64(b.Len())) + } + return MultiBuffer{b}, err + } + + mb, err := r.readMulti() + if r.counter != nil && mb != nil { + r.counter.Add(int64(mb.Len())) + } + if err != nil { + return nil, err + } + r.alloc.Adjust(uint32(len(mb))) + return mb, nil +} + +var useReadv bool + +func init() { + const defaultFlagValue = "NOT_DEFINED_AT_ALL" + value := platform.NewEnvFlag(platform.UseReadV).GetValue(func() string { return defaultFlagValue }) + switch value { + case defaultFlagValue, "auto", "enable": + useReadv = true + } +} diff --git a/subproject/Xray-core-main/common/buf/readv_reader_stub.go b/subproject/Xray-core-main/common/buf/readv_reader_stub.go new file mode 100644 index 00000000..b2be9825 --- /dev/null +++ b/subproject/Xray-core-main/common/buf/readv_reader_stub.go @@ -0,0 +1,17 @@ +//go:build wasm || openbsd +// +build wasm openbsd + +package buf + +import ( + "io" + "syscall" + + "github.com/xtls/xray-core/features/stats" +) + +const useReadv = false + +func NewReadVReader(reader io.Reader, rawConn syscall.RawConn, counter stats.Counter) Reader { + panic("not implemented") +} diff --git a/subproject/Xray-core-main/common/buf/readv_test.go b/subproject/Xray-core-main/common/buf/readv_test.go new file mode 100644 index 00000000..6df4c4e8 --- /dev/null +++ b/subproject/Xray-core-main/common/buf/readv_test.go @@ -0,0 +1,72 @@ +//go:build !wasm && !openbsd +// +build !wasm,!openbsd + +package buf_test + +import ( + "crypto/rand" + "net" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/xtls/xray-core/common" + . "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/testing/servers/tcp" + "golang.org/x/sync/errgroup" +) + +func TestReadvReader(t *testing.T) { + tcpServer := &tcp.Server{ + MsgProcessor: func(b []byte) []byte { + return b + }, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + conn, err := net.Dial("tcp", dest.NetAddr()) + common.Must(err) + defer conn.Close() + + const size = 8192 + data := make([]byte, 8192) + common.Must2(rand.Read(data)) + + var errg errgroup.Group + errg.Go(func() error { + writer := NewWriter(conn) + mb := MergeBytes(nil, data) + + return writer.WriteMultiBuffer(mb) + }) + + defer func() { + if err := errg.Wait(); err != nil { + t.Error(err) + } + }() + + rawConn, err := conn.(*net.TCPConn).SyscallConn() + common.Must(err) + + reader := NewReadVReader(conn, rawConn, nil) + var rmb MultiBuffer + for { + mb, err := reader.ReadMultiBuffer() + if err != nil { + t.Fatal("unexpected error: ", err) + } + rmb, _ = MergeMulti(rmb, mb) + if rmb.Len() == size { + break + } + } + + rdata := make([]byte, size) + SplitBytes(rmb, rdata) + + if r := cmp.Diff(data, rdata); r != "" { + t.Fatal(r) + } +} diff --git a/subproject/Xray-core-main/common/buf/readv_unix.go b/subproject/Xray-core-main/common/buf/readv_unix.go new file mode 100644 index 00000000..f5ac6ad1 --- /dev/null +++ b/subproject/Xray-core-main/common/buf/readv_unix.go @@ -0,0 +1,37 @@ +//go:build illumos +// +build illumos + +package buf + +import "golang.org/x/sys/unix" + +type unixReader struct { + iovs [][]byte +} + +func (r *unixReader) Init(bs []*Buffer) { + iovs := r.iovs + if iovs == nil { + iovs = make([][]byte, 0, len(bs)) + } + for _, b := range bs { + iovs = append(iovs, b.v) + } + r.iovs = iovs +} + +func (r *unixReader) Read(fd uintptr) int32 { + n, e := unix.Readv(int(fd), r.iovs) + if e != nil { + return -1 + } + return int32(n) +} + +func (r *unixReader) Clear() { + r.iovs = r.iovs[:0] +} + +func newMultiReader() multiReader { + return &unixReader{} +} diff --git a/subproject/Xray-core-main/common/buf/readv_windows.go b/subproject/Xray-core-main/common/buf/readv_windows.go new file mode 100644 index 00000000..a812ee04 --- /dev/null +++ b/subproject/Xray-core-main/common/buf/readv_windows.go @@ -0,0 +1,39 @@ +package buf + +import ( + "syscall" +) + +type windowsReader struct { + bufs []syscall.WSABuf +} + +func (r *windowsReader) Init(bs []*Buffer) { + if r.bufs == nil { + r.bufs = make([]syscall.WSABuf, 0, len(bs)) + } + for _, b := range bs { + r.bufs = append(r.bufs, syscall.WSABuf{Len: uint32(Size), Buf: &b.v[0]}) + } +} + +func (r *windowsReader) Clear() { + for idx := range r.bufs { + r.bufs[idx].Buf = nil + } + r.bufs = r.bufs[:0] +} + +func (r *windowsReader) Read(fd uintptr) int32 { + var nBytes uint32 + var flags uint32 + err := syscall.WSARecv(syscall.Handle(fd), &r.bufs[0], uint32(len(r.bufs)), &nBytes, &flags, nil, nil) + if err != nil { + return -1 + } + return int32(nBytes) +} + +func newMultiReader() multiReader { + return new(windowsReader) +} diff --git a/subproject/Xray-core-main/common/buf/writer.go b/subproject/Xray-core-main/common/buf/writer.go new file mode 100644 index 00000000..6ff8a6b6 --- /dev/null +++ b/subproject/Xray-core-main/common/buf/writer.go @@ -0,0 +1,284 @@ +package buf + +import ( + "io" + "net" + "sync" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/features/stats" +) + +// BufferToBytesWriter is a Writer that writes alloc.Buffer into underlying writer. +type BufferToBytesWriter struct { + io.Writer + + counter stats.Counter + cache [][]byte +} + +// WriteMultiBuffer implements Writer. This method takes ownership of the given buffer. +func (w *BufferToBytesWriter) WriteMultiBuffer(mb MultiBuffer) error { + defer ReleaseMulti(mb) + + size := mb.Len() + if size == 0 { + return nil + } + + if len(mb) == 1 { + return WriteAllBytes(w.Writer, mb[0].Bytes(), w.counter) + } + + if cap(w.cache) < len(mb) { + w.cache = make([][]byte, 0, len(mb)) + } + + bs := w.cache + for _, b := range mb { + bs = append(bs, b.Bytes()) + } + + defer func() { + for idx := range bs { + bs[idx] = nil + } + }() + + nb := net.Buffers(bs) + wc := int64(0) + defer func() { + if w.counter != nil { + w.counter.Add(wc) + } + }() + for size > 0 { + n, err := nb.WriteTo(w.Writer) + wc += n + if err != nil { + return err + } + size -= int32(n) + } + + return nil +} + +// ReadFrom implements io.ReaderFrom. +func (w *BufferToBytesWriter) ReadFrom(reader io.Reader) (int64, error) { + var sc SizeCounter + err := Copy(NewReader(reader), w, CountSize(&sc)) + return sc.Size, err +} + +// BufferedWriter is a Writer with internal buffer. +type BufferedWriter struct { + sync.Mutex + writer Writer + buffer *Buffer + buffered bool + flushNext bool +} + +// NewBufferedWriter creates a new BufferedWriter. +func NewBufferedWriter(writer Writer) *BufferedWriter { + return &BufferedWriter{ + writer: writer, + buffer: New(), + buffered: true, + } +} + +// WriteByte implements io.ByteWriter. +func (w *BufferedWriter) WriteByte(c byte) error { + return common.Error2(w.Write([]byte{c})) +} + +// Write implements io.Writer. +func (w *BufferedWriter) Write(b []byte) (int, error) { + if len(b) == 0 { + return 0, nil + } + + w.Lock() + defer w.Unlock() + + if !w.buffered { + if writer, ok := w.writer.(io.Writer); ok { + return writer.Write(b) + } + } + + totalBytes := 0 + for len(b) > 0 { + if w.buffer == nil { + w.buffer = New() + } + + nBytes, err := w.buffer.Write(b) + totalBytes += nBytes + if err != nil { + return totalBytes, err + } + if !w.buffered || w.buffer.IsFull() { + if err := w.flushInternal(); err != nil { + return totalBytes, err + } + } + b = b[nBytes:] + } + + return totalBytes, nil +} + +// WriteMultiBuffer implements Writer. It takes ownership of the given MultiBuffer. +func (w *BufferedWriter) WriteMultiBuffer(b MultiBuffer) error { + if b.IsEmpty() { + return nil + } + + w.Lock() + defer w.Unlock() + + if !w.buffered { + return w.writer.WriteMultiBuffer(b) + } + + reader := MultiBufferContainer{ + MultiBuffer: b, + } + defer reader.Close() + + for !reader.MultiBuffer.IsEmpty() { + if w.buffer == nil { + w.buffer = New() + } + common.Must2(w.buffer.ReadFrom(&reader)) + if w.buffer.IsFull() { + if err := w.flushInternal(); err != nil { + return err + } + } + } + + if w.flushNext { + w.buffered = false + w.flushNext = false + return w.flushInternal() + } + + return nil +} + +// Flush flushes buffered content into underlying writer. +func (w *BufferedWriter) Flush() error { + w.Lock() + defer w.Unlock() + + return w.flushInternal() +} + +func (w *BufferedWriter) flushInternal() error { + if w.buffer.IsEmpty() { + return nil + } + + b := w.buffer + w.buffer = nil + + if writer, ok := w.writer.(io.Writer); ok { + err := WriteAllBytes(writer, b.Bytes(), nil) + b.Release() + return err + } + + return w.writer.WriteMultiBuffer(MultiBuffer{b}) +} + +// SetBuffered sets whether the internal buffer is used. If set to false, Flush() will be called to clear the buffer. +func (w *BufferedWriter) SetBuffered(f bool) error { + w.Lock() + defer w.Unlock() + + w.buffered = f + if !f { + return w.flushInternal() + } + return nil +} + +// SetFlushNext will wait the next WriteMultiBuffer to flush and set buffered = false +func (w *BufferedWriter) SetFlushNext() { + w.Lock() + defer w.Unlock() + w.flushNext = true +} + +// ReadFrom implements io.ReaderFrom. +func (w *BufferedWriter) ReadFrom(reader io.Reader) (int64, error) { + if err := w.SetBuffered(false); err != nil { + return 0, err + } + + var sc SizeCounter + err := Copy(NewReader(reader), w, CountSize(&sc)) + return sc.Size, err +} + +// Close implements io.Closable. +func (w *BufferedWriter) Close() error { + if err := w.Flush(); err != nil { + return err + } + return common.Close(w.writer) +} + +// SequentialWriter is a Writer that writes MultiBuffer sequentially into the underlying io.Writer. +type SequentialWriter struct { + io.Writer +} + +// WriteMultiBuffer implements Writer. +func (w *SequentialWriter) WriteMultiBuffer(mb MultiBuffer) error { + mb, err := WriteMultiBuffer(w.Writer, mb) + ReleaseMulti(mb) + return err +} + +type noOpWriter byte + +func (noOpWriter) WriteMultiBuffer(b MultiBuffer) error { + ReleaseMulti(b) + return nil +} + +func (noOpWriter) Write(b []byte) (int, error) { + return len(b), nil +} + +func (noOpWriter) ReadFrom(reader io.Reader) (int64, error) { + b := New() + defer b.Release() + + totalBytes := int64(0) + for { + b.Clear() + _, err := b.ReadFrom(reader) + totalBytes += int64(b.Len()) + if err != nil { + if errors.Cause(err) == io.EOF { + return totalBytes, nil + } + return totalBytes, err + } + } +} + +var ( + // Discard is a Writer that swallows all contents written in. + Discard Writer = noOpWriter(0) + + // DiscardBytes is an io.Writer that swallows all contents written in. + DiscardBytes io.Writer = noOpWriter(0) +) diff --git a/subproject/Xray-core-main/common/buf/writer_test.go b/subproject/Xray-core-main/common/buf/writer_test.go new file mode 100644 index 00000000..20c3579a --- /dev/null +++ b/subproject/Xray-core-main/common/buf/writer_test.go @@ -0,0 +1,97 @@ +package buf_test + +import ( + "bufio" + "bytes" + "crypto/rand" + "io" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/xtls/xray-core/common" + . "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/transport/pipe" +) + +func TestWriter(t *testing.T) { + lb := New() + common.Must2(lb.ReadFrom(rand.Reader)) + + expectedBytes := append([]byte(nil), lb.Bytes()...) + + writeBuffer := bytes.NewBuffer(make([]byte, 0, 1024*1024)) + + writer := NewBufferedWriter(NewWriter(writeBuffer)) + writer.SetBuffered(false) + common.Must(writer.WriteMultiBuffer(MultiBuffer{lb})) + common.Must(writer.Flush()) + + if r := cmp.Diff(expectedBytes, writeBuffer.Bytes()); r != "" { + t.Error(r) + } +} + +func TestBytesWriterReadFrom(t *testing.T) { + const size = 50000 + pReader, pWriter := pipe.New(pipe.WithSizeLimit(size)) + reader := bufio.NewReader(io.LimitReader(rand.Reader, size)) + writer := NewBufferedWriter(pWriter) + writer.SetBuffered(false) + nBytes, err := reader.WriteTo(writer) + if nBytes != size { + t.Fatal("unexpected size of bytes written: ", nBytes) + } + if err != nil { + t.Fatal("expect success, but actually error: ", err.Error()) + } + + mb, err := pReader.ReadMultiBuffer() + common.Must(err) + if mb.Len() != size { + t.Fatal("unexpected size read: ", mb.Len()) + } +} + +func TestDiscardBytes(t *testing.T) { + b := New() + common.Must2(b.ReadFullFrom(rand.Reader, Size)) + + nBytes, err := io.Copy(DiscardBytes, b) + common.Must(err) + if nBytes != Size { + t.Error("copy size: ", nBytes) + } +} + +func TestDiscardBytesMultiBuffer(t *testing.T) { + const size = 10240*1024 + 1 + buffer := bytes.NewBuffer(make([]byte, 0, size)) + common.Must2(buffer.ReadFrom(io.LimitReader(rand.Reader, size))) + + r := NewReader(buffer) + nBytes, err := io.Copy(DiscardBytes, &BufferedReader{Reader: r}) + common.Must(err) + if nBytes != size { + t.Error("copy size: ", nBytes) + } +} + +func TestWriterInterface(t *testing.T) { + { + var writer interface{} = (*BufferToBytesWriter)(nil) + switch writer.(type) { + case Writer, io.Writer, io.ReaderFrom: + default: + t.Error("BufferToBytesWriter is not Writer, io.Writer or io.ReaderFrom") + } + } + + { + var writer interface{} = (*BufferedWriter)(nil) + switch writer.(type) { + case Writer, io.Writer, io.ReaderFrom, io.ByteWriter: + default: + t.Error("BufferedWriter is not Writer, io.Writer, io.ReaderFrom or io.ByteWriter") + } + } +} diff --git a/subproject/Xray-core-main/common/bytespool/pool.go b/subproject/Xray-core-main/common/bytespool/pool.go new file mode 100644 index 00000000..6f632d52 --- /dev/null +++ b/subproject/Xray-core-main/common/bytespool/pool.go @@ -0,0 +1,72 @@ +package bytespool + +import "sync" + +func createAllocFunc(size int32) func() interface{} { + return func() interface{} { + return make([]byte, size) + } +} + +// The following parameters controls the size of buffer pools. +// There are numPools pools. Starting from 2k size, the size of each pool is sizeMulti of the previous one. +// Package buf is guaranteed to not use buffers larger than the largest pool. +// Other packets may use larger buffers. +const ( + numPools = 4 + sizeMulti = 4 +) + +var ( + pool [numPools]sync.Pool + poolSize [numPools]int32 +) + +func init() { + size := int32(2048) + for i := 0; i < numPools; i++ { + pool[i] = sync.Pool{ + New: createAllocFunc(size), + } + poolSize[i] = size + size *= sizeMulti + } +} + +// GetPool returns a sync.Pool that generates bytes array with at least the given size. +// It may return nil if no such pool exists. +// +// xray:api:stable +func GetPool(size int32) *sync.Pool { + for idx, ps := range poolSize { + if size <= ps { + return &pool[idx] + } + } + return nil +} + +// Alloc returns a byte slice with at least the given size. Minimum size of returned slice is 2048. +// +// xray:api:stable +func Alloc(size int32) []byte { + pool := GetPool(size) + if pool != nil { + return pool.Get().([]byte) + } + return make([]byte, size) +} + +// Free puts a byte slice into the internal pool. +// +// xray:api:stable +func Free(b []byte) { + size := int32(cap(b)) + b = b[0:cap(b)] + for i := numPools - 1; i >= 0; i-- { + if size >= poolSize[i] { + pool[i].Put(b) + return + } + } +} diff --git a/subproject/Xray-core-main/common/cache/lru.go b/subproject/Xray-core-main/common/cache/lru.go new file mode 100644 index 00000000..9eb760d6 --- /dev/null +++ b/subproject/Xray-core-main/common/cache/lru.go @@ -0,0 +1,89 @@ +package cache + +import ( + "container/list" + "sync" +) + +// Lru simple, fast lru cache implementation +type Lru interface { + Get(key interface{}) (value interface{}, ok bool) + GetKeyFromValue(value interface{}) (key interface{}, ok bool) + PeekKeyFromValue(value interface{}) (key interface{}, ok bool) // Peek means check but NOT bring to top + Put(key, value interface{}) +} + +type lru struct { + capacity int + doubleLinkedlist *list.List + keyToElement *sync.Map + valueToElement *sync.Map + mu *sync.Mutex +} + +type lruElement struct { + key interface{} + value interface{} +} + +// NewLru initializes a lru cache +func NewLru(cap int) Lru { + return &lru{ + capacity: cap, + doubleLinkedlist: list.New(), + keyToElement: new(sync.Map), + valueToElement: new(sync.Map), + mu: new(sync.Mutex), + } +} + +func (l *lru) Get(key interface{}) (value interface{}, ok bool) { + l.mu.Lock() + defer l.mu.Unlock() + if v, ok := l.keyToElement.Load(key); ok { + element := v.(*list.Element) + l.doubleLinkedlist.MoveToFront(element) + return element.Value.(*lruElement).value, true + } + return nil, false +} + +func (l *lru) GetKeyFromValue(value interface{}) (key interface{}, ok bool) { + l.mu.Lock() + defer l.mu.Unlock() + if k, ok := l.valueToElement.Load(value); ok { + element := k.(*list.Element) + l.doubleLinkedlist.MoveToFront(element) + return element.Value.(*lruElement).key, true + } + return nil, false +} + +func (l *lru) PeekKeyFromValue(value interface{}) (key interface{}, ok bool) { + if k, ok := l.valueToElement.Load(value); ok { + element := k.(*list.Element) + return element.Value.(*lruElement).key, true + } + return nil, false +} + +func (l *lru) Put(key, value interface{}) { + l.mu.Lock() + e := &lruElement{key, value} + if v, ok := l.keyToElement.Load(key); ok { + element := v.(*list.Element) + element.Value = e + l.doubleLinkedlist.MoveToFront(element) + } else { + element := l.doubleLinkedlist.PushFront(e) + l.keyToElement.Store(key, element) + l.valueToElement.Store(value, element) + if l.doubleLinkedlist.Len() > l.capacity { + toBeRemove := l.doubleLinkedlist.Back() + l.doubleLinkedlist.Remove(toBeRemove) + l.keyToElement.Delete(toBeRemove.Value.(*lruElement).key) + l.valueToElement.Delete(toBeRemove.Value.(*lruElement).value) + } + } + l.mu.Unlock() +} diff --git a/subproject/Xray-core-main/common/cache/lru_test.go b/subproject/Xray-core-main/common/cache/lru_test.go new file mode 100644 index 00000000..6bbae4fd --- /dev/null +++ b/subproject/Xray-core-main/common/cache/lru_test.go @@ -0,0 +1,86 @@ +package cache_test + +import ( + "testing" + + . "github.com/xtls/xray-core/common/cache" +) + +func TestLruReplaceValue(t *testing.T) { + lru := NewLru(2) + lru.Put(2, 6) + lru.Put(1, 5) + lru.Put(1, 2) + v, _ := lru.Get(1) + if v != 2 { + t.Error("should get 2", v) + } + v, _ = lru.Get(2) + if v != 6 { + t.Error("should get 6", v) + } +} + +func TestLruRemoveOld(t *testing.T) { + lru := NewLru(2) + v, ok := lru.Get(2) + if ok { + t.Error("should get nil", v) + } + lru.Put(1, 1) + lru.Put(2, 2) + v, _ = lru.Get(1) + if v != 1 { + t.Error("should get 1", v) + } + lru.Put(3, 3) + v, ok = lru.Get(2) + if ok { + t.Error("should get nil", v) + } + lru.Put(4, 4) + v, ok = lru.Get(1) + if ok { + t.Error("should get nil", v) + } + v, _ = lru.Get(3) + if v != 3 { + t.Error("should get 3", v) + } + v, _ = lru.Get(4) + if v != 4 { + t.Error("should get 4", v) + } +} + +func TestGetKeyFromValue(t *testing.T) { + lru := NewLru(2) + lru.Put(3, 3) + lru.Put(2, 2) + lru.GetKeyFromValue(3) + lru.Put(1, 1) + v, ok := lru.GetKeyFromValue(2) + if ok { + t.Error("should get nil", v) + } + v, _ = lru.GetKeyFromValue(3) + if v != 3 { + t.Error("should get 3", v) + } +} + +func TestPeekKeyFromValue(t *testing.T) { + lru := NewLru(2) + lru.Put(3, 3) + lru.Put(2, 2) + lru.PeekKeyFromValue(3) + lru.Put(1, 1) + v, ok := lru.PeekKeyFromValue(3) + if ok { + t.Error("should get nil", v) + } + v, _ = lru.PeekKeyFromValue(2) + if v != 2 { + t.Error("should get 2", v) + } +} diff --git a/subproject/Xray-core-main/common/cmdarg/cmdarg.go b/subproject/Xray-core-main/common/cmdarg/cmdarg.go new file mode 100644 index 00000000..f524eb37 --- /dev/null +++ b/subproject/Xray-core-main/common/cmdarg/cmdarg.go @@ -0,0 +1,16 @@ +package cmdarg + +import "strings" + +// Arg is used by flag to accept multiple argument. +type Arg []string + +func (c *Arg) String() string { + return strings.Join([]string(*c), " ") +} + +// Set is the method flag package calls +func (c *Arg) Set(value string) error { + *c = append(*c, value) + return nil +} diff --git a/subproject/Xray-core-main/common/common.go b/subproject/Xray-core-main/common/common.go new file mode 100644 index 00000000..f0134243 --- /dev/null +++ b/subproject/Xray-core-main/common/common.go @@ -0,0 +1,167 @@ +// Package common contains common utilities that are shared among other packages. +// See each sub-package for detail. +package common + +import ( + "fmt" + "go/build" + "os" + "path/filepath" + "reflect" + "strings" + + "github.com/xtls/xray-core/common/errors" +) + +// ErrNoClue is for the situation that existing information is not enough to make a decision. For example, Router may return this error when there is no suitable route. +var ErrNoClue = errors.New("not enough information for making a decision") + +// Must panics if err is not nil. +func Must(err error) { + if err != nil { + panic(err) + } +} + +// Must2 panics if the second parameter is not nil, otherwise returns the first parameter. +// This is useful when function returned "sth, err" and avoid many "if err != nil" +// Internal usage only, if user input can cause err, it must be handled +func Must2[T any](v T, err error) T { + Must(err) + return v +} + +// Error2 returns the err from the 2nd parameter. +func Error2(v interface{}, err error) error { + return err +} + +// envFile returns the name of the Go environment configuration file. +// Copy from https://github.com/golang/go/blob/c4f2a9788a7be04daf931ac54382fbe2cb754938/src/cmd/go/internal/cfg/cfg.go#L150-L166 +func envFile() (string, error) { + if file := os.Getenv("GOENV"); file != "" { + if file == "off" { + return "", errors.New("GOENV=off") + } + return file, nil + } + dir, err := os.UserConfigDir() + if err != nil { + return "", err + } + if dir == "" { + return "", errors.New("missing user-config dir") + } + return filepath.Join(dir, "go", "env"), nil +} + +// GetRuntimeEnv returns the value of runtime environment variable, +// that is set by running following command: `go env -w key=value`. +func GetRuntimeEnv(key string) (string, error) { + file, err := envFile() + if err != nil { + return "", err + } + if file == "" { + return "", errors.New("missing runtime env file") + } + var data []byte + var runtimeEnv string + data, readErr := os.ReadFile(file) + if readErr != nil { + return "", readErr + } + envStrings := strings.Split(string(data), "\n") + for _, envItem := range envStrings { + envItem = strings.TrimSuffix(envItem, "\r") + envKeyValue := strings.Split(envItem, "=") + if strings.EqualFold(strings.TrimSpace(envKeyValue[0]), key) { + runtimeEnv = strings.TrimSpace(envKeyValue[1]) + } + } + return runtimeEnv, nil +} + +// GetGOBIN returns GOBIN environment variable as a string. It will NOT be empty. +func GetGOBIN() string { + // The one set by user explicitly by `export GOBIN=/path` or `env GOBIN=/path command` + GOBIN := os.Getenv("GOBIN") + if GOBIN == "" { + var err error + // The one set by user by running `go env -w GOBIN=/path` + GOBIN, err = GetRuntimeEnv("GOBIN") + if err != nil { + // The default one that Golang uses + return filepath.Join(build.Default.GOPATH, "bin") + } + if GOBIN == "" { + return filepath.Join(build.Default.GOPATH, "bin") + } + return GOBIN + } + return GOBIN +} + +// GetGOPATH returns GOPATH environment variable as a string. It will NOT be empty. +func GetGOPATH() string { + // The one set by user explicitly by `export GOPATH=/path` or `env GOPATH=/path command` + GOPATH := os.Getenv("GOPATH") + if GOPATH == "" { + var err error + // The one set by user by running `go env -w GOPATH=/path` + GOPATH, err = GetRuntimeEnv("GOPATH") + if err != nil { + // The default one that Golang uses + return build.Default.GOPATH + } + if GOPATH == "" { + return build.Default.GOPATH + } + return GOPATH + } + return GOPATH +} + +// GetModuleName returns the value of module in `go.mod` file. +func GetModuleName(pathToProjectRoot string) (string, error) { + var moduleName string + loopPath := pathToProjectRoot + for { + if idx := strings.LastIndex(loopPath, string(filepath.Separator)); idx >= 0 { + gomodPath := filepath.Join(loopPath, "go.mod") + gomodBytes, err := os.ReadFile(gomodPath) + if err != nil { + loopPath = loopPath[:idx] + continue + } + + gomodContent := string(gomodBytes) + moduleIdx := strings.Index(gomodContent, "module ") + newLineIdx := strings.Index(gomodContent, "\n") + + if moduleIdx >= 0 { + if newLineIdx >= 0 { + moduleName = strings.TrimSpace(gomodContent[moduleIdx+6 : newLineIdx]) + moduleName = strings.TrimSuffix(moduleName, "\r") + } else { + moduleName = strings.TrimSpace(gomodContent[moduleIdx+6:]) + } + return moduleName, nil + } + return "", fmt.Errorf("can not get module path in `%s`", gomodPath) + } + break + } + return moduleName, fmt.Errorf("no `go.mod` file in every parent directory of `%s`", pathToProjectRoot) +} + +// CloseIfExists call obj.Close() if obj is not nil. +func CloseIfExists(obj any) error { + if obj != nil { + v := reflect.ValueOf(obj) + if !v.IsNil() { + return Close(obj) + } + } + return nil +} diff --git a/subproject/Xray-core-main/common/common_test.go b/subproject/Xray-core-main/common/common_test.go new file mode 100644 index 00000000..d661788c --- /dev/null +++ b/subproject/Xray-core-main/common/common_test.go @@ -0,0 +1,44 @@ +package common_test + +import ( + "errors" + "testing" + + . "github.com/xtls/xray-core/common" +) + +func TestMust(t *testing.T) { + hasPanic := func(f func()) (ret bool) { + defer func() { + if r := recover(); r != nil { + ret = true + } + }() + f() + return false + } + + testCases := []struct { + Input func() + Panic bool + }{ + { + Panic: true, + Input: func() { Must(func() error { return errors.New("test error") }()) }, + }, + { + Panic: true, + Input: func() { Must2(func() (int, error) { return 0, errors.New("test error") }()) }, + }, + { + Panic: false, + Input: func() { Must(func() error { return nil }()) }, + }, + } + + for idx, test := range testCases { + if hasPanic(test.Input) != test.Panic { + t.Error("test case #", idx, " expect panic ", test.Panic, " but actually not") + } + } +} diff --git a/subproject/Xray-core-main/common/crypto/aes.go b/subproject/Xray-core-main/common/crypto/aes.go new file mode 100644 index 00000000..bbc974d9 --- /dev/null +++ b/subproject/Xray-core-main/common/crypto/aes.go @@ -0,0 +1,38 @@ +package crypto + +import ( + "crypto/aes" + "crypto/cipher" + + "github.com/xtls/xray-core/common" +) + +// NewAesDecryptionStream creates a new AES encryption stream based on given key and IV. +// Caller must ensure the length of key and IV is either 16, 24 or 32 bytes. +func NewAesDecryptionStream(key []byte, iv []byte) cipher.Stream { + return NewAesStreamMethod(key, iv, cipher.NewCFBDecrypter) +} + +// NewAesEncryptionStream creates a new AES description stream based on given key and IV. +// Caller must ensure the length of key and IV is either 16, 24 or 32 bytes. +func NewAesEncryptionStream(key []byte, iv []byte) cipher.Stream { + return NewAesStreamMethod(key, iv, cipher.NewCFBEncrypter) +} + +func NewAesStreamMethod(key []byte, iv []byte, f func(cipher.Block, []byte) cipher.Stream) cipher.Stream { + aesBlock, err := aes.NewCipher(key) + common.Must(err) + return f(aesBlock, iv) +} + +// NewAesCTRStream creates a stream cipher based on AES-CTR. +func NewAesCTRStream(key []byte, iv []byte) cipher.Stream { + return NewAesStreamMethod(key, iv, cipher.NewCTR) +} + +// NewAesGcm creates a AEAD cipher based on AES-GCM. +func NewAesGcm(key []byte) cipher.AEAD { + block := common.Must2(aes.NewCipher(key)) + aead := common.Must2(cipher.NewGCM(block)) + return aead +} diff --git a/subproject/Xray-core-main/common/crypto/auth.go b/subproject/Xray-core-main/common/crypto/auth.go new file mode 100644 index 00000000..6259e2a7 --- /dev/null +++ b/subproject/Xray-core-main/common/crypto/auth.go @@ -0,0 +1,350 @@ +package crypto + +import ( + "crypto/cipher" + "crypto/rand" + "io" + + "github.com/xtls/xray-core/common" + "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/protocol" +) + +type BytesGenerator func() []byte + +func GenerateEmptyBytes() BytesGenerator { + var b [1]byte + return func() []byte { + return b[:0] + } +} + +func GenerateStaticBytes(content []byte) BytesGenerator { + return func() []byte { + return content + } +} + +func GenerateIncreasingNonce(nonce []byte) BytesGenerator { + c := append([]byte(nil), nonce...) + return func() []byte { + for i := range c { + c[i]++ + if c[i] != 0 { + break + } + } + return c + } +} + +func GenerateAEADNonceWithSize(nonceSize int) BytesGenerator { + c := make([]byte, nonceSize) + for i := 0; i < nonceSize; i++ { + c[i] = 0xFF + } + return GenerateIncreasingNonce(c) +} + +type Authenticator interface { + NonceSize() int + Overhead() int + Open(dst, cipherText []byte) ([]byte, error) + Seal(dst, plainText []byte) ([]byte, error) +} + +type AEADAuthenticator struct { + cipher.AEAD + NonceGenerator BytesGenerator + AdditionalDataGenerator BytesGenerator +} + +func (v *AEADAuthenticator) Open(dst, cipherText []byte) ([]byte, error) { + iv := v.NonceGenerator() + if len(iv) != v.AEAD.NonceSize() { + return nil, errors.New("invalid AEAD nonce size: ", len(iv)) + } + + var additionalData []byte + if v.AdditionalDataGenerator != nil { + additionalData = v.AdditionalDataGenerator() + } + return v.AEAD.Open(dst, iv, cipherText, additionalData) +} + +func (v *AEADAuthenticator) Seal(dst, plainText []byte) ([]byte, error) { + iv := v.NonceGenerator() + if len(iv) != v.AEAD.NonceSize() { + return nil, errors.New("invalid AEAD nonce size: ", len(iv)) + } + + var additionalData []byte + if v.AdditionalDataGenerator != nil { + additionalData = v.AdditionalDataGenerator() + } + return v.AEAD.Seal(dst, iv, plainText, additionalData), nil +} + +type AuthenticationReader struct { + auth Authenticator + reader *buf.BufferedReader + sizeParser ChunkSizeDecoder + sizeBytes []byte + transferType protocol.TransferType + padding PaddingLengthGenerator + size uint16 + paddingLen uint16 + hasSize bool + done bool +} + +func NewAuthenticationReader(auth Authenticator, sizeParser ChunkSizeDecoder, reader io.Reader, transferType protocol.TransferType, paddingLen PaddingLengthGenerator) *AuthenticationReader { + r := &AuthenticationReader{ + auth: auth, + sizeParser: sizeParser, + transferType: transferType, + padding: paddingLen, + sizeBytes: make([]byte, sizeParser.SizeBytes()), + } + if breader, ok := reader.(*buf.BufferedReader); ok { + r.reader = breader + } else { + r.reader = &buf.BufferedReader{Reader: buf.NewReader(reader)} + } + return r +} + +func (r *AuthenticationReader) readSize() (uint16, uint16, error) { + if r.hasSize { + r.hasSize = false + return r.size, r.paddingLen, nil + } + if _, err := io.ReadFull(r.reader, r.sizeBytes); err != nil { + return 0, 0, err + } + var padding uint16 + if r.padding != nil { + padding = r.padding.NextPaddingLen() + } + size, err := r.sizeParser.Decode(r.sizeBytes) + return size, padding, err +} + +var errSoft = errors.New("waiting for more data") + +func (r *AuthenticationReader) readBuffer(size int32, padding int32) (*buf.Buffer, error) { + b := buf.New() + if _, err := b.ReadFullFrom(r.reader, size); err != nil { + b.Release() + return nil, err + } + size -= padding + rb, err := r.auth.Open(b.BytesTo(0), b.BytesTo(size)) + if err != nil { + b.Release() + return nil, err + } + b.Resize(0, int32(len(rb))) + return b, nil +} + +func (r *AuthenticationReader) readInternal(soft bool, mb *buf.MultiBuffer) error { + if soft && r.reader.BufferedBytes() < r.sizeParser.SizeBytes() { + return errSoft + } + + if r.done { + return io.EOF + } + + size, padding, err := r.readSize() + if err != nil { + return err + } + + if size == uint16(r.auth.Overhead())+padding { + r.done = true + return io.EOF + } + + if soft && int32(size) > r.reader.BufferedBytes() { + r.size = size + r.paddingLen = padding + r.hasSize = true + return errSoft + } + + if size <= buf.Size { + b, err := r.readBuffer(int32(size), int32(padding)) + if err != nil { + return err + } + *mb = append(*mb, b) + return nil + } + + payload := bytespool.Alloc(int32(size)) + defer bytespool.Free(payload) + + if _, err := io.ReadFull(r.reader, payload[:size]); err != nil { + return err + } + + size -= padding + + rb, err := r.auth.Open(payload[:0], payload[:size]) + if err != nil { + return err + } + + *mb = buf.MergeBytes(*mb, rb) + return nil +} + +func (r *AuthenticationReader) ReadMultiBuffer() (buf.MultiBuffer, error) { + const readSize = 16 + mb := make(buf.MultiBuffer, 0, readSize) + if err := r.readInternal(false, &mb); err != nil { + buf.ReleaseMulti(mb) + return nil, err + } + + for i := 1; i < readSize; i++ { + err := r.readInternal(true, &mb) + if err == errSoft || err == io.EOF { + break + } + if err != nil { + buf.ReleaseMulti(mb) + return nil, err + } + } + + return mb, nil +} + +type AuthenticationWriter struct { + auth Authenticator + writer buf.Writer + sizeParser ChunkSizeEncoder + transferType protocol.TransferType + padding PaddingLengthGenerator +} + +func NewAuthenticationWriter(auth Authenticator, sizeParser ChunkSizeEncoder, writer io.Writer, transferType protocol.TransferType, padding PaddingLengthGenerator) *AuthenticationWriter { + w := &AuthenticationWriter{ + auth: auth, + writer: buf.NewWriter(writer), + sizeParser: sizeParser, + transferType: transferType, + } + if padding != nil { + w.padding = padding + } + return w +} + +func (w *AuthenticationWriter) seal(b []byte) (*buf.Buffer, error) { + encryptedSize := int32(len(b) + w.auth.Overhead()) + var paddingSize int32 + if w.padding != nil { + paddingSize = int32(w.padding.NextPaddingLen()) + } + + sizeBytes := w.sizeParser.SizeBytes() + totalSize := sizeBytes + encryptedSize + paddingSize + if totalSize > buf.Size { + return nil, errors.New("size too large: ", totalSize) + } + + eb := buf.New() + w.sizeParser.Encode(uint16(encryptedSize+paddingSize), eb.Extend(sizeBytes)) + if _, err := w.auth.Seal(eb.Extend(encryptedSize)[:0], b); err != nil { + eb.Release() + return nil, err + } + if paddingSize > 0 { + // These paddings will send in clear text. + // To avoid leakage of PRNG internal state, a cryptographically secure PRNG should be used. + paddingBytes := eb.Extend(paddingSize) + common.Must2(rand.Read(paddingBytes)) + } + + return eb, nil +} + +func (w *AuthenticationWriter) writeStream(mb buf.MultiBuffer) error { + defer buf.ReleaseMulti(mb) + + var maxPadding int32 + if w.padding != nil { + maxPadding = int32(w.padding.MaxPaddingLen()) + } + + payloadSize := buf.Size - int32(w.auth.Overhead()) - w.sizeParser.SizeBytes() - maxPadding + mb2Write := make(buf.MultiBuffer, 0, len(mb)+10) + + temp := buf.New() + defer temp.Release() + + rawBytes := temp.Extend(payloadSize) + + for { + nb, nBytes := buf.SplitBytes(mb, rawBytes) + mb = nb + + eb, err := w.seal(rawBytes[:nBytes]) + if err != nil { + buf.ReleaseMulti(mb2Write) + return err + } + mb2Write = append(mb2Write, eb) + if mb.IsEmpty() { + break + } + } + + return w.writer.WriteMultiBuffer(mb2Write) +} + +func (w *AuthenticationWriter) writePacket(mb buf.MultiBuffer) error { + defer buf.ReleaseMulti(mb) + + mb2Write := make(buf.MultiBuffer, 0, len(mb)+1) + + for _, b := range mb { + if b.IsEmpty() { + continue + } + + eb, err := w.seal(b.Bytes()) + if err != nil { + continue + } + + mb2Write = append(mb2Write, eb) + } + + if mb2Write.IsEmpty() { + return nil + } + + return w.writer.WriteMultiBuffer(mb2Write) +} + +// WriteMultiBuffer implements buf.Writer. +func (w *AuthenticationWriter) WriteMultiBuffer(mb buf.MultiBuffer) error { + if mb.IsEmpty() { + eb, err := w.seal([]byte{}) + common.Must(err) + return w.writer.WriteMultiBuffer(buf.MultiBuffer{eb}) + } + + if w.transferType == protocol.TransferTypeStream { + return w.writeStream(mb) + } + + return w.writePacket(mb) +} diff --git a/subproject/Xray-core-main/common/crypto/auth_test.go b/subproject/Xray-core-main/common/crypto/auth_test.go new file mode 100644 index 00000000..7dc5509e --- /dev/null +++ b/subproject/Xray-core-main/common/crypto/auth_test.go @@ -0,0 +1,134 @@ +package crypto_test + +import ( + "bytes" + "crypto/rand" + "io" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + . "github.com/xtls/xray-core/common/crypto" + "github.com/xtls/xray-core/common/protocol" +) + +func TestAuthenticationReaderWriter(t *testing.T) { + key := make([]byte, 16) + rand.Read(key) + + aead := NewAesGcm(key) + + const payloadSize = 1024 * 80 + rawPayload := make([]byte, payloadSize) + rand.Read(rawPayload) + + payload := buf.MergeBytes(nil, rawPayload) + + cache := bytes.NewBuffer(nil) + iv := make([]byte, 12) + rand.Read(iv) + + writer := NewAuthenticationWriter(&AEADAuthenticator{ + AEAD: aead, + NonceGenerator: GenerateStaticBytes(iv), + AdditionalDataGenerator: GenerateEmptyBytes(), + }, PlainChunkSizeParser{}, cache, protocol.TransferTypeStream, nil) + + common.Must(writer.WriteMultiBuffer(payload)) + if cache.Len() <= 1024*80 { + t.Error("cache len: ", cache.Len()) + } + common.Must(writer.WriteMultiBuffer(buf.MultiBuffer{})) + + reader := NewAuthenticationReader(&AEADAuthenticator{ + AEAD: aead, + NonceGenerator: GenerateStaticBytes(iv), + AdditionalDataGenerator: GenerateEmptyBytes(), + }, PlainChunkSizeParser{}, cache, protocol.TransferTypeStream, nil) + + var mb buf.MultiBuffer + + for mb.Len() < payloadSize { + mb2, err := reader.ReadMultiBuffer() + common.Must(err) + + mb, _ = buf.MergeMulti(mb, mb2) + } + + if mb.Len() != payloadSize { + t.Error("mb len: ", mb.Len()) + } + + mbContent := make([]byte, payloadSize) + buf.SplitBytes(mb, mbContent) + if r := cmp.Diff(mbContent, rawPayload); r != "" { + t.Error(r) + } + + _, err := reader.ReadMultiBuffer() + if err != io.EOF { + t.Error("error: ", err) + } +} + +func TestAuthenticationReaderWriterPacket(t *testing.T) { + key := make([]byte, 16) + common.Must2(rand.Read(key)) + + aead := NewAesGcm(key) + + cache := buf.New() + iv := make([]byte, 12) + rand.Read(iv) + + writer := NewAuthenticationWriter(&AEADAuthenticator{ + AEAD: aead, + NonceGenerator: GenerateStaticBytes(iv), + AdditionalDataGenerator: GenerateEmptyBytes(), + }, PlainChunkSizeParser{}, cache, protocol.TransferTypePacket, nil) + + var payload buf.MultiBuffer + pb1 := buf.New() + pb1.Write([]byte("abcd")) + payload = append(payload, pb1) + + pb2 := buf.New() + pb2.Write([]byte("efgh")) + payload = append(payload, pb2) + + common.Must(writer.WriteMultiBuffer(payload)) + if cache.Len() == 0 { + t.Error("cache len: ", cache.Len()) + } + + common.Must(writer.WriteMultiBuffer(buf.MultiBuffer{})) + + reader := NewAuthenticationReader(&AEADAuthenticator{ + AEAD: aead, + NonceGenerator: GenerateStaticBytes(iv), + AdditionalDataGenerator: GenerateEmptyBytes(), + }, PlainChunkSizeParser{}, cache, protocol.TransferTypePacket, nil) + + mb, err := reader.ReadMultiBuffer() + common.Must(err) + + mb, b1 := buf.SplitFirst(mb) + if b1.String() != "abcd" { + t.Error("b1: ", b1.String()) + } + + mb, b2 := buf.SplitFirst(mb) + if b2.String() != "efgh" { + t.Error("b2: ", b2.String()) + } + + if !mb.IsEmpty() { + t.Error("not empty") + } + + _, err = reader.ReadMultiBuffer() + if err != io.EOF { + t.Error("error: ", err) + } +} diff --git a/subproject/Xray-core-main/common/crypto/benchmark_test.go b/subproject/Xray-core-main/common/crypto/benchmark_test.go new file mode 100644 index 00000000..1f30c802 --- /dev/null +++ b/subproject/Xray-core-main/common/crypto/benchmark_test.go @@ -0,0 +1,50 @@ +package crypto_test + +import ( + "crypto/cipher" + "testing" + + . "github.com/xtls/xray-core/common/crypto" +) + +const benchSize = 1024 * 1024 + +func benchmarkStream(b *testing.B, c cipher.Stream) { + b.SetBytes(benchSize) + input := make([]byte, benchSize) + output := make([]byte, benchSize) + b.ResetTimer() + for i := 0; i < b.N; i++ { + c.XORKeyStream(output, input) + } +} + +func BenchmarkChaCha20(b *testing.B) { + key := make([]byte, 32) + nonce := make([]byte, 8) + c := NewChaCha20Stream(key, nonce) + benchmarkStream(b, c) +} + +func BenchmarkChaCha20IETF(b *testing.B) { + key := make([]byte, 32) + nonce := make([]byte, 12) + c := NewChaCha20Stream(key, nonce) + benchmarkStream(b, c) +} + +func BenchmarkAESEncryption(b *testing.B) { + key := make([]byte, 32) + iv := make([]byte, 16) + c := NewAesEncryptionStream(key, iv) + + benchmarkStream(b, c) +} + +func BenchmarkAESDecryption(b *testing.B) { + key := make([]byte, 32) + iv := make([]byte, 16) + c := NewAesDecryptionStream(key, iv) + + benchmarkStream(b, c) +} diff --git a/subproject/Xray-core-main/common/crypto/chacha20.go b/subproject/Xray-core-main/common/crypto/chacha20.go new file mode 100644 index 00000000..87273c3c --- /dev/null +++ b/subproject/Xray-core-main/common/crypto/chacha20.go @@ -0,0 +1,13 @@ +package crypto + +import ( + "crypto/cipher" + + "github.com/xtls/xray-core/common/crypto/internal" +) + +// NewChaCha20Stream creates a new Chacha20 encryption/descryption stream based on give key and IV. +// Caller must ensure the length of key is 32 bytes, and length of IV is either 8 or 12 bytes. +func NewChaCha20Stream(key []byte, iv []byte) cipher.Stream { + return internal.NewChaCha20Stream(key, iv, 20) +} diff --git a/subproject/Xray-core-main/common/crypto/chacha20_test.go b/subproject/Xray-core-main/common/crypto/chacha20_test.go new file mode 100644 index 00000000..a552cdac --- /dev/null +++ b/subproject/Xray-core-main/common/crypto/chacha20_test.go @@ -0,0 +1,76 @@ +package crypto_test + +import ( + "crypto/rand" + "encoding/hex" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/xtls/xray-core/common" + . "github.com/xtls/xray-core/common/crypto" +) + +func mustDecodeHex(s string) []byte { + b, err := hex.DecodeString(s) + common.Must(err) + return b +} + +func TestChaCha20Stream(t *testing.T) { + cases := []struct { + key []byte + iv []byte + output []byte + }{ + { + key: mustDecodeHex("0000000000000000000000000000000000000000000000000000000000000000"), + iv: mustDecodeHex("0000000000000000"), + output: mustDecodeHex("76b8e0ada0f13d90405d6ae55386bd28bdd219b8a08ded1aa836efcc8b770dc7" + + "da41597c5157488d7724e03fb8d84a376a43b8f41518a11cc387b669b2ee6586" + + "9f07e7be5551387a98ba977c732d080dcb0f29a048e3656912c6533e32ee7aed" + + "29b721769ce64e43d57133b074d839d531ed1f28510afb45ace10a1f4b794d6f"), + }, + { + key: mustDecodeHex("5555555555555555555555555555555555555555555555555555555555555555"), + iv: mustDecodeHex("5555555555555555"), + output: mustDecodeHex("bea9411aa453c5434a5ae8c92862f564396855a9ea6e22d6d3b50ae1b3663311" + + "a4a3606c671d605ce16c3aece8e61ea145c59775017bee2fa6f88afc758069f7" + + "e0b8f676e644216f4d2a3422d7fa36c6c4931aca950e9da42788e6d0b6d1cd83" + + "8ef652e97b145b14871eae6c6804c7004db5ac2fce4c68c726d004b10fcaba86"), + }, + { + key: mustDecodeHex("0000000000000000000000000000000000000000000000000000000000000000"), + iv: mustDecodeHex("000000000000000000000000"), + output: mustDecodeHex("76b8e0ada0f13d90405d6ae55386bd28bdd219b8a08ded1aa836efcc8b770dc7da41597c5157488d7724e03fb8d84a376a43b8f41518a11cc387b669b2ee6586"), + }, + } + for _, c := range cases { + s := NewChaCha20Stream(c.key, c.iv) + input := make([]byte, len(c.output)) + actualOutout := make([]byte, len(c.output)) + s.XORKeyStream(actualOutout, input) + if r := cmp.Diff(c.output, actualOutout); r != "" { + t.Fatal(r) + } + } +} + +func TestChaCha20Decoding(t *testing.T) { + key := make([]byte, 32) + common.Must2(rand.Read(key)) + iv := make([]byte, 8) + common.Must2(rand.Read(iv)) + stream := NewChaCha20Stream(key, iv) + + payload := make([]byte, 1024) + common.Must2(rand.Read(payload)) + + x := make([]byte, len(payload)) + stream.XORKeyStream(x, payload) + + stream2 := NewChaCha20Stream(key, iv) + stream2.XORKeyStream(x, x) + if r := cmp.Diff(x, payload); r != "" { + t.Fatal(r) + } +} diff --git a/subproject/Xray-core-main/common/crypto/chunk.go b/subproject/Xray-core-main/common/crypto/chunk.go new file mode 100644 index 00000000..9cd48181 --- /dev/null +++ b/subproject/Xray-core-main/common/crypto/chunk.go @@ -0,0 +1,160 @@ +package crypto + +import ( + "encoding/binary" + "io" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" +) + +// ChunkSizeDecoder is a utility class to decode size value from bytes. +type ChunkSizeDecoder interface { + SizeBytes() int32 + Decode([]byte) (uint16, error) +} + +// ChunkSizeEncoder is a utility class to encode size value into bytes. +type ChunkSizeEncoder interface { + SizeBytes() int32 + Encode(uint16, []byte) []byte +} + +type PaddingLengthGenerator interface { + MaxPaddingLen() uint16 + NextPaddingLen() uint16 +} + +type PlainChunkSizeParser struct{} + +func (PlainChunkSizeParser) SizeBytes() int32 { + return 2 +} + +func (PlainChunkSizeParser) Encode(size uint16, b []byte) []byte { + binary.BigEndian.PutUint16(b, size) + return b[:2] +} + +func (PlainChunkSizeParser) Decode(b []byte) (uint16, error) { + return binary.BigEndian.Uint16(b), nil +} + +type AEADChunkSizeParser struct { + Auth *AEADAuthenticator +} + +func (p *AEADChunkSizeParser) SizeBytes() int32 { + return 2 + int32(p.Auth.Overhead()) +} + +func (p *AEADChunkSizeParser) Encode(size uint16, b []byte) []byte { + binary.BigEndian.PutUint16(b, size-uint16(p.Auth.Overhead())) + b, err := p.Auth.Seal(b[:0], b[:2]) + common.Must(err) + return b +} + +func (p *AEADChunkSizeParser) Decode(b []byte) (uint16, error) { + b, err := p.Auth.Open(b[:0], b) + if err != nil { + return 0, err + } + return binary.BigEndian.Uint16(b) + uint16(p.Auth.Overhead()), nil +} + +type ChunkStreamReader struct { + sizeDecoder ChunkSizeDecoder + reader *buf.BufferedReader + + buffer []byte + leftOverSize int32 + maxNumChunk uint32 + numChunk uint32 +} + +func NewChunkStreamReader(sizeDecoder ChunkSizeDecoder, reader io.Reader) *ChunkStreamReader { + return NewChunkStreamReaderWithChunkCount(sizeDecoder, reader, 0) +} + +func NewChunkStreamReaderWithChunkCount(sizeDecoder ChunkSizeDecoder, reader io.Reader, maxNumChunk uint32) *ChunkStreamReader { + r := &ChunkStreamReader{ + sizeDecoder: sizeDecoder, + buffer: make([]byte, sizeDecoder.SizeBytes()), + maxNumChunk: maxNumChunk, + } + if breader, ok := reader.(*buf.BufferedReader); ok { + r.reader = breader + } else { + r.reader = &buf.BufferedReader{Reader: buf.NewReader(reader)} + } + + return r +} + +func (r *ChunkStreamReader) readSize() (uint16, error) { + if _, err := io.ReadFull(r.reader, r.buffer); err != nil { + return 0, err + } + return r.sizeDecoder.Decode(r.buffer) +} + +func (r *ChunkStreamReader) ReadMultiBuffer() (buf.MultiBuffer, error) { + size := r.leftOverSize + if size == 0 { + r.numChunk++ + if r.maxNumChunk > 0 && r.numChunk > r.maxNumChunk { + return nil, io.EOF + } + nextSize, err := r.readSize() + if err != nil { + return nil, err + } + if nextSize == 0 { + return nil, io.EOF + } + size = int32(nextSize) + } + r.leftOverSize = size + + mb, err := r.reader.ReadAtMost(size) + if !mb.IsEmpty() { + r.leftOverSize -= mb.Len() + return mb, nil + } + return nil, err +} + +type ChunkStreamWriter struct { + sizeEncoder ChunkSizeEncoder + writer buf.Writer +} + +func NewChunkStreamWriter(sizeEncoder ChunkSizeEncoder, writer io.Writer) *ChunkStreamWriter { + return &ChunkStreamWriter{ + sizeEncoder: sizeEncoder, + writer: buf.NewWriter(writer), + } +} + +func (w *ChunkStreamWriter) WriteMultiBuffer(mb buf.MultiBuffer) error { + const sliceSize = 8192 + mbLen := mb.Len() + mb2Write := make(buf.MultiBuffer, 0, mbLen/buf.Size+mbLen/sliceSize+2) + + for { + mb2, slice := buf.SplitSize(mb, sliceSize) + mb = mb2 + + b := buf.New() + w.sizeEncoder.Encode(uint16(slice.Len()), b.Extend(w.sizeEncoder.SizeBytes())) + mb2Write = append(mb2Write, b) + mb2Write = append(mb2Write, slice...) + + if mb.IsEmpty() { + break + } + } + + return w.writer.WriteMultiBuffer(mb2Write) +} diff --git a/subproject/Xray-core-main/common/crypto/chunk_test.go b/subproject/Xray-core-main/common/crypto/chunk_test.go new file mode 100644 index 00000000..7f874f3d --- /dev/null +++ b/subproject/Xray-core-main/common/crypto/chunk_test.go @@ -0,0 +1,51 @@ +package crypto_test + +import ( + "bytes" + "io" + "testing" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + . "github.com/xtls/xray-core/common/crypto" +) + +func TestChunkStreamIO(t *testing.T) { + cache := bytes.NewBuffer(make([]byte, 0, 8192)) + + writer := NewChunkStreamWriter(PlainChunkSizeParser{}, cache) + reader := NewChunkStreamReader(PlainChunkSizeParser{}, cache) + + b := buf.New() + b.WriteString("abcd") + common.Must(writer.WriteMultiBuffer(buf.MultiBuffer{b})) + + b = buf.New() + b.WriteString("efg") + common.Must(writer.WriteMultiBuffer(buf.MultiBuffer{b})) + + common.Must(writer.WriteMultiBuffer(buf.MultiBuffer{})) + + if cache.Len() != 13 { + t.Fatalf("Cache length is %d, want 13", cache.Len()) + } + + mb, err := reader.ReadMultiBuffer() + common.Must(err) + + if s := mb.String(); s != "abcd" { + t.Error("content: ", s) + } + + mb, err = reader.ReadMultiBuffer() + common.Must(err) + + if s := mb.String(); s != "efg" { + t.Error("content: ", s) + } + + _, err = reader.ReadMultiBuffer() + if err != io.EOF { + t.Error("error: ", err) + } +} diff --git a/subproject/Xray-core-main/common/crypto/crypto.go b/subproject/Xray-core-main/common/crypto/crypto.go new file mode 100644 index 00000000..49ce164b --- /dev/null +++ b/subproject/Xray-core-main/common/crypto/crypto.go @@ -0,0 +1,38 @@ +// Package crypto provides common crypto libraries for Xray. +package crypto // import "github.com/xtls/xray-core/common/crypto" + +import ( + "crypto/rand" + "math/big" + + "github.com/xtls/xray-core/common" +) + +// [,) +func RandBetween(from int64, to int64) int64 { + if from == to { + return from + } + if from > to { + from, to = to, from + } + bigInt, _ := rand.Int(rand.Reader, big.NewInt(to-from)) + return from + bigInt.Int64() +} + +// [,] +func RandBytesBetween(b []byte, from, to byte) { + common.Must2(rand.Read(b)) + + if from > to { + from, to = to, from + } + + if to-from == 255 { + return + } + + for i := range b { + b[i] = from + b[i]%(to-from+1) + } +} diff --git a/subproject/Xray-core-main/common/crypto/internal/chacha.go b/subproject/Xray-core-main/common/crypto/internal/chacha.go new file mode 100644 index 00000000..988ac984 --- /dev/null +++ b/subproject/Xray-core-main/common/crypto/internal/chacha.go @@ -0,0 +1,80 @@ +package internal + +//go:generate go run chacha_core_gen.go + +import ( + "encoding/binary" +) + +const ( + wordSize = 4 // the size of ChaCha20's words + stateSize = 16 // the size of ChaCha20's state, in words + blockSize = stateSize * wordSize // the size of ChaCha20's block, in bytes +) + +type ChaCha20Stream struct { + state [stateSize]uint32 // the state as an array of 16 32-bit words + block [blockSize]byte // the keystream as an array of 64 bytes + offset int // the offset of used bytes in block + rounds int +} + +func NewChaCha20Stream(key []byte, nonce []byte, rounds int) *ChaCha20Stream { + s := new(ChaCha20Stream) + // the magic constants for 256-bit keys + s.state[0] = 0x61707865 + s.state[1] = 0x3320646e + s.state[2] = 0x79622d32 + s.state[3] = 0x6b206574 + + for i := 0; i < 8; i++ { + s.state[i+4] = binary.LittleEndian.Uint32(key[i*4 : i*4+4]) + } + + switch len(nonce) { + case 8: + s.state[14] = binary.LittleEndian.Uint32(nonce[0:]) + s.state[15] = binary.LittleEndian.Uint32(nonce[4:]) + case 12: + s.state[13] = binary.LittleEndian.Uint32(nonce[0:4]) + s.state[14] = binary.LittleEndian.Uint32(nonce[4:8]) + s.state[15] = binary.LittleEndian.Uint32(nonce[8:12]) + default: + panic("bad nonce length") + } + + s.rounds = rounds + ChaCha20Block(&s.state, s.block[:], s.rounds) + return s +} + +func (s *ChaCha20Stream) XORKeyStream(dst, src []byte) { + // Stride over the input in 64-byte blocks, minus the amount of keystream + // previously used. This will produce best results when processing blocks + // of a size evenly divisible by 64. + i := 0 + max := len(src) + for i < max { + gap := blockSize - s.offset + + limit := i + gap + if limit > max { + limit = max + } + + o := s.offset + for j := i; j < limit; j++ { + dst[j] = src[j] ^ s.block[o] + o++ + } + + i += gap + s.offset = o + + if o == blockSize { + s.offset = 0 + s.state[12]++ + ChaCha20Block(&s.state, s.block[:], s.rounds) + } + } +} diff --git a/subproject/Xray-core-main/common/crypto/internal/chacha_core.generated.go b/subproject/Xray-core-main/common/crypto/internal/chacha_core.generated.go new file mode 100644 index 00000000..65458776 --- /dev/null +++ b/subproject/Xray-core-main/common/crypto/internal/chacha_core.generated.go @@ -0,0 +1,123 @@ +package internal + +import "encoding/binary" + +func ChaCha20Block(s *[16]uint32, out []byte, rounds int) { + x0, x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14, x15 := s[0], s[1], s[2], s[3], s[4], s[5], s[6], s[7], s[8], s[9], s[10], s[11], s[12], s[13], s[14], s[15] + for i := 0; i < rounds; i += 2 { + var x uint32 + + x0 += x4 + x = x12 ^ x0 + x12 = (x << 16) | (x >> (32 - 16)) + x8 += x12 + x = x4 ^ x8 + x4 = (x << 12) | (x >> (32 - 12)) + x0 += x4 + x = x12 ^ x0 + x12 = (x << 8) | (x >> (32 - 8)) + x8 += x12 + x = x4 ^ x8 + x4 = (x << 7) | (x >> (32 - 7)) + x1 += x5 + x = x13 ^ x1 + x13 = (x << 16) | (x >> (32 - 16)) + x9 += x13 + x = x5 ^ x9 + x5 = (x << 12) | (x >> (32 - 12)) + x1 += x5 + x = x13 ^ x1 + x13 = (x << 8) | (x >> (32 - 8)) + x9 += x13 + x = x5 ^ x9 + x5 = (x << 7) | (x >> (32 - 7)) + x2 += x6 + x = x14 ^ x2 + x14 = (x << 16) | (x >> (32 - 16)) + x10 += x14 + x = x6 ^ x10 + x6 = (x << 12) | (x >> (32 - 12)) + x2 += x6 + x = x14 ^ x2 + x14 = (x << 8) | (x >> (32 - 8)) + x10 += x14 + x = x6 ^ x10 + x6 = (x << 7) | (x >> (32 - 7)) + x3 += x7 + x = x15 ^ x3 + x15 = (x << 16) | (x >> (32 - 16)) + x11 += x15 + x = x7 ^ x11 + x7 = (x << 12) | (x >> (32 - 12)) + x3 += x7 + x = x15 ^ x3 + x15 = (x << 8) | (x >> (32 - 8)) + x11 += x15 + x = x7 ^ x11 + x7 = (x << 7) | (x >> (32 - 7)) + x0 += x5 + x = x15 ^ x0 + x15 = (x << 16) | (x >> (32 - 16)) + x10 += x15 + x = x5 ^ x10 + x5 = (x << 12) | (x >> (32 - 12)) + x0 += x5 + x = x15 ^ x0 + x15 = (x << 8) | (x >> (32 - 8)) + x10 += x15 + x = x5 ^ x10 + x5 = (x << 7) | (x >> (32 - 7)) + x1 += x6 + x = x12 ^ x1 + x12 = (x << 16) | (x >> (32 - 16)) + x11 += x12 + x = x6 ^ x11 + x6 = (x << 12) | (x >> (32 - 12)) + x1 += x6 + x = x12 ^ x1 + x12 = (x << 8) | (x >> (32 - 8)) + x11 += x12 + x = x6 ^ x11 + x6 = (x << 7) | (x >> (32 - 7)) + x2 += x7 + x = x13 ^ x2 + x13 = (x << 16) | (x >> (32 - 16)) + x8 += x13 + x = x7 ^ x8 + x7 = (x << 12) | (x >> (32 - 12)) + x2 += x7 + x = x13 ^ x2 + x13 = (x << 8) | (x >> (32 - 8)) + x8 += x13 + x = x7 ^ x8 + x7 = (x << 7) | (x >> (32 - 7)) + x3 += x4 + x = x14 ^ x3 + x14 = (x << 16) | (x >> (32 - 16)) + x9 += x14 + x = x4 ^ x9 + x4 = (x << 12) | (x >> (32 - 12)) + x3 += x4 + x = x14 ^ x3 + x14 = (x << 8) | (x >> (32 - 8)) + x9 += x14 + x = x4 ^ x9 + x4 = (x << 7) | (x >> (32 - 7)) + } + binary.LittleEndian.PutUint32(out[0:4], s[0]+x0) + binary.LittleEndian.PutUint32(out[4:8], s[1]+x1) + binary.LittleEndian.PutUint32(out[8:12], s[2]+x2) + binary.LittleEndian.PutUint32(out[12:16], s[3]+x3) + binary.LittleEndian.PutUint32(out[16:20], s[4]+x4) + binary.LittleEndian.PutUint32(out[20:24], s[5]+x5) + binary.LittleEndian.PutUint32(out[24:28], s[6]+x6) + binary.LittleEndian.PutUint32(out[28:32], s[7]+x7) + binary.LittleEndian.PutUint32(out[32:36], s[8]+x8) + binary.LittleEndian.PutUint32(out[36:40], s[9]+x9) + binary.LittleEndian.PutUint32(out[40:44], s[10]+x10) + binary.LittleEndian.PutUint32(out[44:48], s[11]+x11) + binary.LittleEndian.PutUint32(out[48:52], s[12]+x12) + binary.LittleEndian.PutUint32(out[52:56], s[13]+x13) + binary.LittleEndian.PutUint32(out[56:60], s[14]+x14) + binary.LittleEndian.PutUint32(out[60:64], s[15]+x15) +} diff --git a/subproject/Xray-core-main/common/crypto/internal/chacha_core_gen.go b/subproject/Xray-core-main/common/crypto/internal/chacha_core_gen.go new file mode 100644 index 00000000..5a285172 --- /dev/null +++ b/subproject/Xray-core-main/common/crypto/internal/chacha_core_gen.go @@ -0,0 +1,70 @@ +//go:build generate +// +build generate + +package main + +import ( + "fmt" + "log" + "os" +) + +func writeQuarterRound(file *os.File, a, b, c, d int) { + add := "x%d+=x%d\n" + xor := "x=x%d^x%d\n" + rotate := "x%d=(x << %d) | (x >> (32 - %d))\n" + + fmt.Fprintf(file, add, a, b) + fmt.Fprintf(file, xor, d, a) + fmt.Fprintf(file, rotate, d, 16, 16) + + fmt.Fprintf(file, add, c, d) + fmt.Fprintf(file, xor, b, c) + fmt.Fprintf(file, rotate, b, 12, 12) + + fmt.Fprintf(file, add, a, b) + fmt.Fprintf(file, xor, d, a) + fmt.Fprintf(file, rotate, d, 8, 8) + + fmt.Fprintf(file, add, c, d) + fmt.Fprintf(file, xor, b, c) + fmt.Fprintf(file, rotate, b, 7, 7) +} + +func writeChacha20Block(file *os.File) { + fmt.Fprintln(file, ` +func ChaCha20Block(s *[16]uint32, out []byte, rounds int) { + var x0,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15 = s[0],s[1],s[2],s[3],s[4],s[5],s[6],s[7],s[8],s[9],s[10],s[11],s[12],s[13],s[14],s[15] + for i := 0; i < rounds; i+=2 { + var x uint32 + `) + + writeQuarterRound(file, 0, 4, 8, 12) + writeQuarterRound(file, 1, 5, 9, 13) + writeQuarterRound(file, 2, 6, 10, 14) + writeQuarterRound(file, 3, 7, 11, 15) + writeQuarterRound(file, 0, 5, 10, 15) + writeQuarterRound(file, 1, 6, 11, 12) + writeQuarterRound(file, 2, 7, 8, 13) + writeQuarterRound(file, 3, 4, 9, 14) + fmt.Fprintln(file, "}") + for i := 0; i < 16; i++ { + fmt.Fprintf(file, "binary.LittleEndian.PutUint32(out[%d:%d], s[%d]+x%d)\n", i*4, i*4+4, i, i) + } + fmt.Fprintln(file, "}") + fmt.Fprintln(file) +} + +func main() { + file, err := os.OpenFile("chacha_core.generated.go", os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0o644) + if err != nil { + log.Fatalf("Failed to generate chacha_core.go: %v", err) + } + defer file.Close() + + fmt.Fprintln(file, "package internal") + fmt.Fprintln(file) + fmt.Fprintln(file, "import \"encoding/binary\"") + fmt.Fprintln(file) + writeChacha20Block(file) +} diff --git a/subproject/Xray-core-main/common/crypto/io.go b/subproject/Xray-core-main/common/crypto/io.go new file mode 100644 index 00000000..acf9e0f6 --- /dev/null +++ b/subproject/Xray-core-main/common/crypto/io.go @@ -0,0 +1,64 @@ +package crypto + +import ( + "crypto/cipher" + "io" + + "github.com/xtls/xray-core/common/buf" +) + +type CryptionReader struct { + stream cipher.Stream + reader io.Reader +} + +func NewCryptionReader(stream cipher.Stream, reader io.Reader) *CryptionReader { + return &CryptionReader{ + stream: stream, + reader: reader, + } +} + +func (r *CryptionReader) Read(data []byte) (int, error) { + nBytes, err := r.reader.Read(data) + if nBytes > 0 { + r.stream.XORKeyStream(data[:nBytes], data[:nBytes]) + } + return nBytes, err +} + +var _ buf.Writer = (*CryptionWriter)(nil) + +type CryptionWriter struct { + stream cipher.Stream + writer io.Writer + bufWriter buf.Writer +} + +// NewCryptionWriter creates a new CryptionWriter. +func NewCryptionWriter(stream cipher.Stream, writer io.Writer) *CryptionWriter { + return &CryptionWriter{ + stream: stream, + writer: writer, + bufWriter: buf.NewWriter(writer), + } +} + +// Write implements io.Writer.Write(). +func (w *CryptionWriter) Write(data []byte) (int, error) { + w.stream.XORKeyStream(data, data) + + if err := buf.WriteAllBytes(w.writer, data, nil); err != nil { + return 0, err + } + return len(data), nil +} + +// WriteMultiBuffer implements buf.Writer. +func (w *CryptionWriter) WriteMultiBuffer(mb buf.MultiBuffer) error { + for _, b := range mb { + w.stream.XORKeyStream(b.Bytes(), b.Bytes()) + } + + return w.bufWriter.WriteMultiBuffer(mb) +} diff --git a/subproject/Xray-core-main/common/ctx/context.go b/subproject/Xray-core-main/common/ctx/context.go new file mode 100644 index 00000000..25144f5f --- /dev/null +++ b/subproject/Xray-core-main/common/ctx/context.go @@ -0,0 +1,25 @@ +package ctx + +import "context" + +type SessionKey int + +// ID of a session. +type ID uint32 + +const ( + idSessionKey SessionKey = 0 +) + +// ContextWithID returns a new context with the given ID. +func ContextWithID(ctx context.Context, id ID) context.Context { + return context.WithValue(ctx, idSessionKey, id) +} + +// IDFromContext returns ID in this context, or 0 if not contained. +func IDFromContext(ctx context.Context) ID { + if id, ok := ctx.Value(idSessionKey).(ID); ok { + return id + } + return 0 +} diff --git a/subproject/Xray-core-main/common/dice/dice.go b/subproject/Xray-core-main/common/dice/dice.go new file mode 100644 index 00000000..0a0a40e4 --- /dev/null +++ b/subproject/Xray-core-main/common/dice/dice.go @@ -0,0 +1,55 @@ +// Package dice contains common functions to generate random number. +// It also initialize math/rand with the time in seconds at launch time. +package dice // import "github.com/xtls/xray-core/common/dice" + +import ( + "math/rand" +) + +// Roll returns a non-negative number between 0 (inclusive) and n (exclusive). +func Roll(n int) int { + if n == 1 { + return 0 + } + return rand.Intn(n) +} + +// RollInt63n returns a non-negative number between 0 (inclusive) and n (exclusive). +func RollInt63n(n int64) int64 { + if n == 1 { + return 0 + } + return rand.Int63n(n) +} + +// Roll returns a non-negative number between 0 (inclusive) and n (exclusive). +func RollDeterministic(n int, seed int64) int { + if n == 1 { + return 0 + } + return rand.New(rand.NewSource(seed)).Intn(n) +} + +// RollUint16 returns a random uint16 value. +func RollUint16() uint16 { + return uint16(rand.Int63() >> 47) +} + +func RollUint64() uint64 { + return rand.Uint64() +} + +func NewDeterministicDice(seed int64) *DeterministicDice { + return &DeterministicDice{rand.New(rand.NewSource(seed))} +} + +type DeterministicDice struct { + *rand.Rand +} + +func (dd *DeterministicDice) Roll(n int) int { + if n == 1 { + return 0 + } + return dd.Intn(n) +} diff --git a/subproject/Xray-core-main/common/dice/dice_test.go b/subproject/Xray-core-main/common/dice/dice_test.go new file mode 100644 index 00000000..02eba57b --- /dev/null +++ b/subproject/Xray-core-main/common/dice/dice_test.go @@ -0,0 +1,50 @@ +package dice_test + +import ( + "math/rand" + "testing" + + . "github.com/xtls/xray-core/common/dice" +) + +func BenchmarkRoll1(b *testing.B) { + for i := 0; i < b.N; i++ { + Roll(1) + } +} + +func BenchmarkRoll20(b *testing.B) { + for i := 0; i < b.N; i++ { + Roll(20) + } +} + +func BenchmarkIntn1(b *testing.B) { + for i := 0; i < b.N; i++ { + rand.Intn(1) + } +} + +func BenchmarkIntn20(b *testing.B) { + for i := 0; i < b.N; i++ { + rand.Intn(20) + } +} + +func BenchmarkInt63(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = uint16(rand.Int63() >> 47) + } +} + +func BenchmarkInt31(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = uint16(rand.Int31() >> 15) + } +} + +func BenchmarkIntn(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = uint16(rand.Intn(65536)) + } +} diff --git a/subproject/Xray-core-main/common/drain/drain.go b/subproject/Xray-core-main/common/drain/drain.go new file mode 100644 index 00000000..5a3be246 --- /dev/null +++ b/subproject/Xray-core-main/common/drain/drain.go @@ -0,0 +1,8 @@ +package drain + +import "io" + +type Drainer interface { + AcknowledgeReceive(size int) + Drain(reader io.Reader) error +} diff --git a/subproject/Xray-core-main/common/drain/drainer.go b/subproject/Xray-core-main/common/drain/drainer.go new file mode 100644 index 00000000..16ed1f23 --- /dev/null +++ b/subproject/Xray-core-main/common/drain/drainer.go @@ -0,0 +1,62 @@ +package drain + +import ( + "io" + + "github.com/xtls/xray-core/common/dice" + "github.com/xtls/xray-core/common/errors" +) + +type BehaviorSeedLimitedDrainer struct { + DrainSize int +} + +func NewBehaviorSeedLimitedDrainer(behaviorSeed int64, drainFoundation, maxBaseDrainSize, maxRandDrain int) (Drainer, error) { + behaviorRand := dice.NewDeterministicDice(behaviorSeed) + BaseDrainSize := behaviorRand.Roll(maxBaseDrainSize) + RandDrainMax := behaviorRand.Roll(maxRandDrain) + 1 + RandDrainRolled := dice.Roll(RandDrainMax) + DrainSize := drainFoundation + BaseDrainSize + RandDrainRolled + return &BehaviorSeedLimitedDrainer{DrainSize: DrainSize}, nil +} + +func (d *BehaviorSeedLimitedDrainer) AcknowledgeReceive(size int) { + d.DrainSize -= size +} + +func (d *BehaviorSeedLimitedDrainer) Drain(reader io.Reader) error { + if d.DrainSize > 0 { + err := drainReadN(reader, d.DrainSize) + if err == nil { + return errors.New("drained connection") + } + return errors.New("unable to drain connection").Base(err) + } + return nil +} + +func drainReadN(reader io.Reader, n int) error { + _, err := io.CopyN(io.Discard, reader, int64(n)) + return err +} + +func WithError(drainer Drainer, reader io.Reader, err error) error { + drainErr := drainer.Drain(reader) + if drainErr == nil { + return err + } + return errors.New(drainErr).Base(err) +} + +type NopDrainer struct{} + +func (n NopDrainer) AcknowledgeReceive(size int) { +} + +func (n NopDrainer) Drain(reader io.Reader) error { + return nil +} + +func NewNopDrainer() Drainer { + return &NopDrainer{} +} diff --git a/subproject/Xray-core-main/common/errors/errors.go b/subproject/Xray-core-main/common/errors/errors.go new file mode 100644 index 00000000..7a35f254 --- /dev/null +++ b/subproject/Xray-core-main/common/errors/errors.go @@ -0,0 +1,227 @@ +// Package errors is a drop-in replacement for Golang lib 'errors'. +package errors // import "github.com/xtls/xray-core/common/errors" + +import ( + "context" + "runtime" + "strings" + + c "github.com/xtls/xray-core/common/ctx" + "github.com/xtls/xray-core/common/log" + "github.com/xtls/xray-core/common/serial" +) + +const trim = len("github.com/xtls/xray-core/") + +type hasInnerError interface { + // Unwrap returns the underlying error of this one. + Unwrap() error +} + +type hasSeverity interface { + Severity() log.Severity +} + +// Error is an error object with underlying error. +type Error struct { + prefix []interface{} + message []interface{} + caller string + inner error + severity log.Severity +} + +// Error implements error.Error(). +func (err *Error) Error() string { + builder := strings.Builder{} + for _, prefix := range err.prefix { + builder.WriteByte('[') + builder.WriteString(serial.ToString(prefix)) + builder.WriteString("] ") + } + + if len(err.caller) > 0 { + builder.WriteString(err.caller) + builder.WriteString(": ") + } + + msg := serial.Concat(err.message...) + builder.WriteString(msg) + + if err.inner != nil { + builder.WriteString(" > ") + builder.WriteString(err.inner.Error()) + } + + return builder.String() +} + +// Unwrap implements hasInnerError.Unwrap() +func (err *Error) Unwrap() error { + if err.inner == nil { + return nil + } + return err.inner +} + +func (err *Error) Base(e error) *Error { + err.inner = e + return err +} + +func (err *Error) atSeverity(s log.Severity) *Error { + err.severity = s + return err +} + +func (err *Error) Severity() log.Severity { + if err.inner == nil { + return err.severity + } + + if s, ok := err.inner.(hasSeverity); ok { + as := s.Severity() + if as < err.severity { + return as + } + } + + return err.severity +} + +// AtDebug sets the severity to debug. +func (err *Error) AtDebug() *Error { + return err.atSeverity(log.Severity_Debug) +} + +// AtInfo sets the severity to info. +func (err *Error) AtInfo() *Error { + return err.atSeverity(log.Severity_Info) +} + +// AtWarning sets the severity to warning. +func (err *Error) AtWarning() *Error { + return err.atSeverity(log.Severity_Warning) +} + +// AtError sets the severity to error. +func (err *Error) AtError() *Error { + return err.atSeverity(log.Severity_Error) +} + +// String returns the string representation of this error. +func (err *Error) String() string { + return err.Error() +} + +type ExportOptionHolder struct { + SessionID uint32 +} + +type ExportOption func(*ExportOptionHolder) + +// New returns a new error object with message formed from given arguments. +func New(msg ...interface{}) *Error { + pc, _, _, _ := runtime.Caller(1) + details := runtime.FuncForPC(pc).Name() + if len(details) >= trim { + details = details[trim:] + } + i := strings.Index(details, ".") + if i > 0 { + details = details[:i] + } + return &Error{ + message: msg, + severity: log.Severity_Info, + caller: details, + } +} + +func LogDebug(ctx context.Context, msg ...interface{}) { + doLog(ctx, nil, log.Severity_Debug, msg...) +} + +func LogDebugInner(ctx context.Context, inner error, msg ...interface{}) { + doLog(ctx, inner, log.Severity_Debug, msg...) +} + +func LogInfo(ctx context.Context, msg ...interface{}) { + doLog(ctx, nil, log.Severity_Info, msg...) +} + +func LogInfoInner(ctx context.Context, inner error, msg ...interface{}) { + doLog(ctx, inner, log.Severity_Info, msg...) +} + +func LogWarning(ctx context.Context, msg ...interface{}) { + doLog(ctx, nil, log.Severity_Warning, msg...) +} + +func LogWarningInner(ctx context.Context, inner error, msg ...interface{}) { + doLog(ctx, inner, log.Severity_Warning, msg...) +} + +func LogError(ctx context.Context, msg ...interface{}) { + doLog(ctx, nil, log.Severity_Error, msg...) +} + +func LogErrorInner(ctx context.Context, inner error, msg ...interface{}) { + doLog(ctx, inner, log.Severity_Error, msg...) +} + +func doLog(ctx context.Context, inner error, severity log.Severity, msg ...interface{}) { + pc, _, _, _ := runtime.Caller(2) + details := runtime.FuncForPC(pc).Name() + if len(details) >= trim { + details = details[trim:] + } + i := strings.Index(details, ".") + if i > 0 { + details = details[:i] + } + err := &Error{ + message: msg, + severity: severity, + caller: details, + inner: inner, + } + if ctx != nil && ctx != context.Background() { + id := uint32(c.IDFromContext(ctx)) + if id > 0 { + err.prefix = append(err.prefix, id) + } + } + log.Record(&log.GeneralMessage{ + Severity: GetSeverity(err), + Content: err, + }) +} + +// Cause returns the root cause of this error. +func Cause(err error) error { + if err == nil { + return nil + } +L: + for { + switch inner := err.(type) { + case hasInnerError: + if inner.Unwrap() == nil { + break L + } + err = inner.Unwrap() + default: + break L + } + } + return err +} + +// GetSeverity returns the actual severity of the error, including inner errors. +func GetSeverity(err error) log.Severity { + if s, ok := err.(hasSeverity); ok { + return s.Severity() + } + return log.Severity_Info +} diff --git a/subproject/Xray-core-main/common/errors/errors_test.go b/subproject/Xray-core-main/common/errors/errors_test.go new file mode 100644 index 00000000..3a1cb134 --- /dev/null +++ b/subproject/Xray-core-main/common/errors/errors_test.go @@ -0,0 +1,55 @@ +package errors_test + +import ( + "io" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + . "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/log" +) + +func TestError(t *testing.T) { + err := New("TestError") + if v := GetSeverity(err); v != log.Severity_Info { + t.Error("severity: ", v) + } + + err = New("TestError2").Base(io.EOF) + if v := GetSeverity(err); v != log.Severity_Info { + t.Error("severity: ", v) + } + + err = New("TestError3").Base(io.EOF).AtWarning() + if v := GetSeverity(err); v != log.Severity_Warning { + t.Error("severity: ", v) + } + + err = New("TestError4").Base(io.EOF).AtWarning() + err = New("TestError5").Base(err) + if v := GetSeverity(err); v != log.Severity_Warning { + t.Error("severity: ", v) + } + if v := err.Error(); !strings.Contains(v, "EOF") { + t.Error("error: ", v) + } +} + +func TestErrorMessage(t *testing.T) { + data := []struct { + err error + msg string + }{ + { + err: New("a").Base(New("b")), + msg: "common/errors_test: a > common/errors_test: b", + }, + } + + for _, d := range data { + if diff := cmp.Diff(d.msg, d.err.Error()); diff != "" { + t.Error(diff) + } + } +} diff --git a/subproject/Xray-core-main/common/errors/feature_errors.go b/subproject/Xray-core-main/common/errors/feature_errors.go new file mode 100644 index 00000000..8c443f27 --- /dev/null +++ b/subproject/Xray-core-main/common/errors/feature_errors.go @@ -0,0 +1,31 @@ +package errors + +import ( + "context" +) + +// PrintNonRemovalDeprecatedFeatureWarning prints a warning of the deprecated feature that won't be removed in the near future. +// Do not remove this function even there is no reference to it. +func PrintNonRemovalDeprecatedFeatureWarning(sourceFeature string, targetFeature string) { + LogWarning(context.Background(), "The feature "+sourceFeature+" is deprecated, not recommended for using and might be removed. Please migrate to "+targetFeature+" as soon as possible.") +} + +// PrintDeprecatedFeatureWarning prints a warning for deprecated and going to be removed feature. +// Do not remove this function even there is no reference to it. +func PrintDeprecatedFeatureWarning(feature string, migrateFeature string) { + if len(migrateFeature) > 0 { + LogWarning(context.Background(), "This feature "+feature+" is deprecated, will be removed soon and being migrated to "+migrateFeature+". Please update your config(s) according to release note and documentation before removal.") + } else { + LogWarning(context.Background(), "This feature "+feature+" is deprecated and will be removed soon. Please update your config(s) according to release note and documentation before removal.") + } +} + +// PrintRemovedFeatureError prints an error message for removed feature then return an error. And after long enough time the message can also be removed, uses as an indicator. +// Do not remove this function even there is no reference to it. +func PrintRemovedFeatureError(feature string, migrateFeature string) error { + if len(migrateFeature) > 0 { + return New("The feature " + feature + " has been removed and migrated to " + migrateFeature + ". Please update your config(s) according to release note and documentation.") + } else { + return New("The feature " + feature + " has been removed. Please update your config(s) according to release note and documentation.") + } +} diff --git a/subproject/Xray-core-main/common/errors/multi_error.go b/subproject/Xray-core-main/common/errors/multi_error.go new file mode 100644 index 00000000..cdfec9cd --- /dev/null +++ b/subproject/Xray-core-main/common/errors/multi_error.go @@ -0,0 +1,48 @@ +package errors + +import ( + "errors" + "strings" +) + +type multiError []error + +func (e multiError) Error() string { + var r strings.Builder + r.WriteString("multierr: ") + for _, err := range e { + r.WriteString(err.Error()) + r.WriteString(" | ") + } + return r.String() +} + +func Combine(maybeError ...error) error { + var errs multiError + for _, err := range maybeError { + if err != nil { + errs = append(errs, err) + } + } + if len(errs) == 0 { + return nil + } + return errs +} + +func AllEqual(expected error, actual error) bool { + switch errs := actual.(type) { + case multiError: + if len(errs) == 0 { + return false + } + for _, err := range errs { + if !errors.Is(err, expected) { + return false + } + } + return true + default: + return errors.Is(errs, expected) + } +} diff --git a/subproject/Xray-core-main/common/interfaces.go b/subproject/Xray-core-main/common/interfaces.go new file mode 100644 index 00000000..80796a08 --- /dev/null +++ b/subproject/Xray-core-main/common/interfaces.go @@ -0,0 +1,68 @@ +package common + +import "github.com/xtls/xray-core/common/errors" + +// Closable is the interface for objects that can release its resources. +// +// xray:api:beta +type Closable interface { + // Close release all resources used by this object, including goroutines. + Close() error +} + +// Interruptible is an interface for objects that can be stopped before its completion. +// +// xray:api:beta +type Interruptible interface { + Interrupt() +} + +// Close closes the obj if it is a Closable. +// +// xray:api:beta +func Close(obj interface{}) error { + if c, ok := obj.(Closable); ok { + return c.Close() + } + return nil +} + +// Interrupt calls Interrupt() if object implements Interruptible interface, or Close() if the object implements Closable interface. +// +// xray:api:beta +func Interrupt(obj interface{}) error { + if c, ok := obj.(Interruptible); ok { + c.Interrupt() + return nil + } + return Close(obj) +} + +// Runnable is the interface for objects that can start to work and stop on demand. +type Runnable interface { + // Start starts the runnable object. Upon the method returning nil, the object begins to function properly. + Start() error + + Closable +} + +// HasType is the interface for objects that knows its type. +type HasType interface { + // Type returns the type of the object. + // Usually it returns (*Type)(nil) of the object. + Type() interface{} +} + +// ChainedClosable is a Closable that consists of multiple Closable objects. +type ChainedClosable []Closable + +// Close implements Closable. +func (cc ChainedClosable) Close() error { + var errs []error + for _, c := range cc { + if err := c.Close(); err != nil { + errs = append(errs, err) + } + } + return errors.Combine(errs...) +} diff --git a/subproject/Xray-core-main/common/log/access.go b/subproject/Xray-core-main/common/log/access.go new file mode 100644 index 00000000..204212dc --- /dev/null +++ b/subproject/Xray-core-main/common/log/access.go @@ -0,0 +1,70 @@ +package log + +import ( + "context" + "strings" + + "github.com/xtls/xray-core/common/serial" +) + +type logKey int + +const ( + accessMessageKey logKey = iota +) + +type AccessStatus string + +const ( + AccessAccepted = AccessStatus("accepted") + AccessRejected = AccessStatus("rejected") +) + +type AccessMessage struct { + From interface{} + To interface{} + Status AccessStatus + Reason interface{} + Email string + Detour string +} + +func (m *AccessMessage) String() string { + builder := strings.Builder{} + builder.WriteString("from") + builder.WriteByte(' ') + builder.WriteString(serial.ToString(m.From)) + builder.WriteByte(' ') + builder.WriteString(string(m.Status)) + builder.WriteByte(' ') + builder.WriteString(serial.ToString(m.To)) + + if len(m.Detour) > 0 { + builder.WriteString(" [") + builder.WriteString(m.Detour) + builder.WriteByte(']') + } + + if reason := serial.ToString(m.Reason); len(reason) > 0 { + builder.WriteString(" ") + builder.WriteString(reason) + } + + if len(m.Email) > 0 { + builder.WriteString(" email: ") + builder.WriteString(m.Email) + } + + return builder.String() +} + +func ContextWithAccessMessage(ctx context.Context, accessMessage *AccessMessage) context.Context { + return context.WithValue(ctx, accessMessageKey, accessMessage) +} + +func AccessMessageFromContext(ctx context.Context) *AccessMessage { + if accessMessage, ok := ctx.Value(accessMessageKey).(*AccessMessage); ok { + return accessMessage + } + return nil +} diff --git a/subproject/Xray-core-main/common/log/dns.go b/subproject/Xray-core-main/common/log/dns.go new file mode 100644 index 00000000..91ecdb92 --- /dev/null +++ b/subproject/Xray-core-main/common/log/dns.go @@ -0,0 +1,60 @@ +package log + +import ( + "net" + "strings" + "time" +) + +type DNSLog struct { + Server string + Domain string + Result []net.IP + Status dnsStatus + Elapsed time.Duration + Error error +} + +func (l *DNSLog) String() string { + builder := &strings.Builder{} + + // Server got answer: domain -> [ip1, ip2] 23ms + builder.WriteString(l.Server) + builder.WriteString(" ") + builder.WriteString(string(l.Status)) + builder.WriteString(" ") + builder.WriteString(l.Domain) + builder.WriteString(" -> [") + builder.WriteString(joinNetIP(l.Result)) + builder.WriteString("]") + + if l.Elapsed > 0 { + builder.WriteString(" ") + builder.WriteString(l.Elapsed.String()) + } + if l.Error != nil { + builder.WriteString(" <") + builder.WriteString(l.Error.Error()) + builder.WriteString(">") + } + return builder.String() +} + +type dnsStatus string + +var ( + DNSQueried = dnsStatus("got answer:") + DNSCacheHit = dnsStatus("cache HIT:") + DNSCacheOptimiste = dnsStatus("cache OPTIMISTE:") +) + +func joinNetIP(ips []net.IP) string { + if len(ips) == 0 { + return "" + } + sips := make([]string, 0, len(ips)) + for _, ip := range ips { + sips = append(sips, ip.String()) + } + return strings.Join(sips, ", ") +} diff --git a/subproject/Xray-core-main/common/log/log.go b/subproject/Xray-core-main/common/log/log.go new file mode 100644 index 00000000..fbc2a509 --- /dev/null +++ b/subproject/Xray-core-main/common/log/log.go @@ -0,0 +1,64 @@ +package log // import "github.com/xtls/xray-core/common/log" + +import ( + "sync" + + "github.com/xtls/xray-core/common/serial" +) + +// Message is the interface for all log messages. +type Message interface { + String() string +} + +// Handler is the interface for log handler. +type Handler interface { + Handle(msg Message) +} + +// GeneralMessage is a general log message that can contain all kind of content. +type GeneralMessage struct { + Severity Severity + Content interface{} +} + +// String implements Message. +func (m *GeneralMessage) String() string { + return serial.Concat("[", m.Severity, "] ", m.Content) +} + +// Record writes a message into log stream. +func Record(msg Message) { + logHandler.Handle(msg) +} + +var logHandler syncHandler + +// RegisterHandler registers a new handler as current log handler. Previous registered handler will be discarded. +func RegisterHandler(handler Handler) { + if handler == nil { + panic("Log handler is nil") + } + logHandler.Set(handler) +} + +type syncHandler struct { + sync.RWMutex + Handler +} + +func (h *syncHandler) Handle(msg Message) { + h.RLock() + defer h.RUnlock() + + if h.Handler != nil { + h.Handler.Handle(msg) + } +} + +func (h *syncHandler) Set(handler Handler) { + h.Lock() + defer h.Unlock() + + h.Handler = handler +} diff --git a/subproject/Xray-core-main/common/log/log.pb.go b/subproject/Xray-core-main/common/log/log.pb.go new file mode 100644 index 00000000..e3cd8356 --- /dev/null +++ b/subproject/Xray-core-main/common/log/log.pb.go @@ -0,0 +1,138 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: common/log/log.proto + +package log + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Severity int32 + +const ( + Severity_Unknown Severity = 0 + Severity_Error Severity = 1 + Severity_Warning Severity = 2 + Severity_Info Severity = 3 + Severity_Debug Severity = 4 +) + +// Enum value maps for Severity. +var ( + Severity_name = map[int32]string{ + 0: "Unknown", + 1: "Error", + 2: "Warning", + 3: "Info", + 4: "Debug", + } + Severity_value = map[string]int32{ + "Unknown": 0, + "Error": 1, + "Warning": 2, + "Info": 3, + "Debug": 4, + } +) + +func (x Severity) Enum() *Severity { + p := new(Severity) + *p = x + return p +} + +func (x Severity) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Severity) Descriptor() protoreflect.EnumDescriptor { + return file_common_log_log_proto_enumTypes[0].Descriptor() +} + +func (Severity) Type() protoreflect.EnumType { + return &file_common_log_log_proto_enumTypes[0] +} + +func (x Severity) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Severity.Descriptor instead. +func (Severity) EnumDescriptor() ([]byte, []int) { + return file_common_log_log_proto_rawDescGZIP(), []int{0} +} + +var File_common_log_log_proto protoreflect.FileDescriptor + +const file_common_log_log_proto_rawDesc = "" + + "\n" + + "\x14common/log/log.proto\x12\x0fxray.common.log*D\n" + + "\bSeverity\x12\v\n" + + "\aUnknown\x10\x00\x12\t\n" + + "\x05Error\x10\x01\x12\v\n" + + "\aWarning\x10\x02\x12\b\n" + + "\x04Info\x10\x03\x12\t\n" + + "\x05Debug\x10\x04BO\n" + + "\x13com.xray.common.logP\x01Z$github.com/xtls/xray-core/common/log\xaa\x02\x0fXray.Common.Logb\x06proto3" + +var ( + file_common_log_log_proto_rawDescOnce sync.Once + file_common_log_log_proto_rawDescData []byte +) + +func file_common_log_log_proto_rawDescGZIP() []byte { + file_common_log_log_proto_rawDescOnce.Do(func() { + file_common_log_log_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_common_log_log_proto_rawDesc), len(file_common_log_log_proto_rawDesc))) + }) + return file_common_log_log_proto_rawDescData +} + +var file_common_log_log_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_common_log_log_proto_goTypes = []any{ + (Severity)(0), // 0: xray.common.log.Severity +} +var file_common_log_log_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_common_log_log_proto_init() } +func file_common_log_log_proto_init() { + if File_common_log_log_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_common_log_log_proto_rawDesc), len(file_common_log_log_proto_rawDesc)), + NumEnums: 1, + NumMessages: 0, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_common_log_log_proto_goTypes, + DependencyIndexes: file_common_log_log_proto_depIdxs, + EnumInfos: file_common_log_log_proto_enumTypes, + }.Build() + File_common_log_log_proto = out.File + file_common_log_log_proto_goTypes = nil + file_common_log_log_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/common/log/log.proto b/subproject/Xray-core-main/common/log/log.proto new file mode 100644 index 00000000..e2145dc8 --- /dev/null +++ b/subproject/Xray-core-main/common/log/log.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; + +package xray.common.log; +option csharp_namespace = "Xray.Common.Log"; +option go_package = "github.com/xtls/xray-core/common/log"; +option java_package = "com.xray.common.log"; +option java_multiple_files = true; + +enum Severity { + Unknown = 0; + Error = 1; + Warning = 2; + Info = 3; + Debug = 4; +} diff --git a/subproject/Xray-core-main/common/log/log_test.go b/subproject/Xray-core-main/common/log/log_test.go new file mode 100644 index 00000000..fd166cc6 --- /dev/null +++ b/subproject/Xray-core-main/common/log/log_test.go @@ -0,0 +1,32 @@ +package log_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/xtls/xray-core/common/log" + "github.com/xtls/xray-core/common/net" +) + +type testLogger struct { + value string +} + +func (l *testLogger) Handle(msg log.Message) { + l.value = msg.String() +} + +func TestLogRecord(t *testing.T) { + var logger testLogger + log.RegisterHandler(&logger) + + ip := "8.8.8.8" + log.Record(&log.GeneralMessage{ + Severity: log.Severity_Error, + Content: net.ParseAddress(ip), + }) + + if diff := cmp.Diff("[Error] "+ip, logger.value); diff != "" { + t.Error(diff) + } +} diff --git a/subproject/Xray-core-main/common/log/logger.go b/subproject/Xray-core-main/common/log/logger.go new file mode 100644 index 00000000..538eda2d --- /dev/null +++ b/subproject/Xray-core-main/common/log/logger.go @@ -0,0 +1,184 @@ +package log + +import ( + "io" + "log" + "os" + "time" + + "github.com/xtls/xray-core/common/platform" + "github.com/xtls/xray-core/common/signal/done" + "github.com/xtls/xray-core/common/signal/semaphore" +) + +// Writer is the interface for writing logs. +type Writer interface { + Write(string) error + io.Closer +} + +// WriterCreator is a function to create LogWriters. +type WriterCreator func() Writer + +type generalLogger struct { + creator WriterCreator + buffer chan Message + access *semaphore.Instance + done *done.Instance +} + +type serverityLogger struct { + inner *generalLogger + logLevel Severity +} + +// NewLogger returns a generic log handler that can handle all type of messages. +func NewLogger(logWriterCreator WriterCreator) Handler { + return &generalLogger{ + creator: logWriterCreator, + buffer: make(chan Message, 128), + access: semaphore.New(1), + done: done.New(), + } +} + +func ReplaceWithSeverityLogger(serverity Severity) { + w := CreateStdoutLogWriter() + g := &generalLogger{ + creator: w, + buffer: make(chan Message, 128), + access: semaphore.New(1), + done: done.New(), + } + s := &serverityLogger{ + inner: g, + logLevel: serverity, + } + RegisterHandler(s) +} + +func (l *serverityLogger) Handle(msg Message) { + switch msg := msg.(type) { + case *GeneralMessage: + if msg.Severity <= l.logLevel { + l.inner.Handle(msg) + } + default: + l.inner.Handle(msg) + } +} + +func (l *generalLogger) run() { + defer l.access.Signal() + + dataWritten := false + ticker := time.NewTicker(time.Minute) + defer ticker.Stop() + + logger := l.creator() + if logger == nil { + return + } + defer logger.Close() + + for { + select { + case <-l.done.Wait(): + return + case msg := <-l.buffer: + logger.Write(msg.String() + platform.LineSeparator()) + dataWritten = true + case <-ticker.C: + if !dataWritten { + return + } + dataWritten = false + } + } +} + +func (l *generalLogger) Handle(msg Message) { + + select { + case l.buffer <- msg: + default: + } + + select { + case <-l.access.Wait(): + go l.run() + default: + } +} + +func (l *generalLogger) Close() error { + return l.done.Close() +} + +type consoleLogWriter struct { + logger *log.Logger +} + +func (w *consoleLogWriter) Write(s string) error { + w.logger.Print(s) + return nil +} + +func (w *consoleLogWriter) Close() error { + return nil +} + +type fileLogWriter struct { + file *os.File + logger *log.Logger +} + +func (w *fileLogWriter) Write(s string) error { + w.logger.Print(s) + return nil +} + +func (w *fileLogWriter) Close() error { + return w.file.Close() +} + +// CreateStdoutLogWriter returns a LogWriterCreator that creates LogWriter for stdout. +func CreateStdoutLogWriter() WriterCreator { + return func() Writer { + return &consoleLogWriter{ + logger: log.New(os.Stdout, "", log.Ldate|log.Ltime|log.Lmicroseconds), + } + } +} + +// CreateStderrLogWriter returns a LogWriterCreator that creates LogWriter for stderr. +func CreateStderrLogWriter() WriterCreator { + return func() Writer { + return &consoleLogWriter{ + logger: log.New(os.Stderr, "", log.Ldate|log.Ltime|log.Lmicroseconds), + } + } +} + +// CreateFileLogWriter returns a LogWriterCreator that creates LogWriter for the given file. +func CreateFileLogWriter(path string) (WriterCreator, error) { + file, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o600) + if err != nil { + return nil, err + } + file.Close() + return func() Writer { + file, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o600) + if err != nil { + return nil + } + return &fileLogWriter{ + file: file, + logger: log.New(file, "", log.Ldate|log.Ltime|log.Lmicroseconds), + } + }, nil +} + +func init() { + RegisterHandler(NewLogger(CreateStdoutLogWriter())) +} diff --git a/subproject/Xray-core-main/common/log/logger_test.go b/subproject/Xray-core-main/common/log/logger_test.go new file mode 100644 index 00000000..6a664e08 --- /dev/null +++ b/subproject/Xray-core-main/common/log/logger_test.go @@ -0,0 +1,39 @@ +package log_test + +import ( + "os" + "strings" + "testing" + "time" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + . "github.com/xtls/xray-core/common/log" +) + +func TestFileLogger(t *testing.T) { + f, err := os.CreateTemp("", "vtest") + common.Must(err) + path := f.Name() + common.Must(f.Close()) + defer os.Remove(path) + + creator, err := CreateFileLogWriter(path) + common.Must(err) + + handler := NewLogger(creator) + handler.Handle(&GeneralMessage{Content: "Test Log"}) + time.Sleep(2 * time.Second) + + common.Must(common.Close(handler)) + + f, err = os.Open(path) + common.Must(err) + defer f.Close() + + b, err := buf.ReadAllToBytes(f) + common.Must(err) + if !strings.Contains(string(b), "Test Log") { + t.Fatal("Expect log text contains 'Test Log', but actually: ", string(b)) + } +} diff --git a/subproject/Xray-core-main/common/mux/client.go b/subproject/Xray-core-main/common/mux/client.go new file mode 100644 index 00000000..28380331 --- /dev/null +++ b/subproject/Xray-core-main/common/mux/client.go @@ -0,0 +1,419 @@ +package mux + +import ( + "context" + goerrors "errors" + "io" + "sync" + "time" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/common/signal/done" + "github.com/xtls/xray-core/common/task" + "github.com/xtls/xray-core/common/xudp" + "github.com/xtls/xray-core/proxy" + "github.com/xtls/xray-core/transport" + "github.com/xtls/xray-core/transport/internet" + "github.com/xtls/xray-core/transport/pipe" +) + +type ClientManager struct { + Enabled bool // whether mux is enabled from user config + Picker WorkerPicker +} + +func (m *ClientManager) Dispatch(ctx context.Context, link *transport.Link) error { + for i := 0; i < 16; i++ { + worker, err := m.Picker.PickAvailable() + if err != nil { + return err + } + if worker.Dispatch(ctx, link) { + return nil + } + } + + return errors.New("unable to find an available mux client").AtWarning() +} + +type WorkerPicker interface { + PickAvailable() (*ClientWorker, error) +} + +type IncrementalWorkerPicker struct { + Factory ClientWorkerFactory + + access sync.Mutex + workers []*ClientWorker + cleanupTask *task.Periodic +} + +func (p *IncrementalWorkerPicker) cleanupFunc() error { + p.access.Lock() + defer p.access.Unlock() + + if len(p.workers) == 0 { + return errors.New("no worker") + } + + p.cleanup() + return nil +} + +func (p *IncrementalWorkerPicker) cleanup() { + var activeWorkers []*ClientWorker + for _, w := range p.workers { + if !w.Closed() { + activeWorkers = append(activeWorkers, w) + } + } + p.workers = activeWorkers +} + +func (p *IncrementalWorkerPicker) findAvailable() int { + for idx, w := range p.workers { + if !w.IsFull() { + return idx + } + } + + return -1 +} + +func (p *IncrementalWorkerPicker) pickInternal() (*ClientWorker, bool, error) { + p.access.Lock() + defer p.access.Unlock() + + idx := p.findAvailable() + if idx >= 0 { + n := len(p.workers) + if n > 1 && idx != n-1 { + p.workers[n-1], p.workers[idx] = p.workers[idx], p.workers[n-1] + } + return p.workers[idx], false, nil + } + + p.cleanup() + + worker, err := p.Factory.Create() + if err != nil { + return nil, false, err + } + p.workers = append(p.workers, worker) + + if p.cleanupTask == nil { + p.cleanupTask = &task.Periodic{ + Interval: time.Second * 30, + Execute: p.cleanupFunc, + } + } + + return worker, true, nil +} + +func (p *IncrementalWorkerPicker) PickAvailable() (*ClientWorker, error) { + worker, start, err := p.pickInternal() + if start { + common.Must(p.cleanupTask.Start()) + } + + return worker, err +} + +type ClientWorkerFactory interface { + Create() (*ClientWorker, error) +} + +type DialingWorkerFactory struct { + Proxy proxy.Outbound + Dialer internet.Dialer + Strategy ClientStrategy +} + +func (f *DialingWorkerFactory) Create() (*ClientWorker, error) { + opts := []pipe.Option{pipe.WithSizeLimit(64 * 1024)} + uplinkReader, upLinkWriter := pipe.New(opts...) + downlinkReader, downlinkWriter := pipe.New(opts...) + + c, err := NewClientWorker(transport.Link{ + Reader: downlinkReader, + Writer: upLinkWriter, + }, f.Strategy) + if err != nil { + return nil, err + } + + go func(p proxy.Outbound, d internet.Dialer, c common.Closable) { + outbounds := []*session.Outbound{{ + Target: net.TCPDestination(muxCoolAddress, muxCoolPort), + }} + ctx := session.ContextWithOutbounds(context.Background(), outbounds) + ctx, cancel := context.WithCancel(ctx) + + if errP := p.Process(ctx, &transport.Link{Reader: uplinkReader, Writer: downlinkWriter}, d); errP != nil { + errC := errors.Cause(errP) + if !(goerrors.Is(errC, io.EOF) || goerrors.Is(errC, io.ErrClosedPipe) || goerrors.Is(errC, context.Canceled)) { + errors.LogInfoInner(ctx, errP, "failed to handler mux client connection") + } + } + common.Must(c.Close()) + cancel() + }(f.Proxy, f.Dialer, c.done) + + return c, nil +} + +type ClientStrategy struct { + MaxConcurrency uint32 + MaxConnection uint32 +} + +type ClientWorker struct { + sessionManager *SessionManager + link transport.Link + done *done.Instance + timer *time.Ticker + strategy ClientStrategy +} + +var ( + muxCoolAddress = net.DomainAddress("v1.mux.cool") + muxCoolPort = net.Port(9527) +) + +// NewClientWorker creates a new mux.Client. +func NewClientWorker(stream transport.Link, s ClientStrategy) (*ClientWorker, error) { + c := &ClientWorker{ + sessionManager: NewSessionManager(), + link: stream, + done: done.New(), + timer: time.NewTicker(time.Second * 16), + strategy: s, + } + + go c.fetchOutput() + go c.monitor() + + return c, nil +} + +func (m *ClientWorker) TotalConnections() uint32 { + return uint32(m.sessionManager.Count()) +} + +func (m *ClientWorker) ActiveConnections() uint32 { + return uint32(m.sessionManager.Size()) +} + +// Closed returns true if this Client is closed. +func (m *ClientWorker) Closed() bool { + return m.done.Done() +} + +func (m *ClientWorker) WaitClosed() <-chan struct{} { + return m.done.Wait() +} + +func (m *ClientWorker) Close() error { + return m.done.Close() +} + +func (m *ClientWorker) monitor() { + defer m.timer.Stop() + + for { + checkSize := m.sessionManager.Size() + checkCount := m.sessionManager.Count() + select { + case <-m.done.Wait(): + m.sessionManager.Close() + common.Interrupt(m.link.Writer) + common.Interrupt(m.link.Reader) + return + case <-m.timer.C: + if m.sessionManager.CloseIfNoSessionAndIdle(checkSize, checkCount) { + common.Must(m.done.Close()) + } + } + } +} + +func writeFirstPayload(reader buf.Reader, writer *Writer) error { + err := buf.CopyOnceTimeout(reader, writer, time.Millisecond*100) + if err == buf.ErrNotTimeoutReader || err == buf.ErrReadTimeout { + return writer.WriteMultiBuffer(buf.MultiBuffer{}) + } + + if err != nil { + return err + } + + return nil +} + +func fetchInput(ctx context.Context, s *Session, output buf.Writer) { + outbounds := session.OutboundsFromContext(ctx) + ob := outbounds[len(outbounds)-1] + transferType := protocol.TransferTypeStream + if ob.Target.Network == net.Network_UDP { + transferType = protocol.TransferTypePacket + } + s.transferType = transferType + var inbound *session.Inbound + if session.IsReverseMuxFromContext(ctx) { + inbound = session.InboundFromContext(ctx) + } + writer := NewWriter(s.ID, ob.Target, output, transferType, xudp.GetGlobalID(ctx), inbound) + defer s.Close(false) + defer writer.Close() + + errors.LogInfo(ctx, "dispatching request to ", ob.Target) + if err := writeFirstPayload(s.input, writer); err != nil { + errors.LogInfoInner(ctx, err, "failed to write first payload") + writer.hasError = true + return + } + + if err := buf.Copy(s.input, writer); err != nil { + errors.LogInfoInner(ctx, err, "failed to fetch all input") + writer.hasError = true + return + } +} + +func (m *ClientWorker) IsClosing() bool { + sm := m.sessionManager + if m.strategy.MaxConnection > 0 && sm.Count() >= int(m.strategy.MaxConnection) { + return true + } + return false +} + +// IsFull returns true if this ClientWorker is unable to accept more connections. +// it might be because it is closing, or the number of connections has reached the limit. +func (m *ClientWorker) IsFull() bool { + if m.IsClosing() || m.Closed() { + return true + } + + sm := m.sessionManager + if m.strategy.MaxConcurrency > 0 && sm.Size() >= int(m.strategy.MaxConcurrency) { + return true + } + return false +} + +func (m *ClientWorker) Dispatch(ctx context.Context, link *transport.Link) bool { + if m.IsFull() { + return false + } + + sm := m.sessionManager + s := sm.Allocate(&m.strategy) + if s == nil { + return false + } + s.input = link.Reader + s.output = link.Writer + go fetchInput(ctx, s, m.link.Writer) + if _, ok := link.Reader.(*pipe.Reader); !ok { + select { + case <-ctx.Done(): + case <-s.done.Wait(): + } + } + return true +} + +func (m *ClientWorker) handleStatueKeepAlive(meta *FrameMetadata, reader *buf.BufferedReader) error { + if meta.Option.Has(OptionData) { + return buf.Copy(NewStreamReader(reader), buf.Discard) + } + return nil +} + +func (m *ClientWorker) handleStatusNew(meta *FrameMetadata, reader *buf.BufferedReader) error { + if meta.Option.Has(OptionData) { + return buf.Copy(NewStreamReader(reader), buf.Discard) + } + return nil +} + +func (m *ClientWorker) handleStatusKeep(meta *FrameMetadata, reader *buf.BufferedReader) error { + if !meta.Option.Has(OptionData) { + return nil + } + + s, found := m.sessionManager.Get(meta.SessionID) + if !found { + // Notify remote peer to close this session. + closingWriter := NewResponseWriter(meta.SessionID, m.link.Writer, protocol.TransferTypeStream) + closingWriter.Close() + + return buf.Copy(NewStreamReader(reader), buf.Discard) + } + + rr := s.NewReader(reader, &meta.Target) + err := buf.Copy(rr, s.output) + if err != nil && buf.IsWriteError(err) { + errors.LogInfoInner(context.Background(), err, "failed to write to downstream. closing session ", s.ID) + s.Close(false) + return buf.Copy(rr, buf.Discard) + } + + return err +} + +func (m *ClientWorker) handleStatusEnd(meta *FrameMetadata, reader *buf.BufferedReader) error { + if s, found := m.sessionManager.Get(meta.SessionID); found { + s.Close(false) + } + if meta.Option.Has(OptionData) { + return buf.Copy(NewStreamReader(reader), buf.Discard) + } + return nil +} + +func (m *ClientWorker) fetchOutput() { + defer func() { + common.Must(m.done.Close()) + }() + + reader := &buf.BufferedReader{Reader: m.link.Reader} + + var meta FrameMetadata + for { + err := meta.Unmarshal(reader, false) + if err != nil { + if errors.Cause(err) != io.EOF { + errors.LogInfoInner(context.Background(), err, "failed to read metadata") + } + break + } + + switch meta.SessionStatus { + case SessionStatusKeepAlive: + err = m.handleStatueKeepAlive(&meta, reader) + case SessionStatusEnd: + err = m.handleStatusEnd(&meta, reader) + case SessionStatusNew: + err = m.handleStatusNew(&meta, reader) + case SessionStatusKeep: + err = m.handleStatusKeep(&meta, reader) + default: + status := meta.SessionStatus + errors.LogError(context.Background(), "unknown status: ", status) + return + } + + if err != nil { + errors.LogInfoInner(context.Background(), err, "failed to process data") + return + } + } +} diff --git a/subproject/Xray-core-main/common/mux/client_test.go b/subproject/Xray-core-main/common/mux/client_test.go new file mode 100644 index 00000000..9626e2a2 --- /dev/null +++ b/subproject/Xray-core-main/common/mux/client_test.go @@ -0,0 +1,116 @@ +package mux_test + +import ( + "context" + "testing" + "time" + + "github.com/golang/mock/gomock" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/mux" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/testing/mocks" + "github.com/xtls/xray-core/transport" + "github.com/xtls/xray-core/transport/pipe" +) + +func TestIncrementalPickerFailure(t *testing.T) { + mockCtl := gomock.NewController(t) + defer mockCtl.Finish() + + mockWorkerFactory := mocks.NewMuxClientWorkerFactory(mockCtl) + mockWorkerFactory.EXPECT().Create().Return(nil, errors.New("test")) + + picker := mux.IncrementalWorkerPicker{ + Factory: mockWorkerFactory, + } + + _, err := picker.PickAvailable() + if err == nil { + t.Error("expected error, but nil") + } +} + +func TestClientWorkerEOF(t *testing.T) { + reader, writer := pipe.New(pipe.WithoutSizeLimit()) + common.Must(writer.Close()) + + worker, err := mux.NewClientWorker(transport.Link{Reader: reader, Writer: writer}, mux.ClientStrategy{}) + common.Must(err) + + time.Sleep(time.Millisecond * 500) + + f := worker.Dispatch(context.Background(), nil) + if f { + t.Error("expected failed dispatching, but actually not") + } +} + +func TestClientWorkerClose(t *testing.T) { + mockCtl := gomock.NewController(t) + defer mockCtl.Finish() + + r1, w1 := pipe.New(pipe.WithoutSizeLimit()) + worker1, err := mux.NewClientWorker(transport.Link{ + Reader: r1, + Writer: w1, + }, mux.ClientStrategy{ + MaxConcurrency: 4, + MaxConnection: 4, + }) + common.Must(err) + + r2, w2 := pipe.New(pipe.WithoutSizeLimit()) + worker2, err := mux.NewClientWorker(transport.Link{ + Reader: r2, + Writer: w2, + }, mux.ClientStrategy{ + MaxConcurrency: 4, + MaxConnection: 4, + }) + common.Must(err) + + factory := mocks.NewMuxClientWorkerFactory(mockCtl) + gomock.InOrder( + factory.EXPECT().Create().Return(worker1, nil), + factory.EXPECT().Create().Return(worker2, nil), + ) + + picker := &mux.IncrementalWorkerPicker{ + Factory: factory, + } + manager := &mux.ClientManager{ + Picker: picker, + } + + tr1, tw1 := pipe.New(pipe.WithoutSizeLimit()) + ctx1 := session.ContextWithOutbounds(context.Background(), []*session.Outbound{{ + Target: net.TCPDestination(net.DomainAddress("www.example.com"), 80), + }}) + common.Must(manager.Dispatch(ctx1, &transport.Link{ + Reader: tr1, + Writer: tw1, + })) + defer tw1.Close() + + common.Must(w1.Close()) + + time.Sleep(time.Millisecond * 500) + if !worker1.Closed() { + t.Error("worker1 is not finished") + } + + tr2, tw2 := pipe.New(pipe.WithoutSizeLimit()) + ctx2 := session.ContextWithOutbounds(context.Background(), []*session.Outbound{{ + Target: net.TCPDestination(net.DomainAddress("www.example.com"), 80), + }}) + common.Must(manager.Dispatch(ctx2, &transport.Link{ + Reader: tr2, + Writer: tw2, + })) + defer tw2.Close() + + common.Must(w2.Close()) +} diff --git a/subproject/Xray-core-main/common/mux/frame.go b/subproject/Xray-core-main/common/mux/frame.go new file mode 100644 index 00000000..f248fbdf --- /dev/null +++ b/subproject/Xray-core-main/common/mux/frame.go @@ -0,0 +1,222 @@ +package mux + +import ( + "encoding/binary" + "io" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/bitmask" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/serial" + "github.com/xtls/xray-core/common/session" +) + +type SessionStatus byte + +const ( + SessionStatusNew SessionStatus = 0x01 + SessionStatusKeep SessionStatus = 0x02 + SessionStatusEnd SessionStatus = 0x03 + SessionStatusKeepAlive SessionStatus = 0x04 +) + +const ( + OptionData bitmask.Byte = 0x01 + OptionError bitmask.Byte = 0x02 +) + +type TargetNetwork byte + +const ( + TargetNetworkTCP TargetNetwork = 0x01 + TargetNetworkUDP TargetNetwork = 0x02 +) + +var addrParser = protocol.NewAddressParser( + protocol.AddressFamilyByte(byte(protocol.AddressTypeIPv4), net.AddressFamilyIPv4), + protocol.AddressFamilyByte(byte(protocol.AddressTypeDomain), net.AddressFamilyDomain), + protocol.AddressFamilyByte(byte(protocol.AddressTypeIPv6), net.AddressFamilyIPv6), + protocol.PortThenAddress(), +) + +/* +Frame format +2 bytes - length +2 bytes - session id +1 bytes - status +1 bytes - option + +1 byte - network +2 bytes - port +n bytes - address + +*/ + +type FrameMetadata struct { + Target net.Destination + SessionID uint16 + Option bitmask.Byte + SessionStatus SessionStatus + GlobalID [8]byte + Inbound *session.Inbound +} + +func (f FrameMetadata) WriteTo(b *buf.Buffer) error { + lenBytes := b.Extend(2) + + len0 := b.Len() + sessionBytes := b.Extend(2) + binary.BigEndian.PutUint16(sessionBytes, f.SessionID) + + common.Must(b.WriteByte(byte(f.SessionStatus))) + common.Must(b.WriteByte(byte(f.Option))) + + if f.SessionStatus == SessionStatusNew { + switch f.Target.Network { + case net.Network_TCP: + common.Must(b.WriteByte(byte(TargetNetworkTCP))) + case net.Network_UDP: + common.Must(b.WriteByte(byte(TargetNetworkUDP))) + } + if err := addrParser.WriteAddressPort(b, f.Target.Address, f.Target.Port); err != nil { + return err + } + if f.Inbound != nil { + if f.Inbound.Source.Network == net.Network_TCP || f.Inbound.Source.Network == net.Network_UDP { + common.Must(b.WriteByte(byte(f.Inbound.Source.Network - 1))) + if err := addrParser.WriteAddressPort(b, f.Inbound.Source.Address, f.Inbound.Source.Port); err != nil { + return err + } + if f.Inbound.Local.Network == net.Network_TCP || f.Inbound.Local.Network == net.Network_UDP { + common.Must(b.WriteByte(byte(f.Inbound.Local.Network - 1))) + if err := addrParser.WriteAddressPort(b, f.Inbound.Local.Address, f.Inbound.Local.Port); err != nil { + return err + } + } + } + } else if b.UDP != nil { // make sure it's user's proxy request + b.Write(f.GlobalID[:]) // no need to check whether it's empty + } + } else if b.UDP != nil { + b.WriteByte(byte(TargetNetworkUDP)) + addrParser.WriteAddressPort(b, b.UDP.Address, b.UDP.Port) + } + + len1 := b.Len() + binary.BigEndian.PutUint16(lenBytes, uint16(len1-len0)) + return nil +} + +// Unmarshal reads FrameMetadata from the given reader. +func (f *FrameMetadata) Unmarshal(reader io.Reader, readSourceAndLocal bool) error { + metaLen, err := serial.ReadUint16(reader) + if err != nil { + return err + } + if metaLen > 512 { + return errors.New("invalid metalen ", metaLen).AtError() + } + + b := buf.New() + defer b.Release() + + if _, err := b.ReadFullFrom(reader, int32(metaLen)); err != nil { + return err + } + return f.UnmarshalFromBuffer(b, readSourceAndLocal) +} + +// UnmarshalFromBuffer reads a FrameMetadata from the given buffer. +// Visible for testing only. +func (f *FrameMetadata) UnmarshalFromBuffer(b *buf.Buffer, readSourceAndLocal bool) error { + if b.Len() < 4 { + return errors.New("insufficient buffer: ", b.Len()) + } + + f.SessionID = binary.BigEndian.Uint16(b.BytesTo(2)) + f.SessionStatus = SessionStatus(b.Byte(2)) + f.Option = bitmask.Byte(b.Byte(3)) + f.Target.Network = net.Network_Unknown + + if f.SessionStatus == SessionStatusNew || (f.SessionStatus == SessionStatusKeep && b.Len() > 4 && + TargetNetwork(b.Byte(4)) == TargetNetworkUDP) { // MUST check the flag first + if b.Len() < 8 { + return errors.New("insufficient buffer: ", b.Len()) + } + network := TargetNetwork(b.Byte(4)) + b.Advance(5) + + addr, port, err := addrParser.ReadAddressPort(nil, b) + if err != nil { + return errors.New("failed to parse address and port").Base(err) + } + + switch network { + case TargetNetworkTCP: + f.Target = net.TCPDestination(addr, port) + case TargetNetworkUDP: + f.Target = net.UDPDestination(addr, port) + default: + return errors.New("unknown network type: ", network) + } + } + + if f.SessionStatus == SessionStatusNew && readSourceAndLocal { + f.Inbound = &session.Inbound{} + + if b.Len() == 0 { + return nil // for heartbeat, etc. + } + network := TargetNetwork(b.Byte(0)) + if network == 0 { + return nil // may be padding + } + b.Advance(1) + addr, port, err := addrParser.ReadAddressPort(nil, b) + if err != nil { + return errors.New("reading source: failed to parse address and port").Base(err) + } + switch network { + case TargetNetworkTCP: + f.Inbound.Source = net.TCPDestination(addr, port) + case TargetNetworkUDP: + f.Inbound.Source = net.UDPDestination(addr, port) + default: + return errors.New("reading source: unknown network type: ", network) + } + + if b.Len() == 0 { + return nil + } + network = TargetNetwork(b.Byte(0)) + if network == 0 { + return nil + } + b.Advance(1) + addr, port, err = addrParser.ReadAddressPort(nil, b) + if err != nil { + return errors.New("reading local: failed to parse address and port").Base(err) + } + switch network { + case TargetNetworkTCP: + f.Inbound.Local = net.TCPDestination(addr, port) + case TargetNetworkUDP: + f.Inbound.Local = net.UDPDestination(addr, port) + default: + return errors.New("reading local: unknown network type: ", network) + } + + return nil + } + + // Application data is essential, to test whether the pipe is closed. + if f.SessionStatus == SessionStatusNew && f.Option.Has(OptionData) && + f.Target.Network == net.Network_UDP && b.Len() >= 8 { + copy(f.GlobalID[:], b.Bytes()) + } + + return nil +} diff --git a/subproject/Xray-core-main/common/mux/frame_test.go b/subproject/Xray-core-main/common/mux/frame_test.go new file mode 100644 index 00000000..e6d54ff1 --- /dev/null +++ b/subproject/Xray-core-main/common/mux/frame_test.go @@ -0,0 +1,25 @@ +package mux_test + +import ( + "testing" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/mux" + "github.com/xtls/xray-core/common/net" +) + +func BenchmarkFrameWrite(b *testing.B) { + frame := mux.FrameMetadata{ + Target: net.TCPDestination(net.DomainAddress("www.example.com"), net.Port(80)), + SessionID: 1, + SessionStatus: mux.SessionStatusNew, + } + writer := buf.New() + defer writer.Release() + + for i := 0; i < b.N; i++ { + common.Must(frame.WriteTo(writer)) + writer.Clear() + } +} diff --git a/subproject/Xray-core-main/common/mux/mux.go b/subproject/Xray-core-main/common/mux/mux.go new file mode 100644 index 00000000..707f4622 --- /dev/null +++ b/subproject/Xray-core-main/common/mux/mux.go @@ -0,0 +1 @@ +package mux diff --git a/subproject/Xray-core-main/common/mux/mux_test.go b/subproject/Xray-core-main/common/mux/mux_test.go new file mode 100644 index 00000000..db01d372 --- /dev/null +++ b/subproject/Xray-core-main/common/mux/mux_test.go @@ -0,0 +1,196 @@ +package mux_test + +import ( + "io" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + . "github.com/xtls/xray-core/common/mux" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/transport/pipe" +) + +func readAll(reader buf.Reader) (buf.MultiBuffer, error) { + var mb buf.MultiBuffer + for { + b, err := reader.ReadMultiBuffer() + if err == io.EOF { + break + } + if err != nil { + return nil, err + } + mb = append(mb, b...) + } + return mb, nil +} + +func TestReaderWriter(t *testing.T) { + pReader, pWriter := pipe.New(pipe.WithSizeLimit(1024)) + + dest := net.TCPDestination(net.DomainAddress("example.com"), 80) + writer := NewWriter(1, dest, pWriter, protocol.TransferTypeStream, [8]byte{}, &session.Inbound{}) + + dest2 := net.TCPDestination(net.LocalHostIP, 443) + writer2 := NewWriter(2, dest2, pWriter, protocol.TransferTypeStream, [8]byte{}, &session.Inbound{}) + + dest3 := net.TCPDestination(net.LocalHostIPv6, 18374) + writer3 := NewWriter(3, dest3, pWriter, protocol.TransferTypeStream, [8]byte{}, &session.Inbound{}) + + writePayload := func(writer *Writer, payload ...byte) error { + b := buf.New() + b.Write(payload) + return writer.WriteMultiBuffer(buf.MultiBuffer{b}) + } + + common.Must(writePayload(writer, 'a', 'b', 'c', 'd')) + common.Must(writePayload(writer2)) + + common.Must(writePayload(writer, 'e', 'f', 'g', 'h')) + common.Must(writePayload(writer3, 'x')) + + writer.Close() + writer3.Close() + + common.Must(writePayload(writer2, 'y')) + writer2.Close() + + bytesReader := &buf.BufferedReader{Reader: pReader} + + { + var meta FrameMetadata + common.Must(meta.Unmarshal(bytesReader, false)) + if r := cmp.Diff(meta, FrameMetadata{ + SessionID: 1, + SessionStatus: SessionStatusNew, + Target: dest, + Option: OptionData, + }); r != "" { + t.Error("metadata: ", r) + } + + data, err := readAll(NewStreamReader(bytesReader)) + common.Must(err) + if s := data.String(); s != "abcd" { + t.Error("data: ", s) + } + } + + { + var meta FrameMetadata + common.Must(meta.Unmarshal(bytesReader, false)) + if r := cmp.Diff(meta, FrameMetadata{ + SessionStatus: SessionStatusNew, + SessionID: 2, + Option: 0, + Target: dest2, + }); r != "" { + t.Error("meta: ", r) + } + } + + { + var meta FrameMetadata + common.Must(meta.Unmarshal(bytesReader, false)) + if r := cmp.Diff(meta, FrameMetadata{ + SessionID: 1, + SessionStatus: SessionStatusKeep, + Option: 1, + }); r != "" { + t.Error("meta: ", r) + } + + data, err := readAll(NewStreamReader(bytesReader)) + common.Must(err) + if s := data.String(); s != "efgh" { + t.Error("data: ", s) + } + } + + { + var meta FrameMetadata + common.Must(meta.Unmarshal(bytesReader, false)) + if r := cmp.Diff(meta, FrameMetadata{ + SessionID: 3, + SessionStatus: SessionStatusNew, + Option: 1, + Target: dest3, + }); r != "" { + t.Error("meta: ", r) + } + + data, err := readAll(NewStreamReader(bytesReader)) + common.Must(err) + if s := data.String(); s != "x" { + t.Error("data: ", s) + } + } + + { + var meta FrameMetadata + common.Must(meta.Unmarshal(bytesReader, false)) + if r := cmp.Diff(meta, FrameMetadata{ + SessionID: 1, + SessionStatus: SessionStatusEnd, + Option: 0, + }); r != "" { + t.Error("meta: ", r) + } + } + + { + var meta FrameMetadata + common.Must(meta.Unmarshal(bytesReader, false)) + if r := cmp.Diff(meta, FrameMetadata{ + SessionID: 3, + SessionStatus: SessionStatusEnd, + Option: 0, + }); r != "" { + t.Error("meta: ", r) + } + } + + { + var meta FrameMetadata + common.Must(meta.Unmarshal(bytesReader, false)) + if r := cmp.Diff(meta, FrameMetadata{ + SessionID: 2, + SessionStatus: SessionStatusKeep, + Option: 1, + }); r != "" { + t.Error("meta: ", r) + } + + data, err := readAll(NewStreamReader(bytesReader)) + common.Must(err) + if s := data.String(); s != "y" { + t.Error("data: ", s) + } + } + + { + var meta FrameMetadata + common.Must(meta.Unmarshal(bytesReader, false)) + if r := cmp.Diff(meta, FrameMetadata{ + SessionID: 2, + SessionStatus: SessionStatusEnd, + Option: 0, + }); r != "" { + t.Error("meta: ", r) + } + } + + pWriter.Close() + + { + var meta FrameMetadata + err := meta.Unmarshal(bytesReader, false) + if err == nil { + t.Error("nil error") + } + } +} diff --git a/subproject/Xray-core-main/common/mux/reader.go b/subproject/Xray-core-main/common/mux/reader.go new file mode 100644 index 00000000..b9714cdf --- /dev/null +++ b/subproject/Xray-core-main/common/mux/reader.go @@ -0,0 +1,59 @@ +package mux + +import ( + "io" + + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/crypto" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/serial" +) + +// PacketReader is an io.Reader that reads whole chunk of Mux frames every time. +type PacketReader struct { + reader io.Reader + eof bool + dest *net.Destination +} + +// NewPacketReader creates a new PacketReader. +func NewPacketReader(reader io.Reader, dest *net.Destination) *PacketReader { + return &PacketReader{ + reader: reader, + eof: false, + dest: dest, + } +} + +// ReadMultiBuffer implements buf.Reader. +func (r *PacketReader) ReadMultiBuffer() (buf.MultiBuffer, error) { + if r.eof { + return nil, io.EOF + } + + size, err := serial.ReadUint16(r.reader) + if err != nil { + return nil, err + } + + if size > buf.Size { + return nil, errors.New("packet size too large: ", size) + } + + b := buf.New() + if _, err := b.ReadFullFrom(r.reader, int32(size)); err != nil { + b.Release() + return nil, err + } + r.eof = true + if r.dest != nil && r.dest.Network == net.Network_UDP { + b.UDP = r.dest + } + return buf.MultiBuffer{b}, nil +} + +// NewStreamReader creates a new StreamReader. +func NewStreamReader(reader *buf.BufferedReader) buf.Reader { + return crypto.NewChunkStreamReaderWithChunkCount(crypto.PlainChunkSizeParser{}, reader, 1) +} diff --git a/subproject/Xray-core-main/common/mux/server.go b/subproject/Xray-core-main/common/mux/server.go new file mode 100644 index 00000000..d1cdac11 --- /dev/null +++ b/subproject/Xray-core-main/common/mux/server.go @@ -0,0 +1,384 @@ +package mux + +import ( + "context" + "io" + "time" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/log" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/common/signal/done" + "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/features/routing" + "github.com/xtls/xray-core/transport" + "github.com/xtls/xray-core/transport/pipe" +) + +type Server struct { + dispatcher routing.Dispatcher +} + +// NewServer creates a new mux.Server. +func NewServer(ctx context.Context) *Server { + s := &Server{} + core.RequireFeatures(ctx, func(d routing.Dispatcher) { + s.dispatcher = d + }) + return s +} + +// Type implements common.HasType. +func (s *Server) Type() interface{} { + return s.dispatcher.Type() +} + +// Dispatch implements routing.Dispatcher +func (s *Server) Dispatch(ctx context.Context, dest net.Destination) (*transport.Link, error) { + if dest.Address != muxCoolAddress { + return s.dispatcher.Dispatch(ctx, dest) + } + + opts := pipe.OptionsFromContext(ctx) + uplinkReader, uplinkWriter := pipe.New(opts...) + downlinkReader, downlinkWriter := pipe.New(opts...) + + _, err := NewServerWorker(ctx, s.dispatcher, &transport.Link{ + Reader: uplinkReader, + Writer: downlinkWriter, + }) + if err != nil { + return nil, err + } + + return &transport.Link{Reader: downlinkReader, Writer: uplinkWriter}, nil +} + +// DispatchLink implements routing.Dispatcher +func (s *Server) DispatchLink(ctx context.Context, dest net.Destination, link *transport.Link) error { + if dest.Address != muxCoolAddress { + return s.dispatcher.DispatchLink(ctx, dest, link) + } + worker, err := NewServerWorker(ctx, s.dispatcher, link) + if err != nil { + return err + } + select { + case <-ctx.Done(): + case <-worker.done.Wait(): + } + return nil +} + +// Start implements common.Runnable. +func (s *Server) Start() error { + return nil +} + +// Close implements common.Closable. +func (s *Server) Close() error { + return nil +} + +type ServerWorker struct { + dispatcher routing.Dispatcher + link *transport.Link + sessionManager *SessionManager + done *done.Instance + timer *time.Ticker +} + +func NewServerWorker(ctx context.Context, d routing.Dispatcher, link *transport.Link) (*ServerWorker, error) { + worker := &ServerWorker{ + dispatcher: d, + link: link, + sessionManager: NewSessionManager(), + done: done.New(), + timer: time.NewTicker(60 * time.Second), + } + if inbound := session.InboundFromContext(ctx); inbound != nil { + inbound.CanSpliceCopy = 3 + } + go worker.run(ctx) + go worker.monitor() + return worker, nil +} + +func handle(ctx context.Context, s *Session, output buf.Writer) { + writer := NewResponseWriter(s.ID, output, s.transferType) + if err := buf.Copy(s.input, writer); err != nil { + errors.LogInfoInner(ctx, err, "session ", s.ID, " ends.") + writer.hasError = true + } + + writer.Close() + s.Close(false) +} + +func (w *ServerWorker) monitor() { + defer w.timer.Stop() + + for { + checkSize := w.sessionManager.Size() + checkCount := w.sessionManager.Count() + select { + case <-w.done.Wait(): + w.sessionManager.Close() + common.Interrupt(w.link.Writer) + common.Interrupt(w.link.Reader) + return + case <-w.timer.C: + if w.sessionManager.CloseIfNoSessionAndIdle(checkSize, checkCount) { + common.Must(w.done.Close()) + } + } + } +} + +func (w *ServerWorker) ActiveConnections() uint32 { + return uint32(w.sessionManager.Size()) +} + +func (w *ServerWorker) Closed() bool { + return w.done.Done() +} + +func (w *ServerWorker) WaitClosed() <-chan struct{} { + return w.done.Wait() +} + +func (w *ServerWorker) Close() error { + return w.done.Close() +} + +func (w *ServerWorker) handleStatusKeepAlive(meta *FrameMetadata, reader *buf.BufferedReader) error { + if meta.Option.Has(OptionData) { + return buf.Copy(NewStreamReader(reader), buf.Discard) + } + return nil +} + +func (w *ServerWorker) handleStatusNew(ctx context.Context, meta *FrameMetadata, reader *buf.BufferedReader) error { + ctx = session.SubContextFromMuxInbound(ctx) + if meta.Inbound != nil && meta.Inbound.Source.IsValid() && meta.Inbound.Local.IsValid() { + if inbound := session.InboundFromContext(ctx); inbound != nil { + newInbound := *inbound + newInbound.Source = meta.Inbound.Source + newInbound.Local = meta.Inbound.Local + ctx = session.ContextWithInbound(ctx, &newInbound) + } + } + errors.LogInfo(ctx, "received request for ", meta.Target) + { + msg := &log.AccessMessage{ + To: meta.Target, + Status: log.AccessAccepted, + Reason: "", + } + if inbound := session.InboundFromContext(ctx); inbound != nil && inbound.Source.IsValid() { + msg.From = inbound.Source + msg.Email = inbound.User.Email + } + ctx = log.ContextWithAccessMessage(ctx, msg) + } + + if network := session.AllowedNetworkFromContext(ctx); network != net.Network_Unknown { + if meta.Target.Network != network { + return errors.New("unexpected network ", meta.Target.Network) // it will break the whole Mux connection + } + } + + if meta.GlobalID != [8]byte{} { // MUST ignore empty Global ID + mb, err := NewPacketReader(reader, &meta.Target).ReadMultiBuffer() + if err != nil { + return err + } + XUDPManager.Lock() + x := XUDPManager.Map[meta.GlobalID] + if x == nil { + x = &XUDP{GlobalID: meta.GlobalID} + XUDPManager.Map[meta.GlobalID] = x + XUDPManager.Unlock() + } else { + if x.Status == Initializing { // nearly impossible + XUDPManager.Unlock() + errors.LogWarningInner(ctx, errors.New("conflict"), "XUDP hit ", meta.GlobalID) + // It's not a good idea to return an err here, so just let client wait. + // Client will receive an End frame after sending a Keep frame. + return nil + } + x.Status = Initializing + XUDPManager.Unlock() + x.Mux.Close(false) // detach from previous Mux + b := buf.New() + b.Write(mb[0].Bytes()) + b.UDP = mb[0].UDP + if err = x.Mux.output.WriteMultiBuffer(mb); err != nil { + x.Interrupt() + mb = buf.MultiBuffer{b} + } else { + b.Release() + mb = nil + } + errors.LogInfoInner(ctx, err, "XUDP hit ", meta.GlobalID) + } + if mb != nil { + ctx = session.ContextWithTimeoutOnly(ctx, true) + // Actually, it won't return an error in Xray-core's implementations. + link, err := w.dispatcher.Dispatch(ctx, meta.Target) + if err != nil { + XUDPManager.Lock() + delete(XUDPManager.Map, x.GlobalID) + XUDPManager.Unlock() + err = errors.New("XUDP new ", meta.GlobalID).Base(errors.New("failed to dispatch request to ", meta.Target).Base(err)) + return err // it will break the whole Mux connection + } + link.Writer.WriteMultiBuffer(mb) // it's meaningless to test a new pipe + x.Mux = &Session{ + input: link.Reader, + output: link.Writer, + } + errors.LogInfoInner(ctx, err, "XUDP new ", meta.GlobalID) + } + x.Mux = &Session{ + input: x.Mux.input, + output: x.Mux.output, + parent: w.sessionManager, + ID: meta.SessionID, + transferType: protocol.TransferTypePacket, + XUDP: x, + } + x.Status = Active + if !w.sessionManager.Add(x.Mux) { + x.Mux.Close(false) + return errors.New("failed to add new session") + } + go handle(ctx, x.Mux, w.link.Writer) + return nil + } + + link, err := w.dispatcher.Dispatch(ctx, meta.Target) + if err != nil { + if meta.Option.Has(OptionData) { + buf.Copy(NewStreamReader(reader), buf.Discard) + } + return errors.New("failed to dispatch request.").Base(err) + } + s := &Session{ + input: link.Reader, + output: link.Writer, + parent: w.sessionManager, + ID: meta.SessionID, + transferType: protocol.TransferTypeStream, + } + if meta.Target.Network == net.Network_UDP { + s.transferType = protocol.TransferTypePacket + } + if !w.sessionManager.Add(s) { + s.Close(false) + return errors.New("failed to add new session") + } + go handle(ctx, s, w.link.Writer) + if !meta.Option.Has(OptionData) { + return nil + } + + rr := s.NewReader(reader, &meta.Target) + err = buf.Copy(rr, s.output) + + if err != nil && buf.IsWriteError(err) { + s.Close(false) + return buf.Copy(rr, buf.Discard) + } + return err +} + +func (w *ServerWorker) handleStatusKeep(meta *FrameMetadata, reader *buf.BufferedReader) error { + if !meta.Option.Has(OptionData) { + return nil + } + + s, found := w.sessionManager.Get(meta.SessionID) + if !found { + // Notify remote peer to close this session. + closingWriter := NewResponseWriter(meta.SessionID, w.link.Writer, protocol.TransferTypeStream) + closingWriter.Close() + + return buf.Copy(NewStreamReader(reader), buf.Discard) + } + + rr := s.NewReader(reader, &meta.Target) + err := buf.Copy(rr, s.output) + + if err != nil && buf.IsWriteError(err) { + errors.LogInfoInner(context.Background(), err, "failed to write to downstream writer. closing session ", s.ID) + s.Close(false) + return buf.Copy(rr, buf.Discard) + } + + return err +} + +func (w *ServerWorker) handleStatusEnd(meta *FrameMetadata, reader *buf.BufferedReader) error { + if s, found := w.sessionManager.Get(meta.SessionID); found { + s.Close(false) + } + if meta.Option.Has(OptionData) { + return buf.Copy(NewStreamReader(reader), buf.Discard) + } + return nil +} + +func (w *ServerWorker) handleFrame(ctx context.Context, reader *buf.BufferedReader) error { + var meta FrameMetadata + err := meta.Unmarshal(reader, session.IsReverseMuxFromContext(ctx)) + if err != nil { + return errors.New("failed to read metadata").Base(err) + } + + switch meta.SessionStatus { + case SessionStatusKeepAlive: + err = w.handleStatusKeepAlive(&meta, reader) + case SessionStatusEnd: + err = w.handleStatusEnd(&meta, reader) + case SessionStatusNew: + err = w.handleStatusNew(session.ContextWithIsReverseMux(ctx, false), &meta, reader) + case SessionStatusKeep: + err = w.handleStatusKeep(&meta, reader) + default: + status := meta.SessionStatus + return errors.New("unknown status: ", status).AtError() + } + + if err != nil { + return errors.New("failed to process data").Base(err) + } + return nil +} + +func (w *ServerWorker) run(ctx context.Context) { + defer func() { + common.Must(w.done.Close()) + }() + + reader := &buf.BufferedReader{Reader: w.link.Reader} + + for { + select { + case <-ctx.Done(): + return + default: + err := w.handleFrame(ctx, reader) + if err != nil { + if errors.Cause(err) != io.EOF { + errors.LogInfoInner(ctx, err, "unexpected EOF") + } + return + } + } + } +} diff --git a/subproject/Xray-core-main/common/mux/server_test.go b/subproject/Xray-core-main/common/mux/server_test.go new file mode 100644 index 00000000..4158bf46 --- /dev/null +++ b/subproject/Xray-core-main/common/mux/server_test.go @@ -0,0 +1,124 @@ +package mux_test + +import ( + "context" + "testing" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/mux" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/features/routing" + "github.com/xtls/xray-core/transport" + "github.com/xtls/xray-core/transport/pipe" +) + +func newLinkPair() (*transport.Link, *transport.Link) { + opt := pipe.WithoutSizeLimit() + uplinkReader, uplinkWriter := pipe.New(opt) + downlinkReader, downlinkWriter := pipe.New(opt) + + uplink := &transport.Link{ + Reader: uplinkReader, + Writer: downlinkWriter, + } + + downlink := &transport.Link{ + Reader: downlinkReader, + Writer: uplinkWriter, + } + + return uplink, downlink +} + +type TestDispatcher struct { + OnDispatch func(ctx context.Context, dest net.Destination) (*transport.Link, error) +} + +func (d *TestDispatcher) Dispatch(ctx context.Context, dest net.Destination) (*transport.Link, error) { + return d.OnDispatch(ctx, dest) +} + +func (d *TestDispatcher) DispatchLink(ctx context.Context, destination net.Destination, outbound *transport.Link) error { + return nil +} + +func (d *TestDispatcher) Start() error { + return nil +} + +func (d *TestDispatcher) Close() error { + return nil +} + +func (*TestDispatcher) Type() interface{} { + return routing.DispatcherType() +} + +func TestRegressionOutboundLeak(t *testing.T) { + originalOutbounds := []*session.Outbound{{}} + serverCtx := session.ContextWithOutbounds(context.Background(), originalOutbounds) + + websiteUplink, websiteDownlink := newLinkPair() + + dispatcher := TestDispatcher{ + OnDispatch: func(ctx context.Context, dest net.Destination) (*transport.Link, error) { + // emulate what DefaultRouter.Dispatch does, and mutate something on the context + ob := session.OutboundsFromContext(ctx)[0] + ob.Target = dest + return websiteDownlink, nil + }, + } + + muxServerUplink, muxServerDownlink := newLinkPair() + _, err := mux.NewServerWorker(serverCtx, &dispatcher, muxServerUplink) + common.Must(err) + + client, err := mux.NewClientWorker(*muxServerDownlink, mux.ClientStrategy{}) + common.Must(err) + + clientCtx := session.ContextWithOutbounds(context.Background(), []*session.Outbound{{ + Target: net.TCPDestination(net.DomainAddress("www.example.com"), 80), + }}) + + muxClientUplink, muxClientDownlink := newLinkPair() + + ok := client.Dispatch(clientCtx, muxClientUplink) + if !ok { + t.Error("failed to dispatch") + } + + { + b := buf.FromBytes([]byte("hello")) + common.Must(muxClientDownlink.Writer.WriteMultiBuffer(buf.MultiBuffer{b})) + } + + resMb, err := websiteUplink.Reader.ReadMultiBuffer() + common.Must(err) + res := resMb.String() + if res != "hello" { + t.Error("upload: ", res) + } + + { + b := buf.FromBytes([]byte("world")) + common.Must(websiteUplink.Writer.WriteMultiBuffer(buf.MultiBuffer{b})) + } + + resMb, err = muxClientDownlink.Reader.ReadMultiBuffer() + common.Must(err) + res = resMb.String() + if res != "world" { + t.Error("download: ", res) + } + + outbounds := session.OutboundsFromContext(serverCtx) + if outbounds[0] != originalOutbounds[0] { + t.Error("outbound got reassigned: ", outbounds[0]) + } + + if outbounds[0].Target.Address != nil { + t.Error("outbound target got leaked: ", outbounds[0].Target.String()) + } +} diff --git a/subproject/Xray-core-main/common/mux/session.go b/subproject/Xray-core-main/common/mux/session.go new file mode 100644 index 00000000..66b9674c --- /dev/null +++ b/subproject/Xray-core-main/common/mux/session.go @@ -0,0 +1,252 @@ +package mux + +import ( + "context" + "io" + "runtime" + "sync" + "time" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/signal/done" + "github.com/xtls/xray-core/transport/pipe" +) + +type SessionManager struct { + sync.RWMutex + sessions map[uint16]*Session + count uint16 + closed bool +} + +func NewSessionManager() *SessionManager { + return &SessionManager{ + count: 0, + sessions: make(map[uint16]*Session, 16), + } +} + +func (m *SessionManager) Closed() bool { + m.RLock() + defer m.RUnlock() + + return m.closed +} + +func (m *SessionManager) Size() int { + m.RLock() + defer m.RUnlock() + + return len(m.sessions) +} + +func (m *SessionManager) Count() int { + m.RLock() + defer m.RUnlock() + + return int(m.count) +} + +func (m *SessionManager) Allocate(Strategy *ClientStrategy) *Session { + m.Lock() + defer m.Unlock() + + MaxConcurrency := int(Strategy.MaxConcurrency) + MaxConnection := uint16(Strategy.MaxConnection) + + if m.closed || (MaxConcurrency > 0 && len(m.sessions) >= MaxConcurrency) || (MaxConnection > 0 && m.count >= MaxConnection) { + return nil + } + + m.count++ + s := &Session{ + ID: m.count, + parent: m, + done: done.New(), + } + m.sessions[s.ID] = s + return s +} + +func (m *SessionManager) Add(s *Session) bool { + m.Lock() + defer m.Unlock() + + if m.closed { + return false + } + + m.count++ + m.sessions[s.ID] = s + return true +} + +func (m *SessionManager) Remove(locked bool, id uint16) { + if !locked { + m.Lock() + defer m.Unlock() + } + locked = true + + if m.closed { + return + } + + delete(m.sessions, id) + + /* + if len(m.sessions) == 0 { + m.sessions = make(map[uint16]*Session, 16) + } + */ +} + +func (m *SessionManager) Get(id uint16) (*Session, bool) { + m.RLock() + defer m.RUnlock() + + if m.closed { + return nil, false + } + + s, found := m.sessions[id] + return s, found +} + +func (m *SessionManager) CloseIfNoSessionAndIdle(checkSize int, checkCount int) bool { + m.Lock() + defer m.Unlock() + + if m.closed { + return true + } + + if len(m.sessions) != 0 || checkSize != 0 || checkCount != int(m.count) { + return false + } + + m.closed = true + + m.sessions = nil + return true +} + +func (m *SessionManager) Close() error { + m.Lock() + defer m.Unlock() + + if m.closed { + return nil + } + + m.closed = true + + for _, s := range m.sessions { + s.Close(true) + } + + m.sessions = nil + return nil +} + +// Session represents a client connection in a Mux connection. +type Session struct { + input buf.Reader + output buf.Writer + parent *SessionManager + ID uint16 + transferType protocol.TransferType + closed bool + done *done.Instance + XUDP *XUDP +} + +// Close closes all resources associated with this session. +func (s *Session) Close(locked bool) error { + if !locked { + s.parent.Lock() + defer s.parent.Unlock() + } + locked = true + if s.closed { + return nil + } + s.closed = true + if s.done != nil { + s.done.Close() + } + if s.XUDP == nil { + common.Interrupt(s.input) + common.Close(s.output) + } else { + // Stop existing handle(), then trigger writer.Close(). + // Note that s.output may be dispatcher.SizeStatWriter. + s.input.(*pipe.Reader).ReturnAnError(io.EOF) + runtime.Gosched() + // If the error set by ReturnAnError still exists, clear it. + s.input.(*pipe.Reader).Recover() + XUDPManager.Lock() + if s.XUDP.Status == Active { + s.XUDP.Expire = time.Now().Add(time.Minute) + s.XUDP.Status = Expiring + errors.LogDebug(context.Background(), "XUDP put ", s.XUDP.GlobalID) + } + XUDPManager.Unlock() + } + s.parent.Remove(locked, s.ID) + return nil +} + +// NewReader creates a buf.Reader based on the transfer type of this Session. +func (s *Session) NewReader(reader *buf.BufferedReader, dest *net.Destination) buf.Reader { + if s.transferType == protocol.TransferTypeStream { + return NewStreamReader(reader) + } + return NewPacketReader(reader, dest) +} + +const ( + Initializing = 0 + Active = 1 + Expiring = 2 +) + +type XUDP struct { + GlobalID [8]byte + Status uint64 + Expire time.Time + Mux *Session +} + +func (x *XUDP) Interrupt() { + common.Interrupt(x.Mux.input) + common.Close(x.Mux.output) +} + +var XUDPManager struct { + sync.Mutex + Map map[[8]byte]*XUDP +} + +func init() { + XUDPManager.Map = make(map[[8]byte]*XUDP) + go func() { + for { + time.Sleep(time.Minute) + now := time.Now() + XUDPManager.Lock() + for id, x := range XUDPManager.Map { + if x.Status == Expiring && now.After(x.Expire) { + x.Interrupt() + delete(XUDPManager.Map, id) + errors.LogDebug(context.Background(), "XUDP del ", id) + } + } + XUDPManager.Unlock() + } + }() +} diff --git a/subproject/Xray-core-main/common/mux/session_test.go b/subproject/Xray-core-main/common/mux/session_test.go new file mode 100644 index 00000000..8ef27877 --- /dev/null +++ b/subproject/Xray-core-main/common/mux/session_test.go @@ -0,0 +1,51 @@ +package mux_test + +import ( + "testing" + + . "github.com/xtls/xray-core/common/mux" +) + +func TestSessionManagerAdd(t *testing.T) { + m := NewSessionManager() + + s := m.Allocate(&ClientStrategy{}) + if s.ID != 1 { + t.Error("id: ", s.ID) + } + if m.Size() != 1 { + t.Error("size: ", m.Size()) + } + + s = m.Allocate(&ClientStrategy{}) + if s.ID != 2 { + t.Error("id: ", s.ID) + } + if m.Size() != 2 { + t.Error("size: ", m.Size()) + } + + s = &Session{ + ID: 4, + } + m.Add(s) + if s.ID != 4 { + t.Error("id: ", s.ID) + } + if m.Size() != 3 { + t.Error("size: ", m.Size()) + } +} + +func TestSessionManagerClose(t *testing.T) { + m := NewSessionManager() + s := m.Allocate(&ClientStrategy{}) + + if m.CloseIfNoSessionAndIdle(m.Size(), m.Count()) { + t.Error("able to close") + } + m.Remove(false, s.ID) + if !m.CloseIfNoSessionAndIdle(m.Size(), m.Count()) { + t.Error("not able to close") + } +} diff --git a/subproject/Xray-core-main/common/mux/writer.go b/subproject/Xray-core-main/common/mux/writer.go new file mode 100644 index 00000000..0429f4fa --- /dev/null +++ b/subproject/Xray-core-main/common/mux/writer.go @@ -0,0 +1,136 @@ +package mux + +import ( + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/serial" + "github.com/xtls/xray-core/common/session" +) + +type Writer struct { + dest net.Destination + writer buf.Writer + id uint16 + followup bool + hasError bool + transferType protocol.TransferType + globalID [8]byte + inbound *session.Inbound +} + +func NewWriter(id uint16, dest net.Destination, writer buf.Writer, transferType protocol.TransferType, globalID [8]byte, inbound *session.Inbound) *Writer { + return &Writer{ + id: id, + dest: dest, + writer: writer, + followup: false, + transferType: transferType, + globalID: globalID, + inbound: inbound, + } +} + +func NewResponseWriter(id uint16, writer buf.Writer, transferType protocol.TransferType) *Writer { + return &Writer{ + id: id, + writer: writer, + followup: true, + transferType: transferType, + } +} + +func (w *Writer) getNextFrameMeta() FrameMetadata { + meta := FrameMetadata{ + SessionID: w.id, + Target: w.dest, + GlobalID: w.globalID, + Inbound: w.inbound, + } + + if w.followup { + meta.SessionStatus = SessionStatusKeep + } else { + w.followup = true + meta.SessionStatus = SessionStatusNew + } + + return meta +} + +func (w *Writer) writeMetaOnly() error { + meta := w.getNextFrameMeta() + b := buf.New() + if err := meta.WriteTo(b); err != nil { + return err + } + return w.writer.WriteMultiBuffer(buf.MultiBuffer{b}) +} + +func writeMetaWithFrame(writer buf.Writer, meta FrameMetadata, data buf.MultiBuffer) error { + frame := buf.New() + if len(data) == 1 { + frame.UDP = data[0].UDP + } + if err := meta.WriteTo(frame); err != nil { + return err + } + if _, err := serial.WriteUint16(frame, uint16(data.Len())); err != nil { + return err + } + + mb2 := make(buf.MultiBuffer, 0, len(data)+1) + mb2 = append(mb2, frame) + mb2 = append(mb2, data...) + return writer.WriteMultiBuffer(mb2) +} + +func (w *Writer) writeData(mb buf.MultiBuffer) error { + meta := w.getNextFrameMeta() + meta.Option.Set(OptionData) + + return writeMetaWithFrame(w.writer, meta, mb) +} + +// WriteMultiBuffer implements buf.Writer. +func (w *Writer) WriteMultiBuffer(mb buf.MultiBuffer) error { + defer buf.ReleaseMulti(mb) + + if mb.IsEmpty() { + return w.writeMetaOnly() + } + + for !mb.IsEmpty() { + var chunk buf.MultiBuffer + if w.transferType == protocol.TransferTypeStream { + mb, chunk = buf.SplitSize(mb, 8*1024) + } else { + mb2, b := buf.SplitFirst(mb) + mb = mb2 + chunk = buf.MultiBuffer{b} + } + if err := w.writeData(chunk); err != nil { + return err + } + } + + return nil +} + +// Close implements common.Closable. +func (w *Writer) Close() error { + meta := FrameMetadata{ + SessionID: w.id, + SessionStatus: SessionStatusEnd, + } + if w.hasError { + meta.Option.Set(OptionError) + } + + frame := buf.New() + common.Must(meta.WriteTo(frame)) + + w.writer.WriteMultiBuffer(buf.MultiBuffer{frame}) + return nil +} diff --git a/subproject/Xray-core-main/common/net/address.go b/subproject/Xray-core-main/common/net/address.go new file mode 100644 index 00000000..1567e4d1 --- /dev/null +++ b/subproject/Xray-core-main/common/net/address.go @@ -0,0 +1,219 @@ +package net + +import ( + "bytes" + "context" + "net" + "strings" + + "github.com/xtls/xray-core/common/errors" +) + +var ( + // LocalHostIP is a constant value for localhost IP in IPv4. + LocalHostIP = IPAddress([]byte{127, 0, 0, 1}) + + // AnyIP is a constant value for any IP in IPv4. + AnyIP = IPAddress([]byte{0, 0, 0, 0}) + + // LocalHostDomain is a constant value for localhost domain. + LocalHostDomain = DomainAddress("localhost") + + // LocalHostIPv6 is a constant value for localhost IP in IPv6. + LocalHostIPv6 = IPAddress([]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}) + + // AnyIPv6 is a constant value for any IP in IPv6. + AnyIPv6 = IPAddress([]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}) +) + +// AddressFamily is the type of address. +type AddressFamily byte + +const ( + // AddressFamilyIPv4 represents address as IPv4 + AddressFamilyIPv4 = AddressFamily(0) + + // AddressFamilyIPv6 represents address as IPv6 + AddressFamilyIPv6 = AddressFamily(1) + + // AddressFamilyDomain represents address as Domain + AddressFamilyDomain = AddressFamily(2) +) + +// IsIPv4 returns true if current AddressFamily is IPv4. +func (af AddressFamily) IsIPv4() bool { + return af == AddressFamilyIPv4 +} + +// IsIPv6 returns true if current AddressFamily is IPv6. +func (af AddressFamily) IsIPv6() bool { + return af == AddressFamilyIPv6 +} + +// IsIP returns true if current AddressFamily is IPv6 or IPv4. +func (af AddressFamily) IsIP() bool { + return af == AddressFamilyIPv4 || af == AddressFamilyIPv6 +} + +// IsDomain returns true if current AddressFamily is Domain. +func (af AddressFamily) IsDomain() bool { + return af == AddressFamilyDomain +} + +// Address represents a network address to be communicated with. It may be an IP address or domain +// address, not both. This interface doesn't resolve IP address for a given domain. +type Address interface { + IP() net.IP // IP of this Address + Domain() string // Domain of this Address + Family() AddressFamily + + String() string // String representation of this Address +} + +func isAlphaNum(c byte) bool { + return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') +} + +// ParseAddress parses a string into an Address. The return value will be an IPAddress when +// the string is in the form of IPv4 or IPv6 address, or a DomainAddress otherwise. +func ParseAddress(addr string) Address { + // Handle IPv6 address in form as "[2001:4860:0:2001::68]" + lenAddr := len(addr) + if lenAddr > 0 && addr[0] == '[' && addr[lenAddr-1] == ']' { + addr = addr[1 : lenAddr-1] + lenAddr -= 2 + } + + if lenAddr > 0 && (!isAlphaNum(addr[0]) || !isAlphaNum(addr[len(addr)-1])) { + addr = strings.TrimSpace(addr) + } + + ip := net.ParseIP(addr) + if ip != nil { + return IPAddress(ip) + } + return DomainAddress(addr) +} + +var bytes0 = []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0} + +// IPAddress creates an Address with given IP. +func IPAddress(ip []byte) Address { + switch len(ip) { + case net.IPv4len: + var addr ipv4Address = [4]byte{ip[0], ip[1], ip[2], ip[3]} + return addr + case net.IPv6len: + if bytes.Equal(ip[:10], bytes0) && ip[10] == 0xff && ip[11] == 0xff { + return IPAddress(ip[12:16]) + } + var addr ipv6Address = [16]byte{ + ip[0], ip[1], ip[2], ip[3], + ip[4], ip[5], ip[6], ip[7], + ip[8], ip[9], ip[10], ip[11], + ip[12], ip[13], ip[14], ip[15], + } + return addr + default: + errors.LogError(context.Background(), "invalid IP format: ", ip) + return nil + } +} + +// DomainAddress creates an Address with given domain. +// This is an internal function that forcibly converts a string to domain. +// It's mainly used in test files and mux. +// Unless you have a specific reason, use net.ParseAddress instead, +// as this function does not check whether the input is an IP address. +// Otherwise, you will get strange results like domain: 1.1.1.1 +func DomainAddress(domain string) Address { + return domainAddress(domain) +} + +type ipv4Address [4]byte + +func (a ipv4Address) IP() net.IP { + return net.IP(a[:]) +} + +func (ipv4Address) Domain() string { + panic("Calling Domain() on an IPv4Address.") +} + +func (ipv4Address) Family() AddressFamily { + return AddressFamilyIPv4 +} + +func (a ipv4Address) String() string { + return a.IP().String() +} + +type ipv6Address [16]byte + +func (a ipv6Address) IP() net.IP { + return net.IP(a[:]) +} + +func (ipv6Address) Domain() string { + panic("Calling Domain() on an IPv6Address.") +} + +func (ipv6Address) Family() AddressFamily { + return AddressFamilyIPv6 +} + +func (a ipv6Address) String() string { + return "[" + a.IP().String() + "]" +} + +type domainAddress string + +func (domainAddress) IP() net.IP { + panic("Calling IP() on a DomainAddress.") +} + +func (a domainAddress) Domain() string { + return string(a) +} + +func (domainAddress) Family() AddressFamily { + return AddressFamilyDomain +} + +func (a domainAddress) String() string { + return a.Domain() +} + +// AsAddress translates IPOrDomain to Address. +func (d *IPOrDomain) AsAddress() Address { + if d == nil { + return nil + } + switch addr := d.Address.(type) { + case *IPOrDomain_Ip: + return IPAddress(addr.Ip) + case *IPOrDomain_Domain: + return DomainAddress(addr.Domain) + } + panic("Common|Net: Invalid address.") +} + +// NewIPOrDomain translates Address to IPOrDomain +func NewIPOrDomain(addr Address) *IPOrDomain { + switch addr.Family() { + case AddressFamilyDomain: + return &IPOrDomain{ + Address: &IPOrDomain_Domain{ + Domain: addr.Domain(), + }, + } + case AddressFamilyIPv4, AddressFamilyIPv6: + return &IPOrDomain{ + Address: &IPOrDomain_Ip{ + Ip: addr.IP(), + }, + } + default: + panic("Unknown Address type.") + } +} diff --git a/subproject/Xray-core-main/common/net/address.pb.go b/subproject/Xray-core-main/common/net/address.pb.go new file mode 100644 index 00000000..2998b97c --- /dev/null +++ b/subproject/Xray-core-main/common/net/address.pb.go @@ -0,0 +1,172 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: common/net/address.proto + +package net + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Address of a network host. It may be either an IP address or a domain +// address. +type IPOrDomain struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Address: + // + // *IPOrDomain_Ip + // *IPOrDomain_Domain + Address isIPOrDomain_Address `protobuf_oneof:"address"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *IPOrDomain) Reset() { + *x = IPOrDomain{} + mi := &file_common_net_address_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *IPOrDomain) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*IPOrDomain) ProtoMessage() {} + +func (x *IPOrDomain) ProtoReflect() protoreflect.Message { + mi := &file_common_net_address_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use IPOrDomain.ProtoReflect.Descriptor instead. +func (*IPOrDomain) Descriptor() ([]byte, []int) { + return file_common_net_address_proto_rawDescGZIP(), []int{0} +} + +func (x *IPOrDomain) GetAddress() isIPOrDomain_Address { + if x != nil { + return x.Address + } + return nil +} + +func (x *IPOrDomain) GetIp() []byte { + if x != nil { + if x, ok := x.Address.(*IPOrDomain_Ip); ok { + return x.Ip + } + } + return nil +} + +func (x *IPOrDomain) GetDomain() string { + if x != nil { + if x, ok := x.Address.(*IPOrDomain_Domain); ok { + return x.Domain + } + } + return "" +} + +type isIPOrDomain_Address interface { + isIPOrDomain_Address() +} + +type IPOrDomain_Ip struct { + // IP address. Must by either 4 or 16 bytes. + Ip []byte `protobuf:"bytes,1,opt,name=ip,proto3,oneof"` +} + +type IPOrDomain_Domain struct { + // Domain address. + Domain string `protobuf:"bytes,2,opt,name=domain,proto3,oneof"` +} + +func (*IPOrDomain_Ip) isIPOrDomain_Address() {} + +func (*IPOrDomain_Domain) isIPOrDomain_Address() {} + +var File_common_net_address_proto protoreflect.FileDescriptor + +const file_common_net_address_proto_rawDesc = "" + + "\n" + + "\x18common/net/address.proto\x12\x0fxray.common.net\"C\n" + + "\n" + + "IPOrDomain\x12\x10\n" + + "\x02ip\x18\x01 \x01(\fH\x00R\x02ip\x12\x18\n" + + "\x06domain\x18\x02 \x01(\tH\x00R\x06domainB\t\n" + + "\aaddressBO\n" + + "\x13com.xray.common.netP\x01Z$github.com/xtls/xray-core/common/net\xaa\x02\x0fXray.Common.Netb\x06proto3" + +var ( + file_common_net_address_proto_rawDescOnce sync.Once + file_common_net_address_proto_rawDescData []byte +) + +func file_common_net_address_proto_rawDescGZIP() []byte { + file_common_net_address_proto_rawDescOnce.Do(func() { + file_common_net_address_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_common_net_address_proto_rawDesc), len(file_common_net_address_proto_rawDesc))) + }) + return file_common_net_address_proto_rawDescData +} + +var file_common_net_address_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_common_net_address_proto_goTypes = []any{ + (*IPOrDomain)(nil), // 0: xray.common.net.IPOrDomain +} +var file_common_net_address_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_common_net_address_proto_init() } +func file_common_net_address_proto_init() { + if File_common_net_address_proto != nil { + return + } + file_common_net_address_proto_msgTypes[0].OneofWrappers = []any{ + (*IPOrDomain_Ip)(nil), + (*IPOrDomain_Domain)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_common_net_address_proto_rawDesc), len(file_common_net_address_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_common_net_address_proto_goTypes, + DependencyIndexes: file_common_net_address_proto_depIdxs, + MessageInfos: file_common_net_address_proto_msgTypes, + }.Build() + File_common_net_address_proto = out.File + file_common_net_address_proto_goTypes = nil + file_common_net_address_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/common/net/address.proto b/subproject/Xray-core-main/common/net/address.proto new file mode 100644 index 00000000..d83f0dfe --- /dev/null +++ b/subproject/Xray-core-main/common/net/address.proto @@ -0,0 +1,19 @@ +syntax = "proto3"; + +package xray.common.net; +option csharp_namespace = "Xray.Common.Net"; +option go_package = "github.com/xtls/xray-core/common/net"; +option java_package = "com.xray.common.net"; +option java_multiple_files = true; + +// Address of a network host. It may be either an IP address or a domain +// address. +message IPOrDomain { + oneof address { + // IP address. Must by either 4 or 16 bytes. + bytes ip = 1; + + // Domain address. + string domain = 2; + } +} diff --git a/subproject/Xray-core-main/common/net/address_test.go b/subproject/Xray-core-main/common/net/address_test.go new file mode 100644 index 00000000..906a7b85 --- /dev/null +++ b/subproject/Xray-core-main/common/net/address_test.go @@ -0,0 +1,193 @@ +package net_test + +import ( + "net" + "testing" + + "github.com/google/go-cmp/cmp" + . "github.com/xtls/xray-core/common/net" +) + +func TestAddressProperty(t *testing.T) { + type addrProprty struct { + IP []byte + Domain string + Family AddressFamily + String string + } + + testCases := []struct { + Input Address + Output addrProprty + }{ + { + Input: IPAddress([]byte{byte(1), byte(2), byte(3), byte(4)}), + Output: addrProprty{ + IP: []byte{byte(1), byte(2), byte(3), byte(4)}, + Family: AddressFamilyIPv4, + String: "1.2.3.4", + }, + }, + { + Input: IPAddress([]byte{ + byte(1), byte(2), byte(3), byte(4), + byte(1), byte(2), byte(3), byte(4), + byte(1), byte(2), byte(3), byte(4), + byte(1), byte(2), byte(3), byte(4), + }), + Output: addrProprty{ + IP: []byte{ + byte(1), byte(2), byte(3), byte(4), + byte(1), byte(2), byte(3), byte(4), + byte(1), byte(2), byte(3), byte(4), + byte(1), byte(2), byte(3), byte(4), + }, + Family: AddressFamilyIPv6, + String: "[102:304:102:304:102:304:102:304]", + }, + }, + { + Input: IPAddress([]byte{ + byte(0), byte(0), byte(0), byte(0), + byte(0), byte(0), byte(0), byte(0), + byte(0), byte(0), byte(255), byte(255), + byte(1), byte(2), byte(3), byte(4), + }), + Output: addrProprty{ + IP: []byte{byte(1), byte(2), byte(3), byte(4)}, + Family: AddressFamilyIPv4, + String: "1.2.3.4", + }, + }, + { + Input: DomainAddress("example.com"), + Output: addrProprty{ + Domain: "example.com", + Family: AddressFamilyDomain, + String: "example.com", + }, + }, + { + Input: IPAddress(net.IPv4(1, 2, 3, 4)), + Output: addrProprty{ + IP: []byte{byte(1), byte(2), byte(3), byte(4)}, + Family: AddressFamilyIPv4, + String: "1.2.3.4", + }, + }, + { + Input: ParseAddress("[2001:4860:0:2001::68]"), + Output: addrProprty{ + IP: []byte{0x20, 0x01, 0x48, 0x60, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x68}, + Family: AddressFamilyIPv6, + String: "[2001:4860:0:2001::68]", + }, + }, + { + Input: ParseAddress("::0"), + Output: addrProprty{ + IP: AnyIPv6.IP(), + Family: AddressFamilyIPv6, + String: "[::]", + }, + }, + { + Input: ParseAddress("[::ffff:123.151.71.143]"), + Output: addrProprty{ + IP: []byte{123, 151, 71, 143}, + Family: AddressFamilyIPv4, + String: "123.151.71.143", + }, + }, + { + Input: NewIPOrDomain(ParseAddress("example.com")).AsAddress(), + Output: addrProprty{ + Domain: "example.com", + Family: AddressFamilyDomain, + String: "example.com", + }, + }, + { + Input: NewIPOrDomain(ParseAddress("8.8.8.8")).AsAddress(), + Output: addrProprty{ + IP: []byte{8, 8, 8, 8}, + Family: AddressFamilyIPv4, + String: "8.8.8.8", + }, + }, + { + Input: NewIPOrDomain(ParseAddress("[2001:4860:0:2001::68]")).AsAddress(), + Output: addrProprty{ + IP: []byte{0x20, 0x01, 0x48, 0x60, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x68}, + Family: AddressFamilyIPv6, + String: "[2001:4860:0:2001::68]", + }, + }, + } + + for _, testCase := range testCases { + actual := addrProprty{ + Family: testCase.Input.Family(), + String: testCase.Input.String(), + } + if testCase.Input.Family().IsIP() { + actual.IP = testCase.Input.IP() + } else { + actual.Domain = testCase.Input.Domain() + } + + if r := cmp.Diff(actual, testCase.Output); r != "" { + t.Error("for input: ", testCase.Input, ":", r) + } + } +} + +func TestInvalidAddressConvertion(t *testing.T) { + panics := func(f func()) (ret bool) { + defer func() { + if r := recover(); r != nil { + ret = true + } + }() + f() + return false + } + + testCases := []func(){ + func() { ParseAddress("8.8.8.8").Domain() }, + func() { ParseAddress("2001:4860:0:2001::68").Domain() }, + func() { ParseAddress("example.com").IP() }, + } + for idx, testCase := range testCases { + if !panics(testCase) { + t.Error("case ", idx, " failed") + } + } +} + +func BenchmarkParseAddressIPv4(b *testing.B) { + for i := 0; i < b.N; i++ { + addr := ParseAddress("8.8.8.8") + if addr.Family() != AddressFamilyIPv4 { + panic("not ipv4") + } + } +} + +func BenchmarkParseAddressIPv6(b *testing.B) { + for i := 0; i < b.N; i++ { + addr := ParseAddress("2001:4860:0:2001::68") + if addr.Family() != AddressFamilyIPv6 { + panic("not ipv6") + } + } +} + +func BenchmarkParseAddressDomain(b *testing.B) { + for i := 0; i < b.N; i++ { + addr := ParseAddress("example.com") + if addr.Family() != AddressFamilyDomain { + panic("not domain") + } + } +} diff --git a/subproject/Xray-core-main/common/net/cnc/connection.go b/subproject/Xray-core-main/common/net/cnc/connection.go new file mode 100644 index 00000000..519918fd --- /dev/null +++ b/subproject/Xray-core-main/common/net/cnc/connection.go @@ -0,0 +1,160 @@ +package cnc + +import ( + "io" + "time" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/signal/done" +) + +type ConnectionOption func(*Connection) + +func ConnectionLocalAddr(a net.Addr) ConnectionOption { + return func(c *Connection) { + c.local = a + } +} + +func ConnectionRemoteAddr(a net.Addr) ConnectionOption { + return func(c *Connection) { + c.remote = a + } +} + +func ConnectionInput(writer io.Writer) ConnectionOption { + return func(c *Connection) { + c.writer = buf.NewWriter(writer) + } +} + +func ConnectionInputMulti(writer buf.Writer) ConnectionOption { + return func(c *Connection) { + c.writer = writer + } +} + +func ConnectionOutput(reader io.Reader) ConnectionOption { + return func(c *Connection) { + c.reader = &buf.BufferedReader{Reader: buf.NewReader(reader)} + } +} + +func ConnectionOutputMulti(reader buf.Reader) ConnectionOption { + return func(c *Connection) { + c.reader = &buf.BufferedReader{Reader: reader} + } +} + +func ConnectionOutputMultiUDP(reader buf.Reader) ConnectionOption { + return func(c *Connection) { + c.reader = &buf.BufferedReader{ + Reader: reader, + Splitter: buf.SplitFirstBytes, + } + } +} + +func ConnectionOnClose(n io.Closer) ConnectionOption { + return func(c *Connection) { + c.onClose = n + } +} + +func NewConnection(opts ...ConnectionOption) net.Conn { + c := &Connection{ + done: done.New(), + local: &net.TCPAddr{ + IP: []byte{0, 0, 0, 0}, + Port: 0, + }, + remote: &net.TCPAddr{ + IP: []byte{0, 0, 0, 0}, + Port: 0, + }, + } + + for _, opt := range opts { + opt(c) + } + + return c +} + +type Connection struct { + reader *buf.BufferedReader + writer buf.Writer + done *done.Instance + onClose io.Closer + local net.Addr + remote net.Addr +} + +func (c *Connection) Read(b []byte) (int, error) { + return c.reader.Read(b) +} + +// ReadMultiBuffer implements buf.Reader. +func (c *Connection) ReadMultiBuffer() (buf.MultiBuffer, error) { + return c.reader.ReadMultiBuffer() +} + +// Write implements net.Conn.Write(). +func (c *Connection) Write(b []byte) (int, error) { + if c.done.Done() { + return 0, io.ErrClosedPipe + } + + l := len(b) + mb := make(buf.MultiBuffer, 0, l/buf.Size+1) + mb = buf.MergeBytes(mb, b) + return l, c.writer.WriteMultiBuffer(mb) +} + +func (c *Connection) WriteMultiBuffer(mb buf.MultiBuffer) error { + if c.done.Done() { + buf.ReleaseMulti(mb) + return io.ErrClosedPipe + } + + return c.writer.WriteMultiBuffer(mb) +} + +// Close implements net.Conn.Close(). +func (c *Connection) Close() error { + common.Must(c.done.Close()) + common.Interrupt(c.reader) + common.Close(c.writer) + if c.onClose != nil { + return c.onClose.Close() + } + + return nil +} + +// LocalAddr implements net.Conn.LocalAddr(). +func (c *Connection) LocalAddr() net.Addr { + return c.local +} + +// RemoteAddr implements net.Conn.RemoteAddr(). +func (c *Connection) RemoteAddr() net.Addr { + return c.remote +} + +// SetDeadline implements net.Conn.SetDeadline(). +func (c *Connection) SetDeadline(t time.Time) error { + return nil +} + +// SetReadDeadline implements net.Conn.SetReadDeadline(). +func (c *Connection) SetReadDeadline(t time.Time) error { + return nil +} + +// SetWriteDeadline implements net.Conn.SetWriteDeadline(). +func (c *Connection) SetWriteDeadline(t time.Time) error { + return nil +} diff --git a/subproject/Xray-core-main/common/net/destination.go b/subproject/Xray-core-main/common/net/destination.go new file mode 100644 index 00000000..90f8298b --- /dev/null +++ b/subproject/Xray-core-main/common/net/destination.go @@ -0,0 +1,155 @@ +package net + +import ( + "net" + "strings" +) + +// Destination represents a network destination including address and protocol (tcp / udp). +type Destination struct { + Address Address + Port Port + Network Network +} + +// DestinationFromAddr generates a Destination from a net address. +func DestinationFromAddr(addr net.Addr) Destination { + switch addr := addr.(type) { + case *net.TCPAddr: + return TCPDestination(IPAddress(addr.IP), Port(addr.Port)) + case *net.UDPAddr: + return UDPDestination(IPAddress(addr.IP), Port(addr.Port)) + case *net.UnixAddr: + return UnixDestination(DomainAddress(addr.Name)) + default: + panic("Net: Unknown address type.") + } +} + +// ParseDestination converts a destination from its string presentation. +func ParseDestination(dest string) (Destination, error) { + d := Destination{ + Address: AnyIP, + Port: Port(0), + } + if strings.HasPrefix(dest, "tcp:") { + d.Network = Network_TCP + dest = dest[4:] + } else if strings.HasPrefix(dest, "udp:") { + d.Network = Network_UDP + dest = dest[4:] + } else if strings.HasPrefix(dest, "unix:") { + d = UnixDestination(DomainAddress(dest[5:])) + return d, nil + } + + hstr, pstr, err := SplitHostPort(dest) + if err != nil { + return d, err + } + if len(hstr) > 0 { + d.Address = ParseAddress(hstr) + } + if len(pstr) > 0 { + port, err := PortFromString(pstr) + if err != nil { + return d, err + } + d.Port = port + } + return d, nil +} + +// TCPDestination creates a TCP destination with given address +func TCPDestination(address Address, port Port) Destination { + return Destination{ + Network: Network_TCP, + Address: address, + Port: port, + } +} + +// UDPDestination creates a UDP destination with given address +func UDPDestination(address Address, port Port) Destination { + return Destination{ + Network: Network_UDP, + Address: address, + Port: port, + } +} + +// UnixDestination creates a Unix destination with given address +func UnixDestination(address Address) Destination { + return Destination{ + Network: Network_UNIX, + Address: address, + } +} + +// NetAddr returns the network address in this Destination in string form. +func (d Destination) NetAddr() string { + addr := "" + if d.Network == Network_TCP || d.Network == Network_UDP { + addr = d.Address.String() + ":" + d.Port.String() + } else if d.Network == Network_UNIX { + addr = d.Address.String() + } + return addr +} + +// RawNetAddr converts a net.Addr from its Destination presentation. +func (d Destination) RawNetAddr() net.Addr { + var addr net.Addr + switch d.Network { + case Network_TCP: + if d.Address.Family().IsIP() { + addr = &net.TCPAddr{ + IP: d.Address.IP(), + Port: int(d.Port), + } + } + case Network_UDP: + if d.Address.Family().IsIP() { + addr = &net.UDPAddr{ + IP: d.Address.IP(), + Port: int(d.Port), + } + } + case Network_UNIX: + if d.Address.Family().IsDomain() { + addr = &net.UnixAddr{ + Name: d.Address.String(), + Net: d.Network.SystemString(), + } + } + } + return addr +} + +// String returns the strings form of this Destination. +func (d Destination) String() string { + prefix := "unknown:" + switch d.Network { + case Network_TCP: + prefix = "tcp:" + case Network_UDP: + prefix = "udp:" + case Network_UNIX: + prefix = "unix:" + } + return prefix + d.NetAddr() +} + +// IsValid returns true if this Destination is valid. +func (d Destination) IsValid() bool { + return d.Network != Network_Unknown +} + +// AsDestination converts current Endpoint into Destination. +func (p *Endpoint) AsDestination() Destination { + return Destination{ + Network: p.Network, + Address: p.Address.AsAddress(), + Port: Port(p.Port), + } +} diff --git a/subproject/Xray-core-main/common/net/destination.pb.go b/subproject/Xray-core-main/common/net/destination.pb.go new file mode 100644 index 00000000..98c312dc --- /dev/null +++ b/subproject/Xray-core-main/common/net/destination.pb.go @@ -0,0 +1,148 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: common/net/destination.proto + +package net + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Endpoint of a network connection. +type Endpoint struct { + state protoimpl.MessageState `protogen:"open.v1"` + Network Network `protobuf:"varint,1,opt,name=network,proto3,enum=xray.common.net.Network" json:"network,omitempty"` + Address *IPOrDomain `protobuf:"bytes,2,opt,name=address,proto3" json:"address,omitempty"` + Port uint32 `protobuf:"varint,3,opt,name=port,proto3" json:"port,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Endpoint) Reset() { + *x = Endpoint{} + mi := &file_common_net_destination_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Endpoint) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Endpoint) ProtoMessage() {} + +func (x *Endpoint) ProtoReflect() protoreflect.Message { + mi := &file_common_net_destination_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Endpoint.ProtoReflect.Descriptor instead. +func (*Endpoint) Descriptor() ([]byte, []int) { + return file_common_net_destination_proto_rawDescGZIP(), []int{0} +} + +func (x *Endpoint) GetNetwork() Network { + if x != nil { + return x.Network + } + return Network_Unknown +} + +func (x *Endpoint) GetAddress() *IPOrDomain { + if x != nil { + return x.Address + } + return nil +} + +func (x *Endpoint) GetPort() uint32 { + if x != nil { + return x.Port + } + return 0 +} + +var File_common_net_destination_proto protoreflect.FileDescriptor + +const file_common_net_destination_proto_rawDesc = "" + + "\n" + + "\x1ccommon/net/destination.proto\x12\x0fxray.common.net\x1a\x18common/net/network.proto\x1a\x18common/net/address.proto\"\x89\x01\n" + + "\bEndpoint\x122\n" + + "\anetwork\x18\x01 \x01(\x0e2\x18.xray.common.net.NetworkR\anetwork\x125\n" + + "\aaddress\x18\x02 \x01(\v2\x1b.xray.common.net.IPOrDomainR\aaddress\x12\x12\n" + + "\x04port\x18\x03 \x01(\rR\x04portBO\n" + + "\x13com.xray.common.netP\x01Z$github.com/xtls/xray-core/common/net\xaa\x02\x0fXray.Common.Netb\x06proto3" + +var ( + file_common_net_destination_proto_rawDescOnce sync.Once + file_common_net_destination_proto_rawDescData []byte +) + +func file_common_net_destination_proto_rawDescGZIP() []byte { + file_common_net_destination_proto_rawDescOnce.Do(func() { + file_common_net_destination_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_common_net_destination_proto_rawDesc), len(file_common_net_destination_proto_rawDesc))) + }) + return file_common_net_destination_proto_rawDescData +} + +var file_common_net_destination_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_common_net_destination_proto_goTypes = []any{ + (*Endpoint)(nil), // 0: xray.common.net.Endpoint + (Network)(0), // 1: xray.common.net.Network + (*IPOrDomain)(nil), // 2: xray.common.net.IPOrDomain +} +var file_common_net_destination_proto_depIdxs = []int32{ + 1, // 0: xray.common.net.Endpoint.network:type_name -> xray.common.net.Network + 2, // 1: xray.common.net.Endpoint.address:type_name -> xray.common.net.IPOrDomain + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_common_net_destination_proto_init() } +func file_common_net_destination_proto_init() { + if File_common_net_destination_proto != nil { + return + } + file_common_net_network_proto_init() + file_common_net_address_proto_init() + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_common_net_destination_proto_rawDesc), len(file_common_net_destination_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_common_net_destination_proto_goTypes, + DependencyIndexes: file_common_net_destination_proto_depIdxs, + MessageInfos: file_common_net_destination_proto_msgTypes, + }.Build() + File_common_net_destination_proto = out.File + file_common_net_destination_proto_goTypes = nil + file_common_net_destination_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/common/net/destination.proto b/subproject/Xray-core-main/common/net/destination.proto new file mode 100644 index 00000000..c6c4230e --- /dev/null +++ b/subproject/Xray-core-main/common/net/destination.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; + +package xray.common.net; +option csharp_namespace = "Xray.Common.Net"; +option go_package = "github.com/xtls/xray-core/common/net"; +option java_package = "com.xray.common.net"; +option java_multiple_files = true; + +import "common/net/network.proto"; +import "common/net/address.proto"; + +// Endpoint of a network connection. +message Endpoint { + Network network = 1; + IPOrDomain address = 2; + uint32 port = 3; +} diff --git a/subproject/Xray-core-main/common/net/destination_test.go b/subproject/Xray-core-main/common/net/destination_test.go new file mode 100644 index 00000000..0c2b1854 --- /dev/null +++ b/subproject/Xray-core-main/common/net/destination_test.go @@ -0,0 +1,110 @@ +package net_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + . "github.com/xtls/xray-core/common/net" +) + +func TestDestinationProperty(t *testing.T) { + testCases := []struct { + Input Destination + Network Network + String string + NetString string + }{ + { + Input: TCPDestination(IPAddress([]byte{1, 2, 3, 4}), 80), + Network: Network_TCP, + String: "tcp:1.2.3.4:80", + NetString: "1.2.3.4:80", + }, + { + Input: UDPDestination(IPAddress([]byte{0x20, 0x01, 0x48, 0x60, 0x48, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x88, 0x88}), 53), + Network: Network_UDP, + String: "udp:[2001:4860:4860::8888]:53", + NetString: "[2001:4860:4860::8888]:53", + }, + { + Input: UnixDestination(DomainAddress("/tmp/test.sock")), + Network: Network_UNIX, + String: "unix:/tmp/test.sock", + NetString: "/tmp/test.sock", + }, + } + + for _, testCase := range testCases { + dest := testCase.Input + if r := cmp.Diff(dest.Network, testCase.Network); r != "" { + t.Error("unexpected Network in ", dest.String(), ": ", r) + } + if r := cmp.Diff(dest.String(), testCase.String); r != "" { + t.Error(r) + } + if r := cmp.Diff(dest.NetAddr(), testCase.NetString); r != "" { + t.Error(r) + } + } +} + +func TestDestinationParse(t *testing.T) { + cases := []struct { + Input string + Output Destination + Error bool + }{ + { + Input: "tcp:127.0.0.1:80", + Output: TCPDestination(LocalHostIP, Port(80)), + }, + { + Input: "udp:8.8.8.8:53", + Output: UDPDestination(IPAddress([]byte{8, 8, 8, 8}), Port(53)), + }, + { + Input: "unix:/tmp/test.sock", + Output: UnixDestination(DomainAddress("/tmp/test.sock")), + }, + { + Input: "8.8.8.8:53", + Output: Destination{ + Address: IPAddress([]byte{8, 8, 8, 8}), + Port: Port(53), + }, + }, + { + Input: ":53", + Output: Destination{ + Address: AnyIP, + Port: Port(53), + }, + }, + { + Input: "8.8.8.8", + Error: true, + }, + { + Input: "8.8.8.8:http", + Error: true, + }, + { + Input: "/tmp/test.sock", + Error: true, + }, + } + + for _, testcase := range cases { + d, err := ParseDestination(testcase.Input) + if !testcase.Error { + if err != nil { + t.Error("for test case: ", testcase.Input, " expected no error, but got ", err) + } + if d != testcase.Output { + t.Error("for test case: ", testcase.Input, " expected output: ", testcase.Output.String(), " but got ", d.String()) + } + } else if err == nil { + t.Error("for test case: ", testcase.Input, " expected error, but got nil") + } + } +} diff --git a/subproject/Xray-core-main/common/net/find_process_linux.go b/subproject/Xray-core-main/common/net/find_process_linux.go new file mode 100644 index 00000000..95e03935 --- /dev/null +++ b/subproject/Xray-core-main/common/net/find_process_linux.go @@ -0,0 +1,176 @@ +//go:build linux + +package net + +import ( + "bufio" + "encoding/hex" + "fmt" + "os" + "strconv" + "strings" + + "github.com/xtls/xray-core/common/errors" +) + +func FindProcess(dest Destination) (PID int, Name string, AbsolutePath string, err error) { + isLocal, err := IsLocal(dest.Address.IP()) + if err != nil { + return 0, "", "", errors.New("failed to determine if address is local: ", err) + } + if !isLocal { + return 0, "", "", ErrNotLocal + } + if dest.Network != Network_TCP && dest.Network != Network_UDP { + panic("Unsupported network type for process lookup.") + } + // the core should never has a domain as source(? + if dest.Address.Family() == AddressFamilyDomain { + panic("Domain addresses are not supported for process lookup.") + } + var procFile string + + switch dest.Network { + case Network_TCP: + if dest.Address.Family() == AddressFamilyIPv4 { + procFile = "/proc/net/tcp" + } + if dest.Address.Family() == AddressFamilyIPv6 { + procFile = "/proc/net/tcp6" + } + case Network_UDP: + if dest.Address.Family() == AddressFamilyIPv4 { + procFile = "/proc/net/udp" + } + if dest.Address.Family() == AddressFamilyIPv6 { + procFile = "/proc/net/udp6" + } + default: + panic("Unsupported network type for process lookup.") + } + + targetHexAddr, err := formatLittleEndianString(dest.Address, dest.Port) + if err != nil { + return 0, "", "", errors.New("failed to format address: ", err) + } + + inode, err := findInodeInFile(procFile, targetHexAddr) + if err != nil { + return 0, "", "", errors.New("could not search in ", procFile).Base(err) + } + if inode == "" { + return 0, "", "", errors.New("connection for ", dest.Address, ":", dest.Port, " not found in ", procFile) + } + + pidStr, err := findPidByInode(inode) + if err != nil { + return 0, "", "", errors.New("could not find PID for inode ", inode, ": ", err) + } + if pidStr == "" { + return 0, "", "", errors.New("no process found for inode ", inode) + } + + absPath, err := getAbsPath(pidStr) + if err != nil { + return 0, "", "", errors.New("could not get process name for PID ", pidStr, ":", err) + } + + nameSplit := strings.Split(absPath, "/") + procName := nameSplit[len(nameSplit)-1] + + pid, err := strconv.Atoi(pidStr) + if err != nil { + return 0, "", "", errors.New("failed to parse PID: ", err) + } + + return pid, procName, absPath, nil +} + +func formatLittleEndianString(addr Address, port Port) (string, error) { + ip := addr.IP() + var ipBytes []byte + if addr.Family() == AddressFamilyIPv4 { + ipBytes = ip.To4() + } else { + ipBytes = ip.To16() + } + if ipBytes == nil { + return "", errors.New("invalid IP format for ", addr.Family(), ": ", ip) + } + + for i, j := 0, len(ipBytes)-1; i < j; i, j = i+1, j-1 { + ipBytes[i], ipBytes[j] = ipBytes[j], ipBytes[i] + } + portHex := fmt.Sprintf("%04X", uint16(port)) + ipHex := strings.ToUpper(hex.EncodeToString(ipBytes)) + return fmt.Sprintf("%s:%s", ipHex, portHex), nil +} + +func findInodeInFile(filePath, targetHexAddr string) (string, error) { + file, err := os.Open(filePath) + if err != nil { + return "", err + } + defer file.Close() + + scanner := bufio.NewScanner(file) + + for scanner.Scan() { + line := scanner.Text() + fields := strings.Fields(line) + + if len(fields) < 10 { + continue + } + + localAddress := fields[1] + if localAddress == targetHexAddr { + inode := fields[9] + return inode, nil + } + } + + return "", scanner.Err() +} + +func findPidByInode(inode string) (string, error) { + procDir, err := os.ReadDir("/proc") + if err != nil { + return "", err + } + + targetLink := "socket:[" + inode + "]" + + for _, entry := range procDir { + if !entry.IsDir() { + continue + } + pid := entry.Name() + if _, err := strconv.Atoi(pid); err != nil { + continue + } + + fdPath := fmt.Sprintf("/proc/%s/fd", pid) + fdDir, err := os.ReadDir(fdPath) + if err != nil { + continue + } + + for _, fdEntry := range fdDir { + linkPath := fmt.Sprintf("%s/%s", fdPath, fdEntry.Name()) + linkTarget, err := os.Readlink(linkPath) + if err != nil { + continue + } + if linkTarget == targetLink { + return pid, nil + } + } + } + return "", nil +} + +func getAbsPath(pid string) (string, error) { + path := fmt.Sprintf("/proc/%s/exe", pid) + return os.Readlink(path) +} diff --git a/subproject/Xray-core-main/common/net/find_process_others.go b/subproject/Xray-core-main/common/net/find_process_others.go new file mode 100644 index 00000000..a47b5b93 --- /dev/null +++ b/subproject/Xray-core-main/common/net/find_process_others.go @@ -0,0 +1,11 @@ +//go:build !windows && !linux + +package net + +import ( + "github.com/xtls/xray-core/common/errors" +) + +func FindProcess(dest Destination) (int, string, string, error) { + return 0, "", "", errors.New("process lookup is not supported on this platform") +} diff --git a/subproject/Xray-core-main/common/net/find_process_windows.go b/subproject/Xray-core-main/common/net/find_process_windows.go new file mode 100644 index 00000000..46ddcc9c --- /dev/null +++ b/subproject/Xray-core-main/common/net/find_process_windows.go @@ -0,0 +1,243 @@ +//go:build windows + +package net + +import ( + "net/netip" + "path/filepath" + "strings" + "sync" + "syscall" + "unsafe" + + "golang.org/x/sys/windows" + + "github.com/xtls/xray-core/common/errors" +) + +const ( + tcpTableFunc = "GetExtendedTcpTable" + tcpTablePidConn = 4 + udpTableFunc = "GetExtendedUdpTable" + udpTablePid = 1 +) + +var ( + getExTCPTable uintptr + getExUDPTable uintptr + + once sync.Once + initErr error +) + +func initWin32API() error { + h, err := windows.LoadLibrary("iphlpapi.dll") + if err != nil { + return errors.New("LoadLibrary iphlpapi.dll failed").Base(err) + } + + getExTCPTable, err = windows.GetProcAddress(h, tcpTableFunc) + if err != nil { + return errors.New("GetProcAddress of ", tcpTableFunc, " failed").Base(err) + } + + getExUDPTable, err = windows.GetProcAddress(h, udpTableFunc) + if err != nil { + return errors.New("GetProcAddress of ", udpTableFunc, " failed").Base(err) + } + + return nil +} + +func FindProcess(dest Destination) (PID int, Name string, AbsolutePath string, err error) { + once.Do(func() { + initErr = initWin32API() + }) + if initErr != nil { + return 0, "", "", initErr + } + isLocal, err := IsLocal(dest.Address.IP()) + if err != nil { + return 0, "", "", errors.New("failed to determine if address is local: ", err) + } + if !isLocal { + return 0, "", "", ErrNotLocal + } + if dest.Network != Network_TCP && dest.Network != Network_UDP { + panic("Unsupported network type for process lookup.") + } + // the core should never has a domain as source(? + if dest.Address.Family() == AddressFamilyDomain { + panic("Domain addresses are not supported for process lookup.") + } + var class int + var fn uintptr + switch dest.Network { + case Network_TCP: + fn = getExTCPTable + class = tcpTablePidConn + case Network_UDP: + fn = getExUDPTable + class = udpTablePid + default: + panic("Unsupported network type for process lookup.") + } + ip := dest.Address.IP() + port := int(dest.Port) + + addr, ok := netip.AddrFromSlice(ip) + if !ok { + return 0, "", "", errors.New("invalid IP address") + } + addr = addr.Unmap() + + family := windows.AF_INET + if addr.Is6() { + family = windows.AF_INET6 + } + + buf, err := getTransportTable(fn, family, class) + if err != nil { + return 0, "", "", err + } + + s := newSearcher(dest.Network, dest.Address.Family()) + + pid, err := s.Search(buf, addr, uint16(port)) + if err != nil { + return 0, "", "", err + } + NameWithPath, err := getExecPathFromPID(pid) + NameWithPath = filepath.ToSlash(NameWithPath) + + // drop .exe and path + nameSplit := strings.Split(NameWithPath, "/") + procName := nameSplit[len(nameSplit)-1] + procName = strings.TrimSuffix(procName, ".exe") + return int(pid), procName, NameWithPath, err +} + +type searcher struct { + itemSize int + port int + ip int + ipSize int + pid int + tcpState int +} + +func (s *searcher) Search(b []byte, ip netip.Addr, port uint16) (uint32, error) { + n := int(readNativeUint32(b[:4])) + itemSize := s.itemSize + for i := range n { + row := b[4+itemSize*i : 4+itemSize*(i+1)] + + if s.tcpState >= 0 { + tcpState := readNativeUint32(row[s.tcpState : s.tcpState+4]) + // MIB_TCP_STATE_ESTAB, only check established connections for TCP + if tcpState != 5 { + continue + } + } + + // according to MSDN, only the lower 16 bits of dwLocalPort are used and the port number is in network endian. + // this field can be illustrated as follows depends on different machine endianess: + // little endian: [ MSB LSB 0 0 ] interpret as native uint32 is ((LSB<<8)|MSB) + // big endian: [ 0 0 MSB LSB ] interpret as native uint32 is ((MSB<<8)|LSB) + // so we need an syscall.Ntohs on the lower 16 bits after read the port as native uint32 + srcPort := syscall.Ntohs(uint16(readNativeUint32(row[s.port : s.port+4]))) + if srcPort != port { + continue + } + + srcIP, _ := netip.AddrFromSlice(row[s.ip : s.ip+s.ipSize]) + srcIP = srcIP.Unmap() + // windows binds an unbound udp socket to 0.0.0.0/[::] while first sendto + if ip != srcIP && (!srcIP.IsUnspecified() || s.tcpState != -1) { + continue + } + + pid := readNativeUint32(row[s.pid : s.pid+4]) + return pid, nil + } + return 0, errors.New("not found") +} + +func newSearcher(network Network, family AddressFamily) *searcher { + var itemSize, port, ip, ipSize, pid int + tcpState := -1 + switch network { + case Network_TCP: + if family == AddressFamilyIPv4 { + // struct MIB_TCPROW_OWNER_PID + itemSize, port, ip, ipSize, pid, tcpState = 24, 8, 4, 4, 20, 0 + } + if family == AddressFamilyIPv6 { + // struct MIB_TCP6ROW_OWNER_PID + itemSize, port, ip, ipSize, pid, tcpState = 56, 20, 0, 16, 52, 48 + } + case Network_UDP: + if family == AddressFamilyIPv4 { + // struct MIB_UDPROW_OWNER_PID + itemSize, port, ip, ipSize, pid = 12, 4, 0, 4, 8 + } + if family == AddressFamilyIPv6 { + // struct MIB_UDP6ROW_OWNER_PID + itemSize, port, ip, ipSize, pid = 28, 20, 0, 16, 24 + } + } + + return &searcher{ + itemSize: itemSize, + port: port, + ip: ip, + ipSize: ipSize, + pid: pid, + tcpState: tcpState, + } +} + +func getTransportTable(fn uintptr, family int, class int) ([]byte, error) { + for size, buf := uint32(8), make([]byte, 8); ; { + ptr := unsafe.Pointer(&buf[0]) + err, _, _ := syscall.Syscall6(fn, 6, uintptr(ptr), uintptr(unsafe.Pointer(&size)), 0, uintptr(family), uintptr(class), 0) + + switch err { + case 0: + return buf, nil + case uintptr(syscall.ERROR_INSUFFICIENT_BUFFER): + buf = make([]byte, size) + default: + return nil, errors.New("syscall error: ", int(err)) + } + } +} + +func readNativeUint32(b []byte) uint32 { + return *(*uint32)(unsafe.Pointer(&b[0])) +} + +func getExecPathFromPID(pid uint32) (string, error) { + // kernel process starts with a colon in order to distinguish with normal processes + switch pid { + case 0: + // reserved pid for system idle process + return ":System Idle Process", nil + case 4: + // reserved pid for windows kernel image + return ":System", nil + } + h, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, pid) + if err != nil { + return "", err + } + defer windows.CloseHandle(h) + + buf := make([]uint16, syscall.MAX_LONG_PATH) + size := uint32(len(buf)) + err = windows.QueryFullProcessImageName(h, 0, &buf[0], &size) + if err != nil { + return "", err + } + return syscall.UTF16ToString(buf[:size]), nil +} diff --git a/subproject/Xray-core-main/common/net/net.go b/subproject/Xray-core-main/common/net/net.go new file mode 100644 index 00000000..9a21313a --- /dev/null +++ b/subproject/Xray-core-main/common/net/net.go @@ -0,0 +1,54 @@ +// 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" + +import ( + "net" + "sync/atomic" + "time" + + "github.com/xtls/xray-core/common/errors" +) + +// 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 + +var ErrNotLocal = errors.New("the source address is not from local machine.") + +type localIPCacheEntry struct { + addrs []net.Addr + lastUpdate time.Time +} + +var localIPCache = atomic.Pointer[localIPCacheEntry]{} + +func IsLocal(ip net.IP) (bool, error) { + var addrs []net.Addr + if entry := localIPCache.Load(); entry == nil || time.Since(entry.lastUpdate) > time.Minute { + var err error + addrs, err = net.InterfaceAddrs() + if err != nil { + return false, err + } + localIPCache.Store(&localIPCacheEntry{ + addrs: addrs, + lastUpdate: time.Now(), + }) + } else { + addrs = entry.addrs + } + for _, addr := range addrs { + if ipnet, ok := addr.(*net.IPNet); ok { + if ipnet.IP.Equal(ip) { + return true, nil + } + } + } + return false, nil +} diff --git a/subproject/Xray-core-main/common/net/network.go b/subproject/Xray-core-main/common/net/network.go new file mode 100644 index 00000000..f2e303b0 --- /dev/null +++ b/subproject/Xray-core-main/common/net/network.go @@ -0,0 +1,24 @@ +package net + +func (n Network) SystemString() string { + switch n { + case Network_TCP: + return "tcp" + case Network_UDP: + return "udp" + case Network_UNIX: + return "unix" + default: + return "unknown" + } +} + +// HasNetwork returns true if the network list has a certain network. +func HasNetwork(list []Network, network Network) bool { + for _, value := range list { + if value == network { + return true + } + } + return false +} diff --git a/subproject/Xray-core-main/common/net/network.pb.go b/subproject/Xray-core-main/common/net/network.pb.go new file mode 100644 index 00000000..e5f1590e --- /dev/null +++ b/subproject/Xray-core-main/common/net/network.pb.go @@ -0,0 +1,185 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: common/net/network.proto + +package net + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Network int32 + +const ( + Network_Unknown Network = 0 + Network_TCP Network = 2 + Network_UDP Network = 3 + Network_UNIX Network = 4 +) + +// Enum value maps for Network. +var ( + Network_name = map[int32]string{ + 0: "Unknown", + 2: "TCP", + 3: "UDP", + 4: "UNIX", + } + Network_value = map[string]int32{ + "Unknown": 0, + "TCP": 2, + "UDP": 3, + "UNIX": 4, + } +) + +func (x Network) Enum() *Network { + p := new(Network) + *p = x + return p +} + +func (x Network) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Network) Descriptor() protoreflect.EnumDescriptor { + return file_common_net_network_proto_enumTypes[0].Descriptor() +} + +func (Network) Type() protoreflect.EnumType { + return &file_common_net_network_proto_enumTypes[0] +} + +func (x Network) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Network.Descriptor instead. +func (Network) EnumDescriptor() ([]byte, []int) { + return file_common_net_network_proto_rawDescGZIP(), []int{0} +} + +// NetworkList is a list of Networks. +type NetworkList struct { + state protoimpl.MessageState `protogen:"open.v1"` + Network []Network `protobuf:"varint,1,rep,packed,name=network,proto3,enum=xray.common.net.Network" json:"network,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NetworkList) Reset() { + *x = NetworkList{} + mi := &file_common_net_network_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NetworkList) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NetworkList) ProtoMessage() {} + +func (x *NetworkList) ProtoReflect() protoreflect.Message { + mi := &file_common_net_network_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NetworkList.ProtoReflect.Descriptor instead. +func (*NetworkList) Descriptor() ([]byte, []int) { + return file_common_net_network_proto_rawDescGZIP(), []int{0} +} + +func (x *NetworkList) GetNetwork() []Network { + if x != nil { + return x.Network + } + return nil +} + +var File_common_net_network_proto protoreflect.FileDescriptor + +const file_common_net_network_proto_rawDesc = "" + + "\n" + + "\x18common/net/network.proto\x12\x0fxray.common.net\"A\n" + + "\vNetworkList\x122\n" + + "\anetwork\x18\x01 \x03(\x0e2\x18.xray.common.net.NetworkR\anetwork*2\n" + + "\aNetwork\x12\v\n" + + "\aUnknown\x10\x00\x12\a\n" + + "\x03TCP\x10\x02\x12\a\n" + + "\x03UDP\x10\x03\x12\b\n" + + "\x04UNIX\x10\x04BO\n" + + "\x13com.xray.common.netP\x01Z$github.com/xtls/xray-core/common/net\xaa\x02\x0fXray.Common.Netb\x06proto3" + +var ( + file_common_net_network_proto_rawDescOnce sync.Once + file_common_net_network_proto_rawDescData []byte +) + +func file_common_net_network_proto_rawDescGZIP() []byte { + file_common_net_network_proto_rawDescOnce.Do(func() { + file_common_net_network_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_common_net_network_proto_rawDesc), len(file_common_net_network_proto_rawDesc))) + }) + return file_common_net_network_proto_rawDescData +} + +var file_common_net_network_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_common_net_network_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_common_net_network_proto_goTypes = []any{ + (Network)(0), // 0: xray.common.net.Network + (*NetworkList)(nil), // 1: xray.common.net.NetworkList +} +var file_common_net_network_proto_depIdxs = []int32{ + 0, // 0: xray.common.net.NetworkList.network:type_name -> xray.common.net.Network + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_common_net_network_proto_init() } +func file_common_net_network_proto_init() { + if File_common_net_network_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_common_net_network_proto_rawDesc), len(file_common_net_network_proto_rawDesc)), + NumEnums: 1, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_common_net_network_proto_goTypes, + DependencyIndexes: file_common_net_network_proto_depIdxs, + EnumInfos: file_common_net_network_proto_enumTypes, + MessageInfos: file_common_net_network_proto_msgTypes, + }.Build() + File_common_net_network_proto = out.File + file_common_net_network_proto_goTypes = nil + file_common_net_network_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/common/net/network.proto b/subproject/Xray-core-main/common/net/network.proto new file mode 100644 index 00000000..e7579a21 --- /dev/null +++ b/subproject/Xray-core-main/common/net/network.proto @@ -0,0 +1,18 @@ +syntax = "proto3"; + +package xray.common.net; +option csharp_namespace = "Xray.Common.Net"; +option go_package = "github.com/xtls/xray-core/common/net"; +option java_package = "com.xray.common.net"; +option java_multiple_files = true; + +enum Network { + Unknown = 0; + + TCP = 2; + UDP = 3; + UNIX = 4; +} + +// NetworkList is a list of Networks. +message NetworkList { repeated Network network = 1; } diff --git a/subproject/Xray-core-main/common/net/port.go b/subproject/Xray-core-main/common/net/port.go new file mode 100644 index 00000000..26f5e3e2 --- /dev/null +++ b/subproject/Xray-core-main/common/net/port.go @@ -0,0 +1,107 @@ +package net + +import ( + "encoding/binary" + "strconv" + + "github.com/xtls/xray-core/common/errors" +) + +// Port represents a network port in TCP and UDP protocol. +type Port uint16 + +// PortFromBytes converts a byte array to a Port, assuming bytes are in big endian order. +// @unsafe Caller must ensure that the byte array has at least 2 elements. +func PortFromBytes(port []byte) Port { + return Port(binary.BigEndian.Uint16(port)) +} + +// PortFromInt converts an integer to a Port. +// @error when the integer is not positive or larger then 65535 +func PortFromInt(val uint32) (Port, error) { + if val > 65535 { + return Port(0), errors.New("invalid port range: ", val) + } + return Port(val), nil +} + +// PortFromString converts a string to a Port. +// @error when the string is not an integer or the integral value is a not a valid Port. +func PortFromString(s string) (Port, error) { + val, err := strconv.ParseUint(s, 10, 32) + if err != nil { + return Port(0), errors.New("invalid port range: ", s) + } + return PortFromInt(uint32(val)) +} + +// Value return the corresponding uint16 value of a Port. +func (p Port) Value() uint16 { + return uint16(p) +} + +// String returns the string presentation of a Port. +func (p Port) String() string { + return strconv.Itoa(int(p)) +} + +// FromPort returns the beginning port of this PortRange. +func (p *PortRange) FromPort() Port { + return Port(p.From) +} + +// ToPort returns the end port of this PortRange. +func (p *PortRange) ToPort() Port { + return Port(p.To) +} + +// Contains returns true if the given port is within the range of a PortRange. +func (p *PortRange) Contains(port Port) bool { + return p.FromPort() <= port && port <= p.ToPort() +} + +// SinglePortRange returns a PortRange contains a single port. +func SinglePortRange(p Port) *PortRange { + return &PortRange{ + From: uint32(p), + To: uint32(p), + } +} + +type MemoryPortRange struct { + From Port + To Port +} + +func (r MemoryPortRange) Contains(port Port) bool { + return r.From <= port && port <= r.To +} + +type MemoryPortList []MemoryPortRange + +func PortListFromProto(l *PortList) MemoryPortList { + mpl := make(MemoryPortList, 0, len(l.Range)) + for _, r := range l.Range { + mpl = append(mpl, MemoryPortRange{From: Port(r.From), To: Port(r.To)}) + } + return mpl +} + +func (l *PortList) Ports() []uint32 { + var ports []uint32 + for _, r := range l.Range { + for i := uint32(r.From); i <= uint32(r.To); i++ { + ports = append(ports, i) + } + } + return ports +} + +func (mpl MemoryPortList) Contains(port Port) bool { + for _, pr := range mpl { + if pr.Contains(port) { + return true + } + } + return false +} diff --git a/subproject/Xray-core-main/common/net/port.pb.go b/subproject/Xray-core-main/common/net/port.pb.go new file mode 100644 index 00000000..764a5cda --- /dev/null +++ b/subproject/Xray-core-main/common/net/port.pb.go @@ -0,0 +1,184 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: common/net/port.proto + +package net + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// PortRange represents a range of ports. +type PortRange struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The port that this range starts from. + From uint32 `protobuf:"varint,1,opt,name=From,proto3" json:"From,omitempty"` + // The port that this range ends with (inclusive). + To uint32 `protobuf:"varint,2,opt,name=To,proto3" json:"To,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PortRange) Reset() { + *x = PortRange{} + mi := &file_common_net_port_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PortRange) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PortRange) ProtoMessage() {} + +func (x *PortRange) ProtoReflect() protoreflect.Message { + mi := &file_common_net_port_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PortRange.ProtoReflect.Descriptor instead. +func (*PortRange) Descriptor() ([]byte, []int) { + return file_common_net_port_proto_rawDescGZIP(), []int{0} +} + +func (x *PortRange) GetFrom() uint32 { + if x != nil { + return x.From + } + return 0 +} + +func (x *PortRange) GetTo() uint32 { + if x != nil { + return x.To + } + return 0 +} + +// PortList is a list of ports. +type PortList struct { + state protoimpl.MessageState `protogen:"open.v1"` + Range []*PortRange `protobuf:"bytes,1,rep,name=range,proto3" json:"range,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PortList) Reset() { + *x = PortList{} + mi := &file_common_net_port_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PortList) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PortList) ProtoMessage() {} + +func (x *PortList) ProtoReflect() protoreflect.Message { + mi := &file_common_net_port_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PortList.ProtoReflect.Descriptor instead. +func (*PortList) Descriptor() ([]byte, []int) { + return file_common_net_port_proto_rawDescGZIP(), []int{1} +} + +func (x *PortList) GetRange() []*PortRange { + if x != nil { + return x.Range + } + return nil +} + +var File_common_net_port_proto protoreflect.FileDescriptor + +const file_common_net_port_proto_rawDesc = "" + + "\n" + + "\x15common/net/port.proto\x12\x0fxray.common.net\"/\n" + + "\tPortRange\x12\x12\n" + + "\x04From\x18\x01 \x01(\rR\x04From\x12\x0e\n" + + "\x02To\x18\x02 \x01(\rR\x02To\"<\n" + + "\bPortList\x120\n" + + "\x05range\x18\x01 \x03(\v2\x1a.xray.common.net.PortRangeR\x05rangeBO\n" + + "\x13com.xray.common.netP\x01Z$github.com/xtls/xray-core/common/net\xaa\x02\x0fXray.Common.Netb\x06proto3" + +var ( + file_common_net_port_proto_rawDescOnce sync.Once + file_common_net_port_proto_rawDescData []byte +) + +func file_common_net_port_proto_rawDescGZIP() []byte { + file_common_net_port_proto_rawDescOnce.Do(func() { + file_common_net_port_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_common_net_port_proto_rawDesc), len(file_common_net_port_proto_rawDesc))) + }) + return file_common_net_port_proto_rawDescData +} + +var file_common_net_port_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_common_net_port_proto_goTypes = []any{ + (*PortRange)(nil), // 0: xray.common.net.PortRange + (*PortList)(nil), // 1: xray.common.net.PortList +} +var file_common_net_port_proto_depIdxs = []int32{ + 0, // 0: xray.common.net.PortList.range:type_name -> xray.common.net.PortRange + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_common_net_port_proto_init() } +func file_common_net_port_proto_init() { + if File_common_net_port_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_common_net_port_proto_rawDesc), len(file_common_net_port_proto_rawDesc)), + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_common_net_port_proto_goTypes, + DependencyIndexes: file_common_net_port_proto_depIdxs, + MessageInfos: file_common_net_port_proto_msgTypes, + }.Build() + File_common_net_port_proto = out.File + file_common_net_port_proto_goTypes = nil + file_common_net_port_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/common/net/port.proto b/subproject/Xray-core-main/common/net/port.proto new file mode 100644 index 00000000..0b626f9e --- /dev/null +++ b/subproject/Xray-core-main/common/net/port.proto @@ -0,0 +1,20 @@ +syntax = "proto3"; + +package xray.common.net; +option csharp_namespace = "Xray.Common.Net"; +option go_package = "github.com/xtls/xray-core/common/net"; +option java_package = "com.xray.common.net"; +option java_multiple_files = true; + +// PortRange represents a range of ports. +message PortRange { + // The port that this range starts from. + uint32 From = 1; + // The port that this range ends with (inclusive). + uint32 To = 2; +} + +// PortList is a list of ports. +message PortList { + repeated PortRange range = 1; +} diff --git a/subproject/Xray-core-main/common/net/port_test.go b/subproject/Xray-core-main/common/net/port_test.go new file mode 100644 index 00000000..23201f97 --- /dev/null +++ b/subproject/Xray-core-main/common/net/port_test.go @@ -0,0 +1,18 @@ +package net_test + +import ( + "testing" + + . "github.com/xtls/xray-core/common/net" +) + +func TestPortRangeContains(t *testing.T) { + portRange := &PortRange{ + From: 53, + To: 53, + } + + if !portRange.Contains(Port(53)) { + t.Error("expected port range containing 53, but actually not") + } +} diff --git a/subproject/Xray-core-main/common/net/system.go b/subproject/Xray-core-main/common/net/system.go new file mode 100644 index 00000000..136da0c1 --- /dev/null +++ b/subproject/Xray-core-main/common/net/system.go @@ -0,0 +1,102 @@ +package net + +import "net" + +// DialTCP is an alias of net.DialTCP. +var ( + DialTCP = net.DialTCP + DialUDP = net.DialUDP + DialUnix = net.DialUnix + Dial = net.Dial +) + +type ListenConfig = net.ListenConfig + +type KeepAliveConfig = net.KeepAliveConfig + +var ( + Listen = net.Listen + ListenTCP = net.ListenTCP + ListenUDP = net.ListenUDP + ListenUnix = net.ListenUnix +) + +var LookupIP = net.LookupIP + +var FileConn = net.FileConn + +// ParseIP is an alias of net.ParseIP +var ParseIP = net.ParseIP + +var ParseCIDR = net.ParseCIDR + +var ResolveIPAddr = net.ResolveIPAddr + +var InterfaceByName = net.InterfaceByName + +var SplitHostPort = net.SplitHostPort + +var CIDRMask = net.CIDRMask + +type ( + Addr = net.Addr + Conn = net.Conn + PacketConn = net.PacketConn +) + +type ( + TCPAddr = net.TCPAddr + TCPConn = net.TCPConn +) + +type ( + UDPAddr = net.UDPAddr + UDPConn = net.UDPConn +) + +type ( + UnixAddr = net.UnixAddr + UnixConn = net.UnixConn +) + +type IPAddr = net.IPAddr + +// IP is an alias for net.IP. +type ( + IP = net.IP + IPMask = net.IPMask + IPNet = net.IPNet +) + +const ( + IPv4len = net.IPv4len + IPv6len = net.IPv6len +) + +type ( + Error = net.Error + AddrError = net.AddrError +) + +type ( + Dialer = net.Dialer + Listener = net.Listener + TCPListener = net.TCPListener + UnixListener = net.UnixListener +) + +var ( + ResolveTCPAddr = net.ResolveTCPAddr + ResolveUDPAddr = net.ResolveUDPAddr + ResolveUnixAddr = net.ResolveUnixAddr +) + +type Resolver = net.Resolver + +var DefaultResolver = net.DefaultResolver + +var JoinHostPort = net.JoinHostPort + +var InterfaceAddrs = net.InterfaceAddrs + +var Interfaces = net.Interfaces diff --git a/subproject/Xray-core-main/common/ocsp/ocsp.go b/subproject/Xray-core-main/common/ocsp/ocsp.go new file mode 100644 index 00000000..d67670af --- /dev/null +++ b/subproject/Xray-core-main/common/ocsp/ocsp.go @@ -0,0 +1,136 @@ +package ocsp + +import ( + "bytes" + "crypto/x509" + "encoding/pem" + "io" + "net/http" + "os" + + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/platform/filesystem" + "golang.org/x/crypto/ocsp" +) + +func GetOCSPForFile(path string) ([]byte, error) { + return filesystem.ReadFile(path) +} + +func CheckOCSPFileIsNotExist(path string) bool { + _, err := os.Stat(path) + if err != nil { + return os.IsNotExist(err) + } + return false +} + +func GetOCSPStapling(cert [][]byte, path string) ([]byte, error) { + ocspData, err := GetOCSPForFile(path) + if err != nil { + ocspData, err = GetOCSPForCert(cert) + if err != nil { + return nil, err + } + if !CheckOCSPFileIsNotExist(path) { + err = os.Remove(path) + if err != nil { + return nil, err + } + } + newFile, err := os.Create(path) + if err != nil { + return nil, err + } + newFile.Write(ocspData) + defer newFile.Close() + } + return ocspData, nil +} + +func GetOCSPForCert(cert [][]byte) ([]byte, error) { + bundle := new(bytes.Buffer) + for _, derBytes := range cert { + err := pem.Encode(bundle, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) + if err != nil { + return nil, err + } + } + pemBundle := bundle.Bytes() + + certificates, err := parsePEMBundle(pemBundle) + if err != nil { + return nil, err + } + issuedCert := certificates[0] + if len(issuedCert.OCSPServer) == 0 { + return nil, errors.New("no OCSP server specified in cert") + } + if len(certificates) == 1 { + if len(issuedCert.IssuingCertificateURL) == 0 { + return nil, errors.New("no issuing certificate URL") + } + resp, errC := http.Get(issuedCert.IssuingCertificateURL[0]) + if errC != nil { + return nil, errors.New("no issuing certificate URL") + } + defer resp.Body.Close() + + issuerBytes, errC := io.ReadAll(resp.Body) + if errC != nil { + return nil, errors.New(errC) + } + + issuerCert, errC := x509.ParseCertificate(issuerBytes) + if errC != nil { + return nil, errors.New(errC) + } + + certificates = append(certificates, issuerCert) + } + issuerCert := certificates[1] + + ocspReq, err := ocsp.CreateRequest(issuedCert, issuerCert, nil) + if err != nil { + return nil, err + } + reader := bytes.NewReader(ocspReq) + req, err := http.Post(issuedCert.OCSPServer[0], "application/ocsp-request", reader) + if err != nil { + return nil, errors.New(err) + } + defer req.Body.Close() + ocspResBytes, err := io.ReadAll(req.Body) + if err != nil { + return nil, errors.New(err) + } + return ocspResBytes, nil +} + +// parsePEMBundle parses a certificate bundle from top to bottom and returns +// a slice of x509 certificates. This function will error if no certificates are found. +func parsePEMBundle(bundle []byte) ([]*x509.Certificate, error) { + var certificates []*x509.Certificate + var certDERBlock *pem.Block + + for { + certDERBlock, bundle = pem.Decode(bundle) + if certDERBlock == nil { + break + } + + if certDERBlock.Type == "CERTIFICATE" { + cert, err := x509.ParseCertificate(certDERBlock.Bytes) + if err != nil { + return nil, err + } + certificates = append(certificates, cert) + } + } + + if len(certificates) == 0 { + return nil, errors.New("no certificates were found while parsing the bundle") + } + + return certificates, nil +} diff --git a/subproject/Xray-core-main/common/peer/latency.go b/subproject/Xray-core-main/common/peer/latency.go new file mode 100644 index 00000000..aae292ed --- /dev/null +++ b/subproject/Xray-core-main/common/peer/latency.go @@ -0,0 +1,30 @@ +package peer + +import ( + "sync" +) + +type Latency interface { + Value() uint64 +} + +type HasLatency interface { + ConnectionLatency() Latency + HandshakeLatency() Latency +} + +type AverageLatency struct { + access sync.Mutex + value uint64 +} + +func (al *AverageLatency) Update(newValue uint64) { + al.access.Lock() + defer al.access.Unlock() + + al.value = (al.value + newValue*2) / 3 +} + +func (al *AverageLatency) Value() uint64 { + return al.value +} diff --git a/subproject/Xray-core-main/common/peer/peer.go b/subproject/Xray-core-main/common/peer/peer.go new file mode 100644 index 00000000..333defff --- /dev/null +++ b/subproject/Xray-core-main/common/peer/peer.go @@ -0,0 +1 @@ +package peer diff --git a/subproject/Xray-core-main/common/platform/filesystem/file.go b/subproject/Xray-core-main/common/platform/filesystem/file.go new file mode 100644 index 00000000..f36838ce --- /dev/null +++ b/subproject/Xray-core-main/common/platform/filesystem/file.go @@ -0,0 +1,56 @@ +package filesystem + +import ( + "io" + "os" + "path/filepath" + + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/platform" +) + +type FileReaderFunc func(path string) (io.ReadCloser, error) + +var NewFileReader FileReaderFunc = func(path string) (io.ReadCloser, error) { + return os.Open(path) +} + +func ReadFile(path string) ([]byte, error) { + reader, err := NewFileReader(path) + if err != nil { + return nil, err + } + defer reader.Close() + + return buf.ReadAllToBytes(reader) +} + +func ReadAsset(file string) ([]byte, error) { + return ReadFile(platform.GetAssetLocation(file)) +} + +func OpenAsset(file string) (io.ReadCloser, error) { + return NewFileReader(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 { + bytes, err := ReadFile(src) + if err != nil { + return err + } + f, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY, 0o644) + if err != nil { + return err + } + defer f.Close() + + _, err = f.Write(bytes) + return err +} diff --git a/subproject/Xray-core-main/common/platform/others.go b/subproject/Xray-core-main/common/platform/others.go new file mode 100644 index 00000000..be86b6fa --- /dev/null +++ b/subproject/Xray-core-main/common/platform/others.go @@ -0,0 +1,41 @@ +//go:build !windows +// +build !windows + +package platform + +import ( + "os" + "path/filepath" +) + +func LineSeparator() string { + return "\n" +} + +// GetAssetLocation searches for `file` in the env dir, the executable dir, and certain locations +func GetAssetLocation(file string) string { + assetPath := NewEnvFlag(AssetLocation).GetValue(getExecutableDir) + defPath := filepath.Join(assetPath, file) + for _, p := range []string{ + defPath, + filepath.Join("/usr/local/share/xray/", file), + filepath.Join("/usr/share/xray/", file), + filepath.Join("/opt/share/xray/", file), + } { + if _, err := os.Stat(p); os.IsNotExist(err) { + continue + } + + // asset found + return p + } + + // asset not found, let the caller throw out the error + 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) +} diff --git a/subproject/Xray-core-main/common/platform/platform.go b/subproject/Xray-core-main/common/platform/platform.go new file mode 100644 index 00000000..6446873b --- /dev/null +++ b/subproject/Xray-core-main/common/platform/platform.go @@ -0,0 +1,93 @@ +package platform // import "github.com/xtls/xray-core/common/platform" + +import ( + "os" + "path/filepath" + "strconv" + "strings" +) + +const ( + ConfigLocation = "xray.location.config" + ConfdirLocation = "xray.location.confdir" + AssetLocation = "xray.location.asset" + CertLocation = "xray.location.cert" + + UseReadV = "xray.buf.readv" + UseFreedomSplice = "xray.buf.splice" + UseVmessPadding = "xray.vmess.padding" + UseCone = "xray.cone.disabled" + + BufferSize = "xray.ray.buffer.size" + BrowserDialerAddress = "xray.browser.dialer" + XUDPLog = "xray.xudp.show" + XUDPBaseKey = "xray.xudp.basekey" + + TunFdKey = "xray.tun.fd" + + MphCachePath = "xray.mph.cache" +) + +type EnvFlag struct { + Name string + AltName string +} + +func NewEnvFlag(name string) EnvFlag { + return EnvFlag{ + Name: name, + AltName: NormalizeEnvName(name), + } +} + +func (f EnvFlag) GetValue(defaultValue func() string) string { + if v, found := os.LookupEnv(f.Name); found { + return v + } + if len(f.AltName) > 0 { + if v, found := os.LookupEnv(f.AltName); found { + return v + } + } + + return defaultValue() +} + +func (f EnvFlag) GetValueAsInt(defaultValue int) int { + useDefaultValue := false + s := f.GetValue(func() string { + useDefaultValue = true + return "" + }) + if useDefaultValue { + return defaultValue + } + v, err := strconv.ParseInt(s, 10, 32) + if err != nil { + return defaultValue + } + return int(v) +} + +func NormalizeEnvName(name string) string { + return strings.ReplaceAll(strings.ToUpper(strings.TrimSpace(name)), ".", "_") +} + +func getExecutableDir() string { + exec, err := os.Executable() + if err != nil { + return "" + } + return filepath.Dir(exec) +} + +func GetConfigurationPath() string { + configPath := NewEnvFlag(ConfigLocation).GetValue(getExecutableDir) + return filepath.Join(configPath, "config.json") +} + +// GetConfDirPath reads "xray.location.confdir" +func GetConfDirPath() string { + configPath := NewEnvFlag(ConfdirLocation).GetValue(func() string { return "" }) + return configPath +} diff --git a/subproject/Xray-core-main/common/platform/platform_test.go b/subproject/Xray-core-main/common/platform/platform_test.go new file mode 100644 index 00000000..ae823217 --- /dev/null +++ b/subproject/Xray-core-main/common/platform/platform_test.go @@ -0,0 +1,65 @@ +package platform_test + +import ( + "os" + "path/filepath" + "runtime" + "testing" + + "github.com/xtls/xray-core/common" + . "github.com/xtls/xray-core/common/platform" +) + +func TestNormalizeEnvName(t *testing.T) { + cases := []struct { + input string + output string + }{ + { + input: "a", + output: "A", + }, + { + input: "a.a", + output: "A_A", + }, + { + input: "A.A.B", + output: "A_A_B", + }, + } + for _, test := range cases { + if v := NormalizeEnvName(test.input); v != test.output { + t.Error("unexpected output: ", v, " want ", test.output) + } + } +} + +func TestEnvFlag(t *testing.T) { + if v := (EnvFlag{ + Name: "xxxxx.y", + }.GetValueAsInt(10)); v != 10 { + t.Error("env value: ", v) + } +} + +func TestGetAssetLocation(t *testing.T) { + exec, err := os.Executable() + common.Must(err) + + loc := GetAssetLocation("t") + if filepath.Dir(loc) != filepath.Dir(exec) { + t.Error("asset dir: ", loc, " not in ", exec) + } + + os.Setenv("xray.location.asset", "/xray") + if runtime.GOOS == "windows" { + if v := GetAssetLocation("t"); v != "\\xray\\t" { + t.Error("asset loc: ", v) + } + } else { + if v := GetAssetLocation("t"); v != "/xray/t" { + t.Error("asset loc: ", v) + } + } +} diff --git a/subproject/Xray-core-main/common/platform/windows.go b/subproject/Xray-core-main/common/platform/windows.go new file mode 100644 index 00000000..684ddc9c --- /dev/null +++ b/subproject/Xray-core-main/common/platform/windows.go @@ -0,0 +1,22 @@ +//go:build windows +// +build windows + +package platform + +import "path/filepath" + +func LineSeparator() string { + return "\r\n" +} + +// GetAssetLocation searches for `file` in the env dir and the executable dir +func GetAssetLocation(file string) string { + assetPath := NewEnvFlag(AssetLocation).GetValue(getExecutableDir) + 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) +} diff --git a/subproject/Xray-core-main/common/protocol/account.go b/subproject/Xray-core-main/common/protocol/account.go new file mode 100644 index 00000000..75568817 --- /dev/null +++ b/subproject/Xray-core-main/common/protocol/account.go @@ -0,0 +1,14 @@ +package protocol + +import "google.golang.org/protobuf/proto" + +// Account is a user identity used for authentication. +type Account interface { + Equals(Account) bool + ToProto() proto.Message +} + +// AsAccount is an object can be converted into account. +type AsAccount interface { + AsAccount() (Account, error) +} diff --git a/subproject/Xray-core-main/common/protocol/address.go b/subproject/Xray-core-main/common/protocol/address.go new file mode 100644 index 00000000..0dcb8165 --- /dev/null +++ b/subproject/Xray-core-main/common/protocol/address.go @@ -0,0 +1,259 @@ +package protocol + +import ( + "io" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/serial" +) + +type AddressOption func(*option) + +func PortThenAddress() AddressOption { + return func(p *option) { + p.portFirst = true + } +} + +func AddressFamilyByte(b byte, f net.AddressFamily) AddressOption { + if b >= 16 { + panic("address family byte too big") + } + return func(p *option) { + p.addrTypeMap[b] = f + p.addrByteMap[f] = b + } +} + +type AddressTypeParser func(byte) byte + +func WithAddressTypeParser(atp AddressTypeParser) AddressOption { + return func(p *option) { + p.typeParser = atp + } +} + +type AddressSerializer interface { + ReadAddressPort(buffer *buf.Buffer, input io.Reader) (net.Address, net.Port, error) + + WriteAddressPort(writer io.Writer, addr net.Address, port net.Port) error +} + +const afInvalid = 255 + +type option struct { + addrTypeMap [16]net.AddressFamily + addrByteMap [16]byte + portFirst bool + typeParser AddressTypeParser +} + +// NewAddressParser creates a new AddressParser +func NewAddressParser(options ...AddressOption) AddressSerializer { + var o option + for i := range o.addrByteMap { + o.addrByteMap[i] = afInvalid + } + for i := range o.addrTypeMap { + o.addrTypeMap[i] = net.AddressFamily(afInvalid) + } + for _, opt := range options { + opt(&o) + } + + ap := &addressParser{ + addrByteMap: o.addrByteMap, + addrTypeMap: o.addrTypeMap, + } + + if o.typeParser != nil { + ap.typeParser = o.typeParser + } + + if o.portFirst { + return portFirstAddressParser{ap: ap} + } + + return portLastAddressParser{ap: ap} +} + +type portFirstAddressParser struct { + ap *addressParser +} + +func (p portFirstAddressParser) ReadAddressPort(buffer *buf.Buffer, input io.Reader) (net.Address, net.Port, error) { + if buffer == nil { + buffer = buf.New() + defer buffer.Release() + } + + port, err := readPort(buffer, input) + if err != nil { + return nil, 0, err + } + + addr, err := p.ap.readAddress(buffer, input) + if err != nil { + return nil, 0, err + } + return addr, port, nil +} + +func (p portFirstAddressParser) WriteAddressPort(writer io.Writer, addr net.Address, port net.Port) error { + if err := writePort(writer, port); err != nil { + return err + } + + return p.ap.writeAddress(writer, addr) +} + +type portLastAddressParser struct { + ap *addressParser +} + +func (p portLastAddressParser) ReadAddressPort(buffer *buf.Buffer, input io.Reader) (net.Address, net.Port, error) { + if buffer == nil { + buffer = buf.New() + defer buffer.Release() + } + + addr, err := p.ap.readAddress(buffer, input) + if err != nil { + return nil, 0, err + } + + port, err := readPort(buffer, input) + if err != nil { + return nil, 0, err + } + + return addr, port, nil +} + +func (p portLastAddressParser) WriteAddressPort(writer io.Writer, addr net.Address, port net.Port) error { + if err := p.ap.writeAddress(writer, addr); err != nil { + return err + } + + return writePort(writer, port) +} + +func readPort(b *buf.Buffer, reader io.Reader) (net.Port, error) { + if _, err := b.ReadFullFrom(reader, 2); err != nil { + return 0, err + } + return net.PortFromBytes(b.BytesFrom(-2)), nil +} + +func writePort(writer io.Writer, port net.Port) error { + return common.Error2(serial.WriteUint16(writer, port.Value())) +} + +func maybeIPPrefix(b byte) bool { + return b == '[' || (b >= '0' && b <= '9') +} + +func isValidDomain(d string) bool { + for _, c := range d { + if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '-' || c == '.' || c == '_') { + return false + } + } + return true +} + +type addressParser struct { + addrTypeMap [16]net.AddressFamily + addrByteMap [16]byte + typeParser AddressTypeParser +} + +func (p *addressParser) readAddress(b *buf.Buffer, reader io.Reader) (net.Address, error) { + if _, err := b.ReadFullFrom(reader, 1); err != nil { + return nil, err + } + + addrType := b.Byte(b.Len() - 1) + if p.typeParser != nil { + addrType = p.typeParser(addrType) + } + + if addrType >= 16 { + return nil, errors.New("unknown address type: ", addrType) + } + + addrFamily := p.addrTypeMap[addrType] + if addrFamily == net.AddressFamily(afInvalid) { + return nil, errors.New("unknown address type: ", addrType) + } + + switch addrFamily { + case net.AddressFamilyIPv4: + if _, err := b.ReadFullFrom(reader, 4); err != nil { + return nil, err + } + return net.IPAddress(b.BytesFrom(-4)), nil + case net.AddressFamilyIPv6: + if _, err := b.ReadFullFrom(reader, 16); err != nil { + return nil, err + } + return net.IPAddress(b.BytesFrom(-16)), nil + case net.AddressFamilyDomain: + if _, err := b.ReadFullFrom(reader, 1); err != nil { + return nil, err + } + domainLength := int32(b.Byte(b.Len() - 1)) + if _, err := b.ReadFullFrom(reader, domainLength); err != nil { + return nil, err + } + domain := string(b.BytesFrom(-domainLength)) + if maybeIPPrefix(domain[0]) { + addr := net.ParseAddress(domain) + if addr.Family().IsIP() { + return addr, nil + } + } + if !isValidDomain(domain) { + return nil, errors.New("invalid domain name: ", domain) + } + return net.DomainAddress(domain), nil + default: + panic("impossible case") + } +} + +func (p *addressParser) writeAddress(writer io.Writer, address net.Address) error { + tb := p.addrByteMap[address.Family()] + if tb == afInvalid { + return errors.New("unknown address family", address.Family()) + } + + switch address.Family() { + case net.AddressFamilyIPv4, net.AddressFamilyIPv6: + if _, err := writer.Write([]byte{tb}); err != nil { + return err + } + if _, err := writer.Write(address.IP()); err != nil { + return err + } + case net.AddressFamilyDomain: + domain := address.Domain() + if isDomainTooLong(domain) { + return errors.New("Super long domain is not supported: ", domain) + } + + if _, err := writer.Write([]byte{tb, byte(len(domain))}); err != nil { + return err + } + if _, err := writer.Write([]byte(domain)); err != nil { + return err + } + default: + panic("Unknown family type.") + } + + return nil +} diff --git a/subproject/Xray-core-main/common/protocol/address_test.go b/subproject/Xray-core-main/common/protocol/address_test.go new file mode 100644 index 00000000..ceecded2 --- /dev/null +++ b/subproject/Xray-core-main/common/protocol/address_test.go @@ -0,0 +1,242 @@ +package protocol_test + +import ( + "bytes" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/net" + . "github.com/xtls/xray-core/common/protocol" +) + +func TestAddressReading(t *testing.T) { + data := []struct { + Options []AddressOption + Input []byte + Address net.Address + Port net.Port + Error bool + }{ + { + Options: []AddressOption{}, + Input: []byte{}, + Error: true, + }, + { + Options: []AddressOption{}, + Input: []byte{0, 0, 0, 0, 0}, + Error: true, + }, + { + Options: []AddressOption{AddressFamilyByte(0x01, net.AddressFamilyIPv4)}, + Input: []byte{1, 0, 0, 0, 0, 0, 53}, + Address: net.IPAddress([]byte{0, 0, 0, 0}), + Port: net.Port(53), + }, + { + Options: []AddressOption{AddressFamilyByte(0x01, net.AddressFamilyIPv4), PortThenAddress()}, + Input: []byte{0, 53, 1, 0, 0, 0, 0}, + Address: net.IPAddress([]byte{0, 0, 0, 0}), + Port: net.Port(53), + }, + { + Options: []AddressOption{AddressFamilyByte(0x01, net.AddressFamilyIPv4)}, + Input: []byte{1, 0, 0, 0, 0}, + Error: true, + }, + { + Options: []AddressOption{AddressFamilyByte(0x04, net.AddressFamilyIPv6)}, + Input: []byte{4, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 0, 80}, + Address: net.IPAddress([]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6}), + Port: net.Port(80), + }, + { + Options: []AddressOption{AddressFamilyByte(0x03, net.AddressFamilyDomain)}, + Input: []byte{3, 11, 101, 120, 97, 109, 112, 108, 101, 46, 99, 111, 109, 0, 80}, + Address: net.DomainAddress("example.com"), + Port: net.Port(80), + }, + { + Options: []AddressOption{AddressFamilyByte(0x03, net.AddressFamilyDomain)}, + Input: []byte{3, 9, 118, 50, 114, 97, 121, 46, 99, 111, 109, 0}, + Error: true, + }, + { + Options: []AddressOption{AddressFamilyByte(0x03, net.AddressFamilyDomain)}, + Input: []byte{3, 7, 56, 46, 56, 46, 56, 46, 56, 0, 80}, + Address: net.ParseAddress("8.8.8.8"), + Port: net.Port(80), + }, + { + Options: []AddressOption{AddressFamilyByte(0x03, net.AddressFamilyDomain)}, + Input: []byte{3, 7, 10, 46, 56, 46, 56, 46, 56, 0, 80}, + Error: true, + }, + { + Options: []AddressOption{AddressFamilyByte(0x03, net.AddressFamilyDomain)}, + Input: append(append([]byte{3, 24}, []byte("2a00:1450:4007:816::200e")...), 0, 80), + Address: net.ParseAddress("2a00:1450:4007:816::200e"), + Port: net.Port(80), + }, + } + + for _, tc := range data { + parser := NewAddressParser(tc.Options...) + + b := buf.New() + addr, port, err := parser.ReadAddressPort(b, bytes.NewReader(tc.Input)) + b.Release() + if tc.Error { + if err == nil { + t.Errorf("Expect error but not: %v", tc) + } + } else { + if err != nil { + t.Errorf("Expect no error but: %s %v", err.Error(), tc) + } + + if addr != tc.Address { + t.Error("Got address ", addr.String(), " want ", tc.Address.String()) + } + + if tc.Port != port { + t.Error("Got port ", port, " want ", tc.Port) + } + } + } +} + +func TestAddressWriting(t *testing.T) { + data := []struct { + Options []AddressOption + Address net.Address + Port net.Port + Bytes []byte + Error bool + }{ + { + Options: []AddressOption{AddressFamilyByte(0x01, net.AddressFamilyIPv4)}, + Address: net.LocalHostIP, + Port: net.Port(80), + Bytes: []byte{1, 127, 0, 0, 1, 0, 80}, + }, + } + + for _, tc := range data { + parser := NewAddressParser(tc.Options...) + + b := buf.New() + err := parser.WriteAddressPort(b, tc.Address, tc.Port) + if tc.Error { + if err == nil { + t.Error("Expect error but nil") + } + } else { + common.Must(err) + if diff := cmp.Diff(tc.Bytes, b.Bytes()); diff != "" { + t.Error(err) + } + } + } +} + +func BenchmarkAddressReadingIPv4(b *testing.B) { + parser := NewAddressParser(AddressFamilyByte(0x01, net.AddressFamilyIPv4)) + cache := buf.New() + defer cache.Release() + + payload := buf.New() + defer payload.Release() + + raw := []byte{1, 0, 0, 0, 0, 0, 53} + payload.Write(raw) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _, err := parser.ReadAddressPort(cache, payload) + common.Must(err) + cache.Clear() + payload.Clear() + payload.Extend(int32(len(raw))) + } +} + +func BenchmarkAddressReadingIPv6(b *testing.B) { + parser := NewAddressParser(AddressFamilyByte(0x04, net.AddressFamilyIPv6)) + cache := buf.New() + defer cache.Release() + + payload := buf.New() + defer payload.Release() + + raw := []byte{4, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 0, 80} + payload.Write(raw) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _, err := parser.ReadAddressPort(cache, payload) + common.Must(err) + cache.Clear() + payload.Clear() + payload.Extend(int32(len(raw))) + } +} + +func BenchmarkAddressReadingDomain(b *testing.B) { + parser := NewAddressParser(AddressFamilyByte(0x03, net.AddressFamilyDomain)) + cache := buf.New() + defer cache.Release() + + payload := buf.New() + defer payload.Release() + + raw := []byte{3, 9, 118, 50, 114, 97, 121, 46, 99, 111, 109, 0, 80} + payload.Write(raw) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _, err := parser.ReadAddressPort(cache, payload) + common.Must(err) + cache.Clear() + payload.Clear() + payload.Extend(int32(len(raw))) + } +} + +func BenchmarkAddressWritingIPv4(b *testing.B) { + parser := NewAddressParser(AddressFamilyByte(0x01, net.AddressFamilyIPv4)) + writer := buf.New() + defer writer.Release() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + common.Must(parser.WriteAddressPort(writer, net.LocalHostIP, net.Port(80))) + writer.Clear() + } +} + +func BenchmarkAddressWritingIPv6(b *testing.B) { + parser := NewAddressParser(AddressFamilyByte(0x04, net.AddressFamilyIPv6)) + writer := buf.New() + defer writer.Release() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + common.Must(parser.WriteAddressPort(writer, net.LocalHostIPv6, net.Port(80))) + writer.Clear() + } +} + +func BenchmarkAddressWritingDomain(b *testing.B) { + parser := NewAddressParser(AddressFamilyByte(0x02, net.AddressFamilyDomain)) + writer := buf.New() + defer writer.Release() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + common.Must(parser.WriteAddressPort(writer, net.DomainAddress("www.example.com"), net.Port(80))) + writer.Clear() + } +} diff --git a/subproject/Xray-core-main/common/protocol/bittorrent/bittorrent.go b/subproject/Xray-core-main/common/protocol/bittorrent/bittorrent.go new file mode 100644 index 00000000..70830764 --- /dev/null +++ b/subproject/Xray-core-main/common/protocol/bittorrent/bittorrent.go @@ -0,0 +1,90 @@ +package bittorrent + +import ( + "encoding/binary" + "errors" + "math" + "time" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" +) + +type SniffHeader struct{} + +func (h *SniffHeader) Protocol() string { + return "bittorrent" +} + +func (h *SniffHeader) Domain() string { + return "" +} + +var errNotBittorrent = errors.New("not bittorrent header") + +func SniffBittorrent(b []byte) (*SniffHeader, error) { + if len(b) < 20 { + return nil, common.ErrNoClue + } + + if b[0] == 19 && string(b[1:20]) == "BitTorrent protocol" { + return &SniffHeader{}, nil + } + + return nil, errNotBittorrent +} + +func SniffUTP(b []byte) (*SniffHeader, error) { + if len(b) < 20 { + return nil, common.ErrNoClue + } + + buffer := buf.FromBytes(b) + + var typeAndVersion uint8 + + if binary.Read(buffer, binary.BigEndian, &typeAndVersion) != nil { + return nil, common.ErrNoClue + } else if b[0]>>4&0xF > 4 || b[0]&0xF != 1 { + return nil, errNotBittorrent + } + + var extension uint8 + + if binary.Read(buffer, binary.BigEndian, &extension) != nil { + return nil, common.ErrNoClue + } else if extension != 0 && extension != 1 { + return nil, errNotBittorrent + } + + for extension != 0 { + if extension != 1 { + return nil, errNotBittorrent + } + if binary.Read(buffer, binary.BigEndian, &extension) != nil { + return nil, common.ErrNoClue + } + + var length uint8 + if err := binary.Read(buffer, binary.BigEndian, &length); err != nil { + return nil, common.ErrNoClue + } + if common.Error2(buffer.ReadBytes(int32(length))) != nil { + return nil, common.ErrNoClue + } + } + + if common.Error2(buffer.ReadBytes(2)) != nil { + return nil, common.ErrNoClue + } + + var timestamp uint32 + if err := binary.Read(buffer, binary.BigEndian, ×tamp); err != nil { + return nil, common.ErrNoClue + } + if math.Abs(float64(time.Now().UnixMicro()-int64(timestamp))) > float64(24*time.Hour) { + return nil, errNotBittorrent + } + + return &SniffHeader{}, nil +} diff --git a/subproject/Xray-core-main/common/protocol/context.go b/subproject/Xray-core-main/common/protocol/context.go new file mode 100644 index 00000000..6bb51042 --- /dev/null +++ b/subproject/Xray-core-main/common/protocol/context.go @@ -0,0 +1,23 @@ +package protocol + +import ( + "context" +) + +type key int + +const ( + requestKey key = iota +) + +func ContextWithRequestHeader(ctx context.Context, request *RequestHeader) context.Context { + return context.WithValue(ctx, requestKey, request) +} + +func RequestHeaderFromContext(ctx context.Context) *RequestHeader { + request := ctx.Value(requestKey) + if request == nil { + return nil + } + return request.(*RequestHeader) +} diff --git a/subproject/Xray-core-main/common/protocol/dns/io.go b/subproject/Xray-core-main/common/protocol/dns/io.go new file mode 100644 index 00000000..dd88b61d --- /dev/null +++ b/subproject/Xray-core-main/common/protocol/dns/io.go @@ -0,0 +1,144 @@ +package dns + +import ( + "encoding/binary" + "sync" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/serial" + "golang.org/x/net/dns/dnsmessage" +) + +func PackMessage(msg *dnsmessage.Message) (*buf.Buffer, error) { + buffer := buf.New() + rawBytes := buffer.Extend(buf.Size) + packed, err := msg.AppendPack(rawBytes[:0]) + if err != nil { + buffer.Release() + return nil, err + } + buffer.Resize(0, int32(len(packed))) + return buffer, nil +} + +type MessageReader interface { + ReadMessage() (*buf.Buffer, error) +} + +type UDPReader struct { + buf.Reader + + access sync.Mutex + cache buf.MultiBuffer +} + +func (r *UDPReader) readCache() *buf.Buffer { + r.access.Lock() + defer r.access.Unlock() + + mb, b := buf.SplitFirst(r.cache) + r.cache = mb + return b +} + +func (r *UDPReader) refill() error { + mb, err := r.Reader.ReadMultiBuffer() + if err != nil { + return err + } + r.access.Lock() + r.cache = mb + r.access.Unlock() + return nil +} + +// ReadMessage implements MessageReader. +func (r *UDPReader) ReadMessage() (*buf.Buffer, error) { + for { + b := r.readCache() + if b != nil { + return b, nil + } + if err := r.refill(); err != nil { + return nil, err + } + } +} + +// Close implements common.Closable. +func (r *UDPReader) Close() error { + defer func() { + r.access.Lock() + buf.ReleaseMulti(r.cache) + r.cache = nil + r.access.Unlock() + }() + + return common.Close(r.Reader) +} + +type TCPReader struct { + reader *buf.BufferedReader +} + +func NewTCPReader(reader buf.Reader) *TCPReader { + return &TCPReader{ + reader: &buf.BufferedReader{ + Reader: reader, + }, + } +} + +func (r *TCPReader) ReadMessage() (*buf.Buffer, error) { + size, err := serial.ReadUint16(r.reader) + if err != nil { + return nil, err + } + if size > buf.Size { + return nil, errors.New("message size too large: ", size) + } + b := buf.New() + if _, err := b.ReadFullFrom(r.reader, int32(size)); err != nil { + return nil, err + } + return b, nil +} + +func (r *TCPReader) Interrupt() { + common.Interrupt(r.reader) +} + +func (r *TCPReader) Close() error { + return common.Close(r.reader) +} + +type MessageWriter interface { + WriteMessage(msg *buf.Buffer) error +} + +type UDPWriter struct { + buf.Writer +} + +func (w *UDPWriter) WriteMessage(b *buf.Buffer) error { + return w.WriteMultiBuffer(buf.MultiBuffer{b}) +} + +type TCPWriter struct { + buf.Writer +} + +func (w *TCPWriter) WriteMessage(b *buf.Buffer) error { + if b.IsEmpty() { + return nil + } + + mb := make(buf.MultiBuffer, 0, 2) + + size := buf.New() + binary.BigEndian.PutUint16(size.Extend(2), uint16(b.Len())) + mb = append(mb, size, b) + return w.WriteMultiBuffer(mb) +} diff --git a/subproject/Xray-core-main/common/protocol/headers.go b/subproject/Xray-core-main/common/protocol/headers.go new file mode 100644 index 00000000..9c6573cb --- /dev/null +++ b/subproject/Xray-core-main/common/protocol/headers.go @@ -0,0 +1,95 @@ +package protocol + +import ( + "runtime" + + "github.com/xtls/xray-core/common/bitmask" + "github.com/xtls/xray-core/common/net" + "golang.org/x/sys/cpu" +) + +// RequestCommand is a custom command in a proxy request. +type RequestCommand byte + +const ( + RequestCommandTCP = RequestCommand(0x01) + RequestCommandUDP = RequestCommand(0x02) + RequestCommandMux = RequestCommand(0x03) + RequestCommandRvs = RequestCommand(0x04) +) + +func (c RequestCommand) TransferType() TransferType { + switch c { + case RequestCommandTCP, RequestCommandMux, RequestCommandRvs: + return TransferTypeStream + case RequestCommandUDP: + return TransferTypePacket + default: + return TransferTypeStream + } +} + +const ( + // [DEPRECATED 2023-06] RequestOptionChunkStream indicates request payload is chunked. Each chunk consists of length, authentication and payload. + RequestOptionChunkStream bitmask.Byte = 0x01 + + // 0x02 legacy setting + + RequestOptionChunkMasking bitmask.Byte = 0x04 + + RequestOptionGlobalPadding bitmask.Byte = 0x08 + + RequestOptionAuthenticatedLength bitmask.Byte = 0x10 +) + +type RequestHeader struct { + Version byte + Command RequestCommand + Option bitmask.Byte + Security SecurityType + Port net.Port + Address net.Address + User *MemoryUser +} + +func (h *RequestHeader) Destination() net.Destination { + if h.Command == RequestCommandUDP { + return net.UDPDestination(h.Address, h.Port) + } + return net.TCPDestination(h.Address, h.Port) +} + +const ( + ResponseOptionConnectionReuse bitmask.Byte = 0x01 +) + +type ResponseCommand interface{} + +type ResponseHeader struct { + Option bitmask.Byte + Command ResponseCommand +} + +var ( + // Keep in sync with crypto/tls/cipher_suites.go. + hasGCMAsmAMD64 = cpu.X86.HasAES && cpu.X86.HasPCLMULQDQ && cpu.X86.HasSSE41 && cpu.X86.HasSSSE3 + hasGCMAsmARM64 = (cpu.ARM64.HasAES && cpu.ARM64.HasPMULL) || (runtime.GOOS == "darwin" && runtime.GOARCH == "arm64") + hasGCMAsmS390X = cpu.S390X.HasAES && cpu.S390X.HasAESCTR && cpu.S390X.HasGHASH + hasGCMAsmPPC64 = runtime.GOARCH == "ppc64" || runtime.GOARCH == "ppc64le" + + HasAESGCMHardwareSupport = hasGCMAsmAMD64 || hasGCMAsmARM64 || hasGCMAsmS390X || hasGCMAsmPPC64 +) + +func (sc *SecurityConfig) GetSecurityType() SecurityType { + if sc == nil || sc.Type == SecurityType_AUTO { + if HasAESGCMHardwareSupport { + return SecurityType_AES128_GCM + } + return SecurityType_CHACHA20_POLY1305 + } + return sc.Type +} + +func isDomainTooLong(domain string) bool { + return len(domain) > 256 +} diff --git a/subproject/Xray-core-main/common/protocol/headers.pb.go b/subproject/Xray-core-main/common/protocol/headers.pb.go new file mode 100644 index 00000000..21ebe895 --- /dev/null +++ b/subproject/Xray-core-main/common/protocol/headers.pb.go @@ -0,0 +1,193 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: common/protocol/headers.proto + +package protocol + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type SecurityType int32 + +const ( + SecurityType_UNKNOWN SecurityType = 0 + SecurityType_AUTO SecurityType = 2 + SecurityType_AES128_GCM SecurityType = 3 + SecurityType_CHACHA20_POLY1305 SecurityType = 4 + SecurityType_NONE SecurityType = 5 // [DEPRECATED 2023-06] + SecurityType_ZERO SecurityType = 6 +) + +// Enum value maps for SecurityType. +var ( + SecurityType_name = map[int32]string{ + 0: "UNKNOWN", + 2: "AUTO", + 3: "AES128_GCM", + 4: "CHACHA20_POLY1305", + 5: "NONE", + 6: "ZERO", + } + SecurityType_value = map[string]int32{ + "UNKNOWN": 0, + "AUTO": 2, + "AES128_GCM": 3, + "CHACHA20_POLY1305": 4, + "NONE": 5, + "ZERO": 6, + } +) + +func (x SecurityType) Enum() *SecurityType { + p := new(SecurityType) + *p = x + return p +} + +func (x SecurityType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (SecurityType) Descriptor() protoreflect.EnumDescriptor { + return file_common_protocol_headers_proto_enumTypes[0].Descriptor() +} + +func (SecurityType) Type() protoreflect.EnumType { + return &file_common_protocol_headers_proto_enumTypes[0] +} + +func (x SecurityType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use SecurityType.Descriptor instead. +func (SecurityType) EnumDescriptor() ([]byte, []int) { + return file_common_protocol_headers_proto_rawDescGZIP(), []int{0} +} + +type SecurityConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + Type SecurityType `protobuf:"varint,1,opt,name=type,proto3,enum=xray.common.protocol.SecurityType" json:"type,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SecurityConfig) Reset() { + *x = SecurityConfig{} + mi := &file_common_protocol_headers_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SecurityConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SecurityConfig) ProtoMessage() {} + +func (x *SecurityConfig) ProtoReflect() protoreflect.Message { + mi := &file_common_protocol_headers_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SecurityConfig.ProtoReflect.Descriptor instead. +func (*SecurityConfig) Descriptor() ([]byte, []int) { + return file_common_protocol_headers_proto_rawDescGZIP(), []int{0} +} + +func (x *SecurityConfig) GetType() SecurityType { + if x != nil { + return x.Type + } + return SecurityType_UNKNOWN +} + +var File_common_protocol_headers_proto protoreflect.FileDescriptor + +const file_common_protocol_headers_proto_rawDesc = "" + + "\n" + + "\x1dcommon/protocol/headers.proto\x12\x14xray.common.protocol\"H\n" + + "\x0eSecurityConfig\x126\n" + + "\x04type\x18\x01 \x01(\x0e2\".xray.common.protocol.SecurityTypeR\x04type*`\n" + + "\fSecurityType\x12\v\n" + + "\aUNKNOWN\x10\x00\x12\b\n" + + "\x04AUTO\x10\x02\x12\x0e\n" + + "\n" + + "AES128_GCM\x10\x03\x12\x15\n" + + "\x11CHACHA20_POLY1305\x10\x04\x12\b\n" + + "\x04NONE\x10\x05\x12\b\n" + + "\x04ZERO\x10\x06B^\n" + + "\x18com.xray.common.protocolP\x01Z)github.com/xtls/xray-core/common/protocol\xaa\x02\x14Xray.Common.Protocolb\x06proto3" + +var ( + file_common_protocol_headers_proto_rawDescOnce sync.Once + file_common_protocol_headers_proto_rawDescData []byte +) + +func file_common_protocol_headers_proto_rawDescGZIP() []byte { + file_common_protocol_headers_proto_rawDescOnce.Do(func() { + file_common_protocol_headers_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_common_protocol_headers_proto_rawDesc), len(file_common_protocol_headers_proto_rawDesc))) + }) + return file_common_protocol_headers_proto_rawDescData +} + +var file_common_protocol_headers_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_common_protocol_headers_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_common_protocol_headers_proto_goTypes = []any{ + (SecurityType)(0), // 0: xray.common.protocol.SecurityType + (*SecurityConfig)(nil), // 1: xray.common.protocol.SecurityConfig +} +var file_common_protocol_headers_proto_depIdxs = []int32{ + 0, // 0: xray.common.protocol.SecurityConfig.type:type_name -> xray.common.protocol.SecurityType + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_common_protocol_headers_proto_init() } +func file_common_protocol_headers_proto_init() { + if File_common_protocol_headers_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_common_protocol_headers_proto_rawDesc), len(file_common_protocol_headers_proto_rawDesc)), + NumEnums: 1, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_common_protocol_headers_proto_goTypes, + DependencyIndexes: file_common_protocol_headers_proto_depIdxs, + EnumInfos: file_common_protocol_headers_proto_enumTypes, + MessageInfos: file_common_protocol_headers_proto_msgTypes, + }.Build() + File_common_protocol_headers_proto = out.File + file_common_protocol_headers_proto_goTypes = nil + file_common_protocol_headers_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/common/protocol/headers.proto b/subproject/Xray-core-main/common/protocol/headers.proto new file mode 100644 index 00000000..1ae3537f --- /dev/null +++ b/subproject/Xray-core-main/common/protocol/headers.proto @@ -0,0 +1,20 @@ +syntax = "proto3"; + +package xray.common.protocol; +option csharp_namespace = "Xray.Common.Protocol"; +option go_package = "github.com/xtls/xray-core/common/protocol"; +option java_package = "com.xray.common.protocol"; +option java_multiple_files = true; + +enum SecurityType { + UNKNOWN = 0; + AUTO = 2; + AES128_GCM = 3; + CHACHA20_POLY1305 = 4; + NONE = 5; // [DEPRECATED 2023-06] + ZERO = 6; +} + +message SecurityConfig { + SecurityType type = 1; +} diff --git a/subproject/Xray-core-main/common/protocol/http/headers.go b/subproject/Xray-core-main/common/protocol/http/headers.go new file mode 100644 index 00000000..db3aa670 --- /dev/null +++ b/subproject/Xray-core-main/common/protocol/http/headers.go @@ -0,0 +1,68 @@ +package http + +import ( + "net/http" + "strconv" + "strings" + + "github.com/xtls/xray-core/common/net" +) + +// ParseXForwardedFor parses X-Forwarded-For header in http headers, and return the IP list in it. +func ParseXForwardedFor(header http.Header) []net.Address { + xff := header.Get("X-Forwarded-For") + if xff == "" { + return nil + } + list := strings.Split(xff, ",") + addrs := make([]net.Address, 0, len(list)) + for _, proxy := range list { + addrs = append(addrs, net.ParseAddress(proxy)) + } + return addrs +} + +// RemoveHopByHopHeaders removes hop by hop headers in http header list. +func RemoveHopByHopHeaders(header http.Header) { + // Strip hop-by-hop header based on RFC: + // http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.1 + // https://www.mnot.net/blog/2011/07/11/what_proxies_must_do + + header.Del("Proxy-Connection") + header.Del("Proxy-Authenticate") + header.Del("Proxy-Authorization") + header.Del("TE") + header.Del("Trailers") + header.Del("Transfer-Encoding") + header.Del("Upgrade") + + connections := header.Get("Connection") + header.Del("Connection") + if connections == "" { + return + } + for _, h := range strings.Split(connections, ",") { + header.Del(strings.TrimSpace(h)) + } +} + +// ParseHost splits host and port from a raw string. Default port is used when raw string doesn't contain port. +func ParseHost(rawHost string, defaultPort net.Port) (net.Destination, error) { + port := defaultPort + host, rawPort, err := net.SplitHostPort(rawHost) + if err != nil { + if addrError, ok := err.(*net.AddrError); ok && strings.Contains(addrError.Err, "missing port") { + host = rawHost + } else { + return net.Destination{}, err + } + } else if len(rawPort) > 0 { + intPort, err := strconv.Atoi(rawPort) + if err != nil { + return net.Destination{}, err + } + port = net.Port(intPort) + } + + return net.TCPDestination(net.ParseAddress(host), port), nil +} diff --git a/subproject/Xray-core-main/common/protocol/http/headers_test.go b/subproject/Xray-core-main/common/protocol/http/headers_test.go new file mode 100644 index 00000000..80e243ae --- /dev/null +++ b/subproject/Xray-core-main/common/protocol/http/headers_test.go @@ -0,0 +1,117 @@ +package http_test + +import ( + "bufio" + "net/http" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/net" + . "github.com/xtls/xray-core/common/protocol/http" +) + +func TestParseXForwardedFor(t *testing.T) { + header := http.Header{} + header.Add("X-Forwarded-For", "129.78.138.66, 129.78.64.103") + addrs := ParseXForwardedFor(header) + if r := cmp.Diff(addrs, []net.Address{net.ParseAddress("129.78.138.66"), net.ParseAddress("129.78.64.103")}); r != "" { + t.Error(r) + } +} + +func TestHopByHopHeadersRemoving(t *testing.T) { + rawRequest := `GET /pkg/net/http/ HTTP/1.1 +Host: golang.org +Connection: keep-alive,Foo, Bar +Foo: foo +Bar: bar +Proxy-Connection: keep-alive +Proxy-Authenticate: abc +Accept-Encoding: gzip +Accept-Charset: ISO-8859-1,UTF-8;q=0.7,*;q=0.7 +Cache-Control: no-cache +Accept-Language: de,en;q=0.7,en-us;q=0.3 + +` + b := bufio.NewReader(strings.NewReader(rawRequest)) + req, err := http.ReadRequest(b) + common.Must(err) + headers := []struct { + Key string + Value string + }{ + { + Key: "Foo", + Value: "foo", + }, + { + Key: "Bar", + Value: "bar", + }, + { + Key: "Connection", + Value: "keep-alive,Foo, Bar", + }, + { + Key: "Proxy-Connection", + Value: "keep-alive", + }, + { + Key: "Proxy-Authenticate", + Value: "abc", + }, + } + for _, header := range headers { + if v := req.Header.Get(header.Key); v != header.Value { + t.Error("header ", header.Key, " = ", v, " want ", header.Value) + } + } + + RemoveHopByHopHeaders(req.Header) + + for _, header := range []string{"Connection", "Foo", "Bar", "Proxy-Connection", "Proxy-Authenticate"} { + if v := req.Header.Get(header); v != "" { + t.Error("header ", header, " = ", v) + } + } +} + +func TestParseHost(t *testing.T) { + testCases := []struct { + RawHost string + DefaultPort net.Port + Destination net.Destination + Error bool + }{ + { + RawHost: "example.com:80", + DefaultPort: 443, + Destination: net.TCPDestination(net.DomainAddress("example.com"), 80), + }, + { + RawHost: "tls.example.com", + DefaultPort: 443, + Destination: net.TCPDestination(net.DomainAddress("tls.example.com"), 443), + }, + { + RawHost: "[2401:1bc0:51f0:ec08::1]:80", + DefaultPort: 443, + Destination: net.TCPDestination(net.ParseAddress("[2401:1bc0:51f0:ec08::1]"), 80), + }, + } + + for _, testCase := range testCases { + dest, err := ParseHost(testCase.RawHost, testCase.DefaultPort) + if testCase.Error { + if err == nil { + t.Error("for test case: ", testCase.RawHost, " expected error, but actually nil") + } + } else { + if dest != testCase.Destination { + t.Error("for test case: ", testCase.RawHost, " expected host: ", testCase.Destination.String(), " but got ", dest.String()) + } + } + } +} diff --git a/subproject/Xray-core-main/common/protocol/http/sniff.go b/subproject/Xray-core-main/common/protocol/http/sniff.go new file mode 100644 index 00000000..e85a0792 --- /dev/null +++ b/subproject/Xray-core-main/common/protocol/http/sniff.go @@ -0,0 +1,117 @@ +package http + +import ( + "bytes" + "context" + "errors" + "strings" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/session" +) + +type version byte + +const ( + HTTP1 version = iota + HTTP2 +) + +type SniffHeader struct { + version version + host string +} + +func (h *SniffHeader) Protocol() string { + switch h.version { + case HTTP1: + return "http1" + case HTTP2: + return "http2" + default: + return "unknown" + } +} + +func (h *SniffHeader) Domain() string { + return h.host +} + +var ( + methods = [...]string{"get", "post", "head", "put", "delete", "options", "connect"} + + errNotHTTPMethod = errors.New("not an HTTP method") +) + +func beginWithHTTPMethod(b []byte) error { + for _, m := range &methods { + if len(b) >= len(m) && strings.EqualFold(string(b[:len(m)]), m) { + return nil + } + + if len(b) < len(m) { + return common.ErrNoClue + } + } + + return errNotHTTPMethod +} + +func SniffHTTP(b []byte, c context.Context) (*SniffHeader, error) { + content := session.ContentFromContext(c) + ShouldSniffAttr := true + // If content.Attributes have information, that means it comes from HTTP inbound PlainHTTP mode. + // It will set attributes, so skip it. + if content == nil || len(content.Attributes) != 0 { + ShouldSniffAttr = false + } + if err := beginWithHTTPMethod(b); err != nil { + return nil, err + } + + sh := &SniffHeader{ + version: HTTP1, + } + + headers := bytes.Split(b, []byte{'\n'}) + for i := 1; i < len(headers); i++ { + header := headers[i] + if len(header) == 0 { + break + } + parts := bytes.SplitN(header, []byte{':'}, 2) + if len(parts) != 2 { + continue + } + key := strings.ToLower(string(parts[0])) + value := string(bytes.TrimSpace(parts[1])) + if ShouldSniffAttr { + content.SetAttribute(key, value) // Put header in attribute + } + if key == "host" { + rawHost := strings.ToLower(value) + dest, err := ParseHost(rawHost, net.Port(80)) + if err != nil { + return nil, err + } + sh.host = dest.Address.String() + } + } + // Parse request line + // Request line is like this + // "GET /homo/114514 HTTP/1.1" + if len(headers) > 0 && ShouldSniffAttr { + RequestLineParts := bytes.Split(headers[0], []byte{' '}) + if len(RequestLineParts) == 3 { + content.SetAttribute(":method", string(RequestLineParts[0])) + content.SetAttribute(":path", string(RequestLineParts[1])) + } + } + + if len(sh.host) > 0 { + return sh, nil + } + + return nil, common.ErrNoClue +} diff --git a/subproject/Xray-core-main/common/protocol/http/sniff_test.go b/subproject/Xray-core-main/common/protocol/http/sniff_test.go new file mode 100644 index 00000000..09ce7d6c --- /dev/null +++ b/subproject/Xray-core-main/common/protocol/http/sniff_test.go @@ -0,0 +1,106 @@ +package http_test + +import ( + "context" + "testing" + + . "github.com/xtls/xray-core/common/protocol/http" +) + +func TestHTTPHeaders(t *testing.T) { + cases := []struct { + input string + domain string + err bool + }{ + { + input: `GET /tutorials/other/top-20-mysql-best-practices/ HTTP/1.1 +Host: net.tutsplus.com +User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.5) Gecko/20091102 Firefox/3.5.5 (.NET CLR 3.5.30729) +Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 +Accept-Language: en-us,en;q=0.5 +Accept-Encoding: gzip,deflate +Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 +Keep-Alive: 300 +Connection: keep-alive +Cookie: PHPSESSID=r2t5uvjq435r4q7ib3vtdjq120 +Pragma: no-cache +Cache-Control: no-cache`, + domain: "net.tutsplus.com", + }, + { + input: `POST /foo.php HTTP/1.1 +Host: localhost +User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.5) Gecko/20091102 Firefox/3.5.5 (.NET CLR 3.5.30729) +Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 +Accept-Language: en-us,en;q=0.5 +Accept-Encoding: gzip,deflate +Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 +Keep-Alive: 300 +Connection: keep-alive +Referer: http://localhost/test.php +Content-Type: application/x-www-form-urlencoded +Content-Length: 43 + +first_name=John&last_name=Doe&action=Submit`, + domain: "localhost", + }, + { + input: `X /foo.php HTTP/1.1 +Host: localhost +User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.5) Gecko/20091102 Firefox/3.5.5 (.NET CLR 3.5.30729) +Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 +Accept-Language: en-us,en;q=0.5 +Accept-Encoding: gzip,deflate +Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 +Keep-Alive: 300 +Connection: keep-alive +Referer: http://localhost/test.php +Content-Type: application/x-www-form-urlencoded +Content-Length: 43 + +first_name=John&last_name=Doe&action=Submit`, + domain: "", + err: true, + }, + { + input: `GET /foo.php HTTP/1.1 +User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.5) Gecko/20091102 Firefox/3.5.5 (.NET CLR 3.5.30729) +Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 +Accept-Language: en-us,en;q=0.5 +Accept-Encoding: gzip,deflate +Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 +Keep-Alive: 300 +Connection: keep-alive +Referer: http://localhost/test.php +Content-Type: application/x-www-form-urlencoded +Content-Length: 43 + +Host: localhost +first_name=John&last_name=Doe&action=Submit`, + domain: "", + err: true, + }, + { + input: `GET /tutorials/other/top-20-mysql-best-practices/ HTTP/1.1`, + domain: "", + err: true, + }, + } + + for _, test := range cases { + header, err := SniffHTTP([]byte(test.input), context.TODO()) + if test.err { + if err == nil { + t.Errorf("Expect error but nil, in test: %v", test) + } + } else { + if err != nil { + t.Errorf("Expect no error but actually %s in test %v", err.Error(), test) + } + if header.Domain() != test.domain { + t.Error("expected domain ", test.domain, " but got ", header.Domain()) + } + } + } +} diff --git a/subproject/Xray-core-main/common/protocol/id.go b/subproject/Xray-core-main/common/protocol/id.go new file mode 100644 index 00000000..211fc578 --- /dev/null +++ b/subproject/Xray-core-main/common/protocol/id.go @@ -0,0 +1,49 @@ +package protocol + +import ( + "crypto/md5" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/uuid" +) + +const ( + IDBytesLen = 16 +) + +// The ID of en entity, in the form of a UUID. +type ID struct { + uuid uuid.UUID + cmdKey [IDBytesLen]byte +} + +// Equals returns true if this ID equals to the other one. +func (id *ID) Equals(another *ID) bool { + return id.uuid.Equals(&(another.uuid)) +} + +func (id *ID) Bytes() []byte { + return id.uuid.Bytes() +} + +func (id *ID) String() string { + return id.uuid.String() +} + +func (id *ID) UUID() uuid.UUID { + return id.uuid +} + +func (id ID) CmdKey() []byte { + return id.cmdKey[:] +} + +// NewID returns an ID with given UUID. +func NewID(uuid uuid.UUID) *ID { + id := &ID{uuid: uuid} + md5hash := md5.New() + common.Must2(md5hash.Write(uuid.Bytes())) + common.Must2(md5hash.Write([]byte("c48619fe-8f02-49e0-b9e9-edf763e17e21"))) + md5hash.Sum(id.cmdKey[:0]) + return id +} diff --git a/subproject/Xray-core-main/common/protocol/id_test.go b/subproject/Xray-core-main/common/protocol/id_test.go new file mode 100644 index 00000000..c2d6dad6 --- /dev/null +++ b/subproject/Xray-core-main/common/protocol/id_test.go @@ -0,0 +1,21 @@ +package protocol_test + +import ( + "testing" + + . "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/uuid" +) + +func TestIdEquals(t *testing.T) { + id1 := NewID(uuid.New()) + id2 := NewID(id1.UUID()) + + if !id1.Equals(id2) { + t.Error("expected id1 to equal id2, but actually not") + } + + if id1.String() != id2.String() { + t.Error(id1.String(), " != ", id2.String()) + } +} diff --git a/subproject/Xray-core-main/common/protocol/payload.go b/subproject/Xray-core-main/common/protocol/payload.go new file mode 100644 index 00000000..2f3c7290 --- /dev/null +++ b/subproject/Xray-core-main/common/protocol/payload.go @@ -0,0 +1,16 @@ +package protocol + +type TransferType byte + +const ( + TransferTypeStream TransferType = 0 + TransferTypePacket TransferType = 1 +) + +type AddressType byte + +const ( + AddressTypeIPv4 AddressType = 1 + AddressTypeDomain AddressType = 2 + AddressTypeIPv6 AddressType = 3 +) diff --git a/subproject/Xray-core-main/common/protocol/protocol.go b/subproject/Xray-core-main/common/protocol/protocol.go new file mode 100644 index 00000000..61c963c5 --- /dev/null +++ b/subproject/Xray-core-main/common/protocol/protocol.go @@ -0,0 +1,7 @@ +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") diff --git a/subproject/Xray-core-main/common/protocol/quic/qtls_go118.go b/subproject/Xray-core-main/common/protocol/quic/qtls_go118.go new file mode 100644 index 00000000..bfa5e245 --- /dev/null +++ b/subproject/Xray-core-main/common/protocol/quic/qtls_go118.go @@ -0,0 +1,18 @@ +package quic + +import ( + "crypto" + "crypto/cipher" + _ "crypto/tls" + _ "unsafe" +) + +type CipherSuiteTLS13 struct { + ID uint16 + KeyLen int + AEAD func(key, fixedNonce []byte) cipher.AEAD + Hash crypto.Hash +} + +//go:linkname AEADAESGCMTLS13 crypto/tls.aeadAESGCMTLS13 +func AEADAESGCMTLS13(key, nonceMask []byte) cipher.AEAD diff --git a/subproject/Xray-core-main/common/protocol/quic/sniff.go b/subproject/Xray-core-main/common/protocol/quic/sniff.go new file mode 100644 index 00000000..5b29d6ff --- /dev/null +++ b/subproject/Xray-core-main/common/protocol/quic/sniff.go @@ -0,0 +1,285 @@ +package quic + +import ( + "crypto" + "crypto/aes" + "crypto/tls" + "encoding/binary" + "io" + + "github.com/apernet/quic-go/quicvarint" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/protocol" + ptls "github.com/xtls/xray-core/common/protocol/tls" + "golang.org/x/crypto/hkdf" +) + +type SniffHeader struct { + domain string +} + +func (s SniffHeader) Protocol() string { + return "quic" +} + +func (s SniffHeader) Domain() string { + return s.domain +} + +const ( + versionDraft29 uint32 = 0xff00001d + version1 uint32 = 0x1 +) + +var ( + quicSaltOld = []byte{0xaf, 0xbf, 0xec, 0x28, 0x99, 0x93, 0xd2, 0x4c, 0x9e, 0x97, 0x86, 0xf1, 0x9c, 0x61, 0x11, 0xe0, 0x43, 0x90, 0xa8, 0x99} + quicSalt = []byte{0x38, 0x76, 0x2c, 0xf7, 0xf5, 0x59, 0x34, 0xb3, 0x4d, 0x17, 0x9a, 0xe6, 0xa4, 0xc8, 0x0c, 0xad, 0xcc, 0xbb, 0x7f, 0x0a} + initialSuite = &CipherSuiteTLS13{ + ID: tls.TLS_AES_128_GCM_SHA256, + KeyLen: 16, + AEAD: AEADAESGCMTLS13, + Hash: crypto.SHA256, + } + errNotQuic = errors.New("not quic") + errNotQuicInitial = errors.New("not initial packet") +) + +func SniffQUIC(b []byte) (*SniffHeader, error) { + if len(b) == 0 { + return nil, common.ErrNoClue + } + + // Crypto data separated across packets + cryptoLen := int32(0) + cryptoDataBuf := buf.NewWithSize(32767) + defer cryptoDataBuf.Release() + cache := buf.New() + defer cache.Release() + + // Parse QUIC packets + for len(b) > 0 { + buffer := buf.FromBytes(b) + typeByte, err := buffer.ReadByte() + if err != nil { + return nil, errNotQuic + } + + isLongHeader := typeByte&0x80 > 0 + if !isLongHeader || typeByte&0x40 == 0 { + return nil, errNotQuicInitial + } + + vb, err := buffer.ReadBytes(4) + if err != nil { + return nil, errNotQuic + } + + versionNumber := binary.BigEndian.Uint32(vb) + if versionNumber != 0 && typeByte&0x40 == 0 { + return nil, errNotQuic + } else if versionNumber != versionDraft29 && versionNumber != version1 { + return nil, errNotQuic + } + + packetType := (typeByte & 0x30) >> 4 + isQuicInitial := packetType == 0x0 + + var destConnID []byte + if l, err := buffer.ReadByte(); err != nil { + return nil, errNotQuic + } else if destConnID, err = buffer.ReadBytes(int32(l)); err != nil { + return nil, errNotQuic + } + + if l, err := buffer.ReadByte(); err != nil { + return nil, errNotQuic + } else if common.Error2(buffer.ReadBytes(int32(l))) != nil { + 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) + if err != nil || tokenLen > uint64(len(b)) { + return nil, errNotQuic + } + + if _, err = buffer.ReadBytes(int32(tokenLen)); err != nil { + return nil, errNotQuic + } + } + + packetLen, err := quicvarint.Read(buffer) + if err != nil { + return nil, errNotQuic + } + // packetLen is impossible to be shorter than this + if packetLen < 4 { + return nil, errNotQuic + } + + hdrLen := len(b) - int(buffer.Len()) + if len(b) < hdrLen+int(packetLen) { + return nil, common.ErrNoClue // Not enough data to read as a QUIC packet. QUIC is UDP-based, so this is unlikely to happen. + } + + restPayload := b[hdrLen+int(packetLen):] + if !isQuicInitial { // Skip this packet if it's not initial packet + b = restPayload + continue + } + + var salt []byte + if versionNumber == version1 { + salt = quicSalt + } else { + salt = quicSaltOld + } + initialSecret := hkdf.Extract(crypto.SHA256.New, destConnID, salt) + secret := hkdfExpandLabel(crypto.SHA256, initialSecret, []byte{}, "client in", crypto.SHA256.Size()) + hpKey := hkdfExpandLabel(initialSuite.Hash, secret, []byte{}, "quic hp", initialSuite.KeyLen) + block, err := aes.NewCipher(hpKey) + if err != nil { + return nil, err + } + + cache.Clear() + mask := cache.Extend(int32(block.BlockSize())) + block.Encrypt(mask, b[hdrLen+4:hdrLen+4+len(mask)]) + b[0] ^= mask[0] & 0xf + packetNumberLength := int(b[0]&0x3 + 1) + for i := range packetNumberLength { + b[hdrLen+i] ^= mask[i+1] + } + + key := hkdfExpandLabel(crypto.SHA256, secret, []byte{}, "quic key", 16) + iv := hkdfExpandLabel(crypto.SHA256, secret, []byte{}, "quic iv", 12) + cipher := AEADAESGCMTLS13(key, iv) + + nonce := cache.Extend(int32(cipher.NonceSize())) + _, err = buffer.Read(nonce[len(nonce)-packetNumberLength:]) + 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]) + if err != nil { + return nil, err + } + buffer = buf.FromBytes(decrypted) + for !buffer.IsEmpty() { + frameType, _ := buffer.ReadByte() + for frameType == 0x0 && !buffer.IsEmpty() { + frameType, _ = buffer.ReadByte() + } + switch frameType { + case 0x00: // PADDING frame + case 0x01: // PING frame + case 0x02, 0x03: // ACK frame + if _, err = quicvarint.Read(buffer); err != nil { // Field: Largest Acknowledged + return nil, io.ErrUnexpectedEOF + } + if _, err = quicvarint.Read(buffer); err != nil { // Field: ACK Delay + return nil, io.ErrUnexpectedEOF + } + ackRangeCount, err := quicvarint.Read(buffer) // Field: ACK Range Count + if err != nil { + return nil, io.ErrUnexpectedEOF + } + if _, err = quicvarint.Read(buffer); err != nil { // Field: First ACK Range + return nil, io.ErrUnexpectedEOF + } + for i := 0; i < int(ackRangeCount); i++ { // Field: ACK Range + if _, err = quicvarint.Read(buffer); err != nil { // Field: ACK Range -> Gap + return nil, io.ErrUnexpectedEOF + } + if _, err = quicvarint.Read(buffer); err != nil { // Field: ACK Range -> ACK Range Length + return nil, io.ErrUnexpectedEOF + } + } + if frameType == 0x03 { + if _, err = quicvarint.Read(buffer); err != nil { // Field: ECN Counts -> ECT0 Count + return nil, io.ErrUnexpectedEOF + } + if _, err = quicvarint.Read(buffer); err != nil { // Field: ECN Counts -> ECT1 Count + return nil, io.ErrUnexpectedEOF + } + if _, err = quicvarint.Read(buffer); err != nil { //nolint:misspell // Field: ECN Counts -> ECT-CE Count + return nil, io.ErrUnexpectedEOF + } + } + case 0x06: // CRYPTO frame, we will use this frame + offset, err := quicvarint.Read(buffer) // Field: Offset + if err != nil { + return nil, io.ErrUnexpectedEOF + } + length, err := quicvarint.Read(buffer) // Field: Length + if err != nil || length > uint64(buffer.Len()) { + return nil, io.ErrUnexpectedEOF + } + currentCryptoLen := int32(offset + length) + if cryptoLen < currentCryptoLen { + if cryptoDataBuf.Cap() < currentCryptoLen { + return nil, io.ErrShortBuffer + } + cryptoDataBuf.Extend(currentCryptoLen - cryptoLen) + cryptoLen = currentCryptoLen + } + if _, err := buffer.Read(cryptoDataBuf.BytesRange(int32(offset), currentCryptoLen)); err != nil { // Field: Crypto Data + return nil, io.ErrUnexpectedEOF + } + case 0x1c: // CONNECTION_CLOSE frame, only 0x1c is permitted in initial packet + if _, err = quicvarint.Read(buffer); err != nil { // Field: Error Code + return nil, io.ErrUnexpectedEOF + } + if _, err = quicvarint.Read(buffer); err != nil { // Field: Frame Type + return nil, io.ErrUnexpectedEOF + } + length, err := quicvarint.Read(buffer) // Field: Reason Phrase Length + if err != nil { + return nil, io.ErrUnexpectedEOF + } + if _, err := buffer.ReadBytes(int32(length)); err != nil { // Field: Reason Phrase + return nil, io.ErrUnexpectedEOF + } + default: + // Only above frame types are permitted in initial packet. + // See https://www.rfc-editor.org/rfc/rfc9000.html#section-17.2.2-8 + return nil, errNotQuicInitial + } + } + + tlsHdr := &ptls.SniffHeader{} + err = ptls.ReadClientHello(cryptoDataBuf.BytesRange(0, cryptoLen), tlsHdr) + if err != nil { + // The crypto data may have not been fully recovered in current packets, + // So we continue to sniff rest packets. + b = restPayload + continue + } + 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, protocol.ErrProtoNeedMoreData +} + +func hkdfExpandLabel(hash crypto.Hash, secret, context []byte, label string, length int) []byte { + b := make([]byte, 3, 3+6+len(label)+1+len(context)) + binary.BigEndian.PutUint16(b, uint16(length)) + b[2] = uint8(6 + len(label)) + b = append(b, []byte("tls13 ")...) + b = append(b, []byte(label)...) + b = b[:3+6+len(label)+1] + b[3+6+len(label)] = uint8(len(context)) + b = append(b, context...) + + out := make([]byte, length) + n, err := hkdf.Expand(hash.New, secret, b).Read(out) + if err != nil || n != length { + panic("quic: HKDF-Expand-Label invocation failed unexpectedly") + } + return out +} diff --git a/subproject/Xray-core-main/common/protocol/quic/sniff_test.go b/subproject/Xray-core-main/common/protocol/quic/sniff_test.go new file mode 100644 index 00000000..8a85ea87 --- /dev/null +++ b/subproject/Xray-core-main/common/protocol/quic/sniff_test.go @@ -0,0 +1,287 @@ +package quic_test + +import ( + "encoding/hex" + "errors" + "testing" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/protocol/quic" +) + +func TestSniffQUIC(t *testing.T) { + pkt, err := hex.DecodeString("cd0000000108f1fb7bcc78aa5e7203a8f86400421531fe825b19541876db6c55c38890cd73149d267a084afee6087304095417a3033df6a81bbb71d8512e7a3e16df1e277cae5df3182cb214b8fe982ba3fdffbaa9ffec474547d55945f0fddbeadfb0b5243890b2fa3da45169e2bd34ec04b2e29382f48d612b28432a559757504d158e9e505407a77dd34f4b60b8d3b555ee85aacd6648686802f4de25e7216b19e54c5f78e8a5963380c742d861306db4c16e4f7fc94957aa50b9578a0b61f1e406b2ad5f0cd3cd271c4d99476409797b0c3cb3efec256118912d4b7e4fd79d9cb9016b6e5eaa4f5e57b637b217755daf8968a4092bed0ed5413f5d04904b3a61e4064f9211b2629e5b52a89c7b19f37a713e41e27743ea6dfa736dfa1bb0a4b2bc8c8dc632c6ce963493a20c550e6fdb2475213665e9a85cfc394da9cec0cf41f0c8abed3fc83be5245b2b5aa5e825d29349f721d30774ef5bf965b540f3d8d98febe20956b1fc8fa047e10e7d2f921c9c6622389e02322e80621a1cf5264e245b7276966eb02932584e3f7038bd36aa908766ad3fb98344025dec18670d6db43a1c5daac00937fce7b7c7d61ff4e6efd01a2bdee0ee183108b926393df4f3d74bbcbb015f240e7e346b7d01c41111a401225ce3b095ab4623a5836169bf9599eeca79d1d2e9b2202b5960a09211e978058d6fc0484eff3e91ce4649a5e3ba15b906d334cf66e28d9ff575406e1ae1ac2febafd72870b6f5d58fc5fb949cb1f40feb7c1d9ce5e71b") + common.Must(err) + quicHdr, err := quic.SniffQUIC(pkt) + if err != nil || quicHdr.Domain() != "www.google.com" { + t.Error("failed") + } +} + +func TestSniffQUICComplex(t *testing.T) { + tests := []struct { + name string + hexData string + domain string + wantErr bool + needsMoreData bool + }{ + { + name: "EmptyPacket", + hexData: "0000000000000000000000000000000000000000000000000000000000000000", + domain: "", + wantErr: true, + needsMoreData: false, + }, + { + name: "NTP Packet Client", + hexData: "23000000000000000000000000000000000000000000000000000000000000000000000000000000acb84a797d4044c9", + domain: "", + wantErr: true, + needsMoreData: false, + }, + { + name: "NTP Packet Server", + hexData: "240106ec000000000000000e47505373ea4dcaef2f4b4c31acb84a797d4044c9eb58b8693dd70c27eb58b8693dd7dde2", + domain: "", + wantErr: true, + needsMoreData: false, + }, + { + name: "DNS Packet Client", + hexData: "4500004a8e2d40003f1146392a2a2d03080808081eea00350036a8175ad4010000010000000000000675706461746504636f64650c76697375616c73747564696f03636f6d0000010001", + domain: "", + wantErr: true, + needsMoreData: false, + }, + { + name: "DNS Packet Client", + hexData: "4500004a667a40003f116dec2a2a2d030808080866980035003605d9b524010000010000000000000675706461746504636f64650c76697375616c73747564696f03636f6d0000410001", + domain: "", + wantErr: true, + needsMoreData: false, + }, + { + name: "DNS Packet Server", + hexData: "b524818000010006000100000675706461746504636f64650c76697375616c73747564696f03636f6d0000410001c00c00050001000000ec00301e7673636f64652d7570646174652d67366763623667676474686b63746439037a303107617a7572656664036e657400c03a000500010000000b002311737461722d617a75726566642d70726f640e747261666669636d616e61676572c065c076000500010000003c002c0473686564086475616c2d6c6f770b732d706172742d3030313706742d3030303908742d6d7365646765c065c0a5000500010000006c001411617a75726566642d742d66622d70726f64c088c0dd000500010000003c0026046475616c0b732d706172742d3030313706742d303030390b66622d742d6d7365646765c065c0fd00050001000000300002c102c1150006000100000030002d036e7331c115066d736e687374096d6963726f736f6674c0257848b78d00000708000003840024ea000000003c", + domain: "", + wantErr: true, + needsMoreData: false, + }, + { + name: "DNS Packet Server", + hexData: "5ad4818000010007000000000675706461746504636f64650c76697375616c73747564696f03636f6d0000010001c00c000500010000008400301e7673636f64652d7570646174652d67366763623667676474686b63746439037a303107617a7572656664036e657400c03a000500010000001e002311737461722d617a75726566642d70726f640e747261666669636d616e61676572c065c076000500010000003c002c0473686564086475616c2d6c6f770b732d706172742d3030313706742d3030303908742d6d7365646765c065c0a50005000100000010001411617a75726566642d742d66622d70726f64c088c0dd000500010000003c0026046475616c0b732d706172742d3030313706742d303030390b66622d742d6d7365646765c065c0fd00050001000000100002c102c102000100010000001000040d6bfd2d", + domain: "", + wantErr: true, + needsMoreData: false, + }, + { + name: "QUIC, NonHandshake Packet", + hexData: "548439ba3a0cffd27dabe08ebf9e603dd4801781e133b1a0276d29a047c3b8856adcced0067c4b11a08985bf93c05863305bd4b43ee9168cd5fdae0c392ff74ae06ce13e8d97dabec81ee927a844fa840f781edf9deb22f3162bf77009b3f5800c5e45539ac104368e7df8ba", + domain: "", + wantErr: true, + needsMoreData: false, + }, + { + name: "QUIC, NonHandshake Packet", + hexData: "53f4144825dab3ba251b83d0089e910210bec1a6507cca92ad9ff539cc21f6c75e3551ca44003d9a", + domain: "", + wantErr: true, + needsMoreData: false, + }, + { + name: "QUIC, NonHandshake Packet", + hexData: "528dc5524c03e7517949422cc3f6ffbfff74b2ec30a87654a71a", + domain: "", + wantErr: true, + needsMoreData: false, + }, + { + name: "QUIC Chromebook Handshake[1]; packet 1", + hexData: "cb00000001088ca3be26059ca269000044d088950f316207d551c91c88d791557c440a19184322536d2c900034358c1b3964f2d2935337b8d044d35bf62b4eea9ceaac64121aa634c7cd28630722d169fa0f215b940d47d7996ca56f0d463dbf97a4a1b5818c5297a26fe58f5553dfb513ad589750a61682f229996555c7121c8bf48b06b68ab06427b01af485d832f9894099a20d3baadcff7b1cf07e2c059d3e7ba88d4ad35ef0ffea1fdc6ac3db271dfcca892a41ab25284936225c9bc593ce242b11b8abed4a8df902987eef0c6d90669e3606f47dd6ad05f44ba3a0cd356854261bbb1e2d8f6b83cc57cfa57eda3e5d7181b6ec418f6eeca81c259a33e4b0a913de720f2f8782764766ac9602a7f52a1082ec3da30dbefcf38c781a3e033810c4f2babf9b72adf7164159d98142181492e4468c0e10ab29013bf238e7360e09767ca49d59a9eb18f06a372bad711fefa90295f8e0839b1080570648212b321e5bd6f614bf0d3dc2817628b0c052a32820c16cb7f531c49244c48eb1429625246f9c164ae4ee1e83eaa8ff0eef1acf5a3d8ca88f1e4597db5ba5c0cb23d6100dd53da4f439ae64c4d3d43d1fbb5677f4fdc3bd2c2948dfc7e0be1a33c842033da15529cfd3cae00da68343d835db867f746854804410ba68f0dd7711b0fe55817b83f6ce1a12ad38acf2a3156f819f0dc68ea799c05583d9728f2856577811b260dba40d6c5e82c9e558c5b8f3f4599caf05ea591118e0b80ad621e0a76e4926047593a896752cb168420cb1b02d4211de5e5b7c891f319b5c0cf687e1d261a01f2acbade6bd73cd1ade0a02e240e9351384e1a6868c21a4878f39f0fa94ee1e36c5a46449241a3fe0147ff50176787eca7f3a936c901aeef56770bff74feecb985e6670d20dfd8ed17952dca5a5292213345c61db09bb5bcf5bf74565f61f9dccab51a289c3160ffe4a9b29cc76ea46778d9317a890efea2ad905f4219463a3baca3c02f5c3682634be7c2e86e366272a8263fec8e871644a79299d4aa74f1b1414b2f963cce6e059978faf813625af7869c1dec92035478c0e46dc66d938d4131aca27a59b2103b8cefa8e08aeb44b53b205b932902aea8d519faaaa12e354a6f532b4f716d7929e655dc2e98b494a99153854af5732a2659f2c21e4069896a1835ad05c5e53781cab16599cf4af47c196deeff9115c80d13f93aeb28b08023e6a1d3cf7da2a4457a9e443176bcdfef8f8de630c02bd0efdc5ddda56ad8f6b47edbda6353205e6e655f690092a48deb7f8a5254a7d778e07216cd97dfefcf740c1acd2977ef0fa17f798ea9752bae46e3aa3ec9b13f4c95c20a7839b8409000fa1f17e8dc46cc05c41bff696ee03c0371cae8638e8018ff4ebedd9f27d56443e534a72dd3d18a64790b676ddd060376759fa4a12ffc17f4be83492126ec1dc0fcd4aefef73a0b9c443ec3532b9a66b1a60daacf45e6557115edc0cc4d08758754a44beffedaa0d1265e50beed1a01752904ee3f7e706ed290b1a79071b142105b7c02e692ff318710e3ce9c3b9ec557cdecef173796417341ada414faa06b52adf645db454b56468ccf0da50a942ebc09487797cb45a085ec1e2e06fcd1f5b72eac291955a62e5aa379a374aea3a0dec3e4e0ba1dde350a94c72dbea7505922e26e99d62f751c2b301413a73fb6b20a36052151473ebecd04d0a771ec326957bc28c2020fdf6f01d9abed69b3c3e73168b404a1748b15310b167396da01c7d", + domain: "", + wantErr: true, + needsMoreData: true, + }, + { + name: "QUIC Chromebook Handshake[1]; packet 1 - 2", + hexData: "cb00000001088ca3be26059ca269000044d088950f316207d551c91c88d791557c440a19184322536d2c900034358c1b3964f2d2935337b8d044d35bf62b4eea9ceaac64121aa634c7cd28630722d169fa0f215b940d47d7996ca56f0d463dbf97a4a1b5818c5297a26fe58f5553dfb513ad589750a61682f229996555c7121c8bf48b06b68ab06427b01af485d832f9894099a20d3baadcff7b1cf07e2c059d3e7ba88d4ad35ef0ffea1fdc6ac3db271dfcca892a41ab25284936225c9bc593ce242b11b8abed4a8df902987eef0c6d90669e3606f47dd6ad05f44ba3a0cd356854261bbb1e2d8f6b83cc57cfa57eda3e5d7181b6ec418f6eeca81c259a33e4b0a913de720f2f8782764766ac9602a7f52a1082ec3da30dbefcf38c781a3e033810c4f2babf9b72adf7164159d98142181492e4468c0e10ab29013bf238e7360e09767ca49d59a9eb18f06a372bad711fefa90295f8e0839b1080570648212b321e5bd6f614bf0d3dc2817628b0c052a32820c16cb7f531c49244c48eb1429625246f9c164ae4ee1e83eaa8ff0eef1acf5a3d8ca88f1e4597db5ba5c0cb23d6100dd53da4f439ae64c4d3d43d1fbb5677f4fdc3bd2c2948dfc7e0be1a33c842033da15529cfd3cae00da68343d835db867f746854804410ba68f0dd7711b0fe55817b83f6ce1a12ad38acf2a3156f819f0dc68ea799c05583d9728f2856577811b260dba40d6c5e82c9e558c5b8f3f4599caf05ea591118e0b80ad621e0a76e4926047593a896752cb168420cb1b02d4211de5e5b7c891f319b5c0cf687e1d261a01f2acbade6bd73cd1ade0a02e240e9351384e1a6868c21a4878f39f0fa94ee1e36c5a46449241a3fe0147ff50176787eca7f3a936c901aeef56770bff74feecb985e6670d20dfd8ed17952dca5a5292213345c61db09bb5bcf5bf74565f61f9dccab51a289c3160ffe4a9b29cc76ea46778d9317a890efea2ad905f4219463a3baca3c02f5c3682634be7c2e86e366272a8263fec8e871644a79299d4aa74f1b1414b2f963cce6e059978faf813625af7869c1dec92035478c0e46dc66d938d4131aca27a59b2103b8cefa8e08aeb44b53b205b932902aea8d519faaaa12e354a6f532b4f716d7929e655dc2e98b494a99153854af5732a2659f2c21e4069896a1835ad05c5e53781cab16599cf4af47c196deeff9115c80d13f93aeb28b08023e6a1d3cf7da2a4457a9e443176bcdfef8f8de630c02bd0efdc5ddda56ad8f6b47edbda6353205e6e655f690092a48deb7f8a5254a7d778e07216cd97dfefcf740c1acd2977ef0fa17f798ea9752bae46e3aa3ec9b13f4c95c20a7839b8409000fa1f17e8dc46cc05c41bff696ee03c0371cae8638e8018ff4ebedd9f27d56443e534a72dd3d18a64790b676ddd060376759fa4a12ffc17f4be83492126ec1dc0fcd4aefef73a0b9c443ec3532b9a66b1a60daacf45e6557115edc0cc4d08758754a44beffedaa0d1265e50beed1a01752904ee3f7e706ed290b1a79071b142105b7c02e692ff318710e3ce9c3b9ec557cdecef173796417341ada414faa06b52adf645db454b56468ccf0da50a942ebc09487797cb45a085ec1e2e06fcd1f5b72eac291955a62e5aa379a374aea3a0dec3e4e0ba1dde350a94c72dbea7505922e26e99d62f751c2b301413a73fb6b20a36052151473ebecd04d0a771ec326957bc28c2020fdf6f01d9abed69b3c3e73168b404a1748b15310b167396da01c7dc700000001088ca3be26059ca269000044d00a7e7a252620d0fdfb63c0c193d6a9fe6a36aa9ce1b29dfa5f11f2567850b88384a2cc682eca2e292749365b833e5f7540019cd4f3143ed078aec07990b0d6ece18310403e73e1fe2975a8f9cb05796fa6196faaba3ee12a22b63a28a624cf4f7bedd44de000dc5ea698c65664df995b7d5fade0aab1cf0ecc5afd5ecb8fb80deecae3a8c97c20171f00ac3b5dc9a9027ca9c25571c72bb32070f6e3fb583560b0da6041b72e0a9601b8ad17d3c45e9dcc059f9f4758e8c35a839a9f6f4c501cb64e32e886fc733bc51069fbe4406f04d908285974c387d5b3e5f0f674941d05993bf8bda0d5ffd8c4fb528e150ff4bf37e38bd9c6346816fe360d4a206da81e815c1f7905184b6146b33427c6e38f1179981c18b82a3544442dd997c182d956037ae8f106eaf67ba133e7f15f1550b257d431f01ba0472659c6a5c2e6ff5e4ce9e692f4ef9fb169a75df4eb13f0b20e1994f3f8687bdca300c7e749af7b7a3b6597a6b950fe378a68c77766fdabe95248ed41d37805756b7ffa9cee0898bd661f6657cbf1af9aa8c7e437d432ca854c95307e6a7dfb6504ee3f7852fb3c246d168a03810b6c3d4e3d40bdee3def579effb66563f5bac98cfa1b071cd6f33e425e016bb3514a183b72cb3a393e9e519ba60e2177c98f530835e3b6eab78cdcb8abdbc769bc07e10c8e38bea710d5de1bdb2fa8d0d9b19e8cc31d16725a696e55342c89b667497e3d7f90e48f8503d8ead2a32a1930c3b24a4a9dcf2d8ec781705dd97d7df6e26828712fe42114419d5b8346bd86c239bd02f34e55f71400cb10c1fac7d8efa1a2ab258c17ace4288c8576ab92447b648fd15f4e038ec1c81a135e3bbb6f581a994c6a4902aeb1b5588cb1b5b53c8540296d96b6d2eccd67bae9609233f36304b5186d4698b88bb3ce8b1191a62b990436cf10718fd5759cb2281ac122f49ccbef8a3206348c1a930e7fc4bb498a11d89374e1480c7b8725b5f65e8c8d6f58da17f9134abce77eb9a6fcda514e7d3ab2e3610f86945f0dca519a3844da1b3a4b0e03c80528a2f79be478d07ff26166e30294bf0e69bf07a5bbd6d879adf6d618a1ec8365023408980bf67f0525a2fdee97fccc38fe104d4f58ed15e3671dfedf684856a27fbe286adba40ff0336def93f0174e9e35d341f5de73190d330d72227db9a866b69418e17e8e19ec884c1ffe2f0ad6deec37c9d49d536d0242fab282b0cf86cc9b15341757e0d361bddcbe5cbb062b3148d7c3c62af5c5dd5922a49920f351647030f62ed16929a404aa514fcbc38e67ba4f275e02a04c486b1a8e5b5efda197fd63e6f41fdeffa652c690dd6b00ca65df3688672ead9744f7d631e42e3b42f3ed1bff51b30f89211a7467cde65eab3659af7690cf307420a5823f31999d8f63c6c6ba0296ed4a46d5df6404f8db33e7252cc6bfcf7f55fee1f1e3b0573b6c6615793ff0691b7cfd23c195f66eb333d7efb0cfb74cf159787f87ad01fc131c6763bb1117bbfb8c2e8197ffba6b8c747565b1332bdbd6553b840939c2f98aa8eb1c549491c640e012fc549852fa7a93f81e5db152c761fc7d01bce0325619965c09f6730a162e7be53af7d9ce4b5ac0f4eb487361d2ac231d4ce92e5d9a084bc7b609ccf60056ecc82cd0c06a088cfbcf7d764b3109331c42f989da82b05cfe4c134a6784e664fa67a89c0624e3cc73ccfdea3f292db28f7c7b1b109f680f6b537f135c62f764", + domain: "dns.google", + wantErr: false, + needsMoreData: false, + }, + { + name: "QUIC Chromebook Handshake[2]; packet 1", + hexData: "cf0000000108452af27900a1723300404600ca8530029a6bc59ff3e85beb5fc838ac3147ba5c2f6421ddcffdd85167d8de70eff2b1fa016dad4918337dec0feb660edb98e078dea51fa914055984b7957bd732fa4c831e448967af34752fd95835e3caba1e022d6d164f3f53f1bd7f60d560a8684079e90626aa1a4d3fe728158f7e1055ff76d1566072113982b193fb932265381e4de7afb35caa4ec56f31595a33fa2eb0bc84feb9f273224050938825fd21aa7317042ad00785ffd36151aee566a5dfe17d72591af1235059171568e5af0d13fc56e7897c3d632be753d8dea184c3d96d92bc56978cc669d94dd4c5e8dc3dcba7f0a39368fb1e87981e54bba7b86fbd8e8023a94d84f0290f402a5244cb4b0eeaaa57610ea59711a43932c521f10edb4560375693cbea60240389b8cebfd94035cabe4fc96ce8a726b979775e06c3bb0e3c4c866fe82e89fb725499e711e39310b93c785b313459f22d4ba37f90b19447165c2584269d98bf47d1f7ca89585797e4d6f1a4a1db7d2b0ae91a93fb15c3bb0ab953c3656b3b2ca20833d15e95329baff6d2ade1b0921b5ed3ae96648bf123b5265e27b049e9a8674455ff5f763f039568026e4fbe9882fef761c573d8f12e342c274a8dd3ad9854a688ce57cdddb52c758161ae3a59f67fc0d5b85f12e27617e7f4366e97a61fcda084e620dde35686f01dac49ce4bd76b986e3223c215919a1b228beeb74b7fcf32827d55be8f1b3b5fed24df2db023faecbb313b18a151cc4af8199d4bb08f8127b8207a0286d52758eaca87fd476ece0e3b17bcd8afb0289e8fd33c4455d4db6f058826c301ea303bfe2c0a6651a8fb6a2e1897852d758076adb04ad907077c5d5f94089da78d8923a34f1022ed672f378fe0dd81a709b372c0a2042a42e683c051c653e42b43c4a0ea8e961074d2901d4157ac9878b13a207b05ec471cff10d922b74d05623513cd6a4ea192ad21d4089de269633d4d2d1388d98d7c8a9e29848d5558b8aa2b73b437446a640230e6adb7f4b317ee5d66681c4aae11f69b1e5f96cb32ca6331405426cb706167d86f6f8fd588a72d7b2a6906798b81f174d808e1e3fc461e598e797c41bced26b87d09282d7b6d95076c285462e0c420a6f0e171ffe2791b5d221c03520409fe36622ff77796d9b7ef82babb25313acda9c621b22bf45ed909f9365b508860645af4c3aca78e6abca2d3a65c9159fbcd577438505d3f65a57c9412c12c069ad4d6db450beb08603abef621a9e029593fb5881dbd524ea2953b4acaaf59269b584c754e88c033247bb7c032e548d34fd9b2678e62fdf953dabf2be21c3e2d7b18ec7e3aedaf2cd082e19a369c1bcd4ca67e3d464e2200ecc3df98b0aa7f349415d68bcab0441ac3366607eff024bb786aec031a4619f8a24f554fe93c8520a03affcf11e40b6d5002f98c1708cac6c56e77eccba85ea6600d1391cfd202cc7914bfbaa3303266d1a820bf2dc84d2dfcdc4cdb79e6de3fbe3c02b288dcf955652f674f3f59b50849ea7dbf755bdafa27fba3db1267fb1354d8bf25a60cacb900b4d7ba913f9ba5f6b00559ad58b2f34a658ff7ef7f7d1ceeffd9c8325f271e6b5ba44d89685b744306963aa5e05ac0e8b00ada772dd5ae5ffb7043109afea86593743564c7acb4c8e7ef0e57d081eb1b9c0916078b113ece8a6036264a9b9781183c035342d50c7b069f3a01a40230e37ed8efde073c07d0e68066541d78c2f3cbe1e603cfcaaa", + domain: "", + wantErr: true, + needsMoreData: true, + }, + { + name: "QUIC Chromebook Handshake[2]; packet 1-2", + hexData: "cf0000000108452af27900a1723300404600ca8530029a6bc59ff3e85beb5fc838ac3147ba5c2f6421ddcffdd85167d8de70eff2b1fa016dad4918337dec0feb660edb98e078dea51fa914055984b7957bd732fa4c831e448967af34752fd95835e3caba1e022d6d164f3f53f1bd7f60d560a8684079e90626aa1a4d3fe728158f7e1055ff76d1566072113982b193fb932265381e4de7afb35caa4ec56f31595a33fa2eb0bc84feb9f273224050938825fd21aa7317042ad00785ffd36151aee566a5dfe17d72591af1235059171568e5af0d13fc56e7897c3d632be753d8dea184c3d96d92bc56978cc669d94dd4c5e8dc3dcba7f0a39368fb1e87981e54bba7b86fbd8e8023a94d84f0290f402a5244cb4b0eeaaa57610ea59711a43932c521f10edb4560375693cbea60240389b8cebfd94035cabe4fc96ce8a726b979775e06c3bb0e3c4c866fe82e89fb725499e711e39310b93c785b313459f22d4ba37f90b19447165c2584269d98bf47d1f7ca89585797e4d6f1a4a1db7d2b0ae91a93fb15c3bb0ab953c3656b3b2ca20833d15e95329baff6d2ade1b0921b5ed3ae96648bf123b5265e27b049e9a8674455ff5f763f039568026e4fbe9882fef761c573d8f12e342c274a8dd3ad9854a688ce57cdddb52c758161ae3a59f67fc0d5b85f12e27617e7f4366e97a61fcda084e620dde35686f01dac49ce4bd76b986e3223c215919a1b228beeb74b7fcf32827d55be8f1b3b5fed24df2db023faecbb313b18a151cc4af8199d4bb08f8127b8207a0286d52758eaca87fd476ece0e3b17bcd8afb0289e8fd33c4455d4db6f058826c301ea303bfe2c0a6651a8fb6a2e1897852d758076adb04ad907077c5d5f94089da78d8923a34f1022ed672f378fe0dd81a709b372c0a2042a42e683c051c653e42b43c4a0ea8e961074d2901d4157ac9878b13a207b05ec471cff10d922b74d05623513cd6a4ea192ad21d4089de269633d4d2d1388d98d7c8a9e29848d5558b8aa2b73b437446a640230e6adb7f4b317ee5d66681c4aae11f69b1e5f96cb32ca6331405426cb706167d86f6f8fd588a72d7b2a6906798b81f174d808e1e3fc461e598e797c41bced26b87d09282d7b6d95076c285462e0c420a6f0e171ffe2791b5d221c03520409fe36622ff77796d9b7ef82babb25313acda9c621b22bf45ed909f9365b508860645af4c3aca78e6abca2d3a65c9159fbcd577438505d3f65a57c9412c12c069ad4d6db450beb08603abef621a9e029593fb5881dbd524ea2953b4acaaf59269b584c754e88c033247bb7c032e548d34fd9b2678e62fdf953dabf2be21c3e2d7b18ec7e3aedaf2cd082e19a369c1bcd4ca67e3d464e2200ecc3df98b0aa7f349415d68bcab0441ac3366607eff024bb786aec031a4619f8a24f554fe93c8520a03affcf11e40b6d5002f98c1708cac6c56e77eccba85ea6600d1391cfd202cc7914bfbaa3303266d1a820bf2dc84d2dfcdc4cdb79e6de3fbe3c02b288dcf955652f674f3f59b50849ea7dbf755bdafa27fba3db1267fb1354d8bf25a60cacb900b4d7ba913f9ba5f6b00559ad58b2f34a658ff7ef7f7d1ceeffd9c8325f271e6b5ba44d89685b744306963aa5e05ac0e8b00ada772dd5ae5ffb7043109afea86593743564c7acb4c8e7ef0e57d081eb1b9c0916078b113ece8a6036264a9b9781183c035342d50c7b069f3a01a40230e37ed8efde073c07d0e68066541d78c2f3cbe1e603cfcaaac40000000108452af27900a1723300404600ca8530029a6bc59ff3e85beb5fc838ac3147ba5c2f6421ddcffdd85167d8de70eff2b1fa016dad4918337dec0feb660edb98e078dea51fa914055984b7957bd732fa4c831e4489522d29bb5c84749f83c8e1edfd9da8d1738164a8a9c59e37a5c9994d90bb982dcfa69b20f868960dc139618f1adc2546d34340ae13d826260c54a456bbf7469ee37b1be1d7177004468d7e92cac62a0b165d6a114ad479861dd58959e094b5a6250359301d4a614d529660760e3d1cdec9bf444a3761309bab40e4a977bc749e0dae431952f5f7e6b1ebc1383d343359a387da4301f7fa4b400475e9b82367e56278376dd1c80349f083988945a13649008109cc12a3acf569ffcc5481fbcd86b544e7dc8434e9dd42bd8e5716a844d37879568db046857389d36cc7550c75f94e314db6749aa987f0fc730fae0fcf465d01c2fb745269dfc10132ddb5404dda2f9455780f5818730834aa9db4740793359884b9927b0bd1a5ca96052b4f17397d8b78aa891401bb8bed6726ea2229d919798c50e24d5f40576ac204847be9244aadbe5c773684c37475036541d209c177d4e9c22a1253292ce4ffb886b925b6cf83cc251976a68887eda2777590f51804b790b51eee77e717b7ef0eea71634594df36e6ae9e7574d65c51ac3196f0b2a3b0f023c81f05f7807f958dda03418ed49e14b645e814b9aa55b37c809be3172ca21fe4c7a78e17e9ece8def2dd2949310ecaa41b1b477f4e85db5288aa144e333f47ef291d0e822941181c13859d9fd6d640904ee764c9276125228c932dff3fb12f564f039b52f5ba1ab4d119641df8fe13f784802b99347f0046da63f471e34b1d12d3111cffe7b5d90cf5999879f6f23e7785f09cb10df32821bb68dd8fdcfcdbedd63f2428b2292b9f0e76ff36403c9644fd43e01112ee6218d0ec1c86f6d147e4b802293e906750c7046f53bf05a144e321d3b45e08e4064fd3828fdd1b5d1ceed74081f61319dc0ad9a6e8a3b9cc802e952d24e2271712e2c2cda7daca2f835e6c804feeef8d918404cc82a1aa9534bddff68a472b208a0d0a7fd68a08fbc411132af47a6b67a32617b7b9991524c21599e8e3cb9395cdab87a3f5bf5d1833a9c7ea021b29cf428c877c6b21d62f99340ac7f85ae721acc10968e7d79f111ca40c75e14060d07cffa046d71151a0b00eab657300344b04bd1a8871650c34ceda8610d7c1ba8d37673da6aaa580400e0230c69fba8ba21927de2f5897656144694550d1df3d268804adc707e7b236501734aeabb2e61cb08012bd96eca5a486d7a55f996992c36233815abd71c30e263ba0c5d9456fe0828df16f6af7929390bb143c426d9dfeaf4bb373554479ebe609b36b4bc3dd08ce216b9cdc5726edb458c5e4036d0edc688d3e39d20f8254b5d1f174518f15b344efc27fc56572c0159aa593d5b46bc33818f986e3df8caebd4c7b702ef50dd582714a2b94ecd1c4e90af37d388445c478a32ff6e8f5852ee115966b708eed04da322b98813a69423e95f90b89ce85518e39bcef36fdd5bd312b2c6c5ee85962675274c18f39ee35155517f70fd74b31bb2de6b5108d369252e6fb289e453833132ef7960da1cc0934790c039b9a1b0c74f23eb3b61fe9b4d0ea67de757b93af451eef303b1373199af446a0fa98d5991bbd4771ee63317e6da86efbe213dfff595c41b98e0e89f4f2df110104e760feebf4cb3361171c9fceb1e1c809a268", + domain: "", + wantErr: true, + needsMoreData: true, + }, + { + name: "QUIC Chromebook Handshake[2]; packet 1-3", + hexData: "cf0000000108452af27900a1723300404600ca8530029a6bc59ff3e85beb5fc838ac3147ba5c2f6421ddcffdd85167d8de70eff2b1fa016dad4918337dec0feb660edb98e078dea51fa914055984b7957bd732fa4c831e448967af34752fd95835e3caba1e022d6d164f3f53f1bd7f60d560a8684079e90626aa1a4d3fe728158f7e1055ff76d1566072113982b193fb932265381e4de7afb35caa4ec56f31595a33fa2eb0bc84feb9f273224050938825fd21aa7317042ad00785ffd36151aee566a5dfe17d72591af1235059171568e5af0d13fc56e7897c3d632be753d8dea184c3d96d92bc56978cc669d94dd4c5e8dc3dcba7f0a39368fb1e87981e54bba7b86fbd8e8023a94d84f0290f402a5244cb4b0eeaaa57610ea59711a43932c521f10edb4560375693cbea60240389b8cebfd94035cabe4fc96ce8a726b979775e06c3bb0e3c4c866fe82e89fb725499e711e39310b93c785b313459f22d4ba37f90b19447165c2584269d98bf47d1f7ca89585797e4d6f1a4a1db7d2b0ae91a93fb15c3bb0ab953c3656b3b2ca20833d15e95329baff6d2ade1b0921b5ed3ae96648bf123b5265e27b049e9a8674455ff5f763f039568026e4fbe9882fef761c573d8f12e342c274a8dd3ad9854a688ce57cdddb52c758161ae3a59f67fc0d5b85f12e27617e7f4366e97a61fcda084e620dde35686f01dac49ce4bd76b986e3223c215919a1b228beeb74b7fcf32827d55be8f1b3b5fed24df2db023faecbb313b18a151cc4af8199d4bb08f8127b8207a0286d52758eaca87fd476ece0e3b17bcd8afb0289e8fd33c4455d4db6f058826c301ea303bfe2c0a6651a8fb6a2e1897852d758076adb04ad907077c5d5f94089da78d8923a34f1022ed672f378fe0dd81a709b372c0a2042a42e683c051c653e42b43c4a0ea8e961074d2901d4157ac9878b13a207b05ec471cff10d922b74d05623513cd6a4ea192ad21d4089de269633d4d2d1388d98d7c8a9e29848d5558b8aa2b73b437446a640230e6adb7f4b317ee5d66681c4aae11f69b1e5f96cb32ca6331405426cb706167d86f6f8fd588a72d7b2a6906798b81f174d808e1e3fc461e598e797c41bced26b87d09282d7b6d95076c285462e0c420a6f0e171ffe2791b5d221c03520409fe36622ff77796d9b7ef82babb25313acda9c621b22bf45ed909f9365b508860645af4c3aca78e6abca2d3a65c9159fbcd577438505d3f65a57c9412c12c069ad4d6db450beb08603abef621a9e029593fb5881dbd524ea2953b4acaaf59269b584c754e88c033247bb7c032e548d34fd9b2678e62fdf953dabf2be21c3e2d7b18ec7e3aedaf2cd082e19a369c1bcd4ca67e3d464e2200ecc3df98b0aa7f349415d68bcab0441ac3366607eff024bb786aec031a4619f8a24f554fe93c8520a03affcf11e40b6d5002f98c1708cac6c56e77eccba85ea6600d1391cfd202cc7914bfbaa3303266d1a820bf2dc84d2dfcdc4cdb79e6de3fbe3c02b288dcf955652f674f3f59b50849ea7dbf755bdafa27fba3db1267fb1354d8bf25a60cacb900b4d7ba913f9ba5f6b00559ad58b2f34a658ff7ef7f7d1ceeffd9c8325f271e6b5ba44d89685b744306963aa5e05ac0e8b00ada772dd5ae5ffb7043109afea86593743564c7acb4c8e7ef0e57d081eb1b9c0916078b113ece8a6036264a9b9781183c035342d50c7b069f3a01a40230e37ed8efde073c07d0e68066541d78c2f3cbe1e603cfcaaac40000000108452af27900a1723300404600ca8530029a6bc59ff3e85beb5fc838ac3147ba5c2f6421ddcffdd85167d8de70eff2b1fa016dad4918337dec0feb660edb98e078dea51fa914055984b7957bd732fa4c831e4489522d29bb5c84749f83c8e1edfd9da8d1738164a8a9c59e37a5c9994d90bb982dcfa69b20f868960dc139618f1adc2546d34340ae13d826260c54a456bbf7469ee37b1be1d7177004468d7e92cac62a0b165d6a114ad479861dd58959e094b5a6250359301d4a614d529660760e3d1cdec9bf444a3761309bab40e4a977bc749e0dae431952f5f7e6b1ebc1383d343359a387da4301f7fa4b400475e9b82367e56278376dd1c80349f083988945a13649008109cc12a3acf569ffcc5481fbcd86b544e7dc8434e9dd42bd8e5716a844d37879568db046857389d36cc7550c75f94e314db6749aa987f0fc730fae0fcf465d01c2fb745269dfc10132ddb5404dda2f9455780f5818730834aa9db4740793359884b9927b0bd1a5ca96052b4f17397d8b78aa891401bb8bed6726ea2229d919798c50e24d5f40576ac204847be9244aadbe5c773684c37475036541d209c177d4e9c22a1253292ce4ffb886b925b6cf83cc251976a68887eda2777590f51804b790b51eee77e717b7ef0eea71634594df36e6ae9e7574d65c51ac3196f0b2a3b0f023c81f05f7807f958dda03418ed49e14b645e814b9aa55b37c809be3172ca21fe4c7a78e17e9ece8def2dd2949310ecaa41b1b477f4e85db5288aa144e333f47ef291d0e822941181c13859d9fd6d640904ee764c9276125228c932dff3fb12f564f039b52f5ba1ab4d119641df8fe13f784802b99347f0046da63f471e34b1d12d3111cffe7b5d90cf5999879f6f23e7785f09cb10df32821bb68dd8fdcfcdbedd63f2428b2292b9f0e76ff36403c9644fd43e01112ee6218d0ec1c86f6d147e4b802293e906750c7046f53bf05a144e321d3b45e08e4064fd3828fdd1b5d1ceed74081f61319dc0ad9a6e8a3b9cc802e952d24e2271712e2c2cda7daca2f835e6c804feeef8d918404cc82a1aa9534bddff68a472b208a0d0a7fd68a08fbc411132af47a6b67a32617b7b9991524c21599e8e3cb9395cdab87a3f5bf5d1833a9c7ea021b29cf428c877c6b21d62f99340ac7f85ae721acc10968e7d79f111ca40c75e14060d07cffa046d71151a0b00eab657300344b04bd1a8871650c34ceda8610d7c1ba8d37673da6aaa580400e0230c69fba8ba21927de2f5897656144694550d1df3d268804adc707e7b236501734aeabb2e61cb08012bd96eca5a486d7a55f996992c36233815abd71c30e263ba0c5d9456fe0828df16f6af7929390bb143c426d9dfeaf4bb373554479ebe609b36b4bc3dd08ce216b9cdc5726edb458c5e4036d0edc688d3e39d20f8254b5d1f174518f15b344efc27fc56572c0159aa593d5b46bc33818f986e3df8caebd4c7b702ef50dd582714a2b94ecd1c4e90af37d388445c478a32ff6e8f5852ee115966b708eed04da322b98813a69423e95f90b89ce85518e39bcef36fdd5bd312b2c6c5ee85962675274c18f39ee35155517f70fd74b31bb2de6b5108d369252e6fb289e453833132ef7960da1cc0934790c039b9a1b0c74f23eb3b61fe9b4d0ea67de757b93af451eef303b1373199af446a0fa98d5991bbd4771ee63317e6da86efbe213dfff595c41b98e0e89f4f2df110104e760feebf4cb3361171c9fceb1e1c809a268c60000000108452af27900a1723300404600ca8530029a6bc59ff3e85beb5fc838ac3147ba5c2f6421ddcffdd85167d8de70eff2b1fa016dad4918337dec0feb660edb98e078dea51fa914055984b7957bd732fa4c831e44892ff5e6b16d8a259a9128c2c0c3c525462781a344c3df7f19a747e0e79ca8714995c867fc697a3cb87b35e769465a8e966bcb35b7e897ad036aa23a6c021e2445a0eb79962151cd20dbb43ae1231847de01caf4e5589dfebf026e95f7d1d742e140d9dda849396a70cc0798f1eef06fd5f4cfbc9a190ddf04cc332c5b7b15e53af311190ced92a1291c12b8799f2b50e076539a8370ee667e1791a78f38e565a48acbaa1c78ba941dba8b0d040f8fb8bbcc9f6bf5705efa613a24b12d6ac9cebb4f3fac1b09a07b49d8a3a62808eb0a324629f13a012e6ad0feb11ad97c1572983c713b62f27584809ba43e64e4af9845af807c0783104838f4e2ac33fa848866f3cc64a7b6203a5c09e8ad231f0f06ae2fb7b39a64cedd823b0ff297ad9be1ccac436777ccb3e22ef6b9c12e6d5e34926f50e8ca8c8c0532c810b074d001c11791a01bf25786b57a5da54065dcee4962822e929f47ee44d3b8c83d45a8b7a936dc2a6fa396e4194fa032d1627eca59f69857fc40dab5835d3613dade1c74b09c345bd32c509e9545d2330b157a7acb76409f3ac8eaa22802414f38c5422fe4c5189caaf5c1b93ce7c0892f0cfc477490d335aa78961d632a973cf106bd974c2714176fb0f98cf12f2887a0d7bd491756dd374331eb3e6adb9f2bd0d6b273403fd14b314eb27ebbb6f6e78ce310437004b757c048149cf04429ae4a6d6e65c9b3e0b9c9c4d4ef52007eaaad9670320f10cd5317b3d3edc374d45c98b217dd28fb3c2c2fb6e74a3aced143e3242084b192ba6df24e69fdb883e850714fe27a45f43883486a986574fd1fc10f259fe90786441554514c8dade1f3b86fdaf5f54ab655e2d803c98aa56073b00c32148a1ed367dff3a2bd934ecba55141389990b661bbd9ce1ef1def13747d45500daf92cec9e60908274703e761cd46affd46622f2a2192a79425ebf51c875fc7ba3598e15e0ba2465fc3e87c8a5da1915d3b8abe4b16d21259f311183eee1e7d2b808a91a7c89b284df0eb6a2a79c610bbe47722b3e04d5a6c0e574816a94d97349b6976010eb8c7debf42210982f78de482b7cd068051bf57908dbf46b5ceaf64f5fb33ede4412c1ce81eb1dfb4e99e10dd9b57ebc6e62ecbf4ee2db04d9e48c62bd45f8fa51704d414296a2d51d25ced6a192034a44c67e09d8985b573f98e03fa36dd8dcce2c04b4d5b1f276b6a642aadbdcafcf09de1d234bf8bbbf64aeadf01519ddafb419b3e62d204e04c3d7ebaf54b09e387ac3e9c4781c11625a2f44fddb7a1886f21929bd01c283f64903b6ccbb463984dfadc00f6af2a421517da023fd319f528195cac5fe907624b70c0172479d07d78e266dbf20ab8fc302228f279ffdba7395a839c4a9d7a4e001a260e1702393968f1e9722f023b204cf09cfee9a7bba045e4a2a449ee9fbb5c36e93028cfc87a2e34914b1b4f01beeded175ac0fa73fea9292f2bc3b1247164d8e05cddc3981bdc24e5f596571c418f6fa00fd9d4d0898cbf0d2f5413bed5f100f1854903017b6bc88bd7e303b5e0e2417bbcc984731128eda550d31f9af0e6e743eb6916466bbd435617d56fa60b05cd7dca66a9f6f4be23d3c5ff5d900822c6d1d8d71b0bab24f57d9682381a87c", + domain: "signaler-pa.clients6.google.com", + wantErr: false, + needsMoreData: false, + }, + { + name: "QUIC Chromebook Handshake[3]; packet 1", + hexData: "cb0000000108676ef9ec3514fd5f004046001ae0c831dde6ac72f1337c9ca111f5b32afdf102d75c017d3bccb6fa89902b750b00c51bb226afa517754b72e962ff007c1c8c8749a21be1d8d94f7bf73437c010cd60e0bd4489d0a84868f32ecd4bd0d4c95a8a08d60d8303ef02323bdbcd96a33940824d25b9594bd8ae5716b9d043ab27ba03a2593c4d149b923711dd98e45456db19c8ea71e982562ae787c1b6cbe3ca1b03df2df62aa8e3127c5f68bdf80ed90588e7ec1f41f5a6281b87a12348cb17a04e9eaa461fef2e0e3ae70ebdc7bffa15c6b61ab270173e46dec0fd081f935a5fe6338d3b9cd38fcb52cb159edb66d2927238294313990da25c22f40d40e3cd72e76bbd066de731cf8fb6b4b7bc7639efb788c0b108dbf8280845a2cc62fbaf5fb8e1ecbe5ba7791aab94786c1c71c9058d0153b34a3f5bc8903e0d120f353defbe973cd33568bd03609dcdab8af1e8563897f5dd0251c6e6514bf40bd447d376fed21b2c54ebf74680df241bdc2ea5579bc0736cf3257c20d275746e8e6853aa89dcda8c2dbe523438ab92ca1ed1ab4f109e4ea84de57dfb6c544d695a5b710fe2d432f2b58644f8aeb965752d3a1d1a3057c2229192f89b254f5d292c22f1060642729df3667ef39e27691c82da9be847a59a17ba7345d23a37e31ec135633cc5ea84c752f56d4ec75878a2920b93e9b4e091e0114552712e1e50ade42e26ac0266b84043a493e1ce2e80cd57422de16a88deceaa55385dc2a977ffc9063e7c427200b6d8511ef9004f89412587bd6d0057898f5ae284db78b0ec861fed36dfb7c7a9679ad0480eefe71985ba6f731bd0e816a901e0c017dd0cb7fc8a4606dec2091a51aab16d6f9bbdecf3fea177671e68250a84fe19de8df78d711e22b81372bc22ae21ac7208ed41201f6e26cd6748e9d6e2f4884f5acba736b2432536718891638d43991bd97c232829e26be6e6bb303d44849b245ef758eb2813bc87cf21a30f132360111e3015de5d1e4f0c5a98aff159c29f6debed7c2f18f455dfc7f33995a90b7625688507ecef1e7db48e7030ea6c4fa835bbc1dfbea6c0a6c704d658d4866a42b9860b1c8b5b64cb669e102c81e369b5f07b8fa08816a566a99f4d2910f6e8d751d52f1e2889f0ec9acfcb4627e0da5c35452be05c7766eddf3c42ceb6a312044075a4231b4203718c886498a313f3ba12e44e368b04ec3ea6e72d6fed9b6b334cbc0ba89f0aa9a129b1bad5b0ad8690291a344967f58e52415859852c6ca3ea24bc93ec1041fd1dc8a6a181326d3026098db0cddec90b3cd6df1e7638a3703f70c9a3baff8f005b90f362459a275a8b39daa78ff24613434594f96b8023a41a17d815e5c0319a39e07d32841339f14f404030b4a22551b86ba94832a1c49053d63140b503f2f64354ce10abe6c08f6cdaf6d8dc361c3c9d1a8077ad34dccc699b6fe07c16f8f7743d04003d672f82e643b3f1d5e263495504e11e6b2e676c11b3d0033d5f837e6bfd01602584ff181e3cb86f081015a9311eed546b42a8280680aa538353949f89674c554b43241e36536430ae9e0190729ce902e8f06a952d23b62816deb3b62b45375033ede2d8065a8e7b38f5aee0a5c66eb2f21f33fa6795d4b086e6f6ac941ba0c883ccf6e54e52164384045e0b0d74a9361f224303c841ec907be250725ab06cf79dd8bff8f46c08963a409b9b71b5c634c987c5e163f73fc32553be1231c72444c5e2a91189824034a784948f", + domain: "", + wantErr: true, + needsMoreData: true, + }, + { + name: "QUIC Chromebook Handshake[3]; packet 1-2", + hexData: "cb0000000108676ef9ec3514fd5f004046001ae0c831dde6ac72f1337c9ca111f5b32afdf102d75c017d3bccb6fa89902b750b00c51bb226afa517754b72e962ff007c1c8c8749a21be1d8d94f7bf73437c010cd60e0bd4489d0a84868f32ecd4bd0d4c95a8a08d60d8303ef02323bdbcd96a33940824d25b9594bd8ae5716b9d043ab27ba03a2593c4d149b923711dd98e45456db19c8ea71e982562ae787c1b6cbe3ca1b03df2df62aa8e3127c5f68bdf80ed90588e7ec1f41f5a6281b87a12348cb17a04e9eaa461fef2e0e3ae70ebdc7bffa15c6b61ab270173e46dec0fd081f935a5fe6338d3b9cd38fcb52cb159edb66d2927238294313990da25c22f40d40e3cd72e76bbd066de731cf8fb6b4b7bc7639efb788c0b108dbf8280845a2cc62fbaf5fb8e1ecbe5ba7791aab94786c1c71c9058d0153b34a3f5bc8903e0d120f353defbe973cd33568bd03609dcdab8af1e8563897f5dd0251c6e6514bf40bd447d376fed21b2c54ebf74680df241bdc2ea5579bc0736cf3257c20d275746e8e6853aa89dcda8c2dbe523438ab92ca1ed1ab4f109e4ea84de57dfb6c544d695a5b710fe2d432f2b58644f8aeb965752d3a1d1a3057c2229192f89b254f5d292c22f1060642729df3667ef39e27691c82da9be847a59a17ba7345d23a37e31ec135633cc5ea84c752f56d4ec75878a2920b93e9b4e091e0114552712e1e50ade42e26ac0266b84043a493e1ce2e80cd57422de16a88deceaa55385dc2a977ffc9063e7c427200b6d8511ef9004f89412587bd6d0057898f5ae284db78b0ec861fed36dfb7c7a9679ad0480eefe71985ba6f731bd0e816a901e0c017dd0cb7fc8a4606dec2091a51aab16d6f9bbdecf3fea177671e68250a84fe19de8df78d711e22b81372bc22ae21ac7208ed41201f6e26cd6748e9d6e2f4884f5acba736b2432536718891638d43991bd97c232829e26be6e6bb303d44849b245ef758eb2813bc87cf21a30f132360111e3015de5d1e4f0c5a98aff159c29f6debed7c2f18f455dfc7f33995a90b7625688507ecef1e7db48e7030ea6c4fa835bbc1dfbea6c0a6c704d658d4866a42b9860b1c8b5b64cb669e102c81e369b5f07b8fa08816a566a99f4d2910f6e8d751d52f1e2889f0ec9acfcb4627e0da5c35452be05c7766eddf3c42ceb6a312044075a4231b4203718c886498a313f3ba12e44e368b04ec3ea6e72d6fed9b6b334cbc0ba89f0aa9a129b1bad5b0ad8690291a344967f58e52415859852c6ca3ea24bc93ec1041fd1dc8a6a181326d3026098db0cddec90b3cd6df1e7638a3703f70c9a3baff8f005b90f362459a275a8b39daa78ff24613434594f96b8023a41a17d815e5c0319a39e07d32841339f14f404030b4a22551b86ba94832a1c49053d63140b503f2f64354ce10abe6c08f6cdaf6d8dc361c3c9d1a8077ad34dccc699b6fe07c16f8f7743d04003d672f82e643b3f1d5e263495504e11e6b2e676c11b3d0033d5f837e6bfd01602584ff181e3cb86f081015a9311eed546b42a8280680aa538353949f89674c554b43241e36536430ae9e0190729ce902e8f06a952d23b62816deb3b62b45375033ede2d8065a8e7b38f5aee0a5c66eb2f21f33fa6795d4b086e6f6ac941ba0c883ccf6e54e52164384045e0b0d74a9361f224303c841ec907be250725ab06cf79dd8bff8f46c08963a409b9b71b5c634c987c5e163f73fc32553be1231c72444c5e2a91189824034a784948fc90000000108676ef9ec3514fd5f004046001ae0c831dde6ac72f1337c9ca111f5b32afdf102d75c017d3bccb6fa89902b750b00c51bb226afa517754b72e962ff007c1c8c8749a21be1d8d94f7bf73437c010cd60e0bd448983eee52163a177650f57b2cd8404bc619b9b59e796f9808bcd549ae6ae30d448c90f2783978bf9314a8038f45c0da5983163bd26f38f559f59447e8cf004f93b6b5c8af7b09603db021d4bdfa641bd83926eae1709a7a427add14df90cb258c6d4663d4d29709da89c90613d2ff9334637d53ca89407804eb863f78e110b866af2734c980705d9f969730a41132e788fc9e426d0f68ed24157aaff0383438d2715262e9b8b03cff850ba88127a05a8b68ac9a5ae5b098bb9ba5eaadd71ae846b3c0f68db728361eb8c8ed899c77725afbdfabf93812c49cbf4ee64047a96ea71258dbe5be3f988029d005fa2d9fc6e1e53fbeb6888074521b972e2ac71b4f22b754fc743e0de21af1e2ab416b2481e03227a1c7d7ea6cac5bc37ee3597d3bf11bf13a688dfa3d9aeff1eb1a7fdfcf8b6c722a4853f7c2b2d31e0b2b691f4273d4793fbb7a00f27a25577bfcba95e60699c9d2a926e71d64f535b633f2fd03320b28fe86c6619c54b34e6caf8f5a71b8a144c9236bf07edaacb486ff8ac63173af099efe7c9d006a5bb756449fb32b1fbff2e315fd5e96b586bb922a9795e29ffe6ead037c556e1bf30e24afb344cf873201007096b6f687f157588e236b71ade4d9245d8f065f2e23b36fad798d0f5504ddf25b828698d0cbdc28478b20d692d2ab605797a67232b0795927d886de798f00b4e7c69517d62b748e62e01d53dd1e77ac9a1605c0408713ff309ad53ff8f2bef17f9074f01134374068bf1f5dc07125180b5ea6902ec2d55c7d6d5f7ed4ef8732f9d34b4627678611fc9579e4321cea012c4e457dee6a11c41bdd1eb965056e885757af389079a558434eb3d59ae56a232302759431172ecc88de1c5400265f0f47e21396e3c38e0ba022c3e55ee4b85527cf49dece94445adc740cd26c18004a1cb984cc1732a138844da1ab003f89c589b6f3cc10c99a1b0d87be763f83e1b12c6fa6938ebc55d2ba33c25ca816dde207f7186f0c70b56b33feb538eb31175fdcfb036e365087f1b630628affdbbdee20d1976cb009f32db5f35aadb8117aa02ff2da9bdeaffbcc8bf3412efefeb00365e5f1ea577afd6e1c3585c67ffe1fa120382aa54028dcae9bbd624432a6256687d05483f2611f1ddd14b40f66fdf547e7eba904a79bd27733c9a8fbfb01154dda3457c4eacff8116941777ec570ff040e217d648ea5076588a6417462481eba68ebc59af04ba49b92f70b68a007977fde48b94b0af35475ea19cbec92df6449b065880bf03452cb3b3582f3d1a010e585be6506f3e067226471a94ce46c515f20502b3866553c10f037d9be89ad5858d6b2d2d94c70159247f66958d0e841d1c5b4254809d52475fdf96d087c3c6647b86006147a9ebb3f52ea6f4b89d886725b9e9243efd95e434bd8dd785143c57c06863b68df8f832987eb0c730c8b96634c1f888da2ef420cb0ebacf81f4b25c65962ae40c09ac4b0b2d440e3bdaa7309d87a1fa6af1c2e13e7a63c253fae027ceb2067cef8421b62d205f5d37c7204eaf594b1b43f9d9b67509a6709df48769ab9e1078f9e59d7656ec2132b5ebccf297e757a052835fffe94ae073131ac49c4f4374a1904cd4bf3041b236b73ea19eaa583db577fe35", + domain: "", + wantErr: true, + needsMoreData: true, + }, + { + name: "QUIC Chromebook Handshake[3]; packet 1-3", + hexData: "cb0000000108676ef9ec3514fd5f004046001ae0c831dde6ac72f1337c9ca111f5b32afdf102d75c017d3bccb6fa89902b750b00c51bb226afa517754b72e962ff007c1c8c8749a21be1d8d94f7bf73437c010cd60e0bd4489d0a84868f32ecd4bd0d4c95a8a08d60d8303ef02323bdbcd96a33940824d25b9594bd8ae5716b9d043ab27ba03a2593c4d149b923711dd98e45456db19c8ea71e982562ae787c1b6cbe3ca1b03df2df62aa8e3127c5f68bdf80ed90588e7ec1f41f5a6281b87a12348cb17a04e9eaa461fef2e0e3ae70ebdc7bffa15c6b61ab270173e46dec0fd081f935a5fe6338d3b9cd38fcb52cb159edb66d2927238294313990da25c22f40d40e3cd72e76bbd066de731cf8fb6b4b7bc7639efb788c0b108dbf8280845a2cc62fbaf5fb8e1ecbe5ba7791aab94786c1c71c9058d0153b34a3f5bc8903e0d120f353defbe973cd33568bd03609dcdab8af1e8563897f5dd0251c6e6514bf40bd447d376fed21b2c54ebf74680df241bdc2ea5579bc0736cf3257c20d275746e8e6853aa89dcda8c2dbe523438ab92ca1ed1ab4f109e4ea84de57dfb6c544d695a5b710fe2d432f2b58644f8aeb965752d3a1d1a3057c2229192f89b254f5d292c22f1060642729df3667ef39e27691c82da9be847a59a17ba7345d23a37e31ec135633cc5ea84c752f56d4ec75878a2920b93e9b4e091e0114552712e1e50ade42e26ac0266b84043a493e1ce2e80cd57422de16a88deceaa55385dc2a977ffc9063e7c427200b6d8511ef9004f89412587bd6d0057898f5ae284db78b0ec861fed36dfb7c7a9679ad0480eefe71985ba6f731bd0e816a901e0c017dd0cb7fc8a4606dec2091a51aab16d6f9bbdecf3fea177671e68250a84fe19de8df78d711e22b81372bc22ae21ac7208ed41201f6e26cd6748e9d6e2f4884f5acba736b2432536718891638d43991bd97c232829e26be6e6bb303d44849b245ef758eb2813bc87cf21a30f132360111e3015de5d1e4f0c5a98aff159c29f6debed7c2f18f455dfc7f33995a90b7625688507ecef1e7db48e7030ea6c4fa835bbc1dfbea6c0a6c704d658d4866a42b9860b1c8b5b64cb669e102c81e369b5f07b8fa08816a566a99f4d2910f6e8d751d52f1e2889f0ec9acfcb4627e0da5c35452be05c7766eddf3c42ceb6a312044075a4231b4203718c886498a313f3ba12e44e368b04ec3ea6e72d6fed9b6b334cbc0ba89f0aa9a129b1bad5b0ad8690291a344967f58e52415859852c6ca3ea24bc93ec1041fd1dc8a6a181326d3026098db0cddec90b3cd6df1e7638a3703f70c9a3baff8f005b90f362459a275a8b39daa78ff24613434594f96b8023a41a17d815e5c0319a39e07d32841339f14f404030b4a22551b86ba94832a1c49053d63140b503f2f64354ce10abe6c08f6cdaf6d8dc361c3c9d1a8077ad34dccc699b6fe07c16f8f7743d04003d672f82e643b3f1d5e263495504e11e6b2e676c11b3d0033d5f837e6bfd01602584ff181e3cb86f081015a9311eed546b42a8280680aa538353949f89674c554b43241e36536430ae9e0190729ce902e8f06a952d23b62816deb3b62b45375033ede2d8065a8e7b38f5aee0a5c66eb2f21f33fa6795d4b086e6f6ac941ba0c883ccf6e54e52164384045e0b0d74a9361f224303c841ec907be250725ab06cf79dd8bff8f46c08963a409b9b71b5c634c987c5e163f73fc32553be1231c72444c5e2a91189824034a784948fc90000000108676ef9ec3514fd5f004046001ae0c831dde6ac72f1337c9ca111f5b32afdf102d75c017d3bccb6fa89902b750b00c51bb226afa517754b72e962ff007c1c8c8749a21be1d8d94f7bf73437c010cd60e0bd448983eee52163a177650f57b2cd8404bc619b9b59e796f9808bcd549ae6ae30d448c90f2783978bf9314a8038f45c0da5983163bd26f38f559f59447e8cf004f93b6b5c8af7b09603db021d4bdfa641bd83926eae1709a7a427add14df90cb258c6d4663d4d29709da89c90613d2ff9334637d53ca89407804eb863f78e110b866af2734c980705d9f969730a41132e788fc9e426d0f68ed24157aaff0383438d2715262e9b8b03cff850ba88127a05a8b68ac9a5ae5b098bb9ba5eaadd71ae846b3c0f68db728361eb8c8ed899c77725afbdfabf93812c49cbf4ee64047a96ea71258dbe5be3f988029d005fa2d9fc6e1e53fbeb6888074521b972e2ac71b4f22b754fc743e0de21af1e2ab416b2481e03227a1c7d7ea6cac5bc37ee3597d3bf11bf13a688dfa3d9aeff1eb1a7fdfcf8b6c722a4853f7c2b2d31e0b2b691f4273d4793fbb7a00f27a25577bfcba95e60699c9d2a926e71d64f535b633f2fd03320b28fe86c6619c54b34e6caf8f5a71b8a144c9236bf07edaacb486ff8ac63173af099efe7c9d006a5bb756449fb32b1fbff2e315fd5e96b586bb922a9795e29ffe6ead037c556e1bf30e24afb344cf873201007096b6f687f157588e236b71ade4d9245d8f065f2e23b36fad798d0f5504ddf25b828698d0cbdc28478b20d692d2ab605797a67232b0795927d886de798f00b4e7c69517d62b748e62e01d53dd1e77ac9a1605c0408713ff309ad53ff8f2bef17f9074f01134374068bf1f5dc07125180b5ea6902ec2d55c7d6d5f7ed4ef8732f9d34b4627678611fc9579e4321cea012c4e457dee6a11c41bdd1eb965056e885757af389079a558434eb3d59ae56a232302759431172ecc88de1c5400265f0f47e21396e3c38e0ba022c3e55ee4b85527cf49dece94445adc740cd26c18004a1cb984cc1732a138844da1ab003f89c589b6f3cc10c99a1b0d87be763f83e1b12c6fa6938ebc55d2ba33c25ca816dde207f7186f0c70b56b33feb538eb31175fdcfb036e365087f1b630628affdbbdee20d1976cb009f32db5f35aadb8117aa02ff2da9bdeaffbcc8bf3412efefeb00365e5f1ea577afd6e1c3585c67ffe1fa120382aa54028dcae9bbd624432a6256687d05483f2611f1ddd14b40f66fdf547e7eba904a79bd27733c9a8fbfb01154dda3457c4eacff8116941777ec570ff040e217d648ea5076588a6417462481eba68ebc59af04ba49b92f70b68a007977fde48b94b0af35475ea19cbec92df6449b065880bf03452cb3b3582f3d1a010e585be6506f3e067226471a94ce46c515f20502b3866553c10f037d9be89ad5858d6b2d2d94c70159247f66958d0e841d1c5b4254809d52475fdf96d087c3c6647b86006147a9ebb3f52ea6f4b89d886725b9e9243efd95e434bd8dd785143c57c06863b68df8f832987eb0c730c8b96634c1f888da2ef420cb0ebacf81f4b25c65962ae40c09ac4b0b2d440e3bdaa7309d87a1fa6af1c2e13e7a63c253fae027ceb2067cef8421b62d205f5d37c7204eaf594b1b43f9d9b67509a6709df48769ab9e1078f9e59d7656ec2132b5ebccf297e757a052835fffe94ae073131ac49c4f4374a1904cd4bf3041b236b73ea19eaa583db577fe35ca0000000108676ef9ec3514fd5f004046001ae0c831dde6ac72f1337c9ca111f5b32afdf102d75c017d3bccb6fa89902b750b00c51bb226afa517754b72e962ff007c1c8c8749a21be1d8d94f7bf73437c010cd60e0bd4489ea77dbb530c7ba127c66c3d7bbc00c336fd4e09e1775c646dffaa8696f7b8b00bf91261fc5164d57a4b9652b7cff4e301d32224b4e48cbfca535b2070ac46181615358d87e244ba6e369f6719bd5a551ac05dc78c222fd0969d0d943cbfaa3570ec25ab2768e9679d1cd1a3528659d010c409a0719526c44e4d9915dc5b0618ebc9e35f06b31bfd8e01fad99dabe32f6bfa00b3a5db5a01920d6685c34efb958729ffc5acfe46b3605715149b65b2f638007885a0866bbdde6765992b9acce2f527de906443f8643845489f1224fd3bbbb3fa78ca4848fe0167ec7cff8a05a17eb7c7a05a80c3106647e5d9aae350f33d10f3a60ab1c705858323a8f610d98cc68ef3cea66eedbb788b9a3da873bfd44ed632aa952ed7bb2004f4502260cef0596ec6e82e7683bdd2cb1f63b01b3f928ffa86b89cbeee922f1fd192fea0bdd17cf62d14f06f9e27bf5cafec90ab26f103e1dcb96ae4335e444b1fbad294cc395c5dc3a1c0c1fb4078d362eb229c42bc42ff53115c51f137d75596b3e6d3d28974720d6935430054c6b630bade51ad508d31fdfd572bd37f70e3dc06021d2b0ccf91a7975aa501e152d62980f02ce0ee94b547a2fbede47cf1f5c0a541ccc8992dd006f77437ce6a6b1f4f91833914a1cc51acf9336a620c4a22073966cde3ecac3224941dec004e741e05c11b43796dc531ce33e7c9a4fa68fa689880842e37a3a04fb75f3fcee86813388df74d443d1c35d7adea290effa98309b22ceca9bc252ccb4c443733db691adf0af559a5b7565043f84e91c5ed9f79ebf49f0bd60b68a7b8730032574e8e21548204c75321a374bbbf822efb1281ddf32feeced4bfe22bcf7c1a309954c1e356175a8a1a1a074a22f4561acd872d813c88ea9f0f22ac8d7b7b2088bc8565e1c56dfbc84f57aa38c2600ee20e8736076a91ee73f3137e8da3fe3871587b8bdb0a08af40babbe5493f036b45eea837dc15f761d9475d27a512a2d9dfc1ccdb81e2b581f91a5d7fe67cf6955427315a4e9c158806e651e4acff40051cd8a44b0108876c82f7b4d69033bfd8216234de545bf8fb58e489bc74d366db5e48711ba7f317dcdd1708ed5de97468a6026e15bc68ab11efc90f5465b4466bf384a8cc95f9c7fff91d776cccceeae5badebc31c3516c93a7b4682212e5a4902a9fd0327234749d83c141db2eee9688a76f4361f8b6213c88ebde69ebb84488c9ab8f42737da123eaf39373ac687df65f817939296f5477f92ad3fda0effbdd5d0594eb59d80265eef6cfcaf81b386c9d03c205c1b6714bf31be15e8f871b4791aec10884938285d6b8c18a0dfe750b753de88a2d2d855b9d1a0068ac4d2ce3a259bfdd30414380bf8b287abf2a28de442552f1d70a0aeb0867d9c7ed4e9717565ebc6aca21d85d2845faacfd8fadb1a76d9a2cd619413e631666009085bc7dad7492654ec20431e37ddd55588d2cd4d256021547cac768dfe3f7dcc9a18f0c72a743de799e98398b9bb2f216aea727d240b2f52ee269f4df4b8d7bdb439f074e3ef179ae2ae44daba64864fe427574b659a5e79defaf43e45e1357e1ff48e28ba6384c559cd036c9229151f917865b5575cda11e1ee0690bfbdb628b9a17e9c37190102", + domain: "signaler-pa.clients6.google.com", + wantErr: false, + needsMoreData: false, + }, + { + name: "QUIC Chromebook Handshake[4]; packet 1", + hexData: "cc0000000108a3e7f3133d37e2970040460094b49b4808cfaa190ce163e91b9d2c0105f36a2c93f670114f7b60598f03d6c596ceb19f410e9660903f590e3f25cf619e5c171001990b1d1b97f789595d3039e666345cc944894c6153ddd936992f160ae349757507f79fd0485766987d986518d9f19270a0021c52bff14e594c074f3c5664cf3de3b761cd36c16acf68565aaaeebf196da581533f19a464b22404b13b46ae3e4819bd4a7a85db6ddf379bb84f8dffcbb412c9ce405d8b4c98d303cb13df2e80a88ca0f09e2e2489c8d0b6d5ba9262b85869f8f989e9b82c4a270592894fda96bd27ce03e0cb4d4a4e130d9655e6da02f4348c949bc9b2aa609fdb4b9a0a3e45be25b18fbde569bb996d6419e98f2d9b7a0ca63f52f054b50771dc7c580596d86b4a94642be9a9b78a01ad2deae4607d8e0e25641aa8ed46b20fe027f4b9f77d1736aa0fec4f837cf5d878b3dcefdf3c6e82eb943dd022e98d623403950e6ea3addc0f93f92a422ed686b7beee437a4040f4a440dfd071d8c09f1344c28545b4488765a455e33e13d17434b9642dedadcb6a13ec35d51c3e7a03ae9a76cca6b1cba8312b7d8bae703e0a378300a17b7483d07893ddd941dd3e545a66faa0fabb4dc967c807665ebf4562fede176719d1a126f228acc0902e7235972a6db4eedd6547c38705629ee2574c5d2dd6c76c5c82e741f33291506e66a8df65ef6d1e7e6628fe4a4f5e141d482fc5f9d26609e64da8061eef5c0fca421d5334b199ca8270612074a1f9fbaad8b98ff7b81f8871f4ada6976f254e47c51e03d4c628beb3471ba375642ae0b41d65dec1419cd31f20ec779f5666717c1e7b4240fdcfa46a774234961083e1915e938ae0d41a66868b91949d856065c4e6813e0cbd9680a916b5eb78655dea9ad0f9c0ad0f5c244a72fcb8b589321519e34f0e6e25e6bb43abfa84a7241bc02555fa9060b9ef55f1e3a9dc3a575e16b23a36aabbd3ddeaf8f2516224179a4039a891e7f29631c2a08745bb184c66ffe98bc960e6c08a14524ae34444433591cdf7adc14419310b74594305f67c3087a2c21733e6e0be748e7af6fb6717946c313fbc0935ad3559e2d6323979cdc3bd48753b5a438605e15832efb8a0c4144060f41ed27a82dd067f2caaea3830abc97d9e080b3fd762aecbd58e8b2b17dae553dacdf3ee44198d2f19c0522b6a1b17923a210cf24902c5590afe808fd22e54e586399665d588a7febd0b402a4e6283679e1f95a2d4d7d3945e2bb8f44225ad8aa07cd07d3323ce94f39ae4c9466c05ceeb0a30981cea022d1bcab8a4b0c8e42e08211ee727728c74d7945f2350a149eb9cb7eb3a280954b64e612b53b19016a4c07427945345ffb86982c113ed797172ded4428d6b95b9ce64b48e98ab96421a179983c4a74b986f3f52d9a2d7fac8ea0955835d241bf4817a42950e2b298e51de20c026df81fdb0d28c68841bf62dfcfb4684def62c13ceddce9a25b446043056006ec8582aea14eee602eb2963f575dedfd2313d7d561c6ceac0d08c94645a222b25b7493542fee52c316f06f583612ab2ec3d420a01a61fe80b099386c2fe647292769d4571239592fe7e27f4324456ef894643f72ac450628cdcc9f376607b85f369a092c64d7d5a0559193e29cbc48e9ed77fa3fa05776d6169fbdbafa507db1e1d33a4550003a3a1a794b266e886f483eba76a8629d17d9596574068ffce61a52b209c21c77f5e7337e5541755c9f1c6b4", + domain: "", + wantErr: true, + needsMoreData: true, + }, + { + name: "QUIC Chromebook Handshake[4]; packet 1-2", + hexData: "cc0000000108a3e7f3133d37e2970040460094b49b4808cfaa190ce163e91b9d2c0105f36a2c93f670114f7b60598f03d6c596ceb19f410e9660903f590e3f25cf619e5c171001990b1d1b97f789595d3039e666345cc944894c6153ddd936992f160ae349757507f79fd0485766987d986518d9f19270a0021c52bff14e594c074f3c5664cf3de3b761cd36c16acf68565aaaeebf196da581533f19a464b22404b13b46ae3e4819bd4a7a85db6ddf379bb84f8dffcbb412c9ce405d8b4c98d303cb13df2e80a88ca0f09e2e2489c8d0b6d5ba9262b85869f8f989e9b82c4a270592894fda96bd27ce03e0cb4d4a4e130d9655e6da02f4348c949bc9b2aa609fdb4b9a0a3e45be25b18fbde569bb996d6419e98f2d9b7a0ca63f52f054b50771dc7c580596d86b4a94642be9a9b78a01ad2deae4607d8e0e25641aa8ed46b20fe027f4b9f77d1736aa0fec4f837cf5d878b3dcefdf3c6e82eb943dd022e98d623403950e6ea3addc0f93f92a422ed686b7beee437a4040f4a440dfd071d8c09f1344c28545b4488765a455e33e13d17434b9642dedadcb6a13ec35d51c3e7a03ae9a76cca6b1cba8312b7d8bae703e0a378300a17b7483d07893ddd941dd3e545a66faa0fabb4dc967c807665ebf4562fede176719d1a126f228acc0902e7235972a6db4eedd6547c38705629ee2574c5d2dd6c76c5c82e741f33291506e66a8df65ef6d1e7e6628fe4a4f5e141d482fc5f9d26609e64da8061eef5c0fca421d5334b199ca8270612074a1f9fbaad8b98ff7b81f8871f4ada6976f254e47c51e03d4c628beb3471ba375642ae0b41d65dec1419cd31f20ec779f5666717c1e7b4240fdcfa46a774234961083e1915e938ae0d41a66868b91949d856065c4e6813e0cbd9680a916b5eb78655dea9ad0f9c0ad0f5c244a72fcb8b589321519e34f0e6e25e6bb43abfa84a7241bc02555fa9060b9ef55f1e3a9dc3a575e16b23a36aabbd3ddeaf8f2516224179a4039a891e7f29631c2a08745bb184c66ffe98bc960e6c08a14524ae34444433591cdf7adc14419310b74594305f67c3087a2c21733e6e0be748e7af6fb6717946c313fbc0935ad3559e2d6323979cdc3bd48753b5a438605e15832efb8a0c4144060f41ed27a82dd067f2caaea3830abc97d9e080b3fd762aecbd58e8b2b17dae553dacdf3ee44198d2f19c0522b6a1b17923a210cf24902c5590afe808fd22e54e586399665d588a7febd0b402a4e6283679e1f95a2d4d7d3945e2bb8f44225ad8aa07cd07d3323ce94f39ae4c9466c05ceeb0a30981cea022d1bcab8a4b0c8e42e08211ee727728c74d7945f2350a149eb9cb7eb3a280954b64e612b53b19016a4c07427945345ffb86982c113ed797172ded4428d6b95b9ce64b48e98ab96421a179983c4a74b986f3f52d9a2d7fac8ea0955835d241bf4817a42950e2b298e51de20c026df81fdb0d28c68841bf62dfcfb4684def62c13ceddce9a25b446043056006ec8582aea14eee602eb2963f575dedfd2313d7d561c6ceac0d08c94645a222b25b7493542fee52c316f06f583612ab2ec3d420a01a61fe80b099386c2fe647292769d4571239592fe7e27f4324456ef894643f72ac450628cdcc9f376607b85f369a092c64d7d5a0559193e29cbc48e9ed77fa3fa05776d6169fbdbafa507db1e1d33a4550003a3a1a794b266e886f483eba76a8629d17d9596574068ffce61a52b209c21c77f5e7337e5541755c9f1c6b4cd0000000108a3e7f3133d37e2970040460094b49b4808cfaa190ce163e91b9d2c0105f36a2c93f670114f7b60598f03d6c596ceb19f410e9660903f590e3f25cf619e5c171001990b1d1b97f789595d3039e666345cc944892d4ba45357f2ca515d03b90820bb91c531a4be27266fda6022856da650cfb9c34139e8a3180e93cb73a6864471f849bdfa26c03e30c0e4d00309207cd46fb48887f60d7c51b208c247d1b311b35da70dd682cb1f7ae6a64215e5fefe25249daf308083837a3898e6052ebcf6cef3cb8e987ee1eb5ea797642d76391ae363b8eb2409d7486dd4a67c9e9b755376ca61009cb853835850e4fc1844f8e9eebca73e89317003482f70c4795ce9e2724c6d62172e010233e7bf203dde6eea9976f29896df562e8640a4ed88b5b3dff50296d0db43885f162c588d72de357c2ee049d9532642576de64d4e13cce77208e0aa9cf9838166f3375a968a5a6a01cf066ea0ca27fe4471cf0bf7eb36227867928076985588d05692d3f81d9a1158d150b2701399ee0a32693aaac43c27b76c657343b2a307e7018c2e9fe6317ec09f9afb762075430140b15016ae44acffc7467f4b1cec619942e916047c2db27f89742e53856d8c7c098beaba710340674a3f8455ab38fa2a4156fa3a45dffca1e20ec86ae792988dcb52bbf2ac97ea878e80511d3e4e70ece4b2816401ad450b9d13a1fecd7a5a363dd120285a972d52c06b632362cb8f897f799fb8342850b6670eb5083347347bd48b559f118839aa627598379963ecf18c2a900399ed936ab77ebdf95bdc5eaa75a903005e38dd99362b3d99f07ee2aea1a0ada77ec5ba76a7da2a80b672f4709bd32fad36787e37467e75fe594a24b402a6fa7e858c2abe9cffd9e885cc7091c035e4354779fc113bc084b5f6fcbd7bc618e9cc15205538cf781c96b658565cd8e39d95001d085d52f87a2970eb8f72149f4061d629ca0a928442b586aef9105326e13c015ce2b987c442b574180550302c48c2cf763b45492e25adee42d23bb2608e284caea4b3a28b77a20768dee6c0002b16e5d714eca22ca0c58195e03951b9564079ffa61c81afb2c5783ea2b8c0605d3994cfaed3c33af2279469c771269174fc5879b67617f571eb376161241ca5a89332074635413661b7fc5f925d86afb56b296dedce33d2b5011fc3b85cfb769c4a1cf5d6217ba3db367ead2159a310cd398b31df83f48f722a1df6c4b6604b2e288aa57cbc9afcf42764ccbc718a3ca42895b8e8287d632d2dd47d933a7472fd7be596c4241220917e636a44a139ec16600d74104fa6055a77c34bdbe14fb1ee56b4084d1d2dc1d56f8d636a8393385fcb4916670eaf8553b2126b12c8f187f58f00ba9bdbe8f06662688b9b8c7327b2d2c2f1443c8b87930d0948c3db62c8becb5b38cc7e484be184400f8c6293568abf1714e2832ad2c0aeb88dde64686fa7b4cd8c452b7365e70221e62dd971db057d4f8eed86e6802bbe8f56f75a8fe65d8216c81265e514042dca73f20a50373fb32e9bd62b741021337a3200b5f0a1b1329e99c57c75a60850f6c2f82cbeb9001edb54d985b7d5cd5957b31f79ee77dd8e1ce5e70d50806273885886b93a12c3dc0f211383d180a475d65bb54b0c5f761f456bfc7d045acd2c7ac648f3bf27bdb52218be48cdef7aa1ddc93e3ce11067ef4819796a4e2a30459c879841789b358b9430368b0910d6e6eaa14894f36b12fe9dd2bd1a20ddb6b12ab8fcec7d0629c9a7", + domain: "play.google.com", + wantErr: false, + needsMoreData: false, + }, + { + name: "QUIC Chromebook Handshake[5]; packet 1", + hexData: "c200000001085b15f32cf8fb5a3f00404600f909e38ed514455146c4ab8e53dd225a775dab2a06eaf73c2ec6b36c95c40873910eaa7e424e1b1fcb15892cf9e37a0ad2a1b67e9e8c6ec0c3a99e3d8775afe1e0aac824704489c14d28b48268df096f2db0c040c0ffdfb1817c4e1dd32005e87a74104515069ccd1b12f6fa3f657891bca8f0d27f4bb42b23c8310897ac09241f511888ac2e431e3ba8d9114b3f8bb1470324c8a0815dd1589686d058d03fa2669ad7367dc8877abce4700ae999112eb7cba755b20b7db66918021a36a66c60ecaaeb3e0859e3f50d14c1f8c3f56966043f437bfc16098deeb02e2236cc42d2f2802f4082bec30afc347a75f78c5e7337c52502e8e8d945bb4d792ef69419e57ee4e8f3465de4d0a49955787dff0756f1bcd98268919c112ae5e87ec333d590988e111724627143bf5a0aa69c77ee322b1240a65718e0033c4b70c6a5dc2d3248aef71598d73effa0eb397b79279ce846d9c2556fca28595eb3b196f40f052f0127711abbbc334571b8de67f7d39bd9437f9196bfe0650c4972462473776eccae42418b16edd5f45414e1a5e13300ae6705a49ef68e1748e876ce04fe611c973f94fcbe18b0c1a0ae506ce6eac268021e9744899c33ffe44dabdf1ea5413c20ab1013326255ae6df2d6861a7e914001e231ce4065e0d89fb439afb928d660cea26f99eafe65c9f89c5639f3ba8ffdcdf7a777d7859ca948f123a5c4f1491e22803754373ab0f1b605ac09ca6018c6acca7f52aad786ce96d9ae28d09a77dd88d77f23e2e33475973a23dea914fb049662800cfbed416b61835bd3bc22dace817b3b271c816ccf9603de2209067def5b8747112c49659cf6c5281402bed38a4176c0331e0841592c503d342188ac97266866a102864f76b0ff2adf781e0af84ae725fb6db418545dbb70adde5d4ea6d87f8c9b64b579d4f969830056dc0007cdad648035ccdf2c24264905422dfc1a85ac7f519f475351e046a27268312d4ab6a66ea55be01226fd5dfce06f804cafe85ad997946db3d029ef28ebdd36103c8fc05b522c1d4debe034523595cef576c771f0848a221d76c63dd044ea61a3adcbe5d4f0ebcd0bb631f4db1d88a609a1c2a6cc3de29ccd8a40e8a770d806abdae300cc178a32b4ba979652d0b8849dff252571f0b09935744c5b33628a190ae2eb43543ed8dee8539fd464abfcd3cf826aa18c4e84cd11975c5e3bfd0827f7cd211a2084c8626ed4e32bb9877f4801dabe695132b835e335563cb2f4c3e9377cac57f7766a10620f1c57c9f485d66918613daf8b257035ec91481d0076899c45abdbec38b63745428dee481c1cba81b0ab6e7efd6b0c431e018b6503cb13d4df18dbbff195bad1063d59ca5066e0733d3f499109111d22304066e7656e755518ceb6d862095ae54e532fad82f6c21c0c4d776dfecc516b00fa59e117e79481d3fd386c9b0c4d6fa4ec295a5b5b67207c3db20a206e0e31dad2c8dafb38e35dde331730bea33af32893653763e82565689009265db76b3b142d60f302a9545d82900aa6a017643c5c60583aefae43066ea3908e299612b078d06f102280dce4429461a42d80acdb6279c7aa190f9a8f61b485ffa430cbef199e1732da3d9b96cfac7ecc3b4959b86260eba763abb74c249180f67997967e716c95788bb6c04f182f40b795a929370c7fc72f1785f94cd36e3ddaddc15c7adb226cc131abc1863352caceb541d3924094db9c8a1c21c21640", + domain: "", + wantErr: true, + needsMoreData: true, + }, + { + name: "QUIC Chromebook Handshake[5]; packet 1-2", + hexData: "c200000001085b15f32cf8fb5a3f00404600f909e38ed514455146c4ab8e53dd225a775dab2a06eaf73c2ec6b36c95c40873910eaa7e424e1b1fcb15892cf9e37a0ad2a1b67e9e8c6ec0c3a99e3d8775afe1e0aac824704489c14d28b48268df096f2db0c040c0ffdfb1817c4e1dd32005e87a74104515069ccd1b12f6fa3f657891bca8f0d27f4bb42b23c8310897ac09241f511888ac2e431e3ba8d9114b3f8bb1470324c8a0815dd1589686d058d03fa2669ad7367dc8877abce4700ae999112eb7cba755b20b7db66918021a36a66c60ecaaeb3e0859e3f50d14c1f8c3f56966043f437bfc16098deeb02e2236cc42d2f2802f4082bec30afc347a75f78c5e7337c52502e8e8d945bb4d792ef69419e57ee4e8f3465de4d0a49955787dff0756f1bcd98268919c112ae5e87ec333d590988e111724627143bf5a0aa69c77ee322b1240a65718e0033c4b70c6a5dc2d3248aef71598d73effa0eb397b79279ce846d9c2556fca28595eb3b196f40f052f0127711abbbc334571b8de67f7d39bd9437f9196bfe0650c4972462473776eccae42418b16edd5f45414e1a5e13300ae6705a49ef68e1748e876ce04fe611c973f94fcbe18b0c1a0ae506ce6eac268021e9744899c33ffe44dabdf1ea5413c20ab1013326255ae6df2d6861a7e914001e231ce4065e0d89fb439afb928d660cea26f99eafe65c9f89c5639f3ba8ffdcdf7a777d7859ca948f123a5c4f1491e22803754373ab0f1b605ac09ca6018c6acca7f52aad786ce96d9ae28d09a77dd88d77f23e2e33475973a23dea914fb049662800cfbed416b61835bd3bc22dace817b3b271c816ccf9603de2209067def5b8747112c49659cf6c5281402bed38a4176c0331e0841592c503d342188ac97266866a102864f76b0ff2adf781e0af84ae725fb6db418545dbb70adde5d4ea6d87f8c9b64b579d4f969830056dc0007cdad648035ccdf2c24264905422dfc1a85ac7f519f475351e046a27268312d4ab6a66ea55be01226fd5dfce06f804cafe85ad997946db3d029ef28ebdd36103c8fc05b522c1d4debe034523595cef576c771f0848a221d76c63dd044ea61a3adcbe5d4f0ebcd0bb631f4db1d88a609a1c2a6cc3de29ccd8a40e8a770d806abdae300cc178a32b4ba979652d0b8849dff252571f0b09935744c5b33628a190ae2eb43543ed8dee8539fd464abfcd3cf826aa18c4e84cd11975c5e3bfd0827f7cd211a2084c8626ed4e32bb9877f4801dabe695132b835e335563cb2f4c3e9377cac57f7766a10620f1c57c9f485d66918613daf8b257035ec91481d0076899c45abdbec38b63745428dee481c1cba81b0ab6e7efd6b0c431e018b6503cb13d4df18dbbff195bad1063d59ca5066e0733d3f499109111d22304066e7656e755518ceb6d862095ae54e532fad82f6c21c0c4d776dfecc516b00fa59e117e79481d3fd386c9b0c4d6fa4ec295a5b5b67207c3db20a206e0e31dad2c8dafb38e35dde331730bea33af32893653763e82565689009265db76b3b142d60f302a9545d82900aa6a017643c5c60583aefae43066ea3908e299612b078d06f102280dce4429461a42d80acdb6279c7aa190f9a8f61b485ffa430cbef199e1732da3d9b96cfac7ecc3b4959b86260eba763abb74c249180f67997967e716c95788bb6c04f182f40b795a929370c7fc72f1785f94cd36e3ddaddc15c7adb226cc131abc1863352caceb541d3924094db9c8a1c21c21640c900000001085b15f32cf8fb5a3f00404600f909e38ed514455146c4ab8e53dd225a775dab2a06eaf73c2ec6b36c95c40873910eaa7e424e1b1fcb15892cf9e37a0ad2a1b67e9e8c6ec0c3a99e3d8775afe1e0aac8247044898c283466f3b200164ad9b30e17b425e07f6722df94b9a77dd555fbf25e5b0bae4fef254daf03f156b78afd967614c78208deaadef3552040c055487804c047604c5d6846e67d33e5fb5f743a81f688220d6f4c87091a860885af95d2db27e9c5dd2361f8196f5c1ade8fb37e159980547c38c6a6ddd0e055fbbd52bd7615615ffca15a0c144899d25f21156c53cb3922d2ccb83073e26074025a3c39f64a67dc02044ce0d630d9120041b2233bd26282bd2d7d1a81d486b64cab6dba7fb4375e200f53523f714a066b96769f9b1dc7c14353fc1faa51c0aeb99507ff3ae90ad6f4bfbc9b9ea00a87f8bf8e213a84a9efe2ce624e629261d93c83642df97fe146bdef478cc92bba387c9b524ac83cde55ad8f4a4d8fb3c09c8245a50ac16ebb67d651110a10ad1f7dc74ed32b9d644bc4b229c56942072aae5c311059165ef6839e7cfa0717c7032b667b8618527722fdc10a4c0ea900e9b414521b89ca83f253ea414a410a8b0b13da25c4b618f2ccd79e5d2a7579b4c431107d56ab8df16125bc25673181a6bd5abb09941515806fda32659e5281457d8c093314da0087169781b306f348d3994d84bdef936bb11355c41ce02c359174dc2a366509c130c96ddf5f156e0eb7912ac56611cf4f59ff3a8785034e81738a53ebc7fb60aaed709d78980d39822aae7e9a9d985aee6c11252a5e984ff1d167b9d4dc5d02ca8ba1167422dfc0f68b3b5902f3fd032204a5020b14a288ecd845816575fca7ef13c85765385e9964a9f0f6e2e5c4e600b190b275cd91ee8a38ecf73d35bad7371c15fdb73059afb3178786e76750845232ef787767c6985ea9b9ed9ff73430b7afb8e0b66aa210ab1943b606021bb8f50c55534e1014003d947533c1c0bd0c16dfaabf4914497583b818e643a2d8bc32d33a07133b4eecf8bcbd802dfaae894ebd473338e074af1b3672c6f03c55e6c3885848a4379cdb5873d9ff3015f9ddf8d954b3cb4eb9f56f2e8ccb519dcba72c5832146011068d3074520370fc8008b62f3fdd65978865df1a85e58fb62315dc617a3367e6c9564fbf74f5d5015932d435f3aaaaed37bc7d14d974c0e15d899c684f17db3b4790bdeba076624f4f45a0cc1c6c078a527695110842e837ded310f8c6ddf69a18e455f09130d4b4136ca6d02eee0f103b9923adefe8cb36b8fd554a27fa7300c4a6173835ca2c4d38cc11e3849ebbf73c475b7d0713caed6e70f55b34ccffe03978f231d5f92bc1e2da7ce589e2cc2cd76183394469fa537cb927db14a383dc589cf10819eb90dbdb7981cd1eeb3a284c9ce55ed859fbfbe73ca22328073b534bc81d319b5fdb97e66d532b5ffc19516b4c70ecb68f963b053a82915f3f74c8809f082bbc89d97dff9e8c82a65ed4be8e2975b82f0b7c73f34797c61db4ea8ee273116564288563a71747cf31568317363dffdff1d9e159b2f9bcd3fe5be96a9c50ac885493cc16234c58cdf471381664c1e3b54d147ea5c779c9c9b06d5c493231d9c03b50c6cba315a5ee766a6e8578105306e0911270d347e8ade2b354037f507e4993708191d32b51a79f023f4fccabb7d2ac262004745cc528b8a6dc1d8849e9d4a0152ab326b1eab8a505eef65076aca156f7b9", + domain: "optimizationguide-pa.googleapis.com", + wantErr: false, + needsMoreData: false, + }, + { + name: "QUIC Chromebook Handshake[6]; packet 1", + hexData: "c500000001087e13dee82c592724000044d0a17c2a188dadf8c82a018cabf25d25211d9987189b2900ad36a8fa366990353230bc8d41ec5543e91e119a0fd35f72d320d670074d02f412a5418c9a3e25489db73b1f5e5ddb8fc8d6c51fd6a12dcade71e1fc8e89d2663e206185247847d218d695793a8a1054afd18d42a4feaa9bb5fae7651eab0bc5e9aa91539c311cbfd17f46d8894ac45eade657bd456ee87b4475f5e84a2c07571b082ecfdafd1a660075692ae22048d163067a60e06cf879eab20a464c6e44b242da94cc6b92e064ee6a52cb34c115056925d4615e51157da4e88f019ae144040827cf2cd2806b3f4018cde5f42e041ef55eab35fe65ff79dba1ddad5aba6221e93d4555510176463888666b1d1f53a2f3c71e8872b05717002717edd3a9c5c651be47a663ee877962623ee92310b2b1dbcd75ec2f31d9e2f0c87affe3ee083aaa0868c03c9b4e78660d8afa4e936f81dfcb2fc33dbc1ed3828195c8de3130a2757641652de23d270c8a25976546c46460d9635f499a650475a4288d1c62241f65ddd346e2bde3f4fb5fec76646f51d829b55e16a0975494c11101abb504f5398a761d59355a4843e14bfeced024239ca06717d27e932df5e050d7c54c9b3cc937f6b0731f0b1dc312b14cf4388bbc0c601abf8ca16ed7d9b597dd414156d75b486ac256f8989a072fe680961ac7446201aca359337ef0e58ffa2aa12bb3c9bd1a9f6237b5c37880cd450d8a95db7c1463c53ac4bca174f278be1a99fb75ded9fe282e0e44a03b51ad943cf8970e4ca7f8567d9a3883d07bc0ff6c11639ebc7614fa88496488cb2a49a158d0f008a6a03caf97d77c3d03a98c5a611b04f865d134e258f4094bd220e00bde789c3b3158e622d509073179c0725c7a14dde1cd76b4c6b61fc7ba87f05742f081a5c42dbe83445132b8ad1539ee36e221a9dc700ccab55e34688b082a1a8edc48335760f3d5fb7e92b548e7a09d11f15b97bda08ebb5ea355e274587720d7c1189e9ffbb00dba3c13da94d646e7742e3641d59aee50a5bd400cd992c5614325d8f1ff3529637c3f60adc1682fa6b5e26f3d52de73236680abde87ead74368e05c600a2fd291a592be00fba64eea78c9e1f5e55e61f691b3eb0ef17b7bd11f06427c89c4c930d18c0b28221a6c918322cae56397c7e2978bf253e7f2d987f4cbe66c44a96b45cddb9d5f6dae47bbc9e1f9dcbe6280e50c5dfc91efb469f408b28fed49c583800009dc8bfb4bc42174175df987a3be833582abd9aa09ba0425973de2ea9a4149a81ae1863e0c9f1b1075c26bf965dcbec2bea47ff6042495ed715b65fdd3266800994463c95960dfb6ceadfa07d58910d329fa7ef7a8f14da4a6d3b09faa5b17cbac8481ea46cbfcfb54f660929e268bcf2f86cd88a1b065dcc27f18110db9efb6fcf1eec62874ed3657b1d43419f39e785c510d239c021b7e97d258d789c90d39b434f1667495bd4f5dc5e0eb97df376d801cced0da3a85aab6ca12893d8622314b5d530f28ead33075891d0ee553d5bedcffb20fce9933ab5e117df816c96398f6a60c9e6f5b9182fd7d58869de01635b2c178ae7738beb81e318934ecd752393129fda6833718d6984d8e1a8d7bf52e9d93ad0902c0fbe3e66ae8305e43363d8996626646a684bebfb1809ac9823be750e84308ef573243b884d09ef294094ef256cfc13cb0fedb1e095ff71687c09a767bff308e562e1a6ce9964014a7afc8db2481d8d07486", + domain: "", + wantErr: true, + needsMoreData: true, + }, + { + name: "QUIC Chromebook Handshake[6]; packet 1-2", + hexData: "c500000001087e13dee82c592724000044d0a17c2a188dadf8c82a018cabf25d25211d9987189b2900ad36a8fa366990353230bc8d41ec5543e91e119a0fd35f72d320d670074d02f412a5418c9a3e25489db73b1f5e5ddb8fc8d6c51fd6a12dcade71e1fc8e89d2663e206185247847d218d695793a8a1054afd18d42a4feaa9bb5fae7651eab0bc5e9aa91539c311cbfd17f46d8894ac45eade657bd456ee87b4475f5e84a2c07571b082ecfdafd1a660075692ae22048d163067a60e06cf879eab20a464c6e44b242da94cc6b92e064ee6a52cb34c115056925d4615e51157da4e88f019ae144040827cf2cd2806b3f4018cde5f42e041ef55eab35fe65ff79dba1ddad5aba6221e93d4555510176463888666b1d1f53a2f3c71e8872b05717002717edd3a9c5c651be47a663ee877962623ee92310b2b1dbcd75ec2f31d9e2f0c87affe3ee083aaa0868c03c9b4e78660d8afa4e936f81dfcb2fc33dbc1ed3828195c8de3130a2757641652de23d270c8a25976546c46460d9635f499a650475a4288d1c62241f65ddd346e2bde3f4fb5fec76646f51d829b55e16a0975494c11101abb504f5398a761d59355a4843e14bfeced024239ca06717d27e932df5e050d7c54c9b3cc937f6b0731f0b1dc312b14cf4388bbc0c601abf8ca16ed7d9b597dd414156d75b486ac256f8989a072fe680961ac7446201aca359337ef0e58ffa2aa12bb3c9bd1a9f6237b5c37880cd450d8a95db7c1463c53ac4bca174f278be1a99fb75ded9fe282e0e44a03b51ad943cf8970e4ca7f8567d9a3883d07bc0ff6c11639ebc7614fa88496488cb2a49a158d0f008a6a03caf97d77c3d03a98c5a611b04f865d134e258f4094bd220e00bde789c3b3158e622d509073179c0725c7a14dde1cd76b4c6b61fc7ba87f05742f081a5c42dbe83445132b8ad1539ee36e221a9dc700ccab55e34688b082a1a8edc48335760f3d5fb7e92b548e7a09d11f15b97bda08ebb5ea355e274587720d7c1189e9ffbb00dba3c13da94d646e7742e3641d59aee50a5bd400cd992c5614325d8f1ff3529637c3f60adc1682fa6b5e26f3d52de73236680abde87ead74368e05c600a2fd291a592be00fba64eea78c9e1f5e55e61f691b3eb0ef17b7bd11f06427c89c4c930d18c0b28221a6c918322cae56397c7e2978bf253e7f2d987f4cbe66c44a96b45cddb9d5f6dae47bbc9e1f9dcbe6280e50c5dfc91efb469f408b28fed49c583800009dc8bfb4bc42174175df987a3be833582abd9aa09ba0425973de2ea9a4149a81ae1863e0c9f1b1075c26bf965dcbec2bea47ff6042495ed715b65fdd3266800994463c95960dfb6ceadfa07d58910d329fa7ef7a8f14da4a6d3b09faa5b17cbac8481ea46cbfcfb54f660929e268bcf2f86cd88a1b065dcc27f18110db9efb6fcf1eec62874ed3657b1d43419f39e785c510d239c021b7e97d258d789c90d39b434f1667495bd4f5dc5e0eb97df376d801cced0da3a85aab6ca12893d8622314b5d530f28ead33075891d0ee553d5bedcffb20fce9933ab5e117df816c96398f6a60c9e6f5b9182fd7d58869de01635b2c178ae7738beb81e318934ecd752393129fda6833718d6984d8e1a8d7bf52e9d93ad0902c0fbe3e66ae8305e43363d8996626646a684bebfb1809ac9823be750e84308ef573243b884d09ef294094ef256cfc13cb0fedb1e095ff71687c09a767bff308e562e1a6ce9964014a7afc8db2481d8d07486cd00000001087e13dee82c592724000044d0b2c403d66eaa310a954540668e9edc4b17a321446ac931672ca3d9097b2854efdfa7a8f610be76592b56ef9154b9ab42f4dafc575a9b9572433fa2bba180194a32a72a92a42560ae840508317cf34015b1d7d88a633588ce4333cd12bec1f9d53fe905130eb136852d4f405ef66254a0807640fe415e6d667b9f5e2148826d5a6c36b3c44b822cfb7a1e76954610a522e1bc9703e155f8f1e0c705e411d09c5dec1183c61608174767408558da03290f5536682373a09c762deb829dfe3ee061ac9f509a2b3d20dbb77f262e9ba8a5ff50f895165742c90c4ff48eb0438d5ff8491700f97fbfced20e8e95ab9b67078ece6664527eb8a4e78944c07c6bc5c48776e141418c3b5a0e58d24114b7d65a83619525f5a10be53a55f6e3e65080cf42109aa2ec166fbb9079444ea6c809b5062227fed7cc81609a6d7ea471bc82cd0759a354aa896c7a8242c3d8c845aa52225bbaf7546a0de6510189cd2852f7b70a29687eee6a3a99a3a288f3c3672a72dba2843fcee320e18ea906adf1629cc7f8220b6be890a8d37f4093022717d8c6d76080d07798ba74e9cda1a4c26189c367a6d12da14621b93fbfd2d2365465350b5864e4981955750c6d4038b74644827cd60c8cad0d6a18869f99d61b214becedd8f71a476c2a1a0ea8fade7b15de6ee522b6b90d0cc4e6d40eb85d9b5097e2969e85eae6710285016ae47fbc858d63e13d882721aaef3cbe0a6e4c4910e8b3e465fa66a738d8979a444b4a4804baf5b4f9a5fc8438f4991c32d280c5d273d0a1e0d9eea30d003162cd313780a5b000f70fb7797e6111b354c3deac241cb816328b77d71584852966392cab9b1a3d64ead878a2cfac085452397ba17b5cddf96415c31846a80fa9a5a21fd5f9963324ad75505d4b5c70903ec3b5f91c3460d88a7d1c6b111a5206d11accac8a2115e1ce8e834b01f48e041dcd71585c6fcdd3a2e83ec2ff1a2b85e75134ec966afec023d375a2a8bcd54ef0c42d50e20c6cd9b9fdab63785e62c5a4ceb8451c560b647a6cd87698e56e0be4e2990337f45f1cb4c73f0dbf98da1158d75df6eb60ab7a61d836c7b7f3e5c809b850a5cc5369ddd4e455da5e88179e932311873d177febf6226a378c72324ea710e10ef74f6462e7dba25b7df336cb2947df749ad0b8455f3c5c9ba5c0c8eb1e665fe50451c928ce87cc81a0ae2102e7a4fc297392818db885943112fae3c4547ea48c89ef9cd44c2edba4856dbc72b956e4fc6a875bdac57b1cd2378ddb9322e5a1d1844977ff2a6e94d8a00f4ef0a7ef7949f0840c6cc781252830c70d37df9846a69eb95bc733ec420b7b3724572521496da27bba1fcb73c28eed1eefb094283ab01284ebd82004e0be9977f23fbf135b8162ac41a2bf9e1b941b05cb52c80279775070cf2f851bb0168235c97de47880d2d51f1d5ba8327af33f16f49d44326b0d08d20ca7880ff88a14d3201452d7fd452b24fc3b69b89c5149795bc4d0bf51b6ace2c9146048e5ca0259b4955a069a16d5a73e72248cec44b2ca542dcd326aca3ba53bd48368719f44d53eddbace11ff28c11c7fd90c38966752532d3f81325d1682839b2da3d69acc85275c71ef3b10c4d983cfb0cb464f554d9360b0d6ec8fd25b85824350866207eef9e2c66e43f5d08e86aa6186b36d9ece72f56e06ac8e51a1b53fab4cc496e69307e9810a9b5cf960f306ec54131dd8e917a5861ba92f6885e41993", + domain: "lh3.google.com", + wantErr: false, + needsMoreData: false, + }, + { + name: "QUIC Chromebook Handshake[7]; packet 1", + hexData: "c9000000010852e6d0c4f5f177b700404600eae9a1449fd711efdca846f59b6c9975a2189c1430f740e0e0eb4d50be086b798a47e037ee6afe9785b45df6302cfb87d39b9c03d0712f11793adab33e74904249722c4a1544896cd650da62160bdb601194f82a98c06df2546b79b5bab9503eafdbfbcfd1128204b52c9c5dde4d661cb84f2e015b7de9e10109874176362b7f06d4b49d850f2dcc8485aacb82fed8017fe454a1c31cfb82d392d6894081fa3ab1e5f2b71df79bf5a52a68a90d4752cf441220acbb274e3473f65c1a6e4cea3295da23e4a07818d5ba80b0668638c6ecb0b3ee12ecb8bed4ca32e27f4eee7376010b3905b8f7055728b8b1eda2e2707338b86f9a6115a23b29a010769d9a0e19d2bd06b81a507c4775d7215030862a026f168664255912c321669a5eff84ded4fec82af394cee909110758e5ad4de945236001bc0627d8e88ead3c3a6790d7e50887ced96f8380a80b11477147abc24dc4963c1907a07fec13e3cd8d5bab83cabd7cf8bb66af4911dfcdaf0f13aade34865b0740bebe4b4a482f1e3105bb9c3e5fece975187fa4eae5cb8fc868376b8b8329f89e407618aa5265f8ce73437e9f2714d752dad78025a38eaca1ebd6617a9a8a0bdefa8b3d3daad006978ba85d17ce3269da06be92eec61e4c5c99712517f01588b0b7deb4d7a75cadd5643daebc731f4a7bfaf17e8f002721b054a5d6d6ca7a3430c9480daff6adfd0a5480968b4fe0ee616d2e1f2f01268dcda2ee523fa593a0c83cbfc051dd7d503d48b152eb53894f79d4f6d38f05af35b287d16df579575b755f36e87b6df082108fda812195d5f60d2ceb59a54f09bb270d660c1d923348c7fdad97dc081e749a393af83bbcdd3c290efa19bce1a0f68dab0061df9dbee778dffb7754db10cbcc354a4b66c614e29d216a3a45a38594d337f775cb88c940924734ad52f8856606147b14ae2cf2990e1c401d79d27e4d6723afa4047454580698b9108b952b78b6bfd31db3b376f0879b2a0d8949b8443ffb9f7eb84b8024c620680481944312e86d183735effadab2d9f144a72d170fb035ed230f1451c94cb3f39bc28929e7480b22bbf6c906070eda03884cdd7ada9b10b7af27315c3bde8b4a273d46d1f764669aca152120da3e0a4fcf1be84b4f3cdfefe235a16598022cc74ad8f32e78aa06eb74ef61cccfc82ed35bcb70f008368ccdc17a6f64a316fcc9f006f307fa7a1a50f28c343c4c93a39df73a0d0a6dfc6f8ab10e966f738bee331e5a29d91ed993fd2638f0ec9d0ce539552b0a312fb85ed5e9f392fbc76a6164298b9de2c47dcb21a895957e92bc1270dfef3f00f44cc42c5f5005132dd030f9aec804045731c6a3eed3776beebe9451488932a1172b979f371aa370308037e57513a8fc9dd03d63fc2e5f7dcd683de26e116ea11a1d3b5e61fb5bbddc98e4ccd20be9ee71c02cacc95cbb17dd404558f586d4f0334bd12fc0a584d29eef3b4c2ce3f87babc462b6d24ca10aa8f1eb1abbd29d11ce3f1c92426c4950e53ba6c914cb4bf0bb1b44b25452cafbf246b76ce17f829bef3178174fbac4f932e6ac18e579fbbf8790611187c6f01de70fa82a21e979c90eed3c7f7b7e416491a000b5f2216e54858fa61893391b115573b2b960a0f7dc1e2a703a6f38485589c9133b4509fb54b4a602cc7d3341298e8e5da88d5e06aad28738650c08d8c71239735375e5bca7ea91dd78d748d65af598192317fd69e03daeecc99b8b", + domain: "", + wantErr: true, + needsMoreData: true, + }, + { + name: "QUIC Chromebook Handshake[7]; packet 1-2", + hexData: "c9000000010852e6d0c4f5f177b700404600eae9a1449fd711efdca846f59b6c9975a2189c1430f740e0e0eb4d50be086b798a47e037ee6afe9785b45df6302cfb87d39b9c03d0712f11793adab33e74904249722c4a1544896cd650da62160bdb601194f82a98c06df2546b79b5bab9503eafdbfbcfd1128204b52c9c5dde4d661cb84f2e015b7de9e10109874176362b7f06d4b49d850f2dcc8485aacb82fed8017fe454a1c31cfb82d392d6894081fa3ab1e5f2b71df79bf5a52a68a90d4752cf441220acbb274e3473f65c1a6e4cea3295da23e4a07818d5ba80b0668638c6ecb0b3ee12ecb8bed4ca32e27f4eee7376010b3905b8f7055728b8b1eda2e2707338b86f9a6115a23b29a010769d9a0e19d2bd06b81a507c4775d7215030862a026f168664255912c321669a5eff84ded4fec82af394cee909110758e5ad4de945236001bc0627d8e88ead3c3a6790d7e50887ced96f8380a80b11477147abc24dc4963c1907a07fec13e3cd8d5bab83cabd7cf8bb66af4911dfcdaf0f13aade34865b0740bebe4b4a482f1e3105bb9c3e5fece975187fa4eae5cb8fc868376b8b8329f89e407618aa5265f8ce73437e9f2714d752dad78025a38eaca1ebd6617a9a8a0bdefa8b3d3daad006978ba85d17ce3269da06be92eec61e4c5c99712517f01588b0b7deb4d7a75cadd5643daebc731f4a7bfaf17e8f002721b054a5d6d6ca7a3430c9480daff6adfd0a5480968b4fe0ee616d2e1f2f01268dcda2ee523fa593a0c83cbfc051dd7d503d48b152eb53894f79d4f6d38f05af35b287d16df579575b755f36e87b6df082108fda812195d5f60d2ceb59a54f09bb270d660c1d923348c7fdad97dc081e749a393af83bbcdd3c290efa19bce1a0f68dab0061df9dbee778dffb7754db10cbcc354a4b66c614e29d216a3a45a38594d337f775cb88c940924734ad52f8856606147b14ae2cf2990e1c401d79d27e4d6723afa4047454580698b9108b952b78b6bfd31db3b376f0879b2a0d8949b8443ffb9f7eb84b8024c620680481944312e86d183735effadab2d9f144a72d170fb035ed230f1451c94cb3f39bc28929e7480b22bbf6c906070eda03884cdd7ada9b10b7af27315c3bde8b4a273d46d1f764669aca152120da3e0a4fcf1be84b4f3cdfefe235a16598022cc74ad8f32e78aa06eb74ef61cccfc82ed35bcb70f008368ccdc17a6f64a316fcc9f006f307fa7a1a50f28c343c4c93a39df73a0d0a6dfc6f8ab10e966f738bee331e5a29d91ed993fd2638f0ec9d0ce539552b0a312fb85ed5e9f392fbc76a6164298b9de2c47dcb21a895957e92bc1270dfef3f00f44cc42c5f5005132dd030f9aec804045731c6a3eed3776beebe9451488932a1172b979f371aa370308037e57513a8fc9dd03d63fc2e5f7dcd683de26e116ea11a1d3b5e61fb5bbddc98e4ccd20be9ee71c02cacc95cbb17dd404558f586d4f0334bd12fc0a584d29eef3b4c2ce3f87babc462b6d24ca10aa8f1eb1abbd29d11ce3f1c92426c4950e53ba6c914cb4bf0bb1b44b25452cafbf246b76ce17f829bef3178174fbac4f932e6ac18e579fbbf8790611187c6f01de70fa82a21e979c90eed3c7f7b7e416491a000b5f2216e54858fa61893391b115573b2b960a0f7dc1e2a703a6f38485589c9133b4509fb54b4a602cc7d3341298e8e5da88d5e06aad28738650c08d8c71239735375e5bca7ea91dd78d748d65af598192317fd69e03daeecc99b8bc4000000010852e6d0c4f5f177b700404600eae9a1449fd711efdca846f59b6c9975a2189c1430f740e0e0eb4d50be086b798a47e037ee6afe9785b45df6302cfb87d39b9c03d0712f11793adab33e74904249722c4a1544898bcc839c7364f9518287bbf01c06e9fd67f74096cab78bc885ad264ca9151a48420ea11d5b9e3bc899cc0d663509c926bee5ef2a389a9945f5919677472b3245a2a68ab91654ba5f75026ae738a9e10b6b7439503961bedbfa2494099a9bba3b9595f70cacd8950abd5d3b8a85ac61e6482847e0ac71cbd63df462f4978afc7084ec5c9e65f3b57ad22f802d699c94f260212a61db6e6958ed60356bf991d3af9582cd1724a90f08e869ebd7f29fc3fb9e1198819a967096de1bdeea692fade6229b41c5efe6cdae313014db328e99bae1f323584a308f575b923a11185634b24c8cc1d608303c8ca5c1ae3d54b8b7ecb689d5e0e22323c5e47d8f9093080bcf1553f7fa238e156658ef50281f633f184e47992645c82ab9985b9b13f97f0bbd9db94285c131fff5870ea38691ade2e017ee6417a118a9b31f5b85ba2c76163f8668d90d448976d9d37238605d36b0b7e7abfb0e9c2866bb11b128bb70f003aeb77f439c6148d28719a4f336f4d622140faff82595233cff94906695e402b9115ba7b99f7d832f55d8de481af808ed9d48a493c3301d2b633cdcdc7a64acefaa9408b99a52768edeb9824cdb49257c6d3ea3f23763f70b74fc46252a97f7b3e173a93e81cca50fb12e8b0ea1083f9262e1607156296b5ff85e1531335441c90b442aa5dc616ed7b520bc040502280a0bdb011d6e9dac154587f033b3e9434cef567b724bf5203c315ef5e283028ec72ff33736499b8d326cd643e3a37d53e70b9a2384ac6037f488075eef8b2e507774d3d450ea4b1b65c1587eb2a60c6aad0ae20745a2c50f8acd8bc1e25740b4e7ab4b03a59731ccc487831b46ca0211264f4fd4a498e145aca0d7339663d29a7f32a14fc2b62a32f6354abf2d7de5ed9d7e0a1d6dbbfe14af25ca8c927dc8eeb2618f308eac81405552f1bafcaaede51f560fe5eb6aa78b8650ae97040b46469503c85994ebcf7ffa0222905657111325f861d380935c2a5b9ad26094e85b92327f79f66b4a2f8cd674344f931f0e056ba58a084a1026bc422b80ec11b17de1ca965e8ac9ede1464588303986ba8fabb395b55de5580131ae3bf61822e4e2817dacf765e034542e435c4e9ebc58f21cfa7901dfbcc2b2be98cc55fa6d9e0030e17a32abf84d551a74f7391dd204847c25ee6382f6374b8ecc5b29b598b9ba562e0a6f25ebb932570ae8ab7ef7d06e444fdf79845e88b3132ffb6e1f330e3424272da082486aba357bd254ef0738bb7f5c9db73d2a9ba0c9afeb34e09ff0e20bd44ba1a46ad3081db2f750d00b647756dec1bb41032e1aaf56f58d7046102aef6ae40517f9cbc148692401ad06ea4ff6ed3e5c8a0cff8f9466a3530a99cfb9b5a857a967675df0cfbe3b798fdc2d929fa4c86e3b3e3c45b75fa5db6c15953f25bcd025d7efc3172b2206a200128f6d8caca97154b2608511b35b0cba9a550d8f791552faed64036cfb8498dc60492207ab81c3f3edeafd9d3acf5bca2f2735117a273c70345d3b7e289b9948edda3d5ebb8adde7de2451fe2d942d92ab383dc9a9adb6fb9e8cbc0b73d852f176e0265a6107e6a59746e2f2e7203fd445e5fe278d02cd5dac1b7988e205c37bc5f35dc27572743810453b9b27e55c", + domain: "", + wantErr: true, + needsMoreData: true, + }, + { + name: "QUIC Chromebook Handshake[7]; packet 1-3", + hexData: "c9000000010852e6d0c4f5f177b700404600eae9a1449fd711efdca846f59b6c9975a2189c1430f740e0e0eb4d50be086b798a47e037ee6afe9785b45df6302cfb87d39b9c03d0712f11793adab33e74904249722c4a1544896cd650da62160bdb601194f82a98c06df2546b79b5bab9503eafdbfbcfd1128204b52c9c5dde4d661cb84f2e015b7de9e10109874176362b7f06d4b49d850f2dcc8485aacb82fed8017fe454a1c31cfb82d392d6894081fa3ab1e5f2b71df79bf5a52a68a90d4752cf441220acbb274e3473f65c1a6e4cea3295da23e4a07818d5ba80b0668638c6ecb0b3ee12ecb8bed4ca32e27f4eee7376010b3905b8f7055728b8b1eda2e2707338b86f9a6115a23b29a010769d9a0e19d2bd06b81a507c4775d7215030862a026f168664255912c321669a5eff84ded4fec82af394cee909110758e5ad4de945236001bc0627d8e88ead3c3a6790d7e50887ced96f8380a80b11477147abc24dc4963c1907a07fec13e3cd8d5bab83cabd7cf8bb66af4911dfcdaf0f13aade34865b0740bebe4b4a482f1e3105bb9c3e5fece975187fa4eae5cb8fc868376b8b8329f89e407618aa5265f8ce73437e9f2714d752dad78025a38eaca1ebd6617a9a8a0bdefa8b3d3daad006978ba85d17ce3269da06be92eec61e4c5c99712517f01588b0b7deb4d7a75cadd5643daebc731f4a7bfaf17e8f002721b054a5d6d6ca7a3430c9480daff6adfd0a5480968b4fe0ee616d2e1f2f01268dcda2ee523fa593a0c83cbfc051dd7d503d48b152eb53894f79d4f6d38f05af35b287d16df579575b755f36e87b6df082108fda812195d5f60d2ceb59a54f09bb270d660c1d923348c7fdad97dc081e749a393af83bbcdd3c290efa19bce1a0f68dab0061df9dbee778dffb7754db10cbcc354a4b66c614e29d216a3a45a38594d337f775cb88c940924734ad52f8856606147b14ae2cf2990e1c401d79d27e4d6723afa4047454580698b9108b952b78b6bfd31db3b376f0879b2a0d8949b8443ffb9f7eb84b8024c620680481944312e86d183735effadab2d9f144a72d170fb035ed230f1451c94cb3f39bc28929e7480b22bbf6c906070eda03884cdd7ada9b10b7af27315c3bde8b4a273d46d1f764669aca152120da3e0a4fcf1be84b4f3cdfefe235a16598022cc74ad8f32e78aa06eb74ef61cccfc82ed35bcb70f008368ccdc17a6f64a316fcc9f006f307fa7a1a50f28c343c4c93a39df73a0d0a6dfc6f8ab10e966f738bee331e5a29d91ed993fd2638f0ec9d0ce539552b0a312fb85ed5e9f392fbc76a6164298b9de2c47dcb21a895957e92bc1270dfef3f00f44cc42c5f5005132dd030f9aec804045731c6a3eed3776beebe9451488932a1172b979f371aa370308037e57513a8fc9dd03d63fc2e5f7dcd683de26e116ea11a1d3b5e61fb5bbddc98e4ccd20be9ee71c02cacc95cbb17dd404558f586d4f0334bd12fc0a584d29eef3b4c2ce3f87babc462b6d24ca10aa8f1eb1abbd29d11ce3f1c92426c4950e53ba6c914cb4bf0bb1b44b25452cafbf246b76ce17f829bef3178174fbac4f932e6ac18e579fbbf8790611187c6f01de70fa82a21e979c90eed3c7f7b7e416491a000b5f2216e54858fa61893391b115573b2b960a0f7dc1e2a703a6f38485589c9133b4509fb54b4a602cc7d3341298e8e5da88d5e06aad28738650c08d8c71239735375e5bca7ea91dd78d748d65af598192317fd69e03daeecc99b8bc4000000010852e6d0c4f5f177b700404600eae9a1449fd711efdca846f59b6c9975a2189c1430f740e0e0eb4d50be086b798a47e037ee6afe9785b45df6302cfb87d39b9c03d0712f11793adab33e74904249722c4a1544898bcc839c7364f9518287bbf01c06e9fd67f74096cab78bc885ad264ca9151a48420ea11d5b9e3bc899cc0d663509c926bee5ef2a389a9945f5919677472b3245a2a68ab91654ba5f75026ae738a9e10b6b7439503961bedbfa2494099a9bba3b9595f70cacd8950abd5d3b8a85ac61e6482847e0ac71cbd63df462f4978afc7084ec5c9e65f3b57ad22f802d699c94f260212a61db6e6958ed60356bf991d3af9582cd1724a90f08e869ebd7f29fc3fb9e1198819a967096de1bdeea692fade6229b41c5efe6cdae313014db328e99bae1f323584a308f575b923a11185634b24c8cc1d608303c8ca5c1ae3d54b8b7ecb689d5e0e22323c5e47d8f9093080bcf1553f7fa238e156658ef50281f633f184e47992645c82ab9985b9b13f97f0bbd9db94285c131fff5870ea38691ade2e017ee6417a118a9b31f5b85ba2c76163f8668d90d448976d9d37238605d36b0b7e7abfb0e9c2866bb11b128bb70f003aeb77f439c6148d28719a4f336f4d622140faff82595233cff94906695e402b9115ba7b99f7d832f55d8de481af808ed9d48a493c3301d2b633cdcdc7a64acefaa9408b99a52768edeb9824cdb49257c6d3ea3f23763f70b74fc46252a97f7b3e173a93e81cca50fb12e8b0ea1083f9262e1607156296b5ff85e1531335441c90b442aa5dc616ed7b520bc040502280a0bdb011d6e9dac154587f033b3e9434cef567b724bf5203c315ef5e283028ec72ff33736499b8d326cd643e3a37d53e70b9a2384ac6037f488075eef8b2e507774d3d450ea4b1b65c1587eb2a60c6aad0ae20745a2c50f8acd8bc1e25740b4e7ab4b03a59731ccc487831b46ca0211264f4fd4a498e145aca0d7339663d29a7f32a14fc2b62a32f6354abf2d7de5ed9d7e0a1d6dbbfe14af25ca8c927dc8eeb2618f308eac81405552f1bafcaaede51f560fe5eb6aa78b8650ae97040b46469503c85994ebcf7ffa0222905657111325f861d380935c2a5b9ad26094e85b92327f79f66b4a2f8cd674344f931f0e056ba58a084a1026bc422b80ec11b17de1ca965e8ac9ede1464588303986ba8fabb395b55de5580131ae3bf61822e4e2817dacf765e034542e435c4e9ebc58f21cfa7901dfbcc2b2be98cc55fa6d9e0030e17a32abf84d551a74f7391dd204847c25ee6382f6374b8ecc5b29b598b9ba562e0a6f25ebb932570ae8ab7ef7d06e444fdf79845e88b3132ffb6e1f330e3424272da082486aba357bd254ef0738bb7f5c9db73d2a9ba0c9afeb34e09ff0e20bd44ba1a46ad3081db2f750d00b647756dec1bb41032e1aaf56f58d7046102aef6ae40517f9cbc148692401ad06ea4ff6ed3e5c8a0cff8f9466a3530a99cfb9b5a857a967675df0cfbe3b798fdc2d929fa4c86e3b3e3c45b75fa5db6c15953f25bcd025d7efc3172b2206a200128f6d8caca97154b2608511b35b0cba9a550d8f791552faed64036cfb8498dc60492207ab81c3f3edeafd9d3acf5bca2f2735117a273c70345d3b7e289b9948edda3d5ebb8adde7de2451fe2d942d92ab383dc9a9adb6fb9e8cbc0b73d852f176e0265a6107e6a59746e2f2e7203fd445e5fe278d02cd5dac1b7988e205c37bc5f35dc27572743810453b9b27e55cc5000000010852e6d0c4f5f177b700404600eae9a1449fd711efdca846f59b6c9975a2189c1430f740e0e0eb4d50be086b798a47e037ee6afe9785b45df6302cfb87d39b9c03d0712f11793adab33e74904249722c4a1544894fb2a6f735bea394066ea4ab5d3ef6f419c4bb099bcea8fd7e36d8ec14fae027d0c84ba8c38a5ea1e2e9ff24ec334dee8085d1fa9d88df2c5ce3dddd981f7b2a68011f2d4034f8e4c6f43e645fc71e19927aec56045f0ef72d9da1942dbe3e42248c2f695525c8d8fcf6dbc970c2eb5d607e07dce5c8c66d4de07de6b2c35bd3bfa12cd4e4fcd4cdda3e0b2ff7575d406db96502f1ec4d9b5748215fe2a4018c7dbdb5ead1ddfc06da5233ce359e4f3924f2af1b80c7af9d13437e0107f7e240e485458a3a656e31a543bc5a80f6a598dcbbd87ff9cb4ce6842abadb72d62ae03e6d12b7f43ac1805e408d738148fa3a5c34deae7378d7d7309a7e09f5bac848f4c031693fbe3382a2de66a9d007420512e24b8a78a9489b30794c7cc51dd751c1cdeb2870b7c4a9b9606547f843c1a16d9be986b2f3e3d2f73f89c8e15da9de3dfdf7a667ced977332c5c62fd83d3c9a5e9718643e716db8f1b6fb58904ca17c24c9a4a191ec42be4c405fb00e443429582ff712dfdbadf1bb7e2d2abb0cc14f39c13a3b7013c62fd99488f043a6aec3ed7998b943ee24905fc915e3144c54393fa67ff34fdce2e48bba044f13ee1cd9c4f59a1a41f7fb7bdd8daee73461234f7cce5d54b078c3ad2b0aa37850ed4bb24f4d310d4ce75ede546ed6e73c0ee495ff8ce4b7256e8f43949539bf9df6e8d0fbf066fb506020c5a72e5e8b686641b78b365474c65fe9d8dd41133e27326fe82744b45b6ad1170433f3746647a68b824e94a213cda4c02f78465acbccdccb5bad1c70806d5a5c98c94338f88bf980b05b72cb82a4fd5b2a8586a5e5c5f2760115df595091809cfd12829f09622b53ca1d3809060aa7ab5d1f3640b3c2792a55c58fc3e80ac5d7f39ab5774a80fa1fc70461d396fa70dad1f90598244ce4197cb3ea42dce4afd61ca8ef93c8bc254d347872db21edcc3857bcb8dbe627508aa4856bd7d46e512db071905b3db100f425ba9f7181f0cce005cee2a95ffc190ddc1939e7049e58791b0e186433b0409f5a49e4e3262690a5160f8267c9099afdc58182236833fe7f825dfca34b08801345c1592bbab4964b34d7efa6c9d92e0106ede9a10fbf2a1be32f61f914211c3caa8b4c14edec5f9c139ee14789fe7d6634ede9bf9789caa60f5bf30b092e65ff95d3b32cbdc3e5842e3b16b935d31a3a0963bb0fe60f41efb6590f24eaf5e84006b28b3c755203113237e43fa70a37a009f71da49ea3f8097914d6128ee2b18adac49b5111fd3d18db9fd61ef8a2202fac5cfce646ccbea7eaaa81df0f1b7243465de15a3900143f479852f0e40bfad434b96eea3941f527b0d31c3d8f43188b911140766b5d7146feb93bf4da1ec47023dcd8f89863e487ba25c3105a4e43c4ea90f479eb0f774f3aa044b817f7e69b5dd1b3954e7dcdb2a6c4d191d5b9178262449413310f3876dd93145716781cb077025deb37d23c35e6fdf867d5353d10303b9e60efa50e9ecd013cd3f5270fc0e117a19ffb63038c594190018bd9c1c18a799f548c08a3e6f768de0de344cff160689fed73aa7fc4edc26f77413145775745c25fc2c9da5b62e24eab9b21895cfcda6e6457ae9fdfa6c54b49b0d160dad0aa7f8cbd3820a3098", + domain: "ogads-pa.clients6.google.com", + wantErr: false, + needsMoreData: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pkt, err := hex.DecodeString(tt.hexData) + if err != nil { + t.Fatalf("failed to decode hex string: %v", err) + } + quicHdr, err := quic.SniffQUIC(pkt) + if (err != nil) != tt.wantErr { + t.Errorf("SniffQUIC() error = %v, wantErr %v", err, tt.wantErr) + return + } + if (errors.Is(err, protocol.ErrProtoNeedMoreData)) != tt.needsMoreData { + t.Errorf("SniffQUIC() error = %v, expectsNoClue %v", err, tt.needsMoreData) + return + } + if err == nil && quicHdr.Domain() != tt.domain { + t.Errorf("SniffQUIC() domain = %v, want %v", quicHdr.Domain(), tt.domain) + } + }) + } +} + +func TestSniffQUICIncompleteServerName(t *testing.T) { + // 2 packets + pkts, err := hex.DecodeString("ca0000000108d799f48baf472fc3004047005411dcdaa24bd4c3cdbef653940a1ea0a671bb8c23222a8502bfcbd0a335af016df330b1190e4b1efd420aa82c4414a7dfd7b1aa63aa7bd0269ec8ed0d24895a467b737fcc514488a8142241b81c4952fb32e9f123f3f8d1be9eecc1340d7fa747c6f4838810209d51ad453fcec084ffaa382f9fed010a0585f8ce73c36dc6c628df940f72fb5b8e3e06f4612128d3b8f87725770d81305489a3a23cd443fcc668fece0ec9a909c82cfdb66819ce031422c3edd49ea72b6216efb940b8c186e2510b0f93d4cf3b6434ed9f5817c5a5a19fa861d24883bd48ba296df81e887fcb9ef18013ab1070df268049f3d35e8ddfffd2b100e4eb687176a02e7975085caa138e9df7cb6f9862356faafba3300f586df09af3b4e3eec15b6d1f14b34fe4eeb8e762ff482f3b7b0595c16d3eb20cf9c892b3f7c6449e51ffa33f3dc877d32e572e10ba4d5a320117110b150bec4a9434103de963aa2de1b22eaa992b59a8583c7a2d4e5743ef98a1b1f5f4aaeb2b49dbe441c331e7cb028e70432ce0890f78d40671f13d5feccc4ab264ae903affaa951897128d598ed8e8542294aa03abb0a2a7ec970ab874387b8777827c4847bb29ff93d0427c1fcc581407465611e4400bfeab39de4c4f65572365156421503706d66138317e514214db27303ad8b5b17eb443e959faad1ccb3d3def5f3843b241dd9a60378cd907a02cc5c6e8527715c7734079cf9143586ed471587690816133fa8f09b8421f2de6ff1f5cecc134ac404e53a367b57fd4f3c85fe7e121e7094fbf93c06d427cec30cd519ee43a6018a8bb8717c05d7660b379101a182bfd3018d485d5b46e81d39445ccd4d8baefdd7590e664f2289f11bbad3d35ea6d70e157aaa7f9e91a692794b85c5b86db33863151468d32792ecfdb2aec52784ded5aa431fa9a7337ddd9e95313a897fe5ca6ef3c7e0cf3d41f7f104506f8695f2ba307715022e5e98472d6ff24125004d14db2c7a4cc71ca39a9874bfb93b64dd54ef451097059bbdfa96aca6c6c01e1f000b8d35bdead1502874e5d80ac00a79593b6d7e2fbee406c2212b4abd7b7e9f0037518de232cd6559443f4e3f0f03c17cef616a74992f65754bb6699b08c0eb2ec59508086e496e070c6239a73f11a9d14a727b10188ca97ebf04851fa475ed1774836a94cfb6543c33888dfc250100e9992b0cca8bd27ae8552541f1b3d81546f15b740e4f07b41af864769fe17290eb7076c0c1f938ce56a45ead6deb6c89164f829543414a1b8cd2361c3d9b22cf85d4ad0d4221dcc87533506e32c1cfdc8240636b669f97f7c6e150ea3753fcb63a7b9eedb5615b257651e090b567a20fdc5c167ad58fa940985572db004db2b14709a6663c1fb79dc045e45cb7228f11eab4c60a48fffc702cd74dcf5aabded8355a5ae1a5e6325a3afff608e53af3aa944437139c438e6200c75bb27c8a8ce7ce9f9ec9723c4423e9e49c807b12484783ed79fc501b544f03e4b29bdf0c74eb055dbac9b78fe87a66efc819f9d2ea637dd478b920b7196232b970ed092050ba0621329aa2b3e26184176137a9c6283e4cc2c03c07e28d2da7b0485da51f131686f4dd6b23735c66316142f48781885c35c9940054a4b9187a20914bedd1c48e94af358994da5c6060dd224b0ba84ddd5e95f395fe941d464e2cccd857a180de756d32407540c3611b649c7a71cbf5a492bf5c8d4e112a743498ce83ea3e94bcaa6c00000000108d799f48baf472fc3004047005411dcdaa24bd4c3cdbef653940a1ea0a671bb8c23222a8502bfcbd0a335af016df330b1190e4b1efd420aa82c4414a7dfd7b1aa63aa7bd0269ec8ed0d24895a467b737fcc514488e7151df796764293639665547941eaaa7d83dfc9d42952583a821611921cba9e0276f9244f197e011b6c2d898255802c81c8792dd68d03cd8982ee6146c8734814e9640e0999312f97e898db60642ceca21700be9e698b5e577062148d1ac840e737f7bc4f18f9ffd13e656fca1517c96d0e0c607b67d7e75795f62131255b44c2178d896192348d5f053060fa21c855236401e853e435fbc71a428c8e126cd8393cb860193fd6304d9576e8a8ce7f6959739afa7d7ffaf86516afb061ade1eef603e8e5c66f002cacbef25546e84b59be707c547837f7b49471a59325ad58a4f189a525a3a354a4be5ffd3db34ce0585ec470f78f0ff6eb807989d5998e2fccc80bd651de654da3581b6c8a858b56c4df47c3041c50d74f0b3a3a5a325882cd356d0c46d6db74559cef934a1655c65a06daff218244f5f4e95b96794e45d371a03afee2c89768fe3d7bc134844f45440620a88d6ddb5d07b7f6c9b2adfbf5743c5c586520504febf14ee4b216f8186ee1bf9802ccebd4dac519bd337a554940a3873cd21ef32f3d4d4fdfd3eaabb172b5314490b3587a9ca2dc12637fa80dcd526859bb83d54f67dedeb173470e052beec9edba4b41444d31253accb3381237179a5381d437fc4d47c0a82f1c9da8c76ad3eee9417bf69acb8d530ac824e65d796b84bee40b14daa9e80c11e466f93233e1768ecc6c0237e71b901f1faa093bbd9e5324b2a160d02f09d4590392279a15c73edc4d1d7501ebad992fc8f1c9dce96c190879225dbbb09d20532f9dcbb845e3484c87cb8c3b06fe892b07bdc60926e3d4b5c9024acde5abed0a72686d546da535a2122cfd6d6488a491dc9612b90f3f6709835e83f59fd612c2259c3ac944fdae87ffd744176a92fbea1efd037565a39d5bc38991cfe6617135b43cd6c6484a3b228e10d3f8184227a7ee63121be20f099c4264461d52aecf4b8ee1a4d0ec41717f9a68e1394544cf3be0a3a171ece35c879b67afdaac49c12731e6dc5ccaa49fa72610901036a7f0752951077cd9f1d77eb6e4f12c16dab08e1a9655fda3ba0e80c34c767f40e61491f706ab9d1b50f48273ca4e37708d075909a3e2c4b173510114d8377b5300994f9a4f794db2781b98c2e9d0056fdbe87b249beb3e7301c86efe6b32242b4c7fabef280a1ca7bdd6abb38624cbebb01b6b75b07d0cd9090b2c296d0e330b9008788dd6f7e1af1dd63b02408f1a67b57be3316152270bf90c36e9f74322c9caba5adab3309e5bd7c44d3c2ac77329e167dafda7395e37313723fb550a6ab96729a9fa8f9e4a3e5957bffa3302617d48fd38196635309928c491ca974a112768de16043fc618001780b49a0bac2ea9fc2607ebd1f25b8ef65a6cab42d16c9184e23af496ac5cfab274d9557006d9c52bb56cd8c019ddf085f260a0582ef465c73f97fedc02271dda53eec20e6aa50503357ed1847f7d976899cb24e5dd42dd87ae48db3c9335b38957fc0f86a6ceee590b1cbee41fc6ace3189886bd86f9b082e66fb263f1a98dd8e564bff9ec412ae79e29fd519ac96e9c5b446b436b78a6ca555c496ca5c5e73c2c1c5837b13f7a953d57738440d4ed60efd4701bbbbcad79e1697b5724695f52dab8bce02e7f") + common.Must(err) + // The first packet ServerName is incomplete + pkt1 := make([]byte, 1250) + copy(pkt1, pkts) + quicHdr, err := quic.SniffQUIC(pkt1) + if !errors.Is(err, protocol.ErrProtoNeedMoreData) { + t.Error("failed") + } else { + quicHdr, err = quic.SniffQUIC(pkts) + } + if err != nil || quicHdr.Domain() != "play.google.com" { + t.Error("failed") + } +} + +func TestSniffQUICPacketNumberLength4(t *testing.T) { + // packetNumberLength = 4 + pkt, err := hex.DecodeString("c60000000108fa6cc47e912693590000449e5ea65fc129945b27f59f0b18f737a53e44f322681ecec36beeb943293db0ed751bd86413a840f9b9e9be718f3e2dba1100a8bab3a21e1541580ffd98e79aa89fa723e8d3a46f7876504c82b9ef3a081b0f8cb551370d9801ee86da77f09eec5aa19b7bf580a80ded3c9c83378285177115fd30b350c2a596ae265b3b538a81c183c0cfd13eabecfbbeb38416a5b19259731b838842c0eb33e646b9bb1f672043e90de33c3442151ee8db7d9cd66238f769f4486432ac28785a5083c616539f8320321060f64f9a0dc6af718754d645892397ff32956c4c1c97d0d9e44cdfa8d1a0ad90c3bbb7810b2196d638fd772a172a9510ea12ef12fe4050c5678851be26ec6ea6ab11824cc86ce071d110f72816166c01622c0207e9d97f8867ec7c63149e974c5a81db9cb5e0061cff2713538daa1c9ad1382ab7d883ecc85158dc76587793b258b4b0aded3f4c12b515a9183ee419b304cf748fc321f15b3f80cc53da1b889d1ac06b996d35e8d01306885851ad253083f37d0d588c9f619da25f6eb8360b846bc26913af616e2c3eabce9dbb61f7dc96b6dcb79e19905ac9ba8f3938d03f8a3647403dc919f37bb585a0d67b7ce955547d15c82eed6d94b04dfa009eaf8b30448e1450043c48845acb4fefcf29bdd55ec14e395d0e8cd8400ffdda5a58747c6e8a66d0dc5fb25f3615081b2b546e004079573e99290f5daf72705ca495707468dd26d94c7f04d7e6f89d8148ecbab67c8c0062984e0ba02539527370a2a157a58eab342ad671641812ed35b4a52ba07d244d9b5d64e29f012133d21fa4afa31645c21f6d836ad937bb75f7177768b5f94bb77e95aaf9a85f8fb7e599d482724f694cf5d7d3f61bfca892794bdcb3de7a5f321db8120560bc32b8839c0a5994917c151cc6bd4c1614c5f117e637c19dab7cff28c4848c3b328eb97e49cefded2d44a824f2705807770c2ef9dd07f0fe0198659ad062e1889e280e5f3d1c52a92ef27d4565ebae9b9a18a803b70f38e5db237ed99583d8952c79492e35e1f1c41664f1f45566126a7ec44a90004b015b893aa805fcd772737fb8dbbe7af56b9eac288ef6cab7cbb6f7a3c0b29a43bc84b6280def0f7d727b3238cba3eda2e2d110de87ad0f10e25a60783cdb0c05116df5359b19b40007812b898d03dc1d697690761856d785b83ac95778db69c3df7a8f0e092ee6ed2c9c189ebe40734b02cee2d02599e931d4cc560d38a7ec355b9f339b932613ee18f8162e8d3cb81301bfc6d726b7c26ec96d5edcaf0de171563482ed2f2de3001dbca2aee1029fdaece4340fd2d5ae8333819d5ece7c9d3f77f99a81fccd1fbcc3ea585c1538e0363141e0fd20338a193695377987afacd0baf1f0dce11b4bbf29965109bddb508e8a0974c08906d4ea340d51af376c3dda55bf97e3ba5d688533980e12704679bb18d0ef4ddd5b3af1b7676528c4bf4a84c84d40892715c0a8808ad51d6ebcc6469da708d6953e9ddcfbd19bae90e9b078f1b6641401b979304b0ef52b1441e1797ed366bb0519ca8bf9c6eba72518647d0a1400ca66a20fdd8e3ff06ee52c199bd8f941b1722a0bbf8c15447452788ba81a68431f735d99e8a80691ef64d1bf470350c8878aed3e2421223d0ba6a3d84928c8e6db0972263df9da49b8f5") + common.Must(err) + quicHdr, err := quic.SniffQUIC(pkt) + if err != nil || quicHdr.Domain() != "www.google.com" { + t.Error("failed") + } +} + +func TestSniffFakeQUICPacketWithInvalidPacketNumberLength(t *testing.T) { + pkt, err := hex.DecodeString("cb00000001081c8c6d5aeb53d54400000090709b8600000000000000000000000000000000") + common.Must(err) + _, err = quic.SniffQUIC(pkt) + if err == nil { + t.Error("failed") + } +} + +func TestSniffFakeQUICPacketWithTooShortData(t *testing.T) { + pkt, err := hex.DecodeString("cb00000001081c8c6d5aeb53d54400000090709b86") + common.Must(err) + _, err = quic.SniffQUIC(pkt) + if err == nil { + t.Error("failed") + } +} diff --git a/subproject/Xray-core-main/common/protocol/server_spec.go b/subproject/Xray-core-main/common/protocol/server_spec.go new file mode 100644 index 00000000..d3ab0089 --- /dev/null +++ b/subproject/Xray-core-main/common/protocol/server_spec.go @@ -0,0 +1,30 @@ +package protocol + +import ( + "github.com/xtls/xray-core/common/net" +) + +type ServerSpec struct { + Destination net.Destination + User *MemoryUser +} + +func NewServerSpec(dest net.Destination, user *MemoryUser) *ServerSpec { + return &ServerSpec{ + Destination: dest, + User: user, + } +} + +func NewServerSpecFromPB(spec *ServerEndpoint) (*ServerSpec, error) { + dest := net.TCPDestination(spec.Address.AsAddress(), net.Port(spec.Port)) + var dUser *MemoryUser + if spec.User != nil { + user, err := spec.User.ToMemoryUser() + if err != nil { + return nil, err + } + dUser = user + } + return NewServerSpec(dest, dUser), nil +} diff --git a/subproject/Xray-core-main/common/protocol/server_spec.pb.go b/subproject/Xray-core-main/common/protocol/server_spec.pb.go new file mode 100644 index 00000000..543ac106 --- /dev/null +++ b/subproject/Xray-core-main/common/protocol/server_spec.pb.go @@ -0,0 +1,147 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: common/protocol/server_spec.proto + +package protocol + +import ( + net "github.com/xtls/xray-core/common/net" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type ServerEndpoint struct { + state protoimpl.MessageState `protogen:"open.v1"` + Address *net.IPOrDomain `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"` + Port uint32 `protobuf:"varint,2,opt,name=port,proto3" json:"port,omitempty"` + User *User `protobuf:"bytes,3,opt,name=user,proto3" json:"user,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ServerEndpoint) Reset() { + *x = ServerEndpoint{} + mi := &file_common_protocol_server_spec_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ServerEndpoint) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ServerEndpoint) ProtoMessage() {} + +func (x *ServerEndpoint) ProtoReflect() protoreflect.Message { + mi := &file_common_protocol_server_spec_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ServerEndpoint.ProtoReflect.Descriptor instead. +func (*ServerEndpoint) Descriptor() ([]byte, []int) { + return file_common_protocol_server_spec_proto_rawDescGZIP(), []int{0} +} + +func (x *ServerEndpoint) GetAddress() *net.IPOrDomain { + if x != nil { + return x.Address + } + return nil +} + +func (x *ServerEndpoint) GetPort() uint32 { + if x != nil { + return x.Port + } + return 0 +} + +func (x *ServerEndpoint) GetUser() *User { + if x != nil { + return x.User + } + return nil +} + +var File_common_protocol_server_spec_proto protoreflect.FileDescriptor + +const file_common_protocol_server_spec_proto_rawDesc = "" + + "\n" + + "!common/protocol/server_spec.proto\x12\x14xray.common.protocol\x1a\x18common/net/address.proto\x1a\x1acommon/protocol/user.proto\"\x8b\x01\n" + + "\x0eServerEndpoint\x125\n" + + "\aaddress\x18\x01 \x01(\v2\x1b.xray.common.net.IPOrDomainR\aaddress\x12\x12\n" + + "\x04port\x18\x02 \x01(\rR\x04port\x12.\n" + + "\x04user\x18\x03 \x01(\v2\x1a.xray.common.protocol.UserR\x04userB^\n" + + "\x18com.xray.common.protocolP\x01Z)github.com/xtls/xray-core/common/protocol\xaa\x02\x14Xray.Common.Protocolb\x06proto3" + +var ( + file_common_protocol_server_spec_proto_rawDescOnce sync.Once + file_common_protocol_server_spec_proto_rawDescData []byte +) + +func file_common_protocol_server_spec_proto_rawDescGZIP() []byte { + file_common_protocol_server_spec_proto_rawDescOnce.Do(func() { + file_common_protocol_server_spec_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_common_protocol_server_spec_proto_rawDesc), len(file_common_protocol_server_spec_proto_rawDesc))) + }) + return file_common_protocol_server_spec_proto_rawDescData +} + +var file_common_protocol_server_spec_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_common_protocol_server_spec_proto_goTypes = []any{ + (*ServerEndpoint)(nil), // 0: xray.common.protocol.ServerEndpoint + (*net.IPOrDomain)(nil), // 1: xray.common.net.IPOrDomain + (*User)(nil), // 2: xray.common.protocol.User +} +var file_common_protocol_server_spec_proto_depIdxs = []int32{ + 1, // 0: xray.common.protocol.ServerEndpoint.address:type_name -> xray.common.net.IPOrDomain + 2, // 1: xray.common.protocol.ServerEndpoint.user:type_name -> xray.common.protocol.User + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_common_protocol_server_spec_proto_init() } +func file_common_protocol_server_spec_proto_init() { + if File_common_protocol_server_spec_proto != nil { + return + } + file_common_protocol_user_proto_init() + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_common_protocol_server_spec_proto_rawDesc), len(file_common_protocol_server_spec_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_common_protocol_server_spec_proto_goTypes, + DependencyIndexes: file_common_protocol_server_spec_proto_depIdxs, + MessageInfos: file_common_protocol_server_spec_proto_msgTypes, + }.Build() + File_common_protocol_server_spec_proto = out.File + file_common_protocol_server_spec_proto_goTypes = nil + file_common_protocol_server_spec_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/common/protocol/server_spec.proto b/subproject/Xray-core-main/common/protocol/server_spec.proto new file mode 100644 index 00000000..79c6e58e --- /dev/null +++ b/subproject/Xray-core-main/common/protocol/server_spec.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +package xray.common.protocol; +option csharp_namespace = "Xray.Common.Protocol"; +option go_package = "github.com/xtls/xray-core/common/protocol"; +option java_package = "com.xray.common.protocol"; +option java_multiple_files = true; + +import "common/net/address.proto"; +import "common/protocol/user.proto"; + +message ServerEndpoint { + xray.common.net.IPOrDomain address = 1; + uint32 port = 2; + xray.common.protocol.User user = 3; +} diff --git a/subproject/Xray-core-main/common/protocol/time.go b/subproject/Xray-core-main/common/protocol/time.go new file mode 100644 index 00000000..195496ed --- /dev/null +++ b/subproject/Xray-core-main/common/protocol/time.go @@ -0,0 +1,22 @@ +package protocol + +import ( + "time" + + "github.com/xtls/xray-core/common/dice" +) + +type Timestamp int64 + +type TimestampGenerator func() Timestamp + +func NowTime() Timestamp { + return Timestamp(time.Now().Unix()) +} + +func NewTimestampGenerator(base Timestamp, delta int) TimestampGenerator { + return func() Timestamp { + rangeInDelta := dice.Roll(delta*2) - delta + return base + Timestamp(rangeInDelta) + } +} diff --git a/subproject/Xray-core-main/common/protocol/time_test.go b/subproject/Xray-core-main/common/protocol/time_test.go new file mode 100644 index 00000000..9031b414 --- /dev/null +++ b/subproject/Xray-core-main/common/protocol/time_test.go @@ -0,0 +1,21 @@ +package protocol_test + +import ( + "testing" + "time" + + . "github.com/xtls/xray-core/common/protocol" +) + +func TestGenerateRandomInt64InRange(t *testing.T) { + base := time.Now().Unix() + delta := 100 + generator := NewTimestampGenerator(Timestamp(base), delta) + + for i := 0; i < 100; i++ { + val := int64(generator()) + if val > base+int64(delta) || val < base-int64(delta) { + t.Error(val, " not between ", base-int64(delta), " and ", base+int64(delta)) + } + } +} diff --git a/subproject/Xray-core-main/common/protocol/tls/cert/cert.go b/subproject/Xray-core-main/common/protocol/tls/cert/cert.go new file mode 100644 index 00000000..0a581e07 --- /dev/null +++ b/subproject/Xray-core-main/common/protocol/tls/cert/cert.go @@ -0,0 +1,185 @@ +package cert + +import ( + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "encoding/asn1" + "encoding/pem" + "math/big" + "time" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/errors" +) + +type Certificate struct { + // certificate in ASN.1 DER format + Certificate []byte + // Private key in ASN.1 DER format + PrivateKey []byte +} + +func ParseCertificate(certPEM []byte, keyPEM []byte) (*Certificate, error) { + certBlock, _ := pem.Decode(certPEM) + if certBlock == nil { + return nil, errors.New("failed to decode certificate") + } + keyBlock, _ := pem.Decode(keyPEM) + if keyBlock == nil { + return nil, errors.New("failed to decode key") + } + return &Certificate{ + Certificate: certBlock.Bytes, + PrivateKey: keyBlock.Bytes, + }, nil +} + +func (c *Certificate) ToPEM() ([]byte, []byte) { + return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: c.Certificate}), + pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: c.PrivateKey}) +} + +type Option func(*x509.Certificate) + +func Authority(isCA bool) Option { + return func(cert *x509.Certificate) { + cert.IsCA = isCA + } +} + +func NotBefore(t time.Time) Option { + return func(c *x509.Certificate) { + c.NotBefore = t + } +} + +func NotAfter(t time.Time) Option { + return func(c *x509.Certificate) { + c.NotAfter = t + } +} + +func DNSNames(names ...string) Option { + return func(c *x509.Certificate) { + c.DNSNames = names + } +} + +func CommonName(name string) Option { + return func(c *x509.Certificate) { + c.Subject.CommonName = name + } +} + +func KeyUsage(usage x509.KeyUsage) Option { + return func(c *x509.Certificate) { + c.KeyUsage = usage + } +} + +func Organization(org string) Option { + return func(c *x509.Certificate) { + c.Subject.Organization = []string{org} + } +} + +func MustGenerate(parent *Certificate, opts ...Option) (*Certificate, [32]byte) { + cert, err := Generate(parent, opts...) + common.Must(err) + return cert, sha256.Sum256(cert.Certificate) +} + +func publicKey(priv interface{}) interface{} { + switch k := priv.(type) { + case *rsa.PrivateKey: + return &k.PublicKey + case *ecdsa.PrivateKey: + return &k.PublicKey + case ed25519.PrivateKey: + return k.Public().(ed25519.PublicKey) + default: + return nil + } +} + +func Generate(parent *Certificate, opts ...Option) (*Certificate, error) { + var ( + pKey interface{} + parentKey interface{} + err error + ) + // higher signing performance than RSA2048 + selfKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, errors.New("failed to generate self private key").Base(err) + } + parentKey = selfKey + if parent != nil { + if _, e := asn1.Unmarshal(parent.PrivateKey, &ecPrivateKey{}); e == nil { + pKey, err = x509.ParseECPrivateKey(parent.PrivateKey) + } else if _, e := asn1.Unmarshal(parent.PrivateKey, &pkcs8{}); e == nil { + pKey, err = x509.ParsePKCS8PrivateKey(parent.PrivateKey) + } else if _, e := asn1.Unmarshal(parent.PrivateKey, &pkcs1PrivateKey{}); e == nil { + pKey, err = x509.ParsePKCS1PrivateKey(parent.PrivateKey) + } + if err != nil { + return nil, errors.New("failed to parse parent private key").Base(err) + } + parentKey = pKey + } + + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + return nil, errors.New("failed to generate serial number").Base(err) + } + + template := &x509.Certificate{ + SerialNumber: serialNumber, + NotBefore: time.Now().Add(time.Hour * -1), + NotAfter: time.Now().Add(time.Hour), + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + } + + parentCert := template + if parent != nil { + pCert, err := x509.ParseCertificate(parent.Certificate) + if err != nil { + return nil, errors.New("failed to parse parent certificate").Base(err) + } + parentCert = pCert + } + + if parentCert.NotAfter.Before(template.NotAfter) { + template.NotAfter = parentCert.NotAfter + } + if parentCert.NotBefore.After(template.NotBefore) { + template.NotBefore = parentCert.NotBefore + } + + for _, opt := range opts { + opt(template) + } + + derBytes, err := x509.CreateCertificate(rand.Reader, template, parentCert, publicKey(selfKey), parentKey) + if err != nil { + return nil, errors.New("failed to create certificate").Base(err) + } + + privateKey, err := x509.MarshalPKCS8PrivateKey(selfKey) + if err != nil { + return nil, errors.New("Unable to marshal private key").Base(err) + } + + return &Certificate{ + Certificate: derBytes, + PrivateKey: privateKey, + }, nil +} diff --git a/subproject/Xray-core-main/common/protocol/tls/cert/cert_test.go b/subproject/Xray-core-main/common/protocol/tls/cert/cert_test.go new file mode 100644 index 00000000..4245f3d6 --- /dev/null +++ b/subproject/Xray-core-main/common/protocol/tls/cert/cert_test.go @@ -0,0 +1,95 @@ +package cert + +import ( + "context" + "crypto/x509" + "encoding/json" + "os" + "strings" + "testing" + "time" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/task" +) + +func TestGenerate(t *testing.T) { + err := generate(nil, true, true, "ca") + if err != nil { + t.Fatal(err) + } +} + +func generate(domainNames []string, isCA bool, jsonOutput bool, fileOutput string) error { + commonName := "Xray Root CA" + organization := "Xray Inc" + + expire := time.Hour * 3 + + var opts []Option + if isCA { + opts = append(opts, Authority(isCA)) + opts = append(opts, KeyUsage(x509.KeyUsageCertSign|x509.KeyUsageKeyEncipherment|x509.KeyUsageDigitalSignature)) + } + + opts = append(opts, NotAfter(time.Now().Add(expire))) + opts = append(opts, CommonName(commonName)) + if len(domainNames) > 0 { + opts = append(opts, DNSNames(domainNames...)) + } + opts = append(opts, Organization(organization)) + + cert, err := Generate(nil, opts...) + if err != nil { + return errors.New("failed to generate TLS certificate").Base(err) + } + + if jsonOutput { + printJSON(cert) + } + + if len(fileOutput) > 0 { + if err := printFile(cert, fileOutput); err != nil { + return err + } + } + + return nil +} + +type jsonCert struct { + Certificate []string `json:"certificate"` + Key []string `json:"key"` +} + +func printJSON(certificate *Certificate) { + certPEM, keyPEM := certificate.ToPEM() + jCert := &jsonCert{ + Certificate: strings.Split(strings.TrimSpace(string(certPEM)), "\n"), + Key: strings.Split(strings.TrimSpace(string(keyPEM)), "\n"), + } + content, err := json.MarshalIndent(jCert, "", " ") + common.Must(err) + os.Stdout.Write(content) + os.Stdout.WriteString("\n") +} + +func printFile(certificate *Certificate, name string) error { + certPEM, keyPEM := certificate.ToPEM() + return task.Run(context.Background(), func() error { + return writeFile(certPEM, name+".crt") + }, func() error { + return writeFile(keyPEM, name+".key") + }) +} + +func writeFile(content []byte, name string) error { + f, err := os.Create(name) + if err != nil { + return err + } + defer f.Close() + + return common.Error2(f.Write(content)) +} diff --git a/subproject/Xray-core-main/common/protocol/tls/cert/privateKey.go b/subproject/Xray-core-main/common/protocol/tls/cert/privateKey.go new file mode 100644 index 00000000..e5e8bd0b --- /dev/null +++ b/subproject/Xray-core-main/common/protocol/tls/cert/privateKey.go @@ -0,0 +1,44 @@ +package cert + +import ( + "crypto/x509/pkix" + "encoding/asn1" + "math/big" +) + +type ecPrivateKey struct { + Version int + PrivateKey []byte + NamedCurveOID asn1.ObjectIdentifier `asn1:"optional,explicit,tag:0"` + PublicKey asn1.BitString `asn1:"optional,explicit,tag:1"` +} + +type pkcs8 struct { + Version int + Algo pkix.AlgorithmIdentifier + PrivateKey []byte + // Optional attributes omitted. +} + +type pkcs1AdditionalRSAPrime struct { + Prime *big.Int + + // We ignore these values because rsa will calculate them. + Exp *big.Int + Coeff *big.Int +} + +type pkcs1PrivateKey struct { + Version int + N *big.Int + E int + D *big.Int + P *big.Int + Q *big.Int + // We ignore these values, if present, because rsa will calculate them. + Dp *big.Int `asn1:"optional"` + Dq *big.Int `asn1:"optional"` + Qinv *big.Int `asn1:"optional"` + + AdditionalPrimes []pkcs1AdditionalRSAPrime `asn1:"optional,omitempty"` +} diff --git a/subproject/Xray-core-main/common/protocol/tls/sniff.go b/subproject/Xray-core-main/common/protocol/tls/sniff.go new file mode 100644 index 00000000..11660b92 --- /dev/null +++ b/subproject/Xray-core-main/common/protocol/tls/sniff.go @@ -0,0 +1,153 @@ +package tls + +import ( + "encoding/binary" + "errors" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/protocol" +) + +type SniffHeader struct { + domain string +} + +func (h *SniffHeader) Protocol() string { + return "tls" +} + +func (h *SniffHeader) Domain() string { + return h.domain +} + +var ( + errNotTLS = errors.New("not TLS header") + errNotClientHello = errors.New("not client hello") +) + +func IsValidTLSVersion(major, minor byte) bool { + return major == 3 +} + +// ReadClientHello returns server name (if any) from TLS client hello message. +// https://github.com/golang/go/blob/master/src/crypto/tls/handshake_messages.go#L300 +func ReadClientHello(data []byte, h *SniffHeader) error { + if len(data) < 42 { + return common.ErrNoClue + } + sessionIDLen := int(data[38]) + if sessionIDLen > 32 || len(data) < 39+sessionIDLen { + return common.ErrNoClue + } + data = data[39+sessionIDLen:] + if len(data) < 2 { + return common.ErrNoClue + } + // cipherSuiteLen is the number of bytes of cipher suite numbers. Since + // they are uint16s, the number must be even. + cipherSuiteLen := int(data[0])<<8 | int(data[1]) + if cipherSuiteLen%2 == 1 || len(data) < 2+cipherSuiteLen { + return errNotClientHello + } + data = data[2+cipherSuiteLen:] + if len(data) < 1 { + return common.ErrNoClue + } + compressionMethodsLen := int(data[0]) + if len(data) < 1+compressionMethodsLen { + return common.ErrNoClue + } + data = data[1+compressionMethodsLen:] + + if len(data) < 2 { + return errNotClientHello + } + + extensionsLength := int(data[0])<<8 | int(data[1]) + data = data[2:] + if extensionsLength != len(data) { + return errNotClientHello + } + + for len(data) != 0 { + if len(data) < 4 { + return errNotClientHello + } + extension := uint16(data[0])<<8 | uint16(data[1]) + length := int(data[2])<<8 | int(data[3]) + data = data[4:] + if len(data) < length { + return errNotClientHello + } + + if extension == 0x00 { /* extensionServerName */ + d := data[:length] + if len(d) < 2 { + return errNotClientHello + } + namesLen := int(d[0])<<8 | int(d[1]) + d = d[2:] + if len(d) != namesLen { + return errNotClientHello + } + for len(d) > 0 { + if len(d) < 3 { + return errNotClientHello + } + nameType := d[0] + nameLen := int(d[1])<<8 | int(d[2]) + d = d[3:] + if len(d) < nameLen { + return errNotClientHello + } + if nameType == 0 { + // QUIC separated across packets + // 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 + // trailing dot. See + // https://tools.ietf.org/html/rfc6066#section-3. + if b == '.' { + return errNotClientHello + } + serverName := string(d[:nameLen]) + h.domain = serverName + return nil + } + d = d[nameLen:] + } + } + data = data[length:] + } + + return errNotTLS +} + +func SniffTLS(b []byte) (*SniffHeader, error) { + if len(b) < 5 { + return nil, common.ErrNoClue + } + + if b[0] != 0x16 /* TLS Handshake */ { + return nil, errNotTLS + } + if !IsValidTLSVersion(b[1], b[2]) { + return nil, errNotTLS + } + headerLen := int(binary.BigEndian.Uint16(b[3:5])) + if 5+headerLen > len(b) { + return nil, common.ErrNoClue + } + + h := &SniffHeader{} + err := ReadClientHello(b[5:5+headerLen], h) + if err == nil { + return h, nil + } + return nil, err +} diff --git a/subproject/Xray-core-main/common/protocol/tls/sniff_test.go b/subproject/Xray-core-main/common/protocol/tls/sniff_test.go new file mode 100644 index 00000000..59f3a35f --- /dev/null +++ b/subproject/Xray-core-main/common/protocol/tls/sniff_test.go @@ -0,0 +1,161 @@ +package tls_test + +import ( + "testing" + + . "github.com/xtls/xray-core/common/protocol/tls" +) + +func TestTLSHeaders(t *testing.T) { + cases := []struct { + input []byte + domain string + err bool + }{ + { + input: []byte{ + 0x16, 0x03, 0x01, 0x00, 0xc8, 0x01, 0x00, 0x00, + 0xc4, 0x03, 0x03, 0x1a, 0xac, 0xb2, 0xa8, 0xfe, + 0xb4, 0x96, 0x04, 0x5b, 0xca, 0xf7, 0xc1, 0xf4, + 0x2e, 0x53, 0x24, 0x6e, 0x34, 0x0c, 0x58, 0x36, + 0x71, 0x97, 0x59, 0xe9, 0x41, 0x66, 0xe2, 0x43, + 0xa0, 0x13, 0xb6, 0x00, 0x00, 0x20, 0x1a, 0x1a, + 0xc0, 0x2b, 0xc0, 0x2f, 0xc0, 0x2c, 0xc0, 0x30, + 0xcc, 0xa9, 0xcc, 0xa8, 0xcc, 0x14, 0xcc, 0x13, + 0xc0, 0x13, 0xc0, 0x14, 0x00, 0x9c, 0x00, 0x9d, + 0x00, 0x2f, 0x00, 0x35, 0x00, 0x0a, 0x01, 0x00, + 0x00, 0x7b, 0xba, 0xba, 0x00, 0x00, 0xff, 0x01, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x16, 0x00, + 0x14, 0x00, 0x00, 0x11, 0x63, 0x2e, 0x73, 0x2d, + 0x6d, 0x69, 0x63, 0x72, 0x6f, 0x73, 0x6f, 0x66, + 0x74, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x17, 0x00, + 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x0d, 0x00, + 0x14, 0x00, 0x12, 0x04, 0x03, 0x08, 0x04, 0x04, + 0x01, 0x05, 0x03, 0x08, 0x05, 0x05, 0x01, 0x08, + 0x06, 0x06, 0x01, 0x02, 0x01, 0x00, 0x05, 0x00, + 0x05, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, + 0x00, 0x00, 0x00, 0x10, 0x00, 0x0e, 0x00, 0x0c, + 0x02, 0x68, 0x32, 0x08, 0x68, 0x74, 0x74, 0x70, + 0x2f, 0x31, 0x2e, 0x31, 0x00, 0x0b, 0x00, 0x02, + 0x01, 0x00, 0x00, 0x0a, 0x00, 0x0a, 0x00, 0x08, + 0xaa, 0xaa, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x18, + 0xaa, 0xaa, 0x00, 0x01, 0x00, + }, + domain: "c.s-microsoft.com", + err: false, + }, + { + input: []byte{ + 0x16, 0x03, 0x01, 0x00, 0xee, 0x01, 0x00, 0x00, + 0xea, 0x03, 0x03, 0xe7, 0x91, 0x9e, 0x93, 0xca, + 0x78, 0x1b, 0x3c, 0xe0, 0x65, 0x25, 0x58, 0xb5, + 0x93, 0xe1, 0x0f, 0x85, 0xec, 0x9a, 0x66, 0x8e, + 0x61, 0x82, 0x88, 0xc8, 0xfc, 0xae, 0x1e, 0xca, + 0xd7, 0xa5, 0x63, 0x20, 0xbd, 0x1c, 0x00, 0x00, + 0x8b, 0xee, 0x09, 0xe3, 0x47, 0x6a, 0x0e, 0x74, + 0xb0, 0xbc, 0xa3, 0x02, 0xa7, 0x35, 0xe8, 0x85, + 0x70, 0x7c, 0x7a, 0xf0, 0x00, 0xdf, 0x4a, 0xea, + 0x87, 0x01, 0x14, 0x91, 0x00, 0x20, 0xea, 0xea, + 0xc0, 0x2b, 0xc0, 0x2f, 0xc0, 0x2c, 0xc0, 0x30, + 0xcc, 0xa9, 0xcc, 0xa8, 0xcc, 0x14, 0xcc, 0x13, + 0xc0, 0x13, 0xc0, 0x14, 0x00, 0x9c, 0x00, 0x9d, + 0x00, 0x2f, 0x00, 0x35, 0x00, 0x0a, 0x01, 0x00, + 0x00, 0x81, 0x9a, 0x9a, 0x00, 0x00, 0xff, 0x01, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, + 0x16, 0x00, 0x00, 0x13, 0x77, 0x77, 0x77, 0x30, + 0x37, 0x2e, 0x63, 0x6c, 0x69, 0x63, 0x6b, 0x74, + 0x61, 0x6c, 0x65, 0x2e, 0x6e, 0x65, 0x74, 0x00, + 0x17, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, + 0x0d, 0x00, 0x14, 0x00, 0x12, 0x04, 0x03, 0x08, + 0x04, 0x04, 0x01, 0x05, 0x03, 0x08, 0x05, 0x05, + 0x01, 0x08, 0x06, 0x06, 0x01, 0x02, 0x01, 0x00, + 0x05, 0x00, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x12, 0x00, 0x00, 0x00, 0x10, 0x00, 0x0e, + 0x00, 0x0c, 0x02, 0x68, 0x32, 0x08, 0x68, 0x74, + 0x74, 0x70, 0x2f, 0x31, 0x2e, 0x31, 0x75, 0x50, + 0x00, 0x00, 0x00, 0x0b, 0x00, 0x02, 0x01, 0x00, + 0x00, 0x0a, 0x00, 0x0a, 0x00, 0x08, 0x9a, 0x9a, + 0x00, 0x1d, 0x00, 0x17, 0x00, 0x18, 0x8a, 0x8a, + 0x00, 0x01, 0x00, + }, + domain: "www07.clicktale.net", + err: false, + }, + { + input: []byte{ + 0x16, 0x03, 0x01, 0x00, 0xe6, 0x01, 0x00, 0x00, 0xe2, 0x03, 0x03, 0x81, 0x47, 0xc1, + 0x66, 0xd5, 0x1b, 0xfa, 0x4b, 0xb5, 0xe0, 0x2a, 0xe1, 0xa7, 0x87, 0x13, 0x1d, 0x11, 0xaa, 0xc6, + 0xce, 0xfc, 0x7f, 0xab, 0x94, 0xc8, 0x62, 0xad, 0xc8, 0xab, 0x0c, 0xdd, 0xcb, 0x20, 0x6f, 0x9d, + 0x07, 0xf1, 0x95, 0x3e, 0x99, 0xd8, 0xf3, 0x6d, 0x97, 0xee, 0x19, 0x0b, 0x06, 0x1b, 0xf4, 0x84, + 0x0b, 0xb6, 0x8f, 0xcc, 0xde, 0xe2, 0xd0, 0x2d, 0x6b, 0x0c, 0x1f, 0x52, 0x53, 0x13, 0x00, 0x08, + 0x13, 0x02, 0x13, 0x03, 0x13, 0x01, 0x00, 0xff, 0x01, 0x00, 0x00, 0x91, 0x00, 0x00, 0x00, 0x0c, + 0x00, 0x0a, 0x00, 0x00, 0x07, 0x64, 0x6f, 0x67, 0x66, 0x69, 0x73, 0x68, 0x00, 0x0b, 0x00, 0x04, + 0x03, 0x00, 0x01, 0x02, 0x00, 0x0a, 0x00, 0x0c, 0x00, 0x0a, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x1e, + 0x00, 0x19, 0x00, 0x18, 0x00, 0x23, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, + 0x00, 0x0d, 0x00, 0x1e, 0x00, 0x1c, 0x04, 0x03, 0x05, 0x03, 0x06, 0x03, 0x08, 0x07, 0x08, 0x08, + 0x08, 0x09, 0x08, 0x0a, 0x08, 0x0b, 0x08, 0x04, 0x08, 0x05, 0x08, 0x06, 0x04, 0x01, 0x05, 0x01, + 0x06, 0x01, 0x00, 0x2b, 0x00, 0x07, 0x06, 0x7f, 0x1c, 0x7f, 0x1b, 0x7f, 0x1a, 0x00, 0x2d, 0x00, + 0x02, 0x01, 0x01, 0x00, 0x33, 0x00, 0x26, 0x00, 0x24, 0x00, 0x1d, 0x00, 0x20, 0x2f, 0x35, 0x0c, + 0xb6, 0x90, 0x0a, 0xb7, 0xd5, 0xc4, 0x1b, 0x2f, 0x60, 0xaa, 0x56, 0x7b, 0x3f, 0x71, 0xc8, 0x01, + 0x7e, 0x86, 0xd3, 0xb7, 0x0c, 0x29, 0x1a, 0x9e, 0x5b, 0x38, 0x3f, 0x01, 0x72, + }, + domain: "dogfish", + err: false, + }, + { + input: []byte{ + 0x16, 0x03, 0x01, 0x01, 0x03, 0x01, 0x00, 0x00, + 0xff, 0x03, 0x03, 0x3d, 0x89, 0x52, 0x9e, 0xee, + 0xbe, 0x17, 0x63, 0x75, 0xef, 0x29, 0xbd, 0x14, + 0x6a, 0x49, 0xe0, 0x2c, 0x37, 0x57, 0x71, 0x62, + 0x82, 0x44, 0x94, 0x8f, 0x6e, 0x94, 0x08, 0x45, + 0x7f, 0xdb, 0xc1, 0x00, 0x00, 0x3e, 0xc0, 0x2c, + 0xc0, 0x30, 0x00, 0x9f, 0xcc, 0xa9, 0xcc, 0xa8, + 0xcc, 0xaa, 0xc0, 0x2b, 0xc0, 0x2f, 0x00, 0x9e, + 0xc0, 0x24, 0xc0, 0x28, 0x00, 0x6b, 0xc0, 0x23, + 0xc0, 0x27, 0x00, 0x67, 0xc0, 0x0a, 0xc0, 0x14, + 0x00, 0x39, 0xc0, 0x09, 0xc0, 0x13, 0x00, 0x33, + 0x00, 0x9d, 0x00, 0x9c, 0x13, 0x02, 0x13, 0x03, + 0x13, 0x01, 0x00, 0x3d, 0x00, 0x3c, 0x00, 0x35, + 0x00, 0x2f, 0x00, 0xff, 0x01, 0x00, 0x00, 0x98, + 0x00, 0x00, 0x00, 0x10, 0x00, 0x0e, 0x00, 0x00, + 0x0b, 0x31, 0x30, 0x2e, 0x34, 0x32, 0x2e, 0x30, + 0x2e, 0x32, 0x34, 0x33, 0x00, 0x0b, 0x00, 0x04, + 0x03, 0x00, 0x01, 0x02, 0x00, 0x0a, 0x00, 0x0a, + 0x00, 0x08, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x19, + 0x00, 0x18, 0x00, 0x23, 0x00, 0x00, 0x00, 0x0d, + 0x00, 0x20, 0x00, 0x1e, 0x04, 0x03, 0x05, 0x03, + 0x06, 0x03, 0x08, 0x04, 0x08, 0x05, 0x08, 0x06, + 0x04, 0x01, 0x05, 0x01, 0x06, 0x01, 0x02, 0x03, + 0x02, 0x01, 0x02, 0x02, 0x04, 0x02, 0x05, 0x02, + 0x06, 0x02, 0x00, 0x16, 0x00, 0x00, 0x00, 0x17, + 0x00, 0x00, 0x00, 0x2b, 0x00, 0x09, 0x08, 0x7f, + 0x14, 0x03, 0x03, 0x03, 0x02, 0x03, 0x01, 0x00, + 0x2d, 0x00, 0x03, 0x02, 0x01, 0x00, 0x00, 0x28, + 0x00, 0x26, 0x00, 0x24, 0x00, 0x1d, 0x00, 0x20, + 0x13, 0x7c, 0x6e, 0x97, 0xc4, 0xfd, 0x09, 0x2e, + 0x70, 0x2f, 0x73, 0x5a, 0x9b, 0x57, 0x4d, 0x5f, + 0x2b, 0x73, 0x2c, 0xa5, 0x4a, 0x98, 0x40, 0x3d, + 0x75, 0x6e, 0xb4, 0x76, 0xf9, 0x48, 0x8f, 0x36, + }, + domain: "10.42.0.243", + err: false, + }, + } + + for _, test := range cases { + header, err := SniffTLS(test.input) + if test.err { + if err == nil { + t.Errorf("Expect error but nil in test %v", test) + } + } else { + if err != nil { + t.Errorf("Expect no error but actually %s in test %v", err.Error(), test) + } + if header.Domain() != test.domain { + t.Error("expect domain ", test.domain, " but got ", header.Domain()) + } + } + } +} diff --git a/subproject/Xray-core-main/common/protocol/udp/packet.go b/subproject/Xray-core-main/common/protocol/udp/packet.go new file mode 100644 index 00000000..cc17af98 --- /dev/null +++ b/subproject/Xray-core-main/common/protocol/udp/packet.go @@ -0,0 +1,13 @@ +package udp + +import ( + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/net" +) + +// Packet is a UDP packet together with its source and destination address. +type Packet struct { + Payload *buf.Buffer + Source net.Destination + Target net.Destination +} diff --git a/subproject/Xray-core-main/common/protocol/udp/udp.go b/subproject/Xray-core-main/common/protocol/udp/udp.go new file mode 100644 index 00000000..0213f41f --- /dev/null +++ b/subproject/Xray-core-main/common/protocol/udp/udp.go @@ -0,0 +1 @@ +package udp diff --git a/subproject/Xray-core-main/common/protocol/user.go b/subproject/Xray-core-main/common/protocol/user.go new file mode 100644 index 00000000..75e8e654 --- /dev/null +++ b/subproject/Xray-core-main/common/protocol/user.go @@ -0,0 +1,55 @@ +package protocol + +import ( + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/serial" +) + +func (u *User) GetTypedAccount() (Account, error) { + if u.GetAccount() == nil { + return nil, errors.New("Account is missing").AtWarning() + } + + rawAccount, err := u.Account.GetInstance() + if err != nil { + return nil, err + } + if asAccount, ok := rawAccount.(AsAccount); ok { + return asAccount.AsAccount() + } + if account, ok := rawAccount.(Account); ok { + return account, nil + } + return nil, errors.New("Unknown account type: ", u.Account.Type) +} + +func (u *User) ToMemoryUser() (*MemoryUser, error) { + account, err := u.GetTypedAccount() + if err != nil { + return nil, err + } + return &MemoryUser{ + Account: account, + Email: u.Email, + Level: u.Level, + }, nil +} + +func ToProtoUser(mu *MemoryUser) *User { + if mu == nil { + return nil + } + return &User{ + Account: serial.ToTypedMessage(mu.Account.ToProto()), + Email: mu.Email, + Level: mu.Level, + } +} + +// MemoryUser is a parsed form of User, to reduce number of parsing of Account proto. +type MemoryUser struct { + // Account is the parsed account of the protocol. + Account Account + Email string + Level uint32 +} diff --git a/subproject/Xray-core-main/common/protocol/user.pb.go b/subproject/Xray-core-main/common/protocol/user.pb.go new file mode 100644 index 00000000..bdefecb9 --- /dev/null +++ b/subproject/Xray-core-main/common/protocol/user.pb.go @@ -0,0 +1,147 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: common/protocol/user.proto + +package protocol + +import ( + serial "github.com/xtls/xray-core/common/serial" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// User is a generic user for all protocols. +type User struct { + state protoimpl.MessageState `protogen:"open.v1"` + Level uint32 `protobuf:"varint,1,opt,name=level,proto3" json:"level,omitempty"` + Email string `protobuf:"bytes,2,opt,name=email,proto3" json:"email,omitempty"` + // Protocol specific account information. Must be the account proto in one of + // the proxies. + Account *serial.TypedMessage `protobuf:"bytes,3,opt,name=account,proto3" json:"account,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *User) Reset() { + *x = User{} + mi := &file_common_protocol_user_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *User) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*User) ProtoMessage() {} + +func (x *User) ProtoReflect() protoreflect.Message { + mi := &file_common_protocol_user_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use User.ProtoReflect.Descriptor instead. +func (*User) Descriptor() ([]byte, []int) { + return file_common_protocol_user_proto_rawDescGZIP(), []int{0} +} + +func (x *User) GetLevel() uint32 { + if x != nil { + return x.Level + } + return 0 +} + +func (x *User) GetEmail() string { + if x != nil { + return x.Email + } + return "" +} + +func (x *User) GetAccount() *serial.TypedMessage { + if x != nil { + return x.Account + } + return nil +} + +var File_common_protocol_user_proto protoreflect.FileDescriptor + +const file_common_protocol_user_proto_rawDesc = "" + + "\n" + + "\x1acommon/protocol/user.proto\x12\x14xray.common.protocol\x1a!common/serial/typed_message.proto\"n\n" + + "\x04User\x12\x14\n" + + "\x05level\x18\x01 \x01(\rR\x05level\x12\x14\n" + + "\x05email\x18\x02 \x01(\tR\x05email\x12:\n" + + "\aaccount\x18\x03 \x01(\v2 .xray.common.serial.TypedMessageR\aaccountB^\n" + + "\x18com.xray.common.protocolP\x01Z)github.com/xtls/xray-core/common/protocol\xaa\x02\x14Xray.Common.Protocolb\x06proto3" + +var ( + file_common_protocol_user_proto_rawDescOnce sync.Once + file_common_protocol_user_proto_rawDescData []byte +) + +func file_common_protocol_user_proto_rawDescGZIP() []byte { + file_common_protocol_user_proto_rawDescOnce.Do(func() { + file_common_protocol_user_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_common_protocol_user_proto_rawDesc), len(file_common_protocol_user_proto_rawDesc))) + }) + return file_common_protocol_user_proto_rawDescData +} + +var file_common_protocol_user_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_common_protocol_user_proto_goTypes = []any{ + (*User)(nil), // 0: xray.common.protocol.User + (*serial.TypedMessage)(nil), // 1: xray.common.serial.TypedMessage +} +var file_common_protocol_user_proto_depIdxs = []int32{ + 1, // 0: xray.common.protocol.User.account:type_name -> xray.common.serial.TypedMessage + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_common_protocol_user_proto_init() } +func file_common_protocol_user_proto_init() { + if File_common_protocol_user_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_common_protocol_user_proto_rawDesc), len(file_common_protocol_user_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_common_protocol_user_proto_goTypes, + DependencyIndexes: file_common_protocol_user_proto_depIdxs, + MessageInfos: file_common_protocol_user_proto_msgTypes, + }.Build() + File_common_protocol_user_proto = out.File + file_common_protocol_user_proto_goTypes = nil + file_common_protocol_user_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/common/protocol/user.proto b/subproject/Xray-core-main/common/protocol/user.proto new file mode 100644 index 00000000..14cf995b --- /dev/null +++ b/subproject/Xray-core-main/common/protocol/user.proto @@ -0,0 +1,19 @@ +syntax = "proto3"; + +package xray.common.protocol; +option csharp_namespace = "Xray.Common.Protocol"; +option go_package = "github.com/xtls/xray-core/common/protocol"; +option java_package = "com.xray.common.protocol"; +option java_multiple_files = true; + +import "common/serial/typed_message.proto"; + +// User is a generic user for all protocols. +message User { + uint32 level = 1; + string email = 2; + + // Protocol specific account information. Must be the account proto in one of + // the proxies. + xray.common.serial.TypedMessage account = 3; +} diff --git a/subproject/Xray-core-main/common/reflect/marshal.go b/subproject/Xray-core-main/common/reflect/marshal.go new file mode 100644 index 00000000..127dc8e0 --- /dev/null +++ b/subproject/Xray-core-main/common/reflect/marshal.go @@ -0,0 +1,273 @@ +package reflect + +import ( + "bytes" + "encoding/json" + "fmt" + "reflect" + "strings" + + cnet "github.com/xtls/xray-core/common/net" + cserial "github.com/xtls/xray-core/common/serial" + "github.com/xtls/xray-core/infra/conf" +) + +func MarshalToJson(v interface{}, insertTypeInfo bool) (string, bool) { + if itf := marshalInterface(v, true, insertTypeInfo); itf != nil { + if b, err := JSONMarshalWithoutEscape(itf); err == nil { + return string(b[:]), true + } + } + return "", false +} + +func JSONMarshalWithoutEscape(t interface{}) ([]byte, error) { + buffer := &bytes.Buffer{} + encoder := json.NewEncoder(buffer) + encoder.SetIndent("", " ") + encoder.SetEscapeHTML(false) + err := encoder.Encode(t) + return buffer.Bytes(), err +} + +func marshalTypedMessage(v *cserial.TypedMessage, ignoreNullValue bool, insertTypeInfo bool) interface{} { + if v == nil { + return nil + } + tmsg, err := v.GetInstance() + if err != nil { + return nil + } + r := marshalInterface(tmsg, ignoreNullValue, insertTypeInfo) + if msg, ok := r.(map[string]interface{}); ok && insertTypeInfo { + msg["_TypedMessage_"] = v.Type + } + return r +} + +func marshalSlice(v reflect.Value, ignoreNullValue bool, insertTypeInfo bool) interface{} { + r := make([]interface{}, 0) + for i := 0; i < v.Len(); i++ { + rv := v.Index(i) + if rv.CanInterface() { + value := rv.Interface() + r = append(r, marshalInterface(value, ignoreNullValue, insertTypeInfo)) + } + } + return r +} + +func isNullValue(f reflect.StructField, rv reflect.Value) bool { + if rv.Kind() == reflect.Struct { + return false + } else if rv.Kind() == reflect.String && rv.Len() == 0 { + return true + } else if !isValueKind(rv.Kind()) && rv.IsNil() { + return true + } else if tag := f.Tag.Get("json"); strings.Contains(tag, "omitempty") { + if !rv.IsValid() || rv.IsZero() { + return true + } + } + return false +} + +func toJsonName(f reflect.StructField) string { + if tags := f.Tag.Get("protobuf"); len(tags) > 0 { + for _, tag := range strings.Split(tags, ",") { + if before, after, ok := strings.Cut(tag, "="); ok && before == "json" { + return after + } + } + } + if tag := f.Tag.Get("json"); len(tag) > 0 { + if before, _, ok := strings.Cut(tag, ","); ok { + return before + } else { + return tag + } + } + return f.Name +} + +func marshalStruct(v reflect.Value, ignoreNullValue bool, insertTypeInfo bool) interface{} { + r := make(map[string]interface{}) + t := v.Type() + for i := 0; i < v.NumField(); i++ { + rv := v.Field(i) + if rv.CanInterface() { + ft := t.Field(i) + if !ignoreNullValue || !isNullValue(ft, rv) { + name := toJsonName(ft) + value := rv.Interface() + tv := marshalInterface(value, ignoreNullValue, insertTypeInfo) + r[name] = tv + } + } + } + return r +} + +func marshalMap(v reflect.Value, ignoreNullValue bool, insertTypeInfo bool) interface{} { + // policy.level is map[uint32] *struct + kt := v.Type().Key() + vt := reflect.TypeOf((*interface{})(nil)) + mt := reflect.MapOf(kt, vt) + r := reflect.MakeMap(mt) + for _, key := range v.MapKeys() { + rv := v.MapIndex(key) + if rv.CanInterface() { + iv := rv.Interface() + tv := marshalInterface(iv, ignoreNullValue, insertTypeInfo) + if tv != nil || !ignoreNullValue { + r.SetMapIndex(key, reflect.ValueOf(&tv)) + } + } + } + return r.Interface() +} + +func marshalIString(v interface{}) (r string, ok bool) { + defer func() { + if err := recover(); err != nil { + r = "" + ok = false + } + }() + if iStringFn, ok := v.(interface{ String() string }); ok { + return iStringFn.String(), true + } + return "", false +} + +func serializePortList(portList *cnet.PortList) (interface{}, bool) { + if portList == nil { + return nil, false + } + + n := len(portList.Range) + if n == 1 { + if first := portList.Range[0]; first.From == first.To { + return first.From, true + } + } + + r := make([]string, 0, n) + for _, pr := range portList.Range { + if pr.From == pr.To { + r = append(r, pr.FromPort().String()) + } else { + r = append(r, fmt.Sprintf("%d-%d", pr.From, pr.To)) + } + } + return strings.Join(r, ","), true +} + +func marshalKnownType(v interface{}, ignoreNullValue bool, insertTypeInfo bool) (interface{}, bool) { + switch ty := v.(type) { + case cserial.TypedMessage: + return marshalTypedMessage(&ty, ignoreNullValue, insertTypeInfo), true + case *cserial.TypedMessage: + return marshalTypedMessage(ty, ignoreNullValue, insertTypeInfo), true + case map[string]json.RawMessage: + return ty, true + case []json.RawMessage: + return ty, true + case *json.RawMessage, json.RawMessage: + return ty, true + case *cnet.IPOrDomain: + if domain := v.(*cnet.IPOrDomain); domain != nil { + return domain.AsAddress().String(), true + } + return nil, false + case *cnet.PortList: + npl := v.(*cnet.PortList) + return serializePortList(npl) + case *conf.PortList: + cpl := v.(*conf.PortList) + 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: + if addr := v.(cnet.Address); addr != nil { + return addr.String(), true + } + return nil, false + default: + return nil, false + } +} + +func isValueKind(kind reflect.Kind) bool { + switch 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{} { + + if r, ok := marshalKnownType(v, ignoreNullValue, insertTypeInfo); ok { + return r + } + + rv := reflect.ValueOf(v) + if rv.Kind() == reflect.Ptr { + rv = rv.Elem() + } + k := rv.Kind() + if k == reflect.Invalid { + return nil + } + + if ty := rv.Type().Name(); isValueKind(k) { + if k.String() != ty { + if s, ok := marshalIString(v); ok { + return s + } + } + return v + } + + // fmt.Println("kind:", k, "type:", rv.Type().Name()) + + switch k { + case reflect.Struct: + return marshalStruct(rv, ignoreNullValue, insertTypeInfo) + case reflect.Slice: + return marshalSlice(rv, ignoreNullValue, insertTypeInfo) + case reflect.Array: + return marshalSlice(rv, ignoreNullValue, insertTypeInfo) + case reflect.Map: + return marshalMap(rv, ignoreNullValue, insertTypeInfo) + default: + break + } + + if str, ok := marshalIString(v); ok { + return str + } + return nil +} diff --git a/subproject/Xray-core-main/common/reflect/marshal_test.go b/subproject/Xray-core-main/common/reflect/marshal_test.go new file mode 100644 index 00000000..1a897ef7 --- /dev/null +++ b/subproject/Xray-core-main/common/reflect/marshal_test.go @@ -0,0 +1,243 @@ +package reflect_test + +import ( + "bytes" + "encoding/json" + "strings" + "testing" + + "github.com/xtls/xray-core/common/protocol" + . "github.com/xtls/xray-core/common/reflect" + cserial "github.com/xtls/xray-core/common/serial" + iserial "github.com/xtls/xray-core/infra/conf/serial" + "github.com/xtls/xray-core/proxy/shadowsocks" +) + +func TestMashalAccount(t *testing.T) { + account := &shadowsocks.Account{ + Password: "shadowsocks-password", + CipherType: shadowsocks.CipherType_CHACHA20_POLY1305, + } + + user := &protocol.User{ + Level: 0, + Email: "love@v2ray.com", + Account: cserial.ToTypedMessage(account), + } + + j, ok := MarshalToJson(user, false) + if !ok || strings.Contains(j, "_TypedMessage_") { + + t.Error("marshal account failed") + } + + kws := []string{"CHACHA20_POLY1305", "cipherType", "shadowsocks-password"} + for _, kw := range kws { + if !strings.Contains(j, kw) { + t.Error("marshal account failed") + } + } + // t.Log(j) +} + +func TestMashalStruct(t *testing.T) { + type Foo = struct { + N int `json:"n"` + Np *int `json:"np"` + S string `json:"s"` + Arr *[]map[string]map[string]string `json:"arr"` + } + + n := 1 + np := &n + arr := make([]map[string]map[string]string, 0) + m1 := make(map[string]map[string]string, 0) + m2 := make(map[string]string, 0) + m2["hello"] = "world" + m1["foo"] = m2 + + arr = append(arr, m1) + + f1 := Foo{ + N: n, + Np: np, + S: "hello", + Arr: &arr, + } + + s, ok1 := MarshalToJson(f1, true) + sp, ok2 := MarshalToJson(&f1, true) + + if !ok1 || !ok2 || s != sp { + t.Error("marshal failed") + } + + f2 := Foo{} + if json.Unmarshal([]byte(s), &f2) != nil { + t.Error("json unmarshal failed") + } + + v := (*f2.Arr)[0]["foo"]["hello"] + + if f1.N != f2.N || *(f1.Np) != *(f2.Np) || f1.S != f2.S || v != "world" { + t.Error("f1 not equal to f2") + } +} + +func TestMarshalConfigJson(t *testing.T) { + + buf := bytes.NewBufferString(getConfig()) + config, err := iserial.DecodeJSONConfig(buf) + if err != nil { + t.Error("decode JSON config failed") + } + + bc, err := config.Build() + if err != nil { + t.Error("build core config failed") + } + + tmsg := cserial.ToTypedMessage(bc) + tc, ok := MarshalToJson(tmsg, true) + if !ok { + t.Error("marshal config failed") + } + + // t.Log(tc) + + keywords := []string{ + "4784f9b8-a879-4fec-9718-ebddefa47750", + "bing.com", + "inboundTag", + "level", + "stats", + "userDownlink", + "userUplink", + "system", + "inboundDownlink", + "outboundUplink", + "XHTTP_IN", + "\"host\": \"bing.com\"", + "scMaxEachPostBytes", + "\"from\": 100", + "\"to\": 1000", + "\"from\": 1000000", + "\"to\": 1000000", + } + for _, kw := range keywords { + if !strings.Contains(tc, kw) { + t.Log("config.json:", tc) + t.Error("keyword not found:", kw) + break + } + } +} + +func getConfig() string { + return `{ + "log": { + "loglevel": "debug" + }, + "stats": {}, + "policy": { + "levels": { + "0": { + "statsUserUplink": true, + "statsUserDownlink": true + } + }, + "system": { + "statsInboundUplink": true, + "statsInboundDownlink": true, + "statsOutboundUplink": true, + "statsOutboundDownlink": true + } + }, + "inbounds": [ + { + "tag": "agentin", + "protocol": "http", + "port": 18080, + "listen": "127.0.0.1", + "settings": {} + }, + { + "listen": "127.0.0.1", + "port": 10085, + "protocol": "dokodemo-door", + "settings": { + "address": "127.0.0.1" + }, + "tag": "api-in" + } + ], + "api": { + "tag": "api", + "services": [ + "HandlerService", + "StatsService" + ] + }, + "routing": { + "rules": [ + { + "inboundTag": [ + "api-in" + ], + "outboundTag": "api" + } + ], + "domainStrategy": "AsIs" + }, + "outbounds": [ + { + "protocol": "vless", + "settings": { + "vnext": [ + { + "address": "1.2.3.4", + "port": 1234, + "users": [ + { + "id": "4784f9b8-a879-4fec-9718-ebddefa47750", + "encryption": "none" + } + ] + } + ] + }, + "tag": "XHTTP_IN", + "streamSettings": { + "network": "xhttp", + "xhttpSettings": { + "host": "bing.com", + "path": "/xhttp_client_upload", + "mode": "auto", + "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 + } + } + ] +}` +} diff --git a/subproject/Xray-core-main/common/retry/retry.go b/subproject/Xray-core-main/common/retry/retry.go new file mode 100644 index 00000000..02dcbfb9 --- /dev/null +++ b/subproject/Xray-core-main/common/retry/retry.go @@ -0,0 +1,62 @@ +package retry // import "github.com/xtls/xray-core/common/retry" + +import ( + "time" + + "github.com/xtls/xray-core/common/errors" +) + +var ErrRetryFailed = errors.New("all retry attempts failed") + +// Strategy is a way to retry on a specific function. +type Strategy interface { + // On performs a retry on a specific function, until it doesn't return any error. + On(func() error) error +} + +type retryer struct { + totalAttempt int + nextDelay func() uint32 +} + +// On implements Strategy.On. +func (r *retryer) On(method func() error) error { + attempt := 0 + accumulatedError := make([]error, 0, r.totalAttempt) + for attempt < r.totalAttempt { + err := method() + if err == nil { + return nil + } + numErrors := len(accumulatedError) + if numErrors == 0 || err.Error() != accumulatedError[numErrors-1].Error() { + accumulatedError = append(accumulatedError, err) + } + delay := r.nextDelay() + time.Sleep(time.Duration(delay) * time.Millisecond) + attempt++ + } + return errors.New(accumulatedError).Base(ErrRetryFailed) +} + +// Timed returns a retry strategy with fixed interval. +func Timed(attempts int, delay uint32) Strategy { + return &retryer{ + totalAttempt: attempts, + nextDelay: func() uint32 { + return delay + }, + } +} + +func ExponentialBackoff(attempts int, delay uint32) Strategy { + nextDelay := uint32(0) + return &retryer{ + totalAttempt: attempts, + nextDelay: func() uint32 { + r := nextDelay + nextDelay += delay + return r + }, + } +} diff --git a/subproject/Xray-core-main/common/retry/retry_test.go b/subproject/Xray-core-main/common/retry/retry_test.go new file mode 100644 index 00000000..52755a8d --- /dev/null +++ b/subproject/Xray-core-main/common/retry/retry_test.go @@ -0,0 +1,96 @@ +package retry_test + +import ( + "testing" + "time" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/errors" + . "github.com/xtls/xray-core/common/retry" +) + +var errorTestOnly = errors.New("this is a fake error") + +func TestNoRetry(t *testing.T) { + startTime := time.Now().Unix() + err := Timed(10, 100000).On(func() error { + return nil + }) + endTime := time.Now().Unix() + + common.Must(err) + if endTime < startTime { + t.Error("endTime < startTime: ", startTime, " -> ", endTime) + } +} + +func TestRetryOnce(t *testing.T) { + startTime := time.Now() + called := 0 + err := Timed(10, 1000).On(func() error { + if called == 0 { + called++ + return errorTestOnly + } + return nil + }) + duration := time.Since(startTime) + + common.Must(err) + if v := int64(duration / time.Millisecond); v < 900 { + t.Error("duration: ", v) + } +} + +func TestRetryMultiple(t *testing.T) { + startTime := time.Now() + called := 0 + err := Timed(10, 1000).On(func() error { + if called < 5 { + called++ + return errorTestOnly + } + return nil + }) + duration := time.Since(startTime) + + common.Must(err) + if v := int64(duration / time.Millisecond); v < 4900 { + t.Error("duration: ", v) + } +} + +func TestRetryExhausted(t *testing.T) { + startTime := time.Now() + called := 0 + err := Timed(2, 1000).On(func() error { + called++ + return errorTestOnly + }) + duration := time.Since(startTime) + + if errors.Cause(err) != ErrRetryFailed { + t.Error("cause: ", err) + } + + if v := int64(duration / time.Millisecond); v < 1900 { + t.Error("duration: ", v) + } +} + +func TestExponentialBackoff(t *testing.T) { + startTime := time.Now() + called := 0 + err := ExponentialBackoff(10, 100).On(func() error { + called++ + return errorTestOnly + }) + duration := time.Since(startTime) + + if errors.Cause(err) != ErrRetryFailed { + t.Error("cause: ", err) + } + if v := int64(duration / time.Millisecond); v < 4000 { + t.Error("duration: ", v) + } +} diff --git a/subproject/Xray-core-main/common/serial/serial.go b/subproject/Xray-core-main/common/serial/serial.go new file mode 100644 index 00000000..9d8cb4ca --- /dev/null +++ b/subproject/Xray-core-main/common/serial/serial.go @@ -0,0 +1,29 @@ +package serial + +import ( + "encoding/binary" + "io" +) + +// ReadUint16 reads first two bytes from the reader, and then converts them to an uint16 value. +func ReadUint16(reader io.Reader) (uint16, error) { + var b [2]byte + if _, err := io.ReadFull(reader, b[:]); err != nil { + return 0, err + } + return binary.BigEndian.Uint16(b[:]), nil +} + +// WriteUint16 writes an uint16 value into writer. +func WriteUint16(writer io.Writer, value uint16) (int, error) { + var b [2]byte + binary.BigEndian.PutUint16(b[:], value) + return writer.Write(b[:]) +} + +// WriteUint64 writes an uint64 value into writer. +func WriteUint64(writer io.Writer, value uint64) (int, error) { + var b [8]byte + binary.BigEndian.PutUint64(b[:], value) + return writer.Write(b[:]) +} diff --git a/subproject/Xray-core-main/common/serial/serial_test.go b/subproject/Xray-core-main/common/serial/serial_test.go new file mode 100644 index 00000000..f3cb511a --- /dev/null +++ b/subproject/Xray-core-main/common/serial/serial_test.go @@ -0,0 +1,87 @@ +package serial_test + +import ( + "bytes" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/serial" +) + +func TestUint16Serial(t *testing.T) { + b := buf.New() + defer b.Release() + + n, err := serial.WriteUint16(b, 10) + common.Must(err) + if n != 2 { + t.Error("expect 2 bytes writtng, but actually ", n) + } + if diff := cmp.Diff(b.Bytes(), []byte{0, 10}); diff != "" { + t.Error(diff) + } +} + +func TestUint64Serial(t *testing.T) { + b := buf.New() + defer b.Release() + + n, err := serial.WriteUint64(b, 10) + common.Must(err) + if n != 8 { + t.Error("expect 8 bytes writtng, but actually ", n) + } + if diff := cmp.Diff(b.Bytes(), []byte{0, 0, 0, 0, 0, 0, 0, 10}); diff != "" { + t.Error(diff) + } +} + +func TestReadUint16(t *testing.T) { + testCases := []struct { + Input []byte + Output uint16 + }{ + { + Input: []byte{0, 1}, + Output: 1, + }, + } + + for _, testCase := range testCases { + v, err := serial.ReadUint16(bytes.NewReader(testCase.Input)) + common.Must(err) + if v != testCase.Output { + t.Error("for input ", testCase.Input, " expect output ", testCase.Output, " but got ", v) + } + } +} + +func BenchmarkReadUint16(b *testing.B) { + reader := buf.New() + defer reader.Release() + + common.Must2(reader.Write([]byte{0, 1})) + b.ResetTimer() + + for i := 0; i < b.N; i++ { + _, err := serial.ReadUint16(reader) + common.Must(err) + reader.Clear() + reader.Extend(2) + } +} + +func BenchmarkWriteUint64(b *testing.B) { + writer := buf.New() + defer writer.Release() + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + _, err := serial.WriteUint64(writer, 8) + common.Must(err) + writer.Clear() + } +} diff --git a/subproject/Xray-core-main/common/serial/string.go b/subproject/Xray-core-main/common/serial/string.go new file mode 100644 index 00000000..8fdcd7f6 --- /dev/null +++ b/subproject/Xray-core-main/common/serial/string.go @@ -0,0 +1,35 @@ +package serial + +import ( + "fmt" + "strings" +) + +// ToString serializes an arbitrary value into string. +func ToString(v interface{}) string { + if v == nil { + return "" + } + + switch value := v.(type) { + case string: + return value + case *string: + return *value + case fmt.Stringer: + return value.String() + case error: + return value.Error() + default: + return fmt.Sprintf("%+v", value) + } +} + +// Concat concatenates all input into a single string. +func Concat(v ...interface{}) string { + builder := strings.Builder{} + for _, value := range v { + builder.WriteString(ToString(value)) + } + return builder.String() +} diff --git a/subproject/Xray-core-main/common/serial/string_test.go b/subproject/Xray-core-main/common/serial/string_test.go new file mode 100644 index 00000000..369d1b10 --- /dev/null +++ b/subproject/Xray-core-main/common/serial/string_test.go @@ -0,0 +1,58 @@ +package serial_test + +import ( + "errors" + "testing" + + "github.com/google/go-cmp/cmp" + . "github.com/xtls/xray-core/common/serial" +) + +func TestToString(t *testing.T) { + s := "a" + data := []struct { + Value interface{} + String string + }{ + {Value: s, String: s}, + {Value: &s, String: s}, + {Value: errors.New("t"), String: "t"}, + {Value: []byte{'b', 'c'}, String: "[98 99]"}, + } + + for _, c := range data { + if r := cmp.Diff(ToString(c.Value), c.String); r != "" { + t.Error(r) + } + } +} + +func TestConcat(t *testing.T) { + testCases := []struct { + Input []interface{} + Output string + }{ + { + Input: []interface{}{ + "a", "b", + }, + Output: "ab", + }, + } + + for _, testCase := range testCases { + actual := Concat(testCase.Input...) + if actual != testCase.Output { + t.Error("Unexpected output: ", actual, " but want: ", testCase.Output) + } + } +} + +func BenchmarkConcat(b *testing.B) { + input := []interface{}{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k"} + + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = Concat(input...) + } +} diff --git a/subproject/Xray-core-main/common/serial/typed_message.go b/subproject/Xray-core-main/common/serial/typed_message.go new file mode 100644 index 00000000..baecc92e --- /dev/null +++ b/subproject/Xray-core-main/common/serial/typed_message.go @@ -0,0 +1,47 @@ +package serial + +import ( + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/reflect/protoregistry" +) + +// ToTypedMessage converts a proto Message into TypedMessage. +func ToTypedMessage(message proto.Message) *TypedMessage { + if message == nil { + return nil + } + settings, _ := proto.Marshal(message) + return &TypedMessage{ + Type: GetMessageType(message), + Value: settings, + } +} + +// GetMessageType returns the name of this proto Message. +func GetMessageType(message proto.Message) string { + return string(message.ProtoReflect().Descriptor().FullName()) +} + +// GetInstance creates a new instance of the message with messageType. +func GetInstance(messageType string) (interface{}, error) { + messageTypeDescriptor := protoreflect.FullName(messageType) + mType, err := protoregistry.GlobalTypes.FindMessageByName(messageTypeDescriptor) + if err != nil { + return nil, err + } + return mType.New().Interface(), nil +} + +// GetInstance converts current TypedMessage into a proto Message. +func (v *TypedMessage) GetInstance() (proto.Message, error) { + instance, err := GetInstance(v.Type) + if err != nil { + return nil, err + } + protoMessage := instance.(proto.Message) + if err := proto.Unmarshal(v.Value, protoMessage); err != nil { + return nil, err + } + return protoMessage, nil +} diff --git a/subproject/Xray-core-main/common/serial/typed_message.pb.go b/subproject/Xray-core-main/common/serial/typed_message.pb.go new file mode 100644 index 00000000..f29d2f77 --- /dev/null +++ b/subproject/Xray-core-main/common/serial/typed_message.pb.go @@ -0,0 +1,135 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: common/serial/typed_message.proto + +package serial + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// TypedMessage is a serialized proto message along with its type name. +type TypedMessage struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The name of the message type, retrieved from protobuf API. + Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` + // Serialized proto message. + Value []byte `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TypedMessage) Reset() { + *x = TypedMessage{} + mi := &file_common_serial_typed_message_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TypedMessage) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TypedMessage) ProtoMessage() {} + +func (x *TypedMessage) ProtoReflect() protoreflect.Message { + mi := &file_common_serial_typed_message_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TypedMessage.ProtoReflect.Descriptor instead. +func (*TypedMessage) Descriptor() ([]byte, []int) { + return file_common_serial_typed_message_proto_rawDescGZIP(), []int{0} +} + +func (x *TypedMessage) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *TypedMessage) GetValue() []byte { + if x != nil { + return x.Value + } + return nil +} + +var File_common_serial_typed_message_proto protoreflect.FileDescriptor + +const file_common_serial_typed_message_proto_rawDesc = "" + + "\n" + + "!common/serial/typed_message.proto\x12\x12xray.common.serial\"8\n" + + "\fTypedMessage\x12\x12\n" + + "\x04type\x18\x01 \x01(\tR\x04type\x12\x14\n" + + "\x05value\x18\x02 \x01(\fR\x05valueBX\n" + + "\x16com.xray.common.serialP\x01Z'github.com/xtls/xray-core/common/serial\xaa\x02\x12Xray.Common.Serialb\x06proto3" + +var ( + file_common_serial_typed_message_proto_rawDescOnce sync.Once + file_common_serial_typed_message_proto_rawDescData []byte +) + +func file_common_serial_typed_message_proto_rawDescGZIP() []byte { + file_common_serial_typed_message_proto_rawDescOnce.Do(func() { + file_common_serial_typed_message_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_common_serial_typed_message_proto_rawDesc), len(file_common_serial_typed_message_proto_rawDesc))) + }) + return file_common_serial_typed_message_proto_rawDescData +} + +var file_common_serial_typed_message_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_common_serial_typed_message_proto_goTypes = []any{ + (*TypedMessage)(nil), // 0: xray.common.serial.TypedMessage +} +var file_common_serial_typed_message_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_common_serial_typed_message_proto_init() } +func file_common_serial_typed_message_proto_init() { + if File_common_serial_typed_message_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_common_serial_typed_message_proto_rawDesc), len(file_common_serial_typed_message_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_common_serial_typed_message_proto_goTypes, + DependencyIndexes: file_common_serial_typed_message_proto_depIdxs, + MessageInfos: file_common_serial_typed_message_proto_msgTypes, + }.Build() + File_common_serial_typed_message_proto = out.File + file_common_serial_typed_message_proto_goTypes = nil + file_common_serial_typed_message_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/common/serial/typed_message.proto b/subproject/Xray-core-main/common/serial/typed_message.proto new file mode 100644 index 00000000..558de64b --- /dev/null +++ b/subproject/Xray-core-main/common/serial/typed_message.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; + +package xray.common.serial; +option csharp_namespace = "Xray.Common.Serial"; +option go_package = "github.com/xtls/xray-core/common/serial"; +option java_package = "com.xray.common.serial"; +option java_multiple_files = true; + +// TypedMessage is a serialized proto message along with its type name. +message TypedMessage { + // The name of the message type, retrieved from protobuf API. + string type = 1; + // Serialized proto message. + bytes value = 2; +} diff --git a/subproject/Xray-core-main/common/serial/typed_message_test.go b/subproject/Xray-core-main/common/serial/typed_message_test.go new file mode 100644 index 00000000..726a7733 --- /dev/null +++ b/subproject/Xray-core-main/common/serial/typed_message_test.go @@ -0,0 +1,24 @@ +package serial_test + +import ( + "testing" + + . "github.com/xtls/xray-core/common/serial" +) + +func TestGetInstance(t *testing.T) { + p, err := GetInstance("") + if p != nil { + t.Error("expected nil instance, but got ", p) + } + if err == nil { + t.Error("expect non-nil error, but got nil") + } +} + +func TestConvertingNilMessage(t *testing.T) { + x := ToTypedMessage(nil) + if x != nil { + t.Error("expect nil, but actually not") + } +} diff --git a/subproject/Xray-core-main/common/session/context.go b/subproject/Xray-core-main/common/session/context.go new file mode 100644 index 00000000..c28f2081 --- /dev/null +++ b/subproject/Xray-core-main/common/session/context.go @@ -0,0 +1,194 @@ +package session + +import ( + "context" + _ "unsafe" + + "github.com/xtls/xray-core/common/ctx" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/features/outbound" + "github.com/xtls/xray-core/features/routing" +) + +//go:linkname IndependentCancelCtx context.newCancelCtx +func IndependentCancelCtx(parent context.Context) context.Context + +const ( + inboundSessionKey ctx.SessionKey = 1 + outboundSessionKey ctx.SessionKey = 2 + contentSessionKey ctx.SessionKey = 3 + isReverseMuxKey ctx.SessionKey = 4 // is reverse mux + sockoptSessionKey ctx.SessionKey = 5 // used by dokodemo to only receive sockopt.Mark + trackedConnectionErrorKey ctx.SessionKey = 6 // used by observer to get outbound error + dispatcherKey ctx.SessionKey = 7 // used by ss2022 inbounds to get dispatcher + timeoutOnlyKey ctx.SessionKey = 8 // mux context's child contexts to only cancel when its own traffic times out + allowedNetworkKey ctx.SessionKey = 9 // muxcool server control incoming request tcp/udp + fullHandlerKey ctx.SessionKey = 10 // outbound gets full handler + mitmAlpn11Key ctx.SessionKey = 11 // used by TLS dialer + mitmServerNameKey ctx.SessionKey = 12 // used by TLS dialer +) + +func ContextWithInbound(ctx context.Context, inbound *Inbound) context.Context { + return context.WithValue(ctx, inboundSessionKey, inbound) +} + +func InboundFromContext(ctx context.Context) *Inbound { + if inbound, ok := ctx.Value(inboundSessionKey).(*Inbound); ok { + return inbound + } + return nil +} + +func ContextWithOutbounds(ctx context.Context, outbounds []*Outbound) context.Context { + return context.WithValue(ctx, outboundSessionKey, outbounds) +} + +func SubContextFromMuxInbound(ctx context.Context) context.Context { + newOutbounds := []*Outbound{{}} + + content := ContentFromContext(ctx) + 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 { + if outbounds, ok := ctx.Value(outboundSessionKey).([]*Outbound); ok { + return outbounds + } + return nil +} + +func ContextWithContent(ctx context.Context, content *Content) context.Context { + return context.WithValue(ctx, contentSessionKey, content) +} + +func ContentFromContext(ctx context.Context) *Content { + if content, ok := ctx.Value(contentSessionKey).(*Content); ok { + return content + } + return nil +} + +func ContextWithIsReverseMux(ctx context.Context, isReverseMux bool) context.Context { + return context.WithValue(ctx, isReverseMuxKey, isReverseMux) +} + +func IsReverseMuxFromContext(ctx context.Context) bool { + if val, ok := ctx.Value(isReverseMuxKey).(bool); ok { + return val + } + return false +} + +func ContextWithSockopt(ctx context.Context, s *Sockopt) context.Context { + return context.WithValue(ctx, sockoptSessionKey, s) +} + +func SockoptFromContext(ctx context.Context) *Sockopt { + if sockopt, ok := ctx.Value(sockoptSessionKey).(*Sockopt); ok { + return sockopt + } + return nil +} + +func GetForcedOutboundTagFromContext(ctx context.Context) string { + if ContentFromContext(ctx) == nil { + return "" + } + return ContentFromContext(ctx).Attribute("forcedOutboundTag") +} + +func SetForcedOutboundTagToContext(ctx context.Context, tag string) context.Context { + if contentFromContext := ContentFromContext(ctx); contentFromContext == nil { + ctx = ContextWithContent(ctx, &Content{}) + } + ContentFromContext(ctx).SetAttribute("forcedOutboundTag", tag) + return ctx +} + +type TrackedRequestErrorFeedback interface { + SubmitError(err error) +} + +func SubmitOutboundErrorToOriginator(ctx context.Context, err error) { + if errorTracker := ctx.Value(trackedConnectionErrorKey); errorTracker != nil { + errorTracker := errorTracker.(TrackedRequestErrorFeedback) + errorTracker.SubmitError(err) + } +} + +func TrackedConnectionError(ctx context.Context, tracker TrackedRequestErrorFeedback) context.Context { + return context.WithValue(ctx, trackedConnectionErrorKey, tracker) +} + +func ContextWithDispatcher(ctx context.Context, dispatcher routing.Dispatcher) context.Context { + return context.WithValue(ctx, dispatcherKey, dispatcher) +} + +func DispatcherFromContext(ctx context.Context) routing.Dispatcher { + if dispatcher, ok := ctx.Value(dispatcherKey).(routing.Dispatcher); ok { + return dispatcher + } + return nil +} + +func ContextWithTimeoutOnly(ctx context.Context, only bool) context.Context { + return context.WithValue(ctx, timeoutOnlyKey, only) +} + +func TimeoutOnlyFromContext(ctx context.Context) bool { + if val, ok := ctx.Value(timeoutOnlyKey).(bool); ok { + return val + } + return false +} + +func ContextWithAllowedNetwork(ctx context.Context, network net.Network) context.Context { + return context.WithValue(ctx, allowedNetworkKey, network) +} + +func AllowedNetworkFromContext(ctx context.Context) net.Network { + if val, ok := ctx.Value(allowedNetworkKey).(net.Network); ok { + return val + } + return net.Network_Unknown +} + +func ContextWithFullHandler(ctx context.Context, handler outbound.Handler) context.Context { + return context.WithValue(ctx, fullHandlerKey, handler) +} + +func FullHandlerFromContext(ctx context.Context) outbound.Handler { + if val, ok := ctx.Value(fullHandlerKey).(outbound.Handler); ok { + return val + } + return nil +} + +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 "" +} diff --git a/subproject/Xray-core-main/common/session/session.go b/subproject/Xray-core-main/common/session/session.go new file mode 100644 index 00000000..7e0c0369 --- /dev/null +++ b/subproject/Xray-core-main/common/session/session.go @@ -0,0 +1,122 @@ +// Package session provides functions for sessions of incoming requests. +package session // import "github.com/xtls/xray-core/common/session" + +import ( + "context" + "math/rand" + + c "github.com/xtls/xray-core/common/ctx" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/signal" +) + +// NewID generates a new ID. The generated ID is high likely to be unique, but not cryptographically secure. +// The generated ID will never be 0. +func NewID() c.ID { + for { + id := c.ID(rand.Uint32()) + if id != 0 { + return id + } + } +} + +// ExportIDToError transfers session.ID into an error object, for logging purpose. +// This can be used with error.WriteToLog(). +func ExportIDToError(ctx context.Context) errors.ExportOption { + id := c.IDFromContext(ctx) + return func(h *errors.ExportOptionHolder) { + h.SessionID = uint32(id) + } +} + +// Inbound is the metadata of an inbound connection. +type Inbound struct { + // Source address of the inbound connection. + Source net.Destination + // Local address of the inbound connection. + Local net.Destination + // Gateway address. + Gateway net.Destination + // Tag of the inbound proxy that handles the connection. + Tag string + // Name of the inbound proxy that handles the connection. + Name string + // User is the user that authenticates for the inbound. May be nil if the protocol allows anonymous traffic. + User *protocol.MemoryUser + // VlessRoute is the user-sent VLESS UUID's 7th<<8 | 8th bytes. + VlessRoute net.Port + // Used by splice copy. Conn is actually internet.Connection. May be nil. + Conn net.Conn + // Used by splice copy. Timer of the inbound buf copier. May be nil. + Timer *signal.ActivityTimer + // CanSpliceCopy is a property for this connection + // 1 = can, 2 = after processing protocol info should be able to, 3 = cannot + CanSpliceCopy int +} + +// Outbound is the metadata of an outbound connection. +type Outbound struct { + // Target address of the outbound connection. + OriginalTarget net.Destination + Target net.Destination + RouteTarget net.Destination + // Gateway address + Gateway net.Address + // Tag of the outbound proxy that handles the connection. + Tag string + // Name of the outbound proxy that handles the connection. + Name string + // Unused. Conn is actually internet.Connection. May be nil. It is currently nil for outbound with proxySettings + Conn net.Conn + // CanSpliceCopy is a property for this connection + // 1 = can, 2 = after processing protocol info should be able to, 3 = cannot + CanSpliceCopy int +} + +// SniffingRequest controls the behavior of content sniffing. They are from inbound config. Read-only +type SniffingRequest struct { + ExcludeForDomain []string + OverrideDestinationForProtocol []string + Enabled bool + MetadataOnly bool + RouteOnly bool +} + +// Content is the metadata of the connection content. Mainly used for routing. +type Content struct { + // Protocol of current content. + Protocol string + + SniffingRequest SniffingRequest + + // HTTP traffic sniffed headers + Attributes map[string]string + + // SkipDNSResolve is set from DNS module. the DOH remote server maybe a domain name, this prevents cycle resolving dead loop + SkipDNSResolve bool +} + +// Sockopt is the settings for socket connection. +type Sockopt struct { + // Mark of the socket connection. + Mark int32 +} + +// SetAttribute attaches additional string attributes to content. +func (c *Content) SetAttribute(name string, value string) { + if c.Attributes == nil { + c.Attributes = make(map[string]string) + } + c.Attributes[name] = value +} + +// Attribute retrieves additional string attributes from content. +func (c *Content) Attribute(name string) string { + if c.Attributes == nil { + return "" + } + return c.Attributes[name] +} diff --git a/subproject/Xray-core-main/common/signal/done/done.go b/subproject/Xray-core-main/common/signal/done/done.go new file mode 100644 index 00000000..189a8cf3 --- /dev/null +++ b/subproject/Xray-core-main/common/signal/done/done.go @@ -0,0 +1,49 @@ +package done + +import ( + "sync" +) + +// Instance is a utility for notifications of something being done. +type Instance struct { + access sync.Mutex + c chan struct{} + closed bool +} + +// New returns a new Done. +func New() *Instance { + return &Instance{ + c: make(chan struct{}), + } +} + +// Done returns true if Close() is called. +func (d *Instance) Done() bool { + select { + case <-d.Wait(): + return true + default: + return false + } +} + +// Wait returns a channel for waiting for done. +func (d *Instance) Wait() <-chan struct{} { + return d.c +} + +// Close marks this Done 'done'. This method may be called multiple times. All calls after first call will have no effect on its status. +func (d *Instance) Close() error { + d.access.Lock() + defer d.access.Unlock() + + if d.closed { + return nil + } + + d.closed = true + close(d.c) + + return nil +} diff --git a/subproject/Xray-core-main/common/signal/notifier.go b/subproject/Xray-core-main/common/signal/notifier.go new file mode 100644 index 00000000..19836e54 --- /dev/null +++ b/subproject/Xray-core-main/common/signal/notifier.go @@ -0,0 +1,26 @@ +package signal + +// Notifier is a utility for notifying changes. The change producer may notify changes multiple time, and the consumer may get notified asynchronously. +type Notifier struct { + c chan struct{} +} + +// NewNotifier creates a new Notifier. +func NewNotifier() *Notifier { + return &Notifier{ + c: make(chan struct{}, 1), + } +} + +// Signal signals a change, usually by producer. This method never blocks. +func (n *Notifier) Signal() { + select { + case n.c <- struct{}{}: + default: + } +} + +// Wait returns a channel for waiting for changes. The returned channel never gets closed. +func (n *Notifier) Wait() <-chan struct{} { + return n.c +} diff --git a/subproject/Xray-core-main/common/signal/notifier_test.go b/subproject/Xray-core-main/common/signal/notifier_test.go new file mode 100644 index 00000000..c753e253 --- /dev/null +++ b/subproject/Xray-core-main/common/signal/notifier_test.go @@ -0,0 +1,20 @@ +package signal_test + +import ( + "testing" + + . "github.com/xtls/xray-core/common/signal" +) + +func TestNotifierSignal(t *testing.T) { + n := NewNotifier() + + w := n.Wait() + n.Signal() + + select { + case <-w: + default: + t.Fail() + } +} diff --git a/subproject/Xray-core-main/common/signal/pubsub/pubsub.go b/subproject/Xray-core-main/common/signal/pubsub/pubsub.go new file mode 100644 index 00000000..cc9b87eb --- /dev/null +++ b/subproject/Xray-core-main/common/signal/pubsub/pubsub.go @@ -0,0 +1,105 @@ +package pubsub + +import ( + "errors" + "sync" + "time" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/signal/done" + "github.com/xtls/xray-core/common/task" +) + +type Subscriber struct { + buffer chan interface{} + done *done.Instance +} + +func (s *Subscriber) push(msg interface{}) { + select { + case s.buffer <- msg: + default: + } +} + +func (s *Subscriber) Wait() <-chan interface{} { + return s.buffer +} + +func (s *Subscriber) Close() error { + return s.done.Close() +} + +func (s *Subscriber) IsClosed() bool { + return s.done.Done() +} + +type Service struct { + sync.RWMutex + subs map[string][]*Subscriber + ctask *task.Periodic +} + +func NewService() *Service { + s := &Service{ + subs: make(map[string][]*Subscriber), + } + s.ctask = &task.Periodic{ + Execute: s.Cleanup, + Interval: time.Second * 30, + } + return s +} + +// Cleanup cleans up internal caches of subscribers. +// Visible for testing only. +func (s *Service) Cleanup() error { + s.Lock() + defer s.Unlock() + + if len(s.subs) == 0 { + return errors.New("nothing to do") + } + + for name, subs := range s.subs { + newSub := make([]*Subscriber, 0, len(s.subs)) + for _, sub := range subs { + if !sub.IsClosed() { + newSub = append(newSub, sub) + } + } + if len(newSub) == 0 { + delete(s.subs, name) + } else { + s.subs[name] = newSub + } + } + + if len(s.subs) == 0 { + s.subs = make(map[string][]*Subscriber) + } + return nil +} + +func (s *Service) Subscribe(name string) *Subscriber { + sub := &Subscriber{ + buffer: make(chan interface{}, 16), + done: done.New(), + } + s.Lock() + s.subs[name] = append(s.subs[name], sub) + s.Unlock() + common.Must(s.ctask.Start()) + return sub +} + +func (s *Service) Publish(name string, message interface{}) { + s.RLock() + defer s.RUnlock() + + for _, sub := range s.subs[name] { + if !sub.IsClosed() { + sub.push(message) + } + } +} diff --git a/subproject/Xray-core-main/common/signal/pubsub/pubsub_test.go b/subproject/Xray-core-main/common/signal/pubsub/pubsub_test.go new file mode 100644 index 00000000..b330e8d9 --- /dev/null +++ b/subproject/Xray-core-main/common/signal/pubsub/pubsub_test.go @@ -0,0 +1,34 @@ +package pubsub_test + +import ( + "testing" + + . "github.com/xtls/xray-core/common/signal/pubsub" +) + +func TestPubsub(t *testing.T) { + service := NewService() + + sub := service.Subscribe("a") + service.Publish("a", 1) + + select { + case v := <-sub.Wait(): + if v != 1 { + t.Error("expected subscribed value 1, but got ", v) + } + default: + t.Fail() + } + + sub.Close() + service.Publish("a", 2) + + select { + case <-sub.Wait(): + t.Fail() + default: + } + + service.Cleanup() +} diff --git a/subproject/Xray-core-main/common/signal/semaphore/semaphore.go b/subproject/Xray-core-main/common/signal/semaphore/semaphore.go new file mode 100644 index 00000000..8696b148 --- /dev/null +++ b/subproject/Xray-core-main/common/signal/semaphore/semaphore.go @@ -0,0 +1,27 @@ +package semaphore + +// Instance is an implementation of semaphore. +type Instance struct { + token chan struct{} +} + +// New create a new Semaphore with n permits. +func New(n int) *Instance { + s := &Instance{ + token: make(chan struct{}, n), + } + for i := 0; i < n; i++ { + s.token <- struct{}{} + } + return s +} + +// Wait returns a channel for acquiring a permit. +func (s *Instance) Wait() <-chan struct{} { + return s.token +} + +// Signal releases a permit into the semaphore. +func (s *Instance) Signal() { + s.token <- struct{}{} +} diff --git a/subproject/Xray-core-main/common/signal/timer.go b/subproject/Xray-core-main/common/signal/timer.go new file mode 100644 index 00000000..d5b35605 --- /dev/null +++ b/subproject/Xray-core-main/common/signal/timer.go @@ -0,0 +1,85 @@ +package signal + +import ( + "context" + "sync" + "sync/atomic" + "time" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/task" +) + +type ActivityUpdater interface { + Update() +} + +type ActivityTimer struct { + mu sync.RWMutex + updated chan struct{} + checkTask *task.Periodic + onTimeout func() + consumed atomic.Bool + once sync.Once +} + +func (t *ActivityTimer) Update() { + select { + case t.updated <- struct{}{}: + default: + } +} + +func (t *ActivityTimer) check() error { + select { + case <-t.updated: + default: + t.finish() + } + return nil +} + +func (t *ActivityTimer) finish() { + t.once.Do(func() { + t.consumed.Store(true) + t.mu.Lock() + defer t.mu.Unlock() + + common.CloseIfExists(t.checkTask) + t.onTimeout() + }) +} + +func (t *ActivityTimer) SetTimeout(timeout time.Duration) { + if t.consumed.Load() { + return + } + if timeout == 0 { + t.finish() + return + } + + t.mu.Lock() + defer t.mu.Unlock() + // double check, just in case + if t.consumed.Load() { + return + } + newCheckTask := &task.Periodic{ + Interval: timeout, + Execute: t.check, + } + common.CloseIfExists(t.checkTask) + t.checkTask = newCheckTask + t.Update() + common.Must(newCheckTask.Start()) +} + +func CancelAfterInactivity(ctx context.Context, cancel context.CancelFunc, timeout time.Duration) *ActivityTimer { + timer := &ActivityTimer{ + updated: make(chan struct{}, 1), + onTimeout: cancel, + } + timer.SetTimeout(timeout) + return timer +} diff --git a/subproject/Xray-core-main/common/signal/timer_test.go b/subproject/Xray-core-main/common/signal/timer_test.go new file mode 100644 index 00000000..263bec9e --- /dev/null +++ b/subproject/Xray-core-main/common/signal/timer_test.go @@ -0,0 +1,60 @@ +package signal_test + +import ( + "context" + "runtime" + "testing" + "time" + + . "github.com/xtls/xray-core/common/signal" +) + +func TestActivityTimer(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + timer := CancelAfterInactivity(ctx, cancel, time.Second*4) + time.Sleep(time.Second * 6) + if ctx.Err() == nil { + t.Error("expected some error, but got nil") + } + runtime.KeepAlive(timer) +} + +func TestActivityTimerUpdate(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + timer := CancelAfterInactivity(ctx, cancel, time.Second*10) + time.Sleep(time.Second * 3) + if ctx.Err() != nil { + t.Error("expected nil, but got ", ctx.Err().Error()) + } + timer.SetTimeout(time.Second * 1) + time.Sleep(time.Second * 2) + if ctx.Err() == nil { + t.Error("expected some error, but got nil") + } + runtime.KeepAlive(timer) +} + +func TestActivityTimerNonBlocking(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + timer := CancelAfterInactivity(ctx, cancel, 0) + time.Sleep(time.Second * 1) + select { + case <-ctx.Done(): + default: + t.Error("context not done") + } + timer.SetTimeout(0) + timer.SetTimeout(1) + timer.SetTimeout(2) +} + +func TestActivityTimerZeroTimeout(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + timer := CancelAfterInactivity(ctx, cancel, 0) + select { + case <-ctx.Done(): + default: + t.Error("context not done") + } + runtime.KeepAlive(timer) +} diff --git a/subproject/Xray-core-main/common/singbridge/destination.go b/subproject/Xray-core-main/common/singbridge/destination.go new file mode 100644 index 00000000..98aed258 --- /dev/null +++ b/subproject/Xray-core-main/common/singbridge/destination.go @@ -0,0 +1,52 @@ +package singbridge + +import ( + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/xtls/xray-core/common/net" +) + +func ToNetwork(network string) net.Network { + switch N.NetworkName(network) { + case N.NetworkTCP: + return net.Network_TCP + case N.NetworkUDP: + return net.Network_UDP + default: + return net.Network_Unknown + } +} + +func ToDestination(socksaddr M.Socksaddr, network net.Network) net.Destination { + // IsFqdn() implicitly checks if the domain name is valid + if socksaddr.IsFqdn() { + return net.Destination{ + Network: network, + Address: net.DomainAddress(socksaddr.Fqdn), + Port: net.Port(socksaddr.Port), + } + } + + // IsIP() implicitly checks if the IP address is valid + if socksaddr.IsIP() { + return net.Destination{ + Network: network, + Address: net.IPAddress(socksaddr.Addr.AsSlice()), + Port: net.Port(socksaddr.Port), + } + } + + return net.Destination{} +} + +func ToSocksaddr(destination net.Destination) M.Socksaddr { + var addr M.Socksaddr + switch destination.Address.Family() { + case net.AddressFamilyDomain: + addr.Fqdn = destination.Address.Domain() + default: + addr.Addr = M.AddrFromIP(destination.Address.IP()) + } + addr.Port = uint16(destination.Port) + return addr +} diff --git a/subproject/Xray-core-main/common/singbridge/dialer.go b/subproject/Xray-core-main/common/singbridge/dialer.go new file mode 100644 index 00000000..a6b32199 --- /dev/null +++ b/subproject/Xray-core-main/common/singbridge/dialer.go @@ -0,0 +1,64 @@ +package singbridge + +import ( + "context" + "os" + + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/net/cnc" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/proxy" + "github.com/xtls/xray-core/transport" + "github.com/xtls/xray-core/transport/internet" + "github.com/xtls/xray-core/transport/pipe" +) + +var _ N.Dialer = (*XrayDialer)(nil) + +type XrayDialer struct { + internet.Dialer +} + +func NewDialer(dialer internet.Dialer) *XrayDialer { + return &XrayDialer{dialer} +} + +func (d *XrayDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + return d.Dialer.Dial(ctx, ToDestination(destination, ToNetwork(network))) +} + +func (d *XrayDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + return nil, os.ErrInvalid +} + +type XrayOutboundDialer struct { + outbound proxy.Outbound + dialer internet.Dialer +} + +func NewOutboundDialer(outbound proxy.Outbound, dialer internet.Dialer) *XrayOutboundDialer { + return &XrayOutboundDialer{outbound, dialer} +} + +func (d *XrayOutboundDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + outbounds := session.OutboundsFromContext(ctx) + if len(outbounds) == 0 { + outbounds = []*session.Outbound{{}} + ctx = session.ContextWithOutbounds(ctx, outbounds) + } + ob := outbounds[len(outbounds)-1] + ob.Target = ToDestination(destination, ToNetwork(network)) + + opts := []pipe.Option{pipe.WithSizeLimit(64 * 1024)} + uplinkReader, uplinkWriter := pipe.New(opts...) + downlinkReader, downlinkWriter := pipe.New(opts...) + conn := cnc.NewConnection(cnc.ConnectionInputMulti(downlinkWriter), cnc.ConnectionOutputMulti(uplinkReader)) + go d.outbound.Process(ctx, &transport.Link{Reader: downlinkReader, Writer: uplinkWriter}, d.dialer) + return conn, nil +} + +func (d *XrayOutboundDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + return nil, os.ErrInvalid +} diff --git a/subproject/Xray-core-main/common/singbridge/error.go b/subproject/Xray-core-main/common/singbridge/error.go new file mode 100644 index 00000000..ac9e6351 --- /dev/null +++ b/subproject/Xray-core-main/common/singbridge/error.go @@ -0,0 +1,10 @@ +package singbridge + +import E "github.com/sagernet/sing/common/exceptions" + +func ReturnError(err error) error { + if E.IsClosedOrCanceled(err) { + return nil + } + return err +} diff --git a/subproject/Xray-core-main/common/singbridge/handler.go b/subproject/Xray-core-main/common/singbridge/handler.go new file mode 100644 index 00000000..f200075c --- /dev/null +++ b/subproject/Xray-core-main/common/singbridge/handler.go @@ -0,0 +1,50 @@ +package singbridge + +import ( + "context" + "io" + + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/features/routing" + "github.com/xtls/xray-core/transport" +) + +var ( + _ N.TCPConnectionHandler = (*Dispatcher)(nil) + _ N.UDPConnectionHandler = (*Dispatcher)(nil) +) + +type Dispatcher struct { + upstream routing.Dispatcher + newErrorFunc func(values ...any) *errors.Error +} + +func NewDispatcher(dispatcher routing.Dispatcher, newErrorFunc func(values ...any) *errors.Error) *Dispatcher { + return &Dispatcher{ + upstream: dispatcher, + newErrorFunc: newErrorFunc, + } +} + +func (d *Dispatcher) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { + xConn := NewConn(conn) + return d.upstream.DispatchLink(ctx, ToDestination(metadata.Destination, net.Network_TCP), &transport.Link{ + Reader: xConn, + Writer: xConn, + }) +} + +func (d *Dispatcher) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error { + return d.upstream.DispatchLink(ctx, ToDestination(metadata.Destination, net.Network_UDP), &transport.Link{ + Reader: buf.NewPacketReader(conn.(io.Reader)), + Writer: buf.NewWriter(conn.(io.Writer)), + }) +} + +func (d *Dispatcher) NewError(ctx context.Context, err error) { + errors.LogInfo(ctx, err.Error()) +} diff --git a/subproject/Xray-core-main/common/singbridge/logger.go b/subproject/Xray-core-main/common/singbridge/logger.go new file mode 100644 index 00000000..16ff29cc --- /dev/null +++ b/subproject/Xray-core-main/common/singbridge/logger.go @@ -0,0 +1,70 @@ +package singbridge + +import ( + "context" + + "github.com/sagernet/sing/common/logger" + "github.com/xtls/xray-core/common/errors" +) + +var _ logger.ContextLogger = (*XrayLogger)(nil) + +type XrayLogger struct { + newError func(values ...any) *errors.Error +} + +func NewLogger(newErrorFunc func(values ...any) *errors.Error) *XrayLogger { + return &XrayLogger{ + newErrorFunc, + } +} + +func (l *XrayLogger) Trace(args ...any) { +} + +func (l *XrayLogger) Debug(args ...any) { + errors.LogDebug(context.Background(), args...) +} + +func (l *XrayLogger) Info(args ...any) { + errors.LogInfo(context.Background(), args...) +} + +func (l *XrayLogger) Warn(args ...any) { + errors.LogWarning(context.Background(), args...) +} + +func (l *XrayLogger) Error(args ...any) { + errors.LogError(context.Background(), args...) +} + +func (l *XrayLogger) Fatal(args ...any) { +} + +func (l *XrayLogger) Panic(args ...any) { +} + +func (l *XrayLogger) TraceContext(ctx context.Context, args ...any) { +} + +func (l *XrayLogger) DebugContext(ctx context.Context, args ...any) { + errors.LogDebug(ctx, args...) +} + +func (l *XrayLogger) InfoContext(ctx context.Context, args ...any) { + errors.LogInfo(ctx, args...) +} + +func (l *XrayLogger) WarnContext(ctx context.Context, args ...any) { + errors.LogWarning(ctx, args...) +} + +func (l *XrayLogger) ErrorContext(ctx context.Context, args ...any) { + errors.LogError(ctx, args...) +} + +func (l *XrayLogger) FatalContext(ctx context.Context, args ...any) { +} + +func (l *XrayLogger) PanicContext(ctx context.Context, args ...any) { +} diff --git a/subproject/Xray-core-main/common/singbridge/packet.go b/subproject/Xray-core-main/common/singbridge/packet.go new file mode 100644 index 00000000..d4dccb4c --- /dev/null +++ b/subproject/Xray-core-main/common/singbridge/packet.go @@ -0,0 +1,106 @@ +package singbridge + +import ( + "context" + "time" + + B "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/bufio" + M "github.com/sagernet/sing/common/metadata" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/transport" +) + +func CopyPacketConn(ctx context.Context, inboundConn net.Conn, link *transport.Link, destination net.Destination, serverConn net.PacketConn) error { + conn := &PacketConnWrapper{ + Reader: link.Reader, + Writer: link.Writer, + Dest: destination, + Conn: inboundConn, + } + return ReturnError(bufio.CopyPacketConn(ctx, conn, bufio.NewPacketConn(serverConn))) +} + +type PacketConnWrapper struct { + buf.Reader + buf.Writer + net.Conn + Dest net.Destination + cached buf.MultiBuffer +} + +// This ReadPacket implemented a timeout to avoid goroutine leak like PipeConnWrapper.Read() +// as a temporarily solution +func (w *PacketConnWrapper) ReadPacket(buffer *B.Buffer) (M.Socksaddr, error) { + if w.cached != nil { + mb, bb := buf.SplitFirst(w.cached) + if bb == nil { + w.cached = nil + } else { + buffer.Write(bb.Bytes()) + w.cached = mb + var destination net.Destination + if bb.UDP != nil { + destination = *bb.UDP + } else { + destination = w.Dest + } + bb.Release() + return ToSocksaddr(destination), nil + } + } + + // timeout + type readResult struct { + mb buf.MultiBuffer + err error + } + c := make(chan readResult, 1) + go func() { + mb, err := w.ReadMultiBuffer() + c <- readResult{mb: mb, err: err} + }() + var mb buf.MultiBuffer + select { + case <-time.After(60 * time.Second): + common.Close(w.Reader) + common.Interrupt(w.Reader) + return M.Socksaddr{}, buf.ErrReadTimeout + case result := <-c: + if result.err != nil { + return M.Socksaddr{}, result.err + } + mb = result.mb + } + + nb, bb := buf.SplitFirst(mb) + if bb == nil { + return M.Socksaddr{}, nil + } else { + buffer.Write(bb.Bytes()) + w.cached = nb + var destination net.Destination + if bb.UDP != nil { + destination = *bb.UDP + } else { + destination = w.Dest + } + bb.Release() + return ToSocksaddr(destination), nil + } +} + +func (w *PacketConnWrapper) WritePacket(buffer *B.Buffer, destination M.Socksaddr) error { + vBuf := buf.New() + vBuf.Write(buffer.Bytes()) + endpoint := ToDestination(destination, net.Network_UDP) + vBuf.UDP = &endpoint + return w.Writer.WriteMultiBuffer(buf.MultiBuffer{vBuf}) +} + +func (w *PacketConnWrapper) Close() error { + buf.ReleaseMulti(w.cached) + return nil +} diff --git a/subproject/Xray-core-main/common/singbridge/pipe.go b/subproject/Xray-core-main/common/singbridge/pipe.go new file mode 100644 index 00000000..d6c0f3d8 --- /dev/null +++ b/subproject/Xray-core-main/common/singbridge/pipe.go @@ -0,0 +1,81 @@ +package singbridge + +import ( + "context" + "io" + "net" + "time" + + "github.com/sagernet/sing/common/bufio" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/transport" +) + +func CopyConn(ctx context.Context, inboundConn net.Conn, link *transport.Link, serverConn net.Conn) error { + conn := &PipeConnWrapper{ + W: link.Writer, + Conn: inboundConn, + } + if ir, ok := link.Reader.(io.Reader); ok { + conn.R = ir + } else { + conn.R = &buf.BufferedReader{Reader: link.Reader} + } + return ReturnError(bufio.CopyConn(ctx, conn, serverConn)) +} + +type PipeConnWrapper struct { + R io.Reader + W buf.Writer + net.Conn +} + +func (w *PipeConnWrapper) Close() error { + return nil +} + +// This Read implemented a timeout to avoid goroutine leak. +// as a temporarily solution +func (w *PipeConnWrapper) Read(b []byte) (n int, err error) { + type readResult struct { + n int + err error + } + c := make(chan readResult, 1) + go func() { + n, err := w.R.Read(b) + c <- readResult{n: n, err: err} + }() + select { + case result := <-c: + return result.n, result.err + case <-time.After(300 * time.Second): + common.Close(w.R) + common.Interrupt(w.R) + return 0, buf.ErrReadTimeout + } +} + +func (w *PipeConnWrapper) Write(p []byte) (n int, err error) { + n = len(p) + var mb buf.MultiBuffer + pLen := len(p) + for pLen > 0 { + buffer := buf.New() + if pLen > buf.Size { + _, err = buffer.Write(p[:buf.Size]) + p = p[buf.Size:] + } else { + buffer.Write(p) + } + pLen -= int(buffer.Len()) + mb = append(mb, buffer) + } + err = w.W.WriteMultiBuffer(mb) + if err != nil { + n = 0 + buf.ReleaseMulti(mb) + } + return +} diff --git a/subproject/Xray-core-main/common/singbridge/reader.go b/subproject/Xray-core-main/common/singbridge/reader.go new file mode 100644 index 00000000..1ace1845 --- /dev/null +++ b/subproject/Xray-core-main/common/singbridge/reader.go @@ -0,0 +1,66 @@ +package singbridge + +import ( + "time" + + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/bufio" + N "github.com/sagernet/sing/common/network" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/net" +) + +var ( + _ buf.Reader = (*Conn)(nil) + _ buf.TimeoutReader = (*Conn)(nil) + _ buf.Writer = (*Conn)(nil) +) + +type Conn struct { + net.Conn + writer N.VectorisedWriter +} + +func NewConn(conn net.Conn) *Conn { + writer, _ := bufio.CreateVectorisedWriter(conn) + return &Conn{ + Conn: conn, + writer: writer, + } +} + +func (c *Conn) ReadMultiBuffer() (buf.MultiBuffer, error) { + buffer, err := buf.ReadBuffer(c.Conn) + if err != nil { + return nil, err + } + return buf.MultiBuffer{buffer}, nil +} + +func (c *Conn) ReadMultiBufferTimeout(duration time.Duration) (buf.MultiBuffer, error) { + err := c.SetReadDeadline(time.Now().Add(duration)) + if err != nil { + return nil, err + } + defer c.SetReadDeadline(time.Time{}) + return c.ReadMultiBuffer() +} + +func (c *Conn) WriteMultiBuffer(bufferList buf.MultiBuffer) error { + defer buf.ReleaseMulti(bufferList) + if c.writer != nil { + bytesList := make([][]byte, len(bufferList)) + for i, buffer := range bufferList { + bytesList[i] = buffer.Bytes() + } + return common.Error(bufio.WriteVectorised(c.writer, bytesList)) + } + // Since this conn is only used by tun, we don't force buffer writes to merge. + for _, buffer := range bufferList { + _, err := c.Conn.Write(buffer.Bytes()) + if err != nil { + return err + } + } + return nil +} diff --git a/subproject/Xray-core-main/common/strmatcher/ac_automaton_matcher.go b/subproject/Xray-core-main/common/strmatcher/ac_automaton_matcher.go new file mode 100644 index 00000000..7844333d --- /dev/null +++ b/subproject/Xray-core-main/common/strmatcher/ac_automaton_matcher.go @@ -0,0 +1,247 @@ +package strmatcher + +import ( + "container/list" +) + +const validCharCount = 53 + +type MatchType struct { + Type Type + Exist bool +} + +const ( + TrieEdge bool = true + FailEdge bool = false +) + +type Edge struct { + Type bool + NextNode int +} + +type ACAutomaton struct { + Trie [][validCharCount]Edge + Fail []int + Exists []MatchType + Count int +} + +func newNode() [validCharCount]Edge { + var s [validCharCount]Edge + for i := range s { + s[i] = Edge{ + Type: FailEdge, + NextNode: 0, + } + } + return s +} + +var char2Index = []int{ + 'A': 0, + 'a': 0, + 'B': 1, + 'b': 1, + 'C': 2, + 'c': 2, + 'D': 3, + 'd': 3, + 'E': 4, + 'e': 4, + 'F': 5, + 'f': 5, + 'G': 6, + 'g': 6, + 'H': 7, + 'h': 7, + 'I': 8, + 'i': 8, + 'J': 9, + 'j': 9, + 'K': 10, + 'k': 10, + 'L': 11, + 'l': 11, + 'M': 12, + 'm': 12, + 'N': 13, + 'n': 13, + 'O': 14, + 'o': 14, + 'P': 15, + 'p': 15, + 'Q': 16, + 'q': 16, + 'R': 17, + 'r': 17, + 'S': 18, + 's': 18, + 'T': 19, + 't': 19, + 'U': 20, + 'u': 20, + 'V': 21, + 'v': 21, + 'W': 22, + 'w': 22, + 'X': 23, + 'x': 23, + 'Y': 24, + 'y': 24, + 'Z': 25, + 'z': 25, + '!': 26, + '$': 27, + '&': 28, + '\'': 29, + '(': 30, + ')': 31, + '*': 32, + '+': 33, + ',': 34, + ';': 35, + '=': 36, + ':': 37, + '%': 38, + '-': 39, + '.': 40, + '_': 41, + '~': 42, + '0': 43, + '1': 44, + '2': 45, + '3': 46, + '4': 47, + '5': 48, + '6': 49, + '7': 50, + '8': 51, + '9': 52, +} + +func NewACAutomaton() *ACAutomaton { + ac := new(ACAutomaton) + ac.Trie = append(ac.Trie, newNode()) + ac.Fail = append(ac.Fail, 0) + ac.Exists = append(ac.Exists, MatchType{ + Type: Full, + Exist: false, + }) + return ac +} + +func (ac *ACAutomaton) Add(domain string, t Type) { + node := 0 + for i := len(domain) - 1; i >= 0; i-- { + idx := char2Index[domain[i]] + if ac.Trie[node][idx].NextNode == 0 { + ac.Count++ + if len(ac.Trie) < ac.Count+1 { + ac.Trie = append(ac.Trie, newNode()) + ac.Fail = append(ac.Fail, 0) + ac.Exists = append(ac.Exists, MatchType{ + Type: Full, + Exist: false, + }) + } + ac.Trie[node][idx] = Edge{ + Type: TrieEdge, + NextNode: ac.Count, + } + } + node = ac.Trie[node][idx].NextNode + } + ac.Exists[node] = MatchType{ + Type: t, + Exist: true, + } + switch t { + case Domain: + ac.Exists[node] = MatchType{ + Type: Full, + Exist: true, + } + idx := char2Index['.'] + if ac.Trie[node][idx].NextNode == 0 { + ac.Count++ + if len(ac.Trie) < ac.Count+1 { + ac.Trie = append(ac.Trie, newNode()) + ac.Fail = append(ac.Fail, 0) + ac.Exists = append(ac.Exists, MatchType{ + Type: Full, + Exist: false, + }) + } + ac.Trie[node][idx] = Edge{ + Type: TrieEdge, + NextNode: ac.Count, + } + } + node = ac.Trie[node][idx].NextNode + ac.Exists[node] = MatchType{ + Type: t, + Exist: true, + } + default: + break + } +} + +func (ac *ACAutomaton) Build() { + queue := list.New() + for i := 0; i < validCharCount; i++ { + if ac.Trie[0][i].NextNode != 0 { + queue.PushBack(ac.Trie[0][i]) + } + } + for { + front := queue.Front() + if front == nil { + break + } else { + node := front.Value.(Edge).NextNode + queue.Remove(front) + for i := 0; i < validCharCount; i++ { + if ac.Trie[node][i].NextNode != 0 { + ac.Fail[ac.Trie[node][i].NextNode] = ac.Trie[ac.Fail[node]][i].NextNode + queue.PushBack(ac.Trie[node][i]) + } else { + ac.Trie[node][i] = Edge{ + Type: FailEdge, + NextNode: ac.Trie[ac.Fail[node]][i].NextNode, + } + } + } + } + } +} + +func (ac *ACAutomaton) Match(s string) bool { + node := 0 + fullMatch := true + // 1. the match string is all through trie edge. FULL MATCH or DOMAIN + // 2. the match string is through a fail edge. NOT FULL MATCH + // 2.1 Through a fail edge, but there exists a valid node. SUBSTR + for i := len(s) - 1; i >= 0; i-- { + chr := int(s[i]) + if chr >= len(char2Index) { + return false + } + idx := char2Index[chr] + fullMatch = fullMatch && ac.Trie[node][idx].Type + node = ac.Trie[node][idx].NextNode + switch ac.Exists[node].Type { + case Substr: + return true + case Domain: + if fullMatch { + return true + } + default: + break + } + } + return fullMatch && ac.Exists[node].Exist +} diff --git a/subproject/Xray-core-main/common/strmatcher/benchmark_test.go b/subproject/Xray-core-main/common/strmatcher/benchmark_test.go new file mode 100644 index 00000000..972570ce --- /dev/null +++ b/subproject/Xray-core-main/common/strmatcher/benchmark_test.go @@ -0,0 +1,62 @@ +package strmatcher_test + +import ( + "strconv" + "testing" + + "github.com/xtls/xray-core/common" + . "github.com/xtls/xray-core/common/strmatcher" +) + +func BenchmarkACAutomaton(b *testing.B) { + ac := NewACAutomaton() + for i := 1; i <= 1024; i++ { + ac.Add(strconv.Itoa(i)+".xray.com", Domain) + } + ac.Build() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = ac.Match("0.xray.com") + } +} + +func BenchmarkDomainMatcherGroup(b *testing.B) { + g := new(DomainMatcherGroup) + + for i := 1; i <= 1024; i++ { + g.Add(strconv.Itoa(i)+".example.com", uint32(i)) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = g.Match("0.example.com") + } +} + +func BenchmarkFullMatcherGroup(b *testing.B) { + g := new(FullMatcherGroup) + + for i := 1; i <= 1024; i++ { + g.Add(strconv.Itoa(i)+".example.com", uint32(i)) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = g.Match("0.example.com") + } +} + +func BenchmarkMarchGroup(b *testing.B) { + g := new(MatcherGroup) + for i := 1; i <= 1024; i++ { + m, err := Domain.New(strconv.Itoa(i) + ".example.com") + common.Must(err) + g.Add(m) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = g.Match("0.example.com") + } +} diff --git a/subproject/Xray-core-main/common/strmatcher/domain_matcher.go b/subproject/Xray-core-main/common/strmatcher/domain_matcher.go new file mode 100644 index 00000000..ae8e65bc --- /dev/null +++ b/subproject/Xray-core-main/common/strmatcher/domain_matcher.go @@ -0,0 +1,98 @@ +package strmatcher + +import "strings" + +func breakDomain(domain string) []string { + return strings.Split(domain, ".") +} + +type node struct { + values []uint32 + sub map[string]*node +} + +// DomainMatcherGroup is a IndexMatcher for a large set of Domain matchers. +// Visible for testing only. +type DomainMatcherGroup struct { + root *node +} + +func (g *DomainMatcherGroup) Add(domain string, value uint32) { + if g.root == nil { + g.root = new(node) + } + + current := g.root + parts := breakDomain(domain) + for i := len(parts) - 1; i >= 0; i-- { + part := parts[i] + if current.sub == nil { + current.sub = make(map[string]*node) + } + next := current.sub[part] + if next == nil { + next = new(node) + current.sub[part] = next + } + current = next + } + + current.values = append(current.values, value) +} + +func (g *DomainMatcherGroup) addMatcher(m domainMatcher, value uint32) { + g.Add(string(m), value) +} + +func (g *DomainMatcherGroup) Match(domain string) []uint32 { + if domain == "" { + return nil + } + + current := g.root + if current == nil { + return nil + } + + nextPart := func(idx int) int { + for i := idx - 1; i >= 0; i-- { + if domain[i] == '.' { + return i + } + } + return -1 + } + + matches := [][]uint32{} + idx := len(domain) + for { + if idx == -1 || current.sub == nil { + break + } + + nidx := nextPart(idx) + part := domain[nidx+1 : idx] + next := current.sub[part] + if next == nil { + break + } + current = next + idx = nidx + if len(current.values) > 0 { + matches = append(matches, current.values) + } + } + switch len(matches) { + case 0: + return nil + case 1: + return matches[0] + default: + result := []uint32{} + for idx := range matches { + // Insert reversely, the subdomain that matches further ranks higher + result = append(result, matches[len(matches)-1-idx]...) + } + return result + } +} diff --git a/subproject/Xray-core-main/common/strmatcher/domain_matcher_test.go b/subproject/Xray-core-main/common/strmatcher/domain_matcher_test.go new file mode 100644 index 00000000..5a8ca35b --- /dev/null +++ b/subproject/Xray-core-main/common/strmatcher/domain_matcher_test.go @@ -0,0 +1,76 @@ +package strmatcher_test + +import ( + "reflect" + "testing" + + . "github.com/xtls/xray-core/common/strmatcher" +) + +func TestDomainMatcherGroup(t *testing.T) { + g := new(DomainMatcherGroup) + g.Add("example.com", 1) + g.Add("google.com", 2) + g.Add("x.a.com", 3) + g.Add("a.b.com", 4) + g.Add("c.a.b.com", 5) + g.Add("x.y.com", 4) + g.Add("x.y.com", 6) + + testCases := []struct { + Domain string + Result []uint32 + }{ + { + Domain: "x.example.com", + Result: []uint32{1}, + }, + { + Domain: "y.com", + Result: nil, + }, + { + Domain: "a.b.com", + Result: []uint32{4}, + }, + { // Matches [c.a.b.com, a.b.com] + Domain: "c.a.b.com", + Result: []uint32{5, 4}, + }, + { + Domain: "c.a..b.com", + Result: nil, + }, + { + Domain: ".com", + Result: nil, + }, + { + Domain: "com", + Result: nil, + }, + { + Domain: "", + Result: nil, + }, + { + Domain: "x.y.com", + Result: []uint32{4, 6}, + }, + } + + for _, testCase := range testCases { + r := g.Match(testCase.Domain) + if !reflect.DeepEqual(r, testCase.Result) { + t.Error("Failed to match domain: ", testCase.Domain, ", expect ", testCase.Result, ", but got ", r) + } + } +} + +func TestEmptyDomainMatcherGroup(t *testing.T) { + g := new(DomainMatcherGroup) + r := g.Match("example.com") + if len(r) != 0 { + t.Error("Expect [], but ", r) + } +} diff --git a/subproject/Xray-core-main/common/strmatcher/full_matcher.go b/subproject/Xray-core-main/common/strmatcher/full_matcher.go new file mode 100644 index 00000000..e00d02aa --- /dev/null +++ b/subproject/Xray-core-main/common/strmatcher/full_matcher.go @@ -0,0 +1,25 @@ +package strmatcher + +type FullMatcherGroup struct { + matchers map[string][]uint32 +} + +func (g *FullMatcherGroup) Add(domain string, value uint32) { + if g.matchers == nil { + g.matchers = make(map[string][]uint32) + } + + g.matchers[domain] = append(g.matchers[domain], value) +} + +func (g *FullMatcherGroup) addMatcher(m fullMatcher, value uint32) { + g.Add(string(m), value) +} + +func (g *FullMatcherGroup) Match(str string) []uint32 { + if g.matchers == nil { + return nil + } + + return g.matchers[str] +} diff --git a/subproject/Xray-core-main/common/strmatcher/full_matcher_test.go b/subproject/Xray-core-main/common/strmatcher/full_matcher_test.go new file mode 100644 index 00000000..73d60d51 --- /dev/null +++ b/subproject/Xray-core-main/common/strmatcher/full_matcher_test.go @@ -0,0 +1,50 @@ +package strmatcher_test + +import ( + "reflect" + "testing" + + . "github.com/xtls/xray-core/common/strmatcher" +) + +func TestFullMatcherGroup(t *testing.T) { + g := new(FullMatcherGroup) + g.Add("example.com", 1) + g.Add("google.com", 2) + g.Add("x.a.com", 3) + g.Add("x.y.com", 4) + g.Add("x.y.com", 6) + + testCases := []struct { + Domain string + Result []uint32 + }{ + { + Domain: "example.com", + Result: []uint32{1}, + }, + { + Domain: "y.com", + Result: nil, + }, + { + Domain: "x.y.com", + Result: []uint32{4, 6}, + }, + } + + for _, testCase := range testCases { + r := g.Match(testCase.Domain) + if !reflect.DeepEqual(r, testCase.Result) { + t.Error("Failed to match domain: ", testCase.Domain, ", expect ", testCase.Result, ", but got ", r) + } + } +} + +func TestEmptyFullMatcherGroup(t *testing.T) { + g := new(FullMatcherGroup) + r := g.Match("example.com") + if len(r) != 0 { + t.Error("Expect [], but ", r) + } +} diff --git a/subproject/Xray-core-main/common/strmatcher/matchers.go b/subproject/Xray-core-main/common/strmatcher/matchers.go new file mode 100644 index 00000000..915927db --- /dev/null +++ b/subproject/Xray-core-main/common/strmatcher/matchers.go @@ -0,0 +1,56 @@ +package strmatcher + +import ( + "regexp" + "strings" +) + +type fullMatcher string + +func (m fullMatcher) Match(s string) bool { + return string(m) == s +} + +func (m fullMatcher) String() string { + return "full:" + string(m) +} + +type substrMatcher string + +func (m substrMatcher) Match(s string) bool { + return strings.Contains(s, string(m)) +} + +func (m substrMatcher) String() string { + return "keyword:" + string(m) +} + +type domainMatcher string + +func (m domainMatcher) Match(s string) bool { + pattern := string(m) + if !strings.HasSuffix(s, pattern) { + return false + } + return len(s) == len(pattern) || s[len(s)-len(pattern)-1] == '.' +} + +func (m domainMatcher) String() string { + return "domain:" + string(m) +} + +type RegexMatcher struct { + Pattern string + reg *regexp.Regexp +} + +func (m *RegexMatcher) Match(s string) bool { + if m.reg == nil { + m.reg = regexp.MustCompile(m.Pattern) + } + return m.reg.MatchString(s) +} + +func (m *RegexMatcher) String() string { + return "regexp:" + m.Pattern +} diff --git a/subproject/Xray-core-main/common/strmatcher/matchers_test.go b/subproject/Xray-core-main/common/strmatcher/matchers_test.go new file mode 100644 index 00000000..d39c522c --- /dev/null +++ b/subproject/Xray-core-main/common/strmatcher/matchers_test.go @@ -0,0 +1,73 @@ +package strmatcher_test + +import ( + "testing" + + "github.com/xtls/xray-core/common" + . "github.com/xtls/xray-core/common/strmatcher" +) + +func TestMatcher(t *testing.T) { + cases := []struct { + pattern string + mType Type + input string + output bool + }{ + { + pattern: "example.com", + mType: Domain, + input: "www.example.com", + output: true, + }, + { + pattern: "example.com", + mType: Domain, + input: "example.com", + output: true, + }, + { + pattern: "example.com", + mType: Domain, + input: "www.fxample.com", + output: false, + }, + { + pattern: "example.com", + mType: Domain, + input: "xample.com", + output: false, + }, + { + pattern: "example.com", + mType: Domain, + input: "xexample.com", + output: false, + }, + { + pattern: "example.com", + mType: Full, + input: "example.com", + output: true, + }, + { + pattern: "example.com", + mType: Full, + input: "xexample.com", + output: false, + }, + { + pattern: "example.com", + mType: Regex, + input: "examplexcom", + output: true, + }, + } + for _, test := range cases { + matcher, err := test.mType.New(test.pattern) + common.Must(err) + if m := matcher.Match(test.input); m != test.output { + t.Error("unexpected output: ", m, " for test case ", test) + } + } +} diff --git a/subproject/Xray-core-main/common/strmatcher/mph_matcher.go b/subproject/Xray-core-main/common/strmatcher/mph_matcher.go new file mode 100644 index 00000000..ff3dea65 --- /dev/null +++ b/subproject/Xray-core-main/common/strmatcher/mph_matcher.go @@ -0,0 +1,308 @@ +package strmatcher + +import ( + "math/bits" + "regexp" + "sort" + "strings" + "unsafe" +) + +// PrimeRK is the prime base used in Rabin-Karp algorithm. +const PrimeRK = 16777619 + +// calculate the rolling murmurHash of given string +func RollingHash(s string) uint32 { + h := uint32(0) + for i := len(s) - 1; i >= 0; i-- { + h = h*PrimeRK + uint32(s[i]) + } + return h +} + +// A MphMatcherGroup is divided into three parts: +// 1. `full` and `domain` patterns are matched by Rabin-Karp algorithm and minimal perfect hash table; +// 2. `substr` patterns are matched by ac automaton; +// 3. `regex` patterns are matched with the regex library. +type MphMatcherGroup struct { + Ac *ACAutomaton + OtherMatchers []MatcherEntry + Rules []string + Level0 []uint32 + Level0Mask int + Level1 []uint32 + Level1Mask int + Count uint32 + RuleMap *map[string]uint32 +} + +func (g *MphMatcherGroup) AddFullOrDomainPattern(pattern string, t Type) { + h := RollingHash(pattern) + switch t { + case Domain: + (*g.RuleMap)["."+pattern] = h*PrimeRK + uint32('.') + fallthrough + case Full: + (*g.RuleMap)[pattern] = h + default: + } +} + +func NewMphMatcherGroup() *MphMatcherGroup { + return &MphMatcherGroup{ + Ac: nil, + OtherMatchers: nil, + Rules: nil, + Level0: nil, + Level0Mask: 0, + Level1: nil, + Level1Mask: 0, + Count: 1, + RuleMap: &map[string]uint32{}, + } +} + +// AddPattern adds a pattern to MphMatcherGroup +func (g *MphMatcherGroup) AddPattern(pattern string, t Type) (uint32, error) { + switch t { + case Substr: + if g.Ac == nil { + g.Ac = NewACAutomaton() + } + g.Ac.Add(pattern, t) + case Full, Domain: + pattern = strings.ToLower(pattern) + g.AddFullOrDomainPattern(pattern, t) + case Regex: + r, err := regexp.Compile(pattern) + if err != nil { + return 0, err + } + g.OtherMatchers = append(g.OtherMatchers, MatcherEntry{ + M: &RegexMatcher{Pattern: pattern, reg: r}, + Id: g.Count, + }) + default: + panic("Unknown type") + } + return g.Count, nil +} + +// Build builds a minimal perfect hash table and ac automaton from insert rules +func (g *MphMatcherGroup) Build() { + if g.Ac != nil { + g.Ac.Build() + } + keyLen := len(*g.RuleMap) + if keyLen == 0 { + keyLen = 1 + (*g.RuleMap)["empty___"] = RollingHash("empty___") + } + g.Level0 = make([]uint32, nextPow2(keyLen/4)) + g.Level0Mask = len(g.Level0) - 1 + g.Level1 = make([]uint32, nextPow2(keyLen)) + g.Level1Mask = len(g.Level1) - 1 + sparseBuckets := make([][]int, len(g.Level0)) + var ruleIdx int + for rule, hash := range *g.RuleMap { + n := int(hash) & g.Level0Mask + g.Rules = append(g.Rules, rule) + sparseBuckets[n] = append(sparseBuckets[n], ruleIdx) + ruleIdx++ + } + g.RuleMap = nil + var buckets []indexBucket + for n, vals := range sparseBuckets { + if len(vals) > 0 { + buckets = append(buckets, indexBucket{n, vals}) + } + } + sort.Sort(bySize(buckets)) + + occ := make([]bool, len(g.Level1)) + var tmpOcc []int + for _, bucket := range buckets { + seed := uint32(0) + for { + findSeed := true + tmpOcc = tmpOcc[:0] + for _, i := range bucket.vals { + n := int(strhashFallback(unsafe.Pointer(&g.Rules[i]), uintptr(seed))) & g.Level1Mask + if occ[n] { + for _, n := range tmpOcc { + occ[n] = false + } + seed++ + findSeed = false + break + } + occ[n] = true + tmpOcc = append(tmpOcc, n) + g.Level1[n] = uint32(i) + } + if findSeed { + g.Level0[bucket.n] = seed + break + } + } + } +} + +func nextPow2(v int) int { + if v <= 1 { + return 1 + } + const MaxUInt = ^uint(0) + n := (MaxUInt >> bits.LeadingZeros(uint(v))) + 1 + return int(n) +} + +// Lookup searches for s in t and returns its index and whether it was found. +func (g *MphMatcherGroup) Lookup(h uint32, s string) bool { + i0 := int(h) & g.Level0Mask + seed := g.Level0[i0] + i1 := int(strhashFallback(unsafe.Pointer(&s), uintptr(seed))) & g.Level1Mask + n := g.Level1[i1] + return s == g.Rules[int(n)] +} + +// Match implements IndexMatcher.Match. +func (g *MphMatcherGroup) Match(pattern string) []uint32 { + result := []uint32{} + hash := uint32(0) + for i := len(pattern) - 1; i >= 0; i-- { + hash = hash*PrimeRK + uint32(pattern[i]) + if pattern[i] == '.' { + if g.Lookup(hash, pattern[i:]) { + result = append(result, 1) + return result + } + } + } + if g.Lookup(hash, pattern) { + result = append(result, 1) + return result + } + if g.Ac != nil && g.Ac.Match(pattern) { + result = append(result, 1) + return result + } + for _, e := range g.OtherMatchers { + if e.M.Match(pattern) { + result = append(result, e.Id) + return result + } + } + return nil +} + +type indexBucket struct { + n int + vals []int +} + +type bySize []indexBucket + +func (s bySize) Len() int { return len(s) } +func (s bySize) Less(i, j int) bool { return len(s[i].vals) > len(s[j].vals) } +func (s bySize) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +type stringStruct struct { + str unsafe.Pointer + len int +} + +func strhashFallback(a unsafe.Pointer, h uintptr) uintptr { + x := (*stringStruct)(a) + return memhashFallback(x.str, h, uintptr(x.len)) +} + +const ( + // Constants for multiplication: four random odd 64-bit numbers. + m1 = 16877499708836156737 + m2 = 2820277070424839065 + m3 = 9497967016996688599 + m4 = 15839092249703872147 +) + +var hashkey = [4]uintptr{1, 1, 1, 1} + +func memhashFallback(p unsafe.Pointer, seed, s uintptr) uintptr { + h := uint64(seed + s*hashkey[0]) +tail: + switch { + case s == 0: + case s < 4: + h ^= uint64(*(*byte)(p)) + h ^= uint64(*(*byte)(add(p, s>>1))) << 8 + h ^= uint64(*(*byte)(add(p, s-1))) << 16 + h = rotl31(h*m1) * m2 + case s <= 8: + h ^= uint64(readUnaligned32(p)) + h ^= uint64(readUnaligned32(add(p, s-4))) << 32 + h = rotl31(h*m1) * m2 + case s <= 16: + h ^= readUnaligned64(p) + h = rotl31(h*m1) * m2 + h ^= readUnaligned64(add(p, s-8)) + h = rotl31(h*m1) * m2 + case s <= 32: + h ^= readUnaligned64(p) + h = rotl31(h*m1) * m2 + h ^= readUnaligned64(add(p, 8)) + h = rotl31(h*m1) * m2 + h ^= readUnaligned64(add(p, s-16)) + h = rotl31(h*m1) * m2 + h ^= readUnaligned64(add(p, s-8)) + h = rotl31(h*m1) * m2 + default: + v1 := h + v2 := uint64(seed * hashkey[1]) + v3 := uint64(seed * hashkey[2]) + v4 := uint64(seed * hashkey[3]) + for s >= 32 { + v1 ^= readUnaligned64(p) + v1 = rotl31(v1*m1) * m2 + p = add(p, 8) + v2 ^= readUnaligned64(p) + v2 = rotl31(v2*m2) * m3 + p = add(p, 8) + v3 ^= readUnaligned64(p) + v3 = rotl31(v3*m3) * m4 + p = add(p, 8) + v4 ^= readUnaligned64(p) + v4 = rotl31(v4*m4) * m1 + p = add(p, 8) + s -= 32 + } + h = v1 ^ v2 ^ v3 ^ v4 + goto tail + } + + h ^= h >> 29 + h *= m3 + h ^= h >> 32 + return uintptr(h) +} + +func add(p unsafe.Pointer, x uintptr) unsafe.Pointer { + return unsafe.Pointer(uintptr(p) + x) +} + +func readUnaligned32(p unsafe.Pointer) uint32 { + q := (*[4]byte)(p) + return uint32(q[0]) | uint32(q[1])<<8 | uint32(q[2])<<16 | uint32(q[3])<<24 +} + +func rotl31(x uint64) uint64 { + return (x << 31) | (x >> (64 - 31)) +} + +func readUnaligned64(p unsafe.Pointer) uint64 { + q := (*[8]byte)(p) + return uint64(q[0]) | uint64(q[1])<<8 | uint64(q[2])<<16 | uint64(q[3])<<24 | uint64(q[4])<<32 | uint64(q[5])<<40 | uint64(q[6])<<48 | uint64(q[7])<<56 +} + +func (g *MphMatcherGroup) Size() uint32 { + return g.Count +} diff --git a/subproject/Xray-core-main/common/strmatcher/mph_matcher_compact.go b/subproject/Xray-core-main/common/strmatcher/mph_matcher_compact.go new file mode 100644 index 00000000..a40b9f56 --- /dev/null +++ b/subproject/Xray-core-main/common/strmatcher/mph_matcher_compact.go @@ -0,0 +1,47 @@ +package strmatcher + +import ( + "bytes" + "encoding/gob" + "io" +) + +func init() { + gob.Register(&RegexMatcher{}) + gob.Register(fullMatcher("")) + gob.Register(substrMatcher("")) + gob.Register(domainMatcher("")) +} + +func (g *MphMatcherGroup) Serialize(w io.Writer) error { + data := MphMatcherGroup{ + Ac: g.Ac, + OtherMatchers: g.OtherMatchers, + Rules: g.Rules, + Level0: g.Level0, + Level0Mask: g.Level0Mask, + Level1: g.Level1, + Level1Mask: g.Level1Mask, + Count: g.Count, + } + return gob.NewEncoder(w).Encode(data) +} + +func NewMphMatcherGroupFromBuffer(data []byte) (*MphMatcherGroup, error) { + var gData MphMatcherGroup + if err := gob.NewDecoder(bytes.NewReader(data)).Decode(&gData); err != nil { + return nil, err + } + + g := NewMphMatcherGroup() + g.Ac = gData.Ac + g.OtherMatchers = gData.OtherMatchers + g.Rules = gData.Rules + g.Level0 = gData.Level0 + g.Level0Mask = gData.Level0Mask + g.Level1 = gData.Level1 + g.Level1Mask = gData.Level1Mask + g.Count = gData.Count + + return g, nil +} diff --git a/subproject/Xray-core-main/common/strmatcher/strmatcher.go b/subproject/Xray-core-main/common/strmatcher/strmatcher.go new file mode 100644 index 00000000..89e7dae6 --- /dev/null +++ b/subproject/Xray-core-main/common/strmatcher/strmatcher.go @@ -0,0 +1,141 @@ +package strmatcher + +import ( + "errors" + "regexp" +) + +// Matcher is the interface to determine a string matches a pattern. +type Matcher interface { + // Match returns true if the given string matches a predefined pattern. + Match(string) bool + String() string +} + +// Type is the type of the matcher. +type Type byte + +const ( + // Full is the type of matcher that the input string must exactly equal to the pattern. + Full Type = iota + // Substr is the type of matcher that the input string must contain the pattern as a sub-string. + Substr + // Domain is the type of matcher that the input string must be a sub-domain or itself of the pattern. + Domain + // Regex is the type of matcher that the input string must matches the regular-expression pattern. + Regex +) + +// New creates a new Matcher based on the given pattern. +func (t Type) New(pattern string) (Matcher, error) { + // 1. regex matching is case-sensitive + switch t { + case Full: + return fullMatcher(pattern), nil + case Substr: + return substrMatcher(pattern), nil + case Domain: + return domainMatcher(pattern), nil + case Regex: + r, err := regexp.Compile(pattern) + if err != nil { + return nil, err + } + return &RegexMatcher{ + Pattern: pattern, + reg: r, + }, nil + default: + return nil, errors.New("unk type") + } +} + +// IndexMatcher is the interface for matching with a group of matchers. +type IndexMatcher interface { + // Match returns the index of a matcher that matches the input. It returns empty array if no such matcher exists. + Match(input string) []uint32 + // Size returns the number of matchers in the group. + Size() uint32 +} + +type MatcherEntry struct { + M Matcher + Id uint32 +} + +// MatcherGroup is an implementation of IndexMatcher. +// Empty initialization works. +type MatcherGroup struct { + count uint32 + fullMatcher FullMatcherGroup + domainMatcher DomainMatcherGroup + otherMatchers []MatcherEntry +} + +// Add adds a new Matcher into the MatcherGroup, and returns its index. The index will never be 0. +func (g *MatcherGroup) Add(m Matcher) uint32 { + g.count++ + c := g.count + + switch tm := m.(type) { + case fullMatcher: + g.fullMatcher.addMatcher(tm, c) + case domainMatcher: + g.domainMatcher.addMatcher(tm, c) + default: + g.otherMatchers = append(g.otherMatchers, MatcherEntry{ + M: m, + Id: c, + }) + } + + return c +} + +// Match implements IndexMatcher.Match. +func (g *MatcherGroup) Match(pattern string) []uint32 { + result := []uint32{} + result = append(result, g.fullMatcher.Match(pattern)...) + result = append(result, g.domainMatcher.Match(pattern)...) + for _, e := range g.otherMatchers { + if e.M.Match(pattern) { + result = append(result, e.Id) + } + } + return result +} + +// Size returns the number of matchers in the MatcherGroup. +func (g *MatcherGroup) Size() uint32 { + return g.count +} + +type IndexMatcherGroup struct { + Matchers []IndexMatcher +} + +func (g *IndexMatcherGroup) Match(input string) []uint32 { + var offset uint32 + for _, m := range g.Matchers { + if res := m.Match(input); len(res) > 0 { + if offset == 0 { + return res + } + shifted := make([]uint32, len(res)) + for i, id := range res { + shifted[i] = id + offset + } + return shifted + } + offset += m.Size() + } + return nil +} + +func (g *IndexMatcherGroup) Size() uint32 { + var count uint32 + for _, m := range g.Matchers { + count += m.Size() + } + return count +} diff --git a/subproject/Xray-core-main/common/strmatcher/strmatcher_test.go b/subproject/Xray-core-main/common/strmatcher/strmatcher_test.go new file mode 100644 index 00000000..408ae628 --- /dev/null +++ b/subproject/Xray-core-main/common/strmatcher/strmatcher_test.go @@ -0,0 +1,265 @@ +package strmatcher_test + +import ( + "reflect" + "testing" + + "github.com/xtls/xray-core/common" + . "github.com/xtls/xray-core/common/strmatcher" +) + +func TestMatcherGroup(t *testing.T) { + rules := []struct { + Type Type + Domain string + }{ + { + Type: Regex, + Domain: "apis\\.us$", + }, + { + Type: Substr, + Domain: "apis", + }, + { + Type: Domain, + Domain: "googleapis.com", + }, + { + Type: Domain, + Domain: "com", + }, + { + Type: Full, + Domain: "www.baidu.com", + }, + { + Type: Substr, + Domain: "apis", + }, + { + Type: Domain, + Domain: "googleapis.com", + }, + { + Type: Full, + Domain: "fonts.googleapis.com", + }, + { + Type: Full, + Domain: "www.baidu.com", + }, + { + Type: Domain, + Domain: "example.com", + }, + } + cases := []struct { + Input string + Output []uint32 + }{ + { + Input: "www.baidu.com", + Output: []uint32{5, 9, 4}, + }, + { + Input: "fonts.googleapis.com", + Output: []uint32{8, 3, 7, 4, 2, 6}, + }, + { + Input: "example.googleapis.com", + Output: []uint32{3, 7, 4, 2, 6}, + }, + { + Input: "testapis.us", + Output: []uint32{1, 2, 6}, + }, + { + Input: "example.com", + Output: []uint32{10, 4}, + }, + } + matcherGroup := &MatcherGroup{} + for _, rule := range rules { + matcher, err := rule.Type.New(rule.Domain) + common.Must(err) + matcherGroup.Add(matcher) + } + for _, test := range cases { + if m := matcherGroup.Match(test.Input); !reflect.DeepEqual(m, test.Output) { + t.Error("unexpected output: ", m, " for test case ", test) + } + } +} + +func TestACAutomaton(t *testing.T) { + cases1 := []struct { + pattern string + mType Type + input string + output bool + }{ + { + pattern: "xtls.github.io", + mType: Domain, + input: "www.xtls.github.io", + output: true, + }, + { + pattern: "xtls.github.io", + mType: Domain, + input: "xtls.github.io", + output: true, + }, + { + pattern: "xtls.github.io", + mType: Domain, + input: "www.xtis.github.io", + output: false, + }, + { + pattern: "xtls.github.io", + mType: Domain, + input: "tls.github.io", + output: false, + }, + { + pattern: "xtls.github.io", + mType: Domain, + input: "xxtls.github.io", + output: false, + }, + { + pattern: "xtls.github.io", + mType: Full, + input: "xtls.github.io", + output: true, + }, + { + pattern: "xtls.github.io", + mType: Full, + input: "xxtls.github.io", + output: false, + }, + } + for _, test := range cases1 { + ac := NewACAutomaton() + ac.Add(test.pattern, test.mType) + ac.Build() + if m := ac.Match(test.input); m != test.output { + t.Error("unexpected output: ", m, " for test case ", test) + } + } + { + cases2Input := []struct { + pattern string + mType Type + }{ + { + pattern: "163.com", + mType: Domain, + }, + { + pattern: "m.126.com", + mType: Full, + }, + { + pattern: "3.com", + mType: Full, + }, + { + pattern: "google.com", + mType: Substr, + }, + { + pattern: "vgoogle.com", + mType: Substr, + }, + } + ac := NewACAutomaton() + for _, test := range cases2Input { + ac.Add(test.pattern, test.mType) + } + ac.Build() + cases2Output := []struct { + pattern string + res bool + }{ + { + pattern: "126.com", + res: false, + }, + { + pattern: "m.163.com", + res: true, + }, + { + pattern: "mm163.com", + res: false, + }, + { + pattern: "m.126.com", + res: true, + }, + { + pattern: "163.com", + res: true, + }, + { + pattern: "63.com", + res: false, + }, + { + pattern: "oogle.com", + res: false, + }, + { + pattern: "vvgoogle.com", + res: true, + }, + { + pattern: "½", + res: false, + }, + } + for _, test := range cases2Output { + if m := ac.Match(test.pattern); m != test.res { + t.Error("unexpected output: ", m, " for test case ", test) + } + } + } + { + cases3Input := []struct { + pattern string + mType Type + }{ + { + pattern: "video.google.com", + mType: Domain, + }, + { + pattern: "gle.com", + mType: Domain, + }, + } + ac := NewACAutomaton() + for _, test := range cases3Input { + ac.Add(test.pattern, test.mType) + } + ac.Build() + cases3Output := []struct { + pattern string + res bool + }{ + { + pattern: "google.com", + res: false, + }, + } + for _, test := range cases3Output { + if m := ac.Match(test.pattern); m != test.res { + t.Error("unexpected output: ", m, " for test case ", test) + } + } + } +} diff --git a/subproject/Xray-core-main/common/task/common.go b/subproject/Xray-core-main/common/task/common.go new file mode 100644 index 00000000..b43cc870 --- /dev/null +++ b/subproject/Xray-core-main/common/task/common.go @@ -0,0 +1,10 @@ +package task + +import "github.com/xtls/xray-core/common" + +// Close returns a func() that closes v. +func Close(v interface{}) func() error { + return func() error { + return common.Close(v) + } +} diff --git a/subproject/Xray-core-main/common/task/periodic.go b/subproject/Xray-core-main/common/task/periodic.go new file mode 100644 index 00000000..6abe41ae --- /dev/null +++ b/subproject/Xray-core-main/common/task/periodic.go @@ -0,0 +1,85 @@ +package task + +import ( + "sync" + "time" +) + +// Periodic is a task that runs periodically. +type Periodic struct { + // Interval of the task being run + Interval time.Duration + // Execute is the task function + Execute func() error + + access sync.Mutex + timer *time.Timer + running bool +} + +func (t *Periodic) hasClosed() bool { + t.access.Lock() + defer t.access.Unlock() + + return !t.running +} + +func (t *Periodic) checkedExecute() error { + if t.hasClosed() { + return nil + } + + if err := t.Execute(); err != nil { + t.access.Lock() + t.running = false + t.access.Unlock() + return err + } + + t.access.Lock() + defer t.access.Unlock() + + if !t.running { + return nil + } + + t.timer = time.AfterFunc(t.Interval, func() { + t.checkedExecute() + }) + + return nil +} + +// Start implements common.Runnable. +func (t *Periodic) Start() error { + t.access.Lock() + if t.running { + t.access.Unlock() + return nil + } + t.running = true + t.access.Unlock() + + if err := t.checkedExecute(); err != nil { + t.access.Lock() + t.running = false + t.access.Unlock() + return err + } + + return nil +} + +// Close implements common.Closable. +func (t *Periodic) Close() error { + t.access.Lock() + defer t.access.Unlock() + + t.running = false + if t.timer != nil { + t.timer.Stop() + t.timer = nil + } + + return nil +} diff --git a/subproject/Xray-core-main/common/task/periodic_test.go b/subproject/Xray-core-main/common/task/periodic_test.go new file mode 100644 index 00000000..9cad3017 --- /dev/null +++ b/subproject/Xray-core-main/common/task/periodic_test.go @@ -0,0 +1,36 @@ +package task_test + +import ( + "testing" + "time" + + "github.com/xtls/xray-core/common" + . "github.com/xtls/xray-core/common/task" +) + +func TestPeriodicTaskStop(t *testing.T) { + value := 0 + task := &Periodic{ + Interval: time.Second * 2, + Execute: func() error { + value++ + return nil + }, + } + common.Must(task.Start()) + time.Sleep(time.Second * 5) + common.Must(task.Close()) + if value != 3 { + t.Fatal("expected 3, but got ", value) + } + time.Sleep(time.Second * 4) + if value != 3 { + t.Fatal("expected 3, but got ", value) + } + common.Must(task.Start()) + time.Sleep(time.Second * 3) + if value != 5 { + t.Fatal("Expected 5, but ", value) + } + common.Must(task.Close()) +} diff --git a/subproject/Xray-core-main/common/task/task.go b/subproject/Xray-core-main/common/task/task.go new file mode 100644 index 00000000..eeba1dcd --- /dev/null +++ b/subproject/Xray-core-main/common/task/task.go @@ -0,0 +1,64 @@ +package task + +import ( + "context" + + "github.com/xtls/xray-core/common/signal/semaphore" +) + +// OnSuccess executes g() after f() returns nil. +func OnSuccess(f func() error, g func() error) func() error { + return func() error { + if err := f(); err != nil { + return err + } + return g() + } +} + +// Run executes a list of tasks in parallel, returns the first error encountered or nil if all tasks pass. +func Run(ctx context.Context, tasks ...func() error) error { + n := len(tasks) + s := semaphore.New(n) + done := make(chan error, 1) + + for _, task := range tasks { + <-s.Wait() + go func(f func() error) { + err := f() + if err == nil { + s.Signal() + return + } + + select { + case done <- err: + default: + } + }(task) + } + + /* + if altctx := ctx.Value("altctx"); altctx != nil { + ctx = altctx.(context.Context) + } + */ + + for i := 0; i < n; i++ { + select { + case err := <-done: + return err + case <-ctx.Done(): + return ctx.Err() + case <-s.Wait(): + } + } + + /* + if cancel := ctx.Value("cancel"); cancel != nil { + cancel.(context.CancelFunc)() + } + */ + + return nil +} diff --git a/subproject/Xray-core-main/common/task/task_test.go b/subproject/Xray-core-main/common/task/task_test.go new file mode 100644 index 00000000..87ebe0aa --- /dev/null +++ b/subproject/Xray-core-main/common/task/task_test.go @@ -0,0 +1,65 @@ +package task_test + +import ( + "context" + "errors" + "strings" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/xtls/xray-core/common" + . "github.com/xtls/xray-core/common/task" +) + +func TestExecuteParallel(t *testing.T) { + err := Run(context.Background(), + func() error { + time.Sleep(time.Millisecond * 200) + return errors.New("test") + }, func() error { + time.Sleep(time.Millisecond * 500) + return errors.New("test2") + }) + + if r := cmp.Diff(err.Error(), "test"); r != "" { + t.Error(r) + } +} + +func TestExecuteParallelContextCancel(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + err := Run(ctx, func() error { + time.Sleep(time.Millisecond * 2000) + return errors.New("test") + }, func() error { + time.Sleep(time.Millisecond * 5000) + return errors.New("test2") + }, func() error { + cancel() + return nil + }) + + errStr := err.Error() + if !strings.Contains(errStr, "canceled") { + t.Error("expected error string to contain 'canceled', but actually not: ", errStr) + } +} + +func BenchmarkExecuteOne(b *testing.B) { + noop := func() error { + return nil + } + for i := 0; i < b.N; i++ { + common.Must(Run(context.Background(), noop)) + } +} + +func BenchmarkExecuteTwo(b *testing.B) { + noop := func() error { + return nil + } + for i := 0; i < b.N; i++ { + common.Must(Run(context.Background(), noop, noop)) + } +} diff --git a/subproject/Xray-core-main/common/type.go b/subproject/Xray-core-main/common/type.go new file mode 100644 index 00000000..8ee8745c --- /dev/null +++ b/subproject/Xray-core-main/common/type.go @@ -0,0 +1,33 @@ +package common + +import ( + "context" + "reflect" + + "github.com/xtls/xray-core/common/errors" +) + +// ConfigCreator is a function to create an object by a config. +type ConfigCreator func(ctx context.Context, config interface{}) (interface{}, error) + +var typeCreatorRegistry = make(map[reflect.Type]ConfigCreator) + +// RegisterConfig registers a global config creator. The config can be nil but must have a type. +func RegisterConfig(config interface{}, configCreator ConfigCreator) error { + configType := reflect.TypeOf(config) + if _, found := typeCreatorRegistry[configType]; found { + return errors.New(configType.Name() + " is already registered").AtError() + } + typeCreatorRegistry[configType] = configCreator + return nil +} + +// CreateObject creates an object by its config. The config type must be registered through RegisterConfig(). +func CreateObject(ctx context.Context, config interface{}) (interface{}, error) { + configType := reflect.TypeOf(config) + creator, found := typeCreatorRegistry[configType] + if !found { + return nil, errors.New(configType.String() + " is not registered").AtError() + } + return creator(ctx, config) +} diff --git a/subproject/Xray-core-main/common/type_test.go b/subproject/Xray-core-main/common/type_test.go new file mode 100644 index 00000000..6a8b920f --- /dev/null +++ b/subproject/Xray-core-main/common/type_test.go @@ -0,0 +1,41 @@ +package common_test + +import ( + "context" + "testing" + + . "github.com/xtls/xray-core/common" +) + +type TConfig struct { + value int +} + +type YConfig struct { + value string +} + +func TestObjectCreation(t *testing.T) { + f := func(ctx context.Context, t interface{}) (interface{}, error) { + return func() int { + return t.(*TConfig).value + }, nil + } + + Must(RegisterConfig((*TConfig)(nil), f)) + err := RegisterConfig((*TConfig)(nil), f) + if err == nil { + t.Error("expect non-nil error, but got nil") + } + + g, err := CreateObject(context.Background(), &TConfig{value: 2}) + Must(err) + if v := g.(func() int)(); v != 2 { + t.Error("expect return value 2, but got ", v) + } + + _, err = CreateObject(context.Background(), &YConfig{value: "T"}) + if err == nil { + t.Error("expect non-nil error, but got nil") + } +} diff --git a/subproject/Xray-core-main/common/units/bytesize.go b/subproject/Xray-core-main/common/units/bytesize.go new file mode 100644 index 00000000..bf3c84d3 --- /dev/null +++ b/subproject/Xray-core-main/common/units/bytesize.go @@ -0,0 +1,100 @@ +package units + +import ( + "errors" + "strconv" + "strings" + "unicode" +) + +var ( + errInvalidSize = errors.New("invalid size") + errInvalidUnit = errors.New("invalid or unsupported unit") +) + +// ByteSize is the size of bytes +type ByteSize uint64 + +const ( + _ = iota + // KB = 1KB + KB ByteSize = 1 << (10 * iota) + // MB = 1MB + MB + // GB = 1GB + GB + // TB = 1TB + TB + // PB = 1PB + PB + // EB = 1EB + EB +) + +func (b ByteSize) String() string { + unit := "" + value := float64(0) + switch { + case b == 0: + return "0" + case b < KB: + unit = "B" + value = float64(b) + case b < MB: + unit = "KB" + value = float64(b) / float64(KB) + case b < GB: + unit = "MB" + value = float64(b) / float64(MB) + case b < TB: + unit = "GB" + value = float64(b) / float64(GB) + case b < PB: + unit = "TB" + value = float64(b) / float64(TB) + case b < EB: + unit = "PB" + value = float64(b) / float64(PB) + default: + unit = "EB" + value = float64(b) / float64(EB) + } + result := strconv.FormatFloat(value, 'f', 2, 64) + result = strings.TrimSuffix(result, ".0") + return result + unit +} + +// Parse parses ByteSize from string +func (b *ByteSize) Parse(s string) error { + s = strings.TrimSpace(s) + s = strings.ToUpper(s) + i := strings.IndexFunc(s, unicode.IsLetter) + if i == -1 { + return errInvalidUnit + } + + bytesString, multiple := s[:i], s[i:] + bytes, err := strconv.ParseFloat(bytesString, 64) + if err != nil || bytes <= 0 { + return errInvalidSize + } + switch multiple { + case "B": + *b = ByteSize(bytes) + case "K", "KB", "KIB": + *b = ByteSize(bytes * float64(KB)) + case "M", "MB", "MIB": + *b = ByteSize(bytes * float64(MB)) + case "G", "GB", "GIB": + *b = ByteSize(bytes * float64(GB)) + case "T", "TB", "TIB": + *b = ByteSize(bytes * float64(TB)) + case "P", "PB", "PIB": + *b = ByteSize(bytes * float64(PB)) + case "E", "EB", "EIB": + *b = ByteSize(bytes * float64(EB)) + default: + return errInvalidUnit + } + return nil +} diff --git a/subproject/Xray-core-main/common/units/bytesize_test.go b/subproject/Xray-core-main/common/units/bytesize_test.go new file mode 100644 index 00000000..3b8b9c88 --- /dev/null +++ b/subproject/Xray-core-main/common/units/bytesize_test.go @@ -0,0 +1,66 @@ +package units_test + +import ( + "testing" + + "github.com/xtls/xray-core/common/units" +) + +func TestByteSizes(t *testing.T) { + size := units.ByteSize(0) + assertSizeString(t, size, "0") + size++ + assertSizeValue(t, + assertSizeString(t, size, "1.00B"), + size, + ) + size <<= 10 + assertSizeValue(t, + assertSizeString(t, size, "1.00KB"), + size, + ) + size <<= 10 + assertSizeValue(t, + assertSizeString(t, size, "1.00MB"), + size, + ) + size <<= 10 + assertSizeValue(t, + assertSizeString(t, size, "1.00GB"), + size, + ) + size <<= 10 + assertSizeValue(t, + assertSizeString(t, size, "1.00TB"), + size, + ) + size <<= 10 + assertSizeValue(t, + assertSizeString(t, size, "1.00PB"), + size, + ) + size <<= 10 + assertSizeValue(t, + assertSizeString(t, size, "1.00EB"), + size, + ) +} + +func assertSizeValue(t *testing.T, size string, expected units.ByteSize) { + actual := units.ByteSize(0) + err := actual.Parse(size) + if err != nil { + t.Error(err) + } + if actual != expected { + t.Errorf("expect %s, but got %s", expected, actual) + } +} + +func assertSizeString(t *testing.T, size units.ByteSize, expected string) string { + actual := size.String() + if actual != expected { + t.Errorf("expect %s, but got %s", expected, actual) + } + return expected +} diff --git a/subproject/Xray-core-main/common/utils/access_field.go b/subproject/Xray-core-main/common/utils/access_field.go new file mode 100644 index 00000000..bc42e67c --- /dev/null +++ b/subproject/Xray-core-main/common/utils/access_field.go @@ -0,0 +1,17 @@ +package utils + +import ( + "reflect" + "unsafe" +) + +// AccessField can used to access unexported field of a struct +// valueType must be the exact type of the field or it will panic +func AccessField[valueType any](obj any, fieldName string) *valueType { + field := reflect.ValueOf(obj).Elem().FieldByName(fieldName) + if field.Type() != reflect.TypeOf(*new(valueType)) { + panic("field type: " + field.Type().String() + ", valueType: " + reflect.TypeOf(*new(valueType)).String()) + } + v := (*valueType)(unsafe.Pointer(field.UnsafeAddr())) + return v +} diff --git a/subproject/Xray-core-main/common/utils/browser.go b/subproject/Xray-core-main/common/utils/browser.go new file mode 100644 index 00000000..2337125a --- /dev/null +++ b/subproject/Xray-core-main/common/utils/browser.go @@ -0,0 +1,191 @@ +package utils + +import ( + "math/rand" + "strconv" + "time" + "net/http" + "strings" + + "github.com/klauspost/cpuid/v2" +) + +func ChromeVersion() int { + // Use only CPU info as seed for PRNG + seed := int64(cpuid.CPU.Family + cpuid.CPU.Model + cpuid.CPU.PhysicalCores + cpuid.CPU.LogicalCores + cpuid.CPU.CacheLine) + rng := rand.New(rand.NewSource(seed)) + // Start from Chrome 144 released on 2026.1.13 + releaseDate := time.Date(2026, 1, 13, 0, 0, 0, 0, time.UTC) + version := 144 + now := time.Now() + // Each version has random 25-45 day interval + for releaseDate.Before(now) { + releaseDate = releaseDate.AddDate(0, 0, rng.Intn(21)+25) + version++ + } + return version - 1 +} + +// The full Chromium brand GREASE implementation +var clientHintGreaseNA = []string{" ", "(", ":", "-", ".", "/", ")", ";", "=", "?", "_"} +var clientHintVersionNA = []string{"8", "99", "24"} +var clientHintShuffle3 = [][3]int{{0, 1, 2}, {0, 2, 1}, {1, 0, 2}, {1, 2, 0}, {2, 0, 1}, {2, 1, 0}} +var clientHintShuffle4 = [][4]int{ + {0, 1, 2, 3}, {0, 1, 3, 2}, {0, 2, 1, 3}, {0, 2, 3, 1}, {0, 3, 1, 2}, {0, 3, 2, 1}, + {1, 0, 2, 3}, {1, 0, 3, 2}, {1, 2, 0, 3}, {1, 2, 3, 0}, {1, 3, 0, 2}, {1, 3, 2, 0}, + {2, 0, 1, 3}, {2, 0, 3, 1}, {2, 1, 0, 3}, {2, 1, 3, 0}, {2, 3, 0, 1}, {2, 3, 1, 0}, + {3, 0, 1, 2}, {3, 0, 2, 1}, {3, 1, 0, 2}, {3, 1, 2, 0}, {3, 2, 0, 1}, {3, 2, 1, 0}} +func getGreasedChInvalidBrand(seed int) string { + return "\"Not" + clientHintGreaseNA[seed % len(clientHintGreaseNA)] + "A" + clientHintGreaseNA[(seed + 1) % len(clientHintGreaseNA)] + "Brand\";v=\"" + clientHintVersionNA[seed % len(clientHintVersionNA)] + "\""; +} +func getGreasedChOrder(brandLength int, seed int) []int { + switch brandLength { + case 1: + return []int{0} + case 2: + return []int{seed % brandLength, (seed + 1) % brandLength} + case 3: + return clientHintShuffle3[seed % len(clientHintShuffle3)][:] + default: + return clientHintShuffle4[seed % len(clientHintShuffle4)][:] + } + return []int{} +} +func getUngreasedChUa(majorVersion int, forkName string) []string { + // Set the capacity to 4, the maximum allowed brand size, so Go will never allocate memory twice + baseChUa := make([]string, 0, 4) + baseChUa = append(baseChUa, getGreasedChInvalidBrand(majorVersion), + "\"Chromium\";v=\"" + strconv.Itoa(majorVersion) + "\"") + switch forkName { + case "chrome": + baseChUa = append(baseChUa, "\"Google Chrome\";v=\"" + strconv.Itoa(majorVersion) + "\"") + case "edge": + baseChUa = append(baseChUa, "\"Microsoft Edge\";v=\"" + strconv.Itoa(majorVersion) + "\"") + } + return baseChUa +} +func getGreasedChUa(majorVersion int, forkName string) string { + ungreasedCh := getUngreasedChUa(majorVersion, forkName) + shuffleMap := getGreasedChOrder(len(ungreasedCh), majorVersion) + shuffledCh := make([]string, len(ungreasedCh)) + for i, e := range shuffleMap { + shuffledCh[e] = ungreasedCh[i] + } + return strings.Join(shuffledCh, ", ") +} + +// It's better to pin on Firefox ESR releases, and there could be a Firefox ESR version generator later. +// However, if the Firefox fingerprint in uTLS doesn't have its update cadence match that of Firefox ESR, then it's better to update the Firefox version manually instead every time a new major ESR release is available. +var FirefoxUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0" + +// The code below provides a coherent default browser user agent string based on a CPU-seeded PRNG. +var AnchoredChromeVersion = ChromeVersion() +var ChromeUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/" + strconv.Itoa(AnchoredChromeVersion) + ".0.0.0 Safari/537.36" +var ChromeUACH = getGreasedChUa(AnchoredChromeVersion, "chrome") +var MSEdgeUA = ChromeUA + "Edg/" + strconv.Itoa(AnchoredChromeVersion) + ".0.0.0" +var MSEdgeUACH = getGreasedChUa(AnchoredChromeVersion, "edge") + +func applyMasqueradedHeaders(header http.Header, browser string, variant string) { + // Browser-specific. + switch browser { + case "chrome": + header["Sec-CH-UA"] = []string{ChromeUACH} + header["Sec-CH-UA-Mobile"] = []string{"?0"} + header["Sec-CH-UA-Platform"] = []string{"\"Windows\""} + header["DNT"] = []string{"1"} + header.Set("User-Agent", ChromeUA) + header.Set("Accept-Language", "en-US,en;q=0.9") + case "edge": + header["Sec-CH-UA"] = []string{MSEdgeUACH} + header["Sec-CH-UA-Mobile"] = []string{"?0"} + header["Sec-CH-UA-Platform"] = []string{"\"Windows\""} + header["DNT"] = []string{"1"} + header.Set("User-Agent", MSEdgeUA) + header.Set("Accept-Language", "en-US,en;q=0.9") + case "firefox": + header.Set("User-Agent", FirefoxUA) + header["DNT"] = []string{"1"} + header.Set("Accept-Language", "en-US,en;q=0.5") + case "golang": + // Expose the default net/http header. + header.Del("User-Agent") + return + } + // Context-specific. + switch variant { + case "nav": + if header.Get("Cache-Control") == "" { + switch browser { + case "chrome", "edge": + header.Set("Cache-Control", "max-age=0") + } + } + header.Set("Upgrade-Insecure-Requests", "1") + if header.Get("Accept") == "" { + switch browser { + case "chrome", "edge": + header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7") + case "firefox": + header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") + } + } + header.Set("Sec-Fetch-Site", "none") + header.Set("Sec-Fetch-Mode", "navigate") + header.Set("Sec-Fetch-User", "?1") + header.Set("Sec-Fetch-Dest", "document") + header.Set("Priority", "u=0, i") + case "ws": + header.Set("Sec-Fetch-Mode", "websocket") + header.Set("Sec-Fetch-Dest", "empty") + header.Set("Sec-Fetch-Site", "same-origin") + if header.Get("Cache-Control") == "" { + header.Set("Cache-Control", "no-cache") + } + if header.Get("Pragma") == "" { + header.Set("Pragma", "no-cache") + } + if header.Get("Accept") == "" { + header.Set("Accept", "*/*") + } + case "fetch": + header.Set("Sec-Fetch-Mode", "cors") + header.Set("Sec-Fetch-Dest", "empty") + header.Set("Sec-Fetch-Site", "same-origin") + if header.Get("Priority") == "" { + switch browser { + case "chrome", "edge": + header.Set("Priority", "u=1, i") + case "firefox": + header.Set("Priority", "u=4") + } + } + if header.Get("Cache-Control") == "" { + header.Set("Cache-Control", "no-cache") + } + if header.Get("Pragma") == "" { + header.Set("Pragma", "no-cache") + } + if header.Get("Accept") == "" { + header.Set("Accept", "*/*") + } + } +} + +func TryDefaultHeadersWith(header http.Header, variant string) { + // The global UA special value handler for transports. Used to be called HandleTransportUASettings. + // Just a FYI to whoever needing to fix this piece of code after some spontaneous event, I tried to make the two methods separate to let the code be cleaner and more organized. + if len(header.Values("User-Agent")) < 1 { + applyMasqueradedHeaders(header, "chrome", variant) + } else { + switch header.Get("User-Agent") { + case "chrome": + applyMasqueradedHeaders(header, "chrome", variant) + case "firefox": + applyMasqueradedHeaders(header, "firefox", variant) + case "edge": + applyMasqueradedHeaders(header, "edge", variant) + case "golang": + applyMasqueradedHeaders(header, "golang", variant) + } + } +} diff --git a/subproject/Xray-core-main/common/utils/padding.go b/subproject/Xray-core-main/common/utils/padding.go new file mode 100644 index 00000000..fe95ba9a --- /dev/null +++ b/subproject/Xray-core-main/common/utils/padding.go @@ -0,0 +1,24 @@ +package utils + +import ( + "math/rand/v2" +) + +var ( + // 8 ÷ (397/62) + h2packCorrectionFactor = 1.2493702770780857 + base62TotalCharsNum = 62 + base62Chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" +) + +// H2Base62Pad generates a base62 padding string for HTTP/2 header +// The total len will be slightly longer than the input to match the length after h2(h3 also) header huffman encoding +func H2Base62Pad[T int32 | int64 | int](expectedLen T) string { + actualLenFloat := float64(expectedLen) * h2packCorrectionFactor + actualLen := int(actualLenFloat) + result := make([]byte, actualLen) + for i := range actualLen { + result[i] = base62Chars[rand.N(base62TotalCharsNum)] + } + return string(result) +} diff --git a/subproject/Xray-core-main/common/utils/typed_sync_map.go b/subproject/Xray-core-main/common/utils/typed_sync_map.go new file mode 100644 index 00000000..39524a6f --- /dev/null +++ b/subproject/Xray-core-main/common/utils/typed_sync_map.go @@ -0,0 +1,112 @@ +package utils + +import ( + "sync" +) + +// TypedSyncMap is a wrapper of sync.Map that provides type-safe for keys and values. +// No need to use type assertions every time, so you can have more time to enjoy other things like GochiUsa +// If sync.Map methods returned nil, it will return the zero value of the type V. +type TypedSyncMap[K, V any] struct { + syncMap *sync.Map +} + +// NewTypedSyncMap creates a new TypedSyncMap +// K is key type, V is value type +// It is recommended to use pointer types for V because sync.Map might return nil +// If sync.Map methods really returned nil, it will return the zero value of the type V +func NewTypedSyncMap[K any, V any]() *TypedSyncMap[K, V] { + return &TypedSyncMap[K, V]{ + syncMap: &sync.Map{}, + } +} + +// Clear deletes all the entries, resulting in an empty Map. +func (m *TypedSyncMap[K, V]) Clear() { + m.syncMap.Clear() +} + +// CompareAndDelete deletes the entry for key if its value is equal to old. +// The old value must be of a comparable type. +// +// If there is no current value for key in the map, CompareAndDelete +// returns false (even if the old value is the nil interface value). +func (m *TypedSyncMap[K, V]) CompareAndDelete(key K, old V) (deleted bool) { + return m.syncMap.CompareAndDelete(key, old) +} + +// CompareAndSwap swaps the old and new values for key +// if the value stored in the map is equal to old. +// The old value must be of a comparable type. +func (m *TypedSyncMap[K, V]) CompareAndSwap(key K, old V, new V) (swapped bool) { + return m.syncMap.CompareAndSwap(key, old, new) +} + +// Delete deletes the value for a key. +func (m *TypedSyncMap[K, V]) Delete(key K) { + m.syncMap.Delete(key) +} + +// Load returns the value stored in the map for a key, or nil if no +// value is present. +// The ok result indicates whether value was found in the map. +func (m *TypedSyncMap[K, V]) Load(key K) (value V, ok bool) { + anyValue, ok := m.syncMap.Load(key) + // anyValue might be nil + if anyValue != nil { + value = anyValue.(V) + } + return value, ok +} + +// LoadAndDelete deletes the value for a key, returning the previous value if any. +// The loaded result reports whether the key was present. +func (m *TypedSyncMap[K, V]) LoadAndDelete(key K) (value V, loaded bool) { + anyValue, loaded := m.syncMap.LoadAndDelete(key) + if anyValue != nil { + value = anyValue.(V) + } + return value, loaded +} + +// LoadOrStore returns the existing value for the key if present. +// Otherwise, it stores and returns the given value. +// The loaded result is true if the value was loaded, false if stored. +func (m *TypedSyncMap[K, V]) LoadOrStore(key K, value V) (actual V, loaded bool) { + anyActual, loaded := m.syncMap.LoadOrStore(key, value) + if anyActual != nil { + actual = anyActual.(V) + } + return actual, loaded +} + +// Range calls f sequentially for each key and value present in the map. +// If f returns false, range stops the iteration. +// +// Range does not necessarily correspond to any consistent snapshot of the Map's +// contents: no key will be visited more than once, but if the value for any key +// is stored or deleted concurrently (including by f), Range may reflect any +// mapping for that key from any point during the Range call. Range does not +// block other methods on the receiver; even f itself may call any method on m. +// +// Range may be O(N) with the number of elements in the map even if f returns +// false after a constant number of calls. +func (m *TypedSyncMap[K, V]) Range(f func(key K, value V) bool) { + m.syncMap.Range(func(key, value any) bool { + return f(key.(K), value.(V)) + }) +} + +// Store sets the value for a key. +func (m *TypedSyncMap[K, V]) Store(key K, value V) { + m.syncMap.Store(key, value) +} + +// Swap swaps the value for a key and returns the previous value if any. The loaded result reports whether the key was present. +func (m *TypedSyncMap[K, V]) Swap(key K, value V) (previous V, loaded bool) { + anyPrevious, loaded := m.syncMap.Swap(key, value) + if anyPrevious != nil { + previous = anyPrevious.(V) + } + return previous, loaded +} \ No newline at end of file diff --git a/subproject/Xray-core-main/common/uuid/uuid.go b/subproject/Xray-core-main/common/uuid/uuid.go new file mode 100644 index 00000000..ef6da4cb --- /dev/null +++ b/subproject/Xray-core-main/common/uuid/uuid.go @@ -0,0 +1,105 @@ +package uuid // import "github.com/xtls/xray-core/common/uuid" + +import ( + "bytes" + "crypto/rand" + "crypto/sha1" + "encoding/hex" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/errors" +) + +var byteGroups = []int{8, 4, 4, 4, 12} + +type UUID [16]byte + +// String returns the string representation of this UUID. +func (u *UUID) String() string { + bytes := u.Bytes() + result := hex.EncodeToString(bytes[0 : byteGroups[0]/2]) + start := byteGroups[0] / 2 + for i := 1; i < len(byteGroups); i++ { + nBytes := byteGroups[i] / 2 + result += "-" + result += hex.EncodeToString(bytes[start : start+nBytes]) + start += nBytes + } + return result +} + +// Bytes returns the bytes representation of this UUID. +func (u *UUID) Bytes() []byte { + return u[:] +} + +// Equals returns true if this UUID equals another UUID by value. +func (u *UUID) Equals(another *UUID) bool { + if u == nil && another == nil { + return true + } + if u == nil || another == nil { + return false + } + return bytes.Equal(u.Bytes(), another.Bytes()) +} + +// New creates a UUID with random value. +func New() UUID { + var uuid UUID + common.Must2(rand.Read(uuid.Bytes())) + uuid[6] = (uuid[6] & 0x0f) | (4 << 4) + uuid[8] = (uuid[8]&(0xff>>2) | (0x02 << 6)) + return uuid +} + +// ParseBytes converts a UUID in byte form to object. +func ParseBytes(b []byte) (UUID, error) { + var uuid UUID + if len(b) != 16 { + return uuid, errors.New("invalid UUID: ", b) + } + copy(uuid[:], b) + return uuid, nil +} + +// ParseString converts a UUID in string form to object. +func ParseString(str string) (UUID, error) { + var uuid UUID + + text := []byte(str) + if l := len(text); l < 32 || l > 36 { + if l == 0 || l > 30 { + return uuid, errors.New("invalid UUID: ", str) + } + h := sha1.New() + h.Write(uuid[:]) + h.Write(text) + u := h.Sum(nil)[:16] + u[6] = (u[6] & 0x0f) | (5 << 4) + u[8] = (u[8]&(0xff>>2) | (0x02 << 6)) + copy(uuid[:], u) + return uuid, nil + } + + b := uuid.Bytes() + + for _, byteGroup := range byteGroups { + if len(text) > 0 && text[0] == '-' { + text = text[1:] + } + + if len(text) < byteGroup { + return uuid, errors.New("invalid UUID: ", str) + } + + if _, err := hex.Decode(b[:byteGroup/2], text[:byteGroup]); err != nil { + return uuid, err + } + + text = text[byteGroup:] + b = b[byteGroup/2:] + } + + return uuid, nil +} diff --git a/subproject/Xray-core-main/common/uuid/uuid_test.go b/subproject/Xray-core-main/common/uuid/uuid_test.go new file mode 100644 index 00000000..7d909a78 --- /dev/null +++ b/subproject/Xray-core-main/common/uuid/uuid_test.go @@ -0,0 +1,87 @@ +package uuid_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/xtls/xray-core/common" + . "github.com/xtls/xray-core/common/uuid" +) + +func TestParseBytes(t *testing.T) { + str := "2418d087-648d-4990-86e8-19dca1d006d3" + bytes := []byte{0x24, 0x18, 0xd0, 0x87, 0x64, 0x8d, 0x49, 0x90, 0x86, 0xe8, 0x19, 0xdc, 0xa1, 0xd0, 0x06, 0xd3} + + uuid, err := ParseBytes(bytes) + common.Must(err) + if diff := cmp.Diff(uuid.String(), str); diff != "" { + t.Error(diff) + } + + _, err = ParseBytes([]byte{1, 3, 2, 4}) + if err == nil { + t.Fatal("Expect error but nil") + } +} + +func TestParseString(t *testing.T) { + str := "2418d087-648d-4990-86e8-19dca1d006d3" + expectedBytes := []byte{0x24, 0x18, 0xd0, 0x87, 0x64, 0x8d, 0x49, 0x90, 0x86, 0xe8, 0x19, 0xdc, 0xa1, 0xd0, 0x06, 0xd3} + + uuid, err := ParseString(str) + common.Must(err) + if r := cmp.Diff(expectedBytes, uuid.Bytes()); r != "" { + t.Fatal(r) + } + + u0, _ := ParseString("example") + u5, _ := ParseString("feb54431-301b-52bb-a6dd-e1e93e81bb9e") + if r := cmp.Diff(u0, u5); r != "" { + t.Fatal(r) + } + + _, err = ParseString("2418d087-648k-4990-86e8-19dca1d006d3") + if err == nil { + t.Fatal("Expect error but nil") + } + + _, err = ParseString("2418d087-648d-4990-86e8-19dca1d0") + if err == nil { + t.Fatal("Expect error but nil") + } +} + +func TestNewUUID(t *testing.T) { + uuid := New() + uuid2, err := ParseString(uuid.String()) + + common.Must(err) + if uuid.String() != uuid2.String() { + t.Error("uuid string: ", uuid.String(), " != ", uuid2.String()) + } + if r := cmp.Diff(uuid.Bytes(), uuid2.Bytes()); r != "" { + t.Error(r) + } +} + +func TestRandom(t *testing.T) { + uuid := New() + uuid2 := New() + + if uuid.String() == uuid2.String() { + t.Error("duplicated uuid") + } +} + +func TestEquals(t *testing.T) { + var uuid *UUID + var uuid2 *UUID + if !uuid.Equals(uuid2) { + t.Error("empty uuid should equal") + } + + uuid3 := New() + if uuid.Equals(&uuid3) { + t.Error("nil uuid equals non-nil uuid") + } +} diff --git a/subproject/Xray-core-main/common/xudp/xudp.go b/subproject/Xray-core-main/common/xudp/xudp.go new file mode 100644 index 00000000..9b4ffbba --- /dev/null +++ b/subproject/Xray-core-main/common/xudp/xudp.go @@ -0,0 +1,192 @@ +package xudp + +import ( + "context" + "crypto/rand" + "encoding/base64" + "fmt" + "io" + "strconv" + "strings" + "time" + + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/platform" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/session" + "lukechampine.com/blake3" +) + +var AddrParser = protocol.NewAddressParser( + protocol.AddressFamilyByte(byte(protocol.AddressTypeIPv4), net.AddressFamilyIPv4), + protocol.AddressFamilyByte(byte(protocol.AddressTypeDomain), net.AddressFamilyDomain), + protocol.AddressFamilyByte(byte(protocol.AddressTypeIPv6), net.AddressFamilyIPv6), + protocol.PortThenAddress(), +) + +var ( + Show bool + BaseKey []byte +) + +func init() { + if strings.ToLower(platform.NewEnvFlag(platform.XUDPLog).GetValue(func() string { return "" })) == "true" { + Show = true + } + BaseKey = make([]byte, 32) + rand.Read(BaseKey) + go func() { + time.Sleep(100 * time.Millisecond) // this is not nice, but need to give some time for Android to setup ENV + if raw := platform.NewEnvFlag(platform.XUDPBaseKey).GetValue(func() string { return "" }); raw != "" { + if BaseKey, _ = base64.RawURLEncoding.DecodeString(raw); len(BaseKey) == 32 { + return + } + panic(platform.XUDPBaseKey + ": invalid value (BaseKey must be 32 bytes): " + raw + " len " + strconv.Itoa(len(BaseKey))) + } + }() +} + +func GetGlobalID(ctx context.Context) (globalID [8]byte) { + if cone := ctx.Value("cone"); cone == nil || !cone.(bool) { // cone is nil only in some unit tests + return + } + if inbound := session.InboundFromContext(ctx); inbound != nil && inbound.Source.Network == net.Network_UDP && + (inbound.Name == "dokodemo-door" || inbound.Name == "socks" || inbound.Name == "shadowsocks" || inbound.Name == "tun" || inbound.Name == "wireguard") { + h := blake3.New(8, BaseKey) + h.Write([]byte(inbound.Source.String())) + copy(globalID[:], h.Sum(nil)) + if Show { + errors.LogInfo(ctx, fmt.Sprintf("XUDP inbound.Source.String(): %v\tglobalID: %v\n", inbound.Source.String(), globalID)) + } + } + return +} + +func NewPacketWriter(writer buf.Writer, dest net.Destination, globalID [8]byte) *PacketWriter { + return &PacketWriter{ + Writer: writer, + Dest: dest, + GlobalID: globalID, + } +} + +type PacketWriter struct { + Writer buf.Writer + Dest net.Destination + GlobalID [8]byte +} + +func (w *PacketWriter) WriteMultiBuffer(mb buf.MultiBuffer) error { + defer buf.ReleaseMulti(mb) + mb2Write := make(buf.MultiBuffer, 0, len(mb)) + for _, b := range mb { + length := b.Len() + if length == 0 || length+666 > buf.Size { + continue + } + + eb := buf.New() + eb.Write([]byte{0, 0, 0, 0}) // Meta data length; Mux Session ID + if w.Dest.Network == net.Network_UDP { + eb.WriteByte(1) // New + eb.WriteByte(1) // Opt + eb.WriteByte(2) // UDP + AddrParser.WriteAddressPort(eb, w.Dest.Address, w.Dest.Port) + if b.UDP != nil { // make sure it's user's proxy request + eb.Write(w.GlobalID[:]) // no need to check whether it's empty + } + w.Dest.Network = net.Network_Unknown + } else { + eb.WriteByte(2) // Keep + eb.WriteByte(1) // Opt + if b.UDP != nil { + eb.WriteByte(2) // UDP + AddrParser.WriteAddressPort(eb, b.UDP.Address, b.UDP.Port) + } + } + l := eb.Len() - 2 + eb.SetByte(0, byte(l>>8)) + eb.SetByte(1, byte(l)) + eb.WriteByte(byte(length >> 8)) + eb.WriteByte(byte(length)) + eb.Write(b.Bytes()) + + mb2Write = append(mb2Write, eb) + } + if mb2Write.IsEmpty() { + return nil + } + return w.Writer.WriteMultiBuffer(mb2Write) +} + +func NewPacketReader(reader io.Reader) *PacketReader { + return &PacketReader{ + Reader: reader, + cache: make([]byte, 2), + } +} + +type PacketReader struct { + Reader io.Reader + cache []byte +} + +func (r *PacketReader) ReadMultiBuffer() (buf.MultiBuffer, error) { + for { + if _, err := io.ReadFull(r.Reader, r.cache); err != nil { + return nil, err + } + l := int32(r.cache[0])<<8 | int32(r.cache[1]) + if l < 4 { + return nil, io.EOF + } + b := buf.New() + if _, err := b.ReadFullFrom(r.Reader, l); err != nil { + b.Release() + return nil, err + } + discard := false + switch b.Byte(2) { + case 2: + if l > 4 && b.Byte(4) == 2 { // MUST check the flag first + b.Advance(5) + // b.Clear() will be called automatically if all data had been read. + addr, port, err := AddrParser.ReadAddressPort(nil, b) + if err != nil { + b.Release() + return nil, err + } + b.UDP = &net.Destination{ + Network: net.Network_UDP, + Address: addr, + Port: port, + } + } + case 4: + discard = true + default: + b.Release() + return nil, io.EOF + } + b.Clear() // in case there is padding (empty bytes) attached + if b.Byte(3) == 1 { + if _, err := io.ReadFull(r.Reader, r.cache); err != nil { + b.Release() + return nil, err + } + length := int32(r.cache[0])<<8 | int32(r.cache[1]) + if length > 0 { + if _, err := b.ReadFullFrom(r.Reader, length); err != nil { + b.Release() + return nil, err + } + if !discard { + return buf.MultiBuffer{b}, nil + } + } + } + b.Release() + } +} diff --git a/subproject/Xray-core-main/common/xudp/xudp_test.go b/subproject/Xray-core-main/common/xudp/xudp_test.go new file mode 100644 index 00000000..78ddfa27 --- /dev/null +++ b/subproject/Xray-core-main/common/xudp/xudp_test.go @@ -0,0 +1,36 @@ +package xudp + +import ( + "testing" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/net" +) + +func TestXudpReadWrite(t *testing.T) { + addr, _ := net.ParseDestination("tcp:127.0.0.1:1345") + mb := make(buf.MultiBuffer, 0, 16) + m := buf.MultiBufferContainer{ + MultiBuffer: mb, + } + var arr [8]byte + writer := NewPacketWriter(&m, addr, arr) + + source := make(buf.MultiBuffer, 0, 16) + b := buf.New() + b.WriteByte('a') + b.UDP = &addr + source = append(source, b) + writer.WriteMultiBuffer(source) + + reader := NewPacketReader(&m) + dest, err := reader.ReadMultiBuffer() + common.Must(err) + if dest[0].Byte(0) != 'a' { + t.Error("failed to parse xudp buffer") + } + if dest[0].UDP.Port != 1345 { + t.Error("failed to parse xudp buffer") + } +} diff --git a/subproject/Xray-core-main/core/annotations.go b/subproject/Xray-core-main/core/annotations.go new file mode 100644 index 00000000..41fc3fbd --- /dev/null +++ b/subproject/Xray-core-main/core/annotations.go @@ -0,0 +1,14 @@ +package core + +// Annotation is a concept in Xray. This struct is only for documentation. It is not used anywhere. +// Annotations begin with "xray:" in comment, as metadata of functions or types. +type Annotation struct { + // API is for types or functions that can be used in other libs. Possible values are: + // + // * xray:api:beta for types or functions that are ready for use, but maybe changed in the future. + // * xray:api:stable for types or functions with guarantee of backward compatibility. + // * xray:api:deprecated for types or functions that should not be used anymore. + // + // Types or functions without api annotation should not be used externally. + API string +} diff --git a/subproject/Xray-core-main/core/config.go b/subproject/Xray-core-main/core/config.go new file mode 100644 index 00000000..3ab2a376 --- /dev/null +++ b/subproject/Xray-core-main/core/config.go @@ -0,0 +1,191 @@ +package core + +import ( + "io" + "slices" + "strings" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/cmdarg" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/main/confloader" + "google.golang.org/protobuf/proto" +) + +// ConfigFormat is a configurable format of Xray config file. +type ConfigFormat struct { + Name string + Extension []string + Loader ConfigLoader +} + +type ConfigSource struct { + Name string + Format string +} + +// ConfigLoader is a utility to load Xray config from external source. +type ConfigLoader func(input interface{}) (*Config, error) + +// ConfigBuilder is a builder to build core.Config from filenames and formats +type ConfigBuilder func(files []*ConfigSource) (*Config, error) + +// ConfigsMerger merges multiple json configs into a single one +type ConfigsMerger func(files []*ConfigSource) (string, error) + +var ( + configLoaderByName = make(map[string]*ConfigFormat) + configLoaderByExt = make(map[string]*ConfigFormat) + ConfigBuilderForFiles ConfigBuilder + ConfigMergedFormFiles ConfigsMerger +) + +// RegisterConfigLoader add a new ConfigLoader. +func RegisterConfigLoader(format *ConfigFormat) error { + name := strings.ToLower(format.Name) + if _, found := configLoaderByName[name]; found { + return errors.New(format.Name, " already registered.") + } + configLoaderByName[name] = format + + for _, ext := range format.Extension { + lext := strings.ToLower(ext) + if f, found := configLoaderByExt[lext]; found { + return errors.New(ext, " already registered to ", f.Name) + } + configLoaderByExt[lext] = format + } + + return nil +} + +func GetMergedConfig(args cmdarg.Arg) (string, error) { + var files []*ConfigSource + supported := []string{"json", "yaml", "toml"} + for _, file := range args { + format := "json" + if file != "stdin:" { + format = GetFormat(file) + } + + if slices.Contains(supported, format) { + files = append(files, &ConfigSource{ + Name: file, + Format: format, + }) + } + } + return ConfigMergedFormFiles(files) +} + +func GetFormatByExtension(ext string) string { + switch strings.ToLower(ext) { + case "pb", "protobuf": + return "protobuf" + case "yaml", "yml": + return "yaml" + case "toml": + return "toml" + case "json", "jsonc": + return "json" + default: + return "" + } +} + +func getExtension(filename string) string { + idx := strings.LastIndexByte(filename, '.') + if idx == -1 { + return "" + } + return filename[idx+1:] +} + +func GetFormat(filename string) string { + return GetFormatByExtension(getExtension(filename)) +} + +func LoadConfig(formatName string, input interface{}) (*Config, error) { + switch v := input.(type) { + case cmdarg.Arg: + files := make([]*ConfigSource, len(v)) + hasProtobuf := false + for i, file := range v { + var f string + + if formatName == "auto" { + if file != "stdin:" { + f = GetFormat(file) + } else { + f = "json" + } + } else { + f = formatName + } + + if f == "" { + return nil, errors.New("Failed to get format of ", file).AtWarning() + } + + if f == "protobuf" { + hasProtobuf = true + } + files[i] = &ConfigSource{ + Name: file, + Format: f, + } + } + + // only one protobuf config file is allowed + if hasProtobuf { + if len(v) == 1 { + return configLoaderByName["protobuf"].Loader(v) + } else { + return nil, errors.New("Only one protobuf config file is allowed").AtWarning() + } + } + + // to avoid import cycle + return ConfigBuilderForFiles(files) + case io.Reader: + if f, found := configLoaderByName[formatName]; found { + return f.Loader(v) + } else { + return nil, errors.New("Unable to load config in", formatName).AtWarning() + } + } + + return nil, errors.New("Unable to load config").AtWarning() +} + +func loadProtobufConfig(data []byte) (*Config, error) { + config := new(Config) + if err := proto.Unmarshal(data, config); err != nil { + return nil, err + } + return config, nil +} + +func init() { + common.Must(RegisterConfigLoader(&ConfigFormat{ + Name: "Protobuf", + Extension: []string{"pb"}, + Loader: func(input interface{}) (*Config, error) { + switch v := input.(type) { + case cmdarg.Arg: + r, err := confloader.LoadConfig(v[0]) + common.Must(err) + data, err := buf.ReadAllToBytes(r) + common.Must(err) + return loadProtobufConfig(data) + case io.Reader: + data, err := buf.ReadAllToBytes(v) + common.Must(err) + return loadProtobufConfig(data) + default: + return nil, errors.New("unknown type") + } + }, + })) +} diff --git a/subproject/Xray-core-main/core/config.pb.go b/subproject/Xray-core-main/core/config.pb.go new file mode 100644 index 00000000..82761518 --- /dev/null +++ b/subproject/Xray-core-main/core/config.pb.go @@ -0,0 +1,330 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: core/config.proto + +package core + +import ( + serial "github.com/xtls/xray-core/common/serial" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Config is the master config of Xray. Xray takes this config as input and +// functions accordingly. +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Inbound handler configurations. Must have at least one item. + Inbound []*InboundHandlerConfig `protobuf:"bytes,1,rep,name=inbound,proto3" json:"inbound,omitempty"` + // Outbound handler configurations. Must have at least one item. The first + // item is used as default for routing. + Outbound []*OutboundHandlerConfig `protobuf:"bytes,2,rep,name=outbound,proto3" json:"outbound,omitempty"` + // App is for configurations of all features in Xray. A feature must + // implement the Feature interface, and its config type must be registered + // through common.RegisterConfig. + App []*serial.TypedMessage `protobuf:"bytes,4,rep,name=app,proto3" json:"app,omitempty"` + // Configuration for extensions. The config may not work if corresponding + // extension is not loaded into Xray. Xray will ignore such config during + // initialization. + Extension []*serial.TypedMessage `protobuf:"bytes,6,rep,name=extension,proto3" json:"extension,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_core_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_core_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_core_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Config) GetInbound() []*InboundHandlerConfig { + if x != nil { + return x.Inbound + } + return nil +} + +func (x *Config) GetOutbound() []*OutboundHandlerConfig { + if x != nil { + return x.Outbound + } + return nil +} + +func (x *Config) GetApp() []*serial.TypedMessage { + if x != nil { + return x.App + } + return nil +} + +func (x *Config) GetExtension() []*serial.TypedMessage { + if x != nil { + return x.Extension + } + return nil +} + +// InboundHandlerConfig is the configuration for inbound handler. +type InboundHandlerConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Tag of the inbound handler. The tag must be unique among all inbound + // handlers + Tag string `protobuf:"bytes,1,opt,name=tag,proto3" json:"tag,omitempty"` + // Settings for how this inbound proxy is handled. + ReceiverSettings *serial.TypedMessage `protobuf:"bytes,2,opt,name=receiver_settings,json=receiverSettings,proto3" json:"receiver_settings,omitempty"` + // Settings for inbound proxy. Must be one of the inbound proxies. + ProxySettings *serial.TypedMessage `protobuf:"bytes,3,opt,name=proxy_settings,json=proxySettings,proto3" json:"proxy_settings,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *InboundHandlerConfig) Reset() { + *x = InboundHandlerConfig{} + mi := &file_core_config_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *InboundHandlerConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InboundHandlerConfig) ProtoMessage() {} + +func (x *InboundHandlerConfig) ProtoReflect() protoreflect.Message { + mi := &file_core_config_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use InboundHandlerConfig.ProtoReflect.Descriptor instead. +func (*InboundHandlerConfig) Descriptor() ([]byte, []int) { + return file_core_config_proto_rawDescGZIP(), []int{1} +} + +func (x *InboundHandlerConfig) GetTag() string { + if x != nil { + return x.Tag + } + return "" +} + +func (x *InboundHandlerConfig) GetReceiverSettings() *serial.TypedMessage { + if x != nil { + return x.ReceiverSettings + } + return nil +} + +func (x *InboundHandlerConfig) GetProxySettings() *serial.TypedMessage { + if x != nil { + return x.ProxySettings + } + return nil +} + +// OutboundHandlerConfig is the configuration for outbound handler. +type OutboundHandlerConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Tag of this outbound handler. + Tag string `protobuf:"bytes,1,opt,name=tag,proto3" json:"tag,omitempty"` + // Settings for how to dial connection for this outbound handler. + SenderSettings *serial.TypedMessage `protobuf:"bytes,2,opt,name=sender_settings,json=senderSettings,proto3" json:"sender_settings,omitempty"` + // Settings for this outbound proxy. Must be one of the outbound proxies. + ProxySettings *serial.TypedMessage `protobuf:"bytes,3,opt,name=proxy_settings,json=proxySettings,proto3" json:"proxy_settings,omitempty"` + // If not zero, this outbound will be expired in seconds. Not used for now. + Expire int64 `protobuf:"varint,4,opt,name=expire,proto3" json:"expire,omitempty"` + // Comment of this outbound handler. Not used for now. + Comment string `protobuf:"bytes,5,opt,name=comment,proto3" json:"comment,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OutboundHandlerConfig) Reset() { + *x = OutboundHandlerConfig{} + mi := &file_core_config_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OutboundHandlerConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OutboundHandlerConfig) ProtoMessage() {} + +func (x *OutboundHandlerConfig) ProtoReflect() protoreflect.Message { + mi := &file_core_config_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OutboundHandlerConfig.ProtoReflect.Descriptor instead. +func (*OutboundHandlerConfig) Descriptor() ([]byte, []int) { + return file_core_config_proto_rawDescGZIP(), []int{2} +} + +func (x *OutboundHandlerConfig) GetTag() string { + if x != nil { + return x.Tag + } + return "" +} + +func (x *OutboundHandlerConfig) GetSenderSettings() *serial.TypedMessage { + if x != nil { + return x.SenderSettings + } + return nil +} + +func (x *OutboundHandlerConfig) GetProxySettings() *serial.TypedMessage { + if x != nil { + return x.ProxySettings + } + return nil +} + +func (x *OutboundHandlerConfig) GetExpire() int64 { + if x != nil { + return x.Expire + } + return 0 +} + +func (x *OutboundHandlerConfig) GetComment() string { + if x != nil { + return x.Comment + } + return "" +} + +var File_core_config_proto protoreflect.FileDescriptor + +const file_core_config_proto_rawDesc = "" + + "\n" + + "\x11core/config.proto\x12\txray.core\x1a!common/serial/typed_message.proto\"\xfb\x01\n" + + "\x06Config\x129\n" + + "\ainbound\x18\x01 \x03(\v2\x1f.xray.core.InboundHandlerConfigR\ainbound\x12<\n" + + "\boutbound\x18\x02 \x03(\v2 .xray.core.OutboundHandlerConfigR\boutbound\x122\n" + + "\x03app\x18\x04 \x03(\v2 .xray.common.serial.TypedMessageR\x03app\x12>\n" + + "\textension\x18\x06 \x03(\v2 .xray.common.serial.TypedMessageR\textensionJ\x04\b\x03\x10\x04\"\xc0\x01\n" + + "\x14InboundHandlerConfig\x12\x10\n" + + "\x03tag\x18\x01 \x01(\tR\x03tag\x12M\n" + + "\x11receiver_settings\x18\x02 \x01(\v2 .xray.common.serial.TypedMessageR\x10receiverSettings\x12G\n" + + "\x0eproxy_settings\x18\x03 \x01(\v2 .xray.common.serial.TypedMessageR\rproxySettings\"\xef\x01\n" + + "\x15OutboundHandlerConfig\x12\x10\n" + + "\x03tag\x18\x01 \x01(\tR\x03tag\x12I\n" + + "\x0fsender_settings\x18\x02 \x01(\v2 .xray.common.serial.TypedMessageR\x0esenderSettings\x12G\n" + + "\x0eproxy_settings\x18\x03 \x01(\v2 .xray.common.serial.TypedMessageR\rproxySettings\x12\x16\n" + + "\x06expire\x18\x04 \x01(\x03R\x06expire\x12\x18\n" + + "\acomment\x18\x05 \x01(\tR\acommentB=\n" + + "\rcom.xray.coreP\x01Z\x1egithub.com/xtls/xray-core/core\xaa\x02\tXray.Coreb\x06proto3" + +var ( + file_core_config_proto_rawDescOnce sync.Once + file_core_config_proto_rawDescData []byte +) + +func file_core_config_proto_rawDescGZIP() []byte { + file_core_config_proto_rawDescOnce.Do(func() { + file_core_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_core_config_proto_rawDesc), len(file_core_config_proto_rawDesc))) + }) + return file_core_config_proto_rawDescData +} + +var file_core_config_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_core_config_proto_goTypes = []any{ + (*Config)(nil), // 0: xray.core.Config + (*InboundHandlerConfig)(nil), // 1: xray.core.InboundHandlerConfig + (*OutboundHandlerConfig)(nil), // 2: xray.core.OutboundHandlerConfig + (*serial.TypedMessage)(nil), // 3: xray.common.serial.TypedMessage +} +var file_core_config_proto_depIdxs = []int32{ + 1, // 0: xray.core.Config.inbound:type_name -> xray.core.InboundHandlerConfig + 2, // 1: xray.core.Config.outbound:type_name -> xray.core.OutboundHandlerConfig + 3, // 2: xray.core.Config.app:type_name -> xray.common.serial.TypedMessage + 3, // 3: xray.core.Config.extension:type_name -> xray.common.serial.TypedMessage + 3, // 4: xray.core.InboundHandlerConfig.receiver_settings:type_name -> xray.common.serial.TypedMessage + 3, // 5: xray.core.InboundHandlerConfig.proxy_settings:type_name -> xray.common.serial.TypedMessage + 3, // 6: xray.core.OutboundHandlerConfig.sender_settings:type_name -> xray.common.serial.TypedMessage + 3, // 7: xray.core.OutboundHandlerConfig.proxy_settings:type_name -> xray.common.serial.TypedMessage + 8, // [8:8] is the sub-list for method output_type + 8, // [8:8] is the sub-list for method input_type + 8, // [8:8] is the sub-list for extension type_name + 8, // [8:8] is the sub-list for extension extendee + 0, // [0:8] is the sub-list for field type_name +} + +func init() { file_core_config_proto_init() } +func file_core_config_proto_init() { + if File_core_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_core_config_proto_rawDesc), len(file_core_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_core_config_proto_goTypes, + DependencyIndexes: file_core_config_proto_depIdxs, + MessageInfos: file_core_config_proto_msgTypes, + }.Build() + File_core_config_proto = out.File + file_core_config_proto_goTypes = nil + file_core_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/core/config.proto b/subproject/Xray-core-main/core/config.proto new file mode 100644 index 00000000..32595d6d --- /dev/null +++ b/subproject/Xray-core-main/core/config.proto @@ -0,0 +1,57 @@ +syntax = "proto3"; + +package xray.core; +option csharp_namespace = "Xray.Core"; +option go_package = "github.com/xtls/xray-core/core"; +option java_package = "com.xray.core"; +option java_multiple_files = true; + +import "common/serial/typed_message.proto"; + +// Config is the master config of Xray. Xray takes this config as input and +// functions accordingly. +message Config { + // Inbound handler configurations. Must have at least one item. + repeated InboundHandlerConfig inbound = 1; + + // Outbound handler configurations. Must have at least one item. The first + // item is used as default for routing. + repeated OutboundHandlerConfig outbound = 2; + + reserved 3; + + // App is for configurations of all features in Xray. A feature must + // implement the Feature interface, and its config type must be registered + // through common.RegisterConfig. + repeated xray.common.serial.TypedMessage app = 4; + + // Configuration for extensions. The config may not work if corresponding + // extension is not loaded into Xray. Xray will ignore such config during + // initialization. + repeated xray.common.serial.TypedMessage extension = 6; +} + +// InboundHandlerConfig is the configuration for inbound handler. +message InboundHandlerConfig { + // Tag of the inbound handler. The tag must be unique among all inbound + // handlers + string tag = 1; + // Settings for how this inbound proxy is handled. + xray.common.serial.TypedMessage receiver_settings = 2; + // Settings for inbound proxy. Must be one of the inbound proxies. + xray.common.serial.TypedMessage proxy_settings = 3; +} + +// OutboundHandlerConfig is the configuration for outbound handler. +message OutboundHandlerConfig { + // Tag of this outbound handler. + string tag = 1; + // Settings for how to dial connection for this outbound handler. + xray.common.serial.TypedMessage sender_settings = 2; + // Settings for this outbound proxy. Must be one of the outbound proxies. + xray.common.serial.TypedMessage proxy_settings = 3; + // If not zero, this outbound will be expired in seconds. Not used for now. + int64 expire = 4; + // Comment of this outbound handler. Not used for now. + string comment = 5; +} diff --git a/subproject/Xray-core-main/core/context.go b/subproject/Xray-core-main/core/context.go new file mode 100644 index 00000000..50427964 --- /dev/null +++ b/subproject/Xray-core-main/core/context.go @@ -0,0 +1,53 @@ +package core + +import ( + "context" +) + +// XrayKey is the key type of Instance in Context, exported for test. +type XrayKey int + +const xrayKey XrayKey = 1 + +// FromContext returns an Instance from the given context, or nil if the context doesn't contain one. +func FromContext(ctx context.Context) *Instance { + if s, ok := ctx.Value(xrayKey).(*Instance); ok { + return s + } + return nil +} + +// MustFromContext returns an Instance from the given context, or panics if not present. +func MustFromContext(ctx context.Context) *Instance { + x := FromContext(ctx) + if x == nil { + panic("X is not in context.") + } + return x +} + +/* + toContext returns ctx from the given context, or creates an Instance if the context doesn't find that. + +It is unsupported to use this function to create a context that is suitable to invoke Xray's internal component +in third party code, you shouldn't use //go:linkname to alias of this function into your own package and +use this function in your third party code. + +For third party code, usage enabled by creating a context to interact with Xray's internal component is unsupported, +and may break at any time. +*/ +func toContext(ctx context.Context, v *Instance) context.Context { + if FromContext(ctx) != v { + ctx = context.WithValue(ctx, xrayKey, v) + } + return ctx +} + +/* +ToBackgroundDetachedContext create a detached context from another context +Internal API +*/ +func ToBackgroundDetachedContext(ctx context.Context) context.Context { + instance := MustFromContext(ctx) + return toContext(context.Background(), instance) +} diff --git a/subproject/Xray-core-main/core/context_test.go b/subproject/Xray-core-main/core/context_test.go new file mode 100644 index 00000000..ec640dfd --- /dev/null +++ b/subproject/Xray-core-main/core/context_test.go @@ -0,0 +1,20 @@ +package core_test + +import ( + "context" + "testing" + _ "unsafe" + + . "github.com/xtls/xray-core/core" +) + +func TestFromContextPanic(t *testing.T) { + defer func() { + r := recover() + if r == nil { + t.Error("expect panic, but nil") + } + }() + + MustFromContext(context.Background()) +} diff --git a/subproject/Xray-core-main/core/core.go b/subproject/Xray-core-main/core/core.go new file mode 100644 index 00000000..c5a8130f --- /dev/null +++ b/subproject/Xray-core-main/core/core.go @@ -0,0 +1,72 @@ +// Package core provides an entry point to use Xray core functionalities. +// +// Xray makes it possible to accept incoming network connections with certain +// protocol, process the data, and send them through another connection with +// the same or a difference protocol on demand. +// +// It may be configured to work with multiple protocols at the same time, and +// uses the internal router to tunnel through different inbound and outbound +// connections. +package core + +import ( + "fmt" + "runtime" + "runtime/debug" + + "github.com/xtls/xray-core/common/serial" +) + +var ( + Version_x byte = 26 + Version_y byte = 3 + Version_z byte = 27 +) + +var ( + build = "Custom" + codename = "Xray, Penetrates Everything." + intro = "A unified platform for anti-censorship." +) + +func init() { + // Manually injected + if build != "Custom" { + return + } + info, ok := debug.ReadBuildInfo() + if !ok { + return + } + var isDirty bool + var foundBuild bool + for _, setting := range info.Settings { + switch setting.Key { + case "vcs.revision": + if len(setting.Value) < 7 { + return + } + build = setting.Value[:7] + foundBuild = true + case "vcs.modified": + isDirty = setting.Value == "true" + } + } + if isDirty && foundBuild { + build += "-dirty" + } +} + +// Version returns Xray's version as a string, in the form of "x.y.z" where x, y and z are numbers. +// ".z" part may be omitted in regular releases. +func Version() string { + return fmt.Sprintf("%v.%v.%v", Version_x, Version_y, Version_z) +} + +// VersionStatement returns a list of strings representing the full version info. +func VersionStatement() []string { + return []string{ + serial.Concat("Xray ", Version(), " (", codename, ") ", build, " (", runtime.Version(), " ", runtime.GOOS, "/", runtime.GOARCH, ")"), + intro, + } +} diff --git a/subproject/Xray-core-main/core/format.go b/subproject/Xray-core-main/core/format.go new file mode 100644 index 00000000..7d88c1c0 --- /dev/null +++ b/subproject/Xray-core-main/core/format.go @@ -0,0 +1,5 @@ +package core + +//go:generate go install -v github.com/daixiang0/gci@latest +//go:generate go install -v mvdan.cc/gofumpt@latest +//go:generate go run ../infra/vformat/main.go -pwd ./.. diff --git a/subproject/Xray-core-main/core/functions.go b/subproject/Xray-core-main/core/functions.go new file mode 100644 index 00000000..09c4b5dd --- /dev/null +++ b/subproject/Xray-core-main/core/functions.go @@ -0,0 +1,84 @@ +package core + +import ( + "bytes" + "context" + + "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/net/cnc" + "github.com/xtls/xray-core/features/routing" + "github.com/xtls/xray-core/transport/internet/udp" +) + +// CreateObject creates a new object based on the given Xray instance and config. The Xray instance may be nil. +func CreateObject(v *Instance, config interface{}) (interface{}, error) { + ctx := v.ctx + if v != nil { + ctx = toContext(v.ctx, v) + } + return common.CreateObject(ctx, config) +} + +// StartInstance starts a new Xray instance with given serialized config. +// By default Xray only support config in protobuf format, i.e., configFormat = "protobuf". Caller need to load other packages to add JSON support. +// +// xray:api:stable +func StartInstance(configFormat string, configBytes []byte) (*Instance, error) { + config, err := LoadConfig(configFormat, bytes.NewReader(configBytes)) + if err != nil { + return nil, err + } + instance, err := New(config) + if err != nil { + return nil, err + } + if err := instance.Start(); err != nil { + return nil, err + } + return instance, nil +} + +// Dial provides an easy way for upstream caller to create net.Conn through Xray. +// It dispatches the request to the given destination by the given Xray instance. +// Since it is under a proxy context, the LocalAddr() and RemoteAddr() in returned net.Conn +// will not show real addresses being used for communication. +// +// xray:api:stable +func Dial(ctx context.Context, v *Instance, dest net.Destination) (net.Conn, error) { + ctx = toContext(ctx, v) + + dispatcher := v.GetFeature(routing.DispatcherType()) + if dispatcher == nil { + return nil, errors.New("routing.Dispatcher is not registered in Xray core") + } + + r, err := dispatcher.(routing.Dispatcher).Dispatch(ctx, dest) + if err != nil { + return nil, err + } + var readerOpt cnc.ConnectionOption + if dest.Network == net.Network_TCP { + readerOpt = cnc.ConnectionOutputMulti(r.Reader) + } else { + readerOpt = cnc.ConnectionOutputMultiUDP(r.Reader) + } + return cnc.NewConnection(cnc.ConnectionInputMulti(r.Writer), readerOpt), nil +} + +// DialUDP provides a way to exchange UDP packets through Xray instance to remote servers. +// Since it is under a proxy context, the LocalAddr() in returned PacketConn will not show the real address. +// +// TODO: SetDeadline() / SetReadDeadline() / SetWriteDeadline() are not implemented. +// +// xray:api:beta +func DialUDP(ctx context.Context, v *Instance) (net.PacketConn, error) { + ctx = toContext(ctx, v) + + dispatcher := v.GetFeature(routing.DispatcherType()) + if dispatcher == nil { + return nil, errors.New("routing.Dispatcher is not registered in Xray core") + } + return udp.DialDispatcher(ctx, dispatcher.(routing.Dispatcher)) +} diff --git a/subproject/Xray-core-main/core/functions_test.go b/subproject/Xray-core-main/core/functions_test.go new file mode 100644 index 00000000..5658de1c --- /dev/null +++ b/subproject/Xray-core-main/core/functions_test.go @@ -0,0 +1,230 @@ +package core_test + +import ( + "context" + "crypto/rand" + "io" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/xtls/xray-core/app/dispatcher" + "github.com/xtls/xray-core/app/proxyman" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/serial" + core "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/proxy/freedom" + "github.com/xtls/xray-core/testing/servers/tcp" + "github.com/xtls/xray-core/testing/servers/udp" + "google.golang.org/protobuf/proto" +) + +func xor(b []byte) []byte { + r := make([]byte, len(b)) + for i, v := range b { + r[i] = v ^ 'c' + } + return r +} + +func xor2(b []byte) []byte { + r := make([]byte, len(b)) + for i, v := range b { + r[i] = v ^ 'd' + } + return r +} + +func TestXrayDial(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + config := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&dispatcher.Config{}), + serial.ToTypedMessage(&proxyman.InboundConfig{}), + serial.ToTypedMessage(&proxyman.OutboundConfig{}), + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + cfgBytes, err := proto.Marshal(config) + common.Must(err) + + server, err := core.StartInstance("protobuf", cfgBytes) + common.Must(err) + defer server.Close() + + conn, err := core.Dial(context.Background(), server, dest) + common.Must(err) + defer conn.Close() + + const size = 10240 * 1024 + payload := make([]byte, size) + common.Must2(rand.Read(payload)) + + if _, err := conn.Write(payload); err != nil { + t.Fatal(err) + } + + receive := make([]byte, size) + if _, err := io.ReadFull(conn, receive); err != nil { + t.Fatal("failed to read all response: ", err) + } + + if r := cmp.Diff(xor(receive), payload); r != "" { + t.Error(r) + } +} + +func TestXrayDialUDPConn(t *testing.T) { + udpServer := udp.Server{ + MsgProcessor: xor, + } + dest, err := udpServer.Start() + common.Must(err) + defer udpServer.Close() + + config := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&dispatcher.Config{}), + serial.ToTypedMessage(&proxyman.InboundConfig{}), + serial.ToTypedMessage(&proxyman.OutboundConfig{}), + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + cfgBytes, err := proto.Marshal(config) + common.Must(err) + + server, err := core.StartInstance("protobuf", cfgBytes) + common.Must(err) + defer server.Close() + + conn, err := core.Dial(context.Background(), server, dest) + common.Must(err) + defer conn.Close() + + const size = 1024 + payload := make([]byte, size) + common.Must2(rand.Read(payload)) + + for i := 0; i < 2; i++ { + if _, err := conn.Write(payload); err != nil { + t.Fatal(err) + } + } + + time.Sleep(time.Millisecond * 500) + + receive := make([]byte, size*2) + for i := 0; i < 2; i++ { + n, err := conn.Read(receive) + if err != nil { + t.Fatal("expect no error, but got ", err) + } + if n != size { + t.Fatal("expect read size ", size, " but got ", n) + } + + if r := cmp.Diff(xor(receive[:n]), payload); r != "" { + t.Fatal(r) + } + } +} + +func TestXrayDialUDP(t *testing.T) { + udpServer1 := udp.Server{ + MsgProcessor: xor, + } + dest1, err := udpServer1.Start() + common.Must(err) + defer udpServer1.Close() + + udpServer2 := udp.Server{ + MsgProcessor: xor2, + } + dest2, err := udpServer2.Start() + common.Must(err) + defer udpServer2.Close() + + config := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&dispatcher.Config{}), + serial.ToTypedMessage(&proxyman.InboundConfig{}), + serial.ToTypedMessage(&proxyman.OutboundConfig{}), + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + cfgBytes, err := proto.Marshal(config) + common.Must(err) + + server, err := core.StartInstance("protobuf", cfgBytes) + common.Must(err) + defer server.Close() + + conn, err := core.DialUDP(context.Background(), server) + common.Must(err) + defer conn.Close() + + const size = 1024 + { + payload := make([]byte, size) + common.Must2(rand.Read(payload)) + + if _, err := conn.WriteTo(payload, &net.UDPAddr{ + IP: dest1.Address.IP(), + Port: int(dest1.Port), + }); err != nil { + t.Fatal(err) + } + + receive := make([]byte, size) + if _, _, err := conn.ReadFrom(receive); err != nil { + t.Fatal(err) + } + + if r := cmp.Diff(xor(receive), payload); r != "" { + t.Error(r) + } + } + + { + payload := make([]byte, size) + common.Must2(rand.Read(payload)) + + if _, err := conn.WriteTo(payload, &net.UDPAddr{ + IP: dest2.Address.IP(), + Port: int(dest2.Port), + }); err != nil { + t.Fatal(err) + } + + receive := make([]byte, size) + if _, _, err := conn.ReadFrom(receive); err != nil { + t.Fatal(err) + } + + if r := cmp.Diff(xor2(receive), payload); r != "" { + t.Error(r) + } + } +} diff --git a/subproject/Xray-core-main/core/mocks.go b/subproject/Xray-core-main/core/mocks.go new file mode 100644 index 00000000..22e47974 --- /dev/null +++ b/subproject/Xray-core-main/core/mocks.go @@ -0,0 +1,8 @@ +package core + +//go:generate go run github.com/golang/mock/mockgen -package mocks -destination ../testing/mocks/io.go -mock_names Reader=Reader,Writer=Writer io Reader,Writer +//go:generate go run github.com/golang/mock/mockgen -package mocks -destination ../testing/mocks/log.go -mock_names Handler=LogHandler github.com/xtls/xray-core/common/log Handler +//go:generate go run github.com/golang/mock/mockgen -package mocks -destination ../testing/mocks/mux.go -mock_names ClientWorkerFactory=MuxClientWorkerFactory github.com/xtls/xray-core/common/mux ClientWorkerFactory +//go:generate go run github.com/golang/mock/mockgen -package mocks -destination ../testing/mocks/dns.go -mock_names Client=DNSClient github.com/xtls/xray-core/features/dns Client +//go:generate go run github.com/golang/mock/mockgen -package mocks -destination ../testing/mocks/outbound.go -mock_names Manager=OutboundManager,HandlerSelector=OutboundHandlerSelector github.com/xtls/xray-core/features/outbound Manager,HandlerSelector +//go:generate go run github.com/golang/mock/mockgen -package mocks -destination ../testing/mocks/proxy.go -mock_names Inbound=ProxyInbound,Outbound=ProxyOutbound github.com/xtls/xray-core/proxy Inbound,Outbound diff --git a/subproject/Xray-core-main/core/proto.go b/subproject/Xray-core-main/core/proto.go new file mode 100644 index 00000000..030365cc --- /dev/null +++ b/subproject/Xray-core-main/core/proto.go @@ -0,0 +1,5 @@ +package core + +//go:generate go install -v google.golang.org/protobuf/cmd/protoc-gen-go@latest +//go:generate go install -v google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest +//go:generate go run ../infra/vprotogen/main.go -pwd ./.. diff --git a/subproject/Xray-core-main/core/xray.go b/subproject/Xray-core-main/core/xray.go new file mode 100644 index 00000000..58135c96 --- /dev/null +++ b/subproject/Xray-core-main/core/xray.go @@ -0,0 +1,398 @@ +package core + +import ( + "context" + "reflect" + "sync" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/platform" + "github.com/xtls/xray-core/common/serial" + "github.com/xtls/xray-core/features" + "github.com/xtls/xray-core/features/dns" + "github.com/xtls/xray-core/features/dns/localdns" + "github.com/xtls/xray-core/features/inbound" + "github.com/xtls/xray-core/features/outbound" + "github.com/xtls/xray-core/features/policy" + "github.com/xtls/xray-core/features/routing" + "github.com/xtls/xray-core/features/stats" + "github.com/xtls/xray-core/transport/internet" +) + +// Server is an instance of Xray. At any time, there must be at most one Server instance running. +type Server interface { + common.Runnable +} + +// ServerType returns the type of the server. +func ServerType() interface{} { + return (*Instance)(nil) +} + +type resolution struct { + deps []reflect.Type + callback interface{} +} + +func getFeature(allFeatures []features.Feature, t reflect.Type) features.Feature { + for _, f := range allFeatures { + if reflect.TypeOf(f.Type()) == t { + return f + } + } + return nil +} + +func (r *resolution) callbackResolution(allFeatures []features.Feature) error { + callback := reflect.ValueOf(r.callback) + var input []reflect.Value + callbackType := callback.Type() + for i := 0; i < callbackType.NumIn(); i++ { + pt := callbackType.In(i) + for _, f := range allFeatures { + if reflect.TypeOf(f).AssignableTo(pt) { + input = append(input, reflect.ValueOf(f)) + break + } + } + } + + if len(input) != callbackType.NumIn() { + panic("Can't get all input parameters") + } + + var err error + ret := callback.Call(input) + errInterface := reflect.TypeOf((*error)(nil)).Elem() + for i := len(ret) - 1; i >= 0; i-- { + if ret[i].Type() == errInterface { + v := ret[i].Interface() + if v != nil { + err = v.(error) + } + break + } + } + + return err +} + +// Instance combines all Xray features. +type Instance struct { + statusLock sync.Mutex + features []features.Feature + pendingResolutions []resolution + pendingOptionalResolutions []resolution + running bool + resolveLock sync.Mutex + + ctx context.Context +} + +// Instance state +func (server *Instance) IsRunning() bool { + return server.running +} + +func AddInboundHandler(server *Instance, config *InboundHandlerConfig) error { + inboundManager := server.GetFeature(inbound.ManagerType()).(inbound.Manager) + rawHandler, err := CreateObject(server, config) + if err != nil { + return err + } + handler, ok := rawHandler.(inbound.Handler) + if !ok { + return errors.New("not an InboundHandler") + } + if err := inboundManager.AddHandler(server.ctx, handler); err != nil { + return err + } + return nil +} + +func addInboundHandlers(server *Instance, configs []*InboundHandlerConfig) error { + for _, inboundConfig := range configs { + if err := AddInboundHandler(server, inboundConfig); err != nil { + return err + } + } + + return nil +} + +func AddOutboundHandler(server *Instance, config *OutboundHandlerConfig) error { + outboundManager := server.GetFeature(outbound.ManagerType()).(outbound.Manager) + rawHandler, err := CreateObject(server, config) + if err != nil { + return err + } + handler, ok := rawHandler.(outbound.Handler) + if !ok { + return errors.New("not an OutboundHandler") + } + if err := outboundManager.AddHandler(server.ctx, handler); err != nil { + return err + } + return nil +} + +func addOutboundHandlers(server *Instance, configs []*OutboundHandlerConfig) error { + for _, outboundConfig := range configs { + if err := AddOutboundHandler(server, outboundConfig); err != nil { + return err + } + } + + return nil +} + +// RequireFeatures is a helper function to require features from Instance in context. +// See Instance.RequireFeatures for more information. +func RequireFeatures(ctx context.Context, callback interface{}) error { + v := MustFromContext(ctx) + return v.RequireFeatures(callback, false) +} + +// OptionalFeatures is a helper function to aquire features from Instance in context. +// See Instance.RequireFeatures for more information. +func OptionalFeatures(ctx context.Context, callback interface{}) error { + v := MustFromContext(ctx) + return v.RequireFeatures(callback, true) +} + +// New returns a new Xray instance based on given configuration. +// The instance is not started at this point. +// To ensure Xray instance works properly, the config must contain one Dispatcher, one InboundHandlerManager and one OutboundHandlerManager. Other features are optional. +func New(config *Config) (*Instance, error) { + server := &Instance{ctx: context.Background()} + + done, err := initInstanceWithConfig(config, server) + if done { + return nil, err + } + + return server, nil +} + +func NewWithContext(ctx context.Context, config *Config) (*Instance, error) { + server := &Instance{ctx: ctx} + + done, err := initInstanceWithConfig(config, server) + if done { + return nil, err + } + + return server, nil +} + +func initInstanceWithConfig(config *Config, server *Instance) (bool, error) { + server.ctx = context.WithValue(server.ctx, "cone", + platform.NewEnvFlag(platform.UseCone).GetValue(func() string { return "" }) != "true") + + for _, appSettings := range config.App { + settings, err := appSettings.GetInstance() + if err != nil { + return true, err + } + obj, err := CreateObject(server, settings) + if err != nil { + return true, err + } + if feature, ok := obj.(features.Feature); ok { + if err := server.AddFeature(feature); err != nil { + return true, err + } + } + } + + essentialFeatures := []struct { + Type interface{} + Instance features.Feature + }{ + {dns.ClientType(), localdns.New()}, + {policy.ManagerType(), policy.DefaultManager{}}, + {routing.RouterType(), routing.DefaultRouter{}}, + {stats.ManagerType(), stats.NoopManager{}}, + } + + for _, f := range essentialFeatures { + if server.GetFeature(f.Type) == nil { + if err := server.AddFeature(f.Instance); err != nil { + return true, err + } + } + } + + internet.InitSystemDialer( + server.GetFeature(dns.ClientType()).(dns.Client), + func() outbound.Manager { + obm, _ := server.GetFeature(outbound.ManagerType()).(outbound.Manager) + return obm + }(), + ) + + server.resolveLock.Lock() + if server.pendingResolutions != nil { + server.resolveLock.Unlock() + return true, errors.New("not all dependencies are resolved.") + } + server.resolveLock.Unlock() + + if err := addInboundHandlers(server, config.Inbound); err != nil { + return true, err + } + + if err := addOutboundHandlers(server, config.Outbound); err != nil { + return true, err + } + return false, nil +} + +// Type implements common.HasType. +func (s *Instance) Type() interface{} { + return ServerType() +} + +// Close shutdown the Xray instance. +func (s *Instance) Close() error { + s.statusLock.Lock() + defer s.statusLock.Unlock() + + s.running = false + + var errs []interface{} + for _, f := range s.features { + if err := f.Close(); err != nil { + errs = append(errs, err) + } + } + if len(errs) > 0 { + return errors.New("failed to close all features").Base(errors.New(serial.Concat(errs...))) + } + + return nil +} + +// RequireFeatures registers a callback, which will be called when all dependent features are registered. +// The callback must be a func(). All its parameters must be features.Feature. +func (s *Instance) RequireFeatures(callback interface{}, optional bool) error { + callbackType := reflect.TypeOf(callback) + if callbackType.Kind() != reflect.Func { + panic("not a function") + } + + var featureTypes []reflect.Type + for i := 0; i < callbackType.NumIn(); i++ { + featureTypes = append(featureTypes, reflect.PtrTo(callbackType.In(i))) + } + + r := resolution{ + deps: featureTypes, + callback: callback, + } + + s.resolveLock.Lock() + foundAll := true + for _, d := range r.deps { + f := getFeature(s.features, d) + if f == nil { + foundAll = false + break + } + } + if foundAll { + s.resolveLock.Unlock() + return r.callbackResolution(s.features) + } else { + if optional { + s.pendingOptionalResolutions = append(s.pendingOptionalResolutions, r) + } else { + s.pendingResolutions = append(s.pendingResolutions, r) + } + s.resolveLock.Unlock() + return nil + } +} + +// AddFeature registers a feature into current Instance. +func (s *Instance) AddFeature(feature features.Feature) error { + if s.running { + if err := feature.Start(); err != nil { + errors.LogInfoInner(s.ctx, err, "failed to start feature") + } + return nil + } + + s.resolveLock.Lock() + s.features = append(s.features, feature) + + var availableResolution []resolution + var pending []resolution + for _, r := range s.pendingResolutions { + foundAll := true + for _, d := range r.deps { + f := getFeature(s.features, d) + if f == nil { + foundAll = false + break + } + } + if foundAll { + availableResolution = append(availableResolution, r) + } else { + pending = append(pending, r) + } + } + s.pendingResolutions = pending + + var pendingOptional []resolution + for _, r := range s.pendingOptionalResolutions { + foundAll := true + for _, d := range r.deps { + f := getFeature(s.features, d) + if f == nil { + foundAll = false + break + } + } + if foundAll { + availableResolution = append(availableResolution, r) + } else { + pendingOptional = append(pendingOptional, r) + } + } + s.pendingOptionalResolutions = pendingOptional + s.resolveLock.Unlock() + + var err error + for _, r := range availableResolution { + err = r.callbackResolution(s.features) // only return the last error for now + } + return err +} + +// GetFeature returns a feature of the given type, or nil if such feature is not registered. +func (s *Instance) GetFeature(featureType interface{}) features.Feature { + return getFeature(s.features, reflect.TypeOf(featureType)) +} + +// Start starts the Xray instance, including all registered features. When Start returns error, the state of the instance is unknown. +// A Xray instance can be started only once. Upon closing, the instance is not guaranteed to start again. +// +// xray:api:stable +func (s *Instance) Start() error { + s.statusLock.Lock() + defer s.statusLock.Unlock() + + s.running = true + for _, f := range s.features { + if err := f.Start(); err != nil { + return err + } + } + + errors.LogWarning(s.ctx, "Xray ", Version(), " started") + + return nil +} diff --git a/subproject/Xray-core-main/core/xray_test.go b/subproject/Xray-core-main/core/xray_test.go new file mode 100644 index 00000000..65ff3fc8 --- /dev/null +++ b/subproject/Xray-core-main/core/xray_test.go @@ -0,0 +1,86 @@ +package core_test + +import ( + "testing" + + "github.com/xtls/xray-core/app/dispatcher" + "github.com/xtls/xray-core/app/proxyman" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/serial" + "github.com/xtls/xray-core/common/uuid" + . "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/features/dns" + "github.com/xtls/xray-core/features/dns/localdns" + _ "github.com/xtls/xray-core/main/distro/all" + "github.com/xtls/xray-core/proxy/dokodemo" + "github.com/xtls/xray-core/proxy/vmess" + "github.com/xtls/xray-core/proxy/vmess/outbound" + "github.com/xtls/xray-core/testing/servers/tcp" + "google.golang.org/protobuf/proto" +) + +func TestXrayDependency(t *testing.T) { + instance := new(Instance) + + wait := make(chan bool, 1) + instance.RequireFeatures(func(d dns.Client) { + if d == nil { + t.Error("expected dns client fulfilled, but actually nil") + } + wait <- true + }, false) + instance.AddFeature(localdns.New()) + <-wait +} + +func TestXrayClose(t *testing.T) { + port := tcp.PickPort() + + userID := uuid.New() + config := &Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&dispatcher.Config{}), + serial.ToTypedMessage(&proxyman.InboundConfig{}), + serial.ToTypedMessage(&proxyman.OutboundConfig{}), + }, + Inbound: []*InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{ + Range: []*net.PortRange{net.SinglePortRange(port)}, + }, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(0), + Networks: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(0), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }), + }, + }, + } + + cfgBytes, err := proto.Marshal(config) + common.Must(err) + + server, err := StartInstance("protobuf", cfgBytes) + common.Must(err) + server.Close() +} diff --git a/subproject/Xray-core-main/features/dns/client.go b/subproject/Xray-core-main/features/dns/client.go new file mode 100644 index 00000000..6eb4bd08 --- /dev/null +++ b/subproject/Xray-core-main/features/dns/client.go @@ -0,0 +1,72 @@ +package dns + +import ( + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/serial" + "github.com/xtls/xray-core/features" +) + +// IPOption is an object for IP query options. +type IPOption struct { + IPv4Enable bool + IPv6Enable bool + FakeEnable bool +} + +// Client is a Xray feature for querying DNS information. +// +// xray:api:stable +type Client interface { + features.Feature + + // 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) +} + +// ClientType returns the type of Client interface. Can be used for implementing common.HasType. +// +// xray:api:beta +func ClientType() interface{} { + return (*Client)(nil) +} + +// ErrEmptyResponse indicates that DNS query succeeded but no answer was returned. +var ErrEmptyResponse = errors.New("empty response") + +const DefaultTTL = 300 + +type RCodeError uint16 + +func (e RCodeError) Error() string { + return serial.Concat("rcode: ", uint16(e)) +} + +func (RCodeError) IP() net.IP { + panic("Calling IP() on a RCodeError.") +} + +func (RCodeError) Domain() string { + panic("Calling Domain() on a RCodeError.") +} + +func (RCodeError) Family() net.AddressFamily { + panic("Calling Family() on a RCodeError.") +} + +func (e RCodeError) String() string { + return e.Error() +} + +var _ net.Address = (*RCodeError)(nil) + +func RCodeFromError(err error) uint16 { + if err == nil { + return 0 + } + cause := errors.Cause(err) + if r, ok := cause.(RCodeError); ok { + return uint16(r) + } + return 0 +} diff --git a/subproject/Xray-core-main/features/dns/fakedns.go b/subproject/Xray-core-main/features/dns/fakedns.go new file mode 100644 index 00000000..7aff1fcb --- /dev/null +++ b/subproject/Xray-core-main/features/dns/fakedns.go @@ -0,0 +1,23 @@ +package dns + +import ( + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/features" +) + +type FakeDNSEngine interface { + features.Feature + GetFakeIPForDomain(domain string) []net.Address + GetDomainFromFakeDNS(ip net.Address) string +} + +var ( + FakeIPv4Pool = "198.18.0.0/15" + FakeIPv6Pool = "fc00::/18" +) + +type FakeDNSEngineRev0 interface { + FakeDNSEngine + IsIPInIPPool(ip net.Address) bool + GetFakeIPForDomain3(domain string, IPv4, IPv6 bool) []net.Address +} diff --git a/subproject/Xray-core-main/features/dns/localdns/client.go b/subproject/Xray-core-main/features/dns/localdns/client.go new file mode 100644 index 00000000..48e740ee --- /dev/null +++ b/subproject/Xray-core-main/features/dns/localdns/client.go @@ -0,0 +1,66 @@ +package localdns + +import ( + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/features/dns" +) + +// Client is an implementation of dns.Client, which queries localhost for DNS. +type Client struct{} + +// Type implements common.HasType. +func (*Client) Type() interface{} { + return dns.ClientType() +} + +// Start implements common.Runnable. +func (*Client) Start() error { return nil } + +// Close implements common.Closable. +func (*Client) Close() error { return nil } + +// LookupIP implements Client. +func (*Client) LookupIP(host string, option dns.IPOption) ([]net.IP, uint32, error) { + ips, err := net.LookupIP(host) + if err != nil { + return nil, 0, err + } + parsedIPs := make([]net.IP, 0, len(ips)) + ipv4 := make([]net.IP, 0, len(ips)) + ipv6 := make([]net.IP, 0, len(ips)) + for _, ip := range ips { + parsed := net.IPAddress(ip) + if parsed == nil { + continue + } + parsedIP := parsed.IP() + parsedIPs = append(parsedIPs, parsedIP) + + if len(parsedIP) == net.IPv4len { + ipv4 = append(ipv4, parsedIP) + } else { + ipv6 = append(ipv6, parsedIP) + } + } + + switch { + case option.IPv4Enable && option.IPv6Enable: + if len(parsedIPs) > 0 { + return parsedIPs, dns.DefaultTTL, nil + } + case option.IPv4Enable: + if len(ipv4) > 0 { + return ipv4, dns.DefaultTTL, nil + } + case option.IPv6Enable: + if len(ipv6) > 0 { + return ipv6, dns.DefaultTTL, nil + } + } + return nil, 0, dns.ErrEmptyResponse +} + +// New create a new dns.Client that queries localhost for DNS. +func New() *Client { + return &Client{} +} diff --git a/subproject/Xray-core-main/features/extension/contextreceiver.go b/subproject/Xray-core-main/features/extension/contextreceiver.go new file mode 100644 index 00000000..2d339479 --- /dev/null +++ b/subproject/Xray-core-main/features/extension/contextreceiver.go @@ -0,0 +1,7 @@ +package extension + +import "context" + +type ContextReceiver interface { + InjectContext(ctx context.Context) +} diff --git a/subproject/Xray-core-main/features/extension/observatory.go b/subproject/Xray-core-main/features/extension/observatory.go new file mode 100644 index 00000000..1b211ba3 --- /dev/null +++ b/subproject/Xray-core-main/features/extension/observatory.go @@ -0,0 +1,23 @@ +package extension + +import ( + "context" + + "github.com/xtls/xray-core/features" + "google.golang.org/protobuf/proto" +) + +type Observatory interface { + features.Feature + + GetObservation(ctx context.Context) (proto.Message, error) +} + +type BurstObservatory interface { + Observatory + Check(tag []string) +} + +func ObservatoryType() interface{} { + return (*Observatory)(nil) +} diff --git a/subproject/Xray-core-main/features/feature.go b/subproject/Xray-core-main/features/feature.go new file mode 100644 index 00000000..026b8fa5 --- /dev/null +++ b/subproject/Xray-core-main/features/feature.go @@ -0,0 +1,12 @@ +package features + +import ( + "github.com/xtls/xray-core/common" +) + +// Feature is the interface for Xray features. All features must implement this interface. +// All existing features have an implementation in app directory. These features can be replaced by third-party ones. +type Feature interface { + common.HasType + common.Runnable +} diff --git a/subproject/Xray-core-main/features/inbound/inbound.go b/subproject/Xray-core-main/features/inbound/inbound.go new file mode 100644 index 00000000..11caf3f9 --- /dev/null +++ b/subproject/Xray-core-main/features/inbound/inbound.go @@ -0,0 +1,46 @@ +package inbound + +import ( + "context" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/serial" + "github.com/xtls/xray-core/features" +) + +// Handler is the interface for handlers that process inbound connections. +// +// xray:api:stable +type Handler interface { + common.Runnable + // The tag of this handler. + Tag() string + // Returns the active receiver settings. + ReceiverSettings() *serial.TypedMessage + // Returns the active proxy settings. + ProxySettings() *serial.TypedMessage +} + +// Manager is a feature that manages InboundHandlers. +// +// xray:api:stable +type Manager interface { + features.Feature + // GetHandler returns an InboundHandler for the given tag. + GetHandler(ctx context.Context, tag string) (Handler, error) + // AddHandler adds the given handler into this Manager. + AddHandler(ctx context.Context, handler Handler) error + + // RemoveHandler removes a handler from Manager. + RemoveHandler(ctx context.Context, tag string) error + + // ListHandlers returns a list of inbound.Handler. + ListHandlers(ctx context.Context) []Handler +} + +// ManagerType returns the type of Manager interface. Can be used for implementing common.HasType. +// +// xray:api:stable +func ManagerType() interface{} { + return (*Manager)(nil) +} diff --git a/subproject/Xray-core-main/features/outbound/outbound.go b/subproject/Xray-core-main/features/outbound/outbound.go new file mode 100644 index 00000000..ecde6e1e --- /dev/null +++ b/subproject/Xray-core-main/features/outbound/outbound.go @@ -0,0 +1,51 @@ +package outbound + +import ( + "context" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/serial" + "github.com/xtls/xray-core/features" + "github.com/xtls/xray-core/transport" +) + +// Handler is the interface for handlers that process outbound connections. +// +// xray:api:stable +type Handler interface { + common.Runnable + Tag() string + Dispatch(ctx context.Context, link *transport.Link) + SenderSettings() *serial.TypedMessage + ProxySettings() *serial.TypedMessage +} + +type HandlerSelector interface { + Select([]string) []string +} + +// Manager is a feature that manages outbound.Handlers. +// +// xray:api:stable +type Manager interface { + features.Feature + // GetHandler returns an outbound.Handler for the given tag. + GetHandler(tag string) Handler + // GetDefaultHandler returns the default outbound.Handler. It is usually the first outbound.Handler specified in the configuration. + GetDefaultHandler() Handler + // AddHandler adds a handler into this outbound.Manager. + AddHandler(ctx context.Context, handler Handler) error + + // RemoveHandler removes a handler from outbound.Manager. + RemoveHandler(ctx context.Context, tag string) error + + // ListHandlers returns a list of outbound.Handler. + ListHandlers(ctx context.Context) []Handler +} + +// ManagerType returns the type of Manager interface. Can be used to implement common.HasType. +// +// xray:api:stable +func ManagerType() interface{} { + return (*Manager)(nil) +} diff --git a/subproject/Xray-core-main/features/policy/default.go b/subproject/Xray-core-main/features/policy/default.go new file mode 100644 index 00000000..8d363c7e --- /dev/null +++ b/subproject/Xray-core-main/features/policy/default.go @@ -0,0 +1,37 @@ +package policy + +import ( + "time" +) + +// DefaultManager is the implementation of the Manager. +type DefaultManager struct{} + +// Type implements common.HasType. +func (DefaultManager) Type() interface{} { + return ManagerType() +} + +// ForLevel implements Manager. +func (DefaultManager) ForLevel(level uint32) Session { + p := SessionDefault() + if level == 1 { + p.Timeouts.ConnectionIdle = time.Second * 600 + } + return p +} + +// ForSystem implements Manager. +func (DefaultManager) ForSystem() System { + return System{} +} + +// Start implements common.Runnable. +func (DefaultManager) Start() error { + return nil +} + +// Close implements common.Closable. +func (DefaultManager) Close() error { + return nil +} diff --git a/subproject/Xray-core-main/features/policy/policy.go b/subproject/Xray-core-main/features/policy/policy.go new file mode 100644 index 00000000..d6fd20d0 --- /dev/null +++ b/subproject/Xray-core-main/features/policy/policy.go @@ -0,0 +1,150 @@ +package policy + +import ( + "context" + "runtime" + "time" + + "github.com/xtls/xray-core/common/platform" + "github.com/xtls/xray-core/features" +) + +// Timeout contains limits for connection timeout. +type Timeout struct { + // Timeout for handshake phase in a connection. + Handshake time.Duration + // Timeout for connection being idle, i.e., there is no egress or ingress traffic in this connection. + ConnectionIdle time.Duration + // Timeout for an uplink only connection, i.e., the downlink of the connection has been closed. + UplinkOnly time.Duration + // Timeout for an downlink only connection, i.e., the uplink of the connection has been closed. + DownlinkOnly time.Duration +} + +// Stats contains settings for stats counters. +type Stats struct { + // Whether or not to enable stat counter for user uplink traffic. + UserUplink bool + // Whether or not to enable stat counter for user downlink traffic. + UserDownlink bool + // Whether or not to enable online map for user. + UserOnline bool +} + +// Buffer contains settings for internal buffer. +type Buffer struct { + // Size of buffer per connection, in bytes. -1 for unlimited buffer. + PerConnection int32 +} + +// SystemStats contains stat policy settings on system level. +type SystemStats struct { + // Whether or not to enable stat counter for uplink traffic in inbound handlers. + InboundUplink bool + // Whether or not to enable stat counter for downlink traffic in inbound handlers. + InboundDownlink bool + // Whether or not to enable stat counter for uplink traffic in outbound handlers. + OutboundUplink bool + // Whether or not to enable stat counter for downlink traffic in outbound handlers. + OutboundDownlink bool +} + +// System contains policy settings at system level. +type System struct { + Stats SystemStats + Buffer Buffer +} + +// Session is session based settings for controlling Xray requests. It contains various settings (or limits) that may differ for different users in the context. +type Session struct { + Timeouts Timeout // Timeout settings + Stats Stats + Buffer Buffer +} + +// Manager is a feature that provides Policy for the given user by its id or level. +// +// xray:api:stable +type Manager interface { + features.Feature + + // ForLevel returns the Session policy for the given user level. + ForLevel(level uint32) Session + + // ForSystem returns the System policy for Xray system. + ForSystem() System +} + +// ManagerType returns the type of Manager interface. Can be used to implement common.HasType. +// +// xray:api:stable +func ManagerType() interface{} { + return (*Manager)(nil) +} + +var defaultBufferSize int32 + +func init() { + const defaultValue = -17 + size := platform.NewEnvFlag(platform.BufferSize).GetValueAsInt(defaultValue) + + switch size { + case 0: + defaultBufferSize = -1 // For pipe to use unlimited size + case defaultValue: // Env flag not defined. Use default values per CPU-arch. + switch runtime.GOARCH { + case "arm", "mips", "mipsle": + defaultBufferSize = 0 + case "arm64", "mips64", "mips64le": + defaultBufferSize = 4 * 1024 // 4k cache for low-end devices + default: + defaultBufferSize = 512 * 1024 + } + default: + defaultBufferSize = int32(size) * 1024 * 1024 + } +} + +func defaultBufferPolicy() Buffer { + return Buffer{ + PerConnection: defaultBufferSize, + } +} + +// SessionDefault returns the Policy when user is not specified. +func SessionDefault() Session { + return Session{ + Timeouts: Timeout{ + // Align Handshake timeout with nginx client_header_timeout + // So that this value will not indicate server identity + Handshake: time.Second * 60, + ConnectionIdle: time.Second * 300, + UplinkOnly: time.Second * 1, + DownlinkOnly: time.Second * 1, + }, + Stats: Stats{ + UserUplink: false, + UserDownlink: false, + UserOnline: false, + }, + Buffer: defaultBufferPolicy(), + } +} + +type policyKey int32 + +const ( + bufferPolicyKey policyKey = 0 +) + +func ContextWithBufferPolicy(ctx context.Context, p Buffer) context.Context { + return context.WithValue(ctx, bufferPolicyKey, p) +} + +func BufferPolicyFromContext(ctx context.Context) Buffer { + pPolicy := ctx.Value(bufferPolicyKey) + if pPolicy == nil { + return defaultBufferPolicy() + } + return pPolicy.(Buffer) +} diff --git a/subproject/Xray-core-main/features/routing/balancer.go b/subproject/Xray-core-main/features/routing/balancer.go new file mode 100644 index 00000000..b1a2339f --- /dev/null +++ b/subproject/Xray-core-main/features/routing/balancer.go @@ -0,0 +1,10 @@ +package routing + +type BalancerOverrider interface { + SetOverrideTarget(tag, target string) error + GetOverrideTarget(tag string) (string, error) +} + +type BalancerPrincipleTarget interface { + GetPrincipleTarget(tag string) ([]string, error) +} diff --git a/subproject/Xray-core-main/features/routing/context.go b/subproject/Xray-core-main/features/routing/context.go new file mode 100644 index 00000000..6b38d175 --- /dev/null +++ b/subproject/Xray-core-main/features/routing/context.go @@ -0,0 +1,52 @@ +package routing + +import ( + "github.com/xtls/xray-core/common/net" +) + +// Context is a feature to store connection information for routing. +// +// xray:api:stable +type Context interface { + // GetInboundTag returns the tag of the inbound the connection was from. + GetInboundTag() string + + // GetSourceIPs returns the source IPs bound to the connection. + GetSourceIPs() []net.IP + + // GetSourcePort returns the source port of the connection. + GetSourcePort() net.Port + + // GetTargetIPs returns the target IP of the connection or resolved IPs of target domain. + GetTargetIPs() []net.IP + + // GetTargetPort returns the target port of the connection. + GetTargetPort() net.Port + + // GetLocalIPs returns the local IPs bound to the connection. + GetLocalIPs() []net.IP + + // GetLocalPort returns the local port of the connection. + GetLocalPort() net.Port + + // GetTargetDomain returns the target domain of the connection, if exists. + GetTargetDomain() string + + // GetNetwork returns the network type of the connection. + GetNetwork() net.Network + + // GetProtocol returns the protocol from the connection content, if sniffed out. + GetProtocol() string + + // GetUser returns the user email from the connection content, if exists. + GetUser() string + + // GetVlessRoute returns the user-sent VLESS UUID's 7th<<8 | 8th bytes, if exists. + GetVlessRoute() net.Port + + // GetAttributes returns extra attributes from the conneciont content. + GetAttributes() map[string]string + + // GetSkipDNSResolve returns a flag switch for weather skip dns resolve during route pick. + GetSkipDNSResolve() bool +} diff --git a/subproject/Xray-core-main/features/routing/dispatcher.go b/subproject/Xray-core-main/features/routing/dispatcher.go new file mode 100644 index 00000000..53d3bf90 --- /dev/null +++ b/subproject/Xray-core-main/features/routing/dispatcher.go @@ -0,0 +1,28 @@ +package routing + +import ( + "context" + + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/features" + "github.com/xtls/xray-core/transport" +) + +// Dispatcher is a feature that dispatches inbound requests to outbound handlers based on rules. +// Dispatcher is required to be registered in a Xray instance to make Xray function properly. +// +// xray:api:stable +type Dispatcher interface { + features.Feature + + // Dispatch returns a Ray for transporting data for the given request. + Dispatch(ctx context.Context, dest net.Destination) (*transport.Link, error) + DispatchLink(ctx context.Context, dest net.Destination, link *transport.Link) error +} + +// DispatcherType returns the type of Dispatcher interface. Can be used to implement common.HasType. +// +// xray:api:stable +func DispatcherType() interface{} { + return (*Dispatcher)(nil) +} diff --git a/subproject/Xray-core-main/features/routing/dns/context.go b/subproject/Xray-core-main/features/routing/dns/context.go new file mode 100644 index 00000000..2cf9d261 --- /dev/null +++ b/subproject/Xray-core-main/features/routing/dns/context.go @@ -0,0 +1,56 @@ +package dns + +import ( + "context" + + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/features/dns" + "github.com/xtls/xray-core/features/routing" +) + +// ResolvableContext is an implementation of routing.Context, with domain resolving capability. +type ResolvableContext struct { + routing.Context + dnsClient dns.Client + cacheIPs []net.IP + hasError bool +} + +// GetTargetIPs overrides original routing.Context's implementation. +func (ctx *ResolvableContext) GetTargetIPs() []net.IP { + if len(ctx.cacheIPs) > 0 { + return ctx.cacheIPs + } + + if ctx.hasError { + return nil + } + + if domain := ctx.GetTargetDomain(); len(domain) != 0 { + ips, _, err := ctx.dnsClient.LookupIP(domain, dns.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + FakeEnable: false, + }) + if err == nil { + ctx.cacheIPs = ips + return ips + } + errors.LogInfoInner(context.Background(), err, "resolve ip for ", domain) + } + + if ips := ctx.Context.GetTargetIPs(); len(ips) != 0 { + ctx.cacheIPs = ips + return ips + } + + ctx.hasError = true + return nil +} + +// ContextWithDNSClient creates a new routing context with domain resolving capability. +// Resolved domain IPs can be retrieved by GetTargetIPs(). +func ContextWithDNSClient(ctx routing.Context, client dns.Client) routing.Context { + return &ResolvableContext{Context: ctx, dnsClient: client} +} diff --git a/subproject/Xray-core-main/features/routing/router.go b/subproject/Xray-core-main/features/routing/router.go new file mode 100644 index 00000000..46172de7 --- /dev/null +++ b/subproject/Xray-core-main/features/routing/router.go @@ -0,0 +1,82 @@ +package routing + +import ( + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/serial" + "github.com/xtls/xray-core/features" +) + +// Router is a feature to choose an outbound tag for the given request. +// +// xray:api:stable +type Router interface { + features.Feature + + // PickRoute returns a route decision based on the given routing context. + PickRoute(ctx Context) (Route, error) + AddRule(config *serial.TypedMessage, shouldAppend bool) error + RemoveRule(tag string) error + ListRule() []Route +} + +// Route is the routing result of Router feature. +// +// xray:api:stable +type Route interface { + // A Route is also a routing context. + Context + + // GetOutboundGroupTags returns the detoured outbound group tags in sequence before a final outbound is chosen. + GetOutboundGroupTags() []string + + // GetOutboundTag returns the tag of the outbound the connection was dispatched to. + GetOutboundTag() string + + // GetRuleTag returns the matching rule tag for debugging if exists + GetRuleTag() string +} + +// RouterType return the type of Router interface. Can be used to implement common.HasType. +// +// xray:api:stable +func RouterType() interface{} { + return (*Router)(nil) +} + +// DefaultRouter is an implementation of Router, which always returns ErrNoClue for routing decisions. +type DefaultRouter struct{} + +// Type implements common.HasType. +func (DefaultRouter) Type() interface{} { + return RouterType() +} + +// PickRoute implements Router. +func (DefaultRouter) PickRoute(ctx Context) (Route, error) { + return nil, common.ErrNoClue +} + +// AddRule implements Router. +func (DefaultRouter) AddRule(config *serial.TypedMessage, shouldAppend bool) error { + return common.ErrNoClue +} + +// RemoveRule implements Router. +func (DefaultRouter) RemoveRule(tag string) error { + return common.ErrNoClue +} + +// ListRule implements Router. +func (DefaultRouter) ListRule() []Route { + return nil +} + +// Start implements common.Runnable. +func (DefaultRouter) Start() error { + return nil +} + +// Close implements common.Closable. +func (DefaultRouter) Close() error { + return nil +} diff --git a/subproject/Xray-core-main/features/routing/session/context.go b/subproject/Xray-core-main/features/routing/session/context.go new file mode 100644 index 00000000..45fa8d8f --- /dev/null +++ b/subproject/Xray-core-main/features/routing/session/context.go @@ -0,0 +1,164 @@ +package session + +import ( + "context" + + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/features/routing" +) + +// Context is an implementation of routing.Context, which is a wrapper of context.context with session info. +type Context struct { + Inbound *session.Inbound + Outbound *session.Outbound + Content *session.Content +} + +// GetInboundTag implements routing.Context. +func (ctx *Context) GetInboundTag() string { + if ctx.Inbound == nil { + return "" + } + return ctx.Inbound.Tag +} + +// GetSourceIPs implements routing.Context. +func (ctx *Context) GetSourceIPs() []net.IP { + if ctx.Inbound == nil || !ctx.Inbound.Source.IsValid() { + return nil + } + + if ctx.Inbound.Source.Address.Family().IsIP() { + return []net.IP{ctx.Inbound.Source.Address.IP()} + } + + return nil + +} + +// GetSourcePort implements routing.Context. +func (ctx *Context) GetSourcePort() net.Port { + if ctx.Inbound == nil || !ctx.Inbound.Source.IsValid() { + return 0 + } + return ctx.Inbound.Source.Port +} + +// GetTargetIPs implements routing.Context. +func (ctx *Context) GetTargetIPs() []net.IP { + if ctx.Outbound == nil || !ctx.Outbound.Target.IsValid() { + return nil + } + + if ctx.Outbound.Target.Address.Family().IsIP() { + return []net.IP{ctx.Outbound.Target.Address.IP()} + } + + return nil +} + +// GetTargetPort implements routing.Context. +func (ctx *Context) GetTargetPort() net.Port { + if ctx.Outbound == nil || !ctx.Outbound.Target.IsValid() { + return 0 + } + return ctx.Outbound.Target.Port +} + +// GetLocalIPs implements routing.Context. +func (ctx *Context) GetLocalIPs() []net.IP { + if ctx.Inbound == nil || !ctx.Inbound.Local.IsValid() { + return nil + } + + if ctx.Inbound.Local.Address.Family().IsIP() { + return []net.IP{ctx.Inbound.Local.Address.IP()} + } + + return nil +} + +// GetLocalPort implements routing.Context. +func (ctx *Context) GetLocalPort() net.Port { + if ctx.Inbound == nil || !ctx.Inbound.Local.IsValid() { + return 0 + } + return ctx.Inbound.Local.Port +} + +// GetTargetDomain implements routing.Context. +func (ctx *Context) GetTargetDomain() string { + if ctx.Outbound == nil || !ctx.Outbound.Target.IsValid() { + return "" + } + dest := ctx.Outbound.RouteTarget + if dest.IsValid() && dest.Address.Family().IsDomain() { + return dest.Address.Domain() + } + + dest = ctx.Outbound.Target + if !dest.Address.Family().IsDomain() { + return "" + } + return dest.Address.Domain() +} + +// GetNetwork implements routing.Context. +func (ctx *Context) GetNetwork() net.Network { + if ctx.Outbound == nil { + return net.Network_Unknown + } + return ctx.Outbound.Target.Network +} + +// GetProtocol implements routing.Context. +func (ctx *Context) GetProtocol() string { + if ctx.Content == nil { + return "" + } + return ctx.Content.Protocol +} + +// GetUser implements routing.Context. +func (ctx *Context) GetUser() string { + if ctx.Inbound == nil || ctx.Inbound.User == nil { + return "" + } + return ctx.Inbound.User.Email +} + +// GetVlessRoute implements routing.Context. +func (ctx *Context) GetVlessRoute() net.Port { + if ctx.Inbound == nil { + return 0 + } + return ctx.Inbound.VlessRoute +} + +// GetAttributes implements routing.Context. +func (ctx *Context) GetAttributes() map[string]string { + if ctx.Content == nil { + return nil + } + return ctx.Content.Attributes +} + +// GetSkipDNSResolve implements routing.Context. +func (ctx *Context) GetSkipDNSResolve() bool { + if ctx.Content == nil { + return false + } + return ctx.Content.SkipDNSResolve +} + +// AsRoutingContext creates a context from context.context with session info. +func AsRoutingContext(ctx context.Context) routing.Context { + outbounds := session.OutboundsFromContext(ctx) + ob := outbounds[len(outbounds)-1] + return &Context{ + Inbound: session.InboundFromContext(ctx), + Outbound: ob, + Content: session.ContentFromContext(ctx), + } +} diff --git a/subproject/Xray-core-main/features/stats/stats.go b/subproject/Xray-core-main/features/stats/stats.go new file mode 100644 index 00000000..abea7459 --- /dev/null +++ b/subproject/Xray-core-main/features/stats/stats.go @@ -0,0 +1,207 @@ +package stats + +import ( + "context" + "time" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/features" +) + +// Counter is the interface for stats counters. +// +// xray:api:stable +type Counter interface { + // Value is the current value of the counter. + Value() int64 + // Set sets a new value to the counter, and returns the previous one. + Set(int64) int64 + // Add adds a value to the current counter value, and returns the previous value. + Add(int64) int64 +} + +// OnlineMap is the interface for stats. +// +// xray:api:stable +type OnlineMap interface { + // Count returns the number of unique online IPs. + Count() int + // AddIP increments the reference count for the given IP. + AddIP(string) + // RemoveIP decrements the reference count for the given IP. Deletes at zero. + RemoveIP(string) + // List returns all currently online IPs. + List() []string + // IPTimeMap returns a snapshot copy of IPs to their last-seen times. + IPTimeMap() map[string]time.Time +} + +// Channel is the interface for stats channel. +// +// xray:api:stable +type Channel interface { + // Runnable implies that Channel is a runnable unit. + common.Runnable + // Publish broadcasts a message through the channel with a controlling context. + Publish(context.Context, interface{}) + // Subscribers returns all subscribers. + Subscribers() []chan interface{} + // Subscribe registers for listening to channel stream and returns a new listener channel. + Subscribe() (chan interface{}, error) + // Unsubscribe unregisters a listener channel from current Channel object. + Unsubscribe(chan interface{}) error +} + +// SubscribeRunnableChannel subscribes the channel and starts it if there is first subscriber coming. +func SubscribeRunnableChannel(c Channel) (chan interface{}, error) { + if len(c.Subscribers()) == 0 { + if err := c.Start(); err != nil { + return nil, err + } + } + return c.Subscribe() +} + +// UnsubscribeClosableChannel unsubscribes the channel and close it if there is no more subscriber. +func UnsubscribeClosableChannel(c Channel, sub chan interface{}) error { + if err := c.Unsubscribe(sub); err != nil { + return err + } + if len(c.Subscribers()) == 0 { + return c.Close() + } + return nil +} + +// Manager is the interface for stats manager. +// +// xray:api:stable +type Manager interface { + features.Feature + + // RegisterCounter registers a new counter to the manager. The identifier string must not be empty, and unique among other counters. + RegisterCounter(string) (Counter, error) + // UnregisterCounter unregisters a counter from the manager by its identifier. + UnregisterCounter(string) error + // GetCounter returns a counter by its identifier. + GetCounter(string) Counter + + // RegisterOnlineMap registers a new onlinemap to the manager. The identifier string must not be empty, and unique among other onlinemaps. + RegisterOnlineMap(string) (OnlineMap, error) + // UnregisterOnlineMap unregisters a onlinemap from the manager by its identifier. + UnregisterOnlineMap(string) error + // GetOnlineMap returns a onlinemap by its identifier. + GetOnlineMap(string) OnlineMap + + // RegisterChannel registers a new channel to the manager. The identifier string must not be empty, and unique among other channels. + RegisterChannel(string) (Channel, error) + // UnregisterChannel unregisters a channel from the manager by its identifier. + UnregisterChannel(string) error + // GetChannel returns a channel by its identifier. + GetChannel(string) Channel + + // GetAllOnlineUsers returns all online users from all OnlineMaps. + GetAllOnlineUsers() []string +} + +// GetOrRegisterCounter tries to get the StatCounter first. If not exist, it then tries to create a new counter. +func GetOrRegisterCounter(m Manager, name string) (Counter, error) { + counter := m.GetCounter(name) + if counter != nil { + return counter, nil + } + + return m.RegisterCounter(name) +} + +// GetOrRegisterOnlineMap tries to get the OnlineMap first. If not exist, it then tries to create a new onlinemap. +func GetOrRegisterOnlineMap(m Manager, name string) (OnlineMap, error) { + onlineMap := m.GetOnlineMap(name) + if onlineMap != nil { + return onlineMap, nil + } + + return m.RegisterOnlineMap(name) +} + +// GetOrRegisterChannel tries to get the StatChannel first. If not exist, it then tries to create a new channel. +func GetOrRegisterChannel(m Manager, name string) (Channel, error) { + channel := m.GetChannel(name) + if channel != nil { + return channel, nil + } + + return m.RegisterChannel(name) +} + +// ManagerType returns the type of Manager interface. Can be used to implement common.HasType. +// +// xray:api:stable +func ManagerType() interface{} { + return (*Manager)(nil) +} + +// NoopManager is an implementation of Manager, which doesn't has actual functionalities. +type NoopManager struct{} + +// Type implements common.HasType. +func (NoopManager) Type() interface{} { + return ManagerType() +} + +// RegisterCounter implements Manager. +func (NoopManager) RegisterCounter(string) (Counter, error) { + return nil, errors.New("not implemented") +} + +// UnregisterCounter implements Manager. +func (NoopManager) UnregisterCounter(string) error { + return nil +} + +// GetCounter implements Manager. +func (NoopManager) GetCounter(string) Counter { + return nil +} + +// RegisterOnlineMap implements Manager. +func (NoopManager) RegisterOnlineMap(string) (OnlineMap, error) { + return nil, errors.New("not implemented") +} + +// UnregisterOnlineMap implements Manager. +func (NoopManager) UnregisterOnlineMap(string) error { + return nil +} + +// GetOnlineMap implements Manager. +func (NoopManager) GetOnlineMap(string) OnlineMap { + return nil +} + +// RegisterChannel implements Manager. +func (NoopManager) RegisterChannel(string) (Channel, error) { + return nil, errors.New("not implemented") +} + +// UnregisterChannel implements Manager. +func (NoopManager) UnregisterChannel(string) error { + return nil +} + +// GetChannel implements Manager. +func (NoopManager) GetChannel(string) Channel { + return nil +} + +// GetAllOnlineUsers implements Manager. +func (NoopManager) GetAllOnlineUsers() []string { + return nil +} + +// Start implements common.Runnable. +func (NoopManager) Start() error { return nil } + +// Close implements common.Closable. +func (NoopManager) Close() error { return nil } diff --git a/subproject/Xray-core-main/go.mod b/subproject/Xray-core-main/go.mod new file mode 100644 index 00000000..c9dcff8c --- /dev/null +++ b/subproject/Xray-core-main/go.mod @@ -0,0 +1,54 @@ +module github.com/xtls/xray-core + +go 1.26 + +require ( + github.com/apernet/quic-go v0.59.1-0.20260330051153-c402ee641eb6 + github.com/cloudflare/circl v1.6.3 + github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 + github.com/golang/mock v1.7.0-rc.1 + github.com/google/go-cmp v0.7.0 + github.com/gorilla/websocket v1.5.3 + github.com/klauspost/cpuid/v2 v2.3.0 + github.com/miekg/dns v1.1.72 + github.com/pelletier/go-toml v1.9.5 + github.com/pires/go-proxyproto v0.11.0 + github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af + github.com/sagernet/sing v0.5.1 + github.com/sagernet/sing-shadowsocks v0.2.7 + github.com/stretchr/testify v1.11.1 + github.com/vishvananda/netlink v1.3.1 + github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f + go4.org/netipx v0.0.0-20231129151722-fdeea329fbba + golang.org/x/crypto v0.49.0 + golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 + golang.org/x/net v0.52.0 + golang.org/x/sync v0.20.0 + golang.org/x/sys v0.42.0 + golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 + golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb + google.golang.org/grpc v1.80.0 + google.golang.org/protobuf v1.36.11 + gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 + h12.io/socks v1.0.3 + lukechampine.com/blake3 v1.4.1 +) + +require ( + github.com/andybalholm/brotli v1.0.6 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/google/btree v1.1.2 // indirect + github.com/juju/ratelimit v1.0.2 // indirect + github.com/klauspost/compress v1.17.4 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/quic-go/qpack v0.6.0 // indirect + github.com/vishvananda/netns v0.0.5 // indirect + golang.org/x/mod v0.33.0 // indirect + golang.org/x/text v0.35.0 // indirect + golang.org/x/time v0.12.0 // indirect + golang.org/x/tools v0.42.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/subproject/Xray-core-main/go.sum b/subproject/Xray-core-main/go.sum new file mode 100644 index 00000000..da6e58fe --- /dev/null +++ b/subproject/Xray-core-main/go.sum @@ -0,0 +1,155 @@ +github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI= +github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/apernet/quic-go v0.59.1-0.20260330051153-c402ee641eb6 h1:cbF95uMsQwCwAzH2i8+2lNO2TReoELLuqeeMfyBjFbY= +github.com/apernet/quic-go v0.59.1-0.20260330051153-c402ee641eb6/go.mod h1:Npbg8qBtAZlsAB3FWmqwlVh5jtVG6a4DlYsOylUpvzA= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= +github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/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/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4= +github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U= +github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +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/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/h12w/go-socks5 v0.0.0-20200522160539-76189e178364 h1:5XxdakFhqd9dnXoAZy1Mb2R/DZ6D1e+0bGC/JhucGYI= +github.com/h12w/go-socks5 v0.0.0-20200522160539-76189e178364/go.mod h1:eDJQioIyy4Yn3MVivT7rv/39gAJTrA7lgmYr8EW950c= +github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI= +github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk= +github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= +github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= +github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +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/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= +github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4= +github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU= +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/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af h1:er2acxbi3N1nvEq6HXHUAR1nTWEJmQfqiGR8EVT9rfs= +github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/sagernet/sing v0.5.1 h1:mhL/MZVq0TjuvHcpYcFtmSD1BFOxZ/+8ofbNZcg1k1Y= +github.com/sagernet/sing v0.5.1/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= +github.com/sagernet/sing-shadowsocks v0.2.7 h1:zaopR1tbHEw5Nk6FAkM05wCslV6ahVegEZaKMv9ipx8= +github.com/sagernet/sing-shadowsocks v0.2.7/go.mod h1:0rIKJZBR65Qi0zwdKezt4s57y/Tl1ofkaq6NlkzVuyE= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0= +github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4= +github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= +github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= +github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f h1:iy2JRioxmUpoJ3SzbFPyTxHZMbR/rSHP7dOOgYaq1O8= +github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f/go.mod h1:DsJblcWDGt76+FVqBVwbwRhxyyNJsGV48gJLch0OOWI= +github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= +go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= +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= +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.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= +golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +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-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +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.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-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-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +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.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.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +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-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= +golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= +golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A= +golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= +google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 h1:Lk6hARj5UPY47dBep70OD/TIMwikJ5fGUGX0Rm3Xigk= +gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0/go.mod h1:QkHjoMIBaYtpVufgwv3keYAbln78mBoCuShZrPrer1Q= +h12.io/socks v1.0.3 h1:Ka3qaQewws4j4/eDQnOdpr4wXsC//dXtWvftlIcCQUo= +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.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo= diff --git a/subproject/Xray-core-main/infra/conf/api.go b/subproject/Xray-core-main/infra/conf/api.go new file mode 100644 index 00000000..dca34910 --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/api.go @@ -0,0 +1,50 @@ +package conf + +import ( + "strings" + + "github.com/xtls/xray-core/app/commander" + loggerservice "github.com/xtls/xray-core/app/log/command" + observatoryservice "github.com/xtls/xray-core/app/observatory/command" + handlerservice "github.com/xtls/xray-core/app/proxyman/command" + routerservice "github.com/xtls/xray-core/app/router/command" + statsservice "github.com/xtls/xray-core/app/stats/command" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/serial" +) + +type APIConfig struct { + Tag string `json:"tag"` + Listen string `json:"listen"` + Services []string `json:"services"` +} + +func (c *APIConfig) Build() (*commander.Config, error) { + if c.Tag == "" { + return nil, errors.New("API tag can't be empty.") + } + + services := make([]*serial.TypedMessage, 0, 16) + for _, s := range c.Services { + switch strings.ToLower(s) { + case "reflectionservice": + services = append(services, serial.ToTypedMessage(&commander.ReflectionConfig{})) + case "handlerservice": + services = append(services, serial.ToTypedMessage(&handlerservice.Config{})) + case "loggerservice": + services = append(services, serial.ToTypedMessage(&loggerservice.Config{})) + case "statsservice": + services = append(services, serial.ToTypedMessage(&statsservice.Config{})) + case "observatoryservice": + services = append(services, serial.ToTypedMessage(&observatoryservice.Config{})) + case "routingservice": + services = append(services, serial.ToTypedMessage(&routerservice.Config{})) + } + } + + return &commander.Config{ + Tag: c.Tag, + Listen: c.Listen, + Service: services, + }, nil +} diff --git a/subproject/Xray-core-main/infra/conf/blackhole.go b/subproject/Xray-core-main/infra/conf/blackhole.go new file mode 100644 index 00000000..7b78742f --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/blackhole.go @@ -0,0 +1,51 @@ +package conf + +import ( + "encoding/json" + + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/serial" + "github.com/xtls/xray-core/proxy/blackhole" + "google.golang.org/protobuf/proto" +) + +type NoneResponse struct{} + +func (*NoneResponse) Build() (proto.Message, error) { + return new(blackhole.NoneResponse), nil +} + +type HTTPResponse struct{} + +func (*HTTPResponse) Build() (proto.Message, error) { + return new(blackhole.HTTPResponse), nil +} + +type BlackholeConfig struct { + Response json.RawMessage `json:"response"` +} + +func (v *BlackholeConfig) Build() (proto.Message, error) { + config := new(blackhole.Config) + if v.Response != nil { + response, _, err := configLoader.Load(v.Response) + if err != nil { + return nil, errors.New("Config: Failed to parse Blackhole response config.").Base(err) + } + responseSettings, err := response.(Buildable).Build() + if err != nil { + return nil, err + } + config.Response = serial.ToTypedMessage(responseSettings) + } + + return config, nil +} + +var configLoader = NewJSONConfigLoader( + ConfigCreatorCache{ + "none": func() interface{} { return new(NoneResponse) }, + "http": func() interface{} { return new(HTTPResponse) }, + }, + "type", + "") diff --git a/subproject/Xray-core-main/infra/conf/blackhole_test.go b/subproject/Xray-core-main/infra/conf/blackhole_test.go new file mode 100644 index 00000000..2b0b9295 --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/blackhole_test.go @@ -0,0 +1,34 @@ +package conf_test + +import ( + "testing" + + "github.com/xtls/xray-core/common/serial" + . "github.com/xtls/xray-core/infra/conf" + "github.com/xtls/xray-core/proxy/blackhole" +) + +func TestHTTPResponseJSON(t *testing.T) { + creator := func() Buildable { + return new(BlackholeConfig) + } + + runMultiTestCase(t, []TestCase{ + { + Input: `{ + "response": { + "type": "http" + } + }`, + Parser: loadJSON(creator), + Output: &blackhole.Config{ + Response: serial.ToTypedMessage(&blackhole.HTTPResponse{}), + }, + }, + { + Input: `{}`, + Parser: loadJSON(creator), + Output: &blackhole.Config{}, + }, + }) +} diff --git a/subproject/Xray-core-main/infra/conf/buildable.go b/subproject/Xray-core-main/infra/conf/buildable.go new file mode 100644 index 00000000..967e9740 --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/buildable.go @@ -0,0 +1,7 @@ +package conf + +import "google.golang.org/protobuf/proto" + +type Buildable interface { + Build() (proto.Message, error) +} diff --git a/subproject/Xray-core-main/infra/conf/cfgcommon/duration/duration.go b/subproject/Xray-core-main/infra/conf/cfgcommon/duration/duration.go new file mode 100644 index 00000000..f1bbd4d7 --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/cfgcommon/duration/duration.go @@ -0,0 +1,35 @@ +package duration + +import ( + "encoding/json" + "fmt" + "time" +) + +type Duration int64 + +// MarshalJSON implements encoding/json.Marshaler.MarshalJSON +func (d *Duration) MarshalJSON() ([]byte, error) { + dr := time.Duration(*d) + return json.Marshal(dr.String()) +} + +// UnmarshalJSON implements encoding/json.Unmarshaler.UnmarshalJSON +func (d *Duration) UnmarshalJSON(b []byte) error { + var v interface{} + if err := json.Unmarshal(b, &v); err != nil { + return err + } + switch value := v.(type) { + case string: + var err error + dr, err := time.ParseDuration(value) + if err != nil { + return err + } + *d = Duration(dr) + return nil + default: + return fmt.Errorf("invalid duration: %v", v) + } +} diff --git a/subproject/Xray-core-main/infra/conf/cfgcommon/duration/duration_test.go b/subproject/Xray-core-main/infra/conf/cfgcommon/duration/duration_test.go new file mode 100644 index 00000000..d504531f --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/cfgcommon/duration/duration_test.go @@ -0,0 +1,33 @@ +package duration_test + +import ( + "encoding/json" + "testing" + "time" + + "github.com/xtls/xray-core/infra/conf/cfgcommon/duration" +) + +type testWithDuration struct { + Duration duration.Duration +} + +func TestDurationJSON(t *testing.T) { + expected := &testWithDuration{ + Duration: duration.Duration(time.Hour), + } + data, err := json.Marshal(expected) + if err != nil { + t.Error(err) + return + } + actual := &testWithDuration{} + err = json.Unmarshal(data, &actual) + if err != nil { + t.Error(err) + return + } + if actual.Duration != expected.Duration { + t.Errorf("expected: %s, actual: %s", time.Duration(expected.Duration), time.Duration(actual.Duration)) + } +} diff --git a/subproject/Xray-core-main/infra/conf/common.go b/subproject/Xray-core-main/infra/conf/common.go new file mode 100644 index 00000000..ab3cfba7 --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/common.go @@ -0,0 +1,376 @@ +package conf + +import ( + "encoding/json" + "fmt" + "strconv" + "strings" + + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/platform" + "github.com/xtls/xray-core/common/protocol" +) + +type StringList []string + +func NewStringList(raw []string) *StringList { + list := StringList(raw) + return &list +} + +func (v StringList) Len() int { + return len(v) +} + +// UnmarshalJSON implements encoding/json.Unmarshaler.UnmarshalJSON +func (v *StringList) UnmarshalJSON(data []byte) error { + var strarray []string + if err := json.Unmarshal(data, &strarray); err == nil { + *v = *NewStringList(strarray) + return nil + } + + var rawstr string + if err := json.Unmarshal(data, &rawstr); err == nil { + strlist := strings.Split(rawstr, ",") + *v = *NewStringList(strlist) + return nil + } + return errors.New("unknown format of a string list: " + string(data)) +} + +type Address struct { + net.Address +} + +// MarshalJSON implements encoding/json.Marshaler.MarshalJSON +func (v *Address) MarshalJSON() ([]byte, error) { + return json.Marshal(v.Address.String()) +} + +// UnmarshalJSON implements encoding/json.Unmarshaler.UnmarshalJSON +func (v *Address) UnmarshalJSON(data []byte) error { + var rawStr string + if err := json.Unmarshal(data, &rawStr); err != nil { + return errors.New("invalid address: ", string(data)).Base(err) + } + if strings.HasPrefix(rawStr, "env:") { + rawStr = platform.NewEnvFlag(rawStr[4:]).GetValue(func() string { return "" }) + } + v.Address = net.ParseAddress(rawStr) + + return nil +} + +func (v *Address) Build() *net.IPOrDomain { + return net.NewIPOrDomain(v.Address) +} + +type Network string + +func (v Network) Build() net.Network { + switch strings.ToLower(string(v)) { + case "tcp": + return net.Network_TCP + case "udp": + return net.Network_UDP + case "unix": + return net.Network_UNIX + default: + return net.Network_Unknown + } +} + +type NetworkList []Network + +// UnmarshalJSON implements encoding/json.Unmarshaler.UnmarshalJSON +func (v *NetworkList) UnmarshalJSON(data []byte) error { + var strarray []Network + if err := json.Unmarshal(data, &strarray); err == nil { + nl := NetworkList(strarray) + *v = nl + return nil + } + + var rawstr Network + if err := json.Unmarshal(data, &rawstr); err == nil { + strlist := strings.Split(string(rawstr), ",") + nl := make([]Network, len(strlist)) + for idx, network := range strlist { + nl[idx] = Network(network) + } + *v = nl + return nil + } + return errors.New("unknown format of a string list: " + string(data)) +} + +func (v *NetworkList) Build() []net.Network { + if v == nil { + return []net.Network{net.Network_TCP} + } + + list := make([]net.Network, 0, len(*v)) + for _, network := range *v { + list = append(list, network.Build()) + } + return list +} + +func parseIntPort(data []byte) (net.Port, error) { + var intPort uint32 + err := json.Unmarshal(data, &intPort) + if err != nil { + return net.Port(0), err + } + return net.PortFromInt(intPort) +} + +func parseStringPort(s string) (net.Port, net.Port, error) { + if strings.HasPrefix(s, "env:") { + s = platform.NewEnvFlag(s[4:]).GetValue(func() string { return "" }) + } + + pair := strings.SplitN(s, "-", 2) + if len(pair) == 0 { + return net.Port(0), net.Port(0), errors.New("invalid port range: ", s) + } + if len(pair) == 1 { + port, err := net.PortFromString(pair[0]) + return port, port, err + } + + fromPort, err := net.PortFromString(pair[0]) + if err != nil { + return net.Port(0), net.Port(0), err + } + toPort, err := net.PortFromString(pair[1]) + if err != nil { + return net.Port(0), net.Port(0), err + } + return fromPort, toPort, nil +} + +func parseJSONStringPort(data []byte) (net.Port, net.Port, error) { + var s string + err := json.Unmarshal(data, &s) + if err != nil { + return net.Port(0), net.Port(0), err + } + return parseStringPort(s) +} + +type PortRange struct { + From uint32 + To uint32 +} + +func (v *PortRange) Build() *net.PortRange { + return &net.PortRange{ + From: v.From, + To: v.To, + } +} + +// 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 +func (v *PortRange) UnmarshalJSON(data []byte) error { + port, err := parseIntPort(data) + if err == nil { + v.From = uint32(port) + v.To = uint32(port) + return nil + } + + from, to, err := parseJSONStringPort(data) + if err == nil { + v.From = uint32(from) + v.To = uint32(to) + if v.From > v.To { + return errors.New("invalid port range ", v.From, " -> ", v.To) + } + return nil + } + + return errors.New("invalid port range: ", string(data)) +} + +type PortList struct { + Range []PortRange +} + +func (list *PortList) Build() *net.PortList { + portList := new(net.PortList) + for _, r := range list.Range { + portList.Range = append(portList.Range, r.Build()) + } + 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 +func (list *PortList) UnmarshalJSON(data []byte) error { + var listStr string + var number uint32 + if err := json.Unmarshal(data, &listStr); err != nil { + if err2 := json.Unmarshal(data, &number); err2 != nil { + return errors.New("invalid port: ", string(data)).Base(err2) + } + } + rangelist := strings.Split(listStr, ",") + for _, rangeStr := range rangelist { + trimmed := strings.TrimSpace(rangeStr) + if len(trimmed) > 0 { + if strings.Contains(trimmed, "-") || strings.Contains(trimmed, "env:") { + from, to, err := parseStringPort(trimmed) + if err != nil { + return errors.New("invalid port range: ", trimmed).Base(err) + } + list.Range = append(list.Range, PortRange{From: uint32(from), To: uint32(to)}) + } else { + port, err := parseIntPort([]byte(trimmed)) + if err != nil { + return errors.New("invalid port: ", trimmed).Base(err) + } + list.Range = append(list.Range, PortRange{From: uint32(port), To: uint32(port)}) + } + } + } + if number != 0 { + list.Range = append(list.Range, PortRange{From: number, To: number}) + } + return nil +} + +type User struct { + EmailString string `json:"email"` + LevelByte byte `json:"level"` +} + +func (v *User) Build() *protocol.User { + return &protocol.User{ + Email: v.EmailString, + Level: uint32(v.LevelByte), + } +} + +// Int32Range deserializes from "1-2" or 1, so can deserialize from both int and number. +// Negative integers can be passed as sentinel values, but do not parse as ranges. +// Value will be exchanged if From > To, use .Left and .Right to get original value if need. +type Int32Range struct { + Left int32 + Right int32 + From int32 + To int32 +} + +// MarshalJSON implements encoding/json.Marshaler.MarshalJSON +func (v *Int32Range) MarshalJSON() ([]byte, error) { + return json.Marshal(v.String()) +} + +func (v Int32Range) String() string { + if v.Left == v.Right { + return strconv.Itoa(int(v.Left)) + } else { + return fmt.Sprintf("%d-%d", v.Left, v.Right) + } +} + +// UnmarshalJSON implements encoding/json.Unmarshaler.UnmarshalJSON +func (v *Int32Range) UnmarshalJSON(data []byte) error { + defer v.ensureOrder() + var str string + var rawint int32 + if err := json.Unmarshal(data, &str); err == nil { + left, right, err := ParseRangeString(str) + if err == nil { + v.Left, v.Right = int32(left), int32(right) + return nil + } + } else if err := json.Unmarshal(data, &rawint); err == nil { + v.Left = rawint + v.Right = rawint + return nil + } + + return errors.New("Invalid integer range, expected either string of form \"1-2\" or plain integer.") +} + +// ensureOrder() gives value to .From & .To and make sure .From < .To +func (r *Int32Range) ensureOrder() { + r.From, r.To = r.Left, r.Right + if r.From > r.To { + r.From, r.To = r.To, r.From + } +} + +// "-114-514" → ["-114","514"] +// "-1919--810" → ["-1919","-810"] +func splitFromSecondDash(s string) []string { + parts := strings.SplitN(s, "-", 3) + if len(parts) < 3 { + return []string{s} + } + return []string{parts[0] + "-" + parts[1], parts[2]} +} + +// Parse rang in string. Support negative number. +// eg: "114-514" "-114-514" "-1919--810" "114514" ""(return 0) +func ParseRangeString(str string) (int, int, error) { + // for number in string format like "114" or "-1" + if value, err := strconv.Atoi(str); err == nil { + return value, value, nil + } + // for empty "", we treat it as 0 + if str == "" { + return 0, 0, nil + } + // for range value, like "114-514" + var pair []string + // Process sth like "-114-514" "-1919--810" + if strings.HasPrefix(str, "-") { + pair = splitFromSecondDash(str) + } else { + pair = strings.SplitN(str, "-", 2) + } + if len(pair) == 2 { + left, err := strconv.Atoi(pair[0]) + right, err2 := strconv.Atoi(pair[1]) + if err == nil && err2 == nil { + return left, right, nil + } + } + return 0, 0, errors.New("invalid range string: ", str) +} diff --git a/subproject/Xray-core-main/infra/conf/common_test.go b/subproject/Xray-core-main/infra/conf/common_test.go new file mode 100644 index 00000000..a5484034 --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/common_test.go @@ -0,0 +1,229 @@ +package conf_test + +import ( + "encoding/json" + "os" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + . "github.com/xtls/xray-core/infra/conf" +) + +func TestStringListUnmarshalError(t *testing.T) { + rawJSON := `1234` + list := new(StringList) + err := json.Unmarshal([]byte(rawJSON), list) + if err == nil { + t.Error("expected error, but got nil") + } +} + +func TestStringListLen(t *testing.T) { + rawJSON := `"a, b, c, d"` + var list StringList + err := json.Unmarshal([]byte(rawJSON), &list) + common.Must(err) + if r := cmp.Diff([]string(list), []string{"a", " b", " c", " d"}); r != "" { + t.Error(r) + } +} + +func TestIPParsing(t *testing.T) { + rawJSON := "\"8.8.8.8\"" + var address Address + err := json.Unmarshal([]byte(rawJSON), &address) + common.Must(err) + if r := cmp.Diff(address.IP(), net.IP{8, 8, 8, 8}); r != "" { + t.Error(r) + } +} + +func TestDomainParsing(t *testing.T) { + rawJSON := "\"example.com\"" + var address Address + common.Must(json.Unmarshal([]byte(rawJSON), &address)) + if address.Domain() != "example.com" { + t.Error("domain: ", address.Domain()) + } +} + +func TestURLParsing(t *testing.T) { + { + rawJSON := "\"https://dns.google/dns-query\"" + var address Address + common.Must(json.Unmarshal([]byte(rawJSON), &address)) + if address.Domain() != "https://dns.google/dns-query" { + t.Error("URL: ", address.Domain()) + } + } + { + rawJSON := "\"https+local://dns.google/dns-query\"" + var address Address + common.Must(json.Unmarshal([]byte(rawJSON), &address)) + if address.Domain() != "https+local://dns.google/dns-query" { + t.Error("URL: ", address.Domain()) + } + } +} + +func TestInvalidAddressJson(t *testing.T) { + rawJSON := "1234" + var address Address + err := json.Unmarshal([]byte(rawJSON), &address) + if err == nil { + t.Error("nil error") + } +} + +func TestStringNetwork(t *testing.T) { + var network Network + common.Must(json.Unmarshal([]byte(`"tcp"`), &network)) + if v := network.Build(); v != net.Network_TCP { + t.Error("network: ", v) + } +} + +func TestArrayNetworkList(t *testing.T) { + var list NetworkList + common.Must(json.Unmarshal([]byte("[\"Tcp\"]"), &list)) + + nlist := list.Build() + if !net.HasNetwork(nlist, net.Network_TCP) { + t.Error("no tcp network") + } + if net.HasNetwork(nlist, net.Network_UDP) { + t.Error("has udp network") + } +} + +func TestStringNetworkList(t *testing.T) { + var list NetworkList + common.Must(json.Unmarshal([]byte("\"TCP, ip\""), &list)) + + nlist := list.Build() + if !net.HasNetwork(nlist, net.Network_TCP) { + t.Error("no tcp network") + } + if net.HasNetwork(nlist, net.Network_UDP) { + t.Error("has udp network") + } +} + +func TestInvalidNetworkJson(t *testing.T) { + var list NetworkList + err := json.Unmarshal([]byte("0"), &list) + if err == nil { + t.Error("nil error") + } +} + +func TestIntPort(t *testing.T) { + var portRange PortRange + common.Must(json.Unmarshal([]byte("1234"), &portRange)) + + if r := cmp.Diff(portRange, PortRange{ + From: 1234, To: 1234, + }); r != "" { + t.Error(r) + } +} + +func TestOverRangeIntPort(t *testing.T) { + var portRange PortRange + err := json.Unmarshal([]byte("70000"), &portRange) + if err == nil { + t.Error("nil error") + } + + err = json.Unmarshal([]byte("-1"), &portRange) + if err == nil { + t.Error("nil error") + } +} + +func TestEnvPort(t *testing.T) { + common.Must(os.Setenv("PORT", "1234")) + + var portRange PortRange + common.Must(json.Unmarshal([]byte("\"env:PORT\""), &portRange)) + + if r := cmp.Diff(portRange, PortRange{ + From: 1234, To: 1234, + }); r != "" { + t.Error(r) + } +} + +func TestSingleStringPort(t *testing.T) { + var portRange PortRange + common.Must(json.Unmarshal([]byte("\"1234\""), &portRange)) + + if r := cmp.Diff(portRange, PortRange{ + From: 1234, To: 1234, + }); r != "" { + t.Error(r) + } +} + +func TestStringPairPort(t *testing.T) { + var portRange PortRange + common.Must(json.Unmarshal([]byte("\"1234-5678\""), &portRange)) + + if r := cmp.Diff(portRange, PortRange{ + From: 1234, To: 5678, + }); r != "" { + t.Error(r) + } +} + +func TestOverRangeStringPort(t *testing.T) { + var portRange PortRange + err := json.Unmarshal([]byte("\"65536\""), &portRange) + if err == nil { + t.Error("nil error") + } + + err = json.Unmarshal([]byte("\"70000-80000\""), &portRange) + if err == nil { + t.Error("nil error") + } + + err = json.Unmarshal([]byte("\"1-90000\""), &portRange) + if err == nil { + t.Error("nil error") + } + + err = json.Unmarshal([]byte("\"700-600\""), &portRange) + if err == nil { + t.Error("nil error") + } +} + +func TestUserParsing(t *testing.T) { + user := new(User) + common.Must(json.Unmarshal([]byte(`{ + "id": "96edb838-6d68-42ef-a933-25f7ac3a9d09", + "email": "love@example.com", + "level": 1 + }`), user)) + + nUser := user.Build() + if r := cmp.Diff(nUser, &protocol.User{ + Level: 1, + Email: "love@example.com", + }, cmpopts.IgnoreUnexported(protocol.User{})); r != "" { + t.Error(r) + } +} + +func TestInvalidUserJson(t *testing.T) { + user := new(User) + err := json.Unmarshal([]byte(`{"email": 1234}`), user) + if err == nil { + t.Error("nil error") + } +} diff --git a/subproject/Xray-core-main/infra/conf/conf.go b/subproject/Xray-core-main/infra/conf/conf.go new file mode 100644 index 00000000..8f52a955 --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/conf.go @@ -0,0 +1 @@ +package conf diff --git a/subproject/Xray-core-main/infra/conf/dns.go b/subproject/Xray-core-main/infra/conf/dns.go new file mode 100644 index 00000000..a65f0ee8 --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/dns.go @@ -0,0 +1,576 @@ +package conf + +import ( + "bufio" + "encoding/json" + "os" + "path/filepath" + "runtime" + "sort" + "strings" + + "github.com/xtls/xray-core/app/dns" + "github.com/xtls/xray-core/app/router" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" +) + +type NameServerConfig struct { + Address *Address `json:"address"` + ClientIP *Address `json:"clientIp"` + Port uint16 `json:"port"` + SkipFallback bool `json:"skipFallback"` + Domains []string `json:"domains"` + ExpectedIPs StringList `json:"expectedIPs"` + ExpectIPs StringList `json:"expectIPs"` + QueryStrategy string `json:"queryStrategy"` + Tag string `json:"tag"` + TimeoutMs uint64 `json:"timeoutMs"` + DisableCache *bool `json:"disableCache"` + ServeStale *bool `json:"serveStale"` + ServeExpiredTTL *uint32 `json:"serveExpiredTTL"` + FinalQuery bool `json:"finalQuery"` + UnexpectedIPs StringList `json:"unexpectedIPs"` +} + +// UnmarshalJSON implements encoding/json.Unmarshaler.UnmarshalJSON +func (c *NameServerConfig) UnmarshalJSON(data []byte) error { + var address Address + if err := json.Unmarshal(data, &address); err == nil { + c.Address = &address + return nil + } + + var advanced struct { + Address *Address `json:"address"` + ClientIP *Address `json:"clientIp"` + Port uint16 `json:"port"` + SkipFallback bool `json:"skipFallback"` + Domains []string `json:"domains"` + ExpectedIPs StringList `json:"expectedIPs"` + ExpectIPs StringList `json:"expectIPs"` + QueryStrategy string `json:"queryStrategy"` + Tag string `json:"tag"` + TimeoutMs uint64 `json:"timeoutMs"` + DisableCache *bool `json:"disableCache"` + ServeStale *bool `json:"serveStale"` + ServeExpiredTTL *uint32 `json:"serveExpiredTTL"` + FinalQuery bool `json:"finalQuery"` + UnexpectedIPs StringList `json:"unexpectedIPs"` + } + if err := json.Unmarshal(data, &advanced); err == nil { + c.Address = advanced.Address + c.ClientIP = advanced.ClientIP + c.Port = advanced.Port + c.SkipFallback = advanced.SkipFallback + c.Domains = advanced.Domains + c.ExpectedIPs = advanced.ExpectedIPs + c.ExpectIPs = advanced.ExpectIPs + c.QueryStrategy = advanced.QueryStrategy + c.Tag = advanced.Tag + c.TimeoutMs = advanced.TimeoutMs + c.DisableCache = advanced.DisableCache + c.ServeStale = advanced.ServeStale + c.ServeExpiredTTL = advanced.ServeExpiredTTL + c.FinalQuery = advanced.FinalQuery + c.UnexpectedIPs = advanced.UnexpectedIPs + return nil + } + + return errors.New("failed to parse name server: ", string(data)) +} + +func toDomainMatchingType(t router.Domain_Type) dns.DomainMatchingType { + switch t { + case router.Domain_Domain: + return dns.DomainMatchingType_Subdomain + case router.Domain_Full: + return dns.DomainMatchingType_Full + case router.Domain_Plain: + return dns.DomainMatchingType_Keyword + case router.Domain_Regex: + return dns.DomainMatchingType_Regex + default: + panic("unknown domain type") + } +} + +func (c *NameServerConfig) Build() (*dns.NameServer, error) { + if c.Address == nil { + return nil, errors.New("NameServer address is not specified.") + } + + var domains []*dns.NameServer_PriorityDomain + var originalRules []*dns.NameServer_OriginalRule + + for _, rule := range c.Domains { + parsedDomain, err := parseDomainRule(rule) + if err != nil { + return nil, errors.New("invalid domain rule: ", rule).Base(err) + } + + for _, pd := range parsedDomain { + domains = append(domains, &dns.NameServer_PriorityDomain{ + Type: toDomainMatchingType(pd.Type), + Domain: pd.Value, + }) + } + originalRules = append(originalRules, &dns.NameServer_OriginalRule{ + Rule: rule, + Size: uint32(len(parsedDomain)), + }) + } + + if len(c.ExpectedIPs) == 0 { + c.ExpectedIPs = c.ExpectIPs + } + + actPrior := false + var newExpectedIPs StringList + for _, s := range c.ExpectedIPs { + if s == "*" { + actPrior = true + } else { + newExpectedIPs = append(newExpectedIPs, s) + } + } + + actUnprior := false + var newUnexpectedIPs StringList + for _, s := range c.UnexpectedIPs { + if s == "*" { + actUnprior = true + } else { + newUnexpectedIPs = append(newUnexpectedIPs, s) + } + } + + expectedGeoipList, err := ToCidrList(newExpectedIPs) + if err != nil { + return nil, errors.New("invalid expected IP rule: ", c.ExpectedIPs).Base(err) + } + + unexpectedGeoipList, err := ToCidrList(newUnexpectedIPs) + if err != nil { + return nil, errors.New("invalid unexpected IP rule: ", c.UnexpectedIPs).Base(err) + } + + var myClientIP []byte + if c.ClientIP != nil { + if !c.ClientIP.Family().IsIP() { + return nil, errors.New("not an IP address:", c.ClientIP.String()) + } + myClientIP = []byte(c.ClientIP.IP()) + } + + return &dns.NameServer{ + Address: &net.Endpoint{ + Network: net.Network_UDP, + Address: c.Address.Build(), + Port: uint32(c.Port), + }, + ClientIp: myClientIP, + SkipFallback: c.SkipFallback, + PrioritizedDomain: domains, + ExpectedGeoip: expectedGeoipList, + OriginalRules: originalRules, + QueryStrategy: resolveQueryStrategy(c.QueryStrategy), + ActPrior: actPrior, + Tag: c.Tag, + TimeoutMs: c.TimeoutMs, + DisableCache: c.DisableCache, + ServeStale: c.ServeStale, + ServeExpiredTTL: c.ServeExpiredTTL, + FinalQuery: c.FinalQuery, + UnexpectedGeoip: unexpectedGeoipList, + ActUnprior: actUnprior, + }, nil +} + +var typeMap = map[router.Domain_Type]dns.DomainMatchingType{ + router.Domain_Full: dns.DomainMatchingType_Full, + router.Domain_Domain: dns.DomainMatchingType_Subdomain, + router.Domain_Plain: dns.DomainMatchingType_Keyword, + router.Domain_Regex: dns.DomainMatchingType_Regex, +} + +// DNSConfig is a JSON serializable object for dns.Config. +type DNSConfig struct { + Servers []*NameServerConfig `json:"servers"` + Hosts *HostsWrapper `json:"hosts"` + ClientIP *Address `json:"clientIp"` + Tag string `json:"tag"` + QueryStrategy string `json:"queryStrategy"` + DisableCache bool `json:"disableCache"` + ServeStale bool `json:"serveStale"` + ServeExpiredTTL uint32 `json:"serveExpiredTTL"` + DisableFallback bool `json:"disableFallback"` + DisableFallbackIfMatch bool `json:"disableFallbackIfMatch"` + EnableParallelQuery bool `json:"enableParallelQuery"` + UseSystemHosts bool `json:"useSystemHosts"` +} + +type HostAddress struct { + addr *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 +func (h *HostAddress) UnmarshalJSON(data []byte) error { + addr := new(Address) + var addrs []*Address + switch { + case json.Unmarshal(data, &addr) == nil: + h.addr = addr + case json.Unmarshal(data, &addrs) == nil: + h.addrs = addrs + default: + return errors.New("invalid address") + } + return nil +} + +type HostsWrapper struct { + Hosts map[string]*HostAddress +} + +func getHostMapping(ha *HostAddress) *dns.Config_HostMapping { + if ha.addr != nil { + if ha.addr.Family().IsDomain() { + return &dns.Config_HostMapping{ + ProxiedDomain: ha.addr.Domain(), + } + } + return &dns.Config_HostMapping{ + Ip: [][]byte{ha.addr.IP()}, + } + } + + ips := make([][]byte, 0, len(ha.addrs)) + for _, addr := range ha.addrs { + if addr.Family().IsDomain() { + return &dns.Config_HostMapping{ + ProxiedDomain: addr.Domain(), + } + } + ips = append(ips, []byte(addr.IP())) + } + return &dns.Config_HostMapping{ + Ip: ips, + } +} + +// MarshalJSON implements encoding/json.Marshaler.MarshalJSON +func (m *HostsWrapper) MarshalJSON() ([]byte, error) { + return json.Marshal(m.Hosts) +} + +// UnmarshalJSON implements encoding/json.Unmarshaler.UnmarshalJSON +func (m *HostsWrapper) UnmarshalJSON(data []byte) error { + hosts := make(map[string]*HostAddress) + err := json.Unmarshal(data, &hosts) + if err == nil { + m.Hosts = hosts + return nil + } + return errors.New("invalid DNS hosts").Base(err) +} + +// Build implements Buildable +func (m *HostsWrapper) Build() ([]*dns.Config_HostMapping, error) { + mappings := make([]*dns.Config_HostMapping, 0, 20) + + domains := make([]string, 0, len(m.Hosts)) + for domain := range m.Hosts { + domains = append(domains, domain) + } + sort.Strings(domains) + + for _, domain := range domains { + switch { + case strings.HasPrefix(domain, "domain:"): + domainName := domain[7:] + if len(domainName) == 0 { + return nil, errors.New("empty domain type of rule: ", domain) + } + mapping := getHostMapping(m.Hosts[domain]) + mapping.Type = dns.DomainMatchingType_Subdomain + mapping.Domain = domainName + mappings = append(mappings, mapping) + + case strings.HasPrefix(domain, "geosite:"): + listName := domain[8:] + if len(listName) == 0 { + return nil, errors.New("empty geosite rule: ", domain) + } + geositeList, err := loadGeositeWithAttr("geosite.dat", listName) + if err != nil { + return nil, errors.New("failed to load geosite: ", listName).Base(err) + } + for _, d := range geositeList { + mapping := getHostMapping(m.Hosts[domain]) + mapping.Type = typeMap[d.Type] + mapping.Domain = d.Value + mappings = append(mappings, mapping) + } + + case strings.HasPrefix(domain, "regexp:"): + regexpVal := domain[7:] + if len(regexpVal) == 0 { + return nil, errors.New("empty regexp type of rule: ", domain) + } + mapping := getHostMapping(m.Hosts[domain]) + mapping.Type = dns.DomainMatchingType_Regex + mapping.Domain = regexpVal + mappings = append(mappings, mapping) + + case strings.HasPrefix(domain, "keyword:"): + keywordVal := domain[8:] + if len(keywordVal) == 0 { + return nil, errors.New("empty keyword type of rule: ", domain) + } + mapping := getHostMapping(m.Hosts[domain]) + mapping.Type = dns.DomainMatchingType_Keyword + mapping.Domain = keywordVal + mappings = append(mappings, mapping) + + case strings.HasPrefix(domain, "full:"): + fullVal := domain[5:] + if len(fullVal) == 0 { + return nil, errors.New("empty full domain type of rule: ", domain) + } + mapping := getHostMapping(m.Hosts[domain]) + mapping.Type = dns.DomainMatchingType_Full + mapping.Domain = fullVal + mappings = append(mappings, mapping) + + case strings.HasPrefix(domain, "dotless:"): + mapping := getHostMapping(m.Hosts[domain]) + mapping.Type = dns.DomainMatchingType_Regex + switch substr := domain[8:]; { + case substr == "": + mapping.Domain = "^[^.]*$" + case !strings.Contains(substr, "."): + mapping.Domain = "^[^.]*" + substr + "[^.]*$" + default: + return nil, errors.New("substr in dotless rule should not contain a dot: ", substr) + } + mappings = append(mappings, mapping) + + case strings.HasPrefix(domain, "ext:"): + kv := strings.Split(domain[4:], ":") + if len(kv) != 2 { + return nil, errors.New("invalid external resource: ", domain) + } + filename := kv[0] + list := kv[1] + geositeList, err := loadGeositeWithAttr(filename, list) + if err != nil { + return nil, errors.New("failed to load domain list: ", list, " from ", filename).Base(err) + } + for _, d := range geositeList { + mapping := getHostMapping(m.Hosts[domain]) + mapping.Type = typeMap[d.Type] + mapping.Domain = d.Value + mappings = append(mappings, mapping) + } + + default: + mapping := getHostMapping(m.Hosts[domain]) + mapping.Type = dns.DomainMatchingType_Full + mapping.Domain = domain + mappings = append(mappings, mapping) + } + } + return mappings, nil +} + +// Build implements Buildable +func (c *DNSConfig) Build() (*dns.Config, error) { + config := &dns.Config{ + Tag: c.Tag, + DisableCache: c.DisableCache, + ServeStale: c.ServeStale, + ServeExpiredTTL: c.ServeExpiredTTL, + DisableFallback: c.DisableFallback, + DisableFallbackIfMatch: c.DisableFallbackIfMatch, + EnableParallelQuery: c.EnableParallelQuery, + QueryStrategy: resolveQueryStrategy(c.QueryStrategy), + } + + if c.ClientIP != nil { + if !c.ClientIP.Family().IsIP() { + return nil, errors.New("not an IP address:", c.ClientIP.String()) + } + config.ClientIp = []byte(c.ClientIP.IP()) + } + + // Build PolicyID + policyMap := map[string]uint32{} + nextPolicyID := uint32(1) + buildPolicyID := func(nsc *NameServerConfig) uint32 { + var sb strings.Builder + + // ClientIP + if nsc.ClientIP != nil { + sb.WriteString("client=") + sb.WriteString(nsc.ClientIP.String()) + sb.WriteByte('|') + } else { + sb.WriteString("client=none|") + } + + // SkipFallback + if nsc.SkipFallback { + sb.WriteString("skip=1|") + } else { + sb.WriteString("skip=0|") + } + + // QueryStrategy + sb.WriteString("qs=") + sb.WriteString(strings.ToLower(strings.TrimSpace(nsc.QueryStrategy))) + sb.WriteByte('|') + + // Tag + sb.WriteString("tag=") + sb.WriteString(strings.ToLower(strings.TrimSpace(nsc.Tag))) + sb.WriteByte('|') + + // []string helper + writeList := func(tag string, lst []string) { + if len(lst) == 0 { + sb.WriteString(tag) + sb.WriteString("=[]|") + return + } + cp := make([]string, len(lst)) + for i, s := range lst { + cp[i] = strings.TrimSpace(strings.ToLower(s)) + } + sort.Strings(cp) + sb.WriteString(tag) + sb.WriteByte('=') + sb.WriteString(strings.Join(cp, ",")) + sb.WriteByte('|') + } + + writeList("domains", nsc.Domains) + writeList("expected", nsc.ExpectedIPs) + writeList("expect", nsc.ExpectIPs) + writeList("unexpected", nsc.UnexpectedIPs) + + key := sb.String() + + if id, ok := policyMap[key]; ok { + return id + } + id := nextPolicyID + nextPolicyID++ + policyMap[key] = id + return id + } + + for _, server := range c.Servers { + ns, err := server.Build() + if err != nil { + return nil, errors.New("failed to build nameserver").Base(err) + } + ns.PolicyID = buildPolicyID(server) + config.NameServer = append(config.NameServer, ns) + } + + if c.Hosts != nil { + staticHosts, err := c.Hosts.Build() + if err != nil { + return nil, errors.New("failed to build hosts").Base(err) + } + config.StaticHosts = append(config.StaticHosts, staticHosts...) + } + if c.UseSystemHosts { + systemHosts, err := readSystemHosts() + if err != nil { + return nil, errors.New("failed to read system hosts").Base(err) + } + for domain, ips := range systemHosts { + config.StaticHosts = append(config.StaticHosts, &dns.Config_HostMapping{Ip: ips, Domain: domain, Type: dns.DomainMatchingType_Full}) + } + } + + return config, nil +} + +func resolveQueryStrategy(queryStrategy string) dns.QueryStrategy { + switch strings.ToLower(queryStrategy) { + case "useip", "use_ip", "use-ip": + return dns.QueryStrategy_USE_IP + case "useip4", "useipv4", "use_ip4", "use_ipv4", "use_ip_v4", "use-ip4", "use-ipv4", "use-ip-v4": + return dns.QueryStrategy_USE_IP4 + case "useip6", "useipv6", "use_ip6", "use_ipv6", "use_ip_v6", "use-ip6", "use-ipv6", "use-ip-v6": + return dns.QueryStrategy_USE_IP6 + case "usesys", "usesystem", "use_sys", "use_system", "use-sys", "use-system": + return dns.QueryStrategy_USE_SYS + default: + return dns.QueryStrategy_USE_IP + } +} + +func readSystemHosts() (map[string][][]byte, error) { + var hostsPath string + switch runtime.GOOS { + case "windows": + hostsPath = filepath.Join(os.Getenv("SystemRoot"), "System32", "drivers", "etc", "hosts") + default: + hostsPath = "/etc/hosts" + } + + file, err := os.Open(hostsPath) + if err != nil { + return nil, err + } + defer file.Close() + + hostsMap := make(map[string][][]byte) + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if i := strings.IndexByte(line, '#'); i >= 0 { + // Discard comments. + line = line[0:i] + } + f := strings.Fields(line) + if len(f) < 2 { + continue + } + addr := net.ParseAddress(f[0]) + if addr.Family().IsDomain() { + continue + } + ip := addr.IP() + for i := 1; i < len(f); i++ { + domain := strings.TrimSuffix(f[i], ".") + domain = strings.ToLower(domain) + if v, ok := hostsMap[domain]; ok { + hostsMap[domain] = append(v, ip) + } else { + hostsMap[domain] = [][]byte{ip} + } + } + } + if err := scanner.Err(); err != nil { + return nil, err + } + return hostsMap, nil +} diff --git a/subproject/Xray-core-main/infra/conf/dns_proxy.go b/subproject/Xray-core-main/infra/conf/dns_proxy.go new file mode 100644 index 00000000..b223e502 --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/dns_proxy.go @@ -0,0 +1,38 @@ +package conf + +import ( + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/proxy/dns" + "google.golang.org/protobuf/proto" +) + +type DNSOutboundConfig struct { + Network Network `json:"network"` + Address *Address `json:"address"` + Port uint16 `json:"port"` + UserLevel uint32 `json:"userLevel"` + NonIPQuery string `json:"nonIPQuery"` + BlockTypes []int32 `json:"blockTypes"` +} + +func (c *DNSOutboundConfig) Build() (proto.Message, error) { + config := &dns.Config{ + Server: &net.Endpoint{ + Network: c.Network.Build(), + Port: uint32(c.Port), + }, + UserLevel: c.UserLevel, + } + if c.Address != nil { + config.Server.Address = c.Address.Build() + } + switch c.NonIPQuery { + case "", "reject", "drop", "skip": + default: + return nil, errors.New(`unknown "nonIPQuery": `, c.NonIPQuery) + } + config.Non_IPQuery = c.NonIPQuery + config.BlockTypes = c.BlockTypes + return config, nil +} diff --git a/subproject/Xray-core-main/infra/conf/dns_proxy_test.go b/subproject/Xray-core-main/infra/conf/dns_proxy_test.go new file mode 100644 index 00000000..805ac323 --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/dns_proxy_test.go @@ -0,0 +1,33 @@ +package conf_test + +import ( + "testing" + + "github.com/xtls/xray-core/common/net" + . "github.com/xtls/xray-core/infra/conf" + "github.com/xtls/xray-core/proxy/dns" +) + +func TestDnsProxyConfig(t *testing.T) { + creator := func() Buildable { + return new(DNSOutboundConfig) + } + + runMultiTestCase(t, []TestCase{ + { + Input: `{ + "address": "8.8.8.8", + "port": 53, + "network": "tcp" + }`, + Parser: loadJSON(creator), + Output: &dns.Config{ + Server: &net.Endpoint{ + Network: net.Network_TCP, + Address: net.NewIPOrDomain(net.IPAddress([]byte{8, 8, 8, 8})), + Port: 53, + }, + }, + }, + }) +} diff --git a/subproject/Xray-core-main/infra/conf/dns_test.go b/subproject/Xray-core-main/infra/conf/dns_test.go new file mode 100644 index 00000000..a9739668 --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/dns_test.go @@ -0,0 +1,117 @@ +package conf_test + +import ( + "encoding/json" + "testing" + + "github.com/xtls/xray-core/app/dns" + "github.com/xtls/xray-core/common/net" + . "github.com/xtls/xray-core/infra/conf" + "google.golang.org/protobuf/proto" +) + +func TestDNSConfigParsing(t *testing.T) { + parserCreator := func() func(string) (proto.Message, error) { + return func(s string) (proto.Message, error) { + config := new(DNSConfig) + if err := json.Unmarshal([]byte(s), config); err != nil { + return nil, err + } + return config.Build() + } + } + expectedServeStale := true + expectedServeExpiredTTL := uint32(172800) + runMultiTestCase(t, []TestCase{ + { + Input: `{ + "servers": [{ + "address": "8.8.8.8", + "port": 5353, + "skipFallback": true, + "domains": ["domain:example.com"], + "serveStale": true, + "serveExpiredTTL": 172800 + }], + "hosts": { + "domain:example.com": "google.com", + "example.com": "127.0.0.1", + "keyword:google": ["8.8.8.8", "8.8.4.4"], + "regexp:.*\\.com": "8.8.4.4", + "www.example.org": ["127.0.0.1", "127.0.0.2"] + }, + "clientIp": "10.0.0.1", + "queryStrategy": "UseIPv4", + "disableCache": true, + "serveStale": false, + "serveExpiredTTL": 86400, + "disableFallback": true + }`, + Parser: parserCreator(), + Output: &dns.Config{ + NameServer: []*dns.NameServer{ + { + Address: &net.Endpoint{ + Address: &net.IPOrDomain{ + Address: &net.IPOrDomain_Ip{ + Ip: []byte{8, 8, 8, 8}, + }, + }, + Network: net.Network_UDP, + Port: 5353, + }, + SkipFallback: true, + PrioritizedDomain: []*dns.NameServer_PriorityDomain{ + { + Type: dns.DomainMatchingType_Subdomain, + Domain: "example.com", + }, + }, + OriginalRules: []*dns.NameServer_OriginalRule{ + { + Rule: "domain:example.com", + Size: 1, + }, + }, + ServeStale: &expectedServeStale, + ServeExpiredTTL: &expectedServeExpiredTTL, + PolicyID: 1, // Servers with certain identical fields share this ID, incrementing starting from 1. See: Build PolicyID + }, + }, + StaticHosts: []*dns.Config_HostMapping{ + { + Type: dns.DomainMatchingType_Subdomain, + Domain: "example.com", + ProxiedDomain: "google.com", + }, + { + Type: dns.DomainMatchingType_Full, + Domain: "example.com", + Ip: [][]byte{{127, 0, 0, 1}}, + }, + { + Type: dns.DomainMatchingType_Keyword, + Domain: "google", + Ip: [][]byte{{8, 8, 8, 8}, {8, 8, 4, 4}}, + }, + { + Type: dns.DomainMatchingType_Regex, + Domain: ".*\\.com", + Ip: [][]byte{{8, 8, 4, 4}}, + }, + { + Type: dns.DomainMatchingType_Full, + Domain: "www.example.org", + Ip: [][]byte{{127, 0, 0, 1}, {127, 0, 0, 2}}, + }, + }, + ClientIp: []byte{10, 0, 0, 1}, + QueryStrategy: dns.QueryStrategy_USE_IP4, + DisableCache: true, + ServeStale: false, + ServeExpiredTTL: 86400, + DisableFallback: true, + }, + }, + }) +} diff --git a/subproject/Xray-core-main/infra/conf/dokodemo.go b/subproject/Xray-core-main/infra/conf/dokodemo.go new file mode 100644 index 00000000..781d5222 --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/dokodemo.go @@ -0,0 +1,35 @@ +package conf + +import ( + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/proxy/dokodemo" + "google.golang.org/protobuf/proto" +) + +type DokodemoConfig struct { + Address *Address `json:"address"` + Port uint16 `json:"port"` + PortMap map[string]string `json:"portMap"` + Network *NetworkList `json:"network"` + FollowRedirect bool `json:"followRedirect"` + UserLevel uint32 `json:"userLevel"` +} + +func (v *DokodemoConfig) Build() (proto.Message, error) { + config := new(dokodemo.Config) + if v.Address != nil { + config.Address = v.Address.Build() + } + config.Port = uint32(v.Port) + config.PortMap = v.PortMap + for _, v := range config.PortMap { + if _, _, err := net.SplitHostPort(v); err != nil { + return nil, errors.New("invalid portMap: ", v).Base(err) + } + } + config.Networks = v.Network.Build() + config.FollowRedirect = v.FollowRedirect + config.UserLevel = v.UserLevel + return config, nil +} diff --git a/subproject/Xray-core-main/infra/conf/dokodemo_test.go b/subproject/Xray-core-main/infra/conf/dokodemo_test.go new file mode 100644 index 00000000..264cc8d7 --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/dokodemo_test.go @@ -0,0 +1,39 @@ +package conf_test + +import ( + "testing" + + "github.com/xtls/xray-core/common/net" + . "github.com/xtls/xray-core/infra/conf" + "github.com/xtls/xray-core/proxy/dokodemo" +) + +func TestDokodemoConfig(t *testing.T) { + creator := func() Buildable { + return new(DokodemoConfig) + } + + runMultiTestCase(t, []TestCase{ + { + Input: `{ + "address": "8.8.8.8", + "port": 53, + "network": "tcp", + "followRedirect": true, + "userLevel": 1 + }`, + Parser: loadJSON(creator), + Output: &dokodemo.Config{ + Address: &net.IPOrDomain{ + Address: &net.IPOrDomain_Ip{ + Ip: []byte{8, 8, 8, 8}, + }, + }, + Port: 53, + Networks: []net.Network{net.Network_TCP}, + FollowRedirect: true, + UserLevel: 1, + }, + }, + }) +} diff --git a/subproject/Xray-core-main/infra/conf/fakedns.go b/subproject/Xray-core-main/infra/conf/fakedns.go new file mode 100644 index 00000000..3aa20115 --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/fakedns.go @@ -0,0 +1,144 @@ +package conf + +import ( + "context" + "encoding/json" + "strings" + + "github.com/xtls/xray-core/app/dns/fakedns" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/features/dns" +) + +type FakeDNSPoolElementConfig struct { + IPPool string `json:"ipPool"` + LRUSize int64 `json:"poolSize"` +} + +type FakeDNSConfig struct { + pool *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 +func (f *FakeDNSConfig) UnmarshalJSON(data []byte) error { + var pool FakeDNSPoolElementConfig + var pools []*FakeDNSPoolElementConfig + switch { + case json.Unmarshal(data, &pool) == nil: + f.pool = &pool + case json.Unmarshal(data, &pools) == nil: + f.pools = pools + default: + return errors.New("invalid fakedns config") + } + return nil +} + +func (f *FakeDNSConfig) Build() (*fakedns.FakeDnsPoolMulti, error) { + fakeDNSPool := fakedns.FakeDnsPoolMulti{} + + if f.pool != nil { + fakeDNSPool.Pools = append(fakeDNSPool.Pools, &fakedns.FakeDnsPool{ + IpPool: f.pool.IPPool, + LruSize: f.pool.LRUSize, + }) + return &fakeDNSPool, nil + } + + if f.pools != nil { + for _, v := range f.pools { + fakeDNSPool.Pools = append(fakeDNSPool.Pools, &fakedns.FakeDnsPool{IpPool: v.IPPool, LruSize: v.LRUSize}) + } + return &fakeDNSPool, nil + } + + return nil, errors.New("no valid FakeDNS config") +} + +type FakeDNSPostProcessingStage struct{} + +func (FakeDNSPostProcessingStage) Process(config *Config) error { + fakeDNSInUse := false + isIPv4Enable, isIPv6Enable := true, true + + if config.DNSConfig != nil { + for _, v := range config.DNSConfig.Servers { + if v.Address.Family().IsDomain() && strings.EqualFold(v.Address.Domain(), "fakedns") { + fakeDNSInUse = true + } + } + + switch strings.ToLower(config.DNSConfig.QueryStrategy) { + case "useip4", "useipv4", "use_ip4", "use_ipv4", "use_ip_v4", "use-ip4", "use-ipv4", "use-ip-v4": + isIPv4Enable, isIPv6Enable = true, false + case "useip6", "useipv6", "use_ip6", "use_ipv6", "use_ip_v6", "use-ip6", "use-ipv6", "use-ip-v6": + isIPv4Enable, isIPv6Enable = false, true + } + } + + if fakeDNSInUse { + // Add a Fake DNS Config if there is none + if config.FakeDNS == nil { + config.FakeDNS = &FakeDNSConfig{} + switch { + case isIPv4Enable && isIPv6Enable: + config.FakeDNS.pools = []*FakeDNSPoolElementConfig{ + { + IPPool: dns.FakeIPv4Pool, + LRUSize: 32768, + }, + { + IPPool: dns.FakeIPv6Pool, + LRUSize: 32768, + }, + } + case !isIPv4Enable && isIPv6Enable: + config.FakeDNS.pool = &FakeDNSPoolElementConfig{ + IPPool: dns.FakeIPv6Pool, + LRUSize: 65535, + } + case isIPv4Enable && !isIPv6Enable: + config.FakeDNS.pool = &FakeDNSPoolElementConfig{ + IPPool: dns.FakeIPv4Pool, + LRUSize: 65535, + } + } + } + + found := false + // Check if there is a Outbound with necessary sniffer on + var inbounds []InboundDetourConfig + + if len(config.InboundConfigs) > 0 { + inbounds = append(inbounds, config.InboundConfigs...) + } + for _, v := range inbounds { + if v.SniffingConfig != nil && v.SniffingConfig.Enabled && v.SniffingConfig.DestOverride != nil { + for _, dov := range *v.SniffingConfig.DestOverride { + if strings.EqualFold(dov, "fakedns") || strings.EqualFold(dov, "fakedns+others") { + found = true + break + } + } + } + } + if !found { + errors.LogWarning(context.Background(), "Defined FakeDNS but haven't enabled FakeDNS destOverride at any inbound.") + } + } + + return nil +} diff --git a/subproject/Xray-core-main/infra/conf/freedom.go b/subproject/Xray-core-main/infra/conf/freedom.go new file mode 100644 index 00000000..82d2c9a8 --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/freedom.go @@ -0,0 +1,222 @@ +package conf + +import ( + "encoding/base64" + "encoding/hex" + "net" + "strings" + + "github.com/xtls/xray-core/common/errors" + v2net "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/proxy/freedom" + "github.com/xtls/xray-core/transport/internet" + "google.golang.org/protobuf/proto" +) + +type FreedomConfig struct { + TargetStrategy string `json:"targetStrategy"` + DomainStrategy string `json:"domainStrategy"` + Redirect string `json:"redirect"` + UserLevel uint32 `json:"userLevel"` + Fragment *Fragment `json:"fragment"` + Noise *Noise `json:"noise"` + Noises []*Noise `json:"noises"` + ProxyProtocol uint32 `json:"proxyProtocol"` +} + +type Fragment struct { + Packets string `json:"packets"` + Length *Int32Range `json:"length"` + Interval *Int32Range `json:"interval"` + MaxSplit *Int32Range `json:"maxSplit"` +} + +type Noise struct { + Type string `json:"type"` + Packet string `json:"packet"` + Delay *Int32Range `json:"delay"` + ApplyTo string `json:"applyTo"` +} + +// Build implements Buildable +func (c *FreedomConfig) Build() (proto.Message, error) { + config := new(freedom.Config) + targetStrategy := c.TargetStrategy + if targetStrategy == "" { + targetStrategy = c.DomainStrategy + } + switch strings.ToLower(targetStrategy) { + case "asis", "": + config.DomainStrategy = internet.DomainStrategy_AS_IS + case "useip": + config.DomainStrategy = internet.DomainStrategy_USE_IP + case "useipv4": + config.DomainStrategy = internet.DomainStrategy_USE_IP4 + case "useipv6": + config.DomainStrategy = internet.DomainStrategy_USE_IP6 + case "useipv4v6": + config.DomainStrategy = internet.DomainStrategy_USE_IP46 + case "useipv6v4": + config.DomainStrategy = internet.DomainStrategy_USE_IP64 + case "forceip": + config.DomainStrategy = internet.DomainStrategy_FORCE_IP + case "forceipv4": + config.DomainStrategy = internet.DomainStrategy_FORCE_IP4 + case "forceipv6": + config.DomainStrategy = internet.DomainStrategy_FORCE_IP6 + case "forceipv4v6": + config.DomainStrategy = internet.DomainStrategy_FORCE_IP46 + case "forceipv6v4": + config.DomainStrategy = internet.DomainStrategy_FORCE_IP64 + default: + return nil, errors.New("unsupported domain strategy: ", targetStrategy) + } + + if c.Fragment != nil { + config.Fragment = new(freedom.Fragment) + + switch strings.ToLower(c.Fragment.Packets) { + case "tlshello": + // TLS Hello Fragmentation (into multiple handshake messages) + config.Fragment.PacketsFrom = 0 + config.Fragment.PacketsTo = 1 + case "": + // TCP Segmentation (all packets) + config.Fragment.PacketsFrom = 0 + config.Fragment.PacketsTo = 0 + default: + // TCP Segmentation (range) + from, to, err := ParseRangeString(c.Fragment.Packets) + if err != nil { + return nil, errors.New("Invalid PacketsFrom").Base(err) + } + config.Fragment.PacketsFrom = uint64(from) + config.Fragment.PacketsTo = uint64(to) + if config.Fragment.PacketsFrom == 0 { + return nil, errors.New("PacketsFrom can't be 0") + } + } + + { + if c.Fragment.Length == nil { + return nil, errors.New("Length can't be empty") + } + config.Fragment.LengthMin = uint64(c.Fragment.Length.From) + config.Fragment.LengthMax = uint64(c.Fragment.Length.To) + if config.Fragment.LengthMin == 0 { + return nil, errors.New("LengthMin can't be 0") + } + } + + { + if c.Fragment.Interval == nil { + return nil, errors.New("Interval can't be empty") + } + config.Fragment.IntervalMin = uint64(c.Fragment.Interval.From) + config.Fragment.IntervalMax = uint64(c.Fragment.Interval.To) + } + + { + if c.Fragment.MaxSplit != nil { + config.Fragment.MaxSplitMin = uint64(c.Fragment.MaxSplit.From) + config.Fragment.MaxSplitMax = uint64(c.Fragment.MaxSplit.To) + } + } + } + + if c.Noise != nil { + return nil, errors.PrintRemovedFeatureError("noise = { ... }", "noises = [ { ... } ]") + } + + if c.Noises != nil { + for _, n := range c.Noises { + NConfig, err := ParseNoise(n) + if err != nil { + return nil, err + } + config.Noises = append(config.Noises, NConfig) + } + } + + config.UserLevel = c.UserLevel + if len(c.Redirect) > 0 { + host, portStr, err := net.SplitHostPort(c.Redirect) + if err != nil { + return nil, errors.New("invalid redirect address: ", c.Redirect, ": ", err).Base(err) + } + port, err := v2net.PortFromString(portStr) + if err != nil { + return nil, errors.New("invalid redirect port: ", c.Redirect, ": ", err).Base(err) + } + config.DestinationOverride = &freedom.DestinationOverride{ + Server: &protocol.ServerEndpoint{ + Port: uint32(port), + }, + } + + if len(host) > 0 { + config.DestinationOverride.Server.Address = v2net.NewIPOrDomain(v2net.ParseAddress(host)) + } + } + if c.ProxyProtocol > 0 && c.ProxyProtocol <= 2 { + config.ProxyProtocol = c.ProxyProtocol + } + return config, nil +} + +func ParseNoise(noise *Noise) (*freedom.Noise, error) { + var err error + NConfig := new(freedom.Noise) + noise.Packet = strings.TrimSpace(noise.Packet) + + switch noise.Type { + case "rand": + min, max, err := ParseRangeString(noise.Packet) + if err != nil { + return nil, errors.New("invalid value for rand Length").Base(err) + } + NConfig.LengthMin = uint64(min) + NConfig.LengthMax = uint64(max) + if NConfig.LengthMin == 0 { + return nil, errors.New("rand lengthMin or lengthMax cannot be 0") + } + + case "str": + // user input string + NConfig.Packet = []byte(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": + // user input base64 + NConfig.Packet, err = base64.RawURLEncoding.DecodeString(strings.NewReplacer("+", "-", "/", "_", "=", "").Replace(noise.Packet)) + if err != nil { + return nil, errors.New("Invalid base64 string").Base(err) + } + + default: + return nil, errors.New("Invalid packet, only rand/str/hex/base64 are supported") + } + + if noise.Delay != nil { + NConfig.DelayMin = uint64(noise.Delay.From) + NConfig.DelayMax = uint64(noise.Delay.To) + } + switch strings.ToLower(noise.ApplyTo) { + case "", "ip", "all": + NConfig.ApplyTo = "ip" + case "ipv4": + NConfig.ApplyTo = "ipv4" + case "ipv6": + NConfig.ApplyTo = "ipv6" + default: + return nil, errors.New("Invalid applyTo, only ip/ipv4/ipv6 are supported") + } + return NConfig, nil +} diff --git a/subproject/Xray-core-main/infra/conf/freedom_test.go b/subproject/Xray-core-main/infra/conf/freedom_test.go new file mode 100644 index 00000000..55a83243 --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/freedom_test.go @@ -0,0 +1,42 @@ +package conf_test + +import ( + "testing" + + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + . "github.com/xtls/xray-core/infra/conf" + "github.com/xtls/xray-core/proxy/freedom" + "github.com/xtls/xray-core/transport/internet" +) + +func TestFreedomConfig(t *testing.T) { + creator := func() Buildable { + return new(FreedomConfig) + } + + runMultiTestCase(t, []TestCase{ + { + Input: `{ + "domainStrategy": "AsIs", + "redirect": "127.0.0.1:3366", + "userLevel": 1 + }`, + Parser: loadJSON(creator), + Output: &freedom.Config{ + DomainStrategy: internet.DomainStrategy_AS_IS, + DestinationOverride: &freedom.DestinationOverride{ + Server: &protocol.ServerEndpoint{ + Address: &net.IPOrDomain{ + Address: &net.IPOrDomain_Ip{ + Ip: []byte{127, 0, 0, 1}, + }, + }, + Port: 3366, + }, + }, + UserLevel: 1, + }, + }, + }) +} diff --git a/subproject/Xray-core-main/infra/conf/general_test.go b/subproject/Xray-core-main/infra/conf/general_test.go new file mode 100644 index 00000000..4d23b3b5 --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/general_test.go @@ -0,0 +1,36 @@ +package conf_test + +import ( + "encoding/json" + "testing" + + "github.com/xtls/xray-core/common" + . "github.com/xtls/xray-core/infra/conf" + "google.golang.org/protobuf/proto" +) + +func loadJSON(creator func() Buildable) func(string) (proto.Message, error) { + return func(s string) (proto.Message, error) { + instance := creator() + if err := json.Unmarshal([]byte(s), instance); err != nil { + return nil, err + } + return instance.Build() + } +} + +type TestCase struct { + Input string + Parser func(string) (proto.Message, error) + Output proto.Message +} + +func runMultiTestCase(t *testing.T, testCases []TestCase) { + for _, testCase := range testCases { + actual, err := testCase.Parser(testCase.Input) + common.Must(err) + if !proto.Equal(actual, testCase.Output) { + t.Fatalf("Failed in test case:\n%s\nActual:\n%v\nExpected:\n%v", testCase.Input, actual, testCase.Output) + } + } +} diff --git a/subproject/Xray-core-main/infra/conf/grpc.go b/subproject/Xray-core-main/infra/conf/grpc.go new file mode 100644 index 00000000..429186b0 --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/grpc.go @@ -0,0 +1,41 @@ +package conf + +import ( + "github.com/xtls/xray-core/transport/internet/grpc" + "google.golang.org/protobuf/proto" +) + +type GRPCConfig struct { + Authority string `json:"authority"` + ServiceName string `json:"serviceName"` + MultiMode bool `json:"multiMode"` + IdleTimeout int32 `json:"idle_timeout"` + HealthCheckTimeout int32 `json:"health_check_timeout"` + PermitWithoutStream bool `json:"permit_without_stream"` + InitialWindowsSize int32 `json:"initial_windows_size"` + UserAgent string `json:"user_agent"` +} + +func (g *GRPCConfig) Build() (proto.Message, error) { + if g.IdleTimeout <= 0 { + g.IdleTimeout = 0 + } + if g.HealthCheckTimeout <= 0 { + g.HealthCheckTimeout = 0 + } + if g.InitialWindowsSize < 0 { + // default window size of gRPC-go + g.InitialWindowsSize = 0 + } + + return &grpc.Config{ + Authority: g.Authority, + ServiceName: g.ServiceName, + MultiMode: g.MultiMode, + IdleTimeout: g.IdleTimeout, + HealthCheckTimeout: g.HealthCheckTimeout, + PermitWithoutStream: g.PermitWithoutStream, + InitialWindowsSize: g.InitialWindowsSize, + UserAgent: g.UserAgent, + }, nil +} diff --git a/subproject/Xray-core-main/infra/conf/http.go b/subproject/Xray-core-main/infra/conf/http.go new file mode 100644 index 00000000..7da8f3f5 --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/http.go @@ -0,0 +1,122 @@ +package conf + +import ( + "encoding/json" + + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/serial" + "github.com/xtls/xray-core/proxy/http" + "google.golang.org/protobuf/proto" +) + +type HTTPAccount struct { + Username string `json:"user"` + Password string `json:"pass"` +} + +func (v *HTTPAccount) Build() *http.Account { + return &http.Account{ + Username: v.Username, + Password: v.Password, + } +} + +type HTTPServerConfig struct { + Accounts []*HTTPAccount `json:"accounts"` + Transparent bool `json:"allowTransparent"` + UserLevel uint32 `json:"userLevel"` +} + +func (c *HTTPServerConfig) Build() (proto.Message, error) { + config := &http.ServerConfig{ + AllowTransparent: c.Transparent, + UserLevel: c.UserLevel, + } + + if len(c.Accounts) > 0 { + config.Accounts = make(map[string]string) + for _, account := range c.Accounts { + config.Accounts[account.Username] = account.Password + } + } + + return config, nil +} + +type HTTPRemoteConfig struct { + Address *Address `json:"address"` + Port uint16 `json:"port"` + Users []json.RawMessage `json:"users"` +} + +type HTTPClientConfig struct { + Address *Address `json:"address"` + Port uint16 `json:"port"` + Level uint32 `json:"level"` + Email string `json:"email"` + Username string `json:"user"` + Password string `json:"pass"` + Servers []*HTTPRemoteConfig `json:"servers"` + Headers map[string]string `json:"headers"` +} + +func (v *HTTPClientConfig) Build() (proto.Message, error) { + config := new(http.ClientConfig) + if v.Address != nil { + v.Servers = []*HTTPRemoteConfig{ + { + Address: v.Address, + Port: v.Port, + }, + } + if len(v.Username) > 0 { + v.Servers[0].Users = []json.RawMessage{{}} + } + } + if len(v.Servers) != 1 { + return nil, errors.New(`HTTP settings: "servers" should have one and only one member. Multiple endpoints in "servers" should use multiple HTTP outbounds and routing balancer instead`) + } + for _, serverConfig := range v.Servers { + if len(serverConfig.Users) > 1 { + return nil, errors.New(`HTTP servers: "users" should have one member at most. Multiple members in "users" should use multiple HTTP outbounds and routing balancer instead`) + } + server := &protocol.ServerEndpoint{ + Address: serverConfig.Address.Build(), + Port: uint32(serverConfig.Port), + } + for _, rawUser := range serverConfig.Users { + user := new(protocol.User) + if v.Address != nil { + user.Level = v.Level + user.Email = v.Email + } else { + if err := json.Unmarshal(rawUser, user); err != nil { + return nil, errors.New("failed to parse HTTP user").Base(err).AtError() + } + } + account := new(HTTPAccount) + if v.Address != nil { + account.Username = v.Username + account.Password = v.Password + } else { + if err := json.Unmarshal(rawUser, account); err != nil { + return nil, errors.New("failed to parse HTTP account").Base(err).AtError() + } + } + user.Account = serial.ToTypedMessage(account.Build()) + server.User = user + break + } + config.Server = server + break + } + config.Header = make([]*http.Header, 0, 32) + for key, value := range v.Headers { + config.Header = append(config.Header, &http.Header{ + Key: key, + Value: value, + }) + } + return config, nil +} diff --git a/subproject/Xray-core-main/infra/conf/http_test.go b/subproject/Xray-core-main/infra/conf/http_test.go new file mode 100644 index 00000000..66cca396 --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/http_test.go @@ -0,0 +1,37 @@ +package conf_test + +import ( + "testing" + + . "github.com/xtls/xray-core/infra/conf" + "github.com/xtls/xray-core/proxy/http" +) + +func TestHTTPServerConfig(t *testing.T) { + creator := func() Buildable { + return new(HTTPServerConfig) + } + + runMultiTestCase(t, []TestCase{ + { + Input: `{ + "accounts": [ + { + "user": "my-username", + "pass": "my-password" + } + ], + "allowTransparent": true, + "userLevel": 1 + }`, + Parser: loadJSON(creator), + Output: &http.ServerConfig{ + Accounts: map[string]string{ + "my-username": "my-password", + }, + AllowTransparent: true, + UserLevel: 1, + }, + }, + }) +} diff --git a/subproject/Xray-core-main/infra/conf/hysteria.go b/subproject/Xray-core-main/infra/conf/hysteria.go new file mode 100644 index 00000000..3574811c --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/hysteria.go @@ -0,0 +1,61 @@ +package conf + +import ( + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/serial" + "github.com/xtls/xray-core/proxy/hysteria" + "github.com/xtls/xray-core/proxy/hysteria/account" + "google.golang.org/protobuf/proto" +) + +type HysteriaClientConfig struct { + Version int32 `json:"version"` + Address *Address `json:"address"` + Port uint16 `json:"port"` +} + +func (c *HysteriaClientConfig) Build() (proto.Message, error) { + if c.Version != 2 { + return nil, errors.New("version != 2") + } + + config := &hysteria.ClientConfig{} + config.Version = c.Version + config.Server = &protocol.ServerEndpoint{ + Address: c.Address.Build(), + Port: uint32(c.Port), + } + + return config, nil +} + +type HysteriaUserConfig struct { + Auth string `json:"auth"` + Level uint32 `json:"level"` + Email string `json:"email"` +} + +type HysteriaServerConfig struct { + Version int32 `json:"version"` + Users []*HysteriaUserConfig `json:"clients"` +} + +func (c *HysteriaServerConfig) Build() (proto.Message, error) { + config := new(hysteria.ServerConfig) + + if c.Users != nil { + for _, user := range c.Users { + account := &account.Account{ + Auth: user.Auth, + } + config.Users = append(config.Users, &protocol.User{ + Email: user.Email, + Level: user.Level, + Account: serial.ToTypedMessage(account), + }) + } + } + + return config, nil +} diff --git a/subproject/Xray-core-main/infra/conf/init.go b/subproject/Xray-core-main/infra/conf/init.go new file mode 100644 index 00000000..519d9fb2 --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/init.go @@ -0,0 +1,5 @@ +package conf + +func init() { + RegisterConfigureFilePostProcessingStage("FakeDNS", &FakeDNSPostProcessingStage{}) +} diff --git a/subproject/Xray-core-main/infra/conf/json/reader.go b/subproject/Xray-core-main/infra/conf/json/reader.go new file mode 100644 index 00000000..5a675557 --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/json/reader.go @@ -0,0 +1,133 @@ +package json + +import ( + "io" + + "github.com/xtls/xray-core/common/buf" +) + +// State is the internal state of parser. +type State byte + +const ( + StateContent State = iota + StateEscape + StateDoubleQuote + StateDoubleQuoteEscape + StateSingleQuote + StateSingleQuoteEscape + StateComment + StateSlash + StateMultilineComment + StateMultilineCommentStar +) + +// Reader is a reader for filtering comments. +// It supports Java style single and multi line comment syntax, and Python style single line comment syntax. +type Reader struct { + io.Reader + + state State + br *buf.BufferedReader +} + +// Read implements io.Reader.Read(). Buffer must be at least 3 bytes. +func (v *Reader) Read(b []byte) (int, error) { + if v.br == nil { + v.br = &buf.BufferedReader{Reader: buf.NewReader(v.Reader)} + } + + p := b[:0] + for len(p) < len(b)-2 { + x, err := v.br.ReadByte() + if err != nil { + if len(p) == 0 { + return 0, err + } + return len(p), nil + } + switch v.state { + case StateContent: + switch x { + case '"': + v.state = StateDoubleQuote + p = append(p, x) + case '\'': + v.state = StateSingleQuote + p = append(p, x) + case '\\': + v.state = StateEscape + case '#': + v.state = StateComment + case '/': + v.state = StateSlash + default: + p = append(p, x) + } + case StateEscape: + p = append(p, '\\', x) + v.state = StateContent + case StateDoubleQuote: + switch x { + case '"': + v.state = StateContent + p = append(p, x) + case '\\': + v.state = StateDoubleQuoteEscape + default: + p = append(p, x) + } + case StateDoubleQuoteEscape: + p = append(p, '\\', x) + v.state = StateDoubleQuote + case StateSingleQuote: + switch x { + case '\'': + v.state = StateContent + p = append(p, x) + case '\\': + v.state = StateSingleQuoteEscape + default: + p = append(p, x) + } + case StateSingleQuoteEscape: + p = append(p, '\\', x) + v.state = StateSingleQuote + case StateComment: + if x == '\n' { + v.state = StateContent + p = append(p, '\n') + } + case StateSlash: + switch x { + case '/': + v.state = StateComment + case '*': + v.state = StateMultilineComment + default: + p = append(p, '/', x) + } + case StateMultilineComment: + switch x { + case '*': + v.state = StateMultilineCommentStar + case '\n': + p = append(p, '\n') + } + case StateMultilineCommentStar: + switch x { + case '/': + v.state = StateContent + case '*': + // Stay + case '\n': + p = append(p, '\n') + default: + v.state = StateMultilineComment + } + default: + panic("Unknown state.") + } + } + return len(p), nil +} diff --git a/subproject/Xray-core-main/infra/conf/json/reader_test.go b/subproject/Xray-core-main/infra/conf/json/reader_test.go new file mode 100644 index 00000000..d30ab072 --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/json/reader_test.go @@ -0,0 +1,96 @@ +package json_test + +import ( + "bytes" + "io" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/xtls/xray-core/common" + . "github.com/xtls/xray-core/infra/conf/json" +) + +func TestReader(t *testing.T) { + data := []struct { + input string + output string + }{ + { + ` +content #comment 1 +#comment 2 +content 2`, + ` +content + +content 2`, + }, + {`content`, `content`}, + {" ", " "}, + {`con/*abcd*/tent`, "content"}, + {` +text // adlkhdf /* +//comment adfkj +text 2*/`, ` +text + +text 2*`}, + {`"//"content`, `"//"content`}, + {`abcd'//'abcd`, `abcd'//'abcd`}, + {`"\""`, `"\""`}, + {`\"/*abcd*/\"`, `\"\"`}, + } + + for _, testCase := range data { + reader := &Reader{ + Reader: bytes.NewReader([]byte(testCase.input)), + } + + actual := make([]byte, 1024) + n, err := reader.Read(actual) + common.Must(err) + if r := cmp.Diff(string(actual[:n]), testCase.output); r != "" { + t.Error(r) + } + } +} + +func TestReader1(t *testing.T) { + type dataStruct struct { + input string + output string + } + + bufLen := 8 + + data := []dataStruct{ + {"loooooooooooooooooooooooooooooooooooooooog", "loooooooooooooooooooooooooooooooooooooooog"}, + {`{"t": "\/testlooooooooooooooooooooooooooooong"}`, `{"t": "\/testlooooooooooooooooooooooooooooong"}`}, + {`{"t": "\/test"}`, `{"t": "\/test"}`}, + {`"\// fake comment"`, `"\// fake comment"`}, + {`"\/\/\/\/\/"`, `"\/\/\/\/\/"`}, + } + + for _, testCase := range data { + reader := &Reader{ + Reader: bytes.NewReader([]byte(testCase.input)), + } + target := make([]byte, 0) + buf := make([]byte, bufLen) + var n int + var err error + for n, err = reader.Read(buf); err == nil; n, err = reader.Read(buf) { + if n > len(buf) { + t.Error("n: ", n) + } + target = append(target, buf[:n]...) + buf = make([]byte, bufLen) + } + if err != nil && err != io.EOF { + t.Error("error: ", err) + } + if string(target) != testCase.output { + t.Error("got ", string(target), " want ", testCase.output) + } + } +} diff --git a/subproject/Xray-core-main/infra/conf/lint.go b/subproject/Xray-core-main/infra/conf/lint.go new file mode 100644 index 00000000..f8a6b38c --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/lint.go @@ -0,0 +1,25 @@ +package conf + +import "github.com/xtls/xray-core/common/errors" + +type ConfigureFilePostProcessingStage interface { + Process(conf *Config) error +} + +var configureFilePostProcessingStages map[string]ConfigureFilePostProcessingStage + +func RegisterConfigureFilePostProcessingStage(name string, stage ConfigureFilePostProcessingStage) { + if configureFilePostProcessingStages == nil { + configureFilePostProcessingStages = make(map[string]ConfigureFilePostProcessingStage) + } + configureFilePostProcessingStages[name] = stage +} + +func PostProcessConfigureFile(conf *Config) error { + for k, v := range configureFilePostProcessingStages { + if err := v.Process(conf); err != nil { + return errors.New("Rejected by Postprocessing Stage ", k).AtError().Base(err) + } + } + return nil +} diff --git a/subproject/Xray-core-main/infra/conf/loader.go b/subproject/Xray-core-main/infra/conf/loader.go new file mode 100644 index 00000000..1dc2de23 --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/loader.go @@ -0,0 +1,85 @@ +package conf + +import ( + "encoding/json" + "strings" + + "github.com/xtls/xray-core/common/errors" +) + +type ConfigCreator func() interface{} + +type ConfigCreatorCache map[string]ConfigCreator + +func (v ConfigCreatorCache) RegisterCreator(id string, creator ConfigCreator) error { + if _, found := v[id]; found { + return errors.New(id, " already registered.").AtError() + } + + v[id] = creator + return nil +} + +func (v ConfigCreatorCache) CreateConfig(id string) (interface{}, error) { + creator, found := v[id] + if !found { + return nil, errors.New("unknown config id: ", id) + } + return creator(), nil +} + +type JSONConfigLoader struct { + cache ConfigCreatorCache + idKey string + configKey string +} + +func NewJSONConfigLoader(cache ConfigCreatorCache, idKey string, configKey string) *JSONConfigLoader { + return &JSONConfigLoader{ + idKey: idKey, + configKey: configKey, + cache: cache, + } +} + +func (v *JSONConfigLoader) LoadWithID(raw []byte, id string) (interface{}, error) { + id = strings.ToLower(id) + config, err := v.cache.CreateConfig(id) + if err != nil { + return nil, err + } + if err := json.Unmarshal(raw, config); err != nil { + return nil, err + } + return config, nil +} + +func (v *JSONConfigLoader) Load(raw []byte) (interface{}, string, error) { + var obj map[string]json.RawMessage + if err := json.Unmarshal(raw, &obj); err != nil { + return nil, "", err + } + rawID, found := obj[v.idKey] + if !found { + return nil, "", errors.New(v.idKey, " not found in JSON context").AtError() + } + var id string + if err := json.Unmarshal(rawID, &id); err != nil { + return nil, "", err + } + rawConfig := json.RawMessage(raw) + if len(v.configKey) > 0 { + configValue, found := obj[v.configKey] + if found { + rawConfig = configValue + } else { + // Default to empty json object. + rawConfig = json.RawMessage([]byte("{}")) + } + } + config, err := v.LoadWithID([]byte(rawConfig), id) + if err != nil { + return nil, id, err + } + return config, id, nil +} diff --git a/subproject/Xray-core-main/infra/conf/log.go b/subproject/Xray-core-main/infra/conf/log.go new file mode 100644 index 00000000..fee8f570 --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/log.go @@ -0,0 +1,65 @@ +package conf + +import ( + "strings" + + "github.com/xtls/xray-core/app/log" + clog "github.com/xtls/xray-core/common/log" +) + +func DefaultLogConfig() *log.Config { + return &log.Config{ + AccessLogType: log.LogType_None, + ErrorLogType: log.LogType_Console, + ErrorLogLevel: clog.Severity_Warning, + } +} + +type LogConfig struct { + AccessLog string `json:"access"` + ErrorLog string `json:"error"` + LogLevel string `json:"loglevel"` + DNSLog bool `json:"dnsLog"` + MaskAddress string `json:"maskAddress"` +} + +func (v *LogConfig) Build() *log.Config { + if v == nil { + return nil + } + config := &log.Config{ + ErrorLogType: log.LogType_Console, + AccessLogType: log.LogType_Console, + EnableDnsLog: v.DNSLog, + } + + if v.AccessLog == "none" { + config.AccessLogType = log.LogType_None + } else if len(v.AccessLog) > 0 { + config.AccessLogPath = v.AccessLog + config.AccessLogType = log.LogType_File + } + if v.ErrorLog == "none" { + config.ErrorLogType = log.LogType_None + } else if len(v.ErrorLog) > 0 { + config.ErrorLogPath = v.ErrorLog + config.ErrorLogType = log.LogType_File + } + + level := strings.ToLower(v.LogLevel) + switch level { + case "debug": + config.ErrorLogLevel = clog.Severity_Debug + case "info": + config.ErrorLogLevel = clog.Severity_Info + case "error": + config.ErrorLogLevel = clog.Severity_Error + case "none": + config.ErrorLogType = log.LogType_None + config.AccessLogType = log.LogType_None + default: + config.ErrorLogLevel = clog.Severity_Warning + } + config.MaskAddress = v.MaskAddress + return config +} diff --git a/subproject/Xray-core-main/infra/conf/loopback.go b/subproject/Xray-core-main/infra/conf/loopback.go new file mode 100644 index 00000000..87d349ce --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/loopback.go @@ -0,0 +1,14 @@ +package conf + +import ( + "github.com/xtls/xray-core/proxy/loopback" + "google.golang.org/protobuf/proto" +) + +type LoopbackConfig struct { + InboundTag string `json:"inboundTag"` +} + +func (l LoopbackConfig) Build() (proto.Message, error) { + return &loopback.Config{InboundTag: l.InboundTag}, nil +} diff --git a/subproject/Xray-core-main/infra/conf/metrics.go b/subproject/Xray-core-main/infra/conf/metrics.go new file mode 100644 index 00000000..75965206 --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/metrics.go @@ -0,0 +1,26 @@ +package conf + +import ( + "github.com/xtls/xray-core/app/metrics" + "github.com/xtls/xray-core/common/errors" +) + +type MetricsConfig struct { + Tag string `json:"tag"` + Listen string `json:"listen"` +} + +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 == "" { + c.Tag = "Metrics" + } + + return &metrics.Config{ + Tag: c.Tag, + Listen: c.Listen, + }, nil +} diff --git a/subproject/Xray-core-main/infra/conf/observatory.go b/subproject/Xray-core-main/infra/conf/observatory.go new file mode 100644 index 00000000..62d6aaba --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/observatory.go @@ -0,0 +1,38 @@ +package conf + +import ( + "google.golang.org/protobuf/proto" + + "github.com/xtls/xray-core/app/observatory" + "github.com/xtls/xray-core/app/observatory/burst" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/infra/conf/cfgcommon/duration" +) + +type ObservatoryConfig struct { + SubjectSelector []string `json:"subjectSelector"` + ProbeURL string `json:"probeURL"` + ProbeInterval duration.Duration `json:"probeInterval"` + EnableConcurrency bool `json:"enableConcurrency"` +} + +func (o *ObservatoryConfig) Build() (proto.Message, error) { + return &observatory.Config{SubjectSelector: o.SubjectSelector, ProbeUrl: o.ProbeURL, ProbeInterval: int64(o.ProbeInterval), EnableConcurrency: o.EnableConcurrency}, nil +} + +type BurstObservatoryConfig struct { + SubjectSelector []string `json:"subjectSelector"` + // health check settings + HealthCheck *healthCheckSettings `json:"pingConfig,omitempty"` +} + +func (b BurstObservatoryConfig) Build() (proto.Message, error) { + if b.HealthCheck == nil { + return nil, errors.New("BurstObservatory requires a valid pingConfig") + } + if result, err := b.HealthCheck.Build(); err == nil { + return &burst.Config{SubjectSelector: b.SubjectSelector, PingConfig: result.(*burst.HealthPingConfig)}, nil + } else { + return nil, err + } +} diff --git a/subproject/Xray-core-main/infra/conf/policy.go b/subproject/Xray-core-main/infra/conf/policy.go new file mode 100644 index 00000000..1182766c --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/policy.go @@ -0,0 +1,102 @@ +package conf + +import ( + "github.com/xtls/xray-core/app/policy" +) + +type Policy struct { + Handshake *uint32 `json:"handshake"` + ConnectionIdle *uint32 `json:"connIdle"` + UplinkOnly *uint32 `json:"uplinkOnly"` + DownlinkOnly *uint32 `json:"downlinkOnly"` + StatsUserUplink bool `json:"statsUserUplink"` + StatsUserDownlink bool `json:"statsUserDownlink"` + StatsUserOnline bool `json:"statsUserOnline"` + BufferSize *int32 `json:"bufferSize"` +} + +func (t *Policy) Build() (*policy.Policy, error) { + config := new(policy.Policy_Timeout) + if t.Handshake != nil { + config.Handshake = &policy.Second{Value: *t.Handshake} + } + if t.ConnectionIdle != nil { + config.ConnectionIdle = &policy.Second{Value: *t.ConnectionIdle} + } + if t.UplinkOnly != nil { + config.UplinkOnly = &policy.Second{Value: *t.UplinkOnly} + } + if t.DownlinkOnly != nil { + config.DownlinkOnly = &policy.Second{Value: *t.DownlinkOnly} + } + + p := &policy.Policy{ + Timeout: config, + Stats: &policy.Policy_Stats{ + UserUplink: t.StatsUserUplink, + UserDownlink: t.StatsUserDownlink, + UserOnline: t.StatsUserOnline, + }, + } + + if t.BufferSize != nil { + bs := int32(-1) + if *t.BufferSize >= 0 { + bs = (*t.BufferSize) * 1024 + } + p.Buffer = &policy.Policy_Buffer{ + Connection: bs, + } + } + + return p, nil +} + +type SystemPolicy struct { + StatsInboundUplink bool `json:"statsInboundUplink"` + StatsInboundDownlink bool `json:"statsInboundDownlink"` + StatsOutboundUplink bool `json:"statsOutboundUplink"` + StatsOutboundDownlink bool `json:"statsOutboundDownlink"` +} + +func (p *SystemPolicy) Build() (*policy.SystemPolicy, error) { + return &policy.SystemPolicy{ + Stats: &policy.SystemPolicy_Stats{ + InboundUplink: p.StatsInboundUplink, + InboundDownlink: p.StatsInboundDownlink, + OutboundUplink: p.StatsOutboundUplink, + OutboundDownlink: p.StatsOutboundDownlink, + }, + }, nil +} + +type PolicyConfig struct { + Levels map[uint32]*Policy `json:"levels"` + System *SystemPolicy `json:"system"` +} + +func (c *PolicyConfig) Build() (*policy.Config, error) { + levels := make(map[uint32]*policy.Policy) + for l, p := range c.Levels { + if p != nil { + pp, err := p.Build() + if err != nil { + return nil, err + } + levels[l] = pp + } + } + config := &policy.Config{ + Level: levels, + } + + if c.System != nil { + sc, err := c.System.Build() + if err != nil { + return nil, err + } + config.System = sc + } + + return config, nil +} diff --git a/subproject/Xray-core-main/infra/conf/policy_test.go b/subproject/Xray-core-main/infra/conf/policy_test.go new file mode 100644 index 00000000..c880f463 --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/policy_test.go @@ -0,0 +1,40 @@ +package conf_test + +import ( + "testing" + + "github.com/xtls/xray-core/common" + . "github.com/xtls/xray-core/infra/conf" +) + +func TestBufferSize(t *testing.T) { + cases := []struct { + Input int32 + Output int32 + }{ + { + Input: 0, + Output: 0, + }, + { + Input: -1, + Output: -1, + }, + { + Input: 1, + Output: 1024, + }, + } + + for _, c := range cases { + bs := c.Input + pConf := Policy{ + BufferSize: &bs, + } + p, err := pConf.Build() + common.Must(err) + if p.Buffer.Connection != c.Output { + t.Error("expected buffer size ", c.Output, " but got ", p.Buffer.Connection) + } + } +} diff --git a/subproject/Xray-core-main/infra/conf/reverse.go b/subproject/Xray-core-main/infra/conf/reverse.go new file mode 100644 index 00000000..f44c9992 --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/reverse.go @@ -0,0 +1,56 @@ +package conf + +import ( + "github.com/xtls/xray-core/app/reverse" + "google.golang.org/protobuf/proto" +) + +type BridgeConfig struct { + Tag string `json:"tag"` + Domain string `json:"domain"` +} + +func (c *BridgeConfig) Build() (*reverse.BridgeConfig, error) { + return &reverse.BridgeConfig{ + Tag: c.Tag, + Domain: c.Domain, + }, nil +} + +type PortalConfig struct { + Tag string `json:"tag"` + Domain string `json:"domain"` +} + +func (c *PortalConfig) Build() (*reverse.PortalConfig, error) { + return &reverse.PortalConfig{ + Tag: c.Tag, + Domain: c.Domain, + }, nil +} + +type ReverseConfig struct { + Bridges []BridgeConfig `json:"bridges"` + Portals []PortalConfig `json:"portals"` +} + +func (c *ReverseConfig) Build() (proto.Message, error) { + config := &reverse.Config{} + for _, bconfig := range c.Bridges { + b, err := bconfig.Build() + if err != nil { + return nil, err + } + config.BridgeConfig = append(config.BridgeConfig, b) + } + + for _, pconfig := range c.Portals { + p, err := pconfig.Build() + if err != nil { + return nil, err + } + config.PortalConfig = append(config.PortalConfig, p) + } + + return config, nil +} diff --git a/subproject/Xray-core-main/infra/conf/reverse_test.go b/subproject/Xray-core-main/infra/conf/reverse_test.go new file mode 100644 index 00000000..15da2b1a --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/reverse_test.go @@ -0,0 +1,45 @@ +package conf_test + +import ( + "testing" + + "github.com/xtls/xray-core/app/reverse" + "github.com/xtls/xray-core/infra/conf" +) + +func TestReverseConfig(t *testing.T) { + creator := func() conf.Buildable { + return new(conf.ReverseConfig) + } + + runMultiTestCase(t, []TestCase{ + { + Input: `{ + "bridges": [{ + "tag": "test", + "domain": "test.example.com" + }] + }`, + Parser: loadJSON(creator), + Output: &reverse.Config{ + BridgeConfig: []*reverse.BridgeConfig{ + {Tag: "test", Domain: "test.example.com"}, + }, + }, + }, + { + Input: `{ + "portals": [{ + "tag": "test", + "domain": "test.example.com" + }] + }`, + Parser: loadJSON(creator), + Output: &reverse.Config{ + PortalConfig: []*reverse.PortalConfig{ + {Tag: "test", Domain: "test.example.com"}, + }, + }, + }, + }) +} diff --git a/subproject/Xray-core-main/infra/conf/router.go b/subproject/Xray-core-main/infra/conf/router.go new file mode 100644 index 00000000..a488d397 --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/router.go @@ -0,0 +1,690 @@ +package conf + +import ( + "bufio" + "bytes" + "encoding/json" + "io" + "runtime" + "strconv" + "strings" + + "github.com/xtls/xray-core/app/router" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/platform" + "github.com/xtls/xray-core/common/platform/filesystem" + "github.com/xtls/xray-core/common/serial" + "google.golang.org/protobuf/proto" +) + +// StrategyConfig represents a strategy config +type StrategyConfig struct { + Type string `json:"type"` + Settings *json.RawMessage `json:"settings"` +} + +type BalancingRule struct { + Tag string `json:"tag"` + Selectors StringList `json:"selector"` + Strategy StrategyConfig `json:"strategy"` + FallbackTag string `json:"fallbackTag"` +} + +// Build builds the balancing rule +func (r *BalancingRule) Build() (*router.BalancingRule, error) { + if r.Tag == "" { + return nil, errors.New("empty balancer tag") + } + if len(r.Selectors) == 0 { + return nil, errors.New("empty selector list") + } + + r.Strategy.Type = strings.ToLower(r.Strategy.Type) + switch r.Strategy.Type { + case "": + r.Strategy.Type = strategyRandom + case strategyRandom, strategyLeastLoad, strategyLeastPing, strategyRoundRobin: + default: + return nil, errors.New("unknown balancing strategy: " + r.Strategy.Type) + } + + settings := []byte("{}") + if r.Strategy.Settings != nil { + settings = ([]byte)(*r.Strategy.Settings) + } + rawConfig, err := strategyConfigLoader.LoadWithID(settings, r.Strategy.Type) + if err != nil { + return nil, errors.New("failed to parse to strategy config.").Base(err) + } + var ts proto.Message + if builder, ok := rawConfig.(Buildable); ok { + ts, err = builder.Build() + if err != nil { + return nil, err + } + } + + return &router.BalancingRule{ + Strategy: r.Strategy.Type, + StrategySettings: serial.ToTypedMessage(ts), + FallbackTag: r.FallbackTag, + OutboundSelector: r.Selectors, + Tag: r.Tag, + }, nil +} + +type RouterConfig struct { + RuleList []json.RawMessage `json:"rules"` + DomainStrategy *string `json:"domainStrategy"` + Balancers []*BalancingRule `json:"balancers"` +} + +func (c *RouterConfig) getDomainStrategy() router.Config_DomainStrategy { + ds := "" + if c.DomainStrategy != nil { + ds = *c.DomainStrategy + } + + switch strings.ToLower(ds) { + case "ipifnonmatch": + return router.Config_IpIfNonMatch + case "ipondemand": + return router.Config_IpOnDemand + default: + return router.Config_AsIs + } +} + +func (c *RouterConfig) Build() (*router.Config, error) { + config := new(router.Config) + config.DomainStrategy = c.getDomainStrategy() + + var rawRuleList []json.RawMessage + if c != nil { + rawRuleList = c.RuleList + } + + for _, rawRule := range rawRuleList { + rule, err := parseRule(rawRule) + if err != nil { + return nil, err + } + + config.Rule = append(config.Rule, rule) + } + for _, rawBalancer := range c.Balancers { + balancer, err := rawBalancer.Build() + if err != nil { + return nil, err + } + config.BalancingRule = append(config.BalancingRule, balancer) + } + return config, nil +} + +type RouterRule struct { + RuleTag string `json:"ruleTag"` + OutboundTag string `json:"outboundTag"` + BalancerTag string `json:"balancerTag"` +} + +func parseIP(s string) (*router.CIDR, error) { + var addr, mask string + i := strings.Index(s, "/") + if i < 0 { + addr = s + } else { + addr = s[:i] + mask = s[i+1:] + } + ip := net.ParseAddress(addr) + switch ip.Family() { + case net.AddressFamilyIPv4: + bits := uint32(32) + if len(mask) > 0 { + bits64, err := strconv.ParseUint(mask, 10, 32) + if err != nil { + return nil, errors.New("invalid network mask for router: ", mask).Base(err) + } + bits = uint32(bits64) + } + if bits > 32 { + return nil, errors.New("invalid network mask for router: ", bits) + } + return &router.CIDR{ + Ip: []byte(ip.IP()), + Prefix: bits, + }, nil + case net.AddressFamilyIPv6: + bits := uint32(128) + if len(mask) > 0 { + bits64, err := strconv.ParseUint(mask, 10, 32) + if err != nil { + return nil, errors.New("invalid network mask for router: ", mask).Base(err) + } + bits = uint32(bits64) + } + if bits > 128 { + return nil, errors.New("invalid network mask for router: ", bits) + } + return &router.CIDR{ + Ip: []byte(ip.IP()), + Prefix: bits, + }, nil + default: + return nil, errors.New("unsupported address for router: ", s) + } +} + +func loadFile(file, code string) ([]byte, error) { + runtime.GC() + r, err := filesystem.OpenAsset(file) + defer r.Close() + if err != nil { + return nil, errors.New("failed to open file: ", file).Base(err) + } + bs := find(r, []byte(code)) + if bs == nil { + return nil, errors.New("code not found in ", file, ": ", code) + } + return bs, nil +} + +func loadIP(file, code string) ([]*router.CIDR, error) { + bs, err := loadFile(file, code) + if err != nil { + return nil, err + } + var geoip router.GeoIP + if err := proto.Unmarshal(bs, &geoip); err != nil { + return nil, errors.New("error unmarshal IP in ", file, ": ", code).Base(err) + } + defer runtime.GC() // or debug.FreeOSMemory() + return geoip.Cidr, nil +} + +func loadSite(file, code string) ([]*router.Domain, error) { + + // Check if domain matcher cache is provided via environment + domainMatcherPath := platform.NewEnvFlag(platform.MphCachePath).GetValue(func() string { return "" }) + if domainMatcherPath != "" { + return []*router.Domain{{}}, nil + } + + bs, err := loadFile(file, code) + if err != nil { + return nil, err + } + var geosite router.GeoSite + if err := proto.Unmarshal(bs, &geosite); err != nil { + return nil, errors.New("error unmarshal Site in ", file, ": ", code).Base(err) + } + defer runtime.GC() // or debug.FreeOSMemory() + return geosite.Domain, nil +} + +func decodeVarint(r *bufio.Reader) (uint64, error) { + var x uint64 + for shift := uint(0); shift < 64; shift += 7 { + b, err := r.ReadByte() + if err != nil { + return 0, err + } + x |= (uint64(b) & 0x7F) << shift + if (b & 0x80) == 0 { + return x, nil + } + } + // The number is too large to represent in a 64-bit value. + return 0, errors.New("varint overflow") +} + +func find(r io.Reader, code []byte) []byte { + codeL := len(code) + if codeL == 0 { + return nil + } + + br := bufio.NewReaderSize(r, 64*1024) + need := 2 + codeL + prefixBuf := make([]byte, need) + + for { + if _, err := br.ReadByte(); err != nil { + return nil + } + + x, err := decodeVarint(br) + if err != nil { + return nil + } + bodyL := int(x) + if bodyL <= 0 { + return nil + } + + prefixL := bodyL + if prefixL > need { + prefixL = need + } + prefix := prefixBuf[:prefixL] + if _, err := io.ReadFull(br, prefix); err != nil { + return nil + } + + match := false + if bodyL >= need { + if int(prefix[1]) == codeL && bytes.Equal(prefix[2:need], code) { + match = true + } + } + + remain := bodyL - prefixL + if match { + out := make([]byte, bodyL) + copy(out, prefix) + if remain > 0 { + if _, err := io.ReadFull(br, out[prefixL:]); err != nil { + return nil + } + } + return out + } + + if remain > 0 { + if _, err := br.Discard(remain); err != nil { + return nil + } + } + } +} + +type AttributeMatcher interface { + Match(*router.Domain) bool +} + +type BooleanMatcher string + +func (m BooleanMatcher) Match(domain *router.Domain) bool { + for _, attr := range domain.Attribute { + if attr.Key == string(m) { + return true + } + } + return false +} + +type AttributeList struct { + matcher []AttributeMatcher +} + +func (al *AttributeList) Match(domain *router.Domain) bool { + for _, matcher := range al.matcher { + if !matcher.Match(domain) { + return false + } + } + return true +} + +func (al *AttributeList) IsEmpty() bool { + return len(al.matcher) == 0 +} + +func parseAttrs(attrs []string) *AttributeList { + al := new(AttributeList) + for _, attr := range attrs { + lc := strings.ToLower(attr) + al.matcher = append(al.matcher, BooleanMatcher(lc)) + } + return al +} + +func loadGeositeWithAttr(file string, siteWithAttr string) ([]*router.Domain, error) { + parts := strings.Split(siteWithAttr, "@") + if len(parts) == 0 { + return nil, errors.New("empty site") + } + country := strings.ToUpper(parts[0]) + attrs := parseAttrs(parts[1:]) + domains, err := loadSite(file, country) + if err != nil { + return nil, err + } + + if attrs.IsEmpty() { + return domains, nil + } + + filteredDomains := make([]*router.Domain, 0, len(domains)) + for _, domain := range domains { + if attrs.Match(domain) { + filteredDomains = append(filteredDomains, domain) + } + } + + return filteredDomains, nil +} + +func parseDomainRule(domain string) ([]*router.Domain, error) { + if strings.HasPrefix(domain, "geosite:") { + country := strings.ToUpper(domain[8:]) + domains, err := loadGeositeWithAttr("geosite.dat", country) + if err != nil { + return nil, errors.New("failed to load geosite: ", country).Base(err) + } + return domains, nil + } + isExtDatFile := 0 + { + const prefix = "ext:" + if strings.HasPrefix(domain, prefix) { + isExtDatFile = len(prefix) + } + const prefixQualified = "ext-domain:" + if strings.HasPrefix(domain, prefixQualified) { + isExtDatFile = len(prefixQualified) + } + } + if isExtDatFile != 0 { + kv := strings.Split(domain[isExtDatFile:], ":") + if len(kv) != 2 { + return nil, errors.New("invalid external resource: ", domain) + } + filename := kv[0] + country := kv[1] + domains, err := loadGeositeWithAttr(filename, country) + if err != nil { + return nil, errors.New("failed to load external sites: ", country, " from ", filename).Base(err) + } + return domains, nil + } + + domainRule := new(router.Domain) + switch { + case strings.HasPrefix(domain, "regexp:"): + domainRule.Type = router.Domain_Regex + domainRule.Value = domain[7:] + + case strings.HasPrefix(domain, "domain:"): + domainRule.Type = router.Domain_Domain + domainRule.Value = domain[7:] + + case strings.HasPrefix(domain, "full:"): + domainRule.Type = router.Domain_Full + domainRule.Value = domain[5:] + + case strings.HasPrefix(domain, "keyword:"): + domainRule.Type = router.Domain_Plain + domainRule.Value = domain[8:] + + case strings.HasPrefix(domain, "dotless:"): + domainRule.Type = router.Domain_Regex + switch substr := domain[8:]; { + case substr == "": + domainRule.Value = "^[^.]*$" + case !strings.Contains(substr, "."): + domainRule.Value = "^[^.]*" + substr + "[^.]*$" + default: + return nil, errors.New("substr in dotless rule should not contain a dot: ", substr) + } + + default: + domainRule.Type = router.Domain_Plain + domainRule.Value = domain + } + return []*router.Domain{domainRule}, nil +} + +func ToCidrList(ips StringList) ([]*router.GeoIP, error) { + var geoipList []*router.GeoIP + var customCidrs []*router.CIDR + + for _, ip := range ips { + if strings.HasPrefix(ip, "geoip:") { + country := ip[6:] + isReverseMatch := false + if strings.HasPrefix(ip, "geoip:!") { + country = ip[7:] + isReverseMatch = true + } + if len(country) == 0 { + return nil, errors.New("empty country name in rule") + } + geoip, err := loadIP("geoip.dat", strings.ToUpper(country)) + if err != nil { + return nil, errors.New("failed to load GeoIP: ", country).Base(err) + } + + geoipList = append(geoipList, &router.GeoIP{ + CountryCode: strings.ToUpper(country), + Cidr: geoip, + ReverseMatch: isReverseMatch, + }) + continue + } + isExtDatFile := 0 + { + const prefix = "ext:" + if strings.HasPrefix(ip, prefix) { + isExtDatFile = len(prefix) + } + const prefixQualified = "ext-ip:" + if strings.HasPrefix(ip, prefixQualified) { + isExtDatFile = len(prefixQualified) + } + } + if isExtDatFile != 0 { + kv := strings.Split(ip[isExtDatFile:], ":") + if len(kv) != 2 { + return nil, errors.New("invalid external resource: ", ip) + } + + filename := kv[0] + country := kv[1] + if len(filename) == 0 || len(country) == 0 { + return nil, errors.New("empty filename or empty country in rule") + } + + isReverseMatch := false + if strings.HasPrefix(country, "!") { + country = country[1:] + isReverseMatch = true + } + geoip, err := loadIP(filename, strings.ToUpper(country)) + if err != nil { + return nil, errors.New("failed to load IPs: ", country, " from ", filename).Base(err) + } + + geoipList = append(geoipList, &router.GeoIP{ + CountryCode: strings.ToUpper(filename + "_" + country), + Cidr: geoip, + ReverseMatch: isReverseMatch, + }) + + continue + } + + ipRule, err := parseIP(ip) + if err != nil { + return nil, errors.New("invalid IP: ", ip).Base(err) + } + customCidrs = append(customCidrs, ipRule) + } + + if len(customCidrs) > 0 { + geoipList = append(geoipList, &router.GeoIP{ + Cidr: customCidrs, + }) + } + + return geoipList, nil +} + +type WebhookRuleConfig struct { + URL string `json:"url"` + Deduplication uint32 `json:"deduplication"` + Headers map[string]string `json:"headers"` +} + +func parseFieldRule(msg json.RawMessage) (*router.RoutingRule, error) { + type RawFieldRule struct { + RouterRule + Domain *StringList `json:"domain"` + Domains *StringList `json:"domains"` + IP *StringList `json:"ip"` + Port *PortList `json:"port"` + Network *NetworkList `json:"network"` + SourceIP *StringList `json:"sourceIP"` + Source *StringList `json:"source"` + SourcePort *PortList `json:"sourcePort"` + User *StringList `json:"user"` + VlessRoute *PortList `json:"vlessRoute"` + InboundTag *StringList `json:"inboundTag"` + Protocols *StringList `json:"protocol"` + Attributes map[string]string `json:"attrs"` + LocalIP *StringList `json:"localIP"` + LocalPort *PortList `json:"localPort"` + Process *StringList `json:"process"` + Webhook *WebhookRuleConfig `json:"webhook"` + } + rawFieldRule := new(RawFieldRule) + err := json.Unmarshal(msg, rawFieldRule) + if err != nil { + return nil, err + } + + rule := new(router.RoutingRule) + rule.RuleTag = rawFieldRule.RuleTag + switch { + case len(rawFieldRule.OutboundTag) > 0: + rule.TargetTag = &router.RoutingRule_Tag{ + Tag: rawFieldRule.OutboundTag, + } + case len(rawFieldRule.BalancerTag) > 0: + rule.TargetTag = &router.RoutingRule_BalancingTag{ + BalancingTag: rawFieldRule.BalancerTag, + } + default: + return nil, errors.New("neither outboundTag nor balancerTag is specified in routing rule") + } + + if rawFieldRule.Domain != nil { + for _, domain := range *rawFieldRule.Domain { + rules, err := parseDomainRule(domain) + if err != nil { + return nil, errors.New("failed to parse domain rule: ", domain).Base(err) + } + rule.Domain = append(rule.Domain, rules...) + } + } + + if rawFieldRule.Domains != nil { + for _, domain := range *rawFieldRule.Domains { + rules, err := parseDomainRule(domain) + if err != nil { + return nil, errors.New("failed to parse domain rule: ", domain).Base(err) + } + rule.Domain = append(rule.Domain, rules...) + } + } + + if rawFieldRule.IP != nil { + geoipList, err := ToCidrList(*rawFieldRule.IP) + if err != nil { + return nil, err + } + rule.Geoip = geoipList + } + + if rawFieldRule.Port != nil { + rule.PortList = rawFieldRule.Port.Build() + } + + if rawFieldRule.Network != nil { + rule.Networks = rawFieldRule.Network.Build() + } + + if rawFieldRule.SourceIP == nil { + rawFieldRule.SourceIP = rawFieldRule.Source + } + + if rawFieldRule.SourceIP != nil { + geoipList, err := ToCidrList(*rawFieldRule.SourceIP) + if err != nil { + return nil, err + } + rule.SourceGeoip = geoipList + } + + if rawFieldRule.SourcePort != nil { + rule.SourcePortList = rawFieldRule.SourcePort.Build() + } + + if rawFieldRule.LocalIP != nil { + geoipList, err := ToCidrList(*rawFieldRule.LocalIP) + if err != nil { + return nil, err + } + rule.LocalGeoip = geoipList + } + + if rawFieldRule.LocalPort != nil { + rule.LocalPortList = rawFieldRule.LocalPort.Build() + } + + if rawFieldRule.User != nil { + for _, s := range *rawFieldRule.User { + rule.UserEmail = append(rule.UserEmail, s) + } + } + + if rawFieldRule.VlessRoute != nil { + rule.VlessRouteList = rawFieldRule.VlessRoute.Build() + } + + if rawFieldRule.InboundTag != nil { + for _, s := range *rawFieldRule.InboundTag { + rule.InboundTag = append(rule.InboundTag, s) + } + } + + if rawFieldRule.Protocols != nil { + for _, s := range *rawFieldRule.Protocols { + rule.Protocol = append(rule.Protocol, s) + } + } + + if len(rawFieldRule.Attributes) > 0 { + rule.Attributes = rawFieldRule.Attributes + } + + if rawFieldRule.Process != nil && len(*rawFieldRule.Process) > 0 { + rule.Process = *rawFieldRule.Process + } + + if rawFieldRule.Webhook != nil && rawFieldRule.Webhook.URL != "" { + rule.Webhook = &router.WebhookConfig{ + Url: rawFieldRule.Webhook.URL, + Deduplication: rawFieldRule.Webhook.Deduplication, + Headers: rawFieldRule.Webhook.Headers, + } + } + + return rule, nil +} + +func parseRule(msg json.RawMessage) (*router.RoutingRule, error) { + rawRule := new(RouterRule) + err := json.Unmarshal(msg, rawRule) + if err != nil { + return nil, errors.New("invalid router rule").Base(err) + } + + fieldrule, err := parseFieldRule(msg) + if err != nil { + return nil, errors.New("invalid field rule").Base(err) + } + return fieldrule, nil +} diff --git a/subproject/Xray-core-main/infra/conf/router_strategy.go b/subproject/Xray-core-main/infra/conf/router_strategy.go new file mode 100644 index 00000000..464cbcfb --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/router_strategy.go @@ -0,0 +1,102 @@ +package conf + +import ( + "google.golang.org/protobuf/proto" + "strings" + + "github.com/xtls/xray-core/app/observatory/burst" + "github.com/xtls/xray-core/app/router" + "github.com/xtls/xray-core/infra/conf/cfgcommon/duration" +) + +const ( + strategyRandom string = "random" + strategyLeastPing string = "leastping" + strategyRoundRobin string = "roundrobin" + strategyLeastLoad string = "leastload" +) + +var ( + strategyConfigLoader = NewJSONConfigLoader(ConfigCreatorCache{ + strategyRandom: func() interface{} { return new(strategyEmptyConfig) }, + strategyLeastPing: func() interface{} { return new(strategyEmptyConfig) }, + strategyRoundRobin: func() interface{} { return new(strategyEmptyConfig) }, + strategyLeastLoad: func() interface{} { return new(strategyLeastLoadConfig) }, + }, "type", "settings") +) + +type strategyEmptyConfig struct { +} + +func (v *strategyEmptyConfig) Build() (proto.Message, error) { + return nil, nil +} + +type strategyLeastLoadConfig struct { + // weight settings + Costs []*router.StrategyWeight `json:"costs,omitempty"` + // ping rtt baselines + Baselines []duration.Duration `json:"baselines,omitempty"` + // expected nodes count to select + Expected int32 `json:"expected,omitempty"` + // max acceptable rtt, filter away high delay nodes. default 0 + MaxRTT duration.Duration `json:"maxRTT,omitempty"` + // acceptable failure rate + Tolerance float64 `json:"tolerance,omitempty"` +} + +// healthCheckSettings holds settings for health Checker +type healthCheckSettings struct { + Destination string `json:"destination"` + Connectivity string `json:"connectivity"` + Interval duration.Duration `json:"interval"` + SamplingCount int `json:"sampling"` + Timeout duration.Duration `json:"timeout"` + HttpMethod string `json:"httpMethod"` +} + +func (h healthCheckSettings) Build() (proto.Message, error) { + var httpMethod string + if h.HttpMethod == "" { + httpMethod = "HEAD" + } else { + httpMethod = strings.TrimSpace(h.HttpMethod) + } + return &burst.HealthPingConfig{ + Destination: h.Destination, + Connectivity: h.Connectivity, + Interval: int64(h.Interval), + Timeout: int64(h.Timeout), + SamplingCount: int32(h.SamplingCount), + HttpMethod: httpMethod, + }, nil +} + +// Build implements Buildable. +func (v *strategyLeastLoadConfig) Build() (proto.Message, error) { + config := &router.StrategyLeastLoadConfig{} + config.Costs = v.Costs + config.Tolerance = float32(v.Tolerance) + if config.Tolerance < 0 { + config.Tolerance = 0 + } + if config.Tolerance > 1 { + config.Tolerance = 1 + } + config.Expected = v.Expected + if config.Expected < 0 { + config.Expected = 0 + } + config.MaxRTT = int64(v.MaxRTT) + if config.MaxRTT < 0 { + config.MaxRTT = 0 + } + config.Baselines = make([]int64, 0) + for _, b := range v.Baselines { + if b <= 0 { + continue + } + config.Baselines = append(config.Baselines, int64(b)) + } + return config, nil +} diff --git a/subproject/Xray-core-main/infra/conf/router_test.go b/subproject/Xray-core-main/infra/conf/router_test.go new file mode 100644 index 00000000..2533046c --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/router_test.go @@ -0,0 +1,305 @@ +package conf_test + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "testing" + "time" + _ "unsafe" + + "github.com/xtls/xray-core/app/router" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/platform" + "github.com/xtls/xray-core/common/platform/filesystem" + "github.com/xtls/xray-core/common/serial" + . "github.com/xtls/xray-core/infra/conf" + "google.golang.org/protobuf/proto" +) + +func getAssetPath(file string) (string, error) { + path := platform.GetAssetLocation(file) + _, err := os.Stat(path) + if os.IsNotExist(err) { + path := filepath.Join("..", "..", "resources", file) + _, err := os.Stat(path) + if os.IsNotExist(err) { + return "", fmt.Errorf("can't find %s in standard asset locations or {project_root}/resources", file) + } + if err != nil { + return "", fmt.Errorf("can't stat %s: %v", path, err) + } + return path, nil + } + if err != nil { + return "", fmt.Errorf("can't stat %s: %v", path, err) + } + + return path, nil +} + +func TestToCidrList(t *testing.T) { + tempDir, err := os.MkdirTemp("", "test-") + if err != nil { + t.Fatalf("can't create temp dir: %v", err) + } + defer os.RemoveAll(tempDir) + + geoipPath, err := getAssetPath("geoip.dat") + if err != nil { + t.Fatal(err) + } + + common.Must(filesystem.CopyFile(filepath.Join(tempDir, "geoip.dat"), geoipPath)) + common.Must(filesystem.CopyFile(filepath.Join(tempDir, "geoiptestrouter.dat"), geoipPath)) + + os.Setenv("xray.location.asset", tempDir) + defer os.Unsetenv("xray.location.asset") + + ips := StringList([]string{ + "geoip:us", + "geoip:cn", + "geoip:!cn", + "ext:geoiptestrouter.dat:!cn", + "ext:geoiptestrouter.dat:ca", + "ext-ip:geoiptestrouter.dat:!cn", + "ext-ip:geoiptestrouter.dat:!ca", + }) + + _, err = ToCidrList(ips) + if err != nil { + t.Fatalf("Failed to parse geoip list, got %s", err) + } +} + +func TestRouterConfig(t *testing.T) { + createParser := func() func(string) (proto.Message, error) { + return func(s string) (proto.Message, error) { + config := new(RouterConfig) + if err := json.Unmarshal([]byte(s), config); err != nil { + return nil, err + } + return config.Build() + } + } + + runMultiTestCase(t, []TestCase{ + { + Input: `{ + "domainStrategy": "AsIs", + "rules": [ + { + "domain": [ + "baidu.com", + "qq.com" + ], + "outboundTag": "direct" + }, + { + "ip": [ + "10.0.0.0/8", + "::1/128" + ], + "outboundTag": "test" + },{ + "port": "53, 443, 1000-2000", + "outboundTag": "test" + },{ + "port": 123, + "outboundTag": "test" + } + ], + "balancers": [ + { + "tag": "b1", + "selector": ["test"], + "fallbackTag": "fall" + }, + { + "tag": "b2", + "selector": ["test"], + "strategy": { + "type": "leastload", + "settings": { + "healthCheck": { + "interval": "5m0s", + "sampling": 2, + "timeout": "5s", + "destination": "dest", + "connectivity": "conn" + }, + "costs": [ + { + "regexp": true, + "match": "\\d+(\\.\\d+)", + "value": 5 + } + ], + "baselines": ["400ms", "600ms"], + "expected": 6, + "maxRTT": "1000ms", + "tolerance": 0.5 + } + }, + "fallbackTag": "fall" + } + ] + }`, + Parser: createParser(), + Output: &router.Config{ + DomainStrategy: router.Config_AsIs, + BalancingRule: []*router.BalancingRule{ + { + Tag: "b1", + OutboundSelector: []string{"test"}, + Strategy: "random", + FallbackTag: "fall", + }, + { + Tag: "b2", + OutboundSelector: []string{"test"}, + Strategy: "leastload", + StrategySettings: serial.ToTypedMessage(&router.StrategyLeastLoadConfig{ + Costs: []*router.StrategyWeight{ + { + Regexp: true, + Match: "\\d+(\\.\\d+)", + Value: 5, + }, + }, + Baselines: []int64{ + int64(time.Duration(400) * time.Millisecond), + int64(time.Duration(600) * time.Millisecond), + }, + Expected: 6, + MaxRTT: int64(time.Duration(1000) * time.Millisecond), + Tolerance: 0.5, + }), + FallbackTag: "fall", + }, + }, + Rule: []*router.RoutingRule{ + { + Domain: []*router.Domain{ + { + Type: router.Domain_Plain, + Value: "baidu.com", + }, + { + Type: router.Domain_Plain, + Value: "qq.com", + }, + }, + TargetTag: &router.RoutingRule_Tag{ + Tag: "direct", + }, + }, + { + Geoip: []*router.GeoIP{ + { + Cidr: []*router.CIDR{ + { + Ip: []byte{10, 0, 0, 0}, + Prefix: 8, + }, + { + Ip: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, + Prefix: 128, + }, + }, + }, + }, + TargetTag: &router.RoutingRule_Tag{ + Tag: "test", + }, + }, + { + PortList: &net.PortList{ + Range: []*net.PortRange{ + {From: 53, To: 53}, + {From: 443, To: 443}, + {From: 1000, To: 2000}, + }, + }, + TargetTag: &router.RoutingRule_Tag{ + Tag: "test", + }, + }, + { + PortList: &net.PortList{ + Range: []*net.PortRange{ + {From: 123, To: 123}, + }, + }, + TargetTag: &router.RoutingRule_Tag{ + Tag: "test", + }, + }, + }, + }, + }, + { + Input: `{ + "domainStrategy": "IPIfNonMatch", + "rules": [ + { + "domain": [ + "baidu.com", + "qq.com" + ], + "outboundTag": "direct" + }, + { + "ip": [ + "10.0.0.0/8", + "::1/128" + ], + "outboundTag": "test" + } + ] + }`, + Parser: createParser(), + Output: &router.Config{ + DomainStrategy: router.Config_IpIfNonMatch, + Rule: []*router.RoutingRule{ + { + Domain: []*router.Domain{ + { + Type: router.Domain_Plain, + Value: "baidu.com", + }, + { + Type: router.Domain_Plain, + Value: "qq.com", + }, + }, + TargetTag: &router.RoutingRule_Tag{ + Tag: "direct", + }, + }, + { + Geoip: []*router.GeoIP{ + { + Cidr: []*router.CIDR{ + { + Ip: []byte{10, 0, 0, 0}, + Prefix: 8, + }, + { + Ip: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, + Prefix: 128, + }, + }, + }, + }, + TargetTag: &router.RoutingRule_Tag{ + Tag: "test", + }, + }, + }, + }, + }, + }) +} diff --git a/subproject/Xray-core-main/infra/conf/serial/builder.go b/subproject/Xray-core-main/infra/conf/serial/builder.go new file mode 100644 index 00000000..3ae98025 --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/serial/builder.go @@ -0,0 +1,66 @@ +package serial + +import ( + "context" + "io" + + "github.com/xtls/xray-core/common/errors" + creflect "github.com/xtls/xray-core/common/reflect" + "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/infra/conf" + "github.com/xtls/xray-core/main/confloader" +) + +func MergeConfigFromFiles(files []*core.ConfigSource) (string, error) { + c, err := mergeConfigs(files) + if err != nil { + return "", err + } + + if j, ok := creflect.MarshalToJson(c, true); ok { + return j, nil + } + return "", errors.New("marshal to json failed.").AtError() +} + +func mergeConfigs(files []*core.ConfigSource) (*conf.Config, error) { + cf := &conf.Config{} + for i, file := range files { + errors.LogInfo(context.Background(), "Reading config: ", file) + r, err := confloader.LoadConfig(file.Name) + if err != nil { + return nil, errors.New("failed to read config: ", file).Base(err) + } + c, err := ReaderDecoderByFormat[file.Format](r) + if err != nil { + return nil, errors.New("failed to decode config: ", file).Base(err) + } + if i == 0 { + *cf = *c + continue + } + cf.Override(c, file.Name) + } + return cf, nil +} + +func BuildConfig(files []*core.ConfigSource) (*core.Config, error) { + config, err := mergeConfigs(files) + if err != nil { + return nil, err + } + return config.Build() +} + +type readerDecoder func(io.Reader) (*conf.Config, error) + +var ReaderDecoderByFormat = make(map[string]readerDecoder) + +func init() { + ReaderDecoderByFormat["json"] = DecodeJSONConfig + ReaderDecoderByFormat["yaml"] = DecodeYAMLConfig + ReaderDecoderByFormat["toml"] = DecodeTOMLConfig + + core.ConfigBuilderForFiles = BuildConfig + core.ConfigMergedFormFiles = MergeConfigFromFiles +} diff --git a/subproject/Xray-core-main/infra/conf/serial/loader.go b/subproject/Xray-core-main/infra/conf/serial/loader.go new file mode 100644 index 00000000..ef9963df --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/serial/loader.go @@ -0,0 +1,149 @@ +package serial + +import ( + "bytes" + "encoding/json" + "io" + + "github.com/ghodss/yaml" + "github.com/pelletier/go-toml" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/infra/conf" + json_reader "github.com/xtls/xray-core/infra/conf/json" +) + +type offset struct { + line int + char int +} + +func findOffset(b []byte, o int) *offset { + if o >= len(b) || o < 0 { + return nil + } + + line := 1 + char := 0 + for i, x := range b { + if i == o { + break + } + if x == '\n' { + line++ + char = 0 + } else { + char++ + } + } + + return &offset{line: line, char: char} +} + +// DecodeJSONConfig reads from reader and decode the config into *conf.Config +// syntax error could be detected. +func DecodeJSONConfig(reader io.Reader) (*conf.Config, error) { + jsonConfig := &conf.Config{} + + jsonContent := bytes.NewBuffer(make([]byte, 0, 10240)) + jsonReader := io.TeeReader(&json_reader.Reader{ + Reader: reader, + }, jsonContent) + decoder := json.NewDecoder(jsonReader) + + if err := decoder.Decode(jsonConfig); err != nil { + var pos *offset + cause := errors.Cause(err) + switch tErr := cause.(type) { + case *json.SyntaxError: + pos = findOffset(jsonContent.Bytes(), int(tErr.Offset)) + case *json.UnmarshalTypeError: + pos = findOffset(jsonContent.Bytes(), int(tErr.Offset)) + } + if pos != nil { + return nil, errors.New("failed to read config file at line ", pos.line, " char ", pos.char).Base(err) + } + return nil, errors.New("failed to read config file").Base(err) + } + + return jsonConfig, nil +} + +func LoadJSONConfig(reader io.Reader) (*core.Config, error) { + jsonConfig, err := DecodeJSONConfig(reader) + if err != nil { + return nil, err + } + + pbConfig, err := jsonConfig.Build() + if err != nil { + return nil, errors.New("failed to parse json config").Base(err) + } + + return pbConfig, nil +} + +// DecodeTOMLConfig reads from reader and decode the config into *conf.Config +// using github.com/pelletier/go-toml and map to convert toml to json. +func DecodeTOMLConfig(reader io.Reader) (*conf.Config, error) { + tomlFile, err := io.ReadAll(reader) + if err != nil { + return nil, errors.New("failed to read config file").Base(err) + } + + configMap := make(map[string]interface{}) + if err := toml.Unmarshal(tomlFile, &configMap); err != nil { + return nil, errors.New("failed to convert toml to map").Base(err) + } + + jsonFile, err := json.Marshal(&configMap) + if err != nil { + return nil, errors.New("failed to convert map to json").Base(err) + } + + return DecodeJSONConfig(bytes.NewReader(jsonFile)) +} + +func LoadTOMLConfig(reader io.Reader) (*core.Config, error) { + tomlConfig, err := DecodeTOMLConfig(reader) + if err != nil { + return nil, err + } + + pbConfig, err := tomlConfig.Build() + if err != nil { + return nil, errors.New("failed to parse toml config").Base(err) + } + + return pbConfig, nil +} + +// DecodeYAMLConfig reads from reader and decode the config into *conf.Config +// using github.com/ghodss/yaml to convert yaml to json. +func DecodeYAMLConfig(reader io.Reader) (*conf.Config, error) { + yamlFile, err := io.ReadAll(reader) + if err != nil { + return nil, errors.New("failed to read config file").Base(err) + } + + jsonFile, err := yaml.YAMLToJSON(yamlFile) + if err != nil { + return nil, errors.New("failed to convert yaml to json").Base(err) + } + + return DecodeJSONConfig(bytes.NewReader(jsonFile)) +} + +func LoadYAMLConfig(reader io.Reader) (*core.Config, error) { + yamlConfig, err := DecodeYAMLConfig(reader) + if err != nil { + return nil, err + } + + pbConfig, err := yamlConfig.Build() + if err != nil { + return nil, errors.New("failed to parse yaml config").Base(err) + } + + return pbConfig, nil +} diff --git a/subproject/Xray-core-main/infra/conf/serial/loader_test.go b/subproject/Xray-core-main/infra/conf/serial/loader_test.go new file mode 100644 index 00000000..e6a745b4 --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/serial/loader_test.go @@ -0,0 +1,63 @@ +package serial_test + +import ( + "bytes" + "strings" + "testing" + + "github.com/xtls/xray-core/infra/conf/serial" +) + +func TestLoaderError(t *testing.T) { + testCases := []struct { + Input string + Output string + }{ + { + Input: `{ + "log": { + // abcd + 0, + "loglevel": "info" + } + }`, + Output: "line 4 char 6", + }, + { + Input: `{ + "log": { + // abcd + "loglevel": "info", + } + }`, + Output: "line 5 char 5", + }, + { + Input: `{ + "port": 1, + "inbounds": [{ + "protocol": "test" + }] + }`, + Output: "parse json config", + }, + { + Input: `{ + "inbounds": [{ + "port": 1, + "listen": 0, + "protocol": "test" + }] + }`, + Output: "line 1 char 1", + }, + } + for _, testCase := range testCases { + reader := bytes.NewReader([]byte(testCase.Input)) + _, err := serial.LoadJSONConfig(reader) + errString := err.Error() + if !strings.Contains(errString, testCase.Output) { + t.Error("unexpected output from json: ", testCase.Input, ". expected ", testCase.Output, ", but actually ", errString) + } + } +} diff --git a/subproject/Xray-core-main/infra/conf/serial/serial.go b/subproject/Xray-core-main/infra/conf/serial/serial.go new file mode 100644 index 00000000..86baba80 --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/serial/serial.go @@ -0,0 +1 @@ +package serial diff --git a/subproject/Xray-core-main/infra/conf/shadowsocks.go b/subproject/Xray-core-main/infra/conf/shadowsocks.go new file mode 100644 index 00000000..490c2997 --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/shadowsocks.go @@ -0,0 +1,266 @@ +package conf + +import ( + "strings" + + "github.com/sagernet/sing-shadowsocks/shadowaead_2022" + C "github.com/sagernet/sing/common" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/serial" + "github.com/xtls/xray-core/proxy/shadowsocks" + "github.com/xtls/xray-core/proxy/shadowsocks_2022" + "google.golang.org/protobuf/proto" +) + +func cipherFromString(c string) shadowsocks.CipherType { + switch strings.ToLower(c) { + case "aes-128-gcm", "aead_aes_128_gcm": + return shadowsocks.CipherType_AES_128_GCM + case "aes-256-gcm", "aead_aes_256_gcm": + return shadowsocks.CipherType_AES_256_GCM + case "chacha20-poly1305", "aead_chacha20_poly1305", "chacha20-ietf-poly1305": + return shadowsocks.CipherType_CHACHA20_POLY1305 + case "xchacha20-poly1305", "aead_xchacha20_poly1305", "xchacha20-ietf-poly1305": + return shadowsocks.CipherType_XCHACHA20_POLY1305 + case "none", "plain": + return shadowsocks.CipherType_NONE + default: + return shadowsocks.CipherType_UNKNOWN + } +} + +type ShadowsocksUserConfig struct { + Cipher string `json:"method"` + Password string `json:"password"` + Level byte `json:"level"` + Email string `json:"email"` + Address *Address `json:"address"` + Port uint16 `json:"port"` +} + +type ShadowsocksServerConfig struct { + Cipher string `json:"method"` + Password string `json:"password"` + Level byte `json:"level"` + Email string `json:"email"` + Users []*ShadowsocksUserConfig `json:"clients"` + NetworkList *NetworkList `json:"network"` +} + +func (v *ShadowsocksServerConfig) Build() (proto.Message, error) { + errors.PrintNonRemovalDeprecatedFeatureWarning("Shadowsocks (with no Forward Secrecy, etc.)", "VLESS Encryption") + + if C.Contains(shadowaead_2022.List, v.Cipher) { + return buildShadowsocks2022(v) + } + + config := new(shadowsocks.ServerConfig) + config.Network = v.NetworkList.Build() + + if v.Users != nil { + for _, user := range v.Users { + account := &shadowsocks.Account{ + Password: user.Password, + CipherType: cipherFromString(user.Cipher), + } + if account.Password == "" { + return nil, errors.New("Shadowsocks password is not specified.") + } + if account.CipherType < shadowsocks.CipherType_AES_128_GCM || + account.CipherType > shadowsocks.CipherType_XCHACHA20_POLY1305 { + return nil, errors.New("unsupported cipher method: ", user.Cipher) + } + config.Users = append(config.Users, &protocol.User{ + Email: user.Email, + Level: uint32(user.Level), + Account: serial.ToTypedMessage(account), + }) + } + } else { + account := &shadowsocks.Account{ + Password: v.Password, + CipherType: cipherFromString(v.Cipher), + } + if account.Password == "" { + return nil, errors.New("Shadowsocks password is not specified.") + } + if account.CipherType == shadowsocks.CipherType_UNKNOWN { + return nil, errors.New("unknown cipher method: ", v.Cipher) + } + config.Users = append(config.Users, &protocol.User{ + Email: v.Email, + Level: uint32(v.Level), + Account: serial.ToTypedMessage(account), + }) + } + + return config, nil +} + +func buildShadowsocks2022(v *ShadowsocksServerConfig) (proto.Message, error) { + if len(v.Users) == 0 { + config := new(shadowsocks_2022.ServerConfig) + config.Method = v.Cipher + config.Key = v.Password + config.Network = v.NetworkList.Build() + config.Email = v.Email + return config, nil + } + + if v.Cipher == "" { + return nil, errors.New("shadowsocks 2022 (multi-user): missing server method") + } + if !strings.Contains(v.Cipher, "aes") { + return nil, errors.New("shadowsocks 2022 (multi-user): only blake3-aes-*-gcm methods are supported") + } + + if v.Users[0].Address == nil { + config := new(shadowsocks_2022.MultiUserServerConfig) + config.Method = v.Cipher + config.Key = v.Password + config.Network = v.NetworkList.Build() + + for _, user := range v.Users { + if user.Cipher != "" { + return nil, errors.New("shadowsocks 2022 (multi-user): users must have empty method") + } + account := &shadowsocks_2022.Account{ + Key: user.Password, + } + config.Users = append(config.Users, &protocol.User{ + Email: user.Email, + Level: uint32(user.Level), + Account: serial.ToTypedMessage(account), + }) + } + return config, nil + } + + config := new(shadowsocks_2022.RelayServerConfig) + config.Method = v.Cipher + config.Key = v.Password + config.Network = v.NetworkList.Build() + for _, user := range v.Users { + if user.Cipher != "" { + return nil, errors.New("shadowsocks 2022 (relay): users must have empty method") + } + if user.Address == nil { + return nil, errors.New("shadowsocks 2022 (relay): all users must have relay address") + } + config.Destinations = append(config.Destinations, &shadowsocks_2022.RelayDestination{ + Key: user.Password, + Email: user.Email, + Address: user.Address.Build(), + Port: uint32(user.Port), + }) + } + return config, nil +} + +type ShadowsocksServerTarget struct { + Address *Address `json:"address"` + Port uint16 `json:"port"` + Level byte `json:"level"` + Email string `json:"email"` + Cipher string `json:"method"` + Password string `json:"password"` + UoT bool `json:"uot"` + UoTVersion int `json:"uotVersion"` +} + +type ShadowsocksClientConfig struct { + Address *Address `json:"address"` + Port uint16 `json:"port"` + Level byte `json:"level"` + Email string `json:"email"` + Cipher string `json:"method"` + Password string `json:"password"` + UoT bool `json:"uot"` + UoTVersion int `json:"uotVersion"` + Servers []*ShadowsocksServerTarget `json:"servers"` +} + +func (v *ShadowsocksClientConfig) Build() (proto.Message, error) { + errors.PrintNonRemovalDeprecatedFeatureWarning("Shadowsocks (with no Forward Secrecy, etc.)", "VLESS Encryption") + + if v.Address != nil { + v.Servers = []*ShadowsocksServerTarget{ + { + Address: v.Address, + Port: v.Port, + Level: v.Level, + Email: v.Email, + Cipher: v.Cipher, + Password: v.Password, + UoT: v.UoT, + UoTVersion: v.UoTVersion, + }, + } + } + if len(v.Servers) != 1 { + return nil, errors.New(`Shadowsocks settings: "servers" should have one and only one member. Multiple endpoints in "servers" should use multiple Shadowsocks outbounds and routing balancer instead`) + } + + if len(v.Servers) == 1 { + server := v.Servers[0] + if C.Contains(shadowaead_2022.List, server.Cipher) { + if server.Address == nil { + return nil, errors.New("Shadowsocks server address is not set.") + } + if server.Port == 0 { + return nil, errors.New("Invalid Shadowsocks port.") + } + if server.Password == "" { + return nil, errors.New("Shadowsocks password is not specified.") + } + + config := new(shadowsocks_2022.ClientConfig) + config.Address = server.Address.Build() + config.Port = uint32(server.Port) + config.Method = server.Cipher + config.Key = server.Password + config.UdpOverTcp = server.UoT + config.UdpOverTcpVersion = uint32(server.UoTVersion) + return config, nil + } + } + + config := new(shadowsocks.ClientConfig) + for _, server := range v.Servers { + if C.Contains(shadowaead_2022.List, server.Cipher) { + return nil, errors.New("Shadowsocks 2022 accept no multi servers") + } + if server.Address == nil { + return nil, errors.New("Shadowsocks server address is not set.") + } + if server.Port == 0 { + return nil, errors.New("Invalid Shadowsocks port.") + } + if server.Password == "" { + return nil, errors.New("Shadowsocks password is not specified.") + } + account := &shadowsocks.Account{ + Password: server.Password, + } + account.CipherType = cipherFromString(server.Cipher) + if account.CipherType == shadowsocks.CipherType_UNKNOWN { + return nil, errors.New("unknown cipher method: ", server.Cipher) + } + + ss := &protocol.ServerEndpoint{ + Address: server.Address.Build(), + Port: uint32(server.Port), + User: &protocol.User{ + Level: uint32(server.Level), + Email: server.Email, + Account: serial.ToTypedMessage(account), + }, + } + + config.Server = ss + break + } + + return config, nil +} diff --git a/subproject/Xray-core-main/infra/conf/shadowsocks_test.go b/subproject/Xray-core-main/infra/conf/shadowsocks_test.go new file mode 100644 index 00000000..a4ca6714 --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/shadowsocks_test.go @@ -0,0 +1,36 @@ +package conf_test + +import ( + "testing" + + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/serial" + . "github.com/xtls/xray-core/infra/conf" + "github.com/xtls/xray-core/proxy/shadowsocks" +) + +func TestShadowsocksServerConfigParsing(t *testing.T) { + creator := func() Buildable { + return new(ShadowsocksServerConfig) + } + + runMultiTestCase(t, []TestCase{ + { + Input: `{ + "method": "aes-256-GCM", + "password": "xray-password" + }`, + Parser: loadJSON(creator), + Output: &shadowsocks.ServerConfig{ + Users: []*protocol.User{{ + Account: serial.ToTypedMessage(&shadowsocks.Account{ + CipherType: shadowsocks.CipherType_AES_256_GCM, + Password: "xray-password", + }), + }}, + Network: []net.Network{net.Network_TCP}, + }, + }, + }) +} diff --git a/subproject/Xray-core-main/infra/conf/socks.go b/subproject/Xray-core-main/infra/conf/socks.go new file mode 100644 index 00000000..d0d68d81 --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/socks.go @@ -0,0 +1,133 @@ +package conf + +import ( + "encoding/json" + + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/serial" + "github.com/xtls/xray-core/proxy/socks" + "google.golang.org/protobuf/proto" +) + +type SocksAccount struct { + Username string `json:"user"` + Password string `json:"pass"` +} + +func (v *SocksAccount) Build() *socks.Account { + return &socks.Account{ + Username: v.Username, + Password: v.Password, + } +} + +const ( + AuthMethodNoAuth = "noauth" + AuthMethodUserPass = "password" +) + +type SocksServerConfig struct { + AuthMethod string `json:"auth"` + Accounts []*SocksAccount `json:"accounts"` + UDP bool `json:"udp"` + Host *Address `json:"ip"` + UserLevel uint32 `json:"userLevel"` +} + +func (v *SocksServerConfig) Build() (proto.Message, error) { + config := new(socks.ServerConfig) + switch v.AuthMethod { + case AuthMethodNoAuth: + config.AuthType = socks.AuthType_NO_AUTH + case AuthMethodUserPass: + config.AuthType = socks.AuthType_PASSWORD + default: + // errors.New("unknown socks auth method: ", v.AuthMethod, ". Default to noauth.").AtWarning().WriteToLog() + config.AuthType = socks.AuthType_NO_AUTH + } + + if len(v.Accounts) > 0 { + config.Accounts = make(map[string]string, len(v.Accounts)) + for _, account := range v.Accounts { + config.Accounts[account.Username] = account.Password + } + } + + config.UdpEnabled = v.UDP + if v.Host != nil { + config.Address = v.Host.Build() + } + + config.UserLevel = v.UserLevel + return config, nil +} + +type SocksRemoteConfig struct { + Address *Address `json:"address"` + Port uint16 `json:"port"` + Users []json.RawMessage `json:"users"` +} + +type SocksClientConfig struct { + Address *Address `json:"address"` + Port uint16 `json:"port"` + Level uint32 `json:"level"` + Email string `json:"email"` + Username string `json:"user"` + Password string `json:"pass"` + Servers []*SocksRemoteConfig `json:"servers"` +} + +func (v *SocksClientConfig) Build() (proto.Message, error) { + config := new(socks.ClientConfig) + if v.Address != nil { + v.Servers = []*SocksRemoteConfig{ + { + Address: v.Address, + Port: v.Port, + }, + } + if len(v.Username) > 0 { + v.Servers[0].Users = []json.RawMessage{{}} + } + } + if len(v.Servers) != 1 { + return nil, errors.New(`SOCKS settings: "servers" should have one and only one member. Multiple endpoints in "servers" should use multiple SOCKS outbounds and routing balancer instead`) + } + for _, serverConfig := range v.Servers { + if len(serverConfig.Users) > 1 { + return nil, errors.New(`SOCKS servers: "users" should have one member at most. Multiple members in "users" should use multiple SOCKS outbounds and routing balancer instead`) + } + server := &protocol.ServerEndpoint{ + Address: serverConfig.Address.Build(), + Port: uint32(serverConfig.Port), + } + for _, rawUser := range serverConfig.Users { + user := new(protocol.User) + if v.Address != nil { + user.Level = v.Level + user.Email = v.Email + } else { + if err := json.Unmarshal(rawUser, user); err != nil { + return nil, errors.New("failed to parse Socks user").Base(err).AtError() + } + } + account := new(SocksAccount) + if v.Address != nil { + account.Username = v.Username + account.Password = v.Password + } else { + if err := json.Unmarshal(rawUser, account); err != nil { + return nil, errors.New("failed to parse socks account").Base(err).AtError() + } + } + user.Account = serial.ToTypedMessage(account.Build()) + server.User = user + break + } + config.Server = server + break + } + return config, nil +} diff --git a/subproject/Xray-core-main/infra/conf/socks_test.go b/subproject/Xray-core-main/infra/conf/socks_test.go new file mode 100644 index 00000000..d94802ec --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/socks_test.go @@ -0,0 +1,113 @@ +package conf_test + +import ( + "testing" + + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/serial" + . "github.com/xtls/xray-core/infra/conf" + "github.com/xtls/xray-core/proxy/socks" +) + +func TestSocksInboundConfig(t *testing.T) { + creator := func() Buildable { + return new(SocksServerConfig) + } + + runMultiTestCase(t, []TestCase{ + { + Input: `{ + "auth": "password", + "accounts": [ + { + "user": "my-username", + "pass": "my-password" + } + ], + "udp": false, + "ip": "127.0.0.1", + "userLevel": 1 + }`, + Parser: loadJSON(creator), + Output: &socks.ServerConfig{ + AuthType: socks.AuthType_PASSWORD, + Accounts: map[string]string{ + "my-username": "my-password", + }, + UdpEnabled: false, + Address: &net.IPOrDomain{ + Address: &net.IPOrDomain_Ip{ + Ip: []byte{127, 0, 0, 1}, + }, + }, + UserLevel: 1, + }, + }, + }) +} + +func TestSocksOutboundConfig(t *testing.T) { + creator := func() Buildable { + return new(SocksClientConfig) + } + + runMultiTestCase(t, []TestCase{ + { + Input: `{ + "servers": [{ + "address": "127.0.0.1", + "port": 1234, + "users": [ + {"user": "test user", "pass": "test pass", "email": "test@email.com"} + ] + }] + }`, + Parser: loadJSON(creator), + Output: &socks.ClientConfig{ + Server: &protocol.ServerEndpoint{ + Address: &net.IPOrDomain{ + Address: &net.IPOrDomain_Ip{ + Ip: []byte{127, 0, 0, 1}, + }, + }, + Port: 1234, + User: &protocol.User{ + Email: "test@email.com", + Account: serial.ToTypedMessage(&socks.Account{ + Username: "test user", + Password: "test pass", + }), + }, + }, + }, + }, + { + Input: `{ + "address": "127.0.0.1", + "port": 1234, + "user": "test user", + "pass": "test pass", + "email": "test@email.com" + }`, + Parser: loadJSON(creator), + Output: &socks.ClientConfig{ + Server: &protocol.ServerEndpoint{ + Address: &net.IPOrDomain{ + Address: &net.IPOrDomain_Ip{ + Ip: []byte{127, 0, 0, 1}, + }, + }, + Port: 1234, + User: &protocol.User{ + Email: "test@email.com", + Account: serial.ToTypedMessage(&socks.Account{ + Username: "test user", + Password: "test pass", + }), + }, + }, + }, + }, + }) +} diff --git a/subproject/Xray-core-main/infra/conf/transport_authenticators.go b/subproject/Xray-core-main/infra/conf/transport_authenticators.go new file mode 100644 index 00000000..a9590af9 --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/transport_authenticators.go @@ -0,0 +1,208 @@ +package conf + +import ( + "sort" + + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/utils" + "github.com/xtls/xray-core/transport/internet/headers/http" + "github.com/xtls/xray-core/transport/internet/headers/noop" + "google.golang.org/protobuf/proto" +) + +type NoOpConnectionAuthenticator struct{} + +func (NoOpConnectionAuthenticator) Build() (proto.Message, error) { + return new(noop.ConnectionConfig), nil +} + +type AuthenticatorRequest struct { + Version string `json:"version"` + Method string `json:"method"` + Path StringList `json:"path"` + Headers map[string]*StringList `json:"headers"` +} + +func sortMapKeys(m map[string]*StringList) []string { + var keys []string + for key := range m { + keys = append(keys, key) + } + sort.Strings(keys) + return keys +} + +func (v *AuthenticatorRequest) Build() (*http.RequestConfig, error) { + config := &http.RequestConfig{ + Uri: []string{"/"}, + Header: []*http.Header{ + { + Name: "Host", + Value: []string{"www.baidu.com", "www.bing.com"}, + }, + { + Name: "User-Agent", + Value: []string{utils.ChromeUA}, + }, + { + Name: "Sec-CH-UA", + Value: []string{utils.ChromeUACH}, + }, + { + Name: "Sec-CH-UA-Mobile", + Value: []string{"?0"}, + }, + { + Name: "Sec-CH-UA-Platform", + Value: []string{"Windows"}, + }, + { + Name: "Sec-Fetch-Mode", + Value: []string{"no-cors", "cors", "same-origin"}, + }, + { + Name: "Sec-Fetch-Dest", + Value: []string{"empty"}, + }, + { + Name: "Sec-Fetch-Site", + Value: []string{"none"}, + }, + { + Name: "Sec-Fetch-User", + Value: []string{"?1"}, + }, + { + Name: "Accept-Encoding", + Value: []string{"gzip, deflate"}, + }, + { + Name: "Connection", + Value: []string{"keep-alive"}, + }, + { + Name: "Pragma", + Value: []string{"no-cache"}, + }, + }, + } + + if len(v.Version) > 0 { + config.Version = &http.Version{Value: v.Version} + } + + if len(v.Method) > 0 { + config.Method = &http.Method{Value: v.Method} + } + + if len(v.Path) > 0 { + config.Uri = append([]string(nil), (v.Path)...) + } + + if len(v.Headers) > 0 { + config.Header = make([]*http.Header, 0, len(v.Headers)) + headerNames := sortMapKeys(v.Headers) + for _, key := range headerNames { + value := v.Headers[key] + if value == nil { + return nil, errors.New("empty HTTP header value: " + key).AtError() + } + config.Header = append(config.Header, &http.Header{ + Name: key, + Value: append([]string(nil), (*value)...), + }) + } + } + + return config, nil +} + +type AuthenticatorResponse struct { + Version string `json:"version"` + Status string `json:"status"` + Reason string `json:"reason"` + Headers map[string]*StringList `json:"headers"` +} + +func (v *AuthenticatorResponse) Build() (*http.ResponseConfig, error) { + config := &http.ResponseConfig{ + Header: []*http.Header{ + { + Name: "Content-Type", + Value: []string{"application/octet-stream", "video/mpeg"}, + }, + { + Name: "Transfer-Encoding", + Value: []string{"chunked"}, + }, + { + Name: "Connection", + Value: []string{"keep-alive"}, + }, + { + Name: "Pragma", + Value: []string{"no-cache"}, + }, + { + Name: "Cache-Control", + Value: []string{"private", "no-cache"}, + }, + }, + } + + if len(v.Version) > 0 { + config.Version = &http.Version{Value: v.Version} + } + + if len(v.Status) > 0 || len(v.Reason) > 0 { + config.Status = &http.Status{ + Code: "200", + Reason: "OK", + } + if len(v.Status) > 0 { + config.Status.Code = v.Status + } + if len(v.Reason) > 0 { + config.Status.Reason = v.Reason + } + } + + if len(v.Headers) > 0 { + config.Header = make([]*http.Header, 0, len(v.Headers)) + headerNames := sortMapKeys(v.Headers) + for _, key := range headerNames { + value := v.Headers[key] + if value == nil { + return nil, errors.New("empty HTTP header value: " + key).AtError() + } + config.Header = append(config.Header, &http.Header{ + Name: key, + Value: append([]string(nil), (*value)...), + }) + } + } + + return config, nil +} + +type Authenticator struct { + Request AuthenticatorRequest `json:"request"` + Response AuthenticatorResponse `json:"response"` +} + +func (v *Authenticator) Build() (proto.Message, error) { + config := new(http.Config) + requestConfig, err := v.Request.Build() + if err != nil { + return nil, err + } + config.Request = requestConfig + + responseConfig, err := v.Response.Build() + if err != nil { + return nil, err + } + config.Response = responseConfig + + return config, nil +} diff --git a/subproject/Xray-core-main/infra/conf/transport_internet.go b/subproject/Xray-core-main/infra/conf/transport_internet.go new file mode 100644 index 00000000..70259507 --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/transport_internet.go @@ -0,0 +1,2019 @@ +package conf + +import ( + "context" + "encoding/base64" + "encoding/hex" + "encoding/json" + "math" + "net/url" + "os" + "runtime" + "strconv" + "strings" + "syscall" + "time" + + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/platform/filesystem" + "github.com/xtls/xray-core/common/serial" + "github.com/xtls/xray-core/transport/internet" + "github.com/xtls/xray-core/transport/internet/finalmask/fragment" + "github.com/xtls/xray-core/transport/internet/finalmask/header/custom" + "github.com/xtls/xray-core/transport/internet/finalmask/header/dns" + "github.com/xtls/xray-core/transport/internet/finalmask/header/dtls" + "github.com/xtls/xray-core/transport/internet/finalmask/header/srtp" + "github.com/xtls/xray-core/transport/internet/finalmask/header/utp" + "github.com/xtls/xray-core/transport/internet/finalmask/header/wechat" + "github.com/xtls/xray-core/transport/internet/finalmask/header/wireguard" + "github.com/xtls/xray-core/transport/internet/finalmask/mkcp/aes128gcm" + "github.com/xtls/xray-core/transport/internet/finalmask/mkcp/original" + "github.com/xtls/xray-core/transport/internet/finalmask/noise" + "github.com/xtls/xray-core/transport/internet/finalmask/salamander" + finalsudoku "github.com/xtls/xray-core/transport/internet/finalmask/sudoku" + "github.com/xtls/xray-core/transport/internet/finalmask/xdns" + "github.com/xtls/xray-core/transport/internet/finalmask/xicmp" + "github.com/xtls/xray-core/transport/internet/httpupgrade" + "github.com/xtls/xray-core/transport/internet/hysteria" + "github.com/xtls/xray-core/transport/internet/hysteria/congestion/bbr" + "github.com/xtls/xray-core/transport/internet/kcp" + "github.com/xtls/xray-core/transport/internet/reality" + "github.com/xtls/xray-core/transport/internet/splithttp" + "github.com/xtls/xray-core/transport/internet/tcp" + "github.com/xtls/xray-core/transport/internet/tls" + "github.com/xtls/xray-core/transport/internet/websocket" + "google.golang.org/protobuf/proto" +) + +var ( + tcpHeaderLoader = NewJSONConfigLoader(ConfigCreatorCache{ + "none": func() interface{} { return new(NoOpConnectionAuthenticator) }, + "http": func() interface{} { return new(Authenticator) }, + }, "type", "") +) + +type KCPConfig struct { + Mtu *uint32 `json:"mtu"` + Tti *uint32 `json:"tti"` + UpCap *uint32 `json:"uplinkCapacity"` + DownCap *uint32 `json:"downlinkCapacity"` + Congestion *bool `json:"congestion"` + ReadBufferSize *uint32 `json:"readBufferSize"` + WriteBufferSize *uint32 `json:"writeBufferSize"` + HeaderConfig json.RawMessage `json:"header"` + Seed *string `json:"seed"` +} + +// Build implements Buildable. +func (c *KCPConfig) Build() (proto.Message, error) { + config := new(kcp.Config) + + if c.Mtu != nil { + mtu := *c.Mtu + // if mtu < 576 || mtu > 1460 { + // return nil, errors.New("invalid mKCP MTU size: ", mtu).AtError() + // } + config.Mtu = &kcp.MTU{Value: mtu} + } + if c.Tti != nil { + tti := *c.Tti + if tti < 10 || tti > 5000 { + return nil, errors.New("invalid mKCP TTI: ", tti).AtError() + } + config.Tti = &kcp.TTI{Value: tti} + } + if c.UpCap != nil { + config.UplinkCapacity = &kcp.UplinkCapacity{Value: *c.UpCap} + } + if c.DownCap != nil { + config.DownlinkCapacity = &kcp.DownlinkCapacity{Value: *c.DownCap} + } + if c.Congestion != nil { + config.Congestion = *c.Congestion + } + if c.ReadBufferSize != nil { + size := *c.ReadBufferSize + if size > 0 { + config.ReadBuffer = &kcp.ReadBuffer{Size: size * 1024 * 1024} + } else { + config.ReadBuffer = &kcp.ReadBuffer{Size: 512 * 1024} + } + } + if c.WriteBufferSize != nil { + size := *c.WriteBufferSize + if size > 0 { + config.WriteBuffer = &kcp.WriteBuffer{Size: size * 1024 * 1024} + } else { + config.WriteBuffer = &kcp.WriteBuffer{Size: 512 * 1024} + } + } + if c.HeaderConfig != nil || c.Seed != nil { + return nil, errors.PrintRemovedFeatureError("mkcp header & seed", "finalmask/udp header-* & mkcp-original & mkcp-aes128gcm") + } + + return config, nil +} + +type TCPConfig struct { + HeaderConfig json.RawMessage `json:"header"` + AcceptProxyProtocol bool `json:"acceptProxyProtocol"` +} + +// Build implements Buildable. +func (c *TCPConfig) Build() (proto.Message, error) { + config := new(tcp.Config) + if len(c.HeaderConfig) > 0 { + headerConfig, _, err := tcpHeaderLoader.Load(c.HeaderConfig) + if err != nil { + return nil, errors.New("invalid TCP header config").Base(err).AtError() + } + ts, err := headerConfig.(Buildable).Build() + if err != nil { + return nil, errors.New("invalid TCP header config").Base(err).AtError() + } + config.HeaderSettings = serial.ToTypedMessage(ts) + } + if c.AcceptProxyProtocol { + config.AcceptProxyProtocol = c.AcceptProxyProtocol + } + return config, nil +} + +type WebSocketConfig struct { + Host string `json:"host"` + Path string `json:"path"` + Headers map[string]string `json:"headers"` + AcceptProxyProtocol bool `json:"acceptProxyProtocol"` + HeartbeatPeriod uint32 `json:"heartbeatPeriod"` +} + +// Build implements Buildable. +func (c *WebSocketConfig) Build() (proto.Message, error) { + path := c.Path + var ed uint32 + if u, err := url.Parse(path); err == nil { + if q := u.Query(); q.Get("ed") != "" { + Ed, _ := strconv.Atoi(q.Get("ed")) + ed = uint32(Ed) + q.Del("ed") + u.RawQuery = q.Encode() + path = u.String() + } + } + // Priority (client): host > serverName > address + for k, v := range c.Headers { + if strings.ToLower(k) == "host" { + errors.PrintDeprecatedFeatureWarning(`"host" in "headers"`, `independent "host"`) + if c.Host == "" { + c.Host = v + } + delete(c.Headers, k) + } + } + config := &websocket.Config{ + Path: path, + Host: c.Host, + Header: c.Headers, + AcceptProxyProtocol: c.AcceptProxyProtocol, + Ed: ed, + HeartbeatPeriod: c.HeartbeatPeriod, + } + return config, nil +} + +type HttpUpgradeConfig struct { + Host string `json:"host"` + Path string `json:"path"` + Headers map[string]string `json:"headers"` + AcceptProxyProtocol bool `json:"acceptProxyProtocol"` +} + +// Build implements Buildable. +func (c *HttpUpgradeConfig) Build() (proto.Message, error) { + path := c.Path + var ed uint32 + if u, err := url.Parse(path); err == nil { + if q := u.Query(); q.Get("ed") != "" { + Ed, _ := strconv.Atoi(q.Get("ed")) + ed = uint32(Ed) + q.Del("ed") + u.RawQuery = q.Encode() + path = u.String() + } + } + // Priority (client): host > serverName > address + for k := range c.Headers { + if strings.ToLower(k) == "host" { + return nil, errors.New(`"headers" can't contain "host"`) + } + } + config := &httpupgrade.Config{ + Path: path, + Host: c.Host, + Header: c.Headers, + AcceptProxyProtocol: c.AcceptProxyProtocol, + Ed: ed, + } + return config, nil +} + +type SplitHTTPConfig struct { + Host string `json:"host"` + Path string `json:"path"` + Mode string `json:"mode"` + Headers map[string]string `json:"headers"` + XPaddingBytes Int32Range `json:"xPaddingBytes"` + XPaddingObfsMode bool `json:"xPaddingObfsMode"` + XPaddingKey string `json:"xPaddingKey"` + XPaddingHeader string `json:"xPaddingHeader"` + XPaddingPlacement string `json:"xPaddingPlacement"` + XPaddingMethod string `json:"xPaddingMethod"` + UplinkHTTPMethod string `json:"uplinkHTTPMethod"` + SessionPlacement string `json:"sessionPlacement"` + SessionKey string `json:"sessionKey"` + SeqPlacement string `json:"seqPlacement"` + SeqKey string `json:"seqKey"` + UplinkDataPlacement string `json:"uplinkDataPlacement"` + UplinkDataKey string `json:"uplinkDataKey"` + UplinkChunkSize Int32Range `json:"uplinkChunkSize"` + NoGRPCHeader bool `json:"noGRPCHeader"` + NoSSEHeader bool `json:"noSSEHeader"` + ScMaxEachPostBytes Int32Range `json:"scMaxEachPostBytes"` + ScMinPostsIntervalMs Int32Range `json:"scMinPostsIntervalMs"` + ScMaxBufferedPosts int64 `json:"scMaxBufferedPosts"` + ScStreamUpServerSecs Int32Range `json:"scStreamUpServerSecs"` + ServerMaxHeaderBytes int32 `json:"serverMaxHeaderBytes"` + Xmux XmuxConfig `json:"xmux"` + DownloadSettings *StreamConfig `json:"downloadSettings"` + Extra json.RawMessage `json:"extra"` +} + +type XmuxConfig struct { + MaxConcurrency Int32Range `json:"maxConcurrency"` + MaxConnections Int32Range `json:"maxConnections"` + CMaxReuseTimes Int32Range `json:"cMaxReuseTimes"` + HMaxRequestTimes Int32Range `json:"hMaxRequestTimes"` + HMaxReusableSecs Int32Range `json:"hMaxReusableSecs"` + HKeepAlivePeriod int64 `json:"hKeepAlivePeriod"` +} + +func newRangeConfig(input Int32Range) *splithttp.RangeConfig { + return &splithttp.RangeConfig{ + From: input.From, + To: input.To, + } +} + +// Build implements Buildable. +func (c *SplitHTTPConfig) Build() (proto.Message, error) { + if c.Extra != nil { + var extra SplitHTTPConfig + if err := json.Unmarshal(c.Extra, &extra); err != nil { + return nil, errors.New(`Failed to unmarshal "extra".`).Base(err) + } + extra.Host = c.Host + extra.Path = c.Path + extra.Mode = c.Mode + c = &extra + } + + switch c.Mode { + case "": + c.Mode = "auto" + case "auto", "packet-up", "stream-up", "stream-one": + default: + return nil, errors.New("unsupported mode: " + c.Mode) + } + + // Priority (client): host > serverName > address + for k := range c.Headers { + if strings.ToLower(k) == "host" { + return nil, errors.New(`"headers" can't contain "host"`) + } + } + + if c.XPaddingBytes != (Int32Range{}) && (c.XPaddingBytes.From <= 0 || c.XPaddingBytes.To <= 0) { + return nil, errors.New("xPaddingBytes cannot be disabled") + } + + if c.XPaddingKey == "" { + c.XPaddingKey = "x_padding" + } + + if c.XPaddingHeader == "" { + c.XPaddingHeader = "X-Padding" + } + + switch c.XPaddingPlacement { + case "": + c.XPaddingPlacement = "queryInHeader" + case "cookie", "header", "query", "queryInHeader": + default: + return nil, errors.New("unsupported padding placement: " + c.XPaddingPlacement) + } + + switch c.XPaddingMethod { + case "": + c.XPaddingMethod = "repeat-x" + case "repeat-x", "tokenish": + default: + return nil, errors.New("unsupported padding method: " + c.XPaddingMethod) + } + + switch c.UplinkDataPlacement { + case "": + c.UplinkDataPlacement = splithttp.PlacementAuto + case splithttp.PlacementAuto, splithttp.PlacementBody: + case splithttp.PlacementCookie, splithttp.PlacementHeader: + if c.Mode != "packet-up" { + return nil, errors.New("UplinkDataPlacement can be " + c.UplinkDataPlacement + " only in packet-up mode") + } + default: + return nil, errors.New("unsupported uplink data placement: " + c.UplinkDataPlacement) + } + + if c.UplinkHTTPMethod == "" { + c.UplinkHTTPMethod = "POST" + } + c.UplinkHTTPMethod = strings.ToUpper(c.UplinkHTTPMethod) + + if c.UplinkHTTPMethod == "GET" && c.Mode != "packet-up" { + return nil, errors.New("uplinkHTTPMethod can be GET only in packet-up mode") + } + + switch c.SessionPlacement { + case "": + c.SessionPlacement = "path" + case "path", "cookie", "header", "query": + default: + return nil, errors.New("unsupported session placement: " + c.SessionPlacement) + } + + switch c.SeqPlacement { + case "": + c.SeqPlacement = "path" + case "path", "cookie", "header", "query": + default: + return nil, errors.New("unsupported seq placement: " + c.SeqPlacement) + } + + if c.SessionPlacement != "path" && c.SessionKey == "" { + switch c.SessionPlacement { + case "cookie", "query": + c.SessionKey = "x_session" + case "header": + c.SessionKey = "X-Session" + } + } + + if c.SeqPlacement != "path" && c.SeqKey == "" { + switch c.SeqPlacement { + case "cookie", "query": + c.SeqKey = "x_seq" + case "header": + c.SeqKey = "X-Seq" + } + } + + if c.UplinkDataPlacement != splithttp.PlacementBody && c.UplinkDataKey == "" { + switch c.UplinkDataPlacement { + case splithttp.PlacementCookie: + c.UplinkDataKey = "x_data" + case splithttp.PlacementAuto, splithttp.PlacementHeader: + c.UplinkDataKey = "X-Data" + } + } + + if c.ServerMaxHeaderBytes < 0 { + return nil, errors.New("invalid negative value of maxHeaderBytes") + } + + if c.Xmux.MaxConnections.To > 0 && c.Xmux.MaxConcurrency.To > 0 { + return nil, errors.New("maxConnections cannot be specified together with maxConcurrency") + } + if c.Xmux == (XmuxConfig{}) { + c.Xmux.MaxConcurrency.From = 1 + c.Xmux.MaxConcurrency.To = 1 + c.Xmux.HMaxRequestTimes.From = 600 + c.Xmux.HMaxRequestTimes.To = 900 + c.Xmux.HMaxReusableSecs.From = 1800 + c.Xmux.HMaxReusableSecs.To = 3000 + } + + config := &splithttp.Config{ + Host: c.Host, + Path: c.Path, + Mode: c.Mode, + Headers: c.Headers, + XPaddingBytes: newRangeConfig(c.XPaddingBytes), + XPaddingObfsMode: c.XPaddingObfsMode, + XPaddingKey: c.XPaddingKey, + XPaddingHeader: c.XPaddingHeader, + XPaddingPlacement: c.XPaddingPlacement, + XPaddingMethod: c.XPaddingMethod, + UplinkHTTPMethod: c.UplinkHTTPMethod, + SessionPlacement: c.SessionPlacement, + SeqPlacement: c.SeqPlacement, + SessionKey: c.SessionKey, + SeqKey: c.SeqKey, + UplinkDataPlacement: c.UplinkDataPlacement, + UplinkDataKey: c.UplinkDataKey, + UplinkChunkSize: newRangeConfig(c.UplinkChunkSize), + NoGRPCHeader: c.NoGRPCHeader, + NoSSEHeader: c.NoSSEHeader, + ScMaxEachPostBytes: newRangeConfig(c.ScMaxEachPostBytes), + ScMinPostsIntervalMs: newRangeConfig(c.ScMinPostsIntervalMs), + ScMaxBufferedPosts: c.ScMaxBufferedPosts, + ScStreamUpServerSecs: newRangeConfig(c.ScStreamUpServerSecs), + ServerMaxHeaderBytes: c.ServerMaxHeaderBytes, + Xmux: &splithttp.XmuxConfig{ + MaxConcurrency: newRangeConfig(c.Xmux.MaxConcurrency), + MaxConnections: newRangeConfig(c.Xmux.MaxConnections), + CMaxReuseTimes: newRangeConfig(c.Xmux.CMaxReuseTimes), + HMaxRequestTimes: newRangeConfig(c.Xmux.HMaxRequestTimes), + HMaxReusableSecs: newRangeConfig(c.Xmux.HMaxReusableSecs), + HKeepAlivePeriod: c.Xmux.HKeepAlivePeriod, + }, + } + + if c.DownloadSettings != nil { + if c.Mode == "stream-one" { + return nil, errors.New(`Can not use "downloadSettings" in "stream-one" mode.`) + } + var err error + if config.DownloadSettings, err = c.DownloadSettings.Build(); err != nil { + return nil, errors.New(`Failed to build "downloadSettings".`).Base(err) + } + } + + return config, nil +} + +const ( + Byte = 1 + Kilobyte = 1024 * Byte + Megabyte = 1024 * Kilobyte + Gigabyte = 1024 * Megabyte + Terabyte = 1024 * Gigabyte +) + +type Bandwidth string + +func (b Bandwidth) Bps() (uint64, error) { + s := strings.TrimSpace(strings.ToLower(string(b))) + if s == "" { + return 0, nil + } + + idx := len(s) + for i, c := range s { + if (c < '0' || c > '9') && c != '.' { + idx = i + break + } + } + + numStr := s[:idx] + unit := strings.TrimSpace(s[idx:]) + + val, err := strconv.ParseFloat(numStr, 64) + if err != nil { + return 0, err + } + + mul := uint64(1) + switch unit { + case "", "b", "bps": + mul = Byte + case "k", "kb", "kbps": + mul = Kilobyte + case "m", "mb", "mbps": + mul = Megabyte + case "g", "gb", "gbps": + mul = Gigabyte + case "t", "tb", "tbps": + mul = Terabyte + default: + return 0, errors.New("unsupported unit: " + unit) + } + + return uint64(val*float64(mul)) / 8, nil +} + +type UdpHop struct { + PortList json.RawMessage `json:"ports"` + Interval *Int32Range `json:"interval"` +} + +type Masquerade struct { + Type string `json:"type"` + + Dir string `json:"dir"` + + Url string `json:"url"` + RewriteHost bool `json:"rewriteHost"` + Insecure bool `json:"insecure"` + + Content string `json:"content"` + Headers map[string]string `json:"headers"` + StatusCode int32 `json:"statusCode"` +} + +type HysteriaConfig struct { + Version int32 `json:"version"` + Auth string `json:"auth"` + + Congestion *string `json:"congestion"` + Up *Bandwidth `json:"up"` + Down *Bandwidth `json:"down"` + UdpHop *UdpHop `json:"udphop"` + + UdpIdleTimeout int64 `json:"udpIdleTimeout"` + Masquerade Masquerade `json:"masquerade"` +} + +func (c *HysteriaConfig) Build() (proto.Message, error) { + if c.Version != 2 { + return nil, errors.New("version != 2") + } + + if c.Congestion != nil || c.Up != nil || c.Down != nil || c.UdpHop != nil { + errors.LogWarning(context.Background(), "congestion & up & down & udphop move to finalmask/quicParams") + } + + if c.UdpIdleTimeout != 0 && (c.UdpIdleTimeout < 2 || c.UdpIdleTimeout > 600) { + return nil, errors.New("UdpIdleTimeout must be between 2 and 600") + } + + config := &hysteria.Config{} + config.Version = c.Version + config.Auth = c.Auth + config.UdpIdleTimeout = c.UdpIdleTimeout + config.MasqType = c.Masquerade.Type + config.MasqFile = c.Masquerade.Dir + config.MasqUrl = c.Masquerade.Url + config.MasqUrlRewriteHost = c.Masquerade.RewriteHost + config.MasqUrlInsecure = c.Masquerade.Insecure + config.MasqString = c.Masquerade.Content + config.MasqStringHeaders = c.Masquerade.Headers + config.MasqStringStatusCode = c.Masquerade.StatusCode + + if config.UdpIdleTimeout == 0 { + config.UdpIdleTimeout = 60 + } + + return config, nil +} + +func readFileOrString(f string, s []string) ([]byte, error) { + if len(f) > 0 { + return filesystem.ReadCert(f) + } + if len(s) > 0 { + return []byte(strings.Join(s, "\n")), nil + } + return nil, errors.New("both file and bytes are empty.") +} + +type TLSCertConfig struct { + CertFile string `json:"certificateFile"` + CertStr []string `json:"certificate"` + KeyFile string `json:"keyFile"` + KeyStr []string `json:"key"` + Usage string `json:"usage"` + OcspStapling uint64 `json:"ocspStapling"` + OneTimeLoading bool `json:"oneTimeLoading"` + BuildChain bool `json:"buildChain"` +} + +// Build implements Buildable. +func (c *TLSCertConfig) Build() (*tls.Certificate, error) { + certificate := new(tls.Certificate) + + cert, err := readFileOrString(c.CertFile, c.CertStr) + if err != nil { + return nil, errors.New("failed to parse certificate").Base(err) + } + certificate.Certificate = cert + certificate.CertificatePath = c.CertFile + + if len(c.KeyFile) > 0 || len(c.KeyStr) > 0 { + key, err := readFileOrString(c.KeyFile, c.KeyStr) + if err != nil { + return nil, errors.New("failed to parse key").Base(err) + } + certificate.Key = key + certificate.KeyPath = c.KeyFile + } + + switch strings.ToLower(c.Usage) { + case "encipherment": + certificate.Usage = tls.Certificate_ENCIPHERMENT + case "verify": + certificate.Usage = tls.Certificate_AUTHORITY_VERIFY + case "issue": + certificate.Usage = tls.Certificate_AUTHORITY_ISSUE + default: + certificate.Usage = tls.Certificate_ENCIPHERMENT + } + if certificate.KeyPath == "" && certificate.CertificatePath == "" { + certificate.OneTimeLoading = true + } else { + certificate.OneTimeLoading = c.OneTimeLoading + } + certificate.OcspStapling = c.OcspStapling + certificate.BuildChain = c.BuildChain + + return certificate, nil +} + +type QuicParamsConfig struct { + Congestion string `json:"congestion"` + Debug bool `json:"debug"` + BbrProfile string `json:"bbrProfile"` + BrutalUp Bandwidth `json:"brutalUp"` + BrutalDown Bandwidth `json:"brutalDown"` + UdpHop UdpHop `json:"udpHop"` + InitStreamReceiveWindow uint64 `json:"initStreamReceiveWindow"` + MaxStreamReceiveWindow uint64 `json:"maxStreamReceiveWindow"` + InitConnectionReceiveWindow uint64 `json:"initConnectionReceiveWindow"` + MaxConnectionReceiveWindow uint64 `json:"maxConnectionReceiveWindow"` + MaxIdleTimeout int64 `json:"maxIdleTimeout"` + KeepAlivePeriod int64 `json:"keepAlivePeriod"` + DisablePathMTUDiscovery bool `json:"disablePathMTUDiscovery"` + MaxIncomingStreams int64 `json:"maxIncomingStreams"` +} + +type TLSConfig struct { + AllowInsecure bool `json:"allowInsecure"` + Certs []*TLSCertConfig `json:"certificates"` + ServerName string `json:"serverName"` + ALPN *StringList `json:"alpn"` + EnableSessionResumption bool `json:"enableSessionResumption"` + DisableSystemRoot bool `json:"disableSystemRoot"` + MinVersion string `json:"minVersion"` + MaxVersion string `json:"maxVersion"` + CipherSuites string `json:"cipherSuites"` + Fingerprint string `json:"fingerprint"` + RejectUnknownSNI bool `json:"rejectUnknownSni"` + CurvePreferences *StringList `json:"curvePreferences"` + MasterKeyLog string `json:"masterKeyLog"` + PinnedPeerCertSha256 string `json:"pinnedPeerCertSha256"` + VerifyPeerCertByName string `json:"verifyPeerCertByName"` + VerifyPeerCertInNames []string `json:"verifyPeerCertInNames"` + ECHServerKeys string `json:"echServerKeys"` + ECHConfigList string `json:"echConfigList"` + ECHForceQuery string `json:"echForceQuery"` + ECHSocketSettings *SocketConfig `json:"echSockopt"` +} + +// Build implements Buildable. +func (c *TLSConfig) Build() (proto.Message, error) { + config := new(tls.Config) + config.Certificate = make([]*tls.Certificate, len(c.Certs)) + for idx, certConf := range c.Certs { + cert, err := certConf.Build() + if err != nil { + return nil, err + } + config.Certificate[idx] = cert + } + serverName := c.ServerName + if len(c.ServerName) > 0 { + config.ServerName = serverName + } + if c.ALPN != nil && len(*c.ALPN) > 0 { + config.NextProtocol = []string(*c.ALPN) + } + if len(config.NextProtocol) > 1 { + for _, p := range config.NextProtocol { + if tls.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 { + config.CurvePreferences = []string(*c.CurvePreferences) + } + config.EnableSessionResumption = c.EnableSessionResumption + config.DisableSystemRoot = c.DisableSystemRoot + config.MinVersion = c.MinVersion + config.MaxVersion = c.MaxVersion + config.CipherSuites = c.CipherSuites + config.Fingerprint = strings.ToLower(c.Fingerprint) + if config.Fingerprint != "unsafe" && tls.GetFingerprint(config.Fingerprint) == nil { + return nil, errors.New(`unknown "fingerprint": `, config.Fingerprint) + } + config.RejectUnknownSni = c.RejectUnknownSNI + config.MasterKeyLog = c.MasterKeyLog + + if c.AllowInsecure { + if time.Now().After(time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)) { + return nil, errors.PrintRemovedFeatureError(`"allowInsecure"`, `"pinnedPeerCertSha256"`) + } else { + errors.LogWarning(context.Background(), `"allowInsecure" will be removed automatically after 2026-06-01, please use "pinnedPeerCertSha256"(pcs) and "verifyPeerCertByName"(vcn) instead, PLEASE CONTACT YOUR SERVICE PROVIDER (AIRPORT)`) + config.AllowInsecure = true + } + } + if c.PinnedPeerCertSha256 != "" { + for v := range strings.SplitSeq(c.PinnedPeerCertSha256, ",") { + v = strings.TrimSpace(v) + if v == "" { + continue + } + // remove colons for OpenSSL format + hashValue, err := hex.DecodeString(strings.ReplaceAll(v, ":", "")) + if err != nil { + return nil, err + } + if len(hashValue) != 32 { + return nil, errors.New("incorrect pinnedPeerCertSha256 length: ", v) + } + config.PinnedPeerCertSha256 = append(config.PinnedPeerCertSha256, hashValue) + } + } + + if c.VerifyPeerCertInNames != nil { + return nil, errors.PrintRemovedFeatureError(`"verifyPeerCertInNames"`, `"verifyPeerCertByName"`) + } + if c.VerifyPeerCertByName != "" { + for v := range strings.SplitSeq(c.VerifyPeerCertByName, ",") { + v = strings.TrimSpace(v) + if v == "" { + continue + } + config.VerifyPeerCertByName = append(config.VerifyPeerCertByName, v) + } + } + + if c.ECHServerKeys != "" { + EchPrivateKey, err := base64.StdEncoding.DecodeString(c.ECHServerKeys) + if err != nil { + return nil, errors.New("invalid ECH Config", c.ECHServerKeys) + } + config.EchServerKeys = EchPrivateKey + } + switch c.ECHForceQuery { + case "none", "half", "full", "": + config.EchForceQuery = c.ECHForceQuery + default: + return nil, errors.New(`invalid "echForceQuery": `, c.ECHForceQuery) + } + config.EchForceQuery = c.ECHForceQuery + config.EchConfigList = c.ECHConfigList + if c.ECHSocketSettings != nil { + ss, err := c.ECHSocketSettings.Build() + if err != nil { + return nil, errors.New("Failed to build ech sockopt.").Base(err) + } + config.EchSocketSettings = ss + } + + return config, nil +} + +type LimitFallback struct { + AfterBytes uint64 + BytesPerSec uint64 + BurstBytesPerSec uint64 +} + +type REALITYConfig struct { + MasterKeyLog string `json:"masterKeyLog"` + Show bool `json:"show"` + Target json.RawMessage `json:"target"` + Dest json.RawMessage `json:"dest"` + Type string `json:"type"` + Xver uint64 `json:"xver"` + ServerNames []string `json:"serverNames"` + PrivateKey string `json:"privateKey"` + MinClientVer string `json:"minClientVer"` + MaxClientVer string `json:"maxClientVer"` + MaxTimeDiff uint64 `json:"maxTimeDiff"` + ShortIds []string `json:"shortIds"` + Mldsa65Seed string `json:"mldsa65Seed"` + + LimitFallbackUpload LimitFallback `json:"limitFallbackUpload"` + LimitFallbackDownload LimitFallback `json:"limitFallbackDownload"` + + Fingerprint string `json:"fingerprint"` + ServerName string `json:"serverName"` + Password string `json:"password"` + PublicKey string `json:"publicKey"` + ShortId string `json:"shortId"` + Mldsa65Verify string `json:"mldsa65Verify"` + SpiderX string `json:"spiderX"` +} + +func (c *REALITYConfig) Build() (proto.Message, error) { + config := new(reality.Config) + config.MasterKeyLog = c.MasterKeyLog + config.Show = c.Show + var err error + if c.Target != nil { + c.Dest = c.Target + } + if c.Dest != nil { + var i uint16 + var s string + if err = json.Unmarshal(c.Dest, &i); err == nil { + s = strconv.Itoa(int(i)) + } else { + _ = json.Unmarshal(c.Dest, &s) + } + if c.Type == "" && s != "" { + switch s[0] { + case '@', '/': + c.Type = "unix" + if s[0] == '@' && len(s) > 1 && s[1] == '@' && (runtime.GOOS == "linux" || runtime.GOOS == "android") { + fullAddr := make([]byte, len(syscall.RawSockaddrUnix{}.Path)) // may need padding to work with haproxy + copy(fullAddr, s[1:]) + s = string(fullAddr) + } + default: + if _, err = strconv.Atoi(s); err == nil { + s = "localhost:" + s + } + if _, _, err = net.SplitHostPort(s); err == nil { + c.Type = "tcp" + } + } + } + if c.Type == "" { + return nil, errors.New(`please fill in a valid value for "target"`) + } + if c.Xver > 2 { + return nil, errors.New(`invalid PROXY protocol version, "xver" only accepts 0, 1, 2`) + } + if len(c.ServerNames) == 0 { + return nil, errors.New(`empty "serverNames"`) + } + if c.PrivateKey == "" { + return nil, errors.New(`empty "privateKey"`) + } + if config.PrivateKey, err = base64.RawURLEncoding.DecodeString(c.PrivateKey); err != nil || len(config.PrivateKey) != 32 { + return nil, errors.New(`invalid "privateKey": `, c.PrivateKey) + } + if c.MinClientVer != "" { + config.MinClientVer = make([]byte, 3) + var u uint64 + for i, s := range strings.Split(c.MinClientVer, ".") { + if i == 3 { + return nil, errors.New(`invalid "minClientVer": `, c.MinClientVer) + } + if u, err = strconv.ParseUint(s, 10, 8); err != nil { + return nil, errors.New(`"minClientVer[`, i, `]" should be less than 256`) + } else { + config.MinClientVer[i] = byte(u) + } + } + } + if c.MaxClientVer != "" { + config.MaxClientVer = make([]byte, 3) + var u uint64 + for i, s := range strings.Split(c.MaxClientVer, ".") { + if i == 3 { + return nil, errors.New(`invalid "maxClientVer": `, c.MaxClientVer) + } + if u, err = strconv.ParseUint(s, 10, 8); err != nil { + return nil, errors.New(`"maxClientVer[`, i, `]" should be less than 256`) + } else { + config.MaxClientVer[i] = byte(u) + } + } + } + if len(c.ShortIds) == 0 { + return nil, errors.New(`empty "shortIds"`) + } + config.ShortIds = make([][]byte, len(c.ShortIds)) + for i, s := range c.ShortIds { + if len(s) > 16 { + return nil, errors.New(`too long "shortIds[`, i, `]": `, s) + } + config.ShortIds[i] = make([]byte, 8) + if _, err = hex.Decode(config.ShortIds[i], []byte(s)); err != nil { + return nil, errors.New(`invalid "shortIds[`, i, `]": `, s) + } + } + config.Dest = s + config.Type = c.Type + config.Xver = c.Xver + config.ServerNames = c.ServerNames + config.MaxTimeDiff = c.MaxTimeDiff + + if c.Mldsa65Seed != "" { + if c.Mldsa65Seed == c.PrivateKey { + return nil, errors.New(`"mldsa65Seed" and "privateKey" can not be the same value: `, c.Mldsa65Seed) + } + if config.Mldsa65Seed, err = base64.RawURLEncoding.DecodeString(c.Mldsa65Seed); err != nil || len(config.Mldsa65Seed) != 32 { + return nil, errors.New(`invalid "mldsa65Seed": `, c.Mldsa65Seed) + } + } + + for _, sn := range config.ServerNames { + if strings.Contains(sn, "apple") || strings.Contains(sn, "icloud") { + errors.LogWarning(context.Background(), `REALITY: Choosing apple, icloud, etc. as the target may get your IP blocked by the GFW`) + } + } + + config.LimitFallbackUpload = new(reality.LimitFallback) + config.LimitFallbackUpload.AfterBytes = c.LimitFallbackUpload.AfterBytes + config.LimitFallbackUpload.BytesPerSec = c.LimitFallbackUpload.BytesPerSec + config.LimitFallbackUpload.BurstBytesPerSec = c.LimitFallbackUpload.BurstBytesPerSec + config.LimitFallbackDownload = new(reality.LimitFallback) + config.LimitFallbackDownload.AfterBytes = c.LimitFallbackDownload.AfterBytes + config.LimitFallbackDownload.BytesPerSec = c.LimitFallbackDownload.BytesPerSec + config.LimitFallbackDownload.BurstBytesPerSec = c.LimitFallbackDownload.BurstBytesPerSec + } else { + config.Fingerprint = strings.ToLower(c.Fingerprint) + if config.Fingerprint == "unsafe" || config.Fingerprint == "hellogolang" { + return nil, errors.New(`invalid "fingerprint": `, config.Fingerprint) + } + if tls.GetFingerprint(config.Fingerprint) == nil { + return nil, errors.New(`unknown "fingerprint": `, config.Fingerprint) + } + if len(c.ServerNames) != 0 { + return nil, errors.New(`non-empty "serverNames", please use "serverName" instead`) + } + if c.Password != "" { + c.PublicKey = c.Password + } + if c.PublicKey == "" { + return nil, errors.New(`empty "password"`) + } + if config.PublicKey, err = base64.RawURLEncoding.DecodeString(c.PublicKey); err != nil || len(config.PublicKey) != 32 { + return nil, errors.New(`invalid "password": `, c.PublicKey) + } + if len(c.ShortIds) != 0 { + return nil, errors.New(`non-empty "shortIds", please use "shortId" instead`) + } + if len(c.ShortId) > 16 { + return nil, errors.New(`too long "shortId": `, c.ShortId) + } + config.ShortId = make([]byte, 8) + if _, err = hex.Decode(config.ShortId, []byte(c.ShortId)); err != nil { + return nil, errors.New(`invalid "shortId": `, c.ShortId) + } + if c.Mldsa65Verify != "" { + if config.Mldsa65Verify, err = base64.RawURLEncoding.DecodeString(c.Mldsa65Verify); err != nil || len(config.Mldsa65Verify) != 1952 { + return nil, errors.New(`invalid "mldsa65Verify": `, c.Mldsa65Verify) + } + } + if c.SpiderX == "" { + c.SpiderX = "/" + } + if c.SpiderX[0] != '/' { + return nil, errors.New(`invalid "spiderX": `, c.SpiderX) + } + config.SpiderY = make([]int64, 10) + u, _ := url.Parse(c.SpiderX) + q := u.Query() + parse := func(param string, index int) { + if q.Get(param) != "" { + s := strings.Split(q.Get(param), "-") + if len(s) == 1 { + config.SpiderY[index], _ = strconv.ParseInt(s[0], 10, 64) + config.SpiderY[index+1], _ = strconv.ParseInt(s[0], 10, 64) + } else { + config.SpiderY[index], _ = strconv.ParseInt(s[0], 10, 64) + config.SpiderY[index+1], _ = strconv.ParseInt(s[1], 10, 64) + } + } + q.Del(param) + } + parse("p", 0) // padding + parse("c", 2) // concurrency + parse("t", 4) // times + parse("i", 6) // interval + parse("r", 8) // return + u.RawQuery = q.Encode() + config.SpiderX = u.String() + config.ServerName = c.ServerName + } + return config, nil +} + +type TransportProtocol string + +// Build implements Buildable. +func (p TransportProtocol) Build() (string, error) { + switch strings.ToLower(string(p)) { + case "raw", "tcp": + return "tcp", nil + case "xhttp", "splithttp": + return "splithttp", nil + case "kcp", "mkcp": + return "mkcp", nil + case "grpc": + errors.PrintNonRemovalDeprecatedFeatureWarning("gRPC transport (with unnecessary costs, etc.)", "XHTTP stream-up H2") + return "grpc", nil + case "ws", "websocket": + errors.PrintNonRemovalDeprecatedFeatureWarning("WebSocket transport (with ALPN http/1.1, etc.)", "XHTTP H2 & H3") + return "websocket", nil + case "httpupgrade": + errors.PrintNonRemovalDeprecatedFeatureWarning("HTTPUpgrade transport (with ALPN http/1.1, etc.)", "XHTTP H2 & H3") + return "httpupgrade", nil + case "h2", "h3", "http": + return "", errors.PrintRemovedFeatureError("HTTP transport (without header padding, etc.)", "XHTTP stream-one H2 & H3") + case "quic": + return "", errors.PrintRemovedFeatureError("QUIC transport (without web service, etc.)", "XHTTP stream-one H3") + case "hysteria": + return "hysteria", nil + default: + return "", errors.New("Config: unknown transport protocol: ", p) + } +} + +type CustomSockoptConfig struct { + Syetem string `json:"system"` + Network string `json:"network"` + Level string `json:"level"` + Opt string `json:"opt"` + Value string `json:"value"` + Type string `json:"type"` +} + +type HappyEyeballsConfig struct { + PrioritizeIPv6 bool `json:"prioritizeIPv6"` + TryDelayMs uint64 `json:"tryDelayMs"` + Interleave uint32 `json:"interleave"` + MaxConcurrentTry uint32 `json:"maxConcurrentTry"` +} + +func (h *HappyEyeballsConfig) UnmarshalJSON(data []byte) error { + var innerHappyEyeballsConfig = struct { + PrioritizeIPv6 bool `json:"prioritizeIPv6"` + TryDelayMs uint64 `json:"tryDelayMs"` + Interleave uint32 `json:"interleave"` + MaxConcurrentTry uint32 `json:"maxConcurrentTry"` + }{PrioritizeIPv6: false, Interleave: 1, TryDelayMs: 0, MaxConcurrentTry: 4} + if err := json.Unmarshal(data, &innerHappyEyeballsConfig); err != nil { + return err + } + h.PrioritizeIPv6 = innerHappyEyeballsConfig.PrioritizeIPv6 + h.TryDelayMs = innerHappyEyeballsConfig.TryDelayMs + h.Interleave = innerHappyEyeballsConfig.Interleave + h.MaxConcurrentTry = innerHappyEyeballsConfig.MaxConcurrentTry + return nil +} + +type SocketConfig struct { + Mark int32 `json:"mark"` + TFO interface{} `json:"tcpFastOpen"` + TProxy string `json:"tproxy"` + AcceptProxyProtocol bool `json:"acceptProxyProtocol"` + DomainStrategy string `json:"domainStrategy"` + DialerProxy string `json:"dialerProxy"` + TCPKeepAliveInterval int32 `json:"tcpKeepAliveInterval"` + TCPKeepAliveIdle int32 `json:"tcpKeepAliveIdle"` + TCPCongestion string `json:"tcpCongestion"` + TCPWindowClamp int32 `json:"tcpWindowClamp"` + TCPMaxSeg int32 `json:"tcpMaxSeg"` + Penetrate bool `json:"penetrate"` + TCPUserTimeout int32 `json:"tcpUserTimeout"` + V6only bool `json:"v6only"` + Interface string `json:"interface"` + TcpMptcp bool `json:"tcpMptcp"` + CustomSockopt []*CustomSockoptConfig `json:"customSockopt"` + AddressPortStrategy string `json:"addressPortStrategy"` + HappyEyeballsSettings *HappyEyeballsConfig `json:"happyEyeballs"` + TrustedXForwardedFor []string `json:"trustedXForwardedFor"` +} + +// Build implements Buildable. +func (c *SocketConfig) Build() (*internet.SocketConfig, error) { + tfo := int32(0) // don't invoke setsockopt() for TFO + if c.TFO != nil { + switch v := c.TFO.(type) { + case bool: + if v { + tfo = 256 + } else { + tfo = -1 // TFO need to be disabled + } + case float64: + tfo = int32(math.Min(v, math.MaxInt32)) + default: + return nil, errors.New("tcpFastOpen: only boolean and integer value is acceptable") + } + } + var tproxy internet.SocketConfig_TProxyMode + switch strings.ToLower(c.TProxy) { + case "tproxy": + tproxy = internet.SocketConfig_TProxy + case "redirect": + tproxy = internet.SocketConfig_Redirect + default: + tproxy = internet.SocketConfig_Off + } + + dStrategy := internet.DomainStrategy_AS_IS + switch strings.ToLower(c.DomainStrategy) { + case "asis", "": + dStrategy = internet.DomainStrategy_AS_IS + case "useip": + dStrategy = internet.DomainStrategy_USE_IP + case "useipv4": + dStrategy = internet.DomainStrategy_USE_IP4 + case "useipv6": + dStrategy = internet.DomainStrategy_USE_IP6 + case "useipv4v6": + dStrategy = internet.DomainStrategy_USE_IP46 + case "useipv6v4": + dStrategy = internet.DomainStrategy_USE_IP64 + case "forceip": + dStrategy = internet.DomainStrategy_FORCE_IP + case "forceipv4": + dStrategy = internet.DomainStrategy_FORCE_IP4 + case "forceipv6": + dStrategy = internet.DomainStrategy_FORCE_IP6 + case "forceipv4v6": + dStrategy = internet.DomainStrategy_FORCE_IP46 + case "forceipv6v4": + dStrategy = internet.DomainStrategy_FORCE_IP64 + default: + return nil, errors.New("unsupported domain strategy: ", c.DomainStrategy) + } + + var customSockopts []*internet.CustomSockopt + + for _, copt := range c.CustomSockopt { + customSockopt := &internet.CustomSockopt{ + System: copt.Syetem, + Network: copt.Network, + Level: copt.Level, + Opt: copt.Opt, + Value: copt.Value, + Type: copt.Type, + } + 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) + } + + var happyEyeballs = &internet.HappyEyeballsConfig{Interleave: 1, PrioritizeIpv6: false, TryDelayMs: 0, MaxConcurrentTry: 4} + if c.HappyEyeballsSettings != nil { + happyEyeballs.PrioritizeIpv6 = c.HappyEyeballsSettings.PrioritizeIPv6 + happyEyeballs.Interleave = c.HappyEyeballsSettings.Interleave + happyEyeballs.TryDelayMs = c.HappyEyeballsSettings.TryDelayMs + happyEyeballs.MaxConcurrentTry = c.HappyEyeballsSettings.MaxConcurrentTry + } + + return &internet.SocketConfig{ + Mark: c.Mark, + Tfo: tfo, + Tproxy: tproxy, + DomainStrategy: dStrategy, + AcceptProxyProtocol: c.AcceptProxyProtocol, + DialerProxy: c.DialerProxy, + TcpKeepAliveInterval: c.TCPKeepAliveInterval, + TcpKeepAliveIdle: c.TCPKeepAliveIdle, + TcpCongestion: c.TCPCongestion, + TcpWindowClamp: c.TCPWindowClamp, + TcpMaxSeg: c.TCPMaxSeg, + Penetrate: c.Penetrate, + TcpUserTimeout: c.TCPUserTimeout, + V6Only: c.V6only, + Interface: c.Interface, + TcpMptcp: c.TcpMptcp, + CustomSockopt: customSockopts, + AddressPortStrategy: addressPortStrategy, + HappyEyeballs: happyEyeballs, + TrustedXForwardedFor: c.TrustedXForwardedFor, + }, nil +} + +func PraseByteSlice(data json.RawMessage, typ string) ([]byte, error) { + switch strings.ToLower(typ) { + case "", "array": + if len(data) == 0 { + return data, nil + } + var packet []byte + if err := json.Unmarshal(data, &packet); err != nil { + return nil, err + } + return packet, nil + case "str": + var str string + if err := json.Unmarshal(data, &str); err != nil { + return nil, err + } + return []byte(str), nil + case "hex": + var str string + if err := json.Unmarshal(data, &str); err != nil { + return nil, err + } + return hex.DecodeString(str) + case "base64": + var str string + if err := json.Unmarshal(data, &str); err != nil { + return nil, err + } + return base64.StdEncoding.DecodeString(str) + default: + return nil, errors.New("unknown type") + } +} + +var ( + tcpmaskLoader = NewJSONConfigLoader(ConfigCreatorCache{ + "header-custom": func() interface{} { return new(HeaderCustomTCP) }, + "fragment": func() interface{} { return new(FragmentMask) }, + "sudoku": func() interface{} { return new(Sudoku) }, + }, "type", "settings") + + udpmaskLoader = NewJSONConfigLoader(ConfigCreatorCache{ + "header-custom": func() interface{} { return new(HeaderCustomUDP) }, + "header-dns": func() interface{} { return new(Dns) }, + "header-dtls": func() interface{} { return new(Dtls) }, + "header-srtp": func() interface{} { return new(Srtp) }, + "header-utp": func() interface{} { return new(Utp) }, + "header-wechat": func() interface{} { return new(Wechat) }, + "header-wireguard": func() interface{} { return new(Wireguard) }, + "mkcp-original": func() interface{} { return new(Original) }, + "mkcp-aes128gcm": func() interface{} { return new(Aes128Gcm) }, + "noise": func() interface{} { return new(NoiseMask) }, + "salamander": func() interface{} { return new(Salamander) }, + "sudoku": func() interface{} { return new(Sudoku) }, + "xdns": func() interface{} { return new(Xdns) }, + "xicmp": func() interface{} { return new(Xicmp) }, + }, "type", "settings") +) + +type TCPItem struct { + Delay Int32Range `json:"delay"` + Rand int32 `json:"rand"` + RandRange *Int32Range `json:"randRange"` + Type string `json:"type"` + Packet json.RawMessage `json:"packet"` +} + +type HeaderCustomTCP struct { + Clients [][]TCPItem `json:"clients"` + Servers [][]TCPItem `json:"servers"` + Errors [][]TCPItem `json:"errors"` +} + +func (c *HeaderCustomTCP) Build() (proto.Message, error) { + for _, value := range c.Clients { + for _, item := range value { + if len(item.Packet) > 0 && item.Rand > 0 { + return nil, errors.New("len(item.Packet) > 0 && item.Rand > 0") + } + } + } + for _, value := range c.Servers { + for _, item := range value { + if len(item.Packet) > 0 && item.Rand > 0 { + return nil, errors.New("len(item.Packet) > 0 && item.Rand > 0") + } + } + } + for _, value := range c.Errors { + for _, item := range value { + if len(item.Packet) > 0 && item.Rand > 0 { + return nil, errors.New("len(item.Packet) > 0 && item.Rand > 0") + } + } + } + + errInvalidRange := errors.New("invalid randRange") + + clients := make([]*custom.TCPSequence, len(c.Clients)) + for i, value := range c.Clients { + clients[i] = &custom.TCPSequence{} + for _, item := range value { + if item.RandRange == nil { + item.RandRange = &Int32Range{From: 0, To: 255} + } + if item.RandRange.From < 0 || item.RandRange.To > 255 { + return nil, errInvalidRange + } + var err error + if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil { + return nil, err + } + clients[i].Sequence = append(clients[i].Sequence, &custom.TCPItem{ + DelayMin: int64(item.Delay.From), + DelayMax: int64(item.Delay.To), + Rand: item.Rand, + RandMin: item.RandRange.From, + RandMax: item.RandRange.To, + Packet: item.Packet, + }) + } + } + + servers := make([]*custom.TCPSequence, len(c.Servers)) + for i, value := range c.Servers { + servers[i] = &custom.TCPSequence{} + for _, item := range value { + if item.RandRange == nil { + item.RandRange = &Int32Range{From: 0, To: 255} + } + if item.RandRange.From < 0 || item.RandRange.To > 255 { + return nil, errInvalidRange + } + var err error + if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil { + return nil, err + } + servers[i].Sequence = append(servers[i].Sequence, &custom.TCPItem{ + DelayMin: int64(item.Delay.From), + DelayMax: int64(item.Delay.To), + Rand: item.Rand, + RandMin: item.RandRange.From, + RandMax: item.RandRange.To, + Packet: item.Packet, + }) + } + } + + errors := make([]*custom.TCPSequence, len(c.Errors)) + for i, value := range c.Errors { + errors[i] = &custom.TCPSequence{} + for _, item := range value { + if item.RandRange == nil { + item.RandRange = &Int32Range{From: 0, To: 255} + } + if item.RandRange.From < 0 || item.RandRange.To > 255 { + return nil, errInvalidRange + } + var err error + if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil { + return nil, err + } + errors[i].Sequence = append(errors[i].Sequence, &custom.TCPItem{ + DelayMin: int64(item.Delay.From), + DelayMax: int64(item.Delay.To), + Rand: item.Rand, + RandMin: item.RandRange.From, + RandMax: item.RandRange.To, + Packet: item.Packet, + }) + } + } + + return &custom.TCPConfig{ + Clients: clients, + Servers: servers, + Errors: errors, + }, nil +} + +type FragmentMask struct { + Packets string `json:"packets"` + Length Int32Range `json:"length"` + Delay Int32Range `json:"delay"` + MaxSplit Int32Range `json:"maxSplit"` +} + +func (c *FragmentMask) Build() (proto.Message, error) { + config := &fragment.Config{} + + switch strings.ToLower(c.Packets) { + case "tlshello": + config.PacketsFrom = 0 + config.PacketsTo = 1 + case "": + config.PacketsFrom = 0 + config.PacketsTo = 0 + default: + from, to, err := ParseRangeString(c.Packets) + if err != nil { + return nil, errors.New("Invalid PacketsFrom").Base(err) + } + config.PacketsFrom = int64(from) + config.PacketsTo = int64(to) + if config.PacketsFrom == 0 { + return nil, errors.New("PacketsFrom can't be 0") + } + } + + config.LengthMin = int64(c.Length.From) + config.LengthMax = int64(c.Length.To) + if config.LengthMin == 0 { + return nil, errors.New("LengthMin can't be 0") + } + + config.DelayMin = int64(c.Delay.From) + config.DelayMax = int64(c.Delay.To) + + config.MaxSplitMin = int64(c.MaxSplit.From) + config.MaxSplitMax = int64(c.MaxSplit.To) + + return config, nil +} + +type NoiseItem struct { + Rand Int32Range `json:"rand"` + RandRange *Int32Range `json:"randRange"` + Type string `json:"type"` + Packet json.RawMessage `json:"packet"` + Delay Int32Range `json:"delay"` +} + +type NoiseMask struct { + Reset Int32Range `json:"reset"` + Noise []NoiseItem `json:"noise"` +} + +func (c *NoiseMask) Build() (proto.Message, error) { + for _, item := range c.Noise { + if len(item.Packet) > 0 && item.Rand.To > 0 { + return nil, errors.New("len(item.Packet) > 0 && item.Rand.To > 0") + } + } + + noiseSlice := make([]*noise.Item, 0, len(c.Noise)) + for _, item := range c.Noise { + if item.RandRange == nil { + item.RandRange = &Int32Range{From: 0, To: 255} + } + if item.RandRange.From < 0 || item.RandRange.To > 255 { + return nil, errors.New("invalid randRange") + } + var err error + if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil { + return nil, err + } + noiseSlice = append(noiseSlice, &noise.Item{ + RandMin: int64(item.Rand.From), + RandMax: int64(item.Rand.To), + RandRangeMin: item.RandRange.From, + RandRangeMax: item.RandRange.To, + Packet: item.Packet, + DelayMin: int64(item.Delay.From), + DelayMax: int64(item.Delay.To), + }) + } + + return &noise.Config{ + ResetMin: int64(c.Reset.From), + ResetMax: int64(c.Reset.To), + Items: noiseSlice, + }, nil +} + +type UDPItem struct { + Rand int32 `json:"rand"` + RandRange *Int32Range `json:"randRange"` + Type string `json:"type"` + Packet json.RawMessage `json:"packet"` +} + +type HeaderCustomUDP struct { + Client []UDPItem `json:"client"` + Server []UDPItem `json:"server"` +} + +func (c *HeaderCustomUDP) Build() (proto.Message, error) { + for _, item := range c.Client { + if len(item.Packet) > 0 && item.Rand > 0 { + return nil, errors.New("len(item.Packet) > 0 && item.Rand > 0") + } + } + for _, item := range c.Server { + if len(item.Packet) > 0 && item.Rand > 0 { + return nil, errors.New("len(item.Packet) > 0 && item.Rand > 0") + } + } + + client := make([]*custom.UDPItem, 0, len(c.Client)) + for _, item := range c.Client { + if item.RandRange == nil { + item.RandRange = &Int32Range{From: 0, To: 255} + } + if item.RandRange.From < 0 || item.RandRange.To > 255 { + return nil, errors.New("invalid randRange") + } + var err error + if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil { + return nil, err + } + client = append(client, &custom.UDPItem{ + Rand: item.Rand, + RandMin: item.RandRange.From, + RandMax: item.RandRange.To, + Packet: item.Packet, + }) + } + + server := make([]*custom.UDPItem, 0, len(c.Server)) + for _, item := range c.Server { + if item.RandRange == nil { + item.RandRange = &Int32Range{From: 0, To: 255} + } + if item.RandRange.From < 0 || item.RandRange.To > 255 { + return nil, errors.New("invalid randRange") + } + var err error + if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil { + return nil, err + } + server = append(server, &custom.UDPItem{ + Rand: item.Rand, + RandMin: item.RandRange.From, + RandMax: item.RandRange.To, + Packet: item.Packet, + }) + } + + return &custom.UDPConfig{ + Client: client, + Server: server, + }, nil +} + +type Dns struct { + Domain string `json:"domain"` +} + +func (c *Dns) Build() (proto.Message, error) { + config := &dns.Config{} + config.Domain = "www.baidu.com" + + if len(c.Domain) > 0 { + config.Domain = c.Domain + } + + return config, nil +} + +type Dtls struct{} + +func (c *Dtls) Build() (proto.Message, error) { + return &dtls.Config{}, nil +} + +type Srtp struct{} + +func (c *Srtp) Build() (proto.Message, error) { + return &srtp.Config{}, nil +} + +type Utp struct{} + +func (c *Utp) Build() (proto.Message, error) { + return &utp.Config{}, nil +} + +type Wechat struct{} + +func (c *Wechat) Build() (proto.Message, error) { + return &wechat.Config{}, nil +} + +type Wireguard struct{} + +func (c *Wireguard) Build() (proto.Message, error) { + return &wireguard.Config{}, nil +} + +type Original struct{} + +func (c *Original) Build() (proto.Message, error) { + return &original.Config{}, nil +} + +type Aes128Gcm struct { + Password string `json:"password"` +} + +func (c *Aes128Gcm) Build() (proto.Message, error) { + return &aes128gcm.Config{ + Password: c.Password, + }, nil +} + +type Salamander struct { + Password string `json:"password"` +} + +func (c *Salamander) Build() (proto.Message, error) { + config := &salamander.Config{} + config.Password = c.Password + return config, nil +} + +type Sudoku struct { + Password string `json:"password"` + ASCII string `json:"ascii"` + + CustomTable string `json:"customTable"` + LegacyCustomTable string `json:"custom_table"` + CustomTables []string `json:"customTables"` + LegacyCustomSets []string `json:"custom_tables"` + + PaddingMin uint32 `json:"paddingMin"` + LegacyPaddingMin uint32 `json:"padding_min"` + PaddingMax uint32 `json:"paddingMax"` + LegacyPaddingMax uint32 `json:"padding_max"` +} + +func (c *Sudoku) Build() (proto.Message, error) { + customTable := c.CustomTable + if customTable == "" { + customTable = c.LegacyCustomTable + } + customTables := c.CustomTables + if len(customTables) == 0 { + customTables = c.LegacyCustomSets + } + + paddingMin := c.PaddingMin + if paddingMin == 0 { + paddingMin = c.LegacyPaddingMin + } + paddingMax := c.PaddingMax + if paddingMax == 0 { + paddingMax = c.LegacyPaddingMax + } + + return &finalsudoku.Config{ + Password: c.Password, + Ascii: c.ASCII, + CustomTable: customTable, + CustomTables: customTables, + PaddingMin: paddingMin, + PaddingMax: paddingMax, + }, nil +} + +type Xdns struct { + Domain string `json:"domain"` +} + +func (c *Xdns) Build() (proto.Message, error) { + if c.Domain == "" { + return nil, errors.New("empty domain") + } + + return &xdns.Config{ + Domain: c.Domain, + }, nil +} + +type Xicmp struct { + ListenIp string `json:"listenIp"` + Id uint16 `json:"id"` +} + +func (c *Xicmp) Build() (proto.Message, error) { + config := &xicmp.Config{ + Ip: c.ListenIp, + Id: int32(c.Id), + } + + if config.Ip == "" { + config.Ip = "0.0.0.0" + } + + return config, nil +} + +type Mask struct { + Type string `json:"type"` + Settings *json.RawMessage `json:"settings"` +} + +func (c *Mask) Build(tcp bool) (proto.Message, error) { + loader := udpmaskLoader + if tcp { + loader = tcpmaskLoader + } + + settings := []byte("{}") + if c.Settings != nil { + settings = ([]byte)(*c.Settings) + } + rawConfig, err := loader.LoadWithID(settings, c.Type) + if err != nil { + return nil, err + } + ts, err := rawConfig.(Buildable).Build() + if err != nil { + return nil, err + } + return ts, nil +} + +type FinalMask struct { + Tcp []Mask `json:"tcp"` + Udp []Mask `json:"udp"` + QuicParams *QuicParamsConfig `json:"quicParams"` +} + +type StreamConfig struct { + Address *Address `json:"address"` + Port uint16 `json:"port"` + Network *TransportProtocol `json:"network"` + Security string `json:"security"` + FinalMask *FinalMask `json:"finalmask"` + TLSSettings *TLSConfig `json:"tlsSettings"` + REALITYSettings *REALITYConfig `json:"realitySettings"` + RAWSettings *TCPConfig `json:"rawSettings"` + TCPSettings *TCPConfig `json:"tcpSettings"` + XHTTPSettings *SplitHTTPConfig `json:"xhttpSettings"` + SplitHTTPSettings *SplitHTTPConfig `json:"splithttpSettings"` + KCPSettings *KCPConfig `json:"kcpSettings"` + GRPCSettings *GRPCConfig `json:"grpcSettings"` + WSSettings *WebSocketConfig `json:"wsSettings"` + HTTPUPGRADESettings *HttpUpgradeConfig `json:"httpupgradeSettings"` + HysteriaSettings *HysteriaConfig `json:"hysteriaSettings"` + SocketSettings *SocketConfig `json:"sockopt"` +} + +// Build implements Buildable. +func (c *StreamConfig) Build() (*internet.StreamConfig, error) { + config := &internet.StreamConfig{ + Port: uint32(c.Port), + ProtocolName: "tcp", + } + if c.Address != nil { + config.Address = c.Address.Build() + } + if c.Network != nil { + protocol, err := c.Network.Build() + if err != nil { + return nil, err + } + config.ProtocolName = protocol + } + + switch strings.ToLower(c.Security) { + case "", "none": + case "tls": + tlsSettings := c.TLSSettings + if tlsSettings == nil { + tlsSettings = &TLSConfig{} + } + ts, err := tlsSettings.Build() + if err != nil { + return nil, errors.New("Failed to build TLS config.").Base(err) + } + tm := serial.ToTypedMessage(ts) + config.SecuritySettings = append(config.SecuritySettings, tm) + config.SecurityType = tm.Type + case "reality": + if config.ProtocolName != "tcp" && config.ProtocolName != "splithttp" && config.ProtocolName != "grpc" { + return nil, errors.New("REALITY only supports RAW, XHTTP and gRPC for now.") + } + if c.REALITYSettings == nil { + return nil, errors.New(`REALITY: Empty "realitySettings".`) + } + ts, err := c.REALITYSettings.Build() + if err != nil { + return nil, errors.New("Failed to build REALITY config.").Base(err) + } + tm := serial.ToTypedMessage(ts) + config.SecuritySettings = append(config.SecuritySettings, tm) + config.SecurityType = tm.Type + case "xtls": + return nil, errors.PrintRemovedFeatureError(`Legacy XTLS`, `xtls-rprx-vision with TLS or REALITY`) + default: + return nil, errors.New(`Unknown security "` + c.Security + `".`) + } + + if c.RAWSettings != nil { + c.TCPSettings = c.RAWSettings + } + if c.TCPSettings != nil { + ts, err := c.TCPSettings.Build() + if err != nil { + return nil, errors.New("Failed to build RAW config.").Base(err) + } + config.TransportSettings = append(config.TransportSettings, &internet.TransportConfig{ + ProtocolName: "tcp", + Settings: serial.ToTypedMessage(ts), + }) + } + if c.XHTTPSettings != nil { + c.SplitHTTPSettings = c.XHTTPSettings + } + if c.SplitHTTPSettings != nil { + hs, err := c.SplitHTTPSettings.Build() + if err != nil { + return nil, errors.New("Failed to build XHTTP config.").Base(err) + } + config.TransportSettings = append(config.TransportSettings, &internet.TransportConfig{ + ProtocolName: "splithttp", + Settings: serial.ToTypedMessage(hs), + }) + } + if c.KCPSettings != nil { + ts, err := c.KCPSettings.Build() + if err != nil { + return nil, errors.New("Failed to build mKCP config.").Base(err) + } + config.TransportSettings = append(config.TransportSettings, &internet.TransportConfig{ + ProtocolName: "mkcp", + Settings: serial.ToTypedMessage(ts), + }) + } + if c.GRPCSettings != nil { + gs, err := c.GRPCSettings.Build() + if err != nil { + return nil, errors.New("Failed to build gRPC config.").Base(err) + } + config.TransportSettings = append(config.TransportSettings, &internet.TransportConfig{ + ProtocolName: "grpc", + Settings: serial.ToTypedMessage(gs), + }) + } + if c.WSSettings != nil { + ts, err := c.WSSettings.Build() + if err != nil { + return nil, errors.New("Failed to build WebSocket config.").Base(err) + } + config.TransportSettings = append(config.TransportSettings, &internet.TransportConfig{ + ProtocolName: "websocket", + Settings: serial.ToTypedMessage(ts), + }) + } + if c.HTTPUPGRADESettings != nil { + hs, err := c.HTTPUPGRADESettings.Build() + if err != nil { + return nil, errors.New("Failed to build HTTPUpgrade config.").Base(err) + } + config.TransportSettings = append(config.TransportSettings, &internet.TransportConfig{ + ProtocolName: "httpupgrade", + Settings: serial.ToTypedMessage(hs), + }) + } + if c.HysteriaSettings != nil { + hs, err := c.HysteriaSettings.Build() + if err != nil { + return nil, errors.New("Failed to build Hysteria config.").Base(err) + } + config.TransportSettings = append(config.TransportSettings, &internet.TransportConfig{ + ProtocolName: "hysteria", + Settings: serial.ToTypedMessage(hs), + }) + } + if c.SocketSettings != nil { + ss, err := c.SocketSettings.Build() + if err != nil { + return nil, errors.New("Failed to build sockopt.").Base(err) + } + config.SocketSettings = ss + } + + if c.FinalMask != nil { + for _, mask := range c.FinalMask.Tcp { + u, err := mask.Build(true) + if err != nil { + return nil, errors.New("failed to build mask with type ", mask.Type).Base(err) + } + config.Tcpmasks = append(config.Tcpmasks, serial.ToTypedMessage(u)) + } + for _, mask := range c.FinalMask.Udp { + u, err := mask.Build(false) + if err != nil { + return nil, errors.New("failed to build mask with type ", mask.Type).Base(err) + } + config.Udpmasks = append(config.Udpmasks, serial.ToTypedMessage(u)) + } + if c.FinalMask.QuicParams != nil { + profile := strings.ToLower(c.FinalMask.QuicParams.BbrProfile) + switch profile { + case "", string(bbr.ProfileConservative), string(bbr.ProfileStandard), string(bbr.ProfileAggressive): + if profile == "" { + profile = string(bbr.ProfileStandard) + } + default: + return nil, errors.New("unknown bbr profile") + } + + up, err := c.FinalMask.QuicParams.BrutalUp.Bps() + if err != nil { + return nil, err + } + down, err := c.FinalMask.QuicParams.BrutalDown.Bps() + if err != nil { + return nil, err + } + + if up > 0 && up < 65536 { + return nil, errors.New("BrutalUp must be at least 65536 bytes per second") + } + if down > 0 && down < 65536 { + return nil, errors.New("BrutalDown must be at least 65536 bytes per second") + } + + c.FinalMask.QuicParams.Congestion = strings.ToLower(c.FinalMask.QuicParams.Congestion) + switch c.FinalMask.QuicParams.Congestion { + case "", "brutal", "reno", "bbr": + case "force-brutal": + if up == 0 { + return nil, errors.New("force-brutal requires up") + } + default: + return nil, errors.New("unknown congestion control: ", c.FinalMask.QuicParams.Congestion, ", valid values: reno, bbr, brutal, force-brutal") + } + + var hop *PortList + if err := json.Unmarshal(c.FinalMask.QuicParams.UdpHop.PortList, &hop); err != nil { + hop = &PortList{} + } + + var inertvalMin, inertvalMax int64 + if c.FinalMask.QuicParams.UdpHop.Interval != nil { + inertvalMin = int64(c.FinalMask.QuicParams.UdpHop.Interval.From) + inertvalMax = int64(c.FinalMask.QuicParams.UdpHop.Interval.To) + } + + if (inertvalMin != 0 && inertvalMin < 5) || (inertvalMax != 0 && inertvalMax < 5) { + return nil, errors.New("Interval must be at least 5") + } + + if c.FinalMask.QuicParams.InitStreamReceiveWindow > 0 && c.FinalMask.QuicParams.InitStreamReceiveWindow < 16384 { + return nil, errors.New("InitStreamReceiveWindow must be at least 16384") + } + if c.FinalMask.QuicParams.MaxStreamReceiveWindow > 0 && c.FinalMask.QuicParams.MaxStreamReceiveWindow < 16384 { + return nil, errors.New("MaxStreamReceiveWindow must be at least 16384") + } + if c.FinalMask.QuicParams.InitConnectionReceiveWindow > 0 && c.FinalMask.QuicParams.InitConnectionReceiveWindow < 16384 { + return nil, errors.New("InitConnectionReceiveWindow must be at least 16384") + } + if c.FinalMask.QuicParams.MaxConnectionReceiveWindow > 0 && c.FinalMask.QuicParams.MaxConnectionReceiveWindow < 16384 { + return nil, errors.New("MaxConnectionReceiveWindow must be at least 16384") + } + if c.FinalMask.QuicParams.MaxIdleTimeout != 0 && (c.FinalMask.QuicParams.MaxIdleTimeout < 4 || c.FinalMask.QuicParams.MaxIdleTimeout > 120) { + return nil, errors.New("MaxIdleTimeout must be between 4 and 120") + } + if c.FinalMask.QuicParams.KeepAlivePeriod != 0 && (c.FinalMask.QuicParams.KeepAlivePeriod < 2 || c.FinalMask.QuicParams.KeepAlivePeriod > 60) { + return nil, errors.New("KeepAlivePeriod must be between 2 and 60") + } + if c.FinalMask.QuicParams.MaxIncomingStreams != 0 && c.FinalMask.QuicParams.MaxIncomingStreams < 8 { + return nil, errors.New("MaxIncomingStreams must be at least 8") + } + + if c.FinalMask.QuicParams.Debug { + os.Setenv("HYSTERIA_BBR_DEBUG", "true") + os.Setenv("HYSTERIA_BRUTAL_DEBUG", "true") + } + + config.QuicParams = &internet.QuicParams{ + Congestion: c.FinalMask.QuicParams.Congestion, + BbrProfile: profile, + BrutalUp: up, + BrutalDown: down, + UdpHop: &internet.UdpHop{ + Ports: hop.Build().Ports(), + IntervalMin: inertvalMin, + IntervalMax: inertvalMax, + }, + InitStreamReceiveWindow: c.FinalMask.QuicParams.InitStreamReceiveWindow, + MaxStreamReceiveWindow: c.FinalMask.QuicParams.MaxStreamReceiveWindow, + InitConnReceiveWindow: c.FinalMask.QuicParams.InitConnectionReceiveWindow, + MaxConnReceiveWindow: c.FinalMask.QuicParams.MaxConnectionReceiveWindow, + MaxIdleTimeout: c.FinalMask.QuicParams.MaxIdleTimeout, + KeepAlivePeriod: c.FinalMask.QuicParams.KeepAlivePeriod, + DisablePathMtuDiscovery: c.FinalMask.QuicParams.DisablePathMTUDiscovery, + MaxIncomingStreams: c.FinalMask.QuicParams.MaxIncomingStreams, + } + } + } + + return config, nil +} + +type ProxyConfig struct { + Tag string `json:"tag"` + + // TransportLayerProxy: For compatibility. + TransportLayerProxy bool `json:"transportLayer"` +} + +// Build implements Buildable. +func (v *ProxyConfig) Build() (*internet.ProxyConfig, error) { + if v.Tag == "" { + return nil, errors.New("Proxy tag is not set.") + } + return &internet.ProxyConfig{ + Tag: v.Tag, + TransportLayerProxy: v.TransportLayerProxy, + }, nil +} diff --git a/subproject/Xray-core-main/infra/conf/transport_test.go b/subproject/Xray-core-main/infra/conf/transport_test.go new file mode 100644 index 00000000..87e5f920 --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/transport_test.go @@ -0,0 +1,158 @@ +package conf_test + +import ( + "encoding/json" + "testing" + + . "github.com/xtls/xray-core/infra/conf" + "github.com/xtls/xray-core/transport/internet" + "google.golang.org/protobuf/proto" +) + +func TestSocketConfig(t *testing.T) { + createParser := func() func(string) (proto.Message, error) { + return func(s string) (proto.Message, error) { + config := new(SocketConfig) + if err := json.Unmarshal([]byte(s), config); err != nil { + return nil, err + } + return config.Build() + } + } + + // test "tcpFastOpen": true, queue length 256 is expected. other parameters are tested here too + expectedOutput := &internet.SocketConfig{ + Mark: 1, + Tfo: 256, + DomainStrategy: internet.DomainStrategy_USE_IP, + DialerProxy: "tag", + HappyEyeballs: &internet.HappyEyeballsConfig{Interleave: 1, TryDelayMs: 0, PrioritizeIpv6: false, MaxConcurrentTry: 4}, + } + runMultiTestCase(t, []TestCase{ + { + Input: `{ + "mark": 1, + "tcpFastOpen": true, + "domainStrategy": "UseIP", + "dialerProxy": "tag" + }`, + Parser: createParser(), + Output: expectedOutput, + }, + }) + if expectedOutput.ParseTFOValue() != 256 { + t.Fatalf("unexpected parsed TFO value, which should be 256") + } + + // test "tcpFastOpen": false, disabled TFO is expected + expectedOutput = &internet.SocketConfig{ + Mark: 0, + Tfo: -1, + HappyEyeballs: &internet.HappyEyeballsConfig{Interleave: 1, TryDelayMs: 0, PrioritizeIpv6: false, MaxConcurrentTry: 4}, + } + runMultiTestCase(t, []TestCase{ + { + Input: `{ + "tcpFastOpen": false + }`, + Parser: createParser(), + Output: expectedOutput, + }, + }) + if expectedOutput.ParseTFOValue() != 0 { + t.Fatalf("unexpected parsed TFO value, which should be 0") + } + + // test "tcpFastOpen": 65535, queue length 65535 is expected + expectedOutput = &internet.SocketConfig{ + Mark: 0, + Tfo: 65535, + HappyEyeballs: &internet.HappyEyeballsConfig{Interleave: 1, TryDelayMs: 0, PrioritizeIpv6: false, MaxConcurrentTry: 4}, + } + runMultiTestCase(t, []TestCase{ + { + Input: `{ + "tcpFastOpen": 65535 + }`, + Parser: createParser(), + Output: expectedOutput, + }, + }) + if expectedOutput.ParseTFOValue() != 65535 { + t.Fatalf("unexpected parsed TFO value, which should be 65535") + } + + // test "tcpFastOpen": -65535, disable TFO is expected + expectedOutput = &internet.SocketConfig{ + Mark: 0, + Tfo: -65535, + HappyEyeballs: &internet.HappyEyeballsConfig{Interleave: 1, TryDelayMs: 0, PrioritizeIpv6: false, MaxConcurrentTry: 4}, + } + runMultiTestCase(t, []TestCase{ + { + Input: `{ + "tcpFastOpen": -65535 + }`, + Parser: createParser(), + Output: expectedOutput, + }, + }) + if expectedOutput.ParseTFOValue() != 0 { + t.Fatalf("unexpected parsed TFO value, which should be 0") + } + + // test "tcpFastOpen": 0, no operation is expected + expectedOutput = &internet.SocketConfig{ + Mark: 0, + Tfo: 0, + HappyEyeballs: &internet.HappyEyeballsConfig{Interleave: 1, TryDelayMs: 0, PrioritizeIpv6: false, MaxConcurrentTry: 4}, + } + runMultiTestCase(t, []TestCase{ + { + Input: `{ + "tcpFastOpen": 0 + }`, + Parser: createParser(), + Output: expectedOutput, + }, + }) + if expectedOutput.ParseTFOValue() != -1 { + t.Fatalf("unexpected parsed TFO value, which should be -1") + } + + // test omit "tcpFastOpen", no operation is expected + expectedOutput = &internet.SocketConfig{ + Mark: 0, + Tfo: 0, + HappyEyeballs: &internet.HappyEyeballsConfig{Interleave: 1, TryDelayMs: 0, PrioritizeIpv6: false, MaxConcurrentTry: 4}, + } + runMultiTestCase(t, []TestCase{ + { + Input: `{}`, + Parser: createParser(), + Output: expectedOutput, + }, + }) + if expectedOutput.ParseTFOValue() != -1 { + t.Fatalf("unexpected parsed TFO value, which should be -1") + } + + // test "tcpFastOpen": null, no operation is expected + expectedOutput = &internet.SocketConfig{ + Mark: 0, + Tfo: 0, + HappyEyeballs: &internet.HappyEyeballsConfig{Interleave: 1, TryDelayMs: 0, PrioritizeIpv6: false, MaxConcurrentTry: 4}, + } + runMultiTestCase(t, []TestCase{ + { + Input: `{ + "tcpFastOpen": null + }`, + Parser: createParser(), + Output: expectedOutput, + }, + }) + if expectedOutput.ParseTFOValue() != -1 { + t.Fatalf("unexpected parsed TFO value, which should be -1") + } +} diff --git a/subproject/Xray-core-main/infra/conf/trojan.go b/subproject/Xray-core-main/infra/conf/trojan.go new file mode 100644 index 00000000..b78b6ffc --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/trojan.go @@ -0,0 +1,194 @@ +package conf + +import ( + "encoding/json" + "path/filepath" + "runtime" + "strconv" + "strings" + "syscall" + + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/serial" + "github.com/xtls/xray-core/proxy/trojan" + "google.golang.org/protobuf/proto" +) + +// TrojanServerTarget is configuration of a single trojan server +type TrojanServerTarget struct { + Address *Address `json:"address"` + Port uint16 `json:"port"` + Level byte `json:"level"` + Email string `json:"email"` + Password string `json:"password"` + Flow string `json:"flow"` +} + +// TrojanClientConfig is configuration of trojan servers +type TrojanClientConfig struct { + Address *Address `json:"address"` + Port uint16 `json:"port"` + Level byte `json:"level"` + Email string `json:"email"` + Password string `json:"password"` + Flow string `json:"flow"` + Servers []*TrojanServerTarget `json:"servers"` +} + +// Build implements Buildable +func (c *TrojanClientConfig) Build() (proto.Message, error) { + errors.PrintNonRemovalDeprecatedFeatureWarning("Trojan (with no Flow, etc.)", "VLESS with Flow & Seed") + + if c.Address != nil { + c.Servers = []*TrojanServerTarget{ + { + Address: c.Address, + Port: c.Port, + Level: c.Level, + Email: c.Email, + Password: c.Password, + Flow: c.Flow, + }, + } + } + if len(c.Servers) != 1 { + return nil, errors.New(`Trojan settings: "servers" should have one and only one member. Multiple endpoints in "servers" should use multiple Trojan outbounds and routing balancer instead`) + } + + config := &trojan.ClientConfig{} + + for _, rec := range c.Servers { + if rec.Address == nil { + return nil, errors.New("Trojan server address is not set.") + } + if rec.Port == 0 { + return nil, errors.New("Invalid Trojan port.") + } + if rec.Password == "" { + return nil, errors.New("Trojan password is not specified.") + } + if rec.Flow != "" { + return nil, errors.PrintRemovedFeatureError(`Flow for Trojan`, ``) + } + + config.Server = &protocol.ServerEndpoint{ + Address: rec.Address.Build(), + Port: uint32(rec.Port), + User: &protocol.User{ + Level: uint32(rec.Level), + Email: rec.Email, + Account: serial.ToTypedMessage(&trojan.Account{ + Password: rec.Password, + }), + }, + } + + break + } + + return config, nil +} + +// TrojanInboundFallback is fallback configuration +type TrojanInboundFallback struct { + Name string `json:"name"` + Alpn string `json:"alpn"` + Path string `json:"path"` + Type string `json:"type"` + Dest json.RawMessage `json:"dest"` + Xver uint64 `json:"xver"` +} + +// TrojanUserConfig is user configuration +type TrojanUserConfig struct { + Password string `json:"password"` + Level byte `json:"level"` + Email string `json:"email"` + Flow string `json:"flow"` +} + +// TrojanServerConfig is Inbound configuration +type TrojanServerConfig struct { + Clients []*TrojanUserConfig `json:"clients"` + Fallbacks []*TrojanInboundFallback `json:"fallbacks"` +} + +// Build implements Buildable +func (c *TrojanServerConfig) Build() (proto.Message, error) { + errors.PrintNonRemovalDeprecatedFeatureWarning("Trojan (with no Flow, etc.)", "VLESS with Flow & Seed") + + config := &trojan.ServerConfig{ + Users: make([]*protocol.User, len(c.Clients)), + } + + for idx, rawUser := range c.Clients { + if rawUser.Flow != "" { + return nil, errors.PrintRemovedFeatureError(`Flow for Trojan`, ``) + } + + config.Users[idx] = &protocol.User{ + Level: uint32(rawUser.Level), + Email: rawUser.Email, + Account: serial.ToTypedMessage(&trojan.Account{ + Password: rawUser.Password, + }), + } + } + + for _, fb := range c.Fallbacks { + var i uint16 + var s string + if err := json.Unmarshal(fb.Dest, &i); err == nil { + s = strconv.Itoa(int(i)) + } else { + _ = json.Unmarshal(fb.Dest, &s) + } + config.Fallbacks = append(config.Fallbacks, &trojan.Fallback{ + Name: fb.Name, + Alpn: fb.Alpn, + Path: fb.Path, + Type: fb.Type, + Dest: s, + Xver: fb.Xver, + }) + } + for _, fb := range config.Fallbacks { + /* + if fb.Alpn == "h2" && fb.Path != "" { + return nil, errors.New(`Trojan fallbacks: "alpn":"h2" doesn't support "path"`) + } + */ + if fb.Path != "" && fb.Path[0] != '/' { + return nil, errors.New(`Trojan fallbacks: "path" must be empty or start with "/"`) + } + if fb.Type == "" && fb.Dest != "" { + if fb.Dest == "serve-ws-none" { + fb.Type = "serve" + } else if filepath.IsAbs(fb.Dest) || fb.Dest[0] == '@' { + fb.Type = "unix" + if strings.HasPrefix(fb.Dest, "@@") && (runtime.GOOS == "linux" || runtime.GOOS == "android") { + fullAddr := make([]byte, len(syscall.RawSockaddrUnix{}.Path)) // may need padding to work with haproxy + copy(fullAddr, fb.Dest[1:]) + fb.Dest = string(fullAddr) + } + } else { + if _, err := strconv.Atoi(fb.Dest); err == nil { + fb.Dest = "localhost:" + fb.Dest + } + if _, _, err := net.SplitHostPort(fb.Dest); err == nil { + fb.Type = "tcp" + } + } + } + if fb.Type == "" { + return nil, errors.New(`Trojan fallbacks: please fill in a valid value for every "dest"`) + } + if fb.Xver > 2 { + return nil, errors.New(`Trojan fallbacks: invalid PROXY protocol version, "xver" only accepts 0, 1, 2`) + } + } + + return config, nil +} diff --git a/subproject/Xray-core-main/infra/conf/tun.go b/subproject/Xray-core-main/infra/conf/tun.go new file mode 100644 index 00000000..2d95e3cd --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/tun.go @@ -0,0 +1,30 @@ +package conf + +import ( + "github.com/xtls/xray-core/proxy/tun" + "google.golang.org/protobuf/proto" +) + +type TunConfig struct { + Name string `json:"name"` + MTU uint32 `json:"MTU"` + UserLevel uint32 `json:"userLevel"` +} + +func (v *TunConfig) Build() (proto.Message, error) { + config := &tun.Config{ + Name: v.Name, + MTU: v.MTU, + UserLevel: v.UserLevel, + } + + if v.Name == "" { + config.Name = "xray0" + } + + if v.MTU == 0 { + config.MTU = 1500 + } + + return config, nil +} diff --git a/subproject/Xray-core-main/infra/conf/version.go b/subproject/Xray-core-main/infra/conf/version.go new file mode 100644 index 00000000..5fedeb08 --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/version.go @@ -0,0 +1,22 @@ +package conf + +import ( + "github.com/xtls/xray-core/app/version" + "github.com/xtls/xray-core/core" + "strconv" +) + +type VersionConfig struct { + MinVersion string `json:"min"` + MaxVersion string `json:"max"` +} + +func (c *VersionConfig) Build() (*version.Config, error) { + coreVersion := strconv.Itoa(int(core.Version_x)) + "." + strconv.Itoa(int(core.Version_y)) + "." + strconv.Itoa(int(core.Version_z)) + + return &version.Config{ + CoreVersion: coreVersion, + MinVersion: c.MinVersion, + MaxVersion: c.MaxVersion, + }, nil +} diff --git a/subproject/Xray-core-main/infra/conf/vless.go b/subproject/Xray-core-main/infra/conf/vless.go new file mode 100644 index 00000000..1e14882c --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/vless.go @@ -0,0 +1,374 @@ +package conf + +import ( + "encoding/base64" + "encoding/json" + "path/filepath" + "runtime" + "strconv" + "strings" + "syscall" + + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/serial" + "github.com/xtls/xray-core/common/uuid" + "github.com/xtls/xray-core/proxy/vless" + "github.com/xtls/xray-core/proxy/vless/inbound" + "github.com/xtls/xray-core/proxy/vless/outbound" + "google.golang.org/protobuf/proto" +) + +type VLessInboundFallback struct { + Name string `json:"name"` + Alpn string `json:"alpn"` + Path string `json:"path"` + Type string `json:"type"` + Dest json.RawMessage `json:"dest"` + Xver uint64 `json:"xver"` +} + +type VLessInboundConfig struct { + Clients []json.RawMessage `json:"clients"` + Decryption string `json:"decryption"` + Fallbacks []*VLessInboundFallback `json:"fallbacks"` + Flow string `json:"flow"` + Testseed []uint32 `json:"testseed"` +} + +// Build implements Buildable +func (c *VLessInboundConfig) Build() (proto.Message, error) { + config := new(inbound.Config) + config.Clients = make([]*protocol.User, len(c.Clients)) + switch c.Flow { + case vless.XRV, "": + default: + return nil, errors.New(`VLESS "settings.flow" doesn't support "` + c.Flow + `" in this version`) + } + for idx, rawUser := range c.Clients { + user := new(protocol.User) + if err := json.Unmarshal(rawUser, user); err != nil { + return nil, errors.New(`VLESS clients: invalid user`).Base(err) + } + account := new(vless.Account) + if err := json.Unmarshal(rawUser, account); err != nil { + return nil, errors.New(`VLESS clients: invalid user`).Base(err) + } + + u, err := uuid.ParseString(account.Id) + if err != nil { + return nil, err + } + account.Id = u.String() + + switch account.Flow { + case "": + account.Flow = c.Flow + case vless.XRV: + default: + return nil, errors.New(`VLESS clients: "flow" doesn't support "` + account.Flow + `" in this version`) + } + + if len(account.Testseed) < 4 { + account.Testseed = c.Testseed + } + + if account.Encryption != "" { + return nil, errors.New(`VLESS clients: "encryption" should not be in inbound settings`) + } + + if account.Reverse != nil { + if account.Reverse.Tag == "" { + return nil, errors.New(`VLESS clients: "tag" can't be empty for "reverse"`) + } + if account.Reverse.Sniffing != nil { // may not be reached: error json unmarshal + return nil, errors.New(`VLESS clients: inbound's "reverse" can't have "sniffing"`) + } + } + + user.Account = serial.ToTypedMessage(account) + config.Clients[idx] = user + } + + config.Decryption = c.Decryption + if !func() bool { + s := strings.Split(config.Decryption, ".") + if len(s) < 4 || s[0] != "mlkem768x25519plus" { + return false + } + switch s[1] { + case "native": + case "xorpub": + config.XorMode = 1 + case "random": + config.XorMode = 2 + default: + return false + } + t := strings.SplitN(strings.TrimSuffix(s[2], "s"), "-", 2) + i, err := strconv.Atoi(t[0]) + if err != nil { + return false + } + config.SecondsFrom = int64(i) + if len(t) == 2 { + i, err := strconv.Atoi(t[1]) + if err != nil { + return false + } + config.SecondsTo = int64(i) + } + padding := 0 + for _, r := range s[3:] { + if len(r) < 20 { + padding += len(r) + 1 + continue + } + if b, _ := base64.RawURLEncoding.DecodeString(r); len(b) != 32 && len(b) != 64 { + return false + } + } + config.Decryption = config.Decryption[27+len(s[2]):] + if padding > 0 { + config.Padding = config.Decryption[:padding-1] + config.Decryption = config.Decryption[padding:] + } + return true + }() && config.Decryption != "none" { + if config.Decryption == "" { + return nil, errors.New(`VLESS settings: please add/set "decryption":"none" to every settings`) + } + return nil, errors.New(`VLESS settings: unsupported "decryption": ` + config.Decryption) + } + + if config.Decryption != "none" && c.Fallbacks != nil { + return nil, errors.New(`VLESS settings: "fallbacks" can not be used together with "decryption"`) + } + + for _, fb := range c.Fallbacks { + var i uint16 + var s string + if err := json.Unmarshal(fb.Dest, &i); err == nil { + s = strconv.Itoa(int(i)) + } else { + _ = json.Unmarshal(fb.Dest, &s) + } + config.Fallbacks = append(config.Fallbacks, &inbound.Fallback{ + Name: fb.Name, + Alpn: fb.Alpn, + Path: fb.Path, + Type: fb.Type, + Dest: s, + Xver: fb.Xver, + }) + } + for _, fb := range config.Fallbacks { + /* + if fb.Alpn == "h2" && fb.Path != "" { + return nil, errors.New(`VLESS fallbacks: "alpn":"h2" doesn't support "path"`) + } + */ + if fb.Path != "" && fb.Path[0] != '/' { + return nil, errors.New(`VLESS fallbacks: "path" must be empty or start with "/"`) + } + if fb.Type == "" && fb.Dest != "" { + if fb.Dest == "serve-ws-none" { + fb.Type = "serve" + } else if filepath.IsAbs(fb.Dest) || fb.Dest[0] == '@' { + fb.Type = "unix" + if strings.HasPrefix(fb.Dest, "@@") && (runtime.GOOS == "linux" || runtime.GOOS == "android") { + fullAddr := make([]byte, len(syscall.RawSockaddrUnix{}.Path)) // may need padding to work with haproxy + copy(fullAddr, fb.Dest[1:]) + fb.Dest = string(fullAddr) + } + } else { + if _, err := strconv.Atoi(fb.Dest); err == nil { + fb.Dest = "localhost:" + fb.Dest + } + if _, _, err := net.SplitHostPort(fb.Dest); err == nil { + fb.Type = "tcp" + } + } + } + if fb.Type == "" { + return nil, errors.New(`VLESS fallbacks: please fill in a valid value for every "dest"`) + } + if fb.Xver > 2 { + return nil, errors.New(`VLESS fallbacks: invalid PROXY protocol version, "xver" only accepts 0, 1, 2`) + } + } + + return config, nil +} + +type VLessReverseConfig struct { + Tag string `json:"tag"` + Sniffing *SniffingConfig `json:"sniffing"` +} + +func (c *VLessReverseConfig) Build() (*vless.Reverse, error) { + if c.Tag == "" { + return nil, errors.New(`VLESS reverse: "tag" can't be empty`) + } + r := &vless.Reverse{ + Tag: c.Tag, + } + if c.Sniffing != nil { + sc, err := c.Sniffing.Build() + if err != nil { + return nil, errors.New(`VLESS reverse: invalid "sniffing" config`).Base(err) + } + r.Sniffing = sc + } + return r, nil +} + +type VLessOutboundVnext struct { + Address *Address `json:"address"` + Port uint16 `json:"port"` + Users []json.RawMessage `json:"users"` +} + +type VLessOutboundConfig struct { + Address *Address `json:"address"` + Port uint16 `json:"port"` + Level uint32 `json:"level"` + Email string `json:"email"` + Id string `json:"id"` + Flow string `json:"flow"` + Seed string `json:"seed"` + Encryption string `json:"encryption"` + Reverse *VLessReverseConfig `json:"reverse"` + Testpre uint32 `json:"testpre"` + Testseed []uint32 `json:"testseed"` + Vnext []*VLessOutboundVnext `json:"vnext"` +} + +// Build implements Buildable +func (c *VLessOutboundConfig) Build() (proto.Message, error) { + config := new(outbound.Config) + if c.Address != nil { + c.Vnext = []*VLessOutboundVnext{ + { + Address: c.Address, + Port: c.Port, + Users: []json.RawMessage{{}}, + }, + } + } + if len(c.Vnext) != 1 { + return nil, errors.New(`VLESS settings: "vnext" should have one and only one member. Multiple endpoints in "vnext" should use multiple VLESS outbounds and routing balancer instead`) + } + for _, rec := range c.Vnext { + if rec.Address == nil { + return nil, errors.New(`VLESS vnext: "address" is not set`) + } + if len(rec.Users) != 1 { + return nil, errors.New(`VLESS vnext: "users" should have one and only one member. Multiple members in "users" should use multiple VLESS outbounds and routing balancer instead`) + } + spec := &protocol.ServerEndpoint{ + Address: rec.Address.Build(), + Port: uint32(rec.Port), + } + for _, rawUser := range rec.Users { + user := new(protocol.User) + if c.Address != nil { + user.Level = c.Level + user.Email = c.Email + } else { + if err := json.Unmarshal(rawUser, user); err != nil { + return nil, errors.New(`VLESS users: invalid user`).Base(err) + } + } + account := new(vless.Account) + if c.Address != nil { + account.Id = c.Id + account.Flow = c.Flow + //account.Seed = c.Seed + account.Encryption = c.Encryption + if c.Reverse != nil { + rvs, err := c.Reverse.Build() + if err != nil { + return nil, err + } + account.Reverse = rvs + } + account.Testpre = c.Testpre + account.Testseed = c.Testseed + } else { + if err := json.Unmarshal(rawUser, account); err != nil { + return nil, errors.New(`VLESS users: invalid user`).Base(err) + } + if account.Reverse != nil { // may not be reached: error json unmarshal + return nil, errors.New(`VLESS users: please use simplified outbound's config style to use "reverse"`) + } + } + + u, err := uuid.ParseString(account.Id) + if err != nil { + return nil, err + } + account.Id = u.String() + + switch account.Flow { + case "": + case vless.XRV, vless.XRV + "-udp443": + default: + return nil, errors.New(`VLESS users: "flow" doesn't support "` + account.Flow + `" in this version`) + } + + if !func() bool { + s := strings.Split(account.Encryption, ".") + if len(s) < 4 || s[0] != "mlkem768x25519plus" { + return false + } + switch s[1] { + case "native": + case "xorpub": + account.XorMode = 1 + case "random": + account.XorMode = 2 + default: + return false + } + switch s[2] { + case "1rtt": + case "0rtt": + account.Seconds = 1 + default: + return false + } + padding := 0 + for _, r := range s[3:] { + if len(r) < 20 { + padding += len(r) + 1 + continue + } + if b, _ := base64.RawURLEncoding.DecodeString(r); len(b) != 32 && len(b) != 1184 { + return false + } + } + account.Encryption = account.Encryption[27+len(s[2]):] + if padding > 0 { + account.Padding = account.Encryption[:padding-1] + account.Encryption = account.Encryption[padding:] + } + return true + }() && account.Encryption != "none" { + if account.Encryption == "" { + return nil, errors.New(`VLESS users: please add/set "encryption":"none" for every user`) + } + return nil, errors.New(`VLESS users: unsupported "encryption": ` + account.Encryption) + } + + user.Account = serial.ToTypedMessage(account) + spec.User = user + break + } + config.Vnext = spec + break + } + + return config, nil +} diff --git a/subproject/Xray-core-main/infra/conf/vless_test.go b/subproject/Xray-core-main/infra/conf/vless_test.go new file mode 100644 index 00000000..2fd97915 --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/vless_test.go @@ -0,0 +1,159 @@ +package conf_test + +import ( + "testing" + + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/serial" + . "github.com/xtls/xray-core/infra/conf" + "github.com/xtls/xray-core/proxy/vless" + "github.com/xtls/xray-core/proxy/vless/inbound" + "github.com/xtls/xray-core/proxy/vless/outbound" +) + +func TestVLessOutbound(t *testing.T) { + creator := func() Buildable { + return new(VLessOutboundConfig) + } + + runMultiTestCase(t, []TestCase{ + { + Input: `{ + "vnext": [{ + "address": "example.com", + "port": 443, + "users": [ + { + "id": "27848739-7e62-4138-9fd3-098a63964b6b", + "flow": "xtls-rprx-vision-udp443", + "encryption": "none", + "level": 0 + } + ] + }] + }`, + Parser: loadJSON(creator), + Output: &outbound.Config{ + Vnext: &protocol.ServerEndpoint{ + Address: &net.IPOrDomain{ + Address: &net.IPOrDomain_Domain{ + Domain: "example.com", + }, + }, + Port: 443, + User: &protocol.User{ + Account: serial.ToTypedMessage(&vless.Account{ + Id: "27848739-7e62-4138-9fd3-098a63964b6b", + Flow: "xtls-rprx-vision-udp443", + Encryption: "none", + }), + Level: 0, + }, + }, + }, + }, + { + Input: `{ + "address": "example.com", + "port": 443, + "id": "27848739-7e62-4138-9fd3-098a63964b6b", + "flow": "xtls-rprx-vision-udp443", + "encryption": "none", + "level": 0 + }`, + Parser: loadJSON(creator), + Output: &outbound.Config{ + Vnext: &protocol.ServerEndpoint{ + Address: &net.IPOrDomain{ + Address: &net.IPOrDomain_Domain{ + Domain: "example.com", + }, + }, + Port: 443, + User: &protocol.User{ + Account: serial.ToTypedMessage(&vless.Account{ + Id: "27848739-7e62-4138-9fd3-098a63964b6b", + Flow: "xtls-rprx-vision-udp443", + Encryption: "none", + }), + Level: 0, + }, + }, + }, + }, + }) +} + +func TestVLessInbound(t *testing.T) { + creator := func() Buildable { + return new(VLessInboundConfig) + } + + runMultiTestCase(t, []TestCase{ + { + Input: `{ + "clients": [ + { + "id": "27848739-7e62-4138-9fd3-098a63964b6b", + "flow": "xtls-rprx-vision", + "level": 0, + "email": "love@example.com" + } + ], + "decryption": "none", + "fallbacks": [ + { + "dest": 80 + }, + { + "alpn": "h2", + "dest": "@/dev/shm/domain.socket", + "xver": 2 + }, + { + "path": "/innerws", + "dest": "serve-ws-none" + } + ] + }`, + Parser: loadJSON(creator), + Output: &inbound.Config{ + Clients: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vless.Account{ + Id: "27848739-7e62-4138-9fd3-098a63964b6b", + Flow: "xtls-rprx-vision", + }), + Level: 0, + Email: "love@example.com", + }, + }, + Decryption: "none", + Fallbacks: []*inbound.Fallback{ + { + Alpn: "", + Path: "", + Type: "tcp", + Dest: "localhost:80", + Xver: 0, + }, + { + Alpn: "h2", + Path: "", + Type: "unix", + Dest: "@/dev/shm/domain.socket", + Xver: 2, + }, + { + Alpn: "", + Path: "/innerws", + Type: "serve", + Dest: "serve-ws-none", + Xver: 0, + }, + }, + }, + }, + }) +} diff --git a/subproject/Xray-core-main/infra/conf/vmess.go b/subproject/Xray-core-main/infra/conf/vmess.go new file mode 100644 index 00000000..cb8ff9c9 --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/vmess.go @@ -0,0 +1,179 @@ +package conf + +import ( + "encoding/json" + "strings" + + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/serial" + "github.com/xtls/xray-core/common/uuid" + "github.com/xtls/xray-core/proxy/vmess" + "github.com/xtls/xray-core/proxy/vmess/inbound" + "github.com/xtls/xray-core/proxy/vmess/outbound" + "google.golang.org/protobuf/proto" +) + +type VMessAccount struct { + ID string `json:"id"` + Security string `json:"security"` + Experiments string `json:"experiments"` +} + +// Build implements Buildable +func (a *VMessAccount) Build() *vmess.Account { + var st protocol.SecurityType + switch strings.ToLower(a.Security) { + case "aes-128-gcm": + st = protocol.SecurityType_AES128_GCM + case "chacha20-poly1305": + st = protocol.SecurityType_CHACHA20_POLY1305 + case "auto": + st = protocol.SecurityType_AUTO + case "none": + st = protocol.SecurityType_NONE + case "zero": + st = protocol.SecurityType_ZERO + default: + st = protocol.SecurityType_AUTO + } + return &vmess.Account{ + Id: a.ID, + SecuritySettings: &protocol.SecurityConfig{ + Type: st, + }, + TestsEnabled: a.Experiments, + } +} + +type VMessDefaultConfig struct { + Level byte `json:"level"` +} + +// Build implements Buildable +func (c *VMessDefaultConfig) Build() *inbound.DefaultConfig { + config := new(inbound.DefaultConfig) + config.Level = uint32(c.Level) + return config +} + +type VMessInboundConfig struct { + Users []json.RawMessage `json:"clients"` + Defaults *VMessDefaultConfig `json:"default"` +} + +// Build implements Buildable +func (c *VMessInboundConfig) Build() (proto.Message, error) { + errors.PrintNonRemovalDeprecatedFeatureWarning("VMess (with no Forward Secrecy, etc.)", "VLESS Encryption") + + config := &inbound.Config{} + + if c.Defaults != nil { + config.Default = c.Defaults.Build() + } + + config.User = make([]*protocol.User, len(c.Users)) + for idx, rawData := range c.Users { + user := new(protocol.User) + if err := json.Unmarshal(rawData, user); err != nil { + return nil, errors.New("invalid VMess user").Base(err) + } + account := new(VMessAccount) + if err := json.Unmarshal(rawData, account); err != nil { + return nil, errors.New("invalid VMess user").Base(err) + } + + u, err := uuid.ParseString(account.ID) + if err != nil { + return nil, err + } + account.ID = u.String() + + user.Account = serial.ToTypedMessage(account.Build()) + config.User[idx] = user + } + + return config, nil +} + +type VMessOutboundTarget struct { + Address *Address `json:"address"` + Port uint16 `json:"port"` + Users []json.RawMessage `json:"users"` +} + +type VMessOutboundConfig struct { + Address *Address `json:"address"` + Port uint16 `json:"port"` + Level uint32 `json:"level"` + Email string `json:"email"` + ID string `json:"id"` + Security string `json:"security"` + Experiments string `json:"experiments"` + Receivers []*VMessOutboundTarget `json:"vnext"` +} + +// Build implements Buildable +func (c *VMessOutboundConfig) Build() (proto.Message, error) { + errors.PrintNonRemovalDeprecatedFeatureWarning("VMess (with no Forward Secrecy, etc.)", "VLESS Encryption") + + config := new(outbound.Config) + if c.Address != nil { + c.Receivers = []*VMessOutboundTarget{ + { + Address: c.Address, + Port: c.Port, + Users: []json.RawMessage{{}}, + }, + } + } + if len(c.Receivers) != 1 { + return nil, errors.New(`VMess settings: "vnext" should have one and only one member. Multiple endpoints in "vnext" should use multiple VMess outbounds and routing balancer instead`) + } + for _, rec := range c.Receivers { + if len(rec.Users) != 1 { + return nil, errors.New(`VMess vnext: "users" should have one and only one member. Multiple members in "users" should use multiple VMess outbounds and routing balancer instead`) + } + if rec.Address == nil { + return nil, errors.New(`VMess vnext: "address" is not set`) + } + spec := &protocol.ServerEndpoint{ + Address: rec.Address.Build(), + Port: uint32(rec.Port), + } + for _, rawUser := range rec.Users { + user := new(protocol.User) + if c.Address != nil { + user.Level = c.Level + user.Email = c.Email + } else { + if err := json.Unmarshal(rawUser, user); err != nil { + return nil, errors.New("invalid VMess user").Base(err) + } + } + account := new(VMessAccount) + if c.Address != nil { + account.ID = c.ID + account.Security = c.Security + account.Experiments = c.Experiments + } else { + if err := json.Unmarshal(rawUser, account); err != nil { + return nil, errors.New("invalid VMess user").Base(err) + } + } + + u, err := uuid.ParseString(account.ID) + if err != nil { + return nil, err + } + account.ID = u.String() + + user.Account = serial.ToTypedMessage(account.Build()) + spec.User = user + break + } + config.Receiver = spec + break + } + return config, nil +} diff --git a/subproject/Xray-core-main/infra/conf/vmess_test.go b/subproject/Xray-core-main/infra/conf/vmess_test.go new file mode 100644 index 00000000..b94db83e --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/vmess_test.go @@ -0,0 +1,130 @@ +package conf_test + +import ( + "testing" + + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/serial" + . "github.com/xtls/xray-core/infra/conf" + "github.com/xtls/xray-core/proxy/vmess" + "github.com/xtls/xray-core/proxy/vmess/inbound" + "github.com/xtls/xray-core/proxy/vmess/outbound" +) + +func TestVMessOutbound(t *testing.T) { + creator := func() Buildable { + return new(VMessOutboundConfig) + } + + runMultiTestCase(t, []TestCase{ + { + Input: `{ + "vnext": [{ + "address": "127.0.0.1", + "port": 80, + "users": [ + { + "id": "e641f5ad-9397-41e3-bf1a-e8740dfed019", + "email": "love@example.com", + "level": 255 + } + ] + }] + }`, + Parser: loadJSON(creator), + Output: &outbound.Config{ + Receiver: &protocol.ServerEndpoint{ + Address: &net.IPOrDomain{ + Address: &net.IPOrDomain_Ip{ + Ip: []byte{127, 0, 0, 1}, + }, + }, + Port: 80, + User: &protocol.User{ + Email: "love@example.com", + Level: 255, + Account: serial.ToTypedMessage(&vmess.Account{ + Id: "e641f5ad-9397-41e3-bf1a-e8740dfed019", + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_AUTO, + }, + }), + }, + }, + }, + }, + { + Input: `{ + "address": "127.0.0.1", + "port": 80, + "id": "e641f5ad-9397-41e3-bf1a-e8740dfed019", + "email": "love@example.com", + "level": 255 + }`, + Parser: loadJSON(creator), + Output: &outbound.Config{ + Receiver: &protocol.ServerEndpoint{ + Address: &net.IPOrDomain{ + Address: &net.IPOrDomain_Ip{ + Ip: []byte{127, 0, 0, 1}, + }, + }, + Port: 80, + User: &protocol.User{ + Email: "love@example.com", + Level: 255, + Account: serial.ToTypedMessage(&vmess.Account{ + Id: "e641f5ad-9397-41e3-bf1a-e8740dfed019", + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_AUTO, + }, + }), + }, + }, + }, + }, + }) +} + +func TestVMessInbound(t *testing.T) { + creator := func() Buildable { + return new(VMessInboundConfig) + } + + runMultiTestCase(t, []TestCase{ + { + Input: `{ + "clients": [ + { + "id": "27848739-7e62-4138-9fd3-098a63964b6b", + "level": 0, + "email": "love@example.com", + "security": "aes-128-gcm" + } + ], + "default": { + "level": 0 + } + }`, + Parser: loadJSON(creator), + Output: &inbound.Config{ + User: []*protocol.User{ + { + Level: 0, + Email: "love@example.com", + Account: serial.ToTypedMessage(&vmess.Account{ + Id: "27848739-7e62-4138-9fd3-098a63964b6b", + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_AES128_GCM, + }, + }), + }, + }, + Default: &inbound.DefaultConfig{ + Level: 0, + }, + }, + }, + }) +} diff --git a/subproject/Xray-core-main/infra/conf/wireguard.go b/subproject/Xray-core-main/infra/conf/wireguard.go new file mode 100644 index 00000000..1ba61f96 --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/wireguard.go @@ -0,0 +1,152 @@ +package conf + +import ( + "encoding/base64" + "encoding/hex" + "strings" + + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/proxy/wireguard" + "google.golang.org/protobuf/proto" +) + +type WireGuardPeerConfig struct { + PublicKey string `json:"publicKey"` + PreSharedKey string `json:"preSharedKey"` + Endpoint string `json:"endpoint"` + KeepAlive uint32 `json:"keepAlive"` + AllowedIPs []string `json:"allowedIPs,omitempty"` +} + +func (c *WireGuardPeerConfig) Build() (proto.Message, error) { + var err error + config := new(wireguard.PeerConfig) + + if c.PublicKey != "" { + config.PublicKey, err = ParseWireGuardKey(c.PublicKey) + if err != nil { + return nil, err + } + } + + if c.PreSharedKey != "" { + config.PreSharedKey, err = ParseWireGuardKey(c.PreSharedKey) + if err != nil { + return nil, err + } + } + + config.Endpoint = c.Endpoint + // default 0 + config.KeepAlive = c.KeepAlive + if c.AllowedIPs == nil { + config.AllowedIps = []string{"0.0.0.0/0", "::0/0"} + } else { + config.AllowedIps = c.AllowedIPs + } + + return config, nil +} + +type WireGuardConfig struct { + IsClient bool `json:""` + + NoKernelTun bool `json:"noKernelTun"` + SecretKey string `json:"secretKey"` + Address []string `json:"address"` + Peers []*WireGuardPeerConfig `json:"peers"` + MTU int32 `json:"mtu"` + NumWorkers int32 `json:"workers"` + Reserved []byte `json:"reserved"` + DomainStrategy string `json:"domainStrategy"` +} + +func (c *WireGuardConfig) Build() (proto.Message, error) { + config := new(wireguard.DeviceConfig) + + var err error + config.SecretKey, err = ParseWireGuardKey(c.SecretKey) + if err != nil { + return nil, errors.New("invalid WireGuard secret key: %w", err) + } + + if c.Address == nil { + // bogon ips + config.Endpoint = []string{"10.0.0.1", "fd59:7153:2388:b5fd:0000:0000:0000:0001"} + } else { + config.Endpoint = c.Address + } + + if c.Peers != nil { + config.Peers = make([]*wireguard.PeerConfig, len(c.Peers)) + for i, p := range c.Peers { + msg, err := p.Build() + if err != nil { + return nil, err + } + config.Peers[i] = msg.(*wireguard.PeerConfig) + } + } + + if c.MTU == 0 { + config.Mtu = 1420 + } else { + config.Mtu = c.MTU + } + // these a fallback code exists in wireguard-go code, + // we don't need to process fallback manually + config.NumWorkers = c.NumWorkers + + if len(c.Reserved) != 0 && len(c.Reserved) != 3 { + return nil, errors.New(`"reserved" should be empty or 3 bytes`) + } + config.Reserved = c.Reserved + + switch strings.ToLower(c.DomainStrategy) { + case "forceip", "": + config.DomainStrategy = wireguard.DeviceConfig_FORCE_IP + case "forceipv4": + config.DomainStrategy = wireguard.DeviceConfig_FORCE_IP4 + case "forceipv6": + config.DomainStrategy = wireguard.DeviceConfig_FORCE_IP6 + case "forceipv4v6": + config.DomainStrategy = wireguard.DeviceConfig_FORCE_IP46 + case "forceipv6v4": + config.DomainStrategy = wireguard.DeviceConfig_FORCE_IP64 + default: + return nil, errors.New("unsupported domain strategy: ", c.DomainStrategy) + } + + config.IsClient = c.IsClient + config.NoKernelTun = c.NoKernelTun + + return config, nil +} + +func ParseWireGuardKey(str string) (string, error) { + var err error + + if str == "" { + return "", errors.New("key must not be empty") + } + + if len(str) == 64 { + _, err = hex.DecodeString(str) + if err == nil { + return str, nil + } + } + + var dat []byte + str = strings.TrimSuffix(str, "=") + if strings.ContainsRune(str, '+') || strings.ContainsRune(str, '/') { + dat, err = base64.RawStdEncoding.DecodeString(str) + } else { + dat, err = base64.RawURLEncoding.DecodeString(str) + } + if err == nil { + return hex.EncodeToString(dat), nil + } + + return "", errors.New("failed to deserialize key").Base(err) +} diff --git a/subproject/Xray-core-main/infra/conf/wireguard_test.go b/subproject/Xray-core-main/infra/conf/wireguard_test.go new file mode 100644 index 00000000..c4c24c44 --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/wireguard_test.go @@ -0,0 +1,52 @@ +package conf_test + +import ( + "testing" + + . "github.com/xtls/xray-core/infra/conf" + "github.com/xtls/xray-core/proxy/wireguard" +) + +func TestWireGuardConfig(t *testing.T) { + creator := func() Buildable { + return new(WireGuardConfig) + } + + runMultiTestCase(t, []TestCase{ + { + Input: `{ + "secretKey": "uJv5tZMDltsiYEn+kUwb0Ll/CXWhMkaSCWWhfPEZM3A=", + "address": ["10.1.1.1", "fd59:7153:2388:b5fd:0000:0000:1234:0001"], + "peers": [ + { + "publicKey": "6e65ce0be17517110c17d77288ad87e7fd5252dcc7d09b95a39d61db03df832a", + "endpoint": "127.0.0.1:1234" + } + ], + "mtu": 1300, + "workers": 2, + "domainStrategy": "ForceIPv6v4", + "noKernelTun": false + }`, + Parser: loadJSON(creator), + Output: &wireguard.DeviceConfig{ + // key converted into hex form + SecretKey: "b89bf9b5930396db226049fe914c1bd0b97f0975a13246920965a17cf1193370", + Endpoint: []string{"10.1.1.1", "fd59:7153:2388:b5fd:0000:0000:1234:0001"}, + Peers: []*wireguard.PeerConfig{ + { + // also can read from hex form directly + PublicKey: "6e65ce0be17517110c17d77288ad87e7fd5252dcc7d09b95a39d61db03df832a", + Endpoint: "127.0.0.1:1234", + KeepAlive: 0, + AllowedIps: []string{"0.0.0.0/0", "::0/0"}, + }, + }, + Mtu: 1300, + NumWorkers: 2, + DomainStrategy: wireguard.DeviceConfig_FORCE_IP64, + NoKernelTun: false, + }, + }, + }) +} diff --git a/subproject/Xray-core-main/infra/conf/xray.go b/subproject/Xray-core-main/infra/conf/xray.go new file mode 100644 index 00000000..a2410244 --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/xray.go @@ -0,0 +1,806 @@ +package conf + +import ( + "bytes" + "context" + "encoding/json" + "os" + "path/filepath" + "sort" + "strings" + + "github.com/xtls/xray-core/app/dispatcher" + "github.com/xtls/xray-core/app/proxyman" + "github.com/xtls/xray-core/app/router" + "github.com/xtls/xray-core/app/stats" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/platform" + "github.com/xtls/xray-core/common/serial" + core "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/transport/internet" +) + +var ( + inboundConfigLoader = NewJSONConfigLoader(ConfigCreatorCache{ + "tunnel": func() interface{} { return new(DokodemoConfig) }, + "dokodemo-door": func() interface{} { return new(DokodemoConfig) }, + "http": func() interface{} { return new(HTTPServerConfig) }, + "shadowsocks": func() interface{} { return new(ShadowsocksServerConfig) }, + "mixed": func() interface{} { return new(SocksServerConfig) }, + "socks": func() interface{} { return new(SocksServerConfig) }, + "vless": func() interface{} { return new(VLessInboundConfig) }, + "vmess": func() interface{} { return new(VMessInboundConfig) }, + "trojan": func() interface{} { return new(TrojanServerConfig) }, + "wireguard": func() interface{} { return &WireGuardConfig{IsClient: false} }, + "hysteria": func() interface{} { return new(HysteriaServerConfig) }, + "tun": func() interface{} { return new(TunConfig) }, + }, "protocol", "settings") + + outboundConfigLoader = NewJSONConfigLoader(ConfigCreatorCache{ + "block": func() interface{} { return new(BlackholeConfig) }, + "blackhole": func() interface{} { return new(BlackholeConfig) }, + "loopback": func() interface{} { return new(LoopbackConfig) }, + "direct": func() interface{} { return new(FreedomConfig) }, + "freedom": func() interface{} { return new(FreedomConfig) }, + "http": func() interface{} { return new(HTTPClientConfig) }, + "shadowsocks": func() interface{} { return new(ShadowsocksClientConfig) }, + "socks": func() interface{} { return new(SocksClientConfig) }, + "vless": func() interface{} { return new(VLessOutboundConfig) }, + "vmess": func() interface{} { return new(VMessOutboundConfig) }, + "trojan": func() interface{} { return new(TrojanClientConfig) }, + "hysteria": func() interface{} { return new(HysteriaClientConfig) }, + "dns": func() interface{} { return new(DNSOutboundConfig) }, + "wireguard": func() interface{} { return &WireGuardConfig{IsClient: true} }, + }, "protocol", "settings") +) + +type SniffingConfig struct { + Enabled bool `json:"enabled"` + DestOverride *StringList `json:"destOverride"` + DomainsExcluded *StringList `json:"domainsExcluded"` + MetadataOnly bool `json:"metadataOnly"` + RouteOnly bool `json:"routeOnly"` +} + +// Build implements Buildable. +func (c *SniffingConfig) Build() (*proxyman.SniffingConfig, error) { + var p []string + if c.DestOverride != nil { + for _, protocol := range *c.DestOverride { + switch strings.ToLower(protocol) { + case "http": + p = append(p, "http") + case "tls", "https", "ssl": + p = append(p, "tls") + case "quic": + p = append(p, "quic") + case "fakedns", "fakedns+others": + p = append(p, "fakedns") + default: + return nil, errors.New("unknown protocol: ", protocol) + } + } + } + + var d []string + if c.DomainsExcluded != nil { + for _, domain := range *c.DomainsExcluded { + d = append(d, strings.ToLower(domain)) + } + } + + return &proxyman.SniffingConfig{ + Enabled: c.Enabled, + DestinationOverride: p, + DomainsExcluded: d, + MetadataOnly: c.MetadataOnly, + RouteOnly: c.RouteOnly, + }, nil +} + +type MuxConfig struct { + Enabled bool `json:"enabled"` + Concurrency int16 `json:"concurrency"` + XudpConcurrency int16 `json:"xudpConcurrency"` + XudpProxyUDP443 string `json:"xudpProxyUDP443"` +} + +// Build creates MultiplexingConfig, Concurrency < 0 completely disables mux. +func (m *MuxConfig) Build() (*proxyman.MultiplexingConfig, error) { + switch m.XudpProxyUDP443 { + case "": + m.XudpProxyUDP443 = "reject" + case "reject", "allow", "skip": + default: + return nil, errors.New(`unknown "xudpProxyUDP443": `, m.XudpProxyUDP443) + } + return &proxyman.MultiplexingConfig{ + Enabled: m.Enabled, + Concurrency: int32(m.Concurrency), + XudpConcurrency: int32(m.XudpConcurrency), + XudpProxyUDP443: m.XudpProxyUDP443, + }, nil +} + +type InboundDetourConfig struct { + Protocol string `json:"protocol"` + PortList *PortList `json:"port"` + ListenOn *Address `json:"listen"` + Settings *json.RawMessage `json:"settings"` + Tag string `json:"tag"` + StreamSetting *StreamConfig `json:"streamSettings"` + SniffingConfig *SniffingConfig `json:"sniffing"` +} + +// Build implements Buildable. +func (c *InboundDetourConfig) Build() (*core.InboundHandlerConfig, error) { + receiverSettings := &proxyman.ReceiverConfig{} + + // TUN inbound doesn't need port configuration as it uses network interface instead + if strings.ToLower(c.Protocol) == "tun" { + // Skip port validation for TUN + } else if c.ListenOn == nil { + // Listen on anyip, must set PortList + if c.PortList == nil { + return nil, errors.New("Listen on AnyIP but no Port(s) set in InboundDetour.") + } + receiverSettings.PortList = c.PortList.Build() + } else { + // Listen on specific IP or Unix Domain Socket + receiverSettings.Listen = c.ListenOn.Build() + listenDS := c.ListenOn.Family().IsDomain() && (filepath.IsAbs(c.ListenOn.Domain()) || c.ListenOn.Domain()[0] == '@') + listenIP := c.ListenOn.Family().IsIP() || (c.ListenOn.Family().IsDomain() && c.ListenOn.Domain() == "localhost") + if listenIP { + // Listen on specific IP, must set PortList + if c.PortList == nil { + return nil, errors.New("Listen on specific ip without port in InboundDetour.") + } + // Listen on IP:Port + receiverSettings.PortList = c.PortList.Build() + } else if listenDS { + if c.PortList != nil { + // Listen on Unix Domain Socket, PortList should be nil + receiverSettings.PortList = nil + } + } else { + return nil, errors.New("unable to listen on domain address: ", c.ListenOn.Domain()) + } + } + + if c.StreamSetting != nil { + ss, err := c.StreamSetting.Build() + if err != nil { + return nil, err + } + receiverSettings.StreamSettings = ss + if strings.Contains(ss.SecurityType, "reality") && (receiverSettings.PortList == nil || + len(receiverSettings.PortList.Ports()) != 1 || receiverSettings.PortList.Ports()[0] != 443) { + errors.LogWarning(context.Background(), `REALITY: Listening on non-443 ports may get your IP blocked by the GFW`) + } + } + if c.SniffingConfig != nil { + s, err := c.SniffingConfig.Build() + if err != nil { + return nil, errors.New("failed to build sniffing config").Base(err) + } + receiverSettings.SniffingSettings = s + } + + settings := []byte("{}") + if c.Settings != nil { + settings = ([]byte)(*c.Settings) + } + rawConfig, err := inboundConfigLoader.LoadWithID(settings, c.Protocol) + if err != nil { + return nil, errors.New("failed to load inbound detour config for protocol ", c.Protocol).Base(err) + } + if dokodemoConfig, ok := rawConfig.(*DokodemoConfig); ok { + receiverSettings.ReceiveOriginalDestination = dokodemoConfig.FollowRedirect + } + ts, err := rawConfig.(Buildable).Build() + if err != nil { + return nil, errors.New("failed to build inbound handler for protocol ", c.Protocol).Base(err) + } + + return &core.InboundHandlerConfig{ + Tag: c.Tag, + ReceiverSettings: serial.ToTypedMessage(receiverSettings), + ProxySettings: serial.ToTypedMessage(ts), + }, nil +} + +type OutboundDetourConfig struct { + Protocol string `json:"protocol"` + SendThrough *string `json:"sendThrough"` + Tag string `json:"tag"` + Settings *json.RawMessage `json:"settings"` + StreamSetting *StreamConfig `json:"streamSettings"` + ProxySettings *ProxyConfig `json:"proxySettings"` + MuxSettings *MuxConfig `json:"mux"` + TargetStrategy string `json:"targetStrategy"` +} + +func (c *OutboundDetourConfig) checkChainProxyConfig() error { + if c.StreamSetting == nil || c.ProxySettings == nil || c.StreamSetting.SocketSettings == nil { + return nil + } + if len(c.ProxySettings.Tag) > 0 && len(c.StreamSetting.SocketSettings.DialerProxy) > 0 { + return errors.New("proxySettings.tag is conflicted with sockopt.dialerProxy").AtWarning() + } + return nil +} + +// Build implements Buildable. +func (c *OutboundDetourConfig) Build() (*core.OutboundHandlerConfig, error) { + senderSettings := &proxyman.SenderConfig{} + switch strings.ToLower(c.TargetStrategy) { + case "asis", "": + senderSettings.TargetStrategy = internet.DomainStrategy_AS_IS + case "useip": + senderSettings.TargetStrategy = internet.DomainStrategy_USE_IP + case "useipv4": + senderSettings.TargetStrategy = internet.DomainStrategy_USE_IP4 + case "useipv6": + senderSettings.TargetStrategy = internet.DomainStrategy_USE_IP6 + case "useipv4v6": + senderSettings.TargetStrategy = internet.DomainStrategy_USE_IP46 + case "useipv6v4": + senderSettings.TargetStrategy = internet.DomainStrategy_USE_IP64 + case "forceip": + senderSettings.TargetStrategy = internet.DomainStrategy_FORCE_IP + case "forceipv4": + senderSettings.TargetStrategy = internet.DomainStrategy_FORCE_IP4 + case "forceipv6": + senderSettings.TargetStrategy = internet.DomainStrategy_FORCE_IP6 + case "forceipv4v6": + senderSettings.TargetStrategy = internet.DomainStrategy_FORCE_IP46 + case "forceipv6v4": + senderSettings.TargetStrategy = internet.DomainStrategy_FORCE_IP64 + default: + return nil, errors.New("unsupported target domain strategy: ", c.TargetStrategy) + } + if err := c.checkChainProxyConfig(); err != nil { + return nil, err + } + + if c.SendThrough != nil { + address := ParseSendThough(c.SendThrough) + //Check if CIDR exists + if strings.Contains(*c.SendThrough, "/") { + senderSettings.ViaCidr = strings.Split(*c.SendThrough, "/")[1] + } else { + if address.Family().IsDomain() { + domain := address.Address.Domain() + if domain != "origin" && domain != "srcip" { + return nil, errors.New("unable to send through: " + address.String()) + } + } + } + senderSettings.Via = address.Build() + } + + if c.StreamSetting != nil { + ss, err := c.StreamSetting.Build() + if err != nil { + return nil, errors.New("failed to build stream settings for outbound detour").Base(err) + } + senderSettings.StreamSettings = ss + } + + if c.ProxySettings != nil { + ps, err := c.ProxySettings.Build() + if err != nil { + return nil, errors.New("invalid outbound detour proxy settings").Base(err) + } + if ps.TransportLayerProxy { + if senderSettings.StreamSettings != nil { + if senderSettings.StreamSettings.SocketSettings != nil { + senderSettings.StreamSettings.SocketSettings.DialerProxy = ps.Tag + } else { + senderSettings.StreamSettings.SocketSettings = &internet.SocketConfig{DialerProxy: ps.Tag} + } + } else { + senderSettings.StreamSettings = &internet.StreamConfig{SocketSettings: &internet.SocketConfig{DialerProxy: ps.Tag}} + } + ps = nil + } + senderSettings.ProxySettings = ps + } + + if c.MuxSettings != nil { + ms, err := c.MuxSettings.Build() + if err != nil { + return nil, errors.New("failed to build Mux config").Base(err) + } + senderSettings.MultiplexSettings = ms + } + + settings := []byte("{}") + if c.Settings != nil { + settings = ([]byte)(*c.Settings) + } + rawConfig, err := outboundConfigLoader.LoadWithID(settings, c.Protocol) + if err != nil { + return nil, errors.New("failed to load outbound detour config for protocol ", c.Protocol).Base(err) + } + ts, err := rawConfig.(Buildable).Build() + if err != nil { + return nil, errors.New("failed to build outbound handler for protocol ", c.Protocol).Base(err) + } + + return &core.OutboundHandlerConfig{ + SenderSettings: serial.ToTypedMessage(senderSettings), + Tag: c.Tag, + ProxySettings: serial.ToTypedMessage(ts), + }, nil +} + +type StatsConfig struct{} + +// Build implements Buildable. +func (c *StatsConfig) Build() (*stats.Config, error) { + return &stats.Config{}, nil +} + +type Config struct { + // Deprecated: Global transport config is no longer used + // left for returning error + Transport map[string]json.RawMessage `json:"transport"` + + LogConfig *LogConfig `json:"log"` + RouterConfig *RouterConfig `json:"routing"` + DNSConfig *DNSConfig `json:"dns"` + InboundConfigs []InboundDetourConfig `json:"inbounds"` + OutboundConfigs []OutboundDetourConfig `json:"outbounds"` + Policy *PolicyConfig `json:"policy"` + API *APIConfig `json:"api"` + Metrics *MetricsConfig `json:"metrics"` + Stats *StatsConfig `json:"stats"` + Reverse *ReverseConfig `json:"reverse"` + FakeDNS *FakeDNSConfig `json:"fakeDns"` + Observatory *ObservatoryConfig `json:"observatory"` + BurstObservatory *BurstObservatoryConfig `json:"burstObservatory"` + Version *VersionConfig `json:"version"` +} + +func (c *Config) findInboundTag(tag string) int { + found := -1 + for idx, ib := range c.InboundConfigs { + if ib.Tag == tag { + found = idx + break + } + } + return found +} + +func (c *Config) findOutboundTag(tag string) int { + found := -1 + for idx, ob := range c.OutboundConfigs { + if ob.Tag == tag { + found = idx + break + } + } + return found +} + +// Override method accepts another Config overrides the current attribute +func (c *Config) Override(o *Config, fn string) { + // only process the non-deprecated members + + if o.LogConfig != nil { + c.LogConfig = o.LogConfig + } + if o.RouterConfig != nil { + c.RouterConfig = o.RouterConfig + } + if o.DNSConfig != nil { + c.DNSConfig = o.DNSConfig + } + if o.Transport != nil { + c.Transport = o.Transport + } + if o.Policy != nil { + c.Policy = o.Policy + } + if o.API != nil { + c.API = o.API + } + if o.Metrics != nil { + c.Metrics = o.Metrics + } + if o.Stats != nil { + c.Stats = o.Stats + } + if o.Reverse != nil { + c.Reverse = o.Reverse + } + + if o.FakeDNS != nil { + c.FakeDNS = o.FakeDNS + } + + if o.Observatory != nil { + c.Observatory = o.Observatory + } + + if o.BurstObservatory != nil { + c.BurstObservatory = o.BurstObservatory + } + + if o.Version != nil { + c.Version = o.Version + } + + // update the Inbound in slice if the only one in override config has same tag + if len(o.InboundConfigs) > 0 { + for i := range o.InboundConfigs { + if idx := c.findInboundTag(o.InboundConfigs[i].Tag); idx > -1 { + c.InboundConfigs[idx] = o.InboundConfigs[i] + errors.LogInfo(context.Background(), "[", fn, "] updated inbound with tag: ", o.InboundConfigs[i].Tag) + + } else { + c.InboundConfigs = append(c.InboundConfigs, o.InboundConfigs[i]) + errors.LogInfo(context.Background(), "[", fn, "] appended inbound with tag: ", o.InboundConfigs[i].Tag) + } + + } + } + + // update the Outbound in slice if the only one in override config has same tag + if len(o.OutboundConfigs) > 0 { + outboundPrepends := []OutboundDetourConfig{} + for i := range o.OutboundConfigs { + if idx := c.findOutboundTag(o.OutboundConfigs[i].Tag); idx > -1 { + c.OutboundConfigs[idx] = o.OutboundConfigs[i] + errors.LogInfo(context.Background(), "[", fn, "] updated outbound with tag: ", o.OutboundConfigs[i].Tag) + } else { + if strings.Contains(strings.ToLower(fn), "tail") { + c.OutboundConfigs = append(c.OutboundConfigs, o.OutboundConfigs[i]) + errors.LogInfo(context.Background(), "[", fn, "] appended outbound with tag: ", o.OutboundConfigs[i].Tag) + } else { + outboundPrepends = append(outboundPrepends, o.OutboundConfigs[i]) + errors.LogInfo(context.Background(), "[", fn, "] prepend outbound with tag: ", o.OutboundConfigs[i].Tag) + } + } + } + if !strings.Contains(strings.ToLower(fn), "tail") && len(outboundPrepends) > 0 { + c.OutboundConfigs = append(outboundPrepends, c.OutboundConfigs...) + } + } +} + +// Build implements Buildable. +func (c *Config) Build() (*core.Config, error) { + if err := PostProcessConfigureFile(c); err != nil { + return nil, errors.New("failed to post-process configuration file").Base(err) + } + + config := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&dispatcher.Config{}), + serial.ToTypedMessage(&proxyman.InboundConfig{}), + serial.ToTypedMessage(&proxyman.OutboundConfig{}), + }, + } + + if c.API != nil { + apiConf, err := c.API.Build() + if err != nil { + return nil, errors.New("failed to build API configuration").Base(err) + } + config.App = append(config.App, serial.ToTypedMessage(apiConf)) + } + if c.Metrics != nil { + metricsConf, err := c.Metrics.Build() + if err != nil { + return nil, errors.New("failed to build metrics configuration").Base(err) + } + config.App = append(config.App, serial.ToTypedMessage(metricsConf)) + } + if c.Stats != nil { + statsConf, err := c.Stats.Build() + if err != nil { + return nil, errors.New("failed to build stats configuration").Base(err) + } + config.App = append(config.App, serial.ToTypedMessage(statsConf)) + } + + var logConfMsg *serial.TypedMessage + if c.LogConfig != nil { + logConfMsg = serial.ToTypedMessage(c.LogConfig.Build()) + } else { + logConfMsg = serial.ToTypedMessage(DefaultLogConfig()) + } + // let logger module be the first App to start, + // so that other modules could print log during initiating + config.App = append([]*serial.TypedMessage{logConfMsg}, config.App...) + + if c.RouterConfig != nil { + routerConfig, err := c.RouterConfig.Build() + if err != nil { + return nil, errors.New("failed to build routing configuration").Base(err) + } + config.App = append(config.App, serial.ToTypedMessage(routerConfig)) + } + + if c.DNSConfig != nil { + dnsApp, err := c.DNSConfig.Build() + if err != nil { + return nil, errors.New("failed to build DNS configuration").Base(err) + } + config.App = append(config.App, serial.ToTypedMessage(dnsApp)) + } + + if c.Policy != nil { + pc, err := c.Policy.Build() + if err != nil { + return nil, errors.New("failed to build policy configuration").Base(err) + } + config.App = append(config.App, serial.ToTypedMessage(pc)) + } + + if c.Reverse != nil { + r, err := c.Reverse.Build() + if err != nil { + return nil, errors.New("failed to build reverse configuration").Base(err) + } + config.App = append(config.App, serial.ToTypedMessage(r)) + } + + if c.FakeDNS != nil { + r, err := c.FakeDNS.Build() + if err != nil { + return nil, errors.New("failed to build fake DNS configuration").Base(err) + } + config.App = append([]*serial.TypedMessage{serial.ToTypedMessage(r)}, config.App...) + } + + if c.Observatory != nil { + r, err := c.Observatory.Build() + if err != nil { + return nil, errors.New("failed to build observatory configuration").Base(err) + } + config.App = append(config.App, serial.ToTypedMessage(r)) + } + + if c.BurstObservatory != nil { + r, err := c.BurstObservatory.Build() + if err != nil { + return nil, errors.New("failed to build burst observatory configuration").Base(err) + } + config.App = append(config.App, serial.ToTypedMessage(r)) + } + + if c.Version != nil { + r, err := c.Version.Build() + if err != nil { + return nil, errors.New("failed to build version configuration").Base(err) + } + config.App = append(config.App, serial.ToTypedMessage(r)) + } + + var inbounds []InboundDetourConfig + + if len(c.InboundConfigs) > 0 { + inbounds = append(inbounds, c.InboundConfigs...) + } + + if len(c.Transport) > 0 { + return nil, errors.PrintRemovedFeatureError("Global transport config", "streamSettings in inbounds and outbounds") + } + + for _, rawInboundConfig := range inbounds { + ic, err := rawInboundConfig.Build() + if err != nil { + return nil, errors.New("failed to build inbound config with tag ", rawInboundConfig.Tag).Base(err) + } + config.Inbound = append(config.Inbound, ic) + } + + var outbounds []OutboundDetourConfig + + if len(c.OutboundConfigs) > 0 { + outbounds = append(outbounds, c.OutboundConfigs...) + } + + for _, rawOutboundConfig := range outbounds { + oc, err := rawOutboundConfig.Build() + if err != nil { + return nil, errors.New("failed to build outbound config with tag ", rawOutboundConfig.Tag).Base(err) + } + config.Outbound = append(config.Outbound, oc) + } + + return config, nil +} + +func (c *Config) BuildMPHCache(customMatcherFilePath *string) error { + var geosite []*router.GeoSite + deps := make(map[string][]string) + uniqueGeosites := make(map[string]bool) + uniqueTags := make(map[string]bool) + matcherFilePath := platform.GetAssetLocation("matcher.cache") + + if customMatcherFilePath != nil { + matcherFilePath = *customMatcherFilePath + } + + processGeosite := func(dStr string) bool { + prefix := "" + if strings.HasPrefix(dStr, "geosite:") { + prefix = "geosite:" + } else if strings.HasPrefix(dStr, "ext-domain:") { + prefix = "ext-domain:" + } + if prefix == "" { + return false + } + key := strings.ToLower(dStr) + country := strings.ToUpper(dStr[len(prefix):]) + if !uniqueGeosites[country] { + ds, err := loadGeositeWithAttr("geosite.dat", country) + if err == nil { + uniqueGeosites[country] = true + geosite = append(geosite, &router.GeoSite{CountryCode: key, Domain: ds}) + } + } + return true + } + + processDomains := func(tag string, rawDomains []string) { + var manualDomains []*router.Domain + var dDeps []string + for _, dStr := range rawDomains { + if processGeosite(dStr) { + dDeps = append(dDeps, strings.ToLower(dStr)) + } else { + ds, err := parseDomainRule(dStr) + if err == nil { + manualDomains = append(manualDomains, ds...) + } + } + } + if len(manualDomains) > 0 { + if !uniqueTags[tag] { + uniqueTags[tag] = true + geosite = append(geosite, &router.GeoSite{CountryCode: tag, Domain: manualDomains}) + } + } + if len(dDeps) > 0 { + deps[tag] = append(deps[tag], dDeps...) + } + } + + // proccess rules + if c.RouterConfig != nil { + for _, rawRule := range c.RouterConfig.RuleList { + type SimpleRule struct { + RuleTag string `json:"ruleTag"` + Domain *StringList `json:"domain"` + Domains *StringList `json:"domains"` + } + var sr SimpleRule + json.Unmarshal(rawRule, &sr) + if sr.RuleTag == "" { + continue + } + var allDomains []string + if sr.Domain != nil { + allDomains = append(allDomains, *sr.Domain...) + } + if sr.Domains != nil { + allDomains = append(allDomains, *sr.Domains...) + } + processDomains(sr.RuleTag, allDomains) + } + } + + // proccess dns servers + if c.DNSConfig != nil { + for _, ns := range c.DNSConfig.Servers { + if ns.Tag == "" { + continue + } + processDomains(ns.Tag, ns.Domains) + } + } + + var hostIPs map[string][]string + if c.DNSConfig != nil && c.DNSConfig.Hosts != nil { + hostIPs = make(map[string][]string) + var hostDeps []string + var hostPatterns []string + + // use raw map to avoid expanding geosites + var domains []string + for domain := range c.DNSConfig.Hosts.Hosts { + domains = append(domains, domain) + } + sort.Strings(domains) + + manualHostGroups := make(map[string][]*router.Domain) + manualHostIPs := make(map[string][]string) + manualHostNames := make(map[string]string) + + for _, domain := range domains { + ha := c.DNSConfig.Hosts.Hosts[domain] + m := getHostMapping(ha) + + var ips []string + if m.ProxiedDomain != "" { + ips = append(ips, m.ProxiedDomain) + } else { + for _, ip := range m.Ip { + ips = append(ips, net.IPAddress(ip).String()) + } + } + + if processGeosite(domain) { + tag := strings.ToLower(domain) + hostDeps = append(hostDeps, tag) + hostIPs[tag] = ips + hostPatterns = append(hostPatterns, domain) + } else { + // build manual domains by their destination IPs + sort.Strings(ips) + ipKey := strings.Join(ips, ",") + ds, err := parseDomainRule(domain) + if err == nil { + manualHostGroups[ipKey] = append(manualHostGroups[ipKey], ds...) + manualHostIPs[ipKey] = ips + if _, ok := manualHostNames[ipKey]; !ok { + manualHostNames[ipKey] = domain + } + } + } + } + + // create manual host groups + var ipKeys []string + for k := range manualHostGroups { + ipKeys = append(ipKeys, k) + } + sort.Strings(ipKeys) + + for _, k := range ipKeys { + tag := manualHostNames[k] + geosite = append(geosite, &router.GeoSite{CountryCode: tag, Domain: manualHostGroups[k]}) + hostDeps = append(hostDeps, tag) + hostIPs[tag] = manualHostIPs[k] + + // record tag _ORDER links the matcher to IP addresses + hostPatterns = append(hostPatterns, tag) + } + + deps["HOSTS"] = hostDeps + hostIPs["_ORDER"] = hostPatterns + } + + f, err := os.Create(matcherFilePath) + if err != nil { + return err + } + defer f.Close() + + var buf bytes.Buffer + + if err := router.SerializeGeoSiteList(geosite, deps, hostIPs, &buf); err != nil { + return err + } + + if _, err := f.Write(buf.Bytes()); err != nil { + return err + } + + return nil +} + +// Convert string to Address. +func ParseSendThough(Addr *string) *Address { + var addr Address + addr.Address = net.ParseAddress(strings.Split(*Addr, "/")[0]) + return &addr +} diff --git a/subproject/Xray-core-main/infra/conf/xray_test.go b/subproject/Xray-core-main/infra/conf/xray_test.go new file mode 100644 index 00000000..c1349fd2 --- /dev/null +++ b/subproject/Xray-core-main/infra/conf/xray_test.go @@ -0,0 +1,290 @@ +package conf_test + +import ( + "encoding/json" + "reflect" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/xtls/xray-core/app/dispatcher" + "github.com/xtls/xray-core/app/log" + "github.com/xtls/xray-core/app/proxyman" + "github.com/xtls/xray-core/app/router" + "github.com/xtls/xray-core/common" + clog "github.com/xtls/xray-core/common/log" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/serial" + core "github.com/xtls/xray-core/core" + . "github.com/xtls/xray-core/infra/conf" + "github.com/xtls/xray-core/proxy/vmess" + "github.com/xtls/xray-core/proxy/vmess/inbound" + "github.com/xtls/xray-core/transport/internet" + "github.com/xtls/xray-core/transport/internet/tls" + "github.com/xtls/xray-core/transport/internet/websocket" + "google.golang.org/protobuf/proto" +) + +func TestXrayConfig(t *testing.T) { + createParser := func() func(string) (proto.Message, error) { + return func(s string) (proto.Message, error) { + config := new(Config) + if err := json.Unmarshal([]byte(s), config); err != nil { + return nil, err + } + return config.Build() + } + } + + runMultiTestCase(t, []TestCase{ + { + Input: `{ + "log": { + "access": "/var/log/xray/access.log", + "loglevel": "error", + "error": "/var/log/xray/error.log" + }, + "inbounds": [{ + "streamSettings": { + "network": "ws", + "wsSettings": { + "host": "example.domain", + "path": "" + }, + "tlsSettings": { + "alpn": "h2" + }, + "security": "tls" + }, + "protocol": "vmess", + "port": "443-500", + "settings": { + "clients": [ + { + "security": "aes-128-gcm", + "id": "0cdf8a45-303d-4fed-9780-29aa7f54175e" + } + ] + } + }], + "routing": { + "rules": [ + { + "ip": [ + "10.0.0.0/8" + ], + "outboundTag": "blocked" + } + ] + } + }`, + Parser: createParser(), + Output: &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogType: log.LogType_File, + ErrorLogPath: "/var/log/xray/error.log", + ErrorLogLevel: clog.Severity_Error, + AccessLogType: log.LogType_File, + AccessLogPath: "/var/log/xray/access.log", + }), + serial.ToTypedMessage(&dispatcher.Config{}), + serial.ToTypedMessage(&proxyman.InboundConfig{}), + serial.ToTypedMessage(&proxyman.OutboundConfig{}), + serial.ToTypedMessage(&router.Config{ + DomainStrategy: router.Config_AsIs, + Rule: []*router.RoutingRule{ + { + Geoip: []*router.GeoIP{ + { + Cidr: []*router.CIDR{ + { + Ip: []byte{10, 0, 0, 0}, + Prefix: 8, + }, + }, + }, + }, + TargetTag: &router.RoutingRule_Tag{ + Tag: "blocked", + }, + }, + }, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{{ + From: 443, + To: 500, + }}}, + StreamSettings: &internet.StreamConfig{ + ProtocolName: "websocket", + TransportSettings: []*internet.TransportConfig{ + { + ProtocolName: "websocket", + Settings: serial.ToTypedMessage(&websocket.Config{ + Host: "example.domain", + }), + }, + }, + SecurityType: "xray.transport.internet.tls.Config", + SecuritySettings: []*serial.TypedMessage{ + serial.ToTypedMessage(&tls.Config{ + NextProtocol: []string{"h2"}, + }), + }, + }, + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Level: 0, + Account: serial.ToTypedMessage(&vmess.Account{ + Id: "0cdf8a45-303d-4fed-9780-29aa7f54175e", + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_AES128_GCM, + }, + }), + }, + }, + }), + }, + }, + }, + }, + }) +} + +func TestMuxConfig_Build(t *testing.T) { + tests := []struct { + name string + fields string + want *proxyman.MultiplexingConfig + }{ + {"default", `{"enabled": true, "concurrency": 16}`, &proxyman.MultiplexingConfig{ + Enabled: true, + Concurrency: 16, + XudpConcurrency: 0, + XudpProxyUDP443: "reject", + }}, + {"empty def", `{}`, &proxyman.MultiplexingConfig{ + Enabled: false, + Concurrency: 0, + XudpConcurrency: 0, + XudpProxyUDP443: "reject", + }}, + {"not enable", `{"enabled": false, "concurrency": 4}`, &proxyman.MultiplexingConfig{ + Enabled: false, + Concurrency: 4, + XudpConcurrency: 0, + XudpProxyUDP443: "reject", + }}, + {"forbidden", `{"enabled": false, "concurrency": -1}`, &proxyman.MultiplexingConfig{ + Enabled: false, + Concurrency: -1, + XudpConcurrency: 0, + XudpProxyUDP443: "reject", + }}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := &MuxConfig{} + common.Must(json.Unmarshal([]byte(tt.fields), m)) + if got, _ := m.Build(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("MuxConfig.Build() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestConfig_Override(t *testing.T) { + tests := []struct { + name string + orig *Config + over *Config + fn string + want *Config + }{ + { + "combine/empty", + &Config{}, + &Config{ + LogConfig: &LogConfig{}, + RouterConfig: &RouterConfig{}, + DNSConfig: &DNSConfig{}, + Policy: &PolicyConfig{}, + API: &APIConfig{}, + Stats: &StatsConfig{}, + Reverse: &ReverseConfig{}, + }, + "", + &Config{ + LogConfig: &LogConfig{}, + RouterConfig: &RouterConfig{}, + DNSConfig: &DNSConfig{}, + Policy: &PolicyConfig{}, + API: &APIConfig{}, + Stats: &StatsConfig{}, + Reverse: &ReverseConfig{}, + }, + }, + { + "combine/newattr", + &Config{InboundConfigs: []InboundDetourConfig{{Tag: "old"}}}, + &Config{LogConfig: &LogConfig{}}, "", + &Config{LogConfig: &LogConfig{}, InboundConfigs: []InboundDetourConfig{{Tag: "old"}}}, + }, + { + "replace/inbounds", + &Config{InboundConfigs: []InboundDetourConfig{{Tag: "pos0"}, {Protocol: "vmess", Tag: "pos1"}}}, + &Config{InboundConfigs: []InboundDetourConfig{{Tag: "pos1", Protocol: "kcp"}}}, + "", + &Config{InboundConfigs: []InboundDetourConfig{{Tag: "pos0"}, {Tag: "pos1", Protocol: "kcp"}}}, + }, + { + "replace/inbounds-replaceall", + &Config{InboundConfigs: []InboundDetourConfig{{Tag: "pos0"}, {Protocol: "vmess", Tag: "pos1"}}}, + &Config{InboundConfigs: []InboundDetourConfig{{Tag: "pos1", Protocol: "kcp"}, {Tag: "pos2", Protocol: "kcp"}}}, + "", + &Config{InboundConfigs: []InboundDetourConfig{{Tag: "pos0"}, {Tag: "pos1", Protocol: "kcp"}, {Tag: "pos2", Protocol: "kcp"}}}, + }, + { + "replace/notag-append", + &Config{InboundConfigs: []InboundDetourConfig{{}, {Protocol: "vmess"}}}, + &Config{InboundConfigs: []InboundDetourConfig{{Tag: "pos1", Protocol: "kcp"}}}, + "", + &Config{InboundConfigs: []InboundDetourConfig{{}, {Protocol: "vmess"}, {Tag: "pos1", Protocol: "kcp"}}}, + }, + { + "replace/outbounds", + &Config{OutboundConfigs: []OutboundDetourConfig{{Tag: "pos0"}, {Protocol: "vmess", Tag: "pos1"}}}, + &Config{OutboundConfigs: []OutboundDetourConfig{{Tag: "pos1", Protocol: "kcp"}}}, + "", + &Config{OutboundConfigs: []OutboundDetourConfig{{Tag: "pos0"}, {Tag: "pos1", Protocol: "kcp"}}}, + }, + { + "replace/outbounds-prepend", + &Config{OutboundConfigs: []OutboundDetourConfig{{Tag: "pos0"}, {Protocol: "vmess", Tag: "pos1"}, {Tag: "pos3"}}}, + &Config{OutboundConfigs: []OutboundDetourConfig{{Tag: "pos1", Protocol: "kcp"}, {Tag: "pos2", Protocol: "kcp"}, {Tag: "pos4", Protocol: "kcp"}}}, + "config.json", + &Config{OutboundConfigs: []OutboundDetourConfig{{Tag: "pos2", Protocol: "kcp"}, {Tag: "pos4", Protocol: "kcp"}, {Tag: "pos0"}, {Tag: "pos1", Protocol: "kcp"}, {Tag: "pos3"}}}, + }, + { + "replace/outbounds-append", + &Config{OutboundConfigs: []OutboundDetourConfig{{Tag: "pos0"}, {Protocol: "vmess", Tag: "pos1"}}}, + &Config{OutboundConfigs: []OutboundDetourConfig{{Tag: "pos2", Protocol: "kcp"}}}, + "config_tail.json", + &Config{OutboundConfigs: []OutboundDetourConfig{{Tag: "pos0"}, {Protocol: "vmess", Tag: "pos1"}, {Tag: "pos2", Protocol: "kcp"}}}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.orig.Override(tt.over, tt.fn) + if r := cmp.Diff(tt.orig, tt.want); r != "" { + t.Error(r) + } + }) + } +} diff --git a/subproject/Xray-core-main/infra/vformat/main.go b/subproject/Xray-core-main/infra/vformat/main.go new file mode 100644 index 00000000..84c63a42 --- /dev/null +++ b/subproject/Xray-core-main/infra/vformat/main.go @@ -0,0 +1,193 @@ +package main + +import ( + "errors" + "flag" + "fmt" + "go/build" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" +) + +var directory = flag.String("pwd", "", "Working directory of Xray vformat.") + +// envFile returns the name of the Go environment configuration file. +// Copy from https://github.com/golang/go/blob/c4f2a9788a7be04daf931ac54382fbe2cb754938/src/cmd/go/internal/cfg/cfg.go#L150-L166 +func envFile() (string, error) { + if file := os.Getenv("GOENV"); file != "" { + if file == "off" { + return "", errors.New("GOENV=off") + } + return file, nil + } + dir, err := os.UserConfigDir() + if err != nil { + return "", err + } + if dir == "" { + return "", errors.New("missing user-config dir") + } + return filepath.Join(dir, "go", "env"), nil +} + +// GetRuntimeEnv returns the value of runtime environment variable, +// that is set by running following command: `go env -w key=value`. +func GetRuntimeEnv(key string) (string, error) { + file, err := envFile() + if err != nil { + return "", err + } + if file == "" { + return "", errors.New("missing runtime env file") + } + var data []byte + var runtimeEnv string + data, readErr := os.ReadFile(file) + if readErr != nil { + return "", readErr + } + envStrings := strings.Split(string(data), "\n") + for _, envItem := range envStrings { + envItem = strings.TrimSuffix(envItem, "\r") + envKeyValue := strings.Split(envItem, "=") + if len(envKeyValue) == 2 && strings.TrimSpace(envKeyValue[0]) == key { + runtimeEnv = strings.TrimSpace(envKeyValue[1]) + } + } + return runtimeEnv, nil +} + +// GetGOBIN returns GOBIN environment variable as a string. It will NOT be empty. +func GetGOBIN() string { + // The one set by user explicitly by `export GOBIN=/path` or `env GOBIN=/path command` + GOBIN := os.Getenv("GOBIN") + if GOBIN == "" { + var err error + // The one set by user by running `go env -w GOBIN=/path` + GOBIN, err = GetRuntimeEnv("GOBIN") + if err != nil { + // The default one that Golang uses + return filepath.Join(build.Default.GOPATH, "bin") + } + if GOBIN == "" { + return filepath.Join(build.Default.GOPATH, "bin") + } + return GOBIN + } + return GOBIN +} + +func Run(binary string, args []string) ([]byte, error) { + cmd := exec.Command(binary, args...) + cmd.Env = append(cmd.Env, os.Environ()...) + output, cmdErr := cmd.CombinedOutput() + if cmdErr != nil { + return nil, cmdErr + } + return output, nil +} + +func RunMany(binary string, args, files []string) { + fmt.Println("Processing...") + + maxTasks := make(chan struct{}, runtime.NumCPU()) + for _, file := range files { + maxTasks <- struct{}{} + go func(file string) { + output, err := Run(binary, append(args, file)) + if err != nil { + fmt.Println(err) + } else if len(output) > 0 { + fmt.Println(string(output)) + } + <-maxTasks + }(file) + } +} + +func main() { + flag.Usage = func() { + fmt.Fprintf(flag.CommandLine.Output(), "Usage of vformat:\n") + flag.PrintDefaults() + } + flag.Parse() + + if !filepath.IsAbs(*directory) { + pwd, wdErr := os.Getwd() + if wdErr != nil { + fmt.Println("Can not get current working directory.") + os.Exit(1) + } + *directory = filepath.Join(pwd, *directory) + } + + pwd := *directory + GOBIN := GetGOBIN() + binPath := os.Getenv("PATH") + pathSlice := []string{pwd, GOBIN, binPath} + binPath = strings.Join(pathSlice, string(os.PathListSeparator)) + os.Setenv("PATH", binPath) + + suffix := "" + if runtime.GOOS == "windows" { + suffix = ".exe" + } + gofmt := "gofumpt" + suffix + goimports := "gci" + suffix + + if gofmtPath, err := exec.LookPath(gofmt); err != nil { + fmt.Println("Can not find", gofmt, "in system path or current working directory.") + os.Exit(1) + } else { + gofmt = gofmtPath + } + + if goimportsPath, err := exec.LookPath(goimports); err != nil { + fmt.Println("Can not find", goimports, "in system path or current working directory.") + os.Exit(1) + } else { + goimports = goimportsPath + } + + rawFilesSlice := make([]string, 0, 1000) + walkErr := filepath.Walk(pwd, func(path string, info os.FileInfo, err error) error { + if err != nil { + fmt.Println(err) + return err + } + + if info.IsDir() { + return nil + } + + dir := filepath.Dir(path) + filename := filepath.Base(path) + if strings.HasSuffix(filename, ".go") && + !strings.HasSuffix(filename, ".pb.go") && + !strings.Contains(dir, filepath.Join("testing", "mocks")) && + !strings.Contains(path, filepath.Join("main", "distro", "all", "all.go")) { + rawFilesSlice = append(rawFilesSlice, path) + } + + return nil + }) + if walkErr != nil { + fmt.Println(walkErr) + os.Exit(1) + } + + gofmtArgs := []string{ + "-s", "-l", "-e", "-w", + } + + goimportsArgs := []string{ + "write", + } + + RunMany(gofmt, gofmtArgs, rawFilesSlice) + RunMany(goimports, goimportsArgs, rawFilesSlice) + fmt.Println("Do NOT forget to commit file changes.") +} diff --git a/subproject/Xray-core-main/infra/vprotogen/main.go b/subproject/Xray-core-main/infra/vprotogen/main.go new file mode 100644 index 00000000..2c1e2298 --- /dev/null +++ b/subproject/Xray-core-main/infra/vprotogen/main.go @@ -0,0 +1,259 @@ +package main + +import ( + "errors" + "flag" + "fmt" + "go/build" + "io" + "net/http" + "os" + "os/exec" + "path/filepath" + "regexp" + "runtime" + "strconv" + "strings" +) + +var directory = flag.String("pwd", "", "Working directory of Xray vprotogen.") + +// envFile returns the name of the Go environment configuration file. +// Copy from https://github.com/golang/go/blob/c4f2a9788a7be04daf931ac54382fbe2cb754938/src/cmd/go/internal/cfg/cfg.go#L150-L166 +func envFile() (string, error) { + if file := os.Getenv("GOENV"); file != "" { + if file == "off" { + return "", errors.New("GOENV=off") + } + return file, nil + } + dir, err := os.UserConfigDir() + if err != nil { + return "", err + } + if dir == "" { + return "", errors.New("missing user-config dir") + } + return filepath.Join(dir, "go", "env"), nil +} + +// GetRuntimeEnv returns the value of runtime environment variable, +// that is set by running following command: `go env -w key=value`. +func GetRuntimeEnv(key string) (string, error) { + file, err := envFile() + if err != nil { + return "", err + } + if file == "" { + return "", errors.New("missing runtime env file") + } + var data []byte + var runtimeEnv string + data, readErr := os.ReadFile(file) + if readErr != nil { + return "", readErr + } + envStrings := strings.Split(string(data), "\n") + for _, envItem := range envStrings { + envItem = strings.TrimSuffix(envItem, "\r") + envKeyValue := strings.Split(envItem, "=") + if strings.EqualFold(strings.TrimSpace(envKeyValue[0]), key) { + runtimeEnv = strings.TrimSpace(envKeyValue[1]) + } + } + return runtimeEnv, nil +} + +// GetGOBIN returns GOBIN environment variable as a string. It will NOT be empty. +func GetGOBIN() string { + // The one set by user explicitly by `export GOBIN=/path` or `env GOBIN=/path command` + GOBIN := os.Getenv("GOBIN") + if GOBIN == "" { + var err error + // The one set by user by running `go env -w GOBIN=/path` + GOBIN, err = GetRuntimeEnv("GOBIN") + if err != nil { + // The default one that Golang uses + return filepath.Join(build.Default.GOPATH, "bin") + } + if GOBIN == "" { + return filepath.Join(build.Default.GOPATH, "bin") + } + return GOBIN + } + return GOBIN +} + +func whichProtoc(suffix, targetedVersion string) (string, error) { + protoc := "protoc" + suffix + + path, err := exec.LookPath(protoc) + if err != nil { + return "", fmt.Errorf(` +Command "%s" not found. +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 +`, protoc, protoc, protoc, targetedVersion) + } + return path, nil +} + +func getProjectProtocVersion(url string) (string, error) { + resp, err := http.Get(url) + if err != nil { + return "", errors.New("can not get the version of protobuf used in xray project") + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", errors.New("can not read from body") + } + versionRegexp := regexp.MustCompile(`\/\/\s*protoc\s*v\d+\.(\d+\.\d+)`) + matched := versionRegexp.FindStringSubmatch(string(body)) + return matched[1], nil +} + +func getInstalledProtocVersion(protocPath string) (string, error) { + cmd := exec.Command(protocPath, "--version") + cmd.Env = append(cmd.Env, os.Environ()...) + output, cmdErr := cmd.CombinedOutput() + if cmdErr != nil { + return "", cmdErr + } + versionRegexp := regexp.MustCompile(`protoc\s*(\d+\.\d+)`) + matched := versionRegexp.FindStringSubmatch(string(output)) + return matched[1], nil +} + +func parseVersion(s string, width int) int64 { + strList := strings.Split(s, ".") + format := fmt.Sprintf("%%s%%0%ds", width) + v := "" + for _, value := range strList { + v = fmt.Sprintf(format, v, value) + } + var result int64 + var err error + if result, err = strconv.ParseInt(v, 10, 64); err != nil { + return 0 + } + return result +} + +func needToUpdate(targetedVersion, installedVersion string) bool { + vt := parseVersion(targetedVersion, 4) + vi := parseVersion(installedVersion, 4) + return vt > vi +} + +func main() { + flag.Usage = func() { + fmt.Fprintf(flag.CommandLine.Output(), "Usage of vprotogen:\n") + flag.PrintDefaults() + } + flag.Parse() + + if !filepath.IsAbs(*directory) { + pwd, wdErr := os.Getwd() + if wdErr != nil { + fmt.Println("Can not get current working directory.") + os.Exit(1) + } + *directory = filepath.Join(pwd, *directory) + } + + pwd := *directory + GOBIN := GetGOBIN() + binPath := os.Getenv("PATH") + pathSlice := []string{pwd, GOBIN, binPath} + binPath = strings.Join(pathSlice, string(os.PathListSeparator)) + os.Setenv("PATH", binPath) + + suffix := "" + if runtime.GOOS == "windows" { + suffix = ".exe" + } + + /* + targetedVersion, err := getProjectProtocVersion("https://raw.githubusercontent.com/XTLS/Xray-core/HEAD/core/config.pb.go") + if err != nil { + fmt.Println(err) + os.Exit(1) + } + */ + targetedVersion := "" + + protoc, err := whichProtoc(suffix, targetedVersion) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + installedVersion, err := getInstalledProtocVersion(protoc) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + if needToUpdate(targetedVersion, installedVersion) { + fmt.Printf(` +You are using an old protobuf version, please update to v%s or later. +Download it from https://github.com/protocolbuffers/protobuf/releases + + * Protobuf version used in xray project: v%s + * Protobuf version you have installed: v%s + +`, targetedVersion, targetedVersion, installedVersion) + os.Exit(1) + } + + protoFilesMap := make(map[string][]string) + walkErr := filepath.Walk(pwd, func(path string, info os.FileInfo, err error) error { + if err != nil { + fmt.Println(err) + return err + } + + if info.IsDir() { + return nil + } + + dir := filepath.Dir(path) + filename := filepath.Base(path) + if strings.HasSuffix(filename, ".proto") { + path = path[len(pwd)+1:] + protoFilesMap[dir] = append(protoFilesMap[dir], path) + } + + return nil + }) + if walkErr != nil { + fmt.Println(walkErr) + os.Exit(1) + } + + for _, files := range protoFilesMap { + for _, relProtoFile := range files { + args := []string{ + "--go_out", pwd, + "--go_opt", "paths=source_relative", + "--go-grpc_out", pwd, + "--go-grpc_opt", "paths=source_relative", + "--plugin", "protoc-gen-go=" + filepath.Join(GOBIN, "protoc-gen-go"+suffix), + "--plugin", "protoc-gen-go-grpc=" + filepath.Join(GOBIN, "protoc-gen-go-grpc"+suffix), + } + args = append(args, relProtoFile) + cmd := exec.Command(protoc, args...) + cmd.Env = append(cmd.Env, os.Environ()...) + cmd.Dir = pwd + output, cmdErr := cmd.CombinedOutput() + if len(output) > 0 { + fmt.Println(string(output)) + } + if cmdErr != nil { + fmt.Println(cmdErr) + os.Exit(1) + } + } + } +} diff --git a/subproject/Xray-core-main/proxy/blackhole/blackhole.go b/subproject/Xray-core-main/proxy/blackhole/blackhole.go new file mode 100644 index 00000000..998666cb --- /dev/null +++ b/subproject/Xray-core-main/proxy/blackhole/blackhole.go @@ -0,0 +1,49 @@ +// Package blackhole is an outbound handler that blocks all connections. +package blackhole + +import ( + "context" + "time" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/transport" + "github.com/xtls/xray-core/transport/internet" +) + +// Handler is an outbound connection that silently swallow the entire payload. +type Handler struct { + response ResponseConfig +} + +// New creates a new blackhole handler. +func New(ctx context.Context, config *Config) (*Handler, error) { + response, err := config.GetInternalResponse() + if err != nil { + return nil, err + } + return &Handler{ + response: response, + }, nil +} + +// Process implements OutboundHandler.Dispatch(). +func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer internet.Dialer) error { + outbounds := session.OutboundsFromContext(ctx) + ob := outbounds[len(outbounds)-1] + ob.Name = "blackhole" + + nBytes := h.response.WriteTo(link.Writer) + if nBytes > 0 { + // Sleep a little here to make sure the response is sent to client. + time.Sleep(time.Second) + } + common.Interrupt(link.Writer) + return nil +} + +func init() { + common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + return New(ctx, config.(*Config)) + })) +} diff --git a/subproject/Xray-core-main/proxy/blackhole/blackhole_test.go b/subproject/Xray-core-main/proxy/blackhole/blackhole_test.go new file mode 100644 index 00000000..6a9cb8e8 --- /dev/null +++ b/subproject/Xray-core-main/proxy/blackhole/blackhole_test.go @@ -0,0 +1,42 @@ +package blackhole_test + +import ( + "context" + "testing" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/serial" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/proxy/blackhole" + "github.com/xtls/xray-core/transport" + "github.com/xtls/xray-core/transport/pipe" +) + +func TestBlackholeHTTPResponse(t *testing.T) { + ctx := session.ContextWithOutbounds(context.Background(), []*session.Outbound{{}}) + handler, err := blackhole.New(ctx, &blackhole.Config{ + Response: serial.ToTypedMessage(&blackhole.HTTPResponse{}), + }) + common.Must(err) + + reader, writer := pipe.New(pipe.WithoutSizeLimit()) + + var mb buf.MultiBuffer + var rerr error + go func() { + b, e := reader.ReadMultiBuffer() + mb = b + rerr = e + }() + + link := transport.Link{ + Reader: reader, + Writer: writer, + } + common.Must(handler.Process(ctx, &link, nil)) + common.Must(rerr) + if mb.IsEmpty() { + t.Error("expect http response, but nothing") + } +} diff --git a/subproject/Xray-core-main/proxy/blackhole/config.go b/subproject/Xray-core-main/proxy/blackhole/config.go new file mode 100644 index 00000000..e4a16684 --- /dev/null +++ b/subproject/Xray-core-main/proxy/blackhole/config.go @@ -0,0 +1,47 @@ +package blackhole + +import ( + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" +) + +const ( + http403response = `HTTP/1.1 403 Forbidden +Connection: close +Cache-Control: max-age=3600, public +Content-Length: 0 + + +` +) + +// ResponseConfig is the configuration for blackhole responses. +type ResponseConfig interface { + // WriteTo writes a predefined response to the specified buffer. + WriteTo(buf.Writer) int32 +} + +// WriteTo implements ResponseConfig.WriteTo(). +func (*NoneResponse) WriteTo(buf.Writer) int32 { return 0 } + +// WriteTo implements ResponseConfig.WriteTo(). +func (*HTTPResponse) WriteTo(writer buf.Writer) int32 { + b := buf.New() + common.Must2(b.WriteString(http403response)) + n := b.Len() + writer.WriteMultiBuffer(buf.MultiBuffer{b}) + return n +} + +// GetInternalResponse converts response settings from proto to internal data structure. +func (c *Config) GetInternalResponse() (ResponseConfig, error) { + if c.GetResponse() == nil { + return new(NoneResponse), nil + } + + config, err := c.GetResponse().GetInstance() + if err != nil { + return nil, err + } + return config.(ResponseConfig), nil +} diff --git a/subproject/Xray-core-main/proxy/blackhole/config.pb.go b/subproject/Xray-core-main/proxy/blackhole/config.pb.go new file mode 100644 index 00000000..e7d52f77 --- /dev/null +++ b/subproject/Xray-core-main/proxy/blackhole/config.pb.go @@ -0,0 +1,202 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: proxy/blackhole/config.proto + +package blackhole + +import ( + serial "github.com/xtls/xray-core/common/serial" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type NoneResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NoneResponse) Reset() { + *x = NoneResponse{} + mi := &file_proxy_blackhole_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NoneResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NoneResponse) ProtoMessage() {} + +func (x *NoneResponse) ProtoReflect() protoreflect.Message { + mi := &file_proxy_blackhole_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NoneResponse.ProtoReflect.Descriptor instead. +func (*NoneResponse) Descriptor() ([]byte, []int) { + return file_proxy_blackhole_config_proto_rawDescGZIP(), []int{0} +} + +type HTTPResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HTTPResponse) Reset() { + *x = HTTPResponse{} + mi := &file_proxy_blackhole_config_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HTTPResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HTTPResponse) ProtoMessage() {} + +func (x *HTTPResponse) ProtoReflect() protoreflect.Message { + mi := &file_proxy_blackhole_config_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HTTPResponse.ProtoReflect.Descriptor instead. +func (*HTTPResponse) Descriptor() ([]byte, []int) { + return file_proxy_blackhole_config_proto_rawDescGZIP(), []int{1} +} + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + Response *serial.TypedMessage `protobuf:"bytes,1,opt,name=response,proto3" json:"response,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_proxy_blackhole_config_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_proxy_blackhole_config_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_proxy_blackhole_config_proto_rawDescGZIP(), []int{2} +} + +func (x *Config) GetResponse() *serial.TypedMessage { + if x != nil { + return x.Response + } + return nil +} + +var File_proxy_blackhole_config_proto protoreflect.FileDescriptor + +const file_proxy_blackhole_config_proto_rawDesc = "" + + "\n" + + "\x1cproxy/blackhole/config.proto\x12\x14xray.proxy.blackhole\x1a!common/serial/typed_message.proto\"\x0e\n" + + "\fNoneResponse\"\x0e\n" + + "\fHTTPResponse\"F\n" + + "\x06Config\x12<\n" + + "\bresponse\x18\x01 \x01(\v2 .xray.common.serial.TypedMessageR\bresponseB^\n" + + "\x18com.xray.proxy.blackholeP\x01Z)github.com/xtls/xray-core/proxy/blackhole\xaa\x02\x14Xray.Proxy.Blackholeb\x06proto3" + +var ( + file_proxy_blackhole_config_proto_rawDescOnce sync.Once + file_proxy_blackhole_config_proto_rawDescData []byte +) + +func file_proxy_blackhole_config_proto_rawDescGZIP() []byte { + file_proxy_blackhole_config_proto_rawDescOnce.Do(func() { + file_proxy_blackhole_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proxy_blackhole_config_proto_rawDesc), len(file_proxy_blackhole_config_proto_rawDesc))) + }) + return file_proxy_blackhole_config_proto_rawDescData +} + +var file_proxy_blackhole_config_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_proxy_blackhole_config_proto_goTypes = []any{ + (*NoneResponse)(nil), // 0: xray.proxy.blackhole.NoneResponse + (*HTTPResponse)(nil), // 1: xray.proxy.blackhole.HTTPResponse + (*Config)(nil), // 2: xray.proxy.blackhole.Config + (*serial.TypedMessage)(nil), // 3: xray.common.serial.TypedMessage +} +var file_proxy_blackhole_config_proto_depIdxs = []int32{ + 3, // 0: xray.proxy.blackhole.Config.response:type_name -> xray.common.serial.TypedMessage + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_proxy_blackhole_config_proto_init() } +func file_proxy_blackhole_config_proto_init() { + if File_proxy_blackhole_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_proxy_blackhole_config_proto_rawDesc), len(file_proxy_blackhole_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_proxy_blackhole_config_proto_goTypes, + DependencyIndexes: file_proxy_blackhole_config_proto_depIdxs, + MessageInfos: file_proxy_blackhole_config_proto_msgTypes, + }.Build() + File_proxy_blackhole_config_proto = out.File + file_proxy_blackhole_config_proto_goTypes = nil + file_proxy_blackhole_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/proxy/blackhole/config.proto b/subproject/Xray-core-main/proxy/blackhole/config.proto new file mode 100644 index 00000000..3214c1b5 --- /dev/null +++ b/subproject/Xray-core-main/proxy/blackhole/config.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; + +package xray.proxy.blackhole; +option csharp_namespace = "Xray.Proxy.Blackhole"; +option go_package = "github.com/xtls/xray-core/proxy/blackhole"; +option java_package = "com.xray.proxy.blackhole"; +option java_multiple_files = true; + +import "common/serial/typed_message.proto"; + +message NoneResponse {} + +message HTTPResponse {} + +message Config { + xray.common.serial.TypedMessage response = 1; +} diff --git a/subproject/Xray-core-main/proxy/blackhole/config_test.go b/subproject/Xray-core-main/proxy/blackhole/config_test.go new file mode 100644 index 00000000..f2755b50 --- /dev/null +++ b/subproject/Xray-core-main/proxy/blackhole/config_test.go @@ -0,0 +1,26 @@ +package blackhole_test + +import ( + "bufio" + "net/http" + "testing" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + . "github.com/xtls/xray-core/proxy/blackhole" +) + +func TestHTTPResponse(t *testing.T) { + buffer := buf.New() + + httpResponse := new(HTTPResponse) + httpResponse.WriteTo(buf.NewWriter(buffer)) + + reader := bufio.NewReader(buffer) + response, err := http.ReadResponse(reader, nil) + common.Must(err) + + if response.StatusCode != 403 { + t.Error("expected status code 403, but got ", response.StatusCode) + } +} diff --git a/subproject/Xray-core-main/proxy/dns/config.pb.go b/subproject/Xray-core-main/proxy/dns/config.pb.go new file mode 100644 index 00000000..436d3d38 --- /dev/null +++ b/subproject/Xray-core-main/proxy/dns/config.pb.go @@ -0,0 +1,158 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: proxy/dns/config.proto + +package dns + +import ( + net "github.com/xtls/xray-core/common/net" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Server is the DNS server address. If specified, this address overrides the + // original one. + Server *net.Endpoint `protobuf:"bytes,1,opt,name=server,proto3" json:"server,omitempty"` + UserLevel uint32 `protobuf:"varint,2,opt,name=user_level,json=userLevel,proto3" json:"user_level,omitempty"` + Non_IPQuery string `protobuf:"bytes,3,opt,name=non_IP_query,json=nonIPQuery,proto3" json:"non_IP_query,omitempty"` + BlockTypes []int32 `protobuf:"varint,4,rep,packed,name=block_types,json=blockTypes,proto3" json:"block_types,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_proxy_dns_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_proxy_dns_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_proxy_dns_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Config) GetServer() *net.Endpoint { + if x != nil { + return x.Server + } + return nil +} + +func (x *Config) GetUserLevel() uint32 { + if x != nil { + return x.UserLevel + } + return 0 +} + +func (x *Config) GetNon_IPQuery() string { + if x != nil { + return x.Non_IPQuery + } + return "" +} + +func (x *Config) GetBlockTypes() []int32 { + if x != nil { + return x.BlockTypes + } + return nil +} + +var File_proxy_dns_config_proto protoreflect.FileDescriptor + +const file_proxy_dns_config_proto_rawDesc = "" + + "\n" + + "\x16proxy/dns/config.proto\x12\x0exray.proxy.dns\x1a\x1ccommon/net/destination.proto\"\x9d\x01\n" + + "\x06Config\x121\n" + + "\x06server\x18\x01 \x01(\v2\x19.xray.common.net.EndpointR\x06server\x12\x1d\n" + + "\n" + + "user_level\x18\x02 \x01(\rR\tuserLevel\x12 \n" + + "\fnon_IP_query\x18\x03 \x01(\tR\n" + + "nonIPQuery\x12\x1f\n" + + "\vblock_types\x18\x04 \x03(\x05R\n" + + "blockTypesBL\n" + + "\x12com.xray.proxy.dnsP\x01Z#github.com/xtls/xray-core/proxy/dns\xaa\x02\x0eXray.Proxy.Dnsb\x06proto3" + +var ( + file_proxy_dns_config_proto_rawDescOnce sync.Once + file_proxy_dns_config_proto_rawDescData []byte +) + +func file_proxy_dns_config_proto_rawDescGZIP() []byte { + file_proxy_dns_config_proto_rawDescOnce.Do(func() { + file_proxy_dns_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proxy_dns_config_proto_rawDesc), len(file_proxy_dns_config_proto_rawDesc))) + }) + return file_proxy_dns_config_proto_rawDescData +} + +var file_proxy_dns_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_proxy_dns_config_proto_goTypes = []any{ + (*Config)(nil), // 0: xray.proxy.dns.Config + (*net.Endpoint)(nil), // 1: xray.common.net.Endpoint +} +var file_proxy_dns_config_proto_depIdxs = []int32{ + 1, // 0: xray.proxy.dns.Config.server:type_name -> xray.common.net.Endpoint + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_proxy_dns_config_proto_init() } +func file_proxy_dns_config_proto_init() { + if File_proxy_dns_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_proxy_dns_config_proto_rawDesc), len(file_proxy_dns_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_proxy_dns_config_proto_goTypes, + DependencyIndexes: file_proxy_dns_config_proto_depIdxs, + MessageInfos: file_proxy_dns_config_proto_msgTypes, + }.Build() + File_proxy_dns_config_proto = out.File + file_proxy_dns_config_proto_goTypes = nil + file_proxy_dns_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/proxy/dns/config.proto b/subproject/Xray-core-main/proxy/dns/config.proto new file mode 100644 index 00000000..af2aad8c --- /dev/null +++ b/subproject/Xray-core-main/proxy/dns/config.proto @@ -0,0 +1,18 @@ +syntax = "proto3"; + +package xray.proxy.dns; +option csharp_namespace = "Xray.Proxy.Dns"; +option go_package = "github.com/xtls/xray-core/proxy/dns"; +option java_package = "com.xray.proxy.dns"; +option java_multiple_files = true; + +import "common/net/destination.proto"; + +message Config { + // Server is the DNS server address. If specified, this address overrides the + // original one. + xray.common.net.Endpoint server = 1; + uint32 user_level = 2; + string non_IP_query = 3; + repeated int32 block_types = 4; +} diff --git a/subproject/Xray-core-main/proxy/dns/dns.go b/subproject/Xray-core-main/proxy/dns/dns.go new file mode 100644 index 00000000..9ae19cbe --- /dev/null +++ b/subproject/Xray-core-main/proxy/dns/dns.go @@ -0,0 +1,457 @@ +package dns + +import ( + "context" + go_errors "errors" + "io" + "strings" + "sync" + "time" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + dns_proto "github.com/xtls/xray-core/common/protocol/dns" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/common/signal" + "github.com/xtls/xray-core/common/task" + "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/features/dns" + "github.com/xtls/xray-core/features/policy" + "github.com/xtls/xray-core/transport" + "github.com/xtls/xray-core/transport/internet" + "github.com/xtls/xray-core/transport/internet/stat" + "golang.org/x/net/dns/dnsmessage" +) + +func init() { + common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + h := new(Handler) + if err := core.RequireFeatures(ctx, func(dnsClient dns.Client, policyManager policy.Manager) error { + core.OptionalFeatures(ctx, func(fdns dns.FakeDNSEngine) { + h.fdns = fdns + }) + return h.Init(config.(*Config), dnsClient, policyManager) + }); err != nil { + return nil, err + } + return h, nil + })) +} + +type ownLinkVerifier interface { + IsOwnLink(ctx context.Context) bool +} + +type Handler struct { + client dns.Client + fdns dns.FakeDNSEngine + ownLinkVerifier ownLinkVerifier + server net.Destination + timeout time.Duration + nonIPQuery string + blockTypes []int32 +} + +func (h *Handler) Init(config *Config, dnsClient dns.Client, policyManager policy.Manager) error { + h.client = dnsClient + h.timeout = policyManager.ForLevel(config.UserLevel).Timeouts.ConnectionIdle + + if v, ok := dnsClient.(ownLinkVerifier); ok { + h.ownLinkVerifier = v + } + + if config.Server != nil { + h.server = config.Server.AsDestination() + } + h.nonIPQuery = config.Non_IPQuery + if h.nonIPQuery == "" { + h.nonIPQuery = "reject" + } + h.blockTypes = config.BlockTypes + return nil +} + +func (h *Handler) isOwnLink(ctx context.Context) bool { + return h.ownLinkVerifier != nil && h.ownLinkVerifier.IsOwnLink(ctx) +} + +func parseIPQuery(b []byte) (r bool, domain string, id uint16, qType dnsmessage.Type) { + var parser dnsmessage.Parser + header, err := parser.Start(b) + if err != nil { + errors.LogInfoInner(context.Background(), err, "parser start") + return + } + + id = header.ID + q, err := parser.Question() + if err != nil { + errors.LogInfoInner(context.Background(), err, "question") + return + } + domain = q.Name.String() + qType = q.Type + if qType != dnsmessage.TypeA && qType != dnsmessage.TypeAAAA { + return + } + + r = true + return +} + +// Process implements proxy.Outbound. +func (h *Handler) Process(ctx context.Context, link *transport.Link, d internet.Dialer) error { + outbounds := session.OutboundsFromContext(ctx) + ob := outbounds[len(outbounds)-1] + if !ob.Target.IsValid() { + return errors.New("invalid outbound") + } + ob.Name = "dns" + + srcNetwork := ob.Target.Network + + dest := ob.Target + if h.server.Network != net.Network_Unknown { + dest.Network = h.server.Network + } + if h.server.Address != nil { + dest.Address = h.server.Address + } + if h.server.Port != 0 { + dest.Port = h.server.Port + } + + errors.LogInfo(ctx, "handling DNS traffic to ", dest) + + conn := &outboundConn{ + dialer: func() (stat.Connection, error) { + return d.Dial(ctx, dest) + }, + connReady: make(chan struct{}, 1), + } + + var reader dns_proto.MessageReader + var writer dns_proto.MessageWriter + if srcNetwork == net.Network_TCP { + reader = dns_proto.NewTCPReader(link.Reader) + writer = &dns_proto.TCPWriter{ + Writer: link.Writer, + } + } else { + reader = &dns_proto.UDPReader{ + Reader: link.Reader, + } + writer = &dns_proto.UDPWriter{ + Writer: link.Writer, + } + } + + var connReader dns_proto.MessageReader + var connWriter dns_proto.MessageWriter + if dest.Network == net.Network_TCP { + connReader = dns_proto.NewTCPReader(buf.NewReader(conn)) + connWriter = &dns_proto.TCPWriter{ + Writer: buf.NewWriter(conn), + } + } else { + connReader = &dns_proto.UDPReader{ + Reader: buf.NewPacketReader(conn), + } + connWriter = &dns_proto.UDPWriter{ + Writer: buf.NewWriter(conn), + } + } + + if session.TimeoutOnlyFromContext(ctx) { + ctx, _ = context.WithCancel(context.Background()) + } + + ctx, cancel := context.WithCancel(ctx) + terminate := func() { + cancel() + conn.Close() + } + timer := signal.CancelAfterInactivity(ctx, terminate, h.timeout) + defer timer.SetTimeout(0) + + request := func() error { + defer timer.SetTimeout(0) + for { + b, err := reader.ReadMessage() + if err == io.EOF { + return nil + } + + if err != nil { + return err + } + + timer.Update() + + if !h.isOwnLink(ctx) { + isIPQuery, domain, id, qType := parseIPQuery(b.Bytes()) + if len(h.blockTypes) > 0 { + for _, blocktype := range h.blockTypes { + if blocktype == int32(qType) { + b.Release() + errors.LogInfo(ctx, "blocked type ", qType, " query for domain ", domain) + if h.nonIPQuery == "reject" { + err := h.rejectNonIPQuery(id, qType, domain, writer) + if err != nil { + return err + } + } + return nil + } + } + } + if isIPQuery { + b.Release() + go h.handleIPQuery(id, qType, domain, writer, timer) + continue + } + if h.nonIPQuery == "drop" { + b.Release() + continue + } + if h.nonIPQuery == "reject" { + b.Release() + err := h.rejectNonIPQuery(id, qType, domain, writer) + if err != nil { + return err + } + continue + } + } + + if err := connWriter.WriteMessage(b); err != nil { + return err + } + } + } + + response := func() error { + defer timer.SetTimeout(0) + for { + b, err := connReader.ReadMessage() + if err == io.EOF { + return nil + } + + if err != nil { + return err + } + + timer.Update() + + if err := writer.WriteMessage(b); err != nil { + return err + } + } + } + + if err := task.Run(ctx, request, response); err != nil { + return errors.New("connection ends").Base(err) + } + + return nil +} + +func (h *Handler) handleIPQuery(id uint16, qType dnsmessage.Type, domain string, writer dns_proto.MessageWriter, timer *signal.ActivityTimer) { + var ips []net.IP + var err error + + var ttl4 uint32 + var ttl6 uint32 + + switch qType { + case dnsmessage.TypeA: + ips, ttl4, err = h.client.LookupIP(domain, dns.IPOption{ + IPv4Enable: true, + IPv6Enable: false, + FakeEnable: true, + }) + case dnsmessage.TypeAAAA: + ips, ttl6, err = h.client.LookupIP(domain, dns.IPOption{ + IPv4Enable: false, + IPv6Enable: true, + FakeEnable: true, + }) + } + + rcode := dns.RCodeFromError(err) + if rcode == 0 && len(ips) == 0 && !go_errors.Is(err, dns.ErrEmptyResponse) { + errors.LogInfoInner(context.Background(), err, "ip query") + return + } + + switch qType { + case dnsmessage.TypeA: + for i, ip := range ips { + ips[i] = ip.To4() + } + case dnsmessage.TypeAAAA: + for i, ip := range ips { + ips[i] = ip.To16() + } + } + + b := buf.New() + rawBytes := b.Extend(buf.Size) + builder := dnsmessage.NewBuilder(rawBytes[:0], dnsmessage.Header{ + ID: id, + RCode: dnsmessage.RCode(rcode), + RecursionAvailable: true, + RecursionDesired: true, + Response: true, + Authoritative: true, + }) + builder.EnableCompression() + common.Must(builder.StartQuestions()) + common.Must(builder.Question(dnsmessage.Question{ + Name: dnsmessage.MustNewName(domain), + Class: dnsmessage.ClassINET, + Type: qType, + })) + common.Must(builder.StartAnswers()) + + rHeader4 := dnsmessage.ResourceHeader{Name: dnsmessage.MustNewName(domain), Class: dnsmessage.ClassINET, TTL: ttl4} + rHeader6 := dnsmessage.ResourceHeader{Name: dnsmessage.MustNewName(domain), Class: dnsmessage.ClassINET, TTL: ttl6} + for _, ip := range ips { + if len(ip) == net.IPv4len { + var r dnsmessage.AResource + copy(r.A[:], ip) + common.Must(builder.AResource(rHeader4, r)) + } else { + var r dnsmessage.AAAAResource + copy(r.AAAA[:], ip) + common.Must(builder.AAAAResource(rHeader6, r)) + } + } + msgBytes, err := builder.Finish() + if err != nil { + errors.LogInfoInner(context.Background(), err, "pack message") + b.Release() + timer.SetTimeout(0) + } + b.Resize(0, int32(len(msgBytes))) + + if err := writer.WriteMessage(b); err != nil { + errors.LogInfoInner(context.Background(), err, "write IP answer") + timer.SetTimeout(0) + } +} + +func (h *Handler) rejectNonIPQuery(id uint16, qType dnsmessage.Type, domain string, writer dns_proto.MessageWriter) error { + domainT := strings.TrimSuffix(domain, ".") + if domainT == "" { + return errors.New("empty domain name") + } + b := buf.New() + rawBytes := b.Extend(buf.Size) + builder := dnsmessage.NewBuilder(rawBytes[:0], dnsmessage.Header{ + ID: id, + RCode: dnsmessage.RCodeRefused, + RecursionAvailable: true, + RecursionDesired: true, + Response: true, + Authoritative: true, + }) + builder.EnableCompression() + common.Must(builder.StartQuestions()) + err := builder.Question(dnsmessage.Question{ + Name: dnsmessage.MustNewName(domain), + Class: dnsmessage.ClassINET, + Type: qType, + }) + if err != nil { + errors.LogInfo(context.Background(), "unexpected domain ", domain, " when building reject message: ", err) + b.Release() + return err + } + + msgBytes, err := builder.Finish() + if err != nil { + errors.LogInfoInner(context.Background(), err, "pack reject message") + b.Release() + return err + } + b.Resize(0, int32(len(msgBytes))) + + if err := writer.WriteMessage(b); err != nil { + errors.LogInfoInner(context.Background(), err, "write reject answer") + return err + } + return nil +} + +type outboundConn struct { + access sync.Mutex + dialer func() (stat.Connection, error) + + conn net.Conn + connReady chan struct{} + closed bool +} + +func (c *outboundConn) dial() error { + conn, err := c.dialer() + if err != nil { + return err + } + c.conn = conn + c.connReady <- struct{}{} + return nil +} + +func (c *outboundConn) Write(b []byte) (int, error) { + c.access.Lock() + if c.closed { + c.access.Unlock() + return 0, errors.New("outbound connection closed") + } + + if c.conn == nil { + if err := c.dial(); err != nil { + c.access.Unlock() + errors.LogWarningInner(context.Background(), err, "failed to dial outbound connection") + return 0, err + } + } + + c.access.Unlock() + + return c.conn.Write(b) +} + +func (c *outboundConn) Read(b []byte) (int, error) { + c.access.Lock() + if c.closed { + c.access.Unlock() + return 0, io.EOF + } + + if c.conn == nil { + c.access.Unlock() + _, open := <-c.connReady + if !open { + return 0, io.EOF + } + return c.conn.Read(b) + } + c.access.Unlock() + return c.conn.Read(b) +} + +func (c *outboundConn) Close() error { + c.access.Lock() + c.closed = true + close(c.connReady) + if c.conn != nil { + c.conn.Close() + } + c.access.Unlock() + return nil +} diff --git a/subproject/Xray-core-main/proxy/dns/dns_test.go b/subproject/Xray-core-main/proxy/dns/dns_test.go new file mode 100644 index 00000000..2a005d68 --- /dev/null +++ b/subproject/Xray-core-main/proxy/dns/dns_test.go @@ -0,0 +1,370 @@ +package dns_test + +import ( + "strconv" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/miekg/dns" + "github.com/xtls/xray-core/app/dispatcher" + dnsapp "github.com/xtls/xray-core/app/dns" + "github.com/xtls/xray-core/app/policy" + "github.com/xtls/xray-core/app/proxyman" + _ "github.com/xtls/xray-core/app/proxyman/inbound" + _ "github.com/xtls/xray-core/app/proxyman/outbound" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/serial" + "github.com/xtls/xray-core/core" + dns_proxy "github.com/xtls/xray-core/proxy/dns" + "github.com/xtls/xray-core/proxy/dokodemo" + "github.com/xtls/xray-core/testing/servers/tcp" + "github.com/xtls/xray-core/testing/servers/udp" +) + +type staticHandler struct{} + +func (*staticHandler) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { + ans := new(dns.Msg) + ans.Id = r.Id + + var clientIP net.IP + + opt := r.IsEdns0() + if opt != nil { + for _, o := range opt.Option { + if o.Option() == dns.EDNS0SUBNET { + subnet := o.(*dns.EDNS0_SUBNET) + clientIP = subnet.Address + } + } + } + + for _, q := range r.Question { + switch { + case q.Name == "google.com." && q.Qtype == dns.TypeA: + if clientIP == nil { + rr, _ := dns.NewRR("google.com. IN A 8.8.8.8") + ans.Answer = append(ans.Answer, rr) + } else { + rr, _ := dns.NewRR("google.com. IN A 8.8.4.4") + ans.Answer = append(ans.Answer, rr) + } + + case q.Name == "facebook.com." && q.Qtype == dns.TypeA: + rr, _ := dns.NewRR("facebook.com. IN A 9.9.9.9") + ans.Answer = append(ans.Answer, rr) + + case q.Name == "ipv6.google.com." && q.Qtype == dns.TypeA: + rr, err := dns.NewRR("ipv6.google.com. IN A 8.8.8.7") + common.Must(err) + ans.Answer = append(ans.Answer, rr) + + case q.Name == "ipv6.google.com." && q.Qtype == dns.TypeAAAA: + rr, err := dns.NewRR("ipv6.google.com. IN AAAA 2001:4860:4860::8888") + common.Must(err) + ans.Answer = append(ans.Answer, rr) + + case q.Name == "notexist.google.com." && q.Qtype == dns.TypeAAAA: + ans.MsgHdr.Rcode = dns.RcodeNameError + } + } + w.WriteMsg(ans) +} + +func TestUDPDNSTunnel(t *testing.T) { + port := udp.PickPort() + + dnsServer := dns.Server{ + Addr: "127.0.0.1:" + port.String(), + Net: "udp", + Handler: &staticHandler{}, + UDPSize: 1200, + } + defer dnsServer.Shutdown() + + go dnsServer.ListenAndServe() + time.Sleep(time.Second) + + serverPort := udp.PickPort() + config := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&dnsapp.Config{ + NameServer: []*dnsapp.NameServer{ + { + Address: &net.Endpoint{ + Network: net.Network_UDP, + Address: &net.IPOrDomain{ + Address: &net.IPOrDomain_Ip{ + Ip: []byte{127, 0, 0, 1}, + }, + }, + Port: uint32(port), + }, + }, + }, + }), + serial.ToTypedMessage(&dispatcher.Config{}), + serial.ToTypedMessage(&proxyman.OutboundConfig{}), + serial.ToTypedMessage(&proxyman.InboundConfig{}), + serial.ToTypedMessage(&policy.Config{}), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(port), + Networks: []net.Network{net.Network_UDP}, + }), + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&dns_proxy.Config{}), + }, + }, + } + + v, err := core.New(config) + common.Must(err) + common.Must(v.Start()) + defer v.Close() + + { + m1 := new(dns.Msg) + m1.Id = dns.Id() + m1.RecursionDesired = true + m1.Question = make([]dns.Question, 1) + m1.Question[0] = dns.Question{Name: "google.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET} + + c := new(dns.Client) + in, _, err := c.Exchange(m1, "127.0.0.1:"+strconv.Itoa(int(serverPort))) + common.Must(err) + + if len(in.Answer) != 1 { + t.Fatal("len(answer): ", len(in.Answer)) + } + + rr, ok := in.Answer[0].(*dns.A) + if !ok { + t.Fatal("not A record") + } + if r := cmp.Diff(rr.A[:], net.IP{8, 8, 8, 8}); r != "" { + t.Error(r) + } + } + + { + m1 := new(dns.Msg) + m1.Id = dns.Id() + m1.RecursionDesired = true + m1.Question = make([]dns.Question, 1) + m1.Question[0] = dns.Question{Name: "ipv4only.google.com.", Qtype: dns.TypeAAAA, Qclass: dns.ClassINET} + + c := new(dns.Client) + c.Timeout = 10 * time.Second + in, _, err := c.Exchange(m1, "127.0.0.1:"+strconv.Itoa(int(serverPort))) + common.Must(err) + + if len(in.Answer) != 0 { + t.Fatal("len(answer): ", len(in.Answer)) + } + } + + { + m1 := new(dns.Msg) + m1.Id = dns.Id() + m1.RecursionDesired = true + m1.Question = make([]dns.Question, 1) + m1.Question[0] = dns.Question{Name: "notexist.google.com.", Qtype: dns.TypeAAAA, Qclass: dns.ClassINET} + + c := new(dns.Client) + in, _, err := c.Exchange(m1, "127.0.0.1:"+strconv.Itoa(int(serverPort))) + common.Must(err) + + if in.Rcode != dns.RcodeNameError { + t.Error("expected NameError, but got ", in.Rcode) + } + } +} + +func TestTCPDNSTunnel(t *testing.T) { + port := udp.PickPort() + + dnsServer := dns.Server{ + Addr: "127.0.0.1:" + port.String(), + Net: "udp", + Handler: &staticHandler{}, + } + defer dnsServer.Shutdown() + + go dnsServer.ListenAndServe() + time.Sleep(time.Second) + + serverPort := tcp.PickPort() + config := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&dnsapp.Config{ + NameServer: []*dnsapp.NameServer{ + { + Address: &net.Endpoint{ + Network: net.Network_UDP, + Address: &net.IPOrDomain{ + Address: &net.IPOrDomain_Ip{ + Ip: []byte{127, 0, 0, 1}, + }, + }, + Port: uint32(port), + }, + }, + }, + }), + serial.ToTypedMessage(&dispatcher.Config{}), + serial.ToTypedMessage(&proxyman.OutboundConfig{}), + serial.ToTypedMessage(&proxyman.InboundConfig{}), + serial.ToTypedMessage(&policy.Config{}), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(port), + Networks: []net.Network{net.Network_TCP}, + }), + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&dns_proxy.Config{}), + }, + }, + } + + v, err := core.New(config) + common.Must(err) + common.Must(v.Start()) + defer v.Close() + + m1 := new(dns.Msg) + m1.Id = dns.Id() + m1.RecursionDesired = true + m1.Question = make([]dns.Question, 1) + m1.Question[0] = dns.Question{Name: "google.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET} + + c := &dns.Client{ + Net: "tcp", + } + in, _, err := c.Exchange(m1, "127.0.0.1:"+serverPort.String()) + common.Must(err) + + if len(in.Answer) != 1 { + t.Fatal("len(answer): ", len(in.Answer)) + } + + rr, ok := in.Answer[0].(*dns.A) + if !ok { + t.Fatal("not A record") + } + if r := cmp.Diff(rr.A[:], net.IP{8, 8, 8, 8}); r != "" { + t.Error(r) + } +} + +func TestUDP2TCPDNSTunnel(t *testing.T) { + port := tcp.PickPort() + + dnsServer := dns.Server{ + Addr: "127.0.0.1:" + port.String(), + Net: "tcp", + Handler: &staticHandler{}, + } + defer dnsServer.Shutdown() + + go dnsServer.ListenAndServe() + time.Sleep(time.Second) + + serverPort := tcp.PickPort() + config := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&dnsapp.Config{ + NameServer: []*dnsapp.NameServer{ + { + Address: &net.Endpoint{ + Network: net.Network_UDP, + Address: &net.IPOrDomain{ + Address: &net.IPOrDomain_Ip{ + Ip: []byte{127, 0, 0, 1}, + }, + }, + Port: uint32(port), + }, + }, + }, + }), + serial.ToTypedMessage(&dispatcher.Config{}), + serial.ToTypedMessage(&proxyman.OutboundConfig{}), + serial.ToTypedMessage(&proxyman.InboundConfig{}), + serial.ToTypedMessage(&policy.Config{}), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(port), + Networks: []net.Network{net.Network_TCP}, + }), + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&dns_proxy.Config{ + Server: &net.Endpoint{ + Network: net.Network_TCP, + }, + }), + }, + }, + } + + v, err := core.New(config) + common.Must(err) + common.Must(v.Start()) + defer v.Close() + + m1 := new(dns.Msg) + m1.Id = dns.Id() + m1.RecursionDesired = true + m1.Question = make([]dns.Question, 1) + m1.Question[0] = dns.Question{Name: "google.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET} + + c := &dns.Client{ + Net: "tcp", + } + in, _, err := c.Exchange(m1, "127.0.0.1:"+serverPort.String()) + common.Must(err) + + if len(in.Answer) != 1 { + t.Fatal("len(answer): ", len(in.Answer)) + } + + rr, ok := in.Answer[0].(*dns.A) + if !ok { + t.Fatal("not A record") + } + if r := cmp.Diff(rr.A[:], net.IP{8, 8, 8, 8}); r != "" { + t.Error(r) + } +} diff --git a/subproject/Xray-core-main/proxy/dokodemo/config.go b/subproject/Xray-core-main/proxy/dokodemo/config.go new file mode 100644 index 00000000..bb367051 --- /dev/null +++ b/subproject/Xray-core-main/proxy/dokodemo/config.go @@ -0,0 +1,14 @@ +package dokodemo + +import ( + "github.com/xtls/xray-core/common/net" +) + +// GetPredefinedAddress returns the defined address from proto config. Null if address is not valid. +func (v *Config) GetPredefinedAddress() net.Address { + addr := v.Address.AsAddress() + if addr == nil { + return nil + } + return addr +} diff --git a/subproject/Xray-core-main/proxy/dokodemo/config.pb.go b/subproject/Xray-core-main/proxy/dokodemo/config.pb.go new file mode 100644 index 00000000..37e3c9db --- /dev/null +++ b/subproject/Xray-core-main/proxy/dokodemo/config.pb.go @@ -0,0 +1,180 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: proxy/dokodemo/config.proto + +package dokodemo + +import ( + net "github.com/xtls/xray-core/common/net" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + Address *net.IPOrDomain `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"` + Port uint32 `protobuf:"varint,2,opt,name=port,proto3" json:"port,omitempty"` + PortMap map[string]string `protobuf:"bytes,3,rep,name=port_map,json=portMap,proto3" json:"port_map,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // List of networks that the Dokodemo accepts. + Networks []net.Network `protobuf:"varint,7,rep,packed,name=networks,proto3,enum=xray.common.net.Network" json:"networks,omitempty"` + FollowRedirect bool `protobuf:"varint,5,opt,name=follow_redirect,json=followRedirect,proto3" json:"follow_redirect,omitempty"` + UserLevel uint32 `protobuf:"varint,6,opt,name=user_level,json=userLevel,proto3" json:"user_level,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_proxy_dokodemo_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_proxy_dokodemo_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_proxy_dokodemo_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Config) GetAddress() *net.IPOrDomain { + if x != nil { + return x.Address + } + return nil +} + +func (x *Config) GetPort() uint32 { + if x != nil { + return x.Port + } + return 0 +} + +func (x *Config) GetPortMap() map[string]string { + if x != nil { + return x.PortMap + } + return nil +} + +func (x *Config) GetNetworks() []net.Network { + if x != nil { + return x.Networks + } + return nil +} + +func (x *Config) GetFollowRedirect() bool { + if x != nil { + return x.FollowRedirect + } + return false +} + +func (x *Config) GetUserLevel() uint32 { + if x != nil { + return x.UserLevel + } + return 0 +} + +var File_proxy_dokodemo_config_proto protoreflect.FileDescriptor + +const file_proxy_dokodemo_config_proto_rawDesc = "" + + "\n" + + "\x1bproxy/dokodemo/config.proto\x12\x13xray.proxy.dokodemo\x1a\x18common/net/address.proto\x1a\x18common/net/network.proto\"\xd2\x02\n" + + "\x06Config\x125\n" + + "\aaddress\x18\x01 \x01(\v2\x1b.xray.common.net.IPOrDomainR\aaddress\x12\x12\n" + + "\x04port\x18\x02 \x01(\rR\x04port\x12C\n" + + "\bport_map\x18\x03 \x03(\v2(.xray.proxy.dokodemo.Config.PortMapEntryR\aportMap\x124\n" + + "\bnetworks\x18\a \x03(\x0e2\x18.xray.common.net.NetworkR\bnetworks\x12'\n" + + "\x0ffollow_redirect\x18\x05 \x01(\bR\x0efollowRedirect\x12\x1d\n" + + "\n" + + "user_level\x18\x06 \x01(\rR\tuserLevel\x1a:\n" + + "\fPortMapEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B[\n" + + "\x17com.xray.proxy.dokodemoP\x01Z(github.com/xtls/xray-core/proxy/dokodemo\xaa\x02\x13Xray.Proxy.Dokodemob\x06proto3" + +var ( + file_proxy_dokodemo_config_proto_rawDescOnce sync.Once + file_proxy_dokodemo_config_proto_rawDescData []byte +) + +func file_proxy_dokodemo_config_proto_rawDescGZIP() []byte { + file_proxy_dokodemo_config_proto_rawDescOnce.Do(func() { + file_proxy_dokodemo_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proxy_dokodemo_config_proto_rawDesc), len(file_proxy_dokodemo_config_proto_rawDesc))) + }) + return file_proxy_dokodemo_config_proto_rawDescData +} + +var file_proxy_dokodemo_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_proxy_dokodemo_config_proto_goTypes = []any{ + (*Config)(nil), // 0: xray.proxy.dokodemo.Config + nil, // 1: xray.proxy.dokodemo.Config.PortMapEntry + (*net.IPOrDomain)(nil), // 2: xray.common.net.IPOrDomain + (net.Network)(0), // 3: xray.common.net.Network +} +var file_proxy_dokodemo_config_proto_depIdxs = []int32{ + 2, // 0: xray.proxy.dokodemo.Config.address:type_name -> xray.common.net.IPOrDomain + 1, // 1: xray.proxy.dokodemo.Config.port_map:type_name -> xray.proxy.dokodemo.Config.PortMapEntry + 3, // 2: xray.proxy.dokodemo.Config.networks:type_name -> xray.common.net.Network + 3, // [3:3] is the sub-list for method output_type + 3, // [3:3] is the sub-list for method input_type + 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_proxy_dokodemo_config_proto_init() } +func file_proxy_dokodemo_config_proto_init() { + if File_proxy_dokodemo_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_proxy_dokodemo_config_proto_rawDesc), len(file_proxy_dokodemo_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_proxy_dokodemo_config_proto_goTypes, + DependencyIndexes: file_proxy_dokodemo_config_proto_depIdxs, + MessageInfos: file_proxy_dokodemo_config_proto_msgTypes, + }.Build() + File_proxy_dokodemo_config_proto = out.File + file_proxy_dokodemo_config_proto_goTypes = nil + file_proxy_dokodemo_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/proxy/dokodemo/config.proto b/subproject/Xray-core-main/proxy/dokodemo/config.proto new file mode 100644 index 00000000..42008932 --- /dev/null +++ b/subproject/Xray-core-main/proxy/dokodemo/config.proto @@ -0,0 +1,23 @@ +syntax = "proto3"; + +package xray.proxy.dokodemo; +option csharp_namespace = "Xray.Proxy.Dokodemo"; +option go_package = "github.com/xtls/xray-core/proxy/dokodemo"; +option java_package = "com.xray.proxy.dokodemo"; +option java_multiple_files = true; + +import "common/net/address.proto"; +import "common/net/network.proto"; + +message Config { + xray.common.net.IPOrDomain address = 1; + uint32 port = 2; + + map port_map = 3; + + // List of networks that the Dokodemo accepts. + repeated xray.common.net.Network networks = 7; + + bool follow_redirect = 5; + uint32 user_level = 6; +} diff --git a/subproject/Xray-core-main/proxy/dokodemo/dokodemo.go b/subproject/Xray-core-main/proxy/dokodemo/dokodemo.go new file mode 100644 index 00000000..e56363db --- /dev/null +++ b/subproject/Xray-core-main/proxy/dokodemo/dokodemo.go @@ -0,0 +1,263 @@ +package dokodemo + +import ( + "context" + "strconv" + "strings" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/log" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/features/policy" + "github.com/xtls/xray-core/features/routing" + "github.com/xtls/xray-core/transport" + "github.com/xtls/xray-core/transport/internet/stat" + "github.com/xtls/xray-core/transport/internet/tls" +) + +func init() { + common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + d := new(DokodemoDoor) + err := core.RequireFeatures(ctx, func(pm policy.Manager) error { + return d.Init(config.(*Config), pm, session.SockoptFromContext(ctx)) + }) + return d, err + })) +} + +type DokodemoDoor struct { + policyManager policy.Manager + config *Config + address net.Address + port net.Port + portMap map[string]string + sockopt *session.Sockopt +} + +// Init initializes the DokodemoDoor instance with necessary parameters. +func (d *DokodemoDoor) Init(config *Config, pm policy.Manager, sockopt *session.Sockopt) error { + if len(config.Networks) == 0 { + return errors.New("no network specified") + } + d.config = config + d.address = config.GetPredefinedAddress() + d.port = net.Port(config.Port) + d.portMap = config.PortMap + d.policyManager = pm + d.sockopt = sockopt + + return nil +} + +// Network implements proxy.Inbound. +func (d *DokodemoDoor) Network() []net.Network { + return d.config.Networks +} + +func (d *DokodemoDoor) policy() policy.Session { + config := d.config + p := d.policyManager.ForLevel(config.UserLevel) + return p +} + +// Process implements proxy.Inbound. +func (d *DokodemoDoor) Process(ctx context.Context, network net.Network, conn stat.Connection, dispatcher routing.Dispatcher) error { + errors.LogDebug(ctx, "processing connection from: ", conn.RemoteAddr()) + dest := net.Destination{ + Network: network, + Address: d.address, + Port: d.port, + } + + if !d.config.FollowRedirect { + host, port, err := net.SplitHostPort(conn.LocalAddr().String()) + if dest.Address == nil { + if err != nil { + dest.Address = net.DomainAddress("localhost") + } else { + if strings.Contains(host, ".") { + dest.Address = net.LocalHostIP + } else { + dest.Address = net.LocalHostIPv6 + } + } + } + if dest.Port == 0 { + dest.Port = net.Port(common.Must2(strconv.Atoi(port))) + } + if d.portMap != nil && d.portMap[port] != "" { + h, p, _ := net.SplitHostPort(d.portMap[port]) + if len(h) > 0 { + dest.Address = net.ParseAddress(h) + } + if len(p) > 0 { + dest.Port = net.Port(common.Must2(strconv.Atoi(p))) + } + } + } + + destinationOverridden := false + if d.config.FollowRedirect { + outbounds := session.OutboundsFromContext(ctx) + if len(outbounds) > 0 { + ob := outbounds[len(outbounds)-1] + if ob.Target.IsValid() { + dest = ob.Target + destinationOverridden = true + } + } + iConn := stat.TryUnwrapStatsConn(conn) + if tlsConn, ok := iConn.(tls.Interface); ok && !destinationOverridden { + if serverName := tlsConn.HandshakeContextServerName(ctx); serverName != "" { + dest.Address = net.DomainAddress(serverName) + destinationOverridden = true + ctx = session.ContextWithMitmServerName(ctx, serverName) + } + if tlsConn.NegotiatedProtocol() != "h2" { + ctx = session.ContextWithMitmAlpn11(ctx, true) + } + } + } + if !dest.IsValid() || dest.Address == nil { + return errors.New("unable to get destination") + } + + inbound := session.InboundFromContext(ctx) + inbound.Name = "dokodemo-door" + inbound.CanSpliceCopy = 1 + inbound.User = &protocol.MemoryUser{ + Level: d.config.UserLevel, + } + + ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{ + From: conn.RemoteAddr(), + To: dest, + Status: log.AccessAccepted, + Reason: "", + }) + errors.LogInfo(ctx, "received request for ", conn.RemoteAddr()) + + var reader buf.Reader + if dest.Network == net.Network_TCP { + reader = buf.NewReader(conn) + } else { + reader = buf.NewPacketReader(conn) + } + + var writer buf.Writer + if network == net.Network_TCP { + writer = buf.NewWriter(conn) + } else { + // if we are in TPROXY mode, use linux's udp forging functionality + if !destinationOverridden { + writer = &buf.SequentialWriter{Writer: conn} + } else { + back := conn.RemoteAddr().(*net.UDPAddr) + if !dest.Address.Family().IsIP() { + if len(back.IP) == 4 { + dest.Address = net.AnyIP + } else { + dest.Address = net.AnyIPv6 + } + } + addr := &net.UDPAddr{ + IP: dest.Address.IP(), + Port: int(dest.Port), + } + var mark int + if d.sockopt != nil { + mark = int(d.sockopt.Mark) + } + pConn, err := FakeUDP(addr, mark) + if err != nil { + return err + } + writer = NewPacketWriter(pConn, &dest, mark, back) + defer writer.(*PacketWriter).Close() // close fake UDP conns + } + } + + if err := dispatcher.DispatchLink(ctx, dest, &transport.Link{ + Reader: reader, + Writer: writer}, + ); err != nil { + return errors.New("failed to dispatch request").Base(err) + } + return nil // Unlike Dispatch(), DispatchLink() will not return until the outbound finishes Process() +} + +func NewPacketWriter(conn net.PacketConn, d *net.Destination, mark int, back *net.UDPAddr) buf.Writer { + writer := &PacketWriter{ + conn: conn, + conns: make(map[net.Destination]net.PacketConn), + mark: mark, + back: back, + } + writer.conns[*d] = conn + return writer +} + +type PacketWriter struct { + conn net.PacketConn + conns map[net.Destination]net.PacketConn + mark int + back *net.UDPAddr +} + +func (w *PacketWriter) WriteMultiBuffer(mb buf.MultiBuffer) error { + for { + mb2, b := buf.SplitFirst(mb) + mb = mb2 + if b == nil { + break + } + var err error + if b.UDP != nil && b.UDP.Address.Family().IsIP() { + conn := w.conns[*b.UDP] + if conn == nil { + conn, err = FakeUDP( + &net.UDPAddr{ + IP: b.UDP.Address.IP(), + Port: int(b.UDP.Port), + }, + w.mark, + ) + if err != nil { + errors.LogInfo(context.Background(), err.Error()) + b.Release() + continue + } + w.conns[*b.UDP] = conn + } + _, err = conn.WriteTo(b.Bytes(), w.back) + if err != nil { + errors.LogInfo(context.Background(), err.Error()) + w.conns[*b.UDP] = nil + conn.Close() + } + b.Release() + } else { + _, err = w.conn.WriteTo(b.Bytes(), w.back) + b.Release() + if err != nil { + buf.ReleaseMulti(mb) + return err + } + } + } + return nil +} + +func (w *PacketWriter) Close() error { + for _, conn := range w.conns { + if conn != nil { + conn.Close() + } + } + return nil +} diff --git a/subproject/Xray-core-main/proxy/dokodemo/fakeudp_linux.go b/subproject/Xray-core-main/proxy/dokodemo/fakeudp_linux.go new file mode 100644 index 00000000..6915349f --- /dev/null +++ b/subproject/Xray-core-main/proxy/dokodemo/fakeudp_linux.go @@ -0,0 +1,67 @@ +//go:build linux +// +build linux + +package dokodemo + +import ( + "fmt" + "net" + "os" + "syscall" + + "golang.org/x/sys/unix" +) + +func FakeUDP(addr *net.UDPAddr, mark int) (net.PacketConn, error) { + var af int + var sockaddr syscall.Sockaddr + + if len(addr.IP) == 4 { + af = syscall.AF_INET + sockaddr = &syscall.SockaddrInet4{Port: addr.Port} + copy(sockaddr.(*syscall.SockaddrInet4).Addr[:], addr.IP) + } else { + af = syscall.AF_INET6 + sockaddr = &syscall.SockaddrInet6{Port: addr.Port} + copy(sockaddr.(*syscall.SockaddrInet6).Addr[:], addr.IP) + } + + var fd int + var err error + + if fd, err = syscall.Socket(af, syscall.SOCK_DGRAM, 0); err != nil { + return nil, &net.OpError{Op: "fake", Err: fmt.Errorf("socket open: %s", err)} + } + + if mark != 0 { + if err = syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_MARK, mark); err != nil { + syscall.Close(fd) + return nil, &net.OpError{Op: "fake", Err: fmt.Errorf("set socket option: SO_MARK: %s", err)} + } + } + + if err = syscall.SetsockoptInt(fd, syscall.SOL_IP, syscall.IP_TRANSPARENT, 1); err != nil { + syscall.Close(fd) + return nil, &net.OpError{Op: "fake", Err: fmt.Errorf("set socket option: IP_TRANSPARENT: %s", err)} + } + + syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1) + + syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, unix.SO_REUSEPORT, 1) + + if err = syscall.Bind(fd, sockaddr); err != nil { + syscall.Close(fd) + return nil, &net.OpError{Op: "fake", Err: fmt.Errorf("socket bind: %s", err)} + } + + fdFile := os.NewFile(uintptr(fd), fmt.Sprintf("net-udp-fake-%s", addr.String())) + defer fdFile.Close() + + packetConn, err := net.FilePacketConn(fdFile) + if err != nil { + syscall.Close(fd) + return nil, &net.OpError{Op: "fake", Err: fmt.Errorf("convert file descriptor to connection: %s", err)} + } + + return packetConn, nil +} diff --git a/subproject/Xray-core-main/proxy/dokodemo/fakeudp_other.go b/subproject/Xray-core-main/proxy/dokodemo/fakeudp_other.go new file mode 100644 index 00000000..490f0912 --- /dev/null +++ b/subproject/Xray-core-main/proxy/dokodemo/fakeudp_other.go @@ -0,0 +1,13 @@ +//go:build !linux +// +build !linux + +package dokodemo + +import ( + "fmt" + "net" +) + +func FakeUDP(addr *net.UDPAddr, mark int) (net.PacketConn, error) { + return nil, &net.OpError{Op: "fake", Err: fmt.Errorf("!linux")} +} diff --git a/subproject/Xray-core-main/proxy/freedom/config.go b/subproject/Xray-core-main/proxy/freedom/config.go new file mode 100644 index 00000000..38bd04fa --- /dev/null +++ b/subproject/Xray-core-main/proxy/freedom/config.go @@ -0,0 +1 @@ +package freedom diff --git a/subproject/Xray-core-main/proxy/freedom/config.pb.go b/subproject/Xray-core-main/proxy/freedom/config.pb.go new file mode 100644 index 00000000..f39abcf7 --- /dev/null +++ b/subproject/Xray-core-main/proxy/freedom/config.pb.go @@ -0,0 +1,432 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: proxy/freedom/config.proto + +package freedom + +import ( + protocol "github.com/xtls/xray-core/common/protocol" + internet "github.com/xtls/xray-core/transport/internet" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type DestinationOverride struct { + state protoimpl.MessageState `protogen:"open.v1"` + Server *protocol.ServerEndpoint `protobuf:"bytes,1,opt,name=server,proto3" json:"server,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DestinationOverride) Reset() { + *x = DestinationOverride{} + mi := &file_proxy_freedom_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DestinationOverride) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DestinationOverride) ProtoMessage() {} + +func (x *DestinationOverride) ProtoReflect() protoreflect.Message { + mi := &file_proxy_freedom_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DestinationOverride.ProtoReflect.Descriptor instead. +func (*DestinationOverride) Descriptor() ([]byte, []int) { + return file_proxy_freedom_config_proto_rawDescGZIP(), []int{0} +} + +func (x *DestinationOverride) GetServer() *protocol.ServerEndpoint { + if x != nil { + return x.Server + } + return nil +} + +type Fragment struct { + state protoimpl.MessageState `protogen:"open.v1"` + PacketsFrom uint64 `protobuf:"varint,1,opt,name=packets_from,json=packetsFrom,proto3" json:"packets_from,omitempty"` + PacketsTo uint64 `protobuf:"varint,2,opt,name=packets_to,json=packetsTo,proto3" json:"packets_to,omitempty"` + LengthMin uint64 `protobuf:"varint,3,opt,name=length_min,json=lengthMin,proto3" json:"length_min,omitempty"` + LengthMax uint64 `protobuf:"varint,4,opt,name=length_max,json=lengthMax,proto3" json:"length_max,omitempty"` + IntervalMin uint64 `protobuf:"varint,5,opt,name=interval_min,json=intervalMin,proto3" json:"interval_min,omitempty"` + IntervalMax uint64 `protobuf:"varint,6,opt,name=interval_max,json=intervalMax,proto3" json:"interval_max,omitempty"` + MaxSplitMin uint64 `protobuf:"varint,7,opt,name=max_split_min,json=maxSplitMin,proto3" json:"max_split_min,omitempty"` + MaxSplitMax uint64 `protobuf:"varint,8,opt,name=max_split_max,json=maxSplitMax,proto3" json:"max_split_max,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Fragment) Reset() { + *x = Fragment{} + mi := &file_proxy_freedom_config_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Fragment) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Fragment) ProtoMessage() {} + +func (x *Fragment) ProtoReflect() protoreflect.Message { + mi := &file_proxy_freedom_config_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Fragment.ProtoReflect.Descriptor instead. +func (*Fragment) Descriptor() ([]byte, []int) { + return file_proxy_freedom_config_proto_rawDescGZIP(), []int{1} +} + +func (x *Fragment) GetPacketsFrom() uint64 { + if x != nil { + return x.PacketsFrom + } + return 0 +} + +func (x *Fragment) GetPacketsTo() uint64 { + if x != nil { + return x.PacketsTo + } + return 0 +} + +func (x *Fragment) GetLengthMin() uint64 { + if x != nil { + return x.LengthMin + } + return 0 +} + +func (x *Fragment) GetLengthMax() uint64 { + if x != nil { + return x.LengthMax + } + return 0 +} + +func (x *Fragment) GetIntervalMin() uint64 { + if x != nil { + return x.IntervalMin + } + return 0 +} + +func (x *Fragment) GetIntervalMax() uint64 { + if x != nil { + return x.IntervalMax + } + return 0 +} + +func (x *Fragment) GetMaxSplitMin() uint64 { + if x != nil { + return x.MaxSplitMin + } + return 0 +} + +func (x *Fragment) GetMaxSplitMax() uint64 { + if x != nil { + return x.MaxSplitMax + } + return 0 +} + +type Noise struct { + state protoimpl.MessageState `protogen:"open.v1"` + LengthMin uint64 `protobuf:"varint,1,opt,name=length_min,json=lengthMin,proto3" json:"length_min,omitempty"` + LengthMax uint64 `protobuf:"varint,2,opt,name=length_max,json=lengthMax,proto3" json:"length_max,omitempty"` + DelayMin uint64 `protobuf:"varint,3,opt,name=delay_min,json=delayMin,proto3" json:"delay_min,omitempty"` + DelayMax uint64 `protobuf:"varint,4,opt,name=delay_max,json=delayMax,proto3" json:"delay_max,omitempty"` + Packet []byte `protobuf:"bytes,5,opt,name=packet,proto3" json:"packet,omitempty"` + ApplyTo string `protobuf:"bytes,6,opt,name=apply_to,json=applyTo,proto3" json:"apply_to,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Noise) Reset() { + *x = Noise{} + mi := &file_proxy_freedom_config_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Noise) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Noise) ProtoMessage() {} + +func (x *Noise) ProtoReflect() protoreflect.Message { + mi := &file_proxy_freedom_config_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Noise.ProtoReflect.Descriptor instead. +func (*Noise) Descriptor() ([]byte, []int) { + return file_proxy_freedom_config_proto_rawDescGZIP(), []int{2} +} + +func (x *Noise) GetLengthMin() uint64 { + if x != nil { + return x.LengthMin + } + return 0 +} + +func (x *Noise) GetLengthMax() uint64 { + if x != nil { + return x.LengthMax + } + return 0 +} + +func (x *Noise) GetDelayMin() uint64 { + if x != nil { + return x.DelayMin + } + return 0 +} + +func (x *Noise) GetDelayMax() uint64 { + if x != nil { + return x.DelayMax + } + return 0 +} + +func (x *Noise) GetPacket() []byte { + if x != nil { + return x.Packet + } + return nil +} + +func (x *Noise) GetApplyTo() string { + if x != nil { + return x.ApplyTo + } + return "" +} + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + DomainStrategy internet.DomainStrategy `protobuf:"varint,1,opt,name=domain_strategy,json=domainStrategy,proto3,enum=xray.transport.internet.DomainStrategy" json:"domain_strategy,omitempty"` + DestinationOverride *DestinationOverride `protobuf:"bytes,3,opt,name=destination_override,json=destinationOverride,proto3" json:"destination_override,omitempty"` + UserLevel uint32 `protobuf:"varint,4,opt,name=user_level,json=userLevel,proto3" json:"user_level,omitempty"` + Fragment *Fragment `protobuf:"bytes,5,opt,name=fragment,proto3" json:"fragment,omitempty"` + ProxyProtocol uint32 `protobuf:"varint,6,opt,name=proxy_protocol,json=proxyProtocol,proto3" json:"proxy_protocol,omitempty"` + Noises []*Noise `protobuf:"bytes,7,rep,name=noises,proto3" json:"noises,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_proxy_freedom_config_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_proxy_freedom_config_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_proxy_freedom_config_proto_rawDescGZIP(), []int{3} +} + +func (x *Config) GetDomainStrategy() internet.DomainStrategy { + if x != nil { + return x.DomainStrategy + } + return internet.DomainStrategy(0) +} + +func (x *Config) GetDestinationOverride() *DestinationOverride { + if x != nil { + return x.DestinationOverride + } + return nil +} + +func (x *Config) GetUserLevel() uint32 { + if x != nil { + return x.UserLevel + } + return 0 +} + +func (x *Config) GetFragment() *Fragment { + if x != nil { + return x.Fragment + } + return nil +} + +func (x *Config) GetProxyProtocol() uint32 { + if x != nil { + return x.ProxyProtocol + } + return 0 +} + +func (x *Config) GetNoises() []*Noise { + if x != nil { + return x.Noises + } + return nil +} + +var File_proxy_freedom_config_proto protoreflect.FileDescriptor + +const file_proxy_freedom_config_proto_rawDesc = "" + + "\n" + + "\x1aproxy/freedom/config.proto\x12\x12xray.proxy.freedom\x1a!common/protocol/server_spec.proto\x1a\x1ftransport/internet/config.proto\"S\n" + + "\x13DestinationOverride\x12<\n" + + "\x06server\x18\x01 \x01(\v2$.xray.common.protocol.ServerEndpointR\x06server\"\x98\x02\n" + + "\bFragment\x12!\n" + + "\fpackets_from\x18\x01 \x01(\x04R\vpacketsFrom\x12\x1d\n" + + "\n" + + "packets_to\x18\x02 \x01(\x04R\tpacketsTo\x12\x1d\n" + + "\n" + + "length_min\x18\x03 \x01(\x04R\tlengthMin\x12\x1d\n" + + "\n" + + "length_max\x18\x04 \x01(\x04R\tlengthMax\x12!\n" + + "\finterval_min\x18\x05 \x01(\x04R\vintervalMin\x12!\n" + + "\finterval_max\x18\x06 \x01(\x04R\vintervalMax\x12\"\n" + + "\rmax_split_min\x18\a \x01(\x04R\vmaxSplitMin\x12\"\n" + + "\rmax_split_max\x18\b \x01(\x04R\vmaxSplitMax\"\xb2\x01\n" + + "\x05Noise\x12\x1d\n" + + "\n" + + "length_min\x18\x01 \x01(\x04R\tlengthMin\x12\x1d\n" + + "\n" + + "length_max\x18\x02 \x01(\x04R\tlengthMax\x12\x1b\n" + + "\tdelay_min\x18\x03 \x01(\x04R\bdelayMin\x12\x1b\n" + + "\tdelay_max\x18\x04 \x01(\x04R\bdelayMax\x12\x16\n" + + "\x06packet\x18\x05 \x01(\fR\x06packet\x12\x19\n" + + "\bapply_to\x18\x06 \x01(\tR\aapplyTo\"\xe9\x02\n" + + "\x06Config\x12P\n" + + "\x0fdomain_strategy\x18\x01 \x01(\x0e2'.xray.transport.internet.DomainStrategyR\x0edomainStrategy\x12Z\n" + + "\x14destination_override\x18\x03 \x01(\v2'.xray.proxy.freedom.DestinationOverrideR\x13destinationOverride\x12\x1d\n" + + "\n" + + "user_level\x18\x04 \x01(\rR\tuserLevel\x128\n" + + "\bfragment\x18\x05 \x01(\v2\x1c.xray.proxy.freedom.FragmentR\bfragment\x12%\n" + + "\x0eproxy_protocol\x18\x06 \x01(\rR\rproxyProtocol\x121\n" + + "\x06noises\x18\a \x03(\v2\x19.xray.proxy.freedom.NoiseR\x06noisesBX\n" + + "\x16com.xray.proxy.freedomP\x01Z'github.com/xtls/xray-core/proxy/freedom\xaa\x02\x12Xray.Proxy.Freedomb\x06proto3" + +var ( + file_proxy_freedom_config_proto_rawDescOnce sync.Once + file_proxy_freedom_config_proto_rawDescData []byte +) + +func file_proxy_freedom_config_proto_rawDescGZIP() []byte { + file_proxy_freedom_config_proto_rawDescOnce.Do(func() { + file_proxy_freedom_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proxy_freedom_config_proto_rawDesc), len(file_proxy_freedom_config_proto_rawDesc))) + }) + return file_proxy_freedom_config_proto_rawDescData +} + +var file_proxy_freedom_config_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_proxy_freedom_config_proto_goTypes = []any{ + (*DestinationOverride)(nil), // 0: xray.proxy.freedom.DestinationOverride + (*Fragment)(nil), // 1: xray.proxy.freedom.Fragment + (*Noise)(nil), // 2: xray.proxy.freedom.Noise + (*Config)(nil), // 3: xray.proxy.freedom.Config + (*protocol.ServerEndpoint)(nil), // 4: xray.common.protocol.ServerEndpoint + (internet.DomainStrategy)(0), // 5: xray.transport.internet.DomainStrategy +} +var file_proxy_freedom_config_proto_depIdxs = []int32{ + 4, // 0: xray.proxy.freedom.DestinationOverride.server:type_name -> xray.common.protocol.ServerEndpoint + 5, // 1: xray.proxy.freedom.Config.domain_strategy:type_name -> xray.transport.internet.DomainStrategy + 0, // 2: xray.proxy.freedom.Config.destination_override:type_name -> xray.proxy.freedom.DestinationOverride + 1, // 3: xray.proxy.freedom.Config.fragment:type_name -> xray.proxy.freedom.Fragment + 2, // 4: xray.proxy.freedom.Config.noises:type_name -> xray.proxy.freedom.Noise + 5, // [5:5] is the sub-list for method output_type + 5, // [5:5] is the sub-list for method input_type + 5, // [5:5] is the sub-list for extension type_name + 5, // [5:5] is the sub-list for extension extendee + 0, // [0:5] is the sub-list for field type_name +} + +func init() { file_proxy_freedom_config_proto_init() } +func file_proxy_freedom_config_proto_init() { + if File_proxy_freedom_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_proxy_freedom_config_proto_rawDesc), len(file_proxy_freedom_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 4, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_proxy_freedom_config_proto_goTypes, + DependencyIndexes: file_proxy_freedom_config_proto_depIdxs, + MessageInfos: file_proxy_freedom_config_proto_msgTypes, + }.Build() + File_proxy_freedom_config_proto = out.File + file_proxy_freedom_config_proto_goTypes = nil + file_proxy_freedom_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/proxy/freedom/config.proto b/subproject/Xray-core-main/proxy/freedom/config.proto new file mode 100644 index 00000000..fd0547d8 --- /dev/null +++ b/subproject/Xray-core-main/proxy/freedom/config.proto @@ -0,0 +1,42 @@ +syntax = "proto3"; + +package xray.proxy.freedom; +option csharp_namespace = "Xray.Proxy.Freedom"; +option go_package = "github.com/xtls/xray-core/proxy/freedom"; +option java_package = "com.xray.proxy.freedom"; +option java_multiple_files = true; + +import "common/protocol/server_spec.proto"; +import "transport/internet/config.proto"; + +message DestinationOverride { + xray.common.protocol.ServerEndpoint server = 1; +} + +message Fragment { + uint64 packets_from = 1; + uint64 packets_to = 2; + uint64 length_min = 3; + uint64 length_max = 4; + uint64 interval_min = 5; + uint64 interval_max = 6; + uint64 max_split_min = 7; + uint64 max_split_max = 8; +} +message Noise { + uint64 length_min = 1; + uint64 length_max = 2; + uint64 delay_min = 3; + uint64 delay_max = 4; + bytes packet = 5; + string apply_to = 6; +} + +message Config { + xray.transport.internet.DomainStrategy domain_strategy = 1; + DestinationOverride destination_override = 3; + uint32 user_level = 4; + Fragment fragment = 5; + uint32 proxy_protocol = 6; + repeated Noise noises = 7; +} diff --git a/subproject/Xray-core-main/proxy/freedom/freedom.go b/subproject/Xray-core-main/proxy/freedom/freedom.go new file mode 100644 index 00000000..d107e357 --- /dev/null +++ b/subproject/Xray-core-main/proxy/freedom/freedom.go @@ -0,0 +1,579 @@ +package freedom + +import ( + "context" + "crypto/rand" + "io" + "time" + + "github.com/pires/go-proxyproto" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/crypto" + "github.com/xtls/xray-core/common/dice" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/platform" + "github.com/xtls/xray-core/common/retry" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/common/signal" + "github.com/xtls/xray-core/common/task" + "github.com/xtls/xray-core/common/utils" + "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/features/policy" + "github.com/xtls/xray-core/features/stats" + "github.com/xtls/xray-core/proxy" + "github.com/xtls/xray-core/transport" + "github.com/xtls/xray-core/transport/internet" + "github.com/xtls/xray-core/transport/internet/stat" +) + +var useSplice bool + +func init() { + common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + h := new(Handler) + if err := core.RequireFeatures(ctx, func(pm policy.Manager) error { + return h.Init(config.(*Config), pm) + }); err != nil { + return nil, err + } + return h, nil + })) + const defaultFlagValue = "NOT_DEFINED_AT_ALL" + value := platform.NewEnvFlag(platform.UseFreedomSplice).GetValue(func() string { return defaultFlagValue }) + switch value { + case defaultFlagValue, "auto", "enable": + useSplice = true + } +} + +// Handler handles Freedom connections. +type Handler struct { + policyManager policy.Manager + config *Config +} + +// Init initializes the Handler with necessary parameters. +func (h *Handler) Init(config *Config, pm policy.Manager) error { + h.config = config + h.policyManager = pm + return nil +} + +func (h *Handler) policy() policy.Session { + p := h.policyManager.ForLevel(h.config.UserLevel) + return p +} + +func isValidAddress(addr *net.IPOrDomain) bool { + if addr == nil { + return false + } + + a := addr.AsAddress() + return a != net.AnyIP && a != net.AnyIPv6 +} + +// Process implements proxy.Outbound. +func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer internet.Dialer) error { + outbounds := session.OutboundsFromContext(ctx) + ob := outbounds[len(outbounds)-1] + if !ob.Target.IsValid() { + return errors.New("target not specified.") + } + ob.Name = "freedom" + ob.CanSpliceCopy = 1 + inbound := session.InboundFromContext(ctx) + + destination := ob.Target + origTargetAddr := ob.OriginalTarget.Address + if origTargetAddr == nil { + origTargetAddr = ob.Target.Address + } + dialer.SetOutboundGateway(ctx, ob) + outGateway := ob.Gateway + UDPOverride := net.UDPDestination(nil, 0) + if h.config.DestinationOverride != nil { + server := h.config.DestinationOverride.Server + if isValidAddress(server.Address) { + destination.Address = server.Address.AsAddress() + UDPOverride.Address = destination.Address + } + if server.Port != 0 { + destination.Port = net.Port(server.Port) + UDPOverride.Port = destination.Port + } + } + + input := link.Reader + output := link.Writer + + var conn stat.Connection + err := retry.ExponentialBackoff(5, 100).On(func() error { + dialDest := destination + if h.config.DomainStrategy.HasStrategy() && dialDest.Address.Family().IsDomain() { + strategy := h.config.DomainStrategy + if destination.Network == net.Network_UDP && origTargetAddr != nil && outGateway == nil { + strategy = strategy.GetDynamicStrategy(origTargetAddr.Family()) + } + ips, err := internet.LookupForIP(dialDest.Address.Domain(), strategy, outGateway) + if err != nil { + errors.LogInfoInner(ctx, err, "failed to get IP address for domain ", dialDest.Address.Domain()) + if h.config.DomainStrategy.ForceIP() { + return err + } + } else { + dialDest = net.Destination{ + Network: dialDest.Network, + Address: net.IPAddress(ips[dice.Roll(len(ips))]), + Port: dialDest.Port, + } + errors.LogInfo(ctx, "dialing to ", dialDest) + } + } + + rawConn, err := dialer.Dial(ctx, dialDest) + if err != nil { + return err + } + + if h.config.ProxyProtocol > 0 && h.config.ProxyProtocol <= 2 { + version := byte(h.config.ProxyProtocol) + srcAddr := inbound.Source.RawNetAddr() + dstAddr := rawConn.RemoteAddr() + header := proxyproto.HeaderProxyFromAddrs(version, srcAddr, dstAddr) + if _, err = header.WriteTo(rawConn); err != nil { + rawConn.Close() + return err + } + } + + conn = rawConn + return nil + }) + if err != nil { + return errors.New("failed to open connection to ", destination).Base(err) + } + defer conn.Close() + errors.LogInfo(ctx, "connection opened to ", destination, ", local endpoint ", conn.LocalAddr(), ", remote endpoint ", conn.RemoteAddr()) + + var newCtx context.Context + var newCancel context.CancelFunc + if session.TimeoutOnlyFromContext(ctx) { + newCtx, newCancel = context.WithCancel(context.Background()) + } + + plcy := h.policy() + ctx, cancel := context.WithCancel(ctx) + timer := signal.CancelAfterInactivity(ctx, func() { + cancel() + if newCancel != nil { + newCancel() + } + }, plcy.Timeouts.ConnectionIdle) + + requestDone := func() error { + defer timer.SetTimeout(plcy.Timeouts.DownlinkOnly) + + var writer buf.Writer + if destination.Network == net.Network_TCP { + if h.config.Fragment != nil { + errors.LogDebug(ctx, "FRAGMENT", h.config.Fragment.PacketsFrom, h.config.Fragment.PacketsTo, h.config.Fragment.LengthMin, h.config.Fragment.LengthMax, + h.config.Fragment.IntervalMin, h.config.Fragment.IntervalMax, h.config.Fragment.MaxSplitMin, h.config.Fragment.MaxSplitMax) + writer = buf.NewWriter(&FragmentWriter{ + fragment: h.config.Fragment, + writer: conn, + }) + } else { + writer = buf.NewWriter(conn) + } + } else { + writer = NewPacketWriter(conn, h, UDPOverride, destination) + if h.config.Noises != nil { + errors.LogDebug(ctx, "NOISE", h.config.Noises) + writer = &NoisePacketWriter{ + Writer: writer, + noises: h.config.Noises, + firstWrite: true, + UDPOverride: UDPOverride, + remoteAddr: net.DestinationFromAddr(conn.RemoteAddr()).Address, + } + } + } + + if err := buf.Copy(input, writer, buf.UpdateActivity(timer)); err != nil { + return errors.New("failed to process request").Base(err) + } + + return nil + } + + responseDone := func() error { + defer timer.SetTimeout(plcy.Timeouts.UplinkOnly) + if destination.Network == net.Network_TCP && useSplice && proxy.IsRAWTransportWithoutSecurity(conn) { // it would be tls conn in special use case of MITM, we need to let link handle traffic + var writeConn net.Conn + var inTimer *signal.ActivityTimer + if inbound := session.InboundFromContext(ctx); inbound != nil && inbound.Conn != nil { + writeConn = inbound.Conn + inTimer = inbound.Timer + } + return proxy.CopyRawConnIfExist(ctx, conn, writeConn, link.Writer, timer, inTimer) + } + var reader buf.Reader + if destination.Network == net.Network_TCP { + reader = buf.NewReader(conn) + } else { + reader = NewPacketReader(conn, UDPOverride, destination) + } + if err := buf.Copy(reader, output, buf.UpdateActivity(timer)); err != nil { + return errors.New("failed to process response").Base(err) + } + return nil + } + + if newCtx != nil { + ctx = newCtx + } + + if err := task.Run(ctx, requestDone, task.OnSuccess(responseDone, task.Close(output))); err != nil { + return errors.New("connection ends").Base(err) + } + + return nil +} + +func NewPacketReader(conn net.Conn, UDPOverride net.Destination, DialDest net.Destination) buf.Reader { + iConn := conn + statConn, ok := iConn.(*stat.CounterConnection) + if ok { + iConn = statConn.Connection + } + var counter stats.Counter + if statConn != nil { + counter = statConn.ReadCounter + } + if c, ok := iConn.(*internet.PacketConnWrapper); ok { + isOverridden := false + if UDPOverride.Address != nil || UDPOverride.Port != 0 { + isOverridden = true + } + + return &PacketReader{ + PacketConnWrapper: c, + Counter: counter, + IsOverridden: isOverridden, + InitUnchangedAddr: DialDest.Address, + InitChangedAddr: net.DestinationFromAddr(conn.RemoteAddr()).Address, + } + } + return &buf.PacketReader{Reader: conn} +} + +type PacketReader struct { + *internet.PacketConnWrapper + stats.Counter + IsOverridden bool + InitUnchangedAddr net.Address + InitChangedAddr net.Address +} + +func (r *PacketReader) ReadMultiBuffer() (buf.MultiBuffer, error) { + b := buf.New() + b.Resize(0, buf.Size) + n, d, err := r.PacketConnWrapper.ReadFrom(b.Bytes()) + if err != nil { + b.Release() + return nil, err + } + b.Resize(0, int32(n)) + // if udp dest addr is changed, we are unable to get the correct src addr + // so we don't attach src info to udp packet, break cone behavior, assuming the dial dest is the expected scr addr + if !r.IsOverridden { + address := net.IPAddress(d.(*net.UDPAddr).IP) + if r.InitChangedAddr == address { + address = r.InitUnchangedAddr + } + b.UDP = &net.Destination{ + Address: address, + Port: net.Port(d.(*net.UDPAddr).Port), + Network: net.Network_UDP, + } + } + if r.Counter != nil { + r.Counter.Add(int64(n)) + } + return buf.MultiBuffer{b}, nil +} + +// DialDest means the dial target used in the dialer when creating conn +func NewPacketWriter(conn net.Conn, h *Handler, UDPOverride net.Destination, DialDest net.Destination) buf.Writer { + iConn := conn + statConn, ok := iConn.(*stat.CounterConnection) + if ok { + iConn = statConn.Connection + } + var counter stats.Counter + if statConn != nil { + counter = statConn.WriteCounter + } + if c, ok := iConn.(*internet.PacketConnWrapper); ok { + // If DialDest is a domain, it will be resolved in dialer + // check this behavior and add it to map + resolvedUDPAddr := utils.NewTypedSyncMap[string, net.Address]() + if DialDest.Address.Family().IsDomain() { + resolvedUDPAddr.Store(DialDest.Address.Domain(), net.DestinationFromAddr(conn.RemoteAddr()).Address) + } + return &PacketWriter{ + PacketConnWrapper: c, + Counter: counter, + Handler: h, + UDPOverride: UDPOverride, + ResolvedUDPAddr: resolvedUDPAddr, + LocalAddr: net.DestinationFromAddr(conn.LocalAddr()).Address, + } + + } + return &buf.SequentialWriter{Writer: conn} +} + +type PacketWriter struct { + *internet.PacketConnWrapper + stats.Counter + *Handler + UDPOverride net.Destination + + // Dest of udp packets might be a domain, we will resolve them to IP + // But resolver will return a random one if the domain has many IPs + // Resulting in these packets being sent to many different IPs randomly + // So, cache and keep the resolve result + ResolvedUDPAddr *utils.TypedSyncMap[string, net.Address] + LocalAddr net.Address +} + +func (w *PacketWriter) WriteMultiBuffer(mb buf.MultiBuffer) error { + for { + mb2, b := buf.SplitFirst(mb) + mb = mb2 + if b == nil { + break + } + var n int + var err error + if b.UDP != nil { + if w.UDPOverride.Address != nil { + b.UDP.Address = w.UDPOverride.Address + } + if w.UDPOverride.Port != 0 { + b.UDP.Port = w.UDPOverride.Port + } + if b.UDP.Address.Family().IsDomain() { + if ip, ok := w.ResolvedUDPAddr.Load(b.UDP.Address.Domain()); ok { + b.UDP.Address = ip + } else { + ShouldUseSystemResolver := true + if w.Handler.config.DomainStrategy.HasStrategy() { + ips, err := internet.LookupForIP(b.UDP.Address.Domain(), w.Handler.config.DomainStrategy, w.LocalAddr) + if err != nil { + // drop packet if resolve failed when forceIP + if w.Handler.config.DomainStrategy.ForceIP() { + b.Release() + continue + } + } else { + ip = net.IPAddress(ips[dice.Roll(len(ips))]) + ShouldUseSystemResolver = false + } + } + if ShouldUseSystemResolver { + udpAddr, err := net.ResolveUDPAddr("udp", b.UDP.NetAddr()) + if err != nil { + b.Release() + continue + } else { + ip = net.IPAddress(udpAddr.IP) + } + } + if ip != nil { + b.UDP.Address, _ = w.ResolvedUDPAddr.LoadOrStore(b.UDP.Address.Domain(), ip) + } + } + } + destAddr := b.UDP.RawNetAddr() + if destAddr == nil { + b.Release() + continue + } + n, err = w.PacketConnWrapper.WriteTo(b.Bytes(), destAddr) + } else { + n, err = w.PacketConnWrapper.Write(b.Bytes()) + } + b.Release() + if err != nil { + buf.ReleaseMulti(mb) + return err + } + if w.Counter != nil { + w.Counter.Add(int64(n)) + } + } + return nil +} + +type NoisePacketWriter struct { + buf.Writer + noises []*Noise + firstWrite bool + UDPOverride net.Destination + remoteAddr net.Address +} + +// MultiBuffer writer with Noise before first packet +func (w *NoisePacketWriter) WriteMultiBuffer(mb buf.MultiBuffer) error { + if w.firstWrite { + w.firstWrite = false + //Do not send Noise for dns requests(just to be safe) + if w.UDPOverride.Port == 53 { + return w.Writer.WriteMultiBuffer(mb) + } + var noise []byte + var err error + if w.remoteAddr.Family().IsDomain() { + panic("impossible, remoteAddr is always IP") + } + for _, n := range w.noises { + switch n.ApplyTo { + case "ipv4": + if w.remoteAddr.Family().IsIPv6() { + continue + } + case "ipv6": + if w.remoteAddr.Family().IsIPv4() { + continue + } + case "ip": + default: + panic("unreachable, applyTo is ip/ipv4/ipv6") + } + //User input string or base64 encoded string or hex string + if n.Packet != nil { + noise = n.Packet + } else { + //Random noise + noise, err = GenerateRandomBytes(crypto.RandBetween(int64(n.LengthMin), + int64(n.LengthMax))) + } + if err != nil { + return err + } + err = w.Writer.WriteMultiBuffer(buf.MultiBuffer{buf.FromBytes(noise)}) + if err != nil { + return err + } + + if n.DelayMin != 0 || n.DelayMax != 0 { + time.Sleep(time.Duration(crypto.RandBetween(int64(n.DelayMin), int64(n.DelayMax))) * time.Millisecond) + } + } + + } + return w.Writer.WriteMultiBuffer(mb) +} + +type FragmentWriter struct { + fragment *Fragment + writer io.Writer + count uint64 +} + +func (f *FragmentWriter) Write(b []byte) (int, error) { + f.count++ + + if f.fragment.PacketsFrom == 0 && f.fragment.PacketsTo == 1 { + if f.count != 1 || len(b) <= 5 || b[0] != 22 { + return f.writer.Write(b) + } + recordLen := 5 + ((int(b[3]) << 8) | int(b[4])) + if len(b) < recordLen { // maybe already fragmented somehow + return f.writer.Write(b) + } + data := b[5:recordLen] + buff := make([]byte, 2048) + var hello []byte + maxSplit := crypto.RandBetween(int64(f.fragment.MaxSplitMin), int64(f.fragment.MaxSplitMax)) + var splitNum int64 + for from := 0; ; { + to := from + int(crypto.RandBetween(int64(f.fragment.LengthMin), int64(f.fragment.LengthMax))) + splitNum++ + if to > len(data) || (maxSplit > 0 && splitNum >= maxSplit) { + to = len(data) + } + l := to - from + if 5+l > len(buff) { + buff = make([]byte, 5+l) + } + copy(buff[:3], b) + copy(buff[5:], data[from:to]) + from = to + buff[3] = byte(l >> 8) + buff[4] = byte(l) + if f.fragment.IntervalMax == 0 { // combine fragmented tlshello if interval is 0 + hello = append(hello, buff[:5+l]...) + } else { + _, err := f.writer.Write(buff[:5+l]) + time.Sleep(time.Duration(crypto.RandBetween(int64(f.fragment.IntervalMin), int64(f.fragment.IntervalMax))) * time.Millisecond) + if err != nil { + return 0, err + } + } + if from == len(data) { + if len(hello) > 0 { + _, err := f.writer.Write(hello) + if err != nil { + return 0, err + } + } + if len(b) > recordLen { + n, err := f.writer.Write(b[recordLen:]) + if err != nil { + return recordLen + n, err + } + } + return len(b), nil + } + } + } + + if f.fragment.PacketsFrom != 0 && (f.count < f.fragment.PacketsFrom || f.count > f.fragment.PacketsTo) { + return f.writer.Write(b) + } + maxSplit := crypto.RandBetween(int64(f.fragment.MaxSplitMin), int64(f.fragment.MaxSplitMax)) + var splitNum int64 + for from := 0; ; { + to := from + int(crypto.RandBetween(int64(f.fragment.LengthMin), int64(f.fragment.LengthMax))) + splitNum++ + if to > len(b) || (maxSplit > 0 && splitNum >= maxSplit) { + to = len(b) + } + n, err := f.writer.Write(b[from:to]) + from += n + if err != nil { + return from, err + } + time.Sleep(time.Duration(crypto.RandBetween(int64(f.fragment.IntervalMin), int64(f.fragment.IntervalMax))) * time.Millisecond) + if from >= len(b) { + return from, nil + } + } +} + +func GenerateRandomBytes(n int64) ([]byte, error) { + b := make([]byte, n) + _, err := rand.Read(b) + // Note that err == nil only if we read len(b) bytes. + if err != nil { + return nil, err + } + + return b, nil +} diff --git a/subproject/Xray-core-main/proxy/http/client.go b/subproject/Xray-core-main/proxy/http/client.go new file mode 100644 index 00000000..0e50edba --- /dev/null +++ b/subproject/Xray-core-main/proxy/http/client.go @@ -0,0 +1,379 @@ +package http + +import ( + "bufio" + "bytes" + "context" + "encoding/base64" + "io" + "net/http" + "net/url" + "sync" + "text/template" + + "github.com/xtls/xray-core/common" + "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/net" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/retry" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/common/signal" + "github.com/xtls/xray-core/common/task" + "github.com/xtls/xray-core/common/utils" + "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/features/policy" + "github.com/xtls/xray-core/transport" + "github.com/xtls/xray-core/transport/internet" + "github.com/xtls/xray-core/transport/internet/stat" + "github.com/xtls/xray-core/transport/internet/tls" + "golang.org/x/net/http2" +) + +type Client struct { + server *protocol.ServerSpec + policyManager policy.Manager + header []*Header +} + +type h2Conn struct { + rawConn net.Conn + h2Conn *http2.ClientConn +} + +var ( + cachedH2Mutex sync.Mutex + cachedH2Conns map[net.Destination]h2Conn +) + +// NewClient create a new http client based on the given config. +func NewClient(ctx context.Context, config *ClientConfig) (*Client, error) { + if config.Server == nil { + return nil, errors.New(`no target server found`) + } + server, err := protocol.NewServerSpecFromPB(config.Server) + if err != nil { + return nil, errors.New("failed to get server spec").Base(err) + } + + v := core.MustFromContext(ctx) + return &Client{ + server: server, + policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager), + header: config.Header, + }, nil +} + +// Process implements proxy.Outbound.Process. We first create a socket tunnel via HTTP CONNECT method, then redirect all inbound traffic to that tunnel. +func (c *Client) Process(ctx context.Context, link *transport.Link, dialer internet.Dialer) error { + outbounds := session.OutboundsFromContext(ctx) + ob := outbounds[len(outbounds)-1] + if !ob.Target.IsValid() { + return errors.New("target not specified.") + } + ob.Name = "http" + ob.CanSpliceCopy = 2 + target := ob.Target + targetAddr := target.NetAddr() + + if target.Network == net.Network_UDP { + return errors.New("UDP is not supported by HTTP outbound") + } + + server := c.server + dest := server.Destination + user := server.User + var conn stat.Connection + + mbuf, _ := link.Reader.ReadMultiBuffer() + len := mbuf.Len() + firstPayload := bytespool.Alloc(len) + mbuf, _ = buf.SplitBytes(mbuf, firstPayload) + firstPayload = firstPayload[:len] + + buf.ReleaseMulti(mbuf) + defer bytespool.Free(firstPayload) + + header, err := fillRequestHeader(ctx, c.header) + if err != nil { + return errors.New("failed to fill out header").Base(err) + } + + if err := retry.ExponentialBackoff(5, 100).On(func() error { + netConn, err := setUpHTTPTunnel(ctx, dest, targetAddr, user, dialer, header, firstPayload) + if netConn != nil { + if _, ok := netConn.(*http2Conn); !ok { + if _, err := netConn.Write(firstPayload); err != nil { + netConn.Close() + return err + } + } + conn = stat.Connection(netConn) + } + return err + }); err != nil { + return errors.New("failed to find an available destination").Base(err) + } + + defer func() { + if err := conn.Close(); err != nil { + errors.LogInfoInner(ctx, err, "failed to closed connection") + } + }() + + p := c.policyManager.ForLevel(0) + if user != nil { + p = c.policyManager.ForLevel(user.Level) + } + + var newCtx context.Context + var newCancel context.CancelFunc + if session.TimeoutOnlyFromContext(ctx) { + newCtx, newCancel = context.WithCancel(context.Background()) + } + + ctx, cancel := context.WithCancel(ctx) + timer := signal.CancelAfterInactivity(ctx, func() { + cancel() + if newCancel != nil { + newCancel() + } + }, p.Timeouts.ConnectionIdle) + + requestFunc := func() error { + defer timer.SetTimeout(p.Timeouts.DownlinkOnly) + return buf.Copy(link.Reader, buf.NewWriter(conn), buf.UpdateActivity(timer)) + } + responseFunc := func() error { + ob.CanSpliceCopy = 1 + defer timer.SetTimeout(p.Timeouts.UplinkOnly) + return buf.Copy(buf.NewReader(conn), link.Writer, buf.UpdateActivity(timer)) + } + + if newCtx != nil { + ctx = newCtx + } + + responseDonePost := task.OnSuccess(responseFunc, task.Close(link.Writer)) + if err := task.Run(ctx, requestFunc, responseDonePost); err != nil { + return errors.New("connection ends").Base(err) + } + + return nil +} + +// fillRequestHeader will fill out the template of the headers +func fillRequestHeader(ctx context.Context, header []*Header) ([]*Header, error) { + if len(header) == 0 { + return header, nil + } + + inbound := session.InboundFromContext(ctx) + outbounds := session.OutboundsFromContext(ctx) + ob := outbounds[len(outbounds)-1] + + if inbound == nil || ob == nil { + return nil, errors.New("missing inbound or outbound metadata from context") + } + + data := struct { + Source net.Destination + Target net.Destination + }{ + Source: inbound.Source, + Target: ob.Target, + } + + filled := make([]*Header, len(header)) + for i, h := range header { + tmpl, err := template.New(h.Key).Parse(h.Value) + if err != nil { + return nil, err + } + var buf bytes.Buffer + + if err = tmpl.Execute(&buf, data); err != nil { + return nil, err + } + filled[i] = &Header{Key: h.Key, Value: buf.String()} + } + + return filled, nil +} + +// setUpHTTPTunnel will create a socket tunnel via HTTP CONNECT method +func setUpHTTPTunnel(ctx context.Context, dest net.Destination, target string, user *protocol.MemoryUser, dialer internet.Dialer, header []*Header, firstPayload []byte) (net.Conn, error) { + req := &http.Request{ + Method: http.MethodConnect, + URL: &url.URL{Host: target}, + Header: make(http.Header), + Host: target, + } + + if user != nil && user.Account != nil { + account := user.Account.(*Account) + auth := account.GetUsername() + ":" + account.GetPassword() + req.Header.Set("Proxy-Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(auth))) + } + + for _, h := range header { + req.Header.Set(h.Key, h.Value) + } + utils.TryDefaultHeadersWith(req.Header, "nav") + + connectHTTP1 := func(rawConn net.Conn) (net.Conn, error) { + req.Header.Set("Proxy-Connection", "Keep-Alive") + + err := req.Write(rawConn) + if err != nil { + rawConn.Close() + return nil, err + } + + resp, err := http.ReadResponse(bufio.NewReader(rawConn), req) + if err != nil { + rawConn.Close() + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + rawConn.Close() + return nil, errors.New("Proxy responded with non 200 code: " + resp.Status) + } + return rawConn, nil + } + + connectHTTP2 := func(rawConn net.Conn, h2clientConn *http2.ClientConn) (net.Conn, error) { + pr, pw := io.Pipe() + req.Body = pr + + var pErr error + var wg sync.WaitGroup + wg.Add(1) + + go func() { + _, pErr = pw.Write(firstPayload) + wg.Done() + }() + + resp, err := h2clientConn.RoundTrip(req) + if err != nil { + rawConn.Close() + return nil, err + } + + wg.Wait() + if pErr != nil { + rawConn.Close() + return nil, pErr + } + + if resp.StatusCode != http.StatusOK { + rawConn.Close() + return nil, errors.New("Proxy responded with non 200 code: " + resp.Status) + } + return newHTTP2Conn(rawConn, pw, resp.Body), nil + } + + cachedH2Mutex.Lock() + cachedConn, cachedConnFound := cachedH2Conns[dest] + cachedH2Mutex.Unlock() + + if cachedConnFound { + rc, cc := cachedConn.rawConn, cachedConn.h2Conn + if cc.CanTakeNewRequest() { + proxyConn, err := connectHTTP2(rc, cc) + if err != nil { + return nil, err + } + + return proxyConn, nil + } + } + + rawConn, err := dialer.Dial(ctx, dest) + if err != nil { + return nil, err + } + + iConn := stat.TryUnwrapStatsConn(rawConn) + + nextProto := "" + if tlsConn, ok := iConn.(*tls.Conn); ok { + if err := tlsConn.HandshakeContext(ctx); err != nil { + rawConn.Close() + return nil, err + } + nextProto = tlsConn.ConnectionState().NegotiatedProtocol + } else if tlsConn, ok := iConn.(*tls.UConn); ok { + if err := tlsConn.HandshakeContext(ctx); err != nil { + rawConn.Close() + return nil, err + } + nextProto = tlsConn.ConnectionState().NegotiatedProtocol + } + + switch nextProto { + case "", "http/1.1": + return connectHTTP1(rawConn) + case "h2": + t := http2.Transport{} + h2clientConn, err := t.NewClientConn(rawConn) + if err != nil { + rawConn.Close() + return nil, err + } + + proxyConn, err := connectHTTP2(rawConn, h2clientConn) + if err != nil { + rawConn.Close() + return nil, err + } + + cachedH2Mutex.Lock() + if cachedH2Conns == nil { + cachedH2Conns = make(map[net.Destination]h2Conn) + } + + cachedH2Conns[dest] = h2Conn{ + rawConn: rawConn, + h2Conn: h2clientConn, + } + cachedH2Mutex.Unlock() + + return proxyConn, err + default: + return nil, errors.New("negotiated unsupported application layer protocol: " + nextProto) + } +} + +func newHTTP2Conn(c net.Conn, pipedReqBody *io.PipeWriter, respBody io.ReadCloser) net.Conn { + return &http2Conn{Conn: c, in: pipedReqBody, out: respBody} +} + +type http2Conn struct { + net.Conn + in *io.PipeWriter + out io.ReadCloser +} + +func (h *http2Conn) Read(p []byte) (n int, err error) { + return h.out.Read(p) +} + +func (h *http2Conn) Write(p []byte) (n int, err error) { + return h.in.Write(p) +} + +func (h *http2Conn) Close() error { + h.in.Close() + return h.out.Close() +} + +func init() { + common.Must(common.RegisterConfig((*ClientConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + return NewClient(ctx, config.(*ClientConfig)) + })) +} diff --git a/subproject/Xray-core-main/proxy/http/config.go b/subproject/Xray-core-main/proxy/http/config.go new file mode 100644 index 00000000..a41bb010 --- /dev/null +++ b/subproject/Xray-core-main/proxy/http/config.go @@ -0,0 +1,34 @@ +package http + +import ( + "google.golang.org/protobuf/proto" + + "github.com/xtls/xray-core/common/protocol" +) + +func (a *Account) Equals(another protocol.Account) bool { + if account, ok := another.(*Account); ok { + return a.Username == account.Username + } + return false +} + +func (a *Account) ToProto() proto.Message { + return a +} + +func (a *Account) AsAccount() (protocol.Account, error) { + return a, nil +} + +func (sc *ServerConfig) HasAccount(username, password string) bool { + if sc.Accounts == nil { + return false + } + + p, found := sc.Accounts[username] + if !found { + return false + } + return p == password +} diff --git a/subproject/Xray-core-main/proxy/http/config.pb.go b/subproject/Xray-core-main/proxy/http/config.pb.go new file mode 100644 index 00000000..fb9ca349 --- /dev/null +++ b/subproject/Xray-core-main/proxy/http/config.pb.go @@ -0,0 +1,322 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: proxy/http/config.proto + +package http + +import ( + protocol "github.com/xtls/xray-core/common/protocol" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Account struct { + state protoimpl.MessageState `protogen:"open.v1"` + Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"` + Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Account) Reset() { + *x = Account{} + mi := &file_proxy_http_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Account) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Account) ProtoMessage() {} + +func (x *Account) ProtoReflect() protoreflect.Message { + mi := &file_proxy_http_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Account.ProtoReflect.Descriptor instead. +func (*Account) Descriptor() ([]byte, []int) { + return file_proxy_http_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Account) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *Account) GetPassword() string { + if x != nil { + return x.Password + } + return "" +} + +// Config for HTTP proxy server. +type ServerConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + Accounts map[string]string `protobuf:"bytes,2,rep,name=accounts,proto3" json:"accounts,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + AllowTransparent bool `protobuf:"varint,3,opt,name=allow_transparent,json=allowTransparent,proto3" json:"allow_transparent,omitempty"` + UserLevel uint32 `protobuf:"varint,4,opt,name=user_level,json=userLevel,proto3" json:"user_level,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ServerConfig) Reset() { + *x = ServerConfig{} + mi := &file_proxy_http_config_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ServerConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ServerConfig) ProtoMessage() {} + +func (x *ServerConfig) ProtoReflect() protoreflect.Message { + mi := &file_proxy_http_config_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ServerConfig.ProtoReflect.Descriptor instead. +func (*ServerConfig) Descriptor() ([]byte, []int) { + return file_proxy_http_config_proto_rawDescGZIP(), []int{1} +} + +func (x *ServerConfig) GetAccounts() map[string]string { + if x != nil { + return x.Accounts + } + return nil +} + +func (x *ServerConfig) GetAllowTransparent() bool { + if x != nil { + return x.AllowTransparent + } + return false +} + +func (x *ServerConfig) GetUserLevel() uint32 { + if x != nil { + return x.UserLevel + } + return 0 +} + +type Header struct { + state protoimpl.MessageState `protogen:"open.v1"` + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Header) Reset() { + *x = Header{} + mi := &file_proxy_http_config_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Header) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Header) ProtoMessage() {} + +func (x *Header) ProtoReflect() protoreflect.Message { + mi := &file_proxy_http_config_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Header.ProtoReflect.Descriptor instead. +func (*Header) Descriptor() ([]byte, []int) { + return file_proxy_http_config_proto_rawDescGZIP(), []int{2} +} + +func (x *Header) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *Header) GetValue() string { + if x != nil { + return x.Value + } + return "" +} + +// ClientConfig is the protobuf config for HTTP proxy client. +type ClientConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Sever is a list of HTTP server addresses. + Server *protocol.ServerEndpoint `protobuf:"bytes,1,opt,name=server,proto3" json:"server,omitempty"` + Header []*Header `protobuf:"bytes,2,rep,name=header,proto3" json:"header,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ClientConfig) Reset() { + *x = ClientConfig{} + mi := &file_proxy_http_config_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ClientConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClientConfig) ProtoMessage() {} + +func (x *ClientConfig) ProtoReflect() protoreflect.Message { + mi := &file_proxy_http_config_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ClientConfig.ProtoReflect.Descriptor instead. +func (*ClientConfig) Descriptor() ([]byte, []int) { + return file_proxy_http_config_proto_rawDescGZIP(), []int{3} +} + +func (x *ClientConfig) GetServer() *protocol.ServerEndpoint { + if x != nil { + return x.Server + } + return nil +} + +func (x *ClientConfig) GetHeader() []*Header { + if x != nil { + return x.Header + } + return nil +} + +var File_proxy_http_config_proto protoreflect.FileDescriptor + +const file_proxy_http_config_proto_rawDesc = "" + + "\n" + + "\x17proxy/http/config.proto\x12\x0fxray.proxy.http\x1a!common/protocol/server_spec.proto\"A\n" + + "\aAccount\x12\x1a\n" + + "\busername\x18\x01 \x01(\tR\busername\x12\x1a\n" + + "\bpassword\x18\x02 \x01(\tR\bpassword\"\xe0\x01\n" + + "\fServerConfig\x12G\n" + + "\baccounts\x18\x02 \x03(\v2+.xray.proxy.http.ServerConfig.AccountsEntryR\baccounts\x12+\n" + + "\x11allow_transparent\x18\x03 \x01(\bR\x10allowTransparent\x12\x1d\n" + + "\n" + + "user_level\x18\x04 \x01(\rR\tuserLevel\x1a;\n" + + "\rAccountsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"0\n" + + "\x06Header\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value\"}\n" + + "\fClientConfig\x12<\n" + + "\x06server\x18\x01 \x01(\v2$.xray.common.protocol.ServerEndpointR\x06server\x12/\n" + + "\x06header\x18\x02 \x03(\v2\x17.xray.proxy.http.HeaderR\x06headerBO\n" + + "\x13com.xray.proxy.httpP\x01Z$github.com/xtls/xray-core/proxy/http\xaa\x02\x0fXray.Proxy.Httpb\x06proto3" + +var ( + file_proxy_http_config_proto_rawDescOnce sync.Once + file_proxy_http_config_proto_rawDescData []byte +) + +func file_proxy_http_config_proto_rawDescGZIP() []byte { + file_proxy_http_config_proto_rawDescOnce.Do(func() { + file_proxy_http_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proxy_http_config_proto_rawDesc), len(file_proxy_http_config_proto_rawDesc))) + }) + return file_proxy_http_config_proto_rawDescData +} + +var file_proxy_http_config_proto_msgTypes = make([]protoimpl.MessageInfo, 5) +var file_proxy_http_config_proto_goTypes = []any{ + (*Account)(nil), // 0: xray.proxy.http.Account + (*ServerConfig)(nil), // 1: xray.proxy.http.ServerConfig + (*Header)(nil), // 2: xray.proxy.http.Header + (*ClientConfig)(nil), // 3: xray.proxy.http.ClientConfig + nil, // 4: xray.proxy.http.ServerConfig.AccountsEntry + (*protocol.ServerEndpoint)(nil), // 5: xray.common.protocol.ServerEndpoint +} +var file_proxy_http_config_proto_depIdxs = []int32{ + 4, // 0: xray.proxy.http.ServerConfig.accounts:type_name -> xray.proxy.http.ServerConfig.AccountsEntry + 5, // 1: xray.proxy.http.ClientConfig.server:type_name -> xray.common.protocol.ServerEndpoint + 2, // 2: xray.proxy.http.ClientConfig.header:type_name -> xray.proxy.http.Header + 3, // [3:3] is the sub-list for method output_type + 3, // [3:3] is the sub-list for method input_type + 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_proxy_http_config_proto_init() } +func file_proxy_http_config_proto_init() { + if File_proxy_http_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_proxy_http_config_proto_rawDesc), len(file_proxy_http_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 5, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_proxy_http_config_proto_goTypes, + DependencyIndexes: file_proxy_http_config_proto_depIdxs, + MessageInfos: file_proxy_http_config_proto_msgTypes, + }.Build() + File_proxy_http_config_proto = out.File + file_proxy_http_config_proto_goTypes = nil + file_proxy_http_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/proxy/http/config.proto b/subproject/Xray-core-main/proxy/http/config.proto new file mode 100644 index 00000000..240c06b5 --- /dev/null +++ b/subproject/Xray-core-main/proxy/http/config.proto @@ -0,0 +1,33 @@ +syntax = "proto3"; + +package xray.proxy.http; +option csharp_namespace = "Xray.Proxy.Http"; +option go_package = "github.com/xtls/xray-core/proxy/http"; +option java_package = "com.xray.proxy.http"; +option java_multiple_files = true; + +import "common/protocol/server_spec.proto"; + +message Account { + string username = 1; + string password = 2; +} + +// Config for HTTP proxy server. +message ServerConfig { + map accounts = 2; + bool allow_transparent = 3; + uint32 user_level = 4; +} + +message Header { + string key = 1; + string value = 2; +} + +// ClientConfig is the protobuf config for HTTP proxy client. +message ClientConfig { + // Sever is a list of HTTP server addresses. + xray.common.protocol.ServerEndpoint server = 1; + repeated Header header = 2; +} diff --git a/subproject/Xray-core-main/proxy/http/http.go b/subproject/Xray-core-main/proxy/http/http.go new file mode 100644 index 00000000..d02cfda6 --- /dev/null +++ b/subproject/Xray-core-main/proxy/http/http.go @@ -0,0 +1 @@ +package http diff --git a/subproject/Xray-core-main/proxy/http/server.go b/subproject/Xray-core-main/proxy/http/server.go new file mode 100644 index 00000000..90a07b38 --- /dev/null +++ b/subproject/Xray-core-main/proxy/http/server.go @@ -0,0 +1,350 @@ +package http + +import ( + "bufio" + "bytes" + "context" + "encoding/base64" + "io" + "net/http" + "strings" + "time" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/log" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + http_proto "github.com/xtls/xray-core/common/protocol/http" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/common/task" + "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/features/policy" + "github.com/xtls/xray-core/features/routing" + "github.com/xtls/xray-core/proxy" + "github.com/xtls/xray-core/transport" + "github.com/xtls/xray-core/transport/internet/stat" +) + +// Server is an HTTP proxy server. +type Server struct { + config *ServerConfig + policyManager policy.Manager +} + +// NewServer creates a new HTTP inbound handler. +func NewServer(ctx context.Context, config *ServerConfig) (*Server, error) { + v := core.MustFromContext(ctx) + s := &Server{ + config: config, + policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager), + } + + return s, nil +} + +func (s *Server) policy() policy.Session { + config := s.config + p := s.policyManager.ForLevel(config.UserLevel) + return p +} + +// Network implements proxy.Inbound. +func (*Server) Network() []net.Network { + return []net.Network{net.Network_TCP, net.Network_UNIX} +} + +func isTimeout(err error) bool { + nerr, ok := errors.Cause(err).(net.Error) + return ok && nerr.Timeout() +} + +func parseBasicAuth(auth string) (username, password string, ok bool) { + const prefix = "Basic " + if !strings.HasPrefix(auth, prefix) { + return + } + c, err := base64.StdEncoding.DecodeString(auth[len(prefix):]) + if err != nil { + return + } + cs := string(c) + s := strings.IndexByte(cs, ':') + if s < 0 { + return + } + return cs[:s], cs[s+1:], true +} + +type readerOnly struct { + io.Reader +} + +func (s *Server) Process(ctx context.Context, network net.Network, conn stat.Connection, dispatcher routing.Dispatcher) error { + return s.ProcessWithFirstbyte(ctx, network, conn, dispatcher) +} + +// Firstbyte is for forwarded conn from SOCKS inbound +// Because it needs first byte to choose protocol +// We need to add it back +// Other parts are the same as the process function +func (s *Server) ProcessWithFirstbyte(ctx context.Context, network net.Network, conn stat.Connection, dispatcher routing.Dispatcher, firstbyte ...byte) error { + inbound := session.InboundFromContext(ctx) + inbound.Name = "http" + inbound.CanSpliceCopy = 2 + inbound.User = &protocol.MemoryUser{ + Level: s.config.UserLevel, + } + if !proxy.IsRAWTransportWithoutSecurity(conn) { + inbound.CanSpliceCopy = 3 + } + var reader *bufio.Reader + if len(firstbyte) > 0 { + readerWithoutFirstbyte := bufio.NewReaderSize(readerOnly{conn}, buf.Size) + multiReader := io.MultiReader(bytes.NewReader(firstbyte), readerWithoutFirstbyte) + reader = bufio.NewReaderSize(multiReader, buf.Size) + } else { + reader = bufio.NewReaderSize(readerOnly{conn}, buf.Size) + } + +Start: + if err := conn.SetReadDeadline(time.Now().Add(s.policy().Timeouts.Handshake)); err != nil { + errors.LogInfoInner(ctx, err, "failed to set read deadline") + } + + request, err := http.ReadRequest(reader) + if err != nil { + trace := errors.New("failed to read http request").Base(err) + if errors.Cause(err) != io.EOF && !isTimeout(errors.Cause(err)) { + trace.AtWarning() + } + return trace + } + + if len(s.config.Accounts) > 0 { + user, pass, ok := parseBasicAuth(request.Header.Get("Proxy-Authorization")) + if !ok || !s.config.HasAccount(user, pass) { + return common.Error2(conn.Write([]byte("HTTP/1.1 407 Proxy Authentication Required\r\nProxy-Authenticate: Basic realm=\"proxy\"\r\n\r\n"))) + } + if inbound != nil { + inbound.User.Email = user + } + } + + errors.LogInfo(ctx, "request to Method [", request.Method, "] Host [", request.Host, "] with URL [", request.URL, "]") + if err := conn.SetReadDeadline(time.Time{}); err != nil { + errors.LogDebugInner(ctx, err, "failed to clear read deadline") + } + + defaultPort := net.Port(80) + if strings.EqualFold(request.URL.Scheme, "https") { + defaultPort = net.Port(443) + } + host := request.Host + if host == "" { + host = request.URL.Host + } + dest, err := http_proto.ParseHost(host, defaultPort) + if err != nil { + return errors.New("malformed proxy host: ", host).AtWarning().Base(err) + } + ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{ + From: conn.RemoteAddr(), + To: request.URL, + Status: log.AccessAccepted, + Reason: "", + }) + + if strings.EqualFold(request.Method, "CONNECT") { + return s.handleConnect(ctx, request, reader, conn, dest, dispatcher, inbound) + } + + keepAlive := (strings.TrimSpace(strings.ToLower(request.Header.Get("Proxy-Connection"))) == "keep-alive") + + err = s.handlePlainHTTP(ctx, request, conn, dest, dispatcher) + if err == errWaitAnother { + if keepAlive { + goto Start + } + err = nil + } + + return err +} + +func (s *Server) handleConnect(ctx context.Context, _ *http.Request, buffer *bufio.Reader, conn stat.Connection, dest net.Destination, dispatcher routing.Dispatcher, inbound *session.Inbound) error { + _, err := conn.Write([]byte("HTTP/1.1 200 Connection established\r\n\r\n")) + if err != nil { + return errors.New("failed to write back OK response").Base(err) + } + + reader := buf.NewReader(conn) + if buffer.Buffered() > 0 { + payload, err := buf.ReadFrom(io.LimitReader(buffer, int64(buffer.Buffered()))) + if err != nil { + return err + } + reader = &buf.BufferedReader{Reader: reader, Buffer: payload} + buffer = nil + } + + if inbound.CanSpliceCopy == 2 { + inbound.CanSpliceCopy = 1 + } + if err := dispatcher.DispatchLink(ctx, dest, &transport.Link{ + Reader: reader, + Writer: buf.NewWriter(conn)}, + ); err != nil { + return errors.New("failed to dispatch request").Base(err) + } + return nil +} + +var errWaitAnother = errors.New("keep alive") + +func (s *Server) handlePlainHTTP(ctx context.Context, request *http.Request, writer io.Writer, dest net.Destination, dispatcher routing.Dispatcher) error { + if !s.config.AllowTransparent && request.URL.Host == "" { + // RFC 2068 (HTTP/1.1) requires URL to be absolute URL in HTTP proxy. + response := &http.Response{ + Status: "Bad Request", + StatusCode: 400, + Proto: "HTTP/1.1", + ProtoMajor: 1, + ProtoMinor: 1, + Header: http.Header(make(map[string][]string)), + Body: nil, + ContentLength: 0, + Close: true, + } + response.Header.Set("Proxy-Connection", "close") + response.Header.Set("Connection", "close") + return response.Write(writer) + } + + if len(request.URL.Host) > 0 { + request.Host = request.URL.Host + } + http_proto.RemoveHopByHopHeaders(request.Header) + + // Prevent UA from being set to golang's default ones + if request.Header.Get("User-Agent") == "" { + request.Header.Set("User-Agent", "") + } + + content := &session.Content{ + Protocol: "http/1.1", + } + + content.SetAttribute(":method", strings.ToUpper(request.Method)) + content.SetAttribute(":path", request.URL.Path) + for key := range request.Header { + value := request.Header.Get(key) + content.SetAttribute(strings.ToLower(key), value) + } + + ctx = session.ContextWithContent(ctx, content) + + link, err := dispatcher.Dispatch(ctx, dest) + if err != nil { + return err + } + + // Plain HTTP request is not a stream. The request always finishes before response. Hense request has to be closed later. + defer common.Close(link.Writer) + var result error = errWaitAnother + + requestDone := func() error { + request.Header.Set("Connection", "close") + + requestWriter := buf.NewBufferedWriter(link.Writer) + common.Must(requestWriter.SetBuffered(false)) + if err := request.Write(requestWriter); err != nil { + return errors.New("failed to write whole request").Base(err).AtWarning() + } + return nil + } + + responseDone := func() error { + responseReader := bufio.NewReaderSize(&buf.BufferedReader{Reader: link.Reader}, buf.Size) + response, err := readResponseAndHandle100Continue(responseReader, request, writer) + if err == nil { + http_proto.RemoveHopByHopHeaders(response.Header) + if response.ContentLength >= 0 { + response.Header.Set("Proxy-Connection", "keep-alive") + response.Header.Set("Connection", "keep-alive") + response.Header.Set("Keep-Alive", "timeout=60") + response.Close = false + } else { + response.Close = true + result = nil + } + defer response.Body.Close() + } else { + errors.LogWarningInner(ctx, err, "failed to read response from ", request.Host) + response = &http.Response{ + Status: "Service Unavailable", + StatusCode: 503, + Proto: "HTTP/1.1", + ProtoMajor: 1, + ProtoMinor: 1, + Header: http.Header(make(map[string][]string)), + Body: nil, + ContentLength: 0, + Close: true, + } + response.Header.Set("Connection", "close") + response.Header.Set("Proxy-Connection", "close") + } + if err := response.Write(writer); err != nil { + return errors.New("failed to write response").Base(err).AtWarning() + } + return nil + } + + if err := task.Run(ctx, requestDone, responseDone); err != nil { + common.Interrupt(link.Reader) + common.Interrupt(link.Writer) + return errors.New("connection ends").Base(err) + } + + return result +} + +// Sometimes, server might send 1xx response to client +// it should not be processed by http proxy handler, just forward it to client +func readResponseAndHandle100Continue(r *bufio.Reader, req *http.Request, writer io.Writer) (*http.Response, error) { + // have a little look of response + peekBytes, err := r.Peek(56) + if err == nil || err == bufio.ErrBufferFull { + str := string(peekBytes) + ResponseLine := strings.Split(str, "\r\n")[0] + _, status, _ := strings.Cut(ResponseLine, " ") + // only handle 1xx response + if strings.HasPrefix(status, "1") { + ResponseHeader1xx := []byte{} + // read until \r\n\r\n (end of http response header) + for { + data, err := r.ReadSlice('\n') + if err != nil { + return nil, errors.New("failed to read http 1xx response").Base(err) + } + ResponseHeader1xx = append(ResponseHeader1xx, data...) + if bytes.Equal(ResponseHeader1xx[len(ResponseHeader1xx)-4:], []byte{'\r', '\n', '\r', '\n'}) { + break + } + if len(ResponseHeader1xx) > 1024 { + return nil, errors.New("too big http 1xx response") + } + } + writer.Write(ResponseHeader1xx) + } + } + return http.ReadResponse(r, req) +} + +func init() { + common.Must(common.RegisterConfig((*ServerConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + return NewServer(ctx, config.(*ServerConfig)) + })) +} diff --git a/subproject/Xray-core-main/proxy/hysteria/account/config.go b/subproject/Xray-core-main/proxy/hysteria/account/config.go new file mode 100644 index 00000000..0e50dcc9 --- /dev/null +++ b/subproject/Xray-core-main/proxy/hysteria/account/config.go @@ -0,0 +1,129 @@ +package account + +import ( + "sync" + + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/protocol" + + "google.golang.org/protobuf/proto" +) + +func (a *Account) AsAccount() (protocol.Account, error) { + return &MemoryAccount{ + Auth: a.Auth, + }, nil +} + +type MemoryAccount struct { + Auth string +} + +func (a *MemoryAccount) Equals(another protocol.Account) bool { + if account, ok := another.(*MemoryAccount); ok { + return a.Auth == account.Auth + } + return false +} + +func (a *MemoryAccount) ToProto() proto.Message { + return &Account{ + Auth: a.Auth, + } +} + +type Validator struct { + emails map[string]struct{} + users map[string]*protocol.MemoryUser + + mutex sync.Mutex +} + +func NewValidator() *Validator { + return &Validator{ + emails: make(map[string]struct{}), + users: make(map[string]*protocol.MemoryUser), + } +} + +func (v *Validator) Add(u *protocol.MemoryUser) error { + v.mutex.Lock() + defer v.mutex.Unlock() + + if u.Email != "" { + if _, ok := v.emails[u.Email]; ok { + return errors.New("User ", u.Email, " already exists.") + } + v.emails[u.Email] = struct{}{} + } + v.users[u.Account.(*MemoryAccount).Auth] = u + + return nil +} + +func (v *Validator) Del(email string) error { + if email == "" { + return errors.New("Email must not be empty.") + } + + v.mutex.Lock() + defer v.mutex.Unlock() + + if _, ok := v.emails[email]; !ok { + return errors.New("User ", email, " not found.") + } + delete(v.emails, email) + for key, user := range v.users { + if user.Email == email { + delete(v.users, key) + break + } + } + + return nil +} + +func (v *Validator) Get(auth string) *protocol.MemoryUser { + v.mutex.Lock() + defer v.mutex.Unlock() + + return v.users[auth] +} + +func (v *Validator) GetByEmail(email string) *protocol.MemoryUser { + if email == "" { + return nil + } + + v.mutex.Lock() + defer v.mutex.Unlock() + + if _, ok := v.emails[email]; ok { + for _, user := range v.users { + if user.Email == email { + return user + } + } + } + + return nil +} + +func (v *Validator) GetAll() []*protocol.MemoryUser { + v.mutex.Lock() + defer v.mutex.Unlock() + + var users = make([]*protocol.MemoryUser, 0, len(v.users)) + for _, user := range v.users { + users = append(users, user) + } + + return users +} + +func (v *Validator) GetCount() int64 { + v.mutex.Lock() + defer v.mutex.Unlock() + + return int64(len(v.users)) +} diff --git a/subproject/Xray-core-main/proxy/hysteria/account/config.pb.go b/subproject/Xray-core-main/proxy/hysteria/account/config.pb.go new file mode 100644 index 00000000..f48dca32 --- /dev/null +++ b/subproject/Xray-core-main/proxy/hysteria/account/config.pb.go @@ -0,0 +1,123 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: proxy/hysteria/account/config.proto + +package account + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Account struct { + state protoimpl.MessageState `protogen:"open.v1"` + Auth string `protobuf:"bytes,1,opt,name=auth,proto3" json:"auth,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Account) Reset() { + *x = Account{} + mi := &file_proxy_hysteria_account_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Account) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Account) ProtoMessage() {} + +func (x *Account) ProtoReflect() protoreflect.Message { + mi := &file_proxy_hysteria_account_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Account.ProtoReflect.Descriptor instead. +func (*Account) Descriptor() ([]byte, []int) { + return file_proxy_hysteria_account_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Account) GetAuth() string { + if x != nil { + return x.Auth + } + return "" +} + +var File_proxy_hysteria_account_config_proto protoreflect.FileDescriptor + +const file_proxy_hysteria_account_config_proto_rawDesc = "" + + "\n" + + "#proxy/hysteria/account/config.proto\x12\x1bxray.proxy.hysteria.account\"\x1d\n" + + "\aAccount\x12\x12\n" + + "\x04auth\x18\x01 \x01(\tR\x04authBs\n" + + "\x1fcom.xray.proxy.hysteria.accountP\x01Z0github.com/xtls/xray-core/proxy/hysteria/account\xaa\x02\x1bXray.Proxy.Hysteria.Accountb\x06proto3" + +var ( + file_proxy_hysteria_account_config_proto_rawDescOnce sync.Once + file_proxy_hysteria_account_config_proto_rawDescData []byte +) + +func file_proxy_hysteria_account_config_proto_rawDescGZIP() []byte { + file_proxy_hysteria_account_config_proto_rawDescOnce.Do(func() { + file_proxy_hysteria_account_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proxy_hysteria_account_config_proto_rawDesc), len(file_proxy_hysteria_account_config_proto_rawDesc))) + }) + return file_proxy_hysteria_account_config_proto_rawDescData +} + +var file_proxy_hysteria_account_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_proxy_hysteria_account_config_proto_goTypes = []any{ + (*Account)(nil), // 0: xray.proxy.hysteria.account.Account +} +var file_proxy_hysteria_account_config_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_proxy_hysteria_account_config_proto_init() } +func file_proxy_hysteria_account_config_proto_init() { + if File_proxy_hysteria_account_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_proxy_hysteria_account_config_proto_rawDesc), len(file_proxy_hysteria_account_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_proxy_hysteria_account_config_proto_goTypes, + DependencyIndexes: file_proxy_hysteria_account_config_proto_depIdxs, + MessageInfos: file_proxy_hysteria_account_config_proto_msgTypes, + }.Build() + File_proxy_hysteria_account_config_proto = out.File + file_proxy_hysteria_account_config_proto_goTypes = nil + file_proxy_hysteria_account_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/proxy/hysteria/account/config.proto b/subproject/Xray-core-main/proxy/hysteria/account/config.proto new file mode 100644 index 00000000..48f64e64 --- /dev/null +++ b/subproject/Xray-core-main/proxy/hysteria/account/config.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; + +package xray.proxy.hysteria.account; +option csharp_namespace = "Xray.Proxy.Hysteria.Account"; +option go_package = "github.com/xtls/xray-core/proxy/hysteria/account"; +option java_package = "com.xray.proxy.hysteria.account"; +option java_multiple_files = true; + +message Account { + string auth = 1; +} \ No newline at end of file diff --git a/subproject/Xray-core-main/proxy/hysteria/client.go b/subproject/Xray-core-main/proxy/hysteria/client.go new file mode 100644 index 00000000..1dcb5cf9 --- /dev/null +++ b/subproject/Xray-core-main/proxy/hysteria/client.go @@ -0,0 +1,283 @@ +package hysteria + +import ( + "context" + go_errors "errors" + "io" + "math/rand" + + "github.com/apernet/quic-go" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/common/signal" + "github.com/xtls/xray-core/common/task" + "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/features/policy" + hyCtx "github.com/xtls/xray-core/proxy/hysteria/ctx" + "github.com/xtls/xray-core/transport" + "github.com/xtls/xray-core/transport/internet" + "github.com/xtls/xray-core/transport/internet/hysteria" + "github.com/xtls/xray-core/transport/internet/stat" +) + +type Client struct { + server *protocol.ServerSpec + policyManager policy.Manager +} + +func NewClient(ctx context.Context, config *ClientConfig) (*Client, error) { + if config.Server == nil { + return nil, errors.New(`no target server found`) + } + server, err := protocol.NewServerSpecFromPB(config.Server) + if err != nil { + return nil, errors.New("failed to get server spec").Base(err) + } + + v := core.MustFromContext(ctx) + client := &Client{ + server: server, + policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager), + } + return client, nil +} + +func (c *Client) Process(ctx context.Context, link *transport.Link, dialer internet.Dialer) error { + outbounds := session.OutboundsFromContext(ctx) + ob := outbounds[len(outbounds)-1] + if !ob.Target.IsValid() { + return errors.New("target not specified") + } + ob.Name = "hysteria" + ob.CanSpliceCopy = 3 + target := ob.Target + + conn, err := dialer.Dial(hyCtx.ContextWithRequireDatagram(ctx, target.Network == net.Network_UDP), c.server.Destination) + if err != nil { + return errors.New("failed to find an available destination").AtWarning().Base(err) + } + defer conn.Close() + errors.LogInfo(ctx, "tunneling request to ", target, " via ", target.Network, ":", c.server.Destination.NetAddr()) + + var newCtx context.Context + var newCancel context.CancelFunc + if session.TimeoutOnlyFromContext(ctx) { + newCtx, newCancel = context.WithCancel(context.Background()) + } + + sessionPolicy := c.policyManager.ForLevel(0) + ctx, cancel := context.WithCancel(ctx) + timer := signal.CancelAfterInactivity(ctx, func() { + cancel() + if newCancel != nil { + newCancel() + } + }, sessionPolicy.Timeouts.ConnectionIdle) + + if newCtx != nil { + ctx = newCtx + } + + if target.Network == net.Network_TCP { + requestDone := func() error { + defer timer.SetTimeout(sessionPolicy.Timeouts.DownlinkOnly) + bufferedWriter := buf.NewBufferedWriter(buf.NewWriter(conn)) + err := WriteTCPRequest(bufferedWriter, target.NetAddr()) + if err != nil { + return errors.New("failed to write request").Base(err) + } + if err := bufferedWriter.SetBuffered(false); err != nil { + return err + } + return buf.Copy(link.Reader, bufferedWriter, buf.UpdateActivity(timer)) + } + + responseDone := func() error { + defer timer.SetTimeout(sessionPolicy.Timeouts.UplinkOnly) + ok, msg, err := ReadTCPResponse(conn) + if err != nil { + return err + } + if !ok { + return errors.New(msg) + } + return buf.Copy(buf.NewReader(conn), link.Writer, buf.UpdateActivity(timer)) + } + + responseDoneAndCloseWriter := task.OnSuccess(responseDone, task.Close(link.Writer)) + if err := task.Run(ctx, requestDone, responseDoneAndCloseWriter); err != nil { + return errors.New("connection ends").Base(err) + } + + return nil + } + + if target.Network == net.Network_UDP { + iConn := stat.TryUnwrapStatsConn(conn) + _, ok := iConn.(*hysteria.InterUdpConn) + if !ok { + return errors.New("udp requires hysteria udp transport") + } + + requestDone := func() error { + defer timer.SetTimeout(sessionPolicy.Timeouts.DownlinkOnly) + + writer := &UDPWriter{ + Writer: conn, + buf: make([]byte, MaxUDPSize), + addr: target.NetAddr(), + } + + if err := buf.Copy(link.Reader, writer, buf.UpdateActivity(timer)); err != nil { + return errors.New("failed to transport all UDP request").Base(err) + } + + return nil + } + + responseDone := func() error { + defer timer.SetTimeout(sessionPolicy.Timeouts.UplinkOnly) + + reader := &UDPReader{ + Reader: conn, + buf: make([]byte, MaxUDPSize), + df: &Defragger{}, + } + + if err := buf.Copy(reader, link.Writer, buf.UpdateActivity(timer)); err != nil { + return errors.New("failed to transport all UDP response").Base(err) + } + + return nil + } + + responseDoneAndCloseWriter := task.OnSuccess(responseDone, task.Close(link.Writer)) + if err := task.Run(ctx, requestDone, responseDoneAndCloseWriter); err != nil { + return errors.New("connection ends").Base(err) + } + + return nil + } + + return nil +} + +func init() { + common.Must(common.RegisterConfig((*ClientConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + return NewClient(ctx, config.(*ClientConfig)) + })) +} + +type UDPWriter struct { + Writer io.Writer + buf []byte + addr string +} + +func (w *UDPWriter) sendMsg(msg *UDPMessage) error { + msgN := msg.Serialize(w.buf) + if msgN < 0 { + return nil + } + _, err := w.Writer.Write(w.buf[:msgN]) + return err +} + +func (w *UDPWriter) WriteMultiBuffer(mb buf.MultiBuffer) error { + for { + mb2, b := buf.SplitFirst(mb) + mb = mb2 + if b == nil { + break + } + + addr := w.addr + if b.UDP != nil { + addr = b.UDP.NetAddr() + } + + msg := &UDPMessage{ + SessionID: 0, + PacketID: 0, + FragID: 0, + FragCount: 1, + Addr: addr, + Data: b.Bytes(), + } + + err := w.sendMsg(msg) + var errTooLarge *quic.DatagramTooLargeError + if go_errors.As(err, &errTooLarge) { + msg.PacketID = uint16(rand.Intn(0xFFFF)) + 1 + fMsgs := FragUDPMessage(msg, int(errTooLarge.MaxDatagramPayloadSize)) + for _, fMsg := range fMsgs { + err := w.sendMsg(&fMsg) + if err != nil { + b.Release() + buf.ReleaseMulti(mb) + return err + } + } + } else if err != nil { + b.Release() + buf.ReleaseMulti(mb) + return err + } + + b.Release() + } + + return nil +} + +type UDPReader struct { + Reader io.Reader + buf []byte + df *Defragger + firstMsg *UDPMessage + firstDest *net.Destination +} + +func (r *UDPReader) ReadMultiBuffer() (buf.MultiBuffer, error) { + if r.firstMsg != nil { + buffer := buf.New() + buffer.Write(r.firstMsg.Data) + buffer.UDP = r.firstDest + + r.firstMsg = nil + + return buf.MultiBuffer{buffer}, nil + } + for { + n, err := r.Reader.Read(r.buf) + if err != nil { + return nil, err + } + + msg, err := ParseUDPMessage(r.buf[:n]) + if err != nil { + continue + } + + dfMsg := r.df.Feed(msg) + if dfMsg == nil { + continue + } + + dest, err := net.ParseDestination("udp:" + dfMsg.Addr) + if err != nil { + errors.LogDebug(context.Background(), dfMsg.Addr, " ParseDestination err ", err) + continue + } + + buffer := buf.New() + buffer.Write(dfMsg.Data) + buffer.UDP = &dest + + return buf.MultiBuffer{buffer}, nil + } +} diff --git a/subproject/Xray-core-main/proxy/hysteria/config.go b/subproject/Xray-core-main/proxy/hysteria/config.go new file mode 100644 index 00000000..1daedf03 --- /dev/null +++ b/subproject/Xray-core-main/proxy/hysteria/config.go @@ -0,0 +1,10 @@ +package hysteria + +import ( + "github.com/xtls/xray-core/transport/internet/hysteria/padding" +) + +var ( + tcpRequestPadding = padding.Padding{Min: 64, Max: 512} + tcpResponsePadding = padding.Padding{Min: 128, Max: 1024} +) diff --git a/subproject/Xray-core-main/proxy/hysteria/config.pb.go b/subproject/Xray-core-main/proxy/hysteria/config.pb.go new file mode 100644 index 00000000..5764b78c --- /dev/null +++ b/subproject/Xray-core-main/proxy/hysteria/config.pb.go @@ -0,0 +1,184 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: proxy/hysteria/config.proto + +package hysteria + +import ( + protocol "github.com/xtls/xray-core/common/protocol" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type ClientConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + Version int32 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"` + Server *protocol.ServerEndpoint `protobuf:"bytes,2,opt,name=server,proto3" json:"server,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ClientConfig) Reset() { + *x = ClientConfig{} + mi := &file_proxy_hysteria_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ClientConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClientConfig) ProtoMessage() {} + +func (x *ClientConfig) ProtoReflect() protoreflect.Message { + mi := &file_proxy_hysteria_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ClientConfig.ProtoReflect.Descriptor instead. +func (*ClientConfig) Descriptor() ([]byte, []int) { + return file_proxy_hysteria_config_proto_rawDescGZIP(), []int{0} +} + +func (x *ClientConfig) GetVersion() int32 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *ClientConfig) GetServer() *protocol.ServerEndpoint { + if x != nil { + return x.Server + } + return nil +} + +type ServerConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + Users []*protocol.User `protobuf:"bytes,1,rep,name=users,proto3" json:"users,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ServerConfig) Reset() { + *x = ServerConfig{} + mi := &file_proxy_hysteria_config_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ServerConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ServerConfig) ProtoMessage() {} + +func (x *ServerConfig) ProtoReflect() protoreflect.Message { + mi := &file_proxy_hysteria_config_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ServerConfig.ProtoReflect.Descriptor instead. +func (*ServerConfig) Descriptor() ([]byte, []int) { + return file_proxy_hysteria_config_proto_rawDescGZIP(), []int{1} +} + +func (x *ServerConfig) GetUsers() []*protocol.User { + if x != nil { + return x.Users + } + return nil +} + +var File_proxy_hysteria_config_proto protoreflect.FileDescriptor + +const file_proxy_hysteria_config_proto_rawDesc = "" + + "\n" + + "\x1bproxy/hysteria/config.proto\x12\x13xray.proxy.hysteria\x1a!common/protocol/server_spec.proto\x1a\x1acommon/protocol/user.proto\"f\n" + + "\fClientConfig\x12\x18\n" + + "\aversion\x18\x01 \x01(\x05R\aversion\x12<\n" + + "\x06server\x18\x02 \x01(\v2$.xray.common.protocol.ServerEndpointR\x06server\"@\n" + + "\fServerConfig\x120\n" + + "\x05users\x18\x01 \x03(\v2\x1a.xray.common.protocol.UserR\x05usersB[\n" + + "\x17com.xray.proxy.hysteriaP\x01Z(github.com/xtls/xray-core/proxy/hysteria\xaa\x02\x13Xray.Proxy.Hysteriab\x06proto3" + +var ( + file_proxy_hysteria_config_proto_rawDescOnce sync.Once + file_proxy_hysteria_config_proto_rawDescData []byte +) + +func file_proxy_hysteria_config_proto_rawDescGZIP() []byte { + file_proxy_hysteria_config_proto_rawDescOnce.Do(func() { + file_proxy_hysteria_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proxy_hysteria_config_proto_rawDesc), len(file_proxy_hysteria_config_proto_rawDesc))) + }) + return file_proxy_hysteria_config_proto_rawDescData +} + +var file_proxy_hysteria_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_proxy_hysteria_config_proto_goTypes = []any{ + (*ClientConfig)(nil), // 0: xray.proxy.hysteria.ClientConfig + (*ServerConfig)(nil), // 1: xray.proxy.hysteria.ServerConfig + (*protocol.ServerEndpoint)(nil), // 2: xray.common.protocol.ServerEndpoint + (*protocol.User)(nil), // 3: xray.common.protocol.User +} +var file_proxy_hysteria_config_proto_depIdxs = []int32{ + 2, // 0: xray.proxy.hysteria.ClientConfig.server:type_name -> xray.common.protocol.ServerEndpoint + 3, // 1: xray.proxy.hysteria.ServerConfig.users:type_name -> xray.common.protocol.User + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_proxy_hysteria_config_proto_init() } +func file_proxy_hysteria_config_proto_init() { + if File_proxy_hysteria_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_proxy_hysteria_config_proto_rawDesc), len(file_proxy_hysteria_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_proxy_hysteria_config_proto_goTypes, + DependencyIndexes: file_proxy_hysteria_config_proto_depIdxs, + MessageInfos: file_proxy_hysteria_config_proto_msgTypes, + }.Build() + File_proxy_hysteria_config_proto = out.File + file_proxy_hysteria_config_proto_goTypes = nil + file_proxy_hysteria_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/proxy/hysteria/config.proto b/subproject/Xray-core-main/proxy/hysteria/config.proto new file mode 100644 index 00000000..03fdf4e6 --- /dev/null +++ b/subproject/Xray-core-main/proxy/hysteria/config.proto @@ -0,0 +1,19 @@ +syntax = "proto3"; + +package xray.proxy.hysteria; +option csharp_namespace = "Xray.Proxy.Hysteria"; +option go_package = "github.com/xtls/xray-core/proxy/hysteria"; +option java_package = "com.xray.proxy.hysteria"; +option java_multiple_files = true; + +import "common/protocol/server_spec.proto"; +import "common/protocol/user.proto"; + +message ClientConfig { + int32 version = 1; + xray.common.protocol.ServerEndpoint server = 2; +} + +message ServerConfig { + repeated xray.common.protocol.User users = 1; +} \ No newline at end of file diff --git a/subproject/Xray-core-main/proxy/hysteria/ctx/ctx.go b/subproject/Xray-core-main/proxy/hysteria/ctx/ctx.go new file mode 100644 index 00000000..4e1b290c --- /dev/null +++ b/subproject/Xray-core-main/proxy/hysteria/ctx/ctx.go @@ -0,0 +1,35 @@ +package ctx + +import ( + "context" + + "github.com/xtls/xray-core/proxy/hysteria/account" +) + +type key int + +const ( + requireDatagram key = iota + validator +) + +func ContextWithRequireDatagram(ctx context.Context, udp bool) context.Context { + if !udp { + return ctx + } + return context.WithValue(ctx, requireDatagram, struct{}{}) +} + +func RequireDatagramFromContext(ctx context.Context) bool { + _, ok := ctx.Value(requireDatagram).(struct{}) + return ok +} + +func ContextWithValidator(ctx context.Context, v *account.Validator) context.Context { + return context.WithValue(ctx, validator, v) +} + +func ValidatorFromContext(ctx context.Context) *account.Validator { + v, _ := ctx.Value(validator).(*account.Validator) + return v +} diff --git a/subproject/Xray-core-main/proxy/hysteria/frag.go b/subproject/Xray-core-main/proxy/hysteria/frag.go new file mode 100644 index 00000000..64a6b0e1 --- /dev/null +++ b/subproject/Xray-core-main/proxy/hysteria/frag.go @@ -0,0 +1,73 @@ +package hysteria + +func FragUDPMessage(m *UDPMessage, maxSize int) []UDPMessage { + if m.Size() <= maxSize { + return []UDPMessage{*m} + } + fullPayload := m.Data + maxPayloadSize := maxSize - m.HeaderSize() + off := 0 + fragID := uint8(0) + fragCount := uint8((len(fullPayload) + maxPayloadSize - 1) / maxPayloadSize) // round up + frags := make([]UDPMessage, fragCount) + for off < len(fullPayload) { + payloadSize := len(fullPayload) - off + if payloadSize > maxPayloadSize { + payloadSize = maxPayloadSize + } + frag := *m + frag.FragID = fragID + frag.FragCount = fragCount + frag.Data = fullPayload[off : off+payloadSize] + frags[fragID] = frag + off += payloadSize + fragID++ + } + return frags +} + +// Defragger handles the defragmentation of UDP messages. +// The current implementation can only handle one packet ID at a time. +// If another packet arrives before a packet has received all fragments +// in their entirety, any previous state is discarded. +type Defragger struct { + pktID uint16 + frags []*UDPMessage + count uint8 + size int // data size +} + +func (d *Defragger) Feed(m *UDPMessage) *UDPMessage { + if m.FragCount <= 1 { + return m + } + if m.FragID >= m.FragCount { + // wtf is this? + return nil + } + if m.PacketID != d.pktID || m.FragCount != uint8(len(d.frags)) { + // new message, clear previous state + d.pktID = m.PacketID + d.frags = make([]*UDPMessage, m.FragCount) + d.frags[m.FragID] = m + d.count = 1 + d.size = len(m.Data) + } else if d.frags[m.FragID] == nil { + d.frags[m.FragID] = m + d.count++ + d.size += len(m.Data) + if int(d.count) == len(d.frags) { + // all fragments received, assemble + data := make([]byte, d.size) + off := 0 + for _, frag := range d.frags { + off += copy(data[off:], frag.Data) + } + m.Data = data + m.FragID = 0 + m.FragCount = 1 + return m + } + } + return nil +} diff --git a/subproject/Xray-core-main/proxy/hysteria/protocol.go b/subproject/Xray-core-main/proxy/hysteria/protocol.go new file mode 100644 index 00000000..b838d15a --- /dev/null +++ b/subproject/Xray-core-main/proxy/hysteria/protocol.go @@ -0,0 +1,249 @@ +package hysteria + +import ( + "bytes" + "encoding/binary" + "fmt" + "io" + + "github.com/apernet/quic-go/quicvarint" + "github.com/xtls/xray-core/common/errors" +) + +const ( + // Max length values are for preventing DoS attacks + + MaxAddressLength = 2048 + MaxMessageLength = 2048 + MaxPaddingLength = 4096 + + MaxUDPSize = 4096 + + maxVarInt1 = 63 + maxVarInt2 = 16383 + maxVarInt4 = 1073741823 + maxVarInt8 = 4611686018427387903 +) + +// TCPRequest format: +// Address length (QUIC varint) +// Address (bytes) +// Padding length (QUIC varint) +// Padding (bytes) + +func ReadTCPRequest(r io.Reader) (string, error) { + bReader := quicvarint.NewReader(r) + addrLen, err := quicvarint.Read(bReader) + if err != nil { + return "", err + } + if addrLen == 0 || addrLen > MaxAddressLength { + return "", errors.New("invalid address length") + } + addrBuf := make([]byte, addrLen) + _, err = io.ReadFull(r, addrBuf) + if err != nil { + return "", err + } + paddingLen, err := quicvarint.Read(bReader) + if err != nil { + return "", err + } + if paddingLen > MaxPaddingLength { + return "", errors.New("invalid padding length") + } + if paddingLen > 0 { + _, err = io.CopyN(io.Discard, r, int64(paddingLen)) + if err != nil { + return "", err + } + } + return string(addrBuf), nil +} + +func WriteTCPRequest(w io.Writer, addr string) error { + padding := tcpRequestPadding.String() + paddingLen := len(padding) + addrLen := len(addr) + sz := int(quicvarint.Len(uint64(addrLen))) + addrLen + + int(quicvarint.Len(uint64(paddingLen))) + paddingLen + buf := make([]byte, sz) + i := varintPut(buf, uint64(addrLen)) + i += copy(buf[i:], addr) + i += varintPut(buf[i:], uint64(paddingLen)) + copy(buf[i:], padding) + _, err := w.Write(buf) + return err +} + +// TCPResponse format: +// Status (byte, 0=ok, 1=error) +// Message length (QUIC varint) +// Message (bytes) +// Padding length (QUIC varint) +// Padding (bytes) + +func ReadTCPResponse(r io.Reader) (bool, string, error) { + var status [1]byte + if _, err := io.ReadFull(r, status[:]); err != nil { + return false, "", err + } + bReader := quicvarint.NewReader(r) + msgLen, err := quicvarint.Read(bReader) + if err != nil { + return false, "", err + } + if msgLen > MaxMessageLength { + return false, "", errors.New("invalid message length") + } + var msgBuf []byte + // No message is fine + if msgLen > 0 { + msgBuf = make([]byte, msgLen) + _, err = io.ReadFull(r, msgBuf) + if err != nil { + return false, "", err + } + } + paddingLen, err := quicvarint.Read(bReader) + if err != nil { + return false, "", err + } + if paddingLen > MaxPaddingLength { + return false, "", errors.New("invalid padding length") + } + if paddingLen > 0 { + _, err = io.CopyN(io.Discard, r, int64(paddingLen)) + if err != nil { + return false, "", err + } + } + return status[0] == 0, string(msgBuf), nil +} + +func WriteTCPResponse(w io.Writer, ok bool, msg string) error { + padding := tcpResponsePadding.String() + paddingLen := len(padding) + msgLen := len(msg) + sz := 1 + int(quicvarint.Len(uint64(msgLen))) + msgLen + + int(quicvarint.Len(uint64(paddingLen))) + paddingLen + buf := make([]byte, sz) + if ok { + buf[0] = 0 + } else { + buf[0] = 1 + } + i := varintPut(buf[1:], uint64(msgLen)) + i += copy(buf[1+i:], msg) + i += varintPut(buf[1+i:], uint64(paddingLen)) + copy(buf[1+i:], padding) + _, err := w.Write(buf) + return err +} + +// UDPMessage format: +// Session ID (uint32 BE) +// Packet ID (uint16 BE) +// Fragment ID (uint8) +// Fragment count (uint8) +// Address length (QUIC varint) +// Address (bytes) +// Data... + +type UDPMessage struct { + SessionID uint32 // 4 + PacketID uint16 // 2 + FragID uint8 // 1 + FragCount uint8 // 1 + Addr string // varint + bytes + Data []byte +} + +func (m *UDPMessage) HeaderSize() int { + lAddr := len(m.Addr) + return 4 + 2 + 1 + 1 + int(quicvarint.Len(uint64(lAddr))) + lAddr +} + +func (m *UDPMessage) Size() int { + return m.HeaderSize() + len(m.Data) +} + +func (m *UDPMessage) Serialize(buf []byte) int { + // Make sure the buffer is big enough + if len(buf) < m.Size() { + return -1 + } + // binary.BigEndian.PutUint32(buf, m.SessionID) + binary.BigEndian.PutUint16(buf[4:], m.PacketID) + buf[6] = m.FragID + buf[7] = m.FragCount + i := varintPut(buf[8:], uint64(len(m.Addr))) + i += copy(buf[8+i:], m.Addr) + i += copy(buf[8+i:], m.Data) + return 8 + i +} + +func ParseUDPMessage(msg []byte) (*UDPMessage, error) { + m := &UDPMessage{} + buf := bytes.NewBuffer(msg) + if err := binary.Read(buf, binary.BigEndian, &m.SessionID); err != nil { + return nil, err + } + if err := binary.Read(buf, binary.BigEndian, &m.PacketID); err != nil { + return nil, err + } + if err := binary.Read(buf, binary.BigEndian, &m.FragID); err != nil { + return nil, err + } + if err := binary.Read(buf, binary.BigEndian, &m.FragCount); err != nil { + return nil, err + } + lAddr, err := quicvarint.Read(buf) + if err != nil { + return nil, err + } + if lAddr == 0 || lAddr > MaxMessageLength { + return nil, errors.New("invalid address length") + } + bs := buf.Bytes() + if len(bs) <= int(lAddr) { + // We use <= instead of < here as we expect at least one byte of data after the address + return nil, errors.New("invalid message length") + } + m.Addr = string(bs[:lAddr]) + m.Data = bs[lAddr:] + return m, nil +} + +// varintPut is like quicvarint.Append, but instead of appending to a slice, +// it writes to a fixed-size buffer. Returns the number of bytes written. +func varintPut(b []byte, i uint64) int { + if i <= maxVarInt1 { + b[0] = uint8(i) + return 1 + } + if i <= maxVarInt2 { + b[0] = uint8(i>>8) | 0x40 + b[1] = uint8(i) + return 2 + } + if i <= maxVarInt4 { + b[0] = uint8(i>>24) | 0x80 + b[1] = uint8(i >> 16) + b[2] = uint8(i >> 8) + b[3] = uint8(i) + return 4 + } + if i <= maxVarInt8 { + b[0] = uint8(i>>56) | 0xc0 + b[1] = uint8(i >> 48) + b[2] = uint8(i >> 40) + b[3] = uint8(i >> 32) + b[4] = uint8(i >> 24) + b[5] = uint8(i >> 16) + b[6] = uint8(i >> 8) + b[7] = uint8(i) + return 8 + } + panic(fmt.Sprintf("%#x doesn't fit into 62 bits", i)) +} diff --git a/subproject/Xray-core-main/proxy/hysteria/server.go b/subproject/Xray-core-main/proxy/hysteria/server.go new file mode 100644 index 00000000..43596b6d --- /dev/null +++ b/subproject/Xray-core-main/proxy/hysteria/server.go @@ -0,0 +1,199 @@ +package hysteria + +import ( + "context" + "io" + "time" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/log" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/features/policy" + "github.com/xtls/xray-core/features/routing" + "github.com/xtls/xray-core/proxy/hysteria/account" + "github.com/xtls/xray-core/transport" + "github.com/xtls/xray-core/transport/internet/hysteria" + "github.com/xtls/xray-core/transport/internet/stat" +) + +type Server struct { + config *ServerConfig + validator *account.Validator + policyManager policy.Manager +} + +func NewServer(ctx context.Context, config *ServerConfig) (*Server, error) { + validator := account.NewValidator() + for _, user := range config.Users { + u, err := user.ToMemoryUser() + if err != nil { + return nil, errors.New("failed to get hysteria user").Base(err).AtError() + } + + if err := validator.Add(u); err != nil { + return nil, errors.New("failed to add user").Base(err).AtError() + } + } + + v := core.MustFromContext(ctx) + s := &Server{ + config: config, + validator: validator, + policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager), + } + + return s, nil +} + +func (s *Server) HysteriaInboundValidator() *account.Validator { + return s.validator +} + +func (s *Server) AddUser(ctx context.Context, u *protocol.MemoryUser) error { + return s.validator.Add(u) +} + +func (s *Server) RemoveUser(ctx context.Context, e string) error { + return s.validator.Del(e) +} + +func (s *Server) GetUser(ctx context.Context, email string) *protocol.MemoryUser { + return s.validator.GetByEmail(email) +} + +func (s *Server) GetUsers(ctx context.Context) []*protocol.MemoryUser { + return s.validator.GetAll() +} + +func (s *Server) GetUsersCount(context.Context) int64 { + return s.validator.GetCount() +} + +func (s *Server) Network() []net.Network { + return []net.Network{net.Network_TCP} +} + +func (s *Server) Process(ctx context.Context, network net.Network, conn stat.Connection, dispatcher routing.Dispatcher) error { + inbound := session.InboundFromContext(ctx) + inbound.Name = "hysteria" + inbound.CanSpliceCopy = 3 + + iConn := stat.TryUnwrapStatsConn(conn) + + var useremail string + var userlevel uint32 + type User interface{ User() *protocol.MemoryUser } + if v, ok := iConn.(User); ok { + inbound.User = v.User() + if inbound.User != nil { + useremail = inbound.User.Email + userlevel = inbound.User.Level + } + } + + if _, ok := iConn.(*hysteria.InterUdpConn); ok { + r := io.Reader(conn) + b := make([]byte, MaxUDPSize) + df := &Defragger{} + var firstMsg *UDPMessage + var firstDest net.Destination + + for { + n, err := r.Read(b) + if err != nil { + return err + } + + msg, err := ParseUDPMessage(b[:n]) + if err != nil { + continue + } + + dfMsg := df.Feed(msg) + if dfMsg == nil { + continue + } + + firstMsg = dfMsg + firstDest, err = net.ParseDestination("udp:" + firstMsg.Addr) + if err != nil { + errors.LogDebug(context.Background(), dfMsg.Addr, " ParseDestination err ", err) + continue + } + + break + } + + reader := &UDPReader{ + Reader: r, + buf: b, + df: df, + firstMsg: firstMsg, + firstDest: &firstDest, + } + + writer := &UDPWriter{ + Writer: conn, + buf: make([]byte, MaxUDPSize), + addr: firstMsg.Addr, + } + + return dispatcher.DispatchLink(ctx, firstDest, &transport.Link{ + Reader: reader, + Writer: writer, + }) + } else { + sessionPolicy := s.policyManager.ForLevel(userlevel) + + common.Must(conn.SetReadDeadline(time.Now().Add(sessionPolicy.Timeouts.Handshake))) + addr, err := ReadTCPRequest(conn) + if err != nil { + log.Record(&log.AccessMessage{ + From: conn.RemoteAddr(), + To: "", + Status: log.AccessRejected, + Reason: err, + }) + return errors.New("failed to create request from: ", conn.RemoteAddr()).Base(err) + } + common.Must(conn.SetReadDeadline(time.Time{})) + + dest, err := net.ParseDestination("tcp:" + addr) + if err != nil { + return err + } + ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{ + From: conn.RemoteAddr(), + To: dest, + Status: log.AccessAccepted, + Reason: "", + Email: useremail, + }) + errors.LogInfo(ctx, "tunnelling request to ", dest) + + bufferedWriter := buf.NewBufferedWriter(buf.NewWriter(conn)) + err = WriteTCPResponse(bufferedWriter, true, "") + if err != nil { + return errors.New("failed to write response").Base(err) + } + if err := bufferedWriter.SetBuffered(false); err != nil { + return err + } + + return dispatcher.DispatchLink(ctx, dest, &transport.Link{ + Reader: buf.NewReader(conn), + Writer: bufferedWriter, + }) + } +} + +func init() { + common.Must(common.RegisterConfig((*ServerConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + return NewServer(ctx, config.(*ServerConfig)) + })) +} diff --git a/subproject/Xray-core-main/proxy/loopback/config.go b/subproject/Xray-core-main/proxy/loopback/config.go new file mode 100644 index 00000000..460b3070 --- /dev/null +++ b/subproject/Xray-core-main/proxy/loopback/config.go @@ -0,0 +1 @@ +package loopback diff --git a/subproject/Xray-core-main/proxy/loopback/config.pb.go b/subproject/Xray-core-main/proxy/loopback/config.pb.go new file mode 100644 index 00000000..ee272884 --- /dev/null +++ b/subproject/Xray-core-main/proxy/loopback/config.pb.go @@ -0,0 +1,124 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: proxy/loopback/config.proto + +package loopback + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + InboundTag string `protobuf:"bytes,1,opt,name=inbound_tag,json=inboundTag,proto3" json:"inbound_tag,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_proxy_loopback_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_proxy_loopback_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_proxy_loopback_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Config) GetInboundTag() string { + if x != nil { + return x.InboundTag + } + return "" +} + +var File_proxy_loopback_config_proto protoreflect.FileDescriptor + +const file_proxy_loopback_config_proto_rawDesc = "" + + "\n" + + "\x1bproxy/loopback/config.proto\x12\x13xray.proxy.loopback\")\n" + + "\x06Config\x12\x1f\n" + + "\vinbound_tag\x18\x01 \x01(\tR\n" + + "inboundTagB[\n" + + "\x17com.xray.proxy.loopbackP\x01Z(github.com/xtls/xray-core/proxy/loopback\xaa\x02\x13Xray.Proxy.Loopbackb\x06proto3" + +var ( + file_proxy_loopback_config_proto_rawDescOnce sync.Once + file_proxy_loopback_config_proto_rawDescData []byte +) + +func file_proxy_loopback_config_proto_rawDescGZIP() []byte { + file_proxy_loopback_config_proto_rawDescOnce.Do(func() { + file_proxy_loopback_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proxy_loopback_config_proto_rawDesc), len(file_proxy_loopback_config_proto_rawDesc))) + }) + return file_proxy_loopback_config_proto_rawDescData +} + +var file_proxy_loopback_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_proxy_loopback_config_proto_goTypes = []any{ + (*Config)(nil), // 0: xray.proxy.loopback.Config +} +var file_proxy_loopback_config_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_proxy_loopback_config_proto_init() } +func file_proxy_loopback_config_proto_init() { + if File_proxy_loopback_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_proxy_loopback_config_proto_rawDesc), len(file_proxy_loopback_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_proxy_loopback_config_proto_goTypes, + DependencyIndexes: file_proxy_loopback_config_proto_depIdxs, + MessageInfos: file_proxy_loopback_config_proto_msgTypes, + }.Build() + File_proxy_loopback_config_proto = out.File + file_proxy_loopback_config_proto_goTypes = nil + file_proxy_loopback_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/proxy/loopback/config.proto b/subproject/Xray-core-main/proxy/loopback/config.proto new file mode 100644 index 00000000..7b8fe901 --- /dev/null +++ b/subproject/Xray-core-main/proxy/loopback/config.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; + +package xray.proxy.loopback; +option csharp_namespace = "Xray.Proxy.Loopback"; +option go_package = "github.com/xtls/xray-core/proxy/loopback"; +option java_package = "com.xray.proxy.loopback"; +option java_multiple_files = true; + +message Config { + string inbound_tag = 1; +} diff --git a/subproject/Xray-core-main/proxy/loopback/loopback.go b/subproject/Xray-core-main/proxy/loopback/loopback.go new file mode 100644 index 00000000..ab3360ae --- /dev/null +++ b/subproject/Xray-core-main/proxy/loopback/loopback.go @@ -0,0 +1,127 @@ +package loopback + +import ( + "context" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/net/cnc" + "github.com/xtls/xray-core/common/retry" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/common/task" + "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/features/routing" + "github.com/xtls/xray-core/transport" + "github.com/xtls/xray-core/transport/internet" +) + +type Loopback struct { + config *Config + dispatcherInstance routing.Dispatcher +} + +func (l *Loopback) Process(ctx context.Context, link *transport.Link, _ internet.Dialer) error { + outbounds := session.OutboundsFromContext(ctx) + ob := outbounds[len(outbounds)-1] + if !ob.Target.IsValid() { + return errors.New("target not specified.") + } + ob.Name = "loopback" + destination := ob.Target + + errors.LogInfo(ctx, "opening connection to ", destination) + + input := link.Reader + output := link.Writer + + var conn net.Conn + err := retry.ExponentialBackoff(2, 100).On(func() error { + dialDest := destination + + content := new(session.Content) + content.SkipDNSResolve = true + + ctx = session.ContextWithContent(ctx, content) + + inbound := session.InboundFromContext(ctx) + if inbound == nil { + inbound = &session.Inbound{} + } + + inbound.Tag = l.config.InboundTag + + ctx = session.ContextWithInbound(ctx, inbound) + + rawConn, err := l.dispatcherInstance.Dispatch(ctx, dialDest) + if err != nil { + return err + } + + var readerOpt cnc.ConnectionOption + if dialDest.Network == net.Network_TCP { + readerOpt = cnc.ConnectionOutputMulti(rawConn.Reader) + } else { + readerOpt = cnc.ConnectionOutputMultiUDP(rawConn.Reader) + } + + conn = cnc.NewConnection(cnc.ConnectionInputMulti(rawConn.Writer), readerOpt) + return nil + }) + if err != nil { + return errors.New("failed to open connection to ", destination).Base(err) + } + defer conn.Close() + + requestDone := func() error { + var writer buf.Writer + if destination.Network == net.Network_TCP { + writer = buf.NewWriter(conn) + } else { + writer = &buf.SequentialWriter{Writer: conn} + } + + if err := buf.Copy(input, writer); err != nil { + return errors.New("failed to process request").Base(err) + } + + return nil + } + + responseDone := func() error { + var reader buf.Reader + if destination.Network == net.Network_TCP { + reader = buf.NewReader(conn) + } else { + reader = buf.NewPacketReader(conn) + } + if err := buf.Copy(reader, output); err != nil { + return errors.New("failed to process response").Base(err) + } + + return nil + } + + if err := task.Run(ctx, requestDone, task.OnSuccess(responseDone, task.Close(output))); err != nil { + return errors.New("connection ends").Base(err) + } + + return nil +} + +func (l *Loopback) init(config *Config, dispatcherInstance routing.Dispatcher) error { + l.dispatcherInstance = dispatcherInstance + l.config = config + return nil +} + +func init() { + common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + l := new(Loopback) + err := core.RequireFeatures(ctx, func(dispatcherInstance routing.Dispatcher) error { + return l.init(config.(*Config), dispatcherInstance) + }) + return l, err + })) +} diff --git a/subproject/Xray-core-main/proxy/proxy.go b/subproject/Xray-core-main/proxy/proxy.go new file mode 100644 index 00000000..dbd81d6b --- /dev/null +++ b/subproject/Xray-core-main/proxy/proxy.go @@ -0,0 +1,809 @@ +// Package proxy contains all proxies used by Xray. +// +// To implement an inbound or outbound proxy, one needs to do the following: +// 1. Implement the interface(s) below. +// 2. Register a config creator through common.RegisterConfig. +package proxy + +import ( + "bytes" + "context" + "crypto/rand" + "io" + "math/big" + "runtime" + "strconv" + "time" + + "github.com/pires/go-proxyproto" + "github.com/xtls/xray-core/app/dispatcher" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/common/signal" + "github.com/xtls/xray-core/features/routing" + "github.com/xtls/xray-core/features/stats" + "github.com/xtls/xray-core/proxy/vless/encryption" + "github.com/xtls/xray-core/transport" + "github.com/xtls/xray-core/transport/internet" + "github.com/xtls/xray-core/transport/internet/finalmask" + "github.com/xtls/xray-core/transport/internet/reality" + "github.com/xtls/xray-core/transport/internet/stat" + "github.com/xtls/xray-core/transport/internet/tls" +) + +var ( + Tls13SupportedVersions = []byte{0x00, 0x2b, 0x00, 0x02, 0x03, 0x04} + TlsClientHandShakeStart = []byte{0x16, 0x03} + TlsServerHandShakeStart = []byte{0x16, 0x03, 0x03} + TlsApplicationDataStart = []byte{0x17, 0x03, 0x03} + + Tls13CipherSuiteDic = map[uint16]string{ + 0x1301: "TLS_AES_128_GCM_SHA256", + 0x1302: "TLS_AES_256_GCM_SHA384", + 0x1303: "TLS_CHACHA20_POLY1305_SHA256", + 0x1304: "TLS_AES_128_CCM_SHA256", + 0x1305: "TLS_AES_128_CCM_8_SHA256", + } +) + +const ( + TlsHandshakeTypeClientHello byte = 0x01 + TlsHandshakeTypeServerHello byte = 0x02 + + CommandPaddingContinue byte = 0x00 + CommandPaddingEnd byte = 0x01 + CommandPaddingDirect byte = 0x02 +) + +// An Inbound processes inbound connections. +type Inbound interface { + // Network returns a list of networks that this inbound supports. Connections with not-supported networks will not be passed into Process(). + Network() []net.Network + + // Process processes a connection of given network. If necessary, the Inbound can dispatch the connection to an Outbound. + Process(context.Context, net.Network, stat.Connection, routing.Dispatcher) error +} + +// An Outbound process outbound connections. +type Outbound interface { + // Process processes the given connection. The given dialer may be used to dial a system outbound connection. + Process(context.Context, *transport.Link, internet.Dialer) error +} + +// UserManager is the interface for Inbounds and Outbounds that can manage their users. +type UserManager interface { + // AddUser adds a new user. + AddUser(context.Context, *protocol.MemoryUser) error + + // RemoveUser removes a user by email. + RemoveUser(context.Context, string) error + + // Get user by email. + GetUser(context.Context, string) *protocol.MemoryUser + + // Get all users. + GetUsers(context.Context) []*protocol.MemoryUser + + // Get users count. + GetUsersCount(context.Context) int64 +} + +type GetInbound interface { + GetInbound() Inbound +} + +type GetOutbound interface { + GetOutbound() Outbound +} + +// TrafficState is used to track uplink and downlink of one connection +// It is used by XTLS to determine if switch to raw copy mode, It is used by Vision to calculate padding +type TrafficState struct { + UserUUID []byte + NumberOfPacketToFilter int + EnableXtls bool + IsTLS12orAbove bool + IsTLS bool + Cipher uint16 + RemainingServerHello int32 + Inbound InboundState + Outbound OutboundState +} + +type InboundState struct { + // reader link state + WithinPaddingBuffers bool + UplinkReaderDirectCopy bool + RemainingCommand int32 + RemainingContent int32 + RemainingPadding int32 + CurrentCommand int + // write link state + IsPadding bool + DownlinkWriterDirectCopy bool +} + +type OutboundState struct { + // reader link state + WithinPaddingBuffers bool + DownlinkReaderDirectCopy bool + RemainingCommand int32 + RemainingContent int32 + RemainingPadding int32 + CurrentCommand int + // write link state + IsPadding bool + UplinkWriterDirectCopy bool +} + +func NewTrafficState(userUUID []byte) *TrafficState { + return &TrafficState{ + UserUUID: userUUID, + NumberOfPacketToFilter: 8, + EnableXtls: false, + IsTLS12orAbove: false, + IsTLS: false, + Cipher: 0, + RemainingServerHello: -1, + Inbound: InboundState{ + WithinPaddingBuffers: true, + UplinkReaderDirectCopy: false, + RemainingCommand: -1, + RemainingContent: -1, + RemainingPadding: -1, + CurrentCommand: 0, + IsPadding: true, + DownlinkWriterDirectCopy: false, + }, + Outbound: OutboundState{ + WithinPaddingBuffers: true, + DownlinkReaderDirectCopy: false, + RemainingCommand: -1, + RemainingContent: -1, + RemainingPadding: -1, + CurrentCommand: 0, + IsPadding: true, + UplinkWriterDirectCopy: false, + }, + } +} + +// VisionReader is used to read xtls vision protocol +// Note Vision probably only make sense as the inner most layer of reader, since it need assess traffic state from origin proxy traffic +type VisionReader struct { + buf.Reader + trafficState *TrafficState + ctx context.Context + isUplink bool + conn net.Conn + input *bytes.Reader + rawInput *bytes.Buffer + ob *session.Outbound + + // internal + directReadCounter stats.Counter +} + +func NewVisionReader(reader buf.Reader, trafficState *TrafficState, isUplink bool, ctx context.Context, conn net.Conn, input *bytes.Reader, rawInput *bytes.Buffer, ob *session.Outbound) *VisionReader { + return &VisionReader{ + Reader: reader, + trafficState: trafficState, + ctx: ctx, + isUplink: isUplink, + conn: conn, + input: input, + rawInput: rawInput, + ob: ob, + } +} + +func (w *VisionReader) ReadMultiBuffer() (buf.MultiBuffer, error) { + buffer, err := w.Reader.ReadMultiBuffer() + if buffer.IsEmpty() { + return buffer, err + } + + var withinPaddingBuffers *bool + var remainingContent *int32 + var remainingPadding *int32 + var currentCommand *int + var switchToDirectCopy *bool + if w.isUplink { + withinPaddingBuffers = &w.trafficState.Inbound.WithinPaddingBuffers + remainingContent = &w.trafficState.Inbound.RemainingContent + remainingPadding = &w.trafficState.Inbound.RemainingPadding + currentCommand = &w.trafficState.Inbound.CurrentCommand + switchToDirectCopy = &w.trafficState.Inbound.UplinkReaderDirectCopy + } else { + withinPaddingBuffers = &w.trafficState.Outbound.WithinPaddingBuffers + remainingContent = &w.trafficState.Outbound.RemainingContent + remainingPadding = &w.trafficState.Outbound.RemainingPadding + currentCommand = &w.trafficState.Outbound.CurrentCommand + switchToDirectCopy = &w.trafficState.Outbound.DownlinkReaderDirectCopy + } + + if *switchToDirectCopy { + if w.directReadCounter != nil { + w.directReadCounter.Add(int64(buffer.Len())) + } + return buffer, err + } + + if *withinPaddingBuffers || w.trafficState.NumberOfPacketToFilter > 0 { + mb2 := make(buf.MultiBuffer, 0, len(buffer)) + for _, b := range buffer { + newbuffer := XtlsUnpadding(b, w.trafficState, w.isUplink, w.ctx) + if newbuffer.Len() > 0 { + mb2 = append(mb2, newbuffer) + } + } + buffer = mb2 + if *remainingContent > 0 || *remainingPadding > 0 || *currentCommand == 0 { + *withinPaddingBuffers = true + } else if *currentCommand == 1 { + *withinPaddingBuffers = false + } else if *currentCommand == 2 { + *withinPaddingBuffers = false + *switchToDirectCopy = true + } else { + errors.LogDebug(w.ctx, "XtlsRead unknown command ", *currentCommand, buffer.Len()) + } + } + if w.trafficState.NumberOfPacketToFilter > 0 { + XtlsFilterTls(buffer, w.trafficState, w.ctx) + } + + if *switchToDirectCopy { + // XTLS Vision processes TLS-like conn's input and rawInput + if inputBuffer, err := buf.ReadFrom(w.input); err == nil && !inputBuffer.IsEmpty() { + buffer, _ = buf.MergeMulti(buffer, inputBuffer) + } + if rawInputBuffer, err := buf.ReadFrom(w.rawInput); err == nil && !rawInputBuffer.IsEmpty() { + buffer, _ = buf.MergeMulti(buffer, rawInputBuffer) + } + *w.input = bytes.Reader{} // release memory + w.input = nil + *w.rawInput = bytes.Buffer{} // release memory + w.rawInput = nil + + if inbound := session.InboundFromContext(w.ctx); inbound != nil && inbound.Conn != nil { + // if w.isUplink && inbound.CanSpliceCopy == 2 { // TODO: enable uplink splice + // inbound.CanSpliceCopy = 1 + // } + if !w.isUplink && w.ob != nil && w.ob.CanSpliceCopy == 2 { // ob need to be passed in due to context can have more than one ob + w.ob.CanSpliceCopy = 1 + } + } + readerConn, readCounter, _ := UnwrapRawConn(w.conn) + w.directReadCounter = readCounter + w.Reader = buf.NewReader(readerConn) + } + return buffer, err +} + +// VisionWriter is used to write xtls vision protocol +// Note Vision probably only make sense as the inner most layer of writer, since it need assess traffic state from origin proxy traffic +type VisionWriter struct { + buf.Writer + trafficState *TrafficState + ctx context.Context + isUplink bool + conn net.Conn + ob *session.Outbound + + // internal + writeOnceUserUUID []byte + directWriteCounter stats.Counter + + testseed []uint32 +} + +func NewVisionWriter(writer buf.Writer, trafficState *TrafficState, isUplink bool, ctx context.Context, conn net.Conn, ob *session.Outbound, testseed []uint32) *VisionWriter { + w := make([]byte, len(trafficState.UserUUID)) + copy(w, trafficState.UserUUID) + if len(testseed) < 4 { + testseed = []uint32{900, 500, 900, 256} + } + return &VisionWriter{ + Writer: writer, + trafficState: trafficState, + ctx: ctx, + writeOnceUserUUID: w, + isUplink: isUplink, + conn: conn, + ob: ob, + testseed: testseed, + } +} + +func (w *VisionWriter) WriteMultiBuffer(mb buf.MultiBuffer) error { + var isPadding *bool + var switchToDirectCopy *bool + var spliceReadyInbound *session.Inbound + if w.isUplink { + isPadding = &w.trafficState.Outbound.IsPadding + switchToDirectCopy = &w.trafficState.Outbound.UplinkWriterDirectCopy + } else { + isPadding = &w.trafficState.Inbound.IsPadding + switchToDirectCopy = &w.trafficState.Inbound.DownlinkWriterDirectCopy + } + + if *switchToDirectCopy { + if inbound := session.InboundFromContext(w.ctx); inbound != nil { + if !w.isUplink && inbound.CanSpliceCopy == 2 { + spliceReadyInbound = inbound + } + // if w.isUplink && w.ob != nil && w.ob.CanSpliceCopy == 2 { // TODO: enable uplink splice + // w.ob.CanSpliceCopy = 1 + // } + } + rawConn, _, writerCounter := UnwrapRawConn(w.conn) + w.Writer = buf.NewWriter(rawConn) + w.directWriteCounter = writerCounter + *switchToDirectCopy = false + } + if !mb.IsEmpty() && w.directWriteCounter != nil { + w.directWriteCounter.Add(int64(mb.Len())) + } + + if w.trafficState.NumberOfPacketToFilter > 0 { + XtlsFilterTls(mb, w.trafficState, w.ctx) + } + + if *isPadding { + if len(mb) == 1 && mb[0] == nil { + mb[0] = XtlsPadding(nil, CommandPaddingContinue, &w.writeOnceUserUUID, true, w.ctx, w.testseed) // we do a long padding to hide vless header + } else { + isComplete := IsCompleteRecord(mb) + mb = ReshapeMultiBuffer(w.ctx, mb) + longPadding := w.trafficState.IsTLS + for i, b := range mb { + if w.trafficState.IsTLS && b.Len() >= 6 && bytes.Equal(TlsApplicationDataStart, b.BytesTo(3)) && isComplete { + if w.trafficState.EnableXtls { + *switchToDirectCopy = true + } + var command byte = CommandPaddingContinue + if i == len(mb)-1 { + command = CommandPaddingEnd + if w.trafficState.EnableXtls { + command = CommandPaddingDirect + } + } + mb[i] = XtlsPadding(b, command, &w.writeOnceUserUUID, true, w.ctx, w.testseed) + *isPadding = false // padding going to end + longPadding = false + continue + } else if !w.trafficState.IsTLS12orAbove && w.trafficState.NumberOfPacketToFilter <= 1 { // For compatibility with earlier vision receiver, we finish padding 1 packet early + *isPadding = false + mb[i] = XtlsPadding(b, CommandPaddingEnd, &w.writeOnceUserUUID, longPadding, w.ctx, w.testseed) + break + } + var command byte = CommandPaddingContinue + if i == len(mb)-1 && !*isPadding { + command = CommandPaddingEnd + if w.trafficState.EnableXtls { + command = CommandPaddingDirect + } + } + mb[i] = XtlsPadding(b, command, &w.writeOnceUserUUID, longPadding, w.ctx, w.testseed) + } + } + } + if err := w.Writer.WriteMultiBuffer(mb); err != nil { + return err + } + if spliceReadyInbound != nil && spliceReadyInbound.CanSpliceCopy == 2 { + // Enable splice only after this write has completed to avoid racing + // concurrent direct writes to the same TCP connection. + spliceReadyInbound.CanSpliceCopy = 1 + } + return nil +} + +// IsCompleteRecord Is complete tls data record +func IsCompleteRecord(buffer buf.MultiBuffer) bool { + b := make([]byte, buffer.Len()) + if buffer.Copy(b) != int(buffer.Len()) { + panic("impossible bytes allocation") + } + var headerLen int = 5 + var recordLen int + + totalLen := len(b) + i := 0 + for i < totalLen { + // record header: 0x17 0x3 0x3 + 2 bytes length + if headerLen > 0 { + data := b[i] + i++ + switch headerLen { + case 5: + if data != 0x17 { + return false + } + case 4: + if data != 0x03 { + return false + } + case 3: + if data != 0x03 { + return false + } + case 2: + recordLen = int(data) << 8 + case 1: + recordLen = recordLen | int(data) + } + headerLen-- + } else if recordLen > 0 { + remaining := totalLen - i + if remaining < recordLen { + return false + } else { + i += recordLen + recordLen = 0 + headerLen = 5 + } + } else { + return false + } + } + if headerLen == 5 && recordLen == 0 { + return true + } + return false +} + +// ReshapeMultiBuffer prepare multi buffer for padding structure (max 21 bytes) +func ReshapeMultiBuffer(ctx context.Context, buffer buf.MultiBuffer) buf.MultiBuffer { + needReshape := 0 + for _, b := range buffer { + if b.Len() >= buf.Size-21 { + needReshape += 1 + } + } + if needReshape == 0 { + return buffer + } + mb2 := make(buf.MultiBuffer, 0, len(buffer)+needReshape) + toPrint := "" + for i, buffer1 := range buffer { + if buffer1.Len() >= buf.Size-21 { + index := int32(bytes.LastIndex(buffer1.Bytes(), TlsApplicationDataStart)) + if index < 21 || index > buf.Size-21 { + index = buf.Size / 2 + } + buffer2 := buf.New() + buffer2.Write(buffer1.BytesFrom(index)) + buffer1.Resize(0, index) + mb2 = append(mb2, buffer1, buffer2) + toPrint += " " + strconv.Itoa(int(buffer1.Len())) + " " + strconv.Itoa(int(buffer2.Len())) + } else { + mb2 = append(mb2, buffer1) + toPrint += " " + strconv.Itoa(int(buffer1.Len())) + } + buffer[i] = nil + } + buffer = buffer[:0] + errors.LogDebug(ctx, "ReshapeMultiBuffer ", toPrint) + return mb2 +} + +// XtlsPadding add padding to eliminate length signature during tls handshake +func XtlsPadding(b *buf.Buffer, command byte, userUUID *[]byte, longPadding bool, ctx context.Context, testseed []uint32) *buf.Buffer { + var contentLen int32 = 0 + var paddingLen int32 = 0 + if b != nil { + contentLen = b.Len() + } + if contentLen < int32(testseed[0]) && longPadding { + l, err := rand.Int(rand.Reader, big.NewInt(int64(testseed[1]))) + if err != nil { + errors.LogDebugInner(ctx, err, "failed to generate padding") + } + paddingLen = int32(l.Int64()) + int32(testseed[2]) - contentLen + } else { + l, err := rand.Int(rand.Reader, big.NewInt(int64(testseed[3]))) + if err != nil { + errors.LogDebugInner(ctx, err, "failed to generate padding") + } + paddingLen = int32(l.Int64()) + } + if paddingLen > buf.Size-21-contentLen { + paddingLen = buf.Size - 21 - contentLen + } + newbuffer := buf.New() + if userUUID != nil { + newbuffer.Write(*userUUID) + *userUUID = nil + } + newbuffer.Write([]byte{command, byte(contentLen >> 8), byte(contentLen), byte(paddingLen >> 8), byte(paddingLen)}) + if b != nil { + newbuffer.Write(b.Bytes()) + b.Release() + b = nil + } + newbuffer.Extend(paddingLen) + errors.LogDebug(ctx, "XtlsPadding ", contentLen, " ", paddingLen, " ", command) + return newbuffer +} + +// XtlsUnpadding remove padding and parse command +func XtlsUnpadding(b *buf.Buffer, s *TrafficState, isUplink bool, ctx context.Context) *buf.Buffer { + var remainingCommand *int32 + var remainingContent *int32 + var remainingPadding *int32 + var currentCommand *int + if isUplink { + remainingCommand = &s.Inbound.RemainingCommand + remainingContent = &s.Inbound.RemainingContent + remainingPadding = &s.Inbound.RemainingPadding + currentCommand = &s.Inbound.CurrentCommand + } else { + remainingCommand = &s.Outbound.RemainingCommand + remainingContent = &s.Outbound.RemainingContent + remainingPadding = &s.Outbound.RemainingPadding + currentCommand = &s.Outbound.CurrentCommand + } + if *remainingCommand == -1 && *remainingContent == -1 && *remainingPadding == -1 { // initial state + if b.Len() >= 21 && bytes.Equal(s.UserUUID, b.BytesTo(16)) { + b.Advance(16) + *remainingCommand = 5 + } else { + return b + } + } + newbuffer := buf.New() + for b.Len() > 0 { + if *remainingCommand > 0 { + data, err := b.ReadByte() + if err != nil { + return newbuffer + } + switch *remainingCommand { + case 5: + *currentCommand = int(data) + case 4: + *remainingContent = int32(data) << 8 + case 3: + *remainingContent = *remainingContent | int32(data) + case 2: + *remainingPadding = int32(data) << 8 + case 1: + *remainingPadding = *remainingPadding | int32(data) + errors.LogDebug(ctx, "Xtls Unpadding new block, content ", *remainingContent, " padding ", *remainingPadding, " command ", *currentCommand) + } + *remainingCommand-- + } else if *remainingContent > 0 { + len := *remainingContent + if b.Len() < len { + len = b.Len() + } + data, err := b.ReadBytes(len) + if err != nil { + return newbuffer + } + newbuffer.Write(data) + *remainingContent -= len + } else { // remainingPadding > 0 + len := *remainingPadding + if b.Len() < len { + len = b.Len() + } + b.Advance(len) + *remainingPadding -= len + } + if *remainingCommand <= 0 && *remainingContent <= 0 && *remainingPadding <= 0 { // this block done + if *currentCommand == 0 { + *remainingCommand = 5 + } else { + *remainingCommand = -1 // set to initial state + *remainingContent = -1 + *remainingPadding = -1 + if b.Len() > 0 { // shouldn't happen + newbuffer.Write(b.Bytes()) + } + break + } + } + } + b.Release() + b = nil + return newbuffer +} + +// XtlsFilterTls filter and recognize tls 1.3 and other info +func XtlsFilterTls(buffer buf.MultiBuffer, trafficState *TrafficState, ctx context.Context) { + for _, b := range buffer { + if b == nil { + continue + } + trafficState.NumberOfPacketToFilter-- + if b.Len() >= 6 { + startsBytes := b.BytesTo(6) + if bytes.Equal(TlsServerHandShakeStart, startsBytes[:3]) && startsBytes[5] == TlsHandshakeTypeServerHello { + trafficState.RemainingServerHello = (int32(startsBytes[3])<<8 | int32(startsBytes[4])) + 5 + trafficState.IsTLS12orAbove = true + trafficState.IsTLS = true + if b.Len() >= 79 && trafficState.RemainingServerHello >= 79 { + sessionIdLen := int32(b.Byte(43)) + cipherSuite := b.BytesRange(43+sessionIdLen+1, 43+sessionIdLen+3) + trafficState.Cipher = uint16(cipherSuite[0])<<8 | uint16(cipherSuite[1]) + } else { + errors.LogDebug(ctx, "XtlsFilterTls short server hello, tls 1.2 or older? ", b.Len(), " ", trafficState.RemainingServerHello) + } + } else if bytes.Equal(TlsClientHandShakeStart, startsBytes[:2]) && startsBytes[5] == TlsHandshakeTypeClientHello { + trafficState.IsTLS = true + errors.LogDebug(ctx, "XtlsFilterTls found tls client hello! ", buffer.Len()) + } + } + if trafficState.RemainingServerHello > 0 { + end := trafficState.RemainingServerHello + if end > b.Len() { + end = b.Len() + } + trafficState.RemainingServerHello -= b.Len() + if bytes.Contains(b.BytesTo(end), Tls13SupportedVersions) { + v, ok := Tls13CipherSuiteDic[trafficState.Cipher] + if !ok { + v = "Old cipher: " + strconv.FormatUint(uint64(trafficState.Cipher), 16) + } else if v != "TLS_AES_128_CCM_8_SHA256" { + trafficState.EnableXtls = true + } + errors.LogDebug(ctx, "XtlsFilterTls found tls 1.3! ", b.Len(), " ", v) + trafficState.NumberOfPacketToFilter = 0 + return + } else if trafficState.RemainingServerHello <= 0 { + errors.LogDebug(ctx, "XtlsFilterTls found tls 1.2! ", b.Len()) + trafficState.NumberOfPacketToFilter = 0 + return + } + errors.LogDebug(ctx, "XtlsFilterTls inconclusive server hello ", b.Len(), " ", trafficState.RemainingServerHello) + } + if trafficState.NumberOfPacketToFilter <= 0 { + errors.LogDebug(ctx, "XtlsFilterTls stop filtering", buffer.Len()) + } + } +} + +// UnwrapRawConn support unwrap encryption, stats, mask wrappers, tls, utls, reality, proxyproto, uds-wrapper conn and get raw tcp/uds conn from it +func UnwrapRawConn(conn net.Conn) (net.Conn, stats.Counter, stats.Counter) { + var readCounter, writerCounter stats.Counter + if conn != nil { + isEncryption := false + if commonConn, ok := conn.(*encryption.CommonConn); ok { + conn = commonConn.Conn + isEncryption = true + } + if xorConn, ok := conn.(*encryption.XorConn); ok { + return xorConn, nil, nil // full-random xorConn should not be penetrated + } + if statConn, ok := conn.(*stat.CounterConnection); ok { + conn = statConn.Connection + readCounter = statConn.ReadCounter + writerCounter = statConn.WriteCounter + } + + if !isEncryption { // avoids double penetration + if xc, ok := conn.(*tls.Conn); ok { + conn = xc.NetConn() + } else if utlsConn, ok := conn.(*tls.UConn); ok { + conn = utlsConn.NetConn() + } else if realityConn, ok := conn.(*reality.Conn); ok { + conn = realityConn.NetConn() + } else if realityUConn, ok := conn.(*reality.UConn); ok { + conn = realityUConn.NetConn() + } + } + + conn = finalmask.UnwrapTcpMask(conn) + + if pc, ok := conn.(*proxyproto.Conn); ok { + conn = pc.Raw() + // 8192 > 4096, there is no need to process pc's bufReader + } + if uc, ok := conn.(*internet.UnixConnWrapper); ok { + conn = uc.UnixConn + } + } + return conn, readCounter, writerCounter +} + +// CopyRawConnIfExist use the most efficient copy method. +// - If caller don't want to turn on splice, do not pass in both reader conn and writer conn +// - writer are from *transport.Link +func CopyRawConnIfExist(ctx context.Context, readerConn net.Conn, writerConn net.Conn, writer buf.Writer, timer *signal.ActivityTimer, inTimer *signal.ActivityTimer) error { + readerConn, readCounter, _ := UnwrapRawConn(readerConn) + writerConn, _, writeCounter := UnwrapRawConn(writerConn) + reader := buf.NewReader(readerConn) + if runtime.GOOS != "linux" && runtime.GOOS != "android" { + return readV(ctx, reader, writer, timer, readCounter) + } + tc, ok := writerConn.(*net.TCPConn) + if !ok || readerConn == nil || writerConn == nil { + return readV(ctx, reader, writer, timer, readCounter) + } + inbound := session.InboundFromContext(ctx) + if inbound == nil || inbound.CanSpliceCopy == 3 { + return readV(ctx, reader, writer, timer, readCounter) + } + outbounds := session.OutboundsFromContext(ctx) + if len(outbounds) == 0 { + return readV(ctx, reader, writer, timer, readCounter) + } + for _, ob := range outbounds { + if ob.CanSpliceCopy == 3 { + return readV(ctx, reader, writer, timer, readCounter) + } + } + + for { + inbound := session.InboundFromContext(ctx) + outbounds := session.OutboundsFromContext(ctx) + var splice = inbound.CanSpliceCopy == 1 + for _, ob := range outbounds { + if ob.CanSpliceCopy != 1 { + splice = false + } + } + if splice { + errors.LogDebug(ctx, "CopyRawConn splice") + statWriter, _ := writer.(*dispatcher.SizeStatWriter) + //runtime.Gosched() // necessary + timer.SetTimeout(24 * time.Hour) // prevent leak, just in case + if inTimer != nil { + inTimer.SetTimeout(24 * time.Hour) + } + w, err := tc.ReadFrom(readerConn) + if readCounter != nil { + readCounter.Add(w) // outbound stats + } + if writeCounter != nil { + writeCounter.Add(w) // inbound stats + } + if statWriter != nil { + statWriter.Counter.Add(w) // user stats + } + if err != nil && errors.Cause(err) != io.EOF { + return err + } + return nil + } + buffer, err := reader.ReadMultiBuffer() + if !buffer.IsEmpty() { + if readCounter != nil { + readCounter.Add(int64(buffer.Len())) + } + timer.Update() + if werr := writer.WriteMultiBuffer(buffer); werr != nil { + return werr + } + } + if err != nil { + if errors.Cause(err) == io.EOF { + return nil + } + return err + } + } +} + +func readV(ctx context.Context, reader buf.Reader, writer buf.Writer, timer signal.ActivityUpdater, readCounter stats.Counter) error { + errors.LogDebug(ctx, "CopyRawConn (maybe) readv") + if err := buf.Copy(reader, writer, buf.UpdateActivity(timer), buf.AddToStatCounter(readCounter)); err != nil { + return errors.New("failed to process response").Base(err) + } + return nil +} + +func IsRAWTransportWithoutSecurity(conn stat.Connection) bool { + iConn := stat.TryUnwrapStatsConn(conn) + iConn = finalmask.UnwrapTcpMask(iConn) + _, ok1 := iConn.(*proxyproto.Conn) + _, ok2 := iConn.(*net.TCPConn) + _, ok3 := iConn.(*internet.UnixConnWrapper) + return ok1 || ok2 || ok3 +} diff --git a/subproject/Xray-core-main/proxy/shadowsocks/client.go b/subproject/Xray-core-main/proxy/shadowsocks/client.go new file mode 100644 index 00000000..29cb0456 --- /dev/null +++ b/subproject/Xray-core-main/proxy/shadowsocks/client.go @@ -0,0 +1,201 @@ +package shadowsocks + +import ( + "context" + "time" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/retry" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/common/signal" + "github.com/xtls/xray-core/common/task" + "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/features/policy" + "github.com/xtls/xray-core/transport" + "github.com/xtls/xray-core/transport/internet" + "github.com/xtls/xray-core/transport/internet/stat" +) + +// Client is a inbound handler for Shadowsocks protocol +type Client struct { + server *protocol.ServerSpec + policyManager policy.Manager +} + +// NewClient create a new Shadowsocks client. +func NewClient(ctx context.Context, config *ClientConfig) (*Client, error) { + if config.Server == nil { + return nil, errors.New(`no target server found`) + } + server, err := protocol.NewServerSpecFromPB(config.Server) + if err != nil { + return nil, errors.New("failed to get server spec").Base(err) + } + + v := core.MustFromContext(ctx) + client := &Client{ + server: server, + policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager), + } + return client, nil +} + +// Process implements OutboundHandler.Process(). +func (c *Client) Process(ctx context.Context, link *transport.Link, dialer internet.Dialer) error { + outbounds := session.OutboundsFromContext(ctx) + ob := outbounds[len(outbounds)-1] + if !ob.Target.IsValid() { + return errors.New("target not specified") + } + ob.Name = "shadowsocks" + ob.CanSpliceCopy = 3 + destination := ob.Target + network := destination.Network + + server := c.server + dest := server.Destination + dest.Network = network + var conn stat.Connection + + err := retry.ExponentialBackoff(5, 100).On(func() error { + rawConn, err := dialer.Dial(ctx, dest) + if err != nil { + return err + } + conn = rawConn + + return nil + }) + if err != nil { + return errors.New("failed to find an available destination").AtWarning().Base(err) + } + errors.LogInfo(ctx, "tunneling request to ", destination, " via ", network, ":", server.Destination.NetAddr()) + + defer conn.Close() + + request := &protocol.RequestHeader{ + Version: Version, + Address: destination.Address, + Port: destination.Port, + } + if destination.Network == net.Network_TCP { + request.Command = protocol.RequestCommandTCP + } else { + request.Command = protocol.RequestCommandUDP + } + + user := server.User + _, ok := user.Account.(*MemoryAccount) + if !ok { + return errors.New("user account is not valid") + } + request.User = user + + var newCtx context.Context + var newCancel context.CancelFunc + if session.TimeoutOnlyFromContext(ctx) { + newCtx, newCancel = context.WithCancel(context.Background()) + } + + sessionPolicy := c.policyManager.ForLevel(user.Level) + ctx, cancel := context.WithCancel(ctx) + timer := signal.CancelAfterInactivity(ctx, func() { + cancel() + if newCancel != nil { + newCancel() + } + }, sessionPolicy.Timeouts.ConnectionIdle) + + if newCtx != nil { + ctx = newCtx + } + + if request.Command == protocol.RequestCommandTCP { + requestDone := func() error { + defer timer.SetTimeout(sessionPolicy.Timeouts.DownlinkOnly) + bufferedWriter := buf.NewBufferedWriter(buf.NewWriter(conn)) + bodyWriter, err := WriteTCPRequest(request, bufferedWriter) + if err != nil { + return errors.New("failed to write request").Base(err) + } + + if err = buf.CopyOnceTimeout(link.Reader, bodyWriter, time.Millisecond*100); err != nil && err != buf.ErrNotTimeoutReader && err != buf.ErrReadTimeout { + return errors.New("failed to write A request payload").Base(err).AtWarning() + } + + if err := bufferedWriter.SetBuffered(false); err != nil { + return err + } + + return buf.Copy(link.Reader, bodyWriter, buf.UpdateActivity(timer)) + } + + responseDone := func() error { + defer timer.SetTimeout(sessionPolicy.Timeouts.UplinkOnly) + + responseReader, err := ReadTCPResponse(user, conn) + if err != nil { + return err + } + + return buf.Copy(responseReader, link.Writer, buf.UpdateActivity(timer)) + } + + responseDoneAndCloseWriter := task.OnSuccess(responseDone, task.Close(link.Writer)) + if err := task.Run(ctx, requestDone, responseDoneAndCloseWriter); err != nil { + return errors.New("connection ends").Base(err) + } + + return nil + } + + if request.Command == protocol.RequestCommandUDP { + + requestDone := func() error { + defer timer.SetTimeout(sessionPolicy.Timeouts.DownlinkOnly) + + writer := &UDPWriter{ + Writer: conn, + Request: request, + } + + if err := buf.Copy(link.Reader, writer, buf.UpdateActivity(timer)); err != nil { + return errors.New("failed to transport all UDP request").Base(err) + } + return nil + } + + responseDone := func() error { + defer timer.SetTimeout(sessionPolicy.Timeouts.UplinkOnly) + + reader := &UDPReader{ + Reader: conn, + User: user, + } + + if err := buf.Copy(reader, link.Writer, buf.UpdateActivity(timer)); err != nil { + return errors.New("failed to transport all UDP response").Base(err) + } + return nil + } + + responseDoneAndCloseWriter := task.OnSuccess(responseDone, task.Close(link.Writer)) + if err := task.Run(ctx, requestDone, responseDoneAndCloseWriter); err != nil { + return errors.New("connection ends").Base(err) + } + + return nil + } + + return nil +} + +func init() { + common.Must(common.RegisterConfig((*ClientConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + return NewClient(ctx, config.(*ClientConfig)) + })) +} diff --git a/subproject/Xray-core-main/proxy/shadowsocks/config.go b/subproject/Xray-core-main/proxy/shadowsocks/config.go new file mode 100644 index 00000000..dc8bff0b --- /dev/null +++ b/subproject/Xray-core-main/proxy/shadowsocks/config.go @@ -0,0 +1,233 @@ +package shadowsocks + +import ( + "bytes" + "crypto/cipher" + "crypto/md5" + "crypto/sha1" + "io" + + "google.golang.org/protobuf/proto" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/crypto" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/protocol" + "golang.org/x/crypto/chacha20poly1305" + "golang.org/x/crypto/hkdf" +) + +// MemoryAccount is an account type converted from Account. +type MemoryAccount struct { + Cipher Cipher + CipherType CipherType + Key []byte + Password string +} + +var ErrIVNotUnique = errors.New("IV is not unique") + +// Equals implements protocol.Account.Equals(). +func (a *MemoryAccount) Equals(another protocol.Account) bool { + if account, ok := another.(*MemoryAccount); ok { + return bytes.Equal(a.Key, account.Key) + } + return false +} + +func (a *MemoryAccount) ToProto() proto.Message { + return &Account{ + CipherType: a.CipherType, + Password: a.Password, + } +} + +func createAesGcm(key []byte) cipher.AEAD { + return crypto.NewAesGcm(key) +} + +func createChaCha20Poly1305(key []byte) cipher.AEAD { + ChaChaPoly1305, err := chacha20poly1305.New(key) + common.Must(err) + return ChaChaPoly1305 +} + +func createXChaCha20Poly1305(key []byte) cipher.AEAD { + XChaChaPoly1305, err := chacha20poly1305.NewX(key) + common.Must(err) + return XChaChaPoly1305 +} + +func (a *Account) getCipher() (Cipher, error) { + switch a.CipherType { + case CipherType_AES_128_GCM: + return &AEADCipher{ + KeyBytes: 16, + IVBytes: 16, + AEADAuthCreator: createAesGcm, + }, nil + case CipherType_AES_256_GCM: + return &AEADCipher{ + KeyBytes: 32, + IVBytes: 32, + AEADAuthCreator: createAesGcm, + }, nil + case CipherType_CHACHA20_POLY1305: + return &AEADCipher{ + KeyBytes: 32, + IVBytes: 32, + AEADAuthCreator: createChaCha20Poly1305, + }, nil + case CipherType_XCHACHA20_POLY1305: + return &AEADCipher{ + KeyBytes: 32, + IVBytes: 32, + AEADAuthCreator: createXChaCha20Poly1305, + }, nil + case CipherType_NONE: + return NoneCipher{}, nil + default: + return nil, errors.New("Unsupported cipher.") + } +} + +// AsAccount implements protocol.AsAccount. +func (a *Account) AsAccount() (protocol.Account, error) { + Cipher, err := a.getCipher() + if err != nil { + return nil, errors.New("failed to get cipher").Base(err) + } + return &MemoryAccount{ + Cipher: Cipher, + CipherType: a.CipherType, + Key: passwordToCipherKey([]byte(a.Password), Cipher.KeySize()), + Password: a.Password, + }, nil +} + +// Cipher is an interface for all Shadowsocks ciphers. +type Cipher interface { + KeySize() int32 + IVSize() int32 + NewEncryptionWriter(key []byte, iv []byte, writer io.Writer) (buf.Writer, error) + NewDecryptionReader(key []byte, iv []byte, reader io.Reader) (buf.Reader, error) + IsAEAD() bool + EncodePacket(key []byte, b *buf.Buffer) error + DecodePacket(key []byte, b *buf.Buffer) error +} + +type AEADCipher struct { + KeyBytes int32 + IVBytes int32 + AEADAuthCreator func(key []byte) cipher.AEAD +} + +func (*AEADCipher) IsAEAD() bool { + return true +} + +func (c *AEADCipher) KeySize() int32 { + return c.KeyBytes +} + +func (c *AEADCipher) IVSize() int32 { + return c.IVBytes +} + +func (c *AEADCipher) createAuthenticator(key []byte, iv []byte) *crypto.AEADAuthenticator { + subkey := make([]byte, c.KeyBytes) + hkdfSHA1(key, iv, subkey) + aead := c.AEADAuthCreator(subkey) + nonce := crypto.GenerateAEADNonceWithSize(aead.NonceSize()) + return &crypto.AEADAuthenticator{ + AEAD: aead, + NonceGenerator: nonce, + } +} + +func (c *AEADCipher) NewEncryptionWriter(key []byte, iv []byte, writer io.Writer) (buf.Writer, error) { + auth := c.createAuthenticator(key, iv) + return crypto.NewAuthenticationWriter(auth, &crypto.AEADChunkSizeParser{ + Auth: auth, + }, writer, protocol.TransferTypeStream, nil), nil +} + +func (c *AEADCipher) NewDecryptionReader(key []byte, iv []byte, reader io.Reader) (buf.Reader, error) { + auth := c.createAuthenticator(key, iv) + return crypto.NewAuthenticationReader(auth, &crypto.AEADChunkSizeParser{ + Auth: auth, + }, reader, protocol.TransferTypeStream, nil), nil +} + +func (c *AEADCipher) EncodePacket(key []byte, b *buf.Buffer) error { + ivLen := c.IVSize() + payloadLen := b.Len() + auth := c.createAuthenticator(key, b.BytesTo(ivLen)) + + b.Extend(int32(auth.Overhead())) + _, err := auth.Seal(b.BytesTo(ivLen), b.BytesRange(ivLen, payloadLen)) + return err +} + +func (c *AEADCipher) DecodePacket(key []byte, b *buf.Buffer) error { + if b.Len() <= c.IVSize() { + return errors.New("insufficient data: ", b.Len()) + } + ivLen := c.IVSize() + payloadLen := b.Len() + auth := c.createAuthenticator(key, b.BytesTo(ivLen)) + + bbb, err := auth.Open(b.BytesTo(ivLen), b.BytesRange(ivLen, payloadLen)) + if err != nil { + return err + } + b.Resize(ivLen, int32(len(bbb))) + return nil +} + +type NoneCipher struct{} + +func (NoneCipher) KeySize() int32 { return 0 } +func (NoneCipher) IVSize() int32 { return 0 } +func (NoneCipher) IsAEAD() bool { + return false +} + +func (NoneCipher) NewDecryptionReader(key []byte, iv []byte, reader io.Reader) (buf.Reader, error) { + return buf.NewReader(reader), nil +} + +func (NoneCipher) NewEncryptionWriter(key []byte, iv []byte, writer io.Writer) (buf.Writer, error) { + return buf.NewWriter(writer), nil +} + +func (NoneCipher) EncodePacket(key []byte, b *buf.Buffer) error { + return nil +} + +func (NoneCipher) DecodePacket(key []byte, b *buf.Buffer) error { + return nil +} + +func passwordToCipherKey(password []byte, keySize int32) []byte { + key := make([]byte, 0, keySize) + + md5Sum := md5.Sum(password) + key = append(key, md5Sum[:]...) + + for int32(len(key)) < keySize { + md5Hash := md5.New() + common.Must2(md5Hash.Write(md5Sum[:])) + common.Must2(md5Hash.Write(password)) + md5Hash.Sum(md5Sum[:0]) + + key = append(key, md5Sum[:]...) + } + return key +} + +func hkdfSHA1(secret, salt, outKey []byte) { + r := hkdf.New(sha1.New, secret, salt, []byte("ss-subkey")) + common.Must2(io.ReadFull(r, outKey)) +} diff --git a/subproject/Xray-core-main/proxy/shadowsocks/config.pb.go b/subproject/Xray-core-main/proxy/shadowsocks/config.pb.go new file mode 100644 index 00000000..ca450a0e --- /dev/null +++ b/subproject/Xray-core-main/proxy/shadowsocks/config.pb.go @@ -0,0 +1,323 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: proxy/shadowsocks/config.proto + +package shadowsocks + +import ( + net "github.com/xtls/xray-core/common/net" + protocol "github.com/xtls/xray-core/common/protocol" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type CipherType int32 + +const ( + CipherType_UNKNOWN CipherType = 0 + CipherType_AES_128_GCM CipherType = 5 + CipherType_AES_256_GCM CipherType = 6 + CipherType_CHACHA20_POLY1305 CipherType = 7 + CipherType_XCHACHA20_POLY1305 CipherType = 8 + CipherType_NONE CipherType = 9 +) + +// Enum value maps for CipherType. +var ( + CipherType_name = map[int32]string{ + 0: "UNKNOWN", + 5: "AES_128_GCM", + 6: "AES_256_GCM", + 7: "CHACHA20_POLY1305", + 8: "XCHACHA20_POLY1305", + 9: "NONE", + } + CipherType_value = map[string]int32{ + "UNKNOWN": 0, + "AES_128_GCM": 5, + "AES_256_GCM": 6, + "CHACHA20_POLY1305": 7, + "XCHACHA20_POLY1305": 8, + "NONE": 9, + } +) + +func (x CipherType) Enum() *CipherType { + p := new(CipherType) + *p = x + return p +} + +func (x CipherType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (CipherType) Descriptor() protoreflect.EnumDescriptor { + return file_proxy_shadowsocks_config_proto_enumTypes[0].Descriptor() +} + +func (CipherType) Type() protoreflect.EnumType { + return &file_proxy_shadowsocks_config_proto_enumTypes[0] +} + +func (x CipherType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use CipherType.Descriptor instead. +func (CipherType) EnumDescriptor() ([]byte, []int) { + return file_proxy_shadowsocks_config_proto_rawDescGZIP(), []int{0} +} + +type Account struct { + state protoimpl.MessageState `protogen:"open.v1"` + Password string `protobuf:"bytes,1,opt,name=password,proto3" json:"password,omitempty"` + CipherType CipherType `protobuf:"varint,2,opt,name=cipher_type,json=cipherType,proto3,enum=xray.proxy.shadowsocks.CipherType" json:"cipher_type,omitempty"` + IvCheck bool `protobuf:"varint,3,opt,name=iv_check,json=ivCheck,proto3" json:"iv_check,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Account) Reset() { + *x = Account{} + mi := &file_proxy_shadowsocks_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Account) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Account) ProtoMessage() {} + +func (x *Account) ProtoReflect() protoreflect.Message { + mi := &file_proxy_shadowsocks_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Account.ProtoReflect.Descriptor instead. +func (*Account) Descriptor() ([]byte, []int) { + return file_proxy_shadowsocks_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Account) GetPassword() string { + if x != nil { + return x.Password + } + return "" +} + +func (x *Account) GetCipherType() CipherType { + if x != nil { + return x.CipherType + } + return CipherType_UNKNOWN +} + +func (x *Account) GetIvCheck() bool { + if x != nil { + return x.IvCheck + } + return false +} + +type ServerConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + Users []*protocol.User `protobuf:"bytes,1,rep,name=users,proto3" json:"users,omitempty"` + Network []net.Network `protobuf:"varint,2,rep,packed,name=network,proto3,enum=xray.common.net.Network" json:"network,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ServerConfig) Reset() { + *x = ServerConfig{} + mi := &file_proxy_shadowsocks_config_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ServerConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ServerConfig) ProtoMessage() {} + +func (x *ServerConfig) ProtoReflect() protoreflect.Message { + mi := &file_proxy_shadowsocks_config_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ServerConfig.ProtoReflect.Descriptor instead. +func (*ServerConfig) Descriptor() ([]byte, []int) { + return file_proxy_shadowsocks_config_proto_rawDescGZIP(), []int{1} +} + +func (x *ServerConfig) GetUsers() []*protocol.User { + if x != nil { + return x.Users + } + return nil +} + +func (x *ServerConfig) GetNetwork() []net.Network { + if x != nil { + return x.Network + } + return nil +} + +type ClientConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + Server *protocol.ServerEndpoint `protobuf:"bytes,1,opt,name=server,proto3" json:"server,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ClientConfig) Reset() { + *x = ClientConfig{} + mi := &file_proxy_shadowsocks_config_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ClientConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClientConfig) ProtoMessage() {} + +func (x *ClientConfig) ProtoReflect() protoreflect.Message { + mi := &file_proxy_shadowsocks_config_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ClientConfig.ProtoReflect.Descriptor instead. +func (*ClientConfig) Descriptor() ([]byte, []int) { + return file_proxy_shadowsocks_config_proto_rawDescGZIP(), []int{2} +} + +func (x *ClientConfig) GetServer() *protocol.ServerEndpoint { + if x != nil { + return x.Server + } + return nil +} + +var File_proxy_shadowsocks_config_proto protoreflect.FileDescriptor + +const file_proxy_shadowsocks_config_proto_rawDesc = "" + + "\n" + + "\x1eproxy/shadowsocks/config.proto\x12\x16xray.proxy.shadowsocks\x1a\x18common/net/network.proto\x1a\x1acommon/protocol/user.proto\x1a!common/protocol/server_spec.proto\"\x85\x01\n" + + "\aAccount\x12\x1a\n" + + "\bpassword\x18\x01 \x01(\tR\bpassword\x12C\n" + + "\vcipher_type\x18\x02 \x01(\x0e2\".xray.proxy.shadowsocks.CipherTypeR\n" + + "cipherType\x12\x19\n" + + "\biv_check\x18\x03 \x01(\bR\aivCheck\"t\n" + + "\fServerConfig\x120\n" + + "\x05users\x18\x01 \x03(\v2\x1a.xray.common.protocol.UserR\x05users\x122\n" + + "\anetwork\x18\x02 \x03(\x0e2\x18.xray.common.net.NetworkR\anetwork\"L\n" + + "\fClientConfig\x12<\n" + + "\x06server\x18\x01 \x01(\v2$.xray.common.protocol.ServerEndpointR\x06server*t\n" + + "\n" + + "CipherType\x12\v\n" + + "\aUNKNOWN\x10\x00\x12\x0f\n" + + "\vAES_128_GCM\x10\x05\x12\x0f\n" + + "\vAES_256_GCM\x10\x06\x12\x15\n" + + "\x11CHACHA20_POLY1305\x10\a\x12\x16\n" + + "\x12XCHACHA20_POLY1305\x10\b\x12\b\n" + + "\x04NONE\x10\tBd\n" + + "\x1acom.xray.proxy.shadowsocksP\x01Z+github.com/xtls/xray-core/proxy/shadowsocks\xaa\x02\x16Xray.Proxy.Shadowsocksb\x06proto3" + +var ( + file_proxy_shadowsocks_config_proto_rawDescOnce sync.Once + file_proxy_shadowsocks_config_proto_rawDescData []byte +) + +func file_proxy_shadowsocks_config_proto_rawDescGZIP() []byte { + file_proxy_shadowsocks_config_proto_rawDescOnce.Do(func() { + file_proxy_shadowsocks_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proxy_shadowsocks_config_proto_rawDesc), len(file_proxy_shadowsocks_config_proto_rawDesc))) + }) + return file_proxy_shadowsocks_config_proto_rawDescData +} + +var file_proxy_shadowsocks_config_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_proxy_shadowsocks_config_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_proxy_shadowsocks_config_proto_goTypes = []any{ + (CipherType)(0), // 0: xray.proxy.shadowsocks.CipherType + (*Account)(nil), // 1: xray.proxy.shadowsocks.Account + (*ServerConfig)(nil), // 2: xray.proxy.shadowsocks.ServerConfig + (*ClientConfig)(nil), // 3: xray.proxy.shadowsocks.ClientConfig + (*protocol.User)(nil), // 4: xray.common.protocol.User + (net.Network)(0), // 5: xray.common.net.Network + (*protocol.ServerEndpoint)(nil), // 6: xray.common.protocol.ServerEndpoint +} +var file_proxy_shadowsocks_config_proto_depIdxs = []int32{ + 0, // 0: xray.proxy.shadowsocks.Account.cipher_type:type_name -> xray.proxy.shadowsocks.CipherType + 4, // 1: xray.proxy.shadowsocks.ServerConfig.users:type_name -> xray.common.protocol.User + 5, // 2: xray.proxy.shadowsocks.ServerConfig.network:type_name -> xray.common.net.Network + 6, // 3: xray.proxy.shadowsocks.ClientConfig.server:type_name -> xray.common.protocol.ServerEndpoint + 4, // [4:4] is the sub-list for method output_type + 4, // [4:4] is the sub-list for method input_type + 4, // [4:4] is the sub-list for extension type_name + 4, // [4:4] is the sub-list for extension extendee + 0, // [0:4] is the sub-list for field type_name +} + +func init() { file_proxy_shadowsocks_config_proto_init() } +func file_proxy_shadowsocks_config_proto_init() { + if File_proxy_shadowsocks_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_proxy_shadowsocks_config_proto_rawDesc), len(file_proxy_shadowsocks_config_proto_rawDesc)), + NumEnums: 1, + NumMessages: 3, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_proxy_shadowsocks_config_proto_goTypes, + DependencyIndexes: file_proxy_shadowsocks_config_proto_depIdxs, + EnumInfos: file_proxy_shadowsocks_config_proto_enumTypes, + MessageInfos: file_proxy_shadowsocks_config_proto_msgTypes, + }.Build() + File_proxy_shadowsocks_config_proto = out.File + file_proxy_shadowsocks_config_proto_goTypes = nil + file_proxy_shadowsocks_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/proxy/shadowsocks/config.proto b/subproject/Xray-core-main/proxy/shadowsocks/config.proto new file mode 100644 index 00000000..879fb77b --- /dev/null +++ b/subproject/Xray-core-main/proxy/shadowsocks/config.proto @@ -0,0 +1,36 @@ +syntax = "proto3"; + +package xray.proxy.shadowsocks; +option csharp_namespace = "Xray.Proxy.Shadowsocks"; +option go_package = "github.com/xtls/xray-core/proxy/shadowsocks"; +option java_package = "com.xray.proxy.shadowsocks"; +option java_multiple_files = true; + +import "common/net/network.proto"; +import "common/protocol/user.proto"; +import "common/protocol/server_spec.proto"; + +message Account { + string password = 1; + CipherType cipher_type = 2; + + bool iv_check = 3; +} + +enum CipherType { + UNKNOWN = 0; + AES_128_GCM = 5; + AES_256_GCM = 6; + CHACHA20_POLY1305 = 7; + XCHACHA20_POLY1305 = 8; + NONE = 9; +} + +message ServerConfig { + repeated xray.common.protocol.User users = 1; + repeated xray.common.net.Network network = 2; +} + +message ClientConfig { + xray.common.protocol.ServerEndpoint server = 1; +} diff --git a/subproject/Xray-core-main/proxy/shadowsocks/config_test.go b/subproject/Xray-core-main/proxy/shadowsocks/config_test.go new file mode 100644 index 00000000..3db6dc2d --- /dev/null +++ b/subproject/Xray-core-main/proxy/shadowsocks/config_test.go @@ -0,0 +1,38 @@ +package shadowsocks_test + +import ( + "crypto/rand" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/proxy/shadowsocks" +) + +func TestAEADCipherUDP(t *testing.T) { + rawAccount := &shadowsocks.Account{ + CipherType: shadowsocks.CipherType_AES_128_GCM, + Password: "test", + } + account, err := rawAccount.AsAccount() + common.Must(err) + + cipher := account.(*shadowsocks.MemoryAccount).Cipher + + key := make([]byte, cipher.KeySize()) + common.Must2(rand.Read(key)) + + payload := make([]byte, 1024) + common.Must2(rand.Read(payload)) + + b1 := buf.New() + common.Must2(b1.ReadFullFrom(rand.Reader, cipher.IVSize())) + common.Must2(b1.Write(payload)) + common.Must(cipher.EncodePacket(key, b1)) + + common.Must(cipher.DecodePacket(key, b1)) + if diff := cmp.Diff(b1.Bytes(), payload); diff != "" { + t.Error(diff) + } +} diff --git a/subproject/Xray-core-main/proxy/shadowsocks/protocol.go b/subproject/Xray-core-main/proxy/shadowsocks/protocol.go new file mode 100644 index 00000000..88006cde --- /dev/null +++ b/subproject/Xray-core-main/proxy/shadowsocks/protocol.go @@ -0,0 +1,341 @@ +package shadowsocks + +import ( + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + goerrors "errors" + "hash/crc32" + "io" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/crypto" + "github.com/xtls/xray-core/common/drain" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" +) + +const ( + Version = 1 +) + +var addrParser = protocol.NewAddressParser( + protocol.AddressFamilyByte(0x01, net.AddressFamilyIPv4), + protocol.AddressFamilyByte(0x04, net.AddressFamilyIPv6), + protocol.AddressFamilyByte(0x03, net.AddressFamilyDomain), + protocol.WithAddressTypeParser(func(b byte) byte { + return b & 0x0F + }), +) + +type FullReader struct { + reader io.Reader + buffer []byte +} + +func (r *FullReader) Read(p []byte) (n int, err error) { + if r.buffer != nil { + n := copy(p, r.buffer) + if n == len(r.buffer) { + r.buffer = nil + } else { + r.buffer = r.buffer[n:] + } + if n == len(p) { + return n, nil + } else { + m, err := r.reader.Read(p[n:]) + return n + m, err + } + } + return r.reader.Read(p) +} + +// ReadTCPSession reads a Shadowsocks TCP session from the given reader, returns its header and remaining parts. +func ReadTCPSession(validator *Validator, reader io.Reader) (*protocol.RequestHeader, buf.Reader, error) { + behaviorSeed := validator.GetBehaviorSeed() + drainer, errDrain := drain.NewBehaviorSeedLimitedDrainer(int64(behaviorSeed), 16+38, 3266, 64) + + if errDrain != nil { + return nil, nil, errors.New("failed to initialize drainer").Base(errDrain) + } + + var r buf.Reader + buffer := buf.New() + defer buffer.Release() + + if _, err := buffer.ReadFullFrom(reader, 50); err != nil { + drainer.AcknowledgeReceive(int(buffer.Len())) + return nil, nil, drain.WithError(drainer, reader, errors.New("failed to read 50 bytes").Base(err)) + } + + bs := buffer.Bytes() + user, aead, _, ivLen, err := validator.Get(bs, protocol.RequestCommandTCP) + + switch err { + case ErrNotFound: + drainer.AcknowledgeReceive(int(buffer.Len())) + return nil, nil, drain.WithError(drainer, reader, errors.New("failed to match an user").Base(err)) + case ErrIVNotUnique: + drainer.AcknowledgeReceive(int(buffer.Len())) + return nil, nil, drain.WithError(drainer, reader, errors.New("failed iv check").Base(err)) + default: + reader = &FullReader{reader, bs[ivLen:]} + drainer.AcknowledgeReceive(int(ivLen)) + + if aead != nil { + auth := &crypto.AEADAuthenticator{ + AEAD: aead, + NonceGenerator: crypto.GenerateAEADNonceWithSize(aead.NonceSize()), + } + r = crypto.NewAuthenticationReader(auth, &crypto.AEADChunkSizeParser{ + Auth: auth, + }, reader, protocol.TransferTypeStream, nil) + } else { + account := user.Account.(*MemoryAccount) + iv := append([]byte(nil), buffer.BytesTo(ivLen)...) + r, err = account.Cipher.NewDecryptionReader(account.Key, iv, reader) + if err != nil { + return nil, nil, drain.WithError(drainer, reader, errors.New("failed to initialize decoding stream").Base(err).AtError()) + } + } + } + + br := &buf.BufferedReader{Reader: r} + + request := &protocol.RequestHeader{ + Version: Version, + User: user, + Command: protocol.RequestCommandTCP, + } + + buffer.Clear() + + addr, port, err := addrParser.ReadAddressPort(buffer, br) + if err != nil { + drainer.AcknowledgeReceive(int(buffer.Len())) + return nil, nil, drain.WithError(drainer, reader, errors.New("failed to read address").Base(err)) + } + + request.Address = addr + request.Port = port + + if request.Address == nil { + drainer.AcknowledgeReceive(int(buffer.Len())) + return nil, nil, drain.WithError(drainer, reader, errors.New("invalid remote address.")) + } + + return request, br, nil +} + +// WriteTCPRequest writes Shadowsocks request into the given writer, and returns a writer for body. +func WriteTCPRequest(request *protocol.RequestHeader, writer io.Writer) (buf.Writer, error) { + user := request.User + account := user.Account.(*MemoryAccount) + + var iv []byte + if account.Cipher.IVSize() > 0 { + iv = make([]byte, account.Cipher.IVSize()) + common.Must2(rand.Read(iv)) + if err := buf.WriteAllBytes(writer, iv, nil); err != nil { + return nil, errors.New("failed to write IV") + } + } + + w, err := account.Cipher.NewEncryptionWriter(account.Key, iv, writer) + if err != nil { + return nil, errors.New("failed to create encoding stream").Base(err).AtError() + } + + header := buf.New() + + if err := addrParser.WriteAddressPort(header, request.Address, request.Port); err != nil { + return nil, errors.New("failed to write address").Base(err) + } + + if err := w.WriteMultiBuffer(buf.MultiBuffer{header}); err != nil { + return nil, errors.New("failed to write header").Base(err) + } + + return w, nil +} + +func ReadTCPResponse(user *protocol.MemoryUser, reader io.Reader) (buf.Reader, error) { + account := user.Account.(*MemoryAccount) + + hashkdf := hmac.New(sha256.New, []byte("SSBSKDF")) + hashkdf.Write(account.Key) + + behaviorSeed := crc32.ChecksumIEEE(hashkdf.Sum(nil)) + + drainer, err := drain.NewBehaviorSeedLimitedDrainer(int64(behaviorSeed), 16+38, 3266, 64) + if err != nil { + return nil, errors.New("failed to initialize drainer").Base(err) + } + + var iv []byte + if account.Cipher.IVSize() > 0 { + iv = make([]byte, account.Cipher.IVSize()) + if n, err := io.ReadFull(reader, iv); err != nil { + return nil, errors.New("failed to read IV").Base(err) + } else { // nolint: golint + drainer.AcknowledgeReceive(n) + } + } + + return account.Cipher.NewDecryptionReader(account.Key, iv, reader) +} + +func WriteTCPResponse(request *protocol.RequestHeader, writer io.Writer) (buf.Writer, error) { + user := request.User + account := user.Account.(*MemoryAccount) + + var iv []byte + if account.Cipher.IVSize() > 0 { + iv = make([]byte, account.Cipher.IVSize()) + common.Must2(rand.Read(iv)) + if err := buf.WriteAllBytes(writer, iv, nil); err != nil { + return nil, errors.New("failed to write IV.").Base(err) + } + } + + return account.Cipher.NewEncryptionWriter(account.Key, iv, writer) +} + +func EncodeUDPPacket(request *protocol.RequestHeader, payload []byte) (*buf.Buffer, error) { + user := request.User + account := user.Account.(*MemoryAccount) + + buffer := buf.New() + ivLen := account.Cipher.IVSize() + if ivLen > 0 { + common.Must2(buffer.ReadFullFrom(rand.Reader, ivLen)) + } + + if err := addrParser.WriteAddressPort(buffer, request.Address, request.Port); err != nil { + return nil, errors.New("failed to write address").Base(err) + } + + buffer.Write(payload) + + if err := account.Cipher.EncodePacket(account.Key, buffer); err != nil { + return nil, errors.New("failed to encrypt UDP payload").Base(err) + } + + return buffer, nil +} + +func DecodeUDPPacket(validator *Validator, payload *buf.Buffer) (*protocol.RequestHeader, *buf.Buffer, error) { + rawPayload := payload.Bytes() + user, _, d, _, err := validator.Get(rawPayload, protocol.RequestCommandUDP) + + if goerrors.Is(err, ErrIVNotUnique) { + return nil, nil, errors.New("failed iv check").Base(err) + } + + if goerrors.Is(err, ErrNotFound) { + return nil, nil, errors.New("failed to match an user").Base(err) + } + + if err != nil { + return nil, nil, errors.New("unexpected error").Base(err) + } + + account, ok := user.Account.(*MemoryAccount) + if !ok { + return nil, nil, errors.New("expected MemoryAccount returned from validator") + } + + if account.Cipher.IsAEAD() { + payload.Clear() + payload.Write(d) + } else { + if account.Cipher.IVSize() > 0 { + iv := make([]byte, account.Cipher.IVSize()) + copy(iv, payload.BytesTo(account.Cipher.IVSize())) + } + if err = account.Cipher.DecodePacket(account.Key, payload); err != nil { + return nil, nil, errors.New("failed to decrypt UDP payload").Base(err) + } + } + + payload.SetByte(0, payload.Byte(0)&0x0F) + + addr, port, err := addrParser.ReadAddressPort(nil, payload) + if err != nil { + return nil, nil, errors.New("failed to parse address").Base(err) + } + + request := &protocol.RequestHeader{ + Version: Version, + User: user, + Command: protocol.RequestCommandUDP, + Address: addr, + Port: port, + } + + return request, payload, nil +} + +type UDPReader struct { + Reader io.Reader + User *protocol.MemoryUser +} + +func (v *UDPReader) ReadMultiBuffer() (buf.MultiBuffer, error) { + buffer := buf.New() + _, err := buffer.ReadFrom(v.Reader) + if err != nil { + buffer.Release() + return nil, err + } + validator := new(Validator) + validator.Add(v.User) + + u, payload, err := DecodeUDPPacket(validator, buffer) + if err != nil { + buffer.Release() + return nil, err + } + dest := u.Destination() + payload.UDP = &dest + return buf.MultiBuffer{payload}, nil +} + +type UDPWriter struct { + Writer io.Writer + Request *protocol.RequestHeader +} + +func (w *UDPWriter) WriteMultiBuffer(mb buf.MultiBuffer) error { + for { + mb2, b := buf.SplitFirst(mb) + mb = mb2 + if b == nil { + break + } + request := w.Request + if b.UDP != nil { + request = &protocol.RequestHeader{ + User: w.Request.User, + Address: b.UDP.Address, + Port: b.UDP.Port, + } + } + packet, err := EncodeUDPPacket(request, b.Bytes()) + b.Release() + if err != nil { + buf.ReleaseMulti(mb) + return err + } + _, err = w.Writer.Write(packet.Bytes()) + packet.Release() + if err != nil { + buf.ReleaseMulti(mb) + return err + } + } + return nil +} diff --git a/subproject/Xray-core-main/proxy/shadowsocks/protocol_test.go b/subproject/Xray-core-main/proxy/shadowsocks/protocol_test.go new file mode 100644 index 00000000..4083905d --- /dev/null +++ b/subproject/Xray-core-main/proxy/shadowsocks/protocol_test.go @@ -0,0 +1,238 @@ +package shadowsocks_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + . "github.com/xtls/xray-core/proxy/shadowsocks" +) + +func toAccount(a *Account) protocol.Account { + account, err := a.AsAccount() + common.Must(err) + return account +} + +func equalRequestHeader(x, y *protocol.RequestHeader) bool { + return cmp.Equal(x, y, cmp.Comparer(func(x, y protocol.RequestHeader) bool { + return x == y + })) +} + +func TestUDPEncodingDecoding(t *testing.T) { + testRequests := []protocol.RequestHeader{ + { + Version: Version, + Command: protocol.RequestCommandUDP, + Address: net.LocalHostIP, + Port: 1234, + User: &protocol.MemoryUser{ + Email: "love@example.com", + Account: toAccount(&Account{ + Password: "password", + CipherType: CipherType_AES_128_GCM, + }), + }, + }, + { + Version: Version, + Command: protocol.RequestCommandUDP, + Address: net.LocalHostIP, + Port: 1234, + User: &protocol.MemoryUser{ + Email: "love@example.com", + Account: toAccount(&Account{ + Password: "123", + CipherType: CipherType_NONE, + }), + }, + }, + } + + for _, request := range testRequests { + data := buf.New() + common.Must2(data.WriteString("test string")) + encodedData, err := EncodeUDPPacket(&request, data.Bytes()) + common.Must(err) + + validator := new(Validator) + validator.Add(request.User) + decodedRequest, decodedData, err := DecodeUDPPacket(validator, encodedData) + common.Must(err) + + if r := cmp.Diff(decodedData.Bytes(), data.Bytes()); r != "" { + t.Error("data: ", r) + } + + if equalRequestHeader(decodedRequest, &request) == false { + t.Error("different request") + } + } +} + +func TestUDPDecodingWithPayloadTooShort(t *testing.T) { + testAccounts := []protocol.Account{ + toAccount(&Account{ + Password: "password", + CipherType: CipherType_AES_128_GCM, + }), + toAccount(&Account{ + Password: "password", + CipherType: CipherType_NONE, + }), + } + + for _, account := range testAccounts { + data := buf.New() + data.WriteString("short payload") + validator := new(Validator) + validator.Add(&protocol.MemoryUser{ + Account: account, + }) + _, _, err := DecodeUDPPacket(validator, data) + if err == nil { + t.Fatal("expected error") + } + } +} + +func TestTCPRequest(t *testing.T) { + cases := []struct { + request *protocol.RequestHeader + payload []byte + }{ + { + request: &protocol.RequestHeader{ + Version: Version, + Command: protocol.RequestCommandTCP, + Address: net.LocalHostIP, + Port: 1234, + User: &protocol.MemoryUser{ + Email: "love@example.com", + Account: toAccount(&Account{ + Password: "tcp-password", + CipherType: CipherType_AES_128_GCM, + }), + }, + }, + payload: []byte("test string"), + }, + { + request: &protocol.RequestHeader{ + Version: Version, + Command: protocol.RequestCommandTCP, + Address: net.LocalHostIPv6, + Port: 1234, + User: &protocol.MemoryUser{ + Email: "love@example.com", + Account: toAccount(&Account{ + Password: "password", + CipherType: CipherType_AES_256_GCM, + }), + }, + }, + payload: []byte("test string"), + }, + { + request: &protocol.RequestHeader{ + Version: Version, + Command: protocol.RequestCommandTCP, + Address: net.DomainAddress("example.com"), + Port: 1234, + User: &protocol.MemoryUser{ + Email: "love@example.com", + Account: toAccount(&Account{ + Password: "password", + CipherType: CipherType_CHACHA20_POLY1305, + }), + }, + }, + payload: []byte("test string"), + }, + } + + runTest := func(request *protocol.RequestHeader, payload []byte) { + data := buf.New() + common.Must2(data.Write(payload)) + + cache := buf.New() + defer cache.Release() + + writer, err := WriteTCPRequest(request, cache) + common.Must(err) + + common.Must(writer.WriteMultiBuffer(buf.MultiBuffer{data})) + + validator := new(Validator) + validator.Add(request.User) + decodedRequest, reader, err := ReadTCPSession(validator, cache) + common.Must(err) + if equalRequestHeader(decodedRequest, request) == false { + t.Error("different request") + } + + decodedData, err := reader.ReadMultiBuffer() + common.Must(err) + if r := cmp.Diff(decodedData[0].Bytes(), payload); r != "" { + t.Error("data: ", r) + } + } + + for _, test := range cases { + runTest(test.request, test.payload) + } +} + +func TestUDPReaderWriter(t *testing.T) { + user := &protocol.MemoryUser{ + Account: toAccount(&Account{ + Password: "test-password", + CipherType: CipherType_CHACHA20_POLY1305, + }), + } + cache := buf.New() + defer cache.Release() + + writer := &UDPWriter{ + Writer: cache, + Request: &protocol.RequestHeader{ + Version: Version, + Address: net.DomainAddress("example.com"), + Port: 123, + User: user, + }, + } + + reader := &UDPReader{ + Reader: cache, + User: user, + } + + { + b := buf.New() + common.Must2(b.WriteString("test payload")) + common.Must(writer.WriteMultiBuffer(buf.MultiBuffer{b})) + + payload, err := reader.ReadMultiBuffer() + common.Must(err) + if payload[0].String() != "test payload" { + t.Error("unexpected output: ", payload[0].String()) + } + } + + { + b := buf.New() + common.Must2(b.WriteString("test payload 2")) + common.Must(writer.WriteMultiBuffer(buf.MultiBuffer{b})) + + payload, err := reader.ReadMultiBuffer() + common.Must(err) + if payload[0].String() != "test payload 2" { + t.Error("unexpected output: ", payload[0].String()) + } + } +} diff --git a/subproject/Xray-core-main/proxy/shadowsocks/server.go b/subproject/Xray-core-main/proxy/shadowsocks/server.go new file mode 100644 index 00000000..360ea38c --- /dev/null +++ b/subproject/Xray-core-main/proxy/shadowsocks/server.go @@ -0,0 +1,299 @@ +package shadowsocks + +import ( + "context" + "time" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/log" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + udp_proto "github.com/xtls/xray-core/common/protocol/udp" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/common/signal" + "github.com/xtls/xray-core/common/task" + "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/features/policy" + "github.com/xtls/xray-core/features/routing" + "github.com/xtls/xray-core/transport/internet/stat" + "github.com/xtls/xray-core/transport/internet/udp" +) + +type Server struct { + config *ServerConfig + validator *Validator + policyManager policy.Manager + cone bool +} + +// NewServer create a new Shadowsocks server. +func NewServer(ctx context.Context, config *ServerConfig) (*Server, error) { + validator := new(Validator) + for _, user := range config.Users { + u, err := user.ToMemoryUser() + if err != nil { + return nil, errors.New("failed to get shadowsocks user").Base(err).AtError() + } + + if err := validator.Add(u); err != nil { + return nil, errors.New("failed to add user").Base(err).AtError() + } + } + + v := core.MustFromContext(ctx) + s := &Server{ + config: config, + validator: validator, + policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager), + cone: ctx.Value("cone").(bool), + } + + return s, nil +} + +// AddUser implements proxy.UserManager.AddUser(). +func (s *Server) AddUser(ctx context.Context, u *protocol.MemoryUser) error { + return s.validator.Add(u) +} + +// RemoveUser implements proxy.UserManager.RemoveUser(). +func (s *Server) RemoveUser(ctx context.Context, e string) error { + return s.validator.Del(e) +} + +// GetUser implements proxy.UserManager.GetUser(). +func (s *Server) GetUser(ctx context.Context, email string) *protocol.MemoryUser { + return s.validator.GetByEmail(email) +} + +// GetUsers implements proxy.UserManager.GetUsers(). +func (s *Server) GetUsers(ctx context.Context) []*protocol.MemoryUser { + return s.validator.GetAll() +} + +// GetUsersCount implements proxy.UserManager.GetUsersCount(). +func (s *Server) GetUsersCount(context.Context) int64 { + return s.validator.GetCount() +} + +func (s *Server) Network() []net.Network { + list := s.config.Network + if len(list) == 0 { + list = append(list, net.Network_TCP) + } + return list +} + +func (s *Server) Process(ctx context.Context, network net.Network, conn stat.Connection, dispatcher routing.Dispatcher) error { + inbound := session.InboundFromContext(ctx) + inbound.Name = "shadowsocks" + inbound.CanSpliceCopy = 3 + + switch network { + case net.Network_TCP: + return s.handleConnection(ctx, conn, dispatcher) + case net.Network_UDP: + return s.handleUDPPayload(ctx, conn, dispatcher) + default: + return errors.New("unknown network: ", network) + } +} + +func (s *Server) handleUDPPayload(ctx context.Context, conn stat.Connection, dispatcher routing.Dispatcher) error { + udpServer := udp.NewDispatcher(dispatcher, func(ctx context.Context, packet *udp_proto.Packet) { + request := protocol.RequestHeaderFromContext(ctx) + payload := packet.Payload + if request == nil { + payload.Release() + return + } + + if payload.UDP != nil { + request = &protocol.RequestHeader{ + User: request.User, + Address: payload.UDP.Address, + Port: payload.UDP.Port, + } + } + + data, err := EncodeUDPPacket(request, payload.Bytes()) + payload.Release() + if err != nil { + errors.LogWarningInner(ctx, err, "failed to encode UDP packet") + return + } + + conn.Write(data.Bytes()) + data.Release() + }) + defer udpServer.RemoveRay() + + inbound := session.InboundFromContext(ctx) + var dest *net.Destination + reader := buf.NewPacketReader(conn) + for { + mpayload, err := reader.ReadMultiBuffer() + if err != nil { + break + } + + for _, payload := range mpayload { + var request *protocol.RequestHeader + var data *buf.Buffer + var err error + + if inbound.User != nil { + validator := new(Validator) + validator.Add(inbound.User) + request, data, err = DecodeUDPPacket(validator, payload) + } else { + request, data, err = DecodeUDPPacket(s.validator, payload) + if err == nil { + inbound.User = request.User + } + } + + if err != nil { + if inbound.Source.IsValid() { + errors.LogInfoInner(ctx, err, "dropping invalid UDP packet from: ", inbound.Source) + log.Record(&log.AccessMessage{ + From: inbound.Source, + To: "", + Status: log.AccessRejected, + Reason: err, + }) + } + payload.Release() + continue + } + + destination := request.Destination() + + currentPacketCtx := ctx + if inbound.Source.IsValid() { + currentPacketCtx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{ + From: inbound.Source, + To: destination, + Status: log.AccessAccepted, + Reason: "", + Email: request.User.Email, + }) + } + errors.LogInfo(ctx, "tunnelling request to ", destination) + + data.UDP = &destination + + if !s.cone || dest == nil { + dest = &destination + } + + currentPacketCtx = protocol.ContextWithRequestHeader(currentPacketCtx, request) + udpServer.Dispatch(currentPacketCtx, *dest, data) + } + } + + return nil +} + +func (s *Server) handleConnection(ctx context.Context, conn stat.Connection, dispatcher routing.Dispatcher) error { + sessionPolicy := s.policyManager.ForLevel(0) + if err := conn.SetReadDeadline(time.Now().Add(sessionPolicy.Timeouts.Handshake)); err != nil { + return errors.New("unable to set read deadline").Base(err).AtWarning() + } + + bufferedReader := buf.BufferedReader{Reader: buf.NewReader(conn)} + request, bodyReader, err := ReadTCPSession(s.validator, &bufferedReader) + if err != nil { + log.Record(&log.AccessMessage{ + From: conn.RemoteAddr(), + To: "", + Status: log.AccessRejected, + Reason: err, + }) + return errors.New("failed to create request from: ", conn.RemoteAddr()).Base(err) + } + conn.SetReadDeadline(time.Time{}) + + inbound := session.InboundFromContext(ctx) + if inbound == nil { + panic("no inbound metadata") + } + inbound.User = request.User + + dest := request.Destination() + ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{ + From: conn.RemoteAddr(), + To: dest, + Status: log.AccessAccepted, + Reason: "", + Email: request.User.Email, + }) + errors.LogInfo(ctx, "tunnelling request to ", dest) + + sessionPolicy = s.policyManager.ForLevel(request.User.Level) + ctx, cancel := context.WithCancel(ctx) + timer := signal.CancelAfterInactivity(ctx, cancel, sessionPolicy.Timeouts.ConnectionIdle) + + ctx = policy.ContextWithBufferPolicy(ctx, sessionPolicy.Buffer) + link, err := dispatcher.Dispatch(ctx, dest) + if err != nil { + return err + } + + responseDone := func() error { + defer timer.SetTimeout(sessionPolicy.Timeouts.UplinkOnly) + + bufferedWriter := buf.NewBufferedWriter(buf.NewWriter(conn)) + responseWriter, err := WriteTCPResponse(request, bufferedWriter) + if err != nil { + return errors.New("failed to write response").Base(err) + } + + { + payload, err := link.Reader.ReadMultiBuffer() + if err != nil { + return err + } + if err := responseWriter.WriteMultiBuffer(payload); err != nil { + return err + } + } + + if err := bufferedWriter.SetBuffered(false); err != nil { + return err + } + + if err := buf.Copy(link.Reader, responseWriter, buf.UpdateActivity(timer)); err != nil { + return errors.New("failed to transport all TCP response").Base(err) + } + + return nil + } + + requestDone := func() error { + defer timer.SetTimeout(sessionPolicy.Timeouts.DownlinkOnly) + + if err := buf.Copy(bodyReader, link.Writer, buf.UpdateActivity(timer)); err != nil { + return errors.New("failed to transport all TCP request").Base(err) + } + + return nil + } + + requestDoneAndCloseWriter := task.OnSuccess(requestDone, task.Close(link.Writer)) + if err := task.Run(ctx, requestDoneAndCloseWriter, responseDone); err != nil { + common.Interrupt(link.Reader) + common.Interrupt(link.Writer) + return errors.New("connection ends").Base(err) + } + + return nil +} + +func init() { + common.Must(common.RegisterConfig((*ServerConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + return NewServer(ctx, config.(*ServerConfig)) + })) +} diff --git a/subproject/Xray-core-main/proxy/shadowsocks/shadowsocks.go b/subproject/Xray-core-main/proxy/shadowsocks/shadowsocks.go new file mode 100644 index 00000000..7bb16943 --- /dev/null +++ b/subproject/Xray-core-main/proxy/shadowsocks/shadowsocks.go @@ -0,0 +1,6 @@ +// Package shadowsocks provides compatible functionality to Shadowsocks. +// +// Shadowsocks client and server are implemented as outbound and inbound respectively in Xray's term. +// +// R.I.P Shadowsocks +package shadowsocks diff --git a/subproject/Xray-core-main/proxy/shadowsocks/validator.go b/subproject/Xray-core-main/proxy/shadowsocks/validator.go new file mode 100644 index 00000000..2c48334f --- /dev/null +++ b/subproject/Xray-core-main/proxy/shadowsocks/validator.go @@ -0,0 +1,165 @@ +package shadowsocks + +import ( + "crypto/cipher" + "crypto/hmac" + "crypto/sha256" + "hash/crc64" + "strings" + "sync" + + "github.com/xtls/xray-core/common/dice" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/protocol" +) + +// Validator stores valid Shadowsocks users. +type Validator struct { + sync.RWMutex + users []*protocol.MemoryUser + + behaviorSeed uint64 + behaviorFused bool +} + +var ErrNotFound = errors.New("Not Found") + +// Add a Shadowsocks user. +func (v *Validator) Add(u *protocol.MemoryUser) error { + v.Lock() + defer v.Unlock() + + account := u.Account.(*MemoryAccount) + if !account.Cipher.IsAEAD() && len(v.users) > 0 { + return errors.New("The cipher is not support Single-port Multi-user") + } + v.users = append(v.users, u) + + if !v.behaviorFused { + hashkdf := hmac.New(sha256.New, []byte("SSBSKDF")) + hashkdf.Write(account.Key) + v.behaviorSeed = crc64.Update(v.behaviorSeed, crc64.MakeTable(crc64.ECMA), hashkdf.Sum(nil)) + } + + return nil +} + +// Del a Shadowsocks user with a non-empty Email. +func (v *Validator) Del(email string) error { + if email == "" { + return errors.New("Email must not be empty.") + } + + v.Lock() + defer v.Unlock() + + email = strings.ToLower(email) + idx := -1 + for i, u := range v.users { + if strings.EqualFold(u.Email, email) { + idx = i + break + } + } + + if idx == -1 { + return errors.New("User ", email, " not found.") + } + ulen := len(v.users) + + v.users[idx] = v.users[ulen-1] + v.users[ulen-1] = nil + v.users = v.users[:ulen-1] + + return nil +} + +// GetByEmail Get a Shadowsocks user with a non-empty Email. +func (v *Validator) GetByEmail(email string) *protocol.MemoryUser { + if email == "" { + return nil + } + + v.Lock() + defer v.Unlock() + + email = strings.ToLower(email) + for _, u := range v.users { + if strings.EqualFold(u.Email, email) { + return u + } + } + return nil +} + +// GetAll get all users +func (v *Validator) GetAll() []*protocol.MemoryUser { + v.Lock() + defer v.Unlock() + dst := make([]*protocol.MemoryUser, len(v.users)) + copy(dst, v.users) + return dst +} + +// GetCount get users count +func (v *Validator) GetCount() int64 { + v.Lock() + defer v.Unlock() + return int64(len(v.users)) +} + +// Get a Shadowsocks user. +func (v *Validator) Get(bs []byte, command protocol.RequestCommand) (u *protocol.MemoryUser, aead cipher.AEAD, ret []byte, ivLen int32, err error) { + v.RLock() + defer v.RUnlock() + + for _, user := range v.users { + if account := user.Account.(*MemoryAccount); account.Cipher.IsAEAD() { + // AEAD payload decoding requires the payload to be over 32 bytes + if len(bs) < 32 { + continue + } + + aeadCipher := account.Cipher.(*AEADCipher) + ivLen = aeadCipher.IVSize() + iv := bs[:ivLen] + subkey := make([]byte, 32) + subkey = subkey[:aeadCipher.KeyBytes] + hkdfSHA1(account.Key, iv, subkey) + aead = aeadCipher.AEADAuthCreator(subkey) + + var matchErr error + switch command { + case protocol.RequestCommandTCP: + data := make([]byte, 4+aead.NonceSize()) + ret, matchErr = aead.Open(data[:0], data[4:], bs[ivLen:ivLen+18], nil) + case protocol.RequestCommandUDP: + data := make([]byte, 8192) + ret, matchErr = aead.Open(data[:0], data[8192-aead.NonceSize():8192], bs[ivLen:], nil) + } + + if matchErr == nil { + u = user + return + } + } else { + u = user + ivLen = user.Account.(*MemoryAccount).Cipher.IVSize() + // err = user.Account.(*MemoryAccount).CheckIV(bs[:ivLen]) // The IV size of None Cipher is 0. + return + } + } + + return nil, nil, nil, 0, ErrNotFound +} + +func (v *Validator) GetBehaviorSeed() uint64 { + v.Lock() + defer v.Unlock() + + v.behaviorFused = true + if v.behaviorSeed == 0 { + v.behaviorSeed = dice.RollUint64() + } + return v.behaviorSeed +} diff --git a/subproject/Xray-core-main/proxy/shadowsocks_2022/config.go b/subproject/Xray-core-main/proxy/shadowsocks_2022/config.go new file mode 100644 index 00000000..9ddd2cf8 --- /dev/null +++ b/subproject/Xray-core-main/proxy/shadowsocks_2022/config.go @@ -0,0 +1,33 @@ +package shadowsocks_2022 + +import ( + "google.golang.org/protobuf/proto" + + "github.com/xtls/xray-core/common/protocol" +) + +// MemoryAccount is an account type converted from Account. +type MemoryAccount struct { + Key string +} + +// AsAccount implements protocol.AsAccount. +func (u *Account) AsAccount() (protocol.Account, error) { + return &MemoryAccount{ + Key: u.GetKey(), + }, nil +} + +// Equals implements protocol.Account.Equals(). +func (a *MemoryAccount) Equals(another protocol.Account) bool { + if account, ok := another.(*MemoryAccount); ok { + return a.Key == account.Key + } + return false +} + +func (a *MemoryAccount) ToProto() proto.Message { + return &Account{ + Key: a.Key, + } +} diff --git a/subproject/Xray-core-main/proxy/shadowsocks_2022/config.pb.go b/subproject/Xray-core-main/proxy/shadowsocks_2022/config.pb.go new file mode 100644 index 00000000..df2ee7cb --- /dev/null +++ b/subproject/Xray-core-main/proxy/shadowsocks_2022/config.pb.go @@ -0,0 +1,542 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: proxy/shadowsocks_2022/config.proto + +package shadowsocks_2022 + +import ( + net "github.com/xtls/xray-core/common/net" + protocol "github.com/xtls/xray-core/common/protocol" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type ServerConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + Method string `protobuf:"bytes,1,opt,name=method,proto3" json:"method,omitempty"` + Key string `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"` + Email string `protobuf:"bytes,3,opt,name=email,proto3" json:"email,omitempty"` + Level int32 `protobuf:"varint,4,opt,name=level,proto3" json:"level,omitempty"` + Network []net.Network `protobuf:"varint,5,rep,packed,name=network,proto3,enum=xray.common.net.Network" json:"network,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ServerConfig) Reset() { + *x = ServerConfig{} + mi := &file_proxy_shadowsocks_2022_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ServerConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ServerConfig) ProtoMessage() {} + +func (x *ServerConfig) ProtoReflect() protoreflect.Message { + mi := &file_proxy_shadowsocks_2022_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ServerConfig.ProtoReflect.Descriptor instead. +func (*ServerConfig) Descriptor() ([]byte, []int) { + return file_proxy_shadowsocks_2022_config_proto_rawDescGZIP(), []int{0} +} + +func (x *ServerConfig) GetMethod() string { + if x != nil { + return x.Method + } + return "" +} + +func (x *ServerConfig) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *ServerConfig) GetEmail() string { + if x != nil { + return x.Email + } + return "" +} + +func (x *ServerConfig) GetLevel() int32 { + if x != nil { + return x.Level + } + return 0 +} + +func (x *ServerConfig) GetNetwork() []net.Network { + if x != nil { + return x.Network + } + return nil +} + +type MultiUserServerConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + Method string `protobuf:"bytes,1,opt,name=method,proto3" json:"method,omitempty"` + Key string `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"` + Users []*protocol.User `protobuf:"bytes,3,rep,name=users,proto3" json:"users,omitempty"` + Network []net.Network `protobuf:"varint,4,rep,packed,name=network,proto3,enum=xray.common.net.Network" json:"network,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MultiUserServerConfig) Reset() { + *x = MultiUserServerConfig{} + mi := &file_proxy_shadowsocks_2022_config_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MultiUserServerConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MultiUserServerConfig) ProtoMessage() {} + +func (x *MultiUserServerConfig) ProtoReflect() protoreflect.Message { + mi := &file_proxy_shadowsocks_2022_config_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MultiUserServerConfig.ProtoReflect.Descriptor instead. +func (*MultiUserServerConfig) Descriptor() ([]byte, []int) { + return file_proxy_shadowsocks_2022_config_proto_rawDescGZIP(), []int{1} +} + +func (x *MultiUserServerConfig) GetMethod() string { + if x != nil { + return x.Method + } + return "" +} + +func (x *MultiUserServerConfig) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *MultiUserServerConfig) GetUsers() []*protocol.User { + if x != nil { + return x.Users + } + return nil +} + +func (x *MultiUserServerConfig) GetNetwork() []net.Network { + if x != nil { + return x.Network + } + return nil +} + +type RelayDestination struct { + state protoimpl.MessageState `protogen:"open.v1"` + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + Address *net.IPOrDomain `protobuf:"bytes,2,opt,name=address,proto3" json:"address,omitempty"` + Port uint32 `protobuf:"varint,3,opt,name=port,proto3" json:"port,omitempty"` + Email string `protobuf:"bytes,4,opt,name=email,proto3" json:"email,omitempty"` + Level int32 `protobuf:"varint,5,opt,name=level,proto3" json:"level,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RelayDestination) Reset() { + *x = RelayDestination{} + mi := &file_proxy_shadowsocks_2022_config_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RelayDestination) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RelayDestination) ProtoMessage() {} + +func (x *RelayDestination) ProtoReflect() protoreflect.Message { + mi := &file_proxy_shadowsocks_2022_config_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RelayDestination.ProtoReflect.Descriptor instead. +func (*RelayDestination) Descriptor() ([]byte, []int) { + return file_proxy_shadowsocks_2022_config_proto_rawDescGZIP(), []int{2} +} + +func (x *RelayDestination) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *RelayDestination) GetAddress() *net.IPOrDomain { + if x != nil { + return x.Address + } + return nil +} + +func (x *RelayDestination) GetPort() uint32 { + if x != nil { + return x.Port + } + return 0 +} + +func (x *RelayDestination) GetEmail() string { + if x != nil { + return x.Email + } + return "" +} + +func (x *RelayDestination) GetLevel() int32 { + if x != nil { + return x.Level + } + return 0 +} + +type RelayServerConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + Method string `protobuf:"bytes,1,opt,name=method,proto3" json:"method,omitempty"` + Key string `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"` + Destinations []*RelayDestination `protobuf:"bytes,3,rep,name=destinations,proto3" json:"destinations,omitempty"` + Network []net.Network `protobuf:"varint,4,rep,packed,name=network,proto3,enum=xray.common.net.Network" json:"network,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RelayServerConfig) Reset() { + *x = RelayServerConfig{} + mi := &file_proxy_shadowsocks_2022_config_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RelayServerConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RelayServerConfig) ProtoMessage() {} + +func (x *RelayServerConfig) ProtoReflect() protoreflect.Message { + mi := &file_proxy_shadowsocks_2022_config_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RelayServerConfig.ProtoReflect.Descriptor instead. +func (*RelayServerConfig) Descriptor() ([]byte, []int) { + return file_proxy_shadowsocks_2022_config_proto_rawDescGZIP(), []int{3} +} + +func (x *RelayServerConfig) GetMethod() string { + if x != nil { + return x.Method + } + return "" +} + +func (x *RelayServerConfig) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *RelayServerConfig) GetDestinations() []*RelayDestination { + if x != nil { + return x.Destinations + } + return nil +} + +func (x *RelayServerConfig) GetNetwork() []net.Network { + if x != nil { + return x.Network + } + return nil +} + +type Account struct { + state protoimpl.MessageState `protogen:"open.v1"` + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Account) Reset() { + *x = Account{} + mi := &file_proxy_shadowsocks_2022_config_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Account) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Account) ProtoMessage() {} + +func (x *Account) ProtoReflect() protoreflect.Message { + mi := &file_proxy_shadowsocks_2022_config_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Account.ProtoReflect.Descriptor instead. +func (*Account) Descriptor() ([]byte, []int) { + return file_proxy_shadowsocks_2022_config_proto_rawDescGZIP(), []int{4} +} + +func (x *Account) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +type ClientConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + Address *net.IPOrDomain `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"` + Port uint32 `protobuf:"varint,2,opt,name=port,proto3" json:"port,omitempty"` + Method string `protobuf:"bytes,3,opt,name=method,proto3" json:"method,omitempty"` + Key string `protobuf:"bytes,4,opt,name=key,proto3" json:"key,omitempty"` + UdpOverTcp bool `protobuf:"varint,5,opt,name=udp_over_tcp,json=udpOverTcp,proto3" json:"udp_over_tcp,omitempty"` + UdpOverTcpVersion uint32 `protobuf:"varint,6,opt,name=udp_over_tcp_version,json=udpOverTcpVersion,proto3" json:"udp_over_tcp_version,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ClientConfig) Reset() { + *x = ClientConfig{} + mi := &file_proxy_shadowsocks_2022_config_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ClientConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClientConfig) ProtoMessage() {} + +func (x *ClientConfig) ProtoReflect() protoreflect.Message { + mi := &file_proxy_shadowsocks_2022_config_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ClientConfig.ProtoReflect.Descriptor instead. +func (*ClientConfig) Descriptor() ([]byte, []int) { + return file_proxy_shadowsocks_2022_config_proto_rawDescGZIP(), []int{5} +} + +func (x *ClientConfig) GetAddress() *net.IPOrDomain { + if x != nil { + return x.Address + } + return nil +} + +func (x *ClientConfig) GetPort() uint32 { + if x != nil { + return x.Port + } + return 0 +} + +func (x *ClientConfig) GetMethod() string { + if x != nil { + return x.Method + } + return "" +} + +func (x *ClientConfig) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *ClientConfig) GetUdpOverTcp() bool { + if x != nil { + return x.UdpOverTcp + } + return false +} + +func (x *ClientConfig) GetUdpOverTcpVersion() uint32 { + if x != nil { + return x.UdpOverTcpVersion + } + return 0 +} + +var File_proxy_shadowsocks_2022_config_proto protoreflect.FileDescriptor + +const file_proxy_shadowsocks_2022_config_proto_rawDesc = "" + + "\n" + + "#proxy/shadowsocks_2022/config.proto\x12\x1bxray.proxy.shadowsocks_2022\x1a\x18common/net/network.proto\x1a\x18common/net/address.proto\x1a\x1acommon/protocol/user.proto\"\x98\x01\n" + + "\fServerConfig\x12\x16\n" + + "\x06method\x18\x01 \x01(\tR\x06method\x12\x10\n" + + "\x03key\x18\x02 \x01(\tR\x03key\x12\x14\n" + + "\x05email\x18\x03 \x01(\tR\x05email\x12\x14\n" + + "\x05level\x18\x04 \x01(\x05R\x05level\x122\n" + + "\anetwork\x18\x05 \x03(\x0e2\x18.xray.common.net.NetworkR\anetwork\"\xa7\x01\n" + + "\x15MultiUserServerConfig\x12\x16\n" + + "\x06method\x18\x01 \x01(\tR\x06method\x12\x10\n" + + "\x03key\x18\x02 \x01(\tR\x03key\x120\n" + + "\x05users\x18\x03 \x03(\v2\x1a.xray.common.protocol.UserR\x05users\x122\n" + + "\anetwork\x18\x04 \x03(\x0e2\x18.xray.common.net.NetworkR\anetwork\"\x9b\x01\n" + + "\x10RelayDestination\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x125\n" + + "\aaddress\x18\x02 \x01(\v2\x1b.xray.common.net.IPOrDomainR\aaddress\x12\x12\n" + + "\x04port\x18\x03 \x01(\rR\x04port\x12\x14\n" + + "\x05email\x18\x04 \x01(\tR\x05email\x12\x14\n" + + "\x05level\x18\x05 \x01(\x05R\x05level\"\xc4\x01\n" + + "\x11RelayServerConfig\x12\x16\n" + + "\x06method\x18\x01 \x01(\tR\x06method\x12\x10\n" + + "\x03key\x18\x02 \x01(\tR\x03key\x12Q\n" + + "\fdestinations\x18\x03 \x03(\v2-.xray.proxy.shadowsocks_2022.RelayDestinationR\fdestinations\x122\n" + + "\anetwork\x18\x04 \x03(\x0e2\x18.xray.common.net.NetworkR\anetwork\"\x1b\n" + + "\aAccount\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\"\xd6\x01\n" + + "\fClientConfig\x125\n" + + "\aaddress\x18\x01 \x01(\v2\x1b.xray.common.net.IPOrDomainR\aaddress\x12\x12\n" + + "\x04port\x18\x02 \x01(\rR\x04port\x12\x16\n" + + "\x06method\x18\x03 \x01(\tR\x06method\x12\x10\n" + + "\x03key\x18\x04 \x01(\tR\x03key\x12 \n" + + "\fudp_over_tcp\x18\x05 \x01(\bR\n" + + "udpOverTcp\x12/\n" + + "\x14udp_over_tcp_version\x18\x06 \x01(\rR\x11udpOverTcpVersionBr\n" + + "\x1fcom.xray.proxy.shadowsocks_2022P\x01Z0github.com/xtls/xray-core/proxy/shadowsocks_2022\xaa\x02\x1aXray.Proxy.Shadowsocks2022b\x06proto3" + +var ( + file_proxy_shadowsocks_2022_config_proto_rawDescOnce sync.Once + file_proxy_shadowsocks_2022_config_proto_rawDescData []byte +) + +func file_proxy_shadowsocks_2022_config_proto_rawDescGZIP() []byte { + file_proxy_shadowsocks_2022_config_proto_rawDescOnce.Do(func() { + file_proxy_shadowsocks_2022_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proxy_shadowsocks_2022_config_proto_rawDesc), len(file_proxy_shadowsocks_2022_config_proto_rawDesc))) + }) + return file_proxy_shadowsocks_2022_config_proto_rawDescData +} + +var file_proxy_shadowsocks_2022_config_proto_msgTypes = make([]protoimpl.MessageInfo, 6) +var file_proxy_shadowsocks_2022_config_proto_goTypes = []any{ + (*ServerConfig)(nil), // 0: xray.proxy.shadowsocks_2022.ServerConfig + (*MultiUserServerConfig)(nil), // 1: xray.proxy.shadowsocks_2022.MultiUserServerConfig + (*RelayDestination)(nil), // 2: xray.proxy.shadowsocks_2022.RelayDestination + (*RelayServerConfig)(nil), // 3: xray.proxy.shadowsocks_2022.RelayServerConfig + (*Account)(nil), // 4: xray.proxy.shadowsocks_2022.Account + (*ClientConfig)(nil), // 5: xray.proxy.shadowsocks_2022.ClientConfig + (net.Network)(0), // 6: xray.common.net.Network + (*protocol.User)(nil), // 7: xray.common.protocol.User + (*net.IPOrDomain)(nil), // 8: xray.common.net.IPOrDomain +} +var file_proxy_shadowsocks_2022_config_proto_depIdxs = []int32{ + 6, // 0: xray.proxy.shadowsocks_2022.ServerConfig.network:type_name -> xray.common.net.Network + 7, // 1: xray.proxy.shadowsocks_2022.MultiUserServerConfig.users:type_name -> xray.common.protocol.User + 6, // 2: xray.proxy.shadowsocks_2022.MultiUserServerConfig.network:type_name -> xray.common.net.Network + 8, // 3: xray.proxy.shadowsocks_2022.RelayDestination.address:type_name -> xray.common.net.IPOrDomain + 2, // 4: xray.proxy.shadowsocks_2022.RelayServerConfig.destinations:type_name -> xray.proxy.shadowsocks_2022.RelayDestination + 6, // 5: xray.proxy.shadowsocks_2022.RelayServerConfig.network:type_name -> xray.common.net.Network + 8, // 6: xray.proxy.shadowsocks_2022.ClientConfig.address:type_name -> xray.common.net.IPOrDomain + 7, // [7:7] is the sub-list for method output_type + 7, // [7:7] is the sub-list for method input_type + 7, // [7:7] is the sub-list for extension type_name + 7, // [7:7] is the sub-list for extension extendee + 0, // [0:7] is the sub-list for field type_name +} + +func init() { file_proxy_shadowsocks_2022_config_proto_init() } +func file_proxy_shadowsocks_2022_config_proto_init() { + if File_proxy_shadowsocks_2022_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_proxy_shadowsocks_2022_config_proto_rawDesc), len(file_proxy_shadowsocks_2022_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 6, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_proxy_shadowsocks_2022_config_proto_goTypes, + DependencyIndexes: file_proxy_shadowsocks_2022_config_proto_depIdxs, + MessageInfos: file_proxy_shadowsocks_2022_config_proto_msgTypes, + }.Build() + File_proxy_shadowsocks_2022_config_proto = out.File + file_proxy_shadowsocks_2022_config_proto_goTypes = nil + file_proxy_shadowsocks_2022_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/proxy/shadowsocks_2022/config.proto b/subproject/Xray-core-main/proxy/shadowsocks_2022/config.proto new file mode 100644 index 00000000..14006648 --- /dev/null +++ b/subproject/Xray-core-main/proxy/shadowsocks_2022/config.proto @@ -0,0 +1,54 @@ +syntax = "proto3"; + +package xray.proxy.shadowsocks_2022; +option csharp_namespace = "Xray.Proxy.Shadowsocks2022"; +option go_package = "github.com/xtls/xray-core/proxy/shadowsocks_2022"; +option java_package = "com.xray.proxy.shadowsocks_2022"; +option java_multiple_files = true; + +import "common/net/network.proto"; +import "common/net/address.proto"; +import "common/protocol/user.proto"; + +message ServerConfig { + string method = 1; + string key = 2; + string email = 3; + int32 level = 4; + repeated xray.common.net.Network network = 5; +} + +message MultiUserServerConfig { + string method = 1; + string key = 2; + repeated xray.common.protocol.User users = 3; + repeated xray.common.net.Network network = 4; +} + +message RelayDestination { + string key = 1; + xray.common.net.IPOrDomain address = 2; + uint32 port = 3; + string email = 4; + int32 level = 5; +} + +message RelayServerConfig { + string method = 1; + string key = 2; + repeated RelayDestination destinations = 3; + repeated xray.common.net.Network network = 4; +} + +message Account { + string key = 1; +} + +message ClientConfig { + xray.common.net.IPOrDomain address = 1; + uint32 port = 2; + string method = 3; + string key = 4; + bool udp_over_tcp = 5; + uint32 udp_over_tcp_version = 6; +} diff --git a/subproject/Xray-core-main/proxy/shadowsocks_2022/inbound.go b/subproject/Xray-core-main/proxy/shadowsocks_2022/inbound.go new file mode 100644 index 00000000..a889fa0f --- /dev/null +++ b/subproject/Xray-core-main/proxy/shadowsocks_2022/inbound.go @@ -0,0 +1,171 @@ +package shadowsocks_2022 + +import ( + "context" + + shadowsocks "github.com/sagernet/sing-shadowsocks" + "github.com/sagernet/sing-shadowsocks/shadowaead_2022" + C "github.com/sagernet/sing/common" + B "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/bufio" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/log" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/common/singbridge" + "github.com/xtls/xray-core/features/routing" + "github.com/xtls/xray-core/transport/internet/stat" +) + +func init() { + common.Must(common.RegisterConfig((*ServerConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + return NewServer(ctx, config.(*ServerConfig)) + })) +} + +type Inbound struct { + networks []net.Network + service shadowsocks.Service + email string + level int +} + +func NewServer(ctx context.Context, config *ServerConfig) (*Inbound, error) { + networks := config.Network + if len(networks) == 0 { + networks = []net.Network{ + net.Network_TCP, + net.Network_UDP, + } + } + inbound := &Inbound{ + networks: networks, + email: config.Email, + level: int(config.Level), + } + if !C.Contains(shadowaead_2022.List, config.Method) { + return nil, errors.New("unsupported method ", config.Method) + } + service, err := shadowaead_2022.NewServiceWithPassword(config.Method, config.Key, 500, inbound, nil) + if err != nil { + return nil, errors.New("create service").Base(err) + } + inbound.service = service + return inbound, nil +} + +func (i *Inbound) Network() []net.Network { + return i.networks +} + +func (i *Inbound) Process(ctx context.Context, network net.Network, connection stat.Connection, dispatcher routing.Dispatcher) error { + inbound := session.InboundFromContext(ctx) + inbound.Name = "shadowsocks-2022" + inbound.CanSpliceCopy = 3 + + var metadata M.Metadata + if inbound.Source.IsValid() { + metadata.Source = M.ParseSocksaddr(inbound.Source.NetAddr()) + } + + ctx = session.ContextWithDispatcher(ctx, dispatcher) + + if network == net.Network_TCP { + return singbridge.ReturnError(i.service.NewConnection(ctx, connection, metadata)) + } else { + reader := buf.NewReader(connection) + pc := &natPacketConn{connection} + for { + mb, err := reader.ReadMultiBuffer() + if err != nil { + buf.ReleaseMulti(mb) + return singbridge.ReturnError(err) + } + for _, buffer := range mb { + packet := B.As(buffer.Bytes()).ToOwned() + buffer.Release() + err = i.service.NewPacket(ctx, pc, packet, metadata) + if err != nil { + packet.Release() + buf.ReleaseMulti(mb) + return err + } + } + } + } +} + +func (i *Inbound) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { + inbound := session.InboundFromContext(ctx) + inbound.User = &protocol.MemoryUser{ + Email: i.email, + Level: uint32(i.level), + } + ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{ + From: metadata.Source, + To: metadata.Destination, + Status: log.AccessAccepted, + Email: i.email, + }) + errors.LogInfo(ctx, "tunnelling request to tcp:", metadata.Destination) + dispatcher := session.DispatcherFromContext(ctx) + link, err := dispatcher.Dispatch(ctx, singbridge.ToDestination(metadata.Destination, net.Network_TCP)) + if err != nil { + return err + } + return singbridge.CopyConn(ctx, nil, link, conn) +} + +func (i *Inbound) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error { + inbound := session.InboundFromContext(ctx) + inbound.User = &protocol.MemoryUser{ + Email: i.email, + Level: uint32(i.level), + } + ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{ + From: metadata.Source, + To: metadata.Destination, + Status: log.AccessAccepted, + Email: i.email, + }) + errors.LogInfo(ctx, "tunnelling request to udp:", metadata.Destination) + dispatcher := session.DispatcherFromContext(ctx) + destination := singbridge.ToDestination(metadata.Destination, net.Network_UDP) + link, err := dispatcher.Dispatch(ctx, destination) + if err != nil { + return err + } + outConn := &singbridge.PacketConnWrapper{ + Reader: link.Reader, + Writer: link.Writer, + Dest: destination, + } + return bufio.CopyPacketConn(ctx, conn, outConn) +} + +func (i *Inbound) NewError(ctx context.Context, err error) { + if E.IsClosed(err) { + return + } + errors.LogWarning(ctx, err.Error()) +} + +type natPacketConn struct { + net.Conn +} + +func (c *natPacketConn) ReadPacket(buffer *B.Buffer) (addr M.Socksaddr, err error) { + _, err = buffer.ReadFrom(c) + return +} + +func (c *natPacketConn) WritePacket(buffer *B.Buffer, addr M.Socksaddr) error { + _, err := buffer.WriteTo(c) + return err +} diff --git a/subproject/Xray-core-main/proxy/shadowsocks_2022/inbound_multi.go b/subproject/Xray-core-main/proxy/shadowsocks_2022/inbound_multi.go new file mode 100644 index 00000000..4bfa086a --- /dev/null +++ b/subproject/Xray-core-main/proxy/shadowsocks_2022/inbound_multi.go @@ -0,0 +1,283 @@ +package shadowsocks_2022 + +import ( + "context" + "encoding/base64" + "strconv" + "strings" + "sync" + + "github.com/sagernet/sing-shadowsocks/shadowaead_2022" + C "github.com/sagernet/sing/common" + A "github.com/sagernet/sing/common/auth" + B "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/bufio" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/log" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/common/singbridge" + "github.com/xtls/xray-core/common/uuid" + "github.com/xtls/xray-core/features/routing" + "github.com/xtls/xray-core/transport/internet/stat" +) + +func init() { + common.Must(common.RegisterConfig((*MultiUserServerConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + return NewMultiServer(ctx, config.(*MultiUserServerConfig)) + })) +} + +type MultiUserInbound struct { + sync.Mutex + networks []net.Network + users []*protocol.MemoryUser + service *shadowaead_2022.MultiService[int] +} + +func NewMultiServer(ctx context.Context, config *MultiUserServerConfig) (*MultiUserInbound, error) { + networks := config.Network + if len(networks) == 0 { + networks = []net.Network{ + net.Network_TCP, + net.Network_UDP, + } + } + memUsers := []*protocol.MemoryUser{} + for i, user := range config.Users { + if user.Email == "" { + u := uuid.New() + user.Email = "unnamed-user-" + strconv.Itoa(i) + "-" + u.String() + } + u, err := user.ToMemoryUser() + if err != nil { + return nil, errors.New("failed to get shadowsocks user").Base(err).AtError() + } + memUsers = append(memUsers, u) + } + + inbound := &MultiUserInbound{ + networks: networks, + users: memUsers, + } + if config.Key == "" { + return nil, errors.New("missing key") + } + psk, err := base64.StdEncoding.DecodeString(config.Key) + if err != nil { + return nil, errors.New("parse config").Base(err) + } + service, err := shadowaead_2022.NewMultiService[int](config.Method, psk, 500, inbound, nil) + if err != nil { + return nil, errors.New("create service").Base(err) + } + err = service.UpdateUsersWithPasswords( + C.MapIndexed(memUsers, func(index int, it *protocol.MemoryUser) int { return index }), + C.Map(memUsers, func(it *protocol.MemoryUser) string { return it.Account.(*MemoryAccount).Key }), + ) + if err != nil { + return nil, errors.New("create service").Base(err) + } + + inbound.service = service + return inbound, nil +} + +// AddUser implements proxy.UserManager.AddUser(). +func (i *MultiUserInbound) AddUser(ctx context.Context, u *protocol.MemoryUser) error { + i.Lock() + defer i.Unlock() + + if u.Email != "" { + for idx := range i.users { + if i.users[idx].Email == u.Email { + return errors.New("User ", u.Email, " already exists.") + } + } + } + i.users = append(i.users, u) + + // sync to multi service + // Considering implements shadowsocks2022 in xray-core may have better performance. + i.service.UpdateUsersWithPasswords( + C.MapIndexed(i.users, func(index int, it *protocol.MemoryUser) int { return index }), + C.Map(i.users, func(it *protocol.MemoryUser) string { return it.Account.(*MemoryAccount).Key }), + ) + + return nil +} + +// RemoveUser implements proxy.UserManager.RemoveUser(). +func (i *MultiUserInbound) RemoveUser(ctx context.Context, email string) error { + if email == "" { + return errors.New("Email must not be empty.") + } + + i.Lock() + defer i.Unlock() + + idx := -1 + for ii, u := range i.users { + if strings.EqualFold(u.Email, email) { + idx = ii + break + } + } + + if idx == -1 { + return errors.New("User ", email, " not found.") + } + + ulen := len(i.users) + + i.users[idx] = i.users[ulen-1] + i.users[ulen-1] = nil + i.users = i.users[:ulen-1] + + // sync to multi service + // Considering implements shadowsocks2022 in xray-core may have better performance. + i.service.UpdateUsersWithPasswords( + C.MapIndexed(i.users, func(index int, it *protocol.MemoryUser) int { return index }), + C.Map(i.users, func(it *protocol.MemoryUser) string { return it.Account.(*MemoryAccount).Key }), + ) + + return nil +} + +// GetUser implements proxy.UserManager.GetUser(). +func (i *MultiUserInbound) GetUser(ctx context.Context, email string) *protocol.MemoryUser { + if email == "" { + return nil + } + + i.Lock() + defer i.Unlock() + + for _, u := range i.users { + if strings.EqualFold(u.Email, email) { + return u + } + } + return nil +} + +// GetUsers implements proxy.UserManager.GetUsers(). +func (i *MultiUserInbound) GetUsers(ctx context.Context) []*protocol.MemoryUser { + i.Lock() + defer i.Unlock() + dst := make([]*protocol.MemoryUser, len(i.users)) + copy(dst, i.users) + return dst +} + +// GetUsersCount implements proxy.UserManager.GetUsersCount(). +func (i *MultiUserInbound) GetUsersCount(context.Context) int64 { + i.Lock() + defer i.Unlock() + return int64(len(i.users)) +} + +func (i *MultiUserInbound) Network() []net.Network { + return i.networks +} + +func (i *MultiUserInbound) Process(ctx context.Context, network net.Network, connection stat.Connection, dispatcher routing.Dispatcher) error { + inbound := session.InboundFromContext(ctx) + inbound.Name = "shadowsocks-2022-multi" + inbound.CanSpliceCopy = 3 + + var metadata M.Metadata + if inbound.Source.IsValid() { + metadata.Source = M.ParseSocksaddr(inbound.Source.NetAddr()) + } + + ctx = session.ContextWithDispatcher(ctx, dispatcher) + + if network == net.Network_TCP { + return singbridge.ReturnError(i.service.NewConnection(ctx, connection, metadata)) + } else { + reader := buf.NewReader(connection) + pc := &natPacketConn{connection} + for { + mb, err := reader.ReadMultiBuffer() + if err != nil { + buf.ReleaseMulti(mb) + return singbridge.ReturnError(err) + } + for _, buffer := range mb { + packet := B.As(buffer.Bytes()).ToOwned() + buffer.Release() + err = i.service.NewPacket(ctx, pc, packet, metadata) + if err != nil { + packet.Release() + buf.ReleaseMulti(mb) + return err + } + } + } + } +} + +func (i *MultiUserInbound) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { + inbound := session.InboundFromContext(ctx) + userInt, _ := A.UserFromContext[int](ctx) + user := i.users[userInt] + inbound.User = user + ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{ + From: metadata.Source, + To: metadata.Destination, + Status: log.AccessAccepted, + Email: user.Email, + }) + errors.LogInfo(ctx, "tunnelling request to tcp:", metadata.Destination) + dispatcher := session.DispatcherFromContext(ctx) + destination := singbridge.ToDestination(metadata.Destination, net.Network_TCP) + if !destination.IsValid() { + return errors.New("invalid destination") + } + + link, err := dispatcher.Dispatch(ctx, destination) + if err != nil { + return err + } + return singbridge.CopyConn(ctx, conn, link, conn) +} + +func (i *MultiUserInbound) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error { + inbound := session.InboundFromContext(ctx) + userInt, _ := A.UserFromContext[int](ctx) + user := i.users[userInt] + inbound.User = user + ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{ + From: metadata.Source, + To: metadata.Destination, + Status: log.AccessAccepted, + Email: user.Email, + }) + errors.LogInfo(ctx, "tunnelling request to udp:", metadata.Destination) + dispatcher := session.DispatcherFromContext(ctx) + destination := singbridge.ToDestination(metadata.Destination, net.Network_UDP) + link, err := dispatcher.Dispatch(ctx, destination) + if err != nil { + return err + } + outConn := &singbridge.PacketConnWrapper{ + Reader: link.Reader, + Writer: link.Writer, + Dest: destination, + } + return bufio.CopyPacketConn(ctx, conn, outConn) +} + +func (i *MultiUserInbound) NewError(ctx context.Context, err error) { + if E.IsClosed(err) { + return + } + errors.LogWarning(ctx, err.Error()) +} diff --git a/subproject/Xray-core-main/proxy/shadowsocks_2022/inbound_relay.go b/subproject/Xray-core-main/proxy/shadowsocks_2022/inbound_relay.go new file mode 100644 index 00000000..19afd462 --- /dev/null +++ b/subproject/Xray-core-main/proxy/shadowsocks_2022/inbound_relay.go @@ -0,0 +1,182 @@ +package shadowsocks_2022 + +import ( + "context" + "strconv" + "strings" + + "github.com/sagernet/sing-shadowsocks/shadowaead_2022" + C "github.com/sagernet/sing/common" + A "github.com/sagernet/sing/common/auth" + B "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/bufio" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/log" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/common/singbridge" + "github.com/xtls/xray-core/common/uuid" + "github.com/xtls/xray-core/features/routing" + "github.com/xtls/xray-core/transport/internet/stat" +) + +func init() { + common.Must(common.RegisterConfig((*RelayServerConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + return NewRelayServer(ctx, config.(*RelayServerConfig)) + })) +} + +type RelayInbound struct { + networks []net.Network + destinations []*RelayDestination + service *shadowaead_2022.RelayService[int] +} + +func NewRelayServer(ctx context.Context, config *RelayServerConfig) (*RelayInbound, error) { + networks := config.Network + if len(networks) == 0 { + networks = []net.Network{ + net.Network_TCP, + net.Network_UDP, + } + } + inbound := &RelayInbound{ + networks: networks, + destinations: config.Destinations, + } + if !C.Contains(shadowaead_2022.List, config.Method) || !strings.Contains(config.Method, "aes") { + return nil, errors.New("unsupported method ", config.Method) + } + service, err := shadowaead_2022.NewRelayServiceWithPassword[int](config.Method, config.Key, 500, inbound) + if err != nil { + return nil, errors.New("create service").Base(err) + } + + for i, destination := range config.Destinations { + if destination.Email == "" { + u := uuid.New() + destination.Email = "unnamed-destination-" + strconv.Itoa(i) + "-" + u.String() + } + } + err = service.UpdateUsersWithPasswords( + C.MapIndexed(config.Destinations, func(index int, it *RelayDestination) int { return index }), + C.Map(config.Destinations, func(it *RelayDestination) string { return it.Key }), + C.Map(config.Destinations, func(it *RelayDestination) M.Socksaddr { + return singbridge.ToSocksaddr(net.Destination{ + Address: it.Address.AsAddress(), + Port: net.Port(it.Port), + }) + }), + ) + if err != nil { + return nil, errors.New("create service").Base(err) + } + inbound.service = service + return inbound, nil +} + +func (i *RelayInbound) Network() []net.Network { + return i.networks +} + +func (i *RelayInbound) Process(ctx context.Context, network net.Network, connection stat.Connection, dispatcher routing.Dispatcher) error { + inbound := session.InboundFromContext(ctx) + inbound.Name = "shadowsocks-2022-relay" + inbound.CanSpliceCopy = 3 + + var metadata M.Metadata + if inbound.Source.IsValid() { + metadata.Source = M.ParseSocksaddr(inbound.Source.NetAddr()) + } + + ctx = session.ContextWithDispatcher(ctx, dispatcher) + + if network == net.Network_TCP { + return singbridge.ReturnError(i.service.NewConnection(ctx, connection, metadata)) + } else { + reader := buf.NewReader(connection) + pc := &natPacketConn{connection} + for { + mb, err := reader.ReadMultiBuffer() + if err != nil { + buf.ReleaseMulti(mb) + return singbridge.ReturnError(err) + } + for _, buffer := range mb { + packet := B.As(buffer.Bytes()).ToOwned() + buffer.Release() + err = i.service.NewPacket(ctx, pc, packet, metadata) + if err != nil { + packet.Release() + buf.ReleaseMulti(mb) + return err + } + } + } + } +} + +func (i *RelayInbound) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { + inbound := session.InboundFromContext(ctx) + userInt, _ := A.UserFromContext[int](ctx) + user := i.destinations[userInt] + inbound.User = &protocol.MemoryUser{ + Email: user.Email, + Level: uint32(user.Level), + } + ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{ + From: metadata.Source, + To: metadata.Destination, + Status: log.AccessAccepted, + Email: user.Email, + }) + errors.LogInfo(ctx, "tunnelling request to tcp:", metadata.Destination) + dispatcher := session.DispatcherFromContext(ctx) + link, err := dispatcher.Dispatch(ctx, singbridge.ToDestination(metadata.Destination, net.Network_TCP)) + if err != nil { + return err + } + return singbridge.CopyConn(ctx, nil, link, conn) +} + +func (i *RelayInbound) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error { + inbound := session.InboundFromContext(ctx) + userInt, _ := A.UserFromContext[int](ctx) + user := i.destinations[userInt] + inbound.User = &protocol.MemoryUser{ + Email: user.Email, + Level: uint32(user.Level), + } + ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{ + From: metadata.Source, + To: metadata.Destination, + Status: log.AccessAccepted, + Email: user.Email, + }) + errors.LogInfo(ctx, "tunnelling request to udp:", metadata.Destination) + dispatcher := session.DispatcherFromContext(ctx) + destination := singbridge.ToDestination(metadata.Destination, net.Network_UDP) + link, err := dispatcher.Dispatch(ctx, destination) + if err != nil { + return err + } + outConn := &singbridge.PacketConnWrapper{ + Reader: link.Reader, + Writer: link.Writer, + Dest: destination, + } + return bufio.CopyPacketConn(ctx, conn, outConn) +} + +func (i *RelayInbound) NewError(ctx context.Context, err error) { + if E.IsClosed(err) { + return + } + errors.LogWarning(ctx, err.Error()) +} diff --git a/subproject/Xray-core-main/proxy/shadowsocks_2022/outbound.go b/subproject/Xray-core-main/proxy/shadowsocks_2022/outbound.go new file mode 100644 index 00000000..dd2e4259 --- /dev/null +++ b/subproject/Xray-core-main/proxy/shadowsocks_2022/outbound.go @@ -0,0 +1,159 @@ +package shadowsocks_2022 + +import ( + "context" + "time" + + shadowsocks "github.com/sagernet/sing-shadowsocks" + "github.com/sagernet/sing-shadowsocks/shadowaead_2022" + C "github.com/sagernet/sing/common" + B "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/bufio" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/uot" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/common/singbridge" + "github.com/xtls/xray-core/transport" + "github.com/xtls/xray-core/transport/internet" +) + +func init() { + common.Must(common.RegisterConfig((*ClientConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + return NewClient(ctx, config.(*ClientConfig)) + })) +} + +type Outbound struct { + ctx context.Context + server net.Destination + method shadowsocks.Method + uotClient *uot.Client +} + +func NewClient(ctx context.Context, config *ClientConfig) (*Outbound, error) { + o := &Outbound{ + ctx: ctx, + server: net.Destination{ + Address: config.Address.AsAddress(), + Port: net.Port(config.Port), + Network: net.Network_TCP, + }, + } + if C.Contains(shadowaead_2022.List, config.Method) { + if config.Key == "" { + return nil, errors.New("missing psk") + } + method, err := shadowaead_2022.NewWithPassword(config.Method, config.Key, nil) + if err != nil { + return nil, errors.New("create method").Base(err) + } + o.method = method + } else { + return nil, errors.New("unknown method ", config.Method) + } + if config.UdpOverTcp { + o.uotClient = &uot.Client{Version: uint8(config.UdpOverTcpVersion)} + } + return o, nil +} + +func (o *Outbound) Process(ctx context.Context, link *transport.Link, dialer internet.Dialer) error { + var inboundConn net.Conn + inbound := session.InboundFromContext(ctx) + if inbound != nil { + inboundConn = inbound.Conn + } + + outbounds := session.OutboundsFromContext(ctx) + ob := outbounds[len(outbounds)-1] + if !ob.Target.IsValid() { + return errors.New("target not specified") + } + ob.Name = "shadowsocks-2022" + ob.CanSpliceCopy = 3 + destination := ob.Target + network := destination.Network + + errors.LogInfo(ctx, "tunneling request to ", destination, " via ", o.server.NetAddr()) + + serverDestination := o.server + if o.uotClient != nil { + serverDestination.Network = net.Network_TCP + } else { + serverDestination.Network = network + } + connection, err := dialer.Dial(ctx, serverDestination) + if err != nil { + return errors.New("failed to connect to server").Base(err) + } + + if session.TimeoutOnlyFromContext(ctx) { + ctx, _ = context.WithCancel(context.Background()) + } + + if network == net.Network_TCP { + serverConn := o.method.DialEarlyConn(connection, singbridge.ToSocksaddr(destination)) + var handshake bool + if timeoutReader, isTimeoutReader := link.Reader.(buf.TimeoutReader); isTimeoutReader { + mb, err := timeoutReader.ReadMultiBufferTimeout(time.Millisecond * 100) + if err != nil && err != buf.ErrNotTimeoutReader && err != buf.ErrReadTimeout { + return errors.New("read payload").Base(err) + } + payload := B.New() + for { + payload.Reset() + nb, n := buf.SplitBytes(mb, payload.FreeBytes()) + if n > 0 { + payload.Truncate(n) + _, err = serverConn.Write(payload.Bytes()) + if err != nil { + payload.Release() + return errors.New("write payload").Base(err) + } + handshake = true + } + if nb.IsEmpty() { + break + } + mb = nb + } + payload.Release() + } + if !handshake { + _, err = serverConn.Write(nil) + if err != nil { + return errors.New("client handshake").Base(err) + } + } + return singbridge.CopyConn(ctx, inboundConn, link, serverConn) + } else { + var packetConn N.PacketConn + if pc, isPacketConn := inboundConn.(N.PacketConn); isPacketConn { + packetConn = pc + } else if nc, isNetPacket := inboundConn.(net.PacketConn); isNetPacket { + packetConn = bufio.NewPacketConn(nc) + } else { + packetConn = &singbridge.PacketConnWrapper{ + Reader: link.Reader, + Writer: link.Writer, + Conn: inboundConn, + Dest: destination, + } + } + + if o.uotClient != nil { + uConn, err := o.uotClient.DialEarlyConn(o.method.DialEarlyConn(connection, uot.RequestDestination(o.uotClient.Version)), false, singbridge.ToSocksaddr(destination)) + if err != nil { + return err + } + return singbridge.ReturnError(bufio.CopyPacketConn(ctx, packetConn, uConn)) + } else { + serverConn := o.method.DialPacketConn(connection) + return singbridge.ReturnError(bufio.CopyPacketConn(ctx, packetConn, serverConn)) + } + } +} diff --git a/subproject/Xray-core-main/proxy/shadowsocks_2022/shadowsocks_2022.go b/subproject/Xray-core-main/proxy/shadowsocks_2022/shadowsocks_2022.go new file mode 100644 index 00000000..96f62c74 --- /dev/null +++ b/subproject/Xray-core-main/proxy/shadowsocks_2022/shadowsocks_2022.go @@ -0,0 +1 @@ +package shadowsocks_2022 diff --git a/subproject/Xray-core-main/proxy/socks/client.go b/subproject/Xray-core-main/proxy/socks/client.go new file mode 100644 index 00000000..eac3f35b --- /dev/null +++ b/subproject/Xray-core-main/proxy/socks/client.go @@ -0,0 +1,181 @@ +package socks + +import ( + "context" + "time" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/retry" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/common/signal" + "github.com/xtls/xray-core/common/task" + "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/features/policy" + "github.com/xtls/xray-core/transport" + "github.com/xtls/xray-core/transport/internet" + "github.com/xtls/xray-core/transport/internet/stat" +) + +// Client is a Socks5 client. +type Client struct { + server *protocol.ServerSpec + policyManager policy.Manager +} + +// NewClient create a new Socks5 client based on the given config. +func NewClient(ctx context.Context, config *ClientConfig) (*Client, error) { + if config.Server == nil { + return nil, errors.New(`no target server found`) + } + server, err := protocol.NewServerSpecFromPB(config.Server) + if err != nil { + return nil, errors.New("failed to get server spec").Base(err) + } + + v := core.MustFromContext(ctx) + c := &Client{ + server: server, + policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager), + } + + return c, nil +} + +// Process implements proxy.Outbound.Process. +func (c *Client) Process(ctx context.Context, link *transport.Link, dialer internet.Dialer) error { + outbounds := session.OutboundsFromContext(ctx) + ob := outbounds[len(outbounds)-1] + if !ob.Target.IsValid() { + return errors.New("target not specified.") + } + ob.Name = "socks" + ob.CanSpliceCopy = 2 + // Destination of the inner request. + destination := ob.Target + + // Outbound server. + server := c.server + dest := server.Destination + // Connection to the outbound server. + var conn stat.Connection + + if err := retry.ExponentialBackoff(5, 100).On(func() error { + rawConn, err := dialer.Dial(ctx, dest) + if err != nil { + return err + } + conn = rawConn + + return nil + }); err != nil { + return errors.New("failed to find an available destination").Base(err) + } + + defer func() { + if err := conn.Close(); err != nil { + errors.LogInfoInner(ctx, err, "failed to closed connection") + } + }() + + p := c.policyManager.ForLevel(0) + + request := &protocol.RequestHeader{ + Version: socks5Version, + Command: protocol.RequestCommandTCP, + Address: destination.Address, + Port: destination.Port, + } + + if destination.Network == net.Network_UDP { + request.Command = protocol.RequestCommandUDP + } + + user := server.User + if user != nil { + request.User = user + p = c.policyManager.ForLevel(user.Level) + } + + if err := conn.SetDeadline(time.Now().Add(p.Timeouts.Handshake)); err != nil { + errors.LogInfoInner(ctx, err, "failed to set deadline for handshake") + } + udpRequest, err := ClientHandshake(request, conn, conn) + if err != nil { + return errors.New("failed to establish connection to server").AtWarning().Base(err) + } + if udpRequest != nil { + if udpRequest.Address == net.AnyIP || udpRequest.Address == net.AnyIPv6 { + udpRequest.Address = dest.Address + } + } + + if err := conn.SetDeadline(time.Time{}); err != nil { + errors.LogInfoInner(ctx, err, "failed to clear deadline after handshake") + } + + var newCtx context.Context + var newCancel context.CancelFunc + if session.TimeoutOnlyFromContext(ctx) { + newCtx, newCancel = context.WithCancel(context.Background()) + } + + ctx, cancel := context.WithCancel(ctx) + timer := signal.CancelAfterInactivity(ctx, func() { + cancel() + if newCancel != nil { + newCancel() + } + }, p.Timeouts.ConnectionIdle) + + var requestFunc func() error + var responseFunc func() error + if request.Command == protocol.RequestCommandTCP { + requestFunc = func() error { + defer timer.SetTimeout(p.Timeouts.DownlinkOnly) + return buf.Copy(link.Reader, buf.NewWriter(conn), buf.UpdateActivity(timer)) + } + responseFunc = func() error { + ob.CanSpliceCopy = 1 + defer timer.SetTimeout(p.Timeouts.UplinkOnly) + return buf.Copy(buf.NewReader(conn), link.Writer, buf.UpdateActivity(timer)) + } + } else if request.Command == protocol.RequestCommandUDP { + udpConn, err := dialer.Dial(ctx, udpRequest.Destination()) + if err != nil { + return errors.New("failed to create UDP connection").Base(err) + } + defer udpConn.Close() + requestFunc = func() error { + defer timer.SetTimeout(p.Timeouts.DownlinkOnly) + writer := &UDPWriter{Writer: udpConn, Request: request} + return buf.Copy(link.Reader, writer, buf.UpdateActivity(timer)) + } + responseFunc = func() error { + ob.CanSpliceCopy = 1 + defer timer.SetTimeout(p.Timeouts.UplinkOnly) + reader := &UDPReader{Reader: udpConn} + return buf.Copy(reader, link.Writer, buf.UpdateActivity(timer)) + } + } + + if newCtx != nil { + ctx = newCtx + } + + responseDonePost := task.OnSuccess(responseFunc, task.Close(link.Writer)) + if err := task.Run(ctx, requestFunc, responseDonePost); err != nil { + return errors.New("connection ends").Base(err) + } + + return nil +} + +func init() { + common.Must(common.RegisterConfig((*ClientConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + return NewClient(ctx, config.(*ClientConfig)) + })) +} diff --git a/subproject/Xray-core-main/proxy/socks/config.go b/subproject/Xray-core-main/proxy/socks/config.go new file mode 100644 index 00000000..e8aa328a --- /dev/null +++ b/subproject/Xray-core-main/proxy/socks/config.go @@ -0,0 +1,33 @@ +package socks + +import ( + "google.golang.org/protobuf/proto" + + "github.com/xtls/xray-core/common/protocol" +) + +func (a *Account) Equals(another protocol.Account) bool { + if account, ok := another.(*Account); ok { + return a.Username == account.Username + } + return false +} + +func (a *Account) ToProto() proto.Message { + return a +} + +func (a *Account) AsAccount() (protocol.Account, error) { + return a, nil +} + +func (c *ServerConfig) HasAccount(username, password string) bool { + if c.Accounts == nil { + return false + } + storedPassed, found := c.Accounts[username] + if !found { + return false + } + return storedPassed == password +} diff --git a/subproject/Xray-core-main/proxy/socks/config.pb.go b/subproject/Xray-core-main/proxy/socks/config.pb.go new file mode 100644 index 00000000..f9a90e80 --- /dev/null +++ b/subproject/Xray-core-main/proxy/socks/config.pb.go @@ -0,0 +1,335 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: proxy/socks/config.proto + +package socks + +import ( + net "github.com/xtls/xray-core/common/net" + protocol "github.com/xtls/xray-core/common/protocol" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// AuthType is the authentication type of Socks proxy. +type AuthType int32 + +const ( + // NO_AUTH is for anonymous authentication. + AuthType_NO_AUTH AuthType = 0 + // PASSWORD is for username/password authentication. + AuthType_PASSWORD AuthType = 1 +) + +// Enum value maps for AuthType. +var ( + AuthType_name = map[int32]string{ + 0: "NO_AUTH", + 1: "PASSWORD", + } + AuthType_value = map[string]int32{ + "NO_AUTH": 0, + "PASSWORD": 1, + } +) + +func (x AuthType) Enum() *AuthType { + p := new(AuthType) + *p = x + return p +} + +func (x AuthType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (AuthType) Descriptor() protoreflect.EnumDescriptor { + return file_proxy_socks_config_proto_enumTypes[0].Descriptor() +} + +func (AuthType) Type() protoreflect.EnumType { + return &file_proxy_socks_config_proto_enumTypes[0] +} + +func (x AuthType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use AuthType.Descriptor instead. +func (AuthType) EnumDescriptor() ([]byte, []int) { + return file_proxy_socks_config_proto_rawDescGZIP(), []int{0} +} + +// Account represents a Socks account. +type Account struct { + state protoimpl.MessageState `protogen:"open.v1"` + Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"` + Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Account) Reset() { + *x = Account{} + mi := &file_proxy_socks_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Account) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Account) ProtoMessage() {} + +func (x *Account) ProtoReflect() protoreflect.Message { + mi := &file_proxy_socks_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Account.ProtoReflect.Descriptor instead. +func (*Account) Descriptor() ([]byte, []int) { + return file_proxy_socks_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Account) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *Account) GetPassword() string { + if x != nil { + return x.Password + } + return "" +} + +// ServerConfig is the protobuf config for Socks server. +type ServerConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + AuthType AuthType `protobuf:"varint,1,opt,name=auth_type,json=authType,proto3,enum=xray.proxy.socks.AuthType" json:"auth_type,omitempty"` + Accounts map[string]string `protobuf:"bytes,2,rep,name=accounts,proto3" json:"accounts,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + Address *net.IPOrDomain `protobuf:"bytes,3,opt,name=address,proto3" json:"address,omitempty"` + UdpEnabled bool `protobuf:"varint,4,opt,name=udp_enabled,json=udpEnabled,proto3" json:"udp_enabled,omitempty"` + UserLevel uint32 `protobuf:"varint,6,opt,name=user_level,json=userLevel,proto3" json:"user_level,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ServerConfig) Reset() { + *x = ServerConfig{} + mi := &file_proxy_socks_config_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ServerConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ServerConfig) ProtoMessage() {} + +func (x *ServerConfig) ProtoReflect() protoreflect.Message { + mi := &file_proxy_socks_config_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ServerConfig.ProtoReflect.Descriptor instead. +func (*ServerConfig) Descriptor() ([]byte, []int) { + return file_proxy_socks_config_proto_rawDescGZIP(), []int{1} +} + +func (x *ServerConfig) GetAuthType() AuthType { + if x != nil { + return x.AuthType + } + return AuthType_NO_AUTH +} + +func (x *ServerConfig) GetAccounts() map[string]string { + if x != nil { + return x.Accounts + } + return nil +} + +func (x *ServerConfig) GetAddress() *net.IPOrDomain { + if x != nil { + return x.Address + } + return nil +} + +func (x *ServerConfig) GetUdpEnabled() bool { + if x != nil { + return x.UdpEnabled + } + return false +} + +func (x *ServerConfig) GetUserLevel() uint32 { + if x != nil { + return x.UserLevel + } + return 0 +} + +// ClientConfig is the protobuf config for Socks client. +type ClientConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Sever is a list of Socks server addresses. + Server *protocol.ServerEndpoint `protobuf:"bytes,1,opt,name=server,proto3" json:"server,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ClientConfig) Reset() { + *x = ClientConfig{} + mi := &file_proxy_socks_config_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ClientConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClientConfig) ProtoMessage() {} + +func (x *ClientConfig) ProtoReflect() protoreflect.Message { + mi := &file_proxy_socks_config_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ClientConfig.ProtoReflect.Descriptor instead. +func (*ClientConfig) Descriptor() ([]byte, []int) { + return file_proxy_socks_config_proto_rawDescGZIP(), []int{2} +} + +func (x *ClientConfig) GetServer() *protocol.ServerEndpoint { + if x != nil { + return x.Server + } + return nil +} + +var File_proxy_socks_config_proto protoreflect.FileDescriptor + +const file_proxy_socks_config_proto_rawDesc = "" + + "\n" + + "\x18proxy/socks/config.proto\x12\x10xray.proxy.socks\x1a\x18common/net/address.proto\x1a!common/protocol/server_spec.proto\"A\n" + + "\aAccount\x12\x1a\n" + + "\busername\x18\x01 \x01(\tR\busername\x12\x1a\n" + + "\bpassword\x18\x02 \x01(\tR\bpassword\"\xc5\x02\n" + + "\fServerConfig\x127\n" + + "\tauth_type\x18\x01 \x01(\x0e2\x1a.xray.proxy.socks.AuthTypeR\bauthType\x12H\n" + + "\baccounts\x18\x02 \x03(\v2,.xray.proxy.socks.ServerConfig.AccountsEntryR\baccounts\x125\n" + + "\aaddress\x18\x03 \x01(\v2\x1b.xray.common.net.IPOrDomainR\aaddress\x12\x1f\n" + + "\vudp_enabled\x18\x04 \x01(\bR\n" + + "udpEnabled\x12\x1d\n" + + "\n" + + "user_level\x18\x06 \x01(\rR\tuserLevel\x1a;\n" + + "\rAccountsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"L\n" + + "\fClientConfig\x12<\n" + + "\x06server\x18\x01 \x01(\v2$.xray.common.protocol.ServerEndpointR\x06server*%\n" + + "\bAuthType\x12\v\n" + + "\aNO_AUTH\x10\x00\x12\f\n" + + "\bPASSWORD\x10\x01BR\n" + + "\x14com.xray.proxy.socksP\x01Z%github.com/xtls/xray-core/proxy/socks\xaa\x02\x10Xray.Proxy.Socksb\x06proto3" + +var ( + file_proxy_socks_config_proto_rawDescOnce sync.Once + file_proxy_socks_config_proto_rawDescData []byte +) + +func file_proxy_socks_config_proto_rawDescGZIP() []byte { + file_proxy_socks_config_proto_rawDescOnce.Do(func() { + file_proxy_socks_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proxy_socks_config_proto_rawDesc), len(file_proxy_socks_config_proto_rawDesc))) + }) + return file_proxy_socks_config_proto_rawDescData +} + +var file_proxy_socks_config_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_proxy_socks_config_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_proxy_socks_config_proto_goTypes = []any{ + (AuthType)(0), // 0: xray.proxy.socks.AuthType + (*Account)(nil), // 1: xray.proxy.socks.Account + (*ServerConfig)(nil), // 2: xray.proxy.socks.ServerConfig + (*ClientConfig)(nil), // 3: xray.proxy.socks.ClientConfig + nil, // 4: xray.proxy.socks.ServerConfig.AccountsEntry + (*net.IPOrDomain)(nil), // 5: xray.common.net.IPOrDomain + (*protocol.ServerEndpoint)(nil), // 6: xray.common.protocol.ServerEndpoint +} +var file_proxy_socks_config_proto_depIdxs = []int32{ + 0, // 0: xray.proxy.socks.ServerConfig.auth_type:type_name -> xray.proxy.socks.AuthType + 4, // 1: xray.proxy.socks.ServerConfig.accounts:type_name -> xray.proxy.socks.ServerConfig.AccountsEntry + 5, // 2: xray.proxy.socks.ServerConfig.address:type_name -> xray.common.net.IPOrDomain + 6, // 3: xray.proxy.socks.ClientConfig.server:type_name -> xray.common.protocol.ServerEndpoint + 4, // [4:4] is the sub-list for method output_type + 4, // [4:4] is the sub-list for method input_type + 4, // [4:4] is the sub-list for extension type_name + 4, // [4:4] is the sub-list for extension extendee + 0, // [0:4] is the sub-list for field type_name +} + +func init() { file_proxy_socks_config_proto_init() } +func file_proxy_socks_config_proto_init() { + if File_proxy_socks_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_proxy_socks_config_proto_rawDesc), len(file_proxy_socks_config_proto_rawDesc)), + NumEnums: 1, + NumMessages: 4, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_proxy_socks_config_proto_goTypes, + DependencyIndexes: file_proxy_socks_config_proto_depIdxs, + EnumInfos: file_proxy_socks_config_proto_enumTypes, + MessageInfos: file_proxy_socks_config_proto_msgTypes, + }.Build() + File_proxy_socks_config_proto = out.File + file_proxy_socks_config_proto_goTypes = nil + file_proxy_socks_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/proxy/socks/config.proto b/subproject/Xray-core-main/proxy/socks/config.proto new file mode 100644 index 00000000..b53d565d --- /dev/null +++ b/subproject/Xray-core-main/proxy/socks/config.proto @@ -0,0 +1,39 @@ +syntax = "proto3"; + +package xray.proxy.socks; +option csharp_namespace = "Xray.Proxy.Socks"; +option go_package = "github.com/xtls/xray-core/proxy/socks"; +option java_package = "com.xray.proxy.socks"; +option java_multiple_files = true; + +import "common/net/address.proto"; +import "common/protocol/server_spec.proto"; + +// Account represents a Socks account. +message Account { + string username = 1; + string password = 2; +} + +// AuthType is the authentication type of Socks proxy. +enum AuthType { + // NO_AUTH is for anonymous authentication. + NO_AUTH = 0; + // PASSWORD is for username/password authentication. + PASSWORD = 1; +} + +// ServerConfig is the protobuf config for Socks server. +message ServerConfig { + AuthType auth_type = 1; + map accounts = 2; + xray.common.net.IPOrDomain address = 3; + bool udp_enabled = 4; + uint32 user_level = 6; +} + +// ClientConfig is the protobuf config for Socks client. +message ClientConfig { + // Sever is a list of Socks server addresses. + xray.common.protocol.ServerEndpoint server = 1; +} diff --git a/subproject/Xray-core-main/proxy/socks/protocol.go b/subproject/Xray-core-main/proxy/socks/protocol.go new file mode 100644 index 00000000..76ceb47c --- /dev/null +++ b/subproject/Xray-core-main/proxy/socks/protocol.go @@ -0,0 +1,515 @@ +package socks + +import ( + "encoding/binary" + "io" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" +) + +const ( + socks5Version = 0x05 + socks4Version = 0x04 + + cmdTCPConnect = 0x01 + cmdTCPBind = 0x02 + cmdUDPAssociate = 0x03 + cmdTorResolve = 0xF0 + cmdTorResolvePTR = 0xF1 + + socks4RequestGranted = 90 + socks4RequestRejected = 91 + + authNotRequired = 0x00 + // authGssAPI = 0x01 + authPassword = 0x02 + authNoMatchingMethod = 0xFF + + statusSuccess = 0x00 + statusCmdNotSupport = 0x07 +) + +var addrParser = protocol.NewAddressParser( + protocol.AddressFamilyByte(0x01, net.AddressFamilyIPv4), + protocol.AddressFamilyByte(0x04, net.AddressFamilyIPv6), + protocol.AddressFamilyByte(0x03, net.AddressFamilyDomain), +) + +type ServerSession struct { + config *ServerConfig + address net.Address + port net.Port + localAddress net.Address +} + +func (s *ServerSession) handshake4(cmd byte, reader io.Reader, writer io.Writer) (*protocol.RequestHeader, error) { + if s.config.AuthType == AuthType_PASSWORD { + writeSocks4Response(writer, socks4RequestRejected, net.AnyIP, net.Port(0)) + return nil, errors.New("socks 4 is not allowed when auth is required.") + } + + var port net.Port + var address net.Address + + { + buffer := buf.StackNew() + if _, err := buffer.ReadFullFrom(reader, 6); err != nil { + buffer.Release() + return nil, errors.New("insufficient header").Base(err) + } + port = net.PortFromBytes(buffer.BytesRange(0, 2)) + address = net.IPAddress(buffer.BytesRange(2, 6)) + buffer.Release() + } + + if _, err := ReadUntilNull(reader); /* user id */ err != nil { + return nil, err + } + if address.IP()[0] == 0x00 { + domain, err := ReadUntilNull(reader) + if err != nil { + return nil, errors.New("failed to read domain for socks 4a").Base(err) + } + address = net.ParseAddress(domain) + } + + switch cmd { + case cmdTCPConnect: + request := &protocol.RequestHeader{ + Command: protocol.RequestCommandTCP, + Address: address, + Port: port, + Version: socks4Version, + } + if err := writeSocks4Response(writer, socks4RequestGranted, net.AnyIP, net.Port(0)); err != nil { + return nil, err + } + return request, nil + default: + writeSocks4Response(writer, socks4RequestRejected, net.AnyIP, net.Port(0)) + return nil, errors.New("unsupported command: ", cmd) + } +} + +func (s *ServerSession) auth5(nMethod byte, reader io.Reader, writer io.Writer) (username string, err error) { + buffer := buf.StackNew() + defer buffer.Release() + + if _, err = buffer.ReadFullFrom(reader, int32(nMethod)); err != nil { + return "", errors.New("failed to read auth methods").Base(err) + } + + var expectedAuth byte = authNotRequired + if s.config.AuthType == AuthType_PASSWORD { + expectedAuth = authPassword + } + + if !hasAuthMethod(expectedAuth, buffer.BytesRange(0, int32(nMethod))) { + writeSocks5AuthenticationResponse(writer, socks5Version, authNoMatchingMethod) + return "", errors.New("no matching auth method") + } + + if err := writeSocks5AuthenticationResponse(writer, socks5Version, expectedAuth); err != nil { + return "", errors.New("failed to write auth response").Base(err) + } + + if expectedAuth == authPassword { + username, password, err := ReadUsernamePassword(reader) + if err != nil { + return "", errors.New("failed to read username and password for authentication").Base(err) + } + + if !s.config.HasAccount(username, password) { + writeSocks5AuthenticationResponse(writer, 0x01, 0xFF) + return "", errors.New("invalid username or password") + } + + if err := writeSocks5AuthenticationResponse(writer, 0x01, 0x00); err != nil { + return "", errors.New("failed to write auth response").Base(err) + } + return username, nil + } + + return "", nil +} + +func (s *ServerSession) handshake5(nMethod byte, reader io.Reader, writer io.Writer) (*protocol.RequestHeader, error) { + var ( + username string + err error + ) + if username, err = s.auth5(nMethod, reader, writer); err != nil { + return nil, err + } + + var cmd byte + { + buffer := buf.StackNew() + if _, err := buffer.ReadFullFrom(reader, 3); err != nil { + buffer.Release() + return nil, errors.New("failed to read request").Base(err) + } + cmd = buffer.Byte(1) + buffer.Release() + } + + request := new(protocol.RequestHeader) + if username != "" { + request.User = &protocol.MemoryUser{Email: username} + } + switch cmd { + case cmdTCPConnect, cmdTorResolve, cmdTorResolvePTR: + // We don't have a solution for Tor case now. Simply treat it as connect command. + request.Command = protocol.RequestCommandTCP + case cmdUDPAssociate: + if !s.config.UdpEnabled { + writeSocks5Response(writer, statusCmdNotSupport, net.AnyIP, net.Port(0)) + return nil, errors.New("UDP is not enabled.") + } + request.Command = protocol.RequestCommandUDP + case cmdTCPBind: + writeSocks5Response(writer, statusCmdNotSupport, net.AnyIP, net.Port(0)) + return nil, errors.New("TCP bind is not supported.") + default: + writeSocks5Response(writer, statusCmdNotSupport, net.AnyIP, net.Port(0)) + return nil, errors.New("unknown command ", cmd) + } + + request.Version = socks5Version + + addr, port, err := addrParser.ReadAddressPort(nil, reader) + if err != nil { + return nil, errors.New("failed to read address").Base(err) + } + request.Address = addr + request.Port = port + + responseAddress := s.address + responsePort := s.port + //nolint:gocritic // Use if else chain for clarity + if request.Command == protocol.RequestCommandUDP { + if s.config.Address != nil { + // Use configured IP as remote address in the response to UDP Associate + responseAddress = s.config.Address.AsAddress() + } else { + // Use conn.LocalAddr() IP as remote address in the response by default + responseAddress = s.localAddress + } + } + if err := writeSocks5Response(writer, statusSuccess, responseAddress, responsePort); err != nil { + return nil, err + } + + return request, nil +} + +// Handshake performs a Socks4/4a/5 handshake. +func (s *ServerSession) Handshake(reader io.Reader, writer io.Writer) (*protocol.RequestHeader, error) { + buffer := buf.StackNew() + if _, err := buffer.ReadFullFrom(reader, 2); err != nil { + buffer.Release() + return nil, errors.New("insufficient header").Base(err) + } + + version := buffer.Byte(0) + cmd := buffer.Byte(1) + buffer.Release() + + switch version { + case socks4Version: + return s.handshake4(cmd, reader, writer) + case socks5Version: + return s.handshake5(cmd, reader, writer) + default: + return nil, errors.New("unknown Socks version: ", version) + } +} + +// ReadUsernamePassword reads Socks 5 username/password message from the given reader. +// +----+------+----------+------+----------+ +// |VER | ULEN | UNAME | PLEN | PASSWD | +// +----+------+----------+------+----------+ +// | 1 | 1 | 1 to 255 | 1 | 1 to 255 | +// +----+------+----------+------+----------+ +func ReadUsernamePassword(reader io.Reader) (string, string, error) { + buffer := buf.StackNew() + defer buffer.Release() + + if _, err := buffer.ReadFullFrom(reader, 2); err != nil { + return "", "", err + } + nUsername := int32(buffer.Byte(1)) + + buffer.Clear() + if _, err := buffer.ReadFullFrom(reader, nUsername); err != nil { + return "", "", err + } + username := buffer.String() + + buffer.Clear() + if _, err := buffer.ReadFullFrom(reader, 1); err != nil { + return "", "", err + } + nPassword := int32(buffer.Byte(0)) + + buffer.Clear() + if _, err := buffer.ReadFullFrom(reader, nPassword); err != nil { + return "", "", err + } + password := buffer.String() + return username, password, nil +} + +// ReadUntilNull reads content from given reader, until a null (0x00) byte. +func ReadUntilNull(reader io.Reader) (string, error) { + b := buf.StackNew() + defer b.Release() + + for { + _, err := b.ReadFullFrom(reader, 1) + if err != nil { + return "", err + } + if b.Byte(b.Len()-1) == 0x00 { + b.Resize(0, b.Len()-1) + return b.String(), nil + } + if b.IsFull() { + return "", errors.New("buffer overrun") + } + } +} + +func hasAuthMethod(expectedAuth byte, authCandidates []byte) bool { + for _, a := range authCandidates { + if a == expectedAuth { + return true + } + } + return false +} + +func writeSocks5AuthenticationResponse(writer io.Writer, version byte, auth byte) error { + return buf.WriteAllBytes(writer, []byte{version, auth}, nil) +} + +func writeSocks5Response(writer io.Writer, errCode byte, address net.Address, port net.Port) error { + buffer := buf.New() + defer buffer.Release() + + common.Must2(buffer.Write([]byte{socks5Version, errCode, 0x00 /* reserved */})) + if err := addrParser.WriteAddressPort(buffer, address, port); err != nil { + return err + } + + return buf.WriteAllBytes(writer, buffer.Bytes(), nil) +} + +func writeSocks4Response(writer io.Writer, errCode byte, address net.Address, port net.Port) error { + buffer := buf.StackNew() + defer buffer.Release() + + common.Must(buffer.WriteByte(0x00)) + common.Must(buffer.WriteByte(errCode)) + portBytes := buffer.Extend(2) + binary.BigEndian.PutUint16(portBytes, port.Value()) + common.Must2(buffer.Write(address.IP())) + return buf.WriteAllBytes(writer, buffer.Bytes(), nil) +} + +func DecodeUDPPacket(packet *buf.Buffer) (*protocol.RequestHeader, error) { + if packet.Len() < 5 { + return nil, errors.New("insufficient length of packet.") + } + request := &protocol.RequestHeader{ + Version: socks5Version, + Command: protocol.RequestCommandUDP, + } + + // packet[0] and packet[1] are reserved + if packet.Byte(2) != 0 /* fragments */ { + return nil, errors.New("discarding fragmented payload.") + } + + packet.Advance(3) + + addr, port, err := addrParser.ReadAddressPort(nil, packet) + if err != nil { + return nil, errors.New("failed to read UDP header").Base(err) + } + request.Address = addr + request.Port = port + return request, nil +} + +func EncodeUDPPacket(request *protocol.RequestHeader, data []byte) (*buf.Buffer, error) { + b := buf.New() + common.Must2(b.Write([]byte{0, 0, 0 /* Fragment */})) + if err := addrParser.WriteAddressPort(b, request.Address, request.Port); err != nil { + b.Release() + return nil, err + } + // if data is too large, return an empty buffer (drop too big data) + if b.Available() < int32(len(data)) { + b.Clear() + return b, nil + } + common.Must2(b.Write(data)) + return b, nil +} + +type UDPReader struct { + Reader io.Reader +} + +func (r *UDPReader) ReadMultiBuffer() (buf.MultiBuffer, error) { + buffer := buf.New() + _, err := buffer.ReadFrom(r.Reader) + if err != nil { + buffer.Release() + return nil, err + } + u, err := DecodeUDPPacket(buffer) + if err != nil { + buffer.Release() + return nil, err + } + dest := u.Destination() + buffer.UDP = &dest + return buf.MultiBuffer{buffer}, nil +} + +type UDPWriter struct { + Writer io.Writer + Request *protocol.RequestHeader +} + +func (w *UDPWriter) WriteMultiBuffer(mb buf.MultiBuffer) error { + for { + mb2, b := buf.SplitFirst(mb) + mb = mb2 + if b == nil { + break + } + request := w.Request + if b.UDP != nil { + request = &protocol.RequestHeader{ + Address: b.UDP.Address, + Port: b.UDP.Port, + } + } + packet, err := EncodeUDPPacket(request, b.Bytes()) + b.Release() + if err != nil { + buf.ReleaseMulti(mb) + return err + } + _, err = w.Writer.Write(packet.Bytes()) + packet.Release() + if err != nil { + buf.ReleaseMulti(mb) + return err + } + } + return nil +} + +func ClientHandshake(request *protocol.RequestHeader, reader io.Reader, writer io.Writer) (*protocol.RequestHeader, error) { + authByte := byte(authNotRequired) + if request.User != nil { + authByte = byte(authPassword) + } + + b := buf.New() + defer b.Release() + + common.Must2(b.Write([]byte{socks5Version, 0x01, authByte})) + if err := buf.WriteAllBytes(writer, b.Bytes(), nil); err != nil { + return nil, err + } + + b.Clear() + if _, err := b.ReadFullFrom(reader, 2); err != nil { + return nil, err + } + + if b.Byte(0) != socks5Version { + return nil, errors.New("unexpected server version: ", b.Byte(0)).AtWarning() + } + if b.Byte(1) != authByte { + return nil, errors.New("auth method not supported.").AtWarning() + } + + if authByte == authPassword { + b.Clear() + account := request.User.Account.(*Account) + common.Must(b.WriteByte(0x01)) + common.Must(b.WriteByte(byte(len(account.Username)))) + common.Must2(b.WriteString(account.Username)) + common.Must(b.WriteByte(byte(len(account.Password)))) + common.Must2(b.WriteString(account.Password)) + if err := buf.WriteAllBytes(writer, b.Bytes(), nil); err != nil { + return nil, err + } + + b.Clear() + if _, err := b.ReadFullFrom(reader, 2); err != nil { + return nil, err + } + if b.Byte(1) != 0x00 { + return nil, errors.New("server rejects account: ", b.Byte(1)) + } + } + + b.Clear() + + command := byte(cmdTCPConnect) + if request.Command == protocol.RequestCommandUDP { + command = byte(cmdUDPAssociate) + } + common.Must2(b.Write([]byte{socks5Version, command, 0x00 /* reserved */})) + if request.Command == protocol.RequestCommandUDP { + common.Must2(b.Write([]byte{1, 0, 0, 0, 0, 0, 0 /* RFC 1928 */})) + } else { + if err := addrParser.WriteAddressPort(b, request.Address, request.Port); err != nil { + return nil, err + } + } + + if err := buf.WriteAllBytes(writer, b.Bytes(), nil); err != nil { + return nil, err + } + + b.Clear() + if _, err := b.ReadFullFrom(reader, 3); err != nil { + return nil, err + } + + resp := b.Byte(1) + if resp != 0x00 { + return nil, errors.New("server rejects request: ", resp) + } + + b.Clear() + + address, port, err := addrParser.ReadAddressPort(b, reader) + if err != nil { + return nil, err + } + + if request.Command == protocol.RequestCommandUDP { + udpRequest := &protocol.RequestHeader{ + Version: socks5Version, + Command: protocol.RequestCommandUDP, + Address: address, + Port: port, + } + return udpRequest, nil + } + + return nil, nil +} diff --git a/subproject/Xray-core-main/proxy/socks/protocol_test.go b/subproject/Xray-core-main/proxy/socks/protocol_test.go new file mode 100644 index 00000000..b8c84f68 --- /dev/null +++ b/subproject/Xray-core-main/proxy/socks/protocol_test.go @@ -0,0 +1,123 @@ +package socks_test + +import ( + "bytes" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + . "github.com/xtls/xray-core/proxy/socks" +) + +func TestUDPEncoding(t *testing.T) { + b := buf.New() + + request := &protocol.RequestHeader{ + Address: net.IPAddress([]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6}), + Port: 1024, + } + writer := &UDPWriter{Writer: b, Request: request} + + content := []byte{'a'} + payload := buf.New() + payload.Write(content) + common.Must(writer.WriteMultiBuffer(buf.MultiBuffer{payload})) + + reader := &UDPReader{Reader: b} + + decodedPayload, err := reader.ReadMultiBuffer() + common.Must(err) + if r := cmp.Diff(decodedPayload[0].Bytes(), content); r != "" { + t.Error(r) + } +} + +func TestReadUsernamePassword(t *testing.T) { + testCases := []struct { + Input []byte + Username string + Password string + Error bool + }{ + { + Input: []byte{0x05, 0x01, 'a', 0x02, 'b', 'c'}, + Username: "a", + Password: "bc", + }, + { + Input: []byte{0x05, 0x18, 'a', 0x02, 'b', 'c'}, + Error: true, + }, + } + + for _, testCase := range testCases { + reader := bytes.NewReader(testCase.Input) + username, password, err := ReadUsernamePassword(reader) + if testCase.Error { + if err == nil { + t.Error("for input: ", testCase.Input, " expect error, but actually nil") + } + } else { + if err != nil { + t.Error("for input: ", testCase.Input, " expect no error, but actually ", err.Error()) + } + if testCase.Username != username { + t.Error("for input: ", testCase.Input, " expect username ", testCase.Username, " but actually ", username) + } + if testCase.Password != password { + t.Error("for input: ", testCase.Input, " expect password ", testCase.Password, " but actually ", password) + } + } + } +} + +func TestReadUntilNull(t *testing.T) { + testCases := []struct { + Input []byte + Output string + Error bool + }{ + { + Input: []byte{'a', 'b', 0x00}, + Output: "ab", + }, + { + Input: []byte{'a'}, + Error: true, + }, + } + + for _, testCase := range testCases { + reader := bytes.NewReader(testCase.Input) + value, err := ReadUntilNull(reader) + if testCase.Error { + if err == nil { + t.Error("for input: ", testCase.Input, " expect error, but actually nil") + } + } else { + if err != nil { + t.Error("for input: ", testCase.Input, " expect no error, but actually ", err.Error()) + } + if testCase.Output != value { + t.Error("for input: ", testCase.Input, " expect output ", testCase.Output, " but actually ", value) + } + } + } +} + +func BenchmarkReadUsernamePassword(b *testing.B) { + input := []byte{0x05, 0x01, 'a', 0x02, 'b', 'c'} + buffer := buf.New() + buffer.Write(input) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _, err := ReadUsernamePassword(buffer) + common.Must(err) + buffer.Clear() + buffer.Extend(int32(len(input))) + } +} diff --git a/subproject/Xray-core-main/proxy/socks/server.go b/subproject/Xray-core-main/proxy/socks/server.go new file mode 100644 index 00000000..478410f3 --- /dev/null +++ b/subproject/Xray-core-main/proxy/socks/server.go @@ -0,0 +1,280 @@ +package socks + +import ( + "context" + goerrors "errors" + "io" + "time" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/log" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + udp_proto "github.com/xtls/xray-core/common/protocol/udp" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/features/policy" + "github.com/xtls/xray-core/features/routing" + "github.com/xtls/xray-core/proxy" + "github.com/xtls/xray-core/proxy/http" + "github.com/xtls/xray-core/transport" + "github.com/xtls/xray-core/transport/internet/stat" + "github.com/xtls/xray-core/transport/internet/udp" +) + +// Server is a SOCKS 5 proxy server +type Server struct { + config *ServerConfig + policyManager policy.Manager + cone bool + udpFilter *UDPFilter + httpServer *http.Server +} + +// NewServer creates a new Server object. +func NewServer(ctx context.Context, config *ServerConfig) (*Server, error) { + v := core.MustFromContext(ctx) + s := &Server{ + config: config, + policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager), + cone: ctx.Value("cone").(bool), + } + httpConfig := &http.ServerConfig{ + UserLevel: config.UserLevel, + } + if config.AuthType == AuthType_PASSWORD { + httpConfig.Accounts = config.Accounts + s.udpFilter = new(UDPFilter) // We only use this when auth is enabled + } + s.httpServer, _ = http.NewServer(ctx, httpConfig) + return s, nil +} + +func (s *Server) policy() policy.Session { + config := s.config + p := s.policyManager.ForLevel(config.UserLevel) + return p +} + +// Network implements proxy.Inbound. +func (s *Server) Network() []net.Network { + list := []net.Network{net.Network_TCP} + if s.config.UdpEnabled { + list = append(list, net.Network_UDP) + } + return list +} + +// Process implements proxy.Inbound. +func (s *Server) Process(ctx context.Context, network net.Network, conn stat.Connection, dispatcher routing.Dispatcher) error { + inbound := session.InboundFromContext(ctx) + inbound.Name = "socks" + inbound.CanSpliceCopy = 2 + inbound.User = &protocol.MemoryUser{ + Level: s.config.UserLevel, + } + if !proxy.IsRAWTransportWithoutSecurity(conn) { + inbound.CanSpliceCopy = 3 + } + + switch network { + case net.Network_TCP: + firstbyte := make([]byte, 1) + if n, err := conn.Read(firstbyte); n == 0 { + if goerrors.Is(err, io.EOF) { + errors.LogInfo(ctx, "Connection closed immediately, likely health check connection") + return nil + } + return errors.New("failed to read from connection").Base(err) + } + if firstbyte[0] != 5 && firstbyte[0] != 4 { // Check if it is Socks5/4/4a + errors.LogDebug(ctx, "Not Socks request, try to parse as HTTP request") + return s.httpServer.ProcessWithFirstbyte(ctx, network, conn, dispatcher, firstbyte...) + } + return s.processTCP(ctx, conn, dispatcher, firstbyte) + case net.Network_UDP: + return s.handleUDPPayload(ctx, conn, dispatcher) + default: + return errors.New("unknown network: ", network) + } +} + +func (s *Server) processTCP(ctx context.Context, conn stat.Connection, dispatcher routing.Dispatcher, firstbyte []byte) error { + plcy := s.policy() + if err := conn.SetReadDeadline(time.Now().Add(plcy.Timeouts.Handshake)); err != nil { + errors.LogInfoInner(ctx, err, "failed to set deadline") + } + + inbound := session.InboundFromContext(ctx) + if inbound == nil || !inbound.Gateway.IsValid() { + return errors.New("inbound gateway not specified") + } + + svrSession := &ServerSession{ + config: s.config, + address: inbound.Gateway.Address, + port: inbound.Gateway.Port, + localAddress: net.IPAddress(conn.LocalAddr().(*net.TCPAddr).IP), + } + + // Firstbyte is for forwarded conn from SOCKS inbound + // Because it needs first byte to choose protocol + // We need to add it back + reader := &buf.BufferedReader{ + Reader: buf.NewReader(conn), + Buffer: buf.MultiBuffer{buf.FromBytes(firstbyte)}, + } + request, err := svrSession.Handshake(reader, conn) + if err != nil { + if inbound.Source.IsValid() { + log.Record(&log.AccessMessage{ + From: inbound.Source, + To: "", + Status: log.AccessRejected, + Reason: err, + }) + } + return errors.New("failed to read request").Base(err) + } + if request.User != nil { + inbound.User.Email = request.User.Email + } + + if err := conn.SetReadDeadline(time.Time{}); err != nil { + errors.LogInfoInner(ctx, err, "failed to clear deadline") + } + + if request.Command == protocol.RequestCommandTCP { + dest := request.Destination() + errors.LogInfo(ctx, "TCP Connect request to ", dest) + if inbound.Source.IsValid() { + ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{ + From: inbound.Source, + To: dest, + Status: log.AccessAccepted, + Reason: "", + }) + } + if inbound.CanSpliceCopy == 2 { + inbound.CanSpliceCopy = 1 + } + if err := dispatcher.DispatchLink(ctx, dest, &transport.Link{ + Reader: reader, + Writer: buf.NewWriter(conn)}, + ); err != nil { + return errors.New("failed to dispatch request").Base(err) + } + return nil + } + + if request.Command == protocol.RequestCommandUDP { + if s.udpFilter != nil { + s.udpFilter.Add(conn.RemoteAddr()) + } + return s.handleUDP(conn) + } + + return nil +} + +func (*Server) handleUDP(c io.Reader) error { + // The TCP connection closes after this method returns. We need to wait until + // the client closes it. + return common.Error2(io.Copy(buf.DiscardBytes, c)) +} + +func (s *Server) handleUDPPayload(ctx context.Context, conn stat.Connection, dispatcher routing.Dispatcher) error { + if s.udpFilter != nil && !s.udpFilter.Check(conn.RemoteAddr()) { + errors.LogDebug(ctx, "Unauthorized UDP access from ", conn.RemoteAddr().String()) + return nil + } + udpServer := udp.NewDispatcher(dispatcher, func(ctx context.Context, packet *udp_proto.Packet) { + payload := packet.Payload + errors.LogDebug(ctx, "writing back UDP response with ", payload.Len(), " bytes") + + request := protocol.RequestHeaderFromContext(ctx) + if request == nil { + payload.Release() + return + } + + if payload.UDP != nil { + request = &protocol.RequestHeader{ + User: request.User, + Address: payload.UDP.Address, + Port: payload.UDP.Port, + } + } + + udpMessage, err := EncodeUDPPacket(request, payload.Bytes()) + payload.Release() + + if err != nil { + errors.LogWarningInner(ctx, err, "failed to write UDP response") + return + } + + conn.Write(udpMessage.Bytes()) + udpMessage.Release() + }) + defer udpServer.RemoveRay() + + inbound := session.InboundFromContext(ctx) + if inbound != nil && inbound.Source.IsValid() { + errors.LogInfo(ctx, "client UDP connection from ", inbound.Source) + } + + var dest *net.Destination + + reader := buf.NewPacketReader(conn) + for { + mpayload, err := reader.ReadMultiBuffer() + if err != nil { + return err + } + + for _, payload := range mpayload { + request, err := DecodeUDPPacket(payload) + if err != nil { + errors.LogInfoInner(ctx, err, "failed to parse UDP request") + payload.Release() + continue + } + + if payload.IsEmpty() { + payload.Release() + continue + } + + destination := request.Destination() + + currentPacketCtx := ctx + errors.LogDebug(ctx, "send packet to ", destination, " with ", payload.Len(), " bytes") + if inbound != nil && inbound.Source.IsValid() { + currentPacketCtx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{ + From: inbound.Source, + To: destination, + Status: log.AccessAccepted, + Reason: "", + }) + } + + payload.UDP = &destination + + if !s.cone || dest == nil { + dest = &destination + } + + currentPacketCtx = protocol.ContextWithRequestHeader(currentPacketCtx, request) + udpServer.Dispatch(currentPacketCtx, *dest, payload) + } + } +} + +func init() { + common.Must(common.RegisterConfig((*ServerConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + return NewServer(ctx, config.(*ServerConfig)) + })) +} diff --git a/subproject/Xray-core-main/proxy/socks/socks.go b/subproject/Xray-core-main/proxy/socks/socks.go new file mode 100644 index 00000000..2f885d39 --- /dev/null +++ b/subproject/Xray-core-main/proxy/socks/socks.go @@ -0,0 +1,2 @@ +// Package socks provides implements of Socks protocol 4, 4a and 5. +package socks diff --git a/subproject/Xray-core-main/proxy/socks/udpfilter.go b/subproject/Xray-core-main/proxy/socks/udpfilter.go new file mode 100644 index 00000000..9ae3e697 --- /dev/null +++ b/subproject/Xray-core-main/proxy/socks/udpfilter.go @@ -0,0 +1,31 @@ +package socks + +import ( + "net" + "sync" +) + +/* +In the sock implementation of * ray, UDP authentication is flawed and can be bypassed. +Tracking a UDP connection may be a bit troublesome. +Here is a simple solution. +We create a filter, add remote IP to the pool when it try to establish a UDP connection with auth. +And drop UDP packets from unauthorized IP. +After discussion, we believe it is not necessary to add a timeout mechanism to this filter. +*/ + +type UDPFilter struct { + ips sync.Map +} + +func (f *UDPFilter) Add(addr net.Addr) bool { + ip, _, _ := net.SplitHostPort(addr.String()) + f.ips.Store(ip, true) + return true +} + +func (f *UDPFilter) Check(addr net.Addr) bool { + ip, _, _ := net.SplitHostPort(addr.String()) + _, ok := f.ips.Load(ip) + return ok +} diff --git a/subproject/Xray-core-main/proxy/trojan/client.go b/subproject/Xray-core-main/proxy/trojan/client.go new file mode 100644 index 00000000..4af8c019 --- /dev/null +++ b/subproject/Xray-core-main/proxy/trojan/client.go @@ -0,0 +1,169 @@ +package trojan + +import ( + "context" + "time" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/retry" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/common/signal" + "github.com/xtls/xray-core/common/task" + core "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/features/policy" + "github.com/xtls/xray-core/transport" + "github.com/xtls/xray-core/transport/internet" + "github.com/xtls/xray-core/transport/internet/stat" +) + +// Client is a inbound handler for trojan protocol +type Client struct { + server *protocol.ServerSpec + policyManager policy.Manager +} + +// NewClient create a new trojan client. +func NewClient(ctx context.Context, config *ClientConfig) (*Client, error) { + if config.Server == nil { + return nil, errors.New(`no target server found`) + } + server, err := protocol.NewServerSpecFromPB(config.Server) + if err != nil { + return nil, errors.New("failed to get server spec").Base(err) + } + + v := core.MustFromContext(ctx) + client := &Client{ + server: server, + policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager), + } + return client, nil +} + +// Process implements OutboundHandler.Process(). +func (c *Client) Process(ctx context.Context, link *transport.Link, dialer internet.Dialer) error { + outbounds := session.OutboundsFromContext(ctx) + ob := outbounds[len(outbounds)-1] + if !ob.Target.IsValid() { + return errors.New("target not specified") + } + ob.Name = "trojan" + ob.CanSpliceCopy = 3 + destination := ob.Target + network := destination.Network + + server := c.server + var conn stat.Connection + + err := retry.ExponentialBackoff(5, 100).On(func() error { + rawConn, err := dialer.Dial(ctx, server.Destination) + if err != nil { + return err + } + + conn = rawConn + return nil + }) + if err != nil { + return errors.New("failed to find an available destination").AtWarning().Base(err) + } + errors.LogInfo(ctx, "tunneling request to ", destination, " via ", server.Destination.NetAddr()) + + defer conn.Close() + + user := server.User + account, ok := user.Account.(*MemoryAccount) + if !ok { + return errors.New("user account is not valid") + } + + var newCtx context.Context + var newCancel context.CancelFunc + if session.TimeoutOnlyFromContext(ctx) { + newCtx, newCancel = context.WithCancel(context.Background()) + } + + sessionPolicy := c.policyManager.ForLevel(user.Level) + ctx, cancel := context.WithCancel(ctx) + timer := signal.CancelAfterInactivity(ctx, func() { + cancel() + if newCancel != nil { + newCancel() + } + }, sessionPolicy.Timeouts.ConnectionIdle) + + postRequest := func() error { + defer timer.SetTimeout(sessionPolicy.Timeouts.DownlinkOnly) + + bufferWriter := buf.NewBufferedWriter(buf.NewWriter(conn)) + + connWriter := &ConnWriter{ + Writer: bufferWriter, + Target: destination, + Account: account, + } + + var bodyWriter buf.Writer + if destination.Network == net.Network_UDP { + bodyWriter = &PacketWriter{Writer: connWriter, Target: destination} + } else { + bodyWriter = connWriter + } + + // write some request payload to buffer + if err = buf.CopyOnceTimeout(link.Reader, bodyWriter, time.Millisecond*100); err != nil && err != buf.ErrNotTimeoutReader && err != buf.ErrReadTimeout { + return errors.New("failed to write A request payload").Base(err).AtWarning() + } + + // Flush; bufferWriter.WriteMultiBuffer now is bufferWriter.writer.WriteMultiBuffer + if err = bufferWriter.SetBuffered(false); err != nil { + return errors.New("failed to flush payload").Base(err).AtWarning() + } + + // Send header if not sent yet + if _, err = connWriter.Write([]byte{}); err != nil { + return err.(*errors.Error).AtWarning() + } + + if err = buf.Copy(link.Reader, bodyWriter, buf.UpdateActivity(timer)); err != nil { + return errors.New("failed to transfer request payload").Base(err).AtInfo() + } + + return nil + } + + getResponse := func() error { + defer timer.SetTimeout(sessionPolicy.Timeouts.UplinkOnly) + + var reader buf.Reader + if network == net.Network_UDP { + reader = &PacketReader{ + Reader: conn, + } + } else { + reader = buf.NewReader(conn) + } + return buf.Copy(reader, link.Writer, buf.UpdateActivity(timer)) + } + + if newCtx != nil { + ctx = newCtx + } + + responseDoneAndCloseWriter := task.OnSuccess(getResponse, task.Close(link.Writer)) + if err := task.Run(ctx, postRequest, responseDoneAndCloseWriter); err != nil { + return errors.New("connection ends").Base(err) + } + + return nil +} + +func init() { + common.Must(common.RegisterConfig((*ClientConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + return NewClient(ctx, config.(*ClientConfig)) + })) +} diff --git a/subproject/Xray-core-main/proxy/trojan/config.go b/subproject/Xray-core-main/proxy/trojan/config.go new file mode 100644 index 00000000..b7591996 --- /dev/null +++ b/subproject/Xray-core-main/proxy/trojan/config.go @@ -0,0 +1,57 @@ +package trojan + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "google.golang.org/protobuf/proto" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/protocol" +) + +// MemoryAccount is an account type converted from Account. +type MemoryAccount struct { + Password string + Key []byte +} + +// AsAccount implements protocol.AsAccount. +func (a *Account) AsAccount() (protocol.Account, error) { + password := a.GetPassword() + key := hexSha224(password) + return &MemoryAccount{ + Password: password, + Key: key, + }, nil +} + +// Equals implements protocol.Account.Equals(). +func (a *MemoryAccount) Equals(another protocol.Account) bool { + if account, ok := another.(*MemoryAccount); ok { + return a.Password == account.Password + } + return false +} + +func (a *MemoryAccount) ToProto() proto.Message { + return &Account{ + Password: a.Password, + } +} + +func hexSha224(password string) []byte { + buf := make([]byte, 56) + hash := sha256.New224() + common.Must2(hash.Write([]byte(password))) + hex.Encode(buf, hash.Sum(nil)) + return buf +} + +func hexString(data []byte) string { + str := "" + for _, v := range data { + str += fmt.Sprintf("%02x", v) + } + return str +} diff --git a/subproject/Xray-core-main/proxy/trojan/config.pb.go b/subproject/Xray-core-main/proxy/trojan/config.pb.go new file mode 100644 index 00000000..dd12c168 --- /dev/null +++ b/subproject/Xray-core-main/proxy/trojan/config.pb.go @@ -0,0 +1,324 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: proxy/trojan/config.proto + +package trojan + +import ( + protocol "github.com/xtls/xray-core/common/protocol" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Account struct { + state protoimpl.MessageState `protogen:"open.v1"` + Password string `protobuf:"bytes,1,opt,name=password,proto3" json:"password,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Account) Reset() { + *x = Account{} + mi := &file_proxy_trojan_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Account) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Account) ProtoMessage() {} + +func (x *Account) ProtoReflect() protoreflect.Message { + mi := &file_proxy_trojan_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Account.ProtoReflect.Descriptor instead. +func (*Account) Descriptor() ([]byte, []int) { + return file_proxy_trojan_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Account) GetPassword() string { + if x != nil { + return x.Password + } + return "" +} + +type Fallback struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Alpn string `protobuf:"bytes,2,opt,name=alpn,proto3" json:"alpn,omitempty"` + Path string `protobuf:"bytes,3,opt,name=path,proto3" json:"path,omitempty"` + Type string `protobuf:"bytes,4,opt,name=type,proto3" json:"type,omitempty"` + Dest string `protobuf:"bytes,5,opt,name=dest,proto3" json:"dest,omitempty"` + Xver uint64 `protobuf:"varint,6,opt,name=xver,proto3" json:"xver,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Fallback) Reset() { + *x = Fallback{} + mi := &file_proxy_trojan_config_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Fallback) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Fallback) ProtoMessage() {} + +func (x *Fallback) ProtoReflect() protoreflect.Message { + mi := &file_proxy_trojan_config_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Fallback.ProtoReflect.Descriptor instead. +func (*Fallback) Descriptor() ([]byte, []int) { + return file_proxy_trojan_config_proto_rawDescGZIP(), []int{1} +} + +func (x *Fallback) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Fallback) GetAlpn() string { + if x != nil { + return x.Alpn + } + return "" +} + +func (x *Fallback) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *Fallback) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *Fallback) GetDest() string { + if x != nil { + return x.Dest + } + return "" +} + +func (x *Fallback) GetXver() uint64 { + if x != nil { + return x.Xver + } + return 0 +} + +type ClientConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + Server *protocol.ServerEndpoint `protobuf:"bytes,1,opt,name=server,proto3" json:"server,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ClientConfig) Reset() { + *x = ClientConfig{} + mi := &file_proxy_trojan_config_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ClientConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClientConfig) ProtoMessage() {} + +func (x *ClientConfig) ProtoReflect() protoreflect.Message { + mi := &file_proxy_trojan_config_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ClientConfig.ProtoReflect.Descriptor instead. +func (*ClientConfig) Descriptor() ([]byte, []int) { + return file_proxy_trojan_config_proto_rawDescGZIP(), []int{2} +} + +func (x *ClientConfig) GetServer() *protocol.ServerEndpoint { + if x != nil { + return x.Server + } + return nil +} + +type ServerConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + Users []*protocol.User `protobuf:"bytes,1,rep,name=users,proto3" json:"users,omitempty"` + Fallbacks []*Fallback `protobuf:"bytes,2,rep,name=fallbacks,proto3" json:"fallbacks,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ServerConfig) Reset() { + *x = ServerConfig{} + mi := &file_proxy_trojan_config_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ServerConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ServerConfig) ProtoMessage() {} + +func (x *ServerConfig) ProtoReflect() protoreflect.Message { + mi := &file_proxy_trojan_config_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ServerConfig.ProtoReflect.Descriptor instead. +func (*ServerConfig) Descriptor() ([]byte, []int) { + return file_proxy_trojan_config_proto_rawDescGZIP(), []int{3} +} + +func (x *ServerConfig) GetUsers() []*protocol.User { + if x != nil { + return x.Users + } + return nil +} + +func (x *ServerConfig) GetFallbacks() []*Fallback { + if x != nil { + return x.Fallbacks + } + return nil +} + +var File_proxy_trojan_config_proto protoreflect.FileDescriptor + +const file_proxy_trojan_config_proto_rawDesc = "" + + "\n" + + "\x19proxy/trojan/config.proto\x12\x11xray.proxy.trojan\x1a\x1acommon/protocol/user.proto\x1a!common/protocol/server_spec.proto\"%\n" + + "\aAccount\x12\x1a\n" + + "\bpassword\x18\x01 \x01(\tR\bpassword\"\x82\x01\n" + + "\bFallback\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x12\n" + + "\x04alpn\x18\x02 \x01(\tR\x04alpn\x12\x12\n" + + "\x04path\x18\x03 \x01(\tR\x04path\x12\x12\n" + + "\x04type\x18\x04 \x01(\tR\x04type\x12\x12\n" + + "\x04dest\x18\x05 \x01(\tR\x04dest\x12\x12\n" + + "\x04xver\x18\x06 \x01(\x04R\x04xver\"L\n" + + "\fClientConfig\x12<\n" + + "\x06server\x18\x01 \x01(\v2$.xray.common.protocol.ServerEndpointR\x06server\"{\n" + + "\fServerConfig\x120\n" + + "\x05users\x18\x01 \x03(\v2\x1a.xray.common.protocol.UserR\x05users\x129\n" + + "\tfallbacks\x18\x02 \x03(\v2\x1b.xray.proxy.trojan.FallbackR\tfallbacksBU\n" + + "\x15com.xray.proxy.trojanP\x01Z&github.com/xtls/xray-core/proxy/trojan\xaa\x02\x11Xray.Proxy.Trojanb\x06proto3" + +var ( + file_proxy_trojan_config_proto_rawDescOnce sync.Once + file_proxy_trojan_config_proto_rawDescData []byte +) + +func file_proxy_trojan_config_proto_rawDescGZIP() []byte { + file_proxy_trojan_config_proto_rawDescOnce.Do(func() { + file_proxy_trojan_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proxy_trojan_config_proto_rawDesc), len(file_proxy_trojan_config_proto_rawDesc))) + }) + return file_proxy_trojan_config_proto_rawDescData +} + +var file_proxy_trojan_config_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_proxy_trojan_config_proto_goTypes = []any{ + (*Account)(nil), // 0: xray.proxy.trojan.Account + (*Fallback)(nil), // 1: xray.proxy.trojan.Fallback + (*ClientConfig)(nil), // 2: xray.proxy.trojan.ClientConfig + (*ServerConfig)(nil), // 3: xray.proxy.trojan.ServerConfig + (*protocol.ServerEndpoint)(nil), // 4: xray.common.protocol.ServerEndpoint + (*protocol.User)(nil), // 5: xray.common.protocol.User +} +var file_proxy_trojan_config_proto_depIdxs = []int32{ + 4, // 0: xray.proxy.trojan.ClientConfig.server:type_name -> xray.common.protocol.ServerEndpoint + 5, // 1: xray.proxy.trojan.ServerConfig.users:type_name -> xray.common.protocol.User + 1, // 2: xray.proxy.trojan.ServerConfig.fallbacks:type_name -> xray.proxy.trojan.Fallback + 3, // [3:3] is the sub-list for method output_type + 3, // [3:3] is the sub-list for method input_type + 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_proxy_trojan_config_proto_init() } +func file_proxy_trojan_config_proto_init() { + if File_proxy_trojan_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_proxy_trojan_config_proto_rawDesc), len(file_proxy_trojan_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 4, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_proxy_trojan_config_proto_goTypes, + DependencyIndexes: file_proxy_trojan_config_proto_depIdxs, + MessageInfos: file_proxy_trojan_config_proto_msgTypes, + }.Build() + File_proxy_trojan_config_proto = out.File + file_proxy_trojan_config_proto_goTypes = nil + file_proxy_trojan_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/proxy/trojan/config.proto b/subproject/Xray-core-main/proxy/trojan/config.proto new file mode 100644 index 00000000..30dedbf4 --- /dev/null +++ b/subproject/Xray-core-main/proxy/trojan/config.proto @@ -0,0 +1,32 @@ +syntax = "proto3"; + +package xray.proxy.trojan; +option csharp_namespace = "Xray.Proxy.Trojan"; +option go_package = "github.com/xtls/xray-core/proxy/trojan"; +option java_package = "com.xray.proxy.trojan"; +option java_multiple_files = true; + +import "common/protocol/user.proto"; +import "common/protocol/server_spec.proto"; + +message Account { + string password = 1; +} + +message Fallback { + string name = 1; + string alpn = 2; + string path = 3; + string type = 4; + string dest = 5; + uint64 xver = 6; +} + +message ClientConfig { + xray.common.protocol.ServerEndpoint server = 1; +} + +message ServerConfig { + repeated xray.common.protocol.User users = 1; + repeated Fallback fallbacks = 2; +} diff --git a/subproject/Xray-core-main/proxy/trojan/protocol.go b/subproject/Xray-core-main/proxy/trojan/protocol.go new file mode 100644 index 00000000..889ccc5c --- /dev/null +++ b/subproject/Xray-core-main/proxy/trojan/protocol.go @@ -0,0 +1,262 @@ +package trojan + +import ( + "encoding/binary" + "io" + + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" +) + +var ( + crlf = []byte{'\r', '\n'} + + addrParser = protocol.NewAddressParser( + protocol.AddressFamilyByte(0x01, net.AddressFamilyIPv4), + protocol.AddressFamilyByte(0x04, net.AddressFamilyIPv6), + protocol.AddressFamilyByte(0x03, net.AddressFamilyDomain), + ) +) + +const ( + maxLength = 8192 + + commandTCP byte = 1 + commandUDP byte = 3 +) + +// ConnWriter is TCP Connection Writer Wrapper for trojan protocol +type ConnWriter struct { + io.Writer + Target net.Destination + Account *MemoryAccount + headerSent bool +} + +// Write implements io.Writer +func (c *ConnWriter) Write(p []byte) (n int, err error) { + if !c.headerSent { + if err := c.writeHeader(); err != nil { + return 0, errors.New("failed to write request header").Base(err) + } + } + + return c.Writer.Write(p) +} + +// WriteMultiBuffer implements buf.Writer +func (c *ConnWriter) WriteMultiBuffer(mb buf.MultiBuffer) error { + defer buf.ReleaseMulti(mb) + + for _, b := range mb { + if !b.IsEmpty() { + if _, err := c.Write(b.Bytes()); err != nil { + return err + } + } + } + + return nil +} + +func (c *ConnWriter) writeHeader() error { + buffer := buf.StackNew() + defer buffer.Release() + + command := commandTCP + if c.Target.Network == net.Network_UDP { + command = commandUDP + } + + if _, err := buffer.Write(c.Account.Key); err != nil { + return err + } + if _, err := buffer.Write(crlf); err != nil { + return err + } + if err := buffer.WriteByte(command); err != nil { + return err + } + if err := addrParser.WriteAddressPort(&buffer, c.Target.Address, c.Target.Port); err != nil { + return err + } + if _, err := buffer.Write(crlf); err != nil { + return err + } + + _, err := c.Writer.Write(buffer.Bytes()) + if err == nil { + c.headerSent = true + } + + return err +} + +// PacketWriter UDP Connection Writer Wrapper for trojan protocol +type PacketWriter struct { + io.Writer + Target net.Destination +} + +// WriteMultiBuffer implements buf.Writer +func (w *PacketWriter) WriteMultiBuffer(mb buf.MultiBuffer) error { + for { + mb2, b := buf.SplitFirst(mb) + mb = mb2 + if b == nil { + break + } + target := &w.Target + if b.UDP != nil { + target = b.UDP + } + if _, err := w.writePacket(b.Bytes(), *target); err != nil { + b.Release() + buf.ReleaseMulti(mb) + return err + } + b.Release() + } + return nil +} + +func (w *PacketWriter) writePacket(payload []byte, dest net.Destination) (int, error) { + buffer := buf.StackNew() + defer buffer.Release() + + length := len(payload) + lengthBuf := [2]byte{} + binary.BigEndian.PutUint16(lengthBuf[:], uint16(length)) + if err := addrParser.WriteAddressPort(&buffer, dest.Address, dest.Port); err != nil { + return 0, err + } + if _, err := buffer.Write(lengthBuf[:]); err != nil { + return 0, err + } + if _, err := buffer.Write(crlf); err != nil { + return 0, err + } + if _, err := buffer.Write(payload); err != nil { + return 0, err + } + _, err := w.Write(buffer.Bytes()) + if err != nil { + return 0, err + } + + return length, nil +} + +// ConnReader is TCP Connection Reader Wrapper for trojan protocol +type ConnReader struct { + io.Reader + Target net.Destination + Flow string + headerParsed bool +} + +// ParseHeader parses the trojan protocol header +func (c *ConnReader) ParseHeader() error { + var crlf [2]byte + var command [1]byte + var hash [56]byte + if _, err := io.ReadFull(c.Reader, hash[:]); err != nil { + return errors.New("failed to read user hash").Base(err) + } + + if _, err := io.ReadFull(c.Reader, crlf[:]); err != nil { + return errors.New("failed to read crlf").Base(err) + } + + if _, err := io.ReadFull(c.Reader, command[:]); err != nil { + return errors.New("failed to read command").Base(err) + } + + network := net.Network_TCP + if command[0] == commandUDP { + network = net.Network_UDP + } + + addr, port, err := addrParser.ReadAddressPort(nil, c.Reader) + if err != nil { + return errors.New("failed to read address and port").Base(err) + } + c.Target = net.Destination{Network: network, Address: addr, Port: port} + + if _, err := io.ReadFull(c.Reader, crlf[:]); err != nil { + return errors.New("failed to read crlf").Base(err) + } + + c.headerParsed = true + return nil +} + +// Read implements io.Reader +func (c *ConnReader) Read(p []byte) (int, error) { + if !c.headerParsed { + if err := c.ParseHeader(); err != nil { + return 0, err + } + } + + return c.Reader.Read(p) +} + +// ReadMultiBuffer implements buf.Reader +func (c *ConnReader) ReadMultiBuffer() (buf.MultiBuffer, error) { + b := buf.New() + _, err := b.ReadFrom(c) + return buf.MultiBuffer{b}, err +} + +// PacketReader is UDP Connection Reader Wrapper for trojan protocol +type PacketReader struct { + io.Reader +} + +// ReadMultiBuffer implements buf.Reader +func (r *PacketReader) ReadMultiBuffer() (buf.MultiBuffer, error) { + addr, port, err := addrParser.ReadAddressPort(nil, r) + if err != nil { + return nil, errors.New("failed to read address and port").Base(err) + } + + var lengthBuf [2]byte + if _, err := io.ReadFull(r, lengthBuf[:]); err != nil { + return nil, errors.New("failed to read payload length").Base(err) + } + + remain := int(binary.BigEndian.Uint16(lengthBuf[:])) + if remain > maxLength { + return nil, errors.New("oversize payload") + } + + var crlf [2]byte + if _, err := io.ReadFull(r, crlf[:]); err != nil { + return nil, errors.New("failed to read crlf").Base(err) + } + + dest := net.UDPDestination(addr, port) + var mb buf.MultiBuffer + for remain > 0 { + length := buf.Size + if remain < length { + length = remain + } + + b := buf.New() + b.UDP = &dest + mb = append(mb, b) + n, err := b.ReadFullFrom(r, int32(length)) + if err != nil { + buf.ReleaseMulti(mb) + return nil, errors.New("failed to read payload").Base(err) + } + + remain -= int(n) + } + + return mb, nil +} diff --git a/subproject/Xray-core-main/proxy/trojan/protocol_test.go b/subproject/Xray-core-main/proxy/trojan/protocol_test.go new file mode 100644 index 00000000..038f45fd --- /dev/null +++ b/subproject/Xray-core-main/proxy/trojan/protocol_test.go @@ -0,0 +1,92 @@ +package trojan_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + . "github.com/xtls/xray-core/proxy/trojan" +) + +func toAccount(a *Account) protocol.Account { + account, err := a.AsAccount() + common.Must(err) + return account +} + +func TestTCPRequest(t *testing.T) { + user := &protocol.MemoryUser{ + Email: "love@example.com", + Account: toAccount(&Account{ + Password: "password", + }), + } + payload := []byte("test string") + data := buf.New() + common.Must2(data.Write(payload)) + + buffer := buf.New() + defer buffer.Release() + + destination := net.Destination{Network: net.Network_TCP, Address: net.LocalHostIP, Port: 1234} + writer := &ConnWriter{Writer: buffer, Target: destination, Account: user.Account.(*MemoryAccount)} + common.Must(writer.WriteMultiBuffer(buf.MultiBuffer{data})) + + reader := &ConnReader{Reader: buffer} + common.Must(reader.ParseHeader()) + + if r := cmp.Diff(reader.Target, destination); r != "" { + t.Error("destination: ", r) + } + + decodedData, err := reader.ReadMultiBuffer() + common.Must(err) + if r := cmp.Diff(decodedData[0].Bytes(), payload); r != "" { + t.Error("data: ", r) + } +} + +func TestUDPRequest(t *testing.T) { + user := &protocol.MemoryUser{ + Email: "love@example.com", + Account: toAccount(&Account{ + Password: "password", + }), + } + payload := []byte("test string") + data := buf.New() + common.Must2(data.Write(payload)) + + buffer := buf.New() + defer buffer.Release() + + destination := net.Destination{Network: net.Network_UDP, Address: net.LocalHostIP, Port: 1234} + writer := &PacketWriter{Writer: &ConnWriter{Writer: buffer, Target: destination, Account: user.Account.(*MemoryAccount)}, Target: destination} + common.Must(writer.WriteMultiBuffer(buf.MultiBuffer{data})) + + connReader := &ConnReader{Reader: buffer} + common.Must(connReader.ParseHeader()) + + packetReader := &PacketReader{Reader: connReader} + mb, err := packetReader.ReadMultiBuffer() + common.Must(err) + + if mb.IsEmpty() { + t.Error("no request data") + } + + mb2, b := buf.SplitFirst(mb) + defer buf.ReleaseMulti(mb2) + + dest := *b.UDP + if r := cmp.Diff(dest, destination); r != "" { + t.Error("destination: ", r) + } + + if r := cmp.Diff(b.Bytes(), payload); r != "" { + t.Error("data: ", r) + } +} diff --git a/subproject/Xray-core-main/proxy/trojan/server.go b/subproject/Xray-core-main/proxy/trojan/server.go new file mode 100644 index 00000000..d66219c4 --- /dev/null +++ b/subproject/Xray-core-main/proxy/trojan/server.go @@ -0,0 +1,550 @@ +package trojan + +import ( + "context" + "io" + "strconv" + "strings" + "time" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/log" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + udp_proto "github.com/xtls/xray-core/common/protocol/udp" + "github.com/xtls/xray-core/common/retry" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/common/signal" + "github.com/xtls/xray-core/common/task" + "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/features/policy" + "github.com/xtls/xray-core/features/routing" + "github.com/xtls/xray-core/transport/internet/reality" + "github.com/xtls/xray-core/transport/internet/stat" + "github.com/xtls/xray-core/transport/internet/tls" + "github.com/xtls/xray-core/transport/internet/udp" +) + +func init() { + common.Must(common.RegisterConfig((*ServerConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + return NewServer(ctx, config.(*ServerConfig)) + })) +} + +// Server is an inbound connection handler that handles messages in trojan protocol. +type Server struct { + policyManager policy.Manager + validator *Validator + fallbacks map[string]map[string]map[string]*Fallback // or nil + cone bool +} + +// NewServer creates a new trojan inbound handler. +func NewServer(ctx context.Context, config *ServerConfig) (*Server, error) { + validator := new(Validator) + for _, user := range config.Users { + u, err := user.ToMemoryUser() + if err != nil { + return nil, errors.New("failed to get trojan user").Base(err).AtError() + } + + if err := validator.Add(u); err != nil { + return nil, errors.New("failed to add user").Base(err).AtError() + } + } + + v := core.MustFromContext(ctx) + server := &Server{ + policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager), + validator: validator, + cone: ctx.Value("cone").(bool), + } + + if config.Fallbacks != nil { + server.fallbacks = make(map[string]map[string]map[string]*Fallback) + for _, fb := range config.Fallbacks { + if server.fallbacks[fb.Name] == nil { + server.fallbacks[fb.Name] = make(map[string]map[string]*Fallback) + } + if server.fallbacks[fb.Name][fb.Alpn] == nil { + server.fallbacks[fb.Name][fb.Alpn] = make(map[string]*Fallback) + } + server.fallbacks[fb.Name][fb.Alpn][fb.Path] = fb + } + if server.fallbacks[""] != nil { + for name, apfb := range server.fallbacks { + if name != "" { + for alpn := range server.fallbacks[""] { + if apfb[alpn] == nil { + apfb[alpn] = make(map[string]*Fallback) + } + } + } + } + } + for _, apfb := range server.fallbacks { + if apfb[""] != nil { + for alpn, pfb := range apfb { + if alpn != "" { // && alpn != "h2" { + for path, fb := range apfb[""] { + if pfb[path] == nil { + pfb[path] = fb + } + } + } + } + } + } + if server.fallbacks[""] != nil { + for name, apfb := range server.fallbacks { + if name != "" { + for alpn, pfb := range server.fallbacks[""] { + for path, fb := range pfb { + if apfb[alpn][path] == nil { + apfb[alpn][path] = fb + } + } + } + } + } + } + } + + return server, nil +} + +// AddUser implements proxy.UserManager.AddUser(). +func (s *Server) AddUser(ctx context.Context, u *protocol.MemoryUser) error { + return s.validator.Add(u) +} + +// RemoveUser implements proxy.UserManager.RemoveUser(). +func (s *Server) RemoveUser(ctx context.Context, e string) error { + return s.validator.Del(e) +} + +// GetUser implements proxy.UserManager.GetUser(). +func (s *Server) GetUser(ctx context.Context, email string) *protocol.MemoryUser { + return s.validator.GetByEmail(email) +} + +// GetUsers implements proxy.UserManager.GetUsers(). +func (s *Server) GetUsers(ctx context.Context) []*protocol.MemoryUser { + return s.validator.GetAll() +} + +// GetUsersCount implements proxy.UserManager.GetUsersCount(). +func (s *Server) GetUsersCount(context.Context) int64 { + return s.validator.GetCount() +} + +// Network implements proxy.Inbound.Network(). +func (s *Server) Network() []net.Network { + return []net.Network{net.Network_TCP, net.Network_UNIX} +} + +// Process implements proxy.Inbound.Process(). +func (s *Server) Process(ctx context.Context, network net.Network, conn stat.Connection, dispatcher routing.Dispatcher) error { + iConn := stat.TryUnwrapStatsConn(conn) + + sessionPolicy := s.policyManager.ForLevel(0) + if err := conn.SetReadDeadline(time.Now().Add(sessionPolicy.Timeouts.Handshake)); err != nil { + return errors.New("unable to set read deadline").Base(err).AtWarning() + } + + first := buf.FromBytes(make([]byte, buf.Size)) + first.Clear() + firstLen, err := first.ReadFrom(conn) + if err != nil { + return errors.New("failed to read first request").Base(err) + } + errors.LogInfo(ctx, "firstLen = ", firstLen) + + bufferedReader := &buf.BufferedReader{ + Reader: buf.NewReader(conn), + Buffer: buf.MultiBuffer{first}, + } + + var user *protocol.MemoryUser + + napfb := s.fallbacks + isfb := napfb != nil + + shouldFallback := false + if firstLen < 58 || first.Byte(56) != '\r' { + // invalid protocol + err = errors.New("not trojan protocol") + log.Record(&log.AccessMessage{ + From: conn.RemoteAddr(), + To: "", + Status: log.AccessRejected, + Reason: err, + }) + + shouldFallback = true + } else { + user = s.validator.Get(hexString(first.BytesTo(56))) + if user == nil { + // invalid user, let's fallback + err = errors.New("not a valid user") + log.Record(&log.AccessMessage{ + From: conn.RemoteAddr(), + To: "", + Status: log.AccessRejected, + Reason: err, + }) + + shouldFallback = true + } + } + + if isfb && shouldFallback { + return s.fallback(ctx, err, sessionPolicy, conn, iConn, napfb, first, firstLen, bufferedReader) + } else if shouldFallback { + return errors.New("invalid protocol or invalid user") + } + + clientReader := &ConnReader{Reader: bufferedReader} + if err := clientReader.ParseHeader(); err != nil { + log.Record(&log.AccessMessage{ + From: conn.RemoteAddr(), + To: "", + Status: log.AccessRejected, + Reason: err, + }) + return errors.New("failed to create request from: ", conn.RemoteAddr()).Base(err) + } + + destination := clientReader.Target + if err := conn.SetReadDeadline(time.Time{}); err != nil { + return errors.New("unable to set read deadline").Base(err).AtWarning() + } + + inbound := session.InboundFromContext(ctx) + inbound.Name = "trojan" + inbound.CanSpliceCopy = 3 + inbound.User = user + sessionPolicy = s.policyManager.ForLevel(user.Level) + + if destination.Network == net.Network_UDP { // handle udp request + return s.handleUDPPayload(ctx, sessionPolicy, &PacketReader{Reader: clientReader}, &PacketWriter{Writer: conn}, dispatcher) + } + + ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{ + From: conn.RemoteAddr(), + To: destination, + Status: log.AccessAccepted, + Reason: "", + Email: user.Email, + }) + + errors.LogInfo(ctx, "received request for ", destination) + return s.handleConnection(ctx, sessionPolicy, destination, clientReader, buf.NewWriter(conn), dispatcher) +} + +func (s *Server) handleUDPPayload(ctx context.Context, sessionPolicy policy.Session, clientReader *PacketReader, clientWriter *PacketWriter, dispatcher routing.Dispatcher) error { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + timer := signal.CancelAfterInactivity(ctx, cancel, sessionPolicy.Timeouts.ConnectionIdle) + defer timer.SetTimeout(0) + udpServer := udp.NewDispatcher(dispatcher, func(ctx context.Context, packet *udp_proto.Packet) { + udpPayload := packet.Payload + if udpPayload.UDP == nil { + udpPayload.UDP = &packet.Source + } + + if err := clientWriter.WriteMultiBuffer(buf.MultiBuffer{udpPayload}); err != nil { + errors.LogWarningInner(ctx, err, "failed to write response") + cancel() + } else { + timer.Update() + } + }) + defer udpServer.RemoveRay() + + inbound := session.InboundFromContext(ctx) + user := inbound.User + + var dest *net.Destination + + requestDone := func() error { + for { + select { + case <-ctx.Done(): + return nil + default: + mb, err := clientReader.ReadMultiBuffer() + if err != nil { + if errors.Cause(err) != io.EOF { + return errors.New("unexpected EOF").Base(err) + } + return nil + } + + mb2, b := buf.SplitFirst(mb) + if b == nil { + continue + } + timer.Update() + destination := *b.UDP + + currentPacketCtx := ctx + if inbound.Source.IsValid() { + currentPacketCtx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{ + From: inbound.Source, + To: destination, + Status: log.AccessAccepted, + Reason: "", + Email: user.Email, + }) + } + errors.LogInfo(ctx, "tunnelling request to ", destination) + + if !s.cone || dest == nil { + dest = &destination + } + + udpServer.Dispatch(currentPacketCtx, *dest, b) // first packet + for _, payload := range mb2 { + udpServer.Dispatch(currentPacketCtx, *dest, payload) + } + } + } + + } + + if err := task.Run(ctx, requestDone); err != nil { + return err + } + return nil +} + +func (s *Server) handleConnection(ctx context.Context, sessionPolicy policy.Session, + destination net.Destination, + clientReader buf.Reader, + clientWriter buf.Writer, dispatcher routing.Dispatcher, +) error { + ctx, cancel := context.WithCancel(ctx) + timer := signal.CancelAfterInactivity(ctx, cancel, sessionPolicy.Timeouts.ConnectionIdle) + ctx = policy.ContextWithBufferPolicy(ctx, sessionPolicy.Buffer) + + link, err := dispatcher.Dispatch(ctx, destination) + if err != nil { + return errors.New("failed to dispatch request to ", destination).Base(err) + } + + requestDone := func() error { + defer timer.SetTimeout(sessionPolicy.Timeouts.DownlinkOnly) + if buf.Copy(clientReader, link.Writer, buf.UpdateActivity(timer)) != nil { + return errors.New("failed to transfer request").Base(err) + } + return nil + } + + responseDone := func() error { + defer timer.SetTimeout(sessionPolicy.Timeouts.UplinkOnly) + + if err := buf.Copy(link.Reader, clientWriter, buf.UpdateActivity(timer)); err != nil { + return errors.New("failed to write response").Base(err) + } + return nil + } + + requestDonePost := task.OnSuccess(requestDone, task.Close(link.Writer)) + if err := task.Run(ctx, requestDonePost, responseDone); err != nil { + common.Must(common.Interrupt(link.Reader)) + common.Must(common.Interrupt(link.Writer)) + return errors.New("connection ends").Base(err) + } + + return nil +} + +func (s *Server) fallback(ctx context.Context, err error, sessionPolicy policy.Session, connection stat.Connection, iConn stat.Connection, napfb map[string]map[string]map[string]*Fallback, first *buf.Buffer, firstLen int64, reader buf.Reader) error { + if err := connection.SetReadDeadline(time.Time{}); err != nil { + errors.LogWarningInner(ctx, err, "unable to set back read deadline") + } + errors.LogInfoInner(ctx, err, "fallback starts") + + name := "" + alpn := "" + if tlsConn, ok := iConn.(*tls.Conn); ok { + cs := tlsConn.ConnectionState() + name = cs.ServerName + alpn = cs.NegotiatedProtocol + errors.LogInfo(ctx, "realName = "+name) + errors.LogInfo(ctx, "realAlpn = "+alpn) + } else if realityConn, ok := iConn.(*reality.Conn); ok { + cs := realityConn.ConnectionState() + name = cs.ServerName + alpn = cs.NegotiatedProtocol + errors.LogInfo(ctx, "realName = "+name) + errors.LogInfo(ctx, "realAlpn = "+alpn) + } + name = strings.ToLower(name) + alpn = strings.ToLower(alpn) + + if len(napfb) > 1 || napfb[""] == nil { + if name != "" && napfb[name] == nil { + match := "" + for n := range napfb { + if n != "" && strings.Contains(name, n) && len(n) > len(match) { + match = n + } + } + name = match + } + } + + if napfb[name] == nil { + name = "" + } + apfb := napfb[name] + if apfb == nil { + return errors.New(`failed to find the default "name" config`).AtWarning() + } + + if apfb[alpn] == nil { + alpn = "" + } + pfb := apfb[alpn] + if pfb == nil { + return errors.New(`failed to find the default "alpn" config`).AtWarning() + } + + path := "" + if len(pfb) > 1 || pfb[""] == nil { + if firstLen >= 18 && first.Byte(4) != '*' { // not h2c + firstBytes := first.Bytes() + for i := 4; i <= 8; i++ { // 5 -> 9 + if firstBytes[i] == '/' && firstBytes[i-1] == ' ' { + search := len(firstBytes) + if search > 64 { + search = 64 // up to about 60 + } + for j := i + 1; j < search; j++ { + k := firstBytes[j] + if k == '\r' || k == '\n' { // avoid logging \r or \n + break + } + if k == '?' || k == ' ' { + path = string(firstBytes[i:j]) + errors.LogInfo(ctx, "realPath = "+path) + if pfb[path] == nil { + path = "" + } + break + } + } + break + } + } + } + } + fb := pfb[path] + if fb == nil { + return errors.New(`failed to find the default "path" config`).AtWarning() + } + + ctx, cancel := context.WithCancel(ctx) + timer := signal.CancelAfterInactivity(ctx, cancel, sessionPolicy.Timeouts.ConnectionIdle) + ctx = policy.ContextWithBufferPolicy(ctx, sessionPolicy.Buffer) + + var conn net.Conn + if err := retry.ExponentialBackoff(5, 100).On(func() error { + var dialer net.Dialer + conn, err = dialer.DialContext(ctx, fb.Type, fb.Dest) + if err != nil { + return err + } + return nil + }); err != nil { + return errors.New("failed to dial to " + fb.Dest).Base(err).AtWarning() + } + defer conn.Close() + + serverReader := buf.NewReader(conn) + serverWriter := buf.NewWriter(conn) + + postRequest := func() error { + defer timer.SetTimeout(sessionPolicy.Timeouts.DownlinkOnly) + if fb.Xver != 0 { + ipType := 4 + remoteAddr, remotePort, err := net.SplitHostPort(connection.RemoteAddr().String()) + if err != nil { + ipType = 0 + } + localAddr, localPort, err := net.SplitHostPort(connection.LocalAddr().String()) + if err != nil { + ipType = 0 + } + if ipType == 4 { + for i := 0; i < len(remoteAddr); i++ { + if remoteAddr[i] == ':' { + ipType = 6 + break + } + } + } + pro := buf.New() + defer pro.Release() + switch fb.Xver { + case 1: + if ipType == 0 { + common.Must2(pro.Write([]byte("PROXY UNKNOWN\r\n"))) + break + } + if ipType == 4 { + common.Must2(pro.Write([]byte("PROXY TCP4 " + remoteAddr + " " + localAddr + " " + remotePort + " " + localPort + "\r\n"))) + } else { + common.Must2(pro.Write([]byte("PROXY TCP6 " + remoteAddr + " " + localAddr + " " + remotePort + " " + localPort + "\r\n"))) + } + case 2: + common.Must2(pro.Write([]byte("\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A"))) // signature + if ipType == 0 { + common.Must2(pro.Write([]byte("\x20\x00\x00\x00"))) // v2 + LOCAL + UNSPEC + UNSPEC + 0 bytes + break + } + if ipType == 4 { + common.Must2(pro.Write([]byte("\x21\x11\x00\x0C"))) // v2 + PROXY + AF_INET + STREAM + 12 bytes + common.Must2(pro.Write(net.ParseIP(remoteAddr).To4())) + common.Must2(pro.Write(net.ParseIP(localAddr).To4())) + } else { + common.Must2(pro.Write([]byte("\x21\x21\x00\x24"))) // v2 + PROXY + AF_INET6 + STREAM + 36 bytes + common.Must2(pro.Write(net.ParseIP(remoteAddr).To16())) + common.Must2(pro.Write(net.ParseIP(localAddr).To16())) + } + p1, _ := strconv.ParseUint(remotePort, 10, 16) + p2, _ := strconv.ParseUint(localPort, 10, 16) + common.Must2(pro.Write([]byte{byte(p1 >> 8), byte(p1), byte(p2 >> 8), byte(p2)})) + } + if err := serverWriter.WriteMultiBuffer(buf.MultiBuffer{pro}); err != nil { + return errors.New("failed to set PROXY protocol v", fb.Xver).Base(err).AtWarning() + } + } + if err := buf.Copy(reader, serverWriter, buf.UpdateActivity(timer)); err != nil { + return errors.New("failed to fallback request payload").Base(err).AtInfo() + } + return nil + } + + writer := buf.NewWriter(connection) + + getResponse := func() error { + defer timer.SetTimeout(sessionPolicy.Timeouts.UplinkOnly) + if err := buf.Copy(serverReader, writer, buf.UpdateActivity(timer)); err != nil { + return errors.New("failed to deliver response payload").Base(err).AtInfo() + } + return nil + } + + if err := task.Run(ctx, task.OnSuccess(postRequest, task.Close(serverWriter)), task.OnSuccess(getResponse, task.Close(writer))); err != nil { + common.Must(common.Interrupt(serverReader)) + common.Must(common.Interrupt(serverWriter)) + return errors.New("fallback ends").Base(err).AtInfo() + } + + return nil +} diff --git a/subproject/Xray-core-main/proxy/trojan/trojan.go b/subproject/Xray-core-main/proxy/trojan/trojan.go new file mode 100644 index 00000000..73b3154f --- /dev/null +++ b/subproject/Xray-core-main/proxy/trojan/trojan.go @@ -0,0 +1 @@ +package trojan diff --git a/subproject/Xray-core-main/proxy/trojan/validator.go b/subproject/Xray-core-main/proxy/trojan/validator.go new file mode 100644 index 00000000..7841a249 --- /dev/null +++ b/subproject/Xray-core-main/proxy/trojan/validator.go @@ -0,0 +1,82 @@ +package trojan + +import ( + "strings" + "sync" + + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/protocol" +) + +// Validator stores valid trojan users. +type Validator struct { + // Considering email's usage here, map + sync.Mutex/RWMutex may have better performance. + email sync.Map + users sync.Map +} + +// Add a trojan user, Email must be empty or unique. +func (v *Validator) Add(u *protocol.MemoryUser) error { + if u.Email != "" { + _, loaded := v.email.LoadOrStore(strings.ToLower(u.Email), u) + if loaded { + return errors.New("User ", u.Email, " already exists.") + } + } + v.users.Store(hexString(u.Account.(*MemoryAccount).Key), u) + return nil +} + +// Del a trojan user with a non-empty Email. +func (v *Validator) Del(e string) error { + if e == "" { + return errors.New("Email must not be empty.") + } + le := strings.ToLower(e) + u, _ := v.email.Load(le) + if u == nil { + return errors.New("User ", e, " not found.") + } + v.email.Delete(le) + v.users.Delete(hexString(u.(*protocol.MemoryUser).Account.(*MemoryAccount).Key)) + return nil +} + +// Get a trojan user with hashed key, nil if user doesn't exist. +func (v *Validator) Get(hash string) *protocol.MemoryUser { + u, _ := v.users.Load(hash) + if u != nil { + return u.(*protocol.MemoryUser) + } + return nil +} + +// Get a trojan user with hashed key, nil if user doesn't exist. +func (v *Validator) GetByEmail(email string) *protocol.MemoryUser { + email = strings.ToLower(email) + u, _ := v.email.Load(email) + if u != nil { + return u.(*protocol.MemoryUser) + } + return nil +} + +// Get all users +func (v *Validator) GetAll() []*protocol.MemoryUser { + var u = make([]*protocol.MemoryUser, 0, 100) + v.email.Range(func(key, value interface{}) bool { + u = append(u, value.(*protocol.MemoryUser)) + return true + }) + return u +} + +// Get users count +func (v *Validator) GetCount() int64 { + var c int64 = 0 + v.email.Range(func(key, value interface{}) bool { + c++ + return true + }) + return c +} diff --git a/subproject/Xray-core-main/proxy/tun/README.md b/subproject/Xray-core-main/proxy/tun/README.md new file mode 100644 index 00000000..ca081f5a --- /dev/null +++ b/subproject/Xray-core-main/proxy/tun/README.md @@ -0,0 +1,232 @@ +# TUN network layer 3 input support + +TUN interface support bridges the gap between network layer 3 and layer 7, introducing raw network input. + +This functionality is targeted to assist applications/end devices that don't have proxy support, or can't run external applications (like Smart TV's). Making it possible to run Xray proxy right on network edge devices (routers) with support to route raw network traffic. \ +Primary targets are Linux based router devices. Like most popular OpenWRT option. \ +Support for Windows, macOS, Android and iOS is also implemented (see below). + +## PLEASE READ FOLLOWING CAREFULLY + +If you are not sure what this is and do you need it or not - you don't. \ +This functionality is intended to be configured by network professionals, who understand the deployment case and scenarios. \ +Plainly enabling it in the config probably will result nothing, or lock your router up in infinite network loop. + +## DETAILS + +Current implementation does not contain options to configure network level addresses, routing or rules. +Enabling the feature will result only tun interface up, and that's it. \ +This is explicit decision, significantly simplifying implementation, and allowing any number of custom configurations, consumers could come up with. Network interface is OS level entity, and OS is what should manage it. \ +Working configuration, is tun enabled in Xray config with specific name (e.g. xray0), and OS level configuration to manage "xray0" interface, applying routing and rules on interface up. +This way consistency of system level routing and rules is ensured from single place of responsibility - the OS itself. \ +Examples of how to achieve this on a simple Linux system (Ubuntu with systemd-networkd) can be found at the end of this README. + +Due to this inbound not actually being a proxy, the configuration ignore required listen and port options, and never listen on any port. \ +Here is simple Xray config snippet to enable the inbound: +``` +{ + "inbounds": [ + { + "port": 0, + "protocol": "tun", + "settings": { + "name": "xray0", + "MTU": 1492 + } + } + ], +``` + +## SUPPORTED FEATURES + +- IPv4 and IPv6 +- TCP and UDP + +## LIMITATION + +- No ICMP support +- Connections are established to any host, as connection success is only a mark of successful accepting packet for proxying. Hosts that are not accepting connections or don't even exists, will look like they opened a connection (SYN-ACK), and never send back a single byte, closing connection (RST) after some time. This is the side effect of the whole process actually being a proxy, and not real network layer 3 vpn + +## CONSIDERATIONS + +This feature being network level interface that require raw routing, bring some ambiguities that need to be taken in account. \ +Xray-core itself is connecting to its uplinks on a network level, therefore, it's really simple to lock network up in an infinite loop, when trying to pass "everything through Xray". \ +You can't just route 0.0.0.0/0 through xray0 interface, as that will result Xray-core itself try to reach its uplink through xray0 interface, resulting infinite network loop. +There are several ways to address this: + +- Approach 1: \ + Add precise static route to Xray upstreams, having them always routed through static internet gateway. + E.g. when 123.123.123.123 is the Xray VLESS uplink, this network configuration will work just fine: + ``` + ip route add 123.123.123.123/32 via + ip route add 0.0.0.0/0 dev xray0 + ``` + This has disadvantages, - a lot of conditions must be kept static: internet gateway address, xray uplink ip address, and so on. +- Approach 1-b: \ + Route only specific networks through Xray, keeping the default gateway unchanged. + This can be done in many different ways, using ip sets, routing daemons like BGP peers, etc... All you need to do is to route the paths through xray0 dev. + The disadvantage in this case is smaller, - you need to make sure the uplink will not become part of those sets and that's it. Can easily be done with route metric priorities. +- Approach 2: \ + Separate main route table and Xray route table with default gateways pointing to different destinations. + This way you can achieve full protection of hosts behind the router, keeping router configuration as flexible as desired. \ + There are two ways to do that: \ + Either configure xray0 interface to appear and operate as default gateway in a separate route table, e.g. 1001. Then mark and route protected traffic by ip rules to that table. \ + It's a simplest way to make a "non-damaging" configuration, when the only thing you need to do to enable/disable proxying is to flip the ip rules off. Which is also a disadvantage of itself - if by accident ip rules will get disabled, the traffic will spill out of the internet interface unprotected. \ + Or, other way around, move default routing to a separate route table, so that all usual routing information is set in e.g. route table 1000, + and Xray interface operate in the main route table. This will allow proper flexibility, but you need to ensure traffic from the Xray process, running on the router, is marked to get routed through table 1000. This again can be achieved in same ways, using ip rules and iptable rules combinations. \ + Big advantage of that, is that traffic of the route itself is going to be wrapped into the proxy, including DNS queries, without any additional effort. Although, the disadvantage of that is, that in any case proxying stops (uplink dies, Xray hangs, encryption start to flap), it will result complete internet inaccessibility. \ + Any approach is applicable, and if you know what you are doing (which is expected, if you read until here) you do understand which one you want and can manage. \ + +### Important: + +TUN is network level entity, therefore communication through it, is always ip to ip, there are no host names. \ +Therefore, DNS resolution will always happen before traffic even enter the interface (it will be separate ip-to-ip packets/connections to resolve hostnames). \ +You always need to consider that DNS queries in any configuration you chose, most likely, will originate from the router itself (hosts behind the router access router DNS, router DNS fire queries to the outside). +Without proper addressing that, DNS queries will expose actual destinations/websites accessed through the router. \ +To address that you can as ignore (not use) DNS of the router (just delegate some public DNS in DHCP configuration to your devices), or make sure routing rules are configured the way, DNS resolution of the router itself runs through Xray interface/routing table. + +You also need to remember that local traffic of the router (e.g. DNS, firmware updates, etc.), is subject of firewall rules as outcoming/incoming traffic (not forward). +If you have restrictive firewall, you need to allow input/output traffic through xray0 interface, for it to properly dispatch and reach the OS back. + +Additionally, working with two route tables is not taken lightly by Linux, and sometimes make it panic about "martian packets", which it calls the packets arriving through interfaces it does not expect they could arrive from. \ +It was just a warning until recent kernel versions, but now traffic is often dropped. \ +In simple case this can be just disabled with +``` +/usr/sbin/sysctl -w net.ipv4.conf.all.rp_filter=0 +``` +But proper approach should be defining your route tables fully and consistently, adding all routes corresponding to traffic that flow through them. + +## EXAMPLES + +systemd-networkd \ +configuration file you can place in /etc/systemd/networkd as 90-xray0.network +which will configure xray0 interface and routing using route table 1001, when the interface will appear in the system (Xray starts). And deconfigure when disappears. +``` +[Match] +Name = xray0 + +[Network] +KeepConfiguration = yes + +[Link] +ActivationPolicy = manual +RequiredForOnline = no + +[Route] +Table = 1001 +Destination = 0.0.0.0/0 + +[RoutingPolicyRule] +From = 192.168.0.0/24 +Table = 1001 +``` +RoutingPolicyRule will add the record into ip rules, that will funnel all traffic from 192.168.0.0/24 through the table 1001 \ +Please note that for ideal configuration of the routing you will also need to add the route to 192.168.0.0/24 to the route table 1001. +You can do that e.g. in the file, describing your adapter serving local network (e.g. 10-br0.network), additionally to its native properties like: +``` +[Match] +Name = br0 +Type = bridge + +[Network] +...skip... +Address = 192.168.0.1/24 +...skip... + +[Route] +Table = 1001 +Destination = 192.168.0.0/24 +PreferredSource = 192.168.0.1 +Scope = link +``` +All in all systemd-networkd and its derivatives (like netplan or NetworkManager) provide all means to configure your networking, according to your wish, that will ensure network consistency of xray0 interface coming up and down, relative to other network configuration like internet interfaces, nat rules and so on. + +## WINDOWS SUPPORT + +Windows version of the same functionality is implemented through Wintun library. \ +To make it start, wintun.dll specific for your Windows/arch must be present next to Xray.exe binary. + +After the start network adapter with the name you chose in the config will be created in the system, and exist while Xray is running. + +You can give the adapter ip address manually, you can live Windows to give it autogenerated ip address (which take few seconds), it doesn't matter, the traffic going _through_ the interface will be forwarded into the app for proxying. \ +Minimal configuration that will work for local machine is routing passing the traffic on-link through the interface. +You will need the interface id for that, unfortunately it is going to change with every Xray start due to implementation ambiguity between Xray and wintun driver. +You can find the interface id with the command +``` +route print +``` +it will be in the list of interfaces on the top of the output +``` +=========================================================================== +Interface List + 8...cc cc cc cc cc cc ......Realtek PCIe GbE Family Controller + 47...........................Xray Tunnel + 1...........................Software Loopback Interface 1 +=========================================================================== +``` +In this case the interface id is "47". \ +Then you can add on-link route through the adapter with (example) command +``` +route add 1.1.1.1 mask 255.0.0.0 0.0.0.0 if 47 +``` +Note on ipv6 support. \ +Despite Windows also giving the adapter autoconfigured ipv6 address, the ipv6 is not possible until the interface has any _routable_ ipv6 address (given link-local address will not accept traffic from external addresses). \ +So everything applicable for ipv4 above also works for ipv6, you only need to give the interface some address manually, e.g. anything private like fc00::a:b:c:d/64 will do just fine + +## MAC OS X SUPPORT + +Darwin (Mac OS X) support of the same functionality is implemented through utun (userspace tunnel). + +Interface name in the configuration must comply to the scheme "utunN", where N is some number. \ +Most running OS'es create some amount of utun interfaces in advance for own needs. Please either check the interfaces you already have occupied by issuing following command: +``` +ifconfig +``` +Produced list will have all system interfaces listed, from which you will see how many "utun" ones already exists. +It's not required to select next available number, e.g. if you have utun1-utun7 interfaces, it's not required to have "utun8" in the config. You can choose any available name, even utun20, to get surely available interface number. + +To attach routing to the interface, route command like following can be executed: +``` +sudo route add -net 1.1.1.0/24 -iface utun10 +``` +``` +sudo route add -inet6 -host 2606:4700:4700::1111 -iface utun10 +sudo route add -inet6 -host 2606:4700:4700::1001 -iface utun10 +``` +Important to remember that everything written above about Linux routing concept, also apply to Mac OS X. If you simply route default route through utun interface, that will result network loop and immediate network failure. + +## ANDROID SUPPORT + +Android uses the VpnService API which provides a TUN file descriptor to the application. + +Obtain the fd from VpnService: +```kotlin +val tunFd = vpnInterface.fd +``` + +Set the environment variable `xray.tun.fd` (or `XRAY_TUN_FD`) to the fd number before starting Xray. This can be done from Kotlin/Java or by exposing a Go function via gomobile bindings. + +Build using gomobile for Android library integration: +``` +gomobile bind -target=android +``` + +## iOS SUPPORT + +iOS uses the same utun packet format as macOS, but the file descriptor is provided by NetworkExtension. + +Obtain the fd from NetworkExtension: +```swift +var buf = [CChar](repeating: 0, count: Int(IFNAMSIZ)) +let utunPrefix = "utun".utf8CString.dropLast() +let tunFd = ((0 ... 1024).first { (_ fd: Int32) -> Bool in var len = socklen_t(buf.count) + return getsockopt(fd, 2, 2, &buf, &len) == 0 && buf.starts(with: utunPrefix) +}! +``` + +Set the environment variable `xray.tun.fd` (or `XRAY_TUN_FD`) to the fd number before starting Xray. This can be done from Swift/Objective-C or by exposing a Go function via gomobile bindings. + +Build using gomobile for iOS framework integration: +``` +gomobile bind -target=ios +``` \ No newline at end of file diff --git a/subproject/Xray-core-main/proxy/tun/config.go b/subproject/Xray-core-main/proxy/tun/config.go new file mode 100644 index 00000000..75e8485f --- /dev/null +++ b/subproject/Xray-core-main/proxy/tun/config.go @@ -0,0 +1 @@ +package tun diff --git a/subproject/Xray-core-main/proxy/tun/config.pb.go b/subproject/Xray-core-main/proxy/tun/config.pb.go new file mode 100644 index 00000000..0c2b2e89 --- /dev/null +++ b/subproject/Xray-core-main/proxy/tun/config.pb.go @@ -0,0 +1,142 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: proxy/tun/config.proto + +package tun + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + MTU uint32 `protobuf:"varint,2,opt,name=MTU,proto3" json:"MTU,omitempty"` + UserLevel uint32 `protobuf:"varint,3,opt,name=user_level,json=userLevel,proto3" json:"user_level,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_proxy_tun_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_proxy_tun_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_proxy_tun_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Config) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Config) GetMTU() uint32 { + if x != nil { + return x.MTU + } + return 0 +} + +func (x *Config) GetUserLevel() uint32 { + if x != nil { + return x.UserLevel + } + return 0 +} + +var File_proxy_tun_config_proto protoreflect.FileDescriptor + +const file_proxy_tun_config_proto_rawDesc = "" + + "\n" + + "\x16proxy/tun/config.proto\x12\x0exray.proxy.tun\"M\n" + + "\x06Config\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x10\n" + + "\x03MTU\x18\x02 \x01(\rR\x03MTU\x12\x1d\n" + + "\n" + + "user_level\x18\x03 \x01(\rR\tuserLevelBL\n" + + "\x12com.xray.proxy.tunP\x01Z#github.com/xtls/xray-core/proxy/tun\xaa\x02\x0eXray.Proxy.Tunb\x06proto3" + +var ( + file_proxy_tun_config_proto_rawDescOnce sync.Once + file_proxy_tun_config_proto_rawDescData []byte +) + +func file_proxy_tun_config_proto_rawDescGZIP() []byte { + file_proxy_tun_config_proto_rawDescOnce.Do(func() { + file_proxy_tun_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proxy_tun_config_proto_rawDesc), len(file_proxy_tun_config_proto_rawDesc))) + }) + return file_proxy_tun_config_proto_rawDescData +} + +var file_proxy_tun_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_proxy_tun_config_proto_goTypes = []any{ + (*Config)(nil), // 0: xray.proxy.tun.Config +} +var file_proxy_tun_config_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_proxy_tun_config_proto_init() } +func file_proxy_tun_config_proto_init() { + if File_proxy_tun_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_proxy_tun_config_proto_rawDesc), len(file_proxy_tun_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_proxy_tun_config_proto_goTypes, + DependencyIndexes: file_proxy_tun_config_proto_depIdxs, + MessageInfos: file_proxy_tun_config_proto_msgTypes, + }.Build() + File_proxy_tun_config_proto = out.File + file_proxy_tun_config_proto_goTypes = nil + file_proxy_tun_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/proxy/tun/config.proto b/subproject/Xray-core-main/proxy/tun/config.proto new file mode 100644 index 00000000..24e5e527 --- /dev/null +++ b/subproject/Xray-core-main/proxy/tun/config.proto @@ -0,0 +1,13 @@ +syntax = "proto3"; + +package xray.proxy.tun; +option csharp_namespace = "Xray.Proxy.Tun"; +option go_package = "github.com/xtls/xray-core/proxy/tun"; +option java_package = "com.xray.proxy.tun"; +option java_multiple_files = true; + +message Config { + string name = 1; + uint32 MTU = 2; + uint32 user_level = 3; +} diff --git a/subproject/Xray-core-main/proxy/tun/handler.go b/subproject/Xray-core-main/proxy/tun/handler.go new file mode 100644 index 00000000..31fc43c6 --- /dev/null +++ b/subproject/Xray-core-main/proxy/tun/handler.go @@ -0,0 +1,168 @@ +package tun + +import ( + "context" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + c "github.com/xtls/xray-core/common/ctx" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/log" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/features/policy" + "github.com/xtls/xray-core/features/routing" + "github.com/xtls/xray-core/transport" + "github.com/xtls/xray-core/transport/internet/stat" +) + +// Handler is managing object that tie together tun interface, ip stack and dispatch connections to the routing +type Handler struct { + ctx context.Context + config *Config + stack Stack + policyManager policy.Manager + dispatcher routing.Dispatcher + tag string + sniffingRequest session.SniffingRequest +} + +// ConnectionHandler interface with the only method that stack is going to push new connections to +type ConnectionHandler interface { + HandleConnection(conn net.Conn, destination net.Destination) +} + +// Handler implements ConnectionHandler +var _ ConnectionHandler = (*Handler)(nil) + +func (t *Handler) policy() policy.Session { + p := t.policyManager.ForLevel(t.config.UserLevel) + return p +} + +// Init the Handler instance with necessary parameters +func (t *Handler) Init(ctx context.Context, pm policy.Manager, dispatcher routing.Dispatcher) error { + var err error + + // Retrieve tag and sniffing config from context (set by AlwaysOnInboundHandler) + if inbound := session.InboundFromContext(ctx); inbound != nil { + t.tag = inbound.Tag + } + if content := session.ContentFromContext(ctx); content != nil { + t.sniffingRequest = content.SniffingRequest + } + + t.ctx = core.ToBackgroundDetachedContext(ctx) + t.policyManager = pm + t.dispatcher = dispatcher + + tunName := t.config.Name + tunOptions := TunOptions{ + Name: tunName, + MTU: t.config.MTU, + } + tunInterface, err := NewTun(tunOptions) + if err != nil { + return err + } + + errors.LogInfo(t.ctx, tunName, " created") + + tunStackOptions := StackOptions{ + Tun: tunInterface, + IdleTimeout: pm.ForLevel(t.config.UserLevel).Timeouts.ConnectionIdle, + } + tunStack, err := NewStack(t.ctx, tunStackOptions, t) + if err != nil { + _ = tunInterface.Close() + return err + } + + err = tunStack.Start() + if err != nil { + _ = tunStack.Close() + _ = tunInterface.Close() + return err + } + + err = tunInterface.Start() + if err != nil { + _ = tunStack.Close() + _ = tunInterface.Close() + return err + } + + t.stack = tunStack + + errors.LogInfo(t.ctx, tunName, " up") + return nil +} + +// HandleConnection pass the connection coming from the ip stack to the routing dispatcher +func (t *Handler) HandleConnection(conn net.Conn, destination net.Destination) { + // when handling is done with any outcome, always signal back to the incoming connection + // to close, send completion packets back to the network, and cleanup + defer conn.Close() + + ctx, cancel := context.WithCancel(t.ctx) + defer cancel() + ctx = c.ContextWithID(ctx, session.NewID()) + + source := net.DestinationFromAddr(conn.RemoteAddr()) + inbound := session.Inbound{ + Name: "tun", + Tag: t.tag, + CanSpliceCopy: 3, + Source: source, + User: &protocol.MemoryUser{ + Level: t.config.UserLevel, + }, + } + + ctx = session.ContextWithInbound(ctx, &inbound) + ctx = session.ContextWithContent(ctx, &session.Content{ + SniffingRequest: t.sniffingRequest, + }) + ctx = session.SubContextFromMuxInbound(ctx) + + ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{ + From: inbound.Source, + To: destination, + Status: log.AccessAccepted, + Reason: "", + }) + errors.LogInfo(ctx, "processing from ", source, " to ", destination) + + link := &transport.Link{ + Reader: &buf.TimeoutWrapperReader{Reader: buf.NewReader(conn)}, + Writer: buf.NewWriter(conn), + } + if err := t.dispatcher.DispatchLink(ctx, destination, link); err != nil { + errors.LogError(ctx, errors.New("connection closed").Base(err)) + } +} + +// Network implements proxy.Inbound +// and exists only to comply to proxy interface, declaring it doesn't listen on any network, +// making the process not open any port for this inbound (input will be network interface) +func (t *Handler) Network() []net.Network { + return []net.Network{} +} + +// Process implements proxy.Inbound +// and exists only to comply to proxy interface, which should never get any inputs due to no listening ports +func (t *Handler) Process(ctx context.Context, network net.Network, conn stat.Connection, dispatcher routing.Dispatcher) error { + return nil +} + +func init() { + common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + t := &Handler{config: config.(*Config)} + err := core.RequireFeatures(ctx, func(pm policy.Manager, dispatcher routing.Dispatcher) error { + return t.Init(ctx, pm, dispatcher) + }) + return t, err + })) +} diff --git a/subproject/Xray-core-main/proxy/tun/stack.go b/subproject/Xray-core-main/proxy/tun/stack.go new file mode 100644 index 00000000..ba67a488 --- /dev/null +++ b/subproject/Xray-core-main/proxy/tun/stack.go @@ -0,0 +1,17 @@ +package tun + +import ( + "time" +) + +// Stack interface implement ip protocol stack, bridging raw network packets and data streams +type Stack interface { + Start() error + Close() error +} + +// StackOptions for the stack implementation +type StackOptions struct { + Tun Tun + IdleTimeout time.Duration +} diff --git a/subproject/Xray-core-main/proxy/tun/stack_gvisor.go b/subproject/Xray-core-main/proxy/tun/stack_gvisor.go new file mode 100644 index 00000000..8bcc4ebe --- /dev/null +++ b/subproject/Xray-core-main/proxy/tun/stack_gvisor.go @@ -0,0 +1,270 @@ +package tun + +import ( + "context" + "time" + + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "gvisor.dev/gvisor/pkg/buffer" + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/adapters/gonet" + "gvisor.dev/gvisor/pkg/tcpip/checksum" + "gvisor.dev/gvisor/pkg/tcpip/header" + "gvisor.dev/gvisor/pkg/tcpip/network/ipv4" + "gvisor.dev/gvisor/pkg/tcpip/network/ipv6" + "gvisor.dev/gvisor/pkg/tcpip/stack" + "gvisor.dev/gvisor/pkg/tcpip/transport/tcp" + "gvisor.dev/gvisor/pkg/tcpip/transport/udp" + "gvisor.dev/gvisor/pkg/waiter" +) + +const ( + defaultNIC tcpip.NICID = 1 + + tcpRXBufMinSize = tcp.MinBufferSize + tcpRXBufDefSize = tcp.DefaultSendBufferSize + tcpRXBufMaxSize = 8 << 20 // 8MiB + + tcpTXBufMinSize = tcp.MinBufferSize + tcpTXBufDefSize = tcp.DefaultReceiveBufferSize + tcpTXBufMaxSize = 6 << 20 // 6MiB +) + +// stackGVisor is ip stack implemented by gVisor package +type stackGVisor struct { + ctx context.Context + tun GVisorTun + idleTimeout time.Duration + handler *Handler + stack *stack.Stack + endpoint stack.LinkEndpoint +} + +// GVisorTun implements a bridge to connect gVisor ip stack to tun interface +type GVisorTun interface { + newEndpoint() (stack.LinkEndpoint, error) +} + +// NewStack builds new ip stack (using gVisor) +func NewStack(ctx context.Context, options StackOptions, handler *Handler) (Stack, error) { + gStack := &stackGVisor{ + ctx: ctx, + tun: options.Tun.(GVisorTun), + idleTimeout: options.IdleTimeout, + handler: handler, + } + + return gStack, nil +} + +// Start is called by Handler to bring stack to life +func (t *stackGVisor) Start() error { + linkEndpoint, err := t.tun.newEndpoint() + if err != nil { + return err + } + + ipStack, err := createStack(linkEndpoint) + if err != nil { + return err + } + + tcpForwarder := tcp.NewForwarder(ipStack, 0, 65535, func(r *tcp.ForwarderRequest) { + go func(r *tcp.ForwarderRequest) { + var wq waiter.Queue + var id = r.ID() + + // Perform a TCP three-way handshake. + ep, err := r.CreateEndpoint(&wq) + if err != nil { + errors.LogError(t.ctx, err.String()) + r.Complete(true) + return + } + + options := ep.SocketOptions() + options.SetKeepAlive(false) + options.SetReuseAddress(true) + options.SetReusePort(true) + + t.handler.HandleConnection( + gonet.NewTCPConn(&wq, ep), + // local address on the gVisor side is connection destination + net.TCPDestination(net.IPAddress(id.LocalAddress.AsSlice()), net.Port(id.LocalPort)), + ) + + // close the socket + ep.Close() + // send connection complete upstream + r.Complete(false) + }(r) + }) + ipStack.SetTransportProtocolHandler(tcp.ProtocolNumber, tcpForwarder.HandlePacket) + + // Use custom UDP packet handler, instead of strict gVisor forwarder, for FullCone NAT support + udpForwarder := newUdpConnectionHandler(t.handler.HandleConnection, t.writeRawUDPPacket) + ipStack.SetTransportProtocolHandler(udp.ProtocolNumber, func(id stack.TransportEndpointID, pkt *stack.PacketBuffer) bool { + data := pkt.Clone().Data().AsRange().ToSlice() + // if len(data) == 0 { + // return false + // } + // source/destination of the packet we process as incoming, on gVisor side are Remote/Local + // in other terms, src is the side behind tun, dst is the side behind gVisor + // this function handle packets passing from the tun to the gVisor, therefore the src/dst assignement + srcIP := net.IPAddress(id.RemoteAddress.AsSlice()) + dstIP := net.IPAddress(id.LocalAddress.AsSlice()) + if srcIP == nil || dstIP == nil { + errors.LogDebug(context.Background(), "drop udp with size ", len(data), " > invalid ip address ", id.RemoteAddress.AsSlice(), " ", id.LocalAddress.AsSlice()) + return true + } + src := net.UDPDestination(srcIP, net.Port(id.RemotePort)) + dst := net.UDPDestination(dstIP, net.Port(id.LocalPort)) + udpForwarder.HandlePacket(src, dst, data) + return true + }) + + t.stack = ipStack + t.endpoint = linkEndpoint + + return nil +} + +func (t *stackGVisor) writeRawUDPPacket(payload []byte, src net.Destination, dst net.Destination) error { + udpLen := header.UDPMinimumSize + len(payload) + srcIP := tcpip.AddrFromSlice(src.Address.IP()) + dstIP := tcpip.AddrFromSlice(dst.Address.IP()) + + // build packet with appropriate IP header size + isIPv4 := dst.Address.Family().IsIPv4() + ipHdrSize := header.IPv6MinimumSize + ipProtocol := header.IPv6ProtocolNumber + if isIPv4 { + ipHdrSize = header.IPv4MinimumSize + ipProtocol = header.IPv4ProtocolNumber + } + + pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{ + ReserveHeaderBytes: ipHdrSize + header.UDPMinimumSize, + Payload: buffer.MakeWithData(payload), + }) + defer pkt.DecRef() + + // Build UDP header + udpHdr := header.UDP(pkt.TransportHeader().Push(header.UDPMinimumSize)) + udpHdr.Encode(&header.UDPFields{ + SrcPort: uint16(src.Port), + DstPort: uint16(dst.Port), + Length: uint16(udpLen), + }) + + // Calculate and set UDP checksum + xsum := header.PseudoHeaderChecksum(header.UDPProtocolNumber, srcIP, dstIP, uint16(udpLen)) + udpHdr.SetChecksum(^udpHdr.CalculateChecksum(checksum.Checksum(payload, xsum))) + + // Build IP header + if isIPv4 { + ipHdr := header.IPv4(pkt.NetworkHeader().Push(header.IPv4MinimumSize)) + ipHdr.Encode(&header.IPv4Fields{ + TotalLength: uint16(header.IPv4MinimumSize + udpLen), + TTL: 64, + Protocol: uint8(header.UDPProtocolNumber), + SrcAddr: srcIP, + DstAddr: dstIP, + }) + ipHdr.SetChecksum(^ipHdr.CalculateChecksum()) + } else { + ipHdr := header.IPv6(pkt.NetworkHeader().Push(header.IPv6MinimumSize)) + ipHdr.Encode(&header.IPv6Fields{ + PayloadLength: uint16(udpLen), + TransportProtocol: header.UDPProtocolNumber, + HopLimit: 64, + SrcAddr: srcIP, + DstAddr: dstIP, + }) + } + + // dispatch the packet + err := t.stack.WriteRawPacket(defaultNIC, ipProtocol, buffer.MakeWithView(pkt.ToView())) + if err != nil { + return errors.New("failed to write raw udp packet back to stack", err) + } + + return nil +} + +// Close is called by Handler to shut down the stack +func (t *stackGVisor) Close() error { + if t.stack == nil { + return nil + } + t.endpoint.Attach(nil) + t.stack.Close() + for _, endpoint := range t.stack.CleanupEndpoints() { + endpoint.Abort() + } + + return nil +} + +// createStack configure gVisor ip stack +func createStack(ep stack.LinkEndpoint) (*stack.Stack, error) { + opts := stack.Options{ + NetworkProtocols: []stack.NetworkProtocolFactory{ipv4.NewProtocol, ipv6.NewProtocol}, + TransportProtocols: []stack.TransportProtocolFactory{tcp.NewProtocol, udp.NewProtocol}, + HandleLocal: false, + } + gStack := stack.New(opts) + + err := gStack.CreateNIC(defaultNIC, ep) + if err != nil { + return nil, errors.New(err.String()) + } + + gStack.SetRouteTable([]tcpip.Route{ + {Destination: header.IPv4EmptySubnet, NIC: defaultNIC}, + {Destination: header.IPv6EmptySubnet, NIC: defaultNIC}, + }) + + err = gStack.SetSpoofing(defaultNIC, true) + if err != nil { + return nil, errors.New(err.String()) + } + err = gStack.SetPromiscuousMode(defaultNIC, true) + if err != nil { + return nil, errors.New(err.String()) + } + + cOpt := tcpip.CongestionControlOption("cubic") + gStack.SetTransportProtocolOption(tcp.ProtocolNumber, &cOpt) + sOpt := tcpip.TCPSACKEnabled(true) + gStack.SetTransportProtocolOption(tcp.ProtocolNumber, &sOpt) + mOpt := tcpip.TCPModerateReceiveBufferOption(true) + gStack.SetTransportProtocolOption(tcp.ProtocolNumber, &mOpt) + + // Disable RACK/TLP loss recovery to fix connection stalls under high load + rOpt := tcpip.TCPRecovery(0) + gStack.SetTransportProtocolOption(tcp.ProtocolNumber, &rOpt) + + tcpRXBufOpt := tcpip.TCPReceiveBufferSizeRangeOption{ + Min: tcpRXBufMinSize, + Default: tcpRXBufDefSize, + Max: tcpRXBufMaxSize, + } + err = gStack.SetTransportProtocolOption(tcp.ProtocolNumber, &tcpRXBufOpt) + if err != nil { + return nil, errors.New(err.String()) + } + + tcpTXBufOpt := tcpip.TCPSendBufferSizeRangeOption{ + Min: tcpTXBufMinSize, + Default: tcpTXBufDefSize, + Max: tcpTXBufMaxSize, + } + err = gStack.SetTransportProtocolOption(tcp.ProtocolNumber, &tcpTXBufOpt) + if err != nil { + return nil, errors.New(err.String()) + } + + return gStack, nil +} diff --git a/subproject/Xray-core-main/proxy/tun/stack_gvisor_endpoint.go b/subproject/Xray-core-main/proxy/tun/stack_gvisor_endpoint.go new file mode 100644 index 00000000..31def35e --- /dev/null +++ b/subproject/Xray-core-main/proxy/tun/stack_gvisor_endpoint.go @@ -0,0 +1,155 @@ +package tun + +import ( + "context" + "errors" + + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/header" + "gvisor.dev/gvisor/pkg/tcpip/stack" +) + +var ErrQueueEmpty = errors.New("queue is empty") + +type GVisorDevice interface { + WritePacket(packet *stack.PacketBuffer) tcpip.Error + ReadPacket() (byte, *stack.PacketBuffer, error) + Wait() +} + +// LinkEndpoint implements GVisor stack.LinkEndpoint +var _ stack.LinkEndpoint = (*LinkEndpoint)(nil) + +type LinkEndpoint struct { + deviceMTU uint32 + device GVisorDevice + dispatcherCancel context.CancelFunc +} + +func (e *LinkEndpoint) MTU() uint32 { + return e.deviceMTU +} + +func (e *LinkEndpoint) SetMTU(_ uint32) { + // not Implemented, as it is not expected GVisor will be asking tun device to be modified +} + +func (e *LinkEndpoint) MaxHeaderLength() uint16 { + return 0 +} + +func (e *LinkEndpoint) LinkAddress() tcpip.LinkAddress { + return "" +} + +func (e *LinkEndpoint) SetLinkAddress(_ tcpip.LinkAddress) { + // not Implemented, as it is not expected GVisor will be asking tun device to be modified +} + +func (e *LinkEndpoint) Capabilities() stack.LinkEndpointCapabilities { + return stack.CapabilityRXChecksumOffload +} + +func (e *LinkEndpoint) Attach(dispatcher stack.NetworkDispatcher) { + if e.dispatcherCancel != nil { + e.dispatcherCancel() + e.dispatcherCancel = nil + } + + if dispatcher != nil { + ctx, cancel := context.WithCancel(context.Background()) + go e.dispatchLoop(ctx, dispatcher) + e.dispatcherCancel = cancel + } +} + +func (e *LinkEndpoint) IsAttached() bool { + return e.dispatcherCancel != nil +} + +func (e *LinkEndpoint) Wait() { + +} + +func (e *LinkEndpoint) ARPHardwareType() header.ARPHardwareType { + return header.ARPHardwareNone +} + +func (e *LinkEndpoint) AddHeader(buffer *stack.PacketBuffer) { + // tun interface doesn't have link layer header, it will be added by the OS +} + +func (e *LinkEndpoint) ParseHeader(ptr *stack.PacketBuffer) bool { + return true +} + +func (e *LinkEndpoint) Close() { + if e.dispatcherCancel != nil { + e.dispatcherCancel() + e.dispatcherCancel = nil + } +} + +func (e *LinkEndpoint) SetOnCloseAction(_ func()) { + +} + +func (e *LinkEndpoint) WritePackets(packetBufferList stack.PacketBufferList) (int, tcpip.Error) { + var n int + var err tcpip.Error + + for _, packetBuffer := range packetBufferList.AsSlice() { + err = e.device.WritePacket(packetBuffer) + if err != nil { + return n, &tcpip.ErrAborted{} + } + n++ + } + + return n, nil +} + +func (e *LinkEndpoint) dispatchLoop(ctx context.Context, dispatcher stack.NetworkDispatcher) { + var networkProtocolNumber tcpip.NetworkProtocolNumber + var version byte + var packet *stack.PacketBuffer + var err error + + for { + select { + case <-ctx.Done(): + return + default: + version, packet, err = e.device.ReadPacket() + // on "queue empty", ask device to yield slightly and continue + if errors.Is(err, ErrQueueEmpty) { + e.device.Wait() + continue + } + // stop dispatcher loop on any other interface failure + if err != nil { + e.Attach(nil) + return + } + + // extract network protocol number from the packet first byte + // (which is returned separately, since it is so incredibly hard to extract one byte from + // stack.PacketBuffer without additional memory allocation and full copying it back and forth) + switch version { + case 4: + networkProtocolNumber = header.IPv4ProtocolNumber + case 6: + networkProtocolNumber = header.IPv6ProtocolNumber + default: + // discard unknown network protocol packet + packet.DecRef() + continue + } + + // dispatch the buffer to the stack + dispatcher.DeliverNetworkPacket(networkProtocolNumber, packet) + // signal the buffer that it can be released + packet.DecRef() + } + } +} diff --git a/subproject/Xray-core-main/proxy/tun/tun.go b/subproject/Xray-core-main/proxy/tun/tun.go new file mode 100644 index 00000000..deb8511f --- /dev/null +++ b/subproject/Xray-core-main/proxy/tun/tun.go @@ -0,0 +1,13 @@ +package tun + +// Tun interface implements tun interface interaction +type Tun interface { + Start() error + Close() error +} + +// TunOptions for tun interface implementation +type TunOptions struct { + Name string + MTU uint32 +} diff --git a/subproject/Xray-core-main/proxy/tun/tun_android.go b/subproject/Xray-core-main/proxy/tun/tun_android.go new file mode 100644 index 00000000..eaa9339f --- /dev/null +++ b/subproject/Xray-core-main/proxy/tun/tun_android.go @@ -0,0 +1,58 @@ +//go:build android + +package tun + +import ( + "context" + "strconv" + + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/platform" + "golang.org/x/sys/unix" + "gvisor.dev/gvisor/pkg/tcpip/link/fdbased" + "gvisor.dev/gvisor/pkg/tcpip/stack" +) + +type AndroidTun struct { + tunFd int + options TunOptions +} + +// DefaultTun implements Tun +var _ Tun = (*AndroidTun)(nil) + +// DefaultTun implements GVisorTun +var _ GVisorTun = (*AndroidTun)(nil) + +// NewTun builds new tun interface handler +func NewTun(options TunOptions) (Tun, error) { + fd, err := strconv.Atoi(platform.NewEnvFlag(platform.TunFdKey).GetValue(func() string { return "0" })) + errors.LogInfo(context.Background(), "read Android Tun Fd ", fd, err) + + err = unix.SetNonblock(fd, true) + if err != nil { + _ = unix.Close(fd) + return nil, err + } + + return &AndroidTun{ + tunFd: fd, + options: options, + }, nil +} + +func (t *AndroidTun) Start() error { + return nil +} + +func (t *AndroidTun) Close() error { + return nil +} + +func (t *AndroidTun) newEndpoint() (stack.LinkEndpoint, error) { + return fdbased.New(&fdbased.Options{ + FDs: []int{t.tunFd}, + MTU: t.options.MTU, + RXChecksumOffload: true, + }) +} diff --git a/subproject/Xray-core-main/proxy/tun/tun_darwin.go b/subproject/Xray-core-main/proxy/tun/tun_darwin.go new file mode 100644 index 00000000..ad9f4783 --- /dev/null +++ b/subproject/Xray-core-main/proxy/tun/tun_darwin.go @@ -0,0 +1,354 @@ +//go:build darwin + +package tun + +import ( + "errors" + "fmt" + "net" + "net/netip" + "os" + "strconv" + "syscall" + "unsafe" + + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/platform" + "golang.org/x/sys/unix" + "gvisor.dev/gvisor/pkg/buffer" + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/stack" +) + +const ( + utunControlName = "com.apple.net.utun_control" + sysprotoControl = 2 + gateway = "169.254.10.1/30" + utunHeaderSize = 4 +) + +const ( + SIOCAIFADDR6 = 2155899162 // netinet6/in6_var.h + IN6_IFF_NODAD = 0x0020 // netinet6/in6_var.h + IN6_IFF_SECURED = 0x0400 // netinet6/in6_var.h + ND6_INFINITE_LIFETIME = 0xFFFFFFFF // netinet6/nd6.h +) + +//go:linkname procyield runtime.procyield +func procyield(cycles uint32) + +type DarwinTun struct { + tunFile *os.File + options TunOptions + ownsFd bool // true for macOS (we created the fd), false for iOS (fd from system) +} + +var _ Tun = (*DarwinTun)(nil) +var _ GVisorTun = (*DarwinTun)(nil) +var _ GVisorDevice = (*DarwinTun)(nil) + +func NewTun(options TunOptions) (Tun, error) { + // Check if fd is provided via environment (iOS mode) + fdStr := platform.NewEnvFlag(platform.TunFdKey).GetValue(func() string { return "" }) + if fdStr != "" { + // iOS: use provided fd from NetworkExtension + fd, err := strconv.Atoi(fdStr) + if err != nil { + return nil, err + } + + if err = unix.SetNonblock(fd, true); err != nil { + return nil, err + } + + return &DarwinTun{ + tunFile: os.NewFile(uintptr(fd), "utun"), + options: options, + ownsFd: false, + }, nil + } + + // macOS: create our own utun interface + tunFile, err := open(options.Name) + if err != nil { + return nil, err + } + + err = setup(options.Name, options.MTU) + if err != nil { + _ = tunFile.Close() + return nil, err + } + + return &DarwinTun{ + tunFile: tunFile, + options: options, + ownsFd: true, + }, nil +} + +func (t *DarwinTun) Start() error { + return nil +} + +func (t *DarwinTun) Close() error { + if t.ownsFd { + return t.tunFile.Close() + } + // iOS: don't close the fd, it's owned by NetworkExtension + return nil +} + +// WritePacket implements GVisorDevice method to write one packet to the tun device +func (t *DarwinTun) WritePacket(packet *stack.PacketBuffer) tcpip.Error { + // request memory to write from reusable buffer pool + b := buf.NewWithSize(int32(t.options.MTU) + utunHeaderSize) + defer b.Release() + + // prepare Darwin specific packet header + _, _ = b.Write([]byte{0x0, 0x0, 0x0, 0x0}) + // copy the bytes of slices that compose the packet into the allocated buffer + for _, packetElement := range packet.AsSlices() { + _, _ = b.Write(packetElement) + } + // fill Darwin specific header from the first raw packet byte, that we can access now + var family byte + switch b.Byte(4) >> 4 { + case 4: + family = unix.AF_INET + case 6: + family = unix.AF_INET6 + default: + return &tcpip.ErrAborted{} + } + b.SetByte(3, family) + + if _, err := t.tunFile.Write(b.Bytes()); err != nil { + if errors.Is(err, unix.EAGAIN) { + return &tcpip.ErrWouldBlock{} + } + return &tcpip.ErrAborted{} + } + return nil +} + +// ReadPacket implements GVisorDevice method to read one packet from the tun device +// It is expected that the method will not block, rather return ErrQueueEmpty when there is nothing on the line, +// which will make the stack call Wait which should implement desired push-back +func (t *DarwinTun) ReadPacket() (byte, *stack.PacketBuffer, error) { + // request memory to write from reusable buffer pool + b := buf.NewWithSize(int32(t.options.MTU) + utunHeaderSize) + + // read the bytes to the interface file + n, err := b.ReadFrom(t.tunFile) + if errors.Is(err, unix.EAGAIN) || errors.Is(err, unix.EINTR) { + b.Release() + return 0, nil, ErrQueueEmpty + } + if err != nil { + b.Release() + return 0, nil, err + } + + // discard empty or sub-empty packets + if n <= utunHeaderSize { + b.Release() + return 0, nil, ErrQueueEmpty + } + + // network protocol version from first byte of the raw packet, the one that follows Darwin specific header + version := b.Byte(utunHeaderSize) >> 4 + packetBuffer := buffer.MakeWithData(b.BytesFrom(utunHeaderSize)) + return version, stack.NewPacketBuffer(stack.PacketBufferOptions{ + Payload: packetBuffer, + IsForwardedPacket: true, + OnRelease: func() { + b.Release() + }, + }), nil +} + +// Wait some cpu cycles +func (t *DarwinTun) Wait() { + procyield(1) +} + +func (t *DarwinTun) newEndpoint() (stack.LinkEndpoint, error) { + return &LinkEndpoint{deviceMTU: t.options.MTU, device: t}, nil +} + +// open the interface, by creating new utunN if in the system and returning its file descriptor +func open(name string) (*os.File, error) { + ifIndex := -1 + _, err := fmt.Sscanf(name, "utun%d", &ifIndex) + if err != nil || ifIndex < 0 { + return nil, errors.New("interface name must be utunN, where N is a number, e.g. utun9, utun11 and so on") + } + + fd, err := unix.Socket(unix.AF_SYSTEM, unix.SOCK_DGRAM, sysprotoControl) + if err != nil { + return nil, err + } + + ctlInfo := &unix.CtlInfo{} + copy(ctlInfo.Name[:], utunControlName) + if err := unix.IoctlCtlInfo(fd, ctlInfo); err != nil { + _ = unix.Close(fd) + return nil, err + } + + sockaddr := &unix.SockaddrCtl{ + ID: ctlInfo.Id, + Unit: uint32(ifIndex) + 1, + } + if err := unix.Connect(fd, sockaddr); err != nil { + _ = unix.Close(fd) + return nil, err + } + + if err := unix.SetNonblock(fd, true); err != nil { + _ = unix.Close(fd) + return nil, err + } + + return os.NewFile(uintptr(fd), name), nil +} + +// setup the interface by name +func setup(name string, MTU uint32) error { + if err := setMTU(name, MTU); err != nil { + return err + } + + /* + * Darwin routing require tunnel type interface to have local and remote address, to be routable. + * To simplify inevitable task, assign the interface static ip address, which in current implementation + * is just some random ip from link-local pool, allowing to not bother about existing routing intersection. + */ + syntheticIP, _ := netip.ParsePrefix(gateway) + if err := setIPAddress(name, syntheticIP); err != nil { + return err + } + + return nil +} + +// setMTU sets MTU on the interface by given name +func setMTU(name string, mtu uint32) error { + socket, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0) + if err != nil { + return err + } + defer unix.Close(socket) + + ifr := unix.IfreqMTU{MTU: int32(mtu)} + copy(ifr.Name[:], name) + return unix.IoctlSetIfreqMTU(socket, &ifr) +} + +type ifAliasReq4 struct { + Name [unix.IFNAMSIZ]byte + Addr unix.RawSockaddrInet4 + Dstaddr unix.RawSockaddrInet4 + Mask unix.RawSockaddrInet4 +} + +type ifAliasReq6 struct { + Name [unix.IFNAMSIZ]byte + Addr unix.RawSockaddrInet6 + Dstaddr unix.RawSockaddrInet6 + Mask unix.RawSockaddrInet6 + Flags uint32 + Lifetime addrLifetime6 +} + +type addrLifetime6 struct { + Expire float64 + Preferred float64 + Vltime uint32 + Pltime uint32 +} + +// setIPAddress sets ipv4 and ipv6 addresses to the interface, required for the routing to work +func setIPAddress(name string, gateway netip.Prefix) error { + socket4, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0) + if err != nil { + return err + } + defer unix.Close(socket4) + + // assume local ip address is next one from the remote address + local4 := gateway.Addr().As4() + local4[3]++ + + // fill the configuration for ipv4 + ifReq4 := ifAliasReq4{ + Addr: unix.RawSockaddrInet4{ + Len: unix.SizeofSockaddrInet4, + Family: unix.AF_INET, + Addr: local4, + }, + Dstaddr: unix.RawSockaddrInet4{ + Len: unix.SizeofSockaddrInet4, + Family: unix.AF_INET, + Addr: gateway.Addr().As4(), + }, + Mask: unix.RawSockaddrInet4{ + Len: unix.SizeofSockaddrInet4, + Family: unix.AF_INET, + Addr: netip.MustParseAddr(net.IP(net.CIDRMask(gateway.Bits(), 32)).String()).As4(), + }, + } + copy(ifReq4.Name[:], name) + if err = ioctlPtr(socket4, unix.SIOCAIFADDR, unsafe.Pointer(&ifReq4)); err != nil { + return os.NewSyscallError("SIOCAIFADDR", err) + } + + socket6, err := unix.Socket(unix.AF_INET6, unix.SOCK_DGRAM, 0) + if err != nil { + return err + } + defer unix.Close(socket6) + + // link-local ipv6 address with suffix from ipv6 + local6 := netip.AddrFrom16([16]byte{0: 0xfe, 1: 0x80, 12: local4[0], 13: local4[1], 14: local4[2], 15: local4[3]}) + + // fill the configuration for ipv6 + // only link-local address without the destination is enough for it + ifReq6 := ifAliasReq6{ + Addr: unix.RawSockaddrInet6{ + Len: unix.SizeofSockaddrInet6, + Family: unix.AF_INET6, + Addr: local6.As16(), + }, + Mask: unix.RawSockaddrInet6{ + Len: unix.SizeofSockaddrInet6, + Family: unix.AF_INET6, + Addr: netip.MustParseAddr(net.IP(net.CIDRMask(64, 128)).String()).As16(), + }, + Flags: IN6_IFF_NODAD, + Lifetime: addrLifetime6{ + Vltime: ND6_INFINITE_LIFETIME, + Pltime: ND6_INFINITE_LIFETIME, + }, + } + // assign link-local ipv6 address to the interface. + // this will additionally trigger OS level autoconfiguration, which will result two different link-local + // addresses - the requested one, and autoconfigured one. + // this really has no known side effects, just look excessive. and actually considered pretty normal way to + // enable the ipv6 on the interface by macOS concepts. + copy(ifReq6.Name[:], name) + if err = ioctlPtr(socket6, SIOCAIFADDR6, unsafe.Pointer(&ifReq6)); err != nil { + return os.NewSyscallError("SIOCAIFADDR6", err) + } + + return nil +} + +func ioctlPtr(fd int, req uint, arg unsafe.Pointer) error { + _, _, errno := unix.Syscall(syscall.SYS_IOCTL, uintptr(fd), uintptr(req), uintptr(arg)) + if errno != 0 { + return errno + } + return nil +} diff --git a/subproject/Xray-core-main/proxy/tun/tun_default.go b/subproject/Xray-core-main/proxy/tun/tun_default.go new file mode 100644 index 00000000..ee061934 --- /dev/null +++ b/subproject/Xray-core-main/proxy/tun/tun_default.go @@ -0,0 +1,34 @@ +//go:build !linux && !windows && !android && !darwin + +package tun + +import ( + "github.com/xtls/xray-core/common/errors" + "gvisor.dev/gvisor/pkg/tcpip/stack" +) + +type DefaultTun struct { +} + +// DefaultTun implements Tun +var _ Tun = (*DefaultTun)(nil) + +// DefaultTun implements GVisorTun +var _ GVisorTun = (*DefaultTun)(nil) + +// NewTun builds new tun interface handler +func NewTun(options TunOptions) (Tun, error) { + return nil, errors.New("Tun is not supported on your platform") +} + +func (t *DefaultTun) Start() error { + return errors.New("Tun is not supported on your platform") +} + +func (t *DefaultTun) Close() error { + return errors.New("Tun is not supported on your platform") +} + +func (t *DefaultTun) newEndpoint() (stack.LinkEndpoint, error) { + return nil, errors.New("Tun is not supported on your platform") +} diff --git a/subproject/Xray-core-main/proxy/tun/tun_linux.go b/subproject/Xray-core-main/proxy/tun/tun_linux.go new file mode 100644 index 00000000..0813a216 --- /dev/null +++ b/subproject/Xray-core-main/proxy/tun/tun_linux.go @@ -0,0 +1,120 @@ +//go:build linux && !android + +package tun + +import ( + "github.com/vishvananda/netlink" + "golang.org/x/sys/unix" + "gvisor.dev/gvisor/pkg/tcpip/link/fdbased" + "gvisor.dev/gvisor/pkg/tcpip/stack" +) + +// LinuxTun is an object that handles tun network interface on linux +// current version is heavily stripped to do nothing more, +// then create a network interface, to be provided as file descriptor to gVisor ip stack +type LinuxTun struct { + tunFd int + tunLink netlink.Link + options TunOptions +} + +// LinuxTun implements Tun +var _ Tun = (*LinuxTun)(nil) + +// LinuxTun implements GVisorTun +var _ GVisorTun = (*LinuxTun)(nil) + +// NewTun builds new tun interface handler (linux specific) +func NewTun(options TunOptions) (Tun, error) { + tunFd, err := open(options.Name) + if err != nil { + return nil, err + } + + tunLink, err := setup(options.Name, int(options.MTU)) + if err != nil { + _ = unix.Close(tunFd) + return nil, err + } + + linuxTun := &LinuxTun{ + tunFd: tunFd, + tunLink: tunLink, + options: options, + } + + return linuxTun, nil +} + +// open the file that implements tun interface in the OS +func open(name string) (int, error) { + fd, err := unix.Open("/dev/net/tun", unix.O_RDWR, 0) + if err != nil { + return -1, err + } + + ifr, err := unix.NewIfreq(name) + if err != nil { + _ = unix.Close(fd) + return 0, err + } + + flags := unix.IFF_TUN | unix.IFF_NO_PI + ifr.SetUint16(uint16(flags)) + err = unix.IoctlIfreq(fd, unix.TUNSETIFF, ifr) + if err != nil { + _ = unix.Close(fd) + return 0, err + } + + err = unix.SetNonblock(fd, true) + if err != nil { + _ = unix.Close(fd) + return 0, err + } + + return fd, nil +} + +// setup the interface through netlink socket +func setup(name string, MTU int) (netlink.Link, error) { + tunLink, err := netlink.LinkByName(name) + if err != nil { + return nil, err + } + + err = netlink.LinkSetMTU(tunLink, MTU) + if err != nil { + _ = netlink.LinkSetDown(tunLink) + return nil, err + } + + return tunLink, nil +} + +// Start is called by handler to bring tun interface to life +func (t *LinuxTun) Start() error { + err := netlink.LinkSetUp(t.tunLink) + if err != nil { + return err + } + + return nil +} + +// Close is called to shut down the tun interface +func (t *LinuxTun) Close() error { + _ = netlink.LinkSetDown(t.tunLink) + _ = unix.Close(t.tunFd) + + return nil +} + +// newEndpoint builds new gVisor stack.LinkEndpoint from the tun interface file descriptor +func (t *LinuxTun) newEndpoint() (stack.LinkEndpoint, error) { + return fdbased.New(&fdbased.Options{ + FDs: []int{t.tunFd}, + MTU: t.options.MTU, + RXChecksumOffload: true, + }) +} diff --git a/subproject/Xray-core-main/proxy/tun/tun_windows.go b/subproject/Xray-core-main/proxy/tun/tun_windows.go new file mode 100644 index 00000000..8b5a09fa --- /dev/null +++ b/subproject/Xray-core-main/proxy/tun/tun_windows.go @@ -0,0 +1,147 @@ +//go:build windows + +package tun + +import ( + "crypto/md5" + "errors" + "unsafe" + + "golang.org/x/sys/windows" + "golang.zx2c4.com/wintun" + "gvisor.dev/gvisor/pkg/buffer" + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/stack" +) + +//go:linkname procyield runtime.procyield +func procyield(cycles uint32) + +// WindowsTun is an object that handles tun network interface on Windows +// current version is heavily stripped to do nothing more, +// then create a network interface, to be provided as endpoint to gVisor ip stack +type WindowsTun struct { + options TunOptions + adapter *wintun.Adapter + session wintun.Session + readWait windows.Handle + MTU uint32 +} + +// WindowsTun implements Tun +var _ Tun = (*WindowsTun)(nil) + +// WindowsTun implements GVisorTun +var _ GVisorTun = (*WindowsTun)(nil) + +// WindowsTun implements GVisorDevice +var _ GVisorDevice = (*WindowsTun)(nil) + +// NewTun creates a Wintun interface with the given name. Should a Wintun +// interface with the same name exist, it tried to be reused. +func NewTun(options TunOptions) (Tun, error) { + // instantiate wintun adapter + adapter, err := open(options.Name) + if err != nil { + return nil, err + } + + // start the interface with ring buffer capacity of 8 MiB + session, err := adapter.StartSession(0x800000) + if err != nil { + _ = adapter.Close() + return nil, err + } + + tun := &WindowsTun{ + options: options, + adapter: adapter, + session: session, + readWait: session.ReadWaitEvent(), + // there is currently no iphndl.dll support, which is the netlink library for windows + // so there is nowhere to change MTU for the Wintun interface, and we take its default value + MTU: wintun.PacketSizeMax, + } + + return tun, nil +} + +func open(name string) (*wintun.Adapter, error) { + // generate a deterministic GUID from the adapter name + id := md5.Sum([]byte(name)) + guid := (*windows.GUID)(unsafe.Pointer(&id[0])) + // try to open existing adapter by name + adapter, err := wintun.OpenAdapter(name) + if err == nil { + return adapter, nil + } + // try to create adapter anew + adapter, err = wintun.CreateAdapter(name, "Xray", guid) + if err == nil { + return adapter, nil + } + return nil, err +} + +func (t *WindowsTun) Start() error { + return nil +} + +func (t *WindowsTun) Close() error { + t.session.End() + _ = t.adapter.Close() + + return nil +} + +// WritePacket implements GVisorDevice method to write one packet to the tun device +func (t *WindowsTun) WritePacket(packetBuffer *stack.PacketBuffer) tcpip.Error { + // request buffer from Wintun + packet, err := t.session.AllocateSendPacket(packetBuffer.Size()) + if err != nil { + return &tcpip.ErrAborted{} + } + + // copy the bytes of slices that compose the packet into the allocated buffer + var index int + for _, packetElement := range packetBuffer.AsSlices() { + index += copy(packet[index:], packetElement) + } + + // signal Wintun to send that buffer as the packet + t.session.SendPacket(packet) + + return nil +} + +// ReadPacket implements GVisorDevice method to read one packet from the tun device +// It is expected that the method will not block, rather return ErrQueueEmpty when there is nothing on the line, +// which will make the stack call Wait which should implement desired push-back +func (t *WindowsTun) ReadPacket() (byte, *stack.PacketBuffer, error) { + packet, err := t.session.ReceivePacket() + if errors.Is(err, windows.ERROR_NO_MORE_ITEMS) { + return 0, nil, ErrQueueEmpty + } + if err != nil { + return 0, nil, err + } + + version := packet[0] >> 4 + packetBuffer := buffer.MakeWithView(buffer.NewViewWithData(packet)) + return version, stack.NewPacketBuffer(stack.PacketBufferOptions{ + Payload: packetBuffer, + IsForwardedPacket: true, + OnRelease: func() { + t.session.ReleaseReceivePacket(packet) + }, + }), nil +} + +func (t *WindowsTun) Wait() { + procyield(1) + _, _ = windows.WaitForSingleObject(t.readWait, windows.INFINITE) +} + +func (t *WindowsTun) newEndpoint() (stack.LinkEndpoint, error) { + return &LinkEndpoint{deviceMTU: t.options.MTU, device: t}, nil +} diff --git a/subproject/Xray-core-main/proxy/tun/udp_fullcone.go b/subproject/Xray-core-main/proxy/tun/udp_fullcone.go new file mode 100644 index 00000000..44612100 --- /dev/null +++ b/subproject/Xray-core-main/proxy/tun/udp_fullcone.go @@ -0,0 +1,186 @@ +package tun + +import ( + "context" + "io" + "sync" + "time" + + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" +) + +type packet struct { + data []byte + dest *net.Destination +} + +// sub-handler specifically for udp connections under main handler +type udpConnectionHandler struct { + sync.RWMutex + + udpConns map[net.Destination]*udpConn + + handleConnection func(conn net.Conn, dest net.Destination) + writePacket func(data []byte, src net.Destination, dst net.Destination) error +} + +func newUdpConnectionHandler(handleConnection func(conn net.Conn, dest net.Destination), writePacket func(data []byte, src net.Destination, dst net.Destination) error) *udpConnectionHandler { + handler := &udpConnectionHandler{ + udpConns: make(map[net.Destination]*udpConn), + handleConnection: handleConnection, + writePacket: writePacket, + } + + return handler +} + +// HandlePacket handles UDP packets coming from tun, to forward to the dispatcher +// this custom handler support FullCone NAT of returning packets, binding connection only by the source addr:port +func (u *udpConnectionHandler) HandlePacket(src net.Destination, dst net.Destination, data []byte) { + u.RLock() + conn, found := u.udpConns[src] + if found { + select { + case conn.egress <- &packet{ + data: data, + dest: &dst, + }: + default: + errors.LogDebug(context.Background(), "drop udp with size ", len(data), " to ", dst.NetAddr(), " original ", conn.dst.NetAddr(), " > queue full") + } + u.RUnlock() + return + } + u.RUnlock() + + u.Lock() + defer u.Unlock() + + conn, found = u.udpConns[src] + if !found { + egress := make(chan *packet, 1024) + conn = &udpConn{handler: u, egress: egress, src: src, dst: dst} + u.udpConns[src] = conn + + go u.handleConnection(conn, dst) + } + + // send packet data to the egress channel, if it has buffer, or discard + select { + case conn.egress <- &packet{ + data: data, + dest: &dst, + }: + default: + errors.LogDebug(context.Background(), "drop udp with size ", len(data), " to ", dst.NetAddr(), " original ", conn.dst.NetAddr(), " > queue full") + } +} + +func (u *udpConnectionHandler) connectionFinished(src net.Destination) { + u.Lock() + conn, found := u.udpConns[src] + if found { + delete(u.udpConns, src) + close(conn.egress) + } + u.Unlock() +} + +// udp connection abstraction +type udpConn struct { + handler *udpConnectionHandler + + egress chan *packet + src net.Destination + dst net.Destination +} + +func (c *udpConn) ReadMultiBuffer() (buf.MultiBuffer, error) { + for { + e, ok := <-c.egress + if !ok { + return nil, io.EOF + } + + b := buf.New() + + _, err := b.Write(e.data) + if err != nil { + errors.LogDebugInner(context.Background(), err, "drop udp with size ", len(e.data), " to ", e.dest.NetAddr(), " original ", c.dst.NetAddr()) + b.Release() + continue + } + + b.UDP = e.dest + + return buf.MultiBuffer{b}, nil + } +} + +// Read packets from the connection +func (c *udpConn) Read(p []byte) (int, error) { + e, ok := <-c.egress + if !ok { + return 0, io.EOF + } + n := copy(p, e.data) + if n != len(e.data) { + return 0, io.ErrShortBuffer + } + return n, nil +} + +func (c *udpConn) WriteMultiBuffer(mb buf.MultiBuffer) error { + for i, b := range mb { + dst := c.dst + if b.UDP != nil { + dst = *b.UDP + } + err := c.handler.writePacket(b.Bytes(), dst, c.src) + if err != nil { + buf.ReleaseMulti(mb[i:]) + return err + } + b.Release() + } + return nil +} + +// Write returning packets back +func (c *udpConn) Write(p []byte) (int, error) { + // sending packets back mean sending payload with source/destination reversed + err := c.handler.writePacket(p, c.dst, c.src) + if err != nil { + return 0, nil + } + + return len(p), nil +} + +func (c *udpConn) Close() error { + c.handler.connectionFinished(c.src) + + return nil +} + +func (c *udpConn) LocalAddr() net.Addr { + return c.dst.RawNetAddr() +} + +func (c *udpConn) RemoteAddr() net.Addr { + return c.src.RawNetAddr() +} + +func (c *udpConn) SetDeadline(t time.Time) error { + return nil +} + +func (c *udpConn) SetReadDeadline(t time.Time) error { + return nil +} + +func (c *udpConn) SetWriteDeadline(t time.Time) error { + return nil +} diff --git a/subproject/Xray-core-main/proxy/vless/account.go b/subproject/Xray-core-main/proxy/vless/account.go new file mode 100644 index 00000000..2f617149 --- /dev/null +++ b/subproject/Xray-core-main/proxy/vless/account.go @@ -0,0 +1,69 @@ +package vless + +import ( + "google.golang.org/protobuf/proto" + + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/uuid" +) + +// AsAccount implements protocol.Account.AsAccount(). +func (a *Account) AsAccount() (protocol.Account, error) { + id, err := uuid.ParseString(a.Id) + if err != nil { + return nil, errors.New("failed to parse ID").Base(err).AtError() + } + return &MemoryAccount{ + ID: protocol.NewID(id), + Flow: a.Flow, // needs parser here? + Encryption: a.Encryption, // needs parser here? + XorMode: a.XorMode, + Seconds: a.Seconds, + Padding: a.Padding, + Reverse: a.Reverse, + Testpre: a.Testpre, + Testseed: a.Testseed, + }, nil +} + +// MemoryAccount is an in-memory form of VLess account. +type MemoryAccount struct { + // ID of the account. + ID *protocol.ID + // Flow of the account. May be "xtls-rprx-vision". + Flow string + + Encryption string + XorMode uint32 + Seconds uint32 + Padding string + + Reverse *Reverse + + Testpre uint32 + Testseed []uint32 +} + +// Equals implements protocol.Account.Equals(). +func (a *MemoryAccount) Equals(account protocol.Account) bool { + vlessAccount, ok := account.(*MemoryAccount) + if !ok { + return false + } + return a.ID.Equals(vlessAccount.ID) +} + +func (a *MemoryAccount) ToProto() proto.Message { + return &Account{ + Id: a.ID.String(), + Flow: a.Flow, + Encryption: a.Encryption, + XorMode: a.XorMode, + Seconds: a.Seconds, + Padding: a.Padding, + Reverse: a.Reverse, + Testpre: a.Testpre, + Testseed: a.Testseed, + } +} diff --git a/subproject/Xray-core-main/proxy/vless/account.pb.go b/subproject/Xray-core-main/proxy/vless/account.pb.go new file mode 100644 index 00000000..a5c35be4 --- /dev/null +++ b/subproject/Xray-core-main/proxy/vless/account.pb.go @@ -0,0 +1,259 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: proxy/vless/account.proto + +package vless + +import ( + proxyman "github.com/xtls/xray-core/app/proxyman" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Reverse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Tag string `protobuf:"bytes,1,opt,name=tag,proto3" json:"tag,omitempty"` + Sniffing *proxyman.SniffingConfig `protobuf:"bytes,2,opt,name=sniffing,proto3" json:"sniffing,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Reverse) Reset() { + *x = Reverse{} + mi := &file_proxy_vless_account_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Reverse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Reverse) ProtoMessage() {} + +func (x *Reverse) ProtoReflect() protoreflect.Message { + mi := &file_proxy_vless_account_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Reverse.ProtoReflect.Descriptor instead. +func (*Reverse) Descriptor() ([]byte, []int) { + return file_proxy_vless_account_proto_rawDescGZIP(), []int{0} +} + +func (x *Reverse) GetTag() string { + if x != nil { + return x.Tag + } + return "" +} + +func (x *Reverse) GetSniffing() *proxyman.SniffingConfig { + if x != nil { + return x.Sniffing + } + return nil +} + +type Account struct { + state protoimpl.MessageState `protogen:"open.v1"` + // ID of the account, in the form of a UUID, e.g., "66ad4540-b58c-4ad2-9926-ea63445a9b57". + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + // Flow settings. May be "xtls-rprx-vision". + Flow string `protobuf:"bytes,2,opt,name=flow,proto3" json:"flow,omitempty"` + Encryption string `protobuf:"bytes,3,opt,name=encryption,proto3" json:"encryption,omitempty"` + XorMode uint32 `protobuf:"varint,4,opt,name=xorMode,proto3" json:"xorMode,omitempty"` + Seconds uint32 `protobuf:"varint,5,opt,name=seconds,proto3" json:"seconds,omitempty"` + Padding string `protobuf:"bytes,6,opt,name=padding,proto3" json:"padding,omitempty"` + Reverse *Reverse `protobuf:"bytes,7,opt,name=reverse,proto3" json:"reverse,omitempty"` + Testpre uint32 `protobuf:"varint,8,opt,name=testpre,proto3" json:"testpre,omitempty"` + Testseed []uint32 `protobuf:"varint,9,rep,packed,name=testseed,proto3" json:"testseed,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Account) Reset() { + *x = Account{} + mi := &file_proxy_vless_account_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Account) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Account) ProtoMessage() {} + +func (x *Account) ProtoReflect() protoreflect.Message { + mi := &file_proxy_vless_account_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Account.ProtoReflect.Descriptor instead. +func (*Account) Descriptor() ([]byte, []int) { + return file_proxy_vless_account_proto_rawDescGZIP(), []int{1} +} + +func (x *Account) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *Account) GetFlow() string { + if x != nil { + return x.Flow + } + return "" +} + +func (x *Account) GetEncryption() string { + if x != nil { + return x.Encryption + } + return "" +} + +func (x *Account) GetXorMode() uint32 { + if x != nil { + return x.XorMode + } + return 0 +} + +func (x *Account) GetSeconds() uint32 { + if x != nil { + return x.Seconds + } + return 0 +} + +func (x *Account) GetPadding() string { + if x != nil { + return x.Padding + } + return "" +} + +func (x *Account) GetReverse() *Reverse { + if x != nil { + return x.Reverse + } + return nil +} + +func (x *Account) GetTestpre() uint32 { + if x != nil { + return x.Testpre + } + return 0 +} + +func (x *Account) GetTestseed() []uint32 { + if x != nil { + return x.Testseed + } + return nil +} + +var File_proxy_vless_account_proto protoreflect.FileDescriptor + +const file_proxy_vless_account_proto_rawDesc = "" + + "\n" + + "\x19proxy/vless/account.proto\x12\x10xray.proxy.vless\x1a\x19app/proxyman/config.proto\"Z\n" + + "\aReverse\x12\x10\n" + + "\x03tag\x18\x01 \x01(\tR\x03tag\x12=\n" + + "\bsniffing\x18\x02 \x01(\v2!.xray.app.proxyman.SniffingConfigR\bsniffing\"\x86\x02\n" + + "\aAccount\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" + + "\x04flow\x18\x02 \x01(\tR\x04flow\x12\x1e\n" + + "\n" + + "encryption\x18\x03 \x01(\tR\n" + + "encryption\x12\x18\n" + + "\axorMode\x18\x04 \x01(\rR\axorMode\x12\x18\n" + + "\aseconds\x18\x05 \x01(\rR\aseconds\x12\x18\n" + + "\apadding\x18\x06 \x01(\tR\apadding\x123\n" + + "\areverse\x18\a \x01(\v2\x19.xray.proxy.vless.ReverseR\areverse\x12\x18\n" + + "\atestpre\x18\b \x01(\rR\atestpre\x12\x1a\n" + + "\btestseed\x18\t \x03(\rR\btestseedBR\n" + + "\x14com.xray.proxy.vlessP\x01Z%github.com/xtls/xray-core/proxy/vless\xaa\x02\x10Xray.Proxy.Vlessb\x06proto3" + +var ( + file_proxy_vless_account_proto_rawDescOnce sync.Once + file_proxy_vless_account_proto_rawDescData []byte +) + +func file_proxy_vless_account_proto_rawDescGZIP() []byte { + file_proxy_vless_account_proto_rawDescOnce.Do(func() { + file_proxy_vless_account_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proxy_vless_account_proto_rawDesc), len(file_proxy_vless_account_proto_rawDesc))) + }) + return file_proxy_vless_account_proto_rawDescData +} + +var file_proxy_vless_account_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_proxy_vless_account_proto_goTypes = []any{ + (*Reverse)(nil), // 0: xray.proxy.vless.Reverse + (*Account)(nil), // 1: xray.proxy.vless.Account + (*proxyman.SniffingConfig)(nil), // 2: xray.app.proxyman.SniffingConfig +} +var file_proxy_vless_account_proto_depIdxs = []int32{ + 2, // 0: xray.proxy.vless.Reverse.sniffing:type_name -> xray.app.proxyman.SniffingConfig + 0, // 1: xray.proxy.vless.Account.reverse:type_name -> xray.proxy.vless.Reverse + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_proxy_vless_account_proto_init() } +func file_proxy_vless_account_proto_init() { + if File_proxy_vless_account_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_proxy_vless_account_proto_rawDesc), len(file_proxy_vless_account_proto_rawDesc)), + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_proxy_vless_account_proto_goTypes, + DependencyIndexes: file_proxy_vless_account_proto_depIdxs, + MessageInfos: file_proxy_vless_account_proto_msgTypes, + }.Build() + File_proxy_vless_account_proto = out.File + file_proxy_vless_account_proto_goTypes = nil + file_proxy_vless_account_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/proxy/vless/account.proto b/subproject/Xray-core-main/proxy/vless/account.proto new file mode 100644 index 00000000..296ce9b7 --- /dev/null +++ b/subproject/Xray-core-main/proxy/vless/account.proto @@ -0,0 +1,31 @@ +syntax = "proto3"; + +package xray.proxy.vless; +option csharp_namespace = "Xray.Proxy.Vless"; +option go_package = "github.com/xtls/xray-core/proxy/vless"; +option java_package = "com.xray.proxy.vless"; +option java_multiple_files = true; + +import "app/proxyman/config.proto"; + +message Reverse { + string tag = 1; + xray.app.proxyman.SniffingConfig sniffing = 2; +} + +message Account { + // ID of the account, in the form of a UUID, e.g., "66ad4540-b58c-4ad2-9926-ea63445a9b57". + string id = 1; + // Flow settings. May be "xtls-rprx-vision". + string flow = 2; + + string encryption = 3; + uint32 xorMode = 4; + uint32 seconds = 5; + string padding = 6; + + Reverse reverse = 7; + + uint32 testpre = 8; + repeated uint32 testseed = 9; +} diff --git a/subproject/Xray-core-main/proxy/vless/encoding/addons.go b/subproject/Xray-core-main/proxy/vless/encoding/addons.go new file mode 100644 index 00000000..724de777 --- /dev/null +++ b/subproject/Xray-core-main/proxy/vless/encoding/addons.go @@ -0,0 +1,191 @@ +package encoding + +import ( + "context" + "io" + "net" + + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/proxy" + "github.com/xtls/xray-core/proxy/vless" + "google.golang.org/protobuf/proto" +) + +func EncodeHeaderAddons(buffer *buf.Buffer, addons *Addons) error { + switch addons.Flow { + case vless.XRV: + bytes, err := proto.Marshal(addons) + if err != nil { + return errors.New("failed to marshal addons protobuf value").Base(err) + } + if err := buffer.WriteByte(byte(len(bytes))); err != nil { + return errors.New("failed to write addons protobuf length").Base(err) + } + if _, err := buffer.Write(bytes); err != nil { + return errors.New("failed to write addons protobuf value").Base(err) + } + default: + if err := buffer.WriteByte(0); err != nil { + return errors.New("failed to write addons protobuf length").Base(err) + } + } + + return nil +} + +func DecodeHeaderAddons(buffer *buf.Buffer, reader io.Reader) (*Addons, error) { + addons := new(Addons) + buffer.Clear() + if _, err := buffer.ReadFullFrom(reader, 1); err != nil { + return nil, errors.New("failed to read addons protobuf length").Base(err) + } + + if length := int32(buffer.Byte(0)); length != 0 { + buffer.Clear() + if _, err := buffer.ReadFullFrom(reader, length); err != nil { + return nil, errors.New("failed to read addons protobuf value").Base(err) + } + + if err := proto.Unmarshal(buffer.Bytes(), addons); err != nil { + return nil, errors.New("failed to unmarshal addons protobuf value").Base(err) + } + + // Verification. + switch addons.Flow { + default: + } + } + + return addons, nil +} + +// EncodeBodyAddons returns a Writer that auto-encrypt content written by caller. +func EncodeBodyAddons(writer buf.Writer, request *protocol.RequestHeader, requestAddons *Addons, state *proxy.TrafficState, isUplink bool, context context.Context, conn net.Conn, ob *session.Outbound) buf.Writer { + if request.Command == protocol.RequestCommandUDP { + return NewMultiLengthPacketWriter(writer) + } + if requestAddons.Flow == vless.XRV { + return proxy.NewVisionWriter(writer, state, isUplink, context, conn, ob, request.User.Account.(*vless.MemoryAccount).Testseed) + } + return writer +} + +// DecodeBodyAddons returns a Reader from which caller can fetch decrypted body. +func DecodeBodyAddons(reader io.Reader, request *protocol.RequestHeader, addons *Addons) buf.Reader { + switch addons.Flow { + default: + if request.Command == protocol.RequestCommandUDP { + return NewLengthPacketReader(reader) + } + } + return buf.NewReader(reader) +} + +func NewMultiLengthPacketWriter(writer buf.Writer) *MultiLengthPacketWriter { + return &MultiLengthPacketWriter{ + Writer: writer, + } +} + +type MultiLengthPacketWriter struct { + buf.Writer +} + +func (w *MultiLengthPacketWriter) WriteMultiBuffer(mb buf.MultiBuffer) error { + defer buf.ReleaseMulti(mb) + mb2Write := make(buf.MultiBuffer, 0, len(mb)+1) + for _, b := range mb { + length := b.Len() + if length == 0 || length+2 > buf.Size { + continue + } + eb := buf.New() + if err := eb.WriteByte(byte(length >> 8)); err != nil { + eb.Release() + continue + } + if err := eb.WriteByte(byte(length)); err != nil { + eb.Release() + continue + } + if _, err := eb.Write(b.Bytes()); err != nil { + eb.Release() + continue + } + mb2Write = append(mb2Write, eb) + } + if mb2Write.IsEmpty() { + return nil + } + return w.Writer.WriteMultiBuffer(mb2Write) +} + +func NewLengthPacketWriter(writer io.Writer) *LengthPacketWriter { + return &LengthPacketWriter{ + Writer: writer, + cache: make([]byte, 0, 65536), + } +} + +type LengthPacketWriter struct { + io.Writer + cache []byte +} + +func (w *LengthPacketWriter) WriteMultiBuffer(mb buf.MultiBuffer) error { + length := mb.Len() // none of mb is nil + // fmt.Println("Write", length) + if length == 0 { + return nil + } + defer func() { + w.cache = w.cache[:0] + }() + w.cache = append(w.cache, byte(length>>8), byte(length)) + for i, b := range mb { + w.cache = append(w.cache, b.Bytes()...) + b.Release() + mb[i] = nil + } + if _, err := w.Write(w.cache); err != nil { + return errors.New("failed to write a packet").Base(err) + } + return nil +} + +func NewLengthPacketReader(reader io.Reader) *LengthPacketReader { + return &LengthPacketReader{ + Reader: reader, + cache: make([]byte, 2), + } +} + +type LengthPacketReader struct { + io.Reader + cache []byte +} + +func (r *LengthPacketReader) ReadMultiBuffer() (buf.MultiBuffer, error) { + if _, err := io.ReadFull(r.Reader, r.cache); err != nil { // maybe EOF + return nil, errors.New("failed to read packet length").Base(err) + } + length := int32(r.cache[0])<<8 | int32(r.cache[1]) + // fmt.Println("Read", length) + mb := make(buf.MultiBuffer, 0, length/buf.Size+1) + for length > 0 { + size := length + if size > buf.Size { + size = buf.Size + } + length -= size + b := buf.New() + if _, err := b.ReadFullFrom(r.Reader, size); err != nil { + return nil, errors.New("failed to read packet payload").Base(err) + } + mb = append(mb, b) + } + return mb, nil +} diff --git a/subproject/Xray-core-main/proxy/vless/encoding/addons.pb.go b/subproject/Xray-core-main/proxy/vless/encoding/addons.pb.go new file mode 100644 index 00000000..5113e4f3 --- /dev/null +++ b/subproject/Xray-core-main/proxy/vless/encoding/addons.pb.go @@ -0,0 +1,132 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: proxy/vless/encoding/addons.proto + +package encoding + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Addons struct { + state protoimpl.MessageState `protogen:"open.v1"` + Flow string `protobuf:"bytes,1,opt,name=Flow,proto3" json:"Flow,omitempty"` + Seed []byte `protobuf:"bytes,2,opt,name=Seed,proto3" json:"Seed,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Addons) Reset() { + *x = Addons{} + mi := &file_proxy_vless_encoding_addons_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Addons) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Addons) ProtoMessage() {} + +func (x *Addons) ProtoReflect() protoreflect.Message { + mi := &file_proxy_vless_encoding_addons_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Addons.ProtoReflect.Descriptor instead. +func (*Addons) Descriptor() ([]byte, []int) { + return file_proxy_vless_encoding_addons_proto_rawDescGZIP(), []int{0} +} + +func (x *Addons) GetFlow() string { + if x != nil { + return x.Flow + } + return "" +} + +func (x *Addons) GetSeed() []byte { + if x != nil { + return x.Seed + } + return nil +} + +var File_proxy_vless_encoding_addons_proto protoreflect.FileDescriptor + +const file_proxy_vless_encoding_addons_proto_rawDesc = "" + + "\n" + + "!proxy/vless/encoding/addons.proto\x12\x19xray.proxy.vless.encoding\"0\n" + + "\x06Addons\x12\x12\n" + + "\x04Flow\x18\x01 \x01(\tR\x04Flow\x12\x12\n" + + "\x04Seed\x18\x02 \x01(\fR\x04SeedBm\n" + + "\x1dcom.xray.proxy.vless.encodingP\x01Z.github.com/xtls/xray-core/proxy/vless/encoding\xaa\x02\x19Xray.Proxy.Vless.Encodingb\x06proto3" + +var ( + file_proxy_vless_encoding_addons_proto_rawDescOnce sync.Once + file_proxy_vless_encoding_addons_proto_rawDescData []byte +) + +func file_proxy_vless_encoding_addons_proto_rawDescGZIP() []byte { + file_proxy_vless_encoding_addons_proto_rawDescOnce.Do(func() { + file_proxy_vless_encoding_addons_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proxy_vless_encoding_addons_proto_rawDesc), len(file_proxy_vless_encoding_addons_proto_rawDesc))) + }) + return file_proxy_vless_encoding_addons_proto_rawDescData +} + +var file_proxy_vless_encoding_addons_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_proxy_vless_encoding_addons_proto_goTypes = []any{ + (*Addons)(nil), // 0: xray.proxy.vless.encoding.Addons +} +var file_proxy_vless_encoding_addons_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_proxy_vless_encoding_addons_proto_init() } +func file_proxy_vless_encoding_addons_proto_init() { + if File_proxy_vless_encoding_addons_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_proxy_vless_encoding_addons_proto_rawDesc), len(file_proxy_vless_encoding_addons_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_proxy_vless_encoding_addons_proto_goTypes, + DependencyIndexes: file_proxy_vless_encoding_addons_proto_depIdxs, + MessageInfos: file_proxy_vless_encoding_addons_proto_msgTypes, + }.Build() + File_proxy_vless_encoding_addons_proto = out.File + file_proxy_vless_encoding_addons_proto_goTypes = nil + file_proxy_vless_encoding_addons_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/proxy/vless/encoding/addons.proto b/subproject/Xray-core-main/proxy/vless/encoding/addons.proto new file mode 100644 index 00000000..3730e87f --- /dev/null +++ b/subproject/Xray-core-main/proxy/vless/encoding/addons.proto @@ -0,0 +1,12 @@ +syntax = "proto3"; + +package xray.proxy.vless.encoding; +option csharp_namespace = "Xray.Proxy.Vless.Encoding"; +option go_package = "github.com/xtls/xray-core/proxy/vless/encoding"; +option java_package = "com.xray.proxy.vless.encoding"; +option java_multiple_files = true; + +message Addons { + string Flow = 1; + bytes Seed = 2; +} diff --git a/subproject/Xray-core-main/proxy/vless/encoding/encoding.go b/subproject/Xray-core-main/proxy/vless/encoding/encoding.go new file mode 100644 index 00000000..6cbacd8d --- /dev/null +++ b/subproject/Xray-core-main/proxy/vless/encoding/encoding.go @@ -0,0 +1,204 @@ +package encoding + +import ( + "context" + "io" + + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/common/signal" + "github.com/xtls/xray-core/common/uuid" + "github.com/xtls/xray-core/proxy" + "github.com/xtls/xray-core/proxy/vless" +) + +const ( + Version = byte(0) +) + +var addrParser = protocol.NewAddressParser( + protocol.AddressFamilyByte(byte(protocol.AddressTypeIPv4), net.AddressFamilyIPv4), + protocol.AddressFamilyByte(byte(protocol.AddressTypeDomain), net.AddressFamilyDomain), + protocol.AddressFamilyByte(byte(protocol.AddressTypeIPv6), net.AddressFamilyIPv6), + protocol.PortThenAddress(), +) + +// EncodeRequestHeader writes encoded request header into the given writer. +func EncodeRequestHeader(writer io.Writer, request *protocol.RequestHeader, requestAddons *Addons) error { + buffer := buf.StackNew() + defer buffer.Release() + + if err := buffer.WriteByte(request.Version); err != nil { + return errors.New("failed to write request version").Base(err) + } + + if _, err := buffer.Write(request.User.Account.(*vless.MemoryAccount).ID.Bytes()); err != nil { + return errors.New("failed to write request user id").Base(err) + } + + if err := EncodeHeaderAddons(&buffer, requestAddons); err != nil { + return errors.New("failed to encode request header addons").Base(err) + } + + if err := buffer.WriteByte(byte(request.Command)); err != nil { + return errors.New("failed to write request command").Base(err) + } + + if request.Command != protocol.RequestCommandMux && request.Command != protocol.RequestCommandRvs { + if err := addrParser.WriteAddressPort(&buffer, request.Address, request.Port); err != nil { + return errors.New("failed to write request address and port").Base(err) + } + } + + if _, err := writer.Write(buffer.Bytes()); err != nil { + return errors.New("failed to write request header").Base(err) + } + + return nil +} + +// DecodeRequestHeader decodes and returns (if successful) a RequestHeader from an input stream. +func DecodeRequestHeader(isfb bool, first *buf.Buffer, reader io.Reader, validator vless.Validator) ([]byte, *protocol.RequestHeader, *Addons, bool, error) { + buffer := buf.StackNew() + defer buffer.Release() + + request := new(protocol.RequestHeader) + + if isfb { + request.Version = first.Byte(0) + } else { + if _, err := buffer.ReadFullFrom(reader, 1); err != nil { + return nil, nil, nil, false, errors.New("failed to read request version").Base(err) + } + request.Version = buffer.Byte(0) + } + + switch request.Version { + case 0: + + var id [16]byte + + if isfb { + copy(id[:], first.BytesRange(1, 17)) + } else { + buffer.Clear() + if _, err := buffer.ReadFullFrom(reader, 16); err != nil { + return nil, nil, nil, false, errors.New("failed to read request user id").Base(err) + } + copy(id[:], buffer.Bytes()) + } + + if request.User = validator.Get(id); request.User == nil { + u := uuid.UUID(id) + return nil, nil, nil, isfb, errors.New("invalid request user id: " + u.String()) + } + + if isfb { + first.Advance(17) + } + + requestAddons, err := DecodeHeaderAddons(&buffer, reader) + if err != nil { + return nil, nil, nil, false, errors.New("failed to decode request header addons").Base(err) + } + + buffer.Clear() + if _, err := buffer.ReadFullFrom(reader, 1); err != nil { + return nil, nil, nil, false, errors.New("failed to read request command").Base(err) + } + + request.Command = protocol.RequestCommand(buffer.Byte(0)) + switch request.Command { + case protocol.RequestCommandMux: + request.Address = net.DomainAddress("v1.mux.cool") + case protocol.RequestCommandRvs: + request.Address = net.DomainAddress("v1.rvs.cool") + case protocol.RequestCommandTCP, protocol.RequestCommandUDP: + if addr, port, err := addrParser.ReadAddressPort(&buffer, reader); err == nil { + request.Address = addr + request.Port = port + } + } + if request.Address == nil { + return nil, nil, nil, false, errors.New("invalid request address") + } + return id[:], request, requestAddons, false, nil + default: + return nil, nil, nil, isfb, errors.New("invalid request version") + } +} + +// EncodeResponseHeader writes encoded response header into the given writer. +func EncodeResponseHeader(writer io.Writer, request *protocol.RequestHeader, responseAddons *Addons) error { + buffer := buf.StackNew() + defer buffer.Release() + + if err := buffer.WriteByte(request.Version); err != nil { + return errors.New("failed to write response version").Base(err) + } + + if err := EncodeHeaderAddons(&buffer, responseAddons); err != nil { + return errors.New("failed to encode response header addons").Base(err) + } + + if _, err := writer.Write(buffer.Bytes()); err != nil { + return errors.New("failed to write response header").Base(err) + } + + return nil +} + +// DecodeResponseHeader decodes and returns (if successful) a ResponseHeader from an input stream. +func DecodeResponseHeader(reader io.Reader, request *protocol.RequestHeader) (*Addons, error) { + buffer := buf.StackNew() + defer buffer.Release() + + if _, err := buffer.ReadFullFrom(reader, 1); err != nil { + return nil, errors.New("failed to read response version").Base(err) + } + + if buffer.Byte(0) != request.Version { + return nil, errors.New("unexpected response version. Expecting ", int(request.Version), " but actually ", int(buffer.Byte(0))) + } + + responseAddons, err := DecodeHeaderAddons(&buffer, reader) + if err != nil { + return nil, errors.New("failed to decode response header addons").Base(err) + } + + return responseAddons, nil +} + +// XtlsRead can switch to splice copy +func XtlsRead(reader buf.Reader, writer buf.Writer, timer *signal.ActivityTimer, conn net.Conn, trafficState *proxy.TrafficState, isUplink bool, ctx context.Context) error { + err := func() error { + for { + if isUplink && trafficState.Inbound.UplinkReaderDirectCopy || !isUplink && trafficState.Outbound.DownlinkReaderDirectCopy { + var writerConn net.Conn + var inTimer *signal.ActivityTimer + if inbound := session.InboundFromContext(ctx); inbound != nil && inbound.Conn != nil { + writerConn = inbound.Conn + inTimer = inbound.Timer + } + return proxy.CopyRawConnIfExist(ctx, conn, writerConn, writer, timer, inTimer) + } + buffer, err := reader.ReadMultiBuffer() + if !buffer.IsEmpty() { + timer.Update() + if werr := writer.WriteMultiBuffer(buffer); werr != nil { + return werr + } + } + if err != nil { + return err + } + } + }() + if err != nil && errors.Cause(err) != io.EOF { + return err + } + return nil +} diff --git a/subproject/Xray-core-main/proxy/vless/encoding/encoding_test.go b/subproject/Xray-core-main/proxy/vless/encoding/encoding_test.go new file mode 100644 index 00000000..ae558865 --- /dev/null +++ b/subproject/Xray-core-main/proxy/vless/encoding/encoding_test.go @@ -0,0 +1,133 @@ +package encoding_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/uuid" + "github.com/xtls/xray-core/proxy/vless" + . "github.com/xtls/xray-core/proxy/vless/encoding" +) + +func toAccount(a *vless.Account) protocol.Account { + account, err := a.AsAccount() + common.Must(err) + return account +} + +func TestRequestSerialization(t *testing.T) { + user := &protocol.MemoryUser{ + Level: 0, + Email: "test@example.com", + } + id := uuid.New() + account := &vless.Account{ + Id: id.String(), + } + user.Account = toAccount(account) + + expectedRequest := &protocol.RequestHeader{ + Version: Version, + User: user, + Command: protocol.RequestCommandTCP, + Address: net.DomainAddress("www.example.com"), + Port: net.Port(443), + } + expectedAddons := &Addons{} + + buffer := buf.StackNew() + common.Must(EncodeRequestHeader(&buffer, expectedRequest, expectedAddons)) + + Validator := new(vless.MemoryValidator) + Validator.Add(user) + + _, actualRequest, actualAddons, _, err := DecodeRequestHeader(false, nil, &buffer, Validator) + common.Must(err) + + if r := cmp.Diff(actualRequest, expectedRequest, cmp.AllowUnexported(protocol.ID{})); r != "" { + t.Error(r) + } + + addonsComparer := func(x, y *Addons) bool { + return (x.Flow == y.Flow) && (cmp.Equal(x.Seed, y.Seed)) + } + if r := cmp.Diff(actualAddons, expectedAddons, cmp.Comparer(addonsComparer)); r != "" { + t.Error(r) + } +} + +func TestInvalidRequest(t *testing.T) { + user := &protocol.MemoryUser{ + Level: 0, + Email: "test@example.com", + } + id := uuid.New() + account := &vless.Account{ + Id: id.String(), + } + user.Account = toAccount(account) + + expectedRequest := &protocol.RequestHeader{ + Version: Version, + User: user, + Command: protocol.RequestCommand(100), + Address: net.DomainAddress("www.example.com"), + Port: net.Port(443), + } + expectedAddons := &Addons{} + + buffer := buf.StackNew() + common.Must(EncodeRequestHeader(&buffer, expectedRequest, expectedAddons)) + + Validator := new(vless.MemoryValidator) + Validator.Add(user) + + _, _, _, _, err := DecodeRequestHeader(false, nil, &buffer, Validator) + if err == nil { + t.Error("nil error") + } +} + +func TestMuxRequest(t *testing.T) { + user := &protocol.MemoryUser{ + Level: 0, + Email: "test@example.com", + } + id := uuid.New() + account := &vless.Account{ + Id: id.String(), + } + user.Account = toAccount(account) + + expectedRequest := &protocol.RequestHeader{ + Version: Version, + User: user, + Command: protocol.RequestCommandMux, + Address: net.DomainAddress("v1.mux.cool"), + } + expectedAddons := &Addons{} + + buffer := buf.StackNew() + common.Must(EncodeRequestHeader(&buffer, expectedRequest, expectedAddons)) + + Validator := new(vless.MemoryValidator) + Validator.Add(user) + + _, actualRequest, actualAddons, _, err := DecodeRequestHeader(false, nil, &buffer, Validator) + common.Must(err) + + if r := cmp.Diff(actualRequest, expectedRequest, cmp.AllowUnexported(protocol.ID{})); r != "" { + t.Error(r) + } + + addonsComparer := func(x, y *Addons) bool { + return (x.Flow == y.Flow) && (cmp.Equal(x.Seed, y.Seed)) + } + if r := cmp.Diff(actualAddons, expectedAddons, cmp.Comparer(addonsComparer)); r != "" { + t.Error(r) + } +} diff --git a/subproject/Xray-core-main/proxy/vless/encryption/client.go b/subproject/Xray-core-main/proxy/vless/encryption/client.go new file mode 100644 index 00000000..54959d0e --- /dev/null +++ b/subproject/Xray-core-main/proxy/vless/encryption/client.go @@ -0,0 +1,210 @@ +package encryption + +import ( + "crypto/cipher" + "crypto/ecdh" + "crypto/mlkem" + "crypto/rand" + "io" + "net" + "sync" + "time" + + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/protocol" + "lukechampine.com/blake3" +) + +type ClientInstance struct { + NfsPKeys []any + NfsPKeysBytes [][]byte + Hash32s [][32]byte + RelaysLength int + XorMode uint32 + Seconds uint32 + PaddingLens [][3]int + PaddingGaps [][3]int + + RWLock sync.RWMutex + Expire time.Time + PfsKey []byte + Ticket []byte +} + +func (i *ClientInstance) Init(nfsPKeysBytes [][]byte, xorMode, seconds uint32, padding string) (err error) { + if i.NfsPKeys != nil { + return errors.New("already initialized") + } + l := len(nfsPKeysBytes) + if l == 0 { + return errors.New("empty nfsPKeysBytes") + } + i.NfsPKeys = make([]any, l) + i.NfsPKeysBytes = nfsPKeysBytes + i.Hash32s = make([][32]byte, l) + for j, k := range nfsPKeysBytes { + if len(k) == 32 { + if i.NfsPKeys[j], err = ecdh.X25519().NewPublicKey(k); err != nil { + return + } + i.RelaysLength += 32 + 32 + } else { + if i.NfsPKeys[j], err = mlkem.NewEncapsulationKey768(k); err != nil { + return + } + i.RelaysLength += 1088 + 32 + } + i.Hash32s[j] = blake3.Sum256(k) + } + i.RelaysLength -= 32 + i.XorMode = xorMode + i.Seconds = seconds + return ParsePadding(padding, &i.PaddingLens, &i.PaddingGaps) +} + +func (i *ClientInstance) Handshake(conn net.Conn) (*CommonConn, error) { + if i.NfsPKeys == nil { + return nil, errors.New("uninitialized") + } + c := NewCommonConn(conn, protocol.HasAESGCMHardwareSupport) + + ivAndRealysLength := 16 + i.RelaysLength + pfsKeyExchangeLength := 18 + 1184 + 32 + 16 + paddingLength, paddingLens, paddingGaps := CreatPadding(i.PaddingLens, i.PaddingGaps) + clientHello := make([]byte, ivAndRealysLength+pfsKeyExchangeLength+paddingLength) + + iv := clientHello[:16] + rand.Read(iv) + relays := clientHello[16:ivAndRealysLength] + var nfsKey []byte + var lastCTR cipher.Stream + for j, k := range i.NfsPKeys { + var index = 32 + if k, ok := k.(*ecdh.PublicKey); ok { + privateKey, _ := ecdh.X25519().GenerateKey(rand.Reader) + copy(relays, privateKey.PublicKey().Bytes()) + var err error + nfsKey, err = privateKey.ECDH(k) + if err != nil { + return nil, err + } + } + if k, ok := k.(*mlkem.EncapsulationKey768); ok { + var ciphertext []byte + nfsKey, ciphertext = k.Encapsulate() + copy(relays, ciphertext) + index = 1088 + } + if i.XorMode > 0 { // this xor can (others can't) be recovered by client's config, revealing an X25519 public key / ML-KEM-768 ciphertext, that's why "native" values + NewCTR(i.NfsPKeysBytes[j], iv).XORKeyStream(relays, relays[:index]) // make X25519 public key / ML-KEM-768 ciphertext distinguishable from random bytes + } + if lastCTR != nil { + lastCTR.XORKeyStream(relays, relays[:32]) // make this relay irreplaceable + } + if j == len(i.NfsPKeys)-1 { + break + } + lastCTR = NewCTR(nfsKey, iv) + lastCTR.XORKeyStream(relays[index:], i.Hash32s[j+1][:]) + relays = relays[index+32:] + } + nfsAEAD := NewAEAD(iv, nfsKey, c.UseAES) + + if i.Seconds > 0 { + i.RWLock.RLock() + if time.Now().Before(i.Expire) { + c.Client = i + c.UnitedKey = append(i.PfsKey, nfsKey...) // different unitedKey for each connection + nfsAEAD.Seal(clientHello[:ivAndRealysLength], nil, EncodeLength(32), nil) + nfsAEAD.Seal(clientHello[:ivAndRealysLength+18], nil, i.Ticket, nil) + i.RWLock.RUnlock() + c.PreWrite = clientHello[:ivAndRealysLength+18+32] + c.AEAD = NewAEAD(clientHello[ivAndRealysLength+18:ivAndRealysLength+18+32], c.UnitedKey, c.UseAES) + if i.XorMode == 2 { + c.Conn = NewXorConn(conn, NewCTR(c.UnitedKey, iv), nil, len(c.PreWrite), 16) + } + return c, nil + } + i.RWLock.RUnlock() + } + + pfsKeyExchange := clientHello[ivAndRealysLength : ivAndRealysLength+pfsKeyExchangeLength] + nfsAEAD.Seal(pfsKeyExchange[:0], nil, EncodeLength(pfsKeyExchangeLength-18), nil) + mlkem768DKey, _ := mlkem.GenerateKey768() + x25519SKey, _ := ecdh.X25519().GenerateKey(rand.Reader) + pfsPublicKey := append(mlkem768DKey.EncapsulationKey().Bytes(), x25519SKey.PublicKey().Bytes()...) + nfsAEAD.Seal(pfsKeyExchange[:18], nil, pfsPublicKey, nil) + + padding := clientHello[ivAndRealysLength+pfsKeyExchangeLength:] + nfsAEAD.Seal(padding[:0], nil, EncodeLength(paddingLength-18), nil) + nfsAEAD.Seal(padding[:18], nil, padding[18:paddingLength-16], nil) + + paddingLens[0] = ivAndRealysLength + pfsKeyExchangeLength + paddingLens[0] + for i, l := range paddingLens { // sends padding in a fragmented way, to create variable traffic pattern, before inner VLESS flow takes control + if l > 0 { + if _, err := conn.Write(clientHello[:l]); err != nil { + return nil, err + } + clientHello = clientHello[l:] + } + if len(paddingGaps) > i { + time.Sleep(paddingGaps[i]) + } + } + + encryptedPfsPublicKey := make([]byte, 1088+32+16) + if _, err := io.ReadFull(conn, encryptedPfsPublicKey); err != nil { + return nil, err + } + nfsAEAD.Open(encryptedPfsPublicKey[:0], MaxNonce, encryptedPfsPublicKey, nil) + mlkem768Key, err := mlkem768DKey.Decapsulate(encryptedPfsPublicKey[:1088]) + if err != nil { + return nil, err + } + peerX25519PKey, err := ecdh.X25519().NewPublicKey(encryptedPfsPublicKey[1088 : 1088+32]) + if err != nil { + return nil, err + } + x25519Key, err := x25519SKey.ECDH(peerX25519PKey) + if err != nil { + return nil, err + } + pfsKey := make([]byte, 32+32) // no more capacity + copy(pfsKey, mlkem768Key) + copy(pfsKey[32:], x25519Key) + c.UnitedKey = append(pfsKey, nfsKey...) + c.AEAD = NewAEAD(pfsPublicKey, c.UnitedKey, c.UseAES) + c.PeerAEAD = NewAEAD(encryptedPfsPublicKey[:1088+32], c.UnitedKey, c.UseAES) + + encryptedTicket := make([]byte, 32) + if _, err := io.ReadFull(conn, encryptedTicket); err != nil { + return nil, err + } + if _, err := c.PeerAEAD.Open(encryptedTicket[:0], nil, encryptedTicket, nil); err != nil { + return nil, err + } + seconds := DecodeLength(encryptedTicket) + + if i.Seconds > 0 && seconds > 0 { + i.RWLock.Lock() + i.Expire = time.Now().Add(time.Duration(seconds) * time.Second) + i.PfsKey = pfsKey + i.Ticket = encryptedTicket[:16] + i.RWLock.Unlock() + } + + encryptedLength := make([]byte, 18) + if _, err := io.ReadFull(conn, encryptedLength); err != nil { + return nil, err + } + if _, err := c.PeerAEAD.Open(encryptedLength[:0], nil, encryptedLength, nil); err != nil { + return nil, err + } + length := DecodeLength(encryptedLength[:2]) + c.PeerPadding = make([]byte, length) // important: allows server sends padding slowly, eliminating 1-RTT's traffic pattern + + if i.XorMode == 2 { + c.Conn = NewXorConn(conn, NewCTR(c.UnitedKey, iv), NewCTR(c.UnitedKey, encryptedTicket[:16]), 0, length) + } + return c, nil +} diff --git a/subproject/Xray-core-main/proxy/vless/encryption/common.go b/subproject/Xray-core-main/proxy/vless/encryption/common.go new file mode 100644 index 00000000..a715a2c3 --- /dev/null +++ b/subproject/Xray-core-main/proxy/vless/encryption/common.go @@ -0,0 +1,280 @@ +package encryption + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "fmt" + "io" + "net" + "strconv" + "strings" + "sync" + "time" + + "github.com/xtls/xray-core/common/crypto" + "github.com/xtls/xray-core/common/errors" + "golang.org/x/crypto/chacha20poly1305" + "lukechampine.com/blake3" +) + +var OutBytesPool = sync.Pool{ + New: func() any { + return make([]byte, 5+8192+16) + }, +} + +type CommonConn struct { + net.Conn + UseAES bool + Client *ClientInstance + UnitedKey []byte + PreWrite []byte + AEAD *AEAD + PeerAEAD *AEAD + PeerPadding []byte + rawInput bytes.Buffer + input bytes.Reader +} + +func NewCommonConn(conn net.Conn, useAES bool) *CommonConn { + return &CommonConn{ + Conn: conn, + UseAES: useAES, + } +} + +func (c *CommonConn) Write(b []byte) (int, error) { + if len(b) == 0 { + return 0, nil + } + outBytes := OutBytesPool.Get().([]byte) + defer OutBytesPool.Put(outBytes) + for n := 0; n < len(b); { + b := b[n:] + if len(b) > 8192 { + b = b[:8192] // for avoiding another copy() in peer's Read() + } + n += len(b) + headerAndData := outBytes[:5+len(b)+16] + EncodeHeader(headerAndData, len(b)+16) + max := false + if bytes.Equal(c.AEAD.Nonce[:], MaxNonce) { + max = true + } + c.AEAD.Seal(headerAndData[:5], nil, b, headerAndData[:5]) + if max { + c.AEAD = NewAEAD(headerAndData, c.UnitedKey, c.UseAES) + } + if c.PreWrite != nil { + headerAndData = append(c.PreWrite, headerAndData...) + c.PreWrite = nil + } + if _, err := c.Conn.Write(headerAndData); err != nil { + return 0, err + } + } + return len(b), nil +} + +func (c *CommonConn) Read(b []byte) (int, error) { + if len(b) == 0 { + return 0, nil + } + if c.PeerAEAD == nil { // client's 0-RTT + serverRandom := make([]byte, 16) + if _, err := io.ReadFull(c.Conn, serverRandom); err != nil { + return 0, err + } + c.PeerAEAD = NewAEAD(serverRandom, c.UnitedKey, c.UseAES) + if xorConn, ok := c.Conn.(*XorConn); ok { + xorConn.PeerCTR = NewCTR(c.UnitedKey, serverRandom) + } + } + if c.PeerPadding != nil { // client's 1-RTT + if _, err := io.ReadFull(c.Conn, c.PeerPadding); err != nil { + return 0, err + } + if _, err := c.PeerAEAD.Open(c.PeerPadding[:0], nil, c.PeerPadding, nil); err != nil { + return 0, err + } + c.PeerPadding = nil + } + if c.input.Len() > 0 { + return c.input.Read(b) + } + peerHeader := [5]byte{} + if _, err := io.ReadFull(c.Conn, peerHeader[:]); err != nil { + return 0, err + } + l, err := DecodeHeader(peerHeader[:]) // l: 17~16640 + if err != nil { + if c.Client != nil && strings.Contains(err.Error(), "invalid header: ") { // client's 0-RTT + c.Client.RWLock.Lock() + if bytes.HasPrefix(c.UnitedKey, c.Client.PfsKey) { + c.Client.Expire = time.Now() // expired + } + c.Client.RWLock.Unlock() + return 0, errors.New("new handshake needed") + } + return 0, err + } + c.Client = nil + if c.rawInput.Cap() < l { + c.rawInput.Grow(l) // no need to use sync.Pool, because we are always reading + } + peerData := c.rawInput.Bytes()[:l] + if _, err := io.ReadFull(c.Conn, peerData); err != nil { + return 0, err + } + dst := peerData[:l-16] + if len(dst) <= len(b) { + dst = b[:len(dst)] // avoids another copy() + } + var newAEAD *AEAD + if bytes.Equal(c.PeerAEAD.Nonce[:], MaxNonce) { + newAEAD = NewAEAD(append(peerHeader[:], peerData...), c.UnitedKey, c.UseAES) + } + _, err = c.PeerAEAD.Open(dst[:0], nil, peerData, peerHeader[:]) + if newAEAD != nil { + c.PeerAEAD = newAEAD + } + if err != nil { + return 0, err + } + if len(dst) > len(b) { + c.input.Reset(dst[copy(b, dst):]) + dst = b // for len(dst) + } + return len(dst), nil +} + +type AEAD struct { + cipher.AEAD + Nonce [12]byte +} + +func NewAEAD(ctx, key []byte, useAES bool) *AEAD { + k := make([]byte, 32) + blake3.DeriveKey(k, string(ctx), key) + var aead cipher.AEAD + if useAES { + block, _ := aes.NewCipher(k) + aead, _ = cipher.NewGCM(block) + } else { + aead, _ = chacha20poly1305.New(k) + } + return &AEAD{AEAD: aead} +} + +func (a *AEAD) Seal(dst, nonce, plaintext, additionalData []byte) []byte { + if nonce == nil { + nonce = IncreaseNonce(a.Nonce[:]) + } + return a.AEAD.Seal(dst, nonce, plaintext, additionalData) +} + +func (a *AEAD) Open(dst, nonce, ciphertext, additionalData []byte) ([]byte, error) { + if nonce == nil { + nonce = IncreaseNonce(a.Nonce[:]) + } + return a.AEAD.Open(dst, nonce, ciphertext, additionalData) +} + +func IncreaseNonce(nonce []byte) []byte { + for i := range 12 { + nonce[11-i]++ + if nonce[11-i] != 0 { + break + } + } + return nonce +} + +var MaxNonce = bytes.Repeat([]byte{255}, 12) + +func EncodeLength(l int) []byte { + return []byte{byte(l >> 8), byte(l)} +} + +func DecodeLength(b []byte) int { + return int(b[0])<<8 | int(b[1]) +} + +func EncodeHeader(h []byte, l int) { + h[0] = 23 + h[1] = 3 + h[2] = 3 + h[3] = byte(l >> 8) + h[4] = byte(l) +} + +func DecodeHeader(h []byte) (l int, err error) { + l = int(h[3])<<8 | int(h[4]) + if h[0] != 23 || h[1] != 3 || h[2] != 3 { + l = 0 + } + if l < 17 || l > 16640 { // TLS 1.3 max record: 16384 + 256 (RFC 8446 §5.2) + err = errors.New("invalid header: " + fmt.Sprintf("%v", h[:5])) // DO NOT CHANGE: relied by client's Read() + } + return +} + +func ParsePadding(padding string, paddingLens, paddingGaps *[][3]int) (err error) { + if padding == "" { + return + } + maxLen := 0 + for i, s := range strings.Split(padding, ".") { + x := strings.Split(s, "-") + if len(x) < 3 || x[0] == "" || x[1] == "" || x[2] == "" { + return errors.New("invalid padding lenth/gap parameter: " + s) + } + y := [3]int{} + if y[0], err = strconv.Atoi(x[0]); err != nil { + return + } + if y[1], err = strconv.Atoi(x[1]); err != nil { + return + } + if y[2], err = strconv.Atoi(x[2]); err != nil { + return + } + if i == 0 && (y[0] < 100 || y[1] < 18+17 || y[2] < 18+17) { + return errors.New("first padding length must not be smaller than 35") + } + if i%2 == 0 { + *paddingLens = append(*paddingLens, y) + maxLen += max(y[1], y[2]) + } else { + *paddingGaps = append(*paddingGaps, y) + } + } + if maxLen > 18+65535 { + return errors.New("total padding length must not be larger than 65553") + } + return +} + +func CreatPadding(paddingLens, paddingGaps [][3]int) (length int, lens []int, gaps []time.Duration) { + if len(paddingLens) == 0 { + paddingLens = [][3]int{{100, 111, 1111}, {50, 0, 3333}} + paddingGaps = [][3]int{{75, 0, 111}} + } + for _, y := range paddingLens { + l := 0 + if y[0] >= int(crypto.RandBetween(0, 100)) { + l = int(crypto.RandBetween(int64(y[1]), int64(y[2]))) + } + lens = append(lens, l) + length += l + } + for _, y := range paddingGaps { + g := 0 + if y[0] >= int(crypto.RandBetween(0, 100)) { + g = int(crypto.RandBetween(int64(y[1]), int64(y[2]))) + } + gaps = append(gaps, time.Duration(g)*time.Millisecond) + } + return +} diff --git a/subproject/Xray-core-main/proxy/vless/encryption/server.go b/subproject/Xray-core-main/proxy/vless/encryption/server.go new file mode 100644 index 00000000..e1d73716 --- /dev/null +++ b/subproject/Xray-core-main/proxy/vless/encryption/server.go @@ -0,0 +1,328 @@ +package encryption + +import ( + "bytes" + "crypto/cipher" + "crypto/ecdh" + "crypto/mlkem" + "crypto/rand" + "fmt" + "io" + "net" + "sync" + "time" + + "github.com/xtls/xray-core/common/crypto" + "github.com/xtls/xray-core/common/errors" + "lukechampine.com/blake3" +) + +type ServerSession struct { + PfsKey []byte + NfsKeys sync.Map +} + +type ServerInstance struct { + NfsSKeys []any + NfsPKeysBytes [][]byte + Hash32s [][32]byte + RelaysLength int + XorMode uint32 + SecondsFrom int64 + SecondsTo int64 + PaddingLens [][3]int + PaddingGaps [][3]int + + RWLock sync.RWMutex + Closed bool + Lasts map[int64][16]byte + Tickets [][16]byte + Sessions map[[16]byte]*ServerSession +} + +func (i *ServerInstance) Init(nfsSKeysBytes [][]byte, xorMode uint32, secondsFrom, secondsTo int64, padding string) (err error) { + if i.NfsSKeys != nil { + return errors.New("already initialized") + } + l := len(nfsSKeysBytes) + if l == 0 { + return errors.New("empty nfsSKeysBytes") + } + i.NfsSKeys = make([]any, l) + i.NfsPKeysBytes = make([][]byte, l) + i.Hash32s = make([][32]byte, l) + for j, k := range nfsSKeysBytes { + if len(k) == 32 { + if i.NfsSKeys[j], err = ecdh.X25519().NewPrivateKey(k); err != nil { + return + } + i.NfsPKeysBytes[j] = i.NfsSKeys[j].(*ecdh.PrivateKey).PublicKey().Bytes() + i.RelaysLength += 32 + 32 + } else { + if i.NfsSKeys[j], err = mlkem.NewDecapsulationKey768(k); err != nil { + return + } + i.NfsPKeysBytes[j] = i.NfsSKeys[j].(*mlkem.DecapsulationKey768).EncapsulationKey().Bytes() + i.RelaysLength += 1088 + 32 + } + i.Hash32s[j] = blake3.Sum256(i.NfsPKeysBytes[j]) + } + i.RelaysLength -= 32 + i.XorMode = xorMode + i.SecondsFrom = secondsFrom + i.SecondsTo = secondsTo + err = ParsePadding(padding, &i.PaddingLens, &i.PaddingGaps) + if err != nil { + return + } + if i.SecondsFrom > 0 || i.SecondsTo > 0 { + i.Lasts = make(map[int64][16]byte) + i.Tickets = make([][16]byte, 0, 1024) + i.Sessions = make(map[[16]byte]*ServerSession) + go func() { + for { + time.Sleep(time.Minute) + i.RWLock.Lock() + if i.Closed { + i.RWLock.Unlock() + return + } + minute := time.Now().Unix() / 60 + last := i.Lasts[minute] + delete(i.Lasts, minute) + delete(i.Lasts, minute-1) // for insurance + if last != [16]byte{} { + for j, ticket := range i.Tickets { + delete(i.Sessions, ticket) + if ticket == last { + i.Tickets = i.Tickets[j+1:] + break + } + } + } + i.RWLock.Unlock() + } + }() + } + return +} + +func (i *ServerInstance) Close() (err error) { + i.RWLock.Lock() + i.Closed = true + i.RWLock.Unlock() + return +} + +func (i *ServerInstance) Handshake(conn net.Conn, fallback *[]byte) (*CommonConn, error) { + if i.NfsSKeys == nil { + return nil, errors.New("uninitialized") + } + c := NewCommonConn(conn, true) + + ivAndRelays := make([]byte, 16+i.RelaysLength) + if _, err := io.ReadFull(conn, ivAndRelays); err != nil { + return nil, err + } + if fallback != nil { + *fallback = append(*fallback, ivAndRelays...) + } + iv := ivAndRelays[:16] + relays := ivAndRelays[16:] + var nfsKey []byte + var lastCTR cipher.Stream + for j, k := range i.NfsSKeys { + if lastCTR != nil { + lastCTR.XORKeyStream(relays, relays[:32]) // recover this relay + } + var index = 32 + if _, ok := k.(*mlkem.DecapsulationKey768); ok { + index = 1088 + } + if i.XorMode > 0 { + NewCTR(i.NfsPKeysBytes[j], iv).XORKeyStream(relays, relays[:index]) // we don't use buggy elligator2, because we have PSK :) + } + if k, ok := k.(*ecdh.PrivateKey); ok { + publicKey, err := ecdh.X25519().NewPublicKey(relays[:index]) + if err != nil { + return nil, err + } + if publicKey.Bytes()[31] > 127 { // we just don't want the observer can change even one bit without breaking the connection, though it has nothing to do with security + return nil, errors.New("the highest bit of the last byte of the peer-sent X25519 public key is not 0") + } + nfsKey, err = k.ECDH(publicKey) + if err != nil { + return nil, err + } + } + if k, ok := k.(*mlkem.DecapsulationKey768); ok { + var err error + nfsKey, err = k.Decapsulate(relays[:index]) + if err != nil { + return nil, err + } + } + if j == len(i.NfsSKeys)-1 { + break + } + relays = relays[index:] + lastCTR = NewCTR(nfsKey, iv) + lastCTR.XORKeyStream(relays, relays[:32]) + if !bytes.Equal(relays[:32], i.Hash32s[j+1][:]) { + return nil, errors.New("unexpected hash32: ", fmt.Sprintf("%v", relays[:32])) + } + relays = relays[32:] + } + nfsAEAD := NewAEAD(iv, nfsKey, c.UseAES) + + encryptedLength := make([]byte, 18) + if _, err := io.ReadFull(conn, encryptedLength); err != nil { + return nil, err + } + if fallback != nil { + *fallback = append(*fallback, encryptedLength...) + } + decryptedLength := make([]byte, 2) + if _, err := nfsAEAD.Open(decryptedLength[:0], nil, encryptedLength, nil); err != nil { + c.UseAES = !c.UseAES + nfsAEAD = NewAEAD(iv, nfsKey, c.UseAES) + if _, err := nfsAEAD.Open(decryptedLength[:0], nil, encryptedLength, nil); err != nil { + return nil, err + } + } + if fallback != nil { + *fallback = nil + } + length := DecodeLength(decryptedLength) + + if length == 32 { + if i.SecondsFrom == 0 && i.SecondsTo == 0 { + return nil, errors.New("0-RTT is not allowed") + } + encryptedTicket := make([]byte, 32) + if _, err := io.ReadFull(conn, encryptedTicket); err != nil { + return nil, err + } + ticket, err := nfsAEAD.Open(nil, nil, encryptedTicket, nil) + if err != nil { + return nil, err + } + i.RWLock.RLock() + s := i.Sessions[[16]byte(ticket)] + i.RWLock.RUnlock() + if s == nil { + noises := make([]byte, crypto.RandBetween(1279, 2279)) // matches 1-RTT's server hello length for "random", though it is not important, just for example + var err error + for err == nil { + rand.Read(noises) + _, err = DecodeHeader(noises) + } + conn.Write(noises) // make client do new handshake + return nil, errors.New("expired ticket") + } + if _, loaded := s.NfsKeys.LoadOrStore([32]byte(nfsKey), true); loaded { // prevents bad client also + return nil, errors.New("replay detected") + } + c.UnitedKey = append(s.PfsKey, nfsKey...) // the same nfsKey links the upload & download (prevents server -> client's another request) + c.PreWrite = make([]byte, 16) + rand.Read(c.PreWrite) // always trust yourself, not the client (also prevents being parsed as TLS thus causing false interruption for "native" and "xorpub") + c.AEAD = NewAEAD(c.PreWrite, c.UnitedKey, c.UseAES) + c.PeerAEAD = NewAEAD(encryptedTicket, c.UnitedKey, c.UseAES) // unchangeable ctx (prevents server -> server), and different ctx length for upload / download (prevents client -> client) + if i.XorMode == 2 { + c.Conn = NewXorConn(conn, NewCTR(c.UnitedKey, c.PreWrite), NewCTR(c.UnitedKey, iv), 16, 0) // it doesn't matter if the attacker sends client's iv back to the client + } + return c, nil + } + + if length < 1184+32+16 { // client may send more public keys in the future's version + return nil, errors.New("too short length") + } + encryptedPfsPublicKey := make([]byte, length) + if _, err := io.ReadFull(conn, encryptedPfsPublicKey); err != nil { + return nil, err + } + if _, err := nfsAEAD.Open(encryptedPfsPublicKey[:0], nil, encryptedPfsPublicKey, nil); err != nil { + return nil, err + } + mlkem768EKey, err := mlkem.NewEncapsulationKey768(encryptedPfsPublicKey[:1184]) + if err != nil { + return nil, err + } + mlkem768Key, encapsulatedPfsKey := mlkem768EKey.Encapsulate() + peerX25519PKey, err := ecdh.X25519().NewPublicKey(encryptedPfsPublicKey[1184 : 1184+32]) + if err != nil { + return nil, err + } + x25519SKey, _ := ecdh.X25519().GenerateKey(rand.Reader) + x25519Key, err := x25519SKey.ECDH(peerX25519PKey) + if err != nil { + return nil, err + } + pfsKey := make([]byte, 32+32) // no more capacity + copy(pfsKey, mlkem768Key) + copy(pfsKey[32:], x25519Key) + pfsPublicKey := append(encapsulatedPfsKey, x25519SKey.PublicKey().Bytes()...) + c.UnitedKey = append(pfsKey, nfsKey...) + c.AEAD = NewAEAD(pfsPublicKey, c.UnitedKey, c.UseAES) + c.PeerAEAD = NewAEAD(encryptedPfsPublicKey[:1184+32], c.UnitedKey, c.UseAES) + + ticket := [16]byte{} + rand.Read(ticket[:]) + var seconds int64 + if i.SecondsTo == 0 { + seconds = i.SecondsFrom * crypto.RandBetween(50, 100) / 100 + } else { + seconds = crypto.RandBetween(i.SecondsFrom, i.SecondsTo) + } + copy(ticket[:], EncodeLength(int(seconds))) + if seconds > 0 { + i.RWLock.Lock() + i.Lasts[(time.Now().Unix()+max(i.SecondsFrom, i.SecondsTo))/60+2] = ticket + i.Tickets = append(i.Tickets, ticket) + i.Sessions[ticket] = &ServerSession{PfsKey: pfsKey} + i.RWLock.Unlock() + } + + pfsKeyExchangeLength := 1088 + 32 + 16 + encryptedTicketLength := 32 + paddingLength, paddingLens, paddingGaps := CreatPadding(i.PaddingLens, i.PaddingGaps) + serverHello := make([]byte, pfsKeyExchangeLength+encryptedTicketLength+paddingLength) + nfsAEAD.Seal(serverHello[:0], MaxNonce, pfsPublicKey, nil) + c.AEAD.Seal(serverHello[:pfsKeyExchangeLength], nil, ticket[:], nil) + padding := serverHello[pfsKeyExchangeLength+encryptedTicketLength:] + c.AEAD.Seal(padding[:0], nil, EncodeLength(paddingLength-18), nil) + c.AEAD.Seal(padding[:18], nil, padding[18:paddingLength-16], nil) + + paddingLens[0] = pfsKeyExchangeLength + encryptedTicketLength + paddingLens[0] + for i, l := range paddingLens { // sends padding in a fragmented way, to create variable traffic pattern, before inner VLESS flow takes control + if l > 0 { + if _, err := conn.Write(serverHello[:l]); err != nil { + return nil, err + } + serverHello = serverHello[l:] + } + if len(paddingGaps) > i { + time.Sleep(paddingGaps[i]) + } + } + + // important: allows client sends padding slowly, eliminating 1-RTT's traffic pattern + if _, err := io.ReadFull(conn, encryptedLength); err != nil { + return nil, err + } + if _, err := nfsAEAD.Open(encryptedLength[:0], nil, encryptedLength, nil); err != nil { + return nil, err + } + encryptedPadding := make([]byte, DecodeLength(encryptedLength[:2])) + if _, err := io.ReadFull(conn, encryptedPadding); err != nil { + return nil, err + } + if _, err := nfsAEAD.Open(encryptedPadding[:0], nil, encryptedPadding, nil); err != nil { + return nil, err + } + + if i.XorMode == 2 { + c.Conn = NewXorConn(conn, NewCTR(c.UnitedKey, ticket[:]), NewCTR(c.UnitedKey, iv), 0, 0) + } + return c, nil +} diff --git a/subproject/Xray-core-main/proxy/vless/encryption/xor.go b/subproject/Xray-core-main/proxy/vless/encryption/xor.go new file mode 100644 index 00000000..e435cb5c --- /dev/null +++ b/subproject/Xray-core-main/proxy/vless/encryption/xor.go @@ -0,0 +1,93 @@ +package encryption + +import ( + "crypto/aes" + "crypto/cipher" + "net" + + "lukechampine.com/blake3" +) + +func NewCTR(key, iv []byte) cipher.Stream { + k := make([]byte, 32) + blake3.DeriveKey(k, "VLESS", key) // avoids using key directly + block, _ := aes.NewCipher(k) + return cipher.NewCTR(block, iv) + //chacha20.NewUnauthenticatedCipher() +} + +type XorConn struct { + net.Conn + CTR cipher.Stream + PeerCTR cipher.Stream + OutSkip int + OutHeader []byte + InSkip int + InHeader []byte +} + +func NewXorConn(conn net.Conn, ctr, peerCTR cipher.Stream, outSkip, inSkip int) *XorConn { + return &XorConn{ + Conn: conn, + CTR: ctr, + PeerCTR: peerCTR, + OutSkip: outSkip, + OutHeader: make([]byte, 0, 5), // important + InSkip: inSkip, + InHeader: make([]byte, 0, 5), // important + } +} + +func (c *XorConn) Write(b []byte) (int, error) { + if len(b) == 0 { + return 0, nil + } + for p := b; ; { + if len(p) <= c.OutSkip { + c.OutSkip -= len(p) + break + } + p = p[c.OutSkip:] + c.OutSkip = 0 + need := 5 - len(c.OutHeader) + if len(p) < need { + c.OutHeader = append(c.OutHeader, p...) + c.CTR.XORKeyStream(p, p) + break + } + c.OutSkip, _ = DecodeHeader(append(c.OutHeader, p[:need]...)) + c.OutHeader = c.OutHeader[:0] + c.CTR.XORKeyStream(p[:need], p[:need]) + p = p[need:] + } + if _, err := c.Conn.Write(b); err != nil { + return 0, err + } + return len(b), nil +} + +func (c *XorConn) Read(b []byte) (int, error) { + if len(b) == 0 { + return 0, nil + } + n, err := c.Conn.Read(b) + for p := b[:n]; ; { + if len(p) <= c.InSkip { + c.InSkip -= len(p) + break + } + p = p[c.InSkip:] + c.InSkip = 0 + need := 5 - len(c.InHeader) + if len(p) < need { + c.PeerCTR.XORKeyStream(p, p) + c.InHeader = append(c.InHeader, p...) + break + } + c.PeerCTR.XORKeyStream(p[:need], p[:need]) + c.InSkip, _ = DecodeHeader(append(c.InHeader, p[:need]...)) + c.InHeader = c.InHeader[:0] + p = p[need:] + } + return n, err +} diff --git a/subproject/Xray-core-main/proxy/vless/inbound/config.go b/subproject/Xray-core-main/proxy/vless/inbound/config.go new file mode 100644 index 00000000..71c65bf6 --- /dev/null +++ b/subproject/Xray-core-main/proxy/vless/inbound/config.go @@ -0,0 +1 @@ +package inbound diff --git a/subproject/Xray-core-main/proxy/vless/inbound/config.pb.go b/subproject/Xray-core-main/proxy/vless/inbound/config.pb.go new file mode 100644 index 00000000..e65249a3 --- /dev/null +++ b/subproject/Xray-core-main/proxy/vless/inbound/config.pb.go @@ -0,0 +1,276 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: proxy/vless/inbound/config.proto + +package inbound + +import ( + protocol "github.com/xtls/xray-core/common/protocol" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Fallback struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Alpn string `protobuf:"bytes,2,opt,name=alpn,proto3" json:"alpn,omitempty"` + Path string `protobuf:"bytes,3,opt,name=path,proto3" json:"path,omitempty"` + Type string `protobuf:"bytes,4,opt,name=type,proto3" json:"type,omitempty"` + Dest string `protobuf:"bytes,5,opt,name=dest,proto3" json:"dest,omitempty"` + Xver uint64 `protobuf:"varint,6,opt,name=xver,proto3" json:"xver,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Fallback) Reset() { + *x = Fallback{} + mi := &file_proxy_vless_inbound_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Fallback) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Fallback) ProtoMessage() {} + +func (x *Fallback) ProtoReflect() protoreflect.Message { + mi := &file_proxy_vless_inbound_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Fallback.ProtoReflect.Descriptor instead. +func (*Fallback) Descriptor() ([]byte, []int) { + return file_proxy_vless_inbound_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Fallback) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Fallback) GetAlpn() string { + if x != nil { + return x.Alpn + } + return "" +} + +func (x *Fallback) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *Fallback) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *Fallback) GetDest() string { + if x != nil { + return x.Dest + } + return "" +} + +func (x *Fallback) GetXver() uint64 { + if x != nil { + return x.Xver + } + return 0 +} + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + Clients []*protocol.User `protobuf:"bytes,1,rep,name=clients,proto3" json:"clients,omitempty"` + Fallbacks []*Fallback `protobuf:"bytes,2,rep,name=fallbacks,proto3" json:"fallbacks,omitempty"` + Decryption string `protobuf:"bytes,3,opt,name=decryption,proto3" json:"decryption,omitempty"` + XorMode uint32 `protobuf:"varint,4,opt,name=xorMode,proto3" json:"xorMode,omitempty"` + SecondsFrom int64 `protobuf:"varint,5,opt,name=seconds_from,json=secondsFrom,proto3" json:"seconds_from,omitempty"` + SecondsTo int64 `protobuf:"varint,6,opt,name=seconds_to,json=secondsTo,proto3" json:"seconds_to,omitempty"` + Padding string `protobuf:"bytes,7,opt,name=padding,proto3" json:"padding,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_proxy_vless_inbound_config_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_proxy_vless_inbound_config_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_proxy_vless_inbound_config_proto_rawDescGZIP(), []int{1} +} + +func (x *Config) GetClients() []*protocol.User { + if x != nil { + return x.Clients + } + return nil +} + +func (x *Config) GetFallbacks() []*Fallback { + if x != nil { + return x.Fallbacks + } + return nil +} + +func (x *Config) GetDecryption() string { + if x != nil { + return x.Decryption + } + return "" +} + +func (x *Config) GetXorMode() uint32 { + if x != nil { + return x.XorMode + } + return 0 +} + +func (x *Config) GetSecondsFrom() int64 { + if x != nil { + return x.SecondsFrom + } + return 0 +} + +func (x *Config) GetSecondsTo() int64 { + if x != nil { + return x.SecondsTo + } + return 0 +} + +func (x *Config) GetPadding() string { + if x != nil { + return x.Padding + } + return "" +} + +var File_proxy_vless_inbound_config_proto protoreflect.FileDescriptor + +const file_proxy_vless_inbound_config_proto_rawDesc = "" + + "\n" + + " proxy/vless/inbound/config.proto\x12\x18xray.proxy.vless.inbound\x1a\x1acommon/protocol/user.proto\"\x82\x01\n" + + "\bFallback\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x12\n" + + "\x04alpn\x18\x02 \x01(\tR\x04alpn\x12\x12\n" + + "\x04path\x18\x03 \x01(\tR\x04path\x12\x12\n" + + "\x04type\x18\x04 \x01(\tR\x04type\x12\x12\n" + + "\x04dest\x18\x05 \x01(\tR\x04dest\x12\x12\n" + + "\x04xver\x18\x06 \x01(\x04R\x04xver\"\x96\x02\n" + + "\x06Config\x124\n" + + "\aclients\x18\x01 \x03(\v2\x1a.xray.common.protocol.UserR\aclients\x12@\n" + + "\tfallbacks\x18\x02 \x03(\v2\".xray.proxy.vless.inbound.FallbackR\tfallbacks\x12\x1e\n" + + "\n" + + "decryption\x18\x03 \x01(\tR\n" + + "decryption\x12\x18\n" + + "\axorMode\x18\x04 \x01(\rR\axorMode\x12!\n" + + "\fseconds_from\x18\x05 \x01(\x03R\vsecondsFrom\x12\x1d\n" + + "\n" + + "seconds_to\x18\x06 \x01(\x03R\tsecondsTo\x12\x18\n" + + "\apadding\x18\a \x01(\tR\apaddingBj\n" + + "\x1ccom.xray.proxy.vless.inboundP\x01Z-github.com/xtls/xray-core/proxy/vless/inbound\xaa\x02\x18Xray.Proxy.Vless.Inboundb\x06proto3" + +var ( + file_proxy_vless_inbound_config_proto_rawDescOnce sync.Once + file_proxy_vless_inbound_config_proto_rawDescData []byte +) + +func file_proxy_vless_inbound_config_proto_rawDescGZIP() []byte { + file_proxy_vless_inbound_config_proto_rawDescOnce.Do(func() { + file_proxy_vless_inbound_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proxy_vless_inbound_config_proto_rawDesc), len(file_proxy_vless_inbound_config_proto_rawDesc))) + }) + return file_proxy_vless_inbound_config_proto_rawDescData +} + +var file_proxy_vless_inbound_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_proxy_vless_inbound_config_proto_goTypes = []any{ + (*Fallback)(nil), // 0: xray.proxy.vless.inbound.Fallback + (*Config)(nil), // 1: xray.proxy.vless.inbound.Config + (*protocol.User)(nil), // 2: xray.common.protocol.User +} +var file_proxy_vless_inbound_config_proto_depIdxs = []int32{ + 2, // 0: xray.proxy.vless.inbound.Config.clients:type_name -> xray.common.protocol.User + 0, // 1: xray.proxy.vless.inbound.Config.fallbacks:type_name -> xray.proxy.vless.inbound.Fallback + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_proxy_vless_inbound_config_proto_init() } +func file_proxy_vless_inbound_config_proto_init() { + if File_proxy_vless_inbound_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_proxy_vless_inbound_config_proto_rawDesc), len(file_proxy_vless_inbound_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_proxy_vless_inbound_config_proto_goTypes, + DependencyIndexes: file_proxy_vless_inbound_config_proto_depIdxs, + MessageInfos: file_proxy_vless_inbound_config_proto_msgTypes, + }.Build() + File_proxy_vless_inbound_config_proto = out.File + file_proxy_vless_inbound_config_proto_goTypes = nil + file_proxy_vless_inbound_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/proxy/vless/inbound/config.proto b/subproject/Xray-core-main/proxy/vless/inbound/config.proto new file mode 100644 index 00000000..5a7e3e19 --- /dev/null +++ b/subproject/Xray-core-main/proxy/vless/inbound/config.proto @@ -0,0 +1,29 @@ +syntax = "proto3"; + +package xray.proxy.vless.inbound; +option csharp_namespace = "Xray.Proxy.Vless.Inbound"; +option go_package = "github.com/xtls/xray-core/proxy/vless/inbound"; +option java_package = "com.xray.proxy.vless.inbound"; +option java_multiple_files = true; + +import "common/protocol/user.proto"; + +message Fallback { + string name = 1; + string alpn = 2; + string path = 3; + string type = 4; + string dest = 5; + uint64 xver = 6; +} + +message Config { + repeated xray.common.protocol.User clients = 1; + repeated Fallback fallbacks = 2; + + string decryption = 3; + uint32 xorMode = 4; + int64 seconds_from = 5; + int64 seconds_to = 6; + string padding = 7; +} diff --git a/subproject/Xray-core-main/proxy/vless/inbound/inbound.go b/subproject/Xray-core-main/proxy/vless/inbound/inbound.go new file mode 100644 index 00000000..02dc947d --- /dev/null +++ b/subproject/Xray-core-main/proxy/vless/inbound/inbound.go @@ -0,0 +1,698 @@ +package inbound + +import ( + "bytes" + "context" + gotls "crypto/tls" + "encoding/base64" + "io" + "reflect" + "strconv" + "strings" + "time" + "unsafe" + + "github.com/xtls/xray-core/app/dispatcher" + "github.com/xtls/xray-core/app/reverse" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/log" + "github.com/xtls/xray-core/common/mux" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/retry" + "github.com/xtls/xray-core/common/serial" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/common/signal" + "github.com/xtls/xray-core/common/task" + "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/features" + "github.com/xtls/xray-core/features/dns" + "github.com/xtls/xray-core/features/extension" + feature_inbound "github.com/xtls/xray-core/features/inbound" + "github.com/xtls/xray-core/features/outbound" + "github.com/xtls/xray-core/features/policy" + "github.com/xtls/xray-core/features/routing" + "github.com/xtls/xray-core/features/stats" + "github.com/xtls/xray-core/proxy" + "github.com/xtls/xray-core/proxy/vless" + "github.com/xtls/xray-core/proxy/vless/encoding" + "github.com/xtls/xray-core/proxy/vless/encryption" + "github.com/xtls/xray-core/transport" + "github.com/xtls/xray-core/transport/internet/reality" + "github.com/xtls/xray-core/transport/internet/stat" + "github.com/xtls/xray-core/transport/internet/tls" +) + +func init() { + common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + var dc dns.Client + if err := core.RequireFeatures(ctx, func(d dns.Client) error { + dc = d + return nil + }); err != nil { + return nil, err + } + + c := config.(*Config) + + validator := new(vless.MemoryValidator) + for _, user := range c.Clients { + u, err := user.ToMemoryUser() + if err != nil { + return nil, errors.New("failed to get VLESS user").Base(err).AtError() + } + if err := validator.Add(u); err != nil { + return nil, errors.New("failed to initiate user").Base(err).AtError() + } + } + + return New(ctx, c, dc, validator) + })) +} + +// Handler is an inbound connection handler that handles messages in VLess protocol. +type Handler struct { + inboundHandlerManager feature_inbound.Manager + policyManager policy.Manager + stats stats.Manager + validator vless.Validator + decryption *encryption.ServerInstance + outboundHandlerManager outbound.Manager + observer features.Feature + defaultDispatcher routing.Dispatcher + ctx context.Context + fallbacks map[string]map[string]map[string]*Fallback // or nil + // regexps map[string]*regexp.Regexp // or nil +} + +// New creates a new VLess inbound handler. +func New(ctx context.Context, config *Config, dc dns.Client, validator vless.Validator) (*Handler, error) { + v := core.MustFromContext(ctx) + handler := &Handler{ + inboundHandlerManager: v.GetFeature(feature_inbound.ManagerType()).(feature_inbound.Manager), + policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager), + stats: v.GetFeature(stats.ManagerType()).(stats.Manager), + validator: validator, + outboundHandlerManager: v.GetFeature(outbound.ManagerType()).(outbound.Manager), + observer: v.GetFeature(extension.ObservatoryType()), + defaultDispatcher: v.GetFeature(routing.DispatcherType()).(routing.Dispatcher), + ctx: ctx, + } + + if config.Decryption != "" && config.Decryption != "none" { + s := strings.Split(config.Decryption, ".") + var nfsSKeysBytes [][]byte + for _, r := range s { + b, _ := base64.RawURLEncoding.DecodeString(r) + nfsSKeysBytes = append(nfsSKeysBytes, b) + } + handler.decryption = &encryption.ServerInstance{} + if err := handler.decryption.Init(nfsSKeysBytes, config.XorMode, config.SecondsFrom, config.SecondsTo, config.Padding); err != nil { + return nil, errors.New("failed to use decryption").Base(err).AtError() + } + } + + if config.Fallbacks != nil { + handler.fallbacks = make(map[string]map[string]map[string]*Fallback) + // handler.regexps = make(map[string]*regexp.Regexp) + for _, fb := range config.Fallbacks { + if handler.fallbacks[fb.Name] == nil { + handler.fallbacks[fb.Name] = make(map[string]map[string]*Fallback) + } + if handler.fallbacks[fb.Name][fb.Alpn] == nil { + handler.fallbacks[fb.Name][fb.Alpn] = make(map[string]*Fallback) + } + handler.fallbacks[fb.Name][fb.Alpn][fb.Path] = fb + /* + if fb.Path != "" { + if r, err := regexp.Compile(fb.Path); err != nil { + return nil, errors.New("invalid path regexp").Base(err).AtError() + } else { + handler.regexps[fb.Path] = r + } + } + */ + } + if handler.fallbacks[""] != nil { + for name, apfb := range handler.fallbacks { + if name != "" { + for alpn := range handler.fallbacks[""] { + if apfb[alpn] == nil { + apfb[alpn] = make(map[string]*Fallback) + } + } + } + } + } + for _, apfb := range handler.fallbacks { + if apfb[""] != nil { + for alpn, pfb := range apfb { + if alpn != "" { // && alpn != "h2" { + for path, fb := range apfb[""] { + if pfb[path] == nil { + pfb[path] = fb + } + } + } + } + } + } + if handler.fallbacks[""] != nil { + for name, apfb := range handler.fallbacks { + if name != "" { + for alpn, pfb := range handler.fallbacks[""] { + for path, fb := range pfb { + if apfb[alpn][path] == nil { + apfb[alpn][path] = fb + } + } + } + } + } + } + } + + return handler, nil +} + +func isMuxAndNotXUDP(request *protocol.RequestHeader, first *buf.Buffer) bool { + if request.Command != protocol.RequestCommandMux { + return false + } + if first.Len() < 7 { + return true + } + firstBytes := first.Bytes() + return !(firstBytes[2] == 0 && // ID high + firstBytes[3] == 0 && // ID low + firstBytes[6] == 2) // Network type: UDP +} + +func (h *Handler) GetReverse(a *vless.MemoryAccount) (*Reverse, error) { + u := h.validator.Get(a.ID.UUID()) + if u == nil { + return nil, errors.New("reverse: user " + a.ID.String() + " doesn't exist anymore") + } + a = u.Account.(*vless.MemoryAccount) + if a.Reverse == nil || a.Reverse.Tag == "" { + return nil, errors.New("reverse: user " + a.ID.String() + " is not allowed to create reverse proxy") + } + r := h.outboundHandlerManager.GetHandler(a.Reverse.Tag) + if r == nil { + picker, _ := reverse.NewStaticMuxPicker() + r = &Reverse{tag: a.Reverse.Tag, picker: picker, client: &mux.ClientManager{Picker: picker}} + for len(h.outboundHandlerManager.ListHandlers(h.ctx)) == 0 { + time.Sleep(time.Second) // prevents this outbound from becoming the default outbound + } + if err := h.outboundHandlerManager.AddHandler(h.ctx, r); err != nil { + return nil, err + } + } + if r, ok := r.(*Reverse); ok { + return r, nil + } + return nil, errors.New("reverse: outbound " + a.Reverse.Tag + " is not type Reverse") +} + +func (h *Handler) RemoveReverse(u *protocol.MemoryUser) { + if u != nil { + a := u.Account.(*vless.MemoryAccount) + if a.Reverse != nil && a.Reverse.Tag != "" { + h.outboundHandlerManager.RemoveHandler(h.ctx, a.Reverse.Tag) + } + } +} + +// Close implements common.Closable.Close(). +func (h *Handler) Close() error { + if h.decryption != nil { + h.decryption.Close() + } + for _, u := range h.validator.GetAll() { + h.RemoveReverse(u) + } + return errors.Combine(common.Close(h.validator)) +} + +// AddUser implements proxy.UserManager.AddUser(). +func (h *Handler) AddUser(ctx context.Context, u *protocol.MemoryUser) error { + return h.validator.Add(u) +} + +// RemoveUser implements proxy.UserManager.RemoveUser(). +func (h *Handler) RemoveUser(ctx context.Context, e string) error { + h.RemoveReverse(h.validator.GetByEmail(e)) + return h.validator.Del(e) +} + +// GetUser implements proxy.UserManager.GetUser(). +func (h *Handler) GetUser(ctx context.Context, email string) *protocol.MemoryUser { + return h.validator.GetByEmail(email) +} + +// GetUsers implements proxy.UserManager.GetUsers(). +func (h *Handler) GetUsers(ctx context.Context) []*protocol.MemoryUser { + return h.validator.GetAll() +} + +// GetUsersCount implements proxy.UserManager.GetUsersCount(). +func (h *Handler) GetUsersCount(context.Context) int64 { + return h.validator.GetCount() +} + +// Network implements proxy.Inbound.Network(). +func (*Handler) Network() []net.Network { + return []net.Network{net.Network_TCP, net.Network_UNIX} +} + +// Process implements proxy.Inbound.Process(). +func (h *Handler) Process(ctx context.Context, network net.Network, connection stat.Connection, dispatch routing.Dispatcher) error { + iConn := stat.TryUnwrapStatsConn(connection) + + if h.decryption != nil { + var err error + if connection, err = h.decryption.Handshake(connection, nil); err != nil { + return errors.New("ML-KEM-768 handshake failed").Base(err).AtInfo() + } + } + + sessionPolicy := h.policyManager.ForLevel(0) + if err := connection.SetReadDeadline(time.Now().Add(sessionPolicy.Timeouts.Handshake)); err != nil { + return errors.New("unable to set read deadline").Base(err).AtWarning() + } + + first := buf.FromBytes(make([]byte, buf.Size)) + first.Clear() + firstLen, errR := first.ReadFrom(connection) + if errR != nil { + return errR + } + errors.LogInfo(ctx, "firstLen = ", firstLen) + + reader := &buf.BufferedReader{ + Reader: buf.NewReader(connection), + Buffer: buf.MultiBuffer{first}, + } + + var userSentID []byte // not MemoryAccount.ID + var request *protocol.RequestHeader + var requestAddons *encoding.Addons + var err error + + napfb := h.fallbacks + isfb := napfb != nil + + if isfb && firstLen < 18 { + err = errors.New("fallback directly") + } else { + userSentID, request, requestAddons, isfb, err = encoding.DecodeRequestHeader(isfb, first, reader, h.validator) + } + + if err != nil { + if isfb { + if err := connection.SetReadDeadline(time.Time{}); err != nil { + errors.LogWarningInner(ctx, err, "unable to set back read deadline") + } + errors.LogInfoInner(ctx, err, "fallback starts") + + name := "" + alpn := "" + if tlsConn, ok := iConn.(*tls.Conn); ok { + cs := tlsConn.ConnectionState() + name = cs.ServerName + alpn = cs.NegotiatedProtocol + errors.LogInfo(ctx, "realName = "+name) + errors.LogInfo(ctx, "realAlpn = "+alpn) + } else if realityConn, ok := iConn.(*reality.Conn); ok { + cs := realityConn.ConnectionState() + name = cs.ServerName + alpn = cs.NegotiatedProtocol + errors.LogInfo(ctx, "realName = "+name) + errors.LogInfo(ctx, "realAlpn = "+alpn) + } + name = strings.ToLower(name) + alpn = strings.ToLower(alpn) + + if len(napfb) > 1 || napfb[""] == nil { + if name != "" && napfb[name] == nil { + match := "" + for n := range napfb { + if n != "" && strings.Contains(name, n) && len(n) > len(match) { + match = n + } + } + name = match + } + } + + if napfb[name] == nil { + name = "" + } + apfb := napfb[name] + if apfb == nil { + return errors.New(`failed to find the default "name" config`).AtWarning() + } + + if apfb[alpn] == nil { + alpn = "" + } + pfb := apfb[alpn] + if pfb == nil { + return errors.New(`failed to find the default "alpn" config`).AtWarning() + } + + path := "" + if len(pfb) > 1 || pfb[""] == nil { + /* + if lines := bytes.Split(firstBytes, []byte{'\r', '\n'}); len(lines) > 1 { + if s := bytes.Split(lines[0], []byte{' '}); len(s) == 3 { + if len(s[0]) < 8 && len(s[1]) > 0 && len(s[2]) == 8 { + errors.New("realPath = " + string(s[1])).AtInfo().WriteToLog(sid) + for _, fb := range pfb { + if fb.Path != "" && h.regexps[fb.Path].Match(s[1]) { + path = fb.Path + break + } + } + } + } + } + */ + if firstLen >= 18 && first.Byte(4) != '*' { // not h2c + firstBytes := first.Bytes() + for i := 4; i <= 8; i++ { // 5 -> 9 + if firstBytes[i] == '/' && firstBytes[i-1] == ' ' { + search := len(firstBytes) + if search > 64 { + search = 64 // up to about 60 + } + for j := i + 1; j < search; j++ { + k := firstBytes[j] + if k == '\r' || k == '\n' { // avoid logging \r or \n + break + } + if k == '?' || k == ' ' { + path = string(firstBytes[i:j]) + errors.LogInfo(ctx, "realPath = "+path) + if pfb[path] == nil { + path = "" + } + break + } + } + break + } + } + } + } + fb := pfb[path] + if fb == nil { + return errors.New(`failed to find the default "path" config`).AtWarning() + } + + ctx, cancel := context.WithCancel(ctx) + timer := signal.CancelAfterInactivity(ctx, cancel, sessionPolicy.Timeouts.ConnectionIdle) + ctx = policy.ContextWithBufferPolicy(ctx, sessionPolicy.Buffer) + + var conn net.Conn + if err := retry.ExponentialBackoff(5, 100).On(func() error { + var dialer net.Dialer + conn, err = dialer.DialContext(ctx, fb.Type, fb.Dest) + if err != nil { + return err + } + return nil + }); err != nil { + return errors.New("failed to dial to " + fb.Dest).Base(err).AtWarning() + } + defer conn.Close() + + serverReader := buf.NewReader(conn) + serverWriter := buf.NewWriter(conn) + + postRequest := func() error { + defer timer.SetTimeout(sessionPolicy.Timeouts.DownlinkOnly) + if fb.Xver != 0 { + ipType := 4 + remoteAddr, remotePort, err := net.SplitHostPort(connection.RemoteAddr().String()) + if err != nil { + ipType = 0 + } + localAddr, localPort, err := net.SplitHostPort(connection.LocalAddr().String()) + if err != nil { + ipType = 0 + } + if ipType == 4 { + for i := 0; i < len(remoteAddr); i++ { + if remoteAddr[i] == ':' { + ipType = 6 + break + } + } + } + pro := buf.New() + defer pro.Release() + switch fb.Xver { + case 1: + if ipType == 0 { + pro.Write([]byte("PROXY UNKNOWN\r\n")) + break + } + if ipType == 4 { + pro.Write([]byte("PROXY TCP4 " + remoteAddr + " " + localAddr + " " + remotePort + " " + localPort + "\r\n")) + } else { + pro.Write([]byte("PROXY TCP6 " + remoteAddr + " " + localAddr + " " + remotePort + " " + localPort + "\r\n")) + } + case 2: + pro.Write([]byte("\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A")) // signature + if ipType == 0 { + pro.Write([]byte("\x20\x00\x00\x00")) // v2 + LOCAL + UNSPEC + UNSPEC + 0 bytes + break + } + if ipType == 4 { + pro.Write([]byte("\x21\x11\x00\x0C")) // v2 + PROXY + AF_INET + STREAM + 12 bytes + pro.Write(net.ParseIP(remoteAddr).To4()) + pro.Write(net.ParseIP(localAddr).To4()) + } else { + pro.Write([]byte("\x21\x21\x00\x24")) // v2 + PROXY + AF_INET6 + STREAM + 36 bytes + pro.Write(net.ParseIP(remoteAddr).To16()) + pro.Write(net.ParseIP(localAddr).To16()) + } + p1, _ := strconv.ParseUint(remotePort, 10, 16) + p2, _ := strconv.ParseUint(localPort, 10, 16) + pro.Write([]byte{byte(p1 >> 8), byte(p1), byte(p2 >> 8), byte(p2)}) + } + if err := serverWriter.WriteMultiBuffer(buf.MultiBuffer{pro}); err != nil { + return errors.New("failed to set PROXY protocol v", fb.Xver).Base(err).AtWarning() + } + } + if err := buf.Copy(reader, serverWriter, buf.UpdateActivity(timer)); err != nil { + return errors.New("failed to fallback request payload").Base(err).AtInfo() + } + return nil + } + + writer := buf.NewWriter(connection) + + getResponse := func() error { + defer timer.SetTimeout(sessionPolicy.Timeouts.UplinkOnly) + if err := buf.Copy(serverReader, writer, buf.UpdateActivity(timer)); err != nil { + return errors.New("failed to deliver response payload").Base(err).AtInfo() + } + return nil + } + + if err := task.Run(ctx, task.OnSuccess(postRequest, task.Close(serverWriter)), task.OnSuccess(getResponse, task.Close(writer))); err != nil { + common.Interrupt(serverReader) + common.Interrupt(serverWriter) + return errors.New("fallback ends").Base(err).AtInfo() + } + return nil + } + + if errors.Cause(err) != io.EOF { + log.Record(&log.AccessMessage{ + From: connection.RemoteAddr(), + To: "", + Status: log.AccessRejected, + Reason: err, + }) + err = errors.New("invalid request from ", connection.RemoteAddr()).Base(err).AtInfo() + } + return err + } + + if err := connection.SetReadDeadline(time.Time{}); err != nil { + errors.LogWarningInner(ctx, err, "unable to set back read deadline") + } + errors.LogInfo(ctx, "received request for ", request.Destination()) + + inbound := session.InboundFromContext(ctx) + if inbound == nil { + panic("no inbound metadata") + } + inbound.Name = "vless" + inbound.User = request.User + inbound.VlessRoute = net.PortFromBytes(userSentID[6:8]) + + account := request.User.Account.(*vless.MemoryAccount) + + if account.Reverse != nil && request.Command != protocol.RequestCommandRvs { + return errors.New("for safety reasons, user " + account.ID.String() + " is not allowed to use forward proxy") + } + + responseAddons := &encoding.Addons{ + // Flow: requestAddons.Flow, + } + + var input *bytes.Reader + var rawInput *bytes.Buffer + switch requestAddons.Flow { + case vless.XRV: + if account.Flow == requestAddons.Flow { + inbound.CanSpliceCopy = 2 + switch request.Command { + case protocol.RequestCommandUDP: + return errors.New(requestAddons.Flow + " doesn't support UDP").AtWarning() + case protocol.RequestCommandMux, protocol.RequestCommandRvs: + inbound.CanSpliceCopy = 3 + fallthrough // we will break Mux connections that contain TCP requests + case protocol.RequestCommandTCP: + var t reflect.Type + var p uintptr + if commonConn, ok := connection.(*encryption.CommonConn); ok { + if _, ok := commonConn.Conn.(*encryption.XorConn); ok || !proxy.IsRAWTransportWithoutSecurity(iConn) { + inbound.CanSpliceCopy = 3 // full-random xorConn / non-RAW transport / another securityConn should not be penetrated + } + t = reflect.TypeOf(commonConn).Elem() + p = uintptr(unsafe.Pointer(commonConn)) + } else if tlsConn, ok := iConn.(*tls.Conn); ok { + if tlsConn.ConnectionState().Version != gotls.VersionTLS13 { + return errors.New(`failed to use `+requestAddons.Flow+`, found outer tls version `, tlsConn.ConnectionState().Version).AtWarning() + } + t = reflect.TypeOf(tlsConn.Conn).Elem() + p = uintptr(unsafe.Pointer(tlsConn.Conn)) + } else if realityConn, ok := iConn.(*reality.Conn); ok { + t = reflect.TypeOf(realityConn.Conn).Elem() + p = uintptr(unsafe.Pointer(realityConn.Conn)) + } else { + return errors.New("XTLS only supports TLS and REALITY directly for now.").AtWarning() + } + i, _ := t.FieldByName("input") + r, _ := t.FieldByName("rawInput") + input = (*bytes.Reader)(unsafe.Pointer(p + i.Offset)) + rawInput = (*bytes.Buffer)(unsafe.Pointer(p + r.Offset)) + } + } else { + return errors.New("account " + account.ID.String() + " is not able to use the flow " + requestAddons.Flow).AtWarning() + } + case "": + inbound.CanSpliceCopy = 3 + if account.Flow == vless.XRV && (request.Command == protocol.RequestCommandTCP || isMuxAndNotXUDP(request, first)) { + return errors.New("account " + account.ID.String() + " is rejected since the client flow is empty. Note that the pure TLS proxy has certain TLS in TLS characters.").AtWarning() + } + default: + return errors.New("unknown request flow " + requestAddons.Flow).AtWarning() + } + + if request.Command != protocol.RequestCommandMux { + ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{ + From: connection.RemoteAddr(), + To: request.Destination(), + Status: log.AccessAccepted, + Reason: "", + Email: request.User.Email, + }) + } else if account.Flow == vless.XRV { + ctx = session.ContextWithAllowedNetwork(ctx, net.Network_UDP) + } + + trafficState := proxy.NewTrafficState(userSentID) + clientReader := encoding.DecodeBodyAddons(reader, request, requestAddons) + if requestAddons.Flow == vless.XRV { + clientReader = proxy.NewVisionReader(clientReader, trafficState, true, ctx, connection, input, rawInput, nil) + } + + bufferWriter := buf.NewBufferedWriter(buf.NewWriter(connection)) + if err := encoding.EncodeResponseHeader(bufferWriter, request, responseAddons); err != nil { + return errors.New("failed to encode response header").Base(err).AtWarning() + } + clientWriter := encoding.EncodeBodyAddons(bufferWriter, request, requestAddons, trafficState, false, ctx, connection, nil) + bufferWriter.SetFlushNext() + + if request.Command == protocol.RequestCommandRvs { + r, err := h.GetReverse(account) + if err != nil { + return err + } + return r.NewMux(ctx, dispatcher.WrapLink(ctx, h.policyManager, h.stats, &transport.Link{Reader: clientReader, Writer: clientWriter}), h.observer) + } + + if err := dispatch.DispatchLink(ctx, request.Destination(), &transport.Link{ + Reader: clientReader, + Writer: clientWriter}, + ); err != nil { + return errors.New("failed to dispatch request").Base(err) + } + return nil +} + +type Reverse struct { + tag string + picker *reverse.StaticMuxPicker + client *mux.ClientManager +} + +func (r *Reverse) Tag() string { + return r.tag +} + +func (r *Reverse) NewMux(ctx context.Context, link *transport.Link, observer features.Feature) error { + muxClient, err := mux.NewClientWorker(*link, mux.ClientStrategy{}) + if err != nil { + return errors.New("failed to create mux client worker").Base(err).AtWarning() + } + worker, err := reverse.NewPortalWorker(muxClient) + if err != nil { + return errors.New("failed to create portal worker").Base(err).AtWarning() + } + r.picker.AddWorker(worker) + if burstObs, ok := observer.(extension.BurstObservatory); ok { + go burstObs.Check([]string{r.Tag()}) + } + select { + case <-ctx.Done(): + case <-muxClient.WaitClosed(): + } + return nil +} + +func (r *Reverse) Dispatch(ctx context.Context, link *transport.Link) { + outbounds := session.OutboundsFromContext(ctx) + ob := outbounds[len(outbounds)-1] + if ob != nil { + if ob.Target.Network == net.Network_UDP && ob.OriginalTarget.Address != nil && ob.OriginalTarget.Address != ob.Target.Address { + link.Reader = &buf.EndpointOverrideReader{Reader: link.Reader, Dest: ob.Target.Address, OriginalDest: ob.OriginalTarget.Address} + link.Writer = &buf.EndpointOverrideWriter{Writer: link.Writer, Dest: ob.Target.Address, OriginalDest: ob.OriginalTarget.Address} + } + r.client.Dispatch(session.ContextWithIsReverseMux(ctx, true), link) + } +} + +func (r *Reverse) Start() error { + return nil +} + +func (r *Reverse) Close() error { + return nil +} + +func (r *Reverse) SenderSettings() *serial.TypedMessage { + return nil +} + +func (r *Reverse) ProxySettings() *serial.TypedMessage { + return nil +} diff --git a/subproject/Xray-core-main/proxy/vless/outbound/config.go b/subproject/Xray-core-main/proxy/vless/outbound/config.go new file mode 100644 index 00000000..a1e73e06 --- /dev/null +++ b/subproject/Xray-core-main/proxy/vless/outbound/config.go @@ -0,0 +1 @@ +package outbound diff --git a/subproject/Xray-core-main/proxy/vless/outbound/config.pb.go b/subproject/Xray-core-main/proxy/vless/outbound/config.pb.go new file mode 100644 index 00000000..7899e657 --- /dev/null +++ b/subproject/Xray-core-main/proxy/vless/outbound/config.pb.go @@ -0,0 +1,126 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: proxy/vless/outbound/config.proto + +package outbound + +import ( + protocol "github.com/xtls/xray-core/common/protocol" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + Vnext *protocol.ServerEndpoint `protobuf:"bytes,1,opt,name=vnext,proto3" json:"vnext,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_proxy_vless_outbound_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_proxy_vless_outbound_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_proxy_vless_outbound_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Config) GetVnext() *protocol.ServerEndpoint { + if x != nil { + return x.Vnext + } + return nil +} + +var File_proxy_vless_outbound_config_proto protoreflect.FileDescriptor + +const file_proxy_vless_outbound_config_proto_rawDesc = "" + + "\n" + + "!proxy/vless/outbound/config.proto\x12\x19xray.proxy.vless.outbound\x1a!common/protocol/server_spec.proto\"D\n" + + "\x06Config\x12:\n" + + "\x05vnext\x18\x01 \x01(\v2$.xray.common.protocol.ServerEndpointR\x05vnextBm\n" + + "\x1dcom.xray.proxy.vless.outboundP\x01Z.github.com/xtls/xray-core/proxy/vless/outbound\xaa\x02\x19Xray.Proxy.Vless.Outboundb\x06proto3" + +var ( + file_proxy_vless_outbound_config_proto_rawDescOnce sync.Once + file_proxy_vless_outbound_config_proto_rawDescData []byte +) + +func file_proxy_vless_outbound_config_proto_rawDescGZIP() []byte { + file_proxy_vless_outbound_config_proto_rawDescOnce.Do(func() { + file_proxy_vless_outbound_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proxy_vless_outbound_config_proto_rawDesc), len(file_proxy_vless_outbound_config_proto_rawDesc))) + }) + return file_proxy_vless_outbound_config_proto_rawDescData +} + +var file_proxy_vless_outbound_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_proxy_vless_outbound_config_proto_goTypes = []any{ + (*Config)(nil), // 0: xray.proxy.vless.outbound.Config + (*protocol.ServerEndpoint)(nil), // 1: xray.common.protocol.ServerEndpoint +} +var file_proxy_vless_outbound_config_proto_depIdxs = []int32{ + 1, // 0: xray.proxy.vless.outbound.Config.vnext:type_name -> xray.common.protocol.ServerEndpoint + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_proxy_vless_outbound_config_proto_init() } +func file_proxy_vless_outbound_config_proto_init() { + if File_proxy_vless_outbound_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_proxy_vless_outbound_config_proto_rawDesc), len(file_proxy_vless_outbound_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_proxy_vless_outbound_config_proto_goTypes, + DependencyIndexes: file_proxy_vless_outbound_config_proto_depIdxs, + MessageInfos: file_proxy_vless_outbound_config_proto_msgTypes, + }.Build() + File_proxy_vless_outbound_config_proto = out.File + file_proxy_vless_outbound_config_proto_goTypes = nil + file_proxy_vless_outbound_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/proxy/vless/outbound/config.proto b/subproject/Xray-core-main/proxy/vless/outbound/config.proto new file mode 100644 index 00000000..150b2d6c --- /dev/null +++ b/subproject/Xray-core-main/proxy/vless/outbound/config.proto @@ -0,0 +1,13 @@ +syntax = "proto3"; + +package xray.proxy.vless.outbound; +option csharp_namespace = "Xray.Proxy.Vless.Outbound"; +option go_package = "github.com/xtls/xray-core/proxy/vless/outbound"; +option java_package = "com.xray.proxy.vless.outbound"; +option java_multiple_files = true; + +import "common/protocol/server_spec.proto"; + +message Config { + xray.common.protocol.ServerEndpoint vnext = 1; +} diff --git a/subproject/Xray-core-main/proxy/vless/outbound/outbound.go b/subproject/Xray-core-main/proxy/vless/outbound/outbound.go new file mode 100644 index 00000000..75e74fe0 --- /dev/null +++ b/subproject/Xray-core-main/proxy/vless/outbound/outbound.go @@ -0,0 +1,489 @@ +package outbound + +import ( + "bytes" + "context" + gotls "crypto/tls" + "encoding/base64" + "reflect" + "strings" + "sync" + "time" + "unsafe" + + utls "github.com/refraction-networking/utls" + proxyman "github.com/xtls/xray-core/app/proxyman/outbound" + "github.com/xtls/xray-core/app/reverse" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + xctx "github.com/xtls/xray-core/common/ctx" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/mux" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/retry" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/common/signal" + "github.com/xtls/xray-core/common/task" + "github.com/xtls/xray-core/common/xudp" + "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/features/policy" + "github.com/xtls/xray-core/features/routing" + "github.com/xtls/xray-core/proxy" + "github.com/xtls/xray-core/proxy/vless" + "github.com/xtls/xray-core/proxy/vless/encoding" + "github.com/xtls/xray-core/proxy/vless/encryption" + "github.com/xtls/xray-core/transport" + "github.com/xtls/xray-core/transport/internet" + "github.com/xtls/xray-core/transport/internet/reality" + "github.com/xtls/xray-core/transport/internet/stat" + "github.com/xtls/xray-core/transport/internet/tls" + "github.com/xtls/xray-core/transport/pipe" +) + +func init() { + common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + return New(ctx, config.(*Config)) + })) +} + +// Handler is an outbound connection handler for VLess protocol. +type Handler struct { + server *protocol.ServerSpec + policyManager policy.Manager + cone bool + encryption *encryption.ClientInstance + reverse *Reverse + + testpre uint32 + initpre sync.Once + preConns chan *ConnExpire +} + +type ConnExpire struct { + Conn stat.Connection + Expire time.Time +} + +// New creates a new VLess outbound handler. +func New(ctx context.Context, config *Config) (*Handler, error) { + if config.Vnext == nil { + return nil, errors.New(`no vnext found`) + } + server, err := protocol.NewServerSpecFromPB(config.Vnext) + if err != nil { + return nil, errors.New("failed to get server spec").Base(err).AtError() + } + + v := core.MustFromContext(ctx) + handler := &Handler{ + server: server, + policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager), + cone: ctx.Value("cone").(bool), + } + + a := handler.server.User.Account.(*vless.MemoryAccount) + if a.Encryption != "" && a.Encryption != "none" { + s := strings.Split(a.Encryption, ".") + var nfsPKeysBytes [][]byte + for _, r := range s { + b, _ := base64.RawURLEncoding.DecodeString(r) + nfsPKeysBytes = append(nfsPKeysBytes, b) + } + handler.encryption = &encryption.ClientInstance{} + if err := handler.encryption.Init(nfsPKeysBytes, a.XorMode, a.Seconds, a.Padding); err != nil { + return nil, errors.New("failed to use encryption").Base(err).AtError() + } + } + + if a.Reverse != nil { + rvsCtx := session.ContextWithInbound(ctx, &session.Inbound{ + Tag: a.Reverse.Tag, + User: handler.server.User, // TODO: email + }) + if sc := a.Reverse.Sniffing; sc != nil && sc.Enabled { + rvsCtx = session.ContextWithContent(rvsCtx, &session.Content{ + SniffingRequest: session.SniffingRequest{ + Enabled: sc.Enabled, + OverrideDestinationForProtocol: sc.DestinationOverride, + ExcludeForDomain: sc.DomainsExcluded, + MetadataOnly: sc.MetadataOnly, + RouteOnly: sc.RouteOnly, + }, + }) + } + handler.reverse = &Reverse{ + tag: a.Reverse.Tag, + dispatcher: v.GetFeature(routing.DispatcherType()).(routing.Dispatcher), + ctx: rvsCtx, + handler: handler, + } + handler.reverse.monitorTask = &task.Periodic{ + Execute: handler.reverse.monitor, + Interval: time.Second * 2, + } + go func() { + time.Sleep(2 * time.Second) + handler.reverse.Start() + }() + } + + handler.testpre = a.Testpre + + return handler, nil +} + +// Close implements common.Closable.Close(). +func (h *Handler) Close() error { + if h.preConns != nil { + close(h.preConns) + } + if h.reverse != nil { + return h.reverse.Close() + } + return nil +} + +// Process implements proxy.Outbound.Process(). +func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer internet.Dialer) error { + outbounds := session.OutboundsFromContext(ctx) + ob := outbounds[len(outbounds)-1] + if !ob.Target.IsValid() && ob.Target.Address.String() != "v1.rvs.cool" { + return errors.New("target not specified").AtError() + } + ob.Name = "vless" + + rec := h.server + var conn stat.Connection + + if h.testpre > 0 && h.reverse == nil { + h.initpre.Do(func() { + h.preConns = make(chan *ConnExpire) + for range h.testpre { // TODO: randomize + go func() { + defer func() { recover() }() + ctx := xctx.ContextWithID(context.Background(), session.NewID()) + for { + conn, err := dialer.Dial(ctx, rec.Destination) + if err != nil { + errors.LogWarningInner(ctx, err, "pre-connect failed") + continue + } + h.preConns <- &ConnExpire{Conn: conn, Expire: time.Now().Add(time.Minute * 2)} // TODO: customize & randomize + time.Sleep(time.Millisecond * 200) // TODO: customize & randomize + } + }() + } + }) + for { + connTime := <-h.preConns + if connTime == nil { + return errors.New("closed handler").AtWarning() + } + if time.Now().Before(connTime.Expire) { + conn = connTime.Conn + break + } + connTime.Conn.Close() + } + } + + if conn == nil { + if err := retry.ExponentialBackoff(5, 200).On(func() error { + var err error + conn, err = dialer.Dial(ctx, rec.Destination) + if err != nil { + return err + } + return nil + }); err != nil { + return errors.New("failed to find an available destination").Base(err).AtWarning() + } + } + defer conn.Close() + + ob.Conn = conn // for Vision's pre-connect + + iConn := stat.TryUnwrapStatsConn(conn) + target := ob.Target + errors.LogInfo(ctx, "tunneling request to ", target, " via ", rec.Destination.NetAddr()) + + if h.encryption != nil { + var err error + if conn, err = h.encryption.Handshake(conn); err != nil { + return errors.New("ML-KEM-768 handshake failed").Base(err).AtInfo() + } + } + + command := protocol.RequestCommandTCP + if target.Network == net.Network_UDP { + command = protocol.RequestCommandUDP + } + if target.Address.Family().IsDomain() { + switch target.Address.Domain() { + case "v1.mux.cool": + command = protocol.RequestCommandMux + case "v1.rvs.cool": + if target.Network != net.Network_Unknown { + return errors.New("nice try baby").AtError() + } + command = protocol.RequestCommandRvs + } + } + + request := &protocol.RequestHeader{ + Version: encoding.Version, + User: rec.User, + Command: command, + Address: target.Address, + Port: target.Port, + } + + account := request.User.Account.(*vless.MemoryAccount) + + requestAddons := &encoding.Addons{ + Flow: account.Flow, + } + + var input *bytes.Reader + var rawInput *bytes.Buffer + allowUDP443 := false + switch requestAddons.Flow { + case vless.XRV + "-udp443": + allowUDP443 = true + requestAddons.Flow = requestAddons.Flow[:16] + fallthrough + case vless.XRV: + ob.CanSpliceCopy = 2 + switch request.Command { + case protocol.RequestCommandUDP: + if !allowUDP443 && request.Port == 443 { + return errors.New("XTLS rejected UDP/443 traffic").AtInfo() + } + case protocol.RequestCommandMux: + fallthrough // let server break Mux connections that contain TCP requests + case protocol.RequestCommandTCP, protocol.RequestCommandRvs: + var t reflect.Type + var p uintptr + if commonConn, ok := conn.(*encryption.CommonConn); ok { + if _, ok := commonConn.Conn.(*encryption.XorConn); ok || !proxy.IsRAWTransportWithoutSecurity(iConn) { + ob.CanSpliceCopy = 3 // full-random xorConn / non-RAW transport / another securityConn should not be penetrated + } + t = reflect.TypeOf(commonConn).Elem() + p = uintptr(unsafe.Pointer(commonConn)) + } else if tlsConn, ok := iConn.(*tls.Conn); ok { + t = reflect.TypeOf(tlsConn.Conn).Elem() + p = uintptr(unsafe.Pointer(tlsConn.Conn)) + } else if utlsConn, ok := iConn.(*tls.UConn); ok { + t = reflect.TypeOf(utlsConn.Conn).Elem() + p = uintptr(unsafe.Pointer(utlsConn.Conn)) + } else if realityConn, ok := iConn.(*reality.UConn); ok { + t = reflect.TypeOf(realityConn.Conn).Elem() + p = uintptr(unsafe.Pointer(realityConn.Conn)) + } else { + return errors.New("XTLS only supports TLS and REALITY directly for now.").AtWarning() + } + i, _ := t.FieldByName("input") + r, _ := t.FieldByName("rawInput") + input = (*bytes.Reader)(unsafe.Pointer(p + i.Offset)) + rawInput = (*bytes.Buffer)(unsafe.Pointer(p + r.Offset)) + default: + panic("unknown VLESS request command") + } + default: + ob.CanSpliceCopy = 3 + } + + var newCtx context.Context + var newCancel context.CancelFunc + if session.TimeoutOnlyFromContext(ctx) { + newCtx, newCancel = context.WithCancel(context.Background()) + } + + sessionPolicy := h.policyManager.ForLevel(request.User.Level) + ctx, cancel := context.WithCancel(ctx) + timer := signal.CancelAfterInactivity(ctx, func() { + cancel() + if newCancel != nil { + newCancel() + } + }, sessionPolicy.Timeouts.ConnectionIdle) + + clientReader := link.Reader // .(*pipe.Reader) + clientWriter := link.Writer // .(*pipe.Writer) + trafficState := proxy.NewTrafficState(account.ID.Bytes()) + if request.Command == protocol.RequestCommandUDP && (requestAddons.Flow == vless.XRV || (h.cone && request.Port != 53 && request.Port != 443)) { + request.Command = protocol.RequestCommandMux + request.Address = net.DomainAddress("v1.mux.cool") + request.Port = net.Port(666) + } + + postRequest := func() error { + defer timer.SetTimeout(sessionPolicy.Timeouts.DownlinkOnly) + + bufferWriter := buf.NewBufferedWriter(buf.NewWriter(conn)) + if err := encoding.EncodeRequestHeader(bufferWriter, request, requestAddons); err != nil { + return errors.New("failed to encode request header").Base(err).AtWarning() + } + + // default: serverWriter := bufferWriter + serverWriter := encoding.EncodeBodyAddons(bufferWriter, request, requestAddons, trafficState, true, ctx, conn, ob) + if request.Command == protocol.RequestCommandMux && request.Port == 666 { + serverWriter = xudp.NewPacketWriter(serverWriter, target, xudp.GetGlobalID(ctx)) + } + timeoutReader, ok := clientReader.(buf.TimeoutReader) + if ok { + multiBuffer, err1 := timeoutReader.ReadMultiBufferTimeout(time.Millisecond * 500) + if err1 == nil { + if err := serverWriter.WriteMultiBuffer(multiBuffer); err != nil { + return err // ... + } + } else if err1 != buf.ErrReadTimeout { + return err1 + } else if requestAddons.Flow == vless.XRV { + mb := make(buf.MultiBuffer, 1) + errors.LogInfo(ctx, "Insert padding with empty content to camouflage VLESS header ", mb.Len()) + if err := serverWriter.WriteMultiBuffer(mb); err != nil { + return err // ... + } + } + } else { + errors.LogDebug(ctx, "Reader is not timeout reader, will send out vless header separately from first payload") + } + // Flush; bufferWriter.WriteMultiBuffer now is bufferWriter.writer.WriteMultiBuffer + if err := bufferWriter.SetBuffered(false); err != nil { + return errors.New("failed to write A request payload").Base(err).AtWarning() + } + + if requestAddons.Flow == vless.XRV { + if tlsConn, ok := iConn.(*tls.Conn); ok { + if tlsConn.ConnectionState().Version != gotls.VersionTLS13 { + return errors.New(`failed to use `+requestAddons.Flow+`, found outer tls version `, tlsConn.ConnectionState().Version).AtWarning() + } + } else if utlsConn, ok := iConn.(*tls.UConn); ok { + if utlsConn.ConnectionState().Version != utls.VersionTLS13 { + return errors.New(`failed to use `+requestAddons.Flow+`, found outer tls version `, utlsConn.ConnectionState().Version).AtWarning() + } + } + } + err := buf.Copy(clientReader, serverWriter, buf.UpdateActivity(timer)) + if err != nil { + return errors.New("failed to transfer request payload").Base(err).AtInfo() + } + + // Indicates the end of request payload. + switch requestAddons.Flow { + default: + } + return nil + } + + getResponse := func() error { + defer timer.SetTimeout(sessionPolicy.Timeouts.UplinkOnly) + + responseAddons, err := encoding.DecodeResponseHeader(conn, request) + if err != nil { + return errors.New("failed to decode response header").Base(err).AtInfo() + } + + // default: serverReader := buf.NewReader(conn) + serverReader := encoding.DecodeBodyAddons(conn, request, responseAddons) + if requestAddons.Flow == vless.XRV { + serverReader = proxy.NewVisionReader(serverReader, trafficState, false, ctx, conn, input, rawInput, ob) + } + if request.Command == protocol.RequestCommandMux && request.Port == 666 { + if requestAddons.Flow == vless.XRV { + serverReader = xudp.NewPacketReader(&buf.BufferedReader{Reader: serverReader}) + } else { + serverReader = xudp.NewPacketReader(conn) + } + } + + if requestAddons.Flow == vless.XRV { + err = encoding.XtlsRead(serverReader, clientWriter, timer, conn, trafficState, false, ctx) + } else { + // from serverReader.ReadMultiBuffer to clientWriter.WriteMultiBuffer + err = buf.Copy(serverReader, clientWriter, buf.UpdateActivity(timer)) + } + + if err != nil { + return errors.New("failed to transfer response payload").Base(err).AtInfo() + } + + return nil + } + + if newCtx != nil { + ctx = newCtx + } + + if err := task.Run(ctx, postRequest, task.OnSuccess(getResponse, task.Close(clientWriter))); err != nil { + return errors.New("connection ends").Base(err).AtInfo() + } + + return nil +} + +type Reverse struct { + tag string + dispatcher routing.Dispatcher + ctx context.Context + handler *Handler + workers []*reverse.BridgeWorker + monitorTask *task.Periodic +} + +func (r *Reverse) monitor() error { + var activeWorkers []*reverse.BridgeWorker + for _, w := range r.workers { + if w.IsActive() { + activeWorkers = append(activeWorkers, w) + } + } + if len(activeWorkers) != len(r.workers) { + r.workers = activeWorkers + } + + var numConnections uint32 + var numWorker uint32 + for _, w := range r.workers { + if w.IsActive() { + numConnections += w.Connections() + numWorker++ + } + } + if numWorker == 0 || numConnections/numWorker > 16 { + reader1, writer1 := pipe.New(pipe.WithSizeLimit(2 * buf.Size)) + reader2, writer2 := pipe.New(pipe.WithSizeLimit(2 * buf.Size)) + link1 := &transport.Link{Reader: reader1, Writer: writer2} + link2 := &transport.Link{Reader: reader2, Writer: writer1} + w := &reverse.BridgeWorker{ + Tag: r.tag, + Dispatcher: r.dispatcher, + } + worker, err := mux.NewServerWorker(session.ContextWithIsReverseMux(r.ctx, true), w, link1) + if err != nil { + errors.LogWarningInner(r.ctx, err, "failed to create mux server worker") + return nil + } + w.Worker = worker + r.workers = append(r.workers, w) + go func() { + ctx := session.ContextWithOutbounds(r.ctx, []*session.Outbound{{ + Target: net.Destination{Address: net.DomainAddress("v1.rvs.cool")}, + }}) + r.handler.Process(ctx, link2, session.FullHandlerFromContext(ctx).(*proxyman.Handler)) + common.Interrupt(reader1) + common.Interrupt(reader2) + }() + } + return nil +} + +func (r *Reverse) Start() error { + return r.monitorTask.Start() +} + +func (r *Reverse) Close() error { + return r.monitorTask.Close() +} diff --git a/subproject/Xray-core-main/proxy/vless/validator.go b/subproject/Xray-core-main/proxy/vless/validator.go new file mode 100644 index 00000000..35a4469a --- /dev/null +++ b/subproject/Xray-core-main/proxy/vless/validator.go @@ -0,0 +1,98 @@ +package vless + +import ( + "strings" + "sync" + + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/uuid" +) + +type Validator interface { + Get(id uuid.UUID) *protocol.MemoryUser + Add(u *protocol.MemoryUser) error + Del(email string) error + GetByEmail(email string) *protocol.MemoryUser + GetAll() []*protocol.MemoryUser + GetCount() int64 +} + +func ProcessUUID(id [16]byte) [16]byte { + id[6] = 0 + id[7] = 0 + return id +} + +// MemoryValidator stores valid VLESS users. +type MemoryValidator struct { + // Considering email's usage here, map + sync.Mutex/RWMutex may have better performance. + email sync.Map + users sync.Map +} + +// Add a VLESS user, Email must be empty or unique. +func (v *MemoryValidator) Add(u *protocol.MemoryUser) error { + if u.Email != "" { + _, loaded := v.email.LoadOrStore(strings.ToLower(u.Email), u) + if loaded { + return errors.New("User ", u.Email, " already exists.") + } + } + v.users.Store(ProcessUUID(u.Account.(*MemoryAccount).ID.UUID()), u) + return nil +} + +// Del a VLESS user with a non-empty Email. +func (v *MemoryValidator) Del(e string) error { + if e == "" { + return errors.New("Email must not be empty.") + } + le := strings.ToLower(e) + u, _ := v.email.Load(le) + if u == nil { + return errors.New("User ", e, " not found.") + } + v.email.Delete(le) + v.users.Delete(ProcessUUID(u.(*protocol.MemoryUser).Account.(*MemoryAccount).ID.UUID())) + return nil +} + +// Get a VLESS user with UUID, nil if user doesn't exist. +func (v *MemoryValidator) Get(id uuid.UUID) *protocol.MemoryUser { + u, _ := v.users.Load(ProcessUUID(id)) + if u != nil { + return u.(*protocol.MemoryUser) + } + return nil +} + +// Get a VLESS user with email, nil if user doesn't exist. +func (v *MemoryValidator) GetByEmail(email string) *protocol.MemoryUser { + email = strings.ToLower(email) + u, _ := v.email.Load(email) + if u != nil { + return u.(*protocol.MemoryUser) + } + return nil +} + +// Get all users +func (v *MemoryValidator) GetAll() []*protocol.MemoryUser { + var u = make([]*protocol.MemoryUser, 0, 100) + v.email.Range(func(key, value interface{}) bool { + u = append(u, value.(*protocol.MemoryUser)) + return true + }) + return u +} + +// Get users count +func (v *MemoryValidator) GetCount() int64 { + var c int64 = 0 + v.email.Range(func(key, value interface{}) bool { + c++ + return true + }) + return c +} diff --git a/subproject/Xray-core-main/proxy/vless/vless.go b/subproject/Xray-core-main/proxy/vless/vless.go new file mode 100644 index 00000000..bdebdca6 --- /dev/null +++ b/subproject/Xray-core-main/proxy/vless/vless.go @@ -0,0 +1,11 @@ +// Package vless contains the implementation of VLess protocol and transportation. +// +// VLess contains both inbound and outbound connections. VLess inbound is usually used on servers +// together with 'freedom' to talk to final destination, while VLess outbound is usually used on +// clients with 'socks' for proxying. +package vless + +const ( + None = "none" + XRV = "xtls-rprx-vision" +) diff --git a/subproject/Xray-core-main/proxy/vmess/account.go b/subproject/Xray-core-main/proxy/vmess/account.go new file mode 100644 index 00000000..df8ba52b --- /dev/null +++ b/subproject/Xray-core-main/proxy/vmess/account.go @@ -0,0 +1,67 @@ +package vmess + +import ( + "google.golang.org/protobuf/proto" + "strings" + + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/uuid" +) + +// MemoryAccount is an in-memory form of VMess account. +type MemoryAccount struct { + // ID is the main ID of the account. + ID *protocol.ID + // Security type of the account. Used for client connections. + Security protocol.SecurityType + + AuthenticatedLengthExperiment bool + NoTerminationSignal bool +} + +// Equals implements protocol.Account. +func (a *MemoryAccount) Equals(account protocol.Account) bool { + vmessAccount, ok := account.(*MemoryAccount) + if !ok { + return false + } + return a.ID.Equals(vmessAccount.ID) +} + +func (a *MemoryAccount) ToProto() proto.Message { + var test = "" + if a.AuthenticatedLengthExperiment { + test = "AuthenticatedLength|" + } + if a.NoTerminationSignal { + test = test + "NoTerminationSignal" + } + return &Account{ + Id: a.ID.String(), + TestsEnabled: test, + SecuritySettings: &protocol.SecurityConfig{Type: a.Security}, + } +} + +// AsAccount implements protocol.Account. +func (a *Account) AsAccount() (protocol.Account, error) { + id, err := uuid.ParseString(a.Id) + if err != nil { + return nil, errors.New("failed to parse ID").Base(err).AtError() + } + protoID := protocol.NewID(id) + var AuthenticatedLength, NoTerminationSignal bool + if strings.Contains(a.TestsEnabled, "AuthenticatedLength") { + AuthenticatedLength = true + } + if strings.Contains(a.TestsEnabled, "NoTerminationSignal") { + NoTerminationSignal = true + } + return &MemoryAccount{ + ID: protoID, + Security: a.SecuritySettings.GetSecurityType(), + AuthenticatedLengthExperiment: AuthenticatedLength, + NoTerminationSignal: NoTerminationSignal, + }, nil +} diff --git a/subproject/Xray-core-main/proxy/vmess/account.pb.go b/subproject/Xray-core-main/proxy/vmess/account.pb.go new file mode 100644 index 00000000..42bd6f0c --- /dev/null +++ b/subproject/Xray-core-main/proxy/vmess/account.pb.go @@ -0,0 +1,148 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: proxy/vmess/account.proto + +package vmess + +import ( + protocol "github.com/xtls/xray-core/common/protocol" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Account struct { + state protoimpl.MessageState `protogen:"open.v1"` + // ID of the account, in the form of a UUID, e.g., + // "66ad4540-b58c-4ad2-9926-ea63445a9b57". + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + // Security settings. Only applies to client side. + SecuritySettings *protocol.SecurityConfig `protobuf:"bytes,3,opt,name=security_settings,json=securitySettings,proto3" json:"security_settings,omitempty"` + // Define tests enabled for this account + TestsEnabled string `protobuf:"bytes,4,opt,name=tests_enabled,json=testsEnabled,proto3" json:"tests_enabled,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Account) Reset() { + *x = Account{} + mi := &file_proxy_vmess_account_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Account) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Account) ProtoMessage() {} + +func (x *Account) ProtoReflect() protoreflect.Message { + mi := &file_proxy_vmess_account_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Account.ProtoReflect.Descriptor instead. +func (*Account) Descriptor() ([]byte, []int) { + return file_proxy_vmess_account_proto_rawDescGZIP(), []int{0} +} + +func (x *Account) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *Account) GetSecuritySettings() *protocol.SecurityConfig { + if x != nil { + return x.SecuritySettings + } + return nil +} + +func (x *Account) GetTestsEnabled() string { + if x != nil { + return x.TestsEnabled + } + return "" +} + +var File_proxy_vmess_account_proto protoreflect.FileDescriptor + +const file_proxy_vmess_account_proto_rawDesc = "" + + "\n" + + "\x19proxy/vmess/account.proto\x12\x10xray.proxy.vmess\x1a\x1dcommon/protocol/headers.proto\"\x91\x01\n" + + "\aAccount\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12Q\n" + + "\x11security_settings\x18\x03 \x01(\v2$.xray.common.protocol.SecurityConfigR\x10securitySettings\x12#\n" + + "\rtests_enabled\x18\x04 \x01(\tR\ftestsEnabledBR\n" + + "\x14com.xray.proxy.vmessP\x01Z%github.com/xtls/xray-core/proxy/vmess\xaa\x02\x10Xray.Proxy.Vmessb\x06proto3" + +var ( + file_proxy_vmess_account_proto_rawDescOnce sync.Once + file_proxy_vmess_account_proto_rawDescData []byte +) + +func file_proxy_vmess_account_proto_rawDescGZIP() []byte { + file_proxy_vmess_account_proto_rawDescOnce.Do(func() { + file_proxy_vmess_account_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proxy_vmess_account_proto_rawDesc), len(file_proxy_vmess_account_proto_rawDesc))) + }) + return file_proxy_vmess_account_proto_rawDescData +} + +var file_proxy_vmess_account_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_proxy_vmess_account_proto_goTypes = []any{ + (*Account)(nil), // 0: xray.proxy.vmess.Account + (*protocol.SecurityConfig)(nil), // 1: xray.common.protocol.SecurityConfig +} +var file_proxy_vmess_account_proto_depIdxs = []int32{ + 1, // 0: xray.proxy.vmess.Account.security_settings:type_name -> xray.common.protocol.SecurityConfig + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_proxy_vmess_account_proto_init() } +func file_proxy_vmess_account_proto_init() { + if File_proxy_vmess_account_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_proxy_vmess_account_proto_rawDesc), len(file_proxy_vmess_account_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_proxy_vmess_account_proto_goTypes, + DependencyIndexes: file_proxy_vmess_account_proto_depIdxs, + MessageInfos: file_proxy_vmess_account_proto_msgTypes, + }.Build() + File_proxy_vmess_account_proto = out.File + file_proxy_vmess_account_proto_goTypes = nil + file_proxy_vmess_account_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/proxy/vmess/account.proto b/subproject/Xray-core-main/proxy/vmess/account.proto new file mode 100644 index 00000000..3fac6399 --- /dev/null +++ b/subproject/Xray-core-main/proxy/vmess/account.proto @@ -0,0 +1,19 @@ +syntax = "proto3"; + +package xray.proxy.vmess; +option csharp_namespace = "Xray.Proxy.Vmess"; +option go_package = "github.com/xtls/xray-core/proxy/vmess"; +option java_package = "com.xray.proxy.vmess"; +option java_multiple_files = true; + +import "common/protocol/headers.proto"; + +message Account { + // ID of the account, in the form of a UUID, e.g., + // "66ad4540-b58c-4ad2-9926-ea63445a9b57". + string id = 1; + // Security settings. Only applies to client side. + xray.common.protocol.SecurityConfig security_settings = 3; + // Define tests enabled for this account + string tests_enabled = 4; +} diff --git a/subproject/Xray-core-main/proxy/vmess/aead/authid.go b/subproject/Xray-core-main/proxy/vmess/aead/authid.go new file mode 100644 index 00000000..478bb19c --- /dev/null +++ b/subproject/Xray-core-main/proxy/vmess/aead/authid.go @@ -0,0 +1,121 @@ +package aead + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + rand3 "crypto/rand" + "encoding/binary" + "errors" + "hash/crc32" + "io" + "math" + "time" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/antireplay" +) + +var ( + ErrNotFound = errors.New("user do not exist") + ErrNeagtiveTime = errors.New("timestamp is negative") + ErrInvalidTime = errors.New("invalid timestamp, perhaps unsynchronized time") + ErrReplay = errors.New("replayed request") +) + +func CreateAuthID(cmdKey []byte, time int64) [16]byte { + buf := bytes.NewBuffer(nil) + common.Must(binary.Write(buf, binary.BigEndian, time)) + var zero uint32 + common.Must2(io.CopyN(buf, rand3.Reader, 4)) + zero = crc32.ChecksumIEEE(buf.Bytes()) + common.Must(binary.Write(buf, binary.BigEndian, zero)) + aesBlock := NewCipherFromKey(cmdKey) + if buf.Len() != 16 { + panic("Size unexpected") + } + var result [16]byte + aesBlock.Encrypt(result[:], buf.Bytes()) + return result +} + +func NewCipherFromKey(cmdKey []byte) cipher.Block { + aesBlock, err := aes.NewCipher(KDF16(cmdKey, KDFSaltConstAuthIDEncryptionKey)) + if err != nil { + panic(err) + } + return aesBlock +} + +type AuthIDDecoder struct { + s cipher.Block +} + +func NewAuthIDDecoder(cmdKey []byte) *AuthIDDecoder { + return &AuthIDDecoder{NewCipherFromKey(cmdKey)} +} + +func (aidd *AuthIDDecoder) Decode(data [16]byte) (int64, uint32, int32, []byte) { + aidd.s.Decrypt(data[:], data[:]) + var t int64 + var zero uint32 + var rand int32 + reader := bytes.NewReader(data[:]) + common.Must(binary.Read(reader, binary.BigEndian, &t)) + common.Must(binary.Read(reader, binary.BigEndian, &rand)) + common.Must(binary.Read(reader, binary.BigEndian, &zero)) + return t, zero, rand, data[:] +} + +func NewAuthIDDecoderHolder() *AuthIDDecoderHolder { + return &AuthIDDecoderHolder{make(map[string]*AuthIDDecoderItem), antireplay.NewMapFilter[[16]byte](120)} +} + +type AuthIDDecoderHolder struct { + decoders map[string]*AuthIDDecoderItem + filter *antireplay.ReplayFilter[[16]byte] +} + +type AuthIDDecoderItem struct { + dec *AuthIDDecoder + ticket interface{} +} + +func NewAuthIDDecoderItem(key [16]byte, ticket interface{}) *AuthIDDecoderItem { + return &AuthIDDecoderItem{ + dec: NewAuthIDDecoder(key[:]), + ticket: ticket, + } +} + +func (a *AuthIDDecoderHolder) AddUser(key [16]byte, ticket interface{}) { + a.decoders[string(key[:])] = NewAuthIDDecoderItem(key, ticket) +} + +func (a *AuthIDDecoderHolder) RemoveUser(key [16]byte) { + delete(a.decoders, string(key[:])) +} + +func (a *AuthIDDecoderHolder) Match(authID [16]byte) (interface{}, error) { + for _, v := range a.decoders { + t, z, _, d := v.dec.Decode(authID) + if z != crc32.ChecksumIEEE(d[:12]) { + continue + } + + if t < 0 { + return nil, ErrNeagtiveTime + } + + if math.Abs(math.Abs(float64(t))-float64(time.Now().Unix())) > 120 { + return nil, ErrInvalidTime + } + + if !a.filter.Check(authID) { + return nil, ErrReplay + } + + return v.ticket, nil + } + return nil, ErrNotFound +} diff --git a/subproject/Xray-core-main/proxy/vmess/aead/authid_test.go b/subproject/Xray-core-main/proxy/vmess/aead/authid_test.go new file mode 100644 index 00000000..837d3372 --- /dev/null +++ b/subproject/Xray-core-main/proxy/vmess/aead/authid_test.go @@ -0,0 +1,127 @@ +package aead + +import ( + "fmt" + "strconv" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestCreateAuthID(t *testing.T) { + key := KDF16([]byte("Demo Key for Auth ID Test"), "Demo Path for Auth ID Test") + authid := CreateAuthID(key, time.Now().Unix()) + + fmt.Println(key) + fmt.Println(authid) +} + +func TestCreateAuthIDAndDecode(t *testing.T) { + key := KDF16([]byte("Demo Key for Auth ID Test"), "Demo Path for Auth ID Test") + authid := CreateAuthID(key, time.Now().Unix()) + + fmt.Println(key) + fmt.Println(authid) + + AuthDecoder := NewAuthIDDecoderHolder() + var keyw [16]byte + copy(keyw[:], key) + AuthDecoder.AddUser(keyw, "Demo User") + res, err := AuthDecoder.Match(authid) + fmt.Println(res) + fmt.Println(err) + assert.Equal(t, "Demo User", res) + assert.Nil(t, err) +} + +func TestCreateAuthIDAndDecode2(t *testing.T) { + key := KDF16([]byte("Demo Key for Auth ID Test"), "Demo Path for Auth ID Test") + authid := CreateAuthID(key, time.Now().Unix()) + + fmt.Println(key) + fmt.Println(authid) + + AuthDecoder := NewAuthIDDecoderHolder() + var keyw [16]byte + copy(keyw[:], key) + AuthDecoder.AddUser(keyw, "Demo User") + res, err := AuthDecoder.Match(authid) + fmt.Println(res) + fmt.Println(err) + assert.Equal(t, "Demo User", res) + assert.Nil(t, err) + + key2 := KDF16([]byte("Demo Key for Auth ID Test2"), "Demo Path for Auth ID Test") + authid2 := CreateAuthID(key2, time.Now().Unix()) + + res2, err2 := AuthDecoder.Match(authid2) + assert.EqualError(t, err2, "user do not exist") + assert.Nil(t, res2) +} + +func TestCreateAuthIDAndDecodeMassive(t *testing.T) { + key := KDF16([]byte("Demo Key for Auth ID Test"), "Demo Path for Auth ID Test") + authid := CreateAuthID(key, time.Now().Unix()) + + fmt.Println(key) + fmt.Println(authid) + + AuthDecoder := NewAuthIDDecoderHolder() + var keyw [16]byte + copy(keyw[:], key) + AuthDecoder.AddUser(keyw, "Demo User") + res, err := AuthDecoder.Match(authid) + fmt.Println(res) + fmt.Println(err) + assert.Equal(t, "Demo User", res) + assert.Nil(t, err) + + for i := 0; i <= 10000; i++ { + key2 := KDF16([]byte("Demo Key for Auth ID Test2"), "Demo Path for Auth ID Test", strconv.Itoa(i)) + var keyw2 [16]byte + copy(keyw2[:], key2) + AuthDecoder.AddUser(keyw2, "Demo User"+strconv.Itoa(i)) + } + + authid3 := CreateAuthID(key, time.Now().Unix()) + + res2, err2 := AuthDecoder.Match(authid3) + assert.Equal(t, "Demo User", res2) + assert.Nil(t, err2) +} + +func TestCreateAuthIDAndDecodeSuperMassive(t *testing.T) { + key := KDF16([]byte("Demo Key for Auth ID Test"), "Demo Path for Auth ID Test") + authid := CreateAuthID(key, time.Now().Unix()) + + fmt.Println(key) + fmt.Println(authid) + + AuthDecoder := NewAuthIDDecoderHolder() + var keyw [16]byte + copy(keyw[:], key) + AuthDecoder.AddUser(keyw, "Demo User") + res, err := AuthDecoder.Match(authid) + fmt.Println(res) + fmt.Println(err) + assert.Equal(t, "Demo User", res) + assert.Nil(t, err) + + for i := 0; i <= 1000000; i++ { + key2 := KDF16([]byte("Demo Key for Auth ID Test2"), "Demo Path for Auth ID Test", strconv.Itoa(i)) + var keyw2 [16]byte + copy(keyw2[:], key2) + AuthDecoder.AddUser(keyw2, "Demo User"+strconv.Itoa(i)) + } + + authid3 := CreateAuthID(key, time.Now().Unix()) + + before := time.Now() + res2, err2 := AuthDecoder.Match(authid3) + after := time.Now() + assert.Equal(t, "Demo User", res2) + assert.Nil(t, err2) + + fmt.Println(after.Sub(before).Seconds()) +} diff --git a/subproject/Xray-core-main/proxy/vmess/aead/consts.go b/subproject/Xray-core-main/proxy/vmess/aead/consts.go new file mode 100644 index 00000000..ef13977d --- /dev/null +++ b/subproject/Xray-core-main/proxy/vmess/aead/consts.go @@ -0,0 +1,14 @@ +package aead + +const ( + KDFSaltConstAuthIDEncryptionKey = "AES Auth ID Encryption" + KDFSaltConstAEADRespHeaderLenKey = "AEAD Resp Header Len Key" + KDFSaltConstAEADRespHeaderLenIV = "AEAD Resp Header Len IV" + KDFSaltConstAEADRespHeaderPayloadKey = "AEAD Resp Header Key" + KDFSaltConstAEADRespHeaderPayloadIV = "AEAD Resp Header IV" + KDFSaltConstVMessAEADKDF = "VMess AEAD KDF" + KDFSaltConstVMessHeaderPayloadAEADKey = "VMess Header AEAD Key" + KDFSaltConstVMessHeaderPayloadAEADIV = "VMess Header AEAD Nonce" + KDFSaltConstVMessHeaderPayloadLengthAEADKey = "VMess Header AEAD Key_Length" + KDFSaltConstVMessHeaderPayloadLengthAEADIV = "VMess Header AEAD Nonce_Length" +) diff --git a/subproject/Xray-core-main/proxy/vmess/aead/encrypt.go b/subproject/Xray-core-main/proxy/vmess/aead/encrypt.go new file mode 100644 index 00000000..a3faec7e --- /dev/null +++ b/subproject/Xray-core-main/proxy/vmess/aead/encrypt.go @@ -0,0 +1,135 @@ +package aead + +import ( + "bytes" + "crypto/rand" + "encoding/binary" + "io" + "time" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/crypto" +) + +func SealVMessAEADHeader(key [16]byte, data []byte) []byte { + generatedAuthID := CreateAuthID(key[:], time.Now().Unix()) + + connectionNonce := make([]byte, 8) + if _, err := io.ReadFull(rand.Reader, connectionNonce); err != nil { + panic(err.Error()) + } + + aeadPayloadLengthSerializeBuffer := bytes.NewBuffer(nil) + + headerPayloadDataLen := uint16(len(data)) + + common.Must(binary.Write(aeadPayloadLengthSerializeBuffer, binary.BigEndian, headerPayloadDataLen)) + + aeadPayloadLengthSerializedByte := aeadPayloadLengthSerializeBuffer.Bytes() + var payloadHeaderLengthAEADEncrypted []byte + + { + payloadHeaderLengthAEADKey := KDF16(key[:], KDFSaltConstVMessHeaderPayloadLengthAEADKey, string(generatedAuthID[:]), string(connectionNonce)) + + payloadHeaderLengthAEADNonce := KDF(key[:], KDFSaltConstVMessHeaderPayloadLengthAEADIV, string(generatedAuthID[:]), string(connectionNonce))[:12] + + payloadHeaderAEAD := crypto.NewAesGcm(payloadHeaderLengthAEADKey) + + payloadHeaderLengthAEADEncrypted = payloadHeaderAEAD.Seal(nil, payloadHeaderLengthAEADNonce, aeadPayloadLengthSerializedByte, generatedAuthID[:]) + } + + var payloadHeaderAEADEncrypted []byte + + { + payloadHeaderAEADKey := KDF16(key[:], KDFSaltConstVMessHeaderPayloadAEADKey, string(generatedAuthID[:]), string(connectionNonce)) + + payloadHeaderAEADNonce := KDF(key[:], KDFSaltConstVMessHeaderPayloadAEADIV, string(generatedAuthID[:]), string(connectionNonce))[:12] + + payloadHeaderAEAD := crypto.NewAesGcm(payloadHeaderAEADKey) + + payloadHeaderAEADEncrypted = payloadHeaderAEAD.Seal(nil, payloadHeaderAEADNonce, data, generatedAuthID[:]) + } + + outputBuffer := bytes.NewBuffer(nil) + + common.Must2(outputBuffer.Write(generatedAuthID[:])) // 16 + common.Must2(outputBuffer.Write(payloadHeaderLengthAEADEncrypted)) // 2+16 + common.Must2(outputBuffer.Write(connectionNonce)) // 8 + common.Must2(outputBuffer.Write(payloadHeaderAEADEncrypted)) + + return outputBuffer.Bytes() +} + +func OpenVMessAEADHeader(key [16]byte, authid [16]byte, data io.Reader) ([]byte, bool, int, error) { + var payloadHeaderLengthAEADEncrypted [18]byte + var nonce [8]byte + + var bytesRead int + + authidCheckValueReadBytesCounts, err := io.ReadFull(data, payloadHeaderLengthAEADEncrypted[:]) + bytesRead += authidCheckValueReadBytesCounts + if err != nil { + return nil, false, bytesRead, err + } + + nonceReadBytesCounts, err := io.ReadFull(data, nonce[:]) + bytesRead += nonceReadBytesCounts + if err != nil { + return nil, false, bytesRead, err + } + + // Decrypt Length + + var decryptedAEADHeaderLengthPayloadResult []byte + + { + payloadHeaderLengthAEADKey := KDF16(key[:], KDFSaltConstVMessHeaderPayloadLengthAEADKey, string(authid[:]), string(nonce[:])) + + payloadHeaderLengthAEADNonce := KDF(key[:], KDFSaltConstVMessHeaderPayloadLengthAEADIV, string(authid[:]), string(nonce[:]))[:12] + + payloadHeaderLengthAEAD := crypto.NewAesGcm(payloadHeaderLengthAEADKey) + + decryptedAEADHeaderLengthPayload, erropenAEAD := payloadHeaderLengthAEAD.Open(nil, payloadHeaderLengthAEADNonce, payloadHeaderLengthAEADEncrypted[:], authid[:]) + + if erropenAEAD != nil { + return nil, true, bytesRead, erropenAEAD + } + + decryptedAEADHeaderLengthPayloadResult = decryptedAEADHeaderLengthPayload + } + + var length uint16 + + common.Must(binary.Read(bytes.NewReader(decryptedAEADHeaderLengthPayloadResult), binary.BigEndian, &length)) + + var decryptedAEADHeaderPayloadR []byte + + var payloadHeaderAEADEncryptedReadedBytesCounts int + + { + payloadHeaderAEADKey := KDF16(key[:], KDFSaltConstVMessHeaderPayloadAEADKey, string(authid[:]), string(nonce[:])) + + payloadHeaderAEADNonce := KDF(key[:], KDFSaltConstVMessHeaderPayloadAEADIV, string(authid[:]), string(nonce[:]))[:12] + + // 16 == AEAD Tag size + payloadHeaderAEADEncrypted := make([]byte, length+16) + + payloadHeaderAEADEncryptedReadedBytesCounts, err = io.ReadFull(data, payloadHeaderAEADEncrypted) + bytesRead += payloadHeaderAEADEncryptedReadedBytesCounts + if err != nil { + return nil, false, bytesRead, err + } + + payloadHeaderAEAD := crypto.NewAesGcm(payloadHeaderAEADKey) + + decryptedAEADHeaderPayload, erropenAEAD := payloadHeaderAEAD.Open(nil, payloadHeaderAEADNonce, payloadHeaderAEADEncrypted, authid[:]) + + if erropenAEAD != nil { + return nil, true, bytesRead, erropenAEAD + } + + decryptedAEADHeaderPayloadR = decryptedAEADHeaderPayload + } + + return decryptedAEADHeaderPayloadR, false, bytesRead, nil +} diff --git a/subproject/Xray-core-main/proxy/vmess/aead/encrypt_test.go b/subproject/Xray-core-main/proxy/vmess/aead/encrypt_test.go new file mode 100644 index 00000000..5f8e33cf --- /dev/null +++ b/subproject/Xray-core-main/proxy/vmess/aead/encrypt_test.go @@ -0,0 +1,104 @@ +package aead + +import ( + "bytes" + "fmt" + "io" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestOpenVMessAEADHeader(t *testing.T) { + TestHeader := []byte("Test Header") + key := KDF16([]byte("Demo Key for Auth ID Test"), "Demo Path for Auth ID Test") + var keyw [16]byte + copy(keyw[:], key) + sealed := SealVMessAEADHeader(keyw, TestHeader) + + AEADR := bytes.NewReader(sealed) + + var authid [16]byte + + io.ReadFull(AEADR, authid[:]) + + out, _, _, err := OpenVMessAEADHeader(keyw, authid, AEADR) + + fmt.Println(string(out)) + fmt.Println(err) +} + +func TestOpenVMessAEADHeader2(t *testing.T) { + TestHeader := []byte("Test Header") + key := KDF16([]byte("Demo Key for Auth ID Test"), "Demo Path for Auth ID Test") + var keyw [16]byte + copy(keyw[:], key) + sealed := SealVMessAEADHeader(keyw, TestHeader) + + AEADR := bytes.NewReader(sealed) + + var authid [16]byte + + io.ReadFull(AEADR, authid[:]) + + out, _, readen, err := OpenVMessAEADHeader(keyw, authid, AEADR) + assert.Equal(t, len(sealed)-16-AEADR.Len(), readen) + assert.Equal(t, string(TestHeader), string(out)) + assert.Nil(t, err) +} + +func TestOpenVMessAEADHeader4(t *testing.T) { + for i := 0; i <= 60; i++ { + TestHeader := []byte("Test Header") + key := KDF16([]byte("Demo Key for Auth ID Test"), "Demo Path for Auth ID Test") + var keyw [16]byte + copy(keyw[:], key) + sealed := SealVMessAEADHeader(keyw, TestHeader) + var sealedm [16]byte + copy(sealedm[:], sealed) + sealed[i] ^= 0xff + AEADR := bytes.NewReader(sealed) + + var authid [16]byte + + io.ReadFull(AEADR, authid[:]) + + out, drain, readen, err := OpenVMessAEADHeader(keyw, authid, AEADR) + assert.Equal(t, len(sealed)-16-AEADR.Len(), readen) + assert.Equal(t, true, drain) + assert.NotNil(t, err) + if err == nil { + fmt.Println(">") + } + assert.Nil(t, out) + } +} + +func TestOpenVMessAEADHeader4Massive(t *testing.T) { + for j := 0; j < 1000; j++ { + for i := 0; i <= 60; i++ { + TestHeader := []byte("Test Header") + key := KDF16([]byte("Demo Key for Auth ID Test"), "Demo Path for Auth ID Test") + var keyw [16]byte + copy(keyw[:], key) + sealed := SealVMessAEADHeader(keyw, TestHeader) + var sealedm [16]byte + copy(sealedm[:], sealed) + sealed[i] ^= 0xff + AEADR := bytes.NewReader(sealed) + + var authid [16]byte + + io.ReadFull(AEADR, authid[:]) + + out, drain, readen, err := OpenVMessAEADHeader(keyw, authid, AEADR) + assert.Equal(t, len(sealed)-16-AEADR.Len(), readen) + assert.Equal(t, true, drain) + assert.NotNil(t, err) + if err == nil { + fmt.Println(">") + } + assert.Nil(t, out) + } + } +} diff --git a/subproject/Xray-core-main/proxy/vmess/aead/kdf.go b/subproject/Xray-core-main/proxy/vmess/aead/kdf.go new file mode 100644 index 00000000..8194f5c5 --- /dev/null +++ b/subproject/Xray-core-main/proxy/vmess/aead/kdf.go @@ -0,0 +1,33 @@ +package aead + +import ( + "crypto/hmac" + "crypto/sha256" + "hash" +) + +type hash2 struct { + hash.Hash +} + +func KDF(key []byte, path ...string) []byte { + hmacf := hmac.New(sha256.New, []byte(KDFSaltConstVMessAEADKDF)) + + for _, v := range path { + first := true + hmacf = hmac.New(func() hash.Hash { + if first { + first = false + return hash2{hmacf} + } + return hmacf + }, []byte(v)) + } + hmacf.Write(key) + return hmacf.Sum(nil) +} + +func KDF16(key []byte, path ...string) []byte { + r := KDF(key, path...) + return r[:16] +} diff --git a/subproject/Xray-core-main/proxy/vmess/encoding/auth.go b/subproject/Xray-core-main/proxy/vmess/encoding/auth.go new file mode 100644 index 00000000..99bdaa49 --- /dev/null +++ b/subproject/Xray-core-main/proxy/vmess/encoding/auth.go @@ -0,0 +1,99 @@ +package encoding + +import ( + "crypto/md5" + "encoding/binary" + "hash/fnv" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/crypto" + "golang.org/x/crypto/sha3" +) + +// Authenticate authenticates a byte array using Fnv hash. +func Authenticate(b []byte) uint32 { + fnv1hash := fnv.New32a() + common.Must2(fnv1hash.Write(b)) + return fnv1hash.Sum32() +} + +// [DEPRECATED 2023-06] +type NoOpAuthenticator struct{} + +func (NoOpAuthenticator) NonceSize() int { + return 0 +} + +func (NoOpAuthenticator) Overhead() int { + return 0 +} + +// Seal implements AEAD.Seal(). +func (NoOpAuthenticator) Seal(dst, nonce, plaintext, additionalData []byte) []byte { + return append(dst[:0], plaintext...) +} + +// Open implements AEAD.Open(). +func (NoOpAuthenticator) Open(dst, nonce, ciphertext, additionalData []byte) ([]byte, error) { + return append(dst[:0], ciphertext...), nil +} + +// GenerateChacha20Poly1305Key generates a 32-byte key from a given 16-byte array. +func GenerateChacha20Poly1305Key(b []byte) []byte { + key := make([]byte, 32) + t := md5.Sum(b) + copy(key, t[:]) + t = md5.Sum(key[:16]) + copy(key[16:], t[:]) + return key +} + +type ShakeSizeParser struct { + shake sha3.ShakeHash + buffer [2]byte +} + +func NewShakeSizeParser(nonce []byte) *ShakeSizeParser { + shake := sha3.NewShake128() + common.Must2(shake.Write(nonce)) + return &ShakeSizeParser{ + shake: shake, + } +} + +func (*ShakeSizeParser) SizeBytes() int32 { + return 2 +} + +func (s *ShakeSizeParser) next() uint16 { + common.Must2(s.shake.Read(s.buffer[:])) + return binary.BigEndian.Uint16(s.buffer[:]) +} + +func (s *ShakeSizeParser) Decode(b []byte) (uint16, error) { + mask := s.next() + size := binary.BigEndian.Uint16(b) + return mask ^ size, nil +} + +func (s *ShakeSizeParser) Encode(size uint16, b []byte) []byte { + mask := s.next() + binary.BigEndian.PutUint16(b, mask^size) + return b[:2] +} + +func (s *ShakeSizeParser) NextPaddingLen() uint16 { + return s.next() % 64 +} + +func (s *ShakeSizeParser) MaxPaddingLen() uint16 { + return 64 +} + +type AEADSizeParser struct { + crypto.AEADChunkSizeParser +} + +func NewAEADSizeParser(auth *crypto.AEADAuthenticator) *AEADSizeParser { + return &AEADSizeParser{crypto.AEADChunkSizeParser{Auth: auth}} +} diff --git a/subproject/Xray-core-main/proxy/vmess/encoding/client.go b/subproject/Xray-core-main/proxy/vmess/encoding/client.go new file mode 100644 index 00000000..d48eddd7 --- /dev/null +++ b/subproject/Xray-core-main/proxy/vmess/encoding/client.go @@ -0,0 +1,340 @@ +package encoding + +import ( + "bytes" + "context" + "crypto/rand" + "crypto/sha256" + "encoding/binary" + "hash/fnv" + "io" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/bitmask" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/crypto" + "github.com/xtls/xray-core/common/dice" + "github.com/xtls/xray-core/common/drain" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/proxy/vmess" + vmessaead "github.com/xtls/xray-core/proxy/vmess/aead" + "golang.org/x/crypto/chacha20poly1305" +) + +// ClientSession stores connection session info for VMess client. +type ClientSession struct { + requestBodyKey [16]byte + requestBodyIV [16]byte + responseBodyKey [16]byte + responseBodyIV [16]byte + responseReader io.Reader + responseHeader byte + + readDrainer drain.Drainer +} + +// NewClientSession creates a new ClientSession. +func NewClientSession(ctx context.Context, behaviorSeed int64) *ClientSession { + session := &ClientSession{} + + randomBytes := make([]byte, 33) // 16 + 16 + 1 + common.Must2(rand.Read(randomBytes)) + copy(session.requestBodyKey[:], randomBytes[:16]) + copy(session.requestBodyIV[:], randomBytes[16:32]) + session.responseHeader = randomBytes[32] + + BodyKey := sha256.Sum256(session.requestBodyKey[:]) + copy(session.responseBodyKey[:], BodyKey[:16]) + BodyIV := sha256.Sum256(session.requestBodyIV[:]) + copy(session.responseBodyIV[:], BodyIV[:16]) + { + var err error + session.readDrainer, err = drain.NewBehaviorSeedLimitedDrainer(behaviorSeed, 18, 3266, 64) + if err != nil { + errors.LogInfoInner(ctx, err, "unable to initialize drainer") + session.readDrainer = drain.NewNopDrainer() + } + } + + return session +} + +func (c *ClientSession) EncodeRequestHeader(header *protocol.RequestHeader, writer io.Writer) error { + account := header.User.Account.(*vmess.MemoryAccount) + + buffer := buf.New() + defer buffer.Release() + + common.Must(buffer.WriteByte(Version)) + common.Must2(buffer.Write(c.requestBodyIV[:])) + common.Must2(buffer.Write(c.requestBodyKey[:])) + common.Must(buffer.WriteByte(c.responseHeader)) + common.Must(buffer.WriteByte(byte(header.Option))) + + paddingLen := dice.Roll(16) + security := byte(paddingLen<<4) | byte(header.Security) + common.Must2(buffer.Write([]byte{security, byte(0), byte(header.Command)})) + + if header.Command != protocol.RequestCommandMux { + if err := addrParser.WriteAddressPort(buffer, header.Address, header.Port); err != nil { + return errors.New("failed to writer address and port").Base(err) + } + } + + if paddingLen > 0 { + common.Must2(buffer.ReadFullFrom(rand.Reader, int32(paddingLen))) + } + + { + fnv1a := fnv.New32a() + common.Must2(fnv1a.Write(buffer.Bytes())) + hashBytes := buffer.Extend(int32(fnv1a.Size())) + fnv1a.Sum(hashBytes[:0]) + } + + var fixedLengthCmdKey [16]byte + copy(fixedLengthCmdKey[:], account.ID.CmdKey()) + vmessout := vmessaead.SealVMessAEADHeader(fixedLengthCmdKey, buffer.Bytes()) + common.Must2(io.Copy(writer, bytes.NewReader(vmessout))) + + return nil +} + +func (c *ClientSession) EncodeRequestBody(request *protocol.RequestHeader, writer io.Writer) (buf.Writer, error) { + var sizeParser crypto.ChunkSizeEncoder = crypto.PlainChunkSizeParser{} + if request.Option.Has(protocol.RequestOptionChunkMasking) { + sizeParser = NewShakeSizeParser(c.requestBodyIV[:]) + } + var padding crypto.PaddingLengthGenerator + if request.Option.Has(protocol.RequestOptionGlobalPadding) { + var ok bool + padding, ok = sizeParser.(crypto.PaddingLengthGenerator) + if !ok { + return nil, errors.New("invalid option: RequestOptionGlobalPadding") + } + } + + switch request.Security { + case protocol.SecurityType_NONE: + if request.Option.Has(protocol.RequestOptionChunkStream) { + if request.Command.TransferType() == protocol.TransferTypeStream { + return crypto.NewChunkStreamWriter(sizeParser, writer), nil + } + auth := &crypto.AEADAuthenticator{ + AEAD: new(NoOpAuthenticator), + NonceGenerator: crypto.GenerateEmptyBytes(), + AdditionalDataGenerator: crypto.GenerateEmptyBytes(), + } + return crypto.NewAuthenticationWriter(auth, sizeParser, writer, protocol.TransferTypePacket, padding), nil + } + + return buf.NewWriter(writer), nil + case protocol.SecurityType_AES128_GCM: + aead := crypto.NewAesGcm(c.requestBodyKey[:]) + auth := &crypto.AEADAuthenticator{ + AEAD: aead, + NonceGenerator: GenerateChunkNonce(c.requestBodyIV[:], uint32(aead.NonceSize())), + AdditionalDataGenerator: crypto.GenerateEmptyBytes(), + } + if request.Option.Has(protocol.RequestOptionAuthenticatedLength) { + AuthenticatedLengthKey := vmessaead.KDF16(c.requestBodyKey[:], "auth_len") + AuthenticatedLengthKeyAEAD := crypto.NewAesGcm(AuthenticatedLengthKey) + + lengthAuth := &crypto.AEADAuthenticator{ + AEAD: AuthenticatedLengthKeyAEAD, + NonceGenerator: GenerateChunkNonce(c.requestBodyIV[:], uint32(aead.NonceSize())), + AdditionalDataGenerator: crypto.GenerateEmptyBytes(), + } + sizeParser = NewAEADSizeParser(lengthAuth) + } + return crypto.NewAuthenticationWriter(auth, sizeParser, writer, request.Command.TransferType(), padding), nil + case protocol.SecurityType_CHACHA20_POLY1305: + aead, err := chacha20poly1305.New(GenerateChacha20Poly1305Key(c.requestBodyKey[:])) + common.Must(err) + + auth := &crypto.AEADAuthenticator{ + AEAD: aead, + NonceGenerator: GenerateChunkNonce(c.requestBodyIV[:], uint32(aead.NonceSize())), + AdditionalDataGenerator: crypto.GenerateEmptyBytes(), + } + if request.Option.Has(protocol.RequestOptionAuthenticatedLength) { + AuthenticatedLengthKey := vmessaead.KDF16(c.requestBodyKey[:], "auth_len") + AuthenticatedLengthKeyAEAD, err := chacha20poly1305.New(GenerateChacha20Poly1305Key(AuthenticatedLengthKey)) + common.Must(err) + + lengthAuth := &crypto.AEADAuthenticator{ + AEAD: AuthenticatedLengthKeyAEAD, + NonceGenerator: GenerateChunkNonce(c.requestBodyIV[:], uint32(aead.NonceSize())), + AdditionalDataGenerator: crypto.GenerateEmptyBytes(), + } + sizeParser = NewAEADSizeParser(lengthAuth) + } + return crypto.NewAuthenticationWriter(auth, sizeParser, writer, request.Command.TransferType(), padding), nil + default: + return nil, errors.New("invalid option: Security") + } +} + +func (c *ClientSession) DecodeResponseHeader(reader io.Reader) (*protocol.ResponseHeader, error) { + aeadResponseHeaderLengthEncryptionKey := vmessaead.KDF16(c.responseBodyKey[:], vmessaead.KDFSaltConstAEADRespHeaderLenKey) + aeadResponseHeaderLengthEncryptionIV := vmessaead.KDF(c.responseBodyIV[:], vmessaead.KDFSaltConstAEADRespHeaderLenIV)[:12] + + aeadResponseHeaderLengthEncryptionAEAD := crypto.NewAesGcm(aeadResponseHeaderLengthEncryptionKey) + + var aeadEncryptedResponseHeaderLength [18]byte + var decryptedResponseHeaderLength int + var decryptedResponseHeaderLengthBinaryDeserializeBuffer uint16 + + if n, err := io.ReadFull(reader, aeadEncryptedResponseHeaderLength[:]); err != nil { + c.readDrainer.AcknowledgeReceive(n) + return nil, drain.WithError(c.readDrainer, reader, errors.New("Unable to Read Header Len").Base(err)) + } else { // nolint: golint + c.readDrainer.AcknowledgeReceive(n) + } + if decryptedResponseHeaderLengthBinaryBuffer, err := aeadResponseHeaderLengthEncryptionAEAD.Open(nil, aeadResponseHeaderLengthEncryptionIV, aeadEncryptedResponseHeaderLength[:], nil); err != nil { + return nil, drain.WithError(c.readDrainer, reader, errors.New("Failed To Decrypt Length").Base(err)) + } else { // nolint: golint + common.Must(binary.Read(bytes.NewReader(decryptedResponseHeaderLengthBinaryBuffer), binary.BigEndian, &decryptedResponseHeaderLengthBinaryDeserializeBuffer)) + decryptedResponseHeaderLength = int(decryptedResponseHeaderLengthBinaryDeserializeBuffer) + } + + aeadResponseHeaderPayloadEncryptionKey := vmessaead.KDF16(c.responseBodyKey[:], vmessaead.KDFSaltConstAEADRespHeaderPayloadKey) + aeadResponseHeaderPayloadEncryptionIV := vmessaead.KDF(c.responseBodyIV[:], vmessaead.KDFSaltConstAEADRespHeaderPayloadIV)[:12] + + aeadResponseHeaderPayloadEncryptionAEAD := crypto.NewAesGcm(aeadResponseHeaderPayloadEncryptionKey) + + encryptedResponseHeaderBuffer := make([]byte, decryptedResponseHeaderLength+16) + + if n, err := io.ReadFull(reader, encryptedResponseHeaderBuffer); err != nil { + c.readDrainer.AcknowledgeReceive(n) + return nil, drain.WithError(c.readDrainer, reader, errors.New("Unable to Read Header Data").Base(err)) + } else { // nolint: golint + c.readDrainer.AcknowledgeReceive(n) + } + + if decryptedResponseHeaderBuffer, err := aeadResponseHeaderPayloadEncryptionAEAD.Open(nil, aeadResponseHeaderPayloadEncryptionIV, encryptedResponseHeaderBuffer, nil); err != nil { + return nil, drain.WithError(c.readDrainer, reader, errors.New("Failed To Decrypt Payload").Base(err)) + } else { // nolint: golint + c.responseReader = bytes.NewReader(decryptedResponseHeaderBuffer) + } + + buffer := buf.StackNew() + defer buffer.Release() + + if _, err := buffer.ReadFullFrom(c.responseReader, 4); err != nil { + return nil, errors.New("failed to read response header").Base(err).AtWarning() + } + + if buffer.Byte(0) != c.responseHeader { + return nil, errors.New("unexpected response header. Expecting ", int(c.responseHeader), " but actually ", int(buffer.Byte(0))) + } + + header := &protocol.ResponseHeader{ + Option: bitmask.Byte(buffer.Byte(1)), + } + + if buffer.Byte(2) != 0 { + cmdID := buffer.Byte(2) + dataLen := int32(buffer.Byte(3)) + + buffer.Clear() + if _, err := buffer.ReadFullFrom(c.responseReader, dataLen); err != nil { + return nil, errors.New("failed to read response command").Base(err) + } + command, err := UnmarshalCommand(cmdID, buffer.Bytes()) + if err == nil { + header.Command = command + } + } + aesStream := crypto.NewAesDecryptionStream(c.responseBodyKey[:], c.responseBodyIV[:]) + c.responseReader = crypto.NewCryptionReader(aesStream, reader) + return header, nil +} + +func (c *ClientSession) DecodeResponseBody(request *protocol.RequestHeader, reader io.Reader) (buf.Reader, error) { + var sizeParser crypto.ChunkSizeDecoder = crypto.PlainChunkSizeParser{} + if request.Option.Has(protocol.RequestOptionChunkMasking) { + sizeParser = NewShakeSizeParser(c.responseBodyIV[:]) + } + var padding crypto.PaddingLengthGenerator + if request.Option.Has(protocol.RequestOptionGlobalPadding) { + var ok bool + padding, ok = sizeParser.(crypto.PaddingLengthGenerator) + if !ok { + return nil, errors.New("invalid option: RequestOptionGlobalPadding") + } + } + + switch request.Security { + case protocol.SecurityType_NONE: + if request.Option.Has(protocol.RequestOptionChunkStream) { + if request.Command.TransferType() == protocol.TransferTypeStream { + return crypto.NewChunkStreamReader(sizeParser, reader), nil + } + + auth := &crypto.AEADAuthenticator{ + AEAD: new(NoOpAuthenticator), + NonceGenerator: crypto.GenerateEmptyBytes(), + AdditionalDataGenerator: crypto.GenerateEmptyBytes(), + } + + return crypto.NewAuthenticationReader(auth, sizeParser, reader, protocol.TransferTypePacket, padding), nil + } + + return buf.NewReader(reader), nil + case protocol.SecurityType_AES128_GCM: + aead := crypto.NewAesGcm(c.responseBodyKey[:]) + + auth := &crypto.AEADAuthenticator{ + AEAD: aead, + NonceGenerator: GenerateChunkNonce(c.responseBodyIV[:], uint32(aead.NonceSize())), + AdditionalDataGenerator: crypto.GenerateEmptyBytes(), + } + if request.Option.Has(protocol.RequestOptionAuthenticatedLength) { + AuthenticatedLengthKey := vmessaead.KDF16(c.requestBodyKey[:], "auth_len") + AuthenticatedLengthKeyAEAD := crypto.NewAesGcm(AuthenticatedLengthKey) + + lengthAuth := &crypto.AEADAuthenticator{ + AEAD: AuthenticatedLengthKeyAEAD, + NonceGenerator: GenerateChunkNonce(c.requestBodyIV[:], uint32(aead.NonceSize())), + AdditionalDataGenerator: crypto.GenerateEmptyBytes(), + } + sizeParser = NewAEADSizeParser(lengthAuth) + } + return crypto.NewAuthenticationReader(auth, sizeParser, reader, request.Command.TransferType(), padding), nil + case protocol.SecurityType_CHACHA20_POLY1305: + aead, _ := chacha20poly1305.New(GenerateChacha20Poly1305Key(c.responseBodyKey[:])) + + auth := &crypto.AEADAuthenticator{ + AEAD: aead, + NonceGenerator: GenerateChunkNonce(c.responseBodyIV[:], uint32(aead.NonceSize())), + AdditionalDataGenerator: crypto.GenerateEmptyBytes(), + } + if request.Option.Has(protocol.RequestOptionAuthenticatedLength) { + AuthenticatedLengthKey := vmessaead.KDF16(c.requestBodyKey[:], "auth_len") + AuthenticatedLengthKeyAEAD, err := chacha20poly1305.New(GenerateChacha20Poly1305Key(AuthenticatedLengthKey)) + common.Must(err) + + lengthAuth := &crypto.AEADAuthenticator{ + AEAD: AuthenticatedLengthKeyAEAD, + NonceGenerator: GenerateChunkNonce(c.requestBodyIV[:], uint32(aead.NonceSize())), + AdditionalDataGenerator: crypto.GenerateEmptyBytes(), + } + sizeParser = NewAEADSizeParser(lengthAuth) + } + return crypto.NewAuthenticationReader(auth, sizeParser, reader, request.Command.TransferType(), padding), nil + default: + return nil, errors.New("invalid option: Security") + } +} + +func GenerateChunkNonce(nonce []byte, size uint32) crypto.BytesGenerator { + c := append([]byte(nil), nonce...) + count := uint16(0) + return func() []byte { + binary.BigEndian.PutUint16(c, count) + count++ + return c[:size] + } +} diff --git a/subproject/Xray-core-main/proxy/vmess/encoding/commands.go b/subproject/Xray-core-main/proxy/vmess/encoding/commands.go new file mode 100644 index 00000000..698052c5 --- /dev/null +++ b/subproject/Xray-core-main/proxy/vmess/encoding/commands.go @@ -0,0 +1,73 @@ +package encoding + +import ( + "encoding/binary" + "io" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/protocol" +) + +var ( + ErrCommandTooLarge = errors.New("Command too large.") + ErrCommandTypeMismatch = errors.New("Command type mismatch.") + ErrInvalidAuth = errors.New("Invalid auth.") + ErrInsufficientLength = errors.New("Insufficient length.") + ErrUnknownCommand = errors.New("Unknown command.") +) + +func MarshalCommand(command interface{}, writer io.Writer) error { + if command == nil { + return ErrUnknownCommand + } + + var cmdID byte + var factory CommandFactory + switch command.(type) { + default: + return ErrUnknownCommand + } + + buffer := buf.New() + defer buffer.Release() + + err := factory.Marshal(command, buffer) + if err != nil { + return err + } + + auth := Authenticate(buffer.Bytes()) + length := buffer.Len() + 4 + if length > 255 { + return ErrCommandTooLarge + } + + common.Must2(writer.Write([]byte{cmdID, byte(length), byte(auth >> 24), byte(auth >> 16), byte(auth >> 8), byte(auth)})) + common.Must2(writer.Write(buffer.Bytes())) + return nil +} + +func UnmarshalCommand(cmdID byte, data []byte) (protocol.ResponseCommand, error) { + if len(data) <= 4 { + return nil, ErrInsufficientLength + } + expectedAuth := Authenticate(data[4:]) + actualAuth := binary.BigEndian.Uint32(data[:4]) + if expectedAuth != actualAuth { + return nil, ErrInvalidAuth + } + + var factory CommandFactory + switch cmdID { + default: + return nil, ErrUnknownCommand + } + return factory.Unmarshal(data[4:]) +} + +type CommandFactory interface { + Marshal(command interface{}, writer io.Writer) error + Unmarshal(data []byte) (interface{}, error) +} diff --git a/subproject/Xray-core-main/proxy/vmess/encoding/encoding.go b/subproject/Xray-core-main/proxy/vmess/encoding/encoding.go new file mode 100644 index 00000000..45c5d207 --- /dev/null +++ b/subproject/Xray-core-main/proxy/vmess/encoding/encoding.go @@ -0,0 +1,17 @@ +package encoding + +import ( + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" +) + +const ( + Version = byte(1) +) + +var addrParser = protocol.NewAddressParser( + protocol.AddressFamilyByte(byte(protocol.AddressTypeIPv4), net.AddressFamilyIPv4), + protocol.AddressFamilyByte(byte(protocol.AddressTypeDomain), net.AddressFamilyDomain), + protocol.AddressFamilyByte(byte(protocol.AddressTypeIPv6), net.AddressFamilyIPv6), + protocol.PortThenAddress(), +) diff --git a/subproject/Xray-core-main/proxy/vmess/encoding/encoding_test.go b/subproject/Xray-core-main/proxy/vmess/encoding/encoding_test.go new file mode 100644 index 00000000..ddae29e1 --- /dev/null +++ b/subproject/Xray-core-main/proxy/vmess/encoding/encoding_test.go @@ -0,0 +1,153 @@ +package encoding_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/uuid" + "github.com/xtls/xray-core/proxy/vmess" + . "github.com/xtls/xray-core/proxy/vmess/encoding" +) + +func toAccount(a *vmess.Account) protocol.Account { + account, err := a.AsAccount() + common.Must(err) + return account +} + +func TestRequestSerialization(t *testing.T) { + user := &protocol.MemoryUser{ + Level: 0, + Email: "test@example.com", + } + id := uuid.New() + account := &vmess.Account{ + Id: id.String(), + } + user.Account = toAccount(account) + + expectedRequest := &protocol.RequestHeader{ + Version: 1, + User: user, + Command: protocol.RequestCommandTCP, + Address: net.DomainAddress("www.example.com"), + Port: net.Port(443), + Security: protocol.SecurityType_AES128_GCM, + } + + buffer := buf.New() + client := NewClientSession(context.TODO(), 0) + common.Must(client.EncodeRequestHeader(expectedRequest, buffer)) + + buffer2 := buf.New() + buffer2.Write(buffer.Bytes()) + + sessionHistory := NewSessionHistory() + defer common.Close(sessionHistory) + + userValidator := vmess.NewTimedUserValidator() + userValidator.Add(user) + defer common.Close(userValidator) + + server := NewServerSession(userValidator, sessionHistory) + actualRequest, err := server.DecodeRequestHeader(buffer, false) + common.Must(err) + + if r := cmp.Diff(actualRequest, expectedRequest, cmp.AllowUnexported(protocol.ID{})); r != "" { + t.Error(r) + } + + _, err = server.DecodeRequestHeader(buffer2, false) + // anti replay attack + if err == nil { + t.Error("nil error") + } +} + +func TestInvalidRequest(t *testing.T) { + user := &protocol.MemoryUser{ + Level: 0, + Email: "test@example.com", + } + id := uuid.New() + account := &vmess.Account{ + Id: id.String(), + } + user.Account = toAccount(account) + + expectedRequest := &protocol.RequestHeader{ + Version: 1, + User: user, + Command: protocol.RequestCommand(100), + Address: net.DomainAddress("www.example.com"), + Port: net.Port(443), + Security: protocol.SecurityType_AES128_GCM, + } + + buffer := buf.New() + client := NewClientSession(context.TODO(), 0) + common.Must(client.EncodeRequestHeader(expectedRequest, buffer)) + + buffer2 := buf.New() + buffer2.Write(buffer.Bytes()) + + sessionHistory := NewSessionHistory() + defer common.Close(sessionHistory) + + userValidator := vmess.NewTimedUserValidator() + userValidator.Add(user) + defer common.Close(userValidator) + + server := NewServerSession(userValidator, sessionHistory) + _, err := server.DecodeRequestHeader(buffer, false) + if err == nil { + t.Error("nil error") + } +} + +func TestMuxRequest(t *testing.T) { + user := &protocol.MemoryUser{ + Level: 0, + Email: "test@example.com", + } + id := uuid.New() + account := &vmess.Account{ + Id: id.String(), + } + user.Account = toAccount(account) + + expectedRequest := &protocol.RequestHeader{ + Version: 1, + User: user, + Command: protocol.RequestCommandMux, + Security: protocol.SecurityType_AES128_GCM, + Address: net.DomainAddress("v1.mux.cool"), + } + + buffer := buf.New() + client := NewClientSession(context.TODO(), 0) + common.Must(client.EncodeRequestHeader(expectedRequest, buffer)) + + buffer2 := buf.New() + buffer2.Write(buffer.Bytes()) + + sessionHistory := NewSessionHistory() + defer common.Close(sessionHistory) + + userValidator := vmess.NewTimedUserValidator() + userValidator.Add(user) + defer common.Close(userValidator) + + server := NewServerSession(userValidator, sessionHistory) + actualRequest, err := server.DecodeRequestHeader(buffer, false) + common.Must(err) + + if r := cmp.Diff(actualRequest, expectedRequest, cmp.AllowUnexported(protocol.ID{})); r != "" { + t.Error(r) + } +} diff --git a/subproject/Xray-core-main/proxy/vmess/encoding/server.go b/subproject/Xray-core-main/proxy/vmess/encoding/server.go new file mode 100644 index 00000000..3a11c747 --- /dev/null +++ b/subproject/Xray-core-main/proxy/vmess/encoding/server.go @@ -0,0 +1,447 @@ +package encoding + +import ( + "bytes" + "crypto/sha256" + "encoding/binary" + "hash/fnv" + "io" + "sync" + "time" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/bitmask" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/crypto" + "github.com/xtls/xray-core/common/drain" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/task" + "github.com/xtls/xray-core/proxy/vmess" + vmessaead "github.com/xtls/xray-core/proxy/vmess/aead" + "golang.org/x/crypto/chacha20poly1305" +) + +type sessionID struct { + user [16]byte + key [16]byte + nonce [16]byte +} + +// SessionHistory keeps track of historical session ids, to prevent replay attacks. +type SessionHistory struct { + sync.RWMutex + cache map[sessionID]time.Time + task *task.Periodic +} + +// NewSessionHistory creates a new SessionHistory object. +func NewSessionHistory() *SessionHistory { + h := &SessionHistory{ + cache: make(map[sessionID]time.Time, 128), + } + h.task = &task.Periodic{ + Interval: time.Second * 30, + Execute: h.removeExpiredEntries, + } + return h +} + +// Close implements common.Closable. +func (h *SessionHistory) Close() error { + return h.task.Close() +} + +func (h *SessionHistory) addIfNotExits(session sessionID) bool { + h.Lock() + + if expire, found := h.cache[session]; found && expire.After(time.Now()) { + h.Unlock() + return false + } + + h.cache[session] = time.Now().Add(time.Minute * 3) + h.Unlock() + common.Must(h.task.Start()) + return true +} + +func (h *SessionHistory) removeExpiredEntries() error { + now := time.Now() + + h.Lock() + defer h.Unlock() + + if len(h.cache) == 0 { + return errors.New("nothing to do") + } + + for session, expire := range h.cache { + if expire.Before(now) { + delete(h.cache, session) + } + } + + if len(h.cache) == 0 { + h.cache = make(map[sessionID]time.Time, 128) + } + + return nil +} + +// ServerSession keeps information for a session in VMess server. +type ServerSession struct { + userValidator *vmess.TimedUserValidator + sessionHistory *SessionHistory + requestBodyKey [16]byte + requestBodyIV [16]byte + responseBodyKey [16]byte + responseBodyIV [16]byte + responseWriter io.Writer + responseHeader byte +} + +// NewServerSession creates a new ServerSession, using the given UserValidator. +// The ServerSession instance doesn't take ownership of the validator. +func NewServerSession(validator *vmess.TimedUserValidator, sessionHistory *SessionHistory) *ServerSession { + return &ServerSession{ + userValidator: validator, + sessionHistory: sessionHistory, + } +} + +func parseSecurityType(b byte) protocol.SecurityType { + if _, f := protocol.SecurityType_name[int32(b)]; f { + st := protocol.SecurityType(b) + // For backward compatibility. + if st == protocol.SecurityType_UNKNOWN { + st = protocol.SecurityType_AUTO + } + return st + } + return protocol.SecurityType_UNKNOWN +} + +// DecodeRequestHeader decodes and returns (if successful) a RequestHeader from an input stream. +func (s *ServerSession) DecodeRequestHeader(reader io.Reader, isDrain bool) (*protocol.RequestHeader, error) { + buffer := buf.New() + + drainer, err := drain.NewBehaviorSeedLimitedDrainer(int64(s.userValidator.GetBehaviorSeed()), 16+38, 3266, 64) + if err != nil { + return nil, errors.New("failed to initialize drainer").Base(err) + } + + drainConnection := func(e error) error { + // We read a deterministic generated length of data before closing the connection to offset padding read pattern + drainer.AcknowledgeReceive(int(buffer.Len())) + if isDrain { + return drain.WithError(drainer, reader, e) + } + return e + } + + defer func() { + buffer.Release() + }() + + if _, err := buffer.ReadFullFrom(reader, protocol.IDBytesLen); err != nil { + return nil, errors.New("failed to read request header").Base(err) + } + + var decryptor io.Reader + var vmessAccount *vmess.MemoryAccount + + user, foundAEAD, errorAEAD := s.userValidator.GetAEAD(buffer.Bytes()) + + var fixedSizeAuthID [16]byte + copy(fixedSizeAuthID[:], buffer.Bytes()) + + switch { + case foundAEAD: + vmessAccount = user.Account.(*vmess.MemoryAccount) + var fixedSizeCmdKey [16]byte + copy(fixedSizeCmdKey[:], vmessAccount.ID.CmdKey()) + aeadData, shouldDrain, bytesRead, errorReason := vmessaead.OpenVMessAEADHeader(fixedSizeCmdKey, fixedSizeAuthID, reader) + if errorReason != nil { + if shouldDrain { + drainer.AcknowledgeReceive(bytesRead) + return nil, drainConnection(errors.New("AEAD read failed").Base(errorReason)) + } else { + return nil, drainConnection(errors.New("AEAD read failed, drain skipped").Base(errorReason)) + } + } + decryptor = bytes.NewReader(aeadData) + default: + return nil, drainConnection(errors.New("invalid user").Base(errorAEAD)) + } + + drainer.AcknowledgeReceive(int(buffer.Len())) + buffer.Clear() + if _, err := buffer.ReadFullFrom(decryptor, 38); err != nil { + return nil, errors.New("failed to read request header").Base(err) + } + + request := &protocol.RequestHeader{ + User: user, + Version: buffer.Byte(0), + } + + copy(s.requestBodyIV[:], buffer.BytesRange(1, 17)) // 16 bytes + copy(s.requestBodyKey[:], buffer.BytesRange(17, 33)) // 16 bytes + var sid sessionID + copy(sid.user[:], vmessAccount.ID.Bytes()) + sid.key = s.requestBodyKey + sid.nonce = s.requestBodyIV + if !s.sessionHistory.addIfNotExits(sid) { + return nil, errors.New("duplicated session id, possibly under replay attack, but this is a AEAD request") + } + + s.responseHeader = buffer.Byte(33) // 1 byte + request.Option = bitmask.Byte(buffer.Byte(34)) // 1 byte + paddingLen := int(buffer.Byte(35) >> 4) + request.Security = parseSecurityType(buffer.Byte(35) & 0x0F) + // 1 bytes reserved + request.Command = protocol.RequestCommand(buffer.Byte(37)) + + switch request.Command { + case protocol.RequestCommandMux: + request.Address = net.DomainAddress("v1.mux.cool") + request.Port = 0 + + case protocol.RequestCommandTCP, protocol.RequestCommandUDP: + if addr, port, err := addrParser.ReadAddressPort(buffer, decryptor); err == nil { + request.Address = addr + request.Port = port + } + } + + if paddingLen > 0 { + if _, err := buffer.ReadFullFrom(decryptor, int32(paddingLen)); err != nil { + return nil, errors.New("failed to read padding").Base(err) + } + } + + if _, err := buffer.ReadFullFrom(decryptor, 4); err != nil { + return nil, errors.New("failed to read checksum").Base(err) + } + + fnv1a := fnv.New32a() + common.Must2(fnv1a.Write(buffer.BytesTo(-4))) + actualHash := fnv1a.Sum32() + expectedHash := binary.BigEndian.Uint32(buffer.BytesFrom(-4)) + + if actualHash != expectedHash { + return nil, errors.New("invalid auth, but this is a AEAD request") + } + + if request.Address == nil { + return nil, errors.New("invalid remote address") + } + + if request.Security == protocol.SecurityType_UNKNOWN || request.Security == protocol.SecurityType_AUTO { + return nil, errors.New("unknown security type: ", request.Security) + } + + return request, nil +} + +// DecodeRequestBody returns Reader from which caller can fetch decrypted body. +func (s *ServerSession) DecodeRequestBody(request *protocol.RequestHeader, reader io.Reader) (buf.Reader, error) { + var sizeParser crypto.ChunkSizeDecoder = crypto.PlainChunkSizeParser{} + if request.Option.Has(protocol.RequestOptionChunkMasking) { + sizeParser = NewShakeSizeParser(s.requestBodyIV[:]) + } + var padding crypto.PaddingLengthGenerator + if request.Option.Has(protocol.RequestOptionGlobalPadding) { + var ok bool + padding, ok = sizeParser.(crypto.PaddingLengthGenerator) + if !ok { + return nil, errors.New("invalid option: RequestOptionGlobalPadding") + } + } + + switch request.Security { + case protocol.SecurityType_NONE: + if request.Option.Has(protocol.RequestOptionChunkStream) { + if request.Command.TransferType() == protocol.TransferTypeStream { + return crypto.NewChunkStreamReader(sizeParser, reader), nil + } + + auth := &crypto.AEADAuthenticator{ + AEAD: new(NoOpAuthenticator), + NonceGenerator: crypto.GenerateEmptyBytes(), + AdditionalDataGenerator: crypto.GenerateEmptyBytes(), + } + return crypto.NewAuthenticationReader(auth, sizeParser, reader, protocol.TransferTypePacket, padding), nil + } + return buf.NewReader(reader), nil + + case protocol.SecurityType_AES128_GCM: + aead := crypto.NewAesGcm(s.requestBodyKey[:]) + auth := &crypto.AEADAuthenticator{ + AEAD: aead, + NonceGenerator: GenerateChunkNonce(s.requestBodyIV[:], uint32(aead.NonceSize())), + AdditionalDataGenerator: crypto.GenerateEmptyBytes(), + } + if request.Option.Has(protocol.RequestOptionAuthenticatedLength) { + AuthenticatedLengthKey := vmessaead.KDF16(s.requestBodyKey[:], "auth_len") + AuthenticatedLengthKeyAEAD := crypto.NewAesGcm(AuthenticatedLengthKey) + + lengthAuth := &crypto.AEADAuthenticator{ + AEAD: AuthenticatedLengthKeyAEAD, + NonceGenerator: GenerateChunkNonce(s.requestBodyIV[:], uint32(aead.NonceSize())), + AdditionalDataGenerator: crypto.GenerateEmptyBytes(), + } + sizeParser = NewAEADSizeParser(lengthAuth) + } + return crypto.NewAuthenticationReader(auth, sizeParser, reader, request.Command.TransferType(), padding), nil + + case protocol.SecurityType_CHACHA20_POLY1305: + aead, _ := chacha20poly1305.New(GenerateChacha20Poly1305Key(s.requestBodyKey[:])) + + auth := &crypto.AEADAuthenticator{ + AEAD: aead, + NonceGenerator: GenerateChunkNonce(s.requestBodyIV[:], uint32(aead.NonceSize())), + AdditionalDataGenerator: crypto.GenerateEmptyBytes(), + } + if request.Option.Has(protocol.RequestOptionAuthenticatedLength) { + AuthenticatedLengthKey := vmessaead.KDF16(s.requestBodyKey[:], "auth_len") + AuthenticatedLengthKeyAEAD, err := chacha20poly1305.New(GenerateChacha20Poly1305Key(AuthenticatedLengthKey)) + common.Must(err) + + lengthAuth := &crypto.AEADAuthenticator{ + AEAD: AuthenticatedLengthKeyAEAD, + NonceGenerator: GenerateChunkNonce(s.requestBodyIV[:], uint32(aead.NonceSize())), + AdditionalDataGenerator: crypto.GenerateEmptyBytes(), + } + sizeParser = NewAEADSizeParser(lengthAuth) + } + return crypto.NewAuthenticationReader(auth, sizeParser, reader, request.Command.TransferType(), padding), nil + + default: + return nil, errors.New("invalid option: Security") + } +} + +// EncodeResponseHeader writes encoded response header into the given writer. +func (s *ServerSession) EncodeResponseHeader(header *protocol.ResponseHeader, writer io.Writer) { + var encryptionWriter io.Writer + BodyKey := sha256.Sum256(s.requestBodyKey[:]) + copy(s.responseBodyKey[:], BodyKey[:16]) + BodyIV := sha256.Sum256(s.requestBodyIV[:]) + copy(s.responseBodyIV[:], BodyIV[:16]) + + aesStream := crypto.NewAesEncryptionStream(s.responseBodyKey[:], s.responseBodyIV[:]) + encryptionWriter = crypto.NewCryptionWriter(aesStream, writer) + s.responseWriter = encryptionWriter + + aeadEncryptedHeaderBuffer := bytes.NewBuffer(nil) + encryptionWriter = aeadEncryptedHeaderBuffer + + common.Must2(encryptionWriter.Write([]byte{s.responseHeader, byte(header.Option)})) + err := MarshalCommand(header.Command, encryptionWriter) + if err != nil { + common.Must2(encryptionWriter.Write([]byte{0x00, 0x00})) + } + + aeadResponseHeaderLengthEncryptionKey := vmessaead.KDF16(s.responseBodyKey[:], vmessaead.KDFSaltConstAEADRespHeaderLenKey) + aeadResponseHeaderLengthEncryptionIV := vmessaead.KDF(s.responseBodyIV[:], vmessaead.KDFSaltConstAEADRespHeaderLenIV)[:12] + + aeadResponseHeaderLengthEncryptionAEAD := crypto.NewAesGcm(aeadResponseHeaderLengthEncryptionKey) + + aeadResponseHeaderLengthEncryptionBuffer := bytes.NewBuffer(nil) + + decryptedResponseHeaderLengthBinaryDeserializeBuffer := uint16(aeadEncryptedHeaderBuffer.Len()) + + common.Must(binary.Write(aeadResponseHeaderLengthEncryptionBuffer, binary.BigEndian, decryptedResponseHeaderLengthBinaryDeserializeBuffer)) + + AEADEncryptedLength := aeadResponseHeaderLengthEncryptionAEAD.Seal(nil, aeadResponseHeaderLengthEncryptionIV, aeadResponseHeaderLengthEncryptionBuffer.Bytes(), nil) + common.Must2(io.Copy(writer, bytes.NewReader(AEADEncryptedLength))) + + aeadResponseHeaderPayloadEncryptionKey := vmessaead.KDF16(s.responseBodyKey[:], vmessaead.KDFSaltConstAEADRespHeaderPayloadKey) + aeadResponseHeaderPayloadEncryptionIV := vmessaead.KDF(s.responseBodyIV[:], vmessaead.KDFSaltConstAEADRespHeaderPayloadIV)[:12] + + aeadResponseHeaderPayloadEncryptionAEAD := crypto.NewAesGcm(aeadResponseHeaderPayloadEncryptionKey) + + aeadEncryptedHeaderPayload := aeadResponseHeaderPayloadEncryptionAEAD.Seal(nil, aeadResponseHeaderPayloadEncryptionIV, aeadEncryptedHeaderBuffer.Bytes(), nil) + common.Must2(io.Copy(writer, bytes.NewReader(aeadEncryptedHeaderPayload))) +} + +// EncodeResponseBody returns a Writer that auto-encrypt content written by caller. +func (s *ServerSession) EncodeResponseBody(request *protocol.RequestHeader, writer io.Writer) (buf.Writer, error) { + var sizeParser crypto.ChunkSizeEncoder = crypto.PlainChunkSizeParser{} + if request.Option.Has(protocol.RequestOptionChunkMasking) { + sizeParser = NewShakeSizeParser(s.responseBodyIV[:]) + } + var padding crypto.PaddingLengthGenerator + if request.Option.Has(protocol.RequestOptionGlobalPadding) { + var ok bool + padding, ok = sizeParser.(crypto.PaddingLengthGenerator) + if !ok { + return nil, errors.New("invalid option: RequestOptionGlobalPadding") + } + } + + switch request.Security { + case protocol.SecurityType_NONE: + if request.Option.Has(protocol.RequestOptionChunkStream) { + if request.Command.TransferType() == protocol.TransferTypeStream { + return crypto.NewChunkStreamWriter(sizeParser, writer), nil + } + + auth := &crypto.AEADAuthenticator{ + AEAD: new(NoOpAuthenticator), + NonceGenerator: crypto.GenerateEmptyBytes(), + AdditionalDataGenerator: crypto.GenerateEmptyBytes(), + } + return crypto.NewAuthenticationWriter(auth, sizeParser, writer, protocol.TransferTypePacket, padding), nil + } + return buf.NewWriter(writer), nil + + case protocol.SecurityType_AES128_GCM: + aead := crypto.NewAesGcm(s.responseBodyKey[:]) + auth := &crypto.AEADAuthenticator{ + AEAD: aead, + NonceGenerator: GenerateChunkNonce(s.responseBodyIV[:], uint32(aead.NonceSize())), + AdditionalDataGenerator: crypto.GenerateEmptyBytes(), + } + if request.Option.Has(protocol.RequestOptionAuthenticatedLength) { + AuthenticatedLengthKey := vmessaead.KDF16(s.requestBodyKey[:], "auth_len") + AuthenticatedLengthKeyAEAD := crypto.NewAesGcm(AuthenticatedLengthKey) + + lengthAuth := &crypto.AEADAuthenticator{ + AEAD: AuthenticatedLengthKeyAEAD, + NonceGenerator: GenerateChunkNonce(s.requestBodyIV[:], uint32(aead.NonceSize())), + AdditionalDataGenerator: crypto.GenerateEmptyBytes(), + } + sizeParser = NewAEADSizeParser(lengthAuth) + } + return crypto.NewAuthenticationWriter(auth, sizeParser, writer, request.Command.TransferType(), padding), nil + + case protocol.SecurityType_CHACHA20_POLY1305: + aead, _ := chacha20poly1305.New(GenerateChacha20Poly1305Key(s.responseBodyKey[:])) + + auth := &crypto.AEADAuthenticator{ + AEAD: aead, + NonceGenerator: GenerateChunkNonce(s.responseBodyIV[:], uint32(aead.NonceSize())), + AdditionalDataGenerator: crypto.GenerateEmptyBytes(), + } + if request.Option.Has(protocol.RequestOptionAuthenticatedLength) { + AuthenticatedLengthKey := vmessaead.KDF16(s.requestBodyKey[:], "auth_len") + AuthenticatedLengthKeyAEAD, err := chacha20poly1305.New(GenerateChacha20Poly1305Key(AuthenticatedLengthKey)) + common.Must(err) + + lengthAuth := &crypto.AEADAuthenticator{ + AEAD: AuthenticatedLengthKeyAEAD, + NonceGenerator: GenerateChunkNonce(s.requestBodyIV[:], uint32(aead.NonceSize())), + AdditionalDataGenerator: crypto.GenerateEmptyBytes(), + } + sizeParser = NewAEADSizeParser(lengthAuth) + } + return crypto.NewAuthenticationWriter(auth, sizeParser, writer, request.Command.TransferType(), padding), nil + + default: + return nil, errors.New("invalid option: Security") + } +} diff --git a/subproject/Xray-core-main/proxy/vmess/inbound/config.go b/subproject/Xray-core-main/proxy/vmess/inbound/config.go new file mode 100644 index 00000000..e4c5fbb3 --- /dev/null +++ b/subproject/Xray-core-main/proxy/vmess/inbound/config.go @@ -0,0 +1,9 @@ +package inbound + +// GetDefaultValue returns default settings of DefaultConfig. +func (c *Config) GetDefaultValue() *DefaultConfig { + if c.GetDefault() == nil { + return &DefaultConfig{} + } + return c.Default +} diff --git a/subproject/Xray-core-main/proxy/vmess/inbound/config.pb.go b/subproject/Xray-core-main/proxy/vmess/inbound/config.pb.go new file mode 100644 index 00000000..2422bbca --- /dev/null +++ b/subproject/Xray-core-main/proxy/vmess/inbound/config.pb.go @@ -0,0 +1,230 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: proxy/vmess/inbound/config.proto + +package inbound + +import ( + protocol "github.com/xtls/xray-core/common/protocol" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type DetourConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + To string `protobuf:"bytes,1,opt,name=to,proto3" json:"to,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DetourConfig) Reset() { + *x = DetourConfig{} + mi := &file_proxy_vmess_inbound_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DetourConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DetourConfig) ProtoMessage() {} + +func (x *DetourConfig) ProtoReflect() protoreflect.Message { + mi := &file_proxy_vmess_inbound_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DetourConfig.ProtoReflect.Descriptor instead. +func (*DetourConfig) Descriptor() ([]byte, []int) { + return file_proxy_vmess_inbound_config_proto_rawDescGZIP(), []int{0} +} + +func (x *DetourConfig) GetTo() string { + if x != nil { + return x.To + } + return "" +} + +type DefaultConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + Level uint32 `protobuf:"varint,2,opt,name=level,proto3" json:"level,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DefaultConfig) Reset() { + *x = DefaultConfig{} + mi := &file_proxy_vmess_inbound_config_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DefaultConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DefaultConfig) ProtoMessage() {} + +func (x *DefaultConfig) ProtoReflect() protoreflect.Message { + mi := &file_proxy_vmess_inbound_config_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DefaultConfig.ProtoReflect.Descriptor instead. +func (*DefaultConfig) Descriptor() ([]byte, []int) { + return file_proxy_vmess_inbound_config_proto_rawDescGZIP(), []int{1} +} + +func (x *DefaultConfig) GetLevel() uint32 { + if x != nil { + return x.Level + } + return 0 +} + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + User []*protocol.User `protobuf:"bytes,1,rep,name=user,proto3" json:"user,omitempty"` + Default *DefaultConfig `protobuf:"bytes,2,opt,name=default,proto3" json:"default,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_proxy_vmess_inbound_config_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_proxy_vmess_inbound_config_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_proxy_vmess_inbound_config_proto_rawDescGZIP(), []int{2} +} + +func (x *Config) GetUser() []*protocol.User { + if x != nil { + return x.User + } + return nil +} + +func (x *Config) GetDefault() *DefaultConfig { + if x != nil { + return x.Default + } + return nil +} + +var File_proxy_vmess_inbound_config_proto protoreflect.FileDescriptor + +const file_proxy_vmess_inbound_config_proto_rawDesc = "" + + "\n" + + " proxy/vmess/inbound/config.proto\x12\x18xray.proxy.vmess.inbound\x1a\x1acommon/protocol/user.proto\"\x1e\n" + + "\fDetourConfig\x12\x0e\n" + + "\x02to\x18\x01 \x01(\tR\x02to\"%\n" + + "\rDefaultConfig\x12\x14\n" + + "\x05level\x18\x02 \x01(\rR\x05level\"{\n" + + "\x06Config\x12.\n" + + "\x04user\x18\x01 \x03(\v2\x1a.xray.common.protocol.UserR\x04user\x12A\n" + + "\adefault\x18\x02 \x01(\v2'.xray.proxy.vmess.inbound.DefaultConfigR\adefaultBj\n" + + "\x1ccom.xray.proxy.vmess.inboundP\x01Z-github.com/xtls/xray-core/proxy/vmess/inbound\xaa\x02\x18Xray.Proxy.Vmess.Inboundb\x06proto3" + +var ( + file_proxy_vmess_inbound_config_proto_rawDescOnce sync.Once + file_proxy_vmess_inbound_config_proto_rawDescData []byte +) + +func file_proxy_vmess_inbound_config_proto_rawDescGZIP() []byte { + file_proxy_vmess_inbound_config_proto_rawDescOnce.Do(func() { + file_proxy_vmess_inbound_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proxy_vmess_inbound_config_proto_rawDesc), len(file_proxy_vmess_inbound_config_proto_rawDesc))) + }) + return file_proxy_vmess_inbound_config_proto_rawDescData +} + +var file_proxy_vmess_inbound_config_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_proxy_vmess_inbound_config_proto_goTypes = []any{ + (*DetourConfig)(nil), // 0: xray.proxy.vmess.inbound.DetourConfig + (*DefaultConfig)(nil), // 1: xray.proxy.vmess.inbound.DefaultConfig + (*Config)(nil), // 2: xray.proxy.vmess.inbound.Config + (*protocol.User)(nil), // 3: xray.common.protocol.User +} +var file_proxy_vmess_inbound_config_proto_depIdxs = []int32{ + 3, // 0: xray.proxy.vmess.inbound.Config.user:type_name -> xray.common.protocol.User + 1, // 1: xray.proxy.vmess.inbound.Config.default:type_name -> xray.proxy.vmess.inbound.DefaultConfig + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_proxy_vmess_inbound_config_proto_init() } +func file_proxy_vmess_inbound_config_proto_init() { + if File_proxy_vmess_inbound_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_proxy_vmess_inbound_config_proto_rawDesc), len(file_proxy_vmess_inbound_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_proxy_vmess_inbound_config_proto_goTypes, + DependencyIndexes: file_proxy_vmess_inbound_config_proto_depIdxs, + MessageInfos: file_proxy_vmess_inbound_config_proto_msgTypes, + }.Build() + File_proxy_vmess_inbound_config_proto = out.File + file_proxy_vmess_inbound_config_proto_goTypes = nil + file_proxy_vmess_inbound_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/proxy/vmess/inbound/config.proto b/subproject/Xray-core-main/proxy/vmess/inbound/config.proto new file mode 100644 index 00000000..89311ddf --- /dev/null +++ b/subproject/Xray-core-main/proxy/vmess/inbound/config.proto @@ -0,0 +1,22 @@ +syntax = "proto3"; + +package xray.proxy.vmess.inbound; +option csharp_namespace = "Xray.Proxy.Vmess.Inbound"; +option go_package = "github.com/xtls/xray-core/proxy/vmess/inbound"; +option java_package = "com.xray.proxy.vmess.inbound"; +option java_multiple_files = true; + +import "common/protocol/user.proto"; + +message DetourConfig { + string to = 1; +} + +message DefaultConfig { + uint32 level = 2; +} + +message Config { + repeated xray.common.protocol.User user = 1; + DefaultConfig default = 2; +} diff --git a/subproject/Xray-core-main/proxy/vmess/inbound/inbound.go b/subproject/Xray-core-main/proxy/vmess/inbound/inbound.go new file mode 100644 index 00000000..6a8591ad --- /dev/null +++ b/subproject/Xray-core-main/proxy/vmess/inbound/inbound.go @@ -0,0 +1,330 @@ +package inbound + +import ( + "context" + "io" + "strings" + "sync" + "time" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/log" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/common/signal" + "github.com/xtls/xray-core/common/task" + "github.com/xtls/xray-core/common/uuid" + "github.com/xtls/xray-core/core" + feature_inbound "github.com/xtls/xray-core/features/inbound" + "github.com/xtls/xray-core/features/policy" + "github.com/xtls/xray-core/features/routing" + "github.com/xtls/xray-core/proxy/vmess" + "github.com/xtls/xray-core/proxy/vmess/encoding" + "github.com/xtls/xray-core/transport/internet/stat" +) + +type userByEmail struct { + sync.Mutex + cache map[string]*protocol.MemoryUser + defaultLevel uint32 +} + +func newUserByEmail(config *DefaultConfig) *userByEmail { + return &userByEmail{ + cache: make(map[string]*protocol.MemoryUser), + defaultLevel: config.Level, + } +} + +func (v *userByEmail) addNoLock(u *protocol.MemoryUser) bool { + email := strings.ToLower(u.Email) + _, found := v.cache[email] + if found { + return false + } + v.cache[email] = u + return true +} + +func (v *userByEmail) Add(u *protocol.MemoryUser) bool { + v.Lock() + defer v.Unlock() + + return v.addNoLock(u) +} + +func (v *userByEmail) GetOrGenerate(email string) (*protocol.MemoryUser, bool) { + email = strings.ToLower(email) + + v.Lock() + defer v.Unlock() + + user, found := v.cache[email] + if !found { + id := uuid.New() + rawAccount := &vmess.Account{ + Id: id.String(), + } + account, err := rawAccount.AsAccount() + common.Must(err) + user = &protocol.MemoryUser{ + Level: v.defaultLevel, + Email: email, + Account: account, + } + v.cache[email] = user + } + return user, found +} + +func (v *userByEmail) Get(email string) *protocol.MemoryUser { + email = strings.ToLower(email) + v.Lock() + defer v.Unlock() + return v.cache[email] +} + +func (v *userByEmail) Remove(email string) bool { + email = strings.ToLower(email) + + v.Lock() + defer v.Unlock() + + if _, found := v.cache[email]; !found { + return false + } + delete(v.cache, email) + return true +} + +// Handler is an inbound connection handler that handles messages in VMess protocol. +type Handler struct { + policyManager policy.Manager + inboundHandlerManager feature_inbound.Manager + clients *vmess.TimedUserValidator + usersByEmail *userByEmail + sessionHistory *encoding.SessionHistory +} + +// New creates a new VMess inbound handler. +func New(ctx context.Context, config *Config) (*Handler, error) { + v := core.MustFromContext(ctx) + handler := &Handler{ + policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager), + inboundHandlerManager: v.GetFeature(feature_inbound.ManagerType()).(feature_inbound.Manager), + clients: vmess.NewTimedUserValidator(), + usersByEmail: newUserByEmail(config.GetDefaultValue()), + sessionHistory: encoding.NewSessionHistory(), + } + + for _, user := range config.User { + mUser, err := user.ToMemoryUser() + if err != nil { + return nil, errors.New("failed to get VMess user").Base(err) + } + + if err := handler.AddUser(ctx, mUser); err != nil { + return nil, errors.New("failed to initiate user").Base(err) + } + } + + return handler, nil +} + +// Close implements common.Closable. +func (h *Handler) Close() error { + return errors.Combine( + h.sessionHistory.Close(), + common.Close(h.usersByEmail)) +} + +// Network implements proxy.Inbound.Network(). +func (*Handler) Network() []net.Network { + return []net.Network{net.Network_TCP, net.Network_UNIX} +} + +func (h *Handler) GetOrGenerateUser(email string) *protocol.MemoryUser { + user, existing := h.usersByEmail.GetOrGenerate(email) + if !existing { + h.clients.Add(user) + } + return user +} + +func (h *Handler) GetUser(ctx context.Context, email string) *protocol.MemoryUser { + return h.usersByEmail.Get(email) +} + +func (h *Handler) GetUsers(ctx context.Context) []*protocol.MemoryUser { + return h.clients.GetUsers() +} + +func (h *Handler) GetUsersCount(context.Context) int64 { + return h.clients.GetCount() +} + +func (h *Handler) AddUser(ctx context.Context, user *protocol.MemoryUser) error { + if len(user.Email) > 0 && !h.usersByEmail.Add(user) { + return errors.New("User ", user.Email, " already exists.") + } + return h.clients.Add(user) +} + +func (h *Handler) RemoveUser(ctx context.Context, email string) error { + if email == "" { + return errors.New("Email must not be empty.") + } + if !h.usersByEmail.Remove(email) { + return errors.New("User ", email, " not found.") + } + h.clients.Remove(email) + return nil +} + +func transferResponse(timer signal.ActivityUpdater, session *encoding.ServerSession, request *protocol.RequestHeader, response *protocol.ResponseHeader, input buf.Reader, output *buf.BufferedWriter) error { + session.EncodeResponseHeader(response, output) + + bodyWriter, err := session.EncodeResponseBody(request, output) + if err != nil { + return errors.New("failed to start decoding response").Base(err) + } + { + // Optimize for small response packet + data, err := input.ReadMultiBuffer() + if err != nil { + return err + } + + if err := bodyWriter.WriteMultiBuffer(data); err != nil { + return err + } + } + + if err := output.SetBuffered(false); err != nil { + return err + } + + if err := buf.Copy(input, bodyWriter, buf.UpdateActivity(timer)); err != nil { + return err + } + + account := request.User.Account.(*vmess.MemoryAccount) + + if request.Option.Has(protocol.RequestOptionChunkStream) && !account.NoTerminationSignal { + if err := bodyWriter.WriteMultiBuffer(buf.MultiBuffer{}); err != nil { + return err + } + } + + return nil +} + +// Process implements proxy.Inbound.Process(). +func (h *Handler) Process(ctx context.Context, network net.Network, connection stat.Connection, dispatcher routing.Dispatcher) error { + sessionPolicy := h.policyManager.ForLevel(0) + if err := connection.SetReadDeadline(time.Now().Add(sessionPolicy.Timeouts.Handshake)); err != nil { + return errors.New("unable to set read deadline").Base(err).AtWarning() + } + + iConn := stat.TryUnwrapStatsConn(connection) + _, isDrain := iConn.(*net.TCPConn) + if !isDrain { + _, isDrain = iConn.(*net.UnixConn) + } + + reader := &buf.BufferedReader{Reader: buf.NewReader(connection)} + svrSession := encoding.NewServerSession(h.clients, h.sessionHistory) + request, err := svrSession.DecodeRequestHeader(reader, isDrain) + if err != nil { + if errors.Cause(err) != io.EOF { + log.Record(&log.AccessMessage{ + From: connection.RemoteAddr(), + To: "", + Status: log.AccessRejected, + Reason: err, + }) + err = errors.New("invalid request from ", connection.RemoteAddr()).Base(err).AtInfo() + } + return err + } + + if request.Command != protocol.RequestCommandMux { + ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{ + From: connection.RemoteAddr(), + To: request.Destination(), + Status: log.AccessAccepted, + Reason: "", + Email: request.User.Email, + }) + } + + errors.LogInfo(ctx, "received request for ", request.Destination()) + + if err := connection.SetReadDeadline(time.Time{}); err != nil { + errors.LogInfoInner(ctx, err, "unable to set back read deadline") + } + + inbound := session.InboundFromContext(ctx) + inbound.Name = "vmess" + inbound.CanSpliceCopy = 3 + inbound.User = request.User + + sessionPolicy = h.policyManager.ForLevel(request.User.Level) + + ctx, cancel := context.WithCancel(ctx) + timer := signal.CancelAfterInactivity(ctx, cancel, sessionPolicy.Timeouts.ConnectionIdle) + + ctx = policy.ContextWithBufferPolicy(ctx, sessionPolicy.Buffer) + link, err := dispatcher.Dispatch(ctx, request.Destination()) + if err != nil { + return errors.New("failed to dispatch request to ", request.Destination()).Base(err) + } + + requestDone := func() error { + defer timer.SetTimeout(sessionPolicy.Timeouts.DownlinkOnly) + + bodyReader, err := svrSession.DecodeRequestBody(request, reader) + if err != nil { + return errors.New("failed to start decoding").Base(err) + } + if err := buf.Copy(bodyReader, link.Writer, buf.UpdateActivity(timer)); err != nil { + return errors.New("failed to transfer request").Base(err) + } + return nil + } + + responseDone := func() error { + defer timer.SetTimeout(sessionPolicy.Timeouts.UplinkOnly) + + writer := buf.NewBufferedWriter(buf.NewWriter(connection)) + defer writer.Flush() + + response := &protocol.ResponseHeader{ + Command: h.generateCommand(ctx, request), + } + return transferResponse(timer, svrSession, request, response, link.Reader, writer) + } + + requestDonePost := task.OnSuccess(requestDone, task.Close(link.Writer)) + if err := task.Run(ctx, requestDonePost, responseDone); err != nil { + common.Interrupt(link.Reader) + common.Interrupt(link.Writer) + return errors.New("connection ends").Base(err) + } + + return nil +} + +// Stub command generator +func (h *Handler) generateCommand(ctx context.Context, request *protocol.RequestHeader) protocol.ResponseCommand { + return nil +} + +func init() { + common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + return New(ctx, config.(*Config)) + })) +} diff --git a/subproject/Xray-core-main/proxy/vmess/outbound/command.go b/subproject/Xray-core-main/proxy/vmess/outbound/command.go new file mode 100644 index 00000000..c284bfb6 --- /dev/null +++ b/subproject/Xray-core-main/proxy/vmess/outbound/command.go @@ -0,0 +1,14 @@ +package outbound + +import ( + + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" +) + +// As a stub command consumer. +func (h *Handler) handleCommand(dest net.Destination, cmd protocol.ResponseCommand) { + switch cmd.(type) { + default: + } +} diff --git a/subproject/Xray-core-main/proxy/vmess/outbound/config.go b/subproject/Xray-core-main/proxy/vmess/outbound/config.go new file mode 100644 index 00000000..a1e73e06 --- /dev/null +++ b/subproject/Xray-core-main/proxy/vmess/outbound/config.go @@ -0,0 +1 @@ +package outbound diff --git a/subproject/Xray-core-main/proxy/vmess/outbound/config.pb.go b/subproject/Xray-core-main/proxy/vmess/outbound/config.pb.go new file mode 100644 index 00000000..4fcaee43 --- /dev/null +++ b/subproject/Xray-core-main/proxy/vmess/outbound/config.pb.go @@ -0,0 +1,126 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: proxy/vmess/outbound/config.proto + +package outbound + +import ( + protocol "github.com/xtls/xray-core/common/protocol" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + Receiver *protocol.ServerEndpoint `protobuf:"bytes,1,opt,name=Receiver,proto3" json:"Receiver,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_proxy_vmess_outbound_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_proxy_vmess_outbound_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_proxy_vmess_outbound_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Config) GetReceiver() *protocol.ServerEndpoint { + if x != nil { + return x.Receiver + } + return nil +} + +var File_proxy_vmess_outbound_config_proto protoreflect.FileDescriptor + +const file_proxy_vmess_outbound_config_proto_rawDesc = "" + + "\n" + + "!proxy/vmess/outbound/config.proto\x12\x19xray.proxy.vmess.outbound\x1a!common/protocol/server_spec.proto\"J\n" + + "\x06Config\x12@\n" + + "\bReceiver\x18\x01 \x01(\v2$.xray.common.protocol.ServerEndpointR\bReceiverBm\n" + + "\x1dcom.xray.proxy.vmess.outboundP\x01Z.github.com/xtls/xray-core/proxy/vmess/outbound\xaa\x02\x19Xray.Proxy.Vmess.Outboundb\x06proto3" + +var ( + file_proxy_vmess_outbound_config_proto_rawDescOnce sync.Once + file_proxy_vmess_outbound_config_proto_rawDescData []byte +) + +func file_proxy_vmess_outbound_config_proto_rawDescGZIP() []byte { + file_proxy_vmess_outbound_config_proto_rawDescOnce.Do(func() { + file_proxy_vmess_outbound_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proxy_vmess_outbound_config_proto_rawDesc), len(file_proxy_vmess_outbound_config_proto_rawDesc))) + }) + return file_proxy_vmess_outbound_config_proto_rawDescData +} + +var file_proxy_vmess_outbound_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_proxy_vmess_outbound_config_proto_goTypes = []any{ + (*Config)(nil), // 0: xray.proxy.vmess.outbound.Config + (*protocol.ServerEndpoint)(nil), // 1: xray.common.protocol.ServerEndpoint +} +var file_proxy_vmess_outbound_config_proto_depIdxs = []int32{ + 1, // 0: xray.proxy.vmess.outbound.Config.Receiver:type_name -> xray.common.protocol.ServerEndpoint + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_proxy_vmess_outbound_config_proto_init() } +func file_proxy_vmess_outbound_config_proto_init() { + if File_proxy_vmess_outbound_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_proxy_vmess_outbound_config_proto_rawDesc), len(file_proxy_vmess_outbound_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_proxy_vmess_outbound_config_proto_goTypes, + DependencyIndexes: file_proxy_vmess_outbound_config_proto_depIdxs, + MessageInfos: file_proxy_vmess_outbound_config_proto_msgTypes, + }.Build() + File_proxy_vmess_outbound_config_proto = out.File + file_proxy_vmess_outbound_config_proto_goTypes = nil + file_proxy_vmess_outbound_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/proxy/vmess/outbound/config.proto b/subproject/Xray-core-main/proxy/vmess/outbound/config.proto new file mode 100644 index 00000000..fe3041c6 --- /dev/null +++ b/subproject/Xray-core-main/proxy/vmess/outbound/config.proto @@ -0,0 +1,13 @@ +syntax = "proto3"; + +package xray.proxy.vmess.outbound; +option csharp_namespace = "Xray.Proxy.Vmess.Outbound"; +option go_package = "github.com/xtls/xray-core/proxy/vmess/outbound"; +option java_package = "com.xray.proxy.vmess.outbound"; +option java_multiple_files = true; + +import "common/protocol/server_spec.proto"; + +message Config { + xray.common.protocol.ServerEndpoint Receiver = 1; +} diff --git a/subproject/Xray-core-main/proxy/vmess/outbound/outbound.go b/subproject/Xray-core-main/proxy/vmess/outbound/outbound.go new file mode 100644 index 00000000..4850e728 --- /dev/null +++ b/subproject/Xray-core-main/proxy/vmess/outbound/outbound.go @@ -0,0 +1,246 @@ +package outbound + +import ( + "context" + "crypto/hmac" + "crypto/sha256" + "hash/crc64" + "time" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/platform" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/retry" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/common/signal" + "github.com/xtls/xray-core/common/task" + "github.com/xtls/xray-core/common/xudp" + core "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/features/policy" + "github.com/xtls/xray-core/proxy/vmess" + "github.com/xtls/xray-core/proxy/vmess/encoding" + "github.com/xtls/xray-core/transport" + "github.com/xtls/xray-core/transport/internet" + "github.com/xtls/xray-core/transport/internet/stat" +) + +// Handler is an outbound connection handler for VMess protocol. +type Handler struct { + server *protocol.ServerSpec + policyManager policy.Manager + cone bool +} + +// New creates a new VMess outbound handler. +func New(ctx context.Context, config *Config) (*Handler, error) { + if config.Receiver == nil { + return nil, errors.New(`no vnext found`) + } + server, err := protocol.NewServerSpecFromPB(config.Receiver) + if err != nil { + return nil, errors.New("failed to get server spec").Base(err) + } + + v := core.MustFromContext(ctx) + handler := &Handler{ + server: server, + policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager), + cone: ctx.Value("cone").(bool), + } + + return handler, nil +} + +// Process implements proxy.Outbound.Process(). +func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer internet.Dialer) error { + outbounds := session.OutboundsFromContext(ctx) + ob := outbounds[len(outbounds)-1] + if !ob.Target.IsValid() { + return errors.New("target not specified").AtError() + } + ob.Name = "vmess" + ob.CanSpliceCopy = 3 + + rec := h.server + var conn stat.Connection + + err := retry.ExponentialBackoff(5, 200).On(func() error { + rawConn, err := dialer.Dial(ctx, rec.Destination) + if err != nil { + return err + } + conn = rawConn + + return nil + }) + if err != nil { + return errors.New("failed to find an available destination").Base(err).AtWarning() + } + defer conn.Close() + + target := ob.Target + errors.LogInfo(ctx, "tunneling request to ", target, " via ", rec.Destination.NetAddr()) + + command := protocol.RequestCommandTCP + if target.Network == net.Network_UDP { + command = protocol.RequestCommandUDP + } + if target.Address.Family().IsDomain() && target.Address.Domain() == "v1.mux.cool" { + command = protocol.RequestCommandMux + } + + user := rec.User + request := &protocol.RequestHeader{ + Version: encoding.Version, + User: user, + Command: command, + Address: target.Address, + Port: target.Port, + Option: protocol.RequestOptionChunkStream, + } + + account := request.User.Account.(*vmess.MemoryAccount) + request.Security = account.Security + + if request.Security == protocol.SecurityType_AES128_GCM || request.Security == protocol.SecurityType_NONE || request.Security == protocol.SecurityType_CHACHA20_POLY1305 { + request.Option.Set(protocol.RequestOptionChunkMasking) + } + + if shouldEnablePadding(request.Security) && request.Option.Has(protocol.RequestOptionChunkMasking) { + request.Option.Set(protocol.RequestOptionGlobalPadding) + } + + if request.Security == protocol.SecurityType_ZERO { + request.Security = protocol.SecurityType_NONE + request.Option.Clear(protocol.RequestOptionChunkStream) + request.Option.Clear(protocol.RequestOptionChunkMasking) + } + + if account.AuthenticatedLengthExperiment { + request.Option.Set(protocol.RequestOptionAuthenticatedLength) + } + + input := link.Reader + output := link.Writer + + hashkdf := hmac.New(sha256.New, []byte("VMessBF")) + hashkdf.Write(account.ID.Bytes()) + + behaviorSeed := crc64.Checksum(hashkdf.Sum(nil), crc64.MakeTable(crc64.ISO)) + + var newCtx context.Context + var newCancel context.CancelFunc + if session.TimeoutOnlyFromContext(ctx) { + newCtx, newCancel = context.WithCancel(context.Background()) + } + + session := encoding.NewClientSession(ctx, int64(behaviorSeed)) + sessionPolicy := h.policyManager.ForLevel(request.User.Level) + + ctx, cancel := context.WithCancel(ctx) + timer := signal.CancelAfterInactivity(ctx, func() { + cancel() + if newCancel != nil { + newCancel() + } + }, sessionPolicy.Timeouts.ConnectionIdle) + + if request.Command == protocol.RequestCommandUDP && h.cone && request.Port != 53 && request.Port != 443 { + request.Command = protocol.RequestCommandMux + request.Address = net.DomainAddress("v1.mux.cool") + request.Port = net.Port(666) + } + + requestDone := func() error { + defer timer.SetTimeout(sessionPolicy.Timeouts.DownlinkOnly) + + writer := buf.NewBufferedWriter(buf.NewWriter(conn)) + if err := session.EncodeRequestHeader(request, writer); err != nil { + return errors.New("failed to encode request").Base(err).AtWarning() + } + + bodyWriter, err := session.EncodeRequestBody(request, writer) + if err != nil { + return errors.New("failed to start encoding").Base(err) + } + bodyWriter2 := bodyWriter + if request.Command == protocol.RequestCommandMux && request.Port == 666 { + bodyWriter = xudp.NewPacketWriter(bodyWriter, target, xudp.GetGlobalID(ctx)) + } + if err := buf.CopyOnceTimeout(input, bodyWriter, time.Millisecond*100); err != nil && err != buf.ErrNotTimeoutReader && err != buf.ErrReadTimeout { + return errors.New("failed to write first payload").Base(err) + } + + if err := writer.SetBuffered(false); err != nil { + return err + } + + if err := buf.Copy(input, bodyWriter, buf.UpdateActivity(timer)); err != nil { + return err + } + + if request.Option.Has(protocol.RequestOptionChunkStream) && !account.NoTerminationSignal { + if err := bodyWriter2.WriteMultiBuffer(buf.MultiBuffer{}); err != nil { + return err + } + } + + return nil + } + + responseDone := func() error { + defer timer.SetTimeout(sessionPolicy.Timeouts.UplinkOnly) + + reader := &buf.BufferedReader{Reader: buf.NewReader(conn)} + header, err := session.DecodeResponseHeader(reader) + if err != nil { + return errors.New("failed to read header").Base(err) + } + h.handleCommand(rec.Destination, header.Command) + + bodyReader, err := session.DecodeResponseBody(request, reader) + if err != nil { + return errors.New("failed to start encoding response").Base(err) + } + if request.Command == protocol.RequestCommandMux && request.Port == 666 { + bodyReader = xudp.NewPacketReader(&buf.BufferedReader{Reader: bodyReader}) + } + + return buf.Copy(bodyReader, output, buf.UpdateActivity(timer)) + } + + if newCtx != nil { + ctx = newCtx + } + + responseDonePost := task.OnSuccess(responseDone, task.Close(output)) + if err := task.Run(ctx, requestDone, responseDonePost); err != nil { + return errors.New("connection ends").Base(err) + } + + return nil +} + +var ( + enablePadding = false +) + +func shouldEnablePadding(s protocol.SecurityType) bool { + return enablePadding || s == protocol.SecurityType_AES128_GCM || s == protocol.SecurityType_CHACHA20_POLY1305 || s == protocol.SecurityType_AUTO +} + +func init() { + common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + return New(ctx, config.(*Config)) + })) + + const defaultFlagValue = "NOT_DEFINED_AT_ALL" + + paddingValue := platform.NewEnvFlag(platform.UseVmessPadding).GetValue(func() string { return defaultFlagValue }) + if paddingValue != defaultFlagValue { + enablePadding = true + } +} diff --git a/subproject/Xray-core-main/proxy/vmess/validator.go b/subproject/Xray-core-main/proxy/vmess/validator.go new file mode 100644 index 00000000..bfffadcf --- /dev/null +++ b/subproject/Xray-core-main/proxy/vmess/validator.go @@ -0,0 +1,127 @@ +package vmess + +import ( + "crypto/hmac" + "crypto/sha256" + "hash/crc64" + "strings" + "sync" + + "github.com/xtls/xray-core/common/dice" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/proxy/vmess/aead" +) + +// TimedUserValidator is a user Validator based on time. +type TimedUserValidator struct { + sync.RWMutex + users []*protocol.MemoryUser + + behaviorSeed uint64 + behaviorFused bool + + aeadDecoderHolder *aead.AuthIDDecoderHolder +} + +// NewTimedUserValidator creates a new TimedUserValidator. +func NewTimedUserValidator() *TimedUserValidator { + tuv := &TimedUserValidator{ + users: make([]*protocol.MemoryUser, 0, 16), + aeadDecoderHolder: aead.NewAuthIDDecoderHolder(), + } + return tuv +} + +func (v *TimedUserValidator) Add(u *protocol.MemoryUser) error { + v.Lock() + defer v.Unlock() + + v.users = append(v.users, u) + + account, ok := u.Account.(*MemoryAccount) + if !ok { + return errors.New("account type is incorrect") + } + if !v.behaviorFused { + hashkdf := hmac.New(sha256.New, []byte("VMESSBSKDF")) + hashkdf.Write(account.ID.Bytes()) + v.behaviorSeed = crc64.Update(v.behaviorSeed, crc64.MakeTable(crc64.ECMA), hashkdf.Sum(nil)) + } + + var cmdkeyfl [16]byte + copy(cmdkeyfl[:], account.ID.CmdKey()) + v.aeadDecoderHolder.AddUser(cmdkeyfl, u) + + return nil +} + +func (v *TimedUserValidator) GetUsers() []*protocol.MemoryUser { + v.Lock() + defer v.Unlock() + dst := make([]*protocol.MemoryUser, len(v.users)) + copy(dst, v.users) + return dst +} + +func (v *TimedUserValidator) GetCount() int64 { + v.Lock() + defer v.Unlock() + return int64(len(v.users)) +} + +func (v *TimedUserValidator) GetAEAD(userHash []byte) (*protocol.MemoryUser, bool, error) { + v.RLock() + defer v.RUnlock() + + var userHashFL [16]byte + copy(userHashFL[:], userHash) + + userd, err := v.aeadDecoderHolder.Match(userHashFL) + if err != nil { + return nil, false, err + } + return userd.(*protocol.MemoryUser), true, nil +} + +func (v *TimedUserValidator) Remove(email string) bool { + v.Lock() + defer v.Unlock() + + email = strings.ToLower(email) + idx := -1 + for i, u := range v.users { + if strings.EqualFold(u.Email, email) { + idx = i + var cmdkeyfl [16]byte + copy(cmdkeyfl[:], u.Account.(*MemoryAccount).ID.CmdKey()) + v.aeadDecoderHolder.RemoveUser(cmdkeyfl) + break + } + } + if idx == -1 { + return false + } + ulen := len(v.users) + + v.users[idx] = v.users[ulen-1] + v.users[ulen-1] = nil + v.users = v.users[:ulen-1] + + return true +} + +func (v *TimedUserValidator) GetBehaviorSeed() uint64 { + v.Lock() + defer v.Unlock() + + v.behaviorFused = true + if v.behaviorSeed == 0 { + v.behaviorSeed = dice.RollUint64() + } + return v.behaviorSeed +} + +var ErrNotFound = errors.New("Not Found") + +var ErrTainted = errors.New("ErrTainted") diff --git a/subproject/Xray-core-main/proxy/vmess/validator_test.go b/subproject/Xray-core-main/proxy/vmess/validator_test.go new file mode 100644 index 00000000..83313cbc --- /dev/null +++ b/subproject/Xray-core-main/proxy/vmess/validator_test.go @@ -0,0 +1,34 @@ +package vmess_test + +import ( + "testing" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/uuid" + . "github.com/xtls/xray-core/proxy/vmess" +) + +func toAccount(a *Account) protocol.Account { + account, err := a.AsAccount() + common.Must(err) + return account +} + +func BenchmarkUserValidator(b *testing.B) { + for i := 0; i < b.N; i++ { + v := NewTimedUserValidator() + + for j := 0; j < 1500; j++ { + id := uuid.New() + v.Add(&protocol.MemoryUser{ + Email: "test", + Account: toAccount(&Account{ + Id: id.String(), + }), + }) + } + + common.Close(v) + } +} diff --git a/subproject/Xray-core-main/proxy/vmess/vmess.go b/subproject/Xray-core-main/proxy/vmess/vmess.go new file mode 100644 index 00000000..dd86f516 --- /dev/null +++ b/subproject/Xray-core-main/proxy/vmess/vmess.go @@ -0,0 +1,6 @@ +// Package vmess contains the implementation of VMess protocol and transportation. +// +// VMess contains both inbound and outbound connections. VMess inbound is usually used on servers +// together with 'freedom' to talk to final destination, while VMess outbound is usually used on +// clients with 'socks' for proxying. +package vmess diff --git a/subproject/Xray-core-main/proxy/wireguard/bind.go b/subproject/Xray-core-main/proxy/wireguard/bind.go new file mode 100644 index 00000000..ddbc2178 --- /dev/null +++ b/subproject/Xray-core-main/proxy/wireguard/bind.go @@ -0,0 +1,267 @@ +package wireguard + +import ( + "context" + gonet "net" + "net/netip" + "runtime" + "strconv" + + "golang.zx2c4.com/wireguard/conn" + "golang.zx2c4.com/wireguard/device" + + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/features/dns" + "github.com/xtls/xray-core/transport/internet" +) + +type netReadInfo struct { + buff *buf.Buffer + endpoint conn.Endpoint +} + +// reduce duplicated code +type netBind struct { + dns dns.Client + dnsOption dns.IPOption + + workers int + readQueue chan *netReadInfo + closedCh chan struct{} +} + +// SetMark implements conn.Bind +func (bind *netBind) SetMark(mark uint32) error { + return nil +} + +// ParseEndpoint implements conn.Bind +func (n *netBind) ParseEndpoint(s string) (conn.Endpoint, error) { + ipStr, port, err := net.SplitHostPort(s) + if err != nil { + return nil, err + } + portNum, err := strconv.Atoi(port) + if err != nil { + return nil, err + } + + addr := net.ParseAddress(ipStr) + if addr.Family() == net.AddressFamilyDomain { + ips, _, err := n.dns.LookupIP(addr.Domain(), n.dnsOption) + if err != nil { + return nil, err + } else if len(ips) == 0 { + return nil, dns.ErrEmptyResponse + } + addr = net.IPAddress(ips[0]) + } + + dst := net.Destination{ + Address: addr, + Port: net.Port(portNum), + Network: net.Network_UDP, + } + + return &netEndpoint{ + dst: dst, + }, nil +} + +// BatchSize implements conn.Bind +func (bind *netBind) BatchSize() int { + return 1 +} + +// Open implements conn.Bind +func (bind *netBind) Open(uport uint16) ([]conn.ReceiveFunc, uint16, error) { + bind.closedCh = make(chan struct{}) + errors.LogDebug(context.Background(), "bind opened") + + fun := func(bufs [][]byte, sizes []int, eps []conn.Endpoint) (n int, err error) { + select { + case r := <-bind.readQueue: + sizes[0], eps[0] = copy(bufs[0], r.buff.Bytes()), r.endpoint + r.buff.Release() + return 1, nil + case <-bind.closedCh: + errors.LogDebug(context.Background(), "recv func closed") + return 0, gonet.ErrClosed + } + } + workers := bind.workers + if workers <= 0 { + workers = runtime.NumCPU() + } + if workers <= 0 { + workers = 1 + } + arr := make([]conn.ReceiveFunc, workers) + for i := 0; i < workers; i++ { + arr[i] = fun + } + + return arr, uint16(uport), nil +} + +// Close implements conn.Bind +func (bind *netBind) Close() error { + errors.LogDebug(context.Background(), "bind closed") + if bind.closedCh != nil { + close(bind.closedCh) + } + return nil +} + +type netBindClient struct { + netBind + + ctx context.Context + dialer internet.Dialer + reserved []byte +} + +func (bind *netBindClient) connectTo(endpoint *netEndpoint) error { + c, err := bind.dialer.Dial(bind.ctx, endpoint.dst) + if err != nil { + return err + } + endpoint.conn = c + + go func() { + for { + buff := buf.NewWithSize(device.MaxMessageSize) + n, err := buff.ReadFrom(c) + + if err != nil { + buff.Release() + endpoint.conn = nil + c.Close() + return + } + + rawBytes := buff.Bytes() + if n > 3 { + rawBytes[1] = 0 + rawBytes[2] = 0 + rawBytes[3] = 0 + } + + select { + case bind.readQueue <- &netReadInfo{ + buff: buff, + endpoint: endpoint, + }: + case <-bind.closedCh: + buff.Release() + endpoint.conn = nil + c.Close() + return + } + } + }() + + return nil +} + +func (bind *netBindClient) Send(buff [][]byte, endpoint conn.Endpoint) error { + var err error + + nend, ok := endpoint.(*netEndpoint) + if !ok { + return conn.ErrWrongEndpointType + } + + if nend.conn == nil { + err = bind.connectTo(nend) + if err != nil { + return err + } + } + + for _, buff := range buff { + if len(buff) > 3 && len(bind.reserved) == 3 { + copy(buff[1:], bind.reserved) + } + if _, err = nend.conn.Write(buff); err != nil { + return err + } + } + return nil +} + +type netBindServer struct { + netBind +} + +func (bind *netBindServer) Send(buff [][]byte, endpoint conn.Endpoint) error { + var err error + + nend, ok := endpoint.(*netEndpoint) + if !ok { + return conn.ErrWrongEndpointType + } + + if nend.conn == nil { + errors.LogDebug(context.Background(), nend.dst.NetAddr(), " send on closed peer") + return errors.New("peer closed") + } + + for _, buff := range buff { + if _, err = nend.conn.Write(buff); err != nil { + return err + } + } + + return err +} + +type netEndpoint struct { + dst net.Destination + conn net.Conn +} + +func (netEndpoint) ClearSrc() {} + +func (e netEndpoint) DstIP() netip.Addr { + return netip.Addr{} +} + +func (e netEndpoint) SrcIP() netip.Addr { + return netip.Addr{} +} + +func (e netEndpoint) DstToBytes() []byte { + var dat []byte + if e.dst.Address.Family().IsIPv4() { + dat = e.dst.Address.IP().To4()[:] + } else { + dat = e.dst.Address.IP().To16()[:] + } + dat = append(dat, byte(e.dst.Port), byte(e.dst.Port>>8)) + return dat +} + +func (e netEndpoint) DstToString() string { + return e.dst.NetAddr() +} + +func (e netEndpoint) SrcToString() string { + return "" +} + +func toNetIpAddr(addr net.Address) netip.Addr { + if addr.Family().IsIPv4() { + ip := addr.IP() + return netip.AddrFrom4([4]byte{ip[0], ip[1], ip[2], ip[3]}) + } else { + ip := addr.IP() + arr := [16]byte{} + for i := 0; i < 16; i++ { + arr[i] = ip[i] + } + return netip.AddrFrom16(arr) + } +} diff --git a/subproject/Xray-core-main/proxy/wireguard/client.go b/subproject/Xray-core-main/proxy/wireguard/client.go new file mode 100644 index 00000000..3ee3a2c5 --- /dev/null +++ b/subproject/Xray-core-main/proxy/wireguard/client.go @@ -0,0 +1,387 @@ +/* + +Some of codes are copied from https://github.com/octeep/wireproxy, license below. + +Copyright (c) 2022 Wind T.F. Wong + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +*/ + +package wireguard + +import ( + "context" + "fmt" + "net/netip" + "strings" + "sync" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/dice" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/log" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/common/signal" + "github.com/xtls/xray-core/common/task" + "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/features/dns" + "github.com/xtls/xray-core/features/policy" + "github.com/xtls/xray-core/transport" + "github.com/xtls/xray-core/transport/internet" +) + +// Handler is an outbound connection that silently swallow the entire payload. +type Handler struct { + conf *DeviceConfig + net Tunnel + bind *netBindClient + policyManager policy.Manager + dns dns.Client + // cached configuration + endpoints []netip.Addr + hasIPv4, hasIPv6 bool + wgLock sync.Mutex +} + +// New creates a new wireguard handler. +func New(ctx context.Context, conf *DeviceConfig) (*Handler, error) { + v := core.MustFromContext(ctx) + + endpoints, hasIPv4, hasIPv6, err := parseEndpoints(conf) + if err != nil { + return nil, err + } + + d := v.GetFeature(dns.ClientType()).(dns.Client) + return &Handler{ + conf: conf, + policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager), + dns: d, + endpoints: endpoints, + hasIPv4: hasIPv4, + hasIPv6: hasIPv6, + }, nil +} + +func (h *Handler) Close() (err error) { + go func() { + h.wgLock.Lock() + defer h.wgLock.Unlock() + + if h.net != nil { + _ = h.net.Close() + h.net = nil + } + }() + + return nil +} + +func (h *Handler) processWireGuard(ctx context.Context, dialer internet.Dialer) (err error) { + h.wgLock.Lock() + defer h.wgLock.Unlock() + + if h.bind != nil && h.bind.dialer == dialer && h.net != nil { + return nil + } + + log.Record(&log.GeneralMessage{ + Severity: log.Severity_Info, + Content: "switching dialer", + }) + + if h.net != nil { + _ = h.net.Close() + h.net = nil + } + if h.bind != nil { + _ = h.bind.Close() + h.bind = nil + } + + // bind := conn.NewStdNetBind() // TODO: conn.Bind wrapper for dialer + h.bind = &netBindClient{ + netBind: netBind{ + dns: h.dns, + dnsOption: dns.IPOption{ + IPv4Enable: h.hasIPv4, + IPv6Enable: h.hasIPv6, + }, + workers: int(h.conf.NumWorkers), + readQueue: make(chan *netReadInfo), + }, + ctx: ctx, + dialer: dialer, + reserved: h.conf.Reserved, + } + defer func() { + if err != nil { + h.bind.Close() + h.bind = nil + } + }() + + h.net, err = h.makeVirtualTun() + if err != nil { + return errors.New("failed to create virtual tun interface").Base(err) + } + return nil +} + +// Process implements OutboundHandler.Dispatch(). +func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer internet.Dialer) error { + outbounds := session.OutboundsFromContext(ctx) + ob := outbounds[len(outbounds)-1] + if !ob.Target.IsValid() { + return errors.New("target not specified") + } + ob.Name = "wireguard" + ob.CanSpliceCopy = 3 + + if err := h.processWireGuard(ctx, dialer); err != nil { + return err + } + + // Destination of the inner request. + destination := ob.Target + command := protocol.RequestCommandTCP + if destination.Network == net.Network_UDP { + command = protocol.RequestCommandUDP + } + + // resolve dns + addr := destination.Address + if addr.Family().IsDomain() { + ips, _, err := h.dns.LookupIP(addr.Domain(), dns.IPOption{ + IPv4Enable: h.hasIPv4 && h.conf.preferIP4(), + IPv6Enable: h.hasIPv6 && h.conf.preferIP6(), + }) + { // Resolve fallback + if (len(ips) == 0 || err != nil) && h.conf.hasFallback() { + ips, _, err = h.dns.LookupIP(addr.Domain(), dns.IPOption{ + IPv4Enable: h.hasIPv4 && h.conf.fallbackIP4(), + IPv6Enable: h.hasIPv6 && h.conf.fallbackIP6(), + }) + } + } + if err != nil { + return errors.New("failed to lookup DNS").Base(err) + } else if len(ips) == 0 { + return dns.ErrEmptyResponse + } + addr = net.IPAddress(ips[dice.Roll(len(ips))]) + } + + var newCtx context.Context + var newCancel context.CancelFunc + if session.TimeoutOnlyFromContext(ctx) { + newCtx, newCancel = context.WithCancel(context.Background()) + } + + p := h.policyManager.ForLevel(0) + + ctx, cancel := context.WithCancel(ctx) + timer := signal.CancelAfterInactivity(ctx, func() { + cancel() + if newCancel != nil { + newCancel() + } + }, p.Timeouts.ConnectionIdle) + addrPort := netip.AddrPortFrom(toNetIpAddr(addr), destination.Port.Value()) + + var requestFunc func() error + var responseFunc func() error + + if command == protocol.RequestCommandTCP { + conn, err := h.net.DialContextTCPAddrPort(ctx, addrPort) + if err != nil { + return errors.New("failed to create TCP connection").Base(err) + } + defer conn.Close() + + requestFunc = func() error { + defer timer.SetTimeout(p.Timeouts.DownlinkOnly) + return buf.Copy(link.Reader, buf.NewWriter(conn), buf.UpdateActivity(timer)) + } + responseFunc = func() error { + defer timer.SetTimeout(p.Timeouts.UplinkOnly) + return buf.Copy(buf.NewReader(conn), link.Writer, buf.UpdateActivity(timer)) + } + } else if command == protocol.RequestCommandUDP { + conn, err := h.net.DialUDPAddrPort(netip.AddrPort{}, addrPort) + if err != nil { + return errors.New("failed to create UDP connection").Base(err) + } + defer conn.Close() + + conn = &udpConnClient{ + Conn: conn, + dest: destination, + } + + requestFunc = func() error { + defer timer.SetTimeout(p.Timeouts.DownlinkOnly) + return buf.Copy(link.Reader, buf.NewWriter(conn), buf.UpdateActivity(timer)) + } + responseFunc = func() error { + defer timer.SetTimeout(p.Timeouts.UplinkOnly) + return buf.Copy(buf.NewReader(conn), link.Writer, buf.UpdateActivity(timer)) + } + } + + if newCtx != nil { + ctx = newCtx + } + + responseDonePost := task.OnSuccess(responseFunc, task.Close(link.Writer)) + if err := task.Run(ctx, requestFunc, responseDonePost); err != nil { + common.Interrupt(link.Reader) + common.Interrupt(link.Writer) + return errors.New("connection ends").Base(err) + } + + return nil +} + +// creates a tun interface on netstack given a configuration +func (h *Handler) makeVirtualTun() (Tunnel, error) { + t, err := h.conf.createTun()(h.endpoints, int(h.conf.Mtu), nil) + if err != nil { + return nil, err + } + + h.bind.dnsOption.IPv4Enable = h.hasIPv4 + h.bind.dnsOption.IPv6Enable = h.hasIPv6 + + if err = t.BuildDevice(h.createIPCRequest(), h.bind); err != nil { + _ = t.Close() + return nil, err + } + return t, nil +} + +// serialize the config into an IPC request +func (h *Handler) createIPCRequest() string { + var request strings.Builder + + request.WriteString(fmt.Sprintf("private_key=%s\n", h.conf.SecretKey)) + + if !h.conf.IsClient { + // placeholder, we'll handle actual port listening on Xray + request.WriteString("listen_port=1337\n") + } + + for _, peer := range h.conf.Peers { + if peer.PublicKey != "" { + request.WriteString(fmt.Sprintf("public_key=%s\n", peer.PublicKey)) + } + + if peer.PreSharedKey != "" { + request.WriteString(fmt.Sprintf("preshared_key=%s\n", peer.PreSharedKey)) + } + + address, port, err := net.SplitHostPort(peer.Endpoint) + if err != nil { + errors.LogError(h.bind.ctx, "failed to split endpoint ", peer.Endpoint, " into address and port") + } + addr := net.ParseAddress(address) + if addr.Family().IsDomain() { + dialerIp := h.bind.dialer.DestIpAddress() + if dialerIp != nil { + addr = net.ParseAddress(dialerIp.String()) + errors.LogInfo(h.bind.ctx, "createIPCRequest use dialer dest ip: ", addr) + } else { + ips, _, err := h.dns.LookupIP(addr.Domain(), dns.IPOption{ + IPv4Enable: h.conf.preferIP4(), + IPv6Enable: h.conf.preferIP6(), + }) + { // Resolve fallback + if (len(ips) == 0 || err != nil) && h.conf.hasFallback() { + ips, _, err = h.dns.LookupIP(addr.Domain(), dns.IPOption{ + IPv4Enable: h.conf.fallbackIP4(), + IPv6Enable: h.conf.fallbackIP6(), + }) + } + } + if err != nil { + errors.LogInfoInner(h.bind.ctx, err, "createIPCRequest failed to lookup DNS") + } else if len(ips) == 0 { + errors.LogInfo(h.bind.ctx, "createIPCRequest empty lookup DNS") + } else { + addr = net.IPAddress(ips[dice.Roll(len(ips))]) + } + } + } + + if peer.Endpoint != "" { + request.WriteString(fmt.Sprintf("endpoint=%s:%s\n", addr, port)) + } + + for _, ip := range peer.AllowedIps { + request.WriteString(fmt.Sprintf("allowed_ip=%s\n", ip)) + } + + if peer.KeepAlive != 0 { + request.WriteString(fmt.Sprintf("persistent_keepalive_interval=%d\n", peer.KeepAlive)) + } + } + + return request.String()[:request.Len()] +} + +type udpConnClient struct { + net.Conn + dest net.Destination +} + +func (c *udpConnClient) ReadMultiBuffer() (buf.MultiBuffer, error) { + b := buf.New() + b.Resize(0, buf.Size) + n, addr, err := c.Conn.(net.PacketConn).ReadFrom(b.Bytes()) + if err != nil { + b.Release() + return nil, err + } + if addr == nil { // should never hit + addr = c.dest.RawNetAddr() + } + b.Resize(0, int32(n)) + + b.UDP = &net.Destination{ + Address: net.IPAddress(addr.(*net.UDPAddr).IP), + Port: net.Port(addr.(*net.UDPAddr).Port), + Network: net.Network_UDP, + } + + return buf.MultiBuffer{b}, nil +} + +func (c *udpConnClient) WriteMultiBuffer(mb buf.MultiBuffer) error { + for i, b := range mb { + dst := c.dest + if b.UDP != nil { + dst = *b.UDP + } + _, err := c.Conn.(net.PacketConn).WriteTo(b.Bytes(), dst.RawNetAddr()) + if err != nil { + buf.ReleaseMulti(mb[i:]) + return err + } + b.Release() + } + return nil +} diff --git a/subproject/Xray-core-main/proxy/wireguard/config.go b/subproject/Xray-core-main/proxy/wireguard/config.go new file mode 100644 index 00000000..cbaa670b --- /dev/null +++ b/subproject/Xray-core-main/proxy/wireguard/config.go @@ -0,0 +1,54 @@ +package wireguard + +import ( + "context" + + "github.com/xtls/xray-core/common/errors" +) + +func (c *DeviceConfig) preferIP4() bool { + return c.DomainStrategy == DeviceConfig_FORCE_IP || + c.DomainStrategy == DeviceConfig_FORCE_IP4 || + c.DomainStrategy == DeviceConfig_FORCE_IP46 +} + +func (c *DeviceConfig) preferIP6() bool { + return c.DomainStrategy == DeviceConfig_FORCE_IP || + c.DomainStrategy == DeviceConfig_FORCE_IP6 || + c.DomainStrategy == DeviceConfig_FORCE_IP64 +} + +func (c *DeviceConfig) hasFallback() bool { + return c.DomainStrategy == DeviceConfig_FORCE_IP46 || c.DomainStrategy == DeviceConfig_FORCE_IP64 +} + +func (c *DeviceConfig) fallbackIP4() bool { + return c.DomainStrategy == DeviceConfig_FORCE_IP64 +} + +func (c *DeviceConfig) fallbackIP6() bool { + return c.DomainStrategy == DeviceConfig_FORCE_IP46 +} + +func (c *DeviceConfig) createTun() tunCreator { + if !c.IsClient { + // See tun_linux.go createKernelTun() + errors.LogWarning(context.Background(), "Using gVisor TUN. WG inbound doesn't support kernel TUN yet.") + return createGVisorTun + } + if c.NoKernelTun { + errors.LogWarning(context.Background(), "Using gVisor TUN. NoKernelTun is set to true.") + return createGVisorTun + } + kernelTunSupported, err := KernelTunSupported() + if err != nil { + errors.LogWarning(context.Background(), "Using gVisor TUN. Failed to check kernel TUN support:", err) + return createGVisorTun + } + if !kernelTunSupported { + errors.LogWarning(context.Background(), "Using gVisor TUN. Kernel TUN is not supported on your OS, or your permission is insufficient.") + return createGVisorTun + } + errors.LogWarning(context.Background(), "Using kernel TUN.") + return createKernelTun +} diff --git a/subproject/Xray-core-main/proxy/wireguard/config.pb.go b/subproject/Xray-core-main/proxy/wireguard/config.pb.go new file mode 100644 index 00000000..16134c8d --- /dev/null +++ b/subproject/Xray-core-main/proxy/wireguard/config.pb.go @@ -0,0 +1,352 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: proxy/wireguard/config.proto + +package wireguard + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type DeviceConfig_DomainStrategy int32 + +const ( + DeviceConfig_FORCE_IP DeviceConfig_DomainStrategy = 0 + DeviceConfig_FORCE_IP4 DeviceConfig_DomainStrategy = 1 + DeviceConfig_FORCE_IP6 DeviceConfig_DomainStrategy = 2 + DeviceConfig_FORCE_IP46 DeviceConfig_DomainStrategy = 3 + DeviceConfig_FORCE_IP64 DeviceConfig_DomainStrategy = 4 +) + +// Enum value maps for DeviceConfig_DomainStrategy. +var ( + DeviceConfig_DomainStrategy_name = map[int32]string{ + 0: "FORCE_IP", + 1: "FORCE_IP4", + 2: "FORCE_IP6", + 3: "FORCE_IP46", + 4: "FORCE_IP64", + } + DeviceConfig_DomainStrategy_value = map[string]int32{ + "FORCE_IP": 0, + "FORCE_IP4": 1, + "FORCE_IP6": 2, + "FORCE_IP46": 3, + "FORCE_IP64": 4, + } +) + +func (x DeviceConfig_DomainStrategy) Enum() *DeviceConfig_DomainStrategy { + p := new(DeviceConfig_DomainStrategy) + *p = x + return p +} + +func (x DeviceConfig_DomainStrategy) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (DeviceConfig_DomainStrategy) Descriptor() protoreflect.EnumDescriptor { + return file_proxy_wireguard_config_proto_enumTypes[0].Descriptor() +} + +func (DeviceConfig_DomainStrategy) Type() protoreflect.EnumType { + return &file_proxy_wireguard_config_proto_enumTypes[0] +} + +func (x DeviceConfig_DomainStrategy) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use DeviceConfig_DomainStrategy.Descriptor instead. +func (DeviceConfig_DomainStrategy) EnumDescriptor() ([]byte, []int) { + return file_proxy_wireguard_config_proto_rawDescGZIP(), []int{1, 0} +} + +type PeerConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + PublicKey string `protobuf:"bytes,1,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty"` + PreSharedKey string `protobuf:"bytes,2,opt,name=pre_shared_key,json=preSharedKey,proto3" json:"pre_shared_key,omitempty"` + Endpoint string `protobuf:"bytes,3,opt,name=endpoint,proto3" json:"endpoint,omitempty"` + KeepAlive uint32 `protobuf:"varint,4,opt,name=keep_alive,json=keepAlive,proto3" json:"keep_alive,omitempty"` + AllowedIps []string `protobuf:"bytes,5,rep,name=allowed_ips,json=allowedIps,proto3" json:"allowed_ips,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PeerConfig) Reset() { + *x = PeerConfig{} + mi := &file_proxy_wireguard_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PeerConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PeerConfig) ProtoMessage() {} + +func (x *PeerConfig) ProtoReflect() protoreflect.Message { + mi := &file_proxy_wireguard_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PeerConfig.ProtoReflect.Descriptor instead. +func (*PeerConfig) Descriptor() ([]byte, []int) { + return file_proxy_wireguard_config_proto_rawDescGZIP(), []int{0} +} + +func (x *PeerConfig) GetPublicKey() string { + if x != nil { + return x.PublicKey + } + return "" +} + +func (x *PeerConfig) GetPreSharedKey() string { + if x != nil { + return x.PreSharedKey + } + return "" +} + +func (x *PeerConfig) GetEndpoint() string { + if x != nil { + return x.Endpoint + } + return "" +} + +func (x *PeerConfig) GetKeepAlive() uint32 { + if x != nil { + return x.KeepAlive + } + return 0 +} + +func (x *PeerConfig) GetAllowedIps() []string { + if x != nil { + return x.AllowedIps + } + return nil +} + +type DeviceConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + SecretKey string `protobuf:"bytes,1,opt,name=secret_key,json=secretKey,proto3" json:"secret_key,omitempty"` + Endpoint []string `protobuf:"bytes,2,rep,name=endpoint,proto3" json:"endpoint,omitempty"` + Peers []*PeerConfig `protobuf:"bytes,3,rep,name=peers,proto3" json:"peers,omitempty"` + Mtu int32 `protobuf:"varint,4,opt,name=mtu,proto3" json:"mtu,omitempty"` + NumWorkers int32 `protobuf:"varint,5,opt,name=num_workers,json=numWorkers,proto3" json:"num_workers,omitempty"` + Reserved []byte `protobuf:"bytes,6,opt,name=reserved,proto3" json:"reserved,omitempty"` + DomainStrategy DeviceConfig_DomainStrategy `protobuf:"varint,7,opt,name=domain_strategy,json=domainStrategy,proto3,enum=xray.proxy.wireguard.DeviceConfig_DomainStrategy" json:"domain_strategy,omitempty"` + IsClient bool `protobuf:"varint,8,opt,name=is_client,json=isClient,proto3" json:"is_client,omitempty"` + NoKernelTun bool `protobuf:"varint,9,opt,name=no_kernel_tun,json=noKernelTun,proto3" json:"no_kernel_tun,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeviceConfig) Reset() { + *x = DeviceConfig{} + mi := &file_proxy_wireguard_config_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeviceConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeviceConfig) ProtoMessage() {} + +func (x *DeviceConfig) ProtoReflect() protoreflect.Message { + mi := &file_proxy_wireguard_config_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeviceConfig.ProtoReflect.Descriptor instead. +func (*DeviceConfig) Descriptor() ([]byte, []int) { + return file_proxy_wireguard_config_proto_rawDescGZIP(), []int{1} +} + +func (x *DeviceConfig) GetSecretKey() string { + if x != nil { + return x.SecretKey + } + return "" +} + +func (x *DeviceConfig) GetEndpoint() []string { + if x != nil { + return x.Endpoint + } + return nil +} + +func (x *DeviceConfig) GetPeers() []*PeerConfig { + if x != nil { + return x.Peers + } + return nil +} + +func (x *DeviceConfig) GetMtu() int32 { + if x != nil { + return x.Mtu + } + return 0 +} + +func (x *DeviceConfig) GetNumWorkers() int32 { + if x != nil { + return x.NumWorkers + } + return 0 +} + +func (x *DeviceConfig) GetReserved() []byte { + if x != nil { + return x.Reserved + } + return nil +} + +func (x *DeviceConfig) GetDomainStrategy() DeviceConfig_DomainStrategy { + if x != nil { + return x.DomainStrategy + } + return DeviceConfig_FORCE_IP +} + +func (x *DeviceConfig) GetIsClient() bool { + if x != nil { + return x.IsClient + } + return false +} + +func (x *DeviceConfig) GetNoKernelTun() bool { + if x != nil { + return x.NoKernelTun + } + return false +} + +var File_proxy_wireguard_config_proto protoreflect.FileDescriptor + +const file_proxy_wireguard_config_proto_rawDesc = "" + + "\n" + + "\x1cproxy/wireguard/config.proto\x12\x14xray.proxy.wireguard\"\xad\x01\n" + + "\n" + + "PeerConfig\x12\x1d\n" + + "\n" + + "public_key\x18\x01 \x01(\tR\tpublicKey\x12$\n" + + "\x0epre_shared_key\x18\x02 \x01(\tR\fpreSharedKey\x12\x1a\n" + + "\bendpoint\x18\x03 \x01(\tR\bendpoint\x12\x1d\n" + + "\n" + + "keep_alive\x18\x04 \x01(\rR\tkeepAlive\x12\x1f\n" + + "\vallowed_ips\x18\x05 \x03(\tR\n" + + "allowedIps\"\xcb\x03\n" + + "\fDeviceConfig\x12\x1d\n" + + "\n" + + "secret_key\x18\x01 \x01(\tR\tsecretKey\x12\x1a\n" + + "\bendpoint\x18\x02 \x03(\tR\bendpoint\x126\n" + + "\x05peers\x18\x03 \x03(\v2 .xray.proxy.wireguard.PeerConfigR\x05peers\x12\x10\n" + + "\x03mtu\x18\x04 \x01(\x05R\x03mtu\x12\x1f\n" + + "\vnum_workers\x18\x05 \x01(\x05R\n" + + "numWorkers\x12\x1a\n" + + "\breserved\x18\x06 \x01(\fR\breserved\x12Z\n" + + "\x0fdomain_strategy\x18\a \x01(\x0e21.xray.proxy.wireguard.DeviceConfig.DomainStrategyR\x0edomainStrategy\x12\x1b\n" + + "\tis_client\x18\b \x01(\bR\bisClient\x12\"\n" + + "\rno_kernel_tun\x18\t \x01(\bR\vnoKernelTun\"\\\n" + + "\x0eDomainStrategy\x12\f\n" + + "\bFORCE_IP\x10\x00\x12\r\n" + + "\tFORCE_IP4\x10\x01\x12\r\n" + + "\tFORCE_IP6\x10\x02\x12\x0e\n" + + "\n" + + "FORCE_IP46\x10\x03\x12\x0e\n" + + "\n" + + "FORCE_IP64\x10\x04B^\n" + + "\x18com.xray.proxy.wireguardP\x01Z)github.com/xtls/xray-core/proxy/wireguard\xaa\x02\x14Xray.Proxy.WireGuardb\x06proto3" + +var ( + file_proxy_wireguard_config_proto_rawDescOnce sync.Once + file_proxy_wireguard_config_proto_rawDescData []byte +) + +func file_proxy_wireguard_config_proto_rawDescGZIP() []byte { + file_proxy_wireguard_config_proto_rawDescOnce.Do(func() { + file_proxy_wireguard_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proxy_wireguard_config_proto_rawDesc), len(file_proxy_wireguard_config_proto_rawDesc))) + }) + return file_proxy_wireguard_config_proto_rawDescData +} + +var file_proxy_wireguard_config_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_proxy_wireguard_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_proxy_wireguard_config_proto_goTypes = []any{ + (DeviceConfig_DomainStrategy)(0), // 0: xray.proxy.wireguard.DeviceConfig.DomainStrategy + (*PeerConfig)(nil), // 1: xray.proxy.wireguard.PeerConfig + (*DeviceConfig)(nil), // 2: xray.proxy.wireguard.DeviceConfig +} +var file_proxy_wireguard_config_proto_depIdxs = []int32{ + 1, // 0: xray.proxy.wireguard.DeviceConfig.peers:type_name -> xray.proxy.wireguard.PeerConfig + 0, // 1: xray.proxy.wireguard.DeviceConfig.domain_strategy:type_name -> xray.proxy.wireguard.DeviceConfig.DomainStrategy + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_proxy_wireguard_config_proto_init() } +func file_proxy_wireguard_config_proto_init() { + if File_proxy_wireguard_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_proxy_wireguard_config_proto_rawDesc), len(file_proxy_wireguard_config_proto_rawDesc)), + NumEnums: 1, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_proxy_wireguard_config_proto_goTypes, + DependencyIndexes: file_proxy_wireguard_config_proto_depIdxs, + EnumInfos: file_proxy_wireguard_config_proto_enumTypes, + MessageInfos: file_proxy_wireguard_config_proto_msgTypes, + }.Build() + File_proxy_wireguard_config_proto = out.File + file_proxy_wireguard_config_proto_goTypes = nil + file_proxy_wireguard_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/proxy/wireguard/config.proto b/subproject/Xray-core-main/proxy/wireguard/config.proto new file mode 100644 index 00000000..aa05b822 --- /dev/null +++ b/subproject/Xray-core-main/proxy/wireguard/config.proto @@ -0,0 +1,34 @@ +syntax = "proto3"; + +package xray.proxy.wireguard; +option csharp_namespace = "Xray.Proxy.WireGuard"; +option go_package = "github.com/xtls/xray-core/proxy/wireguard"; +option java_package = "com.xray.proxy.wireguard"; +option java_multiple_files = true; + +message PeerConfig { + string public_key = 1; + string pre_shared_key = 2; + string endpoint = 3; + uint32 keep_alive = 4; + repeated string allowed_ips = 5; +} + +message DeviceConfig { + enum DomainStrategy { + FORCE_IP = 0; + FORCE_IP4 = 1; + FORCE_IP6 = 2; + FORCE_IP46 = 3; + FORCE_IP64 = 4; + } + string secret_key = 1; + repeated string endpoint = 2; + repeated PeerConfig peers = 3; + int32 mtu = 4; + int32 num_workers = 5; + bytes reserved = 6; + DomainStrategy domain_strategy = 7; + bool is_client = 8; + bool no_kernel_tun = 9; +} \ No newline at end of file diff --git a/subproject/Xray-core-main/proxy/wireguard/gvisortun/tun.go b/subproject/Xray-core-main/proxy/wireguard/gvisortun/tun.go new file mode 100644 index 00000000..379fad42 --- /dev/null +++ b/subproject/Xray-core-main/proxy/wireguard/gvisortun/tun.go @@ -0,0 +1,226 @@ +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2017-2022 WireGuard LLC. All Rights Reserved. + */ + +package gvisortun + +import ( + "context" + "fmt" + "net/netip" + "os" + "sync" + "syscall" + + "golang.zx2c4.com/wireguard/tun" + "gvisor.dev/gvisor/pkg/buffer" + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/adapters/gonet" + "gvisor.dev/gvisor/pkg/tcpip/header" + "gvisor.dev/gvisor/pkg/tcpip/link/channel" + "gvisor.dev/gvisor/pkg/tcpip/network/ipv4" + "gvisor.dev/gvisor/pkg/tcpip/network/ipv6" + "gvisor.dev/gvisor/pkg/tcpip/stack" + "gvisor.dev/gvisor/pkg/tcpip/transport/icmp" + "gvisor.dev/gvisor/pkg/tcpip/transport/tcp" + "gvisor.dev/gvisor/pkg/tcpip/transport/udp" +) + +type netTun struct { + ep *channel.Endpoint + stack *stack.Stack + events chan tun.Event + notifyHandle *channel.NotificationHandle + incomingPacket chan *buffer.View + mtu int + hasV4, hasV6 bool + closeOnce sync.Once +} + +type Net netTun + +func CreateNetTUN(localAddresses []netip.Addr, mtu int, promiscuousMode bool) (tun.Device, *Net, *stack.Stack, error) { + opts := stack.Options{ + NetworkProtocols: []stack.NetworkProtocolFactory{ipv4.NewProtocol, ipv6.NewProtocol}, + TransportProtocols: []stack.TransportProtocolFactory{tcp.NewProtocol, udp.NewProtocol, icmp.NewProtocol6, icmp.NewProtocol4}, + HandleLocal: !promiscuousMode, + } + dev := &netTun{ + ep: channel.New(1024, uint32(mtu), ""), + stack: stack.New(opts), + events: make(chan tun.Event, 10), + incomingPacket: make(chan *buffer.View), + mtu: mtu, + } + sackEnabledOpt := tcpip.TCPSACKEnabled(true) // TCP SACK is disabled by default + tcpipErr := dev.stack.SetTransportProtocolOption(tcp.ProtocolNumber, &sackEnabledOpt) + if tcpipErr != nil { + return nil, nil, dev.stack, fmt.Errorf("could not enable TCP SACK: %v", tcpipErr) + } + dev.notifyHandle = dev.ep.AddNotify(dev) + tcpipErr = dev.stack.CreateNIC(1, dev.ep) + if tcpipErr != nil { + return nil, nil, dev.stack, fmt.Errorf("CreateNIC: %v", tcpipErr) + } + for _, ip := range localAddresses { + var protoNumber tcpip.NetworkProtocolNumber + if ip.Is4() { + protoNumber = ipv4.ProtocolNumber + } else if ip.Is6() { + protoNumber = ipv6.ProtocolNumber + } + protoAddr := tcpip.ProtocolAddress{ + Protocol: protoNumber, + AddressWithPrefix: tcpip.AddrFromSlice(ip.AsSlice()).WithPrefix(), + } + tcpipErr := dev.stack.AddProtocolAddress(1, protoAddr, stack.AddressProperties{}) + if tcpipErr != nil { + return nil, nil, dev.stack, fmt.Errorf("AddProtocolAddress(%v): %v", ip, tcpipErr) + } + if ip.Is4() { + dev.hasV4 = true + } else if ip.Is6() { + dev.hasV6 = true + } + } + if dev.hasV4 { + dev.stack.AddRoute(tcpip.Route{Destination: header.IPv4EmptySubnet, NIC: 1}) + } + if dev.hasV6 { + dev.stack.AddRoute(tcpip.Route{Destination: header.IPv6EmptySubnet, NIC: 1}) + } + if promiscuousMode { + // enable promiscuous mode to handle all packets processed by netstack + dev.stack.SetPromiscuousMode(1, true) + dev.stack.SetSpoofing(1, true) + } + + dev.events <- tun.EventUp + return dev, (*Net)(dev), dev.stack, nil +} + +// Name implements tun.Device +func (tun *netTun) Name() (string, error) { + return "go", nil +} + +// File implements tun.Device +func (tun *netTun) File() *os.File { + return nil +} + +// Events implements tun.Device +func (tun *netTun) Events() <-chan tun.Event { + return tun.events +} + +// Read implements tun.Device +func (tun *netTun) Read(buf [][]byte, sizes []int, offset int) (int, error) { + view, ok := <-tun.incomingPacket + if !ok { + return 0, os.ErrClosed + } + + n, err := view.Read(buf[0][offset:]) + if err != nil { + return 0, err + } + sizes[0] = n + return 1, nil +} + +// Write implements tun.Device +func (tun *netTun) Write(buf [][]byte, offset int) (int, error) { + for _, buf := range buf { + packet := buf[offset:] + if len(packet) == 0 { + continue + } + + pkb := stack.NewPacketBuffer(stack.PacketBufferOptions{Payload: buffer.MakeWithData(packet)}) + switch packet[0] >> 4 { + case 4: + tun.ep.InjectInbound(header.IPv4ProtocolNumber, pkb) + case 6: + tun.ep.InjectInbound(header.IPv6ProtocolNumber, pkb) + default: + return 0, syscall.EAFNOSUPPORT + } + } + return len(buf), nil +} + +// WriteNotify implements channel.Notification +func (tun *netTun) WriteNotify() { + pkt := tun.ep.Read() + if pkt == nil { + return + } + + view := pkt.ToView() + pkt.DecRef() + + tun.incomingPacket <- view +} + +// Close implements tun.Device +func (tun *netTun) Close() error { + tun.closeOnce.Do(func() { + tun.stack.RemoveNIC(1) + tun.stack.Close() + tun.ep.RemoveNotify(tun.notifyHandle) + tun.ep.Close() + + close(tun.events) + + close(tun.incomingPacket) + }) + return nil +} + +// MTU implements tun.Device +func (tun *netTun) MTU() (int, error) { + return tun.mtu, nil +} + +// BatchSize implements tun.Device +func (tun *netTun) BatchSize() int { + return 1 +} + +func convertToFullAddr(endpoint netip.AddrPort) (tcpip.FullAddress, tcpip.NetworkProtocolNumber) { + var protoNumber tcpip.NetworkProtocolNumber + if endpoint.Addr().Is4() { + protoNumber = ipv4.ProtocolNumber + } else { + protoNumber = ipv6.ProtocolNumber + } + return tcpip.FullAddress{ + NIC: 1, + Addr: tcpip.AddrFromSlice(endpoint.Addr().AsSlice()), + Port: endpoint.Port(), + }, protoNumber +} + +func (net *Net) DialContextTCPAddrPort(ctx context.Context, addr netip.AddrPort) (*gonet.TCPConn, error) { + fa, pn := convertToFullAddr(addr) + return gonet.DialContextTCP(ctx, net.stack, fa, pn) +} + +func (net *Net) DialUDPAddrPort(laddr, raddr netip.AddrPort) (*gonet.UDPConn, error) { + var lfa, rfa *tcpip.FullAddress + var pn tcpip.NetworkProtocolNumber + if laddr.IsValid() || laddr.Port() > 0 { + var addr tcpip.FullAddress + addr, pn = convertToFullAddr(laddr) + lfa = &addr + } + if raddr.IsValid() || raddr.Port() > 0 { + var addr tcpip.FullAddress + addr, pn = convertToFullAddr(raddr) + rfa = &addr + rfa = nil // do not ep connect + } + return gonet.DialUDP(net.stack, lfa, rfa, pn) +} diff --git a/subproject/Xray-core-main/proxy/wireguard/server.go b/subproject/Xray-core-main/proxy/wireguard/server.go new file mode 100644 index 00000000..1f358a38 --- /dev/null +++ b/subproject/Xray-core-main/proxy/wireguard/server.go @@ -0,0 +1,172 @@ +package wireguard + +import ( + "context" + + "github.com/xtls/xray-core/common/buf" + c "github.com/xtls/xray-core/common/ctx" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/log" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/features/dns" + "github.com/xtls/xray-core/features/policy" + "github.com/xtls/xray-core/features/routing" + "github.com/xtls/xray-core/transport" + "github.com/xtls/xray-core/transport/internet/stat" +) + +var nullDestination = net.TCPDestination(net.AnyIP, 0) + +type Server struct { + bindServer *netBindServer + + info routingInfo + policyManager policy.Manager +} + +type routingInfo struct { + ctx context.Context + dispatcher routing.Dispatcher + inboundTag *session.Inbound + contentTag *session.Content +} + +func NewServer(ctx context.Context, conf *DeviceConfig) (*Server, error) { + v := core.MustFromContext(ctx) + + endpoints, hasIPv4, hasIPv6, err := parseEndpoints(conf) + if err != nil { + return nil, err + } + + server := &Server{ + bindServer: &netBindServer{ + netBind: netBind{ + dns: v.GetFeature(dns.ClientType()).(dns.Client), + dnsOption: dns.IPOption{ + IPv4Enable: hasIPv4, + IPv6Enable: hasIPv6, + }, + workers: int(conf.NumWorkers), + readQueue: make(chan *netReadInfo), + }, + }, + policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager), + } + + tun, err := conf.createTun()(endpoints, int(conf.Mtu), server.forwardConnection) + if err != nil { + return nil, err + } + + if err = tun.BuildDevice(createIPCRequest(conf), server.bindServer); err != nil { + _ = tun.Close() + return nil, err + } + + return server, nil +} + +// Network implements proxy.Inbound. +func (*Server) Network() []net.Network { + return []net.Network{net.Network_UDP} +} + +// Process implements proxy.Inbound. +func (s *Server) Process(ctx context.Context, network net.Network, conn stat.Connection, dispatcher routing.Dispatcher) error { + s.info = routingInfo{ + ctx: ctx, + dispatcher: dispatcher, + inboundTag: session.InboundFromContext(ctx), + contentTag: session.ContentFromContext(ctx), + } + + ep, err := s.bindServer.ParseEndpoint(conn.RemoteAddr().String()) + if err != nil { + return err + } + + nep := ep.(*netEndpoint) + nep.conn = conn + + reader := buf.NewPacketReader(conn) + for { + mb, err := reader.ReadMultiBuffer() + if err != nil { + nep.conn = nil + buf.ReleaseMulti(mb) + return err + } + + for i, b := range mb { + + rawBytes := b.Bytes() + if b.Len() > 3 { + rawBytes[1] = 0 + rawBytes[2] = 0 + rawBytes[3] = 0 + } + + select { + case s.bindServer.readQueue <- &netReadInfo{ + buff: b, + endpoint: nep, + }: + case <-s.bindServer.closedCh: + nep.conn = nil + buf.ReleaseMulti(mb[i:]) + return errors.New("bind closed") + } + } + } +} + +func (s *Server) forwardConnection(dest net.Destination, conn net.Conn) { + if s.info.dispatcher == nil { + errors.LogError(s.info.ctx, "unexpected: dispatcher == nil") + return + } + + ctx, cancel := context.WithCancel(core.ToBackgroundDetachedContext(s.info.ctx)) + sid := session.NewID() + ctx = c.ContextWithID(ctx, sid) + inbound := session.Inbound{} // since promiscuousModeHandler mixed-up context, we shallow copy inbound (tag) and content (configs) + if s.info.inboundTag != nil { + inbound = *s.info.inboundTag + } + inbound.Name = "wireguard" + inbound.CanSpliceCopy = 3 + + // overwrite the source to use the tun address for each sub context. + // Since gvisor.ForwarderRequest doesn't provide any info to associate the sub-context with the Parent context + // Currently we have no way to link to the original source address + inbound.Source = net.DestinationFromAddr(conn.RemoteAddr()) + ctx = session.ContextWithInbound(ctx, &inbound) + content := new(session.Content) + if s.info.contentTag != nil { + content.SniffingRequest = s.info.contentTag.SniffingRequest + } + ctx = session.ContextWithContent(ctx, content) + ctx = session.SubContextFromMuxInbound(ctx) + + ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{ + From: nullDestination, + To: dest, + Status: log.AccessAccepted, + Reason: "", + }) + + err := s.info.dispatcher.DispatchLink(ctx, dest, &transport.Link{ + Reader: buf.NewReader(conn), + Writer: buf.NewWriter(conn), + }) + + if err != nil { + errors.LogInfoInner(ctx, err, "connection ends") + } + + cancel() + conn.Close() +} diff --git a/subproject/Xray-core-main/proxy/wireguard/server_test.go b/subproject/Xray-core-main/proxy/wireguard/server_test.go new file mode 100644 index 00000000..057b508e --- /dev/null +++ b/subproject/Xray-core-main/proxy/wireguard/server_test.go @@ -0,0 +1,52 @@ +package wireguard_test + +import ( + "context" + "github.com/stretchr/testify/assert" + "runtime/debug" + "testing" + + "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/proxy/wireguard" +) + +// TestWireGuardServerInitializationError verifies that an error during TUN initialization +// (triggered by an empty SecretKey) in the WireGuard server does not cause a panic and returns an error instead. +func TestWireGuardServerInitializationError(t *testing.T) { + // Create a minimal core instance with default features + config := &core.Config{} + instance, err := core.New(config) + if err != nil { + t.Fatalf("Failed to create core instance: %v", err) + } + // Set the Xray instance in the context + ctx := context.WithValue(context.Background(), core.XrayKey(1), instance) + + // Define the server configuration with an empty SecretKey to trigger error + conf := &wireguard.DeviceConfig{ + IsClient: false, + Endpoint: []string{"10.0.0.1/32"}, + Mtu: 1420, + SecretKey: "", // Empty SecretKey to trigger error + Peers: []*wireguard.PeerConfig{ + { + PublicKey: "some_public_key", + AllowedIps: []string{"10.0.0.2/32"}, + }, + }, + } + + // Use defer to catch any panic and fail the test explicitly + defer func() { + if r := recover(); r != nil { + t.Errorf("TUN initialization panicked: %v", r) + debug.PrintStack() + } + }() + + // Attempt to initialize the WireGuard server + _, err = wireguard.NewServer(ctx, conf) + + // Check that an error is returned + assert.ErrorContains(t, err, "failed to set private_key: hex string does not fit the slice") +} diff --git a/subproject/Xray-core-main/proxy/wireguard/tun.go b/subproject/Xray-core-main/proxy/wireguard/tun.go new file mode 100644 index 00000000..86ff9f45 --- /dev/null +++ b/subproject/Xray-core-main/proxy/wireguard/tun.go @@ -0,0 +1,429 @@ +package wireguard + +import ( + "context" + "fmt" + "io" + "net/netip" + "runtime" + "strconv" + "strings" + "sync" + "time" + + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/log" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/proxy/wireguard/gvisortun" + "gvisor.dev/gvisor/pkg/buffer" + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/adapters/gonet" + "gvisor.dev/gvisor/pkg/tcpip/checksum" + "gvisor.dev/gvisor/pkg/tcpip/header" + "gvisor.dev/gvisor/pkg/tcpip/stack" + "gvisor.dev/gvisor/pkg/tcpip/transport/tcp" + "gvisor.dev/gvisor/pkg/tcpip/transport/udp" + "gvisor.dev/gvisor/pkg/waiter" + + "golang.zx2c4.com/wireguard/conn" + "golang.zx2c4.com/wireguard/device" + "golang.zx2c4.com/wireguard/tun" +) + +type tunCreator func(localAddresses []netip.Addr, mtu int, handler promiscuousModeHandler) (Tunnel, error) + +type promiscuousModeHandler func(dest net.Destination, conn net.Conn) + +type Tunnel interface { + BuildDevice(ipc string, bind conn.Bind) error + DialContextTCPAddrPort(ctx context.Context, addr netip.AddrPort) (net.Conn, error) + DialUDPAddrPort(laddr, raddr netip.AddrPort) (net.Conn, error) + Close() error +} + +type tunnel struct { + tun tun.Device + device *device.Device + rw sync.Mutex +} + +func (t *tunnel) BuildDevice(ipc string, bind conn.Bind) (err error) { + t.rw.Lock() + defer t.rw.Unlock() + + if t.device != nil { + return errors.New("device is already initialized") + } + + logger := &device.Logger{ + Verbosef: func(format string, args ...any) { + log.Record(&log.GeneralMessage{ + Severity: log.Severity_Debug, + Content: fmt.Sprintf(format, args...), + }) + }, + Errorf: func(format string, args ...any) { + log.Record(&log.GeneralMessage{ + Severity: log.Severity_Error, + Content: fmt.Sprintf(format, args...), + }) + }, + } + + t.device = device.NewDevice(t.tun, bind, logger) + if err = t.device.IpcSet(ipc); err != nil { + return err + } + if err = t.device.Up(); err != nil { + return err + } + return nil +} + +func (t *tunnel) Close() (err error) { + t.rw.Lock() + defer t.rw.Unlock() + + if t.device == nil { + return nil + } + + t.device.Close() + t.device = nil + err = t.tun.Close() + t.tun = nil + return nil +} + +func CalculateInterfaceName(name string) (tunName string) { + if runtime.GOOS == "darwin" { + tunName = "utun" + } else if name != "" { + tunName = name + } else { + tunName = "tun" + } + interfaces, err := net.Interfaces() + if err != nil { + return + } + var tunIndex int + for _, netInterface := range interfaces { + if strings.HasPrefix(netInterface.Name, tunName) { + index, parseErr := strconv.ParseInt(netInterface.Name[len(tunName):], 10, 16) + if parseErr == nil { + tunIndex = int(index) + 1 + } + } + } + tunName = fmt.Sprintf("%s%d", tunName, tunIndex) + return +} + +var _ Tunnel = (*gvisorNet)(nil) + +type gvisorNet struct { + tunnel + net *gvisortun.Net +} + +func (g *gvisorNet) Close() error { + return g.tunnel.Close() +} + +func (g *gvisorNet) DialContextTCPAddrPort(ctx context.Context, addr netip.AddrPort) ( + net.Conn, error, +) { + return g.net.DialContextTCPAddrPort(ctx, addr) +} + +func (g *gvisorNet) DialUDPAddrPort(laddr, raddr netip.AddrPort) (net.Conn, error) { + return g.net.DialUDPAddrPort(laddr, raddr) +} + +func createGVisorTun(localAddresses []netip.Addr, mtu int, handler promiscuousModeHandler) (Tunnel, error) { + out := &gvisorNet{} + tun, n, gstack, err := gvisortun.CreateNetTUN(localAddresses, mtu, handler != nil) + if err != nil { + return nil, err + } + + if handler != nil { + // handler is only used for promiscuous mode + // capture all packets and send to handler + + tcpForwarder := tcp.NewForwarder(gstack, 0, 65535, func(r *tcp.ForwarderRequest) { + go func(r *tcp.ForwarderRequest) { + var wq waiter.Queue + var id = r.ID() + + ep, err := r.CreateEndpoint(&wq) + if err != nil { + errors.LogError(context.Background(), err.String()) + r.Complete(true) + return + } + + options := ep.SocketOptions() + options.SetKeepAlive(false) + options.SetReuseAddress(true) + options.SetReusePort(true) + + handler(net.TCPDestination(net.IPAddress(id.LocalAddress.AsSlice()), net.Port(id.LocalPort)), gonet.NewTCPConn(&wq, ep)) + + ep.Close() + r.Complete(false) + }(r) + }) + gstack.SetTransportProtocolHandler(tcp.ProtocolNumber, tcpForwarder.HandlePacket) + + manager := &udpManager{ + stack: gstack, + handler: handler, + m: make(map[string]*udpConn), + } + + gstack.SetTransportProtocolHandler(udp.ProtocolNumber, func(id stack.TransportEndpointID, pkt *stack.PacketBuffer) bool { + data := pkt.Clone().Data().AsRange().ToSlice() + // if len(data) == 0 { + // return false + // } + srcIP := net.IPAddress(id.RemoteAddress.AsSlice()) + dstIP := net.IPAddress(id.LocalAddress.AsSlice()) + if srcIP == nil || dstIP == nil { + errors.LogDebug(context.Background(), "drop udp with size ", len(data), " > invalid ip address ", id.RemoteAddress.AsSlice(), " ", id.LocalAddress.AsSlice()) + return true + } + src := net.UDPDestination(srcIP, net.Port(id.RemotePort)) + dst := net.UDPDestination(dstIP, net.Port(id.LocalPort)) + manager.feed(src, dst, data) + return true + }) + } + + out.tun, out.net = tun, n + return out, nil +} + +type udpManager struct { + stack *stack.Stack + handler func(dest net.Destination, conn net.Conn) + m map[string]*udpConn + mutex sync.RWMutex +} + +func (m *udpManager) feed(src net.Destination, dst net.Destination, data []byte) { + m.mutex.RLock() + uc, ok := m.m[src.NetAddr()] + if ok { + select { + case uc.queue <- &packet{ + p: data, + dest: &dst, + }: + default: + errors.LogDebug(context.Background(), "drop udp with size ", len(data), " to ", dst.NetAddr(), " original ", uc.dst.NetAddr(), " > queue full") + } + m.mutex.RUnlock() + return + } + m.mutex.RUnlock() + + m.mutex.Lock() + defer m.mutex.Unlock() + + uc, ok = m.m[src.NetAddr()] + if !ok { + uc = &udpConn{ + queue: make(chan *packet, 1024), + src: src, + dst: dst, + } + uc.writeFunc = m.writeRawUDPPacket + uc.closeFunc = func() { + m.mutex.Lock() + m.close(uc) + m.mutex.Unlock() + } + m.m[src.NetAddr()] = uc + go m.handler(dst, uc) + } + + select { + case uc.queue <- &packet{ + p: data, + dest: &dst, + }: + default: + errors.LogDebug(context.Background(), "drop udp with size ", len(data), " to ", dst.NetAddr(), " original ", uc.dst.NetAddr(), " > queue full") + } +} + +func (m *udpManager) close(uc *udpConn) { + if !uc.closed { + uc.closed = true + close(uc.queue) + delete(m.m, uc.src.NetAddr()) + } +} + +func (m *udpManager) writeRawUDPPacket(payload []byte, src net.Destination, dst net.Destination) error { + udpLen := header.UDPMinimumSize + len(payload) + srcIP := tcpip.AddrFromSlice(src.Address.IP()) + dstIP := tcpip.AddrFromSlice(dst.Address.IP()) + + // build packet with appropriate IP header size + isIPv4 := dst.Address.Family().IsIPv4() + ipHdrSize := header.IPv6MinimumSize + ipProtocol := header.IPv6ProtocolNumber + if isIPv4 { + ipHdrSize = header.IPv4MinimumSize + ipProtocol = header.IPv4ProtocolNumber + } + + pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{ + ReserveHeaderBytes: ipHdrSize + header.UDPMinimumSize, + Payload: buffer.MakeWithData(payload), + }) + defer pkt.DecRef() + + // Build UDP header + udpHdr := header.UDP(pkt.TransportHeader().Push(header.UDPMinimumSize)) + udpHdr.Encode(&header.UDPFields{ + SrcPort: uint16(src.Port), + DstPort: uint16(dst.Port), + Length: uint16(udpLen), + }) + + // Calculate and set UDP checksum + xsum := header.PseudoHeaderChecksum(header.UDPProtocolNumber, srcIP, dstIP, uint16(udpLen)) + udpHdr.SetChecksum(^udpHdr.CalculateChecksum(checksum.Checksum(payload, xsum))) + + // Build IP header + if isIPv4 { + ipHdr := header.IPv4(pkt.NetworkHeader().Push(header.IPv4MinimumSize)) + ipHdr.Encode(&header.IPv4Fields{ + TotalLength: uint16(header.IPv4MinimumSize + udpLen), + TTL: 64, + Protocol: uint8(header.UDPProtocolNumber), + SrcAddr: srcIP, + DstAddr: dstIP, + }) + ipHdr.SetChecksum(^ipHdr.CalculateChecksum()) + } else { + ipHdr := header.IPv6(pkt.NetworkHeader().Push(header.IPv6MinimumSize)) + ipHdr.Encode(&header.IPv6Fields{ + PayloadLength: uint16(udpLen), + TransportProtocol: header.UDPProtocolNumber, + HopLimit: 64, + SrcAddr: srcIP, + DstAddr: dstIP, + }) + } + + // dispatch the packet + err := m.stack.WriteRawPacket(1, ipProtocol, buffer.MakeWithView(pkt.ToView())) + if err != nil { + return errors.New("failed to write raw udp packet back to stack err ", err) + } + + return nil +} + +type packet struct { + p []byte + dest *net.Destination +} + +type udpConn struct { + queue chan *packet + src net.Destination + dst net.Destination + writeFunc func(payload []byte, src net.Destination, dst net.Destination) error + closeFunc func() + closed bool +} + +func (c *udpConn) ReadMultiBuffer() (buf.MultiBuffer, error) { + for { + q, ok := <-c.queue + if !ok { + return nil, io.EOF + } + + b := buf.New() + + _, err := b.Write(q.p) + if err != nil { + errors.LogDebugInner(context.Background(), err, "drop udp with size ", len(q.p), " to ", q.dest.NetAddr(), " original ", c.dst.NetAddr()) + b.Release() + continue + } + + b.UDP = q.dest + + return buf.MultiBuffer{b}, nil + } +} + +func (c *udpConn) Read(p []byte) (int, error) { + q, ok := <-c.queue + if !ok { + return 0, io.EOF + } + n := copy(p, q.p) + if n != len(q.p) { + return 0, io.ErrShortBuffer + } + return n, nil +} + +func (c *udpConn) WriteMultiBuffer(mb buf.MultiBuffer) error { + for i, b := range mb { + dst := c.dst + if b.UDP != nil { + dst = *b.UDP + } + err := c.writeFunc(b.Bytes(), dst, c.src) + if err != nil { + buf.ReleaseMulti(mb[i:]) + return err + } + b.Release() + } + return nil +} + +func (c *udpConn) Write(p []byte) (int, error) { + err := c.writeFunc(p, c.dst, c.src) + if err != nil { + return 0, err + } + return len(p), nil +} + +func (c *udpConn) Close() error { + c.closeFunc() + return nil +} + +func (c *udpConn) LocalAddr() net.Addr { + return c.dst.RawNetAddr() +} + +func (c *udpConn) RemoteAddr() net.Addr { + return c.src.RawNetAddr() +} + +func (c *udpConn) SetDeadline(t time.Time) error { + return nil +} + +func (c *udpConn) SetReadDeadline(t time.Time) error { + return nil +} + +func (c *udpConn) SetWriteDeadline(t time.Time) error { + return nil +} diff --git a/subproject/Xray-core-main/proxy/wireguard/tun_default.go b/subproject/Xray-core-main/proxy/wireguard/tun_default.go new file mode 100644 index 00000000..50a50944 --- /dev/null +++ b/subproject/Xray-core-main/proxy/wireguard/tun_default.go @@ -0,0 +1,16 @@ +//go:build !linux || android + +package wireguard + +import ( + "errors" + "net/netip" +) + +func createKernelTun(localAddresses []netip.Addr, mtu int, handler promiscuousModeHandler) (t Tunnel, err error) { + return nil, errors.New("not implemented") +} + +func KernelTunSupported() (bool, error) { + return false, nil +} diff --git a/subproject/Xray-core-main/proxy/wireguard/tun_linux.go b/subproject/Xray-core-main/proxy/wireguard/tun_linux.go new file mode 100644 index 00000000..068e21ee --- /dev/null +++ b/subproject/Xray-core-main/proxy/wireguard/tun_linux.go @@ -0,0 +1,293 @@ +//go:build linux && !android + +package wireguard + +import ( + "context" + goerrors "errors" + "fmt" + "net" + "net/netip" + "os" + "sync" + "syscall" + + "golang.org/x/sys/unix" + + "github.com/vishvananda/netlink" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/transport/internet" + "golang.zx2c4.com/wireguard/tun" +) + +type deviceNet struct { + tunnel + dialer *net.Dialer + lc *net.ListenConfig + + handle *netlink.Handle + linkAddrs []netlink.Addr + routes []*netlink.Route + rules []*netlink.Rule +} + +var ( + tableIndex int = 10230 + mu sync.Mutex +) + +func allocateIPv6TableIndex() int { + mu.Lock() + defer mu.Unlock() + + if tableIndex > 10230 { + errors.LogInfo(context.Background(), "allocate new ipv6 table index: ", tableIndex) + } + currentIndex := tableIndex + tableIndex++ + return currentIndex +} + +func newDeviceNet(interfaceName string) *deviceNet { + dialer := &net.Dialer{} + dialer.Control = func(network, address string, c syscall.RawConn) error { + return c.Control(func(fd uintptr) { + if err := syscall.BindToDevice(int(fd), interfaceName); err != nil { + errors.LogInfoInner(context.Background(), err, "failed to bind to device") + } + }) + } + lc := &net.ListenConfig{} + lc.Control = func(network, address string, c syscall.RawConn) error { + return c.Control(func(fd uintptr) { + if err := syscall.BindToDevice(int(fd), interfaceName); err != nil { + errors.LogInfoInner(context.Background(), err, "failed to bind to device") + } + }) + } + return &deviceNet{dialer: dialer, lc: lc} +} + +func (d *deviceNet) DialContextTCPAddrPort(ctx context.Context, addr netip.AddrPort) ( + net.Conn, error, +) { + return d.dialer.DialContext(ctx, "tcp", addr.String()) +} + +func (d *deviceNet) DialUDPAddrPort(laddr, raddr netip.AddrPort) (net.Conn, error) { + var conn net.PacketConn + var err error + if raddr.Addr().Is4() { + conn, err = d.lc.ListenPacket(context.Background(), "udp4", ":0") + } else { + conn, err = d.lc.ListenPacket(context.Background(), "udp6", ":0") + } + if err != nil { + return nil, err + } + return &internet.PacketConnWrapper{ + PacketConn: conn, + Dest: &net.UDPAddr{ + IP: raddr.Addr().AsSlice(), + Port: int(raddr.Port()), + }, + }, nil +} + +func (d *deviceNet) Close() (err error) { + var errs []error + for _, rule := range d.rules { + if err = d.handle.RuleDel(rule); err != nil { + errs = append(errs, fmt.Errorf("failed to delete rule: %w", err)) + } + } + for _, route := range d.routes { + if err = d.handle.RouteDel(route); err != nil { + errs = append(errs, fmt.Errorf("failed to delete route: %w", err)) + } + } + if err = d.tunnel.Close(); err != nil { + errs = append(errs, fmt.Errorf("failed to close tunnel: %w", err)) + } + if d.handle != nil { + d.handle.Close() + d.handle = nil + } + if len(errs) == 0 { + return nil + } + return goerrors.Join(errs...) +} + +func createKernelTun(localAddresses []netip.Addr, mtu int, handler promiscuousModeHandler) (t Tunnel, err error) { + if handler != nil { + return nil, errors.New("TODO: support promiscuous mode") + } + + var v4, v6 *netip.Addr + for _, prefixes := range localAddresses { + if v4 == nil && prefixes.Is4() { + x := prefixes + v4 = &x + } + if v6 == nil && prefixes.Is6() { + x := prefixes + v6 = &x + } + } + + writeSysctlZero := func(path string) error { + _, err := os.Stat(path) + if os.IsNotExist(err) { + return nil + } + if err != nil { + return err + } + return os.WriteFile(path, []byte("0"), 0o644) + } + + // system configs. + if v4 != nil { + if err = writeSysctlZero("/proc/sys/net/ipv4/conf/all/rp_filter"); err != nil { + return nil, fmt.Errorf("failed to disable ipv4 rp_filter for all: %w", err) + } + } + if v6 != nil { + if err = writeSysctlZero("/proc/sys/net/ipv6/conf/all/disable_ipv6"); err != nil { + return nil, fmt.Errorf("failed to enable ipv6: %w", err) + } + if err = writeSysctlZero("/proc/sys/net/ipv6/conf/all/rp_filter"); err != nil { + return nil, fmt.Errorf("failed to disable ipv6 rp_filter for all: %w", err) + } + } + + n := CalculateInterfaceName("wg") + wgt, err := tun.CreateTUN(n, mtu) + if err != nil { + return nil, err + } + defer func() { + if err != nil { + _ = wgt.Close() + } + }() + + // disable linux rp_filter for tunnel device to avoid packet drop. + // the operation require root privilege on container require '--privileged' flag. + if v4 != nil { + if err = writeSysctlZero("/proc/sys/net/ipv4/conf/" + n + "/rp_filter"); err != nil { + return nil, fmt.Errorf("failed to disable ipv4 rp_filter for tunnel: %w", err) + } + } + if v6 != nil { + if err = writeSysctlZero("/proc/sys/net/ipv6/conf/" + n + "/rp_filter"); err != nil { + return nil, fmt.Errorf("failed to disable ipv6 rp_filter for tunnel: %w", err) + } + } + + ipv6TableIndex := allocateIPv6TableIndex() + if v6 != nil { + r := &netlink.Route{Table: ipv6TableIndex} + for { + routeList, fErr := netlink.RouteListFiltered(netlink.FAMILY_V6, r, netlink.RT_FILTER_TABLE) + if len(routeList) == 0 || fErr != nil { + break + } + ipv6TableIndex-- + if ipv6TableIndex < 0 { + return nil, fmt.Errorf("failed to find available ipv6 table index") + } + } + } + + out := newDeviceNet(n) + out.handle, err = netlink.NewHandle() + if err != nil { + return nil, err + } + defer func() { + if err != nil { + _ = out.Close() + } + }() + + l, err := netlink.LinkByName(n) + if err != nil { + return nil, err + } + + if v4 != nil { + addr := netlink.Addr{ + IPNet: &net.IPNet{ + IP: v4.AsSlice(), + Mask: net.CIDRMask(v4.BitLen(), v4.BitLen()), + }, + } + out.linkAddrs = append(out.linkAddrs, addr) + } + if v6 != nil { + addr := netlink.Addr{ + IPNet: &net.IPNet{ + IP: v6.AsSlice(), + Mask: net.CIDRMask(v6.BitLen(), v6.BitLen()), + }, + } + out.linkAddrs = append(out.linkAddrs, addr) + + rt := &netlink.Route{ + LinkIndex: l.Attrs().Index, + Dst: &net.IPNet{ + IP: net.IPv6zero, + Mask: net.CIDRMask(0, 128), + }, + Table: ipv6TableIndex, + } + out.routes = append(out.routes, rt) + + r := netlink.NewRule() + r.Table, r.Family, r.Src = ipv6TableIndex, unix.AF_INET6, addr.IPNet + out.rules = append(out.rules, r) + r = netlink.NewRule() + r.Table, r.Family, r.OifName = ipv6TableIndex, unix.AF_INET6, n + out.rules = append(out.rules, r) + } + + for _, addr := range out.linkAddrs { + if err = out.handle.AddrAdd(l, &addr); err != nil { + return nil, fmt.Errorf("failed to add address %s to %s: %w", addr, n, err) + } + } + if err = out.handle.LinkSetMTU(l, mtu); err != nil { + return nil, err + } + if err = out.handle.LinkSetUp(l); err != nil { + return nil, err + } + + for _, route := range out.routes { + if err = out.handle.RouteAdd(route); err != nil { + return nil, fmt.Errorf("failed to add route %s: %w", route, err) + } + } + for _, rule := range out.rules { + if err = out.handle.RuleAdd(rule); err != nil { + return nil, fmt.Errorf("failed to add rule %s: %w", rule, err) + } + } + out.tun = wgt + return out, nil +} + +func KernelTunSupported() (bool, error) { + var hdr unix.CapUserHeader + hdr.Version = unix.LINUX_CAPABILITY_VERSION_3 + hdr.Pid = 0 // 0 means current process + + var data unix.CapUserData + if err := unix.Capget(&hdr, &data); err != nil { + return false, fmt.Errorf("failed to get capabilities: %v", err) + } + + return (data.Effective & (1 << unix.CAP_NET_ADMIN)) != 0, nil +} diff --git a/subproject/Xray-core-main/proxy/wireguard/wireguard.go b/subproject/Xray-core-main/proxy/wireguard/wireguard.go new file mode 100644 index 00000000..4f489114 --- /dev/null +++ b/subproject/Xray-core-main/proxy/wireguard/wireguard.go @@ -0,0 +1,93 @@ +package wireguard + +import ( + "context" + "errors" + "fmt" + "net/netip" + "strings" + + "github.com/xtls/xray-core/common" +) + +func init() { + common.Must(common.RegisterConfig((*DeviceConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + deviceConfig := config.(*DeviceConfig) + if deviceConfig.IsClient { + return New(ctx, deviceConfig) + } else { + return NewServer(ctx, deviceConfig) + } + })) +} + +// convert endpoint string to netip.Addr +func parseEndpoints(conf *DeviceConfig) ([]netip.Addr, bool, bool, error) { + var hasIPv4, hasIPv6 bool + + endpoints := make([]netip.Addr, len(conf.Endpoint)) + for i, str := range conf.Endpoint { + var addr netip.Addr + if strings.Contains(str, "/") { + prefix, err := netip.ParsePrefix(str) + if err != nil { + return nil, false, false, err + } + addr = prefix.Addr() + if prefix.Bits() != addr.BitLen() { + return nil, false, false, errors.New("interface address subnet should be /32 for IPv4 and /128 for IPv6") + } + } else { + var err error + addr, err = netip.ParseAddr(str) + if err != nil { + return nil, false, false, err + } + } + endpoints[i] = addr + + if addr.Is4() { + hasIPv4 = true + } else if addr.Is6() { + hasIPv6 = true + } + } + + return endpoints, hasIPv4, hasIPv6, nil +} + +// serialize the config into an IPC request +func createIPCRequest(conf *DeviceConfig) string { + var request strings.Builder + + request.WriteString(fmt.Sprintf("private_key=%s\n", conf.SecretKey)) + + if !conf.IsClient { + // placeholder, we'll handle actual port listening on Xray + request.WriteString("listen_port=1337\n") + } + + for _, peer := range conf.Peers { + if peer.PublicKey != "" { + request.WriteString(fmt.Sprintf("public_key=%s\n", peer.PublicKey)) + } + + if peer.PreSharedKey != "" { + request.WriteString(fmt.Sprintf("preshared_key=%s\n", peer.PreSharedKey)) + } + + if peer.Endpoint != "" { + request.WriteString(fmt.Sprintf("endpoint=%s\n", peer.Endpoint)) + } + + for _, ip := range peer.AllowedIps { + request.WriteString(fmt.Sprintf("allowed_ip=%s\n", ip)) + } + + if peer.KeepAlive != 0 { + request.WriteString(fmt.Sprintf("persistent_keepalive_interval=%d\n", peer.KeepAlive)) + } + } + + return request.String()[:request.Len()] +} diff --git a/subproject/Xray-core-main/testing/coverage/coverall b/subproject/Xray-core-main/testing/coverage/coverall new file mode 100644 index 00000000..a8463437 --- /dev/null +++ b/subproject/Xray-core-main/testing/coverage/coverall @@ -0,0 +1,48 @@ +#!/bin/bash + +FAIL=0 + +XRAY_OUT=${PWD}/out/xray +export XRAY_COV=${XRAY_OUT}/cov +COVERAGE_FILE=${XRAY_COV}/coverage.txt + +function test_package { + DIR=".$1" + DEP=$(go list -f '{{ join .Deps "\n" }}' $DIR | grep xray | tr '\n' ',') + DEP=${DEP}$DIR + RND_NAME=$(openssl rand -hex 16) + COV_PROFILE=${XRAY_COV}/${RND_NAME}.out + go test -tags "json coverage" -coverprofile=${COV_PROFILE} -coverpkg=$DEP $DIR || FAIL=1 +} + +rm -rf ${XRAY_OUT} +mkdir -p ${XRAY_COV} +touch ${COVERAGE_FILE} + +TEST_FILES=(./*_test.go) +if [ -f ${TEST_FILES[0]} ]; then + test_package "" +fi + +for DIR in $(find * -type d ! -path "*.git*" ! -path "*vendor*" ! -path "*external*"); do + TEST_FILES=($DIR/*_test.go) + if [ -f ${TEST_FILES[0]} ]; then + test_package "/$DIR" + fi +done + +for OUT_FILE in $(find ${XRAY_COV} -name "*.out"); do + echo "Merging file ${OUT_FILE}" + cat ${OUT_FILE} | grep -v "mode: set" >> ${COVERAGE_FILE} +done + +COV_SORTED=${XRAY_COV}/coverallsorted.out +cat ${COVERAGE_FILE} | sort -t: -k1 | grep -vw "testing" | grep -v ".pb.go" | grep -vw "vendor" | grep -vw "external" > ${COV_SORTED} +echo "mode: set" | cat - ${COV_SORTED} > ${COVERAGE_FILE} + +if [ "$FAIL" -eq 0 ]; then + echo "Uploading coverage datea to codecov." + #bash <(curl -s https://codecov.io/bash) -f ${COVERAGE_FILE} -v || echo "Codecov did not collect coverage reports." +fi + +exit $FAIL diff --git a/subproject/Xray-core-main/testing/coverage/coverall2 b/subproject/Xray-core-main/testing/coverage/coverall2 new file mode 100644 index 00000000..ce6b0319 --- /dev/null +++ b/subproject/Xray-core-main/testing/coverage/coverall2 @@ -0,0 +1,40 @@ +#!/bin/bash + +COVERAGE_FILE=${PWD}/coverage.txt +COV_SORTED=${PWD}/coverallsorted.out + +touch "$COVERAGE_FILE" + +function test_package { + DIR=".$1" + DEP=$(go list -f '{{ join .Deps "\n" }}' "$DIR" | grep xray | tr '\n' ',') + DEP=${DEP}$DIR + RND_NAME=$(openssl rand -hex 16) + COV_PROFILE=${RND_NAME}.out + go test -coverprofile="$COV_PROFILE" -coverpkg="$DEP" "$DIR" || return +} + +TEST_FILES=(./*_test.go) +if [ -f "${TEST_FILES[0]}" ]; then + test_package "" +fi + +# shellcheck disable=SC2044 +for DIR in $(find ./* -type d ! -path "*.git*" ! -path "*vendor*" ! -path "*external*"); do + TEST_FILES=("$DIR"/*_test.go) + if [ -f "${TEST_FILES[0]}" ]; then + test_package "/$DIR" + fi +done + +# merge out +while IFS= read -r -d '' OUT_FILE +do + echo "Merging file ${OUT_FILE}" + < "${OUT_FILE}" grep -v "mode: set" >> "$COVERAGE_FILE" +done < <(find ./* -name "*.out" -print0) + +< "$COVERAGE_FILE" sort -t: -k1 | grep -vw "testing" | grep -v ".pb.go" | grep -vw "vendor" | grep -vw "external" > "$COV_SORTED" +echo "mode: set" | cat - "${COV_SORTED}" > "${COVERAGE_FILE}" + +bash <(curl -s https://codecov.io/bash) || echo 'Codecov failed to upload' \ No newline at end of file diff --git a/subproject/Xray-core-main/testing/mocks/dns.go b/subproject/Xray-core-main/testing/mocks/dns.go new file mode 100644 index 00000000..fb398366 --- /dev/null +++ b/subproject/Xray-core-main/testing/mocks/dns.go @@ -0,0 +1,94 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/xtls/xray-core/features/dns (interfaces: Client) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + net "net" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + dns "github.com/xtls/xray-core/features/dns" +) + +// DNSClient is a mock of Client interface +type DNSClient struct { + ctrl *gomock.Controller + recorder *DNSClientMockRecorder +} + +// DNSClientMockRecorder is the mock recorder for DNSClient +type DNSClientMockRecorder struct { + mock *DNSClient +} + +// NewDNSClient creates a new mock instance +func NewDNSClient(ctrl *gomock.Controller) *DNSClient { + mock := &DNSClient{ctrl: ctrl} + mock.recorder = &DNSClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *DNSClient) EXPECT() *DNSClientMockRecorder { + return m.recorder +} + +// Close mocks base method +func (m *DNSClient) Close() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Close") + ret0, _ := ret[0].(error) + return ret0 +} + +// Close indicates an expected call of Close +func (mr *DNSClientMockRecorder) Close() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*DNSClient)(nil).Close)) +} + +// LookupIP mocks base method +func (m *DNSClient) LookupIP(arg0 string, arg1 dns.IPOption) ([]net.IP, uint32, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LookupIP", arg0, arg1) + ret0, _ := ret[0].([]net.IP) + ret1, _ := ret[1].(uint32) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// LookupIP indicates an expected call of LookupIP +func (mr *DNSClientMockRecorder) LookupIP(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LookupIP", reflect.TypeOf((*DNSClient)(nil).LookupIP), arg0, arg1) +} + +// Start mocks base method +func (m *DNSClient) Start() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Start") + ret0, _ := ret[0].(error) + return ret0 +} + +// Start indicates an expected call of Start +func (mr *DNSClientMockRecorder) Start() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*DNSClient)(nil).Start)) +} + +// Type mocks base method +func (m *DNSClient) Type() interface{} { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Type") + ret0, _ := ret[0].(interface{}) + return ret0 +} + +// Type indicates an expected call of Type +func (mr *DNSClientMockRecorder) Type() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Type", reflect.TypeOf((*DNSClient)(nil).Type)) +} diff --git a/subproject/Xray-core-main/testing/mocks/io.go b/subproject/Xray-core-main/testing/mocks/io.go new file mode 100644 index 00000000..e1b72c0f --- /dev/null +++ b/subproject/Xray-core-main/testing/mocks/io.go @@ -0,0 +1,87 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: io (interfaces: Reader,Writer) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" +) + +// Reader is a mock of Reader interface +type Reader struct { + ctrl *gomock.Controller + recorder *ReaderMockRecorder +} + +// ReaderMockRecorder is the mock recorder for Reader +type ReaderMockRecorder struct { + mock *Reader +} + +// NewReader creates a new mock instance +func NewReader(ctrl *gomock.Controller) *Reader { + mock := &Reader{ctrl: ctrl} + mock.recorder = &ReaderMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *Reader) EXPECT() *ReaderMockRecorder { + return m.recorder +} + +// Read mocks base method +func (m *Reader) Read(arg0 []byte) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Read", arg0) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Read indicates an expected call of Read +func (mr *ReaderMockRecorder) Read(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*Reader)(nil).Read), arg0) +} + +// Writer is a mock of Writer interface +type Writer struct { + ctrl *gomock.Controller + recorder *WriterMockRecorder +} + +// WriterMockRecorder is the mock recorder for Writer +type WriterMockRecorder struct { + mock *Writer +} + +// NewWriter creates a new mock instance +func NewWriter(ctrl *gomock.Controller) *Writer { + mock := &Writer{ctrl: ctrl} + mock.recorder = &WriterMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *Writer) EXPECT() *WriterMockRecorder { + return m.recorder +} + +// Write mocks base method +func (m *Writer) Write(arg0 []byte) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Write", arg0) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Write indicates an expected call of Write +func (mr *WriterMockRecorder) Write(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Write", reflect.TypeOf((*Writer)(nil).Write), arg0) +} diff --git a/subproject/Xray-core-main/testing/mocks/log.go b/subproject/Xray-core-main/testing/mocks/log.go new file mode 100644 index 00000000..2014714a --- /dev/null +++ b/subproject/Xray-core-main/testing/mocks/log.go @@ -0,0 +1,47 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/xtls/xray-core/common/log (interfaces: Handler) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + log "github.com/xtls/xray-core/common/log" +) + +// LogHandler is a mock of Handler interface +type LogHandler struct { + ctrl *gomock.Controller + recorder *LogHandlerMockRecorder +} + +// LogHandlerMockRecorder is the mock recorder for LogHandler +type LogHandlerMockRecorder struct { + mock *LogHandler +} + +// NewLogHandler creates a new mock instance +func NewLogHandler(ctrl *gomock.Controller) *LogHandler { + mock := &LogHandler{ctrl: ctrl} + mock.recorder = &LogHandlerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *LogHandler) EXPECT() *LogHandlerMockRecorder { + return m.recorder +} + +// Handle mocks base method +func (m *LogHandler) Handle(arg0 log.Message) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Handle", arg0) +} + +// Handle indicates an expected call of Handle +func (mr *LogHandlerMockRecorder) Handle(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Handle", reflect.TypeOf((*LogHandler)(nil).Handle), arg0) +} diff --git a/subproject/Xray-core-main/testing/mocks/mux.go b/subproject/Xray-core-main/testing/mocks/mux.go new file mode 100644 index 00000000..99690896 --- /dev/null +++ b/subproject/Xray-core-main/testing/mocks/mux.go @@ -0,0 +1,50 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/xtls/xray-core/common/mux (interfaces: ClientWorkerFactory) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + mux "github.com/xtls/xray-core/common/mux" +) + +// MuxClientWorkerFactory is a mock of ClientWorkerFactory interface +type MuxClientWorkerFactory struct { + ctrl *gomock.Controller + recorder *MuxClientWorkerFactoryMockRecorder +} + +// MuxClientWorkerFactoryMockRecorder is the mock recorder for MuxClientWorkerFactory +type MuxClientWorkerFactoryMockRecorder struct { + mock *MuxClientWorkerFactory +} + +// NewMuxClientWorkerFactory creates a new mock instance +func NewMuxClientWorkerFactory(ctrl *gomock.Controller) *MuxClientWorkerFactory { + mock := &MuxClientWorkerFactory{ctrl: ctrl} + mock.recorder = &MuxClientWorkerFactoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MuxClientWorkerFactory) EXPECT() *MuxClientWorkerFactoryMockRecorder { + return m.recorder +} + +// Create mocks base method +func (m *MuxClientWorkerFactory) Create() (*mux.ClientWorker, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Create") + ret0, _ := ret[0].(*mux.ClientWorker) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Create indicates an expected call of Create +func (mr *MuxClientWorkerFactoryMockRecorder) Create() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MuxClientWorkerFactory)(nil).Create)) +} diff --git a/subproject/Xray-core-main/testing/mocks/outbound.go b/subproject/Xray-core-main/testing/mocks/outbound.go new file mode 100644 index 00000000..f6352bff --- /dev/null +++ b/subproject/Xray-core-main/testing/mocks/outbound.go @@ -0,0 +1,185 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/xtls/xray-core/features/outbound (interfaces: Manager,HandlerSelector) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + outbound "github.com/xtls/xray-core/features/outbound" +) + +// OutboundManager is a mock of Manager interface +type OutboundManager struct { + ctrl *gomock.Controller + recorder *OutboundManagerMockRecorder +} + +// OutboundManagerMockRecorder is the mock recorder for OutboundManager +type OutboundManagerMockRecorder struct { + mock *OutboundManager +} + +// NewOutboundManager creates a new mock instance +func NewOutboundManager(ctrl *gomock.Controller) *OutboundManager { + mock := &OutboundManager{ctrl: ctrl} + mock.recorder = &OutboundManagerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *OutboundManager) EXPECT() *OutboundManagerMockRecorder { + return m.recorder +} + +// AddHandler mocks base method +func (m *OutboundManager) AddHandler(arg0 context.Context, arg1 outbound.Handler) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddHandler", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// AddHandler indicates an expected call of AddHandler +func (mr *OutboundManagerMockRecorder) AddHandler(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddHandler", reflect.TypeOf((*OutboundManager)(nil).AddHandler), arg0, arg1) +} + +// Close mocks base method +func (m *OutboundManager) Close() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Close") + ret0, _ := ret[0].(error) + return ret0 +} + +// Close indicates an expected call of Close +func (mr *OutboundManagerMockRecorder) Close() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*OutboundManager)(nil).Close)) +} + +// GetDefaultHandler mocks base method +func (m *OutboundManager) GetDefaultHandler() outbound.Handler { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetDefaultHandler") + ret0, _ := ret[0].(outbound.Handler) + return ret0 +} + +// GetDefaultHandler indicates an expected call of GetDefaultHandler +func (mr *OutboundManagerMockRecorder) GetDefaultHandler() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDefaultHandler", reflect.TypeOf((*OutboundManager)(nil).GetDefaultHandler)) +} + +// GetHandler mocks base method +func (m *OutboundManager) GetHandler(arg0 string) outbound.Handler { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetHandler", arg0) + ret0, _ := ret[0].(outbound.Handler) + return ret0 +} + +// GetHandler indicates an expected call of GetHandler +func (mr *OutboundManagerMockRecorder) GetHandler(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetHandler", reflect.TypeOf((*OutboundManager)(nil).GetHandler), arg0) +} + +// ListHandlers mocks base method +func (m *OutboundManager) ListHandlers(arg0 context.Context) []outbound.Handler { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListHandlers", arg0) + ret0, _ := ret[0].([]outbound.Handler) + return ret0 +} + +// ListHandlers indicates an expected call of ListHandlers +func (mr *OutboundManagerMockRecorder) ListHandlers(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListHandlers", reflect.TypeOf((*OutboundManager)(nil).ListHandlers), arg0) +} + +// RemoveHandler mocks base method +func (m *OutboundManager) RemoveHandler(arg0 context.Context, arg1 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RemoveHandler", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// RemoveHandler indicates an expected call of RemoveHandler +func (mr *OutboundManagerMockRecorder) RemoveHandler(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveHandler", reflect.TypeOf((*OutboundManager)(nil).RemoveHandler), arg0, arg1) +} + +// Start mocks base method +func (m *OutboundManager) Start() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Start") + ret0, _ := ret[0].(error) + return ret0 +} + +// Start indicates an expected call of Start +func (mr *OutboundManagerMockRecorder) Start() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*OutboundManager)(nil).Start)) +} + +// Type mocks base method +func (m *OutboundManager) Type() interface{} { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Type") + ret0, _ := ret[0].(interface{}) + return ret0 +} + +// Type indicates an expected call of Type +func (mr *OutboundManagerMockRecorder) Type() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Type", reflect.TypeOf((*OutboundManager)(nil).Type)) +} + +// OutboundHandlerSelector is a mock of HandlerSelector interface +type OutboundHandlerSelector struct { + ctrl *gomock.Controller + recorder *OutboundHandlerSelectorMockRecorder +} + +// OutboundHandlerSelectorMockRecorder is the mock recorder for OutboundHandlerSelector +type OutboundHandlerSelectorMockRecorder struct { + mock *OutboundHandlerSelector +} + +// NewOutboundHandlerSelector creates a new mock instance +func NewOutboundHandlerSelector(ctrl *gomock.Controller) *OutboundHandlerSelector { + mock := &OutboundHandlerSelector{ctrl: ctrl} + mock.recorder = &OutboundHandlerSelectorMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *OutboundHandlerSelector) EXPECT() *OutboundHandlerSelectorMockRecorder { + return m.recorder +} + +// Select mocks base method +func (m *OutboundHandlerSelector) Select(arg0 []string) []string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Select", arg0) + ret0, _ := ret[0].([]string) + return ret0 +} + +// Select indicates an expected call of Select +func (mr *OutboundHandlerSelectorMockRecorder) Select(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Select", reflect.TypeOf((*OutboundHandlerSelector)(nil).Select), arg0) +} diff --git a/subproject/Xray-core-main/testing/mocks/proxy.go b/subproject/Xray-core-main/testing/mocks/proxy.go new file mode 100644 index 00000000..19c36aae --- /dev/null +++ b/subproject/Xray-core-main/testing/mocks/proxy.go @@ -0,0 +1,105 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/xtls/xray-core/proxy (interfaces: Inbound,Outbound) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + net "github.com/xtls/xray-core/common/net" + routing "github.com/xtls/xray-core/features/routing" + transport "github.com/xtls/xray-core/transport" + internet "github.com/xtls/xray-core/transport/internet" + "github.com/xtls/xray-core/transport/internet/stat" +) + +// ProxyInbound is a mock of Inbound interface +type ProxyInbound struct { + ctrl *gomock.Controller + recorder *ProxyInboundMockRecorder +} + +// ProxyInboundMockRecorder is the mock recorder for ProxyInbound +type ProxyInboundMockRecorder struct { + mock *ProxyInbound +} + +// NewProxyInbound creates a new mock instance +func NewProxyInbound(ctrl *gomock.Controller) *ProxyInbound { + mock := &ProxyInbound{ctrl: ctrl} + mock.recorder = &ProxyInboundMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *ProxyInbound) EXPECT() *ProxyInboundMockRecorder { + return m.recorder +} + +// Network mocks base method +func (m *ProxyInbound) Network() []net.Network { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Network") + ret0, _ := ret[0].([]net.Network) + return ret0 +} + +// Network indicates an expected call of Network +func (mr *ProxyInboundMockRecorder) Network() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Network", reflect.TypeOf((*ProxyInbound)(nil).Network)) +} + +// Process mocks base method +func (m *ProxyInbound) Process(arg0 context.Context, arg1 net.Network, arg2 stat.Connection, arg3 routing.Dispatcher) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Process", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(error) + return ret0 +} + +// Process indicates an expected call of Process +func (mr *ProxyInboundMockRecorder) Process(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Process", reflect.TypeOf((*ProxyInbound)(nil).Process), arg0, arg1, arg2, arg3) +} + +// ProxyOutbound is a mock of Outbound interface +type ProxyOutbound struct { + ctrl *gomock.Controller + recorder *ProxyOutboundMockRecorder +} + +// ProxyOutboundMockRecorder is the mock recorder for ProxyOutbound +type ProxyOutboundMockRecorder struct { + mock *ProxyOutbound +} + +// NewProxyOutbound creates a new mock instance +func NewProxyOutbound(ctrl *gomock.Controller) *ProxyOutbound { + mock := &ProxyOutbound{ctrl: ctrl} + mock.recorder = &ProxyOutboundMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *ProxyOutbound) EXPECT() *ProxyOutboundMockRecorder { + return m.recorder +} + +// Process mocks base method +func (m *ProxyOutbound) Process(arg0 context.Context, arg1 *transport.Link, arg2 internet.Dialer) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Process", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// Process indicates an expected call of Process +func (mr *ProxyOutboundMockRecorder) Process(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Process", reflect.TypeOf((*ProxyOutbound)(nil).Process), arg0, arg1, arg2) +} diff --git a/subproject/Xray-core-main/testing/scenarios/command_test.go b/subproject/Xray-core-main/testing/scenarios/command_test.go new file mode 100644 index 00000000..7afe47d0 --- /dev/null +++ b/subproject/Xray-core-main/testing/scenarios/command_test.go @@ -0,0 +1,664 @@ +package scenarios + +import ( + "context" + "fmt" + "io" + "strings" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/xtls/xray-core/app/commander" + "github.com/xtls/xray-core/app/policy" + "github.com/xtls/xray-core/app/proxyman" + "github.com/xtls/xray-core/app/proxyman/command" + "github.com/xtls/xray-core/app/router" + "github.com/xtls/xray-core/app/stats" + statscmd "github.com/xtls/xray-core/app/stats/command" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/serial" + "github.com/xtls/xray-core/common/uuid" + core "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/proxy/dokodemo" + "github.com/xtls/xray-core/proxy/freedom" + "github.com/xtls/xray-core/proxy/vmess" + "github.com/xtls/xray-core/proxy/vmess/inbound" + "github.com/xtls/xray-core/proxy/vmess/outbound" + "github.com/xtls/xray-core/testing/servers/tcp" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/protobuf/testing/protocmp" +) + +func TestCommanderListenConfigurationItem(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + clientPort := tcp.PickPort() + cmdPort := tcp.PickPort() + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&commander.Config{ + Tag: "api", + Listen: fmt.Sprintf("127.0.0.1:%d", cmdPort), + Service: []*serial.TypedMessage{ + serial.ToTypedMessage(&command.Config{}), + }, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + Tag: "d", + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + Tag: "default-outbound", + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + servers, err := InitializeServerConfigs(clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + if err := testTCPConn(clientPort, 1024, time.Second*5)(); err != nil { + t.Fatal(err) + } + + cmdConn, err := grpc.Dial(fmt.Sprintf("127.0.0.1:%d", cmdPort), grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithBlock()) + common.Must(err) + defer cmdConn.Close() + + hsClient := command.NewHandlerServiceClient(cmdConn) + resp, err := hsClient.RemoveInbound(context.Background(), &command.RemoveInboundRequest{ + Tag: "d", + }) + common.Must(err) + if resp == nil { + t.Error("unexpected nil response") + } + + { + _, err := net.DialTCP("tcp", nil, &net.TCPAddr{ + IP: []byte{127, 0, 0, 1}, + Port: int(clientPort), + }) + if err == nil { + t.Error("unexpected nil error") + } + } +} + +func TestCommanderRemoveHandler(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + clientPort := tcp.PickPort() + cmdPort := tcp.PickPort() + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&commander.Config{ + Tag: "api", + Service: []*serial.TypedMessage{ + serial.ToTypedMessage(&command.Config{}), + }, + }), + serial.ToTypedMessage(&router.Config{ + Rule: []*router.RoutingRule{ + { + InboundTag: []string{"api"}, + TargetTag: &router.RoutingRule_Tag{ + Tag: "api", + }, + }, + }, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + Tag: "d", + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + { + Tag: "api", + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(cmdPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + Tag: "default-outbound", + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + servers, err := InitializeServerConfigs(clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + if err := testTCPConn(clientPort, 1024, time.Second*5)(); err != nil { + t.Fatal(err) + } + + cmdConn, err := grpc.Dial(fmt.Sprintf("127.0.0.1:%d", cmdPort), grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithBlock()) + common.Must(err) + defer cmdConn.Close() + + hsClient := command.NewHandlerServiceClient(cmdConn) + resp, err := hsClient.RemoveInbound(context.Background(), &command.RemoveInboundRequest{ + Tag: "d", + }) + common.Must(err) + if resp == nil { + t.Error("unexpected nil response") + } + + { + _, err := net.DialTCP("tcp", nil, &net.TCPAddr{ + IP: []byte{127, 0, 0, 1}, + Port: int(clientPort), + }) + if err == nil { + t.Error("unexpected nil error") + } + } +} + +func TestCommanderListHandlers(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + clientPort := tcp.PickPort() + cmdPort := tcp.PickPort() + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&commander.Config{ + Tag: "api", + Service: []*serial.TypedMessage{ + serial.ToTypedMessage(&command.Config{}), + }, + }), + serial.ToTypedMessage(&router.Config{ + Rule: []*router.RoutingRule{ + { + InboundTag: []string{"api"}, + TargetTag: &router.RoutingRule_Tag{ + Tag: "api", + }, + }, + }, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + Tag: "d", + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + { + Tag: "api", + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(cmdPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + Tag: "default-outbound", + SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{}), + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + servers, err := InitializeServerConfigs(clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + if err := testTCPConn(clientPort, 1024, time.Second*5)(); err != nil { + t.Fatal(err) + } + + cmdConn, err := grpc.Dial(fmt.Sprintf("127.0.0.1:%d", cmdPort), grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithBlock()) + common.Must(err) + defer cmdConn.Close() + + hsClient := command.NewHandlerServiceClient(cmdConn) + inboundResp, err := hsClient.ListInbounds(context.Background(), &command.ListInboundsRequest{}) + common.Must(err) + if inboundResp == nil { + t.Error("unexpected nil response") + } + + if diff := cmp.Diff( + inboundResp.Inbounds, + clientConfig.Inbound, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b *core.InboundHandlerConfig) bool { + return a.Tag < b.Tag + })); diff != "" { + t.Fatalf("inbound response doesn't match config (-want +got):\n%s", diff) + } + + outboundResp, err := hsClient.ListOutbounds(context.Background(), &command.ListOutboundsRequest{}) + common.Must(err) + if outboundResp == nil { + t.Error("unexpected nil response") + } + + if diff := cmp.Diff( + outboundResp.Outbounds, + clientConfig.Outbound, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b *core.InboundHandlerConfig) bool { + return a.Tag < b.Tag + })); diff != "" { + t.Fatalf("outbound response doesn't match config (-want +got):\n%s", diff) + } +} + +func TestCommanderAddRemoveUser(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + u1 := protocol.NewID(uuid.New()) + u2 := protocol.NewID(uuid.New()) + + cmdPort := tcp.PickPort() + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&commander.Config{ + Tag: "api", + Service: []*serial.TypedMessage{ + serial.ToTypedMessage(&command.Config{}), + }, + }), + serial.ToTypedMessage(&router.Config{ + Rule: []*router.RoutingRule{ + { + InboundTag: []string{"api"}, + TargetTag: &router.RoutingRule_Tag{ + Tag: "api", + }, + }, + }, + }), + serial.ToTypedMessage(&policy.Config{ + Level: map[uint32]*policy.Policy{ + 0: { + Timeout: &policy.Policy_Timeout{ + UplinkOnly: &policy.Second{Value: 0}, + DownlinkOnly: &policy.Second{Value: 0}, + }, + }, + }, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + Tag: "v", + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: u1.String(), + }), + }, + }, + }), + }, + { + Tag: "api", + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(cmdPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&policy.Config{ + Level: map[uint32]*policy.Policy{ + 0: { + Timeout: &policy.Policy_Timeout{ + UplinkOnly: &policy.Second{Value: 0}, + DownlinkOnly: &policy.Second{Value: 0}, + }, + }, + }, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + Tag: "d", + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: u2.String(), + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_AES128_GCM, + }, + }), + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + if err := testTCPConn(clientPort, 1024, time.Second*5)(); err != io.EOF && + /*We might wish to drain the connection*/ + (err != nil && !strings.HasSuffix(err.Error(), "i/o timeout")) { + t.Fatal("expected error: ", err) + } + + cmdConn, err := grpc.Dial(fmt.Sprintf("127.0.0.1:%d", cmdPort), grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithBlock()) + common.Must(err) + defer cmdConn.Close() + + hsClient := command.NewHandlerServiceClient(cmdConn) + resp, err := hsClient.AlterInbound(context.Background(), &command.AlterInboundRequest{ + Tag: "v", + Operation: serial.ToTypedMessage( + &command.AddUserOperation{ + User: &protocol.User{ + Email: "test@example.com", + Account: serial.ToTypedMessage(&vmess.Account{ + Id: u2.String(), + }), + }, + }), + }) + common.Must(err) + if resp == nil { + t.Fatal("nil response") + } + + if err := testTCPConn(clientPort, 1024, time.Second*5)(); err != nil { + t.Fatal(err) + } + + resp, err = hsClient.AlterInbound(context.Background(), &command.AlterInboundRequest{ + Tag: "v", + Operation: serial.ToTypedMessage(&command.RemoveUserOperation{Email: "test@example.com"}), + }) + common.Must(err) + if resp == nil { + t.Fatal("nil response") + } +} + +func TestCommanderStats(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + userID := protocol.NewID(uuid.New()) + serverPort := tcp.PickPort() + cmdPort := tcp.PickPort() + + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&stats.Config{}), + serial.ToTypedMessage(&commander.Config{ + Tag: "api", + Service: []*serial.TypedMessage{ + serial.ToTypedMessage(&statscmd.Config{}), + }, + }), + serial.ToTypedMessage(&router.Config{ + Rule: []*router.RoutingRule{ + { + InboundTag: []string{"api"}, + TargetTag: &router.RoutingRule_Tag{ + Tag: "api", + }, + }, + }, + }), + serial.ToTypedMessage(&policy.Config{ + Level: map[uint32]*policy.Policy{ + 0: { + Timeout: &policy.Policy_Timeout{ + UplinkOnly: &policy.Second{Value: 0}, + DownlinkOnly: &policy.Second{Value: 0}, + }, + }, + 1: { + Stats: &policy.Policy_Stats{ + UserUplink: true, + UserDownlink: true, + }, + }, + }, + System: &policy.SystemPolicy{ + Stats: &policy.SystemPolicy_Stats{ + InboundUplink: true, + }, + }, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + Tag: "vmess", + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Level: 1, + Email: "test", + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }), + }, + { + Tag: "api", + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(cmdPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_AES128_GCM, + }, + }), + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + if err != nil { + t.Fatal("Failed to create all servers", err) + } + defer CloseAllServers(servers) + + if err := testTCPConn(clientPort, 10240*1024, time.Second*20)(); err != nil { + t.Fatal(err) + } + + cmdConn, err := grpc.Dial(fmt.Sprintf("127.0.0.1:%d", cmdPort), grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithBlock()) + common.Must(err) + defer cmdConn.Close() + + const name = "user>>>test>>>traffic>>>uplink" + sClient := statscmd.NewStatsServiceClient(cmdConn) + + sresp, err := sClient.GetStats(context.Background(), &statscmd.GetStatsRequest{ + Name: name, + Reset_: true, + }) + common.Must(err) + if r := cmp.Diff(sresp.Stat, &statscmd.Stat{ + Name: name, + Value: 10240 * 1024, + }, cmpopts.IgnoreUnexported(statscmd.Stat{})); r != "" { + t.Error(r) + } + + sresp, err = sClient.GetStats(context.Background(), &statscmd.GetStatsRequest{ + Name: name, + }) + common.Must(err) + if r := cmp.Diff(sresp.Stat, &statscmd.Stat{ + Name: name, + Value: 0, + }, cmpopts.IgnoreUnexported(statscmd.Stat{})); r != "" { + t.Error(r) + } + + sresp, err = sClient.GetStats(context.Background(), &statscmd.GetStatsRequest{ + Name: "inbound>>>vmess>>>traffic>>>uplink", + Reset_: true, + }) + common.Must(err) + if sresp.Stat.Value <= 10240*1024 { + t.Error("value < 10240*1024: ", sresp.Stat.Value) + } +} diff --git a/subproject/Xray-core-main/testing/scenarios/common.go b/subproject/Xray-core-main/testing/scenarios/common.go new file mode 100644 index 00000000..a6eea215 --- /dev/null +++ b/subproject/Xray-core-main/testing/scenarios/common.go @@ -0,0 +1,269 @@ +package scenarios + +import ( + "bytes" + "crypto/rand" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "runtime" + "sync" + "syscall" + "testing" + "time" + + "github.com/xtls/xray-core/app/dispatcher" + "github.com/xtls/xray-core/app/proxyman" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/log" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/retry" + "github.com/xtls/xray-core/common/serial" + "github.com/xtls/xray-core/common/units" + core "github.com/xtls/xray-core/core" + "google.golang.org/protobuf/proto" +) + +func xor(b []byte) []byte { + r := make([]byte, len(b)) + for i, v := range b { + r[i] = v ^ 'c' + } + return r +} + +func readFrom(conn net.Conn, timeout time.Duration, length int) []byte { + b := make([]byte, length) + deadline := time.Now().Add(timeout) + conn.SetReadDeadline(deadline) + n, err := io.ReadFull(conn, b[:length]) + if err != nil { + fmt.Println("Unexpected error from readFrom:", err) + } + return b[:n] +} + +func readFrom2(conn net.Conn, timeout time.Duration, length int) ([]byte, error) { + b := make([]byte, length) + deadline := time.Now().Add(timeout) + conn.SetReadDeadline(deadline) + n, err := io.ReadFull(conn, b[:length]) + if err != nil { + return nil, err + } + return b[:n], nil +} + +func InitializeServerConfigs(configs ...*core.Config) ([]*exec.Cmd, error) { + servers := make([]*exec.Cmd, 0, 10) + + for _, config := range configs { + server, err := InitializeServerConfig(config) + if err != nil { + CloseAllServers(servers) + return nil, err + } + servers = append(servers, server) + } + + time.Sleep(time.Second * 2) + + return servers, nil +} + +func InitializeServerConfig(config *core.Config) (*exec.Cmd, error) { + err := BuildXray() + if err != nil { + return nil, err + } + + config = withDefaultApps(config) + configBytes, err := proto.Marshal(config) + if err != nil { + return nil, err + } + proc := RunXrayProtobuf(configBytes) + + if err := proc.Start(); err != nil { + return nil, err + } + + return proc, nil +} + +var ( + testBinaryPath string + testBinaryCleanFn func() + testBinaryPathGen sync.Once +) + +func genTestBinaryPath() { + testBinaryPathGen.Do(func() { + var tempDir string + common.Must(retry.Timed(5, 100).On(func() error { + dir, err := os.MkdirTemp("", "xray") + if err != nil { + return err + } + tempDir = dir + testBinaryCleanFn = func() { os.RemoveAll(dir) } + return nil + })) + file := filepath.Join(tempDir, "xray.test") + if runtime.GOOS == "windows" { + file += ".exe" + } + testBinaryPath = file + fmt.Printf("Generated binary path: %s\n", file) + }) +} + +func GetSourcePath() string { + return filepath.Join("github.com", "xtls", "xray-core", "main") +} + +func CloseAllServers(servers []*exec.Cmd) { + log.Record(&log.GeneralMessage{ + Severity: log.Severity_Info, + Content: "Closing all servers.", + }) + for _, server := range servers { + if runtime.GOOS == "windows" { + server.Process.Kill() + } else { + server.Process.Signal(syscall.SIGTERM) + } + } + for _, server := range servers { + server.Process.Wait() + } + log.Record(&log.GeneralMessage{ + Severity: log.Severity_Info, + Content: "All server closed.", + }) +} + +func CloseServer(server *exec.Cmd) { + log.Record(&log.GeneralMessage{ + Severity: log.Severity_Info, + Content: "Closing server.", + }) + if runtime.GOOS == "windows" { + server.Process.Kill() + } else { + server.Process.Signal(syscall.SIGTERM) + } + server.Process.Wait() + log.Record(&log.GeneralMessage{ + Severity: log.Severity_Info, + Content: "Server closed.", + }) +} + +func withDefaultApps(config *core.Config) *core.Config { + config.App = append(config.App, serial.ToTypedMessage(&dispatcher.Config{})) + config.App = append(config.App, serial.ToTypedMessage(&proxyman.InboundConfig{})) + config.App = append(config.App, serial.ToTypedMessage(&proxyman.OutboundConfig{})) + return config +} + +func testTCPConn(port net.Port, payloadSize int, timeout time.Duration) func() error { + return func() error { + conn, err := net.DialTCP("tcp", nil, &net.TCPAddr{ + IP: []byte{127, 0, 0, 1}, + Port: int(port), + }) + if err != nil { + return err + } + defer conn.Close() + + return testTCPConn2(conn, payloadSize, timeout)() + } +} + +func testUDPConn(port net.Port, payloadSize int, timeout time.Duration) func() error { + return func() error { + conn, err := net.DialUDP("udp", nil, &net.UDPAddr{ + IP: []byte{127, 0, 0, 1}, + Port: int(port), + }) + if err != nil { + return err + } + defer conn.Close() + + return testTCPConn2(conn, payloadSize, timeout)() + } +} + +func testTCPConn2(conn net.Conn, payloadSize int, timeout time.Duration) func() error { + return func() (err1 error) { + start := time.Now() + defer func() { + var m runtime.MemStats + runtime.ReadMemStats(&m) + // For info on each, see: https://golang.org/pkg/runtime/#MemStats + fmt.Println("testConn finishes:", time.Since(start).Milliseconds(), "ms\t", + err1, "\tAlloc =", units.ByteSize(m.Alloc).String(), + "\tTotalAlloc =", units.ByteSize(m.TotalAlloc).String(), + "\tSys =", units.ByteSize(m.Sys).String(), + "\tNumGC =", m.NumGC) + }() + singleWrite := func(length int) error { + payload := make([]byte, length) + common.Must2(rand.Read(payload)) + + nBytes, err := conn.Write(payload) + if err != nil { + return err + } + if nBytes != len(payload) { + return errors.New("expect ", len(payload), " written, but actually ", nBytes) + } + + response, err := readFrom2(conn, timeout, length) + if err != nil { + return err + } + _ = response + + if r := bytes.Compare(response, xor(payload)); r != 0 { + return errors.New(r) + } + + return nil + } + for payloadSize > 0 { + sizeToWrite := 1024 + if payloadSize < 1024 { + sizeToWrite = payloadSize + } + if err := singleWrite(sizeToWrite); err != nil { + return err + } + payloadSize -= sizeToWrite + } + return nil + } +} + +func WaitConnAvailableWithTest(t *testing.T, testFunc func() error) bool { + for i := 1; ; i++ { + if i > 10 { + t.Log("All attempts failed to test tcp conn") + return false + } + time.Sleep(time.Millisecond * 10) + if err := testFunc(); err != nil { + t.Log("err ", err) + } else { + t.Log("success with", i, "attempts") + break + } + } + return true +} diff --git a/subproject/Xray-core-main/testing/scenarios/common_coverage.go b/subproject/Xray-core-main/testing/scenarios/common_coverage.go new file mode 100644 index 00000000..160df47c --- /dev/null +++ b/subproject/Xray-core-main/testing/scenarios/common_coverage.go @@ -0,0 +1,37 @@ +//go:build coverage +// +build coverage + +package scenarios + +import ( + "bytes" + "os" + "os/exec" + + "github.com/xtls/xray-core/common/uuid" +) + +func BuildXray() error { + genTestBinaryPath() + if _, err := os.Stat(testBinaryPath); err == nil { + return nil + } + + cmd := exec.Command("go", "test", "-tags", "coverage coveragemain", "-coverpkg", "github.com/xtls/xray-core/...", "-c", "-o", testBinaryPath, GetSourcePath()) + return cmd.Run() +} + +func RunXrayProtobuf(config []byte) *exec.Cmd { + genTestBinaryPath() + + covDir := os.Getenv("XRAY_COV") + os.MkdirAll(covDir, os.ModeDir) + randomID := uuid.New() + profile := randomID.String() + ".out" + proc := exec.Command(testBinaryPath, "-config=stdin:", "-format=pb", "-test.run", "TestRunMainForCoverage", "-test.coverprofile", profile, "-test.outputdir", covDir) + proc.Stdin = bytes.NewBuffer(config) + proc.Stderr = os.Stderr + proc.Stdout = os.Stdout + + return proc +} diff --git a/subproject/Xray-core-main/testing/scenarios/common_regular.go b/subproject/Xray-core-main/testing/scenarios/common_regular.go new file mode 100644 index 00000000..19efc713 --- /dev/null +++ b/subproject/Xray-core-main/testing/scenarios/common_regular.go @@ -0,0 +1,34 @@ +//go:build !coverage +// +build !coverage + +package scenarios + +import ( + "bytes" + "fmt" + "os" + "os/exec" +) + +func BuildXray() error { + genTestBinaryPath() + if _, err := os.Stat(testBinaryPath); err == nil { + return nil + } + + fmt.Printf("Building Xray into path (%s)\n", testBinaryPath) + cmd := exec.Command("go", "build", "-o="+testBinaryPath, GetSourcePath()) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} + +func RunXrayProtobuf(config []byte) *exec.Cmd { + genTestBinaryPath() + proc := exec.Command(testBinaryPath, "-config=stdin:", "-format=pb") + proc.Stdin = bytes.NewBuffer(config) + proc.Stderr = os.Stderr + proc.Stdout = os.Stdout + + return proc +} diff --git a/subproject/Xray-core-main/testing/scenarios/dns_test.go b/subproject/Xray-core-main/testing/scenarios/dns_test.go new file mode 100644 index 00000000..4de2fe7e --- /dev/null +++ b/subproject/Xray-core-main/testing/scenarios/dns_test.go @@ -0,0 +1,108 @@ +package scenarios + +import ( + "fmt" + "testing" + "time" + + "github.com/xtls/xray-core/app/dns" + "github.com/xtls/xray-core/app/proxyman" + "github.com/xtls/xray-core/app/router" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/serial" + "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/proxy/blackhole" + "github.com/xtls/xray-core/proxy/freedom" + "github.com/xtls/xray-core/proxy/socks" + "github.com/xtls/xray-core/testing/servers/tcp" + "github.com/xtls/xray-core/transport/internet" + xproxy "golang.org/x/net/proxy" +) + +func TestResolveIP(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&dns.Config{ + StaticHosts: []*dns.Config_HostMapping{ + { + Type: dns.DomainMatchingType_Full, + Domain: "google.com", + Ip: [][]byte{dest.Address.IP()}, + }, + }, + }), + serial.ToTypedMessage(&router.Config{ + DomainStrategy: router.Config_IpIfNonMatch, + Rule: []*router.RoutingRule{ + { + Geoip: []*router.GeoIP{ + { + Cidr: []*router.CIDR{ + { + Ip: []byte{127, 0, 0, 0}, + Prefix: 8, + }, + }, + }, + }, + TargetTag: &router.RoutingRule_Tag{ + Tag: "direct", + }, + }, + }, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&socks.ServerConfig{ + AuthType: socks.AuthType_NO_AUTH, + Accounts: map[string]string{ + "Test Account": "Test Password", + }, + Address: net.NewIPOrDomain(net.LocalHostIP), + UdpEnabled: false, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&blackhole.Config{}), + }, + { + Tag: "direct", + ProxySettings: serial.ToTypedMessage(&freedom.Config{ + DomainStrategy: internet.DomainStrategy_USE_IP, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig) + common.Must(err) + defer CloseAllServers(servers) + + { + noAuthDialer, err := xproxy.SOCKS5("tcp", net.TCPDestination(net.LocalHostIP, serverPort).NetAddr(), nil, xproxy.Direct) + common.Must(err) + conn, err := noAuthDialer.Dial("tcp", fmt.Sprintf("google.com:%d", dest.Port)) + common.Must(err) + defer conn.Close() + + if err := testTCPConn2(conn, 1024, time.Second*5)(); err != nil { + t.Error(err) + } + } +} diff --git a/subproject/Xray-core-main/testing/scenarios/dokodemo_test.go b/subproject/Xray-core-main/testing/scenarios/dokodemo_test.go new file mode 100644 index 00000000..81234cd1 --- /dev/null +++ b/subproject/Xray-core-main/testing/scenarios/dokodemo_test.go @@ -0,0 +1,222 @@ +package scenarios + +import ( + "testing" + "time" + + "github.com/xtls/xray-core/app/log" + "github.com/xtls/xray-core/app/proxyman" + "github.com/xtls/xray-core/common" + clog "github.com/xtls/xray-core/common/log" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/serial" + "github.com/xtls/xray-core/common/uuid" + "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/proxy/dokodemo" + "github.com/xtls/xray-core/proxy/freedom" + "github.com/xtls/xray-core/proxy/vmess" + "github.com/xtls/xray-core/proxy/vmess/inbound" + "github.com/xtls/xray-core/proxy/vmess/outbound" + "github.com/xtls/xray-core/testing/servers/tcp" + "github.com/xtls/xray-core/testing/servers/udp" + "golang.org/x/sync/errgroup" +) + +func TestDokodemoTCP(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + userID := protocol.NewID(uuid.New()) + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + server, err := InitializeServerConfig(serverConfig) + common.Must(err) + defer CloseServer(server) + + clientPortRange := uint32(5) + retry := 1 + clientPort := uint32(tcp.PickPort()) + for { + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{{From: clientPort, To: clientPort + clientPortRange}}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }), + }, + }, + } + + server, _ := InitializeServerConfig(clientConfig) + if server != nil && WaitConnAvailableWithTest(t, testTCPConn(net.Port(clientPort), 1024, time.Second*2)) { + defer CloseServer(server) + break + } + retry++ + if retry > 5 { + t.Fatal("All attempts failed to start client") + } + clientPort = uint32(tcp.PickPort()) + } + + for port := clientPort; port <= clientPort+clientPortRange; port++ { + if err := testTCPConn(net.Port(port), 1024, time.Second*2)(); err != nil { + t.Error(err) + } + } +} + +func TestDokodemoUDP(t *testing.T) { + udpServer := udp.Server{ + MsgProcessor: xor, + } + dest, err := udpServer.Start() + common.Must(err) + defer udpServer.Close() + + userID := protocol.NewID(uuid.New()) + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + server, err := InitializeServerConfig(serverConfig) + common.Must(err) + defer CloseServer(server) + + clientPortRange := uint32(3) + retry := 1 + clientPort := uint32(udp.PickPort()) + for { + clientConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{{From: clientPort, To: clientPort + clientPortRange}}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_UDP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }), + }, + }, + } + + server, _ := InitializeServerConfig(clientConfig) + if server != nil && WaitConnAvailableWithTest(t, testUDPConn(net.Port(clientPort), 1024, time.Second*2)) { + defer CloseServer(server) + break + } + retry++ + if retry > 5 { + t.Fatal("All attempts failed to start client") + } + clientPort = uint32(udp.PickPort()) + } + + var errg errgroup.Group + for port := clientPort; port <= clientPort+clientPortRange; port++ { + errg.Go(testUDPConn(net.Port(port), 1024, time.Second*5)) + } + if err := errg.Wait(); err != nil { + t.Error(err) + } +} diff --git a/subproject/Xray-core-main/testing/scenarios/feature_test.go b/subproject/Xray-core-main/testing/scenarios/feature_test.go new file mode 100644 index 00000000..e668587e --- /dev/null +++ b/subproject/Xray-core-main/testing/scenarios/feature_test.go @@ -0,0 +1,703 @@ +package scenarios + +import ( + "context" + "io" + "net/http" + "net/url" + "testing" + "time" + + "github.com/xtls/xray-core/app/dispatcher" + "github.com/xtls/xray-core/app/log" + "github.com/xtls/xray-core/app/proxyman" + _ "github.com/xtls/xray-core/app/proxyman/inbound" + _ "github.com/xtls/xray-core/app/proxyman/outbound" + "github.com/xtls/xray-core/app/router" + "github.com/xtls/xray-core/common" + clog "github.com/xtls/xray-core/common/log" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/serial" + "github.com/xtls/xray-core/common/uuid" + core "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/proxy/blackhole" + "github.com/xtls/xray-core/proxy/dokodemo" + "github.com/xtls/xray-core/proxy/freedom" + v2http "github.com/xtls/xray-core/proxy/http" + "github.com/xtls/xray-core/proxy/socks" + "github.com/xtls/xray-core/proxy/vmess" + "github.com/xtls/xray-core/proxy/vmess/inbound" + "github.com/xtls/xray-core/proxy/vmess/outbound" + "github.com/xtls/xray-core/testing/servers/tcp" + "github.com/xtls/xray-core/testing/servers/udp" + "github.com/xtls/xray-core/transport/internet" + xproxy "golang.org/x/net/proxy" +) + +func TestPassiveConnection(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + SendFirst: []byte("send first"), + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig) + common.Must(err) + defer CloseAllServers(servers) + + conn, err := net.DialTCP("tcp", nil, &net.TCPAddr{ + IP: []byte{127, 0, 0, 1}, + Port: int(serverPort), + }) + common.Must(err) + + { + response := make([]byte, 1024) + nBytes, err := conn.Read(response) + common.Must(err) + if string(response[:nBytes]) != "send first" { + t.Error("unexpected first response: ", string(response[:nBytes])) + } + } + + if err := testTCPConn2(conn, 1024, time.Second*5)(); err != nil { + t.Error(err) + } +} + +func TestProxy(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + serverUserID := protocol.NewID(uuid.New()) + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: serverUserID.String(), + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + proxyUserID := protocol.NewID(uuid.New()) + proxyPort := tcp.PickPort() + proxyConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(proxyPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: proxyUserID.String(), + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: serverUserID.String(), + }), + }, + }, + }), + SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{ + ProxySettings: &internet.ProxyConfig{ + Tag: "proxy", + }, + }), + }, + { + Tag: "proxy", + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(proxyPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: proxyUserID.String(), + }), + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, proxyConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + if err := testTCPConn(clientPort, 1024, time.Second*5)(); err != nil { + t.Error(err) + } +} + +func TestProxyOverKCP(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + serverUserID := protocol.NewID(uuid.New()) + serverPort := udp.PickPort() + serverConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + StreamSettings: &internet.StreamConfig{ + ProtocolName: "mkcp", + }, + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: serverUserID.String(), + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + proxyUserID := protocol.NewID(uuid.New()) + proxyPort := tcp.PickPort() + proxyConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(proxyPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: proxyUserID.String(), + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{ + StreamSettings: &internet.StreamConfig{ + ProtocolName: "mkcp", + }, + }), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: serverUserID.String(), + }), + }, + }, + }), + SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{ + ProxySettings: &internet.ProxyConfig{ + Tag: "proxy", + }, + StreamSettings: &internet.StreamConfig{ + ProtocolName: "mkcp", + }, + }), + }, + { + Tag: "proxy", + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(proxyPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: proxyUserID.String(), + }), + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, proxyConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + if err := testTCPConn(clientPort, 1024, time.Second*5)(); err != nil { + t.Error(err) + } +} + +func TestBlackhole(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + tcpServer2 := tcp.Server{ + MsgProcessor: xor, + } + dest2, err := tcpServer2.Start() + common.Must(err) + defer tcpServer2.Close() + + serverPort := tcp.PickPort() + serverPort2 := tcp.PickPort() + serverConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort2)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest2.Address), + Port: uint32(dest2.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + Tag: "direct", + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + { + Tag: "blocked", + ProxySettings: serial.ToTypedMessage(&blackhole.Config{}), + }, + }, + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&router.Config{ + Rule: []*router.RoutingRule{ + { + TargetTag: &router.RoutingRule_Tag{ + Tag: "blocked", + }, + PortList: &net.PortList{ + Range: []*net.PortRange{net.SinglePortRange(dest2.Port)}, + }, + }, + }, + }), + }, + } + + servers, err := InitializeServerConfigs(serverConfig) + common.Must(err) + defer CloseAllServers(servers) + + if err := testTCPConn(serverPort2, 1024, time.Second*5)(); err == nil { + t.Error("nil error") + } +} + +func TestForward(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&socks.ServerConfig{ + AuthType: socks.AuthType_NO_AUTH, + Accounts: map[string]string{ + "Test Account": "Test Password", + }, + Address: net.NewIPOrDomain(net.LocalHostIP), + UdpEnabled: false, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{ + DestinationOverride: &freedom.DestinationOverride{ + Server: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(dest.Port), + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig) + common.Must(err) + defer CloseAllServers(servers) + + { + noAuthDialer, err := xproxy.SOCKS5("tcp", net.TCPDestination(net.LocalHostIP, serverPort).NetAddr(), nil, xproxy.Direct) + common.Must(err) + conn, err := noAuthDialer.Dial("tcp", "google.com:80") + common.Must(err) + defer conn.Close() + + if err := testTCPConn2(conn, 1024, time.Second*5)(); err != nil { + t.Error(err) + } + } +} + +func TestUDPConnection(t *testing.T) { + udpServer := udp.Server{ + MsgProcessor: xor, + } + dest, err := udpServer.Start() + common.Must(err) + defer udpServer.Close() + + clientPort := udp.PickPort() + clientConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_UDP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + servers, err := InitializeServerConfigs(clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + if err := testUDPConn(clientPort, 1024, time.Second*5)(); err != nil { + t.Error(err) + } + + time.Sleep(20 * time.Second) + + if err := testUDPConn(clientPort, 1024, time.Second*5)(); err != nil { + t.Error(err) + } +} + +func TestDomainSniffing(t *testing.T) { + sniffingPort := tcp.PickPort() + httpPort := tcp.PickPort() + serverConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + Tag: "snif", + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(sniffingPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + SniffingSettings: &proxyman.SniffingConfig{ + Enabled: true, + DestinationOverride: []string{"tls"}, + }, + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: 443, + Networks: []net.Network{net.Network_TCP}, + }), + }, + { + Tag: "http", + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(httpPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&v2http.ServerConfig{}), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + Tag: "redir", + ProxySettings: serial.ToTypedMessage(&freedom.Config{ + DestinationOverride: &freedom.DestinationOverride{ + Server: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(sniffingPort), + }, + }, + }), + }, + { + Tag: "direct", + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&router.Config{ + Rule: []*router.RoutingRule{ + { + TargetTag: &router.RoutingRule_Tag{ + Tag: "direct", + }, + InboundTag: []string{"snif"}, + }, { + TargetTag: &router.RoutingRule_Tag{ + Tag: "redir", + }, + InboundTag: []string{"http"}, + }, + }, + }), + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + } + + servers, err := InitializeServerConfigs(serverConfig) + common.Must(err) + defer CloseAllServers(servers) + + { + transport := &http.Transport{ + Proxy: func(req *http.Request) (*url.URL, error) { + return url.Parse("http://127.0.0.1:" + httpPort.String()) + }, + } + + client := &http.Client{ + Transport: transport, + } + + resp, err := client.Get("https://www.github.com/") + common.Must(err) + if resp.StatusCode != 200 { + t.Error("unexpected status code: ", resp.StatusCode) + } + common.Must(resp.Write(io.Discard)) + } +} + +func TestDialXray(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + userID := protocol.NewID(uuid.New()) + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&dispatcher.Config{}), + serial.ToTypedMessage(&proxyman.InboundConfig{}), + serial.ToTypedMessage(&proxyman.OutboundConfig{}), + }, + Inbound: []*core.InboundHandlerConfig{}, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_AES128_GCM, + }, + }), + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig) + common.Must(err) + defer CloseAllServers(servers) + + client, err := core.New(clientConfig) + common.Must(err) + + conn, err := core.Dial(context.Background(), client, dest) + common.Must(err) + defer conn.Close() + + if err := testTCPConn2(conn, 1024, time.Second*5)(); err != nil { + t.Error(err) + } +} diff --git a/subproject/Xray-core-main/testing/scenarios/http_test.go b/subproject/Xray-core-main/testing/scenarios/http_test.go new file mode 100644 index 00000000..b9b112ff --- /dev/null +++ b/subproject/Xray-core-main/testing/scenarios/http_test.go @@ -0,0 +1,369 @@ +package scenarios + +import ( + "bytes" + "context" + "crypto/rand" + "io" + "net/http" + "net/url" + "strings" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/xtls/xray-core/app/proxyman" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/serial" + "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/proxy/freedom" + v2http "github.com/xtls/xray-core/proxy/http" + v2httptest "github.com/xtls/xray-core/testing/servers/http" + "github.com/xtls/xray-core/testing/servers/tcp" +) + +func TestHttpConformance(t *testing.T) { + httpServerPort := tcp.PickPort() + httpServer := &v2httptest.Server{ + Port: httpServerPort, + PathHandler: make(map[string]http.HandlerFunc), + } + _, err := httpServer.Start() + common.Must(err) + defer httpServer.Close() + + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&v2http.ServerConfig{}), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig) + common.Must(err) + defer CloseAllServers(servers) + + { + transport := &http.Transport{ + Proxy: func(req *http.Request) (*url.URL, error) { + return url.Parse("http://127.0.0.1:" + serverPort.String()) + }, + } + + client := &http.Client{ + Transport: transport, + } + + resp, err := client.Get("http://127.0.0.1:" + httpServerPort.String()) + common.Must(err) + if resp.StatusCode != 200 { + t.Fatal("status: ", resp.StatusCode) + } + + content, err := io.ReadAll(resp.Body) + common.Must(err) + if string(content) != "Home" { + t.Fatal("body: ", string(content)) + } + } +} + +func TestHttpError(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: func(msg []byte) []byte { + return []byte{} + }, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + time.AfterFunc(time.Second*2, func() { + tcpServer.ShouldClose = true + }) + + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&v2http.ServerConfig{}), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig) + common.Must(err) + defer CloseAllServers(servers) + + { + transport := &http.Transport{ + Proxy: func(req *http.Request) (*url.URL, error) { + return url.Parse("http://127.0.0.1:" + serverPort.String()) + }, + } + + client := &http.Client{ + Transport: transport, + } + + resp, err := client.Get("http://127.0.0.1:" + dest.Port.String()) + if resp != nil && resp.StatusCode != 503 || err != nil && !strings.Contains(err.Error(), "malformed HTTP status code") { + t.Error("should not receive http response", err) + } + } +} + +func TestHTTPConnectMethod(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&v2http.ServerConfig{}), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig) + common.Must(err) + defer CloseAllServers(servers) + + { + transport := &http.Transport{ + Proxy: func(req *http.Request) (*url.URL, error) { + return url.Parse("http://127.0.0.1:" + serverPort.String()) + }, + } + + client := &http.Client{ + Transport: transport, + } + + payload := make([]byte, 1024*64) + common.Must2(rand.Read(payload)) + + ctx := context.Background() + req, err := http.NewRequestWithContext(ctx, "Connect", "http://"+dest.NetAddr()+"/", bytes.NewReader(payload)) + req.Header.Set("X-a", "b") + req.Header.Set("X-b", "d") + common.Must(err) + + resp, err := client.Do(req) + common.Must(err) + if resp.StatusCode != 200 { + t.Fatal("status: ", resp.StatusCode) + } + + content := make([]byte, len(payload)) + common.Must2(io.ReadFull(resp.Body, content)) + if r := cmp.Diff(content, xor(payload)); r != "" { + t.Fatal(r) + } + } +} + +func TestHttpPost(t *testing.T) { + httpServerPort := tcp.PickPort() + httpServer := &v2httptest.Server{ + Port: httpServerPort, + PathHandler: map[string]http.HandlerFunc{ + "/testpost": func(w http.ResponseWriter, r *http.Request) { + payload, err := buf.ReadAllToBytes(r.Body) + r.Body.Close() + + if err != nil { + w.WriteHeader(500) + w.Write([]byte("Unable to read all payload")) + return + } + payload = xor(payload) + w.Write(payload) + }, + }, + } + + _, err := httpServer.Start() + common.Must(err) + defer httpServer.Close() + + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&v2http.ServerConfig{}), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig) + common.Must(err) + defer CloseAllServers(servers) + + { + transport := &http.Transport{ + Proxy: func(req *http.Request) (*url.URL, error) { + return url.Parse("http://127.0.0.1:" + serverPort.String()) + }, + } + + client := &http.Client{ + Transport: transport, + } + + payload := make([]byte, 1024*64) + common.Must2(rand.Read(payload)) + + resp, err := client.Post("http://127.0.0.1:"+httpServerPort.String()+"/testpost", "application/x-www-form-urlencoded", bytes.NewReader(payload)) + common.Must(err) + if resp.StatusCode != 200 { + t.Fatal("status: ", resp.StatusCode) + } + + content, err := io.ReadAll(resp.Body) + common.Must(err) + if r := cmp.Diff(content, xor(payload)); r != "" { + t.Fatal(r) + } + } +} + +func setProxyBasicAuth(req *http.Request, user, pass string) { + req.SetBasicAuth(user, pass) + req.Header.Set("Proxy-Authorization", req.Header.Get("Authorization")) + req.Header.Del("Authorization") +} + +func TestHttpBasicAuth(t *testing.T) { + httpServerPort := tcp.PickPort() + httpServer := &v2httptest.Server{ + Port: httpServerPort, + PathHandler: make(map[string]http.HandlerFunc), + } + _, err := httpServer.Start() + common.Must(err) + defer httpServer.Close() + + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&v2http.ServerConfig{ + Accounts: map[string]string{ + "a": "b", + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig) + common.Must(err) + defer CloseAllServers(servers) + + { + transport := &http.Transport{ + Proxy: func(req *http.Request) (*url.URL, error) { + return url.Parse("http://127.0.0.1:" + serverPort.String()) + }, + } + + client := &http.Client{ + Transport: transport, + } + + { + resp, err := client.Get("http://127.0.0.1:" + httpServerPort.String()) + common.Must(err) + if resp.StatusCode != 407 { + t.Fatal("status: ", resp.StatusCode) + } + } + + { + ctx := context.Background() + req, err := http.NewRequestWithContext(ctx, "GET", "http://127.0.0.1:"+httpServerPort.String(), nil) + common.Must(err) + + setProxyBasicAuth(req, "a", "c") + resp, err := client.Do(req) + common.Must(err) + if resp.StatusCode != 407 { + t.Fatal("status: ", resp.StatusCode) + } + } + + { + ctx := context.Background() + req, err := http.NewRequestWithContext(ctx, "GET", "http://127.0.0.1:"+httpServerPort.String(), nil) + common.Must(err) + + setProxyBasicAuth(req, "a", "b") + resp, err := client.Do(req) + common.Must(err) + if resp.StatusCode != 200 { + t.Fatal("status: ", resp.StatusCode) + } + + content, err := io.ReadAll(resp.Body) + common.Must(err) + if string(content) != "Home" { + t.Fatal("body: ", string(content)) + } + } + } +} diff --git a/subproject/Xray-core-main/testing/scenarios/main_test.go b/subproject/Xray-core-main/testing/scenarios/main_test.go new file mode 100644 index 00000000..269081c6 --- /dev/null +++ b/subproject/Xray-core-main/testing/scenarios/main_test.go @@ -0,0 +1,12 @@ +package scenarios + +import ( + "testing" +) + +func TestMain(m *testing.M) { + genTestBinaryPath() + defer testBinaryCleanFn() + + m.Run() +} diff --git a/subproject/Xray-core-main/testing/scenarios/metrics_test.go b/subproject/Xray-core-main/testing/scenarios/metrics_test.go new file mode 100644 index 00000000..eee2d942 --- /dev/null +++ b/subproject/Xray-core-main/testing/scenarios/metrics_test.go @@ -0,0 +1,107 @@ +package scenarios + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "testing" + + "github.com/xtls/xray-core/app/metrics" + "github.com/xtls/xray-core/app/proxyman" + "github.com/xtls/xray-core/app/router" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/serial" + "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/proxy/dokodemo" + "github.com/xtls/xray-core/proxy/freedom" + "github.com/xtls/xray-core/testing/servers/tcp" +) + +const expectedMessage = "goroutine profile: total" + +func TestMetrics(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + metricsPort := tcp.PickPort() + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&metrics.Config{ + Tag: "metrics_out", + }), + serial.ToTypedMessage(&router.Config{ + Rule: []*router.RoutingRule{ + { + InboundTag: []string{"metrics_in"}, + TargetTag: &router.RoutingRule_Tag{ + Tag: "metrics_out", + }, + }, + }, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + Tag: "metrics_in", + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(metricsPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + Tag: "default-outbound", + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + servers, err := InitializeServerConfigs(clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + resp, err := http.Get(fmt.Sprintf("http://127.0.0.1:%d/debug/pprof/goroutine?debug=1", metricsPort)) + common.Must(err) + if resp == nil { + t.Error("unexpected pprof nil response") + } + if resp.StatusCode != http.StatusOK { + t.Error("unexpected pprof status code") + } + body, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatal(err) + } + if string(body)[0:len(expectedMessage)] != expectedMessage { + t.Error("unexpected response body from pprof handler") + } + + resp2, err2 := http.Get(fmt.Sprintf("http://127.0.0.1:%d/debug/vars", metricsPort)) + common.Must(err2) + if resp2 == nil { + t.Error("unexpected expvars nil response") + } + if resp2.StatusCode != http.StatusOK { + t.Error("unexpected expvars status code") + } + body2, err2 := io.ReadAll(resp2.Body) + if err2 != nil { + t.Fatal(err2) + } + var json2 map[string]interface{} + if json.Unmarshal(body2, &json2) != nil { + t.Error("unexpected response body from expvars handler") + } +} diff --git a/subproject/Xray-core-main/testing/scenarios/policy_test.go b/subproject/Xray-core-main/testing/scenarios/policy_test.go new file mode 100644 index 00000000..c5d68a50 --- /dev/null +++ b/subproject/Xray-core-main/testing/scenarios/policy_test.go @@ -0,0 +1,250 @@ +package scenarios + +import ( + "io" + "testing" + "time" + + "github.com/xtls/xray-core/app/log" + "github.com/xtls/xray-core/app/policy" + "github.com/xtls/xray-core/app/proxyman" + "github.com/xtls/xray-core/common" + clog "github.com/xtls/xray-core/common/log" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/serial" + "github.com/xtls/xray-core/common/uuid" + "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/proxy/dokodemo" + "github.com/xtls/xray-core/proxy/freedom" + "github.com/xtls/xray-core/proxy/vmess" + "github.com/xtls/xray-core/proxy/vmess/inbound" + "github.com/xtls/xray-core/proxy/vmess/outbound" + "github.com/xtls/xray-core/testing/servers/tcp" + "golang.org/x/sync/errgroup" +) + +func startQuickClosingTCPServer() (net.Listener, error) { + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + return nil, err + } + go func() { + for { + conn, err := listener.Accept() + if err != nil { + break + } + b := make([]byte, 1024) + conn.Read(b) + conn.Close() + } + }() + return listener, nil +} + +func TestVMessClosing(t *testing.T) { + tcpServer, err := startQuickClosingTCPServer() + common.Must(err) + defer tcpServer.Close() + + dest := net.DestinationFromAddr(tcpServer.Addr()) + + userID := protocol.NewID(uuid.New()) + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&policy.Config{ + Level: map[uint32]*policy.Policy{ + 0: { + Timeout: &policy.Policy_Timeout{ + UplinkOnly: &policy.Second{Value: 0}, + DownlinkOnly: &policy.Second{Value: 0}, + }, + }, + }, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&policy.Config{ + Level: map[uint32]*policy.Policy{ + 0: { + Timeout: &policy.Policy_Timeout{ + UplinkOnly: &policy.Second{Value: 0}, + DownlinkOnly: &policy.Second{Value: 0}, + }, + }, + }, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_AES128_GCM, + }, + }), + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + if err := testTCPConn(clientPort, 1024, time.Second*2)(); err != io.EOF { + t.Error(err) + } +} + +func TestZeroBuffer(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + userID := protocol.NewID(uuid.New()) + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&policy.Config{ + Level: map[uint32]*policy.Policy{ + 0: { + Timeout: &policy.Policy_Timeout{ + UplinkOnly: &policy.Second{Value: 0}, + DownlinkOnly: &policy.Second{Value: 0}, + }, + Buffer: &policy.Policy_Buffer{ + Connection: 0, + }, + }, + }, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_AES128_GCM, + }, + }), + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + var errg errgroup.Group + for range 3 { + errg.Go(testTCPConn(clientPort, 10240*1024, time.Second*20)) + } + if err := errg.Wait(); err != nil { + t.Error(err) + } +} diff --git a/subproject/Xray-core-main/testing/scenarios/reverse_test.go b/subproject/Xray-core-main/testing/scenarios/reverse_test.go new file mode 100644 index 00000000..1e41d2a7 --- /dev/null +++ b/subproject/Xray-core-main/testing/scenarios/reverse_test.go @@ -0,0 +1,374 @@ +package scenarios + +import ( + "testing" + "time" + + "github.com/xtls/xray-core/app/log" + "github.com/xtls/xray-core/app/policy" + "github.com/xtls/xray-core/app/proxyman" + "github.com/xtls/xray-core/app/reverse" + "github.com/xtls/xray-core/app/router" + "github.com/xtls/xray-core/common" + clog "github.com/xtls/xray-core/common/log" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/serial" + "github.com/xtls/xray-core/common/uuid" + core "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/proxy/blackhole" + "github.com/xtls/xray-core/proxy/dokodemo" + "github.com/xtls/xray-core/proxy/freedom" + "github.com/xtls/xray-core/proxy/vmess" + "github.com/xtls/xray-core/proxy/vmess/inbound" + "github.com/xtls/xray-core/proxy/vmess/outbound" + "github.com/xtls/xray-core/testing/servers/tcp" + "golang.org/x/sync/errgroup" +) + +func TestReverseProxy(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + + defer tcpServer.Close() + + userID := protocol.NewID(uuid.New()) + externalPort := tcp.PickPort() + reversePort := tcp.PickPort() + + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&reverse.Config{ + PortalConfig: []*reverse.PortalConfig{ + { + Tag: "portal", + Domain: "test.example.com", + }, + }, + }), + serial.ToTypedMessage(&router.Config{ + Rule: []*router.RoutingRule{ + { + Domain: []*router.Domain{ + {Type: router.Domain_Full, Value: "test.example.com"}, + }, + TargetTag: &router.RoutingRule_Tag{ + Tag: "portal", + }, + }, + { + InboundTag: []string{"external"}, + TargetTag: &router.RoutingRule_Tag{ + Tag: "portal", + }, + }, + }, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + Tag: "external", + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(externalPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(reversePort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&blackhole.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&reverse.Config{ + BridgeConfig: []*reverse.BridgeConfig{ + { + Tag: "bridge", + Domain: "test.example.com", + }, + }, + }), + serial.ToTypedMessage(&router.Config{ + Rule: []*router.RoutingRule{ + { + Domain: []*router.Domain{ + {Type: router.Domain_Full, Value: "test.example.com"}, + }, + TargetTag: &router.RoutingRule_Tag{ + Tag: "reverse", + }, + }, + { + InboundTag: []string{"bridge"}, + TargetTag: &router.RoutingRule_Tag{ + Tag: "freedom", + }, + }, + }, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + Tag: "freedom", + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + { + Tag: "reverse", + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(reversePort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_AES128_GCM, + }, + }), + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + + defer CloseAllServers(servers) + + var errg errgroup.Group + for range 32 { + errg.Go(testTCPConn(externalPort, 10240*1024, time.Second*40)) + } + + if err := errg.Wait(); err != nil { + t.Fatal(err) + } +} + +func TestReverseProxyLongRunning(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + + defer tcpServer.Close() + + userID := protocol.NewID(uuid.New()) + externalPort := tcp.PickPort() + reversePort := tcp.PickPort() + + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Warning, + ErrorLogType: log.LogType_Console, + }), + serial.ToTypedMessage(&policy.Config{ + Level: map[uint32]*policy.Policy{ + 0: { + Timeout: &policy.Policy_Timeout{ + UplinkOnly: &policy.Second{Value: 0}, + DownlinkOnly: &policy.Second{Value: 0}, + }, + }, + }, + }), + serial.ToTypedMessage(&reverse.Config{ + PortalConfig: []*reverse.PortalConfig{ + { + Tag: "portal", + Domain: "test.example.com", + }, + }, + }), + serial.ToTypedMessage(&router.Config{ + Rule: []*router.RoutingRule{ + { + Domain: []*router.Domain{ + {Type: router.Domain_Full, Value: "test.example.com"}, + }, + TargetTag: &router.RoutingRule_Tag{ + Tag: "portal", + }, + }, + { + InboundTag: []string{"external"}, + TargetTag: &router.RoutingRule_Tag{ + Tag: "portal", + }, + }, + }, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + Tag: "external", + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(externalPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(reversePort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&blackhole.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Warning, + ErrorLogType: log.LogType_Console, + }), + serial.ToTypedMessage(&policy.Config{ + Level: map[uint32]*policy.Policy{ + 0: { + Timeout: &policy.Policy_Timeout{ + UplinkOnly: &policy.Second{Value: 0}, + DownlinkOnly: &policy.Second{Value: 0}, + }, + }, + }, + }), + serial.ToTypedMessage(&reverse.Config{ + BridgeConfig: []*reverse.BridgeConfig{ + { + Tag: "bridge", + Domain: "test.example.com", + }, + }, + }), + serial.ToTypedMessage(&router.Config{ + Rule: []*router.RoutingRule{ + { + Domain: []*router.Domain{ + {Type: router.Domain_Full, Value: "test.example.com"}, + }, + TargetTag: &router.RoutingRule_Tag{ + Tag: "reverse", + }, + }, + { + InboundTag: []string{"bridge"}, + TargetTag: &router.RoutingRule_Tag{ + Tag: "freedom", + }, + }, + }, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + Tag: "freedom", + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + { + Tag: "reverse", + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(reversePort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_AES128_GCM, + }, + }), + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + + defer CloseAllServers(servers) + + for range 4096 { + if err := testTCPConn(externalPort, 1024, time.Second*20)(); err != nil { + t.Error(err) + } + } +} diff --git a/subproject/Xray-core-main/testing/scenarios/shadowsocks_2022_test.go b/subproject/Xray-core-main/testing/scenarios/shadowsocks_2022_test.go new file mode 100644 index 00000000..7282eff3 --- /dev/null +++ b/subproject/Xray-core-main/testing/scenarios/shadowsocks_2022_test.go @@ -0,0 +1,217 @@ +package scenarios + +import ( + "crypto/rand" + "encoding/base64" + "testing" + "time" + + "github.com/sagernet/sing-shadowsocks/shadowaead_2022" + "github.com/xtls/xray-core/app/log" + "github.com/xtls/xray-core/app/proxyman" + "github.com/xtls/xray-core/common" + clog "github.com/xtls/xray-core/common/log" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/serial" + "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/proxy/dokodemo" + "github.com/xtls/xray-core/proxy/freedom" + "github.com/xtls/xray-core/proxy/shadowsocks_2022" + "github.com/xtls/xray-core/testing/servers/tcp" + "github.com/xtls/xray-core/testing/servers/udp" + "golang.org/x/sync/errgroup" +) + +func TestShadowsocks2022Tcp(t *testing.T) { + for _, method := range shadowaead_2022.List { + password := make([]byte, 32) + rand.Read(password) + t.Run(method, func(t *testing.T) { + testShadowsocks2022Tcp(t, method, base64.StdEncoding.EncodeToString(password)) + }) + } +} + +func TestShadowsocks2022UdpAES128(t *testing.T) { + password := make([]byte, 32) + rand.Read(password) + testShadowsocks2022Udp(t, shadowaead_2022.List[0], base64.StdEncoding.EncodeToString(password)) +} + +func TestShadowsocks2022UdpAES256(t *testing.T) { + password := make([]byte, 32) + rand.Read(password) + testShadowsocks2022Udp(t, shadowaead_2022.List[1], base64.StdEncoding.EncodeToString(password)) +} + +func TestShadowsocks2022UdpChacha(t *testing.T) { + password := make([]byte, 32) + rand.Read(password) + testShadowsocks2022Udp(t, shadowaead_2022.List[2], base64.StdEncoding.EncodeToString(password)) +} + +func testShadowsocks2022Tcp(t *testing.T, method string, password string) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&shadowsocks_2022.ServerConfig{ + Method: method, + Key: password, + Network: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&shadowsocks_2022.ClientConfig{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + Method: method, + Key: password, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + var errGroup errgroup.Group + for range 3 { + errGroup.Go(testTCPConn(clientPort, 10240*1024, time.Second*20)) + } + + if err := errGroup.Wait(); err != nil { + t.Error(err) + } +} + +func testShadowsocks2022Udp(t *testing.T, method string, password string) { + udpServer := udp.Server{ + MsgProcessor: xor, + } + udpDest, err := udpServer.Start() + common.Must(err) + defer udpServer.Close() + + serverPort := udp.PickPort() + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&shadowsocks_2022.ServerConfig{ + Method: method, + Key: password, + Network: []net.Network{net.Network_UDP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + udpClientPort := udp.PickPort() + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(udpClientPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(udpDest.Address), + Port: uint32(udpDest.Port), + Networks: []net.Network{net.Network_UDP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&shadowsocks_2022.ClientConfig{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + Method: method, + Key: password, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + var errGroup errgroup.Group + for range 3 { + errGroup.Go(testUDPConn(udpClientPort, 1024, time.Second*5)) + } + + if err := errGroup.Wait(); err != nil { + t.Error(err) + } +} diff --git a/subproject/Xray-core-main/testing/scenarios/shadowsocks_test.go b/subproject/Xray-core-main/testing/scenarios/shadowsocks_test.go new file mode 100644 index 00000000..2fdd6204 --- /dev/null +++ b/subproject/Xray-core-main/testing/scenarios/shadowsocks_test.go @@ -0,0 +1,467 @@ +package scenarios + +import ( + "testing" + "time" + + "github.com/xtls/xray-core/app/log" + "github.com/xtls/xray-core/app/proxyman" + "github.com/xtls/xray-core/common" + clog "github.com/xtls/xray-core/common/log" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/serial" + "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/proxy/dokodemo" + "github.com/xtls/xray-core/proxy/freedom" + "github.com/xtls/xray-core/proxy/shadowsocks" + "github.com/xtls/xray-core/testing/servers/tcp" + "github.com/xtls/xray-core/testing/servers/udp" + "golang.org/x/sync/errgroup" +) + +func TestShadowsocksChaCha20Poly1305TCP(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + account := serial.ToTypedMessage(&shadowsocks.Account{ + Password: "shadowsocks-password", + CipherType: shadowsocks.CipherType_CHACHA20_POLY1305, + }) + + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&shadowsocks.ServerConfig{ + Users: []*protocol.User{{ + Account: account, + Level: 1, + }}, + Network: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&shadowsocks.ClientConfig{ + Server: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: account, + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + var errGroup errgroup.Group + for range 3 { + errGroup.Go(testTCPConn(clientPort, 10240*1024, time.Second*20)) + } + if err := errGroup.Wait(); err != nil { + t.Error(err) + } +} + +func TestShadowsocksAES256GCMTCP(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + account := serial.ToTypedMessage(&shadowsocks.Account{ + Password: "shadowsocks-password", + CipherType: shadowsocks.CipherType_AES_256_GCM, + }) + + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&shadowsocks.ServerConfig{ + Users: []*protocol.User{{ + Account: account, + Level: 1, + }}, + Network: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&shadowsocks.ClientConfig{ + Server: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: account, + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + var errGroup errgroup.Group + for range 3 { + errGroup.Go(testTCPConn(clientPort, 10240*1024, time.Second*20)) + } + + if err := errGroup.Wait(); err != nil { + t.Error(err) + } +} + +func TestShadowsocksAES128GCMUDP(t *testing.T) { + udpServer := udp.Server{ + MsgProcessor: xor, + } + dest, err := udpServer.Start() + common.Must(err) + defer udpServer.Close() + + account := serial.ToTypedMessage(&shadowsocks.Account{ + Password: "shadowsocks-password", + CipherType: shadowsocks.CipherType_AES_128_GCM, + }) + + serverPort := udp.PickPort() + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&shadowsocks.ServerConfig{ + Users: []*protocol.User{{ + Account: account, + Level: 1, + }}, + Network: []net.Network{net.Network_UDP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := udp.PickPort() + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_UDP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&shadowsocks.ClientConfig{ + Server: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: account, + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + var errGroup errgroup.Group + for range 3 { + errGroup.Go(testUDPConn(clientPort, 1024, time.Second*5)) + } + if err := errGroup.Wait(); err != nil { + t.Error(err) + } +} + +func TestShadowsocksAES128GCMUDPMux(t *testing.T) { + udpServer := udp.Server{ + MsgProcessor: xor, + } + dest, err := udpServer.Start() + common.Must(err) + defer udpServer.Close() + + account := serial.ToTypedMessage(&shadowsocks.Account{ + Password: "shadowsocks-password", + CipherType: shadowsocks.CipherType_AES_128_GCM, + }) + + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&shadowsocks.ServerConfig{ + Users: []*protocol.User{{ + Account: account, + Level: 1, + }}, + Network: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := udp.PickPort() + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_UDP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{ + MultiplexSettings: &proxyman.MultiplexingConfig{ + Enabled: true, + Concurrency: 8, + }, + }), + ProxySettings: serial.ToTypedMessage(&shadowsocks.ClientConfig{ + Server: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: account, + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + var errGroup errgroup.Group + for range 3 { + errGroup.Go(testUDPConn(clientPort, 1024, time.Second*5)) + } + if err := errGroup.Wait(); err != nil { + t.Error(err) + } +} + +func TestShadowsocksNone(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + + defer tcpServer.Close() + + account := serial.ToTypedMessage(&shadowsocks.Account{ + Password: "shadowsocks-password", + CipherType: shadowsocks.CipherType_NONE, + }) + + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&shadowsocks.ServerConfig{ + Users: []*protocol.User{{ + Account: account, + Level: 1, + }}, + Network: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&shadowsocks.ClientConfig{ + Server: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: account, + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + + defer CloseAllServers(servers) + + var errGroup errgroup.Group + for range 3 { + errGroup.Go(testTCPConn(clientPort, 10240*1024, time.Second*20)) + } + + if err := errGroup.Wait(); err != nil { + t.Fatal(err) + } +} diff --git a/subproject/Xray-core-main/testing/scenarios/socks_test.go b/subproject/Xray-core-main/testing/scenarios/socks_test.go new file mode 100644 index 00000000..a139e5fe --- /dev/null +++ b/subproject/Xray-core-main/testing/scenarios/socks_test.go @@ -0,0 +1,481 @@ +package scenarios + +import ( + "testing" + "time" + + "github.com/xtls/xray-core/app/proxyman" + "github.com/xtls/xray-core/app/router" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/serial" + "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/proxy/blackhole" + "github.com/xtls/xray-core/proxy/dokodemo" + "github.com/xtls/xray-core/proxy/freedom" + "github.com/xtls/xray-core/proxy/http" + "github.com/xtls/xray-core/proxy/socks" + "github.com/xtls/xray-core/testing/servers/tcp" + "github.com/xtls/xray-core/testing/servers/udp" + xproxy "golang.org/x/net/proxy" + socks4 "h12.io/socks" +) + +func TestSocksBridgeTCP(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&socks.ServerConfig{ + AuthType: socks.AuthType_PASSWORD, + Accounts: map[string]string{ + "Test Account": "Test Password", + }, + Address: net.NewIPOrDomain(net.LocalHostIP), + UdpEnabled: false, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&socks.ClientConfig{ + Server: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&socks.Account{ + Username: "Test Account", + Password: "Test Password", + }), + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + if err := testTCPConn(clientPort, 1024, time.Second*2)(); err != nil { + t.Error(err) + } +} + +func TestSocksWithHttpRequest(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&socks.ServerConfig{ + AuthType: socks.AuthType_PASSWORD, + Accounts: map[string]string{ + "Test Account": "Test Password", + }, + Address: net.NewIPOrDomain(net.LocalHostIP), + UdpEnabled: false, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&http.ClientConfig{ + Server: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&http.Account{ + Username: "Test Account", + Password: "Test Password", + }), + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + if err := testTCPConn(clientPort, 1024, time.Second*2)(); err != nil { + t.Error(err) + } +} + +func TestSocksBridageUDP(t *testing.T) { + udpServer := udp.Server{ + MsgProcessor: xor, + } + dest, err := udpServer.Start() + common.Must(err) + defer udpServer.Close() + + retry := 1 + serverPort := tcp.PickPort() + for { + serverConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&socks.ServerConfig{ + AuthType: socks.AuthType_PASSWORD, + Accounts: map[string]string{ + "Test Account": "Test Password", + }, + Address: net.NewIPOrDomain(net.LocalHostIP), + UdpEnabled: true, + }), + }, + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort + 1)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_UDP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + server, _ := InitializeServerConfig(serverConfig) + if server != nil && WaitConnAvailableWithTest(t, testUDPConn(serverPort+1, 1024, time.Second*2)) { + defer CloseServer(server) + break + } + retry++ + if retry > 5 { + t.Fatal("All attempts failed to start server") + } + serverPort = tcp.PickPort() + } + + clientPort := udp.PickPort() + clientConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_UDP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&socks.ClientConfig{ + Server: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&socks.Account{ + Username: "Test Account", + Password: "Test Password", + }), + }, + }, + }), + }, + }, + } + + server, err := InitializeServerConfig(clientConfig) + common.Must(err) + defer CloseServer(server) + + if !WaitConnAvailableWithTest(t, testUDPConn(clientPort, 1024, time.Second*2)) { + t.Fail() + } +} + +func TestSocksBridageUDPWithRouting(t *testing.T) { + udpServer := udp.Server{ + MsgProcessor: xor, + } + dest, err := udpServer.Start() + common.Must(err) + defer udpServer.Close() + + retry := 1 + serverPort := tcp.PickPort() + for { + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&router.Config{ + Rule: []*router.RoutingRule{ + { + TargetTag: &router.RoutingRule_Tag{ + Tag: "out", + }, + InboundTag: []string{"socks", "dokodemo"}, + }, + }, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + Tag: "socks", + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&socks.ServerConfig{ + AuthType: socks.AuthType_NO_AUTH, + Address: net.NewIPOrDomain(net.LocalHostIP), + UdpEnabled: true, + }), + }, + { + Tag: "dokodemo", + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort + 1)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_UDP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&blackhole.Config{}), + }, + { + Tag: "out", + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + server, _ := InitializeServerConfig(serverConfig) + if server != nil && WaitConnAvailableWithTest(t, testUDPConn(serverPort+1, 1024, time.Second*2)) { + defer CloseServer(server) + break + } + retry++ + if retry > 5 { + t.Fatal("All attempts failed to start server") + } + serverPort = tcp.PickPort() + } + + clientPort := udp.PickPort() + clientConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_UDP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&socks.ClientConfig{ + Server: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + }, + }), + }, + }, + } + + server, err := InitializeServerConfig(clientConfig) + common.Must(err) + defer CloseServer(server) + + if !WaitConnAvailableWithTest(t, testUDPConn(clientPort, 1024, time.Second*2)) { + t.Fail() + } +} + +func TestSocksConformanceMod(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + authPort := tcp.PickPort() + noAuthPort := tcp.PickPort() + serverConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(authPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&socks.ServerConfig{ + AuthType: socks.AuthType_PASSWORD, + Accounts: map[string]string{ + "Test Account": "Test Password", + }, + Address: net.NewIPOrDomain(net.LocalHostIP), + UdpEnabled: false, + }), + }, + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(noAuthPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&socks.ServerConfig{ + AuthType: socks.AuthType_NO_AUTH, + Accounts: map[string]string{ + "Test Account": "Test Password", + }, + Address: net.NewIPOrDomain(net.LocalHostIP), + UdpEnabled: false, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig) + common.Must(err) + defer CloseAllServers(servers) + + { + noAuthDialer, err := xproxy.SOCKS5("tcp", net.TCPDestination(net.LocalHostIP, noAuthPort).NetAddr(), nil, xproxy.Direct) + common.Must(err) + conn, err := noAuthDialer.Dial("tcp", dest.NetAddr()) + common.Must(err) + defer conn.Close() + + if err := testTCPConn2(conn, 1024, time.Second*5)(); err != nil { + t.Error(err) + } + } + + { + authDialer, err := xproxy.SOCKS5("tcp", net.TCPDestination(net.LocalHostIP, authPort).NetAddr(), &xproxy.Auth{User: "Test Account", Password: "Test Password"}, xproxy.Direct) + common.Must(err) + conn, err := authDialer.Dial("tcp", dest.NetAddr()) + common.Must(err) + defer conn.Close() + + if err := testTCPConn2(conn, 1024, time.Second*5)(); err != nil { + t.Error(err) + } + } + + { + dialer := socks4.Dial("socks4://" + net.TCPDestination(net.LocalHostIP, noAuthPort).NetAddr()) + conn, err := dialer("tcp", dest.NetAddr()) + common.Must(err) + defer conn.Close() + + if err := testTCPConn2(conn, 1024, time.Second*5)(); err != nil { + t.Error(err) + } + } + + { + dialer := socks4.Dial("socks4://" + net.TCPDestination(net.LocalHostIP, noAuthPort).NetAddr()) + conn, err := dialer("tcp", net.TCPDestination(net.LocalHostIP, tcpServer.Port).NetAddr()) + common.Must(err) + defer conn.Close() + + if err := testTCPConn2(conn, 1024, time.Second*5)(); err != nil { + t.Error(err) + } + } +} diff --git a/subproject/Xray-core-main/testing/scenarios/tls_test.go b/subproject/Xray-core-main/testing/scenarios/tls_test.go new file mode 100644 index 00000000..1a2f9661 --- /dev/null +++ b/subproject/Xray-core-main/testing/scenarios/tls_test.go @@ -0,0 +1,1064 @@ +package scenarios + +import ( + "crypto/x509" + "runtime" + "testing" + "time" + + "github.com/xtls/xray-core/app/proxyman" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/protocol/tls/cert" + "github.com/xtls/xray-core/common/serial" + "github.com/xtls/xray-core/common/uuid" + core "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/proxy/dokodemo" + "github.com/xtls/xray-core/proxy/freedom" + "github.com/xtls/xray-core/proxy/vmess" + "github.com/xtls/xray-core/proxy/vmess/inbound" + "github.com/xtls/xray-core/proxy/vmess/outbound" + "github.com/xtls/xray-core/testing/servers/tcp" + "github.com/xtls/xray-core/testing/servers/udp" + "github.com/xtls/xray-core/transport/internet" + "github.com/xtls/xray-core/transport/internet/grpc" + "github.com/xtls/xray-core/transport/internet/tls" + "github.com/xtls/xray-core/transport/internet/websocket" + "golang.org/x/sync/errgroup" +) + +func TestSimpleTLSConnection(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + ct, ctHash := cert.MustGenerate(nil, cert.CommonName("localhost")) + + userID := protocol.NewID(uuid.New()) + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + StreamSettings: &internet.StreamConfig{ + SecurityType: serial.GetMessageType(&tls.Config{}), + SecuritySettings: []*serial.TypedMessage{ + serial.ToTypedMessage(&tls.Config{ + Certificate: []*tls.Certificate{tls.ParseCertificate(ct)}, + }), + }, + }, + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }), + SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{ + StreamSettings: &internet.StreamConfig{ + SecurityType: serial.GetMessageType(&tls.Config{}), + SecuritySettings: []*serial.TypedMessage{ + serial.ToTypedMessage(&tls.Config{ + PinnedPeerCertSha256: [][]byte{ctHash[:]}, + }), + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + if err := testTCPConn(clientPort, 1024, time.Second*20)(); err != nil { + t.Fatal(err) + } +} + +func TestAutoIssuingCertificate(t *testing.T) { + if runtime.GOOS == "windows" { + // Not supported on Windows yet. + return + } + + if runtime.GOARCH == "arm64" { + return + } + + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + caCert, err := cert.Generate(nil, cert.Authority(true), cert.KeyUsage(x509.KeyUsageDigitalSignature|x509.KeyUsageKeyEncipherment|x509.KeyUsageCertSign)) + common.Must(err) + certPEM, keyPEM := caCert.ToPEM() + + userID := protocol.NewID(uuid.New()) + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + StreamSettings: &internet.StreamConfig{ + SecurityType: serial.GetMessageType(&tls.Config{}), + SecuritySettings: []*serial.TypedMessage{ + serial.ToTypedMessage(&tls.Config{ + Certificate: []*tls.Certificate{{ + Certificate: certPEM, + Key: keyPEM, + Usage: tls.Certificate_AUTHORITY_ISSUE, + }}, + }), + }, + }, + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }), + SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{ + StreamSettings: &internet.StreamConfig{ + SecurityType: serial.GetMessageType(&tls.Config{}), + SecuritySettings: []*serial.TypedMessage{ + serial.ToTypedMessage(&tls.Config{ + ServerName: "example.com", + Certificate: []*tls.Certificate{{ + Certificate: certPEM, + Usage: tls.Certificate_AUTHORITY_VERIFY, + }}, + }), + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + for range 3 { + if err := testTCPConn(clientPort, 1024, time.Second*20)(); err != nil { + t.Error(err) + } + } +} + +func TestTLSOverKCP(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + ct, ctHash := cert.MustGenerate(nil, cert.CommonName("localhost")) + + userID := protocol.NewID(uuid.New()) + serverPort := udp.PickPort() + serverConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + StreamSettings: &internet.StreamConfig{ + ProtocolName: "mkcp", + SecurityType: serial.GetMessageType(&tls.Config{}), + SecuritySettings: []*serial.TypedMessage{ + serial.ToTypedMessage(&tls.Config{ + Certificate: []*tls.Certificate{tls.ParseCertificate(ct)}, + }), + }, + }, + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }), + SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{ + StreamSettings: &internet.StreamConfig{ + ProtocolName: "mkcp", + SecurityType: serial.GetMessageType(&tls.Config{}), + SecuritySettings: []*serial.TypedMessage{ + serial.ToTypedMessage(&tls.Config{ + PinnedPeerCertSha256: [][]byte{ctHash[:]}, + }), + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + if err := testTCPConn(clientPort, 1024, time.Second*20)(); err != nil { + t.Error(err) + } +} + +func TestTLSOverWebSocket(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + ct, ctHash := cert.MustGenerate(nil, cert.CommonName("localhost")) + + userID := protocol.NewID(uuid.New()) + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + StreamSettings: &internet.StreamConfig{ + ProtocolName: "websocket", + SecurityType: serial.GetMessageType(&tls.Config{}), + SecuritySettings: []*serial.TypedMessage{ + serial.ToTypedMessage(&tls.Config{ + Certificate: []*tls.Certificate{tls.ParseCertificate(ct)}, + }), + }, + }, + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }), + SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{ + StreamSettings: &internet.StreamConfig{ + ProtocolName: "websocket", + TransportSettings: []*internet.TransportConfig{ + { + ProtocolName: "websocket", + Settings: serial.ToTypedMessage(&websocket.Config{}), + }, + }, + SecurityType: serial.GetMessageType(&tls.Config{}), + SecuritySettings: []*serial.TypedMessage{ + serial.ToTypedMessage(&tls.Config{ + PinnedPeerCertSha256: [][]byte{ctHash[:]}, + }), + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + var errg errgroup.Group + for range 3 { + errg.Go(testTCPConn(clientPort, 10240*1024, time.Second*20)) + } + if err := errg.Wait(); err != nil { + t.Error(err) + } +} + +func TestGRPC(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + ct, ctHash := cert.MustGenerate(nil, cert.CommonName("localhost")) + + userID := protocol.NewID(uuid.New()) + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + StreamSettings: &internet.StreamConfig{ + ProtocolName: "grpc", + TransportSettings: []*internet.TransportConfig{ + { + ProtocolName: "grpc", + Settings: serial.ToTypedMessage(&grpc.Config{ServiceName: "🍉"}), + }, + }, + SecurityType: serial.GetMessageType(&tls.Config{}), + SecuritySettings: []*serial.TypedMessage{ + serial.ToTypedMessage(&tls.Config{ + Certificate: []*tls.Certificate{tls.ParseCertificate(ct)}, + }), + }, + }, + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }), + SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{ + StreamSettings: &internet.StreamConfig{ + ProtocolName: "grpc", + TransportSettings: []*internet.TransportConfig{ + { + ProtocolName: "grpc", + Settings: serial.ToTypedMessage(&grpc.Config{ServiceName: "🍉"}), + }, + }, + SecurityType: serial.GetMessageType(&tls.Config{}), + SecuritySettings: []*serial.TypedMessage{ + serial.ToTypedMessage(&tls.Config{ + PinnedPeerCertSha256: [][]byte{ctHash[:]}, + }), + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + var errg errgroup.Group + for range 3 { + errg.Go(testTCPConn(clientPort, 1024*10240, time.Second*40)) + } + if err := errg.Wait(); err != nil { + t.Error(err) + } +} + +func TestGRPCMultiMode(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + ct, ctHash := cert.MustGenerate(nil, cert.CommonName("localhost")) + + userID := protocol.NewID(uuid.New()) + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + StreamSettings: &internet.StreamConfig{ + ProtocolName: "grpc", + TransportSettings: []*internet.TransportConfig{ + { + ProtocolName: "grpc", + Settings: serial.ToTypedMessage(&grpc.Config{ServiceName: "🍉"}), + }, + }, + SecurityType: serial.GetMessageType(&tls.Config{}), + SecuritySettings: []*serial.TypedMessage{ + serial.ToTypedMessage(&tls.Config{ + Certificate: []*tls.Certificate{tls.ParseCertificate(ct)}, + }), + }, + }, + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }), + SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{ + StreamSettings: &internet.StreamConfig{ + ProtocolName: "grpc", + TransportSettings: []*internet.TransportConfig{ + { + ProtocolName: "grpc", + Settings: serial.ToTypedMessage(&grpc.Config{ServiceName: "🍉", MultiMode: true}), + }, + }, + SecurityType: serial.GetMessageType(&tls.Config{}), + SecuritySettings: []*serial.TypedMessage{ + serial.ToTypedMessage(&tls.Config{ + PinnedPeerCertSha256: [][]byte{ctHash[:]}, + }), + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + var errg errgroup.Group + for range 3 { + errg.Go(testTCPConn(clientPort, 1024*10240, time.Second*40)) + } + if err := errg.Wait(); err != nil { + t.Error(err) + } +} + +func TestSimpleTLSConnectionPinned(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + certificateDer, _ := cert.MustGenerate(nil) + certificate := tls.ParseCertificate(certificateDer) + certHash := tls.GenerateCertHash(certificateDer.Certificate) + userID := protocol.NewID(uuid.New()) + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + StreamSettings: &internet.StreamConfig{ + SecurityType: serial.GetMessageType(&tls.Config{}), + SecuritySettings: []*serial.TypedMessage{ + serial.ToTypedMessage(&tls.Config{ + Certificate: []*tls.Certificate{certificate}, + }), + }, + }, + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }), + SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{ + StreamSettings: &internet.StreamConfig{ + SecurityType: serial.GetMessageType(&tls.Config{}), + SecuritySettings: []*serial.TypedMessage{ + serial.ToTypedMessage(&tls.Config{ + PinnedPeerCertSha256: [][]byte{certHash}, + }), + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + if err := testTCPConn(clientPort, 1024, time.Second*20)(); err != nil { + t.Fatal(err) + } +} + +func TestSimpleTLSConnectionPinnedWrongCert(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + certificateDer, _ := cert.MustGenerate(nil) + certificate := tls.ParseCertificate(certificateDer) + certHash := tls.GenerateCertHash(certificateDer.Certificate) + certHash[1] += 1 + userID := protocol.NewID(uuid.New()) + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + StreamSettings: &internet.StreamConfig{ + SecurityType: serial.GetMessageType(&tls.Config{}), + SecuritySettings: []*serial.TypedMessage{ + serial.ToTypedMessage(&tls.Config{ + Certificate: []*tls.Certificate{certificate}, + }), + }, + }, + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }), + SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{ + StreamSettings: &internet.StreamConfig{ + SecurityType: serial.GetMessageType(&tls.Config{}), + SecuritySettings: []*serial.TypedMessage{ + serial.ToTypedMessage(&tls.Config{ + PinnedPeerCertSha256: [][]byte{certHash}, + }), + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + if err := testTCPConn(clientPort, 1024, time.Second*20)(); err == nil { + t.Fatal(err) + } +} + +func TestUTLSConnectionPinned(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + certificateDer, _ := cert.MustGenerate(nil) + certificate := tls.ParseCertificate(certificateDer) + certHash := tls.GenerateCertHash(certificateDer.Certificate) + userID := protocol.NewID(uuid.New()) + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + StreamSettings: &internet.StreamConfig{ + SecurityType: serial.GetMessageType(&tls.Config{}), + SecuritySettings: []*serial.TypedMessage{ + serial.ToTypedMessage(&tls.Config{ + Certificate: []*tls.Certificate{certificate}, + }), + }, + }, + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }), + SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{ + StreamSettings: &internet.StreamConfig{ + SecurityType: serial.GetMessageType(&tls.Config{}), + SecuritySettings: []*serial.TypedMessage{ + serial.ToTypedMessage(&tls.Config{ + Fingerprint: "random", + PinnedPeerCertSha256: [][]byte{certHash}, + }), + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + if err := testTCPConn(clientPort, 1024, time.Second*20)(); err != nil { + t.Fatal(err) + } +} + +func TestUTLSConnectionPinnedWrongCert(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + certificateDer, _ := cert.MustGenerate(nil) + certificate := tls.ParseCertificate(certificateDer) + certHash := tls.GenerateCertHash(certificateDer.Certificate) + certHash[1] += 1 + userID := protocol.NewID(uuid.New()) + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + StreamSettings: &internet.StreamConfig{ + SecurityType: serial.GetMessageType(&tls.Config{}), + SecuritySettings: []*serial.TypedMessage{ + serial.ToTypedMessage(&tls.Config{ + Certificate: []*tls.Certificate{certificate}, + }), + }, + }, + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }), + SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{ + StreamSettings: &internet.StreamConfig{ + SecurityType: serial.GetMessageType(&tls.Config{}), + SecuritySettings: []*serial.TypedMessage{ + serial.ToTypedMessage(&tls.Config{ + Fingerprint: "random", + PinnedPeerCertSha256: [][]byte{certHash}, + }), + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + if err := testTCPConn(clientPort, 1024, time.Second*20)(); err == nil { + t.Fatal(err) + } +} diff --git a/subproject/Xray-core-main/testing/scenarios/transport_test.go b/subproject/Xray-core-main/testing/scenarios/transport_test.go new file mode 100644 index 00000000..80fd13db --- /dev/null +++ b/subproject/Xray-core-main/testing/scenarios/transport_test.go @@ -0,0 +1,121 @@ +package scenarios + +import ( + "testing" + "time" + + "github.com/xtls/xray-core/app/proxyman" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/serial" + "github.com/xtls/xray-core/common/uuid" + "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/proxy/dokodemo" + "github.com/xtls/xray-core/proxy/freedom" + "github.com/xtls/xray-core/proxy/vmess" + "github.com/xtls/xray-core/proxy/vmess/inbound" + "github.com/xtls/xray-core/proxy/vmess/outbound" + "github.com/xtls/xray-core/testing/servers/tcp" + "github.com/xtls/xray-core/transport/internet" + "github.com/xtls/xray-core/transport/internet/headers/http" + tcptransport "github.com/xtls/xray-core/transport/internet/tcp" +) + +func TestHTTPConnectionHeader(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + userID := protocol.NewID(uuid.New()) + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + StreamSettings: &internet.StreamConfig{ + TransportSettings: []*internet.TransportConfig{ + { + ProtocolName: "tcp", + Settings: serial.ToTypedMessage(&tcptransport.Config{ + HeaderSettings: serial.ToTypedMessage(&http.Config{}), + }), + }, + }, + }, + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }), + SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{ + StreamSettings: &internet.StreamConfig{ + TransportSettings: []*internet.TransportConfig{ + { + ProtocolName: "tcp", + Settings: serial.ToTypedMessage(&tcptransport.Config{ + HeaderSettings: serial.ToTypedMessage(&http.Config{}), + }), + }, + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + if err := testTCPConn(clientPort, 1024, time.Second*2)(); err != nil { + t.Error(err) + } +} diff --git a/subproject/Xray-core-main/testing/scenarios/vless_test.go b/subproject/Xray-core-main/testing/scenarios/vless_test.go new file mode 100644 index 00000000..cdc75c59 --- /dev/null +++ b/subproject/Xray-core-main/testing/scenarios/vless_test.go @@ -0,0 +1,649 @@ +package scenarios + +import ( + "encoding/base64" + "encoding/hex" + "sync" + "testing" + "time" + + "github.com/xtls/xray-core/app/log" + "github.com/xtls/xray-core/app/proxyman" + "github.com/xtls/xray-core/common" + clog "github.com/xtls/xray-core/common/log" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/protocol/tls/cert" + "github.com/xtls/xray-core/common/serial" + "github.com/xtls/xray-core/common/uuid" + core "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/proxy/dokodemo" + "github.com/xtls/xray-core/proxy/freedom" + "github.com/xtls/xray-core/proxy/vless" + "github.com/xtls/xray-core/proxy/vless/inbound" + "github.com/xtls/xray-core/proxy/vless/outbound" + "github.com/xtls/xray-core/testing/servers/tcp" + "github.com/xtls/xray-core/transport/internet" + "github.com/xtls/xray-core/transport/internet/reality" + transtcp "github.com/xtls/xray-core/transport/internet/tcp" + "github.com/xtls/xray-core/transport/internet/tls" + "golang.org/x/sync/errgroup" +) + +func TestVless(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + userID := protocol.NewID(uuid.New()) + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + Clients: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vless.Account{ + Id: userID.String(), + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Vnext: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vless.Account{ + Id: userID.String(), + }), + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + var errg errgroup.Group + for range 3 { + errg.Go(testTCPConn(clientPort, 1024*1024, time.Second*30)) + } + if err := errg.Wait(); err != nil { + t.Error(err) + } +} + +func TestVlessTls(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + ct, ctHash := cert.MustGenerate(nil, cert.CommonName("localhost")) + + userID := protocol.NewID(uuid.New()) + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + StreamSettings: &internet.StreamConfig{ + ProtocolName: "tcp", + SecurityType: serial.GetMessageType(&tls.Config{}), + SecuritySettings: []*serial.TypedMessage{ + serial.ToTypedMessage(&tls.Config{ + Certificate: []*tls.Certificate{tls.ParseCertificate(ct)}, + }), + }, + }, + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + Clients: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vless.Account{ + Id: userID.String(), + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Vnext: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vless.Account{ + Id: userID.String(), + }), + }, + }, + }), + SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{ + StreamSettings: &internet.StreamConfig{ + ProtocolName: "tcp", + TransportSettings: []*internet.TransportConfig{ + { + ProtocolName: "tcp", + Settings: serial.ToTypedMessage(&transtcp.Config{}), + }, + }, + SecurityType: serial.GetMessageType(&tls.Config{}), + SecuritySettings: []*serial.TypedMessage{ + serial.ToTypedMessage(&tls.Config{ + PinnedPeerCertSha256: [][]byte{ctHash[:]}, + }), + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + var errg errgroup.Group + for range 3 { + errg.Go(testTCPConn(clientPort, 1024*1024, time.Second*30)) + } + if err := errg.Wait(); err != nil { + t.Error(err) + } +} + +func TestVlessXtlsVision(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + ct, ctHash := cert.MustGenerate(nil, cert.CommonName("localhost")) + + userID := protocol.NewID(uuid.New()) + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + StreamSettings: &internet.StreamConfig{ + ProtocolName: "tcp", + SecurityType: serial.GetMessageType(&tls.Config{}), + SecuritySettings: []*serial.TypedMessage{ + serial.ToTypedMessage(&tls.Config{ + Certificate: []*tls.Certificate{tls.ParseCertificate(ct)}, + }), + }, + }, + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + Clients: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vless.Account{ + Id: userID.String(), + Flow: vless.XRV, + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Vnext: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vless.Account{ + Id: userID.String(), + Flow: vless.XRV, + }), + }, + }, + }), + SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{ + StreamSettings: &internet.StreamConfig{ + ProtocolName: "tcp", + TransportSettings: []*internet.TransportConfig{ + { + ProtocolName: "tcp", + Settings: serial.ToTypedMessage(&transtcp.Config{}), + }, + }, + SecurityType: serial.GetMessageType(&tls.Config{}), + SecuritySettings: []*serial.TypedMessage{ + serial.ToTypedMessage(&tls.Config{ + PinnedPeerCertSha256: [][]byte{ctHash[:]}, + }), + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + var errg errgroup.Group + for range 3 { + errg.Go(testTCPConn(clientPort, 1024*1024, time.Second*30)) + } + if err := errg.Wait(); err != nil { + t.Error(err) + } +} + +func TestVlessXtlsVisionReality(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + userID := protocol.NewID(uuid.New()) + serverPort := tcp.PickPort() + privateKey, _ := base64.RawURLEncoding.DecodeString("aGSYystUbf59_9_6LKRxD27rmSW_-2_nyd9YG_Gwbks") + publicKey, _ := base64.RawURLEncoding.DecodeString("E59WjnvZcQMu7tR7_BgyhycuEdBS-CtKxfImRCdAvFM") + shortIds := make([][]byte, 1) + shortIds[0] = make([]byte, 8) + hex.Decode(shortIds[0], []byte("0123456789abcdef")) + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + StreamSettings: &internet.StreamConfig{ + ProtocolName: "tcp", + SecurityType: serial.GetMessageType(&reality.Config{}), + SecuritySettings: []*serial.TypedMessage{ + serial.ToTypedMessage(&reality.Config{ + Show: true, + Dest: "www.google.com:443", // use google for now, may fail in some region + ServerNames: []string{"www.google.com"}, + PrivateKey: privateKey, + ShortIds: shortIds, + Type: "tcp", + }), + }, + }, + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + Clients: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vless.Account{ + Id: userID.String(), + Flow: vless.XRV, + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Vnext: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vless.Account{ + Id: userID.String(), + Flow: vless.XRV, + }), + }, + }, + }), + SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{ + StreamSettings: &internet.StreamConfig{ + ProtocolName: "tcp", + TransportSettings: []*internet.TransportConfig{ + { + ProtocolName: "tcp", + Settings: serial.ToTypedMessage(&transtcp.Config{}), + }, + }, + SecurityType: serial.GetMessageType(&reality.Config{}), + SecuritySettings: []*serial.TypedMessage{ + serial.ToTypedMessage(&reality.Config{ + Show: true, + Fingerprint: "chrome", + ServerName: "www.google.com", + PublicKey: publicKey, + ShortId: shortIds[0], + SpiderX: "/", + }), + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + var errg errgroup.Group + for range 3 { + errg.Go(testTCPConn(clientPort, 1024*1024, time.Second*30)) + } + if err := errg.Wait(); err != nil { + t.Error(err) + } +} + +// This testing test all known utls fingerprint in tls.PresetFingerprints that support reality (expect unsafe and random*) +// Beacuse figerprint support may be broken after utls/reality update +// Known broken fingerprint: android, 360 +func TestVlessRealityFingerprints(t *testing.T) { + TestFingerprint := func(fingerprint string) error { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + userID := protocol.NewID(uuid.New()) + serverPort := tcp.PickPort() + privateKey, _ := base64.RawURLEncoding.DecodeString("aGSYystUbf59_9_6LKRxD27rmSW_-2_nyd9YG_Gwbks") + publicKey, _ := base64.RawURLEncoding.DecodeString("E59WjnvZcQMu7tR7_BgyhycuEdBS-CtKxfImRCdAvFM") + shortIds := make([][]byte, 1) + shortIds[0] = make([]byte, 8) + hex.Decode(shortIds[0], []byte("0123456789abcdef")) + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogType: log.LogType_None, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + StreamSettings: &internet.StreamConfig{ + ProtocolName: "tcp", + SecurityType: serial.GetMessageType(&reality.Config{}), + SecuritySettings: []*serial.TypedMessage{ + serial.ToTypedMessage(&reality.Config{ + Show: false, + Dest: "www.google.com:443", // use google for now, may fail in some region + ServerNames: []string{"www.google.com"}, + PrivateKey: privateKey, + ShortIds: shortIds, + Type: "tcp", + }), + }, + }, + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + Clients: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vless.Account{ + Id: userID.String(), + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogType: log.LogType_None, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Vnext: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vless.Account{ + Id: userID.String(), + }), + }, + }, + }), + SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{ + StreamSettings: &internet.StreamConfig{ + ProtocolName: "tcp", + TransportSettings: []*internet.TransportConfig{ + { + ProtocolName: "tcp", + Settings: serial.ToTypedMessage(&transtcp.Config{}), + }, + }, + SecurityType: serial.GetMessageType(&reality.Config{}), + SecuritySettings: []*serial.TypedMessage{ + serial.ToTypedMessage(&reality.Config{ + Show: false, + Fingerprint: fingerprint, + ServerName: "www.google.com", + PublicKey: publicKey, + ShortId: shortIds[0], + SpiderX: "/", + }), + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + err = testTCPConn(clientPort, 1024*1024, time.Second*15)() + if err != nil { + return err + } + return nil + } + fingerPrints := []string{"chrome", "firefox", "safari", "ios", "edge", "qq"} + wg := sync.WaitGroup{} + wg.Add(len(fingerPrints)) + for _, fp := range fingerPrints { + go func() { + err := TestFingerprint(fp) + if err != nil { + t.Errorf("Fingerprint %s test failed: %v", fp, err) + } else { + t.Logf("Fingerprint %s test passed", fp) + } + wg.Done() + }() + } + wg.Wait() +} diff --git a/subproject/Xray-core-main/testing/scenarios/vmess_test.go b/subproject/Xray-core-main/testing/scenarios/vmess_test.go new file mode 100644 index 00000000..402cf940 --- /dev/null +++ b/subproject/Xray-core-main/testing/scenarios/vmess_test.go @@ -0,0 +1,1286 @@ +package scenarios + +import ( + "os" + "testing" + "time" + + "github.com/xtls/xray-core/app/log" + "github.com/xtls/xray-core/app/proxyman" + "github.com/xtls/xray-core/common" + clog "github.com/xtls/xray-core/common/log" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/serial" + "github.com/xtls/xray-core/common/uuid" + core "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/proxy/dokodemo" + "github.com/xtls/xray-core/proxy/freedom" + "github.com/xtls/xray-core/proxy/vmess" + "github.com/xtls/xray-core/proxy/vmess/inbound" + "github.com/xtls/xray-core/proxy/vmess/outbound" + "github.com/xtls/xray-core/testing/servers/tcp" + "github.com/xtls/xray-core/testing/servers/udp" + "github.com/xtls/xray-core/transport/internet" + "github.com/xtls/xray-core/transport/internet/kcp" + "golang.org/x/sync/errgroup" +) + +func TestVMessGCM(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + userID := protocol.NewID(uuid.New()) + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_AES128_GCM, + }, + }), + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + if err != nil { + t.Fatal("Failed to initialize all servers: ", err.Error()) + } + defer CloseAllServers(servers) + + var errg errgroup.Group + for range 3 { + errg.Go(testTCPConn(clientPort, 10240*1024, time.Second*40)) + } + + if err := errg.Wait(); err != nil { + t.Error(err) + } +} + +func TestVMessGCMReadv(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + userID := protocol.NewID(uuid.New()) + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_AES128_GCM, + }, + }), + }, + }, + }), + }, + }, + } + + const envName = "XRAY_BUF_READV" + common.Must(os.Setenv(envName, "enable")) + defer os.Unsetenv(envName) + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + if err != nil { + t.Fatal("Failed to initialize all servers: ", err.Error()) + } + defer CloseAllServers(servers) + + var errg errgroup.Group + for range 3 { + errg.Go(testTCPConn(clientPort, 10240*1024, time.Second*40)) + } + if err := errg.Wait(); err != nil { + t.Error(err) + } +} + +func TestVMessGCMUDP(t *testing.T) { + udpServer := udp.Server{ + MsgProcessor: xor, + } + dest, err := udpServer.Start() + common.Must(err) + defer udpServer.Close() + + userID := protocol.NewID(uuid.New()) + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := udp.PickPort() + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_UDP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_AES128_GCM, + }, + }), + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + var errg errgroup.Group + for range 3 { + errg.Go(testUDPConn(clientPort, 1024, time.Second*5)) + } + if err := errg.Wait(); err != nil { + t.Error(err) + } +} + +func TestVMessChacha20(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + userID := protocol.NewID(uuid.New()) + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_CHACHA20_POLY1305, + }, + }), + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + var errg errgroup.Group + for range 3 { + errg.Go(testTCPConn(clientPort, 10240*1024, time.Second*20)) + } + + if err := errg.Wait(); err != nil { + t.Error(err) + } +} + +func TestVMessNone(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + userID := protocol.NewID(uuid.New()) + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_NONE, + }, + }), + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + var errg errgroup.Group + for range 3 { + errg.Go(testTCPConn(clientPort, 1024*1024, time.Second*30)) + } + if err := errg.Wait(); err != nil { + t.Error(err) + } +} + +func TestVMessKCP(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + userID := protocol.NewID(uuid.New()) + serverPort := udp.PickPort() + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + StreamSettings: &internet.StreamConfig{ + ProtocolName: "mkcp", + }, + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_AES128_GCM, + }, + }), + }, + }, + }), + SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{ + StreamSettings: &internet.StreamConfig{ + ProtocolName: "mkcp", + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + var errg errgroup.Group + for range 3 { + errg.Go(testTCPConn(clientPort, 1024, time.Minute*2)) + } + if err := errg.Wait(); err != nil { + t.Error(err) + } +} + +func TestVMessKCPLarge(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + userID := protocol.NewID(uuid.New()) + serverPort := udp.PickPort() + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + StreamSettings: &internet.StreamConfig{ + ProtocolName: "mkcp", + TransportSettings: []*internet.TransportConfig{ + { + ProtocolName: "mkcp", + Settings: serial.ToTypedMessage(&kcp.Config{ + ReadBuffer: &kcp.ReadBuffer{ + Size: 512 * 1024, + }, + WriteBuffer: &kcp.WriteBuffer{ + Size: 512 * 1024, + }, + UplinkCapacity: &kcp.UplinkCapacity{ + Value: 20, + }, + DownlinkCapacity: &kcp.DownlinkCapacity{ + Value: 20, + }, + }), + }, + }, + }, + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_AES128_GCM, + }, + }), + }, + }, + }), + SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{ + StreamSettings: &internet.StreamConfig{ + ProtocolName: "mkcp", + TransportSettings: []*internet.TransportConfig{ + { + ProtocolName: "mkcp", + Settings: serial.ToTypedMessage(&kcp.Config{ + ReadBuffer: &kcp.ReadBuffer{ + Size: 512 * 1024, + }, + WriteBuffer: &kcp.WriteBuffer{ + Size: 512 * 1024, + }, + UplinkCapacity: &kcp.UplinkCapacity{ + Value: 20, + }, + DownlinkCapacity: &kcp.DownlinkCapacity{ + Value: 20, + }, + }), + }, + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + + var errg errgroup.Group + for range 3 { + errg.Go(testTCPConn(clientPort, 513*1024, time.Minute*5)) + } + if err := errg.Wait(); err != nil { + t.Error(err) + } + + defer func() { + <-time.After(5 * time.Second) + CloseAllServers(servers) + }() +} + +func TestVMessGCMMux(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + userID := protocol.NewID(uuid.New()) + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{ + MultiplexSettings: &proxyman.MultiplexingConfig{ + Enabled: true, + Concurrency: 4, + }, + }), + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_AES128_GCM, + }, + }), + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + for range "abcd" { + var errg errgroup.Group + for range 3 { + errg.Go(testTCPConn(clientPort, 10240, time.Second*20)) + } + if err := errg.Wait(); err != nil { + t.Fatal(err) + } + time.Sleep(time.Second) + } +} + +func TestVMessGCMMuxUDP(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + udpServer := udp.Server{ + MsgProcessor: xor, + } + udpDest, err := udpServer.Start() + common.Must(err) + defer udpServer.Close() + + userID := protocol.NewID(uuid.New()) + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientUDPPort := udp.PickPort() + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientUDPPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(udpDest.Address), + Port: uint32(udpDest.Port), + Networks: []net.Network{net.Network_UDP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{ + MultiplexSettings: &proxyman.MultiplexingConfig{ + Enabled: true, + Concurrency: 4, + }, + }), + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_AES128_GCM, + }, + }), + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + + for range "ab" { + var errg errgroup.Group + for range 3 { + errg.Go(testTCPConn(clientPort, 1024, time.Second*10)) + errg.Go(testUDPConn(clientUDPPort, 1024, time.Second*10)) + } + if err := errg.Wait(); err != nil { + t.Error(err) + } + time.Sleep(time.Second) + } + + defer func() { + <-time.After(5 * time.Second) + CloseAllServers(servers) + }() +} + +func TestVMessZero(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + userID := protocol.NewID(uuid.New()) + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_ZERO, + }, + }), + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + var errg errgroup.Group + for range 3 { + errg.Go(testTCPConn(clientPort, 1024*1024, time.Second*30)) + } + if err := errg.Wait(); err != nil { + t.Error(err) + } +} + +func TestVMessGCMLengthAuth(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + userID := protocol.NewID(uuid.New()) + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_AES128_GCM, + }, + TestsEnabled: "AuthenticatedLength", + }), + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + if err != nil { + t.Fatal("Failed to initialize all servers: ", err.Error()) + } + defer CloseAllServers(servers) + + var errg errgroup.Group + for range 3 { + errg.Go(testTCPConn(clientPort, 10240*1024, time.Second*40)) + } + + if err := errg.Wait(); err != nil { + t.Error(err) + } +} + +func TestVMessGCMLengthAuthPlusNoTerminationSignal(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + userID := protocol.NewID(uuid.New()) + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + TestsEnabled: "AuthenticatedLength|NoTerminationSignal", + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: &protocol.ServerEndpoint{ + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_AES128_GCM, + }, + TestsEnabled: "AuthenticatedLength|NoTerminationSignal", + }), + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + if err != nil { + t.Fatal("Failed to initialize all servers: ", err.Error()) + } + defer CloseAllServers(servers) + + var errg errgroup.Group + for range 3 { + errg.Go(testTCPConn(clientPort, 10240*1024, time.Second*40)) + } + + if err := errg.Wait(); err != nil { + t.Error(err) + } +} diff --git a/subproject/Xray-core-main/testing/scenarios/wireguard_test.go b/subproject/Xray-core-main/testing/scenarios/wireguard_test.go new file mode 100644 index 00000000..deaee114 --- /dev/null +++ b/subproject/Xray-core-main/testing/scenarios/wireguard_test.go @@ -0,0 +1,122 @@ +package scenarios + +import ( + "testing" + //"time" + + "github.com/xtls/xray-core/app/log" + "github.com/xtls/xray-core/app/proxyman" + "github.com/xtls/xray-core/common" + clog "github.com/xtls/xray-core/common/log" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/serial" + core "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/infra/conf" + "github.com/xtls/xray-core/proxy/dokodemo" + "github.com/xtls/xray-core/proxy/freedom" + "github.com/xtls/xray-core/proxy/wireguard" + "github.com/xtls/xray-core/testing/servers/tcp" + "github.com/xtls/xray-core/testing/servers/udp" + //"golang.org/x/sync/errgroup" +) + +func TestWireguard(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + serverPrivate, _ := conf.ParseWireGuardKey("EGs4lTSJPmgELx6YiJAmPR2meWi6bY+e9rTdCipSj10=") + serverPublic, _ := conf.ParseWireGuardKey("osAMIyil18HeZXGGBDC9KpZoM+L2iGyXWVSYivuM9B0=") + clientPrivate, _ := conf.ParseWireGuardKey("CPQSpgxgdQRZa5SUbT3HLv+mmDVHLW5YR/rQlzum/2I=") + clientPublic, _ := conf.ParseWireGuardKey("MmLJ5iHFVVBp7VsB0hxfpQ0wEzAbT2KQnpQpj0+RtBw=") + + serverPort := udp.PickPort() + serverConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&wireguard.DeviceConfig{ + IsClient: false, + NoKernelTun: false, + Endpoint: []string{"10.0.0.1"}, + Mtu: 1420, + SecretKey: serverPrivate, + Peers: []*wireguard.PeerConfig{{ + PublicKey: serverPublic, + AllowedIps: []string{"0.0.0.0/0", "::0/0"}, + }}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + App: []*serial.TypedMessage{ + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Debug, + ErrorLogType: log.LogType_Console, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + Networks: []net.Network{net.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&wireguard.DeviceConfig{ + IsClient: true, + NoKernelTun: false, + Endpoint: []string{"10.0.0.2"}, + Mtu: 1420, + SecretKey: clientPrivate, + Peers: []*wireguard.PeerConfig{{ + Endpoint: "127.0.0.1:" + serverPort.String(), + PublicKey: clientPublic, + AllowedIps: []string{"0.0.0.0/0", "::0/0"}, + }}, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + common.Must(err) + defer CloseAllServers(servers) + + // FIXME: for some reason wg server does not receive + + // var errg errgroup.Group + // for i := 0; i < 1; i++ { + // errg.Go(testTCPConn(clientPort, 1024, time.Second*2)) + // } + // if err := errg.Wait(); err != nil { + // t.Error(err) + // } +} diff --git a/subproject/Xray-core-main/testing/servers/http/http.go b/subproject/Xray-core-main/testing/servers/http/http.go new file mode 100644 index 00000000..dabd6a2a --- /dev/null +++ b/subproject/Xray-core-main/testing/servers/http/http.go @@ -0,0 +1,40 @@ +package tcp + +import ( + "net/http" + + "github.com/xtls/xray-core/common/net" +) + +type Server struct { + Port net.Port + PathHandler map[string]http.HandlerFunc + server *http.Server +} + +func (s *Server) ServeHTTP(resp http.ResponseWriter, req *http.Request) { + if req.URL.Path == "/" { + resp.Header().Set("Content-Type", "text/plain; charset=utf-8") + resp.WriteHeader(http.StatusOK) + resp.Write([]byte("Home")) + return + } + + handler, found := s.PathHandler[req.URL.Path] + if found { + handler(resp, req) + } +} + +func (s *Server) Start() (net.Destination, error) { + s.server = &http.Server{ + Addr: "127.0.0.1:" + s.Port.String(), + Handler: s, + } + go s.server.ListenAndServe() + return net.TCPDestination(net.LocalHostIP, s.Port), nil +} + +func (s *Server) Close() error { + return s.server.Close() +} diff --git a/subproject/Xray-core-main/testing/servers/tcp/port.go b/subproject/Xray-core-main/testing/servers/tcp/port.go new file mode 100644 index 00000000..67f85c87 --- /dev/null +++ b/subproject/Xray-core-main/testing/servers/tcp/port.go @@ -0,0 +1,16 @@ +package tcp + +import ( + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/net" +) + +// PickPort returns an unused TCP port in the system. The port returned is highly likely to be unused, but not guaranteed. +func PickPort() net.Port { + listener, err := net.Listen("tcp4", "127.0.0.1:0") + common.Must(err) + defer listener.Close() + + addr := listener.Addr().(*net.TCPAddr) + return net.Port(addr.Port) +} diff --git a/subproject/Xray-core-main/testing/servers/tcp/tcp.go b/subproject/Xray-core-main/testing/servers/tcp/tcp.go new file mode 100644 index 00000000..7fffecf6 --- /dev/null +++ b/subproject/Xray-core-main/testing/servers/tcp/tcp.go @@ -0,0 +1,109 @@ +package tcp + +import ( + "context" + "fmt" + "io" + + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/task" + "github.com/xtls/xray-core/transport/internet" + "github.com/xtls/xray-core/transport/pipe" +) + +type Server struct { + Port net.Port + MsgProcessor func(msg []byte) []byte + ShouldClose bool + SendFirst []byte + Listen net.Address + listener net.Listener +} + +func (server *Server) Start() (net.Destination, error) { + return server.StartContext(context.Background(), nil) +} + +func (server *Server) StartContext(ctx context.Context, sockopt *internet.SocketConfig) (net.Destination, error) { + listenerAddr := server.Listen + if listenerAddr == nil { + listenerAddr = net.LocalHostIP + } + listener, err := internet.ListenSystem(ctx, &net.TCPAddr{ + IP: listenerAddr.IP(), + Port: int(server.Port), + }, sockopt) + if err != nil { + return net.Destination{}, err + } + + localAddr := listener.Addr().(*net.TCPAddr) + server.Port = net.Port(localAddr.Port) + server.listener = listener + go server.acceptConnections(listener.(*net.TCPListener)) + + return net.TCPDestination(net.IPAddress(localAddr.IP), net.Port(localAddr.Port)), nil +} + +func (server *Server) acceptConnections(listener *net.TCPListener) { + for { + conn, err := listener.Accept() + if err != nil { + fmt.Printf("Failed accept TCP connection: %v\n", err) + return + } + + go server.handleConnection(conn) + } +} + +func (server *Server) handleConnection(conn net.Conn) { + if len(server.SendFirst) > 0 { + conn.Write(server.SendFirst) + } + + pReader, pWriter := pipe.New(pipe.WithoutSizeLimit()) + err := task.Run(context.Background(), func() error { + defer pWriter.Close() + + for { + b := buf.New() + if _, err := b.ReadFrom(conn); err != nil { + if err == io.EOF { + return nil + } + return err + } + copy(b.Bytes(), server.MsgProcessor(b.Bytes())) + if err := pWriter.WriteMultiBuffer(buf.MultiBuffer{b}); err != nil { + return err + } + } + }, func() error { + defer pReader.Interrupt() + + w := buf.NewWriter(conn) + for { + mb, err := pReader.ReadMultiBuffer() + if err != nil { + if err == io.EOF { + return nil + } + return err + } + if err := w.WriteMultiBuffer(mb); err != nil { + return err + } + } + }) + if err != nil { + fmt.Println("failed to transfer data: ", err.Error()) + } + + conn.Close() +} + +func (server *Server) Close() error { + return server.listener.Close() +} diff --git a/subproject/Xray-core-main/testing/servers/udp/port.go b/subproject/Xray-core-main/testing/servers/udp/port.go new file mode 100644 index 00000000..ca8d3e47 --- /dev/null +++ b/subproject/Xray-core-main/testing/servers/udp/port.go @@ -0,0 +1,19 @@ +package udp + +import ( + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/net" +) + +// PickPort returns an unused UDP port in the system. The port returned is highly likely to be unused, but not guaranteed. +func PickPort() net.Port { + conn, err := net.ListenUDP("udp4", &net.UDPAddr{ + IP: net.LocalHostIP.IP(), + Port: 0, + }) + common.Must(err) + defer conn.Close() + + addr := conn.LocalAddr().(*net.UDPAddr) + return net.Port(addr.Port) +} diff --git a/subproject/Xray-core-main/testing/servers/udp/udp.go b/subproject/Xray-core-main/testing/servers/udp/udp.go new file mode 100644 index 00000000..c927d5e0 --- /dev/null +++ b/subproject/Xray-core-main/testing/servers/udp/udp.go @@ -0,0 +1,54 @@ +package udp + +import ( + "fmt" + + "github.com/xtls/xray-core/common/net" +) + +type Server struct { + Port net.Port + MsgProcessor func(msg []byte) []byte + accepting bool + conn *net.UDPConn +} + +func (server *Server) Start() (net.Destination, error) { + conn, err := net.ListenUDP("udp", &net.UDPAddr{ + IP: []byte{127, 0, 0, 1}, + Port: int(server.Port), + Zone: "", + }) + if err != nil { + return net.Destination{}, err + } + server.Port = net.Port(conn.LocalAddr().(*net.UDPAddr).Port) + fmt.Println("UDP server started on port ", server.Port) + + server.conn = conn + go server.handleConnection(conn) + localAddr := conn.LocalAddr().(*net.UDPAddr) + return net.UDPDestination(net.IPAddress(localAddr.IP), net.Port(localAddr.Port)), nil +} + +func (server *Server) handleConnection(conn *net.UDPConn) { + server.accepting = true + for server.accepting { + buffer := make([]byte, 2*1024) + nBytes, addr, err := conn.ReadFromUDP(buffer) + if err != nil { + fmt.Printf("Failed to read from UDP: %v\n", err) + continue + } + + response := server.MsgProcessor(buffer[:nBytes]) + if _, err := conn.WriteToUDP(response, addr); err != nil { + fmt.Println("Failed to write to UDP: ", err.Error()) + } + } +} + +func (server *Server) Close() error { + server.accepting = false + return server.conn.Close() +} diff --git a/subproject/Xray-core-main/transport/internet/browser_dialer/dialer.go b/subproject/Xray-core-main/transport/internet/browser_dialer/dialer.go new file mode 100644 index 00000000..be2f137e --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/browser_dialer/dialer.go @@ -0,0 +1,196 @@ +package browser_dialer + +import ( + "bytes" + "context" + _ "embed" + "encoding/base64" + "encoding/json" + "net/http" + "time" + + "github.com/gorilla/websocket" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/platform" + "github.com/xtls/xray-core/common/uuid" +) + +//go:embed dialer.html +var webpage []byte + +type task struct { + Method string `json:"method"` + URL string `json:"url"` + Extra any `json:"extra,omitempty"` + StreamResponse bool `json:"streamResponse"` +} + +var conns chan *websocket.Conn + +var upgrader = &websocket.Upgrader{ + ReadBufferSize: 0, + WriteBufferSize: 0, + HandshakeTimeout: time.Second * 4, + CheckOrigin: func(r *http.Request) bool { + return true + }, +} + +func init() { + addr := platform.NewEnvFlag(platform.BrowserDialerAddress).GetValue(func() string { return "" }) + if addr != "" { + token := uuid.New() + csrfToken := token.String() + webpage = bytes.ReplaceAll(webpage, []byte("csrfToken"), []byte(csrfToken)) + conns = make(chan *websocket.Conn, 256) + go http.ListenAndServe(addr, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/websocket" { + if r.URL.Query().Get("token") == csrfToken { + if conn, err := upgrader.Upgrade(w, r, nil); err == nil { + conns <- conn + } else { + errors.LogError(context.Background(), "Browser dialer http upgrade unexpected error") + } + } + } else { + w.Header().Set("Access-Control-Allow-Origin", "*"); + w.Write(webpage) + } + })) + } +} + +func HasBrowserDialer() bool { + return conns != nil +} + +type webSocketExtra struct { + Protocol string `json:"protocol,omitempty"` +} + +func DialWS(uri string, ed []byte) (*websocket.Conn, error) { + task := task{ + Method: "WS", + URL: uri, + StreamResponse: true, + } + + if ed != nil { + task.Extra = webSocketExtra{ + Protocol: base64.RawURLEncoding.EncodeToString(ed), + } + } + + return dialTask(task) +} + +type httpExtra struct { + Referrer string `json:"referrer,omitempty"` + Headers map[string]string `json:"headers,omitempty"` + Cookies map[string]string `json:"cookies,omitempty"` +} + +func httpExtraFromHeadersAndCookies(headers http.Header, cookies []*http.Cookie) *httpExtra { + if len(headers) == 0 { + return nil + } + + extra := httpExtra{} + if referrer := headers.Get("Referer"); referrer != "" { + extra.Referrer = referrer + headers.Del("Referer") + } + + if len(headers) > 0 { + extra.Headers = make(map[string]string) + for header := range headers { + extra.Headers[header] = headers.Get(header) + } + } + + if len(cookies) > 0 { + extra.Cookies = make(map[string]string) + for _, cookie := range cookies { + extra.Cookies[cookie.Name] = cookie.Value + } + } + + return &extra +} + +func DialGet(uri string, headers http.Header, cookies []*http.Cookie) (*websocket.Conn, error) { + task := task{ + Method: "GET", + URL: uri, + Extra: httpExtraFromHeadersAndCookies(headers, cookies), + StreamResponse: true, + } + + return dialTask(task) +} + +func DialPacket(method string, uri string, headers http.Header, cookies []*http.Cookie, payload []byte) error { + return dialWithBody(method, uri, headers, cookies, payload) +} + +func dialWithBody(method string, uri string, headers http.Header, cookies []*http.Cookie, payload []byte) error { + task := task{ + Method: method, + URL: uri, + Extra: httpExtraFromHeadersAndCookies(headers, cookies), + StreamResponse: false, + } + + conn, err := dialTask(task) + if err != nil { + return err + } + + err = conn.WriteMessage(websocket.BinaryMessage, payload) + if err != nil { + return err + } + + err = CheckOK(conn) + if err != nil { + return err + } + + conn.Close() + return nil +} + +func dialTask(task task) (*websocket.Conn, error) { + data, err := json.Marshal(task) + if err != nil { + return nil, err + } + + var conn *websocket.Conn + for { + conn = <-conns + if conn.WriteMessage(websocket.TextMessage, data) != nil { + conn.Close() + } else { + break + } + } + err = CheckOK(conn) + if err != nil { + return nil, err + } + + return conn, nil +} + +func CheckOK(conn *websocket.Conn) error { + if _, p, err := conn.ReadMessage(); err != nil { + conn.Close() + return err + } else if s := string(p); s != "ok" { + conn.Close() + return errors.New(s) + } + + return nil +} diff --git a/subproject/Xray-core-main/transport/internet/browser_dialer/dialer.html b/subproject/Xray-core-main/transport/internet/browser_dialer/dialer.html new file mode 100644 index 00000000..5a0df489 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/browser_dialer/dialer.html @@ -0,0 +1,206 @@ + + + + Browser Dialer + + + + + + diff --git a/subproject/Xray-core-main/transport/internet/config.go b/subproject/Xray-core-main/transport/internet/config.go new file mode 100644 index 00000000..0b6f9324 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/config.go @@ -0,0 +1,152 @@ +package internet + +import ( + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/serial" +) + +type ConfigCreator func() interface{} + +var ( + globalTransportConfigCreatorCache = make(map[string]ConfigCreator) +) + +var strategy = [][]byte{ + // name strategy, prefer, fallback + {0, 0, 0}, // AsIs none, /, / + {1, 0, 0}, // UseIP use, both, none + {1, 4, 0}, // UseIPv4 use, 4, none + {1, 6, 0}, // UseIPv6 use, 6, none + {1, 4, 6}, // UseIPv4v6 use, 4, 6 + {1, 6, 4}, // UseIPv6v4 use, 6, 4 + {2, 0, 0}, // ForceIP force, both, none + {2, 4, 0}, // ForceIPv4 force, 4, none + {2, 6, 0}, // ForceIPv6 force, 6, none + {2, 4, 6}, // ForceIPv4v6 force, 4, 6 + {2, 6, 4}, // ForceIPv6v4 force, 6, 4 +} + +const unknownProtocol = "unknown" + +func RegisterProtocolConfigCreator(name string, creator ConfigCreator) error { + if _, found := globalTransportConfigCreatorCache[name]; found { + return errors.New("protocol ", name, " is already registered").AtError() + } + globalTransportConfigCreatorCache[name] = creator + return nil +} + +// Note: Each new transport needs to add init() func in transport/internet/xxx/config.go +// Otherwise, it will cause #3244 +func CreateTransportConfig(name string) (interface{}, error) { + creator, ok := globalTransportConfigCreatorCache[name] + if !ok { + return nil, errors.New("unknown transport protocol: ", name) + } + return creator(), nil +} + +func (c *TransportConfig) GetTypedSettings() (interface{}, error) { + return c.Settings.GetInstance() +} + +func (c *TransportConfig) GetUnifiedProtocolName() string { + return c.ProtocolName +} + +func (c *StreamConfig) GetEffectiveProtocol() string { + if c == nil || len(c.ProtocolName) == 0 { + return "tcp" + } + + return c.ProtocolName +} + +func (c *StreamConfig) GetEffectiveTransportSettings() (interface{}, error) { + protocol := c.GetEffectiveProtocol() + return c.GetTransportSettingsFor(protocol) +} + +func (c *StreamConfig) GetTransportSettingsFor(protocol string) (interface{}, error) { + if c != nil { + for _, settings := range c.TransportSettings { + if settings.GetUnifiedProtocolName() == protocol { + return settings.GetTypedSettings() + } + } + } + + return CreateTransportConfig(protocol) +} + +func (c *StreamConfig) GetEffectiveSecuritySettings() (interface{}, error) { + for _, settings := range c.SecuritySettings { + if settings.Type == c.SecurityType { + return settings.GetInstance() + } + } + return serial.GetInstance(c.SecurityType) +} + +func (c *StreamConfig) HasSecuritySettings() bool { + return len(c.SecuritySettings) > 0 +} + +func (c *ProxyConfig) HasTag() bool { + return c != nil && len(c.Tag) > 0 +} + +func (m SocketConfig_TProxyMode) IsEnabled() bool { + return m != SocketConfig_Off +} + +func (s DomainStrategy) HasStrategy() bool { + return strategy[s][0] != 0 +} + +func (s DomainStrategy) ForceIP() bool { + return strategy[s][0] == 2 +} + +func (s DomainStrategy) PreferIP4() bool { + return strategy[s][1] == 4 || strategy[s][1] == 0 +} + +func (s DomainStrategy) PreferIP6() bool { + return strategy[s][1] == 6 || strategy[s][1] == 0 +} + +func (s DomainStrategy) HasFallback() bool { + return strategy[s][2] != 0 +} + +func (s DomainStrategy) FallbackIP4() bool { + return strategy[s][2] == 4 +} + +func (s DomainStrategy) FallbackIP6() bool { + return strategy[s][2] == 6 +} + +func (s DomainStrategy) GetDynamicStrategy(addrFamily net.AddressFamily) DomainStrategy { + if addrFamily.IsDomain() { + return s + } + switch s { + case DomainStrategy_USE_IP: + if addrFamily.IsIPv4() { + return DomainStrategy_USE_IP46 + } else if addrFamily.IsIPv6() { + return DomainStrategy_USE_IP64 + } + case DomainStrategy_FORCE_IP: + if addrFamily.IsIPv4() { + return DomainStrategy_FORCE_IP46 + } else if addrFamily.IsIPv6() { + return DomainStrategy_FORCE_IP64 + } + default: + } + return s +} diff --git a/subproject/Xray-core-main/transport/internet/config.pb.go b/subproject/Xray-core-main/transport/internet/config.pb.go new file mode 100644 index 00000000..e2339fe8 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/config.pb.go @@ -0,0 +1,1209 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: transport/internet/config.proto + +package internet + +import ( + net "github.com/xtls/xray-core/common/net" + serial "github.com/xtls/xray-core/common/serial" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type DomainStrategy int32 + +const ( + DomainStrategy_AS_IS DomainStrategy = 0 + DomainStrategy_USE_IP DomainStrategy = 1 + DomainStrategy_USE_IP4 DomainStrategy = 2 + DomainStrategy_USE_IP6 DomainStrategy = 3 + DomainStrategy_USE_IP46 DomainStrategy = 4 + DomainStrategy_USE_IP64 DomainStrategy = 5 + DomainStrategy_FORCE_IP DomainStrategy = 6 + DomainStrategy_FORCE_IP4 DomainStrategy = 7 + DomainStrategy_FORCE_IP6 DomainStrategy = 8 + DomainStrategy_FORCE_IP46 DomainStrategy = 9 + DomainStrategy_FORCE_IP64 DomainStrategy = 10 +) + +// Enum value maps for DomainStrategy. +var ( + DomainStrategy_name = map[int32]string{ + 0: "AS_IS", + 1: "USE_IP", + 2: "USE_IP4", + 3: "USE_IP6", + 4: "USE_IP46", + 5: "USE_IP64", + 6: "FORCE_IP", + 7: "FORCE_IP4", + 8: "FORCE_IP6", + 9: "FORCE_IP46", + 10: "FORCE_IP64", + } + DomainStrategy_value = map[string]int32{ + "AS_IS": 0, + "USE_IP": 1, + "USE_IP4": 2, + "USE_IP6": 3, + "USE_IP46": 4, + "USE_IP64": 5, + "FORCE_IP": 6, + "FORCE_IP4": 7, + "FORCE_IP6": 8, + "FORCE_IP46": 9, + "FORCE_IP64": 10, + } +) + +func (x DomainStrategy) Enum() *DomainStrategy { + p := new(DomainStrategy) + *p = x + return p +} + +func (x DomainStrategy) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (DomainStrategy) Descriptor() protoreflect.EnumDescriptor { + return file_transport_internet_config_proto_enumTypes[0].Descriptor() +} + +func (DomainStrategy) Type() protoreflect.EnumType { + return &file_transport_internet_config_proto_enumTypes[0] +} + +func (x DomainStrategy) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use DomainStrategy.Descriptor instead. +func (DomainStrategy) EnumDescriptor() ([]byte, []int) { + return file_transport_internet_config_proto_rawDescGZIP(), []int{0} +} + +type AddressPortStrategy int32 + +const ( + AddressPortStrategy_None AddressPortStrategy = 0 + AddressPortStrategy_SrvPortOnly AddressPortStrategy = 1 + AddressPortStrategy_SrvAddressOnly AddressPortStrategy = 2 + AddressPortStrategy_SrvPortAndAddress AddressPortStrategy = 3 + AddressPortStrategy_TxtPortOnly AddressPortStrategy = 4 + AddressPortStrategy_TxtAddressOnly AddressPortStrategy = 5 + AddressPortStrategy_TxtPortAndAddress AddressPortStrategy = 6 +) + +// Enum value maps for AddressPortStrategy. +var ( + AddressPortStrategy_name = map[int32]string{ + 0: "None", + 1: "SrvPortOnly", + 2: "SrvAddressOnly", + 3: "SrvPortAndAddress", + 4: "TxtPortOnly", + 5: "TxtAddressOnly", + 6: "TxtPortAndAddress", + } + AddressPortStrategy_value = map[string]int32{ + "None": 0, + "SrvPortOnly": 1, + "SrvAddressOnly": 2, + "SrvPortAndAddress": 3, + "TxtPortOnly": 4, + "TxtAddressOnly": 5, + "TxtPortAndAddress": 6, + } +) + +func (x AddressPortStrategy) Enum() *AddressPortStrategy { + p := new(AddressPortStrategy) + *p = x + return p +} + +func (x AddressPortStrategy) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (AddressPortStrategy) Descriptor() protoreflect.EnumDescriptor { + return file_transport_internet_config_proto_enumTypes[1].Descriptor() +} + +func (AddressPortStrategy) Type() protoreflect.EnumType { + return &file_transport_internet_config_proto_enumTypes[1] +} + +func (x AddressPortStrategy) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use AddressPortStrategy.Descriptor instead. +func (AddressPortStrategy) EnumDescriptor() ([]byte, []int) { + return file_transport_internet_config_proto_rawDescGZIP(), []int{1} +} + +type SocketConfig_TProxyMode int32 + +const ( + // TProxy is off. + SocketConfig_Off SocketConfig_TProxyMode = 0 + // TProxy mode. + SocketConfig_TProxy SocketConfig_TProxyMode = 1 + // Redirect mode. + SocketConfig_Redirect SocketConfig_TProxyMode = 2 +) + +// Enum value maps for SocketConfig_TProxyMode. +var ( + SocketConfig_TProxyMode_name = map[int32]string{ + 0: "Off", + 1: "TProxy", + 2: "Redirect", + } + SocketConfig_TProxyMode_value = map[string]int32{ + "Off": 0, + "TProxy": 1, + "Redirect": 2, + } +) + +func (x SocketConfig_TProxyMode) Enum() *SocketConfig_TProxyMode { + p := new(SocketConfig_TProxyMode) + *p = x + return p +} + +func (x SocketConfig_TProxyMode) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (SocketConfig_TProxyMode) Descriptor() protoreflect.EnumDescriptor { + return file_transport_internet_config_proto_enumTypes[2].Descriptor() +} + +func (SocketConfig_TProxyMode) Type() protoreflect.EnumType { + return &file_transport_internet_config_proto_enumTypes[2] +} + +func (x SocketConfig_TProxyMode) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use SocketConfig_TProxyMode.Descriptor instead. +func (SocketConfig_TProxyMode) EnumDescriptor() ([]byte, []int) { + return file_transport_internet_config_proto_rawDescGZIP(), []int{6, 0} +} + +type TransportConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Transport protocol name. + ProtocolName string `protobuf:"bytes,3,opt,name=protocol_name,json=protocolName,proto3" json:"protocol_name,omitempty"` + // Specific transport protocol settings. + Settings *serial.TypedMessage `protobuf:"bytes,2,opt,name=settings,proto3" json:"settings,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TransportConfig) Reset() { + *x = TransportConfig{} + mi := &file_transport_internet_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TransportConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TransportConfig) ProtoMessage() {} + +func (x *TransportConfig) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TransportConfig.ProtoReflect.Descriptor instead. +func (*TransportConfig) Descriptor() ([]byte, []int) { + return file_transport_internet_config_proto_rawDescGZIP(), []int{0} +} + +func (x *TransportConfig) GetProtocolName() string { + if x != nil { + return x.ProtocolName + } + return "" +} + +func (x *TransportConfig) GetSettings() *serial.TypedMessage { + if x != nil { + return x.Settings + } + return nil +} + +type StreamConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + Address *net.IPOrDomain `protobuf:"bytes,8,opt,name=address,proto3" json:"address,omitempty"` + Port uint32 `protobuf:"varint,9,opt,name=port,proto3" json:"port,omitempty"` + // Effective network. + ProtocolName string `protobuf:"bytes,5,opt,name=protocol_name,json=protocolName,proto3" json:"protocol_name,omitempty"` + TransportSettings []*TransportConfig `protobuf:"bytes,2,rep,name=transport_settings,json=transportSettings,proto3" json:"transport_settings,omitempty"` + // Type of security. Must be a message name of the settings proto. + SecurityType string `protobuf:"bytes,3,opt,name=security_type,json=securityType,proto3" json:"security_type,omitempty"` + // Transport security settings. They can be either TLS or REALITY. + SecuritySettings []*serial.TypedMessage `protobuf:"bytes,4,rep,name=security_settings,json=securitySettings,proto3" json:"security_settings,omitempty"` + Udpmasks []*serial.TypedMessage `protobuf:"bytes,10,rep,name=udpmasks,proto3" json:"udpmasks,omitempty"` + Tcpmasks []*serial.TypedMessage `protobuf:"bytes,11,rep,name=tcpmasks,proto3" json:"tcpmasks,omitempty"` + QuicParams *QuicParams `protobuf:"bytes,12,opt,name=quic_params,json=quicParams,proto3" json:"quic_params,omitempty"` + SocketSettings *SocketConfig `protobuf:"bytes,6,opt,name=socket_settings,json=socketSettings,proto3" json:"socket_settings,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StreamConfig) Reset() { + *x = StreamConfig{} + mi := &file_transport_internet_config_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StreamConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StreamConfig) ProtoMessage() {} + +func (x *StreamConfig) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_config_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StreamConfig.ProtoReflect.Descriptor instead. +func (*StreamConfig) Descriptor() ([]byte, []int) { + return file_transport_internet_config_proto_rawDescGZIP(), []int{1} +} + +func (x *StreamConfig) GetAddress() *net.IPOrDomain { + if x != nil { + return x.Address + } + return nil +} + +func (x *StreamConfig) GetPort() uint32 { + if x != nil { + return x.Port + } + return 0 +} + +func (x *StreamConfig) GetProtocolName() string { + if x != nil { + return x.ProtocolName + } + return "" +} + +func (x *StreamConfig) GetTransportSettings() []*TransportConfig { + if x != nil { + return x.TransportSettings + } + return nil +} + +func (x *StreamConfig) GetSecurityType() string { + if x != nil { + return x.SecurityType + } + return "" +} + +func (x *StreamConfig) GetSecuritySettings() []*serial.TypedMessage { + if x != nil { + return x.SecuritySettings + } + return nil +} + +func (x *StreamConfig) GetUdpmasks() []*serial.TypedMessage { + if x != nil { + return x.Udpmasks + } + return nil +} + +func (x *StreamConfig) GetTcpmasks() []*serial.TypedMessage { + if x != nil { + return x.Tcpmasks + } + return nil +} + +func (x *StreamConfig) GetQuicParams() *QuicParams { + if x != nil { + return x.QuicParams + } + return nil +} + +func (x *StreamConfig) GetSocketSettings() *SocketConfig { + if x != nil { + return x.SocketSettings + } + return nil +} + +type UdpHop struct { + state protoimpl.MessageState `protogen:"open.v1"` + Ports []uint32 `protobuf:"varint,1,rep,packed,name=ports,proto3" json:"ports,omitempty"` + IntervalMin int64 `protobuf:"varint,2,opt,name=interval_min,json=intervalMin,proto3" json:"interval_min,omitempty"` + IntervalMax int64 `protobuf:"varint,3,opt,name=interval_max,json=intervalMax,proto3" json:"interval_max,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UdpHop) Reset() { + *x = UdpHop{} + mi := &file_transport_internet_config_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UdpHop) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UdpHop) ProtoMessage() {} + +func (x *UdpHop) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_config_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UdpHop.ProtoReflect.Descriptor instead. +func (*UdpHop) Descriptor() ([]byte, []int) { + return file_transport_internet_config_proto_rawDescGZIP(), []int{2} +} + +func (x *UdpHop) GetPorts() []uint32 { + if x != nil { + return x.Ports + } + return nil +} + +func (x *UdpHop) GetIntervalMin() int64 { + if x != nil { + return x.IntervalMin + } + return 0 +} + +func (x *UdpHop) GetIntervalMax() int64 { + if x != nil { + return x.IntervalMax + } + return 0 +} + +type QuicParams struct { + state protoimpl.MessageState `protogen:"open.v1"` + Congestion string `protobuf:"bytes,1,opt,name=congestion,proto3" json:"congestion,omitempty"` + BbrProfile string `protobuf:"bytes,2,opt,name=bbr_profile,json=bbrProfile,proto3" json:"bbr_profile,omitempty"` + BrutalUp uint64 `protobuf:"varint,3,opt,name=brutal_up,json=brutalUp,proto3" json:"brutal_up,omitempty"` + BrutalDown uint64 `protobuf:"varint,4,opt,name=brutal_down,json=brutalDown,proto3" json:"brutal_down,omitempty"` + UdpHop *UdpHop `protobuf:"bytes,5,opt,name=udp_hop,json=udpHop,proto3" json:"udp_hop,omitempty"` + InitStreamReceiveWindow uint64 `protobuf:"varint,6,opt,name=init_stream_receive_window,json=initStreamReceiveWindow,proto3" json:"init_stream_receive_window,omitempty"` + MaxStreamReceiveWindow uint64 `protobuf:"varint,7,opt,name=max_stream_receive_window,json=maxStreamReceiveWindow,proto3" json:"max_stream_receive_window,omitempty"` + InitConnReceiveWindow uint64 `protobuf:"varint,8,opt,name=init_conn_receive_window,json=initConnReceiveWindow,proto3" json:"init_conn_receive_window,omitempty"` + MaxConnReceiveWindow uint64 `protobuf:"varint,9,opt,name=max_conn_receive_window,json=maxConnReceiveWindow,proto3" json:"max_conn_receive_window,omitempty"` + MaxIdleTimeout int64 `protobuf:"varint,10,opt,name=max_idle_timeout,json=maxIdleTimeout,proto3" json:"max_idle_timeout,omitempty"` + KeepAlivePeriod int64 `protobuf:"varint,11,opt,name=keep_alive_period,json=keepAlivePeriod,proto3" json:"keep_alive_period,omitempty"` + DisablePathMtuDiscovery bool `protobuf:"varint,12,opt,name=disable_path_mtu_discovery,json=disablePathMtuDiscovery,proto3" json:"disable_path_mtu_discovery,omitempty"` + MaxIncomingStreams int64 `protobuf:"varint,13,opt,name=max_incoming_streams,json=maxIncomingStreams,proto3" json:"max_incoming_streams,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *QuicParams) Reset() { + *x = QuicParams{} + mi := &file_transport_internet_config_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *QuicParams) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*QuicParams) ProtoMessage() {} + +func (x *QuicParams) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_config_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use QuicParams.ProtoReflect.Descriptor instead. +func (*QuicParams) Descriptor() ([]byte, []int) { + return file_transport_internet_config_proto_rawDescGZIP(), []int{3} +} + +func (x *QuicParams) GetCongestion() string { + if x != nil { + return x.Congestion + } + return "" +} + +func (x *QuicParams) GetBbrProfile() string { + if x != nil { + return x.BbrProfile + } + return "" +} + +func (x *QuicParams) GetBrutalUp() uint64 { + if x != nil { + return x.BrutalUp + } + return 0 +} + +func (x *QuicParams) GetBrutalDown() uint64 { + if x != nil { + return x.BrutalDown + } + return 0 +} + +func (x *QuicParams) GetUdpHop() *UdpHop { + if x != nil { + return x.UdpHop + } + return nil +} + +func (x *QuicParams) GetInitStreamReceiveWindow() uint64 { + if x != nil { + return x.InitStreamReceiveWindow + } + return 0 +} + +func (x *QuicParams) GetMaxStreamReceiveWindow() uint64 { + if x != nil { + return x.MaxStreamReceiveWindow + } + return 0 +} + +func (x *QuicParams) GetInitConnReceiveWindow() uint64 { + if x != nil { + return x.InitConnReceiveWindow + } + return 0 +} + +func (x *QuicParams) GetMaxConnReceiveWindow() uint64 { + if x != nil { + return x.MaxConnReceiveWindow + } + return 0 +} + +func (x *QuicParams) GetMaxIdleTimeout() int64 { + if x != nil { + return x.MaxIdleTimeout + } + return 0 +} + +func (x *QuicParams) GetKeepAlivePeriod() int64 { + if x != nil { + return x.KeepAlivePeriod + } + return 0 +} + +func (x *QuicParams) GetDisablePathMtuDiscovery() bool { + if x != nil { + return x.DisablePathMtuDiscovery + } + return false +} + +func (x *QuicParams) GetMaxIncomingStreams() int64 { + if x != nil { + return x.MaxIncomingStreams + } + return 0 +} + +type ProxyConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + Tag string `protobuf:"bytes,1,opt,name=tag,proto3" json:"tag,omitempty"` + TransportLayerProxy bool `protobuf:"varint,2,opt,name=transportLayerProxy,proto3" json:"transportLayerProxy,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProxyConfig) Reset() { + *x = ProxyConfig{} + mi := &file_transport_internet_config_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProxyConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProxyConfig) ProtoMessage() {} + +func (x *ProxyConfig) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_config_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProxyConfig.ProtoReflect.Descriptor instead. +func (*ProxyConfig) Descriptor() ([]byte, []int) { + return file_transport_internet_config_proto_rawDescGZIP(), []int{4} +} + +func (x *ProxyConfig) GetTag() string { + if x != nil { + return x.Tag + } + return "" +} + +func (x *ProxyConfig) GetTransportLayerProxy() bool { + if x != nil { + return x.TransportLayerProxy + } + return false +} + +type CustomSockopt struct { + state protoimpl.MessageState `protogen:"open.v1"` + System string `protobuf:"bytes,1,opt,name=system,proto3" json:"system,omitempty"` + Network string `protobuf:"bytes,2,opt,name=network,proto3" json:"network,omitempty"` + Level string `protobuf:"bytes,3,opt,name=level,proto3" json:"level,omitempty"` + Opt string `protobuf:"bytes,4,opt,name=opt,proto3" json:"opt,omitempty"` + Value string `protobuf:"bytes,5,opt,name=value,proto3" json:"value,omitempty"` + Type string `protobuf:"bytes,6,opt,name=type,proto3" json:"type,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CustomSockopt) Reset() { + *x = CustomSockopt{} + mi := &file_transport_internet_config_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CustomSockopt) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CustomSockopt) ProtoMessage() {} + +func (x *CustomSockopt) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_config_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CustomSockopt.ProtoReflect.Descriptor instead. +func (*CustomSockopt) Descriptor() ([]byte, []int) { + return file_transport_internet_config_proto_rawDescGZIP(), []int{5} +} + +func (x *CustomSockopt) GetSystem() string { + if x != nil { + return x.System + } + return "" +} + +func (x *CustomSockopt) GetNetwork() string { + if x != nil { + return x.Network + } + return "" +} + +func (x *CustomSockopt) GetLevel() string { + if x != nil { + return x.Level + } + return "" +} + +func (x *CustomSockopt) GetOpt() string { + if x != nil { + return x.Opt + } + return "" +} + +func (x *CustomSockopt) GetValue() string { + if x != nil { + return x.Value + } + return "" +} + +func (x *CustomSockopt) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +// SocketConfig is options to be applied on network sockets. +type SocketConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Mark of the connection. If non-zero, the value will be set to SO_MARK. + Mark int32 `protobuf:"varint,1,opt,name=mark,proto3" json:"mark,omitempty"` + // TFO is the state of TFO settings. + Tfo int32 `protobuf:"varint,2,opt,name=tfo,proto3" json:"tfo,omitempty"` + // TProxy is for enabling TProxy socket option. + Tproxy SocketConfig_TProxyMode `protobuf:"varint,3,opt,name=tproxy,proto3,enum=xray.transport.internet.SocketConfig_TProxyMode" json:"tproxy,omitempty"` + // ReceiveOriginalDestAddress is for enabling IP_RECVORIGDSTADDR socket + // option. This option is for UDP only. + ReceiveOriginalDestAddress bool `protobuf:"varint,4,opt,name=receive_original_dest_address,json=receiveOriginalDestAddress,proto3" json:"receive_original_dest_address,omitempty"` + BindAddress []byte `protobuf:"bytes,5,opt,name=bind_address,json=bindAddress,proto3" json:"bind_address,omitempty"` + BindPort uint32 `protobuf:"varint,6,opt,name=bind_port,json=bindPort,proto3" json:"bind_port,omitempty"` + AcceptProxyProtocol bool `protobuf:"varint,7,opt,name=accept_proxy_protocol,json=acceptProxyProtocol,proto3" json:"accept_proxy_protocol,omitempty"` + DomainStrategy DomainStrategy `protobuf:"varint,8,opt,name=domain_strategy,json=domainStrategy,proto3,enum=xray.transport.internet.DomainStrategy" json:"domain_strategy,omitempty"` + DialerProxy string `protobuf:"bytes,9,opt,name=dialer_proxy,json=dialerProxy,proto3" json:"dialer_proxy,omitempty"` + TcpKeepAliveInterval int32 `protobuf:"varint,10,opt,name=tcp_keep_alive_interval,json=tcpKeepAliveInterval,proto3" json:"tcp_keep_alive_interval,omitempty"` + TcpKeepAliveIdle int32 `protobuf:"varint,11,opt,name=tcp_keep_alive_idle,json=tcpKeepAliveIdle,proto3" json:"tcp_keep_alive_idle,omitempty"` + TcpCongestion string `protobuf:"bytes,12,opt,name=tcp_congestion,json=tcpCongestion,proto3" json:"tcp_congestion,omitempty"` + Interface string `protobuf:"bytes,13,opt,name=interface,proto3" json:"interface,omitempty"` + V6Only bool `protobuf:"varint,14,opt,name=v6only,proto3" json:"v6only,omitempty"` + TcpWindowClamp int32 `protobuf:"varint,15,opt,name=tcp_window_clamp,json=tcpWindowClamp,proto3" json:"tcp_window_clamp,omitempty"` + TcpUserTimeout int32 `protobuf:"varint,16,opt,name=tcp_user_timeout,json=tcpUserTimeout,proto3" json:"tcp_user_timeout,omitempty"` + TcpMaxSeg int32 `protobuf:"varint,17,opt,name=tcp_max_seg,json=tcpMaxSeg,proto3" json:"tcp_max_seg,omitempty"` + Penetrate bool `protobuf:"varint,18,opt,name=penetrate,proto3" json:"penetrate,omitempty"` + TcpMptcp bool `protobuf:"varint,19,opt,name=tcp_mptcp,json=tcpMptcp,proto3" json:"tcp_mptcp,omitempty"` + CustomSockopt []*CustomSockopt `protobuf:"bytes,20,rep,name=customSockopt,proto3" json:"customSockopt,omitempty"` + AddressPortStrategy AddressPortStrategy `protobuf:"varint,21,opt,name=address_port_strategy,json=addressPortStrategy,proto3,enum=xray.transport.internet.AddressPortStrategy" json:"address_port_strategy,omitempty"` + HappyEyeballs *HappyEyeballsConfig `protobuf:"bytes,22,opt,name=happy_eyeballs,json=happyEyeballs,proto3" json:"happy_eyeballs,omitempty"` + TrustedXForwardedFor []string `protobuf:"bytes,23,rep,name=trusted_x_forwarded_for,json=trustedXForwardedFor,proto3" json:"trusted_x_forwarded_for,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SocketConfig) Reset() { + *x = SocketConfig{} + mi := &file_transport_internet_config_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SocketConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SocketConfig) ProtoMessage() {} + +func (x *SocketConfig) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_config_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SocketConfig.ProtoReflect.Descriptor instead. +func (*SocketConfig) Descriptor() ([]byte, []int) { + return file_transport_internet_config_proto_rawDescGZIP(), []int{6} +} + +func (x *SocketConfig) GetMark() int32 { + if x != nil { + return x.Mark + } + return 0 +} + +func (x *SocketConfig) GetTfo() int32 { + if x != nil { + return x.Tfo + } + return 0 +} + +func (x *SocketConfig) GetTproxy() SocketConfig_TProxyMode { + if x != nil { + return x.Tproxy + } + return SocketConfig_Off +} + +func (x *SocketConfig) GetReceiveOriginalDestAddress() bool { + if x != nil { + return x.ReceiveOriginalDestAddress + } + return false +} + +func (x *SocketConfig) GetBindAddress() []byte { + if x != nil { + return x.BindAddress + } + return nil +} + +func (x *SocketConfig) GetBindPort() uint32 { + if x != nil { + return x.BindPort + } + return 0 +} + +func (x *SocketConfig) GetAcceptProxyProtocol() bool { + if x != nil { + return x.AcceptProxyProtocol + } + return false +} + +func (x *SocketConfig) GetDomainStrategy() DomainStrategy { + if x != nil { + return x.DomainStrategy + } + return DomainStrategy_AS_IS +} + +func (x *SocketConfig) GetDialerProxy() string { + if x != nil { + return x.DialerProxy + } + return "" +} + +func (x *SocketConfig) GetTcpKeepAliveInterval() int32 { + if x != nil { + return x.TcpKeepAliveInterval + } + return 0 +} + +func (x *SocketConfig) GetTcpKeepAliveIdle() int32 { + if x != nil { + return x.TcpKeepAliveIdle + } + return 0 +} + +func (x *SocketConfig) GetTcpCongestion() string { + if x != nil { + return x.TcpCongestion + } + return "" +} + +func (x *SocketConfig) GetInterface() string { + if x != nil { + return x.Interface + } + return "" +} + +func (x *SocketConfig) GetV6Only() bool { + if x != nil { + return x.V6Only + } + return false +} + +func (x *SocketConfig) GetTcpWindowClamp() int32 { + if x != nil { + return x.TcpWindowClamp + } + return 0 +} + +func (x *SocketConfig) GetTcpUserTimeout() int32 { + if x != nil { + return x.TcpUserTimeout + } + return 0 +} + +func (x *SocketConfig) GetTcpMaxSeg() int32 { + if x != nil { + return x.TcpMaxSeg + } + return 0 +} + +func (x *SocketConfig) GetPenetrate() bool { + if x != nil { + return x.Penetrate + } + return false +} + +func (x *SocketConfig) GetTcpMptcp() bool { + if x != nil { + return x.TcpMptcp + } + return false +} + +func (x *SocketConfig) GetCustomSockopt() []*CustomSockopt { + if x != nil { + return x.CustomSockopt + } + return nil +} + +func (x *SocketConfig) GetAddressPortStrategy() AddressPortStrategy { + if x != nil { + return x.AddressPortStrategy + } + return AddressPortStrategy_None +} + +func (x *SocketConfig) GetHappyEyeballs() *HappyEyeballsConfig { + if x != nil { + return x.HappyEyeballs + } + return nil +} + +func (x *SocketConfig) GetTrustedXForwardedFor() []string { + if x != nil { + return x.TrustedXForwardedFor + } + return nil +} + +type HappyEyeballsConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + PrioritizeIpv6 bool `protobuf:"varint,1,opt,name=prioritize_ipv6,json=prioritizeIpv6,proto3" json:"prioritize_ipv6,omitempty"` + Interleave uint32 `protobuf:"varint,2,opt,name=interleave,proto3" json:"interleave,omitempty"` + TryDelayMs uint64 `protobuf:"varint,3,opt,name=try_delayMs,json=tryDelayMs,proto3" json:"try_delayMs,omitempty"` + MaxConcurrentTry uint32 `protobuf:"varint,4,opt,name=max_concurrent_try,json=maxConcurrentTry,proto3" json:"max_concurrent_try,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HappyEyeballsConfig) Reset() { + *x = HappyEyeballsConfig{} + mi := &file_transport_internet_config_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HappyEyeballsConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HappyEyeballsConfig) ProtoMessage() {} + +func (x *HappyEyeballsConfig) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_config_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 HappyEyeballsConfig.ProtoReflect.Descriptor instead. +func (*HappyEyeballsConfig) Descriptor() ([]byte, []int) { + return file_transport_internet_config_proto_rawDescGZIP(), []int{7} +} + +func (x *HappyEyeballsConfig) GetPrioritizeIpv6() bool { + if x != nil { + return x.PrioritizeIpv6 + } + return false +} + +func (x *HappyEyeballsConfig) GetInterleave() uint32 { + if x != nil { + return x.Interleave + } + return 0 +} + +func (x *HappyEyeballsConfig) GetTryDelayMs() uint64 { + if x != nil { + return x.TryDelayMs + } + return 0 +} + +func (x *HappyEyeballsConfig) GetMaxConcurrentTry() uint32 { + if x != nil { + return x.MaxConcurrentTry + } + return 0 +} + +var File_transport_internet_config_proto protoreflect.FileDescriptor + +const file_transport_internet_config_proto_rawDesc = "" + + "\n" + + "\x1ftransport/internet/config.proto\x12\x17xray.transport.internet\x1a!common/serial/typed_message.proto\x1a\x18common/net/address.proto\"t\n" + + "\x0fTransportConfig\x12#\n" + + "\rprotocol_name\x18\x03 \x01(\tR\fprotocolName\x12<\n" + + "\bsettings\x18\x02 \x01(\v2 .xray.common.serial.TypedMessageR\bsettings\"\xdd\x04\n" + + "\fStreamConfig\x125\n" + + "\aaddress\x18\b \x01(\v2\x1b.xray.common.net.IPOrDomainR\aaddress\x12\x12\n" + + "\x04port\x18\t \x01(\rR\x04port\x12#\n" + + "\rprotocol_name\x18\x05 \x01(\tR\fprotocolName\x12W\n" + + "\x12transport_settings\x18\x02 \x03(\v2(.xray.transport.internet.TransportConfigR\x11transportSettings\x12#\n" + + "\rsecurity_type\x18\x03 \x01(\tR\fsecurityType\x12M\n" + + "\x11security_settings\x18\x04 \x03(\v2 .xray.common.serial.TypedMessageR\x10securitySettings\x12<\n" + + "\budpmasks\x18\n" + + " \x03(\v2 .xray.common.serial.TypedMessageR\budpmasks\x12<\n" + + "\btcpmasks\x18\v \x03(\v2 .xray.common.serial.TypedMessageR\btcpmasks\x12D\n" + + "\vquic_params\x18\f \x01(\v2#.xray.transport.internet.QuicParamsR\n" + + "quicParams\x12N\n" + + "\x0fsocket_settings\x18\x06 \x01(\v2%.xray.transport.internet.SocketConfigR\x0esocketSettings\"d\n" + + "\x06UdpHop\x12\x14\n" + + "\x05ports\x18\x01 \x03(\rR\x05ports\x12!\n" + + "\finterval_min\x18\x02 \x01(\x03R\vintervalMin\x12!\n" + + "\finterval_max\x18\x03 \x01(\x03R\vintervalMax\"\xf2\x04\n" + + "\n" + + "QuicParams\x12\x1e\n" + + "\n" + + "congestion\x18\x01 \x01(\tR\n" + + "congestion\x12\x1f\n" + + "\vbbr_profile\x18\x02 \x01(\tR\n" + + "bbrProfile\x12\x1b\n" + + "\tbrutal_up\x18\x03 \x01(\x04R\bbrutalUp\x12\x1f\n" + + "\vbrutal_down\x18\x04 \x01(\x04R\n" + + "brutalDown\x128\n" + + "\audp_hop\x18\x05 \x01(\v2\x1f.xray.transport.internet.UdpHopR\x06udpHop\x12;\n" + + "\x1ainit_stream_receive_window\x18\x06 \x01(\x04R\x17initStreamReceiveWindow\x129\n" + + "\x19max_stream_receive_window\x18\a \x01(\x04R\x16maxStreamReceiveWindow\x127\n" + + "\x18init_conn_receive_window\x18\b \x01(\x04R\x15initConnReceiveWindow\x125\n" + + "\x17max_conn_receive_window\x18\t \x01(\x04R\x14maxConnReceiveWindow\x12(\n" + + "\x10max_idle_timeout\x18\n" + + " \x01(\x03R\x0emaxIdleTimeout\x12*\n" + + "\x11keep_alive_period\x18\v \x01(\x03R\x0fkeepAlivePeriod\x12;\n" + + "\x1adisable_path_mtu_discovery\x18\f \x01(\bR\x17disablePathMtuDiscovery\x120\n" + + "\x14max_incoming_streams\x18\r \x01(\x03R\x12maxIncomingStreams\"Q\n" + + "\vProxyConfig\x12\x10\n" + + "\x03tag\x18\x01 \x01(\tR\x03tag\x120\n" + + "\x13transportLayerProxy\x18\x02 \x01(\bR\x13transportLayerProxy\"\x93\x01\n" + + "\rCustomSockopt\x12\x16\n" + + "\x06system\x18\x01 \x01(\tR\x06system\x12\x18\n" + + "\anetwork\x18\x02 \x01(\tR\anetwork\x12\x14\n" + + "\x05level\x18\x03 \x01(\tR\x05level\x12\x10\n" + + "\x03opt\x18\x04 \x01(\tR\x03opt\x12\x14\n" + + "\x05value\x18\x05 \x01(\tR\x05value\x12\x12\n" + + "\x04type\x18\x06 \x01(\tR\x04type\"\x89\t\n" + + "\fSocketConfig\x12\x12\n" + + "\x04mark\x18\x01 \x01(\x05R\x04mark\x12\x10\n" + + "\x03tfo\x18\x02 \x01(\x05R\x03tfo\x12H\n" + + "\x06tproxy\x18\x03 \x01(\x0e20.xray.transport.internet.SocketConfig.TProxyModeR\x06tproxy\x12A\n" + + "\x1dreceive_original_dest_address\x18\x04 \x01(\bR\x1areceiveOriginalDestAddress\x12!\n" + + "\fbind_address\x18\x05 \x01(\fR\vbindAddress\x12\x1b\n" + + "\tbind_port\x18\x06 \x01(\rR\bbindPort\x122\n" + + "\x15accept_proxy_protocol\x18\a \x01(\bR\x13acceptProxyProtocol\x12P\n" + + "\x0fdomain_strategy\x18\b \x01(\x0e2'.xray.transport.internet.DomainStrategyR\x0edomainStrategy\x12!\n" + + "\fdialer_proxy\x18\t \x01(\tR\vdialerProxy\x125\n" + + "\x17tcp_keep_alive_interval\x18\n" + + " \x01(\x05R\x14tcpKeepAliveInterval\x12-\n" + + "\x13tcp_keep_alive_idle\x18\v \x01(\x05R\x10tcpKeepAliveIdle\x12%\n" + + "\x0etcp_congestion\x18\f \x01(\tR\rtcpCongestion\x12\x1c\n" + + "\tinterface\x18\r \x01(\tR\tinterface\x12\x16\n" + + "\x06v6only\x18\x0e \x01(\bR\x06v6only\x12(\n" + + "\x10tcp_window_clamp\x18\x0f \x01(\x05R\x0etcpWindowClamp\x12(\n" + + "\x10tcp_user_timeout\x18\x10 \x01(\x05R\x0etcpUserTimeout\x12\x1e\n" + + "\vtcp_max_seg\x18\x11 \x01(\x05R\ttcpMaxSeg\x12\x1c\n" + + "\tpenetrate\x18\x12 \x01(\bR\tpenetrate\x12\x1b\n" + + "\ttcp_mptcp\x18\x13 \x01(\bR\btcpMptcp\x12L\n" + + "\rcustomSockopt\x18\x14 \x03(\v2&.xray.transport.internet.CustomSockoptR\rcustomSockopt\x12`\n" + + "\x15address_port_strategy\x18\x15 \x01(\x0e2,.xray.transport.internet.AddressPortStrategyR\x13addressPortStrategy\x12S\n" + + "\x0ehappy_eyeballs\x18\x16 \x01(\v2,.xray.transport.internet.HappyEyeballsConfigR\rhappyEyeballs\x125\n" + + "\x17trusted_x_forwarded_for\x18\x17 \x03(\tR\x14trustedXForwardedFor\"/\n" + + "\n" + + "TProxyMode\x12\a\n" + + "\x03Off\x10\x00\x12\n" + + "\n" + + "\x06TProxy\x10\x01\x12\f\n" + + "\bRedirect\x10\x02\"\xad\x01\n" + + "\x13HappyEyeballsConfig\x12'\n" + + "\x0fprioritize_ipv6\x18\x01 \x01(\bR\x0eprioritizeIpv6\x12\x1e\n" + + "\n" + + "interleave\x18\x02 \x01(\rR\n" + + "interleave\x12\x1f\n" + + "\vtry_delayMs\x18\x03 \x01(\x04R\n" + + "tryDelayMs\x12,\n" + + "\x12max_concurrent_try\x18\x04 \x01(\rR\x10maxConcurrentTry*\xa9\x01\n" + + "\x0eDomainStrategy\x12\t\n" + + "\x05AS_IS\x10\x00\x12\n" + + "\n" + + "\x06USE_IP\x10\x01\x12\v\n" + + "\aUSE_IP4\x10\x02\x12\v\n" + + "\aUSE_IP6\x10\x03\x12\f\n" + + "\bUSE_IP46\x10\x04\x12\f\n" + + "\bUSE_IP64\x10\x05\x12\f\n" + + "\bFORCE_IP\x10\x06\x12\r\n" + + "\tFORCE_IP4\x10\a\x12\r\n" + + "\tFORCE_IP6\x10\b\x12\x0e\n" + + "\n" + + "FORCE_IP46\x10\t\x12\x0e\n" + + "\n" + + "FORCE_IP64\x10\n" + + "*\x97\x01\n" + + "\x13AddressPortStrategy\x12\b\n" + + "\x04None\x10\x00\x12\x0f\n" + + "\vSrvPortOnly\x10\x01\x12\x12\n" + + "\x0eSrvAddressOnly\x10\x02\x12\x15\n" + + "\x11SrvPortAndAddress\x10\x03\x12\x0f\n" + + "\vTxtPortOnly\x10\x04\x12\x12\n" + + "\x0eTxtAddressOnly\x10\x05\x12\x15\n" + + "\x11TxtPortAndAddress\x10\x06Bg\n" + + "\x1bcom.xray.transport.internetP\x01Z,github.com/xtls/xray-core/transport/internet\xaa\x02\x17Xray.Transport.Internetb\x06proto3" + +var ( + file_transport_internet_config_proto_rawDescOnce sync.Once + file_transport_internet_config_proto_rawDescData []byte +) + +func file_transport_internet_config_proto_rawDescGZIP() []byte { + file_transport_internet_config_proto_rawDescOnce.Do(func() { + file_transport_internet_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_transport_internet_config_proto_rawDesc), len(file_transport_internet_config_proto_rawDesc))) + }) + return file_transport_internet_config_proto_rawDescData +} + +var file_transport_internet_config_proto_enumTypes = make([]protoimpl.EnumInfo, 3) +var file_transport_internet_config_proto_msgTypes = make([]protoimpl.MessageInfo, 8) +var file_transport_internet_config_proto_goTypes = []any{ + (DomainStrategy)(0), // 0: xray.transport.internet.DomainStrategy + (AddressPortStrategy)(0), // 1: xray.transport.internet.AddressPortStrategy + (SocketConfig_TProxyMode)(0), // 2: xray.transport.internet.SocketConfig.TProxyMode + (*TransportConfig)(nil), // 3: xray.transport.internet.TransportConfig + (*StreamConfig)(nil), // 4: xray.transport.internet.StreamConfig + (*UdpHop)(nil), // 5: xray.transport.internet.UdpHop + (*QuicParams)(nil), // 6: xray.transport.internet.QuicParams + (*ProxyConfig)(nil), // 7: xray.transport.internet.ProxyConfig + (*CustomSockopt)(nil), // 8: xray.transport.internet.CustomSockopt + (*SocketConfig)(nil), // 9: xray.transport.internet.SocketConfig + (*HappyEyeballsConfig)(nil), // 10: xray.transport.internet.HappyEyeballsConfig + (*serial.TypedMessage)(nil), // 11: xray.common.serial.TypedMessage + (*net.IPOrDomain)(nil), // 12: xray.common.net.IPOrDomain +} +var file_transport_internet_config_proto_depIdxs = []int32{ + 11, // 0: xray.transport.internet.TransportConfig.settings:type_name -> xray.common.serial.TypedMessage + 12, // 1: xray.transport.internet.StreamConfig.address:type_name -> xray.common.net.IPOrDomain + 3, // 2: xray.transport.internet.StreamConfig.transport_settings:type_name -> xray.transport.internet.TransportConfig + 11, // 3: xray.transport.internet.StreamConfig.security_settings:type_name -> xray.common.serial.TypedMessage + 11, // 4: xray.transport.internet.StreamConfig.udpmasks:type_name -> xray.common.serial.TypedMessage + 11, // 5: xray.transport.internet.StreamConfig.tcpmasks:type_name -> xray.common.serial.TypedMessage + 6, // 6: xray.transport.internet.StreamConfig.quic_params:type_name -> xray.transport.internet.QuicParams + 9, // 7: xray.transport.internet.StreamConfig.socket_settings:type_name -> xray.transport.internet.SocketConfig + 5, // 8: xray.transport.internet.QuicParams.udp_hop:type_name -> xray.transport.internet.UdpHop + 2, // 9: xray.transport.internet.SocketConfig.tproxy:type_name -> xray.transport.internet.SocketConfig.TProxyMode + 0, // 10: xray.transport.internet.SocketConfig.domain_strategy:type_name -> xray.transport.internet.DomainStrategy + 8, // 11: xray.transport.internet.SocketConfig.customSockopt:type_name -> xray.transport.internet.CustomSockopt + 1, // 12: xray.transport.internet.SocketConfig.address_port_strategy:type_name -> xray.transport.internet.AddressPortStrategy + 10, // 13: xray.transport.internet.SocketConfig.happy_eyeballs:type_name -> xray.transport.internet.HappyEyeballsConfig + 14, // [14:14] is the sub-list for method output_type + 14, // [14:14] is the sub-list for method input_type + 14, // [14:14] is the sub-list for extension type_name + 14, // [14:14] is the sub-list for extension extendee + 0, // [0:14] is the sub-list for field type_name +} + +func init() { file_transport_internet_config_proto_init() } +func file_transport_internet_config_proto_init() { + if File_transport_internet_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_config_proto_rawDesc), len(file_transport_internet_config_proto_rawDesc)), + NumEnums: 3, + NumMessages: 8, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_transport_internet_config_proto_goTypes, + DependencyIndexes: file_transport_internet_config_proto_depIdxs, + EnumInfos: file_transport_internet_config_proto_enumTypes, + MessageInfos: file_transport_internet_config_proto_msgTypes, + }.Build() + File_transport_internet_config_proto = out.File + file_transport_internet_config_proto_goTypes = nil + file_transport_internet_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/transport/internet/config.proto b/subproject/Xray-core-main/transport/internet/config.proto new file mode 100644 index 00000000..ad23f047 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/config.proto @@ -0,0 +1,171 @@ +syntax = "proto3"; + +package xray.transport.internet; +option csharp_namespace = "Xray.Transport.Internet"; +option go_package = "github.com/xtls/xray-core/transport/internet"; +option java_package = "com.xray.transport.internet"; +option java_multiple_files = true; + +import "common/serial/typed_message.proto"; +import "common/net/address.proto"; + +enum DomainStrategy { + AS_IS = 0; + USE_IP = 1; + USE_IP4 = 2; + USE_IP6 = 3; + USE_IP46 = 4; + USE_IP64 = 5; + FORCE_IP = 6; + FORCE_IP4 = 7; + FORCE_IP6 = 8; + FORCE_IP46 = 9; + FORCE_IP64 = 10; +} + +enum AddressPortStrategy { + None = 0; + SrvPortOnly = 1; + SrvAddressOnly = 2; + SrvPortAndAddress = 3; + TxtPortOnly = 4; + TxtAddressOnly = 5; + TxtPortAndAddress = 6; +} + +message TransportConfig { + // Transport protocol name. + string protocol_name = 3; + + // Specific transport protocol settings. + xray.common.serial.TypedMessage settings = 2; +} + +message StreamConfig { + xray.common.net.IPOrDomain address = 8; + uint32 port = 9; + + // Effective network. + string protocol_name = 5; + + repeated TransportConfig transport_settings = 2; + + // Type of security. Must be a message name of the settings proto. + string security_type = 3; + + // Transport security settings. They can be either TLS or REALITY. + repeated xray.common.serial.TypedMessage security_settings = 4; + + repeated xray.common.serial.TypedMessage udpmasks = 10; + repeated xray.common.serial.TypedMessage tcpmasks = 11; + + QuicParams quic_params = 12; + + SocketConfig socket_settings = 6; +} + +message UdpHop { + repeated uint32 ports = 1; + int64 interval_min = 2; + int64 interval_max = 3; +} + +message QuicParams { + string congestion = 1; + string bbr_profile = 2; + uint64 brutal_up = 3; + uint64 brutal_down = 4; + UdpHop udp_hop = 5; + uint64 init_stream_receive_window = 6; + uint64 max_stream_receive_window = 7; + uint64 init_conn_receive_window = 8; + uint64 max_conn_receive_window = 9; + int64 max_idle_timeout = 10; + int64 keep_alive_period = 11; + bool disable_path_mtu_discovery = 12; + int64 max_incoming_streams = 13; +} + +message ProxyConfig { + string tag = 1; + bool transportLayerProxy = 2; +} + +message CustomSockopt { + string system = 1; + string network = 2; + string level = 3; + string opt = 4; + string value = 5; + string type = 6; +} + +// SocketConfig is options to be applied on network sockets. +message SocketConfig { + // Mark of the connection. If non-zero, the value will be set to SO_MARK. + int32 mark = 1; + + // TFO is the state of TFO settings. + int32 tfo = 2; + + enum TProxyMode { + // TProxy is off. + Off = 0; + // TProxy mode. + TProxy = 1; + // Redirect mode. + Redirect = 2; + } + + // TProxy is for enabling TProxy socket option. + TProxyMode tproxy = 3; + + // ReceiveOriginalDestAddress is for enabling IP_RECVORIGDSTADDR socket + // option. This option is for UDP only. + bool receive_original_dest_address = 4; + + bytes bind_address = 5; + + uint32 bind_port = 6; + + bool accept_proxy_protocol = 7; + + DomainStrategy domain_strategy = 8; + + string dialer_proxy = 9; + + int32 tcp_keep_alive_interval = 10; + + int32 tcp_keep_alive_idle = 11; + + string tcp_congestion = 12; + + string interface = 13; + + bool v6only = 14; + + int32 tcp_window_clamp = 15; + + int32 tcp_user_timeout = 16; + + int32 tcp_max_seg = 17; + + bool penetrate = 18; + + bool tcp_mptcp = 19; + + repeated CustomSockopt customSockopt = 20; + + AddressPortStrategy address_port_strategy = 21; + + HappyEyeballsConfig happy_eyeballs = 22; + + repeated string trusted_x_forwarded_for = 23; +} + +message HappyEyeballsConfig { + bool prioritize_ipv6 = 1; + uint32 interleave = 2; + uint64 try_delayMs = 3; + uint32 max_concurrent_try = 4; +} diff --git a/subproject/Xray-core-main/transport/internet/dialer.go b/subproject/Xray-core-main/transport/internet/dialer.go new file mode 100644 index 00000000..9342f26f --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/dialer.go @@ -0,0 +1,288 @@ +package internet + +import ( + "context" + "fmt" + "strings" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/dice" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/net/cnc" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/features/dns" + "github.com/xtls/xray-core/features/outbound" + "github.com/xtls/xray-core/transport" + "github.com/xtls/xray-core/transport/internet/stat" + "github.com/xtls/xray-core/transport/pipe" +) + +// Dialer is the interface for dialing outbound connections. +type Dialer interface { + // Dial dials a system connection to the given destination. + Dial(ctx context.Context, destination net.Destination) (stat.Connection, error) + + // DestIpAddress returns the ip of proxy server. It is useful in case of Android client, which prepare an IP before proxy connection is established + DestIpAddress() net.IP + + // SetOutboundGateway set outbound gateway + SetOutboundGateway(ctx context.Context, ob *session.Outbound) +} + +// dialFunc is an interface to dial network connection to a specific destination. +type dialFunc func(ctx context.Context, dest net.Destination, streamSettings *MemoryStreamConfig) (stat.Connection, error) + +var transportDialerCache = make(map[string]dialFunc) + +// RegisterTransportDialer registers a Dialer with given name. +func RegisterTransportDialer(protocol string, dialer dialFunc) error { + if _, found := transportDialerCache[protocol]; found { + return errors.New(protocol, " dialer already registered").AtError() + } + transportDialerCache[protocol] = dialer + return nil +} + +// Dial dials a internet connection towards the given destination. +func Dial(ctx context.Context, dest net.Destination, streamSettings *MemoryStreamConfig) (stat.Connection, error) { + if dest.Network == net.Network_TCP { + if streamSettings == nil { + s, err := ToMemoryStreamConfig(nil) + if err != nil { + return nil, errors.New("failed to create default stream settings").Base(err) + } + streamSettings = s + } + + protocol := streamSettings.ProtocolName + dialer := transportDialerCache[protocol] + if dialer == nil { + return nil, errors.New(protocol, " dialer not registered").AtError() + } + return dialer(ctx, dest, streamSettings) + } + + if dest.Network == net.Network_UDP { + udpDialer := transportDialerCache["udp"] + if udpDialer == nil { + return nil, errors.New("UDP dialer not registered").AtError() + } + return udpDialer(ctx, dest, streamSettings) + } + + return nil, errors.New("unknown network ", dest.Network) +} + +// DestIpAddress returns the ip of proxy server. It is useful in case of Android client, which prepare an IP before proxy connection is established +func DestIpAddress() net.IP { + return effectiveSystemDialer.DestIpAddress() +} + +var ( + dnsClient dns.Client + obm outbound.Manager +) + +func LookupForIP(domain string, strategy DomainStrategy, localAddr net.Address) ([]net.IP, error) { + if dnsClient == nil { + return nil, errors.New("DNS client not initialized").AtError() + } + + ips, _, err := dnsClient.LookupIP(domain, dns.IPOption{ + IPv4Enable: (localAddr == nil && strategy.PreferIP4()) || (localAddr != nil && localAddr.Family().IsIPv4() && (strategy.PreferIP4() || strategy.FallbackIP4())), + IPv6Enable: (localAddr == nil && strategy.PreferIP6()) || (localAddr != nil && localAddr.Family().IsIPv6() && (strategy.PreferIP6() || strategy.FallbackIP6())), + }) + { // Resolve fallback + if (len(ips) == 0 || err != nil) && strategy.HasFallback() && localAddr == nil { + ips, _, err = dnsClient.LookupIP(domain, dns.IPOption{ + IPv4Enable: strategy.FallbackIP4(), + IPv6Enable: strategy.FallbackIP6(), + }) + } + } + + if err == nil && len(ips) == 0 { + return nil, dns.ErrEmptyResponse + } + return ips, err +} + +func redirect(ctx context.Context, dst net.Destination, obt string, h outbound.Handler) net.Conn { + errors.LogInfo(ctx, "redirecting request "+dst.String()+" to "+obt) + outbounds := session.OutboundsFromContext(ctx) + ctx = session.ContextWithOutbounds(ctx, append(outbounds, &session.Outbound{ + Target: dst, + Gateway: nil, + Tag: obt, + })) // add another outbound in session ctx + + ur, uw := pipe.New(pipe.OptionsFromContext(ctx)...) + dr, dw := pipe.New(pipe.OptionsFromContext(ctx)...) + + go h.Dispatch(context.WithoutCancel(ctx), &transport.Link{Reader: ur, Writer: dw}) + var readerOpt cnc.ConnectionOption + if dst.Network == net.Network_TCP { + readerOpt = cnc.ConnectionOutputMulti(dr) + } else { + readerOpt = cnc.ConnectionOutputMultiUDP(dr) + } + nc := cnc.NewConnection( + cnc.ConnectionInputMulti(uw), + readerOpt, + cnc.ConnectionOnClose(common.ChainedClosable{uw, dw}), + ) + return nc + +} + +func checkAddressPortStrategy(ctx context.Context, dest net.Destination, sockopt *SocketConfig) (*net.Destination, error) { + if sockopt.AddressPortStrategy == AddressPortStrategy_None { + return nil, nil + } + newDest := dest + var OverridePort, OverrideAddress bool + var OverrideBy string + switch sockopt.AddressPortStrategy { + case AddressPortStrategy_SrvPortOnly: + OverridePort = true + OverrideAddress = false + OverrideBy = "srv" + case AddressPortStrategy_SrvAddressOnly: + OverridePort = false + OverrideAddress = true + OverrideBy = "srv" + case AddressPortStrategy_SrvPortAndAddress: + OverridePort = true + OverrideAddress = true + OverrideBy = "srv" + case AddressPortStrategy_TxtPortOnly: + OverridePort = true + OverrideAddress = false + OverrideBy = "txt" + case AddressPortStrategy_TxtAddressOnly: + OverridePort = false + OverrideAddress = true + OverrideBy = "txt" + case AddressPortStrategy_TxtPortAndAddress: + OverridePort = true + OverrideAddress = true + OverrideBy = "txt" + default: + return nil, errors.New("unknown AddressPortStrategy") + } + + if !dest.Address.Family().IsDomain() { + return nil, nil + } + + if OverrideBy == "srv" { + errors.LogDebug(ctx, "query SRV record for "+dest.Address.String()) + parts := strings.SplitN(dest.Address.String(), ".", 3) + if len(parts) != 3 { + return nil, errors.New("invalid address format", dest.Address.String()) + } + _, srvRecords, err := net.DefaultResolver.LookupSRV(context.Background(), parts[0][1:], parts[1][1:], parts[2]) + if err != nil { + return nil, errors.New("failed to lookup SRV record").Base(err) + } + errors.LogDebug(ctx, "SRV record: "+fmt.Sprintf("addr=%s, port=%d, priority=%d, weight=%d", srvRecords[0].Target, srvRecords[0].Port, srvRecords[0].Priority, srvRecords[0].Weight)) + if OverridePort { + newDest.Port = net.Port(srvRecords[0].Port) + } + if OverrideAddress { + newDest.Address = net.ParseAddress(srvRecords[0].Target) + } + return &newDest, nil + } + if OverrideBy == "txt" { + errors.LogDebug(ctx, "query TXT record for "+dest.Address.String()) + txtRecords, err := net.DefaultResolver.LookupTXT(ctx, dest.Address.String()) + if err != nil { + errors.LogError(ctx, "failed to lookup SRV record: "+err.Error()) + return nil, errors.New("failed to lookup SRV record").Base(err) + } + for _, txtRecord := range txtRecords { + errors.LogDebug(ctx, "TXT record: "+txtRecord) + addr_s, port_s, _ := net.SplitHostPort(string(txtRecord)) + addr := net.ParseAddress(addr_s) + port, err := net.PortFromString(port_s) + if err != nil { + continue + } + + if OverridePort { + newDest.Port = port + } + if OverrideAddress { + newDest.Address = addr + } + return &newDest, nil + } + } + return nil, nil +} + +// DialSystem calls system dialer to create a network connection. +func DialSystem(ctx context.Context, dest net.Destination, sockopt *SocketConfig) (net.Conn, error) { + var src net.Address + outbounds := session.OutboundsFromContext(ctx) + var outboundName string + var origTargetAddr net.Address + if len(outbounds) > 0 { + ob := outbounds[len(outbounds)-1] + if sockopt == nil || len(sockopt.DialerProxy) == 0 { + src = ob.Gateway + } + outboundName = ob.Name + origTargetAddr = ob.OriginalTarget.Address + if origTargetAddr == nil { + origTargetAddr = ob.Target.Address + } + } + if sockopt == nil { + return effectiveSystemDialer.Dial(ctx, src, dest, sockopt) + } + + if newDest, err := checkAddressPortStrategy(ctx, dest, sockopt); err == nil && newDest != nil { + errors.LogInfo(ctx, "replace destination with "+newDest.String()) + dest = *newDest + } + + if sockopt.DomainStrategy.HasStrategy() && dest.Address.Family().IsDomain() { + finalStrategy := sockopt.DomainStrategy + if outboundName == "freedom" && dest.Network == net.Network_UDP && origTargetAddr != nil && src == nil { + finalStrategy = finalStrategy.GetDynamicStrategy(origTargetAddr.Family()) + } + ips, err := LookupForIP(dest.Address.Domain(), finalStrategy, src) + if err != nil { + errors.LogErrorInner(ctx, err, "failed to resolve ip") + if sockopt.DomainStrategy.ForceIP() { + return nil, err + } + } else if sockopt.HappyEyeballs == nil || sockopt.HappyEyeballs.TryDelayMs == 0 || sockopt.HappyEyeballs.MaxConcurrentTry == 0 || len(ips) < 2 || len(sockopt.DialerProxy) > 0 || dest.Network != net.Network_TCP { + dest.Address = net.IPAddress(ips[dice.Roll(len(ips))]) + errors.LogInfo(ctx, "replace destination with "+dest.String()) + } else { + return TcpRaceDial(ctx, src, ips, dest.Port, sockopt, dest.Address.String()) + } + } + + if len(sockopt.DialerProxy) > 0 { + if obm == nil { + return nil, errors.New("there is no outbound manager for dialerProxy").AtError() + } + h := obm.GetHandler(sockopt.DialerProxy) + if h == nil { + return nil, errors.New("there is no outbound handler for dialerProxy").AtError() + } + return redirect(ctx, dest, sockopt.DialerProxy, h), nil + } + + return effectiveSystemDialer.Dial(ctx, src, dest, sockopt) +} + +func InitSystemDialer(dc dns.Client, om outbound.Manager) { + dnsClient = dc + obm = om +} diff --git a/subproject/Xray-core-main/transport/internet/dialer_test.go b/subproject/Xray-core-main/transport/internet/dialer_test.go new file mode 100644 index 00000000..c20f7a6e --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/dialer_test.go @@ -0,0 +1,26 @@ +package internet_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/testing/servers/tcp" + . "github.com/xtls/xray-core/transport/internet" +) + +func TestDialWithLocalAddr(t *testing.T) { + server := &tcp.Server{} + dest, err := server.Start() + common.Must(err) + defer server.Close() + + conn, err := DialSystem(context.Background(), net.TCPDestination(net.LocalHostIP, dest.Port), nil) + common.Must(err) + if r := cmp.Diff(conn.RemoteAddr().String(), "127.0.0.1:"+dest.Port.String()); r != "" { + t.Error(r) + } + conn.Close() +} diff --git a/subproject/Xray-core-main/transport/internet/filelocker.go b/subproject/Xray-core-main/transport/internet/filelocker.go new file mode 100644 index 00000000..33dec736 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/filelocker.go @@ -0,0 +1,11 @@ +package internet + +import ( + "os" +) + +// FileLocker is UDS access lock +type FileLocker struct { + path string + file *os.File +} diff --git a/subproject/Xray-core-main/transport/internet/filelocker_other.go b/subproject/Xray-core-main/transport/internet/filelocker_other.go new file mode 100644 index 00000000..36d937c1 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/filelocker_other.go @@ -0,0 +1,39 @@ +//go:build !windows +// +build !windows + +package internet + +import ( + "context" + "os" + + "github.com/xtls/xray-core/common/errors" + "golang.org/x/sys/unix" +) + +// Acquire lock +func (fl *FileLocker) Acquire() error { + f, err := os.Create(fl.path) + if err != nil { + return err + } + if err := unix.Flock(int(f.Fd()), unix.LOCK_EX); err != nil { + f.Close() + return errors.New("failed to lock file: ", fl.path).Base(err) + } + fl.file = f + return nil +} + +// Release lock +func (fl *FileLocker) Release() { + if err := unix.Flock(int(fl.file.Fd()), unix.LOCK_UN); err != nil { + errors.LogInfoInner(context.Background(), err, "failed to unlock file: ", fl.path) + } + if err := fl.file.Close(); err != nil { + errors.LogInfoInner(context.Background(), err, "failed to close file: ", fl.path) + } + if err := os.Remove(fl.path); err != nil { + errors.LogInfoInner(context.Background(), err, "failed to remove file: ", fl.path) + } +} diff --git a/subproject/Xray-core-main/transport/internet/filelocker_windows.go b/subproject/Xray-core-main/transport/internet/filelocker_windows.go new file mode 100644 index 00000000..adbe2e8e --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/filelocker_windows.go @@ -0,0 +1,11 @@ +package internet + +// Acquire lock +func (fl *FileLocker) Acquire() error { + return nil +} + +// Release lock +func (fl *FileLocker) Release() { + return +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/finalmask.go b/subproject/Xray-core-main/transport/internet/finalmask/finalmask.go new file mode 100644 index 00000000..a98aa3fc --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/finalmask.go @@ -0,0 +1,276 @@ +package finalmask + +import ( + "context" + "net" + "sync" + + "github.com/xtls/xray-core/common/errors" +) + +type Udpmask interface { + UDP() + + WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) + WrapPacketConnServer(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) +} + +type UdpmaskManager struct { + udpmasks []Udpmask +} + +func NewUdpmaskManager(udpmasks []Udpmask) *UdpmaskManager { + return &UdpmaskManager{ + udpmasks: udpmasks, + } +} + +func (m *UdpmaskManager) WrapPacketConnClient(raw net.PacketConn) (net.PacketConn, error) { + var sizes []int + var conns []net.PacketConn + for i, mask := range m.udpmasks { + if _, ok := mask.(headerConn); ok { + conn, err := mask.WrapPacketConnClient(nil, i, len(m.udpmasks)-1) + if err != nil { + return nil, err + } + sizes = append(sizes, conn.(headerSize).Size()) + conns = append(conns, conn) + } else { + if len(conns) > 0 { + raw = &headerManagerConn{sizes: sizes, conns: conns, PacketConn: raw} + sizes = nil + conns = nil + } + var err error + raw, err = mask.WrapPacketConnClient(raw, i, len(m.udpmasks)-1) + if err != nil { + return nil, err + } + } + } + + if len(conns) > 0 { + raw = &headerManagerConn{sizes: sizes, conns: conns, PacketConn: raw} + sizes = nil + conns = nil + } + return raw, nil +} + +func (m *UdpmaskManager) WrapPacketConnServer(raw net.PacketConn) (net.PacketConn, error) { + var sizes []int + var conns []net.PacketConn + for i, mask := range m.udpmasks { + if _, ok := mask.(headerConn); ok { + conn, err := mask.WrapPacketConnServer(nil, i, len(m.udpmasks)-1) + if err != nil { + return nil, err + } + sizes = append(sizes, conn.(headerSize).Size()) + conns = append(conns, conn) + } else { + if len(conns) > 0 { + raw = &headerManagerConn{sizes: sizes, conns: conns, PacketConn: raw} + sizes = nil + conns = nil + } + var err error + raw, err = mask.WrapPacketConnServer(raw, i, len(m.udpmasks)-1) + if err != nil { + return nil, err + } + } + } + + if len(conns) > 0 { + raw = &headerManagerConn{sizes: sizes, conns: conns, PacketConn: raw} + sizes = nil + conns = nil + } + return raw, nil +} + +const ( + UDPSize = 4096 +) + +type headerConn interface { + HeaderConn() +} + +type headerSize interface { + Size() int +} + +type headerManagerConn struct { + sizes []int + conns []net.PacketConn + net.PacketConn + m sync.Mutex + writeBuf [UDPSize]byte +} + +func (c *headerManagerConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { + buf := p + if len(buf) < UDPSize { + buf = make([]byte, UDPSize) + } + + n, addr, err = c.PacketConn.ReadFrom(buf) + if n == 0 || err != nil { + return 0, addr, err + } + newBuf := buf[:n] + + sum := 0 + for _, size := range c.sizes { + sum += size + } + + if n < sum { + errors.LogDebug(context.Background(), addr, " mask read err short length") + return 0, addr, nil + } + + for i := range c.conns { + n, _, err = c.conns[i].ReadFrom(newBuf) + if n == 0 || err != nil { + errors.LogDebug(context.Background(), addr, " mask read err ", err) + return 0, addr, nil + } + newBuf = newBuf[c.sizes[i] : n+c.sizes[i]] + } + + if len(p) < n { + errors.LogDebug(context.Background(), addr, " mask read err short buffer") + return 0, addr, nil + } + + copy(p, buf[sum:sum+n]) + + return n, addr, nil +} + +func (c *headerManagerConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { + c.m.Lock() + defer c.m.Unlock() + + sum := 0 + for _, size := range c.sizes { + sum += size + } + + if sum+len(p) > UDPSize { + errors.LogDebug(context.Background(), addr, " mask write err short write") + return 0, nil + } + + n = copy(c.writeBuf[sum:], p) + + for i := len(c.conns) - 1; i >= 0; i-- { + n, err = c.conns[i].WriteTo(c.writeBuf[sum-c.sizes[i]:n+sum], nil) + if n == 0 || err != nil { + errors.LogDebug(context.Background(), addr, " mask write err ", err) + return 0, nil + } + sum -= c.sizes[i] + } + + n, err = c.PacketConn.WriteTo(c.writeBuf[:n], addr) + if n == 0 || err != nil { + return n, err + } + + return len(p), nil +} + +type Tcpmask interface { + TCP() + + WrapConnClient(net.Conn) (net.Conn, error) + WrapConnServer(net.Conn) (net.Conn, error) +} + +type TcpmaskManager struct { + tcpmasks []Tcpmask +} + +func NewTcpmaskManager(tcpmasks []Tcpmask) *TcpmaskManager { + return &TcpmaskManager{ + tcpmasks: tcpmasks, + } +} + +func (m *TcpmaskManager) WrapConnClient(raw net.Conn) (net.Conn, error) { + var err error + for _, mask := range m.tcpmasks { + raw, err = mask.WrapConnClient(raw) + if err != nil { + return nil, err + } + } + return raw, nil +} + +func (m *TcpmaskManager) WrapConnServer(raw net.Conn) (net.Conn, error) { + var err error + for _, mask := range m.tcpmasks { + raw, err = mask.WrapConnServer(raw) + if err != nil { + return nil, err + } + } + return raw, nil +} + +func (m *TcpmaskManager) WrapListener(l net.Listener) (net.Listener, error) { + return NewTcpListener(m, l) +} + +type tcpListener struct { + m *TcpmaskManager + net.Listener +} + +func NewTcpListener(m *TcpmaskManager, l net.Listener) (net.Listener, error) { + return &tcpListener{ + m: m, + Listener: l, + }, nil +} + +func (l *tcpListener) Accept() (net.Conn, error) { + conn, err := l.Listener.Accept() + if err != nil { + return conn, err + } + + newConn, err := l.m.WrapConnServer(conn) + if err != nil { + errors.LogDebugInner(context.Background(), err, "mask err") + // conn.Close() + return conn, nil + } + + return newConn, nil +} + +type TcpMaskConn interface { + TcpMaskConn() + RawConn() net.Conn + Splice() bool +} + +func UnwrapTcpMask(conn net.Conn) net.Conn { + for { + if v, ok := conn.(TcpMaskConn); ok { + if !v.Splice() { + return conn + } + conn = v.RawConn() + } else { + return conn + } + } +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/fragment/config.go b/subproject/Xray-core-main/transport/internet/finalmask/fragment/config.go new file mode 100644 index 00000000..165f11ca --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/fragment/config.go @@ -0,0 +1,14 @@ +package fragment + +import "net" + +func (c *Config) TCP() { +} + +func (c *Config) WrapConnClient(raw net.Conn) (net.Conn, error) { + return NewConnClient(c, raw, false) +} + +func (c *Config) WrapConnServer(raw net.Conn) (net.Conn, error) { + return NewConnServer(c, raw, true) +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/fragment/config.pb.go b/subproject/Xray-core-main/transport/internet/finalmask/fragment/config.pb.go new file mode 100644 index 00000000..c8660f5e --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/fragment/config.pb.go @@ -0,0 +1,189 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: transport/internet/finalmask/fragment/config.proto + +package fragment + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + PacketsFrom int64 `protobuf:"varint,1,opt,name=packets_from,json=packetsFrom,proto3" json:"packets_from,omitempty"` + PacketsTo int64 `protobuf:"varint,2,opt,name=packets_to,json=packetsTo,proto3" json:"packets_to,omitempty"` + LengthMin int64 `protobuf:"varint,3,opt,name=length_min,json=lengthMin,proto3" json:"length_min,omitempty"` + LengthMax int64 `protobuf:"varint,4,opt,name=length_max,json=lengthMax,proto3" json:"length_max,omitempty"` + DelayMin int64 `protobuf:"varint,5,opt,name=delay_min,json=delayMin,proto3" json:"delay_min,omitempty"` + DelayMax int64 `protobuf:"varint,6,opt,name=delay_max,json=delayMax,proto3" json:"delay_max,omitempty"` + MaxSplitMin int64 `protobuf:"varint,7,opt,name=max_split_min,json=maxSplitMin,proto3" json:"max_split_min,omitempty"` + MaxSplitMax int64 `protobuf:"varint,8,opt,name=max_split_max,json=maxSplitMax,proto3" json:"max_split_max,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_transport_internet_finalmask_fragment_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_finalmask_fragment_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_transport_internet_finalmask_fragment_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Config) GetPacketsFrom() int64 { + if x != nil { + return x.PacketsFrom + } + return 0 +} + +func (x *Config) GetPacketsTo() int64 { + if x != nil { + return x.PacketsTo + } + return 0 +} + +func (x *Config) GetLengthMin() int64 { + if x != nil { + return x.LengthMin + } + return 0 +} + +func (x *Config) GetLengthMax() int64 { + if x != nil { + return x.LengthMax + } + return 0 +} + +func (x *Config) GetDelayMin() int64 { + if x != nil { + return x.DelayMin + } + return 0 +} + +func (x *Config) GetDelayMax() int64 { + if x != nil { + return x.DelayMax + } + return 0 +} + +func (x *Config) GetMaxSplitMin() int64 { + if x != nil { + return x.MaxSplitMin + } + return 0 +} + +func (x *Config) GetMaxSplitMax() int64 { + if x != nil { + return x.MaxSplitMax + } + return 0 +} + +var File_transport_internet_finalmask_fragment_config_proto protoreflect.FileDescriptor + +const file_transport_internet_finalmask_fragment_config_proto_rawDesc = "" + + "\n" + + "2transport/internet/finalmask/fragment/config.proto\x12*xray.transport.internet.finalmask.fragment\"\x8a\x02\n" + + "\x06Config\x12!\n" + + "\fpackets_from\x18\x01 \x01(\x03R\vpacketsFrom\x12\x1d\n" + + "\n" + + "packets_to\x18\x02 \x01(\x03R\tpacketsTo\x12\x1d\n" + + "\n" + + "length_min\x18\x03 \x01(\x03R\tlengthMin\x12\x1d\n" + + "\n" + + "length_max\x18\x04 \x01(\x03R\tlengthMax\x12\x1b\n" + + "\tdelay_min\x18\x05 \x01(\x03R\bdelayMin\x12\x1b\n" + + "\tdelay_max\x18\x06 \x01(\x03R\bdelayMax\x12\"\n" + + "\rmax_split_min\x18\a \x01(\x03R\vmaxSplitMin\x12\"\n" + + "\rmax_split_max\x18\b \x01(\x03R\vmaxSplitMaxB\xa0\x01\n" + + ".com.xray.transport.internet.finalmask.fragmentP\x01Z?github.com/xtls/xray-core/transport/internet/finalmask/fragment\xaa\x02*Xray.Transport.Internet.Finalmask.Fragmentb\x06proto3" + +var ( + file_transport_internet_finalmask_fragment_config_proto_rawDescOnce sync.Once + file_transport_internet_finalmask_fragment_config_proto_rawDescData []byte +) + +func file_transport_internet_finalmask_fragment_config_proto_rawDescGZIP() []byte { + file_transport_internet_finalmask_fragment_config_proto_rawDescOnce.Do(func() { + file_transport_internet_finalmask_fragment_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_fragment_config_proto_rawDesc), len(file_transport_internet_finalmask_fragment_config_proto_rawDesc))) + }) + return file_transport_internet_finalmask_fragment_config_proto_rawDescData +} + +var file_transport_internet_finalmask_fragment_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_transport_internet_finalmask_fragment_config_proto_goTypes = []any{ + (*Config)(nil), // 0: xray.transport.internet.finalmask.fragment.Config +} +var file_transport_internet_finalmask_fragment_config_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_transport_internet_finalmask_fragment_config_proto_init() } +func file_transport_internet_finalmask_fragment_config_proto_init() { + if File_transport_internet_finalmask_fragment_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_fragment_config_proto_rawDesc), len(file_transport_internet_finalmask_fragment_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_transport_internet_finalmask_fragment_config_proto_goTypes, + DependencyIndexes: file_transport_internet_finalmask_fragment_config_proto_depIdxs, + MessageInfos: file_transport_internet_finalmask_fragment_config_proto_msgTypes, + }.Build() + File_transport_internet_finalmask_fragment_config_proto = out.File + file_transport_internet_finalmask_fragment_config_proto_goTypes = nil + file_transport_internet_finalmask_fragment_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/fragment/config.proto b/subproject/Xray-core-main/transport/internet/finalmask/fragment/config.proto new file mode 100644 index 00000000..62aaf186 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/fragment/config.proto @@ -0,0 +1,18 @@ +syntax = "proto3"; + +package xray.transport.internet.finalmask.fragment; +option csharp_namespace = "Xray.Transport.Internet.Finalmask.Fragment"; +option go_package = "github.com/xtls/xray-core/transport/internet/finalmask/fragment"; +option java_package = "com.xray.transport.internet.finalmask.fragment"; +option java_multiple_files = true; + +message Config { + int64 packets_from = 1; + int64 packets_to = 2; + int64 length_min = 3; + int64 length_max = 4; + int64 delay_min = 5; + int64 delay_max = 6; + int64 max_split_min = 7; + int64 max_split_max = 8; +} \ No newline at end of file diff --git a/subproject/Xray-core-main/transport/internet/finalmask/fragment/conn.go b/subproject/Xray-core-main/transport/internet/finalmask/fragment/conn.go new file mode 100644 index 00000000..91822ace --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/fragment/conn.go @@ -0,0 +1,125 @@ +package fragment + +import ( + "net" + "time" + + "github.com/xtls/xray-core/common/crypto" +) + +type fragmentConn struct { + net.Conn + config *Config + count uint64 + + server bool +} + +func NewConnClient(c *Config, raw net.Conn, server bool) (net.Conn, error) { + conn := &fragmentConn{ + Conn: raw, + config: c, + + server: server, + } + + return conn, nil +} + +func NewConnServer(c *Config, raw net.Conn, server bool) (net.Conn, error) { + return NewConnClient(c, raw, server) +} + +func (c *fragmentConn) TcpMaskConn() {} + +func (c *fragmentConn) RawConn() net.Conn { + return c.Conn +} + +func (c *fragmentConn) Splice() bool { + if c.server { + return false + } + return true +} + +func (c *fragmentConn) Write(p []byte) (n int, err error) { + c.count++ + + if c.config.PacketsFrom == 0 && c.config.PacketsTo == 1 { + if c.count != 1 || len(p) <= 5 || p[0] != 22 { + return c.Conn.Write(p) + } + recordLen := 5 + ((int(p[3]) << 8) | int(p[4])) + if len(p) < recordLen { + return c.Conn.Write(p) + } + data := p[5:recordLen] + buff := make([]byte, 2048) + var hello []byte + maxSplit := crypto.RandBetween(c.config.MaxSplitMin, c.config.MaxSplitMax) + var splitNum int64 + for from := 0; ; { + to := from + int(crypto.RandBetween(c.config.LengthMin, c.config.LengthMax)) + splitNum++ + if to > len(data) || (maxSplit > 0 && splitNum >= maxSplit) { + to = len(data) + } + l := to - from + if 5+l > len(buff) { + buff = make([]byte, 5+l) + } + copy(buff[:3], p) + copy(buff[5:], data[from:to]) + from = to + buff[3] = byte(l >> 8) + buff[4] = byte(l) + if c.config.DelayMax == 0 { + hello = append(hello, buff[:5+l]...) + } else { + _, err := c.Conn.Write(buff[:5+l]) + time.Sleep(time.Duration(crypto.RandBetween(c.config.DelayMin, c.config.DelayMax)) * time.Millisecond) + if err != nil { + return 0, err + } + } + if from == len(data) { + if len(hello) > 0 { + _, err := c.Conn.Write(hello) + if err != nil { + return 0, err + } + } + if len(p) > recordLen { + n, err := c.Conn.Write(p[recordLen:]) + if err != nil { + return recordLen + n, err + } + } + return len(p), nil + } + } + } + + if c.config.PacketsFrom != 0 && (c.count < uint64(c.config.PacketsFrom) || c.count > uint64(c.config.PacketsTo)) { + return c.Conn.Write(p) + } + maxSplit := crypto.RandBetween(c.config.MaxSplitMin, c.config.MaxSplitMax) + var splitNum int64 + for from := 0; ; { + to := from + int(crypto.RandBetween(c.config.LengthMin, c.config.LengthMax)) + splitNum++ + if to > len(p) || (maxSplit > 0 && splitNum >= maxSplit) { + to = len(p) + } + n, err := c.Conn.Write(p[from:to]) + from += n + if err != nil { + return from, err + } + time.Sleep(time.Duration(crypto.RandBetween(c.config.DelayMin, c.config.DelayMax)) * time.Millisecond) + if from >= len(p) { + return from, nil + } + } +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/header/custom/config.go b/subproject/Xray-core-main/transport/internet/finalmask/header/custom/config.go new file mode 100644 index 00000000..be094f21 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/header/custom/config.go @@ -0,0 +1,30 @@ +package custom + +import ( + "net" +) + +func (c *TCPConfig) TCP() { +} + +func (c *TCPConfig) WrapConnClient(raw net.Conn) (net.Conn, error) { + return NewConnClientTCP(c, raw) +} + +func (c *TCPConfig) WrapConnServer(raw net.Conn) (net.Conn, error) { + return NewConnServerTCP(c, raw) +} + +func (c *UDPConfig) UDP() { +} + +func (c *UDPConfig) WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) { + return NewConnClientUDP(c, raw) +} + +func (c *UDPConfig) WrapPacketConnServer(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) { + return NewConnServerUDP(c, raw) +} + +func (c *UDPConfig) HeaderConn() { +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/header/custom/config.pb.go b/subproject/Xray-core-main/transport/internet/finalmask/header/custom/config.pb.go new file mode 100644 index 00000000..c8db3e1a --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/header/custom/config.pb.go @@ -0,0 +1,416 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: transport/internet/finalmask/header/custom/config.proto + +package custom + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type TCPItem struct { + state protoimpl.MessageState `protogen:"open.v1"` + DelayMin int64 `protobuf:"varint,1,opt,name=delay_min,json=delayMin,proto3" json:"delay_min,omitempty"` + DelayMax int64 `protobuf:"varint,2,opt,name=delay_max,json=delayMax,proto3" json:"delay_max,omitempty"` + Rand int32 `protobuf:"varint,3,opt,name=rand,proto3" json:"rand,omitempty"` + RandMin int32 `protobuf:"varint,4,opt,name=rand_min,json=randMin,proto3" json:"rand_min,omitempty"` + RandMax int32 `protobuf:"varint,5,opt,name=rand_max,json=randMax,proto3" json:"rand_max,omitempty"` + Packet []byte `protobuf:"bytes,6,opt,name=packet,proto3" json:"packet,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TCPItem) Reset() { + *x = TCPItem{} + mi := &file_transport_internet_finalmask_header_custom_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TCPItem) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TCPItem) ProtoMessage() {} + +func (x *TCPItem) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_finalmask_header_custom_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TCPItem.ProtoReflect.Descriptor instead. +func (*TCPItem) Descriptor() ([]byte, []int) { + return file_transport_internet_finalmask_header_custom_config_proto_rawDescGZIP(), []int{0} +} + +func (x *TCPItem) GetDelayMin() int64 { + if x != nil { + return x.DelayMin + } + return 0 +} + +func (x *TCPItem) GetDelayMax() int64 { + if x != nil { + return x.DelayMax + } + return 0 +} + +func (x *TCPItem) GetRand() int32 { + if x != nil { + return x.Rand + } + return 0 +} + +func (x *TCPItem) GetRandMin() int32 { + if x != nil { + return x.RandMin + } + return 0 +} + +func (x *TCPItem) GetRandMax() int32 { + if x != nil { + return x.RandMax + } + return 0 +} + +func (x *TCPItem) GetPacket() []byte { + if x != nil { + return x.Packet + } + return nil +} + +type TCPSequence struct { + state protoimpl.MessageState `protogen:"open.v1"` + Sequence []*TCPItem `protobuf:"bytes,1,rep,name=sequence,proto3" json:"sequence,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TCPSequence) Reset() { + *x = TCPSequence{} + mi := &file_transport_internet_finalmask_header_custom_config_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TCPSequence) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TCPSequence) ProtoMessage() {} + +func (x *TCPSequence) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_finalmask_header_custom_config_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TCPSequence.ProtoReflect.Descriptor instead. +func (*TCPSequence) Descriptor() ([]byte, []int) { + return file_transport_internet_finalmask_header_custom_config_proto_rawDescGZIP(), []int{1} +} + +func (x *TCPSequence) GetSequence() []*TCPItem { + if x != nil { + return x.Sequence + } + return nil +} + +type TCPConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + Clients []*TCPSequence `protobuf:"bytes,1,rep,name=clients,proto3" json:"clients,omitempty"` + Servers []*TCPSequence `protobuf:"bytes,2,rep,name=servers,proto3" json:"servers,omitempty"` + Errors []*TCPSequence `protobuf:"bytes,3,rep,name=errors,proto3" json:"errors,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TCPConfig) Reset() { + *x = TCPConfig{} + mi := &file_transport_internet_finalmask_header_custom_config_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TCPConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TCPConfig) ProtoMessage() {} + +func (x *TCPConfig) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_finalmask_header_custom_config_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TCPConfig.ProtoReflect.Descriptor instead. +func (*TCPConfig) Descriptor() ([]byte, []int) { + return file_transport_internet_finalmask_header_custom_config_proto_rawDescGZIP(), []int{2} +} + +func (x *TCPConfig) GetClients() []*TCPSequence { + if x != nil { + return x.Clients + } + return nil +} + +func (x *TCPConfig) GetServers() []*TCPSequence { + if x != nil { + return x.Servers + } + return nil +} + +func (x *TCPConfig) GetErrors() []*TCPSequence { + if x != nil { + return x.Errors + } + return nil +} + +type UDPItem struct { + state protoimpl.MessageState `protogen:"open.v1"` + Rand int32 `protobuf:"varint,1,opt,name=rand,proto3" json:"rand,omitempty"` + RandMin int32 `protobuf:"varint,2,opt,name=rand_min,json=randMin,proto3" json:"rand_min,omitempty"` + RandMax int32 `protobuf:"varint,3,opt,name=rand_max,json=randMax,proto3" json:"rand_max,omitempty"` + Packet []byte `protobuf:"bytes,4,opt,name=packet,proto3" json:"packet,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UDPItem) Reset() { + *x = UDPItem{} + mi := &file_transport_internet_finalmask_header_custom_config_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UDPItem) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UDPItem) ProtoMessage() {} + +func (x *UDPItem) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_finalmask_header_custom_config_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UDPItem.ProtoReflect.Descriptor instead. +func (*UDPItem) Descriptor() ([]byte, []int) { + return file_transport_internet_finalmask_header_custom_config_proto_rawDescGZIP(), []int{3} +} + +func (x *UDPItem) GetRand() int32 { + if x != nil { + return x.Rand + } + return 0 +} + +func (x *UDPItem) GetRandMin() int32 { + if x != nil { + return x.RandMin + } + return 0 +} + +func (x *UDPItem) GetRandMax() int32 { + if x != nil { + return x.RandMax + } + return 0 +} + +func (x *UDPItem) GetPacket() []byte { + if x != nil { + return x.Packet + } + return nil +} + +type UDPConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + Client []*UDPItem `protobuf:"bytes,1,rep,name=client,proto3" json:"client,omitempty"` + Server []*UDPItem `protobuf:"bytes,2,rep,name=server,proto3" json:"server,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UDPConfig) Reset() { + *x = UDPConfig{} + mi := &file_transport_internet_finalmask_header_custom_config_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UDPConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UDPConfig) ProtoMessage() {} + +func (x *UDPConfig) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_finalmask_header_custom_config_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UDPConfig.ProtoReflect.Descriptor instead. +func (*UDPConfig) Descriptor() ([]byte, []int) { + return file_transport_internet_finalmask_header_custom_config_proto_rawDescGZIP(), []int{4} +} + +func (x *UDPConfig) GetClient() []*UDPItem { + if x != nil { + return x.Client + } + return nil +} + +func (x *UDPConfig) GetServer() []*UDPItem { + if x != nil { + return x.Server + } + return nil +} + +var File_transport_internet_finalmask_header_custom_config_proto protoreflect.FileDescriptor + +const file_transport_internet_finalmask_header_custom_config_proto_rawDesc = "" + + "\n" + + "7transport/internet/finalmask/header/custom/config.proto\x12/xray.transport.internet.finalmask.header.custom\"\xa5\x01\n" + + "\aTCPItem\x12\x1b\n" + + "\tdelay_min\x18\x01 \x01(\x03R\bdelayMin\x12\x1b\n" + + "\tdelay_max\x18\x02 \x01(\x03R\bdelayMax\x12\x12\n" + + "\x04rand\x18\x03 \x01(\x05R\x04rand\x12\x19\n" + + "\brand_min\x18\x04 \x01(\x05R\arandMin\x12\x19\n" + + "\brand_max\x18\x05 \x01(\x05R\arandMax\x12\x16\n" + + "\x06packet\x18\x06 \x01(\fR\x06packet\"c\n" + + "\vTCPSequence\x12T\n" + + "\bsequence\x18\x01 \x03(\v28.xray.transport.internet.finalmask.header.custom.TCPItemR\bsequence\"\x91\x02\n" + + "\tTCPConfig\x12V\n" + + "\aclients\x18\x01 \x03(\v2<.xray.transport.internet.finalmask.header.custom.TCPSequenceR\aclients\x12V\n" + + "\aservers\x18\x02 \x03(\v2<.xray.transport.internet.finalmask.header.custom.TCPSequenceR\aservers\x12T\n" + + "\x06errors\x18\x03 \x03(\v2<.xray.transport.internet.finalmask.header.custom.TCPSequenceR\x06errors\"k\n" + + "\aUDPItem\x12\x12\n" + + "\x04rand\x18\x01 \x01(\x05R\x04rand\x12\x19\n" + + "\brand_min\x18\x02 \x01(\x05R\arandMin\x12\x19\n" + + "\brand_max\x18\x03 \x01(\x05R\arandMax\x12\x16\n" + + "\x06packet\x18\x04 \x01(\fR\x06packet\"\xaf\x01\n" + + "\tUDPConfig\x12P\n" + + "\x06client\x18\x01 \x03(\v28.xray.transport.internet.finalmask.header.custom.UDPItemR\x06client\x12P\n" + + "\x06server\x18\x02 \x03(\v28.xray.transport.internet.finalmask.header.custom.UDPItemR\x06serverB\xaf\x01\n" + + "3com.xray.transport.internet.finalmask.header.customP\x01ZDgithub.com/xtls/xray-core/transport/internet/finalmask/header/custom\xaa\x02/Xray.Transport.Internet.Finalmask.Header.Customb\x06proto3" + +var ( + file_transport_internet_finalmask_header_custom_config_proto_rawDescOnce sync.Once + file_transport_internet_finalmask_header_custom_config_proto_rawDescData []byte +) + +func file_transport_internet_finalmask_header_custom_config_proto_rawDescGZIP() []byte { + file_transport_internet_finalmask_header_custom_config_proto_rawDescOnce.Do(func() { + file_transport_internet_finalmask_header_custom_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_header_custom_config_proto_rawDesc), len(file_transport_internet_finalmask_header_custom_config_proto_rawDesc))) + }) + return file_transport_internet_finalmask_header_custom_config_proto_rawDescData +} + +var file_transport_internet_finalmask_header_custom_config_proto_msgTypes = make([]protoimpl.MessageInfo, 5) +var file_transport_internet_finalmask_header_custom_config_proto_goTypes = []any{ + (*TCPItem)(nil), // 0: xray.transport.internet.finalmask.header.custom.TCPItem + (*TCPSequence)(nil), // 1: xray.transport.internet.finalmask.header.custom.TCPSequence + (*TCPConfig)(nil), // 2: xray.transport.internet.finalmask.header.custom.TCPConfig + (*UDPItem)(nil), // 3: xray.transport.internet.finalmask.header.custom.UDPItem + (*UDPConfig)(nil), // 4: xray.transport.internet.finalmask.header.custom.UDPConfig +} +var file_transport_internet_finalmask_header_custom_config_proto_depIdxs = []int32{ + 0, // 0: xray.transport.internet.finalmask.header.custom.TCPSequence.sequence:type_name -> xray.transport.internet.finalmask.header.custom.TCPItem + 1, // 1: xray.transport.internet.finalmask.header.custom.TCPConfig.clients:type_name -> xray.transport.internet.finalmask.header.custom.TCPSequence + 1, // 2: xray.transport.internet.finalmask.header.custom.TCPConfig.servers:type_name -> xray.transport.internet.finalmask.header.custom.TCPSequence + 1, // 3: xray.transport.internet.finalmask.header.custom.TCPConfig.errors:type_name -> xray.transport.internet.finalmask.header.custom.TCPSequence + 3, // 4: xray.transport.internet.finalmask.header.custom.UDPConfig.client:type_name -> xray.transport.internet.finalmask.header.custom.UDPItem + 3, // 5: xray.transport.internet.finalmask.header.custom.UDPConfig.server:type_name -> xray.transport.internet.finalmask.header.custom.UDPItem + 6, // [6:6] is the sub-list for method output_type + 6, // [6:6] is the sub-list for method input_type + 6, // [6:6] is the sub-list for extension type_name + 6, // [6:6] is the sub-list for extension extendee + 0, // [0:6] is the sub-list for field type_name +} + +func init() { file_transport_internet_finalmask_header_custom_config_proto_init() } +func file_transport_internet_finalmask_header_custom_config_proto_init() { + if File_transport_internet_finalmask_header_custom_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_header_custom_config_proto_rawDesc), len(file_transport_internet_finalmask_header_custom_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 5, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_transport_internet_finalmask_header_custom_config_proto_goTypes, + DependencyIndexes: file_transport_internet_finalmask_header_custom_config_proto_depIdxs, + MessageInfos: file_transport_internet_finalmask_header_custom_config_proto_msgTypes, + }.Build() + File_transport_internet_finalmask_header_custom_config_proto = out.File + file_transport_internet_finalmask_header_custom_config_proto_goTypes = nil + file_transport_internet_finalmask_header_custom_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/header/custom/config.proto b/subproject/Xray-core-main/transport/internet/finalmask/header/custom/config.proto new file mode 100644 index 00000000..cbf498ef --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/header/custom/config.proto @@ -0,0 +1,38 @@ +syntax = "proto3"; + +package xray.transport.internet.finalmask.header.custom; +option csharp_namespace = "Xray.Transport.Internet.Finalmask.Header.Custom"; +option go_package = "github.com/xtls/xray-core/transport/internet/finalmask/header/custom"; +option java_package = "com.xray.transport.internet.finalmask.header.custom"; +option java_multiple_files = true; + +message TCPItem { + int64 delay_min = 1; + int64 delay_max = 2; + int32 rand = 3; + int32 rand_min = 4; + int32 rand_max = 5; + bytes packet = 6; +} + +message TCPSequence { + repeated TCPItem sequence = 1; +} + +message TCPConfig { + repeated TCPSequence clients = 1; + repeated TCPSequence servers = 2; + repeated TCPSequence errors = 3; +} + +message UDPItem { + int32 rand = 1; + int32 rand_min = 2; + int32 rand_max = 3; + bytes packet = 4; +} + +message UDPConfig { + repeated UDPItem client = 1; + repeated UDPItem server = 2; +} \ No newline at end of file diff --git a/subproject/Xray-core-main/transport/internet/finalmask/header/custom/tcp.go b/subproject/Xray-core-main/transport/internet/finalmask/header/custom/tcp.go new file mode 100644 index 00000000..ef631984 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/header/custom/tcp.go @@ -0,0 +1,246 @@ +package custom + +import ( + "bytes" + "io" + "net" + "sync" + "time" + + "github.com/xtls/xray-core/common/crypto" + "github.com/xtls/xray-core/common/errors" +) + +type tcpCustomClient struct { + clients []*TCPSequence + servers []*TCPSequence +} + +type tcpCustomClientConn struct { + net.Conn + header *tcpCustomClient + + auth bool + wg sync.WaitGroup + once sync.Once +} + +func NewConnClientTCP(c *TCPConfig, raw net.Conn) (net.Conn, error) { + conn := &tcpCustomClientConn{ + Conn: raw, + header: &tcpCustomClient{ + clients: c.Clients, + servers: c.Servers, + }, + } + + conn.wg.Add(1) + + return conn, nil +} + +func (c *tcpCustomClientConn) TcpMaskConn() {} + +func (c *tcpCustomClientConn) RawConn() net.Conn { + // c.wg.Wait() + + return c.Conn +} + +func (c *tcpCustomClientConn) Splice() bool { + return true +} + +func (c *tcpCustomClientConn) Read(p []byte) (n int, err error) { + c.wg.Wait() + + if !c.auth { + return 0, errors.New("header auth failed") + } + + return c.Conn.Read(p) +} + +func (c *tcpCustomClientConn) Write(p []byte) (n int, err error) { + c.once.Do(func() { + i := 0 + j := 0 + for i = range c.header.clients { + if !writeSequence(c.Conn, c.header.clients[i]) { + c.wg.Done() + return + } + + if j < len(c.header.servers) { + if !readSequence(c.Conn, c.header.servers[j]) { + c.wg.Done() + return + } + j++ + } + } + + for j < len(c.header.servers) { + if !readSequence(c.Conn, c.header.servers[j]) { + c.wg.Done() + return + } + j++ + } + + c.auth = true + c.wg.Done() + }) + + c.wg.Wait() + + if !c.auth { + return 0, errors.New("header auth failed") + } + + return c.Conn.Write(p) +} + +type tcpCustomServer struct { + clients []*TCPSequence + servers []*TCPSequence + errors []*TCPSequence +} + +type tcpCustomServerConn struct { + net.Conn + header *tcpCustomServer + + auth bool + wg sync.WaitGroup + once sync.Once +} + +func NewConnServerTCP(c *TCPConfig, raw net.Conn) (net.Conn, error) { + conn := &tcpCustomServerConn{ + Conn: raw, + header: &tcpCustomServer{ + clients: c.Clients, + servers: c.Servers, + errors: c.Errors, + }, + } + + conn.wg.Add(1) + + return conn, nil +} + +func (c *tcpCustomServerConn) TcpMaskConn() {} + +func (c *tcpCustomServerConn) RawConn() net.Conn { + // c.wg.Wait() + + return c.Conn +} + +func (c *tcpCustomServerConn) Splice() bool { + return true +} + +func (c *tcpCustomServerConn) Read(p []byte) (n int, err error) { + c.once.Do(func() { + i := 0 + j := 0 + for i = range c.header.clients { + if !readSequence(c.Conn, c.header.clients[i]) { + if i < len(c.header.errors) { + writeSequence(c.Conn, c.header.errors[i]) + } + c.wg.Done() + return + } + + if j < len(c.header.servers) { + if !writeSequence(c.Conn, c.header.servers[j]) { + c.wg.Done() + return + } + j++ + } + } + + for j < len(c.header.servers) { + if !writeSequence(c.Conn, c.header.servers[j]) { + c.wg.Done() + return + } + j++ + } + + c.auth = true + c.wg.Done() + }) + + c.wg.Wait() + + if !c.auth { + return 0, errors.New("header auth failed") + } + + return c.Conn.Read(p) +} + +func (c *tcpCustomServerConn) Write(p []byte) (n int, err error) { + c.wg.Wait() + + if !c.auth { + return 0, errors.New("header auth failed") + } + + return c.Conn.Write(p) +} + +func readSequence(r io.Reader, sequence *TCPSequence) bool { + for _, item := range sequence.Sequence { + length := max(int(item.Rand), len(item.Packet)) + buf := make([]byte, length) + n, err := io.ReadFull(r, buf) + if err != nil { + return false + } + if item.Rand > 0 && n != length { + return false + } + if len(item.Packet) > 0 && !bytes.Equal(item.Packet, buf[:n]) { + return false + } + } + return true +} + +func writeSequence(w io.Writer, sequence *TCPSequence) bool { + var merged []byte + for _, item := range sequence.Sequence { + if item.DelayMax > 0 { + if len(merged) > 0 { + _, err := w.Write(merged) + if err != nil { + return false + } + merged = nil + } + time.Sleep(time.Duration(crypto.RandBetween(item.DelayMin, item.DelayMax)) * time.Millisecond) + } + if item.Rand > 0 { + buf := make([]byte, item.Rand) + crypto.RandBytesBetween(buf, byte(item.RandMin), byte(item.RandMax)) + merged = append(merged, buf...) + } else { + merged = append(merged, item.Packet...) + } + } + if len(merged) > 0 { + _, err := w.Write(merged) + if err != nil { + return false + } + merged = nil + } + return true +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/header/custom/udp.go b/subproject/Xray-core-main/transport/internet/finalmask/header/custom/udp.go new file mode 100644 index 00000000..033351e1 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/header/custom/udp.go @@ -0,0 +1,183 @@ +package custom + +import ( + "bytes" + "net" + + "github.com/xtls/xray-core/common/crypto" + "github.com/xtls/xray-core/common/errors" +) + +type udpCustomClient struct { + client []*UDPItem + server []*UDPItem + merged []byte +} + +func (h *udpCustomClient) Serialize(b []byte) { + index := 0 + for _, item := range h.client { + if item.Rand > 0 { + crypto.RandBytesBetween(h.merged[index:index+int(item.Rand)], byte(item.RandMin), byte(item.RandMax)) + index += int(item.Rand) + } else { + index += len(item.Packet) + } + } + copy(b, h.merged) +} + +func (h *udpCustomClient) Match(b []byte) bool { + if len(b) < len(h.merged) { + return false + } + + data := b + match := true + + for _, item := range h.server { + length := max(int(item.Rand), len(item.Packet)) + + if len(item.Packet) > 0 && !bytes.Equal(item.Packet, data[:length]) { + match = false + break + } + + data = data[length:] + } + + return match +} + +type udpCustomClientConn struct { + net.PacketConn + header *udpCustomClient +} + +func NewConnClientUDP(c *UDPConfig, raw net.PacketConn) (net.PacketConn, error) { + conn := &udpCustomClientConn{ + PacketConn: raw, + header: &udpCustomClient{ + client: c.Client, + server: c.Server, + }, + } + + index := 0 + for _, item := range conn.header.client { + if item.Rand > 0 { + conn.header.merged = append(conn.header.merged, make([]byte, item.Rand)...) + index += int(item.Rand) + } else { + conn.header.merged = append(conn.header.merged, item.Packet...) + index += len(item.Packet) + } + } + + return conn, nil +} + +func (c *udpCustomClientConn) Size() int { + return len(c.header.merged) +} + +func (c *udpCustomClientConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { + if !c.header.Match(p) { + return 0, addr, errors.New("header mismatch") + } + + return len(p) - len(c.header.merged), addr, nil +} + +func (c *udpCustomClientConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { + c.header.Serialize(p) + + return len(p), nil +} + +type udpCustomServer struct { + client []*UDPItem + server []*UDPItem + merged []byte +} + +func (h *udpCustomServer) Serialize(b []byte) { + index := 0 + for _, item := range h.server { + if item.Rand > 0 { + crypto.RandBytesBetween(h.merged[index:index+int(item.Rand)], byte(item.RandMin), byte(item.RandMax)) + index += int(item.Rand) + } else { + index += len(item.Packet) + } + } + copy(b, h.merged) +} + +func (h *udpCustomServer) Match(b []byte) bool { + if len(b) < len(h.merged) { + return false + } + + data := b + match := true + + for _, item := range h.client { + length := max(int(item.Rand), len(item.Packet)) + + if len(item.Packet) > 0 && !bytes.Equal(item.Packet, data[:length]) { + match = false + break + } + + data = data[length:] + } + + return match +} + +type udpCustomServerConn struct { + net.PacketConn + header *udpCustomServer +} + +func NewConnServerUDP(c *UDPConfig, raw net.PacketConn) (net.PacketConn, error) { + conn := &udpCustomServerConn{ + PacketConn: raw, + header: &udpCustomServer{ + client: c.Client, + server: c.Server, + }, + } + + index := 0 + for _, item := range conn.header.server { + if item.Rand > 0 { + conn.header.merged = append(conn.header.merged, make([]byte, item.Rand)...) + index += int(item.Rand) + } else { + conn.header.merged = append(conn.header.merged, item.Packet...) + index += len(item.Packet) + } + } + + return conn, nil +} + +func (c *udpCustomServerConn) Size() int { + return len(c.header.merged) +} + +func (c *udpCustomServerConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { + if !c.header.Match(p) { + return 0, addr, errors.New("header mismatch") + } + + return len(p) - len(c.header.merged), addr, nil +} + +func (c *udpCustomServerConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { + c.header.Serialize(p) + + return len(p), nil +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/header/dns/config.go b/subproject/Xray-core-main/transport/internet/finalmask/header/dns/config.go new file mode 100644 index 00000000..48ede126 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/header/dns/config.go @@ -0,0 +1,19 @@ +package dns + +import ( + "net" +) + +func (c *Config) UDP() { +} + +func (c *Config) WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) { + return NewConnClient(c, raw) +} + +func (c *Config) WrapPacketConnServer(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) { + return NewConnServer(c, raw) +} + +func (c *Config) HeaderConn() { +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/header/dns/config.pb.go b/subproject/Xray-core-main/transport/internet/finalmask/header/dns/config.pb.go new file mode 100644 index 00000000..156cfa63 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/header/dns/config.pb.go @@ -0,0 +1,123 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: transport/internet/finalmask/header/dns/config.proto + +package dns + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + Domain string `protobuf:"bytes,1,opt,name=domain,proto3" json:"domain,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_transport_internet_finalmask_header_dns_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_finalmask_header_dns_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_transport_internet_finalmask_header_dns_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Config) GetDomain() string { + if x != nil { + return x.Domain + } + return "" +} + +var File_transport_internet_finalmask_header_dns_config_proto protoreflect.FileDescriptor + +const file_transport_internet_finalmask_header_dns_config_proto_rawDesc = "" + + "\n" + + "4transport/internet/finalmask/header/dns/config.proto\x12,xray.transport.internet.finalmask.header.dns\" \n" + + "\x06Config\x12\x16\n" + + "\x06domain\x18\x01 \x01(\tR\x06domainB\xa6\x01\n" + + "0com.xray.transport.internet.finalmask.header.dnsP\x01ZAgithub.com/xtls/xray-core/transport/internet/finalmask/header/dns\xaa\x02,Xray.Transport.Internet.Finalmask.Header.Dnsb\x06proto3" + +var ( + file_transport_internet_finalmask_header_dns_config_proto_rawDescOnce sync.Once + file_transport_internet_finalmask_header_dns_config_proto_rawDescData []byte +) + +func file_transport_internet_finalmask_header_dns_config_proto_rawDescGZIP() []byte { + file_transport_internet_finalmask_header_dns_config_proto_rawDescOnce.Do(func() { + file_transport_internet_finalmask_header_dns_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_header_dns_config_proto_rawDesc), len(file_transport_internet_finalmask_header_dns_config_proto_rawDesc))) + }) + return file_transport_internet_finalmask_header_dns_config_proto_rawDescData +} + +var file_transport_internet_finalmask_header_dns_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_transport_internet_finalmask_header_dns_config_proto_goTypes = []any{ + (*Config)(nil), // 0: xray.transport.internet.finalmask.header.dns.Config +} +var file_transport_internet_finalmask_header_dns_config_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_transport_internet_finalmask_header_dns_config_proto_init() } +func file_transport_internet_finalmask_header_dns_config_proto_init() { + if File_transport_internet_finalmask_header_dns_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_header_dns_config_proto_rawDesc), len(file_transport_internet_finalmask_header_dns_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_transport_internet_finalmask_header_dns_config_proto_goTypes, + DependencyIndexes: file_transport_internet_finalmask_header_dns_config_proto_depIdxs, + MessageInfos: file_transport_internet_finalmask_header_dns_config_proto_msgTypes, + }.Build() + File_transport_internet_finalmask_header_dns_config_proto = out.File + file_transport_internet_finalmask_header_dns_config_proto_goTypes = nil + file_transport_internet_finalmask_header_dns_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/header/dns/config.proto b/subproject/Xray-core-main/transport/internet/finalmask/header/dns/config.proto new file mode 100644 index 00000000..10e1baca --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/header/dns/config.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; + +package xray.transport.internet.finalmask.header.dns; +option csharp_namespace = "Xray.Transport.Internet.Finalmask.Header.Dns"; +option go_package = "github.com/xtls/xray-core/transport/internet/finalmask/header/dns"; +option java_package = "com.xray.transport.internet.finalmask.header.dns"; +option java_multiple_files = true; + +message Config { + string domain = 1; +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/header/dns/conn.go b/subproject/Xray-core-main/transport/internet/finalmask/header/dns/conn.go new file mode 100644 index 00000000..263ca7ef --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/header/dns/conn.go @@ -0,0 +1,138 @@ +package dns + +import ( + "encoding/binary" + "net" + + "github.com/xtls/xray-core/common/dice" + "github.com/xtls/xray-core/common/errors" +) + +func packDomainName(s string, msg []byte) (off1 int, err error) { + off := 0 + ls := len(s) + // Each dot ends a segment of the name. + // We trade each dot byte for a length byte. + // Except for escaped dots (\.), which are normal dots. + // There is also a trailing zero. + + // Emit sequence of counted strings, chopping at dots. + var ( + begin int + bs []byte + ) + for i := 0; i < ls; i++ { + var c byte + if bs == nil { + c = s[i] + } else { + c = bs[i] + } + + switch c { + case '\\': + if off+1 > len(msg) { + return len(msg), errors.New("buffer size too small") + } + + if bs == nil { + bs = []byte(s) + } + + copy(bs[i:ls-1], bs[i+1:]) + ls-- + case '.': + labelLen := i - begin + if labelLen >= 1<<6 { // top two bits of length must be clear + return len(msg), errors.New("bad rdata") + } + + // off can already (we're in a loop) be bigger than len(msg) + // this happens when a name isn't fully qualified + if off+1+labelLen > len(msg) { + return len(msg), errors.New("buffer size too small") + } + + // The following is covered by the length check above. + msg[off] = byte(labelLen) + + if bs == nil { + copy(msg[off+1:], s[begin:i]) + } else { + copy(msg[off+1:], bs[begin:i]) + } + off += 1 + labelLen + begin = i + 1 + default: + } + } + + if off < len(msg) { + msg[off] = 0 + } + + return off + 1, nil +} + +type dns struct { + header []byte +} + +func (h *dns) Size() int { + return len(h.header) +} + +func (h *dns) Serialize(b []byte) { + copy(b, h.header) + binary.BigEndian.PutUint16(b[0:], dice.RollUint16()) +} + +type dnsConn struct { + net.PacketConn + header *dns +} + +func NewConnClient(c *Config, raw net.PacketConn) (net.PacketConn, error) { + var header []byte + header = binary.BigEndian.AppendUint16(header, 0x0000) // Transaction ID + header = binary.BigEndian.AppendUint16(header, 0x0100) // Flags: Standard query + header = binary.BigEndian.AppendUint16(header, 0x0001) // Questions + header = binary.BigEndian.AppendUint16(header, 0x0000) // Answer RRs + header = binary.BigEndian.AppendUint16(header, 0x0000) // Authority RRs + header = binary.BigEndian.AppendUint16(header, 0x0000) // Additional RRs + buf := make([]byte, 0x100) + off1, err := packDomainName(c.Domain+".", buf) + if err != nil { + return nil, err + } + header = append(header, buf[:off1]...) + header = binary.BigEndian.AppendUint16(header, 0x0001) // Type: A + header = binary.BigEndian.AppendUint16(header, 0x0001) // Class: IN + + conn := &dnsConn{ + PacketConn: raw, + header: &dns{ + header: header, + }, + } + + return conn, nil +} + +func NewConnServer(c *Config, raw net.PacketConn) (net.PacketConn, error) { + return NewConnClient(c, raw) +} + +func (c *dnsConn) Size() int { + return c.header.Size() +} + +func (c *dnsConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { + return len(p) - c.header.Size(), addr, nil +} + +func (c *dnsConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { + c.header.Serialize(p) + + return len(p), nil +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/header/dtls/config.go b/subproject/Xray-core-main/transport/internet/finalmask/header/dtls/config.go new file mode 100644 index 00000000..decb69e3 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/header/dtls/config.go @@ -0,0 +1,19 @@ +package dtls + +import ( + "net" +) + +func (c *Config) UDP() { +} + +func (c *Config) WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) { + return NewConnClient(c, raw) +} + +func (c *Config) WrapPacketConnServer(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) { + return NewConnServer(c, raw) +} + +func (c *Config) HeaderConn() { +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/header/dtls/config.pb.go b/subproject/Xray-core-main/transport/internet/finalmask/header/dtls/config.pb.go new file mode 100644 index 00000000..ef42f5c4 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/header/dtls/config.pb.go @@ -0,0 +1,114 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: transport/internet/finalmask/header/dtls/config.proto + +package dtls + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_transport_internet_finalmask_header_dtls_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_finalmask_header_dtls_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_transport_internet_finalmask_header_dtls_config_proto_rawDescGZIP(), []int{0} +} + +var File_transport_internet_finalmask_header_dtls_config_proto protoreflect.FileDescriptor + +const file_transport_internet_finalmask_header_dtls_config_proto_rawDesc = "" + + "\n" + + "5transport/internet/finalmask/header/dtls/config.proto\x12-xray.transport.internet.finalmask.header.dtls\"\b\n" + + "\x06ConfigB\xa9\x01\n" + + "1com.xray.transport.internet.finalmask.header.dtlsP\x01ZBgithub.com/xtls/xray-core/transport/internet/finalmask/header/dtls\xaa\x02-Xray.Transport.Internet.Finalmask.Header.Dtlsb\x06proto3" + +var ( + file_transport_internet_finalmask_header_dtls_config_proto_rawDescOnce sync.Once + file_transport_internet_finalmask_header_dtls_config_proto_rawDescData []byte +) + +func file_transport_internet_finalmask_header_dtls_config_proto_rawDescGZIP() []byte { + file_transport_internet_finalmask_header_dtls_config_proto_rawDescOnce.Do(func() { + file_transport_internet_finalmask_header_dtls_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_header_dtls_config_proto_rawDesc), len(file_transport_internet_finalmask_header_dtls_config_proto_rawDesc))) + }) + return file_transport_internet_finalmask_header_dtls_config_proto_rawDescData +} + +var file_transport_internet_finalmask_header_dtls_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_transport_internet_finalmask_header_dtls_config_proto_goTypes = []any{ + (*Config)(nil), // 0: xray.transport.internet.finalmask.header.dtls.Config +} +var file_transport_internet_finalmask_header_dtls_config_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_transport_internet_finalmask_header_dtls_config_proto_init() } +func file_transport_internet_finalmask_header_dtls_config_proto_init() { + if File_transport_internet_finalmask_header_dtls_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_header_dtls_config_proto_rawDesc), len(file_transport_internet_finalmask_header_dtls_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_transport_internet_finalmask_header_dtls_config_proto_goTypes, + DependencyIndexes: file_transport_internet_finalmask_header_dtls_config_proto_depIdxs, + MessageInfos: file_transport_internet_finalmask_header_dtls_config_proto_msgTypes, + }.Build() + File_transport_internet_finalmask_header_dtls_config_proto = out.File + file_transport_internet_finalmask_header_dtls_config_proto_goTypes = nil + file_transport_internet_finalmask_header_dtls_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/header/dtls/config.proto b/subproject/Xray-core-main/transport/internet/finalmask/header/dtls/config.proto new file mode 100644 index 00000000..7f9f7971 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/header/dtls/config.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +package xray.transport.internet.finalmask.header.dtls; +option csharp_namespace = "Xray.Transport.Internet.Finalmask.Header.Dtls"; +option go_package = "github.com/xtls/xray-core/transport/internet/finalmask/header/dtls"; +option java_package = "com.xray.transport.internet.finalmask.header.dtls"; +option java_multiple_files = true; + +message Config {} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/header/dtls/conn.go b/subproject/Xray-core-main/transport/internet/finalmask/header/dtls/conn.go new file mode 100644 index 00000000..3f875c2e --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/header/dtls/conn.go @@ -0,0 +1,74 @@ +package dtls + +import ( + "net" + + "github.com/xtls/xray-core/common/dice" +) + +type dtls struct { + epoch uint16 + length uint16 + sequence uint32 +} + +func (*dtls) Size() int { + return 1 + 2 + 2 + 6 + 2 +} + +func (h *dtls) Serialize(b []byte) { + b[0] = 23 + b[1] = 254 + b[2] = 253 + b[3] = byte(h.epoch >> 8) + b[4] = byte(h.epoch) + b[5] = 0 + b[6] = 0 + b[7] = byte(h.sequence >> 24) + b[8] = byte(h.sequence >> 16) + b[9] = byte(h.sequence >> 8) + b[10] = byte(h.sequence) + h.sequence++ + b[11] = byte(h.length >> 8) + b[12] = byte(h.length) + h.length += 17 + if h.length > 100 { + h.length -= 50 + } +} + +type dtlsConn struct { + net.PacketConn + header *dtls +} + +func NewConnClient(c *Config, raw net.PacketConn) (net.PacketConn, error) { + conn := &dtlsConn{ + PacketConn: raw, + header: &dtls{ + epoch: dice.RollUint16(), + sequence: 0, + length: 17, + }, + } + + return conn, nil +} + +func NewConnServer(c *Config, raw net.PacketConn) (net.PacketConn, error) { + return NewConnClient(c, raw) +} + +func (c *dtlsConn) Size() int { + return c.header.Size() +} + +func (c *dtlsConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { + return len(p) - c.header.Size(), addr, nil +} + +func (c *dtlsConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { + c.header.Serialize(p) + + return len(p), nil +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/header/srtp/config.go b/subproject/Xray-core-main/transport/internet/finalmask/header/srtp/config.go new file mode 100644 index 00000000..006964d9 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/header/srtp/config.go @@ -0,0 +1,19 @@ +package srtp + +import ( + "net" +) + +func (c *Config) UDP() { +} + +func (c *Config) WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) { + return NewConnClient(c, raw) +} + +func (c *Config) WrapPacketConnServer(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) { + return NewConnServer(c, raw) +} + +func (c *Config) HeaderConn() { +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/header/srtp/config.pb.go b/subproject/Xray-core-main/transport/internet/finalmask/header/srtp/config.pb.go new file mode 100644 index 00000000..2d0f3232 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/header/srtp/config.pb.go @@ -0,0 +1,114 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: transport/internet/finalmask/header/srtp/config.proto + +package srtp + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_transport_internet_finalmask_header_srtp_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_finalmask_header_srtp_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_transport_internet_finalmask_header_srtp_config_proto_rawDescGZIP(), []int{0} +} + +var File_transport_internet_finalmask_header_srtp_config_proto protoreflect.FileDescriptor + +const file_transport_internet_finalmask_header_srtp_config_proto_rawDesc = "" + + "\n" + + "5transport/internet/finalmask/header/srtp/config.proto\x12-xray.transport.internet.finalmask.header.srtp\"\b\n" + + "\x06ConfigB\xa9\x01\n" + + "1com.xray.transport.internet.finalmask.header.srtpP\x01ZBgithub.com/xtls/xray-core/transport/internet/finalmask/header/srtp\xaa\x02-Xray.Transport.Internet.Finalmask.Header.Srtpb\x06proto3" + +var ( + file_transport_internet_finalmask_header_srtp_config_proto_rawDescOnce sync.Once + file_transport_internet_finalmask_header_srtp_config_proto_rawDescData []byte +) + +func file_transport_internet_finalmask_header_srtp_config_proto_rawDescGZIP() []byte { + file_transport_internet_finalmask_header_srtp_config_proto_rawDescOnce.Do(func() { + file_transport_internet_finalmask_header_srtp_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_header_srtp_config_proto_rawDesc), len(file_transport_internet_finalmask_header_srtp_config_proto_rawDesc))) + }) + return file_transport_internet_finalmask_header_srtp_config_proto_rawDescData +} + +var file_transport_internet_finalmask_header_srtp_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_transport_internet_finalmask_header_srtp_config_proto_goTypes = []any{ + (*Config)(nil), // 0: xray.transport.internet.finalmask.header.srtp.Config +} +var file_transport_internet_finalmask_header_srtp_config_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_transport_internet_finalmask_header_srtp_config_proto_init() } +func file_transport_internet_finalmask_header_srtp_config_proto_init() { + if File_transport_internet_finalmask_header_srtp_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_header_srtp_config_proto_rawDesc), len(file_transport_internet_finalmask_header_srtp_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_transport_internet_finalmask_header_srtp_config_proto_goTypes, + DependencyIndexes: file_transport_internet_finalmask_header_srtp_config_proto_depIdxs, + MessageInfos: file_transport_internet_finalmask_header_srtp_config_proto_msgTypes, + }.Build() + File_transport_internet_finalmask_header_srtp_config_proto = out.File + file_transport_internet_finalmask_header_srtp_config_proto_goTypes = nil + file_transport_internet_finalmask_header_srtp_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/header/srtp/config.proto b/subproject/Xray-core-main/transport/internet/finalmask/header/srtp/config.proto new file mode 100644 index 00000000..6792cb91 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/header/srtp/config.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +package xray.transport.internet.finalmask.header.srtp; +option csharp_namespace = "Xray.Transport.Internet.Finalmask.Header.Srtp"; +option go_package = "github.com/xtls/xray-core/transport/internet/finalmask/header/srtp"; +option java_package = "com.xray.transport.internet.finalmask.header.srtp"; +option java_multiple_files = true; + +message Config {} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/header/srtp/conn.go b/subproject/Xray-core-main/transport/internet/finalmask/header/srtp/conn.go new file mode 100644 index 00000000..94de0f9f --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/header/srtp/conn.go @@ -0,0 +1,58 @@ +package srtp + +import ( + "encoding/binary" + "net" + + "github.com/xtls/xray-core/common/dice" +) + +type srtp struct { + header uint16 + number uint16 +} + +func (*srtp) Size() int { + return 4 +} + +func (h *srtp) Serialize(b []byte) { + h.number++ + binary.BigEndian.PutUint16(b, h.header) + binary.BigEndian.PutUint16(b[2:], h.number) +} + +type srtpConn struct { + net.PacketConn + header *srtp +} + +func NewConnClient(c *Config, raw net.PacketConn) (net.PacketConn, error) { + conn := &srtpConn{ + PacketConn: raw, + header: &srtp{ + header: 0xB5E8, + number: dice.RollUint16(), + }, + } + + return conn, nil +} + +func NewConnServer(c *Config, raw net.PacketConn) (net.PacketConn, error) { + return NewConnClient(c, raw) +} + +func (c *srtpConn) Size() int { + return c.header.Size() +} + +func (c *srtpConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { + return len(p) - c.header.Size(), addr, nil +} + +func (c *srtpConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { + c.header.Serialize(p) + + return len(p), nil +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/header/utp/config.go b/subproject/Xray-core-main/transport/internet/finalmask/header/utp/config.go new file mode 100644 index 00000000..45d9a20c --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/header/utp/config.go @@ -0,0 +1,19 @@ +package utp + +import ( + "net" +) + +func (c *Config) UDP() { +} + +func (c *Config) WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) { + return NewConnClient(c, raw) +} + +func (c *Config) WrapPacketConnServer(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) { + return NewConnServer(c, raw) +} + +func (c *Config) HeaderConn() { +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/header/utp/config.pb.go b/subproject/Xray-core-main/transport/internet/finalmask/header/utp/config.pb.go new file mode 100644 index 00000000..bd67647f --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/header/utp/config.pb.go @@ -0,0 +1,114 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: transport/internet/finalmask/header/utp/config.proto + +package utp + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_transport_internet_finalmask_header_utp_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_finalmask_header_utp_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_transport_internet_finalmask_header_utp_config_proto_rawDescGZIP(), []int{0} +} + +var File_transport_internet_finalmask_header_utp_config_proto protoreflect.FileDescriptor + +const file_transport_internet_finalmask_header_utp_config_proto_rawDesc = "" + + "\n" + + "4transport/internet/finalmask/header/utp/config.proto\x12,xray.transport.internet.finalmask.header.utp\"\b\n" + + "\x06ConfigB\xa6\x01\n" + + "0com.xray.transport.internet.finalmask.header.utpP\x01ZAgithub.com/xtls/xray-core/transport/internet/finalmask/header/utp\xaa\x02,Xray.Transport.Internet.Finalmask.Header.Utpb\x06proto3" + +var ( + file_transport_internet_finalmask_header_utp_config_proto_rawDescOnce sync.Once + file_transport_internet_finalmask_header_utp_config_proto_rawDescData []byte +) + +func file_transport_internet_finalmask_header_utp_config_proto_rawDescGZIP() []byte { + file_transport_internet_finalmask_header_utp_config_proto_rawDescOnce.Do(func() { + file_transport_internet_finalmask_header_utp_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_header_utp_config_proto_rawDesc), len(file_transport_internet_finalmask_header_utp_config_proto_rawDesc))) + }) + return file_transport_internet_finalmask_header_utp_config_proto_rawDescData +} + +var file_transport_internet_finalmask_header_utp_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_transport_internet_finalmask_header_utp_config_proto_goTypes = []any{ + (*Config)(nil), // 0: xray.transport.internet.finalmask.header.utp.Config +} +var file_transport_internet_finalmask_header_utp_config_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_transport_internet_finalmask_header_utp_config_proto_init() } +func file_transport_internet_finalmask_header_utp_config_proto_init() { + if File_transport_internet_finalmask_header_utp_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_header_utp_config_proto_rawDesc), len(file_transport_internet_finalmask_header_utp_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_transport_internet_finalmask_header_utp_config_proto_goTypes, + DependencyIndexes: file_transport_internet_finalmask_header_utp_config_proto_depIdxs, + MessageInfos: file_transport_internet_finalmask_header_utp_config_proto_msgTypes, + }.Build() + File_transport_internet_finalmask_header_utp_config_proto = out.File + file_transport_internet_finalmask_header_utp_config_proto_goTypes = nil + file_transport_internet_finalmask_header_utp_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/header/utp/config.proto b/subproject/Xray-core-main/transport/internet/finalmask/header/utp/config.proto new file mode 100644 index 00000000..ce76ef2e --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/header/utp/config.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +package xray.transport.internet.finalmask.header.utp; +option csharp_namespace = "Xray.Transport.Internet.Finalmask.Header.Utp"; +option go_package = "github.com/xtls/xray-core/transport/internet/finalmask/header/utp"; +option java_package = "com.xray.transport.internet.finalmask.header.utp"; +option java_multiple_files = true; + +message Config {} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/header/utp/conn.go b/subproject/Xray-core-main/transport/internet/finalmask/header/utp/conn.go new file mode 100644 index 00000000..59005325 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/header/utp/conn.go @@ -0,0 +1,60 @@ +package utp + +import ( + "encoding/binary" + "net" + + "github.com/xtls/xray-core/common/dice" +) + +type utp struct { + header byte + extension byte + connectionID uint16 +} + +func (*utp) Size() int { + return 4 +} + +func (h *utp) Serialize(b []byte) { + binary.BigEndian.PutUint16(b, h.connectionID) + b[2] = h.header + b[3] = h.extension +} + +type utpConn struct { + net.PacketConn + header *utp +} + +func NewConnClient(c *Config, raw net.PacketConn) (net.PacketConn, error) { + conn := &utpConn{ + PacketConn: raw, + header: &utp{ + header: 1, + extension: 0, + connectionID: dice.RollUint16(), + }, + } + + return conn, nil +} + +func NewConnServer(c *Config, raw net.PacketConn) (net.PacketConn, error) { + return NewConnClient(c, raw) +} + +func (c *utpConn) Size() int { + return c.header.Size() +} + +func (c *utpConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { + return len(p) - c.header.Size(), addr, nil +} + +func (c *utpConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { + c.header.Serialize(p) + + return len(p), nil +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/header/wechat/config.go b/subproject/Xray-core-main/transport/internet/finalmask/header/wechat/config.go new file mode 100644 index 00000000..a433318e --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/header/wechat/config.go @@ -0,0 +1,19 @@ +package wechat + +import ( + "net" +) + +func (c *Config) UDP() { +} + +func (c *Config) WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) { + return NewConnClient(c, raw) +} + +func (c *Config) WrapPacketConnServer(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) { + return NewConnServer(c, raw) +} + +func (c *Config) HeaderConn() { +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/header/wechat/config.pb.go b/subproject/Xray-core-main/transport/internet/finalmask/header/wechat/config.pb.go new file mode 100644 index 00000000..da8e428d --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/header/wechat/config.pb.go @@ -0,0 +1,114 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: transport/internet/finalmask/header/wechat/config.proto + +package wechat + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_transport_internet_finalmask_header_wechat_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_finalmask_header_wechat_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_transport_internet_finalmask_header_wechat_config_proto_rawDescGZIP(), []int{0} +} + +var File_transport_internet_finalmask_header_wechat_config_proto protoreflect.FileDescriptor + +const file_transport_internet_finalmask_header_wechat_config_proto_rawDesc = "" + + "\n" + + "7transport/internet/finalmask/header/wechat/config.proto\x12/xray.transport.internet.finalmask.header.wechat\"\b\n" + + "\x06ConfigB\xaf\x01\n" + + "3com.xray.transport.internet.finalmask.header.wechatP\x01ZDgithub.com/xtls/xray-core/transport/internet/finalmask/header/wechat\xaa\x02/Xray.Transport.Internet.Finalmask.Header.Wechatb\x06proto3" + +var ( + file_transport_internet_finalmask_header_wechat_config_proto_rawDescOnce sync.Once + file_transport_internet_finalmask_header_wechat_config_proto_rawDescData []byte +) + +func file_transport_internet_finalmask_header_wechat_config_proto_rawDescGZIP() []byte { + file_transport_internet_finalmask_header_wechat_config_proto_rawDescOnce.Do(func() { + file_transport_internet_finalmask_header_wechat_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_header_wechat_config_proto_rawDesc), len(file_transport_internet_finalmask_header_wechat_config_proto_rawDesc))) + }) + return file_transport_internet_finalmask_header_wechat_config_proto_rawDescData +} + +var file_transport_internet_finalmask_header_wechat_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_transport_internet_finalmask_header_wechat_config_proto_goTypes = []any{ + (*Config)(nil), // 0: xray.transport.internet.finalmask.header.wechat.Config +} +var file_transport_internet_finalmask_header_wechat_config_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_transport_internet_finalmask_header_wechat_config_proto_init() } +func file_transport_internet_finalmask_header_wechat_config_proto_init() { + if File_transport_internet_finalmask_header_wechat_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_header_wechat_config_proto_rawDesc), len(file_transport_internet_finalmask_header_wechat_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_transport_internet_finalmask_header_wechat_config_proto_goTypes, + DependencyIndexes: file_transport_internet_finalmask_header_wechat_config_proto_depIdxs, + MessageInfos: file_transport_internet_finalmask_header_wechat_config_proto_msgTypes, + }.Build() + File_transport_internet_finalmask_header_wechat_config_proto = out.File + file_transport_internet_finalmask_header_wechat_config_proto_goTypes = nil + file_transport_internet_finalmask_header_wechat_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/header/wechat/config.proto b/subproject/Xray-core-main/transport/internet/finalmask/header/wechat/config.proto new file mode 100644 index 00000000..3127773a --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/header/wechat/config.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +package xray.transport.internet.finalmask.header.wechat; +option csharp_namespace = "Xray.Transport.Internet.Finalmask.Header.Wechat"; +option go_package = "github.com/xtls/xray-core/transport/internet/finalmask/header/wechat"; +option java_package = "com.xray.transport.internet.finalmask.header.wechat"; +option java_multiple_files = true; + +message Config {} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/header/wechat/conn.go b/subproject/Xray-core-main/transport/internet/finalmask/header/wechat/conn.go new file mode 100644 index 00000000..cb1fff62 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/header/wechat/conn.go @@ -0,0 +1,64 @@ +package wechat + +import ( + "encoding/binary" + "net" + + "github.com/xtls/xray-core/common/dice" +) + +type wechat struct { + sn uint32 +} + +func (*wechat) Size() int { + return 13 +} + +func (h *wechat) Serialize(b []byte) { + h.sn++ + b[0] = 0xa1 + b[1] = 0x08 + binary.BigEndian.PutUint32(b[2:], h.sn) + b[6] = 0x00 + b[7] = 0x10 + b[8] = 0x11 + b[9] = 0x18 + b[10] = 0x30 + b[11] = 0x22 + b[12] = 0x30 +} + +type wechatConn struct { + net.PacketConn + header *wechat +} + +func NewConnClient(c *Config, raw net.PacketConn) (net.PacketConn, error) { + conn := &wechatConn{ + PacketConn: raw, + header: &wechat{ + sn: uint32(dice.RollUint16()), + }, + } + + return conn, nil +} + +func NewConnServer(c *Config, raw net.PacketConn) (net.PacketConn, error) { + return NewConnClient(c, raw) +} + +func (c *wechatConn) Size() int { + return c.header.Size() +} + +func (c *wechatConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { + return len(p) - c.header.Size(), addr, nil +} + +func (c *wechatConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { + c.header.Serialize(p) + + return len(p), nil +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/header/wireguard/config.go b/subproject/Xray-core-main/transport/internet/finalmask/header/wireguard/config.go new file mode 100644 index 00000000..dd3609d8 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/header/wireguard/config.go @@ -0,0 +1,19 @@ +package wireguard + +import ( + "net" +) + +func (c *Config) UDP() { +} + +func (c *Config) WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) { + return NewConnClient(c, raw) +} + +func (c *Config) WrapPacketConnServer(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) { + return NewConnServer(c, raw) +} + +func (c *Config) HeaderConn() { +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/header/wireguard/config.pb.go b/subproject/Xray-core-main/transport/internet/finalmask/header/wireguard/config.pb.go new file mode 100644 index 00000000..fb49f3e6 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/header/wireguard/config.pb.go @@ -0,0 +1,114 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: transport/internet/finalmask/header/wireguard/config.proto + +package wireguard + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_transport_internet_finalmask_header_wireguard_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_finalmask_header_wireguard_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_transport_internet_finalmask_header_wireguard_config_proto_rawDescGZIP(), []int{0} +} + +var File_transport_internet_finalmask_header_wireguard_config_proto protoreflect.FileDescriptor + +const file_transport_internet_finalmask_header_wireguard_config_proto_rawDesc = "" + + "\n" + + ":transport/internet/finalmask/header/wireguard/config.proto\x122xray.transport.internet.finalmask.header.wireguard\"\b\n" + + "\x06ConfigB\xb8\x01\n" + + "6com.xray.transport.internet.finalmask.header.wireguardP\x01ZGgithub.com/xtls/xray-core/transport/internet/finalmask/header/wireguard\xaa\x022Xray.Transport.Internet.Finalmask.Header.Wireguardb\x06proto3" + +var ( + file_transport_internet_finalmask_header_wireguard_config_proto_rawDescOnce sync.Once + file_transport_internet_finalmask_header_wireguard_config_proto_rawDescData []byte +) + +func file_transport_internet_finalmask_header_wireguard_config_proto_rawDescGZIP() []byte { + file_transport_internet_finalmask_header_wireguard_config_proto_rawDescOnce.Do(func() { + file_transport_internet_finalmask_header_wireguard_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_header_wireguard_config_proto_rawDesc), len(file_transport_internet_finalmask_header_wireguard_config_proto_rawDesc))) + }) + return file_transport_internet_finalmask_header_wireguard_config_proto_rawDescData +} + +var file_transport_internet_finalmask_header_wireguard_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_transport_internet_finalmask_header_wireguard_config_proto_goTypes = []any{ + (*Config)(nil), // 0: xray.transport.internet.finalmask.header.wireguard.Config +} +var file_transport_internet_finalmask_header_wireguard_config_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_transport_internet_finalmask_header_wireguard_config_proto_init() } +func file_transport_internet_finalmask_header_wireguard_config_proto_init() { + if File_transport_internet_finalmask_header_wireguard_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_header_wireguard_config_proto_rawDesc), len(file_transport_internet_finalmask_header_wireguard_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_transport_internet_finalmask_header_wireguard_config_proto_goTypes, + DependencyIndexes: file_transport_internet_finalmask_header_wireguard_config_proto_depIdxs, + MessageInfos: file_transport_internet_finalmask_header_wireguard_config_proto_msgTypes, + }.Build() + File_transport_internet_finalmask_header_wireguard_config_proto = out.File + file_transport_internet_finalmask_header_wireguard_config_proto_goTypes = nil + file_transport_internet_finalmask_header_wireguard_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/header/wireguard/config.proto b/subproject/Xray-core-main/transport/internet/finalmask/header/wireguard/config.proto new file mode 100644 index 00000000..476cbfba --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/header/wireguard/config.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +package xray.transport.internet.finalmask.header.wireguard; +option csharp_namespace = "Xray.Transport.Internet.Finalmask.Header.Wireguard"; +option go_package = "github.com/xtls/xray-core/transport/internet/finalmask/header/wireguard"; +option java_package = "com.xray.transport.internet.finalmask.header.wireguard"; +option java_multiple_files = true; + +message Config {} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/header/wireguard/conn.go b/subproject/Xray-core-main/transport/internet/finalmask/header/wireguard/conn.go new file mode 100644 index 00000000..4a38969f --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/header/wireguard/conn.go @@ -0,0 +1,50 @@ +package wireguard + +import ( + "net" +) + +type wireguare struct{} + +func (*wireguare) Size() int { + return 4 +} + +func (h *wireguare) Serialize(b []byte) { + b[0] = 0x04 + b[1] = 0x00 + b[2] = 0x00 + b[3] = 0x00 +} + +type wireguareConn struct { + net.PacketConn + header *wireguare +} + +func NewConnClient(c *Config, raw net.PacketConn) (net.PacketConn, error) { + conn := &wireguareConn{ + PacketConn: raw, + header: &wireguare{}, + } + + return conn, nil +} + +func NewConnServer(c *Config, raw net.PacketConn) (net.PacketConn, error) { + return NewConnClient(c, raw) +} + +func (c *wireguareConn) Size() int { + return c.header.Size() +} + +func (c *wireguareConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { + return len(p) - c.header.Size(), addr, nil +} + +func (c *wireguareConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { + c.header.Serialize(p) + + return len(p), nil +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/mkcp/aes128gcm/aes128gcm_test.go b/subproject/Xray-core-main/transport/internet/finalmask/mkcp/aes128gcm/aes128gcm_test.go new file mode 100644 index 00000000..4806dfc2 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/mkcp/aes128gcm/aes128gcm_test.go @@ -0,0 +1,70 @@ +package aes128gcm_test + +import ( + "crypto/rand" + "crypto/sha256" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/xtls/xray-core/common/crypto" +) + +func TestAes128GcmSealInPlace(t *testing.T) { + hashedPsk := sha256.Sum256([]byte("psk")) + aead := crypto.NewAesGcm(hashedPsk[:16]) + + text := []byte("0123456789012") + buf := make([]byte, 8192) + + nonceSize := aead.NonceSize() + nonce := buf[:nonceSize] + rand.Read(nonce) + copy(buf[nonceSize:], text) + plaintext := buf[nonceSize : nonceSize+len(text)] + + sealed := aead.Seal(nil, nonce, plaintext, nil) + + _ = aead.Seal(plaintext[:0], nonce, plaintext, nil) + + assert.Equal(t, sealed, buf[nonceSize:nonceSize+aead.Overhead()+len(text)]) +} + +func encrypted(plain []byte) ([]byte, []byte) { + hashedPsk := sha256.Sum256([]byte("psk")) + aead := crypto.NewAesGcm(hashedPsk[:16]) + + nonce := make([]byte, 12) + rand.Read(nonce) + + return nonce, aead.Seal(nil, nonce, plain, nil) +} + +func TestAes128GcmOpenInPlace(t *testing.T) { + a, b := encrypted([]byte("0123456789012")) + buf := make([]byte, 8192) + copy(buf, a) + copy(buf[len(a):], b) + + hashedPsk := sha256.Sum256([]byte("psk")) + aead := crypto.NewAesGcm(hashedPsk[:16]) + + nonceSize := aead.NonceSize() + nonce := buf[:nonceSize] + ciphertext := buf[nonceSize : nonceSize+len(b)] + + opened, _ := aead.Open(nil, nonce, ciphertext, nil) + _, _ = aead.Open(ciphertext[:0], nonce, ciphertext, nil) + + assert.Equal(t, opened, ciphertext[:len(ciphertext)-aead.Overhead()]) +} + +func TestAes128GcmBounce(t *testing.T) { + hashedPsk := sha256.Sum256([]byte("psk")) + aead := crypto.NewAesGcm(hashedPsk[:16]) + buf := make([]byte, aead.NonceSize()+aead.Overhead()) + for i := 0; i < 1000; i++ { + _, _ = rand.Read(buf) + _, err := aead.Open(buf[aead.NonceSize():aead.NonceSize()], buf[:aead.NonceSize()], buf[aead.NonceSize():], nil) + assert.NotEqual(t, err, nil) + } +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/mkcp/aes128gcm/config.go b/subproject/Xray-core-main/transport/internet/finalmask/mkcp/aes128gcm/config.go new file mode 100644 index 00000000..a7160e2b --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/mkcp/aes128gcm/config.go @@ -0,0 +1,19 @@ +package aes128gcm + +import ( + "net" +) + +func (c *Config) UDP() { +} + +func (c *Config) WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) { + return NewConnClient(c, raw) +} + +func (c *Config) WrapPacketConnServer(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) { + return NewConnServer(c, raw) +} + +func (c *Config) HeaderConn() { +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/mkcp/aes128gcm/config.pb.go b/subproject/Xray-core-main/transport/internet/finalmask/mkcp/aes128gcm/config.pb.go new file mode 100644 index 00000000..ddeb3209 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/mkcp/aes128gcm/config.pb.go @@ -0,0 +1,123 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: transport/internet/finalmask/mkcp/aes128gcm/config.proto + +package aes128gcm + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + Password string `protobuf:"bytes,1,opt,name=password,proto3" json:"password,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_transport_internet_finalmask_mkcp_aes128gcm_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_finalmask_mkcp_aes128gcm_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_transport_internet_finalmask_mkcp_aes128gcm_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Config) GetPassword() string { + if x != nil { + return x.Password + } + return "" +} + +var File_transport_internet_finalmask_mkcp_aes128gcm_config_proto protoreflect.FileDescriptor + +const file_transport_internet_finalmask_mkcp_aes128gcm_config_proto_rawDesc = "" + + "\n" + + "8transport/internet/finalmask/mkcp/aes128gcm/config.proto\x120xray.transport.internet.finalmask.mkcp.aes128gcm\"$\n" + + "\x06Config\x12\x1a\n" + + "\bpassword\x18\x01 \x01(\tR\bpasswordB\xb2\x01\n" + + "4com.xray.transport.internet.finalmask.mkcp.aes128gcmP\x01ZEgithub.com/xtls/xray-core/transport/internet/finalmask/mkcp/aes128gcm\xaa\x020Xray.Transport.Internet.Finalmask.Mkcp.Aes128Gcmb\x06proto3" + +var ( + file_transport_internet_finalmask_mkcp_aes128gcm_config_proto_rawDescOnce sync.Once + file_transport_internet_finalmask_mkcp_aes128gcm_config_proto_rawDescData []byte +) + +func file_transport_internet_finalmask_mkcp_aes128gcm_config_proto_rawDescGZIP() []byte { + file_transport_internet_finalmask_mkcp_aes128gcm_config_proto_rawDescOnce.Do(func() { + file_transport_internet_finalmask_mkcp_aes128gcm_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_mkcp_aes128gcm_config_proto_rawDesc), len(file_transport_internet_finalmask_mkcp_aes128gcm_config_proto_rawDesc))) + }) + return file_transport_internet_finalmask_mkcp_aes128gcm_config_proto_rawDescData +} + +var file_transport_internet_finalmask_mkcp_aes128gcm_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_transport_internet_finalmask_mkcp_aes128gcm_config_proto_goTypes = []any{ + (*Config)(nil), // 0: xray.transport.internet.finalmask.mkcp.aes128gcm.Config +} +var file_transport_internet_finalmask_mkcp_aes128gcm_config_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_transport_internet_finalmask_mkcp_aes128gcm_config_proto_init() } +func file_transport_internet_finalmask_mkcp_aes128gcm_config_proto_init() { + if File_transport_internet_finalmask_mkcp_aes128gcm_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_mkcp_aes128gcm_config_proto_rawDesc), len(file_transport_internet_finalmask_mkcp_aes128gcm_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_transport_internet_finalmask_mkcp_aes128gcm_config_proto_goTypes, + DependencyIndexes: file_transport_internet_finalmask_mkcp_aes128gcm_config_proto_depIdxs, + MessageInfos: file_transport_internet_finalmask_mkcp_aes128gcm_config_proto_msgTypes, + }.Build() + File_transport_internet_finalmask_mkcp_aes128gcm_config_proto = out.File + file_transport_internet_finalmask_mkcp_aes128gcm_config_proto_goTypes = nil + file_transport_internet_finalmask_mkcp_aes128gcm_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/mkcp/aes128gcm/config.proto b/subproject/Xray-core-main/transport/internet/finalmask/mkcp/aes128gcm/config.proto new file mode 100644 index 00000000..76af2401 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/mkcp/aes128gcm/config.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; + +package xray.transport.internet.finalmask.mkcp.aes128gcm; +option csharp_namespace = "Xray.Transport.Internet.Finalmask.Mkcp.Aes128Gcm"; +option go_package = "github.com/xtls/xray-core/transport/internet/finalmask/mkcp/aes128gcm"; +option java_package = "com.xray.transport.internet.finalmask.mkcp.aes128gcm"; +option java_multiple_files = true; + +message Config { + string password = 1; +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/mkcp/aes128gcm/conn.go b/subproject/Xray-core-main/transport/internet/finalmask/mkcp/aes128gcm/conn.go new file mode 100644 index 00000000..055803af --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/mkcp/aes128gcm/conn.go @@ -0,0 +1,67 @@ +package aes128gcm + +import ( + "crypto/cipher" + "crypto/rand" + "crypto/sha256" + "net" + + "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/transport/internet/finalmask" +) + +type aes128gcmConn struct { + net.PacketConn + aead cipher.AEAD +} + +func NewConnClient(c *Config, raw net.PacketConn) (net.PacketConn, error) { + hashedPsk := sha256.Sum256([]byte(c.Password)) + + conn := &aes128gcmConn{ + PacketConn: raw, + aead: crypto.NewAesGcm(hashedPsk[:16]), + } + + return conn, nil +} + +func NewConnServer(c *Config, raw net.PacketConn) (net.PacketConn, error) { + return NewConnClient(c, raw) +} + +func (c *aes128gcmConn) Size() int { + return c.aead.NonceSize() +} + +func (c *aes128gcmConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { + if len(p) < c.aead.NonceSize()+c.aead.Overhead() { + return 0, addr, errors.New("aead short lenth") + } + + nonceSize := c.aead.NonceSize() + nonce := p[:nonceSize] + ciphertext := p[nonceSize:] + _, err = c.aead.Open(ciphertext[:0], nonce, ciphertext, nil) + if err != nil { + return 0, addr, errors.New("aead open").Base(err) + } + + return len(p) - c.aead.NonceSize() - c.aead.Overhead(), addr, nil +} + +func (c *aes128gcmConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { + if c.aead.Overhead()+len(p) > finalmask.UDPSize { + return 0, errors.New("aead short write") + } + + nonceSize := c.aead.NonceSize() + nonce := p[:nonceSize] + common.Must2(rand.Read(nonce)) + plaintext := p[nonceSize:] + _ = c.aead.Seal(plaintext[:0], nonce, plaintext, nil) + + return len(p) + c.aead.Overhead(), nil +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/mkcp/original/config.go b/subproject/Xray-core-main/transport/internet/finalmask/mkcp/original/config.go new file mode 100644 index 00000000..d18b1391 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/mkcp/original/config.go @@ -0,0 +1,19 @@ +package original + +import ( + "net" +) + +func (c *Config) UDP() { +} + +func (c *Config) WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) { + return NewConnClient(c, raw) +} + +func (c *Config) WrapPacketConnServer(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) { + return NewConnServer(c, raw) +} + +func (c *Config) HeaderConn() { +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/mkcp/original/config.pb.go b/subproject/Xray-core-main/transport/internet/finalmask/mkcp/original/config.pb.go new file mode 100644 index 00000000..1f4dffec --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/mkcp/original/config.pb.go @@ -0,0 +1,114 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: transport/internet/finalmask/mkcp/original/config.proto + +package original + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_transport_internet_finalmask_mkcp_original_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_finalmask_mkcp_original_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_transport_internet_finalmask_mkcp_original_config_proto_rawDescGZIP(), []int{0} +} + +var File_transport_internet_finalmask_mkcp_original_config_proto protoreflect.FileDescriptor + +const file_transport_internet_finalmask_mkcp_original_config_proto_rawDesc = "" + + "\n" + + "7transport/internet/finalmask/mkcp/original/config.proto\x12/xray.transport.internet.finalmask.mkcp.original\"\b\n" + + "\x06ConfigB\xaf\x01\n" + + "3com.xray.transport.internet.finalmask.mkcp.originalP\x01ZDgithub.com/xtls/xray-core/transport/internet/finalmask/mkcp/original\xaa\x02/Xray.Transport.Internet.Finalmask.Mkcp.Originalb\x06proto3" + +var ( + file_transport_internet_finalmask_mkcp_original_config_proto_rawDescOnce sync.Once + file_transport_internet_finalmask_mkcp_original_config_proto_rawDescData []byte +) + +func file_transport_internet_finalmask_mkcp_original_config_proto_rawDescGZIP() []byte { + file_transport_internet_finalmask_mkcp_original_config_proto_rawDescOnce.Do(func() { + file_transport_internet_finalmask_mkcp_original_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_mkcp_original_config_proto_rawDesc), len(file_transport_internet_finalmask_mkcp_original_config_proto_rawDesc))) + }) + return file_transport_internet_finalmask_mkcp_original_config_proto_rawDescData +} + +var file_transport_internet_finalmask_mkcp_original_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_transport_internet_finalmask_mkcp_original_config_proto_goTypes = []any{ + (*Config)(nil), // 0: xray.transport.internet.finalmask.mkcp.original.Config +} +var file_transport_internet_finalmask_mkcp_original_config_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_transport_internet_finalmask_mkcp_original_config_proto_init() } +func file_transport_internet_finalmask_mkcp_original_config_proto_init() { + if File_transport_internet_finalmask_mkcp_original_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_mkcp_original_config_proto_rawDesc), len(file_transport_internet_finalmask_mkcp_original_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_transport_internet_finalmask_mkcp_original_config_proto_goTypes, + DependencyIndexes: file_transport_internet_finalmask_mkcp_original_config_proto_depIdxs, + MessageInfos: file_transport_internet_finalmask_mkcp_original_config_proto_msgTypes, + }.Build() + File_transport_internet_finalmask_mkcp_original_config_proto = out.File + file_transport_internet_finalmask_mkcp_original_config_proto_goTypes = nil + file_transport_internet_finalmask_mkcp_original_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/mkcp/original/config.proto b/subproject/Xray-core-main/transport/internet/finalmask/mkcp/original/config.proto new file mode 100644 index 00000000..4b0f2630 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/mkcp/original/config.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +package xray.transport.internet.finalmask.mkcp.original; +option csharp_namespace = "Xray.Transport.Internet.Finalmask.Mkcp.Original"; +option go_package = "github.com/xtls/xray-core/transport/internet/finalmask/mkcp/original"; +option java_package = "com.xray.transport.internet.finalmask.mkcp.original"; +option java_multiple_files = true; + +message Config {} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/mkcp/original/conn.go b/subproject/Xray-core-main/transport/internet/finalmask/mkcp/original/conn.go new file mode 100644 index 00000000..15abe99b --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/mkcp/original/conn.go @@ -0,0 +1,109 @@ +package original + +import ( + "crypto/cipher" + "encoding/binary" + "hash/fnv" + "net" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/errors" +) + +type simple struct{} + +func NewSimple() *simple { + return &simple{} +} + +func (*simple) NonceSize() int { + return 0 +} + +func (*simple) Overhead() int { + return 6 +} + +func (a *simple) Seal(dst, nonce, plain, extra []byte) []byte { + dst = append(dst, 0, 0, 0, 0, 0, 0) + binary.BigEndian.PutUint16(dst[4:], uint16(len(plain))) + dst = append(dst, plain...) + + fnvHash := fnv.New32a() + common.Must2(fnvHash.Write(dst[4:])) + fnvHash.Sum(dst[:0]) + + dstLen := len(dst) + xtra := 4 - dstLen%4 + if xtra != 4 { + dst = append(dst, make([]byte, xtra)...) + } + xorfwd(dst) + if xtra != 4 { + dst = dst[:dstLen] + } + return dst +} + +func (a *simple) Open(dst, nonce, cipherText, extra []byte) ([]byte, error) { + dst = append(dst, cipherText...) + dstLen := len(dst) + xtra := 4 - dstLen%4 + if xtra != 4 { + dst = append(dst, make([]byte, xtra)...) + } + xorbkd(dst) + if xtra != 4 { + dst = dst[:dstLen] + } + + fnvHash := fnv.New32a() + common.Must2(fnvHash.Write(dst[4:])) + if binary.BigEndian.Uint32(dst[:4]) != fnvHash.Sum32() { + return nil, errors.New("invalid auth") + } + + length := binary.BigEndian.Uint16(dst[4:6]) + if len(dst)-6 != int(length) { + return nil, errors.New("invalid auth") + } + + return dst[6:], nil +} + +type simpleConn struct { + net.PacketConn + aead cipher.AEAD +} + +func NewConnClient(c *Config, raw net.PacketConn) (net.PacketConn, error) { + conn := &simpleConn{ + PacketConn: raw, + aead: &simple{}, + } + + return conn, nil +} + +func NewConnServer(c *Config, raw net.PacketConn) (net.PacketConn, error) { + return NewConnClient(c, raw) +} + +func (c *simpleConn) Size() int { + return c.aead.Overhead() +} + +func (c *simpleConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { + _, err = c.aead.Open(p[:0], nil, p, nil) + if err != nil { + return 0, addr, errors.New("aead open").Base(err) + } + + return len(p) - c.aead.Overhead(), addr, nil +} + +func (c *simpleConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { + _ = c.aead.Seal(p[:0], nil, p[c.aead.Overhead():], nil) + + return len(p), nil +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/mkcp/original/simple_test.go b/subproject/Xray-core-main/transport/internet/finalmask/mkcp/original/simple_test.go new file mode 100644 index 00000000..be9d6839 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/mkcp/original/simple_test.go @@ -0,0 +1,35 @@ +package original_test + +import ( + "crypto/rand" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/xtls/xray-core/transport/internet/finalmask/mkcp/original" +) + +func TestSimpleSealInPlace(t *testing.T) { + aead := original.NewSimple() + + text := []byte("0123456789012") + buf := make([]byte, 8192) + + copy(buf[aead.Overhead():], text) + plaintext := buf[aead.Overhead() : aead.Overhead()+len(text)] + + sealed := aead.Seal(nil, nil, plaintext, nil) + + _ = aead.Seal(buf[:0], nil, plaintext, nil) + + assert.Equal(t, sealed, buf[:aead.Overhead()+len(text)]) +} + +func TestOriginalBounce(t *testing.T) { + aead := original.NewSimple() + buf := make([]byte, aead.NonceSize()+aead.Overhead()) + for i := 0; i < 1000; i++ { + _, _ = rand.Read(buf) + _, err := aead.Open(buf[:0], nil, buf, nil) + assert.NotEqual(t, err, nil) + } +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/mkcp/original/xor.go b/subproject/Xray-core-main/transport/internet/finalmask/mkcp/original/xor.go new file mode 100644 index 00000000..b2a06179 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/mkcp/original/xor.go @@ -0,0 +1,18 @@ +//go:build !amd64 +// +build !amd64 + +package original + +// xorfwd performs XOR forwards in words, x[i] ^= x[i-4], i from 0 to len +func xorfwd(x []byte) { + for i := 4; i < len(x); i++ { + x[i] ^= x[i-4] + } +} + +// xorbkd performs XOR backwords in words, x[i] ^= x[i-4], i from len to 0 +func xorbkd(x []byte) { + for i := len(x) - 1; i >= 4; i-- { + x[i] ^= x[i-4] + } +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/mkcp/original/xor_amd64.go b/subproject/Xray-core-main/transport/internet/finalmask/mkcp/original/xor_amd64.go new file mode 100644 index 00000000..7352ace9 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/mkcp/original/xor_amd64.go @@ -0,0 +1,7 @@ +package original + +//go:noescape +func xorfwd(x []byte) + +//go:noescape +func xorbkd(x []byte) diff --git a/subproject/Xray-core-main/transport/internet/finalmask/mkcp/original/xor_amd64.s b/subproject/Xray-core-main/transport/internet/finalmask/mkcp/original/xor_amd64.s new file mode 100644 index 00000000..0c2759d7 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/mkcp/original/xor_amd64.s @@ -0,0 +1,47 @@ +#include "textflag.h" + +// func xorfwd(x []byte) +TEXT ·xorfwd(SB),NOSPLIT,$0 + MOVQ x+0(FP), SI // x[i] + MOVQ x_len+8(FP), CX // x.len + MOVQ x+0(FP), DI + ADDQ $4, DI // x[i+4] + SUBQ $4, CX +xorfwdloop: + MOVL (SI), AX + XORL AX, (DI) + ADDQ $4, SI + ADDQ $4, DI + SUBQ $4, CX + + CMPL CX, $0 + JE xorfwddone + + JMP xorfwdloop +xorfwddone: + RET + +// func xorbkd(x []byte) +TEXT ·xorbkd(SB),NOSPLIT,$0 + MOVQ x+0(FP), SI + MOVQ x_len+8(FP), CX // x.len + MOVQ x+0(FP), DI + ADDQ CX, SI // x[-8] + SUBQ $8, SI + ADDQ CX, DI // x[-4] + SUBQ $4, DI + SUBQ $4, CX +xorbkdloop: + MOVL (SI), AX + XORL AX, (DI) + SUBQ $4, SI + SUBQ $4, DI + SUBQ $4, CX + + CMPL CX, $0 + JE xorbkddone + + JMP xorbkdloop + +xorbkddone: + RET diff --git a/subproject/Xray-core-main/transport/internet/finalmask/noise/config.go b/subproject/Xray-core-main/transport/internet/finalmask/noise/config.go new file mode 100644 index 00000000..1764c0b1 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/noise/config.go @@ -0,0 +1,14 @@ +package noise + +import "net" + +func (c *Config) UDP() { +} + +func (c *Config) WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) { + return NewConnClient(c, raw) +} + +func (c *Config) WrapPacketConnServer(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) { + return NewConnServer(c, raw) +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/noise/config.pb.go b/subproject/Xray-core-main/transport/internet/finalmask/noise/config.pb.go new file mode 100644 index 00000000..71ba461a --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/noise/config.pb.go @@ -0,0 +1,243 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: transport/internet/finalmask/noise/config.proto + +package noise + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Item struct { + state protoimpl.MessageState `protogen:"open.v1"` + RandMin int64 `protobuf:"varint,1,opt,name=rand_min,json=randMin,proto3" json:"rand_min,omitempty"` + RandMax int64 `protobuf:"varint,2,opt,name=rand_max,json=randMax,proto3" json:"rand_max,omitempty"` + RandRangeMin int32 `protobuf:"varint,3,opt,name=rand_range_min,json=randRangeMin,proto3" json:"rand_range_min,omitempty"` + RandRangeMax int32 `protobuf:"varint,4,opt,name=rand_range_max,json=randRangeMax,proto3" json:"rand_range_max,omitempty"` + Packet []byte `protobuf:"bytes,5,opt,name=packet,proto3" json:"packet,omitempty"` + DelayMin int64 `protobuf:"varint,6,opt,name=delay_min,json=delayMin,proto3" json:"delay_min,omitempty"` + DelayMax int64 `protobuf:"varint,7,opt,name=delay_max,json=delayMax,proto3" json:"delay_max,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Item) Reset() { + *x = Item{} + mi := &file_transport_internet_finalmask_noise_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Item) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Item) ProtoMessage() {} + +func (x *Item) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_finalmask_noise_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Item.ProtoReflect.Descriptor instead. +func (*Item) Descriptor() ([]byte, []int) { + return file_transport_internet_finalmask_noise_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Item) GetRandMin() int64 { + if x != nil { + return x.RandMin + } + return 0 +} + +func (x *Item) GetRandMax() int64 { + if x != nil { + return x.RandMax + } + return 0 +} + +func (x *Item) GetRandRangeMin() int32 { + if x != nil { + return x.RandRangeMin + } + return 0 +} + +func (x *Item) GetRandRangeMax() int32 { + if x != nil { + return x.RandRangeMax + } + return 0 +} + +func (x *Item) GetPacket() []byte { + if x != nil { + return x.Packet + } + return nil +} + +func (x *Item) GetDelayMin() int64 { + if x != nil { + return x.DelayMin + } + return 0 +} + +func (x *Item) GetDelayMax() int64 { + if x != nil { + return x.DelayMax + } + return 0 +} + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + ResetMin int64 `protobuf:"varint,1,opt,name=reset_min,json=resetMin,proto3" json:"reset_min,omitempty"` + ResetMax int64 `protobuf:"varint,2,opt,name=reset_max,json=resetMax,proto3" json:"reset_max,omitempty"` + Items []*Item `protobuf:"bytes,3,rep,name=items,proto3" json:"items,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_transport_internet_finalmask_noise_config_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_finalmask_noise_config_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_transport_internet_finalmask_noise_config_proto_rawDescGZIP(), []int{1} +} + +func (x *Config) GetResetMin() int64 { + if x != nil { + return x.ResetMin + } + return 0 +} + +func (x *Config) GetResetMax() int64 { + if x != nil { + return x.ResetMax + } + return 0 +} + +func (x *Config) GetItems() []*Item { + if x != nil { + return x.Items + } + return nil +} + +var File_transport_internet_finalmask_noise_config_proto protoreflect.FileDescriptor + +const file_transport_internet_finalmask_noise_config_proto_rawDesc = "" + + "\n" + + "/transport/internet/finalmask/noise/config.proto\x12'xray.transport.internet.finalmask.noise\"\xda\x01\n" + + "\x04Item\x12\x19\n" + + "\brand_min\x18\x01 \x01(\x03R\arandMin\x12\x19\n" + + "\brand_max\x18\x02 \x01(\x03R\arandMax\x12$\n" + + "\x0erand_range_min\x18\x03 \x01(\x05R\frandRangeMin\x12$\n" + + "\x0erand_range_max\x18\x04 \x01(\x05R\frandRangeMax\x12\x16\n" + + "\x06packet\x18\x05 \x01(\fR\x06packet\x12\x1b\n" + + "\tdelay_min\x18\x06 \x01(\x03R\bdelayMin\x12\x1b\n" + + "\tdelay_max\x18\a \x01(\x03R\bdelayMax\"\x87\x01\n" + + "\x06Config\x12\x1b\n" + + "\treset_min\x18\x01 \x01(\x03R\bresetMin\x12\x1b\n" + + "\treset_max\x18\x02 \x01(\x03R\bresetMax\x12C\n" + + "\x05items\x18\x03 \x03(\v2-.xray.transport.internet.finalmask.noise.ItemR\x05itemsB\x97\x01\n" + + "+com.xray.transport.internet.finalmask.noiseP\x01Z xray.transport.internet.finalmask.noise.Item + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_transport_internet_finalmask_noise_config_proto_init() } +func file_transport_internet_finalmask_noise_config_proto_init() { + if File_transport_internet_finalmask_noise_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_noise_config_proto_rawDesc), len(file_transport_internet_finalmask_noise_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_transport_internet_finalmask_noise_config_proto_goTypes, + DependencyIndexes: file_transport_internet_finalmask_noise_config_proto_depIdxs, + MessageInfos: file_transport_internet_finalmask_noise_config_proto_msgTypes, + }.Build() + File_transport_internet_finalmask_noise_config_proto = out.File + file_transport_internet_finalmask_noise_config_proto_goTypes = nil + file_transport_internet_finalmask_noise_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/noise/config.proto b/subproject/Xray-core-main/transport/internet/finalmask/noise/config.proto new file mode 100644 index 00000000..d874b973 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/noise/config.proto @@ -0,0 +1,23 @@ +syntax = "proto3"; + +package xray.transport.internet.finalmask.noise; +option csharp_namespace = "Xray.Transport.Internet.Finalmask.Noise"; +option go_package = "github.com/xtls/xray-core/transport/internet/finalmask/noise"; +option java_package = "com.xray.transport.internet.finalmask.noise"; +option java_multiple_files = true; + +message Item { + int64 rand_min = 1; + int64 rand_max = 2; + int32 rand_range_min = 3; + int32 rand_range_max = 4; + bytes packet = 5; + int64 delay_min = 6; + int64 delay_max = 7; +} + +message Config { + int64 reset_min = 1; + int64 reset_max = 2; + repeated Item items = 3; +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/noise/conn.go b/subproject/Xray-core-main/transport/internet/finalmask/noise/conn.go new file mode 100644 index 00000000..c3ab89ab --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/noise/conn.go @@ -0,0 +1,96 @@ +package noise + +import ( + "net" + "sync" + "time" + + "github.com/xtls/xray-core/common/crypto" +) + +type noiseConn struct { + net.PacketConn + config *Config + m map[string]time.Time + stop chan struct{} + once sync.Once + mutex sync.RWMutex +} + +func NewConnClient(c *Config, raw net.PacketConn) (net.PacketConn, error) { + conn := &noiseConn{ + PacketConn: raw, + config: c, + m: make(map[string]time.Time), + stop: make(chan struct{}), + } + + if conn.config.ResetMax > 0 { + go conn.reset() + } + + return conn, nil +} + +func NewConnServer(c *Config, raw net.PacketConn) (net.PacketConn, error) { + return NewConnClient(c, raw) +} + +func (c *noiseConn) reset() { + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + for { + select { + case <-ticker.C: + c.mutex.RLock() + now := time.Now() + timeOut := make([]string, 0, len(c.m)) + for key, last := range c.m { + if now.After(last) { + timeOut = append(timeOut, key) + } + } + c.mutex.RUnlock() + + for _, key := range timeOut { + c.mutex.Lock() + delete(c.m, key) + c.mutex.Unlock() + } + case <-c.stop: + return + } + } +} + +func (c *noiseConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { + c.mutex.RLock() + _, ready := c.m[addr.String()] + c.mutex.RUnlock() + + if !ready { + c.mutex.Lock() + _, ready = c.m[addr.String()] + if !ready { + for _, item := range c.config.Items { + if item.RandMax > 0 { + item.Packet = make([]byte, crypto.RandBetween(item.RandMin, item.RandMax)) + crypto.RandBytesBetween(item.Packet, byte(item.RandRangeMin), byte(item.RandRangeMax)) + } + c.PacketConn.WriteTo(item.Packet, addr) + time.Sleep(time.Duration(crypto.RandBetween(item.DelayMin, item.DelayMax)) * time.Millisecond) + } + c.m[addr.String()] = time.Now().Add(time.Duration(crypto.RandBetween(c.config.ResetMin, c.config.ResetMax)) * time.Second) + } + c.mutex.Unlock() + } + + return c.PacketConn.WriteTo(p, addr) +} + +func (c *noiseConn) Close() error { + c.once.Do(func() { + close(c.stop) + }) + return c.PacketConn.Close() +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/salamander/config.go b/subproject/Xray-core-main/transport/internet/finalmask/salamander/config.go new file mode 100644 index 00000000..8df1285d --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/salamander/config.go @@ -0,0 +1,19 @@ +package salamander + +import ( + "net" +) + +func (c *Config) UDP() { +} + +func (c *Config) WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) { + return NewConnClient(c, raw) +} + +func (c *Config) WrapPacketConnServer(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) { + return NewConnServer(c, raw) +} + +func (c *Config) HeaderConn() { +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/salamander/config.pb.go b/subproject/Xray-core-main/transport/internet/finalmask/salamander/config.pb.go new file mode 100644 index 00000000..949df60b --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/salamander/config.pb.go @@ -0,0 +1,123 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: transport/internet/finalmask/salamander/config.proto + +package salamander + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + Password string `protobuf:"bytes,1,opt,name=password,proto3" json:"password,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_transport_internet_finalmask_salamander_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_finalmask_salamander_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_transport_internet_finalmask_salamander_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Config) GetPassword() string { + if x != nil { + return x.Password + } + return "" +} + +var File_transport_internet_finalmask_salamander_config_proto protoreflect.FileDescriptor + +const file_transport_internet_finalmask_salamander_config_proto_rawDesc = "" + + "\n" + + "4transport/internet/finalmask/salamander/config.proto\x12,xray.transport.internet.finalmask.salamander\"$\n" + + "\x06Config\x12\x1a\n" + + "\bpassword\x18\x01 \x01(\tR\bpasswordB\xa6\x01\n" + + "0com.xray.transport.internet.finalmask.salamanderP\x01ZAgithub.com/xtls/xray-core/transport/internet/finalmask/salamander\xaa\x02,Xray.Transport.Internet.Finalmask.Salamanderb\x06proto3" + +var ( + file_transport_internet_finalmask_salamander_config_proto_rawDescOnce sync.Once + file_transport_internet_finalmask_salamander_config_proto_rawDescData []byte +) + +func file_transport_internet_finalmask_salamander_config_proto_rawDescGZIP() []byte { + file_transport_internet_finalmask_salamander_config_proto_rawDescOnce.Do(func() { + file_transport_internet_finalmask_salamander_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_salamander_config_proto_rawDesc), len(file_transport_internet_finalmask_salamander_config_proto_rawDesc))) + }) + return file_transport_internet_finalmask_salamander_config_proto_rawDescData +} + +var file_transport_internet_finalmask_salamander_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_transport_internet_finalmask_salamander_config_proto_goTypes = []any{ + (*Config)(nil), // 0: xray.transport.internet.finalmask.salamander.Config +} +var file_transport_internet_finalmask_salamander_config_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_transport_internet_finalmask_salamander_config_proto_init() } +func file_transport_internet_finalmask_salamander_config_proto_init() { + if File_transport_internet_finalmask_salamander_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_salamander_config_proto_rawDesc), len(file_transport_internet_finalmask_salamander_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_transport_internet_finalmask_salamander_config_proto_goTypes, + DependencyIndexes: file_transport_internet_finalmask_salamander_config_proto_depIdxs, + MessageInfos: file_transport_internet_finalmask_salamander_config_proto_msgTypes, + }.Build() + File_transport_internet_finalmask_salamander_config_proto = out.File + file_transport_internet_finalmask_salamander_config_proto_goTypes = nil + file_transport_internet_finalmask_salamander_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/salamander/config.proto b/subproject/Xray-core-main/transport/internet/finalmask/salamander/config.proto new file mode 100644 index 00000000..34bd4cef --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/salamander/config.proto @@ -0,0 +1,12 @@ +syntax = "proto3"; + +package xray.transport.internet.finalmask.salamander; +option csharp_namespace = "Xray.Transport.Internet.Finalmask.Salamander"; +option go_package = "github.com/xtls/xray-core/transport/internet/finalmask/salamander"; +option java_package = "com.xray.transport.internet.finalmask.salamander"; +option java_multiple_files = true; + +message Config { + string password = 1; +} + diff --git a/subproject/Xray-core-main/transport/internet/finalmask/salamander/conn.go b/subproject/Xray-core-main/transport/internet/finalmask/salamander/conn.go new file mode 100644 index 00000000..bfb4934a --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/salamander/conn.go @@ -0,0 +1,46 @@ +package salamander + +import ( + "net" + + "github.com/xtls/xray-core/common/errors" +) + +type salamanderConn struct { + net.PacketConn + obfs *SalamanderObfuscator +} + +func NewConnClient(c *Config, raw net.PacketConn) (net.PacketConn, error) { + ob, err := NewSalamanderObfuscator([]byte(c.Password)) + if err != nil { + return nil, errors.New("salamander err").Base(err) + } + + conn := &salamanderConn{ + PacketConn: raw, + obfs: ob, + } + + return conn, nil +} + +func NewConnServer(c *Config, raw net.PacketConn) (net.PacketConn, error) { + return NewConnClient(c, raw) +} + +func (c *salamanderConn) Size() int { + return smSaltLen +} + +func (c *salamanderConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { + c.obfs.Deobfuscate(p, p[smSaltLen:]) + + return len(p) - smSaltLen, addr, nil +} + +func (c *salamanderConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { + c.obfs.Obfuscate(p[smSaltLen:], p) + + return len(p), nil +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/salamander/salamander.go b/subproject/Xray-core-main/transport/internet/finalmask/salamander/salamander.go new file mode 100644 index 00000000..86d92dcd --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/salamander/salamander.go @@ -0,0 +1,69 @@ +package salamander + +import ( + "fmt" + "math/rand" + "sync" + "time" + + "golang.org/x/crypto/blake2b" +) + +const ( + smPSKMinLen = 4 + smSaltLen = 8 + smKeyLen = blake2b.Size256 +) + +var ErrPSKTooShort = fmt.Errorf("PSK must be at least %d bytes", smPSKMinLen) + +// SalamanderObfuscator is an obfuscator that obfuscates each packet with +// the BLAKE2b-256 hash of a pre-shared key combined with a random salt. +// Packet format: [8-byte salt][payload] +type SalamanderObfuscator struct { + PSK []byte + RandSrc *rand.Rand + + lk sync.Mutex +} + +func NewSalamanderObfuscator(psk []byte) (*SalamanderObfuscator, error) { + if len(psk) < smPSKMinLen { + return nil, ErrPSKTooShort + } + return &SalamanderObfuscator{ + PSK: psk, + RandSrc: rand.New(rand.NewSource(time.Now().UnixNano())), + }, nil +} + +func (o *SalamanderObfuscator) Obfuscate(in, out []byte) int { + outLen := len(in) + smSaltLen + if len(out) < outLen { + return 0 + } + o.lk.Lock() + _, _ = o.RandSrc.Read(out[:smSaltLen]) + o.lk.Unlock() + key := o.key(out[:smSaltLen]) + for i, c := range in { + out[i+smSaltLen] = c ^ key[i%smKeyLen] + } + return outLen +} + +func (o *SalamanderObfuscator) Deobfuscate(in, out []byte) int { + outLen := len(in) - smSaltLen + if outLen <= 0 || len(out) < outLen { + return 0 + } + key := o.key(in[:smSaltLen]) + for i, c := range in[smSaltLen:] { + out[i] = c ^ key[i%smKeyLen] + } + return outLen +} + +func (o *SalamanderObfuscator) key(salt []byte) [smKeyLen]byte { + return blake2b.Sum256(append(o.PSK, salt...)) +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/salamander/salamander_test.go b/subproject/Xray-core-main/transport/internet/finalmask/salamander/salamander_test.go new file mode 100644 index 00000000..ffd50821 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/salamander/salamander_test.go @@ -0,0 +1,81 @@ +package salamander_test + +import ( + "crypto/rand" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/xtls/xray-core/transport/internet/finalmask/salamander" +) + +const ( + smSaltLen = 8 +) + +func BenchmarkSalamanderObfuscator_Obfuscate(b *testing.B) { + o, _ := salamander.NewSalamanderObfuscator([]byte("average_password")) + in := make([]byte, 1200) + _, _ = rand.Read(in) + out := make([]byte, 2048) + b.ResetTimer() + for i := 0; i < b.N; i++ { + o.Obfuscate(in, out) + } +} + +func BenchmarkSalamanderObfuscator_Deobfuscate(b *testing.B) { + o, _ := salamander.NewSalamanderObfuscator([]byte("average_password")) + in := make([]byte, 1200) + _, _ = rand.Read(in) + out := make([]byte, 2048) + b.ResetTimer() + for i := 0; i < b.N; i++ { + o.Deobfuscate(in, out) + } +} + +func TestSalamanderObfuscator(t *testing.T) { + o, _ := salamander.NewSalamanderObfuscator([]byte("average_password")) + in := make([]byte, 1200) + oOut := make([]byte, 2048) + dOut := make([]byte, 2048) + for i := 0; i < 1000; i++ { + _, _ = rand.Read(in) + n := o.Obfuscate(in, oOut) + assert.Equal(t, len(in)+smSaltLen, n) + n = o.Deobfuscate(oOut[:n], dOut) + assert.Equal(t, len(in), n) + assert.Equal(t, in, dOut[:n]) + } +} + +func TestSalamanderInPlace(t *testing.T) { + o, _ := salamander.NewSalamanderObfuscator([]byte("average_password")) + + in := make([]byte, 1200) + out := make([]byte, 2048) + _, _ = rand.Read(in) + o.Obfuscate(in, out) + + out2 := make([]byte, 2048) + copy(out2[smSaltLen:], in) + o.Obfuscate(out2[smSaltLen:], out2) + + dOut := make([]byte, 2048) + o.Deobfuscate(out, dOut) + + o.Deobfuscate(out2, out2) + + assert.Equal(t, in, dOut[:1200]) + assert.Equal(t, in, out2[:1200]) +} + +func TestSalamanderBounce(t *testing.T) { + o, _ := salamander.NewSalamanderObfuscator([]byte("average_password")) + buf := make([]byte, 8) + for i := 0; i < 1000; i++ { + _, _ = rand.Read(buf) + n := o.Deobfuscate(buf, buf) + assert.Equal(t, 0, n) + } +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/sudoku/codec.go b/subproject/Xray-core-main/transport/internet/finalmask/sudoku/codec.go new file mode 100644 index 00000000..e748b8b7 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/sudoku/codec.go @@ -0,0 +1,163 @@ +package sudoku + +import ( + "fmt" + "math/rand" +) + +var perm4 = [24][4]byte{ + {0, 1, 2, 3}, + {0, 1, 3, 2}, + {0, 2, 1, 3}, + {0, 2, 3, 1}, + {0, 3, 1, 2}, + {0, 3, 2, 1}, + {1, 0, 2, 3}, + {1, 0, 3, 2}, + {1, 2, 0, 3}, + {1, 2, 3, 0}, + {1, 3, 0, 2}, + {1, 3, 2, 0}, + {2, 0, 1, 3}, + {2, 0, 3, 1}, + {2, 1, 0, 3}, + {2, 1, 3, 0}, + {2, 3, 0, 1}, + {2, 3, 1, 0}, + {3, 0, 1, 2}, + {3, 0, 2, 1}, + {3, 1, 0, 2}, + {3, 1, 2, 0}, + {3, 2, 0, 1}, + {3, 2, 1, 0}, +} + +type codec struct { + tables []*table + rng *rand.Rand + paddingChance int + tableIndex int +} + +func newCodec(tables []*table, pMin, pMax int) *codec { + if len(tables) == 0 { + tables = nil + } + rng := newSeededRand() + return &codec{ + tables: tables, + rng: rng, + paddingChance: pickPaddingChance(rng, pMin, pMax), + } +} + +func pickPaddingChance(rng *rand.Rand, pMin, pMax int) int { + if pMin < 0 { + pMin = 0 + } + if pMax < pMin { + pMax = pMin + } + if pMin > 100 { + pMin = 100 + } + if pMax > 100 { + pMax = 100 + } + if pMax == pMin { + return pMin + } + return pMin + rng.Intn(pMax-pMin+1) +} + +func (c *codec) shouldPad() bool { + if c.paddingChance <= 0 { + return false + } + if c.paddingChance >= 100 { + return true + } + return c.rng.Intn(100) < c.paddingChance +} + +func (c *codec) currentTable() *table { + if len(c.tables) == 0 { + return nil + } + return c.tables[c.tableIndex%len(c.tables)] +} + +func (c *codec) randomPadding(t *table) byte { + pool := t.layout.paddingPool + return pool[c.rng.Intn(len(pool))] +} + +func (c *codec) encode(in []byte) ([]byte, error) { + if len(in) == 0 { + return nil, nil + } + + out := make([]byte, 0, len(in)*6+8) + for _, b := range in { + t := c.currentTable() + if t == nil { + return nil, fmt.Errorf("sudoku table set missing") + } + if c.shouldPad() { + out = append(out, c.randomPadding(t)) + } + + enc := t.encode[b] + if len(enc) == 0 { + return nil, fmt.Errorf("sudoku encode table missing for byte %d", b) + } + + hints := enc[c.rng.Intn(len(enc))] + perm := perm4[c.rng.Intn(len(perm4))] + for _, idx := range perm { + if c.shouldPad() { + out = append(out, c.randomPadding(t)) + } + out = append(out, hints[idx]) + } + c.tableIndex++ + } + + if c.shouldPad() { + if t := c.currentTable(); t != nil { + out = append(out, c.randomPadding(t)) + } + } + + return out, nil +} + +func decodeBytes(tables []*table, tableIndex *int, in []byte, hintBuf []byte, out []byte) ([]byte, []byte, error) { + if len(tables) == 0 { + return hintBuf, out, fmt.Errorf("sudoku table set missing") + } + for _, b := range in { + t := tables[*tableIndex%len(tables)] + if !t.layout.isHint(b) { + continue + } + + hintBuf = append(hintBuf, b) + if len(hintBuf) < 4 { + continue + } + + keyBytes := sort4([4]byte{hintBuf[0], hintBuf[1], hintBuf[2], hintBuf[3]}) + key := packKey(keyBytes) + decoded, ok := t.decode[key] + if !ok { + return hintBuf[:0], out, fmt.Errorf("invalid sudoku hint tuple") + } + + out = append(out, decoded) + hintBuf = hintBuf[:0] + *tableIndex++ + } + + return hintBuf, out, nil +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/sudoku/config.go b/subproject/Xray-core-main/transport/internet/finalmask/sudoku/config.go new file mode 100644 index 00000000..58a4562f --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/sudoku/config.go @@ -0,0 +1,57 @@ +package sudoku + +import ( + "net" + + "github.com/xtls/xray-core/common/errors" +) + +func (c *Config) TCP() { +} + +func (c *Config) UDP() { +} + +// Sudoku in finalmask mode is a pure appearance transform with no standalone handshake. +// TCP always keeps classic sudoku on uplink and uses packed downlink optimization on server writes. +func (c *Config) WrapConnClient(raw net.Conn) (net.Conn, error) { + return newPackedDirectionalConn(raw, c, true) +} + +func (c *Config) WrapConnServer(raw net.Conn) (net.Conn, error) { + return newPackedDirectionalConn(raw, c, false) +} + +func newPackedDirectionalConn(raw net.Conn, config *Config, readPacked bool) (net.Conn, error) { + pureReader, pureWriter, err := newPureReaderWriter(raw, config) + if err != nil { + return nil, err + } + packedReader, packedWriter, err := newPackedReaderWriter(raw, config) + if err != nil { + return nil, err + } + + reader, writer := pureReader, pureWriter + if readPacked { + reader = packedReader + } else { + writer = packedWriter + } + + return newWrappedConn(raw, reader, writer), nil +} + +func (c *Config) WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) { + if level != levelCount { + return nil, errors.New("sudoku udp mask must be the innermost mask in chain") + } + return NewUDPConn(raw, c) +} + +func (c *Config) WrapPacketConnServer(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) { + if level != levelCount { + return nil, errors.New("sudoku udp mask must be the innermost mask in chain") + } + return NewUDPConn(raw, c) +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/sudoku/config.pb.go b/subproject/Xray-core-main/transport/internet/finalmask/sudoku/config.pb.go new file mode 100644 index 00000000..56b544eb --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/sudoku/config.pb.go @@ -0,0 +1,170 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: transport/internet/finalmask/sudoku/config.proto + +package sudoku + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + Password string `protobuf:"bytes,1,opt,name=password,proto3" json:"password,omitempty"` + Ascii string `protobuf:"bytes,2,opt,name=ascii,proto3" json:"ascii,omitempty"` + CustomTable string `protobuf:"bytes,3,opt,name=custom_table,json=customTable,proto3" json:"custom_table,omitempty"` + PaddingMin uint32 `protobuf:"varint,4,opt,name=padding_min,json=paddingMin,proto3" json:"padding_min,omitempty"` + PaddingMax uint32 `protobuf:"varint,5,opt,name=padding_max,json=paddingMax,proto3" json:"padding_max,omitempty"` + CustomTables []string `protobuf:"bytes,7,rep,name=custom_tables,json=customTables,proto3" json:"custom_tables,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_transport_internet_finalmask_sudoku_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_finalmask_sudoku_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_transport_internet_finalmask_sudoku_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Config) GetPassword() string { + if x != nil { + return x.Password + } + return "" +} + +func (x *Config) GetAscii() string { + if x != nil { + return x.Ascii + } + return "" +} + +func (x *Config) GetCustomTable() string { + if x != nil { + return x.CustomTable + } + return "" +} + +func (x *Config) GetPaddingMin() uint32 { + if x != nil { + return x.PaddingMin + } + return 0 +} + +func (x *Config) GetPaddingMax() uint32 { + if x != nil { + return x.PaddingMax + } + return 0 +} + +func (x *Config) GetCustomTables() []string { + if x != nil { + return x.CustomTables + } + return nil +} + +var File_transport_internet_finalmask_sudoku_config_proto protoreflect.FileDescriptor + +const file_transport_internet_finalmask_sudoku_config_proto_rawDesc = "" + + "\n" + + "0transport/internet/finalmask/sudoku/config.proto\x12(xray.transport.internet.finalmask.sudoku\"\xc4\x01\n" + + "\x06Config\x12\x1a\n" + + "\bpassword\x18\x01 \x01(\tR\bpassword\x12\x14\n" + + "\x05ascii\x18\x02 \x01(\tR\x05ascii\x12!\n" + + "\fcustom_table\x18\x03 \x01(\tR\vcustomTable\x12\x1f\n" + + "\vpadding_min\x18\x04 \x01(\rR\n" + + "paddingMin\x12\x1f\n" + + "\vpadding_max\x18\x05 \x01(\rR\n" + + "paddingMax\x12#\n" + + "\rcustom_tables\x18\a \x03(\tR\fcustomTablesB\x9a\x01\n" + + ",com.xray.transport.internet.finalmask.sudokuP\x01Z=github.com/xtls/xray-core/transport/internet/finalmask/sudoku\xaa\x02(Xray.Transport.Internet.Finalmask.Sudokub\x06proto3" + +var ( + file_transport_internet_finalmask_sudoku_config_proto_rawDescOnce sync.Once + file_transport_internet_finalmask_sudoku_config_proto_rawDescData []byte +) + +func file_transport_internet_finalmask_sudoku_config_proto_rawDescGZIP() []byte { + file_transport_internet_finalmask_sudoku_config_proto_rawDescOnce.Do(func() { + file_transport_internet_finalmask_sudoku_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_sudoku_config_proto_rawDesc), len(file_transport_internet_finalmask_sudoku_config_proto_rawDesc))) + }) + return file_transport_internet_finalmask_sudoku_config_proto_rawDescData +} + +var file_transport_internet_finalmask_sudoku_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_transport_internet_finalmask_sudoku_config_proto_goTypes = []any{ + (*Config)(nil), // 0: xray.transport.internet.finalmask.sudoku.Config +} +var file_transport_internet_finalmask_sudoku_config_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_transport_internet_finalmask_sudoku_config_proto_init() } +func file_transport_internet_finalmask_sudoku_config_proto_init() { + if File_transport_internet_finalmask_sudoku_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_sudoku_config_proto_rawDesc), len(file_transport_internet_finalmask_sudoku_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_transport_internet_finalmask_sudoku_config_proto_goTypes, + DependencyIndexes: file_transport_internet_finalmask_sudoku_config_proto_depIdxs, + MessageInfos: file_transport_internet_finalmask_sudoku_config_proto_msgTypes, + }.Build() + File_transport_internet_finalmask_sudoku_config_proto = out.File + file_transport_internet_finalmask_sudoku_config_proto_goTypes = nil + file_transport_internet_finalmask_sudoku_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/sudoku/config.proto b/subproject/Xray-core-main/transport/internet/finalmask/sudoku/config.proto new file mode 100644 index 00000000..7089e0dd --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/sudoku/config.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +package xray.transport.internet.finalmask.sudoku; +option csharp_namespace = "Xray.Transport.Internet.Finalmask.Sudoku"; +option go_package = "github.com/xtls/xray-core/transport/internet/finalmask/sudoku"; +option java_package = "com.xray.transport.internet.finalmask.sudoku"; +option java_multiple_files = true; + +message Config { + string password = 1; + string ascii = 2; + string custom_table = 3; + uint32 padding_min = 4; + uint32 padding_max = 5; + repeated string custom_tables = 7; +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/sudoku/conn_tcp.go b/subproject/Xray-core-main/transport/internet/finalmask/sudoku/conn_tcp.go new file mode 100644 index 00000000..75abb98e --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/sudoku/conn_tcp.go @@ -0,0 +1,212 @@ +package sudoku + +import ( + "bufio" + "io" + "net" + "sync" + + "github.com/xtls/xray-core/transport/internet/finalmask" +) + +const ioBufferSize = 32 * 1024 + +var _ finalmask.TcpMaskConn = (*wrappedConn)(nil) + +type streamDecoder interface { + decodeChunk(in []byte, pending []byte) ([]byte, error) + reset() +} + +type streamReader struct { + reader *bufio.Reader + rawBuf []byte + pending []byte + decode streamDecoder + mu sync.Mutex +} + +func newStreamReader(raw net.Conn, decode streamDecoder) io.Reader { + return &streamReader{ + reader: bufio.NewReaderSize(raw, ioBufferSize), + rawBuf: make([]byte, ioBufferSize), + pending: make([]byte, 0, 4096), + decode: decode, + } +} + +func (r *streamReader) Read(p []byte) (int, error) { + r.mu.Lock() + defer r.mu.Unlock() + + if n, ok := drainPending(p, &r.pending); ok { + return n, nil + } + + for len(r.pending) == 0 { + nr, rErr := r.reader.Read(r.rawBuf) + if nr > 0 { + var dErr error + r.pending, dErr = r.decode.decodeChunk(r.rawBuf[:nr], r.pending) + if dErr != nil { + return 0, dErr + } + } + + if rErr != nil { + if rErr == io.EOF { + r.decode.reset() + if len(r.pending) > 0 { + break + } + } + return 0, rErr + } + } + + n, _ := drainPending(p, &r.pending) + return n, nil +} + +type streamWriter struct { + conn net.Conn + encode func([]byte) ([]byte, error) + mu sync.Mutex +} + +func newStreamWriter(raw net.Conn, encode func([]byte) ([]byte, error)) io.Writer { + return &streamWriter{ + conn: raw, + encode: encode, + } +} + +func (w *streamWriter) Write(p []byte) (int, error) { + if len(p) == 0 { + return 0, nil + } + + w.mu.Lock() + defer w.mu.Unlock() + + encoded, err := w.encode(p) + if err != nil { + return 0, err + } + if err := writeAll(w.conn, encoded); err != nil { + return 0, err + } + return len(p), nil +} + +type wrappedConn struct { + net.Conn + reader io.Reader + writer io.Writer +} + +type closeWriteConn interface { + CloseWrite() error +} + +func newWrappedConn(raw net.Conn, reader io.Reader, writer io.Writer) net.Conn { + return &wrappedConn{ + Conn: raw, + reader: reader, + writer: writer, + } +} + +func (c *wrappedConn) Read(p []byte) (int, error) { + return c.reader.Read(p) +} + +func (c *wrappedConn) Write(p []byte) (int, error) { + return c.writer.Write(p) +} + +func (c *wrappedConn) TcpMaskConn() {} + +func (c *wrappedConn) RawConn() net.Conn { + return c.Conn +} + +func (c *wrappedConn) Splice() bool { + // Sudoku transforms the entire stream; bypassing it would disable masking. + return false +} + +func (c *wrappedConn) CloseWrite() error { + if raw, ok := c.Conn.(closeWriteConn); ok { + return raw.CloseWrite() + } + return net.ErrClosed +} + +func NewTCPConn(raw net.Conn, config *Config) (net.Conn, error) { + reader, writer, err := newPureReaderWriter(raw, config) + if err != nil { + return nil, err + } + return newWrappedConn(raw, reader, writer), nil +} + +func newPureReaderWriter(raw net.Conn, config *Config) (io.Reader, io.Writer, error) { + tables, err := getTables(config) + if err != nil { + return nil, nil, err + } + + pMin, pMax := normalizedPadding(config) + c := newCodec(tables, pMin, pMax) + return newStreamReader(raw, newHintStreamDecoder(tables)), newStreamWriter(raw, c.encode), nil +} + +type hintStreamDecoder struct { + tables []*table + tableIndex int + hintBuf []byte +} + +func newHintStreamDecoder(tables []*table) *hintStreamDecoder { + return &hintStreamDecoder{ + tables: tables, + hintBuf: make([]byte, 0, 4), + } +} + +func (d *hintStreamDecoder) decodeChunk(in []byte, pending []byte) ([]byte, error) { + var err error + d.hintBuf, pending, err = decodeBytes(d.tables, &d.tableIndex, in, d.hintBuf, pending) + return pending, err +} + +func (d *hintStreamDecoder) reset() {} + +func drainPending(p []byte, pending *[]byte) (int, bool) { + if len(*pending) == 0 { + return 0, false + } + + n := copy(p, *pending) + if n >= len(*pending) { + *pending = (*pending)[:0] + return n, true + } + + remaining := len(*pending) - n + copy(*pending, (*pending)[n:]) + *pending = (*pending)[:remaining] + return n, true +} + +func writeAll(conn net.Conn, b []byte) error { + for len(b) > 0 { + n, err := conn.Write(b) + if err != nil { + return err + } + b = b[n:] + } + return nil +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/sudoku/conn_tcp_packed.go b/subproject/Xray-core-main/transport/internet/finalmask/sudoku/conn_tcp_packed.go new file mode 100644 index 00000000..fa3c4c86 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/sudoku/conn_tcp_packed.go @@ -0,0 +1,182 @@ +package sudoku + +import ( + "fmt" + "io" + "net" +) + +type packedEncoder struct { + layouts []*byteLayout + codec *codec + groupIndex int +} + +func newPackedEncoder(tables []*table, pMin, pMax int) *packedEncoder { + layouts := make([]*byteLayout, 0, len(tables)) + for _, t := range tables { + layouts = append(layouts, t.layout) + } + if len(layouts) == 0 { + layouts = append(layouts, entropyLayout()) + } + return &packedEncoder{ + layouts: layouts, + codec: newCodec(nil, pMin, pMax), + } +} + +func (e *packedEncoder) encode(p []byte) ([]byte, error) { + out := make([]byte, 0, len(p)*2+8) + var bitBuf uint64 + var bitCount uint8 + + for _, b := range p { + bitBuf = (bitBuf << 8) | uint64(b) + bitCount += 8 + + for bitCount >= 6 { + bitCount -= 6 + layout := e.layouts[e.groupIndex%len(e.layouts)] + group := byte(bitBuf >> bitCount) + out = e.maybePad(out, layout) + out = append(out, layout.encodeGroup(group&0x3f)) + e.groupIndex++ + if bitCount > 0 { + bitBuf &= (uint64(1) << bitCount) - 1 + } else { + bitBuf = 0 + } + } + } + + if bitCount > 0 { + layout := e.layouts[e.groupIndex%len(e.layouts)] + group := byte(bitBuf << (6 - bitCount)) + out = e.maybePad(out, layout) + out = append(out, layout.encodeGroup(group&0x3f)) + e.groupIndex++ + nextLayout := e.layouts[e.groupIndex%len(e.layouts)] + out = append(out, nextLayout.padMarker) + } + + out = e.maybePad(out, e.layouts[e.groupIndex%len(e.layouts)]) + return out, nil +} + +func (e *packedEncoder) maybePad(out []byte, layout *byteLayout) []byte { + if !e.codec.shouldPad() { + return out + } + if len(layout.paddingPool) == 1 { + return append(out, layout.paddingPool[0]) + } + for { + b := layout.paddingPool[e.codec.rng.Intn(len(layout.paddingPool))] + if b != layout.padMarker { + return append(out, b) + } + } +} + +type packedStreamDecoder struct { + layouts []*byteLayout + groupIndex int + bitBuf uint64 + bitCount int +} + +func (d *packedStreamDecoder) decodeChunk(in []byte, pending []byte) ([]byte, error) { + var err error + d.bitBuf, d.bitCount, d.groupIndex, pending, err = decodePackedBytes( + d.layouts, + in, + d.bitBuf, + d.bitCount, + d.groupIndex, + pending, + ) + return pending, err +} + +func (d *packedStreamDecoder) reset() { + d.bitBuf = 0 + d.bitCount = 0 +} + +func NewPackedTCPConn(raw net.Conn, config *Config) (net.Conn, error) { + reader, writer, err := newPackedReaderWriter(raw, config) + if err != nil { + return nil, err + } + return newWrappedConn(raw, reader, writer), nil +} + +func newPackedReaderWriter(raw net.Conn, config *Config) (io.Reader, io.Writer, error) { + tables, err := getTables(config) + if err != nil { + return nil, nil, err + } + + pMin, pMax := normalizedPadding(config) + encoder := newPackedEncoder(tables, pMin, pMax) + decoder := &packedStreamDecoder{ + layouts: tablesToLayouts(tables), + } + return newStreamReader(raw, decoder), newStreamWriter(raw, encoder.encode), nil +} + +func tablesToLayouts(tables []*table) []*byteLayout { + layouts := make([]*byteLayout, 0, len(tables)) + for _, t := range tables { + layouts = append(layouts, t.layout) + } + if len(layouts) == 0 { + layouts = append(layouts, entropyLayout()) + } + return layouts +} + +func decodePackedBytes( + layouts []*byteLayout, + in []byte, + bitBuf uint64, + bitCount int, + groupIndex int, + out []byte, +) (uint64, int, int, []byte, error) { + if len(layouts) == 0 { + return bitBuf, bitCount, groupIndex, out, fmt.Errorf("sudoku layout set missing") + } + for _, b := range in { + layout := layouts[groupIndex%len(layouts)] + if !layout.isHint(b) { + if b == layout.padMarker { + bitBuf = 0 + bitCount = 0 + } + continue + } + + group, ok := layout.decodeGroup(b) + if !ok { + return bitBuf, bitCount, groupIndex, out, fmt.Errorf("invalid packed sudoku byte: %d", b) + } + groupIndex++ + + bitBuf = (bitBuf << 6) | uint64(group) + bitCount += 6 + + for bitCount >= 8 { + bitCount -= 8 + out = append(out, byte(bitBuf>>bitCount)) + if bitCount > 0 { + bitBuf &= (uint64(1) << bitCount) - 1 + } else { + bitBuf = 0 + } + } + } + + return bitBuf, bitCount, groupIndex, out, nil +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/sudoku/conn_udp.go b/subproject/Xray-core-main/transport/internet/finalmask/sudoku/conn_udp.go new file mode 100644 index 00000000..c2b2f4dd --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/sudoku/conn_udp.go @@ -0,0 +1,102 @@ +package sudoku + +import ( + "io" + "net" + "sync" + "time" +) + +type udpConn struct { + conn net.PacketConn + tables []*table + pMin int + pMax int + + readBuf []byte + + readMu sync.Mutex + writeMu sync.Mutex +} + +func NewUDPConn(raw net.PacketConn, config *Config) (net.PacketConn, error) { + tables, err := getTables(config) + if err != nil { + return nil, err + } + + pMin, pMax := normalizedPadding(config) + return &udpConn{ + conn: raw, + tables: tables, + pMin: pMin, + pMax: pMax, + readBuf: make([]byte, 65535), + }, nil +} + +func (c *udpConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { + c.readMu.Lock() + defer c.readMu.Unlock() + + n, addr, err = c.conn.ReadFrom(c.readBuf) + if err != nil { + return n, addr, err + } + + decoded := make([]byte, 0, n/4+1) + hints := make([]byte, 0, 4) + tableIndex := 0 + hints, decoded, err = decodeBytes(c.tables, &tableIndex, c.readBuf[:n], hints, decoded) + if err != nil { + return 0, addr, err + } + if len(hints) != 0 { + return 0, addr, io.ErrUnexpectedEOF + } + if len(p) < len(decoded) { + return 0, addr, io.ErrShortBuffer + } + copy(p, decoded) + return len(decoded), addr, nil +} + +func (c *udpConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { + c.writeMu.Lock() + defer c.writeMu.Unlock() + + // UDP decoding restarts at table 0 for every datagram, so encoding must do the same. + encoded, err := newCodec(c.tables, c.pMin, c.pMax).encode(p) + if err != nil { + return 0, err + } + + nn, err := c.conn.WriteTo(encoded, addr) + if err != nil { + return 0, err + } + if nn != len(encoded) { + return 0, io.ErrShortWrite + } + return len(p), nil +} + +func (c *udpConn) Close() error { + return c.conn.Close() +} + +func (c *udpConn) LocalAddr() net.Addr { + return c.conn.LocalAddr() +} + +func (c *udpConn) SetDeadline(t time.Time) error { + return c.conn.SetDeadline(t) +} + +func (c *udpConn) SetReadDeadline(t time.Time) error { + return c.conn.SetReadDeadline(t) +} + +func (c *udpConn) SetWriteDeadline(t time.Time) error { + return c.conn.SetWriteDeadline(t) +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/sudoku/sudoku_test.go b/subproject/Xray-core-main/transport/internet/finalmask/sudoku/sudoku_test.go new file mode 100644 index 00000000..4713e4bc --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/sudoku/sudoku_test.go @@ -0,0 +1,1390 @@ +package sudoku + +import ( + "bytes" + "crypto/ecdh" + "crypto/rand" + cryptotls "crypto/tls" + "encoding/base64" + "encoding/hex" + "fmt" + "io" + stdnet "net" + "os" + "os/exec" + "os/signal" + "path/filepath" + "sync" + "syscall" + "testing" + "time" + + "github.com/xtls/xray-core/app/dispatcher" + "github.com/xtls/xray-core/app/log" + "github.com/xtls/xray-core/app/proxyman" + clog "github.com/xtls/xray-core/common/log" + xnet "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/common/protocol/tls/cert" + "github.com/xtls/xray-core/common/serial" + "github.com/xtls/xray-core/common/uuid" + core "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/proxy/dokodemo" + "github.com/xtls/xray-core/proxy/freedom" + hyproxy "github.com/xtls/xray-core/proxy/hysteria" + hyaccount "github.com/xtls/xray-core/proxy/hysteria/account" + "github.com/xtls/xray-core/proxy/vless" + vin "github.com/xtls/xray-core/proxy/vless/inbound" + vout "github.com/xtls/xray-core/proxy/vless/outbound" + testingtcp "github.com/xtls/xray-core/testing/servers/tcp" + "github.com/xtls/xray-core/transport/internet" + hytransport "github.com/xtls/xray-core/transport/internet/hysteria" + "github.com/xtls/xray-core/transport/internet/reality" + splithttp "github.com/xtls/xray-core/transport/internet/splithttp" + transtcp "github.com/xtls/xray-core/transport/internet/tcp" + xtls "github.com/xtls/xray-core/transport/internet/tls" + "google.golang.org/protobuf/proto" +) + +var ( + e2eBinaryOnce sync.Once + e2eBinaryPath string + e2eBinaryErr error +) + +type trafficMode struct { + name string + config *Config +} + +type protocolCase struct { + name string + transport string + run func(t *testing.T, bin string, mode trafficMode) caseResult +} + +type caseResult struct { + Protocol string + Mode string + TotalBytes int + ASCIIBytes int + ASCIIRatio float64 + AvgHammingOnes float64 + RotationSeen int + RotationExpected int + DecodedUnits int + ClientToServer directionResult + ServerToClient directionResult +} + +type directionResult struct { + RawBytes int + ASCIIBytes int + ASCIIRatio float64 + AvgHammingOnes float64 + RotationSeen int + DecodedUnits int +} + +type tcpRelay struct { + listener stdnet.Listener + target string + + mu sync.Mutex + captures []*tcpCapture + wg sync.WaitGroup + stopCh chan struct{} +} + +type tcpCapture struct { + mu sync.Mutex + c2s []byte + s2c []byte +} + +type udpRelay struct { + conn stdnet.PacketConn + target *stdnet.UDPAddr + clientMu sync.Mutex + client *stdnet.UDPAddr + stopCh chan struct{} + wg sync.WaitGroup + captureMu sync.Mutex + c2s [][]byte + s2c [][]byte +} + +type tlsDecoy struct { + ln stdnet.Listener + done chan struct{} + wg sync.WaitGroup +} + +func TestSudokuE2ETemp(t *testing.T) { + if testing.Short() { + t.Skip("skipping sudoku e2e harness in short mode") + } + + bin := buildE2EBinary(t) + payloadSize := 192 * 1024 + modes := []trafficMode{ + { + name: "prefer_ascii", + config: &Config{ + Password: "sudoku-e2e-shared-secret", + Ascii: "prefer_ascii", + }, + }, + { + name: "prefer_entropy", + config: &Config{ + Password: "sudoku-e2e-shared-secret", + Ascii: "prefer_entropy", + CustomTables: []string{ + "xpxvvpvv", + "vxpvxvvp", + "pxvvxvvp", + "vpxvxvpv", + "xvpvvxpv", + "vvxpxpvv", + }, + }, + }, + } + + cases := []protocolCase{ + {name: "vless-reality", transport: "tcp", run: func(t *testing.T, bin string, mode trafficMode) caseResult { + return runVLESSRealityCase(t, bin, mode, payloadSize) + }}, + {name: "hysteria2", transport: "udp", run: func(t *testing.T, bin string, mode trafficMode) caseResult { + return runHysteria2Case(t, bin, mode, payloadSize) + }}, + {name: "vless-enc", transport: "tcp", run: func(t *testing.T, bin string, mode trafficMode) caseResult { + return runVLesseEncCase(t, bin, mode, payloadSize) + }}, + {name: "vless-xhttp", transport: "tcp", run: func(t *testing.T, bin string, mode trafficMode) caseResult { + return runVLESSXHTTPCase(t, bin, mode, payloadSize) + }}, + } + + results := make([]caseResult, 0, len(cases)*len(modes)) + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + for _, mode := range modes { + mode := mode + t.Run(mode.name, func(t *testing.T) { + result := tc.run(t, bin, mode) + if mode.name == "prefer_ascii" && result.ASCIIRatio < 0.97 { + t.Fatalf("%s %s ascii ratio %.4f < 0.97", tc.name, mode.name, result.ASCIIRatio) + } + if mode.name == "prefer_entropy" { + if result.RotationSeen != result.RotationExpected { + t.Fatalf("%s %s saw %d/%d rotation tables", tc.name, mode.name, result.RotationSeen, result.RotationExpected) + } + if diff := result.AvgHammingOnes - 5.0; diff < -0.3 || diff > 0.3 { + t.Fatalf("%s %s average ones %.4f too far from 5", tc.name, mode.name, result.AvgHammingOnes) + } + } + t.Logf( + "%s %s total=%d ascii=%.4f avg_ones=%.4f rotation=%d/%d c2s_ascii=%.4f s2c_ascii=%.4f", + tc.name, + mode.name, + result.TotalBytes, + result.ASCIIRatio, + result.AvgHammingOnes, + result.RotationSeen, + result.RotationExpected, + result.ClientToServer.ASCIIRatio, + result.ServerToClient.ASCIIRatio, + ) + results = append(results, result) + }) + } + }) + } + + for _, result := range results { + t.Logf( + "summary protocol=%s mode=%s bytes=%d ascii=%.4f avg_ones=%.4f rotation=%d/%d decoded=%d", + result.Protocol, + result.Mode, + result.TotalBytes, + result.ASCIIRatio, + result.AvgHammingOnes, + result.RotationSeen, + result.RotationExpected, + result.DecodedUnits, + ) + } +} + +func runVLESSRealityCase(t *testing.T, bin string, mode trafficMode, payloadSize int) caseResult { + backend := startXOREchoServer(t) + defer backend.Close() + + decoyCert, _ := cert.MustGenerate(nil, cert.CommonName("localhost"), cert.DNSNames("localhost")) + decoy := startTLSEchoDecoy(t, decoyCert) + defer decoy.Close() + + serverPort := testingtcp.PickPort() + relayPort := testingtcp.PickPort() + clientPort := testingtcp.PickPort() + + relay := startTCPRelay(t, int(relayPort), fmt.Sprintf("127.0.0.1:%d", serverPort)) + defer relay.Close() + + userID := protocol.NewID(uuid.New()) + realityPriv, realityPub := mustX25519Keypair(t) + shortID := mustDecodeHex(t, "0123456789abcdef") + + serverConfig := defaultApps(&core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &xnet.PortList{Range: []*xnet.PortRange{xnet.SinglePortRange(serverPort)}}, + Listen: xnet.NewIPOrDomain(xnet.LocalHostIP), + StreamSettings: &internet.StreamConfig{ + ProtocolName: "tcp", + TransportSettings: []*internet.TransportConfig{ + { + ProtocolName: "tcp", + Settings: serial.ToTypedMessage(&transtcp.Config{}), + }, + }, + SecurityType: serial.GetMessageType(&reality.Config{}), + SecuritySettings: []*serial.TypedMessage{ + serial.ToTypedMessage(&reality.Config{ + Show: true, + Dest: fmt.Sprintf("localhost:%d", decoy.Port()), + ServerNames: []string{"localhost"}, + PrivateKey: realityPriv, + ShortIds: [][]byte{shortID}, + Type: "tcp", + }), + }, + Tcpmasks: []*serial.TypedMessage{serial.ToTypedMessage(cloneConfig(mode.config))}, + }, + }), + ProxySettings: serial.ToTypedMessage(&vin.Config{ + Clients: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vless.Account{ + Id: userID.String(), + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + {ProxySettings: serial.ToTypedMessage(&freedom.Config{})}, + }, + }) + + clientConfig := defaultApps(&core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &xnet.PortList{Range: []*xnet.PortRange{xnet.SinglePortRange(clientPort)}}, + Listen: xnet.NewIPOrDomain(xnet.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: xnet.NewIPOrDomain(backend.Address()), + Port: uint32(backend.Port()), + Networks: []xnet.Network{xnet.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&vout.Config{ + Vnext: &protocol.ServerEndpoint{ + Address: xnet.NewIPOrDomain(xnet.LocalHostIP), + Port: uint32(relayPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vless.Account{ + Id: userID.String(), + }), + }, + }, + }), + SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{ + StreamSettings: &internet.StreamConfig{ + ProtocolName: "tcp", + TransportSettings: []*internet.TransportConfig{ + { + ProtocolName: "tcp", + Settings: serial.ToTypedMessage(&transtcp.Config{}), + }, + }, + SecurityType: serial.GetMessageType(&reality.Config{}), + SecuritySettings: []*serial.TypedMessage{ + serial.ToTypedMessage(&reality.Config{ + Show: true, + Fingerprint: "chrome", + ServerName: "localhost", + PublicKey: realityPub, + ShortId: shortID, + SpiderX: "/", + }), + }, + Tcpmasks: []*serial.TypedMessage{serial.ToTypedMessage(cloneConfig(mode.config))}, + }, + }), + }, + }, + }) + + serverCmd, clientCmd := runXrayPair(t, bin, serverConfig, clientConfig) + defer stopCmd(clientCmd) + defer stopCmd(serverCmd) + exerciseTCPClient(t, int(clientPort), payloadSize) + + return analyzeTCPRelay(t, "vless-reality", mode, relay.Snapshots()) +} + +func runHysteria2Case(t *testing.T, bin string, mode trafficMode, payloadSize int) caseResult { + backend := startXOREchoServer(t) + defer backend.Close() + + serverPort := testingtcp.PickPort() + relayPort := testingtcp.PickPort() + clientPort := testingtcp.PickPort() + + relay := startUDPRelay(t, int(relayPort), int(serverPort)) + defer relay.Close() + + ct, ctHash := cert.MustGenerate(nil, cert.CommonName("localhost"), cert.DNSNames("localhost")) + auth := "hy2-auth-secret" + + serverConfig := defaultApps(&core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &xnet.PortList{Range: []*xnet.PortRange{xnet.SinglePortRange(serverPort)}}, + Listen: xnet.NewIPOrDomain(xnet.LocalHostIP), + StreamSettings: &internet.StreamConfig{ + ProtocolName: "hysteria", + TransportSettings: []*internet.TransportConfig{ + { + ProtocolName: "hysteria", + Settings: serial.ToTypedMessage(&hytransport.Config{ + Version: 2, + Auth: auth, + UdpIdleTimeout: 60, + }), + }, + }, + SecurityType: serial.GetMessageType(&xtls.Config{}), + SecuritySettings: []*serial.TypedMessage{ + serial.ToTypedMessage(&xtls.Config{ + Certificate: []*xtls.Certificate{xtls.ParseCertificate(ct)}, + NextProtocol: []string{"h3"}, + }), + }, + Udpmasks: []*serial.TypedMessage{serial.ToTypedMessage(cloneConfig(mode.config))}, + }, + }), + ProxySettings: serial.ToTypedMessage(&hyproxy.ServerConfig{ + Users: []*protocol.User{ + { + Account: serial.ToTypedMessage(&hyaccount.Account{Auth: auth}), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + {ProxySettings: serial.ToTypedMessage(&freedom.Config{})}, + }, + }) + + clientConfig := defaultApps(&core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &xnet.PortList{Range: []*xnet.PortRange{xnet.SinglePortRange(clientPort)}}, + Listen: xnet.NewIPOrDomain(xnet.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: xnet.NewIPOrDomain(backend.Address()), + Port: uint32(backend.Port()), + Networks: []xnet.Network{xnet.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&hyproxy.ClientConfig{ + Version: 2, + Server: &protocol.ServerEndpoint{ + Address: xnet.NewIPOrDomain(xnet.LocalHostIP), + Port: uint32(relayPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&hyaccount.Account{Auth: auth}), + }, + }, + }), + SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{ + StreamSettings: &internet.StreamConfig{ + ProtocolName: "hysteria", + TransportSettings: []*internet.TransportConfig{ + { + ProtocolName: "hysteria", + Settings: serial.ToTypedMessage(&hytransport.Config{ + Version: 2, + Auth: auth, + UdpIdleTimeout: 60, + }), + }, + }, + SecurityType: serial.GetMessageType(&xtls.Config{}), + SecuritySettings: []*serial.TypedMessage{ + serial.ToTypedMessage(&xtls.Config{ + ServerName: "localhost", + PinnedPeerCertSha256: [][]byte{ctHash[:]}, + NextProtocol: []string{"h3"}, + }), + }, + Udpmasks: []*serial.TypedMessage{serial.ToTypedMessage(cloneConfig(mode.config))}, + }, + }), + }, + }, + }) + + serverCmd, clientCmd := runXrayPair(t, bin, serverConfig, clientConfig) + defer stopCmd(clientCmd) + defer stopCmd(serverCmd) + if err := exerciseTCPClientErr(t, int(clientPort), payloadSize); err != nil { + c2s, s2c := relay.Snapshots() + t.Fatalf("hy2 traffic failed: %v (udp packets c2s=%d s2c=%d first_c2s=%d first_s2c=%d)", err, len(c2s), len(s2c), firstChunkLen(c2s), firstChunkLen(s2c)) + } + + c2s, s2c := relay.Snapshots() + return analyzeUDPRelay(t, "hysteria2", mode, c2s, s2c) +} + +func runVLesseEncCase(t *testing.T, bin string, mode trafficMode, payloadSize int) caseResult { + backend := startXOREchoServer(t) + defer backend.Close() + + serverPort := testingtcp.PickPort() + relayPort := testingtcp.PickPort() + clientPort := testingtcp.PickPort() + + relay := startTCPRelay(t, int(relayPort), fmt.Sprintf("127.0.0.1:%d", serverPort)) + defer relay.Close() + + userID := protocol.NewID(uuid.New()) + priv, pub := mustX25519Keypair(t) + pubB64 := base64.RawURLEncoding.EncodeToString(pub) + privB64 := base64.RawURLEncoding.EncodeToString(priv) + + serverConfig := defaultApps(&core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &xnet.PortList{Range: []*xnet.PortRange{xnet.SinglePortRange(serverPort)}}, + Listen: xnet.NewIPOrDomain(xnet.LocalHostIP), + StreamSettings: &internet.StreamConfig{ + ProtocolName: "tcp", + TransportSettings: []*internet.TransportConfig{ + {ProtocolName: "tcp", Settings: serial.ToTypedMessage(&transtcp.Config{})}, + }, + Tcpmasks: []*serial.TypedMessage{serial.ToTypedMessage(cloneConfig(mode.config))}, + }, + }), + ProxySettings: serial.ToTypedMessage(&vin.Config{ + Clients: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vless.Account{ + Id: userID.String(), + }), + }, + }, + Decryption: privB64, + XorMode: 1, + SecondsFrom: 0, + SecondsTo: 0, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + {ProxySettings: serial.ToTypedMessage(&freedom.Config{})}, + }, + }) + + clientConfig := defaultApps(&core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &xnet.PortList{Range: []*xnet.PortRange{xnet.SinglePortRange(clientPort)}}, + Listen: xnet.NewIPOrDomain(xnet.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: xnet.NewIPOrDomain(backend.Address()), + Port: uint32(backend.Port()), + Networks: []xnet.Network{xnet.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&vout.Config{ + Vnext: &protocol.ServerEndpoint{ + Address: xnet.NewIPOrDomain(xnet.LocalHostIP), + Port: uint32(relayPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vless.Account{ + Id: userID.String(), + Encryption: pubB64, + XorMode: 1, + }), + }, + }, + }), + SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{ + StreamSettings: &internet.StreamConfig{ + ProtocolName: "tcp", + TransportSettings: []*internet.TransportConfig{ + {ProtocolName: "tcp", Settings: serial.ToTypedMessage(&transtcp.Config{})}, + }, + Tcpmasks: []*serial.TypedMessage{serial.ToTypedMessage(cloneConfig(mode.config))}, + }, + }), + }, + }, + }) + + serverCmd, clientCmd := runXrayPair(t, bin, serverConfig, clientConfig) + defer stopCmd(clientCmd) + defer stopCmd(serverCmd) + exerciseTCPClient(t, int(clientPort), payloadSize) + + return analyzeTCPRelay(t, "vless-enc", mode, relay.Snapshots()) +} + +func runVLESSXHTTPCase(t *testing.T, bin string, mode trafficMode, payloadSize int) caseResult { + backend := startXOREchoServer(t) + defer backend.Close() + + serverPort := testingtcp.PickPort() + relayPort := testingtcp.PickPort() + clientPort := testingtcp.PickPort() + + relay := startTCPRelay(t, int(relayPort), fmt.Sprintf("127.0.0.1:%d", serverPort)) + defer relay.Close() + + userID := protocol.NewID(uuid.New()) + xhttpConfig := &splithttp.Config{ + Host: "localhost", + Path: "/sudoku", + Mode: "auto", + } + + serverConfig := defaultApps(&core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &xnet.PortList{Range: []*xnet.PortRange{xnet.SinglePortRange(serverPort)}}, + Listen: xnet.NewIPOrDomain(xnet.LocalHostIP), + StreamSettings: &internet.StreamConfig{ + ProtocolName: "splithttp", + TransportSettings: []*internet.TransportConfig{ + {ProtocolName: "splithttp", Settings: serial.ToTypedMessage(xhttpConfig)}, + }, + Tcpmasks: []*serial.TypedMessage{serial.ToTypedMessage(cloneConfig(mode.config))}, + }, + }), + ProxySettings: serial.ToTypedMessage(&vin.Config{ + Clients: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vless.Account{ + Id: userID.String(), + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + {ProxySettings: serial.ToTypedMessage(&freedom.Config{})}, + }, + }) + + clientConfig := defaultApps(&core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &xnet.PortList{Range: []*xnet.PortRange{xnet.SinglePortRange(clientPort)}}, + Listen: xnet.NewIPOrDomain(xnet.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: xnet.NewIPOrDomain(backend.Address()), + Port: uint32(backend.Port()), + Networks: []xnet.Network{xnet.Network_TCP}, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&vout.Config{ + Vnext: &protocol.ServerEndpoint{ + Address: xnet.NewIPOrDomain(xnet.LocalHostIP), + Port: uint32(relayPort), + User: &protocol.User{ + Account: serial.ToTypedMessage(&vless.Account{ + Id: userID.String(), + }), + }, + }, + }), + SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{ + StreamSettings: &internet.StreamConfig{ + ProtocolName: "splithttp", + TransportSettings: []*internet.TransportConfig{ + {ProtocolName: "splithttp", Settings: serial.ToTypedMessage(xhttpConfig)}, + }, + Tcpmasks: []*serial.TypedMessage{serial.ToTypedMessage(cloneConfig(mode.config))}, + }, + }), + }, + }, + }) + + serverCmd, clientCmd := runXrayPair(t, bin, serverConfig, clientConfig) + defer stopCmd(clientCmd) + defer stopCmd(serverCmd) + exerciseTCPClient(t, int(clientPort), payloadSize) + + return analyzeTCPRelay(t, "vless-xhttp", mode, relay.Snapshots()) +} + +func analyzeTCPRelay(t *testing.T, protocol string, mode trafficMode, captures []*tcpCapture) caseResult { + tables, err := getTables(mode.config) + if err != nil { + t.Fatal(err) + } + + allC2S := make([][]byte, 0, len(captures)) + allS2C := make([][]byte, 0, len(captures)) + for _, capture := range captures { + c2s, s2c := capture.snapshot() + if len(c2s) > 0 { + allC2S = append(allC2S, c2s) + } + if len(s2c) > 0 { + allS2C = append(allS2C, s2c) + } + } + + c2sMetrics := metricFromBytes(flattenChunks(allC2S)) + s2cMetrics := metricFromBytes(flattenChunks(allS2C)) + + c2sUsed, c2sDecoded, err := analyzePureChunks(tables, allC2S) + if err != nil { + t.Fatalf("%s %s pure decode failed: %v", protocol, mode.name, err) + } + s2cUsed, s2cDecoded, err := analyzePackedChunks(tables, allS2C) + if err != nil { + t.Fatalf("%s %s packed decode failed: %v", protocol, mode.name, err) + } + + allBytes := append(append([]byte{}, flattenChunks(allC2S)...), flattenChunks(allS2C)...) + totalMetrics := metricFromBytes(allBytes) + rotationSeen := len(unionKeys(c2sUsed, s2cUsed)) + + return caseResult{ + Protocol: protocol, + Mode: mode.name, + TotalBytes: len(allBytes), + ASCIIBytes: totalMetrics.asciiBytes, + ASCIIRatio: totalMetrics.asciiRatio, + AvgHammingOnes: totalMetrics.avgOnes, + RotationSeen: rotationSeen, + RotationExpected: expectedRotation(mode.config), + DecodedUnits: c2sDecoded + s2cDecoded, + ClientToServer: directionResult{ + RawBytes: len(flattenChunks(allC2S)), + ASCIIBytes: c2sMetrics.asciiBytes, + ASCIIRatio: c2sMetrics.asciiRatio, + AvgHammingOnes: c2sMetrics.avgOnes, + RotationSeen: len(c2sUsed), + DecodedUnits: c2sDecoded, + }, + ServerToClient: directionResult{ + RawBytes: len(flattenChunks(allS2C)), + ASCIIBytes: s2cMetrics.asciiBytes, + ASCIIRatio: s2cMetrics.asciiRatio, + AvgHammingOnes: s2cMetrics.avgOnes, + RotationSeen: len(s2cUsed), + DecodedUnits: s2cDecoded, + }, + } +} + +func analyzeUDPRelay(t *testing.T, protocol string, mode trafficMode, c2s [][]byte, s2c [][]byte) caseResult { + tables, err := getTables(mode.config) + if err != nil { + t.Fatal(err) + } + + c2sMetrics := metricFromBytes(flattenChunks(c2s)) + s2cMetrics := metricFromBytes(flattenChunks(s2c)) + + c2sUsed, c2sDecoded, err := analyzePureChunks(tables, c2s) + if err != nil { + t.Fatalf("%s %s udp c2s decode failed: %v", protocol, mode.name, err) + } + s2cUsed, s2cDecoded, err := analyzePureChunks(tables, s2c) + if err != nil { + t.Fatalf("%s %s udp s2c decode failed: %v", protocol, mode.name, err) + } + + allBytes := append(append([]byte{}, flattenChunks(c2s)...), flattenChunks(s2c)...) + totalMetrics := metricFromBytes(allBytes) + rotationSeen := len(unionKeys(c2sUsed, s2cUsed)) + + return caseResult{ + Protocol: protocol, + Mode: mode.name, + TotalBytes: len(allBytes), + ASCIIBytes: totalMetrics.asciiBytes, + ASCIIRatio: totalMetrics.asciiRatio, + AvgHammingOnes: totalMetrics.avgOnes, + RotationSeen: rotationSeen, + RotationExpected: expectedRotation(mode.config), + DecodedUnits: c2sDecoded + s2cDecoded, + ClientToServer: directionResult{ + RawBytes: len(flattenChunks(c2s)), + ASCIIBytes: c2sMetrics.asciiBytes, + ASCIIRatio: c2sMetrics.asciiRatio, + AvgHammingOnes: c2sMetrics.avgOnes, + RotationSeen: len(c2sUsed), + DecodedUnits: c2sDecoded, + }, + ServerToClient: directionResult{ + RawBytes: len(flattenChunks(s2c)), + ASCIIBytes: s2cMetrics.asciiBytes, + ASCIIRatio: s2cMetrics.asciiRatio, + AvgHammingOnes: s2cMetrics.avgOnes, + RotationSeen: len(s2cUsed), + DecodedUnits: s2cDecoded, + }, + } +} + +type byteMetrics struct { + asciiBytes int + asciiRatio float64 + avgOnes float64 +} + +func metricFromBytes(b []byte) byteMetrics { + if len(b) == 0 { + return byteMetrics{} + } + var ascii, ones int + for _, v := range b { + if v < 0x80 { + ascii++ + } + ones += bitsInByte(v) + } + return byteMetrics{ + asciiBytes: ascii, + asciiRatio: float64(ascii) / float64(len(b)), + avgOnes: float64(ones) / float64(len(b)), + } +} + +func bitsInByte(b byte) int { + n := 0 + for b != 0 { + n += int(b & 1) + b >>= 1 + } + return n +} + +func analyzePureChunks(tables []*table, chunks [][]byte) (map[int]int, int, error) { + if len(tables) == 0 { + return nil, 0, fmt.Errorf("no sudoku tables") + } + used := make(map[int]int) + decoded := 0 + for _, chunk := range chunks { + hintBuf := make([]byte, 0, 4) + tableIndex := 0 + for _, b := range chunk { + t := tables[tableIndex%len(tables)] + if !t.layout.isHint(b) { + continue + } + hintBuf = append(hintBuf, b) + if len(hintBuf) < 4 { + continue + } + keyBytes := sort4([4]byte{hintBuf[0], hintBuf[1], hintBuf[2], hintBuf[3]}) + key := packKey(keyBytes) + if _, ok := t.decode[key]; !ok { + return nil, 0, fmt.Errorf("invalid pure tuple at table %d", tableIndex%len(tables)) + } + used[tableIndex%len(tables)]++ + decoded++ + tableIndex++ + hintBuf = hintBuf[:0] + } + if len(hintBuf) != 0 { + return nil, 0, fmt.Errorf("leftover pure hints") + } + } + return used, decoded, nil +} + +func analyzePackedChunks(tables []*table, chunks [][]byte) (map[int]int, int, error) { + layouts := tablesToLayouts(tables) + if len(layouts) == 0 { + return nil, 0, fmt.Errorf("no sudoku layouts") + } + used := make(map[int]int) + decoded := 0 + for _, chunk := range chunks { + var bitBuf uint64 + var bitCount int + groupIndex := 0 + for _, b := range chunk { + layout := layouts[groupIndex%len(layouts)] + if !layout.isHint(b) { + if b == layout.padMarker { + bitBuf = 0 + bitCount = 0 + } + continue + } + group, ok := layout.decodeGroup(b) + if !ok { + return nil, 0, fmt.Errorf("invalid packed byte %d", b) + } + used[groupIndex%len(layouts)]++ + groupIndex++ + bitBuf = (bitBuf << 6) | uint64(group) + bitCount += 6 + for bitCount >= 8 { + bitCount -= 8 + decoded++ + if bitCount > 0 { + bitBuf &= (uint64(1) << bitCount) - 1 + } else { + bitBuf = 0 + } + } + } + } + return used, decoded, nil +} + +func expectedRotation(cfg *Config) int { + tables, err := getTables(cfg) + if err != nil { + return 0 + } + return len(tables) +} + +func unionKeys(a, b map[int]int) map[int]struct{} { + out := make(map[int]struct{}, len(a)+len(b)) + for k := range a { + out[k] = struct{}{} + } + for k := range b { + out[k] = struct{}{} + } + return out +} + +func flattenChunks(chunks [][]byte) []byte { + total := 0 + for _, chunk := range chunks { + total += len(chunk) + } + out := make([]byte, 0, total) + for _, chunk := range chunks { + out = append(out, chunk...) + } + return out +} + +func cloneConfig(cfg *Config) *Config { + if cfg == nil { + return nil + } + out := proto.Clone(cfg).(*Config) + return out +} + +func defaultApps(cfg *core.Config) *core.Config { + cfg.App = append(cfg.App, + serial.ToTypedMessage(&log.Config{ + ErrorLogLevel: clog.Severity_Warning, + ErrorLogType: log.LogType_Console, + }), + serial.ToTypedMessage(&dispatcher.Config{}), + serial.ToTypedMessage(&proxyman.InboundConfig{}), + serial.ToTypedMessage(&proxyman.OutboundConfig{}), + ) + return cfg +} + +func buildE2EBinary(t *testing.T) string { + t.Helper() + e2eBinaryOnce.Do(func() { + tempDir, err := os.MkdirTemp("", "xray-sudoku-e2e-*") + if err != nil { + e2eBinaryErr = err + return + } + e2eBinaryPath = filepath.Join(tempDir, "xray.test") + cmd := exec.Command("go", "build", "-o", e2eBinaryPath, "./main") + cmd.Dir = repoRoot(t) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + e2eBinaryErr = cmd.Run() + }) + if e2eBinaryErr != nil { + t.Fatal(e2eBinaryErr) + } + return e2eBinaryPath +} + +func repoRoot(t *testing.T) string { + t.Helper() + dir, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + for { + if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil { + return dir + } + parent := filepath.Dir(dir) + if parent == dir { + t.Fatal("failed to locate repo root") + } + dir = parent + } +} + +func runXrayPair(t *testing.T, bin string, serverCfg, clientCfg *core.Config) (*exec.Cmd, *exec.Cmd) { + t.Helper() + serverCmd := runXray(t, bin, serverCfg) + + time.Sleep(500 * time.Millisecond) + + clientCmd := runXray(t, bin, clientCfg) + + time.Sleep(1500 * time.Millisecond) + return serverCmd, clientCmd +} + +func runXray(t *testing.T, bin string, cfg *core.Config) *exec.Cmd { + t.Helper() + cfgBytes, err := proto.Marshal(cfg) + if err != nil { + t.Fatal(err) + } + cmd := exec.Command(bin, "-config=stdin:", "-format=pb") + cmd.Stdin = bytes.NewReader(cfgBytes) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Start(); err != nil { + t.Fatal(err) + } + return cmd +} + +func stopCmd(cmd *exec.Cmd) { + if cmd == nil || cmd.Process == nil { + return + } + _ = cmd.Process.Signal(syscall.SIGTERM) + done := make(chan struct{}) + go func() { + _, _ = cmd.Process.Wait() + close(done) + }() + select { + case <-done: + case <-time.After(3 * time.Second): + _ = cmd.Process.Kill() + <-done + } +} + +func startTCPRelay(t *testing.T, listenPort int, target string) *tcpRelay { + t.Helper() + ln, err := stdnet.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", listenPort)) + if err != nil { + t.Fatal(err) + } + r := &tcpRelay{ + listener: ln, + target: target, + stopCh: make(chan struct{}), + } + r.wg.Add(1) + go func() { + defer r.wg.Done() + for { + conn, err := ln.Accept() + if err != nil { + select { + case <-r.stopCh: + return + default: + } + return + } + targetConn, err := stdnet.Dial("tcp", target) + if err != nil { + _ = conn.Close() + continue + } + capture := &tcpCapture{} + r.mu.Lock() + r.captures = append(r.captures, capture) + r.mu.Unlock() + r.wg.Add(1) + go func(client, server stdnet.Conn, cap *tcpCapture) { + defer r.wg.Done() + defer client.Close() + defer server.Close() + var inner sync.WaitGroup + inner.Add(2) + go func() { + defer inner.Done() + _, _ = io.Copy(server, io.TeeReader(client, &captureWriter{capture: cap, dir: "c2s"})) + if tcp, ok := server.(*stdnet.TCPConn); ok { + _ = tcp.CloseWrite() + } + }() + go func() { + defer inner.Done() + _, _ = io.Copy(client, io.TeeReader(server, &captureWriter{capture: cap, dir: "s2c"})) + if tcp, ok := client.(*stdnet.TCPConn); ok { + _ = tcp.CloseWrite() + } + }() + inner.Wait() + }(conn, targetConn, capture) + } + }() + return r +} + +func (r *tcpRelay) Close() { + close(r.stopCh) + _ = r.listener.Close() + r.wg.Wait() +} + +func (r *tcpRelay) Snapshots() []*tcpCapture { + r.mu.Lock() + defer r.mu.Unlock() + out := make([]*tcpCapture, 0, len(r.captures)) + for _, capture := range r.captures { + out = append(out, capture) + } + return out +} + +func (c *tcpCapture) snapshot() ([]byte, []byte) { + c.mu.Lock() + defer c.mu.Unlock() + return append([]byte{}, c.c2s...), append([]byte{}, c.s2c...) +} + +type captureWriter struct { + capture *tcpCapture + dir string +} + +func (w *captureWriter) Write(p []byte) (int, error) { + w.capture.mu.Lock() + defer w.capture.mu.Unlock() + if w.dir == "c2s" { + w.capture.c2s = append(w.capture.c2s, p...) + } else { + w.capture.s2c = append(w.capture.s2c, p...) + } + return len(p), nil +} + +func startUDPRelay(t *testing.T, listenPort, targetPort int) *udpRelay { + t.Helper() + conn, err := stdnet.ListenPacket("udp", fmt.Sprintf("127.0.0.1:%d", listenPort)) + if err != nil { + t.Fatal(err) + } + targetAddr := &stdnet.UDPAddr{IP: stdnet.IPv4(127, 0, 0, 1), Port: targetPort} + r := &udpRelay{ + conn: conn, + target: targetAddr, + stopCh: make(chan struct{}), + } + r.wg.Add(1) + go func() { + defer r.wg.Done() + buf := make([]byte, 64*1024) + for { + n, addr, err := conn.ReadFrom(buf) + if err != nil { + select { + case <-r.stopCh: + return + default: + } + return + } + payload := append([]byte{}, buf[:n]...) + udpAddr := addr.(*stdnet.UDPAddr) + if udpAddr.IP.Equal(r.target.IP) && udpAddr.Port == r.target.Port { + r.captureMu.Lock() + r.s2c = append(r.s2c, payload) + r.captureMu.Unlock() + r.clientMu.Lock() + client := r.client + r.clientMu.Unlock() + if client != nil { + _, _ = conn.WriteTo(payload, client) + } + continue + } + r.clientMu.Lock() + r.client = udpAddr + r.clientMu.Unlock() + r.captureMu.Lock() + r.c2s = append(r.c2s, payload) + r.captureMu.Unlock() + _, _ = conn.WriteTo(payload, r.target) + } + }() + return r +} + +func (r *udpRelay) Close() { + close(r.stopCh) + _ = r.conn.Close() + r.wg.Wait() +} + +func (r *udpRelay) Snapshots() ([][]byte, [][]byte) { + r.captureMu.Lock() + defer r.captureMu.Unlock() + c2s := make([][]byte, 0, len(r.c2s)) + s2c := make([][]byte, 0, len(r.s2c)) + for _, packet := range r.c2s { + c2s = append(c2s, append([]byte{}, packet...)) + } + for _, packet := range r.s2c { + s2c = append(s2c, append([]byte{}, packet...)) + } + return c2s, s2c +} + +type xorEchoServer struct { + ln stdnet.Listener + wg sync.WaitGroup +} + +func startXOREchoServer(t *testing.T) *xorEchoServer { + t.Helper() + ln, err := stdnet.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + s := &xorEchoServer{ln: ln} + s.wg.Add(1) + go func() { + defer s.wg.Done() + for { + conn, err := ln.Accept() + if err != nil { + return + } + s.wg.Add(1) + go func(c stdnet.Conn) { + defer s.wg.Done() + defer c.Close() + buf := make([]byte, 4096) + for { + n, err := c.Read(buf) + if err != nil { + return + } + for i := 0; i < n; i++ { + buf[i] ^= 'c' + } + if _, err := c.Write(buf[:n]); err != nil { + return + } + for i := 0; i < n; i++ { + buf[i] ^= 'c' + } + } + }(conn) + } + }() + return s +} + +func (s *xorEchoServer) Address() xnet.Address { + return xnet.IPAddress(s.ln.Addr().(*stdnet.TCPAddr).IP) +} + +func (s *xorEchoServer) Port() xnet.Port { + return xnet.Port(s.ln.Addr().(*stdnet.TCPAddr).Port) +} + +func (s *xorEchoServer) Close() { + _ = s.ln.Close() + s.wg.Wait() +} + +func startTLSEchoDecoy(t *testing.T, c *cert.Certificate) *tlsDecoy { + t.Helper() + certPEM, keyPEM := c.ToPEM() + keyPair, err := cryptotls.X509KeyPair(certPEM, keyPEM) + if err != nil { + t.Fatal(err) + } + config := &cryptotls.Config{ + Certificates: []cryptotls.Certificate{keyPair}, + } + ln, err := stdnet.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + tlsLn := cryptotls.NewListener(ln, config) + d := &tlsDecoy{ + ln: tlsLn, + done: make(chan struct{}), + } + d.wg.Add(1) + go func() { + defer d.wg.Done() + for { + conn, err := tlsLn.Accept() + if err != nil { + return + } + d.wg.Add(1) + go func(c stdnet.Conn) { + defer d.wg.Done() + defer c.Close() + buf := make([]byte, 2048) + _, _ = c.Read(buf) + _, _ = c.Write([]byte("HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK")) + }(conn) + } + }() + return d +} + +func (d *tlsDecoy) Port() int { + return d.ln.Addr().(*stdnet.TCPAddr).Port +} + +func (d *tlsDecoy) Close() { + _ = d.ln.Close() + d.wg.Wait() +} + +func exerciseTCPClient(t *testing.T, port int, payloadSize int) { + t.Helper() + if err := exerciseTCPClientErr(t, port, payloadSize); err != nil { + t.Fatal(err) + } +} + +func exerciseTCPClientErr(t *testing.T, port int, payloadSize int) error { + conn := waitTCPConn(t, port, 10*time.Second) + defer conn.Close() + payload := make([]byte, payloadSize) + if _, err := rand.Read(payload); err != nil { + return err + } + offset := 0 + for offset < len(payload) { + chunk := 1024 + if remain := len(payload) - offset; remain < chunk { + chunk = remain + } + part := payload[offset : offset+chunk] + if _, err := conn.Write(part); err != nil { + return err + } + resp := make([]byte, chunk) + if _, err := io.ReadFull(conn, resp); err != nil { + return err + } + for i := range part { + if resp[i] != (part[i] ^ 'c') { + return fmt.Errorf("unexpected xor response at offset %d", offset+i) + } + } + offset += chunk + } + return nil +} + +func firstChunkLen(chunks [][]byte) int { + if len(chunks) == 0 { + return 0 + } + return len(chunks[0]) +} + +func waitTCPConn(t *testing.T, port int, timeout time.Duration) stdnet.Conn { + t.Helper() + deadline := time.Now().Add(timeout) + for { + conn, err := stdnet.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", port), 500*time.Millisecond) + if err == nil { + return conn + } + if time.Now().After(deadline) { + t.Fatal(err) + } + time.Sleep(100 * time.Millisecond) + } +} + +func mustX25519Keypair(t *testing.T) ([]byte, []byte) { + t.Helper() + priv, err := ecdh.X25519().GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + return priv.Bytes(), priv.PublicKey().Bytes() +} + +func mustDecodeHex(t *testing.T, s string) []byte { + t.Helper() + out := make([]byte, len(s)/2) + if _, err := hex.Decode(out, []byte(s)); err != nil { + t.Fatal(err) + } + return out +} + +func init() { + ch := make(chan os.Signal, 1) + signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM) + go func() { + <-ch + os.Exit(130) + }() +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/sudoku/table.go b/subproject/Xray-core-main/transport/internet/finalmask/sudoku/table.go new file mode 100644 index 00000000..6396d864 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/sudoku/table.go @@ -0,0 +1,580 @@ +package sudoku + +import ( + crypto_rand "crypto/rand" + "crypto/sha256" + "encoding/binary" + "fmt" + "math/bits" + "math/rand" + "sort" + "strings" + "sync" + "time" +) + +type table struct { + encode [256][][4]byte + decode map[uint32]byte + layout *byteLayout +} + +type tableCacheKey struct { + password string + ascii string + customTable string +} + +var ( + tableCache sync.Map + tableSetCache sync.Map + + basePatternsOnce sync.Once + basePatterns [][][4]byte + basePatternsErr error +) + +type byteLayout struct { + hintMask byte + hintValue byte + padMarker byte + paddingPool []byte + encodeHint func(group byte) byte + encodeGroup func(group byte) byte + decodeGroup func(b byte) (byte, bool) +} + +func (l *byteLayout) isHint(b byte) bool { + if (b & l.hintMask) == l.hintValue { + return true + } + // ASCII layout maps 0x7f to '\n' to avoid DEL on the wire. + return l.hintMask == 0x40 && b == '\n' +} + +func getTable(config *Config) (*table, error) { + tables, err := getTables(config) + if err != nil { + return nil, err + } + if len(tables) == 0 { + return nil, fmt.Errorf("empty sudoku table set") + } + return tables[0], nil +} + +func getTables(config *Config) ([]*table, error) { + if config == nil { + return nil, fmt.Errorf("nil sudoku config") + } + + mode, err := normalizeASCII(config.GetAscii()) + if err != nil { + return nil, err + } + + patterns, err := normalizedCustomPatterns(config, mode) + if err != nil { + return nil, err + } + + cacheKey := tableCacheKey{ + password: config.GetPassword(), + ascii: mode, + customTable: strings.Join(patterns, "\x00"), + } + if cached, ok := tableSetCache.Load(cacheKey); ok { + return cached.([]*table), nil + } + + tables := make([]*table, 0, len(patterns)) + for _, pattern := range patterns { + layout, err := resolveLayout(mode, pattern) + if err != nil { + return nil, err + } + t, err := buildTable(config.GetPassword(), layout) + if err != nil { + return nil, err + } + tables = append(tables, t) + } + + actual, _ := tableSetCache.LoadOrStore(cacheKey, tables) + return actual.([]*table), nil +} + +func normalizedCustomPatterns(config *Config, mode string) ([]string, error) { + if config == nil { + return []string{""}, nil + } + if mode == "prefer_ascii" { + return []string{""}, nil + } + + rawPatterns := config.GetCustomTables() + if len(rawPatterns) == 0 { + rawPatterns = []string{config.GetCustomTable()} + } + + patterns := make([]string, 0, len(rawPatterns)) + seen := make(map[string]struct{}, len(rawPatterns)) + for _, raw := range rawPatterns { + pattern := strings.TrimSpace(raw) + if pattern != "" { + var err error + pattern, err = normalizeCustomTable(pattern) + if err != nil { + return nil, err + } + } + if _, ok := seen[pattern]; ok { + continue + } + seen[pattern] = struct{}{} + patterns = append(patterns, pattern) + } + + if len(patterns) == 0 { + return []string{""}, nil + } + + return patterns, nil +} + +func normalizedPadding(config *Config) (int, int) { + if config == nil { + return 0, 0 + } + + pMin := int(config.GetPaddingMin()) + pMax := int(config.GetPaddingMax()) + + if pMin > 100 { + pMin = 100 + } + if pMax > 100 { + pMax = 100 + } + if pMax < pMin { + pMax = pMin + } + return pMin, pMax +} + +func normalizeASCII(mode string) (string, error) { + switch strings.ToLower(strings.TrimSpace(mode)) { + case "", "entropy", "prefer_entropy": + return "prefer_entropy", nil + case "ascii", "prefer_ascii": + return "prefer_ascii", nil + default: + return "", fmt.Errorf("invalid sudoku ascii mode: %s", mode) + } +} + +func normalizeCustomTable(pattern string) (string, error) { + cleaned := strings.ToLower(strings.TrimSpace(pattern)) + cleaned = strings.ReplaceAll(cleaned, " ", "") + if len(cleaned) != 8 { + return "", fmt.Errorf("customTable must be 8 chars, got %d", len(cleaned)) + } + + var xCount, pCount, vCount int + for _, ch := range cleaned { + switch ch { + case 'x': + xCount++ + case 'p': + pCount++ + case 'v': + vCount++ + default: + return "", fmt.Errorf("customTable has invalid char %q", ch) + } + } + if xCount != 2 || pCount != 2 || vCount != 4 { + return "", fmt.Errorf("customTable must contain exactly 2 x, 2 p and 4 v") + } + return cleaned, nil +} + +func resolveLayout(mode, customTable string) (*byteLayout, error) { + if mode == "prefer_ascii" { + return asciiLayout(), nil + } + + if customTable != "" { + return customLayout(customTable) + } + return entropyLayout(), nil +} + +func asciiLayout() *byteLayout { + padding := make([]byte, 0, 32) + for i := 0; i < 32; i++ { + padding = append(padding, byte(0x20+i)) + } + + encodeGroup := func(group byte) byte { + b := byte(0x40 | (group & 0x3f)) + if b == 0x7f { + return '\n' + } + return b + } + + return &byteLayout{ + hintMask: 0x40, + hintValue: 0x40, + padMarker: 0x3f, + paddingPool: padding, + encodeHint: encodeGroup, + encodeGroup: encodeGroup, + decodeGroup: func(b byte) (byte, bool) { + if b == '\n' { + return 0x3f, true + } + if (b & 0x40) == 0 { + return 0, false + } + return b & 0x3f, true + }, + } +} + +func entropyLayout() *byteLayout { + padding := make([]byte, 0, 16) + for i := 0; i < 8; i++ { + padding = append(padding, byte(0x80+i), byte(0x10+i)) + } + + encodeGroup := func(group byte) byte { + v := group & 0x3f + return ((v & 0x30) << 1) | (v & 0x0f) + } + + return &byteLayout{ + hintMask: 0x90, + hintValue: 0x00, + padMarker: 0x80, + paddingPool: padding, + encodeHint: encodeGroup, + encodeGroup: encodeGroup, + decodeGroup: func(b byte) (byte, bool) { + if (b & 0x90) != 0 { + return 0, false + } + return ((b >> 1) & 0x30) | (b & 0x0f), true + }, + } +} + +func customLayout(pattern string) (*byteLayout, error) { + pattern, err := normalizeCustomTable(pattern) + if err != nil { + return nil, err + } + + var xBits, pBits, vBits []uint8 + for i, c := range pattern { + bit := uint8(7 - i) + switch c { + case 'x': + xBits = append(xBits, bit) + case 'p': + pBits = append(pBits, bit) + case 'v': + vBits = append(vBits, bit) + } + } + + xMask := byte(0) + for _, bit := range xBits { + xMask |= 1 << bit + } + + encodeGroupWithDropX := func(group byte, dropX int) byte { + out := xMask + if dropX >= 0 { + out &^= 1 << xBits[dropX] + } + + val := (group >> 4) & 0x03 + pos := group & 0x0f + + if (val & 0x02) != 0 { + out |= 1 << pBits[0] + } + if (val & 0x01) != 0 { + out |= 1 << pBits[1] + } + for i, bit := range vBits { + if (pos>>(3-uint8(i)))&0x01 == 1 { + out |= 1 << bit + } + } + + return out + } + + paddingSet := make(map[byte]struct{}, 64) + padding := make([]byte, 0, 64) + for drop := range xBits { + for val := byte(0); val < 4; val++ { + for pos := byte(0); pos < 16; pos++ { + group := (val << 4) | pos + b := encodeGroupWithDropX(group, drop) + if bits.OnesCount8(b) >= 5 { + if _, exists := paddingSet[b]; !exists { + paddingSet[b] = struct{}{} + padding = append(padding, b) + } + } + } + } + } + sort.Slice(padding, func(i, j int) bool { return padding[i] < padding[j] }) + if len(padding) == 0 { + return nil, fmt.Errorf("customTable produced empty padding pool") + } + + decodeGroup := func(b byte) (byte, bool) { + if (b & xMask) != xMask { + return 0, false + } + + var val, pos byte + if b&(1< in[1] { + in[0], in[1] = in[1], in[0] + } + if in[2] > in[3] { + in[2], in[3] = in[3], in[2] + } + if in[0] > in[2] { + in[0], in[2] = in[2], in[0] + } + if in[1] > in[3] { + in[1], in[3] = in[3], in[1] + } + if in[1] > in[2] { + in[1], in[2] = in[2], in[1] + } + return in +} + +func newSeededRand() *rand.Rand { + seed := time.Now().UnixNano() + var seedBytes [8]byte + if _, err := crypto_rand.Read(seedBytes[:]); err == nil { + seed = int64(binary.BigEndian.Uint64(seedBytes[:])) + } + return rand.New(rand.NewSource(seed)) +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/tcp_test.go b/subproject/Xray-core-main/transport/internet/finalmask/tcp_test.go new file mode 100644 index 00000000..7febb185 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/tcp_test.go @@ -0,0 +1,123 @@ +package finalmask_test + +import ( + "bytes" + "io" + "net" + "testing" + "time" + + "github.com/xtls/xray-core/transport/internet/finalmask" + "github.com/xtls/xray-core/transport/internet/finalmask/header/custom" +) + +func mustSendRecvTcp( + t *testing.T, + from net.Conn, + to net.Conn, + msg []byte, +) { + t.Helper() + + go func() { + _, err := from.Write(msg) + if err != nil { + t.Error(err) + } + }() + + buf := make([]byte, 1024) + n, err := io.ReadFull(to, buf[:len(msg)]) + if err != nil { + t.Fatal(err) + } + + if n != len(msg) { + t.Fatalf("unexpected size: %d", n) + } + + if !bytes.Equal(buf[:n], msg) { + t.Fatalf("unexpected data %q", buf[:n]) + } +} + +type layerMaskTcp struct { + name string + mask finalmask.Tcpmask +} + +func TestConnReadWrite(t *testing.T) { + cases := []layerMaskTcp{ + { + name: "custom", + mask: &custom.TCPConfig{ + Clients: []*custom.TCPSequence{ + { + Sequence: []*custom.TCPItem{ + { + Packet: []byte{1}, + }, + { + Rand: 1, + }, + }, + }, + }, + Servers: []*custom.TCPSequence{ + { + Sequence: []*custom.TCPItem{ + { + Packet: []byte{2}, + }, + { + Rand: 1, + }, + }, + }, + }, + }, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + mask := c.mask + + maskManager := finalmask.NewTcpmaskManager([]finalmask.Tcpmask{mask}) + + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + + client, err := net.Dial("tcp", ln.Addr().String()) + if err != nil { + t.Fatal(err) + } + + client, err = maskManager.WrapConnClient(client) + if err != nil { + t.Fatal(err) + } + + server, err := ln.Accept() + if err != nil { + t.Fatal(err) + } + + server, err = maskManager.WrapConnServer(server) + if err != nil { + t.Fatal(err) + } + + _ = client.SetDeadline(time.Now().Add(time.Second)) + _ = server.SetDeadline(time.Now().Add(time.Second)) + + mustSendRecvTcp(t, client, server, []byte("client -> server")) + mustSendRecvTcp(t, server, client, []byte("server -> client")) + + mustSendRecvTcp(t, client, server, []byte{}) + mustSendRecvTcp(t, server, client, []byte{}) + }) + } +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/udp_test.go b/subproject/Xray-core-main/transport/internet/finalmask/udp_test.go new file mode 100644 index 00000000..f2f18f2a --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/udp_test.go @@ -0,0 +1,632 @@ +package finalmask_test + +import ( + "bytes" + "io" + "net" + "sync/atomic" + "testing" + "time" + + "github.com/xtls/xray-core/proxy" + "github.com/xtls/xray-core/transport/internet/finalmask" + "github.com/xtls/xray-core/transport/internet/finalmask/header/custom" + "github.com/xtls/xray-core/transport/internet/finalmask/header/dns" + "github.com/xtls/xray-core/transport/internet/finalmask/header/srtp" + "github.com/xtls/xray-core/transport/internet/finalmask/header/utp" + "github.com/xtls/xray-core/transport/internet/finalmask/header/wechat" + "github.com/xtls/xray-core/transport/internet/finalmask/header/wireguard" + "github.com/xtls/xray-core/transport/internet/finalmask/mkcp/aes128gcm" + "github.com/xtls/xray-core/transport/internet/finalmask/mkcp/original" + "github.com/xtls/xray-core/transport/internet/finalmask/salamander" + "github.com/xtls/xray-core/transport/internet/finalmask/sudoku" +) + +func mustSendRecv( + t *testing.T, + from net.PacketConn, + to net.PacketConn, + msg []byte, +) { + t.Helper() + + go func() { + _, err := from.WriteTo(msg, to.LocalAddr()) + if err != nil { + t.Error(err) + } + }() + + buf := make([]byte, 1024) + n, _, err := to.ReadFrom(buf) + if err != nil { + t.Fatal(err) + } + + if n != len(msg) { + t.Fatalf("unexpected size: %d", n) + } + + if !bytes.Equal(buf[:n], msg) { + t.Fatalf("unexpected data") + } +} + +type layerMask struct { + name string + mask finalmask.Udpmask + layers int +} + +type countingConn struct { + net.Conn + written atomic.Int64 +} + +func (c *countingConn) Write(p []byte) (int, error) { + n, err := c.Conn.Write(p) + c.written.Add(int64(n)) + return n, err +} + +func (c *countingConn) Written() int64 { + return c.written.Load() +} + +func TestPacketConnReadWrite(t *testing.T) { + cases := []layerMask{ + { + name: "aes128gcm", + mask: &aes128gcm.Config{Password: "123"}, + layers: 2, + }, + { + name: "original", + mask: &original.Config{}, + layers: 2, + }, + { + name: "dns", + mask: &dns.Config{Domain: "www.baidu.com"}, + layers: 2, + }, + { + name: "srtp", + mask: &srtp.Config{}, + layers: 2, + }, + { + name: "utp", + mask: &utp.Config{}, + layers: 2, + }, + { + name: "wechat", + mask: &wechat.Config{}, + layers: 2, + }, + { + name: "wireguard", + mask: &wireguard.Config{}, + layers: 2, + }, + { + name: "salamander", + mask: &salamander.Config{Password: "1234"}, + layers: 2, + }, + { + name: "sudoku-prefer-ascii", + mask: &sudoku.Config{ + Password: "sudoku-mask", + Ascii: "prefer_ascii", + }, + layers: 1, + }, + { + name: "sudoku-custom-table", + mask: &sudoku.Config{ + Password: "sudoku-mask", + Ascii: "prefer_entropy", + CustomTable: "xpxvvpvv", + }, + layers: 1, + }, + { + name: "sudoku-custom-tables", + mask: &sudoku.Config{ + Password: "sudoku-mask", + Ascii: "prefer_entropy", + CustomTables: []string{"xpxvvpvv", "vxpvxvvp"}, + }, + layers: 1, + }, + { + name: "custom", + mask: &custom.UDPConfig{ + Client: []*custom.UDPItem{ + { + Packet: []byte{1}, + }, + { + Rand: 1, + }, + }, + Server: []*custom.UDPItem{ + { + Packet: []byte{1}, + }, + { + Rand: 1, + }, + }, + }, + layers: 1, + }, + { + name: "salamander-single", + mask: &salamander.Config{Password: "1234"}, + layers: 1, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + mask := c.mask + layers := c.layers + if layers <= 0 { + layers = 1 + } + masks := make([]finalmask.Udpmask, 0, layers) + for i := 0; i < layers; i++ { + masks = append(masks, mask) + } + maskManager := finalmask.NewUdpmaskManager(masks) + + client, err := net.ListenPacket("udp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + + client, err = maskManager.WrapPacketConnClient(client) + if err != nil { + t.Fatal(err) + } + + server, err := net.ListenPacket("udp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + + server, err = maskManager.WrapPacketConnServer(server) + if err != nil { + t.Fatal(err) + } + + _ = client.SetDeadline(time.Now().Add(time.Second)) + _ = server.SetDeadline(time.Now().Add(time.Second)) + + mustSendRecv(t, client, server, []byte("client -> server")) + mustSendRecv(t, server, client, []byte("server -> client")) + + mustSendRecv(t, client, server, []byte{}) + mustSendRecv(t, server, client, []byte{}) + }) + } +} + +func TestSudokuBDD(t *testing.T) { + t.Run("GivenSudokuTCPMask_WhenRoundTripWithAsciiPreference_ThenPayloadMatches", func(t *testing.T) { + cfg := &sudoku.Config{ + Password: "sudoku-tcp", + Ascii: "prefer_ascii", + } + + clientRaw, serverRaw := net.Pipe() + defer clientRaw.Close() + defer serverRaw.Close() + + clientConn, err := cfg.WrapConnClient(clientRaw) + if err != nil { + t.Fatal(err) + } + serverConn, err := cfg.WrapConnServer(serverRaw) + if err != nil { + t.Fatal(err) + } + + send := bytes.Repeat([]byte("client->server"), 1024) + recv := make([]byte, len(send)) + + writeErr := make(chan error, 1) + go func() { + _, wErr := clientConn.Write(send) + writeErr <- wErr + }() + + if _, err := io.ReadFull(serverConn, recv); err != nil { + t.Fatal(err) + } + if err := <-writeErr; err != nil { + t.Fatal(err) + } + if !bytes.Equal(send, recv) { + t.Fatal("tcp sudoku payload mismatch") + } + }) + + t.Run("GivenSudokuTCPMask_WhenRoundTrip_ThenBothDirectionsMatch", func(t *testing.T) { + cfg := &sudoku.Config{ + Password: "sudoku-packed", + Ascii: "prefer_ascii", + PaddingMin: 0, + PaddingMax: 0, + } + + clientRaw, serverRaw := net.Pipe() + defer clientRaw.Close() + defer serverRaw.Close() + + clientConn, err := cfg.WrapConnClient(clientRaw) + if err != nil { + t.Fatal(err) + } + serverConn, err := cfg.WrapConnServer(serverRaw) + if err != nil { + t.Fatal(err) + } + + clientToServer := bytes.Repeat([]byte("client-packed->server"), 257) + serverToClient := bytes.Repeat([]byte("server-packed->client"), 263) + + c2sRecv := make([]byte, len(clientToServer)) + c2sErr := make(chan error, 1) + go func() { + _, err := clientConn.Write(clientToServer) + c2sErr <- err + }() + if _, err := io.ReadFull(serverConn, c2sRecv); err != nil { + t.Fatal(err) + } + if err := <-c2sErr; err != nil { + t.Fatal(err) + } + if !bytes.Equal(clientToServer, c2sRecv) { + t.Fatal("tcp client->server payload mismatch") + } + + s2cRecv := make([]byte, len(serverToClient)) + s2cErr := make(chan error, 1) + go func() { + _, err := serverConn.Write(serverToClient) + s2cErr <- err + }() + if _, err := io.ReadFull(clientConn, s2cRecv); err != nil { + t.Fatal(err) + } + if err := <-s2cErr; err != nil { + t.Fatal(err) + } + if !bytes.Equal(serverToClient, s2cRecv) { + t.Fatal("tcp server->client payload mismatch") + } + }) + + t.Run("GivenSudokuTCPMask_WhenServerWritesDownlink_ThenWireBytesAreReduced", func(t *testing.T) { + payload := bytes.Repeat([]byte("0123456789abcdef"), 192) // 3072 bytes, divisible by 3. + + countWireBytes := func(wrapServer func(net.Conn, *sudoku.Config) (net.Conn, error), cfg *sudoku.Config) int64 { + t.Helper() + + clientRaw, serverRaw := net.Pipe() + watchedServerRaw := &countingConn{Conn: serverRaw} + + clientConn, err := cfg.WrapConnClient(clientRaw) + if err != nil { + t.Fatal(err) + } + serverConn, err := wrapServer(watchedServerRaw, cfg) + if err != nil { + t.Fatal(err) + } + + readErr := make(chan error, 1) + go func() { + _, err := io.CopyN(io.Discard, clientConn, int64(len(payload))) + readErr <- err + }() + + if _, err := serverConn.Write(payload); err != nil { + t.Fatal(err) + } + if err := <-readErr; err != nil { + t.Fatal(err) + } + + _ = clientConn.Close() + _ = serverConn.Close() + return watchedServerRaw.Written() + } + + pureUplinkPackedDownlink := &sudoku.Config{ + Password: "sudoku-bandwidth", + Ascii: "prefer_entropy", + PaddingMin: 0, + PaddingMax: 0, + } + packedDownlinkBytes := countWireBytes(func(raw net.Conn, cfg *sudoku.Config) (net.Conn, error) { + return cfg.WrapConnServer(raw) + }, pureUplinkPackedDownlink) + legacyPureBytes := countWireBytes(func(raw net.Conn, cfg *sudoku.Config) (net.Conn, error) { + return sudoku.NewTCPConn(raw, cfg) + }, pureUplinkPackedDownlink) + + if packedDownlinkBytes >= legacyPureBytes { + t.Fatalf("expected default packed downlink bytes < legacy pure bytes, got packed=%d pure=%d", packedDownlinkBytes, legacyPureBytes) + } + }) + + t.Run("GivenSudokuMultiTableTCPMask_WhenRoundTrip_ThenPayloadMatches", func(t *testing.T) { + cfg := &sudoku.Config{ + Password: "sudoku-multi-tcp", + Ascii: "prefer_entropy", + CustomTables: []string{"xpxvvpvv", "vxpvxvvp"}, + } + + clientRaw, serverRaw := net.Pipe() + defer clientRaw.Close() + defer serverRaw.Close() + + clientConn, err := cfg.WrapConnClient(clientRaw) + if err != nil { + t.Fatal(err) + } + serverConn, err := cfg.WrapConnServer(serverRaw) + if err != nil { + t.Fatal(err) + } + + send := bytes.Repeat([]byte("rotate-table"), 513) + recv := make([]byte, len(send)) + + writeErr := make(chan error, 1) + go func() { + _, wErr := clientConn.Write(send) + writeErr <- wErr + }() + + if _, err := io.ReadFull(serverConn, recv); err != nil { + t.Fatal(err) + } + if err := <-writeErr; err != nil { + t.Fatal(err) + } + if !bytes.Equal(send, recv) { + t.Fatal("multi-table tcp sudoku payload mismatch") + } + }) + + t.Run("GivenSudokuMultiTableTCPMask_WhenPackedDownlink_ThenPayloadMatches", func(t *testing.T) { + cfg := &sudoku.Config{ + Password: "sudoku-multi-packed", + Ascii: "prefer_entropy", + CustomTables: []string{"xpxvvpvv", "vxpvxvvp"}, + PaddingMin: 0, + PaddingMax: 0, + } + + clientRaw, serverRaw := net.Pipe() + defer clientRaw.Close() + defer serverRaw.Close() + + clientConn, err := cfg.WrapConnClient(clientRaw) + if err != nil { + t.Fatal(err) + } + serverConn, err := cfg.WrapConnServer(serverRaw) + if err != nil { + t.Fatal(err) + } + + send := bytes.Repeat([]byte("packed-rotate"), 257) + recv := make([]byte, len(send)) + + writeErr := make(chan error, 1) + go func() { + _, wErr := clientConn.Write(send) + writeErr <- wErr + }() + + if _, err := io.ReadFull(serverConn, recv); err != nil { + t.Fatal(err) + } + if err := <-writeErr; err != nil { + t.Fatal(err) + } + if !bytes.Equal(send, recv) { + t.Fatal("multi-table tcp sudoku payload mismatch") + } + }) + + t.Run("GivenSudokuUDPMask_WhenNotInnermost_ThenWrapFails", func(t *testing.T) { + cfg := &sudoku.Config{Password: "sudoku-udp"} + raw, err := net.ListenPacket("udp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + defer raw.Close() + + if _, err := cfg.WrapPacketConnClient(raw, 0, 1); err == nil { + t.Fatal("expected innermost check failure") + } + }) + + t.Run("GivenSudokuMultiTableUDPMask_WhenClientSendsMultipleDatagrams_ThenPayloadMatches", func(t *testing.T) { + cfg := &sudoku.Config{ + Password: "sudoku-udp-multi", + Ascii: "prefer_entropy", + CustomTables: []string{"xpxvvpvv", "vxpvxvvp"}, + PaddingMin: 0, + PaddingMax: 0, + } + maskManager := finalmask.NewUdpmaskManager([]finalmask.Udpmask{cfg}) + + clientRaw, err := net.ListenPacket("udp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + defer clientRaw.Close() + + serverRaw, err := net.ListenPacket("udp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + defer serverRaw.Close() + + client, err := maskManager.WrapPacketConnClient(clientRaw) + if err != nil { + t.Fatal(err) + } + server, err := maskManager.WrapPacketConnServer(serverRaw) + if err != nil { + t.Fatal(err) + } + + _ = client.SetDeadline(time.Now().Add(2 * time.Second)) + _ = server.SetDeadline(time.Now().Add(2 * time.Second)) + + mustSendRecv(t, client, server, []byte("first-datagram")) + mustSendRecv(t, client, server, []byte("second-datagram")) + mustSendRecv(t, client, server, []byte("third-datagram")) + }) + + t.Run("GivenSudokuTCPMask_WhenCloseWriteIsCalled_ThenEOFPropagates", func(t *testing.T) { + cfg := &sudoku.Config{ + Password: "sudoku-closewrite", + Ascii: "prefer_ascii", + PaddingMin: 0, + PaddingMax: 0, + } + + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + defer listener.Close() + + acceptCh := make(chan net.Conn, 1) + errCh := make(chan error, 1) + go func() { + conn, err := listener.Accept() + if err != nil { + errCh <- err + return + } + acceptCh <- conn + }() + + clientRaw, err := net.Dial("tcp", listener.Addr().String()) + if err != nil { + t.Fatal(err) + } + defer clientRaw.Close() + + var serverRaw net.Conn + select { + case serverRaw = <-acceptCh: + case err := <-errCh: + t.Fatal(err) + case <-time.After(2 * time.Second): + t.Fatal("accept timeout") + } + defer serverRaw.Close() + + clientConn, err := cfg.WrapConnClient(clientRaw) + if err != nil { + t.Fatal(err) + } + serverConn, err := cfg.WrapConnServer(serverRaw) + if err != nil { + t.Fatal(err) + } + + closeWriter, ok := clientConn.(interface{ CloseWrite() error }) + if !ok { + t.Fatalf("wrapped conn does not expose CloseWrite: %T", clientConn) + } + + writeErr := make(chan error, 1) + go func() { + if _, err := clientConn.Write([]byte("closewrite")); err != nil { + writeErr <- err + return + } + writeErr <- closeWriter.CloseWrite() + }() + + buf := make([]byte, len("closewrite")) + if _, err := io.ReadFull(serverConn, buf); err != nil { + t.Fatal(err) + } + if !bytes.Equal(buf, []byte("closewrite")) { + t.Fatal("unexpected payload before closewrite") + } + if err := <-writeErr; err != nil { + t.Fatal(err) + } + + one := make([]byte, 1) + n, err := serverConn.Read(one) + if n != 0 || err != io.EOF { + t.Fatalf("expected EOF after CloseWrite, got n=%d err=%v", n, err) + } + }) + + t.Run("GivenSudokuTCPMask_WhenProxyUnwrapRawConn_ThenMaskConnIsRetained", func(t *testing.T) { + cfg := &sudoku.Config{ + Password: "sudoku-unwrap", + Ascii: "prefer_entropy", + } + + clientRaw, serverRaw := net.Pipe() + defer clientRaw.Close() + defer serverRaw.Close() + + clientConn, err := cfg.WrapConnClient(clientRaw) + if err != nil { + t.Fatal(err) + } + + unwrapped, readCounter, writeCounter := proxy.UnwrapRawConn(clientConn) + if readCounter != nil || writeCounter != nil { + t.Fatal("unexpected stat counters while unwrapping sudoku conn") + } + if unwrapped != clientConn { + t.Fatalf("expected sudoku conn to stay wrapped, got %T", unwrapped) + } + }) + + t.Run("GivenSudokuTCPMask_WhenProxyUnwrapRawConn_AfterDownlinkOptimization_ThenMaskConnIsRetained", func(t *testing.T) { + cfg := &sudoku.Config{ + Password: "sudoku-packed-unwrap", + Ascii: "prefer_entropy", + } + + clientRaw, serverRaw := net.Pipe() + defer clientRaw.Close() + defer serverRaw.Close() + + clientConn, err := cfg.WrapConnClient(clientRaw) + if err != nil { + t.Fatal(err) + } + + unwrapped, readCounter, writeCounter := proxy.UnwrapRawConn(clientConn) + if readCounter != nil || writeCounter != nil { + t.Fatal("unexpected stat counters while unwrapping sudoku conn") + } + if unwrapped != clientConn { + t.Fatalf("expected sudoku conn to stay wrapped, got %T", unwrapped) + } + }) +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/xdns/client.go b/subproject/Xray-core-main/transport/internet/finalmask/xdns/client.go new file mode 100644 index 00000000..d6867b0a --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/xdns/client.go @@ -0,0 +1,362 @@ +package xdns + +import ( + "bytes" + "context" + "crypto/rand" + "encoding/base32" + "encoding/binary" + go_errors "errors" + "io" + "net" + "sync" + "time" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/transport/internet/finalmask" +) + +const ( + numPadding = 3 + numPaddingForPoll = 8 + initPollDelay = 500 * time.Millisecond + maxPollDelay = 10 * time.Second + pollDelayMultiplier = 2.0 + pollLimit = 16 +) + +var base32Encoding = base32.StdEncoding.WithPadding(base32.NoPadding) + +type packet struct { + p []byte + addr net.Addr +} + +type xdnsConnClient struct { + net.PacketConn + + clientID []byte + domain Name + + pollChan chan struct{} + readQueue chan *packet + writeQueue chan *packet + + closed bool + mutex sync.Mutex +} + +func NewConnClient(c *Config, raw net.PacketConn) (net.PacketConn, error) { + domain, err := ParseName(c.Domain) + if err != nil { + return nil, err + } + + conn := &xdnsConnClient{ + PacketConn: raw, + + clientID: make([]byte, 8), + domain: domain, + + pollChan: make(chan struct{}, pollLimit), + readQueue: make(chan *packet, 256), + writeQueue: make(chan *packet, 256), + } + + common.Must2(rand.Read(conn.clientID)) + + go conn.recvLoop() + go conn.sendLoop() + + return conn, nil +} + +func (c *xdnsConnClient) recvLoop() { + var buf [finalmask.UDPSize]byte + + for { + if c.closed { + break + } + + n, addr, err := c.PacketConn.ReadFrom(buf[:]) + if err != nil || n == 0 { + if go_errors.Is(err, net.ErrClosed) || go_errors.Is(err, io.EOF) { + break + } + continue + } + + resp, err := MessageFromWireFormat(buf[:n]) + if err != nil { + errors.LogDebug(context.Background(), addr, " xdns from wireformat err ", err) + continue + } + + payload := dnsResponsePayload(&resp, c.domain) + + r := bytes.NewReader(payload) + anyPacket := false + for { + p, err := nextPacket(r) + if err != nil { + break + } + anyPacket = true + + buf := make([]byte, len(p)) + copy(buf, p) + select { + case c.readQueue <- &packet{ + p: buf, + addr: addr, + }: + default: + errors.LogDebug(context.Background(), addr, " mask read err queue full") + } + } + + if anyPacket { + select { + case c.pollChan <- struct{}{}: + default: + } + } + } + + errors.LogDebug(context.Background(), "xdns closed") + + close(c.pollChan) + close(c.readQueue) + + c.mutex.Lock() + defer c.mutex.Unlock() + + c.closed = true + close(c.writeQueue) +} + +func (c *xdnsConnClient) sendLoop() { + var addr net.Addr + + pollDelay := initPollDelay + pollTimer := time.NewTimer(pollDelay) + for { + var p *packet + pollTimerExpired := false + + select { + case p = <-c.writeQueue: + default: + select { + case p = <-c.writeQueue: + case <-c.pollChan: + case <-pollTimer.C: + pollTimerExpired = true + } + } + + if p != nil { + addr = p.addr + + select { + case <-c.pollChan: + default: + } + } else if addr != nil { + encoded, _ := encode(nil, c.clientID, c.domain) + p = &packet{ + p: encoded, + addr: addr, + } + } + + if pollTimerExpired { + pollDelay = time.Duration(float64(pollDelay) * pollDelayMultiplier) + if pollDelay > maxPollDelay { + pollDelay = maxPollDelay + } + } else { + if !pollTimer.Stop() { + <-pollTimer.C + } + pollDelay = initPollDelay + } + pollTimer.Reset(pollDelay) + + if c.closed { + return + } + + if p != nil { + _, err := c.PacketConn.WriteTo(p.p, p.addr) + if go_errors.Is(err, net.ErrClosed) || go_errors.Is(err, io.ErrClosedPipe) { + c.closed = true + break + } + } + } +} + +func (c *xdnsConnClient) ReadFrom(p []byte) (n int, addr net.Addr, err error) { + packet, ok := <-c.readQueue + if !ok { + return 0, nil, net.ErrClosed + } + if len(p) < len(packet.p) { + errors.LogDebug(context.Background(), packet.addr, " mask read err short buffer ", len(p), " ", len(packet.p)) + return 0, packet.addr, nil + } + copy(p, packet.p) + return len(packet.p), packet.addr, nil +} + +func (c *xdnsConnClient) WriteTo(p []byte, addr net.Addr) (n int, err error) { + c.mutex.Lock() + defer c.mutex.Unlock() + + if c.closed { + return 0, io.ErrClosedPipe + } + + encoded, err := encode(p, c.clientID, c.domain) + if err != nil { + errors.LogDebug(context.Background(), addr, " xdns wireformat err ", err, " ", len(p)) + return 0, nil + } + + select { + case c.writeQueue <- &packet{ + p: encoded, + addr: addr, + }: + return len(p), nil + default: + errors.LogDebug(context.Background(), addr, " mask write err queue full") + return 0, nil + } +} + +func (c *xdnsConnClient) Close() error { + c.closed = true + return c.PacketConn.Close() +} + +func encode(p []byte, clientID []byte, domain Name) ([]byte, error) { + var decoded []byte + { + if len(p) >= 224 { + return nil, errors.New("too long") + } + var buf bytes.Buffer + buf.Write(clientID[:]) + n := numPadding + if len(p) == 0 { + n = numPaddingForPoll + } + buf.WriteByte(byte(224 + n)) + _, _ = io.CopyN(&buf, rand.Reader, int64(n)) + if len(p) > 0 { + buf.WriteByte(byte(len(p))) + buf.Write(p) + } + decoded = buf.Bytes() + } + + encoded := make([]byte, base32Encoding.EncodedLen(len(decoded))) + base32Encoding.Encode(encoded, decoded) + encoded = bytes.ToLower(encoded) + labels := chunks(encoded, 63) + labels = append(labels, domain...) + name, err := NewName(labels) + if err != nil { + return nil, err + } + + var id uint16 + _ = binary.Read(rand.Reader, binary.BigEndian, &id) + query := &Message{ + ID: id, + Flags: 0x0100, + Question: []Question{ + { + Name: name, + Type: RRTypeTXT, + Class: ClassIN, + }, + }, + Additional: []RR{ + { + Name: Name{}, + Type: RRTypeOPT, + Class: 4096, + TTL: 0, + Data: []byte{}, + }, + }, + } + + buf, err := query.WireFormat() + if err != nil { + return nil, err + } + + return buf, nil +} + +func chunks(p []byte, n int) [][]byte { + var result [][]byte + for len(p) > 0 { + sz := len(p) + if sz > n { + sz = n + } + result = append(result, p[:sz]) + p = p[sz:] + } + return result +} + +func nextPacket(r *bytes.Reader) ([]byte, error) { + var n uint16 + err := binary.Read(r, binary.BigEndian, &n) + if err != nil { + return nil, err + } + p := make([]byte, n) + _, err = io.ReadFull(r, p) + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + return p, err +} + +func dnsResponsePayload(resp *Message, domain Name) []byte { + if resp.Flags&0x8000 != 0x8000 { + return nil + } + if resp.Flags&0x000f != RcodeNoError { + return nil + } + + if len(resp.Answer) != 1 { + return nil + } + answer := resp.Answer[0] + + _, ok := answer.Name.TrimSuffix(domain) + if !ok { + return nil + } + + if answer.Type != RRTypeTXT { + return nil + } + payload, err := DecodeRDataTXT(answer.Data) + if err != nil { + return nil + } + + return payload +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/xdns/config.go b/subproject/Xray-core-main/transport/internet/finalmask/xdns/config.go new file mode 100644 index 00000000..157102da --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/xdns/config.go @@ -0,0 +1,16 @@ +package xdns + +import ( + "net" +) + +func (c *Config) UDP() { +} + +func (c *Config) WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) { + return NewConnClient(c, raw) +} + +func (c *Config) WrapPacketConnServer(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) { + return NewConnServer(c, raw) +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/xdns/config.pb.go b/subproject/Xray-core-main/transport/internet/finalmask/xdns/config.pb.go new file mode 100644 index 00000000..27924051 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/xdns/config.pb.go @@ -0,0 +1,123 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: transport/internet/finalmask/xdns/config.proto + +package xdns + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + Domain string `protobuf:"bytes,1,opt,name=domain,proto3" json:"domain,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_transport_internet_finalmask_xdns_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_finalmask_xdns_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_transport_internet_finalmask_xdns_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Config) GetDomain() string { + if x != nil { + return x.Domain + } + return "" +} + +var File_transport_internet_finalmask_xdns_config_proto protoreflect.FileDescriptor + +const file_transport_internet_finalmask_xdns_config_proto_rawDesc = "" + + "\n" + + ".transport/internet/finalmask/xdns/config.proto\x12&xray.transport.internet.finalmask.xdns\" \n" + + "\x06Config\x12\x16\n" + + "\x06domain\x18\x01 \x01(\tR\x06domainB\x94\x01\n" + + "*com.xray.transport.internet.finalmask.xdnsP\x01Z;github.com/xtls/xray-core/transport/internet/finalmask/xdns\xaa\x02&Xray.Transport.Internet.Finalmask.Xdnsb\x06proto3" + +var ( + file_transport_internet_finalmask_xdns_config_proto_rawDescOnce sync.Once + file_transport_internet_finalmask_xdns_config_proto_rawDescData []byte +) + +func file_transport_internet_finalmask_xdns_config_proto_rawDescGZIP() []byte { + file_transport_internet_finalmask_xdns_config_proto_rawDescOnce.Do(func() { + file_transport_internet_finalmask_xdns_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_xdns_config_proto_rawDesc), len(file_transport_internet_finalmask_xdns_config_proto_rawDesc))) + }) + return file_transport_internet_finalmask_xdns_config_proto_rawDescData +} + +var file_transport_internet_finalmask_xdns_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_transport_internet_finalmask_xdns_config_proto_goTypes = []any{ + (*Config)(nil), // 0: xray.transport.internet.finalmask.xdns.Config +} +var file_transport_internet_finalmask_xdns_config_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_transport_internet_finalmask_xdns_config_proto_init() } +func file_transport_internet_finalmask_xdns_config_proto_init() { + if File_transport_internet_finalmask_xdns_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_xdns_config_proto_rawDesc), len(file_transport_internet_finalmask_xdns_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_transport_internet_finalmask_xdns_config_proto_goTypes, + DependencyIndexes: file_transport_internet_finalmask_xdns_config_proto_depIdxs, + MessageInfos: file_transport_internet_finalmask_xdns_config_proto_msgTypes, + }.Build() + File_transport_internet_finalmask_xdns_config_proto = out.File + file_transport_internet_finalmask_xdns_config_proto_goTypes = nil + file_transport_internet_finalmask_xdns_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/xdns/config.proto b/subproject/Xray-core-main/transport/internet/finalmask/xdns/config.proto new file mode 100644 index 00000000..e1c71770 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/xdns/config.proto @@ -0,0 +1,12 @@ +syntax = "proto3"; + +package xray.transport.internet.finalmask.xdns; +option csharp_namespace = "Xray.Transport.Internet.Finalmask.Xdns"; +option go_package = "github.com/xtls/xray-core/transport/internet/finalmask/xdns"; +option java_package = "com.xray.transport.internet.finalmask.xdns"; +option java_multiple_files = true; + +message Config { + string domain = 1; +} + diff --git a/subproject/Xray-core-main/transport/internet/finalmask/xdns/dns.go b/subproject/Xray-core-main/transport/internet/finalmask/xdns/dns.go new file mode 100644 index 00000000..4cdac7cd --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/xdns/dns.go @@ -0,0 +1,575 @@ +// Package dns deals with encoding and decoding DNS wire format. +package xdns + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "io" + "strings" +) + +// The maximum number of DNS name compression pointers we are willing to follow. +// Without something like this, infinite loops are possible. +const compressionPointerLimit = 10 + +var ( + // ErrZeroLengthLabel is the error returned for names that contain a + // zero-length label, like "example..com". + ErrZeroLengthLabel = errors.New("name contains a zero-length label") + + // ErrLabelTooLong is the error returned for labels that are longer than + // 63 octets. + ErrLabelTooLong = errors.New("name contains a label longer than 63 octets") + + // ErrNameTooLong is the error returned for names whose encoded + // representation is longer than 255 octets. + ErrNameTooLong = errors.New("name is longer than 255 octets") + + // ErrReservedLabelType is the error returned when reading a label type + // prefix whose two most significant bits are not 00 or 11. + ErrReservedLabelType = errors.New("reserved label type") + + // ErrTooManyPointers is the error returned when reading a compressed + // name that has too many compression pointers. + ErrTooManyPointers = errors.New("too many compression pointers") + + // ErrTrailingBytes is the error returned when bytes remain in the parse + // buffer after parsing a message. + ErrTrailingBytes = errors.New("trailing bytes after message") + + // ErrIntegerOverflow is the error returned when trying to encode an + // integer greater than 65535 into a 16-bit field. + ErrIntegerOverflow = errors.New("integer overflow") +) + +const ( + // https://tools.ietf.org/html/rfc1035#section-3.2.2 + RRTypeTXT = 16 + // https://tools.ietf.org/html/rfc6891#section-6.1.1 + RRTypeOPT = 41 + + // https://tools.ietf.org/html/rfc1035#section-3.2.4 + ClassIN = 1 + + // https://tools.ietf.org/html/rfc1035#section-4.1.1 + RcodeNoError = 0 // a.k.a. NOERROR + RcodeFormatError = 1 // a.k.a. FORMERR + RcodeNameError = 3 // a.k.a. NXDOMAIN + RcodeNotImplemented = 4 // a.k.a. NOTIMPL + // https://tools.ietf.org/html/rfc6891#section-9 + ExtendedRcodeBadVers = 16 // a.k.a. BADVERS +) + +// Name represents a domain name, a sequence of labels each of which is 63 +// octets or less in length. +// +// https://tools.ietf.org/html/rfc1035#section-3.1 +type Name [][]byte + +// NewName returns a Name from a slice of labels, after checking the labels for +// validity. Does not include a zero-length label at the end of the slice. +func NewName(labels [][]byte) (Name, error) { + name := Name(labels) + // https://tools.ietf.org/html/rfc1035#section-2.3.4 + // Various objects and parameters in the DNS have size limits. + // labels 63 octets or less + // names 255 octets or less + for _, label := range labels { + if len(label) == 0 { + return nil, ErrZeroLengthLabel + } + if len(label) > 63 { + return nil, ErrLabelTooLong + } + } + // Check the total length. + builder := newMessageBuilder() + builder.WriteName(name) + if len(builder.Bytes()) > 255 { + return nil, ErrNameTooLong + } + return name, nil +} + +// ParseName returns a new Name from a string of labels separated by dots, after +// checking the name for validity. A single dot at the end of the string is +// ignored. +func ParseName(s string) (Name, error) { + b := bytes.TrimSuffix([]byte(s), []byte(".")) + if len(b) == 0 { + // bytes.Split(b, ".") would return [""] in this case + return NewName([][]byte{}) + } else { + return NewName(bytes.Split(b, []byte("."))) + } +} + +// String returns a reversible string representation of name. Labels are +// separated by dots, and any bytes in a label that are outside the set +// [0-9A-Za-z-] are replaced with a \xXX hex escape sequence. +func (name Name) String() string { + if len(name) == 0 { + return "." + } + + var buf strings.Builder + for i, label := range name { + if i > 0 { + buf.WriteByte('.') + } + for _, b := range label { + if b == '-' || + ('0' <= b && b <= '9') || + ('A' <= b && b <= 'Z') || + ('a' <= b && b <= 'z') { + buf.WriteByte(b) + } else { + fmt.Fprintf(&buf, "\\x%02x", b) + } + } + } + return buf.String() +} + +// TrimSuffix returns a Name with the given suffix removed, if it was present. +// The second return value indicates whether the suffix was present. If the +// suffix was not present, the first return value is nil. +func (name Name) TrimSuffix(suffix Name) (Name, bool) { + if len(name) < len(suffix) { + return nil, false + } + split := len(name) - len(suffix) + fore, aft := name[:split], name[split:] + for i := 0; i < len(aft); i++ { + if !bytes.Equal(bytes.ToLower(aft[i]), bytes.ToLower(suffix[i])) { + return nil, false + } + } + return fore, true +} + +// Message represents a DNS message. +// +// https://tools.ietf.org/html/rfc1035#section-4.1 +type Message struct { + ID uint16 + Flags uint16 + + Question []Question + Answer []RR + Authority []RR + Additional []RR +} + +// Opcode extracts the OPCODE part of the Flags field. +// +// https://tools.ietf.org/html/rfc1035#section-4.1.1 +func (message *Message) Opcode() uint16 { + return (message.Flags >> 11) & 0xf +} + +// Rcode extracts the RCODE part of the Flags field. +// +// https://tools.ietf.org/html/rfc1035#section-4.1.1 +func (message *Message) Rcode() uint16 { + return message.Flags & 0x000f +} + +// Question represents an entry in the question section of a message. +// +// https://tools.ietf.org/html/rfc1035#section-4.1.2 +type Question struct { + Name Name + Type uint16 + Class uint16 +} + +// RR represents a resource record. +// +// https://tools.ietf.org/html/rfc1035#section-4.1.3 +type RR struct { + Name Name + Type uint16 + Class uint16 + TTL uint32 + Data []byte +} + +// readName parses a DNS name from r. It leaves r positioned just after the +// parsed name. +func readName(r io.ReadSeeker) (Name, error) { + var labels [][]byte + // We limit the number of compression pointers we are willing to follow. + numPointers := 0 + // If we followed any compression pointers, we must finally seek to just + // past the first pointer. + var seekTo int64 +loop: + for { + var labelType byte + err := binary.Read(r, binary.BigEndian, &labelType) + if err != nil { + return nil, err + } + + switch labelType & 0xc0 { + case 0x00: + // This is an ordinary label. + // https://tools.ietf.org/html/rfc1035#section-3.1 + length := int(labelType & 0x3f) + if length == 0 { + break loop + } + label := make([]byte, length) + _, err := io.ReadFull(r, label) + if err != nil { + return nil, err + } + labels = append(labels, label) + case 0xc0: + // This is a compression pointer. + // https://tools.ietf.org/html/rfc1035#section-4.1.4 + upper := labelType & 0x3f + var lower byte + err := binary.Read(r, binary.BigEndian, &lower) + if err != nil { + return nil, err + } + offset := (uint16(upper) << 8) | uint16(lower) + + if numPointers == 0 { + // The first time we encounter a pointer, + // remember our position so we can seek back to + // it when done. + seekTo, err = r.Seek(0, io.SeekCurrent) + if err != nil { + return nil, err + } + } + numPointers++ + if numPointers > compressionPointerLimit { + return nil, ErrTooManyPointers + } + + // Follow the pointer and continue. + _, err = r.Seek(int64(offset), io.SeekStart) + if err != nil { + return nil, err + } + default: + // "The 10 and 01 combinations are reserved for future + // use." + return nil, ErrReservedLabelType + } + } + // If we followed any pointers, then seek back to just after the first + // one. + if numPointers > 0 { + _, err := r.Seek(seekTo, io.SeekStart) + if err != nil { + return nil, err + } + } + return NewName(labels) +} + +// readQuestion parses one entry from the Question section. It leaves r +// positioned just after the parsed entry. +// +// https://tools.ietf.org/html/rfc1035#section-4.1.2 +func readQuestion(r io.ReadSeeker) (Question, error) { + var question Question + var err error + question.Name, err = readName(r) + if err != nil { + return question, err + } + for _, ptr := range []*uint16{&question.Type, &question.Class} { + err := binary.Read(r, binary.BigEndian, ptr) + if err != nil { + return question, err + } + } + + return question, nil +} + +// readRR parses one resource record. It leaves r positioned just after the +// parsed resource record. +// +// https://tools.ietf.org/html/rfc1035#section-4.1.3 +func readRR(r io.ReadSeeker) (RR, error) { + var rr RR + var err error + rr.Name, err = readName(r) + if err != nil { + return rr, err + } + for _, ptr := range []*uint16{&rr.Type, &rr.Class} { + err := binary.Read(r, binary.BigEndian, ptr) + if err != nil { + return rr, err + } + } + err = binary.Read(r, binary.BigEndian, &rr.TTL) + if err != nil { + return rr, err + } + var rdLength uint16 + err = binary.Read(r, binary.BigEndian, &rdLength) + if err != nil { + return rr, err + } + rr.Data = make([]byte, rdLength) + _, err = io.ReadFull(r, rr.Data) + if err != nil { + return rr, err + } + + return rr, nil +} + +// readMessage parses a complete DNS message. It leaves r positioned just after +// the parsed message. +func readMessage(r io.ReadSeeker) (Message, error) { + var message Message + + // Header section + // https://tools.ietf.org/html/rfc1035#section-4.1.1 + var qdCount, anCount, nsCount, arCount uint16 + for _, ptr := range []*uint16{ + &message.ID, &message.Flags, + &qdCount, &anCount, &nsCount, &arCount, + } { + err := binary.Read(r, binary.BigEndian, ptr) + if err != nil { + return message, err + } + } + + // Question section + // https://tools.ietf.org/html/rfc1035#section-4.1.2 + for i := 0; i < int(qdCount); i++ { + question, err := readQuestion(r) + if err != nil { + return message, err + } + message.Question = append(message.Question, question) + } + + // Answer, Authority, and Additional sections + // https://tools.ietf.org/html/rfc1035#section-4.1.3 + for _, rec := range []struct { + ptr *[]RR + count uint16 + }{ + {&message.Answer, anCount}, + {&message.Authority, nsCount}, + {&message.Additional, arCount}, + } { + for i := 0; i < int(rec.count); i++ { + rr, err := readRR(r) + if err != nil { + return message, err + } + *rec.ptr = append(*rec.ptr, rr) + } + } + + return message, nil +} + +// MessageFromWireFormat parses a message from buf and returns a Message object. +// It returns ErrTrailingBytes if there are bytes remaining in buf after parsing +// is done. +func MessageFromWireFormat(buf []byte) (Message, error) { + r := bytes.NewReader(buf) + message, err := readMessage(r) + if err == io.EOF { + err = io.ErrUnexpectedEOF + } else if err == nil { + // Check for trailing bytes. + _, err = r.ReadByte() + if err == io.EOF { + err = nil + } else if err == nil { + err = ErrTrailingBytes + } + } + return message, err +} + +// messageBuilder manages the state of serializing a DNS message. Its main +// function is to keep track of names already written for the purpose of name +// compression. +type messageBuilder struct { + w bytes.Buffer + nameCache map[string]int +} + +// newMessageBuilder creates a new messageBuilder with an empty name cache. +func newMessageBuilder() *messageBuilder { + return &messageBuilder{ + nameCache: make(map[string]int), + } +} + +// Bytes returns the serialized DNS message as a slice of bytes. +func (builder *messageBuilder) Bytes() []byte { + return builder.w.Bytes() +} + +// WriteName appends name to the in-progress messageBuilder, employing +// compression pointers to previously written names if possible. +func (builder *messageBuilder) WriteName(name Name) { + // https://tools.ietf.org/html/rfc1035#section-3.1 + for i := range name { + // Has this suffix already been encoded in the message? + if ptr, ok := builder.nameCache[name[i:].String()]; ok && ptr&0x3fff == ptr { + // If so, we can write a compression pointer. + binary.Write(&builder.w, binary.BigEndian, uint16(0xc000|ptr)) + return + } + // Not cached; we must encode this label verbatim. Store a cache + // entry pointing to the beginning of it. + builder.nameCache[name[i:].String()] = builder.w.Len() + length := len(name[i]) + if length == 0 || length > 63 { + panic(length) + } + builder.w.WriteByte(byte(length)) + builder.w.Write(name[i]) + } + builder.w.WriteByte(0) +} + +// WriteQuestion appends a Question section entry to the in-progress +// messageBuilder. +func (builder *messageBuilder) WriteQuestion(question *Question) { + // https://tools.ietf.org/html/rfc1035#section-4.1.2 + builder.WriteName(question.Name) + binary.Write(&builder.w, binary.BigEndian, question.Type) + binary.Write(&builder.w, binary.BigEndian, question.Class) +} + +// WriteRR appends a resource record to the in-progress messageBuilder. It +// returns ErrIntegerOverflow if the length of rr.Data does not fit in 16 bits. +func (builder *messageBuilder) WriteRR(rr *RR) error { + // https://tools.ietf.org/html/rfc1035#section-4.1.3 + builder.WriteName(rr.Name) + binary.Write(&builder.w, binary.BigEndian, rr.Type) + binary.Write(&builder.w, binary.BigEndian, rr.Class) + binary.Write(&builder.w, binary.BigEndian, rr.TTL) + rdLength := uint16(len(rr.Data)) + if int(rdLength) != len(rr.Data) { + return ErrIntegerOverflow + } + binary.Write(&builder.w, binary.BigEndian, rdLength) + builder.w.Write(rr.Data) + return nil +} + +// WriteMessage appends a complete DNS message to the in-progress +// messageBuilder. It returns ErrIntegerOverflow if the number of entries in any +// section, or the length of the data in any resource record, does not fit in 16 +// bits. +func (builder *messageBuilder) WriteMessage(message *Message) error { + // Header section + // https://tools.ietf.org/html/rfc1035#section-4.1.1 + binary.Write(&builder.w, binary.BigEndian, message.ID) + binary.Write(&builder.w, binary.BigEndian, message.Flags) + for _, count := range []int{ + len(message.Question), + len(message.Answer), + len(message.Authority), + len(message.Additional), + } { + count16 := uint16(count) + if int(count16) != count { + return ErrIntegerOverflow + } + binary.Write(&builder.w, binary.BigEndian, count16) + } + + // Question section + // https://tools.ietf.org/html/rfc1035#section-4.1.2 + for _, question := range message.Question { + builder.WriteQuestion(&question) + } + + // Answer, Authority, and Additional sections + // https://tools.ietf.org/html/rfc1035#section-4.1.3 + for _, rrs := range [][]RR{message.Answer, message.Authority, message.Additional} { + for _, rr := range rrs { + err := builder.WriteRR(&rr) + if err != nil { + return err + } + } + } + + return nil +} + +// WireFormat encodes a Message as a slice of bytes in DNS wire format. It +// returns ErrIntegerOverflow if the number of entries in any section, or the +// length of the data in any resource record, does not fit in 16 bits. +func (message *Message) WireFormat() ([]byte, error) { + builder := newMessageBuilder() + err := builder.WriteMessage(message) + if err != nil { + return nil, err + } + return builder.Bytes(), nil +} + +// DecodeRDataTXT decodes TXT-DATA (as found in the RDATA for a resource record +// with TYPE=TXT) as a raw byte slice, by concatenating all the +// s it contains. +// +// https://tools.ietf.org/html/rfc1035#section-3.3.14 +func DecodeRDataTXT(p []byte) ([]byte, error) { + var buf bytes.Buffer + for { + if len(p) == 0 { + return nil, io.ErrUnexpectedEOF + } + n := int(p[0]) + p = p[1:] + if len(p) < n { + return nil, io.ErrUnexpectedEOF + } + buf.Write(p[:n]) + p = p[n:] + if len(p) == 0 { + break + } + } + return buf.Bytes(), nil +} + +// EncodeRDataTXT encodes a slice of bytes as TXT-DATA, as appropriate for the +// RDATA of a resource record with TYPE=TXT. No length restriction is enforced +// here; that must be checked at a higher level. +// +// https://tools.ietf.org/html/rfc1035#section-3.3.14 +func EncodeRDataTXT(p []byte) []byte { + // https://tools.ietf.org/html/rfc1035#section-3.3 + // https://tools.ietf.org/html/rfc1035#section-3.3.14 + // TXT data is a sequence of one or more s, where + // is a length octet followed by that number of + // octets. + var buf bytes.Buffer + for len(p) > 255 { + buf.WriteByte(255) + buf.Write(p[:255]) + p = p[255:] + } + // Must write here, even if len(p) == 0, because it's "*one or more* + // s". + buf.WriteByte(byte(len(p))) + buf.Write(p) + return buf.Bytes() +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/xdns/dns_test.go b/subproject/Xray-core-main/transport/internet/finalmask/xdns/dns_test.go new file mode 100644 index 00000000..aa163476 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/xdns/dns_test.go @@ -0,0 +1,594 @@ +package xdns + +import ( + "bytes" + "fmt" + "io" + "strconv" + "strings" + "testing" +) + +func namesEqual(a, b Name) bool { + if len(a) != len(b) { + return false + } + for i := 0; i < len(a); i++ { + if !bytes.Equal(a[i], b[i]) { + return false + } + } + return true +} + +func TestName(t *testing.T) { + for _, test := range []struct { + labels [][]byte + err error + s string + }{ + {[][]byte{}, nil, "."}, + {[][]byte{[]byte("test")}, nil, "test"}, + {[][]byte{[]byte("a"), []byte("b"), []byte("c")}, nil, "a.b.c"}, + + {[][]byte{{}}, ErrZeroLengthLabel, ""}, + {[][]byte{[]byte("a"), {}, []byte("c")}, ErrZeroLengthLabel, ""}, + + // 63 octets. + {[][]byte{[]byte("0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789ABCDE")}, nil, + "0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789ABCDE"}, + // 64 octets. + {[][]byte{[]byte("0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789ABCDEF")}, ErrLabelTooLong, ""}, + + // 64+64+64+62 octets. + {[][]byte{ + []byte("0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789ABCDE"), + []byte("0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789ABCDE"), + []byte("0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789ABCDE"), + []byte("0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789ABC"), + }, nil, + "0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789ABCDE.0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789ABCDE.0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789ABCDE.0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789ABC"}, + // 64+64+64+63 octets. + {[][]byte{ + []byte("0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789ABCDE"), + []byte("0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789ABCDE"), + []byte("0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789ABCDE"), + []byte("0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789ABCD"), + }, ErrNameTooLong, ""}, + // 127 one-octet labels. + {[][]byte{ + {'0'}, {'1'}, {'2'}, {'3'}, {'4'}, {'5'}, {'6'}, {'7'}, {'8'}, {'9'}, {'a'}, {'b'}, {'c'}, {'d'}, {'e'}, {'f'}, + {'0'}, {'1'}, {'2'}, {'3'}, {'4'}, {'5'}, {'6'}, {'7'}, {'8'}, {'9'}, {'A'}, {'B'}, {'C'}, {'D'}, {'E'}, {'F'}, + {'0'}, {'1'}, {'2'}, {'3'}, {'4'}, {'5'}, {'6'}, {'7'}, {'8'}, {'9'}, {'a'}, {'b'}, {'c'}, {'d'}, {'e'}, {'f'}, + {'0'}, {'1'}, {'2'}, {'3'}, {'4'}, {'5'}, {'6'}, {'7'}, {'8'}, {'9'}, {'A'}, {'B'}, {'C'}, {'D'}, {'E'}, {'F'}, + {'0'}, {'1'}, {'2'}, {'3'}, {'4'}, {'5'}, {'6'}, {'7'}, {'8'}, {'9'}, {'a'}, {'b'}, {'c'}, {'d'}, {'e'}, {'f'}, + {'0'}, {'1'}, {'2'}, {'3'}, {'4'}, {'5'}, {'6'}, {'7'}, {'8'}, {'9'}, {'A'}, {'B'}, {'C'}, {'D'}, {'E'}, {'F'}, + {'0'}, {'1'}, {'2'}, {'3'}, {'4'}, {'5'}, {'6'}, {'7'}, {'8'}, {'9'}, {'a'}, {'b'}, {'c'}, {'d'}, {'e'}, {'f'}, + {'0'}, {'1'}, {'2'}, {'3'}, {'4'}, {'5'}, {'6'}, {'7'}, {'8'}, {'9'}, {'A'}, {'B'}, {'C'}, {'D'}, {'E'}, + }, nil, + "0.1.2.3.4.5.6.7.8.9.a.b.c.d.e.f.0.1.2.3.4.5.6.7.8.9.A.B.C.D.E.F.0.1.2.3.4.5.6.7.8.9.a.b.c.d.e.f.0.1.2.3.4.5.6.7.8.9.A.B.C.D.E.F.0.1.2.3.4.5.6.7.8.9.a.b.c.d.e.f.0.1.2.3.4.5.6.7.8.9.A.B.C.D.E.F.0.1.2.3.4.5.6.7.8.9.a.b.c.d.e.f.0.1.2.3.4.5.6.7.8.9.A.B.C.D.E"}, + // 128 one-octet labels. + {[][]byte{ + {'0'}, {'1'}, {'2'}, {'3'}, {'4'}, {'5'}, {'6'}, {'7'}, {'8'}, {'9'}, {'a'}, {'b'}, {'c'}, {'d'}, {'e'}, {'f'}, + {'0'}, {'1'}, {'2'}, {'3'}, {'4'}, {'5'}, {'6'}, {'7'}, {'8'}, {'9'}, {'A'}, {'B'}, {'C'}, {'D'}, {'E'}, {'F'}, + {'0'}, {'1'}, {'2'}, {'3'}, {'4'}, {'5'}, {'6'}, {'7'}, {'8'}, {'9'}, {'a'}, {'b'}, {'c'}, {'d'}, {'e'}, {'f'}, + {'0'}, {'1'}, {'2'}, {'3'}, {'4'}, {'5'}, {'6'}, {'7'}, {'8'}, {'9'}, {'A'}, {'B'}, {'C'}, {'D'}, {'E'}, {'F'}, + {'0'}, {'1'}, {'2'}, {'3'}, {'4'}, {'5'}, {'6'}, {'7'}, {'8'}, {'9'}, {'a'}, {'b'}, {'c'}, {'d'}, {'e'}, {'f'}, + {'0'}, {'1'}, {'2'}, {'3'}, {'4'}, {'5'}, {'6'}, {'7'}, {'8'}, {'9'}, {'A'}, {'B'}, {'C'}, {'D'}, {'E'}, {'F'}, + {'0'}, {'1'}, {'2'}, {'3'}, {'4'}, {'5'}, {'6'}, {'7'}, {'8'}, {'9'}, {'a'}, {'b'}, {'c'}, {'d'}, {'e'}, {'f'}, + {'0'}, {'1'}, {'2'}, {'3'}, {'4'}, {'5'}, {'6'}, {'7'}, {'8'}, {'9'}, {'A'}, {'B'}, {'C'}, {'D'}, {'E'}, {'F'}, + }, ErrNameTooLong, ""}, + } { + // Test that NewName returns proper error codes, and otherwise + // returns an equal slice of labels. + name, err := NewName(test.labels) + if err != test.err || (err == nil && !namesEqual(name, test.labels)) { + t.Errorf("%+q returned (%+q, %v), expected (%+q, %v)", + test.labels, name, err, test.labels, test.err) + continue + } + if test.err != nil { + continue + } + + // Test that the string version of the name comes out as + // expected. + s := name.String() + if s != test.s { + t.Errorf("%+q became string %+q, expected %+q", test.labels, s, test.s) + continue + } + + // Test that parsing from a string back to a Name results in the + // original slice of labels. + name, err = ParseName(s) + if err != nil || !namesEqual(name, test.labels) { + t.Errorf("%+q parsing %+q returned (%+q, %v), expected (%+q, %v)", + test.labels, s, name, err, test.labels, nil) + continue + } + // A trailing dot should be ignored. + if !strings.HasSuffix(s, ".") { + dotName, dotErr := ParseName(s + ".") + if dotErr != err || !namesEqual(dotName, name) { + t.Errorf("%+q parsing %+q returned (%+q, %v), expected (%+q, %v)", + test.labels, s+".", dotName, dotErr, name, err) + continue + } + } + } +} + +func TestParseName(t *testing.T) { + for _, test := range []struct { + s string + name Name + err error + }{ + // This case can't be tested by TestName above because String + // will never produce "" (it produces "." instead). + {"", [][]byte{}, nil}, + } { + name, err := ParseName(test.s) + if err != test.err || (err == nil && !namesEqual(name, test.name)) { + t.Errorf("%+q returned (%+q, %v), expected (%+q, %v)", + test.s, name, err, test.name, test.err) + continue + } + } +} + +func unescapeString(s string) ([][]byte, error) { + if s == "." { + return [][]byte{}, nil + } + + var result [][]byte + for _, label := range strings.Split(s, ".") { + var buf bytes.Buffer + i := 0 + for i < len(label) { + switch label[i] { + case '\\': + if i+3 >= len(label) { + return nil, fmt.Errorf("truncated escape sequence at index %v", i) + } + if label[i+1] != 'x' { + return nil, fmt.Errorf("malformed escape sequence at index %v", i) + } + b, err := strconv.ParseUint(string(label[i+2:i+4]), 16, 8) + if err != nil { + return nil, fmt.Errorf("malformed hex sequence at index %v", i+2) + } + buf.WriteByte(byte(b)) + i += 4 + default: + buf.WriteByte(label[i]) + i++ + } + } + result = append(result, buf.Bytes()) + } + return result, nil +} + +func TestNameString(t *testing.T) { + for _, test := range []struct { + name Name + s string + }{ + {[][]byte{}, "."}, + {[][]byte{[]byte("\x00"), []byte("a.b"), []byte("c\nd\\")}, "\\x00.a\\x2eb.c\\x0ad\\x5c"}, + {[][]byte{ + []byte("\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !\"#$%&'()*+,-./0123456789:;<=>"), + []byte("?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}"), + []byte("~\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc"), + []byte("\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb"), + []byte("\xfc\xfd\xfe\xff"), + }, "\\x00\\x01\\x02\\x03\\x04\\x05\\x06\\x07\\x08\\x09\\x0a\\x0b\\x0c\\x0d\\x0e\\x0f\\x10\\x11\\x12\\x13\\x14\\x15\\x16\\x17\\x18\\x19\\x1a\\x1b\\x1c\\x1d\\x1e\\x1f\\x20\\x21\\x22\\x23\\x24\\x25\\x26\\x27\\x28\\x29\\x2a\\x2b\\x2c-\\x2e\\x2f0123456789\\x3a\\x3b\\x3c\\x3d\\x3e.\\x3f\\x40ABCDEFGHIJKLMNOPQRSTUVWXYZ\\x5b\\x5c\\x5d\\x5e\\x5f\\x60abcdefghijklmnopqrstuvwxyz\\x7b\\x7c\\x7d.\\x7e\\x7f\\x80\\x81\\x82\\x83\\x84\\x85\\x86\\x87\\x88\\x89\\x8a\\x8b\\x8c\\x8d\\x8e\\x8f\\x90\\x91\\x92\\x93\\x94\\x95\\x96\\x97\\x98\\x99\\x9a\\x9b\\x9c\\x9d\\x9e\\x9f\\xa0\\xa1\\xa2\\xa3\\xa4\\xa5\\xa6\\xa7\\xa8\\xa9\\xaa\\xab\\xac\\xad\\xae\\xaf\\xb0\\xb1\\xb2\\xb3\\xb4\\xb5\\xb6\\xb7\\xb8\\xb9\\xba\\xbb\\xbc.\\xbd\\xbe\\xbf\\xc0\\xc1\\xc2\\xc3\\xc4\\xc5\\xc6\\xc7\\xc8\\xc9\\xca\\xcb\\xcc\\xcd\\xce\\xcf\\xd0\\xd1\\xd2\\xd3\\xd4\\xd5\\xd6\\xd7\\xd8\\xd9\\xda\\xdb\\xdc\\xdd\\xde\\xdf\\xe0\\xe1\\xe2\\xe3\\xe4\\xe5\\xe6\\xe7\\xe8\\xe9\\xea\\xeb\\xec\\xed\\xee\\xef\\xf0\\xf1\\xf2\\xf3\\xf4\\xf5\\xf6\\xf7\\xf8\\xf9\\xfa\\xfb.\\xfc\\xfd\\xfe\\xff"}, + } { + s := test.name.String() + if s != test.s { + t.Errorf("%+q escaped to %+q, expected %+q", test.name, s, test.s) + continue + } + unescaped, err := unescapeString(s) + if err != nil { + t.Errorf("%+q unescaping %+q resulted in error %v", test.name, s, err) + continue + } + if !namesEqual(Name(unescaped), test.name) { + t.Errorf("%+q roundtripped through %+q to %+q", test.name, s, unescaped) + continue + } + } +} + +func TestNameTrimSuffix(t *testing.T) { + for _, test := range []struct { + name, suffix string + trimmed string + ok bool + }{ + {"", "", ".", true}, + {".", ".", ".", true}, + {"abc", "", "abc", true}, + {"abc", ".", "abc", true}, + {"", "abc", ".", false}, + {".", "abc", ".", false}, + {"example.com", "com", "example", true}, + {"example.com", "net", ".", false}, + {"example.com", "example.com", ".", true}, + {"example.com", "test.com", ".", false}, + {"example.com", "xample.com", ".", false}, + {"example.com", "example", ".", false}, + {"example.com", "COM", "example", true}, + {"EXAMPLE.COM", "com", "EXAMPLE", true}, + } { + tmp, ok := mustParseName(test.name).TrimSuffix(mustParseName(test.suffix)) + trimmed := tmp.String() + if ok != test.ok || trimmed != test.trimmed { + t.Errorf("TrimSuffix %+q %+q returned (%+q, %v), expected (%+q, %v)", + test.name, test.suffix, trimmed, ok, test.trimmed, test.ok) + continue + } + } +} + +func TestReadName(t *testing.T) { + // Good tests. + for _, test := range []struct { + start int64 + end int64 + input string + s string + }{ + // Empty name. + {0, 1, "\x00abcd", "."}, + // No pointers. + {12, 25, "AAAABBBBCCCC\x07example\x03com\x00", "example.com"}, + // Backward pointer. + {25, 31, "AAAABBBBCCCC\x07example\x03com\x00\x03sub\xc0\x0c", "sub.example.com"}, + // Forward pointer. + {0, 4, "\x01a\xc0\x04\x03bcd\x00", "a.bcd"}, + // Two backwards pointers. + {31, 38, "AAAABBBBCCCC\x07example\x03com\x00\x03sub\xc0\x0c\x04sub2\xc0\x19", "sub2.sub.example.com"}, + // Forward then backward pointer. + {25, 31, "AAAABBBBCCCC\x07example\x03com\x00\x03sub\xc0\x1f\x04sub2\xc0\x0c", "sub.sub2.example.com"}, + // Overlapping codons. + {0, 4, "\x01a\xc0\x03bcd\x00", "a.bcd"}, + // Pointer to empty label. + {0, 10, "\x07example\xc0\x0a\x00", "example"}, + {1, 11, "\x00\x07example\xc0\x00", "example"}, + // Pointer to pointer to empty label. + {0, 10, "\x07example\xc0\x0a\xc0\x0c\x00", "example"}, + {1, 11, "\x00\x07example\xc0\x0c\xc0\x00", "example"}, + } { + r := bytes.NewReader([]byte(test.input)) + _, err := r.Seek(test.start, io.SeekStart) + if err != nil { + panic(err) + } + name, err := readName(r) + if err != nil { + t.Errorf("%+q returned error %s", test.input, err) + continue + } + s := name.String() + if s != test.s { + t.Errorf("%+q returned %+q, expected %+q", test.input, s, test.s) + continue + } + cur, _ := r.Seek(0, io.SeekCurrent) + if cur != test.end { + t.Errorf("%+q left offset %d, expected %d", test.input, cur, test.end) + continue + } + } + + // Bad tests. + for _, test := range []struct { + start int64 + input string + err error + }{ + {0, "", io.ErrUnexpectedEOF}, + // Reserved label type. + {0, "\x80example", ErrReservedLabelType}, + // Reserved label type. + {0, "\x40example", ErrReservedLabelType}, + // No Terminating empty label. + {0, "\x07example\x03com", io.ErrUnexpectedEOF}, + // Pointer past end of buffer. + {0, "\x07example\xc0\xff", io.ErrUnexpectedEOF}, + // Pointer to self. + {0, "\x07example\x03com\xc0\x0c", ErrTooManyPointers}, + // Pointer to self with intermediate label. + {0, "\x07example\x03com\xc0\x08", ErrTooManyPointers}, + // Two pointers that point to each other. + {0, "\xc0\x02\xc0\x00", ErrTooManyPointers}, + // Two pointers that point to each other, with intermediate labels. + {0, "\x01a\xc0\x04\x01b\xc0\x00", ErrTooManyPointers}, + // EOF while reading label. + {0, "\x0aexample", io.ErrUnexpectedEOF}, + // EOF before second byte of pointer. + {0, "\xc0", io.ErrUnexpectedEOF}, + {0, "\x07example\xc0", io.ErrUnexpectedEOF}, + } { + r := bytes.NewReader([]byte(test.input)) + _, err := r.Seek(test.start, io.SeekStart) + if err != nil { + panic(err) + } + name, err := readName(r) + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + if err != test.err { + t.Errorf("%+q returned (%+q, %v), expected %v", test.input, name, err, test.err) + continue + } + } +} + +func mustParseName(s string) Name { + name, err := ParseName(s) + if err != nil { + panic(err) + } + return name +} + +func questionsEqual(a, b *Question) bool { + if !namesEqual(a.Name, b.Name) { + return false + } + if a.Type != b.Type || a.Class != b.Class { + return false + } + return true +} + +func rrsEqual(a, b *RR) bool { + if !namesEqual(a.Name, b.Name) { + return false + } + if a.Type != b.Type || a.Class != b.Class || a.TTL != b.TTL { + return false + } + if !bytes.Equal(a.Data, b.Data) { + return false + } + return true +} + +func messagesEqual(a, b *Message) bool { + if a.ID != b.ID || a.Flags != b.Flags { + return false + } + if len(a.Question) != len(b.Question) { + return false + } + for i := 0; i < len(a.Question); i++ { + if !questionsEqual(&a.Question[i], &b.Question[i]) { + return false + } + } + for _, rec := range []struct{ rrA, rrB []RR }{ + {a.Answer, b.Answer}, + {a.Authority, b.Authority}, + {a.Additional, b.Additional}, + } { + if len(rec.rrA) != len(rec.rrB) { + return false + } + for i := 0; i < len(rec.rrA); i++ { + if !rrsEqual(&rec.rrA[i], &rec.rrB[i]) { + return false + } + } + } + return true +} + +func TestMessageFromWireFormat(t *testing.T) { + for _, test := range []struct { + buf string + expected Message + err error + }{ + { + "\x12\x34", + Message{}, + io.ErrUnexpectedEOF, + }, + { + "\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x03www\x07example\x03com\x00\x00\x01\x00\x01", + Message{ + ID: 0x1234, + Flags: 0x0100, + Question: []Question{ + { + Name: mustParseName("www.example.com"), + Type: 1, + Class: 1, + }, + }, + Answer: []RR{}, + Authority: []RR{}, + Additional: []RR{}, + }, + nil, + }, + { + "\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x03www\x07example\x03com\x00\x00\x01\x00\x01X", + Message{}, + ErrTrailingBytes, + }, + { + "\x12\x34\x81\x80\x00\x01\x00\x01\x00\x00\x00\x00\x03www\x07example\x03com\x00\x00\x01\x00\x01\x03www\x07example\x03com\x00\x00\x01\x00\x01\x00\x00\x00\x80\x00\x04\xc0\x00\x02\x01", + Message{ + ID: 0x1234, + Flags: 0x8180, + Question: []Question{ + { + Name: mustParseName("www.example.com"), + Type: 1, + Class: 1, + }, + }, + Answer: []RR{ + { + Name: mustParseName("www.example.com"), + Type: 1, + Class: 1, + TTL: 128, + Data: []byte{192, 0, 2, 1}, + }, + }, + Authority: []RR{}, + Additional: []RR{}, + }, + nil, + }, + } { + message, err := MessageFromWireFormat([]byte(test.buf)) + if err != test.err || (err == nil && !messagesEqual(&message, &test.expected)) { + t.Errorf("%+q\nreturned (%+v, %v)\nexpected (%+v, %v)", + test.buf, message, err, test.expected, test.err) + continue + } + } +} + +func TestMessageWireFormatRoundTrip(t *testing.T) { + for _, message := range []Message{ + { + ID: 0x1234, + Flags: 0x0100, + Question: []Question{ + { + Name: mustParseName("www.example.com"), + Type: 1, + Class: 1, + }, + { + Name: mustParseName("www2.example.com"), + Type: 2, + Class: 2, + }, + }, + Answer: []RR{ + { + Name: mustParseName("abc"), + Type: 2, + Class: 3, + TTL: 0xffffffff, + Data: []byte{1}, + }, + { + Name: mustParseName("xyz"), + Type: 2, + Class: 3, + TTL: 255, + Data: []byte{}, + }, + }, + Authority: []RR{ + { + Name: mustParseName("."), + Type: 65535, + Class: 65535, + TTL: 0, + Data: []byte("XXXXXXXXXXXXXXXXXXX"), + }, + }, + Additional: []RR{}, + }, + } { + buf, err := message.WireFormat() + if err != nil { + t.Errorf("%+v cannot make wire format: %v", message, err) + continue + } + message2, err := MessageFromWireFormat(buf) + if err != nil { + t.Errorf("%+q cannot parse wire format: %v", buf, err) + continue + } + if !messagesEqual(&message, &message2) { + t.Errorf("messages unequal\nbefore: %+v\n after: %+v", message, message2) + continue + } + } +} + +func TestDecodeRDataTXT(t *testing.T) { + for _, test := range []struct { + p []byte + decoded []byte + err error + }{ + {[]byte{}, nil, io.ErrUnexpectedEOF}, + {[]byte("\x00"), []byte{}, nil}, + {[]byte("\x01"), nil, io.ErrUnexpectedEOF}, + } { + decoded, err := DecodeRDataTXT(test.p) + if err != test.err || (err == nil && !bytes.Equal(decoded, test.decoded)) { + t.Errorf("%+q\nreturned (%+q, %v)\nexpected (%+q, %v)", + test.p, decoded, err, test.decoded, test.err) + continue + } + } +} + +func TestEncodeRDataTXT(t *testing.T) { + // Encoding 0 bytes needs to return at least a single length octet of + // zero, not an empty slice. + p := make([]byte, 0) + encoded := EncodeRDataTXT(p) + if len(encoded) < 0 { + t.Errorf("EncodeRDataTXT(%v) returned %v", p, encoded) + } + + // 255 bytes should be able to be encoded into 256 bytes. + p = make([]byte, 255) + encoded = EncodeRDataTXT(p) + if len(encoded) > 256 { + t.Errorf("EncodeRDataTXT(%d bytes) returned %d bytes", len(p), len(encoded)) + } + + fmt.Println(EncodeRDataTXT(nil)) +} + +func TestRDataTXTRoundTrip(t *testing.T) { + for _, p := range [][]byte{ + {}, + []byte("\x00"), + { + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, + 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, + 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, + 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, + 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f, + 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f, + 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d, 0x8e, 0x8f, + 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0x9b, 0x9c, 0x9d, 0x9e, 0x9f, + 0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xab, 0xac, 0xad, 0xae, 0xaf, + 0xb0, 0xb1, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xbb, 0xbc, 0xbd, 0xbe, 0xbf, + 0xc0, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xcb, 0xcc, 0xcd, 0xce, 0xcf, + 0xd0, 0xd1, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xdb, 0xdc, 0xdd, 0xde, 0xdf, + 0xe0, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xeb, 0xec, 0xed, 0xee, 0xef, + 0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff, + }, + } { + rdata := EncodeRDataTXT(p) + decoded, err := DecodeRDataTXT(rdata) + if err != nil || !bytes.Equal(decoded, p) { + t.Errorf("%+q returned (%+q, %v)", p, decoded, err) + continue + } + } +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/xdns/server.go b/subproject/Xray-core-main/transport/internet/finalmask/xdns/server.go new file mode 100644 index 00000000..ec2f18f9 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/xdns/server.go @@ -0,0 +1,557 @@ +package xdns + +import ( + "bytes" + "context" + "encoding/binary" + go_errors "errors" + "io" + "net" + "sync" + "time" + + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/transport/internet/finalmask" +) + +const ( + idleTimeout = 10 * time.Second + responseTTL = 60 + maxResponseDelay = 1 * time.Second +) + +var ( + maxUDPPayload = 1280 - 40 - 8 + maxEncodedPayload = computeMaxEncodedPayload(maxUDPPayload) +) + +func clientIDToAddr(clientID [8]byte) *net.UDPAddr { + ip := make(net.IP, 16) + + copy(ip, []byte{0xfd, 0x00, 0, 0, 0, 0, 0, 0}) + copy(ip[8:], clientID[:]) + + return &net.UDPAddr{ + IP: ip, + } +} + +type record struct { + Resp *Message + Addr net.Addr + // ClientID [8]byte + ClientAddr net.Addr +} + +type queue struct { + last time.Time + queue chan []byte + stash chan []byte +} + +type xdnsConnServer struct { + net.PacketConn + + domain Name + + ch chan *record + readQueue chan *packet + writeQueueMap map[string]*queue + + closed bool + mutex sync.Mutex +} + +func NewConnServer(c *Config, raw net.PacketConn) (net.PacketConn, error) { + domain, err := ParseName(c.Domain) + if err != nil { + return nil, err + } + + conn := &xdnsConnServer{ + PacketConn: raw, + + domain: domain, + + ch: make(chan *record, 500), + readQueue: make(chan *packet, 512), + writeQueueMap: make(map[string]*queue), + } + + go conn.clean() + go conn.recvLoop() + go conn.sendLoop() + + return conn, nil +} + +func (c *xdnsConnServer) clean() { + f := func() bool { + c.mutex.Lock() + defer c.mutex.Unlock() + + if c.closed { + return true + } + + now := time.Now() + + for key, q := range c.writeQueueMap { + if now.Sub(q.last) >= idleTimeout { + close(q.queue) + close(q.stash) + delete(c.writeQueueMap, key) + } + } + + return false + } + + for { + time.Sleep(idleTimeout / 2) + if f() { + return + } + } +} + +func (c *xdnsConnServer) ensureQueue(addr net.Addr) *queue { + if c.closed { + return nil + } + + q, ok := c.writeQueueMap[addr.String()] + if !ok { + q = &queue{ + queue: make(chan []byte, 512), + stash: make(chan []byte, 1), + } + c.writeQueueMap[addr.String()] = q + } + q.last = time.Now() + + return q +} + +func (c *xdnsConnServer) stash(queue *queue, p []byte) { + c.mutex.Lock() + defer c.mutex.Unlock() + + if c.closed { + return + } + + select { + case queue.stash <- p: + default: + } +} + +func (c *xdnsConnServer) recvLoop() { + var buf [finalmask.UDPSize]byte + + for { + if c.closed { + break + } + + n, addr, err := c.PacketConn.ReadFrom(buf[:]) + if err != nil || n == 0 { + if go_errors.Is(err, net.ErrClosed) || go_errors.Is(err, io.EOF) { + break + } + continue + } + + query, err := MessageFromWireFormat(buf[:n]) + if err != nil { + errors.LogDebug(context.Background(), addr, " xdns from wireformat err ", err) + continue + } + + resp, payload := responseFor(&query, c.domain) + + var clientID [8]byte + n = copy(clientID[:], payload) + payload = payload[n:] + if n == len(clientID) { + r := bytes.NewReader(payload) + for { + p, err := nextPacketServer(r) + if err != nil { + break + } + + buf := make([]byte, len(p)) + copy(buf, p) + select { + case c.readQueue <- &packet{ + p: buf, + addr: clientIDToAddr(clientID), + }: + default: + errors.LogDebug(context.Background(), addr, " ", clientID, " mask read err queue full") + } + } + } else { + if resp != nil && resp.Rcode() == RcodeNoError { + resp.Flags |= RcodeNameError + } + } + + if resp != nil { + select { + case c.ch <- &record{resp, addr, clientIDToAddr(clientID)}: + default: + errors.LogDebug(context.Background(), addr, " ", clientID, " mask read err record queue full") + } + } + } + + errors.LogDebug(context.Background(), "xdns closed") + + close(c.ch) + close(c.readQueue) + + c.mutex.Lock() + defer c.mutex.Unlock() + + c.closed = true + for key, q := range c.writeQueueMap { + close(q.queue) + close(q.stash) + delete(c.writeQueueMap, key) + } +} + +func (c *xdnsConnServer) sendLoop() { + var nextRec *record + for { + rec := nextRec + nextRec = nil + + if rec == nil { + var ok bool + rec, ok = <-c.ch + if !ok { + break + } + } + + if rec.Resp.Rcode() == RcodeNoError && len(rec.Resp.Question) == 1 { + rec.Resp.Answer = []RR{ + { + Name: rec.Resp.Question[0].Name, + Type: rec.Resp.Question[0].Type, + Class: rec.Resp.Question[0].Class, + TTL: responseTTL, + Data: nil, + }, + } + + var payload bytes.Buffer + limit := maxEncodedPayload + timer := time.NewTimer(maxResponseDelay) + + for { + c.mutex.Lock() + q := c.ensureQueue(rec.ClientAddr) + if q == nil { + c.mutex.Unlock() + return + } + c.mutex.Unlock() + + var p []byte + + select { + case p = <-q.stash: + default: + select { + case p = <-q.stash: + case p = <-q.queue: + default: + select { + case p = <-q.stash: + case p = <-q.queue: + case <-timer.C: + case nextRec = <-c.ch: + } + } + } + + timer.Reset(0) + + if len(p) == 0 { + break + } + + limit -= 2 + len(p) + if payload.Len() > 0 && limit < 0 { + c.stash(q, p) + break + } + + // if len(p) > 65535 { + // panic(len(p)) + // } + + _ = binary.Write(&payload, binary.BigEndian, uint16(len(p))) + payload.Write(p) + } + + timer.Stop() + rec.Resp.Answer[0].Data = EncodeRDataTXT(payload.Bytes()) + } + + buf, err := rec.Resp.WireFormat() + if err != nil { + errors.LogDebug(context.Background(), rec.Addr, " ", rec.ClientAddr, " xdns wireformat err ", err) + continue + } + + if len(buf) > maxUDPPayload { + errors.LogDebug(context.Background(), rec.Addr, " ", rec.ClientAddr, " xdns truncate ", len(buf)) + buf = buf[:maxUDPPayload] + buf[2] |= 0x02 + } + + if c.closed { + return + } + + _, err = c.PacketConn.WriteTo(buf, rec.Addr) + if go_errors.Is(err, net.ErrClosed) || go_errors.Is(err, io.ErrClosedPipe) { + c.closed = true + break + } + } +} + +func (c *xdnsConnServer) ReadFrom(p []byte) (n int, addr net.Addr, err error) { + packet, ok := <-c.readQueue + if !ok { + return 0, nil, net.ErrClosed + } + if len(p) < len(packet.p) { + errors.LogDebug(context.Background(), packet.addr, " mask read err short buffer ", len(p), " ", len(packet.p)) + return 0, packet.addr, nil + } + copy(p, packet.p) + return len(packet.p), packet.addr, nil +} + +func (c *xdnsConnServer) WriteTo(p []byte, addr net.Addr) (n int, err error) { + if len(p)+2 > maxEncodedPayload { + errors.LogDebug(context.Background(), addr, " mask write err short write ", len(p), "+2 > ", maxEncodedPayload) + return 0, nil + } + + c.mutex.Lock() + defer c.mutex.Unlock() + + q := c.ensureQueue(addr) + if q == nil { + return 0, io.ErrClosedPipe + } + + buf := make([]byte, len(p)) + copy(buf, p) + + select { + case q.queue <- buf: + return len(p), nil + default: + // errors.LogDebug(context.Background(), addr, " mask write err queue full") + return 0, nil + } +} + +func (c *xdnsConnServer) Close() error { + c.closed = true + return c.PacketConn.Close() +} + +func nextPacketServer(r *bytes.Reader) ([]byte, error) { + eof := func(err error) error { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + return err + } + + for { + prefix, err := r.ReadByte() + if err != nil { + return nil, err + } + if prefix >= 224 { + paddingLen := prefix - 224 + _, err := io.CopyN(io.Discard, r, int64(paddingLen)) + if err != nil { + return nil, eof(err) + } + } else { + p := make([]byte, int(prefix)) + _, err = io.ReadFull(r, p) + return p, eof(err) + } + } +} + +func responseFor(query *Message, domain Name) (*Message, []byte) { + resp := &Message{ + ID: query.ID, + Flags: 0x8000, + Question: query.Question, + } + + if query.Flags&0x8000 != 0 { + return nil, nil + } + + payloadSize := 0 + for _, rr := range query.Additional { + if rr.Type != RRTypeOPT { + continue + } + if len(resp.Additional) != 0 { + resp.Flags |= RcodeFormatError + return resp, nil + } + resp.Additional = append(resp.Additional, RR{ + Name: Name{}, + Type: RRTypeOPT, + Class: 4096, + TTL: 0, + Data: []byte{}, + }) + additional := &resp.Additional[0] + + version := (rr.TTL >> 16) & 0xff + if version != 0 { + resp.Flags |= ExtendedRcodeBadVers & 0xf + additional.TTL = (ExtendedRcodeBadVers >> 4) << 24 + return resp, nil + } + + payloadSize = int(rr.Class) + } + if payloadSize < 512 { + payloadSize = 512 + } + + if len(query.Question) != 1 { + resp.Flags |= RcodeFormatError + return resp, nil + } + question := query.Question[0] + + prefix, ok := question.Name.TrimSuffix(domain) + if !ok { + resp.Flags |= RcodeNameError + return resp, nil + } + resp.Flags |= 0x0400 + + if query.Opcode() != 0 { + resp.Flags |= RcodeNotImplemented + return resp, nil + } + + if question.Type != RRTypeTXT { + resp.Flags |= RcodeNameError + return resp, nil + } + + encoded := bytes.ToUpper(bytes.Join(prefix, nil)) + payload := make([]byte, base32Encoding.DecodedLen(len(encoded))) + n, err := base32Encoding.Decode(payload, encoded) + if err != nil { + resp.Flags |= RcodeNameError + return resp, nil + } + payload = payload[:n] + + if payloadSize < maxUDPPayload { + resp.Flags |= RcodeFormatError + return resp, nil + } + + return resp, payload +} + +func computeMaxEncodedPayload(limit int) int { + maxLengthName, err := NewName([][]byte{ + []byte("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"), + []byte("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"), + []byte("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"), + []byte("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"), + }) + if err != nil { + panic(err) + } + { + n := 0 + for _, label := range maxLengthName { + n += len(label) + 1 + } + n += 1 + if n != 255 { + panic("computeMaxEncodedPayload n != 255") + } + } + + queryLimit := uint16(limit) + if int(queryLimit) != limit { + queryLimit = 0xffff + } + query := &Message{ + Question: []Question{ + { + Name: maxLengthName, + Type: RRTypeTXT, + Class: RRTypeTXT, + }, + }, + + Additional: []RR{ + { + Name: Name{}, + Type: RRTypeOPT, + Class: queryLimit, + TTL: 0, + Data: []byte{}, + }, + }, + } + resp, _ := responseFor(query, [][]byte{}) + + resp.Answer = []RR{ + { + Name: query.Question[0].Name, + Type: query.Question[0].Type, + Class: query.Question[0].Class, + TTL: responseTTL, + Data: nil, + }, + } + + low := 0 + high := 32768 + for low+1 < high { + mid := (low + high) / 2 + resp.Answer[0].Data = EncodeRDataTXT(make([]byte, mid)) + buf, err := resp.WireFormat() + if err != nil { + panic(err) + } + if len(buf) <= limit { + low = mid + } else { + high = mid + } + } + + return low +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/xicmp/client.go b/subproject/Xray-core-main/transport/internet/finalmask/xicmp/client.go new file mode 100644 index 00000000..6ceaf267 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/xicmp/client.go @@ -0,0 +1,354 @@ +package xicmp + +import ( + "context" + "io" + "net" + "strings" + "sync" + "time" + + "github.com/xtls/xray-core/common/crypto" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/transport/internet" + "github.com/xtls/xray-core/transport/internet/finalmask" + "github.com/xtls/xray-core/transport/internet/hysteria/udphop" + "golang.org/x/net/icmp" + "golang.org/x/net/ipv4" + "golang.org/x/net/ipv6" +) + +const ( + initPollDelay = 500 * time.Millisecond + maxPollDelay = 10 * time.Second + pollDelayMultiplier = 2.0 + pollLimit = 16 + windowSize = 1000 +) + +type packet struct { + p []byte + addr net.Addr +} + +type seqStatus struct { + needSeqByte bool + seqByte byte +} + +type xicmpConnClient struct { + conn net.PacketConn + icmpConn *icmp.PacketConn + + typ icmp.Type + id int + seq int + proto int + seqStatus map[int]*seqStatus + + pollChan chan struct{} + readQueue chan *packet + writeQueue chan *packet + + closed bool + mutex sync.Mutex +} + +func NewConnClient(c *Config, raw net.PacketConn, level int) (net.PacketConn, error) { + _, ok1 := raw.(*internet.FakePacketConn) + _, ok2 := raw.(*udphop.UdpHopPacketConn) + if level != 0 || ok1 || ok2 { + return nil, errors.New("xicmp requires being at the outermost level") + } + + network := "ip4:icmp" + typ := icmp.Type(ipv4.ICMPTypeEcho) + proto := 1 + if strings.Contains(c.Ip, ":") { + network = "ip6:ipv6-icmp" + typ = ipv6.ICMPTypeEchoRequest + proto = 58 + } + + icmpConn, err := icmp.ListenPacket(network, c.Ip) + if err != nil { + return nil, errors.New("xicmp listen err").Base(err) + } + + if c.Id == 0 { + c.Id = int32(crypto.RandBetween(0, 65535)) + } + + conn := &xicmpConnClient{ + conn: raw, + icmpConn: icmpConn, + + typ: typ, + id: int(c.Id), + seq: 1, + proto: proto, + seqStatus: make(map[int]*seqStatus), + + pollChan: make(chan struct{}, pollLimit), + readQueue: make(chan *packet, 256), + writeQueue: make(chan *packet, 256), + } + + go conn.recvLoop() + go conn.sendLoop() + + return conn, nil +} + +func (c *xicmpConnClient) encode(p []byte) ([]byte, error) { + c.mutex.Lock() + defer c.mutex.Unlock() + + needSeqByte := false + var seqByte byte + data := p + if len(p) > 0 { + needSeqByte = true + seqByte = p[0] + } + + msg := icmp.Message{ + Type: c.typ, + Code: 0, + Body: &icmp.Echo{ + ID: c.id, + Seq: c.seq, + Data: data, + }, + } + + buf, err := msg.Marshal(nil) + if err != nil { + return nil, err + } + + if len(buf) > finalmask.UDPSize { + return nil, errors.New("xicmp len(buf) > finalmask.UDPSize") + } + + c.seqStatus[c.seq] = &seqStatus{ + needSeqByte: needSeqByte, + seqByte: seqByte, + } + + delete(c.seqStatus, int(uint16(c.seq-windowSize))) + + c.seq++ + + if c.seq == 65536 { + delete(c.seqStatus, int(uint16(c.seq-windowSize))) + c.seq = 1 + } + + return buf, nil +} + +func (c *xicmpConnClient) recvLoop() { + var buf [finalmask.UDPSize]byte + + for { + if c.closed { + break + } + + n, addr, err := c.icmpConn.ReadFrom(buf[:]) + if err != nil { + continue + } + + msg, err := icmp.ParseMessage(c.proto, buf[:n]) + if err != nil { + continue + } + + if msg.Type != ipv4.ICMPTypeEchoReply && msg.Type != ipv6.ICMPTypeEchoReply { + continue + } + + echo, ok := msg.Body.(*icmp.Echo) + if !ok { + continue + } + + c.mutex.Lock() + seqStatus, ok := c.seqStatus[echo.Seq] + c.mutex.Unlock() + + if !ok { + continue + } + + if seqStatus.needSeqByte { + if len(echo.Data) <= 1 { + continue + } + if echo.Data[0] == seqStatus.seqByte { + continue + } + echo.Data = echo.Data[1:] + } + + if len(echo.Data) > 0 { + c.mutex.Lock() + delete(c.seqStatus, echo.Seq) + c.mutex.Unlock() + + buf := make([]byte, len(echo.Data)) + copy(buf, echo.Data) + select { + case c.readQueue <- &packet{ + p: buf, + addr: &net.UDPAddr{IP: addr.(*net.IPAddr).IP}, + }: + default: + errors.LogDebug(context.Background(), addr, " ", echo.Seq, " ", echo.ID, " mask read err queue full") + } + + select { + case c.pollChan <- struct{}{}: + default: + } + } + } + + errors.LogDebug(context.Background(), "xicmp closed") + + close(c.pollChan) + close(c.readQueue) + + c.mutex.Lock() + defer c.mutex.Unlock() + + c.closed = true + close(c.writeQueue) +} + +func (c *xicmpConnClient) sendLoop() { + var addr net.Addr + + pollDelay := initPollDelay + pollTimer := time.NewTimer(pollDelay) + for { + var p *packet + pollTimerExpired := false + + select { + case p = <-c.writeQueue: + default: + select { + case p = <-c.writeQueue: + case <-c.pollChan: + case <-pollTimer.C: + pollTimerExpired = true + } + } + + if p != nil { + addr = p.addr + + select { + case <-c.pollChan: + default: + } + } else if addr != nil { + encoded, _ := c.encode(nil) + p = &packet{ + p: encoded, + addr: addr, + } + } + + if pollTimerExpired { + pollDelay = time.Duration(float64(pollDelay) * pollDelayMultiplier) + if pollDelay > maxPollDelay { + pollDelay = maxPollDelay + } + } else { + if !pollTimer.Stop() { + <-pollTimer.C + } + pollDelay = initPollDelay + } + pollTimer.Reset(pollDelay) + + if c.closed { + return + } + + if p != nil { + _, err := c.icmpConn.WriteTo(p.p, p.addr) + if err != nil { + errors.LogDebug(context.Background(), p.addr, " xicmp writeto err ", err) + } + } + } +} + +func (c *xicmpConnClient) ReadFrom(p []byte) (n int, addr net.Addr, err error) { + packet, ok := <-c.readQueue + if !ok { + return 0, nil, net.ErrClosed + } + if len(p) < len(packet.p) { + errors.LogDebug(context.Background(), packet.addr, " mask read err short buffer ", len(p), " ", len(packet.p)) + return 0, packet.addr, nil + } + copy(p, packet.p) + return len(packet.p), packet.addr, nil +} + +func (c *xicmpConnClient) WriteTo(p []byte, addr net.Addr) (n int, err error) { + encoded, err := c.encode(p) + if err != nil { + errors.LogDebug(context.Background(), addr, " xicmp wireformat err ", err) + return 0, nil + } + + c.mutex.Lock() + defer c.mutex.Unlock() + + if c.closed { + return 0, io.ErrClosedPipe + } + + select { + case c.writeQueue <- &packet{ + p: encoded, + addr: &net.IPAddr{IP: addr.(*net.UDPAddr).IP}, + }: + return len(p), nil + default: + errors.LogDebug(context.Background(), addr, " mask write err queue full") + return 0, nil + } +} + +func (c *xicmpConnClient) Close() error { + c.closed = true + _ = c.icmpConn.Close() + return c.conn.Close() +} + +func (c *xicmpConnClient) LocalAddr() net.Addr { + return &net.UDPAddr{ + IP: c.icmpConn.LocalAddr().(*net.IPAddr).IP, + Port: c.id, + } +} + +func (c *xicmpConnClient) SetDeadline(t time.Time) error { + return c.icmpConn.SetDeadline(t) +} + +func (c *xicmpConnClient) SetReadDeadline(t time.Time) error { + return c.icmpConn.SetReadDeadline(t) +} + +func (c *xicmpConnClient) SetWriteDeadline(t time.Time) error { + return c.icmpConn.SetWriteDeadline(t) +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/xicmp/config.go b/subproject/Xray-core-main/transport/internet/finalmask/xicmp/config.go new file mode 100644 index 00000000..c570ce96 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/xicmp/config.go @@ -0,0 +1,16 @@ +package xicmp + +import ( + "net" +) + +func (c *Config) UDP() { +} + +func (c *Config) WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) { + return NewConnClient(c, raw, level) +} + +func (c *Config) WrapPacketConnServer(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) { + return NewConnServer(c, raw, level) +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/xicmp/config.pb.go b/subproject/Xray-core-main/transport/internet/finalmask/xicmp/config.pb.go new file mode 100644 index 00000000..be75dfe5 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/xicmp/config.pb.go @@ -0,0 +1,132 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: transport/internet/finalmask/xicmp/config.proto + +package xicmp + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + Ip string `protobuf:"bytes,1,opt,name=ip,proto3" json:"ip,omitempty"` + Id int32 `protobuf:"varint,2,opt,name=id,proto3" json:"id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_transport_internet_finalmask_xicmp_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_finalmask_xicmp_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_transport_internet_finalmask_xicmp_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Config) GetIp() string { + if x != nil { + return x.Ip + } + return "" +} + +func (x *Config) GetId() int32 { + if x != nil { + return x.Id + } + return 0 +} + +var File_transport_internet_finalmask_xicmp_config_proto protoreflect.FileDescriptor + +const file_transport_internet_finalmask_xicmp_config_proto_rawDesc = "" + + "\n" + + "/transport/internet/finalmask/xicmp/config.proto\x12'xray.transport.internet.finalmask.xicmp\"(\n" + + "\x06Config\x12\x0e\n" + + "\x02ip\x18\x01 \x01(\tR\x02ip\x12\x0e\n" + + "\x02id\x18\x02 \x01(\x05R\x02idB\x97\x01\n" + + "+com.xray.transport.internet.finalmask.xicmpP\x01Z= idleTimeout { + close(q.queue) + delete(c.writeQueueMap, key) + } + } + + return false + } + + for { + time.Sleep(idleTimeout / 2) + if f() { + return + } + } +} + +func (c *xicmpConnServer) ensureQueue(addr net.Addr) *queue { + if c.closed { + return nil + } + + q, ok := c.writeQueueMap[addr.String()] + if !ok { + q = &queue{ + queue: make(chan []byte, 512), + } + c.writeQueueMap[addr.String()] = q + } + q.last = time.Now() + + return q +} + +func (c *xicmpConnServer) encode(p []byte, id int, seq int, needSeqByte bool, seqByte byte) ([]byte, error) { + data := p + if needSeqByte { + b2 := c.randUntil(seqByte) + data = append([]byte{b2}, p...) + } + + msg := icmp.Message{ + Type: c.typ, + Code: 0, + Body: &icmp.Echo{ + ID: id, + Seq: seq, + Data: data, + }, + } + + buf, err := msg.Marshal(nil) + if err != nil { + return nil, err + } + + if len(buf) > finalmask.UDPSize { + return nil, errors.New("xicmp len(buf) > finalmask.UDPSize") + } + + return buf, nil +} + +func (c *xicmpConnServer) randUntil(b1 byte) byte { + b2 := byte(crypto.RandBetween(0, 255)) + for { + if b2 != b1 { + return b2 + } + b2 = byte(crypto.RandBetween(0, 255)) + } +} + +func (c *xicmpConnServer) recvLoop() { + var buf [finalmask.UDPSize]byte + + for { + if c.closed { + break + } + + n, addr, err := c.icmpConn.ReadFrom(buf[:]) + if err != nil { + continue + } + + msg, err := icmp.ParseMessage(c.proto, buf[:n]) + if err != nil { + continue + } + + if msg.Type != ipv4.ICMPTypeEcho && msg.Type != ipv6.ICMPTypeEchoRequest { + continue + } + + echo, ok := msg.Body.(*icmp.Echo) + if !ok { + continue + } + + if c.config.Id != 0 && echo.ID != int(c.config.Id) { + continue + } + + needSeqByte := false + var seqByte byte + + if len(echo.Data) > 0 { + needSeqByte = true + seqByte = echo.Data[0] + + buf := make([]byte, len(echo.Data)) + copy(buf, echo.Data) + select { + case c.readQueue <- &packet{ + p: buf, + addr: &net.UDPAddr{ + IP: addr.(*net.IPAddr).IP, + Port: echo.ID, + }, + }: + default: + errors.LogDebug(context.Background(), addr, " ", echo.ID, " ", echo.Seq, " mask read err queue full") + } + } + + select { + case c.ch <- &record{ + id: echo.ID, + seq: echo.Seq, + needSeqByte: needSeqByte, + seqByte: seqByte, + addr: &net.UDPAddr{ + IP: addr.(*net.IPAddr).IP, + Port: echo.ID, + }, + }: + default: + errors.LogDebug(context.Background(), addr, " ", echo.ID, " ", echo.Seq, " mask read err record queue full") + } + } + + errors.LogDebug(context.Background(), "xicmp closed") + + close(c.ch) + close(c.readQueue) + + c.mutex.Lock() + defer c.mutex.Unlock() + + c.closed = true + for key, q := range c.writeQueueMap { + close(q.queue) + delete(c.writeQueueMap, key) + } +} + +func (c *xicmpConnServer) sendLoop() { + var nextRec *record + for { + rec := nextRec + nextRec = nil + + if rec == nil { + var ok bool + rec, ok = <-c.ch + if !ok { + break + } + } + + c.mutex.Lock() + q := c.ensureQueue(rec.addr) + if q == nil { + c.mutex.Unlock() + return + } + c.mutex.Unlock() + + var p []byte + + timer := time.NewTimer(maxResponseDelay) + + select { + case p = <-q.queue: + default: + select { + case p = <-q.queue: + case <-timer.C: + case nextRec = <-c.ch: + } + } + + timer.Stop() + + if len(p) == 0 { + continue + } + + buf, err := c.encode(p, rec.id, rec.seq, rec.needSeqByte, rec.seqByte) + if err != nil { + errors.LogDebug(context.Background(), rec.addr, " ", rec.id, " ", rec.seq, " xicmp wireformat err ", err) + continue + } + + if c.closed { + return + } + + _, err = c.icmpConn.WriteTo(buf, &net.IPAddr{IP: rec.addr.(*net.UDPAddr).IP}) + if err != nil { + errors.LogDebug(context.Background(), rec.addr, " ", rec.id, " ", rec.seq, " xicmp writeto err ", err) + } + } +} + +func (c *xicmpConnServer) ReadFrom(p []byte) (n int, addr net.Addr, err error) { + packet, ok := <-c.readQueue + if !ok { + return 0, nil, net.ErrClosed + } + if len(p) < len(packet.p) { + errors.LogDebug(context.Background(), packet.addr, " mask read err short buffer ", len(p), " ", len(packet.p)) + return 0, packet.addr, nil + } + copy(p, packet.p) + return len(packet.p), packet.addr, nil +} + +func (c *xicmpConnServer) WriteTo(p []byte, addr net.Addr) (n int, err error) { + if len(p)+8+1 > finalmask.UDPSize { + errors.LogDebug(context.Background(), addr, " mask write err short write ", len(p), "+8+1 > ", finalmask.UDPSize) + return 0, nil + } + + c.mutex.Lock() + defer c.mutex.Unlock() + + q := c.ensureQueue(addr) + if q == nil { + return 0, io.ErrClosedPipe + } + + buf := make([]byte, len(p)) + copy(buf, p) + + select { + case q.queue <- buf: + return len(p), nil + default: + // errors.LogDebug(context.Background(), addr, " mask write err queue full") + return 0, nil + } +} + +func (c *xicmpConnServer) Close() error { + c.closed = true + _ = c.icmpConn.Close() + return c.conn.Close() +} + +func (c *xicmpConnServer) LocalAddr() net.Addr { + return &net.UDPAddr{IP: c.icmpConn.LocalAddr().(*net.IPAddr).IP} +} + +func (c *xicmpConnServer) SetDeadline(t time.Time) error { + return c.icmpConn.SetDeadline(t) +} + +func (c *xicmpConnServer) SetReadDeadline(t time.Time) error { + return c.icmpConn.SetReadDeadline(t) +} + +func (c *xicmpConnServer) SetWriteDeadline(t time.Time) error { + return c.icmpConn.SetWriteDeadline(t) +} diff --git a/subproject/Xray-core-main/transport/internet/finalmask/xicmp/xicmp_test.go b/subproject/Xray-core-main/transport/internet/finalmask/xicmp/xicmp_test.go new file mode 100644 index 00000000..1ac92181 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/finalmask/xicmp/xicmp_test.go @@ -0,0 +1,74 @@ +package xicmp_test + +import ( + "bytes" + "fmt" + "testing" + + "golang.org/x/net/icmp" + "golang.org/x/net/ipv4" + "golang.org/x/net/ipv6" +) + +func TestICMPEchoMarshal(t *testing.T) { + msg := icmp.Message{ + Type: ipv4.ICMPTypeEcho, + Code: 0, + Body: &icmp.Echo{ + ID: 65535, + Seq: 65537, + Data: nil, + }, + } + ICMPTypeEcho, _ := msg.Marshal(nil) + fmt.Println("ICMPTypeEcho", len(ICMPTypeEcho), ICMPTypeEcho) + + msg = icmp.Message{ + Type: ipv4.ICMPTypeEchoReply, + Code: 0, + Body: &icmp.Echo{ + ID: 65535, + Seq: 65537, + Data: nil, + }, + } + ICMPTypeEchoReply, _ := msg.Marshal(nil) + fmt.Println("ICMPTypeEchoReply", len(ICMPTypeEchoReply), ICMPTypeEchoReply) + + msg = icmp.Message{ + Type: ipv6.ICMPTypeEchoRequest, + Code: 0, + Body: &icmp.Echo{ + ID: 65535, + Seq: 65537, + Data: nil, + }, + } + ICMPTypeEchoRequest, _ := msg.Marshal(nil) + fmt.Println("ICMPTypeEchoRequest", len(ICMPTypeEchoRequest), ICMPTypeEchoRequest) + + msg = icmp.Message{ + Type: ipv6.ICMPTypeEchoReply, + Code: 0, + Body: &icmp.Echo{ + ID: 65535, + Seq: 65537, + Data: nil, + }, + } + V6ICMPTypeEchoReply, _ := msg.Marshal(nil) + fmt.Println("V6ICMPTypeEchoReply", len(V6ICMPTypeEchoReply), V6ICMPTypeEchoReply) + + if !bytes.Equal(ICMPTypeEcho[0:2], []byte{8, 0}) || !bytes.Equal(ICMPTypeEcho[4:], []byte{255, 255, 0, 1}) { + t.Fatalf("ICMPTypeEcho Type/Code or ID/Seq mismatch: %v", ICMPTypeEcho) + } + if !bytes.Equal(ICMPTypeEchoReply[0:2], []byte{0, 0}) || !bytes.Equal(ICMPTypeEchoReply[4:], []byte{255, 255, 0, 1}) { + t.Fatalf("ICMPTypeEchoReply Type/Code or ID/Seq mismatch: %v", ICMPTypeEchoReply) + } + if !bytes.Equal(ICMPTypeEchoRequest[0:2], []byte{128, 0}) || !bytes.Equal(ICMPTypeEchoRequest[4:], []byte{255, 255, 0, 1}) { + t.Fatalf("ICMPTypeEchoRequest Type/Code or ID/Seq mismatch: %v", ICMPTypeEchoRequest) + } + if !bytes.Equal(V6ICMPTypeEchoReply[0:2], []byte{129, 0}) || !bytes.Equal(V6ICMPTypeEchoReply[4:], []byte{255, 255, 0, 1}) { + t.Fatalf("V6ICMPTypeEchoReply Type/Code or ID/Seq mismatch: %v", V6ICMPTypeEchoReply) + } +} diff --git a/subproject/Xray-core-main/transport/internet/grpc/config.go b/subproject/Xray-core-main/transport/internet/grpc/config.go new file mode 100644 index 00000000..c9903018 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/grpc/config.go @@ -0,0 +1,59 @@ +package grpc + +import ( + "net/url" + "strings" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/transport/internet" +) + +func init() { + common.Must(internet.RegisterProtocolConfigCreator(protocolName, func() interface{} { + return new(Config) + })) +} + +func (c *Config) getServiceName() string { + // Normal old school config + if !strings.HasPrefix(c.ServiceName, "/") { + return url.PathEscape(c.ServiceName) + } + + // Otherwise new custom paths + lastIndex := strings.LastIndex(c.ServiceName, "/") + if lastIndex < 1 { + lastIndex = 1 + } + rawServiceName := c.ServiceName[1:lastIndex] // trim from first to last '/' + serviceNameParts := strings.Split(rawServiceName, "/") + for i := range serviceNameParts { + serviceNameParts[i] = url.PathEscape(serviceNameParts[i]) + } + return strings.Join(serviceNameParts, "/") +} + +func (c *Config) getTunStreamName() string { + // Normal old school config + if !strings.HasPrefix(c.ServiceName, "/") { + return "Tun" + } + // Otherwise new custom paths + endingPath := c.ServiceName[strings.LastIndex(c.ServiceName, "/")+1:] // from the last '/' to end of string + return url.PathEscape(strings.Split(endingPath, "|")[0]) +} + +func (c *Config) getTunMultiStreamName() string { + // Normal old school config + if !strings.HasPrefix(c.ServiceName, "/") { + return "TunMulti" + } + // Otherwise new custom paths + endingPath := c.ServiceName[strings.LastIndex(c.ServiceName, "/")+1:] // from the last '/' to end of string + streamNames := strings.Split(endingPath, "|") + if len(streamNames) == 1 { // client side. Service name is the full path to multi tun + return url.PathEscape(streamNames[0]) + } else { // server side. The second part is the path to multi tun + return url.PathEscape(streamNames[1]) + } +} diff --git a/subproject/Xray-core-main/transport/internet/grpc/config.pb.go b/subproject/Xray-core-main/transport/internet/grpc/config.pb.go new file mode 100644 index 00000000..f9b994c6 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/grpc/config.pb.go @@ -0,0 +1,187 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: transport/internet/grpc/config.proto + +package grpc + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + Authority string `protobuf:"bytes,1,opt,name=authority,proto3" json:"authority,omitempty"` + ServiceName string `protobuf:"bytes,2,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` + MultiMode bool `protobuf:"varint,3,opt,name=multi_mode,json=multiMode,proto3" json:"multi_mode,omitempty"` + IdleTimeout int32 `protobuf:"varint,4,opt,name=idle_timeout,json=idleTimeout,proto3" json:"idle_timeout,omitempty"` + HealthCheckTimeout int32 `protobuf:"varint,5,opt,name=health_check_timeout,json=healthCheckTimeout,proto3" json:"health_check_timeout,omitempty"` + PermitWithoutStream bool `protobuf:"varint,6,opt,name=permit_without_stream,json=permitWithoutStream,proto3" json:"permit_without_stream,omitempty"` + InitialWindowsSize int32 `protobuf:"varint,7,opt,name=initial_windows_size,json=initialWindowsSize,proto3" json:"initial_windows_size,omitempty"` + UserAgent string `protobuf:"bytes,8,opt,name=user_agent,json=userAgent,proto3" json:"user_agent,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_transport_internet_grpc_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_grpc_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_transport_internet_grpc_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Config) GetAuthority() string { + if x != nil { + return x.Authority + } + return "" +} + +func (x *Config) GetServiceName() string { + if x != nil { + return x.ServiceName + } + return "" +} + +func (x *Config) GetMultiMode() bool { + if x != nil { + return x.MultiMode + } + return false +} + +func (x *Config) GetIdleTimeout() int32 { + if x != nil { + return x.IdleTimeout + } + return 0 +} + +func (x *Config) GetHealthCheckTimeout() int32 { + if x != nil { + return x.HealthCheckTimeout + } + return 0 +} + +func (x *Config) GetPermitWithoutStream() bool { + if x != nil { + return x.PermitWithoutStream + } + return false +} + +func (x *Config) GetInitialWindowsSize() int32 { + if x != nil { + return x.InitialWindowsSize + } + return 0 +} + +func (x *Config) GetUserAgent() string { + if x != nil { + return x.UserAgent + } + return "" +} + +var File_transport_internet_grpc_config_proto protoreflect.FileDescriptor + +const file_transport_internet_grpc_config_proto_rawDesc = "" + + "\n" + + "$transport/internet/grpc/config.proto\x12%xray.transport.internet.grpc.encoding\"\xc2\x02\n" + + "\x06Config\x12\x1c\n" + + "\tauthority\x18\x01 \x01(\tR\tauthority\x12!\n" + + "\fservice_name\x18\x02 \x01(\tR\vserviceName\x12\x1d\n" + + "\n" + + "multi_mode\x18\x03 \x01(\bR\tmultiMode\x12!\n" + + "\fidle_timeout\x18\x04 \x01(\x05R\vidleTimeout\x120\n" + + "\x14health_check_timeout\x18\x05 \x01(\x05R\x12healthCheckTimeout\x122\n" + + "\x15permit_without_stream\x18\x06 \x01(\bR\x13permitWithoutStream\x120\n" + + "\x14initial_windows_size\x18\a \x01(\x05R\x12initialWindowsSize\x12\x1d\n" + + "\n" + + "user_agent\x18\b \x01(\tR\tuserAgentB3Z1github.com/xtls/xray-core/transport/internet/grpcb\x06proto3" + +var ( + file_transport_internet_grpc_config_proto_rawDescOnce sync.Once + file_transport_internet_grpc_config_proto_rawDescData []byte +) + +func file_transport_internet_grpc_config_proto_rawDescGZIP() []byte { + file_transport_internet_grpc_config_proto_rawDescOnce.Do(func() { + file_transport_internet_grpc_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_transport_internet_grpc_config_proto_rawDesc), len(file_transport_internet_grpc_config_proto_rawDesc))) + }) + return file_transport_internet_grpc_config_proto_rawDescData +} + +var file_transport_internet_grpc_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_transport_internet_grpc_config_proto_goTypes = []any{ + (*Config)(nil), // 0: xray.transport.internet.grpc.encoding.Config +} +var file_transport_internet_grpc_config_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_transport_internet_grpc_config_proto_init() } +func file_transport_internet_grpc_config_proto_init() { + if File_transport_internet_grpc_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_grpc_config_proto_rawDesc), len(file_transport_internet_grpc_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_transport_internet_grpc_config_proto_goTypes, + DependencyIndexes: file_transport_internet_grpc_config_proto_depIdxs, + MessageInfos: file_transport_internet_grpc_config_proto_msgTypes, + }.Build() + File_transport_internet_grpc_config_proto = out.File + file_transport_internet_grpc_config_proto_goTypes = nil + file_transport_internet_grpc_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/transport/internet/grpc/config.proto b/subproject/Xray-core-main/transport/internet/grpc/config.proto new file mode 100644 index 00000000..fcaa2ed9 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/grpc/config.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; + +package xray.transport.internet.grpc.encoding; +option go_package = "github.com/xtls/xray-core/transport/internet/grpc"; + +message Config { + string authority = 1; + string service_name = 2; + bool multi_mode = 3; + int32 idle_timeout = 4; + int32 health_check_timeout = 5; + bool permit_without_stream = 6; + int32 initial_windows_size = 7; + string user_agent = 8; +} diff --git a/subproject/Xray-core-main/transport/internet/grpc/config_test.go b/subproject/Xray-core-main/transport/internet/grpc/config_test.go new file mode 100644 index 00000000..04a995a4 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/grpc/config_test.go @@ -0,0 +1,129 @@ +package grpc + +import ( + "reflect" + "testing" + + "github.com/stretchr/testify/assert" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +func TestConfig_GetServiceName(t *testing.T) { + tests := []struct { + TestName string + ServiceName string + Expected string + }{ + { + TestName: "simple no absolute path", + ServiceName: "hello", + Expected: "hello", + }, + { + TestName: "escape no absolute path", + ServiceName: "hello/world!", + Expected: "hello%2Fworld%21", + }, + { + TestName: "absolute path", + ServiceName: "/my/sample/path/a|b", + Expected: "my/sample/path", + }, + { + TestName: "escape absolute path", + ServiceName: "/hello /world!/a|b", + Expected: "hello%20/world%21", + }, + { + TestName: "path with only one '/'", + ServiceName: "/foo", + Expected: "", + }, + } + for _, test := range tests { + t.Run(test.TestName, func(t *testing.T) { + config := Config{ServiceName: test.ServiceName} + assert.Equal(t, test.Expected, config.getServiceName()) + }) + } +} + +func TestConfig_GetTunStreamName(t *testing.T) { + tests := []struct { + TestName string + ServiceName string + Expected string + }{ + { + TestName: "no absolute path", + ServiceName: "hello", + Expected: "Tun", + }, + { + TestName: "absolute path server", + ServiceName: "/my/sample/path/tun_service|multi_service", + Expected: "tun_service", + }, + { + TestName: "absolute path client", + ServiceName: "/my/sample/path/tun_service", + Expected: "tun_service", + }, + { + TestName: "escape absolute path client", + ServiceName: "/m y/sa !mple/pa\\th/tun\\_serv!ice", + Expected: "tun%5C_serv%21ice", + }, + } + for _, test := range tests { + t.Run(test.TestName, func(t *testing.T) { + config := Config{ServiceName: test.ServiceName} + assert.Equal(t, test.Expected, config.getTunStreamName()) + }) + } +} + +func TestConfig_GetTunMultiStreamName(t *testing.T) { + tests := []struct { + TestName string + ServiceName string + Expected string + }{ + { + TestName: "no absolute path", + ServiceName: "hello", + Expected: "TunMulti", + }, + { + TestName: "absolute path server", + ServiceName: "/my/sample/path/tun_service|multi_service", + Expected: "multi_service", + }, + { + TestName: "absolute path client", + ServiceName: "/my/sample/path/multi_service", + Expected: "multi_service", + }, + { + TestName: "escape absolute path client", + ServiceName: "/m y/sa !mple/pa\\th/mu%lti\\_serv!ice", + Expected: "mu%25lti%5C_serv%21ice", + }, + } + for _, test := range tests { + t.Run(test.TestName, func(t *testing.T) { + config := Config{ServiceName: test.ServiceName} + assert.Equal(t, test.Expected, config.getTunMultiStreamName()) + }) + } +} + +func TestSetUserAgent(t *testing.T) { + ua := "Test/1.0" + conn, err := grpc.NewClient("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithUserAgent(ua)) + assert.NoError(t, err) + defer conn.Close() + setUserAgent(conn, ua) + assert.Equal(t, ua, reflect.ValueOf(conn).Elem().FieldByName("dopts").FieldByName("copts").FieldByName("UserAgent").String()) +} diff --git a/subproject/Xray-core-main/transport/internet/grpc/dial.go b/subproject/Xray-core-main/transport/internet/grpc/dial.go new file mode 100644 index 00000000..c8b8423c --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/grpc/dial.go @@ -0,0 +1,218 @@ +package grpc + +import ( + "context" + "reflect" + "sync" + "time" + + "github.com/xtls/xray-core/common" + c "github.com/xtls/xray-core/common/ctx" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/utils" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/transport/internet" + "github.com/xtls/xray-core/transport/internet/grpc/encoding" + "github.com/xtls/xray-core/transport/internet/reality" + "github.com/xtls/xray-core/transport/internet/stat" + "github.com/xtls/xray-core/transport/internet/tls" + "google.golang.org/grpc" + "google.golang.org/grpc/backoff" + "google.golang.org/grpc/connectivity" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/keepalive" +) + +func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.MemoryStreamConfig) (stat.Connection, error) { + errors.LogInfo(ctx, "creating connection to ", dest) + + conn, err := dialgRPC(ctx, dest, streamSettings) + if err != nil { + return nil, errors.New("failed to dial gRPC").Base(err) + } + return stat.Connection(conn), nil +} + +func init() { + common.Must(internet.RegisterTransportDialer(protocolName, Dial)) +} + +type dialerConf struct { + net.Destination + *internet.MemoryStreamConfig +} + +var ( + globalDialerMap map[dialerConf]*grpc.ClientConn + globalDialerAccess sync.Mutex +) + +func dialgRPC(ctx context.Context, dest net.Destination, streamSettings *internet.MemoryStreamConfig) (net.Conn, error) { + grpcSettings := streamSettings.ProtocolSettings.(*Config) + + conn, err := getGrpcClient(ctx, dest, streamSettings) + if err != nil { + return nil, errors.New("Cannot dial gRPC").Base(err) + } + client := encoding.NewGRPCServiceClient(conn) + if grpcSettings.MultiMode { + errors.LogDebug(ctx, "using gRPC multi mode service name: `"+grpcSettings.getServiceName()+"` stream name: `"+grpcSettings.getTunMultiStreamName()+"`") + grpcService, err := client.(encoding.GRPCServiceClientX).TunMultiCustomName(ctx, grpcSettings.getServiceName(), grpcSettings.getTunMultiStreamName()) + if err != nil { + return nil, errors.New("Cannot dial gRPC").Base(err) + } + return encoding.NewMultiHunkConn(grpcService, nil), nil + } + + errors.LogDebug(ctx, "using gRPC tun mode service name: `"+grpcSettings.getServiceName()+"` stream name: `"+grpcSettings.getTunStreamName()+"`") + grpcService, err := client.(encoding.GRPCServiceClientX).TunCustomName(ctx, grpcSettings.getServiceName(), grpcSettings.getTunStreamName()) + if err != nil { + return nil, errors.New("Cannot dial gRPC").Base(err) + } + + return encoding.NewHunkConn(grpcService, nil), nil +} + +func getGrpcClient(ctx context.Context, dest net.Destination, streamSettings *internet.MemoryStreamConfig) (*grpc.ClientConn, error) { + globalDialerAccess.Lock() + defer globalDialerAccess.Unlock() + + if globalDialerMap == nil { + globalDialerMap = make(map[dialerConf]*grpc.ClientConn) + } + tlsConfig := tls.ConfigFromStreamSettings(streamSettings) + realityConfig := reality.ConfigFromStreamSettings(streamSettings) + sockopt := streamSettings.SocketSettings + grpcSettings := streamSettings.ProtocolSettings.(*Config) + + if client, found := globalDialerMap[dialerConf{dest, streamSettings}]; found && client.GetState() != connectivity.Shutdown { + return client, nil + } + + dialOptions := []grpc.DialOption{ + grpc.WithConnectParams(grpc.ConnectParams{ + Backoff: backoff.Config{ + BaseDelay: 500 * time.Millisecond, + Multiplier: 1.5, + Jitter: 0.2, + MaxDelay: 19 * time.Second, + }, + MinConnectTimeout: 5 * time.Second, + }), + grpc.WithContextDialer(func(gctx context.Context, s string) (net.Conn, error) { + select { + case <-gctx.Done(): + return nil, gctx.Err() + default: + } + + rawHost, rawPort, err := net.SplitHostPort(s) + if err != nil { + return nil, err + } + if len(rawPort) == 0 { + rawPort = "443" + } + port, err := net.PortFromString(rawPort) + if err != nil { + return nil, err + } + address := net.ParseAddress(rawHost) + + gctx = c.ContextWithID(gctx, c.IDFromContext(ctx)) + gctx = session.ContextWithOutbounds(gctx, session.OutboundsFromContext(ctx)) + gctx = session.ContextWithTimeoutOnly(gctx, true) + + c, err := internet.DialSystem(gctx, net.TCPDestination(address, port), sockopt) + if err == nil { + if streamSettings.TcpmaskManager != nil { + newConn, err := streamSettings.TcpmaskManager.WrapConnClient(c) + if err != nil { + c.Close() + return nil, errors.New("mask err").Base(err) + } + c = newConn + } + + if tlsConfig != nil { + config := tlsConfig.GetTLSConfig() + if config.ServerName == "" && address.Family().IsDomain() { + config.ServerName = address.Domain() + } + if fingerprint := tls.GetFingerprint(tlsConfig.Fingerprint); fingerprint != nil { + return tls.UClient(c, config, fingerprint), nil + } else { // Fallback to normal gRPC TLS + return tls.Client(c, config), nil + } + } + if realityConfig != nil { + return reality.UClient(c, realityConfig, gctx, dest) + } + } + return c, err + }), + } + + dialOptions = append(dialOptions, grpc.WithTransportCredentials(insecure.NewCredentials())) + + authority := "" + if grpcSettings.Authority != "" { + authority = grpcSettings.Authority + } else if tlsConfig != nil && tlsConfig.ServerName != "" { + authority = tlsConfig.ServerName + } else if realityConfig == nil && dest.Address.Family().IsDomain() { + authority = dest.Address.Domain() + } + dialOptions = append(dialOptions, grpc.WithAuthority(authority)) + + if grpcSettings.IdleTimeout > 0 || grpcSettings.HealthCheckTimeout > 0 || grpcSettings.PermitWithoutStream { + dialOptions = append(dialOptions, grpc.WithKeepaliveParams(keepalive.ClientParameters{ + Time: time.Second * time.Duration(grpcSettings.IdleTimeout), + Timeout: time.Second * time.Duration(grpcSettings.HealthCheckTimeout), + PermitWithoutStream: grpcSettings.PermitWithoutStream, + })) + } + + if grpcSettings.InitialWindowsSize > 0 { + dialOptions = append(dialOptions, grpc.WithInitialWindowSize(grpcSettings.InitialWindowsSize)) + } + + var grpcDestHost string + if dest.Address.Family().IsDomain() { + grpcDestHost = dest.Address.Domain() + } else { + grpcDestHost = dest.Address.IP().String() + } + + conn, err := grpc.NewClient( + "passthrough:///"+net.JoinHostPort(grpcDestHost, dest.Port.String()), + dialOptions..., + ) + if err == nil { + userAgent := grpcSettings.UserAgent + // It's NOT recommended to set the UA of gRPC connections to that of real browsers, as they are fundamentally incapable of initiating real gRPC connections. + switch userAgent { + case "chrome", "": + userAgent = utils.ChromeUA + case "firefox": + userAgent = utils.FirefoxUA + case "edge": + userAgent = utils.MSEdgeUA + case "golang": + userAgent = "" + } + setUserAgent(conn, userAgent) + conn.Connect() + } + globalDialerMap[dialerConf{dest, streamSettings}] = conn + return conn, err +} + +// setUserAgent overrides the user-agent on a ClientConn to remove the +// "grpc-go/version" suffix that grpc.WithUserAgent unconditionally appends. +func setUserAgent(conn *grpc.ClientConn, ua string) { + if f := reflect.ValueOf(conn).Elem().FieldByName("dopts").FieldByName("copts").FieldByName("UserAgent"); f.IsValid() { + *(*string)(f.Addr().UnsafePointer()) = ua + } +} diff --git a/subproject/Xray-core-main/transport/internet/grpc/encoding/customSeviceName.go b/subproject/Xray-core-main/transport/internet/grpc/encoding/customSeviceName.go new file mode 100644 index 00000000..3b136fd3 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/grpc/encoding/customSeviceName.go @@ -0,0 +1,60 @@ +package encoding + +import ( + "context" + + "google.golang.org/grpc" +) + +func ServerDesc(name, tun, tunMulti string) grpc.ServiceDesc { + return grpc.ServiceDesc{ + ServiceName: name, + HandlerType: (*GRPCServiceServer)(nil), + Methods: []grpc.MethodDesc{}, + Streams: []grpc.StreamDesc{ + { + StreamName: tun, + Handler: _GRPCService_Tun_Handler, + ServerStreams: true, + ClientStreams: true, + }, + { + StreamName: tunMulti, + Handler: _GRPCService_TunMulti_Handler, + ServerStreams: true, + ClientStreams: true, + }, + }, + Metadata: "grpc.proto", + } +} + +func (c *gRPCServiceClient) TunCustomName(ctx context.Context, name, tun string, opts ...grpc.CallOption) (GRPCService_TunClient, error) { + stream, err := c.cc.NewStream(ctx, &ServerDesc(name, tun, "").Streams[0], "/"+name+"/"+tun, opts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[Hunk, Hunk]{ClientStream: stream} + return x, nil +} + +func (c *gRPCServiceClient) TunMultiCustomName(ctx context.Context, name, tunMulti string, opts ...grpc.CallOption) (GRPCService_TunMultiClient, error) { + stream, err := c.cc.NewStream(ctx, &ServerDesc(name, "", tunMulti).Streams[1], "/"+name+"/"+tunMulti, opts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[MultiHunk, MultiHunk]{ClientStream: stream} + return x, nil +} + +type GRPCServiceClientX interface { + TunCustomName(ctx context.Context, name, tun string, opts ...grpc.CallOption) (GRPCService_TunClient, error) + TunMultiCustomName(ctx context.Context, name, tunMulti string, opts ...grpc.CallOption) (GRPCService_TunMultiClient, error) + Tun(ctx context.Context, opts ...grpc.CallOption) (GRPCService_TunClient, error) + TunMulti(ctx context.Context, opts ...grpc.CallOption) (GRPCService_TunMultiClient, error) +} + +func RegisterGRPCServiceServerX(s *grpc.Server, srv GRPCServiceServer, name, tun, tunMulti string) { + desc := ServerDesc(name, tun, tunMulti) + s.RegisterService(&desc, srv) +} diff --git a/subproject/Xray-core-main/transport/internet/grpc/encoding/encoding.go b/subproject/Xray-core-main/transport/internet/grpc/encoding/encoding.go new file mode 100644 index 00000000..523b90ce --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/grpc/encoding/encoding.go @@ -0,0 +1 @@ +package encoding diff --git a/subproject/Xray-core-main/transport/internet/grpc/encoding/hunkconn.go b/subproject/Xray-core-main/transport/internet/grpc/encoding/hunkconn.go new file mode 100644 index 00000000..f1155de8 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/grpc/encoding/hunkconn.go @@ -0,0 +1,152 @@ +package encoding + +import ( + "context" + "io" + + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/net/cnc" + "github.com/xtls/xray-core/common/signal/done" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/peer" +) + +type HunkConn interface { + Context() context.Context + Send(*Hunk) error + Recv() (*Hunk, error) + SendMsg(m interface{}) error + RecvMsg(m interface{}) error +} + +type StreamCloser interface { + CloseSend() error +} + +type HunkReaderWriter struct { + hc HunkConn + cancel context.CancelFunc + done *done.Instance + + buf []byte + index int +} + +func NewHunkReadWriter(hc HunkConn, cancel context.CancelFunc) *HunkReaderWriter { + return &HunkReaderWriter{hc, cancel, done.New(), nil, 0} +} + +func NewHunkConn(hc HunkConn, cancel context.CancelFunc) net.Conn { + var rAddr net.Addr + pr, ok := peer.FromContext(hc.Context()) + if ok { + rAddr = pr.Addr + } else { + rAddr = &net.TCPAddr{ + IP: []byte{0, 0, 0, 0}, + Port: 0, + } + } + + md, ok := metadata.FromIncomingContext(hc.Context()) + if ok { + header := md.Get("x-real-ip") + if len(header) > 0 { + realip := net.ParseAddress(header[0]) + if realip.Family().IsIP() { + rAddr = &net.TCPAddr{ + IP: realip.IP(), + Port: 0, + } + } + } + } + wrc := NewHunkReadWriter(hc, cancel) + return cnc.NewConnection( + cnc.ConnectionInput(wrc), + cnc.ConnectionOutput(wrc), + cnc.ConnectionOnClose(wrc), + cnc.ConnectionRemoteAddr(rAddr), + ) +} + +func (h *HunkReaderWriter) forceFetch() error { + hunk, err := h.hc.Recv() + if err != nil { + if err == io.EOF { + return err + } + + return errors.New("failed to fetch hunk from gRPC tunnel").Base(err) + } + + h.buf = hunk.Data + h.index = 0 + + return nil +} + +func (h *HunkReaderWriter) Read(buf []byte) (int, error) { + if h.done.Done() { + return 0, io.EOF + } + + if h.index >= len(h.buf) { + if err := h.forceFetch(); err != nil { + return 0, err + } + } + n := copy(buf, h.buf[h.index:]) + h.index += n + + return n, nil +} + +func (h *HunkReaderWriter) ReadMultiBuffer() (buf.MultiBuffer, error) { + if h.done.Done() { + return nil, io.EOF + } + if h.index >= len(h.buf) { + if err := h.forceFetch(); err != nil { + return nil, err + } + } + + if cap(h.buf) >= buf.Size { + b := h.buf + h.index = len(h.buf) + return buf.MultiBuffer{buf.NewExisted(b)}, nil + } + + b := buf.New() + _, err := b.ReadFrom(h) + if err != nil { + return nil, err + } + return buf.MultiBuffer{b}, nil +} + +func (h *HunkReaderWriter) Write(buf []byte) (int, error) { + if h.done.Done() { + return 0, io.ErrClosedPipe + } + + err := h.hc.Send(&Hunk{Data: buf[:]}) + if err != nil { + return 0, errors.New("failed to send data over gRPC tunnel").Base(err) + } + return len(buf), nil +} + +func (h *HunkReaderWriter) Close() error { + if h.cancel != nil { + h.cancel() + } + if sc, match := h.hc.(StreamCloser); match { + return sc.CloseSend() + } + + return h.done.Close() +} diff --git a/subproject/Xray-core-main/transport/internet/grpc/encoding/multiconn.go b/subproject/Xray-core-main/transport/internet/grpc/encoding/multiconn.go new file mode 100644 index 00000000..fa95cba8 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/grpc/encoding/multiconn.go @@ -0,0 +1,145 @@ +package encoding + +import ( + "context" + "io" + "net" + + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/errors" + xnet "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/net/cnc" + "github.com/xtls/xray-core/common/signal/done" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/peer" +) + +type MultiHunkConn interface { + Context() context.Context + Send(*MultiHunk) error + Recv() (*MultiHunk, error) + SendMsg(m interface{}) error + RecvMsg(m interface{}) error +} + +type MultiHunkReaderWriter struct { + hc MultiHunkConn + cancel context.CancelFunc + done *done.Instance + + buf [][]byte +} + +func NewMultiHunkReadWriter(hc MultiHunkConn, cancel context.CancelFunc) *MultiHunkReaderWriter { + return &MultiHunkReaderWriter{hc, cancel, done.New(), nil} +} + +func NewMultiHunkConn(hc MultiHunkConn, cancel context.CancelFunc) net.Conn { + var rAddr net.Addr + pr, ok := peer.FromContext(hc.Context()) + if ok { + rAddr = pr.Addr + } else { + rAddr = &net.TCPAddr{ + IP: []byte{0, 0, 0, 0}, + Port: 0, + } + } + + md, ok := metadata.FromIncomingContext(hc.Context()) + if ok { + header := md.Get("x-real-ip") + if len(header) > 0 { + realip := xnet.ParseAddress(header[0]) + if realip.Family().IsIP() { + rAddr = &net.TCPAddr{ + IP: realip.IP(), + Port: 0, + } + } + } + } + wrc := NewMultiHunkReadWriter(hc, cancel) + return cnc.NewConnection( + cnc.ConnectionInputMulti(wrc), + cnc.ConnectionOutputMulti(wrc), + cnc.ConnectionOnClose(wrc), + cnc.ConnectionRemoteAddr(rAddr), + ) +} + +func (h *MultiHunkReaderWriter) forceFetch() error { + hunk, err := h.hc.Recv() + if err != nil { + if err == io.EOF { + return err + } + + return errors.New("failed to fetch hunk from gRPC tunnel").Base(err) + } + + h.buf = hunk.Data + + return nil +} + +func (h *MultiHunkReaderWriter) ReadMultiBuffer() (buf.MultiBuffer, error) { + if h.done.Done() { + return nil, io.EOF + } + + if err := h.forceFetch(); err != nil { + return nil, err + } + + mb := make(buf.MultiBuffer, 0, len(h.buf)) + for _, b := range h.buf { + if len(b) == 0 { + continue + } + + if cap(b) >= buf.Size { + mb = append(mb, buf.NewExisted(b)) + } else { + nb := buf.New() + nb.Extend(int32(len(b))) + copy(nb.Bytes(), b) + + mb = append(mb, nb) + } + + } + return mb, nil +} + +func (h *MultiHunkReaderWriter) WriteMultiBuffer(mb buf.MultiBuffer) error { + defer buf.ReleaseMulti(mb) + if h.done.Done() { + return io.ErrClosedPipe + } + + hunks := make([][]byte, 0, len(mb)) + + for _, b := range mb { + if b.Len() > 0 { + hunks = append(hunks, b.Bytes()) + } + } + + err := h.hc.Send(&MultiHunk{Data: hunks}) + if err != nil { + return err + } + return nil +} + +func (h *MultiHunkReaderWriter) Close() error { + if h.cancel != nil { + h.cancel() + } + if sc, match := h.hc.(StreamCloser); match { + return sc.CloseSend() + } + + return h.done.Close() +} diff --git a/subproject/Xray-core-main/transport/internet/grpc/encoding/stream.pb.go b/subproject/Xray-core-main/transport/internet/grpc/encoding/stream.pb.go new file mode 100644 index 00000000..6f3d242e --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/grpc/encoding/stream.pb.go @@ -0,0 +1,176 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: transport/internet/grpc/encoding/stream.proto + +package encoding + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Hunk struct { + state protoimpl.MessageState `protogen:"open.v1"` + Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Hunk) Reset() { + *x = Hunk{} + mi := &file_transport_internet_grpc_encoding_stream_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Hunk) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Hunk) ProtoMessage() {} + +func (x *Hunk) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_grpc_encoding_stream_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Hunk.ProtoReflect.Descriptor instead. +func (*Hunk) Descriptor() ([]byte, []int) { + return file_transport_internet_grpc_encoding_stream_proto_rawDescGZIP(), []int{0} +} + +func (x *Hunk) GetData() []byte { + if x != nil { + return x.Data + } + return nil +} + +type MultiHunk struct { + state protoimpl.MessageState `protogen:"open.v1"` + Data [][]byte `protobuf:"bytes,1,rep,name=data,proto3" json:"data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MultiHunk) Reset() { + *x = MultiHunk{} + mi := &file_transport_internet_grpc_encoding_stream_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MultiHunk) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MultiHunk) ProtoMessage() {} + +func (x *MultiHunk) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_grpc_encoding_stream_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MultiHunk.ProtoReflect.Descriptor instead. +func (*MultiHunk) Descriptor() ([]byte, []int) { + return file_transport_internet_grpc_encoding_stream_proto_rawDescGZIP(), []int{1} +} + +func (x *MultiHunk) GetData() [][]byte { + if x != nil { + return x.Data + } + return nil +} + +var File_transport_internet_grpc_encoding_stream_proto protoreflect.FileDescriptor + +const file_transport_internet_grpc_encoding_stream_proto_rawDesc = "" + + "\n" + + "-transport/internet/grpc/encoding/stream.proto\x12%xray.transport.internet.grpc.encoding\"\x1a\n" + + "\x04Hunk\x12\x12\n" + + "\x04data\x18\x01 \x01(\fR\x04data\"\x1f\n" + + "\tMultiHunk\x12\x12\n" + + "\x04data\x18\x01 \x03(\fR\x04data2\xe6\x01\n" + + "\vGRPCService\x12c\n" + + "\x03Tun\x12+.xray.transport.internet.grpc.encoding.Hunk\x1a+.xray.transport.internet.grpc.encoding.Hunk(\x010\x01\x12r\n" + + "\bTunMulti\x120.xray.transport.internet.grpc.encoding.MultiHunk\x1a0.xray.transport.internet.grpc.encoding.MultiHunk(\x010\x01B xray.transport.internet.grpc.encoding.Hunk + 1, // 1: xray.transport.internet.grpc.encoding.GRPCService.TunMulti:input_type -> xray.transport.internet.grpc.encoding.MultiHunk + 0, // 2: xray.transport.internet.grpc.encoding.GRPCService.Tun:output_type -> xray.transport.internet.grpc.encoding.Hunk + 1, // 3: xray.transport.internet.grpc.encoding.GRPCService.TunMulti:output_type -> xray.transport.internet.grpc.encoding.MultiHunk + 2, // [2:4] is the sub-list for method output_type + 0, // [0:2] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_transport_internet_grpc_encoding_stream_proto_init() } +func file_transport_internet_grpc_encoding_stream_proto_init() { + if File_transport_internet_grpc_encoding_stream_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_grpc_encoding_stream_proto_rawDesc), len(file_transport_internet_grpc_encoding_stream_proto_rawDesc)), + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_transport_internet_grpc_encoding_stream_proto_goTypes, + DependencyIndexes: file_transport_internet_grpc_encoding_stream_proto_depIdxs, + MessageInfos: file_transport_internet_grpc_encoding_stream_proto_msgTypes, + }.Build() + File_transport_internet_grpc_encoding_stream_proto = out.File + file_transport_internet_grpc_encoding_stream_proto_goTypes = nil + file_transport_internet_grpc_encoding_stream_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/transport/internet/grpc/encoding/stream.proto b/subproject/Xray-core-main/transport/internet/grpc/encoding/stream.proto new file mode 100644 index 00000000..63898a77 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/grpc/encoding/stream.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; + +package xray.transport.internet.grpc.encoding; +option go_package = "github.com/xtls/xray-core/transport/internet/grpc/encoding"; + +message Hunk { + bytes data = 1; +} + +message MultiHunk { + repeated bytes data = 1; +} + +service GRPCService { + rpc Tun (stream Hunk) returns (stream Hunk); + rpc TunMulti (stream MultiHunk) returns (stream MultiHunk); +} diff --git a/subproject/Xray-core-main/transport/internet/grpc/encoding/stream_grpc.pb.go b/subproject/Xray-core-main/transport/internet/grpc/encoding/stream_grpc.pb.go new file mode 100644 index 00000000..d3ad629e --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/grpc/encoding/stream_grpc.pb.go @@ -0,0 +1,147 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.0 +// - protoc v6.33.5 +// source: transport/internet/grpc/encoding/stream.proto + +package encoding + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + GRPCService_Tun_FullMethodName = "/xray.transport.internet.grpc.encoding.GRPCService/Tun" + GRPCService_TunMulti_FullMethodName = "/xray.transport.internet.grpc.encoding.GRPCService/TunMulti" +) + +// GRPCServiceClient is the client API for GRPCService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type GRPCServiceClient interface { + Tun(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[Hunk, Hunk], error) + TunMulti(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[MultiHunk, MultiHunk], error) +} + +type gRPCServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewGRPCServiceClient(cc grpc.ClientConnInterface) GRPCServiceClient { + return &gRPCServiceClient{cc} +} + +func (c *gRPCServiceClient) Tun(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[Hunk, Hunk], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &GRPCService_ServiceDesc.Streams[0], GRPCService_Tun_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[Hunk, Hunk]{ClientStream: stream} + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type GRPCService_TunClient = grpc.BidiStreamingClient[Hunk, Hunk] + +func (c *gRPCServiceClient) TunMulti(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[MultiHunk, MultiHunk], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &GRPCService_ServiceDesc.Streams[1], GRPCService_TunMulti_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[MultiHunk, MultiHunk]{ClientStream: stream} + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type GRPCService_TunMultiClient = grpc.BidiStreamingClient[MultiHunk, MultiHunk] + +// GRPCServiceServer is the server API for GRPCService service. +// All implementations must embed UnimplementedGRPCServiceServer +// for forward compatibility. +type GRPCServiceServer interface { + Tun(grpc.BidiStreamingServer[Hunk, Hunk]) error + TunMulti(grpc.BidiStreamingServer[MultiHunk, MultiHunk]) error + mustEmbedUnimplementedGRPCServiceServer() +} + +// UnimplementedGRPCServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedGRPCServiceServer struct{} + +func (UnimplementedGRPCServiceServer) Tun(grpc.BidiStreamingServer[Hunk, Hunk]) error { + return status.Error(codes.Unimplemented, "method Tun not implemented") +} +func (UnimplementedGRPCServiceServer) TunMulti(grpc.BidiStreamingServer[MultiHunk, MultiHunk]) error { + return status.Error(codes.Unimplemented, "method TunMulti not implemented") +} +func (UnimplementedGRPCServiceServer) mustEmbedUnimplementedGRPCServiceServer() {} +func (UnimplementedGRPCServiceServer) testEmbeddedByValue() {} + +// UnsafeGRPCServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to GRPCServiceServer will +// result in compilation errors. +type UnsafeGRPCServiceServer interface { + mustEmbedUnimplementedGRPCServiceServer() +} + +func RegisterGRPCServiceServer(s grpc.ServiceRegistrar, srv GRPCServiceServer) { + // If the following call panics, it indicates UnimplementedGRPCServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&GRPCService_ServiceDesc, srv) +} + +func _GRPCService_Tun_Handler(srv interface{}, stream grpc.ServerStream) error { + return srv.(GRPCServiceServer).Tun(&grpc.GenericServerStream[Hunk, Hunk]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type GRPCService_TunServer = grpc.BidiStreamingServer[Hunk, Hunk] + +func _GRPCService_TunMulti_Handler(srv interface{}, stream grpc.ServerStream) error { + return srv.(GRPCServiceServer).TunMulti(&grpc.GenericServerStream[MultiHunk, MultiHunk]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type GRPCService_TunMultiServer = grpc.BidiStreamingServer[MultiHunk, MultiHunk] + +// GRPCService_ServiceDesc is the grpc.ServiceDesc for GRPCService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var GRPCService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "xray.transport.internet.grpc.encoding.GRPCService", + HandlerType: (*GRPCServiceServer)(nil), + Methods: []grpc.MethodDesc{}, + Streams: []grpc.StreamDesc{ + { + StreamName: "Tun", + Handler: _GRPCService_Tun_Handler, + ServerStreams: true, + ClientStreams: true, + }, + { + StreamName: "TunMulti", + Handler: _GRPCService_TunMulti_Handler, + ServerStreams: true, + ClientStreams: true, + }, + }, + Metadata: "transport/internet/grpc/encoding/stream.proto", +} diff --git a/subproject/Xray-core-main/transport/internet/grpc/grpc.go b/subproject/Xray-core-main/transport/internet/grpc/grpc.go new file mode 100644 index 00000000..bcf78b0a --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/grpc/grpc.go @@ -0,0 +1,3 @@ +package grpc + +const protocolName = "grpc" diff --git a/subproject/Xray-core-main/transport/internet/grpc/hub.go b/subproject/Xray-core-main/transport/internet/grpc/hub.go new file mode 100644 index 00000000..34f8f1c0 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/grpc/hub.go @@ -0,0 +1,143 @@ +package grpc + +import ( + "context" + "time" + + goreality "github.com/xtls/reality" + "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/transport/internet" + "github.com/xtls/xray-core/transport/internet/grpc/encoding" + "github.com/xtls/xray-core/transport/internet/reality" + "github.com/xtls/xray-core/transport/internet/tls" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/keepalive" +) + +type Listener struct { + encoding.UnimplementedGRPCServiceServer + ctx context.Context + handler internet.ConnHandler + local net.Addr + config *Config + + s *grpc.Server +} + +func (l Listener) Tun(server encoding.GRPCService_TunServer) error { + tunCtx, cancel := context.WithCancel(l.ctx) + l.handler(encoding.NewHunkConn(server, cancel)) + <-tunCtx.Done() + return nil +} + +func (l Listener) TunMulti(server encoding.GRPCService_TunMultiServer) error { + tunCtx, cancel := context.WithCancel(l.ctx) + l.handler(encoding.NewMultiHunkConn(server, cancel)) + <-tunCtx.Done() + return nil +} + +func (l Listener) Close() error { + l.s.Stop() + return nil +} + +func (l Listener) Addr() net.Addr { + return l.local +} + +func Listen(ctx context.Context, address net.Address, port net.Port, settings *internet.MemoryStreamConfig, handler internet.ConnHandler) (internet.Listener, error) { + grpcSettings := settings.ProtocolSettings.(*Config) + var listener *Listener + if port == net.Port(0) { // unix + listener = &Listener{ + handler: handler, + local: &net.UnixAddr{ + Name: address.Domain(), + Net: "unix", + }, + config: grpcSettings, + } + } else { // tcp + listener = &Listener{ + handler: handler, + local: &net.TCPAddr{ + IP: address.IP(), + Port: int(port), + }, + config: grpcSettings, + } + } + + listener.ctx = ctx + + config := tls.ConfigFromStreamSettings(settings) + + var options []grpc.ServerOption + var s *grpc.Server + if config != nil { + // gRPC server may silently ignore TLS errors + options = append(options, grpc.Creds(credentials.NewTLS(config.GetTLSConfig(tls.WithNextProto("h2"))))) + } + if grpcSettings.IdleTimeout > 0 || grpcSettings.HealthCheckTimeout > 0 { + options = append(options, grpc.KeepaliveParams(keepalive.ServerParameters{ + Time: time.Second * time.Duration(grpcSettings.IdleTimeout), + Timeout: time.Second * time.Duration(grpcSettings.HealthCheckTimeout), + })) + } + + s = grpc.NewServer(options...) + listener.s = s + + if settings.SocketSettings != nil && settings.SocketSettings.AcceptProxyProtocol { + errors.LogWarning(ctx, "accepting PROXY protocol") + } + + go func() { + var streamListener net.Listener + var err error + if port == net.Port(0) { // unix + streamListener, err = internet.ListenSystem(ctx, &net.UnixAddr{ + Name: address.Domain(), + Net: "unix", + }, settings.SocketSettings) + if err != nil { + errors.LogErrorInner(ctx, err, "failed to listen on ", address) + return + } + } else { // tcp + streamListener, err = internet.ListenSystem(ctx, &net.TCPAddr{ + IP: address.IP(), + Port: int(port), + }, settings.SocketSettings) + if err != nil { + errors.LogErrorInner(ctx, err, "failed to listen on ", address, ":", port) + return + } + } + + if settings.TcpmaskManager != nil { + streamListener, _ = settings.TcpmaskManager.WrapListener(streamListener) + } + + errors.LogDebug(ctx, "gRPC listen for service name `"+grpcSettings.getServiceName()+"` tun `"+grpcSettings.getTunStreamName()+"` multi tun `"+grpcSettings.getTunMultiStreamName()+"`") + encoding.RegisterGRPCServiceServerX(s, listener, grpcSettings.getServiceName(), grpcSettings.getTunStreamName(), grpcSettings.getTunMultiStreamName()) + + if config := reality.ConfigFromStreamSettings(settings); config != nil { + streamListener = goreality.NewListener(streamListener, config.GetREALITYConfig()) + } + if err = s.Serve(streamListener); err != nil { + errors.LogInfoInner(ctx, err, "Listener for gRPC ended") + } + }() + + return listener, nil +} + +func init() { + common.Must(internet.RegisterTransportListener(protocolName, Listen)) +} diff --git a/subproject/Xray-core-main/transport/internet/happy_eyeballs.go b/subproject/Xray-core-main/transport/internet/happy_eyeballs.go new file mode 100644 index 00000000..d7f30227 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/happy_eyeballs.go @@ -0,0 +1,175 @@ +package internet + +import ( + "context" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "time" +) + +type result struct { + err error + conn net.Conn + index int +} + +func TcpRaceDial(ctx context.Context, src net.Address, ips []net.IP, port net.Port, sockopt *SocketConfig, domain string) (net.Conn, error) { + if len(ips) < 2 { + panic("at least 2 ips is required to race dial") + } + + prioritizeIPv6 := sockopt.HappyEyeballs.PrioritizeIpv6 + interleave := sockopt.HappyEyeballs.Interleave + tryDelayMs := time.Duration(sockopt.HappyEyeballs.TryDelayMs) * time.Millisecond + maxConcurrentTry := sockopt.HappyEyeballs.MaxConcurrentTry + + ips = sortIPs(ips, prioritizeIPv6, interleave) + newCtx, cancel := context.WithCancel(ctx) + defer cancel() + var resultCh = make(chan *result, len(ips)) + nextTryIndex := 0 + activeNum := uint32(0) + timer := time.NewTimer(0) + var winConn net.Conn + errors.LogDebug(ctx, "happy eyeballs racing dial for ", domain, " with IPs ", ips) + for { + select { + case r := <-resultCh: + activeNum-- + select { + case <-ctx.Done(): + cancel() + timer.Stop() + if winConn != nil { + winConn.Close() + } + if r.conn != nil { + r.conn.Close() + } + if activeNum == 0 { + return nil, ctx.Err() + } + continue + default: + if r.conn != nil { + cancel() + timer.Stop() + if winConn == nil { + winConn = r.conn + errors.LogDebug(ctx, "happy eyeballs established connection for ", domain, " with IP ", ips[r.index]) + } else { + r.conn.Close() + } + } + if winConn != nil && activeNum == 0 { + return winConn, nil + } + if winConn != nil { + continue + } + if nextTryIndex < len(ips) { + timer.Reset(0) + continue + } + if activeNum == 0 { + errors.LogDebugInner(ctx, r.err, "happy eyeballs no connection established for ", domain) + return nil, r.err + } + timer.Stop() + continue + } + + case <-timer.C: + if nextTryIndex == len(ips) || activeNum == maxConcurrentTry { + panic("impossible situation") + } + go tcpTryDial(newCtx, src, sockopt, ips[nextTryIndex], port, nextTryIndex, resultCh) + activeNum++ + nextTryIndex++ + if nextTryIndex == len(ips) || activeNum == maxConcurrentTry { + timer.Stop() + } else { + timer.Reset(tryDelayMs) + } + continue + } + } +} + +// sortIPs sort IPs according to rfc 8305. +func sortIPs(ips []net.IP, prioritizeIPv6 bool, interleave uint32) []net.IP { + if len(ips) == 0 { + return ips + } + var ip4 = make([]net.IP, 0, len(ips)) + var ip6 = make([]net.IP, 0, len(ips)) + for _, ip := range ips { + parsedIp := net.IPAddress(ip).IP() + if len(parsedIp) == net.IPv4len { + ip4 = append(ip4, parsedIp) + } else { + ip6 = append(ip6, parsedIp) + } + } + + if len(ip4) == 0 || len(ip6) == 0 { + return ips + } + + var newIPs = make([]net.IP, 0, len(ips)) + consumeIP4 := 0 + consumeIP6 := 0 + consumeTurn := uint32(0) + ip4turn := true + if prioritizeIPv6 { + ip4turn = false + } + for { + if ip4turn { + newIPs = append(newIPs, ip4[consumeIP4]) + consumeIP4++ + if consumeIP4 == len(ip4) { + newIPs = append(newIPs, ip6[consumeIP6:]...) + break + } + consumeTurn++ + if consumeTurn == interleave { + ip4turn = false + consumeTurn = uint32(0) + } + } else { + newIPs = append(newIPs, ip6[consumeIP6]) + consumeIP6++ + if consumeIP6 == len(ip6) { + newIPs = append(newIPs, ip4[consumeIP4:]...) + break + } + consumeTurn++ + if consumeTurn == interleave { + ip4turn = true + consumeTurn = uint32(0) + } + } + } + + return newIPs +} + +func tcpTryDial(ctx context.Context, src net.Address, sockopt *SocketConfig, ip net.IP, port net.Port, index int, resultCh chan<- *result) { + conn, err := effectiveSystemDialer.Dial(ctx, src, net.Destination{Address: net.IPAddress(ip), Network: net.Network_TCP, Port: port}, sockopt) + select { + case <-ctx.Done(): + if conn != nil { + conn.Close() + } + resultCh <- &result{err: ctx.Err(), index: index} + return + default: + if err != nil { + resultCh <- &result{err: err, index: index} + return + } + resultCh <- &result{conn: conn, index: index} + return + } +} diff --git a/subproject/Xray-core-main/transport/internet/header.go b/subproject/Xray-core-main/transport/internet/header.go new file mode 100644 index 00000000..95c76343 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/header.go @@ -0,0 +1,41 @@ +package internet + +import ( + "context" + "net" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/errors" +) + +type PacketHeader interface { + Size() int32 + Serialize([]byte) +} + +func CreatePacketHeader(config interface{}) (PacketHeader, error) { + header, err := common.CreateObject(context.Background(), config) + if err != nil { + return nil, err + } + if h, ok := header.(PacketHeader); ok { + return h, nil + } + return nil, errors.New("not a packet header") +} + +type ConnectionAuthenticator interface { + Client(net.Conn) net.Conn + Server(net.Conn) net.Conn +} + +func CreateConnectionAuthenticator(config interface{}) (ConnectionAuthenticator, error) { + auth, err := common.CreateObject(context.Background(), config) + if err != nil { + return nil, err + } + if a, ok := auth.(ConnectionAuthenticator); ok { + return a, nil + } + return nil, errors.New("not a ConnectionAuthenticator") +} diff --git a/subproject/Xray-core-main/transport/internet/headers/http/config.go b/subproject/Xray-core-main/transport/internet/headers/http/config.go new file mode 100644 index 00000000..baa69663 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/headers/http/config.go @@ -0,0 +1,100 @@ +package http + +import ( + "strings" + + "github.com/xtls/xray-core/common/dice" +) + +func pickString(arr []string) string { + n := len(arr) + switch n { + case 0: + return "" + case 1: + return arr[0] + default: + return arr[dice.Roll(n)] + } +} + +func (v *RequestConfig) PickURI() string { + return pickString(v.Uri) +} + +func (v *RequestConfig) PickHeaders() []string { + n := len(v.Header) + if n == 0 { + return nil + } + headers := make([]string, n) + for idx, headerConfig := range v.Header { + headerName := headerConfig.Name + headerValue := pickString(headerConfig.Value) + headers[idx] = headerName + ": " + headerValue + } + return headers +} + +func (v *RequestConfig) GetVersionValue() string { + if v == nil || v.Version == nil { + return "1.1" + } + return v.Version.Value +} + +func (v *RequestConfig) GetMethodValue() string { + if v == nil || v.Method == nil { + return "GET" + } + return v.Method.Value +} + +func (v *RequestConfig) GetFullVersion() string { + return "HTTP/" + v.GetVersionValue() +} + +func (v *ResponseConfig) HasHeader(header string) bool { + cHeader := strings.ToLower(header) + for _, tHeader := range v.Header { + if strings.EqualFold(tHeader.Name, cHeader) { + return true + } + } + return false +} + +func (v *ResponseConfig) PickHeaders() []string { + n := len(v.Header) + if n == 0 { + return nil + } + headers := make([]string, n) + for idx, headerConfig := range v.Header { + headerName := headerConfig.Name + headerValue := pickString(headerConfig.Value) + headers[idx] = headerName + ": " + headerValue + } + return headers +} + +func (v *ResponseConfig) GetVersionValue() string { + if v == nil || v.Version == nil { + return "1.1" + } + return v.Version.Value +} + +func (v *ResponseConfig) GetFullVersion() string { + return "HTTP/" + v.GetVersionValue() +} + +func (v *ResponseConfig) GetStatusValue() *Status { + if v == nil || v.Status == nil { + return &Status{ + Code: "200", + Reason: "OK", + } + } + return v.Status +} diff --git a/subproject/Xray-core-main/transport/internet/headers/http/config.pb.go b/subproject/Xray-core-main/transport/internet/headers/http/config.pb.go new file mode 100644 index 00000000..15acc068 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/headers/http/config.pb.go @@ -0,0 +1,499 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: transport/internet/headers/http/config.proto + +package http + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Header struct { + state protoimpl.MessageState `protogen:"open.v1"` + // "Accept", "Cookie", etc + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Each entry must be valid in one piece. Random entry will be chosen if + // multiple entries present. + Value []string `protobuf:"bytes,2,rep,name=value,proto3" json:"value,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Header) Reset() { + *x = Header{} + mi := &file_transport_internet_headers_http_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Header) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Header) ProtoMessage() {} + +func (x *Header) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_headers_http_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Header.ProtoReflect.Descriptor instead. +func (*Header) Descriptor() ([]byte, []int) { + return file_transport_internet_headers_http_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Header) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Header) GetValue() []string { + if x != nil { + return x.Value + } + return nil +} + +// HTTP version. Default value "1.1". +type Version struct { + state protoimpl.MessageState `protogen:"open.v1"` + Value string `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Version) Reset() { + *x = Version{} + mi := &file_transport_internet_headers_http_config_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Version) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Version) ProtoMessage() {} + +func (x *Version) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_headers_http_config_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Version.ProtoReflect.Descriptor instead. +func (*Version) Descriptor() ([]byte, []int) { + return file_transport_internet_headers_http_config_proto_rawDescGZIP(), []int{1} +} + +func (x *Version) GetValue() string { + if x != nil { + return x.Value + } + return "" +} + +// HTTP method. Default value "GET". +type Method struct { + state protoimpl.MessageState `protogen:"open.v1"` + Value string `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Method) Reset() { + *x = Method{} + mi := &file_transport_internet_headers_http_config_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Method) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Method) ProtoMessage() {} + +func (x *Method) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_headers_http_config_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Method.ProtoReflect.Descriptor instead. +func (*Method) Descriptor() ([]byte, []int) { + return file_transport_internet_headers_http_config_proto_rawDescGZIP(), []int{2} +} + +func (x *Method) GetValue() string { + if x != nil { + return x.Value + } + return "" +} + +type RequestConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Full HTTP version like "1.1". + Version *Version `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"` + // GET, POST, CONNECT etc + Method *Method `protobuf:"bytes,2,opt,name=method,proto3" json:"method,omitempty"` + // URI like "/login.php" + Uri []string `protobuf:"bytes,3,rep,name=uri,proto3" json:"uri,omitempty"` + Header []*Header `protobuf:"bytes,4,rep,name=header,proto3" json:"header,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RequestConfig) Reset() { + *x = RequestConfig{} + mi := &file_transport_internet_headers_http_config_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RequestConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RequestConfig) ProtoMessage() {} + +func (x *RequestConfig) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_headers_http_config_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RequestConfig.ProtoReflect.Descriptor instead. +func (*RequestConfig) Descriptor() ([]byte, []int) { + return file_transport_internet_headers_http_config_proto_rawDescGZIP(), []int{3} +} + +func (x *RequestConfig) GetVersion() *Version { + if x != nil { + return x.Version + } + return nil +} + +func (x *RequestConfig) GetMethod() *Method { + if x != nil { + return x.Method + } + return nil +} + +func (x *RequestConfig) GetUri() []string { + if x != nil { + return x.Uri + } + return nil +} + +func (x *RequestConfig) GetHeader() []*Header { + if x != nil { + return x.Header + } + return nil +} + +type Status struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Status code. Default "200". + Code string `protobuf:"bytes,1,opt,name=code,proto3" json:"code,omitempty"` + // Statue reason. Default "OK". + Reason string `protobuf:"bytes,2,opt,name=reason,proto3" json:"reason,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Status) Reset() { + *x = Status{} + mi := &file_transport_internet_headers_http_config_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Status) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Status) ProtoMessage() {} + +func (x *Status) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_headers_http_config_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Status.ProtoReflect.Descriptor instead. +func (*Status) Descriptor() ([]byte, []int) { + return file_transport_internet_headers_http_config_proto_rawDescGZIP(), []int{4} +} + +func (x *Status) GetCode() string { + if x != nil { + return x.Code + } + return "" +} + +func (x *Status) GetReason() string { + if x != nil { + return x.Reason + } + return "" +} + +type ResponseConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + Version *Version `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"` + Status *Status `protobuf:"bytes,2,opt,name=status,proto3" json:"status,omitempty"` + Header []*Header `protobuf:"bytes,3,rep,name=header,proto3" json:"header,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ResponseConfig) Reset() { + *x = ResponseConfig{} + mi := &file_transport_internet_headers_http_config_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ResponseConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResponseConfig) ProtoMessage() {} + +func (x *ResponseConfig) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_headers_http_config_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ResponseConfig.ProtoReflect.Descriptor instead. +func (*ResponseConfig) Descriptor() ([]byte, []int) { + return file_transport_internet_headers_http_config_proto_rawDescGZIP(), []int{5} +} + +func (x *ResponseConfig) GetVersion() *Version { + if x != nil { + return x.Version + } + return nil +} + +func (x *ResponseConfig) GetStatus() *Status { + if x != nil { + return x.Status + } + return nil +} + +func (x *ResponseConfig) GetHeader() []*Header { + if x != nil { + return x.Header + } + return nil +} + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Settings for authenticating requests. If not set, client side will not send + // authentication header, and server side will bypass authentication. + Request *RequestConfig `protobuf:"bytes,1,opt,name=request,proto3" json:"request,omitempty"` + // Settings for authenticating responses. If not set, client side will bypass + // authentication, and server side will not send authentication header. + Response *ResponseConfig `protobuf:"bytes,2,opt,name=response,proto3" json:"response,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_transport_internet_headers_http_config_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_headers_http_config_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_transport_internet_headers_http_config_proto_rawDescGZIP(), []int{6} +} + +func (x *Config) GetRequest() *RequestConfig { + if x != nil { + return x.Request + } + return nil +} + +func (x *Config) GetResponse() *ResponseConfig { + if x != nil { + return x.Response + } + return nil +} + +var File_transport_internet_headers_http_config_proto protoreflect.FileDescriptor + +const file_transport_internet_headers_http_config_proto_rawDesc = "" + + "\n" + + ",transport/internet/headers/http/config.proto\x12$xray.transport.internet.headers.http\"2\n" + + "\x06Header\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x14\n" + + "\x05value\x18\x02 \x03(\tR\x05value\"\x1f\n" + + "\aVersion\x12\x14\n" + + "\x05value\x18\x01 \x01(\tR\x05value\"\x1e\n" + + "\x06Method\x12\x14\n" + + "\x05value\x18\x01 \x01(\tR\x05value\"\xf6\x01\n" + + "\rRequestConfig\x12G\n" + + "\aversion\x18\x01 \x01(\v2-.xray.transport.internet.headers.http.VersionR\aversion\x12D\n" + + "\x06method\x18\x02 \x01(\v2,.xray.transport.internet.headers.http.MethodR\x06method\x12\x10\n" + + "\x03uri\x18\x03 \x03(\tR\x03uri\x12D\n" + + "\x06header\x18\x04 \x03(\v2,.xray.transport.internet.headers.http.HeaderR\x06header\"4\n" + + "\x06Status\x12\x12\n" + + "\x04code\x18\x01 \x01(\tR\x04code\x12\x16\n" + + "\x06reason\x18\x02 \x01(\tR\x06reason\"\xe5\x01\n" + + "\x0eResponseConfig\x12G\n" + + "\aversion\x18\x01 \x01(\v2-.xray.transport.internet.headers.http.VersionR\aversion\x12D\n" + + "\x06status\x18\x02 \x01(\v2,.xray.transport.internet.headers.http.StatusR\x06status\x12D\n" + + "\x06header\x18\x03 \x03(\v2,.xray.transport.internet.headers.http.HeaderR\x06header\"\xa9\x01\n" + + "\x06Config\x12M\n" + + "\arequest\x18\x01 \x01(\v23.xray.transport.internet.headers.http.RequestConfigR\arequest\x12P\n" + + "\bresponse\x18\x02 \x01(\v24.xray.transport.internet.headers.http.ResponseConfigR\bresponseB\x8e\x01\n" + + "(com.xray.transport.internet.headers.httpP\x01Z9github.com/xtls/xray-core/transport/internet/headers/http\xaa\x02$Xray.Transport.Internet.Headers.Httpb\x06proto3" + +var ( + file_transport_internet_headers_http_config_proto_rawDescOnce sync.Once + file_transport_internet_headers_http_config_proto_rawDescData []byte +) + +func file_transport_internet_headers_http_config_proto_rawDescGZIP() []byte { + file_transport_internet_headers_http_config_proto_rawDescOnce.Do(func() { + file_transport_internet_headers_http_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_transport_internet_headers_http_config_proto_rawDesc), len(file_transport_internet_headers_http_config_proto_rawDesc))) + }) + return file_transport_internet_headers_http_config_proto_rawDescData +} + +var file_transport_internet_headers_http_config_proto_msgTypes = make([]protoimpl.MessageInfo, 7) +var file_transport_internet_headers_http_config_proto_goTypes = []any{ + (*Header)(nil), // 0: xray.transport.internet.headers.http.Header + (*Version)(nil), // 1: xray.transport.internet.headers.http.Version + (*Method)(nil), // 2: xray.transport.internet.headers.http.Method + (*RequestConfig)(nil), // 3: xray.transport.internet.headers.http.RequestConfig + (*Status)(nil), // 4: xray.transport.internet.headers.http.Status + (*ResponseConfig)(nil), // 5: xray.transport.internet.headers.http.ResponseConfig + (*Config)(nil), // 6: xray.transport.internet.headers.http.Config +} +var file_transport_internet_headers_http_config_proto_depIdxs = []int32{ + 1, // 0: xray.transport.internet.headers.http.RequestConfig.version:type_name -> xray.transport.internet.headers.http.Version + 2, // 1: xray.transport.internet.headers.http.RequestConfig.method:type_name -> xray.transport.internet.headers.http.Method + 0, // 2: xray.transport.internet.headers.http.RequestConfig.header:type_name -> xray.transport.internet.headers.http.Header + 1, // 3: xray.transport.internet.headers.http.ResponseConfig.version:type_name -> xray.transport.internet.headers.http.Version + 4, // 4: xray.transport.internet.headers.http.ResponseConfig.status:type_name -> xray.transport.internet.headers.http.Status + 0, // 5: xray.transport.internet.headers.http.ResponseConfig.header:type_name -> xray.transport.internet.headers.http.Header + 3, // 6: xray.transport.internet.headers.http.Config.request:type_name -> xray.transport.internet.headers.http.RequestConfig + 5, // 7: xray.transport.internet.headers.http.Config.response:type_name -> xray.transport.internet.headers.http.ResponseConfig + 8, // [8:8] is the sub-list for method output_type + 8, // [8:8] is the sub-list for method input_type + 8, // [8:8] is the sub-list for extension type_name + 8, // [8:8] is the sub-list for extension extendee + 0, // [0:8] is the sub-list for field type_name +} + +func init() { file_transport_internet_headers_http_config_proto_init() } +func file_transport_internet_headers_http_config_proto_init() { + if File_transport_internet_headers_http_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_headers_http_config_proto_rawDesc), len(file_transport_internet_headers_http_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 7, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_transport_internet_headers_http_config_proto_goTypes, + DependencyIndexes: file_transport_internet_headers_http_config_proto_depIdxs, + MessageInfos: file_transport_internet_headers_http_config_proto_msgTypes, + }.Build() + File_transport_internet_headers_http_config_proto = out.File + file_transport_internet_headers_http_config_proto_goTypes = nil + file_transport_internet_headers_http_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/transport/internet/headers/http/config.proto b/subproject/Xray-core-main/transport/internet/headers/http/config.proto new file mode 100644 index 00000000..fd5799c4 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/headers/http/config.proto @@ -0,0 +1,65 @@ +syntax = "proto3"; + +package xray.transport.internet.headers.http; +option csharp_namespace = "Xray.Transport.Internet.Headers.Http"; +option go_package = "github.com/xtls/xray-core/transport/internet/headers/http"; +option java_package = "com.xray.transport.internet.headers.http"; +option java_multiple_files = true; + +message Header { + // "Accept", "Cookie", etc + string name = 1; + + // Each entry must be valid in one piece. Random entry will be chosen if + // multiple entries present. + repeated string value = 2; +} + +// HTTP version. Default value "1.1". +message Version { + string value = 1; +} + +// HTTP method. Default value "GET". +message Method { + string value = 1; +} + +message RequestConfig { + // Full HTTP version like "1.1". + Version version = 1; + + // GET, POST, CONNECT etc + Method method = 2; + + // URI like "/login.php" + repeated string uri = 3; + + repeated Header header = 4; +} + +message Status { + // Status code. Default "200". + string code = 1; + + // Statue reason. Default "OK". + string reason = 2; +} + +message ResponseConfig { + Version version = 1; + + Status status = 2; + + repeated Header header = 3; +} + +message Config { + // Settings for authenticating requests. If not set, client side will not send + // authentication header, and server side will bypass authentication. + RequestConfig request = 1; + + // Settings for authenticating responses. If not set, client side will bypass + // authentication, and server side will not send authentication header. + ResponseConfig response = 2; +} diff --git a/subproject/Xray-core-main/transport/internet/headers/http/http.go b/subproject/Xray-core-main/transport/internet/headers/http/http.go new file mode 100644 index 00000000..3c1ff06c --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/headers/http/http.go @@ -0,0 +1,320 @@ +package http + +import ( + "bufio" + "bytes" + "context" + "io" + "net" + "net/http" + "strings" + "time" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/errors" +) + +const ( + // CRLF is the line ending in HTTP header + CRLF = "\r\n" + + // ENDING is the double line ending between HTTP header and body. + ENDING = CRLF + CRLF + + // max length of HTTP header. Safety precaution for DDoS attack. + maxHeaderLength = 8192 +) + +var ( + ErrHeaderToLong = errors.New("Header too long.") + + ErrHeaderMisMatch = errors.New("Header Mismatch.") +) + +type Reader interface { + Read(io.Reader) (*buf.Buffer, error) +} + +type Writer interface { + Write(io.Writer) error +} + +type NoOpReader struct{} + +func (NoOpReader) Read(io.Reader) (*buf.Buffer, error) { + return nil, nil +} + +type NoOpWriter struct{} + +func (NoOpWriter) Write(io.Writer) error { + return nil +} + +type HeaderReader struct { + req *http.Request + expectedHeader *RequestConfig +} + +func (h *HeaderReader) ExpectThisRequest(expectedHeader *RequestConfig) *HeaderReader { + h.expectedHeader = expectedHeader + return h +} + +func (h *HeaderReader) Read(reader io.Reader) (*buf.Buffer, error) { + buffer := buf.New() + totalBytes := int32(0) + endingDetected := false + + var headerBuf bytes.Buffer + + for totalBytes < maxHeaderLength { + _, err := buffer.ReadFrom(reader) + if err != nil { + buffer.Release() + return nil, err + } + if n := bytes.Index(buffer.Bytes(), []byte(ENDING)); n != -1 { + headerBuf.Write(buffer.BytesRange(0, int32(n+len(ENDING)))) + buffer.Advance(int32(n + len(ENDING))) + endingDetected = true + break + } + lenEnding := int32(len(ENDING)) + if buffer.Len() >= lenEnding { + totalBytes += buffer.Len() - lenEnding + headerBuf.Write(buffer.BytesRange(0, buffer.Len()-lenEnding)) + leftover := buffer.BytesFrom(-lenEnding) + buffer.Clear() + copy(buffer.Extend(lenEnding), leftover) + + if _, err := readRequest(bufio.NewReader(bytes.NewReader(headerBuf.Bytes()))); err != io.ErrUnexpectedEOF { + return nil, err + } + } + } + + if !endingDetected { + buffer.Release() + return nil, ErrHeaderToLong + } + + if h.expectedHeader == nil { + if buffer.IsEmpty() { + buffer.Release() + return nil, nil + } + return buffer, nil + } + + // Parse the request + if req, err := readRequest(bufio.NewReader(bytes.NewReader(headerBuf.Bytes()))); err != nil { + return nil, err + } else { + h.req = req + } + + // Check req + path := h.req.URL.Path + hasThisURI := false + for _, u := range h.expectedHeader.Uri { + if u == path { + hasThisURI = true + } + } + + if !hasThisURI { + return nil, ErrHeaderMisMatch + } + + if buffer.IsEmpty() { + buffer.Release() + return nil, nil + } + + return buffer, nil +} + +type HeaderWriter struct { + header *buf.Buffer +} + +func NewHeaderWriter(header *buf.Buffer) *HeaderWriter { + return &HeaderWriter{ + header: header, + } +} + +func (w *HeaderWriter) Write(writer io.Writer) error { + if w.header == nil { + return nil + } + err := buf.WriteAllBytes(writer, w.header.Bytes(), nil) + w.header.Release() + w.header = nil + return err +} + +type Conn struct { + net.Conn + + readBuffer *buf.Buffer + oneTimeReader Reader + oneTimeWriter Writer + errorWriter Writer + errorMismatchWriter Writer + errorTooLongWriter Writer + errReason error +} + +func NewConn(conn net.Conn, reader Reader, writer Writer, errorWriter Writer, errorMismatchWriter Writer, errorTooLongWriter Writer) *Conn { + return &Conn{ + Conn: conn, + oneTimeReader: reader, + oneTimeWriter: writer, + errorWriter: errorWriter, + errorMismatchWriter: errorMismatchWriter, + errorTooLongWriter: errorTooLongWriter, + } +} + +func (c *Conn) Read(b []byte) (int, error) { + if c.oneTimeReader != nil { + buffer, err := c.oneTimeReader.Read(c.Conn) + if err != nil { + c.errReason = err + return 0, err + } + c.readBuffer = buffer + c.oneTimeReader = nil + } + + if !c.readBuffer.IsEmpty() { + nBytes, _ := c.readBuffer.Read(b) + if c.readBuffer.IsEmpty() { + c.readBuffer.Release() + c.readBuffer = nil + } + return nBytes, nil + } + + return c.Conn.Read(b) +} + +// Write implements io.Writer. +func (c *Conn) Write(b []byte) (int, error) { + if c.oneTimeWriter != nil { + err := c.oneTimeWriter.Write(c.Conn) + c.oneTimeWriter = nil + if err != nil { + return 0, err + } + } + + return c.Conn.Write(b) +} + +// Close implements net.Conn.Close(). +func (c *Conn) Close() error { + if c.oneTimeWriter != nil && c.errorWriter != nil { + // Connection is being closed but header wasn't sent. This means the client request + // is probably not valid. Sending back a server error header in this case. + + // Write response based on error reason + switch c.errReason { + case ErrHeaderMisMatch: + c.errorMismatchWriter.Write(c.Conn) + case ErrHeaderToLong: + c.errorTooLongWriter.Write(c.Conn) + default: + c.errorWriter.Write(c.Conn) + } + } + + return c.Conn.Close() +} + +func formResponseHeader(config *ResponseConfig) *HeaderWriter { + header := buf.New() + common.Must2(header.WriteString(strings.Join([]string{config.GetFullVersion(), config.GetStatusValue().Code, config.GetStatusValue().Reason}, " "))) + common.Must2(header.WriteString(CRLF)) + + headers := config.PickHeaders() + for _, h := range headers { + common.Must2(header.WriteString(h)) + common.Must2(header.WriteString(CRLF)) + } + if !config.HasHeader("Date") { + common.Must2(header.WriteString("Date: ")) + common.Must2(header.WriteString(time.Now().Format(http.TimeFormat))) + common.Must2(header.WriteString(CRLF)) + } + common.Must2(header.WriteString(CRLF)) + return &HeaderWriter{ + header: header, + } +} + +type Authenticator struct { + config *Config +} + +func (a Authenticator) GetClientWriter() *HeaderWriter { + header := buf.New() + config := a.config.Request + common.Must2(header.WriteString(strings.Join([]string{config.GetMethodValue(), config.PickURI(), config.GetFullVersion()}, " "))) + common.Must2(header.WriteString(CRLF)) + + headers := config.PickHeaders() + for _, h := range headers { + common.Must2(header.WriteString(h)) + common.Must2(header.WriteString(CRLF)) + } + common.Must2(header.WriteString(CRLF)) + return &HeaderWriter{ + header: header, + } +} + +func (a Authenticator) GetServerWriter() *HeaderWriter { + return formResponseHeader(a.config.Response) +} + +func (a Authenticator) Client(conn net.Conn) net.Conn { + if a.config.Request == nil && a.config.Response == nil { + return conn + } + var reader Reader = NoOpReader{} + if a.config.Request != nil { + reader = new(HeaderReader) + } + + var writer Writer = NoOpWriter{} + if a.config.Response != nil { + writer = a.GetClientWriter() + } + return NewConn(conn, reader, writer, NoOpWriter{}, NoOpWriter{}, NoOpWriter{}) +} + +func (a Authenticator) Server(conn net.Conn) net.Conn { + if a.config.Request == nil && a.config.Response == nil { + return conn + } + return NewConn(conn, new(HeaderReader).ExpectThisRequest(a.config.Request), a.GetServerWriter(), + formResponseHeader(resp400), + formResponseHeader(resp404), + formResponseHeader(resp400)) +} + +func NewAuthenticator(ctx context.Context, config *Config) (Authenticator, error) { + return Authenticator{ + config: config, + }, nil +} + +func init() { + common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + return NewAuthenticator(ctx, config.(*Config)) + })) +} diff --git a/subproject/Xray-core-main/transport/internet/headers/http/http_test.go b/subproject/Xray-core-main/transport/internet/headers/http/http_test.go new file mode 100644 index 00000000..8591a199 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/headers/http/http_test.go @@ -0,0 +1,305 @@ +package http_test + +import ( + "bufio" + "bytes" + "context" + "crypto/rand" + "strings" + "testing" + "time" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/net" + . "github.com/xtls/xray-core/transport/internet/headers/http" +) + +func TestReaderWriter(t *testing.T) { + cache := buf.New() + b := buf.New() + common.Must2(b.WriteString("abcd" + ENDING)) + writer := NewHeaderWriter(b) + err := writer.Write(cache) + common.Must(err) + if v := cache.Len(); v != 8 { + t.Error("cache len: ", v) + } + _, err = cache.Write([]byte{'e', 'f', 'g'}) + common.Must(err) + + reader := &HeaderReader{} + _, err = reader.Read(cache) + if err != nil && !strings.HasPrefix(err.Error(), "malformed HTTP request") { + t.Error("unknown error ", err) + } +} + +func TestRequestHeader(t *testing.T) { + auth, err := NewAuthenticator(context.Background(), &Config{ + Request: &RequestConfig{ + Uri: []string{"/"}, + Header: []*Header{ + { + Name: "Test", + Value: []string{"Value"}, + }, + }, + }, + }) + common.Must(err) + + cache := buf.New() + err = auth.GetClientWriter().Write(cache) + common.Must(err) + + if cache.String() != "GET / HTTP/1.1\r\nTest: Value\r\n\r\n" { + t.Error("cache: ", cache.String()) + } +} + +func TestLongRequestHeader(t *testing.T) { + payload := make([]byte, buf.Size+2) + common.Must2(rand.Read(payload[:buf.Size-2])) + copy(payload[buf.Size-2:], ENDING) + payload = append(payload, []byte("abcd")...) + + reader := HeaderReader{} + _, err := reader.Read(bytes.NewReader(payload)) + + if err != nil && !(strings.HasPrefix(err.Error(), "invalid") || strings.HasPrefix(err.Error(), "malformed")) { + t.Error("unknown error ", err) + } +} + +func TestConnection(t *testing.T) { + auth, err := NewAuthenticator(context.Background(), &Config{ + Request: &RequestConfig{ + Method: &Method{Value: "Post"}, + Uri: []string{"/testpath"}, + Header: []*Header{ + { + Name: "Host", + Value: []string{"www.example.com", "www.google.com"}, + }, + { + Name: "User-Agent", + Value: []string{"Test-Agent"}, + }, + }, + }, + Response: &ResponseConfig{ + Version: &Version{ + Value: "1.1", + }, + Status: &Status{ + Code: "404", + Reason: "Not Found", + }, + }, + }) + common.Must(err) + + listener, err := net.Listen("tcp", "127.0.0.1:0") + common.Must(err) + + go func() { + conn, err := listener.Accept() + common.Must(err) + authConn := auth.Server(conn) + b := make([]byte, 256) + for { + n, err := authConn.Read(b) + if err != nil { + break + } + _, err = authConn.Write(b[:n]) + common.Must(err) + } + }() + + conn, err := net.DialTCP("tcp", nil, listener.Addr().(*net.TCPAddr)) + common.Must(err) + + authConn := auth.Client(conn) + defer authConn.Close() + + authConn.Write([]byte("Test payload")) + authConn.Write([]byte("Test payload 2")) + + expectedResponse := "Test payloadTest payload 2" + actualResponse := make([]byte, 256) + deadline := time.Now().Add(time.Second * 5) + totalBytes := 0 + for { + n, err := authConn.Read(actualResponse[totalBytes:]) + common.Must(err) + totalBytes += n + if totalBytes >= len(expectedResponse) || time.Now().After(deadline) { + break + } + } + + if string(actualResponse[:totalBytes]) != expectedResponse { + t.Error("response: ", string(actualResponse[:totalBytes])) + } +} + +func TestConnectionInvPath(t *testing.T) { + auth, err := NewAuthenticator(context.Background(), &Config{ + Request: &RequestConfig{ + Method: &Method{Value: "Post"}, + Uri: []string{"/testpath"}, + Header: []*Header{ + { + Name: "Host", + Value: []string{"www.example.com", "www.google.com"}, + }, + { + Name: "User-Agent", + Value: []string{"Test-Agent"}, + }, + }, + }, + Response: &ResponseConfig{ + Version: &Version{ + Value: "1.1", + }, + Status: &Status{ + Code: "404", + Reason: "Not Found", + }, + }, + }) + common.Must(err) + + authR, err := NewAuthenticator(context.Background(), &Config{ + Request: &RequestConfig{ + Method: &Method{Value: "Post"}, + Uri: []string{"/testpathErr"}, + Header: []*Header{ + { + Name: "Host", + Value: []string{"www.example.com", "www.google.com"}, + }, + { + Name: "User-Agent", + Value: []string{"Test-Agent"}, + }, + }, + }, + Response: &ResponseConfig{ + Version: &Version{ + Value: "1.1", + }, + Status: &Status{ + Code: "404", + Reason: "Not Found", + }, + }, + }) + common.Must(err) + + listener, err := net.Listen("tcp", "127.0.0.1:0") + common.Must(err) + + go func() { + conn, err := listener.Accept() + common.Must(err) + authConn := auth.Server(conn) + b := make([]byte, 256) + for { + n, err := authConn.Read(b) + if err != nil { + authConn.Close() + break + } + _, err = authConn.Write(b[:n]) + common.Must(err) + } + }() + + conn, err := net.DialTCP("tcp", nil, listener.Addr().(*net.TCPAddr)) + common.Must(err) + + authConn := authR.Client(conn) + defer authConn.Close() + + authConn.Write([]byte("Test payload")) + authConn.Write([]byte("Test payload 2")) + + expectedResponse := "Test payloadTest payload 2" + actualResponse := make([]byte, 256) + deadline := time.Now().Add(time.Second * 5) + totalBytes := 0 + for { + n, err := authConn.Read(actualResponse[totalBytes:]) + if err == nil { + t.Error("Error Expected", err) + } else { + return + } + totalBytes += n + if totalBytes >= len(expectedResponse) || time.Now().After(deadline) { + break + } + } +} + +func TestConnectionInvReq(t *testing.T) { + auth, err := NewAuthenticator(context.Background(), &Config{ + Request: &RequestConfig{ + Method: &Method{Value: "Post"}, + Uri: []string{"/testpath"}, + Header: []*Header{ + { + Name: "Host", + Value: []string{"www.example.com", "www.google.com"}, + }, + { + Name: "User-Agent", + Value: []string{"Test-Agent"}, + }, + }, + }, + Response: &ResponseConfig{ + Version: &Version{ + Value: "1.1", + }, + Status: &Status{ + Code: "404", + Reason: "Not Found", + }, + }, + }) + common.Must(err) + + listener, err := net.Listen("tcp", "127.0.0.1:0") + common.Must(err) + + go func() { + conn, err := listener.Accept() + common.Must(err) + authConn := auth.Server(conn) + b := make([]byte, 256) + for { + n, err := authConn.Read(b) + if err != nil { + authConn.Close() + break + } + _, err = authConn.Write(b[:n]) + common.Must(err) + } + }() + + conn, err := net.DialTCP("tcp", nil, listener.Addr().(*net.TCPAddr)) + common.Must(err) + + conn.Write([]byte("ABCDEFGHIJKMLN\r\n\r\n")) + l, _, err := bufio.NewReader(conn).ReadLine() + common.Must(err) + if !strings.HasPrefix(string(l), "HTTP/1.1 400 Bad Request") { + t.Error("Resp to non http conn", string(l)) + } +} diff --git a/subproject/Xray-core-main/transport/internet/headers/http/linkedreadRequest.go b/subproject/Xray-core-main/transport/internet/headers/http/linkedreadRequest.go new file mode 100644 index 00000000..a485403e --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/headers/http/linkedreadRequest.go @@ -0,0 +1,11 @@ +package http + +import ( + "bufio" + "net/http" + // required to use go:linkname + _ "unsafe" +) + +//go:linkname readRequest net/http.readRequest +func readRequest(b *bufio.Reader) (req *http.Request, err error) diff --git a/subproject/Xray-core-main/transport/internet/headers/http/resp.go b/subproject/Xray-core-main/transport/internet/headers/http/resp.go new file mode 100644 index 00000000..6050d639 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/headers/http/resp.go @@ -0,0 +1,49 @@ +package http + +var resp400 = &ResponseConfig{ + Version: &Version{ + Value: "1.1", + }, + Status: &Status{ + Code: "400", + Reason: "Bad Request", + }, + Header: []*Header{ + { + Name: "Connection", + Value: []string{"close"}, + }, + { + Name: "Cache-Control", + Value: []string{"private"}, + }, + { + Name: "Content-Length", + Value: []string{"0"}, + }, + }, +} + +var resp404 = &ResponseConfig{ + Version: &Version{ + Value: "1.1", + }, + Status: &Status{ + Code: "404", + Reason: "Not Found", + }, + Header: []*Header{ + { + Name: "Connection", + Value: []string{"close"}, + }, + { + Name: "Cache-Control", + Value: []string{"private"}, + }, + { + Name: "Content-Length", + Value: []string{"0"}, + }, + }, +} diff --git a/subproject/Xray-core-main/transport/internet/headers/noop/config.pb.go b/subproject/Xray-core-main/transport/internet/headers/noop/config.pb.go new file mode 100644 index 00000000..5d06e614 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/headers/noop/config.pb.go @@ -0,0 +1,152 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: transport/internet/headers/noop/config.proto + +package noop + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_transport_internet_headers_noop_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_headers_noop_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_transport_internet_headers_noop_config_proto_rawDescGZIP(), []int{0} +} + +type ConnectionConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ConnectionConfig) Reset() { + *x = ConnectionConfig{} + mi := &file_transport_internet_headers_noop_config_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ConnectionConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ConnectionConfig) ProtoMessage() {} + +func (x *ConnectionConfig) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_headers_noop_config_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ConnectionConfig.ProtoReflect.Descriptor instead. +func (*ConnectionConfig) Descriptor() ([]byte, []int) { + return file_transport_internet_headers_noop_config_proto_rawDescGZIP(), []int{1} +} + +var File_transport_internet_headers_noop_config_proto protoreflect.FileDescriptor + +const file_transport_internet_headers_noop_config_proto_rawDesc = "" + + "\n" + + ",transport/internet/headers/noop/config.proto\x12$xray.transport.internet.headers.noop\"\b\n" + + "\x06Config\"\x12\n" + + "\x10ConnectionConfigB\x8e\x01\n" + + "(com.xray.transport.internet.headers.noopP\x01Z9github.com/xtls/xray-core/transport/internet/headers/noop\xaa\x02$Xray.Transport.Internet.Headers.Noopb\x06proto3" + +var ( + file_transport_internet_headers_noop_config_proto_rawDescOnce sync.Once + file_transport_internet_headers_noop_config_proto_rawDescData []byte +) + +func file_transport_internet_headers_noop_config_proto_rawDescGZIP() []byte { + file_transport_internet_headers_noop_config_proto_rawDescOnce.Do(func() { + file_transport_internet_headers_noop_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_transport_internet_headers_noop_config_proto_rawDesc), len(file_transport_internet_headers_noop_config_proto_rawDesc))) + }) + return file_transport_internet_headers_noop_config_proto_rawDescData +} + +var file_transport_internet_headers_noop_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_transport_internet_headers_noop_config_proto_goTypes = []any{ + (*Config)(nil), // 0: xray.transport.internet.headers.noop.Config + (*ConnectionConfig)(nil), // 1: xray.transport.internet.headers.noop.ConnectionConfig +} +var file_transport_internet_headers_noop_config_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_transport_internet_headers_noop_config_proto_init() } +func file_transport_internet_headers_noop_config_proto_init() { + if File_transport_internet_headers_noop_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_headers_noop_config_proto_rawDesc), len(file_transport_internet_headers_noop_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_transport_internet_headers_noop_config_proto_goTypes, + DependencyIndexes: file_transport_internet_headers_noop_config_proto_depIdxs, + MessageInfos: file_transport_internet_headers_noop_config_proto_msgTypes, + }.Build() + File_transport_internet_headers_noop_config_proto = out.File + file_transport_internet_headers_noop_config_proto_goTypes = nil + file_transport_internet_headers_noop_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/transport/internet/headers/noop/config.proto b/subproject/Xray-core-main/transport/internet/headers/noop/config.proto new file mode 100644 index 00000000..f23bc0a8 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/headers/noop/config.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; + +package xray.transport.internet.headers.noop; +option csharp_namespace = "Xray.Transport.Internet.Headers.Noop"; +option go_package = "github.com/xtls/xray-core/transport/internet/headers/noop"; +option java_package = "com.xray.transport.internet.headers.noop"; +option java_multiple_files = true; + +message Config {} + +message ConnectionConfig {} diff --git a/subproject/Xray-core-main/transport/internet/headers/noop/noop.go b/subproject/Xray-core-main/transport/internet/headers/noop/noop.go new file mode 100644 index 00000000..90dae2ed --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/headers/noop/noop.go @@ -0,0 +1,40 @@ +package noop + +import ( + "context" + "net" + + "github.com/xtls/xray-core/common" +) + +type NoOpHeader struct{} + +func (NoOpHeader) Size() int32 { + return 0 +} + +// Serialize implements PacketHeader. +func (NoOpHeader) Serialize([]byte) {} + +func NewNoOpHeader(context.Context, interface{}) (interface{}, error) { + return NoOpHeader{}, nil +} + +type NoOpConnectionHeader struct{} + +func (NoOpConnectionHeader) Client(conn net.Conn) net.Conn { + return conn +} + +func (NoOpConnectionHeader) Server(conn net.Conn) net.Conn { + return conn +} + +func NewNoOpConnectionHeader(context.Context, interface{}) (interface{}, error) { + return NoOpConnectionHeader{}, nil +} + +func init() { + common.Must(common.RegisterConfig((*Config)(nil), NewNoOpHeader)) + common.Must(common.RegisterConfig((*ConnectionConfig)(nil), NewNoOpConnectionHeader)) +} diff --git a/subproject/Xray-core-main/transport/internet/httpupgrade/config.go b/subproject/Xray-core-main/transport/internet/httpupgrade/config.go new file mode 100644 index 00000000..3404930e --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/httpupgrade/config.go @@ -0,0 +1,23 @@ +package httpupgrade + +import ( + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/transport/internet" +) + +func (c *Config) GetNormalizedPath() string { + path := c.Path + if path == "" { + return "/" + } + if path[0] != '/' { + return "/" + path + } + return path +} + +func init() { + common.Must(internet.RegisterProtocolConfigCreator(protocolName, func() interface{} { + return new(Config) + })) +} diff --git a/subproject/Xray-core-main/transport/internet/httpupgrade/config.pb.go b/subproject/Xray-core-main/transport/internet/httpupgrade/config.pb.go new file mode 100644 index 00000000..f838eecc --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/httpupgrade/config.pb.go @@ -0,0 +1,164 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: transport/internet/httpupgrade/config.proto + +package httpupgrade + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + Host string `protobuf:"bytes,1,opt,name=host,proto3" json:"host,omitempty"` + Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` + Header map[string]string `protobuf:"bytes,3,rep,name=header,proto3" json:"header,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + AcceptProxyProtocol bool `protobuf:"varint,4,opt,name=accept_proxy_protocol,json=acceptProxyProtocol,proto3" json:"accept_proxy_protocol,omitempty"` + Ed uint32 `protobuf:"varint,5,opt,name=ed,proto3" json:"ed,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_transport_internet_httpupgrade_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_httpupgrade_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_transport_internet_httpupgrade_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Config) GetHost() string { + if x != nil { + return x.Host + } + return "" +} + +func (x *Config) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *Config) GetHeader() map[string]string { + if x != nil { + return x.Header + } + return nil +} + +func (x *Config) GetAcceptProxyProtocol() bool { + if x != nil { + return x.AcceptProxyProtocol + } + return false +} + +func (x *Config) GetEd() uint32 { + if x != nil { + return x.Ed + } + return 0 +} + +var File_transport_internet_httpupgrade_config_proto protoreflect.FileDescriptor + +const file_transport_internet_httpupgrade_config_proto_rawDesc = "" + + "\n" + + "+transport/internet/httpupgrade/config.proto\x12#xray.transport.internet.httpupgrade\"\x80\x02\n" + + "\x06Config\x12\x12\n" + + "\x04host\x18\x01 \x01(\tR\x04host\x12\x12\n" + + "\x04path\x18\x02 \x01(\tR\x04path\x12O\n" + + "\x06header\x18\x03 \x03(\v27.xray.transport.internet.httpupgrade.Config.HeaderEntryR\x06header\x122\n" + + "\x15accept_proxy_protocol\x18\x04 \x01(\bR\x13acceptProxyProtocol\x12\x0e\n" + + "\x02ed\x18\x05 \x01(\rR\x02ed\x1a9\n" + + "\vHeaderEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\x8b\x01\n" + + "'com.xray.transport.internet.httpupgradeP\x01Z8github.com/xtls/xray-core/transport/internet/httpupgrade\xaa\x02#Xray.Transport.Internet.HttpUpgradeb\x06proto3" + +var ( + file_transport_internet_httpupgrade_config_proto_rawDescOnce sync.Once + file_transport_internet_httpupgrade_config_proto_rawDescData []byte +) + +func file_transport_internet_httpupgrade_config_proto_rawDescGZIP() []byte { + file_transport_internet_httpupgrade_config_proto_rawDescOnce.Do(func() { + file_transport_internet_httpupgrade_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_transport_internet_httpupgrade_config_proto_rawDesc), len(file_transport_internet_httpupgrade_config_proto_rawDesc))) + }) + return file_transport_internet_httpupgrade_config_proto_rawDescData +} + +var file_transport_internet_httpupgrade_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_transport_internet_httpupgrade_config_proto_goTypes = []any{ + (*Config)(nil), // 0: xray.transport.internet.httpupgrade.Config + nil, // 1: xray.transport.internet.httpupgrade.Config.HeaderEntry +} +var file_transport_internet_httpupgrade_config_proto_depIdxs = []int32{ + 1, // 0: xray.transport.internet.httpupgrade.Config.header:type_name -> xray.transport.internet.httpupgrade.Config.HeaderEntry + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_transport_internet_httpupgrade_config_proto_init() } +func file_transport_internet_httpupgrade_config_proto_init() { + if File_transport_internet_httpupgrade_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_httpupgrade_config_proto_rawDesc), len(file_transport_internet_httpupgrade_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_transport_internet_httpupgrade_config_proto_goTypes, + DependencyIndexes: file_transport_internet_httpupgrade_config_proto_depIdxs, + MessageInfos: file_transport_internet_httpupgrade_config_proto_msgTypes, + }.Build() + File_transport_internet_httpupgrade_config_proto = out.File + file_transport_internet_httpupgrade_config_proto_goTypes = nil + file_transport_internet_httpupgrade_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/transport/internet/httpupgrade/config.proto b/subproject/Xray-core-main/transport/internet/httpupgrade/config.proto new file mode 100644 index 00000000..98206f51 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/httpupgrade/config.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; + +package xray.transport.internet.httpupgrade; +option csharp_namespace = "Xray.Transport.Internet.HttpUpgrade"; +option go_package = "github.com/xtls/xray-core/transport/internet/httpupgrade"; +option java_package = "com.xray.transport.internet.httpupgrade"; +option java_multiple_files = true; + +message Config { + string host = 1; + string path = 2; + map header = 3; + bool accept_proxy_protocol = 4; + uint32 ed = 5; +} diff --git a/subproject/Xray-core-main/transport/internet/httpupgrade/connection.go b/subproject/Xray-core-main/transport/internet/httpupgrade/connection.go new file mode 100644 index 00000000..1bc4d755 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/httpupgrade/connection.go @@ -0,0 +1,19 @@ +package httpupgrade + +import "net" + +type connection struct { + net.Conn + remoteAddr net.Addr +} + +func newConnection(conn net.Conn, remoteAddr net.Addr) *connection { + return &connection{ + Conn: conn, + remoteAddr: remoteAddr, + } +} + +func (c *connection) RemoteAddr() net.Addr { + return c.remoteAddr +} diff --git a/subproject/Xray-core-main/transport/internet/httpupgrade/dialer.go b/subproject/Xray-core-main/transport/internet/httpupgrade/dialer.go new file mode 100644 index 00000000..571797f6 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/httpupgrade/dialer.go @@ -0,0 +1,143 @@ +package httpupgrade + +import ( + "bufio" + "context" + "net/http" + "net/url" + "strings" + + "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/utils" + "github.com/xtls/xray-core/transport/internet" + "github.com/xtls/xray-core/transport/internet/stat" + "github.com/xtls/xray-core/transport/internet/tls" +) + +type ConnRF struct { + net.Conn + Req *http.Request + First bool +} + +func (c *ConnRF) Read(b []byte) (int, error) { + if c.First { + c.First = false + // create reader capped to size of `b`, so it can be fully drained into + // `b` later with a single Read call + reader := bufio.NewReaderSize(c.Conn, len(b)) + resp, err := http.ReadResponse(reader, c.Req) // nolint:bodyclose + if err != nil { + return 0, err + } + if resp.Status != "101 Switching Protocols" || + strings.ToLower(resp.Header.Get("Upgrade")) != "websocket" || + strings.ToLower(resp.Header.Get("Connection")) != "upgrade" { + return 0, errors.New("unrecognized reply") + } + // drain remaining bufreader + return reader.Read(b[:reader.Buffered()]) + } + return c.Conn.Read(b) +} + +func dialhttpUpgrade(ctx context.Context, dest net.Destination, streamSettings *internet.MemoryStreamConfig) (net.Conn, error) { + transportConfiguration := streamSettings.ProtocolSettings.(*Config) + + pconn, err := internet.DialSystem(ctx, dest, streamSettings.SocketSettings) + if err != nil { + errors.LogErrorInner(ctx, err, "failed to dial to ", dest) + return nil, err + } + + if streamSettings.TcpmaskManager != nil { + newConn, err := streamSettings.TcpmaskManager.WrapConnClient(pconn) + if err != nil { + pconn.Close() + return nil, errors.New("mask err").Base(err) + } + pconn = newConn + } + + var conn net.Conn + var requestURL url.URL + tConfig := tls.ConfigFromStreamSettings(streamSettings) + if tConfig != nil { + tlsConfig := tConfig.GetTLSConfig(tls.WithDestination(dest), tls.WithNextProto("http/1.1")) + if fingerprint := tls.GetFingerprint(tConfig.Fingerprint); fingerprint != nil { + conn = tls.UClient(pconn, tlsConfig, fingerprint) + if err := conn.(*tls.UConn).WebsocketHandshakeContext(ctx); err != nil { + return nil, err + } + } else { + conn = tls.Client(pconn, tlsConfig) + } + requestURL.Scheme = "https" + } else { + conn = pconn + requestURL.Scheme = "http" + } + + requestURL.Host = transportConfiguration.Host + if requestURL.Host == "" && tConfig != nil { + requestURL.Host = tConfig.ServerName + } + if requestURL.Host == "" { + requestURL.Host = dest.Address.String() + } + requestURL.Path = transportConfiguration.GetNormalizedPath() + req := &http.Request{ + Method: http.MethodGet, + URL: &requestURL, + Header: make(http.Header), + } + for key, value := range transportConfiguration.Header { + AddHeader(req.Header, key, value) + } + utils.TryDefaultHeadersWith(req.Header, "ws") + req.Header.Set("Connection", "Upgrade") + req.Header.Set("Upgrade", "websocket") + + err = req.Write(conn) + if err != nil { + return nil, err + } + + connRF := &ConnRF{ + Conn: conn, + Req: req, + First: true, + } + + if transportConfiguration.Ed == 0 { + _, err = connRF.Read([]byte{}) + if err != nil { + return nil, err + } + } + + return connRF, nil +} + +// http.Header.Add() will convert headers to MIME header format. +// Some people don't like this because they want to send "Web*S*ocket". +// So we add a simple function to replace that method. +func AddHeader(header http.Header, key, value string) { + header[key] = append(header[key], value) +} + +func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.MemoryStreamConfig) (stat.Connection, error) { + errors.LogInfo(ctx, "creating connection to ", dest) + + conn, err := dialhttpUpgrade(ctx, dest, streamSettings) + if err != nil { + return nil, errors.New("failed to dial request to ", dest).Base(err) + } + return stat.Connection(conn), nil +} + +func init() { + common.Must(internet.RegisterTransportDialer(protocolName, Dial)) +} diff --git a/subproject/Xray-core-main/transport/internet/httpupgrade/httpupgrade.go b/subproject/Xray-core-main/transport/internet/httpupgrade/httpupgrade.go new file mode 100644 index 00000000..0a902398 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/httpupgrade/httpupgrade.go @@ -0,0 +1,3 @@ +package httpupgrade + +const protocolName = "httpupgrade" diff --git a/subproject/Xray-core-main/transport/internet/httpupgrade/httpupgrade_test.go b/subproject/Xray-core-main/transport/internet/httpupgrade/httpupgrade_test.go new file mode 100644 index 00000000..c0310b95 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/httpupgrade/httpupgrade_test.go @@ -0,0 +1,214 @@ +package httpupgrade_test + +import ( + "context" + "runtime" + "testing" + "time" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol/tls/cert" + "github.com/xtls/xray-core/testing/servers/tcp" + "github.com/xtls/xray-core/transport/internet" + . "github.com/xtls/xray-core/transport/internet/httpupgrade" + "github.com/xtls/xray-core/transport/internet/stat" + "github.com/xtls/xray-core/transport/internet/tls" +) + +func Test_listenHTTPUpgradeAndDial(t *testing.T) { + listenPort := tcp.PickPort() + listen, err := ListenHTTPUpgrade(context.Background(), net.LocalHostIP, listenPort, &internet.MemoryStreamConfig{ + ProtocolName: "httpupgrade", + ProtocolSettings: &Config{ + Path: "httpupgrade", + }, + }, func(conn stat.Connection) { + go func(c stat.Connection) { + defer c.Close() + + var b [1024]byte + c.SetReadDeadline(time.Now().Add(2 * time.Second)) + _, err := c.Read(b[:]) + if err != nil { + return + } + + common.Must2(c.Write([]byte("Response"))) + }(conn) + }) + common.Must(err) + + ctx := context.Background() + streamSettings := &internet.MemoryStreamConfig{ + ProtocolName: "httpupgrade", + ProtocolSettings: &Config{Path: "httpupgrade"}, + } + conn, err := Dial(ctx, net.TCPDestination(net.DomainAddress("localhost"), listenPort), streamSettings) + + common.Must(err) + _, err = conn.Write([]byte("Test connection 1")) + common.Must(err) + + var b [1024]byte + n, err := conn.Read(b[:]) + common.Must(err) + if string(b[:n]) != "Response" { + t.Error("response: ", string(b[:n])) + } + + common.Must(conn.Close()) + conn, err = Dial(ctx, net.TCPDestination(net.DomainAddress("localhost"), listenPort), streamSettings) + common.Must(err) + _, err = conn.Write([]byte("Test connection 2")) + common.Must(err) + n, err = conn.Read(b[:]) + common.Must(err) + if string(b[:n]) != "Response" { + t.Error("response: ", string(b[:n])) + } + common.Must(conn.Close()) + + common.Must(listen.Close()) +} + +func Test_listenHTTPUpgradeAndDialWithHeaders(t *testing.T) { + listenPort := tcp.PickPort() + listen, err := ListenHTTPUpgrade(context.Background(), net.LocalHostIP, listenPort, &internet.MemoryStreamConfig{ + ProtocolName: "httpupgrade", + ProtocolSettings: &Config{ + Path: "httpupgrade", + Header: map[string]string{ + "User-Agent": "Mozilla", + }, + }, + }, func(conn stat.Connection) { + go func(c stat.Connection) { + defer c.Close() + + var b [1024]byte + c.SetReadDeadline(time.Now().Add(2 * time.Second)) + _, err := c.Read(b[:]) + if err != nil { + return + } + + common.Must2(c.Write([]byte("Response"))) + }(conn) + }) + common.Must(err) + + ctx := context.Background() + streamSettings := &internet.MemoryStreamConfig{ + ProtocolName: "httpupgrade", + ProtocolSettings: &Config{Path: "httpupgrade"}, + } + conn, err := Dial(ctx, net.TCPDestination(net.DomainAddress("localhost"), listenPort), streamSettings) + + common.Must(err) + _, err = conn.Write([]byte("Test connection 1")) + common.Must(err) + + var b [1024]byte + n, err := conn.Read(b[:]) + common.Must(err) + if string(b[:n]) != "Response" { + t.Error("response: ", string(b[:n])) + } + + common.Must(conn.Close()) + conn, err = Dial(ctx, net.TCPDestination(net.DomainAddress("localhost"), listenPort), streamSettings) + common.Must(err) + _, err = conn.Write([]byte("Test connection 2")) + common.Must(err) + n, err = conn.Read(b[:]) + common.Must(err) + if string(b[:n]) != "Response" { + t.Error("response: ", string(b[:n])) + } + common.Must(conn.Close()) + + common.Must(listen.Close()) +} + +func TestDialWithRemoteAddr(t *testing.T) { + listenPort := tcp.PickPort() + listen, err := ListenHTTPUpgrade(context.Background(), net.LocalHostIP, listenPort, &internet.MemoryStreamConfig{ + ProtocolName: "httpupgrade", + ProtocolSettings: &Config{ + Path: "httpupgrade", + }, + }, func(conn stat.Connection) { + go func(c stat.Connection) { + defer c.Close() + + var b [1024]byte + _, err := c.Read(b[:]) + // common.Must(err) + if err != nil { + return + } + + _, err = c.Write([]byte(c.RemoteAddr().String())) + common.Must(err) + }(conn) + }) + common.Must(err) + + conn, err := Dial(context.Background(), net.TCPDestination(net.DomainAddress("localhost"), listenPort), &internet.MemoryStreamConfig{ + ProtocolName: "httpupgrade", + ProtocolSettings: &Config{Path: "httpupgrade", Header: map[string]string{"X-Forwarded-For": "1.1.1.1"}}, + }) + + common.Must(err) + _, err = conn.Write([]byte("Test connection 1")) + common.Must(err) + + var b [1024]byte + n, err := conn.Read(b[:]) + common.Must(err) + if string(b[:n]) != "1.1.1.1:0" { + t.Error("response: ", string(b[:n])) + } + + common.Must(listen.Close()) +} + +func Test_listenHTTPUpgradeAndDial_TLS(t *testing.T) { + listenPort := tcp.PickPort() + if runtime.GOARCH == "arm64" { + return + } + + start := time.Now() + + ct, ctHash := cert.MustGenerate(nil, cert.CommonName("localhost")) + + streamSettings := &internet.MemoryStreamConfig{ + ProtocolName: "httpupgrade", + ProtocolSettings: &Config{ + Path: "httpupgrades", + }, + SecurityType: "tls", + SecuritySettings: &tls.Config{ + Certificate: []*tls.Certificate{tls.ParseCertificate(ct)}, + PinnedPeerCertSha256: [][]byte{ctHash[:]}, + }, + } + listen, err := ListenHTTPUpgrade(context.Background(), net.LocalHostIP, listenPort, streamSettings, func(conn stat.Connection) { + go func() { + _ = conn.Close() + }() + }) + common.Must(err) + defer listen.Close() + + conn, err := Dial(context.Background(), net.TCPDestination(net.DomainAddress("localhost"), listenPort), streamSettings) + common.Must(err) + _ = conn.Close() + + end := time.Now() + if !end.Before(start.Add(time.Second * 5)) { + t.Error("end: ", end, " start: ", start) + } +} diff --git a/subproject/Xray-core-main/transport/internet/httpupgrade/hub.go b/subproject/Xray-core-main/transport/internet/httpupgrade/hub.go new file mode 100644 index 00000000..8e70ad08 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/httpupgrade/hub.go @@ -0,0 +1,171 @@ +package httpupgrade + +import ( + "bufio" + "context" + "crypto/tls" + "net/http" + "strings" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + http_proto "github.com/xtls/xray-core/common/protocol/http" + "github.com/xtls/xray-core/transport/internet" + "github.com/xtls/xray-core/transport/internet/stat" + v2tls "github.com/xtls/xray-core/transport/internet/tls" +) + +type server struct { + config *Config + addConn internet.ConnHandler + innnerListener net.Listener + socketSettings *internet.SocketConfig +} + +func (s *server) Close() error { + return s.innnerListener.Close() +} + +func (s *server) Addr() net.Addr { + return nil +} + +func (s *server) Handle(conn net.Conn) { + upgradedConn, err := s.upgrade(conn) + if err != nil { + common.CloseIfExists(conn) + errors.LogInfoInner(context.Background(), err, "failed to handle request") + return + } + s.addConn(upgradedConn) +} + +// upgrade execute a fake websocket upgrade process and return the available connection +func (s *server) upgrade(conn net.Conn) (stat.Connection, error) { + connReader := bufio.NewReader(conn) + req, err := http.ReadRequest(connReader) + if err != nil { + return nil, err + } + + if s.config != nil { + host := req.Host + if len(s.config.Host) > 0 && !internet.IsValidHTTPHost(host, s.config.Host) { + return nil, errors.New("bad host: ", host) + } + path := s.config.GetNormalizedPath() + if req.URL.Path != path { + return nil, errors.New("bad path: ", req.URL.Path) + } + } + + connection := strings.ToLower(req.Header.Get("Connection")) + upgrade := strings.ToLower(req.Header.Get("Upgrade")) + if connection != "upgrade" || upgrade != "websocket" { + return nil, errors.New("unrecognized request") + } + resp := &http.Response{ + Status: "101 Switching Protocols", + StatusCode: 101, + Proto: "HTTP/1.1", + ProtoMajor: 1, + ProtoMinor: 1, + Header: http.Header{}, + } + resp.Header.Set("Connection", "Upgrade") + resp.Header.Set("Upgrade", "websocket") + err = resp.Write(conn) + if err != nil { + return nil, err + } + + var forwardedAddrs []net.Address + if s.socketSettings != nil && len(s.socketSettings.TrustedXForwardedFor) > 0 { + for _, key := range s.socketSettings.TrustedXForwardedFor { + if len(req.Header.Values(key)) > 0 { + forwardedAddrs = http_proto.ParseXForwardedFor(req.Header) + break + } + } + } else { + forwardedAddrs = http_proto.ParseXForwardedFor(req.Header) + } + remoteAddr := conn.RemoteAddr() + if len(forwardedAddrs) > 0 && forwardedAddrs[0].Family().IsIP() { + remoteAddr = &net.TCPAddr{ + IP: forwardedAddrs[0].IP(), + Port: int(0), + } + } + + return stat.Connection(newConnection(conn, remoteAddr)), nil +} + +func (s *server) keepAccepting() { + for { + conn, err := s.innnerListener.Accept() + if err != nil { + return + } + go s.Handle(conn) + } +} + +func ListenHTTPUpgrade(ctx context.Context, address net.Address, port net.Port, streamSettings *internet.MemoryStreamConfig, addConn internet.ConnHandler) (internet.Listener, error) { + transportConfiguration := streamSettings.ProtocolSettings.(*Config) + if transportConfiguration != nil { + if streamSettings.SocketSettings == nil { + streamSettings.SocketSettings = &internet.SocketConfig{} + } + streamSettings.SocketSettings.AcceptProxyProtocol = transportConfiguration.AcceptProxyProtocol || streamSettings.SocketSettings.AcceptProxyProtocol + } + var listener net.Listener + var err error + if port == net.Port(0) { // unix + listener, err = internet.ListenSystem(ctx, &net.UnixAddr{ + Name: address.Domain(), + Net: "unix", + }, streamSettings.SocketSettings) + if err != nil { + return nil, errors.New("failed to listen unix domain socket(for HttpUpgrade) on ", address).Base(err) + } + errors.LogInfo(ctx, "listening unix domain socket(for HttpUpgrade) on ", address) + } else { // tcp + listener, err = internet.ListenSystem(ctx, &net.TCPAddr{ + IP: address.IP(), + Port: int(port), + }, streamSettings.SocketSettings) + if err != nil { + return nil, errors.New("failed to listen TCP(for HttpUpgrade) on ", address, ":", port).Base(err) + } + errors.LogInfo(ctx, "listening TCP(for HttpUpgrade) on ", address, ":", port) + } + + if streamSettings.TcpmaskManager != nil { + listener, _ = streamSettings.TcpmaskManager.WrapListener(listener) + } + + if streamSettings.SocketSettings != nil && streamSettings.SocketSettings.AcceptProxyProtocol { + errors.LogWarning(ctx, "accepting PROXY protocol") + } + + if config := v2tls.ConfigFromStreamSettings(streamSettings); config != nil { + if tlsConfig := config.GetTLSConfig(); tlsConfig != nil { + listener = tls.NewListener(listener, tlsConfig) + } + } + + serverInstance := &server{ + config: transportConfiguration, + addConn: addConn, + innnerListener: listener, + socketSettings: streamSettings.SocketSettings, + } + go serverInstance.keepAccepting() + return serverInstance, nil +} + +func init() { + common.Must(internet.RegisterTransportListener(protocolName, ListenHTTPUpgrade)) +} diff --git a/subproject/Xray-core-main/transport/internet/hysteria/config.go b/subproject/Xray-core-main/transport/internet/hysteria/config.go new file mode 100644 index 00000000..7636983f --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/hysteria/config.go @@ -0,0 +1,53 @@ +package hysteria + +import ( + "time" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/transport/internet" + "github.com/xtls/xray-core/transport/internet/hysteria/padding" +) + +const ( + closeErrCodeOK = 0x100 // HTTP3 ErrCodeNoError + closeErrCodeProtocolError = 0x101 // HTTP3 ErrCodeGeneralProtocolError + + MaxDatagramFrameSize = 1200 + + URLHost = "hysteria" + URLPath = "/auth" + + RequestHeaderAuth = "Hysteria-Auth" + ResponseHeaderUDPEnabled = "Hysteria-UDP" + CommonHeaderCCRX = "Hysteria-CC-RX" + CommonHeaderPadding = "Hysteria-Padding" + + StatusAuthOK = 233 + + udpMessageChanSize = 1024 + + FrameTypeTCPRequest = 0x401 + + idleCleanupInterval = 1 * time.Second +) + +var ( + authRequestPadding = padding.Padding{Min: 256, Max: 2048} + authResponsePadding = padding.Padding{Min: 256, Max: 2048} +) + +type Status int + +const ( + StatusUnknown Status = iota + StatusActive + StatusInactive +) + +const protocolName = "hysteria" + +func init() { + common.Must(internet.RegisterProtocolConfigCreator(protocolName, func() interface{} { + return new(Config) + })) +} diff --git a/subproject/Xray-core-main/transport/internet/hysteria/config.pb.go b/subproject/Xray-core-main/transport/internet/hysteria/config.pb.go new file mode 100644 index 00000000..7a0b02ca --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/hysteria/config.pb.go @@ -0,0 +1,220 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: transport/internet/hysteria/config.proto + +package hysteria + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + Version int32 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"` + Auth string `protobuf:"bytes,2,opt,name=auth,proto3" json:"auth,omitempty"` + UdpIdleTimeout int64 `protobuf:"varint,3,opt,name=udp_idle_timeout,json=udpIdleTimeout,proto3" json:"udp_idle_timeout,omitempty"` + MasqType string `protobuf:"bytes,4,opt,name=masq_type,json=masqType,proto3" json:"masq_type,omitempty"` + MasqFile string `protobuf:"bytes,5,opt,name=masq_file,json=masqFile,proto3" json:"masq_file,omitempty"` + MasqUrl string `protobuf:"bytes,6,opt,name=masq_url,json=masqUrl,proto3" json:"masq_url,omitempty"` + MasqUrlRewriteHost bool `protobuf:"varint,7,opt,name=masq_url_rewrite_host,json=masqUrlRewriteHost,proto3" json:"masq_url_rewrite_host,omitempty"` + MasqUrlInsecure bool `protobuf:"varint,8,opt,name=masq_url_insecure,json=masqUrlInsecure,proto3" json:"masq_url_insecure,omitempty"` + MasqString string `protobuf:"bytes,9,opt,name=masq_string,json=masqString,proto3" json:"masq_string,omitempty"` + MasqStringHeaders map[string]string `protobuf:"bytes,10,rep,name=masq_string_headers,json=masqStringHeaders,proto3" json:"masq_string_headers,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + MasqStringStatusCode int32 `protobuf:"varint,11,opt,name=masq_string_status_code,json=masqStringStatusCode,proto3" json:"masq_string_status_code,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_transport_internet_hysteria_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_hysteria_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_transport_internet_hysteria_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Config) GetVersion() int32 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *Config) GetAuth() string { + if x != nil { + return x.Auth + } + return "" +} + +func (x *Config) GetUdpIdleTimeout() int64 { + if x != nil { + return x.UdpIdleTimeout + } + return 0 +} + +func (x *Config) GetMasqType() string { + if x != nil { + return x.MasqType + } + return "" +} + +func (x *Config) GetMasqFile() string { + if x != nil { + return x.MasqFile + } + return "" +} + +func (x *Config) GetMasqUrl() string { + if x != nil { + return x.MasqUrl + } + return "" +} + +func (x *Config) GetMasqUrlRewriteHost() bool { + if x != nil { + return x.MasqUrlRewriteHost + } + return false +} + +func (x *Config) GetMasqUrlInsecure() bool { + if x != nil { + return x.MasqUrlInsecure + } + return false +} + +func (x *Config) GetMasqString() string { + if x != nil { + return x.MasqString + } + return "" +} + +func (x *Config) GetMasqStringHeaders() map[string]string { + if x != nil { + return x.MasqStringHeaders + } + return nil +} + +func (x *Config) GetMasqStringStatusCode() int32 { + if x != nil { + return x.MasqStringStatusCode + } + return 0 +} + +var File_transport_internet_hysteria_config_proto protoreflect.FileDescriptor + +const file_transport_internet_hysteria_config_proto_rawDesc = "" + + "\n" + + "(transport/internet/hysteria/config.proto\x12 xray.transport.internet.hysteria\"\xa3\x04\n" + + "\x06Config\x12\x18\n" + + "\aversion\x18\x01 \x01(\x05R\aversion\x12\x12\n" + + "\x04auth\x18\x02 \x01(\tR\x04auth\x12(\n" + + "\x10udp_idle_timeout\x18\x03 \x01(\x03R\x0eudpIdleTimeout\x12\x1b\n" + + "\tmasq_type\x18\x04 \x01(\tR\bmasqType\x12\x1b\n" + + "\tmasq_file\x18\x05 \x01(\tR\bmasqFile\x12\x19\n" + + "\bmasq_url\x18\x06 \x01(\tR\amasqUrl\x121\n" + + "\x15masq_url_rewrite_host\x18\a \x01(\bR\x12masqUrlRewriteHost\x12*\n" + + "\x11masq_url_insecure\x18\b \x01(\bR\x0fmasqUrlInsecure\x12\x1f\n" + + "\vmasq_string\x18\t \x01(\tR\n" + + "masqString\x12o\n" + + "\x13masq_string_headers\x18\n" + + " \x03(\v2?.xray.transport.internet.hysteria.Config.MasqStringHeadersEntryR\x11masqStringHeaders\x125\n" + + "\x17masq_string_status_code\x18\v \x01(\x05R\x14masqStringStatusCode\x1aD\n" + + "\x16MasqStringHeadersEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\x82\x01\n" + + "$com.xray.transport.internet.hysteriaP\x01Z5github.com/xtls/xray-core/transport/internet/hysteria\xaa\x02 Xray.Transport.Internet.Hysteriab\x06proto3" + +var ( + file_transport_internet_hysteria_config_proto_rawDescOnce sync.Once + file_transport_internet_hysteria_config_proto_rawDescData []byte +) + +func file_transport_internet_hysteria_config_proto_rawDescGZIP() []byte { + file_transport_internet_hysteria_config_proto_rawDescOnce.Do(func() { + file_transport_internet_hysteria_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_transport_internet_hysteria_config_proto_rawDesc), len(file_transport_internet_hysteria_config_proto_rawDesc))) + }) + return file_transport_internet_hysteria_config_proto_rawDescData +} + +var file_transport_internet_hysteria_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_transport_internet_hysteria_config_proto_goTypes = []any{ + (*Config)(nil), // 0: xray.transport.internet.hysteria.Config + nil, // 1: xray.transport.internet.hysteria.Config.MasqStringHeadersEntry +} +var file_transport_internet_hysteria_config_proto_depIdxs = []int32{ + 1, // 0: xray.transport.internet.hysteria.Config.masq_string_headers:type_name -> xray.transport.internet.hysteria.Config.MasqStringHeadersEntry + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_transport_internet_hysteria_config_proto_init() } +func file_transport_internet_hysteria_config_proto_init() { + if File_transport_internet_hysteria_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_hysteria_config_proto_rawDesc), len(file_transport_internet_hysteria_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_transport_internet_hysteria_config_proto_goTypes, + DependencyIndexes: file_transport_internet_hysteria_config_proto_depIdxs, + MessageInfos: file_transport_internet_hysteria_config_proto_msgTypes, + }.Build() + File_transport_internet_hysteria_config_proto = out.File + file_transport_internet_hysteria_config_proto_goTypes = nil + file_transport_internet_hysteria_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/transport/internet/hysteria/config.proto b/subproject/Xray-core-main/transport/internet/hysteria/config.proto new file mode 100644 index 00000000..3d039da8 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/hysteria/config.proto @@ -0,0 +1,22 @@ +syntax = "proto3"; + +package xray.transport.internet.hysteria; +option csharp_namespace = "Xray.Transport.Internet.Hysteria"; +option go_package = "github.com/xtls/xray-core/transport/internet/hysteria"; +option java_package = "com.xray.transport.internet.hysteria"; +option java_multiple_files = true; + +message Config { + int32 version = 1; + string auth = 2; + + int64 udp_idle_timeout = 3; + string masq_type = 4; + string masq_file = 5; + string masq_url = 6; + bool masq_url_rewrite_host = 7; + bool masq_url_insecure = 8; + string masq_string = 9; + map masq_string_headers = 10; + int32 masq_string_status_code = 11; +} \ No newline at end of file diff --git a/subproject/Xray-core-main/transport/internet/hysteria/congestion/bbr/bandwidth.go b/subproject/Xray-core-main/transport/internet/hysteria/congestion/bbr/bandwidth.go new file mode 100644 index 00000000..52deb249 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/hysteria/congestion/bbr/bandwidth.go @@ -0,0 +1,27 @@ +package bbr + +import ( + "math" + "time" + + "github.com/apernet/quic-go/congestion" +) + +const ( + infBandwidth = Bandwidth(math.MaxUint64) +) + +// Bandwidth of a connection +type Bandwidth uint64 + +const ( + // BitsPerSecond is 1 bit per second + BitsPerSecond Bandwidth = 1 + // BytesPerSecond is 1 byte per second + BytesPerSecond = 8 * BitsPerSecond +) + +// BandwidthFromDelta calculates the bandwidth from a number of bytes and a time delta +func BandwidthFromDelta(bytes congestion.ByteCount, delta time.Duration) Bandwidth { + return Bandwidth(bytes) * Bandwidth(time.Second) / Bandwidth(delta) * BytesPerSecond +} diff --git a/subproject/Xray-core-main/transport/internet/hysteria/congestion/bbr/bandwidth_sampler.go b/subproject/Xray-core-main/transport/internet/hysteria/congestion/bbr/bandwidth_sampler.go new file mode 100644 index 00000000..35b63c06 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/hysteria/congestion/bbr/bandwidth_sampler.go @@ -0,0 +1,875 @@ +package bbr + +import ( + "math" + "time" + + "github.com/apernet/quic-go/congestion" + "github.com/apernet/quic-go/monotime" +) + +const ( + infRTT = time.Duration(math.MaxInt64) + defaultConnectionStateMapQueueSize = 256 + defaultCandidatesBufferSize = 256 +) + +type roundTripCount uint64 + +// SendTimeState is a subset of ConnectionStateOnSentPacket which is returned +// to the caller when the packet is acked or lost. +type sendTimeState struct { + // Whether other states in this object is valid. + isValid bool + // Whether the sender is app limited at the time the packet was sent. + // App limited bandwidth sample might be artificially low because the sender + // did not have enough data to send in order to saturate the link. + isAppLimited bool + // Total number of sent bytes at the time the packet was sent. + // Includes the packet itself. + totalBytesSent congestion.ByteCount + // Total number of acked bytes at the time the packet was sent. + totalBytesAcked congestion.ByteCount + // Total number of lost bytes at the time the packet was sent. + totalBytesLost congestion.ByteCount + // Total number of inflight bytes at the time the packet was sent. + // Includes the packet itself. + // It should be equal to |total_bytes_sent| minus the sum of + // |total_bytes_acked|, |total_bytes_lost| and total neutered bytes. + bytesInFlight congestion.ByteCount +} + +func newSendTimeState( + isAppLimited bool, + totalBytesSent congestion.ByteCount, + totalBytesAcked congestion.ByteCount, + totalBytesLost congestion.ByteCount, + bytesInFlight congestion.ByteCount, +) *sendTimeState { + return &sendTimeState{ + isValid: true, + isAppLimited: isAppLimited, + totalBytesSent: totalBytesSent, + totalBytesAcked: totalBytesAcked, + totalBytesLost: totalBytesLost, + bytesInFlight: bytesInFlight, + } +} + +type extraAckedEvent struct { + // The excess bytes acknowlwedged in the time delta for this event. + extraAcked congestion.ByteCount + + // The bytes acknowledged and time delta from the event. + bytesAcked congestion.ByteCount + timeDelta time.Duration + // The round trip of the event. + round roundTripCount +} + +func maxExtraAckedEventFunc(a, b extraAckedEvent) int { + if a.extraAcked > b.extraAcked { + return 1 + } else if a.extraAcked < b.extraAcked { + return -1 + } + return 0 +} + +// BandwidthSample +type bandwidthSample struct { + // The bandwidth at that particular sample. Zero if no valid bandwidth sample + // is available. + bandwidth Bandwidth + // The RTT measurement at this particular sample. Zero if no RTT sample is + // available. Does not correct for delayed ack time. + rtt time.Duration + // |send_rate| is computed from the current packet being acked('P') and an + // earlier packet that is acked before P was sent. + sendRate Bandwidth + // States captured when the packet was sent. + stateAtSend sendTimeState +} + +func newBandwidthSample() *bandwidthSample { + return &bandwidthSample{ + sendRate: infBandwidth, + } +} + +// MaxAckHeightTracker is part of the BandwidthSampler. It is called after every +// ack event to keep track the degree of ack aggregation(a.k.a "ack height"). +type maxAckHeightTracker struct { + // Tracks the maximum number of bytes acked faster than the estimated + // bandwidth. + maxAckHeightFilter *WindowedFilter[extraAckedEvent, roundTripCount] + // The time this aggregation started and the number of bytes acked during it. + aggregationEpochStartTime monotime.Time + aggregationEpochBytes congestion.ByteCount + // The last sent packet number before the current aggregation epoch started. + lastSentPacketNumberBeforeEpoch congestion.PacketNumber + // The number of ack aggregation epochs ever started, including the ongoing + // one. Stats only. + numAckAggregationEpochs uint64 + ackAggregationBandwidthThreshold float64 + startNewAggregationEpochAfterFullRound bool + reduceExtraAckedOnBandwidthIncrease bool +} + +func newMaxAckHeightTracker(windowLength roundTripCount) *maxAckHeightTracker { + return &maxAckHeightTracker{ + maxAckHeightFilter: NewWindowedFilter(windowLength, maxExtraAckedEventFunc), + lastSentPacketNumberBeforeEpoch: invalidPacketNumber, + ackAggregationBandwidthThreshold: 1.0, + } +} + +func (m *maxAckHeightTracker) Get() congestion.ByteCount { + return m.maxAckHeightFilter.GetBest().extraAcked +} + +func (m *maxAckHeightTracker) Update( + bandwidthEstimate Bandwidth, + isNewMaxBandwidth bool, + roundTripCount roundTripCount, + lastSentPacketNumber congestion.PacketNumber, + lastAckedPacketNumber congestion.PacketNumber, + ackTime monotime.Time, + bytesAcked congestion.ByteCount, +) congestion.ByteCount { + forceNewEpoch := false + + if m.reduceExtraAckedOnBandwidthIncrease && isNewMaxBandwidth { + // Save and clear existing entries. + best := m.maxAckHeightFilter.GetBest() + secondBest := m.maxAckHeightFilter.GetSecondBest() + thirdBest := m.maxAckHeightFilter.GetThirdBest() + m.maxAckHeightFilter.Clear() + + // Reinsert the heights into the filter after recalculating. + expectedBytesAcked := bytesFromBandwidthAndTimeDelta(bandwidthEstimate, best.timeDelta) + if expectedBytesAcked < best.bytesAcked { + best.extraAcked = best.bytesAcked - expectedBytesAcked + m.maxAckHeightFilter.Update(best, best.round) + } + expectedBytesAcked = bytesFromBandwidthAndTimeDelta(bandwidthEstimate, secondBest.timeDelta) + if expectedBytesAcked < secondBest.bytesAcked { + secondBest.extraAcked = secondBest.bytesAcked - expectedBytesAcked + m.maxAckHeightFilter.Update(secondBest, secondBest.round) + } + expectedBytesAcked = bytesFromBandwidthAndTimeDelta(bandwidthEstimate, thirdBest.timeDelta) + if expectedBytesAcked < thirdBest.bytesAcked { + thirdBest.extraAcked = thirdBest.bytesAcked - expectedBytesAcked + m.maxAckHeightFilter.Update(thirdBest, thirdBest.round) + } + } + + // If any packet sent after the start of the epoch has been acked, start a new + // epoch. + if m.startNewAggregationEpochAfterFullRound && + m.lastSentPacketNumberBeforeEpoch != invalidPacketNumber && + lastAckedPacketNumber != invalidPacketNumber && + lastAckedPacketNumber > m.lastSentPacketNumberBeforeEpoch { + forceNewEpoch = true + } + if m.aggregationEpochStartTime.IsZero() || forceNewEpoch { + m.aggregationEpochBytes = bytesAcked + m.aggregationEpochStartTime = ackTime + m.lastSentPacketNumberBeforeEpoch = lastSentPacketNumber + m.numAckAggregationEpochs++ + return 0 + } + + // Compute how many bytes are expected to be delivered, assuming max bandwidth + // is correct. + aggregationDelta := ackTime.Sub(m.aggregationEpochStartTime) + expectedBytesAcked := bytesFromBandwidthAndTimeDelta(bandwidthEstimate, aggregationDelta) + // Reset the current aggregation epoch as soon as the ack arrival rate is less + // than or equal to the max bandwidth. + if m.aggregationEpochBytes <= congestion.ByteCount(m.ackAggregationBandwidthThreshold*float64(expectedBytesAcked)) { + // Reset to start measuring a new aggregation epoch. + m.aggregationEpochBytes = bytesAcked + m.aggregationEpochStartTime = ackTime + m.lastSentPacketNumberBeforeEpoch = lastSentPacketNumber + m.numAckAggregationEpochs++ + return 0 + } + + m.aggregationEpochBytes += bytesAcked + + // Compute how many extra bytes were delivered vs max bandwidth. + extraBytesAcked := m.aggregationEpochBytes - expectedBytesAcked + newEvent := extraAckedEvent{ + extraAcked: extraBytesAcked, + bytesAcked: m.aggregationEpochBytes, + timeDelta: aggregationDelta, + } + m.maxAckHeightFilter.Update(newEvent, roundTripCount) + return extraBytesAcked +} + +func (m *maxAckHeightTracker) SetFilterWindowLength(length roundTripCount) { + m.maxAckHeightFilter.SetWindowLength(length) +} + +func (m *maxAckHeightTracker) Reset(newHeight congestion.ByteCount, newTime roundTripCount) { + newEvent := extraAckedEvent{ + extraAcked: newHeight, + round: newTime, + } + m.maxAckHeightFilter.Reset(newEvent, newTime) +} + +func (m *maxAckHeightTracker) SetAckAggregationBandwidthThreshold(threshold float64) { + m.ackAggregationBandwidthThreshold = threshold +} + +func (m *maxAckHeightTracker) SetStartNewAggregationEpochAfterFullRound(value bool) { + m.startNewAggregationEpochAfterFullRound = value +} + +func (m *maxAckHeightTracker) SetReduceExtraAckedOnBandwidthIncrease(value bool) { + m.reduceExtraAckedOnBandwidthIncrease = value +} + +func (m *maxAckHeightTracker) AckAggregationBandwidthThreshold() float64 { + return m.ackAggregationBandwidthThreshold +} + +func (m *maxAckHeightTracker) NumAckAggregationEpochs() uint64 { + return m.numAckAggregationEpochs +} + +// AckPoint represents a point on the ack line. +type ackPoint struct { + ackTime monotime.Time + totalBytesAcked congestion.ByteCount +} + +// RecentAckPoints maintains the most recent 2 ack points at distinct times. +type recentAckPoints struct { + ackPoints [2]ackPoint +} + +func (r *recentAckPoints) Update(ackTime monotime.Time, totalBytesAcked congestion.ByteCount) { + if ackTime.Before(r.ackPoints[1].ackTime) { + r.ackPoints[1].ackTime = ackTime + } else if ackTime.After(r.ackPoints[1].ackTime) { + r.ackPoints[0] = r.ackPoints[1] + r.ackPoints[1].ackTime = ackTime + } + + r.ackPoints[1].totalBytesAcked = totalBytesAcked +} + +func (r *recentAckPoints) Clear() { + r.ackPoints[0] = ackPoint{} + r.ackPoints[1] = ackPoint{} +} + +func (r *recentAckPoints) MostRecentPoint() *ackPoint { + return &r.ackPoints[1] +} + +func (r *recentAckPoints) LessRecentPoint() *ackPoint { + if r.ackPoints[0].totalBytesAcked != 0 { + return &r.ackPoints[0] + } + + return &r.ackPoints[1] +} + +// ConnectionStateOnSentPacket represents the information about a sent packet +// and the state of the connection at the moment the packet was sent, +// specifically the information about the most recently acknowledged packet at +// that moment. +type connectionStateOnSentPacket struct { + // Time at which the packet is sent. + sentTime monotime.Time + // Size of the packet. + size congestion.ByteCount + // The value of |totalBytesSentAtLastAckedPacket| at the time the + // packet was sent. + totalBytesSentAtLastAckedPacket congestion.ByteCount + // The value of |lastAckedPacketSentTime| at the time the packet was + // sent. + lastAckedPacketSentTime monotime.Time + // The value of |lastAckedPacketAckTime| at the time the packet was + // sent. + lastAckedPacketAckTime monotime.Time + // Send time states that are returned to the congestion controller when the + // packet is acked or lost. + sendTimeState sendTimeState +} + +// Snapshot constructor. Records the current state of the bandwidth +// sampler. +// |bytes_in_flight| is the bytes in flight right after the packet is sent. +func newConnectionStateOnSentPacket( + sentTime monotime.Time, + size congestion.ByteCount, + bytesInFlight congestion.ByteCount, + sampler *bandwidthSampler, +) *connectionStateOnSentPacket { + return &connectionStateOnSentPacket{ + sentTime: sentTime, + size: size, + totalBytesSentAtLastAckedPacket: sampler.totalBytesSentAtLastAckedPacket, + lastAckedPacketSentTime: sampler.lastAckedPacketSentTime, + lastAckedPacketAckTime: sampler.lastAckedPacketAckTime, + sendTimeState: *newSendTimeState( + sampler.isAppLimited, + sampler.totalBytesSent, + sampler.totalBytesAcked, + sampler.totalBytesLost, + bytesInFlight, + ), + } +} + +// BandwidthSampler keeps track of sent and acknowledged packets and outputs a +// bandwidth sample for every packet acknowledged. The samples are taken for +// individual packets, and are not filtered; the consumer has to filter the +// bandwidth samples itself. In certain cases, the sampler will locally severely +// underestimate the bandwidth, hence a maximum filter with a size of at least +// one RTT is recommended. +// +// This class bases its samples on the slope of two curves: the number of bytes +// sent over time, and the number of bytes acknowledged as received over time. +// It produces a sample of both slopes for every packet that gets acknowledged, +// based on a slope between two points on each of the corresponding curves. Note +// that due to the packet loss, the number of bytes on each curve might get +// further and further away from each other, meaning that it is not feasible to +// compare byte values coming from different curves with each other. +// +// The obvious points for measuring slope sample are the ones corresponding to +// the packet that was just acknowledged. Let us denote them as S_1 (point at +// which the current packet was sent) and A_1 (point at which the current packet +// was acknowledged). However, taking a slope requires two points on each line, +// so estimating bandwidth requires picking a packet in the past with respect to +// which the slope is measured. +// +// For that purpose, BandwidthSampler always keeps track of the most recently +// acknowledged packet, and records it together with every outgoing packet. +// When a packet gets acknowledged (A_1), it has not only information about when +// it itself was sent (S_1), but also the information about the latest +// acknowledged packet right before it was sent (S_0 and A_0). +// +// Based on that data, send and ack rate are estimated as: +// +// send_rate = (bytes(S_1) - bytes(S_0)) / (time(S_1) - time(S_0)) +// ack_rate = (bytes(A_1) - bytes(A_0)) / (time(A_1) - time(A_0)) +// +// Here, the ack rate is intuitively the rate we want to treat as bandwidth. +// However, in certain cases (e.g. ack compression) the ack rate at a point may +// end up higher than the rate at which the data was originally sent, which is +// not indicative of the real bandwidth. Hence, we use the send rate as an upper +// bound, and the sample value is +// +// rate_sample = min(send_rate, ack_rate) +// +// An important edge case handled by the sampler is tracking the app-limited +// samples. There are multiple meaning of "app-limited" used interchangeably, +// hence it is important to understand and to be able to distinguish between +// them. +// +// Meaning 1: connection state. The connection is said to be app-limited when +// there is no outstanding data to send. This means that certain bandwidth +// samples in the future would not be an accurate indication of the link +// capacity, and it is important to inform consumer about that. Whenever +// connection becomes app-limited, the sampler is notified via OnAppLimited() +// method. +// +// Meaning 2: a phase in the bandwidth sampler. As soon as the bandwidth +// sampler becomes notified about the connection being app-limited, it enters +// app-limited phase. In that phase, all *sent* packets are marked as +// app-limited. Note that the connection itself does not have to be +// app-limited during the app-limited phase, and in fact it will not be +// (otherwise how would it send packets?). The boolean flag below indicates +// whether the sampler is in that phase. +// +// Meaning 3: a flag on the sent packet and on the sample. If a sent packet is +// sent during the app-limited phase, the resulting sample related to the +// packet will be marked as app-limited. +// +// With the terminology issue out of the way, let us consider the question of +// what kind of situation it addresses. +// +// Consider a scenario where we first send packets 1 to 20 at a regular +// bandwidth, and then immediately run out of data. After a few seconds, we send +// packets 21 to 60, and only receive ack for 21 between sending packets 40 and +// 41. In this case, when we sample bandwidth for packets 21 to 40, the S_0/A_0 +// we use to compute the slope is going to be packet 20, a few seconds apart +// from the current packet, hence the resulting estimate would be extremely low +// and not indicative of anything. Only at packet 41 the S_0/A_0 will become 21, +// meaning that the bandwidth sample would exclude the quiescence. +// +// Based on the analysis of that scenario, we implement the following rule: once +// OnAppLimited() is called, all sent packets will produce app-limited samples +// up until an ack for a packet that was sent after OnAppLimited() was called. +// Note that while the scenario above is not the only scenario when the +// connection is app-limited, the approach works in other cases too. + +type congestionEventSample struct { + // The maximum bandwidth sample from all acked packets. + // QuicBandwidth::Zero() if no samples are available. + sampleMaxBandwidth Bandwidth + // Whether |sample_max_bandwidth| is from a app-limited sample. + sampleIsAppLimited bool + // The minimum rtt sample from all acked packets. + // QuicTime::Delta::Infinite() if no samples are available. + sampleRtt time.Duration + // For each packet p in acked packets, this is the max value of INFLIGHT(p), + // where INFLIGHT(p) is the number of bytes acked while p is inflight. + sampleMaxInflight congestion.ByteCount + // The send state of the largest packet in acked_packets, unless it is + // empty. If acked_packets is empty, it's the send state of the largest + // packet in lost_packets. + lastPacketSendState sendTimeState + // The number of extra bytes acked from this ack event, compared to what is + // expected from the flow's bandwidth. Larger value means more ack + // aggregation. + extraAcked congestion.ByteCount +} + +func newCongestionEventSample() *congestionEventSample { + return &congestionEventSample{ + sampleRtt: infRTT, + } +} + +type bandwidthSampler struct { + // The total number of congestion controlled bytes sent during the connection. + totalBytesSent congestion.ByteCount + + // The total number of congestion controlled bytes which were acknowledged. + totalBytesAcked congestion.ByteCount + + // The total number of congestion controlled bytes which were lost. + totalBytesLost congestion.ByteCount + + // The total number of congestion controlled bytes which have been neutered. + totalBytesNeutered congestion.ByteCount + + // The value of |total_bytes_sent_| at the time the last acknowledged packet + // was sent. Valid only when |last_acked_packet_sent_time_| is valid. + totalBytesSentAtLastAckedPacket congestion.ByteCount + + // The time at which the last acknowledged packet was sent. Set to + // QuicTime::Zero() if no valid timestamp is available. + lastAckedPacketSentTime monotime.Time + + // The time at which the most recent packet was acknowledged. + lastAckedPacketAckTime monotime.Time + + // The most recently sent packet. + lastSentPacket congestion.PacketNumber + + // The most recently acked packet. + lastAckedPacket congestion.PacketNumber + + // Indicates whether the bandwidth sampler is currently in an app-limited + // phase. + isAppLimited bool + + // The packet that will be acknowledged after this one will cause the sampler + // to exit the app-limited phase. + endOfAppLimitedPhase congestion.PacketNumber + + // Record of the connection state at the point where each packet in flight was + // sent, indexed by the packet number. + connectionStateMap *packetNumberIndexedQueue[connectionStateOnSentPacket] + + recentAckPoints recentAckPoints + a0Candidates RingBuffer[ackPoint] + + // Maximum number of tracked packets. + maxTrackedPackets congestion.ByteCount + + maxAckHeightTracker *maxAckHeightTracker + totalBytesAckedAfterLastAckEvent congestion.ByteCount + + // True if connection option 'BSAO' is set. + overestimateAvoidance bool + + // True if connection option 'BBRB' is set. + limitMaxAckHeightTrackerBySendRate bool +} + +func newBandwidthSampler(maxAckHeightTrackerWindowLength roundTripCount) *bandwidthSampler { + b := &bandwidthSampler{ + maxAckHeightTracker: newMaxAckHeightTracker(maxAckHeightTrackerWindowLength), + connectionStateMap: newPacketNumberIndexedQueue[connectionStateOnSentPacket](defaultConnectionStateMapQueueSize), + lastSentPacket: invalidPacketNumber, + lastAckedPacket: invalidPacketNumber, + endOfAppLimitedPhase: invalidPacketNumber, + } + + b.a0Candidates.Init(defaultCandidatesBufferSize) + + return b +} + +func (b *bandwidthSampler) MaxAckHeight() congestion.ByteCount { + return b.maxAckHeightTracker.Get() +} + +func (b *bandwidthSampler) NumAckAggregationEpochs() uint64 { + return b.maxAckHeightTracker.NumAckAggregationEpochs() +} + +func (b *bandwidthSampler) SetMaxAckHeightTrackerWindowLength(length roundTripCount) { + b.maxAckHeightTracker.SetFilterWindowLength(length) +} + +func (b *bandwidthSampler) ResetMaxAckHeightTracker(newHeight congestion.ByteCount, newTime roundTripCount) { + b.maxAckHeightTracker.Reset(newHeight, newTime) +} + +func (b *bandwidthSampler) SetStartNewAggregationEpochAfterFullRound(value bool) { + b.maxAckHeightTracker.SetStartNewAggregationEpochAfterFullRound(value) +} + +func (b *bandwidthSampler) SetLimitMaxAckHeightTrackerBySendRate(value bool) { + b.limitMaxAckHeightTrackerBySendRate = value +} + +func (b *bandwidthSampler) SetReduceExtraAckedOnBandwidthIncrease(value bool) { + b.maxAckHeightTracker.SetReduceExtraAckedOnBandwidthIncrease(value) +} + +func (b *bandwidthSampler) EnableOverestimateAvoidance() { + if b.overestimateAvoidance { + return + } + + b.overestimateAvoidance = true + b.maxAckHeightTracker.SetAckAggregationBandwidthThreshold(2.0) +} + +func (b *bandwidthSampler) IsOverestimateAvoidanceEnabled() bool { + return b.overestimateAvoidance +} + +func (b *bandwidthSampler) OnPacketSent( + sentTime monotime.Time, + packetNumber congestion.PacketNumber, + bytes congestion.ByteCount, + bytesInFlight congestion.ByteCount, + isRetransmittable bool, +) { + b.lastSentPacket = packetNumber + + if !isRetransmittable { + return + } + + b.totalBytesSent += bytes + + // If there are no packets in flight, the time at which the new transmission + // opens can be treated as the A_0 point for the purpose of bandwidth + // sampling. This underestimates bandwidth to some extent, and produces some + // artificially low samples for most packets in flight, but it provides with + // samples at important points where we would not have them otherwise, most + // importantly at the beginning of the connection. + if bytesInFlight == 0 { + b.lastAckedPacketAckTime = sentTime + if b.overestimateAvoidance { + b.recentAckPoints.Clear() + b.recentAckPoints.Update(sentTime, b.totalBytesAcked) + b.a0Candidates.Clear() + b.a0Candidates.PushBack(*b.recentAckPoints.MostRecentPoint()) + } + b.totalBytesSentAtLastAckedPacket = b.totalBytesSent + + // In this situation ack compression is not a concern, set send rate to + // effectively infinite. + b.lastAckedPacketSentTime = sentTime + } + + b.connectionStateMap.Emplace(packetNumber, newConnectionStateOnSentPacket( + sentTime, + bytes, + bytesInFlight+bytes, + b, + )) +} + +func (b *bandwidthSampler) OnCongestionEvent( + ackTime monotime.Time, + ackedPackets []congestion.AckedPacketInfo, + lostPackets []congestion.LostPacketInfo, + maxBandwidth Bandwidth, + estBandwidthUpperBound Bandwidth, + roundTripCount roundTripCount, +) congestionEventSample { + eventSample := newCongestionEventSample() + + var lastLostPacketSendState sendTimeState + + for _, p := range lostPackets { + sendState := b.OnPacketLost(p.PacketNumber, p.BytesLost) + if sendState.isValid { + lastLostPacketSendState = sendState + } + } + + if len(ackedPackets) == 0 { + // Only populate send state for a loss-only event. + eventSample.lastPacketSendState = lastLostPacketSendState + return *eventSample + } + + var lastAckedPacketSendState sendTimeState + var maxSendRate Bandwidth + + for _, p := range ackedPackets { + sample := b.onPacketAcknowledged(ackTime, p.PacketNumber) + if !sample.stateAtSend.isValid { + continue + } + + lastAckedPacketSendState = sample.stateAtSend + + if sample.rtt != 0 { + eventSample.sampleRtt = min(eventSample.sampleRtt, sample.rtt) + } + if sample.bandwidth > eventSample.sampleMaxBandwidth { + eventSample.sampleMaxBandwidth = sample.bandwidth + eventSample.sampleIsAppLimited = sample.stateAtSend.isAppLimited + } + if sample.sendRate != infBandwidth { + maxSendRate = max(maxSendRate, sample.sendRate) + } + inflightSample := b.totalBytesAcked - lastAckedPacketSendState.totalBytesAcked + if inflightSample > eventSample.sampleMaxInflight { + eventSample.sampleMaxInflight = inflightSample + } + } + + if !lastLostPacketSendState.isValid { + eventSample.lastPacketSendState = lastAckedPacketSendState + } else if !lastAckedPacketSendState.isValid { + eventSample.lastPacketSendState = lastLostPacketSendState + } else { + // If two packets are inflight and an alarm is armed to lose a packet and it + // wakes up late, then the first of two in flight packets could have been + // acknowledged before the wakeup, which re-evaluates loss detection, and + // could declare the later of the two lost. + if lostPackets[len(lostPackets)-1].PacketNumber > ackedPackets[len(ackedPackets)-1].PacketNumber { + eventSample.lastPacketSendState = lastLostPacketSendState + } else { + eventSample.lastPacketSendState = lastAckedPacketSendState + } + } + + isNewMaxBandwidth := eventSample.sampleMaxBandwidth > maxBandwidth + maxBandwidth = max(maxBandwidth, eventSample.sampleMaxBandwidth) + if b.limitMaxAckHeightTrackerBySendRate { + maxBandwidth = max(maxBandwidth, maxSendRate) + } + + eventSample.extraAcked = b.onAckEventEnd(min(estBandwidthUpperBound, maxBandwidth), isNewMaxBandwidth, roundTripCount) + + return *eventSample +} + +func (b *bandwidthSampler) OnPacketLost(packetNumber congestion.PacketNumber, bytesLost congestion.ByteCount) (s sendTimeState) { + b.totalBytesLost += bytesLost + if sentPacketPointer := b.connectionStateMap.GetEntry(packetNumber); sentPacketPointer != nil { + sentPacketToSendTimeState(sentPacketPointer, &s) + } + return s +} + +func (b *bandwidthSampler) OnPacketNeutered(packetNumber congestion.PacketNumber) { + b.connectionStateMap.Remove(packetNumber, func(sentPacket connectionStateOnSentPacket) { + b.totalBytesNeutered += sentPacket.size + }) +} + +func (b *bandwidthSampler) OnAppLimited() { + b.isAppLimited = true + b.endOfAppLimitedPhase = b.lastSentPacket +} + +func (b *bandwidthSampler) RemoveObsoletePackets(leastUnacked congestion.PacketNumber) { + // A packet can become obsolete when it is removed from QuicUnackedPacketMap's + // view of inflight before it is acked or marked as lost. For example, when + // QuicSentPacketManager::RetransmitCryptoPackets retransmits a crypto packet, + // the packet is removed from QuicUnackedPacketMap's inflight, but is not + // marked as acked or lost in the BandwidthSampler. + b.connectionStateMap.RemoveUpTo(leastUnacked) +} + +func (b *bandwidthSampler) TotalBytesSent() congestion.ByteCount { + return b.totalBytesSent +} + +func (b *bandwidthSampler) TotalBytesLost() congestion.ByteCount { + return b.totalBytesLost +} + +func (b *bandwidthSampler) TotalBytesAcked() congestion.ByteCount { + return b.totalBytesAcked +} + +func (b *bandwidthSampler) TotalBytesNeutered() congestion.ByteCount { + return b.totalBytesNeutered +} + +func (b *bandwidthSampler) IsAppLimited() bool { + return b.isAppLimited +} + +func (b *bandwidthSampler) EndOfAppLimitedPhase() congestion.PacketNumber { + return b.endOfAppLimitedPhase +} + +func (b *bandwidthSampler) max_ack_height() congestion.ByteCount { + return b.maxAckHeightTracker.Get() +} + +func (b *bandwidthSampler) chooseA0Point(totalBytesAcked congestion.ByteCount, a0 *ackPoint) bool { + if b.a0Candidates.Empty() { + return false + } + + if b.a0Candidates.Len() == 1 { + *a0 = *b.a0Candidates.Front() + return true + } + + for i := 1; i < b.a0Candidates.Len(); i++ { + if b.a0Candidates.Offset(i).totalBytesAcked > totalBytesAcked { + *a0 = *b.a0Candidates.Offset(i - 1) + if i > 1 { + for j := 0; j < i-1; j++ { + b.a0Candidates.PopFront() + } + } + return true + } + } + + *a0 = *b.a0Candidates.Back() + for k := 0; k < b.a0Candidates.Len()-1; k++ { + b.a0Candidates.PopFront() + } + return true +} + +func (b *bandwidthSampler) onPacketAcknowledged(ackTime monotime.Time, packetNumber congestion.PacketNumber) bandwidthSample { + sample := newBandwidthSample() + b.lastAckedPacket = packetNumber + sentPacketPointer := b.connectionStateMap.GetEntry(packetNumber) + if sentPacketPointer == nil { + return *sample + } + + // OnPacketAcknowledgedInner + b.totalBytesAcked += sentPacketPointer.size + b.totalBytesSentAtLastAckedPacket = sentPacketPointer.sendTimeState.totalBytesSent + b.lastAckedPacketSentTime = sentPacketPointer.sentTime + b.lastAckedPacketAckTime = ackTime + if b.overestimateAvoidance { + b.recentAckPoints.Update(ackTime, b.totalBytesAcked) + } + + if b.isAppLimited { + // Exit app-limited phase in two cases: + // (1) end_of_app_limited_phase_ is not initialized, i.e., so far all + // packets are sent while there are buffered packets or pending data. + // (2) The current acked packet is after the sent packet marked as the end + // of the app limit phase. + if b.endOfAppLimitedPhase == invalidPacketNumber || + packetNumber > b.endOfAppLimitedPhase { + b.isAppLimited = false + } + } + + // There might have been no packets acknowledged at the moment when the + // current packet was sent. In that case, there is no bandwidth sample to + // make. + if sentPacketPointer.lastAckedPacketSentTime.IsZero() { + return *sample + } + + // Infinite rate indicates that the sampler is supposed to discard the + // current send rate sample and use only the ack rate. + sendRate := infBandwidth + if sentPacketPointer.sentTime.After(sentPacketPointer.lastAckedPacketSentTime) { + sendRate = BandwidthFromDelta( + sentPacketPointer.sendTimeState.totalBytesSent-sentPacketPointer.totalBytesSentAtLastAckedPacket, + sentPacketPointer.sentTime.Sub(sentPacketPointer.lastAckedPacketSentTime)) + } + + var a0 ackPoint + if b.overestimateAvoidance && b.chooseA0Point(sentPacketPointer.sendTimeState.totalBytesAcked, &a0) { + } else { + a0.ackTime = sentPacketPointer.lastAckedPacketAckTime + a0.totalBytesAcked = sentPacketPointer.sendTimeState.totalBytesAcked + } + + // During the slope calculation, ensure that ack time of the current packet is + // always larger than the time of the previous packet, otherwise division by + // zero or integer underflow can occur. + if ackTime.Sub(a0.ackTime) <= 0 { + return *sample + } + + ackRate := BandwidthFromDelta(b.totalBytesAcked-a0.totalBytesAcked, ackTime.Sub(a0.ackTime)) + + sample.bandwidth = min(sendRate, ackRate) + // Note: this sample does not account for delayed acknowledgement time. This + // means that the RTT measurements here can be artificially high, especially + // on low bandwidth connections. + sample.rtt = ackTime.Sub(sentPacketPointer.sentTime) + sample.sendRate = sendRate + sentPacketToSendTimeState(sentPacketPointer, &sample.stateAtSend) + + return *sample +} + +func (b *bandwidthSampler) onAckEventEnd( + bandwidthEstimate Bandwidth, + isNewMaxBandwidth bool, + roundTripCount roundTripCount, +) congestion.ByteCount { + newlyAckedBytes := b.totalBytesAcked - b.totalBytesAckedAfterLastAckEvent + if newlyAckedBytes == 0 { + return 0 + } + b.totalBytesAckedAfterLastAckEvent = b.totalBytesAcked + extraAcked := b.maxAckHeightTracker.Update( + bandwidthEstimate, + isNewMaxBandwidth, + roundTripCount, + b.lastSentPacket, + b.lastAckedPacket, + b.lastAckedPacketAckTime, + newlyAckedBytes) + // If |extra_acked| is zero, i.e. this ack event marks the start of a new ack + // aggregation epoch, save LessRecentPoint, which is the last ack point of the + // previous epoch, as a A0 candidate. + if b.overestimateAvoidance && extraAcked == 0 { + b.a0Candidates.PushBack(*b.recentAckPoints.LessRecentPoint()) + } + return extraAcked +} + +func sentPacketToSendTimeState(sentPacket *connectionStateOnSentPacket, sendTimeState *sendTimeState) { + *sendTimeState = sentPacket.sendTimeState + sendTimeState.isValid = true +} + +// BytesFromBandwidthAndTimeDelta calculates the bytes +// from a bandwidth(bits per second) and a time delta +func bytesFromBandwidthAndTimeDelta(bandwidth Bandwidth, delta time.Duration) congestion.ByteCount { + return (congestion.ByteCount(bandwidth) * congestion.ByteCount(delta)) / + (congestion.ByteCount(time.Second) * 8) +} + +func timeDeltaFromBytesAndBandwidth(bytes congestion.ByteCount, bandwidth Bandwidth) time.Duration { + return time.Duration(bytes*8) * time.Second / time.Duration(bandwidth) +} diff --git a/subproject/Xray-core-main/transport/internet/hysteria/congestion/bbr/bbr_sender.go b/subproject/Xray-core-main/transport/internet/hysteria/congestion/bbr/bbr_sender.go new file mode 100644 index 00000000..bcbf8133 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/hysteria/congestion/bbr/bbr_sender.go @@ -0,0 +1,1087 @@ +package bbr + +import ( + "fmt" + "math/rand" + "net" + "os" + "strconv" + "strings" + "time" + + "github.com/apernet/quic-go/congestion" + "github.com/apernet/quic-go/monotime" + + "github.com/xtls/xray-core/transport/internet/hysteria/congestion/common" +) + +// BbrSender implements BBR congestion control algorithm. BBR aims to estimate +// the current available Bottleneck Bandwidth and RTT (hence the name), and +// regulates the pacing rate and the size of the congestion window based on +// those signals. +// +// BBR relies on pacing in order to function properly. Do not use BBR when +// pacing is disabled. +// + +const ( + minBps = 65536 // 64 KB/s + + invalidPacketNumber = -1 + initialCongestionWindowPackets = 32 + minCongestionWindowPackets = 4 + + // Constants based on TCP defaults. + // The minimum CWND to ensure delayed acks don't reduce bandwidth measurements. + // Does not inflate the pacing rate. + // The gain used for the STARTUP, equal to 2/ln(2). + defaultHighGain = 2.885 + // The newly derived CWND gain for STARTUP, 2. + derivedHighCWNDGain = 2.0 + + debugEnv = "HYSTERIA_BBR_DEBUG" +) + +// The cycle of gains used during the PROBE_BW stage. +var pacingGain = [...]float64{1.25, 0.75, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0} + +const ( + // The length of the gain cycle. + gainCycleLength = len(pacingGain) + // The size of the bandwidth filter window, in round-trips. + bandwidthWindowSize = gainCycleLength + 2 + + // The time after which the current min_rtt value expires. + minRttExpiry = 10 * time.Second + // The minimum time the connection can spend in PROBE_RTT mode. + probeRttTime = 200 * time.Millisecond + // If the bandwidth does not increase by the factor of |kStartupGrowthTarget| + // within |kRoundTripsWithoutGrowthBeforeExitingStartup| rounds, the connection + // will exit the STARTUP mode. + startupGrowthTarget = 1.25 + roundTripsWithoutGrowthBeforeExitingStartup = int64(3) + + // Flag. + defaultStartupFullLossCount = 8 + quicBbr2DefaultLossThreshold = 0.02 +) + +type bbrMode int + +const ( + // Startup phase of the connection. + bbrModeStartup = iota + // After achieving the highest possible bandwidth during the startup, lower + // the pacing rate in order to drain the queue. + bbrModeDrain + // Cruising mode. + bbrModeProbeBw + // Temporarily slow down sending in order to empty the buffer and measure + // the real minimum RTT. + bbrModeProbeRtt +) + +// Indicates how the congestion control limits the amount of bytes in flight. +type bbrRecoveryState int + +const ( + // Do not limit. + bbrRecoveryStateNotInRecovery = iota + // Allow an extra outstanding byte for each byte acknowledged. + bbrRecoveryStateConservation + // Allow two extra outstanding bytes for each byte acknowledged (slow + // start). + bbrRecoveryStateGrowth +) + +type Profile string + +const ( + ProfileConservative Profile = "conservative" + ProfileStandard Profile = "standard" + ProfileAggressive Profile = "aggressive" +) + +type profileConfig struct { + highGain float64 + highCwndGain float64 + congestionWindowGainConstant float64 + numStartupRtts int64 + drainToTarget bool + detectOvershooting bool + bytesLostMultiplier uint8 + enableAckAggregationStartup bool + expireAckAggregationStartup bool + enableOverestimateAvoidance bool + reduceExtraAckedOnBandwidthIncrease bool +} + +func ParseProfile(profile string) (Profile, error) { + switch normalized := strings.ToLower(profile); normalized { + case "", string(ProfileStandard): + return ProfileStandard, nil + case string(ProfileConservative): + return ProfileConservative, nil + case string(ProfileAggressive): + return ProfileAggressive, nil + default: + return "", fmt.Errorf("unsupported BBR profile %q", profile) + } +} + +func configForProfile(profile Profile) profileConfig { + switch profile { + case ProfileConservative: + return profileConfig{ + highGain: 2.25, + highCwndGain: 1.75, + congestionWindowGainConstant: 1.75, + numStartupRtts: 2, + drainToTarget: true, + detectOvershooting: true, + bytesLostMultiplier: 1, + enableOverestimateAvoidance: true, + reduceExtraAckedOnBandwidthIncrease: true, + } + case ProfileAggressive: + return profileConfig{ + highGain: 3.0, + highCwndGain: 2.25, + congestionWindowGainConstant: 2.5, + numStartupRtts: 4, + bytesLostMultiplier: 2, + enableAckAggregationStartup: true, + expireAckAggregationStartup: true, + } + default: + return profileConfig{ + highGain: defaultHighGain, + highCwndGain: derivedHighCWNDGain, + congestionWindowGainConstant: 2.0, + numStartupRtts: roundTripsWithoutGrowthBeforeExitingStartup, + bytesLostMultiplier: 2, + } + } +} + +type bbrSender struct { + rttStats congestion.RTTStatsProvider + clock Clock + pacer *common.Pacer + + mode bbrMode + + // Bandwidth sampler provides BBR with the bandwidth measurements at + // individual points. + sampler *bandwidthSampler + + // The number of the round trips that have occurred during the connection. + roundTripCount roundTripCount + + // The packet number of the most recently sent packet. + lastSentPacket congestion.PacketNumber + // Acknowledgement of any packet after |current_round_trip_end_| will cause + // the round trip counter to advance. + currentRoundTripEnd congestion.PacketNumber + + // Number of congestion events with some losses, in the current round. + numLossEventsInRound uint64 + + // Number of total bytes lost in the current round. + bytesLostInRound congestion.ByteCount + + // The filter that tracks the maximum bandwidth over the multiple recent + // round-trips. + maxBandwidth *WindowedFilter[Bandwidth, roundTripCount] + + // Minimum RTT estimate. Automatically expires within 10 seconds (and + // triggers PROBE_RTT mode) if no new value is sampled during that period. + minRtt time.Duration + // The time at which the current value of |min_rtt_| was assigned. + minRttTimestamp monotime.Time + + // The maximum allowed number of bytes in flight. + congestionWindow congestion.ByteCount + + // The initial value of the |congestion_window_|. + initialCongestionWindow congestion.ByteCount + + // The largest value the |congestion_window_| can achieve. + maxCongestionWindow congestion.ByteCount + + // The smallest value the |congestion_window_| can achieve. + minCongestionWindow congestion.ByteCount + + // The BBR profile used by the sender. + profile Profile + + // The pacing gain applied during the STARTUP phase. + highGain float64 + + // The CWND gain applied during the STARTUP phase. + highCwndGain float64 + + // The pacing gain applied during the DRAIN phase. + drainGain float64 + + // The current pacing rate of the connection. + pacingRate Bandwidth + + // The gain currently applied to the pacing rate. + pacingGain float64 + // The gain currently applied to the congestion window. + congestionWindowGain float64 + + // The gain used for the congestion window during PROBE_BW. Latched from + // quic_bbr_cwnd_gain flag. + congestionWindowGainConstant float64 + // The number of RTTs to stay in STARTUP mode. Defaults to 3. + numStartupRtts int64 + + // Number of round-trips in PROBE_BW mode, used for determining the current + // pacing gain cycle. + cycleCurrentOffset int + // The time at which the last pacing gain cycle was started. + lastCycleStart monotime.Time + + // Indicates whether the connection has reached the full bandwidth mode. + isAtFullBandwidth bool + // Number of rounds during which there was no significant bandwidth increase. + roundsWithoutBandwidthGain int64 + // The bandwidth compared to which the increase is measured. + bandwidthAtLastRound Bandwidth + + // Set to true upon exiting quiescence. + exitingQuiescence bool + + // Time at which PROBE_RTT has to be exited. Setting it to zero indicates + // that the time is yet unknown as the number of packets in flight has not + // reached the required value. + exitProbeRttAt monotime.Time + // Indicates whether a round-trip has passed since PROBE_RTT became active. + probeRttRoundPassed bool + + // Indicates whether the most recent bandwidth sample was marked as + // app-limited. + lastSampleIsAppLimited bool + // Indicates whether any non app-limited samples have been recorded. + hasNoAppLimitedSample bool + + // Current state of recovery. + recoveryState bbrRecoveryState + // Receiving acknowledgement of a packet after |end_recovery_at_| will cause + // BBR to exit the recovery mode. A value above zero indicates at least one + // loss has been detected, so it must not be set back to zero. + endRecoveryAt congestion.PacketNumber + // A window used to limit the number of bytes in flight during loss recovery. + recoveryWindow congestion.ByteCount + // If true, consider all samples in recovery app-limited. + isAppLimitedRecovery bool // not used + + // When true, pace at 1.5x and disable packet conservation in STARTUP. + slowerStartup bool // not used + // When true, disables packet conservation in STARTUP. + rateBasedStartup bool // not used + + // When true, add the most recent ack aggregation measurement during STARTUP. + enableAckAggregationDuringStartup bool + // When true, expire the windowed ack aggregation values in STARTUP when + // bandwidth increases more than 25%. + expireAckAggregationInStartup bool + + // If true, will not exit low gain mode until bytes_in_flight drops below BDP + // or it's time for high gain mode. + drainToTarget bool + + // If true, slow down pacing rate in STARTUP when overshooting is detected. + detectOvershooting bool + // Bytes lost while detect_overshooting_ is true. + bytesLostWhileDetectingOvershooting congestion.ByteCount + // Slow down pacing rate if + // bytes_lost_while_detecting_overshooting_ * + // bytes_lost_multiplier_while_detecting_overshooting_ > IW. + bytesLostMultiplierWhileDetectingOvershooting uint8 + // When overshooting is detected, do not drop pacing_rate_ below this value / + // min_rtt. + cwndToCalculateMinPacingRate congestion.ByteCount + + // Max congestion window when adjusting network parameters. + maxCongestionWindowWithNetworkParametersAdjusted congestion.ByteCount // not used + + // Params. + maxDatagramSize congestion.ByteCount + // Recorded on packet sent. equivalent |unacked_packets_->bytes_in_flight()| + bytesInFlight congestion.ByteCount + + debug bool +} + +var _ congestion.CongestionControl = &bbrSender{} + +func NewBbrSender( + clock Clock, + initialMaxDatagramSize congestion.ByteCount, + profile Profile, +) *bbrSender { + return newBbrSender( + clock, + initialMaxDatagramSize, + initialCongestionWindowPackets*initialMaxDatagramSize, + congestion.MaxCongestionWindowPackets*initialMaxDatagramSize, + profile, + ) +} + +func newBbrSender( + clock Clock, + initialMaxDatagramSize, + initialCongestionWindow, + initialMaxCongestionWindow congestion.ByteCount, + profile Profile, +) *bbrSender { + debug, _ := strconv.ParseBool(os.Getenv(debugEnv)) + b := &bbrSender{ + clock: clock, + mode: bbrModeStartup, + sampler: newBandwidthSampler(roundTripCount(bandwidthWindowSize)), + lastSentPacket: invalidPacketNumber, + currentRoundTripEnd: invalidPacketNumber, + maxBandwidth: NewWindowedFilter(roundTripCount(bandwidthWindowSize), MaxFilter[Bandwidth]), + congestionWindow: initialCongestionWindow, + initialCongestionWindow: initialCongestionWindow, + maxCongestionWindow: initialMaxCongestionWindow, + minCongestionWindow: minCongestionWindowForMaxDatagramSize(initialMaxDatagramSize), + profile: ProfileStandard, + highGain: defaultHighGain, + highCwndGain: derivedHighCWNDGain, + drainGain: 1.0 / defaultHighGain, + pacingGain: 1.0, + congestionWindowGain: 1.0, + congestionWindowGainConstant: 2.0, + numStartupRtts: roundTripsWithoutGrowthBeforeExitingStartup, + recoveryState: bbrRecoveryStateNotInRecovery, + endRecoveryAt: invalidPacketNumber, + recoveryWindow: initialMaxCongestionWindow, + bytesLostMultiplierWhileDetectingOvershooting: 2, + cwndToCalculateMinPacingRate: initialCongestionWindow, + maxCongestionWindowWithNetworkParametersAdjusted: initialMaxCongestionWindow, + maxDatagramSize: initialMaxDatagramSize, + debug: debug, + } + b.pacer = common.NewPacer(b.bandwidthForPacer) + b.applyProfile(profile) + if b.debug { + b.debugPrint("Profile: %s", b.profile) + } + + b.enterStartupMode(b.clock.Now()) + + return b +} + +func (b *bbrSender) applyProfile(profile Profile) { + if profile == "" { + profile = ProfileStandard + } + cfg := configForProfile(profile) + b.profile = profile + b.highGain = cfg.highGain + b.highCwndGain = cfg.highCwndGain + b.drainGain = 1.0 / cfg.highGain + b.congestionWindowGainConstant = cfg.congestionWindowGainConstant + b.numStartupRtts = cfg.numStartupRtts + b.drainToTarget = cfg.drainToTarget + b.detectOvershooting = cfg.detectOvershooting + b.bytesLostMultiplierWhileDetectingOvershooting = cfg.bytesLostMultiplier + b.enableAckAggregationDuringStartup = cfg.enableAckAggregationStartup + b.expireAckAggregationInStartup = cfg.expireAckAggregationStartup + if cfg.enableOverestimateAvoidance { + b.sampler.EnableOverestimateAvoidance() + } + b.sampler.SetReduceExtraAckedOnBandwidthIncrease(cfg.reduceExtraAckedOnBandwidthIncrease) +} + +func minCongestionWindowForMaxDatagramSize(maxDatagramSize congestion.ByteCount) congestion.ByteCount { + return minCongestionWindowPackets * maxDatagramSize +} + +func scaleByteWindowForDatagramSize(window, oldMaxDatagramSize, newMaxDatagramSize congestion.ByteCount) congestion.ByteCount { + if oldMaxDatagramSize == newMaxDatagramSize { + return window + } + return congestion.ByteCount(uint64(window) * uint64(newMaxDatagramSize) / uint64(oldMaxDatagramSize)) +} + +func (b *bbrSender) rescalePacketSizedWindows(maxDatagramSize congestion.ByteCount) { + oldMaxDatagramSize := b.maxDatagramSize + b.maxDatagramSize = maxDatagramSize + b.initialCongestionWindow = scaleByteWindowForDatagramSize(b.initialCongestionWindow, oldMaxDatagramSize, maxDatagramSize) + b.maxCongestionWindow = scaleByteWindowForDatagramSize(b.maxCongestionWindow, oldMaxDatagramSize, maxDatagramSize) + b.minCongestionWindow = minCongestionWindowForMaxDatagramSize(maxDatagramSize) + b.cwndToCalculateMinPacingRate = scaleByteWindowForDatagramSize(b.cwndToCalculateMinPacingRate, oldMaxDatagramSize, maxDatagramSize) + b.maxCongestionWindowWithNetworkParametersAdjusted = scaleByteWindowForDatagramSize( + b.maxCongestionWindowWithNetworkParametersAdjusted, + oldMaxDatagramSize, + maxDatagramSize, + ) +} + +func (b *bbrSender) SetRTTStatsProvider(provider congestion.RTTStatsProvider) { + b.rttStats = provider +} + +// TimeUntilSend implements the SendAlgorithm interface. +func (b *bbrSender) TimeUntilSend(bytesInFlight congestion.ByteCount) monotime.Time { + return b.pacer.TimeUntilSend() +} + +// HasPacingBudget implements the SendAlgorithm interface. +func (b *bbrSender) HasPacingBudget(now monotime.Time) bool { + return b.pacer.Budget(now) >= b.maxDatagramSize +} + +// OnPacketSent implements the SendAlgorithm interface. +func (b *bbrSender) OnPacketSent( + sentTime monotime.Time, + bytesInFlight congestion.ByteCount, + packetNumber congestion.PacketNumber, + bytes congestion.ByteCount, + isRetransmittable bool, +) { + b.pacer.SentPacket(sentTime, bytes) + + b.lastSentPacket = packetNumber + b.bytesInFlight = bytesInFlight + + if bytesInFlight == 0 { + b.exitingQuiescence = true + } + + b.sampler.OnPacketSent(sentTime, packetNumber, bytes, bytesInFlight, isRetransmittable) +} + +// CanSend implements the SendAlgorithm interface. +func (b *bbrSender) CanSend(bytesInFlight congestion.ByteCount) bool { + return bytesInFlight < b.GetCongestionWindow() +} + +// MaybeExitSlowStart implements the SendAlgorithm interface. +func (b *bbrSender) MaybeExitSlowStart() { + // Do nothing +} + +// OnPacketAcked implements the SendAlgorithm interface. +func (b *bbrSender) OnPacketAcked(number congestion.PacketNumber, ackedBytes, priorInFlight congestion.ByteCount, eventTime monotime.Time) { + // Do nothing. +} + +// OnPacketLost implements the SendAlgorithm interface. +func (b *bbrSender) OnPacketLost(number congestion.PacketNumber, lostBytes, priorInFlight congestion.ByteCount) { + // Do nothing. +} + +// OnRetransmissionTimeout implements the SendAlgorithm interface. +func (b *bbrSender) OnRetransmissionTimeout(packetsRetransmitted bool) { + // Do nothing. +} + +// SetMaxDatagramSize implements the SendAlgorithm interface. +func (b *bbrSender) SetMaxDatagramSize(s congestion.ByteCount) { + if b.debug { + b.debugPrint("Max Datagram Size: %d", s) + } + if s < b.maxDatagramSize { + panic(fmt.Sprintf("congestion BUG: decreased max datagram size from %d to %d", b.maxDatagramSize, s)) + } + oldMinCongestionWindow := b.minCongestionWindow + oldInitialCongestionWindow := b.initialCongestionWindow + b.rescalePacketSizedWindows(s) + switch b.congestionWindow { + case oldMinCongestionWindow: + b.congestionWindow = b.minCongestionWindow + case oldInitialCongestionWindow: + b.congestionWindow = b.initialCongestionWindow + default: + b.congestionWindow = min(b.maxCongestionWindow, max(b.congestionWindow, b.minCongestionWindow)) + } + b.recoveryWindow = min(b.maxCongestionWindow, max(b.recoveryWindow, b.minCongestionWindow)) + b.pacer.SetMaxDatagramSize(s) +} + +// InSlowStart implements the SendAlgorithmWithDebugInfos interface. +func (b *bbrSender) InSlowStart() bool { + return b.mode == bbrModeStartup +} + +// InRecovery implements the SendAlgorithmWithDebugInfos interface. +func (b *bbrSender) InRecovery() bool { + return b.recoveryState != bbrRecoveryStateNotInRecovery +} + +// GetCongestionWindow implements the SendAlgorithmWithDebugInfos interface. +func (b *bbrSender) GetCongestionWindow() congestion.ByteCount { + if b.mode == bbrModeProbeRtt { + return b.probeRttCongestionWindow() + } + + if b.InRecovery() { + return min(b.congestionWindow, b.recoveryWindow) + } + + return b.congestionWindow +} + +func (b *bbrSender) OnCongestionEvent(number congestion.PacketNumber, lostBytes, priorInFlight congestion.ByteCount) { + // Do nothing. +} + +func (b *bbrSender) OnCongestionEventEx(priorInFlight congestion.ByteCount, eventTime monotime.Time, ackedPackets []congestion.AckedPacketInfo, lostPackets []congestion.LostPacketInfo) { + totalBytesAckedBefore := b.sampler.TotalBytesAcked() + totalBytesLostBefore := b.sampler.TotalBytesLost() + + var isRoundStart, minRttExpired bool + var excessAcked, bytesLost congestion.ByteCount + + // The send state of the largest packet in acked_packets, unless it is + // empty. If acked_packets is empty, it's the send state of the largest + // packet in lost_packets. + var lastPacketSendState sendTimeState + + b.maybeAppLimited(priorInFlight) + + // Update bytesInFlight + b.bytesInFlight = priorInFlight + for _, p := range ackedPackets { + b.bytesInFlight -= p.BytesAcked + } + for _, p := range lostPackets { + b.bytesInFlight -= p.BytesLost + } + + if len(ackedPackets) != 0 { + lastAckedPacket := ackedPackets[len(ackedPackets)-1].PacketNumber + isRoundStart = b.updateRoundTripCounter(lastAckedPacket) + b.updateRecoveryState(lastAckedPacket, len(lostPackets) != 0, isRoundStart) + } + + sample := b.sampler.OnCongestionEvent(eventTime, + ackedPackets, lostPackets, b.maxBandwidth.GetBest(), infBandwidth, b.roundTripCount) + if sample.lastPacketSendState.isValid { + b.lastSampleIsAppLimited = sample.lastPacketSendState.isAppLimited + b.hasNoAppLimitedSample = b.hasNoAppLimitedSample || !b.lastSampleIsAppLimited + } + // Avoid updating |max_bandwidth_| if a) this is a loss-only event, or b) all + // packets in |acked_packets| did not generate valid samples. (e.g. ack of + // ack-only packets). In both cases, sampler_.total_bytes_acked() will not + // change. + if totalBytesAckedBefore != b.sampler.TotalBytesAcked() { + if !sample.sampleIsAppLimited || sample.sampleMaxBandwidth > b.maxBandwidth.GetBest() { + b.maxBandwidth.Update(sample.sampleMaxBandwidth, b.roundTripCount) + } + } + + if sample.sampleRtt != infRTT { + minRttExpired = b.maybeUpdateMinRtt(eventTime, sample.sampleRtt) + } + bytesLost = b.sampler.TotalBytesLost() - totalBytesLostBefore + + excessAcked = sample.extraAcked + lastPacketSendState = sample.lastPacketSendState + + if len(lostPackets) != 0 { + b.numLossEventsInRound++ + b.bytesLostInRound += bytesLost + } + + // Handle logic specific to PROBE_BW mode. + if b.mode == bbrModeProbeBw { + b.updateGainCyclePhase(eventTime, priorInFlight, len(lostPackets) != 0) + } + + // Handle logic specific to STARTUP and DRAIN modes. + if isRoundStart && !b.isAtFullBandwidth { + b.checkIfFullBandwidthReached(&lastPacketSendState) + } + + b.maybeExitStartupOrDrain(eventTime) + + // Handle logic specific to PROBE_RTT. + b.maybeEnterOrExitProbeRtt(eventTime, isRoundStart, minRttExpired) + + // Calculate number of packets acked and lost. + bytesAcked := b.sampler.TotalBytesAcked() - totalBytesAckedBefore + + // After the model is updated, recalculate the pacing rate and congestion + // window. + b.calculatePacingRate(bytesLost) + b.calculateCongestionWindow(bytesAcked, excessAcked) + b.calculateRecoveryWindow(bytesAcked, bytesLost) + + // Cleanup internal state. + // This is where we clean up obsolete (acked or lost) packets from the bandwidth sampler. + // The "least unacked" should actually be FirstOutstanding, but since we are not passing + // that through OnCongestionEventEx, we will only do an estimate using acked/lost packets + // for now. Because of fast retransmission, they should differ by no more than 2 packets. + // (this is controlled by packetThreshold in quic-go's sentPacketHandler) + var leastUnacked congestion.PacketNumber + if len(ackedPackets) != 0 { + leastUnacked = ackedPackets[len(ackedPackets)-1].PacketNumber - 2 + } else { + leastUnacked = lostPackets[len(lostPackets)-1].PacketNumber + 1 + } + b.sampler.RemoveObsoletePackets(leastUnacked) + + if isRoundStart { + b.numLossEventsInRound = 0 + b.bytesLostInRound = 0 + } +} + +func (b *bbrSender) PacingRate() Bandwidth { + if b.pacingRate == 0 { + return Bandwidth(b.highGain * float64( + BandwidthFromDelta(b.initialCongestionWindow, b.getMinRtt()))) + } + + return b.pacingRate +} + +// Sets the CWND gain used in STARTUP. Must be greater than 1. +func (b *bbrSender) setHighCwndGain(highCwndGain float64) { + b.highCwndGain = highCwndGain + if b.mode == bbrModeStartup { + b.congestionWindowGain = highCwndGain + } +} + +// Get the current bandwidth estimate. Note that Bandwidth is in bits per second. +func (b *bbrSender) bandwidthEstimate() Bandwidth { + return b.maxBandwidth.GetBest() +} + +func (b *bbrSender) bandwidthForPacer() congestion.ByteCount { + bps := congestion.ByteCount(float64(b.PacingRate()) / float64(BytesPerSecond)) + if bps < minBps { + // We need to make sure that the bandwidth value for pacer is never zero, + // otherwise it will go into an edge case where HasPacingBudget = false + // but TimeUntilSend is before, causing the quic-go send loop to go crazy and get stuck. + return minBps + } + return bps +} + +// Returns the current estimate of the RTT of the connection. Outside of the +// edge cases, this is minimum RTT. +func (b *bbrSender) getMinRtt() time.Duration { + if b.minRtt != 0 { + return b.minRtt + } + // min_rtt could be available if the handshake packet gets neutered then + // gets acknowledged. This could only happen for QUIC crypto where we do not + // drop keys. + minRtt := b.rttStats.MinRTT() + if minRtt == 0 { + return 100 * time.Millisecond + } else { + return minRtt + } +} + +// Computes the target congestion window using the specified gain. +func (b *bbrSender) getTargetCongestionWindow(gain float64) congestion.ByteCount { + bdp := bdpFromRttAndBandwidth(b.getMinRtt(), b.bandwidthEstimate()) + congestionWindow := congestion.ByteCount(gain * float64(bdp)) + + // BDP estimate will be zero if no bandwidth samples are available yet. + if congestionWindow == 0 { + congestionWindow = congestion.ByteCount(gain * float64(b.initialCongestionWindow)) + } + + return max(congestionWindow, b.minCongestionWindow) +} + +// The target congestion window during PROBE_RTT. +func (b *bbrSender) probeRttCongestionWindow() congestion.ByteCount { + return b.minCongestionWindow +} + +func (b *bbrSender) maybeUpdateMinRtt(now monotime.Time, sampleMinRtt time.Duration) bool { + // Do not expire min_rtt if none was ever available. + minRttExpired := b.minRtt != 0 && now.After(b.minRttTimestamp.Add(minRttExpiry)) + if minRttExpired || sampleMinRtt < b.minRtt || b.minRtt == 0 { + b.minRtt = sampleMinRtt + b.minRttTimestamp = now + } + + return minRttExpired +} + +// Enters the STARTUP mode. +func (b *bbrSender) enterStartupMode(now monotime.Time) { + b.mode = bbrModeStartup + // b.maybeTraceStateChange(logging.CongestionStateStartup) + b.pacingGain = b.highGain + b.congestionWindowGain = b.highCwndGain + + if b.debug { + b.debugPrint("Phase: STARTUP") + } +} + +// Enters the PROBE_BW mode. +func (b *bbrSender) enterProbeBandwidthMode(now monotime.Time) { + b.mode = bbrModeProbeBw + // b.maybeTraceStateChange(logging.CongestionStateProbeBw) + b.congestionWindowGain = b.congestionWindowGainConstant + + // Pick a random offset for the gain cycle out of {0, 2..7} range. 1 is + // excluded because in that case increased gain and decreased gain would not + // follow each other. + b.cycleCurrentOffset = int(rand.Int31n(congestion.PacketsPerConnectionID)) % (gainCycleLength - 1) + if b.cycleCurrentOffset >= 1 { + b.cycleCurrentOffset += 1 + } + + b.lastCycleStart = now + b.pacingGain = pacingGain[b.cycleCurrentOffset] + + if b.debug { + b.debugPrint("Phase: PROBE_BW") + } +} + +// Updates the round-trip counter if a round-trip has passed. Returns true if +// the counter has been advanced. +func (b *bbrSender) updateRoundTripCounter(lastAckedPacket congestion.PacketNumber) bool { + if b.currentRoundTripEnd == invalidPacketNumber || lastAckedPacket > b.currentRoundTripEnd { + b.roundTripCount++ + b.currentRoundTripEnd = b.lastSentPacket + return true + } + return false +} + +// Updates the current gain used in PROBE_BW mode. +func (b *bbrSender) updateGainCyclePhase(now monotime.Time, priorInFlight congestion.ByteCount, hasLosses bool) { + // In most cases, the cycle is advanced after an RTT passes. + shouldAdvanceGainCycling := now.After(b.lastCycleStart.Add(b.getMinRtt())) + // If the pacing gain is above 1.0, the connection is trying to probe the + // bandwidth by increasing the number of bytes in flight to at least + // pacing_gain * BDP. Make sure that it actually reaches the target, as long + // as there are no losses suggesting that the buffers are not able to hold + // that much. + if b.pacingGain > 1.0 && !hasLosses && priorInFlight < b.getTargetCongestionWindow(b.pacingGain) { + shouldAdvanceGainCycling = false + } + + // If pacing gain is below 1.0, the connection is trying to drain the extra + // queue which could have been incurred by probing prior to it. If the number + // of bytes in flight falls down to the estimated BDP value earlier, conclude + // that the queue has been successfully drained and exit this cycle early. + if b.pacingGain < 1.0 && b.bytesInFlight <= b.getTargetCongestionWindow(1) { + shouldAdvanceGainCycling = true + } + + if shouldAdvanceGainCycling { + b.cycleCurrentOffset = (b.cycleCurrentOffset + 1) % gainCycleLength + b.lastCycleStart = now + // Stay in low gain mode until the target BDP is hit. + // Low gain mode will be exited immediately when the target BDP is achieved. + if b.drainToTarget && b.pacingGain < 1 && + pacingGain[b.cycleCurrentOffset] == 1 && + b.bytesInFlight > b.getTargetCongestionWindow(1) { + return + } + b.pacingGain = pacingGain[b.cycleCurrentOffset] + } +} + +// Tracks for how many round-trips the bandwidth has not increased +// significantly. +func (b *bbrSender) checkIfFullBandwidthReached(lastPacketSendState *sendTimeState) { + if b.lastSampleIsAppLimited { + return + } + + target := Bandwidth(float64(b.bandwidthAtLastRound) * startupGrowthTarget) + if b.bandwidthEstimate() >= target { + b.bandwidthAtLastRound = b.bandwidthEstimate() + b.roundsWithoutBandwidthGain = 0 + if b.expireAckAggregationInStartup { + // Expire old excess delivery measurements now that bandwidth increased. + b.sampler.ResetMaxAckHeightTracker(0, b.roundTripCount) + } + return + } + + b.roundsWithoutBandwidthGain++ + if b.roundsWithoutBandwidthGain >= b.numStartupRtts || + b.shouldExitStartupDueToLoss(lastPacketSendState) { + b.isAtFullBandwidth = true + } +} + +func (b *bbrSender) maybeAppLimited(bytesInFlight congestion.ByteCount) { + if bytesInFlight < b.getTargetCongestionWindow(1) { + b.sampler.OnAppLimited() + } +} + +// Transitions from STARTUP to DRAIN and from DRAIN to PROBE_BW if +// appropriate. +func (b *bbrSender) maybeExitStartupOrDrain(now monotime.Time) { + if b.mode == bbrModeStartup && b.isAtFullBandwidth { + b.mode = bbrModeDrain + // b.maybeTraceStateChange(logging.CongestionStateDrain) + b.pacingGain = b.drainGain + b.congestionWindowGain = b.highCwndGain + + if b.debug { + b.debugPrint("Phase: DRAIN") + } + } + if b.mode == bbrModeDrain && b.bytesInFlight <= b.getTargetCongestionWindow(1) { + b.enterProbeBandwidthMode(now) + } +} + +// Decides whether to enter or exit PROBE_RTT. +func (b *bbrSender) maybeEnterOrExitProbeRtt(now monotime.Time, isRoundStart, minRttExpired bool) { + if minRttExpired && !b.exitingQuiescence && b.mode != bbrModeProbeRtt { + b.mode = bbrModeProbeRtt + // b.maybeTraceStateChange(logging.CongestionStateProbRtt) + b.pacingGain = 1.0 + // Do not decide on the time to exit PROBE_RTT until the |bytes_in_flight| + // is at the target small value. + b.exitProbeRttAt = 0 + + if b.debug { + b.debugPrint("BandwidthEstimate: %s, CongestionWindowGain: %.2f, PacingGain: %.2f, PacingRate: %s", + formatSpeed(b.bandwidthEstimate()), b.congestionWindowGain, b.pacingGain, formatSpeed(b.PacingRate())) + b.debugPrint("Phase: PROBE_RTT") + } + } + + if b.mode == bbrModeProbeRtt { + b.sampler.OnAppLimited() + // b.maybeTraceStateChange(logging.CongestionStateApplicationLimited) + + if b.exitProbeRttAt.IsZero() { + // If the window has reached the appropriate size, schedule exiting + // PROBE_RTT. The CWND during PROBE_RTT is kMinimumCongestionWindow, but + // we allow an extra packet since QUIC checks CWND before sending a + // packet. + if b.bytesInFlight < b.probeRttCongestionWindow()+congestion.MaxPacketBufferSize { + b.exitProbeRttAt = now.Add(probeRttTime) + b.probeRttRoundPassed = false + } + } else { + if isRoundStart { + b.probeRttRoundPassed = true + } + if now.Sub(b.exitProbeRttAt) >= 0 && b.probeRttRoundPassed { + b.minRttTimestamp = now + if b.debug { + b.debugPrint("MinRTT: %s", b.getMinRtt()) + } + if !b.isAtFullBandwidth { + b.enterStartupMode(now) + } else { + b.enterProbeBandwidthMode(now) + } + } + } + } + + b.exitingQuiescence = false +} + +// Determines whether BBR needs to enter, exit or advance state of the +// recovery. +func (b *bbrSender) updateRecoveryState(lastAckedPacket congestion.PacketNumber, hasLosses, isRoundStart bool) { + // Disable recovery in startup, if loss-based exit is enabled. + if !b.isAtFullBandwidth { + return + } + + // Exit recovery when there are no losses for a round. + if hasLosses { + b.endRecoveryAt = b.lastSentPacket + } + + switch b.recoveryState { + case bbrRecoveryStateNotInRecovery: + if hasLosses { + b.recoveryState = bbrRecoveryStateConservation + // This will cause the |recovery_window_| to be set to the correct + // value in CalculateRecoveryWindow(). + b.recoveryWindow = 0 + // Since the conservation phase is meant to be lasting for a whole + // round, extend the current round as if it were started right now. + b.currentRoundTripEnd = b.lastSentPacket + } + case bbrRecoveryStateConservation: + if isRoundStart { + b.recoveryState = bbrRecoveryStateGrowth + } + fallthrough + case bbrRecoveryStateGrowth: + // Exit recovery if appropriate. + if !hasLosses && lastAckedPacket > b.endRecoveryAt { + b.recoveryState = bbrRecoveryStateNotInRecovery + } + } +} + +// Determines the appropriate pacing rate for the connection. +func (b *bbrSender) calculatePacingRate(bytesLost congestion.ByteCount) { + if b.bandwidthEstimate() == 0 { + return + } + + targetRate := Bandwidth(b.pacingGain * float64(b.bandwidthEstimate())) + if b.isAtFullBandwidth { + b.pacingRate = targetRate + return + } + + // Pace at the rate of initial_window / RTT as soon as RTT measurements are + // available. + if b.pacingRate == 0 && b.rttStats.MinRTT() != 0 { + b.pacingRate = BandwidthFromDelta(b.initialCongestionWindow, b.rttStats.MinRTT()) + return + } + + if b.detectOvershooting { + b.bytesLostWhileDetectingOvershooting += bytesLost + // Check for overshooting with network parameters adjusted when pacing rate + // > target_rate and loss has been detected. + if b.pacingRate > targetRate && b.bytesLostWhileDetectingOvershooting > 0 { + if b.hasNoAppLimitedSample || + b.bytesLostWhileDetectingOvershooting*congestion.ByteCount(b.bytesLostMultiplierWhileDetectingOvershooting) > b.initialCongestionWindow { + // We are fairly sure overshoot happens if 1) there is at least one + // non app-limited bw sample or 2) half of IW gets lost. Slow pacing + // rate. + b.pacingRate = max(targetRate, BandwidthFromDelta(b.cwndToCalculateMinPacingRate, b.rttStats.MinRTT())) + b.bytesLostWhileDetectingOvershooting = 0 + b.detectOvershooting = false + } + } + } + + // Do not decrease the pacing rate during startup. + b.pacingRate = max(b.pacingRate, targetRate) +} + +// Determines the appropriate congestion window for the connection. +func (b *bbrSender) calculateCongestionWindow(bytesAcked, excessAcked congestion.ByteCount) { + if b.mode == bbrModeProbeRtt { + return + } + + targetWindow := b.getTargetCongestionWindow(b.congestionWindowGain) + if b.isAtFullBandwidth { + // Add the max recently measured ack aggregation to CWND. + targetWindow += b.sampler.MaxAckHeight() + } else if b.enableAckAggregationDuringStartup { + // Add the most recent excess acked. Because CWND never decreases in + // STARTUP, this will automatically create a very localized max filter. + targetWindow += excessAcked + } + + // Instead of immediately setting the target CWND as the new one, BBR grows + // the CWND towards |target_window| by only increasing it |bytes_acked| at a + // time. + if b.isAtFullBandwidth { + b.congestionWindow = min(targetWindow, b.congestionWindow+bytesAcked) + } else if b.congestionWindow < targetWindow || + b.sampler.TotalBytesAcked() < b.initialCongestionWindow { + // If the connection is not yet out of startup phase, do not decrease the + // window. + b.congestionWindow += bytesAcked + } + + // Enforce the limits on the congestion window. + b.congestionWindow = max(b.congestionWindow, b.minCongestionWindow) + b.congestionWindow = min(b.congestionWindow, b.maxCongestionWindow) +} + +// Determines the appropriate window that constrains the in-flight during recovery. +func (b *bbrSender) calculateRecoveryWindow(bytesAcked, bytesLost congestion.ByteCount) { + if b.recoveryState == bbrRecoveryStateNotInRecovery { + return + } + + // Set up the initial recovery window. + if b.recoveryWindow == 0 { + b.recoveryWindow = b.bytesInFlight + bytesAcked + b.recoveryWindow = max(b.minCongestionWindow, b.recoveryWindow) + return + } + + // Remove losses from the recovery window, while accounting for a potential + // integer underflow. + if b.recoveryWindow >= bytesLost { + b.recoveryWindow = b.recoveryWindow - bytesLost + } else { + b.recoveryWindow = b.maxDatagramSize + } + + // In CONSERVATION mode, just subtracting losses is sufficient. In GROWTH, + // release additional |bytes_acked| to achieve a slow-start-like behavior. + if b.recoveryState == bbrRecoveryStateGrowth { + b.recoveryWindow += bytesAcked + } + + // Always allow sending at least |bytes_acked| in response. + b.recoveryWindow = max(b.recoveryWindow, b.bytesInFlight+bytesAcked) + b.recoveryWindow = max(b.minCongestionWindow, b.recoveryWindow) +} + +// Return whether we should exit STARTUP due to excessive loss. +func (b *bbrSender) shouldExitStartupDueToLoss(lastPacketSendState *sendTimeState) bool { + if b.numLossEventsInRound < defaultStartupFullLossCount || !lastPacketSendState.isValid { + return false + } + + inflightAtSend := lastPacketSendState.bytesInFlight + + if inflightAtSend > 0 && b.bytesLostInRound > 0 { + if b.bytesLostInRound > congestion.ByteCount(float64(inflightAtSend)*quicBbr2DefaultLossThreshold) { + return true + } + return false + } + return false +} + +func (b *bbrSender) debugPrint(format string, a ...any) { + fmt.Printf("[BBRSender] [%s] %s\n", + time.Now().Format("15:04:05"), + fmt.Sprintf(format, a...)) +} + +func bdpFromRttAndBandwidth(rtt time.Duration, bandwidth Bandwidth) congestion.ByteCount { + return congestion.ByteCount(rtt) * congestion.ByteCount(bandwidth) / congestion.ByteCount(BytesPerSecond) / congestion.ByteCount(time.Second) +} + +func GetInitialPacketSize(addr net.Addr) congestion.ByteCount { + // If this is not a UDP address, we don't know anything about the MTU. + // Use the minimum size of an Initial packet as the max packet size. + if _, ok := addr.(*net.UDPAddr); ok { + return congestion.InitialPacketSize + } else { + return congestion.MinInitialPacketSize + } +} + +func formatSpeed(bw Bandwidth) string { + bwf := float64(bw) + units := []string{"bps", "Kbps", "Mbps", "Gbps"} + unitIndex := 0 + for bwf > 1000 && unitIndex < len(units)-1 { + bwf /= 1000 + unitIndex++ + } + return fmt.Sprintf("%.2f %s", bwf, units[unitIndex]) +} diff --git a/subproject/Xray-core-main/transport/internet/hysteria/congestion/bbr/bbr_sender_test.go b/subproject/Xray-core-main/transport/internet/hysteria/congestion/bbr/bbr_sender_test.go new file mode 100644 index 00000000..5aff552f --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/hysteria/congestion/bbr/bbr_sender_test.go @@ -0,0 +1,130 @@ +package bbr + +import ( + "testing" + + "github.com/apernet/quic-go/congestion" + "github.com/stretchr/testify/require" +) + +func TestSetMaxDatagramSizeRescalesPacketSizedWindows(t *testing.T) { + const oldMaxDatagramSize = congestion.ByteCount(1000) + const newMaxDatagramSize = congestion.ByteCount(1400) + const initialCongestionWindowPackets = congestion.ByteCount(20) + const maxCongestionWindowPackets = congestion.ByteCount(80) + + b := newBbrSender( + DefaultClock{}, + oldMaxDatagramSize, + initialCongestionWindowPackets*oldMaxDatagramSize, + maxCongestionWindowPackets*oldMaxDatagramSize, + ProfileStandard, + ) + b.congestionWindow = b.initialCongestionWindow + + b.SetMaxDatagramSize(newMaxDatagramSize) + + require.Equal(t, initialCongestionWindowPackets*newMaxDatagramSize, b.initialCongestionWindow) + require.Equal(t, maxCongestionWindowPackets*newMaxDatagramSize, b.maxCongestionWindow) + require.Equal(t, minCongestionWindowPackets*newMaxDatagramSize, b.minCongestionWindow) + require.Equal(t, initialCongestionWindowPackets*newMaxDatagramSize, b.congestionWindow) +} + +func TestSetMaxDatagramSizeClampsCongestionWindow(t *testing.T) { + const oldMaxDatagramSize = congestion.ByteCount(1000) + const newMaxDatagramSize = congestion.ByteCount(1400) + + b := NewBbrSender(DefaultClock{}, oldMaxDatagramSize, ProfileStandard) + b.congestionWindow = b.minCongestionWindow + oldMaxDatagramSize + b.recoveryWindow = b.minCongestionWindow + oldMaxDatagramSize + + b.SetMaxDatagramSize(newMaxDatagramSize) + + require.Equal(t, b.minCongestionWindow, b.congestionWindow) + require.Equal(t, b.minCongestionWindow, b.recoveryWindow) +} + +func TestNewBbrSenderAppliesProfiles(t *testing.T) { + testCases := []struct { + name string + profile Profile + highGain float64 + highCwndGain float64 + congestionWindowGainConstant float64 + numStartupRtts int64 + drainToTarget bool + detectOvershooting bool + bytesLostMultiplier uint8 + enableAckAggregationDuringStartup bool + expireAckAggregationInStartup bool + enableOverestimateAvoidance bool + reduceExtraAckedOnBandwidthIncrease bool + }{ + { + name: "standard", + profile: ProfileStandard, + highGain: defaultHighGain, + highCwndGain: derivedHighCWNDGain, + congestionWindowGainConstant: 2.0, + numStartupRtts: roundTripsWithoutGrowthBeforeExitingStartup, + bytesLostMultiplier: 2, + }, + { + name: "conservative", + profile: ProfileConservative, + highGain: 2.25, + highCwndGain: 1.75, + congestionWindowGainConstant: 1.75, + numStartupRtts: 2, + drainToTarget: true, + detectOvershooting: true, + bytesLostMultiplier: 1, + enableOverestimateAvoidance: true, + reduceExtraAckedOnBandwidthIncrease: true, + }, + { + name: "aggressive", + profile: ProfileAggressive, + highGain: 3.0, + highCwndGain: 2.25, + congestionWindowGainConstant: 2.5, + numStartupRtts: 4, + bytesLostMultiplier: 2, + enableAckAggregationDuringStartup: true, + expireAckAggregationInStartup: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + b := NewBbrSender(DefaultClock{}, congestion.InitialPacketSize, tc.profile) + require.Equal(t, tc.profile, b.profile) + require.Equal(t, tc.highGain, b.highGain) + require.Equal(t, tc.highCwndGain, b.highCwndGain) + require.Equal(t, tc.congestionWindowGainConstant, b.congestionWindowGainConstant) + require.Equal(t, tc.numStartupRtts, b.numStartupRtts) + require.Equal(t, tc.drainToTarget, b.drainToTarget) + require.Equal(t, tc.detectOvershooting, b.detectOvershooting) + require.Equal(t, tc.bytesLostMultiplier, b.bytesLostMultiplierWhileDetectingOvershooting) + require.Equal(t, tc.enableAckAggregationDuringStartup, b.enableAckAggregationDuringStartup) + require.Equal(t, tc.expireAckAggregationInStartup, b.expireAckAggregationInStartup) + require.Equal(t, tc.enableOverestimateAvoidance, b.sampler.IsOverestimateAvoidanceEnabled()) + require.Equal(t, tc.reduceExtraAckedOnBandwidthIncrease, b.sampler.maxAckHeightTracker.reduceExtraAckedOnBandwidthIncrease) + require.Equal(t, b.highGain, b.pacingGain) + require.Equal(t, b.highCwndGain, b.congestionWindowGain) + }) + } +} + +func TestParseProfile(t *testing.T) { + profile, err := ParseProfile("") + require.NoError(t, err) + require.Equal(t, ProfileStandard, profile) + + profile, err = ParseProfile("Aggressive") + require.NoError(t, err) + require.Equal(t, ProfileAggressive, profile) + + _, err = ParseProfile("turbo") + require.EqualError(t, err, `unsupported BBR profile "turbo"`) +} diff --git a/subproject/Xray-core-main/transport/internet/hysteria/congestion/bbr/clock.go b/subproject/Xray-core-main/transport/internet/hysteria/congestion/bbr/clock.go new file mode 100644 index 00000000..541987ef --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/hysteria/congestion/bbr/clock.go @@ -0,0 +1,18 @@ +package bbr + +import "github.com/apernet/quic-go/monotime" + +// A Clock returns the current time +type Clock interface { + Now() monotime.Time +} + +// DefaultClock implements the Clock interface using the Go stdlib clock. +type DefaultClock struct{} + +var _ Clock = DefaultClock{} + +// Now gets the current time +func (DefaultClock) Now() monotime.Time { + return monotime.Now() +} diff --git a/subproject/Xray-core-main/transport/internet/hysteria/congestion/bbr/packet_number_indexed_queue.go b/subproject/Xray-core-main/transport/internet/hysteria/congestion/bbr/packet_number_indexed_queue.go new file mode 100644 index 00000000..08b99dea --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/hysteria/congestion/bbr/packet_number_indexed_queue.go @@ -0,0 +1,199 @@ +package bbr + +import ( + "github.com/apernet/quic-go/congestion" +) + +// packetNumberIndexedQueue is a queue of mostly continuous numbered entries +// which supports the following operations: +// - adding elements to the end of the queue, or at some point past the end +// - removing elements in any order +// - retrieving elements +// If all elements are inserted in order, all of the operations above are +// amortized O(1) time. +// +// Internally, the data structure is a deque where each element is marked as +// present or not. The deque starts at the lowest present index. Whenever an +// element is removed, it's marked as not present, and the front of the deque is +// cleared of elements that are not present. +// +// The tail of the queue is not cleared due to the assumption of entries being +// inserted in order, though removing all elements of the queue will return it +// to its initial state. +// +// Note that this data structure is inherently hazardous, since an addition of +// just two entries will cause it to consume all of the memory available. +// Because of that, it is not a general-purpose container and should not be used +// as one. + +type entryWrapper[T any] struct { + present bool + entry T +} + +type packetNumberIndexedQueue[T any] struct { + entries RingBuffer[entryWrapper[T]] + numberOfPresentEntries int + firstPacket congestion.PacketNumber +} + +func newPacketNumberIndexedQueue[T any](size int) *packetNumberIndexedQueue[T] { + q := &packetNumberIndexedQueue[T]{ + firstPacket: invalidPacketNumber, + } + + q.entries.Init(size) + + return q +} + +// Emplace inserts data associated |packet_number| into (or past) the end of the +// queue, filling up the missing intermediate entries as necessary. Returns +// true if the element has been inserted successfully, false if it was already +// in the queue or inserted out of order. +func (p *packetNumberIndexedQueue[T]) Emplace(packetNumber congestion.PacketNumber, entry *T) bool { + if packetNumber == invalidPacketNumber || entry == nil { + return false + } + + if p.IsEmpty() { + p.entries.PushBack(entryWrapper[T]{ + present: true, + entry: *entry, + }) + p.numberOfPresentEntries = 1 + p.firstPacket = packetNumber + return true + } + + // Do not allow insertion out-of-order. + if packetNumber <= p.LastPacket() { + return false + } + + // Handle potentially missing elements. + offset := int(packetNumber - p.FirstPacket()) + if gap := offset - p.entries.Len(); gap > 0 { + for i := 0; i < gap; i++ { + p.entries.PushBack(entryWrapper[T]{}) + } + } + + p.entries.PushBack(entryWrapper[T]{ + present: true, + entry: *entry, + }) + p.numberOfPresentEntries++ + return true +} + +// GetEntry Retrieve the entry associated with the packet number. Returns the pointer +// to the entry in case of success, or nullptr if the entry does not exist. +func (p *packetNumberIndexedQueue[T]) GetEntry(packetNumber congestion.PacketNumber) *T { + ew := p.getEntryWraper(packetNumber) + if ew == nil { + return nil + } + + return &ew.entry +} + +// Remove, Same as above, but if an entry is present in the queue, also call f(entry) +// before removing it. +func (p *packetNumberIndexedQueue[T]) Remove(packetNumber congestion.PacketNumber, f func(T)) bool { + ew := p.getEntryWraper(packetNumber) + if ew == nil { + return false + } + if f != nil { + f(ew.entry) + } + ew.present = false + p.numberOfPresentEntries-- + + if packetNumber == p.FirstPacket() { + p.clearup() + } + + return true +} + +// RemoveUpTo, but not including |packet_number|. +// Unused slots in the front are also removed, which means when the function +// returns, |first_packet()| can be larger than |packet_number|. +func (p *packetNumberIndexedQueue[T]) RemoveUpTo(packetNumber congestion.PacketNumber) { + for !p.entries.Empty() && + p.firstPacket != invalidPacketNumber && + p.firstPacket < packetNumber { + if p.entries.Front().present { + p.numberOfPresentEntries-- + } + p.entries.PopFront() + p.firstPacket++ + } + p.clearup() + + return +} + +// IsEmpty return if queue is empty. +func (p *packetNumberIndexedQueue[T]) IsEmpty() bool { + return p.numberOfPresentEntries == 0 +} + +// NumberOfPresentEntries returns the number of entries in the queue. +func (p *packetNumberIndexedQueue[T]) NumberOfPresentEntries() int { + return p.numberOfPresentEntries +} + +// EntrySlotsUsed returns the number of entries allocated in the underlying deque. This is +// proportional to the memory usage of the queue. +func (p *packetNumberIndexedQueue[T]) EntrySlotsUsed() int { + return p.entries.Len() +} + +// FirstPacket returns packet number of the first entry in the queue. +func (p *packetNumberIndexedQueue[T]) FirstPacket() (packetNumber congestion.PacketNumber) { + return p.firstPacket +} + +// LastPacket returns packet number of the last entry ever inserted in the queue. Note that the +// entry in question may have already been removed. Zero if the queue is +// empty. +func (p *packetNumberIndexedQueue[T]) LastPacket() (packetNumber congestion.PacketNumber) { + if p.IsEmpty() { + return invalidPacketNumber + } + + return p.firstPacket + congestion.PacketNumber(p.entries.Len()-1) +} + +func (p *packetNumberIndexedQueue[T]) clearup() { + for !p.entries.Empty() && !p.entries.Front().present { + p.entries.PopFront() + p.firstPacket++ + } + if p.entries.Empty() { + p.firstPacket = invalidPacketNumber + } +} + +func (p *packetNumberIndexedQueue[T]) getEntryWraper(packetNumber congestion.PacketNumber) *entryWrapper[T] { + if packetNumber == invalidPacketNumber || + p.IsEmpty() || + packetNumber < p.firstPacket { + return nil + } + + offset := int(packetNumber - p.firstPacket) + if offset >= p.entries.Len() { + return nil + } + + ew := p.entries.Offset(offset) + if ew == nil || !ew.present { + return nil + } + + return ew +} diff --git a/subproject/Xray-core-main/transport/internet/hysteria/congestion/bbr/ringbuffer.go b/subproject/Xray-core-main/transport/internet/hysteria/congestion/bbr/ringbuffer.go new file mode 100644 index 00000000..ed92d4ce --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/hysteria/congestion/bbr/ringbuffer.go @@ -0,0 +1,118 @@ +package bbr + +// A RingBuffer is a ring buffer. +// It acts as a heap that doesn't cause any allocations. +type RingBuffer[T any] struct { + ring []T + headPos, tailPos int + full bool +} + +// Init preallocs a buffer with a certain size. +func (r *RingBuffer[T]) Init(size int) { + r.ring = make([]T, size) +} + +// Len returns the number of elements in the ring buffer. +func (r *RingBuffer[T]) Len() int { + if r.full { + return len(r.ring) + } + if r.tailPos >= r.headPos { + return r.tailPos - r.headPos + } + return r.tailPos - r.headPos + len(r.ring) +} + +// Empty says if the ring buffer is empty. +func (r *RingBuffer[T]) Empty() bool { + return !r.full && r.headPos == r.tailPos +} + +// PushBack adds a new element. +// If the ring buffer is full, its capacity is increased first. +func (r *RingBuffer[T]) PushBack(t T) { + if r.full || len(r.ring) == 0 { + r.grow() + } + r.ring[r.tailPos] = t + r.tailPos++ + if r.tailPos == len(r.ring) { + r.tailPos = 0 + } + if r.tailPos == r.headPos { + r.full = true + } +} + +// PopFront returns the next element. +// It must not be called when the buffer is empty, that means that +// callers might need to check if there are elements in the buffer first. +func (r *RingBuffer[T]) PopFront() T { + if r.Empty() { + panic("github.com/quic-go/quic-go/internal/utils/ringbuffer: pop from an empty queue") + } + r.full = false + t := r.ring[r.headPos] + r.ring[r.headPos] = *new(T) + r.headPos++ + if r.headPos == len(r.ring) { + r.headPos = 0 + } + return t +} + +// Offset returns the offset element. +// It must not be called when the buffer is empty, that means that +// callers might need to check if there are elements in the buffer first +// and check if the index larger than buffer length. +func (r *RingBuffer[T]) Offset(index int) *T { + if r.Empty() || index >= r.Len() { + panic("github.com/quic-go/quic-go/internal/utils/ringbuffer: offset from invalid index") + } + offset := (r.headPos + index) % len(r.ring) + return &r.ring[offset] +} + +// Front returns the front element. +// It must not be called when the buffer is empty, that means that +// callers might need to check if there are elements in the buffer first. +func (r *RingBuffer[T]) Front() *T { + if r.Empty() { + panic("github.com/quic-go/quic-go/internal/utils/ringbuffer: front from an empty queue") + } + return &r.ring[r.headPos] +} + +// Back returns the back element. +// It must not be called when the buffer is empty, that means that +// callers might need to check if there are elements in the buffer first. +func (r *RingBuffer[T]) Back() *T { + if r.Empty() { + panic("github.com/quic-go/quic-go/internal/utils/ringbuffer: back from an empty queue") + } + return r.Offset(r.Len() - 1) +} + +// Grow the maximum size of the queue. +// This method assume the queue is full. +func (r *RingBuffer[T]) grow() { + oldRing := r.ring + newSize := len(oldRing) * 2 + if newSize == 0 { + newSize = 1 + } + r.ring = make([]T, newSize) + headLen := copy(r.ring, oldRing[r.headPos:]) + copy(r.ring[headLen:], oldRing[:r.headPos]) + r.headPos, r.tailPos, r.full = 0, len(oldRing), false +} + +// Clear removes all elements. +func (r *RingBuffer[T]) Clear() { + var zeroValue T + for i := range r.ring { + r.ring[i] = zeroValue + } + r.headPos, r.tailPos, r.full = 0, 0, false +} diff --git a/subproject/Xray-core-main/transport/internet/hysteria/congestion/bbr/windowed_filter.go b/subproject/Xray-core-main/transport/internet/hysteria/congestion/bbr/windowed_filter.go new file mode 100644 index 00000000..4773bce5 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/hysteria/congestion/bbr/windowed_filter.go @@ -0,0 +1,162 @@ +package bbr + +import ( + "golang.org/x/exp/constraints" +) + +// Implements Kathleen Nichols' algorithm for tracking the minimum (or maximum) +// estimate of a stream of samples over some fixed time interval. (E.g., +// the minimum RTT over the past five minutes.) The algorithm keeps track of +// the best, second best, and third best min (or max) estimates, maintaining an +// invariant that the measurement time of the n'th best >= n-1'th best. + +// The algorithm works as follows. On a reset, all three estimates are set to +// the same sample. The second best estimate is then recorded in the second +// quarter of the window, and a third best estimate is recorded in the second +// half of the window, bounding the worst case error when the true min is +// monotonically increasing (or true max is monotonically decreasing) over the +// window. +// +// A new best sample replaces all three estimates, since the new best is lower +// (or higher) than everything else in the window and it is the most recent. +// The window thus effectively gets reset on every new min. The same property +// holds true for second best and third best estimates. Specifically, when a +// sample arrives that is better than the second best but not better than the +// best, it replaces the second and third best estimates but not the best +// estimate. Similarly, a sample that is better than the third best estimate +// but not the other estimates replaces only the third best estimate. +// +// Finally, when the best expires, it is replaced by the second best, which in +// turn is replaced by the third best. The newest sample replaces the third +// best. + +type WindowedFilterValue interface { + any +} + +type WindowedFilterTime interface { + constraints.Integer | constraints.Float +} + +type WindowedFilter[V WindowedFilterValue, T WindowedFilterTime] struct { + // Time length of window. + windowLength T + estimates []entry[V, T] + comparator func(V, V) int +} + +type entry[V WindowedFilterValue, T WindowedFilterTime] struct { + sample V + time T +} + +// Compares two values and returns true if the first is greater than or equal +// to the second. +func MaxFilter[O constraints.Ordered](a, b O) int { + if a > b { + return 1 + } else if a < b { + return -1 + } + return 0 +} + +// Compares two values and returns true if the first is less than or equal +// to the second. +func MinFilter[O constraints.Ordered](a, b O) int { + if a < b { + return 1 + } else if a > b { + return -1 + } + return 0 +} + +func NewWindowedFilter[V WindowedFilterValue, T WindowedFilterTime](windowLength T, comparator func(V, V) int) *WindowedFilter[V, T] { + return &WindowedFilter[V, T]{ + windowLength: windowLength, + estimates: make([]entry[V, T], 3, 3), + comparator: comparator, + } +} + +// Changes the window length. Does not update any current samples. +func (f *WindowedFilter[V, T]) SetWindowLength(windowLength T) { + f.windowLength = windowLength +} + +func (f *WindowedFilter[V, T]) GetBest() V { + return f.estimates[0].sample +} + +func (f *WindowedFilter[V, T]) GetSecondBest() V { + return f.estimates[1].sample +} + +func (f *WindowedFilter[V, T]) GetThirdBest() V { + return f.estimates[2].sample +} + +// Updates best estimates with |sample|, and expires and updates best +// estimates as necessary. +func (f *WindowedFilter[V, T]) Update(newSample V, newTime T) { + // Reset all estimates if they have not yet been initialized, if new sample + // is a new best, or if the newest recorded estimate is too old. + if f.comparator(f.estimates[0].sample, *new(V)) == 0 || + f.comparator(newSample, f.estimates[0].sample) >= 0 || + newTime-f.estimates[2].time > f.windowLength { + f.Reset(newSample, newTime) + return + } + + if f.comparator(newSample, f.estimates[1].sample) >= 0 { + f.estimates[1] = entry[V, T]{newSample, newTime} + f.estimates[2] = f.estimates[1] + } else if f.comparator(newSample, f.estimates[2].sample) >= 0 { + f.estimates[2] = entry[V, T]{newSample, newTime} + } + + // Expire and update estimates as necessary. + if newTime-f.estimates[0].time > f.windowLength { + // The best estimate hasn't been updated for an entire window, so promote + // second and third best estimates. + f.estimates[0] = f.estimates[1] + f.estimates[1] = f.estimates[2] + f.estimates[2] = entry[V, T]{newSample, newTime} + // Need to iterate one more time. Check if the new best estimate is + // outside the window as well, since it may also have been recorded a + // long time ago. Don't need to iterate once more since we cover that + // case at the beginning of the method. + if newTime-f.estimates[0].time > f.windowLength { + f.estimates[0] = f.estimates[1] + f.estimates[1] = f.estimates[2] + } + return + } + if f.comparator(f.estimates[1].sample, f.estimates[0].sample) == 0 && + newTime-f.estimates[1].time > f.windowLength/4 { + // A quarter of the window has passed without a better sample, so the + // second-best estimate is taken from the second quarter of the window. + f.estimates[1] = entry[V, T]{newSample, newTime} + f.estimates[2] = f.estimates[1] + return + } + + if f.comparator(f.estimates[2].sample, f.estimates[1].sample) == 0 && + newTime-f.estimates[2].time > f.windowLength/2 { + // We've passed a half of the window without a better estimate, so take + // a third-best estimate from the second half of the window. + f.estimates[2] = entry[V, T]{newSample, newTime} + } +} + +// Resets all estimates to new sample. +func (f *WindowedFilter[V, T]) Reset(newSample V, newTime T) { + f.estimates[2] = entry[V, T]{newSample, newTime} + f.estimates[1] = f.estimates[2] + f.estimates[0] = f.estimates[1] +} + +func (f *WindowedFilter[V, T]) Clear() { + f.estimates = make([]entry[V, T], 3, 3) +} diff --git a/subproject/Xray-core-main/transport/internet/hysteria/congestion/brutal/brutal.go b/subproject/Xray-core-main/transport/internet/hysteria/congestion/brutal/brutal.go new file mode 100644 index 00000000..969923a5 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/hysteria/congestion/brutal/brutal.go @@ -0,0 +1,186 @@ +package brutal + +import ( + "fmt" + "os" + "strconv" + "time" + + "github.com/xtls/xray-core/transport/internet/hysteria/congestion/common" + + "github.com/apernet/quic-go/congestion" + "github.com/apernet/quic-go/monotime" +) + +const ( + pktInfoSlotCount = 5 // slot index is based on seconds, so this is basically how many seconds we sample + minSampleCount = 50 + minAckRate = 0.8 + congestionWindowMultiplier = 2 + + debugEnv = "HYSTERIA_BRUTAL_DEBUG" + debugPrintInterval = 2 +) + +var _ congestion.CongestionControl = &BrutalSender{} + +type BrutalSender struct { + rttStats congestion.RTTStatsProvider + bps congestion.ByteCount + maxDatagramSize congestion.ByteCount + pacer *common.Pacer + + pktInfoSlots [pktInfoSlotCount]pktInfo + ackRate float64 + + debug bool + lastAckPrintTimestamp int64 +} + +type pktInfo struct { + Timestamp int64 + AckCount uint64 + LossCount uint64 +} + +func NewBrutalSender(bps uint64) *BrutalSender { + debug, _ := strconv.ParseBool(os.Getenv(debugEnv)) + bs := &BrutalSender{ + bps: congestion.ByteCount(bps), + maxDatagramSize: congestion.InitialPacketSize, + ackRate: 1, + debug: debug, + } + bs.pacer = common.NewPacer(func() congestion.ByteCount { + return congestion.ByteCount(float64(bs.bps) / bs.ackRate) + }) + return bs +} + +func (b *BrutalSender) SetRTTStatsProvider(rttStats congestion.RTTStatsProvider) { + b.rttStats = rttStats +} + +func (b *BrutalSender) TimeUntilSend(bytesInFlight congestion.ByteCount) monotime.Time { + return b.pacer.TimeUntilSend() +} + +func (b *BrutalSender) HasPacingBudget(now monotime.Time) bool { + return b.pacer.Budget(now) >= b.maxDatagramSize +} + +func (b *BrutalSender) CanSend(bytesInFlight congestion.ByteCount) bool { + return bytesInFlight <= b.GetCongestionWindow() +} + +func (b *BrutalSender) GetCongestionWindow() congestion.ByteCount { + rtt := b.rttStats.SmoothedRTT() + if rtt <= 0 { + return 10240 + } + cwnd := congestion.ByteCount(float64(b.bps) * rtt.Seconds() * congestionWindowMultiplier / b.ackRate) + if cwnd < b.maxDatagramSize { + cwnd = b.maxDatagramSize + } + return cwnd +} + +func (b *BrutalSender) OnPacketSent(sentTime monotime.Time, bytesInFlight congestion.ByteCount, + packetNumber congestion.PacketNumber, bytes congestion.ByteCount, isRetransmittable bool, +) { + b.pacer.SentPacket(sentTime, bytes) +} + +func (b *BrutalSender) OnPacketAcked(number congestion.PacketNumber, ackedBytes congestion.ByteCount, + priorInFlight congestion.ByteCount, eventTime monotime.Time, +) { + // Stub +} + +func (b *BrutalSender) OnCongestionEvent(number congestion.PacketNumber, lostBytes congestion.ByteCount, + priorInFlight congestion.ByteCount, +) { + // Stub +} + +func (b *BrutalSender) OnCongestionEventEx(priorInFlight congestion.ByteCount, eventTime monotime.Time, ackedPackets []congestion.AckedPacketInfo, lostPackets []congestion.LostPacketInfo) { + currentTimestamp := int64(time.Duration(eventTime) / time.Second) + slot := currentTimestamp % pktInfoSlotCount + if b.pktInfoSlots[slot].Timestamp == currentTimestamp { + b.pktInfoSlots[slot].LossCount += uint64(len(lostPackets)) + b.pktInfoSlots[slot].AckCount += uint64(len(ackedPackets)) + } else { + // uninitialized slot or too old, reset + b.pktInfoSlots[slot].Timestamp = currentTimestamp + b.pktInfoSlots[slot].AckCount = uint64(len(ackedPackets)) + b.pktInfoSlots[slot].LossCount = uint64(len(lostPackets)) + } + b.updateAckRate(currentTimestamp) +} + +func (b *BrutalSender) SetMaxDatagramSize(size congestion.ByteCount) { + b.maxDatagramSize = size + b.pacer.SetMaxDatagramSize(size) + if b.debug { + b.debugPrint("SetMaxDatagramSize: %d", size) + } +} + +func (b *BrutalSender) updateAckRate(currentTimestamp int64) { + minTimestamp := currentTimestamp - pktInfoSlotCount + var ackCount, lossCount uint64 + for _, info := range b.pktInfoSlots { + if info.Timestamp < minTimestamp { + continue + } + ackCount += info.AckCount + lossCount += info.LossCount + } + if ackCount+lossCount < minSampleCount { + b.ackRate = 1 + if b.canPrintAckRate(currentTimestamp) { + b.lastAckPrintTimestamp = currentTimestamp + b.debugPrint("Not enough samples (total=%d, ack=%d, loss=%d, rtt=%d)", + ackCount+lossCount, ackCount, lossCount, b.rttStats.SmoothedRTT().Milliseconds()) + } + return + } + rate := float64(ackCount) / float64(ackCount+lossCount) + if rate < minAckRate { + b.ackRate = minAckRate + if b.canPrintAckRate(currentTimestamp) { + b.lastAckPrintTimestamp = currentTimestamp + b.debugPrint("ACK rate too low: %.2f, clamped to %.2f (total=%d, ack=%d, loss=%d, rtt=%d)", + rate, minAckRate, ackCount+lossCount, ackCount, lossCount, b.rttStats.SmoothedRTT().Milliseconds()) + } + return + } + b.ackRate = rate + if b.canPrintAckRate(currentTimestamp) { + b.lastAckPrintTimestamp = currentTimestamp + b.debugPrint("ACK rate: %.2f (total=%d, ack=%d, loss=%d, rtt=%d)", + rate, ackCount+lossCount, ackCount, lossCount, b.rttStats.SmoothedRTT().Milliseconds()) + } +} + +func (b *BrutalSender) InSlowStart() bool { + return false +} + +func (b *BrutalSender) InRecovery() bool { + return false +} + +func (b *BrutalSender) MaybeExitSlowStart() {} + +func (b *BrutalSender) OnRetransmissionTimeout(packetsRetransmitted bool) {} + +func (b *BrutalSender) canPrintAckRate(currentTimestamp int64) bool { + return b.debug && currentTimestamp-b.lastAckPrintTimestamp >= debugPrintInterval +} + +func (b *BrutalSender) debugPrint(format string, a ...any) { + fmt.Printf("[BrutalSender] [%s] %s\n", + time.Now().Format("15:04:05"), + fmt.Sprintf(format, a...)) +} diff --git a/subproject/Xray-core-main/transport/internet/hysteria/congestion/common/pacer.go b/subproject/Xray-core-main/transport/internet/hysteria/congestion/common/pacer.go new file mode 100644 index 00000000..e0bddbe2 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/hysteria/congestion/common/pacer.go @@ -0,0 +1,80 @@ +package common + +import ( + "time" + + "github.com/apernet/quic-go/congestion" + "github.com/apernet/quic-go/monotime" +) + +const ( + maxBurstPackets = 10 + maxBurstPacingDelayMultiplier = 4 +) + +// Pacer implements a token bucket pacing algorithm. +type Pacer struct { + budgetAtLastSent congestion.ByteCount + maxDatagramSize congestion.ByteCount + lastSentTime monotime.Time + getBandwidth func() congestion.ByteCount // in bytes/s +} + +func NewPacer(getBandwidth func() congestion.ByteCount) *Pacer { + p := &Pacer{ + budgetAtLastSent: maxBurstPackets * congestion.InitialPacketSize, + maxDatagramSize: congestion.InitialPacketSize, + getBandwidth: getBandwidth, + } + return p +} + +func (p *Pacer) SentPacket(sendTime monotime.Time, size congestion.ByteCount) { + budget := p.Budget(sendTime) + if size > budget { + p.budgetAtLastSent = 0 + } else { + p.budgetAtLastSent = budget - size + } + p.lastSentTime = sendTime +} + +func (p *Pacer) Budget(now monotime.Time) congestion.ByteCount { + if p.lastSentTime.IsZero() { + return p.maxBurstSize() + } + budget := p.budgetAtLastSent + (p.getBandwidth()*congestion.ByteCount(now.Sub(p.lastSentTime).Nanoseconds()))/1e9 + if budget < 0 { // protect against overflows + budget = congestion.ByteCount(1<<62 - 1) + } + return min(p.maxBurstSize(), budget) +} + +func (p *Pacer) maxBurstSize() congestion.ByteCount { + return max( + congestion.ByteCount((maxBurstPacingDelayMultiplier*congestion.MinPacingDelay).Nanoseconds())*p.getBandwidth()/1e9, + maxBurstPackets*p.maxDatagramSize, + ) +} + +// TimeUntilSend returns when the next packet should be sent. +// It returns the zero value if a packet can be sent immediately. +func (p *Pacer) TimeUntilSend() monotime.Time { + if p.budgetAtLastSent >= p.maxDatagramSize { + return 0 + } + diff := 1e9 * uint64(p.maxDatagramSize-p.budgetAtLastSent) + bw := uint64(p.getBandwidth()) + // We might need to round up this value. + // Otherwise, we might have a budget (slightly) smaller than the datagram size when the timer expires. + d := diff / bw + // this is effectively a math.Ceil, but using only integer math + if diff%bw > 0 { + d++ + } + return p.lastSentTime.Add(max(congestion.MinPacingDelay, time.Duration(d)*time.Nanosecond)) +} + +func (p *Pacer) SetMaxDatagramSize(s congestion.ByteCount) { + p.maxDatagramSize = s +} diff --git a/subproject/Xray-core-main/transport/internet/hysteria/congestion/utils.go b/subproject/Xray-core-main/transport/internet/hysteria/congestion/utils.go new file mode 100644 index 00000000..0f04318d --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/hysteria/congestion/utils.go @@ -0,0 +1,55 @@ +package congestion + +import ( + "fmt" + "strings" + + "github.com/apernet/quic-go" + "github.com/xtls/xray-core/transport/internet/hysteria/congestion/bbr" + "github.com/xtls/xray-core/transport/internet/hysteria/congestion/brutal" +) + +const ( + TypeBBR = "bbr" + TypeReno = "reno" +) + +func NormalizeType(congestionType string) (string, error) { + switch normalized := strings.ToLower(congestionType); normalized { + case "", TypeBBR: + return TypeBBR, nil + case TypeReno: + return TypeReno, nil + default: + return "", fmt.Errorf("unsupported congestion type %q", congestionType) + } +} + +func NormalizeBBRProfile(profile string) (string, error) { + normalized, err := bbr.ParseProfile(profile) + if err != nil { + return "", err + } + return string(normalized), nil +} + +func UseBBR(conn *quic.Conn, profile bbr.Profile) { + conn.SetCongestionControl(bbr.NewBbrSender( + bbr.DefaultClock{}, + bbr.GetInitialPacketSize(conn.RemoteAddr()), + profile, + )) +} + +func UseBrutal(conn *quic.Conn, tx uint64) { + conn.SetCongestionControl(brutal.NewBrutalSender(tx)) +} + +func UseConfigured(conn *quic.Conn, congestionType, bbrProfile string) { + switch congestionType { + case TypeReno: + return + default: + UseBBR(conn, bbr.Profile(bbrProfile)) + } +} diff --git a/subproject/Xray-core-main/transport/internet/hysteria/conn.go b/subproject/Xray-core-main/transport/internet/hysteria/conn.go new file mode 100644 index 00000000..cf0920d8 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/hysteria/conn.go @@ -0,0 +1,161 @@ +package hysteria + +import ( + "encoding/binary" + "io" + "sync" + "time" + + "github.com/apernet/quic-go" + "github.com/apernet/quic-go/quicvarint" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol" +) + +type interConn struct { + stream *quic.Stream + local net.Addr + remote net.Addr + + client bool + mutex sync.Mutex + + user *protocol.MemoryUser +} + +func (i *interConn) User() *protocol.MemoryUser { + return i.user +} + +func (i *interConn) Read(b []byte) (int, error) { + return i.stream.Read(b) +} + +func (i *interConn) Write(b []byte) (int, error) { + if i.client { + i.mutex.Lock() + defer i.mutex.Unlock() + if i.client { + buf := make([]byte, 0, quicvarint.Len(FrameTypeTCPRequest)+len(b)) + buf = quicvarint.Append(buf, FrameTypeTCPRequest) + buf = append(buf, b...) + _, err := i.stream.Write(buf) + if err != nil { + return 0, err + } + i.client = false + return len(b), nil + } + } + + return i.stream.Write(b) +} + +func (i *interConn) Close() error { + i.stream.CancelRead(0) + return i.stream.Close() +} + +func (i *interConn) LocalAddr() net.Addr { + return i.local +} + +func (i *interConn) RemoteAddr() net.Addr { + return i.remote +} + +func (i *interConn) SetDeadline(t time.Time) error { + return i.stream.SetDeadline(t) +} + +func (i *interConn) SetReadDeadline(t time.Time) error { + return i.stream.SetReadDeadline(t) +} + +func (i *interConn) SetWriteDeadline(t time.Time) error { + return i.stream.SetWriteDeadline(t) +} + +type InterUdpConn struct { + conn *quic.Conn + local net.Addr + remote net.Addr + + id uint32 + ch chan []byte + + closed bool + closeFunc func() + + last time.Time + mutex sync.Mutex + + user *protocol.MemoryUser +} + +func (i *InterUdpConn) User() *protocol.MemoryUser { + return i.user +} + +func (i *InterUdpConn) SetLast() { + i.mutex.Lock() + defer i.mutex.Unlock() + + i.last = time.Now() +} + +func (i *InterUdpConn) GetLast() time.Time { + i.mutex.Lock() + defer i.mutex.Unlock() + + return i.last +} + +func (i *InterUdpConn) Read(p []byte) (int, error) { + b, ok := <-i.ch + if !ok { + return 0, io.EOF + } + n := copy(p, b) + if n != len(b) { + return 0, io.ErrShortBuffer + } + + i.SetLast() + return n, nil +} + +func (i *InterUdpConn) Write(p []byte) (int, error) { + i.SetLast() + + binary.BigEndian.PutUint32(p, i.id) + if err := i.conn.SendDatagram(p); err != nil { + return 0, err + } + return len(p), nil +} + +func (i *InterUdpConn) Close() error { + i.closeFunc() + return nil +} + +func (i *InterUdpConn) LocalAddr() net.Addr { + return i.local +} + +func (i *InterUdpConn) RemoteAddr() net.Addr { + return i.remote +} + +func (i *InterUdpConn) SetDeadline(t time.Time) error { + return nil +} + +func (i *InterUdpConn) SetReadDeadline(t time.Time) error { + return nil +} + +func (i *InterUdpConn) SetWriteDeadline(t time.Time) error { + return nil +} diff --git a/subproject/Xray-core-main/transport/internet/hysteria/dialer.go b/subproject/Xray-core-main/transport/internet/hysteria/dialer.go new file mode 100644 index 00000000..b4ce8e4b --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/hysteria/dialer.go @@ -0,0 +1,489 @@ +package hysteria + +import ( + "context" + go_tls "crypto/tls" + "encoding/binary" + "math/rand" + "net/http" + "net/url" + "reflect" + "strconv" + "sync" + "time" + + "github.com/apernet/quic-go" + "github.com/apernet/quic-go/http3" + "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/net/cnc" + "github.com/xtls/xray-core/common/task" + hyCtx "github.com/xtls/xray-core/proxy/hysteria/ctx" + "github.com/xtls/xray-core/transport/internet" + "github.com/xtls/xray-core/transport/internet/finalmask" + "github.com/xtls/xray-core/transport/internet/hysteria/congestion" + "github.com/xtls/xray-core/transport/internet/hysteria/congestion/bbr" + "github.com/xtls/xray-core/transport/internet/hysteria/udphop" + "github.com/xtls/xray-core/transport/internet/stat" + "github.com/xtls/xray-core/transport/internet/tls" +) + +type udpSessionManagerClient struct { + conn *quic.Conn + m map[uint32]*InterUdpConn + next uint32 + closed bool + mutex sync.RWMutex +} + +func (m *udpSessionManagerClient) close(udpConn *InterUdpConn) { + if !udpConn.closed { + udpConn.closed = true + close(udpConn.ch) + delete(m.m, udpConn.id) + } +} + +func (m *udpSessionManagerClient) run() { + for { + d, err := m.conn.ReceiveDatagram(context.Background()) + if err != nil { + break + } + + if len(d) < 4 { + continue + } + id := binary.BigEndian.Uint32(d[:4]) + + m.feed(id, d) + } + + m.mutex.Lock() + defer m.mutex.Unlock() + + m.closed = true + + for _, udpConn := range m.m { + m.close(udpConn) + } +} + +func (m *udpSessionManagerClient) udp() (*InterUdpConn, error) { + m.mutex.Lock() + defer m.mutex.Unlock() + + if m.closed { + return nil, errors.New("closed") + } + + udpConn := &InterUdpConn{ + conn: m.conn, + local: m.conn.LocalAddr(), + remote: m.conn.RemoteAddr(), + + id: m.next, + ch: make(chan []byte, udpMessageChanSize), + } + udpConn.closeFunc = func() { + m.mutex.Lock() + defer m.mutex.Unlock() + m.close(udpConn) + } + m.m[m.next] = udpConn + m.next++ + + return udpConn, nil +} + +func (m *udpSessionManagerClient) feed(id uint32, d []byte) { + m.mutex.RLock() + defer m.mutex.RUnlock() + + udpConn, ok := m.m[id] + if !ok { + return + } + + select { + case udpConn.ch <- d: + default: + } +} + +type client struct { + ctx context.Context + dest net.Destination + pktConn net.PacketConn + conn *quic.Conn + config *Config + tlsConfig *go_tls.Config + socketConfig *internet.SocketConfig + udpmaskManager *finalmask.UdpmaskManager + quicParams *internet.QuicParams + + udpSM *udpSessionManagerClient + mutex sync.Mutex +} + +func (c *client) status() Status { + if c.conn == nil { + return StatusUnknown + } + select { + case <-c.conn.Context().Done(): + return StatusInactive + default: + return StatusActive + } +} + +func (c *client) close() { + _ = c.conn.CloseWithError(closeErrCodeOK, "") + _ = c.pktConn.Close() + c.pktConn = nil + c.conn = nil + c.udpSM = nil +} + +func (c *client) dial() error { + status := c.status() + if status == StatusActive { + return nil + } + if status == StatusInactive { + c.close() + } + + quicParams := c.quicParams + if quicParams == nil { + quicParams = &internet.QuicParams{ + BbrProfile: string(bbr.ProfileStandard), + UdpHop: &internet.UdpHop{}, + } + } + + var index int + if len(quicParams.UdpHop.Ports) > 0 { + index = rand.Intn(len(quicParams.UdpHop.Ports)) + c.dest.Port = net.Port(quicParams.UdpHop.Ports[index]) + } + + raw, err := internet.DialSystem(c.ctx, c.dest, c.socketConfig) + if err != nil { + return errors.New("failed to dial to dest").Base(err) + } + + var pktConn net.PacketConn + var remote *net.UDPAddr + + switch conn := raw.(type) { + case *internet.PacketConnWrapper: + pktConn = conn.PacketConn + remote = conn.RemoteAddr().(*net.UDPAddr) + case *net.UDPConn: + pktConn = conn + remote = conn.RemoteAddr().(*net.UDPAddr) + case *cnc.Connection: + fakeConn := &internet.FakePacketConn{Conn: conn} + pktConn = fakeConn + remote = fakeConn.RemoteAddr().(*net.UDPAddr) + + if len(quicParams.UdpHop.Ports) > 0 { + raw.Close() + return errors.New("udphop requires being at the outermost level") + } + default: + raw.Close() + return errors.New("unknown conn ", reflect.TypeOf(conn)) + } + + if len(quicParams.UdpHop.Ports) > 0 { + addr := &udphop.UDPHopAddr{ + IP: remote.IP, + Ports: quicParams.UdpHop.Ports, + } + pktConn, err = udphop.NewUDPHopPacketConn(addr, index, quicParams.UdpHop.IntervalMin, quicParams.UdpHop.IntervalMax, c.udphopDialer, pktConn) + if err != nil { + raw.Close() + return errors.New("udphop err").Base(err) + } + } + + if c.udpmaskManager != nil { + pktConn, err = c.udpmaskManager.WrapPacketConnClient(pktConn) + if err != nil { + raw.Close() + return errors.New("mask err").Base(err) + } + } + + quicConfig := &quic.Config{ + InitialStreamReceiveWindow: quicParams.InitStreamReceiveWindow, + MaxStreamReceiveWindow: quicParams.MaxStreamReceiveWindow, + InitialConnectionReceiveWindow: quicParams.InitConnReceiveWindow, + MaxConnectionReceiveWindow: quicParams.MaxConnReceiveWindow, + MaxIdleTimeout: time.Duration(quicParams.MaxIdleTimeout) * time.Second, + KeepAlivePeriod: time.Duration(quicParams.KeepAlivePeriod) * time.Second, + DisablePathMTUDiscovery: quicParams.DisablePathMtuDiscovery, + EnableDatagrams: true, + MaxDatagramFrameSize: MaxDatagramFrameSize, + DisablePathManager: true, + } + if quicParams.InitStreamReceiveWindow == 0 { + quicConfig.InitialStreamReceiveWindow = 8388608 + } + if quicParams.MaxStreamReceiveWindow == 0 { + quicConfig.MaxStreamReceiveWindow = 8388608 + } + if quicParams.InitConnReceiveWindow == 0 { + quicConfig.InitialConnectionReceiveWindow = 8388608 * 5 / 2 + } + if quicParams.MaxConnReceiveWindow == 0 { + quicConfig.MaxConnectionReceiveWindow = 8388608 * 5 / 2 + } + if quicParams.MaxIdleTimeout == 0 { + quicConfig.MaxIdleTimeout = 30 * time.Second + } + // if quicParams.KeepAlivePeriod == 0 { + // quicConfig.KeepAlivePeriod = 10 * time.Second + // } + + var quicConn *quic.Conn + rt := &http3.Transport{ + TLSClientConfig: c.tlsConfig, + QUICConfig: quicConfig, + Dial: func(ctx context.Context, _ string, tlsCfg *go_tls.Config, cfg *quic.Config) (*quic.Conn, error) { + qc, err := quic.DialEarly(ctx, pktConn, remote, tlsCfg, cfg) + if err != nil { + return nil, err + } + quicConn = qc + return qc, nil + }, + } + req := &http.Request{ + Method: http.MethodPost, + URL: &url.URL{ + Scheme: "https", + Host: URLHost, + Path: URLPath, + }, + Header: http.Header{ + RequestHeaderAuth: []string{c.config.Auth}, + CommonHeaderCCRX: []string{strconv.FormatUint(quicParams.BrutalDown, 10)}, + CommonHeaderPadding: []string{authRequestPadding.String()}, + }, + } + resp, err := rt.RoundTrip(req) + if err != nil { + if quicConn != nil { + _ = quicConn.CloseWithError(closeErrCodeProtocolError, "") + } + _ = pktConn.Close() + return errors.New("RoundTrip err").Base(err) + } + if resp.StatusCode != StatusAuthOK { + _ = quicConn.CloseWithError(closeErrCodeProtocolError, "") + _ = pktConn.Close() + return errors.New("auth failed") + } + _ = resp.Body.Close() + + serverUdp, _ := strconv.ParseBool(resp.Header.Get(ResponseHeaderUDPEnabled)) + serverAuto := resp.Header.Get(CommonHeaderCCRX) + serverDown, _ := strconv.ParseUint(serverAuto, 10, 64) + + switch quicParams.Congestion { + case "reno": + errors.LogDebug(c.ctx, "congestion reno") + case "bbr": + errors.LogDebug(c.ctx, "congestion bbr ", quicParams.BbrProfile) + congestion.UseBBR(quicConn, bbr.Profile(quicParams.BbrProfile)) + case "brutal", "": + if serverAuto == "auto" || quicParams.BrutalUp == 0 || serverDown == 0 { + errors.LogDebug(c.ctx, "congestion bbr ", quicParams.BbrProfile) + congestion.UseBBR(quicConn, bbr.Profile(quicParams.BbrProfile)) + } else { + errors.LogDebug(c.ctx, "congestion brutal bytes per second ", min(quicParams.BrutalUp, serverDown)) + congestion.UseBrutal(quicConn, min(quicParams.BrutalUp, serverDown)) + } + case "force-brutal": + errors.LogDebug(c.ctx, "congestion brutal bytes per second ", quicParams.BrutalUp) + congestion.UseBrutal(quicConn, quicParams.BrutalUp) + default: + errors.LogDebug(c.ctx, "congestion reno") + } + + c.pktConn = pktConn + c.conn = quicConn + if serverUdp { + c.udpSM = &udpSessionManagerClient{ + conn: quicConn, + m: make(map[uint32]*InterUdpConn), + next: 1, + } + go c.udpSM.run() + } + + return nil +} + +func (c *client) clean() { + c.mutex.Lock() + defer c.mutex.Unlock() + + if c.status() == StatusInactive { + c.close() + } +} + +func (c *client) tcp() (stat.Connection, error) { + c.mutex.Lock() + defer c.mutex.Unlock() + + err := c.dial() + if err != nil { + return nil, err + } + + stream, err := c.conn.OpenStream() + if err != nil { + return nil, err + } + + return &interConn{ + stream: stream, + local: c.conn.LocalAddr(), + remote: c.conn.RemoteAddr(), + + client: true, + }, nil +} + +func (c *client) udp() (stat.Connection, error) { + c.mutex.Lock() + defer c.mutex.Unlock() + + err := c.dial() + if err != nil { + return nil, err + } + + if c.udpSM == nil { + return nil, errors.New("server does not support udp") + } + + return c.udpSM.udp() +} + +func (c *client) setCtx(ctx context.Context) { + c.mutex.Lock() + defer c.mutex.Unlock() + + c.ctx = ctx +} + +func (c *client) udphopDialer(addr *net.UDPAddr) (net.PacketConn, error) { + c.mutex.Lock() + defer c.mutex.Unlock() + + if c.status() != StatusActive { + errors.LogDebug(context.Background(), "skip hop: disconnected QUIC") + return nil, errors.New() + } + + raw, err := internet.DialSystem(c.ctx, net.UDPDestination(net.IPAddress(addr.IP), net.Port(addr.Port)), c.socketConfig) + if err != nil { + errors.LogDebug(context.Background(), "skip hop: failed to dial to dest") + raw.Close() + return nil, errors.New() + } + + var pktConn net.PacketConn + + switch conn := raw.(type) { + case *internet.PacketConnWrapper: + pktConn = conn.PacketConn + case *net.UDPConn: + pktConn = conn + case *cnc.Connection: + errors.LogDebug(context.Background(), "skip hop: udphop requires being at the outermost level") + raw.Close() + return nil, errors.New() + default: + errors.LogDebug(context.Background(), "skip hop: unknown conn ", reflect.TypeOf(conn)) + raw.Close() + return nil, errors.New() + } + + return pktConn, nil +} + +type clientManager struct { + m map[string]*client + mutex sync.Mutex +} + +func (m *clientManager) clean() { + m.mutex.Lock() + defer m.mutex.Unlock() + + for _, c := range m.m { + c.clean() + } +} + +var manger *clientManager + +func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.MemoryStreamConfig) (stat.Connection, error) { + tlsConfig := tls.ConfigFromStreamSettings(streamSettings) + if tlsConfig == nil { + return nil, errors.New("tls config is nil") + } + + requireDatagram := hyCtx.RequireDatagramFromContext(ctx) + addr := dest.NetAddr() + config := streamSettings.ProtocolSettings.(*Config) + + manger.mutex.Lock() + c, ok := manger.m[addr] + if !ok { + dest.Network = net.Network_UDP + c = &client{ + ctx: ctx, + dest: dest, + config: config, + tlsConfig: tlsConfig.GetTLSConfig(), + socketConfig: streamSettings.SocketSettings, + udpmaskManager: streamSettings.UdpmaskManager, + quicParams: streamSettings.QuicParams, + } + manger.m[addr] = c + } + c.setCtx(ctx) + manger.mutex.Unlock() + + if requireDatagram { + return c.udp() + } + return c.tcp() +} + +func init() { + manger = &clientManager{ + m: make(map[string]*client), + } + (&task.Periodic{ + Interval: 30 * time.Second, + Execute: func() error { + manger.clean() + return nil + }, + }).Start() +} + +func init() { + common.Must(internet.RegisterTransportDialer(protocolName, Dial)) +} diff --git a/subproject/Xray-core-main/transport/internet/hysteria/hub.go b/subproject/Xray-core-main/transport/internet/hysteria/hub.go new file mode 100644 index 00000000..c7a685a1 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/hysteria/hub.go @@ -0,0 +1,455 @@ +package hysteria + +import ( + "context" + gotls "crypto/tls" + "encoding/binary" + "net/http" + "net/http/httputil" + "net/url" + "strconv" + "strings" + "sync" + "time" + + "github.com/apernet/quic-go" + "github.com/apernet/quic-go/http3" + "github.com/apernet/quic-go/quicvarint" + "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/protocol" + "github.com/xtls/xray-core/proxy/hysteria/account" + hyCtx "github.com/xtls/xray-core/proxy/hysteria/ctx" + "github.com/xtls/xray-core/transport/internet" + "github.com/xtls/xray-core/transport/internet/hysteria/congestion" + "github.com/xtls/xray-core/transport/internet/hysteria/congestion/bbr" + "github.com/xtls/xray-core/transport/internet/tls" +) + +type udpSessionManagerServer struct { + conn *quic.Conn + m map[uint32]*InterUdpConn + addConn internet.ConnHandler + stopCh chan struct{} + udpIdleTimeout time.Duration + mutex sync.RWMutex + + user *protocol.MemoryUser +} + +func (m *udpSessionManagerServer) close(udpConn *InterUdpConn) { + if !udpConn.closed { + udpConn.closed = true + close(udpConn.ch) + delete(m.m, udpConn.id) + } +} + +func (m *udpSessionManagerServer) clean() { + ticker := time.NewTicker(idleCleanupInterval) + defer ticker.Stop() + for { + select { + case <-ticker.C: + m.mutex.RLock() + now := time.Now() + timeoutConn := make([]*InterUdpConn, 0, len(m.m)) + for _, udpConn := range m.m { + if now.Sub(udpConn.GetLast()) > m.udpIdleTimeout { + timeoutConn = append(timeoutConn, udpConn) + } + } + m.mutex.RUnlock() + + for _, udpConn := range timeoutConn { + m.mutex.Lock() + m.close(udpConn) + m.mutex.Unlock() + } + case <-m.stopCh: + return + } + } +} + +func (m *udpSessionManagerServer) run() { + for { + d, err := m.conn.ReceiveDatagram(context.Background()) + if err != nil { + break + } + + if len(d) < 4 { + continue + } + id := binary.BigEndian.Uint32(d[:4]) + + m.feed(id, d) + } + + m.mutex.Lock() + defer m.mutex.Unlock() + + close(m.stopCh) + + for _, udpConn := range m.m { + m.close(udpConn) + } +} + +func (m *udpSessionManagerServer) feed(id uint32, d []byte) { + m.mutex.RLock() + udpConn, ok := m.m[id] + if ok { + select { + case udpConn.ch <- d: + default: + } + m.mutex.RUnlock() + return + } + m.mutex.RUnlock() + + m.mutex.Lock() + defer m.mutex.Unlock() + + udpConn, ok = m.m[id] + if !ok { + udpConn = &InterUdpConn{ + conn: m.conn, + local: m.conn.LocalAddr(), + remote: m.conn.RemoteAddr(), + + id: id, + ch: make(chan []byte, udpMessageChanSize), + last: time.Now(), + + user: m.user, + } + udpConn.closeFunc = func() { + m.mutex.Lock() + m.close(udpConn) + m.mutex.Unlock() + } + m.m[id] = udpConn + m.addConn(udpConn) + } + + select { + case udpConn.ch <- d: + default: + } +} + +type httpHandler struct { + ctx context.Context + conn *quic.Conn + addConn internet.ConnHandler + + config *Config + quicParams *internet.QuicParams + validator *account.Validator + masqHandler http.Handler + + auth bool + mutex sync.Mutex + user *protocol.MemoryUser +} + +func (h *httpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPost && r.Host == URLHost && r.URL.Path == URLPath { + h.mutex.Lock() + defer h.mutex.Unlock() + + if h.auth { + w.Header().Set(ResponseHeaderUDPEnabled, strconv.FormatBool(hyCtx.RequireDatagramFromContext(h.ctx))) + w.Header().Set(CommonHeaderCCRX, strconv.FormatUint(h.quicParams.BrutalDown, 10)) + w.Header().Set(CommonHeaderPadding, authResponsePadding.String()) + w.WriteHeader(StatusAuthOK) + return + } + + auth := r.Header.Get(RequestHeaderAuth) + clientDown, _ := strconv.ParseUint(r.Header.Get(CommonHeaderCCRX), 10, 64) + + var user *protocol.MemoryUser + var ok bool + if h.validator != nil { + user = h.validator.Get(auth) + } else if auth == h.config.Auth { + ok = true + } + + if user != nil || ok { + h.auth = true + h.user = user + + switch h.quicParams.Congestion { + case "reno": + errors.LogDebug(context.Background(), h.conn.RemoteAddr(), " ", "congestion reno") + case "bbr": + errors.LogDebug(context.Background(), h.conn.RemoteAddr(), " ", "congestion bbr ", h.quicParams.BbrProfile) + congestion.UseBBR(h.conn, bbr.Profile(h.quicParams.BbrProfile)) + case "brutal", "": + if h.quicParams.BrutalUp == 0 || clientDown == 0 { + errors.LogDebug(context.Background(), h.conn.RemoteAddr(), " ", "congestion bbr ", h.quicParams.BbrProfile) + congestion.UseBBR(h.conn, bbr.Profile(h.quicParams.BbrProfile)) + } else { + errors.LogDebug(context.Background(), h.conn.RemoteAddr(), " ", "congestion brutal bytes per second ", min(h.quicParams.BrutalUp, clientDown)) + congestion.UseBrutal(h.conn, min(h.quicParams.BrutalUp, clientDown)) + } + case "force-brutal": + errors.LogDebug(context.Background(), h.conn.RemoteAddr(), " ", "congestion brutal bytes per second ", h.quicParams.BrutalUp) + congestion.UseBrutal(h.conn, h.quicParams.BrutalUp) + default: + errors.LogDebug(context.Background(), h.conn.RemoteAddr(), " ", "congestion reno") + } + + if hyCtx.RequireDatagramFromContext(h.ctx) { + udpSM := &udpSessionManagerServer{ + conn: h.conn, + m: make(map[uint32]*InterUdpConn), + addConn: h.addConn, + stopCh: make(chan struct{}), + udpIdleTimeout: time.Duration(h.config.UdpIdleTimeout) * time.Second, + + user: h.user, + } + go udpSM.clean() + go udpSM.run() + } + + w.Header().Set(ResponseHeaderUDPEnabled, strconv.FormatBool(hyCtx.RequireDatagramFromContext(h.ctx))) + w.Header().Set(CommonHeaderCCRX, strconv.FormatUint(h.quicParams.BrutalDown, 10)) + w.Header().Set(CommonHeaderPadding, authResponsePadding.String()) + w.WriteHeader(StatusAuthOK) + return + } + } + + h.masqHandler.ServeHTTP(w, r) +} + +func (h *httpHandler) StreamDispatcher(ft http3.FrameType, stream *quic.Stream, err error) (bool, error) { + if err != nil || !h.auth { + return false, nil + } + + switch ft { + case FrameTypeTCPRequest: + if _, err := quicvarint.Read(quicvarint.NewReader(stream)); err != nil { + return false, err + } + + h.addConn(&interConn{ + stream: stream, + local: h.conn.LocalAddr(), + remote: h.conn.RemoteAddr(), + + user: h.user, + }) + return true, nil + default: + return false, nil + } +} + +type Listener struct { + ctx context.Context + pktConn net.PacketConn + listener *quic.Listener + addConn internet.ConnHandler + + config *Config + quicParams *internet.QuicParams + validator *account.Validator + masqHandler http.Handler +} + +func (l *Listener) handleClient(conn *quic.Conn) { + handler := &httpHandler{ + ctx: l.ctx, + conn: conn, + addConn: l.addConn, + + config: l.config, + quicParams: l.quicParams, + validator: l.validator, + masqHandler: l.masqHandler, + } + h3 := http3.Server{ + Handler: handler, + StreamDispatcher: handler.StreamDispatcher, + } + err := h3.ServeQUICConn(conn) + _ = conn.CloseWithError(closeErrCodeOK, "") + errors.LogDebug(context.Background(), conn.RemoteAddr(), " disconnected with err ", err) +} + +func (l *Listener) keepAccepting() { + for { + conn, err := l.listener.Accept(context.Background()) + if err != nil { + errors.LogInfoInner(context.Background(), err, "failed to accept QUIC connection") + break + } + go l.handleClient(conn) + } +} + +func (l *Listener) Addr() net.Addr { + return l.listener.Addr() +} + +func (l *Listener) Close() error { + err := l.listener.Close() + _ = l.pktConn.Close() + return err +} + +func Listen(ctx context.Context, address net.Address, port net.Port, streamSettings *internet.MemoryStreamConfig, handler internet.ConnHandler) (internet.Listener, error) { + if address.Family().IsDomain() { + return nil, errors.New("address is domain") + } + + tlsConfig := tls.ConfigFromStreamSettings(streamSettings) + if tlsConfig == nil { + return nil, errors.New("tls config is nil") + } + + config := streamSettings.ProtocolSettings.(*Config) + + validator := hyCtx.ValidatorFromContext(ctx) + + if config.Auth == "" && validator == nil { + return nil, errors.New("validator is nil") + } + + var masqHandler http.Handler + switch strings.ToLower(config.MasqType) { + case "", "404": + masqHandler = http.NotFoundHandler() + case "file": + masqHandler = http.FileServer(http.Dir(config.MasqFile)) + case "proxy": + u, err := url.Parse(config.MasqUrl) + if err != nil { + return nil, err + } + transport := http.DefaultTransport.(*http.Transport) + if config.MasqUrlInsecure { + transport = transport.Clone() + transport.TLSClientConfig = &gotls.Config{ + InsecureSkipVerify: true, + } + } + masqHandler = &httputil.ReverseProxy{ + Rewrite: func(pr *httputil.ProxyRequest) { + pr.SetURL(u) + if !config.MasqUrlRewriteHost { + pr.Out.Host = pr.In.Host + } + }, + Transport: transport, + ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) { + w.WriteHeader(http.StatusBadGateway) + }, + } + case "string": + masqHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + for k, v := range config.MasqStringHeaders { + w.Header().Set(k, v) + } + if config.MasqStringStatusCode != 0 { + w.WriteHeader(int(config.MasqStringStatusCode)) + } else { + w.WriteHeader(http.StatusOK) + } + _, _ = w.Write([]byte(config.MasqString)) + }) + default: + return nil, errors.New("unknown masq type") + } + + raw, err := internet.ListenSystemPacket(context.Background(), &net.UDPAddr{IP: address.IP(), Port: int(port)}, streamSettings.SocketSettings) + if err != nil { + return nil, err + } + + var pktConn net.PacketConn + pktConn = raw + + if streamSettings.UdpmaskManager != nil { + pktConn, err = streamSettings.UdpmaskManager.WrapPacketConnServer(raw) + if err != nil { + raw.Close() + return nil, errors.New("mask err").Base(err) + } + } + + quicParams := streamSettings.QuicParams + if quicParams == nil { + quicParams = &internet.QuicParams{ + BbrProfile: string(bbr.ProfileStandard), + UdpHop: &internet.UdpHop{}, + } + } + + quicConfig := &quic.Config{ + InitialStreamReceiveWindow: quicParams.InitStreamReceiveWindow, + MaxStreamReceiveWindow: quicParams.MaxStreamReceiveWindow, + InitialConnectionReceiveWindow: quicParams.InitConnReceiveWindow, + MaxConnectionReceiveWindow: quicParams.MaxConnReceiveWindow, + MaxIdleTimeout: time.Duration(quicParams.MaxIdleTimeout) * time.Second, + MaxIncomingStreams: quicParams.MaxIncomingStreams, + DisablePathMTUDiscovery: quicParams.DisablePathMtuDiscovery, + EnableDatagrams: true, + MaxDatagramFrameSize: MaxDatagramFrameSize, + DisablePathManager: true, + } + if quicParams.InitStreamReceiveWindow == 0 { + quicConfig.InitialStreamReceiveWindow = 8388608 + } + if quicParams.MaxStreamReceiveWindow == 0 { + quicConfig.MaxStreamReceiveWindow = 8388608 + } + if quicParams.InitConnReceiveWindow == 0 { + quicConfig.InitialConnectionReceiveWindow = 8388608 * 5 / 2 + } + if quicParams.MaxConnReceiveWindow == 0 { + quicConfig.MaxConnectionReceiveWindow = 8388608 * 5 / 2 + } + if quicParams.MaxIdleTimeout == 0 { + quicConfig.MaxIdleTimeout = 30 * time.Second + } + if quicParams.MaxIncomingStreams == 0 { + quicConfig.MaxIncomingStreams = 1024 + } + + qListener, err := quic.Listen(pktConn, tlsConfig.GetTLSConfig(), quicConfig) + if err != nil { + _ = pktConn.Close() + return nil, err + } + + listener := &Listener{ + ctx: ctx, + pktConn: pktConn, + listener: qListener, + addConn: handler, + + config: config, + quicParams: quicParams, + validator: validator, + masqHandler: masqHandler, + } + + go listener.keepAccepting() + + return listener, nil +} + +func init() { + common.Must(internet.RegisterTransportListener(protocolName, Listen)) +} diff --git a/subproject/Xray-core-main/transport/internet/hysteria/padding/padding.go b/subproject/Xray-core-main/transport/internet/hysteria/padding/padding.go new file mode 100644 index 00000000..b134601e --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/hysteria/padding/padding.go @@ -0,0 +1,24 @@ +package padding + +import ( + "math/rand" +) + +const ( + paddingChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" +) + +// padding specifies a half-open range [Min, Max). +type Padding struct { + Min int + Max int +} + +func (p Padding) String() string { + n := p.Min + rand.Intn(p.Max-p.Min) + bs := make([]byte, n) + for i := range bs { + bs[i] = paddingChars[rand.Intn(len(paddingChars))] + } + return string(bs) +} diff --git a/subproject/Xray-core-main/transport/internet/hysteria/udphop/addr.go b/subproject/Xray-core-main/transport/internet/hysteria/udphop/addr.go new file mode 100644 index 00000000..70dae2a2 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/hysteria/udphop/addr.go @@ -0,0 +1,65 @@ +package udphop + +import ( + "fmt" + "net" +) + +type InvalidPortError struct { + PortStr string +} + +func (e InvalidPortError) Error() string { + return fmt.Sprintf("%s is not a valid port number or range", e.PortStr) +} + +// UDPHopAddr contains an IP address and a list of ports. +type UDPHopAddr struct { + IP net.IP + Ports []uint32 + PortStr string +} + +func (a *UDPHopAddr) Network() string { + return "udphop" +} + +func (a *UDPHopAddr) String() string { + return net.JoinHostPort(a.IP.String(), a.PortStr) +} + +// addrs returns a list of net.Addr's, one for each port. +func (a *UDPHopAddr) addrs() ([]net.Addr, error) { + var addrs []net.Addr + for _, port := range a.Ports { + addr := &net.UDPAddr{ + IP: a.IP, + Port: int(port), + } + addrs = append(addrs, addr) + } + return addrs, nil +} + +// func ResolveUDPHopAddr(addr string) (*UDPHopAddr, error) { +// host, portStr, err := net.SplitHostPort(addr) +// if err != nil { +// return nil, err +// } +// ip, err := net.ResolveIPAddr("ip", host) +// if err != nil { +// return nil, err +// } +// result := &UDPHopAddr{ +// IP: ip.IP, +// PortStr: portStr, +// } + +// pu := utils.ParsePortUnion(portStr) +// if pu == nil { +// return nil, InvalidPortError{portStr} +// } +// result.Ports = pu.Ports() + +// return result, nil +// } diff --git a/subproject/Xray-core-main/transport/internet/hysteria/udphop/conn.go b/subproject/Xray-core-main/transport/internet/hysteria/udphop/conn.go new file mode 100644 index 00000000..50dcc36d --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/hysteria/udphop/conn.go @@ -0,0 +1,305 @@ +package udphop + +import ( + "errors" + "math/rand" + "net" + "sync" + "syscall" + "time" + + "github.com/xtls/xray-core/common/crypto" + "github.com/xtls/xray-core/transport/internet/finalmask" +) + +const ( + packetQueueSize = 1024 + udpBufferSize = finalmask.UDPSize + + defaultHopInterval = 30 * time.Second +) + +type UdpHopPacketConn struct { + Addr net.Addr + Addrs []net.Addr + HopIntervalMin int64 + HopIntervalMax int64 + ListenUDPFunc ListenUDPFunc + + connMutex sync.RWMutex + prevConn net.PacketConn + currentConn net.PacketConn + addrIndex int + + readBufferSize int + writeBufferSize int + + recvQueue chan *udpPacket + closeChan chan struct{} + closed bool + + bufPool sync.Pool +} + +type udpPacket struct { + Buf []byte + N int + Addr net.Addr + Err error +} + +type ListenUDPFunc = func(*net.UDPAddr) (net.PacketConn, error) + +func NewUDPHopPacketConn(addr *UDPHopAddr, index int, intervalMin int64, intervalMax int64, listenUDPFunc ListenUDPFunc, pktConn net.PacketConn) (net.PacketConn, error) { + if intervalMin == 0 || intervalMax == 0 { + intervalMin = int64(defaultHopInterval) + intervalMax = int64(defaultHopInterval) + } + if intervalMin < 5 || intervalMax < 5 { + return nil, errors.New("hop interval must be at least 5 seconds") + } + // if listenUDPFunc == nil { + // listenUDPFunc = func() (net.PacketConn, error) { + // return net.ListenUDP("udp", nil) + // } + // } + if listenUDPFunc == nil { + return nil, errors.New("nil listenUDPFunc") + } + addrs, err := addr.addrs() + if err != nil { + return nil, err + } + // curConn, err := listenUDPFunc() + // if err != nil { + // return nil, err + // } + hConn := &UdpHopPacketConn{ + Addr: addr, + Addrs: addrs, + HopIntervalMin: intervalMin, + HopIntervalMax: intervalMax, + ListenUDPFunc: listenUDPFunc, + prevConn: nil, + currentConn: pktConn, + addrIndex: index, + recvQueue: make(chan *udpPacket, packetQueueSize), + closeChan: make(chan struct{}), + bufPool: sync.Pool{ + New: func() interface{} { + return make([]byte, udpBufferSize) + }, + }, + } + go hConn.recvLoop(pktConn) + go hConn.hopLoop() + return hConn, nil +} + +func (u *UdpHopPacketConn) recvLoop(conn net.PacketConn) { + for { + buf := u.bufPool.Get().([]byte) + n, addr, err := conn.ReadFrom(buf) + if err != nil { + u.bufPool.Put(buf) + var netErr net.Error + if errors.As(err, &netErr) && netErr.Timeout() { + // Only pass through timeout errors here, not permanent errors + // like connection closed. Connection close is normal as we close + // the old connection to exit this loop every time we hop. + u.recvQueue <- &udpPacket{nil, 0, nil, netErr} + } + return + } + select { + case u.recvQueue <- &udpPacket{buf, n, addr, nil}: + // Packet successfully queued + default: + // Queue is full, drop the packet + u.bufPool.Put(buf) + } + } +} + +func (u *UdpHopPacketConn) hopLoop() { + ticker := time.NewTicker(time.Duration(crypto.RandBetween(u.HopIntervalMin, u.HopIntervalMax)) * time.Second) + defer ticker.Stop() + for { + select { + case <-ticker.C: + u.hop() + ticker.Reset(time.Duration(crypto.RandBetween(u.HopIntervalMin, u.HopIntervalMax)) * time.Second) + case <-u.closeChan: + return + } + } +} + +func (u *UdpHopPacketConn) hop() { + u.connMutex.Lock() + defer u.connMutex.Unlock() + if u.closed { + return + } + // Update addrIndex to a new random value + u.addrIndex = rand.Intn(len(u.Addrs)) + newConn, err := u.ListenUDPFunc(u.Addrs[u.addrIndex].(*net.UDPAddr)) + if err != nil { + // Could be temporary, just skip this hop + return + } + // We need to keep receiving packets from the previous connection, + // because otherwise there will be packet loss due to the time gap + // between we hop to a new port and the server acknowledges this change. + // So we do the following: + // Close prevConn, + // move currentConn to prevConn, + // set newConn as currentConn, + // start recvLoop on newConn. + if u.prevConn != nil { + _ = u.prevConn.Close() // recvLoop for this conn will exit + } + u.prevConn = u.currentConn + u.currentConn = newConn + // Set buffer sizes if previously set + if u.readBufferSize > 0 { + _ = trySetReadBuffer(u.currentConn, u.readBufferSize) + } + if u.writeBufferSize > 0 { + _ = trySetWriteBuffer(u.currentConn, u.writeBufferSize) + } + go u.recvLoop(newConn) +} + +func (u *UdpHopPacketConn) ReadFrom(b []byte) (n int, addr net.Addr, err error) { + for { + select { + case p := <-u.recvQueue: + if p.Err != nil { + return 0, nil, p.Err + } + // Currently we do not check whether the packet is from + // the server or not due to performance reasons. + n := copy(b, p.Buf[:p.N]) + u.bufPool.Put(p.Buf) + return n, u.Addr, nil + case <-u.closeChan: + return 0, nil, net.ErrClosed + } + } +} + +func (u *UdpHopPacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) { + u.connMutex.RLock() + defer u.connMutex.RUnlock() + if u.closed { + return 0, net.ErrClosed + } + // Skip the check for now, always write to the server, + // for the same reason as in ReadFrom. + return u.currentConn.WriteTo(b, u.Addrs[u.addrIndex]) +} + +func (u *UdpHopPacketConn) Close() error { + u.connMutex.Lock() + defer u.connMutex.Unlock() + if u.closed { + return nil + } + // Close prevConn and currentConn + // Close closeChan to unblock ReadFrom & hopLoop + // Set closed flag to true to prevent double close + if u.prevConn != nil { + _ = u.prevConn.Close() + } + err := u.currentConn.Close() + close(u.closeChan) + u.closed = true + u.Addrs = nil // For GC + return err +} + +func (u *UdpHopPacketConn) LocalAddr() net.Addr { + u.connMutex.RLock() + defer u.connMutex.RUnlock() + return u.currentConn.LocalAddr() +} + +func (u *UdpHopPacketConn) SetDeadline(t time.Time) error { + u.connMutex.RLock() + defer u.connMutex.RUnlock() + if u.prevConn != nil { + _ = u.prevConn.SetDeadline(t) + } + return u.currentConn.SetDeadline(t) +} + +func (u *UdpHopPacketConn) SetReadDeadline(t time.Time) error { + u.connMutex.RLock() + defer u.connMutex.RUnlock() + if u.prevConn != nil { + _ = u.prevConn.SetReadDeadline(t) + } + return u.currentConn.SetReadDeadline(t) +} + +func (u *UdpHopPacketConn) SetWriteDeadline(t time.Time) error { + u.connMutex.RLock() + defer u.connMutex.RUnlock() + if u.prevConn != nil { + _ = u.prevConn.SetWriteDeadline(t) + } + return u.currentConn.SetWriteDeadline(t) +} + +// UDP-specific methods below + +func (u *UdpHopPacketConn) SetReadBuffer(bytes int) error { + u.connMutex.Lock() + defer u.connMutex.Unlock() + u.readBufferSize = bytes + if u.prevConn != nil { + _ = trySetReadBuffer(u.prevConn, bytes) + } + return trySetReadBuffer(u.currentConn, bytes) +} + +func (u *UdpHopPacketConn) SetWriteBuffer(bytes int) error { + u.connMutex.Lock() + defer u.connMutex.Unlock() + u.writeBufferSize = bytes + if u.prevConn != nil { + _ = trySetWriteBuffer(u.prevConn, bytes) + } + return trySetWriteBuffer(u.currentConn, bytes) +} + +func (u *UdpHopPacketConn) SyscallConn() (syscall.RawConn, error) { + u.connMutex.RLock() + defer u.connMutex.RUnlock() + sc, ok := u.currentConn.(syscall.Conn) + if !ok { + return nil, errors.New("not supported") + } + return sc.SyscallConn() +} + +func trySetReadBuffer(pc net.PacketConn, bytes int) error { + sc, ok := pc.(interface { + SetReadBuffer(bytes int) error + }) + if ok { + return sc.SetReadBuffer(bytes) + } + return nil +} + +func trySetWriteBuffer(pc net.PacketConn, bytes int) error { + sc, ok := pc.(interface { + SetWriteBuffer(bytes int) error + }) + if ok { + return sc.SetWriteBuffer(bytes) + } + return nil +} diff --git a/subproject/Xray-core-main/transport/internet/internet.go b/subproject/Xray-core-main/transport/internet/internet.go new file mode 100644 index 00000000..19529e63 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/internet.go @@ -0,0 +1,16 @@ +package internet + +import ( + "net" + "strings" +) + +func IsValidHTTPHost(request string, config string) bool { + r := strings.ToLower(request) + c := strings.ToLower(config) + if strings.Contains(r, ":") { + h, _, _ := net.SplitHostPort(r) + return h == c + } + return r == c +} diff --git a/subproject/Xray-core-main/transport/internet/kcp/config.go b/subproject/Xray-core-main/transport/internet/kcp/config.go new file mode 100644 index 00000000..fd51118c --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/kcp/config.go @@ -0,0 +1,84 @@ +package kcp + +import ( + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/transport/internet" +) + +// GetMTUValue returns the value of MTU settings. +func (c *Config) GetMTUValue() uint32 { + if c == nil || c.Mtu == nil { + return 1350 + } + return c.Mtu.Value +} + +// GetTTIValue returns the value of TTI settings. +func (c *Config) GetTTIValue() uint32 { + if c == nil || c.Tti == nil { + return 50 + } + return c.Tti.Value +} + +// GetUplinkCapacityValue returns the value of UplinkCapacity settings. +func (c *Config) GetUplinkCapacityValue() uint32 { + if c == nil || c.UplinkCapacity == nil { + return 5 + } + return c.UplinkCapacity.Value +} + +// GetDownlinkCapacityValue returns the value of DownlinkCapacity settings. +func (c *Config) GetDownlinkCapacityValue() uint32 { + if c == nil || c.DownlinkCapacity == nil { + return 20 + } + return c.DownlinkCapacity.Value +} + +// GetWriteBufferSize returns the size of WriterBuffer in bytes. +func (c *Config) GetWriteBufferSize() uint32 { + if c == nil || c.WriteBuffer == nil { + return 2 * 1024 * 1024 + } + return c.WriteBuffer.Size +} + +// GetReadBufferSize returns the size of ReadBuffer in bytes. +// func (c *Config) GetReadBufferSize() uint32 { +// if c == nil || c.ReadBuffer == nil { +// return 2 * 1024 * 1024 +// } +// return c.ReadBuffer.Size +// } + +func (c *Config) GetSendingInFlightSize() uint32 { + size := c.GetUplinkCapacityValue() * 1024 * 1024 / c.GetMTUValue() / (1000 / c.GetTTIValue()) + if size < 8 { + size = 8 + } + return size +} + +func (c *Config) GetSendingBufferSize() uint32 { + return c.GetWriteBufferSize() / c.GetMTUValue() +} + +func (c *Config) GetReceivingInFlightSize() uint32 { + size := c.GetDownlinkCapacityValue() * 1024 * 1024 / c.GetMTUValue() / (1000 / c.GetTTIValue()) + if size < 8 { + size = 8 + } + return size +} + +// func (c *Config) GetReceivingBufferSize() uint32 { +// return c.GetReadBufferSize() / c.GetMTUValue() +// } + +func init() { + common.Must(internet.RegisterProtocolConfigCreator(protocolName, func() interface{} { + return new(Config) + })) +} diff --git a/subproject/Xray-core-main/transport/internet/kcp/config.pb.go b/subproject/Xray-core-main/transport/internet/kcp/config.pb.go new file mode 100644 index 00000000..8a03702b --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/kcp/config.pb.go @@ -0,0 +1,596 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: transport/internet/kcp/config.proto + +package kcp + +import ( + serial "github.com/xtls/xray-core/common/serial" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Maximum Transmission Unit, in bytes. +type MTU struct { + state protoimpl.MessageState `protogen:"open.v1"` + Value uint32 `protobuf:"varint,1,opt,name=value,proto3" json:"value,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MTU) Reset() { + *x = MTU{} + mi := &file_transport_internet_kcp_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MTU) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MTU) ProtoMessage() {} + +func (x *MTU) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_kcp_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MTU.ProtoReflect.Descriptor instead. +func (*MTU) Descriptor() ([]byte, []int) { + return file_transport_internet_kcp_config_proto_rawDescGZIP(), []int{0} +} + +func (x *MTU) GetValue() uint32 { + if x != nil { + return x.Value + } + return 0 +} + +// Transmission Time Interview, in milli-sec. +type TTI struct { + state protoimpl.MessageState `protogen:"open.v1"` + Value uint32 `protobuf:"varint,1,opt,name=value,proto3" json:"value,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TTI) Reset() { + *x = TTI{} + mi := &file_transport_internet_kcp_config_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TTI) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TTI) ProtoMessage() {} + +func (x *TTI) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_kcp_config_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TTI.ProtoReflect.Descriptor instead. +func (*TTI) Descriptor() ([]byte, []int) { + return file_transport_internet_kcp_config_proto_rawDescGZIP(), []int{1} +} + +func (x *TTI) GetValue() uint32 { + if x != nil { + return x.Value + } + return 0 +} + +// Uplink capacity, in MB. +type UplinkCapacity struct { + state protoimpl.MessageState `protogen:"open.v1"` + Value uint32 `protobuf:"varint,1,opt,name=value,proto3" json:"value,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UplinkCapacity) Reset() { + *x = UplinkCapacity{} + mi := &file_transport_internet_kcp_config_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UplinkCapacity) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UplinkCapacity) ProtoMessage() {} + +func (x *UplinkCapacity) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_kcp_config_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UplinkCapacity.ProtoReflect.Descriptor instead. +func (*UplinkCapacity) Descriptor() ([]byte, []int) { + return file_transport_internet_kcp_config_proto_rawDescGZIP(), []int{2} +} + +func (x *UplinkCapacity) GetValue() uint32 { + if x != nil { + return x.Value + } + return 0 +} + +// Downlink capacity, in MB. +type DownlinkCapacity struct { + state protoimpl.MessageState `protogen:"open.v1"` + Value uint32 `protobuf:"varint,1,opt,name=value,proto3" json:"value,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DownlinkCapacity) Reset() { + *x = DownlinkCapacity{} + mi := &file_transport_internet_kcp_config_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DownlinkCapacity) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DownlinkCapacity) ProtoMessage() {} + +func (x *DownlinkCapacity) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_kcp_config_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DownlinkCapacity.ProtoReflect.Descriptor instead. +func (*DownlinkCapacity) Descriptor() ([]byte, []int) { + return file_transport_internet_kcp_config_proto_rawDescGZIP(), []int{3} +} + +func (x *DownlinkCapacity) GetValue() uint32 { + if x != nil { + return x.Value + } + return 0 +} + +type WriteBuffer struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Buffer size in bytes. + Size uint32 `protobuf:"varint,1,opt,name=size,proto3" json:"size,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WriteBuffer) Reset() { + *x = WriteBuffer{} + mi := &file_transport_internet_kcp_config_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WriteBuffer) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WriteBuffer) ProtoMessage() {} + +func (x *WriteBuffer) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_kcp_config_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WriteBuffer.ProtoReflect.Descriptor instead. +func (*WriteBuffer) Descriptor() ([]byte, []int) { + return file_transport_internet_kcp_config_proto_rawDescGZIP(), []int{4} +} + +func (x *WriteBuffer) GetSize() uint32 { + if x != nil { + return x.Size + } + return 0 +} + +type ReadBuffer struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Buffer size in bytes. + Size uint32 `protobuf:"varint,1,opt,name=size,proto3" json:"size,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReadBuffer) Reset() { + *x = ReadBuffer{} + mi := &file_transport_internet_kcp_config_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReadBuffer) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReadBuffer) ProtoMessage() {} + +func (x *ReadBuffer) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_kcp_config_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ReadBuffer.ProtoReflect.Descriptor instead. +func (*ReadBuffer) Descriptor() ([]byte, []int) { + return file_transport_internet_kcp_config_proto_rawDescGZIP(), []int{5} +} + +func (x *ReadBuffer) GetSize() uint32 { + if x != nil { + return x.Size + } + return 0 +} + +type ConnectionReuse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Enable bool `protobuf:"varint,1,opt,name=enable,proto3" json:"enable,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ConnectionReuse) Reset() { + *x = ConnectionReuse{} + mi := &file_transport_internet_kcp_config_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ConnectionReuse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ConnectionReuse) ProtoMessage() {} + +func (x *ConnectionReuse) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_kcp_config_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ConnectionReuse.ProtoReflect.Descriptor instead. +func (*ConnectionReuse) Descriptor() ([]byte, []int) { + return file_transport_internet_kcp_config_proto_rawDescGZIP(), []int{6} +} + +func (x *ConnectionReuse) GetEnable() bool { + if x != nil { + return x.Enable + } + return false +} + +// Pre-shared secret between client and server. It is used for traffic obfuscation. +// Note that if seed is absent in the config, the traffic will still be obfuscated, +// but by a predefined algorithm. +type EncryptionSeed struct { + state protoimpl.MessageState `protogen:"open.v1"` + Seed string `protobuf:"bytes,1,opt,name=seed,proto3" json:"seed,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EncryptionSeed) Reset() { + *x = EncryptionSeed{} + mi := &file_transport_internet_kcp_config_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EncryptionSeed) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EncryptionSeed) ProtoMessage() {} + +func (x *EncryptionSeed) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_kcp_config_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 EncryptionSeed.ProtoReflect.Descriptor instead. +func (*EncryptionSeed) Descriptor() ([]byte, []int) { + return file_transport_internet_kcp_config_proto_rawDescGZIP(), []int{7} +} + +func (x *EncryptionSeed) GetSeed() string { + if x != nil { + return x.Seed + } + return "" +} + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + Mtu *MTU `protobuf:"bytes,1,opt,name=mtu,proto3" json:"mtu,omitempty"` + Tti *TTI `protobuf:"bytes,2,opt,name=tti,proto3" json:"tti,omitempty"` + UplinkCapacity *UplinkCapacity `protobuf:"bytes,3,opt,name=uplink_capacity,json=uplinkCapacity,proto3" json:"uplink_capacity,omitempty"` + DownlinkCapacity *DownlinkCapacity `protobuf:"bytes,4,opt,name=downlink_capacity,json=downlinkCapacity,proto3" json:"downlink_capacity,omitempty"` + Congestion bool `protobuf:"varint,5,opt,name=congestion,proto3" json:"congestion,omitempty"` + WriteBuffer *WriteBuffer `protobuf:"bytes,6,opt,name=write_buffer,json=writeBuffer,proto3" json:"write_buffer,omitempty"` + ReadBuffer *ReadBuffer `protobuf:"bytes,7,opt,name=read_buffer,json=readBuffer,proto3" json:"read_buffer,omitempty"` + HeaderConfig *serial.TypedMessage `protobuf:"bytes,8,opt,name=header_config,json=headerConfig,proto3" json:"header_config,omitempty"` + Seed *EncryptionSeed `protobuf:"bytes,10,opt,name=seed,proto3" json:"seed,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_transport_internet_kcp_config_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_kcp_config_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_transport_internet_kcp_config_proto_rawDescGZIP(), []int{8} +} + +func (x *Config) GetMtu() *MTU { + if x != nil { + return x.Mtu + } + return nil +} + +func (x *Config) GetTti() *TTI { + if x != nil { + return x.Tti + } + return nil +} + +func (x *Config) GetUplinkCapacity() *UplinkCapacity { + if x != nil { + return x.UplinkCapacity + } + return nil +} + +func (x *Config) GetDownlinkCapacity() *DownlinkCapacity { + if x != nil { + return x.DownlinkCapacity + } + return nil +} + +func (x *Config) GetCongestion() bool { + if x != nil { + return x.Congestion + } + return false +} + +func (x *Config) GetWriteBuffer() *WriteBuffer { + if x != nil { + return x.WriteBuffer + } + return nil +} + +func (x *Config) GetReadBuffer() *ReadBuffer { + if x != nil { + return x.ReadBuffer + } + return nil +} + +func (x *Config) GetHeaderConfig() *serial.TypedMessage { + if x != nil { + return x.HeaderConfig + } + return nil +} + +func (x *Config) GetSeed() *EncryptionSeed { + if x != nil { + return x.Seed + } + return nil +} + +var File_transport_internet_kcp_config_proto protoreflect.FileDescriptor + +const file_transport_internet_kcp_config_proto_rawDesc = "" + + "\n" + + "#transport/internet/kcp/config.proto\x12\x1bxray.transport.internet.kcp\x1a!common/serial/typed_message.proto\"\x1b\n" + + "\x03MTU\x12\x14\n" + + "\x05value\x18\x01 \x01(\rR\x05value\"\x1b\n" + + "\x03TTI\x12\x14\n" + + "\x05value\x18\x01 \x01(\rR\x05value\"&\n" + + "\x0eUplinkCapacity\x12\x14\n" + + "\x05value\x18\x01 \x01(\rR\x05value\"(\n" + + "\x10DownlinkCapacity\x12\x14\n" + + "\x05value\x18\x01 \x01(\rR\x05value\"!\n" + + "\vWriteBuffer\x12\x12\n" + + "\x04size\x18\x01 \x01(\rR\x04size\" \n" + + "\n" + + "ReadBuffer\x12\x12\n" + + "\x04size\x18\x01 \x01(\rR\x04size\")\n" + + "\x0fConnectionReuse\x12\x16\n" + + "\x06enable\x18\x01 \x01(\bR\x06enable\"$\n" + + "\x0eEncryptionSeed\x12\x12\n" + + "\x04seed\x18\x01 \x01(\tR\x04seed\"\xe7\x04\n" + + "\x06Config\x122\n" + + "\x03mtu\x18\x01 \x01(\v2 .xray.transport.internet.kcp.MTUR\x03mtu\x122\n" + + "\x03tti\x18\x02 \x01(\v2 .xray.transport.internet.kcp.TTIR\x03tti\x12T\n" + + "\x0fuplink_capacity\x18\x03 \x01(\v2+.xray.transport.internet.kcp.UplinkCapacityR\x0euplinkCapacity\x12Z\n" + + "\x11downlink_capacity\x18\x04 \x01(\v2-.xray.transport.internet.kcp.DownlinkCapacityR\x10downlinkCapacity\x12\x1e\n" + + "\n" + + "congestion\x18\x05 \x01(\bR\n" + + "congestion\x12K\n" + + "\fwrite_buffer\x18\x06 \x01(\v2(.xray.transport.internet.kcp.WriteBufferR\vwriteBuffer\x12H\n" + + "\vread_buffer\x18\a \x01(\v2'.xray.transport.internet.kcp.ReadBufferR\n" + + "readBuffer\x12E\n" + + "\rheader_config\x18\b \x01(\v2 .xray.common.serial.TypedMessageR\fheaderConfig\x12?\n" + + "\x04seed\x18\n" + + " \x01(\v2+.xray.transport.internet.kcp.EncryptionSeedR\x04seedJ\x04\b\t\x10\n" + + "Bs\n" + + "\x1fcom.xray.transport.internet.kcpP\x01Z0github.com/xtls/xray-core/transport/internet/kcp\xaa\x02\x1bXray.Transport.Internet.Kcpb\x06proto3" + +var ( + file_transport_internet_kcp_config_proto_rawDescOnce sync.Once + file_transport_internet_kcp_config_proto_rawDescData []byte +) + +func file_transport_internet_kcp_config_proto_rawDescGZIP() []byte { + file_transport_internet_kcp_config_proto_rawDescOnce.Do(func() { + file_transport_internet_kcp_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_transport_internet_kcp_config_proto_rawDesc), len(file_transport_internet_kcp_config_proto_rawDesc))) + }) + return file_transport_internet_kcp_config_proto_rawDescData +} + +var file_transport_internet_kcp_config_proto_msgTypes = make([]protoimpl.MessageInfo, 9) +var file_transport_internet_kcp_config_proto_goTypes = []any{ + (*MTU)(nil), // 0: xray.transport.internet.kcp.MTU + (*TTI)(nil), // 1: xray.transport.internet.kcp.TTI + (*UplinkCapacity)(nil), // 2: xray.transport.internet.kcp.UplinkCapacity + (*DownlinkCapacity)(nil), // 3: xray.transport.internet.kcp.DownlinkCapacity + (*WriteBuffer)(nil), // 4: xray.transport.internet.kcp.WriteBuffer + (*ReadBuffer)(nil), // 5: xray.transport.internet.kcp.ReadBuffer + (*ConnectionReuse)(nil), // 6: xray.transport.internet.kcp.ConnectionReuse + (*EncryptionSeed)(nil), // 7: xray.transport.internet.kcp.EncryptionSeed + (*Config)(nil), // 8: xray.transport.internet.kcp.Config + (*serial.TypedMessage)(nil), // 9: xray.common.serial.TypedMessage +} +var file_transport_internet_kcp_config_proto_depIdxs = []int32{ + 0, // 0: xray.transport.internet.kcp.Config.mtu:type_name -> xray.transport.internet.kcp.MTU + 1, // 1: xray.transport.internet.kcp.Config.tti:type_name -> xray.transport.internet.kcp.TTI + 2, // 2: xray.transport.internet.kcp.Config.uplink_capacity:type_name -> xray.transport.internet.kcp.UplinkCapacity + 3, // 3: xray.transport.internet.kcp.Config.downlink_capacity:type_name -> xray.transport.internet.kcp.DownlinkCapacity + 4, // 4: xray.transport.internet.kcp.Config.write_buffer:type_name -> xray.transport.internet.kcp.WriteBuffer + 5, // 5: xray.transport.internet.kcp.Config.read_buffer:type_name -> xray.transport.internet.kcp.ReadBuffer + 9, // 6: xray.transport.internet.kcp.Config.header_config:type_name -> xray.common.serial.TypedMessage + 7, // 7: xray.transport.internet.kcp.Config.seed:type_name -> xray.transport.internet.kcp.EncryptionSeed + 8, // [8:8] is the sub-list for method output_type + 8, // [8:8] is the sub-list for method input_type + 8, // [8:8] is the sub-list for extension type_name + 8, // [8:8] is the sub-list for extension extendee + 0, // [0:8] is the sub-list for field type_name +} + +func init() { file_transport_internet_kcp_config_proto_init() } +func file_transport_internet_kcp_config_proto_init() { + if File_transport_internet_kcp_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_kcp_config_proto_rawDesc), len(file_transport_internet_kcp_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 9, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_transport_internet_kcp_config_proto_goTypes, + DependencyIndexes: file_transport_internet_kcp_config_proto_depIdxs, + MessageInfos: file_transport_internet_kcp_config_proto_msgTypes, + }.Build() + File_transport_internet_kcp_config_proto = out.File + file_transport_internet_kcp_config_proto_goTypes = nil + file_transport_internet_kcp_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/transport/internet/kcp/config.proto b/subproject/Xray-core-main/transport/internet/kcp/config.proto new file mode 100644 index 00000000..8690f09f --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/kcp/config.proto @@ -0,0 +1,63 @@ +syntax = "proto3"; + +package xray.transport.internet.kcp; +option csharp_namespace = "Xray.Transport.Internet.Kcp"; +option go_package = "github.com/xtls/xray-core/transport/internet/kcp"; +option java_package = "com.xray.transport.internet.kcp"; +option java_multiple_files = true; + +import "common/serial/typed_message.proto"; + +// Maximum Transmission Unit, in bytes. +message MTU { + uint32 value = 1; +} + +// Transmission Time Interview, in milli-sec. +message TTI { + uint32 value = 1; +} + +// Uplink capacity, in MB. +message UplinkCapacity { + uint32 value = 1; +} + +// Downlink capacity, in MB. +message DownlinkCapacity { + uint32 value = 1; +} + +message WriteBuffer { + // Buffer size in bytes. + uint32 size = 1; +} + +message ReadBuffer { + // Buffer size in bytes. + uint32 size = 1; +} + +message ConnectionReuse { + bool enable = 1; +} + +// Pre-shared secret between client and server. It is used for traffic obfuscation. +// Note that if seed is absent in the config, the traffic will still be obfuscated, +// but by a predefined algorithm. +message EncryptionSeed { + string seed = 1; +} + +message Config { + MTU mtu = 1; + TTI tti = 2; + UplinkCapacity uplink_capacity = 3; + DownlinkCapacity downlink_capacity = 4; + bool congestion = 5; + WriteBuffer write_buffer = 6; + ReadBuffer read_buffer = 7; + xray.common.serial.TypedMessage header_config = 8; + reserved 9; + EncryptionSeed seed = 10; +} diff --git a/subproject/Xray-core-main/transport/internet/kcp/connection.go b/subproject/Xray-core-main/transport/internet/kcp/connection.go new file mode 100644 index 00000000..90c4b7b8 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/kcp/connection.go @@ -0,0 +1,663 @@ +package kcp + +import ( + "bytes" + "context" + "io" + "net" + "runtime" + "sync" + "sync/atomic" + "time" + + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/signal" + "github.com/xtls/xray-core/common/signal/semaphore" +) + +var ( + ErrIOTimeout = errors.New("Read/Write timeout") + ErrClosedListener = errors.New("Listener closed.") + ErrClosedConnection = errors.New("Connection closed.") +) + +// State of the connection +type State int32 + +// Is returns true if current State is one of the candidates. +func (s State) Is(states ...State) bool { + for _, state := range states { + if s == state { + return true + } + } + return false +} + +const ( + StateActive State = 0 // Connection is active + StateReadyToClose State = 1 // Connection is closed locally + StatePeerClosed State = 2 // Connection is closed on remote + StateTerminating State = 3 // Connection is ready to be destroyed locally + StatePeerTerminating State = 4 // Connection is ready to be destroyed on remote + StateTerminated State = 5 // Connection is destroyed. +) + +func nowMillisec() int64 { + now := time.Now() + return now.Unix()*1000 + int64(now.Nanosecond()/1000000) +} + +type RoundTripInfo struct { + sync.RWMutex + variation uint32 + srtt uint32 + rto uint32 + minRtt uint32 + updatedTimestamp uint32 +} + +func (info *RoundTripInfo) UpdatePeerRTO(rto uint32, current uint32) { + info.Lock() + defer info.Unlock() + + if current-info.updatedTimestamp < 3000 { + return + } + + info.updatedTimestamp = current + info.rto = rto +} + +func (info *RoundTripInfo) Update(rtt uint32, current uint32) { + if rtt > 0x7FFFFFFF { + return + } + info.Lock() + defer info.Unlock() + + // https://tools.ietf.org/html/rfc6298 + if info.srtt == 0 { + info.srtt = rtt + info.variation = rtt / 2 + } else { + delta := rtt - info.srtt + if info.srtt > rtt { + delta = info.srtt - rtt + } + info.variation = (3*info.variation + delta) / 4 + info.srtt = (7*info.srtt + rtt) / 8 + if info.srtt < info.minRtt { + info.srtt = info.minRtt + } + } + var rto uint32 + if info.minRtt < 4*info.variation { + rto = info.srtt + 4*info.variation + } else { + rto = info.srtt + info.variation + } + + if rto > 10000 { + rto = 10000 + } + info.rto = rto * 5 / 4 + info.updatedTimestamp = current +} + +func (info *RoundTripInfo) Timeout() uint32 { + info.RLock() + defer info.RUnlock() + + return info.rto +} + +func (info *RoundTripInfo) SmoothedTime() uint32 { + info.RLock() + defer info.RUnlock() + + return info.srtt +} + +type Updater struct { + interval int64 + shouldContinue func() bool + shouldTerminate func() bool + updateFunc func() + notifier *semaphore.Instance +} + +func NewUpdater(interval uint32, shouldContinue func() bool, shouldTerminate func() bool, updateFunc func()) *Updater { + u := &Updater{ + interval: int64(time.Duration(interval) * time.Millisecond), + shouldContinue: shouldContinue, + shouldTerminate: shouldTerminate, + updateFunc: updateFunc, + notifier: semaphore.New(1), + } + return u +} + +func (u *Updater) WakeUp() { + select { + case <-u.notifier.Wait(): + go u.run() + default: + } +} + +func (u *Updater) run() { + defer u.notifier.Signal() + + if u.shouldTerminate() { + return + } + ticker := time.NewTicker(u.Interval()) + for u.shouldContinue() { + u.updateFunc() + <-ticker.C + } + ticker.Stop() +} + +func (u *Updater) Interval() time.Duration { + return time.Duration(atomic.LoadInt64(&u.interval)) +} + +func (u *Updater) SetInterval(d time.Duration) { + atomic.StoreInt64(&u.interval, int64(d)) +} + +type ConnMetadata struct { + LocalAddr net.Addr + RemoteAddr net.Addr + Conversation uint16 +} + +// Connection is a KCP connection over UDP. +type Connection struct { + meta ConnMetadata + closer io.Closer + rd time.Time + wd time.Time // write deadline + since int64 + dataInput *signal.Notifier + dataOutput *signal.Notifier + Config *Config + + state State + stateBeginTime uint32 + lastIncomingTime uint32 + lastPingTime uint32 + + mss uint32 + roundTrip *RoundTripInfo + + receivingWorker *ReceivingWorker + sendingWorker *SendingWorker + + output SegmentWriter + + dataUpdater *Updater + pingUpdater *Updater +} + +// NewConnection create a new KCP connection between local and remote. +func NewConnection(meta ConnMetadata, writer io.Writer, closer io.Closer, config *Config) *Connection { + errors.LogInfo(context.Background(), "#", meta.Conversation, " creating connection to ", meta.RemoteAddr) + + conn := &Connection{ + meta: meta, + closer: closer, + since: nowMillisec(), + dataInput: signal.NewNotifier(), + dataOutput: signal.NewNotifier(), + Config: config, + output: NewRetryableWriter(NewSegmentWriter(writer)), + mss: config.GetMTUValue() - DataSegmentOverhead, + roundTrip: &RoundTripInfo{ + rto: 100, + minRtt: config.GetTTIValue(), + }, + } + + conn.receivingWorker = NewReceivingWorker(conn) + conn.sendingWorker = NewSendingWorker(conn) + + isTerminating := func() bool { + return conn.State().Is(StateTerminating, StateTerminated) + } + isTerminated := func() bool { + return conn.State() == StateTerminated + } + conn.dataUpdater = NewUpdater( + config.GetTTIValue(), + func() bool { + return !isTerminating() && (conn.sendingWorker.UpdateNecessary() || conn.receivingWorker.UpdateNecessary()) + }, + isTerminating, + conn.updateTask) + conn.pingUpdater = NewUpdater( + 5000, // 5 seconds + func() bool { return !isTerminated() }, + isTerminated, + conn.updateTask) + conn.pingUpdater.WakeUp() + + return conn +} + +func (c *Connection) Elapsed() uint32 { + return uint32(nowMillisec() - c.since) +} + +// ReadMultiBuffer implements buf.Reader. +func (c *Connection) ReadMultiBuffer() (buf.MultiBuffer, error) { + if c == nil { + return nil, io.EOF + } + + for { + if c.State().Is(StateReadyToClose, StateTerminating, StateTerminated) { + return nil, io.EOF + } + mb := c.receivingWorker.ReadMultiBuffer() + if !mb.IsEmpty() { + c.dataUpdater.WakeUp() + return mb, nil + } + + if c.State() == StatePeerTerminating { + return nil, io.EOF + } + + if err := c.waitForDataInput(); err != nil { + return nil, err + } + } +} + +func (c *Connection) waitForDataInput() error { + for i := 0; i < 16; i++ { + select { + case <-c.dataInput.Wait(): + return nil + default: + runtime.Gosched() + } + } + + duration := time.Second * 16 + if !c.rd.IsZero() { + duration = time.Until(c.rd) + if duration < 0 { + return ErrIOTimeout + } + } + + timeout := time.NewTimer(duration) + defer timeout.Stop() + + select { + case <-c.dataInput.Wait(): + case <-timeout.C: + if !c.rd.IsZero() && c.rd.Before(time.Now()) { + return ErrIOTimeout + } + } + + return nil +} + +// Read implements the Conn Read method. +func (c *Connection) Read(b []byte) (int, error) { + if c == nil { + return 0, io.EOF + } + + for { + if c.State().Is(StateReadyToClose, StateTerminating, StateTerminated) { + return 0, io.EOF + } + nBytes := c.receivingWorker.Read(b) + if nBytes > 0 { + c.dataUpdater.WakeUp() + return nBytes, nil + } + + if err := c.waitForDataInput(); err != nil { + return 0, err + } + } +} + +func (c *Connection) waitForDataOutput() error { + for i := 0; i < 16; i++ { + select { + case <-c.dataOutput.Wait(): + return nil + default: + runtime.Gosched() + } + } + + duration := time.Second * 16 + if !c.wd.IsZero() { + duration = time.Until(c.wd) + if duration < 0 { + return ErrIOTimeout + } + } + + timeout := time.NewTimer(duration) + defer timeout.Stop() + + select { + case <-c.dataOutput.Wait(): + case <-timeout.C: + if !c.wd.IsZero() && c.wd.Before(time.Now()) { + return ErrIOTimeout + } + } + + return nil +} + +// Write implements io.Writer. +func (c *Connection) Write(b []byte) (int, error) { + reader := bytes.NewReader(b) + if err := c.writeMultiBufferInternal(reader); err != nil { + return 0, err + } + return len(b), nil +} + +// WriteMultiBuffer implements buf.Writer. +func (c *Connection) WriteMultiBuffer(mb buf.MultiBuffer) error { + reader := &buf.MultiBufferContainer{ + MultiBuffer: mb, + } + defer reader.Close() + + return c.writeMultiBufferInternal(reader) +} + +func (c *Connection) writeMultiBufferInternal(reader io.Reader) error { + updatePending := false + defer func() { + if updatePending { + c.dataUpdater.WakeUp() + } + }() + + var b *buf.Buffer + defer b.Release() + + for { + for { + if c == nil || c.State() != StateActive { + return io.ErrClosedPipe + } + + if b == nil { + b = buf.New() + _, err := b.ReadFrom(io.LimitReader(reader, int64(c.mss))) + if err != nil { + return nil + } + } + + if !c.sendingWorker.Push(b) { + break + } + updatePending = true + b = nil + } + + if updatePending { + c.dataUpdater.WakeUp() + updatePending = false + } + + if err := c.waitForDataOutput(); err != nil { + return err + } + } +} + +func (c *Connection) SetState(state State) { + current := c.Elapsed() + atomic.StoreInt32((*int32)(&c.state), int32(state)) + atomic.StoreUint32(&c.stateBeginTime, current) + errors.LogDebug(context.Background(), "#", c.meta.Conversation, " entering state ", state, " at ", current) + + switch state { + case StateReadyToClose: + c.receivingWorker.CloseRead() + case StatePeerClosed: + c.sendingWorker.CloseWrite() + case StateTerminating: + c.receivingWorker.CloseRead() + c.sendingWorker.CloseWrite() + c.pingUpdater.SetInterval(time.Second) + case StatePeerTerminating: + c.sendingWorker.CloseWrite() + c.pingUpdater.SetInterval(time.Second) + case StateTerminated: + c.receivingWorker.CloseRead() + c.sendingWorker.CloseWrite() + c.pingUpdater.SetInterval(time.Second) + c.dataUpdater.WakeUp() + c.pingUpdater.WakeUp() + go c.Terminate() + } +} + +// Close closes the connection. +func (c *Connection) Close() error { + if c == nil { + return ErrClosedConnection + } + + c.dataInput.Signal() + c.dataOutput.Signal() + + switch c.State() { + case StateReadyToClose, StateTerminating, StateTerminated: + return ErrClosedConnection + case StateActive: + c.SetState(StateReadyToClose) + case StatePeerClosed: + c.SetState(StateTerminating) + case StatePeerTerminating: + c.SetState(StateTerminated) + } + + errors.LogInfo(context.Background(), "#", c.meta.Conversation, " closing connection to ", c.meta.RemoteAddr) + + return nil +} + +// LocalAddr returns the local network address. The Addr returned is shared by all invocations of LocalAddr, so do not modify it. +func (c *Connection) LocalAddr() net.Addr { + if c == nil { + return nil + } + return c.meta.LocalAddr +} + +// RemoteAddr returns the remote network address. The Addr returned is shared by all invocations of RemoteAddr, so do not modify it. +func (c *Connection) RemoteAddr() net.Addr { + if c == nil { + return nil + } + return c.meta.RemoteAddr +} + +// SetDeadline sets the deadline associated with the listener. A zero time value disables the deadline. +func (c *Connection) SetDeadline(t time.Time) error { + if err := c.SetReadDeadline(t); err != nil { + return err + } + return c.SetWriteDeadline(t) +} + +// SetReadDeadline implements the Conn SetReadDeadline method. +func (c *Connection) SetReadDeadline(t time.Time) error { + if c == nil || c.State() != StateActive { + return ErrClosedConnection + } + c.rd = t + return nil +} + +// SetWriteDeadline implements the Conn SetWriteDeadline method. +func (c *Connection) SetWriteDeadline(t time.Time) error { + if c == nil || c.State() != StateActive { + return ErrClosedConnection + } + c.wd = t + return nil +} + +// kcp update, input loop +func (c *Connection) updateTask() { + c.flush() +} + +func (c *Connection) Terminate() { + if c == nil { + return + } + errors.LogInfo(context.Background(), "#", c.meta.Conversation, " terminating connection to ", c.RemoteAddr()) + + // v.SetState(StateTerminated) + c.dataInput.Signal() + c.dataOutput.Signal() + + c.closer.Close() + c.sendingWorker.Release() + c.receivingWorker.Release() +} + +func (c *Connection) HandleOption(opt SegmentOption) { + if (opt & SegmentOptionClose) == SegmentOptionClose { + c.OnPeerClosed() + } +} + +func (c *Connection) OnPeerClosed() { + switch c.State() { + case StateReadyToClose: + c.SetState(StateTerminating) + case StateActive: + c.SetState(StatePeerClosed) + } +} + +// Input when you received a low level packet (eg. UDP packet), call it +func (c *Connection) Input(segments []Segment) { + current := c.Elapsed() + atomic.StoreUint32(&c.lastIncomingTime, current) + + for _, seg := range segments { + if seg.Conversation() != c.meta.Conversation { + break + } + + switch seg := seg.(type) { + case *DataSegment: + c.HandleOption(seg.Option) + c.receivingWorker.ProcessSegment(seg) + if c.receivingWorker.IsDataAvailable() { + c.dataInput.Signal() + } + c.dataUpdater.WakeUp() + case *AckSegment: + c.HandleOption(seg.Option) + c.sendingWorker.ProcessSegment(current, seg, c.roundTrip.Timeout()) + c.dataOutput.Signal() + c.dataUpdater.WakeUp() + case *CmdOnlySegment: + c.HandleOption(seg.Option) + if seg.Command() == CommandTerminate { + switch c.State() { + case StateActive, StatePeerClosed: + c.SetState(StatePeerTerminating) + case StateReadyToClose: + c.SetState(StateTerminating) + case StateTerminating: + c.SetState(StateTerminated) + } + } + if seg.Option == SegmentOptionClose || seg.Command() == CommandTerminate { + c.dataInput.Signal() + c.dataOutput.Signal() + } + c.sendingWorker.ProcessReceivingNext(seg.ReceivingNext) + c.receivingWorker.ProcessSendingNext(seg.SendingNext) + c.roundTrip.UpdatePeerRTO(seg.PeerRTO, current) + seg.Release() + default: + } + } +} + +func (c *Connection) flush() { + current := c.Elapsed() + + if c.State() == StateTerminated { + return + } + if c.State() == StateActive && current-atomic.LoadUint32(&c.lastIncomingTime) >= 30000 { + c.Close() + } + if c.State() == StateReadyToClose && c.sendingWorker.IsEmpty() { + c.SetState(StateTerminating) + } + + if c.State() == StateTerminating { + errors.LogDebug(context.Background(), "#", c.meta.Conversation, " sending terminating cmd.") + c.Ping(current, CommandTerminate) + + if current-atomic.LoadUint32(&c.stateBeginTime) > 8000 { + c.SetState(StateTerminated) + } + return + } + if c.State() == StatePeerTerminating && current-atomic.LoadUint32(&c.stateBeginTime) > 4000 { + c.SetState(StateTerminating) + } + + if c.State() == StateReadyToClose && current-atomic.LoadUint32(&c.stateBeginTime) > 15000 { + c.SetState(StateTerminating) + } + + // flush acknowledges + c.receivingWorker.Flush(current) + c.sendingWorker.Flush(current) + + if current-atomic.LoadUint32(&c.lastPingTime) >= 3000 { + c.Ping(current, CommandPing) + } +} + +func (c *Connection) State() State { + return State(atomic.LoadInt32((*int32)(&c.state))) +} + +func (c *Connection) Ping(current uint32, cmd Command) { + seg := NewCmdOnlySegment() + seg.Conv = c.meta.Conversation + seg.Cmd = cmd + seg.ReceivingNext = c.receivingWorker.NextNumber() + seg.SendingNext = c.sendingWorker.FirstUnacknowledged() + seg.PeerRTO = c.roundTrip.Timeout() + if c.State() == StateReadyToClose { + seg.Option = SegmentOptionClose + } + c.output.Write(seg) + atomic.StoreUint32(&c.lastPingTime, current) + seg.Release() +} diff --git a/subproject/Xray-core-main/transport/internet/kcp/connection_test.go b/subproject/Xray-core-main/transport/internet/kcp/connection_test.go new file mode 100644 index 00000000..81bc6703 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/kcp/connection_test.go @@ -0,0 +1,36 @@ +package kcp_test + +import ( + "io" + "testing" + "time" + + "github.com/xtls/xray-core/common/buf" + . "github.com/xtls/xray-core/transport/internet/kcp" +) + +type NoOpCloser int + +func (NoOpCloser) Close() error { + return nil +} + +func TestConnectionReadTimeout(t *testing.T) { + conn := NewConnection(ConnMetadata{Conversation: 1}, buf.DiscardBytes, NoOpCloser(0), &Config{}) + conn.SetReadDeadline(time.Now().Add(time.Second)) + + b := make([]byte, 1024) + nBytes, err := conn.Read(b) + if nBytes != 0 || err == nil { + t.Error("unexpected read: ", nBytes, err) + } + + conn.Terminate() +} + +func TestConnectionInterface(t *testing.T) { + _ = (io.Writer)(new(Connection)) + _ = (io.Reader)(new(Connection)) + _ = (buf.Reader)(new(Connection)) + _ = (buf.Writer)(new(Connection)) +} diff --git a/subproject/Xray-core-main/transport/internet/kcp/dialer.go b/subproject/Xray-core-main/transport/internet/kcp/dialer.go new file mode 100644 index 00000000..310bbd53 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/kcp/dialer.go @@ -0,0 +1,122 @@ +package kcp + +import ( + "context" + "io" + reflect "reflect" + "sync/atomic" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/dice" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/net/cnc" + "github.com/xtls/xray-core/transport/internet" + "github.com/xtls/xray-core/transport/internet/stat" + "github.com/xtls/xray-core/transport/internet/tls" +) + +var globalConv = uint32(dice.RollUint16()) + +func fetchInput(_ context.Context, input io.Reader, reader PacketReader, conn *Connection) { + cache := make(chan *buf.Buffer, 1024) + go func() { + for { + payload := buf.New() + if _, err := payload.ReadFrom(input); err != nil { + payload.Release() + close(cache) + return + } + select { + case cache <- payload: + default: + payload.Release() + } + } + }() + + for payload := range cache { + segments := reader.Read(payload.Bytes()) + payload.Release() + if len(segments) > 0 { + conn.Input(segments) + } + } +} + +// DialKCP dials a new KCP connections to the specific destination. +func DialKCP(ctx context.Context, dest net.Destination, streamSettings *internet.MemoryStreamConfig) (stat.Connection, error) { + dest.Network = net.Network_UDP + errors.LogInfo(ctx, "dialing mKCP to ", dest) + + conn, err := internet.DialSystem(ctx, dest, streamSettings.SocketSettings) + if err != nil { + return nil, errors.New("failed to dial to dest: ", err).AtWarning().Base(err) + } + + if streamSettings.UdpmaskManager != nil { + switch c := conn.(type) { + case *internet.PacketConnWrapper: + pktConn, err := streamSettings.UdpmaskManager.WrapPacketConnClient(c.PacketConn) + if err != nil { + conn.Close() + return nil, errors.New("mask err").Base(err) + } + c.PacketConn = pktConn + case *net.UDPConn: + pktConn, err := streamSettings.UdpmaskManager.WrapPacketConnClient(c) + if err != nil { + conn.Close() + return nil, errors.New("mask err").Base(err) + } + conn = &internet.PacketConnWrapper{ + PacketConn: pktConn, + Dest: c.RemoteAddr().(*net.UDPAddr), + } + case *cnc.Connection: + fakeConn := &internet.FakePacketConn{Conn: c} + pktConn, err := streamSettings.UdpmaskManager.WrapPacketConnClient(fakeConn) + if err != nil { + conn.Close() + return nil, errors.New("mask err").Base(err) + } + conn = &internet.PacketConnWrapper{ + PacketConn: pktConn, + Dest: &net.UDPAddr{ + IP: []byte{0, 0, 0, 0}, + Port: 0, + }, + } + default: + conn.Close() + return nil, errors.New("unknown conn ", reflect.TypeOf(c)) + } + } + + kcpSettings := streamSettings.ProtocolSettings.(*Config) + + reader := &KCPPacketReader{} + + conv := uint16(atomic.AddUint32(&globalConv, 1)) + session := NewConnection(ConnMetadata{ + LocalAddr: conn.LocalAddr(), + RemoteAddr: conn.RemoteAddr(), + Conversation: conv, + }, conn, conn, kcpSettings) + + go fetchInput(ctx, conn, reader, session) + + var iConn stat.Connection = session + + if config := tls.ConfigFromStreamSettings(streamSettings); config != nil { + iConn = tls.Client(iConn, config.GetTLSConfig(tls.WithDestination(dest))) + } + + return iConn, nil +} + +func init() { + common.Must(internet.RegisterTransportDialer(protocolName, DialKCP)) +} diff --git a/subproject/Xray-core-main/transport/internet/kcp/io.go b/subproject/Xray-core-main/transport/internet/kcp/io.go new file mode 100644 index 00000000..fac9945f --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/kcp/io.go @@ -0,0 +1,20 @@ +package kcp + +type PacketReader interface { + Read([]byte) []Segment +} + +type KCPPacketReader struct{} + +func (r *KCPPacketReader) Read(b []byte) []Segment { + var result []Segment + for len(b) > 0 { + seg, x := ReadSegment(b) + if seg == nil { + break + } + result = append(result, seg) + b = x + } + return result +} diff --git a/subproject/Xray-core-main/transport/internet/kcp/io_test.go b/subproject/Xray-core-main/transport/internet/kcp/io_test.go new file mode 100644 index 00000000..d63efa66 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/kcp/io_test.go @@ -0,0 +1,34 @@ +package kcp_test + +import ( + "testing" + + . "github.com/xtls/xray-core/transport/internet/kcp" +) + +func TestKCPPacketReader(t *testing.T) { + reader := KCPPacketReader{} + + testCases := []struct { + Input []byte + Output []Segment + }{ + { + Input: []byte{}, + Output: nil, + }, + { + Input: []byte{1}, + Output: nil, + }, + } + + for _, testCase := range testCases { + seg := reader.Read(testCase.Input) + if testCase.Output == nil && seg != nil { + t.Errorf("Expect nothing returned, but actually %v", seg) + } else if testCase.Output != nil && seg == nil { + t.Errorf("Expect some output, but got nil") + } + } +} diff --git a/subproject/Xray-core-main/transport/internet/kcp/kcp.go b/subproject/Xray-core-main/transport/internet/kcp/kcp.go new file mode 100644 index 00000000..31c0633c --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/kcp/kcp.go @@ -0,0 +1,9 @@ +// Package kcp - A Fast and Reliable ARQ Protocol +// +// Acknowledgement: +// +// skywind3000@github for inventing the KCP protocol +// xtaci@github for translating to Golang +package kcp + +const protocolName = "mkcp" diff --git a/subproject/Xray-core-main/transport/internet/kcp/kcp_test.go b/subproject/Xray-core-main/transport/internet/kcp/kcp_test.go new file mode 100644 index 00000000..985d33d2 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/kcp/kcp_test.go @@ -0,0 +1,85 @@ +package kcp_test + +import ( + "context" + "crypto/rand" + "io" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "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/transport/internet" + . "github.com/xtls/xray-core/transport/internet/kcp" + "github.com/xtls/xray-core/transport/internet/stat" + "golang.org/x/sync/errgroup" +) + +func TestDialAndListen(t *testing.T) { + listerner, err := NewListener(context.Background(), net.LocalHostIP, net.Port(0), &internet.MemoryStreamConfig{ + ProtocolName: "mkcp", + ProtocolSettings: &Config{}, + }, func(conn stat.Connection) { + go func(c stat.Connection) { + payload := make([]byte, 4096) + for { + nBytes, err := c.Read(payload) + if err != nil { + break + } + for idx, b := range payload[:nBytes] { + payload[idx] = b ^ 'c' + } + c.Write(payload[:nBytes]) + } + c.Close() + }(conn) + }) + common.Must(err) + defer listerner.Close() + + port := net.Port(listerner.Addr().(*net.UDPAddr).Port) + + var errg errgroup.Group + for i := 0; i < 10; i++ { + errg.Go(func() error { + clientConn, err := DialKCP(context.Background(), net.UDPDestination(net.LocalHostIP, port), &internet.MemoryStreamConfig{ + ProtocolName: "mkcp", + ProtocolSettings: &Config{}, + }) + if err != nil { + return err + } + defer clientConn.Close() + + clientSend := make([]byte, 1024*1024) + rand.Read(clientSend) + go clientConn.Write(clientSend) + + clientReceived := make([]byte, 1024*1024) + common.Must2(io.ReadFull(clientConn, clientReceived)) + + clientExpected := make([]byte, 1024*1024) + for idx, b := range clientSend { + clientExpected[idx] = b ^ 'c' + } + if r := cmp.Diff(clientReceived, clientExpected); r != "" { + return errors.New(r) + } + return nil + }) + } + + if err := errg.Wait(); err != nil { + t.Fatal(err) + } + + for i := 0; i < 60 && listerner.ActiveConnections() > 0; i++ { + time.Sleep(500 * time.Millisecond) + } + if v := listerner.ActiveConnections(); v != 0 { + t.Error("active connections: ", v) + } +} diff --git a/subproject/Xray-core-main/transport/internet/kcp/listener.go b/subproject/Xray-core-main/transport/internet/kcp/listener.go new file mode 100644 index 00000000..aabec65f --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/kcp/listener.go @@ -0,0 +1,178 @@ +package kcp + +import ( + "context" + gotls "crypto/tls" + "sync" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/transport/internet" + "github.com/xtls/xray-core/transport/internet/stat" + "github.com/xtls/xray-core/transport/internet/tls" + "github.com/xtls/xray-core/transport/internet/udp" +) + +type ConnectionID struct { + Remote net.Address + Port net.Port + Conv uint16 +} + +// Listener defines a server listening for connections +type Listener struct { + sync.Mutex + sessions map[ConnectionID]*Connection + hub *udp.Hub + tlsConfig *gotls.Config + config *Config + reader PacketReader + addConn internet.ConnHandler +} + +func NewListener(ctx context.Context, address net.Address, port net.Port, streamSettings *internet.MemoryStreamConfig, addConn internet.ConnHandler) (*Listener, error) { + kcpSettings := streamSettings.ProtocolSettings.(*Config) + + l := &Listener{ + reader: &KCPPacketReader{}, + sessions: make(map[ConnectionID]*Connection), + config: kcpSettings, + addConn: addConn, + } + + if config := tls.ConfigFromStreamSettings(streamSettings); config != nil { + l.tlsConfig = config.GetTLSConfig() + } + + hub, err := udp.ListenUDP(ctx, address, port, streamSettings, udp.HubCapacity(1024)) + if err != nil { + return nil, err + } + l.Lock() + l.hub = hub + l.Unlock() + errors.LogInfo(ctx, "listening on ", address, ":", port) + + go l.handlePackets() + + return l, nil +} + +func (l *Listener) handlePackets() { + receive := l.hub.Receive() + for payload := range receive { + l.OnReceive(payload.Payload, payload.Source) + } +} + +func (l *Listener) OnReceive(payload *buf.Buffer, src net.Destination) { + segments := l.reader.Read(payload.Bytes()) + payload.Release() + + if len(segments) == 0 { + errors.LogInfo(context.Background(), "discarding invalid payload from ", src) + return + } + + conv := segments[0].Conversation() + cmd := segments[0].Command() + + id := ConnectionID{ + Remote: src.Address, + Port: src.Port, + Conv: conv, + } + + l.Lock() + defer l.Unlock() + + conn, found := l.sessions[id] + + if !found { + if cmd == CommandTerminate { + return + } + writer := &Writer{ + id: id, + hub: l.hub, + dest: src, + listener: l, + } + remoteAddr := &net.UDPAddr{ + IP: src.Address.IP(), + Port: int(src.Port), + } + localAddr := l.hub.Addr() + conn = NewConnection(ConnMetadata{ + LocalAddr: localAddr, + RemoteAddr: remoteAddr, + Conversation: conv, + }, writer, writer, l.config) + var netConn stat.Connection = conn + if l.tlsConfig != nil { + netConn = tls.Server(conn, l.tlsConfig) + } + + l.addConn(netConn) + l.sessions[id] = conn + } + conn.Input(segments) +} + +func (l *Listener) Remove(id ConnectionID) { + l.Lock() + delete(l.sessions, id) + l.Unlock() +} + +// Close stops listening on the UDP address. Already Accepted connections are not closed. +func (l *Listener) Close() error { + l.hub.Close() + + l.Lock() + defer l.Unlock() + + for _, conn := range l.sessions { + go conn.Terminate() + } + + return nil +} + +func (l *Listener) ActiveConnections() int { + l.Lock() + defer l.Unlock() + + return len(l.sessions) +} + +// Addr returns the listener's network address, The Addr returned is shared by all invocations of Addr, so do not modify it. +func (l *Listener) Addr() net.Addr { + return l.hub.Addr() +} + +type Writer struct { + id ConnectionID + dest net.Destination + hub *udp.Hub + listener *Listener +} + +func (w *Writer) Write(payload []byte) (int, error) { + return w.hub.WriteTo(payload, w.dest) +} + +func (w *Writer) Close() error { + w.listener.Remove(w.id) + return nil +} + +func ListenKCP(ctx context.Context, address net.Address, port net.Port, streamSettings *internet.MemoryStreamConfig, addConn internet.ConnHandler) (internet.Listener, error) { + return NewListener(ctx, address, port, streamSettings, addConn) +} + +func init() { + common.Must(internet.RegisterTransportListener(protocolName, ListenKCP)) +} diff --git a/subproject/Xray-core-main/transport/internet/kcp/output.go b/subproject/Xray-core-main/transport/internet/kcp/output.go new file mode 100644 index 00000000..3aed95f4 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/kcp/output.go @@ -0,0 +1,53 @@ +package kcp + +import ( + "io" + "sync" + + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/retry" +) + +type SegmentWriter interface { + Write(seg Segment) error +} + +type SimpleSegmentWriter struct { + sync.Mutex + buffer *buf.Buffer + writer io.Writer +} + +func NewSegmentWriter(writer io.Writer) SegmentWriter { + return &SimpleSegmentWriter{ + writer: writer, + buffer: buf.New(), + } +} + +func (w *SimpleSegmentWriter) Write(seg Segment) error { + w.Lock() + defer w.Unlock() + + w.buffer.Clear() + rawBytes := w.buffer.Extend(seg.ByteSize()) + seg.Serialize(rawBytes) + _, err := w.writer.Write(w.buffer.Bytes()) + return err +} + +type RetryableWriter struct { + writer SegmentWriter +} + +func NewRetryableWriter(writer SegmentWriter) SegmentWriter { + return &RetryableWriter{ + writer: writer, + } +} + +func (w *RetryableWriter) Write(seg Segment) error { + return retry.Timed(5, 100).On(func() error { + return w.writer.Write(seg) + }) +} diff --git a/subproject/Xray-core-main/transport/internet/kcp/receiving.go b/subproject/Xray-core-main/transport/internet/kcp/receiving.go new file mode 100644 index 00000000..3d1fc014 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/kcp/receiving.go @@ -0,0 +1,261 @@ +package kcp + +import ( + "sync" + + "github.com/xtls/xray-core/common/buf" +) + +type ReceivingWindow struct { + cache map[uint32]*DataSegment +} + +func NewReceivingWindow() *ReceivingWindow { + return &ReceivingWindow{ + cache: make(map[uint32]*DataSegment), + } +} + +func (w *ReceivingWindow) Set(id uint32, value *DataSegment) bool { + _, f := w.cache[id] + if f { + return false + } + w.cache[id] = value + return true +} + +func (w *ReceivingWindow) Has(id uint32) bool { + _, f := w.cache[id] + return f +} + +func (w *ReceivingWindow) Remove(id uint32) *DataSegment { + v, f := w.cache[id] + if !f { + return nil + } + delete(w.cache, id) + return v +} + +type AckList struct { + writer SegmentWriter + timestamps []uint32 + numbers []uint32 + nextFlush []uint32 + + flushCandidates []uint32 + dirty bool + + mss uint32 +} + +func NewAckList(writer SegmentWriter, mss uint32) *AckList { + return &AckList{ + writer: writer, + timestamps: make([]uint32, 0, 128), + numbers: make([]uint32, 0, 128), + nextFlush: make([]uint32, 0, 128), + flushCandidates: make([]uint32, 0, 128), + mss: mss, + } +} + +func (l *AckList) Add(number uint32, timestamp uint32) { + l.timestamps = append(l.timestamps, timestamp) + l.numbers = append(l.numbers, number) + l.nextFlush = append(l.nextFlush, 0) + l.dirty = true +} + +func (l *AckList) Clear(una uint32) { + count := 0 + for i := 0; i < len(l.numbers); i++ { + if l.numbers[i] < una { + continue + } + if i != count { + l.numbers[count] = l.numbers[i] + l.timestamps[count] = l.timestamps[i] + l.nextFlush[count] = l.nextFlush[i] + } + count++ + } + if count < len(l.numbers) { + l.numbers = l.numbers[:count] + l.timestamps = l.timestamps[:count] + l.nextFlush = l.nextFlush[:count] + l.dirty = true + } +} + +func (l *AckList) Flush(current uint32, rto uint32) { + l.flushCandidates = l.flushCandidates[:0] + + seg := NewAckSegment((int(l.mss) - 17) / 4) + for i := 0; i < len(l.numbers); i++ { + if l.nextFlush[i] > current { + if len(l.flushCandidates) < cap(l.flushCandidates) { + l.flushCandidates = append(l.flushCandidates, l.numbers[i]) + } + continue + } + seg.PutNumber(l.numbers[i]) + seg.PutTimestamp(l.timestamps[i]) + timeout := rto / 2 + if timeout < 20 { + timeout = 20 + } + l.nextFlush[i] = current + timeout + + if seg.IsFull() { + l.writer.Write(seg) + seg.Release() + seg = NewAckSegment((int(l.mss) - 17) / 4) + l.dirty = false + } + } + + if l.dirty || !seg.IsEmpty() { + for _, number := range l.flushCandidates { + if seg.IsFull() { + break + } + seg.PutNumber(number) + } + l.writer.Write(seg) + l.dirty = false + } + + seg.Release() +} + +type ReceivingWorker struct { + sync.RWMutex + conn *Connection + leftOver buf.MultiBuffer + window *ReceivingWindow + acklist *AckList + nextNumber uint32 + windowSize uint32 +} + +func NewReceivingWorker(kcp *Connection) *ReceivingWorker { + worker := &ReceivingWorker{ + conn: kcp, + window: NewReceivingWindow(), + windowSize: kcp.Config.GetReceivingInFlightSize(), + } + worker.acklist = NewAckList(worker, kcp.mss+DataSegmentOverhead) + return worker +} + +func (w *ReceivingWorker) Release() { + w.Lock() + buf.ReleaseMulti(w.leftOver) + w.leftOver = nil + w.Unlock() +} + +func (w *ReceivingWorker) ProcessSendingNext(number uint32) { + w.Lock() + defer w.Unlock() + + w.acklist.Clear(number) +} + +func (w *ReceivingWorker) ProcessSegment(seg *DataSegment) { + w.Lock() + defer w.Unlock() + + number := seg.Number + idx := number - w.nextNumber + if idx >= w.windowSize { + return + } + w.acklist.Clear(seg.SendingNext) + w.acklist.Add(number, seg.Timestamp) + + if !w.window.Set(seg.Number, seg) { + seg.Release() + } +} + +func (w *ReceivingWorker) ReadMultiBuffer() buf.MultiBuffer { + if w.leftOver != nil { + mb := w.leftOver + w.leftOver = nil + return mb + } + + mb := make(buf.MultiBuffer, 0, 32) + + w.Lock() + defer w.Unlock() + for { + seg := w.window.Remove(w.nextNumber) + if seg == nil { + break + } + w.nextNumber++ + mb = append(mb, seg.Detach()) + seg.Release() + } + + return mb +} + +func (w *ReceivingWorker) Read(b []byte) int { + mb := w.ReadMultiBuffer() + if mb.IsEmpty() { + return 0 + } + mb, nBytes := buf.SplitBytes(mb, b) + if !mb.IsEmpty() { + w.leftOver = mb + } + return nBytes +} + +func (w *ReceivingWorker) IsDataAvailable() bool { + w.RLock() + defer w.RUnlock() + return w.window.Has(w.nextNumber) +} + +func (w *ReceivingWorker) NextNumber() uint32 { + w.RLock() + defer w.RUnlock() + + return w.nextNumber +} + +func (w *ReceivingWorker) Flush(current uint32) { + w.Lock() + defer w.Unlock() + + w.acklist.Flush(current, w.conn.roundTrip.Timeout()) +} + +func (w *ReceivingWorker) Write(seg Segment) error { + ackSeg := seg.(*AckSegment) + ackSeg.Conv = w.conn.meta.Conversation + ackSeg.ReceivingNext = w.nextNumber + ackSeg.ReceivingWindow = w.nextNumber + w.windowSize + ackSeg.Option = 0 + if w.conn.State() == StateReadyToClose { + ackSeg.Option = SegmentOptionClose + } + return w.conn.output.Write(ackSeg) +} + +func (*ReceivingWorker) CloseRead() { +} + +func (w *ReceivingWorker) UpdateNecessary() bool { + w.RLock() + defer w.RUnlock() + + return len(w.acklist.numbers) > 0 +} diff --git a/subproject/Xray-core-main/transport/internet/kcp/segment.go b/subproject/Xray-core-main/transport/internet/kcp/segment.go new file mode 100644 index 00000000..2beaa0b6 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/kcp/segment.go @@ -0,0 +1,313 @@ +package kcp + +import ( + "encoding/binary" + + "github.com/xtls/xray-core/common/buf" +) + +// Command is a KCP command that indicate the purpose of a Segment. +type Command byte + +const ( + // CommandACK indicates an AckSegment. + CommandACK Command = 0 + // CommandData indicates a DataSegment. + CommandData Command = 1 + // CommandTerminate indicates that peer terminates the connection. + CommandTerminate Command = 2 + // CommandPing indicates a ping. + CommandPing Command = 3 +) + +type SegmentOption byte + +const ( + SegmentOptionClose SegmentOption = 1 +) + +type Segment interface { + Release() + Conversation() uint16 + Command() Command + ByteSize() int32 + Serialize([]byte) + parse(conv uint16, cmd Command, opt SegmentOption, buf []byte) (bool, []byte) +} + +const ( + DataSegmentOverhead = 18 +) + +type DataSegment struct { + Conv uint16 + Option SegmentOption + Timestamp uint32 + Number uint32 + SendingNext uint32 + + payload *buf.Buffer + timeout uint32 + transmit uint32 +} + +func NewDataSegment() *DataSegment { + return new(DataSegment) +} + +func (s *DataSegment) parse(conv uint16, cmd Command, opt SegmentOption, buf []byte) (bool, []byte) { + s.Conv = conv + s.Option = opt + if len(buf) < 15 { + return false, nil + } + s.Timestamp = binary.BigEndian.Uint32(buf) + buf = buf[4:] + + s.Number = binary.BigEndian.Uint32(buf) + buf = buf[4:] + + s.SendingNext = binary.BigEndian.Uint32(buf) + buf = buf[4:] + + dataLen := int(binary.BigEndian.Uint16(buf)) + buf = buf[2:] + + if len(buf) < dataLen { + return false, nil + } + s.Data().Clear() + s.Data().Write(buf[:dataLen]) + buf = buf[dataLen:] + + return true, buf +} + +func (s *DataSegment) Conversation() uint16 { + return s.Conv +} + +func (*DataSegment) Command() Command { + return CommandData +} + +func (s *DataSegment) Detach() *buf.Buffer { + r := s.payload + s.payload = nil + return r +} + +func (s *DataSegment) Data() *buf.Buffer { + if s.payload == nil { + s.payload = buf.New() + } + return s.payload +} + +func (s *DataSegment) Serialize(b []byte) { + binary.BigEndian.PutUint16(b, s.Conv) + b[2] = byte(CommandData) + b[3] = byte(s.Option) + binary.BigEndian.PutUint32(b[4:], s.Timestamp) + binary.BigEndian.PutUint32(b[8:], s.Number) + binary.BigEndian.PutUint32(b[12:], s.SendingNext) + binary.BigEndian.PutUint16(b[16:], uint16(s.payload.Len())) + copy(b[18:], s.payload.Bytes()) +} + +func (s *DataSegment) ByteSize() int32 { + return 2 + 1 + 1 + 4 + 4 + 4 + 2 + s.payload.Len() +} + +func (s *DataSegment) Release() { + s.payload.Release() + s.payload = nil +} + +type AckSegment struct { + Conv uint16 + Option SegmentOption + ReceivingWindow uint32 + ReceivingNext uint32 + Timestamp uint32 + NumberList []uint32 + + Limit int +} + +const ackNumberLimit = 128 + +func NewAckSegment(limit int) *AckSegment { + if limit <= 0 { + limit = 1 + } + if limit > ackNumberLimit { + limit = ackNumberLimit + } + return &AckSegment{ + Limit: limit, + } +} + +func (s *AckSegment) parse(conv uint16, cmd Command, opt SegmentOption, buf []byte) (bool, []byte) { + s.Conv = conv + s.Option = opt + if len(buf) < 13 { + return false, nil + } + + s.ReceivingWindow = binary.BigEndian.Uint32(buf) + buf = buf[4:] + + s.ReceivingNext = binary.BigEndian.Uint32(buf) + buf = buf[4:] + + s.Timestamp = binary.BigEndian.Uint32(buf) + buf = buf[4:] + + count := int(buf[0]) + buf = buf[1:] + + if len(buf) < count*4 { + return false, nil + } + for i := 0; i < count; i++ { + s.PutNumber(binary.BigEndian.Uint32(buf)) + buf = buf[4:] + } + + return true, buf +} + +func (s *AckSegment) Conversation() uint16 { + return s.Conv +} + +func (*AckSegment) Command() Command { + return CommandACK +} + +func (s *AckSegment) PutTimestamp(timestamp uint32) { + if timestamp-s.Timestamp < 0x7FFFFFFF { + s.Timestamp = timestamp + } +} + +func (s *AckSegment) PutNumber(number uint32) { + s.NumberList = append(s.NumberList, number) +} + +func (s *AckSegment) IsFull() bool { + return len(s.NumberList) == s.Limit +} + +func (s *AckSegment) IsEmpty() bool { + return len(s.NumberList) == 0 +} + +func (s *AckSegment) ByteSize() int32 { + return 2 + 1 + 1 + 4 + 4 + 4 + 1 + int32(len(s.NumberList)*4) +} + +func (s *AckSegment) Serialize(b []byte) { + binary.BigEndian.PutUint16(b, s.Conv) + b[2] = byte(CommandACK) + b[3] = byte(s.Option) + binary.BigEndian.PutUint32(b[4:], s.ReceivingWindow) + binary.BigEndian.PutUint32(b[8:], s.ReceivingNext) + binary.BigEndian.PutUint32(b[12:], s.Timestamp) + b[16] = byte(len(s.NumberList)) + n := 17 + for _, number := range s.NumberList { + binary.BigEndian.PutUint32(b[n:], number) + n += 4 + } +} + +func (s *AckSegment) Release() {} + +type CmdOnlySegment struct { + Conv uint16 + Cmd Command + Option SegmentOption + SendingNext uint32 + ReceivingNext uint32 + PeerRTO uint32 +} + +func NewCmdOnlySegment() *CmdOnlySegment { + return new(CmdOnlySegment) +} + +func (s *CmdOnlySegment) parse(conv uint16, cmd Command, opt SegmentOption, buf []byte) (bool, []byte) { + s.Conv = conv + s.Cmd = cmd + s.Option = opt + + if len(buf) < 12 { + return false, nil + } + + s.SendingNext = binary.BigEndian.Uint32(buf) + buf = buf[4:] + + s.ReceivingNext = binary.BigEndian.Uint32(buf) + buf = buf[4:] + + s.PeerRTO = binary.BigEndian.Uint32(buf) + buf = buf[4:] + + return true, buf +} + +func (s *CmdOnlySegment) Conversation() uint16 { + return s.Conv +} + +func (s *CmdOnlySegment) Command() Command { + return s.Cmd +} + +func (*CmdOnlySegment) ByteSize() int32 { + return 2 + 1 + 1 + 4 + 4 + 4 +} + +func (s *CmdOnlySegment) Serialize(b []byte) { + binary.BigEndian.PutUint16(b, s.Conv) + b[2] = byte(s.Cmd) + b[3] = byte(s.Option) + binary.BigEndian.PutUint32(b[4:], s.SendingNext) + binary.BigEndian.PutUint32(b[8:], s.ReceivingNext) + binary.BigEndian.PutUint32(b[12:], s.PeerRTO) +} + +func (*CmdOnlySegment) Release() {} + +func ReadSegment(buf []byte) (Segment, []byte) { + if len(buf) < 4 { + return nil, nil + } + + conv := binary.BigEndian.Uint16(buf) + buf = buf[2:] + + cmd := Command(buf[0]) + opt := SegmentOption(buf[1]) + buf = buf[2:] + + var seg Segment + switch cmd { + case CommandData: + seg = NewDataSegment() + case CommandACK: + seg = NewAckSegment(128) + default: + seg = NewCmdOnlySegment() + } + + valid, extra := seg.parse(conv, cmd, opt, buf) + if !valid { + return nil, nil + } + return seg, extra +} diff --git a/subproject/Xray-core-main/transport/internet/kcp/segment_test.go b/subproject/Xray-core-main/transport/internet/kcp/segment_test.go new file mode 100644 index 00000000..cc12ea9b --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/kcp/segment_test.go @@ -0,0 +1,107 @@ +package kcp_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + . "github.com/xtls/xray-core/transport/internet/kcp" +) + +func TestBadSegment(t *testing.T) { + seg, buf := ReadSegment(nil) + if seg != nil { + t.Error("non-nil seg") + } + if len(buf) != 0 { + t.Error("buf len: ", len(buf)) + } +} + +func TestDataSegment(t *testing.T) { + seg := &DataSegment{ + Conv: 1, + Timestamp: 3, + Number: 4, + SendingNext: 5, + } + seg.Data().Write([]byte{'a', 'b', 'c', 'd'}) + + nBytes := seg.ByteSize() + bytes := make([]byte, nBytes) + seg.Serialize(bytes) + + iseg, _ := ReadSegment(bytes) + seg2 := iseg.(*DataSegment) + if r := cmp.Diff(seg2, seg, cmpopts.IgnoreUnexported(DataSegment{})); r != "" { + t.Error(r) + } + if r := cmp.Diff(seg2.Data().Bytes(), seg.Data().Bytes()); r != "" { + t.Error(r) + } +} + +func Test1ByteDataSegment(t *testing.T) { + seg := &DataSegment{ + Conv: 1, + Timestamp: 3, + Number: 4, + SendingNext: 5, + } + seg.Data().WriteByte('a') + + nBytes := seg.ByteSize() + bytes := make([]byte, nBytes) + seg.Serialize(bytes) + + iseg, _ := ReadSegment(bytes) + seg2 := iseg.(*DataSegment) + if r := cmp.Diff(seg2, seg, cmpopts.IgnoreUnexported(DataSegment{})); r != "" { + t.Error(r) + } + if r := cmp.Diff(seg2.Data().Bytes(), seg.Data().Bytes()); r != "" { + t.Error(r) + } +} + +func TestACKSegment(t *testing.T) { + seg := &AckSegment{ + Conv: 1, + ReceivingWindow: 2, + ReceivingNext: 3, + Timestamp: 10, + NumberList: []uint32{1, 3, 5, 7, 9}, + Limit: 128, + } + + nBytes := seg.ByteSize() + bytes := make([]byte, nBytes) + seg.Serialize(bytes) + + iseg, _ := ReadSegment(bytes) + seg2 := iseg.(*AckSegment) + if r := cmp.Diff(seg2, seg); r != "" { + t.Error(r) + } +} + +func TestCmdSegment(t *testing.T) { + seg := &CmdOnlySegment{ + Conv: 1, + Cmd: CommandPing, + Option: SegmentOptionClose, + SendingNext: 11, + ReceivingNext: 13, + PeerRTO: 15, + } + + nBytes := seg.ByteSize() + bytes := make([]byte, nBytes) + seg.Serialize(bytes) + + iseg, _ := ReadSegment(bytes) + seg2 := iseg.(*CmdOnlySegment) + if r := cmp.Diff(seg2, seg); r != "" { + t.Error(r) + } +} diff --git a/subproject/Xray-core-main/transport/internet/kcp/sending.go b/subproject/Xray-core-main/transport/internet/kcp/sending.go new file mode 100644 index 00000000..ac8e98c1 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/kcp/sending.go @@ -0,0 +1,364 @@ +package kcp + +import ( + "container/list" + "sync" + + "github.com/xtls/xray-core/common/buf" +) + +type SendingWindow struct { + cache *list.List + totalInFlightSize uint32 + writer SegmentWriter + onPacketLoss func(uint32) +} + +func NewSendingWindow(writer SegmentWriter, onPacketLoss func(uint32)) *SendingWindow { + window := &SendingWindow{ + cache: list.New(), + writer: writer, + onPacketLoss: onPacketLoss, + } + return window +} + +func (sw *SendingWindow) Release() { + if sw == nil { + return + } + for sw.cache.Len() > 0 { + seg := sw.cache.Front().Value.(*DataSegment) + seg.Release() + sw.cache.Remove(sw.cache.Front()) + } +} + +func (sw *SendingWindow) Len() uint32 { + return uint32(sw.cache.Len()) +} + +func (sw *SendingWindow) IsEmpty() bool { + return sw.cache.Len() == 0 +} + +func (sw *SendingWindow) Push(number uint32, b *buf.Buffer) { + seg := NewDataSegment() + seg.Number = number + seg.payload = b + + sw.cache.PushBack(seg) +} + +func (sw *SendingWindow) FirstNumber() uint32 { + return sw.cache.Front().Value.(*DataSegment).Number +} + +func (sw *SendingWindow) Clear(una uint32) { + for !sw.IsEmpty() { + seg := sw.cache.Front().Value.(*DataSegment) + if seg.Number >= una { + break + } + seg.Release() + sw.cache.Remove(sw.cache.Front()) + } +} + +func (sw *SendingWindow) HandleFastAck(number uint32, rto uint32) { + if sw.IsEmpty() { + return + } + + sw.Visit(func(seg *DataSegment) bool { + if number == seg.Number || number-seg.Number > 0x7FFFFFFF { + return false + } + + if seg.transmit > 0 && seg.timeout > rto/3 { + seg.timeout -= rto / 3 + } + return true + }) +} + +func (sw *SendingWindow) Visit(visitor func(seg *DataSegment) bool) { + if sw.IsEmpty() { + return + } + + for e := sw.cache.Front(); e != nil; e = e.Next() { + seg := e.Value.(*DataSegment) + if !visitor(seg) { + break + } + } +} + +func (sw *SendingWindow) Flush(current uint32, rto uint32, maxInFlightSize uint32) { + if sw.IsEmpty() { + return + } + + var lost uint32 + var inFlightSize uint32 + + sw.Visit(func(segment *DataSegment) bool { + if current-segment.timeout >= 0x7FFFFFFF { + return true + } + if segment.transmit == 0 { + // First time + sw.totalInFlightSize++ + } else { + lost++ + } + segment.timeout = current + rto + + segment.Timestamp = current + segment.transmit++ + sw.writer.Write(segment) + inFlightSize++ + return inFlightSize < maxInFlightSize + }) + + if sw.onPacketLoss != nil && inFlightSize > 0 && sw.totalInFlightSize != 0 { + rate := lost * 100 / sw.totalInFlightSize + sw.onPacketLoss(rate) + } +} + +func (sw *SendingWindow) Remove(number uint32) bool { + if sw.IsEmpty() { + return false + } + + for e := sw.cache.Front(); e != nil; e = e.Next() { + seg := e.Value.(*DataSegment) + if seg.Number > number { + return false + } else if seg.Number == number { + if sw.totalInFlightSize > 0 { + sw.totalInFlightSize-- + } + seg.Release() + sw.cache.Remove(e) + return true + } + } + + return false +} + +type SendingWorker struct { + sync.RWMutex + conn *Connection + window *SendingWindow + firstUnacknowledged uint32 + nextNumber uint32 + remoteNextNumber uint32 + controlWindow uint32 + fastResend uint32 + windowSize uint32 + firstUnacknowledgedUpdated bool + closed bool +} + +func NewSendingWorker(kcp *Connection) *SendingWorker { + worker := &SendingWorker{ + conn: kcp, + fastResend: 2, + remoteNextNumber: 32, + controlWindow: kcp.Config.GetSendingInFlightSize(), + windowSize: kcp.Config.GetSendingBufferSize(), + } + worker.window = NewSendingWindow(worker, worker.OnPacketLoss) + return worker +} + +func (w *SendingWorker) Release() { + w.Lock() + w.window.Release() + w.closed = true + w.Unlock() +} + +func (w *SendingWorker) ProcessReceivingNext(nextNumber uint32) { + w.Lock() + defer w.Unlock() + + w.ProcessReceivingNextWithoutLock(nextNumber) +} + +func (w *SendingWorker) ProcessReceivingNextWithoutLock(nextNumber uint32) { + w.window.Clear(nextNumber) + w.FindFirstUnacknowledged() +} + +func (w *SendingWorker) FindFirstUnacknowledged() { + first := w.firstUnacknowledged + if !w.window.IsEmpty() { + w.firstUnacknowledged = w.window.FirstNumber() + } else { + w.firstUnacknowledged = w.nextNumber + } + if first != w.firstUnacknowledged { + w.firstUnacknowledgedUpdated = true + } +} + +func (w *SendingWorker) processAck(number uint32) bool { + // number < v.firstUnacknowledged || number >= v.nextNumber + if number-w.firstUnacknowledged > 0x7FFFFFFF || number-w.nextNumber < 0x7FFFFFFF { + return false + } + + removed := w.window.Remove(number) + if removed { + w.FindFirstUnacknowledged() + } + return removed +} + +func (w *SendingWorker) ProcessSegment(current uint32, seg *AckSegment, rto uint32) { + defer seg.Release() + + w.Lock() + defer w.Unlock() + + if w.closed { + return + } + + if w.remoteNextNumber < seg.ReceivingWindow { + w.remoteNextNumber = seg.ReceivingWindow + } + w.ProcessReceivingNextWithoutLock(seg.ReceivingNext) + + if seg.IsEmpty() { + return + } + + var maxack uint32 + var maxackRemoved bool + for _, number := range seg.NumberList { + removed := w.processAck(number) + if maxack < number { + maxack = number + maxackRemoved = removed + } + } + + if maxackRemoved { + w.window.HandleFastAck(maxack, rto) + if current-seg.Timestamp < 10000 { + w.conn.roundTrip.Update(current-seg.Timestamp, current) + } + } +} + +func (w *SendingWorker) Push(b *buf.Buffer) bool { + w.Lock() + defer w.Unlock() + + if w.closed { + return false + } + + if w.window.Len() > w.windowSize { + return false + } + + w.window.Push(w.nextNumber, b) + w.nextNumber++ + return true +} + +func (w *SendingWorker) Write(seg Segment) error { + dataSeg := seg.(*DataSegment) + + dataSeg.Conv = w.conn.meta.Conversation + dataSeg.SendingNext = w.firstUnacknowledged + dataSeg.Option = 0 + if w.conn.State() == StateReadyToClose { + dataSeg.Option = SegmentOptionClose + } + + return w.conn.output.Write(dataSeg) +} + +func (w *SendingWorker) OnPacketLoss(lossRate uint32) { + if !w.conn.Config.Congestion || w.conn.roundTrip.Timeout() == 0 { + return + } + + if lossRate >= 15 { + w.controlWindow = 3 * w.controlWindow / 4 + } else if lossRate <= 5 { + w.controlWindow += w.controlWindow / 4 + } + if w.controlWindow < 16 { + w.controlWindow = 16 + } + if w.controlWindow > 2*w.conn.Config.GetSendingInFlightSize() { + w.controlWindow = 2 * w.conn.Config.GetSendingInFlightSize() + } +} + +func (w *SendingWorker) Flush(current uint32) { + w.Lock() + + if w.closed { + w.Unlock() + return + } + + cwnd := w.conn.Config.GetSendingInFlightSize() + if cwnd > w.remoteNextNumber-w.firstUnacknowledged { + cwnd = w.remoteNextNumber - w.firstUnacknowledged + } + if w.conn.Config.Congestion && cwnd > w.controlWindow { + cwnd = w.controlWindow + } + + cwnd *= 20 // magic + + if !w.window.IsEmpty() { + w.window.Flush(current, w.conn.roundTrip.Timeout(), cwnd) + w.firstUnacknowledgedUpdated = false + } + + updated := w.firstUnacknowledgedUpdated + w.firstUnacknowledgedUpdated = false + + w.Unlock() + + if updated { + w.conn.Ping(current, CommandPing) + } +} + +func (w *SendingWorker) CloseWrite() { + w.Lock() + defer w.Unlock() + + w.window.Clear(0xFFFFFFFF) +} + +func (w *SendingWorker) IsEmpty() bool { + w.RLock() + defer w.RUnlock() + + return w.window.IsEmpty() +} + +func (w *SendingWorker) UpdateNecessary() bool { + return !w.IsEmpty() +} + +func (w *SendingWorker) FirstUnacknowledged() uint32 { + w.RLock() + defer w.RUnlock() + + return w.firstUnacknowledged +} diff --git a/subproject/Xray-core-main/transport/internet/memory_settings.go b/subproject/Xray-core-main/transport/internet/memory_settings.go new file mode 100644 index 00000000..02fb247f --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/memory_settings.go @@ -0,0 +1,83 @@ +package internet + +import ( + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/transport/internet/finalmask" +) + +// MemoryStreamConfig is a parsed form of StreamConfig. It is used to reduce the number of Protobuf parses. +type MemoryStreamConfig struct { + Destination *net.Destination + ProtocolName string + ProtocolSettings interface{} + SecurityType string + SecuritySettings interface{} + TcpmaskManager *finalmask.TcpmaskManager + UdpmaskManager *finalmask.UdpmaskManager + QuicParams *QuicParams + SocketSettings *SocketConfig + DownloadSettings *MemoryStreamConfig +} + +// ToMemoryStreamConfig converts a StreamConfig to MemoryStreamConfig. It returns a default non-nil MemoryStreamConfig for nil input. +func ToMemoryStreamConfig(s *StreamConfig) (*MemoryStreamConfig, error) { + ets, err := s.GetEffectiveTransportSettings() + if err != nil { + return nil, err + } + + mss := &MemoryStreamConfig{ + ProtocolName: s.GetEffectiveProtocol(), + ProtocolSettings: ets, + } + + if s != nil { + if s.Address != nil { + mss.Destination = &net.Destination{ + Address: s.Address.AsAddress(), + Port: net.Port(s.Port), + Network: net.Network_TCP, + } + } + mss.SocketSettings = s.SocketSettings + } + + if s != nil && s.HasSecuritySettings() { + ess, err := s.GetEffectiveSecuritySettings() + if err != nil { + return nil, err + } + mss.SecurityType = s.SecurityType + mss.SecuritySettings = ess + } + + if s != nil && len(s.Tcpmasks) > 0 { + var masks []finalmask.Tcpmask + for _, msg := range s.Tcpmasks { + instance, err := msg.GetInstance() + if err != nil { + return nil, err + } + masks = append(masks, instance.(finalmask.Tcpmask)) + } + mss.TcpmaskManager = finalmask.NewTcpmaskManager(masks) + } + + if s != nil && s.QuicParams != nil { + mss.QuicParams = s.QuicParams + } + + if s != nil && len(s.Udpmasks) > 0 { + var masks []finalmask.Udpmask + for _, msg := range s.Udpmasks { + instance, err := msg.GetInstance() + if err != nil { + return nil, err + } + masks = append(masks, instance.(finalmask.Udpmask)) + } + mss.UdpmaskManager = finalmask.NewUdpmaskManager(masks) + } + + return mss, nil +} diff --git a/subproject/Xray-core-main/transport/internet/reality/config.go b/subproject/Xray-core-main/transport/internet/reality/config.go new file mode 100644 index 00000000..9bc6ee15 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/reality/config.go @@ -0,0 +1,83 @@ +package reality + +import ( + "context" + "io" + "net" + "os" + "time" + + "github.com/cloudflare/circl/sign/mldsa/mldsa65" + "github.com/xtls/reality" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/transport/internet" +) + +func (c *Config) GetREALITYConfig() *reality.Config { + var dialer net.Dialer + config := &reality.Config{ + DialContext: dialer.DialContext, + + Show: c.Show, + Type: c.Type, + Dest: c.Dest, + Xver: byte(c.Xver), + + PrivateKey: c.PrivateKey, + MinClientVer: c.MinClientVer, + MaxClientVer: c.MaxClientVer, + MaxTimeDiff: time.Duration(c.MaxTimeDiff) * time.Millisecond, + + NextProtos: nil, // should be nil + SessionTicketsDisabled: true, + + KeyLogWriter: KeyLogWriterFromConfig(c), + } + if c.Mldsa65Seed != nil { + _, key := mldsa65.NewKeyFromSeed((*[32]byte)(c.Mldsa65Seed)) + config.Mldsa65Key = key.Bytes() + } + if c.LimitFallbackUpload != nil { + config.LimitFallbackUpload.AfterBytes = c.LimitFallbackUpload.AfterBytes + config.LimitFallbackUpload.BytesPerSec = c.LimitFallbackUpload.BytesPerSec + config.LimitFallbackUpload.BurstBytesPerSec = c.LimitFallbackUpload.BurstBytesPerSec + } + if c.LimitFallbackDownload != nil { + config.LimitFallbackDownload.AfterBytes = c.LimitFallbackDownload.AfterBytes + config.LimitFallbackDownload.BytesPerSec = c.LimitFallbackDownload.BytesPerSec + config.LimitFallbackDownload.BurstBytesPerSec = c.LimitFallbackDownload.BurstBytesPerSec + } + config.ServerNames = make(map[string]bool) + for _, serverName := range c.ServerNames { + config.ServerNames[serverName] = true + } + config.ShortIds = make(map[[8]byte]bool) + for _, shortId := range c.ShortIds { + config.ShortIds[*(*[8]byte)(shortId)] = true + } + return config +} + +func KeyLogWriterFromConfig(c *Config) io.Writer { + if len(c.MasterKeyLog) <= 0 || c.MasterKeyLog == "none" { + return nil + } + + writer, err := os.OpenFile(c.MasterKeyLog, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0644) + if err != nil { + errors.LogErrorInner(context.Background(), err, "failed to open ", c.MasterKeyLog, " as master key log") + } + + return writer +} + +func ConfigFromStreamSettings(settings *internet.MemoryStreamConfig) *Config { + if settings == nil { + return nil + } + config, ok := settings.SecuritySettings.(*Config) + if !ok { + return nil + } + return config +} diff --git a/subproject/Xray-core-main/transport/internet/reality/config.pb.go b/subproject/Xray-core-main/transport/internet/reality/config.pb.go new file mode 100644 index 00000000..ceb65204 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/reality/config.pb.go @@ -0,0 +1,375 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: transport/internet/reality/config.proto + +package reality + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + Show bool `protobuf:"varint,1,opt,name=show,proto3" json:"show,omitempty"` + Dest string `protobuf:"bytes,2,opt,name=dest,proto3" json:"dest,omitempty"` + Type string `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"` + Xver uint64 `protobuf:"varint,4,opt,name=xver,proto3" json:"xver,omitempty"` + ServerNames []string `protobuf:"bytes,5,rep,name=server_names,json=serverNames,proto3" json:"server_names,omitempty"` + PrivateKey []byte `protobuf:"bytes,6,opt,name=private_key,json=privateKey,proto3" json:"private_key,omitempty"` + MinClientVer []byte `protobuf:"bytes,7,opt,name=min_client_ver,json=minClientVer,proto3" json:"min_client_ver,omitempty"` + MaxClientVer []byte `protobuf:"bytes,8,opt,name=max_client_ver,json=maxClientVer,proto3" json:"max_client_ver,omitempty"` + MaxTimeDiff uint64 `protobuf:"varint,9,opt,name=max_time_diff,json=maxTimeDiff,proto3" json:"max_time_diff,omitempty"` + ShortIds [][]byte `protobuf:"bytes,10,rep,name=short_ids,json=shortIds,proto3" json:"short_ids,omitempty"` + Mldsa65Seed []byte `protobuf:"bytes,11,opt,name=mldsa65_seed,json=mldsa65Seed,proto3" json:"mldsa65_seed,omitempty"` + LimitFallbackUpload *LimitFallback `protobuf:"bytes,12,opt,name=limit_fallback_upload,json=limitFallbackUpload,proto3" json:"limit_fallback_upload,omitempty"` + LimitFallbackDownload *LimitFallback `protobuf:"bytes,13,opt,name=limit_fallback_download,json=limitFallbackDownload,proto3" json:"limit_fallback_download,omitempty"` + Fingerprint string `protobuf:"bytes,21,opt,name=Fingerprint,proto3" json:"Fingerprint,omitempty"` + ServerName string `protobuf:"bytes,22,opt,name=server_name,json=serverName,proto3" json:"server_name,omitempty"` + PublicKey []byte `protobuf:"bytes,23,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty"` + ShortId []byte `protobuf:"bytes,24,opt,name=short_id,json=shortId,proto3" json:"short_id,omitempty"` + Mldsa65Verify []byte `protobuf:"bytes,25,opt,name=mldsa65_verify,json=mldsa65Verify,proto3" json:"mldsa65_verify,omitempty"` + SpiderX string `protobuf:"bytes,26,opt,name=spider_x,json=spiderX,proto3" json:"spider_x,omitempty"` + SpiderY []int64 `protobuf:"varint,27,rep,packed,name=spider_y,json=spiderY,proto3" json:"spider_y,omitempty"` + MasterKeyLog string `protobuf:"bytes,31,opt,name=master_key_log,json=masterKeyLog,proto3" json:"master_key_log,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_transport_internet_reality_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_reality_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_transport_internet_reality_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Config) GetShow() bool { + if x != nil { + return x.Show + } + return false +} + +func (x *Config) GetDest() string { + if x != nil { + return x.Dest + } + return "" +} + +func (x *Config) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *Config) GetXver() uint64 { + if x != nil { + return x.Xver + } + return 0 +} + +func (x *Config) GetServerNames() []string { + if x != nil { + return x.ServerNames + } + return nil +} + +func (x *Config) GetPrivateKey() []byte { + if x != nil { + return x.PrivateKey + } + return nil +} + +func (x *Config) GetMinClientVer() []byte { + if x != nil { + return x.MinClientVer + } + return nil +} + +func (x *Config) GetMaxClientVer() []byte { + if x != nil { + return x.MaxClientVer + } + return nil +} + +func (x *Config) GetMaxTimeDiff() uint64 { + if x != nil { + return x.MaxTimeDiff + } + return 0 +} + +func (x *Config) GetShortIds() [][]byte { + if x != nil { + return x.ShortIds + } + return nil +} + +func (x *Config) GetMldsa65Seed() []byte { + if x != nil { + return x.Mldsa65Seed + } + return nil +} + +func (x *Config) GetLimitFallbackUpload() *LimitFallback { + if x != nil { + return x.LimitFallbackUpload + } + return nil +} + +func (x *Config) GetLimitFallbackDownload() *LimitFallback { + if x != nil { + return x.LimitFallbackDownload + } + return nil +} + +func (x *Config) GetFingerprint() string { + if x != nil { + return x.Fingerprint + } + return "" +} + +func (x *Config) GetServerName() string { + if x != nil { + return x.ServerName + } + return "" +} + +func (x *Config) GetPublicKey() []byte { + if x != nil { + return x.PublicKey + } + return nil +} + +func (x *Config) GetShortId() []byte { + if x != nil { + return x.ShortId + } + return nil +} + +func (x *Config) GetMldsa65Verify() []byte { + if x != nil { + return x.Mldsa65Verify + } + return nil +} + +func (x *Config) GetSpiderX() string { + if x != nil { + return x.SpiderX + } + return "" +} + +func (x *Config) GetSpiderY() []int64 { + if x != nil { + return x.SpiderY + } + return nil +} + +func (x *Config) GetMasterKeyLog() string { + if x != nil { + return x.MasterKeyLog + } + return "" +} + +type LimitFallback struct { + state protoimpl.MessageState `protogen:"open.v1"` + AfterBytes uint64 `protobuf:"varint,1,opt,name=after_bytes,json=afterBytes,proto3" json:"after_bytes,omitempty"` + BytesPerSec uint64 `protobuf:"varint,2,opt,name=bytes_per_sec,json=bytesPerSec,proto3" json:"bytes_per_sec,omitempty"` + BurstBytesPerSec uint64 `protobuf:"varint,3,opt,name=burst_bytes_per_sec,json=burstBytesPerSec,proto3" json:"burst_bytes_per_sec,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LimitFallback) Reset() { + *x = LimitFallback{} + mi := &file_transport_internet_reality_config_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LimitFallback) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LimitFallback) ProtoMessage() {} + +func (x *LimitFallback) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_reality_config_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LimitFallback.ProtoReflect.Descriptor instead. +func (*LimitFallback) Descriptor() ([]byte, []int) { + return file_transport_internet_reality_config_proto_rawDescGZIP(), []int{1} +} + +func (x *LimitFallback) GetAfterBytes() uint64 { + if x != nil { + return x.AfterBytes + } + return 0 +} + +func (x *LimitFallback) GetBytesPerSec() uint64 { + if x != nil { + return x.BytesPerSec + } + return 0 +} + +func (x *LimitFallback) GetBurstBytesPerSec() uint64 { + if x != nil { + return x.BurstBytesPerSec + } + return 0 +} + +var File_transport_internet_reality_config_proto protoreflect.FileDescriptor + +const file_transport_internet_reality_config_proto_rawDesc = "" + + "\n" + + "'transport/internet/reality/config.proto\x12\x1fxray.transport.internet.reality\"\x98\x06\n" + + "\x06Config\x12\x12\n" + + "\x04show\x18\x01 \x01(\bR\x04show\x12\x12\n" + + "\x04dest\x18\x02 \x01(\tR\x04dest\x12\x12\n" + + "\x04type\x18\x03 \x01(\tR\x04type\x12\x12\n" + + "\x04xver\x18\x04 \x01(\x04R\x04xver\x12!\n" + + "\fserver_names\x18\x05 \x03(\tR\vserverNames\x12\x1f\n" + + "\vprivate_key\x18\x06 \x01(\fR\n" + + "privateKey\x12$\n" + + "\x0emin_client_ver\x18\a \x01(\fR\fminClientVer\x12$\n" + + "\x0emax_client_ver\x18\b \x01(\fR\fmaxClientVer\x12\"\n" + + "\rmax_time_diff\x18\t \x01(\x04R\vmaxTimeDiff\x12\x1b\n" + + "\tshort_ids\x18\n" + + " \x03(\fR\bshortIds\x12!\n" + + "\fmldsa65_seed\x18\v \x01(\fR\vmldsa65Seed\x12b\n" + + "\x15limit_fallback_upload\x18\f \x01(\v2..xray.transport.internet.reality.LimitFallbackR\x13limitFallbackUpload\x12f\n" + + "\x17limit_fallback_download\x18\r \x01(\v2..xray.transport.internet.reality.LimitFallbackR\x15limitFallbackDownload\x12 \n" + + "\vFingerprint\x18\x15 \x01(\tR\vFingerprint\x12\x1f\n" + + "\vserver_name\x18\x16 \x01(\tR\n" + + "serverName\x12\x1d\n" + + "\n" + + "public_key\x18\x17 \x01(\fR\tpublicKey\x12\x19\n" + + "\bshort_id\x18\x18 \x01(\fR\ashortId\x12%\n" + + "\x0emldsa65_verify\x18\x19 \x01(\fR\rmldsa65Verify\x12\x19\n" + + "\bspider_x\x18\x1a \x01(\tR\aspiderX\x12\x19\n" + + "\bspider_y\x18\x1b \x03(\x03R\aspiderY\x12$\n" + + "\x0emaster_key_log\x18\x1f \x01(\tR\fmasterKeyLog\"\x83\x01\n" + + "\rLimitFallback\x12\x1f\n" + + "\vafter_bytes\x18\x01 \x01(\x04R\n" + + "afterBytes\x12\"\n" + + "\rbytes_per_sec\x18\x02 \x01(\x04R\vbytesPerSec\x12-\n" + + "\x13burst_bytes_per_sec\x18\x03 \x01(\x04R\x10burstBytesPerSecB\x7f\n" + + "#com.xray.transport.internet.realityP\x01Z4github.com/xtls/xray-core/transport/internet/reality\xaa\x02\x1fXray.Transport.Internet.Realityb\x06proto3" + +var ( + file_transport_internet_reality_config_proto_rawDescOnce sync.Once + file_transport_internet_reality_config_proto_rawDescData []byte +) + +func file_transport_internet_reality_config_proto_rawDescGZIP() []byte { + file_transport_internet_reality_config_proto_rawDescOnce.Do(func() { + file_transport_internet_reality_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_transport_internet_reality_config_proto_rawDesc), len(file_transport_internet_reality_config_proto_rawDesc))) + }) + return file_transport_internet_reality_config_proto_rawDescData +} + +var file_transport_internet_reality_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_transport_internet_reality_config_proto_goTypes = []any{ + (*Config)(nil), // 0: xray.transport.internet.reality.Config + (*LimitFallback)(nil), // 1: xray.transport.internet.reality.LimitFallback +} +var file_transport_internet_reality_config_proto_depIdxs = []int32{ + 1, // 0: xray.transport.internet.reality.Config.limit_fallback_upload:type_name -> xray.transport.internet.reality.LimitFallback + 1, // 1: xray.transport.internet.reality.Config.limit_fallback_download:type_name -> xray.transport.internet.reality.LimitFallback + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_transport_internet_reality_config_proto_init() } +func file_transport_internet_reality_config_proto_init() { + if File_transport_internet_reality_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_reality_config_proto_rawDesc), len(file_transport_internet_reality_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_transport_internet_reality_config_proto_goTypes, + DependencyIndexes: file_transport_internet_reality_config_proto_depIdxs, + MessageInfos: file_transport_internet_reality_config_proto_msgTypes, + }.Build() + File_transport_internet_reality_config_proto = out.File + file_transport_internet_reality_config_proto_goTypes = nil + file_transport_internet_reality_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/transport/internet/reality/config.proto b/subproject/Xray-core-main/transport/internet/reality/config.proto new file mode 100644 index 00000000..7da43629 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/reality/config.proto @@ -0,0 +1,40 @@ +syntax = "proto3"; + +package xray.transport.internet.reality; +option csharp_namespace = "Xray.Transport.Internet.Reality"; +option go_package = "github.com/xtls/xray-core/transport/internet/reality"; +option java_package = "com.xray.transport.internet.reality"; +option java_multiple_files = true; + +message Config { + bool show = 1; + string dest = 2; + string type = 3; + uint64 xver = 4; + repeated string server_names = 5; + bytes private_key = 6; + bytes min_client_ver = 7; + bytes max_client_ver = 8; + uint64 max_time_diff = 9; + repeated bytes short_ids = 10; + + bytes mldsa65_seed = 11; + LimitFallback limit_fallback_upload = 12; + LimitFallback limit_fallback_download = 13; + + string Fingerprint = 21; + string server_name = 22; + bytes public_key = 23; + bytes short_id = 24; + bytes mldsa65_verify = 25; + string spider_x = 26; + repeated int64 spider_y = 27; + + string master_key_log = 31; +} + +message LimitFallback { + uint64 after_bytes = 1; + uint64 bytes_per_sec = 2; + uint64 burst_bytes_per_sec = 3; +} diff --git a/subproject/Xray-core-main/transport/internet/reality/reality.go b/subproject/Xray-core-main/transport/internet/reality/reality.go new file mode 100644 index 00000000..50b2e02f --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/reality/reality.go @@ -0,0 +1,299 @@ +package reality + +import ( + "bytes" + "context" + "crypto/ecdh" + "crypto/ed25519" + "crypto/hmac" + "crypto/sha256" + "crypto/sha512" + gotls "crypto/tls" + "crypto/x509" + "encoding/binary" + "fmt" + "io" + "net/http" + "reflect" + "regexp" + "strings" + "sync" + "time" + "unsafe" + + "github.com/cloudflare/circl/sign/mldsa/mldsa65" + utls "github.com/refraction-networking/utls" + "github.com/xtls/reality" + "github.com/xtls/xray-core/common/crypto" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/utils" + "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/transport/internet/tls" + "golang.org/x/crypto/hkdf" + "golang.org/x/net/http2" +) + +type Conn struct { + *reality.Conn +} + +func (c *Conn) HandshakeAddress() net.Address { + if err := c.Handshake(); err != nil { + return nil + } + state := c.ConnectionState() + if state.ServerName == "" { + return nil + } + return net.ParseAddress(state.ServerName) +} + +func Server(c net.Conn, config *reality.Config) (net.Conn, error) { + realityConn, err := reality.Server(context.Background(), c, config) + return &Conn{Conn: realityConn}, err +} + +type UConn struct { + *utls.UConn + Config *Config + ServerName string + AuthKey []byte + Verified bool +} + +func (c *UConn) HandshakeAddress() net.Address { + if err := c.Handshake(); err != nil { + return nil + } + state := c.ConnectionState() + if state.ServerName == "" { + return nil + } + return net.ParseAddress(state.ServerName) +} + +func (c *UConn) VerifyPeerCertificate(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { + if c.Config.Show { + localAddr := c.LocalAddr().String() + fmt.Printf("REALITY localAddr: %v\tis using X25519MLKEM768 for TLS' communication: %v\n", localAddr, c.HandshakeState.ServerHello.ServerShare.Group == utls.X25519MLKEM768) + fmt.Printf("REALITY localAddr: %v\tis using ML-DSA-65 for cert's extra verification: %v\n", localAddr, len(c.Config.Mldsa65Verify) > 0) + } + p, _ := reflect.TypeOf(c.Conn).Elem().FieldByName("peerCertificates") + certs := *(*([]*x509.Certificate))(unsafe.Pointer(uintptr(unsafe.Pointer(c.Conn)) + p.Offset)) + if pub, ok := certs[0].PublicKey.(ed25519.PublicKey); ok { + h := hmac.New(sha512.New, c.AuthKey) + h.Write(pub) + if bytes.Equal(h.Sum(nil), certs[0].Signature) { + if len(c.Config.Mldsa65Verify) > 0 { + if len(certs[0].Extensions) > 0 { + h.Write(c.HandshakeState.Hello.Raw) + h.Write(c.HandshakeState.ServerHello.Raw) + verify, _ := mldsa65.Scheme().UnmarshalBinaryPublicKey(c.Config.Mldsa65Verify) + if mldsa65.Verify(verify.(*mldsa65.PublicKey), h.Sum(nil), nil, certs[0].Extensions[0].Value) { + c.Verified = true + return nil + } + } + } else { + c.Verified = true + return nil + } + } + } + opts := x509.VerifyOptions{ + DNSName: c.ServerName, + Intermediates: x509.NewCertPool(), + } + for _, cert := range certs[1:] { + opts.Intermediates.AddCert(cert) + } + if _, err := certs[0].Verify(opts); err != nil { + return err + } + return nil +} + +func UClient(c net.Conn, config *Config, ctx context.Context, dest net.Destination) (net.Conn, error) { + localAddr := c.LocalAddr().String() + uConn := &UConn{ + Config: config, + } + utlsConfig := &utls.Config{ + VerifyPeerCertificate: uConn.VerifyPeerCertificate, + ServerName: config.ServerName, + InsecureSkipVerify: true, + SessionTicketsDisabled: true, + KeyLogWriter: KeyLogWriterFromConfig(config), + } + if utlsConfig.ServerName == "" { + utlsConfig.ServerName = dest.Address.String() + } + uConn.ServerName = utlsConfig.ServerName + fingerprint := tls.GetFingerprint(config.Fingerprint) + if fingerprint == nil { + return nil, errors.New("REALITY: failed to get fingerprint").AtError() + } + uConn.UConn = utls.UClient(c, utlsConfig, *fingerprint) + { + uConn.BuildHandshakeState() + hello := uConn.HandshakeState.Hello + hello.SessionId = make([]byte, 32) + copy(hello.Raw[39:], hello.SessionId) // the fixed location of `Session ID` + hello.SessionId[0] = core.Version_x + hello.SessionId[1] = core.Version_y + hello.SessionId[2] = core.Version_z + hello.SessionId[3] = 0 // reserved + binary.BigEndian.PutUint32(hello.SessionId[4:], uint32(time.Now().Unix())) + copy(hello.SessionId[8:], config.ShortId) + if config.Show { + fmt.Printf("REALITY localAddr: %v\thello.SessionId[:16]: %v\n", localAddr, hello.SessionId[:16]) + } + publicKey, err := ecdh.X25519().NewPublicKey(config.PublicKey) + if err != nil { + return nil, errors.New("REALITY: publicKey == nil") + } + ecdhe := uConn.HandshakeState.State13.KeyShareKeys.Ecdhe + if ecdhe == nil { + ecdhe = uConn.HandshakeState.State13.KeyShareKeys.MlkemEcdhe + } + if ecdhe == nil { + return nil, errors.New("Current fingerprint ", uConn.ClientHelloID.Client, uConn.ClientHelloID.Version, " does not support TLS 1.3, REALITY handshake cannot establish.") + } + uConn.AuthKey, _ = ecdhe.ECDH(publicKey) + if uConn.AuthKey == nil { + return nil, errors.New("REALITY: SharedKey == nil") + } + if _, err := hkdf.New(sha256.New, uConn.AuthKey, hello.Random[:20], []byte("REALITY")).Read(uConn.AuthKey); err != nil { + return nil, err + } + aead := crypto.NewAesGcm(uConn.AuthKey) + if config.Show { + fmt.Printf("REALITY localAddr: %v\tuConn.AuthKey[:16]: %v\tAEAD: %T\n", localAddr, uConn.AuthKey[:16], aead) + } + aead.Seal(hello.SessionId[:0], hello.Random[20:], hello.SessionId[:16], hello.Raw) + copy(hello.Raw[39:], hello.SessionId) + } + if err := uConn.HandshakeContext(ctx); err != nil { + return nil, err + } + if config.Show { + fmt.Printf("REALITY localAddr: %v\tuConn.Verified: %v\n", localAddr, uConn.Verified) + } + if !uConn.Verified { + errors.LogError(ctx, "REALITY: received real certificate (potential MITM or redirection)") + go func() { + client := &http.Client{ + Transport: &http2.Transport{ + DialTLSContext: func(ctx context.Context, network, addr string, cfg *gotls.Config) (net.Conn, error) { + if config.Show { + fmt.Printf("REALITY localAddr: %v\tDialTLSContext\n", localAddr) + } + return uConn, nil + }, + }, + } + prefix := []byte("https://" + uConn.ServerName) + maps.Lock() + if maps.maps == nil { + maps.maps = make(map[string]map[string]struct{}) + } + paths := maps.maps[uConn.ServerName] + if paths == nil { + paths = make(map[string]struct{}) + paths[config.SpiderX] = struct{}{} + maps.maps[uConn.ServerName] = paths + } + firstURL := string(prefix) + getPathLocked(paths) + maps.Unlock() + get := func(first bool) { + var ( + req *http.Request + resp *http.Response + err error + body []byte + ) + if first { + req, _ = http.NewRequest("GET", firstURL, nil) + } else { + maps.Lock() + req, _ = http.NewRequest("GET", string(prefix)+getPathLocked(paths), nil) + maps.Unlock() + } + if req == nil { + return + } + utils.TryDefaultHeadersWith(req.Header, "nav") + if first && config.Show { + fmt.Printf("REALITY localAddr: %v\treq.UserAgent(): %v\n", localAddr, req.UserAgent()) + } + times := 1 + if !first { + times = int(crypto.RandBetween(config.SpiderY[4], config.SpiderY[5])) + } + for j := 0; j < times; j++ { + if !first && j == 0 { + req.Header.Set("Referer", firstURL) + } + req.AddCookie(&http.Cookie{Name: "padding", Value: strings.Repeat("0", int(crypto.RandBetween(config.SpiderY[0], config.SpiderY[1])))}) + if resp, err = client.Do(req); err != nil { + break + } + defer resp.Body.Close() + req.Header.Set("Referer", req.URL.String()) + if body, err = io.ReadAll(resp.Body); err != nil { + break + } + maps.Lock() + for _, m := range href.FindAllSubmatch(body, -1) { + m[1] = bytes.TrimPrefix(m[1], prefix) + if !bytes.Contains(m[1], dot) { + paths[string(m[1])] = struct{}{} + } + } + req.URL.Path = getPathLocked(paths) + if config.Show { + fmt.Printf("REALITY localAddr: %v\treq.Referer(): %v\n", localAddr, req.Referer()) + fmt.Printf("REALITY localAddr: %v\tlen(body): %v\n", localAddr, len(body)) + fmt.Printf("REALITY localAddr: %v\tlen(paths): %v\n", localAddr, len(paths)) + } + maps.Unlock() + if !first { + time.Sleep(time.Duration(crypto.RandBetween(config.SpiderY[6], config.SpiderY[7])) * time.Millisecond) // interval + } + } + } + get(true) + concurrency := int(crypto.RandBetween(config.SpiderY[2], config.SpiderY[3])) + for i := 0; i < concurrency; i++ { + go get(false) + } + // Do not close the connection + }() + time.Sleep(time.Duration(crypto.RandBetween(config.SpiderY[8], config.SpiderY[9])) * time.Millisecond) // return + return nil, errors.New("REALITY: processed invalid connection").AtWarning() + } + return uConn, nil +} + +var ( + href = regexp.MustCompile(`href="([/h].*?)"`) + dot = []byte(".") +) + +var maps struct { + sync.Mutex + maps map[string]map[string]struct{} +} + +func getPathLocked(paths map[string]struct{}) string { + stopAt := int(crypto.RandBetween(0, int64(len(paths)-1))) + i := 0 + for s := range paths { + if i == stopAt { + return s + } + i++ + } + return "/" +} diff --git a/subproject/Xray-core-main/transport/internet/sockopt.go b/subproject/Xray-core-main/transport/internet/sockopt.go new file mode 100644 index 00000000..d79191ab --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/sockopt.go @@ -0,0 +1,30 @@ +package internet + +func isTCPSocket(network string) bool { + switch network { + case "tcp", "tcp4", "tcp6": + return true + default: + return false + } +} + +func isUDPSocket(network string) bool { + switch network { + case "udp", "udp4", "udp6": + return true + default: + return false + } +} + +func (v *SocketConfig) ParseTFOValue() int { + if v.Tfo == 0 { + return -1 + } + tfo := int(v.Tfo) + if tfo < 0 { + tfo = 0 + } + return tfo +} diff --git a/subproject/Xray-core-main/transport/internet/sockopt_darwin.go b/subproject/Xray-core-main/transport/internet/sockopt_darwin.go new file mode 100644 index 00000000..bcae2f3f --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/sockopt_darwin.go @@ -0,0 +1,329 @@ +package internet + +import ( + "context" + "os" + "runtime" + "strconv" + "strings" + "syscall" + "unsafe" + + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "golang.org/x/sys/unix" +) + +const ( + // TCP_FASTOPEN_SERVER is the value to enable TCP fast open on darwin for server connections. + TCP_FASTOPEN_SERVER = 0x01 + // TCP_FASTOPEN_CLIENT is the value to enable TCP fast open on darwin for client connections. + TCP_FASTOPEN_CLIENT = 0x02 // nolint: revive,stylecheck + // syscall.TCP_KEEPINTVL is missing on some darwin architectures. + sysTCP_KEEPINTVL = 0x101 // nolint: revive,stylecheck +) + +const ( + PfOut = 2 + IOCOut = 0x40000000 + IOCIn = 0x80000000 + IOCInOut = IOCIn | IOCOut + IOCPARMMask = 0x1FFF + LEN = 4*16 + 4*4 + 4*1 + // #define _IOC(inout,group,num,len) (inout | ((len & IOCPARMMask) << 16) | ((group) << 8) | (num)) + // #define _IOWR(g,n,t) _IOC(IOCInOut, (g), (n), sizeof(t)) + // #define DIOCNATLOOK _IOWR('D', 23, struct pfioc_natlook) + DIOCNATLOOK = IOCInOut | ((LEN & IOCPARMMask) << 16) | ('D' << 8) | 23 +) + +// OriginalDst uses ioctl to read original destination from /dev/pf +func OriginalDst(la, ra net.Addr) (net.IP, int, error) { + f, err := os.Open("/dev/pf") + if err != nil { + return net.IP{}, -1, errors.New("failed to open device /dev/pf").Base(err) + } + defer f.Close() + fd := f.Fd() + nl := struct { // struct pfioc_natlook + saddr, daddr, rsaddr, rdaddr [16]byte + sxport, dxport, rsxport, rdxport [4]byte + af, proto, protoVariant, direction uint8 + }{ + af: syscall.AF_INET, + proto: syscall.IPPROTO_TCP, + direction: PfOut, + } + var raIP, laIP net.IP + var raPort, laPort int + switch la.(type) { + case *net.TCPAddr: + raIP = ra.(*net.TCPAddr).IP + laIP = la.(*net.TCPAddr).IP + raPort = ra.(*net.TCPAddr).Port + laPort = la.(*net.TCPAddr).Port + case *net.UDPAddr: + raIP = ra.(*net.UDPAddr).IP + laIP = la.(*net.UDPAddr).IP + raPort = ra.(*net.UDPAddr).Port + laPort = la.(*net.UDPAddr).Port + } + if raIP.To4() != nil { + if laIP.IsUnspecified() { + laIP = net.ParseIP("127.0.0.1") + } + copy(nl.saddr[:net.IPv4len], raIP.To4()) + copy(nl.daddr[:net.IPv4len], laIP.To4()) + } + if raIP.To16() != nil && raIP.To4() == nil { + if laIP.IsUnspecified() { + laIP = net.ParseIP("::1") + } + copy(nl.saddr[:], raIP) + copy(nl.daddr[:], laIP) + } + nl.sxport[0], nl.sxport[1] = byte(raPort>>8), byte(raPort) + nl.dxport[0], nl.dxport[1] = byte(laPort>>8), byte(laPort) + if _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, fd, DIOCNATLOOK, uintptr(unsafe.Pointer(&nl))); errno != 0 { + return net.IP{}, -1, os.NewSyscallError("ioctl", err) + } + + odPort := nl.rdxport + var odIP net.IP + switch nl.af { + case syscall.AF_INET: + odIP = make(net.IP, net.IPv4len) + copy(odIP, nl.rdaddr[:net.IPv4len]) + case syscall.AF_INET6: + odIP = make(net.IP, net.IPv6len) + copy(odIP, nl.rdaddr[:]) + } + return odIP, int(net.PortFromBytes(odPort[:2])), nil +} + +func applyOutboundSocketOptions(network string, address string, fd uintptr, config *SocketConfig) error { + if isTCPSocket(network) { + tfo := config.ParseTFOValue() + if tfo > 0 { + tfo = TCP_FASTOPEN_CLIENT + } + if tfo >= 0 { + if err := unix.SetsockoptInt(int(fd), unix.IPPROTO_TCP, unix.TCP_FASTOPEN, tfo); err != nil { + return err + } + } + + if config.TcpKeepAliveIdle > 0 || config.TcpKeepAliveInterval > 0 { + if config.TcpKeepAliveIdle > 0 { + if err := unix.SetsockoptInt(int(fd), unix.IPPROTO_TCP, unix.TCP_KEEPALIVE, int(config.TcpKeepAliveInterval)); err != nil { + return errors.New("failed to set TCP_KEEPINTVL", err) + } + } + if config.TcpKeepAliveInterval > 0 { + if err := unix.SetsockoptInt(int(fd), unix.IPPROTO_TCP, sysTCP_KEEPINTVL, int(config.TcpKeepAliveIdle)); err != nil { + return errors.New("failed to set TCP_KEEPIDLE", err) + } + } + if err := unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_KEEPALIVE, 1); err != nil { + return errors.New("failed to set SO_KEEPALIVE", err) + } + } else if config.TcpKeepAliveInterval < 0 || config.TcpKeepAliveIdle < 0 { + if err := unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_KEEPALIVE, 0); err != nil { + return errors.New("failed to unset SO_KEEPALIVE", err) + } + } + } + + if config.Interface != "" { + iface, err := net.InterfaceByName(config.Interface) + + if err != nil { + return errors.New("failed to get interface ", config.Interface).Base(err) + } + if network == "tcp6" || network == "udp6" { + if err := unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_BOUND_IF, iface.Index); err != nil { + return errors.New("failed to set IPV6_BOUND_IF").Base(err) + } + } else { + if err := unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_BOUND_IF, iface.Index); err != nil { + return errors.New("failed to set IP_BOUND_IF").Base(err) + } + } + } + + if len(config.CustomSockopt) > 0 { + for _, custom := range config.CustomSockopt { + if custom.System != "" && custom.System != runtime.GOOS { + errors.LogDebug(context.Background(), "CustomSockopt system not match: ", "want ", custom.System, " got ", runtime.GOOS) + continue + } + // Skip unwanted network type + // network might be tcp4 or tcp6 + // use HasPrefix so that "tcp" can match tcp4/6 with "tcp" if user want to control all tcp (udp is also the same) + // if it is empty, strings.HasPrefix will always return true to make it apply for all networks + if !strings.HasPrefix(network, custom.Network) { + continue + } + var level = 0x6 // default TCP + var opt int + if len(custom.Opt) == 0 { + return errors.New("No opt!") + } else { + opt, _ = strconv.Atoi(custom.Opt) + } + if custom.Level != "" { + level, _ = strconv.Atoi(custom.Level) + } + if custom.Type == "int" { + value, _ := strconv.Atoi(custom.Value) + if err := syscall.SetsockoptInt(int(fd), level, opt, value); err != nil { + return errors.New("failed to set CustomSockoptInt", opt, value, err) + } + } else if custom.Type == "str" { + if err := syscall.SetsockoptString(int(fd), level, opt, custom.Value); err != nil { + return errors.New("failed to set CustomSockoptString", opt, custom.Value, err) + } + } else { + return errors.New("unknown CustomSockopt type:", custom.Type) + } + } + } + + return nil +} + +func applyInboundSocketOptions(network string, fd uintptr, config *SocketConfig) error { + if isTCPSocket(network) { + tfo := config.ParseTFOValue() + if tfo > 0 { + tfo = TCP_FASTOPEN_SERVER + } + if tfo >= 0 { + if err := unix.SetsockoptInt(int(fd), unix.IPPROTO_TCP, unix.TCP_FASTOPEN, tfo); err != nil { + return err + } + } + + if config.TcpKeepAliveIdle > 0 || config.TcpKeepAliveInterval > 0 { + if config.TcpKeepAliveIdle > 0 { + if err := unix.SetsockoptInt(int(fd), unix.IPPROTO_TCP, unix.TCP_KEEPALIVE, int(config.TcpKeepAliveInterval)); err != nil { + return errors.New("failed to set TCP_KEEPINTVL", err) + } + } + if config.TcpKeepAliveInterval > 0 { + if err := unix.SetsockoptInt(int(fd), unix.IPPROTO_TCP, sysTCP_KEEPINTVL, int(config.TcpKeepAliveIdle)); err != nil { + return errors.New("failed to set TCP_KEEPIDLE", err) + } + } + if err := unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_KEEPALIVE, 1); err != nil { + return errors.New("failed to set SO_KEEPALIVE", err) + } + } else if config.TcpKeepAliveInterval < 0 || config.TcpKeepAliveIdle < 0 { + if err := unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_KEEPALIVE, 0); err != nil { + return errors.New("failed to unset SO_KEEPALIVE", err) + } + } + } + + if config.Interface != "" { + iface, err := net.InterfaceByName(config.Interface) + + if err != nil { + return errors.New("failed to get interface ", config.Interface).Base(err) + } + if network == "tcp6" || network == "udp6" { + if err := unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_BOUND_IF, iface.Index); err != nil { + return errors.New("failed to set IPV6_BOUND_IF").Base(err) + } + } else { + if err := unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_BOUND_IF, iface.Index); err != nil { + return errors.New("failed to set IP_BOUND_IF").Base(err) + } + } + } + + if config.V6Only { + if err := unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_V6ONLY, 1); err != nil { + return errors.New("failed to set IPV6_V6ONLY").Base(err) + } + } + + if len(config.CustomSockopt) > 0 { + for _, custom := range config.CustomSockopt { + if custom.System != "" && custom.System != runtime.GOOS { + errors.LogDebug(context.Background(), "CustomSockopt system not match: ", "want ", custom.System, " got ", runtime.GOOS) + continue + } + // Skip unwanted network type + // network might be tcp4 or tcp6 + // use HasPrefix so that "tcp" can match tcp4/6 with "tcp" if user want to control all tcp (udp is also the same) + // if it is empty, strings.HasPrefix will always return true to make it apply for all networks + if !strings.HasPrefix(network, custom.Network) { + continue + } + var level = 0x6 // default TCP + var opt int + if len(custom.Opt) == 0 { + return errors.New("No opt!") + } else { + opt, _ = strconv.Atoi(custom.Opt) + } + if custom.Level != "" { + level, _ = strconv.Atoi(custom.Level) + } + if custom.Type == "int" { + value, _ := strconv.Atoi(custom.Value) + if err := syscall.SetsockoptInt(int(fd), level, opt, value); err != nil { + return errors.New("failed to set CustomSockoptInt", opt, value, err) + } + } else if custom.Type == "str" { + if err := syscall.SetsockoptString(int(fd), level, opt, custom.Value); err != nil { + return errors.New("failed to set CustomSockoptString", opt, custom.Value, err) + } + } else { + return errors.New("unknown CustomSockopt type:", custom.Type) + } + } + } + + return nil +} + +func bindAddr(fd uintptr, address []byte, port uint32) error { + setReuseAddr(fd) + setReusePort(fd) + + var sockaddr unix.Sockaddr + + switch len(address) { + case net.IPv4len: + a4 := &unix.SockaddrInet4{ + Port: int(port), + } + copy(a4.Addr[:], address) + sockaddr = a4 + case net.IPv6len: + a6 := &unix.SockaddrInet6{ + Port: int(port), + } + copy(a6.Addr[:], address) + sockaddr = a6 + default: + return errors.New("unexpected length of ip") + } + + return unix.Bind(int(fd), sockaddr) +} + +func setReuseAddr(fd uintptr) error { + if err := unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEADDR, 1); err != nil { + return errors.New("failed to set SO_REUSEADDR").Base(err).AtWarning() + } + return nil +} + +func setReusePort(fd uintptr) error { + if err := unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1); err != nil { + return errors.New("failed to set SO_REUSEPORT").Base(err).AtWarning() + } + return nil +} diff --git a/subproject/Xray-core-main/transport/internet/sockopt_freebsd.go b/subproject/Xray-core-main/transport/internet/sockopt_freebsd.go new file mode 100644 index 00000000..4d490a40 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/sockopt_freebsd.go @@ -0,0 +1,265 @@ +package internet + +import ( + "encoding/binary" + "net" + "os" + "syscall" + "unsafe" + + "github.com/xtls/xray-core/common/errors" + "golang.org/x/sys/unix" +) + +const ( + sysPFINOUT = 0x0 + sysPFIN = 0x1 + sysPFOUT = 0x2 + sysPFFWD = 0x3 + sysDIOCNATLOOK = 0xc04c4417 +) + +type pfiocNatlook struct { + Saddr [16]byte /* pf_addr */ + Daddr [16]byte /* pf_addr */ + Rsaddr [16]byte /* pf_addr */ + Rdaddr [16]byte /* pf_addr */ + Sport uint16 + Dport uint16 + Rsport uint16 + Rdport uint16 + Af uint8 + Proto uint8 + Direction uint8 + Pad [1]byte +} + +const ( + sizeofPfiocNatlook = 0x4c + soReUsePort = 0x00000200 + soReUsePortLB = 0x00010000 +) + +func ioctl(s uintptr, ioc int, b []byte) error { + if _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, s, uintptr(ioc), uintptr(unsafe.Pointer(&b[0]))); errno != 0 { + return error(errno) + } + return nil +} + +func (nl *pfiocNatlook) rdPort() int { + return int(binary.BigEndian.Uint16((*[2]byte)(unsafe.Pointer(&nl.Rdport))[:])) +} + +func (nl *pfiocNatlook) setPort(remote, local int) { + binary.BigEndian.PutUint16((*[2]byte)(unsafe.Pointer(&nl.Sport))[:], uint16(remote)) + binary.BigEndian.PutUint16((*[2]byte)(unsafe.Pointer(&nl.Dport))[:], uint16(local)) +} + +// OriginalDst uses ioctl to read original destination from /dev/pf +func OriginalDst(la, ra net.Addr) (net.IP, int, error) { + f, err := os.Open("/dev/pf") + if err != nil { + return net.IP{}, -1, errors.New("failed to open device /dev/pf").Base(err) + } + defer f.Close() + fd := f.Fd() + b := make([]byte, sizeofPfiocNatlook) + nl := (*pfiocNatlook)(unsafe.Pointer(&b[0])) + var raIP, laIP net.IP + var raPort, laPort int + switch la.(type) { + case *net.TCPAddr: + raIP = ra.(*net.TCPAddr).IP + laIP = la.(*net.TCPAddr).IP + raPort = ra.(*net.TCPAddr).Port + laPort = la.(*net.TCPAddr).Port + nl.Proto = syscall.IPPROTO_TCP + case *net.UDPAddr: + raIP = ra.(*net.UDPAddr).IP + laIP = la.(*net.UDPAddr).IP + raPort = ra.(*net.UDPAddr).Port + laPort = la.(*net.UDPAddr).Port + nl.Proto = syscall.IPPROTO_UDP + } + if raIP.To4() != nil { + if laIP.IsUnspecified() { + laIP = net.ParseIP("127.0.0.1") + } + copy(nl.Saddr[:net.IPv4len], raIP.To4()) + copy(nl.Daddr[:net.IPv4len], laIP.To4()) + nl.Af = syscall.AF_INET + } + if raIP.To16() != nil && raIP.To4() == nil { + if laIP.IsUnspecified() { + laIP = net.ParseIP("::1") + } + copy(nl.Saddr[:], raIP) + copy(nl.Daddr[:], laIP) + nl.Af = syscall.AF_INET6 + } + nl.setPort(raPort, laPort) + ioc := uintptr(sysDIOCNATLOOK) + for _, dir := range []byte{sysPFOUT, sysPFIN} { + nl.Direction = dir + err = ioctl(fd, int(ioc), b) + if err == nil || err != syscall.ENOENT { + break + } + } + if err != nil { + return net.IP{}, -1, os.NewSyscallError("ioctl", err) + } + + odPort := nl.rdPort() + var odIP net.IP + switch nl.Af { + case syscall.AF_INET: + odIP = make(net.IP, net.IPv4len) + copy(odIP, nl.Rdaddr[:net.IPv4len]) + case syscall.AF_INET6: + odIP = make(net.IP, net.IPv6len) + copy(odIP, nl.Rdaddr[:]) + } + return odIP, odPort, nil +} + +func applyOutboundSocketOptions(network string, address string, fd uintptr, config *SocketConfig) error { + if config.Mark != 0 { + if err := syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_USER_COOKIE, int(config.Mark)); err != nil { + return errors.New("failed to set SO_USER_COOKIE").Base(err) + } + } + + if isTCPSocket(network) { + tfo := config.ParseTFOValue() + if tfo > 0 { + tfo = 1 + } + if tfo >= 0 { + if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, unix.TCP_FASTOPEN, tfo); err != nil { + return errors.New("failed to set TCP_FASTOPEN_CONNECT=", tfo).Base(err) + } + } + if config.TcpKeepAliveIdle > 0 || config.TcpKeepAliveInterval > 0 { + if config.TcpKeepAliveIdle > 0 { + if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, syscall.TCP_KEEPIDLE, int(config.TcpKeepAliveIdle)); err != nil { + return errors.New("failed to set TCP_KEEPIDLE", err) + } + } + if config.TcpKeepAliveInterval > 0 { + if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, syscall.TCP_KEEPINTVL, int(config.TcpKeepAliveInterval)); err != nil { + return errors.New("failed to set TCP_KEEPINTVL", err) + } + } + if err := syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_KEEPALIVE, 1); err != nil { + return errors.New("failed to set SO_KEEPALIVE", err) + } + } else if config.TcpKeepAliveInterval < 0 || config.TcpKeepAliveIdle < 0 { + if err := syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_KEEPALIVE, 0); err != nil { + return errors.New("failed to unset SO_KEEPALIVE", err) + } + } + } + + if config.Tproxy.IsEnabled() { + ip, _, _ := net.SplitHostPort(address) + if net.ParseIP(ip).To4() != nil { + if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IP, syscall.IP_BINDANY, 1); err != nil { + return errors.New("failed to set outbound IP_BINDANY").Base(err) + } + } else { + if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IPV6, syscall.IPV6_BINDANY, 1); err != nil { + return errors.New("failed to set outbound IPV6_BINDANY").Base(err) + } + } + } + return nil +} + +func applyInboundSocketOptions(network string, fd uintptr, config *SocketConfig) error { + if config.Mark != 0 { + if err := syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_USER_COOKIE, int(config.Mark)); err != nil { + return errors.New("failed to set SO_USER_COOKIE").Base(err) + } + } + if isTCPSocket(network) { + tfo := config.ParseTFOValue() + if tfo >= 0 { + if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, unix.TCP_FASTOPEN, tfo); err != nil { + return errors.New("failed to set TCP_FASTOPEN=", tfo).Base(err) + } + } + if config.TcpKeepAliveIdle > 0 || config.TcpKeepAliveInterval > 0 { + if config.TcpKeepAliveIdle > 0 { + if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, syscall.TCP_KEEPIDLE, int(config.TcpKeepAliveIdle)); err != nil { + return errors.New("failed to set TCP_KEEPIDLE", err) + } + } + if config.TcpKeepAliveInterval > 0 { + if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, syscall.TCP_KEEPINTVL, int(config.TcpKeepAliveInterval)); err != nil { + return errors.New("failed to set TCP_KEEPINTVL", err) + } + } + if err := syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_KEEPALIVE, 1); err != nil { + return errors.New("failed to set SO_KEEPALIVE", err) + } + } else if config.TcpKeepAliveInterval < 0 || config.TcpKeepAliveIdle < 0 { + if err := syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_KEEPALIVE, 0); err != nil { + return errors.New("failed to unset SO_KEEPALIVE", err) + } + } + } + + if config.Tproxy.IsEnabled() { + if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IPV6, syscall.IPV6_BINDANY, 1); err != nil { + if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IP, syscall.IP_BINDANY, 1); err != nil { + return errors.New("failed to set inbound IP_BINDANY").Base(err) + } + } + } + + return nil +} + +func bindAddr(fd uintptr, ip []byte, port uint32) error { + setReuseAddr(fd) + setReusePort(fd) + + var sockaddr syscall.Sockaddr + + switch len(ip) { + case net.IPv4len: + a4 := &syscall.SockaddrInet4{ + Port: int(port), + } + copy(a4.Addr[:], ip) + sockaddr = a4 + case net.IPv6len: + a6 := &syscall.SockaddrInet6{ + Port: int(port), + } + copy(a6.Addr[:], ip) + sockaddr = a6 + default: + return errors.New("unexpected length of ip") + } + + return syscall.Bind(int(fd), sockaddr) +} + +func setReuseAddr(fd uintptr) error { + if err := syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1); err != nil { + return errors.New("failed to set SO_REUSEADDR").Base(err).AtWarning() + } + return nil +} + +func setReusePort(fd uintptr) error { + if err := syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, soReUsePortLB, 1); err != nil { + if err := syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, soReUsePort, 1); err != nil { + return errors.New("failed to set SO_REUSEPORT").Base(err).AtWarning() + } + } + return nil +} diff --git a/subproject/Xray-core-main/transport/internet/sockopt_linux.go b/subproject/Xray-core-main/transport/internet/sockopt_linux.go new file mode 100644 index 00000000..9a305a40 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/sockopt_linux.go @@ -0,0 +1,273 @@ +package internet + +import ( + "context" + "net" + "runtime" + "strconv" + "strings" + "syscall" + + "github.com/xtls/xray-core/common/errors" + "golang.org/x/sys/unix" +) + +func bindAddr(fd uintptr, ip []byte, port uint32) error { + setReuseAddr(fd) + setReusePort(fd) + + var sockaddr syscall.Sockaddr + + switch len(ip) { + case net.IPv4len: + a4 := &syscall.SockaddrInet4{ + Port: int(port), + } + copy(a4.Addr[:], ip) + sockaddr = a4 + case net.IPv6len: + a6 := &syscall.SockaddrInet6{ + Port: int(port), + } + copy(a6.Addr[:], ip) + sockaddr = a6 + default: + return errors.New("unexpected length of ip") + } + + return syscall.Bind(int(fd), sockaddr) +} + +// applyOutboundSocketOptions applies socket options for outbound connection. +// note that unlike other part of Xray, this function needs network with speified network stack(tcp4/tcp6/udp4/udp6) +func applyOutboundSocketOptions(network string, address string, fd uintptr, config *SocketConfig) error { + if config.Mark != 0 { + if err := syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, int(config.Mark)); err != nil { + return errors.New("failed to set SO_MARK").Base(err) + } + } + + if config.Interface != "" { + if err := syscall.BindToDevice(int(fd), config.Interface); err != nil { + return errors.New("failed to set Interface").Base(err) + } + } + + if isTCPSocket(network) { + tfo := config.ParseTFOValue() + if tfo > 0 { + tfo = 1 + } + if tfo >= 0 { + if err := syscall.SetsockoptInt(int(fd), syscall.SOL_TCP, unix.TCP_FASTOPEN_CONNECT, tfo); err != nil { + return errors.New("failed to set TCP_FASTOPEN_CONNECT", tfo).Base(err) + } + } + + if config.TcpCongestion != "" { + if err := syscall.SetsockoptString(int(fd), syscall.SOL_TCP, syscall.TCP_CONGESTION, config.TcpCongestion); err != nil { + return errors.New("failed to set TCP_CONGESTION", err) + } + } + + if config.TcpWindowClamp > 0 { + if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, syscall.TCP_WINDOW_CLAMP, int(config.TcpWindowClamp)); err != nil { + return errors.New("failed to set TCP_WINDOW_CLAMP", err) + } + } + + if config.TcpUserTimeout > 0 { + if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, unix.TCP_USER_TIMEOUT, int(config.TcpUserTimeout)); err != nil { + return errors.New("failed to set TCP_USER_TIMEOUT", err) + } + } + + if config.TcpMaxSeg > 0 { + if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, unix.TCP_MAXSEG, int(config.TcpMaxSeg)); err != nil { + return errors.New("failed to set TCP_MAXSEG", err) + } + } + + } + + if len(config.CustomSockopt) > 0 { + for _, custom := range config.CustomSockopt { + if custom.System != "" && custom.System != runtime.GOOS { + errors.LogDebug(context.Background(), "CustomSockopt system not match: ", "want ", custom.System, " got ", runtime.GOOS) + continue + } + // Skip unwanted network type + // network might be tcp4 or tcp6 + // use HasPrefix so that "tcp" can match tcp4/6 with "tcp" if user want to control all tcp (udp is also the same) + // if it is empty, strings.HasPrefix will always return true to make it apply for all networks + if !strings.HasPrefix(network, custom.Network) { + continue + } + var level = 0x6 // default TCP + var opt int + if len(custom.Opt) == 0 { + return errors.New("No opt!") + } else { + opt, _ = strconv.Atoi(custom.Opt) + } + if custom.Level != "" { + level, _ = strconv.Atoi(custom.Level) + } + if custom.Type == "int" { + value, _ := strconv.Atoi(custom.Value) + if err := syscall.SetsockoptInt(int(fd), level, opt, value); err != nil { + return errors.New("failed to set CustomSockoptInt", opt, value, err) + } + } else if custom.Type == "str" { + if err := syscall.SetsockoptString(int(fd), level, opt, custom.Value); err != nil { + return errors.New("failed to set CustomSockoptString", opt, custom.Value, err) + } + } else { + return errors.New("unknown CustomSockopt type:", custom.Type) + } + } + } + + if config.Tproxy.IsEnabled() { + if err := syscall.SetsockoptInt(int(fd), syscall.SOL_IP, syscall.IP_TRANSPARENT, 1); err != nil { + return errors.New("failed to set IP_TRANSPARENT").Base(err) + } + } + + return nil +} + +// applyInboundSocketOptions applies socket options for inbound listener. +// note that unlike other part of Xray, this function needs network with speified network stack(tcp4/tcp6/udp4/udp6) +func applyInboundSocketOptions(network string, fd uintptr, config *SocketConfig) error { + if config.Mark != 0 { + if err := syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, int(config.Mark)); err != nil { + return errors.New("failed to set SO_MARK").Base(err) + } + } + if isTCPSocket(network) { + tfo := config.ParseTFOValue() + if tfo >= 0 { + if err := syscall.SetsockoptInt(int(fd), syscall.SOL_TCP, unix.TCP_FASTOPEN, tfo); err != nil { + return errors.New("failed to set TCP_FASTOPEN", tfo).Base(err) + } + } + + if config.TcpKeepAliveInterval > 0 || config.TcpKeepAliveIdle > 0 { + if config.TcpKeepAliveInterval > 0 { + if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, syscall.TCP_KEEPINTVL, int(config.TcpKeepAliveInterval)); err != nil { + return errors.New("failed to set TCP_KEEPINTVL", err) + } + } + if config.TcpKeepAliveIdle > 0 { + if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, syscall.TCP_KEEPIDLE, int(config.TcpKeepAliveIdle)); err != nil { + return errors.New("failed to set TCP_KEEPIDLE", err) + } + } + if err := syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_KEEPALIVE, 1); err != nil { + return errors.New("failed to set SO_KEEPALIVE", err) + } + } else if config.TcpKeepAliveInterval < 0 || config.TcpKeepAliveIdle < 0 { + if err := syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_KEEPALIVE, 0); err != nil { + return errors.New("failed to unset SO_KEEPALIVE", err) + } + } + + if config.TcpCongestion != "" { + if err := syscall.SetsockoptString(int(fd), syscall.SOL_TCP, syscall.TCP_CONGESTION, config.TcpCongestion); err != nil { + return errors.New("failed to set TCP_CONGESTION", err) + } + } + + if config.TcpWindowClamp > 0 { + if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, syscall.TCP_WINDOW_CLAMP, int(config.TcpWindowClamp)); err != nil { + return errors.New("failed to set TCP_WINDOW_CLAMP", err) + } + } + + if config.TcpUserTimeout > 0 { + if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, unix.TCP_USER_TIMEOUT, int(config.TcpUserTimeout)); err != nil { + return errors.New("failed to set TCP_USER_TIMEOUT", err) + } + } + + if config.TcpMaxSeg > 0 { + if err := syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, unix.TCP_MAXSEG, int(config.TcpMaxSeg)); err != nil { + return errors.New("failed to set TCP_MAXSEG", err) + } + } + if len(config.CustomSockopt) > 0 { + for _, custom := range config.CustomSockopt { + if custom.System != "" && custom.System != runtime.GOOS { + errors.LogDebug(context.Background(), "CustomSockopt system not match: ", "want ", custom.System, " got ", runtime.GOOS) + continue + } + // Skip unwanted network type + // network might be tcp4 or tcp6 + // use HasPrefix so that "tcp" can match tcp4/6 with "tcp" if user want to control all tcp (udp is also the same) + // if it is empty, strings.HasPrefix will always return true to make it apply for all networks + if !strings.HasPrefix(network, custom.Network) { + continue + } + var level = 0x6 // default TCP + var opt int + if len(custom.Opt) == 0 { + return errors.New("No opt!") + } else { + opt, _ = strconv.Atoi(custom.Opt) + } + if custom.Level != "" { + level, _ = strconv.Atoi(custom.Level) + } + if custom.Type == "int" { + value, _ := strconv.Atoi(custom.Value) + if err := syscall.SetsockoptInt(int(fd), level, opt, value); err != nil { + return errors.New("failed to set CustomSockoptInt", opt, value, err) + } + } else if custom.Type == "str" { + if err := syscall.SetsockoptString(int(fd), level, opt, custom.Value); err != nil { + return errors.New("failed to set CustomSockoptString", opt, custom.Value, err) + } + } else { + return errors.New("unknown CustomSockopt type:", custom.Type) + } + } + } + } + + if config.Tproxy.IsEnabled() { + if err := syscall.SetsockoptInt(int(fd), syscall.SOL_IP, syscall.IP_TRANSPARENT, 1); err != nil { + return errors.New("failed to set IP_TRANSPARENT").Base(err) + } + } + + if config.ReceiveOriginalDestAddress && isUDPSocket(network) { + err1 := syscall.SetsockoptInt(int(fd), syscall.SOL_IPV6, unix.IPV6_RECVORIGDSTADDR, 1) + err2 := syscall.SetsockoptInt(int(fd), syscall.SOL_IP, syscall.IP_RECVORIGDSTADDR, 1) + if err1 != nil && err2 != nil { + return err1 + } + } + + if config.V6Only { + if err := syscall.SetsockoptInt(int(fd), syscall.SOL_IPV6, syscall.IPV6_V6ONLY, 1); err != nil { + return errors.New("failed to set IPV6_V6ONLY", err) + } + } + + return nil +} + +func setReuseAddr(fd uintptr) error { + if err := syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1); err != nil { + return errors.New("failed to set SO_REUSEADDR").Base(err).AtWarning() + } + return nil +} + +func setReusePort(fd uintptr) error { + if err := syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, unix.SO_REUSEPORT, 1); err != nil { + return errors.New("failed to set SO_REUSEPORT").Base(err).AtWarning() + } + return nil +} diff --git a/subproject/Xray-core-main/transport/internet/sockopt_linux_test.go b/subproject/Xray-core-main/transport/internet/sockopt_linux_test.go new file mode 100644 index 00000000..17b7cee0 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/sockopt_linux_test.go @@ -0,0 +1,42 @@ +package internet_test + +import ( + "context" + "syscall" + "testing" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/testing/servers/tcp" + . "github.com/xtls/xray-core/transport/internet" +) + +func TestSockOptMark(t *testing.T) { + t.Skip("requires CAP_NET_ADMIN") + + tcpServer := tcp.Server{ + MsgProcessor: func(b []byte) []byte { + return b + }, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + const mark = 1 + dialer := DefaultSystemDialer{} + conn, err := dialer.Dial(context.Background(), nil, dest, &SocketConfig{Mark: mark}) + common.Must(err) + defer conn.Close() + + rawConn, err := conn.(*net.TCPConn).SyscallConn() + common.Must(err) + err = rawConn.Control(func(fd uintptr) { + m, err := syscall.GetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK) + common.Must(err) + if mark != m { + t.Fatal("unexpected connection mark", m, " want ", mark) + } + }) + common.Must(err) +} diff --git a/subproject/Xray-core-main/transport/internet/sockopt_other.go b/subproject/Xray-core-main/transport/internet/sockopt_other.go new file mode 100644 index 00000000..7e91110e --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/sockopt_other.go @@ -0,0 +1,24 @@ +//go:build js || netbsd || openbsd || solaris +// +build js netbsd openbsd solaris + +package internet + +func applyOutboundSocketOptions(network string, address string, fd uintptr, config *SocketConfig) error { + return nil +} + +func applyInboundSocketOptions(network string, fd uintptr, config *SocketConfig) error { + return nil +} + +func bindAddr(fd uintptr, ip []byte, port uint32) error { + return nil +} + +func setReuseAddr(fd uintptr) error { + return nil +} + +func setReusePort(fd uintptr) error { + return nil +} diff --git a/subproject/Xray-core-main/transport/internet/sockopt_test.go b/subproject/Xray-core-main/transport/internet/sockopt_test.go new file mode 100644 index 00000000..cc82bc21 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/sockopt_test.go @@ -0,0 +1,40 @@ +package internet_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/testing/servers/tcp" + . "github.com/xtls/xray-core/transport/internet" +) + +func TestTCPFastOpen(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: func(b []byte) []byte { + return b + }, + } + dest, err := tcpServer.StartContext(context.Background(), &SocketConfig{Tfo: 256}) + common.Must(err) + defer tcpServer.Close() + + ctx := context.Background() + dialer := DefaultSystemDialer{} + conn, err := dialer.Dial(ctx, nil, dest, &SocketConfig{ + Tfo: 1, + }) + common.Must(err) + defer conn.Close() + + _, err = conn.Write([]byte("abcd")) + common.Must(err) + + b := buf.New() + common.Must2(b.ReadFrom(conn)) + if r := cmp.Diff(b.Bytes(), []byte("abcd")); r != "" { + t.Fatal(r) + } +} diff --git a/subproject/Xray-core-main/transport/internet/sockopt_windows.go b/subproject/Xray-core-main/transport/internet/sockopt_windows.go new file mode 100644 index 00000000..1389ca06 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/sockopt_windows.go @@ -0,0 +1,194 @@ +package internet + +import ( + "context" + "encoding/binary" + "net" + "runtime" + "strconv" + "strings" + "syscall" + "unsafe" + + "github.com/xtls/xray-core/common/errors" +) + +const ( + TCP_FASTOPEN = 15 + IP_UNICAST_IF = 31 + IPV6_UNICAST_IF = 31 +) + +func setTFO(fd syscall.Handle, tfo int) error { + if tfo > 0 { + tfo = 1 + } + if tfo >= 0 { + if err := syscall.SetsockoptInt(fd, syscall.IPPROTO_TCP, TCP_FASTOPEN, tfo); err != nil { + return err + } + } + return nil +} + +func applyOutboundSocketOptions(network string, address string, fd uintptr, config *SocketConfig) error { + if config.Interface != "" { + inf, err := net.InterfaceByName(config.Interface) + if err != nil { + return errors.New("failed to find the interface").Base(err) + } + // easy way to check if the address is ipv4 + isV4 := strings.Contains(address, ".") + // note: DO NOT trust the passed network variable, it can be udp6 even if the address is ipv4 + // because operating system might(always) use ipv6 socket to process ipv4 + host, _, err := net.SplitHostPort(address) + if isV4 { + var bytes [4]byte + binary.BigEndian.PutUint32(bytes[:], uint32(inf.Index)) + idx := *(*uint32)(unsafe.Pointer(&bytes[0])) + if err := syscall.SetsockoptInt(syscall.Handle(fd), syscall.IPPROTO_IP, IP_UNICAST_IF, int(idx)); err != nil { + return errors.New("failed to set IP_UNICAST_IF").Base(err) + } + if ip := net.ParseIP(host); ip != nil && ip.IsMulticast() && isUDPSocket(network) { + if err := syscall.SetsockoptInt(syscall.Handle(fd), syscall.IPPROTO_IP, syscall.IP_MULTICAST_IF, int(idx)); err != nil { + return errors.New("failed to set IP_MULTICAST_IF").Base(err) + } + } + } else { + if err := syscall.SetsockoptInt(syscall.Handle(fd), syscall.IPPROTO_IPV6, IPV6_UNICAST_IF, inf.Index); err != nil { + return errors.New("failed to set IPV6_UNICAST_IF").Base(err) + } + if ip := net.ParseIP(host); ip != nil && ip.IsMulticast() && isUDPSocket(network) { + if err := syscall.SetsockoptInt(syscall.Handle(fd), syscall.IPPROTO_IPV6, syscall.IPV6_MULTICAST_IF, inf.Index); err != nil { + return errors.New("failed to set IPV6_MULTICAST_IF").Base(err) + } + } + } + } + + if isTCPSocket(network) { + if err := setTFO(syscall.Handle(fd), config.ParseTFOValue()); err != nil { + return err + } + if config.TcpKeepAliveIdle > 0 { + if err := syscall.SetsockoptInt(syscall.Handle(fd), syscall.SOL_SOCKET, syscall.SO_KEEPALIVE, 1); err != nil { + return errors.New("failed to set SO_KEEPALIVE", err) + } + } else if config.TcpKeepAliveIdle < 0 { + if err := syscall.SetsockoptInt(syscall.Handle(fd), syscall.SOL_SOCKET, syscall.SO_KEEPALIVE, 0); err != nil { + return errors.New("failed to unset SO_KEEPALIVE", err) + } + } + } + + if len(config.CustomSockopt) > 0 { + for _, custom := range config.CustomSockopt { + if custom.System != "" && custom.System != runtime.GOOS { + errors.LogDebug(context.Background(), "CustomSockopt system not match: ", "want ", custom.System, " got ", runtime.GOOS) + continue + } + // Skip unwanted network type + // network might be tcp4 or tcp6 + // use HasPrefix so that "tcp" can match tcp4/6 with "tcp" if user want to control all tcp (udp is also the same) + // if it is empty, strings.HasPrefix will always return true to make it apply for all networks + if !strings.HasPrefix(network, custom.Network) { + continue + } + var level = 0x6 // default TCP + var opt int + if len(custom.Opt) == 0 { + return errors.New("No opt!") + } else { + opt, _ = strconv.Atoi(custom.Opt) + } + if custom.Level != "" { + level, _ = strconv.Atoi(custom.Level) + } + if custom.Type == "int" { + value, _ := strconv.Atoi(custom.Value) + if err := syscall.SetsockoptInt(syscall.Handle(fd), level, opt, value); err != nil { + return errors.New("failed to set CustomSockoptInt", opt, value, err) + } + } else if custom.Type == "str" { + return errors.New("failed to set CustomSockoptString: Str type does not supported on windows") + } else { + return errors.New("unknown CustomSockopt type:", custom.Type) + } + } + } + + return nil +} + +func applyInboundSocketOptions(network string, fd uintptr, config *SocketConfig) error { + if isTCPSocket(network) { + if err := setTFO(syscall.Handle(fd), config.ParseTFOValue()); err != nil { + return err + } + if config.TcpKeepAliveIdle > 0 { + if err := syscall.SetsockoptInt(syscall.Handle(fd), syscall.SOL_SOCKET, syscall.SO_KEEPALIVE, 1); err != nil { + return errors.New("failed to set SO_KEEPALIVE", err) + } + } else if config.TcpKeepAliveIdle < 0 { + if err := syscall.SetsockoptInt(syscall.Handle(fd), syscall.SOL_SOCKET, syscall.SO_KEEPALIVE, 0); err != nil { + return errors.New("failed to unset SO_KEEPALIVE", err) + } + } + } + + if config.V6Only { + if err := syscall.SetsockoptInt(syscall.Handle(fd), syscall.IPPROTO_IPV6, syscall.IPV6_V6ONLY, 1); err != nil { + return errors.New("failed to set IPV6_V6ONLY").Base(err) + } + } + + if len(config.CustomSockopt) > 0 { + for _, custom := range config.CustomSockopt { + if custom.System != "" && custom.System != runtime.GOOS { + errors.LogDebug(context.Background(), "CustomSockopt system not match: ", "want ", custom.System, " got ", runtime.GOOS) + continue + } + // Skip unwanted network type + // network might be tcp4 or tcp6 + // use HasPrefix so that "tcp" can match tcp4/6 with "tcp" if user want to control all tcp (udp is also the same) + // if it is empty, strings.HasPrefix will always return true to make it apply for all networks + if !strings.HasPrefix(network, custom.Network) { + continue + } + var level = 0x6 // default TCP + var opt int + if len(custom.Opt) == 0 { + return errors.New("No opt!") + } else { + opt, _ = strconv.Atoi(custom.Opt) + } + if custom.Level != "" { + level, _ = strconv.Atoi(custom.Level) + } + if custom.Type == "int" { + value, _ := strconv.Atoi(custom.Value) + if err := syscall.SetsockoptInt(syscall.Handle(fd), level, opt, value); err != nil { + return errors.New("failed to set CustomSockoptInt", opt, value, err) + } + } else if custom.Type == "str" { + return errors.New("failed to set CustomSockoptString: Str type does not supported on windows") + } else { + return errors.New("unknown CustomSockopt type:", custom.Type) + } + } + } + + return nil +} + +func bindAddr(fd uintptr, ip []byte, port uint32) error { + return nil +} + +func setReuseAddr(fd uintptr) error { + return nil +} + +func setReusePort(fd uintptr) error { + return nil +} diff --git a/subproject/Xray-core-main/transport/internet/splithttp/browser_client.go b/subproject/Xray-core-main/transport/internet/splithttp/browser_client.go new file mode 100644 index 00000000..a70447f2 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/splithttp/browser_client.go @@ -0,0 +1,71 @@ +package splithttp + +import ( + "context" + "io" + "net/http" + + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/transport/internet/browser_dialer" + "github.com/xtls/xray-core/transport/internet/websocket" +) + +// BrowserDialerClient implements splithttp.DialerClient in terms of browser dialer +type BrowserDialerClient struct { + transportConfig *Config +} + +func (c *BrowserDialerClient) IsClosed() bool { + panic("not implemented yet") +} + +func (c *BrowserDialerClient) OpenStream(ctx context.Context, url string, sessionId string, body io.Reader, uploadOnly bool) (io.ReadCloser, net.Addr, net.Addr, error) { + if body != nil { + return nil, nil, nil, errors.New("bidirectional streaming for browser dialer not implemented yet") + } + + request, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, nil, nil, err + } + + c.transportConfig.FillStreamRequest(request, sessionId, "") + + conn, err := browser_dialer.DialGet(request.URL.String(), request.Header, request.Cookies()) + dummyAddr := &net.IPAddr{} + if err != nil { + return nil, dummyAddr, dummyAddr, err + } + + return websocket.NewConnection(conn, dummyAddr, nil, 0), conn.RemoteAddr(), conn.LocalAddr(), nil +} + +func (c *BrowserDialerClient) PostPacket(ctx context.Context, url string, sessionId string, seqStr string, payload buf.MultiBuffer) error { + method := c.transportConfig.GetNormalizedUplinkHTTPMethod() + request, err := http.NewRequest(method, url, nil) + if err != nil { + return err + } + + err = c.transportConfig.FillPacketRequest(request, sessionId, seqStr, payload) + if err != nil { + return err + } + + var bytes []byte + if request.Body != nil { + bytes, err = io.ReadAll(request.Body) + if err != nil { + return err + } + } + + err = browser_dialer.DialPacket(method, request.URL.String(), request.Header, request.Cookies(), bytes) + if err != nil { + return err + } + + return nil +} diff --git a/subproject/Xray-core-main/transport/internet/splithttp/client.go b/subproject/Xray-core-main/transport/internet/splithttp/client.go new file mode 100644 index 00000000..c156509a --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/splithttp/client.go @@ -0,0 +1,208 @@ +package splithttp + +import ( + "bytes" + "context" + "fmt" + "io" + "net/http" + "net/http/httptrace" + "sync" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/signal/done" +) + +// interface to abstract between use of browser dialer, vs net/http +type DialerClient interface { + IsClosed() bool + + // ctx, url, sessionId, body, uploadOnly + OpenStream(context.Context, string, string, io.Reader, bool) (io.ReadCloser, net.Addr, net.Addr, error) + + // ctx, url, sessionId, seqStr, body, contentLength + PostPacket(context.Context, string, string, string, buf.MultiBuffer) error +} + +// implements splithttp.DialerClient in terms of direct network connections +type DefaultDialerClient struct { + transportConfig *Config + client *http.Client + closed bool + httpVersion string + // pool of net.Conn, created using dialUploadConn + uploadRawPool *sync.Pool + dialUploadConn func(ctxInner context.Context) (net.Conn, error) +} + +func (c *DefaultDialerClient) IsClosed() bool { + return c.closed +} + +func (c *DefaultDialerClient) OpenStream(ctx context.Context, url string, sessionId string, body io.Reader, uploadOnly bool) (wrc io.ReadCloser, remoteAddr, localAddr net.Addr, err error) { + // this is done when the TCP/UDP connection to the server was established, + // and we can unblock the Dial function and print correct net addresses in + // logs + gotConn := done.New() + ctx = httptrace.WithClientTrace(ctx, &httptrace.ClientTrace{ + GotConn: func(connInfo httptrace.GotConnInfo) { + remoteAddr = connInfo.Conn.RemoteAddr() + localAddr = connInfo.Conn.LocalAddr() + gotConn.Close() + }, + }) + + method := "GET" // stream-down + if body != nil { + method = c.transportConfig.GetNormalizedUplinkHTTPMethod() // stream-up/one + } + req, _ := http.NewRequestWithContext(context.WithoutCancel(ctx), method, url, body) + c.transportConfig.FillStreamRequest(req, sessionId, "") + + wrc = &WaitReadCloser{Wait: make(chan struct{})} + go func() { + resp, err := c.client.Do(req) + if err != nil { + if !uploadOnly { // stream-down is enough + c.closed = true + errors.LogInfoInner(ctx, err, "failed to "+method+" "+url) + } + gotConn.Close() + wrc.Close() + return + } + if resp.StatusCode != 200 && !uploadOnly { + errors.LogInfo(ctx, "unexpected status ", resp.StatusCode) + } + if resp.StatusCode != 200 || uploadOnly { // stream-up + io.Copy(io.Discard, resp.Body) + resp.Body.Close() // if it is called immediately, the upload will be interrupted also + wrc.Close() + return + } + wrc.(*WaitReadCloser).Set(resp.Body) + }() + + <-gotConn.Wait() + return +} + +func (c *DefaultDialerClient) PostPacket(ctx context.Context, url string, sessionId string, seqStr string, payload buf.MultiBuffer) error { + method := c.transportConfig.GetNormalizedUplinkHTTPMethod() + req, err := http.NewRequestWithContext(context.WithoutCancel(ctx), method, url, nil) + if err != nil { + return err + } + c.transportConfig.FillPacketRequest(req, sessionId, seqStr, payload) + + if c.httpVersion != "1.1" { + resp, err := c.client.Do(req) + if err != nil { + c.closed = true + return err + } + + io.Copy(io.Discard, resp.Body) + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return errors.New("bad status code:", resp.Status) + } + } else { + // stringify the entire HTTP/1.1 request so it can be + // safely retried. if instead req.Write is called multiple + // times, the body is already drained after the first + // request + requestBuff := new(bytes.Buffer) + requestBuff.Grow(512 + int(req.ContentLength)) + common.Must(req.Write(requestBuff)) + + var uploadConn any + var h1UploadConn *H1Conn + + for { + uploadConn = c.uploadRawPool.Get() + newConnection := uploadConn == nil + if newConnection { + newConn, err := c.dialUploadConn(context.WithoutCancel(ctx)) + if err != nil { + return err + } + h1UploadConn = NewH1Conn(newConn) + uploadConn = h1UploadConn + } else { + h1UploadConn = uploadConn.(*H1Conn) + + // TODO: Replace 0 here with a config value later + // Or add some other condition for optimization purposes + if h1UploadConn.UnreadedResponsesCount > 0 { + resp, err := http.ReadResponse(h1UploadConn.RespBufReader, req) + if err != nil { + c.closed = true + return fmt.Errorf("error while reading response: %s", err.Error()) + } + io.Copy(io.Discard, resp.Body) + defer resp.Body.Close() + if resp.StatusCode != 200 { + return fmt.Errorf("got non-200 error response code: %d", resp.StatusCode) + } + } + } + + _, err := h1UploadConn.Write(requestBuff.Bytes()) + // if the write failed, we try another connection from + // the pool, until the write on a new connection fails. + // failed writes to a pooled connection are normal when + // the connection has been closed in the meantime. + if err == nil { + break + } else if newConnection { + return err + } + } + + c.uploadRawPool.Put(uploadConn) + } + + return nil +} + +type WaitReadCloser struct { + Wait chan struct{} + io.ReadCloser +} + +func (w *WaitReadCloser) Set(rc io.ReadCloser) { + w.ReadCloser = rc + defer func() { + if recover() != nil { + rc.Close() + } + }() + close(w.Wait) +} + +func (w *WaitReadCloser) Read(b []byte) (int, error) { + if w.ReadCloser == nil { + if <-w.Wait; w.ReadCloser == nil { + return 0, io.ErrClosedPipe + } + } + return w.ReadCloser.Read(b) +} + +func (w *WaitReadCloser) Close() error { + if w.ReadCloser != nil { + return w.ReadCloser.Close() + } + defer func() { + if recover() != nil && w.ReadCloser != nil { + w.ReadCloser.Close() + } + }() + close(w.Wait) + return nil +} diff --git a/subproject/Xray-core-main/transport/internet/splithttp/common.go b/subproject/Xray-core-main/transport/internet/splithttp/common.go new file mode 100644 index 00000000..d49a5afd --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/splithttp/common.go @@ -0,0 +1,11 @@ +package splithttp + +const ( + PlacementQueryInHeader = "queryInHeader" + PlacementCookie = "cookie" + PlacementHeader = "header" + PlacementQuery = "query" + PlacementPath = "path" + PlacementBody = "body" + PlacementAuto = "auto" +) diff --git a/subproject/Xray-core-main/transport/internet/splithttp/config.go b/subproject/Xray-core-main/transport/internet/splithttp/config.go new file mode 100644 index 00000000..61f861a3 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/splithttp/config.go @@ -0,0 +1,490 @@ +package splithttp + +import ( + "encoding/base64" + "fmt" + "io" + "net/http" + "strings" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/crypto" + "github.com/xtls/xray-core/common/utils" + "github.com/xtls/xray-core/transport/internet" +) + +func (c *Config) GetNormalizedPath() string { + pathAndQuery := strings.SplitN(c.Path, "?", 2) + path := pathAndQuery[0] + + if path == "" || path[0] != '/' { + path = "/" + path + } + + if path[len(path)-1] != '/' { + path = path + "/" + } + + return path +} + +func (c *Config) GetNormalizedQuery() string { + pathAndQuery := strings.SplitN(c.Path, "?", 2) + query := "" + + if len(pathAndQuery) > 1 { + query = pathAndQuery[1] + } + + /* + if query != "" { + query += "&" + } + query += "x_version=" + core.Version() + */ + + return query +} + +func (c *Config) GetRequestHeader() http.Header { + header := http.Header{} + for k, v := range c.Headers { + header.Add(k, v) + } + utils.TryDefaultHeadersWith(header, "fetch") + return header +} + +func (c *Config) GetRequestHeaderWithPayload(payload []byte) http.Header { + header := c.GetRequestHeader() + + key := c.UplinkDataKey + encodedData := base64.RawURLEncoding.EncodeToString(payload) + + for i := 0; len(encodedData) > 0; i++ { + chunkSize := min(int(c.GetNormalizedUplinkChunkSize().rand()), len(encodedData)) + chunk := encodedData[:chunkSize] + encodedData = encodedData[chunkSize:] + headerKey := fmt.Sprintf("%s-%d", key, i) + header.Set(headerKey, chunk) + } + + return header +} + +func (c *Config) GetRequestCookiesWithPayload(payload []byte) []*http.Cookie { + cookies := []*http.Cookie{} + + key := c.UplinkDataKey + encodedData := base64.RawURLEncoding.EncodeToString(payload) + + for i := 0; len(encodedData) > 0; i++ { + chunkSize := min(int(c.GetNormalizedUplinkChunkSize().rand()), len(encodedData)) + chunk := encodedData[:chunkSize] + encodedData = encodedData[chunkSize:] + cookieName := fmt.Sprintf("%s_%d", key, i) + cookies = append(cookies, &http.Cookie{Name: cookieName, Value: chunk}) + } + + return cookies +} + +func (c *Config) WriteResponseHeader(writer http.ResponseWriter, requestMethod string, requestHeader http.Header) { + // CORS headers for the browser dialer + if origin := requestHeader.Get("Origin"); origin == "" { + writer.Header().Set("Access-Control-Allow-Origin", "*") + } else { + // Chrome says: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'. + writer.Header().Set("Access-Control-Allow-Origin", origin) + } + + if c.GetNormalizedSessionPlacement() == PlacementCookie || + c.GetNormalizedSeqPlacement() == PlacementCookie || + c.XPaddingPlacement == PlacementCookie || + c.GetNormalizedUplinkDataPlacement() == PlacementCookie { + writer.Header().Set("Access-Control-Allow-Credentials", "true") + } + + if requestMethod == "OPTIONS" { + requestedMethod := requestHeader.Get("Access-Control-Request-Method") + if requestedMethod != "" { + writer.Header().Set("Access-Control-Allow-Methods", requestedMethod) + } else { + writer.Header().Set("Access-Control-Allow-Methods", "*") + } + + requestedHeaders := requestHeader.Get("Access-Control-Request-Headers") + if requestedHeaders == "" { + writer.Header().Set("Access-Control-Allow-Headers", "*") + } else { + writer.Header().Set("Access-Control-Allow-Headers", requestedHeaders) + } + } +} + +func (c *Config) GetNormalizedUplinkHTTPMethod() string { + if c.UplinkHTTPMethod == "" { + return "POST" + } + + return c.UplinkHTTPMethod +} + +func (c *Config) GetNormalizedScMaxEachPostBytes() RangeConfig { + if c.ScMaxEachPostBytes == nil || c.ScMaxEachPostBytes.To == 0 { + return RangeConfig{ + From: 1000000, + To: 1000000, + } + } + + return *c.ScMaxEachPostBytes +} + +func (c *Config) GetNormalizedScMinPostsIntervalMs() RangeConfig { + if c.ScMinPostsIntervalMs == nil || c.ScMinPostsIntervalMs.To == 0 { + return RangeConfig{ + From: 30, + To: 30, + } + } + + return *c.ScMinPostsIntervalMs +} + +func (c *Config) GetNormalizedScMaxBufferedPosts() int { + if c.ScMaxBufferedPosts == 0 { + return 30 + } + + return int(c.ScMaxBufferedPosts) +} + +func (c *Config) GetNormalizedScStreamUpServerSecs() RangeConfig { + if c.ScStreamUpServerSecs == nil || c.ScStreamUpServerSecs.To == 0 { + return RangeConfig{ + From: 20, + To: 80, + } + } + + return *c.ScStreamUpServerSecs +} + +func (c *Config) GetNormalizedUplinkChunkSize() RangeConfig { + if c.UplinkChunkSize == nil || c.UplinkChunkSize.To == 0 { + switch c.UplinkDataPlacement { + case PlacementCookie: + return RangeConfig{ + From: 2 * 1024, // 2 KiB + To: 3 * 1024, // 3 KiB + } + case PlacementHeader: + return RangeConfig{ + From: 3 * 1000, // 3 KB + To: 4 * 1000, // 4 KB + } + default: + return c.GetNormalizedScMaxEachPostBytes() + } + } else if c.UplinkChunkSize.From < 64 { + return RangeConfig{ + From: 64, + To: max(64, c.UplinkChunkSize.To), + } + } + + return *c.UplinkChunkSize +} + +func (c *Config) GetNormalizedServerMaxHeaderBytes() int { + if c.ServerMaxHeaderBytes <= 0 { + return 8192 + } else { + return int(c.ServerMaxHeaderBytes) + } +} + +func (c *Config) GetNormalizedSessionPlacement() string { + if c.SessionPlacement == "" { + return PlacementPath + } + return c.SessionPlacement +} + +func (c *Config) GetNormalizedSeqPlacement() string { + if c.SeqPlacement == "" { + return PlacementPath + } + return c.SeqPlacement +} + +func (c *Config) GetNormalizedUplinkDataPlacement() string { + if c.UplinkDataPlacement == "" { + return PlacementBody + } + return c.UplinkDataPlacement +} + +func (c *Config) GetNormalizedSessionKey() string { + if c.SessionKey != "" { + return c.SessionKey + } + switch c.GetNormalizedSessionPlacement() { + case PlacementHeader: + return "X-Session" + case PlacementCookie, PlacementQuery: + return "x_session" + default: + return "" + } +} + +func (c *Config) GetNormalizedSeqKey() string { + if c.SeqKey != "" { + return c.SeqKey + } + switch c.GetNormalizedSeqPlacement() { + case PlacementHeader: + return "X-Seq" + case PlacementCookie, PlacementQuery: + return "x_seq" + default: + return "" + } +} + +func (c *Config) ApplyMetaToRequest(req *http.Request, sessionId string, seqStr string) { + sessionPlacement := c.GetNormalizedSessionPlacement() + seqPlacement := c.GetNormalizedSeqPlacement() + sessionKey := c.GetNormalizedSessionKey() + seqKey := c.GetNormalizedSeqKey() + + if sessionId != "" { + switch sessionPlacement { + case PlacementPath: + req.URL.Path = appendToPath(req.URL.Path, sessionId) + case PlacementQuery: + q := req.URL.Query() + q.Set(sessionKey, sessionId) + req.URL.RawQuery = q.Encode() + case PlacementHeader: + req.Header.Set(sessionKey, sessionId) + case PlacementCookie: + req.AddCookie(&http.Cookie{Name: sessionKey, Value: sessionId}) + } + } + + if seqStr != "" { + switch seqPlacement { + case PlacementPath: + req.URL.Path = appendToPath(req.URL.Path, seqStr) + case PlacementQuery: + q := req.URL.Query() + q.Set(seqKey, seqStr) + req.URL.RawQuery = q.Encode() + case PlacementHeader: + req.Header.Set(seqKey, seqStr) + case PlacementCookie: + req.AddCookie(&http.Cookie{Name: seqKey, Value: seqStr}) + } + } +} + +func (c *Config) FillStreamRequest(request *http.Request, sessionId string, seqStr string) { + request.Header = c.GetRequestHeader() + length := int(c.GetNormalizedXPaddingBytes().rand()) + config := XPaddingConfig{Length: length} + + if c.XPaddingObfsMode { + config.Placement = XPaddingPlacement{ + Placement: c.XPaddingPlacement, + Key: c.XPaddingKey, + Header: c.XPaddingHeader, + RawURL: request.URL.String(), + } + config.Method = PaddingMethod(c.XPaddingMethod) + } else { + config.Placement = XPaddingPlacement{ + Placement: PlacementQueryInHeader, + Key: "x_padding", + Header: "Referer", + RawURL: request.URL.String(), + } + } + + c.ApplyXPaddingToRequest(request, config) + c.ApplyMetaToRequest(request, sessionId, "") + + if request.Body != nil && !c.NoGRPCHeader { // stream-up/one + request.Header.Set("Content-Type", "application/grpc") + } +} + +func (c *Config) FillPacketRequest(request *http.Request, sessionId string, seqStr string, payload buf.MultiBuffer) error { + dataPlacement := c.GetNormalizedUplinkDataPlacement() + + if dataPlacement == PlacementBody || dataPlacement == PlacementAuto { + request.Header = c.GetRequestHeader() + request.Body = io.NopCloser(&buf.MultiBufferContainer{MultiBuffer: payload}) + request.ContentLength = int64(payload.Len()) + } else { + data := make([]byte, payload.Len()) + payload.Copy(data) + buf.ReleaseMulti(payload) + switch dataPlacement { + case PlacementHeader: + request.Header = c.GetRequestHeaderWithPayload(data) + case PlacementCookie: + request.Header = c.GetRequestHeader() + for _, cookie := range c.GetRequestCookiesWithPayload(data) { + request.AddCookie(cookie) + } + } + } + + length := int(c.GetNormalizedXPaddingBytes().rand()) + config := XPaddingConfig{Length: length} + + if c.XPaddingObfsMode { + config.Placement = XPaddingPlacement{ + Placement: c.XPaddingPlacement, + Key: c.XPaddingKey, + Header: c.XPaddingHeader, + RawURL: request.URL.String(), + } + config.Method = PaddingMethod(c.XPaddingMethod) + } else { + config.Placement = XPaddingPlacement{ + Placement: PlacementQueryInHeader, + Key: "x_padding", + Header: "Referer", + RawURL: request.URL.String(), + } + } + + c.ApplyXPaddingToRequest(request, config) + c.ApplyMetaToRequest(request, sessionId, seqStr) + + return nil +} + +func (c *Config) ExtractMetaFromRequest(req *http.Request, path string) (sessionId string, seqStr string) { + sessionPlacement := c.GetNormalizedSessionPlacement() + seqPlacement := c.GetNormalizedSeqPlacement() + sessionKey := c.GetNormalizedSessionKey() + seqKey := c.GetNormalizedSeqKey() + + var subpath []string + pathPart := 0 + if sessionPlacement == PlacementPath || seqPlacement == PlacementPath { + subpath = strings.Split(req.URL.Path[len(path):], "/") + } + + switch sessionPlacement { + case PlacementPath: + if len(subpath) > pathPart { + sessionId = subpath[pathPart] + pathPart += 1 + } + case PlacementQuery: + sessionId = req.URL.Query().Get(sessionKey) + case PlacementHeader: + sessionId = req.Header.Get(sessionKey) + case PlacementCookie: + if cookie, e := req.Cookie(sessionKey); e == nil { + sessionId = cookie.Value + } + } + + switch seqPlacement { + case PlacementPath: + if len(subpath) > pathPart { + seqStr = subpath[pathPart] + pathPart += 1 + } + case PlacementQuery: + seqStr = req.URL.Query().Get(seqKey) + case PlacementHeader: + seqStr = req.Header.Get(seqKey) + case PlacementCookie: + if cookie, e := req.Cookie(seqKey); e == nil { + seqStr = cookie.Value + } + } + + return sessionId, seqStr +} + +func (m *XmuxConfig) GetNormalizedMaxConcurrency() RangeConfig { + if m.MaxConcurrency == nil { + return RangeConfig{ + From: 0, + To: 0, + } + } + + return *m.MaxConcurrency +} + +func (m *XmuxConfig) GetNormalizedMaxConnections() RangeConfig { + if m.MaxConnections == nil { + return RangeConfig{ + From: 0, + To: 0, + } + } + + return *m.MaxConnections +} + +func (m *XmuxConfig) GetNormalizedCMaxReuseTimes() RangeConfig { + if m.CMaxReuseTimes == nil { + return RangeConfig{ + From: 0, + To: 0, + } + } + + return *m.CMaxReuseTimes +} + +func (m *XmuxConfig) GetNormalizedHMaxRequestTimes() RangeConfig { + if m.HMaxRequestTimes == nil { + return RangeConfig{ + From: 0, + To: 0, + } + } + + return *m.HMaxRequestTimes +} + +func (m *XmuxConfig) GetNormalizedHMaxReusableSecs() RangeConfig { + if m.HMaxReusableSecs == nil { + return RangeConfig{ + From: 0, + To: 0, + } + } + + return *m.HMaxReusableSecs +} + +func init() { + common.Must(internet.RegisterProtocolConfigCreator(protocolName, func() interface{} { + return new(Config) + })) +} + +func (c RangeConfig) rand() int32 { + return int32(crypto.RandBetween(int64(c.From), int64(c.To))) +} + +func appendToPath(path, value string) string { + if strings.HasSuffix(path, "/") { + return path + value + } + return path + "/" + value +} diff --git a/subproject/Xray-core-main/transport/internet/splithttp/config.pb.go b/subproject/Xray-core-main/transport/internet/splithttp/config.pb.go new file mode 100644 index 00000000..4e99d8a8 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/splithttp/config.pb.go @@ -0,0 +1,528 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: transport/internet/splithttp/config.proto + +package splithttp + +import ( + internet "github.com/xtls/xray-core/transport/internet" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type RangeConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + From int32 `protobuf:"varint,1,opt,name=from,proto3" json:"from,omitempty"` + To int32 `protobuf:"varint,2,opt,name=to,proto3" json:"to,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RangeConfig) Reset() { + *x = RangeConfig{} + mi := &file_transport_internet_splithttp_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RangeConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RangeConfig) ProtoMessage() {} + +func (x *RangeConfig) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_splithttp_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RangeConfig.ProtoReflect.Descriptor instead. +func (*RangeConfig) Descriptor() ([]byte, []int) { + return file_transport_internet_splithttp_config_proto_rawDescGZIP(), []int{0} +} + +func (x *RangeConfig) GetFrom() int32 { + if x != nil { + return x.From + } + return 0 +} + +func (x *RangeConfig) GetTo() int32 { + if x != nil { + return x.To + } + return 0 +} + +type XmuxConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + MaxConcurrency *RangeConfig `protobuf:"bytes,1,opt,name=maxConcurrency,proto3" json:"maxConcurrency,omitempty"` + MaxConnections *RangeConfig `protobuf:"bytes,2,opt,name=maxConnections,proto3" json:"maxConnections,omitempty"` + CMaxReuseTimes *RangeConfig `protobuf:"bytes,3,opt,name=cMaxReuseTimes,proto3" json:"cMaxReuseTimes,omitempty"` + HMaxRequestTimes *RangeConfig `protobuf:"bytes,4,opt,name=hMaxRequestTimes,proto3" json:"hMaxRequestTimes,omitempty"` + HMaxReusableSecs *RangeConfig `protobuf:"bytes,5,opt,name=hMaxReusableSecs,proto3" json:"hMaxReusableSecs,omitempty"` + HKeepAlivePeriod int64 `protobuf:"varint,6,opt,name=hKeepAlivePeriod,proto3" json:"hKeepAlivePeriod,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *XmuxConfig) Reset() { + *x = XmuxConfig{} + mi := &file_transport_internet_splithttp_config_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *XmuxConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*XmuxConfig) ProtoMessage() {} + +func (x *XmuxConfig) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_splithttp_config_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use XmuxConfig.ProtoReflect.Descriptor instead. +func (*XmuxConfig) Descriptor() ([]byte, []int) { + return file_transport_internet_splithttp_config_proto_rawDescGZIP(), []int{1} +} + +func (x *XmuxConfig) GetMaxConcurrency() *RangeConfig { + if x != nil { + return x.MaxConcurrency + } + return nil +} + +func (x *XmuxConfig) GetMaxConnections() *RangeConfig { + if x != nil { + return x.MaxConnections + } + return nil +} + +func (x *XmuxConfig) GetCMaxReuseTimes() *RangeConfig { + if x != nil { + return x.CMaxReuseTimes + } + return nil +} + +func (x *XmuxConfig) GetHMaxRequestTimes() *RangeConfig { + if x != nil { + return x.HMaxRequestTimes + } + return nil +} + +func (x *XmuxConfig) GetHMaxReusableSecs() *RangeConfig { + if x != nil { + return x.HMaxReusableSecs + } + return nil +} + +func (x *XmuxConfig) GetHKeepAlivePeriod() int64 { + if x != nil { + return x.HKeepAlivePeriod + } + return 0 +} + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + Host string `protobuf:"bytes,1,opt,name=host,proto3" json:"host,omitempty"` + Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` + Mode string `protobuf:"bytes,3,opt,name=mode,proto3" json:"mode,omitempty"` + Headers map[string]string `protobuf:"bytes,4,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + XPaddingBytes *RangeConfig `protobuf:"bytes,5,opt,name=xPaddingBytes,proto3" json:"xPaddingBytes,omitempty"` + NoGRPCHeader bool `protobuf:"varint,6,opt,name=noGRPCHeader,proto3" json:"noGRPCHeader,omitempty"` + NoSSEHeader bool `protobuf:"varint,7,opt,name=noSSEHeader,proto3" json:"noSSEHeader,omitempty"` + ScMaxEachPostBytes *RangeConfig `protobuf:"bytes,8,opt,name=scMaxEachPostBytes,proto3" json:"scMaxEachPostBytes,omitempty"` + ScMinPostsIntervalMs *RangeConfig `protobuf:"bytes,9,opt,name=scMinPostsIntervalMs,proto3" json:"scMinPostsIntervalMs,omitempty"` + ScMaxBufferedPosts int64 `protobuf:"varint,10,opt,name=scMaxBufferedPosts,proto3" json:"scMaxBufferedPosts,omitempty"` + ScStreamUpServerSecs *RangeConfig `protobuf:"bytes,11,opt,name=scStreamUpServerSecs,proto3" json:"scStreamUpServerSecs,omitempty"` + Xmux *XmuxConfig `protobuf:"bytes,12,opt,name=xmux,proto3" json:"xmux,omitempty"` + DownloadSettings *internet.StreamConfig `protobuf:"bytes,13,opt,name=downloadSettings,proto3" json:"downloadSettings,omitempty"` + XPaddingObfsMode bool `protobuf:"varint,14,opt,name=xPaddingObfsMode,proto3" json:"xPaddingObfsMode,omitempty"` + XPaddingKey string `protobuf:"bytes,15,opt,name=xPaddingKey,proto3" json:"xPaddingKey,omitempty"` + XPaddingHeader string `protobuf:"bytes,16,opt,name=xPaddingHeader,proto3" json:"xPaddingHeader,omitempty"` + XPaddingPlacement string `protobuf:"bytes,17,opt,name=xPaddingPlacement,proto3" json:"xPaddingPlacement,omitempty"` + XPaddingMethod string `protobuf:"bytes,18,opt,name=xPaddingMethod,proto3" json:"xPaddingMethod,omitempty"` + UplinkHTTPMethod string `protobuf:"bytes,19,opt,name=uplinkHTTPMethod,proto3" json:"uplinkHTTPMethod,omitempty"` + SessionPlacement string `protobuf:"bytes,20,opt,name=sessionPlacement,proto3" json:"sessionPlacement,omitempty"` + SessionKey string `protobuf:"bytes,21,opt,name=sessionKey,proto3" json:"sessionKey,omitempty"` + SeqPlacement string `protobuf:"bytes,22,opt,name=seqPlacement,proto3" json:"seqPlacement,omitempty"` + SeqKey string `protobuf:"bytes,23,opt,name=seqKey,proto3" json:"seqKey,omitempty"` + UplinkDataPlacement string `protobuf:"bytes,24,opt,name=uplinkDataPlacement,proto3" json:"uplinkDataPlacement,omitempty"` + UplinkDataKey string `protobuf:"bytes,25,opt,name=uplinkDataKey,proto3" json:"uplinkDataKey,omitempty"` + UplinkChunkSize *RangeConfig `protobuf:"bytes,26,opt,name=uplinkChunkSize,proto3" json:"uplinkChunkSize,omitempty"` + ServerMaxHeaderBytes int32 `protobuf:"varint,27,opt,name=serverMaxHeaderBytes,proto3" json:"serverMaxHeaderBytes,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_transport_internet_splithttp_config_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_splithttp_config_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_transport_internet_splithttp_config_proto_rawDescGZIP(), []int{2} +} + +func (x *Config) GetHost() string { + if x != nil { + return x.Host + } + return "" +} + +func (x *Config) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *Config) GetMode() string { + if x != nil { + return x.Mode + } + return "" +} + +func (x *Config) GetHeaders() map[string]string { + if x != nil { + return x.Headers + } + return nil +} + +func (x *Config) GetXPaddingBytes() *RangeConfig { + if x != nil { + return x.XPaddingBytes + } + return nil +} + +func (x *Config) GetNoGRPCHeader() bool { + if x != nil { + return x.NoGRPCHeader + } + return false +} + +func (x *Config) GetNoSSEHeader() bool { + if x != nil { + return x.NoSSEHeader + } + return false +} + +func (x *Config) GetScMaxEachPostBytes() *RangeConfig { + if x != nil { + return x.ScMaxEachPostBytes + } + return nil +} + +func (x *Config) GetScMinPostsIntervalMs() *RangeConfig { + if x != nil { + return x.ScMinPostsIntervalMs + } + return nil +} + +func (x *Config) GetScMaxBufferedPosts() int64 { + if x != nil { + return x.ScMaxBufferedPosts + } + return 0 +} + +func (x *Config) GetScStreamUpServerSecs() *RangeConfig { + if x != nil { + return x.ScStreamUpServerSecs + } + return nil +} + +func (x *Config) GetXmux() *XmuxConfig { + if x != nil { + return x.Xmux + } + return nil +} + +func (x *Config) GetDownloadSettings() *internet.StreamConfig { + if x != nil { + return x.DownloadSettings + } + return nil +} + +func (x *Config) GetXPaddingObfsMode() bool { + if x != nil { + return x.XPaddingObfsMode + } + return false +} + +func (x *Config) GetXPaddingKey() string { + if x != nil { + return x.XPaddingKey + } + return "" +} + +func (x *Config) GetXPaddingHeader() string { + if x != nil { + return x.XPaddingHeader + } + return "" +} + +func (x *Config) GetXPaddingPlacement() string { + if x != nil { + return x.XPaddingPlacement + } + return "" +} + +func (x *Config) GetXPaddingMethod() string { + if x != nil { + return x.XPaddingMethod + } + return "" +} + +func (x *Config) GetUplinkHTTPMethod() string { + if x != nil { + return x.UplinkHTTPMethod + } + return "" +} + +func (x *Config) GetSessionPlacement() string { + if x != nil { + return x.SessionPlacement + } + return "" +} + +func (x *Config) GetSessionKey() string { + if x != nil { + return x.SessionKey + } + return "" +} + +func (x *Config) GetSeqPlacement() string { + if x != nil { + return x.SeqPlacement + } + return "" +} + +func (x *Config) GetSeqKey() string { + if x != nil { + return x.SeqKey + } + return "" +} + +func (x *Config) GetUplinkDataPlacement() string { + if x != nil { + return x.UplinkDataPlacement + } + return "" +} + +func (x *Config) GetUplinkDataKey() string { + if x != nil { + return x.UplinkDataKey + } + return "" +} + +func (x *Config) GetUplinkChunkSize() *RangeConfig { + if x != nil { + return x.UplinkChunkSize + } + return nil +} + +func (x *Config) GetServerMaxHeaderBytes() int32 { + if x != nil { + return x.ServerMaxHeaderBytes + } + return 0 +} + +var File_transport_internet_splithttp_config_proto protoreflect.FileDescriptor + +const file_transport_internet_splithttp_config_proto_rawDesc = "" + + "\n" + + ")transport/internet/splithttp/config.proto\x12!xray.transport.internet.splithttp\x1a\x1ftransport/internet/config.proto\"1\n" + + "\vRangeConfig\x12\x12\n" + + "\x04from\x18\x01 \x01(\x05R\x04from\x12\x0e\n" + + "\x02to\x18\x02 \x01(\x05R\x02to\"\xf8\x03\n" + + "\n" + + "XmuxConfig\x12V\n" + + "\x0emaxConcurrency\x18\x01 \x01(\v2..xray.transport.internet.splithttp.RangeConfigR\x0emaxConcurrency\x12V\n" + + "\x0emaxConnections\x18\x02 \x01(\v2..xray.transport.internet.splithttp.RangeConfigR\x0emaxConnections\x12V\n" + + "\x0ecMaxReuseTimes\x18\x03 \x01(\v2..xray.transport.internet.splithttp.RangeConfigR\x0ecMaxReuseTimes\x12Z\n" + + "\x10hMaxRequestTimes\x18\x04 \x01(\v2..xray.transport.internet.splithttp.RangeConfigR\x10hMaxRequestTimes\x12Z\n" + + "\x10hMaxReusableSecs\x18\x05 \x01(\v2..xray.transport.internet.splithttp.RangeConfigR\x10hMaxReusableSecs\x12*\n" + + "\x10hKeepAlivePeriod\x18\x06 \x01(\x03R\x10hKeepAlivePeriod\"\xc2\v\n" + + "\x06Config\x12\x12\n" + + "\x04host\x18\x01 \x01(\tR\x04host\x12\x12\n" + + "\x04path\x18\x02 \x01(\tR\x04path\x12\x12\n" + + "\x04mode\x18\x03 \x01(\tR\x04mode\x12P\n" + + "\aheaders\x18\x04 \x03(\v26.xray.transport.internet.splithttp.Config.HeadersEntryR\aheaders\x12T\n" + + "\rxPaddingBytes\x18\x05 \x01(\v2..xray.transport.internet.splithttp.RangeConfigR\rxPaddingBytes\x12\"\n" + + "\fnoGRPCHeader\x18\x06 \x01(\bR\fnoGRPCHeader\x12 \n" + + "\vnoSSEHeader\x18\a \x01(\bR\vnoSSEHeader\x12^\n" + + "\x12scMaxEachPostBytes\x18\b \x01(\v2..xray.transport.internet.splithttp.RangeConfigR\x12scMaxEachPostBytes\x12b\n" + + "\x14scMinPostsIntervalMs\x18\t \x01(\v2..xray.transport.internet.splithttp.RangeConfigR\x14scMinPostsIntervalMs\x12.\n" + + "\x12scMaxBufferedPosts\x18\n" + + " \x01(\x03R\x12scMaxBufferedPosts\x12b\n" + + "\x14scStreamUpServerSecs\x18\v \x01(\v2..xray.transport.internet.splithttp.RangeConfigR\x14scStreamUpServerSecs\x12A\n" + + "\x04xmux\x18\f \x01(\v2-.xray.transport.internet.splithttp.XmuxConfigR\x04xmux\x12Q\n" + + "\x10downloadSettings\x18\r \x01(\v2%.xray.transport.internet.StreamConfigR\x10downloadSettings\x12*\n" + + "\x10xPaddingObfsMode\x18\x0e \x01(\bR\x10xPaddingObfsMode\x12 \n" + + "\vxPaddingKey\x18\x0f \x01(\tR\vxPaddingKey\x12&\n" + + "\x0exPaddingHeader\x18\x10 \x01(\tR\x0exPaddingHeader\x12,\n" + + "\x11xPaddingPlacement\x18\x11 \x01(\tR\x11xPaddingPlacement\x12&\n" + + "\x0exPaddingMethod\x18\x12 \x01(\tR\x0exPaddingMethod\x12*\n" + + "\x10uplinkHTTPMethod\x18\x13 \x01(\tR\x10uplinkHTTPMethod\x12*\n" + + "\x10sessionPlacement\x18\x14 \x01(\tR\x10sessionPlacement\x12\x1e\n" + + "\n" + + "sessionKey\x18\x15 \x01(\tR\n" + + "sessionKey\x12\"\n" + + "\fseqPlacement\x18\x16 \x01(\tR\fseqPlacement\x12\x16\n" + + "\x06seqKey\x18\x17 \x01(\tR\x06seqKey\x120\n" + + "\x13uplinkDataPlacement\x18\x18 \x01(\tR\x13uplinkDataPlacement\x12$\n" + + "\ruplinkDataKey\x18\x19 \x01(\tR\ruplinkDataKey\x12X\n" + + "\x0fuplinkChunkSize\x18\x1a \x01(\v2..xray.transport.internet.splithttp.RangeConfigR\x0fuplinkChunkSize\x122\n" + + "\x14serverMaxHeaderBytes\x18\x1b \x01(\x05R\x14serverMaxHeaderBytes\x1a:\n" + + "\fHeadersEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\x85\x01\n" + + "%com.xray.transport.internet.splithttpP\x01Z6github.com/xtls/xray-core/transport/internet/splithttp\xaa\x02!Xray.Transport.Internet.SplitHttpb\x06proto3" + +var ( + file_transport_internet_splithttp_config_proto_rawDescOnce sync.Once + file_transport_internet_splithttp_config_proto_rawDescData []byte +) + +func file_transport_internet_splithttp_config_proto_rawDescGZIP() []byte { + file_transport_internet_splithttp_config_proto_rawDescOnce.Do(func() { + file_transport_internet_splithttp_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_transport_internet_splithttp_config_proto_rawDesc), len(file_transport_internet_splithttp_config_proto_rawDesc))) + }) + return file_transport_internet_splithttp_config_proto_rawDescData +} + +var file_transport_internet_splithttp_config_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_transport_internet_splithttp_config_proto_goTypes = []any{ + (*RangeConfig)(nil), // 0: xray.transport.internet.splithttp.RangeConfig + (*XmuxConfig)(nil), // 1: xray.transport.internet.splithttp.XmuxConfig + (*Config)(nil), // 2: xray.transport.internet.splithttp.Config + nil, // 3: xray.transport.internet.splithttp.Config.HeadersEntry + (*internet.StreamConfig)(nil), // 4: xray.transport.internet.StreamConfig +} +var file_transport_internet_splithttp_config_proto_depIdxs = []int32{ + 0, // 0: xray.transport.internet.splithttp.XmuxConfig.maxConcurrency:type_name -> xray.transport.internet.splithttp.RangeConfig + 0, // 1: xray.transport.internet.splithttp.XmuxConfig.maxConnections:type_name -> xray.transport.internet.splithttp.RangeConfig + 0, // 2: xray.transport.internet.splithttp.XmuxConfig.cMaxReuseTimes:type_name -> xray.transport.internet.splithttp.RangeConfig + 0, // 3: xray.transport.internet.splithttp.XmuxConfig.hMaxRequestTimes:type_name -> xray.transport.internet.splithttp.RangeConfig + 0, // 4: xray.transport.internet.splithttp.XmuxConfig.hMaxReusableSecs:type_name -> xray.transport.internet.splithttp.RangeConfig + 3, // 5: xray.transport.internet.splithttp.Config.headers:type_name -> xray.transport.internet.splithttp.Config.HeadersEntry + 0, // 6: xray.transport.internet.splithttp.Config.xPaddingBytes:type_name -> xray.transport.internet.splithttp.RangeConfig + 0, // 7: xray.transport.internet.splithttp.Config.scMaxEachPostBytes:type_name -> xray.transport.internet.splithttp.RangeConfig + 0, // 8: xray.transport.internet.splithttp.Config.scMinPostsIntervalMs:type_name -> xray.transport.internet.splithttp.RangeConfig + 0, // 9: xray.transport.internet.splithttp.Config.scStreamUpServerSecs:type_name -> xray.transport.internet.splithttp.RangeConfig + 1, // 10: xray.transport.internet.splithttp.Config.xmux:type_name -> xray.transport.internet.splithttp.XmuxConfig + 4, // 11: xray.transport.internet.splithttp.Config.downloadSettings:type_name -> xray.transport.internet.StreamConfig + 0, // 12: xray.transport.internet.splithttp.Config.uplinkChunkSize:type_name -> xray.transport.internet.splithttp.RangeConfig + 13, // [13:13] is the sub-list for method output_type + 13, // [13:13] is the sub-list for method input_type + 13, // [13:13] is the sub-list for extension type_name + 13, // [13:13] is the sub-list for extension extendee + 0, // [0:13] is the sub-list for field type_name +} + +func init() { file_transport_internet_splithttp_config_proto_init() } +func file_transport_internet_splithttp_config_proto_init() { + if File_transport_internet_splithttp_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_splithttp_config_proto_rawDesc), len(file_transport_internet_splithttp_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 4, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_transport_internet_splithttp_config_proto_goTypes, + DependencyIndexes: file_transport_internet_splithttp_config_proto_depIdxs, + MessageInfos: file_transport_internet_splithttp_config_proto_msgTypes, + }.Build() + File_transport_internet_splithttp_config_proto = out.File + file_transport_internet_splithttp_config_proto_goTypes = nil + file_transport_internet_splithttp_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/transport/internet/splithttp/config.proto b/subproject/Xray-core-main/transport/internet/splithttp/config.proto new file mode 100644 index 00000000..4c303d29 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/splithttp/config.proto @@ -0,0 +1,53 @@ +syntax = "proto3"; + +package xray.transport.internet.splithttp; +option csharp_namespace = "Xray.Transport.Internet.SplitHttp"; +option go_package = "github.com/xtls/xray-core/transport/internet/splithttp"; +option java_package = "com.xray.transport.internet.splithttp"; +option java_multiple_files = true; + +import "transport/internet/config.proto"; + +message RangeConfig { + int32 from = 1; + int32 to = 2; +} + +message XmuxConfig { + RangeConfig maxConcurrency = 1; + RangeConfig maxConnections = 2; + RangeConfig cMaxReuseTimes = 3; + RangeConfig hMaxRequestTimes = 4; + RangeConfig hMaxReusableSecs = 5; + int64 hKeepAlivePeriod = 6; +} + +message Config { + string host = 1; + string path = 2; + string mode = 3; + map headers = 4; + RangeConfig xPaddingBytes = 5; + bool noGRPCHeader = 6; + bool noSSEHeader = 7; + RangeConfig scMaxEachPostBytes = 8; + RangeConfig scMinPostsIntervalMs = 9; + int64 scMaxBufferedPosts = 10; + RangeConfig scStreamUpServerSecs = 11; + XmuxConfig xmux = 12; + xray.transport.internet.StreamConfig downloadSettings = 13; + bool xPaddingObfsMode = 14; + string xPaddingKey = 15; + string xPaddingHeader = 16; + string xPaddingPlacement = 17; + string xPaddingMethod = 18; + string uplinkHTTPMethod = 19; + string sessionPlacement = 20; + string sessionKey = 21; + string seqPlacement = 22; + string seqKey = 23; + string uplinkDataPlacement = 24; + string uplinkDataKey = 25; + RangeConfig uplinkChunkSize = 26; + int32 serverMaxHeaderBytes = 27; +} diff --git a/subproject/Xray-core-main/transport/internet/splithttp/config_test.go b/subproject/Xray-core-main/transport/internet/splithttp/config_test.go new file mode 100644 index 00000000..39c3fd95 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/splithttp/config_test.go @@ -0,0 +1,18 @@ +package splithttp_test + +import ( + "testing" + + . "github.com/xtls/xray-core/transport/internet/splithttp" +) + +func Test_GetNormalizedPath(t *testing.T) { + c := Config{ + Path: "/?world", + } + + path := c.GetNormalizedPath() + if path != "/" { + t.Error("Unexpected: ", path) + } +} diff --git a/subproject/Xray-core-main/transport/internet/splithttp/connection.go b/subproject/Xray-core-main/transport/internet/splithttp/connection.go new file mode 100644 index 00000000..613e1f36 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/splithttp/connection.go @@ -0,0 +1,64 @@ +package splithttp + +import ( + "io" + "net" + "time" +) + +type splitConn struct { + writer io.WriteCloser + reader io.ReadCloser + remoteAddr net.Addr + localAddr net.Addr + onClose func() +} + +func (c *splitConn) Write(b []byte) (int, error) { + return c.writer.Write(b) +} + +func (c *splitConn) Read(b []byte) (int, error) { + return c.reader.Read(b) +} + +func (c *splitConn) Close() error { + if c.onClose != nil { + c.onClose() + } + + err := c.writer.Close() + err2 := c.reader.Close() + if err != nil { + return err + } + + if err2 != nil { + return err + } + + return nil +} + +func (c *splitConn) LocalAddr() net.Addr { + return c.localAddr +} + +func (c *splitConn) RemoteAddr() net.Addr { + return c.remoteAddr +} + +func (c *splitConn) SetDeadline(t time.Time) error { + // TODO cannot do anything useful + return nil +} + +func (c *splitConn) SetReadDeadline(t time.Time) error { + // TODO cannot do anything useful + return nil +} + +func (c *splitConn) SetWriteDeadline(t time.Time) error { + // TODO cannot do anything useful + return nil +} diff --git a/subproject/Xray-core-main/transport/internet/splithttp/dialer.go b/subproject/Xray-core-main/transport/internet/splithttp/dialer.go new file mode 100644 index 00000000..6f4ec1d8 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/splithttp/dialer.go @@ -0,0 +1,618 @@ +package splithttp + +import ( + "context" + gotls "crypto/tls" + "fmt" + "io" + "math/rand" + "net/http" + "net/http/httptrace" + "net/url" + reflect "reflect" + "strconv" + "sync" + "sync/atomic" + "time" + + "github.com/apernet/quic-go" + "github.com/apernet/quic-go/http3" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/signal/done" + "github.com/xtls/xray-core/common/uuid" + "github.com/xtls/xray-core/transport/internet" + "github.com/xtls/xray-core/transport/internet/browser_dialer" + "github.com/xtls/xray-core/transport/internet/hysteria/congestion" + "github.com/xtls/xray-core/transport/internet/hysteria/congestion/bbr" + "github.com/xtls/xray-core/transport/internet/hysteria/udphop" + "github.com/xtls/xray-core/transport/internet/reality" + "github.com/xtls/xray-core/transport/internet/stat" + "github.com/xtls/xray-core/transport/internet/tls" + "github.com/xtls/xray-core/transport/pipe" + "golang.org/x/net/http2" +) + +type dialerConf struct { + net.Destination + *internet.MemoryStreamConfig +} + +var ( + globalDialerMap map[dialerConf]*XmuxManager + globalDialerAccess sync.Mutex +) + +func getHTTPClient(ctx context.Context, dest net.Destination, streamSettings *internet.MemoryStreamConfig) (DialerClient, *XmuxClient) { + realityConfig := reality.ConfigFromStreamSettings(streamSettings) + + if browser_dialer.HasBrowserDialer() && realityConfig == nil { + return &BrowserDialerClient{transportConfig: streamSettings.ProtocolSettings.(*Config)}, nil + } + + globalDialerAccess.Lock() + defer globalDialerAccess.Unlock() + + if globalDialerMap == nil { + globalDialerMap = make(map[dialerConf]*XmuxManager) + } + + key := dialerConf{dest, streamSettings} + + xmuxManager, found := globalDialerMap[key] + + if !found { + transportConfig := streamSettings.ProtocolSettings.(*Config) + var xmuxConfig XmuxConfig + if transportConfig.Xmux != nil { + xmuxConfig = *transportConfig.Xmux + } + + xmuxManager = NewXmuxManager(xmuxConfig, func() XmuxConn { + return createHTTPClient(dest, streamSettings) + }) + globalDialerMap[key] = xmuxManager + } + + xmuxClient := xmuxManager.GetXmuxClient(ctx) + return xmuxClient.XmuxConn.(DialerClient), xmuxClient +} + +func decideHTTPVersion(tlsConfig *tls.Config, realityConfig *reality.Config) string { + if realityConfig != nil { + return "2" + } + if tlsConfig == nil { + return "1.1" + } + if len(tlsConfig.NextProtocol) != 1 { + return "2" + } + if tlsConfig.NextProtocol[0] == "http/1.1" { + return "1.1" + } + if tlsConfig.NextProtocol[0] == "h3" { + return "3" + } + return "2" +} + +func createHTTPClient(dest net.Destination, streamSettings *internet.MemoryStreamConfig) DialerClient { + tlsConfig := tls.ConfigFromStreamSettings(streamSettings) + realityConfig := reality.ConfigFromStreamSettings(streamSettings) + + httpVersion := decideHTTPVersion(tlsConfig, realityConfig) + if httpVersion == "3" { + dest.Network = net.Network_UDP // better to keep this line + } + + var gotlsConfig *gotls.Config + + if tlsConfig != nil { + gotlsConfig = tlsConfig.GetTLSConfig(tls.WithDestination(dest)) + } + + transportConfig := streamSettings.ProtocolSettings.(*Config) + + dialContext := func(ctxInner context.Context) (net.Conn, error) { + conn, err := internet.DialSystem(ctxInner, dest, streamSettings.SocketSettings) + if err != nil { + return nil, err + } + + if streamSettings.TcpmaskManager != nil { + newConn, err := streamSettings.TcpmaskManager.WrapConnClient(conn) + if err != nil { + conn.Close() + return nil, errors.New("mask err").Base(err) + } + conn = newConn + } + + if realityConfig != nil { + return reality.UClient(conn, realityConfig, ctxInner, dest) + } + + if gotlsConfig != nil { + if fingerprint := tls.GetFingerprint(tlsConfig.Fingerprint); fingerprint != nil { + conn = tls.UClient(conn, gotlsConfig, fingerprint) + if err := conn.(*tls.UConn).HandshakeContext(ctxInner); err != nil { + return nil, err + } + } else { + conn = tls.Client(conn, gotlsConfig) + } + } + + return conn, nil + } + + var keepAlivePeriod time.Duration + if streamSettings.ProtocolSettings.(*Config).Xmux != nil { + keepAlivePeriod = time.Duration(streamSettings.ProtocolSettings.(*Config).Xmux.HKeepAlivePeriod) * time.Second + } + + var transport http.RoundTripper + + if httpVersion == "3" { + quicParams := streamSettings.QuicParams + if quicParams == nil { + quicParams = &internet.QuicParams{ + BbrProfile: string(bbr.ProfileStandard), + UdpHop: &internet.UdpHop{}, + } + } + + quicConfig := &quic.Config{ + InitialStreamReceiveWindow: quicParams.InitStreamReceiveWindow, + MaxStreamReceiveWindow: quicParams.MaxStreamReceiveWindow, + InitialConnectionReceiveWindow: quicParams.InitConnReceiveWindow, + MaxConnectionReceiveWindow: quicParams.MaxConnReceiveWindow, + MaxIdleTimeout: time.Duration(quicParams.MaxIdleTimeout) * time.Second, + KeepAlivePeriod: time.Duration(quicParams.KeepAlivePeriod) * time.Second, + MaxIncomingStreams: quicParams.MaxIncomingStreams, + DisablePathMTUDiscovery: quicParams.DisablePathMtuDiscovery, + } + if quicParams.MaxIdleTimeout == 0 { + quicConfig.MaxIdleTimeout = net.ConnIdleTimeout + } + if quicParams.KeepAlivePeriod == 0 { + if keepAlivePeriod == 0 { + quicConfig.KeepAlivePeriod = net.QuicgoH3KeepAlivePeriod + } + } + if quicParams.MaxIncomingStreams == 0 { + // these two are defaults of quic-go/http3. the default of quic-go (no + // http3) is different, so it is hardcoded here for clarity. + // https://github.com/quic-go/quic-go/blob/b8ea5c798155950fb5bbfdd06cad1939c9355878/http3/client.go#L36-L39 + quicConfig.MaxIncomingStreams = -1 + } + + transport = &http3.Transport{ + QUICConfig: quicConfig, + TLSClientConfig: gotlsConfig, + Dial: func(ctx context.Context, addr string, tlsCfg *gotls.Config, cfg *quic.Config) (*quic.Conn, error) { + udphopDialer := func(addr *net.UDPAddr) (net.PacketConn, error) { + conn, err := internet.DialSystem(ctx, net.UDPDestination(net.IPAddress(addr.IP), net.Port(addr.Port)), streamSettings.SocketSettings) + if err != nil { + errors.LogDebug(context.Background(), "skip hop: failed to dial to dest") + conn.Close() + return nil, errors.New() + } + + var udpConn net.PacketConn + + switch c := conn.(type) { + case *internet.PacketConnWrapper: + udpConn = c.PacketConn + case *net.UDPConn: + udpConn = c + default: + errors.LogDebug(context.Background(), "skip hop: udphop requires being at the outermost level ", reflect.TypeOf(c)) + conn.Close() + return nil, errors.New() + } + + return udpConn, nil + } + + var index int + if len(quicParams.UdpHop.Ports) > 0 { + index = rand.Intn(len(quicParams.UdpHop.Ports)) + dest.Port = net.Port(quicParams.UdpHop.Ports[index]) + } + + conn, err := internet.DialSystem(ctx, dest, streamSettings.SocketSettings) + if err != nil { + return nil, err + } + + var udpConn net.PacketConn + var udpAddr *net.UDPAddr + + switch c := conn.(type) { + case *internet.PacketConnWrapper: + udpConn = c.PacketConn + udpAddr, err = net.ResolveUDPAddr("udp", c.Dest.String()) + if err != nil { + conn.Close() + return nil, err + } + case *net.UDPConn: + udpConn = c + udpAddr, err = net.ResolveUDPAddr("udp", c.RemoteAddr().String()) + if err != nil { + conn.Close() + return nil, err + } + default: + udpConn = &internet.FakePacketConn{Conn: c} + udpAddr, err = net.ResolveUDPAddr("udp", c.RemoteAddr().String()) + if err != nil { + conn.Close() + return nil, err + } + + if len(quicParams.UdpHop.Ports) > 0 { + conn.Close() + return nil, errors.New("udphop requires being at the outermost level ", reflect.TypeOf(c)) + } + } + + if len(quicParams.UdpHop.Ports) > 0 { + addr := &udphop.UDPHopAddr{ + IP: udpAddr.IP, + Ports: quicParams.UdpHop.Ports, + } + udpConn, err = udphop.NewUDPHopPacketConn(addr, index, quicParams.UdpHop.IntervalMin, quicParams.UdpHop.IntervalMax, udphopDialer, udpConn) + if err != nil { + conn.Close() + return nil, errors.New("udphop err").Base(err) + } + } + + if streamSettings.UdpmaskManager != nil { + udpConn, err = streamSettings.UdpmaskManager.WrapPacketConnClient(udpConn) + if err != nil { + conn.Close() + return nil, errors.New("mask err").Base(err) + } + } + + quicConn, err := quic.DialEarly(ctx, udpConn, udpAddr, tlsCfg, cfg) + if err != nil { + return nil, err + } + + switch quicParams.Congestion { + case "force-brutal": + errors.LogDebug(context.Background(), quicConn.RemoteAddr(), " ", "congestion brutal bytes per second ", quicParams.BrutalUp) + congestion.UseBrutal(quicConn, quicParams.BrutalUp) + case "reno": + errors.LogDebug(context.Background(), quicConn.RemoteAddr(), " ", "congestion reno") + default: + errors.LogDebug(context.Background(), quicConn.RemoteAddr(), " ", "congestion bbr ", quicParams.BbrProfile) + congestion.UseBBR(quicConn, bbr.Profile(quicParams.BbrProfile)) + } + + return quicConn, nil + }, + } + } else if httpVersion == "2" { + if keepAlivePeriod == 0 { + keepAlivePeriod = net.ChromeH2KeepAlivePeriod + } + if keepAlivePeriod < 0 { + keepAlivePeriod = 0 + } + transport = &http2.Transport{ + DialTLSContext: func(ctxInner context.Context, network string, addr string, cfg *gotls.Config) (net.Conn, error) { + return dialContext(ctxInner) + }, + IdleConnTimeout: net.ConnIdleTimeout, + ReadIdleTimeout: keepAlivePeriod, + } + } else { + httpDialContext := func(ctxInner context.Context, network string, addr string) (net.Conn, error) { + return dialContext(ctxInner) + } + + transport = &http.Transport{ + DialTLSContext: httpDialContext, + DialContext: httpDialContext, + IdleConnTimeout: net.ConnIdleTimeout, + // chunked transfer download with KeepAlives is buggy with + // http.Client and our custom dial context. + DisableKeepAlives: true, + } + } + + client := &DefaultDialerClient{ + transportConfig: transportConfig, + client: &http.Client{ + Transport: transport, + }, + httpVersion: httpVersion, + uploadRawPool: &sync.Pool{}, + dialUploadConn: dialContext, + } + + return client +} + +func init() { + common.Must(internet.RegisterTransportDialer(protocolName, Dial)) +} + +func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.MemoryStreamConfig) (stat.Connection, error) { + tlsConfig := tls.ConfigFromStreamSettings(streamSettings) + realityConfig := reality.ConfigFromStreamSettings(streamSettings) + + httpVersion := decideHTTPVersion(tlsConfig, realityConfig) + if httpVersion == "3" { + dest.Network = net.Network_UDP + } + + transportConfiguration := streamSettings.ProtocolSettings.(*Config) + var requestURL url.URL + + if tlsConfig != nil || realityConfig != nil { + requestURL.Scheme = "https" + } else { + requestURL.Scheme = "http" + } + requestURL.Host = transportConfiguration.Host + if requestURL.Host == "" && tlsConfig != nil { + requestURL.Host = tlsConfig.ServerName + } + if requestURL.Host == "" && realityConfig != nil { + requestURL.Host = realityConfig.ServerName + } + if requestURL.Host == "" { + requestURL.Host = dest.Address.String() + } + + requestURL.Path = transportConfiguration.GetNormalizedPath() + requestURL.RawQuery = transportConfiguration.GetNormalizedQuery() + + httpClient, xmuxClient := getHTTPClient(ctx, dest, streamSettings) + + mode := transportConfiguration.Mode + if mode == "" || mode == "auto" { + mode = "packet-up" + if realityConfig != nil { + mode = "stream-one" + if transportConfiguration.DownloadSettings != nil { + mode = "stream-up" + } + } + } + + sessionId := "" + if mode != "stream-one" { + sessionIdUuid := uuid.New() + sessionId = sessionIdUuid.String() + } + + errors.LogInfo(ctx, fmt.Sprintf("XHTTP is dialing to %s, mode %s, HTTP version %s, host %s", dest, mode, httpVersion, requestURL.Host)) + + requestURL2 := requestURL + httpClient2 := httpClient + xmuxClient2 := xmuxClient + if transportConfiguration.DownloadSettings != nil { + globalDialerAccess.Lock() + if streamSettings.DownloadSettings == nil { + streamSettings.DownloadSettings = common.Must2(internet.ToMemoryStreamConfig(transportConfiguration.DownloadSettings)) + if streamSettings.SocketSettings != nil && streamSettings.SocketSettings.Penetrate { + streamSettings.DownloadSettings.SocketSettings = streamSettings.SocketSettings + } + } + globalDialerAccess.Unlock() + memory2 := streamSettings.DownloadSettings + dest2 := *memory2.Destination // just panic + tlsConfig2 := tls.ConfigFromStreamSettings(memory2) + realityConfig2 := reality.ConfigFromStreamSettings(memory2) + httpVersion2 := decideHTTPVersion(tlsConfig2, realityConfig2) + if httpVersion2 == "3" { + dest2.Network = net.Network_UDP + } + if tlsConfig2 != nil || realityConfig2 != nil { + requestURL2.Scheme = "https" + } else { + requestURL2.Scheme = "http" + } + config2 := memory2.ProtocolSettings.(*Config) + requestURL2.Host = config2.Host + if requestURL2.Host == "" && tlsConfig2 != nil { + requestURL2.Host = tlsConfig2.ServerName + } + if requestURL2.Host == "" && realityConfig2 != nil { + requestURL2.Host = realityConfig2.ServerName + } + if requestURL2.Host == "" { + requestURL2.Host = dest2.Address.String() + } + requestURL2.Path = config2.GetNormalizedPath() + requestURL2.RawQuery = config2.GetNormalizedQuery() + httpClient2, xmuxClient2 = getHTTPClient(ctx, dest2, memory2) + errors.LogInfo(ctx, fmt.Sprintf("XHTTP is downloading from %s, mode %s, HTTP version %s, host %s", dest2, "stream-down", httpVersion2, requestURL2.Host)) + } + + if xmuxClient != nil { + xmuxClient.OpenUsage.Add(1) + } + if xmuxClient2 != nil && xmuxClient2 != xmuxClient { + xmuxClient2.OpenUsage.Add(1) + } + var closed atomic.Int32 + + reader, writer := io.Pipe() + conn := splitConn{ + writer: writer, + onClose: func() { + if closed.Add(1) > 1 { + return + } + if xmuxClient != nil { + xmuxClient.OpenUsage.Add(-1) + } + if xmuxClient2 != nil && xmuxClient2 != xmuxClient { + xmuxClient2.OpenUsage.Add(-1) + } + }, + } + + var err error + if mode == "stream-one" { + requestURL.Path = transportConfiguration.GetNormalizedPath() + if xmuxClient != nil { + xmuxClient.LeftRequests.Add(-1) + } + conn.reader, conn.remoteAddr, conn.localAddr, err = httpClient.OpenStream(ctx, requestURL.String(), sessionId, reader, false) + if err != nil { // browser dialer only + return nil, err + } + return stat.Connection(&conn), nil + } else { // stream-down + if xmuxClient2 != nil { + xmuxClient2.LeftRequests.Add(-1) + } + conn.reader, conn.remoteAddr, conn.localAddr, err = httpClient2.OpenStream(ctx, requestURL2.String(), sessionId, nil, false) + if err != nil { // browser dialer only + return nil, err + } + } + if mode == "stream-up" { + if xmuxClient != nil { + xmuxClient.LeftRequests.Add(-1) + } + _, _, _, err = httpClient.OpenStream(ctx, requestURL.String(), sessionId, reader, true) + if err != nil { // browser dialer only + return nil, err + } + return stat.Connection(&conn), nil + } + + scMaxEachPostBytes := transportConfiguration.GetNormalizedScMaxEachPostBytes() + scMinPostsIntervalMs := transportConfiguration.GetNormalizedScMinPostsIntervalMs() + + if scMaxEachPostBytes.From <= 0 { + panic("`scMaxEachPostBytes` should be bigger than 0") + } + + maxUploadSize := scMaxEachPostBytes.rand() + // WithSizeLimit(0) will still allow single bytes to pass, and a lot of + // code relies on this behavior. Subtract 1 so that together with + // uploadWriter wrapper, exact size limits can be enforced + // uploadPipeReader, uploadPipeWriter := pipe.New(pipe.WithSizeLimit(maxUploadSize - 1)) + uploadPipeReader, uploadPipeWriter := pipe.New(pipe.WithSizeLimit(max(0, maxUploadSize-buf.Size))) + + conn.writer = uploadWriter{ + uploadPipeWriter, + maxUploadSize, + } + + go func() { + var seq int64 + var lastWrite time.Time + + for { + // by offloading the uploads into a buffered pipe, multiple conn.Write + // calls get automatically batched together into larger POST requests. + // without batching, bandwidth is extremely limited. + remainder, err := uploadPipeReader.ReadMultiBuffer() + if err != nil { + break + } + + doSplit := atomic.Bool{} + for doSplit.Store(true); doSplit.Load(); { + var chunk buf.MultiBuffer + remainder, chunk = buf.SplitSize(remainder, maxUploadSize) + if chunk.IsEmpty() { + break + } + + wroteRequest := done.New() + + ctx := httptrace.WithClientTrace(ctx, &httptrace.ClientTrace{ + WroteRequest: func(httptrace.WroteRequestInfo) { + wroteRequest.Close() + }, + }) + + seqStr := strconv.FormatInt(seq, 10) + seq += 1 + + if scMinPostsIntervalMs.From > 0 { + time.Sleep(time.Duration(scMinPostsIntervalMs.rand())*time.Millisecond - time.Since(lastWrite)) + } + + lastWrite = time.Now() + + if xmuxClient != nil && (xmuxClient.LeftRequests.Add(-1) <= 0 || + (xmuxClient.UnreusableAt != time.Time{} && lastWrite.After(xmuxClient.UnreusableAt))) { + httpClient, xmuxClient = getHTTPClient(ctx, dest, streamSettings) + } + + go func() { + err := httpClient.PostPacket( + ctx, + requestURL.String(), + sessionId, + seqStr, + chunk, + ) + wroteRequest.Close() + if err != nil { + errors.LogInfoInner(ctx, err, "failed to send upload") + uploadPipeReader.Interrupt() + doSplit.Store(false) + } + }() + + if _, ok := httpClient.(*DefaultDialerClient); ok { + <-wroteRequest.Wait() + } + } + } + }() + + return stat.Connection(&conn), nil +} + +// A wrapper around pipe that ensures the size limit is exactly honored. +// +// The MultiBuffer pipe accepts any single WriteMultiBuffer call even if that +// single MultiBuffer exceeds the size limit, and then starts blocking on the +// next WriteMultiBuffer call. This means that ReadMultiBuffer can return more +// bytes than the size limit. We work around this by splitting a potentially +// too large write up into multiple. +type uploadWriter struct { + *pipe.Writer + maxLen int32 +} + +func (w uploadWriter) Write(b []byte) (int, error) { + /* + capacity := int(w.maxLen - w.Len()) + if capacity > 0 && capacity < len(b) { + b = b[:capacity] + } + */ + + buffer := buf.MultiBufferContainer{} + common.Must2(buffer.Write(b)) + + var writed int + for _, buff := range buffer.MultiBuffer { + err := w.WriteMultiBuffer(buf.MultiBuffer{buff}) + if err != nil { + return writed, err + } + writed += int(buff.Len()) + } + return writed, nil +} diff --git a/subproject/Xray-core-main/transport/internet/splithttp/h1_conn.go b/subproject/Xray-core-main/transport/internet/splithttp/h1_conn.go new file mode 100644 index 00000000..f89f2a66 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/splithttp/h1_conn.go @@ -0,0 +1,19 @@ +package splithttp + +import ( + "bufio" + "net" +) + +type H1Conn struct { + UnreadedResponsesCount int + RespBufReader *bufio.Reader + net.Conn +} + +func NewH1Conn(conn net.Conn) *H1Conn { + return &H1Conn{ + RespBufReader: bufio.NewReader(conn), + Conn: conn, + } +} diff --git a/subproject/Xray-core-main/transport/internet/splithttp/hub.go b/subproject/Xray-core-main/transport/internet/splithttp/hub.go new file mode 100644 index 00000000..8b281457 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/splithttp/hub.go @@ -0,0 +1,635 @@ +package splithttp + +import ( + "bytes" + "context" + gotls "crypto/tls" + "encoding/base64" + "fmt" + "io" + "net/http" + "slices" + "strconv" + "strings" + "sync" + "time" + + "github.com/apernet/quic-go" + "github.com/apernet/quic-go/http3" + goreality "github.com/xtls/reality" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + http_proto "github.com/xtls/xray-core/common/protocol/http" + "github.com/xtls/xray-core/common/signal/done" + "github.com/xtls/xray-core/transport/internet" + "github.com/xtls/xray-core/transport/internet/hysteria/congestion" + "github.com/xtls/xray-core/transport/internet/hysteria/congestion/bbr" + "github.com/xtls/xray-core/transport/internet/reality" + "github.com/xtls/xray-core/transport/internet/stat" + "github.com/xtls/xray-core/transport/internet/tls" +) + +type requestHandler struct { + config *Config + host string + path string + ln *Listener + sessionMu *sync.Mutex + sessions sync.Map + localAddr net.Addr + socketSettings *internet.SocketConfig +} + +type httpSession struct { + uploadQueue *uploadQueue + // for as long as the GET request is not opened by the client, this will be + // open ("undone"), and the session may be expired within a certain TTL. + // after the client connects, this becomes "done" and the session lives as + // long as the GET request. + isFullyConnected *done.Instance +} + +func (h *requestHandler) upsertSession(sessionId string) *httpSession { + // fast path + currentSessionAny, ok := h.sessions.Load(sessionId) + if ok { + return currentSessionAny.(*httpSession) + } + + // slow path + h.sessionMu.Lock() + defer h.sessionMu.Unlock() + + currentSessionAny, ok = h.sessions.Load(sessionId) + if ok { + return currentSessionAny.(*httpSession) + } + + s := &httpSession{ + uploadQueue: NewUploadQueue(h.ln.config.GetNormalizedScMaxBufferedPosts()), + isFullyConnected: done.New(), + } + + h.sessions.Store(sessionId, s) + + shouldReap := done.New() + go func() { + time.Sleep(30 * time.Second) + shouldReap.Close() + }() + go func() { + select { + case <-shouldReap.Wait(): + h.sessions.Delete(sessionId) + s.uploadQueue.Close() + case <-s.isFullyConnected.Wait(): + } + }() + + return s +} + +func (h *requestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) { + if len(h.host) > 0 && !internet.IsValidHTTPHost(request.Host, h.host) { + errors.LogInfo(context.Background(), "failed to validate host, request:", request.Host, ", config:", h.host) + writer.WriteHeader(http.StatusNotFound) + return + } + + if !strings.HasPrefix(request.URL.Path, h.path) { + errors.LogInfo(context.Background(), "failed to validate path, request:", request.URL.Path, ", config:", h.path) + writer.WriteHeader(http.StatusNotFound) + return + } + + h.config.WriteResponseHeader(writer, request.Method, request.Header) + length := int(h.config.GetNormalizedXPaddingBytes().rand()) + config := XPaddingConfig{Length: length} + + if h.config.XPaddingObfsMode { + config.Placement = XPaddingPlacement{ + Placement: h.config.XPaddingPlacement, + Key: h.config.XPaddingKey, + Header: h.config.XPaddingHeader, + } + config.Method = PaddingMethod(h.config.XPaddingMethod) + } else { + config.Placement = XPaddingPlacement{ + Placement: PlacementHeader, + Header: "X-Padding", + } + } + + h.config.ApplyXPaddingToResponse(writer, config) + + if request.Method == "OPTIONS" { + writer.WriteHeader(http.StatusOK) + return + } + + /* + clientVer := []int{0, 0, 0} + x_version := strings.Split(request.URL.Query().Get("x_version"), ".") + for j := 0; j < 3 && len(x_version) > j; j++ { + clientVer[j], _ = strconv.Atoi(x_version[j]) + } + */ + + validRange := h.config.GetNormalizedXPaddingBytes() + paddingValue, paddingPlacement := h.config.ExtractXPaddingFromRequest(request, h.config.XPaddingObfsMode) + + if !h.config.IsPaddingValid(paddingValue, validRange.From, validRange.To, PaddingMethod(h.config.XPaddingMethod)) { + errors.LogInfo(context.Background(), "invalid padding ("+paddingPlacement+") length:", int32(len(paddingValue))) + writer.WriteHeader(http.StatusBadRequest) + return + } + + sessionId, seqStr := h.config.ExtractMetaFromRequest(request, h.path) + + if sessionId == "" && h.config.Mode != "" && h.config.Mode != "auto" && h.config.Mode != "stream-one" && h.config.Mode != "stream-up" { + errors.LogInfo(context.Background(), "stream-one mode is not allowed") + writer.WriteHeader(http.StatusBadRequest) + return + } + + var forwardedAddrs []net.Address + if h.socketSettings != nil && len(h.socketSettings.TrustedXForwardedFor) > 0 { + for _, key := range h.socketSettings.TrustedXForwardedFor { + if len(request.Header.Values(key)) > 0 { + forwardedAddrs = http_proto.ParseXForwardedFor(request.Header) + break + } + } + } else { + forwardedAddrs = http_proto.ParseXForwardedFor(request.Header) + } + var remoteAddr net.Addr + var err error + remoteAddr, err = net.ResolveTCPAddr("tcp", request.RemoteAddr) + if err != nil { + remoteAddr = &net.TCPAddr{ + IP: []byte{0, 0, 0, 0}, + Port: 0, + } + } + if request.ProtoMajor == 3 { + remoteAddr = &net.UDPAddr{ + IP: remoteAddr.(*net.TCPAddr).IP, + Port: remoteAddr.(*net.TCPAddr).Port, + } + } + if len(forwardedAddrs) > 0 && forwardedAddrs[0].Family().IsIP() { + remoteAddr = &net.TCPAddr{ + IP: forwardedAddrs[0].IP(), + Port: 0, + } + } + + var currentSession *httpSession + if sessionId != "" { + currentSession = h.upsertSession(sessionId) + } + scMaxEachPostBytes := int(h.ln.config.GetNormalizedScMaxEachPostBytes().To) + isUplinkRequest := false + + switch request.Method { + case "GET": + isUplinkRequest = seqStr != "" + default: + isUplinkRequest = true + } + + uplinkDataKey := h.config.UplinkDataKey + + if isUplinkRequest && sessionId != "" { // stream-up, packet-up + if seqStr == "" { + if h.config.Mode != "" && h.config.Mode != "auto" && h.config.Mode != "stream-up" { + errors.LogInfo(context.Background(), "stream-up mode is not allowed") + writer.WriteHeader(http.StatusBadRequest) + return + } + httpSC := &httpServerConn{ + Instance: done.New(), + Reader: request.Body, + ResponseWriter: writer, + } + err = currentSession.uploadQueue.Push(Packet{ + Reader: httpSC, + }) + if err != nil { + errors.LogInfoInner(context.Background(), err, "failed to upload (PushReader)") + writer.WriteHeader(http.StatusConflict) + } else { + writer.Header().Set("X-Accel-Buffering", "no") + writer.Header().Set("Cache-Control", "no-store") + writer.WriteHeader(http.StatusOK) + scStreamUpServerSecs := h.config.GetNormalizedScStreamUpServerSecs() + referrer := request.Header.Get("Referer") + if referrer != "" && scStreamUpServerSecs.To > 0 { + go func() { + for { + _, err := httpSC.Write(bytes.Repeat([]byte{'X'}, int(h.config.GetNormalizedXPaddingBytes().rand()))) + if err != nil { + break + } + time.Sleep(time.Duration(scStreamUpServerSecs.rand()) * time.Second) + } + }() + } + select { + case <-request.Context().Done(): + case <-httpSC.Wait(): + } + } + httpSC.Close() + return + } + + if h.config.Mode != "" && h.config.Mode != "auto" && h.config.Mode != "packet-up" { + errors.LogInfo(context.Background(), "packet-up mode is not allowed") + writer.WriteHeader(http.StatusBadRequest) + return + } + + dataPlacement := h.config.GetNormalizedUplinkDataPlacement() + var headerPayload []byte + if dataPlacement == PlacementAuto || dataPlacement == PlacementHeader { + var headerPayloadChunks []string + for i := 0; true; i++ { + chunk := request.Header.Get(fmt.Sprintf("%s-%d", uplinkDataKey, i)) + if chunk == "" { + break + } + headerPayloadChunks = append(headerPayloadChunks, chunk) + } + headerPayloadEncoded := strings.Join(headerPayloadChunks, "") + headerPayload, err = base64.RawURLEncoding.DecodeString(headerPayloadEncoded) + if err != nil { + errors.LogInfo(context.Background(), "Invalid base64 in header's payload: ", err.Error()) + writer.WriteHeader(http.StatusBadRequest) + return + } + } + + var cookiePayload []byte + if dataPlacement == PlacementAuto || dataPlacement == PlacementCookie { + var cookiePayloadChunks []string + for i := 0; true; i++ { + cookieName := fmt.Sprintf("%s_%d", uplinkDataKey, i) + if c, _ := request.Cookie(cookieName); c != nil { + cookiePayloadChunks = append(cookiePayloadChunks, c.Value) + } else { + break + } + } + cookiePayloadEncoded := strings.Join(cookiePayloadChunks, "") + cookiePayload, err = base64.RawURLEncoding.DecodeString(cookiePayloadEncoded) + if err != nil { + errors.LogInfo(context.Background(), "Invalid base64 in cookies' payload: ", err.Error()) + writer.WriteHeader(http.StatusBadRequest) + return + } + } + + var bodyPayload []byte + if dataPlacement == PlacementAuto || dataPlacement == PlacementBody { + var readErr error + if request.ContentLength > int64(scMaxEachPostBytes) { + errors.LogInfo(context.Background(), "Too large upload. scMaxEachPostBytes is set to ", scMaxEachPostBytes, "but request size exceed it. Adjust scMaxEachPostBytes on the server to be at least as large as client.") + writer.WriteHeader(http.StatusRequestEntityTooLarge) + return + } + if request.ContentLength > 0 { + bodyPayload = make([]byte, request.ContentLength) + _, readErr = io.ReadFull(request.Body, bodyPayload) + } else { + bodyPayload, readErr = buf.ReadAllToBytes(io.LimitReader(request.Body, int64(scMaxEachPostBytes)+1)) + } + if readErr != nil { + errors.LogInfoInner(context.Background(), readErr, "failed to read body payload") + writer.WriteHeader(http.StatusBadRequest) + return + } + } + + var payload []byte + switch dataPlacement { + case PlacementHeader: + payload = headerPayload + case PlacementCookie: + payload = cookiePayload + case PlacementBody: + payload = bodyPayload + case PlacementAuto: + payload = slices.Concat(headerPayload, cookiePayload, bodyPayload) + } + + if len(payload) > scMaxEachPostBytes { + errors.LogInfo(context.Background(), "Too large upload. scMaxEachPostBytes is set to ", scMaxEachPostBytes, "but request size exceed it. Adjust scMaxEachPostBytes on the server to be at least as large as client.") + writer.WriteHeader(http.StatusRequestEntityTooLarge) + return + } + + seq, err := strconv.ParseUint(seqStr, 10, 64) + if err != nil { + errors.LogInfoInner(context.Background(), err, "failed to upload (ParseUint)") + writer.WriteHeader(http.StatusInternalServerError) + return + } + + err = currentSession.uploadQueue.Push(Packet{ + Payload: payload, + Seq: seq, + }) + + if err != nil { + errors.LogInfoInner(context.Background(), err, "failed to upload (PushPayload)") + writer.WriteHeader(http.StatusInternalServerError) + return + } + + if len(bodyPayload) == 0 { + // Methods without a body are usually cached by default. + writer.Header().Set("Cache-Control", "no-store") + } + + writer.WriteHeader(http.StatusOK) + } else if request.Method == "GET" || sessionId == "" { // stream-down, stream-one + if sessionId != "" { + // after GET is done, the connection is finished. disable automatic + // session reaping, and handle it in defer + currentSession.isFullyConnected.Close() + defer h.sessions.Delete(sessionId) + } + + // magic header instructs nginx + apache to not buffer response body + writer.Header().Set("X-Accel-Buffering", "no") + // A web-compliant header telling all middleboxes to disable caching. + // Should be able to prevent overloading the cache, or stop CDNs from + // teeing the response stream into their cache, causing slowdowns. + writer.Header().Set("Cache-Control", "no-store") + + if !h.config.NoSSEHeader { + // magic header to make the HTTP middle box consider this as SSE to disable buffer + writer.Header().Set("Content-Type", "text/event-stream") + } + + writer.WriteHeader(http.StatusOK) + writer.(http.Flusher).Flush() + + httpSC := &httpServerConn{ + Instance: done.New(), + Reader: request.Body, + ResponseWriter: writer, + } + conn := splitConn{ + writer: httpSC, + reader: httpSC, + remoteAddr: remoteAddr, + localAddr: h.localAddr, + } + if sessionId != "" { // if not stream-one + conn.reader = currentSession.uploadQueue + } + + h.ln.addConn(stat.Connection(&conn)) + + // "A ResponseWriter may not be used after [Handler.ServeHTTP] has returned." + select { + case <-request.Context().Done(): + case <-httpSC.Wait(): + } + + conn.Close() + } else { + errors.LogInfo(context.Background(), "unsupported method: ", request.Method) + writer.WriteHeader(http.StatusMethodNotAllowed) + } +} + +type httpServerConn struct { + sync.Mutex + *done.Instance + io.Reader // no need to Close request.Body + http.ResponseWriter +} + +func (c *httpServerConn) Write(b []byte) (int, error) { + c.Lock() + defer c.Unlock() + if c.Done() { + return 0, io.ErrClosedPipe + } + n, err := c.ResponseWriter.Write(b) + if err == nil { + c.ResponseWriter.(http.Flusher).Flush() + } + return n, err +} + +func (c *httpServerConn) Close() error { + c.Lock() + defer c.Unlock() + return c.Instance.Close() +} + +type Listener struct { + sync.Mutex + server http.Server + h3server *http3.Server + listener net.Listener + h3listener *quic.EarlyListener + config *Config + addConn internet.ConnHandler + isH3 bool +} + +func ListenXH(ctx context.Context, address net.Address, port net.Port, streamSettings *internet.MemoryStreamConfig, addConn internet.ConnHandler) (internet.Listener, error) { + l := &Listener{ + addConn: addConn, + } + l.config = streamSettings.ProtocolSettings.(*Config) + if l.config != nil { + if streamSettings.SocketSettings == nil { + streamSettings.SocketSettings = &internet.SocketConfig{} + } + } + handler := &requestHandler{ + config: l.config, + host: l.config.Host, + path: l.config.GetNormalizedPath(), + ln: l, + sessionMu: &sync.Mutex{}, + sessions: sync.Map{}, + socketSettings: streamSettings.SocketSettings, + } + tlsConfig := getTLSConfig(streamSettings) + l.isH3 = len(tlsConfig.NextProtos) == 1 && tlsConfig.NextProtos[0] == "h3" + + var err error + if port == net.Port(0) { // unix + l.listener, err = internet.ListenSystem(ctx, &net.UnixAddr{ + Name: address.Domain(), + Net: "unix", + }, streamSettings.SocketSettings) + if err != nil { + return nil, errors.New("failed to listen UNIX domain socket for XHTTP on ", address).Base(err) + } + errors.LogInfo(ctx, "listening UNIX domain socket for XHTTP on ", address) + } else if l.isH3 { // quic + Conn, err := internet.ListenSystemPacket(context.Background(), &net.UDPAddr{ + IP: address.IP(), + Port: int(port), + }, streamSettings.SocketSettings) + if err != nil { + return nil, errors.New("failed to listen UDP for XHTTP/3 on ", address, ":", port).Base(err) + } + if streamSettings.UdpmaskManager != nil { + pktConn, err := streamSettings.UdpmaskManager.WrapPacketConnServer(Conn) + if err != nil { + Conn.Close() + return nil, errors.New("mask err").Base(err) + } + Conn = pktConn + } + + quicParams := streamSettings.QuicParams + if quicParams == nil { + quicParams = &internet.QuicParams{ + BbrProfile: string(bbr.ProfileStandard), + UdpHop: &internet.UdpHop{}, + } + } + + quicConfig := &quic.Config{ + InitialStreamReceiveWindow: quicParams.InitStreamReceiveWindow, + MaxStreamReceiveWindow: quicParams.MaxStreamReceiveWindow, + InitialConnectionReceiveWindow: quicParams.InitConnReceiveWindow, + MaxConnectionReceiveWindow: quicParams.MaxConnReceiveWindow, + MaxIdleTimeout: time.Duration(quicParams.MaxIdleTimeout) * time.Second, + MaxIncomingStreams: quicParams.MaxIncomingStreams, + DisablePathMTUDiscovery: quicParams.DisablePathMtuDiscovery, + } + + l.h3listener, err = quic.ListenEarly(Conn, tlsConfig, quicConfig) + if err != nil { + return nil, errors.New("failed to listen QUIC for XHTTP/3 on ", address, ":", port).Base(err) + } + errors.LogInfo(ctx, "listening QUIC for XHTTP/3 on ", address, ":", port) + + handler.localAddr = l.h3listener.Addr() + + l.h3server = &http3.Server{ + Handler: handler, + } + go func() { + for { + conn, err := l.h3listener.Accept(context.Background()) + if err != nil { + errors.LogInfoInner(ctx, err, "XHTTP/3 listener closed") + return + } + + switch quicParams.Congestion { + case "force-brutal": + errors.LogDebug(context.Background(), conn.RemoteAddr(), " ", "congestion brutal bytes per second ", quicParams.BrutalUp) + congestion.UseBrutal(conn, quicParams.BrutalUp) + case "reno": + errors.LogDebug(context.Background(), conn.RemoteAddr(), " ", "congestion reno") + default: + errors.LogDebug(context.Background(), conn.RemoteAddr(), " ", "congestion bbr ", quicParams.BbrProfile) + congestion.UseBBR(conn, bbr.Profile(quicParams.BbrProfile)) + } + + go func() { + if err := l.h3server.ServeQUICConn(conn); err != nil { + errors.LogDebugInner(ctx, err, "XHTTP/3 connection ended") + } + _ = conn.CloseWithError(0, "") + }() + } + }() + } else { // tcp + l.listener, err = internet.ListenSystem(ctx, &net.TCPAddr{ + IP: address.IP(), + Port: int(port), + }, streamSettings.SocketSettings) + if err != nil { + return nil, errors.New("failed to listen TCP for XHTTP on ", address, ":", port).Base(err) + } + errors.LogInfo(ctx, "listening TCP for XHTTP on ", address, ":", port) + } + + if !l.isH3 && streamSettings.TcpmaskManager != nil { + l.listener, _ = streamSettings.TcpmaskManager.WrapListener(l.listener) + } + + // tcp/unix (h1/h2) + if l.listener != nil { + if config := tls.ConfigFromStreamSettings(streamSettings); config != nil { + if tlsConfig := config.GetTLSConfig(); tlsConfig != nil { + l.listener = gotls.NewListener(l.listener, tlsConfig) + } + } + if config := reality.ConfigFromStreamSettings(streamSettings); config != nil { + l.listener = goreality.NewListener(l.listener, config.GetREALITYConfig()) + } + + handler.localAddr = l.listener.Addr() + + // server can handle both plaintext HTTP/1.1 and h2c + protocols := new(http.Protocols) + protocols.SetHTTP1(true) + protocols.SetUnencryptedHTTP2(true) + l.server = http.Server{ + Handler: handler, + ReadHeaderTimeout: time.Second * 4, + MaxHeaderBytes: l.config.GetNormalizedServerMaxHeaderBytes(), + Protocols: protocols, + } + go func() { + if err := l.server.Serve(l.listener); err != nil { + errors.LogErrorInner(ctx, err, "failed to serve HTTP for XHTTP") + } + }() + } + + return l, err +} + +// Addr implements net.Listener.Addr(). +func (ln *Listener) Addr() net.Addr { + if ln.h3listener != nil { + return ln.h3listener.Addr() + } + if ln.listener != nil { + return ln.listener.Addr() + } + return nil +} + +// Close implements net.Listener.Close(). +func (ln *Listener) Close() error { + if ln.h3server != nil { + if err := ln.h3server.Close(); err != nil { + _ = ln.h3listener.Close() + return err + } + return ln.h3listener.Close() + } else if ln.listener != nil { + return ln.listener.Close() + } + return errors.New("listener does not have an HTTP/3 server or a net.listener") +} +func getTLSConfig(streamSettings *internet.MemoryStreamConfig) *gotls.Config { + config := tls.ConfigFromStreamSettings(streamSettings) + if config == nil { + return &gotls.Config{} + } + return config.GetTLSConfig() +} +func init() { + common.Must(internet.RegisterTransportListener(protocolName, ListenXH)) +} diff --git a/subproject/Xray-core-main/transport/internet/splithttp/mux.go b/subproject/Xray-core-main/transport/internet/splithttp/mux.go new file mode 100644 index 00000000..093ddd12 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/splithttp/mux.go @@ -0,0 +1,113 @@ +package splithttp + +import ( + "context" + "crypto/rand" + "math" + "math/big" + "sync/atomic" + "time" + + "github.com/xtls/xray-core/common/errors" +) + +type XmuxConn interface { + IsClosed() bool +} + +type XmuxClient struct { + XmuxConn XmuxConn + OpenUsage atomic.Int32 + leftUsage int32 + LeftRequests atomic.Int32 + UnreusableAt time.Time +} + +type XmuxManager struct { + xmuxConfig XmuxConfig + concurrency int32 + connections int32 + newConnFunc func() XmuxConn + xmuxClients []*XmuxClient +} + +func NewXmuxManager(xmuxConfig XmuxConfig, newConnFunc func() XmuxConn) *XmuxManager { + return &XmuxManager{ + xmuxConfig: xmuxConfig, + concurrency: xmuxConfig.GetNormalizedMaxConcurrency().rand(), + connections: xmuxConfig.GetNormalizedMaxConnections().rand(), + newConnFunc: newConnFunc, + xmuxClients: make([]*XmuxClient, 0), + } +} + +func (m *XmuxManager) newXmuxClient() *XmuxClient { + xmuxClient := &XmuxClient{ + XmuxConn: m.newConnFunc(), + leftUsage: -1, + } + if x := m.xmuxConfig.GetNormalizedCMaxReuseTimes().rand(); x > 0 { + xmuxClient.leftUsage = x - 1 + } + xmuxClient.LeftRequests.Store(math.MaxInt32) + if x := m.xmuxConfig.GetNormalizedHMaxRequestTimes().rand(); x > 0 { + xmuxClient.LeftRequests.Store(x) + } + if x := m.xmuxConfig.GetNormalizedHMaxReusableSecs().rand(); x > 0 { + xmuxClient.UnreusableAt = time.Now().Add(time.Duration(x) * time.Second) + } + m.xmuxClients = append(m.xmuxClients, xmuxClient) + return xmuxClient +} + +func (m *XmuxManager) GetXmuxClient(ctx context.Context) *XmuxClient { // when locking + for i := 0; i < len(m.xmuxClients); { + xmuxClient := m.xmuxClients[i] + if xmuxClient.XmuxConn.IsClosed() || + xmuxClient.leftUsage == 0 || + xmuxClient.LeftRequests.Load() <= 0 || + (xmuxClient.UnreusableAt != time.Time{} && time.Now().After(xmuxClient.UnreusableAt)) { + errors.LogDebug(ctx, "XMUX: removing xmuxClient, IsClosed() = ", xmuxClient.XmuxConn.IsClosed(), + ", OpenUsage = ", xmuxClient.OpenUsage.Load(), + ", leftUsage = ", xmuxClient.leftUsage, + ", LeftRequests = ", xmuxClient.LeftRequests.Load(), + ", UnreusableAt = ", xmuxClient.UnreusableAt) + m.xmuxClients = append(m.xmuxClients[:i], m.xmuxClients[i+1:]...) + } else { + i++ + } + } + + if len(m.xmuxClients) == 0 { + errors.LogDebug(ctx, "XMUX: creating xmuxClient because xmuxClients is empty") + return m.newXmuxClient() + } + + if m.connections > 0 && len(m.xmuxClients) < int(m.connections) { + errors.LogDebug(ctx, "XMUX: creating xmuxClient because maxConnections was not hit, xmuxClients = ", len(m.xmuxClients)) + return m.newXmuxClient() + } + + xmuxClients := make([]*XmuxClient, 0) + if m.concurrency > 0 { + for _, xmuxClient := range m.xmuxClients { + if xmuxClient.OpenUsage.Load() < m.concurrency { + xmuxClients = append(xmuxClients, xmuxClient) + } + } + } else { + xmuxClients = m.xmuxClients + } + + if len(xmuxClients) == 0 { + errors.LogDebug(ctx, "XMUX: creating xmuxClient because maxConcurrency was hit, xmuxClients = ", len(m.xmuxClients)) + return m.newXmuxClient() + } + + i, _ := rand.Int(rand.Reader, big.NewInt(int64(len(xmuxClients)))) + xmuxClient := xmuxClients[i.Int64()] + if xmuxClient.leftUsage > 0 { + xmuxClient.leftUsage -= 1 + } + return xmuxClient +} diff --git a/subproject/Xray-core-main/transport/internet/splithttp/mux_test.go b/subproject/Xray-core-main/transport/internet/splithttp/mux_test.go new file mode 100644 index 00000000..835d07f0 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/splithttp/mux_test.go @@ -0,0 +1,92 @@ +package splithttp_test + +import ( + "context" + "testing" + + . "github.com/xtls/xray-core/transport/internet/splithttp" +) + +type fakeRoundTripper struct{} + +func (f *fakeRoundTripper) IsClosed() bool { + return false +} + +func TestMaxConnections(t *testing.T) { + xmuxConfig := XmuxConfig{ + MaxConnections: &RangeConfig{From: 4, To: 4}, + } + + xmuxManager := NewXmuxManager(xmuxConfig, func() XmuxConn { + return &fakeRoundTripper{} + }) + + xmuxClients := make(map[interface{}]struct{}) + for i := 0; i < 8; i++ { + xmuxClients[xmuxManager.GetXmuxClient(context.Background())] = struct{}{} + } + + if len(xmuxClients) != 4 { + t.Error("did not get 4 distinct clients, got ", len(xmuxClients)) + } +} + +func TestCMaxReuseTimes(t *testing.T) { + xmuxConfig := XmuxConfig{ + CMaxReuseTimes: &RangeConfig{From: 2, To: 2}, + } + + xmuxManager := NewXmuxManager(xmuxConfig, func() XmuxConn { + return &fakeRoundTripper{} + }) + + xmuxClients := make(map[interface{}]struct{}) + for i := 0; i < 64; i++ { + xmuxClients[xmuxManager.GetXmuxClient(context.Background())] = struct{}{} + } + + if len(xmuxClients) != 32 { + t.Error("did not get 32 distinct clients, got ", len(xmuxClients)) + } +} + +func TestMaxConcurrency(t *testing.T) { + xmuxConfig := XmuxConfig{ + MaxConcurrency: &RangeConfig{From: 2, To: 2}, + } + + xmuxManager := NewXmuxManager(xmuxConfig, func() XmuxConn { + return &fakeRoundTripper{} + }) + + xmuxClients := make(map[interface{}]struct{}) + for i := 0; i < 64; i++ { + xmuxClient := xmuxManager.GetXmuxClient(context.Background()) + xmuxClient.OpenUsage.Add(1) + xmuxClients[xmuxClient] = struct{}{} + } + + if len(xmuxClients) != 32 { + t.Error("did not get 32 distinct clients, got ", len(xmuxClients)) + } +} + +func TestDefault(t *testing.T) { + xmuxConfig := XmuxConfig{} + + xmuxManager := NewXmuxManager(xmuxConfig, func() XmuxConn { + return &fakeRoundTripper{} + }) + + xmuxClients := make(map[interface{}]struct{}) + for i := 0; i < 64; i++ { + xmuxClient := xmuxManager.GetXmuxClient(context.Background()) + xmuxClient.OpenUsage.Add(1) + xmuxClients[xmuxClient] = struct{}{} + } + + if len(xmuxClients) != 1 { + t.Error("did not get 1 distinct clients, got ", len(xmuxClients)) + } +} diff --git a/subproject/Xray-core-main/transport/internet/splithttp/splithttp.go b/subproject/Xray-core-main/transport/internet/splithttp/splithttp.go new file mode 100644 index 00000000..2076e933 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/splithttp/splithttp.go @@ -0,0 +1,3 @@ +package splithttp + +const protocolName = "splithttp" diff --git a/subproject/Xray-core-main/transport/internet/splithttp/splithttp_test.go b/subproject/Xray-core-main/transport/internet/splithttp/splithttp_test.go new file mode 100644 index 00000000..ab02b619 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/splithttp/splithttp_test.go @@ -0,0 +1,464 @@ +package splithttp_test + +import ( + "bytes" + "context" + "crypto/rand" + "fmt" + "io" + "net/http" + "runtime" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol/tls/cert" + "github.com/xtls/xray-core/testing/servers/tcp" + "github.com/xtls/xray-core/testing/servers/udp" + "github.com/xtls/xray-core/transport/internet" + . "github.com/xtls/xray-core/transport/internet/splithttp" + "github.com/xtls/xray-core/transport/internet/stat" + "github.com/xtls/xray-core/transport/internet/tls" +) + +func Test_ListenXHAndDial(t *testing.T) { + listenPort := tcp.PickPort() + listen, err := ListenXH(context.Background(), net.LocalHostIP, listenPort, &internet.MemoryStreamConfig{ + ProtocolName: "splithttp", + ProtocolSettings: &Config{ + Path: "/sh", + }, + }, func(conn stat.Connection) { + go func(c stat.Connection) { + defer c.Close() + + var b [1024]byte + c.SetReadDeadline(time.Now().Add(2 * time.Second)) + _, err := c.Read(b[:]) + if err != nil { + return + } + + common.Must2(c.Write([]byte("Response"))) + }(conn) + }) + common.Must(err) + ctx := context.Background() + streamSettings := &internet.MemoryStreamConfig{ + ProtocolName: "splithttp", + ProtocolSettings: &Config{Path: "sh"}, + } + conn, err := Dial(ctx, net.TCPDestination(net.DomainAddress("localhost"), listenPort), streamSettings) + + common.Must(err) + _, err = conn.Write([]byte("Test connection 1")) + common.Must(err) + + var b [1024]byte + fmt.Println("test2") + n, _ := io.ReadFull(conn, b[:]) + fmt.Println("string is", n) + if string(b[:n]) != "Response" { + t.Error("response: ", string(b[:n])) + } + + common.Must(conn.Close()) + conn, err = Dial(ctx, net.TCPDestination(net.DomainAddress("localhost"), listenPort), streamSettings) + + common.Must(err) + _, err = conn.Write([]byte("Test connection 2")) + common.Must(err) + n, _ = io.ReadFull(conn, b[:]) + common.Must(err) + if string(b[:n]) != "Response" { + t.Error("response: ", string(b[:n])) + } + common.Must(conn.Close()) + + common.Must(listen.Close()) +} + +func TestDialWithRemoteAddr(t *testing.T) { + listenPort := tcp.PickPort() + listen, err := ListenXH(context.Background(), net.LocalHostIP, listenPort, &internet.MemoryStreamConfig{ + ProtocolName: "splithttp", + ProtocolSettings: &Config{ + Path: "sh", + }, + }, func(conn stat.Connection) { + go func(c stat.Connection) { + defer c.Close() + + var b [1024]byte + _, err := c.Read(b[:]) + // common.Must(err) + if err != nil { + return + } + + _, err = c.Write([]byte(c.RemoteAddr().String())) + common.Must(err) + }(conn) + }) + common.Must(err) + + conn, err := Dial(context.Background(), net.TCPDestination(net.DomainAddress("localhost"), listenPort), &internet.MemoryStreamConfig{ + ProtocolName: "splithttp", + ProtocolSettings: &Config{Path: "sh", Headers: map[string]string{"X-Forwarded-For": "1.1.1.1"}}, + }) + + common.Must(err) + _, err = conn.Write([]byte("Test connection 1")) + common.Must(err) + + var b [1024]byte + n, _ := io.ReadFull(conn, b[:]) + if string(b[:n]) != "1.1.1.1:0" { + t.Error("response: ", string(b[:n])) + } + + common.Must(listen.Close()) +} + +func Test_ListenXHAndDial_TLS(t *testing.T) { + if runtime.GOARCH == "arm64" { + return + } + + listenPort := tcp.PickPort() + + start := time.Now() + + ct, ctHash := cert.MustGenerate(nil, cert.CommonName("localhost")) + + streamSettings := &internet.MemoryStreamConfig{ + ProtocolName: "splithttp", + ProtocolSettings: &Config{ + Path: "shs", + }, + SecurityType: "tls", + SecuritySettings: &tls.Config{ + Certificate: []*tls.Certificate{tls.ParseCertificate(ct)}, + PinnedPeerCertSha256: [][]byte{ctHash[:]}, + }, + } + listen, err := ListenXH(context.Background(), net.LocalHostIP, listenPort, streamSettings, func(conn stat.Connection) { + go func() { + defer conn.Close() + + var b [1024]byte + conn.SetReadDeadline(time.Now().Add(2 * time.Second)) + _, err := conn.Read(b[:]) + if err != nil { + return + } + + common.Must2(conn.Write([]byte("Response"))) + }() + }) + common.Must(err) + defer listen.Close() + + conn, err := Dial(context.Background(), net.TCPDestination(net.DomainAddress("localhost"), listenPort), streamSettings) + common.Must(err) + + _, err = conn.Write([]byte("Test connection 1")) + common.Must(err) + + var b [1024]byte + n, _ := io.ReadFull(conn, b[:]) + if string(b[:n]) != "Response" { + t.Error("response: ", string(b[:n])) + } + + end := time.Now() + if !end.Before(start.Add(time.Second * 5)) { + t.Error("end: ", end, " start: ", start) + } +} + +func Test_ListenXHAndDial_H2C(t *testing.T) { + if runtime.GOARCH == "arm64" { + return + } + + listenPort := tcp.PickPort() + + streamSettings := &internet.MemoryStreamConfig{ + ProtocolName: "splithttp", + ProtocolSettings: &Config{ + Path: "shs", + }, + } + listen, err := ListenXH(context.Background(), net.LocalHostIP, listenPort, streamSettings, func(conn stat.Connection) { + go func() { + _ = conn.Close() + }() + }) + common.Must(err) + defer listen.Close() + + protocols := new(http.Protocols) + protocols.SetUnencryptedHTTP2(true) + client := http.Client{ + Transport: &http.Transport{ + Protocols: protocols, + }, + } + + resp, err := client.Get("http://" + net.LocalHostIP.String() + ":" + listenPort.String()) + common.Must(err) + + if resp.StatusCode != 404 { + t.Error("Expected 404 but got:", resp.StatusCode) + } + + if resp.ProtoMajor != 2 { + t.Error("Expected h2 but got:", resp.ProtoMajor) + } +} + +func Test_ListenXHAndDial_QUIC(t *testing.T) { + if runtime.GOARCH == "arm64" { + return + } + + listenPort := udp.PickPort() + + start := time.Now() + + ct, ctHash := cert.MustGenerate(nil, cert.CommonName("localhost")) + + streamSettings := &internet.MemoryStreamConfig{ + ProtocolName: "splithttp", + ProtocolSettings: &Config{ + Path: "shs", + }, + SecurityType: "tls", + SecuritySettings: &tls.Config{ + Certificate: []*tls.Certificate{tls.ParseCertificate(ct)}, + PinnedPeerCertSha256: [][]byte{ctHash[:]}, + NextProtocol: []string{"h3"}, + }, + } + + serverClosed := false + listen, err := ListenXH(context.Background(), net.LocalHostIP, listenPort, streamSettings, func(conn stat.Connection) { + go func() { + defer conn.Close() + + b := buf.New() + defer b.Release() + + for { + b.Clear() + if _, err := b.ReadFrom(conn); err != nil { + break + } + common.Must2(conn.Write(b.Bytes())) + } + + serverClosed = true + }() + }) + common.Must(err) + defer listen.Close() + + time.Sleep(time.Second) + + conn, err := Dial(context.Background(), net.UDPDestination(net.DomainAddress("localhost"), listenPort), streamSettings) + common.Must(err) + + const N = 1024 + b1 := make([]byte, N) + common.Must2(rand.Read(b1)) + b2 := buf.New() + + common.Must2(conn.Write(b1)) + + b2.Clear() + common.Must2(b2.ReadFullFrom(conn, N)) + if r := cmp.Diff(b2.Bytes(), b1); r != "" { + t.Error(r) + } + + common.Must2(conn.Write(b1)) + + b2.Clear() + common.Must2(b2.ReadFullFrom(conn, N)) + if r := cmp.Diff(b2.Bytes(), b1); r != "" { + t.Error(r) + } + + conn.Close() + time.Sleep(100 * time.Millisecond) + if !serverClosed { + t.Error("server did not get closed") + } + + end := time.Now() + if !end.Before(start.Add(time.Second * 5)) { + t.Error("end: ", end, " start: ", start) + } +} + +func Test_ListenXHAndDial_Unix(t *testing.T) { + tempDir := t.TempDir() + tempSocket := tempDir + "/server.sock" + + listen, err := ListenXH(context.Background(), net.DomainAddress(tempSocket), 0, &internet.MemoryStreamConfig{ + ProtocolName: "splithttp", + ProtocolSettings: &Config{ + Path: "/sh", + }, + }, func(conn stat.Connection) { + go func(c stat.Connection) { + defer c.Close() + + var b [1024]byte + c.SetReadDeadline(time.Now().Add(2 * time.Second)) + _, err := c.Read(b[:]) + if err != nil { + return + } + + common.Must2(c.Write([]byte("Response"))) + }(conn) + }) + common.Must(err) + ctx := context.Background() + streamSettings := &internet.MemoryStreamConfig{ + ProtocolName: "splithttp", + ProtocolSettings: &Config{ + Host: "example.com", + Path: "sh", + }, + } + conn, err := Dial(ctx, net.UnixDestination(net.DomainAddress(tempSocket)), streamSettings) + + common.Must(err) + _, err = conn.Write([]byte("Test connection 1")) + common.Must(err) + + var b [1024]byte + fmt.Println("test2") + n, _ := io.ReadFull(conn, b[:]) + fmt.Println("string is", n) + if string(b[:n]) != "Response" { + t.Error("response: ", string(b[:n])) + } + + common.Must(conn.Close()) + conn, err = Dial(ctx, net.UnixDestination(net.DomainAddress(tempSocket)), streamSettings) + + common.Must(err) + _, err = conn.Write([]byte("Test connection 2")) + common.Must(err) + n, _ = io.ReadFull(conn, b[:]) + common.Must(err) + if string(b[:n]) != "Response" { + t.Error("response: ", string(b[:n])) + } + common.Must(conn.Close()) + + common.Must(listen.Close()) +} + +func Test_queryString(t *testing.T) { + listenPort := tcp.PickPort() + listen, err := ListenXH(context.Background(), net.LocalHostIP, listenPort, &internet.MemoryStreamConfig{ + ProtocolName: "splithttp", + ProtocolSettings: &Config{ + // this querystring does not have any effect, but sometimes people blindly copy it from websocket config. make sure the outbound doesn't break + Path: "/sh?ed=2048", + }, + }, func(conn stat.Connection) { + go func(c stat.Connection) { + defer c.Close() + + var b [1024]byte + c.SetReadDeadline(time.Now().Add(2 * time.Second)) + _, err := c.Read(b[:]) + if err != nil { + return + } + + common.Must2(c.Write([]byte("Response"))) + }(conn) + }) + common.Must(err) + ctx := context.Background() + streamSettings := &internet.MemoryStreamConfig{ + ProtocolName: "splithttp", + ProtocolSettings: &Config{Path: "sh?ed=2048"}, + } + conn, err := Dial(ctx, net.TCPDestination(net.DomainAddress("localhost"), listenPort), streamSettings) + + common.Must(err) + _, err = conn.Write([]byte("Test connection 1")) + common.Must(err) + + var b [1024]byte + fmt.Println("test2") + n, _ := io.ReadFull(conn, b[:]) + fmt.Println("string is", n) + if string(b[:n]) != "Response" { + t.Error("response: ", string(b[:n])) + } + + common.Must(conn.Close()) + common.Must(listen.Close()) +} + +func Test_maxUpload(t *testing.T) { + listenPort := tcp.PickPort() + streamSettings := &internet.MemoryStreamConfig{ + ProtocolName: "splithttp", + ProtocolSettings: &Config{ + Path: "/sh", + ScMaxEachPostBytes: &RangeConfig{ + From: 10000, + To: 10000, + }, + }, + } + + uploadReceived := make([]byte, 10001) + listen, err := ListenXH(context.Background(), net.LocalHostIP, listenPort, streamSettings, func(conn stat.Connection) { + go func(c stat.Connection) { + defer c.Close() + c.SetReadDeadline(time.Now().Add(2 * time.Second)) + io.ReadFull(c, uploadReceived) + + common.Must2(c.Write([]byte("Response"))) + }(conn) + }) + common.Must(err) + ctx := context.Background() + + conn, err := Dial(ctx, net.TCPDestination(net.DomainAddress("localhost"), listenPort), streamSettings) + common.Must(err) + + // send a slightly too large upload + upload := make([]byte, 10001) + rand.Read(upload) + _, err = conn.Write(upload) + common.Must(err) + + var b [10240]byte + n, _ := io.ReadFull(conn, b[:]) + fmt.Println("string is", n) + if string(b[:n]) != "Response" { + t.Error("response: ", string(b[:n])) + } + common.Must(conn.Close()) + + if !bytes.Equal(upload, uploadReceived) { + t.Error("incorrect upload", upload, uploadReceived) + } + + common.Must(listen.Close()) +} diff --git a/subproject/Xray-core-main/transport/internet/splithttp/upload_queue.go b/subproject/Xray-core-main/transport/internet/splithttp/upload_queue.go new file mode 100644 index 00000000..69b9a972 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/splithttp/upload_queue.go @@ -0,0 +1,164 @@ +package splithttp + +// upload_queue is a specialized priorityqueue + channel to reorder generic +// packets by a sequence number + +import ( + "container/heap" + "io" + "runtime" + "sync" + + "github.com/xtls/xray-core/common/errors" +) + +type Packet struct { + Reader io.ReadCloser + Payload []byte + Seq uint64 +} + +type uploadQueue struct { + reader io.ReadCloser + nomore bool + pushedPackets chan Packet + writeCloseMutex sync.Mutex + heap uploadHeap + nextSeq uint64 + closed bool + maxPackets int +} + +func NewUploadQueue(maxPackets int) *uploadQueue { + return &uploadQueue{ + pushedPackets: make(chan Packet, maxPackets), + heap: uploadHeap{}, + nextSeq: 0, + closed: false, + maxPackets: maxPackets, + } +} + +func (h *uploadQueue) Push(p Packet) error { + h.writeCloseMutex.Lock() + defer h.writeCloseMutex.Unlock() + + if h.closed { + return errors.New("packet queue closed") + } + if h.nomore { + return errors.New("h.reader already exists") + } + if p.Reader != nil { + h.nomore = true + } + h.pushedPackets <- p + return nil +} + +func (h *uploadQueue) Close() error { + h.writeCloseMutex.Lock() + defer h.writeCloseMutex.Unlock() + + if !h.closed { + h.closed = true + runtime.Gosched() // hope Read() gets the packet + f: + for { + select { + case p := <-h.pushedPackets: + if p.Reader != nil { + h.reader = p.Reader + } + default: + break f + } + } + close(h.pushedPackets) + } + if h.reader != nil { + return h.reader.Close() + } + return nil +} + +func (h *uploadQueue) Read(b []byte) (int, error) { + if h.reader != nil { + return h.reader.Read(b) + } + + if h.closed { + return 0, io.EOF + } + + if len(h.heap) == 0 { + packet, more := <-h.pushedPackets + if !more { + return 0, io.EOF + } + if packet.Reader != nil { + h.reader = packet.Reader + return h.reader.Read(b) + } + heap.Push(&h.heap, packet) + } + + for len(h.heap) > 0 { + packet := heap.Pop(&h.heap).(Packet) + n := 0 + + if packet.Seq == h.nextSeq { + copy(b, packet.Payload) + n = min(len(b), len(packet.Payload)) + + if n < len(packet.Payload) { + // partial read + packet.Payload = packet.Payload[n:] + heap.Push(&h.heap, packet) + } else { + h.nextSeq = packet.Seq + 1 + } + + return n, nil + } + + // misordered packet + if packet.Seq > h.nextSeq { + if len(h.heap) > h.maxPackets { + // the "reassembly buffer" is too large, and we want to + // constrain memory usage somehow. let's tear down the + // connection, and hope the application retries. + return 0, errors.New("packet queue is too large") + } + heap.Push(&h.heap, packet) + packet2, more := <-h.pushedPackets + if !more { + return 0, io.EOF + } + heap.Push(&h.heap, packet2) + } + } + + return 0, nil +} + +// heap code directly taken from https://pkg.go.dev/container/heap +type uploadHeap []Packet + +func (h uploadHeap) Len() int { return len(h) } +func (h uploadHeap) Less(i, j int) bool { return h[i].Seq < h[j].Seq } +func (h uploadHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] } + +func (h *uploadHeap) Push(x any) { + // Push and Pop use pointer receivers because they modify the slice's length, + // not just its contents. + *h = append(*h, x.(Packet)) +} + +func (h *uploadHeap) Pop() any { + old := *h + n := len(old) + x := old[n-1] + *h = old[0 : n-1] + return x +} diff --git a/subproject/Xray-core-main/transport/internet/splithttp/upload_queue_test.go b/subproject/Xray-core-main/transport/internet/splithttp/upload_queue_test.go new file mode 100644 index 00000000..8185cd8f --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/splithttp/upload_queue_test.go @@ -0,0 +1,22 @@ +package splithttp_test + +import ( + "testing" + + "github.com/xtls/xray-core/common" + . "github.com/xtls/xray-core/transport/internet/splithttp" +) + +func Test_regression_readzero(t *testing.T) { + q := NewUploadQueue(10) + q.Push(Packet{ + Payload: []byte("x"), + Seq: 0, + }) + buf := make([]byte, 20) + n, err := q.Read(buf) + common.Must(err) + if n != 1 { + t.Error("n=", n) + } +} diff --git a/subproject/Xray-core-main/transport/internet/splithttp/xpadding.go b/subproject/Xray-core-main/transport/internet/splithttp/xpadding.go new file mode 100644 index 00000000..1dcbc3e2 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/splithttp/xpadding.go @@ -0,0 +1,334 @@ +package splithttp + +import ( + "crypto/rand" + "math" + "net/http" + "net/url" + "strings" + + "golang.org/x/net/http2/hpack" +) + +type PaddingMethod string + +const ( + PaddingMethodRepeatX PaddingMethod = "repeat-x" + PaddingMethodTokenish PaddingMethod = "tokenish" +) + +const charsetBase62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + +// Huffman encoding gives ~20% size reduction for base62 sequences +const avgHuffmanBytesPerCharBase62 = 0.8 + +const validationTolerance = 2 + +type XPaddingPlacement struct { + Placement string + Key string + Header string + RawURL string +} + +type XPaddingConfig struct { + Length int + Placement XPaddingPlacement + Method PaddingMethod +} + +func randStringFromCharset(n int, charset string) (string, bool) { + if n <= 0 || len(charset) == 0 { + return "", false + } + + m := len(charset) + limit := byte(256 - (256 % m)) + + result := make([]byte, n) + i := 0 + + buf := make([]byte, 256) + for i < n { + if _, err := rand.Read(buf); err != nil { + return "", false + } + for _, rb := range buf { + if rb >= limit { + continue + } + result[i] = charset[int(rb)%m] + i++ + if i == n { + break + } + } + } + + return string(result), true +} + +func absInt(x int) int { + if x < 0 { + return -x + } + return x +} + +func GenerateTokenishPaddingBase62(targetHuffmanBytes int) string { + n := int(math.Ceil(float64(targetHuffmanBytes) / avgHuffmanBytesPerCharBase62)) + if n < 1 { + n = 1 + } + + randBase62Str, ok := randStringFromCharset(n, charsetBase62) + if !ok { + return "" + } + + const maxIter = 150 + adjustChar := byte('X') + + // Adjust until close enough + for iter := 0; iter < maxIter; iter++ { + currentLength := int(hpack.HuffmanEncodeLength(randBase62Str)) + diff := currentLength - targetHuffmanBytes + + if absInt(diff) <= validationTolerance { + return randBase62Str + } + + if diff < 0 { + // Too small -> append padding char(s) + randBase62Str += string(adjustChar) + + // Avoid a long run of identical chars + if adjustChar == 'X' { + adjustChar = 'Z' + } else { + adjustChar = 'X' + } + } else { + // Too big -> remove from the end + if len(randBase62Str) <= 1 { + return randBase62Str + } + randBase62Str = randBase62Str[:len(randBase62Str)-1] + } + } + + return randBase62Str +} + +func GeneratePadding(method PaddingMethod, length int) string { + if length <= 0 { + return "" + } + + // https://www.rfc-editor.org/rfc/rfc7541.html#appendix-B + // h2's HPACK Header Compression feature employs a huffman encoding using a static table. + // 'X' and 'Z' are assigned an 8 bit code, so HPACK compression won't change actual padding length on the wire. + // https://www.rfc-editor.org/rfc/rfc9204.html#section-4.1.2-2 + // h3's similar QPACK feature uses the same huffman table. + + switch method { + case PaddingMethodRepeatX: + return strings.Repeat("X", length) + case PaddingMethodTokenish: + paddingValue := GenerateTokenishPaddingBase62(length) + if paddingValue == "" { + return strings.Repeat("X", length) + } + return paddingValue + default: + return strings.Repeat("X", length) + } +} + +func ApplyPaddingToCookie(req *http.Request, name, value string) { + if req == nil || name == "" || value == "" { + return + } + req.AddCookie(&http.Cookie{ + Name: name, + Value: value, + Path: "/", + }) +} + +func ApplyPaddingToResponseCookie(writer http.ResponseWriter, name, value string) { + if name == "" || value == "" { + return + } + http.SetCookie(writer, &http.Cookie{ + Name: name, + Value: value, + Path: "/", + }) +} + +func ApplyPaddingToQuery(u *url.URL, key, value string) { + if u == nil || key == "" || value == "" { + return + } + q := u.Query() + q.Set(key, value) + u.RawQuery = q.Encode() +} + +func (c *Config) GetNormalizedXPaddingBytes() RangeConfig { + if c.XPaddingBytes == nil || c.XPaddingBytes.To == 0 { + return RangeConfig{ + From: 100, + To: 1000, + } + } + + return *c.XPaddingBytes +} + +func (c *Config) ApplyXPaddingToHeader(h http.Header, config XPaddingConfig) { + if h == nil { + return + } + + paddingValue := GeneratePadding(config.Method, config.Length) + + switch p := config.Placement; p.Placement { + case PlacementHeader: + h.Set(p.Header, paddingValue) + case PlacementQueryInHeader: + u, err := url.Parse(p.RawURL) + if err != nil || u == nil { + return + } + u.RawQuery = p.Key + "=" + paddingValue + h.Set(p.Header, u.String()) + } +} + +func (c *Config) ApplyXPaddingToRequest(req *http.Request, config XPaddingConfig) { + if req == nil { + return + } + if req.Header == nil { + req.Header = make(http.Header) + } + + placement := config.Placement.Placement + + if placement == PlacementHeader || placement == PlacementQueryInHeader { + c.ApplyXPaddingToHeader(req.Header, config) + return + } + + paddingValue := GeneratePadding(config.Method, config.Length) + + switch placement { + case PlacementCookie: + ApplyPaddingToCookie(req, config.Placement.Key, paddingValue) + case PlacementQuery: + ApplyPaddingToQuery(req.URL, config.Placement.Key, paddingValue) + } +} + +func (c *Config) ApplyXPaddingToResponse(writer http.ResponseWriter, config XPaddingConfig) { + placement := config.Placement.Placement + + if placement == PlacementHeader || placement == PlacementQueryInHeader { + c.ApplyXPaddingToHeader(writer.Header(), config) + return + } + + paddingValue := GeneratePadding(config.Method, config.Length) + + switch placement { + case PlacementCookie: + ApplyPaddingToResponseCookie(writer, config.Placement.Key, paddingValue) + } +} + +func (c *Config) ExtractXPaddingFromRequest(req *http.Request, obfsMode bool) (string, string) { + if req == nil { + return "", "" + } + + if !obfsMode { + referrer := req.Header.Get("Referer") + + if referrer != "" { + if referrerURL, err := url.Parse(referrer); err == nil { + paddingValue := referrerURL.Query().Get("x_padding") + paddingPlacement := PlacementQueryInHeader + "=Referer, key=x_padding" + return paddingValue, paddingPlacement + } + } else { + paddingValue := req.URL.Query().Get("x_padding") + return paddingValue, PlacementQuery + ", key=x_padding" + } + } + + key := c.XPaddingKey + header := c.XPaddingHeader + + if cookie, err := req.Cookie(key); err == nil { + if cookie != nil && cookie.Value != "" { + paddingValue := cookie.Value + paddingPlacement := PlacementCookie + ", key=" + key + return paddingValue, paddingPlacement + } + } + + headerValue := req.Header.Get(header) + + if headerValue != "" { + if c.XPaddingPlacement == PlacementHeader { + paddingPlacement := PlacementHeader + "=" + header + return headerValue, paddingPlacement + } + + if parsedURL, err := url.Parse(headerValue); err == nil { + paddingPlacement := PlacementQueryInHeader + "=" + header + ", key=" + key + + return parsedURL.Query().Get(key), paddingPlacement + } + } + + queryValue := req.URL.Query().Get(key) + + if queryValue != "" { + paddingPlacement := PlacementQuery + ", key=" + key + return queryValue, paddingPlacement + } + + return "", "" +} + +func (c *Config) IsPaddingValid(paddingValue string, from, to int32, method PaddingMethod) bool { + if paddingValue == "" { + return false + } + if to <= 0 { + r := c.GetNormalizedXPaddingBytes() + from, to = r.From, r.To + } + + switch method { + case PaddingMethodRepeatX: + n := int32(len(paddingValue)) + return n >= from && n <= to + case PaddingMethodTokenish: + const tolerance = int32(validationTolerance) + + n := int32(hpack.HuffmanEncodeLength(paddingValue)) + f := from - tolerance + t := to + tolerance + if f < 0 { + f = 0 + } + return n >= f && n <= t + default: + n := int32(len(paddingValue)) + return n >= from && n <= to + } +} diff --git a/subproject/Xray-core-main/transport/internet/stat/connection.go b/subproject/Xray-core-main/transport/internet/stat/connection.go new file mode 100644 index 00000000..b039b29c --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/stat/connection.go @@ -0,0 +1,44 @@ +package stat + +import ( + "net" + + "github.com/xtls/xray-core/features/stats" +) + +type Connection interface { + net.Conn +} + +type CounterConnection struct { + Connection + ReadCounter stats.Counter + WriteCounter stats.Counter +} + +func (c *CounterConnection) Read(b []byte) (int, error) { + nBytes, err := c.Connection.Read(b) + if c.ReadCounter != nil { + c.ReadCounter.Add(int64(nBytes)) + } + + return nBytes, err +} + +func (c *CounterConnection) Write(b []byte) (int, error) { + nBytes, err := c.Connection.Write(b) + if c.WriteCounter != nil { + c.WriteCounter.Add(int64(nBytes)) + } + return nBytes, err +} + +func TryUnwrapStatsConn(conn net.Conn) net.Conn { + if conn == nil { + return conn + } + if conn, ok := conn.(*CounterConnection); ok { + return conn.Connection + } + return conn +} diff --git a/subproject/Xray-core-main/transport/internet/system_dialer.go b/subproject/Xray-core-main/transport/internet/system_dialer.go new file mode 100644 index 00000000..2d604481 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/system_dialer.go @@ -0,0 +1,245 @@ +package internet + +import ( + "context" + "syscall" + "time" + + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/features/dns" + "github.com/xtls/xray-core/features/outbound" +) + +var effectiveSystemDialer SystemDialer = &DefaultSystemDialer{} + +type SystemDialer interface { + Dial(ctx context.Context, source net.Address, destination net.Destination, sockopt *SocketConfig) (net.Conn, error) + DestIpAddress() net.IP +} + +type DefaultSystemDialer struct { + controllers []func(network, address string, c syscall.RawConn) error + dns dns.Client + obm outbound.Manager +} + +func resolveSrcAddr(network net.Network, src net.Address) net.Addr { + if src == nil || src == net.AnyIP { + return nil + } + + if network == net.Network_TCP { + return &net.TCPAddr{ + IP: src.IP(), + Port: 0, + } + } + + return &net.UDPAddr{ + IP: src.IP(), + Port: 0, + } +} + +func hasBindAddr(sockopt *SocketConfig) bool { + return sockopt != nil && len(sockopt.BindAddress) > 0 && sockopt.BindPort > 0 +} + +func (d *DefaultSystemDialer) Dial(ctx context.Context, src net.Address, dest net.Destination, sockopt *SocketConfig) (net.Conn, error) { + errors.LogDebug(ctx, "dialing to "+dest.String()) + + if dest.Network == net.Network_UDP && !hasBindAddr(sockopt) { + srcAddr := resolveSrcAddr(net.Network_UDP, src) + if srcAddr == nil { + srcAddr = &net.UDPAddr{ + IP: []byte{0, 0, 0, 0}, + Port: 0, + } + } + var lc net.ListenConfig + destAddr, err := net.ResolveUDPAddr("udp", dest.NetAddr()) + if err != nil { + return nil, err + } + lc.Control = func(network, address string, c syscall.RawConn) error { + for _, ctl := range d.controllers { + if err := ctl(network, address, c); err != nil { + errors.LogInfoInner(ctx, err, "failed to apply external controller") + } + } + return c.Control(func(fd uintptr) { + if sockopt != nil { + if err := applyOutboundSocketOptions(network, destAddr.String(), fd, sockopt); err != nil { + errors.LogInfo(ctx, err, "failed to apply socket options") + } + } + }) + } + packetConn, err := lc.ListenPacket(ctx, srcAddr.Network(), srcAddr.String()) + if err != nil { + return nil, err + } + return &PacketConnWrapper{ + PacketConn: packetConn, + Dest: destAddr, + }, nil + } + // Chrome defaults + keepAliveConfig := net.KeepAliveConfig{ + Enable: true, + Idle: 45 * time.Second, + Interval: 45 * time.Second, + Count: -1, + } + keepAlive := time.Duration(0) + if sockopt != nil { + if sockopt.TcpKeepAliveIdle*sockopt.TcpKeepAliveInterval < 0 { + return nil, errors.New("invalid TcpKeepAliveIdle or TcpKeepAliveInterval value: ", sockopt.TcpKeepAliveIdle, " ", sockopt.TcpKeepAliveInterval) + } + if sockopt.TcpKeepAliveIdle < 0 || sockopt.TcpKeepAliveInterval < 0 { + keepAlive = -1 + keepAliveConfig.Enable = false + } + if sockopt.TcpKeepAliveIdle > 0 { + keepAliveConfig.Idle = time.Duration(sockopt.TcpKeepAliveIdle) * time.Second + } + if sockopt.TcpKeepAliveInterval > 0 { + keepAliveConfig.Interval = time.Duration(sockopt.TcpKeepAliveInterval) * time.Second + } + } + dialer := &net.Dialer{ + Timeout: time.Second * 16, + LocalAddr: resolveSrcAddr(dest.Network, src), + KeepAlive: keepAlive, + KeepAliveConfig: keepAliveConfig, + } + + if sockopt != nil || len(d.controllers) > 0 { + if sockopt != nil && sockopt.TcpMptcp { + dialer.SetMultipathTCP(true) + } + dialer.Control = func(network, address string, c syscall.RawConn) error { + for _, ctl := range d.controllers { + if err := ctl(network, address, c); err != nil { + errors.LogInfoInner(ctx, err, "failed to apply external controller") + } + } + return c.Control(func(fd uintptr) { + if sockopt != nil { + if err := applyOutboundSocketOptions(network, address, fd, sockopt); err != nil { + errors.LogInfoInner(ctx, err, "failed to apply socket options") + } + if dest.Network == net.Network_UDP && hasBindAddr(sockopt) { + if err := bindAddr(fd, sockopt.BindAddress, sockopt.BindPort); err != nil { + errors.LogInfoInner(ctx, err, "failed to bind source address to ", sockopt.BindAddress) + } + } + } + }) + } + } + + return dialer.DialContext(ctx, dest.Network.SystemString(), dest.NetAddr()) +} + +func (d *DefaultSystemDialer) DestIpAddress() net.IP { + return nil +} + +type PacketConnWrapper struct { + net.PacketConn + Dest net.Addr +} + +func (c *PacketConnWrapper) Read(p []byte) (int, error) { + n, _, err := c.PacketConn.ReadFrom(p) + return n, err +} + +func (c *PacketConnWrapper) Write(p []byte) (int, error) { + return c.PacketConn.WriteTo(p, c.Dest) +} + +func (c *PacketConnWrapper) RemoteAddr() net.Addr { + return c.Dest +} + +type SystemDialerAdapter interface { + Dial(network string, address string) (net.Conn, error) +} + +type SimpleSystemDialer struct { + adapter SystemDialerAdapter +} + +func WithAdapter(dialer SystemDialerAdapter) SystemDialer { + return &SimpleSystemDialer{ + adapter: dialer, + } +} + +func (v *SimpleSystemDialer) Dial(ctx context.Context, src net.Address, dest net.Destination, sockopt *SocketConfig) (net.Conn, error) { + return v.adapter.Dial(dest.Network.SystemString(), dest.NetAddr()) +} + +func (d *SimpleSystemDialer) DestIpAddress() net.IP { + return nil +} + +// UseAlternativeSystemDialer replaces the current system dialer with a given one. +// Caller must ensure there is no race condition. +// +// xray:api:stable +func UseAlternativeSystemDialer(dialer SystemDialer) { + if dialer == nil { + dialer = &DefaultSystemDialer{} + } + effectiveSystemDialer = dialer +} + +// RegisterDialerController adds a controller to the effective system dialer. +// The controller can be used to operate on file descriptors before they are put into use. +// It only works when effective dialer is the default dialer. +// +// xray:api:beta +func RegisterDialerController(ctl func(network, address string, c syscall.RawConn) error) error { + if ctl == nil { + return errors.New("nil listener controller") + } + + dialer, ok := effectiveSystemDialer.(*DefaultSystemDialer) + if !ok { + return errors.New("RegisterListenerController not supported in custom dialer") + } + + dialer.controllers = append(dialer.controllers, ctl) + return nil +} + +type FakePacketConn struct { + net.Conn +} + +func (c *FakePacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { + n, err = c.Read(p) + return n, c.RemoteAddr(), err +} + +func (c *FakePacketConn) WriteTo(p []byte, _ net.Addr) (n int, err error) { + return c.Write(p) +} + +func (c *FakePacketConn) LocalAddr() net.Addr { + return &net.UDPAddr{ + IP: []byte{0, 0, 0, 0}, + Port: 0, + } +} + +func (c *FakePacketConn) RemoteAddr() net.Addr { + return &net.UDPAddr{ + IP: []byte{0, 0, 0, 0}, + Port: 0, + } +} diff --git a/subproject/Xray-core-main/transport/internet/system_listener.go b/subproject/Xray-core-main/transport/internet/system_listener.go new file mode 100644 index 00000000..1999953e --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/system_listener.go @@ -0,0 +1,195 @@ +package internet + +import ( + "context" + "os" + "runtime" + "strconv" + "strings" + "syscall" + "time" + + "github.com/pires/go-proxyproto" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" +) + +var effectiveListener = DefaultListener{} + +type DefaultListener struct { + controllers []func(network, address string, c syscall.RawConn) error +} + +func getControlFunc(ctx context.Context, sockopt *SocketConfig, controllers []func(network, address string, c syscall.RawConn) error) func(network, address string, c syscall.RawConn) error { + return func(network, address string, c syscall.RawConn) error { + return c.Control(func(fd uintptr) { + for _, controller := range controllers { + if err := controller(network, address, c); err != nil { + errors.LogInfoInner(ctx, err, "failed to apply external controller") + } + } + + if sockopt != nil { + if err := applyInboundSocketOptions(network, fd, sockopt); err != nil { + errors.LogInfoInner(ctx, err, "failed to apply socket options to incoming connection") + } + } + + setReusePort(fd) + }) + } +} + +// For some reason, other component of ray will assume the listener is a TCP listener and have valid remote address. +// But in fact it doesn't. So we need to wrap the listener to make it return 0.0.0.0(unspecified) as remote address. +// If other issues encountered, we should able to fix it here. +type UnixListenerWrapper struct { + *net.UnixListener + locker *FileLocker +} + +func (l *UnixListenerWrapper) Accept() (net.Conn, error) { + conn, err := l.UnixListener.Accept() + if err != nil { + return nil, err + } + return &UnixConnWrapper{UnixConn: conn.(*net.UnixConn)}, nil +} + +func (l *UnixListenerWrapper) Close() error { + if l.locker != nil { + l.locker.Release() + l.locker = nil + } + return l.UnixListener.Close() +} + +type UnixConnWrapper struct { + *net.UnixConn +} + +func (conn *UnixConnWrapper) RemoteAddr() net.Addr { + return &net.TCPAddr{ + IP: []byte{0, 0, 0, 0}, + } +} + +func (dl *DefaultListener) Listen(ctx context.Context, addr net.Addr, sockopt *SocketConfig) (l net.Listener, err error) { + var lc net.ListenConfig + var network, address string + // callback is called after the Listen function returns + callback := func(l net.Listener, err error) (net.Listener, error) { + return l, err + } + + switch addr := addr.(type) { + case *net.TCPAddr: + network = addr.Network() + address = addr.String() + lc.Control = getControlFunc(ctx, sockopt, dl.controllers) + // default disable keepalive + lc.KeepAlive = -1 + if sockopt != nil { + if sockopt.TcpKeepAliveIdle*sockopt.TcpKeepAliveInterval < 0 { + return nil, errors.New("invalid TcpKeepAliveIdle or TcpKeepAliveInterval value: ", sockopt.TcpKeepAliveIdle, " ", sockopt.TcpKeepAliveInterval) + } + lc.KeepAliveConfig = net.KeepAliveConfig{ + Enable: false, + Idle: -1, + Interval: -1, + Count: -1, + } + if sockopt.TcpKeepAliveIdle > 0 { + lc.KeepAliveConfig.Enable = true + lc.KeepAliveConfig.Idle = time.Duration(sockopt.TcpKeepAliveIdle) * time.Second + } + if sockopt.TcpKeepAliveInterval > 0 { + lc.KeepAliveConfig.Enable = true + lc.KeepAliveConfig.Interval = time.Duration(sockopt.TcpKeepAliveInterval) * time.Second + } + if sockopt.TcpMptcp { + lc.SetMultipathTCP(true) + } + } + case *net.UnixAddr: + lc.Control = nil + network = addr.Network() + address = addr.Name + + if (runtime.GOOS == "linux" || runtime.GOOS == "android") && address[0] == '@' { + // linux abstract unix domain socket is lockfree + if len(address) > 1 && address[1] == '@' { + // but may need padding to work with haproxy + fullAddr := make([]byte, len(syscall.RawSockaddrUnix{}.Path)) + copy(fullAddr, address[1:]) + address = string(fullAddr) + } + } else { + // split permission from address + var filePerm *os.FileMode + if s := strings.Split(address, ","); len(s) == 2 { + address = s[0] + perm, perr := strconv.ParseUint(s[1], 8, 32) + if perr != nil { + return nil, errors.New("failed to parse permission: " + s[1]).Base(perr) + } + + mode := os.FileMode(perm) + filePerm = &mode + } + // normal unix domain socket needs lock + locker := &FileLocker{ + path: address + ".lock", + } + if err := locker.Acquire(); err != nil { + return nil, err + } + + // set callback to combine listener and set permission + callback = func(l net.Listener, err error) (net.Listener, error) { + if err != nil { + locker.Release() + return nil, err + } + l = &UnixListenerWrapper{UnixListener: l.(*net.UnixListener), locker: locker} + if filePerm == nil { + return l, nil + } + err = os.Chmod(address, *filePerm) + if err != nil { + l.Close() + return nil, errors.New("failed to set permission for " + address).Base(err) + } + return l, nil + } + } + } + + l, err = callback(lc.Listen(ctx, network, address)) + if err == nil && sockopt != nil && sockopt.AcceptProxyProtocol { + policyFunc := func(upstream net.Addr) (proxyproto.Policy, error) { return proxyproto.REQUIRE, nil } + l = &proxyproto.Listener{Listener: l, Policy: policyFunc} + } + return l, err +} + +func (dl *DefaultListener) ListenPacket(ctx context.Context, addr net.Addr, sockopt *SocketConfig) (net.PacketConn, error) { + var lc net.ListenConfig + + lc.Control = getControlFunc(ctx, sockopt, dl.controllers) + + return lc.ListenPacket(ctx, addr.Network(), addr.String()) +} + +// RegisterListenerController adds a controller to the effective system listener. +// The controller can be used to operate on file descriptors before they are put into use. +// +// xray:api:beta +func RegisterListenerController(controller func(network, address string, c syscall.RawConn) error) error { + if controller == nil { + return errors.New("nil listener controller") + } + + effectiveListener.controllers = append(effectiveListener.controllers, controller) + return nil +} diff --git a/subproject/Xray-core-main/transport/internet/system_listener_test.go b/subproject/Xray-core-main/transport/internet/system_listener_test.go new file mode 100644 index 00000000..b80fdfa8 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/system_listener_test.go @@ -0,0 +1,31 @@ +package internet_test + +import ( + "context" + "net" + "syscall" + "testing" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/transport/internet" +) + +func TestRegisterListenerController(t *testing.T) { + var gotFd uintptr + + common.Must(internet.RegisterListenerController(func(network, address string, c syscall.RawConn) error { + return c.Control(func(fd uintptr) { + gotFd = fd + }) + })) + + conn, err := internet.ListenSystemPacket(context.Background(), &net.UDPAddr{ + IP: net.IPv4zero, + }, nil) + common.Must(err) + common.Must(conn.Close()) + + if gotFd == 0 { + t.Error("expected none-zero fd, but actually 0") + } +} diff --git a/subproject/Xray-core-main/transport/internet/tagged/tagged.go b/subproject/Xray-core-main/transport/internet/tagged/tagged.go new file mode 100644 index 00000000..2cd9dcd2 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/tagged/tagged.go @@ -0,0 +1,12 @@ +package tagged + +import ( + "context" + + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/features/routing" +) + +type DialFunc func(ctx context.Context, dispatcher routing.Dispatcher, dest net.Destination, tag string) (net.Conn, error) + +var Dialer DialFunc diff --git a/subproject/Xray-core-main/transport/internet/tagged/taggedimpl/impl.go b/subproject/Xray-core-main/transport/internet/tagged/taggedimpl/impl.go new file mode 100644 index 00000000..2a773401 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/tagged/taggedimpl/impl.go @@ -0,0 +1,40 @@ +package taggedimpl + +import ( + "context" + + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/net/cnc" + "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/features/routing" + "github.com/xtls/xray-core/transport/internet/tagged" +) + +func DialTaggedOutbound(ctx context.Context, dispatcher routing.Dispatcher, dest net.Destination, tag string) (net.Conn, error) { + if core.FromContext(ctx) == nil { + return nil, errors.New("Instance context variable is not in context, dial denied. ") + } + content := new(session.Content) + content.SkipDNSResolve = true + + ctx = session.ContextWithContent(ctx, content) + ctx = session.SetForcedOutboundTagToContext(ctx, tag) + + r, err := dispatcher.Dispatch(ctx, dest) + if err != nil { + return nil, err + } + var readerOpt cnc.ConnectionOption + if dest.Network == net.Network_TCP { + readerOpt = cnc.ConnectionOutputMulti(r.Reader) + } else { + readerOpt = cnc.ConnectionOutputMultiUDP(r.Reader) + } + return cnc.NewConnection(cnc.ConnectionInputMulti(r.Writer), readerOpt), nil +} + +func init() { + tagged.Dialer = DialTaggedOutbound +} diff --git a/subproject/Xray-core-main/transport/internet/tagged/taggedimpl/taggedimpl.go b/subproject/Xray-core-main/transport/internet/tagged/taggedimpl/taggedimpl.go new file mode 100644 index 00000000..3116922e --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/tagged/taggedimpl/taggedimpl.go @@ -0,0 +1 @@ +package taggedimpl diff --git a/subproject/Xray-core-main/transport/internet/tcp/config.go b/subproject/Xray-core-main/transport/internet/tcp/config.go new file mode 100644 index 00000000..e8851d0e --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/tcp/config.go @@ -0,0 +1,12 @@ +package tcp + +import ( + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/transport/internet" +) + +func init() { + common.Must(internet.RegisterProtocolConfigCreator(protocolName, func() interface{} { + return new(Config) + })) +} diff --git a/subproject/Xray-core-main/transport/internet/tcp/config.pb.go b/subproject/Xray-core-main/transport/internet/tcp/config.pb.go new file mode 100644 index 00000000..f773f63b --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/tcp/config.pb.go @@ -0,0 +1,135 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: transport/internet/tcp/config.proto + +package tcp + +import ( + serial "github.com/xtls/xray-core/common/serial" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + HeaderSettings *serial.TypedMessage `protobuf:"bytes,2,opt,name=header_settings,json=headerSettings,proto3" json:"header_settings,omitempty"` + AcceptProxyProtocol bool `protobuf:"varint,3,opt,name=accept_proxy_protocol,json=acceptProxyProtocol,proto3" json:"accept_proxy_protocol,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_transport_internet_tcp_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_tcp_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_transport_internet_tcp_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Config) GetHeaderSettings() *serial.TypedMessage { + if x != nil { + return x.HeaderSettings + } + return nil +} + +func (x *Config) GetAcceptProxyProtocol() bool { + if x != nil { + return x.AcceptProxyProtocol + } + return false +} + +var File_transport_internet_tcp_config_proto protoreflect.FileDescriptor + +const file_transport_internet_tcp_config_proto_rawDesc = "" + + "\n" + + "#transport/internet/tcp/config.proto\x12\x1bxray.transport.internet.tcp\x1a!common/serial/typed_message.proto\"\x8d\x01\n" + + "\x06Config\x12I\n" + + "\x0fheader_settings\x18\x02 \x01(\v2 .xray.common.serial.TypedMessageR\x0eheaderSettings\x122\n" + + "\x15accept_proxy_protocol\x18\x03 \x01(\bR\x13acceptProxyProtocolJ\x04\b\x01\x10\x02Bs\n" + + "\x1fcom.xray.transport.internet.tcpP\x01Z0github.com/xtls/xray-core/transport/internet/tcp\xaa\x02\x1bXray.Transport.Internet.Tcpb\x06proto3" + +var ( + file_transport_internet_tcp_config_proto_rawDescOnce sync.Once + file_transport_internet_tcp_config_proto_rawDescData []byte +) + +func file_transport_internet_tcp_config_proto_rawDescGZIP() []byte { + file_transport_internet_tcp_config_proto_rawDescOnce.Do(func() { + file_transport_internet_tcp_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_transport_internet_tcp_config_proto_rawDesc), len(file_transport_internet_tcp_config_proto_rawDesc))) + }) + return file_transport_internet_tcp_config_proto_rawDescData +} + +var file_transport_internet_tcp_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_transport_internet_tcp_config_proto_goTypes = []any{ + (*Config)(nil), // 0: xray.transport.internet.tcp.Config + (*serial.TypedMessage)(nil), // 1: xray.common.serial.TypedMessage +} +var file_transport_internet_tcp_config_proto_depIdxs = []int32{ + 1, // 0: xray.transport.internet.tcp.Config.header_settings:type_name -> xray.common.serial.TypedMessage + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_transport_internet_tcp_config_proto_init() } +func file_transport_internet_tcp_config_proto_init() { + if File_transport_internet_tcp_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_tcp_config_proto_rawDesc), len(file_transport_internet_tcp_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_transport_internet_tcp_config_proto_goTypes, + DependencyIndexes: file_transport_internet_tcp_config_proto_depIdxs, + MessageInfos: file_transport_internet_tcp_config_proto_msgTypes, + }.Build() + File_transport_internet_tcp_config_proto = out.File + file_transport_internet_tcp_config_proto_goTypes = nil + file_transport_internet_tcp_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/transport/internet/tcp/config.proto b/subproject/Xray-core-main/transport/internet/tcp/config.proto new file mode 100644 index 00000000..a12acdfa --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/tcp/config.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; + +package xray.transport.internet.tcp; +option csharp_namespace = "Xray.Transport.Internet.Tcp"; +option go_package = "github.com/xtls/xray-core/transport/internet/tcp"; +option java_package = "com.xray.transport.internet.tcp"; +option java_multiple_files = true; + +import "common/serial/typed_message.proto"; + +message Config { + reserved 1; + xray.common.serial.TypedMessage header_settings = 2; + bool accept_proxy_protocol = 3; +} diff --git a/subproject/Xray-core-main/transport/internet/tcp/dialer.go b/subproject/Xray-core-main/transport/internet/tcp/dialer.go new file mode 100644 index 00000000..92fa7557 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/tcp/dialer.go @@ -0,0 +1,121 @@ +package tcp + +import ( + "context" + gotls "crypto/tls" + "slices" + "strings" + + "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/session" + "github.com/xtls/xray-core/transport/internet" + "github.com/xtls/xray-core/transport/internet/reality" + "github.com/xtls/xray-core/transport/internet/stat" + "github.com/xtls/xray-core/transport/internet/tls" +) + +// Dial dials a new TCP connection to the given destination. +func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.MemoryStreamConfig) (stat.Connection, error) { + errors.LogInfo(ctx, "dialing TCP to ", dest) + conn, err := internet.DialSystem(ctx, dest, streamSettings.SocketSettings) + if err != nil { + return nil, err + } + + if streamSettings.TcpmaskManager != nil { + newConn, err := streamSettings.TcpmaskManager.WrapConnClient(conn) + if err != nil { + conn.Close() + return nil, errors.New("mask err").Base(err) + } + conn = newConn + } + + if config := tls.ConfigFromStreamSettings(streamSettings); config != nil { + mitmServerName := session.MitmServerNameFromContext(ctx) + mitmAlpn11 := session.MitmAlpn11FromContext(ctx) + var tlsConfig *gotls.Config + if tls.IsFromMitm(config.ServerName) { + tlsConfig = config.GetTLSConfig(tls.WithOverrideName(mitmServerName)) + } else { + tlsConfig = config.GetTLSConfig(tls.WithDestination(dest)) + } + + isFromMitmVerify := false + if r, ok := tlsConfig.Rand.(*tls.RandCarrier); ok && len(r.VerifyPeerCertByName) > 0 { + for i, name := range r.VerifyPeerCertByName { + if tls.IsFromMitm(name) { + isFromMitmVerify = true + r.VerifyPeerCertByName[0], r.VerifyPeerCertByName[i] = r.VerifyPeerCertByName[i], r.VerifyPeerCertByName[0] + r.VerifyPeerCertByName = r.VerifyPeerCertByName[1:] + after := mitmServerName + for { + if len(after) > 0 { + r.VerifyPeerCertByName = append(r.VerifyPeerCertByName, after) + } + _, after, _ = strings.Cut(after, ".") + if !strings.Contains(after, ".") { + break + } + } + slices.Reverse(r.VerifyPeerCertByName) + break + } + } + } + isFromMitmAlpn := len(tlsConfig.NextProtos) == 1 && tls.IsFromMitm(tlsConfig.NextProtos[0]) + if isFromMitmAlpn { + if mitmAlpn11 { + tlsConfig.NextProtos[0] = "http/1.1" + } else { + tlsConfig.NextProtos = []string{"h2", "http/1.1"} + } + } + if fingerprint := tls.GetFingerprint(config.Fingerprint); fingerprint != nil { + conn = tls.UClient(conn, tlsConfig, fingerprint) + if len(tlsConfig.NextProtos) == 1 && tlsConfig.NextProtos[0] == "http/1.1" { // allow manually specify + err = conn.(*tls.UConn).WebsocketHandshakeContext(ctx) + } else { + err = conn.(*tls.UConn).HandshakeContext(ctx) + } + } else { + conn = tls.Client(conn, tlsConfig) + err = conn.(*tls.Conn).HandshakeContext(ctx) + } + if err != nil { + if isFromMitmVerify { + return nil, errors.New("MITM freedom RAW TLS: failed to verify Domain Fronting certificate from " + mitmServerName).Base(err).AtWarning() + } + return nil, err + } + negotiatedProtocol := conn.(tls.Interface).NegotiatedProtocol() + if isFromMitmAlpn && !mitmAlpn11 && negotiatedProtocol != "h2" { + conn.Close() + return nil, errors.New("MITM freedom RAW TLS: unexpected Negotiated Protocol (" + negotiatedProtocol + ") with " + mitmServerName).AtWarning() + } + } else if config := reality.ConfigFromStreamSettings(streamSettings); config != nil { + if conn, err = reality.UClient(conn, config, ctx, dest); err != nil { + return nil, err + } + } + + tcpSettings := streamSettings.ProtocolSettings.(*Config) + if tcpSettings.HeaderSettings != nil { + headerConfig, err := tcpSettings.HeaderSettings.GetInstance() + if err != nil { + return nil, errors.New("failed to get header settings").Base(err).AtError() + } + auth, err := internet.CreateConnectionAuthenticator(headerConfig) + if err != nil { + return nil, errors.New("failed to create header authenticator").Base(err).AtError() + } + conn = auth.Client(conn) + } + return stat.Connection(conn), nil +} + +func init() { + common.Must(internet.RegisterTransportDialer(protocolName, Dial)) +} diff --git a/subproject/Xray-core-main/transport/internet/tcp/hub.go b/subproject/Xray-core-main/transport/internet/tcp/hub.go new file mode 100644 index 00000000..ede97499 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/tcp/hub.go @@ -0,0 +1,145 @@ +package tcp + +import ( + "context" + gotls "crypto/tls" + "strings" + "time" + + goreality "github.com/xtls/reality" + "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/transport/internet" + "github.com/xtls/xray-core/transport/internet/reality" + "github.com/xtls/xray-core/transport/internet/stat" + "github.com/xtls/xray-core/transport/internet/tls" +) + +// Listener is an internet.Listener that listens for TCP connections. +type Listener struct { + listener net.Listener + tlsConfig *gotls.Config + realityConfig *goreality.Config + authConfig internet.ConnectionAuthenticator + config *Config + addConn internet.ConnHandler +} + +// ListenTCP creates a new Listener based on configurations. +func ListenTCP(ctx context.Context, address net.Address, port net.Port, streamSettings *internet.MemoryStreamConfig, handler internet.ConnHandler) (internet.Listener, error) { + l := &Listener{ + addConn: handler, + } + tcpSettings := streamSettings.ProtocolSettings.(*Config) + l.config = tcpSettings + if l.config != nil { + if streamSettings.SocketSettings == nil { + streamSettings.SocketSettings = &internet.SocketConfig{} + } + streamSettings.SocketSettings.AcceptProxyProtocol = l.config.AcceptProxyProtocol || streamSettings.SocketSettings.AcceptProxyProtocol + } + var listener net.Listener + var err error + if port == net.Port(0) { // unix + if !address.Family().IsDomain() { + return nil, errors.New("invalid unix listen: ", address).AtError() + } + listener, err = internet.ListenSystem(ctx, &net.UnixAddr{ + Name: address.Domain(), + Net: "unix", + }, streamSettings.SocketSettings) + if err != nil { + return nil, errors.New("failed to listen Unix Domain Socket on ", address).Base(err) + } + errors.LogInfo(ctx, "listening Unix Domain Socket on ", address) + } else { + listener, err = internet.ListenSystem(ctx, &net.TCPAddr{ + IP: address.IP(), + Port: int(port), + }, streamSettings.SocketSettings) + if err != nil { + return nil, errors.New("failed to listen TCP on ", address, ":", port).Base(err) + } + errors.LogInfo(ctx, "listening TCP on ", address, ":", port) + } + + if streamSettings.TcpmaskManager != nil { + listener, _ = streamSettings.TcpmaskManager.WrapListener(listener) + } + + if streamSettings.SocketSettings != nil && streamSettings.SocketSettings.AcceptProxyProtocol { + errors.LogWarning(ctx, "accepting PROXY protocol") + } + + l.listener = listener + + if config := tls.ConfigFromStreamSettings(streamSettings); config != nil { + l.tlsConfig = config.GetTLSConfig() + } + if config := reality.ConfigFromStreamSettings(streamSettings); config != nil { + l.realityConfig = config.GetREALITYConfig() + go goreality.DetectPostHandshakeRecordsLens(l.realityConfig) + } + + if tcpSettings.HeaderSettings != nil { + headerConfig, err := tcpSettings.HeaderSettings.GetInstance() + if err != nil { + return nil, errors.New("invalid header settings").Base(err).AtError() + } + auth, err := internet.CreateConnectionAuthenticator(headerConfig) + if err != nil { + return nil, errors.New("invalid header settings.").Base(err).AtError() + } + l.authConfig = auth + } + + go l.keepAccepting() + return l, nil +} + +func (v *Listener) keepAccepting() { + for { + conn, err := v.listener.Accept() + if err != nil { + errStr := err.Error() + if strings.Contains(errStr, "closed") { + break + } + errors.LogWarningInner(context.Background(), err, "failed to accepted raw connections") + if strings.Contains(errStr, "too many") { + time.Sleep(time.Millisecond * 500) + } + continue + } + + go func() { + if v.tlsConfig != nil { + conn = tls.Server(conn, v.tlsConfig) + } else if v.realityConfig != nil { + if conn, err = reality.Server(conn, v.realityConfig); err != nil { + errors.LogInfo(context.Background(), err.Error()) + return + } + } + if v.authConfig != nil { + conn = v.authConfig.Server(conn) + } + v.addConn(stat.Connection(conn)) + }() + } +} + +// Addr implements internet.Listener.Addr. +func (v *Listener) Addr() net.Addr { + return v.listener.Addr() +} + +// Close implements internet.Listener.Close. +func (v *Listener) Close() error { + return v.listener.Close() +} + +func init() { + common.Must(internet.RegisterTransportListener(protocolName, ListenTCP)) +} diff --git a/subproject/Xray-core-main/transport/internet/tcp/sockopt_darwin.go b/subproject/Xray-core-main/transport/internet/tcp/sockopt_darwin.go new file mode 100644 index 00000000..ec0da29a --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/tcp/sockopt_darwin.go @@ -0,0 +1,26 @@ +//go:build darwin +// +build darwin + +package tcp + +import ( + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/transport/internet" + "github.com/xtls/xray-core/transport/internet/stat" +) + +// GetOriginalDestination from tcp conn +func GetOriginalDestination(conn stat.Connection) (net.Destination, error) { + la := conn.LocalAddr() + ra := conn.RemoteAddr() + ip, port, err := internet.OriginalDst(la, ra) + if err != nil { + return net.Destination{}, errors.New("failed to get destination").Base(err) + } + dest := net.TCPDestination(net.IPAddress(ip), net.Port(port)) + if !dest.IsValid() { + return net.Destination{}, errors.New("failed to parse destination.") + } + return dest, nil +} diff --git a/subproject/Xray-core-main/transport/internet/tcp/sockopt_freebsd.go b/subproject/Xray-core-main/transport/internet/tcp/sockopt_freebsd.go new file mode 100644 index 00000000..d7ab3ff0 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/tcp/sockopt_freebsd.go @@ -0,0 +1,26 @@ +//go:build freebsd +// +build freebsd + +package tcp + +import ( + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/transport/internet" + "github.com/xtls/xray-core/transport/internet/stat" +) + +// GetOriginalDestination from tcp conn +func GetOriginalDestination(conn stat.Connection) (net.Destination, error) { + la := conn.LocalAddr() + ra := conn.RemoteAddr() + ip, port, err := internet.OriginalDst(la, ra) + if err != nil { + return net.Destination{}, errors.New("failed to get destination").Base(err) + } + dest := net.TCPDestination(net.IPAddress(ip), net.Port(port)) + if !dest.IsValid() { + return net.Destination{}, errors.New("failed to parse destination.") + } + return dest, nil +} diff --git a/subproject/Xray-core-main/transport/internet/tcp/sockopt_linux.go b/subproject/Xray-core-main/transport/internet/tcp/sockopt_linux.go new file mode 100644 index 00000000..f5648e8f --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/tcp/sockopt_linux.go @@ -0,0 +1,52 @@ +//go:build linux +// +build linux + +package tcp + +import ( + "context" + "syscall" + "unsafe" + + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/transport/internet/stat" +) + +const SO_ORIGINAL_DST = 80 + +func GetOriginalDestination(conn stat.Connection) (net.Destination, error) { + sysrawconn, f := conn.(syscall.Conn) + if !f { + return net.Destination{}, errors.New("unable to get syscall.Conn") + } + rawConn, err := sysrawconn.SyscallConn() + if err != nil { + return net.Destination{}, errors.New("failed to get sys fd").Base(err) + } + var dest net.Destination + err = rawConn.Control(func(fd uintptr) { + level := syscall.IPPROTO_IP + if conn.RemoteAddr().String()[0] == '[' { + level = syscall.IPPROTO_IPV6 + } + addr, err := syscall.GetsockoptIPv6MTUInfo(int(fd), level, SO_ORIGINAL_DST) + if err != nil { + errors.LogInfoInner(context.Background(), err, "failed to call getsockopt") + return + } + ip := (*[4]byte)(unsafe.Pointer(&addr.Addr.Flowinfo))[:4] + if level == syscall.IPPROTO_IPV6 { + ip = addr.Addr.Addr[:] + } + port := (*[2]byte)(unsafe.Pointer(&addr.Addr.Port))[:2] + dest = net.TCPDestination(net.IPAddress(ip), net.PortFromBytes(port)) + }) + if err != nil { + return net.Destination{}, errors.New("failed to control connection").Base(err) + } + if !dest.IsValid() { + return net.Destination{}, errors.New("failed to call getsockopt") + } + return dest, nil +} diff --git a/subproject/Xray-core-main/transport/internet/tcp/sockopt_linux_test.go b/subproject/Xray-core-main/transport/internet/tcp/sockopt_linux_test.go new file mode 100644 index 00000000..8210cb4b --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/tcp/sockopt_linux_test.go @@ -0,0 +1,33 @@ +//go:build linux +// +build linux + +package tcp_test + +import ( + "context" + "strings" + "testing" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/testing/servers/tcp" + "github.com/xtls/xray-core/transport/internet" + . "github.com/xtls/xray-core/transport/internet/tcp" +) + +func TestGetOriginalDestination(t *testing.T) { + tcpServer := tcp.Server{} + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + config, err := internet.ToMemoryStreamConfig(nil) + common.Must(err) + conn, err := Dial(context.Background(), dest, config) + common.Must(err) + defer conn.Close() + + originalDest, err := GetOriginalDestination(conn) + if !(dest == originalDest || strings.Contains(err.Error(), "failed to call getsockopt")) { + t.Error("unexpected state") + } +} diff --git a/subproject/Xray-core-main/transport/internet/tcp/sockopt_other.go b/subproject/Xray-core-main/transport/internet/tcp/sockopt_other.go new file mode 100644 index 00000000..3f657354 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/tcp/sockopt_other.go @@ -0,0 +1,13 @@ +//go:build !linux && !freebsd && !darwin +// +build !linux,!freebsd,!darwin + +package tcp + +import ( + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/transport/internet/stat" +) + +func GetOriginalDestination(conn stat.Connection) (net.Destination, error) { + return net.Destination{}, nil +} diff --git a/subproject/Xray-core-main/transport/internet/tcp/tcp.go b/subproject/Xray-core-main/transport/internet/tcp/tcp.go new file mode 100644 index 00000000..9a86457a --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/tcp/tcp.go @@ -0,0 +1,3 @@ +package tcp + +const protocolName = "tcp" diff --git a/subproject/Xray-core-main/transport/internet/tcp_hub.go b/subproject/Xray-core-main/transport/internet/tcp_hub.go new file mode 100644 index 00000000..cf0ad80d --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/tcp_hub.go @@ -0,0 +1,93 @@ +package internet + +import ( + "context" + + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/transport/internet/stat" +) + +var transportListenerCache = make(map[string]ListenFunc) + +func RegisterTransportListener(protocol string, listener ListenFunc) error { + if _, found := transportListenerCache[protocol]; found { + return errors.New(protocol, " listener already registered.").AtError() + } + transportListenerCache[protocol] = listener + return nil +} + +type ConnHandler func(stat.Connection) + +type ListenFunc func(ctx context.Context, address net.Address, port net.Port, settings *MemoryStreamConfig, handler ConnHandler) (Listener, error) + +type Listener interface { + Close() error + Addr() net.Addr +} + +// ListenUnix is the UDS version of ListenTCP +func ListenUnix(ctx context.Context, address net.Address, settings *MemoryStreamConfig, handler ConnHandler) (Listener, error) { + if settings == nil { + s, err := ToMemoryStreamConfig(nil) + if err != nil { + return nil, errors.New("failed to create default unix stream settings").Base(err) + } + settings = s + } + + protocol := settings.ProtocolName + listenFunc := transportListenerCache[protocol] + if listenFunc == nil { + return nil, errors.New(protocol, " unix listener not registered.").AtError() + } + listener, err := listenFunc(ctx, address, net.Port(0), settings, handler) + if err != nil { + return nil, errors.New("failed to listen on unix address: ", address).Base(err) + } + return listener, nil +} + +func ListenTCP(ctx context.Context, address net.Address, port net.Port, settings *MemoryStreamConfig, handler ConnHandler) (Listener, error) { + if settings == nil { + s, err := ToMemoryStreamConfig(nil) + if err != nil { + return nil, errors.New("failed to create default stream settings").Base(err) + } + settings = s + } + + if address.Family().IsDomain() && address.Domain() == "localhost" { + address = net.LocalHostIP + } + + if address.Family().IsDomain() { + return nil, errors.New("domain address is not allowed for listening: ", address.Domain()) + } + + protocol := settings.ProtocolName + listenFunc := transportListenerCache[protocol] + if listenFunc == nil { + return nil, errors.New(protocol, " listener not registered.").AtError() + } + listener, err := listenFunc(ctx, address, port, settings, handler) + if err != nil { + return nil, errors.New("failed to listen on address: ", address, ":", port).Base(err) + } + return listener, nil +} + +// ListenSystem listens on a local address for incoming TCP connections. +// +// xray:api:beta +func ListenSystem(ctx context.Context, addr net.Addr, sockopt *SocketConfig) (net.Listener, error) { + return effectiveListener.Listen(ctx, addr, sockopt) +} + +// ListenSystemPacket listens on a local address for incoming UDP connections. +// +// xray:api:beta +func ListenSystemPacket(ctx context.Context, addr net.Addr, sockopt *SocketConfig) (net.PacketConn, error) { + return effectiveListener.ListenPacket(ctx, addr, sockopt) +} diff --git a/subproject/Xray-core-main/transport/internet/tls/config.go b/subproject/Xray-core-main/transport/internet/tls/config.go new file mode 100644 index 00000000..9a41f379 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/tls/config.go @@ -0,0 +1,578 @@ +package tls + +import ( + "bytes" + "context" + "crypto/hmac" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "os" + "slices" + "strings" + "sync" + "time" + + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/ocsp" + "github.com/xtls/xray-core/common/platform/filesystem" + "github.com/xtls/xray-core/common/protocol/tls/cert" + "github.com/xtls/xray-core/transport/internet" +) + +var globalSessionCache = tls.NewLRUClientSessionCache(128) + +// ParseCertificate converts a cert.Certificate to Certificate. +func ParseCertificate(c *cert.Certificate) *Certificate { + if c != nil { + certPEM, keyPEM := c.ToPEM() + return &Certificate{ + Certificate: certPEM, + Key: keyPEM, + } + } + return nil +} + +func (c *Config) loadSelfCertPool() (*x509.CertPool, error) { + root := x509.NewCertPool() + for _, cert := range c.Certificate { + if !root.AppendCertsFromPEM(cert.Certificate) { + return nil, errors.New("failed to append cert").AtWarning() + } + } + return root, nil +} + +// BuildCertificates builds a list of TLS certificates from proto definition. +func (c *Config) BuildCertificates() []*tls.Certificate { + certs := make([]*tls.Certificate, 0, len(c.Certificate)) + for _, entry := range c.Certificate { + if entry.Usage != Certificate_ENCIPHERMENT { + continue + } + getX509KeyPair := func() *tls.Certificate { + keyPair, err := tls.X509KeyPair(entry.Certificate, entry.Key) + if err != nil { + errors.LogWarningInner(context.Background(), err, "ignoring invalid X509 key pair") + return nil + } + keyPair.Leaf, err = x509.ParseCertificate(keyPair.Certificate[0]) + if err != nil { + errors.LogWarningInner(context.Background(), err, "ignoring invalid certificate") + return nil + } + return &keyPair + } + if keyPair := getX509KeyPair(); keyPair != nil { + certs = append(certs, keyPair) + } else { + continue + } + index := len(certs) - 1 + setupOcspTicker(entry, func(isReloaded, isOcspstapling bool) { + cert := certs[index] + if isReloaded { + if newKeyPair := getX509KeyPair(); newKeyPair != nil { + cert = newKeyPair + } else { + return + } + } + if isOcspstapling { + if newOCSPData, err := ocsp.GetOCSPForCert(cert.Certificate); err != nil { + errors.LogWarningInner(context.Background(), err, "ignoring invalid OCSP") + } else if string(newOCSPData) != string(cert.OCSPStaple) { + cert.OCSPStaple = newOCSPData + } + } + certs[index] = cert + }) + } + return certs +} + +func setupOcspTicker(entry *Certificate, callback func(isReloaded, isOcspstapling bool)) { + go func() { + if entry.OneTimeLoading { + return + } + var isOcspstapling bool + hotReloadCertInterval := uint64(3600) + if entry.OcspStapling != 0 { + hotReloadCertInterval = entry.OcspStapling + isOcspstapling = true + } + t := time.NewTicker(time.Duration(hotReloadCertInterval) * time.Second) + for { + var isReloaded bool + if entry.CertificatePath != "" && entry.KeyPath != "" { + newCert, err := filesystem.ReadCert(entry.CertificatePath) + if err != nil { + errors.LogErrorInner(context.Background(), err, "failed to parse certificate") + return + } + newKey, err := filesystem.ReadCert(entry.KeyPath) + if err != nil { + errors.LogErrorInner(context.Background(), err, "failed to parse key") + return + } + if string(newCert) != string(entry.Certificate) || string(newKey) != string(entry.Key) { + entry.Certificate = newCert + entry.Key = newKey + isReloaded = true + } + } + callback(isReloaded, isOcspstapling) + <-t.C + } + }() +} + +func isCertificateExpired(c *tls.Certificate) bool { + if c.Leaf == nil && len(c.Certificate) > 0 { + if pc, err := x509.ParseCertificate(c.Certificate[0]); err == nil { + c.Leaf = pc + } + } + + // If leaf is not there, the certificate is probably not used yet. We trust user to provide a valid certificate. + return c.Leaf != nil && c.Leaf.NotAfter.Before(time.Now().Add(time.Minute*2)) +} + +func issueCertificate(rawCA *Certificate, domain string) (*tls.Certificate, error) { + parent, err := cert.ParseCertificate(rawCA.Certificate, rawCA.Key) + if err != nil { + return nil, errors.New("failed to parse raw certificate").Base(err) + } + newCert, err := cert.Generate(parent, cert.CommonName(domain), cert.DNSNames(domain)) + if err != nil { + return nil, errors.New("failed to generate new certificate for ", domain).Base(err) + } + newCertPEM, newKeyPEM := newCert.ToPEM() + if rawCA.BuildChain { + newCertPEM = bytes.Join([][]byte{newCertPEM, rawCA.Certificate}, []byte("\n")) + } + cert, err := tls.X509KeyPair(newCertPEM, newKeyPEM) + return &cert, err +} + +func (c *Config) getCustomCA() []*Certificate { + certs := make([]*Certificate, 0, len(c.Certificate)) + for _, certificate := range c.Certificate { + if certificate.Usage == Certificate_AUTHORITY_ISSUE { + certs = append(certs, certificate) + setupOcspTicker(certificate, func(isReloaded, isOcspstapling bool) {}) + } + } + return certs +} + +func getGetCertificateFunc(c *tls.Config, ca []*Certificate) func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { + var access sync.RWMutex + + return func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { + domain := hello.ServerName + certExpired := false + + access.RLock() + certificate, found := c.NameToCertificate[domain] + access.RUnlock() + + if found { + if !isCertificateExpired(certificate) { + return certificate, nil + } + certExpired = true + } + + if certExpired { + newCerts := make([]tls.Certificate, 0, len(c.Certificates)) + + access.Lock() + for _, certificate := range c.Certificates { + if !isCertificateExpired(&certificate) { + newCerts = append(newCerts, certificate) + } else if certificate.Leaf != nil { + expTime := certificate.Leaf.NotAfter.Format(time.RFC3339) + errors.LogInfo(context.Background(), "old certificate for ", domain, " (expire on ", expTime, ") discarded") + } + } + + c.Certificates = newCerts + access.Unlock() + } + + var issuedCertificate *tls.Certificate + + // Create a new certificate from existing CA if possible + for _, rawCert := range ca { + if rawCert.Usage == Certificate_AUTHORITY_ISSUE { + newCert, err := issueCertificate(rawCert, domain) + if err != nil { + errors.LogInfoInner(context.Background(), err, "failed to issue new certificate for ", domain) + continue + } + parsed, err := x509.ParseCertificate(newCert.Certificate[0]) + if err == nil { + newCert.Leaf = parsed + expTime := parsed.NotAfter.Format(time.RFC3339) + errors.LogInfo(context.Background(), "new certificate for ", domain, " (expire on ", expTime, ") issued") + } else { + errors.LogInfoInner(context.Background(), err, "failed to parse new certificate for ", domain) + } + + access.Lock() + c.Certificates = append(c.Certificates, *newCert) + issuedCertificate = &c.Certificates[len(c.Certificates)-1] + access.Unlock() + break + } + } + + if issuedCertificate == nil { + return nil, errors.New("failed to create a new certificate for ", domain) + } + + access.Lock() + c.BuildNameToCertificate() + access.Unlock() + + return issuedCertificate, nil + } +} + +func getNewGetCertificateFunc(certs []*tls.Certificate, rejectUnknownSNI bool) func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { + return func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { + if len(certs) == 0 { + return nil, errNoCertificates + } + sni := strings.ToLower(hello.ServerName) + if !rejectUnknownSNI && (len(certs) == 1 || sni == "") { + return certs[0], nil + } + gsni := "*" + if index := strings.IndexByte(sni, '.'); index != -1 { + gsni += sni[index:] + } + for _, keyPair := range certs { + if keyPair.Leaf.Subject.CommonName == sni || keyPair.Leaf.Subject.CommonName == gsni { + return keyPair, nil + } + for _, name := range keyPair.Leaf.DNSNames { + if name == sni || name == gsni { + return keyPair, nil + } + } + } + if rejectUnknownSNI { + return nil, errNoCertificates + } + return certs[0], nil + } +} + +func (c *Config) parseServerName() string { + if IsFromMitm(c.ServerName) { + return "" + } + return c.ServerName +} + +func (r *RandCarrier) verifyPeerCert(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) (err error) { + // extract x509 certificates from rawCerts (verifiedChains will be nil if InsecureSkipVerify is true) + certs := make([]*x509.Certificate, len(rawCerts)) + for i, asn1Data := range rawCerts { + certs[i], _ = x509.ParseCertificate(asn1Data) + } + if len(certs) == 0 { + return errors.New("unexpected certs") + } + + // directly return success if pinned cert is leaf + // or replace RootCAs if pinned cert is CA (and can be used in VerifyPeerCertByName) + CAs := r.RootCAs + var verifyResult verifyResult + var verifiedCert *x509.Certificate + if r.PinnedPeerCertSha256 != nil { + verifyResult, verifiedCert = verifyChain(certs, r.PinnedPeerCertSha256) + switch verifyResult { + case certNotFound: + return errors.New("peer cert is unrecognized (against pinnedPeerCertSha256)") + case foundLeaf: + return nil + case foundCA: + CAs = x509.NewCertPool() + CAs.AddCert(verifiedCert) + default: + panic("impossible pinnedPeerCertSha256 verify result") + } + } + + if r.VerifyPeerCertByName != nil { // RAW's Dial() may make it empty but not nil + opts := x509.VerifyOptions{ + Roots: CAs, + CurrentTime: time.Now(), + Intermediates: x509.NewCertPool(), + } + for _, cert := range certs[1:] { + opts.Intermediates.AddCert(cert) + } + for _, opts.DNSName = range r.VerifyPeerCertByName { + if _, err := certs[0].Verify(opts); err == nil { + return nil + } + } + if verifyResult == foundCA { + errors.New("peer cert is invalid (against pinned CA and verifyPeerCertByName)") + } + return errors.New("peer cert is invalid (against root CAs and verifyPeerCertByName)") + } + + if verifyResult == foundCA { // if found CA, we need to verify here + opts := x509.VerifyOptions{ + Roots: CAs, + CurrentTime: time.Now(), + Intermediates: x509.NewCertPool(), + DNSName: r.Config.ServerName, + } + for _, cert := range certs[1:] { + opts.Intermediates.AddCert(cert) + } + if _, err := certs[0].Verify(opts); err == nil { + return nil + } + return errors.New("peer cert is invalid (against pinned CA and serverName)") + } + + return nil // r.PinnedPeerCertSha256==nil && r.verifyPeerCertByName==nil +} + +type RandCarrier struct { + Config *tls.Config + RootCAs *x509.CertPool + VerifyPeerCertByName []string + PinnedPeerCertSha256 [][]byte +} + +func (r *RandCarrier) Read(p []byte) (n int, err error) { + return rand.Read(p) +} + +// GetTLSConfig converts this Config into tls.Config. +func (c *Config) GetTLSConfig(opts ...Option) *tls.Config { + root, err := c.getCertPool() + if err != nil { + errors.LogErrorInner(context.Background(), err, "failed to load system root certificate") + } + + if c == nil { + return &tls.Config{ + ClientSessionCache: globalSessionCache, + RootCAs: root, + SessionTicketsDisabled: true, + } + } + + randCarrier := &RandCarrier{ + RootCAs: root, + VerifyPeerCertByName: slices.Clone(c.VerifyPeerCertByName), + PinnedPeerCertSha256: c.PinnedPeerCertSha256, + } + config := &tls.Config{ + InsecureSkipVerify: c.AllowInsecure, + Rand: randCarrier, + ClientSessionCache: globalSessionCache, + RootCAs: root, + NextProtos: slices.Clone(c.NextProtocol), + SessionTicketsDisabled: !c.EnableSessionResumption, + VerifyPeerCertificate: randCarrier.verifyPeerCert, + } + randCarrier.Config = config + if len(c.VerifyPeerCertByName) > 0 { + config.InsecureSkipVerify = true + } else { + randCarrier.VerifyPeerCertByName = nil + } + if len(c.PinnedPeerCertSha256) > 0 { + config.InsecureSkipVerify = true + } else { + randCarrier.PinnedPeerCertSha256 = nil + } + + for _, opt := range opts { + opt(config) + } + + caCerts := c.getCustomCA() + if len(caCerts) > 0 { + config.GetCertificate = getGetCertificateFunc(config, caCerts) + } else { + config.GetCertificate = getNewGetCertificateFunc(c.BuildCertificates(), c.RejectUnknownSni) + } + + if sn := c.parseServerName(); len(sn) > 0 { + config.ServerName = sn + } + + if len(c.CurvePreferences) > 0 { + config.CurvePreferences = ParseCurveName(c.CurvePreferences) + } + + if len(config.NextProtos) == 0 { + config.NextProtos = []string{"h2", "http/1.1"} + } + + switch c.MinVersion { + case "1.0": + config.MinVersion = tls.VersionTLS10 + case "1.1": + config.MinVersion = tls.VersionTLS11 + case "1.2": + config.MinVersion = tls.VersionTLS12 + case "1.3": + config.MinVersion = tls.VersionTLS13 + } + + switch c.MaxVersion { + case "1.0": + config.MaxVersion = tls.VersionTLS10 + case "1.1": + config.MaxVersion = tls.VersionTLS11 + case "1.2": + config.MaxVersion = tls.VersionTLS12 + case "1.3": + config.MaxVersion = tls.VersionTLS13 + } + + if len(c.CipherSuites) > 0 { + id := make(map[string]uint16) + for _, s := range tls.CipherSuites() { + id[s.Name] = s.ID + } + for _, n := range strings.Split(c.CipherSuites, ":") { + if id[n] != 0 { + config.CipherSuites = append(config.CipherSuites, id[n]) + } + } + } + + if len(c.MasterKeyLog) > 0 && c.MasterKeyLog != "none" { + writer, err := os.OpenFile(c.MasterKeyLog, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0644) + if err != nil { + errors.LogErrorInner(context.Background(), err, "failed to open ", c.MasterKeyLog, " as master key log") + } else { + config.KeyLogWriter = writer + } + } + if len(c.EchConfigList) > 0 || len(c.EchServerKeys) > 0 { + err := ApplyECH(c, config) + if err != nil { + if c.EchForceQuery == "full" { + errors.LogError(context.Background(), err) + } else { + errors.LogInfo(context.Background(), err) + } + } + } + + return config +} + +// Option for building TLS config. +type Option func(*tls.Config) + +// WithDestination sets the server name in TLS config. +// Due to the incorrect structure of GetTLSConfig(), the config.ServerName will always be empty. +// So the real logic for SNI is: +// set it to dest -> overwrite it with servername(if it's len>0). +func WithDestination(dest net.Destination) Option { + return func(config *tls.Config) { + if config.ServerName == "" { + config.ServerName = dest.Address.String() + } + } +} + +func WithOverrideName(serverName string) Option { + return func(config *tls.Config) { + config.ServerName = serverName + } +} + +// WithNextProto sets the ALPN values in TLS config. +func WithNextProto(protocol ...string) Option { + return func(config *tls.Config) { + if len(config.NextProtos) == 0 { + config.NextProtos = protocol + } + } +} + +// ConfigFromStreamSettings fetches Config from stream settings. Nil if not found. +func ConfigFromStreamSettings(settings *internet.MemoryStreamConfig) *Config { + if settings == nil { + return nil + } + config, ok := settings.SecuritySettings.(*Config) + if !ok { + return nil + } + return config +} + +func ParseCurveName(curveNames []string) []tls.CurveID { + curveMap := map[string]tls.CurveID{ + "curvep256": tls.CurveP256, + "curvep384": tls.CurveP384, + "curvep521": tls.CurveP521, + "x25519": tls.X25519, + "x25519mlkem768": tls.X25519MLKEM768, + "secp256r1mlkem768": tls.SecP256r1MLKEM768, + "secp384r1mlkem1024": tls.SecP384r1MLKEM1024, + } + + var curveIDs []tls.CurveID + for _, name := range curveNames { + if curveID, ok := curveMap[strings.ToLower(name)]; ok { + curveIDs = append(curveIDs, curveID) + } else { + errors.LogWarning(context.Background(), "unsupported curve name: "+name) + } + } + return curveIDs +} + +func IsFromMitm(str string) bool { + return strings.ToLower(str) == "frommitm" +} + +type verifyResult int + +const ( + certNotFound verifyResult = iota + foundLeaf + foundCA +) + +func verifyChain(certs []*x509.Certificate, pinnedPeerCertSha256 [][]byte) (verifyResult, *x509.Certificate) { + leafHash := GenerateCertHash(certs[0]) + for _, c := range pinnedPeerCertSha256 { + if hmac.Equal(leafHash, c) { + return foundLeaf, nil + } + } + certs = certs[1:] // skip leaf + for _, cert := range certs { + certHash := GenerateCertHash(cert) + for _, c := range pinnedPeerCertSha256 { + if hmac.Equal(certHash, c) { + if cert.IsCA { + return foundCA, cert + } + } + } + } + return certNotFound, nil +} diff --git a/subproject/Xray-core-main/transport/internet/tls/config.pb.go b/subproject/Xray-core-main/transport/internet/tls/config.pb.go new file mode 100644 index 00000000..37628755 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/tls/config.pb.go @@ -0,0 +1,476 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: transport/internet/tls/config.proto + +package tls + +import ( + internet "github.com/xtls/xray-core/transport/internet" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Certificate_Usage int32 + +const ( + Certificate_ENCIPHERMENT Certificate_Usage = 0 + Certificate_AUTHORITY_VERIFY Certificate_Usage = 1 + Certificate_AUTHORITY_ISSUE Certificate_Usage = 2 +) + +// Enum value maps for Certificate_Usage. +var ( + Certificate_Usage_name = map[int32]string{ + 0: "ENCIPHERMENT", + 1: "AUTHORITY_VERIFY", + 2: "AUTHORITY_ISSUE", + } + Certificate_Usage_value = map[string]int32{ + "ENCIPHERMENT": 0, + "AUTHORITY_VERIFY": 1, + "AUTHORITY_ISSUE": 2, + } +) + +func (x Certificate_Usage) Enum() *Certificate_Usage { + p := new(Certificate_Usage) + *p = x + return p +} + +func (x Certificate_Usage) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Certificate_Usage) Descriptor() protoreflect.EnumDescriptor { + return file_transport_internet_tls_config_proto_enumTypes[0].Descriptor() +} + +func (Certificate_Usage) Type() protoreflect.EnumType { + return &file_transport_internet_tls_config_proto_enumTypes[0] +} + +func (x Certificate_Usage) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Certificate_Usage.Descriptor instead. +func (Certificate_Usage) EnumDescriptor() ([]byte, []int) { + return file_transport_internet_tls_config_proto_rawDescGZIP(), []int{0, 0} +} + +type Certificate struct { + state protoimpl.MessageState `protogen:"open.v1"` + // TLS certificate in x509 format. + Certificate []byte `protobuf:"bytes,1,opt,name=certificate,proto3" json:"certificate,omitempty"` + // TLS key in x509 format. + Key []byte `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"` + Usage Certificate_Usage `protobuf:"varint,3,opt,name=usage,proto3,enum=xray.transport.internet.tls.Certificate_Usage" json:"usage,omitempty"` + OcspStapling uint64 `protobuf:"varint,4,opt,name=ocsp_stapling,json=ocspStapling,proto3" json:"ocsp_stapling,omitempty"` + // TLS certificate path + CertificatePath string `protobuf:"bytes,5,opt,name=certificate_path,json=certificatePath,proto3" json:"certificate_path,omitempty"` + // TLS Key path + KeyPath string `protobuf:"bytes,6,opt,name=key_path,json=keyPath,proto3" json:"key_path,omitempty"` + // If true, one-Time Loading + OneTimeLoading bool `protobuf:"varint,7,opt,name=One_time_loading,json=OneTimeLoading,proto3" json:"One_time_loading,omitempty"` + BuildChain bool `protobuf:"varint,8,opt,name=build_chain,json=buildChain,proto3" json:"build_chain,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Certificate) Reset() { + *x = Certificate{} + mi := &file_transport_internet_tls_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Certificate) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Certificate) ProtoMessage() {} + +func (x *Certificate) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_tls_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Certificate.ProtoReflect.Descriptor instead. +func (*Certificate) Descriptor() ([]byte, []int) { + return file_transport_internet_tls_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Certificate) GetCertificate() []byte { + if x != nil { + return x.Certificate + } + return nil +} + +func (x *Certificate) GetKey() []byte { + if x != nil { + return x.Key + } + return nil +} + +func (x *Certificate) GetUsage() Certificate_Usage { + if x != nil { + return x.Usage + } + return Certificate_ENCIPHERMENT +} + +func (x *Certificate) GetOcspStapling() uint64 { + if x != nil { + return x.OcspStapling + } + return 0 +} + +func (x *Certificate) GetCertificatePath() string { + if x != nil { + return x.CertificatePath + } + return "" +} + +func (x *Certificate) GetKeyPath() string { + if x != nil { + return x.KeyPath + } + return "" +} + +func (x *Certificate) GetOneTimeLoading() bool { + if x != nil { + return x.OneTimeLoading + } + return false +} + +func (x *Certificate) GetBuildChain() bool { + if x != nil { + return x.BuildChain + } + return false +} + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + AllowInsecure bool `protobuf:"varint,1,opt,name=allow_insecure,json=allowInsecure,proto3" json:"allow_insecure,omitempty"` + // List of certificates to be served on server. + Certificate []*Certificate `protobuf:"bytes,2,rep,name=certificate,proto3" json:"certificate,omitempty"` + // Override server name. + ServerName string `protobuf:"bytes,3,opt,name=server_name,json=serverName,proto3" json:"server_name,omitempty"` + // Lists of string as ALPN values. + NextProtocol []string `protobuf:"bytes,4,rep,name=next_protocol,json=nextProtocol,proto3" json:"next_protocol,omitempty"` + // Whether or not to enable session (ticket) resumption. + EnableSessionResumption bool `protobuf:"varint,5,opt,name=enable_session_resumption,json=enableSessionResumption,proto3" json:"enable_session_resumption,omitempty"` + // If true, root certificates on the system will not be loaded for + // verification. + DisableSystemRoot bool `protobuf:"varint,6,opt,name=disable_system_root,json=disableSystemRoot,proto3" json:"disable_system_root,omitempty"` + // The minimum TLS version. + MinVersion string `protobuf:"bytes,7,opt,name=min_version,json=minVersion,proto3" json:"min_version,omitempty"` + // The maximum TLS version. + MaxVersion string `protobuf:"bytes,8,opt,name=max_version,json=maxVersion,proto3" json:"max_version,omitempty"` + // Specify cipher suites, except for TLS 1.3. + CipherSuites string `protobuf:"bytes,9,opt,name=cipher_suites,json=cipherSuites,proto3" json:"cipher_suites,omitempty"` + // TLS Client Hello fingerprint (uTLS). + Fingerprint string `protobuf:"bytes,11,opt,name=fingerprint,proto3" json:"fingerprint,omitempty"` + RejectUnknownSni bool `protobuf:"varint,12,opt,name=reject_unknown_sni,json=rejectUnknownSni,proto3" json:"reject_unknown_sni,omitempty"` + MasterKeyLog string `protobuf:"bytes,15,opt,name=master_key_log,json=masterKeyLog,proto3" json:"master_key_log,omitempty"` + // Lists of string as CurvePreferences values. + CurvePreferences []string `protobuf:"bytes,16,rep,name=curve_preferences,json=curvePreferences,proto3" json:"curve_preferences,omitempty"` + VerifyPeerCertByName []string `protobuf:"bytes,17,rep,name=verify_peer_cert_by_name,json=verifyPeerCertByName,proto3" json:"verify_peer_cert_by_name,omitempty"` + EchServerKeys []byte `protobuf:"bytes,18,opt,name=ech_server_keys,json=echServerKeys,proto3" json:"ech_server_keys,omitempty"` + EchConfigList string `protobuf:"bytes,19,opt,name=ech_config_list,json=echConfigList,proto3" json:"ech_config_list,omitempty"` + EchForceQuery string `protobuf:"bytes,20,opt,name=ech_force_query,json=echForceQuery,proto3" json:"ech_force_query,omitempty"` + EchSocketSettings *internet.SocketConfig `protobuf:"bytes,21,opt,name=ech_socket_settings,json=echSocketSettings,proto3" json:"ech_socket_settings,omitempty"` + PinnedPeerCertSha256 [][]byte `protobuf:"bytes,22,rep,name=pinned_peer_cert_sha256,json=pinnedPeerCertSha256,proto3" json:"pinned_peer_cert_sha256,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_transport_internet_tls_config_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_tls_config_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_transport_internet_tls_config_proto_rawDescGZIP(), []int{1} +} + +func (x *Config) GetAllowInsecure() bool { + if x != nil { + return x.AllowInsecure + } + return false +} + +func (x *Config) GetCertificate() []*Certificate { + if x != nil { + return x.Certificate + } + return nil +} + +func (x *Config) GetServerName() string { + if x != nil { + return x.ServerName + } + return "" +} + +func (x *Config) GetNextProtocol() []string { + if x != nil { + return x.NextProtocol + } + return nil +} + +func (x *Config) GetEnableSessionResumption() bool { + if x != nil { + return x.EnableSessionResumption + } + return false +} + +func (x *Config) GetDisableSystemRoot() bool { + if x != nil { + return x.DisableSystemRoot + } + return false +} + +func (x *Config) GetMinVersion() string { + if x != nil { + return x.MinVersion + } + return "" +} + +func (x *Config) GetMaxVersion() string { + if x != nil { + return x.MaxVersion + } + return "" +} + +func (x *Config) GetCipherSuites() string { + if x != nil { + return x.CipherSuites + } + return "" +} + +func (x *Config) GetFingerprint() string { + if x != nil { + return x.Fingerprint + } + return "" +} + +func (x *Config) GetRejectUnknownSni() bool { + if x != nil { + return x.RejectUnknownSni + } + return false +} + +func (x *Config) GetMasterKeyLog() string { + if x != nil { + return x.MasterKeyLog + } + return "" +} + +func (x *Config) GetCurvePreferences() []string { + if x != nil { + return x.CurvePreferences + } + return nil +} + +func (x *Config) GetVerifyPeerCertByName() []string { + if x != nil { + return x.VerifyPeerCertByName + } + return nil +} + +func (x *Config) GetEchServerKeys() []byte { + if x != nil { + return x.EchServerKeys + } + return nil +} + +func (x *Config) GetEchConfigList() string { + if x != nil { + return x.EchConfigList + } + return "" +} + +func (x *Config) GetEchForceQuery() string { + if x != nil { + return x.EchForceQuery + } + return "" +} + +func (x *Config) GetEchSocketSettings() *internet.SocketConfig { + if x != nil { + return x.EchSocketSettings + } + return nil +} + +func (x *Config) GetPinnedPeerCertSha256() [][]byte { + if x != nil { + return x.PinnedPeerCertSha256 + } + return nil +} + +var File_transport_internet_tls_config_proto protoreflect.FileDescriptor + +const file_transport_internet_tls_config_proto_rawDesc = "" + + "\n" + + "#transport/internet/tls/config.proto\x12\x1bxray.transport.internet.tls\x1a\x1ftransport/internet/config.proto\"\x83\x03\n" + + "\vCertificate\x12 \n" + + "\vcertificate\x18\x01 \x01(\fR\vcertificate\x12\x10\n" + + "\x03key\x18\x02 \x01(\fR\x03key\x12D\n" + + "\x05usage\x18\x03 \x01(\x0e2..xray.transport.internet.tls.Certificate.UsageR\x05usage\x12#\n" + + "\rocsp_stapling\x18\x04 \x01(\x04R\focspStapling\x12)\n" + + "\x10certificate_path\x18\x05 \x01(\tR\x0fcertificatePath\x12\x19\n" + + "\bkey_path\x18\x06 \x01(\tR\akeyPath\x12(\n" + + "\x10One_time_loading\x18\a \x01(\bR\x0eOneTimeLoading\x12\x1f\n" + + "\vbuild_chain\x18\b \x01(\bR\n" + + "buildChain\"D\n" + + "\x05Usage\x12\x10\n" + + "\fENCIPHERMENT\x10\x00\x12\x14\n" + + "\x10AUTHORITY_VERIFY\x10\x01\x12\x13\n" + + "\x0fAUTHORITY_ISSUE\x10\x02\"\xf5\x06\n" + + "\x06Config\x12%\n" + + "\x0eallow_insecure\x18\x01 \x01(\bR\rallowInsecure\x12J\n" + + "\vcertificate\x18\x02 \x03(\v2(.xray.transport.internet.tls.CertificateR\vcertificate\x12\x1f\n" + + "\vserver_name\x18\x03 \x01(\tR\n" + + "serverName\x12#\n" + + "\rnext_protocol\x18\x04 \x03(\tR\fnextProtocol\x12:\n" + + "\x19enable_session_resumption\x18\x05 \x01(\bR\x17enableSessionResumption\x12.\n" + + "\x13disable_system_root\x18\x06 \x01(\bR\x11disableSystemRoot\x12\x1f\n" + + "\vmin_version\x18\a \x01(\tR\n" + + "minVersion\x12\x1f\n" + + "\vmax_version\x18\b \x01(\tR\n" + + "maxVersion\x12#\n" + + "\rcipher_suites\x18\t \x01(\tR\fcipherSuites\x12 \n" + + "\vfingerprint\x18\v \x01(\tR\vfingerprint\x12,\n" + + "\x12reject_unknown_sni\x18\f \x01(\bR\x10rejectUnknownSni\x12$\n" + + "\x0emaster_key_log\x18\x0f \x01(\tR\fmasterKeyLog\x12+\n" + + "\x11curve_preferences\x18\x10 \x03(\tR\x10curvePreferences\x126\n" + + "\x18verify_peer_cert_by_name\x18\x11 \x03(\tR\x14verifyPeerCertByName\x12&\n" + + "\x0fech_server_keys\x18\x12 \x01(\fR\rechServerKeys\x12&\n" + + "\x0fech_config_list\x18\x13 \x01(\tR\rechConfigList\x12&\n" + + "\x0fech_force_query\x18\x14 \x01(\tR\rechForceQuery\x12U\n" + + "\x13ech_socket_settings\x18\x15 \x01(\v2%.xray.transport.internet.SocketConfigR\x11echSocketSettings\x125\n" + + "\x17pinned_peer_cert_sha256\x18\x16 \x03(\fR\x14pinnedPeerCertSha256Bs\n" + + "\x1fcom.xray.transport.internet.tlsP\x01Z0github.com/xtls/xray-core/transport/internet/tls\xaa\x02\x1bXray.Transport.Internet.Tlsb\x06proto3" + +var ( + file_transport_internet_tls_config_proto_rawDescOnce sync.Once + file_transport_internet_tls_config_proto_rawDescData []byte +) + +func file_transport_internet_tls_config_proto_rawDescGZIP() []byte { + file_transport_internet_tls_config_proto_rawDescOnce.Do(func() { + file_transport_internet_tls_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_transport_internet_tls_config_proto_rawDesc), len(file_transport_internet_tls_config_proto_rawDesc))) + }) + return file_transport_internet_tls_config_proto_rawDescData +} + +var file_transport_internet_tls_config_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_transport_internet_tls_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_transport_internet_tls_config_proto_goTypes = []any{ + (Certificate_Usage)(0), // 0: xray.transport.internet.tls.Certificate.Usage + (*Certificate)(nil), // 1: xray.transport.internet.tls.Certificate + (*Config)(nil), // 2: xray.transport.internet.tls.Config + (*internet.SocketConfig)(nil), // 3: xray.transport.internet.SocketConfig +} +var file_transport_internet_tls_config_proto_depIdxs = []int32{ + 0, // 0: xray.transport.internet.tls.Certificate.usage:type_name -> xray.transport.internet.tls.Certificate.Usage + 1, // 1: xray.transport.internet.tls.Config.certificate:type_name -> xray.transport.internet.tls.Certificate + 3, // 2: xray.transport.internet.tls.Config.ech_socket_settings:type_name -> xray.transport.internet.SocketConfig + 3, // [3:3] is the sub-list for method output_type + 3, // [3:3] is the sub-list for method input_type + 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_transport_internet_tls_config_proto_init() } +func file_transport_internet_tls_config_proto_init() { + if File_transport_internet_tls_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_tls_config_proto_rawDesc), len(file_transport_internet_tls_config_proto_rawDesc)), + NumEnums: 1, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_transport_internet_tls_config_proto_goTypes, + DependencyIndexes: file_transport_internet_tls_config_proto_depIdxs, + EnumInfos: file_transport_internet_tls_config_proto_enumTypes, + MessageInfos: file_transport_internet_tls_config_proto_msgTypes, + }.Build() + File_transport_internet_tls_config_proto = out.File + file_transport_internet_tls_config_proto_goTypes = nil + file_transport_internet_tls_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/transport/internet/tls/config.proto b/subproject/Xray-core-main/transport/internet/tls/config.proto new file mode 100644 index 00000000..57cd7866 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/tls/config.proto @@ -0,0 +1,89 @@ +syntax = "proto3"; + +package xray.transport.internet.tls; +option csharp_namespace = "Xray.Transport.Internet.Tls"; +option go_package = "github.com/xtls/xray-core/transport/internet/tls"; +option java_package = "com.xray.transport.internet.tls"; +option java_multiple_files = true; + +import "transport/internet/config.proto"; + +message Certificate { + // TLS certificate in x509 format. + bytes certificate = 1; + + // TLS key in x509 format. + bytes key = 2; + + enum Usage { + ENCIPHERMENT = 0; + AUTHORITY_VERIFY = 1; + AUTHORITY_ISSUE = 2; + } + + Usage usage = 3; + + uint64 ocsp_stapling = 4; + + // TLS certificate path + string certificate_path = 5; + + // TLS Key path + string key_path = 6; + + // If true, one-Time Loading + bool One_time_loading = 7; + + bool build_chain = 8; +} + +message Config { + bool allow_insecure = 1; + + // List of certificates to be served on server. + repeated Certificate certificate = 2; + + // Override server name. + string server_name = 3; + + // Lists of string as ALPN values. + repeated string next_protocol = 4; + + // Whether or not to enable session (ticket) resumption. + bool enable_session_resumption = 5; + + // If true, root certificates on the system will not be loaded for + // verification. + bool disable_system_root = 6; + + // The minimum TLS version. + string min_version = 7; + + // The maximum TLS version. + string max_version = 8; + + // Specify cipher suites, except for TLS 1.3. + string cipher_suites = 9; + + // TLS Client Hello fingerprint (uTLS). + string fingerprint = 11; + + bool reject_unknown_sni = 12; + + string master_key_log = 15; + + // Lists of string as CurvePreferences values. + repeated string curve_preferences = 16; + + repeated string verify_peer_cert_by_name = 17; + + bytes ech_server_keys = 18; + + string ech_config_list = 19; + + string ech_force_query = 20; + + SocketConfig ech_socket_settings = 21; + + repeated bytes pinned_peer_cert_sha256 = 22; +} diff --git a/subproject/Xray-core-main/transport/internet/tls/config_other.go b/subproject/Xray-core-main/transport/internet/tls/config_other.go new file mode 100644 index 00000000..efd18c93 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/tls/config_other.go @@ -0,0 +1,55 @@ +//go:build !windows +// +build !windows + +package tls + +import ( + "crypto/x509" + "sync" + + "github.com/xtls/xray-core/common/errors" +) + +type rootCertsCache struct { + sync.Mutex + pool *x509.CertPool +} + +func (c *rootCertsCache) load() (*x509.CertPool, error) { + c.Lock() + defer c.Unlock() + + if c.pool != nil { + return c.pool, nil + } + + pool, err := x509.SystemCertPool() + if err != nil { + return nil, err + } + c.pool = pool + return pool, nil +} + +var rootCerts rootCertsCache + +func (c *Config) getCertPool() (*x509.CertPool, error) { + if c.DisableSystemRoot { + return c.loadSelfCertPool() + } + + if len(c.Certificate) == 0 { + return rootCerts.load() + } + + pool, err := x509.SystemCertPool() + if err != nil { + return nil, errors.New("system root").AtWarning().Base(err) + } + for _, cert := range c.Certificate { + if !pool.AppendCertsFromPEM(cert.Certificate) { + return nil, errors.New("append cert to root").AtWarning().Base(err) + } + } + return pool, nil +} diff --git a/subproject/Xray-core-main/transport/internet/tls/config_test.go b/subproject/Xray-core-main/transport/internet/tls/config_test.go new file mode 100644 index 00000000..7265ed27 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/tls/config_test.go @@ -0,0 +1,99 @@ +package tls_test + +import ( + gotls "crypto/tls" + "crypto/x509" + "testing" + "time" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/protocol/tls/cert" + . "github.com/xtls/xray-core/transport/internet/tls" +) + +func TestCertificateIssuing(t *testing.T) { + ct, _ := cert.MustGenerate(nil, cert.Authority(true), cert.KeyUsage(x509.KeyUsageCertSign)) + certificate := ParseCertificate(ct) + certificate.Usage = Certificate_AUTHORITY_ISSUE + + c := &Config{ + Certificate: []*Certificate{ + certificate, + }, + } + + tlsConfig := c.GetTLSConfig() + xrayCert, err := tlsConfig.GetCertificate(&gotls.ClientHelloInfo{ + ServerName: "www.example.com", + }) + common.Must(err) + + x509Cert, err := x509.ParseCertificate(xrayCert.Certificate[0]) + common.Must(err) + if !x509Cert.NotAfter.After(time.Now()) { + t.Error("NotAfter: ", x509Cert.NotAfter) + } +} + +func TestExpiredCertificate(t *testing.T) { + caCert, _ := cert.MustGenerate(nil, cert.Authority(true), cert.KeyUsage(x509.KeyUsageCertSign)) + expiredCert, _ := cert.MustGenerate(caCert, cert.NotAfter(time.Now().Add(time.Minute*-2)), cert.CommonName("www.example.com"), cert.DNSNames("www.example.com")) + + certificate := ParseCertificate(caCert) + certificate.Usage = Certificate_AUTHORITY_ISSUE + + certificate2 := ParseCertificate(expiredCert) + + c := &Config{ + Certificate: []*Certificate{ + certificate, + certificate2, + }, + } + + tlsConfig := c.GetTLSConfig() + xrayCert, err := tlsConfig.GetCertificate(&gotls.ClientHelloInfo{ + ServerName: "www.example.com", + }) + common.Must(err) + + x509Cert, err := x509.ParseCertificate(xrayCert.Certificate[0]) + common.Must(err) + if !x509Cert.NotAfter.After(time.Now()) { + t.Error("NotAfter: ", x509Cert.NotAfter) + } +} + +func TestInsecureCertificates(t *testing.T) { + c := &Config{} + + tlsConfig := c.GetTLSConfig() + if len(tlsConfig.CipherSuites) > 0 { + t.Fatal("Unexpected tls cipher suites list: ", tlsConfig.CipherSuites) + } +} + +func BenchmarkCertificateIssuing(b *testing.B) { + ct, _ := cert.MustGenerate(nil, cert.Authority(true), cert.KeyUsage(x509.KeyUsageCertSign)) + certificate := ParseCertificate(ct) + certificate.Usage = Certificate_AUTHORITY_ISSUE + + c := &Config{ + Certificate: []*Certificate{ + certificate, + }, + } + + tlsConfig := c.GetTLSConfig() + lenCerts := len(tlsConfig.Certificates) + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + _, _ = tlsConfig.GetCertificate(&gotls.ClientHelloInfo{ + ServerName: "www.example.com", + }) + delete(tlsConfig.NameToCertificate, "www.example.com") + tlsConfig.Certificates = tlsConfig.Certificates[:lenCerts] + } +} diff --git a/subproject/Xray-core-main/transport/internet/tls/config_windows.go b/subproject/Xray-core-main/transport/internet/tls/config_windows.go new file mode 100644 index 00000000..8818befe --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/tls/config_windows.go @@ -0,0 +1,14 @@ +//go:build windows +// +build windows + +package tls + +import "crypto/x509" + +func (c *Config) getCertPool() (*x509.CertPool, error) { + if c.DisableSystemRoot { + return c.loadSelfCertPool() + } + + return nil, nil +} diff --git a/subproject/Xray-core-main/transport/internet/tls/ech.go b/subproject/Xray-core-main/transport/internet/tls/ech.go new file mode 100644 index 00000000..8cfb1251 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/tls/ech.go @@ -0,0 +1,362 @@ +package tls + +import ( + "bytes" + "context" + "crypto/tls" + "encoding/base64" + "encoding/binary" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "sync" + "sync/atomic" + "time" + + utls "github.com/refraction-networking/utls" + "github.com/xtls/xray-core/common/crypto" + dns2 "github.com/xtls/xray-core/features/dns" + "golang.org/x/net/http2" + + "github.com/miekg/dns" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/utils" + "github.com/xtls/xray-core/transport/internet" + "golang.org/x/crypto/cryptobyte" +) + +func ApplyECH(c *Config, config *tls.Config) error { + var ECHConfig []byte + var err error + + var nameToQuery string + if net.ParseAddress(config.ServerName).Family().IsDomain() { + nameToQuery = config.ServerName + } + var DNSServer string + + // for server + if len(c.EchServerKeys) != 0 { + KeySets, err := ConvertToGoECHKeys(c.EchServerKeys) + if err != nil { + return errors.New("Failed to unmarshal ECHKeySetList: ", err) + } + config.EncryptedClientHelloKeys = KeySets + } + + // for client + if len(c.EchConfigList) != 0 { + ECHForceQuery := c.EchForceQuery + switch ECHForceQuery { + case "none", "half", "full": + case "": + ECHForceQuery = "full" // default to full + default: + panic("Invalid ECHForceQuery: " + c.EchForceQuery) + } + defer func() { + // if failed to get ECHConfig, use an invalid one to make connection fail + if err != nil || len(ECHConfig) == 0 { + if ECHForceQuery == "full" { + ECHConfig = []byte{1, 1, 4, 5, 1, 4} + } + } + config.EncryptedClientHelloConfigList = ECHConfig + }() + // direct base64 config + if strings.Contains(c.EchConfigList, "://") { + // query config from dns + parts := strings.Split(c.EchConfigList, "+") + if len(parts) == 2 { + // parse ECH DNS server in format of "example.com+https://1.1.1.1/dns-query" + nameToQuery = parts[0] + DNSServer = parts[1] + } else if len(parts) == 1 { + // normal format + DNSServer = parts[0] + } else { + return errors.New("Invalid ECH DNS server format: ", c.EchConfigList) + } + if nameToQuery == "" { + return errors.New("Using DNS for ECH Config needs serverName or use Server format example.com+https://1.1.1.1/dns-query") + } + ECHConfig, err = QueryRecord(nameToQuery, DNSServer, c.EchForceQuery, c.EchSocketSettings) + if err != nil { + return errors.New("Failed to query ECH DNS record for domain: ", nameToQuery, " at server: ", DNSServer).Base(err) + } + } else { + ECHConfig, err = base64.StdEncoding.DecodeString(c.EchConfigList) + if err != nil { + return errors.New("Failed to unmarshal ECHConfigList: ", err) + } + } + } + + return nil +} + +type ECHConfigCache struct { + configRecord atomic.Pointer[echConfigRecord] + // updateLock is not for preventing concurrent read/write, but for preventing concurrent update + UpdateLock sync.Mutex +} + +type echConfigRecord struct { + config []byte + expire time.Time + err error +} + +var ( + // The keys for both maps must be generated by ECHCacheKey(). + GlobalECHConfigCache = utils.NewTypedSyncMap[string, *ECHConfigCache]() + clientForECHDOH = utils.NewTypedSyncMap[string, *http.Client]() +) + +// sockopt can be nil if not specified. +// if for clientForECHDOH, domain can be empty. +func ECHCacheKey(server, domain string, sockopt *internet.SocketConfig) string { + return server + "|" + domain + "|" + fmt.Sprintf("%p", sockopt) +} + +// Update updates the ECH config for given domain and server. +// this method is concurrent safe, only one update request will be sent, others get the cache. +// if isLockedUpdate is true, it will not try to acquire the lock. +func (c *ECHConfigCache) Update(domain string, server string, isLockedUpdate bool, forceQuery string, sockopt *internet.SocketConfig) ([]byte, error) { + if !isLockedUpdate { + c.UpdateLock.Lock() + defer c.UpdateLock.Unlock() + } + // Double check cache after acquiring lock + configRecord := c.configRecord.Load() + if configRecord.expire.After(time.Now()) && configRecord.err == nil { + errors.LogDebug(context.Background(), "Cache hit for domain after double check: ", domain) + return configRecord.config, configRecord.err + } + // Query ECH config from DNS server + errors.LogDebug(context.Background(), "Trying to query ECH config for domain: ", domain, " with ECH server: ", server) + echConfig, ttl, err := dnsQuery(server, domain, sockopt) + // if in "full", directly return + if err != nil && forceQuery == "full" { + return nil, err + } + if ttl == 0 { + ttl = dns2.DefaultTTL + } + configRecord = &echConfigRecord{ + config: echConfig, + expire: time.Now().Add(time.Duration(ttl) * time.Second), + err: err, + } + c.configRecord.Store(configRecord) + return configRecord.config, configRecord.err +} + +// QueryRecord returns the ECH config for given domain. +// If the record is not in cache or expired, it will query the DNS server and update the cache. +func QueryRecord(domain string, server string, forceQuery string, sockopt *internet.SocketConfig) ([]byte, error) { + GlobalECHConfigCacheKey := ECHCacheKey(server, domain, sockopt) + echConfigCache, ok := GlobalECHConfigCache.Load(GlobalECHConfigCacheKey) + if !ok { + echConfigCache = &ECHConfigCache{} + echConfigCache.configRecord.Store(&echConfigRecord{}) + echConfigCache, _ = GlobalECHConfigCache.LoadOrStore(GlobalECHConfigCacheKey, echConfigCache) + } + configRecord := echConfigCache.configRecord.Load() + if configRecord.expire.After(time.Now()) && (configRecord.err == nil || forceQuery == "none") { + errors.LogDebug(context.Background(), "Cache hit for domain: ", domain) + return configRecord.config, configRecord.err + } + + // If expire is zero value, it means we are in initial state, wait for the query to finish + // otherwise return old value immediately and update in a goroutine + // but if the cache is too old, wait for update + if configRecord.expire == (time.Time{}) || configRecord.expire.Add(time.Hour*4).Before(time.Now()) { + return echConfigCache.Update(domain, server, false, forceQuery, sockopt) + } else { + // If someone already acquired the lock, it means it is updating, do not start another update goroutine + if echConfigCache.UpdateLock.TryLock() { + go func() { + defer echConfigCache.UpdateLock.Unlock() + echConfigCache.Update(domain, server, true, forceQuery, sockopt) + }() + } + return configRecord.config, configRecord.err + } +} + +// dnsQuery is the real func for sending type65 query for given domain to given DNS server. +// return ECH config, TTL and error +func dnsQuery(server string, domain string, sockopt *internet.SocketConfig) ([]byte, uint32, error) { + m := new(dns.Msg) + var dnsResolve []byte + m.SetQuestion(dns.Fqdn(domain), dns.TypeHTTPS) + // for DOH server + if strings.HasPrefix(server, "https://") || strings.HasPrefix(server, "h2c://") { + h2c := strings.HasPrefix(server, "h2c://") + m.SetEdns0(4096, false) // 4096 is the buffer size, false means no DNSSEC + padding := &dns.EDNS0_PADDING{Padding: make([]byte, int(crypto.RandBetween(100, 300)))} + if opt := m.IsEdns0(); opt != nil { + opt.Option = append(opt.Option, padding) + } + // always 0 in DOH + m.Id = 0 + msg, err := m.Pack() + if err != nil { + return nil, 0, err + } + var client *http.Client + serverKey := ECHCacheKey(server, "", sockopt) + if client, _ = clientForECHDOH.Load(serverKey); client == nil { + // All traffic sent by core should via xray's internet.DialSystem + // This involves the behavior of some Android VPN GUI clients + tr := &http2.Transport{ + IdleConnTimeout: net.ConnIdleTimeout, + ReadIdleTimeout: net.ChromeH2KeepAlivePeriod, + DialTLSContext: func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) { + dest, err := net.ParseDestination(network + ":" + addr) + if err != nil { + return nil, err + } + var conn net.Conn + + conn, err = internet.DialSystem(ctx, dest, sockopt) + if err != nil { + return nil, err + } + + if !h2c { + u, err := url.Parse(server) + if err != nil { + return nil, err + } + conn = utls.UClient(conn, &utls.Config{ServerName: u.Hostname()}, utls.HelloChrome_Auto) + if err := conn.(*utls.UConn).HandshakeContext(ctx); err != nil { + return nil, err + } + } + return conn, nil + }, + } + c := &http.Client{ + Timeout: 30 * time.Second, + Transport: tr, + } + client, _ = clientForECHDOH.LoadOrStore(serverKey, c) + } + req, err := http.NewRequest("POST", server, bytes.NewReader(msg)) + if err != nil { + return nil, 0, err + } + req.Header.Set("Accept", "application/dns-message") + req.Header.Set("Content-Type", "application/dns-message") + utils.TryDefaultHeadersWith(req.Header, "fetch") + req.Header.Set("X-Padding", utils.H2Base62Pad(crypto.RandBetween(100, 1000))) + + resp, err := client.Do(req) + if err != nil { + return nil, 0, err + } + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, 0, err + } + if resp.StatusCode != http.StatusOK { + return nil, 0, errors.New("query failed with response code:", resp.StatusCode) + } + dnsResolve = respBody + } else if strings.HasPrefix(server, "udp://") { // for classic udp dns server + udpServerAddr := server[len("udp://"):] + // default port 53 if not specified + if !strings.Contains(udpServerAddr, ":") { + udpServerAddr = udpServerAddr + ":53" + } + dest, err := net.ParseDestination("udp" + ":" + udpServerAddr) + if err != nil { + return nil, 0, errors.New("failed to parse udp dns server ", udpServerAddr, " for ECH: ", err) + } + dnsTimeoutCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + // use xray's internet.DialSystem as mentioned above + conn, err := internet.DialSystem(dnsTimeoutCtx, dest, sockopt) + if err != nil { + return nil, 0, err + } + defer func() { + err := conn.Close() + if err != nil { + errors.LogDebug(context.Background(), "Failed to close connection: ", err) + } + }() + msg, err := m.Pack() + if err != nil { + return nil, 0, err + } + conn.Write(msg) + udpResponse := make([]byte, 512) + conn.SetReadDeadline(time.Now().Add(5 * time.Second)) + _, err = conn.Read(udpResponse) + if err != nil { + return nil, 0, err + } + dnsResolve = udpResponse + } + respMsg := new(dns.Msg) + err := respMsg.Unpack(dnsResolve) + if err != nil { + return nil, 0, errors.New("failed to unpack dns response for ECH: ", err) + } + if len(respMsg.Answer) > 0 { + for _, answer := range respMsg.Answer { + if https, ok := answer.(*dns.HTTPS); ok && https.Hdr.Name == dns.Fqdn(domain) { + for _, v := range https.Value { + if echConfig, ok := v.(*dns.SVCBECHConfig); ok { + errors.LogDebug(context.Background(), "Get ECH config:", echConfig.String(), " TTL:", respMsg.Answer[0].Header().Ttl) + return echConfig.ECH, answer.Header().Ttl, nil + } + } + } + } + } + // empty is valid, means no ECH config found + return nil, dns2.DefaultTTL, nil +} + +var ErrInvalidLen = errors.New("goech: invalid length") + +func ConvertToGoECHKeys(data []byte) ([]tls.EncryptedClientHelloKey, error) { + var keys []tls.EncryptedClientHelloKey + s := cryptobyte.String(data) + for !s.Empty() { + if len(s) < 2 { + return keys, ErrInvalidLen + } + keyLength := int(binary.BigEndian.Uint16(s[:2])) + if len(s) < keyLength+4 { + return keys, ErrInvalidLen + } + configLength := int(binary.BigEndian.Uint16(s[keyLength+2 : keyLength+4])) + if len(s) < 2+keyLength+2+configLength { + return keys, ErrInvalidLen + } + child := cryptobyte.String(s[:2+keyLength+2+configLength]) + var ( + sk, config cryptobyte.String + ) + if !child.ReadUint16LengthPrefixed(&sk) || !child.ReadUint16LengthPrefixed(&config) || !child.Empty() { + return keys, ErrInvalidLen + } + if !s.Skip(2 + keyLength + 2 + configLength) { + return keys, ErrInvalidLen + } + keys = append(keys, tls.EncryptedClientHelloKey{ + Config: config, + PrivateKey: sk, + }) + } + return keys, nil +} diff --git a/subproject/Xray-core-main/transport/internet/tls/ech_test.go b/subproject/Xray-core-main/transport/internet/tls/ech_test.go new file mode 100644 index 00000000..bdf87868 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/tls/ech_test.go @@ -0,0 +1,79 @@ +package tls + +import ( + "io" + "net/http" + "strings" + "sync" + "testing" + + "github.com/xtls/xray-core/common" +) + +func TestECHDial(t *testing.T) { + config := &Config{ + ServerName: "cloudflare.com", + EchConfigList: "encryptedsni.com+udp://1.1.1.1", + } + // test concurrent Dial(to test cache problem) + wg := sync.WaitGroup{} + for range 10 { + wg.Add(1) + go func() { + TLSConfig := config.GetTLSConfig() + TLSConfig.NextProtos = []string{"http/1.1"} + client := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: TLSConfig, + }, + } + resp, err := client.Get("https://cloudflare.com/cdn-cgi/trace") + common.Must(err) + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + common.Must(err) + if !strings.Contains(string(body), "sni=encrypted") { + t.Error("ECH Dial success but SNI is not encrypted") + } + wg.Done() + }() + } + wg.Wait() + // check cache + echConfigCache, ok := GlobalECHConfigCache.Load(ECHCacheKey("udp://1.1.1.1", "encryptedsni.com", nil)) + if !ok { + t.Error("ECH config cache not found") + + } + ok = echConfigCache.UpdateLock.TryLock() + if !ok { + t.Error("ECH config cache dead lock detected") + } + echConfigCache.UpdateLock.Unlock() + configRecord := echConfigCache.configRecord.Load() + if configRecord == nil { + t.Error("ECH config record not found in cache") + } +} + +func TestECHDialFail(t *testing.T) { + config := &Config{ + ServerName: "cloudflare.com", + EchConfigList: "udp://127.0.0.1", + EchForceQuery: "half", + } + config.GetTLSConfig() + // check cache + echConfigCache, ok := GlobalECHConfigCache.Load(ECHCacheKey("udp://127.0.0.1", "cloudflare.com", nil)) + if !ok { + t.Error("ECH config cache not found") + } + configRecord := echConfigCache.configRecord.Load() + if configRecord == nil { + t.Error("ECH config record not found in cache") + return + } + if configRecord.err == nil { + t.Error("unexpected nil error in ECH config record") + } +} diff --git a/subproject/Xray-core-main/transport/internet/tls/grpc.go b/subproject/Xray-core-main/transport/internet/tls/grpc.go new file mode 100644 index 00000000..6e5dc578 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/tls/grpc.go @@ -0,0 +1,108 @@ +package tls + +import ( + "context" + gotls "crypto/tls" + "net" + "net/url" + "strconv" + + utls "github.com/refraction-networking/utls" + "google.golang.org/grpc/credentials" +) + +// grpcUtlsInfo contains the auth information for a TLS authenticated connection. +// It implements the AuthInfo interface. +type grpcUtlsInfo struct { + State utls.ConnectionState + credentials.CommonAuthInfo + // This API is experimental. + SPIFFEID *url.URL +} + +// AuthType returns the type of TLSInfo as a string. +func (t grpcUtlsInfo) AuthType() string { + return "utls" +} + +// GetSecurityValue returns security info requested by channelz. +func (t grpcUtlsInfo) GetSecurityValue() credentials.ChannelzSecurityValue { + v := &credentials.TLSChannelzSecurityValue{ + StandardName: "0x" + strconv.FormatUint(uint64(t.State.CipherSuite), 16), + } + // Currently there's no way to get LocalCertificate info from tls package. + if len(t.State.PeerCertificates) > 0 { + v.RemoteCertificate = t.State.PeerCertificates[0].Raw + } + return v +} + +// grpcUtls is the credentials required for authenticating a connection using TLS. +type grpcUtls struct { + config *gotls.Config + fingerprint *utls.ClientHelloID +} + +func (c grpcUtls) Info() credentials.ProtocolInfo { + return credentials.ProtocolInfo{ + SecurityProtocol: "tls", + SecurityVersion: "1.2", + ServerName: c.config.ServerName, + } +} + +func (c *grpcUtls) ClientHandshake(ctx context.Context, authority string, rawConn net.Conn) (_ net.Conn, _ credentials.AuthInfo, err error) { + // use local cfg to avoid clobbering ServerName if using multiple endpoints + cfg := c.config.Clone() + if cfg.ServerName == "" { + serverName, _, err := net.SplitHostPort(authority) + if err != nil { + // If the authority had no host port or if the authority cannot be parsed, use it as-is. + serverName = authority + } + cfg.ServerName = serverName + } + conn := UClient(rawConn, cfg, c.fingerprint).(*UConn) + errChannel := make(chan error, 1) + go func() { + errChannel <- conn.HandshakeContext(ctx) + close(errChannel) + }() + select { + case err := <-errChannel: + if err != nil { + conn.Close() + return nil, nil, err + } + case <-ctx.Done(): + conn.Close() + return nil, nil, ctx.Err() + } + tlsInfo := grpcUtlsInfo{ + State: conn.ConnectionState(), + CommonAuthInfo: credentials.CommonAuthInfo{ + SecurityLevel: credentials.PrivacyAndIntegrity, + }, + } + return conn, tlsInfo, nil +} + +// ServerHandshake will always panic. We don't support running uTLS as server. +func (c *grpcUtls) ServerHandshake(net.Conn) (net.Conn, credentials.AuthInfo, error) { + panic("not available!") +} + +func (c *grpcUtls) Clone() credentials.TransportCredentials { + return NewGrpcUtls(c.config, c.fingerprint) +} + +func (c *grpcUtls) OverrideServerName(serverNameOverride string) error { + c.config.ServerName = serverNameOverride + return nil +} + +// NewGrpcUtls uses c to construct a TransportCredentials based on uTLS. +func NewGrpcUtls(c *gotls.Config, fingerprint *utls.ClientHelloID) credentials.TransportCredentials { + tc := &grpcUtls{c.Clone(), fingerprint} + return tc +} diff --git a/subproject/Xray-core-main/transport/internet/tls/pin.go b/subproject/Xray-core-main/transport/internet/tls/pin.go new file mode 100644 index 00000000..54029572 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/tls/pin.go @@ -0,0 +1,30 @@ +package tls + +import ( + "crypto/sha256" + "crypto/x509" + "encoding/hex" +) + +// []byte must be ASN.1 DER content +func GenerateCertHash[T *x509.Certificate | []byte](cert T) []byte { + var out [32]byte + switch v := any(cert).(type) { + case *x509.Certificate: + out = sha256.Sum256(v.Raw) + case []byte: + out = sha256.Sum256(v) + } + return out[:] +} + +func GenerateCertHashHex[T *x509.Certificate | []byte](cert T) string { + var out [32]byte + switch v := any(cert).(type) { + case *x509.Certificate: + out = sha256.Sum256(v.Raw) + case []byte: + out = sha256.Sum256(v) + } + return hex.EncodeToString(out[:]) +} diff --git a/subproject/Xray-core-main/transport/internet/tls/pin_test.go b/subproject/Xray-core-main/transport/internet/tls/pin_test.go new file mode 100644 index 00000000..50568df6 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/tls/pin_test.go @@ -0,0 +1,153 @@ +package tls + +import ( + "crypto/tls" + "crypto/x509" + "encoding/hex" + "encoding/pem" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/protocol/tls/cert" +) + +func TestCalculateCertHash(t *testing.T) { + const Single = `-----BEGIN CERTIFICATE----- +MIINWzCCC0OgAwIBAgITMwK6ajqdrV0tahuIrQAAArpqOjANBgkqhkiG9w0BAQwF +ADBdMQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9u +MS4wLAYDVQQDEyVNaWNyb3NvZnQgQXp1cmUgUlNBIFRMUyBJc3N1aW5nIENBIDA0 +MB4XDTI1MDkwOTEwMzE1NloXDTI2MDMwODEwMzE1NlowYzELMAkGA1UEBhMCVVMx +CzAJBgNVBAgTAldBMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3Nv +ZnQgQ29ycG9yYXRpb24xFTATBgNVBAMTDHd3dy5iaW5nLmNvbTCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAMBflymLifrVkjp8K4/XrHSt+/xDrrZIJyTI +JOhIGZJZ88sNjo4OChQWV8O3CTQwrbKJDd6KjZFFc6BPKpEJZ891w2zkymMbE7wh +vQVviSCIVCO+49pLrEvfh5ZvdbXhtNzm/ZRvkoI8h4ZKPBRNmX5sGpSQ9p0loJBj +Jk1HbzLv0vRk5bLb/J6x7YexaAu86C9TjqnC4irO+AZZNI/0S70ZHxX+ETZVV0EX +QU8UmqV68e4YhAQwiLYdAQw125n2hGWoLokQSZTyEiIIoubB00pE5zf0Qaq6Q4s8 +Go5Ukw1A4HjWMisHVKq369pgI8VDZtMzOhS+O0DEQZLwOFETZxECAwEAAaOCCQww +ggkIMIIBgAYKKwYBBAHWeQIEAgSCAXAEggFsAWoAdgCWl2S/VViXrfdDh2g3CEJ3 +6fA61fak8zZuRqQ/D8qpxgAAAZkuEXLdAAAEAwBHMEUCIBLzX4AJgVJdQshSMBLS +hBMQX8zgRm2U3IXjLk37JM3QAiEAkVrmCFx0+BM3NOoCAXBU1WzVuniPxJP3Ysbd +OO3dkEAAdwBkEcRspBLsp4kcogIuALyrTygH1B41J6vq/tUDyX3N8AAAAZkuEXKd +AAAEAwBIMEYCIQCCO1ys+tlI8Fhp4J/Dqk3VVtSi408Nuw8T6YciDL6LPgIhAPjp +fm/gMkASgNimNuMFH8oiJbqeQ/yo2zQfub894iMuAHcAVmzVo3a+g9/jQrZ1xJwj +JJinabrDgsurSaOHfZqzLQEAAAGZLhFy2QAABAMASDBGAiEA/93O6XiiYhfeANHh +0n2nJyVvFAc6sBNT2S7WOR28vR0CIQC7i+leDRRIeY2BYJwaRlAqHlSyU4DZu5IG +caxiWFeavzAnBgkrBgEEAYI3FQoEGjAYMAoGCCsGAQUFBwMCMAoGCCsGAQUFBwMB +MDwGCSsGAQQBgjcVBwQvMC0GJSsGAQQBgjcVCIe91xuB5+tGgoGdLo7QDIfw2h1d +gqvnMIft8R8CAWQCAS0wgbQGCCsGAQUFBwEBBIGnMIGkMHMGCCsGAQUFBzAChmdo +dHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01pY3Jvc29mdCUy +MEF6dXJlJTIwUlNBJTIwVExTJTIwSXNzdWluZyUyMENBJTIwMDQlMjAtJTIweHNp +Z24uY3J0MC0GCCsGAQUFBzABhiFodHRwOi8vb25lb2NzcC5taWNyb3NvZnQuY29t +L29jc3AwHQYDVR0OBBYEFAsWImxddBew8yEv3yGDsmy90FzPMA4GA1UdDwEB/wQE +AwIFoDCCBREGA1UdEQSCBQgwggUEghMqLnBsYXRmb3JtLmJpbmcuY29tggoqLmJp +bmcuY29tgghiaW5nLmNvbYIWaWVvbmxpbmUubWljcm9zb2Z0LmNvbYITKi53aW5k +b3dzc2VhcmNoLmNvbYIZY24uaWVvbmxpbmUubWljcm9zb2Z0LmNvbYIRKi5vcmln +aW4uYmluZy5jb22CDSoubW0uYmluZy5uZXSCDiouYXBpLmJpbmcuY29tgg0qLmNu +LmJpbmcubmV0gg0qLmNuLmJpbmcuY29tghBzc2wtYXBpLmJpbmcuY29tghBzc2wt +YXBpLmJpbmcubmV0gg4qLmFwaS5iaW5nLm5ldIIOKi5iaW5nYXBpcy5jb22CD2Jp +bmdzYW5kYm94LmNvbYIWZmVlZGJhY2subWljcm9zb2Z0LmNvbYIbaW5zZXJ0bWVk +aWEuYmluZy5vZmZpY2UubmV0gg5yLmJhdC5iaW5nLmNvbYIQKi5yLmJhdC5iaW5n +LmNvbYIPKi5kaWN0LmJpbmcuY29tgg4qLnNzbC5iaW5nLmNvbYIQKi5hcHBleC5i +aW5nLmNvbYIWKi5wbGF0Zm9ybS5jbi5iaW5nLmNvbYINd3AubS5iaW5nLmNvbYIM +Ki5tLmJpbmcuY29tgg9nbG9iYWwuYmluZy5jb22CEXdpbmRvd3NzZWFyY2guY29t +gg5zZWFyY2gubXNuLmNvbYIRKi5iaW5nc2FuZGJveC5jb22CGSouYXBpLnRpbGVz +LmRpdHUubGl2ZS5jb22CGCoudDAudGlsZXMuZGl0dS5saXZlLmNvbYIYKi50MS50 +aWxlcy5kaXR1LmxpdmUuY29tghgqLnQyLnRpbGVzLmRpdHUubGl2ZS5jb22CGCou +dDMudGlsZXMuZGl0dS5saXZlLmNvbYILM2QubGl2ZS5jb22CE2FwaS5zZWFyY2gu +bGl2ZS5jb22CFGJldGEuc2VhcmNoLmxpdmUuY29tghVjbndlYi5zZWFyY2gubGl2 +ZS5jb22CDWRpdHUubGl2ZS5jb22CEWZhcmVjYXN0LmxpdmUuY29tgg5pbWFnZS5s +aXZlLmNvbYIPaW1hZ2VzLmxpdmUuY29tghFsb2NhbC5saXZlLmNvbS5hdYIUbG9j +YWxzZWFyY2gubGl2ZS5jb22CFGxzNGQuc2VhcmNoLmxpdmUuY29tgg1tYWlsLmxp +dmUuY29tghFtYXBpbmRpYS5saXZlLmNvbYIObG9jYWwubGl2ZS5jb22CDW1hcHMu +bGl2ZS5jb22CEG1hcHMubGl2ZS5jb20uYXWCD21pbmRpYS5saXZlLmNvbYINbmV3 +cy5saXZlLmNvbYIcb3JpZ2luLmNud2ViLnNlYXJjaC5saXZlLmNvbYIWcHJldmll +dy5sb2NhbC5saXZlLmNvbYIPc2VhcmNoLmxpdmUuY29tghJ0ZXN0Lm1hcHMubGl2 +ZS5jb22CDnZpZGVvLmxpdmUuY29tgg92aWRlb3MubGl2ZS5jb22CFXZpcnR1YWxl +YXJ0aC5saXZlLmNvbYIMd2FwLmxpdmUuY29tghJ3ZWJtYXN0ZXIubGl2ZS5jb22C +FXd3dy5sb2NhbC5saXZlLmNvbS5hdYIUd3d3Lm1hcHMubGl2ZS5jb20uYXWCE3dl +Ym1hc3RlcnMubGl2ZS5jb22CGGVjbi5kZXYudmlydHVhbGVhcnRoLm5ldIIMd3d3 +LmJpbmcuY29tMAwGA1UdEwEB/wQCMAAwagYDVR0fBGMwYTBfoF2gW4ZZaHR0cDov +L3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jcmwvTWljcm9zb2Z0JTIwQXp1cmUl +MjBSU0ElMjBUTFMlMjBJc3N1aW5nJTIwQ0ElMjAwNC5jcmwwZgYDVR0gBF8wXTBR +BgwrBgEEAYI3TIN9AQEwQTA/BggrBgEFBQcCARYzaHR0cDovL3d3dy5taWNyb3Nv +ZnQuY29tL3BraW9wcy9Eb2NzL1JlcG9zaXRvcnkuaHRtMAgGBmeBDAECAjAfBgNV +HSMEGDAWgBQ7cNFT6XYlnWCoymYPxpuub1QWajAdBgNVHSUEFjAUBggrBgEFBQcD +AgYIKwYBBQUHAwEwDQYJKoZIhvcNAQEMBQADggIBAEQCoppNllgoHtfLJt2m7cVL +AILYFxJdi9qc4LUBfaQEdUwAfsC1pSk5YFB0aGcmVFKMvMMOeENOrWgNJVTLYI05 +8mu6XmbiqUeIu1Rlye/yNirYm33Js2f3VXYp6HSzisF5cWq4QwYqA6XIMfDl61/y +IXVb5l5eTfproM2grn3RcVVbk5DuEUfyDPzYYNm8elxzac4RrbkDif/b+tVFxmrJ +CUx1o3VLiVVzbIFCDc5r6pPArm1EdgseJ7pRdXzg6flwA0INRpeLCpjtvkHeZCh7 +GS2JUBhFv7M+lneJljNU/trTkYiho+ZRW9AgLcN73c4+1wHttPHk+w19m5Ge182V +HzCQdO27IGovKN8jkprGafGxYhyCn4KdSYbRrG7fjkckzpJrjCpF2/bJJ+o4Zi9P +rJIKHzY5lIMXcD7wwwT2WwlKXoTDrgm4QKN18V+kZaoOILdKyMlEww4jPFUqk6j1 +0Qeod55F5h4tCq2lmwDIa/jyWTGgqTr4UESqj46NB5+JkGYl0O1PPbS1nUm9sN1l +hkY45iskXVXqLl6AVVcXyxMTefD43M81tFVuJJgpdD/BaMaXAuBdNDfTQcJwhP99 +uI6HqHFD3iEct8fBkYfQiwH2e1eu9OwgujiWHsutyK8VvzVB3/YnhQ/TzciRjPqz +7ykUutQNUALq8dQwoTnK +-----END CERTIFICATE----- + +` + t.Run("singlepublickey", func(t *testing.T) { + block, _ := pem.Decode([]byte(Single)) + cert, err := x509.ParseCertificate(block.Bytes) + assert.Equal(t, err, nil) + hash := GenerateCertHash(cert) + fingerprint, _ := hex.DecodeString("ae243d668ec9c7f74a0dcd1ad21c6676b4efe30c39728934b362093af886bf77") + assert.Equal(t, fingerprint, hash) + }) +} + +func TestVerifyPeerLeafCert(t *testing.T) { + leafCert, leafHash := cert.MustGenerate(nil, cert.DNSNames("example.com")) + leaf := common.Must2(x509.ParseCertificate(leafCert.Certificate)) + + r := &RandCarrier{ + Config: &tls.Config{ + ServerName: "example.com", + }, + PinnedPeerCertSha256: [][]byte{leafHash[:]}, + } + + rawCerts := [][]byte{leaf.Raw} + err := r.verifyPeerCert(rawCerts, nil) + if err != nil { + t.Fatal("expected to verify leaf cert signed by pinned CA, but got error:", err) + } + + // make the pinned hash incorrect + r.PinnedPeerCertSha256[0][0] += 1 + err = r.verifyPeerCert(rawCerts, nil) + if err == nil { + t.Fatal("expected to fail verifying leaf cert with incorrect pinned CA hash, but got no error") + } +} + +func TestVerifyPeerCACert(t *testing.T) { + caCert, caHash := cert.MustGenerate(nil, cert.Authority(true), cert.KeyUsage(x509.KeyUsageCertSign)) + ca := common.Must2(x509.ParseCertificate(caCert.Certificate)) + + leafCert, _ := cert.MustGenerate(caCert, cert.DNSNames("example.com")) + leaf := common.Must2(x509.ParseCertificate(leafCert.Certificate)) + + r := &RandCarrier{ + Config: &tls.Config{ + ServerName: "example.com", + }, + PinnedPeerCertSha256: [][]byte{caHash[:]}, + } + + rawCerts := [][]byte{leaf.Raw, ca.Raw} + err := r.verifyPeerCert(rawCerts, nil) + if err != nil { + t.Fatal("expected to verify leaf cert signed by pinned CA, but got error:", err) + } + + // make the pinned hash incorrect + r.PinnedPeerCertSha256[0][0] += 1 + err = r.verifyPeerCert(rawCerts, nil) + if err == nil { + t.Fatal("expected to fail verifying leaf cert with incorrect pinned CA hash, but got no error") + } +} diff --git a/subproject/Xray-core-main/transport/internet/tls/tls.go b/subproject/Xray-core-main/transport/internet/tls/tls.go new file mode 100644 index 00000000..7bd64a7e --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/tls/tls.go @@ -0,0 +1,268 @@ +package tls + +import ( + "context" + "crypto/rand" + "crypto/tls" + "math/big" + "time" + + utls "github.com/refraction-networking/utls" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/utils" +) + +type Interface interface { + net.Conn + HandshakeContext(ctx context.Context) error + VerifyHostname(host string) error + HandshakeContextServerName(ctx context.Context) string + NegotiatedProtocol() string +} + +var _ buf.Writer = (*Conn)(nil) +var _ Interface = (*Conn)(nil) + +type Conn struct { + *tls.Conn +} + +const tlsCloseTimeout = 250 * time.Millisecond + +func (c *Conn) Close() error { + timer := time.AfterFunc(tlsCloseTimeout, func() { + c.Conn.NetConn().Close() + }) + defer timer.Stop() + return c.Conn.Close() +} + +func (c *Conn) WriteMultiBuffer(mb buf.MultiBuffer) error { + mb = buf.Compact(mb) + mb, err := buf.WriteMultiBuffer(c, mb) + buf.ReleaseMulti(mb) + return err +} + +func (c *Conn) HandshakeContextServerName(ctx context.Context) string { + if err := c.HandshakeContext(ctx); err != nil { + return "" + } + return c.ConnectionState().ServerName +} + +func (c *Conn) NegotiatedProtocol() string { + state := c.ConnectionState() + return state.NegotiatedProtocol +} + +// Client initiates a TLS client handshake on the given connection. +func Client(c net.Conn, config *tls.Config) net.Conn { + tlsConn := tls.Client(c, config) + return &Conn{Conn: tlsConn} +} + +// Server initiates a TLS server handshake on the given connection. +func Server(c net.Conn, config *tls.Config) net.Conn { + tlsConn := tls.Server(c, config) + return &Conn{Conn: tlsConn} +} + +type UConn struct { + *utls.UConn +} + +var _ Interface = (*UConn)(nil) + +func (c *UConn) Close() error { + timer := time.AfterFunc(tlsCloseTimeout, func() { + c.Conn.NetConn().Close() + }) + defer timer.Stop() + return c.Conn.Close() +} + +func (c *UConn) HandshakeContextServerName(ctx context.Context) string { + if err := c.HandshakeContext(ctx); err != nil { + return "" + } + return c.ConnectionState().ServerName +} + +// WebsocketHandshake basically calls UConn.Handshake inside it but it will only send +// http/1.1 in its ALPN. +func (c *UConn) WebsocketHandshakeContext(ctx context.Context) error { + // Build the handshake state. This will apply every variable of the TLS of the + // fingerprint in the UConn + if err := c.BuildHandshakeState(); err != nil { + return err + } + config := *utils.AccessField[*utls.Config](c, "config") + // Do not modify outer ALPN to http/1.1 if ECH is used + // Outer ALPN will be h2,http/1.1, and real ALPN in config will be hidden in ECH + if config.EncryptedClientHelloConfigList != nil { + return c.HandshakeContext(ctx) + } + // Iterate over extensions and check for utls.ALPNExtension + hasALPNExtension := false + for _, extension := range c.Extensions { + if alpn, ok := extension.(*utls.ALPNExtension); ok { + hasALPNExtension = true + alpn.AlpnProtocols = []string{"http/1.1"} + break + } + } + if !hasALPNExtension { // Append extension if doesn't exists + c.Extensions = append(c.Extensions, &utls.ALPNExtension{AlpnProtocols: []string{"http/1.1"}}) + } + // Rebuild the client hello and do the handshake + if err := c.BuildHandshakeState(); err != nil { + return err + } + return c.HandshakeContext(ctx) +} + +func (c *UConn) NegotiatedProtocol() string { + state := c.ConnectionState() + return state.NegotiatedProtocol +} + +func UClient(c net.Conn, config *tls.Config, fingerprint *utls.ClientHelloID) net.Conn { + utlsConn := utls.UClient(c, copyConfig(config), *fingerprint) + return &UConn{UConn: utlsConn} +} + +func GeneraticUClient(c net.Conn, config *tls.Config) *utls.UConn { + return utls.UClient(c, copyConfig(config), utls.HelloChrome_Auto) +} + +func copyConfig(c *tls.Config) *utls.Config { + config := &utls.Config{ + Rand: c.Rand, + RootCAs: c.RootCAs, + ServerName: c.ServerName, + InsecureSkipVerify: c.InsecureSkipVerify, + VerifyPeerCertificate: c.VerifyPeerCertificate, + KeyLogWriter: c.KeyLogWriter, + EncryptedClientHelloConfigList: c.EncryptedClientHelloConfigList, + } + if config.EncryptedClientHelloConfigList != nil { + config.NextProtos = c.NextProtos + } + return config +} + +func init() { + bigInt, _ := rand.Int(rand.Reader, big.NewInt(int64(len(ModernFingerprints)))) + stopAt := int(bigInt.Int64()) + i := 0 + for _, v := range ModernFingerprints { + if i == stopAt { + PresetFingerprints["random"] = v + break + } + i++ + } + weights := utls.DefaultWeights + weights.TLSVersMax_Set_VersionTLS13 = 1 + weights.FirstKeyShare_Set_CurveP256 = 0 + randomized := utls.HelloRandomizedALPN + randomized.Seed, _ = utls.NewPRNGSeed() + randomized.Weights = &weights + randomizednoalpn := utls.HelloRandomizedNoALPN + randomizednoalpn.Seed, _ = utls.NewPRNGSeed() + randomizednoalpn.Weights = &weights + PresetFingerprints["randomized"] = &randomized + PresetFingerprints["randomizednoalpn"] = &randomizednoalpn +} + +func GetFingerprint(name string) (fingerprint *utls.ClientHelloID) { + if name == "" { + return &utls.HelloChrome_Auto + } + if fingerprint = PresetFingerprints[name]; fingerprint != nil { + return + } + if fingerprint = ModernFingerprints[name]; fingerprint != nil { + return + } + if fingerprint = OtherFingerprints[name]; fingerprint != nil { + return + } + return +} + +var PresetFingerprints = map[string]*utls.ClientHelloID{ + // Recommended preset options in GUI clients + "chrome": &utls.HelloChrome_Auto, + "firefox": &utls.HelloFirefox_Auto, + "safari": &utls.HelloSafari_Auto, + "ios": &utls.HelloIOS_Auto, + "android": &utls.HelloAndroid_11_OkHttp, + "edge": &utls.HelloEdge_Auto, + "360": &utls.Hello360_Auto, + "qq": &utls.HelloQQ_Auto, + "random": nil, + "randomized": nil, + "randomizednoalpn": nil, + "unsafe": nil, +} + +var ModernFingerprints = map[string]*utls.ClientHelloID{ + // One of these will be chosen as `random` at startup + "hellofirefox_99": &utls.HelloFirefox_99, + "hellofirefox_102": &utls.HelloFirefox_102, + "hellofirefox_105": &utls.HelloFirefox_105, + "hellofirefox_120": &utls.HelloFirefox_120, + "hellochrome_83": &utls.HelloChrome_83, + "hellochrome_87": &utls.HelloChrome_87, + "hellochrome_96": &utls.HelloChrome_96, + "hellochrome_100": &utls.HelloChrome_100, + "hellochrome_102": &utls.HelloChrome_102, + "hellochrome_106_shuffle": &utls.HelloChrome_106_Shuffle, + "hellochrome_120": &utls.HelloChrome_120, + "hellochrome_131": &utls.HelloChrome_131, + "helloios_13": &utls.HelloIOS_13, + "helloios_14": &utls.HelloIOS_14, + "helloedge_85": &utls.HelloEdge_85, + "helloedge_106": &utls.HelloEdge_106, + "hellosafari_16_0": &utls.HelloSafari_16_0, + "hello360_11_0": &utls.Hello360_11_0, + "helloqq_11_1": &utls.HelloQQ_11_1, +} + +var OtherFingerprints = map[string]*utls.ClientHelloID{ + // Golang, randomized, auto, and fingerprints that are too old + "hellogolang": &utls.HelloGolang, + "hellorandomized": &utls.HelloRandomized, + "hellorandomizedalpn": &utls.HelloRandomizedALPN, + "hellorandomizednoalpn": &utls.HelloRandomizedNoALPN, + "hellofirefox_auto": &utls.HelloFirefox_Auto, + "hellofirefox_55": &utls.HelloFirefox_55, + "hellofirefox_56": &utls.HelloFirefox_56, + "hellofirefox_63": &utls.HelloFirefox_63, + "hellofirefox_65": &utls.HelloFirefox_65, + "hellochrome_auto": &utls.HelloChrome_Auto, + "hellochrome_58": &utls.HelloChrome_58, + "hellochrome_62": &utls.HelloChrome_62, + "hellochrome_70": &utls.HelloChrome_70, + "hellochrome_72": &utls.HelloChrome_72, + "helloios_auto": &utls.HelloIOS_Auto, + "helloios_11_1": &utls.HelloIOS_11_1, + "helloios_12_1": &utls.HelloIOS_12_1, + "helloandroid_11_okhttp": &utls.HelloAndroid_11_OkHttp, + "helloedge_auto": &utls.HelloEdge_Auto, + "hellosafari_auto": &utls.HelloSafari_Auto, + "hello360_auto": &utls.Hello360_Auto, + "hello360_7_5": &utls.Hello360_7_5, + "helloqq_auto": &utls.HelloQQ_Auto, + + // Chrome betas' + "hellochrome_100_psk": &utls.HelloChrome_100_PSK, + "hellochrome_112_psk_shuf": &utls.HelloChrome_112_PSK_Shuf, + "hellochrome_114_padding_psk_shuf": &utls.HelloChrome_114_Padding_PSK_Shuf, + "hellochrome_115_pq": &utls.HelloChrome_115_PQ, + "hellochrome_115_pq_psk": &utls.HelloChrome_115_PQ_PSK, + "hellochrome_120_pq": &utls.HelloChrome_120_PQ, +} diff --git a/subproject/Xray-core-main/transport/internet/tls/unsafe.go b/subproject/Xray-core-main/transport/internet/tls/unsafe.go new file mode 100644 index 00000000..bb212abd --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/tls/unsafe.go @@ -0,0 +1,6 @@ +package tls + +import _ "unsafe" + +//go:linkname errNoCertificates crypto/tls.errNoCertificates +var errNoCertificates error diff --git a/subproject/Xray-core-main/transport/internet/udp/config.go b/subproject/Xray-core-main/transport/internet/udp/config.go new file mode 100644 index 00000000..fada89c2 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/udp/config.go @@ -0,0 +1,12 @@ +package udp + +import ( + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/transport/internet" +) + +func init() { + common.Must(internet.RegisterProtocolConfigCreator(protocolName, func() interface{} { + return new(Config) + })) +} diff --git a/subproject/Xray-core-main/transport/internet/udp/config.pb.go b/subproject/Xray-core-main/transport/internet/udp/config.pb.go new file mode 100644 index 00000000..78900525 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/udp/config.pb.go @@ -0,0 +1,114 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: transport/internet/udp/config.proto + +package udp + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_transport_internet_udp_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_udp_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_transport_internet_udp_config_proto_rawDescGZIP(), []int{0} +} + +var File_transport_internet_udp_config_proto protoreflect.FileDescriptor + +const file_transport_internet_udp_config_proto_rawDesc = "" + + "\n" + + "#transport/internet/udp/config.proto\x12\x1bxray.transport.internet.udp\"\b\n" + + "\x06ConfigBs\n" + + "\x1fcom.xray.transport.internet.udpP\x01Z0github.com/xtls/xray-core/transport/internet/udp\xaa\x02\x1bXray.Transport.Internet.Udpb\x06proto3" + +var ( + file_transport_internet_udp_config_proto_rawDescOnce sync.Once + file_transport_internet_udp_config_proto_rawDescData []byte +) + +func file_transport_internet_udp_config_proto_rawDescGZIP() []byte { + file_transport_internet_udp_config_proto_rawDescOnce.Do(func() { + file_transport_internet_udp_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_transport_internet_udp_config_proto_rawDesc), len(file_transport_internet_udp_config_proto_rawDesc))) + }) + return file_transport_internet_udp_config_proto_rawDescData +} + +var file_transport_internet_udp_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_transport_internet_udp_config_proto_goTypes = []any{ + (*Config)(nil), // 0: xray.transport.internet.udp.Config +} +var file_transport_internet_udp_config_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_transport_internet_udp_config_proto_init() } +func file_transport_internet_udp_config_proto_init() { + if File_transport_internet_udp_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_udp_config_proto_rawDesc), len(file_transport_internet_udp_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_transport_internet_udp_config_proto_goTypes, + DependencyIndexes: file_transport_internet_udp_config_proto_depIdxs, + MessageInfos: file_transport_internet_udp_config_proto_msgTypes, + }.Build() + File_transport_internet_udp_config_proto = out.File + file_transport_internet_udp_config_proto_goTypes = nil + file_transport_internet_udp_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/transport/internet/udp/config.proto b/subproject/Xray-core-main/transport/internet/udp/config.proto new file mode 100644 index 00000000..16e043f9 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/udp/config.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +package xray.transport.internet.udp; +option csharp_namespace = "Xray.Transport.Internet.Udp"; +option go_package = "github.com/xtls/xray-core/transport/internet/udp"; +option java_package = "com.xray.transport.internet.udp"; +option java_multiple_files = true; + +message Config {} diff --git a/subproject/Xray-core-main/transport/internet/udp/dialer.go b/subproject/Xray-core-main/transport/internet/udp/dialer.go new file mode 100644 index 00000000..c930c355 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/udp/dialer.go @@ -0,0 +1,69 @@ +package udp + +import ( + "context" + reflect "reflect" + + "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/net/cnc" + "github.com/xtls/xray-core/transport/internet" + "github.com/xtls/xray-core/transport/internet/stat" +) + +func init() { + common.Must(internet.RegisterTransportDialer(protocolName, + func(ctx context.Context, dest net.Destination, streamSettings *internet.MemoryStreamConfig) (stat.Connection, error) { + var sockopt *internet.SocketConfig + if streamSettings != nil { + sockopt = streamSettings.SocketSettings + } + conn, err := internet.DialSystem(ctx, dest, sockopt) + if err != nil { + return nil, err + } + + if streamSettings != nil && streamSettings.UdpmaskManager != nil { + switch c := conn.(type) { + case *internet.PacketConnWrapper: + pktConn, err := streamSettings.UdpmaskManager.WrapPacketConnClient(c.PacketConn) + if err != nil { + conn.Close() + return nil, errors.New("mask err").Base(err) + } + c.PacketConn = pktConn + case *net.UDPConn: + pktConn, err := streamSettings.UdpmaskManager.WrapPacketConnClient(c) + if err != nil { + conn.Close() + return nil, errors.New("mask err").Base(err) + } + conn = &internet.PacketConnWrapper{ + PacketConn: pktConn, + Dest: c.RemoteAddr().(*net.UDPAddr), + } + case *cnc.Connection: + fakeConn := &internet.FakePacketConn{Conn: c} + pktConn, err := streamSettings.UdpmaskManager.WrapPacketConnClient(fakeConn) + if err != nil { + conn.Close() + return nil, errors.New("mask err").Base(err) + } + conn = &internet.PacketConnWrapper{ + PacketConn: pktConn, + Dest: &net.UDPAddr{ + IP: []byte{0, 0, 0, 0}, + Port: 0, + }, + } + default: + conn.Close() + return nil, errors.New("unknown conn ", reflect.TypeOf(c)) + } + } + + // TODO: handle dialer options + return conn, nil + })) +} diff --git a/subproject/Xray-core-main/transport/internet/udp/dispatcher.go b/subproject/Xray-core-main/transport/internet/udp/dispatcher.go new file mode 100644 index 00000000..963ce662 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/udp/dispatcher.go @@ -0,0 +1,252 @@ +package udp + +import ( + "context" + goerrors "errors" + "io" + "sync" + "time" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol/udp" + "github.com/xtls/xray-core/common/signal" + "github.com/xtls/xray-core/common/signal/done" + "github.com/xtls/xray-core/features/routing" + "github.com/xtls/xray-core/transport" +) + +type ResponseCallback func(ctx context.Context, packet *udp.Packet) + +type connEntry struct { + link *transport.Link + timer *signal.ActivityTimer + cancel context.CancelFunc + closed bool +} + +func (c *connEntry) Close() error { + c.timer.SetTimeout(0) + return nil +} + +func (c *connEntry) terminate() { + if c.closed { + panic("terminate called more than once") + } + c.closed = true + c.cancel() + common.Interrupt(c.link.Reader) + common.Interrupt(c.link.Writer) +} + +type Dispatcher struct { + sync.RWMutex + conn *connEntry + dispatcher routing.Dispatcher + callback ResponseCallback + callClose func() error + closed bool +} + +func NewDispatcher(dispatcher routing.Dispatcher, callback ResponseCallback) *Dispatcher { + return &Dispatcher{ + dispatcher: dispatcher, + callback: callback, + } +} + +func (v *Dispatcher) RemoveRay() { + v.Lock() + defer v.Unlock() + v.closed = true + if v.conn != nil { + v.conn.Close() + v.conn = nil + } +} + +func (v *Dispatcher) getInboundRay(ctx context.Context, dest net.Destination) (*connEntry, error) { + v.Lock() + defer v.Unlock() + + if v.closed { + return nil, errors.New("dispatcher is closed") + } + + if v.conn != nil { + if v.conn.closed { + v.conn = nil + } else { + return v.conn, nil + } + } + + errors.LogInfo(ctx, "establishing new connection for ", dest) + + ctx, cancel := context.WithCancel(ctx) + + link, err := v.dispatcher.Dispatch(ctx, dest) + if err != nil { + cancel() + return nil, errors.New("failed to dispatch request to ", dest).Base(err) + } + + entry := &connEntry{ + link: link, + cancel: cancel, + } + + entry.timer = signal.CancelAfterInactivity(ctx, entry.terminate, time.Minute) + v.conn = entry + go handleInput(ctx, entry, dest, v.callback, v.callClose) + return entry, nil +} + +func (v *Dispatcher) Dispatch(ctx context.Context, destination net.Destination, payload *buf.Buffer) { + // TODO: Add user to destString + errors.LogDebug(ctx, "dispatch request to: ", destination) + + conn, err := v.getInboundRay(ctx, destination) + if err != nil { + errors.LogInfoInner(ctx, err, "failed to get inbound") + return + } + outputStream := conn.link.Writer + if outputStream != nil { + if err := outputStream.WriteMultiBuffer(buf.MultiBuffer{payload}); err != nil { + errors.LogInfoInner(ctx, err, "failed to write first UDP payload") + conn.Close() + return + } + } +} + +func handleInput(ctx context.Context, conn *connEntry, dest net.Destination, callback ResponseCallback, callClose func() error) { + defer func() { + conn.Close() + if callClose != nil { + callClose() + } + }() + + input := conn.link.Reader + timer := conn.timer + + for { + select { + case <-ctx.Done(): + return + default: + } + + mb, err := input.ReadMultiBuffer() + if err != nil { + if !goerrors.Is(err, io.EOF) { + errors.LogInfoInner(ctx, err, "failed to handle UDP input") + } + return + } + timer.Update() + for _, b := range mb { + if b.UDP != nil { + dest = *b.UDP + } + callback(ctx, &udp.Packet{ + Payload: b, + Source: dest, + }) + } + } +} + +type dispatcherConn struct { + dispatcher *Dispatcher + cache chan *udp.Packet + done *done.Instance + ctx context.Context +} + +func DialDispatcher(ctx context.Context, dispatcher routing.Dispatcher) (net.PacketConn, error) { + c := &dispatcherConn{ + cache: make(chan *udp.Packet, 16), + done: done.New(), + ctx: ctx, + } + + d := &Dispatcher{ + dispatcher: dispatcher, + callback: c.callback, + callClose: c.Close, + } + c.dispatcher = d + return c, nil +} + +func (c *dispatcherConn) callback(ctx context.Context, packet *udp.Packet) { + select { + case <-c.done.Wait(): + packet.Payload.Release() + return + case c.cache <- packet: + default: + packet.Payload.Release() + return + } +} + +func (c *dispatcherConn) ReadFrom(p []byte) (int, net.Addr, error) { + var packet *udp.Packet +s: + select { + case <-c.done.Wait(): + select { + case packet = <-c.cache: + break s + default: + return 0, nil, io.EOF + } + case packet = <-c.cache: + } + return copy(p, packet.Payload.Bytes()), &net.UDPAddr{ + IP: packet.Source.Address.IP(), + Port: int(packet.Source.Port), + }, nil +} + +func (c *dispatcherConn) WriteTo(p []byte, addr net.Addr) (int, error) { + buffer := buf.New() + raw := buffer.Extend(buf.Size) + n := copy(raw, p) + buffer.Resize(0, int32(n)) + + destination := net.DestinationFromAddr(addr) + buffer.UDP = &destination + c.dispatcher.Dispatch(c.ctx, destination, buffer) + return n, nil +} + +func (c *dispatcherConn) Close() error { + return c.done.Close() +} + +func (c *dispatcherConn) LocalAddr() net.Addr { + return &net.UDPAddr{ + IP: []byte{0, 0, 0, 0}, + Port: 0, + } +} + +func (c *dispatcherConn) SetDeadline(t time.Time) error { + return nil +} + +func (c *dispatcherConn) SetReadDeadline(t time.Time) error { + return nil +} + +func (c *dispatcherConn) SetWriteDeadline(t time.Time) error { + return nil +} diff --git a/subproject/Xray-core-main/transport/internet/udp/dispatcher_test.go b/subproject/Xray-core-main/transport/internet/udp/dispatcher_test.go new file mode 100644 index 00000000..d33a47be --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/udp/dispatcher_test.go @@ -0,0 +1,90 @@ +package udp_test + +import ( + "context" + "sync/atomic" + "testing" + "time" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol/udp" + "github.com/xtls/xray-core/features/routing" + "github.com/xtls/xray-core/transport" + . "github.com/xtls/xray-core/transport/internet/udp" + "github.com/xtls/xray-core/transport/pipe" +) + +type TestDispatcher struct { + OnDispatch func(ctx context.Context, dest net.Destination) (*transport.Link, error) +} + +func (d *TestDispatcher) Dispatch(ctx context.Context, dest net.Destination) (*transport.Link, error) { + return d.OnDispatch(ctx, dest) +} + +func (d *TestDispatcher) DispatchLink(ctx context.Context, destination net.Destination, outbound *transport.Link) error { + return nil +} + +func (d *TestDispatcher) Start() error { + return nil +} + +func (d *TestDispatcher) Close() error { + return nil +} + +func (*TestDispatcher) Type() interface{} { + return routing.DispatcherType() +} + +func TestSameDestinationDispatching(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + uplinkReader, uplinkWriter := pipe.New(pipe.WithSizeLimit(1024)) + downlinkReader, downlinkWriter := pipe.New(pipe.WithSizeLimit(1024)) + + go func() { + for { + data, err := uplinkReader.ReadMultiBuffer() + if err != nil { + break + } + err = downlinkWriter.WriteMultiBuffer(data) + common.Must(err) + } + }() + + var count uint32 + td := &TestDispatcher{ + OnDispatch: func(ctx context.Context, dest net.Destination) (*transport.Link, error) { + atomic.AddUint32(&count, 1) + return &transport.Link{Reader: downlinkReader, Writer: uplinkWriter}, nil + }, + } + dest := net.UDPDestination(net.LocalHostIP, 53) + + b := buf.New() + b.WriteString("abcd") + + var msgCount uint32 + dispatcher := NewDispatcher(td, func(ctx context.Context, packet *udp.Packet) { + atomic.AddUint32(&msgCount, 1) + }) + + dispatcher.Dispatch(ctx, dest, b) + for i := 0; i < 5; i++ { + dispatcher.Dispatch(ctx, dest, b) + } + + time.Sleep(time.Second) + cancel() + + if count != 1 { + t.Error("count: ", count) + } + if v := atomic.LoadUint32(&msgCount); v != 6 { + t.Error("msgCount: ", v) + } +} diff --git a/subproject/Xray-core-main/transport/internet/udp/hub.go b/subproject/Xray-core-main/transport/internet/udp/hub.go new file mode 100644 index 00000000..5d29d203 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/udp/hub.go @@ -0,0 +1,165 @@ +package udp + +import ( + "context" + + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol/udp" + "github.com/xtls/xray-core/transport/internet" +) + +type HubOption func(h *Hub) + +func HubCapacity(capacity int) HubOption { + return func(h *Hub) { + h.capacity = capacity + } +} + +func HubReceiveOriginalDestination(r bool) HubOption { + return func(h *Hub) { + h.recvOrigDest = r + } +} + +type Hub struct { + conn net.PacketConn + udpConn *net.UDPConn + cache chan *udp.Packet + capacity int + recvOrigDest bool +} + +func ListenUDP(ctx context.Context, address net.Address, port net.Port, streamSettings *internet.MemoryStreamConfig, options ...HubOption) (*Hub, error) { + hub := &Hub{ + capacity: 256, + recvOrigDest: false, + } + for _, opt := range options { + opt(hub) + } + + if address.Family().IsDomain() && address.Domain() == "localhost" { + address = net.LocalHostIP + } + + if address.Family().IsDomain() { + return nil, errors.New("domain address is not allowed for listening: ", address.Domain()) + } + + var sockopt *internet.SocketConfig + if streamSettings != nil { + sockopt = streamSettings.SocketSettings + } + if sockopt != nil && sockopt.ReceiveOriginalDestAddress { + hub.recvOrigDest = true + } + + var err error + hub.conn, err = internet.ListenSystemPacket(ctx, &net.UDPAddr{ + IP: address.IP(), + Port: int(port), + }, sockopt) + if err != nil { + return nil, err + } + + raw := hub.conn + + if streamSettings.UdpmaskManager != nil { + hub.conn, err = streamSettings.UdpmaskManager.WrapPacketConnServer(raw) + if err != nil { + raw.Close() + return nil, errors.New("mask err").Base(err) + } + } + + errors.LogInfo(ctx, "listening UDP on ", address, ":", port) + hub.udpConn, _ = hub.conn.(*net.UDPConn) + hub.cache = make(chan *udp.Packet, hub.capacity) + + go hub.start() + return hub, nil +} + +// Close implements net.Listener. +func (h *Hub) Close() error { + h.conn.Close() + return nil +} + +func (h *Hub) WriteTo(payload []byte, dest net.Destination) (int, error) { + return h.conn.WriteTo(payload, &net.UDPAddr{ + IP: dest.Address.IP(), + Port: int(dest.Port), + }) +} + +func (h *Hub) start() { + c := h.cache + defer close(c) + + oobBytes := make([]byte, 256) + + for { + buffer := buf.New() + var noob int + var udpAddr *net.UDPAddr + rawBytes := buffer.Extend(buf.Size) + + var n int + var err error + if h.udpConn != nil { + n, noob, _, udpAddr, err = ReadUDPMsg(h.udpConn, rawBytes, oobBytes) + } else { + var addr net.Addr + n, addr, err = h.conn.ReadFrom(rawBytes) + if err == nil { + udpAddr = addr.(*net.UDPAddr) + } + } + + if err != nil { + errors.LogInfoInner(context.Background(), err, "failed to read UDP msg") + buffer.Release() + break + } + buffer.Resize(0, int32(n)) + + if buffer.IsEmpty() { + buffer.Release() + continue + } + + payload := &udp.Packet{ + Payload: buffer, + Source: net.UDPDestination(net.IPAddress(udpAddr.IP), net.Port(udpAddr.Port)), + } + if h.recvOrigDest && noob > 0 { + payload.Target = RetrieveOriginalDest(oobBytes[:noob]) + if payload.Target.IsValid() { + errors.LogDebug(context.Background(), "UDP original destination: ", payload.Target) + } else { + errors.LogInfo(context.Background(), "failed to read UDP original destination") + } + } + + select { + case c <- payload: + default: + buffer.Release() + payload.Payload = nil + } + } +} + +// Addr implements net.Listener. +func (h *Hub) Addr() net.Addr { + return h.conn.LocalAddr() +} + +func (h *Hub) Receive() <-chan *udp.Packet { + return h.cache +} diff --git a/subproject/Xray-core-main/transport/internet/udp/hub_darwin.go b/subproject/Xray-core-main/transport/internet/udp/hub_darwin.go new file mode 100644 index 00000000..fa965b3c --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/udp/hub_darwin.go @@ -0,0 +1,46 @@ +//go:build darwin +// +build darwin + +package udp + +import ( + "bytes" + "encoding/gob" + "io" + + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/transport/internet" +) + +// RetrieveOriginalDest from stored laddr, caddr +func RetrieveOriginalDest(oob []byte) net.Destination { + dec := gob.NewDecoder(bytes.NewBuffer(oob)) + var la, ra net.UDPAddr + dec.Decode(&la) + dec.Decode(&ra) + ip, port, err := internet.OriginalDst(&la, &ra) + if err != nil { + return net.Destination{} + } + return net.UDPDestination(net.IPAddress(ip), net.Port(port)) +} + +// ReadUDPMsg stores laddr, caddr for later use +func ReadUDPMsg(conn *net.UDPConn, payload []byte, oob []byte) (int, int, int, *net.UDPAddr, error) { + nBytes, addr, err := conn.ReadFromUDP(payload) + var buf bytes.Buffer + enc := gob.NewEncoder(&buf) + udpAddr, ok := conn.LocalAddr().(*net.UDPAddr) + if !ok { + return 0, 0, 0, nil, errors.New("invalid local address") + } + if addr == nil { + return 0, 0, 0, nil, errors.New("invalid remote address") + } + enc.Encode(udpAddr) + enc.Encode(addr) + var reader io.Reader = &buf + noob, _ := reader.Read(oob) + return nBytes, noob, 0, addr, err +} diff --git a/subproject/Xray-core-main/transport/internet/udp/hub_freebsd.go b/subproject/Xray-core-main/transport/internet/udp/hub_freebsd.go new file mode 100644 index 00000000..6bf9fd87 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/udp/hub_freebsd.go @@ -0,0 +1,46 @@ +//go:build freebsd +// +build freebsd + +package udp + +import ( + "bytes" + "encoding/gob" + "io" + + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/transport/internet" +) + +// RetrieveOriginalDest from stored laddr, caddr +func RetrieveOriginalDest(oob []byte) net.Destination { + dec := gob.NewDecoder(bytes.NewBuffer(oob)) + var la, ra net.UDPAddr + dec.Decode(&la) + dec.Decode(&ra) + ip, port, err := internet.OriginalDst(&la, &ra) + if err != nil { + return net.Destination{} + } + return net.UDPDestination(net.IPAddress(ip), net.Port(port)) +} + +// ReadUDPMsg stores laddr, caddr for later use +func ReadUDPMsg(conn *net.UDPConn, payload []byte, oob []byte) (int, int, int, *net.UDPAddr, error) { + nBytes, addr, err := conn.ReadFromUDP(payload) + var buf bytes.Buffer + enc := gob.NewEncoder(&buf) + udpAddr, ok := conn.LocalAddr().(*net.UDPAddr) + if !ok { + return 0, 0, 0, nil, errors.New("invalid local address") + } + if addr == nil { + return 0, 0, 0, nil, errors.New("invalid remote address") + } + enc.Encode(udpAddr) + enc.Encode(addr) + var reader io.Reader = &buf + noob, _ := reader.Read(oob) + return nBytes, noob, 0, addr, err +} diff --git a/subproject/Xray-core-main/transport/internet/udp/hub_linux.go b/subproject/Xray-core-main/transport/internet/udp/hub_linux.go new file mode 100644 index 00000000..07dc472e --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/udp/hub_linux.go @@ -0,0 +1,34 @@ +//go:build linux +// +build linux + +package udp + +import ( + "syscall" + + "github.com/xtls/xray-core/common/net" + "golang.org/x/sys/unix" +) + +func RetrieveOriginalDest(oob []byte) net.Destination { + msgs, err := syscall.ParseSocketControlMessage(oob) + if err != nil { + return net.Destination{} + } + for _, msg := range msgs { + if msg.Header.Level == syscall.SOL_IP && msg.Header.Type == syscall.IP_RECVORIGDSTADDR { + ip := net.IPAddress(msg.Data[4:8]) + port := net.PortFromBytes(msg.Data[2:4]) + return net.UDPDestination(ip, port) + } else if msg.Header.Level == syscall.SOL_IPV6 && msg.Header.Type == unix.IPV6_RECVORIGDSTADDR { + ip := net.IPAddress(msg.Data[8:24]) + port := net.PortFromBytes(msg.Data[2:4]) + return net.UDPDestination(ip, port) + } + } + return net.Destination{} +} + +func ReadUDPMsg(conn *net.UDPConn, payload []byte, oob []byte) (int, int, int, *net.UDPAddr, error) { + return conn.ReadMsgUDP(payload, oob) +} diff --git a/subproject/Xray-core-main/transport/internet/udp/hub_other.go b/subproject/Xray-core-main/transport/internet/udp/hub_other.go new file mode 100644 index 00000000..3a784183 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/udp/hub_other.go @@ -0,0 +1,17 @@ +//go:build !linux && !freebsd && !darwin +// +build !linux,!freebsd,!darwin + +package udp + +import ( + "github.com/xtls/xray-core/common/net" +) + +func RetrieveOriginalDest(oob []byte) net.Destination { + return net.Destination{} +} + +func ReadUDPMsg(conn *net.UDPConn, payload []byte, oob []byte) (int, int, int, *net.UDPAddr, error) { + nBytes, addr, err := conn.ReadFromUDP(payload) + return nBytes, 0, 0, addr, err +} diff --git a/subproject/Xray-core-main/transport/internet/udp/udp.go b/subproject/Xray-core-main/transport/internet/udp/udp.go new file mode 100644 index 00000000..154bcc7e --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/udp/udp.go @@ -0,0 +1,3 @@ +package udp + +const protocolName = "udp" diff --git a/subproject/Xray-core-main/transport/internet/websocket/config.go b/subproject/Xray-core-main/transport/internet/websocket/config.go new file mode 100644 index 00000000..1778c960 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/websocket/config.go @@ -0,0 +1,35 @@ +package websocket + +import ( + "net/http" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/utils" + "github.com/xtls/xray-core/transport/internet" +) + +func (c *Config) GetNormalizedPath() string { + path := c.Path + if path == "" { + return "/" + } + if path[0] != '/' { + return "/" + path + } + return path +} + +func (c *Config) GetRequestHeader() http.Header { + header := http.Header{} + for k, v := range c.Header { + header.Add(k, v) + } + utils.TryDefaultHeadersWith(header, "ws") + return header +} + +func init() { + common.Must(internet.RegisterProtocolConfigCreator(protocolName, func() interface{} { + return new(Config) + })) +} diff --git a/subproject/Xray-core-main/transport/internet/websocket/config.pb.go b/subproject/Xray-core-main/transport/internet/websocket/config.pb.go new file mode 100644 index 00000000..df49c15a --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/websocket/config.pb.go @@ -0,0 +1,173 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: transport/internet/websocket/config.proto + +package websocket + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + Host string `protobuf:"bytes,1,opt,name=host,proto3" json:"host,omitempty"` + Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` // URL path to the WebSocket service. Empty value means root(/). + Header map[string]string `protobuf:"bytes,3,rep,name=header,proto3" json:"header,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + AcceptProxyProtocol bool `protobuf:"varint,4,opt,name=accept_proxy_protocol,json=acceptProxyProtocol,proto3" json:"accept_proxy_protocol,omitempty"` + Ed uint32 `protobuf:"varint,5,opt,name=ed,proto3" json:"ed,omitempty"` + HeartbeatPeriod uint32 `protobuf:"varint,6,opt,name=heartbeatPeriod,proto3" json:"heartbeatPeriod,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_transport_internet_websocket_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_websocket_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_transport_internet_websocket_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Config) GetHost() string { + if x != nil { + return x.Host + } + return "" +} + +func (x *Config) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *Config) GetHeader() map[string]string { + if x != nil { + return x.Header + } + return nil +} + +func (x *Config) GetAcceptProxyProtocol() bool { + if x != nil { + return x.AcceptProxyProtocol + } + return false +} + +func (x *Config) GetEd() uint32 { + if x != nil { + return x.Ed + } + return 0 +} + +func (x *Config) GetHeartbeatPeriod() uint32 { + if x != nil { + return x.HeartbeatPeriod + } + return 0 +} + +var File_transport_internet_websocket_config_proto protoreflect.FileDescriptor + +const file_transport_internet_websocket_config_proto_rawDesc = "" + + "\n" + + ")transport/internet/websocket/config.proto\x12!xray.transport.internet.websocket\"\xa8\x02\n" + + "\x06Config\x12\x12\n" + + "\x04host\x18\x01 \x01(\tR\x04host\x12\x12\n" + + "\x04path\x18\x02 \x01(\tR\x04path\x12M\n" + + "\x06header\x18\x03 \x03(\v25.xray.transport.internet.websocket.Config.HeaderEntryR\x06header\x122\n" + + "\x15accept_proxy_protocol\x18\x04 \x01(\bR\x13acceptProxyProtocol\x12\x0e\n" + + "\x02ed\x18\x05 \x01(\rR\x02ed\x12(\n" + + "\x0fheartbeatPeriod\x18\x06 \x01(\rR\x0fheartbeatPeriod\x1a9\n" + + "\vHeaderEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\x85\x01\n" + + "%com.xray.transport.internet.websocketP\x01Z6github.com/xtls/xray-core/transport/internet/websocket\xaa\x02!Xray.Transport.Internet.Websocketb\x06proto3" + +var ( + file_transport_internet_websocket_config_proto_rawDescOnce sync.Once + file_transport_internet_websocket_config_proto_rawDescData []byte +) + +func file_transport_internet_websocket_config_proto_rawDescGZIP() []byte { + file_transport_internet_websocket_config_proto_rawDescOnce.Do(func() { + file_transport_internet_websocket_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_transport_internet_websocket_config_proto_rawDesc), len(file_transport_internet_websocket_config_proto_rawDesc))) + }) + return file_transport_internet_websocket_config_proto_rawDescData +} + +var file_transport_internet_websocket_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_transport_internet_websocket_config_proto_goTypes = []any{ + (*Config)(nil), // 0: xray.transport.internet.websocket.Config + nil, // 1: xray.transport.internet.websocket.Config.HeaderEntry +} +var file_transport_internet_websocket_config_proto_depIdxs = []int32{ + 1, // 0: xray.transport.internet.websocket.Config.header:type_name -> xray.transport.internet.websocket.Config.HeaderEntry + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_transport_internet_websocket_config_proto_init() } +func file_transport_internet_websocket_config_proto_init() { + if File_transport_internet_websocket_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_websocket_config_proto_rawDesc), len(file_transport_internet_websocket_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_transport_internet_websocket_config_proto_goTypes, + DependencyIndexes: file_transport_internet_websocket_config_proto_depIdxs, + MessageInfos: file_transport_internet_websocket_config_proto_msgTypes, + }.Build() + File_transport_internet_websocket_config_proto = out.File + file_transport_internet_websocket_config_proto_goTypes = nil + file_transport_internet_websocket_config_proto_depIdxs = nil +} diff --git a/subproject/Xray-core-main/transport/internet/websocket/config.proto b/subproject/Xray-core-main/transport/internet/websocket/config.proto new file mode 100644 index 00000000..0ae73075 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/websocket/config.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +package xray.transport.internet.websocket; +option csharp_namespace = "Xray.Transport.Internet.Websocket"; +option go_package = "github.com/xtls/xray-core/transport/internet/websocket"; +option java_package = "com.xray.transport.internet.websocket"; +option java_multiple_files = true; + +message Config { + string host = 1; + string path = 2; // URL path to the WebSocket service. Empty value means root(/). + map header = 3; + bool accept_proxy_protocol = 4; + uint32 ed = 5; + uint32 heartbeatPeriod = 6; +} diff --git a/subproject/Xray-core-main/transport/internet/websocket/connection.go b/subproject/Xray-core-main/transport/internet/websocket/connection.go new file mode 100644 index 00000000..26082fc6 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/websocket/connection.go @@ -0,0 +1,124 @@ +package websocket + +import ( + "io" + "net" + "time" + + "github.com/gorilla/websocket" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/serial" +) + +var _ buf.Writer = (*connection)(nil) + +// connection is a wrapper for net.Conn over WebSocket connection. +// remoteAddr is used to pass "virtual" remote IP addresses in X-Forwarded-For. +// so we shouldn't directly read it form conn. +type connection struct { + conn *websocket.Conn + reader io.Reader + remoteAddr net.Addr +} + +func NewConnection(conn *websocket.Conn, remoteAddr net.Addr, extraReader io.Reader, heartbeatPeriod uint32) *connection { + if heartbeatPeriod != 0 { + go func() { + for { + time.Sleep(time.Duration(heartbeatPeriod) * time.Second) + if err := conn.WriteControl(websocket.PingMessage, []byte{}, time.Time{}); err != nil { + break + } + } + }() + } + + return &connection{ + conn: conn, + remoteAddr: remoteAddr, + reader: extraReader, + } +} + +// Read implements net.Conn.Read() +func (c *connection) Read(b []byte) (int, error) { + for { + reader, err := c.getReader() + if err != nil { + return 0, err + } + + nBytes, err := reader.Read(b) + if errors.Cause(err) == io.EOF { + c.reader = nil + continue + } + return nBytes, err + } +} + +func (c *connection) getReader() (io.Reader, error) { + if c.reader != nil { + return c.reader, nil + } + + _, reader, err := c.conn.NextReader() + if err != nil { + return nil, err + } + c.reader = reader + return reader, nil +} + +// Write implements io.Writer. +func (c *connection) Write(b []byte) (int, error) { + if err := c.conn.WriteMessage(websocket.BinaryMessage, b); err != nil { + return 0, err + } + return len(b), nil +} + +func (c *connection) WriteMultiBuffer(mb buf.MultiBuffer) error { + mb = buf.Compact(mb) + mb, err := buf.WriteMultiBuffer(c, mb) + buf.ReleaseMulti(mb) + return err +} + +func (c *connection) Close() error { + var errs []interface{} + if err := c.conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""), time.Now().Add(time.Second*5)); err != nil { + errs = append(errs, err) + } + if err := c.conn.Close(); err != nil { + errs = append(errs, err) + } + if len(errs) > 0 { + return errors.New("failed to close connection").Base(errors.New(serial.Concat(errs...))) + } + return nil +} + +func (c *connection) LocalAddr() net.Addr { + return c.conn.LocalAddr() +} + +func (c *connection) RemoteAddr() net.Addr { + return c.remoteAddr +} + +func (c *connection) SetDeadline(t time.Time) error { + if err := c.SetReadDeadline(t); err != nil { + return err + } + return c.SetWriteDeadline(t) +} + +func (c *connection) SetReadDeadline(t time.Time) error { + return c.conn.SetReadDeadline(t) +} + +func (c *connection) SetWriteDeadline(t time.Time) error { + return c.conn.SetWriteDeadline(t) +} diff --git a/subproject/Xray-core-main/transport/internet/websocket/dialer.go b/subproject/Xray-core-main/transport/internet/websocket/dialer.go new file mode 100644 index 00000000..e5354908 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/websocket/dialer.go @@ -0,0 +1,208 @@ +package websocket + +import ( + "context" + _ "embed" + "encoding/base64" + "io" + "time" + + "github.com/gorilla/websocket" + "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/transport/internet" + "github.com/xtls/xray-core/transport/internet/browser_dialer" + "github.com/xtls/xray-core/transport/internet/stat" + "github.com/xtls/xray-core/transport/internet/tls" +) + +// Dial dials a WebSocket connection to the given destination. +func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.MemoryStreamConfig) (stat.Connection, error) { + errors.LogInfo(ctx, "creating connection to ", dest) + var conn net.Conn + if streamSettings.ProtocolSettings.(*Config).Ed > 0 { + ctx, cancel := context.WithCancel(ctx) + conn = &delayDialConn{ + dialed: make(chan bool, 1), + cancel: cancel, + ctx: ctx, + dest: dest, + streamSettings: streamSettings, + } + } else { + var err error + if conn, err = dialWebSocket(ctx, dest, streamSettings, nil); err != nil { + return nil, errors.New("failed to dial WebSocket").Base(err) + } + } + return stat.Connection(conn), nil +} + +func init() { + common.Must(internet.RegisterTransportDialer(protocolName, Dial)) +} + +func dialWebSocket(ctx context.Context, dest net.Destination, streamSettings *internet.MemoryStreamConfig, ed []byte) (net.Conn, error) { + wsSettings := streamSettings.ProtocolSettings.(*Config) + + dialer := &websocket.Dialer{ + NetDial: func(network, addr string) (net.Conn, error) { + conn, err := internet.DialSystem(ctx, dest, streamSettings.SocketSettings) + if err != nil { + return nil, err + } + + if streamSettings.TcpmaskManager != nil { + newConn, err := streamSettings.TcpmaskManager.WrapConnClient(conn) + if err != nil { + conn.Close() + return nil, errors.New("mask err").Base(err) + } + conn = newConn + } + + return conn, err + }, + ReadBufferSize: 4 * 1024, + WriteBufferSize: 4 * 1024, + HandshakeTimeout: time.Second * 8, + } + + protocol := "ws" + + tConfig := tls.ConfigFromStreamSettings(streamSettings) + if tConfig != nil { + protocol = "wss" + tlsConfig := tConfig.GetTLSConfig(tls.WithDestination(dest), tls.WithNextProto("http/1.1")) + dialer.TLSClientConfig = tlsConfig + if fingerprint := tls.GetFingerprint(tConfig.Fingerprint); fingerprint != nil { + dialer.NetDialTLSContext = func(_ context.Context, _, addr string) (net.Conn, error) { + // Like the NetDial in the dialer + pconn, err := internet.DialSystem(ctx, dest, streamSettings.SocketSettings) + if err != nil { + errors.LogErrorInner(ctx, err, "failed to dial to "+addr) + return nil, err + } + + if streamSettings.TcpmaskManager != nil { + newConn, err := streamSettings.TcpmaskManager.WrapConnClient(pconn) + if err != nil { + pconn.Close() + return nil, errors.New("mask err").Base(err) + } + pconn = newConn + } + + // TLS and apply the handshake + cn := tls.UClient(pconn, tlsConfig, fingerprint).(*tls.UConn) + if err := cn.WebsocketHandshakeContext(ctx); err != nil { + errors.LogErrorInner(ctx, err, "failed to dial to "+addr) + return nil, err + } + if !tlsConfig.InsecureSkipVerify { + if err := cn.VerifyHostname(tlsConfig.ServerName); err != nil { + errors.LogErrorInner(ctx, err, "failed to dial to "+addr) + return nil, err + } + } + return cn, nil + } + } + } + + host := dest.NetAddr() + if (protocol == "ws" && dest.Port == 80) || (protocol == "wss" && dest.Port == 443) { + host = dest.Address.String() + } + uri := protocol + "://" + host + wsSettings.GetNormalizedPath() + + if browser_dialer.HasBrowserDialer() { + conn, err := browser_dialer.DialWS(uri, ed) + if err != nil { + return nil, err + } + + return NewConnection(conn, conn.RemoteAddr(), nil, wsSettings.HeartbeatPeriod), nil + } + + header := wsSettings.GetRequestHeader() + // See dialer.DialContext() + header.Set("Host", wsSettings.Host) + if header.Get("Host") == "" && tConfig != nil { + header.Set("Host", tConfig.ServerName) + } + if header.Get("Host") == "" { + header.Set("Host", dest.Address.String()) + } + if ed != nil { + // RawURLEncoding is support by both V2Ray/V2Fly and XRay. + header.Set("Sec-WebSocket-Protocol", base64.RawURLEncoding.EncodeToString(ed)) + } + + conn, resp, err := dialer.DialContext(ctx, uri, header) + if err != nil { + var reason string + if resp != nil { + reason = resp.Status + } + return nil, errors.New("failed to dial to (", uri, "): ", reason).Base(err) + } + + return NewConnection(conn, conn.RemoteAddr(), nil, wsSettings.HeartbeatPeriod), nil +} + +type delayDialConn struct { + net.Conn + closed bool + dialed chan bool + cancel context.CancelFunc + ctx context.Context + dest net.Destination + streamSettings *internet.MemoryStreamConfig +} + +func (d *delayDialConn) Write(b []byte) (int, error) { + if d.closed { + return 0, io.ErrClosedPipe + } + if d.Conn == nil { + ed := b + if len(ed) > int(d.streamSettings.ProtocolSettings.(*Config).Ed) { + ed = nil + } + var err error + if d.Conn, err = dialWebSocket(d.ctx, d.dest, d.streamSettings, ed); err != nil { + d.Close() + return 0, errors.New("failed to dial WebSocket").Base(err) + } + d.dialed <- true + if ed != nil { + return len(ed), nil + } + } + return d.Conn.Write(b) +} + +func (d *delayDialConn) Read(b []byte) (int, error) { + if d.closed { + return 0, io.ErrClosedPipe + } + if d.Conn == nil { + select { + case <-d.ctx.Done(): + return 0, io.ErrUnexpectedEOF + case <-d.dialed: + } + } + return d.Conn.Read(b) +} + +func (d *delayDialConn) Close() error { + d.closed = true + d.cancel() + if d.Conn == nil { + return nil + } + return d.Conn.Close() +} diff --git a/subproject/Xray-core-main/transport/internet/websocket/hub.go b/subproject/Xray-core-main/transport/internet/websocket/hub.go new file mode 100644 index 00000000..73799174 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/websocket/hub.go @@ -0,0 +1,180 @@ +package websocket + +import ( + "bytes" + "context" + "crypto/tls" + "encoding/base64" + "io" + "net/http" + "strings" + "sync" + "time" + + "github.com/gorilla/websocket" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + http_proto "github.com/xtls/xray-core/common/protocol/http" + "github.com/xtls/xray-core/transport/internet" + v2tls "github.com/xtls/xray-core/transport/internet/tls" +) + +type requestHandler struct { + host string + path string + ln *Listener + socketSettings *internet.SocketConfig +} + +var replacer = strings.NewReplacer("+", "-", "/", "_", "=", "") + +var upgrader = &websocket.Upgrader{ + ReadBufferSize: 0, + WriteBufferSize: 0, + HandshakeTimeout: time.Second * 4, + CheckOrigin: func(r *http.Request) bool { + return true + }, +} + +func (h *requestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) { + if len(h.host) > 0 && !internet.IsValidHTTPHost(request.Host, h.host) { + errors.LogInfo(context.Background(), "failed to validate host, request:", request.Host, ", config:", h.host) + writer.WriteHeader(http.StatusNotFound) + return + } + if request.URL.Path != h.path { + errors.LogInfo(context.Background(), "failed to validate path, request:", request.URL.Path, ", config:", h.path) + writer.WriteHeader(http.StatusNotFound) + return + } + + var extraReader io.Reader + responseHeader := http.Header{} + if str := request.Header.Get("Sec-WebSocket-Protocol"); str != "" { + if ed, err := base64.RawURLEncoding.DecodeString(replacer.Replace(str)); err == nil && len(ed) > 0 { + extraReader = bytes.NewReader(ed) + responseHeader.Set("Sec-WebSocket-Protocol", str) + } + } + + conn, err := upgrader.Upgrade(writer, request, responseHeader) + if err != nil { + errors.LogInfoInner(context.Background(), err, "failed to convert to WebSocket connection") + return + } + + var forwardedAddrs []net.Address + if h.socketSettings != nil && len(h.socketSettings.TrustedXForwardedFor) > 0 { + for _, key := range h.socketSettings.TrustedXForwardedFor { + if len(request.Header.Values(key)) > 0 { + forwardedAddrs = http_proto.ParseXForwardedFor(request.Header) + break + } + } + } else { + forwardedAddrs = http_proto.ParseXForwardedFor(request.Header) + } + remoteAddr := conn.RemoteAddr() + if len(forwardedAddrs) > 0 && forwardedAddrs[0].Family().IsIP() { + remoteAddr = &net.TCPAddr{ + IP: forwardedAddrs[0].IP(), + Port: int(0), + } + } + + h.ln.addConn(NewConnection(conn, remoteAddr, extraReader, h.ln.config.HeartbeatPeriod)) +} + +type Listener struct { + sync.Mutex + server http.Server + listener net.Listener + config *Config + addConn internet.ConnHandler +} + +func ListenWS(ctx context.Context, address net.Address, port net.Port, streamSettings *internet.MemoryStreamConfig, addConn internet.ConnHandler) (internet.Listener, error) { + l := &Listener{ + addConn: addConn, + } + wsSettings := streamSettings.ProtocolSettings.(*Config) + l.config = wsSettings + if l.config != nil { + if streamSettings.SocketSettings == nil { + streamSettings.SocketSettings = &internet.SocketConfig{} + } + streamSettings.SocketSettings.AcceptProxyProtocol = l.config.AcceptProxyProtocol || streamSettings.SocketSettings.AcceptProxyProtocol + } + var listener net.Listener + var err error + if port == net.Port(0) { // unix + listener, err = internet.ListenSystem(ctx, &net.UnixAddr{ + Name: address.Domain(), + Net: "unix", + }, streamSettings.SocketSettings) + if err != nil { + return nil, errors.New("failed to listen unix domain socket(for WS) on ", address).Base(err) + } + errors.LogInfo(ctx, "listening unix domain socket(for WS) on ", address) + } else { // tcp + listener, err = internet.ListenSystem(ctx, &net.TCPAddr{ + IP: address.IP(), + Port: int(port), + }, streamSettings.SocketSettings) + if err != nil { + return nil, errors.New("failed to listen TCP(for WS) on ", address, ":", port).Base(err) + } + errors.LogInfo(ctx, "listening TCP(for WS) on ", address, ":", port) + } + + if streamSettings.TcpmaskManager != nil { + listener, _ = streamSettings.TcpmaskManager.WrapListener(listener) + } + + if streamSettings.SocketSettings != nil && streamSettings.SocketSettings.AcceptProxyProtocol { + errors.LogWarning(ctx, "accepting PROXY protocol") + } + + if config := v2tls.ConfigFromStreamSettings(streamSettings); config != nil { + if tlsConfig := config.GetTLSConfig(); tlsConfig != nil { + listener = tls.NewListener(listener, tlsConfig) + } + } + + l.listener = listener + + l.server = http.Server{ + Handler: &requestHandler{ + host: wsSettings.Host, + path: wsSettings.GetNormalizedPath(), + ln: l, + socketSettings: streamSettings.SocketSettings, + }, + ReadHeaderTimeout: time.Second * 4, + MaxHeaderBytes: 8192, + } + + go func() { + if err := l.server.Serve(l.listener); err != nil { + errors.LogWarningInner(ctx, err, "failed to serve http for WebSocket") + } + }() + + return l, err +} + +// Addr implements net.Listener.Addr(). +func (ln *Listener) Addr() net.Addr { + return ln.listener.Addr() +} + +// Close implements net.Listener.Close(). +func (ln *Listener) Close() error { + return ln.listener.Close() +} + +func init() { + common.Must(internet.RegisterTransportListener(protocolName, ListenWS)) +} diff --git a/subproject/Xray-core-main/transport/internet/websocket/ws.go b/subproject/Xray-core-main/transport/internet/websocket/ws.go new file mode 100644 index 00000000..f05ded37 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/websocket/ws.go @@ -0,0 +1,8 @@ +/* +Package websocket implements WebSocket transport + +WebSocket transport implements an HTTP(S) compliable, surveillance proof transport method with plausible deniability. +*/ +package websocket + +const protocolName = "websocket" diff --git a/subproject/Xray-core-main/transport/internet/websocket/ws_test.go b/subproject/Xray-core-main/transport/internet/websocket/ws_test.go new file mode 100644 index 00000000..aab28af6 --- /dev/null +++ b/subproject/Xray-core-main/transport/internet/websocket/ws_test.go @@ -0,0 +1,155 @@ +package websocket_test + +import ( + "context" + "runtime" + "testing" + "time" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/protocol/tls/cert" + "github.com/xtls/xray-core/testing/servers/tcp" + "github.com/xtls/xray-core/transport/internet" + "github.com/xtls/xray-core/transport/internet/stat" + "github.com/xtls/xray-core/transport/internet/tls" + . "github.com/xtls/xray-core/transport/internet/websocket" +) + +func Test_listenWSAndDial(t *testing.T) { + listenPort := tcp.PickPort() + listen, err := ListenWS(context.Background(), net.LocalHostIP, listenPort, &internet.MemoryStreamConfig{ + ProtocolName: "websocket", + ProtocolSettings: &Config{ + Path: "ws", + }, + }, func(conn stat.Connection) { + go func(c stat.Connection) { + defer c.Close() + + var b [1024]byte + c.SetReadDeadline(time.Now().Add(2 * time.Second)) + _, err := c.Read(b[:]) + if err != nil { + return + } + + common.Must2(c.Write([]byte("Response"))) + }(conn) + }) + common.Must(err) + + ctx := context.Background() + streamSettings := &internet.MemoryStreamConfig{ + ProtocolName: "websocket", + ProtocolSettings: &Config{Path: "ws"}, + } + conn, err := Dial(ctx, net.TCPDestination(net.DomainAddress("localhost"), listenPort), streamSettings) + + common.Must(err) + _, err = conn.Write([]byte("Test connection 1")) + common.Must(err) + + var b [1024]byte + n, err := conn.Read(b[:]) + common.Must(err) + if string(b[:n]) != "Response" { + t.Error("response: ", string(b[:n])) + } + + common.Must(conn.Close()) + conn, err = Dial(ctx, net.TCPDestination(net.DomainAddress("localhost"), listenPort), streamSettings) + common.Must(err) + _, err = conn.Write([]byte("Test connection 2")) + common.Must(err) + n, err = conn.Read(b[:]) + common.Must(err) + if string(b[:n]) != "Response" { + t.Error("response: ", string(b[:n])) + } + common.Must(conn.Close()) + + common.Must(listen.Close()) +} + +func TestDialWithRemoteAddr(t *testing.T) { + listenPort := tcp.PickPort() + listen, err := ListenWS(context.Background(), net.LocalHostIP, listenPort, &internet.MemoryStreamConfig{ + ProtocolName: "websocket", + ProtocolSettings: &Config{ + Path: "ws", + }, + }, func(conn stat.Connection) { + go func(c stat.Connection) { + defer c.Close() + + var b [1024]byte + _, err := c.Read(b[:]) + // common.Must(err) + if err != nil { + return + } + + _, err = c.Write([]byte(c.RemoteAddr().String())) + common.Must(err) + }(conn) + }) + common.Must(err) + + conn, err := Dial(context.Background(), net.TCPDestination(net.DomainAddress("localhost"), listenPort), &internet.MemoryStreamConfig{ + ProtocolName: "websocket", + ProtocolSettings: &Config{Path: "ws", Header: map[string]string{"X-Forwarded-For": "1.1.1.1"}}, + }) + + common.Must(err) + _, err = conn.Write([]byte("Test connection 1")) + common.Must(err) + + var b [1024]byte + n, err := conn.Read(b[:]) + common.Must(err) + if string(b[:n]) != "1.1.1.1:0" { + t.Error("response: ", string(b[:n])) + } + + common.Must(listen.Close()) +} + +func Test_listenWSAndDial_TLS(t *testing.T) { + listenPort := tcp.PickPort() + if runtime.GOARCH == "arm64" { + return + } + + start := time.Now() + + ct, ctHash := cert.MustGenerate(nil, cert.CommonName("localhost")) + + streamSettings := &internet.MemoryStreamConfig{ + ProtocolName: "websocket", + ProtocolSettings: &Config{ + Path: "wss", + }, + SecurityType: "tls", + SecuritySettings: &tls.Config{ + Certificate: []*tls.Certificate{tls.ParseCertificate(ct)}, + PinnedPeerCertSha256: [][]byte{ctHash[:]}, + }, + } + listen, err := ListenWS(context.Background(), net.LocalHostIP, listenPort, streamSettings, func(conn stat.Connection) { + go func() { + _ = conn.Close() + }() + }) + common.Must(err) + defer listen.Close() + + conn, err := Dial(context.Background(), net.TCPDestination(net.DomainAddress("localhost"), listenPort), streamSettings) + common.Must(err) + _ = conn.Close() + + end := time.Now() + if !end.Before(start.Add(time.Second * 5)) { + t.Error("end: ", end, " start: ", start) + } +} diff --git a/subproject/Xray-core-main/transport/link.go b/subproject/Xray-core-main/transport/link.go new file mode 100644 index 00000000..53e310e0 --- /dev/null +++ b/subproject/Xray-core-main/transport/link.go @@ -0,0 +1,9 @@ +package transport + +import "github.com/xtls/xray-core/common/buf" + +// Link is a utility for connecting between an inbound and an outbound proxy handler. +type Link struct { + Reader buf.Reader + Writer buf.Writer +} diff --git a/subproject/Xray-core-main/transport/pipe/impl.go b/subproject/Xray-core-main/transport/pipe/impl.go new file mode 100644 index 00000000..81172906 --- /dev/null +++ b/subproject/Xray-core-main/transport/pipe/impl.go @@ -0,0 +1,209 @@ +package pipe + +import ( + "errors" + "io" + "sync" + "time" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/signal" + "github.com/xtls/xray-core/common/signal/done" +) + +type state byte + +const ( + open state = iota + closed + errord +) + +type pipeOption struct { + limit int32 // maximum buffer size in bytes + discardOverflow bool +} + +func (o *pipeOption) isFull(curSize int32) bool { + return o.limit >= 0 && curSize > o.limit +} + +type pipe struct { + sync.Mutex + data buf.MultiBuffer + readSignal *signal.Notifier + writeSignal *signal.Notifier + done *done.Instance + errChan chan error + option pipeOption + state state +} + +var ( + errBufferFull = errors.New("buffer full") + errSlowDown = errors.New("slow down") +) + +func (p *pipe) Len() int32 { + data := p.data + if data == nil { + return 0 + } + return data.Len() +} + +func (p *pipe) getState(forRead bool) error { + switch p.state { + case open: + if !forRead && p.option.isFull(p.data.Len()) { + return errBufferFull + } + return nil + case closed: + if !forRead { + return io.ErrClosedPipe + } + if !p.data.IsEmpty() { + return nil + } + return io.EOF + case errord: + return io.ErrClosedPipe + default: + panic("impossible case") + } +} + +func (p *pipe) readMultiBufferInternal() (buf.MultiBuffer, error) { + p.Lock() + defer p.Unlock() + + if err := p.getState(true); err != nil { + return nil, err + } + + data := p.data + p.data = nil + return data, nil +} + +func (p *pipe) ReadMultiBuffer() (buf.MultiBuffer, error) { + for { + data, err := p.readMultiBufferInternal() + if data != nil || err != nil { + p.writeSignal.Signal() + return data, err + } + + select { + case <-p.readSignal.Wait(): + case <-p.done.Wait(): + case err = <-p.errChan: + return nil, err + } + } +} + +func (p *pipe) ReadMultiBufferTimeout(d time.Duration) (buf.MultiBuffer, error) { + timer := time.NewTimer(d) + defer timer.Stop() + + for { + data, err := p.readMultiBufferInternal() + if data != nil || err != nil { + p.writeSignal.Signal() + return data, err + } + + select { + case <-p.readSignal.Wait(): + case <-p.done.Wait(): + case <-timer.C: + return nil, buf.ErrReadTimeout + } + } +} + +func (p *pipe) writeMultiBufferInternal(mb buf.MultiBuffer) error { + p.Lock() + defer p.Unlock() + + if err := p.getState(false); err != nil { + return err + } + + if p.data == nil { + p.data = mb + } else { + p.data, _ = buf.MergeMulti(p.data, mb) + } + return nil +} + +func (p *pipe) WriteMultiBuffer(mb buf.MultiBuffer) error { + if mb.IsEmpty() { + return nil + } + + for { + err := p.writeMultiBufferInternal(mb) + if err == nil { + p.readSignal.Signal() + return nil + } + + if err == errBufferFull { + if p.option.discardOverflow { + buf.ReleaseMulti(mb) + return nil + } + select { + case <-p.writeSignal.Wait(): + continue + case <-p.done.Wait(): + buf.ReleaseMulti(mb) + return io.ErrClosedPipe + } + } + + buf.ReleaseMulti(mb) + p.readSignal.Signal() + return err + } +} + +func (p *pipe) Close() error { + p.Lock() + defer p.Unlock() + + if p.state == closed || p.state == errord { + return nil + } + + p.state = closed + common.Must(p.done.Close()) + return nil +} + +// Interrupt implements common.Interruptible. +func (p *pipe) Interrupt() { + p.Lock() + defer p.Unlock() + + if !p.data.IsEmpty() { + buf.ReleaseMulti(p.data) + p.data = nil + if p.state == closed { + p.state = errord + } + } + + if p.state == closed || p.state == errord { + return + } + + p.state = errord + + common.Must(p.done.Close()) +} diff --git a/subproject/Xray-core-main/transport/pipe/pipe.go b/subproject/Xray-core-main/transport/pipe/pipe.go new file mode 100644 index 00000000..f4b78303 --- /dev/null +++ b/subproject/Xray-core-main/transport/pipe/pipe.go @@ -0,0 +1,70 @@ +package pipe + +import ( + "context" + + "github.com/xtls/xray-core/common/signal" + "github.com/xtls/xray-core/common/signal/done" + "github.com/xtls/xray-core/features/policy" +) + +// Option for creating new Pipes. +type Option func(*pipeOption) + +// WithoutSizeLimit returns an Option for Pipe to have no size limit. +func WithoutSizeLimit() Option { + return func(opt *pipeOption) { + opt.limit = -1 + } +} + +// WithSizeLimit returns an Option for Pipe to have the given size limit. +func WithSizeLimit(limit int32) Option { + return func(opt *pipeOption) { + opt.limit = limit + } +} + +// DiscardOverflow returns an Option for Pipe to discard writes if full. +func DiscardOverflow() Option { + return func(opt *pipeOption) { + opt.discardOverflow = true + } +} + +// OptionsFromContext returns a list of Options from context. +func OptionsFromContext(ctx context.Context) []Option { + var opt []Option + + bp := policy.BufferPolicyFromContext(ctx) + if bp.PerConnection >= 0 { + opt = append(opt, WithSizeLimit(bp.PerConnection)) + } else { + opt = append(opt, WithoutSizeLimit()) + } + + return opt +} + +// New creates a new Reader and Writer that connects to each other. +func New(opts ...Option) (*Reader, *Writer) { + p := &pipe{ + readSignal: signal.NewNotifier(), + writeSignal: signal.NewNotifier(), + done: done.New(), + errChan: make(chan error, 1), + option: pipeOption{ + limit: -1, + }, + } + + for _, opt := range opts { + opt(&(p.option)) + } + + return &Reader{ + pipe: p, + }, &Writer{ + pipe: p, + } +} diff --git a/subproject/Xray-core-main/transport/pipe/pipe_test.go b/subproject/Xray-core-main/transport/pipe/pipe_test.go new file mode 100644 index 00000000..a5cb25c5 --- /dev/null +++ b/subproject/Xray-core-main/transport/pipe/pipe_test.go @@ -0,0 +1,153 @@ +package pipe_test + +import ( + "errors" + "io" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "golang.org/x/sync/errgroup" + + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/buf" + . "github.com/xtls/xray-core/transport/pipe" +) + +func TestPipeReadWrite(t *testing.T) { + pReader, pWriter := New(WithSizeLimit(1024)) + + b := buf.New() + b.WriteString("abcd") + common.Must(pWriter.WriteMultiBuffer(buf.MultiBuffer{b})) + + b2 := buf.New() + b2.WriteString("efg") + common.Must(pWriter.WriteMultiBuffer(buf.MultiBuffer{b2})) + + rb, err := pReader.ReadMultiBuffer() + common.Must(err) + if r := cmp.Diff(rb.String(), "abcdefg"); r != "" { + t.Error(r) + } +} + +func TestPipeInterrupt(t *testing.T) { + pReader, pWriter := New(WithSizeLimit(1024)) + payload := []byte{'a', 'b', 'c', 'd'} + b := buf.New() + b.Write(payload) + common.Must(pWriter.WriteMultiBuffer(buf.MultiBuffer{b})) + pWriter.Interrupt() + + rb, err := pReader.ReadMultiBuffer() + if err != io.ErrClosedPipe { + t.Fatal("expect io.ErrClosePipe, but got ", err) + } + if !rb.IsEmpty() { + t.Fatal("expect empty buffer, but got ", rb.Len()) + } +} + +func TestPipeClose(t *testing.T) { + pReader, pWriter := New(WithSizeLimit(1024)) + payload := []byte{'a', 'b', 'c', 'd'} + b := buf.New() + common.Must2(b.Write(payload)) + common.Must(pWriter.WriteMultiBuffer(buf.MultiBuffer{b})) + common.Must(pWriter.Close()) + + rb, err := pReader.ReadMultiBuffer() + common.Must(err) + if rb.String() != string(payload) { + t.Fatal("expect content ", string(payload), " but actually ", rb.String()) + } + + rb, err = pReader.ReadMultiBuffer() + if err != io.EOF { + t.Fatal("expected EOF, but got ", err) + } + if !rb.IsEmpty() { + t.Fatal("expect empty buffer, but got ", rb.String()) + } +} + +func TestPipeLimitZero(t *testing.T) { + pReader, pWriter := New(WithSizeLimit(0)) + bb := buf.New() + common.Must2(bb.Write([]byte{'a', 'b'})) + common.Must(pWriter.WriteMultiBuffer(buf.MultiBuffer{bb})) + + var errg errgroup.Group + errg.Go(func() error { + b := buf.New() + b.Write([]byte{'c', 'd'}) + return pWriter.WriteMultiBuffer(buf.MultiBuffer{b}) + }) + errg.Go(func() error { + time.Sleep(time.Second) + + var container buf.MultiBufferContainer + if err := buf.Copy(pReader, &container); err != nil { + return err + } + + if r := cmp.Diff(container.String(), "abcd"); r != "" { + return errors.New(r) + } + return nil + }) + errg.Go(func() error { + time.Sleep(time.Second * 2) + return pWriter.Close() + }) + if err := errg.Wait(); err != nil { + t.Error(err) + } +} + +func TestPipeWriteMultiThread(t *testing.T) { + pReader, pWriter := New(WithSizeLimit(0)) + + var errg errgroup.Group + for i := 0; i < 10; i++ { + errg.Go(func() error { + b := buf.New() + b.WriteString("abcd") + return pWriter.WriteMultiBuffer(buf.MultiBuffer{b}) + }) + } + time.Sleep(time.Millisecond * 100) + pWriter.Close() + errg.Wait() + + b, err := pReader.ReadMultiBuffer() + common.Must(err) + if r := cmp.Diff(b[0].Bytes(), []byte{'a', 'b', 'c', 'd'}); r != "" { + t.Error(r) + } +} + +func TestInterfaces(t *testing.T) { + _ = (buf.Reader)(new(Reader)) + _ = (buf.TimeoutReader)(new(Reader)) + + _ = (common.Interruptible)(new(Reader)) + _ = (common.Interruptible)(new(Writer)) + _ = (common.Closable)(new(Writer)) +} + +func BenchmarkPipeReadWrite(b *testing.B) { + reader, writer := New(WithoutSizeLimit()) + a := buf.New() + a.Extend(buf.Size) + c := buf.MultiBuffer{a} + + b.ResetTimer() + for i := 0; i < b.N; i++ { + common.Must(writer.WriteMultiBuffer(c)) + d, err := reader.ReadMultiBuffer() + common.Must(err) + c = d + } +} diff --git a/subproject/Xray-core-main/transport/pipe/reader.go b/subproject/Xray-core-main/transport/pipe/reader.go new file mode 100644 index 00000000..79f0ac03 --- /dev/null +++ b/subproject/Xray-core-main/transport/pipe/reader.go @@ -0,0 +1,41 @@ +package pipe + +import ( + "time" + + "github.com/xtls/xray-core/common/buf" +) + +// Reader is a buf.Reader that reads content from a pipe. +type Reader struct { + pipe *pipe +} + +// ReadMultiBuffer implements buf.Reader. +func (r *Reader) ReadMultiBuffer() (buf.MultiBuffer, error) { + return r.pipe.ReadMultiBuffer() +} + +// ReadMultiBufferTimeout reads content from a pipe within the given duration, or returns buf.ErrTimeout otherwise. +func (r *Reader) ReadMultiBufferTimeout(d time.Duration) (buf.MultiBuffer, error) { + return r.pipe.ReadMultiBufferTimeout(d) +} + +// Interrupt implements common.Interruptible. +func (r *Reader) Interrupt() { + r.pipe.Interrupt() +} + +// ReturnAnError makes ReadMultiBuffer return an error, only once. +func (r *Reader) ReturnAnError(err error) { + r.pipe.errChan <- err +} + +// Recover catches an error set by ReturnAnError, if exists. +func (r *Reader) Recover() (err error) { + select { + case err = <-r.pipe.errChan: + default: + } + return +} diff --git a/subproject/Xray-core-main/transport/pipe/writer.go b/subproject/Xray-core-main/transport/pipe/writer.go new file mode 100644 index 00000000..4ba26ccc --- /dev/null +++ b/subproject/Xray-core-main/transport/pipe/writer.go @@ -0,0 +1,29 @@ +package pipe + +import ( + "github.com/xtls/xray-core/common/buf" +) + +// Writer is a buf.Writer that writes data into a pipe. +type Writer struct { + pipe *pipe +} + +// WriteMultiBuffer implements buf.Writer. +func (w *Writer) WriteMultiBuffer(mb buf.MultiBuffer) error { + return w.pipe.WriteMultiBuffer(mb) +} + +// Close implements io.Closer. After the pipe is closed, writing to the pipe will return io.ErrClosedPipe, while reading will return io.EOF. +func (w *Writer) Close() error { + return w.pipe.Close() +} + +func (w *Writer) Len() int32 { + return w.pipe.Len() +} + +// Interrupt implements common.Interruptible. +func (w *Writer) Interrupt() { + w.pipe.Interrupt() +} diff --git a/web/html/form/stream/stream_xhttp.html b/web/html/form/stream/stream_xhttp.html index 447612c9..b43def75 100644 --- a/web/html/form/stream/stream_xhttp.html +++ b/web/html/form/stream/stream_xhttp.html @@ -70,6 +70,8 @@ queryInHeader header + cookie + query @@ -127,7 +129,7 @@ Default (body) body header - query + cookie