├── .github ├── build │ └── friendly-filenames.json ├── dependabot.yml └── workflows │ ├── Publish Docker image.yml │ ├── codeql-analysis.yml │ ├── release.yml │ └── stale.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README-en.md ├── README-vi.md ├── README.md ├── README_Fa.md ├── api ├── api.go ├── apimodel.go ├── bunpanel │ ├── bunpanel.go │ ├── bunpanel_test.go │ └── model.go ├── gov2panel │ ├── gov2panel.go │ ├── gov2panel_test.go │ └── model.go ├── newV2board │ ├── model.go │ ├── v2board.go │ └── v2board_test.go ├── pmpanel │ ├── model.go │ ├── pmpanel.go │ └── pmpanel_test.go ├── proxypanel │ ├── model.go │ ├── proxypanel.go │ └── proypanel_test.go ├── sspanel │ ├── model.go │ ├── sspanel.go │ └── sspanel_test.go └── v2raysocks │ ├── model.go │ ├── v2raysocks.go │ └── v2raysocks_test.go ├── app ├── app.go └── mydispatcher │ ├── config.pb.go │ ├── config.proto │ ├── default.go │ ├── dispatcher.go │ ├── errors.generated.go │ ├── fakednssniffer.go │ ├── sniffer.go │ ├── stats.go │ └── stats_test.go ├── cmd ├── distro │ └── all │ │ └── all.go ├── root.go ├── version.go └── x25519.go ├── common ├── common.go ├── limiter │ ├── limiter.go │ ├── model.go │ └── rate.go ├── mylego │ ├── account.go │ ├── accounts_storage.go │ ├── certs_storage.go │ ├── lego_test.go │ ├── model.go │ ├── mylego.go │ ├── renew.go │ ├── renew_test.go │ ├── run.go │ └── setup.go ├── rule │ └── rule.go └── serverstatus │ └── serverstatus.go ├── go.mod ├── go.sum ├── main.go ├── panel ├── config.go ├── defaultConfig.go └── panel.go ├── release └── config │ ├── config.yml.example │ ├── custom_inbound.json │ ├── custom_outbound.json │ ├── dns.json │ ├── geoip.dat │ ├── geosite.dat │ ├── route.json │ └── rulelist └── service ├── controller ├── config.go ├── control.go ├── controller.go ├── controller_test.go ├── errors.generated.go ├── inboundbuilder.go ├── inboundbuilder_test.go ├── outboundbuilder.go └── userbuilder.go └── service.go /.github/build/friendly-filenames.json: -------------------------------------------------------------------------------- 1 | { 2 | "android-arm64": { 3 | "friendlyName": "android-arm64-v8a" 4 | }, 5 | "darwin-amd64": { 6 | "friendlyName": "macos-64" 7 | }, 8 | "darwin-arm64": { 9 | "friendlyName": "macos-arm64-v8a" 10 | }, 11 | "dragonfly-amd64": { 12 | "friendlyName": "dragonfly-64" 13 | }, 14 | "freebsd-386": { 15 | "friendlyName": "freebsd-32" 16 | }, 17 | "freebsd-amd64": { 18 | "friendlyName": "freebsd-64" 19 | }, 20 | "freebsd-arm64": { 21 | "friendlyName": "freebsd-arm64-v8a" 22 | }, 23 | "freebsd-arm7": { 24 | "friendlyName": "freebsd-arm32-v7a" 25 | }, 26 | "linux-386": { 27 | "friendlyName": "linux-32" 28 | }, 29 | "linux-amd64": { 30 | "friendlyName": "linux-64" 31 | }, 32 | "linux-arm5": { 33 | "friendlyName": "linux-arm32-v5" 34 | }, 35 | "linux-arm64": { 36 | "friendlyName": "linux-arm64-v8a" 37 | }, 38 | "linux-arm6": { 39 | "friendlyName": "linux-arm32-v6" 40 | }, 41 | "linux-arm7": { 42 | "friendlyName": "linux-arm32-v7a" 43 | }, 44 | "linux-mips64le": { 45 | "friendlyName": "linux-mips64le" 46 | }, 47 | "linux-mips64": { 48 | "friendlyName": "linux-mips64" 49 | }, 50 | "linux-mipslesoftfloat": { 51 | "friendlyName": "linux-mips32le-softfloat" 52 | }, 53 | "linux-mipsle": { 54 | "friendlyName": "linux-mips32le" 55 | }, 56 | "linux-mipssoftfloat": { 57 | "friendlyName": "linux-mips32-softfloat" 58 | }, 59 | "linux-mips": { 60 | "friendlyName": "linux-mips32" 61 | }, 62 | "linux-ppc64le": { 63 | "friendlyName": "linux-ppc64le" 64 | }, 65 | "linux-ppc64": { 66 | "friendlyName": "linux-ppc64" 67 | }, 68 | "linux-riscv64": { 69 | "friendlyName": "linux-riscv64" 70 | }, 71 | "linux-s390x": { 72 | "friendlyName": "linux-s390x" 73 | }, 74 | "openbsd-386": { 75 | "friendlyName": "openbsd-32" 76 | }, 77 | "openbsd-amd64": { 78 | "friendlyName": "openbsd-64" 79 | }, 80 | "openbsd-arm64": { 81 | "friendlyName": "openbsd-arm64-v8a" 82 | }, 83 | "openbsd-arm7": { 84 | "friendlyName": "openbsd-arm32-v7a" 85 | }, 86 | "windows-386": { 87 | "friendlyName": "windows-32" 88 | }, 89 | "windows-amd64": { 90 | "friendlyName": "windows-64" 91 | }, 92 | "windows-arm7": { 93 | "friendlyName": "windows-arm32-v7a" 94 | } 95 | } -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /.github/workflows/Publish Docker image.yml: -------------------------------------------------------------------------------- 1 | name: Publish Docker image 2 | on: 3 | workflow_dispatch: 4 | release: 5 | types: 6 | - published 7 | env: 8 | REGISTRY: ghcr.io 9 | IMAGE_NAME: ${{ github.repository_owner }}/xrayr 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | platform: 18 | - linux/amd64 19 | - linux/arm64 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | - name: Docker meta 24 | id: meta 25 | uses: docker/metadata-action@v5 26 | with: 27 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 28 | - name: Set up QEMU 29 | uses: docker/setup-qemu-action@v3 30 | - name: Set up Docker Buildx 31 | uses: docker/setup-buildx-action@v3 32 | - name: Login to GitHub Container Registry 33 | uses: docker/login-action@v3 34 | with: 35 | registry: ${{ env.REGISTRY }} 36 | username: ${{ github.actor }} 37 | password: ${{ secrets.GITHUB_TOKEN }} 38 | - name: Build and push by digest 39 | id: build 40 | uses: docker/build-push-action@v5 41 | with: 42 | context: . 43 | platforms: ${{ matrix.platform }} 44 | labels: ${{ steps.meta.outputs.labels }} 45 | outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true 46 | - name: Export digest 47 | run: | 48 | mkdir -p /tmp/digests 49 | digest="${{ steps.build.outputs.digest }}" 50 | touch "/tmp/digests/${digest#sha256:}" 51 | - name: Upload digest 52 | uses: actions/upload-artifact@v3 53 | with: 54 | name: digests 55 | path: /tmp/digests/* 56 | if-no-files-found: error 57 | retention-days: 1 58 | 59 | merge: 60 | runs-on: ubuntu-latest 61 | needs: 62 | - build 63 | steps: 64 | - name: Download digests 65 | uses: actions/download-artifact@v3 66 | with: 67 | name: digests 68 | path: /tmp/digests 69 | - name: Set up Docker Buildx 70 | uses: docker/setup-buildx-action@v3 71 | - name: Docker meta 72 | id: meta 73 | uses: docker/metadata-action@v5 74 | with: 75 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 76 | - name: Login to GitHub Container Registry 77 | uses: docker/login-action@v3 78 | with: 79 | registry: ${{ env.REGISTRY }} 80 | username: ${{ github.actor }} 81 | password: ${{ secrets.GITHUB_TOKEN }} 82 | - name: Create manifest list and push 83 | working-directory: /tmp/digests 84 | run: | 85 | ls -al 86 | echo docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ 87 | $(printf '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *) 88 | docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ 89 | $(printf '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *) 90 | - name: Inspect image 91 | run: | 92 | docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }} 93 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '43 22 * * 3' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'go' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v3 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v2 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | - name: Install Go 51 | uses: actions/setup-go@v4 52 | with: 53 | go-version-file: go.mod 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v2 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v2 72 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Build and Release 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - master 8 | paths: 9 | - "**/*.go" 10 | - "go.mod" 11 | - "go.sum" 12 | - ".github/workflows/*.yml" 13 | pull_request: 14 | types: [ opened, synchronize, reopened ] 15 | paths: 16 | - "**/*.go" 17 | - "go.mod" 18 | - "go.sum" 19 | - ".github/workflows/*.yml" 20 | release: 21 | types: [ published ] 22 | 23 | jobs: 24 | 25 | build: 26 | strategy: 27 | matrix: 28 | # Include amd64 on all platforms. 29 | goos: [ windows, freebsd, openbsd, linux, darwin ] 30 | goarch: [ amd64, 386 ] 31 | exclude: 32 | # Exclude i386 on darwin. 33 | - goarch: 386 34 | goos: darwin 35 | - goarch: 386 36 | goos: openbsd 37 | include: 38 | # BEIGIN MacOS ARM64 39 | - goos: darwin 40 | goarch: arm64 41 | # END MacOS ARM64 42 | # BEGIN Linux ARM 5 6 7 43 | - goos: linux 44 | goarch: arm 45 | goarm: 7 46 | - goos: linux 47 | goarch: arm 48 | goarm: 6 49 | - goos: linux 50 | goarch: arm 51 | goarm: 5 52 | # END Linux ARM 5 6 7 53 | # BEGIN Android ARM 8 54 | - goos: android 55 | goarch: arm64 56 | # END Android ARM 8 57 | # BEGIN Other architectures 58 | # BEGIN riscv64 & ARM64 59 | - goos: linux 60 | goarch: arm64 61 | - goos: linux 62 | goarch: riscv64 63 | # END riscv64 & ARM64 64 | # BEGIN MIPS 65 | - goos: linux 66 | goarch: mips64 67 | - goos: linux 68 | goarch: mips64le 69 | - goos: linux 70 | goarch: mipsle 71 | - goos: linux 72 | goarch: mips 73 | # END MIPS 74 | # BEGIN PPC 75 | # - goos: linux # Removed due to the unsupport of shirou/gopsutil 76 | # goarch: ppc64 77 | - goos: linux 78 | goarch: ppc64le 79 | # END PPC 80 | # BEGIN FreeBSD ARM 81 | - goos: freebsd 82 | goarch: arm64 83 | - goos: freebsd 84 | goarch: arm 85 | goarm: 7 86 | # END FreeBSD ARM 87 | # BEGIN S390X 88 | - goos: linux 89 | goarch: s390x 90 | # END S390X 91 | # END Other architectures 92 | fail-fast: false 93 | 94 | runs-on: ubuntu-latest 95 | env: 96 | GOOS: ${{ matrix.goos }} 97 | GOARCH: ${{ matrix.goarch }} 98 | GOARM: ${{ matrix.goarm }} 99 | CGO_ENABLED: 0 100 | steps: 101 | - name: Checkout codebase 102 | uses: actions/checkout@v3 103 | - name: Show workflow information 104 | id: get_filename 105 | run: | 106 | export _NAME=$(jq ".[\"$GOOS-$GOARCH$GOARM$GOMIPS\"].friendlyName" -r < .github/build/friendly-filenames.json) 107 | echo "GOOS: $GOOS, GOARCH: $GOARCH, GOARM: $GOARM, GOMIPS: $GOMIPS, RELEASE_NAME: $_NAME" 108 | echo "ASSET_NAME=$_NAME" >> $GITHUB_OUTPUT 109 | echo "ASSET_NAME=$_NAME" >> $GITHUB_ENV 110 | 111 | - name: Set up Go 112 | uses: actions/setup-go@v3 113 | with: 114 | go-version: '^1.21' 115 | 116 | - name: Get project dependencies 117 | run: go mod download 118 | 119 | 120 | - name: Build XrayR 121 | run: | 122 | mkdir -p build_assets 123 | go build -v -o build_assets/XrayR -trimpath -ldflags "-s -w -buildid=" 124 | 125 | - name: Build Mips softfloat XrayR 126 | if: matrix.goarch == 'mips' || matrix.goarch == 'mipsle' 127 | run: | 128 | GOMIPS=softfloat go build -v -o build_assets/XrayR_softfloat -trimpath -ldflags "-s -w -buildid=" 129 | - name: Rename Windows XrayR 130 | if: matrix.goos == 'windows' 131 | run: | 132 | cd ./build_assets || exit 1 133 | mv XrayR XrayR.exe 134 | 135 | - name: Prepare to release 136 | uses: nick-fields/retry@v3 137 | with: 138 | timeout_minutes: 60 139 | retry_wait_seconds: 60 140 | max_attempts: 5 141 | command: | 142 | cp ${GITHUB_WORKSPACE}/README.md ./build_assets/README.md 143 | cp ${GITHUB_WORKSPACE}/LICENSE ./build_assets/LICENSE 144 | cp ${GITHUB_WORKSPACE}/release/config/dns.json ./build_assets/dns.json 145 | cp ${GITHUB_WORKSPACE}/release/config/route.json ./build_assets/route.json 146 | cp ${GITHUB_WORKSPACE}/release/config/custom_outbound.json ./build_assets/custom_outbound.json 147 | cp ${GITHUB_WORKSPACE}/release/config/custom_inbound.json ./build_assets/custom_inbound.json 148 | cp ${GITHUB_WORKSPACE}/release/config/rulelist ./build_assets/rulelist 149 | cp ${GITHUB_WORKSPACE}/release/config/config.yml.example ./build_assets/config.yml 150 | LIST=('geoip' 'geosite') 151 | for i in "${LIST[@]}" 152 | do 153 | DOWNLOAD_URL="https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/${i}.dat" 154 | FILE_NAME="${i}.dat" 155 | echo -e "Downloading ${DOWNLOAD_URL}..." 156 | curl -L "${DOWNLOAD_URL}" -o ./build_assets/${FILE_NAME} 157 | done 158 | - name: Create ZIP archive 159 | shell: bash 160 | run: | 161 | pushd build_assets || exit 1 162 | touch -mt $(date +%Y01010000) * 163 | zip -9vr ../XrayR-$ASSET_NAME.zip . 164 | popd || exit 1 165 | FILE=./XrayR-$ASSET_NAME.zip 166 | DGST=$FILE.dgst 167 | for METHOD in {"md5","sha1","sha256","sha512"} 168 | do 169 | openssl dgst -$METHOD $FILE | sed 's/([^)]*)//g' >>$DGST 170 | done 171 | - name: Change the name 172 | run: | 173 | mv build_assets XrayR-$ASSET_NAME 174 | - name: Upload files to Artifacts 175 | uses: actions/upload-artifact@v3 176 | with: 177 | name: XrayR-${{ steps.get_filename.outputs.ASSET_NAME }} 178 | path: | 179 | ./XrayR-${{ steps.get_filename.outputs.ASSET_NAME }}/* 180 | - name: Upload binaries to release 181 | uses: svenstaro/upload-release-action@v2 182 | if: github.event_name == 'release' 183 | with: 184 | repo_token: ${{ secrets.GITHUB_TOKEN }} 185 | file: ./XrayR-${{ steps.get_filename.outputs.ASSET_NAME }}.zip* 186 | tag: ${{ github.ref }} 187 | file_glob: true 188 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Close inactive issues 2 | on: 3 | schedule: 4 | - cron: "30 1 * * *" 5 | 6 | jobs: 7 | close-issues: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | issues: write 11 | pull-requests: write 12 | steps: 13 | - uses: actions/stale@v5 14 | with: 15 | days-before-issue-stale: 30 16 | days-before-issue-close: 14 17 | stale-issue-label: "stale" 18 | stale-issue-message: "This issue is stale because it has been open for 30 days with no activity." 19 | close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale." 20 | days-before-pr-stale: -1 21 | days-before-pr-close: -1 22 | repo-token: ${{ secrets.GITHUB_TOKEN }} 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | out 4 | gen 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | *.test 11 | *.out 12 | go.work 13 | main 14 | XrayR 15 | XrayR* 16 | access.log 17 | error.log 18 | .lego 19 | cert 20 | config.yml 21 | updateR.sh 22 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build go 2 | FROM golang:1.23.1-alpine AS builder 3 | WORKDIR /app 4 | COPY . . 5 | ENV CGO_ENABLED=0 6 | RUN go mod download \ 7 | && go build -v -o XrayR -trimpath -ldflags "-s -w -buildid=" 8 | 9 | # Release 10 | FROM alpine:latest 11 | # 安装必要的工具包 12 | RUN apk --update --no-cache add curl tzdata ca-certificates \ 13 | && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ 14 | && mkdir /etc/XrayR/ \ 15 | && curl -L "https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/geoip.dat" -o /etc/XrayR/geoip.dat \ 16 | && curl -L "https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/geosite.dat" -o /etc/XrayR/geosite.dat 17 | 18 | COPY --from=builder /app/XrayR /usr/local/bin 19 | 20 | ENTRYPOINT [ "XrayR", "--config", "/etc/XrayR/config.yml"] 21 | -------------------------------------------------------------------------------- /README-en.md: -------------------------------------------------------------------------------- 1 | # XrayR 2 | 3 | [![](https://img.shields.io/badge/TgChat-@XrayR讨论-blue.svg)](https://t.me/XrayR_project) 4 | [![](https://img.shields.io/badge/Channel-@XrayR通知-blue.svg)](https://t.me/XrayR_channel) 5 | ![](https://img.shields.io/github/stars/wyx2685/XrayR) 6 | ![](https://img.shields.io/github/forks/wyx2685/XrayR) 7 | ![](https://github.com/wyx2685/XrayR/actions/workflows/release.yml/badge.svg) 8 | ![](https://github.com/wyx2685/XrayR/actions/workflows/docker.yml/badge.svg) 9 | [![Github All Releases](https://img.shields.io/github/downloads/wyx2685/XrayR/total.svg)]() 10 | 11 | [Iranian(farsi) README](https://github.com/wyx2685/XrayR/blob/master/README_Fa.md), [Vietnamese(vi) README](https://github.com/wyx2685/XrayR/blob/master/README-vi.md), [English(en) README](https://github.com/wyx2685/XrayR/blob/master/README-en.md) 12 | 13 | A Xray backend framework that can easily support many panels. 14 | 15 | A back -end framework based on XRAY supports V2ay, Trojan, Shadowsocks protocols, which are easy to expand and support multi -panel docker. 16 | 17 | 18 | If you like this project, you can click STAR+WATCH in the upper right corner to continue to pay attention to the progress of this project. 19 | 20 | ## Guide for use 21 | 22 | Tutorial:[Detailed tutorial](https://xrayr-project.github.io/XrayR-doc/) 23 | 24 | 25 | ## Disclaimer 26 | 27 | This project is just my personal learning and development and maintenance. I do not guarantee any availability and is not responsible for any consequences caused by the use of this software. 28 | 29 | ## Features 30 | 31 | * Permanent open source and free. 32 | * Support V2Ray, Trojan, Shadowsocks multiple protocols. 33 | * Support new features such as Vless and XTLS. 34 | * Support single instance docking multi -panel and multi -node, no need to start repeatedly. 35 | * Support restriction online IP 36 | * Support node port level and user level speed limit. 37 | * The configuration is simple and clear. 38 | * Modify the automatic restart instance. 39 | * Easy to compile and upgrade, you can quickly update the core version and support the new features of XRAY-CORE. 40 | 41 | ## Function 42 | 43 | | Function | v2ray | trojan | shadowsocks | 44 | |-----------|-------|--------|-------------| 45 | | Get node information | √ | √ | √ | 46 | | Get user information | √ | √ | √ | 47 | | User traffic statistics | √ | √ | √ | 48 | | Server information report | √ | √ | √ | 49 | | Automatically apply for a TLS certificate | √ | √ | √ | 50 | | Automatic renewal TLS certificate | √ | √ | √ | 51 | | Number of online people | √ | √ | √ | 52 | | Online user restrictions | √ | √ | √ | 53 | | Audit rules | √ | √ | √ | 54 | | Node port speed limit | √ | √ | √ | 55 | | According to user speed limit | √ | √ | √ | 56 | | Custom DNS | √ | √ | √ | 57 | 58 | ## Support for panels 59 | 60 | | Panel | v2ray | trojan | shadowsocks | 61 | |--------------------------------------------------------|-------|--------|-------------------------| 62 | | sspanel-uim | √ | √ | √ (Single-ended multi-user and V2Ray-Plugin) | 63 | | v2board | √ | √ | √ | 64 | | [PMPanel](https://github.com/ByteInternetHK/PMPanel) | √ | √ | √ | 65 | | [ProxyPanel](https://github.com/ProxyPanel/ProxyPanel) | √ | √ | √ | 66 | | [WHMCS (V2RaySocks)](https://v2raysocks.doxtex.com/) | √ | √ | √ | 67 | | [BunPanel](https://github.com/pennyMorant/bunpanel-release) | √ | √ | √ | 68 | 69 | ## Software Installation 70 | 71 | ### 1-Click installation 72 | 73 | ``` 74 | wget -N https://raw.githubusercontent.com/wyx2685/XrayR-release/master/install.sh && bash install.sh 75 | ``` 76 | 77 | 78 | ### Manual installation 79 | 80 | [Manual installation tutorial](https://xrayr-project.github.io/XrayR-doc/xrayr-xia-zai-he-an-zhuang/install/manual) 81 | 82 | ## Configuration file and detailed use tutorial 83 | 84 | [Detailed tutorial](https://xrayr-project.github.io/XrayR-doc/) 85 | 86 | ## Thanks 87 | 88 | * [Project X](https://github.com/XTLS/) 89 | * [V2Fly](https://github.com/v2fly) 90 | * [VNet-V2ray](https://github.com/ProxyPanel/VNet-V2ray) 91 | * [Air-Universe](https://github.com/crossfw/Air-Universe) 92 | 93 | ## Licence 94 | 95 | [Mozilla Public License Version 2.0](https://github.com/wyx2685/XrayR/blob/master/LICENSE) 96 | 97 | ## Telgram 98 | 99 | [Xrayr back-end discussion](https://t.me/XrayR_project) 100 | 101 | [Xrayr notification](https://t.me/XrayR_channel) 102 | 103 | ## Stargazers over time 104 | 105 | [![Stargazers over time](https://starchart.cc/wyx2685/XrayR.svg)](https://starchart.cc/wyx2685/XrayR) 106 | 107 | 108 | -------------------------------------------------------------------------------- /README-vi.md: -------------------------------------------------------------------------------- 1 | # XrayR 2 | 3 | [![](https://img.shields.io/badge/TgChat-@XrayR讨论-blue.svg)](https://t.me/XrayR_project) 4 | [![](https://img.shields.io/badge/Channel-@XrayR通知-blue.svg)](https://t.me/XrayR_channel) 5 | ![](https://img.shields.io/github/stars/wyx2685/XrayR) 6 | ![](https://img.shields.io/github/forks/wyx2685/XrayR) 7 | ![](https://github.com/wyx2685/XrayR/actions/workflows/release.yml/badge.svg) 8 | ![](https://github.com/wyx2685/XrayR/actions/workflows/docker.yml/badge.svg) 9 | [![Github All Releases](https://img.shields.io/github/downloads/wyx2685/XrayR/total.svg)]() 10 | 11 | [Iranian(farsi) README](https://github.com/wyx2685/XrayR/blob/master/README_Fa.md), [Vietnamese(vi) README](https://github.com/wyx2685/XrayR/blob/master/README-vi.md), [English(en) README](https://github.com/wyx2685/XrayR/blob/master/README-en.md) 12 | 13 | A Xray backend framework that can easily support many panels. 14 | 15 | Khung trở lại dựa trên XRay hỗ trợ các giao thức V2ay, Trojan, Shadowsocks, dễ dàng mở rộng và hỗ trợ kết nối nhiều người. 16 | 17 | Nếu bạn thích dự án này, bạn có thể nhấp vào Star+Watch ở góc trên bên phải để tiếp tục chú ý đến tiến trình của dự án này. 18 | 19 | ## Tài liệu 20 | Sử dụng hướng dẫn: [Hướng dẫn chi tiết](https://xrayr-project.github.io/XrayR-doc/) ( Tiếng Trung ) 21 | 22 | ## Tuyên bố miễn trừ 23 | 24 | Dự án này chỉ là học tập và phát triển và bảo trì cá nhân của tôi. Tôi không đảm bảo bất kỳ sự sẵn có nào và không chịu trách nhiệm cho bất kỳ hậu quả nào do việc sử dụng phần mềm này. 25 | 26 | ## Đặt điểm nổi bật 27 | 28 | * Nguồn mở vĩnh viễn và miễn phí. 29 | * Hỗ trợ V2Ray, Trojan, Shadowsocks nhiều giao thức. 30 | * Hỗ trợ các tính năng mới như Vless và XTL. 31 | * Hỗ trợ trường hợp đơn lẻ kết nối Multi -Panel và Multi -Node, không cần phải bắt đầu nhiều lần. 32 | * Hỗ trợ hạn chế IP trực tuyến 33 | * Hỗ trợ cấp cổng nút và giới hạn tốc độ cấp người dùng. 34 | * Cấu hình đơn giản và rõ ràng. 35 | * Sửa đổi phiên bản khởi động lại tự động. 36 | * Dễ dàng biên dịch và nâng cấp, bạn có thể nhanh chóng cập nhật phiên bản cốt lõi và hỗ trợ các tính năng mới của Xray-Core. 37 | 38 | ## Chức năng 39 | 40 | | Chức năng | v2ray | trojan | shadowsocks | 41 | |-----------|-------|--------|-------------| 42 | | Nhận thông tin Node | √ | √ | √ | 43 | | Nhận thông tin người dùng | √ | √ | √ | 44 | | Thống kê lưu lượng người dùng | √ | √ | √ | 45 | | Báo cáo thông tin máy chủ | √ | √ | √ | 46 | | Tự động đăng ký chứng chỉ TLS | √ | √ | √ | 47 | | Chứng chỉ TLS gia hạn tự động | √ | √ | √ | 48 | | Số người trực tuyến | √ | √ | √ | 49 | | Hạn chế người dùng trực tuyến | √ | √ | √ | 50 | | Quy tắc kiểm toán | √ | √ | √ | 51 | | Giới hạn tốc độ cổng nút | √ | √ | √ | 52 | | Theo giới hạn tốc độ người dùng | √ | √ | √ | 53 | | DNS tùy chỉnh | √ | √ | √ | 54 | 55 | ## Hỗ trợ Panel 56 | 57 | | Panel | v2ray | trojan | shadowsocks | 58 | |--------------------------------------------------------|-------|--------|-------------------------| 59 | | sspanel-uim | √ | √ | √ (Nhiều người dùng cuối và v2ray-plugin) | 60 | | v2board | √ | √ | √ | 61 | | [PMPanel](https://github.com/ByteInternetHK/PMPanel) | √ | √ | √ | 62 | | [ProxyPanel](https://github.com/ProxyPanel/ProxyPanel) | √ | √ | √ | 63 | | [WHMCS (V2RaySocks)](https://v2raysocks.doxtex.com/) | √ | √ | √ | 64 | | [BunPanel](https://github.com/pennyMorant/bunpanel-release) | √ | √ | √ | 65 | 66 | ## Cài đặt phần mềm 67 | 68 | ### Một cài đặt chính 69 | 70 | ``` 71 | wget -N https://raw.githubusercontent.com/wyx2685/XrayR-release/master/install.sh && bash install.sh 72 | ``` 73 | 74 | 75 | ### Hướng dẫn cài đặt 76 | 77 | [Hướng dẫn cài đặt thủ công](https://xrayr-project.github.io/XrayR-doc/xrayr-xia-zai-he-an-zhuang/install/manual) 78 | 79 | ## Tệp cấu hình và hướng dẫn sử dụng chi tiết 80 | 81 | [Hướng dẫn chi tiết](https://xrayr-project.github.io/XrayR-doc/) 82 | 83 | ## Thanks 84 | 85 | * [Project X](https://github.com/XTLS/) 86 | * [V2Fly](https://github.com/v2fly) 87 | * [VNet-V2ray](https://github.com/ProxyPanel/VNet-V2ray) 88 | * [Air-Universe](https://github.com/crossfw/Air-Universe) 89 | 90 | ## Licence 91 | 92 | [Mozilla Public License Version 2.0](https://github.com/wyx2685/XrayR/blob/master/LICENSE) 93 | 94 | ## Telgram 95 | 96 | [Xrayr Back-end Thảo luận](https://t.me/XrayR_project) 97 | 98 | [Thông báo Xrayr](https://t.me/XrayR_channel) 99 | 100 | ## Stargazers over time 101 | 102 | [![Stargazers over time](https://starchart.cc/wyx2685/XrayR.svg)](https://starchart.cc/wyx2685/XrayR) 103 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 本分支需要配合修改过的V2board面板进行使用 2 | https://github.com/wyx2685/v2board 3 | 4 | # XrayR 5 | [![](https://img.shields.io/badge/TgChat-@UnOfficialV2board讨论-blue.svg)](https://t.me/unofficialV2board) 6 | [![](https://img.shields.io/badge/TgChat-@XrayR讨论-blue.svg)](https://t.me/XrayR_project) 7 | [![](https://img.shields.io/badge/Channel-@XrayR通知-blue.svg)](https://t.me/XrayR_channel) 8 | ![](https://img.shields.io/github/stars/wyx2685/XrayR) 9 | ![](https://img.shields.io/github/forks/wyx2685/XrayR) 10 | ![](https://github.com/wyx2685/XrayR/actions/workflows/release.yml/badge.svg) 11 | ![](https://github.com/wyx2685/XrayR/actions/workflows/docker.yml/badge.svg) 12 | [![Github All Releases](https://img.shields.io/github/downloads/wyx2685/XrayR/total.svg)]() 13 | 14 | 15 | [English](https://github.com/wyx2685/XrayR/blob/master/README-en.md)|[Iranian](https://github.com/wyx2685/XrayR/blob/master/README_Fa.md)|[Vietnamese](https://github.com/wyx2685/XrayR/blob/master/README-vi.md) 16 | 17 | A Xray backend framework that can easily support many panels. 18 | 19 | 一个基于Xray的后端框架,支持V2ay,Trojan,Shadowsocks协议,极易扩展,支持多面板对接。 20 | 21 | 如果您喜欢本项目,可以右上角点个star+watch,持续关注本项目的进展。 22 | 23 | 使用教程:[详细使用教程](https://xrayr-project.github.io/XrayR-doc/) 24 | 25 | 26 | ## 免责声明 27 | 28 | 本项目只是本人个人学习开发并维护,本人不保证任何可用性,也不对使用本软件造成的任何后果负责。 29 | 30 | ## 特点 31 | 32 | * 永久开源且免费。 33 | * 支持V2ray,Trojan, Shadowsocks多种协议。 34 | * 支持Vless和XTLS等新特性。 35 | * 支持单实例对接多面板、多节点,无需重复启动。 36 | * 支持限制在线IP 37 | * 支持节点端口级别、用户级别限速。 38 | * 配置简单明了。 39 | * 修改配置自动重启实例。 40 | * 方便编译和升级,可以快速更新核心版本, 支持Xray-core新特性。 41 | 42 | ## 功能介绍 43 | 44 | | 功能 | vmess | vless | trojan | shadowsocks | 45 | |-----------|-------|-------|--------|-------------| 46 | | 获取节点信息| √ | √ | √ |√ | 47 | | 获取用户信息| √ | √ | √ |√ | 48 | | 用户流量统计| √ | √ | √ |√ | 49 | | 服务器信息上报| √ | √ | √ |√ | 50 | | 自动申请tls证书| √ | √ | √ |√ | 51 | | 自动续签tls证书| √ | √ | √ |√ | 52 | | 在线人数统计| √ | √ | √ |√ | 53 | | 在线用户限制| √ | √ | √ |√ | 54 | | 审计规则| √ | √ | √ |√ | 55 | | 节点端口限速| √ | √ | √ |√ | 56 | | 按照用户限速| √ | √ | √ |√ | 57 | | 自定义DNS| √ | √ | √ |√ | 58 | 59 | ## 支持前端 60 | 61 | 62 | | 前端 | vmess | vless | trojan | shadowsocks | 63 | |--------------------------------------------------------|-------|-------|--------|-------------------------| 64 | | sspanel-uim | √ | √ | √ | √ (单端口多用户和V2ray-Plugin) | 65 | | v2board | √ | √ | √ | √ | 66 | | [PMPanel](https://github.com/ByteInternetHK/PMPanel) | √ | √ | √ | √ | 67 | | [ProxyPanel](https://github.com/ProxyPanel/ProxyPanel) | √ | √ | √ | √ | 68 | | [WHMCS (V2RaySocks)](https://v2raysocks.doxtex.com/) | √ | √ | √ | √ | 69 | | [GoV2Panel](https://github.com/pingProMax/gov2panel) | √ | √ | √ | √ | 70 | | [BunPanel](https://github.com/pennyMorant/bunpanel-release) | √ | √ | √ | √ | 71 | 72 | ## 软件安装 73 | 74 | ### 一键安装 75 | 76 | ``` 77 | wget -N https://raw.githubusercontent.com/wyx2685/XrayR-release/master/install.sh && bash install.sh 78 | ``` 79 | 80 | ### 手动安装 81 | 82 | [手动安装教程](https://xrayr-project.github.io/XrayR-doc/xrayr-xia-zai-he-an-zhuang/install/manual) 83 | 84 | ## 配置文件及详细使用教程 85 | 86 | [详细使用教程](https://xrayr-project.github.io/XrayR-doc/) 87 | 88 | ## Thanks 89 | 90 | * [Project X](https://github.com/XTLS/) 91 | * [V2Fly](https://github.com/v2fly) 92 | * [VNet-V2ray](https://github.com/ProxyPanel/VNet-V2ray) 93 | * [Air-Universe](https://github.com/crossfw/Air-Universe) 94 | 95 | ## Licence 96 | 97 | [Mozilla Public License Version 2.0](https://github.com/wyx2685/XrayR/blob/master/LICENSE) 98 | 99 | ## Telgram 100 | 101 | [XrayR后端讨论](https://t.me/XrayR_project) 102 | 103 | [XrayR通知](https://t.me/XrayR_channel) 104 | 105 | ## Stargazers over time 106 | 107 | [![Stargazers over time](https://starchart.cc/wyx2685/XrayR.svg)](https://starchart.cc/wyx2685/XrayR) 108 | 109 | 110 | -------------------------------------------------------------------------------- /README_Fa.md: -------------------------------------------------------------------------------- 1 | # XrayR 2 | 3 | [![](https://img.shields.io/badge/TgChat-@XrayR讨论-blue.svg)](https://t.me/XrayR_project) 4 | [![](https://img.shields.io/badge/Channel-@XrayR通知-blue.svg)](https://t.me/XrayR_channel) 5 | ![](https://img.shields.io/github/stars/wyx2685/XrayR) 6 | ![](https://img.shields.io/github/forks/wyx2685/XrayR) 7 | ![](https://github.com/wyx2685/XrayR/actions/workflows/release.yml/badge.svg) 8 | ![](https://github.com/wyx2685/XrayR/actions/workflows/docker.yml/badge.svg) 9 | [![Github All Releases](https://img.shields.io/github/downloads/wyx2685/XrayR/total.svg)]() 10 | 11 | [Iranian(farsi) README](https://github.com/wyx2685/XrayR/blob/master/README_Fa.md), [Vietnamese(vi) README](https://github.com/wyx2685/XrayR/blob/master/README-vi.md), [English(en) README](https://github.com/wyx2685/XrayR/blob/master/README-en.md) 12 | 13 | یک فریمورک بک اند مبتنی بر xray که از چند از پنل پشتیبانی می کند 14 | 15 | یک چارچوب بک‌اند مبتنی بر Xray که از پروتکل‌های V2ay، Trojan و Shadowsocks پشتیبانی می‌کند، به راحتی قابل گسترش است و از اتصال چند پنل پشتیبانی می‌کند. 16 | 17 | اگر این پروژه را دوست دارید، می توانید با کلیک بر روی ستاره+ساعت در گوشه بالا سمت راست به ادامه روند پیشرفت این پروژه توجه کنید. 18 | 19 | آموزش:[اموزش با جزئیات](https://xrayr-project.github.io/XrayR-doc/) 20 | 21 | ## سلب مسئولیت 22 | 23 | این پروژه فقط مطالعه، توسعه و نگهداری شخصی من است. من هیچ گونه قابلیت استفاده را تضمین نمی کنم و مسئولیتی در قبال عواقب ناشی از استفاده از این نرم افزار ندارم. 24 | ## امکانات 25 | 26 | * منبع باز دائمی و رایگان 27 | * پشتیبانی از چندین پروتکل V2ray، Trojan، Shadowsocks. 28 | * پشتیبانی از ویژگی های جدید مانند Vless و XTLS. 29 | * پشتیبانی از اتصال یک نمونه چند پانل، چند گره، بدون نیاز به شروع مکرر. 30 | * پشتیبانی محدود IP آنلاین 31 | * پشتیبانی از سطح پورت گره، محدودیت سرعت سطح کاربر. 32 | * پیکربندی ساده و سرراست است. 33 | * پیکربندی را تغییر دهید تا نمونه به طور خودکار راه اندازی مجدد شود. 34 | * کامپایل و ارتقاء آن آسان است و می تواند به سرعت نسخه اصلی را به روز کند و از ویژگی های جدید Xray-core پشتیبانی می کند. 35 | 36 | ## امکانات 37 | 38 | | امکانات | v2ray | trojan | shadowsocks | 39 | |-----------|-------|--------|-------------| 40 | | اطلاعات گره را دریافت کنید | √ | √ | √ | 41 | | دریافت اطلاعات کاربر | √ | √ | √ | 42 | | آمار ترافیک کاربران | √ | √ | √ | 43 | | گزارش اطلاعات سرور | √ | √ | √ | 44 | | به طور خودکار برای گواهی tls درخواست دهید | √ | √ | √ | 45 | | تمدید خودکار گواهی tls | √ | √ | √ | 46 | | آمار آنلاین | √ | √ | √ | 47 | | محدودیت کاربر آنلاین | √ | √ | √ | 48 | | قوانین حسابرسی | √ | √ | √ | 49 | | محدودیت سرعت پورت گره | √ | √ | √ | 50 | | محدودیت سرعت بر اساس کاربر | √ | √ | √ | 51 | | DNS سفارشی | √ | √ | √ | 52 | 53 | ## پشتیبانی از قسمت فرانت 54 | 55 | | قسمت فرانت | v2ray | trojan | shadowsocks | 56 | |--------------------------------------------------------|-------|--------|-------------------------| 57 | | sspanel-uim | √ | √ | √ (تک پورت چند کاربره و V2ray-Plugin) | 58 | | v2board | √ | √ | √ | 59 | | [PMPanel](https://github.com/ByteInternetHK/PMPanel) | √ | √ | √ | 60 | | [ProxyPanel](https://github.com/ProxyPanel/ProxyPanel) | √ | √ | √ | 61 | | [WHMCS (V2RaySocks)](https://v2raysocks.doxtex.com/) | √ | √ | √ | 62 | | [BunPanel](https://github.com/pennyMorant/bunpanel-release) | √ | √ | √ | 63 | 64 | ## نصب نرم افزار 65 | 66 | ### نصب بصورت یکپارچه 67 | 68 | ``` 69 | wget -N https://raw.githubusercontent.com/wyx2685/XrayR-release/master/install.sh && bash install.sh 70 | ``` 71 | 72 | 73 | ### نصب دستی 74 | 75 | [آموزش نصب دستی](https://xrayr-project.github.io/XrayR-doc/xrayr-xia-zai-he-an-zhuang/install/manual) 76 | 77 | ## فایل های پیکربندی و آموزش های با جرئیات 78 | 79 | [آموزش مفصل](https://xrayr-project.github.io/XrayR-doc/) 80 | 81 | ## Thanks 82 | 83 | * [Project X](https://github.com/XTLS/) 84 | * [V2Fly](https://github.com/v2fly) 85 | * [VNet-V2ray](https://github.com/ProxyPanel/VNet-V2ray) 86 | * [Air-Universe](https://github.com/crossfw/Air-Universe) 87 | 88 | ## Licence 89 | 90 | [Mozilla Public License Version 2.0](https://github.com/wyx2685/XrayR/blob/master/LICENSE) 91 | 92 | ## Telgram 93 | 94 | [بحث در مورد XrayR Backend](https://t.me/XrayR_project) 95 | 96 | [کانال اعلان در مورد XrayR](https://t.me/XrayR_channel) 97 | 98 | ## Stargazers over time 99 | 100 | [![Stargazers over time](https://starchart.cc/wyx2685/XrayR.svg)](https://starchart.cc/wyx2685/XrayR) 101 | 102 | 103 | -------------------------------------------------------------------------------- /api/api.go: -------------------------------------------------------------------------------- 1 | // Package api contains all the api used by XrayR 2 | // To implement an api , one needs to implement the interface below. 3 | 4 | package api 5 | 6 | // API is the interface for different panel's api. 7 | type API interface { 8 | GetNodeInfo() (nodeInfo *NodeInfo, err error) 9 | GetUserList() (userList *[]UserInfo, err error) 10 | ReportNodeStatus(nodeStatus *NodeStatus) (err error) 11 | ReportNodeOnlineUsers(onlineUser *[]OnlineUser) (err error) 12 | ReportUserTraffic(userTraffic *[]UserTraffic) (err error) 13 | Describe() ClientInfo 14 | GetNodeRule() (ruleList *[]DetectRule, err error) 15 | ReportIllegal(detectResultList *[]DetectResult) (err error) 16 | Debug() 17 | } 18 | -------------------------------------------------------------------------------- /api/apimodel.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/json" 5 | "regexp" 6 | 7 | "github.com/xtls/xray-core/infra/conf" 8 | ) 9 | 10 | const ( 11 | UserNotModified = "users not modified" 12 | NodeNotModified = "node not modified" 13 | RuleNotModified = "rules not modified" 14 | ) 15 | 16 | // Config API config 17 | type Config struct { 18 | APIHost string `mapstructure:"ApiHost"` 19 | NodeID int `mapstructure:"NodeID"` 20 | Key string `mapstructure:"ApiKey"` 21 | NodeType string `mapstructure:"NodeType"` 22 | EnableVless bool `mapstructure:"EnableVless"` 23 | VlessFlow string `mapstructure:"VlessFlow"` 24 | Timeout int `mapstructure:"Timeout"` 25 | SpeedLimit float64 `mapstructure:"SpeedLimit"` 26 | DeviceLimit int `mapstructure:"DeviceLimit"` 27 | RuleListPath string `mapstructure:"RuleListPath"` 28 | DisableCustomConfig bool `mapstructure:"DisableCustomConfig"` 29 | } 30 | 31 | // NodeStatus Node status 32 | type NodeStatus struct { 33 | CPU float64 34 | Mem float64 35 | Disk float64 36 | Uptime uint64 37 | } 38 | 39 | type NodeInfo struct { 40 | AcceptProxyProtocol bool 41 | Authority string 42 | NodeType string // Must be V2ray, Trojan, and Shadowsocks 43 | NodeID int 44 | Port uint32 45 | SpeedLimit uint64 // Bps 46 | AlterID uint16 47 | TransportProtocol string 48 | FakeType string 49 | Host string 50 | Path string 51 | EnableTLS bool 52 | EnableSniffing bool 53 | RouteOnly bool 54 | EnableVless bool 55 | VlessFlow string 56 | CypherMethod string 57 | ServerKey string 58 | ServiceName string 59 | Method string 60 | Header json.RawMessage 61 | HttpHeaders map[string]*conf.StringList 62 | Headers map[string]string 63 | NameServerConfig []*conf.NameServerConfig 64 | EnableREALITY bool 65 | REALITYConfig *REALITYConfig 66 | Show bool 67 | EnableTFO bool 68 | Dest string 69 | ProxyProtocolVer uint64 70 | ServerNames []string 71 | PrivateKey string 72 | MinClientVer string 73 | MaxClientVer string 74 | MaxTimeDiff uint64 75 | ShortIds []string 76 | Xver uint64 77 | Flow string 78 | Security string 79 | Key string 80 | RejectUnknownSni bool 81 | } 82 | 83 | type UserInfo struct { 84 | UID int 85 | Email string 86 | UUID string 87 | Passwd string 88 | Port uint32 89 | AlterID uint16 90 | Method string 91 | SpeedLimit uint64 // Bps 92 | DeviceLimit int 93 | } 94 | 95 | type OnlineUser struct { 96 | UID int 97 | IP string 98 | } 99 | 100 | type UserTraffic struct { 101 | UID int 102 | Email string 103 | Upload int64 104 | Download int64 105 | } 106 | 107 | type ClientInfo struct { 108 | APIHost string 109 | NodeID int 110 | Key string 111 | NodeType string 112 | } 113 | 114 | type DetectRule struct { 115 | ID int 116 | Pattern *regexp.Regexp 117 | } 118 | 119 | type DetectResult struct { 120 | UID int 121 | RuleID int 122 | } 123 | 124 | type REALITYConfig struct { 125 | Dest string 126 | ProxyProtocolVer uint64 127 | ServerNames []string 128 | PrivateKey string 129 | MinClientVer string 130 | MaxClientVer string 131 | MaxTimeDiff uint64 132 | ShortIds []string 133 | } 134 | -------------------------------------------------------------------------------- /api/bunpanel/bunpanel_test.go: -------------------------------------------------------------------------------- 1 | package bunpanel_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/wyx2685/XrayR/api" 7 | "github.com/wyx2685/XrayR/api/bunpanel" 8 | ) 9 | 10 | func CreateClient() api.API { 11 | apiConfig := &api.Config{ 12 | APIHost: "http://localhost:8080", 13 | Key: "123456", 14 | NodeID: 1, 15 | NodeType: "V2ray", 16 | } 17 | client := bunpanel.New(apiConfig) 18 | return client 19 | } 20 | 21 | func TestGetV2rayNodeInfo(t *testing.T) { 22 | client := CreateClient() 23 | nodeInfo, err := client.GetNodeInfo() 24 | if err != nil { 25 | t.Error(err) 26 | } 27 | t.Log(nodeInfo) 28 | } 29 | 30 | func TestGetSSNodeInfo(t *testing.T) { 31 | apiConfig := &api.Config{ 32 | APIHost: "http://127.0.0.1:668", 33 | Key: "qwertyuiopasdfghjkl", 34 | NodeID: 1, 35 | NodeType: "Shadowsocks", 36 | } 37 | client := bunpanel.New(apiConfig) 38 | nodeInfo, err := client.GetNodeInfo() 39 | if err != nil { 40 | t.Error(err) 41 | } 42 | t.Log(nodeInfo) 43 | } 44 | 45 | func TestGetTrojanNodeInfo(t *testing.T) { 46 | apiConfig := &api.Config{ 47 | APIHost: "http://127.0.0.1:668", 48 | Key: "qwertyuiopasdfghjkl", 49 | NodeID: 1, 50 | NodeType: "Trojan", 51 | } 52 | client := bunpanel.New(apiConfig) 53 | nodeInfo, err := client.GetNodeInfo() 54 | if err != nil { 55 | t.Error(err) 56 | } 57 | t.Log(nodeInfo) 58 | } 59 | 60 | func TestGetUserList(t *testing.T) { 61 | client := CreateClient() 62 | 63 | userList, err := client.GetUserList() 64 | if err != nil { 65 | t.Error(err) 66 | } 67 | 68 | t.Log(userList) 69 | } 70 | 71 | func TestReportReportUserTraffic(t *testing.T) { 72 | client := CreateClient() 73 | userList, err := client.GetUserList() 74 | if err != nil { 75 | t.Error(err) 76 | } 77 | generalUserTraffic := make([]api.UserTraffic, len(*userList)) 78 | for i, userInfo := range *userList { 79 | generalUserTraffic[i] = api.UserTraffic{ 80 | UID: userInfo.UID, 81 | Upload: 1111, 82 | Download: 2222, 83 | } 84 | } 85 | // client.Debug() 86 | err = client.ReportUserTraffic(&generalUserTraffic) 87 | if err != nil { 88 | t.Error(err) 89 | } 90 | } 91 | 92 | func TestGetNodeRule(t *testing.T) { 93 | client := CreateClient() 94 | client.Debug() 95 | ruleList, err := client.GetNodeRule() 96 | if err != nil { 97 | t.Error(err) 98 | } 99 | 100 | t.Log(ruleList) 101 | } 102 | -------------------------------------------------------------------------------- /api/bunpanel/model.go: -------------------------------------------------------------------------------- 1 | package bunpanel 2 | 3 | import "encoding/json" 4 | 5 | type Server struct { 6 | Port int `json:"serverPort"` 7 | Network string `json:"network"` 8 | Method string `json:"method"` 9 | Security string `json:"security"` 10 | Flow string `json:"flow"` 11 | WsSettings json.RawMessage `json:"wsSettings"` 12 | RealitySettings json.RawMessage `json:"realitySettings"` 13 | GrpcSettings json.RawMessage `json:"grpcSettings"` 14 | TcpSettings json.RawMessage `json:"tcpSettings"` 15 | } 16 | 17 | type WsSettings struct { 18 | Path string `json:"path"` 19 | Headers struct { 20 | Host string `json:"Host"` 21 | } `json:"headers"` 22 | } 23 | 24 | type GrpcSettigns struct { 25 | ServiceName string `json:"serviceName"` 26 | } 27 | 28 | type TcpSettings struct { 29 | Header json.RawMessage `json:"header"` 30 | } 31 | 32 | type RealitySettings struct { 33 | Show bool `json:"show"` 34 | Dest string `json:"dest"` 35 | Xver uint64 `json:"xver"` 36 | ServerNames []string `json:"serverNames"` 37 | PrivateKey string `json:"privateKey"` 38 | MinClientVer string `json:"minClientVer"` 39 | MaxClientVer string `json:"maxClientVer"` 40 | MaxTimeDiff uint64 `json:"maxTimeDiff"` 41 | ProxyProtocolVer uint64 `json:"proxyProtocolVer"` 42 | ShortIds []string `json:"shortIds"` 43 | } 44 | 45 | type User struct { 46 | ID int `json:"id"` 47 | UUID string `json:"uuid"` 48 | SpeedLimit float64 `json:"speedLimit"` 49 | DeviceLimit int `json:"ipLimit"` 50 | AliveIP int `json:"onlineIp"` 51 | } 52 | 53 | type OnlineUser struct { 54 | UID int `json:"userId"` 55 | IP string `json:"ip"` 56 | } 57 | 58 | // UserTraffic is the data structure of traffic 59 | type UserTraffic struct { 60 | UID int `json:"userId"` 61 | Upload int64 `json:"u"` 62 | Download int64 `json:"d"` 63 | } 64 | 65 | type Response struct { 66 | StatusCode int `json:"statusCode"` 67 | Datas json.RawMessage `json:"datas"` 68 | } 69 | 70 | type PostData struct { 71 | Data interface{} `json:"data"` 72 | } -------------------------------------------------------------------------------- /api/gov2panel/gov2panel.go: -------------------------------------------------------------------------------- 1 | package gov2panel 2 | 3 | import ( 4 | "bufio" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "os" 9 | "regexp" 10 | "strconv" 11 | "strings" 12 | "sync/atomic" 13 | "time" 14 | 15 | "github.com/bitly/go-simplejson" 16 | log "github.com/sirupsen/logrus" 17 | 18 | "github.com/go-resty/resty/v2" 19 | "github.com/gogf/gf/v2/util/gconv" 20 | "github.com/xtls/xray-core/common/net" 21 | "github.com/xtls/xray-core/infra/conf" 22 | 23 | "github.com/wyx2685/XrayR/api" 24 | ) 25 | 26 | // APIClient create an api client to the panel. 27 | type APIClient struct { 28 | client *resty.Client 29 | APIHost string 30 | NodeID int 31 | Key string 32 | NodeType string 33 | EnableVless bool 34 | VlessFlow string 35 | SpeedLimit float64 36 | DeviceLimit int 37 | LocalRuleList []api.DetectRule 38 | resp atomic.Value 39 | eTags map[string]string 40 | } 41 | 42 | // New create an api instance 43 | func New(apiConfig *api.Config) *APIClient { 44 | client := resty.New() 45 | client.SetRetryCount(3) 46 | if apiConfig.Timeout > 0 { 47 | client.SetTimeout(time.Duration(apiConfig.Timeout) * time.Second) 48 | } else { 49 | client.SetTimeout(5 * time.Second) 50 | } 51 | client.OnError(func(req *resty.Request, err error) { 52 | if v, ok := err.(*resty.ResponseError); ok { 53 | // v.Response contains the last response from the server 54 | // v.Err contains the original error 55 | log.Print(v.Err) 56 | } 57 | }) 58 | client.SetBaseURL(apiConfig.APIHost) 59 | // Create Key for each requests 60 | client.SetQueryParams(map[string]string{ 61 | "node_id": strconv.Itoa(apiConfig.NodeID), 62 | "node_type": strings.ToLower(apiConfig.NodeType), 63 | "token": apiConfig.Key, 64 | }) 65 | // Read local rule list 66 | localRuleList := readLocalRuleList(apiConfig.RuleListPath) 67 | apiClient := &APIClient{ 68 | client: client, 69 | NodeID: apiConfig.NodeID, 70 | Key: apiConfig.Key, 71 | APIHost: apiConfig.APIHost, 72 | NodeType: apiConfig.NodeType, 73 | EnableVless: apiConfig.EnableVless, 74 | VlessFlow: apiConfig.VlessFlow, 75 | SpeedLimit: apiConfig.SpeedLimit, 76 | DeviceLimit: apiConfig.DeviceLimit, 77 | LocalRuleList: localRuleList, 78 | eTags: make(map[string]string), 79 | } 80 | return apiClient 81 | } 82 | 83 | // readLocalRuleList reads the local rule list file 84 | func readLocalRuleList(path string) (LocalRuleList []api.DetectRule) { 85 | LocalRuleList = make([]api.DetectRule, 0) 86 | 87 | if path != "" { 88 | // open the file 89 | file, err := os.Open(path) 90 | defer file.Close() 91 | // handle errors while opening 92 | if err != nil { 93 | log.Printf("Error when opening file: %s", err) 94 | return LocalRuleList 95 | } 96 | 97 | fileScanner := bufio.NewScanner(file) 98 | 99 | // read line by line 100 | for fileScanner.Scan() { 101 | LocalRuleList = append(LocalRuleList, api.DetectRule{ 102 | ID: -1, 103 | Pattern: regexp.MustCompile(fileScanner.Text()), 104 | }) 105 | } 106 | // handle first encountered error while reading 107 | if err := fileScanner.Err(); err != nil { 108 | log.Fatalf("Error while reading file: %s", err) 109 | return 110 | } 111 | } 112 | 113 | return LocalRuleList 114 | } 115 | 116 | // Describe return a description of the client 117 | func (c *APIClient) Describe() api.ClientInfo { 118 | return api.ClientInfo{APIHost: c.APIHost, NodeID: c.NodeID, Key: c.Key, NodeType: c.NodeType} 119 | } 120 | 121 | // Debug set the client debug for client 122 | func (c *APIClient) Debug() { 123 | c.client.SetDebug(true) 124 | } 125 | 126 | func (c *APIClient) assembleURL(path string) string { 127 | return c.APIHost + path 128 | } 129 | 130 | func (c *APIClient) parseResponse(res *resty.Response, path string, err error) (*simplejson.Json, error) { 131 | if err != nil { 132 | return nil, fmt.Errorf("request %s failed: %v", c.assembleURL(path), err) 133 | } 134 | 135 | if res.StatusCode() > 399 { 136 | return nil, fmt.Errorf("request %s failed: %s, %v", c.assembleURL(path), res.String(), err) 137 | } 138 | 139 | rtn, err := simplejson.NewJson(res.Body()) 140 | if err != nil { 141 | return nil, fmt.Errorf("ret %s invalid", res.String()) 142 | } 143 | 144 | return rtn, nil 145 | } 146 | 147 | // GetNodeInfo will pull NodeInfo Config from panel 148 | func (c *APIClient) GetNodeInfo() (nodeInfo *api.NodeInfo, err error) { 149 | server := new(serverConfig) 150 | path := "/api/server/config" 151 | 152 | res, err := c.client.R(). 153 | SetHeader("If-None-Match", c.eTags["node"]). 154 | ForceContentType("application/json"). 155 | Get(path) 156 | 157 | // Etag identifier for a specific version of a resource. StatusCode = 304 means no changed 158 | if res.StatusCode() == 304 { 159 | return nil, errors.New(api.NodeNotModified) 160 | } 161 | // update etag 162 | if res.Header().Get("Etag") != "" && res.Header().Get("Etag") != c.eTags["node"] { 163 | c.eTags["node"] = res.Header().Get("Etag") 164 | } 165 | 166 | nodeInfoResp, err := c.parseResponse(res, path, err) 167 | if err != nil { 168 | return nil, err 169 | } 170 | b, _ := nodeInfoResp.Encode() 171 | json.Unmarshal(b, server) 172 | 173 | if gconv.Uint32(server.Port) == 0 { 174 | return nil, errors.New("server port must > 0") 175 | } 176 | 177 | c.resp.Store(server) 178 | 179 | switch c.NodeType { 180 | case "V2ray", "Vmess", "Vless": 181 | nodeInfo, err = c.parseV2rayNodeResponse(server) 182 | case "Trojan": 183 | nodeInfo, err = c.parseTrojanNodeResponse(server) 184 | case "Shadowsocks": 185 | nodeInfo, err = c.parseSSNodeResponse(server) 186 | default: 187 | return nil, fmt.Errorf("unsupported node type: %s", c.NodeType) 188 | } 189 | 190 | if err != nil { 191 | return nil, fmt.Errorf("parse node info failed: %s, \nError: %v", res.String(), err) 192 | } 193 | 194 | return nodeInfo, nil 195 | } 196 | 197 | // GetUserList will pull user form panel 198 | func (c *APIClient) GetUserList() (UserList *[]api.UserInfo, err error) { 199 | var users []*user 200 | path := "/api/server/user" 201 | 202 | switch c.NodeType { 203 | case "V2ray", "Trojan", "Shadowsocks", "Vmess", "Vless": 204 | break 205 | default: 206 | return nil, fmt.Errorf("unsupported node type: %s", c.NodeType) 207 | } 208 | 209 | res, err := c.client.R(). 210 | SetHeader("If-None-Match", c.eTags["users"]). 211 | ForceContentType("application/json"). 212 | Get(path) 213 | 214 | // Etag identifier for a specific version of a resource. StatusCode = 304 means no changed 215 | if res.StatusCode() == 304 { 216 | return nil, errors.New(api.UserNotModified) 217 | } 218 | // update etag 219 | if res.Header().Get("Etag") != "" && res.Header().Get("Etag") != c.eTags["users"] { 220 | c.eTags["users"] = res.Header().Get("Etag") 221 | } 222 | 223 | usersResp, err := c.parseResponse(res, path, err) 224 | if err != nil { 225 | return nil, err 226 | } 227 | b, _ := usersResp.Get("users").Encode() 228 | json.Unmarshal(b, &users) 229 | if len(users) == 0 { 230 | return nil, errors.New("users is null") 231 | } 232 | 233 | userList := make([]api.UserInfo, len(users)) 234 | for i := 0; i < len(users); i++ { 235 | u := api.UserInfo{ 236 | UID: users[i].Id, 237 | UUID: users[i].Uuid, 238 | } 239 | 240 | // Support 1.7.1 speed limit 241 | if c.SpeedLimit > 0 { 242 | u.SpeedLimit = uint64(c.SpeedLimit * 1000000 / 8) 243 | } else { 244 | u.SpeedLimit = uint64(users[i].SpeedLimit * 1000000 / 8) 245 | } 246 | 247 | u.DeviceLimit = c.DeviceLimit // todo waiting v2board send configuration 248 | u.Email = u.UUID + "@gov2panel.user" 249 | if c.NodeType == "Shadowsocks" { 250 | u.Passwd = u.UUID 251 | } 252 | userList[i] = u 253 | } 254 | 255 | return &userList, nil 256 | } 257 | 258 | // ReportUserTraffic reports the user traffic 259 | func (c *APIClient) ReportUserTraffic(userTraffic *[]api.UserTraffic) error { 260 | path := "/api/server/push" 261 | 262 | res, err := c.client.R().SetBody(userTraffic).ForceContentType("application/json").Post(path) 263 | _, err = c.parseResponse(res, path, err) 264 | if err != nil { 265 | return err 266 | } 267 | 268 | return nil 269 | } 270 | 271 | // GetNodeRule implements the API interface 272 | func (c *APIClient) GetNodeRule() (*[]api.DetectRule, error) { 273 | routes := c.resp.Load().(*serverConfig).Routes 274 | 275 | ruleList := c.LocalRuleList 276 | 277 | for i := range routes { 278 | if routes[i].Action == "block" { 279 | 280 | ruleList = append(ruleList, api.DetectRule{ 281 | ID: i, 282 | Pattern: regexp.MustCompile(strings.Join(routes[i].Match, "|")), 283 | }) 284 | } 285 | } 286 | 287 | return &ruleList, nil 288 | } 289 | 290 | // ReportNodeStatus implements the API interface 291 | func (c *APIClient) ReportNodeStatus(nodeStatus *api.NodeStatus) (err error) { 292 | return nil 293 | } 294 | 295 | // ReportNodeOnlineUsers implements the API interface 296 | func (c *APIClient) ReportNodeOnlineUsers(onlineUserList *[]api.OnlineUser) error { 297 | return nil 298 | } 299 | 300 | // ReportIllegal implements the API interface 301 | func (c *APIClient) ReportIllegal(detectResultList *[]api.DetectResult) error { 302 | return nil 303 | } 304 | 305 | // parseTrojanNodeResponse parse the response for the given nodeInfo format 306 | func (c *APIClient) parseTrojanNodeResponse(s *serverConfig) (*api.NodeInfo, error) { 307 | // Create GeneralNodeInfo 308 | nodeInfo := &api.NodeInfo{ 309 | NodeType: c.NodeType, 310 | NodeID: c.NodeID, 311 | Port: gconv.Uint32(s.Port), 312 | TransportProtocol: "tcp", 313 | EnableTLS: true, 314 | Host: s.Host, 315 | ServiceName: s.Sni, 316 | NameServerConfig: s.parseDNSConfig(), 317 | } 318 | return nodeInfo, nil 319 | } 320 | 321 | // parseSSNodeResponse parse the response for the given nodeInfo format 322 | func (c *APIClient) parseSSNodeResponse(s *serverConfig) (*api.NodeInfo, error) { 323 | var header json.RawMessage 324 | 325 | if s.Obfs == "http" { 326 | path := "/" 327 | if p := s.ObfsSettings.Path; p != "" { 328 | if strings.HasPrefix(p, "/") { 329 | path = p 330 | } else { 331 | path += p 332 | } 333 | } 334 | h := simplejson.New() 335 | h.Set("type", "http") 336 | h.SetPath([]string{"request", "path"}, path) 337 | header, _ = h.Encode() 338 | } 339 | // Create GeneralNodeInfo 340 | return &api.NodeInfo{ 341 | NodeType: c.NodeType, 342 | NodeID: c.NodeID, 343 | Port: gconv.Uint32(s.Port), 344 | TransportProtocol: "tcp", 345 | CypherMethod: s.Encryption, 346 | ServerKey: s.ServerKey, // shadowsocks2022 share key 347 | NameServerConfig: s.parseDNSConfig(), 348 | Header: header, 349 | }, nil 350 | } 351 | 352 | // parseV2rayNodeResponse parse the response for the given nodeInfo format 353 | func (c *APIClient) parseV2rayNodeResponse(s *serverConfig) (*api.NodeInfo, error) { 354 | var ( 355 | header json.RawMessage 356 | enableTLS bool 357 | ) 358 | 359 | switch s.Net { 360 | case "tcp": 361 | if s.Header != nil { 362 | if httpHeader, err := s.Header.MarshalJSON(); err != nil { 363 | return nil, err 364 | } else { 365 | header = httpHeader 366 | } 367 | } 368 | } 369 | 370 | if s.TLS == "tls" { 371 | enableTLS = true 372 | } 373 | 374 | // Create GeneralNodeInfo 375 | return &api.NodeInfo{ 376 | NodeType: c.NodeType, 377 | NodeID: c.NodeID, 378 | Port: gconv.Uint32(s.Port), 379 | AlterID: 0, 380 | TransportProtocol: s.Net, 381 | EnableTLS: enableTLS, 382 | Path: s.Path, 383 | Host: s.Host, 384 | EnableVless: c.EnableVless, 385 | VlessFlow: c.VlessFlow, 386 | ServiceName: s.Sni, 387 | Header: header, 388 | NameServerConfig: s.parseDNSConfig(), 389 | }, nil 390 | } 391 | 392 | func (s *serverConfig) parseDNSConfig() (nameServerList []*conf.NameServerConfig) { 393 | for i := range s.Routes { 394 | if s.Routes[i].Action == "dns" { 395 | nameServerList = append(nameServerList, &conf.NameServerConfig{ 396 | Address: &conf.Address{Address: net.ParseAddress(s.Routes[i].ActionValue)}, 397 | Domains: s.Routes[i].Match, 398 | }) 399 | } 400 | } 401 | 402 | return 403 | } 404 | -------------------------------------------------------------------------------- /api/gov2panel/gov2panel_test.go: -------------------------------------------------------------------------------- 1 | package gov2panel_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/wyx2685/XrayR/api" 7 | "github.com/wyx2685/XrayR/api/gov2panel" 8 | ) 9 | 10 | func CreateClient() api.API { 11 | apiConfig := &api.Config{ 12 | APIHost: "http://localhost:8080", 13 | Key: "123456", 14 | NodeID: 1, 15 | NodeType: "V2ray", 16 | } 17 | client := gov2panel.New(apiConfig) 18 | return client 19 | } 20 | 21 | func TestGetV2rayNodeInfo(t *testing.T) { 22 | client := CreateClient() 23 | nodeInfo, err := client.GetNodeInfo() 24 | if err != nil { 25 | t.Error(err) 26 | } 27 | t.Log(nodeInfo) 28 | } 29 | 30 | func TestGetSSNodeInfo(t *testing.T) { 31 | apiConfig := &api.Config{ 32 | APIHost: "http://127.0.0.1:668", 33 | Key: "qwertyuiopasdfghjkl", 34 | NodeID: 1, 35 | NodeType: "Shadowsocks", 36 | } 37 | client := gov2panel.New(apiConfig) 38 | nodeInfo, err := client.GetNodeInfo() 39 | if err != nil { 40 | t.Error(err) 41 | } 42 | t.Log(nodeInfo) 43 | } 44 | 45 | func TestGetTrojanNodeInfo(t *testing.T) { 46 | apiConfig := &api.Config{ 47 | APIHost: "http://127.0.0.1:668", 48 | Key: "qwertyuiopasdfghjkl", 49 | NodeID: 1, 50 | NodeType: "Trojan", 51 | } 52 | client := gov2panel.New(apiConfig) 53 | nodeInfo, err := client.GetNodeInfo() 54 | if err != nil { 55 | t.Error(err) 56 | } 57 | t.Log(nodeInfo) 58 | } 59 | 60 | func TestGetUserList(t *testing.T) { 61 | client := CreateClient() 62 | 63 | userList, err := client.GetUserList() 64 | if err != nil { 65 | t.Error(err) 66 | } 67 | 68 | t.Log(userList) 69 | } 70 | 71 | func TestReportReportUserTraffic(t *testing.T) { 72 | client := CreateClient() 73 | userList, err := client.GetUserList() 74 | if err != nil { 75 | t.Error(err) 76 | } 77 | generalUserTraffic := make([]api.UserTraffic, len(*userList)) 78 | for i, userInfo := range *userList { 79 | generalUserTraffic[i] = api.UserTraffic{ 80 | UID: userInfo.UID, 81 | Upload: 1111, 82 | Download: 2222, 83 | } 84 | } 85 | // client.Debug() 86 | err = client.ReportUserTraffic(&generalUserTraffic) 87 | if err != nil { 88 | t.Error(err) 89 | } 90 | } 91 | 92 | func TestGetNodeRule(t *testing.T) { 93 | client := CreateClient() 94 | client.Debug() 95 | ruleList, err := client.GetNodeRule() 96 | if err != nil { 97 | t.Error(err) 98 | } 99 | 100 | t.Log(ruleList) 101 | } 102 | -------------------------------------------------------------------------------- /api/gov2panel/model.go: -------------------------------------------------------------------------------- 1 | package gov2panel 2 | 3 | import "encoding/json" 4 | 5 | type serverConfig struct { 6 | v2ray 7 | shadowsocks 8 | //--- 9 | Routes []route `json:"routes"` 10 | Header *json.RawMessage `json:"header"` 11 | } 12 | 13 | type v2ray struct { 14 | Port string `json:"port"` 15 | Scy string `json:"scy"` 16 | Net string `json:"net"` 17 | Type string `json:"type"` 18 | Host string `json:"host"` 19 | Path string `json:"path"` 20 | TLS string `json:"tls"` 21 | Sni string `json:"sni"` 22 | Alpn string `json:"alpn"` 23 | } 24 | 25 | type shadowsocks struct { 26 | Encryption string `json:"encryption"` 27 | Obfs string `json:"obfs"` 28 | ObfsSettings struct { 29 | Path string `json:"path"` 30 | Host string `json:"host"` 31 | } `json:"obfs_settings"` 32 | ServerKey string `json:"server_key"` 33 | } 34 | 35 | type route struct { 36 | Id int `json:"id"` 37 | Match []string `json:"match"` 38 | Action string `json:"action"` 39 | ActionValue string `json:"action_value"` 40 | } 41 | 42 | type user struct { 43 | Id int `json:"id"` 44 | Uuid string `json:"uuid"` 45 | SpeedLimit int `json:"speed_limit"` 46 | } 47 | -------------------------------------------------------------------------------- /api/newV2board/model.go: -------------------------------------------------------------------------------- 1 | package newV2board 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | type serverConfig struct { 8 | shadowsocks 9 | v2ray 10 | trojan 11 | 12 | ServerPort int `json:"server_port"` 13 | BaseConfig struct { 14 | PushInterval int `json:"push_interval"` 15 | PullInterval int `json:"pull_interval"` 16 | } `json:"base_config"` 17 | Routes []route `json:"routes"` 18 | } 19 | 20 | type shadowsocks struct { 21 | Cipher string `json:"cipher"` 22 | Obfs string `json:"obfs"` 23 | ObfsSettings struct { 24 | Path string `json:"path"` 25 | Host string `json:"host"` 26 | } `json:"obfs_settings"` 27 | ServerKey string `json:"server_key"` 28 | } 29 | 30 | type v2ray struct { 31 | Network string `json:"network"` 32 | NetworkSettings struct { 33 | Path string `json:"path"` 34 | Host string `json:"host"` 35 | Headers *json.RawMessage `json:"headers"` 36 | ServiceName string `json:"serviceName"` 37 | Header *json.RawMessage `json:"header"` 38 | } `json:"networkSettings"` 39 | VlessFlow string `json:"flow"` 40 | TlsSettings struct { 41 | ServerPort string `json:"server_port"` 42 | Dest string `json:"dest"` 43 | Xver uint64 `json:"xver,string"` 44 | Sni string `json:"server_name"` 45 | PrivateKey string `json:"private_key"` 46 | ShortId string `json:"short_id"` 47 | } `json:"tls_settings"` 48 | Tls int `json:"tls"` 49 | } 50 | 51 | type trojan struct { 52 | Host string `json:"host"` 53 | ServerName string `json:"server_name"` 54 | } 55 | 56 | type route struct { 57 | Id int `json:"id"` 58 | Match []string `json:"match"` 59 | Action string `json:"action"` 60 | ActionValue string `json:"action_value"` 61 | } 62 | 63 | type user struct { 64 | Id int `json:"id"` 65 | Uuid string `json:"uuid"` 66 | SpeedLimit int `json:"speed_limit"` 67 | DeviceLimit int `json:"device_limit"` 68 | } 69 | 70 | type AliveMap struct { 71 | Alive map[int]int `json:"alive"` 72 | } 73 | -------------------------------------------------------------------------------- /api/newV2board/v2board_test.go: -------------------------------------------------------------------------------- 1 | package newV2board_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/wyx2685/XrayR/api" 7 | "github.com/wyx2685/XrayR/api/newV2board" 8 | ) 9 | 10 | func CreateClient() api.API { 11 | apiConfig := &api.Config{ 12 | APIHost: "http://localhost:9897", 13 | Key: "qwertyuiopasdfghjkl", 14 | NodeID: 1, 15 | NodeType: "V2ray", 16 | } 17 | client := newV2board.New(apiConfig) 18 | return client 19 | } 20 | 21 | func TestGetV2rayNodeInfo(t *testing.T) { 22 | client := CreateClient() 23 | nodeInfo, err := client.GetNodeInfo() 24 | if err != nil { 25 | t.Error(err) 26 | } 27 | t.Log(nodeInfo) 28 | } 29 | 30 | func TestGetSSNodeInfo(t *testing.T) { 31 | apiConfig := &api.Config{ 32 | APIHost: "http://127.0.0.1:668", 33 | Key: "qwertyuiopasdfghjkl", 34 | NodeID: 1, 35 | NodeType: "Shadowsocks", 36 | } 37 | client := newV2board.New(apiConfig) 38 | nodeInfo, err := client.GetNodeInfo() 39 | if err != nil { 40 | t.Error(err) 41 | } 42 | t.Log(nodeInfo) 43 | } 44 | 45 | func TestGetTrojanNodeInfo(t *testing.T) { 46 | apiConfig := &api.Config{ 47 | APIHost: "http://127.0.0.1:668", 48 | Key: "qwertyuiopasdfghjkl", 49 | NodeID: 1, 50 | NodeType: "Trojan", 51 | } 52 | client := newV2board.New(apiConfig) 53 | nodeInfo, err := client.GetNodeInfo() 54 | if err != nil { 55 | t.Error(err) 56 | } 57 | t.Log(nodeInfo) 58 | } 59 | 60 | func TestGetUserList(t *testing.T) { 61 | client := CreateClient() 62 | 63 | userList, err := client.GetUserList() 64 | if err != nil { 65 | t.Error(err) 66 | } 67 | 68 | t.Log(userList) 69 | } 70 | 71 | func TestReportReportUserTraffic(t *testing.T) { 72 | client := CreateClient() 73 | userList, err := client.GetUserList() 74 | if err != nil { 75 | t.Error(err) 76 | } 77 | generalUserTraffic := make([]api.UserTraffic, len(*userList)) 78 | for i, userInfo := range *userList { 79 | generalUserTraffic[i] = api.UserTraffic{ 80 | UID: userInfo.UID, 81 | Upload: 114514, 82 | Download: 114514, 83 | } 84 | } 85 | // client.Debug() 86 | err = client.ReportUserTraffic(&generalUserTraffic) 87 | if err != nil { 88 | t.Error(err) 89 | } 90 | } 91 | 92 | func TestGetNodeRule(t *testing.T) { 93 | client := CreateClient() 94 | client.Debug() 95 | ruleList, err := client.GetNodeRule() 96 | if err != nil { 97 | t.Error(err) 98 | } 99 | 100 | t.Log(ruleList) 101 | } 102 | -------------------------------------------------------------------------------- /api/pmpanel/model.go: -------------------------------------------------------------------------------- 1 | package pmpanel 2 | 3 | import "encoding/json" 4 | 5 | // NodeInfoResponse is the response of node 6 | type NodeInfoResponse struct { 7 | Class int `json:"clazz"` 8 | SpeedLimit float64 `json:"speedlimit"` 9 | Method string `json:"method"` 10 | TrafficRate float64 `json:"trafficRate"` 11 | RawServerString string `json:"outServer"` 12 | Port uint32 `json:"outPort"` 13 | AlterId uint16 `json:"alterId"` 14 | Network string `json:"network"` 15 | Security string `json:"security"` 16 | Host string `json:"host"` 17 | Path string `json:"path"` 18 | Grpc bool `json:"grpc"` 19 | Sni string `json:"sni"` 20 | } 21 | 22 | // UserResponse is the response of user 23 | type UserResponse struct { 24 | ID int `json:"id"` 25 | Passwd string `json:"passwd"` 26 | SpeedLimit float64 `json:"nodeSpeedlimit"` 27 | DeviceLimit int `json:"nodeConnector"` 28 | } 29 | 30 | // Response is the common response 31 | type Response struct { 32 | Ret uint `json:"ret"` 33 | Data json.RawMessage `json:"data"` 34 | } 35 | 36 | // PostData is the data structure of post data 37 | type PostData struct { 38 | Type string `json:"type"` 39 | NodeId int `json:"nodeId"` 40 | Users interface{} `json:"users"` 41 | Onlines interface{} `json:"onlines"` 42 | } 43 | 44 | // SystemLoad is the data structure of systemload 45 | type SystemLoad struct { 46 | Uptime string `json:"uptime"` 47 | Load string `json:"load"` 48 | } 49 | 50 | // OnlineUser is the data structure of online user 51 | type OnlineUser struct { 52 | UID int `json:"user_id"` 53 | IP string `json:"ip"` 54 | } 55 | 56 | // UserTraffic is the data structure of traffic 57 | type UserTraffic struct { 58 | UID int `json:"id"` 59 | Upload int64 `json:"up"` 60 | Download int64 `json:"down"` 61 | Ip string `json:"ip"` 62 | } 63 | 64 | type RuleItem struct { 65 | ID int `json:"id"` 66 | Content string `json:"regex"` 67 | } 68 | 69 | type IllegalItem struct { 70 | ID int `json:"list_id"` 71 | UID int `json:"user_id"` 72 | } 73 | -------------------------------------------------------------------------------- /api/pmpanel/pmpanel_test.go: -------------------------------------------------------------------------------- 1 | package pmpanel_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/wyx2685/XrayR/api" 8 | "github.com/wyx2685/XrayR/api/pmpanel" 9 | ) 10 | 11 | func CreateClient() api.API { 12 | apiConfig := &api.Config{ 13 | APIHost: "http://webapi.yyds.me", 14 | Key: "123456", 15 | NodeID: 4, 16 | NodeType: "V2ray", 17 | } 18 | client := pmpanel.New(apiConfig) 19 | return client 20 | } 21 | 22 | func TestGetV2rayNodeinfo(t *testing.T) { 23 | client := CreateClient() 24 | client.Debug() 25 | nodeInfo, err := client.GetNodeInfo() 26 | if err != nil { 27 | t.Error(err) 28 | } 29 | t.Log(nodeInfo) 30 | } 31 | 32 | func TestGetSSNodeinfo(t *testing.T) { 33 | apiConfig := &api.Config{ 34 | APIHost: "http://webapi.yyds.me", 35 | Key: "123456", 36 | NodeID: 1, 37 | NodeType: "Shadowsocks", 38 | } 39 | client := pmpanel.New(apiConfig) 40 | client.Debug() 41 | nodeInfo, err := client.GetNodeInfo() 42 | if err != nil { 43 | t.Error(err) 44 | } 45 | t.Log(nodeInfo) 46 | } 47 | 48 | func TestGetTrojanNodeinfo(t *testing.T) { 49 | apiConfig := &api.Config{ 50 | APIHost: "http://webapi.yyds.me", 51 | Key: "123456", 52 | NodeID: 1, 53 | NodeType: "Trojan", 54 | } 55 | client := pmpanel.New(apiConfig) 56 | client.Debug() 57 | nodeInfo, err := client.GetNodeInfo() 58 | if err != nil { 59 | t.Error(err) 60 | } 61 | t.Log(nodeInfo) 62 | } 63 | 64 | func TestGetSSinfo(t *testing.T) { 65 | client := CreateClient() 66 | 67 | nodeInfo, err := client.GetNodeInfo() 68 | if err != nil { 69 | t.Error(err) 70 | } 71 | t.Log(nodeInfo) 72 | } 73 | 74 | func TestGetUserList(t *testing.T) { 75 | client := CreateClient() 76 | 77 | userList, err := client.GetUserList() 78 | if err != nil { 79 | t.Error(err) 80 | } 81 | 82 | t.Log(userList) 83 | } 84 | 85 | func TestReportNodeStatus(t *testing.T) { 86 | client := CreateClient() 87 | nodeStatus := &api.NodeStatus{ 88 | CPU: 1, Mem: 1, Disk: 1, Uptime: 256, 89 | } 90 | err := client.ReportNodeStatus(nodeStatus) 91 | if err != nil { 92 | t.Error(err) 93 | } 94 | } 95 | 96 | func TestReportReportNodeOnlineUsers(t *testing.T) { 97 | client := CreateClient() 98 | userList, err := client.GetUserList() 99 | if err != nil { 100 | t.Error(err) 101 | } 102 | 103 | onlineUserList := make([]api.OnlineUser, len(*userList)) 104 | for i, userInfo := range *userList { 105 | onlineUserList[i] = api.OnlineUser{ 106 | UID: userInfo.UID, 107 | IP: fmt.Sprintf("1.1.1.%d", i), 108 | } 109 | } 110 | // client.Debug() 111 | err = client.ReportNodeOnlineUsers(&onlineUserList) 112 | if err != nil { 113 | t.Error(err) 114 | } 115 | } 116 | 117 | func TestReportReportUserTraffic(t *testing.T) { 118 | client := CreateClient() 119 | userList, err := client.GetUserList() 120 | if err != nil { 121 | t.Error(err) 122 | } 123 | generalUserTraffic := make([]api.UserTraffic, len(*userList)) 124 | for i, userInfo := range *userList { 125 | generalUserTraffic[i] = api.UserTraffic{ 126 | UID: userInfo.UID, 127 | Upload: 114514, 128 | Download: 114514, 129 | } 130 | } 131 | // client.Debug() 132 | err = client.ReportUserTraffic(&generalUserTraffic) 133 | if err != nil { 134 | t.Error(err) 135 | } 136 | } 137 | 138 | func TestGetNodeRule(t *testing.T) { 139 | client := CreateClient() 140 | 141 | ruleList, err := client.GetNodeRule() 142 | if err != nil { 143 | t.Error(err) 144 | } 145 | 146 | t.Log(ruleList) 147 | } 148 | 149 | func TestReportIllegal(t *testing.T) { 150 | client := CreateClient() 151 | 152 | detectResult := []api.DetectResult{ 153 | {1, 2}, 154 | {1, 3}, 155 | } 156 | client.Debug() 157 | err := client.ReportIllegal(&detectResult) 158 | if err != nil { 159 | t.Error(err) 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /api/proxypanel/model.go: -------------------------------------------------------------------------------- 1 | package proxypanel 2 | 3 | import "encoding/json" 4 | 5 | type Response struct { 6 | Status string `json:"status"` 7 | Code int `json:"code"` 8 | Data json.RawMessage `json:"data"` 9 | Message string `json:"message"` 10 | } 11 | 12 | type V2rayNodeInfo struct { 13 | ID int `json:"id"` 14 | IsUDP bool `json:"is_udp"` 15 | SpeedLimit uint64 `json:"speed_limit"` 16 | ClientLimit int `json:"client_limit"` 17 | PushPort int `json:"push_port"` 18 | Secret string `json:"secret"` 19 | Key string `json:"key"` 20 | Cert string `json:"pem"` 21 | V2License string `json:"v2_license"` 22 | V2AlterID uint16 `json:"v2_alter_id"` 23 | V2Port uint32 `json:"v2_port"` 24 | V2Method string `json:"v2_method"` 25 | V2Net string `json:"v2_net"` 26 | V2Type string `json:"v2_type"` 27 | V2Host string `json:"v2_host"` 28 | V2Path string `json:"v2_path"` 29 | V2TLS bool `json:"v2_tls"` 30 | V2Cdn bool `json:"v2_cdn"` 31 | V2TLSProvider string `json:"v2_tls_provider"` 32 | RedirectUrl string `json:"redirect_url"` 33 | } 34 | 35 | type ShadowsocksNodeInfo struct { 36 | ID int `json:"id"` 37 | SpeedLimit uint64 `json:"speed_limit"` 38 | ClientLimit int `json:"client_limit"` 39 | Method string `json:"method"` 40 | Port uint32 `json:"port"` 41 | } 42 | 43 | type TrojanNodeInfo struct { 44 | ID int `json:"id"` 45 | IsUDP bool `json:"is_udp"` 46 | SpeedLimit uint64 `json:"speed_limit"` 47 | ClientLimit int `json:"client_limit"` 48 | PushPort int `json:"push_port"` 49 | TrojanPort uint32 `json:"trojan_port"` 50 | } 51 | 52 | // NodeStatus Node status report 53 | type NodeStatus struct { 54 | CPU string `json:"cpu"` 55 | Mem string `json:"mem"` 56 | Net string `json:"net"` 57 | Disk string `json:"disk"` 58 | Uptime int `json:"uptime"` 59 | } 60 | 61 | type NodeOnline struct { 62 | UID int `json:"uid"` 63 | IP string `json:"ip"` 64 | } 65 | 66 | type VMessUser struct { 67 | UID int `json:"uid"` 68 | VmessUID string `json:"vmess_uid"` 69 | SpeedLimit uint64 `json:"speed_limit"` 70 | } 71 | 72 | type TrojanUser struct { 73 | UID int `json:"uid"` 74 | Password string `json:"password"` 75 | SpeedLimit uint64 `json:"speed_limit"` 76 | } 77 | 78 | type SSUser struct { 79 | UID int `json:"uid"` 80 | Password string `json:"passwd"` 81 | SpeedLimit uint64 `json:"speed_limit"` 82 | } 83 | 84 | type UserTraffic struct { 85 | UID int `json:"uid"` 86 | Upload int64 `json:"upload"` 87 | Download int64 `json:"download"` 88 | } 89 | 90 | type NodeRule struct { 91 | Mode string `json:"mode"` 92 | Rules []NodeRuleItem `json:"rules"` 93 | } 94 | 95 | type NodeRuleItem struct { 96 | ID int `json:"id"` 97 | Type string `json:"type"` 98 | Pattern string `json:"pattern"` 99 | } 100 | 101 | type IllegalReport struct { 102 | UID int `json:"uid"` 103 | RuleID int `json:"rule_id"` 104 | Reason string `json:"reason"` 105 | } 106 | 107 | type Certificate struct { 108 | Key string `json:"key"` 109 | Pem string `json:"pem"` 110 | } 111 | -------------------------------------------------------------------------------- /api/proxypanel/proypanel_test.go: -------------------------------------------------------------------------------- 1 | package proxypanel_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/wyx2685/XrayR/api" 8 | "github.com/wyx2685/XrayR/api/proxypanel" 9 | ) 10 | 11 | func CreateClient() api.API { 12 | apiConfig := &api.Config{ 13 | APIHost: "http://127.0.0.1:8888", 14 | Key: "naBDpLvREiwY9qPr", 15 | NodeID: 1, 16 | NodeType: "V2ray", 17 | } 18 | client := proxypanel.New(apiConfig) 19 | return client 20 | } 21 | 22 | func TestGetV2rayNodeinfo(t *testing.T) { 23 | apiConfig := &api.Config{ 24 | APIHost: "http://127.0.0.1:8888", 25 | Key: "naBDpLvREiwY9qPr", 26 | NodeID: 1, 27 | NodeType: "V2ray", 28 | } 29 | client := proxypanel.New(apiConfig) 30 | 31 | nodeInfo, err := client.GetNodeInfo() 32 | if err != nil { 33 | t.Error(err) 34 | } 35 | t.Log(nodeInfo) 36 | } 37 | 38 | func TestGetSSNodeinfo(t *testing.T) { 39 | apiConfig := &api.Config{ 40 | APIHost: "http://127.0.0.1:8888", 41 | Key: "8VtrYVGFHL0Q9azc", 42 | NodeID: 3, 43 | NodeType: "Shadowsocks", 44 | } 45 | client := proxypanel.New(apiConfig) 46 | nodeInfo, err := client.GetNodeInfo() 47 | if err != nil { 48 | t.Error(err) 49 | } 50 | t.Log(nodeInfo) 51 | } 52 | 53 | func TestGetTrojanNodeinfo(t *testing.T) { 54 | apiConfig := &api.Config{ 55 | APIHost: "http://127.0.0.1:8888", 56 | Key: "kgnO2O66FmvP8rDV", 57 | NodeID: 2, 58 | NodeType: "Trojan", 59 | } 60 | client := proxypanel.New(apiConfig) 61 | nodeInfo, err := client.GetNodeInfo() 62 | if err != nil { 63 | t.Error(err) 64 | } 65 | t.Log(nodeInfo) 66 | } 67 | 68 | func TestGetSSinfo(t *testing.T) { 69 | client := CreateClient() 70 | 71 | nodeInfo, err := client.GetNodeInfo() 72 | if err != nil { 73 | t.Error(err) 74 | } 75 | t.Log(nodeInfo) 76 | } 77 | 78 | func TestGetUserList(t *testing.T) { 79 | client := CreateClient() 80 | 81 | userList, err := client.GetUserList() 82 | if err != nil { 83 | t.Error(err) 84 | } 85 | 86 | t.Log(userList) 87 | } 88 | 89 | func TestReportNodeStatus(t *testing.T) { 90 | client := CreateClient() 91 | nodeStatus := &api.NodeStatus{ 92 | CPU: 1, Mem: 1, Disk: 1, Uptime: 256, 93 | } 94 | err := client.ReportNodeStatus(nodeStatus) 95 | if err != nil { 96 | t.Error(err) 97 | } 98 | } 99 | 100 | func TestReportReportNodeOnlineUsers(t *testing.T) { 101 | client := CreateClient() 102 | userList, err := client.GetUserList() 103 | if err != nil { 104 | t.Error(err) 105 | } 106 | 107 | onlineUserList := make([]api.OnlineUser, len(*userList)) 108 | for i, userInfo := range *userList { 109 | onlineUserList[i] = api.OnlineUser{ 110 | UID: userInfo.UID, 111 | IP: fmt.Sprintf("1.1.1.%d", i), 112 | } 113 | } 114 | // client.Debug() 115 | err = client.ReportNodeOnlineUsers(&onlineUserList) 116 | if err != nil { 117 | t.Error(err) 118 | } 119 | } 120 | 121 | func TestReportReportUserTraffic(t *testing.T) { 122 | client := CreateClient() 123 | userList, err := client.GetUserList() 124 | if err != nil { 125 | t.Error(err) 126 | } 127 | generalUserTraffic := make([]api.UserTraffic, len(*userList)) 128 | for i, userInfo := range *userList { 129 | generalUserTraffic[i] = api.UserTraffic{ 130 | UID: userInfo.UID, 131 | Upload: 114514, 132 | Download: 114514, 133 | } 134 | } 135 | client.Debug() 136 | err = client.ReportUserTraffic(&generalUserTraffic) 137 | if err != nil { 138 | t.Error(err) 139 | } 140 | } 141 | 142 | func TestGetNodeRule(t *testing.T) { 143 | client := CreateClient() 144 | client.Debug() 145 | ruleList, err := client.GetNodeRule() 146 | if err != nil { 147 | t.Error(err) 148 | } 149 | 150 | t.Log(ruleList) 151 | } 152 | 153 | func TestReportIllegal(t *testing.T) { 154 | client := CreateClient() 155 | 156 | detectResult := []api.DetectResult{ 157 | {UID: 1, RuleID: 1}, 158 | {UID: 1, RuleID: 2}, 159 | } 160 | client.Debug() 161 | err := client.ReportIllegal(&detectResult) 162 | if err != nil { 163 | t.Error(err) 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /api/sspanel/model.go: -------------------------------------------------------------------------------- 1 | package sspanel 2 | 3 | import "encoding/json" 4 | 5 | // NodeInfoResponse is the response of node 6 | type NodeInfoResponse struct { 7 | Group int `json:"node_group"` 8 | Class int `json:"node_class"` 9 | SpeedLimit float64 `json:"node_speedlimit"` 10 | TrafficRate float64 `json:"traffic_rate"` 11 | Sort int `json:"sort"` 12 | RawServerString string `json:"server"` 13 | Type string `json:"type"` 14 | CustomConfig json.RawMessage `json:"custom_config"` 15 | Version string `json:"version"` 16 | } 17 | 18 | type CustomConfig struct { 19 | OffsetPortNode string `json:"offset_port_node"` 20 | Host string `json:"host"` 21 | Method string `json:"method"` 22 | TLS string `json:"tls"` 23 | EnableVless string `json:"enable_vless"` 24 | Network string `json:"network"` 25 | Security string `json:"security"` 26 | Path string `json:"path"` 27 | VerifyCert bool `json:"verify_cert"` 28 | Obfs string `json:"obfs"` 29 | Header json.RawMessage `json:"header"` 30 | AllowInsecure string `json:"allow_insecure"` 31 | Servicename string `json:"servicename"` 32 | EnableXtls string `json:"enable_xtls"` 33 | Flow string `json:"flow"` 34 | EnableREALITY bool `json:"enable_reality"` 35 | RealityOpts *REALITYConfig `json:"reality-opts"` 36 | } 37 | 38 | // UserResponse is the response of user 39 | type UserResponse struct { 40 | ID int `json:"id"` 41 | Passwd string `json:"passwd"` 42 | Port uint32 `json:"port"` 43 | Method string `json:"method"` 44 | SpeedLimit float64 `json:"node_speedlimit"` 45 | DeviceLimit int `json:"node_iplimit"` 46 | UUID string `json:"uuid"` 47 | AliveIP int `json:"alive_ip"` 48 | } 49 | 50 | // Response is the common response 51 | type Response struct { 52 | Ret uint `json:"ret"` 53 | Data json.RawMessage `json:"data"` 54 | } 55 | 56 | // PostData is the data structure of post data 57 | type PostData struct { 58 | Data interface{} `json:"data"` 59 | } 60 | 61 | // SystemLoad is the data structure of system load 62 | type SystemLoad struct { 63 | Uptime string `json:"uptime"` 64 | Load string `json:"load"` 65 | } 66 | 67 | // OnlineUser is the data structure of online user 68 | type OnlineUser struct { 69 | UID int `json:"user_id"` 70 | IP string `json:"ip"` 71 | } 72 | 73 | // UserTraffic is the data structure of traffic 74 | type UserTraffic struct { 75 | UID int `json:"user_id"` 76 | Upload int64 `json:"u"` 77 | Download int64 `json:"d"` 78 | } 79 | 80 | type RuleItem struct { 81 | ID int `json:"id"` 82 | Content string `json:"regex"` 83 | } 84 | 85 | type IllegalItem struct { 86 | ID int `json:"list_id"` 87 | UID int `json:"user_id"` 88 | } 89 | 90 | type REALITYConfig struct { 91 | Dest string `json:"dest,omitempty"` 92 | ProxyProtocolVer uint64 `json:"proxy_protocol_ver,omitempty"` 93 | ServerNames []string `json:"server_names,omitempty"` 94 | PrivateKey string `json:"private_key,omitempty"` 95 | MinClientVer string `json:"min_client_ver,omitempty"` 96 | MaxClientVer string `json:"max_client_ver,omitempty"` 97 | MaxTimeDiff uint64 `json:"max_time_diff,omitempty"` 98 | ShortIds []string `json:"short_ids,omitempty"` 99 | } 100 | -------------------------------------------------------------------------------- /api/sspanel/sspanel_test.go: -------------------------------------------------------------------------------- 1 | package sspanel_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/wyx2685/XrayR/api" 8 | "github.com/wyx2685/XrayR/api/sspanel" 9 | ) 10 | 11 | func CreateClient() api.API { 12 | apiConfig := &api.Config{ 13 | APIHost: "http://127.0.0.1:667", 14 | Key: "123", 15 | NodeID: 3, 16 | NodeType: "V2ray", 17 | } 18 | client := sspanel.New(apiConfig) 19 | return client 20 | } 21 | 22 | func TestGetV2rayNodeInfo(t *testing.T) { 23 | client := CreateClient() 24 | 25 | nodeInfo, err := client.GetNodeInfo() 26 | if err != nil { 27 | t.Error(err) 28 | } 29 | t.Log(nodeInfo) 30 | } 31 | 32 | func TestGetSSNodeInfo(t *testing.T) { 33 | apiConfig := &api.Config{ 34 | APIHost: "http://127.0.0.1:667", 35 | Key: "123", 36 | NodeID: 64, 37 | NodeType: "Shadowsocks", 38 | } 39 | client := sspanel.New(apiConfig) 40 | nodeInfo, err := client.GetNodeInfo() 41 | if err != nil { 42 | t.Error(err) 43 | } 44 | t.Log(nodeInfo) 45 | } 46 | 47 | func TestGetTrojanNodeInfo(t *testing.T) { 48 | apiConfig := &api.Config{ 49 | APIHost: "http://127.0.0.1:667", 50 | Key: "123", 51 | NodeID: 72, 52 | NodeType: "Trojan", 53 | } 54 | client := sspanel.New(apiConfig) 55 | nodeInfo, err := client.GetNodeInfo() 56 | if err != nil { 57 | t.Error(err) 58 | } 59 | t.Log(nodeInfo) 60 | } 61 | 62 | func TestGetSSInfo(t *testing.T) { 63 | client := CreateClient() 64 | 65 | nodeInfo, err := client.GetNodeInfo() 66 | if err != nil { 67 | t.Error(err) 68 | } 69 | t.Log(nodeInfo) 70 | } 71 | 72 | func TestGetUserList(t *testing.T) { 73 | client := CreateClient() 74 | 75 | userList, err := client.GetUserList() 76 | if err != nil { 77 | t.Error(err) 78 | } 79 | 80 | t.Log(userList) 81 | } 82 | 83 | func TestReportNodeStatus(t *testing.T) { 84 | client := CreateClient() 85 | nodeStatus := &api.NodeStatus{ 86 | CPU: 1, Mem: 1, Disk: 1, Uptime: 256, 87 | } 88 | err := client.ReportNodeStatus(nodeStatus) 89 | if err != nil { 90 | t.Error(err) 91 | } 92 | } 93 | 94 | func TestReportReportNodeOnlineUsers(t *testing.T) { 95 | client := CreateClient() 96 | userList, err := client.GetUserList() 97 | if err != nil { 98 | t.Error(err) 99 | } 100 | 101 | onlineUserList := make([]api.OnlineUser, len(*userList)) 102 | for i, userInfo := range *userList { 103 | onlineUserList[i] = api.OnlineUser{ 104 | UID: userInfo.UID, 105 | IP: fmt.Sprintf("1.1.1.%d", i), 106 | } 107 | } 108 | // client.Debug() 109 | err = client.ReportNodeOnlineUsers(&onlineUserList) 110 | if err != nil { 111 | t.Error(err) 112 | } 113 | } 114 | 115 | func TestReportReportUserTraffic(t *testing.T) { 116 | client := CreateClient() 117 | userList, err := client.GetUserList() 118 | if err != nil { 119 | t.Error(err) 120 | } 121 | generalUserTraffic := make([]api.UserTraffic, len(*userList)) 122 | for i, userInfo := range *userList { 123 | generalUserTraffic[i] = api.UserTraffic{ 124 | UID: userInfo.UID, 125 | Upload: 114514, 126 | Download: 114514, 127 | } 128 | } 129 | // client.Debug() 130 | err = client.ReportUserTraffic(&generalUserTraffic) 131 | if err != nil { 132 | t.Error(err) 133 | } 134 | } 135 | 136 | func TestGetNodeRule(t *testing.T) { 137 | client := CreateClient() 138 | 139 | ruleList, err := client.GetNodeRule() 140 | if err != nil { 141 | t.Error(err) 142 | } 143 | 144 | t.Log(ruleList) 145 | } 146 | 147 | func TestReportIllegal(t *testing.T) { 148 | client := CreateClient() 149 | 150 | detectResult := []api.DetectResult{ 151 | {UID: 1, RuleID: 2}, 152 | {UID: 1, RuleID: 3}, 153 | } 154 | client.Debug() 155 | err := client.ReportIllegal(&detectResult) 156 | if err != nil { 157 | t.Error(err) 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /api/v2raysocks/model.go: -------------------------------------------------------------------------------- 1 | package v2raysocks 2 | 3 | type UserTraffic struct { 4 | UID int `json:"user_id"` 5 | Upload int64 `json:"u"` 6 | Download int64 `json:"d"` 7 | } 8 | 9 | type NodeStatus struct { 10 | CPU string `json:"cpu"` 11 | Mem string `json:"mem"` 12 | Net string `json:"net"` 13 | Disk string `json:"disk"` 14 | Uptime int `json:"uptime"` 15 | } 16 | 17 | type NodeOnline struct { 18 | UID int `json:"uid"` 19 | IP string `json:"ip"` 20 | } 21 | 22 | type REALITYConfig struct { 23 | Dest string `json:"dest,omitempty"` 24 | ProxyProtocolVer uint64 `json:"proxy_protocol_ver,omitempty"` 25 | ServerNames []string `json:"server_names,omitempty"` 26 | PrivateKey string `json:"private_key,omitempty"` 27 | MinClientVer string `json:"min_client_ver,omitempty"` 28 | MaxClientVer string `json:"max_client_ver,omitempty"` 29 | MaxTimeDiff uint64 `json:"max_time_diff,omitempty"` 30 | ShortIds []string `json:"short_ids,omitempty"` 31 | } -------------------------------------------------------------------------------- /api/v2raysocks/v2raysocks_test.go: -------------------------------------------------------------------------------- 1 | package v2raysocks_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/wyx2685/XrayR/api" 7 | "github.com/wyx2685/XrayR/api/v2raysocks" 8 | ) 9 | 10 | func CreateClient() api.API { 11 | apiConfig := &api.Config{ 12 | APIHost: "https://127.0.0.1/", 13 | Key: "123456789", 14 | NodeID: 280002, 15 | NodeType: "V2ray", 16 | } 17 | client := v2raysocks.New(apiConfig) 18 | return client 19 | } 20 | 21 | func TestGetV2rayNodeinfo(t *testing.T) { 22 | client := CreateClient() 23 | client.Debug() 24 | nodeInfo, err := client.GetNodeInfo() 25 | if err != nil { 26 | t.Error(err) 27 | } 28 | t.Log(nodeInfo) 29 | } 30 | 31 | func TestGetSSNodeinfo(t *testing.T) { 32 | apiConfig := &api.Config{ 33 | APIHost: "https://127.0.0.1/", 34 | Key: "123456789", 35 | NodeID: 280009, 36 | NodeType: "Shadowsocks", 37 | } 38 | client := v2raysocks.New(apiConfig) 39 | nodeInfo, err := client.GetNodeInfo() 40 | if err != nil { 41 | t.Error(err) 42 | } 43 | t.Log(nodeInfo) 44 | } 45 | 46 | func TestGetTrojanNodeinfo(t *testing.T) { 47 | apiConfig := &api.Config{ 48 | APIHost: "https://127.0.0.1/", 49 | Key: "123456789", 50 | NodeID: 280008, 51 | NodeType: "Trojan", 52 | } 53 | client := v2raysocks.New(apiConfig) 54 | nodeInfo, err := client.GetNodeInfo() 55 | if err != nil { 56 | t.Error(err) 57 | } 58 | t.Log(nodeInfo) 59 | } 60 | 61 | func TestGetUserList(t *testing.T) { 62 | client := CreateClient() 63 | 64 | userList, err := client.GetUserList() 65 | if err != nil { 66 | t.Error(err) 67 | } 68 | 69 | t.Log(userList) 70 | } 71 | 72 | func TestReportReportUserTraffic(t *testing.T) { 73 | client := CreateClient() 74 | userList, err := client.GetUserList() 75 | if err != nil { 76 | t.Error(err) 77 | } 78 | generalUserTraffic := make([]api.UserTraffic, len(*userList)) 79 | for i, userInfo := range *userList { 80 | generalUserTraffic[i] = api.UserTraffic{ 81 | UID: userInfo.UID, 82 | Upload: 114514, 83 | Download: 114514, 84 | } 85 | } 86 | // client.Debug() 87 | err = client.ReportUserTraffic(&generalUserTraffic) 88 | if err != nil { 89 | t.Error(err) 90 | } 91 | } 92 | 93 | func TestGetNodeRule(t *testing.T) { 94 | client := CreateClient() 95 | client.Debug() 96 | ruleList, err := client.GetNodeRule() 97 | if err != nil { 98 | t.Error(err) 99 | } 100 | 101 | t.Log(ruleList) 102 | } 103 | -------------------------------------------------------------------------------- /app/app.go: -------------------------------------------------------------------------------- 1 | // Package app contains the third-party app used to replace the default app in xray-core 2 | package app 3 | -------------------------------------------------------------------------------- /app/mydispatcher/config.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.28.0 4 | // protoc v3.19.4 5 | // source: app/mydispatcher/config.proto 6 | 7 | package mydispatcher 8 | 9 | import ( 10 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 11 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 12 | reflect "reflect" 13 | sync "sync" 14 | ) 15 | 16 | const ( 17 | // Verify that this generated code is sufficiently up-to-date. 18 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 19 | // Verify that runtime/protoimpl is sufficiently up-to-date. 20 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 21 | ) 22 | 23 | type SessionConfig struct { 24 | state protoimpl.MessageState 25 | sizeCache protoimpl.SizeCache 26 | unknownFields protoimpl.UnknownFields 27 | } 28 | 29 | func (x *SessionConfig) Reset() { 30 | *x = SessionConfig{} 31 | if protoimpl.UnsafeEnabled { 32 | mi := &file_app_mydispatcher_config_proto_msgTypes[0] 33 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 34 | ms.StoreMessageInfo(mi) 35 | } 36 | } 37 | 38 | func (x *SessionConfig) String() string { 39 | return protoimpl.X.MessageStringOf(x) 40 | } 41 | 42 | func (*SessionConfig) ProtoMessage() {} 43 | 44 | func (x *SessionConfig) ProtoReflect() protoreflect.Message { 45 | mi := &file_app_mydispatcher_config_proto_msgTypes[0] 46 | if protoimpl.UnsafeEnabled && x != nil { 47 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 48 | if ms.LoadMessageInfo() == nil { 49 | ms.StoreMessageInfo(mi) 50 | } 51 | return ms 52 | } 53 | return mi.MessageOf(x) 54 | } 55 | 56 | // Deprecated: Use SessionConfig.ProtoReflect.Descriptor instead. 57 | func (*SessionConfig) Descriptor() ([]byte, []int) { 58 | return file_app_mydispatcher_config_proto_rawDescGZIP(), []int{0} 59 | } 60 | 61 | type Config struct { 62 | state protoimpl.MessageState 63 | sizeCache protoimpl.SizeCache 64 | unknownFields protoimpl.UnknownFields 65 | 66 | Settings *SessionConfig `protobuf:"bytes,1,opt,name=settings,proto3" json:"settings,omitempty"` 67 | } 68 | 69 | func (x *Config) Reset() { 70 | *x = Config{} 71 | if protoimpl.UnsafeEnabled { 72 | mi := &file_app_mydispatcher_config_proto_msgTypes[1] 73 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 74 | ms.StoreMessageInfo(mi) 75 | } 76 | } 77 | 78 | func (x *Config) String() string { 79 | return protoimpl.X.MessageStringOf(x) 80 | } 81 | 82 | func (*Config) ProtoMessage() {} 83 | 84 | func (x *Config) ProtoReflect() protoreflect.Message { 85 | mi := &file_app_mydispatcher_config_proto_msgTypes[1] 86 | if protoimpl.UnsafeEnabled && x != nil { 87 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 88 | if ms.LoadMessageInfo() == nil { 89 | ms.StoreMessageInfo(mi) 90 | } 91 | return ms 92 | } 93 | return mi.MessageOf(x) 94 | } 95 | 96 | // Deprecated: Use Config.ProtoReflect.Descriptor instead. 97 | func (*Config) Descriptor() ([]byte, []int) { 98 | return file_app_mydispatcher_config_proto_rawDescGZIP(), []int{1} 99 | } 100 | 101 | func (x *Config) GetSettings() *SessionConfig { 102 | if x != nil { 103 | return x.Settings 104 | } 105 | return nil 106 | } 107 | 108 | var File_app_mydispatcher_config_proto protoreflect.FileDescriptor 109 | 110 | var file_app_mydispatcher_config_proto_rawDesc = []byte{ 111 | 0x0a, 0x1d, 0x61, 0x70, 0x70, 0x2f, 0x6d, 0x79, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 112 | 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 113 | 0x16, 0x78, 0x72, 0x61, 0x79, 0x72, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x6d, 0x79, 0x64, 0x69, 0x73, 114 | 0x70, 0x61, 0x74, 0x63, 0x68, 0x65, 0x72, 0x22, 0x15, 0x0a, 0x0d, 0x53, 0x65, 0x73, 0x73, 0x69, 115 | 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x22, 0x4b, 116 | 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x41, 0x0a, 0x08, 0x73, 0x65, 0x74, 0x74, 117 | 0x69, 0x6e, 0x67, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x78, 0x72, 0x61, 118 | 0x79, 0x72, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x6d, 0x79, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 119 | 0x68, 0x65, 0x72, 0x2e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 120 | 0x67, 0x52, 0x08, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x42, 0x67, 0x0a, 0x1a, 0x63, 121 | 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x72, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x6d, 0x79, 0x64, 122 | 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x65, 0x72, 0x50, 0x01, 0x5a, 0x2f, 0x67, 0x69, 0x74, 123 | 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x58, 0x72, 0x61, 0x79, 0x52, 0x2d, 0x70, 0x72, 124 | 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x2f, 0x58, 0x72, 0x61, 0x79, 0x52, 0x2f, 0x61, 0x70, 0x70, 0x2f, 125 | 0x6d, 0x79, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x65, 0x72, 0xaa, 0x02, 0x15, 0x58, 126 | 0x72, 0x61, 0x79, 0x52, 0x2e, 0x41, 0x70, 0x70, 0x2e, 0x4d, 0x79, 0x69, 0x73, 0x70, 0x61, 0x74, 127 | 0x63, 0x68, 0x65, 0x72, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 128 | } 129 | 130 | var ( 131 | file_app_mydispatcher_config_proto_rawDescOnce sync.Once 132 | file_app_mydispatcher_config_proto_rawDescData = file_app_mydispatcher_config_proto_rawDesc 133 | ) 134 | 135 | func file_app_mydispatcher_config_proto_rawDescGZIP() []byte { 136 | file_app_mydispatcher_config_proto_rawDescOnce.Do(func() { 137 | file_app_mydispatcher_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_app_mydispatcher_config_proto_rawDescData) 138 | }) 139 | return file_app_mydispatcher_config_proto_rawDescData 140 | } 141 | 142 | var file_app_mydispatcher_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2) 143 | var file_app_mydispatcher_config_proto_goTypes = []interface{}{ 144 | (*SessionConfig)(nil), // 0: xrayr.app.mydispatcher.SessionConfig 145 | (*Config)(nil), // 1: xrayr.app.mydispatcher.Config 146 | } 147 | var file_app_mydispatcher_config_proto_depIdxs = []int32{ 148 | 0, // 0: xrayr.app.mydispatcher.Config.settings:type_name -> xrayr.app.mydispatcher.SessionConfig 149 | 1, // [1:1] is the sub-list for method output_type 150 | 1, // [1:1] is the sub-list for method input_type 151 | 1, // [1:1] is the sub-list for extension type_name 152 | 1, // [1:1] is the sub-list for extension extendee 153 | 0, // [0:1] is the sub-list for field type_name 154 | } 155 | 156 | func init() { file_app_mydispatcher_config_proto_init() } 157 | func file_app_mydispatcher_config_proto_init() { 158 | if File_app_mydispatcher_config_proto != nil { 159 | return 160 | } 161 | if !protoimpl.UnsafeEnabled { 162 | file_app_mydispatcher_config_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 163 | switch v := v.(*SessionConfig); i { 164 | case 0: 165 | return &v.state 166 | case 1: 167 | return &v.sizeCache 168 | case 2: 169 | return &v.unknownFields 170 | default: 171 | return nil 172 | } 173 | } 174 | file_app_mydispatcher_config_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { 175 | switch v := v.(*Config); i { 176 | case 0: 177 | return &v.state 178 | case 1: 179 | return &v.sizeCache 180 | case 2: 181 | return &v.unknownFields 182 | default: 183 | return nil 184 | } 185 | } 186 | } 187 | type x struct{} 188 | out := protoimpl.TypeBuilder{ 189 | File: protoimpl.DescBuilder{ 190 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 191 | RawDescriptor: file_app_mydispatcher_config_proto_rawDesc, 192 | NumEnums: 0, 193 | NumMessages: 2, 194 | NumExtensions: 0, 195 | NumServices: 0, 196 | }, 197 | GoTypes: file_app_mydispatcher_config_proto_goTypes, 198 | DependencyIndexes: file_app_mydispatcher_config_proto_depIdxs, 199 | MessageInfos: file_app_mydispatcher_config_proto_msgTypes, 200 | }.Build() 201 | File_app_mydispatcher_config_proto = out.File 202 | file_app_mydispatcher_config_proto_rawDesc = nil 203 | file_app_mydispatcher_config_proto_goTypes = nil 204 | file_app_mydispatcher_config_proto_depIdxs = nil 205 | } 206 | -------------------------------------------------------------------------------- /app/mydispatcher/config.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package xrayr.app.mydispatcher; 4 | option csharp_namespace = "XrayR.App.Mydispatcher"; 5 | option go_package = "github.com/wyx2685/XrayR/app/mydispatcher"; 6 | option java_package = "com.xrayr.app.mydispatcher"; 7 | option java_multiple_files = true; 8 | 9 | message SessionConfig { 10 | reserved 1; 11 | } 12 | 13 | message Config { 14 | SessionConfig settings = 1; 15 | } 16 | -------------------------------------------------------------------------------- /app/mydispatcher/dispatcher.go: -------------------------------------------------------------------------------- 1 | // Package mydispatcher Package dispatcher implement the rate limiter and the online device counter 2 | package mydispatcher 3 | 4 | //go:generate go run github.com/xtls/xray-core/common/errors/errorgen 5 | -------------------------------------------------------------------------------- /app/mydispatcher/errors.generated.go: -------------------------------------------------------------------------------- 1 | package mydispatcher 2 | 3 | import "github.com/xtls/xray-core/common/errors" 4 | 5 | func newError(values ...interface{}) *errors.Error { 6 | return errors.New(values...) 7 | } 8 | -------------------------------------------------------------------------------- /app/mydispatcher/fakednssniffer.go: -------------------------------------------------------------------------------- 1 | package mydispatcher 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "github.com/xtls/xray-core/common" 8 | "github.com/xtls/xray-core/common/errors" 9 | "github.com/xtls/xray-core/common/net" 10 | "github.com/xtls/xray-core/common/session" 11 | "github.com/xtls/xray-core/core" 12 | "github.com/xtls/xray-core/features/dns" 13 | ) 14 | 15 | // newFakeDNSSniffer Create a Fake DNS metadata sniffer 16 | func newFakeDNSSniffer(ctx context.Context) (protocolSnifferWithMetadata, error) { 17 | var fakeDNSEngine dns.FakeDNSEngine 18 | { 19 | fakeDNSEngineFeat := core.MustFromContext(ctx).GetFeature((*dns.FakeDNSEngine)(nil)) 20 | if fakeDNSEngineFeat != nil { 21 | fakeDNSEngine = fakeDNSEngineFeat.(dns.FakeDNSEngine) 22 | } 23 | } 24 | 25 | if fakeDNSEngine == nil { 26 | errNotInit := newError("FakeDNSEngine is not initialized, but such a sniffer is used").AtError() 27 | return protocolSnifferWithMetadata{}, errNotInit 28 | } 29 | return protocolSnifferWithMetadata{protocolSniffer: func(ctx context.Context, bytes []byte) (SniffResult, error) { 30 | outbounds := session.OutboundsFromContext(ctx) 31 | ob := outbounds[len(outbounds)-1] 32 | Target := ob.Target 33 | if Target.Network == net.Network_TCP || Target.Network == net.Network_UDP { 34 | domainFromFakeDNS := fakeDNSEngine.GetDomainFromFakeDNS(Target.Address) 35 | if domainFromFakeDNS != "" { 36 | errors.LogInfo(ctx, "fake dns got domain: ", domainFromFakeDNS, " for ip: ", ob.Target.Address.String()) 37 | return &fakeDNSSniffResult{domainName: domainFromFakeDNS}, nil 38 | } 39 | } 40 | 41 | if ipAddressInRangeValueI := ctx.Value(ipAddressInRange); ipAddressInRangeValueI != nil { 42 | ipAddressInRangeValue := ipAddressInRangeValueI.(*ipAddressInRangeOpt) 43 | if fkr0, ok := fakeDNSEngine.(dns.FakeDNSEngineRev0); ok { 44 | inPool := fkr0.IsIPInIPPool(ob.Target.Address) 45 | ipAddressInRangeValue.addressInRange = &inPool 46 | } 47 | } 48 | 49 | return nil, common.ErrNoClue 50 | }, metadataSniffer: true}, nil 51 | } 52 | 53 | type fakeDNSSniffResult struct { 54 | domainName string 55 | } 56 | 57 | func (fakeDNSSniffResult) Protocol() string { 58 | return "fakedns" 59 | } 60 | 61 | func (f fakeDNSSniffResult) Domain() string { 62 | return f.domainName 63 | } 64 | 65 | type fakeDNSExtraOpts int 66 | 67 | const ipAddressInRange fakeDNSExtraOpts = 1 68 | 69 | type ipAddressInRangeOpt struct { 70 | addressInRange *bool 71 | } 72 | 73 | type DNSThenOthersSniffResult struct { 74 | domainName string 75 | protocolOriginalName string 76 | } 77 | 78 | func (f DNSThenOthersSniffResult) IsProtoSubsetOf(protocolName string) bool { 79 | return strings.HasPrefix(protocolName, f.protocolOriginalName) 80 | } 81 | 82 | func (DNSThenOthersSniffResult) Protocol() string { 83 | return "fakedns+others" 84 | } 85 | 86 | func (f DNSThenOthersSniffResult) Domain() string { 87 | return f.domainName 88 | } 89 | 90 | func newFakeDNSThenOthers(ctx context.Context, fakeDNSSniffer protocolSnifferWithMetadata, others []protocolSnifferWithMetadata) ( 91 | protocolSnifferWithMetadata, error) { // nolint: unparam 92 | // ctx may be used in the future 93 | _ = ctx 94 | return protocolSnifferWithMetadata{ 95 | protocolSniffer: func(ctx context.Context, bytes []byte) (SniffResult, error) { 96 | ipAddressInRangeValue := &ipAddressInRangeOpt{} 97 | ctx = context.WithValue(ctx, ipAddressInRange, ipAddressInRangeValue) 98 | result, err := fakeDNSSniffer.protocolSniffer(ctx, bytes) 99 | if err == nil { 100 | return result, nil 101 | } 102 | if ipAddressInRangeValue.addressInRange != nil { 103 | if *ipAddressInRangeValue.addressInRange { 104 | for _, v := range others { 105 | if v.metadataSniffer || bytes != nil { 106 | if result, err := v.protocolSniffer(ctx, bytes); err == nil { 107 | return DNSThenOthersSniffResult{domainName: result.Domain(), protocolOriginalName: result.Protocol()}, nil 108 | } 109 | } 110 | } 111 | return nil, common.ErrNoClue 112 | } 113 | errors.LogDebug(ctx, "ip address not in fake dns range, return as is") 114 | return nil, common.ErrNoClue 115 | } 116 | errors.LogWarning(ctx, "fake dns sniffer did not set address in range option, assume false.") 117 | return nil, common.ErrNoClue 118 | }, 119 | metadataSniffer: false, 120 | }, nil 121 | } 122 | -------------------------------------------------------------------------------- /app/mydispatcher/sniffer.go: -------------------------------------------------------------------------------- 1 | package mydispatcher 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/xtls/xray-core/common" 7 | "github.com/xtls/xray-core/common/net" 8 | "github.com/xtls/xray-core/common/protocol/bittorrent" 9 | "github.com/xtls/xray-core/common/protocol/http" 10 | "github.com/xtls/xray-core/common/protocol/quic" 11 | "github.com/xtls/xray-core/common/protocol/tls" 12 | ) 13 | 14 | type SniffResult interface { 15 | Protocol() string 16 | Domain() string 17 | } 18 | 19 | type protocolSniffer func(context.Context, []byte) (SniffResult, error) 20 | 21 | type protocolSnifferWithMetadata struct { 22 | protocolSniffer protocolSniffer 23 | // A Metadata sniffer will be invoked on connection establishment only, with nil body, 24 | // for both TCP and UDP connections 25 | // It will not be shown as a traffic type for routing unless there is no other successful sniffing. 26 | metadataSniffer bool 27 | network net.Network 28 | } 29 | 30 | type Sniffer struct { 31 | sniffer []protocolSnifferWithMetadata 32 | } 33 | 34 | func NewSniffer(ctx context.Context) *Sniffer { 35 | ret := &Sniffer{ 36 | sniffer: []protocolSnifferWithMetadata{ 37 | {func(c context.Context, b []byte) (SniffResult, error) { return http.SniffHTTP(b, ctx) }, false, net.Network_TCP}, 38 | {func(c context.Context, b []byte) (SniffResult, error) { return tls.SniffTLS(b) }, false, net.Network_TCP}, 39 | {func(c context.Context, b []byte) (SniffResult, error) { return bittorrent.SniffBittorrent(b) }, false, net.Network_TCP}, 40 | {func(c context.Context, b []byte) (SniffResult, error) { return quic.SniffQUIC(b) }, false, net.Network_UDP}, 41 | {func(c context.Context, b []byte) (SniffResult, error) { return bittorrent.SniffUTP(b) }, false, net.Network_UDP}, 42 | }, 43 | } 44 | if sniffer, err := newFakeDNSSniffer(ctx); err == nil { 45 | others := ret.sniffer 46 | ret.sniffer = append(ret.sniffer, sniffer) 47 | fakeDNSThenOthers, err := newFakeDNSThenOthers(ctx, sniffer, others) 48 | if err == nil { 49 | ret.sniffer = append([]protocolSnifferWithMetadata{fakeDNSThenOthers}, ret.sniffer...) 50 | } 51 | } 52 | return ret 53 | } 54 | 55 | var errUnknownContent = newError("unknown content") 56 | 57 | func (s *Sniffer) Sniff(c context.Context, payload []byte, network net.Network) (SniffResult, error) { 58 | var pendingSniffer []protocolSnifferWithMetadata 59 | for _, si := range s.sniffer { 60 | s := si.protocolSniffer 61 | if si.metadataSniffer || si.network != network { 62 | continue 63 | } 64 | result, err := s(c, payload) 65 | if err == common.ErrNoClue { 66 | pendingSniffer = append(pendingSniffer, si) 67 | continue 68 | } 69 | 70 | if err == nil && result != nil { 71 | return result, nil 72 | } 73 | } 74 | 75 | if len(pendingSniffer) > 0 { 76 | s.sniffer = pendingSniffer 77 | return nil, common.ErrNoClue 78 | } 79 | 80 | return nil, errUnknownContent 81 | } 82 | 83 | func (s *Sniffer) SniffMetadata(c context.Context) (SniffResult, error) { 84 | var pendingSniffer []protocolSnifferWithMetadata 85 | for _, si := range s.sniffer { 86 | s := si.protocolSniffer 87 | if !si.metadataSniffer { 88 | pendingSniffer = append(pendingSniffer, si) 89 | continue 90 | } 91 | result, err := s(c, nil) 92 | if err == common.ErrNoClue { 93 | pendingSniffer = append(pendingSniffer, si) 94 | continue 95 | } 96 | 97 | if err == nil && result != nil { 98 | return result, nil 99 | } 100 | } 101 | 102 | if len(pendingSniffer) > 0 { 103 | s.sniffer = pendingSniffer 104 | return nil, common.ErrNoClue 105 | } 106 | 107 | return nil, errUnknownContent 108 | } 109 | 110 | func CompositeResult(domainResult SniffResult, protocolResult SniffResult) SniffResult { 111 | return &compositeResult{domainResult: domainResult, protocolResult: protocolResult} 112 | } 113 | 114 | type compositeResult struct { 115 | domainResult SniffResult 116 | protocolResult SniffResult 117 | } 118 | 119 | func (c compositeResult) Protocol() string { 120 | return c.protocolResult.Protocol() 121 | } 122 | 123 | func (c compositeResult) Domain() string { 124 | return c.domainResult.Domain() 125 | } 126 | 127 | func (c compositeResult) ProtocolForDomainResult() string { 128 | return c.domainResult.Protocol() 129 | } 130 | 131 | type SnifferResultComposite interface { 132 | ProtocolForDomainResult() string 133 | } 134 | 135 | type SnifferIsProtoSubsetOf interface { 136 | IsProtoSubsetOf(protocolName string) bool 137 | } 138 | -------------------------------------------------------------------------------- /app/mydispatcher/stats.go: -------------------------------------------------------------------------------- 1 | package mydispatcher 2 | 3 | import ( 4 | "github.com/xtls/xray-core/common" 5 | "github.com/xtls/xray-core/common/buf" 6 | "github.com/xtls/xray-core/features/stats" 7 | ) 8 | 9 | type SizeStatWriter struct { 10 | Counter stats.Counter 11 | Writer buf.Writer 12 | } 13 | 14 | func (w *SizeStatWriter) WriteMultiBuffer(mb buf.MultiBuffer) error { 15 | w.Counter.Add(int64(mb.Len())) 16 | return w.Writer.WriteMultiBuffer(mb) 17 | } 18 | 19 | func (w *SizeStatWriter) Close() error { 20 | return common.Close(w.Writer) 21 | } 22 | 23 | func (w *SizeStatWriter) Interrupt() { 24 | common.Interrupt(w.Writer) 25 | } 26 | -------------------------------------------------------------------------------- /app/mydispatcher/stats_test.go: -------------------------------------------------------------------------------- 1 | package mydispatcher_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/xtls/xray-core/app/dispatcher" 7 | "github.com/xtls/xray-core/common" 8 | "github.com/xtls/xray-core/common/buf" 9 | ) 10 | 11 | type TestCounter int64 12 | 13 | func (c *TestCounter) Value() int64 { 14 | return int64(*c) 15 | } 16 | 17 | func (c *TestCounter) Add(v int64) int64 { 18 | x := int64(*c) + v 19 | *c = TestCounter(x) 20 | return x 21 | } 22 | 23 | func (c *TestCounter) Set(v int64) int64 { 24 | *c = TestCounter(v) 25 | return v 26 | } 27 | 28 | func TestStatsWriter(t *testing.T) { 29 | var c TestCounter 30 | writer := &SizeStatWriter{ 31 | Counter: &c, 32 | Writer: buf.Discard, 33 | } 34 | 35 | mb := buf.MergeBytes(nil, []byte("abcd")) 36 | common.Must(writer.WriteMultiBuffer(mb)) 37 | 38 | mb = buf.MergeBytes(nil, []byte("efg")) 39 | common.Must(writer.WriteMultiBuffer(mb)) 40 | 41 | if c.Value() != 7 { 42 | t.Fatal("unexpected counter value. want 7, but got ", c.Value()) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /cmd/distro/all/all.go: -------------------------------------------------------------------------------- 1 | package all 2 | 3 | import ( 4 | // The following are necessary as they register handlers in their init functions. 5 | 6 | _ "github.com/xtls/xray-core/app/proxyman/inbound" 7 | _ "github.com/xtls/xray-core/app/proxyman/outbound" 8 | 9 | // Required features. Can't remove unless there is replacements. 10 | // _ "github.com/xtls/xray-core/app/dispatcher" 11 | _ "github.com/wyx2685/XrayR/app/mydispatcher" 12 | 13 | // Default commander and all its services. This is an optional feature. 14 | _ "github.com/xtls/xray-core/app/commander" 15 | _ "github.com/xtls/xray-core/app/log/command" 16 | _ "github.com/xtls/xray-core/app/proxyman/command" 17 | _ "github.com/xtls/xray-core/app/stats/command" 18 | 19 | // Other optional features. 20 | _ "github.com/xtls/xray-core/app/dns" 21 | _ "github.com/xtls/xray-core/app/log" 22 | _ "github.com/xtls/xray-core/app/metrics" 23 | _ "github.com/xtls/xray-core/app/policy" 24 | _ "github.com/xtls/xray-core/app/reverse" 25 | _ "github.com/xtls/xray-core/app/router" 26 | _ "github.com/xtls/xray-core/app/stats" 27 | 28 | // Inbound and outbound proxies. 29 | _ "github.com/xtls/xray-core/proxy/blackhole" 30 | _ "github.com/xtls/xray-core/proxy/dns" 31 | _ "github.com/xtls/xray-core/proxy/dokodemo" 32 | _ "github.com/xtls/xray-core/proxy/freedom" 33 | _ "github.com/xtls/xray-core/proxy/http" 34 | _ "github.com/xtls/xray-core/proxy/loopback" 35 | _ "github.com/xtls/xray-core/proxy/shadowsocks" 36 | _ "github.com/xtls/xray-core/proxy/shadowsocks_2022" 37 | _ "github.com/xtls/xray-core/proxy/socks" 38 | _ "github.com/xtls/xray-core/proxy/trojan" 39 | _ "github.com/xtls/xray-core/proxy/vless/inbound" 40 | _ "github.com/xtls/xray-core/proxy/vless/outbound" 41 | _ "github.com/xtls/xray-core/proxy/vmess/inbound" 42 | _ "github.com/xtls/xray-core/proxy/vmess/outbound" 43 | _ "github.com/xtls/xray-core/proxy/wireguard" 44 | 45 | // Transports 46 | _ "github.com/xtls/xray-core/transport/internet/grpc" 47 | _ "github.com/xtls/xray-core/transport/internet/kcp" 48 | _ "github.com/xtls/xray-core/transport/internet/reality" 49 | _ "github.com/xtls/xray-core/transport/internet/splithttp" 50 | _ "github.com/xtls/xray-core/transport/internet/tcp" 51 | _ "github.com/xtls/xray-core/transport/internet/tls" 52 | _ "github.com/xtls/xray-core/transport/internet/udp" 53 | _ "github.com/xtls/xray-core/transport/internet/websocket" 54 | 55 | // Transport headers 56 | _ "github.com/xtls/xray-core/transport/internet/headers/http" 57 | _ "github.com/xtls/xray-core/transport/internet/headers/noop" 58 | _ "github.com/xtls/xray-core/transport/internet/headers/srtp" 59 | _ "github.com/xtls/xray-core/transport/internet/headers/tls" 60 | _ "github.com/xtls/xray-core/transport/internet/headers/utp" 61 | _ "github.com/xtls/xray-core/transport/internet/headers/wechat" 62 | _ "github.com/xtls/xray-core/transport/internet/headers/wireguard" 63 | 64 | // JSON & TOML & YAML 65 | _ "github.com/xtls/xray-core/main/json" 66 | _ "github.com/xtls/xray-core/main/toml" 67 | _ "github.com/xtls/xray-core/main/yaml" 68 | 69 | // Load config from file or http(s) 70 | _ "github.com/xtls/xray-core/main/confloader/external" 71 | 72 | // Commands 73 | _ "github.com/xtls/xray-core/main/commands/all" 74 | ) 75 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/signal" 7 | "path" 8 | "runtime" 9 | "strings" 10 | "syscall" 11 | "time" 12 | 13 | log "github.com/sirupsen/logrus" 14 | 15 | "github.com/fsnotify/fsnotify" 16 | "github.com/spf13/cobra" 17 | "github.com/spf13/viper" 18 | 19 | "github.com/wyx2685/XrayR/panel" 20 | ) 21 | 22 | var ( 23 | cfgFile string 24 | rootCmd = &cobra.Command{ 25 | Use: "XrayR", 26 | Run: func(cmd *cobra.Command, args []string) { 27 | if err := run(); err != nil { 28 | log.Fatal(err) 29 | } 30 | }, 31 | } 32 | ) 33 | 34 | func init() { 35 | rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "Config file for XrayR.") 36 | } 37 | 38 | func getConfig() *viper.Viper { 39 | config := viper.New() 40 | 41 | // Set custom path and name 42 | if cfgFile != "" { 43 | configName := path.Base(cfgFile) 44 | configFileExt := path.Ext(cfgFile) 45 | configNameOnly := strings.TrimSuffix(configName, configFileExt) 46 | configPath := path.Dir(cfgFile) 47 | config.SetConfigName(configNameOnly) 48 | config.SetConfigType(strings.TrimPrefix(configFileExt, ".")) 49 | config.AddConfigPath(configPath) 50 | // Set ASSET Path and Config Path for XrayR 51 | os.Setenv("XRAY_LOCATION_ASSET", configPath) 52 | os.Setenv("XRAY_LOCATION_CONFIG", configPath) 53 | } else { 54 | // Set default config path 55 | config.SetConfigName("config") 56 | config.SetConfigType("yml") 57 | config.AddConfigPath(".") 58 | 59 | } 60 | 61 | if err := config.ReadInConfig(); err != nil { 62 | log.Panicf("Config file error: %s \n", err) 63 | } 64 | 65 | config.WatchConfig() // Watch the config 66 | 67 | return config 68 | } 69 | 70 | func run() error { 71 | showVersion() 72 | 73 | config := getConfig() 74 | panelConfig := &panel.Config{} 75 | if err := config.Unmarshal(panelConfig); err != nil { 76 | return fmt.Errorf("Parse config file %v failed: %s \n", cfgFile, err) 77 | } 78 | 79 | if panelConfig.LogConfig.Level == "debug" { 80 | log.SetReportCaller(true) 81 | } 82 | 83 | p := panel.New(panelConfig) 84 | lastTime := time.Now() 85 | config.OnConfigChange(func(e fsnotify.Event) { 86 | // Discarding event received within a short period of time after receiving an event. 87 | if time.Now().After(lastTime.Add(3 * time.Second)) { 88 | // Hot reload function 89 | fmt.Println("Config file changed:", e.Name) 90 | p.Close() 91 | // Delete old instance and trigger GC 92 | runtime.GC() 93 | if err := config.Unmarshal(panelConfig); err != nil { 94 | log.Panicf("Parse config file %v failed: %s \n", cfgFile, err) 95 | } 96 | 97 | if panelConfig.LogConfig.Level == "debug" { 98 | log.SetReportCaller(true) 99 | } 100 | 101 | p.Start() 102 | lastTime = time.Now() 103 | } 104 | }) 105 | 106 | p.Start() 107 | defer p.Close() 108 | 109 | // Explicitly triggering GC to remove garbage from config loading. 110 | runtime.GC() 111 | // Running backend 112 | osSignals := make(chan os.Signal, 1) 113 | signal.Notify(osSignals, os.Interrupt, os.Kill, syscall.SIGTERM) 114 | <-osSignals 115 | 116 | return nil 117 | } 118 | 119 | func Execute() error { 120 | return rootCmd.Execute() 121 | } 122 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | var ( 10 | version = "0.9.4" 11 | codename = "XrayR" 12 | intro = "A Xray backend that supports many panels" 13 | ) 14 | 15 | func init() { 16 | rootCmd.AddCommand(&cobra.Command{ 17 | Use: "version", 18 | Short: "Print current version of XrayR", 19 | Run: func(cmd *cobra.Command, args []string) { 20 | showVersion() 21 | }, 22 | }) 23 | } 24 | 25 | func showVersion() { 26 | fmt.Printf("%s %s (%s) \n", codename, version, intro) 27 | } 28 | -------------------------------------------------------------------------------- /cmd/x25519.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/base64" 6 | "errors" 7 | "fmt" 8 | 9 | "github.com/spf13/cobra" 10 | "golang.org/x/crypto/curve25519" 11 | ) 12 | 13 | var ( 14 | priKey string 15 | x25519Cmd = &cobra.Command{ 16 | Use: "x25519", 17 | Short: "Generate key pair for x25519 key exchange", 18 | Run: func(cmd *cobra.Command, args []string) { 19 | if err := x25519(); err != nil { 20 | fmt.Println(err) 21 | } 22 | }, 23 | } 24 | ) 25 | 26 | func init() { 27 | x25519Cmd.PersistentFlags().StringVarP(&priKey, "input", "i", "", "Input private key (base64.RawURLEncoding)") 28 | rootCmd.AddCommand(x25519Cmd) 29 | } 30 | 31 | func x25519() error { 32 | privateKey := make([]byte, curve25519.ScalarSize) 33 | 34 | if priKey == "" { 35 | if _, err := rand.Read(privateKey); err != nil { 36 | return err 37 | } 38 | } else { 39 | p, err := base64.RawURLEncoding.DecodeString(priKey) 40 | if err != nil { 41 | return err 42 | } 43 | if len(p) != curve25519.ScalarSize { 44 | return errors.New("invalid private key") 45 | } 46 | privateKey = p 47 | } 48 | 49 | // Modify random bytes using algorithm described at: 50 | // https://cr.yp.to/ecdh.html. 51 | privateKey[0] &= 248 52 | privateKey[31] &= 127 53 | privateKey[31] |= 64 54 | 55 | publicKey, err := curve25519.X25519(privateKey, curve25519.Basepoint) 56 | if err != nil { 57 | return err 58 | } 59 | 60 | output := fmt.Sprintf("Private key: %v\nPublic key: %v", 61 | base64.RawURLEncoding.EncodeToString(privateKey), 62 | base64.RawURLEncoding.EncodeToString(publicKey)) 63 | fmt.Println(output) 64 | 65 | return nil 66 | } 67 | -------------------------------------------------------------------------------- /common/common.go: -------------------------------------------------------------------------------- 1 | // Package common contains common utilities that are shared among other packages. 2 | package common 3 | -------------------------------------------------------------------------------- /common/limiter/limiter.go: -------------------------------------------------------------------------------- 1 | // Package limiter is to control the links that go into the dispatcher 2 | package limiter 3 | 4 | import ( 5 | "context" 6 | "fmt" 7 | "strconv" 8 | "strings" 9 | "sync" 10 | "time" 11 | 12 | "github.com/eko/gocache/lib/v4/cache" 13 | "github.com/eko/gocache/lib/v4/marshaler" 14 | "github.com/eko/gocache/lib/v4/store" 15 | goCacheStore "github.com/eko/gocache/store/go_cache/v4" 16 | redisStore "github.com/eko/gocache/store/redis/v4" 17 | goCache "github.com/patrickmn/go-cache" 18 | "github.com/redis/go-redis/v9" 19 | log "github.com/sirupsen/logrus" 20 | "github.com/wyx2685/XrayR/api" 21 | "golang.org/x/time/rate" 22 | ) 23 | 24 | type UserInfo struct { 25 | UID int 26 | SpeedLimit uint64 27 | DeviceLimit int 28 | } 29 | 30 | type InboundInfo struct { 31 | Tag string 32 | NodeSpeedLimit uint64 33 | UserInfo *sync.Map // Key: Email value: UserInfo 34 | BucketHub *sync.Map // key: Email, value: *rate.Limiter 35 | UserOnlineIP *sync.Map // Key: Email, value: {Key: IP, value: UID} 36 | GlobalLimit struct { 37 | config *GlobalDeviceLimitConfig 38 | globalOnlineIP *marshaler.Marshaler 39 | } 40 | AliveList map[int]int // Key: Uid, value: alive_ip 41 | OldUserOnline *sync.Map // Key: Ip, value: Uid 42 | } 43 | 44 | type Limiter struct { 45 | InboundInfo *sync.Map // Key: Tag, Value: *InboundInfo 46 | } 47 | 48 | func New() *Limiter { 49 | return &Limiter{ 50 | InboundInfo: new(sync.Map), 51 | } 52 | } 53 | 54 | func (l *Limiter) AddInboundLimiter(tag string, nodeSpeedLimit uint64, userList *[]api.UserInfo, globalLimit *GlobalDeviceLimitConfig) error { 55 | inboundInfo := &InboundInfo{ 56 | Tag: tag, 57 | NodeSpeedLimit: nodeSpeedLimit, 58 | BucketHub: new(sync.Map), 59 | UserOnlineIP: new(sync.Map), 60 | OldUserOnline: new(sync.Map), 61 | } 62 | 63 | if globalLimit != nil && globalLimit.Enable { 64 | inboundInfo.GlobalLimit.config = globalLimit 65 | 66 | // init local store 67 | gs := goCacheStore.NewGoCache(goCache.New(time.Duration(globalLimit.Expiry)*time.Second, 1*time.Minute)) 68 | 69 | // init redis store 70 | rs := redisStore.NewRedis(redis.NewClient( 71 | &redis.Options{ 72 | Network: globalLimit.RedisNetwork, 73 | Addr: globalLimit.RedisAddr, 74 | Username: globalLimit.RedisUsername, 75 | Password: globalLimit.RedisPassword, 76 | DB: globalLimit.RedisDB, 77 | }), 78 | store.WithExpiration(time.Duration(globalLimit.Expiry)*time.Second)) 79 | 80 | // init chained cache. First use local go-cache, if go-cache is nil, then use redis cache 81 | cacheManager := cache.NewChain[any]( 82 | cache.New[any](gs), // go-cache is priority 83 | cache.New[any](rs), 84 | ) 85 | inboundInfo.GlobalLimit.globalOnlineIP = marshaler.New(cacheManager) 86 | } 87 | 88 | userMap := new(sync.Map) 89 | for _, u := range *userList { 90 | userMap.Store(fmt.Sprintf("%s|%s|%d", tag, u.Email, u.UID), UserInfo{ 91 | UID: u.UID, 92 | SpeedLimit: u.SpeedLimit, 93 | DeviceLimit: u.DeviceLimit, 94 | }) 95 | } 96 | inboundInfo.UserInfo = userMap 97 | l.InboundInfo.Store(tag, inboundInfo) // Replace the old inbound info 98 | return nil 99 | } 100 | 101 | func (l *Limiter) UpdateInboundLimiter(tag string, updatedUserList *[]api.UserInfo) error { 102 | if value, ok := l.InboundInfo.Load(tag); ok { 103 | inboundInfo := value.(*InboundInfo) 104 | // Update User info 105 | for _, u := range *updatedUserList { 106 | inboundInfo.UserInfo.Store(fmt.Sprintf("%s|%s|%d", tag, u.Email, u.UID), UserInfo{ 107 | UID: u.UID, 108 | SpeedLimit: u.SpeedLimit, 109 | DeviceLimit: u.DeviceLimit, 110 | }) 111 | // Update old limiter bucket 112 | limit := determineRate(inboundInfo.NodeSpeedLimit, u.SpeedLimit) 113 | if limit > 0 { 114 | if bucket, ok := inboundInfo.BucketHub.Load(fmt.Sprintf("%s|%s|%d", tag, u.Email, u.UID)); ok { 115 | limiter := bucket.(*rate.Limiter) 116 | limiter.SetLimit(rate.Limit(limit)) 117 | limiter.SetBurst(int(limit)) 118 | } 119 | } else { 120 | inboundInfo.BucketHub.Delete(fmt.Sprintf("%s|%s|%d", tag, u.Email, u.UID)) 121 | } 122 | } 123 | } else { 124 | return fmt.Errorf("no such inbound in limiter: %s", tag) 125 | } 126 | return nil 127 | } 128 | 129 | func (l *Limiter) DeleteInboundLimiter(tag string) error { 130 | l.InboundInfo.Delete(tag) 131 | return nil 132 | } 133 | 134 | func (l *Limiter) GetOnlineDevice(tag string) (*[]api.OnlineUser, error) { 135 | var onlineUser []api.OnlineUser 136 | 137 | if value, ok := l.InboundInfo.Load(tag); ok { 138 | inboundInfo := value.(*InboundInfo) 139 | // Clear Speed Limiter bucket for users who are not online 140 | inboundInfo.BucketHub.Range(func(key, value interface{}) bool { 141 | email := key.(string) 142 | if _, exists := inboundInfo.UserOnlineIP.Load(email); !exists { 143 | inboundInfo.BucketHub.Delete(email) 144 | } 145 | return true 146 | }) 147 | inboundInfo.UserOnlineIP.Range(func(key, value interface{}) bool { 148 | email := key.(string) 149 | ipMap := value.(*sync.Map) 150 | ipMap.Range(func(key, value interface{}) bool { 151 | uid := value.(int) 152 | ip := key.(string) 153 | inboundInfo.OldUserOnline.Store(ip, uid) 154 | onlineUser = append(onlineUser, api.OnlineUser{UID: uid, IP: ip}) 155 | return true 156 | }) 157 | inboundInfo.UserOnlineIP.Delete(email) // Reset online device 158 | return true 159 | }) 160 | } else { 161 | return nil, fmt.Errorf("no such inbound in limiter: %s", tag) 162 | } 163 | 164 | return &onlineUser, nil 165 | } 166 | 167 | func (l *Limiter) GetUserBucket(tag string, email string, ip string, isSourceTCP bool) (limiter *rate.Limiter, SpeedLimit bool, Reject bool) { 168 | if value, ok := l.InboundInfo.Load(tag); ok { 169 | var ( 170 | userLimit uint64 = 0 171 | deviceLimit, uid int 172 | ) 173 | 174 | inboundInfo := value.(*InboundInfo) 175 | nodeLimit := inboundInfo.NodeSpeedLimit 176 | 177 | if v, ok := inboundInfo.UserInfo.Load(email); ok { 178 | u := v.(UserInfo) 179 | uid = u.UID 180 | userLimit = u.SpeedLimit 181 | deviceLimit = u.DeviceLimit 182 | } 183 | 184 | // Local device limit, only for TCP connection 185 | if isSourceTCP { 186 | ipMap := new(sync.Map) 187 | ipMap.Store(ip, uid) 188 | aliveIp := inboundInfo.AliveList[uid] 189 | // If any device is online 190 | if v, ok := inboundInfo.UserOnlineIP.LoadOrStore(email, ipMap); ok { 191 | ipMap := v.(*sync.Map) 192 | // If this is a new ip 193 | if _, ok := ipMap.LoadOrStore(ip, uid); !ok { 194 | if deviceLimit > 0 { 195 | if deviceLimit <= aliveIp { 196 | ipMap.Delete(ip) 197 | return nil, false, true 198 | } 199 | } 200 | } 201 | } else if v, ok := inboundInfo.OldUserOnline.Load(ip); ok { 202 | if v.(int) == uid { 203 | inboundInfo.OldUserOnline.Delete(ip) 204 | } 205 | } else { 206 | if deviceLimit > 0 { 207 | if deviceLimit <= aliveIp { 208 | inboundInfo.UserOnlineIP.Delete(email) 209 | return nil, false, true 210 | } 211 | } 212 | } 213 | } 214 | 215 | // GlobalLimit 216 | if inboundInfo.GlobalLimit.config != nil && inboundInfo.GlobalLimit.config.Enable { 217 | if reject := globalLimit(inboundInfo, email, uid, ip, deviceLimit); reject { 218 | return nil, false, true 219 | } 220 | } 221 | 222 | // Speed limit 223 | limit := determineRate(nodeLimit, userLimit) // Determine the speed limit rate 224 | if limit > 0 { 225 | limiter := rate.NewLimiter(rate.Limit(limit), int(limit)) // Byte/s 226 | if v, ok := inboundInfo.BucketHub.LoadOrStore(email, limiter); ok { 227 | bucket := v.(*rate.Limiter) 228 | return bucket, true, false 229 | } else { 230 | return limiter, true, false 231 | } 232 | } else { 233 | return nil, false, false 234 | } 235 | } else { 236 | log.Error("Get Inbound Limiter information failed") 237 | return nil, false, false 238 | } 239 | } 240 | 241 | // Global device limit 242 | func globalLimit(inboundInfo *InboundInfo, email string, uid int, ip string, deviceLimit int) bool { 243 | 244 | ctx, cancel := context.WithTimeout(context.Background(), time.Duration(inboundInfo.GlobalLimit.config.Timeout)*time.Second) 245 | defer cancel() 246 | 247 | // reformat email for unique key 248 | uniqueKey := strings.Replace(email, inboundInfo.Tag, strconv.Itoa(deviceLimit), 1) 249 | 250 | v, err := inboundInfo.GlobalLimit.globalOnlineIP.Get(ctx, uniqueKey, new(map[string]int)) 251 | if err != nil { 252 | if _, ok := err.(*store.NotFound); ok { 253 | // If the email is a new device 254 | go pushIP(inboundInfo, uniqueKey, &map[string]int{ip: uid}) 255 | } else { 256 | log.Error("cache service", err) 257 | } 258 | return false 259 | } 260 | 261 | ipMap := v.(*map[string]int) 262 | // Reject device reach limit directly 263 | if deviceLimit > 0 && len(*ipMap) > deviceLimit { 264 | return true 265 | } 266 | 267 | // If the ip is not in cache 268 | if _, ok := (*ipMap)[ip]; !ok { 269 | (*ipMap)[ip] = uid 270 | go pushIP(inboundInfo, uniqueKey, ipMap) 271 | } 272 | 273 | return false 274 | } 275 | 276 | // push the ip to cache 277 | func pushIP(inboundInfo *InboundInfo, uniqueKey string, ipMap *map[string]int) { 278 | ctx, cancel := context.WithTimeout(context.Background(), time.Duration(inboundInfo.GlobalLimit.config.Timeout)*time.Second) 279 | defer cancel() 280 | 281 | if err := inboundInfo.GlobalLimit.globalOnlineIP.Set(ctx, uniqueKey, ipMap); err != nil { 282 | log.Error("cache service", err) 283 | } 284 | } 285 | 286 | // determineRate returns the minimum non-zero rate 287 | func determineRate(nodeLimit, userLimit uint64) (limit uint64) { 288 | if nodeLimit == 0 || userLimit == 0 { 289 | if nodeLimit > userLimit { 290 | return nodeLimit 291 | } else if nodeLimit < userLimit { 292 | return userLimit 293 | } else { 294 | return 0 295 | } 296 | } else { 297 | if nodeLimit > userLimit { 298 | return userLimit 299 | } else if nodeLimit < userLimit { 300 | return nodeLimit 301 | } else { 302 | return nodeLimit 303 | } 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /common/limiter/model.go: -------------------------------------------------------------------------------- 1 | package limiter 2 | 3 | type GlobalDeviceLimitConfig struct { 4 | Enable bool `mapstructure:"Enable"` 5 | RedisNetwork string `mapstructure:"RedisNetwork"` // tcp or unix 6 | RedisAddr string `mapstructure:"RedisAddr"` // host:port, or /path/to/unix.sock 7 | RedisUsername string `mapstructure:"RedisUsername"` 8 | RedisPassword string `mapstructure:"RedisPassword"` 9 | RedisDB int `mapstructure:"RedisDB"` 10 | Timeout int `mapstructure:"Timeout"` 11 | Expiry int `mapstructure:"Expiry"` // second 12 | } 13 | -------------------------------------------------------------------------------- /common/limiter/rate.go: -------------------------------------------------------------------------------- 1 | package limiter 2 | 3 | import ( 4 | "context" 5 | "io" 6 | 7 | "github.com/xtls/xray-core/common" 8 | "github.com/xtls/xray-core/common/buf" 9 | "golang.org/x/time/rate" 10 | ) 11 | 12 | type Writer struct { 13 | writer buf.Writer 14 | limiter *rate.Limiter 15 | w io.Writer 16 | } 17 | 18 | func (l *Limiter) RateWriter(writer buf.Writer, limiter *rate.Limiter) buf.Writer { 19 | return &Writer{ 20 | writer: writer, 21 | limiter: limiter, 22 | } 23 | } 24 | 25 | func (w *Writer) Close() error { 26 | return common.Close(w.writer) 27 | } 28 | 29 | func (w *Writer) WriteMultiBuffer(mb buf.MultiBuffer) error { 30 | ctx := context.Background() 31 | w.limiter.WaitN(ctx, int(mb.Len())) 32 | return w.writer.WriteMultiBuffer(mb) 33 | } 34 | -------------------------------------------------------------------------------- /common/mylego/account.go: -------------------------------------------------------------------------------- 1 | package mylego 2 | 3 | import ( 4 | "crypto" 5 | 6 | "github.com/go-acme/lego/v4/registration" 7 | ) 8 | 9 | // Account represents a users local saved credentials. 10 | type Account struct { 11 | Email string `json:"email"` 12 | Registration *registration.Resource `json:"registration"` 13 | key crypto.PrivateKey 14 | } 15 | 16 | /** Implementation of the registration.User interface **/ 17 | 18 | // GetEmail returns the email address for the account. 19 | func (a *Account) GetEmail() string { 20 | return a.Email 21 | } 22 | 23 | // GetPrivateKey returns the private RSA account key. 24 | func (a *Account) GetPrivateKey() crypto.PrivateKey { 25 | return a.key 26 | } 27 | 28 | // GetRegistration returns the server registration. 29 | func (a *Account) GetRegistration() *registration.Resource { 30 | return a.Registration 31 | } 32 | 33 | /** End **/ 34 | -------------------------------------------------------------------------------- /common/mylego/accounts_storage.go: -------------------------------------------------------------------------------- 1 | package mylego 2 | 3 | import ( 4 | "crypto" 5 | "crypto/x509" 6 | "encoding/json" 7 | "encoding/pem" 8 | "errors" 9 | "net/url" 10 | "os" 11 | "path/filepath" 12 | "strings" 13 | 14 | log "github.com/sirupsen/logrus" 15 | 16 | "github.com/go-acme/lego/v4/certcrypto" 17 | "github.com/go-acme/lego/v4/lego" 18 | "github.com/go-acme/lego/v4/registration" 19 | "golang.org/x/crypto/acme" 20 | ) 21 | 22 | const ( 23 | baseAccountsRootFolderName = "accounts" 24 | baseKeysFolderName = "keys" 25 | accountFileName = "account.json" 26 | ) 27 | 28 | // AccountsStorage A storage for account data. 29 | // 30 | // rootPath: 31 | // 32 | // ./.lego/accounts/ 33 | // │ └── root accounts directory 34 | // └── "path" option 35 | // 36 | // rootUserPath: 37 | // 38 | // ./.lego/accounts/localhost_14000/hubert@hubert.com/ 39 | // │ │ │ └── userID ("email" option) 40 | // │ │ └── CA server ("server" option) 41 | // │ └── root accounts directory 42 | // └── "path" option 43 | // 44 | // keysPath: 45 | // 46 | // ./.lego/accounts/localhost_14000/hubert@hubert.com/keys/ 47 | // │ │ │ │ └── root keys directory 48 | // │ │ │ └── userID ("email" option) 49 | // │ │ └── CA server ("server" option) 50 | // │ └── root accounts directory 51 | // └── "path" option 52 | // 53 | // accountFilePath: 54 | // 55 | // ./.lego/accounts/localhost_14000/hubert@hubert.com/account.json 56 | // │ │ │ │ └── account file 57 | // │ │ │ └── userID ("email" option) 58 | // │ │ └── CA server ("server" option) 59 | // │ └── root accounts directory 60 | // └── "path" option 61 | type AccountsStorage struct { 62 | userID string 63 | rootPath string 64 | rootUserPath string 65 | keysPath string 66 | accountFilePath string 67 | } 68 | 69 | // NewAccountsStorage Creates a new AccountsStorage. 70 | func NewAccountsStorage(l *LegoCMD) *AccountsStorage { 71 | email := l.C.Email 72 | 73 | serverURL, err := url.Parse(acme.LetsEncryptURL) 74 | if err != nil { 75 | log.Panic(err) 76 | } 77 | 78 | rootPath := filepath.Join(l.path, baseAccountsRootFolderName) 79 | serverPath := strings.NewReplacer(":", "_", "/", string(os.PathSeparator)).Replace(serverURL.Host) 80 | accountsPath := filepath.Join(rootPath, serverPath) 81 | rootUserPath := filepath.Join(accountsPath, email) 82 | 83 | return &AccountsStorage{ 84 | userID: email, 85 | rootPath: rootPath, 86 | rootUserPath: rootUserPath, 87 | keysPath: filepath.Join(rootUserPath, baseKeysFolderName), 88 | accountFilePath: filepath.Join(rootUserPath, accountFileName), 89 | } 90 | } 91 | 92 | func (s *AccountsStorage) ExistsAccountFilePath() bool { 93 | accountFile := filepath.Join(s.rootUserPath, accountFileName) 94 | if _, err := os.Stat(accountFile); os.IsNotExist(err) { 95 | return false 96 | } else if err != nil { 97 | log.Panic(err) 98 | } 99 | return true 100 | } 101 | 102 | func (s *AccountsStorage) GetRootPath() string { 103 | return s.rootPath 104 | } 105 | 106 | func (s *AccountsStorage) GetRootUserPath() string { 107 | return s.rootUserPath 108 | } 109 | 110 | func (s *AccountsStorage) GetUserID() string { 111 | return s.userID 112 | } 113 | 114 | func (s *AccountsStorage) Save(account *Account) error { 115 | jsonBytes, err := json.MarshalIndent(account, "", "\t") 116 | if err != nil { 117 | return err 118 | } 119 | 120 | return os.WriteFile(s.accountFilePath, jsonBytes, filePerm) 121 | } 122 | 123 | func (s *AccountsStorage) LoadAccount(privateKey crypto.PrivateKey) *Account { 124 | fileBytes, err := os.ReadFile(s.accountFilePath) 125 | if err != nil { 126 | log.Panicf("Could not load file for account %s: %v", s.userID, err) 127 | } 128 | 129 | var account Account 130 | err = json.Unmarshal(fileBytes, &account) 131 | if err != nil { 132 | log.Panicf("Could not parse file for account %s: %v", s.userID, err) 133 | } 134 | 135 | account.key = privateKey 136 | 137 | if account.Registration == nil || account.Registration.Body.Status == "" { 138 | reg, err := tryRecoverRegistration(privateKey) 139 | if err != nil { 140 | log.Panicf("Could not load account for %s. Registration is nil: %#v", s.userID, err) 141 | } 142 | 143 | account.Registration = reg 144 | err = s.Save(&account) 145 | if err != nil { 146 | log.Panicf("Could not save account for %s. Registration is nil: %#v", s.userID, err) 147 | } 148 | } 149 | 150 | return &account 151 | } 152 | 153 | func (s *AccountsStorage) GetPrivateKey(keyType certcrypto.KeyType) crypto.PrivateKey { 154 | accKeyPath := filepath.Join(s.keysPath, s.userID+".key") 155 | 156 | if _, err := os.Stat(accKeyPath); os.IsNotExist(err) { 157 | log.Printf("No key found for account %s. Generating a %s key.", s.userID, keyType) 158 | s.createKeysFolder() 159 | 160 | privateKey, err := generatePrivateKey(accKeyPath, keyType) 161 | if err != nil { 162 | log.Panicf("Could not generate RSA private account key for account %s: %v", s.userID, err) 163 | } 164 | 165 | log.Printf("Saved key to %s", accKeyPath) 166 | return privateKey 167 | } 168 | 169 | privateKey, err := loadPrivateKey(accKeyPath) 170 | if err != nil { 171 | log.Panicf("Could not load RSA private key from file %s: %v", accKeyPath, err) 172 | } 173 | 174 | return privateKey 175 | } 176 | 177 | func (s *AccountsStorage) createKeysFolder() { 178 | if err := createNonExistingFolder(s.keysPath); err != nil { 179 | log.Panicf("Could not check/create directory for account %s: %v", s.userID, err) 180 | } 181 | } 182 | 183 | func generatePrivateKey(file string, keyType certcrypto.KeyType) (crypto.PrivateKey, error) { 184 | privateKey, err := certcrypto.GeneratePrivateKey(keyType) 185 | if err != nil { 186 | return nil, err 187 | } 188 | 189 | certOut, err := os.Create(file) 190 | if err != nil { 191 | return nil, err 192 | } 193 | defer certOut.Close() 194 | 195 | pemKey := certcrypto.PEMBlock(privateKey) 196 | err = pem.Encode(certOut, pemKey) 197 | if err != nil { 198 | return nil, err 199 | } 200 | 201 | return privateKey, nil 202 | } 203 | 204 | func loadPrivateKey(file string) (crypto.PrivateKey, error) { 205 | keyBytes, err := os.ReadFile(file) 206 | if err != nil { 207 | return nil, err 208 | } 209 | 210 | keyBlock, _ := pem.Decode(keyBytes) 211 | 212 | switch keyBlock.Type { 213 | case "RSA PRIVATE KEY": 214 | return x509.ParsePKCS1PrivateKey(keyBlock.Bytes) 215 | case "EC PRIVATE KEY": 216 | return x509.ParseECPrivateKey(keyBlock.Bytes) 217 | } 218 | 219 | return nil, errors.New("unknown private key type") 220 | } 221 | 222 | func tryRecoverRegistration(privateKey crypto.PrivateKey) (*registration.Resource, error) { 223 | // couldn't load account but got a key. Try to look the account up. 224 | config := lego.NewConfig(&Account{key: privateKey}) 225 | config.CADirURL = acme.LetsEncryptURL 226 | config.UserAgent = "lego-cli/dev" 227 | 228 | client, err := lego.NewClient(config) 229 | if err != nil { 230 | return nil, err 231 | } 232 | 233 | reg, err := client.Registration.ResolveAccountByKey() 234 | if err != nil { 235 | return nil, err 236 | } 237 | return reg, nil 238 | } 239 | -------------------------------------------------------------------------------- /common/mylego/certs_storage.go: -------------------------------------------------------------------------------- 1 | package mylego 2 | 3 | import ( 4 | "bytes" 5 | "crypto/x509" 6 | "encoding/json" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | 11 | log "github.com/sirupsen/logrus" 12 | 13 | "github.com/go-acme/lego/v4/certcrypto" 14 | "github.com/go-acme/lego/v4/certificate" 15 | "golang.org/x/net/idna" 16 | ) 17 | 18 | const ( 19 | baseCertificatesFolderName = "certificates" 20 | ) 21 | 22 | // CertificatesStorage a certificates' storage. 23 | // 24 | // rootPath: 25 | // 26 | // ./.lego/certificates/ 27 | // │ └── root certificates directory 28 | // └── "path" option 29 | // 30 | // archivePath: 31 | // 32 | // ./.lego/archives/ 33 | // │ └── archived certificates directory 34 | // └── "path" option 35 | type CertificatesStorage struct { 36 | rootPath string 37 | pem bool 38 | } 39 | 40 | // NewCertificatesStorage create a new certificates storage. 41 | func NewCertificatesStorage(path string) *CertificatesStorage { 42 | return &CertificatesStorage{ 43 | rootPath: filepath.Join(path, baseCertificatesFolderName), 44 | } 45 | } 46 | 47 | func (s *CertificatesStorage) CreateRootFolder() { 48 | err := createNonExistingFolder(s.rootPath) 49 | if err != nil { 50 | log.Panicf("Could not check/create path: %v", err) 51 | } 52 | } 53 | 54 | func (s *CertificatesStorage) GetRootPath() string { 55 | return s.rootPath 56 | } 57 | 58 | func (s *CertificatesStorage) SaveResource(certRes *certificate.Resource) { 59 | domain := certRes.Domain 60 | 61 | // We store the certificate, private key and metadata in different files 62 | // as web servers would not be able to work with a combined file. 63 | err := s.WriteFile(domain, ".crt", certRes.Certificate) 64 | if err != nil { 65 | log.Panicf("Unable to save Certificate for domain %s\n\t%v", domain, err) 66 | } 67 | 68 | if certRes.IssuerCertificate != nil { 69 | err = s.WriteFile(domain, ".issuer.crt", certRes.IssuerCertificate) 70 | if err != nil { 71 | log.Panicf("Unable to save IssuerCertificate for domain %s\n\t%v", domain, err) 72 | } 73 | } 74 | 75 | if certRes.PrivateKey != nil { 76 | // if we were given a CSR, we don't know the private key 77 | err = s.WriteFile(domain, ".key", certRes.PrivateKey) 78 | if err != nil { 79 | log.Panicf("Unable to save PrivateKey for domain %s\n\t%v", domain, err) 80 | } 81 | 82 | if s.pem { 83 | err = s.WriteFile(domain, ".pem", bytes.Join([][]byte{certRes.Certificate, certRes.PrivateKey}, nil)) 84 | if err != nil { 85 | log.Panicf("Unable to save Certificate and PrivateKey in .pem for domain %s\n\t%v", domain, err) 86 | } 87 | } 88 | } else if s.pem { 89 | // we don't have the private key; can't write the .pem file 90 | log.Panicf("Unable to save pem without private key for domain %s\n\t%v; are you using a CSR?", domain, err) 91 | } 92 | 93 | jsonBytes, err := json.MarshalIndent(certRes, "", "\t") 94 | if err != nil { 95 | log.Panicf("Unable to marshal CertResource for domain %s\n\t%v", domain, err) 96 | } 97 | 98 | err = s.WriteFile(domain, ".json", jsonBytes) 99 | if err != nil { 100 | log.Panicf("Unable to save CertResource for domain %s\n\t%v", domain, err) 101 | } 102 | } 103 | 104 | func (s *CertificatesStorage) ReadResource(domain string) certificate.Resource { 105 | raw, err := s.ReadFile(domain, ".json") 106 | if err != nil { 107 | log.Panicf("Error while loading the meta data for domain %s\n\t%v", domain, err) 108 | } 109 | 110 | var resource certificate.Resource 111 | if err = json.Unmarshal(raw, &resource); err != nil { 112 | log.Panicf("Error while marshaling the meta data for domain %s\n\t%v", domain, err) 113 | } 114 | 115 | return resource 116 | } 117 | 118 | func (s *CertificatesStorage) ExistsFile(domain, extension string) bool { 119 | filePath := s.GetFileName(domain, extension) 120 | 121 | if _, err := os.Stat(filePath); os.IsNotExist(err) { 122 | return false 123 | } else if err != nil { 124 | log.Panic(err) 125 | } 126 | return true 127 | } 128 | 129 | func (s *CertificatesStorage) ReadFile(domain, extension string) ([]byte, error) { 130 | return os.ReadFile(s.GetFileName(domain, extension)) 131 | } 132 | 133 | func (s *CertificatesStorage) GetFileName(domain, extension string) string { 134 | filename := sanitizedDomain(domain) + extension 135 | return filepath.Join(s.rootPath, filename) 136 | } 137 | 138 | func (s *CertificatesStorage) ReadCertificate(domain, extension string) ([]*x509.Certificate, error) { 139 | content, err := s.ReadFile(domain, extension) 140 | if err != nil { 141 | return nil, err 142 | } 143 | 144 | // The input may be a bundle or a single certificate. 145 | return certcrypto.ParsePEMBundle(content) 146 | } 147 | 148 | func (s *CertificatesStorage) WriteFile(domain, extension string, data []byte) error { 149 | var baseFileName = sanitizedDomain(domain) 150 | 151 | filePath := filepath.Join(s.rootPath, baseFileName+extension) 152 | 153 | return os.WriteFile(filePath, data, filePerm) 154 | } 155 | 156 | // sanitizedDomain Make sure no funny chars are in the cert names (like wildcards ;)). 157 | func sanitizedDomain(domain string) string { 158 | safe, err := idna.ToASCII(strings.ReplaceAll(domain, "*", "_")) 159 | if err != nil { 160 | log.Panic(err) 161 | } 162 | return safe 163 | } 164 | -------------------------------------------------------------------------------- /common/mylego/lego_test.go: -------------------------------------------------------------------------------- 1 | package mylego_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/wyx2685/XrayR/common/mylego" 7 | ) 8 | 9 | func TestLegoClient(t *testing.T) { 10 | _, err := mylego.New(&mylego.CertConfig{}) 11 | if err != nil { 12 | t.Error(err) 13 | } 14 | } 15 | 16 | func TestLegoDNSCert(t *testing.T) { 17 | lego, err := mylego.New(&mylego.CertConfig{ 18 | CertDomain: "node1.test.com", 19 | Provider: "alidns", 20 | Email: "test@gmail.com", 21 | DNSEnv: map[string]string{ 22 | "ALICLOUD_ACCESS_KEY": "aaa", 23 | "ALICLOUD_SECRET_KEY": "bbb", 24 | }, 25 | }, 26 | ) 27 | if err != nil { 28 | t.Error(err) 29 | } 30 | 31 | certPath, keyPath, err := lego.DNSCert() 32 | if err != nil { 33 | t.Error(err) 34 | } 35 | t.Log(certPath) 36 | t.Log(keyPath) 37 | } 38 | 39 | func TestLegoHTTPCert(t *testing.T) { 40 | lego, err := mylego.New(&mylego.CertConfig{ 41 | CertMode: "http", 42 | CertDomain: "node1.test.com", 43 | Email: "test@gmail.com", 44 | }) 45 | if err != nil { 46 | t.Error(err) 47 | } 48 | 49 | certPath, keyPath, err := lego.HTTPCert() 50 | if err != nil { 51 | t.Error(err) 52 | } 53 | t.Log(certPath) 54 | t.Log(keyPath) 55 | } 56 | 57 | func TestLegoRenewCert(t *testing.T) { 58 | lego, err := mylego.New(&mylego.CertConfig{ 59 | CertDomain: "node1.test.com", 60 | Email: "test@gmail.com", 61 | Provider: "alidns", 62 | DNSEnv: map[string]string{ 63 | "ALICLOUD_ACCESS_KEY": "aaa", 64 | "ALICLOUD_SECRET_KEY": "bbb", 65 | }, 66 | }) 67 | if err != nil { 68 | t.Error(err) 69 | } 70 | lego.C.CertMode = "http" 71 | certPath, keyPath, ok, err := lego.RenewCert() 72 | if err != nil { 73 | t.Error(err) 74 | } 75 | t.Log(certPath) 76 | t.Log(keyPath) 77 | t.Log(ok) 78 | 79 | lego.C.CertMode = "dns" 80 | certPath, keyPath, ok, err = lego.RenewCert() 81 | if err != nil { 82 | t.Error(err) 83 | } 84 | t.Log(certPath) 85 | t.Log(keyPath) 86 | t.Log(ok) 87 | } 88 | -------------------------------------------------------------------------------- /common/mylego/model.go: -------------------------------------------------------------------------------- 1 | package mylego 2 | 3 | type CertConfig struct { 4 | CertMode string `mapstructure:"CertMode"` // none, file, http, dns 5 | CertDomain string `mapstructure:"CertDomain"` 6 | CertFile string `mapstructure:"CertFile"` 7 | KeyFile string `mapstructure:"KeyFile"` 8 | Provider string `mapstructure:"Provider"` // alidns, cloudflare, gandi, godaddy.... 9 | Email string `mapstructure:"Email"` 10 | DNSEnv map[string]string `mapstructure:"DNSEnv"` 11 | RejectUnknownSni bool `mapstructure:"RejectUnknownSni"` 12 | } 13 | 14 | type LegoCMD struct { 15 | C *CertConfig 16 | path string 17 | } 18 | -------------------------------------------------------------------------------- /common/mylego/mylego.go: -------------------------------------------------------------------------------- 1 | package mylego 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "path" 8 | "path/filepath" 9 | "strings" 10 | ) 11 | 12 | var defaultPath string 13 | 14 | func New(certConf *CertConfig) (*LegoCMD, error) { 15 | // Set default path to configPath/cert 16 | var p = "" 17 | configPath := os.Getenv("XRAY_LOCATION_CONFIG") 18 | if configPath != "" { 19 | p = configPath 20 | } else if cwd, err := os.Getwd(); err == nil { 21 | p = cwd 22 | } else { 23 | p = "." 24 | } 25 | 26 | defaultPath = filepath.Join(p, "cert") 27 | lego := &LegoCMD{ 28 | C: certConf, 29 | path: defaultPath, 30 | } 31 | 32 | return lego, nil 33 | } 34 | 35 | func (l *LegoCMD) getPath() string { 36 | return l.path 37 | } 38 | 39 | func (l *LegoCMD) getCertConfig() *CertConfig { 40 | return l.C 41 | } 42 | 43 | // DNSCert cert a domain using DNS API 44 | func (l *LegoCMD) DNSCert() (CertPath string, KeyPath string, err error) { 45 | defer func() (string, string, error) { 46 | // Handle any error 47 | if r := recover(); r != nil { 48 | switch x := r.(type) { 49 | case string: 50 | err = errors.New(x) 51 | case error: 52 | err = x 53 | default: 54 | err = errors.New("unknown panic") 55 | } 56 | return "", "", err 57 | } 58 | return CertPath, KeyPath, nil 59 | }() 60 | 61 | // Set Env for DNS configuration 62 | for key, value := range l.C.DNSEnv { 63 | os.Setenv(strings.ToUpper(key), value) 64 | } 65 | 66 | // First check if the certificate exists 67 | CertPath, KeyPath, err = checkCertFile(l.C.CertDomain) 68 | if err == nil { 69 | return CertPath, KeyPath, err 70 | } 71 | 72 | err = l.Run() 73 | if err != nil { 74 | return "", "", err 75 | } 76 | CertPath, KeyPath, err = checkCertFile(l.C.CertDomain) 77 | if err != nil { 78 | return "", "", err 79 | } 80 | return CertPath, KeyPath, nil 81 | } 82 | 83 | // HTTPCert cert a domain using http methods 84 | func (l *LegoCMD) HTTPCert() (CertPath string, KeyPath string, err error) { 85 | defer func() (string, string, error) { 86 | // Handle any error 87 | if r := recover(); r != nil { 88 | switch x := r.(type) { 89 | case string: 90 | err = errors.New(x) 91 | case error: 92 | err = x 93 | default: 94 | err = errors.New("unknown panic") 95 | } 96 | return "", "", err 97 | } 98 | return CertPath, KeyPath, nil 99 | }() 100 | 101 | // First check if the certificate exists 102 | CertPath, KeyPath, err = checkCertFile(l.C.CertDomain) 103 | if err == nil { 104 | return CertPath, KeyPath, err 105 | } 106 | 107 | err = l.Run() 108 | if err != nil { 109 | return "", "", err 110 | } 111 | 112 | CertPath, KeyPath, err = checkCertFile(l.C.CertDomain) 113 | if err != nil { 114 | return "", "", err 115 | } 116 | 117 | return CertPath, KeyPath, nil 118 | } 119 | 120 | // RenewCert renew a domain cert 121 | func (l *LegoCMD) RenewCert() (CertPath string, KeyPath string, ok bool, err error) { 122 | defer func() (string, string, bool, error) { 123 | // Handle any error 124 | if r := recover(); r != nil { 125 | switch x := r.(type) { 126 | case string: 127 | err = errors.New(x) 128 | case error: 129 | err = x 130 | default: 131 | err = errors.New("unknown panic") 132 | } 133 | return "", "", false, err 134 | } 135 | return CertPath, KeyPath, ok, nil 136 | }() 137 | 138 | ok, err = l.Renew() 139 | if err != nil { 140 | return 141 | } 142 | 143 | CertPath, KeyPath, err = checkCertFile(l.C.CertDomain) 144 | if err != nil { 145 | return 146 | } 147 | 148 | return 149 | } 150 | 151 | func checkCertFile(domain string) (string, string, error) { 152 | keyPath := path.Join(defaultPath, "certificates", fmt.Sprintf("%s.key", domain)) 153 | certPath := path.Join(defaultPath, "certificates", fmt.Sprintf("%s.crt", domain)) 154 | if _, err := os.Stat(keyPath); os.IsNotExist(err) { 155 | return "", "", fmt.Errorf("cert key failed: %s", domain) 156 | } 157 | if _, err := os.Stat(certPath); os.IsNotExist(err) { 158 | return "", "", fmt.Errorf("cert cert failed: %s", domain) 159 | } 160 | absKeyPath, _ := filepath.Abs(keyPath) 161 | absCertPath, _ := filepath.Abs(certPath) 162 | return absCertPath, absKeyPath, nil 163 | } 164 | -------------------------------------------------------------------------------- /common/mylego/renew.go: -------------------------------------------------------------------------------- 1 | package mylego 2 | 3 | import ( 4 | "crypto" 5 | "crypto/x509" 6 | "time" 7 | 8 | log "github.com/sirupsen/logrus" 9 | 10 | "github.com/go-acme/lego/v4/certcrypto" 11 | "github.com/go-acme/lego/v4/certificate" 12 | "github.com/go-acme/lego/v4/lego" 13 | ) 14 | 15 | func (l *LegoCMD) Renew() (bool, error) { 16 | account, client := setup(NewAccountsStorage(l)) 17 | setupChallenges(l, client) 18 | 19 | if account.Registration == nil { 20 | log.Panicf("Account %s is not registered. Use 'run' to register a new account.\n", account.Email) 21 | } 22 | 23 | return renewForDomains(l.C.CertDomain, client, NewCertificatesStorage(l.path)) 24 | } 25 | 26 | func renewForDomains(domain string, client *lego.Client, certsStorage *CertificatesStorage) (bool, error) { 27 | // load the cert resource from files. 28 | // We store the certificate, private key and metadata in different files 29 | // as web servers would not be able to work with a combined file. 30 | certificates, err := certsStorage.ReadCertificate(domain, ".crt") 31 | if err != nil { 32 | log.Panicf("Error while loading the certificate for domain %s\n\t%v", domain, err) 33 | } 34 | 35 | cert := certificates[0] 36 | 37 | if !needRenewal(cert, domain, 30) { 38 | return false, nil 39 | } 40 | 41 | // This is just meant to be informal for the user. 42 | timeLeft := cert.NotAfter.Sub(time.Now().UTC()) 43 | log.Printf("[%s] acme: Trying renewal with %d hours remaining", domain, int(timeLeft.Hours())) 44 | 45 | certDomains := certcrypto.ExtractDomains(cert) 46 | 47 | var privateKey crypto.PrivateKey 48 | request := certificate.ObtainRequest{ 49 | Domains: certDomains, 50 | Bundle: true, 51 | PrivateKey: privateKey, 52 | } 53 | certRes, err := client.Certificate.Obtain(request) 54 | if err != nil { 55 | log.Panic(err) 56 | } 57 | 58 | certsStorage.SaveResource(certRes) 59 | 60 | return true, nil 61 | } 62 | 63 | func needRenewal(x509Cert *x509.Certificate, domain string, days int) bool { 64 | if x509Cert.IsCA { 65 | log.Panicf("[%s] Certificate bundle starts with a CA certificate", domain) 66 | } 67 | 68 | if days >= 0 { 69 | notAfter := int(time.Until(x509Cert.NotAfter).Hours() / 24.0) 70 | if notAfter > days { 71 | log.Printf("[%s] The certificate expires in %d days, the number of days defined to perform the renewal is %d: no renewal.", 72 | domain, notAfter, days) 73 | return false 74 | } 75 | } 76 | 77 | return true 78 | } 79 | -------------------------------------------------------------------------------- /common/mylego/renew_test.go: -------------------------------------------------------------------------------- 1 | package mylego 2 | 3 | import ( 4 | "crypto/x509" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func Test_merge(t *testing.T) { 12 | testCases := []struct { 13 | desc string 14 | prevDomains []string 15 | nextDomains []string 16 | expected []string 17 | }{ 18 | { 19 | desc: "all empty", 20 | prevDomains: []string{}, 21 | nextDomains: []string{}, 22 | expected: []string{}, 23 | }, 24 | { 25 | desc: "next empty", 26 | prevDomains: []string{"a", "b", "c"}, 27 | nextDomains: []string{}, 28 | expected: []string{"a", "b", "c"}, 29 | }, 30 | { 31 | desc: "prev empty", 32 | prevDomains: []string{}, 33 | nextDomains: []string{"a", "b", "c"}, 34 | expected: []string{"a", "b", "c"}, 35 | }, 36 | { 37 | desc: "merge append", 38 | prevDomains: []string{"a", "b", "c"}, 39 | nextDomains: []string{"a", "c", "d"}, 40 | expected: []string{"a", "b", "c", "d"}, 41 | }, 42 | { 43 | desc: "merge same", 44 | prevDomains: []string{"a", "b", "c"}, 45 | nextDomains: []string{"a", "b", "c"}, 46 | expected: []string{"a", "b", "c"}, 47 | }, 48 | } 49 | 50 | for _, test := range testCases { 51 | test := test 52 | t.Run(test.desc, func(t *testing.T) { 53 | t.Parallel() 54 | 55 | actual := merge(test.prevDomains, test.nextDomains) 56 | assert.Equal(t, test.expected, actual) 57 | }) 58 | } 59 | } 60 | 61 | func Test_needRenewal(t *testing.T) { 62 | testCases := []struct { 63 | desc string 64 | x509Cert *x509.Certificate 65 | days int 66 | expected bool 67 | }{ 68 | { 69 | desc: "30 days, NotAfter now", 70 | x509Cert: &x509.Certificate{ 71 | NotAfter: time.Now(), 72 | }, 73 | days: 30, 74 | expected: true, 75 | }, 76 | { 77 | desc: "30 days, NotAfter 31 days", 78 | x509Cert: &x509.Certificate{ 79 | NotAfter: time.Now().Add(31*24*time.Hour + 1*time.Second), 80 | }, 81 | days: 30, 82 | expected: false, 83 | }, 84 | { 85 | desc: "30 days, NotAfter 30 days", 86 | x509Cert: &x509.Certificate{ 87 | NotAfter: time.Now().Add(30 * 24 * time.Hour), 88 | }, 89 | days: 30, 90 | expected: true, 91 | }, 92 | { 93 | desc: "0 days, NotAfter 30 days: only the day of the expiration", 94 | x509Cert: &x509.Certificate{ 95 | NotAfter: time.Now().Add(30 * 24 * time.Hour), 96 | }, 97 | days: 0, 98 | expected: false, 99 | }, 100 | { 101 | desc: "-1 days, NotAfter 30 days: always renew", 102 | x509Cert: &x509.Certificate{ 103 | NotAfter: time.Now().Add(30 * 24 * time.Hour), 104 | }, 105 | days: -1, 106 | expected: true, 107 | }, 108 | } 109 | 110 | for _, test := range testCases { 111 | test := test 112 | t.Run(test.desc, func(t *testing.T) { 113 | actual := needRenewal(test.x509Cert, "foo.com", test.days) 114 | 115 | assert.Equal(t, test.expected, actual) 116 | }) 117 | } 118 | } 119 | 120 | func merge(prevDomains, nextDomains []string) []string { 121 | for _, next := range nextDomains { 122 | var found bool 123 | for _, prev := range prevDomains { 124 | if prev == next { 125 | found = true 126 | break 127 | } 128 | } 129 | if !found { 130 | prevDomains = append(prevDomains, next) 131 | } 132 | } 133 | return prevDomains 134 | } 135 | -------------------------------------------------------------------------------- /common/mylego/run.go: -------------------------------------------------------------------------------- 1 | package mylego 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/go-acme/lego/v4/certificate" 7 | "github.com/go-acme/lego/v4/lego" 8 | "github.com/go-acme/lego/v4/registration" 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | const rootPathWarningMessage = `!!!! HEADS UP !!!! 13 | 14 | Your account credentials have been saved in your Let's Encrypt 15 | configuration directory at "%s". 16 | 17 | You should make a secure backup of this folder now. This 18 | configuration directory will also contain certificates and 19 | private keys obtained from Let's Encrypt so making regular 20 | backups of this folder is ideal. 21 | ` 22 | 23 | func (l *LegoCMD) Run() error { 24 | accountsStorage := NewAccountsStorage(l) 25 | 26 | account, client := setup(accountsStorage) 27 | setupChallenges(l, client) 28 | 29 | if account.Registration == nil { 30 | reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) 31 | if err != nil { 32 | log.Panicf("Could not complete registration\n\t%v", err) 33 | } 34 | 35 | account.Registration = reg 36 | if err = accountsStorage.Save(account); err != nil { 37 | log.Panic(err) 38 | } 39 | 40 | fmt.Printf(rootPathWarningMessage, accountsStorage.GetRootPath()) 41 | } 42 | 43 | certsStorage := NewCertificatesStorage(l.path) 44 | certsStorage.CreateRootFolder() 45 | 46 | cert, err := obtainCertificate([]string{l.C.CertDomain}, client) 47 | if err != nil { 48 | // Make sure to return a non-zero exit code if ObtainSANCertificate returned at least one error. 49 | // Due to us not returning partial certificate we can just exit here instead of at the end. 50 | log.Panicf("Could not obtain certificates:\n\t%v", err) 51 | } 52 | 53 | certsStorage.SaveResource(cert) 54 | 55 | return nil 56 | } 57 | 58 | func obtainCertificate(domains []string, client *lego.Client) (*certificate.Resource, error) { 59 | if len(domains) > 0 { 60 | // obtain a certificate, generating a new private key 61 | request := certificate.ObtainRequest{ 62 | Domains: domains, 63 | Bundle: true, 64 | } 65 | return client.Certificate.Obtain(request) 66 | } 67 | return nil, fmt.Errorf("not a valid domain") 68 | } 69 | -------------------------------------------------------------------------------- /common/mylego/setup.go: -------------------------------------------------------------------------------- 1 | package mylego 2 | 3 | import ( 4 | "os" 5 | "time" 6 | 7 | log "github.com/sirupsen/logrus" 8 | 9 | "github.com/go-acme/lego/v4/certcrypto" 10 | "github.com/go-acme/lego/v4/challenge/dns01" 11 | "github.com/go-acme/lego/v4/challenge/http01" 12 | "github.com/go-acme/lego/v4/challenge/tlsalpn01" 13 | "github.com/go-acme/lego/v4/lego" 14 | "github.com/go-acme/lego/v4/providers/dns" 15 | "github.com/go-acme/lego/v4/registration" 16 | "golang.org/x/crypto/acme" 17 | ) 18 | 19 | const filePerm os.FileMode = 0o600 20 | 21 | func setup(accountsStorage *AccountsStorage) (*Account, *lego.Client) { 22 | keyType := certcrypto.EC256 23 | privateKey := accountsStorage.GetPrivateKey(keyType) 24 | 25 | var account *Account 26 | if accountsStorage.ExistsAccountFilePath() { 27 | account = accountsStorage.LoadAccount(privateKey) 28 | } else { 29 | account = &Account{Email: accountsStorage.GetUserID(), key: privateKey} 30 | } 31 | 32 | client := newClient(account, keyType) 33 | 34 | return account, client 35 | } 36 | 37 | func newClient(acc registration.User, keyType certcrypto.KeyType) *lego.Client { 38 | config := lego.NewConfig(acc) 39 | config.CADirURL = acme.LetsEncryptURL 40 | 41 | config.Certificate = lego.CertificateConfig{ 42 | KeyType: keyType, 43 | Timeout: 30 * time.Second, 44 | } 45 | config.UserAgent = "lego-cli/dev" 46 | 47 | client, err := lego.NewClient(config) 48 | if err != nil { 49 | log.Panicf("Could not create client: %v", err) 50 | } 51 | 52 | return client 53 | } 54 | 55 | func createNonExistingFolder(path string) error { 56 | if _, err := os.Stat(path); os.IsNotExist(err) { 57 | return os.MkdirAll(path, 0o700) 58 | } else if err != nil { 59 | return err 60 | } 61 | return nil 62 | } 63 | 64 | func setupChallenges(l *LegoCMD, client *lego.Client) { 65 | switch l.C.CertMode { 66 | case "http": 67 | err := client.Challenge.SetHTTP01Provider(http01.NewProviderServer("", "")) 68 | if err != nil { 69 | log.Panic(err) 70 | } 71 | case "tls": 72 | err := client.Challenge.SetTLSALPN01Provider(tlsalpn01.NewProviderServer("", "")) 73 | if err != nil { 74 | log.Panic(err) 75 | } 76 | case "dns": 77 | setupDNS(l.C.Provider, client) 78 | default: 79 | log.Panic("No challenge selected. You must specify at least one challenge: `http`, `tls`, `dns`.") 80 | } 81 | } 82 | 83 | func setupDNS(p string, client *lego.Client) { 84 | provider, err := dns.NewDNSChallengeProviderByName(p) 85 | if err != nil { 86 | log.Panic(err) 87 | } 88 | 89 | err = client.Challenge.SetDNS01Provider( 90 | provider, 91 | dns01.CondOption(true, dns01.AddDNSTimeout(10*time.Second)), 92 | ) 93 | if err != nil { 94 | log.Panic(err) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /common/rule/rule.go: -------------------------------------------------------------------------------- 1 | // Package rule is to control the audit rule behaviors 2 | package rule 3 | 4 | import ( 5 | "fmt" 6 | "reflect" 7 | "strconv" 8 | "strings" 9 | "sync" 10 | 11 | mapset "github.com/deckarep/golang-set" 12 | log "github.com/sirupsen/logrus" 13 | "github.com/wyx2685/XrayR/api" 14 | ) 15 | 16 | type Manager struct { 17 | InboundRule *sync.Map // Key: Tag, Value: []api.DetectRule 18 | InboundDetectResult *sync.Map // key: Tag, Value: mapset.NewSet []api.DetectResult 19 | } 20 | 21 | func New() *Manager { 22 | return &Manager{ 23 | InboundRule: new(sync.Map), 24 | InboundDetectResult: new(sync.Map), 25 | } 26 | } 27 | 28 | func (r *Manager) UpdateRule(tag string, newRuleList []api.DetectRule) error { 29 | if value, ok := r.InboundRule.LoadOrStore(tag, newRuleList); ok { 30 | oldRuleList := value.([]api.DetectRule) 31 | if !reflect.DeepEqual(oldRuleList, newRuleList) { 32 | r.InboundRule.Store(tag, newRuleList) 33 | } 34 | } 35 | return nil 36 | } 37 | 38 | func (r *Manager) GetDetectResult(tag string) (*[]api.DetectResult, error) { 39 | detectResult := make([]api.DetectResult, 0) 40 | if value, ok := r.InboundDetectResult.LoadAndDelete(tag); ok { 41 | resultSet := value.(mapset.Set) 42 | it := resultSet.Iterator() 43 | for result := range it.C { 44 | detectResult = append(detectResult, result.(api.DetectResult)) 45 | } 46 | } 47 | return &detectResult, nil 48 | } 49 | 50 | func (r *Manager) Detect(tag string, destination string, email string) (reject bool) { 51 | reject = false 52 | var hitRuleID = -1 53 | // If we have some rule for this inbound 54 | if value, ok := r.InboundRule.Load(tag); ok { 55 | ruleList := value.([]api.DetectRule) 56 | for _, r := range ruleList { 57 | if r.Pattern.Match([]byte(destination)) { 58 | hitRuleID = r.ID 59 | reject = true 60 | break 61 | } 62 | } 63 | // If we hit some rule 64 | if reject && hitRuleID != -1 { 65 | l := strings.Split(email, "|") 66 | uid, err := strconv.Atoi(l[len(l)-1]) 67 | if err != nil { 68 | log.Debug(fmt.Sprintf("Record illegal behavior failed! Cannot find user's uid: %s", email)) 69 | return reject 70 | } 71 | newSet := mapset.NewSetWith(api.DetectResult{UID: uid, RuleID: hitRuleID}) 72 | // If there are any hit history 73 | if v, ok := r.InboundDetectResult.LoadOrStore(tag, newSet); ok { 74 | resultSet := v.(mapset.Set) 75 | // If this is a new record 76 | if resultSet.Add(api.DetectResult{UID: uid, RuleID: hitRuleID}) { 77 | r.InboundDetectResult.Store(tag, resultSet) 78 | } 79 | } 80 | } 81 | } 82 | return reject 83 | } 84 | -------------------------------------------------------------------------------- /common/serverstatus/serverstatus.go: -------------------------------------------------------------------------------- 1 | // Package serverstatus generate the server system status 2 | package serverstatus 3 | 4 | import ( 5 | "fmt" 6 | 7 | "github.com/shirou/gopsutil/v3/cpu" 8 | "github.com/shirou/gopsutil/v3/disk" 9 | "github.com/shirou/gopsutil/v3/host" 10 | "github.com/shirou/gopsutil/v3/mem" 11 | ) 12 | 13 | // GetSystemInfo get the system info of a given periodic 14 | func GetSystemInfo() (Cpu float64, Mem float64, Disk float64, Uptime uint64, err error) { 15 | 16 | errorString := "" 17 | 18 | cpuPercent, err := cpu.Percent(0, false) 19 | // Check if cpuPercent is empty 20 | if len(cpuPercent) > 0 && err == nil { 21 | Cpu = cpuPercent[0] 22 | } else { 23 | Cpu = 0 24 | errorString += fmt.Sprintf("get cpu usage failed: %s ", err) 25 | } 26 | 27 | memUsage, err := mem.VirtualMemory() 28 | if err != nil { 29 | errorString += fmt.Sprintf("get mem usage failed: %s ", err) 30 | } else { 31 | Mem = memUsage.UsedPercent 32 | } 33 | 34 | diskUsage, err := disk.Usage("/") 35 | if err != nil { 36 | errorString += fmt.Sprintf("get disk usage failed: %s ", err) 37 | } else { 38 | Disk = diskUsage.UsedPercent 39 | } 40 | 41 | uptime, err := host.Uptime() 42 | if err != nil { 43 | errorString += fmt.Sprintf("get uptime failed: %s ", err) 44 | } else { 45 | Uptime = uptime 46 | } 47 | 48 | if errorString != "" { 49 | err = fmt.Errorf(errorString) 50 | } 51 | 52 | return Cpu, Mem, Disk, Uptime, err 53 | } 54 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | log "github.com/sirupsen/logrus" 5 | "github.com/wyx2685/XrayR/cmd" 6 | ) 7 | 8 | func main() { 9 | if err := cmd.Execute(); err != nil { 10 | log.Fatal(err) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /panel/config.go: -------------------------------------------------------------------------------- 1 | package panel 2 | 3 | import ( 4 | "github.com/wyx2685/XrayR/api" 5 | "github.com/wyx2685/XrayR/service/controller" 6 | ) 7 | 8 | type Config struct { 9 | LogConfig *LogConfig `mapstructure:"Log"` 10 | DnsConfigPath string `mapstructure:"DnsConfigPath"` 11 | InboundConfigPath string `mapstructure:"InboundConfigPath"` 12 | OutboundConfigPath string `mapstructure:"OutboundConfigPath"` 13 | RouteConfigPath string `mapstructure:"RouteConfigPath"` 14 | ConnectionConfig *ConnectionConfig `mapstructure:"ConnectionConfig"` 15 | NodesConfig []*NodesConfig `mapstructure:"Nodes"` 16 | } 17 | 18 | type NodesConfig struct { 19 | PanelType string `mapstructure:"PanelType"` 20 | ApiConfig *api.Config `mapstructure:"ApiConfig"` 21 | ControllerConfig *controller.Config `mapstructure:"ControllerConfig"` 22 | } 23 | 24 | type LogConfig struct { 25 | Level string `mapstructure:"Level"` 26 | AccessPath string `mapstructure:"AccessPath"` 27 | ErrorPath string `mapstructure:"ErrorPath"` 28 | } 29 | 30 | type ConnectionConfig struct { 31 | Handshake uint32 `mapstructure:"handshake"` 32 | ConnIdle uint32 `mapstructure:"connIdle"` 33 | UplinkOnly uint32 `mapstructure:"uplinkOnly"` 34 | DownlinkOnly uint32 `mapstructure:"downlinkOnly"` 35 | BufferSize int32 `mapstructure:"bufferSize"` 36 | } 37 | -------------------------------------------------------------------------------- /panel/defaultConfig.go: -------------------------------------------------------------------------------- 1 | package panel 2 | 3 | import "github.com/wyx2685/XrayR/service/controller" 4 | 5 | func getDefaultLogConfig() *LogConfig { 6 | return &LogConfig{ 7 | Level: "none", 8 | AccessPath: "", 9 | ErrorPath: "", 10 | } 11 | } 12 | 13 | func getDefaultConnectionConfig() *ConnectionConfig { 14 | return &ConnectionConfig{ 15 | Handshake: 4, 16 | ConnIdle: 30, 17 | UplinkOnly: 2, 18 | DownlinkOnly: 4, 19 | BufferSize: 64, 20 | } 21 | } 22 | 23 | func getDefaultControllerConfig() *controller.Config { 24 | return &controller.Config{ 25 | ListenIP: "0.0.0.0", 26 | SendIP: "0.0.0.0", 27 | UpdatePeriodic: 60, 28 | DNSType: "AsIs", 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /panel/panel.go: -------------------------------------------------------------------------------- 1 | package panel 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "os" 7 | "sync" 8 | 9 | "github.com/wyx2685/XrayR/api/bunpanel" 10 | "github.com/wyx2685/XrayR/api/gov2panel" 11 | "github.com/wyx2685/XrayR/api/newV2board" 12 | "github.com/wyx2685/XrayR/app/mydispatcher" 13 | 14 | "dario.cat/mergo" 15 | "github.com/r3labs/diff/v2" 16 | "github.com/xtls/xray-core/app/proxyman" 17 | "github.com/xtls/xray-core/app/stats" 18 | "github.com/xtls/xray-core/common/serial" 19 | "github.com/xtls/xray-core/core" 20 | "github.com/xtls/xray-core/infra/conf" 21 | 22 | "github.com/wyx2685/XrayR/api" 23 | "github.com/wyx2685/XrayR/api/pmpanel" 24 | "github.com/wyx2685/XrayR/api/proxypanel" 25 | "github.com/wyx2685/XrayR/api/sspanel" 26 | "github.com/wyx2685/XrayR/api/v2raysocks" 27 | _ "github.com/wyx2685/XrayR/cmd/distro/all" 28 | "github.com/wyx2685/XrayR/service" 29 | "github.com/wyx2685/XrayR/service/controller" 30 | ) 31 | 32 | // Panel Structure 33 | type Panel struct { 34 | access sync.Mutex 35 | panelConfig *Config 36 | Server *core.Instance 37 | Service []service.Service 38 | Running bool 39 | } 40 | 41 | func New(panelConfig *Config) *Panel { 42 | p := &Panel{panelConfig: panelConfig} 43 | return p 44 | } 45 | 46 | func (p *Panel) loadCore(panelConfig *Config) *core.Instance { 47 | // Log Config 48 | coreLogConfig := &conf.LogConfig{} 49 | logConfig := getDefaultLogConfig() 50 | if panelConfig.LogConfig != nil { 51 | if _, err := diff.Merge(logConfig, panelConfig.LogConfig, logConfig); err != nil { 52 | log.Panicf("Read Log config failed: %s", err) 53 | } 54 | } 55 | coreLogConfig.LogLevel = logConfig.Level 56 | coreLogConfig.AccessLog = logConfig.AccessPath 57 | coreLogConfig.ErrorLog = logConfig.ErrorPath 58 | 59 | // DNS config 60 | coreDnsConfig := &conf.DNSConfig{} 61 | if panelConfig.DnsConfigPath != "" { 62 | if data, err := os.ReadFile(panelConfig.DnsConfigPath); err != nil { 63 | log.Panicf("Failed to read DNS config file at: %s", panelConfig.DnsConfigPath) 64 | } else { 65 | if err = json.Unmarshal(data, coreDnsConfig); err != nil { 66 | log.Panicf("Failed to unmarshal DNS config: %s", panelConfig.DnsConfigPath) 67 | } 68 | } 69 | } 70 | 71 | // init controller's DNS config 72 | // for _, config := range p.panelConfig.NodesConfig { 73 | // config.ControllerConfig.DNSConfig = coreDnsConfig 74 | // } 75 | 76 | dnsConfig, err := coreDnsConfig.Build() 77 | if err != nil { 78 | log.Panicf("Failed to understand DNS config, Please check: https://xtls.github.io/config/dns.html for help: %s", err) 79 | } 80 | 81 | // Routing config 82 | coreRouterConfig := &conf.RouterConfig{} 83 | if panelConfig.RouteConfigPath != "" { 84 | if data, err := os.ReadFile(panelConfig.RouteConfigPath); err != nil { 85 | log.Panicf("Failed to read Routing config file at: %s", panelConfig.RouteConfigPath) 86 | } else { 87 | if err = json.Unmarshal(data, coreRouterConfig); err != nil { 88 | log.Panicf("Failed to unmarshal Routing config: %s", panelConfig.RouteConfigPath) 89 | } 90 | } 91 | } 92 | routeConfig, err := coreRouterConfig.Build() 93 | if err != nil { 94 | log.Panicf("Failed to understand Routing config Please check: https://xtls.github.io/config/routing.html for help: %s", err) 95 | } 96 | // Custom Inbound config 97 | var coreCustomInboundConfig []conf.InboundDetourConfig 98 | if panelConfig.InboundConfigPath != "" { 99 | if data, err := os.ReadFile(panelConfig.InboundConfigPath); err != nil { 100 | log.Panicf("Failed to read Custom Inbound config file at: %s", panelConfig.InboundConfigPath) 101 | } else { 102 | if err = json.Unmarshal(data, &coreCustomInboundConfig); err != nil { 103 | log.Panicf("Failed to unmarshal Custom Inbound config: %s", panelConfig.InboundConfigPath) 104 | } 105 | } 106 | } 107 | var inBoundConfig []*core.InboundHandlerConfig 108 | for _, config := range coreCustomInboundConfig { 109 | oc, err := config.Build() 110 | if err != nil { 111 | log.Panicf("Failed to understand Inbound config, Please check: https://xtls.github.io/config/inbound.html for help: %s", err) 112 | } 113 | inBoundConfig = append(inBoundConfig, oc) 114 | } 115 | // Custom Outbound config 116 | var coreCustomOutboundConfig []conf.OutboundDetourConfig 117 | if panelConfig.OutboundConfigPath != "" { 118 | if data, err := os.ReadFile(panelConfig.OutboundConfigPath); err != nil { 119 | log.Panicf("Failed to read Custom Outbound config file at: %s", panelConfig.OutboundConfigPath) 120 | } else { 121 | if err = json.Unmarshal(data, &coreCustomOutboundConfig); err != nil { 122 | log.Panicf("Failed to unmarshal Custom Outbound config: %s", panelConfig.OutboundConfigPath) 123 | } 124 | } 125 | } 126 | var outBoundConfig []*core.OutboundHandlerConfig 127 | for _, config := range coreCustomOutboundConfig { 128 | oc, err := config.Build() 129 | if err != nil { 130 | log.Panicf("Failed to understand Outbound config, Please check: https://xtls.github.io/config/outbound.html for help: %s", err) 131 | } 132 | outBoundConfig = append(outBoundConfig, oc) 133 | } 134 | // Policy config 135 | levelPolicyConfig := parseConnectionConfig(panelConfig.ConnectionConfig) 136 | corePolicyConfig := &conf.PolicyConfig{} 137 | corePolicyConfig.Levels = map[uint32]*conf.Policy{0: levelPolicyConfig} 138 | policyConfig, _ := corePolicyConfig.Build() 139 | // Build Core Config 140 | config := &core.Config{ 141 | App: []*serial.TypedMessage{ 142 | serial.ToTypedMessage(coreLogConfig.Build()), 143 | serial.ToTypedMessage(&mydispatcher.Config{}), 144 | serial.ToTypedMessage(&stats.Config{}), 145 | serial.ToTypedMessage(&proxyman.InboundConfig{}), 146 | serial.ToTypedMessage(&proxyman.OutboundConfig{}), 147 | serial.ToTypedMessage(policyConfig), 148 | serial.ToTypedMessage(dnsConfig), 149 | serial.ToTypedMessage(routeConfig), 150 | }, 151 | Inbound: inBoundConfig, 152 | Outbound: outBoundConfig, 153 | } 154 | server, err := core.New(config) 155 | if err != nil { 156 | log.Panicf("failed to create instance: %s", err) 157 | } 158 | log.Printf("Xray Core Version: %s", core.Version()) 159 | 160 | return server 161 | } 162 | 163 | // Start the panel 164 | func (p *Panel) Start() { 165 | p.access.Lock() 166 | defer p.access.Unlock() 167 | log.Print("Start the panel..") 168 | // Load Core 169 | server := p.loadCore(p.panelConfig) 170 | if err := server.Start(); err != nil { 171 | log.Panicf("Failed to start instance: %s", err) 172 | } 173 | p.Server = server 174 | 175 | // Load Nodes config 176 | for _, nodeConfig := range p.panelConfig.NodesConfig { 177 | var apiClient api.API 178 | switch nodeConfig.PanelType { 179 | case "SSpanel": 180 | apiClient = sspanel.New(nodeConfig.ApiConfig) 181 | case "NewV2board", "V2board": 182 | apiClient = newV2board.New(nodeConfig.ApiConfig) 183 | case "PMpanel": 184 | apiClient = pmpanel.New(nodeConfig.ApiConfig) 185 | case "Proxypanel": 186 | apiClient = proxypanel.New(nodeConfig.ApiConfig) 187 | case "V2RaySocks": 188 | apiClient = v2raysocks.New(nodeConfig.ApiConfig) 189 | case "GoV2Panel": 190 | apiClient = gov2panel.New(nodeConfig.ApiConfig) 191 | case "BunPanel": 192 | apiClient = bunpanel.New(nodeConfig.ApiConfig) 193 | default: 194 | log.Panicf("Unsupport panel type: %s", nodeConfig.PanelType) 195 | } 196 | var controllerService service.Service 197 | // Register controller service 198 | controllerConfig := getDefaultControllerConfig() 199 | if nodeConfig.ControllerConfig != nil { 200 | if err := mergo.Merge(controllerConfig, nodeConfig.ControllerConfig, mergo.WithOverride); err != nil { 201 | log.Panicf("Read Controller Config Failed") 202 | } 203 | } 204 | controllerService = controller.New(server, apiClient, controllerConfig, nodeConfig.PanelType) 205 | p.Service = append(p.Service, controllerService) 206 | 207 | } 208 | 209 | // Start all the service 210 | for _, s := range p.Service { 211 | err := s.Start() 212 | if err != nil { 213 | log.Panicf("Panel Start failed: %s", err) 214 | } 215 | } 216 | p.Running = true 217 | return 218 | } 219 | 220 | // Close the panel 221 | func (p *Panel) Close() { 222 | p.access.Lock() 223 | defer p.access.Unlock() 224 | for _, s := range p.Service { 225 | err := s.Close() 226 | if err != nil { 227 | log.Panicf("Panel Close failed: %s", err) 228 | } 229 | } 230 | p.Service = nil 231 | p.Server.Close() 232 | p.Running = false 233 | return 234 | } 235 | 236 | func parseConnectionConfig(c *ConnectionConfig) (policy *conf.Policy) { 237 | connectionConfig := getDefaultConnectionConfig() 238 | if c != nil { 239 | if _, err := diff.Merge(connectionConfig, c, connectionConfig); err != nil { 240 | log.Panicf("Read ConnectionConfig failed: %s", err) 241 | } 242 | } 243 | policy = &conf.Policy{ 244 | StatsUserUplink: true, 245 | StatsUserDownlink: true, 246 | Handshake: &connectionConfig.Handshake, 247 | ConnectionIdle: &connectionConfig.ConnIdle, 248 | UplinkOnly: &connectionConfig.UplinkOnly, 249 | DownlinkOnly: &connectionConfig.DownlinkOnly, 250 | BufferSize: &connectionConfig.BufferSize, 251 | } 252 | 253 | return 254 | } 255 | -------------------------------------------------------------------------------- /release/config/config.yml.example: -------------------------------------------------------------------------------- 1 | Log: 2 | Level: warning # Log level: none, error, warning, info, debug 3 | AccessPath: # /etc/XrayR/access.Log 4 | ErrorPath: # /etc/XrayR/error.log 5 | DnsConfigPath: # /etc/XrayR/dns.json # Path to dns config, check https://xtls.github.io/config/dns.html for help 6 | RouteConfigPath: /etc/XrayR/route.json # Path to route config, check https://xtls.github.io/config/routing.html for help 7 | InboundConfigPath: # /etc/XrayR/custom_inbound.json # Path to custom inbound config, check https://xtls.github.io/config/inbound.html for help 8 | OutboundConfigPath: /etc/XrayR/custom_outbound.json # Path to custom outbound config, check https://xtls.github.io/config/outbound.html for help 9 | ConnectionConfig: 10 | Handshake: 4 # Handshake time limit, Second 11 | ConnIdle: 30 # Connection idle time limit, Second 12 | UplinkOnly: 2 # Time limit when the connection downstream is closed, Second 13 | DownlinkOnly: 4 # Time limit when the connection is closed after the uplink is closed, Second 14 | BufferSize: 64 # The internal cache size of each connection, kB 15 | Nodes: 16 | - PanelType: "NewV2board" # Panel type: SSpanel, NewV2board, PMpanel, Proxypanel, V2RaySocks, GoV2Panel 17 | ApiConfig: 18 | ApiHost: "http://127.0.0.1:667" 19 | ApiKey: "123" 20 | NodeID: 41 21 | NodeType: V2ray # Node type: V2ray, Vmess, Vless, Shadowsocks, Trojan, Shadowsocks-Plugin 22 | Timeout: 30 # Timeout for the api request 23 | EnableVless: true # Enable Vless for V2ray Type 24 | SpeedLimit: 0 # Mbps, Local settings will replace remote settings, 0 means disable 25 | DeviceLimit: 0 # Local settings will replace remote settings, 0 means disable 26 | RuleListPath: # /etc/XrayR/rulelist Path to local rulelist file 27 | DisableCustomConfig: false # disable custom config for sspanel 28 | ControllerConfig: 29 | ListenIP: 0.0.0.0 # IP address you want to listen 30 | SendIP: 0.0.0.0 # IP address you want to send pacakage 31 | UpdatePeriodic: 60 # Time to update the nodeinfo, how many sec. 32 | DeviceOnlineMinTraffic: 100 # V2board面板设备数限制统计阈值,大于此流量时上报设备数在线,单位kB,不填则默认上报 33 | EnableDNS: false # Use custom DNS config, Please ensure that you set the dns.json well 34 | DNSType: AsIs # AsIs, UseIP, UseIPv4, UseIPv6, DNS strategy 35 | EnableProxyProtocol: false # Only works for WebSocket and TCP 36 | AutoSpeedLimitConfig: 37 | Limit: 0 # Warned speed. Set to 0 to disable AutoSpeedLimit (mbps) 38 | WarnTimes: 0 # After (WarnTimes) consecutive warnings, the user will be limited. Set to 0 to punish overspeed user immediately. 39 | LimitSpeed: 0 # The speedlimit of a limited user (unit: mbps) 40 | LimitDuration: 0 # How many minutes will the limiting last (unit: minute) 41 | GlobalDeviceLimitConfig: 42 | Enable: false # Enable the global device limit of a user 43 | RedisNetwork: tcp # Redis protocol, tcp or unix 44 | RedisAddr: 127.0.0.1:6379 # Redis server address, or unix socket path 45 | RedisUsername: # Redis username 46 | RedisPassword: YOUR PASSWORD # Redis password 47 | RedisDB: 0 # Redis DB 48 | Timeout: 5 # Timeout for redis request 49 | Expiry: 60 # Expiry time (second) 50 | EnableFallback: false # Only support for Trojan and Vless 51 | FallBackConfigs: # Support multiple fallbacks 52 | - SNI: # TLS SNI(Server Name Indication), Empty for any 53 | Alpn: # Alpn, Empty for any 54 | Path: # HTTP PATH, Empty for any 55 | Dest: 80 # Required, Destination of fallback, check https://xtls.github.io/config/features/fallback.html for details. 56 | ProxyProtocolVer: 0 # Send PROXY protocol version, 0 for disable 57 | EnableREALITY: false # 是否开启 REALITY 58 | DisableLocalREALITYConfig: false # 是否忽略本地 REALITY 配置 59 | REALITYConfigs: # 本地 REALITY 配置 60 | Show: false # Show REALITY debug 61 | Dest: m.media-amazon.com:443 # REALITY 目标地址 62 | ProxyProtocolVer: 0 # Send PROXY protocol version, 0 for disable 63 | ServerNames: # Required, list of available serverNames for the client, * wildcard is not supported at the moment. 64 | - m.media-amazon.com 65 | PrivateKey: # 可不填 66 | MinClientVer: # Optional, minimum version of Xray client, format is x.y.z. 67 | MaxClientVer: # Optional, maximum version of Xray client, format is x.y.z. 68 | MaxTimeDiff: 0 # Optional, maximum allowed time difference, unit is in milliseconds. 69 | ShortIds: # 可不填 70 | - "" 71 | CertConfig: 72 | CertMode: none # Option about how to get certificate: none, file, http, tls, dns. Choose "none" will forcedly disable the tls config. 73 | CertDomain: "node1.test.com" # Domain to cert 74 | CertFile: /etc/XrayR/cert/node1.test.com.cert # Provided if the CertMode is file 75 | KeyFile: /etc/XrayR/cert/node1.test.com.key 76 | Provider: alidns # DNS cert provider, Get the full support list here: https://go-acme.github.io/lego/dns/ 77 | Email: test@me.com 78 | DNSEnv: # DNS ENV option used by DNS provider 79 | ALICLOUD_ACCESS_KEY: aaa 80 | ALICLOUD_SECRET_KEY: bbb 81 | 82 | # - PanelType: "SSpanel" # Panel type: SSpanel, V2board, NewV2board, PMpanel, Proxypanel, V2RaySocks, GoV2Panel 83 | # ApiConfig: 84 | # ApiHost: "http://127.0.0.1:668" 85 | # ApiKey: "123" 86 | # NodeID: 41 87 | # NodeType: V2ray # Node type: V2ray, Shadowsocks, Trojan, Shadowsocks-Plugin 88 | # Timeout: 30 # Timeout for the api request 89 | # EnableVless: false # Enable Vless for V2ray Type 90 | # VlessFlow: "xtls-rprx-vision" # Only support vless 91 | # SpeedLimit: 0 # Mbps, Local settings will replace remote settings, 0 means disable 92 | # DeviceLimit: 0 # Local settings will replace remote settings, 0 means disable 93 | # RuleListPath: # /etc/XrayR/rulelist Path to local rulelist file 94 | # ControllerConfig: 95 | # ListenIP: 0.0.0.0 # IP address you want to listen 96 | # SendIP: 0.0.0.0 # IP address you want to send pacakage 97 | # UpdatePeriodic: 60 # Time to update the nodeinfo, how many sec. 98 | # EnableDNS: false # Use custom DNS config, Please ensure that you set the dns.json well 99 | # DNSType: AsIs # AsIs, UseIP, UseIPv4, UseIPv6, DNS strategy 100 | # EnableProxyProtocol: false # Only works for WebSocket and TCP 101 | # AutoSpeedLimitConfig: 102 | # Limit: 0 # Warned speed. Set to 0 to disable AutoSpeedLimit (mbps) 103 | # WarnTimes: 0 # After (WarnTimes) consecutive warnings, the user will be limited. Set to 0 to punish overspeed user immediately. 104 | # LimitSpeed: 0 # The speedlimit of a limited user (unit: mbps) 105 | # LimitDuration: 0 # How many minutes will the limiting last (unit: minute) 106 | # GlobalDeviceLimitConfig: 107 | # Enable: false # Enable the global device limit of a user 108 | # RedisAddr: 127.0.0.1:6379 # The redis server address 109 | # RedisPassword: YOUR PASSWORD # Redis password 110 | # RedisDB: 0 # Redis DB 111 | # Timeout: 5 # Timeout for redis request 112 | # Expiry: 60 # Expiry time (second) 113 | # EnableFallback: false # Only support for Trojan and Vless 114 | # FallBackConfigs: # Support multiple fallbacks 115 | # - SNI: # TLS SNI(Server Name Indication), Empty for any 116 | # Alpn: # Alpn, Empty for any 117 | # Path: # HTTP PATH, Empty for any 118 | # Dest: 80 # Required, Destination of fallback, check https://xtls.github.io/config/features/fallback.html for details. 119 | # ProxyProtocolVer: 0 # Send PROXY protocol version, 0 for disable 120 | # EnableREALITY: true # Enable REALITY 121 | # REALITYConfigs: 122 | # Show: true # Show REALITY debug 123 | # Dest: www.amazon.com:443 # Required, Same as fallback 124 | # ProxyProtocolVer: 0 # Send PROXY protocol version, 0 for disable 125 | # ServerNames: # Required, list of available serverNames for the client, * wildcard is not supported at the moment. 126 | # - www.amazon.com 127 | # PrivateKey: YOUR_PRIVATE_KEY # Required, execute './XrayR x25519' to generate. 128 | # MinClientVer: # Optional, minimum version of Xray client, format is x.y.z. 129 | # MaxClientVer: # Optional, maximum version of Xray client, format is x.y.z. 130 | # MaxTimeDiff: 0 # Optional, maximum allowed time difference, unit is in milliseconds. 131 | # ShortIds: # Required, list of available shortIds for the client, can be used to differentiate between different clients. 132 | # - "" 133 | # - 0123456789abcdef 134 | # CertConfig: 135 | # CertMode: dns # Option about how to get certificate: none, file, http, tls, dns. Choose "none" will forcedly disable the tls config. 136 | # CertDomain: "node1.test.com" # Domain to cert 137 | # CertFile: /etc/XrayR/cert/node1.test.com.cert # Provided if the CertMode is file 138 | # KeyFile: /etc/XrayR/cert/node1.test.com.key 139 | # Provider: alidns # DNS cert provider, Get the full support list here: https://go-acme.github.io/lego/dns/ 140 | # Email: test@me.com 141 | # DNSEnv: # DNS ENV option used by DNS provider 142 | # ALICLOUD_ACCESS_KEY: aaa 143 | # ALICLOUD_SECRET_KEY: bbb 144 | -------------------------------------------------------------------------------- /release/config/custom_inbound.json: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /release/config/custom_outbound.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "tag": "IPv4_out", 4 | "protocol": "freedom", 5 | "settings": { 6 | "domainStrategy": "UseIPv4v6" 7 | } 8 | }, 9 | { 10 | "tag": "IPv6_out", 11 | "protocol": "freedom", 12 | "settings": { 13 | "domainStrategy": "UseIPv6" 14 | } 15 | }, 16 | { 17 | "protocol": "wireguard", 18 | "tag": "warp", 19 | "settings": { 20 | "secretKey": "2DRzSLT1OBh+mN4fxZc+gu+hfYb8X4a9d0oD5NCb60Q=", 21 | "address":[ 22 | "172.16.0.2/32", 23 | "2606:4700:110:8e15:9080:66bd:20e8:c4e7/128" 24 | ], 25 | "mtu": 1380, 26 | "reserved": [45, 85, 21], 27 | "peers": [ 28 | { 29 | "publicKey": "bmXOC+F1FxEMF9dyiK2H5/1SUtzH0JuVo51h2wPfgyo=", 30 | "endpoint": "engage.cloudflareclient.com:2408", 31 | "keepAlive": 25 32 | } 33 | ] 34 | } 35 | }, 36 | { 37 | "protocol": "blackhole", 38 | "tag": "block" 39 | } 40 | ] 41 | -------------------------------------------------------------------------------- /release/config/dns.json: -------------------------------------------------------------------------------- 1 | { 2 | "servers": [ 3 | "1.1.1.1", 4 | "8.8.8.8", 5 | "localhost" 6 | ], 7 | "tag": "dns_inbound" 8 | } -------------------------------------------------------------------------------- /release/config/geoip.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wyx2685/XrayR/068dc075ca1ab03a722380d2cec5ad38bb3ed357/release/config/geoip.dat -------------------------------------------------------------------------------- /release/config/geosite.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wyx2685/XrayR/068dc075ca1ab03a722380d2cec5ad38bb3ed357/release/config/geosite.dat -------------------------------------------------------------------------------- /release/config/route.json: -------------------------------------------------------------------------------- 1 | { 2 | "domainStrategy": "IPIfNonMatch", 3 | "rules": [ 4 | { 5 | "outboundTag": "block", 6 | "ip": [ 7 | "geoip:private" 8 | ] 9 | }, 10 | { 11 | "outboundTag": "block", 12 | "protocol": [ 13 | "bittorrent" 14 | ] 15 | }, 16 | { 17 | "domain": [ 18 | "geosite:google" 19 | ], 20 | "outboundTag": "IPv4_out" 21 | }, 22 | { 23 | "domain": [ 24 | "geosite:cn" 25 | ], 26 | "outboundTag": "warp" 27 | }, 28 | { 29 | "ip": [ 30 | "geoip:cn" 31 | ], 32 | "outboundTag": "warp" 33 | }, 34 | { 35 | "outboundTag": "IPv4_out", 36 | "network": "udp,tcp" 37 | } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /release/config/rulelist: -------------------------------------------------------------------------------- 1 | (.+\.|^)(360|so)\.(cn|com) 2 | baidu.com 3 | google.com -------------------------------------------------------------------------------- /service/controller/config.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/wyx2685/XrayR/common/limiter" 5 | "github.com/wyx2685/XrayR/common/mylego" 6 | ) 7 | 8 | type Config struct { 9 | ListenIP string `mapstructure:"ListenIP"` 10 | SendIP string `mapstructure:"SendIP"` 11 | UpdatePeriodic int `mapstructure:"UpdatePeriodic"` 12 | DeviceOnlineMinTraffic int `mapstructure:"DeviceOnlineMinTraffic"` 13 | CertConfig *mylego.CertConfig `mapstructure:"CertConfig"` 14 | EnableDNS bool `mapstructure:"EnableDNS"` 15 | DNSType string `mapstructure:"DNSType"` 16 | DisableUploadTraffic bool `mapstructure:"DisableUploadTraffic"` 17 | DisableGetRule bool `mapstructure:"DisableGetRule"` 18 | EnableProxyProtocol bool `mapstructure:"EnableProxyProtocol"` 19 | EnableFallback bool `mapstructure:"EnableFallback"` 20 | DisableIVCheck bool `mapstructure:"DisableIVCheck"` 21 | DisableSniffing bool `mapstructure:"DisableSniffing"` 22 | AutoSpeedLimitConfig *AutoSpeedLimitConfig `mapstructure:"AutoSpeedLimitConfig"` 23 | GlobalDeviceLimitConfig *limiter.GlobalDeviceLimitConfig `mapstructure:"GlobalDeviceLimitConfig"` 24 | FallBackConfigs []*FallBackConfig `mapstructure:"FallBackConfigs"` 25 | DisableLocalREALITYConfig bool `mapstructure:"DisableLocalREALITYConfig"` 26 | EnableREALITY bool `mapstructure:"EnableREALITY"` 27 | REALITYConfigs *REALITYConfig `mapstructure:"REALITYConfigs"` 28 | } 29 | 30 | type AutoSpeedLimitConfig struct { 31 | Limit int `mapstructure:"Limit"` // mbps 32 | WarnTimes int `mapstructure:"WarnTimes"` 33 | LimitSpeed int `mapstructure:"LimitSpeed"` // mbps 34 | LimitDuration int `mapstructure:"LimitDuration"` // minute 35 | } 36 | 37 | type FallBackConfig struct { 38 | SNI string `mapstructure:"SNI"` 39 | Alpn string `mapstructure:"Alpn"` 40 | Path string `mapstructure:"Path"` 41 | Dest string `mapstructure:"Dest"` 42 | ProxyProtocolVer uint64 `mapstructure:"ProxyProtocolVer"` 43 | } 44 | 45 | type REALITYConfig struct { 46 | Show bool `mapstructure:"Show"` 47 | Dest string `mapstructure:"Dest"` 48 | ProxyProtocolVer uint64 `mapstructure:"ProxyProtocolVer"` 49 | ServerNames []string `mapstructure:"ServerNames"` 50 | PrivateKey string `mapstructure:"PrivateKey"` 51 | MinClientVer string `mapstructure:"MinClientVer"` 52 | MaxClientVer string `mapstructure:"MaxClientVer"` 53 | MaxTimeDiff uint64 `mapstructure:"MaxTimeDiff"` 54 | ShortIds []string `mapstructure:"ShortIds"` 55 | } 56 | -------------------------------------------------------------------------------- /service/controller/control.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/xtls/xray-core/common/protocol" 8 | "github.com/xtls/xray-core/core" 9 | "github.com/xtls/xray-core/features/inbound" 10 | "github.com/xtls/xray-core/features/outbound" 11 | "github.com/xtls/xray-core/features/stats" 12 | "github.com/xtls/xray-core/proxy" 13 | 14 | "github.com/wyx2685/XrayR/api" 15 | "github.com/wyx2685/XrayR/common/limiter" 16 | ) 17 | 18 | func (c *Controller) removeInbound(tag string) error { 19 | err := c.ibm.RemoveHandler(context.Background(), tag) 20 | return err 21 | } 22 | 23 | func (c *Controller) removeOutbound(tag string) error { 24 | err := c.obm.RemoveHandler(context.Background(), tag) 25 | return err 26 | } 27 | 28 | func (c *Controller) addInbound(config *core.InboundHandlerConfig) error { 29 | rawHandler, err := core.CreateObject(c.server, config) 30 | if err != nil { 31 | return err 32 | } 33 | handler, ok := rawHandler.(inbound.Handler) 34 | if !ok { 35 | return fmt.Errorf("not an InboundHandler: %s", err) 36 | } 37 | if err := c.ibm.AddHandler(context.Background(), handler); err != nil { 38 | return err 39 | } 40 | return nil 41 | } 42 | 43 | func (c *Controller) addOutbound(config *core.OutboundHandlerConfig) error { 44 | rawHandler, err := core.CreateObject(c.server, config) 45 | if err != nil { 46 | return err 47 | } 48 | handler, ok := rawHandler.(outbound.Handler) 49 | if !ok { 50 | return fmt.Errorf("not an InboundHandler: %s", err) 51 | } 52 | if err := c.obm.AddHandler(context.Background(), handler); err != nil { 53 | return err 54 | } 55 | return nil 56 | } 57 | 58 | func (c *Controller) addUsers(users []*protocol.User, tag string) error { 59 | handler, err := c.ibm.GetHandler(context.Background(), tag) 60 | if err != nil { 61 | return fmt.Errorf("no such inbound tag: %s", err) 62 | } 63 | inboundInstance, ok := handler.(proxy.GetInbound) 64 | if !ok { 65 | return fmt.Errorf("handler %s has not implemented proxy.GetInbound", tag) 66 | } 67 | 68 | userManager, ok := inboundInstance.GetInbound().(proxy.UserManager) 69 | if !ok { 70 | return fmt.Errorf("handler %s has not implemented proxy.UserManager", tag) 71 | } 72 | for _, item := range users { 73 | mUser, err := item.ToMemoryUser() 74 | if err != nil { 75 | return err 76 | } 77 | err = userManager.AddUser(context.Background(), mUser) 78 | if err != nil { 79 | return err 80 | } 81 | } 82 | return nil 83 | } 84 | 85 | func (c *Controller) removeUsers(users []string, tag string) error { 86 | handler, err := c.ibm.GetHandler(context.Background(), tag) 87 | if err != nil { 88 | return fmt.Errorf("no such inbound tag: %s", err) 89 | } 90 | inboundInstance, ok := handler.(proxy.GetInbound) 91 | if !ok { 92 | return fmt.Errorf("handler %s is not implement proxy.GetInbound", tag) 93 | } 94 | 95 | userManager, ok := inboundInstance.GetInbound().(proxy.UserManager) 96 | if !ok { 97 | return fmt.Errorf("handler %s is not implement proxy.UserManager", err) 98 | } 99 | for _, email := range users { 100 | err = userManager.RemoveUser(context.Background(), email) 101 | if err != nil { 102 | return err 103 | } 104 | } 105 | return nil 106 | } 107 | 108 | func (c *Controller) getTraffic(email string) (up int64, down int64, upCounter stats.Counter, downCounter stats.Counter) { 109 | upName := "user>>>" + email + ">>>traffic>>>uplink" 110 | downName := "user>>>" + email + ">>>traffic>>>downlink" 111 | upCounter = c.stm.GetCounter(upName) 112 | downCounter = c.stm.GetCounter(downName) 113 | if upCounter != nil && upCounter.Value() != 0 { 114 | up = upCounter.Value() 115 | } else { 116 | upCounter = nil 117 | } 118 | if downCounter != nil && downCounter.Value() != 0 { 119 | down = downCounter.Value() 120 | } else { 121 | downCounter = nil 122 | } 123 | return up, down, upCounter, downCounter 124 | } 125 | 126 | func (c *Controller) resetTraffic(upCounterList *[]stats.Counter, downCounterList *[]stats.Counter) { 127 | for _, upCounter := range *upCounterList { 128 | upCounter.Set(0) 129 | } 130 | for _, downCounter := range *downCounterList { 131 | downCounter.Set(0) 132 | } 133 | } 134 | 135 | func (c *Controller) AddInboundLimiter(tag string, nodeSpeedLimit uint64, userList *[]api.UserInfo, globalDeviceLimitConfig *limiter.GlobalDeviceLimitConfig) error { 136 | err := c.dispatcher.Limiter.AddInboundLimiter(tag, nodeSpeedLimit, userList, globalDeviceLimitConfig) 137 | return err 138 | } 139 | 140 | func (c *Controller) UpdateInboundLimiter(tag string, updatedUserList *[]api.UserInfo) error { 141 | err := c.dispatcher.Limiter.UpdateInboundLimiter(tag, updatedUserList) 142 | return err 143 | } 144 | 145 | func (c *Controller) DeleteInboundLimiter(tag string) error { 146 | err := c.dispatcher.Limiter.DeleteInboundLimiter(tag) 147 | return err 148 | } 149 | 150 | func (c *Controller) GetOnlineDevice(tag string) (*[]api.OnlineUser, error) { 151 | return c.dispatcher.Limiter.GetOnlineDevice(tag) 152 | } 153 | 154 | func (c *Controller) UpdateRule(tag string, newRuleList []api.DetectRule) error { 155 | err := c.dispatcher.RuleManager.UpdateRule(tag, newRuleList) 156 | return err 157 | } 158 | 159 | func (c *Controller) GetDetectResult(tag string) (*[]api.DetectResult, error) { 160 | return c.dispatcher.RuleManager.GetDetectResult(tag) 161 | } 162 | -------------------------------------------------------------------------------- /service/controller/controller_test.go: -------------------------------------------------------------------------------- 1 | package controller_test 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/signal" 7 | "runtime" 8 | "syscall" 9 | "testing" 10 | 11 | "github.com/xtls/xray-core/core" 12 | "github.com/xtls/xray-core/infra/conf" 13 | 14 | "github.com/wyx2685/XrayR/api" 15 | "github.com/wyx2685/XrayR/api/sspanel" 16 | _ "github.com/wyx2685/XrayR/cmd/distro/all" 17 | "github.com/wyx2685/XrayR/common/mylego" 18 | . "github.com/wyx2685/XrayR/service/controller" 19 | ) 20 | 21 | func TestController(t *testing.T) { 22 | serverConfig := &conf.Config{ 23 | Stats: &conf.StatsConfig{}, 24 | LogConfig: &conf.LogConfig{LogLevel: "debug"}, 25 | } 26 | policyConfig := &conf.PolicyConfig{} 27 | policyConfig.Levels = map[uint32]*conf.Policy{0: { 28 | StatsUserUplink: true, 29 | StatsUserDownlink: true, 30 | }} 31 | serverConfig.Policy = policyConfig 32 | config, _ := serverConfig.Build() 33 | 34 | // config := &core.Config{ 35 | // App: []*serial.TypedMessage{ 36 | // serial.ToTypedMessage(&dispatcher.Config{}), 37 | // serial.ToTypedMessage(&proxyman.InboundConfig{}), 38 | // serial.ToTypedMessage(&proxyman.OutboundConfig{}), 39 | // serial.ToTypedMessage(&stats.Config{}), 40 | // }} 41 | 42 | server, err := core.New(config) 43 | defer server.Close() 44 | if err != nil { 45 | t.Errorf("failed to create instance: %s", err) 46 | } 47 | if err = server.Start(); err != nil { 48 | t.Errorf("Failed to start instance: %s", err) 49 | } 50 | certConfig := &mylego.CertConfig{ 51 | CertMode: "http", 52 | CertDomain: "test.ss.tk", 53 | Provider: "alidns", 54 | Email: "ss@ss.com", 55 | } 56 | controlerConfig := &Config{ 57 | UpdatePeriodic: 5, 58 | CertConfig: certConfig, 59 | } 60 | apiConfig := &api.Config{ 61 | APIHost: "http://127.0.0.1:667", 62 | Key: "123", 63 | NodeID: 41, 64 | NodeType: "V2ray", 65 | } 66 | apiClient := sspanel.New(apiConfig) 67 | c := New(server, apiClient, controlerConfig, "SSpanel") 68 | fmt.Println("Sleep 1s") 69 | err = c.Start() 70 | if err != nil { 71 | t.Error(err) 72 | } 73 | // Explicitly triggering GC to remove garbage from config loading. 74 | runtime.GC() 75 | 76 | { 77 | osSignals := make(chan os.Signal, 1) 78 | signal.Notify(osSignals, os.Interrupt, os.Kill, syscall.SIGTERM) 79 | <-osSignals 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /service/controller/errors.generated.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import "github.com/xtls/xray-core/common/errors" 4 | 5 | func newError(values ...interface{}) *errors.Error { 6 | return errors.New(values...) 7 | } 8 | -------------------------------------------------------------------------------- /service/controller/inboundbuilder.go: -------------------------------------------------------------------------------- 1 | // Package controller Package generate the InboundConfig used by add inbound 2 | package controller 3 | 4 | import ( 5 | "crypto/rand" 6 | "encoding/base64" 7 | "encoding/hex" 8 | "encoding/json" 9 | "fmt" 10 | "strings" 11 | 12 | "github.com/sagernet/sing-shadowsocks/shadowaead_2022" 13 | C "github.com/sagernet/sing/common" 14 | "github.com/xtls/xray-core/common/net" 15 | "github.com/xtls/xray-core/core" 16 | "github.com/xtls/xray-core/infra/conf" 17 | 18 | "github.com/wyx2685/XrayR/api" 19 | "github.com/wyx2685/XrayR/common/mylego" 20 | ) 21 | 22 | // InboundBuilder build Inbound config for different protocol 23 | func InboundBuilder(config *Config, nodeInfo *api.NodeInfo, tag string) (*core.InboundHandlerConfig, error) { 24 | inboundDetourConfig := &conf.InboundDetourConfig{} 25 | // Build Listen IP address 26 | if nodeInfo.NodeType == "Shadowsocks-Plugin" { 27 | // Shdowsocks listen in 127.0.0.1 for safety 28 | inboundDetourConfig.ListenOn = &conf.Address{Address: net.ParseAddress("127.0.0.1")} 29 | } else if config.ListenIP != "" { 30 | ipAddress := net.ParseAddress(config.ListenIP) 31 | inboundDetourConfig.ListenOn = &conf.Address{Address: ipAddress} 32 | } 33 | 34 | // Build Port 35 | portList := &conf.PortList{ 36 | Range: []conf.PortRange{{From: nodeInfo.Port, To: nodeInfo.Port}}, 37 | } 38 | inboundDetourConfig.PortList = portList 39 | // Build Tag 40 | inboundDetourConfig.Tag = tag 41 | // SniffingConfig 42 | sniffingConfig := &conf.SniffingConfig{ 43 | Enabled: true, 44 | DestOverride: &conf.StringList{"http", "tls", "quic", "fakedns"}, 45 | } 46 | if config.DisableSniffing { 47 | sniffingConfig.Enabled = false 48 | } 49 | inboundDetourConfig.SniffingConfig = sniffingConfig 50 | 51 | var ( 52 | protocol string 53 | streamSetting *conf.StreamConfig 54 | setting json.RawMessage 55 | ) 56 | 57 | var proxySetting any 58 | // Build Protocol and Protocol setting 59 | switch nodeInfo.NodeType { 60 | case "V2ray", "Vmess", "Vless": 61 | if (nodeInfo.NodeType == "V2ray" && nodeInfo.EnableVless) || nodeInfo.NodeType == "Vless" { 62 | protocol = "vless" 63 | // Enable fallback 64 | if config.EnableFallback { 65 | fallbackConfigs, err := buildVlessFallbacks(config.FallBackConfigs) 66 | if err == nil { 67 | proxySetting = &conf.VLessInboundConfig{ 68 | Decryption: "none", 69 | Fallbacks: fallbackConfigs, 70 | } 71 | } else { 72 | return nil, err 73 | } 74 | } else { 75 | proxySetting = &conf.VLessInboundConfig{ 76 | Decryption: "none", 77 | } 78 | } 79 | } else { 80 | protocol = "vmess" 81 | proxySetting = &conf.VMessInboundConfig{} 82 | } 83 | case "Trojan": 84 | protocol = "trojan" 85 | // Enable fallback 86 | if config.EnableFallback { 87 | fallbackConfigs, err := buildTrojanFallbacks(config.FallBackConfigs) 88 | if err == nil { 89 | proxySetting = &conf.TrojanServerConfig{ 90 | Fallbacks: fallbackConfigs, 91 | } 92 | } else { 93 | return nil, err 94 | } 95 | } else { 96 | proxySetting = &conf.TrojanServerConfig{} 97 | } 98 | case "Shadowsocks", "Shadowsocks-Plugin": 99 | protocol = "shadowsocks" 100 | cipher := strings.ToLower(nodeInfo.CypherMethod) 101 | 102 | proxySetting = &conf.ShadowsocksServerConfig{ 103 | Cipher: cipher, 104 | Password: nodeInfo.ServerKey, // shadowsocks2022 shareKey 105 | } 106 | 107 | proxySetting, _ := proxySetting.(*conf.ShadowsocksServerConfig) 108 | // shadowsocks must have a random password 109 | // shadowsocks2022's password == user PSK, thus should a length of string >= 32 and base64 encoder 110 | b := make([]byte, 32) 111 | rand.Read(b) 112 | randPasswd := hex.EncodeToString(b) 113 | if C.Contains(shadowaead_2022.List, cipher) { 114 | proxySetting.Users = append(proxySetting.Users, &conf.ShadowsocksUserConfig{ 115 | Password: base64.StdEncoding.EncodeToString(b), 116 | }) 117 | } else { 118 | proxySetting.Password = randPasswd 119 | } 120 | 121 | proxySetting.NetworkList = &conf.NetworkList{"tcp", "udp"} 122 | proxySetting.IVCheck = true 123 | if config.DisableIVCheck { 124 | proxySetting.IVCheck = false 125 | } 126 | 127 | case "dokodemo-door": 128 | protocol = "dokodemo-door" 129 | proxySetting = struct { 130 | Host string `json:"address"` 131 | NetworkList []string `json:"network"` 132 | }{ 133 | Host: "v1.mux.cool", 134 | NetworkList: []string{"tcp", "udp"}, 135 | } 136 | default: 137 | return nil, fmt.Errorf("unsupported node type: %s, Only support: V2ray, Trojan, Shadowsocks, and Shadowsocks-Plugin", nodeInfo.NodeType) 138 | } 139 | 140 | setting, err := json.Marshal(proxySetting) 141 | if err != nil { 142 | return nil, fmt.Errorf("marshal proxy %s config failed: %s", nodeInfo.NodeType, err) 143 | } 144 | inboundDetourConfig.Protocol = protocol 145 | inboundDetourConfig.Settings = &setting 146 | 147 | // Build streamSettings 148 | streamSetting = new(conf.StreamConfig) 149 | transportProtocol := conf.TransportProtocol(nodeInfo.TransportProtocol) 150 | networkType, err := transportProtocol.Build() 151 | if err != nil { 152 | return nil, fmt.Errorf("convert TransportProtocol failed: %s", err) 153 | } 154 | 155 | switch networkType { 156 | case "tcp": 157 | tcpSetting := &conf.TCPConfig{ 158 | AcceptProxyProtocol: config.EnableProxyProtocol, 159 | HeaderConfig: nodeInfo.Header, 160 | } 161 | streamSetting.TCPSettings = tcpSetting 162 | case "websocket": 163 | headers := make(map[string]string) 164 | headers["Host"] = nodeInfo.Host 165 | wsSettings := &conf.WebSocketConfig{ 166 | AcceptProxyProtocol: config.EnableProxyProtocol, 167 | Host: nodeInfo.Host, 168 | Path: nodeInfo.Path, 169 | Headers: headers, 170 | } 171 | streamSetting.WSSettings = wsSettings 172 | case "grpc": 173 | grpcSettings := &conf.GRPCConfig{ 174 | ServiceName: nodeInfo.ServiceName, 175 | Authority: nodeInfo.Authority, 176 | } 177 | streamSetting.GRPCSettings = grpcSettings 178 | case "httpupgrade": 179 | httpupgradeSettings := &conf.HttpUpgradeConfig{ 180 | Headers: nodeInfo.Headers, 181 | Path: nodeInfo.Path, 182 | Host: nodeInfo.Host, 183 | AcceptProxyProtocol: nodeInfo.AcceptProxyProtocol, 184 | } 185 | streamSetting.HTTPUPGRADESettings = httpupgradeSettings 186 | case "splithttp", "xhttp": 187 | splithttpSetting := &conf.SplitHTTPConfig{ 188 | Path: nodeInfo.Path, 189 | Host: nodeInfo.Host, 190 | } 191 | streamSetting.SplitHTTPSettings = splithttpSetting 192 | } 193 | streamSetting.Network = &transportProtocol 194 | 195 | // Build TLS and REALITY settings 196 | var isREALITY bool 197 | if config.DisableLocalREALITYConfig { 198 | if nodeInfo.REALITYConfig != nil && nodeInfo.EnableREALITY { 199 | isREALITY = true 200 | streamSetting.Security = "reality" 201 | 202 | r := nodeInfo.REALITYConfig 203 | streamSetting.REALITYSettings = &conf.REALITYConfig{ 204 | Show: config.REALITYConfigs.Show, 205 | Dest: []byte(`"` + r.Dest + `"`), 206 | Xver: r.ProxyProtocolVer, 207 | ServerNames: r.ServerNames, 208 | PrivateKey: r.PrivateKey, 209 | MinClientVer: r.MinClientVer, 210 | MaxClientVer: r.MaxClientVer, 211 | MaxTimeDiff: r.MaxTimeDiff, 212 | ShortIds: r.ShortIds, 213 | } 214 | } 215 | } else if config.EnableREALITY && config.REALITYConfigs != nil { 216 | isREALITY = true 217 | dest, err := json.Marshal(config.REALITYConfigs.Dest) 218 | if err != nil { 219 | return nil, fmt.Errorf("marshal dest %s config fialed: %s", dest, err) 220 | } 221 | streamSetting.Security = "reality" 222 | var private_key string 223 | if nodeInfo.REALITYConfig != nil { 224 | private_key = nodeInfo.REALITYConfig.PrivateKey 225 | } else { 226 | private_key = config.REALITYConfigs.PrivateKey 227 | } 228 | streamSetting.REALITYSettings = &conf.REALITYConfig{ 229 | Show: config.REALITYConfigs.Show, 230 | Dest: dest, 231 | Xver: config.REALITYConfigs.ProxyProtocolVer, 232 | ServerNames: config.REALITYConfigs.ServerNames, 233 | PrivateKey: private_key, 234 | MinClientVer: config.REALITYConfigs.MinClientVer, 235 | MaxClientVer: config.REALITYConfigs.MaxClientVer, 236 | MaxTimeDiff: config.REALITYConfigs.MaxTimeDiff, 237 | ShortIds: append(config.REALITYConfigs.ShortIds, nodeInfo.REALITYConfig.ShortIds...), 238 | } 239 | } 240 | 241 | if !isREALITY && nodeInfo.EnableTLS && config.CertConfig.CertMode != "none" { 242 | streamSetting.Security = "tls" 243 | certFile, keyFile, err := getCertFile(config.CertConfig) 244 | if err != nil { 245 | return nil, err 246 | } 247 | tlsSettings := &conf.TLSConfig{ 248 | RejectUnknownSNI: config.CertConfig.RejectUnknownSni, 249 | } 250 | tlsSettings.Certs = append(tlsSettings.Certs, &conf.TLSCertConfig{CertFile: certFile, KeyFile: keyFile, OcspStapling: 3600}) 251 | streamSetting.TLSSettings = tlsSettings 252 | } 253 | 254 | // Support ProxyProtocol for any transport protocol 255 | if networkType != "tcp" && networkType != "ws" && config.EnableProxyProtocol { 256 | sockoptConfig := &conf.SocketConfig{ 257 | AcceptProxyProtocol: config.EnableProxyProtocol, 258 | } 259 | streamSetting.SocketSettings = sockoptConfig 260 | } 261 | inboundDetourConfig.StreamSetting = streamSetting 262 | 263 | return inboundDetourConfig.Build() 264 | } 265 | 266 | func getCertFile(certConfig *mylego.CertConfig) (certFile string, keyFile string, err error) { 267 | switch certConfig.CertMode { 268 | case "file": 269 | if certConfig.CertFile == "" || certConfig.KeyFile == "" { 270 | return "", "", fmt.Errorf("cert file path or key file path not exist") 271 | } 272 | return certConfig.CertFile, certConfig.KeyFile, nil 273 | case "dns": 274 | lego, err := mylego.New(certConfig) 275 | if err != nil { 276 | return "", "", err 277 | } 278 | certPath, keyPath, err := lego.DNSCert() 279 | if err != nil { 280 | return "", "", err 281 | } 282 | return certPath, keyPath, err 283 | case "http", "tls": 284 | lego, err := mylego.New(certConfig) 285 | if err != nil { 286 | return "", "", err 287 | } 288 | certPath, keyPath, err := lego.HTTPCert() 289 | if err != nil { 290 | return "", "", err 291 | } 292 | return certPath, keyPath, err 293 | default: 294 | return "", "", fmt.Errorf("unsupported certmode: %s", certConfig.CertMode) 295 | } 296 | } 297 | 298 | func buildVlessFallbacks(fallbackConfigs []*FallBackConfig) ([]*conf.VLessInboundFallback, error) { 299 | if fallbackConfigs == nil { 300 | return nil, fmt.Errorf("you must provide FallBackConfigs") 301 | } 302 | 303 | vlessFallBacks := make([]*conf.VLessInboundFallback, len(fallbackConfigs)) 304 | for i, c := range fallbackConfigs { 305 | 306 | if c.Dest == "" { 307 | return nil, fmt.Errorf("dest is required for fallback failed") 308 | } 309 | 310 | var dest json.RawMessage 311 | dest, err := json.Marshal(c.Dest) 312 | if err != nil { 313 | return nil, fmt.Errorf("marshal dest %s config failed: %s", dest, err) 314 | } 315 | vlessFallBacks[i] = &conf.VLessInboundFallback{ 316 | Name: c.SNI, 317 | Alpn: c.Alpn, 318 | Path: c.Path, 319 | Dest: dest, 320 | Xver: c.ProxyProtocolVer, 321 | } 322 | } 323 | return vlessFallBacks, nil 324 | } 325 | 326 | func buildTrojanFallbacks(fallbackConfigs []*FallBackConfig) ([]*conf.TrojanInboundFallback, error) { 327 | if fallbackConfigs == nil { 328 | return nil, fmt.Errorf("you must provide FallBackConfigs") 329 | } 330 | 331 | trojanFallBacks := make([]*conf.TrojanInboundFallback, len(fallbackConfigs)) 332 | for i, c := range fallbackConfigs { 333 | 334 | if c.Dest == "" { 335 | return nil, fmt.Errorf("dest is required for fallback failed") 336 | } 337 | 338 | var dest json.RawMessage 339 | dest, err := json.Marshal(c.Dest) 340 | if err != nil { 341 | return nil, fmt.Errorf("marshal dest %s config failed: %s", dest, err) 342 | } 343 | trojanFallBacks[i] = &conf.TrojanInboundFallback{ 344 | Name: c.SNI, 345 | Alpn: c.Alpn, 346 | Path: c.Path, 347 | Dest: dest, 348 | Xver: c.ProxyProtocolVer, 349 | } 350 | } 351 | return trojanFallBacks, nil 352 | } 353 | -------------------------------------------------------------------------------- /service/controller/inboundbuilder_test.go: -------------------------------------------------------------------------------- 1 | package controller_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/wyx2685/XrayR/api" 7 | "github.com/wyx2685/XrayR/common/mylego" 8 | . "github.com/wyx2685/XrayR/service/controller" 9 | ) 10 | 11 | func TestBuildV2ray(t *testing.T) { 12 | nodeInfo := &api.NodeInfo{ 13 | NodeType: "V2ray", 14 | NodeID: 1, 15 | Port: 1145, 16 | SpeedLimit: 0, 17 | AlterID: 2, 18 | TransportProtocol: "ws", 19 | Host: "test.test.tk", 20 | Path: "v2ray", 21 | EnableTLS: false, 22 | } 23 | certConfig := &mylego.CertConfig{ 24 | CertMode: "http", 25 | CertDomain: "test.test.tk", 26 | Provider: "alidns", 27 | Email: "test@gmail.com", 28 | } 29 | config := &Config{ 30 | CertConfig: certConfig, 31 | } 32 | _, err := InboundBuilder(config, nodeInfo, "test_tag") 33 | if err != nil { 34 | t.Error(err) 35 | } 36 | } 37 | 38 | func TestBuildTrojan(t *testing.T) { 39 | nodeInfo := &api.NodeInfo{ 40 | NodeType: "Trojan", 41 | NodeID: 1, 42 | Port: 1145, 43 | SpeedLimit: 0, 44 | AlterID: 2, 45 | TransportProtocol: "tcp", 46 | Host: "trojan.test.tk", 47 | Path: "v2ray", 48 | EnableTLS: false, 49 | } 50 | DNSEnv := make(map[string]string) 51 | DNSEnv["ALICLOUD_ACCESS_KEY"] = "aaa" 52 | DNSEnv["ALICLOUD_SECRET_KEY"] = "bbb" 53 | certConfig := &mylego.CertConfig{ 54 | CertMode: "dns", 55 | CertDomain: "trojan.test.tk", 56 | Provider: "alidns", 57 | Email: "test@gmail.com", 58 | DNSEnv: DNSEnv, 59 | } 60 | config := &Config{ 61 | CertConfig: certConfig, 62 | } 63 | _, err := InboundBuilder(config, nodeInfo, "test_tag") 64 | if err != nil { 65 | t.Error(err) 66 | } 67 | } 68 | 69 | func TestBuildSS(t *testing.T) { 70 | nodeInfo := &api.NodeInfo{ 71 | NodeType: "Shadowsocks", 72 | NodeID: 1, 73 | Port: 1145, 74 | SpeedLimit: 0, 75 | AlterID: 2, 76 | TransportProtocol: "tcp", 77 | Host: "test.test.tk", 78 | Path: "v2ray", 79 | EnableTLS: false, 80 | } 81 | DNSEnv := make(map[string]string) 82 | DNSEnv["ALICLOUD_ACCESS_KEY"] = "aaa" 83 | DNSEnv["ALICLOUD_SECRET_KEY"] = "bbb" 84 | certConfig := &mylego.CertConfig{ 85 | CertMode: "dns", 86 | CertDomain: "trojan.test.tk", 87 | Provider: "alidns", 88 | Email: "test@me.com", 89 | DNSEnv: DNSEnv, 90 | } 91 | config := &Config{ 92 | CertConfig: certConfig, 93 | } 94 | _, err := InboundBuilder(config, nodeInfo, "test_tag") 95 | if err != nil { 96 | t.Error(err) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /service/controller/outboundbuilder.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/xtls/xray-core/core" 8 | "github.com/xtls/xray-core/infra/conf" 9 | 10 | "github.com/wyx2685/XrayR/api" 11 | ) 12 | 13 | // OutboundBuilder build freedom outbound config for addOutbound 14 | func OutboundBuilder(config *Config, nodeInfo *api.NodeInfo, tag string) (*core.OutboundHandlerConfig, error) { 15 | outboundDetourConfig := &conf.OutboundDetourConfig{} 16 | outboundDetourConfig.Protocol = "freedom" 17 | outboundDetourConfig.Tag = tag 18 | 19 | // SendThrough setting 20 | outboundDetourConfig.SendThrough = &config.SendIP 21 | 22 | // Freedom Protocol setting 23 | var domainStrategy = "Asis" 24 | if config.EnableDNS { 25 | if config.DNSType != "" { 26 | domainStrategy = config.DNSType 27 | } else { 28 | domainStrategy = "UseIP" 29 | } 30 | } 31 | proxySetting := &conf.FreedomConfig{ 32 | DomainStrategy: domainStrategy, 33 | } 34 | // Used for Shadowsocks-Plugin 35 | if nodeInfo.NodeType == "dokodemo-door" { 36 | proxySetting.Redirect = fmt.Sprintf("127.0.0.1:%d", nodeInfo.Port-1) 37 | } 38 | var setting json.RawMessage 39 | setting, err := json.Marshal(proxySetting) 40 | if err != nil { 41 | return nil, fmt.Errorf("marshal proxy %s config failed: %s", nodeInfo.NodeType, err) 42 | } 43 | outboundDetourConfig.Settings = &setting 44 | return outboundDetourConfig.Build() 45 | } 46 | -------------------------------------------------------------------------------- /service/controller/userbuilder.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/sagernet/sing-shadowsocks/shadowaead_2022" 9 | C "github.com/sagernet/sing/common" 10 | log "github.com/sirupsen/logrus" 11 | "github.com/wyx2685/XrayR/api" 12 | "github.com/xtls/xray-core/common/protocol" 13 | "github.com/xtls/xray-core/common/serial" 14 | "github.com/xtls/xray-core/infra/conf" 15 | "github.com/xtls/xray-core/proxy/shadowsocks" 16 | "github.com/xtls/xray-core/proxy/shadowsocks_2022" 17 | "github.com/xtls/xray-core/proxy/trojan" 18 | "github.com/xtls/xray-core/proxy/vless" 19 | ) 20 | 21 | var AEADMethod = map[shadowsocks.CipherType]uint8{ 22 | shadowsocks.CipherType_AES_128_GCM: 0, 23 | shadowsocks.CipherType_AES_256_GCM: 0, 24 | shadowsocks.CipherType_CHACHA20_POLY1305: 0, 25 | shadowsocks.CipherType_XCHACHA20_POLY1305: 0, 26 | } 27 | 28 | func (c *Controller) buildVmessUser(userInfo *[]api.UserInfo) (users []*protocol.User) { 29 | users = make([]*protocol.User, len(*userInfo)) 30 | for i, user := range *userInfo { 31 | vmessAccount := &conf.VMessAccount{ 32 | ID: user.UUID, 33 | Security: "auto", 34 | } 35 | users[i] = &protocol.User{ 36 | Level: 0, 37 | Email: c.buildUserTag(&user), // Email: InboundTag|email|uid 38 | Account: serial.ToTypedMessage(vmessAccount.Build()), 39 | } 40 | } 41 | return users 42 | } 43 | 44 | func (c *Controller) buildVlessUser(userInfo *[]api.UserInfo) (users []*protocol.User) { 45 | users = make([]*protocol.User, len(*userInfo)) 46 | for i, user := range *userInfo { 47 | vlessAccount := &vless.Account{ 48 | Id: user.UUID, 49 | Flow: c.nodeInfo.VlessFlow, 50 | } 51 | users[i] = &protocol.User{ 52 | Level: 0, 53 | Email: c.buildUserTag(&user), 54 | Account: serial.ToTypedMessage(vlessAccount), 55 | } 56 | } 57 | return users 58 | } 59 | 60 | func (c *Controller) buildTrojanUser(userInfo *[]api.UserInfo) (users []*protocol.User) { 61 | users = make([]*protocol.User, len(*userInfo)) 62 | for i, user := range *userInfo { 63 | trojanAccount := &trojan.Account{ 64 | Password: user.UUID, 65 | } 66 | users[i] = &protocol.User{ 67 | Level: 0, 68 | Email: c.buildUserTag(&user), 69 | Account: serial.ToTypedMessage(trojanAccount), 70 | } 71 | } 72 | return users 73 | } 74 | 75 | func (c *Controller) buildSSUser(userInfo *[]api.UserInfo, method string) (users []*protocol.User) { 76 | users = make([]*protocol.User, len(*userInfo)) 77 | 78 | for i, user := range *userInfo { 79 | // shadowsocks2022 Key = "openssl rand -base64 32" and multi users needn't cipher method 80 | if C.Contains(shadowaead_2022.List, strings.ToLower(method)) { 81 | e := c.buildUserTag(&user) 82 | userKey, err := c.checkShadowsocksPassword(user.Passwd, method) 83 | if err != nil { 84 | log.Error(fmt.Errorf("[UID: %d] %s", user.UID, err)) 85 | continue 86 | } 87 | users[i] = &protocol.User{ 88 | Level: 0, 89 | Email: e, 90 | Account: serial.ToTypedMessage(&shadowsocks_2022.Account{ 91 | Key: userKey, 92 | }), 93 | } 94 | } else { 95 | users[i] = &protocol.User{ 96 | Level: 0, 97 | Email: c.buildUserTag(&user), 98 | Account: serial.ToTypedMessage(&shadowsocks.Account{ 99 | Password: user.Passwd, 100 | CipherType: cipherFromString(method), 101 | }), 102 | } 103 | } 104 | } 105 | return users 106 | } 107 | 108 | func (c *Controller) buildSSPluginUser(userInfo *[]api.UserInfo) (users []*protocol.User) { 109 | users = make([]*protocol.User, len(*userInfo)) 110 | 111 | for i, user := range *userInfo { 112 | // shadowsocks2022 Key = openssl rand -base64 32 and multi users needn't cipher method 113 | if C.Contains(shadowaead_2022.List, strings.ToLower(user.Method)) { 114 | e := c.buildUserTag(&user) 115 | userKey, err := c.checkShadowsocksPassword(user.Passwd, user.Method) 116 | if err != nil { 117 | log.Error(fmt.Errorf("[UID: %d] %s", user.UID, err)) 118 | continue 119 | } 120 | users[i] = &protocol.User{ 121 | Level: 0, 122 | Email: e, 123 | Account: serial.ToTypedMessage(&shadowsocks_2022.Account{ 124 | Key: userKey, 125 | }), 126 | } 127 | } else { 128 | // Check if the cypher method is AEAD 129 | cypherMethod := cipherFromString(user.Method) 130 | if _, ok := AEADMethod[cypherMethod]; ok { 131 | users[i] = &protocol.User{ 132 | Level: 0, 133 | Email: c.buildUserTag(&user), 134 | Account: serial.ToTypedMessage(&shadowsocks.Account{ 135 | Password: user.Passwd, 136 | CipherType: cypherMethod, 137 | }), 138 | } 139 | } 140 | } 141 | } 142 | return users 143 | } 144 | 145 | func cipherFromString(c string) shadowsocks.CipherType { 146 | switch strings.ToLower(c) { 147 | case "aes-128-gcm", "aead_aes_128_gcm": 148 | return shadowsocks.CipherType_AES_128_GCM 149 | case "aes-256-gcm", "aead_aes_256_gcm": 150 | return shadowsocks.CipherType_AES_256_GCM 151 | case "chacha20-poly1305", "aead_chacha20_poly1305", "chacha20-ietf-poly1305": 152 | return shadowsocks.CipherType_CHACHA20_POLY1305 153 | case "none", "plain": 154 | return shadowsocks.CipherType_NONE 155 | default: 156 | return shadowsocks.CipherType_UNKNOWN 157 | } 158 | } 159 | 160 | func (c *Controller) buildUserTag(user *api.UserInfo) string { 161 | return fmt.Sprintf("%s|%s|%d", c.Tag, user.Email, user.UID) 162 | } 163 | 164 | func (c *Controller) checkShadowsocksPassword(password string, method string) (string, error) { 165 | if strings.Contains(c.panelType, "V2board") { 166 | var userKey string 167 | if len(password) < 16 { 168 | return "", newError("shadowsocks2022 key's length must be greater than 16").AtWarning() 169 | } 170 | if method == "2022-blake3-aes-128-gcm" { 171 | userKey = password[:16] 172 | } else { 173 | if len(password) < 32 { 174 | return "", newError("shadowsocks2022 key's length must be greater than 32").AtWarning() 175 | } 176 | userKey = password[:32] 177 | } 178 | return base64.StdEncoding.EncodeToString([]byte(userKey)), nil 179 | } else { 180 | return password, nil 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /service/service.go: -------------------------------------------------------------------------------- 1 | // Package service contains all the services used by XrayR 2 | // To implement a service, one needs to implement the interface below. 3 | package service 4 | 5 | // Service is the interface of all the services running in the panel 6 | type Service interface { 7 | Start() error 8 | Close() error 9 | Restart 10 | } 11 | 12 | // Restart the service 13 | type Restart interface { 14 | Start() error 15 | Close() error 16 | } 17 | --------------------------------------------------------------------------------