├── .github ├── ISSUE_TEMPLATE │ └── bug-report.md ├── build │ └── friendly-filenames.json ├── dependabot.yml └── workflows │ ├── Publish Docker image.yml │ ├── codeql-analysis.yml │ └── release.yml ├── .gitignore ├── .gitmodules ├── Dockerfile ├── LICENSE ├── README.md ├── api └── panel │ ├── node.go │ ├── node_test.go │ ├── panel.go │ ├── user.go │ └── utils.go ├── cmd ├── action_linux.go ├── cmd.go ├── common.go ├── common_test.go ├── install_linux.go ├── server.go ├── server_test.go ├── synctime.go ├── version.go ├── x25519.go └── x25519_test.go ├── common ├── counter │ ├── conn.go │ └── traffic.go ├── crypt │ ├── aes.go │ └── x25519.go ├── exec │ └── exec.go ├── file │ └── file.go ├── format │ └── user.go ├── json5 │ └── json5.go ├── rate │ ├── conn.go │ └── writer.go ├── systime │ ├── time_stub.go │ ├── time_unix.go │ └── time_windows.go └── task │ ├── task.go │ └── task_test.go ├── conf ├── cert.go ├── conf.go ├── conf_test.go ├── core.go ├── hy.go ├── limit.go ├── log.go ├── node.go ├── sing.go ├── watch.go └── xray.go ├── core ├── core.go ├── hy2 │ ├── config.go │ ├── geoloader.go │ ├── hook.go │ ├── hy2.go │ ├── logger.go │ ├── node.go │ ├── serverConfig.go │ └── user.go ├── imports │ ├── hy2.go │ ├── imports.go │ ├── sing.go │ └── xray.go ├── interface.go ├── selector.go ├── sing │ ├── hook.go │ ├── node.go │ ├── sing.go │ ├── user.go │ └── utils.go └── xray │ ├── app │ ├── app.go │ └── dispatcher │ │ ├── config.pb.go │ │ ├── config.proto │ │ ├── default.go │ │ ├── dispatcher.go │ │ ├── fakednssniffer.go │ │ ├── sniffer.go │ │ ├── stats.go │ │ └── stats_test.go │ ├── distro │ └── all │ │ └── all.go │ ├── dns.go │ ├── inbound.go │ ├── node.go │ ├── outbound.go │ ├── ss.go │ ├── trojan.go │ ├── user.go │ ├── vmess.go │ └── xray.go ├── example ├── config.json ├── config_full.json ├── config_full_node1.json ├── custom_inbound.json ├── custom_outbound.json ├── dns.json ├── geoip.dat ├── geoip.db ├── geosite.dat ├── geosite.db └── route.json ├── go.mod ├── go.sum ├── limiter ├── dynamic.go ├── limiter.go └── rule.go ├── main.go ├── node ├── cert.go ├── cert_test.go ├── controller.go ├── lego.go ├── lego_test.go ├── node.go ├── task.go └── user.go └── test_data ├── 1.key └── 1.pem /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "Bug 反馈" 3 | about: 创建一个报告以帮助我们修复并改进V2bX 4 | title: '' 5 | labels: 6 | assignees: '' 7 | --- 8 | 9 | **描述该错误** 10 | 简单地描述一下这个bug是什么 11 | 12 | 13 | 14 | **复现** 15 | 请自行复现,并贴出详细步骤操作过程 16 | 17 | 18 | 19 | **日志和错误** 20 | 请使用`v2bx log`查看并添加日志,没有日志的issue不会得到答复并且会被直接关闭 21 | 22 | 23 | 24 | **额外的内容** 25 | 在这里添加关于问题的任何其他内容 -------------------------------------------------------------------------------- /.github/build/friendly-filenames.json: -------------------------------------------------------------------------------- 1 | { 2 | "android-arm64": { "friendlyName": "android-arm64-v8a" }, 3 | "darwin-amd64": { "friendlyName": "macos-64" }, 4 | "darwin-arm64": { "friendlyName": "macos-arm64-v8a" }, 5 | "dragonfly-amd64": { "friendlyName": "dragonfly-64" }, 6 | "freebsd-386": { "friendlyName": "freebsd-32" }, 7 | "freebsd-amd64": { "friendlyName": "freebsd-64" }, 8 | "freebsd-arm64": { "friendlyName": "freebsd-arm64-v8a" }, 9 | "freebsd-arm7": { "friendlyName": "freebsd-arm32-v7a" }, 10 | "linux-386": { "friendlyName": "linux-32" }, 11 | "linux-amd64": { "friendlyName": "linux-64" }, 12 | "linux-arm5": { "friendlyName": "linux-arm32-v5" }, 13 | "linux-arm64": { "friendlyName": "linux-arm64-v8a" }, 14 | "linux-arm6": { "friendlyName": "linux-arm32-v6" }, 15 | "linux-arm7": { "friendlyName": "linux-arm32-v7a" }, 16 | "linux-mips64le": { "friendlyName": "linux-mips64le" }, 17 | "linux-mips64": { "friendlyName": "linux-mips64" }, 18 | "linux-mipslesoftfloat": { "friendlyName": "linux-mips32le-softfloat" }, 19 | "linux-mipsle": { "friendlyName": "linux-mips32le" }, 20 | "linux-mipssoftfloat": { "friendlyName": "linux-mips32-softfloat" }, 21 | "linux-mips": { "friendlyName": "linux-mips32" }, 22 | "linux-ppc64le": { "friendlyName": "linux-ppc64le" }, 23 | "linux-ppc64": { "friendlyName": "linux-ppc64" }, 24 | "linux-riscv64": { "friendlyName": "linux-riscv64" }, 25 | "linux-s390x": { "friendlyName": "linux-s390x" }, 26 | "openbsd-386": { "friendlyName": "openbsd-32" }, 27 | "openbsd-amd64": { "friendlyName": "openbsd-64" }, 28 | "openbsd-arm64": { "friendlyName": "openbsd-arm64-v8a" }, 29 | "openbsd-arm7": { "friendlyName": "openbsd-arm32-v7a" }, 30 | "windows-386": { "friendlyName": "windows-32" }, 31 | "windows-amd64": { "friendlyName": "windows-64" }, 32 | "windows-arm7": { "friendlyName": "windows-arm32-v7a" } 33 | } -------------------------------------------------------------------------------- /.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 | 3 | on: 4 | workflow_dispatch: 5 | release: 6 | types: [published] 7 | pull_request: 8 | branches: 9 | - 'dev_new' 10 | 11 | env: 12 | REGISTRY: ghcr.io 13 | IMAGE_NAME: ${{ github.repository_owner }}/v2bx 14 | 15 | jobs: 16 | build: 17 | runs-on: ubuntu-latest 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | platform: 22 | - linux/amd64 23 | - linux/arm64 24 | steps: 25 | - name: Prepare 26 | run: | 27 | platform=${{ matrix.platform }} 28 | echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV 29 | 30 | - name: Checkout 31 | uses: actions/checkout@v4 32 | 33 | - name: Docker meta 34 | id: meta 35 | uses: docker/metadata-action@v5 36 | with: 37 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 38 | 39 | - name: Set up QEMU 40 | uses: docker/setup-qemu-action@v3 41 | 42 | - name: Set up Docker Buildx 43 | uses: docker/setup-buildx-action@v3 44 | 45 | - name: Login to GitHub Container Registry 46 | uses: docker/login-action@v3 47 | with: 48 | registry: ${{ env.REGISTRY }} 49 | username: ${{ github.actor }} 50 | password: ${{ secrets.GITHUB_TOKEN }} 51 | 52 | - name: Build and push by digest 53 | id: build 54 | uses: docker/build-push-action@v6 55 | with: 56 | platforms: ${{ matrix.platform }} 57 | labels: ${{ steps.meta.outputs.labels }} 58 | outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true 59 | 60 | - name: Export digest 61 | run: | 62 | mkdir -p /tmp/digests 63 | digest="${{ steps.build.outputs.digest }}" 64 | echo "${digest#sha256:}" > "/tmp/digests/${digest#sha256:}" 65 | 66 | - name: Upload digest 67 | uses: actions/upload-artifact@v4 68 | with: 69 | name: digests-${{ env.PLATFORM_PAIR }} 70 | path: /tmp/digests/* 71 | if-no-files-found: error 72 | retention-days: 1 73 | 74 | merge: 75 | runs-on: ubuntu-latest 76 | needs: 77 | - build 78 | steps: 79 | - name: Download digests 80 | uses: actions/download-artifact@v4 81 | with: 82 | path: /tmp/digests 83 | pattern: digests-* 84 | merge-multiple: true 85 | 86 | - name: Set up Docker Buildx 87 | uses: docker/setup-buildx-action@v3 88 | 89 | - name: Docker meta 90 | id: meta 91 | uses: docker/metadata-action@v5 92 | with: 93 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 94 | 95 | - name: Login to GitHub Container Registry 96 | uses: docker/login-action@v3 97 | with: 98 | registry: ${{ env.REGISTRY }} 99 | username: ${{ github.actor }} 100 | password: ${{ secrets.GITHUB_TOKEN }} 101 | 102 | - name: Create manifest list and push 103 | run: | 104 | cd /tmp/digests 105 | tags=$(echo '${{ steps.meta.outputs.json }}' | jq -cr '.tags | map("-t " + .) | join(" ")') 106 | images=$(printf "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s " $(find . -type f -exec cat {} \;)) 107 | echo "Creating manifest with tags: $tags" 108 | echo "Using images: $images" 109 | docker buildx imagetools create $tags $images 110 | 111 | - name: Inspect image 112 | run: | 113 | docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }} -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Build and Release 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - master 8 | - dev_new 9 | paths: 10 | - "**/*.go" 11 | - "go.mod" 12 | - "go.sum" 13 | - ".github/workflows/release.yml" 14 | pull_request: 15 | types: [opened, synchronize, reopened] 16 | paths: 17 | - "**/*.go" 18 | - "go.mod" 19 | - "go.sum" 20 | - ".github/workflows/release.yml" 21 | release: 22 | types: [published] 23 | 24 | jobs: 25 | 26 | build: 27 | strategy: 28 | matrix: 29 | # Include amd64 on all platforms. 30 | goos: [windows, freebsd, linux, darwin] 31 | goarch: [amd64, 386] 32 | exclude: 33 | # Exclude i386 on darwin. 34 | - goarch: 386 35 | goos: darwin 36 | include: 37 | # BEIGIN MacOS ARM64 38 | - goos: darwin 39 | goarch: arm64 40 | # END MacOS ARM64 41 | # BEGIN Linux ARM 5 6 7 42 | - goos: linux 43 | goarch: arm 44 | goarm: 7 45 | - goos: linux 46 | goarch: arm 47 | goarm: 6 48 | - goos: linux 49 | goarch: arm 50 | goarm: 5 51 | # END Linux ARM 5 6 7 52 | # BEGIN Android ARM 8 53 | - goos: android 54 | goarch: arm64 55 | # END Android ARM 8 56 | # BEGIN Other architectures 57 | # BEGIN riscv64 & ARM64 58 | - goos: linux 59 | goarch: arm64 60 | - goos: linux 61 | goarch: riscv64 62 | # END riscv64 & ARM64 63 | # BEGIN MIPS 64 | - goos: linux 65 | goarch: mips64 66 | - goos: linux 67 | goarch: mips64le 68 | - goos: linux 69 | goarch: mipsle 70 | - goos: linux 71 | goarch: mips 72 | # END MIPS 73 | # BEGIN PPC 74 | - goos: linux 75 | goarch: ppc64 76 | - goos: linux 77 | goarch: ppc64le 78 | # END PPC 79 | # BEGIN FreeBSD ARM 80 | - goos: freebsd 81 | goarch: arm64 82 | - goos: freebsd 83 | goarch: arm 84 | goarm: 7 85 | # END FreeBSD ARM 86 | # BEGIN S390X 87 | - goos: linux 88 | goarch: s390x 89 | # END S390X 90 | # END Other architectures 91 | fail-fast: false 92 | 93 | runs-on: ubuntu-latest 94 | env: 95 | GOOS: ${{ matrix.goos }} 96 | GOARCH: ${{ matrix.goarch }} 97 | GOARM: ${{ matrix.goarm }} 98 | CGO_ENABLED: 0 99 | steps: 100 | - name: Checkout codebase 101 | uses: actions/checkout@v4 102 | - name: Show workflow information 103 | id: get_filename 104 | run: | 105 | export _NAME=$(jq ".[\"$GOOS-$GOARCH$GOARM$GOMIPS\"].friendlyName" -r < .github/build/friendly-filenames.json) 106 | echo "GOOS: $GOOS, GOARCH: $GOARCH, GOARM: $GOARM, GOMIPS: $GOMIPS, RELEASE_NAME: $_NAME" 107 | echo "ASSET_NAME=$_NAME" >> $GITHUB_OUTPUT 108 | echo "ASSET_NAME=$_NAME" >> $GITHUB_ENV 109 | - name: Set up Go 110 | uses: actions/setup-go@v5 111 | with: 112 | go-version: '1.24.1' 113 | 114 | - name: Get project dependencies 115 | run: | 116 | go mod download 117 | - name: Get release version 118 | if: ${{ github.event_name == 'release' }} 119 | run: | 120 | echo "version=$(echo $GITHUB_REF | cut -d / -f 3)" >> $GITHUB_ENV 121 | - name: Get other version 122 | if: ${{ github.event_name != 'release' }} 123 | run: | 124 | echo "version=${{ github.sha }}" >> $GITHUB_ENV 125 | - name: Build V2bX 126 | run: | 127 | echo "version: $version" 128 | mkdir -p build_assets 129 | go build -v -o build_assets/V2bX -tags "sing xray hysteria2 with_quic with_grpc with_utls with_wireguard with_acme with_gvisor" -trimpath -ldflags "-X 'github.com/InazumaV/V2bX/cmd.version=$version' -s -w -buildid=" 130 | 131 | - name: Build Mips softfloat V2bX 132 | if: matrix.goarch == 'mips' || matrix.goarch == 'mipsle' 133 | run: | 134 | echo "version: $version" 135 | GOMIPS=softfloat go build -v -o build_assets/V2bX_softfloat -tags "sing xray hysteria2 with_quic with_grpc with_utls with_wireguard with_acme with_gvisor" -trimpath -ldflags "-X 'github.com/InazumaV/V2bX/cmd.version=$version' -s -w -buildid=" 136 | - name: Rename Windows V2bX 137 | if: matrix.goos == 'windows' 138 | run: | 139 | cd ./build_assets || exit 1 140 | mv V2bX V2bX.exe 141 | - name: Prepare to release 142 | run: | 143 | cp ${GITHUB_WORKSPACE}/README.md ./build_assets/README.md 144 | cp ${GITHUB_WORKSPACE}/LICENSE ./build_assets/LICENSE 145 | cp ${GITHUB_WORKSPACE}/example/*.json ./build_assets/ 146 | LIST=('geoip' 'geosite') 147 | for i in "${LIST[@]}" 148 | do 149 | DOWNLOAD_URL="https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/${i}.dat" 150 | FILE_NAME="${i}.dat" 151 | echo -e "Downloading ${DOWNLOAD_URL}..." 152 | curl -L "${DOWNLOAD_URL}" -o ./build_assets/${FILE_NAME} 153 | done 154 | - name: Create ZIP archive 155 | shell: bash 156 | run: | 157 | pushd build_assets || exit 1 158 | touch -mt $(date +%Y01010000) * 159 | zip -9vr ../V2bX-$ASSET_NAME.zip . 160 | popd || exit 1 161 | FILE=./V2bX-$ASSET_NAME.zip 162 | DGST=$FILE.dgst 163 | for METHOD in {"md5","sha1","sha256","sha512"} 164 | do 165 | openssl dgst -$METHOD $FILE | sed 's/([^)]*)//g' >>$DGST 166 | done 167 | - name: Change the name 168 | run: | 169 | mv build_assets V2bX-$ASSET_NAME 170 | - name: Upload files to Artifacts 171 | uses: actions/upload-artifact@v4 172 | with: 173 | name: V2bX-${{ steps.get_filename.outputs.ASSET_NAME }} 174 | path: | 175 | ./V2bX-${{ steps.get_filename.outputs.ASSET_NAME }}/* 176 | - name: Upload binaries to release 177 | uses: svenstaro/upload-release-action@v2 178 | if: github.event_name == 'release' 179 | with: 180 | repo_token: ${{ secrets.GITHUB_TOKEN }} 181 | file: ./V2bX-${{ steps.get_filename.outputs.ASSET_NAME }}.zip* 182 | tag: ${{ github.ref }} 183 | file_glob: true 184 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | example/config.yml 2 | example/main 3 | example/XrayR 4 | example/XrayR* 5 | example/mytest 6 | example/access.logo 7 | example/error.log 8 | api/chooseparser.go.bak 9 | app/Inboundbuilder/.lego/ 10 | app/legocmd/.lego/ 11 | .vscode/launch.json 12 | example/.lego 13 | example/cert 14 | ./vscode 15 | output/* 16 | .idea/* 17 | newV2bX.sh -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "sing-box_mod"] 2 | path = sing-box_mod 3 | url = https://github.com/wyx2685/sing-box_mod.git 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build go 2 | FROM golang:1.24.1-alpine AS builder 3 | WORKDIR /app 4 | COPY . . 5 | ENV CGO_ENABLED=0 6 | RUN go mod download 7 | RUN go build -v -o V2bX -tags "sing xray hysteria2 with_quic with_grpc with_utls with_wireguard with_acme with_gvisor" 8 | 9 | # Release 10 | FROM alpine 11 | # 安装必要的工具包 12 | RUN apk --update --no-cache add tzdata ca-certificates \ 13 | && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime 14 | RUN mkdir /etc/V2bX/ 15 | COPY --from=builder /app/V2bX /usr/local/bin 16 | 17 | ENTRYPOINT [ "V2bX", "server", "--config", "/etc/V2bX/config.json"] 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # V2bX 2 | 3 | [![](https://img.shields.io/badge/TgChat-UnOfficialV2Board%E4%BA%A4%E6%B5%81%E7%BE%A4-green)](https://t.me/unofficialV2board) 4 | [![](https://img.shields.io/badge/TgChat-YuzukiProjects%E4%BA%A4%E6%B5%81%E7%BE%A4-blue)](https://t.me/YuzukiProjects) 5 | 6 | A V2board node server based on multi core, modified from XrayR. 7 | 一个基于多种内核的V2board节点服务端,修改自XrayR,支持V2ay,Trojan,Shadowsocks协议。 8 | 9 | **注意: 本项目需要搭配[修改版V2board](https://github.com/wyx2685/v2board)** 10 | 11 | ## 特点 12 | 13 | * 永久开源且免费。 14 | * 支持Vmess/Vless, Trojan, Shadowsocks, Hysteria1/2多种协议。 15 | * 支持Vless和XTLS等新特性。 16 | * 支持单实例对接多节点,无需重复启动。 17 | * 支持限制在线IP。 18 | * 支持限制Tcp连接数。 19 | * 支持节点端口级别、用户级别限速。 20 | * 配置简单明了。 21 | * 修改配置自动重启实例。 22 | * 支持多种内核,易扩展。 23 | * 支持条件编译,可仅编译需要的内核。 24 | 25 | ## 功能介绍 26 | 27 | | 功能 | v2ray | trojan | shadowsocks | hysteria1/2 | 28 | |-----------|-------|--------|-------------|----------| 29 | | 自动申请tls证书 | √ | √ | √ | √ | 30 | | 自动续签tls证书 | √ | √ | √ | √ | 31 | | 在线人数统计 | √ | √ | √ | √ | 32 | | 审计规则 | √ | √ | √ | √ | 33 | | 自定义DNS | √ | √ | √ | √ | 34 | | 在线IP数限制 | √ | √ | √ | √ | 35 | | 连接数限制 | √ | √ | √ | √ | 36 | | 跨节点IP数限制 |√ |√ |√ |√ | 37 | | 按照用户限速 | √ | √ | √ | √ | 38 | | 动态限速(未测试) | √ | √ | √ | √ | 39 | 40 | ## TODO 41 | 42 | - [ ] 重新实现动态限速 43 | - [ ] 重新实现在线IP同步(跨节点在线IP限制) 44 | - [ ] 完善使用文档 45 | 46 | ## 软件安装 47 | 48 | ### 一键安装 49 | 50 | ``` 51 | wget -N https://raw.githubusercontent.com/wyx2685/V2bX-script/master/install.sh && bash install.sh 52 | ``` 53 | 54 | ### 手动安装 55 | 56 | [手动安装教程](https://v2bx.v-50.me/v2bx/v2bx-xia-zai-he-an-zhuang/install/manual) 57 | 58 | ## 构建 59 | ``` bash 60 | # 通过-tags选项指定要编译的内核, 可选 xray, sing, hysteria2 61 | go build -v -o ./V2bX -tags "xray sing hysteria2 with_quic with_grpc with_utls with_wireguard with_acme" -trimpath -ldflags "-s -w -buildid=" 62 | ``` 63 | 64 | ## 配置文件及详细使用教程 65 | 66 | [详细使用教程](https://v2bx.v-50.me/) 67 | 68 | ## 免责声明 69 | 70 | * 此项目用于本人自用,因此本人不能保证向后兼容性。 71 | * 由于本人能力有限,不能保证所有功能的可用性,如果出现问题请在Issues反馈。 72 | * 本人不对任何人使用本项目造成的任何后果承担责任。 73 | * 本人比较多变,因此本项目可能会随想法或思路的变动随性更改项目结构或大规模重构代码,若不能接受请勿使用。 74 | 75 | ## Thanks 76 | 77 | * [Project X](https://github.com/XTLS/) 78 | * [V2Fly](https://github.com/v2fly) 79 | * [VNet-V2ray](https://github.com/ProxyPanel/VNet-V2ray) 80 | * [Air-Universe](https://github.com/crossfw/Air-Universe) 81 | * [XrayR](https://github.com/XrayR/XrayR) 82 | * [sing-box](https://github.com/SagerNet/sing-box) 83 | 84 | ## Stars 增长记录 85 | 86 | [![Stargazers over time](https://starchart.cc/wyx2685/V2bX.svg)](https://starchart.cc/wyx2685/V2bX) 87 | -------------------------------------------------------------------------------- /api/panel/node.go: -------------------------------------------------------------------------------- 1 | package panel 2 | 3 | import ( 4 | "crypto/sha256" 5 | "encoding/hex" 6 | "fmt" 7 | "reflect" 8 | "strconv" 9 | "strings" 10 | "time" 11 | 12 | "github.com/goccy/go-json" 13 | ) 14 | 15 | // Security type 16 | const ( 17 | None = 0 18 | Tls = 1 19 | Reality = 2 20 | ) 21 | 22 | type NodeInfo struct { 23 | Id int 24 | Type string 25 | Security int 26 | PushInterval time.Duration 27 | PullInterval time.Duration 28 | RawDNS RawDNS 29 | Rules Rules 30 | 31 | // origin 32 | VAllss *VAllssNode 33 | Shadowsocks *ShadowsocksNode 34 | Trojan *TrojanNode 35 | Tuic *TuicNode 36 | AnyTls *AnyTlsNode 37 | Hysteria *HysteriaNode 38 | Hysteria2 *Hysteria2Node 39 | Common *CommonNode 40 | } 41 | 42 | type CommonNode struct { 43 | Host string `json:"host"` 44 | ServerPort int `json:"server_port"` 45 | ServerName string `json:"server_name"` 46 | Routes []Route `json:"routes"` 47 | BaseConfig *BaseConfig `json:"base_config"` 48 | } 49 | 50 | type Route struct { 51 | Id int `json:"id"` 52 | Match interface{} `json:"match"` 53 | Action string `json:"action"` 54 | ActionValue string `json:"action_value"` 55 | } 56 | type BaseConfig struct { 57 | PushInterval any `json:"push_interval"` 58 | PullInterval any `json:"pull_interval"` 59 | } 60 | 61 | // VAllssNode is vmess and vless node info 62 | type VAllssNode struct { 63 | CommonNode 64 | Tls int `json:"tls"` 65 | TlsSettings TlsSettings `json:"tls_settings"` 66 | TlsSettingsBack *TlsSettings `json:"tlsSettings"` 67 | Network string `json:"network"` 68 | NetworkSettings json.RawMessage `json:"network_settings"` 69 | NetworkSettingsBack json.RawMessage `json:"networkSettings"` 70 | ServerName string `json:"server_name"` 71 | 72 | // vless only 73 | Flow string `json:"flow"` 74 | RealityConfig RealityConfig `json:"-"` 75 | } 76 | 77 | type TlsSettings struct { 78 | ServerName string `json:"server_name"` 79 | Dest string `json:"dest"` 80 | ServerPort string `json:"server_port"` 81 | ShortId string `json:"short_id"` 82 | PrivateKey string `json:"private_key"` 83 | Xver uint64 `json:"xver,string"` 84 | } 85 | 86 | type RealityConfig struct { 87 | Xver uint64 `json:"Xver"` 88 | MinClientVer string `json:"MinClientVer"` 89 | MaxClientVer string `json:"MaxClientVer"` 90 | MaxTimeDiff string `json:"MaxTimeDiff"` 91 | } 92 | 93 | type ShadowsocksNode struct { 94 | CommonNode 95 | Cipher string `json:"cipher"` 96 | ServerKey string `json:"server_key"` 97 | } 98 | 99 | type TrojanNode struct { 100 | CommonNode 101 | Network string `json:"network"` 102 | NetworkSettings json.RawMessage `json:"networkSettings"` 103 | } 104 | 105 | type TuicNode struct { 106 | CommonNode 107 | CongestionControl string `json:"congestion_control"` 108 | ZeroRTTHandshake bool `json:"zero_rtt_handshake"` 109 | } 110 | 111 | type AnyTlsNode struct { 112 | CommonNode 113 | PaddingScheme []string `json:"padding_scheme,omitempty"` 114 | } 115 | 116 | type HysteriaNode struct { 117 | CommonNode 118 | UpMbps int `json:"up_mbps"` 119 | DownMbps int `json:"down_mbps"` 120 | Obfs string `json:"obfs"` 121 | } 122 | 123 | type Hysteria2Node struct { 124 | CommonNode 125 | Ignore_Client_Bandwidth bool `json:"ignore_client_bandwidth"` 126 | UpMbps int `json:"up_mbps"` 127 | DownMbps int `json:"down_mbps"` 128 | ObfsType string `json:"obfs"` 129 | ObfsPassword string `json:"obfs-password"` 130 | } 131 | 132 | type RawDNS struct { 133 | DNSMap map[string]map[string]interface{} 134 | DNSJson []byte 135 | } 136 | 137 | type Rules struct { 138 | Regexp []string 139 | Protocol []string 140 | } 141 | 142 | func (c *Client) GetNodeInfo() (node *NodeInfo, err error) { 143 | const path = "/api/v1/server/UniProxy/config" 144 | r, err := c.client. 145 | R(). 146 | SetHeader("If-None-Match", c.nodeEtag). 147 | ForceContentType("application/json"). 148 | Get(path) 149 | 150 | if r.StatusCode() == 304 { 151 | return nil, nil 152 | } 153 | hash := sha256.Sum256(r.Body()) 154 | newBodyHash := hex.EncodeToString(hash[:]) 155 | if c.responseBodyHash == newBodyHash { 156 | return nil, nil 157 | } 158 | c.responseBodyHash = newBodyHash 159 | c.nodeEtag = r.Header().Get("ETag") 160 | if err = c.checkResponse(r, path, err); err != nil { 161 | return nil, err 162 | } 163 | 164 | if r != nil { 165 | defer func() { 166 | if r.RawBody() != nil { 167 | r.RawBody().Close() 168 | } 169 | }() 170 | } else { 171 | return nil, fmt.Errorf("received nil response") 172 | } 173 | node = &NodeInfo{ 174 | Id: c.NodeId, 175 | Type: c.NodeType, 176 | RawDNS: RawDNS{ 177 | DNSMap: make(map[string]map[string]interface{}), 178 | DNSJson: []byte(""), 179 | }, 180 | } 181 | // parse protocol params 182 | var cm *CommonNode 183 | switch c.NodeType { 184 | case "vmess", "vless": 185 | rsp := &VAllssNode{} 186 | err = json.Unmarshal(r.Body(), rsp) 187 | if err != nil { 188 | return nil, fmt.Errorf("decode v2ray params error: %s", err) 189 | } 190 | if len(rsp.NetworkSettingsBack) > 0 { 191 | rsp.NetworkSettings = rsp.NetworkSettingsBack 192 | rsp.NetworkSettingsBack = nil 193 | } 194 | if rsp.TlsSettingsBack != nil { 195 | rsp.TlsSettings = *rsp.TlsSettingsBack 196 | rsp.TlsSettingsBack = nil 197 | } 198 | cm = &rsp.CommonNode 199 | node.VAllss = rsp 200 | node.Security = node.VAllss.Tls 201 | case "shadowsocks": 202 | rsp := &ShadowsocksNode{} 203 | err = json.Unmarshal(r.Body(), rsp) 204 | if err != nil { 205 | return nil, fmt.Errorf("decode shadowsocks params error: %s", err) 206 | } 207 | cm = &rsp.CommonNode 208 | node.Shadowsocks = rsp 209 | node.Security = None 210 | case "trojan": 211 | rsp := &TrojanNode{} 212 | err = json.Unmarshal(r.Body(), rsp) 213 | if err != nil { 214 | return nil, fmt.Errorf("decode trojan params error: %s", err) 215 | } 216 | cm = &rsp.CommonNode 217 | node.Trojan = rsp 218 | node.Security = Tls 219 | case "tuic": 220 | rsp := &TuicNode{} 221 | err = json.Unmarshal(r.Body(), rsp) 222 | if err != nil { 223 | return nil, fmt.Errorf("decode tuic params error: %s", err) 224 | } 225 | cm = &rsp.CommonNode 226 | node.Tuic = rsp 227 | node.Security = Tls 228 | case "anytls": 229 | rsp := &AnyTlsNode{} 230 | err = json.Unmarshal(r.Body(), rsp) 231 | if err != nil { 232 | return nil, fmt.Errorf("decode anytls params error: %s", err) 233 | } 234 | cm = &rsp.CommonNode 235 | node.AnyTls = rsp 236 | node.Security = Tls 237 | case "hysteria": 238 | rsp := &HysteriaNode{} 239 | err = json.Unmarshal(r.Body(), rsp) 240 | if err != nil { 241 | return nil, fmt.Errorf("decode hysteria params error: %s", err) 242 | } 243 | cm = &rsp.CommonNode 244 | node.Hysteria = rsp 245 | node.Security = Tls 246 | case "hysteria2": 247 | rsp := &Hysteria2Node{} 248 | err = json.Unmarshal(r.Body(), rsp) 249 | if err != nil { 250 | return nil, fmt.Errorf("decode hysteria2 params error: %s", err) 251 | } 252 | cm = &rsp.CommonNode 253 | node.Hysteria2 = rsp 254 | node.Security = Tls 255 | } 256 | 257 | // parse rules and dns 258 | for i := range cm.Routes { 259 | var matchs []string 260 | if _, ok := cm.Routes[i].Match.(string); ok { 261 | matchs = strings.Split(cm.Routes[i].Match.(string), ",") 262 | } else if _, ok = cm.Routes[i].Match.([]string); ok { 263 | matchs = cm.Routes[i].Match.([]string) 264 | } else { 265 | temp := cm.Routes[i].Match.([]interface{}) 266 | matchs = make([]string, len(temp)) 267 | for i := range temp { 268 | matchs[i] = temp[i].(string) 269 | } 270 | } 271 | switch cm.Routes[i].Action { 272 | case "block": 273 | for _, v := range matchs { 274 | if strings.HasPrefix(v, "protocol:") { 275 | // protocol 276 | node.Rules.Protocol = append(node.Rules.Protocol, strings.TrimPrefix(v, "protocol:")) 277 | } else { 278 | // domain 279 | node.Rules.Regexp = append(node.Rules.Regexp, strings.TrimPrefix(v, "regexp:")) 280 | } 281 | } 282 | case "dns": 283 | var domains []string 284 | domains = append(domains, matchs...) 285 | if matchs[0] != "main" { 286 | node.RawDNS.DNSMap[strconv.Itoa(i)] = map[string]interface{}{ 287 | "address": cm.Routes[i].ActionValue, 288 | "domains": domains, 289 | } 290 | } else { 291 | dns := []byte(strings.Join(matchs[1:], "")) 292 | node.RawDNS.DNSJson = dns 293 | } 294 | } 295 | } 296 | 297 | // set interval 298 | node.PushInterval = intervalToTime(cm.BaseConfig.PushInterval) 299 | node.PullInterval = intervalToTime(cm.BaseConfig.PullInterval) 300 | 301 | node.Common = cm 302 | // clear 303 | cm.Routes = nil 304 | cm.BaseConfig = nil 305 | 306 | return node, nil 307 | } 308 | 309 | func intervalToTime(i interface{}) time.Duration { 310 | switch reflect.TypeOf(i).Kind() { 311 | case reflect.Int: 312 | return time.Duration(i.(int)) * time.Second 313 | case reflect.String: 314 | i, _ := strconv.Atoi(i.(string)) 315 | return time.Duration(i) * time.Second 316 | case reflect.Float64: 317 | return time.Duration(i.(float64)) * time.Second 318 | default: 319 | return time.Duration(reflect.ValueOf(i).Int()) * time.Second 320 | } 321 | } 322 | -------------------------------------------------------------------------------- /api/panel/node_test.go: -------------------------------------------------------------------------------- 1 | package panel 2 | 3 | import ( 4 | "log" 5 | "testing" 6 | 7 | "github.com/InazumaV/V2bX/conf" 8 | ) 9 | 10 | var client *Client 11 | 12 | func init() { 13 | c, err := New(&conf.ApiConfig{ 14 | APIHost: "http://127.0.0.1", 15 | Key: "token", 16 | NodeType: "V2ray", 17 | NodeID: 1, 18 | }) 19 | if err != nil { 20 | log.Panic(err) 21 | } 22 | client = c 23 | } 24 | 25 | func TestClient_GetNodeInfo(t *testing.T) { 26 | log.Println(client.GetNodeInfo()) 27 | log.Println(client.GetNodeInfo()) 28 | } 29 | 30 | func TestClient_ReportUserTraffic(t *testing.T) { 31 | log.Println(client.ReportUserTraffic([]UserTraffic{ 32 | { 33 | UID: 10372, 34 | Upload: 1000, 35 | Download: 1000, 36 | }, 37 | })) 38 | } 39 | -------------------------------------------------------------------------------- /api/panel/panel.go: -------------------------------------------------------------------------------- 1 | package panel 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | "time" 9 | 10 | "github.com/sirupsen/logrus" 11 | 12 | "github.com/InazumaV/V2bX/conf" 13 | "github.com/go-resty/resty/v2" 14 | ) 15 | 16 | // Panel is the interface for different panel's api. 17 | 18 | type Client struct { 19 | client *resty.Client 20 | APIHost string 21 | Token string 22 | NodeType string 23 | NodeId int 24 | nodeEtag string 25 | userEtag string 26 | responseBodyHash string 27 | UserList *UserListBody 28 | AliveMap *AliveMap 29 | } 30 | 31 | func New(c *conf.ApiConfig) (*Client, error) { 32 | client := resty.New() 33 | client.SetRetryCount(3) 34 | if c.Timeout > 0 { 35 | client.SetTimeout(time.Duration(c.Timeout) * time.Second) 36 | } else { 37 | client.SetTimeout(5 * time.Second) 38 | } 39 | client.OnError(func(req *resty.Request, err error) { 40 | var v *resty.ResponseError 41 | if errors.As(err, &v) { 42 | // v.Response contains the last response from the server 43 | // v.Err contains the original error 44 | logrus.Error(v.Err) 45 | } 46 | }) 47 | client.SetBaseURL(c.APIHost) 48 | // Check node type 49 | c.NodeType = strings.ToLower(c.NodeType) 50 | switch c.NodeType { 51 | case "v2ray": 52 | c.NodeType = "vmess" 53 | case 54 | "vmess", 55 | "trojan", 56 | "shadowsocks", 57 | "hysteria", 58 | "hysteria2", 59 | "tuic", 60 | "anytls", 61 | "vless": 62 | default: 63 | return nil, fmt.Errorf("unsupported Node type: %s", c.NodeType) 64 | } 65 | // set params 66 | client.SetQueryParams(map[string]string{ 67 | "node_type": c.NodeType, 68 | "node_id": strconv.Itoa(c.NodeID), 69 | "token": c.Key, 70 | }) 71 | return &Client{ 72 | client: client, 73 | Token: c.Key, 74 | APIHost: c.APIHost, 75 | NodeType: c.NodeType, 76 | NodeId: c.NodeID, 77 | UserList: &UserListBody{}, 78 | AliveMap: &AliveMap{}, 79 | }, nil 80 | } 81 | -------------------------------------------------------------------------------- /api/panel/user.go: -------------------------------------------------------------------------------- 1 | package panel 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/goccy/go-json" 7 | ) 8 | 9 | type OnlineUser struct { 10 | UID int 11 | IP string 12 | } 13 | 14 | type UserInfo struct { 15 | Id int `json:"id"` 16 | Uuid string `json:"uuid"` 17 | SpeedLimit int `json:"speed_limit"` 18 | DeviceLimit int `json:"device_limit"` 19 | } 20 | 21 | type UserListBody struct { 22 | //Msg string `json:"msg"` 23 | Users []UserInfo `json:"users"` 24 | } 25 | 26 | type AliveMap struct { 27 | Alive map[int]int `json:"alive"` 28 | } 29 | 30 | // GetUserList will pull user from v2board 31 | func (c *Client) GetUserList() ([]UserInfo, error) { 32 | const path = "/api/v1/server/UniProxy/user" 33 | r, err := c.client.R(). 34 | SetHeader("If-None-Match", c.userEtag). 35 | ForceContentType("application/json"). 36 | Get(path) 37 | if r == nil || r.RawResponse == nil { 38 | return nil, fmt.Errorf("received nil response or raw response") 39 | } 40 | defer r.RawResponse.Body.Close() 41 | 42 | if r.StatusCode() == 304 { 43 | return nil, nil 44 | } 45 | 46 | if err = c.checkResponse(r, path, err); err != nil { 47 | return nil, err 48 | } 49 | userlist := &UserListBody{} 50 | if err := json.Unmarshal(r.Body(), userlist); err != nil { 51 | return nil, fmt.Errorf("unmarshal user list error: %w", err) 52 | } 53 | c.userEtag = r.Header().Get("ETag") 54 | return userlist.Users, nil 55 | } 56 | 57 | // GetUserAlive will fetch the alive_ip count for users 58 | func (c *Client) GetUserAlive() (map[int]int, error) { 59 | c.AliveMap = &AliveMap{} 60 | const path = "/api/v1/server/UniProxy/alivelist" 61 | r, err := c.client.R(). 62 | ForceContentType("application/json"). 63 | Get(path) 64 | if err != nil || r.StatusCode() >= 399 { 65 | c.AliveMap.Alive = make(map[int]int) 66 | return c.AliveMap.Alive, nil 67 | } 68 | if r == nil || r.RawResponse == nil { 69 | fmt.Printf("received nil response or raw response") 70 | c.AliveMap.Alive = make(map[int]int) 71 | return c.AliveMap.Alive, nil 72 | } 73 | defer r.RawResponse.Body.Close() 74 | if err := json.Unmarshal(r.Body(), c.AliveMap); err != nil { 75 | fmt.Printf("unmarshal user alive list error: %s", err) 76 | c.AliveMap.Alive = make(map[int]int) 77 | } 78 | 79 | return c.AliveMap.Alive, nil 80 | } 81 | 82 | type UserTraffic struct { 83 | UID int 84 | Upload int64 85 | Download int64 86 | } 87 | 88 | // ReportUserTraffic reports the user traffic 89 | func (c *Client) ReportUserTraffic(userTraffic []UserTraffic) error { 90 | data := make(map[int][]int64, len(userTraffic)) 91 | for i := range userTraffic { 92 | data[userTraffic[i].UID] = []int64{userTraffic[i].Upload, userTraffic[i].Download} 93 | } 94 | const path = "/api/v1/server/UniProxy/push" 95 | r, err := c.client.R(). 96 | SetBody(data). 97 | ForceContentType("application/json"). 98 | Post(path) 99 | err = c.checkResponse(r, path, err) 100 | if err != nil { 101 | return err 102 | } 103 | return nil 104 | } 105 | 106 | func (c *Client) ReportNodeOnlineUsers(data *map[int][]string) error { 107 | const path = "/api/v1/server/UniProxy/alive" 108 | r, err := c.client.R(). 109 | SetBody(data). 110 | ForceContentType("application/json"). 111 | Post(path) 112 | err = c.checkResponse(r, path, err) 113 | 114 | if err != nil { 115 | return nil 116 | } 117 | 118 | return nil 119 | } 120 | -------------------------------------------------------------------------------- /api/panel/utils.go: -------------------------------------------------------------------------------- 1 | package panel 2 | 3 | import ( 4 | "fmt" 5 | "github.com/go-resty/resty/v2" 6 | path2 "path" 7 | ) 8 | 9 | // Debug set the client debug for client 10 | func (c *Client) Debug() { 11 | c.client.SetDebug(true) 12 | } 13 | 14 | func (c *Client) assembleURL(path string) string { 15 | return path2.Join(c.APIHost + path) 16 | } 17 | func (c *Client) checkResponse(res *resty.Response, path string, err error) error { 18 | if err != nil { 19 | return fmt.Errorf("request %s failed: %s", c.assembleURL(path), err) 20 | } 21 | if res.StatusCode() >= 400 { 22 | body := res.Body() 23 | return fmt.Errorf("request %s failed: %s", c.assembleURL(path), string(body)) 24 | } 25 | return nil 26 | } 27 | -------------------------------------------------------------------------------- /cmd/action_linux.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/InazumaV/V2bX/common/exec" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | var ( 12 | startCommand = cobra.Command{ 13 | Use: "start", 14 | Short: "Start V2bX service", 15 | Run: startHandle, 16 | } 17 | stopCommand = cobra.Command{ 18 | Use: "stop", 19 | Short: "Stop V2bX service", 20 | Run: stopHandle, 21 | } 22 | restartCommand = cobra.Command{ 23 | Use: "restart", 24 | Short: "Restart V2bX service", 25 | Run: restartHandle, 26 | } 27 | logCommand = cobra.Command{ 28 | Use: "log", 29 | Short: "Output V2bX log", 30 | Run: func(_ *cobra.Command, _ []string) { 31 | exec.RunCommandStd("journalctl", "-u", "V2bX.service", "-e", "--no-pager", "-f") 32 | }, 33 | } 34 | ) 35 | 36 | func init() { 37 | command.AddCommand(&startCommand) 38 | command.AddCommand(&stopCommand) 39 | command.AddCommand(&restartCommand) 40 | command.AddCommand(&logCommand) 41 | } 42 | 43 | func startHandle(_ *cobra.Command, _ []string) { 44 | r, err := checkRunning() 45 | if err != nil { 46 | fmt.Println(Err("check status error: ", err)) 47 | fmt.Println(Err("V2bX启动失败")) 48 | return 49 | } 50 | if r { 51 | fmt.Println(Ok("V2bX已运行,无需再次启动,如需重启请选择重启")) 52 | } 53 | _, err = exec.RunCommandByShell("systemctl start V2bX.service") 54 | if err != nil { 55 | fmt.Println(Err("exec start cmd error: ", err)) 56 | fmt.Println(Err("V2bX启动失败")) 57 | return 58 | } 59 | time.Sleep(time.Second * 3) 60 | r, err = checkRunning() 61 | if err != nil { 62 | fmt.Println(Err("check status error: ", err)) 63 | fmt.Println(Err("V2bX启动失败")) 64 | } 65 | if !r { 66 | fmt.Println(Err("V2bX可能启动失败,请稍后使用 V2bX log 查看日志信息")) 67 | return 68 | } 69 | fmt.Println(Ok("V2bX 启动成功,请使用 V2bX log 查看运行日志")) 70 | } 71 | 72 | func stopHandle(_ *cobra.Command, _ []string) { 73 | _, err := exec.RunCommandByShell("systemctl stop V2bX.service") 74 | if err != nil { 75 | fmt.Println(Err("exec stop cmd error: ", err)) 76 | fmt.Println(Err("V2bX停止失败")) 77 | return 78 | } 79 | time.Sleep(2 * time.Second) 80 | r, err := checkRunning() 81 | if err != nil { 82 | fmt.Println(Err("check status error:", err)) 83 | fmt.Println(Err("V2bX停止失败")) 84 | return 85 | } 86 | if r { 87 | fmt.Println(Err("V2bX停止失败,可能是因为停止时间超过了两秒,请稍后查看日志信息")) 88 | return 89 | } 90 | fmt.Println(Ok("V2bX 停止成功")) 91 | } 92 | 93 | func restartHandle(_ *cobra.Command, _ []string) { 94 | _, err := exec.RunCommandByShell("systemctl restart V2bX.service") 95 | if err != nil { 96 | fmt.Println(Err("exec restart cmd error: ", err)) 97 | fmt.Println(Err("V2bX重启失败")) 98 | return 99 | } 100 | r, err := checkRunning() 101 | if err != nil { 102 | fmt.Println(Err("check status error: ", err)) 103 | fmt.Println(Err("V2bX重启失败")) 104 | return 105 | } 106 | if !r { 107 | fmt.Println(Err("V2bX可能启动失败,请稍后使用 V2bX log 查看日志信息")) 108 | return 109 | } 110 | fmt.Println(Ok("V2bX重启成功")) 111 | } 112 | -------------------------------------------------------------------------------- /cmd/cmd.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | log "github.com/sirupsen/logrus" 5 | 6 | _ "github.com/InazumaV/V2bX/core/imports" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | var command = &cobra.Command{ 11 | Use: "V2bX", 12 | } 13 | 14 | func Run() { 15 | err := command.Execute() 16 | if err != nil { 17 | log.WithField("err", err).Error("Execute command failed") 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /cmd/common.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/InazumaV/V2bX/common/exec" 8 | ) 9 | 10 | const ( 11 | red = "\033[0;31m" 12 | green = "\033[0;32m" 13 | yellow = "\033[0;33m" 14 | plain = "\033[0m" 15 | ) 16 | 17 | func checkRunning() (bool, error) { 18 | o, err := exec.RunCommandByShell("systemctl status V2bX | grep Active") 19 | if err != nil { 20 | return false, err 21 | } 22 | return strings.Contains(o, "running"), nil 23 | } 24 | 25 | func Err(msg ...any) string { 26 | return red + fmt.Sprint(msg...) + plain 27 | } 28 | 29 | func Ok(msg ...any) string { 30 | return green + fmt.Sprint(msg...) + plain 31 | } 32 | 33 | func Warn(msg ...any) string { 34 | return yellow + fmt.Sprint(msg...) + plain 35 | } 36 | -------------------------------------------------------------------------------- /cmd/common_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import "testing" 4 | 5 | func Test_printFailed(t *testing.T) { 6 | t.Log(Err("test")) 7 | } 8 | -------------------------------------------------------------------------------- /cmd/install_linux.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "github.com/InazumaV/V2bX/common/exec" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | var targetVersion string 13 | 14 | var ( 15 | updateCommand = cobra.Command{ 16 | Use: "update", 17 | Short: "Update V2bX version", 18 | Run: func(_ *cobra.Command, _ []string) { 19 | exec.RunCommandStd("bash", 20 | "<(curl -Ls https://raw.githubusercontents.com/InazumaV/V2bX-script/master/install.sh)", 21 | targetVersion) 22 | }, 23 | Args: cobra.NoArgs, 24 | } 25 | uninstallCommand = cobra.Command{ 26 | Use: "uninstall", 27 | Short: "Uninstall V2bX", 28 | Run: uninstallHandle, 29 | } 30 | ) 31 | 32 | func init() { 33 | updateCommand.PersistentFlags().StringVar(&targetVersion, "version", "", "update target version") 34 | command.AddCommand(&updateCommand) 35 | command.AddCommand(&uninstallCommand) 36 | } 37 | 38 | func uninstallHandle(_ *cobra.Command, _ []string) { 39 | var yes string 40 | fmt.Println(Warn("确定要卸载 V2bX 吗?(Y/n)")) 41 | fmt.Scan(&yes) 42 | if strings.ToLower(yes) != "y" { 43 | fmt.Println("已取消卸载") 44 | } 45 | _, err := exec.RunCommandByShell("systemctl stop V2bX&&systemctl disable V2bX") 46 | if err != nil { 47 | fmt.Println(Err("exec cmd error: ", err)) 48 | fmt.Println(Err("卸载失败")) 49 | return 50 | } 51 | _ = os.RemoveAll("/etc/systemd/system/V2bX.service") 52 | _ = os.RemoveAll("/etc/V2bX/") 53 | _ = os.RemoveAll("/usr/local/V2bX/") 54 | _ = os.RemoveAll("/bin/V2bX") 55 | _, err = exec.RunCommandByShell("systemctl daemon-reload&&systemctl reset-failed") 56 | if err != nil { 57 | fmt.Println(Err("exec cmd error: ", err)) 58 | fmt.Println(Err("卸载失败")) 59 | return 60 | } 61 | fmt.Println(Ok("卸载成功")) 62 | } 63 | -------------------------------------------------------------------------------- /cmd/server.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | "os/signal" 6 | "runtime" 7 | "syscall" 8 | 9 | "github.com/InazumaV/V2bX/conf" 10 | vCore "github.com/InazumaV/V2bX/core" 11 | "github.com/InazumaV/V2bX/limiter" 12 | "github.com/InazumaV/V2bX/node" 13 | log "github.com/sirupsen/logrus" 14 | "github.com/spf13/cobra" 15 | "gopkg.in/natefinch/lumberjack.v2" 16 | ) 17 | 18 | var ( 19 | config string 20 | watch bool 21 | ) 22 | 23 | var serverCommand = cobra.Command{ 24 | Use: "server", 25 | Short: "Run V2bX server", 26 | Run: serverHandle, 27 | Args: cobra.NoArgs, 28 | } 29 | 30 | func init() { 31 | serverCommand.PersistentFlags(). 32 | StringVarP(&config, "config", "c", 33 | "/etc/V2bX/config.json", "config file path") 34 | serverCommand.PersistentFlags(). 35 | BoolVarP(&watch, "watch", "w", 36 | true, "watch file path change") 37 | command.AddCommand(&serverCommand) 38 | } 39 | 40 | func serverHandle(_ *cobra.Command, _ []string) { 41 | showVersion() 42 | c := conf.New() 43 | err := c.LoadFromPath(config) 44 | if err != nil { 45 | log.WithField("err", err).Error("Load config file failed") 46 | return 47 | } 48 | switch c.LogConfig.Level { 49 | case "debug": 50 | log.SetLevel(log.DebugLevel) 51 | case "info": 52 | log.SetLevel(log.InfoLevel) 53 | case "warn": 54 | log.SetLevel(log.WarnLevel) 55 | case "error": 56 | log.SetLevel(log.ErrorLevel) 57 | } 58 | if c.LogConfig.Output != "" { 59 | w := &lumberjack.Logger{ 60 | Filename: c.LogConfig.Output, 61 | MaxSize: 100, 62 | MaxBackups: 3, 63 | MaxAge: 28, 64 | Compress: true, 65 | } 66 | log.SetOutput(w) 67 | } 68 | limiter.Init() 69 | log.Info("Start V2bX...") 70 | vc, err := vCore.NewCore(c.CoresConfig) 71 | if err != nil { 72 | log.WithField("err", err).Error("new core failed") 73 | return 74 | } 75 | err = vc.Start() 76 | if err != nil { 77 | log.WithField("err", err).Error("Start core failed") 78 | return 79 | } 80 | defer vc.Close() 81 | log.Info("Core ", vc.Type(), " started") 82 | nodes := node.New() 83 | err = nodes.Start(c.NodeConfig, vc) 84 | if err != nil { 85 | log.WithField("err", err).Error("Run nodes failed") 86 | return 87 | } 88 | log.Info("Nodes started") 89 | xdns := os.Getenv("XRAY_DNS_PATH") 90 | sdns := os.Getenv("SING_DNS_PATH") 91 | if watch { 92 | err = c.Watch(config, xdns, sdns, func() { 93 | nodes.Close() 94 | err = vc.Close() 95 | if err != nil { 96 | log.WithField("err", err).Error("Restart node failed") 97 | return 98 | } 99 | vc, err = vCore.NewCore(c.CoresConfig) 100 | if err != nil { 101 | log.WithField("err", err).Error("New core failed") 102 | return 103 | } 104 | err = vc.Start() 105 | if err != nil { 106 | log.WithField("err", err).Error("Start core failed") 107 | return 108 | } 109 | log.Info("Core ", vc.Type(), " restarted") 110 | err = nodes.Start(c.NodeConfig, vc) 111 | if err != nil { 112 | log.WithField("err", err).Error("Run nodes failed") 113 | return 114 | } 115 | log.Info("Nodes restarted") 116 | runtime.GC() 117 | }) 118 | if err != nil { 119 | log.WithField("err", err).Error("start watch failed") 120 | return 121 | } 122 | } 123 | // clear memory 124 | runtime.GC() 125 | // wait exit signal 126 | { 127 | osSignals := make(chan os.Signal, 1) 128 | signal.Notify(osSignals, syscall.SIGINT, syscall.SIGKILL, syscall.SIGTERM) 129 | <-osSignals 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /cmd/server_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import "testing" 4 | 5 | func TestRun(t *testing.T) { 6 | Run() 7 | } 8 | -------------------------------------------------------------------------------- /cmd/synctime.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/InazumaV/V2bX/common/systime" 7 | "github.com/beevik/ntp" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | var ntpServer string 12 | 13 | var commandSyncTime = &cobra.Command{ 14 | Use: "synctime", 15 | Short: "Sync time from ntp server", 16 | Args: cobra.NoArgs, 17 | Run: synctimeHandle, 18 | } 19 | 20 | func init() { 21 | commandSyncTime.Flags().StringVar(&ntpServer, "server", "time.apple.com", "ntp server") 22 | command.AddCommand(commandSyncTime) 23 | } 24 | 25 | func synctimeHandle(_ *cobra.Command, _ []string) { 26 | t, err := ntp.Time(ntpServer) 27 | if err != nil { 28 | fmt.Println(Err("get time from server error: ", err)) 29 | fmt.Println(Err("同步时间失败")) 30 | return 31 | } 32 | err = systime.SetSystemTime(t) 33 | if err != nil { 34 | fmt.Println(Err("set system time error: ", err)) 35 | fmt.Println(Err("同步时间失败")) 36 | return 37 | } 38 | fmt.Println(Ok("同步时间成功")) 39 | } 40 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | var ( 10 | version = "TempVersion" //use ldflags replace 11 | codename = "V2bX" 12 | intro = "A V2board backend based on multi core" 13 | ) 14 | 15 | var versionCommand = cobra.Command{ 16 | Use: "version", 17 | Short: "Print version info", 18 | Run: func(_ *cobra.Command, _ []string) { 19 | showVersion() 20 | }, 21 | } 22 | 23 | func init() { 24 | command.AddCommand(&versionCommand) 25 | } 26 | 27 | func showVersion() { 28 | fmt.Println(` 29 | _/ _/ _/_/ _/ _/ _/ 30 | _/ _/ _/ _/ _/_/_/ _/ _/ 31 | _/ _/ _/ _/ _/ _/ 32 | _/ _/ _/ _/ _/ _/ _/ 33 | _/ _/_/_/_/ _/_/_/ _/ _/ 34 | `) 35 | fmt.Printf("%s %s (%s) \n", codename, version, intro) 36 | //fmt.Printf("Supported cores: %s\n", strings.Join(vCore.RegisteredCore(), ", ")) 37 | // Warning 38 | //fmt.Println(Warn("This version need V2board version >= 1.7.0.")) 39 | //fmt.Println(Warn("The version have many changed for config, please check your config file")) 40 | } 41 | -------------------------------------------------------------------------------- /cmd/x25519.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/base64" 6 | "fmt" 7 | "strings" 8 | 9 | "github.com/InazumaV/V2bX/common/crypt" 10 | 11 | "github.com/spf13/cobra" 12 | "golang.org/x/crypto/curve25519" 13 | ) 14 | 15 | var x25519Command = cobra.Command{ 16 | Use: "x25519", 17 | Short: "Generate key pair for x25519 key exchange", 18 | Run: func(cmd *cobra.Command, args []string) { 19 | executeX25519() 20 | }, 21 | } 22 | 23 | func init() { 24 | command.AddCommand(&x25519Command) 25 | } 26 | 27 | func executeX25519() { 28 | var output string 29 | var err error 30 | defer func() { 31 | fmt.Println(output) 32 | }() 33 | var privateKey []byte 34 | var publicKey []byte 35 | var yes, key string 36 | fmt.Println("要基于节点信息生成密钥吗?(Y/n)") 37 | fmt.Scan(&yes) 38 | if strings.ToLower(yes) == "y" { 39 | var temp string 40 | fmt.Println("请输入节点id:") 41 | fmt.Scan(&temp) 42 | key = temp 43 | fmt.Println("请输入节点类型:") 44 | fmt.Scan(&temp) 45 | key += strings.ToLower(temp) 46 | fmt.Println("请输入Token:") 47 | fmt.Scan(&temp) 48 | key += temp 49 | privateKey = crypt.GenX25519Private([]byte(key)) 50 | } else { 51 | privateKey = make([]byte, curve25519.ScalarSize) 52 | if _, err = rand.Read(privateKey); err != nil { 53 | output = Err("read rand error: ", err) 54 | return 55 | } 56 | } 57 | if publicKey, err = curve25519.X25519(privateKey, curve25519.Basepoint); err != nil { 58 | output = Err("gen X25519 error: ", err) 59 | return 60 | } 61 | p := base64.RawURLEncoding.EncodeToString(privateKey) 62 | output = fmt.Sprint("Private key: ", 63 | p, 64 | "\nPublic key: ", 65 | base64.RawURLEncoding.EncodeToString(publicKey)) 66 | } 67 | -------------------------------------------------------------------------------- /cmd/x25519_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import "testing" 4 | 5 | func Test_executeX25519(t *testing.T) { 6 | executeX25519() 7 | } 8 | -------------------------------------------------------------------------------- /common/counter/conn.go: -------------------------------------------------------------------------------- 1 | package counter 2 | 3 | import ( 4 | "io" 5 | "net" 6 | 7 | "github.com/sagernet/sing/common/bufio" 8 | 9 | "github.com/sagernet/sing/common/buf" 10 | 11 | M "github.com/sagernet/sing/common/metadata" 12 | "github.com/sagernet/sing/common/network" 13 | ) 14 | 15 | type ConnCounter struct { 16 | network.ExtendedConn 17 | storage *TrafficStorage 18 | readFunc network.CountFunc 19 | writeFunc network.CountFunc 20 | } 21 | 22 | func NewConnCounter(conn net.Conn, s *TrafficStorage) net.Conn { 23 | return &ConnCounter{ 24 | ExtendedConn: bufio.NewExtendedConn(conn), 25 | storage: s, 26 | readFunc: func(n int64) { 27 | s.UpCounter.Add(n) 28 | }, 29 | writeFunc: func(n int64) { 30 | s.DownCounter.Add(n) 31 | }, 32 | } 33 | } 34 | 35 | func (c *ConnCounter) Read(b []byte) (n int, err error) { 36 | n, err = c.ExtendedConn.Read(b) 37 | c.storage.UpCounter.Store(int64(n)) 38 | return 39 | } 40 | 41 | func (c *ConnCounter) Write(b []byte) (n int, err error) { 42 | n, err = c.ExtendedConn.Write(b) 43 | c.storage.DownCounter.Store(int64(n)) 44 | return 45 | } 46 | 47 | func (c *ConnCounter) ReadBuffer(buffer *buf.Buffer) error { 48 | err := c.ExtendedConn.ReadBuffer(buffer) 49 | if err != nil { 50 | return err 51 | } 52 | if buffer.Len() > 0 { 53 | c.storage.UpCounter.Add(int64(buffer.Len())) 54 | } 55 | return nil 56 | } 57 | 58 | func (c *ConnCounter) WriteBuffer(buffer *buf.Buffer) error { 59 | dataLen := int64(buffer.Len()) 60 | err := c.ExtendedConn.WriteBuffer(buffer) 61 | if err != nil { 62 | return err 63 | } 64 | if dataLen > 0 { 65 | c.storage.DownCounter.Add(dataLen) 66 | } 67 | return nil 68 | } 69 | 70 | func (c *ConnCounter) UnwrapReader() (io.Reader, []network.CountFunc) { 71 | return c.ExtendedConn, []network.CountFunc{ 72 | c.readFunc, 73 | } 74 | } 75 | 76 | func (c *ConnCounter) UnwrapWriter() (io.Writer, []network.CountFunc) { 77 | return c.ExtendedConn, []network.CountFunc{ 78 | c.writeFunc, 79 | } 80 | } 81 | 82 | func (c *ConnCounter) Upstream() any { 83 | return c.ExtendedConn 84 | } 85 | 86 | type PacketConnCounter struct { 87 | network.PacketConn 88 | storage *TrafficStorage 89 | readFunc network.CountFunc 90 | writeFunc network.CountFunc 91 | } 92 | 93 | func NewPacketConnCounter(conn network.PacketConn, s *TrafficStorage) network.PacketConn { 94 | return &PacketConnCounter{ 95 | PacketConn: conn, 96 | storage: s, 97 | readFunc: func(n int64) { 98 | s.UpCounter.Add(n) 99 | }, 100 | writeFunc: func(n int64) { 101 | s.DownCounter.Add(n) 102 | }, 103 | } 104 | } 105 | 106 | func (p *PacketConnCounter) ReadPacket(buff *buf.Buffer) (destination M.Socksaddr, err error) { 107 | destination, err = p.PacketConn.ReadPacket(buff) 108 | if err != nil { 109 | return 110 | } 111 | p.storage.UpCounter.Add(int64(buff.Len())) 112 | return 113 | } 114 | 115 | func (p *PacketConnCounter) WritePacket(buff *buf.Buffer, destination M.Socksaddr) (err error) { 116 | n := buff.Len() 117 | err = p.PacketConn.WritePacket(buff, destination) 118 | if err != nil { 119 | return 120 | } 121 | if n > 0 { 122 | p.storage.DownCounter.Add(int64(n)) 123 | } 124 | return 125 | } 126 | 127 | func (p *PacketConnCounter) UnwrapPacketReader() (network.PacketReader, []network.CountFunc) { 128 | return p.PacketConn, []network.CountFunc{ 129 | p.readFunc, 130 | } 131 | } 132 | 133 | func (p *PacketConnCounter) UnwrapPacketWriter() (network.PacketWriter, []network.CountFunc) { 134 | return p.PacketConn, []network.CountFunc{ 135 | p.writeFunc, 136 | } 137 | } 138 | 139 | func (p *PacketConnCounter) Upstream() any { 140 | return p.PacketConn 141 | } 142 | -------------------------------------------------------------------------------- /common/counter/traffic.go: -------------------------------------------------------------------------------- 1 | package counter 2 | 3 | import ( 4 | "sync" 5 | "sync/atomic" 6 | ) 7 | 8 | type TrafficCounter struct { 9 | counters sync.Map 10 | } 11 | 12 | type TrafficStorage struct { 13 | UpCounter atomic.Int64 14 | DownCounter atomic.Int64 15 | } 16 | 17 | func NewTrafficCounter() *TrafficCounter { 18 | return &TrafficCounter{} 19 | } 20 | 21 | func (c *TrafficCounter) GetCounter(id string) *TrafficStorage { 22 | if cts, ok := c.counters.Load(id); ok { 23 | return cts.(*TrafficStorage) 24 | } 25 | newStorage := &TrafficStorage{} 26 | if cts, loaded := c.counters.LoadOrStore(id, newStorage); loaded { 27 | return cts.(*TrafficStorage) 28 | } 29 | return newStorage 30 | } 31 | 32 | func (c *TrafficCounter) GetUpCount(id string) int64 { 33 | if cts, ok := c.counters.Load(id); ok { 34 | return cts.(*TrafficStorage).UpCounter.Load() 35 | } 36 | return 0 37 | } 38 | 39 | func (c *TrafficCounter) GetDownCount(id string) int64 { 40 | if cts, ok := c.counters.Load(id); ok { 41 | return cts.(*TrafficStorage).DownCounter.Load() 42 | } 43 | return 0 44 | } 45 | 46 | func (c *TrafficCounter) Len() int { 47 | length := 0 48 | c.counters.Range(func(_, _ interface{}) bool { 49 | length++ 50 | return true 51 | }) 52 | return length 53 | } 54 | 55 | func (c *TrafficCounter) Reset(id string) { 56 | if cts, ok := c.counters.Load(id); ok { 57 | cts.(*TrafficStorage).UpCounter.Store(0) 58 | cts.(*TrafficStorage).DownCounter.Store(0) 59 | } 60 | } 61 | 62 | func (c *TrafficCounter) Delete(id string) { 63 | c.counters.Delete(id) 64 | } 65 | 66 | func (c *TrafficCounter) Rx(id string, n int) { 67 | cts := c.GetCounter(id) 68 | cts.DownCounter.Add(int64(n)) 69 | } 70 | 71 | func (c *TrafficCounter) Tx(id string, n int) { 72 | cts := c.GetCounter(id) 73 | cts.UpCounter.Add(int64(n)) 74 | } 75 | -------------------------------------------------------------------------------- /common/crypt/aes.go: -------------------------------------------------------------------------------- 1 | package crypt 2 | 3 | import ( 4 | "crypto/aes" 5 | "encoding/base64" 6 | ) 7 | 8 | func AesEncrypt(data []byte, key []byte) (string, error) { 9 | a, err := aes.NewCipher(key) 10 | if err != nil { 11 | return "", err 12 | } 13 | en := make([]byte, len(data)) 14 | a.Encrypt(en, data) 15 | return base64.StdEncoding.EncodeToString(en), nil 16 | } 17 | 18 | func AesDecrypt(data string, key []byte) (string, error) { 19 | d, err := base64.StdEncoding.DecodeString(data) 20 | if err != nil { 21 | return "", err 22 | } 23 | a, err := aes.NewCipher(key) 24 | if err != nil { 25 | return "", err 26 | } 27 | de := make([]byte, len(data)) 28 | a.Decrypt(de, d) 29 | return string(de), nil 30 | } 31 | -------------------------------------------------------------------------------- /common/crypt/x25519.go: -------------------------------------------------------------------------------- 1 | package crypt 2 | 3 | import ( 4 | "crypto/sha256" 5 | ) 6 | 7 | func GenX25519Private(data []byte) []byte { 8 | key := sha256.Sum256(data) 9 | key[0] &= 248 10 | key[31] &= 127 11 | key[31] |= 64 12 | return key[:32] 13 | } 14 | -------------------------------------------------------------------------------- /common/exec/exec.go: -------------------------------------------------------------------------------- 1 | package exec 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "os/exec" 7 | ) 8 | 9 | func RunCommandByShell(cmd string) (string, error) { 10 | e := exec.Command("bash", "-c", cmd) 11 | out, err := e.CombinedOutput() 12 | if errors.Unwrap(err) == exec.ErrNotFound { 13 | e = exec.Command("sh", "-c", cmd) 14 | out, err = e.CombinedOutput() 15 | } 16 | return string(out), err 17 | } 18 | 19 | func RunCommandStd(name string, args ...string) { 20 | e := exec.Command(name, args...) 21 | e.Stdout = os.Stdout 22 | e.Stdin = os.Stdin 23 | e.Stderr = os.Stderr 24 | _ = e.Run() 25 | } 26 | -------------------------------------------------------------------------------- /common/file/file.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import "os" 4 | 5 | func IsExist(path string) bool { 6 | _, err := os.Stat(path) 7 | return err == nil || !os.IsNotExist(err) 8 | } 9 | -------------------------------------------------------------------------------- /common/format/user.go: -------------------------------------------------------------------------------- 1 | package format 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | func UserTag(tag string, uuid string) string { 8 | return fmt.Sprintf("%s|%s", tag, uuid) 9 | } 10 | -------------------------------------------------------------------------------- /common/json5/json5.go: -------------------------------------------------------------------------------- 1 | package json5 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | ) 7 | 8 | type TrimNodeReader struct { 9 | r io.Reader 10 | br *bytes.Reader 11 | } 12 | 13 | func isNL(c byte) bool { 14 | return c == '\n' || c == '\r' 15 | } 16 | 17 | func isWS(c byte) bool { 18 | return c == ' ' || c == '\t' || isNL(c) 19 | } 20 | 21 | func consumeComment(s []byte, i int) int { 22 | if i < len(s) && s[i] == '/' { 23 | s[i-1] = ' ' 24 | for ; i < len(s) && !isNL(s[i]); i += 1 { 25 | s[i] = ' ' 26 | } 27 | } 28 | if i < len(s) && s[i] == '*' { 29 | s[i-1] = ' ' 30 | s[i] = ' ' 31 | for ; i < len(s); i += 1 { 32 | if s[i] != '*' { 33 | s[i] = ' ' 34 | } else { 35 | s[i] = ' ' 36 | i++ 37 | if i < len(s) { 38 | if s[i] == '/' { 39 | s[i] = ' ' 40 | break 41 | } 42 | } 43 | } 44 | } 45 | } 46 | return i 47 | } 48 | 49 | func prep(r io.Reader) (s []byte, err error) { 50 | buf := &bytes.Buffer{} 51 | _, err = io.Copy(buf, r) 52 | s = buf.Bytes() 53 | if err != nil { 54 | return 55 | } 56 | 57 | i := 0 58 | for i < len(s) { 59 | switch s[i] { 60 | case '"': 61 | i += 1 62 | for i < len(s) { 63 | if s[i] == '"' { 64 | i += 1 65 | break 66 | } else if s[i] == '\\' { 67 | i += 1 68 | } 69 | i += 1 70 | } 71 | case '/': 72 | i = consumeComment(s, i+1) 73 | case ',': 74 | j := i 75 | for { 76 | i += 1 77 | if i >= len(s) { 78 | break 79 | } else if s[i] == '}' || s[i] == ']' { 80 | s[j] = ' ' 81 | break 82 | } else if s[i] == '/' { 83 | i = consumeComment(s, i+1) 84 | } else if !isWS(s[i]) { 85 | break 86 | } 87 | } 88 | default: 89 | i += 1 90 | } 91 | } 92 | return 93 | } 94 | 95 | // Read acts as a proxy for the underlying reader and cleans p 96 | // of comments and trailing commas preceeding ] and } 97 | // comments are delimitted by // up until the end the line 98 | func (st *TrimNodeReader) Read(p []byte) (n int, err error) { 99 | if st.br == nil { 100 | var s []byte 101 | if s, err = prep(st.r); err != nil { 102 | return 103 | } 104 | st.br = bytes.NewReader(s) 105 | } 106 | return st.br.Read(p) 107 | } 108 | 109 | // NewTrimNodeReader New returns an io.Reader acting as proxy to r 110 | func NewTrimNodeReader(r io.Reader) io.Reader { 111 | return &TrimNodeReader{r: r} 112 | } 113 | -------------------------------------------------------------------------------- /common/rate/conn.go: -------------------------------------------------------------------------------- 1 | package rate 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/juju/ratelimit" 7 | ) 8 | 9 | func NewConnRateLimiter(c net.Conn, l *ratelimit.Bucket) *Conn { 10 | return &Conn{ 11 | Conn: c, 12 | limiter: l, 13 | } 14 | } 15 | 16 | type Conn struct { 17 | net.Conn 18 | limiter *ratelimit.Bucket 19 | } 20 | 21 | func (c *Conn) Read(b []byte) (n int, err error) { 22 | c.limiter.Wait(int64(len(b))) 23 | return c.Conn.Read(b) 24 | } 25 | 26 | func (c *Conn) Write(b []byte) (n int, err error) { 27 | c.limiter.Wait(int64(len(b))) 28 | return c.Conn.Write(b) 29 | } 30 | 31 | /* 32 | type PacketConnCounter struct { 33 | network.PacketConn 34 | limiter *ratelimit.Bucket 35 | } 36 | 37 | func NewPacketConnCounter(conn network.PacketConn, l *ratelimit.Bucket) network.PacketConn { 38 | return &PacketConnCounter{ 39 | PacketConn: conn, 40 | limiter: l, 41 | } 42 | } 43 | 44 | func (p *PacketConnCounter) ReadPacket(buff *buf.Buffer) (destination M.Socksaddr, err error) { 45 | pLen := buff.Len() 46 | destination, err = p.PacketConn.ReadPacket(buff) 47 | p.limiter.Wait(int64(buff.Len() - pLen)) 48 | return destination, err 49 | } 50 | 51 | func (p *PacketConnCounter) WritePacket(buff *buf.Buffer, destination M.Socksaddr) (err error) { 52 | p.limiter.Wait(int64(buff.Len())) 53 | return p.PacketConn.WritePacket(buff, destination) 54 | } 55 | */ 56 | -------------------------------------------------------------------------------- /common/rate/writer.go: -------------------------------------------------------------------------------- 1 | package rate 2 | 3 | import ( 4 | "github.com/juju/ratelimit" 5 | "github.com/xtls/xray-core/common" 6 | "github.com/xtls/xray-core/common/buf" 7 | ) 8 | 9 | type Writer struct { 10 | writer buf.Writer 11 | limiter *ratelimit.Bucket 12 | } 13 | 14 | func NewRateLimitWriter(writer buf.Writer, limiter *ratelimit.Bucket) buf.Writer { 15 | return &Writer{ 16 | writer: writer, 17 | limiter: limiter, 18 | } 19 | } 20 | 21 | func (w *Writer) Close() error { 22 | return common.Close(w.writer) 23 | } 24 | 25 | func (w *Writer) WriteMultiBuffer(mb buf.MultiBuffer) error { 26 | w.limiter.Wait(int64(mb.Len())) 27 | return w.writer.WriteMultiBuffer(mb) 28 | } 29 | -------------------------------------------------------------------------------- /common/systime/time_stub.go: -------------------------------------------------------------------------------- 1 | //go:build !(windows || linux || darwin) 2 | 3 | package systime 4 | 5 | import ( 6 | "os" 7 | "time" 8 | ) 9 | 10 | func SetSystemTime(nowTime time.Time) error { 11 | return os.ErrInvalid 12 | } 13 | -------------------------------------------------------------------------------- /common/systime/time_unix.go: -------------------------------------------------------------------------------- 1 | //go:build linux || darwin 2 | 3 | package systime 4 | 5 | import ( 6 | "time" 7 | 8 | "golang.org/x/sys/unix" 9 | ) 10 | 11 | func SetSystemTime(nowTime time.Time) error { 12 | timeVal := unix.NsecToTimeval(nowTime.UnixNano()) 13 | return unix.Settimeofday(&timeVal) 14 | } 15 | -------------------------------------------------------------------------------- /common/systime/time_windows.go: -------------------------------------------------------------------------------- 1 | package systime 2 | 3 | import ( 4 | "time" 5 | "unsafe" 6 | 7 | "golang.org/x/sys/windows" 8 | ) 9 | 10 | func SetSystemTime(nowTime time.Time) error { 11 | var systemTime windows.Systemtime 12 | systemTime.Year = uint16(nowTime.Year()) 13 | systemTime.Month = uint16(nowTime.Month()) 14 | systemTime.Day = uint16(nowTime.Day()) 15 | systemTime.Hour = uint16(nowTime.Hour()) 16 | systemTime.Minute = uint16(nowTime.Minute()) 17 | systemTime.Second = uint16(nowTime.Second()) 18 | systemTime.Milliseconds = uint16(nowTime.UnixMilli() - nowTime.Unix()*1000) 19 | 20 | dllKernel32 := windows.NewLazySystemDLL("kernel32.dll") 21 | proc := dllKernel32.NewProc("SetSystemTime") 22 | 23 | _, _, err := proc.Call( 24 | uintptr(unsafe.Pointer(&systemTime)), 25 | ) 26 | 27 | if err != nil && err.Error() != "The operation completed successfully." { 28 | return err 29 | } 30 | 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /common/task/task.go: -------------------------------------------------------------------------------- 1 | package task 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | // Task is a task that runs periodically. 9 | type Task struct { 10 | // Interval of the task being run 11 | Interval time.Duration 12 | // Execute is the task function 13 | Execute func() error 14 | 15 | access sync.Mutex 16 | timer *time.Timer 17 | running bool 18 | } 19 | 20 | func (t *Task) hasClosed() bool { 21 | t.access.Lock() 22 | defer t.access.Unlock() 23 | 24 | return !t.running 25 | } 26 | 27 | func (t *Task) checkedExecute(first bool) error { 28 | if t.hasClosed() { 29 | return nil 30 | } 31 | 32 | t.access.Lock() 33 | defer t.access.Unlock() 34 | if first { 35 | if err := t.Execute(); err != nil { 36 | t.running = false 37 | return err 38 | } 39 | } 40 | if !t.running { 41 | return nil 42 | } 43 | t.timer = time.AfterFunc(t.Interval, func() { 44 | t.checkedExecute(true) 45 | }) 46 | 47 | return nil 48 | } 49 | 50 | // Start implements common.Runnable. 51 | func (t *Task) Start(first bool) error { 52 | t.access.Lock() 53 | if t.running { 54 | t.access.Unlock() 55 | return nil 56 | } 57 | t.running = true 58 | t.access.Unlock() 59 | if err := t.checkedExecute(first); err != nil { 60 | t.access.Lock() 61 | t.running = false 62 | t.access.Unlock() 63 | return err 64 | } 65 | return nil 66 | } 67 | 68 | // Close implements common.Closable. 69 | func (t *Task) Close() { 70 | t.access.Lock() 71 | defer t.access.Unlock() 72 | 73 | t.running = false 74 | if t.timer != nil { 75 | t.timer.Stop() 76 | t.timer = nil 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /common/task/task_test.go: -------------------------------------------------------------------------------- 1 | package task 2 | 3 | import ( 4 | "log" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestTask(t *testing.T) { 10 | ts := Task{Execute: func() error { 11 | log.Println("q") 12 | return nil 13 | }, Interval: time.Second} 14 | ts.Start(false) 15 | } 16 | -------------------------------------------------------------------------------- /conf/cert.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | type CertConfig struct { 4 | CertMode string `json:"CertMode"` // none, file, http, dns 5 | RejectUnknownSni bool `json:"RejectUnknownSni"` 6 | CertDomain string `json:"CertDomain"` 7 | CertFile string `json:"CertFile"` 8 | KeyFile string `json:"KeyFile"` 9 | Provider string `json:"Provider"` // alidns, cloudflare, gandi, godaddy.... 10 | Email string `json:"Email"` 11 | DNSEnv map[string]string `json:"DNSEnv"` 12 | } 13 | 14 | func NewCertConfig() *CertConfig { 15 | return &CertConfig{ 16 | CertMode: "none", 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /conf/conf.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | 8 | "github.com/InazumaV/V2bX/common/json5" 9 | 10 | "github.com/goccy/go-json" 11 | ) 12 | 13 | type Conf struct { 14 | LogConfig LogConfig `json:"Log"` 15 | CoresConfig []CoreConfig `json:"Cores"` 16 | NodeConfig []NodeConfig `json:"Nodes"` 17 | } 18 | 19 | func New() *Conf { 20 | return &Conf{ 21 | LogConfig: LogConfig{ 22 | Level: "info", 23 | Output: "", 24 | }, 25 | } 26 | } 27 | 28 | func (p *Conf) LoadFromPath(filePath string) error { 29 | f, err := os.Open(filePath) 30 | if err != nil { 31 | return fmt.Errorf("open config file error: %s", err) 32 | } 33 | defer f.Close() 34 | 35 | reader := json5.NewTrimNodeReader(f) 36 | data, err := io.ReadAll(reader) 37 | if err != nil { 38 | return fmt.Errorf("read config file error: %s", err) 39 | } 40 | 41 | err = json.Unmarshal(data, p) 42 | if err != nil { 43 | return fmt.Errorf("unmarshal config error: %s", err) 44 | } 45 | 46 | return nil 47 | } 48 | -------------------------------------------------------------------------------- /conf/conf_test.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestConf_LoadFromPath(t *testing.T) { 8 | c := New() 9 | t.Log(c.LoadFromPath("../example/config.json"), c.NodeConfig) 10 | } 11 | 12 | func TestConf_Watch(t *testing.T) { 13 | c := New() 14 | t.Log(c.Watch("./1.json", "", "", func() {})) 15 | select {} 16 | } 17 | -------------------------------------------------------------------------------- /conf/core.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | type CoreConfig struct { 8 | Type string `json:"Type"` 9 | Name string `json:"Name"` 10 | XrayConfig *XrayConfig `json:"-"` 11 | SingConfig *SingConfig `json:"-"` 12 | Hysteria2Config *Hysteria2Config `json:"-"` 13 | } 14 | 15 | type _CoreConfig CoreConfig 16 | 17 | func (c *CoreConfig) UnmarshalJSON(b []byte) error { 18 | err := json.Unmarshal(b, (*_CoreConfig)(c)) 19 | if err != nil { 20 | return err 21 | } 22 | switch c.Type { 23 | case "xray": 24 | c.XrayConfig = NewXrayConfig() 25 | return json.Unmarshal(b, c.XrayConfig) 26 | case "sing": 27 | c.SingConfig = NewSingConfig() 28 | return json.Unmarshal(b, c.SingConfig) 29 | case "hysteria2": 30 | c.Hysteria2Config = NewHysteria2Config() 31 | return json.Unmarshal(b, c.Hysteria2Config) 32 | } 33 | return nil 34 | } 35 | -------------------------------------------------------------------------------- /conf/hy.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | type Hysteria2Config struct { 4 | LogConfig Hysteria2LogConfig `json:"Log"` 5 | } 6 | 7 | type Hysteria2LogConfig struct { 8 | Level string `json:"Level"` 9 | } 10 | 11 | func NewHysteria2Config() *Hysteria2Config { 12 | return &Hysteria2Config{ 13 | LogConfig: Hysteria2LogConfig{ 14 | Level: "error", 15 | }, 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /conf/limit.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | type LimitConfig struct { 4 | EnableRealtime bool `json:"EnableRealtime"` 5 | SpeedLimit int `json:"SpeedLimit"` 6 | IPLimit int `json:"DeviceLimit"` 7 | ConnLimit int `json:"ConnLimit"` 8 | EnableIpRecorder bool `json:"EnableIpRecorder"` 9 | IpRecorderConfig *IpReportConfig `json:"IpRecorderConfig"` 10 | EnableDynamicSpeedLimit bool `json:"EnableDynamicSpeedLimit"` 11 | DynamicSpeedLimitConfig *DynamicSpeedLimitConfig `json:"DynamicSpeedLimitConfig"` 12 | } 13 | 14 | type RecorderConfig struct { 15 | Url string `json:"Url"` 16 | Token string `json:"Token"` 17 | Timeout int `json:"Timeout"` 18 | } 19 | 20 | type RedisConfig struct { 21 | Address string `json:"Address"` 22 | Password string `json:"Password"` 23 | Db int `json:"Db"` 24 | Expiry int `json:"Expiry"` 25 | } 26 | 27 | type IpReportConfig struct { 28 | Periodic int `json:"Periodic"` 29 | Type string `json:"Type"` 30 | RecorderConfig *RecorderConfig `json:"RecorderConfig"` 31 | RedisConfig *RedisConfig `json:"RedisConfig"` 32 | EnableIpSync bool `json:"EnableIpSync"` 33 | } 34 | 35 | type DynamicSpeedLimitConfig struct { 36 | Periodic int `json:"Periodic"` 37 | Traffic int64 `json:"Traffic"` 38 | SpeedLimit int `json:"SpeedLimit"` 39 | ExpireTime int `json:"ExpireTime"` 40 | } 41 | -------------------------------------------------------------------------------- /conf/log.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | type LogConfig struct { 4 | Level string `json:"Level"` 5 | Output string `json:"Output"` 6 | } 7 | -------------------------------------------------------------------------------- /conf/node.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | "os" 8 | "strings" 9 | 10 | "github.com/InazumaV/V2bX/common/json5" 11 | "github.com/goccy/go-json" 12 | ) 13 | 14 | type NodeConfig struct { 15 | ApiConfig ApiConfig `json:"-"` 16 | Options Options `json:"-"` 17 | } 18 | 19 | type rawNodeConfig struct { 20 | Include string `json:"Include"` 21 | ApiRaw json.RawMessage `json:"ApiConfig"` 22 | OptRaw json.RawMessage `json:"Options"` 23 | } 24 | 25 | type ApiConfig struct { 26 | APIHost string `json:"ApiHost"` 27 | NodeID int `json:"NodeID"` 28 | Key string `json:"ApiKey"` 29 | NodeType string `json:"NodeType"` 30 | Timeout int `json:"Timeout"` 31 | RuleListPath string `json:"RuleListPath"` 32 | } 33 | 34 | func (n *NodeConfig) UnmarshalJSON(data []byte) (err error) { 35 | rn := rawNodeConfig{} 36 | err = json.Unmarshal(data, &rn) 37 | if err != nil { 38 | return err 39 | } 40 | if len(rn.Include) != 0 { 41 | file, _ := strings.CutPrefix(rn.Include, ":") 42 | switch file { 43 | case "http", "https": 44 | rsp, err := http.Get(file) 45 | if err != nil { 46 | return err 47 | } 48 | defer rsp.Body.Close() 49 | data, err = io.ReadAll(json5.NewTrimNodeReader(rsp.Body)) 50 | if err != nil { 51 | return fmt.Errorf("open include file error: %s", err) 52 | } 53 | default: 54 | f, err := os.Open(rn.Include) 55 | if err != nil { 56 | return fmt.Errorf("open include file error: %s", err) 57 | } 58 | defer f.Close() 59 | data, err = io.ReadAll(json5.NewTrimNodeReader(f)) 60 | if err != nil { 61 | return fmt.Errorf("open include file error: %s", err) 62 | } 63 | } 64 | err = json.Unmarshal(data, &rn) 65 | if err != nil { 66 | return fmt.Errorf("unmarshal include file error: %s", err) 67 | } 68 | } 69 | 70 | n.ApiConfig = ApiConfig{ 71 | APIHost: "http://127.0.0.1", 72 | Timeout: 30, 73 | } 74 | if len(rn.ApiRaw) > 0 { 75 | err = json.Unmarshal(rn.ApiRaw, &n.ApiConfig) 76 | if err != nil { 77 | return 78 | } 79 | } else { 80 | err = json.Unmarshal(data, &n.ApiConfig) 81 | if err != nil { 82 | return 83 | } 84 | } 85 | 86 | n.Options = Options{ 87 | ListenIP: "0.0.0.0", 88 | SendIP: "0.0.0.0", 89 | CertConfig: NewCertConfig(), 90 | } 91 | if len(rn.OptRaw) > 0 { 92 | err = json.Unmarshal(rn.OptRaw, &n.Options) 93 | if err != nil { 94 | return 95 | } 96 | } else { 97 | err = json.Unmarshal(data, &n.Options) 98 | if err != nil { 99 | return 100 | } 101 | } 102 | return 103 | } 104 | 105 | type Options struct { 106 | Name string `json:"Name"` 107 | Core string `json:"Core"` 108 | CoreName string `json:"CoreName"` 109 | ListenIP string `json:"ListenIP"` 110 | SendIP string `json:"SendIP"` 111 | DeviceOnlineMinTraffic int64 `json:"DeviceOnlineMinTraffic"` 112 | LimitConfig LimitConfig `json:"LimitConfig"` 113 | RawOptions json.RawMessage `json:"RawOptions"` 114 | XrayOptions *XrayOptions `json:"XrayOptions"` 115 | SingOptions *SingOptions `json:"SingOptions"` 116 | Hysteria2ConfigPath string `json:"Hysteria2ConfigPath"` 117 | CertConfig *CertConfig `json:"CertConfig"` 118 | } 119 | 120 | func (o *Options) UnmarshalJSON(data []byte) error { 121 | type opt Options 122 | err := json.Unmarshal(data, (*opt)(o)) 123 | if err != nil { 124 | return err 125 | } 126 | switch o.Core { 127 | case "xray": 128 | o.XrayOptions = NewXrayOptions() 129 | return json.Unmarshal(data, o.XrayOptions) 130 | case "sing": 131 | o.SingOptions = NewSingOptions() 132 | return json.Unmarshal(data, o.SingOptions) 133 | case "hysteria2": 134 | o.RawOptions = data 135 | return nil 136 | default: 137 | o.Core = "" 138 | o.RawOptions = data 139 | } 140 | return nil 141 | } 142 | -------------------------------------------------------------------------------- /conf/sing.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "github.com/sagernet/sing-box/option" 5 | ) 6 | 7 | type SingConfig struct { 8 | LogConfig SingLogConfig `json:"Log"` 9 | NtpConfig SingNtpConfig `json:"NTP"` 10 | OriginalPath string `json:"OriginalPath"` 11 | } 12 | 13 | type SingLogConfig struct { 14 | Disabled bool `json:"Disable"` 15 | Level string `json:"Level"` 16 | Output string `json:"Output"` 17 | Timestamp bool `json:"Timestamp"` 18 | } 19 | 20 | func NewSingConfig() *SingConfig { 21 | return &SingConfig{ 22 | LogConfig: SingLogConfig{ 23 | Level: "error", 24 | Timestamp: true, 25 | }, 26 | NtpConfig: SingNtpConfig{ 27 | Enable: false, 28 | Server: "time.apple.com", 29 | ServerPort: 0, 30 | }, 31 | } 32 | } 33 | 34 | type SingOptions struct { 35 | TCPFastOpen bool `json:"EnableTFO"` 36 | SniffEnabled bool `json:"EnableSniff"` 37 | SniffOverrideDestination bool `json:"SniffOverrideDestination"` 38 | EnableDNS bool `json:"EnableDNS"` 39 | DomainStrategy option.DomainStrategy `json:"DomainStrategy"` 40 | FallBackConfigs *FallBackConfigForSing `json:"FallBackConfigs"` 41 | Multiplex *MultiplexConfig `json:"MultiplexConfig"` 42 | } 43 | 44 | type SingNtpConfig struct { 45 | Enable bool `json:"Enable"` 46 | Server string `json:"Server"` 47 | ServerPort uint16 `json:"ServerPort"` 48 | } 49 | 50 | type FallBackConfigForSing struct { 51 | // sing-box 52 | FallBack FallBack `json:"FallBack"` 53 | FallBackForALPN map[string]FallBack `json:"FallBackForALPN"` 54 | } 55 | 56 | type FallBack struct { 57 | Server string `json:"Server"` 58 | ServerPort string `json:"ServerPort"` 59 | } 60 | 61 | type MultiplexConfig struct { 62 | Enabled bool `json:"Enable"` 63 | Padding bool `json:"Padding"` 64 | Brutal BrutalOptions `json:"Brutal"` 65 | } 66 | 67 | type BrutalOptions struct { 68 | Enabled bool `json:"Enable"` 69 | UpMbps int `json:"UpMbps"` 70 | DownMbps int `json:"DownMbps"` 71 | } 72 | 73 | func NewSingOptions() *SingOptions { 74 | return &SingOptions{ 75 | EnableDNS: false, 76 | TCPFastOpen: false, 77 | SniffEnabled: true, 78 | SniffOverrideDestination: true, 79 | FallBackConfigs: &FallBackConfigForSing{}, 80 | Multiplex: &MultiplexConfig{}, 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /conf/watch.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "path/filepath" 7 | "strings" 8 | "time" 9 | 10 | "github.com/fsnotify/fsnotify" 11 | ) 12 | 13 | func (p *Conf) Watch(filePath, xDnsPath string, sDnsPath string, reload func()) error { 14 | watcher, err := fsnotify.NewWatcher() 15 | if err != nil { 16 | return fmt.Errorf("new watcher error: %s", err) 17 | } 18 | go func() { 19 | var pre time.Time 20 | defer watcher.Close() 21 | for { 22 | select { 23 | case e := <-watcher.Events: 24 | if e.Has(fsnotify.Chmod) { 25 | continue 26 | } 27 | if pre.Add(10 * time.Second).After(time.Now()) { 28 | continue 29 | } 30 | pre = time.Now() 31 | go func() { 32 | time.Sleep(5 * time.Second) 33 | switch filepath.Base(strings.TrimSuffix(e.Name, "~")) { 34 | case filepath.Base(xDnsPath), filepath.Base(sDnsPath): 35 | log.Println("DNS file changed, reloading...") 36 | default: 37 | log.Println("config file changed, reloading...") 38 | } 39 | *p = *New() 40 | err := p.LoadFromPath(filePath) 41 | if err != nil { 42 | log.Printf("reload config error: %s", err) 43 | } 44 | reload() 45 | log.Println("reload config success") 46 | }() 47 | case err := <-watcher.Errors: 48 | if err != nil { 49 | log.Printf("File watcher error: %s", err) 50 | } 51 | } 52 | } 53 | }() 54 | err = watcher.Add(filePath) 55 | if err != nil { 56 | return fmt.Errorf("watch file error: %s", err) 57 | } 58 | if xDnsPath != "" { 59 | err = watcher.Add(xDnsPath) 60 | if err != nil { 61 | return fmt.Errorf("watch dns file error: %s", err) 62 | } 63 | } 64 | if sDnsPath != "" { 65 | err = watcher.Add(sDnsPath) 66 | if err != nil { 67 | return fmt.Errorf("watch dns file error: %s", err) 68 | } 69 | } 70 | return nil 71 | } 72 | -------------------------------------------------------------------------------- /conf/xray.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | type XrayConfig struct { 4 | LogConfig *XrayLogConfig `json:"Log"` 5 | AssetPath string `json:"AssetPath"` 6 | DnsConfigPath string `json:"DnsConfigPath"` 7 | RouteConfigPath string `json:"RouteConfigPath"` 8 | ConnectionConfig *XrayConnectionConfig `json:"XrayConnectionConfig"` 9 | InboundConfigPath string `json:"InboundConfigPath"` 10 | OutboundConfigPath string `json:"OutboundConfigPath"` 11 | } 12 | 13 | type XrayLogConfig struct { 14 | Level string `json:"Level"` 15 | AccessPath string `json:"AccessPath"` 16 | ErrorPath string `json:"ErrorPath"` 17 | } 18 | 19 | type XrayConnectionConfig struct { 20 | Handshake uint32 `json:"handshake"` 21 | ConnIdle uint32 `json:"connIdle"` 22 | UplinkOnly uint32 `json:"uplinkOnly"` 23 | DownlinkOnly uint32 `json:"downlinkOnly"` 24 | BufferSize int32 `json:"bufferSize"` 25 | } 26 | 27 | func NewXrayConfig() *XrayConfig { 28 | return &XrayConfig{ 29 | LogConfig: &XrayLogConfig{ 30 | Level: "warning", 31 | AccessPath: "", 32 | ErrorPath: "", 33 | }, 34 | AssetPath: "/etc/V2bX/", 35 | DnsConfigPath: "", 36 | InboundConfigPath: "", 37 | OutboundConfigPath: "", 38 | RouteConfigPath: "", 39 | ConnectionConfig: &XrayConnectionConfig{ 40 | Handshake: 4, 41 | ConnIdle: 30, 42 | UplinkOnly: 2, 43 | DownlinkOnly: 4, 44 | BufferSize: 64, 45 | }, 46 | } 47 | } 48 | 49 | type XrayOptions struct { 50 | EnableProxyProtocol bool `json:"EnableProxyProtocol"` 51 | EnableDNS bool `json:"EnableDNS"` 52 | DNSType string `json:"DNSType"` 53 | EnableUot bool `json:"EnableUot"` 54 | EnableTFO bool `json:"EnableTFO"` 55 | DisableIVCheck bool `json:"DisableIVCheck"` 56 | DisableSniffing bool `json:"DisableSniffing"` 57 | EnableFallback bool `json:"EnableFallback"` 58 | FallBackConfigs []FallBackConfigForXray `json:"FallBackConfigs"` 59 | } 60 | 61 | type FallBackConfigForXray struct { 62 | SNI string `json:"SNI"` 63 | Alpn string `json:"Alpn"` 64 | Path string `json:"Path"` 65 | Dest string `json:"Dest"` 66 | ProxyProtocolVer uint64 `json:"ProxyProtocolVer"` 67 | } 68 | 69 | func NewXrayOptions() *XrayOptions { 70 | return &XrayOptions{ 71 | EnableProxyProtocol: false, 72 | EnableDNS: false, 73 | DNSType: "AsIs", 74 | EnableUot: false, 75 | EnableTFO: false, 76 | DisableIVCheck: false, 77 | DisableSniffing: false, 78 | EnableFallback: false, 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /core/core.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/InazumaV/V2bX/conf" 7 | ) 8 | 9 | var ( 10 | cores = map[string]func(c *conf.CoreConfig) (Core, error){} 11 | ) 12 | 13 | func NewCore(c []conf.CoreConfig) (Core, error) { 14 | if len(c) < 0 { 15 | return nil, errors.New("no have vail core") 16 | } 17 | // multi core 18 | if len(c) > 1 { 19 | return NewSelector(c) 20 | } 21 | // one core 22 | if f, ok := cores[c[0].Type]; ok { 23 | return f(&c[0]) 24 | } else { 25 | return nil, errors.New("unknown core type") 26 | } 27 | } 28 | 29 | func RegisterCore(t string, f func(c *conf.CoreConfig) (Core, error)) { 30 | cores[t] = f 31 | } 32 | 33 | func RegisteredCore() []string { 34 | cs := make([]string, 0, len(cores)) 35 | for k := range cores { 36 | cs = append(cs, k) 37 | } 38 | return cs 39 | } 40 | -------------------------------------------------------------------------------- /core/hy2/geoloader.go: -------------------------------------------------------------------------------- 1 | package hy2 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | "os" 8 | "time" 9 | 10 | "github.com/apernet/hysteria/extras/v2/outbounds/acl" 11 | "github.com/apernet/hysteria/extras/v2/outbounds/acl/v2geo" 12 | "go.uber.org/zap" 13 | ) 14 | 15 | const ( 16 | geoipFilename = "geoip.dat" 17 | geoipURL = "https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/geoip.dat" 18 | geositeFilename = "geosite.dat" 19 | geositeURL = "https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/geosite.dat" 20 | geoDlTmpPattern = ".hysteria-geoloader.dlpart.*" 21 | 22 | geoDefaultUpdateInterval = 7 * 24 * time.Hour // 7 days 23 | ) 24 | 25 | var _ acl.GeoLoader = (*GeoLoader)(nil) 26 | 27 | // GeoLoader provides the on-demand GeoIP/GeoSite database 28 | // loading functionality required by the ACL engine. 29 | // Empty filenames = automatic download from built-in URLs. 30 | type GeoLoader struct { 31 | GeoIPFilename string 32 | GeoSiteFilename string 33 | UpdateInterval time.Duration 34 | 35 | geoipMap map[string]*v2geo.GeoIP 36 | geositeMap map[string]*v2geo.GeoSite 37 | 38 | Logger *zap.Logger 39 | } 40 | 41 | func (l *GeoLoader) shouldDownload(filename string) bool { 42 | info, err := os.Stat(filename) 43 | if os.IsNotExist(err) { 44 | return true 45 | } 46 | if info.Size() == 0 { 47 | // empty files are loadable by v2geo, but we consider it broken 48 | return true 49 | } 50 | dt := time.Since(info.ModTime()) 51 | if l.UpdateInterval == 0 { 52 | return dt > geoDefaultUpdateInterval 53 | } else { 54 | return dt > l.UpdateInterval 55 | } 56 | } 57 | 58 | func (l *GeoLoader) downloadAndCheck(filename, url string, checkFunc func(filename string) error) error { 59 | l.geoDownloadFunc(filename, url) 60 | 61 | resp, err := http.Get(url) 62 | if err != nil { 63 | l.geoDownloadErrFunc(err) 64 | return err 65 | } 66 | defer resp.Body.Close() 67 | 68 | f, err := os.CreateTemp(".", geoDlTmpPattern) 69 | if err != nil { 70 | l.geoDownloadErrFunc(err) 71 | return err 72 | } 73 | defer os.Remove(f.Name()) 74 | 75 | _, err = io.Copy(f, resp.Body) 76 | if err != nil { 77 | f.Close() 78 | l.geoDownloadErrFunc(err) 79 | return err 80 | } 81 | f.Close() 82 | 83 | err = checkFunc(f.Name()) 84 | if err != nil { 85 | l.geoDownloadErrFunc(fmt.Errorf("integrity check failed: %w", err)) 86 | return err 87 | } 88 | 89 | err = os.Rename(f.Name(), filename) 90 | if err != nil { 91 | l.geoDownloadErrFunc(fmt.Errorf("rename failed: %w", err)) 92 | return err 93 | } 94 | 95 | return nil 96 | } 97 | func (l *GeoLoader) LoadGeoIP() (map[string]*v2geo.GeoIP, error) { 98 | if l.geoipMap != nil { 99 | return l.geoipMap, nil 100 | } 101 | autoDL := false 102 | filename := l.GeoIPFilename 103 | if filename == "" { 104 | autoDL = true 105 | filename = geoipFilename 106 | } 107 | if autoDL { 108 | if !l.shouldDownload(filename) { 109 | m, err := v2geo.LoadGeoIP(filename) 110 | if err == nil { 111 | l.geoipMap = m 112 | return m, nil 113 | } 114 | // file is broken, download it again 115 | } 116 | err := l.downloadAndCheck(filename, geoipURL, func(filename string) error { 117 | _, err := v2geo.LoadGeoIP(filename) 118 | return err 119 | }) 120 | if err != nil { 121 | // as long as the previous download exists, fallback to it 122 | if _, serr := os.Stat(filename); os.IsNotExist(serr) { 123 | return nil, err 124 | } 125 | } 126 | } 127 | m, err := v2geo.LoadGeoIP(filename) 128 | if err != nil { 129 | return nil, err 130 | } 131 | l.geoipMap = m 132 | return m, nil 133 | } 134 | 135 | func (l *GeoLoader) LoadGeoSite() (map[string]*v2geo.GeoSite, error) { 136 | if l.geositeMap != nil { 137 | return l.geositeMap, nil 138 | } 139 | autoDL := false 140 | filename := l.GeoSiteFilename 141 | if filename == "" { 142 | autoDL = true 143 | filename = geositeFilename 144 | } 145 | if autoDL { 146 | if !l.shouldDownload(filename) { 147 | m, err := v2geo.LoadGeoSite(filename) 148 | if err == nil { 149 | l.geositeMap = m 150 | return m, nil 151 | } 152 | // file is broken, download it again 153 | } 154 | err := l.downloadAndCheck(filename, geositeURL, func(filename string) error { 155 | _, err := v2geo.LoadGeoSite(filename) 156 | return err 157 | }) 158 | if err != nil { 159 | // as long as the previous download exists, fallback to it 160 | if _, serr := os.Stat(filename); os.IsNotExist(serr) { 161 | return nil, err 162 | } 163 | } 164 | } 165 | m, err := v2geo.LoadGeoSite(filename) 166 | if err != nil { 167 | return nil, err 168 | } 169 | l.geositeMap = m 170 | return m, nil 171 | } 172 | 173 | func (l *GeoLoader) geoDownloadFunc(filename, url string) { 174 | l.Logger.Info("downloading database", zap.String("filename", filename), zap.String("url", url)) 175 | } 176 | 177 | func (l *GeoLoader) geoDownloadErrFunc(err error) { 178 | if err != nil { 179 | l.Logger.Error("failed to download database", zap.Error(err)) 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /core/hy2/hook.go: -------------------------------------------------------------------------------- 1 | package hy2 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/InazumaV/V2bX/common/counter" 7 | "github.com/InazumaV/V2bX/common/format" 8 | "github.com/InazumaV/V2bX/limiter" 9 | "github.com/apernet/hysteria/core/v2/server" 10 | quic "github.com/apernet/quic-go" 11 | "go.uber.org/zap" 12 | ) 13 | 14 | var _ server.TrafficLogger = (*HookServer)(nil) 15 | 16 | type HookServer struct { 17 | Tag string 18 | logger *zap.Logger 19 | Counter sync.Map 20 | } 21 | 22 | func (h *HookServer) TraceStream(stream quic.Stream, stats *server.StreamStats) { 23 | } 24 | 25 | func (h *HookServer) UntraceStream(stream quic.Stream) { 26 | } 27 | 28 | func (h *HookServer) LogTraffic(id string, tx, rx uint64) (ok bool) { 29 | var c interface{} 30 | var exists bool 31 | 32 | limiterinfo, err := limiter.GetLimiter(h.Tag) 33 | if err != nil { 34 | h.logger.Error("Get limiter error", zap.String("tag", h.Tag), zap.Error(err)) 35 | return false 36 | } 37 | 38 | userLimit, ok := limiterinfo.UserLimitInfo.Load(format.UserTag(h.Tag, id)) 39 | if ok { 40 | userlimitInfo := userLimit.(*limiter.UserLimitInfo) 41 | if userlimitInfo.OverLimit { 42 | userlimitInfo.OverLimit = false 43 | return false 44 | } 45 | } 46 | 47 | if c, exists = h.Counter.Load(h.Tag); !exists { 48 | c = counter.NewTrafficCounter() 49 | h.Counter.Store(h.Tag, c) 50 | } 51 | 52 | if tc, ok := c.(*counter.TrafficCounter); ok { 53 | tc.Rx(id, int(rx)) 54 | tc.Tx(id, int(tx)) 55 | return true 56 | } 57 | 58 | return false 59 | } 60 | 61 | func (s *HookServer) LogOnlineState(id string, online bool) { 62 | } 63 | -------------------------------------------------------------------------------- /core/hy2/hy2.go: -------------------------------------------------------------------------------- 1 | package hy2 2 | 3 | import ( 4 | "github.com/InazumaV/V2bX/conf" 5 | vCore "github.com/InazumaV/V2bX/core" 6 | "go.uber.org/zap" 7 | ) 8 | 9 | var _ vCore.Core = (*Hysteria2)(nil) 10 | 11 | type Hysteria2 struct { 12 | Hy2nodes map[string]Hysteria2node 13 | Auth *V2bX 14 | Logger *zap.Logger 15 | } 16 | 17 | func init() { 18 | vCore.RegisterCore("hysteria2", New) 19 | } 20 | 21 | func New(c *conf.CoreConfig) (vCore.Core, error) { 22 | loglever := "error" 23 | if c.Hysteria2Config.LogConfig.Level != "" { 24 | loglever = c.Hysteria2Config.LogConfig.Level 25 | } 26 | log, err := initLogger(loglever, "console") 27 | if err != nil { 28 | return nil, err 29 | } 30 | return &Hysteria2{ 31 | Hy2nodes: make(map[string]Hysteria2node), 32 | Auth: &V2bX{ 33 | usersMap: make(map[string]int), 34 | }, 35 | Logger: log, 36 | }, nil 37 | } 38 | 39 | func (h *Hysteria2) Protocols() []string { 40 | return []string{ 41 | "hysteria2", 42 | } 43 | } 44 | 45 | func (h *Hysteria2) Start() error { 46 | return nil 47 | } 48 | 49 | func (h *Hysteria2) Close() error { 50 | for _, n := range h.Hy2nodes { 51 | err := n.Hy2server.Close() 52 | if err != nil { 53 | return err 54 | } 55 | } 56 | return nil 57 | } 58 | 59 | func (h *Hysteria2) Type() string { 60 | return "hysteria2" 61 | } 62 | -------------------------------------------------------------------------------- /core/hy2/logger.go: -------------------------------------------------------------------------------- 1 | package hy2 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "strings" 7 | 8 | "github.com/InazumaV/V2bX/common/format" 9 | "github.com/InazumaV/V2bX/limiter" 10 | "go.uber.org/zap" 11 | "go.uber.org/zap/zapcore" 12 | ) 13 | 14 | type serverLogger struct { 15 | Tag string 16 | logger *zap.Logger 17 | } 18 | 19 | var logLevelMap = map[string]zapcore.Level{ 20 | "debug": zapcore.DebugLevel, 21 | "info": zapcore.InfoLevel, 22 | "warn": zapcore.WarnLevel, 23 | "error": zapcore.ErrorLevel, 24 | } 25 | 26 | var logFormatMap = map[string]zapcore.EncoderConfig{ 27 | "console": { 28 | TimeKey: "time", 29 | LevelKey: "level", 30 | NameKey: "logger", 31 | MessageKey: "msg", 32 | LineEnding: zapcore.DefaultLineEnding, 33 | EncodeLevel: zapcore.CapitalColorLevelEncoder, 34 | EncodeTime: zapcore.RFC3339TimeEncoder, 35 | EncodeDuration: zapcore.SecondsDurationEncoder, 36 | }, 37 | "json": { 38 | TimeKey: "time", 39 | LevelKey: "level", 40 | NameKey: "logger", 41 | MessageKey: "msg", 42 | LineEnding: zapcore.DefaultLineEnding, 43 | EncodeLevel: zapcore.LowercaseLevelEncoder, 44 | EncodeTime: zapcore.EpochMillisTimeEncoder, 45 | EncodeDuration: zapcore.SecondsDurationEncoder, 46 | }, 47 | } 48 | 49 | func (l *serverLogger) Connect(addr net.Addr, uuid string, tx uint64) { 50 | limiterinfo, err := limiter.GetLimiter(l.Tag) 51 | if err != nil { 52 | l.logger.Panic("Get limiter error", zap.String("tag", l.Tag), zap.Error(err)) 53 | } 54 | if _, r := limiterinfo.CheckLimit(format.UserTag(l.Tag, uuid), extractIPFromAddr(addr), addr.Network() == "tcp", true); r { 55 | if userLimit, ok := limiterinfo.UserLimitInfo.Load(format.UserTag(l.Tag, uuid)); ok { 56 | userLimit.(*limiter.UserLimitInfo).OverLimit = true 57 | } 58 | } else { 59 | if userLimit, ok := limiterinfo.UserLimitInfo.Load(format.UserTag(l.Tag, uuid)); ok { 60 | userLimit.(*limiter.UserLimitInfo).OverLimit = false 61 | } 62 | } 63 | l.logger.Info("client connected", zap.String("addr", addr.String()), zap.String("uuid", uuid), zap.Uint64("tx", tx)) 64 | } 65 | 66 | func (l *serverLogger) Disconnect(addr net.Addr, uuid string, err error) { 67 | l.logger.Info("client disconnected", zap.String("addr", addr.String()), zap.String("uuid", uuid), zap.Error(err)) 68 | } 69 | 70 | func (l *serverLogger) TCPRequest(addr net.Addr, uuid, reqAddr string) { 71 | limiterinfo, err := limiter.GetLimiter(l.Tag) 72 | if err != nil { 73 | l.logger.Panic("Get limiter error", zap.String("tag", l.Tag), zap.Error(err)) 74 | } 75 | if _, r := limiterinfo.CheckLimit(format.UserTag(l.Tag, uuid), extractIPFromAddr(addr), addr.Network() == "tcp", true); r { 76 | if userLimit, ok := limiterinfo.UserLimitInfo.Load(format.UserTag(l.Tag, uuid)); ok { 77 | userLimit.(*limiter.UserLimitInfo).OverLimit = true 78 | } 79 | } else { 80 | if userLimit, ok := limiterinfo.UserLimitInfo.Load(format.UserTag(l.Tag, uuid)); ok { 81 | userLimit.(*limiter.UserLimitInfo).OverLimit = false 82 | } 83 | } 84 | l.logger.Debug("TCP request", zap.String("addr", addr.String()), zap.String("uuid", uuid), zap.String("reqAddr", reqAddr)) 85 | } 86 | 87 | func (l *serverLogger) TCPError(addr net.Addr, uuid, reqAddr string, err error) { 88 | if err == nil { 89 | l.logger.Debug("TCP closed", zap.String("addr", addr.String()), zap.String("uuid", uuid), zap.String("reqAddr", reqAddr)) 90 | } else { 91 | l.logger.Debug("TCP error", zap.String("addr", addr.String()), zap.String("uuid", uuid), zap.String("reqAddr", reqAddr), zap.Error(err)) 92 | } 93 | } 94 | 95 | func (l *serverLogger) UDPRequest(addr net.Addr, uuid string, sessionId uint32, reqAddr string) { 96 | limiterinfo, err := limiter.GetLimiter(l.Tag) 97 | if err != nil { 98 | l.logger.Panic("Get limiter error", zap.String("tag", l.Tag), zap.Error(err)) 99 | } 100 | if _, r := limiterinfo.CheckLimit(format.UserTag(l.Tag, uuid), extractIPFromAddr(addr), addr.Network() == "tcp", true); r { 101 | if userLimit, ok := limiterinfo.UserLimitInfo.Load(format.UserTag(l.Tag, uuid)); ok { 102 | userLimit.(*limiter.UserLimitInfo).OverLimit = true 103 | } 104 | } else { 105 | if userLimit, ok := limiterinfo.UserLimitInfo.Load(format.UserTag(l.Tag, uuid)); ok { 106 | userLimit.(*limiter.UserLimitInfo).OverLimit = false 107 | } 108 | } 109 | l.logger.Debug("UDP request", zap.String("addr", addr.String()), zap.String("uuid", uuid), zap.Uint32("sessionId", sessionId), zap.String("reqAddr", reqAddr)) 110 | } 111 | 112 | func (l *serverLogger) UDPError(addr net.Addr, uuid string, sessionId uint32, err error) { 113 | if err == nil { 114 | l.logger.Debug("UDP closed", zap.String("addr", addr.String()), zap.String("uuid", uuid), zap.Uint32("sessionId", sessionId)) 115 | } else { 116 | l.logger.Debug("UDP error", zap.String("addr", addr.String()), zap.String("uuid", uuid), zap.Uint32("sessionId", sessionId), zap.Error(err)) 117 | } 118 | } 119 | 120 | func initLogger(logLevel string, logFormat string) (*zap.Logger, error) { 121 | level, ok := logLevelMap[strings.ToLower(logLevel)] 122 | if !ok { 123 | return nil, fmt.Errorf("unsupported log level: %s", logLevel) 124 | } 125 | enc, ok := logFormatMap[strings.ToLower(logFormat)] 126 | if !ok { 127 | return nil, fmt.Errorf("unsupported log format: %s", logFormat) 128 | } 129 | c := zap.Config{ 130 | Level: zap.NewAtomicLevelAt(level), 131 | DisableCaller: true, 132 | DisableStacktrace: true, 133 | Encoding: strings.ToLower(logFormat), 134 | EncoderConfig: enc, 135 | OutputPaths: []string{"stderr"}, 136 | ErrorOutputPaths: []string{"stderr"}, 137 | } 138 | logger, err := c.Build() 139 | if err != nil { 140 | return nil, fmt.Errorf("failed to initialize logger: %s", err) 141 | } 142 | return logger, nil 143 | } 144 | 145 | func extractIPFromAddr(addr net.Addr) string { 146 | switch v := addr.(type) { 147 | case *net.TCPAddr: 148 | return v.IP.String() 149 | case *net.UDPAddr: 150 | return v.IP.String() 151 | case *net.IPAddr: 152 | return v.IP.String() 153 | default: 154 | return "" 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /core/hy2/node.go: -------------------------------------------------------------------------------- 1 | package hy2 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/InazumaV/V2bX/api/panel" 7 | "github.com/InazumaV/V2bX/conf" 8 | "github.com/apernet/hysteria/core/v2/server" 9 | "github.com/spf13/viper" 10 | "go.uber.org/zap" 11 | ) 12 | 13 | type Hysteria2node struct { 14 | Hy2server server.Server 15 | Tag string 16 | Logger *zap.Logger 17 | EventLogger server.EventLogger 18 | TrafficLogger server.TrafficLogger 19 | } 20 | 21 | func (h *Hysteria2) AddNode(tag string, info *panel.NodeInfo, config *conf.Options) error { 22 | var err error 23 | hyconfig := &server.Config{} 24 | var c serverConfig 25 | v := viper.New() 26 | if len(config.Hysteria2ConfigPath) != 0 { 27 | v.SetConfigFile(config.Hysteria2ConfigPath) 28 | if err := v.ReadInConfig(); err != nil { 29 | h.Logger.Fatal("failed to read server config", zap.Error(err)) 30 | } 31 | if err := v.Unmarshal(&c); err != nil { 32 | h.Logger.Fatal("failed to parse server config", zap.Error(err)) 33 | } 34 | } 35 | n := Hysteria2node{ 36 | Tag: tag, 37 | Logger: h.Logger, 38 | EventLogger: &serverLogger{ 39 | Tag: tag, 40 | logger: h.Logger, 41 | }, 42 | TrafficLogger: &HookServer{ 43 | Tag: tag, 44 | logger: h.Logger, 45 | }, 46 | } 47 | 48 | hyconfig, err = n.getHyConfig(info, config, &c) 49 | if err != nil { 50 | return err 51 | } 52 | hyconfig.Authenticator = h.Auth 53 | s, err := server.NewServer(hyconfig) 54 | if err != nil { 55 | return err 56 | } 57 | n.Hy2server = s 58 | h.Hy2nodes[tag] = n 59 | go func() { 60 | if err := s.Serve(); err != nil { 61 | if !strings.Contains(err.Error(), "quic: server closed") { 62 | h.Logger.Error("Server Error", zap.Error(err)) 63 | } 64 | } 65 | }() 66 | return nil 67 | } 68 | 69 | func (h *Hysteria2) DelNode(tag string) error { 70 | err := h.Hy2nodes[tag].Hy2server.Close() 71 | if err != nil { 72 | return err 73 | } 74 | delete(h.Hy2nodes, tag) 75 | return nil 76 | } 77 | -------------------------------------------------------------------------------- /core/hy2/user.go: -------------------------------------------------------------------------------- 1 | package hy2 2 | 3 | import ( 4 | "net" 5 | "sync" 6 | 7 | "github.com/InazumaV/V2bX/api/panel" 8 | "github.com/InazumaV/V2bX/common/counter" 9 | vCore "github.com/InazumaV/V2bX/core" 10 | "github.com/apernet/hysteria/core/v2/server" 11 | ) 12 | 13 | var _ server.Authenticator = &V2bX{} 14 | 15 | type V2bX struct { 16 | usersMap map[string]int 17 | mutex sync.Mutex 18 | } 19 | 20 | func (v *V2bX) Authenticate(addr net.Addr, auth string, tx uint64) (ok bool, id string) { 21 | v.mutex.Lock() 22 | defer v.mutex.Unlock() 23 | if _, exists := v.usersMap[auth]; exists { 24 | return true, auth 25 | } 26 | return false, "" 27 | } 28 | 29 | func (h *Hysteria2) AddUsers(p *vCore.AddUsersParams) (added int, err error) { 30 | var wg sync.WaitGroup 31 | for _, user := range p.Users { 32 | wg.Add(1) 33 | go func(u panel.UserInfo) { 34 | defer wg.Done() 35 | h.Auth.mutex.Lock() 36 | h.Auth.usersMap[u.Uuid] = u.Id 37 | h.Auth.mutex.Unlock() 38 | }(user) 39 | } 40 | wg.Wait() 41 | return len(p.Users), nil 42 | } 43 | 44 | func (h *Hysteria2) DelUsers(users []panel.UserInfo, tag string, _ *panel.NodeInfo) error { 45 | var wg sync.WaitGroup 46 | for _, user := range users { 47 | wg.Add(1) 48 | go func(u panel.UserInfo) { 49 | defer wg.Done() 50 | h.Auth.mutex.Lock() 51 | delete(h.Auth.usersMap, u.Uuid) 52 | h.Auth.mutex.Unlock() 53 | }(user) 54 | } 55 | wg.Wait() 56 | return nil 57 | } 58 | 59 | func (h *Hysteria2) GetUserTraffic(tag string, uuid string, reset bool) (up int64, down int64) { 60 | if v, ok := h.Hy2nodes[tag].TrafficLogger.(*HookServer).Counter.Load(tag); ok { 61 | c := v.(*counter.TrafficCounter) 62 | up = c.GetUpCount(uuid) 63 | down = c.GetDownCount(uuid) 64 | if reset { 65 | c.Reset(uuid) 66 | } 67 | return up, down 68 | } 69 | return 0, 0 70 | } 71 | -------------------------------------------------------------------------------- /core/imports/hy2.go: -------------------------------------------------------------------------------- 1 | //go:build hysteria2 2 | 3 | package imports 4 | 5 | import _ "github.com/InazumaV/V2bX/core/hy2" 6 | -------------------------------------------------------------------------------- /core/imports/imports.go: -------------------------------------------------------------------------------- 1 | package imports 2 | -------------------------------------------------------------------------------- /core/imports/sing.go: -------------------------------------------------------------------------------- 1 | //go:build sing 2 | 3 | package imports 4 | 5 | import _ "github.com/InazumaV/V2bX/core/sing" 6 | -------------------------------------------------------------------------------- /core/imports/xray.go: -------------------------------------------------------------------------------- 1 | //go:build xray 2 | 3 | package imports 4 | 5 | import _ "github.com/InazumaV/V2bX/core/xray" 6 | -------------------------------------------------------------------------------- /core/interface.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "github.com/InazumaV/V2bX/api/panel" 5 | "github.com/InazumaV/V2bX/conf" 6 | ) 7 | 8 | type AddUsersParams struct { 9 | Tag string 10 | Users []panel.UserInfo 11 | *panel.NodeInfo 12 | } 13 | 14 | type Core interface { 15 | Start() error 16 | Close() error 17 | AddNode(tag string, info *panel.NodeInfo, config *conf.Options) error 18 | DelNode(tag string) error 19 | AddUsers(p *AddUsersParams) (added int, err error) 20 | GetUserTraffic(tag, uuid string, reset bool) (up int64, down int64) 21 | DelUsers(users []panel.UserInfo, tag string, info *panel.NodeInfo) error 22 | Protocols() []string 23 | Type() string 24 | } 25 | -------------------------------------------------------------------------------- /core/selector.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | "sync" 8 | 9 | "github.com/hashicorp/go-multierror" 10 | 11 | "github.com/InazumaV/V2bX/api/panel" 12 | "github.com/InazumaV/V2bX/conf" 13 | ) 14 | 15 | type Selector struct { 16 | cores map[string]Core 17 | nodes sync.Map 18 | } 19 | 20 | func NewSelector(c []conf.CoreConfig) (Core, error) { 21 | cs := make(map[string]Core, len(c)) 22 | for _, t := range c { 23 | f, ok := cores[strings.ToLower(t.Type)] 24 | if !ok { 25 | return nil, errors.New("unknown core type: " + t.Type) 26 | } 27 | core1, err := f(&t) 28 | if err != nil { 29 | return nil, err 30 | } 31 | if t.Name == "" { 32 | cs[t.Type] = core1 33 | } else { 34 | cs[t.Name] = core1 35 | } 36 | } 37 | return &Selector{ 38 | cores: cs, 39 | }, nil 40 | } 41 | 42 | func (s *Selector) Start() error { 43 | for i := range s.cores { 44 | err := s.cores[i].Start() 45 | if err != nil { 46 | return err 47 | } 48 | } 49 | return nil 50 | } 51 | 52 | func (s *Selector) Close() error { 53 | var errs error 54 | for i := range s.cores { 55 | err := s.cores[i].Close() 56 | if err != nil { 57 | errs = multierror.Append(errs, err) 58 | } 59 | } 60 | return errs 61 | } 62 | 63 | func isSupported(protocol string, protocols []string) bool { 64 | for i := range protocols { 65 | if protocol == protocols[i] { 66 | return true 67 | } 68 | } 69 | return false 70 | } 71 | 72 | func (s *Selector) AddNode(tag string, info *panel.NodeInfo, option *conf.Options) error { 73 | var core Core 74 | if len(option.CoreName) > 0 { 75 | // use name to select core 76 | if c, ok := s.cores[option.CoreName]; ok { 77 | core = c 78 | } 79 | } else { 80 | // use type to select core 81 | for _, c := range s.cores { 82 | if len(option.Core) == 0 { 83 | if !isSupported(info.Type, c.Protocols()) { 84 | continue 85 | } 86 | } else if option.Core != c.Type() { 87 | continue 88 | } 89 | core = c 90 | } 91 | } 92 | if core == nil { 93 | return errors.New("the node type is not support") 94 | } 95 | if len(option.Core) == 0 { 96 | option.Core = core.Type() 97 | err := option.UnmarshalJSON(option.RawOptions) 98 | if err != nil { 99 | return fmt.Errorf("unmarshal option error: %s", err) 100 | } 101 | option.RawOptions = nil 102 | } 103 | err := core.AddNode(tag, info, option) 104 | if err != nil { 105 | return err 106 | } 107 | s.nodes.Store(tag, core) 108 | return nil 109 | } 110 | 111 | func (s *Selector) DelNode(tag string) error { 112 | if t, e := s.nodes.Load(tag); e { 113 | err := t.(Core).DelNode(tag) 114 | if err != nil { 115 | return err 116 | } 117 | s.nodes.Delete(tag) 118 | return nil 119 | } 120 | return errors.New("the node is not have") 121 | } 122 | 123 | func (s *Selector) AddUsers(p *AddUsersParams) (added int, err error) { 124 | t, e := s.nodes.Load(p.Tag) 125 | if !e { 126 | return 0, errors.New("the node is not have") 127 | } 128 | return t.(Core).AddUsers(p) 129 | } 130 | 131 | func (s *Selector) GetUserTraffic(tag, uuid string, reset bool) (up int64, down int64) { 132 | t, e := s.nodes.Load(tag) 133 | if !e { 134 | return 0, 0 135 | } 136 | return t.(Core).GetUserTraffic(tag, uuid, reset) 137 | } 138 | 139 | func (s *Selector) DelUsers(users []panel.UserInfo, tag string, info *panel.NodeInfo) error { 140 | t, e := s.nodes.Load(tag) 141 | if !e { 142 | return errors.New("the node is not have") 143 | } 144 | return t.(Core).DelUsers(users, tag, info) 145 | } 146 | 147 | func (s *Selector) Protocols() []string { 148 | protocols := make([]string, 0) 149 | for i := range s.cores { 150 | protocols = append(protocols, s.cores[i].Protocols()...) 151 | } 152 | return protocols 153 | } 154 | 155 | func (s *Selector) Type() string { 156 | t := "Selector(" 157 | var flag bool 158 | for n, c := range s.cores { 159 | if flag { 160 | t += " " 161 | } else { 162 | flag = true 163 | } 164 | if len(n) == 0 { 165 | t += c.Type() 166 | } else { 167 | t += n 168 | } 169 | } 170 | t += ")" 171 | return t 172 | } 173 | -------------------------------------------------------------------------------- /core/sing/hook.go: -------------------------------------------------------------------------------- 1 | package sing 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "sync" 8 | 9 | "github.com/InazumaV/V2bX/common/format" 10 | "github.com/InazumaV/V2bX/common/rate" 11 | 12 | "github.com/InazumaV/V2bX/limiter" 13 | 14 | "github.com/InazumaV/V2bX/common/counter" 15 | "github.com/sagernet/sing-box/adapter" 16 | "github.com/sagernet/sing-box/log" 17 | N "github.com/sagernet/sing/common/network" 18 | ) 19 | 20 | var _ adapter.ConnectionTracker = (*HookServer)(nil) 21 | 22 | type HookServer struct { 23 | counter sync.Map //map[string]*counter.TrafficCounter 24 | } 25 | 26 | func (h *HookServer) ModeList() []string { 27 | return nil 28 | } 29 | 30 | func NewHookServer() *HookServer { 31 | server := &HookServer{ 32 | counter: sync.Map{}, 33 | } 34 | return server 35 | } 36 | 37 | func (h *HookServer) RoutedConnection(_ context.Context, conn net.Conn, m adapter.InboundContext, _ adapter.Rule, _ adapter.Outbound) net.Conn { 38 | l, err := limiter.GetLimiter(m.Inbound) 39 | if err != nil { 40 | log.Warn("get limiter for ", m.Inbound, " error: ", err) 41 | return conn 42 | } 43 | taguuid := format.UserTag(m.Inbound, m.User) 44 | ip := m.Source.Addr.String() 45 | if b, r := l.CheckLimit(taguuid, ip, true, true); r { 46 | conn.Close() 47 | log.Error("[", m.Inbound, "] ", "Limited ", m.User, " by ip or conn") 48 | return conn 49 | } else if b != nil { 50 | conn = rate.NewConnRateLimiter(conn, b) 51 | } 52 | if l != nil { 53 | destStr := m.Destination.AddrString() 54 | protocol := m.Destination.Network() 55 | if l.CheckDomainRule(destStr) { 56 | log.Error(fmt.Sprintf( 57 | "User %s access domain %s reject by rule", 58 | m.User, 59 | destStr)) 60 | conn.Close() 61 | return conn 62 | } 63 | if len(protocol) != 0 { 64 | if l.CheckProtocolRule(protocol) { 65 | log.Error(fmt.Sprintf( 66 | "User %s access protocol %s reject by rule", 67 | m.User, 68 | protocol)) 69 | conn.Close() 70 | return conn 71 | } 72 | } 73 | } 74 | var t *counter.TrafficCounter 75 | if c, ok := h.counter.Load(m.Inbound); !ok { 76 | t = counter.NewTrafficCounter() 77 | h.counter.Store(m.Inbound, t) 78 | } else { 79 | t = c.(*counter.TrafficCounter) 80 | } 81 | conn = counter.NewConnCounter(conn, t.GetCounter(m.User)) 82 | return conn 83 | } 84 | 85 | func (h *HookServer) RoutedPacketConnection(_ context.Context, conn N.PacketConn, m adapter.InboundContext, _ adapter.Rule, _ adapter.Outbound) N.PacketConn { 86 | l, err := limiter.GetLimiter(m.Inbound) 87 | if err != nil { 88 | log.Warn("get limiter for ", m.Inbound, " error: ", err) 89 | return conn 90 | } 91 | ip := m.Source.Addr.String() 92 | taguuid := format.UserTag(m.Inbound, m.User) 93 | if b, r := l.CheckLimit(taguuid, ip, false, false); r { 94 | conn.Close() 95 | log.Error("[", m.Inbound, "] ", "Limited ", m.User, " by ip or conn") 96 | return conn 97 | } else if b != nil { 98 | //conn = rate.NewPacketConnCounter(conn, b) 99 | } 100 | if l != nil { 101 | destStr := m.Destination.AddrString() 102 | protocol := m.Destination.Network() 103 | if l.CheckDomainRule(destStr) { 104 | log.Error(fmt.Sprintf( 105 | "User %s access domain %s reject by rule", 106 | m.User, 107 | destStr)) 108 | conn.Close() 109 | return conn 110 | } 111 | if len(protocol) != 0 { 112 | if l.CheckProtocolRule(protocol) { 113 | log.Error(fmt.Sprintf( 114 | "User %s access protocol %s reject by rule", 115 | m.User, 116 | protocol)) 117 | conn.Close() 118 | return conn 119 | } 120 | } 121 | } 122 | var t *counter.TrafficCounter 123 | if c, ok := h.counter.Load(m.Inbound); !ok { 124 | t = counter.NewTrafficCounter() 125 | h.counter.Store(m.Inbound, t) 126 | } else { 127 | t = c.(*counter.TrafficCounter) 128 | } 129 | conn = counter.NewPacketConnCounter(conn, t.GetCounter(m.User)) 130 | return conn 131 | } 132 | -------------------------------------------------------------------------------- /core/sing/sing.go: -------------------------------------------------------------------------------- 1 | package sing 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/sagernet/sing-box/include" 9 | "github.com/sagernet/sing-box/log" 10 | 11 | "github.com/InazumaV/V2bX/conf" 12 | vCore "github.com/InazumaV/V2bX/core" 13 | box "github.com/sagernet/sing-box" 14 | "github.com/sagernet/sing-box/adapter" 15 | "github.com/sagernet/sing-box/option" 16 | "github.com/sagernet/sing/common/json" 17 | ) 18 | 19 | var _ vCore.Core = (*Sing)(nil) 20 | 21 | type DNSConfig struct { 22 | Servers []map[string]interface{} `json:"servers"` 23 | Rules []map[string]interface{} `json:"rules"` 24 | } 25 | 26 | type Sing struct { 27 | box *box.Box 28 | ctx context.Context 29 | hookServer *HookServer 30 | router adapter.Router 31 | logFactory log.Factory 32 | } 33 | 34 | func init() { 35 | vCore.RegisterCore("sing", New) 36 | } 37 | 38 | func New(c *conf.CoreConfig) (vCore.Core, error) { 39 | ctx := context.Background() 40 | ctx = box.Context(ctx, include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), include.DNSTransportRegistry(), include.ServiceRegistry()) 41 | options := option.Options{} 42 | if len(c.SingConfig.OriginalPath) != 0 { 43 | data, err := os.ReadFile(c.SingConfig.OriginalPath) 44 | if err != nil { 45 | return nil, fmt.Errorf("read original config error: %s", err) 46 | } 47 | options, err = json.UnmarshalExtendedContext[option.Options](ctx, data) 48 | if err != nil { 49 | return nil, fmt.Errorf("unmarshal original config error: %s", err) 50 | } 51 | } 52 | options.Log = &option.LogOptions{ 53 | Disabled: c.SingConfig.LogConfig.Disabled, 54 | Level: c.SingConfig.LogConfig.Level, 55 | Timestamp: c.SingConfig.LogConfig.Timestamp, 56 | Output: c.SingConfig.LogConfig.Output, 57 | } 58 | options.NTP = &option.NTPOptions{ 59 | Enabled: c.SingConfig.NtpConfig.Enable, 60 | WriteToSystem: true, 61 | ServerOptions: option.ServerOptions{ 62 | Server: c.SingConfig.NtpConfig.Server, 63 | ServerPort: c.SingConfig.NtpConfig.ServerPort, 64 | }, 65 | } 66 | os.Setenv("SING_DNS_PATH", "") 67 | b, err := box.New(box.Options{ 68 | Context: ctx, 69 | Options: options, 70 | }) 71 | if err != nil { 72 | return nil, err 73 | } 74 | hs := NewHookServer() 75 | b.Router().AppendTracker(hs) 76 | return &Sing{ 77 | ctx: b.Router().GetCtx(), 78 | box: b, 79 | hookServer: hs, 80 | router: b.Router(), 81 | logFactory: b.LogFactory(), 82 | }, nil 83 | } 84 | 85 | func (b *Sing) Start() error { 86 | return b.box.Start() 87 | } 88 | 89 | func (b *Sing) Close() error { 90 | return b.box.Close() 91 | } 92 | 93 | func (b *Sing) Protocols() []string { 94 | return []string{ 95 | "vmess", 96 | "vless", 97 | "shadowsocks", 98 | "trojan", 99 | "tuic", 100 | "anytls", 101 | "hysteria", 102 | "hysteria2", 103 | } 104 | } 105 | 106 | func (b *Sing) Type() string { 107 | return "sing" 108 | } 109 | -------------------------------------------------------------------------------- /core/sing/user.go: -------------------------------------------------------------------------------- 1 | package sing 2 | 3 | import ( 4 | "encoding/base64" 5 | "errors" 6 | 7 | "github.com/InazumaV/V2bX/api/panel" 8 | "github.com/InazumaV/V2bX/common/counter" 9 | "github.com/InazumaV/V2bX/core" 10 | "github.com/sagernet/sing-box/option" 11 | "github.com/sagernet/sing-box/protocol/anytls" 12 | "github.com/sagernet/sing-box/protocol/hysteria" 13 | "github.com/sagernet/sing-box/protocol/hysteria2" 14 | "github.com/sagernet/sing-box/protocol/shadowsocks" 15 | "github.com/sagernet/sing-box/protocol/trojan" 16 | "github.com/sagernet/sing-box/protocol/tuic" 17 | "github.com/sagernet/sing-box/protocol/vless" 18 | "github.com/sagernet/sing-box/protocol/vmess" 19 | ) 20 | 21 | func (b *Sing) AddUsers(p *core.AddUsersParams) (added int, err error) { 22 | in, found := b.box.Inbound().Get(p.Tag) 23 | if !found { 24 | return 0, errors.New("the inbound not found") 25 | } 26 | switch p.NodeInfo.Type { 27 | case "vless": 28 | us := make([]option.VLESSUser, len(p.Users)) 29 | for i := range p.Users { 30 | us[i] = option.VLESSUser{ 31 | Name: p.Users[i].Uuid, 32 | Flow: p.VAllss.Flow, 33 | UUID: p.Users[i].Uuid, 34 | } 35 | } 36 | err = in.(*vless.Inbound).AddUsers(us) 37 | case "vmess": 38 | us := make([]option.VMessUser, len(p.Users)) 39 | for i := range p.Users { 40 | us[i] = option.VMessUser{ 41 | Name: p.Users[i].Uuid, 42 | UUID: p.Users[i].Uuid, 43 | } 44 | } 45 | err = in.(*vmess.Inbound).AddUsers(us) 46 | case "shadowsocks": 47 | us := make([]option.ShadowsocksUser, len(p.Users)) 48 | for i := range p.Users { 49 | var password = p.Users[i].Uuid 50 | switch p.Shadowsocks.Cipher { 51 | case "2022-blake3-aes-128-gcm": 52 | password = base64.StdEncoding.EncodeToString([]byte(password[:16])) 53 | case "2022-blake3-aes-256-gcm": 54 | password = base64.StdEncoding.EncodeToString([]byte(password[:32])) 55 | } 56 | us[i] = option.ShadowsocksUser{ 57 | Name: p.Users[i].Uuid, 58 | Password: password, 59 | } 60 | } 61 | err = in.(*shadowsocks.MultiInbound).AddUsers(us) 62 | case "trojan": 63 | us := make([]option.TrojanUser, len(p.Users)) 64 | for i := range p.Users { 65 | us[i] = option.TrojanUser{ 66 | Name: p.Users[i].Uuid, 67 | Password: p.Users[i].Uuid, 68 | } 69 | } 70 | err = in.(*trojan.Inbound).AddUsers(us) 71 | case "tuic": 72 | us := make([]option.TUICUser, len(p.Users)) 73 | id := make([]int, len(p.Users)) 74 | for i := range p.Users { 75 | us[i] = option.TUICUser{ 76 | Name: p.Users[i].Uuid, 77 | UUID: p.Users[i].Uuid, 78 | Password: p.Users[i].Uuid, 79 | } 80 | id[i] = p.Users[i].Id 81 | } 82 | err = in.(*tuic.Inbound).AddUsers(us, id) 83 | case "hysteria": 84 | us := make([]option.HysteriaUser, len(p.Users)) 85 | for i := range p.Users { 86 | us[i] = option.HysteriaUser{ 87 | Name: p.Users[i].Uuid, 88 | AuthString: p.Users[i].Uuid, 89 | } 90 | } 91 | err = in.(*hysteria.Inbound).AddUsers(us) 92 | case "hysteria2": 93 | us := make([]option.Hysteria2User, len(p.Users)) 94 | id := make([]int, len(p.Users)) 95 | for i := range p.Users { 96 | us[i] = option.Hysteria2User{ 97 | Name: p.Users[i].Uuid, 98 | Password: p.Users[i].Uuid, 99 | } 100 | id[i] = p.Users[i].Id 101 | } 102 | err = in.(*hysteria2.Inbound).AddUsers(us, id) 103 | case "anytls": 104 | us := make([]option.AnyTLSUser, len(p.Users)) 105 | for i := range p.Users { 106 | us[i] = option.AnyTLSUser{ 107 | Name: p.Users[i].Uuid, 108 | Password: p.Users[i].Uuid, 109 | } 110 | } 111 | err = in.(*anytls.Inbound).AddUsers(us) 112 | } 113 | if err != nil { 114 | return 0, err 115 | } 116 | return len(p.Users), err 117 | } 118 | 119 | func (b *Sing) GetUserTraffic(tag, uuid string, reset bool) (up int64, down int64) { 120 | if v, ok := b.hookServer.counter.Load(tag); ok { 121 | c := v.(*counter.TrafficCounter) 122 | up = c.GetUpCount(uuid) 123 | down = c.GetDownCount(uuid) 124 | if reset { 125 | c.Reset(uuid) 126 | } 127 | return 128 | } 129 | return 0, 0 130 | } 131 | 132 | type UserDeleter interface { 133 | DelUsers(uuid []string) error 134 | } 135 | 136 | func (b *Sing) DelUsers(users []panel.UserInfo, tag string, info *panel.NodeInfo) error { 137 | var del UserDeleter 138 | if i, found := b.box.Inbound().Get(tag); found { 139 | switch info.Type { 140 | case "vmess": 141 | del = i.(*vmess.Inbound) 142 | case "vless": 143 | del = i.(*vless.Inbound) 144 | case "shadowsocks": 145 | del = i.(*shadowsocks.MultiInbound) 146 | case "trojan": 147 | del = i.(*trojan.Inbound) 148 | case "tuic": 149 | del = i.(*tuic.Inbound) 150 | case "hysteria": 151 | del = i.(*hysteria.Inbound) 152 | case "hysteria2": 153 | del = i.(*hysteria2.Inbound) 154 | case "anytls": 155 | del = i.(*anytls.Inbound) 156 | } 157 | } else { 158 | return errors.New("the inbound not found") 159 | } 160 | uuids := make([]string, len(users)) 161 | for i := range users { 162 | uuids[i] = users[i].Uuid 163 | } 164 | err := del.DelUsers(uuids) 165 | if err != nil { 166 | return err 167 | } 168 | return nil 169 | } 170 | -------------------------------------------------------------------------------- /core/sing/utils.go: -------------------------------------------------------------------------------- 1 | package sing 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | 7 | "github.com/InazumaV/V2bX/conf" 8 | "github.com/sagernet/sing-box/option" 9 | ) 10 | 11 | func processFallback(c *conf.Options, fallbackForALPN map[string]*option.ServerOptions) error { 12 | for k, v := range c.SingOptions.FallBackConfigs.FallBackForALPN { 13 | fallbackPort, err := strconv.Atoi(v.ServerPort) 14 | if err != nil { 15 | return fmt.Errorf("unable to parse fallbackForALPN server port error: %s", err) 16 | } 17 | fallbackForALPN[k] = &option.ServerOptions{Server: v.Server, ServerPort: uint16(fallbackPort)} 18 | } 19 | return nil 20 | } 21 | -------------------------------------------------------------------------------- /core/xray/app/app.go: -------------------------------------------------------------------------------- 1 | // Package app contains the third-party app used to replace the default app in xray-core 2 | package app 3 | -------------------------------------------------------------------------------- /core/xray/app/dispatcher/config.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.35.1 4 | // protoc v3.21.12 5 | // source: config.proto 6 | 7 | package dispatcher 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 | mi := &file_config_proto_msgTypes[0] 32 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 33 | ms.StoreMessageInfo(mi) 34 | } 35 | 36 | func (x *SessionConfig) String() string { 37 | return protoimpl.X.MessageStringOf(x) 38 | } 39 | 40 | func (*SessionConfig) ProtoMessage() {} 41 | 42 | func (x *SessionConfig) ProtoReflect() protoreflect.Message { 43 | mi := &file_config_proto_msgTypes[0] 44 | if x != nil { 45 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 46 | if ms.LoadMessageInfo() == nil { 47 | ms.StoreMessageInfo(mi) 48 | } 49 | return ms 50 | } 51 | return mi.MessageOf(x) 52 | } 53 | 54 | // Deprecated: Use SessionConfig.ProtoReflect.Descriptor instead. 55 | func (*SessionConfig) Descriptor() ([]byte, []int) { 56 | return file_config_proto_rawDescGZIP(), []int{0} 57 | } 58 | 59 | type Config struct { 60 | state protoimpl.MessageState 61 | sizeCache protoimpl.SizeCache 62 | unknownFields protoimpl.UnknownFields 63 | 64 | Settings *SessionConfig `protobuf:"bytes,1,opt,name=settings,proto3" json:"settings,omitempty"` 65 | } 66 | 67 | func (x *Config) Reset() { 68 | *x = Config{} 69 | mi := &file_config_proto_msgTypes[1] 70 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 71 | ms.StoreMessageInfo(mi) 72 | } 73 | 74 | func (x *Config) String() string { 75 | return protoimpl.X.MessageStringOf(x) 76 | } 77 | 78 | func (*Config) ProtoMessage() {} 79 | 80 | func (x *Config) ProtoReflect() protoreflect.Message { 81 | mi := &file_config_proto_msgTypes[1] 82 | if x != nil { 83 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 84 | if ms.LoadMessageInfo() == nil { 85 | ms.StoreMessageInfo(mi) 86 | } 87 | return ms 88 | } 89 | return mi.MessageOf(x) 90 | } 91 | 92 | // Deprecated: Use Config.ProtoReflect.Descriptor instead. 93 | func (*Config) Descriptor() ([]byte, []int) { 94 | return file_config_proto_rawDescGZIP(), []int{1} 95 | } 96 | 97 | func (x *Config) GetSettings() *SessionConfig { 98 | if x != nil { 99 | return x.Settings 100 | } 101 | return nil 102 | } 103 | 104 | var File_config_proto protoreflect.FileDescriptor 105 | 106 | var file_config_proto_rawDesc = []byte{ 107 | 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x18, 108 | 0x76, 0x32, 0x62, 0x78, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x64, 0x69, 109 | 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x65, 0x72, 0x22, 0x15, 0x0a, 0x0d, 0x53, 0x65, 0x73, 0x73, 110 | 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x22, 111 | 0x4d, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x43, 0x0a, 0x08, 0x73, 0x65, 0x74, 112 | 0x74, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x76, 0x32, 113 | 0x62, 0x78, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x64, 0x69, 0x73, 0x70, 114 | 0x61, 0x74, 0x63, 0x68, 0x65, 0x72, 0x2e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 115 | 0x6e, 0x66, 0x69, 0x67, 0x52, 0x08, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x42, 0x6e, 116 | 0x0a, 0x1c, 0x63, 0x6f, 0x6d, 0x2e, 0x76, 0x32, 0x62, 0x78, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 117 | 0x61, 0x70, 0x70, 0x2e, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x65, 0x72, 0x50, 0x01, 118 | 0x5a, 0x31, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x49, 0x6e, 0x61, 119 | 0x7a, 0x75, 0x6d, 0x61, 0x56, 0x2f, 0x56, 0x32, 0x62, 0x58, 0x2f, 0x63, 0x6f, 0x72, 0x65, 0x2f, 120 | 0x78, 0x72, 0x61, 0x79, 0x2f, 0x61, 0x70, 0x70, 0x2f, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 121 | 0x68, 0x65, 0x72, 0xaa, 0x02, 0x18, 0x56, 0x32, 0x62, 0x58, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 122 | 0x61, 0x70, 0x70, 0x2e, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x65, 0x72, 0x62, 0x06, 123 | 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 124 | } 125 | 126 | var ( 127 | file_config_proto_rawDescOnce sync.Once 128 | file_config_proto_rawDescData = file_config_proto_rawDesc 129 | ) 130 | 131 | func file_config_proto_rawDescGZIP() []byte { 132 | file_config_proto_rawDescOnce.Do(func() { 133 | file_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_config_proto_rawDescData) 134 | }) 135 | return file_config_proto_rawDescData 136 | } 137 | 138 | var file_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2) 139 | var file_config_proto_goTypes = []any{ 140 | (*SessionConfig)(nil), // 0: v2bx.core.app.dispatcher.SessionConfig 141 | (*Config)(nil), // 1: v2bx.core.app.dispatcher.Config 142 | } 143 | var file_config_proto_depIdxs = []int32{ 144 | 0, // 0: v2bx.core.app.dispatcher.Config.settings:type_name -> v2bx.core.app.dispatcher.SessionConfig 145 | 1, // [1:1] is the sub-list for method output_type 146 | 1, // [1:1] is the sub-list for method input_type 147 | 1, // [1:1] is the sub-list for extension type_name 148 | 1, // [1:1] is the sub-list for extension extendee 149 | 0, // [0:1] is the sub-list for field type_name 150 | } 151 | 152 | func init() { file_config_proto_init() } 153 | func file_config_proto_init() { 154 | if File_config_proto != nil { 155 | return 156 | } 157 | type x struct{} 158 | out := protoimpl.TypeBuilder{ 159 | File: protoimpl.DescBuilder{ 160 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 161 | RawDescriptor: file_config_proto_rawDesc, 162 | NumEnums: 0, 163 | NumMessages: 2, 164 | NumExtensions: 0, 165 | NumServices: 0, 166 | }, 167 | GoTypes: file_config_proto_goTypes, 168 | DependencyIndexes: file_config_proto_depIdxs, 169 | MessageInfos: file_config_proto_msgTypes, 170 | }.Build() 171 | File_config_proto = out.File 172 | file_config_proto_rawDesc = nil 173 | file_config_proto_goTypes = nil 174 | file_config_proto_depIdxs = nil 175 | } 176 | -------------------------------------------------------------------------------- /core/xray/app/dispatcher/config.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package v2bx.core.app.dispatcher; 4 | option csharp_namespace = "V2bX.core.app.dispatcher"; 5 | option go_package = "github.com/InazumaV/V2bX/core/xray/app/dispatcher"; 6 | option java_package = "com.v2bx.core.app.dispatcher"; 7 | option java_multiple_files = true; 8 | 9 | message SessionConfig { 10 | reserved 1; 11 | } 12 | 13 | message Config { 14 | SessionConfig settings = 1; 15 | } 16 | -------------------------------------------------------------------------------- /core/xray/app/dispatcher/dispatcher.go: -------------------------------------------------------------------------------- 1 | package dispatcher 2 | 3 | //go:generate go run github.com/xtls/xray-core/common/errors/errorgen 4 | -------------------------------------------------------------------------------- /core/xray/app/dispatcher/fakednssniffer.go: -------------------------------------------------------------------------------- 1 | package dispatcher 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 Creates 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 := errors.New("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 | if ob.Target.Network == net.Network_TCP || ob.Target.Network == net.Network_UDP { 33 | domainFromFakeDNS := fakeDNSEngine.GetDomainFromFakeDNS(ob.Target.Address) 34 | if domainFromFakeDNS != "" { 35 | errors.LogInfo(ctx, "fake dns got domain: ", domainFromFakeDNS, " for ip: ", ob.Target.Address.String()) 36 | return &fakeDNSSniffResult{domainName: domainFromFakeDNS}, nil 37 | } 38 | } 39 | 40 | if ipAddressInRangeValueI := ctx.Value(ipAddressInRange); ipAddressInRangeValueI != nil { 41 | ipAddressInRangeValue := ipAddressInRangeValueI.(*ipAddressInRangeOpt) 42 | if fkr0, ok := fakeDNSEngine.(dns.FakeDNSEngineRev0); ok { 43 | inPool := fkr0.IsIPInIPPool(ob.Target.Address) 44 | ipAddressInRangeValue.addressInRange = &inPool 45 | } 46 | } 47 | 48 | return nil, common.ErrNoClue 49 | }, metadataSniffer: true}, nil 50 | } 51 | 52 | type fakeDNSSniffResult struct { 53 | domainName string 54 | } 55 | 56 | func (fakeDNSSniffResult) Protocol() string { 57 | return "fakedns" 58 | } 59 | 60 | func (f fakeDNSSniffResult) Domain() string { 61 | return f.domainName 62 | } 63 | 64 | type fakeDNSExtraOpts int 65 | 66 | const ipAddressInRange fakeDNSExtraOpts = 1 67 | 68 | type ipAddressInRangeOpt struct { 69 | addressInRange *bool 70 | } 71 | 72 | type DNSThenOthersSniffResult struct { 73 | domainName string 74 | protocolOriginalName string 75 | } 76 | 77 | func (f DNSThenOthersSniffResult) IsProtoSubsetOf(protocolName string) bool { 78 | return strings.HasPrefix(protocolName, f.protocolOriginalName) 79 | } 80 | 81 | func (DNSThenOthersSniffResult) Protocol() string { 82 | return "fakedns+others" 83 | } 84 | 85 | func (f DNSThenOthersSniffResult) Domain() string { 86 | return f.domainName 87 | } 88 | 89 | func newFakeDNSThenOthers(ctx context.Context, fakeDNSSniffer protocolSnifferWithMetadata, others []protocolSnifferWithMetadata) ( 90 | protocolSnifferWithMetadata, error, 91 | ) { // 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 | -------------------------------------------------------------------------------- /core/xray/app/dispatcher/sniffer.go: -------------------------------------------------------------------------------- 1 | package dispatcher 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/xtls/xray-core/common" 7 | "github.com/xtls/xray-core/common/errors" 8 | "github.com/xtls/xray-core/common/net" 9 | "github.com/xtls/xray-core/common/protocol/bittorrent" 10 | "github.com/xtls/xray-core/common/protocol/http" 11 | "github.com/xtls/xray-core/common/protocol/quic" 12 | "github.com/xtls/xray-core/common/protocol/tls" 13 | ) 14 | 15 | type SniffResult interface { 16 | Protocol() string 17 | Domain() string 18 | } 19 | 20 | type protocolSniffer func(context.Context, []byte) (SniffResult, error) 21 | 22 | type protocolSnifferWithMetadata struct { 23 | protocolSniffer protocolSniffer 24 | // A Metadata sniffer will be invoked on connection establishment only, with nil body, 25 | // for both TCP and UDP connections 26 | // It will not be shown as a traffic type for routing unless there is no other successful sniffing. 27 | metadataSniffer bool 28 | network net.Network 29 | } 30 | 31 | type Sniffer struct { 32 | sniffer []protocolSnifferWithMetadata 33 | } 34 | 35 | func NewSniffer(ctx context.Context) *Sniffer { 36 | ret := &Sniffer{ 37 | sniffer: []protocolSnifferWithMetadata{ 38 | {func(c context.Context, b []byte) (SniffResult, error) { return http.SniffHTTP(b, ctx) }, false, net.Network_TCP}, 39 | {func(c context.Context, b []byte) (SniffResult, error) { return tls.SniffTLS(b) }, false, net.Network_TCP}, 40 | {func(c context.Context, b []byte) (SniffResult, error) { return bittorrent.SniffBittorrent(b) }, false, net.Network_TCP}, 41 | {func(c context.Context, b []byte) (SniffResult, error) { return quic.SniffQUIC(b) }, false, net.Network_UDP}, 42 | {func(c context.Context, b []byte) (SniffResult, error) { return bittorrent.SniffUTP(b) }, false, net.Network_UDP}, 43 | }, 44 | } 45 | if sniffer, err := newFakeDNSSniffer(ctx); err == nil { 46 | others := ret.sniffer 47 | ret.sniffer = append(ret.sniffer, sniffer) 48 | fakeDNSThenOthers, err := newFakeDNSThenOthers(ctx, sniffer, others) 49 | if err == nil { 50 | ret.sniffer = append([]protocolSnifferWithMetadata{fakeDNSThenOthers}, ret.sniffer...) 51 | } 52 | } 53 | return ret 54 | } 55 | 56 | var errUnknownContent = errors.New("unknown content") 57 | 58 | func (s *Sniffer) Sniff(c context.Context, payload []byte, network net.Network) (SniffResult, error) { 59 | var pendingSniffer []protocolSnifferWithMetadata 60 | for _, si := range s.sniffer { 61 | s := si.protocolSniffer 62 | if si.metadataSniffer || si.network != network { 63 | continue 64 | } 65 | result, err := s(c, payload) 66 | if err == common.ErrNoClue { 67 | pendingSniffer = append(pendingSniffer, si) 68 | continue 69 | } 70 | 71 | if err == nil && result != nil { 72 | return result, nil 73 | } 74 | } 75 | 76 | if len(pendingSniffer) > 0 { 77 | s.sniffer = pendingSniffer 78 | return nil, common.ErrNoClue 79 | } 80 | 81 | return nil, errUnknownContent 82 | } 83 | 84 | func (s *Sniffer) SniffMetadata(c context.Context) (SniffResult, error) { 85 | var pendingSniffer []protocolSnifferWithMetadata 86 | for _, si := range s.sniffer { 87 | s := si.protocolSniffer 88 | if !si.metadataSniffer { 89 | pendingSniffer = append(pendingSniffer, si) 90 | continue 91 | } 92 | result, err := s(c, nil) 93 | if err == common.ErrNoClue { 94 | pendingSniffer = append(pendingSniffer, si) 95 | continue 96 | } 97 | 98 | if err == nil && result != nil { 99 | return result, nil 100 | } 101 | } 102 | 103 | if len(pendingSniffer) > 0 { 104 | s.sniffer = pendingSniffer 105 | return nil, common.ErrNoClue 106 | } 107 | 108 | return nil, errUnknownContent 109 | } 110 | 111 | func CompositeResult(domainResult SniffResult, protocolResult SniffResult) SniffResult { 112 | return &compositeResult{domainResult: domainResult, protocolResult: protocolResult} 113 | } 114 | 115 | type compositeResult struct { 116 | domainResult SniffResult 117 | protocolResult SniffResult 118 | } 119 | 120 | func (c compositeResult) Protocol() string { 121 | return c.protocolResult.Protocol() 122 | } 123 | 124 | func (c compositeResult) Domain() string { 125 | return c.domainResult.Domain() 126 | } 127 | 128 | func (c compositeResult) ProtocolForDomainResult() string { 129 | return c.domainResult.Protocol() 130 | } 131 | 132 | type SnifferResultComposite interface { 133 | ProtocolForDomainResult() string 134 | } 135 | 136 | type SnifferIsProtoSubsetOf interface { 137 | IsProtoSubsetOf(protocolName string) bool 138 | } 139 | -------------------------------------------------------------------------------- /core/xray/app/dispatcher/stats.go: -------------------------------------------------------------------------------- 1 | package dispatcher 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 | -------------------------------------------------------------------------------- /core/xray/app/dispatcher/stats_test.go: -------------------------------------------------------------------------------- 1 | package dispatcher_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 | -------------------------------------------------------------------------------- /core/xray/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 | // Mandatory features. Can't remove unless there are replacements. 7 | _ "github.com/xtls/xray-core/app/dispatcher" 8 | _ "github.com/xtls/xray-core/app/proxyman/inbound" 9 | _ "github.com/xtls/xray-core/app/proxyman/outbound" 10 | 11 | // Default commander and all its services. This is an optional feature. 12 | //_ "github.com/xtls/xray-core/app/commander" 13 | //_ "github.com/xtls/xray-core/app/log/command" 14 | //_ "github.com/xtls/xray-core/app/proxyman/command" 15 | //_ "github.com/xtls/xray-core/app/stats/command" 16 | 17 | // Developer preview services 18 | //_ "github.com/xtls/xray-core/app/observatory/command" 19 | 20 | // Other optional features. 21 | _ "github.com/xtls/xray-core/app/dns" 22 | _ "github.com/xtls/xray-core/app/dns/fakedns" 23 | _ "github.com/xtls/xray-core/app/log" 24 | _ "github.com/xtls/xray-core/app/metrics" 25 | _ "github.com/xtls/xray-core/app/policy" 26 | _ "github.com/xtls/xray-core/app/reverse" 27 | _ "github.com/xtls/xray-core/app/router" 28 | _ "github.com/xtls/xray-core/app/stats" 29 | 30 | // Fix dependency cycle caused by core import in internet package 31 | _ "github.com/xtls/xray-core/transport/internet/tagged/taggedimpl" 32 | 33 | // Developer preview features 34 | //_ "github.com/xtls/xray-core/app/observatory" 35 | 36 | // Inbound and outbound proxies. 37 | _ "github.com/xtls/xray-core/proxy/blackhole" 38 | _ "github.com/xtls/xray-core/proxy/dns" 39 | _ "github.com/xtls/xray-core/proxy/dokodemo" 40 | _ "github.com/xtls/xray-core/proxy/freedom" 41 | _ "github.com/xtls/xray-core/proxy/http" 42 | _ "github.com/xtls/xray-core/proxy/loopback" 43 | _ "github.com/xtls/xray-core/proxy/shadowsocks" 44 | _ "github.com/xtls/xray-core/proxy/shadowsocks_2022" 45 | _ "github.com/xtls/xray-core/proxy/socks" 46 | _ "github.com/xtls/xray-core/proxy/trojan" 47 | _ "github.com/xtls/xray-core/proxy/vless/inbound" 48 | _ "github.com/xtls/xray-core/proxy/vless/outbound" 49 | _ "github.com/xtls/xray-core/proxy/vmess/inbound" 50 | _ "github.com/xtls/xray-core/proxy/vmess/outbound" 51 | 52 | //_ "github.com/xtls/xray-core/proxy/wireguard" 53 | 54 | // Transports 55 | _ "github.com/xtls/xray-core/transport/internet/grpc" 56 | _ "github.com/xtls/xray-core/transport/internet/kcp" 57 | _ "github.com/xtls/xray-core/transport/internet/reality" 58 | _ "github.com/xtls/xray-core/transport/internet/splithttp" 59 | _ "github.com/xtls/xray-core/transport/internet/tcp" 60 | _ "github.com/xtls/xray-core/transport/internet/tls" 61 | _ "github.com/xtls/xray-core/transport/internet/udp" 62 | _ "github.com/xtls/xray-core/transport/internet/websocket" 63 | 64 | // Transport headers 65 | _ "github.com/xtls/xray-core/transport/internet/headers/http" 66 | _ "github.com/xtls/xray-core/transport/internet/headers/noop" 67 | _ "github.com/xtls/xray-core/transport/internet/headers/srtp" 68 | _ "github.com/xtls/xray-core/transport/internet/headers/tls" 69 | _ "github.com/xtls/xray-core/transport/internet/headers/utp" 70 | _ "github.com/xtls/xray-core/transport/internet/headers/wechat" 71 | _ "github.com/xtls/xray-core/transport/internet/headers/wireguard" 72 | ) 73 | -------------------------------------------------------------------------------- /core/xray/dns.go: -------------------------------------------------------------------------------- 1 | package xray 2 | 3 | import ( 4 | "bytes" 5 | "net" 6 | "os" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/InazumaV/V2bX/api/panel" 11 | "github.com/goccy/go-json" 12 | log "github.com/sirupsen/logrus" 13 | coreConf "github.com/xtls/xray-core/infra/conf" 14 | ) 15 | 16 | func updateDNSConfig(node *panel.NodeInfo) (err error) { 17 | dnsPath := os.Getenv("XRAY_DNS_PATH") 18 | if len(node.RawDNS.DNSJson) != 0 { 19 | var prettyJSON bytes.Buffer 20 | if err := json.Indent(&prettyJSON, node.RawDNS.DNSJson, "", " "); err != nil { 21 | return err 22 | } 23 | err = saveDnsConfig(prettyJSON.Bytes(), dnsPath) 24 | } else if len(node.RawDNS.DNSMap) != 0 { 25 | dnsConfig := DNSConfig{ 26 | Servers: []interface{}{ 27 | "1.1.1.1", 28 | "localhost"}, 29 | Tag: "dns_inbound", 30 | } 31 | for _, value := range node.RawDNS.DNSMap { 32 | address := value["address"].(string) 33 | if strings.Contains(address, ":") && !strings.Contains(address, "/") { 34 | host, port, err := net.SplitHostPort(address) 35 | if err != nil { 36 | return err 37 | } 38 | var uint16Port uint16 39 | if port, err := strconv.ParseUint(port, 10, 16); err == nil { 40 | uint16Port = uint16(port) 41 | } 42 | value["address"] = host 43 | value["port"] = uint16Port 44 | } 45 | dnsConfig.Servers = append(dnsConfig.Servers, value) 46 | 47 | } 48 | dnsConfigJSON, err := json.MarshalIndent(dnsConfig, "", " ") 49 | if err != nil { 50 | log.WithField("err", err).Error("Error marshaling dnsConfig to JSON") 51 | return err 52 | } 53 | err = saveDnsConfig(dnsConfigJSON, dnsPath) 54 | } 55 | return err 56 | } 57 | 58 | func saveDnsConfig(dns []byte, dnsPath string) (err error) { 59 | currentData, err := os.ReadFile(dnsPath) 60 | if err != nil { 61 | log.WithField("err", err).Error("Failed to read XRAY_DNS_PATH") 62 | return err 63 | } 64 | if !bytes.Equal(currentData, dns) { 65 | coreDnsConfig := &coreConf.DNSConfig{} 66 | if err = json.Unmarshal(dns, coreDnsConfig); err != nil { 67 | log.WithField("err", err).Error("Failed to unmarshal DNS config") 68 | } 69 | _, err := coreDnsConfig.Build() 70 | if err != nil { 71 | log.WithField("err", err).Error("Failed to understand DNS config, Please check: https://xtls.github.io/config/dns.html for help") 72 | return err 73 | } 74 | if err = os.Truncate(dnsPath, 0); err != nil { 75 | log.WithField("err", err).Error("Failed to clear XRAY DNS PATH file") 76 | } 77 | if err = os.WriteFile(dnsPath, dns, 0644); err != nil { 78 | log.WithField("err", err).Error("Failed to write DNS to XRAY DNS PATH file") 79 | } 80 | } 81 | return err 82 | } 83 | -------------------------------------------------------------------------------- /core/xray/node.go: -------------------------------------------------------------------------------- 1 | package xray 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/InazumaV/V2bX/api/panel" 8 | "github.com/InazumaV/V2bX/conf" 9 | "github.com/xtls/xray-core/core" 10 | "github.com/xtls/xray-core/features/inbound" 11 | "github.com/xtls/xray-core/features/outbound" 12 | ) 13 | 14 | type DNSConfig struct { 15 | Servers []interface{} `json:"servers"` 16 | Tag string `json:"tag"` 17 | } 18 | 19 | func (c *Xray) AddNode(tag string, info *panel.NodeInfo, config *conf.Options) error { 20 | err := updateDNSConfig(info) 21 | if err != nil { 22 | return fmt.Errorf("build dns error: %s", err) 23 | } 24 | inboundConfig, err := buildInbound(config, info, tag) 25 | if err != nil { 26 | return fmt.Errorf("build inbound error: %s", err) 27 | } 28 | err = c.addInbound(inboundConfig) 29 | if err != nil { 30 | return fmt.Errorf("add inbound error: %s", err) 31 | } 32 | outBoundConfig, err := buildOutbound(config, tag) 33 | if err != nil { 34 | return fmt.Errorf("build outbound error: %s", err) 35 | } 36 | err = c.addOutbound(outBoundConfig) 37 | if err != nil { 38 | return fmt.Errorf("add outbound error: %s", err) 39 | } 40 | return nil 41 | } 42 | 43 | func (c *Xray) addInbound(config *core.InboundHandlerConfig) error { 44 | rawHandler, err := core.CreateObject(c.Server, config) 45 | if err != nil { 46 | return err 47 | } 48 | handler, ok := rawHandler.(inbound.Handler) 49 | if !ok { 50 | return fmt.Errorf("not an InboundHandler: %s", err) 51 | } 52 | if err := c.ihm.AddHandler(context.Background(), handler); err != nil { 53 | return err 54 | } 55 | return nil 56 | } 57 | 58 | func (c *Xray) addOutbound(config *core.OutboundHandlerConfig) error { 59 | rawHandler, err := core.CreateObject(c.Server, config) 60 | if err != nil { 61 | return err 62 | } 63 | handler, ok := rawHandler.(outbound.Handler) 64 | if !ok { 65 | return fmt.Errorf("not an InboundHandler: %s", err) 66 | } 67 | if err := c.ohm.AddHandler(context.Background(), handler); err != nil { 68 | return err 69 | } 70 | return nil 71 | } 72 | 73 | func (c *Xray) DelNode(tag string) error { 74 | err := c.removeInbound(tag) 75 | if err != nil { 76 | return fmt.Errorf("remove in error: %s", err) 77 | } 78 | err = c.removeOutbound(tag) 79 | if err != nil { 80 | return fmt.Errorf("remove out error: %s", err) 81 | } 82 | return nil 83 | } 84 | 85 | func (c *Xray) removeInbound(tag string) error { 86 | return c.ihm.RemoveHandler(context.Background(), tag) 87 | } 88 | 89 | func (c *Xray) removeOutbound(tag string) error { 90 | err := c.ohm.RemoveHandler(context.Background(), tag) 91 | return err 92 | } 93 | -------------------------------------------------------------------------------- /core/xray/outbound.go: -------------------------------------------------------------------------------- 1 | package xray 2 | 3 | import ( 4 | "fmt" 5 | 6 | conf2 "github.com/InazumaV/V2bX/conf" 7 | "github.com/goccy/go-json" 8 | "github.com/xtls/xray-core/core" 9 | "github.com/xtls/xray-core/infra/conf" 10 | ) 11 | 12 | // BuildOutbound build freedom outbund config for addoutbound 13 | func buildOutbound(config *conf2.Options, tag string) (*core.OutboundHandlerConfig, error) { 14 | outboundDetourConfig := &conf.OutboundDetourConfig{} 15 | outboundDetourConfig.Protocol = "freedom" 16 | outboundDetourConfig.Tag = tag 17 | 18 | // Build Send IP address 19 | if config.SendIP != "" { 20 | outboundDetourConfig.SendThrough = &config.SendIP 21 | } 22 | 23 | // Freedom Protocol setting 24 | var domainStrategy = "Asis" 25 | if config.XrayOptions.EnableDNS { 26 | if config.XrayOptions.DNSType != "" { 27 | domainStrategy = config.XrayOptions.DNSType 28 | } else { 29 | domainStrategy = "UseIP" 30 | } 31 | } 32 | proxySetting := &conf.FreedomConfig{ 33 | DomainStrategy: domainStrategy, 34 | } 35 | var setting json.RawMessage 36 | setting, err := json.Marshal(proxySetting) 37 | if err != nil { 38 | return nil, fmt.Errorf("marshal proxy config error: %s", err) 39 | } 40 | outboundDetourConfig.Settings = &setting 41 | return outboundDetourConfig.Build() 42 | } 43 | -------------------------------------------------------------------------------- /core/xray/ss.go: -------------------------------------------------------------------------------- 1 | package xray 2 | 3 | import ( 4 | "encoding/base64" 5 | "strings" 6 | 7 | "github.com/InazumaV/V2bX/api/panel" 8 | "github.com/InazumaV/V2bX/common/format" 9 | "github.com/xtls/xray-core/common/protocol" 10 | "github.com/xtls/xray-core/common/serial" 11 | "github.com/xtls/xray-core/proxy/shadowsocks" 12 | "github.com/xtls/xray-core/proxy/shadowsocks_2022" 13 | ) 14 | 15 | func buildSSUsers(tag string, userInfo []panel.UserInfo, cypher string, serverKey string) (users []*protocol.User) { 16 | users = make([]*protocol.User, len(userInfo)) 17 | for i := range userInfo { 18 | users[i] = buildSSUser(tag, &userInfo[i], cypher, serverKey) 19 | } 20 | return users 21 | } 22 | 23 | func buildSSUser(tag string, userInfo *panel.UserInfo, cypher string, serverKey string) (user *protocol.User) { 24 | if serverKey == "" { 25 | ssAccount := &shadowsocks.Account{ 26 | Password: userInfo.Uuid, 27 | CipherType: getCipherFromString(cypher), 28 | } 29 | return &protocol.User{ 30 | Level: 0, 31 | Email: format.UserTag(tag, userInfo.Uuid), 32 | Account: serial.ToTypedMessage(ssAccount), 33 | } 34 | } else { 35 | var keyLength int 36 | switch cypher { 37 | case "2022-blake3-aes-128-gcm": 38 | keyLength = 16 39 | case "2022-blake3-aes-256-gcm": 40 | keyLength = 32 41 | case "2022-blake3-chacha20-poly1305": 42 | keyLength = 32 43 | } 44 | ssAccount := &shadowsocks_2022.Account{ 45 | Key: base64.StdEncoding.EncodeToString([]byte(userInfo.Uuid[:keyLength])), 46 | } 47 | return &protocol.User{ 48 | Level: 0, 49 | Email: format.UserTag(tag, userInfo.Uuid), 50 | Account: serial.ToTypedMessage(ssAccount), 51 | } 52 | } 53 | } 54 | 55 | func getCipherFromString(c string) shadowsocks.CipherType { 56 | switch strings.ToLower(c) { 57 | case "aes-128-gcm", "aead_aes_128_gcm": 58 | return shadowsocks.CipherType_AES_128_GCM 59 | case "aes-256-gcm", "aead_aes_256_gcm": 60 | return shadowsocks.CipherType_AES_256_GCM 61 | case "chacha20-poly1305", "aead_chacha20_poly1305", "chacha20-ietf-poly1305": 62 | return shadowsocks.CipherType_CHACHA20_POLY1305 63 | case "none", "plain": 64 | return shadowsocks.CipherType_NONE 65 | default: 66 | return shadowsocks.CipherType_UNKNOWN 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /core/xray/trojan.go: -------------------------------------------------------------------------------- 1 | package xray 2 | 3 | import ( 4 | "github.com/InazumaV/V2bX/api/panel" 5 | "github.com/InazumaV/V2bX/common/format" 6 | "github.com/xtls/xray-core/common/protocol" 7 | "github.com/xtls/xray-core/common/serial" 8 | "github.com/xtls/xray-core/proxy/trojan" 9 | ) 10 | 11 | func buildTrojanUsers(tag string, userInfo []panel.UserInfo) (users []*protocol.User) { 12 | users = make([]*protocol.User, len(userInfo)) 13 | for i := range userInfo { 14 | users[i] = buildTrojanUser(tag, &(userInfo)[i]) 15 | } 16 | return users 17 | } 18 | 19 | func buildTrojanUser(tag string, userInfo *panel.UserInfo) (user *protocol.User) { 20 | trojanAccount := &trojan.Account{ 21 | Password: userInfo.Uuid, 22 | } 23 | return &protocol.User{ 24 | Level: 0, 25 | Email: format.UserTag(tag, userInfo.Uuid), 26 | Account: serial.ToTypedMessage(trojanAccount), 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /core/xray/user.go: -------------------------------------------------------------------------------- 1 | package xray 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/InazumaV/V2bX/api/panel" 8 | "github.com/InazumaV/V2bX/common/format" 9 | vCore "github.com/InazumaV/V2bX/core" 10 | "github.com/xtls/xray-core/common/protocol" 11 | "github.com/xtls/xray-core/proxy" 12 | ) 13 | 14 | func (c *Xray) GetUserManager(tag string) (proxy.UserManager, error) { 15 | handler, err := c.ihm.GetHandler(context.Background(), tag) 16 | if err != nil { 17 | return nil, fmt.Errorf("no such inbound tag: %s", err) 18 | } 19 | inboundInstance, ok := handler.(proxy.GetInbound) 20 | if !ok { 21 | return nil, fmt.Errorf("handler %s is not implement proxy.GetInbound", tag) 22 | } 23 | userManager, ok := inboundInstance.GetInbound().(proxy.UserManager) 24 | if !ok { 25 | return nil, fmt.Errorf("handler %s is not implement proxy.UserManager", tag) 26 | } 27 | return userManager, nil 28 | } 29 | 30 | func (c *Xray) DelUsers(users []panel.UserInfo, tag string, _ *panel.NodeInfo) error { 31 | userManager, err := c.GetUserManager(tag) 32 | if err != nil { 33 | return fmt.Errorf("get user manager error: %s", err) 34 | } 35 | var up, down, user string 36 | for i := range users { 37 | user = format.UserTag(tag, users[i].Uuid) 38 | err = userManager.RemoveUser(context.Background(), user) 39 | if err != nil { 40 | return err 41 | } 42 | up = "user>>>" + user + ">>>traffic>>>uplink" 43 | down = "user>>>" + user + ">>>traffic>>>downlink" 44 | c.shm.UnregisterCounter(up) 45 | c.shm.UnregisterCounter(down) 46 | } 47 | return nil 48 | } 49 | 50 | func (c *Xray) GetUserTraffic(tag, uuid string, reset bool) (up int64, down int64) { 51 | upName := "user>>>" + format.UserTag(tag, uuid) + ">>>traffic>>>uplink" 52 | downName := "user>>>" + format.UserTag(tag, uuid) + ">>>traffic>>>downlink" 53 | upCounter := c.shm.GetCounter(upName) 54 | downCounter := c.shm.GetCounter(downName) 55 | if reset { 56 | if upCounter != nil { 57 | up = upCounter.Set(0) 58 | } 59 | if downCounter != nil { 60 | down = downCounter.Set(0) 61 | } 62 | } else { 63 | if upCounter != nil { 64 | up = upCounter.Value() 65 | } 66 | if downCounter != nil { 67 | down = downCounter.Value() 68 | } 69 | } 70 | return up, down 71 | } 72 | 73 | func (c *Xray) AddUsers(p *vCore.AddUsersParams) (added int, err error) { 74 | users := make([]*protocol.User, 0, len(p.Users)) 75 | switch p.NodeInfo.Type { 76 | case "vmess": 77 | users = buildVmessUsers(p.Tag, p.Users) 78 | case "vless": 79 | users = buildVlessUsers(p.Tag, p.Users, p.VAllss.Flow) 80 | case "trojan": 81 | users = buildTrojanUsers(p.Tag, p.Users) 82 | case "shadowsocks": 83 | users = buildSSUsers(p.Tag, 84 | p.Users, 85 | p.Shadowsocks.Cipher, 86 | p.Shadowsocks.ServerKey) 87 | default: 88 | return 0, fmt.Errorf("unsupported node type: %s", p.NodeInfo.Type) 89 | } 90 | man, err := c.GetUserManager(p.Tag) 91 | if err != nil { 92 | return 0, fmt.Errorf("get user manager error: %s", err) 93 | } 94 | for _, u := range users { 95 | mUser, err := u.ToMemoryUser() 96 | if err != nil { 97 | return 0, err 98 | } 99 | err = man.AddUser(context.Background(), mUser) 100 | if err != nil { 101 | return 0, err 102 | } 103 | } 104 | return len(users), nil 105 | } 106 | -------------------------------------------------------------------------------- /core/xray/vmess.go: -------------------------------------------------------------------------------- 1 | package xray 2 | 3 | import ( 4 | "github.com/InazumaV/V2bX/api/panel" 5 | "github.com/InazumaV/V2bX/common/format" 6 | "github.com/xtls/xray-core/common/protocol" 7 | "github.com/xtls/xray-core/common/serial" 8 | "github.com/xtls/xray-core/infra/conf" 9 | "github.com/xtls/xray-core/proxy/vless" 10 | ) 11 | 12 | func buildVmessUsers(tag string, userInfo []panel.UserInfo) (users []*protocol.User) { 13 | users = make([]*protocol.User, len(userInfo)) 14 | for i, user := range userInfo { 15 | users[i] = buildVmessUser(tag, &user) 16 | } 17 | return users 18 | } 19 | 20 | func buildVmessUser(tag string, userInfo *panel.UserInfo) (user *protocol.User) { 21 | vmessAccount := &conf.VMessAccount{ 22 | ID: userInfo.Uuid, 23 | Security: "auto", 24 | } 25 | return &protocol.User{ 26 | Level: 0, 27 | Email: format.UserTag(tag, userInfo.Uuid), // Uid: InboundTag|email 28 | Account: serial.ToTypedMessage(vmessAccount.Build()), 29 | } 30 | } 31 | 32 | func buildVlessUsers(tag string, userInfo []panel.UserInfo, flow string) (users []*protocol.User) { 33 | users = make([]*protocol.User, len(userInfo)) 34 | for i := range userInfo { 35 | users[i] = buildVlessUser(tag, &(userInfo)[i], flow) 36 | } 37 | return users 38 | } 39 | 40 | func buildVlessUser(tag string, userInfo *panel.UserInfo, flow string) (user *protocol.User) { 41 | vlessAccount := &vless.Account{ 42 | Id: userInfo.Uuid, 43 | } 44 | vlessAccount.Flow = flow 45 | return &protocol.User{ 46 | Level: 0, 47 | Email: format.UserTag(tag, userInfo.Uuid), 48 | Account: serial.ToTypedMessage(vlessAccount), 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /core/xray/xray.go: -------------------------------------------------------------------------------- 1 | package xray 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "sync" 7 | 8 | "github.com/InazumaV/V2bX/conf" 9 | vCore "github.com/InazumaV/V2bX/core" 10 | "github.com/InazumaV/V2bX/core/xray/app/dispatcher" 11 | _ "github.com/InazumaV/V2bX/core/xray/distro/all" 12 | "github.com/goccy/go-json" 13 | log "github.com/sirupsen/logrus" 14 | "github.com/xtls/xray-core/app/proxyman" 15 | "github.com/xtls/xray-core/app/stats" 16 | "github.com/xtls/xray-core/common/serial" 17 | "github.com/xtls/xray-core/core" 18 | "github.com/xtls/xray-core/features/inbound" 19 | "github.com/xtls/xray-core/features/outbound" 20 | "github.com/xtls/xray-core/features/routing" 21 | statsFeature "github.com/xtls/xray-core/features/stats" 22 | coreConf "github.com/xtls/xray-core/infra/conf" 23 | ) 24 | 25 | var _ vCore.Core = (*Xray)(nil) 26 | 27 | func init() { 28 | vCore.RegisterCore("xray", New) 29 | } 30 | 31 | // Xray Structure 32 | type Xray struct { 33 | access sync.Mutex 34 | Server *core.Instance 35 | ihm inbound.Manager 36 | ohm outbound.Manager 37 | shm statsFeature.Manager 38 | dispatcher *dispatcher.DefaultDispatcher 39 | } 40 | 41 | func New(c *conf.CoreConfig) (vCore.Core, error) { 42 | return &Xray{Server: getCore(c.XrayConfig)}, nil 43 | } 44 | 45 | func parseConnectionConfig(c *conf.XrayConnectionConfig) (policy *coreConf.Policy) { 46 | policy = &coreConf.Policy{ 47 | StatsUserUplink: true, 48 | StatsUserDownlink: true, 49 | Handshake: &c.Handshake, 50 | ConnectionIdle: &c.ConnIdle, 51 | UplinkOnly: &c.UplinkOnly, 52 | DownlinkOnly: &c.DownlinkOnly, 53 | BufferSize: &c.BufferSize, 54 | } 55 | return 56 | } 57 | 58 | func getCore(c *conf.XrayConfig) *core.Instance { 59 | os.Setenv("XRAY_LOCATION_ASSET", c.AssetPath) 60 | // Log Config 61 | coreLogConfig := &coreConf.LogConfig{ 62 | LogLevel: c.LogConfig.Level, 63 | AccessLog: c.LogConfig.AccessPath, 64 | ErrorLog: c.LogConfig.ErrorPath, 65 | } 66 | // DNS config 67 | coreDnsConfig := &coreConf.DNSConfig{} 68 | os.Setenv("XRAY_DNS_PATH", "") 69 | if c.DnsConfigPath != "" { 70 | data, err := os.ReadFile(c.DnsConfigPath) 71 | if err != nil { 72 | log.Error(fmt.Sprintf("Failed to read xray dns config file: %v", err)) 73 | coreDnsConfig = &coreConf.DNSConfig{} 74 | } else { 75 | if err := json.Unmarshal(data, coreDnsConfig); err != nil { 76 | log.Error(fmt.Sprintf("Failed to unmarshal xray dns config: %v. Using default DNS options.", err)) 77 | coreDnsConfig = &coreConf.DNSConfig{} 78 | } 79 | } 80 | os.Setenv("XRAY_DNS_PATH", c.DnsConfigPath) 81 | } 82 | dnsConfig, err := coreDnsConfig.Build() 83 | if err != nil { 84 | log.WithField("err", err).Panic("Failed to understand DNS config, Please check: https://xtls.github.io/config/dns.html for help") 85 | } 86 | // Routing config 87 | coreRouterConfig := &coreConf.RouterConfig{} 88 | if c.RouteConfigPath != "" { 89 | data, err := os.ReadFile(c.RouteConfigPath) 90 | if err != nil { 91 | log.WithField("err", err).Panic("Failed to read Routing config file") 92 | } else { 93 | if err = json.Unmarshal(data, coreRouterConfig); err != nil { 94 | log.WithField("err", err).Panic("Failed to unmarshal Routing config") 95 | } 96 | } 97 | } 98 | routeConfig, err := coreRouterConfig.Build() 99 | if err != nil { 100 | log.WithField("err", err).Panic("Failed to understand Routing config. Please check: https://xtls.github.io/config/routing.html for help") 101 | } 102 | // Custom Inbound config 103 | var coreCustomInboundConfig []coreConf.InboundDetourConfig 104 | if c.InboundConfigPath != "" { 105 | data, err := os.ReadFile(c.InboundConfigPath) 106 | if err != nil { 107 | log.WithField("err", err).Panic("Failed to read Custom Inbound config file") 108 | } else { 109 | if err = json.Unmarshal(data, &coreCustomInboundConfig); err != nil { 110 | log.WithField("err", err).Panic("Failed to unmarshal Custom Inbound config") 111 | } 112 | } 113 | } 114 | var inBoundConfig []*core.InboundHandlerConfig 115 | for _, config := range coreCustomInboundConfig { 116 | oc, err := config.Build() 117 | if err != nil { 118 | log.WithField("err", err).Panic("Failed to understand Inbound config. Please check: https://xtls.github.io/config/inbound.html for help") 119 | } 120 | inBoundConfig = append(inBoundConfig, oc) 121 | } 122 | // Custom Outbound config 123 | var coreCustomOutboundConfig []coreConf.OutboundDetourConfig 124 | if c.OutboundConfigPath != "" { 125 | data, err := os.ReadFile(c.OutboundConfigPath) 126 | if err != nil { 127 | log.WithField("err", err).Panic("Failed to read Custom Outbound config file") 128 | } else { 129 | if err = json.Unmarshal(data, &coreCustomOutboundConfig); err != nil { 130 | log.WithField("err", err).Panic("Failed to unmarshal Custom Outbound config") 131 | } 132 | } 133 | } 134 | var outBoundConfig []*core.OutboundHandlerConfig 135 | for _, config := range coreCustomOutboundConfig { 136 | oc, err := config.Build() 137 | if err != nil { 138 | log.WithField("err", err).Panic("Failed to understand Outbound config, Please check: https://xtls.github.io/config/outbound.html for help") 139 | } 140 | outBoundConfig = append(outBoundConfig, oc) 141 | } 142 | // Policy config 143 | levelPolicyConfig := parseConnectionConfig(c.ConnectionConfig) 144 | corePolicyConfig := &coreConf.PolicyConfig{} 145 | corePolicyConfig.Levels = map[uint32]*coreConf.Policy{0: levelPolicyConfig} 146 | policyConfig, _ := corePolicyConfig.Build() 147 | // Build Xray conf 148 | config := &core.Config{ 149 | App: []*serial.TypedMessage{ 150 | serial.ToTypedMessage(coreLogConfig.Build()), 151 | serial.ToTypedMessage(&dispatcher.Config{}), 152 | serial.ToTypedMessage(&stats.Config{}), 153 | serial.ToTypedMessage(&proxyman.InboundConfig{}), 154 | serial.ToTypedMessage(&proxyman.OutboundConfig{}), 155 | serial.ToTypedMessage(policyConfig), 156 | serial.ToTypedMessage(dnsConfig), 157 | serial.ToTypedMessage(routeConfig), 158 | }, 159 | Inbound: inBoundConfig, 160 | Outbound: outBoundConfig, 161 | } 162 | server, err := core.New(config) 163 | if err != nil { 164 | log.WithField("err", err).Panic("failed to create instance") 165 | } 166 | log.Info("Xray Core Version: ", core.Version()) 167 | return server 168 | } 169 | 170 | // Start the Xray 171 | func (c *Xray) Start() error { 172 | c.access.Lock() 173 | defer c.access.Unlock() 174 | if err := c.Server.Start(); err != nil { 175 | return err 176 | } 177 | c.shm = c.Server.GetFeature(statsFeature.ManagerType()).(statsFeature.Manager) 178 | c.ihm = c.Server.GetFeature(inbound.ManagerType()).(inbound.Manager) 179 | c.ohm = c.Server.GetFeature(outbound.ManagerType()).(outbound.Manager) 180 | c.dispatcher = c.Server.GetFeature(routing.DispatcherType()).(*dispatcher.DefaultDispatcher) 181 | return nil 182 | } 183 | 184 | // Close the core 185 | func (c *Xray) Close() error { 186 | c.access.Lock() 187 | defer c.access.Unlock() 188 | c.ihm = nil 189 | c.ohm = nil 190 | c.shm = nil 191 | c.dispatcher = nil 192 | err := c.Server.Close() 193 | if err != nil { 194 | return err 195 | } 196 | return nil 197 | } 198 | 199 | func (c *Xray) Protocols() []string { 200 | return []string{ 201 | "vmess", 202 | "vless", 203 | "shadowsocks", 204 | "trojan", 205 | } 206 | } 207 | 208 | func (c *Xray) Type() string { 209 | return "xray" 210 | } 211 | -------------------------------------------------------------------------------- /example/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "Log": { 3 | "Level": "info", 4 | "Output": "" 5 | }, 6 | "Cores": [ 7 | { 8 | "Type": "sing", 9 | "Log": { 10 | "Level": "error", 11 | "Timestamp": true 12 | }, 13 | "NTP": { 14 | "Enable": true, 15 | "Server": "time.apple.com", 16 | "ServerPort": 0 17 | } 18 | } 19 | ], 20 | "Nodes": [ 21 | { 22 | "Core": "sing", 23 | "ApiHost": "http://127.0.0.1", 24 | "ApiKey": "test", 25 | "NodeID": 33, 26 | "NodeType": "shadowsocks", 27 | "Timeout": 30, 28 | "ListenIP": "0.0.0.0", 29 | "SendIP": "0.0.0.0", 30 | "EnableProxyProtocol": false, 31 | "EnableDNS": true, 32 | "DomainStrategy": "ipv4_only", 33 | "LimitConfig": { 34 | "EnableRealtime": false, 35 | "SpeedLimit": 0, 36 | "IPLimit": 0, 37 | "ConnLimit": 0, 38 | "EnableDynamicSpeedLimit": false, 39 | "DynamicSpeedLimitConfig": { 40 | "Periodic": 60, 41 | "Traffic": 1000, 42 | "SpeedLimit": 100, 43 | "ExpireTime": 60 44 | } 45 | } 46 | } 47 | ] 48 | } -------------------------------------------------------------------------------- /example/config_full.json: -------------------------------------------------------------------------------- 1 | { 2 | "Log": { 3 | // V2bX 的日志配置,独立于各 Core 的 log 配置 4 | 5 | // 日志等级,info, warn, error, none 6 | "Level": "error", 7 | // 日志输出路径,默认输出到标准输出 8 | "Output": "" 9 | }, 10 | "Cores": [ 11 | { 12 | // Core类型 13 | "Type": "sing", 14 | // Core标识名,可选,如果需要启动多个同类型内核则必填 15 | "Name": "sing1", 16 | "Log": { 17 | // 同 SingBox log 部分配置 18 | 19 | "Level": "error", 20 | "Timestamp": true 21 | }, 22 | "NTP": { 23 | // 同 SingBox ntp 部分配置 24 | // VMess VLESS 建议开启 25 | "Enable": true, 26 | "Server": "time.apple.com", 27 | "ServerPort": 0 28 | }, 29 | "DnsConfigPath": "/etc/V2bX/dns.json", 30 | // SingBox源配置文件目录,用于引用标准SingBox配置文件 31 | "OriginalPath": "/etc/V2bX/sing_origin.json" 32 | }, 33 | { 34 | "Type": "sing", 35 | "Name": "sing2", 36 | "Log": { 37 | "Level": "info", 38 | "Timestamp": false 39 | } 40 | }, 41 | { 42 | "Type": "xray", 43 | "Log": { 44 | // 同 Xray-core log 部分配置 45 | 46 | "Level": "error" 47 | }, 48 | // 静态资源文件目录 49 | "AssetPath": "", 50 | // DNS配置文件目录 51 | "DnsConfigPath": "", 52 | // 路由配置文件目录 53 | "RouteConfigPath": "", 54 | // 本地策略相关配置 55 | "ConnectionConfig": { 56 | // 详见 https://xtls.github.io/config/policy.html#levelpolicyobject 57 | 58 | "handshake": 4, 59 | "connIdle": 300, 60 | "uplinkOnly": 2, 61 | "downlinkOnly": 5, 62 | "statsUserUplink": false, 63 | "statsUserDownlink": false, 64 | "bufferSize": 4 65 | }, 66 | // Inbound配置文件目录 67 | "InboundConfigPath": "", 68 | // Outbound配置文件目录 69 | "OutboundConfigPath": "" 70 | } 71 | ], 72 | "Nodes": [ 73 | // Node配置有两种写法 74 | { 75 | // 写法1 76 | // sing内核 77 | 78 | // Node标识名,便于查看日志,不填将通过下发的节点配置自动生成 79 | // 务必注意不要重复,否则会出现问题 80 | "Name": "sing_node1", 81 | 82 | // 要使用的Core的类型 83 | // 如果填写了CoreName可不填,但单内核务必填写 84 | // 建议视情况填写Core和CoreName其中一个,如果均没有填写将随机选择支持的内核 85 | "Core": "sing", 86 | 87 | // 要使用的Core的标识名,如果没有定义多个同类型内核可不填 88 | "CoreName": "sing1", 89 | 90 | // API接口地址 91 | "ApiHost": "http://127.0.0.1", 92 | 93 | // API密钥,即Token 94 | "ApiKey": "test", 95 | 96 | // 节点ID 97 | "NodeID": 33, 98 | 99 | // 节点类型 100 | "NodeType": "shadowsocks", 101 | 102 | // 请求超时时间 103 | "Timeout": 30, 104 | 105 | // 监听IP 106 | "ListenIP": "0.0.0.0", 107 | 108 | // 发送IP 109 | "SendIP": "0.0.0.0", 110 | 111 | // 开启 Proxy Protocol,参见 https://github.com/haproxy/haproxy/blob/master/doc/proxy-protocol.txt 112 | "EnableProxyProtocol": false, 113 | 114 | // 开启 TCP Fast Open 115 | "EnableTFO": true, 116 | 117 | // 开启 DNS 118 | "EnableDNS" : true, 119 | // 设置 Domain Strategy 需要开启 DNS ,默认 AsIS 120 | // 可选 prefer_ipv4 / prefer_ipv6 / ipv4_only / ipv6_only 121 | "DomainStrategy": "ipv4_only", 122 | 123 | // 限制器相关配置 124 | "LimitConfig": { 125 | // 开启实时连接数及IP数限制 126 | "EnableRealtime": false, 127 | 128 | // 用户速度限制 129 | "SpeedLimit": 0, 130 | 131 | // 用户IP限制 132 | "IPLimit": 0, 133 | 134 | // 用户连接数限制 135 | "ConnLimit": 0, 136 | 137 | // 开启动态限速 138 | "EnableDynamicSpeedLimit": false, 139 | 140 | // 动态限速相关配置 141 | "DynamicSpeedLimitConfig": { 142 | // 检查周期 143 | "Periodic": 60, 144 | 145 | // 检查周期内触发限制的流量数 146 | "Traffic": 1000, 147 | 148 | // 触发限制后的速度限制 149 | "SpeedLimit": 100, 150 | 151 | // 速度限制过期时间 152 | "ExpireTime": 60 153 | 154 | } 155 | }, 156 | 157 | // 证书相关配置 158 | "CertConfig": { 159 | // 证书申请模式,none、http、dns、self 160 | "CertMode": "none", 161 | 162 | "RejectUnknownSni": false, 163 | 164 | // 证书域名 165 | "CertDomain": "test.com", 166 | 167 | // 证书文件目录 168 | "CertFile": "/etc/V2bX/cert/1.pem", 169 | 170 | // 密钥文件目录 171 | "KeyFile": "/etc/V2bX/cert/1.key", 172 | 173 | // 申请证书时使用的用户邮箱 174 | "Email": "1@test.com", 175 | 176 | // DNS解析提供者 177 | "Provider": "cloudflare", 178 | 179 | // DNS解析提供者的环境变量,详见 https://go-acme.github.io/lego/dns/ 180 | "DNSEnv": { 181 | "EnvName": "env1" 182 | } 183 | } 184 | }, 185 | { 186 | // xray内核 187 | 188 | "Name": "xray_node1", 189 | "Core": "xray", 190 | "CoreName": "", 191 | "ApiHost": "http://127.0.0.1", 192 | "ApiKey": "test", 193 | "NodeID": 33, 194 | "NodeType": "shadowsocks", 195 | "Timeout": 30, 196 | "ListenIP": "0.0.0.0", 197 | "SendIP": "0.0.0.0", 198 | "EnableProxyProtocol": true, 199 | "EnableTFO": true, 200 | // 以上同 sing 201 | 202 | // 开启自定义DNS 203 | "EnableDNS": false, 204 | 205 | // DNS解析类型,AsIs、UseIP、UseIPv4、UseIPv6 206 | "DNSType": "AsIs", 207 | 208 | // 开启udp over tcp 209 | "EnableUot": false, 210 | 211 | // 禁用IVCheck 212 | "DisableIVCheck": false, 213 | 214 | // 禁用嗅探 215 | "DisableSniffing": false, 216 | 217 | // 开启回落 218 | "EnableFallback": false, 219 | 220 | // 回落相关配置 221 | "FallBackConfigs":{ 222 | // 详见 https://xtls.github.io/config/features/fallback.html#fallbackobject 223 | 224 | "SNI": "", 225 | "Alpn": "", 226 | "Path": "", 227 | "Dest": "", 228 | "ProxyProtocolVer": 0 229 | } 230 | }, 231 | { 232 | // 写法2 233 | 234 | // 类似旧配置文件 ApiConfig 部分 235 | "ApiConfig": { 236 | "ApiHost": "http://127.0.0.1", 237 | "ApiKey": "test", 238 | "NodeID": 33, 239 | "Timeout": 30 240 | }, 241 | // 类似旧配置文件 ControllerConfig 部分 242 | "Options": { 243 | "Core": "sing", 244 | "EnableProxyProtocol": true, 245 | "EnableTFO": true, 246 | "DomainStrategy": "ipv4_only" 247 | // More 248 | } 249 | }, 250 | { 251 | // 引用本地其他配置文件 252 | "Include": "../example/config_full_node1.json" 253 | }, 254 | { 255 | // 通过Http引用远端配置文件 256 | "Include": "http://127.0.0.1:11451/config_full_node1.json" 257 | } 258 | ] 259 | } -------------------------------------------------------------------------------- /example/config_full_node1.json: -------------------------------------------------------------------------------- 1 | { 2 | "Core": "xray", 3 | "ApiHost": "https://127.0.0.1", 4 | "ApiKey": "key", 5 | "NodeID": 1, 6 | "NodeType": "vmess", 7 | "Timeout": 30, 8 | "ListenIP": "0.0.0.0", 9 | "SendIP": "0.0.0.0", 10 | "EnableProxyProtocol": false, 11 | "EnableTFO": true, 12 | "DNSType": "ipv4_only" 13 | } -------------------------------------------------------------------------------- /example/custom_inbound.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "listen": "0.0.0.0", 4 | "port": 1234, 5 | "protocol": "socks", 6 | "settings": { 7 | "auth": "noauth", 8 | "accounts": [ 9 | { 10 | "user": "my-username", 11 | "pass": "my-password" 12 | } 13 | ], 14 | "udp": false, 15 | "ip": "127.0.0.1", 16 | "userLevel": 0 17 | } 18 | } 19 | ] -------------------------------------------------------------------------------- /example/custom_outbound.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "tag": "IPv4_out", 4 | "protocol": "freedom", 5 | "settings": {} 6 | }, 7 | { 8 | "tag": "IPv6_out", 9 | "protocol": "freedom", 10 | "settings": { 11 | "domainStrategy": "UseIPv6" 12 | } 13 | }, 14 | { 15 | "tag": "socks5-warp", 16 | "protocol": "socks", 17 | "settings": { 18 | "servers": [{ 19 | "address": "127.0.0.1", 20 | "port": 40000 21 | }] 22 | } 23 | }, 24 | { 25 | "protocol": "blackhole", 26 | "tag": "block" 27 | } 28 | ] -------------------------------------------------------------------------------- /example/dns.json: -------------------------------------------------------------------------------- 1 | { 2 | "servers": [ 3 | "1.1.1.1", 4 | "8.8.8.8", 5 | "localhost" 6 | ], 7 | "tag": "dns_inbound" 8 | } -------------------------------------------------------------------------------- /example/geoip.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wyx2685/V2bX/fc284b3b9f1ec09319c6f2bb13d7131bae4eda89/example/geoip.dat -------------------------------------------------------------------------------- /example/geoip.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wyx2685/V2bX/fc284b3b9f1ec09319c6f2bb13d7131bae4eda89/example/geoip.db -------------------------------------------------------------------------------- /example/geosite.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wyx2685/V2bX/fc284b3b9f1ec09319c6f2bb13d7131bae4eda89/example/geosite.dat -------------------------------------------------------------------------------- /example/geosite.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wyx2685/V2bX/fc284b3b9f1ec09319c6f2bb13d7131bae4eda89/example/geosite.db -------------------------------------------------------------------------------- /example/route.json: -------------------------------------------------------------------------------- 1 | { 2 | "domainStrategy": "IPOnDemand", 3 | "rules": [ 4 | { 5 | "type": "field", 6 | "outboundTag": "block", 7 | "ip": [ 8 | "geoip:private" 9 | ] 10 | }, 11 | { 12 | "type": "field", 13 | "outboundTag": "block", 14 | "protocol": [ 15 | "bittorrent" 16 | ] 17 | }, 18 | { 19 | "type": "field", 20 | "outboundTag": "socks5-warp", 21 | "domain": [""] 22 | }, 23 | { 24 | "type": "field", 25 | "outboundTag": "IPv6_out", 26 | "domain": [ 27 | "geosite:netflix" 28 | ] 29 | }, 30 | { 31 | "type": "field", 32 | "outboundTag": "IPv4_out", 33 | "network": "udp,tcp" 34 | } 35 | ] 36 | } -------------------------------------------------------------------------------- /limiter/dynamic.go: -------------------------------------------------------------------------------- 1 | package limiter 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/InazumaV/V2bX/api/panel" 7 | "github.com/InazumaV/V2bX/common/format" 8 | ) 9 | 10 | func (l *Limiter) AddDynamicSpeedLimit(tag string, userInfo *panel.UserInfo, limitNum int, expire int64) error { 11 | userLimit := &UserLimitInfo{ 12 | DynamicSpeedLimit: limitNum, 13 | ExpireTime: time.Now().Add(time.Duration(expire) * time.Second).Unix(), 14 | } 15 | l.UserLimitInfo.Store(format.UserTag(tag, userInfo.Uuid), userLimit) 16 | return nil 17 | } 18 | 19 | // determineSpeedLimit returns the minimum non-zero rate 20 | func determineSpeedLimit(limit1, limit2 int) (limit int) { 21 | if limit1 == 0 || limit2 == 0 { 22 | if limit1 > limit2 { 23 | return limit1 24 | } else if limit1 < limit2 { 25 | return limit2 26 | } else { 27 | return 0 28 | } 29 | } else { 30 | if limit1 > limit2 { 31 | return limit2 32 | } else if limit1 < limit2 { 33 | return limit1 34 | } else { 35 | return limit1 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /limiter/limiter.go: -------------------------------------------------------------------------------- 1 | package limiter 2 | 3 | import ( 4 | "errors" 5 | "regexp" 6 | "strings" 7 | "sync" 8 | "time" 9 | 10 | "github.com/InazumaV/V2bX/api/panel" 11 | "github.com/InazumaV/V2bX/common/format" 12 | "github.com/InazumaV/V2bX/conf" 13 | "github.com/juju/ratelimit" 14 | ) 15 | 16 | var limitLock sync.RWMutex 17 | var limiter map[string]*Limiter 18 | 19 | func Init() { 20 | limiter = map[string]*Limiter{} 21 | } 22 | 23 | type Limiter struct { 24 | DomainRules []*regexp.Regexp 25 | ProtocolRules []string 26 | SpeedLimit int 27 | UserOnlineIP *sync.Map // Key: Name, value: {Key: Ip, value: Uid} 28 | OldUserOnline *sync.Map // Key: Ip, value: Uid 29 | UUIDtoUID map[string]int // Key: UUID, value: Uid 30 | UserLimitInfo *sync.Map // Key: Uid value: UserLimitInfo 31 | SpeedLimiter *sync.Map // key: Uid, value: *ratelimit.Bucket 32 | AliveList map[int]int // Key: Uid, value: alive_ip 33 | } 34 | 35 | type UserLimitInfo struct { 36 | UID int 37 | SpeedLimit int 38 | DeviceLimit int 39 | DynamicSpeedLimit int 40 | ExpireTime int64 41 | OverLimit bool 42 | } 43 | 44 | func AddLimiter(tag string, l *conf.LimitConfig, users []panel.UserInfo, aliveList map[int]int) *Limiter { 45 | info := &Limiter{ 46 | SpeedLimit: l.SpeedLimit, 47 | UserOnlineIP: new(sync.Map), 48 | UserLimitInfo: new(sync.Map), 49 | SpeedLimiter: new(sync.Map), 50 | AliveList: aliveList, 51 | OldUserOnline: new(sync.Map), 52 | } 53 | uuidmap := make(map[string]int) 54 | for i := range users { 55 | uuidmap[users[i].Uuid] = users[i].Id 56 | userLimit := &UserLimitInfo{} 57 | userLimit.UID = users[i].Id 58 | if users[i].SpeedLimit != 0 { 59 | userLimit.SpeedLimit = users[i].SpeedLimit 60 | } 61 | if users[i].DeviceLimit != 0 { 62 | userLimit.DeviceLimit = users[i].DeviceLimit 63 | } 64 | userLimit.OverLimit = false 65 | info.UserLimitInfo.Store(format.UserTag(tag, users[i].Uuid), userLimit) 66 | } 67 | info.UUIDtoUID = uuidmap 68 | limitLock.Lock() 69 | limiter[tag] = info 70 | limitLock.Unlock() 71 | return info 72 | } 73 | 74 | func GetLimiter(tag string) (info *Limiter, err error) { 75 | limitLock.RLock() 76 | info, ok := limiter[tag] 77 | limitLock.RUnlock() 78 | if !ok { 79 | return nil, errors.New("not found") 80 | } 81 | return info, nil 82 | } 83 | 84 | func DeleteLimiter(tag string) { 85 | limitLock.Lock() 86 | delete(limiter, tag) 87 | limitLock.Unlock() 88 | } 89 | 90 | func (l *Limiter) UpdateUser(tag string, added []panel.UserInfo, deleted []panel.UserInfo) { 91 | for i := range deleted { 92 | l.UserLimitInfo.Delete(format.UserTag(tag, deleted[i].Uuid)) 93 | l.UserOnlineIP.Delete(format.UserTag(tag, deleted[i].Uuid)) 94 | delete(l.UUIDtoUID, deleted[i].Uuid) 95 | delete(l.AliveList, deleted[i].Id) 96 | } 97 | for i := range added { 98 | userLimit := &UserLimitInfo{ 99 | UID: added[i].Id, 100 | } 101 | if added[i].SpeedLimit != 0 { 102 | userLimit.SpeedLimit = added[i].SpeedLimit 103 | userLimit.ExpireTime = 0 104 | } 105 | if added[i].DeviceLimit != 0 { 106 | userLimit.DeviceLimit = added[i].DeviceLimit 107 | } 108 | userLimit.OverLimit = false 109 | l.UserLimitInfo.Store(format.UserTag(tag, added[i].Uuid), userLimit) 110 | l.UUIDtoUID[added[i].Uuid] = added[i].Id 111 | } 112 | } 113 | 114 | func (l *Limiter) UpdateDynamicSpeedLimit(tag, uuid string, limit int, expire time.Time) error { 115 | if v, ok := l.UserLimitInfo.Load(format.UserTag(tag, uuid)); ok { 116 | info := v.(*UserLimitInfo) 117 | info.DynamicSpeedLimit = limit 118 | info.ExpireTime = expire.Unix() 119 | } else { 120 | return errors.New("not found") 121 | } 122 | return nil 123 | } 124 | 125 | func (l *Limiter) CheckLimit(taguuid string, ip string, isTcp bool, noSSUDP bool) (Bucket *ratelimit.Bucket, Reject bool) { 126 | // check if ipv4 mapped ipv6 127 | ip = strings.TrimPrefix(ip, "::ffff:") 128 | 129 | // check and gen speed limit Bucket 130 | nodeLimit := l.SpeedLimit 131 | userLimit := 0 132 | deviceLimit := 0 133 | var uid int 134 | if v, ok := l.UserLimitInfo.Load(taguuid); ok { 135 | u := v.(*UserLimitInfo) 136 | deviceLimit = u.DeviceLimit 137 | uid = u.UID 138 | if u.ExpireTime < time.Now().Unix() && u.ExpireTime != 0 { 139 | if u.SpeedLimit != 0 { 140 | userLimit = u.SpeedLimit 141 | u.DynamicSpeedLimit = 0 142 | u.ExpireTime = 0 143 | } else { 144 | l.UserLimitInfo.Delete(taguuid) 145 | } 146 | } else { 147 | userLimit = determineSpeedLimit(u.SpeedLimit, u.DynamicSpeedLimit) 148 | } 149 | } else { 150 | return nil, true 151 | } 152 | if noSSUDP { 153 | // Store online user for device limit 154 | newipMap := new(sync.Map) 155 | newipMap.Store(ip, uid) 156 | aliveIp := l.AliveList[uid] 157 | // If any device is online 158 | if v, loaded := l.UserOnlineIP.LoadOrStore(taguuid, newipMap); loaded { 159 | oldipMap := v.(*sync.Map) 160 | // If this is a new ip 161 | if _, loaded := oldipMap.LoadOrStore(ip, uid); !loaded { 162 | if v, loaded := l.OldUserOnline.Load(ip); loaded { 163 | if v.(int) == uid { 164 | l.OldUserOnline.Delete(ip) 165 | } 166 | } else if deviceLimit > 0 { 167 | if deviceLimit <= aliveIp { 168 | oldipMap.Delete(ip) 169 | return nil, true 170 | } 171 | } 172 | } 173 | } else if v, ok := l.OldUserOnline.Load(ip); ok { 174 | if v.(int) == uid { 175 | l.OldUserOnline.Delete(ip) 176 | } 177 | } else { 178 | if deviceLimit > 0 { 179 | if deviceLimit <= aliveIp { 180 | l.UserOnlineIP.Delete(taguuid) 181 | return nil, true 182 | } 183 | } 184 | } 185 | } 186 | 187 | limit := int64(determineSpeedLimit(nodeLimit, userLimit)) * 1000000 / 8 // If you need the Speed limit 188 | if limit > 0 { 189 | Bucket = ratelimit.NewBucketWithQuantum(time.Second, limit, limit) // Byte/s 190 | if v, ok := l.SpeedLimiter.LoadOrStore(taguuid, Bucket); ok { 191 | return v.(*ratelimit.Bucket), false 192 | } else { 193 | l.SpeedLimiter.Store(taguuid, Bucket) 194 | return Bucket, false 195 | } 196 | } else { 197 | return nil, false 198 | } 199 | } 200 | 201 | func (l *Limiter) GetOnlineDevice() (*[]panel.OnlineUser, error) { 202 | var onlineUser []panel.OnlineUser 203 | l.OldUserOnline = new(sync.Map) 204 | l.UserOnlineIP.Range(func(key, value interface{}) bool { 205 | taguuid := key.(string) 206 | ipMap := value.(*sync.Map) 207 | ipMap.Range(func(key, value interface{}) bool { 208 | uid := value.(int) 209 | ip := key.(string) 210 | l.OldUserOnline.Store(ip, uid) 211 | onlineUser = append(onlineUser, panel.OnlineUser{UID: uid, IP: ip}) 212 | return true 213 | }) 214 | l.UserOnlineIP.Delete(taguuid) // Reset online device 215 | return true 216 | }) 217 | 218 | return &onlineUser, nil 219 | } 220 | 221 | type UserIpList struct { 222 | Uid int `json:"Uid"` 223 | IpList []string `json:"Ips"` 224 | } 225 | -------------------------------------------------------------------------------- /limiter/rule.go: -------------------------------------------------------------------------------- 1 | package limiter 2 | 3 | import ( 4 | "regexp" 5 | 6 | "github.com/InazumaV/V2bX/api/panel" 7 | ) 8 | 9 | func (l *Limiter) CheckDomainRule(destination string) (reject bool) { 10 | // have rule 11 | for i := range l.DomainRules { 12 | if l.DomainRules[i].MatchString(destination) { 13 | reject = true 14 | break 15 | } 16 | } 17 | return 18 | } 19 | 20 | func (l *Limiter) CheckProtocolRule(protocol string) (reject bool) { 21 | for i := range l.ProtocolRules { 22 | if l.ProtocolRules[i] == protocol { 23 | reject = true 24 | break 25 | } 26 | } 27 | return 28 | } 29 | 30 | func (l *Limiter) UpdateRule(rule *panel.Rules) error { 31 | l.DomainRules = make([]*regexp.Regexp, len(rule.Regexp)) 32 | for i := range rule.Regexp { 33 | l.DomainRules[i] = regexp.MustCompile(rule.Regexp[i]) 34 | } 35 | l.ProtocolRules = rule.Protocol 36 | return nil 37 | } 38 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/InazumaV/V2bX/cmd" 5 | ) 6 | 7 | func main() { 8 | cmd.Run() 9 | } 10 | -------------------------------------------------------------------------------- /node/cert.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | import ( 4 | "crypto/rand" 5 | "crypto/rsa" 6 | "crypto/x509" 7 | "crypto/x509/pkix" 8 | "encoding/pem" 9 | "fmt" 10 | "math/big" 11 | "os" 12 | "time" 13 | 14 | "github.com/InazumaV/V2bX/common/file" 15 | log "github.com/sirupsen/logrus" 16 | ) 17 | 18 | func (c *Controller) renewCertTask() error { 19 | l, err := NewLego(c.CertConfig) 20 | if err != nil { 21 | log.WithField("tag", c.tag).Info("new lego error: ", err) 22 | return nil 23 | } 24 | err = l.RenewCert() 25 | if err != nil { 26 | log.WithField("tag", c.tag).Info("renew cert error: ", err) 27 | return nil 28 | } 29 | return nil 30 | } 31 | 32 | func (c *Controller) requestCert() error { 33 | switch c.CertConfig.CertMode { 34 | case "none", "": 35 | case "file": 36 | if c.CertConfig.CertFile == "" || c.CertConfig.KeyFile == "" { 37 | return fmt.Errorf("cert file path or key file path not exist") 38 | } 39 | case "dns", "http": 40 | if c.CertConfig.CertFile == "" || c.CertConfig.KeyFile == "" { 41 | return fmt.Errorf("cert file path or key file path not exist") 42 | } 43 | if file.IsExist(c.CertConfig.CertFile) && file.IsExist(c.CertConfig.KeyFile) { 44 | return nil 45 | } 46 | l, err := NewLego(c.CertConfig) 47 | if err != nil { 48 | return fmt.Errorf("create lego object error: %s", err) 49 | } 50 | err = l.CreateCert() 51 | if err != nil { 52 | return fmt.Errorf("create lego cert error: %s", err) 53 | } 54 | case "self": 55 | if c.CertConfig.CertFile == "" || c.CertConfig.KeyFile == "" { 56 | return fmt.Errorf("cert file path or key file path not exist") 57 | } 58 | if file.IsExist(c.CertConfig.CertFile) && file.IsExist(c.CertConfig.KeyFile) { 59 | return nil 60 | } 61 | err := generateSelfSslCertificate( 62 | c.CertConfig.CertDomain, 63 | c.CertConfig.CertFile, 64 | c.CertConfig.KeyFile) 65 | if err != nil { 66 | return fmt.Errorf("generate self cert error: %s", err) 67 | } 68 | default: 69 | return fmt.Errorf("unsupported certmode: %s", c.CertConfig.CertMode) 70 | } 71 | return nil 72 | } 73 | 74 | func generateSelfSslCertificate(domain, certPath, keyPath string) error { 75 | key, _ := rsa.GenerateKey(rand.Reader, 2048) 76 | tmpl := &x509.Certificate{ 77 | Version: 3, 78 | SerialNumber: big.NewInt(time.Now().Unix()), 79 | Subject: pkix.Name{ 80 | CommonName: domain, 81 | }, 82 | DNSNames: []string{domain}, 83 | BasicConstraintsValid: true, 84 | KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, 85 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, 86 | NotBefore: time.Now(), 87 | NotAfter: time.Now().AddDate(30, 0, 0), 88 | } 89 | cert, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, key.Public(), key) 90 | if err != nil { 91 | return err 92 | } 93 | f, err := os.OpenFile(certPath, os.O_CREATE|os.O_RDWR, 0644) 94 | if err != nil { 95 | return err 96 | } 97 | err = pem.Encode(f, &pem.Block{ 98 | Type: "CERTIFICATE", 99 | Bytes: cert, 100 | }) 101 | if err != nil { 102 | return err 103 | } 104 | f, err = os.OpenFile(keyPath, os.O_CREATE|os.O_RDWR, 0644) 105 | if err != nil { 106 | return err 107 | } 108 | err = pem.Encode(f, &pem.Block{ 109 | Type: "EC PRIVATE KEY", 110 | Bytes: x509.MarshalPKCS1PrivateKey(key), 111 | }) 112 | if err != nil { 113 | return err 114 | } 115 | return nil 116 | } 117 | -------------------------------------------------------------------------------- /node/cert_test.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | import "testing" 4 | 5 | func Test_generateSelfSslCertificate(t *testing.T) { 6 | t.Log(generateSelfSslCertificate("domain.com", "1.pem", "1.key")) 7 | } 8 | -------------------------------------------------------------------------------- /node/controller.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/InazumaV/V2bX/api/panel" 8 | "github.com/InazumaV/V2bX/common/task" 9 | "github.com/InazumaV/V2bX/conf" 10 | vCore "github.com/InazumaV/V2bX/core" 11 | "github.com/InazumaV/V2bX/limiter" 12 | log "github.com/sirupsen/logrus" 13 | ) 14 | 15 | type Controller struct { 16 | server vCore.Core 17 | apiClient *panel.Client 18 | tag string 19 | limiter *limiter.Limiter 20 | traffic map[string]int64 21 | userList []panel.UserInfo 22 | aliveMap map[int]int 23 | info *panel.NodeInfo 24 | nodeInfoMonitorPeriodic *task.Task 25 | userReportPeriodic *task.Task 26 | renewCertPeriodic *task.Task 27 | dynamicSpeedLimitPeriodic *task.Task 28 | onlineIpReportPeriodic *task.Task 29 | *conf.Options 30 | } 31 | 32 | // NewController return a Node controller with default parameters. 33 | func NewController(server vCore.Core, api *panel.Client, config *conf.Options) *Controller { 34 | controller := &Controller{ 35 | server: server, 36 | Options: config, 37 | apiClient: api, 38 | } 39 | return controller 40 | } 41 | 42 | // Start implement the Start() function of the service interface 43 | func (c *Controller) Start() error { 44 | // First fetch Node Info 45 | var err error 46 | node, err := c.apiClient.GetNodeInfo() 47 | if err != nil { 48 | return fmt.Errorf("get node info error: %s", err) 49 | } 50 | // Update user 51 | c.userList, err = c.apiClient.GetUserList() 52 | if err != nil { 53 | return fmt.Errorf("get user list error: %s", err) 54 | } 55 | if len(c.userList) == 0 { 56 | return errors.New("add users error: not have any user") 57 | } 58 | c.aliveMap, err = c.apiClient.GetUserAlive() 59 | if err != nil { 60 | return fmt.Errorf("failed to get user alive list: %s", err) 61 | } 62 | if len(c.Options.Name) == 0 { 63 | c.tag = c.buildNodeTag(node) 64 | } else { 65 | c.tag = c.Options.Name 66 | } 67 | 68 | // add limiter 69 | l := limiter.AddLimiter(c.tag, &c.LimitConfig, c.userList, c.aliveMap) 70 | // add rule limiter 71 | if err = l.UpdateRule(&node.Rules); err != nil { 72 | return fmt.Errorf("update rule error: %s", err) 73 | } 74 | c.limiter = l 75 | if node.Security == panel.Tls { 76 | err = c.requestCert() 77 | if err != nil { 78 | return fmt.Errorf("request cert error: %s", err) 79 | } 80 | } 81 | // Add new tag 82 | err = c.server.AddNode(c.tag, node, c.Options) 83 | if err != nil { 84 | return fmt.Errorf("add new node error: %s", err) 85 | } 86 | added, err := c.server.AddUsers(&vCore.AddUsersParams{ 87 | Tag: c.tag, 88 | Users: c.userList, 89 | NodeInfo: node, 90 | }) 91 | if err != nil { 92 | return fmt.Errorf("add users error: %s", err) 93 | } 94 | log.WithField("tag", c.tag).Infof("Added %d new users", added) 95 | c.info = node 96 | c.startTasks(node) 97 | return nil 98 | } 99 | 100 | // Close implement the Close() function of the service interface 101 | func (c *Controller) Close() error { 102 | limiter.DeleteLimiter(c.tag) 103 | if c.nodeInfoMonitorPeriodic != nil { 104 | c.nodeInfoMonitorPeriodic.Close() 105 | } 106 | if c.userReportPeriodic != nil { 107 | c.userReportPeriodic.Close() 108 | } 109 | if c.renewCertPeriodic != nil { 110 | c.renewCertPeriodic.Close() 111 | } 112 | if c.dynamicSpeedLimitPeriodic != nil { 113 | c.dynamicSpeedLimitPeriodic.Close() 114 | } 115 | if c.onlineIpReportPeriodic != nil { 116 | c.onlineIpReportPeriodic.Close() 117 | } 118 | err := c.server.DelNode(c.tag) 119 | if err != nil { 120 | return fmt.Errorf("del node error: %s", err) 121 | } 122 | return nil 123 | } 124 | 125 | func (c *Controller) buildNodeTag(node *panel.NodeInfo) string { 126 | return fmt.Sprintf("[%s]-%s:%d", c.apiClient.APIHost, node.Type, node.Id) 127 | } 128 | -------------------------------------------------------------------------------- /node/lego.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | import ( 4 | "crypto" 5 | "crypto/ecdsa" 6 | "crypto/elliptic" 7 | "crypto/rand" 8 | "crypto/x509" 9 | "encoding/pem" 10 | "fmt" 11 | "os" 12 | "path" 13 | "strings" 14 | "time" 15 | 16 | "github.com/go-acme/lego/v4/certificate" 17 | "github.com/go-acme/lego/v4/challenge/http01" 18 | "github.com/go-acme/lego/v4/providers/dns" 19 | "github.com/go-acme/lego/v4/registration" 20 | "github.com/goccy/go-json" 21 | 22 | "github.com/InazumaV/V2bX/common/file" 23 | "github.com/InazumaV/V2bX/conf" 24 | "github.com/go-acme/lego/v4/certcrypto" 25 | "github.com/go-acme/lego/v4/lego" 26 | ) 27 | 28 | type Lego struct { 29 | client *lego.Client 30 | config *conf.CertConfig 31 | } 32 | 33 | func NewLego(config *conf.CertConfig) (*Lego, error) { 34 | user, err := NewLegoUser(path.Join(path.Dir(config.CertFile), 35 | "user", 36 | fmt.Sprintf("user-%s.json", config.Email)), 37 | config.Email) 38 | if err != nil { 39 | return nil, fmt.Errorf("create user error: %s", err) 40 | } 41 | c := lego.NewConfig(user) 42 | //c.CADirURL = "http://192.168.99.100:4000/directory" 43 | c.Certificate.KeyType = certcrypto.RSA2048 44 | client, err := lego.NewClient(c) 45 | if err != nil { 46 | return nil, err 47 | } 48 | l := Lego{ 49 | client: client, 50 | config: config, 51 | } 52 | err = l.SetProvider() 53 | if err != nil { 54 | return nil, fmt.Errorf("set provider error: %s", err) 55 | } 56 | return &l, nil 57 | } 58 | 59 | func checkPath(p string) error { 60 | if !file.IsExist(path.Dir(p)) { 61 | err := os.MkdirAll(path.Dir(p), 0755) 62 | if err != nil { 63 | return fmt.Errorf("create dir error: %s", err) 64 | } 65 | } 66 | return nil 67 | } 68 | 69 | func (l *Lego) SetProvider() error { 70 | switch l.config.CertMode { 71 | case "http": 72 | err := l.client.Challenge.SetHTTP01Provider(http01.NewProviderServer("", "80")) 73 | if err != nil { 74 | return err 75 | } 76 | case "dns": 77 | for k, v := range l.config.DNSEnv { 78 | os.Setenv(k, v) 79 | } 80 | p, err := dns.NewDNSChallengeProviderByName(l.config.Provider) 81 | if err != nil { 82 | return fmt.Errorf("create dns challenge provider error: %s", err) 83 | } 84 | err = l.client.Challenge.SetDNS01Provider(p) 85 | if err != nil { 86 | return fmt.Errorf("set dns provider error: %s", err) 87 | } 88 | } 89 | return nil 90 | } 91 | 92 | func (l *Lego) CreateCert() (err error) { 93 | request := certificate.ObtainRequest{ 94 | Domains: []string{l.config.CertDomain}, 95 | Bundle: true, 96 | } 97 | certificates, err := l.client.Certificate.Obtain(request) 98 | if err != nil { 99 | return fmt.Errorf("obtain certificate error: %s", err) 100 | } 101 | err = l.writeCert(certificates) 102 | if err != nil { 103 | return fmt.Errorf("write certificate error: %s", err) 104 | } 105 | return nil 106 | } 107 | 108 | func (l *Lego) RenewCert() error { 109 | file, err := os.ReadFile(l.config.CertFile) 110 | if err != nil { 111 | return fmt.Errorf("read cert file error: %s", err) 112 | } 113 | if e, err := l.CheckCert(file); !e { 114 | return nil 115 | } else if err != nil { 116 | return fmt.Errorf("check cert error: %s", err) 117 | } 118 | res, err := l.client.Certificate.Renew(certificate.Resource{ 119 | Domain: l.config.CertDomain, 120 | Certificate: file, 121 | }, true, false, "") 122 | if err != nil { 123 | return err 124 | } 125 | err = l.writeCert(res) 126 | if err != nil { 127 | return fmt.Errorf("write certificate error: %s", err) 128 | } 129 | return nil 130 | } 131 | 132 | func (l *Lego) CheckCert(file []byte) (bool, error) { 133 | cert, err := certcrypto.ParsePEMCertificate(file) 134 | if err != nil { 135 | return false, err 136 | } 137 | notAfter := int(time.Until(cert.NotAfter).Hours() / 24.0) 138 | if notAfter > 30 { 139 | return false, nil 140 | } 141 | return true, nil 142 | } 143 | func (l *Lego) parseParams(path string) string { 144 | r := strings.NewReplacer("{domain}", l.config.CertDomain, 145 | "{email}", l.config.Email) 146 | return r.Replace(path) 147 | } 148 | func (l *Lego) writeCert(certificates *certificate.Resource) error { 149 | err := checkPath(l.config.CertFile) 150 | if err != nil { 151 | return fmt.Errorf("check path error: %s", err) 152 | } 153 | err = os.WriteFile(l.parseParams(l.config.CertFile), certificates.Certificate, 0644) 154 | if err != nil { 155 | return err 156 | } 157 | err = checkPath(l.config.KeyFile) 158 | if err != nil { 159 | return fmt.Errorf("check path error: %s", err) 160 | } 161 | err = os.WriteFile(l.parseParams(l.config.KeyFile), certificates.PrivateKey, 0644) 162 | if err != nil { 163 | return err 164 | } 165 | return nil 166 | } 167 | 168 | type User struct { 169 | Email string `json:"Email"` 170 | Registration *registration.Resource `json:"Registration"` 171 | key crypto.PrivateKey 172 | KeyEncoded string `json:"Key"` 173 | } 174 | 175 | func (u *User) GetEmail() string { 176 | return u.Email 177 | } 178 | func (u *User) GetRegistration() *registration.Resource { 179 | return u.Registration 180 | } 181 | func (u *User) GetPrivateKey() crypto.PrivateKey { 182 | return u.key 183 | } 184 | 185 | func NewLegoUser(path string, email string) (*User, error) { 186 | var user User 187 | if file.IsExist(path) { 188 | err := user.Load(path) 189 | if err != nil { 190 | return nil, err 191 | } 192 | if user.Email != email { 193 | user.Registration = nil 194 | user.Email = email 195 | err := registerUser(&user, path) 196 | if err != nil { 197 | return nil, err 198 | } 199 | } 200 | } else { 201 | user.Email = email 202 | err := registerUser(&user, path) 203 | if err != nil { 204 | return nil, err 205 | } 206 | } 207 | return &user, nil 208 | } 209 | 210 | func registerUser(user *User, path string) error { 211 | privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 212 | if err != nil { 213 | return fmt.Errorf("generate key error: %s", err) 214 | } 215 | user.key = privateKey 216 | c := lego.NewConfig(user) 217 | client, err := lego.NewClient(c) 218 | if err != nil { 219 | return fmt.Errorf("create lego client error: %s", err) 220 | } 221 | reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) 222 | if err != nil { 223 | return err 224 | } 225 | user.Registration = reg 226 | err = user.Save(path) 227 | if err != nil { 228 | return fmt.Errorf("save user error: %s", err) 229 | } 230 | return nil 231 | } 232 | 233 | func EncodePrivate(privKey *ecdsa.PrivateKey) (string, error) { 234 | encoded, err := x509.MarshalECPrivateKey(privKey) 235 | if err != nil { 236 | return "", err 237 | } 238 | pemEncoded := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: encoded}) 239 | return string(pemEncoded), nil 240 | } 241 | func (u *User) Save(path string) error { 242 | err := checkPath(path) 243 | if err != nil { 244 | return fmt.Errorf("check path error: %s", err) 245 | } 246 | u.KeyEncoded, _ = EncodePrivate(u.key.(*ecdsa.PrivateKey)) 247 | f, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) 248 | if err != nil { 249 | return err 250 | } 251 | err = json.NewEncoder(f).Encode(u) 252 | if err != nil { 253 | return fmt.Errorf("marshal json error: %s", err) 254 | } 255 | u.KeyEncoded = "" 256 | return nil 257 | } 258 | 259 | func (u *User) DecodePrivate(pemEncodedPriv string) (*ecdsa.PrivateKey, error) { 260 | blockPriv, _ := pem.Decode([]byte(pemEncodedPriv)) 261 | x509EncodedPriv := blockPriv.Bytes 262 | privateKey, err := x509.ParseECPrivateKey(x509EncodedPriv) 263 | return privateKey, err 264 | } 265 | 266 | func (u *User) Load(path string) error { 267 | data, err := os.ReadFile(path) 268 | if err != nil { 269 | return fmt.Errorf("open file error: %s", err) 270 | } 271 | 272 | err = json.Unmarshal(data, u) 273 | if err != nil { 274 | return fmt.Errorf("unmarshal json error: %s", err) 275 | } 276 | u.key, err = u.DecodePrivate(u.KeyEncoded) 277 | if err != nil { 278 | return fmt.Errorf("decode private key error: %s", err) 279 | } 280 | return nil 281 | } 282 | -------------------------------------------------------------------------------- /node/lego_test.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "testing" 7 | 8 | "github.com/InazumaV/V2bX/conf" 9 | ) 10 | 11 | var l *Lego 12 | 13 | func init() { 14 | var err error 15 | l, err = NewLego(&conf.CertConfig{ 16 | CertMode: "dns", 17 | Email: "test@test.com", 18 | CertDomain: "test.test.com", 19 | Provider: "cloudflare", 20 | DNSEnv: map[string]string{ 21 | "CF_DNS_API_TOKEN": "123", 22 | }, 23 | CertFile: "./cert/1.pem", 24 | KeyFile: "./cert/1.key", 25 | }) 26 | if err != nil { 27 | log.Println(err) 28 | os.Exit(1) 29 | } 30 | } 31 | 32 | func TestLego_CreateCertByDns(t *testing.T) { 33 | err := l.CreateCert() 34 | if err != nil { 35 | t.Error(err) 36 | } 37 | } 38 | 39 | func TestLego_RenewCert(t *testing.T) { 40 | log.Println(l.RenewCert()) 41 | } 42 | -------------------------------------------------------------------------------- /node/node.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/InazumaV/V2bX/api/panel" 7 | "github.com/InazumaV/V2bX/conf" 8 | vCore "github.com/InazumaV/V2bX/core" 9 | ) 10 | 11 | type Node struct { 12 | controllers []*Controller 13 | } 14 | 15 | func New() *Node { 16 | return &Node{} 17 | } 18 | 19 | func (n *Node) Start(nodes []conf.NodeConfig, core vCore.Core) error { 20 | n.controllers = make([]*Controller, len(nodes)) 21 | for i := range nodes { 22 | p, err := panel.New(&nodes[i].ApiConfig) 23 | if err != nil { 24 | return err 25 | } 26 | // Register controller service 27 | n.controllers[i] = NewController(core, p, &nodes[i].Options) 28 | err = n.controllers[i].Start() 29 | if err != nil { 30 | return fmt.Errorf("start node controller [%s-%s-%d] error: %s", 31 | nodes[i].ApiConfig.APIHost, 32 | nodes[i].ApiConfig.NodeType, 33 | nodes[i].ApiConfig.NodeID, 34 | err) 35 | } 36 | } 37 | return nil 38 | } 39 | 40 | func (n *Node) Close() { 41 | for _, c := range n.controllers { 42 | err := c.Close() 43 | if err != nil { 44 | panic(err) 45 | } 46 | } 47 | n.controllers = nil 48 | } 49 | -------------------------------------------------------------------------------- /node/task.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/InazumaV/V2bX/api/panel" 7 | "github.com/InazumaV/V2bX/common/task" 8 | vCore "github.com/InazumaV/V2bX/core" 9 | "github.com/InazumaV/V2bX/limiter" 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | func (c *Controller) startTasks(node *panel.NodeInfo) { 14 | // fetch node info task 15 | c.nodeInfoMonitorPeriodic = &task.Task{ 16 | Interval: node.PullInterval, 17 | Execute: c.nodeInfoMonitor, 18 | } 19 | // fetch user list task 20 | c.userReportPeriodic = &task.Task{ 21 | Interval: node.PushInterval, 22 | Execute: c.reportUserTrafficTask, 23 | } 24 | log.WithField("tag", c.tag).Info("Start monitor node status") 25 | // delay to start nodeInfoMonitor 26 | _ = c.nodeInfoMonitorPeriodic.Start(false) 27 | log.WithField("tag", c.tag).Info("Start report node status") 28 | _ = c.userReportPeriodic.Start(false) 29 | if node.Security == panel.Tls { 30 | switch c.CertConfig.CertMode { 31 | case "none", "", "file", "self": 32 | default: 33 | c.renewCertPeriodic = &task.Task{ 34 | Interval: time.Hour * 24, 35 | Execute: c.renewCertTask, 36 | } 37 | log.WithField("tag", c.tag).Info("Start renew cert") 38 | // delay to start renewCert 39 | _ = c.renewCertPeriodic.Start(true) 40 | } 41 | } 42 | if c.LimitConfig.EnableDynamicSpeedLimit { 43 | c.traffic = make(map[string]int64) 44 | c.dynamicSpeedLimitPeriodic = &task.Task{ 45 | Interval: time.Duration(c.LimitConfig.DynamicSpeedLimitConfig.Periodic) * time.Second, 46 | Execute: c.SpeedChecker, 47 | } 48 | log.Printf("[%s: %d] Start dynamic speed limit", c.apiClient.NodeType, c.apiClient.NodeId) 49 | } 50 | } 51 | 52 | func (c *Controller) nodeInfoMonitor() (err error) { 53 | // get node info 54 | newN, err := c.apiClient.GetNodeInfo() 55 | if err != nil { 56 | log.WithFields(log.Fields{ 57 | "tag": c.tag, 58 | "err": err, 59 | }).Error("Get node info failed") 60 | return nil 61 | } 62 | // get user info 63 | newU, err := c.apiClient.GetUserList() 64 | if err != nil { 65 | log.WithFields(log.Fields{ 66 | "tag": c.tag, 67 | "err": err, 68 | }).Error("Get user list failed") 69 | return nil 70 | } 71 | // get user alive 72 | newA, err := c.apiClient.GetUserAlive() 73 | if err != nil { 74 | log.WithFields(log.Fields{ 75 | "tag": c.tag, 76 | "err": err, 77 | }).Error("Get alive list failed") 78 | return nil 79 | } 80 | if newN != nil { 81 | c.info = newN 82 | // nodeInfo changed 83 | if newU != nil { 84 | c.userList = newU 85 | } 86 | c.traffic = make(map[string]int64) 87 | // Remove old node 88 | log.WithField("tag", c.tag).Info("Node changed, reload") 89 | err = c.server.DelNode(c.tag) 90 | if err != nil { 91 | log.WithFields(log.Fields{ 92 | "tag": c.tag, 93 | "err": err, 94 | }).Panic("Delete node failed") 95 | return nil 96 | } 97 | 98 | // Update limiter 99 | if len(c.Options.Name) == 0 { 100 | c.tag = c.buildNodeTag(newN) 101 | // Remove Old limiter 102 | limiter.DeleteLimiter(c.tag) 103 | // Add new Limiter 104 | l := limiter.AddLimiter(c.tag, &c.LimitConfig, c.userList, newA) 105 | c.limiter = l 106 | } 107 | // update alive list 108 | if newA != nil { 109 | c.limiter.AliveList = newA 110 | } 111 | // Update rule 112 | err = c.limiter.UpdateRule(&newN.Rules) 113 | if err != nil { 114 | log.WithFields(log.Fields{ 115 | "tag": c.tag, 116 | "err": err, 117 | }).Error("Update Rule failed") 118 | return nil 119 | } 120 | 121 | // check cert 122 | if newN.Security == panel.Tls { 123 | err = c.requestCert() 124 | if err != nil { 125 | log.WithFields(log.Fields{ 126 | "tag": c.tag, 127 | "err": err, 128 | }).Error("Request cert failed") 129 | return nil 130 | } 131 | } 132 | // add new node 133 | err = c.server.AddNode(c.tag, newN, c.Options) 134 | if err != nil { 135 | log.WithFields(log.Fields{ 136 | "tag": c.tag, 137 | "err": err, 138 | }).Panic("Add node failed") 139 | return nil 140 | } 141 | _, err = c.server.AddUsers(&vCore.AddUsersParams{ 142 | Tag: c.tag, 143 | Users: c.userList, 144 | NodeInfo: newN, 145 | }) 146 | if err != nil { 147 | log.WithFields(log.Fields{ 148 | "tag": c.tag, 149 | "err": err, 150 | }).Error("Add users failed") 151 | return nil 152 | } 153 | // Check interval 154 | if c.nodeInfoMonitorPeriodic.Interval != newN.PullInterval && 155 | newN.PullInterval != 0 { 156 | c.nodeInfoMonitorPeriodic.Interval = newN.PullInterval 157 | c.nodeInfoMonitorPeriodic.Close() 158 | _ = c.nodeInfoMonitorPeriodic.Start(false) 159 | } 160 | if c.userReportPeriodic.Interval != newN.PushInterval && 161 | newN.PushInterval != 0 { 162 | c.userReportPeriodic.Interval = newN.PullInterval 163 | c.userReportPeriodic.Close() 164 | _ = c.userReportPeriodic.Start(false) 165 | } 166 | log.WithField("tag", c.tag).Infof("Added %d new users", len(c.userList)) 167 | // exit 168 | return nil 169 | } 170 | // update alive list 171 | if newA != nil { 172 | c.limiter.AliveList = newA 173 | } 174 | // node no changed, check users 175 | if len(newU) == 0 { 176 | return nil 177 | } 178 | deleted, added := compareUserList(c.userList, newU) 179 | if len(deleted) > 0 { 180 | // have deleted users 181 | err = c.server.DelUsers(deleted, c.tag, c.info) 182 | if err != nil { 183 | log.WithFields(log.Fields{ 184 | "tag": c.tag, 185 | "err": err, 186 | }).Error("Delete users failed") 187 | return nil 188 | } 189 | } 190 | if len(added) > 0 { 191 | // have added users 192 | _, err = c.server.AddUsers(&vCore.AddUsersParams{ 193 | Tag: c.tag, 194 | NodeInfo: c.info, 195 | Users: added, 196 | }) 197 | if err != nil { 198 | log.WithFields(log.Fields{ 199 | "tag": c.tag, 200 | "err": err, 201 | }).Error("Add users failed") 202 | return nil 203 | } 204 | } 205 | if len(added) > 0 || len(deleted) > 0 { 206 | // update Limiter 207 | c.limiter.UpdateUser(c.tag, added, deleted) 208 | if err != nil { 209 | log.WithFields(log.Fields{ 210 | "tag": c.tag, 211 | "err": err, 212 | }).Error("limiter users failed") 213 | return nil 214 | } 215 | // clear traffic record 216 | if c.LimitConfig.EnableDynamicSpeedLimit { 217 | for i := range deleted { 218 | delete(c.traffic, deleted[i].Uuid) 219 | } 220 | } 221 | } 222 | c.userList = newU 223 | if len(added)+len(deleted) != 0 { 224 | log.WithField("tag", c.tag). 225 | Infof("%d user deleted, %d user added", len(deleted), len(added)) 226 | } 227 | return nil 228 | } 229 | 230 | func (c *Controller) SpeedChecker() error { 231 | for u, t := range c.traffic { 232 | if t >= c.LimitConfig.DynamicSpeedLimitConfig.Traffic { 233 | err := c.limiter.UpdateDynamicSpeedLimit(c.tag, u, 234 | c.LimitConfig.DynamicSpeedLimitConfig.SpeedLimit, 235 | time.Now().Add(time.Duration(c.LimitConfig.DynamicSpeedLimitConfig.ExpireTime)*time.Minute)) 236 | log.WithField("err", err).Error("Update dynamic speed limit failed") 237 | delete(c.traffic, u) 238 | } 239 | } 240 | return nil 241 | } 242 | -------------------------------------------------------------------------------- /node/user.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/InazumaV/V2bX/api/panel" 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | func (c *Controller) reportUserTrafficTask() (err error) { 11 | // Get User traffic 12 | userTraffic := make([]panel.UserTraffic, 0) 13 | for i := range c.userList { 14 | up, down := c.server.GetUserTraffic(c.tag, c.userList[i].Uuid, true) 15 | if up > 0 || down > 0 { 16 | if c.LimitConfig.EnableDynamicSpeedLimit { 17 | c.traffic[c.userList[i].Uuid] += up + down 18 | } 19 | userTraffic = append(userTraffic, panel.UserTraffic{ 20 | UID: (c.userList)[i].Id, 21 | Upload: up, 22 | Download: down}) 23 | } 24 | } 25 | if len(userTraffic) > 0 { 26 | err = c.apiClient.ReportUserTraffic(userTraffic) 27 | if err != nil { 28 | log.WithFields(log.Fields{ 29 | "tag": c.tag, 30 | "err": err, 31 | }).Info("Report user traffic failed") 32 | } else { 33 | log.WithField("tag", c.tag).Infof("Report %d users traffic", len(userTraffic)) 34 | } 35 | } 36 | 37 | if onlineDevice, err := c.limiter.GetOnlineDevice(); err != nil { 38 | log.Print(err) 39 | } else if len(*onlineDevice) > 0 { 40 | // Only report user has traffic > 100kb to allow ping test 41 | var result []panel.OnlineUser 42 | var nocountUID = make(map[int]struct{}) 43 | for _, traffic := range userTraffic { 44 | total := traffic.Upload + traffic.Download 45 | if total < int64(c.Options.DeviceOnlineMinTraffic*1000) { 46 | nocountUID[traffic.UID] = struct{}{} 47 | } 48 | } 49 | for _, online := range *onlineDevice { 50 | if _, ok := nocountUID[online.UID]; !ok { 51 | result = append(result, online) 52 | } 53 | } 54 | data := make(map[int][]string) 55 | for _, onlineuser := range result { 56 | // json structure: { UID1:["ip1","ip2"],UID2:["ip3","ip4"] } 57 | data[onlineuser.UID] = append(data[onlineuser.UID], onlineuser.IP) 58 | } 59 | if err = c.apiClient.ReportNodeOnlineUsers(&data); err != nil { 60 | log.WithFields(log.Fields{ 61 | "tag": c.tag, 62 | "err": err, 63 | }).Info("Report online users failed") 64 | } else { 65 | log.WithField("tag", c.tag).Infof("Total %d online users, %d Reported", len(*onlineDevice), len(result)) 66 | } 67 | } 68 | 69 | userTraffic = nil 70 | return nil 71 | } 72 | 73 | func compareUserList(old, new []panel.UserInfo) (deleted, added []panel.UserInfo) { 74 | oldMap := make(map[string]int) 75 | for i, user := range old { 76 | key := user.Uuid + strconv.Itoa(user.SpeedLimit) 77 | oldMap[key] = i 78 | } 79 | 80 | for _, user := range new { 81 | key := user.Uuid + strconv.Itoa(user.SpeedLimit) 82 | if _, exists := oldMap[key]; !exists { 83 | added = append(added, user) 84 | } else { 85 | delete(oldMap, key) 86 | } 87 | } 88 | 89 | for _, index := range oldMap { 90 | deleted = append(deleted, old[index]) 91 | } 92 | 93 | return deleted, added 94 | } 95 | -------------------------------------------------------------------------------- /test_data/1.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAyryUpSI01T4jgPywpt4CIaf+MNgPn9DxHtJmuo1VB7Ysk13Z 3 | uOBVfS9HJoikbrcVeqENkR8Bq1vdgMLbHmlxrSTwe/pynmRvWx+L1hKscxEmR6uV 4 | jgqWsPkoUAKVJlHBfjw6oFEQzmgwDOfyS90TVr4gwDo/GCLX1iX5jC+KrteuAMOp 5 | V6Yiu1FAhd0TeUDPMKs4wbyE8qx3+Mn6FqGorIwyefCX9m9QCz1N/5Ut6I5uT5Vr 6 | 28Ox1Q++mZ0s5ZuFpMlzKvrfav7fxt31rgSUro7/NTNuIydHvwMcZYeVm+DT+Vfp 7 | wRZW9uoB4jiwLDxnfMOMLd72yGId+9EJ6HpOIQIDAQABAoIBAHhV/xUVfK6mN4S0 8 | eFZTqIg5otNzK7L83mIhGQDaKwJsy4CdUEJARf4MNftVV+Svn3wuZFMjSGZiHNP0 9 | 1QL0K5lON8AfJDGIA+DelK34X4vdPg+EdTzeZBufiKIVJlqcZHF9Zn8KHyOlDABd 10 | HKCTFIuERwRSjmjRJbPizoC7J2Ina15pi+35T3x3oye4V5kQlJH3WxlMOhp0Z+iO 11 | qGlqJITWeOqxXaLfPIoOYcJ2LBPzvgv4IRVbmhi4mxphfPTEAlGnvITauDa9fx7S 12 | REBAFyt5NyU9M7dusmlLiTIHryOensBzCQOwbMseeUS0Z1rIfbusqwBM3X5oPcx8 13 | jOP1QxECgYEA+qCU4Q84hDBhC2SLfH4D1QyOixeho5re6E0NchSposIhrxLdRDDk 14 | 04GGNTKUflgfoskaI9loe84MkTqqrjxguRWVXf5qPMAtfpXr2GYiPq+VhiEeo7ZG 15 | f1rHYqK7Ity9jzy7z/CpTLekveUT93I7/yF5iEOUYQgnIZhDg+Hr0ysCgYEAzxUu 16 | DmL7GceRauoD9w3tT2nsa23hXc+jo1QWV5I7veZyosXnz8gxpax4SgwZ/iCH8pDJ 17 | RagWTHQHO3X6DdkovzPEWYiLtLCE2APlfClzkZif5tv7kO4B3BuyuDlNleuCH894 18 | 5WlieyCHcPtXqG/60tXxVrlNrevI+ccsYRb+reMCgYBgu8AaybQnmUCrlAgeacjy 19 | 3yDZYKqbqfflM3BAGueKkWFM4HwUiMaZOAHj4Hzd8wdq3jG/qncgadwB5eHg1B8E 20 | 8Oaw27SHdClbFWRtJqaLCVwt4/SefYjiONiCIosWHprvgSKAVMQTf0IPpS46sJWl 21 | mHb++A56ERqBZfKRIY7S9wKBgCKf+flx12ZyFgB4bH1MmNdkcKFt1/bllwjiMHIo 22 | A1E3TQema6I0aQi4k8xdxaLWMaT/TIgXGNNjuynYCh1yp/uAXl5SFHn74dp0nFRs 23 | YeSATow9UAzlnu38u59OBYkBvdovyJkjS9ImmD7t57RENP43w4iqpzBjclFBWkxJ 24 | mf/dAoGBAOZa8+FmFyx2VmgwR4qPrNKg6EuhO6OgDJ8NvRnx4Mu5/0L1JoYoM0Yc 25 | 2NMKh14fqmDB/CvCbMGxCj86WF8534t+eBqhY6/AUkSolh8139KNmYHNB0ynqDYn 26 | QDm/HJ4cux2rkLUCKlbGBnS9M+PSJA/MJeh0JTIrSOu4bH7aanVl 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test_data/1.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIID2zCCAsOgAwIBAgIRAJP0pRlp9k2eiBLK2a2B7LYwDQYJKoZIhvcNAQELBQAw 3 | XjELMAkGA1UEBhMCQ04xDjAMBgNVBAoTBU15U1NMMSswKQYDVQQLEyJNeVNTTCBU 4 | ZXN0IFJTQSAtIEZvciB0ZXN0IHVzZSBvbmx5MRIwEAYDVQQDEwlNeVNTTC5jb20w 5 | HhcNMjMwNjA4MTQxOTQzWhcNMjgwNjA2MTQxOTQzWjAiMQswCQYDVQQGEwJDTjET 6 | MBEGA1UEAxMKMTE0NTE0LmdheTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC 7 | ggEBAMq8lKUiNNU+I4D8sKbeAiGn/jDYD5/Q8R7SZrqNVQe2LJNd2bjgVX0vRyaI 8 | pG63FXqhDZEfAatb3YDC2x5pca0k8Hv6cp5kb1sfi9YSrHMRJkerlY4KlrD5KFAC 9 | lSZRwX48OqBREM5oMAzn8kvdE1a+IMA6Pxgi19Yl+Ywviq7XrgDDqVemIrtRQIXd 10 | E3lAzzCrOMG8hPKsd/jJ+hahqKyMMnnwl/ZvUAs9Tf+VLeiObk+Va9vDsdUPvpmd 11 | LOWbhaTJcyr632r+38bd9a4ElK6O/zUzbiMnR78DHGWHlZvg0/lX6cEWVvbqAeI4 12 | sCw8Z3zDjC3e9shiHfvRCeh6TiECAwEAAaOBzzCBzDAOBgNVHQ8BAf8EBAMCBaAw 13 | HQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB8GA1UdIwQYMBaAFCiBJgXR 14 | NBo/wXMPu5PPFRw/A79/MGMGCCsGAQUFBwEBBFcwVTAhBggrBgEFBQcwAYYVaHR0 15 | cDovL29jc3AubXlzc2wuY29tMDAGCCsGAQUFBzAChiRodHRwOi8vY2EubXlzc2wu 16 | Y29tL215c3NsdGVzdHJzYS5jcnQwFQYDVR0RBA4wDIIKMTE0NTE0LmdheTANBgkq 17 | hkiG9w0BAQsFAAOCAQEAdlvVKnB1OHojoHfgPKUVTk5+OXk9X/q++wkkrGa1rQs0 18 | bPikKdc1TQoW/ylpX9wN3rwLLYGf/Hs9SqHr4RkAhAb6v+K1O5HUMqJARONdB7j0 19 | /1BuKd0wx5pIqJhs6qRf+grsOGj9EdTfKXqElCljAES0t+2ZbZ2666XftwSjybtF 20 | yKg+9iS9PX5VA1SIsa7XSVTlJ8oXy91KuCm07UxnSEKovpzV4TIlxPgKOfWBUYh9 21 | JfOhwh1rpUy6tqNjFyJdGHxBJsf7HLcO9VJe/RD55c54ovZaTT24Cy/II5DKiQ26 22 | TUeMj1BMedu5Ou0YCH7W9QhH40fvwi/hSQrjysQAoA== 23 | -----END CERTIFICATE----- 24 | --------------------------------------------------------------------------------