├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.md │ └── feature-request.md ├── build │ └── friendly-filenames.json ├── dependabot.yml └── workflows │ ├── codeql-analysis.yml │ └── release.yml ├── .gitignore ├── LICENSE ├── README.md ├── api ├── iprecoder │ ├── interface.go │ ├── recorder.go │ ├── redis.go │ └── redis_test.go └── 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 ├── limit.go ├── log.go ├── node.go ├── sing.go ├── watch.go └── xray.go ├── core ├── core.go ├── imports │ ├── imports.go │ ├── sing.go │ └── xray.go ├── interface.go ├── selector.go ├── sing │ ├── box_outbound.go │ ├── debug_go118.go │ ├── debug_go119.go │ ├── debug_http.go │ ├── debug_linux.go │ ├── debug_stub.go │ ├── dns.go │ ├── hook.go │ ├── node.go │ ├── sing.go │ ├── user.go │ └── utils.go └── xray │ ├── app │ ├── app.go │ └── dispatcher │ │ ├── config.pb.go │ │ ├── config.proto │ │ ├── default.go │ │ ├── dispatcher.go │ │ ├── errors.generated.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 ├── geosite.dat └── route.json ├── go.mod ├── go.sum ├── limiter ├── clear.go ├── conn.go ├── conn_test.go ├── 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: 创建一个报告以帮助我们修复并改进XrayR 4 | title: '' 5 | labels: awaiting reply, bug 6 | assignees: '' 7 | --- 8 | 9 | **描述该错误** 10 | 简单地描述一下这个bug是什么 11 | 12 | **复现** 13 | 复现该bug的步骤 14 | 15 | **环境和版本** 16 | - 系统 [例如:Debian 11] 17 | - 架构 [例如:AMD64] 18 | - 面板 [例如:V2board] 19 | - 协议 [例如:vmess] 20 | - 版本 [例如:0.8.2.2] 21 | - 部署方式 [例如:一键脚本] 22 | 23 | **日志和错误** 24 | 请使用`xrayr log`查看并添加日志,以帮助解释你的问题 25 | 26 | **额外的内容** 27 | 在这里添加关于问题的任何其他内容 -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "功能建议" 3 | about: 给XrayR提出建议,让我们做得更好 4 | title: '' 5 | labels: awaiting reply, feature-request 6 | assignees: '' 7 | --- 8 | 9 | **描述您想要的功能** 10 | 11 | 清晰简洁的功能描述。 12 | 13 | **描述您考虑过的替代方案** 14 | 15 | 是否有任何替代方案可以解决这个问题? 16 | 17 | **附加上下文** 18 | 19 | 在此处添加有关功能请求的任何其他上下文或截图。 -------------------------------------------------------------------------------- /.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/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@v2 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v1 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 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v1 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v1 68 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Build and Release 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - master 8 | paths: 9 | - "**/*.go" 10 | - "go.mod" 11 | - "go.sum" 12 | - ".github/workflows/*.yml" 13 | pull_request: 14 | types: [opened, synchronize, reopened] 15 | paths: 16 | - "**/*.go" 17 | - "go.mod" 18 | - "go.sum" 19 | - ".github/workflows/*.yml" 20 | release: 21 | types: [published] 22 | 23 | jobs: 24 | 25 | build: 26 | strategy: 27 | matrix: 28 | # Include amd64 on all platforms. 29 | goos: [windows, freebsd, linux, dragonfly, darwin] 30 | goarch: [amd64, 386] 31 | exclude: 32 | # Exclude i386 on darwin and dragonfly. 33 | - goarch: 386 34 | goos: dragonfly 35 | - goarch: 386 36 | goos: darwin 37 | include: 38 | # BEIGIN MacOS ARM64 39 | - goos: darwin 40 | goarch: arm64 41 | # END MacOS ARM64 42 | # BEGIN Linux ARM 5 6 7 43 | - goos: linux 44 | goarch: arm 45 | goarm: 7 46 | - goos: linux 47 | goarch: arm 48 | goarm: 6 49 | - goos: linux 50 | goarch: arm 51 | goarm: 5 52 | # END Linux ARM 5 6 7 53 | # BEGIN Android ARM 8 54 | - goos: android 55 | goarch: arm64 56 | # END Android ARM 8 57 | # BEGIN Other architectures 58 | # BEGIN riscv64 & ARM64 59 | - goos: linux 60 | goarch: arm64 61 | - goos: linux 62 | goarch: riscv64 63 | # END riscv64 & ARM64 64 | # BEGIN MIPS 65 | - goos: linux 66 | goarch: mips64 67 | - goos: linux 68 | goarch: mips64le 69 | - goos: linux 70 | goarch: mipsle 71 | - goos: linux 72 | goarch: mips 73 | # END MIPS 74 | # BEGIN PPC 75 | - goos: linux 76 | goarch: ppc64 77 | - goos: linux 78 | goarch: ppc64le 79 | # END PPC 80 | # BEGIN FreeBSD ARM 81 | - goos: freebsd 82 | goarch: arm64 83 | - goos: freebsd 84 | goarch: arm 85 | goarm: 7 86 | # END FreeBSD ARM 87 | # BEGIN S390X 88 | - goos: linux 89 | goarch: s390x 90 | # END S390X 91 | # END Other architectures 92 | fail-fast: false 93 | 94 | runs-on: ubuntu-latest 95 | env: 96 | GOOS: ${{ matrix.goos }} 97 | GOARCH: ${{ matrix.goarch }} 98 | GOARM: ${{ matrix.goarm }} 99 | CGO_ENABLED: 0 100 | steps: 101 | - name: Checkout codebase 102 | uses: actions/checkout@v3 103 | - name: Show workflow information 104 | id: get_filename 105 | run: | 106 | export _NAME=$(jq ".[\"$GOOS-$GOARCH$GOARM$GOMIPS\"].friendlyName" -r < .github/build/friendly-filenames.json) 107 | echo "GOOS: $GOOS, GOARCH: $GOARCH, GOARM: $GOARM, GOMIPS: $GOMIPS, RELEASE_NAME: $_NAME" 108 | echo "::set-output name=ASSET_NAME::$_NAME" 109 | echo "ASSET_NAME=$_NAME" >> $GITHUB_ENV 110 | - name: Set up Go 111 | uses: actions/setup-go@v3 112 | with: 113 | go-version: '1.20' 114 | 115 | - name: Get project dependencies 116 | run: 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 with_reality_server with_quic" -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 with_reality_server with_quic" -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 geoip geoip' 'domain-list-community dlc geosite') 147 | for i in "${LIST[@]}" 148 | do 149 | INFO=($(echo $i | awk 'BEGIN{FS=" ";OFS=" "} {print $1,$2,$3}')) 150 | FILE_NAME="${INFO[2]}.dat" 151 | echo -e "Downloading ${FILE_NAME}..." 152 | curl -L "https://github.com/v2fly/${INFO[0]}/releases/latest/download/${INFO[1]}.dat" -o ./build_assets/${FILE_NAME} 153 | echo -e "Verifying HASH key..." 154 | HASH="$(curl -sL "https://github.com/v2fly/${INFO[0]}/releases/latest/download/${INFO[1]}.dat.sha256sum" | awk -F ' ' '{print $1}')" 155 | [ "$(sha256sum "./build_assets/${FILE_NAME}" | awk -F ' ' '{print $1}')" == "${HASH}" ] || { echo -e "The HASH key of ${FILE_NAME} does not match cloud one."; exit 1; } 156 | done 157 | - name: Create ZIP archive 158 | shell: bash 159 | run: | 160 | pushd build_assets || exit 1 161 | touch -mt $(date +%Y01010000) * 162 | zip -9vr ../V2bX-$ASSET_NAME.zip . 163 | popd || exit 1 164 | FILE=./V2bX-$ASSET_NAME.zip 165 | DGST=$FILE.dgst 166 | for METHOD in {"md5","sha1","sha256","sha512"} 167 | do 168 | openssl dgst -$METHOD $FILE | sed 's/([^)]*)//g' >>$DGST 169 | done 170 | - name: Change the name 171 | run: | 172 | mv build_assets V2bX-$ASSET_NAME 173 | - name: Upload files to Artifacts 174 | uses: actions/upload-artifact@v3 175 | with: 176 | name: V2bX-${{ steps.get_filename.outputs.ASSET_NAME }} 177 | path: | 178 | ./V2bX-${{ steps.get_filename.outputs.ASSET_NAME }}/* 179 | - name: Upload binaries to release 180 | uses: svenstaro/upload-release-action@v2 181 | if: github.event_name == 'release' 182 | with: 183 | repo_token: ${{ secrets.GITHUB_TOKEN }} 184 | file: ./V2bX-${{ steps.get_filename.outputs.ASSET_NAME }}.zip* 185 | tag: ${{ github.ref }} 186 | file_glob: true 187 | -------------------------------------------------------------------------------- /.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 | .idea/* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # V2bX 2 | 3 | [![](https://img.shields.io/badge/TgChat-%E4%BA%A4%E6%B5%81%E7%BE%A4-blue)](https://t.me/YuzukiProjects) 4 | 5 | A V2board node server based on multi core, modified from XrayR. 6 | 一个基于多种内核的V2board节点服务端,修改自XrayR,支持V2ay,Trojan,Shadowsocks协议。 7 | 8 | **注意: 本项目需要V2board版本 >= 1.7.0** 9 | 10 | **由于本人身体欠佳,导致此项目搁置已久,且由于多内核的原因导致项目臃肿以及各种依赖问题层出不穷,并且已有仍在持续维护的分支项目存在,因此本项目将不再进行维护。** 11 | 作为V2bX的继任者的[Ratte](https://github.com/InazumaV/Ratte)仍在构思及逐步实现中,该项目尝试通过基于RPC的插件化去解决项目臃肿以及依赖问题,但由于本人身体原因,进展缓慢。 12 | 13 | ## 特点 14 | 15 | * 永久开源且免费。 16 | * 支持Vmess/Vless, Trojan, Shadowsocks, Hysteria多种协议。 17 | * 支持Vless和XTLS等新特性。 18 | * 支持单实例对接多节点,无需重复启动。 19 | * 支持限制在线IP。 20 | * 支持限制Tcp连接数。 21 | * 支持节点端口级别、用户级别限速。 22 | * 配置简单明了。 23 | * 修改配置自动重启实例。 24 | * 支持多种内核,易扩展。 25 | * 支持条件编译,可仅编译需要的内核。 26 | 27 | ## 功能介绍 28 | 29 | | 功能 | v2ray | trojan | shadowsocks | hysteria | 30 | |-----------|-------|--------|-------------|----------| 31 | | 自动申请tls证书 | √ | √ | √ | √ | 32 | | 自动续签tls证书 | √ | √ | √ | √ | 33 | | 在线人数统计 | √ | √ | √ | √ | 34 | | 审计规则 | √ | √ | √ | √ | 35 | | 自定义DNS | √ | √ | √ | √ | 36 | | 在线IP数限制 | √ | √ | √ | √ | 37 | | 连接数限制 | √ | √ | √ | √ | 38 | | 跨节点IP数限制 | | | | | 39 | | 按照用户限速 | √ | √ | √ | √ | 40 | | 动态限速(未测试) | √ | √ | √ | √ | 41 | 42 | ## TODO 43 | 44 | - [ ] 重新实现动态限速 45 | - [ ] 重新实现在线IP同步(跨节点在线IP限制) 46 | - [ ] 完善使用文档 47 | 48 | ## 软件安装 49 | 50 | ### 一键安装 51 | 52 | ``` 53 | wget -N https://raw.githubusercontents.com/InazumaV/V2bX-script/master/install.sh && bash install.sh 54 | ``` 55 | 56 | ### 手动安装 57 | 58 | [手动安装教程(过时待更新)](https://yuzuki-1.gitbook.io/v2bx-doc/xrayr-xia-zai-he-an-zhuang/install/manual) 59 | 60 | ## 构建 61 | ``` bash 62 | # 通过-tags选项指定要编译的内核, 可选 xray, sing 63 | go build -o V2bX -ldflags '-s -w' -gcflags="all=-trimpath=${PWD}" -asmflags="all=-trimpath=${PWD} -tags "xray sing" 64 | ``` 65 | 66 | ## 配置文件及详细使用教程 67 | 68 | [详细使用教程](https://yuzuki-1.gitbook.io/v2bx-doc/) 69 | 70 | ## 免责声明 71 | 72 | * 此项目用于本人自用,因此本人不能保证向后兼容性。 73 | * 由于本人能力有限,不能保证所有功能的可用性,如果出现问题请在Issues反馈。 74 | * 本人不对任何人使用本项目造成的任何后果承担责任。 75 | * 本人比较多变,因此本项目可能会随想法或思路的变动随性更改项目结构或大规模重构代码,若不能接受请勿使用。 76 | 77 | ## Thanks 78 | 79 | * [Project X](https://github.com/XTLS/) 80 | * [V2Fly](https://github.com/v2fly) 81 | * [VNet-V2ray](https://github.com/ProxyPanel/VNet-V2ray) 82 | * [Air-Universe](https://github.com/crossfw/Air-Universe) 83 | * [XrayR](https://github.com/XrayR/XrayR) 84 | * [sing-box](https://github.com/SagerNet/sing-box) 85 | 86 | ## Stars 增长记录 87 | 88 | [![Stargazers over time](https://starchart.cc/InazumaV/V2bX.svg)](https://starchart.cc/InazumaV/V2bX) 89 | -------------------------------------------------------------------------------- /api/iprecoder/interface.go: -------------------------------------------------------------------------------- 1 | package iprecoder 2 | 3 | import ( 4 | "github.com/InazumaV/V2bX/limiter" 5 | ) 6 | 7 | type IpRecorder interface { 8 | SyncOnlineIp(Ips []limiter.UserIpList) ([]limiter.UserIpList, error) 9 | } 10 | -------------------------------------------------------------------------------- /api/iprecoder/recorder.go: -------------------------------------------------------------------------------- 1 | package iprecoder 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | "github.com/InazumaV/V2bX/conf" 8 | "github.com/InazumaV/V2bX/limiter" 9 | "github.com/go-resty/resty/v2" 10 | "github.com/goccy/go-json" 11 | ) 12 | 13 | type Recorder struct { 14 | client *resty.Client 15 | *conf.RecorderConfig 16 | } 17 | 18 | func NewRecorder(c *conf.RecorderConfig) *Recorder { 19 | return &Recorder{ 20 | client: resty.New().SetTimeout(time.Duration(c.Timeout) * time.Second), 21 | RecorderConfig: c, 22 | } 23 | } 24 | 25 | func (r *Recorder) SyncOnlineIp(ips []limiter.UserIpList) ([]limiter.UserIpList, error) { 26 | rsp, err := r.client.R(). 27 | SetBody(ips). 28 | Post(r.Url + "/api/v1/SyncOnlineIp?token=" + r.Token) 29 | if err != nil { 30 | return nil, err 31 | } 32 | if rsp.StatusCode() != 200 { 33 | return nil, errors.New(rsp.String()) 34 | } 35 | ips = []limiter.UserIpList{} 36 | err = json.Unmarshal(rsp.Body(), &ips) 37 | if err != nil { 38 | return nil, err 39 | } 40 | return ips, nil 41 | } 42 | -------------------------------------------------------------------------------- /api/iprecoder/redis.go: -------------------------------------------------------------------------------- 1 | package iprecoder 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strconv" 7 | "time" 8 | 9 | "github.com/InazumaV/V2bX/conf" 10 | "github.com/InazumaV/V2bX/limiter" 11 | "github.com/go-redis/redis/v8" 12 | ) 13 | 14 | type Redis struct { 15 | *conf.RedisConfig 16 | client *redis.Client 17 | } 18 | 19 | func NewRedis(c *conf.RedisConfig) *Redis { 20 | return &Redis{ 21 | RedisConfig: c, 22 | client: redis.NewClient(&redis.Options{ 23 | Addr: c.Address, 24 | Password: c.Password, 25 | DB: c.Db, 26 | }), 27 | } 28 | } 29 | 30 | func (r *Redis) SyncOnlineIp(Ips []limiter.UserIpList) ([]limiter.UserIpList, error) { 31 | ctx := context.Background() 32 | for i := range Ips { 33 | err := r.client.SAdd(ctx, "UserList", Ips[i].Uid).Err() 34 | if err != nil { 35 | return nil, fmt.Errorf("add user failed: %s", err) 36 | } 37 | r.client.Expire(ctx, "UserList", time.Second*time.Duration(r.Expiry)) 38 | for _, ip := range Ips[i].IpList { 39 | err := r.client.SAdd(ctx, strconv.Itoa(Ips[i].Uid), ip).Err() 40 | if err != nil { 41 | return nil, fmt.Errorf("add ip failed: %s", err) 42 | } 43 | r.client.Expire(ctx, strconv.Itoa(Ips[i].Uid), time.Second*time.Duration(r.Expiry)) 44 | } 45 | } 46 | c := r.client.SMembers(ctx, "UserList") 47 | if c.Err() != nil { 48 | return nil, fmt.Errorf("get user list failed: %s", c.Err()) 49 | } 50 | Ips = make([]limiter.UserIpList, 0, len(c.Val())) 51 | for _, uid := range c.Val() { 52 | uidInt, err := strconv.Atoi(uid) 53 | if err != nil { 54 | return nil, fmt.Errorf("convert uid failed: %s", err) 55 | } 56 | ips := r.client.SMembers(ctx, uid) 57 | if ips.Err() != nil { 58 | return nil, fmt.Errorf("get ip list failed: %s", ips.Err()) 59 | } 60 | Ips = append(Ips, limiter.UserIpList{ 61 | Uid: uidInt, 62 | IpList: ips.Val(), 63 | }) 64 | } 65 | return Ips, nil 66 | } 67 | -------------------------------------------------------------------------------- /api/iprecoder/redis_test.go: -------------------------------------------------------------------------------- 1 | package iprecoder 2 | 3 | import ( 4 | "log" 5 | "testing" 6 | 7 | "github.com/InazumaV/V2bX/conf" 8 | "github.com/InazumaV/V2bX/limiter" 9 | ) 10 | 11 | func TestRedis_SyncOnlineIp(t *testing.T) { 12 | r := NewRedis(&conf.RedisConfig{ 13 | Address: "127.0.0.1:6379", 14 | Password: "", 15 | Db: 0, 16 | }) 17 | users, err := r.SyncOnlineIp([]limiter.UserIpList{ 18 | {1, []string{"5.5.5.5", "4.4.4.4"}}, 19 | }) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | log.Println(users) 24 | } 25 | -------------------------------------------------------------------------------- /api/panel/node.go: -------------------------------------------------------------------------------- 1 | package panel 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "reflect" 7 | "strconv" 8 | "strings" 9 | "time" 10 | 11 | "github.com/InazumaV/V2bX/common/crypt" 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 | Hysteria *HysteriaNode 36 | Common *CommonNode 37 | } 38 | 39 | type CommonNode struct { 40 | Host string `json:"host"` 41 | ServerPort int `json:"server_port"` 42 | ServerName string `json:"server_name"` 43 | Routes []Route `json:"routes"` 44 | BaseConfig *BaseConfig `json:"base_config"` 45 | } 46 | 47 | type Route struct { 48 | Id int `json:"id"` 49 | Match interface{} `json:"match"` 50 | Action string `json:"action"` 51 | ActionValue string `json:"action_value"` 52 | } 53 | type BaseConfig struct { 54 | PushInterval any `json:"push_interval"` 55 | PullInterval any `json:"pull_interval"` 56 | } 57 | 58 | // VAllssNode is vmess and vless node info 59 | type VAllssNode struct { 60 | CommonNode 61 | Tls int `json:"tls"` 62 | TlsSettings TlsSettings `json:"tls_settings"` 63 | TlsSettingsBack *TlsSettings `json:"tlsSettings"` 64 | Network string `json:"network"` 65 | NetworkSettings json.RawMessage `json:"network_settings"` 66 | NetworkSettingsBack json.RawMessage `json:"networkSettings"` 67 | ServerName string `json:"server_name"` 68 | 69 | // vless only 70 | Flow string `json:"flow"` 71 | RealityConfig RealityConfig `json:"-"` 72 | } 73 | 74 | type TlsSettings struct { 75 | ServerName string `json:"server_name"` 76 | ServerPort string `json:"server_port"` 77 | ShortId string `json:"short_id"` 78 | PrivateKey string `json:"private_key"` 79 | } 80 | 81 | type RealityConfig struct { 82 | Xver uint64 `json:"Xver"` 83 | MinClientVer string `json:"MinClientVer"` 84 | MaxClientVer string `json:"MaxClientVer"` 85 | MaxTimeDiff string `json:"MaxTimeDiff"` 86 | } 87 | 88 | type ShadowsocksNode struct { 89 | CommonNode 90 | Cipher string `json:"cipher"` 91 | ServerKey string `json:"server_key"` 92 | } 93 | 94 | type TrojanNode CommonNode 95 | 96 | type HysteriaNode struct { 97 | CommonNode 98 | UpMbps int `json:"up_mbps"` 99 | DownMbps int `json:"down_mbps"` 100 | Obfs string `json:"obfs"` 101 | } 102 | 103 | type RawDNS struct { 104 | DNSMap map[string]map[string]interface{} 105 | DNSJson []byte 106 | } 107 | 108 | type Rules struct { 109 | Regexp []string 110 | Protocol []string 111 | } 112 | 113 | func (c *Client) GetNodeInfo() (node *NodeInfo, err error) { 114 | const path = "/api/v1/server/UniProxy/config" 115 | r, err := c.client. 116 | R(). 117 | SetHeader("If-None-Match", c.nodeEtag). 118 | Get(path) 119 | if err = c.checkResponse(r, path, err); err != nil { 120 | return 121 | } 122 | if r.StatusCode() == 304 { 123 | return nil, nil 124 | } 125 | node = &NodeInfo{ 126 | Id: c.NodeId, 127 | Type: c.NodeType, 128 | RawDNS: RawDNS{ 129 | DNSMap: make(map[string]map[string]interface{}), 130 | DNSJson: []byte(""), 131 | }, 132 | } 133 | // parse protocol params 134 | var cm *CommonNode 135 | switch c.NodeType { 136 | case "vmess", "vless": 137 | rsp := &VAllssNode{} 138 | err = json.Unmarshal(r.Body(), rsp) 139 | if err != nil { 140 | return nil, fmt.Errorf("decode v2ray params error: %s", err) 141 | } 142 | if len(rsp.NetworkSettingsBack) > 0 { 143 | rsp.NetworkSettings = rsp.NetworkSettingsBack 144 | rsp.NetworkSettingsBack = nil 145 | } 146 | if rsp.TlsSettingsBack != nil { 147 | rsp.TlsSettings = *rsp.TlsSettingsBack 148 | rsp.TlsSettingsBack = nil 149 | } 150 | cm = &rsp.CommonNode 151 | node.VAllss = rsp 152 | node.Security = node.VAllss.Tls 153 | if len(rsp.NetworkSettings) > 0 { 154 | err = json.Unmarshal(rsp.NetworkSettings, &rsp.RealityConfig) 155 | if err != nil { 156 | return nil, fmt.Errorf("decode reality config error: %s", err) 157 | } 158 | } 159 | if node.Security == Reality { 160 | if rsp.TlsSettings.PrivateKey == "" { 161 | key := crypt.GenX25519Private([]byte("vless" + c.Token)) 162 | rsp.TlsSettings.PrivateKey = base64.RawURLEncoding.EncodeToString(key) 163 | } 164 | } 165 | case "shadowsocks": 166 | rsp := &ShadowsocksNode{} 167 | err = json.Unmarshal(r.Body(), rsp) 168 | if err != nil { 169 | return nil, fmt.Errorf("decode v2ray params error: %s", err) 170 | } 171 | cm = &rsp.CommonNode 172 | node.Shadowsocks = rsp 173 | node.Security = None 174 | case "trojan": 175 | rsp := &TrojanNode{} 176 | err = json.Unmarshal(r.Body(), rsp) 177 | if err != nil { 178 | return nil, fmt.Errorf("decode v2ray params error: %s", err) 179 | } 180 | cm = (*CommonNode)(rsp) 181 | node.Trojan = rsp 182 | node.Security = Tls 183 | case "hysteria": 184 | rsp := &HysteriaNode{} 185 | err = json.Unmarshal(r.Body(), rsp) 186 | if err != nil { 187 | return nil, fmt.Errorf("decode v2ray params error: %s", err) 188 | } 189 | cm = &rsp.CommonNode 190 | node.Hysteria = rsp 191 | node.Security = Tls 192 | } 193 | 194 | // parse rules and dns 195 | for i := range cm.Routes { 196 | var matchs []string 197 | if _, ok := cm.Routes[i].Match.(string); ok { 198 | matchs = strings.Split(cm.Routes[i].Match.(string), ",") 199 | } else if _, ok = cm.Routes[i].Match.([]string); ok { 200 | matchs = cm.Routes[i].Match.([]string) 201 | } else { 202 | temp := cm.Routes[i].Match.([]interface{}) 203 | matchs = make([]string, len(temp)) 204 | for i := range temp { 205 | matchs[i] = temp[i].(string) 206 | } 207 | } 208 | switch cm.Routes[i].Action { 209 | case "block": 210 | for _, v := range matchs { 211 | if strings.HasPrefix(v, "protocol:") { 212 | // protocol 213 | node.Rules.Protocol = append(node.Rules.Protocol, strings.TrimPrefix(v, "protocol:")) 214 | } else { 215 | // domain 216 | node.Rules.Regexp = append(node.Rules.Regexp, strings.TrimPrefix(v, "regexp:")) 217 | } 218 | } 219 | case "dns": 220 | var domains []string 221 | for _, v := range matchs { 222 | domains = append(domains, v) 223 | } 224 | if matchs[0] != "main" { 225 | node.RawDNS.DNSMap[strconv.Itoa(i)] = map[string]interface{}{ 226 | "address": cm.Routes[i].ActionValue, 227 | "domains": domains, 228 | } 229 | } else { 230 | dns := []byte(strings.Join(matchs[1:], "")) 231 | node.RawDNS.DNSJson = dns 232 | break 233 | } 234 | } 235 | } 236 | 237 | // set interval 238 | node.PushInterval = intervalToTime(cm.BaseConfig.PushInterval) 239 | node.PullInterval = intervalToTime(cm.BaseConfig.PullInterval) 240 | 241 | node.Common = cm 242 | // clear 243 | cm.Routes = nil 244 | cm.BaseConfig = nil 245 | 246 | c.nodeEtag = r.Header().Get("ETag") 247 | return 248 | } 249 | 250 | func intervalToTime(i interface{}) time.Duration { 251 | switch reflect.TypeOf(i).Kind() { 252 | case reflect.Int: 253 | return time.Duration(i.(int)) * time.Second 254 | case reflect.String: 255 | i, _ := strconv.Atoi(i.(string)) 256 | return time.Duration(i) * time.Second 257 | case reflect.Float64: 258 | return time.Duration(i.(float64)) * time.Second 259 | default: 260 | return time.Duration(reflect.ValueOf(i).Int()) * time.Second 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /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 | } 27 | 28 | func New(c *conf.ApiConfig) (*Client, error) { 29 | client := resty.New() 30 | client.SetRetryCount(3) 31 | if c.Timeout > 0 { 32 | client.SetTimeout(time.Duration(c.Timeout) * time.Second) 33 | } else { 34 | client.SetTimeout(5 * time.Second) 35 | } 36 | client.OnError(func(req *resty.Request, err error) { 37 | var v *resty.ResponseError 38 | if errors.As(err, &v) { 39 | // v.Response contains the last response from the server 40 | // v.Err contains the original error 41 | logrus.Error(v.Err) 42 | } 43 | }) 44 | client.SetBaseURL(c.APIHost) 45 | // Check node type 46 | c.NodeType = strings.ToLower(c.NodeType) 47 | switch c.NodeType { 48 | case "v2ray": 49 | c.NodeType = "vmess" 50 | case 51 | "vmess", 52 | "trojan", 53 | "shadowsocks", 54 | "hysteria", 55 | "vless": 56 | default: 57 | return nil, fmt.Errorf("unsupported Node type: %s", c.NodeType) 58 | } 59 | // set params 60 | client.SetQueryParams(map[string]string{ 61 | "node_type": c.NodeType, 62 | "node_id": strconv.Itoa(c.NodeID), 63 | "token": c.Key, 64 | }) 65 | return &Client{ 66 | client: client, 67 | Token: c.Key, 68 | APIHost: c.APIHost, 69 | NodeType: c.NodeType, 70 | NodeId: c.NodeID, 71 | }, nil 72 | } 73 | -------------------------------------------------------------------------------- /api/panel/user.go: -------------------------------------------------------------------------------- 1 | package panel 2 | 3 | import ( 4 | "fmt" 5 | log "github.com/sirupsen/logrus" 6 | 7 | "github.com/goccy/go-json" 8 | ) 9 | 10 | type OnlineUser struct { 11 | UID int 12 | IP string 13 | } 14 | 15 | type UserInfo struct { 16 | Id int `json:"id"` 17 | Uuid string `json:"uuid"` 18 | SpeedLimit int `json:"speed_limit"` 19 | } 20 | 21 | type UserListBody struct { 22 | //Msg string `json:"msg"` 23 | Users []UserInfo `json:"users"` 24 | } 25 | 26 | // GetUserList will pull user form sspanel 27 | func (c *Client) GetUserList() (UserList []UserInfo, err error) { 28 | const path = "/api/v1/server/UniProxy/user" 29 | r, err := c.client.R(). 30 | SetHeader("If-None-Match", c.userEtag). 31 | Get(path) 32 | err = c.checkResponse(r, path, err) 33 | if err != nil { 34 | return nil, err 35 | } 36 | err = c.checkResponse(r, path, err) 37 | if r.StatusCode() == 304 { 38 | return nil, nil 39 | } 40 | var userList *UserListBody 41 | err = json.Unmarshal(r.Body(), &userList) 42 | if err != nil { 43 | return nil, fmt.Errorf("unmarshal userlist error: %s", err) 44 | } 45 | c.userEtag = r.Header().Get("ETag") 46 | return userList.Users, nil 47 | } 48 | 49 | type UserTraffic struct { 50 | UID int 51 | Upload int64 52 | Download int64 53 | } 54 | 55 | // ReportUserTraffic reports the user traffic 56 | func (c *Client) ReportUserTraffic(userTraffic []UserTraffic) error { 57 | data := make(map[int][]int64, len(userTraffic)) 58 | for i := range userTraffic { 59 | data[userTraffic[i].UID] = []int64{userTraffic[i].Upload, userTraffic[i].Download} 60 | } 61 | const path = "/api/v1/server/UniProxy/push" 62 | r, err := c.client.R(). 63 | SetBody(data). 64 | ForceContentType("application/json"). 65 | Post(path) 66 | err = c.checkResponse(r, path, err) 67 | if err != nil { 68 | return err 69 | } 70 | log.Println(r.String()) 71 | return nil 72 | } 73 | -------------------------------------------------------------------------------- /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 | "github.com/InazumaV/V2bX/conf" 5 | vCore "github.com/InazumaV/V2bX/core" 6 | "github.com/InazumaV/V2bX/limiter" 7 | "github.com/InazumaV/V2bX/node" 8 | log "github.com/sirupsen/logrus" 9 | "github.com/spf13/cobra" 10 | "gopkg.in/natefinch/lumberjack.v2" 11 | "os" 12 | "os/signal" 13 | "runtime" 14 | "syscall" 15 | ) 16 | 17 | var ( 18 | config string 19 | watch bool 20 | ) 21 | 22 | var serverCommand = cobra.Command{ 23 | Use: "server", 24 | Short: "Run V2bX server", 25 | Run: serverHandle, 26 | Args: cobra.NoArgs, 27 | } 28 | 29 | func init() { 30 | serverCommand.PersistentFlags(). 31 | StringVarP(&config, "config", "c", 32 | "/etc/V2bX/config.json", "config file path") 33 | serverCommand.PersistentFlags(). 34 | BoolVarP(&watch, "watch", "w", 35 | true, "watch file path change") 36 | command.AddCommand(&serverCommand) 37 | } 38 | 39 | func serverHandle(_ *cobra.Command, _ []string) { 40 | showVersion() 41 | c := conf.New() 42 | err := c.LoadFromPath(config) 43 | if err != nil { 44 | log.WithField("err", err).Error("Load config file failed") 45 | return 46 | } 47 | switch c.LogConfig.Level { 48 | case "debug": 49 | log.SetLevel(log.DebugLevel) 50 | case "info": 51 | log.SetLevel(log.InfoLevel) 52 | case "warn": 53 | log.SetLevel(log.WarnLevel) 54 | case "error": 55 | log.SetLevel(log.ErrorLevel) 56 | } 57 | if c.LogConfig.Output != "" { 58 | w := &lumberjack.Logger{ 59 | Filename: c.LogConfig.Output, 60 | MaxSize: 100, 61 | MaxBackups: 3, 62 | MaxAge: 28, 63 | Compress: true, 64 | } 65 | log.SetOutput(w) 66 | } 67 | limiter.Init() 68 | log.Info("Start V2bX...") 69 | vc, err := vCore.NewCore(c.CoresConfig) 70 | if err != nil { 71 | log.WithField("err", err).Error("new core failed") 72 | return 73 | } 74 | err = vc.Start() 75 | if err != nil { 76 | log.WithField("err", err).Error("Start core failed") 77 | return 78 | } 79 | defer vc.Close() 80 | log.Info("Core ", vc.Type(), " started") 81 | nodes := node.New() 82 | err = nodes.Start(c.NodeConfig, vc) 83 | if err != nil { 84 | log.WithField("err", err).Error("Run nodes failed") 85 | return 86 | } 87 | log.Info("Nodes started") 88 | xdns := os.Getenv("XRAY_DNS_PATH") 89 | sdns := os.Getenv("SING_DNS_PATH") 90 | if watch { 91 | err = c.Watch(config, xdns, sdns, func() { 92 | nodes.Close() 93 | err = vc.Close() 94 | if err != nil { 95 | log.WithField("err", err).Error("Restart node failed") 96 | return 97 | } 98 | vc, err = vCore.NewCore(c.CoresConfig) 99 | if err != nil { 100 | log.WithField("err", err).Error("New core failed") 101 | return 102 | } 103 | err = vc.Start() 104 | if err != nil { 105 | log.WithField("err", err).Error("Start core failed") 106 | return 107 | } 108 | log.Info("Core ", vc.Type(), " restarted") 109 | err = nodes.Start(c.NodeConfig, vc) 110 | if err != nil { 111 | log.WithField("err", err).Error("Run nodes failed") 112 | return 113 | } 114 | log.Info("Nodes restarted") 115 | runtime.GC() 116 | }) 117 | if err != nil { 118 | log.WithField("err", err).Error("start watch failed") 119 | return 120 | } 121 | } 122 | // clear memory 123 | runtime.GC() 124 | // wait exit signal 125 | { 126 | osSignals := make(chan os.Signal, 1) 127 | signal.Notify(osSignals, syscall.SIGINT, syscall.SIGKILL, syscall.SIGTERM) 128 | <-osSignals 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /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 | "strings" 6 | 7 | vCore "github.com/InazumaV/V2bX/core" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | var ( 12 | version = "TempVersion" //use ldflags replace 13 | codename = "V2bX" 14 | intro = "A V2board backend based on multi core" 15 | ) 16 | 17 | var versionCommand = cobra.Command{ 18 | Use: "version", 19 | Short: "Print version info", 20 | Run: func(_ *cobra.Command, _ []string) { 21 | showVersion() 22 | }, 23 | } 24 | 25 | func init() { 26 | command.AddCommand(&versionCommand) 27 | } 28 | 29 | func showVersion() { 30 | fmt.Printf("%s %s (%s) \n", codename, version, intro) 31 | fmt.Printf("Supported cores: %s\n", strings.Join(vCore.RegisteredCore(), ", ")) 32 | // Warning 33 | fmt.Println(Warn("This version need V2board version >= 1.7.0.")) 34 | fmt.Println(Warn("The version have many changed for config, please check your config file")) 35 | } 36 | -------------------------------------------------------------------------------- /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 map[string]*TrafficStorage 10 | lock sync.RWMutex 11 | } 12 | 13 | type TrafficStorage struct { 14 | UpCounter atomic.Int64 15 | DownCounter atomic.Int64 16 | } 17 | 18 | func NewTrafficCounter() *TrafficCounter { 19 | return &TrafficCounter{ 20 | counters: map[string]*TrafficStorage{}, 21 | } 22 | } 23 | 24 | func (c *TrafficCounter) GetCounter(id string) *TrafficStorage { 25 | c.lock.RLock() 26 | cts, ok := c.counters[id] 27 | c.lock.RUnlock() 28 | if !ok { 29 | cts = &TrafficStorage{} 30 | c.counters[id] = cts 31 | } 32 | return cts 33 | } 34 | 35 | func (c *TrafficCounter) GetUpCount(id string) int64 { 36 | c.lock.RLock() 37 | cts, ok := c.counters[id] 38 | c.lock.RUnlock() 39 | if ok { 40 | return cts.UpCounter.Load() 41 | } 42 | return 0 43 | } 44 | 45 | func (c *TrafficCounter) GetDownCount(id string) int64 { 46 | c.lock.RLock() 47 | cts, ok := c.counters[id] 48 | c.lock.RUnlock() 49 | if ok { 50 | return cts.DownCounter.Load() 51 | } 52 | return 0 53 | } 54 | 55 | func (c *TrafficCounter) Len() int { 56 | c.lock.RLock() 57 | defer c.lock.RUnlock() 58 | return len(c.counters) 59 | } 60 | 61 | func (c *TrafficCounter) Reset(id string) { 62 | c.lock.RLock() 63 | cts := c.GetCounter(id) 64 | c.lock.RUnlock() 65 | cts.UpCounter.Store(0) 66 | cts.DownCounter.Store(0) 67 | } 68 | 69 | func (c *TrafficCounter) Delete(id string) { 70 | c.lock.Lock() 71 | delete(c.counters, id) 72 | c.lock.Unlock() 73 | } 74 | 75 | func (c *TrafficCounter) Rx(id string, n int) { 76 | cts := c.GetCounter(id) 77 | cts.DownCounter.Add(int64(n)) 78 | } 79 | 80 | func (c *TrafficCounter) Tx(id string, n int) { 81 | cts := c.GetCounter(id) 82 | cts.UpCounter.Add(int64(n)) 83 | } 84 | 85 | func (c *TrafficCounter) IncConn(auth string) { 86 | return 87 | } 88 | 89 | func (c *TrafficCounter) DecConn(auth string) { 90 | return 91 | } 92 | -------------------------------------------------------------------------------- /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 | "github.com/sagernet/sing/common/buf" 8 | M "github.com/sagernet/sing/common/metadata" 9 | "github.com/sagernet/sing/common/network" 10 | ) 11 | 12 | func NewConnRateLimiter(c net.Conn, l *ratelimit.Bucket) *Conn { 13 | return &Conn{ 14 | Conn: c, 15 | limiter: l, 16 | } 17 | } 18 | 19 | type Conn struct { 20 | net.Conn 21 | limiter *ratelimit.Bucket 22 | } 23 | 24 | func (c *Conn) Read(b []byte) (n int, err error) { 25 | c.limiter.Wait(int64(len(b))) 26 | return c.Conn.Read(b) 27 | } 28 | 29 | func (c *Conn) Write(b []byte) (n int, err error) { 30 | c.limiter.Wait(int64(len(b))) 31 | return c.Conn.Write(b) 32 | } 33 | 34 | type PacketConnCounter struct { 35 | network.PacketConn 36 | limiter *ratelimit.Bucket 37 | } 38 | 39 | func NewPacketConnCounter(conn network.PacketConn, l *ratelimit.Bucket) network.PacketConn { 40 | return &PacketConnCounter{ 41 | PacketConn: conn, 42 | limiter: l, 43 | } 44 | } 45 | 46 | func (p *PacketConnCounter) ReadPacket(buff *buf.Buffer) (destination M.Socksaddr, err error) { 47 | pLen := buff.Len() 48 | destination, err = p.ReadPacket(buff) 49 | p.limiter.Wait(int64(buff.Len() - pLen)) 50 | return 51 | } 52 | 53 | func (p *PacketConnCounter) WritePacket(buff *buf.Buffer, destination M.Socksaddr) (err error) { 54 | p.limiter.Wait(int64(buff.Len())) 55 | return p.PacketConn.WritePacket(buff, destination) 56 | } 57 | -------------------------------------------------------------------------------- /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 | "github.com/InazumaV/V2bX/common/json5" 6 | "os" 7 | 8 | "github.com/goccy/go-json" 9 | ) 10 | 11 | type Conf struct { 12 | LogConfig LogConfig `json:"Log"` 13 | CoresConfig []CoreConfig `json:"Cores"` 14 | NodeConfig []NodeConfig `json:"Nodes"` 15 | } 16 | 17 | func New() *Conf { 18 | return &Conf{ 19 | LogConfig: LogConfig{ 20 | Level: "info", 21 | Output: "", 22 | }, 23 | } 24 | } 25 | 26 | func (p *Conf) LoadFromPath(filePath string) error { 27 | f, err := os.Open(filePath) 28 | if err != nil { 29 | return fmt.Errorf("open config file error: %s", err) 30 | } 31 | defer f.Close() 32 | return json.NewDecoder(json5.NewTrimNodeReader(f)).Decode(p) 33 | } 34 | -------------------------------------------------------------------------------- /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 | } 13 | 14 | type _CoreConfig CoreConfig 15 | 16 | func (c *CoreConfig) UnmarshalJSON(b []byte) error { 17 | err := json.Unmarshal(b, (*_CoreConfig)(c)) 18 | if err != nil { 19 | return err 20 | } 21 | switch c.Type { 22 | case "xray": 23 | c.XrayConfig = NewXrayConfig() 24 | return json.Unmarshal(b, c.XrayConfig) 25 | case "sing": 26 | c.SingConfig = NewSingConfig() 27 | return json.Unmarshal(b, c.SingConfig) 28 | } 29 | return nil 30 | } 31 | -------------------------------------------------------------------------------- /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 | "github.com/InazumaV/V2bX/common/json5" 6 | "github.com/goccy/go-json" 7 | "io" 8 | "net/http" 9 | "os" 10 | "strings" 11 | ) 12 | 13 | type NodeConfig struct { 14 | ApiConfig ApiConfig `json:"-"` 15 | Options Options `json:"-"` 16 | } 17 | 18 | type rawNodeConfig struct { 19 | Include string `json:"Include"` 20 | ApiRaw json.RawMessage `json:"ApiConfig"` 21 | OptRaw json.RawMessage `json:"Options"` 22 | } 23 | 24 | type ApiConfig struct { 25 | APIHost string `json:"ApiHost"` 26 | NodeID int `json:"NodeID"` 27 | Key string `json:"ApiKey"` 28 | NodeType string `json:"NodeType"` 29 | Timeout int `json:"Timeout"` 30 | RuleListPath string `json:"RuleListPath"` 31 | } 32 | 33 | func (n *NodeConfig) UnmarshalJSON(data []byte) (err error) { 34 | rn := rawNodeConfig{} 35 | err = json.Unmarshal(data, &rn) 36 | if err != nil { 37 | return err 38 | } 39 | if len(rn.Include) != 0 { 40 | file, _ := strings.CutPrefix(rn.Include, ":") 41 | switch file { 42 | case "http", "https": 43 | rsp, err := http.Get(file) 44 | if err != nil { 45 | return err 46 | } 47 | defer rsp.Body.Close() 48 | data, err = io.ReadAll(json5.NewTrimNodeReader(rsp.Body)) 49 | if err != nil { 50 | return fmt.Errorf("open include file error: %s", err) 51 | } 52 | default: 53 | f, err := os.Open(rn.Include) 54 | if err != nil { 55 | return fmt.Errorf("open include file error: %s", err) 56 | } 57 | defer f.Close() 58 | data, err = io.ReadAll(json5.NewTrimNodeReader(f)) 59 | if err != nil { 60 | return fmt.Errorf("open include file error: %s", err) 61 | } 62 | } 63 | err = json.Unmarshal(data, &rn) 64 | if err != nil { 65 | return fmt.Errorf("unmarshal include file error: %s", err) 66 | } 67 | } 68 | 69 | n.ApiConfig = ApiConfig{ 70 | APIHost: "http://127.0.0.1", 71 | Timeout: 30, 72 | } 73 | if len(rn.ApiRaw) > 0 { 74 | err = json.Unmarshal(rn.ApiRaw, &n.ApiConfig) 75 | if err != nil { 76 | return 77 | } 78 | } else { 79 | err = json.Unmarshal(data, &n.ApiConfig) 80 | if err != nil { 81 | return 82 | } 83 | } 84 | 85 | n.Options = Options{ 86 | ListenIP: "0.0.0.0", 87 | SendIP: "0.0.0.0", 88 | CertConfig: NewCertConfig(), 89 | } 90 | if len(rn.OptRaw) > 0 { 91 | err = json.Unmarshal(rn.OptRaw, &n.Options) 92 | if err != nil { 93 | return 94 | } 95 | } else { 96 | err = json.Unmarshal(data, &n.Options) 97 | if err != nil { 98 | return 99 | } 100 | } 101 | return 102 | } 103 | 104 | type Options struct { 105 | Name string `json:"Name"` 106 | Core string `json:"Core"` 107 | CoreName string `json:"CoreName"` 108 | ListenIP string `json:"ListenIP"` 109 | SendIP string `json:"SendIP"` 110 | LimitConfig LimitConfig `json:"LimitConfig"` 111 | RawOptions json.RawMessage `json:"RawOptions"` 112 | XrayOptions *XrayOptions `json:"XrayOptions"` 113 | SingOptions *SingOptions `json:"SingOptions"` 114 | CertConfig *CertConfig `json:"CertConfig"` 115 | } 116 | 117 | func (o *Options) UnmarshalJSON(data []byte) error { 118 | type opt Options 119 | err := json.Unmarshal(data, (*opt)(o)) 120 | if err != nil { 121 | return err 122 | } 123 | switch o.Core { 124 | case "xray": 125 | o.XrayOptions = NewXrayOptions() 126 | return json.Unmarshal(data, o.XrayOptions) 127 | case "sing": 128 | o.SingOptions = NewSingOptions() 129 | return json.Unmarshal(data, o.SingOptions) 130 | default: 131 | o.Core = "" 132 | o.RawOptions = data 133 | } 134 | return nil 135 | } 136 | -------------------------------------------------------------------------------- /conf/sing.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "github.com/inazumav/sing-box/option" 5 | ) 6 | 7 | type SingConfig struct { 8 | LogConfig SingLogConfig `json:"Log"` 9 | NtpConfig SingNtpConfig `json:"NTP"` 10 | DnsConfigPath string `json:"DnsConfigPath"` 11 | OriginalPath string `json:"OriginalPath"` 12 | } 13 | 14 | type SingLogConfig struct { 15 | Disabled bool `json:"Disable"` 16 | Level string `json:"Level"` 17 | Output string `json:"Output"` 18 | Timestamp bool `json:"Timestamp"` 19 | } 20 | 21 | func NewSingConfig() *SingConfig { 22 | return &SingConfig{ 23 | LogConfig: SingLogConfig{ 24 | Level: "error", 25 | Timestamp: true, 26 | }, 27 | NtpConfig: SingNtpConfig{ 28 | Enable: false, 29 | Server: "time.apple.com", 30 | ServerPort: 0, 31 | }, 32 | } 33 | } 34 | 35 | type SingOptions struct { 36 | EnableProxyProtocol bool `json:"EnableProxyProtocol"` 37 | TCPFastOpen bool `json:"EnableTFO"` 38 | SniffEnabled bool `json:"EnableSniff"` 39 | EnableDNS bool `json:"EnableDNS"` 40 | DomainStrategy option.DomainStrategy `json:"DomainStrategy"` 41 | SniffOverrideDestination bool `json:"SniffOverrideDestination"` 42 | FallBackConfigs *FallBackConfigForSing `json:"FallBackConfigs"` 43 | } 44 | 45 | type SingNtpConfig struct { 46 | Enable bool `json:"Enable"` 47 | Server string `json:"Server"` 48 | ServerPort uint16 `json:"ServerPort"` 49 | } 50 | 51 | type FallBackConfigForSing struct { 52 | // sing-box 53 | FallBack FallBack `json:"FallBack"` 54 | FallBackForALPN map[string]FallBack `json:"FallBackForALPN"` 55 | } 56 | 57 | type FallBack struct { 58 | Server string `json:"Server"` 59 | ServerPort string `json:"ServerPort"` 60 | } 61 | 62 | func NewSingOptions() *SingOptions { 63 | return &SingOptions{ 64 | EnableDNS: false, 65 | EnableProxyProtocol: false, 66 | TCPFastOpen: false, 67 | SniffEnabled: true, 68 | SniffOverrideDestination: true, 69 | FallBackConfigs: &FallBackConfigForSing{}, 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /conf/watch.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "fmt" 5 | "github.com/fsnotify/fsnotify" 6 | "log" 7 | "path" 8 | "path/filepath" 9 | "strings" 10 | "time" 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 dir 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(path.Dir(filePath)) 55 | if err != nil { 56 | return fmt.Errorf("watch file error: %s", err) 57 | } 58 | if xDnsPath != "" { 59 | err = watcher.Add(path.Dir(xDnsPath)) 60 | if err != nil { 61 | return fmt.Errorf("watch dns file error: %s", err) 62 | } 63 | } 64 | if sDnsPath != "" { 65 | err = watcher.Add(path.Dir(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/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) 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/InazumaV/V2bX/api/panel" 10 | "github.com/InazumaV/V2bX/conf" 11 | "github.com/hashicorp/go-multierror" 12 | ) 13 | 14 | type Selector struct { 15 | cores map[string]Core 16 | nodes sync.Map 17 | } 18 | 19 | func NewSelector(c []conf.CoreConfig) (Core, error) { 20 | cs := make(map[string]Core, len(c)) 21 | for _, t := range c { 22 | f, ok := cores[strings.ToLower(t.Type)] 23 | if !ok { 24 | return nil, errors.New("unknown core type: " + t.Type) 25 | } 26 | core1, err := f(&t) 27 | if err != nil { 28 | return nil, err 29 | } 30 | if t.Name == "" { 31 | cs[t.Type] = core1 32 | } else { 33 | cs[t.Name] = core1 34 | } 35 | } 36 | return &Selector{ 37 | cores: cs, 38 | }, nil 39 | } 40 | 41 | func (s *Selector) Start() error { 42 | for i := range s.cores { 43 | err := s.cores[i].Start() 44 | if err != nil { 45 | return err 46 | } 47 | } 48 | return nil 49 | } 50 | 51 | func (s *Selector) Close() error { 52 | var errs error 53 | for i := range s.cores { 54 | errs = multierror.Append(errs, s.cores[i].Close()) 55 | } 56 | return errs 57 | } 58 | 59 | func isSupported(protocol string, protocols []string) bool { 60 | for i := range protocols { 61 | if protocol == protocols[i] { 62 | return true 63 | } 64 | } 65 | return false 66 | } 67 | 68 | func (s *Selector) AddNode(tag string, info *panel.NodeInfo, option *conf.Options) error { 69 | var core Core 70 | if len(option.CoreName) > 0 { 71 | // use name to select core 72 | if c, ok := s.cores[option.CoreName]; ok { 73 | core = c 74 | } 75 | } else { 76 | // use type to select core 77 | for _, c := range s.cores { 78 | if len(option.Core) == 0 { 79 | if !isSupported(info.Type, c.Protocols()) { 80 | continue 81 | } 82 | } else if option.Core != c.Type() { 83 | continue 84 | } 85 | core = c 86 | } 87 | } 88 | if core == nil { 89 | return errors.New("the node type is not support") 90 | } 91 | if len(option.Core) == 0 { 92 | option.Core = core.Type() 93 | err := option.UnmarshalJSON(option.RawOptions) 94 | if err != nil { 95 | return fmt.Errorf("unmarshal option error: %s", err) 96 | } 97 | option.RawOptions = nil 98 | } 99 | err := core.AddNode(tag, info, option) 100 | if err != nil { 101 | return err 102 | } 103 | s.nodes.Store(tag, core) 104 | return nil 105 | } 106 | 107 | func (s *Selector) DelNode(tag string) error { 108 | if t, e := s.nodes.Load(tag); e { 109 | err := s.cores[t.(string)].DelNode(tag) 110 | if err != nil { 111 | return err 112 | } 113 | s.nodes.Delete(tag) 114 | return nil 115 | } 116 | return errors.New("the node is not have") 117 | } 118 | 119 | func (s *Selector) AddUsers(p *AddUsersParams) (added int, err error) { 120 | t, e := s.nodes.Load(p.Tag) 121 | if !e { 122 | return 0, errors.New("the node is not have") 123 | } 124 | return t.(Core).AddUsers(p) 125 | } 126 | 127 | func (s *Selector) GetUserTraffic(tag, uuid string, reset bool) (up int64, down int64) { 128 | t, e := s.nodes.Load(tag) 129 | if !e { 130 | return 0, 0 131 | } 132 | return t.(Core).GetUserTraffic(tag, uuid, reset) 133 | } 134 | 135 | func (s *Selector) DelUsers(users []panel.UserInfo, tag string) error { 136 | t, e := s.nodes.Load(tag) 137 | if !e { 138 | return errors.New("the node is not have") 139 | } 140 | return t.(Core).DelUsers(users, tag) 141 | } 142 | 143 | func (s *Selector) Protocols() []string { 144 | protocols := make([]string, 0) 145 | for i := range s.cores { 146 | protocols = append(protocols, s.cores[i].Protocols()...) 147 | } 148 | return protocols 149 | } 150 | 151 | func (s *Selector) Type() string { 152 | t := "Selector(" 153 | var flag bool 154 | for n, c := range s.cores { 155 | if flag { 156 | t += " " 157 | } else { 158 | flag = true 159 | } 160 | if len(n) == 0 { 161 | t += c.Type() 162 | } else { 163 | t += n 164 | } 165 | } 166 | t += ")" 167 | return t 168 | } 169 | -------------------------------------------------------------------------------- /core/sing/box_outbound.go: -------------------------------------------------------------------------------- 1 | package sing 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/inazumav/sing-box/adapter" 7 | "github.com/sagernet/sing/common" 8 | E "github.com/sagernet/sing/common/exceptions" 9 | F "github.com/sagernet/sing/common/format" 10 | ) 11 | 12 | func (b *Box) startOutbounds() error { 13 | outboundTags := make(map[adapter.Outbound]string) 14 | outbounds := make(map[string]adapter.Outbound) 15 | for i, outboundToStart := range b.outbounds { 16 | var outboundTag string 17 | if outboundToStart.Tag() == "" { 18 | outboundTag = F.ToString(i) 19 | } else { 20 | outboundTag = outboundToStart.Tag() 21 | } 22 | if _, exists := outbounds[outboundTag]; exists { 23 | return E.New("outbound tag ", outboundTag, " duplicated") 24 | } 25 | outboundTags[outboundToStart] = outboundTag 26 | outbounds[outboundTag] = outboundToStart 27 | } 28 | started := make(map[string]bool) 29 | for { 30 | canContinue := false 31 | startOne: 32 | for _, outboundToStart := range b.outbounds { 33 | outboundTag := outboundTags[outboundToStart] 34 | if started[outboundTag] { 35 | continue 36 | } 37 | dependencies := outboundToStart.Dependencies() 38 | for _, dependency := range dependencies { 39 | if !started[dependency] { 40 | continue startOne 41 | } 42 | } 43 | started[outboundTag] = true 44 | canContinue = true 45 | if starter, isStarter := outboundToStart.(common.Starter); isStarter { 46 | b.logger.Trace("initializing outbound/", outboundToStart.Type(), "[", outboundTag, "]") 47 | err := starter.Start() 48 | if err != nil { 49 | return E.Cause(err, "initialize outbound/", outboundToStart.Type(), "[", outboundTag, "]") 50 | } 51 | } 52 | } 53 | if len(started) == len(b.outbounds) { 54 | break 55 | } 56 | if canContinue { 57 | continue 58 | } 59 | currentOutbound := common.Find(b.outbounds, func(it adapter.Outbound) bool { 60 | return !started[outboundTags[it]] 61 | }) 62 | var lintOutbound func(oTree []string, oCurrent adapter.Outbound) error 63 | lintOutbound = func(oTree []string, oCurrent adapter.Outbound) error { 64 | problemOutboundTag := common.Find(oCurrent.Dependencies(), func(it string) bool { 65 | return !started[it] 66 | }) 67 | if common.Contains(oTree, problemOutboundTag) { 68 | return E.New("circular outbound dependency: ", strings.Join(oTree, " -> "), " -> ", problemOutboundTag) 69 | } 70 | problemOutbound := outbounds[problemOutboundTag] 71 | if problemOutbound == nil { 72 | return E.New("dependency[", problemOutbound, "] not found for outbound[", outboundTags[oCurrent], "]") 73 | } 74 | return lintOutbound(append(oTree, problemOutboundTag), problemOutbound) 75 | } 76 | return lintOutbound([]string{outboundTags[currentOutbound]}, currentOutbound) 77 | } 78 | return nil 79 | } 80 | -------------------------------------------------------------------------------- /core/sing/debug_go118.go: -------------------------------------------------------------------------------- 1 | //go:build !go1.19 2 | 3 | package sing 4 | 5 | import ( 6 | "runtime/debug" 7 | 8 | "github.com/inazumav/sing-box/common/dialer/conntrack" 9 | "github.com/inazumav/sing-box/option" 10 | ) 11 | 12 | func applyDebugOptions(options option.DebugOptions) { 13 | applyDebugListenOption(options) 14 | if options.GCPercent != nil { 15 | debug.SetGCPercent(*options.GCPercent) 16 | } 17 | if options.MaxStack != nil { 18 | debug.SetMaxStack(*options.MaxStack) 19 | } 20 | if options.MaxThreads != nil { 21 | debug.SetMaxThreads(*options.MaxThreads) 22 | } 23 | if options.PanicOnFault != nil { 24 | debug.SetPanicOnFault(*options.PanicOnFault) 25 | } 26 | if options.TraceBack != "" { 27 | debug.SetTraceback(options.TraceBack) 28 | } 29 | if options.MemoryLimit != 0 { 30 | // debug.SetMemoryLimit(int64(options.MemoryLimit)) 31 | conntrack.MemoryLimit = int64(options.MemoryLimit) 32 | } 33 | if options.OOMKiller != nil { 34 | conntrack.KillerEnabled = *options.OOMKiller 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /core/sing/debug_go119.go: -------------------------------------------------------------------------------- 1 | //go:build go1.19 2 | 3 | package sing 4 | 5 | import ( 6 | "runtime/debug" 7 | 8 | "github.com/inazumav/sing-box/common/dialer/conntrack" 9 | "github.com/inazumav/sing-box/option" 10 | ) 11 | 12 | func applyDebugOptions(options option.DebugOptions) { 13 | applyDebugListenOption(options) 14 | if options.GCPercent != nil { 15 | debug.SetGCPercent(*options.GCPercent) 16 | } 17 | if options.MaxStack != nil { 18 | debug.SetMaxStack(*options.MaxStack) 19 | } 20 | if options.MaxThreads != nil { 21 | debug.SetMaxThreads(*options.MaxThreads) 22 | } 23 | if options.PanicOnFault != nil { 24 | debug.SetPanicOnFault(*options.PanicOnFault) 25 | } 26 | if options.TraceBack != "" { 27 | debug.SetTraceback(options.TraceBack) 28 | } 29 | if options.MemoryLimit != 0 { 30 | debug.SetMemoryLimit(int64(options.MemoryLimit)) 31 | conntrack.MemoryLimit = int64(options.MemoryLimit) 32 | } 33 | if options.OOMKiller != nil { 34 | conntrack.KillerEnabled = *options.OOMKiller 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /core/sing/debug_http.go: -------------------------------------------------------------------------------- 1 | package sing 2 | 3 | import ( 4 | "net/http" 5 | "net/http/pprof" 6 | "runtime" 7 | "runtime/debug" 8 | 9 | "github.com/inazumav/sing-box/common/badjson" 10 | "github.com/inazumav/sing-box/common/json" 11 | "github.com/inazumav/sing-box/log" 12 | "github.com/inazumav/sing-box/option" 13 | E "github.com/sagernet/sing/common/exceptions" 14 | 15 | "github.com/dustin/go-humanize" 16 | "github.com/go-chi/chi/v5" 17 | ) 18 | 19 | var debugHTTPServer *http.Server 20 | 21 | func applyDebugListenOption(options option.DebugOptions) { 22 | if debugHTTPServer != nil { 23 | debugHTTPServer.Close() 24 | debugHTTPServer = nil 25 | } 26 | if options.Listen == "" { 27 | return 28 | } 29 | r := chi.NewMux() 30 | r.Route("/debug", func(r chi.Router) { 31 | r.Get("/gc", func(writer http.ResponseWriter, request *http.Request) { 32 | writer.WriteHeader(http.StatusNoContent) 33 | go debug.FreeOSMemory() 34 | }) 35 | r.Get("/memory", func(writer http.ResponseWriter, request *http.Request) { 36 | var memStats runtime.MemStats 37 | runtime.ReadMemStats(&memStats) 38 | 39 | var memObject badjson.JSONObject 40 | memObject.Put("heap", humanize.IBytes(memStats.HeapInuse)) 41 | memObject.Put("stack", humanize.IBytes(memStats.StackInuse)) 42 | memObject.Put("idle", humanize.IBytes(memStats.HeapIdle-memStats.HeapReleased)) 43 | memObject.Put("goroutines", runtime.NumGoroutine()) 44 | memObject.Put("rss", rusageMaxRSS()) 45 | 46 | encoder := json.NewEncoder(writer) 47 | encoder.SetIndent("", " ") 48 | encoder.Encode(memObject) 49 | }) 50 | r.HandleFunc("/pprof", pprof.Index) 51 | r.HandleFunc("/pprof/*", pprof.Index) 52 | r.HandleFunc("/pprof/cmdline", pprof.Cmdline) 53 | r.HandleFunc("/pprof/profile", pprof.Profile) 54 | r.HandleFunc("/pprof/symbol", pprof.Symbol) 55 | r.HandleFunc("/pprof/trace", pprof.Trace) 56 | }) 57 | debugHTTPServer = &http.Server{ 58 | Addr: options.Listen, 59 | Handler: r, 60 | } 61 | go func() { 62 | err := debugHTTPServer.ListenAndServe() 63 | if err != nil && !E.IsClosed(err) { 64 | log.Error(E.Cause(err, "serve debug HTTP server")) 65 | } 66 | }() 67 | } 68 | -------------------------------------------------------------------------------- /core/sing/debug_linux.go: -------------------------------------------------------------------------------- 1 | package sing 2 | 3 | import ( 4 | "runtime" 5 | "syscall" 6 | ) 7 | 8 | func rusageMaxRSS() float64 { 9 | ru := syscall.Rusage{} 10 | err := syscall.Getrusage(syscall.RUSAGE_SELF, &ru) 11 | if err != nil { 12 | return 0 13 | } 14 | 15 | rss := float64(ru.Maxrss) 16 | if runtime.GOOS == "darwin" || runtime.GOOS == "ios" { 17 | rss /= 1 << 20 // ru_maxrss is bytes on darwin 18 | } else { 19 | // ru_maxrss is kilobytes elsewhere (linux, openbsd, etc) 20 | rss /= 1 << 10 21 | } 22 | return rss 23 | } 24 | -------------------------------------------------------------------------------- /core/sing/debug_stub.go: -------------------------------------------------------------------------------- 1 | //go:build !linux 2 | 3 | package sing 4 | 5 | func rusageMaxRSS() float64 { 6 | return -1 7 | } 8 | -------------------------------------------------------------------------------- /core/sing/dns.go: -------------------------------------------------------------------------------- 1 | package sing 2 | 3 | import ( 4 | "bytes" 5 | "github.com/InazumaV/V2bX/api/panel" 6 | "github.com/goccy/go-json" 7 | log "github.com/sirupsen/logrus" 8 | "os" 9 | "strings" 10 | ) 11 | 12 | func updateDNSConfig(node *panel.NodeInfo) (err error) { 13 | dnsPath := os.Getenv("SING_DNS_PATH") 14 | if len(node.RawDNS.DNSJson) != 0 { 15 | err = saveDnsConfig(node.RawDNS.DNSJson, dnsPath) 16 | } else if len(node.RawDNS.DNSMap) != 0 { 17 | dnsConfig := DNSConfig{ 18 | Servers: []map[string]interface{}{ 19 | { 20 | "tag": "default", 21 | "address": "https://8.8.8.8/dns-query", 22 | "detour": "direct", 23 | }, 24 | }, 25 | } 26 | for id, value := range node.RawDNS.DNSMap { 27 | dnsConfig.Servers = append(dnsConfig.Servers, 28 | map[string]interface{}{ 29 | "tag": id, 30 | "address": value["address"], 31 | "address_resolver": "default", 32 | "detour": "direct", 33 | }, 34 | ) 35 | rule := map[string]interface{}{ 36 | "server": id, 37 | "disable_cache": true, 38 | } 39 | for _, ruleType := range []string{"domain_suffix", "domain_keyword", "domain_regex", "geosite"} { 40 | var domains []string 41 | for _, v := range value["domains"].([]string) { 42 | split := strings.SplitN(v, ":", 2) 43 | prefix := strings.ToLower(split[0]) 44 | if prefix == ruleType || (prefix == "domain" && ruleType == "domain_suffix") { 45 | if len(split) > 1 { 46 | domains = append(domains, split[1]) 47 | } 48 | if len(domains) > 0 { 49 | rule[ruleType] = domains 50 | } 51 | } 52 | } 53 | } 54 | dnsConfig.Rules = append(dnsConfig.Rules, rule) 55 | } 56 | dnsConfigJSON, err := json.MarshalIndent(dnsConfig, "", " ") 57 | if err != nil { 58 | log.WithField("err", err).Error("Error marshaling dnsConfig to JSON") 59 | return err 60 | } 61 | err = saveDnsConfig(dnsConfigJSON, dnsPath) 62 | } 63 | return err 64 | } 65 | 66 | func saveDnsConfig(dns []byte, dnsPath string) (err error) { 67 | currentData, err := os.ReadFile(dnsPath) 68 | if err != nil { 69 | log.WithField("err", err).Error("Failed to read SING_DNS_PATH") 70 | return err 71 | } 72 | if !bytes.Equal(currentData, dns) { 73 | if err = os.Truncate(dnsPath, 0); err != nil { 74 | log.WithField("err", err).Error("Failed to clear SING DNS PATH file") 75 | } 76 | if err = os.WriteFile(dnsPath, dns, 0644); err != nil { 77 | log.WithField("err", err).Error("Failed to write DNS to SING DNS PATH file") 78 | } 79 | } 80 | return err 81 | } 82 | -------------------------------------------------------------------------------- /core/sing/hook.go: -------------------------------------------------------------------------------- 1 | package sing 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "sync" 7 | 8 | "github.com/inazumav/sing-box/common/urltest" 9 | 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/inazumav/sing-box/adapter" 16 | "github.com/inazumav/sing-box/log" 17 | N "github.com/sagernet/sing/common/network" 18 | ) 19 | 20 | type HookServer struct { 21 | logger log.Logger 22 | counter sync.Map 23 | } 24 | 25 | func (h *HookServer) ModeList() []string { 26 | return nil 27 | } 28 | 29 | func NewHookServer(logger log.Logger) *HookServer { 30 | return &HookServer{ 31 | logger: logger, 32 | counter: sync.Map{}, 33 | } 34 | } 35 | 36 | func (h *HookServer) Start() error { 37 | return nil 38 | } 39 | 40 | func (h *HookServer) Close() error { 41 | return nil 42 | } 43 | 44 | func (h *HookServer) PreStart() error { 45 | return nil 46 | } 47 | 48 | func (h *HookServer) RoutedConnection(_ context.Context, conn net.Conn, m adapter.InboundContext, _ adapter.Rule) (net.Conn, adapter.Tracker) { 49 | t := &Tracker{l: func() {}} 50 | l, err := limiter.GetLimiter(m.Inbound) 51 | if err != nil { 52 | log.Error("get limiter for ", m.Inbound, " error: ", err) 53 | } 54 | if l.CheckDomainRule(m.Domain) { 55 | conn.Close() 56 | h.logger.Error("[", m.Inbound, "] ", 57 | "Limited ", m.User, " access to ", m.Domain, " by domain rule") 58 | return conn, t 59 | } 60 | if l.CheckProtocolRule(m.Protocol) { 61 | conn.Close() 62 | h.logger.Error("[", m.Inbound, "] ", 63 | "Limited ", m.User, " use ", m.Domain, " by protocol rule") 64 | return conn, t 65 | } 66 | ip := m.Source.Addr.String() 67 | if b, r := l.CheckLimit(m.User, ip, true); r { 68 | conn.Close() 69 | h.logger.Error("[", m.Inbound, "] ", "Limited ", m.User, " by ip or conn") 70 | return conn, t 71 | } else if b != nil { 72 | conn = rate.NewConnRateLimiter(conn, b) 73 | } 74 | t.l = func() { 75 | l.ConnLimiter.DelConnCount(m.User, ip) 76 | } 77 | if c, ok := h.counter.Load(m.Inbound); ok { 78 | return counter.NewConnCounter(conn, c.(*counter.TrafficCounter).GetCounter(m.User)), t 79 | } else { 80 | c := counter.NewTrafficCounter() 81 | h.counter.Store(m.Inbound, c) 82 | return counter.NewConnCounter(conn, c.GetCounter(m.User)), t 83 | } 84 | } 85 | 86 | func (h *HookServer) RoutedPacketConnection(_ context.Context, conn N.PacketConn, m adapter.InboundContext, _ adapter.Rule) (N.PacketConn, adapter.Tracker) { 87 | t := &Tracker{ 88 | l: func() {}, 89 | } 90 | l, err := limiter.GetLimiter(m.Inbound) 91 | if err != nil { 92 | log.Error("get limiter for ", m.Inbound, " error: ", err) 93 | } 94 | if l.CheckDomainRule(m.Domain) { 95 | conn.Close() 96 | h.logger.Error("[", m.Inbound, "] ", 97 | "Limited ", m.User, " access to ", m.Domain, " by domain rule") 98 | return conn, t 99 | } 100 | if l.CheckProtocolRule(m.Protocol) { 101 | conn.Close() 102 | h.logger.Error("[", m.Inbound, "] ", 103 | "Limited ", m.User, " use ", m.Domain, " by protocol rule") 104 | return conn, t 105 | } 106 | ip := m.Source.Addr.String() 107 | if b, r := l.CheckLimit(m.User, ip, true); r { 108 | conn.Close() 109 | h.logger.Error("[", m.Inbound, "] ", "Limited ", m.User, " by ip or conn") 110 | return conn, &Tracker{l: func() {}} 111 | } else if b != nil { 112 | conn = rate.NewPacketConnCounter(conn, b) 113 | } 114 | if c, ok := h.counter.Load(m.Inbound); ok { 115 | return counter.NewPacketConnCounter(conn, c.(*counter.TrafficCounter).GetCounter(m.User)), t 116 | } else { 117 | c := counter.NewTrafficCounter() 118 | h.counter.Store(m.Inbound, c) 119 | return counter.NewPacketConnCounter(conn, c.GetCounter(m.User)), t 120 | } 121 | } 122 | 123 | // not need 124 | 125 | func (h *HookServer) Mode() string { 126 | return "" 127 | } 128 | func (h *HookServer) StoreSelected() bool { 129 | return false 130 | } 131 | func (h *HookServer) CacheFile() adapter.ClashCacheFile { 132 | return nil 133 | } 134 | func (h *HookServer) HistoryStorage() *urltest.HistoryStorage { 135 | return nil 136 | } 137 | 138 | func (h *HookServer) StoreFakeIP() bool { 139 | return false 140 | } 141 | 142 | type Tracker struct { 143 | l func() 144 | } 145 | 146 | func (t *Tracker) Leave() { 147 | t.l() 148 | } 149 | -------------------------------------------------------------------------------- /core/sing/node.go: -------------------------------------------------------------------------------- 1 | package sing 2 | 3 | import ( 4 | "context" 5 | "crypto/rand" 6 | "encoding/base64" 7 | "fmt" 8 | "net/netip" 9 | "net/url" 10 | "strconv" 11 | "strings" 12 | "time" 13 | 14 | "github.com/inazumav/sing-box/inbound" 15 | F "github.com/sagernet/sing/common/format" 16 | 17 | "github.com/InazumaV/V2bX/api/panel" 18 | "github.com/InazumaV/V2bX/conf" 19 | "github.com/goccy/go-json" 20 | "github.com/inazumav/sing-box/option" 21 | ) 22 | 23 | type WsNetworkConfig struct { 24 | Path string `json:"path"` 25 | Headers map[string]string `json:"headers"` 26 | } 27 | 28 | func getInboundOptions(tag string, info *panel.NodeInfo, c *conf.Options) (option.Inbound, error) { 29 | addr, err := netip.ParseAddr(c.ListenIP) 30 | if err != nil { 31 | return option.Inbound{}, fmt.Errorf("the listen ip not vail") 32 | } 33 | var domainStrategy option.DomainStrategy 34 | if c.SingOptions.EnableDNS { 35 | domainStrategy = c.SingOptions.DomainStrategy 36 | } 37 | listen := option.ListenOptions{ 38 | Listen: (*option.ListenAddress)(&addr), 39 | ListenPort: uint16(info.Common.ServerPort), 40 | ProxyProtocol: c.SingOptions.EnableProxyProtocol, 41 | TCPFastOpen: c.SingOptions.TCPFastOpen, 42 | InboundOptions: option.InboundOptions{ 43 | SniffEnabled: c.SingOptions.SniffEnabled, 44 | SniffOverrideDestination: c.SingOptions.SniffOverrideDestination, 45 | DomainStrategy: domainStrategy, 46 | }, 47 | } 48 | var tls option.InboundTLSOptions 49 | switch info.Security { 50 | case panel.Tls: 51 | if c.CertConfig == nil { 52 | return option.Inbound{}, fmt.Errorf("the CertConfig is not vail") 53 | } 54 | switch c.CertConfig.CertMode { 55 | case "none", "": 56 | break // disable 57 | default: 58 | tls.Enabled = true 59 | tls.CertificatePath = c.CertConfig.CertFile 60 | tls.KeyPath = c.CertConfig.KeyFile 61 | } 62 | case panel.Reality: 63 | tls.Enabled = true 64 | v := info.VAllss 65 | tls.ServerName = v.TlsSettings.ServerName 66 | dest, _ := strconv.Atoi(v.TlsSettings.ServerPort) 67 | mtd, _ := time.ParseDuration(v.RealityConfig.MaxTimeDiff) 68 | tls.Reality = &option.InboundRealityOptions{ 69 | Enabled: true, 70 | ShortID: []string{v.TlsSettings.ShortId}, 71 | PrivateKey: v.TlsSettings.PrivateKey, 72 | Handshake: option.InboundRealityHandshakeOptions{ 73 | ServerOptions: option.ServerOptions{ 74 | Server: tls.ServerName, 75 | ServerPort: uint16(dest), 76 | }, 77 | }, 78 | MaxTimeDifference: option.Duration(mtd), 79 | } 80 | } 81 | in := option.Inbound{ 82 | Tag: tag, 83 | } 84 | switch info.Type { 85 | case "vmess", "vless": 86 | n := info.VAllss 87 | t := option.V2RayTransportOptions{ 88 | Type: n.Network, 89 | } 90 | switch n.Network { 91 | case "tcp": 92 | t.Type = "" 93 | case "ws": 94 | var ( 95 | path string 96 | ed int 97 | headers map[string]option.Listable[string] 98 | ) 99 | if len(n.NetworkSettings) != 0 { 100 | network := WsNetworkConfig{} 101 | err := json.Unmarshal(n.NetworkSettings, &network) 102 | if err != nil { 103 | return option.Inbound{}, fmt.Errorf("decode NetworkSettings error: %s", err) 104 | } 105 | var u *url.URL 106 | u, err = url.Parse(network.Path) 107 | if err != nil { 108 | return option.Inbound{}, fmt.Errorf("parse path error: %s", err) 109 | } 110 | path = u.Path 111 | ed, _ = strconv.Atoi(u.Query().Get("ed")) 112 | headers = make(map[string]option.Listable[string], len(network.Headers)) 113 | for k, v := range network.Headers { 114 | headers[k] = option.Listable[string]{ 115 | v, 116 | } 117 | } 118 | } 119 | t.WebsocketOptions = option.V2RayWebsocketOptions{ 120 | Path: path, 121 | EarlyDataHeaderName: "Sec-WebSocket-Protocol", 122 | MaxEarlyData: uint32(ed), 123 | Headers: headers, 124 | } 125 | case "grpc": 126 | if len(n.NetworkSettings) != 0 { 127 | err := json.Unmarshal(n.NetworkSettings, &t.GRPCOptions) 128 | if err != nil { 129 | return option.Inbound{}, fmt.Errorf("decode NetworkSettings error: %s", err) 130 | } 131 | } 132 | } 133 | if info.Type == "vless" { 134 | in.Type = "vless" 135 | in.VLESSOptions = option.VLESSInboundOptions{ 136 | ListenOptions: listen, 137 | TLS: &tls, 138 | Transport: &t, 139 | } 140 | } else { 141 | in.Type = "vmess" 142 | in.VMessOptions = option.VMessInboundOptions{ 143 | ListenOptions: listen, 144 | TLS: &tls, 145 | Transport: &t, 146 | } 147 | } 148 | case "shadowsocks": 149 | in.Type = "shadowsocks" 150 | n := info.Shadowsocks 151 | var keyLength int 152 | switch n.Cipher { 153 | case "2022-blake3-aes-128-gcm": 154 | keyLength = 16 155 | case "2022-blake3-aes-256-gcm": 156 | keyLength = 32 157 | default: 158 | keyLength = 16 159 | } 160 | in.ShadowsocksOptions = option.ShadowsocksInboundOptions{ 161 | ListenOptions: listen, 162 | Method: n.Cipher, 163 | } 164 | p := make([]byte, keyLength) 165 | _, _ = rand.Read(p) 166 | randomPasswd := string(p) 167 | if strings.Contains(n.Cipher, "2022") { 168 | in.ShadowsocksOptions.Password = n.ServerKey 169 | randomPasswd = base64.StdEncoding.EncodeToString([]byte(randomPasswd)) 170 | } 171 | in.ShadowsocksOptions.Users = []option.ShadowsocksUser{{ 172 | Password: randomPasswd, 173 | }} 174 | case "trojan": 175 | in.Type = "trojan" 176 | in.TrojanOptions = option.TrojanInboundOptions{ 177 | ListenOptions: listen, 178 | TLS: &tls, 179 | } 180 | if c.SingOptions.FallBackConfigs != nil { 181 | // fallback handling 182 | fallback := c.SingOptions.FallBackConfigs.FallBack 183 | fallbackPort, err := strconv.Atoi(fallback.ServerPort) 184 | if err == nil { 185 | in.TrojanOptions.Fallback = &option.ServerOptions{ 186 | Server: fallback.Server, 187 | ServerPort: uint16(fallbackPort), 188 | } 189 | } 190 | fallbackForALPNMap := c.SingOptions.FallBackConfigs.FallBackForALPN 191 | fallbackForALPN := make(map[string]*option.ServerOptions, len(fallbackForALPNMap)) 192 | if err := processFallback(c, fallbackForALPN); err == nil { 193 | in.TrojanOptions.FallbackForALPN = fallbackForALPN 194 | } 195 | } 196 | case "hysteria": 197 | in.Type = "hysteria" 198 | in.HysteriaOptions = option.HysteriaInboundOptions{ 199 | ListenOptions: listen, 200 | UpMbps: info.Hysteria.UpMbps, 201 | DownMbps: info.Hysteria.DownMbps, 202 | Obfs: info.Hysteria.Obfs, 203 | TLS: &tls, 204 | } 205 | } 206 | return in, nil 207 | } 208 | 209 | func (b *Box) AddNode(tag string, info *panel.NodeInfo, config *conf.Options) error { 210 | err := updateDNSConfig(info) 211 | if err != nil { 212 | return fmt.Errorf("build dns error: %s", err) 213 | } 214 | c, err := getInboundOptions(tag, info, config) 215 | if err != nil { 216 | return err 217 | } 218 | in, err := inbound.New( 219 | context.Background(), 220 | b.router, 221 | b.logFactory.NewLogger(F.ToString("inbound/", c.Type, "[", tag, "]")), 222 | c, 223 | nil, 224 | ) 225 | if err != nil { 226 | return fmt.Errorf("init inbound error: %s", err) 227 | } 228 | err = in.Start() 229 | if err != nil { 230 | return fmt.Errorf("start inbound error: %s", err) 231 | } 232 | b.inbounds[tag] = in 233 | err = b.router.AddInbound(in) 234 | if err != nil { 235 | return fmt.Errorf("add inbound error: %s", err) 236 | } 237 | return nil 238 | } 239 | 240 | func (b *Box) DelNode(tag string) error { 241 | err := b.inbounds[tag].Close() 242 | if err != nil { 243 | return fmt.Errorf("close inbound error: %s", err) 244 | } 245 | err = b.router.DelInbound(tag) 246 | if err != nil { 247 | return fmt.Errorf("delete inbound error: %s", err) 248 | } 249 | return nil 250 | } 251 | -------------------------------------------------------------------------------- /core/sing/sing.go: -------------------------------------------------------------------------------- 1 | package sing 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/InazumaV/V2bX/conf" 7 | vCore "github.com/InazumaV/V2bX/core" 8 | "github.com/goccy/go-json" 9 | "github.com/inazumav/sing-box/adapter" 10 | "github.com/inazumav/sing-box/inbound" 11 | "github.com/inazumav/sing-box/log" 12 | "github.com/inazumav/sing-box/option" 13 | "github.com/inazumav/sing-box/outbound" 14 | "github.com/inazumav/sing-box/route" 15 | "github.com/sagernet/sing/common" 16 | E "github.com/sagernet/sing/common/exceptions" 17 | F "github.com/sagernet/sing/common/format" 18 | "github.com/sagernet/sing/service" 19 | "github.com/sagernet/sing/service/pause" 20 | "io" 21 | "os" 22 | "runtime/debug" 23 | "time" 24 | ) 25 | 26 | var _ adapter.Service = (*Box)(nil) 27 | 28 | type DNSConfig struct { 29 | Servers []map[string]interface{} `json:"servers"` 30 | Rules []map[string]interface{} `json:"rules"` 31 | } 32 | 33 | type Box struct { 34 | createdAt time.Time 35 | router adapter.Router 36 | inbounds map[string]adapter.Inbound 37 | outbounds []adapter.Outbound 38 | logFactory log.Factory 39 | logger log.ContextLogger 40 | hookServer *HookServer 41 | done chan struct{} 42 | } 43 | 44 | func init() { 45 | vCore.RegisterCore("sing", New) 46 | } 47 | 48 | func New(c *conf.CoreConfig) (vCore.Core, error) { 49 | options := option.Options{} 50 | if len(c.SingConfig.OriginalPath) != 0 { 51 | f, err := os.Open(c.SingConfig.OriginalPath) 52 | if err != nil { 53 | return nil, fmt.Errorf("open original config error: %s", err) 54 | } 55 | defer f.Close() 56 | err = json.NewDecoder(f).Decode(&options) 57 | if err != nil { 58 | return nil, fmt.Errorf("decode original config error: %s", err) 59 | } 60 | } 61 | options.Log = &option.LogOptions{ 62 | Disabled: c.SingConfig.LogConfig.Disabled, 63 | Level: c.SingConfig.LogConfig.Level, 64 | Timestamp: c.SingConfig.LogConfig.Timestamp, 65 | Output: c.SingConfig.LogConfig.Output, 66 | } 67 | options.NTP = &option.NTPOptions{ 68 | Enabled: c.SingConfig.NtpConfig.Enable, 69 | WriteToSystem: true, 70 | ServerOptions: option.ServerOptions{ 71 | Server: c.SingConfig.NtpConfig.Server, 72 | ServerPort: c.SingConfig.NtpConfig.ServerPort, 73 | }, 74 | } 75 | os.Setenv("SING_DNS_PATH", "") 76 | if c.SingConfig.DnsConfigPath != "" { 77 | if f, err := os.Open(c.SingConfig.DnsConfigPath); err != nil { 78 | log.Error("Failed to read DNS config file") 79 | } else { 80 | if err = json.NewDecoder(f).Decode(&option.DNSOptions{}); err != nil { 81 | log.Error("Failed to unmarshal DNS config") 82 | } 83 | } 84 | os.Setenv("SING_DNS_PATH", c.SingConfig.DnsConfigPath) 85 | } 86 | ctx := context.Background() 87 | ctx = service.ContextWithDefaultRegistry(ctx) 88 | ctx = pause.ContextWithDefaultManager(ctx) 89 | createdAt := time.Now() 90 | experimentalOptions := common.PtrValueOrDefault(options.Experimental) 91 | applyDebugOptions(common.PtrValueOrDefault(experimentalOptions.Debug)) 92 | var defaultLogWriter io.Writer 93 | logFactory, err := log.New(log.Options{ 94 | Context: ctx, 95 | Options: common.PtrValueOrDefault(options.Log), 96 | DefaultWriter: defaultLogWriter, 97 | BaseTime: createdAt, 98 | }) 99 | if err != nil { 100 | return nil, E.Cause(err, "create log factory") 101 | } 102 | router, err := route.NewRouter( 103 | ctx, 104 | logFactory, 105 | common.PtrValueOrDefault(options.Route), 106 | common.PtrValueOrDefault(options.DNS), 107 | common.PtrValueOrDefault(options.NTP), 108 | options.Inbounds, 109 | nil, 110 | ) 111 | if err != nil { 112 | return nil, E.Cause(err, "parse route options") 113 | } 114 | inbounds := make([]adapter.Inbound, len(options.Inbounds)) 115 | inMap := make(map[string]adapter.Inbound, len(inbounds)) 116 | outbounds := make([]adapter.Outbound, 0, len(options.Outbounds)) 117 | for i, inboundOptions := range options.Inbounds { 118 | var in adapter.Inbound 119 | var tag string 120 | if inboundOptions.Tag != "" { 121 | tag = inboundOptions.Tag 122 | } else { 123 | tag = F.ToString(i) 124 | } 125 | in, err = inbound.New( 126 | ctx, 127 | router, 128 | logFactory.NewLogger(F.ToString("inbound/", inboundOptions.Type, "[", tag, "]")), 129 | inboundOptions, 130 | nil, 131 | ) 132 | if err != nil { 133 | return nil, E.Cause(err, "parse inbound[", i, "]") 134 | } 135 | inbounds[i] = in 136 | inMap[inboundOptions.Tag] = in 137 | } 138 | for i, outboundOptions := range options.Outbounds { 139 | var out adapter.Outbound 140 | var tag string 141 | if outboundOptions.Tag != "" { 142 | tag = outboundOptions.Tag 143 | } else { 144 | tag = F.ToString(i) 145 | } 146 | out, err = outbound.New( 147 | ctx, 148 | router, 149 | logFactory.NewLogger(F.ToString("outbound/", outboundOptions.Type, "[", tag, "]")), 150 | tag, 151 | outboundOptions) 152 | if err != nil { 153 | return nil, E.Cause(err, "parse outbound[", i, "]") 154 | } 155 | outbounds = append(outbounds, out) 156 | } 157 | err = router.Initialize(inbounds, outbounds, func() adapter.Outbound { 158 | out, oErr := outbound.New(ctx, router, logFactory.NewLogger("outbound/direct"), "direct", option.Outbound{Type: "direct", Tag: "default"}) 159 | common.Must(oErr) 160 | outbounds = append(outbounds, out) 161 | return out 162 | }) 163 | if err != nil { 164 | return nil, err 165 | } 166 | server := NewHookServer(logFactory.NewLogger("Hook-Server")) 167 | if err != nil { 168 | return nil, E.Cause(err, "create v2ray api server") 169 | } 170 | router.SetClashServer(server) 171 | return &Box{ 172 | router: router, 173 | inbounds: inMap, 174 | outbounds: outbounds, 175 | createdAt: createdAt, 176 | logFactory: logFactory, 177 | logger: logFactory.Logger(), 178 | hookServer: server, 179 | done: make(chan struct{}), 180 | }, nil 181 | } 182 | 183 | func (b *Box) PreStart() error { 184 | err := b.preStart() 185 | if err != nil { 186 | // TODO: remove catch error 187 | defer func() { 188 | v := recover() 189 | if v != nil { 190 | log.Error(E.Cause(err, "origin error")) 191 | debug.PrintStack() 192 | panic("panic on early close: " + fmt.Sprint(v)) 193 | } 194 | }() 195 | b.Close() 196 | return err 197 | } 198 | b.logger.Info("sing-box pre-started (", F.Seconds(time.Since(b.createdAt).Seconds()), "s)") 199 | return nil 200 | } 201 | 202 | func (b *Box) Start() error { 203 | err := b.start() 204 | if err != nil { 205 | // TODO: remove catch error 206 | defer func() { 207 | v := recover() 208 | if v != nil { 209 | log.Error(E.Cause(err, "origin error")) 210 | debug.PrintStack() 211 | panic("panic on early close: " + fmt.Sprint(v)) 212 | } 213 | }() 214 | b.Close() 215 | return err 216 | } 217 | b.logger.Info("sing-box started (", F.Seconds(time.Since(b.createdAt).Seconds()), "s)") 218 | return nil 219 | } 220 | 221 | func (b *Box) preStart() error { 222 | err := b.startOutbounds() 223 | if err != nil { 224 | return err 225 | } 226 | return b.router.Start() 227 | } 228 | 229 | func (b *Box) start() error { 230 | err := b.preStart() 231 | if err != nil { 232 | return err 233 | } 234 | for i, in := range b.inbounds { 235 | var tag string 236 | if in.Tag() == "" { 237 | tag = F.ToString(i) 238 | } else { 239 | tag = in.Tag() 240 | } 241 | b.logger.Trace("initializing inbound/", in.Type(), "[", tag, "]") 242 | err = in.Start() 243 | if err != nil { 244 | return E.Cause(err, "initialize inbound/", in.Type(), "[", tag, "]") 245 | } 246 | } 247 | return nil 248 | } 249 | 250 | func (b *Box) postStart() error { 251 | for serviceName, service := range b.outbounds { 252 | if lateService, isLateService := service.(adapter.PostStarter); isLateService { 253 | b.logger.Trace("post-starting ", service) 254 | err := lateService.PostStart() 255 | if err != nil { 256 | return E.Cause(err, "post-start ", serviceName) 257 | } 258 | } 259 | } 260 | return nil 261 | } 262 | 263 | func (b *Box) Close() error { 264 | select { 265 | case <-b.done: 266 | return os.ErrClosed 267 | default: 268 | close(b.done) 269 | } 270 | var errors error 271 | for i, in := range b.inbounds { 272 | b.logger.Trace("closing inbound/", in.Type(), "[", i, "]") 273 | errors = E.Append(errors, in.Close(), func(err error) error { 274 | return E.Cause(err, "close inbound/", in.Type(), "[", i, "]") 275 | }) 276 | } 277 | for i, out := range b.outbounds { 278 | b.logger.Trace("closing outbound/", out.Type(), "[", i, "]") 279 | errors = E.Append(errors, common.Close(out), func(err error) error { 280 | return E.Cause(err, "close outbound/", out.Type(), "[", i, "]") 281 | }) 282 | } 283 | b.logger.Trace("closing router") 284 | if err := common.Close(b.router); err != nil { 285 | errors = E.Append(errors, err, func(err error) error { 286 | return E.Cause(err, "close router") 287 | }) 288 | } 289 | b.logger.Trace("closing log factory") 290 | if err := common.Close(b.logFactory); err != nil { 291 | errors = E.Append(errors, err, func(err error) error { 292 | return E.Cause(err, "close log factory") 293 | }) 294 | } 295 | return errors 296 | } 297 | 298 | func (b *Box) Router() adapter.Router { 299 | return b.router 300 | } 301 | 302 | func (b *Box) Protocols() []string { 303 | return []string{ 304 | "vmess", 305 | "vless", 306 | "shadowsocks", 307 | "trojan", 308 | "hysteria", 309 | } 310 | } 311 | 312 | func (b *Box) Type() string { 313 | return "sing" 314 | } 315 | -------------------------------------------------------------------------------- /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/inazumav/sing-box/inbound" 11 | "github.com/inazumav/sing-box/option" 12 | ) 13 | 14 | func (b *Box) AddUsers(p *core.AddUsersParams) (added int, err error) { 15 | switch p.NodeInfo.Type { 16 | case "vmess", "vless": 17 | if p.NodeInfo.Type == "vless" { 18 | us := make([]option.VLESSUser, len(p.Users)) 19 | for i := range p.Users { 20 | us[i] = option.VLESSUser{ 21 | Name: p.Users[i].Uuid, 22 | Flow: p.VAllss.Flow, 23 | UUID: p.Users[i].Uuid, 24 | } 25 | } 26 | err = b.inbounds[p.Tag].(*inbound.VLESS).AddUsers(us) 27 | } else { 28 | us := make([]option.VMessUser, len(p.Users)) 29 | for i := range p.Users { 30 | us[i] = option.VMessUser{ 31 | Name: p.Users[i].Uuid, 32 | UUID: p.Users[i].Uuid, 33 | } 34 | } 35 | err = b.inbounds[p.Tag].(*inbound.VMess).AddUsers(us) 36 | } 37 | case "shadowsocks": 38 | us := make([]option.ShadowsocksUser, len(p.Users)) 39 | for i := range p.Users { 40 | var password = p.Users[i].Uuid 41 | switch p.Shadowsocks.Cipher { 42 | case "2022-blake3-aes-128-gcm": 43 | password = base64.StdEncoding.EncodeToString([]byte(password[:16])) 44 | case "2022-blake3-aes-256-gcm": 45 | password = base64.StdEncoding.EncodeToString([]byte(password[:32])) 46 | } 47 | us[i] = option.ShadowsocksUser{ 48 | Name: p.Users[i].Uuid, 49 | Password: password, 50 | } 51 | } 52 | err = b.inbounds[p.Tag].(*inbound.ShadowsocksMulti).AddUsers(us) 53 | case "trojan": 54 | us := make([]option.TrojanUser, len(p.Users)) 55 | for i := range p.Users { 56 | us[i] = option.TrojanUser{ 57 | Name: p.Users[i].Uuid, 58 | Password: p.Users[i].Uuid, 59 | } 60 | } 61 | err = b.inbounds[p.Tag].(*inbound.Trojan).AddUsers(us) 62 | case "hysteria": 63 | us := make([]option.HysteriaUser, len(p.Users)) 64 | for i := range p.Users { 65 | us[i] = option.HysteriaUser{ 66 | Name: p.Users[i].Uuid, 67 | AuthString: p.Users[i].Uuid, 68 | } 69 | } 70 | err = b.inbounds[p.Tag].(*inbound.Hysteria).AddUsers(us) 71 | } 72 | if err != nil { 73 | return 0, err 74 | } 75 | return len(p.Users), err 76 | } 77 | 78 | func (b *Box) GetUserTraffic(tag, uuid string, reset bool) (up int64, down int64) { 79 | if v, ok := b.hookServer.counter.Load(tag); ok { 80 | c := v.(*counter.TrafficCounter) 81 | up = c.GetUpCount(uuid) 82 | down = c.GetDownCount(uuid) 83 | if reset { 84 | c.Reset(uuid) 85 | } 86 | return 87 | } 88 | return 0, 0 89 | } 90 | 91 | type UserDeleter interface { 92 | DelUsers(uuid []string) error 93 | } 94 | 95 | func (b *Box) DelUsers(users []panel.UserInfo, tag string) error { 96 | var del UserDeleter 97 | if i, ok := b.inbounds[tag]; ok { 98 | switch i.Type() { 99 | case "vmess": 100 | del = i.(*inbound.VMess) 101 | case "vless": 102 | del = i.(*inbound.VLESS) 103 | case "shadowsocks": 104 | del = i.(*inbound.ShadowsocksMulti) 105 | case "trojan": 106 | del = i.(*inbound.Trojan) 107 | case "hysteria": 108 | del = i.(*inbound.Hysteria) 109 | } 110 | } else { 111 | return errors.New("the inbound not found") 112 | } 113 | uuids := make([]string, len(users)) 114 | for i := range users { 115 | uuids[i] = users[i].Uuid 116 | } 117 | err := del.DelUsers(uuids) 118 | if err != nil { 119 | return err 120 | } 121 | return nil 122 | } 123 | -------------------------------------------------------------------------------- /core/sing/utils.go: -------------------------------------------------------------------------------- 1 | package sing 2 | 3 | import ( 4 | "fmt" 5 | "github.com/InazumaV/V2bX/conf" 6 | "github.com/inazumav/sing-box/option" 7 | "strconv" 8 | ) 9 | 10 | func processFallback(c *conf.Options, fallbackForALPN map[string]*option.ServerOptions) error { 11 | for k, v := range c.SingOptions.FallBackConfigs.FallBackForALPN { 12 | fallbackPort, err := strconv.Atoi(v.ServerPort) 13 | if err != nil { 14 | return fmt.Errorf("unable to parse fallbackForALPN server port error: %s", err) 15 | } 16 | fallbackForALPN[k] = &option.ServerOptions{Server: v.Server, ServerPort: uint16(fallbackPort)} 17 | } 18 | return nil 19 | } 20 | -------------------------------------------------------------------------------- /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.31.0 4 | // protoc v4.23.4 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 | if protoimpl.UnsafeEnabled { 32 | mi := &file_config_proto_msgTypes[0] 33 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 34 | ms.StoreMessageInfo(mi) 35 | } 36 | } 37 | 38 | func (x *SessionConfig) String() string { 39 | return protoimpl.X.MessageStringOf(x) 40 | } 41 | 42 | func (*SessionConfig) ProtoMessage() {} 43 | 44 | func (x *SessionConfig) ProtoReflect() protoreflect.Message { 45 | mi := &file_config_proto_msgTypes[0] 46 | if protoimpl.UnsafeEnabled && x != nil { 47 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 48 | if ms.LoadMessageInfo() == nil { 49 | ms.StoreMessageInfo(mi) 50 | } 51 | return ms 52 | } 53 | return mi.MessageOf(x) 54 | } 55 | 56 | // Deprecated: Use SessionConfig.ProtoReflect.Descriptor instead. 57 | func (*SessionConfig) Descriptor() ([]byte, []int) { 58 | return file_config_proto_rawDescGZIP(), []int{0} 59 | } 60 | 61 | type Config struct { 62 | state protoimpl.MessageState 63 | sizeCache protoimpl.SizeCache 64 | unknownFields protoimpl.UnknownFields 65 | 66 | Settings *SessionConfig `protobuf:"bytes,1,opt,name=settings,proto3" json:"settings,omitempty"` 67 | } 68 | 69 | func (x *Config) Reset() { 70 | *x = Config{} 71 | if protoimpl.UnsafeEnabled { 72 | mi := &file_config_proto_msgTypes[1] 73 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 74 | ms.StoreMessageInfo(mi) 75 | } 76 | } 77 | 78 | func (x *Config) String() string { 79 | return protoimpl.X.MessageStringOf(x) 80 | } 81 | 82 | func (*Config) ProtoMessage() {} 83 | 84 | func (x *Config) ProtoReflect() protoreflect.Message { 85 | mi := &file_config_proto_msgTypes[1] 86 | if protoimpl.UnsafeEnabled && x != nil { 87 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 88 | if ms.LoadMessageInfo() == nil { 89 | ms.StoreMessageInfo(mi) 90 | } 91 | return ms 92 | } 93 | return mi.MessageOf(x) 94 | } 95 | 96 | // Deprecated: Use Config.ProtoReflect.Descriptor instead. 97 | func (*Config) Descriptor() ([]byte, []int) { 98 | return file_config_proto_rawDescGZIP(), []int{1} 99 | } 100 | 101 | func (x *Config) GetSettings() *SessionConfig { 102 | if x != nil { 103 | return x.Settings 104 | } 105 | return nil 106 | } 107 | 108 | var File_config_proto protoreflect.FileDescriptor 109 | 110 | var file_config_proto_rawDesc = []byte{ 111 | 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x18, 112 | 0x76, 0x32, 0x62, 0x78, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x64, 0x69, 113 | 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x65, 0x72, 0x22, 0x15, 0x0a, 0x0d, 0x53, 0x65, 0x73, 0x73, 114 | 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x22, 115 | 0x4d, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x43, 0x0a, 0x08, 0x73, 0x65, 0x74, 116 | 0x74, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x76, 0x32, 117 | 0x62, 0x78, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x64, 0x69, 0x73, 0x70, 118 | 0x61, 0x74, 0x63, 0x68, 0x65, 0x72, 0x2e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 119 | 0x6e, 0x66, 0x69, 0x67, 0x52, 0x08, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x42, 0x6e, 120 | 0x0a, 0x1c, 0x63, 0x6f, 0x6d, 0x2e, 0x76, 0x32, 0x62, 0x78, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 121 | 0x61, 0x70, 0x70, 0x2e, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x65, 0x72, 0x50, 0x01, 122 | 0x5a, 0x31, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x49, 0x6e, 0x61, 123 | 0x7a, 0x75, 0x6d, 0x61, 0x56, 0x2f, 0x56, 0x32, 0x62, 0x58, 0x2f, 0x63, 0x6f, 0x72, 0x65, 0x2f, 124 | 0x78, 0x72, 0x61, 0x79, 0x2f, 0x61, 0x70, 0x70, 0x2f, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 125 | 0x68, 0x65, 0x72, 0xaa, 0x02, 0x18, 0x56, 0x32, 0x62, 0x58, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 126 | 0x61, 0x70, 0x70, 0x2e, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x65, 0x72, 0x62, 0x06, 127 | 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 128 | } 129 | 130 | var ( 131 | file_config_proto_rawDescOnce sync.Once 132 | file_config_proto_rawDescData = file_config_proto_rawDesc 133 | ) 134 | 135 | func file_config_proto_rawDescGZIP() []byte { 136 | file_config_proto_rawDescOnce.Do(func() { 137 | file_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_config_proto_rawDescData) 138 | }) 139 | return file_config_proto_rawDescData 140 | } 141 | 142 | var file_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2) 143 | var file_config_proto_goTypes = []interface{}{ 144 | (*SessionConfig)(nil), // 0: v2bx.core.app.dispatcher.SessionConfig 145 | (*Config)(nil), // 1: v2bx.core.app.dispatcher.Config 146 | } 147 | var file_config_proto_depIdxs = []int32{ 148 | 0, // 0: v2bx.core.app.dispatcher.Config.settings:type_name -> v2bx.core.app.dispatcher.SessionConfig 149 | 1, // [1:1] is the sub-list for method output_type 150 | 1, // [1:1] is the sub-list for method input_type 151 | 1, // [1:1] is the sub-list for extension type_name 152 | 1, // [1:1] is the sub-list for extension extendee 153 | 0, // [0:1] is the sub-list for field type_name 154 | } 155 | 156 | func init() { file_config_proto_init() } 157 | func file_config_proto_init() { 158 | if File_config_proto != nil { 159 | return 160 | } 161 | if !protoimpl.UnsafeEnabled { 162 | file_config_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 163 | switch v := v.(*SessionConfig); i { 164 | case 0: 165 | return &v.state 166 | case 1: 167 | return &v.sizeCache 168 | case 2: 169 | return &v.unknownFields 170 | default: 171 | return nil 172 | } 173 | } 174 | file_config_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { 175 | switch v := v.(*Config); i { 176 | case 0: 177 | return &v.state 178 | case 1: 179 | return &v.sizeCache 180 | case 2: 181 | return &v.unknownFields 182 | default: 183 | return nil 184 | } 185 | } 186 | } 187 | type x struct{} 188 | out := protoimpl.TypeBuilder{ 189 | File: protoimpl.DescBuilder{ 190 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 191 | RawDescriptor: file_config_proto_rawDesc, 192 | NumEnums: 0, 193 | NumMessages: 2, 194 | NumExtensions: 0, 195 | NumServices: 0, 196 | }, 197 | GoTypes: file_config_proto_goTypes, 198 | DependencyIndexes: file_config_proto_depIdxs, 199 | MessageInfos: file_config_proto_msgTypes, 200 | }.Build() 201 | File_config_proto = out.File 202 | file_config_proto_rawDesc = nil 203 | file_config_proto_goTypes = nil 204 | file_config_proto_depIdxs = nil 205 | } 206 | -------------------------------------------------------------------------------- /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/errors.generated.go: -------------------------------------------------------------------------------- 1 | package dispatcher 2 | 3 | import "github.com/xtls/xray-core/common/errors" 4 | 5 | type errPathObjHolder struct{} 6 | 7 | func newError(values ...interface{}) *errors.Error { 8 | return errors.New(values...).WithPathObj(errPathObjHolder{}) 9 | } 10 | -------------------------------------------------------------------------------- /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/net" 9 | "github.com/xtls/xray-core/common/session" 10 | "github.com/xtls/xray-core/core" 11 | "github.com/xtls/xray-core/features/dns" 12 | ) 13 | 14 | // newFakeDNSSniffer Creates a Fake DNS metadata sniffer 15 | func newFakeDNSSniffer(ctx context.Context) (protocolSnifferWithMetadata, error) { 16 | var fakeDNSEngine dns.FakeDNSEngine 17 | { 18 | fakeDNSEngineFeat := core.MustFromContext(ctx).GetFeature((*dns.FakeDNSEngine)(nil)) 19 | if fakeDNSEngineFeat != nil { 20 | fakeDNSEngine = fakeDNSEngineFeat.(dns.FakeDNSEngine) 21 | } 22 | } 23 | 24 | if fakeDNSEngine == nil { 25 | errNotInit := newError("FakeDNSEngine is not initialized, but such a sniffer is used").AtError() 26 | return protocolSnifferWithMetadata{}, errNotInit 27 | } 28 | return protocolSnifferWithMetadata{protocolSniffer: func(ctx context.Context, bytes []byte) (SniffResult, error) { 29 | Target := session.OutboundFromContext(ctx).Target 30 | if Target.Network == net.Network_TCP || Target.Network == net.Network_UDP { 31 | domainFromFakeDNS := fakeDNSEngine.GetDomainFromFakeDNS(Target.Address) 32 | if domainFromFakeDNS != "" { 33 | newError("fake dns got domain: ", domainFromFakeDNS, " for ip: ", Target.Address.String()).WriteToLog(session.ExportIDToError(ctx)) 34 | return &fakeDNSSniffResult{domainName: domainFromFakeDNS}, nil 35 | } 36 | } 37 | 38 | if ipAddressInRangeValueI := ctx.Value(ipAddressInRange); ipAddressInRangeValueI != nil { 39 | ipAddressInRangeValue := ipAddressInRangeValueI.(*ipAddressInRangeOpt) 40 | if fkr0, ok := fakeDNSEngine.(dns.FakeDNSEngineRev0); ok { 41 | inPool := fkr0.IsIPInIPPool(Target.Address) 42 | ipAddressInRangeValue.addressInRange = &inPool 43 | } 44 | } 45 | 46 | return nil, common.ErrNoClue 47 | }, metadataSniffer: true}, nil 48 | } 49 | 50 | type fakeDNSSniffResult struct { 51 | domainName string 52 | } 53 | 54 | func (fakeDNSSniffResult) Protocol() string { 55 | return "fakedns" 56 | } 57 | 58 | func (f fakeDNSSniffResult) Domain() string { 59 | return f.domainName 60 | } 61 | 62 | type fakeDNSExtraOpts int 63 | 64 | const ipAddressInRange fakeDNSExtraOpts = 1 65 | 66 | type ipAddressInRangeOpt struct { 67 | addressInRange *bool 68 | } 69 | 70 | type DNSThenOthersSniffResult struct { 71 | domainName string 72 | protocolOriginalName string 73 | } 74 | 75 | func (f DNSThenOthersSniffResult) IsProtoSubsetOf(protocolName string) bool { 76 | return strings.HasPrefix(protocolName, f.protocolOriginalName) 77 | } 78 | 79 | func (DNSThenOthersSniffResult) Protocol() string { 80 | return "fakedns+others" 81 | } 82 | 83 | func (f DNSThenOthersSniffResult) Domain() string { 84 | return f.domainName 85 | } 86 | 87 | func newFakeDNSThenOthers(ctx context.Context, fakeDNSSniffer protocolSnifferWithMetadata, others []protocolSnifferWithMetadata) ( 88 | protocolSnifferWithMetadata, error, 89 | ) { // nolint: unparam 90 | // ctx may be used in the future 91 | _ = ctx 92 | return protocolSnifferWithMetadata{ 93 | protocolSniffer: func(ctx context.Context, bytes []byte) (SniffResult, error) { 94 | ipAddressInRangeValue := &ipAddressInRangeOpt{} 95 | ctx = context.WithValue(ctx, ipAddressInRange, ipAddressInRangeValue) 96 | result, err := fakeDNSSniffer.protocolSniffer(ctx, bytes) 97 | if err == nil { 98 | return result, nil 99 | } 100 | if ipAddressInRangeValue.addressInRange != nil { 101 | if *ipAddressInRangeValue.addressInRange { 102 | for _, v := range others { 103 | if v.metadataSniffer || bytes != nil { 104 | if result, err := v.protocolSniffer(ctx, bytes); err == nil { 105 | return DNSThenOthersSniffResult{domainName: result.Domain(), protocolOriginalName: result.Protocol()}, nil 106 | } 107 | } 108 | } 109 | return nil, common.ErrNoClue 110 | } 111 | newError("ip address not in fake dns range, return as is").AtDebug().WriteToLog() 112 | return nil, common.ErrNoClue 113 | } 114 | newError("fake dns sniffer did not set address in range option, assume false.").AtWarning().WriteToLog() 115 | return nil, common.ErrNoClue 116 | }, 117 | metadataSniffer: false, 118 | }, nil 119 | } 120 | -------------------------------------------------------------------------------- /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/net" 8 | "github.com/xtls/xray-core/common/protocol/bittorrent" 9 | "github.com/xtls/xray-core/common/protocol/http" 10 | "github.com/xtls/xray-core/common/protocol/quic" 11 | "github.com/xtls/xray-core/common/protocol/tls" 12 | ) 13 | 14 | type SniffResult interface { 15 | Protocol() string 16 | Domain() string 17 | } 18 | 19 | type protocolSniffer func(context.Context, []byte) (SniffResult, error) 20 | 21 | type protocolSnifferWithMetadata struct { 22 | protocolSniffer protocolSniffer 23 | // A Metadata sniffer will be invoked on connection establishment only, with nil body, 24 | // for both TCP and UDP connections 25 | // It will not be shown as a traffic type for routing unless there is no other successful sniffing. 26 | metadataSniffer bool 27 | network net.Network 28 | } 29 | 30 | type Sniffer struct { 31 | sniffer []protocolSnifferWithMetadata 32 | } 33 | 34 | func NewSniffer(ctx context.Context) *Sniffer { 35 | ret := &Sniffer{ 36 | sniffer: []protocolSnifferWithMetadata{ 37 | {func(c context.Context, b []byte) (SniffResult, error) { return http.SniffHTTP(b) }, false, net.Network_TCP}, 38 | {func(c context.Context, b []byte) (SniffResult, error) { return tls.SniffTLS(b) }, false, net.Network_TCP}, 39 | {func(c context.Context, b []byte) (SniffResult, error) { return bittorrent.SniffBittorrent(b) }, false, net.Network_TCP}, 40 | {func(c context.Context, b []byte) (SniffResult, error) { return quic.SniffQUIC(b) }, false, net.Network_UDP}, 41 | {func(c context.Context, b []byte) (SniffResult, error) { return bittorrent.SniffUTP(b) }, false, net.Network_UDP}, 42 | }, 43 | } 44 | if sniffer, err := newFakeDNSSniffer(ctx); err == nil { 45 | others := ret.sniffer 46 | ret.sniffer = append(ret.sniffer, sniffer) 47 | fakeDNSThenOthers, err := newFakeDNSThenOthers(ctx, sniffer, others) 48 | if err == nil { 49 | ret.sniffer = append([]protocolSnifferWithMetadata{fakeDNSThenOthers}, ret.sniffer...) 50 | } 51 | } 52 | return ret 53 | } 54 | 55 | var errUnknownContent = newError("unknown content") 56 | 57 | func (s *Sniffer) Sniff(c context.Context, payload []byte, network net.Network) (SniffResult, error) { 58 | var pendingSniffer []protocolSnifferWithMetadata 59 | for _, si := range s.sniffer { 60 | s := si.protocolSniffer 61 | if si.metadataSniffer || si.network != network { 62 | continue 63 | } 64 | result, err := s(c, payload) 65 | if err == common.ErrNoClue { 66 | pendingSniffer = append(pendingSniffer, si) 67 | continue 68 | } 69 | 70 | if err == nil && result != nil { 71 | return result, nil 72 | } 73 | } 74 | 75 | if len(pendingSniffer) > 0 { 76 | s.sniffer = pendingSniffer 77 | return nil, common.ErrNoClue 78 | } 79 | 80 | return nil, errUnknownContent 81 | } 82 | 83 | func (s *Sniffer) SniffMetadata(c context.Context) (SniffResult, error) { 84 | var pendingSniffer []protocolSnifferWithMetadata 85 | for _, si := range s.sniffer { 86 | s := si.protocolSniffer 87 | if !si.metadataSniffer { 88 | pendingSniffer = append(pendingSniffer, si) 89 | continue 90 | } 91 | result, err := s(c, nil) 92 | if err == common.ErrNoClue { 93 | pendingSniffer = append(pendingSniffer, si) 94 | continue 95 | } 96 | 97 | if err == nil && result != nil { 98 | return result, nil 99 | } 100 | } 101 | 102 | if len(pendingSniffer) > 0 { 103 | s.sniffer = pendingSniffer 104 | return nil, common.ErrNoClue 105 | } 106 | 107 | return nil, errUnknownContent 108 | } 109 | 110 | func CompositeResult(domainResult SniffResult, protocolResult SniffResult) SniffResult { 111 | return &compositeResult{domainResult: domainResult, protocolResult: protocolResult} 112 | } 113 | 114 | type compositeResult struct { 115 | domainResult SniffResult 116 | protocolResult SniffResult 117 | } 118 | 119 | func (c compositeResult) Protocol() string { 120 | return c.protocolResult.Protocol() 121 | } 122 | 123 | func (c compositeResult) Domain() string { 124 | return c.domainResult.Domain() 125 | } 126 | 127 | func (c compositeResult) ProtocolForDomainResult() string { 128 | return c.domainResult.Protocol() 129 | } 130 | 131 | type SnifferResultComposite interface { 132 | ProtocolForDomainResult() string 133 | } 134 | 135 | type SnifferIsProtoSubsetOf interface { 136 | IsProtoSubsetOf(protocolName string) bool 137 | } 138 | -------------------------------------------------------------------------------- /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/socks" 45 | _ "github.com/xtls/xray-core/proxy/trojan" 46 | _ "github.com/xtls/xray-core/proxy/vless/inbound" 47 | _ "github.com/xtls/xray-core/proxy/vless/outbound" 48 | _ "github.com/xtls/xray-core/proxy/vmess/inbound" 49 | _ "github.com/xtls/xray-core/proxy/vmess/outbound" 50 | 51 | //_ "github.com/xtls/xray-core/proxy/wireguard" 52 | 53 | // Transports 54 | //_ "github.com/xtls/xray-core/transport/internet/domainsocket" 55 | _ "github.com/xtls/xray-core/transport/internet/grpc" 56 | _ "github.com/xtls/xray-core/transport/internet/http" 57 | 58 | //_ "github.com/xtls/xray-core/transport/internet/kcp" 59 | //_ "github.com/xtls/xray-core/transport/internet/quic" 60 | _ "github.com/xtls/xray-core/transport/internet/reality" 61 | _ "github.com/xtls/xray-core/transport/internet/tcp" 62 | _ "github.com/xtls/xray-core/transport/internet/tls" 63 | _ "github.com/xtls/xray-core/transport/internet/udp" 64 | _ "github.com/xtls/xray-core/transport/internet/websocket" 65 | 66 | // Transport headers 67 | _ "github.com/xtls/xray-core/transport/internet/headers/http" 68 | _ "github.com/xtls/xray-core/transport/internet/headers/noop" 69 | _ "github.com/xtls/xray-core/transport/internet/headers/srtp" 70 | _ "github.com/xtls/xray-core/transport/internet/headers/tls" 71 | _ "github.com/xtls/xray-core/transport/internet/headers/utp" 72 | _ "github.com/xtls/xray-core/transport/internet/headers/wechat" 73 | //_ "github.com/xtls/xray-core/transport/internet/headers/wireguard" 74 | ) 75 | -------------------------------------------------------------------------------- /core/xray/dns.go: -------------------------------------------------------------------------------- 1 | package xray 2 | 3 | import ( 4 | "bytes" 5 | "github.com/InazumaV/V2bX/api/panel" 6 | "github.com/goccy/go-json" 7 | log "github.com/sirupsen/logrus" 8 | coreConf "github.com/xtls/xray-core/infra/conf" 9 | "os" 10 | ) 11 | 12 | func updateDNSConfig(node *panel.NodeInfo) (err error) { 13 | dnsPath := os.Getenv("XRAY_DNS_PATH") 14 | if len(node.RawDNS.DNSJson) != 0 { 15 | err = saveDnsConfig(node.RawDNS.DNSJson, dnsPath) 16 | } else if len(node.RawDNS.DNSMap) != 0 { 17 | dnsConfig := DNSConfig{ 18 | Servers: []interface{}{ 19 | "1.1.1.1", 20 | "localhost"}, 21 | Tag: "dns_inbound", 22 | } 23 | for _, value := range node.RawDNS.DNSMap { 24 | dnsConfig.Servers = append(dnsConfig.Servers, value) 25 | } 26 | dnsConfigJSON, err := json.MarshalIndent(dnsConfig, "", " ") 27 | if err != nil { 28 | log.WithField("err", err).Error("Error marshaling dnsConfig to JSON") 29 | return err 30 | } 31 | err = saveDnsConfig(dnsConfigJSON, dnsPath) 32 | } 33 | return err 34 | } 35 | 36 | func saveDnsConfig(dns []byte, dnsPath string) (err error) { 37 | currentData, err := os.ReadFile(dnsPath) 38 | if err != nil { 39 | log.WithField("err", err).Error("Failed to read XRAY_DNS_PATH") 40 | return err 41 | } 42 | if !bytes.Equal(currentData, dns) { 43 | coreDnsConfig := &coreConf.DNSConfig{} 44 | if err = json.NewDecoder(bytes.NewReader(dns)).Decode(coreDnsConfig); err != nil { 45 | log.WithField("err", err).Error("Failed to unmarshal DNS config") 46 | } 47 | _, err := coreDnsConfig.Build() 48 | if err != nil { 49 | log.WithField("err", err).Error("Failed to understand DNS config, Please check: https://xtls.github.io/config/dns.html for help") 50 | return err 51 | } 52 | if err = os.Truncate(dnsPath, 0); err != nil { 53 | log.WithField("err", err).Error("Failed to clear XRAY DNS PATH file") 54 | } 55 | if err = os.WriteFile(dnsPath, dns, 0644); err != nil { 56 | log.WithField("err", err).Error("Failed to write DNS to XRAY DNS PATH file") 57 | } 58 | } 59 | return err 60 | } 61 | -------------------------------------------------------------------------------- /core/xray/node.go: -------------------------------------------------------------------------------- 1 | package xray 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/InazumaV/V2bX/api/panel" 7 | "github.com/InazumaV/V2bX/conf" 8 | "github.com/xtls/xray-core/core" 9 | "github.com/xtls/xray-core/features/inbound" 10 | "github.com/xtls/xray-core/features/outbound" 11 | ) 12 | 13 | type DNSConfig struct { 14 | Servers []interface{} `json:"servers"` 15 | Tag string `json:"tag"` 16 | } 17 | 18 | func (c *Core) AddNode(tag string, info *panel.NodeInfo, config *conf.Options) error { 19 | err := updateDNSConfig(info) 20 | if err != nil { 21 | return fmt.Errorf("build dns error: %s", err) 22 | } 23 | inboundConfig, err := buildInbound(config, info, tag) 24 | if err != nil { 25 | return fmt.Errorf("build inbound error: %s", err) 26 | } 27 | err = c.addInbound(inboundConfig) 28 | if err != nil { 29 | return fmt.Errorf("add inbound error: %s", err) 30 | } 31 | outBoundConfig, err := buildOutbound(config, tag) 32 | if err != nil { 33 | return fmt.Errorf("build outbound error: %s", err) 34 | } 35 | err = c.addOutbound(outBoundConfig) 36 | if err != nil { 37 | return fmt.Errorf("add outbound error: %s", err) 38 | } 39 | return nil 40 | } 41 | 42 | func (c *Core) addInbound(config *core.InboundHandlerConfig) error { 43 | rawHandler, err := core.CreateObject(c.Server, config) 44 | if err != nil { 45 | return err 46 | } 47 | handler, ok := rawHandler.(inbound.Handler) 48 | if !ok { 49 | return fmt.Errorf("not an InboundHandler: %s", err) 50 | } 51 | if err := c.ihm.AddHandler(context.Background(), handler); err != nil { 52 | return err 53 | } 54 | return nil 55 | } 56 | 57 | func (c *Core) addOutbound(config *core.OutboundHandlerConfig) error { 58 | rawHandler, err := core.CreateObject(c.Server, config) 59 | if err != nil { 60 | return err 61 | } 62 | handler, ok := rawHandler.(outbound.Handler) 63 | if !ok { 64 | return fmt.Errorf("not an InboundHandler: %s", err) 65 | } 66 | if err := c.ohm.AddHandler(context.Background(), handler); err != nil { 67 | return err 68 | } 69 | return nil 70 | } 71 | 72 | func (c *Core) DelNode(tag string) error { 73 | err := c.removeInbound(tag) 74 | if err != nil { 75 | return fmt.Errorf("remove in error: %s", err) 76 | } 77 | err = c.removeOutbound(tag) 78 | if err != nil { 79 | return fmt.Errorf("remove out error: %s", err) 80 | } 81 | return nil 82 | } 83 | 84 | func (c *Core) removeInbound(tag string) error { 85 | return c.ihm.RemoveHandler(context.Background(), tag) 86 | } 87 | 88 | func (c *Core) removeOutbound(tag string) error { 89 | err := c.ohm.RemoveHandler(context.Background(), tag) 90 | return err 91 | } 92 | -------------------------------------------------------------------------------- /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/common/net" 9 | "github.com/xtls/xray-core/core" 10 | "github.com/xtls/xray-core/infra/conf" 11 | ) 12 | 13 | // BuildOutbound build freedom outbund config for addoutbound 14 | func buildOutbound(config *conf2.Options, tag string) (*core.OutboundHandlerConfig, error) { 15 | outboundDetourConfig := &conf.OutboundDetourConfig{} 16 | outboundDetourConfig.Protocol = "freedom" 17 | outboundDetourConfig.Tag = tag 18 | 19 | // Build Send IP address 20 | if config.SendIP != "" { 21 | ipAddress := net.ParseAddress(config.SendIP) 22 | outboundDetourConfig.SendThrough = &conf.Address{Address: ipAddress} 23 | } 24 | 25 | // Freedom Protocol setting 26 | var domainStrategy = "Asis" 27 | if config.XrayOptions.EnableDNS { 28 | if config.XrayOptions.DNSType != "" { 29 | domainStrategy = config.XrayOptions.DNSType 30 | } else { 31 | domainStrategy = "UseIP" 32 | } 33 | } 34 | proxySetting := &conf.FreedomConfig{ 35 | DomainStrategy: domainStrategy, 36 | } 37 | var setting json.RawMessage 38 | setting, err := json.Marshal(proxySetting) 39 | if err != nil { 40 | return nil, fmt.Errorf("marshal proxy config error: %s", err) 41 | } 42 | outboundDetourConfig.Settings = &setting 43 | return outboundDetourConfig.Build() 44 | } 45 | -------------------------------------------------------------------------------- /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 | } 42 | ssAccount := &shadowsocks_2022.User{ 43 | Key: base64.StdEncoding.EncodeToString([]byte(userInfo.Uuid[:keyLength])), 44 | } 45 | return &protocol.User{ 46 | Level: 0, 47 | Email: format.UserTag(tag, userInfo.Uuid), 48 | Account: serial.ToTypedMessage(ssAccount), 49 | } 50 | } 51 | } 52 | 53 | func getCipherFromString(c string) shadowsocks.CipherType { 54 | switch strings.ToLower(c) { 55 | case "aes-128-gcm", "aead_aes_128_gcm": 56 | return shadowsocks.CipherType_AES_128_GCM 57 | case "aes-256-gcm", "aead_aes_256_gcm": 58 | return shadowsocks.CipherType_AES_256_GCM 59 | case "chacha20-poly1305", "aead_chacha20_poly1305", "chacha20-ietf-poly1305": 60 | return shadowsocks.CipherType_CHACHA20_POLY1305 61 | case "none", "plain": 62 | return shadowsocks.CipherType_NONE 63 | default: 64 | return shadowsocks.CipherType_UNKNOWN 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /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 *Core) 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 *Core) DelUsers(users []panel.UserInfo, tag string) 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 *Core) 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 *Core) 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 | "os" 5 | "sync" 6 | 7 | "github.com/InazumaV/V2bX/conf" 8 | vCore "github.com/InazumaV/V2bX/core" 9 | "github.com/InazumaV/V2bX/core/xray/app/dispatcher" 10 | _ "github.com/InazumaV/V2bX/core/xray/distro/all" 11 | "github.com/goccy/go-json" 12 | log "github.com/sirupsen/logrus" 13 | "github.com/xtls/xray-core/app/proxyman" 14 | "github.com/xtls/xray-core/app/stats" 15 | "github.com/xtls/xray-core/common/serial" 16 | "github.com/xtls/xray-core/core" 17 | "github.com/xtls/xray-core/features/inbound" 18 | "github.com/xtls/xray-core/features/outbound" 19 | "github.com/xtls/xray-core/features/routing" 20 | statsFeature "github.com/xtls/xray-core/features/stats" 21 | coreConf "github.com/xtls/xray-core/infra/conf" 22 | ) 23 | 24 | func init() { 25 | vCore.RegisterCore("xray", New) 26 | } 27 | 28 | // Core Structure 29 | type Core struct { 30 | access sync.Mutex 31 | Server *core.Instance 32 | ihm inbound.Manager 33 | ohm outbound.Manager 34 | shm statsFeature.Manager 35 | dispatcher *dispatcher.DefaultDispatcher 36 | } 37 | 38 | func New(c *conf.CoreConfig) (vCore.Core, error) { 39 | return &Core{Server: getCore(c.XrayConfig)}, nil 40 | } 41 | 42 | func parseConnectionConfig(c *conf.XrayConnectionConfig) (policy *coreConf.Policy) { 43 | policy = &coreConf.Policy{ 44 | StatsUserUplink: true, 45 | StatsUserDownlink: true, 46 | Handshake: &c.Handshake, 47 | ConnectionIdle: &c.ConnIdle, 48 | UplinkOnly: &c.UplinkOnly, 49 | DownlinkOnly: &c.DownlinkOnly, 50 | BufferSize: &c.BufferSize, 51 | } 52 | return 53 | } 54 | 55 | func getCore(c *conf.XrayConfig) *core.Instance { 56 | os.Setenv("XRAY_LOCATION_ASSET", c.AssetPath) 57 | // Log Config 58 | coreLogConfig := &coreConf.LogConfig{} 59 | coreLogConfig.LogLevel = c.LogConfig.Level 60 | coreLogConfig.AccessLog = c.LogConfig.AccessPath 61 | coreLogConfig.ErrorLog = c.LogConfig.ErrorPath 62 | // DNS config 63 | coreDnsConfig := &coreConf.DNSConfig{} 64 | os.Setenv("XRAY_DNS_PATH", "") 65 | if c.DnsConfigPath != "" { 66 | if f, err := os.Open(c.DnsConfigPath); err != nil { 67 | log.WithField("err", err).Panic("Failed to read DNS config file") 68 | } else { 69 | if err = json.NewDecoder(f).Decode(coreDnsConfig); err != nil { 70 | log.WithField("err", err).Error("Failed to unmarshal DNS config") 71 | } 72 | } 73 | os.Setenv("XRAY_DNS_PATH", c.DnsConfigPath) 74 | } 75 | dnsConfig, err := coreDnsConfig.Build() 76 | if err != nil { 77 | log.WithField("err", err).Panic("Failed to understand DNS config, Please check: https://xtls.github.io/config/dns.html for help") 78 | } 79 | // Routing config 80 | coreRouterConfig := &coreConf.RouterConfig{} 81 | if c.RouteConfigPath != "" { 82 | if f, err := os.Open(c.RouteConfigPath); err != nil { 83 | log.WithField("err", err).Panic("Failed to read Routing config file") 84 | } else { 85 | if err = json.NewDecoder(f).Decode(coreRouterConfig); err != nil { 86 | log.WithField("err", err).Panic("Failed to unmarshal Routing config") 87 | } 88 | } 89 | } 90 | routeConfig, err := coreRouterConfig.Build() 91 | if err != nil { 92 | log.WithField("err", err).Panic("Failed to understand Routing config Please check: https://xtls.github.io/config/routing.html") 93 | } 94 | // Custom Inbound config 95 | var coreCustomInboundConfig []coreConf.InboundDetourConfig 96 | if c.InboundConfigPath != "" { 97 | if f, err := os.Open(c.InboundConfigPath); err != nil { 98 | log.WithField("err", err).Panic("Failed to read Custom Inbound config file") 99 | } else { 100 | if err = json.NewDecoder(f).Decode(&coreCustomInboundConfig); err != nil { 101 | log.WithField("err", err).Panic("Failed to unmarshal Custom Inbound config") 102 | } 103 | } 104 | } 105 | var inBoundConfig []*core.InboundHandlerConfig 106 | for _, config := range coreCustomInboundConfig { 107 | oc, err := config.Build() 108 | if err != nil { 109 | log.WithField("err", err).Panic("Failed to understand Inbound config, Please check: https://xtls.github.io/config/inbound.html for help") 110 | } 111 | inBoundConfig = append(inBoundConfig, oc) 112 | } 113 | // Custom Outbound config 114 | var coreCustomOutboundConfig []coreConf.OutboundDetourConfig 115 | if c.OutboundConfigPath != "" { 116 | if f, err := os.Open(c.OutboundConfigPath); err != nil { 117 | log.WithField("err", err).Panic("Failed to read Custom Outbound config file") 118 | } else { 119 | if err = json.NewDecoder(f).Decode(&coreCustomOutboundConfig); err != nil { 120 | log.WithField("err", err).Panic("Failed to unmarshal Custom Outbound config") 121 | } 122 | } 123 | } 124 | var outBoundConfig []*core.OutboundHandlerConfig 125 | for _, config := range coreCustomOutboundConfig { 126 | oc, err := config.Build() 127 | if err != nil { 128 | log.WithField("err", err).Panic("Failed to understand Outbound config, Please check: https://xtls.github.io/config/outbound.html for help") 129 | } 130 | outBoundConfig = append(outBoundConfig, oc) 131 | } 132 | // Policy config 133 | levelPolicyConfig := parseConnectionConfig(c.ConnectionConfig) 134 | corePolicyConfig := &coreConf.PolicyConfig{} 135 | corePolicyConfig.Levels = map[uint32]*coreConf.Policy{0: levelPolicyConfig} 136 | policyConfig, _ := corePolicyConfig.Build() 137 | // Build Core conf 138 | config := &core.Config{ 139 | App: []*serial.TypedMessage{ 140 | serial.ToTypedMessage(coreLogConfig.Build()), 141 | serial.ToTypedMessage(&dispatcher.Config{}), 142 | serial.ToTypedMessage(&stats.Config{}), 143 | serial.ToTypedMessage(&proxyman.InboundConfig{}), 144 | serial.ToTypedMessage(&proxyman.OutboundConfig{}), 145 | serial.ToTypedMessage(policyConfig), 146 | serial.ToTypedMessage(dnsConfig), 147 | serial.ToTypedMessage(routeConfig), 148 | }, 149 | Inbound: inBoundConfig, 150 | Outbound: outBoundConfig, 151 | } 152 | server, err := core.New(config) 153 | if err != nil { 154 | log.WithField("err", err).Panic("failed to create instance") 155 | } 156 | log.Info("Xray Core Version: ", core.Version()) 157 | return server 158 | } 159 | 160 | // Start the Core 161 | func (c *Core) Start() error { 162 | c.access.Lock() 163 | defer c.access.Unlock() 164 | if err := c.Server.Start(); err != nil { 165 | return err 166 | } 167 | c.shm = c.Server.GetFeature(statsFeature.ManagerType()).(statsFeature.Manager) 168 | c.ihm = c.Server.GetFeature(inbound.ManagerType()).(inbound.Manager) 169 | c.ohm = c.Server.GetFeature(outbound.ManagerType()).(outbound.Manager) 170 | c.dispatcher = c.Server.GetFeature(routing.DispatcherType()).(*dispatcher.DefaultDispatcher) 171 | return nil 172 | } 173 | 174 | // Close the core 175 | func (c *Core) Close() error { 176 | c.access.Lock() 177 | defer c.access.Unlock() 178 | c.ihm = nil 179 | c.ohm = nil 180 | c.shm = nil 181 | c.dispatcher = nil 182 | err := c.Server.Close() 183 | if err != nil { 184 | return err 185 | } 186 | return nil 187 | } 188 | 189 | func (c *Core) Protocols() []string { 190 | return []string{ 191 | "vmess", 192 | "vless", 193 | "shadowsocks", 194 | "trojan", 195 | } 196 | } 197 | 198 | func (c *Core) Type() string { 199 | return "xray" 200 | } 201 | -------------------------------------------------------------------------------- /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 | "DnsConfigPath": "/etc/V2bX/dns.json", 14 | "NTP": { 15 | "Enable": true, 16 | "Server": "time.apple.com", 17 | "ServerPort": 0 18 | } 19 | } 20 | ], 21 | "Nodes": [ 22 | { 23 | "Core": "sing", 24 | "ApiHost": "http://127.0.0.1", 25 | "ApiKey": "test", 26 | "NodeID": 33, 27 | "NodeType": "shadowsocks", 28 | "Timeout": 30, 29 | "ListenIP": "0.0.0.0", 30 | "SendIP": "0.0.0.0", 31 | "EnableProxyProtocol": false, 32 | "EnableDNS": true, 33 | "DomainStrategy": "ipv4_only", 34 | "LimitConfig": { 35 | "EnableRealtime": false, 36 | "SpeedLimit": 0, 37 | "IPLimit": 0, 38 | "ConnLimit": 0, 39 | "EnableDynamicSpeedLimit": false, 40 | "DynamicSpeedLimitConfig": { 41 | "Periodic": 60, 42 | "Traffic": 1000, 43 | "SpeedLimit": 100, 44 | "ExpireTime": 60 45 | } 46 | } 47 | } 48 | ] 49 | } -------------------------------------------------------------------------------- /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/InazumaV/V2bX/20cd5086215cbceea3bc5691109b73395f26518d/example/geoip.dat -------------------------------------------------------------------------------- /example/geosite.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InazumaV/V2bX/20cd5086215cbceea3bc5691109b73395f26518d/example/geosite.dat -------------------------------------------------------------------------------- /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/clear.go: -------------------------------------------------------------------------------- 1 | package limiter 2 | 3 | import log "github.com/sirupsen/logrus" 4 | 5 | func ClearOnlineIP() error { 6 | log.WithField("Type", "Limiter"). 7 | Debug("Clear online ip...") 8 | limitLock.RLock() 9 | for _, l := range limiter { 10 | l.ConnLimiter.ClearOnlineIP() 11 | } 12 | limitLock.RUnlock() 13 | log.WithField("Type", "Limiter"). 14 | Debug("Clear online ip done") 15 | return nil 16 | } 17 | -------------------------------------------------------------------------------- /limiter/conn.go: -------------------------------------------------------------------------------- 1 | package limiter 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | type ConnLimiter struct { 9 | realtime bool 10 | ipLimit int 11 | connLimit int 12 | count sync.Map // map[string]int 13 | ip sync.Map // map[string]map[string]int 14 | } 15 | 16 | func NewConnLimiter(conn int, ip int, realtime bool) *ConnLimiter { 17 | return &ConnLimiter{ 18 | realtime: realtime, 19 | connLimit: conn, 20 | ipLimit: ip, 21 | count: sync.Map{}, 22 | ip: sync.Map{}, 23 | } 24 | } 25 | 26 | func (c *ConnLimiter) AddConnCount(user string, ip string, isTcp bool) (limit bool) { 27 | if c.connLimit != 0 { 28 | if v, ok := c.count.Load(user); ok { 29 | if v.(int) >= c.connLimit { 30 | // over connection limit 31 | return true 32 | } else if isTcp { 33 | // tcp protocol 34 | // connection count add 35 | c.count.Store(user, v.(int)+1) 36 | } 37 | } else if isTcp { 38 | // tcp protocol 39 | // store connection count 40 | c.count.Store(user, 1) 41 | } 42 | } 43 | if c.ipLimit == 0 { 44 | return false 45 | } 46 | // first user map 47 | ipMap := new(sync.Map) 48 | if c.realtime { 49 | if isTcp { 50 | ipMap.Store(ip, 2) 51 | } else { 52 | ipMap.Store(ip, 1) 53 | } 54 | } else { 55 | ipMap.Store(ip, time.Now()) 56 | } 57 | // check user online ip 58 | if v, ok := c.ip.LoadOrStore(user, ipMap); ok { 59 | // have user 60 | ips := v.(*sync.Map) 61 | cn := 0 62 | if online, ok := ips.Load(ip); ok { 63 | // online ip 64 | if c.realtime { 65 | if isTcp { 66 | // tcp count add 67 | ips.Store(ip, online.(int)+2) 68 | } 69 | } else { 70 | // update connect time for not realtime 71 | ips.Store(ip, time.Now()) 72 | } 73 | } else { 74 | // not online ip 75 | ips.Range(func(_, _ interface{}) bool { 76 | cn++ 77 | if cn >= c.ipLimit { 78 | limit = true 79 | return false 80 | } 81 | return true 82 | }) 83 | if limit { 84 | // over ip limit 85 | return 86 | } 87 | if c.realtime { 88 | if isTcp { 89 | ips.Store(ip, 2) 90 | } else { 91 | ips.Store(ip, 1) 92 | } 93 | } else { 94 | ips.Store(ip, time.Now()) 95 | } 96 | } 97 | } 98 | return 99 | } 100 | 101 | // DelConnCount Delete tcp connection count, no tcp do not use 102 | func (c *ConnLimiter) DelConnCount(user string, ip string) { 103 | if !c.realtime { 104 | return 105 | } 106 | if c.connLimit != 0 { 107 | if v, ok := c.count.Load(user); ok { 108 | if v.(int) == 1 { 109 | c.count.Delete(user) 110 | } else { 111 | c.count.Store(user, v.(int)-1) 112 | } 113 | } 114 | } 115 | if c.ipLimit == 0 { 116 | return 117 | } 118 | if i, ok := c.ip.Load(user); ok { 119 | is := i.(*sync.Map) 120 | if i, ok := is.Load(ip); ok { 121 | if i.(int) == 2 { 122 | is.Delete(ip) 123 | } else { 124 | is.Store(user, i.(int)-2) 125 | } 126 | notDel := false 127 | c.ip.Range(func(_, _ any) bool { 128 | notDel = true 129 | return false 130 | }) 131 | if !notDel { 132 | c.ip.Delete(user) 133 | } 134 | } 135 | } 136 | } 137 | 138 | // ClearOnlineIP Clear udp,icmp and other packet protocol online ip 139 | func (c *ConnLimiter) ClearOnlineIP() { 140 | c.ip.Range(func(u, v any) bool { 141 | userIp := v.(*sync.Map) 142 | notDel := false 143 | userIp.Range(func(ip, v any) bool { 144 | notDel = true 145 | if _, ok := v.(int); ok { 146 | if v.(int) == 1 { 147 | // clear packet ip for realtime 148 | userIp.Delete(ip) 149 | } 150 | return true 151 | } else { 152 | // clear ip for not realtime 153 | if v.(time.Time).Before(time.Now().Add(time.Minute)) { 154 | // 1 minute no active 155 | userIp.Delete(ip) 156 | } 157 | } 158 | return true 159 | }) 160 | if !notDel { 161 | c.ip.Delete(u) 162 | } 163 | return true 164 | }) 165 | } 166 | -------------------------------------------------------------------------------- /limiter/conn_test.go: -------------------------------------------------------------------------------- 1 | package limiter 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | var c *ConnLimiter 10 | 11 | func init() { 12 | c = NewConnLimiter(1, 1, true) 13 | } 14 | 15 | func TestConnLimiter_AddConnCount(t *testing.T) { 16 | t.Log(c.AddConnCount("1", "1", true)) 17 | t.Log(c.AddConnCount("1", "2", true)) 18 | } 19 | 20 | func TestConnLimiter_DelConnCount(t *testing.T) { 21 | t.Log(c.AddConnCount("1", "1", true)) 22 | t.Log(c.AddConnCount("1", "2", true)) 23 | c.DelConnCount("1", "1") 24 | t.Log(c.AddConnCount("1", "2", true)) 25 | } 26 | 27 | func TestConnLimiter_ClearOnlineIP(t *testing.T) { 28 | t.Log(c.AddConnCount("1", "1", false)) 29 | t.Log(c.AddConnCount("1", "2", false)) 30 | c.ClearOnlineIP() 31 | t.Log(c.AddConnCount("1", "2", true)) 32 | c.DelConnCount("1", "2") 33 | t.Log(c.AddConnCount("1", "1", false)) 34 | // not realtime 35 | c.realtime = false 36 | t.Log(c.AddConnCount("3", "2", true)) 37 | c.ClearOnlineIP() 38 | t.Log(c.ip.Load("3")) 39 | time.Sleep(time.Minute) 40 | c.ClearOnlineIP() 41 | t.Log(c.ip.Load("3")) 42 | } 43 | 44 | func BenchmarkConnLimiter(b *testing.B) { 45 | wg := sync.WaitGroup{} 46 | for i := 0; i < b.N; i++ { 47 | wg.Add(1) 48 | go func() { 49 | c.AddConnCount("1", "2", true) 50 | c.DelConnCount("1", "2") 51 | wg.Done() 52 | }() 53 | } 54 | wg.Wait() 55 | 56 | } 57 | -------------------------------------------------------------------------------- /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 | "sync" 7 | "time" 8 | 9 | "github.com/InazumaV/V2bX/api/panel" 10 | "github.com/InazumaV/V2bX/common/format" 11 | "github.com/InazumaV/V2bX/conf" 12 | "github.com/juju/ratelimit" 13 | log "github.com/sirupsen/logrus" 14 | "github.com/xtls/xray-core/common/task" 15 | ) 16 | 17 | var limitLock sync.RWMutex 18 | var limiter map[string]*Limiter 19 | 20 | func Init() { 21 | limiter = map[string]*Limiter{} 22 | c := task.Periodic{ 23 | Interval: time.Minute * 2, 24 | Execute: ClearOnlineIP, 25 | } 26 | go func() { 27 | log.WithField("Type", "Limiter"). 28 | Debug("ClearOnlineIP started") 29 | time.Sleep(time.Minute * 2) 30 | _ = c.Start() 31 | }() 32 | } 33 | 34 | type Limiter struct { 35 | DomainRules []*regexp.Regexp 36 | ProtocolRules []string 37 | SpeedLimit int 38 | UserLimitInfo *sync.Map // Key: Uid value: UserLimitInfo 39 | ConnLimiter *ConnLimiter // Key: Uid value: ConnLimiter 40 | SpeedLimiter *sync.Map // key: Uid, value: *ratelimit.Bucket 41 | } 42 | 43 | type UserLimitInfo struct { 44 | UID int 45 | SpeedLimit int 46 | DynamicSpeedLimit int 47 | ExpireTime int64 48 | } 49 | 50 | func AddLimiter(tag string, l *conf.LimitConfig, users []panel.UserInfo) *Limiter { 51 | info := &Limiter{ 52 | SpeedLimit: l.SpeedLimit, 53 | UserLimitInfo: new(sync.Map), 54 | ConnLimiter: NewConnLimiter(l.ConnLimit, l.IPLimit, l.EnableRealtime), 55 | SpeedLimiter: new(sync.Map), 56 | } 57 | for i := range users { 58 | if users[i].SpeedLimit != 0 { 59 | userLimit := &UserLimitInfo{ 60 | UID: users[i].Id, 61 | SpeedLimit: users[i].SpeedLimit, 62 | } 63 | info.UserLimitInfo.Store(format.UserTag(tag, users[i].Uuid), userLimit) 64 | } 65 | } 66 | limitLock.Lock() 67 | limiter[tag] = info 68 | limitLock.Unlock() 69 | return info 70 | } 71 | 72 | func GetLimiter(tag string) (info *Limiter, err error) { 73 | limitLock.RLock() 74 | info, ok := limiter[tag] 75 | limitLock.RUnlock() 76 | if !ok { 77 | return nil, errors.New("not found") 78 | } 79 | return 80 | } 81 | 82 | func DeleteLimiter(tag string) { 83 | limitLock.Lock() 84 | delete(limiter, tag) 85 | limitLock.Unlock() 86 | } 87 | 88 | func (l *Limiter) UpdateUser(tag string, added []panel.UserInfo, deleted []panel.UserInfo) { 89 | for i := range deleted { 90 | l.UserLimitInfo.Delete(format.UserTag(tag, deleted[i].Uuid)) 91 | } 92 | for i := range added { 93 | if added[i].SpeedLimit != 0 { 94 | userLimit := &UserLimitInfo{ 95 | UID: added[i].Id, 96 | SpeedLimit: added[i].SpeedLimit, 97 | ExpireTime: 0, 98 | } 99 | l.UserLimitInfo.Store(format.UserTag(tag, added[i].Uuid), userLimit) 100 | } 101 | } 102 | } 103 | 104 | func (l *Limiter) UpdateDynamicSpeedLimit(tag, uuid string, limit int, expire time.Time) error { 105 | if v, ok := l.UserLimitInfo.Load(format.UserTag(tag, uuid)); ok { 106 | info := v.(*UserLimitInfo) 107 | info.DynamicSpeedLimit = limit 108 | info.ExpireTime = expire.Unix() 109 | } else { 110 | return errors.New("not found") 111 | } 112 | return nil 113 | } 114 | 115 | func (l *Limiter) CheckLimit(email string, ip string, isTcp bool) (Bucket *ratelimit.Bucket, Reject bool) { 116 | // ip and conn limiter 117 | if l.ConnLimiter.AddConnCount(email, ip, isTcp) { 118 | return nil, true 119 | } 120 | // check and gen speed limit Bucket 121 | nodeLimit := l.SpeedLimit 122 | userLimit := 0 123 | if v, ok := l.UserLimitInfo.Load(email); ok { 124 | u := v.(*UserLimitInfo) 125 | if u.ExpireTime < time.Now().Unix() && u.ExpireTime != 0 { 126 | if u.SpeedLimit != 0 { 127 | userLimit = u.SpeedLimit 128 | u.DynamicSpeedLimit = 0 129 | u.ExpireTime = 0 130 | } else { 131 | l.UserLimitInfo.Delete(email) 132 | } 133 | } else { 134 | userLimit = determineSpeedLimit(u.SpeedLimit, u.DynamicSpeedLimit) 135 | } 136 | } 137 | limit := int64(determineSpeedLimit(nodeLimit, userLimit)) * 1000000 / 8 // If you need the Speed limit 138 | if limit > 0 { 139 | Bucket = ratelimit.NewBucketWithQuantum(time.Second, limit, limit) // Byte/s 140 | if v, ok := l.SpeedLimiter.LoadOrStore(email, Bucket); ok { 141 | return v.(*ratelimit.Bucket), false 142 | } else { 143 | l.SpeedLimiter.Store(email, Bucket) 144 | return Bucket, false 145 | } 146 | } else { 147 | return nil, false 148 | } 149 | } 150 | 151 | type UserIpList struct { 152 | Uid int `json:"Uid"` 153 | IpList []string `json:"Ips"` 154 | } 155 | 156 | func determineDeviceLimit(nodeLimit, userLimit int) (limit int) { 157 | if nodeLimit == 0 || userLimit == 0 { 158 | if nodeLimit > userLimit { 159 | return nodeLimit 160 | } else if nodeLimit < userLimit { 161 | return userLimit 162 | } else { 163 | return 0 164 | } 165 | } else { 166 | if nodeLimit > userLimit { 167 | return userLimit 168 | } else if nodeLimit < userLimit { 169 | return nodeLimit 170 | } else { 171 | return nodeLimit 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /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 "github.com/InazumaV/V2bX/cmd" 4 | 5 | func main() { 6 | cmd.Run() 7 | } 8 | -------------------------------------------------------------------------------- /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/iprecoder" 8 | "github.com/InazumaV/V2bX/api/panel" 9 | "github.com/InazumaV/V2bX/common/task" 10 | "github.com/InazumaV/V2bX/conf" 11 | vCore "github.com/InazumaV/V2bX/core" 12 | "github.com/InazumaV/V2bX/limiter" 13 | log "github.com/sirupsen/logrus" 14 | ) 15 | 16 | type Controller struct { 17 | server vCore.Core 18 | apiClient *panel.Client 19 | tag string 20 | limiter *limiter.Limiter 21 | traffic map[string]int64 22 | userList []panel.UserInfo 23 | info *panel.NodeInfo 24 | ipRecorder iprecoder.IpRecorder 25 | nodeInfoMonitorPeriodic *task.Task 26 | userReportPeriodic *task.Task 27 | renewCertPeriodic *task.Task 28 | dynamicSpeedLimitPeriodic *task.Task 29 | onlineIpReportPeriodic *task.Task 30 | *conf.Options 31 | } 32 | 33 | // NewController return a Node controller with default parameters. 34 | func NewController(server vCore.Core, api *panel.Client, config *conf.Options) *Controller { 35 | controller := &Controller{ 36 | server: server, 37 | Options: config, 38 | apiClient: api, 39 | } 40 | return controller 41 | } 42 | 43 | // Start implement the Start() function of the service interface 44 | func (c *Controller) Start() error { 45 | // First fetch Node Info 46 | var err error 47 | node, err := c.apiClient.GetNodeInfo() 48 | if err != nil { 49 | return fmt.Errorf("get node info error: %s", err) 50 | } 51 | // Update user 52 | c.userList, err = c.apiClient.GetUserList() 53 | if err != nil { 54 | return fmt.Errorf("get user list error: %s", err) 55 | } 56 | if len(c.userList) == 0 { 57 | return errors.New("add users error: not have any user") 58 | } 59 | if len(c.Options.Name) == 0 { 60 | c.tag = c.buildNodeTag(node) 61 | } else { 62 | c.tag = c.Options.Name 63 | } 64 | 65 | // add limiter 66 | l := limiter.AddLimiter(c.tag, &c.LimitConfig, c.userList) 67 | // add rule limiter 68 | if err = l.UpdateRule(&node.Rules); err != nil { 69 | return fmt.Errorf("update rule error: %s", err) 70 | } 71 | c.limiter = l 72 | if node.Security == panel.Tls { 73 | err = c.requestCert() 74 | if err != nil { 75 | return fmt.Errorf("request cert error: %s", err) 76 | } 77 | } 78 | // Add new tag 79 | err = c.server.AddNode(c.tag, node, c.Options) 80 | if err != nil { 81 | return fmt.Errorf("add new node error: %s", err) 82 | } 83 | added, err := c.server.AddUsers(&vCore.AddUsersParams{ 84 | Tag: c.tag, 85 | Users: c.userList, 86 | NodeInfo: node, 87 | }) 88 | if err != nil { 89 | return fmt.Errorf("add users error: %s", err) 90 | } 91 | log.WithField("tag", c.tag).Infof("Added %d new users", added) 92 | c.info = node 93 | c.startTasks(node) 94 | return nil 95 | } 96 | 97 | // Close implement the Close() function of the service interface 98 | func (c *Controller) Close() error { 99 | limiter.DeleteLimiter(c.tag) 100 | if c.nodeInfoMonitorPeriodic != nil { 101 | c.nodeInfoMonitorPeriodic.Close() 102 | } 103 | if c.userReportPeriodic != nil { 104 | c.userReportPeriodic.Close() 105 | } 106 | if c.renewCertPeriodic != nil { 107 | c.renewCertPeriodic.Close() 108 | } 109 | if c.dynamicSpeedLimitPeriodic != nil { 110 | c.dynamicSpeedLimitPeriodic.Close() 111 | } 112 | if c.onlineIpReportPeriodic != nil { 113 | c.onlineIpReportPeriodic.Close() 114 | } 115 | return nil 116 | } 117 | 118 | func (c *Controller) buildNodeTag(node *panel.NodeInfo) string { 119 | return fmt.Sprintf("[%s]-%s:%d", c.apiClient.APIHost, node.Type, node.Id) 120 | } 121 | -------------------------------------------------------------------------------- /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 | return nil 103 | } 104 | 105 | func (l *Lego) RenewCert() error { 106 | file, err := os.ReadFile(l.config.CertFile) 107 | if err != nil { 108 | return fmt.Errorf("read cert file error: %s", err) 109 | } 110 | if e, err := l.CheckCert(file); !e { 111 | return nil 112 | } else if err != nil { 113 | return fmt.Errorf("check cert error: %s", err) 114 | } 115 | res, err := l.client.Certificate.Renew(certificate.Resource{ 116 | Domain: l.config.CertDomain, 117 | Certificate: file, 118 | }, true, false, "") 119 | if err != nil { 120 | return err 121 | } 122 | err = l.writeCert(res) 123 | return nil 124 | } 125 | 126 | func (l *Lego) CheckCert(file []byte) (bool, error) { 127 | cert, err := certcrypto.ParsePEMCertificate(file) 128 | if err != nil { 129 | return false, err 130 | } 131 | notAfter := int(time.Until(cert.NotAfter).Hours() / 24.0) 132 | if notAfter > 30 { 133 | return false, nil 134 | } 135 | return true, nil 136 | } 137 | func (l *Lego) parseParams(path string) string { 138 | r := strings.NewReplacer("{domain}", l.config.CertDomain, 139 | "{email}", l.config.Email) 140 | return r.Replace(path) 141 | } 142 | func (l *Lego) writeCert(certificates *certificate.Resource) error { 143 | err := checkPath(l.config.CertFile) 144 | if err != nil { 145 | return fmt.Errorf("check path error: %s", err) 146 | } 147 | err = os.WriteFile(l.parseParams(l.config.CertFile), certificates.Certificate, 0644) 148 | if err != nil { 149 | return err 150 | } 151 | err = checkPath(l.config.KeyFile) 152 | if err != nil { 153 | return fmt.Errorf("check path error: %s", err) 154 | } 155 | err = os.WriteFile(l.parseParams(l.config.KeyFile), certificates.PrivateKey, 0644) 156 | if err != nil { 157 | return err 158 | } 159 | return nil 160 | } 161 | 162 | type User struct { 163 | Email string `json:"Email"` 164 | Registration *registration.Resource `json:"Registration"` 165 | key crypto.PrivateKey 166 | KeyEncoded string `json:"Key"` 167 | } 168 | 169 | func (u *User) GetEmail() string { 170 | return u.Email 171 | } 172 | func (u *User) GetRegistration() *registration.Resource { 173 | return u.Registration 174 | } 175 | func (u *User) GetPrivateKey() crypto.PrivateKey { 176 | return u.key 177 | } 178 | 179 | func NewLegoUser(path string, email string) (*User, error) { 180 | var user User 181 | if file.IsExist(path) { 182 | err := user.Load(path) 183 | if err != nil { 184 | return nil, err 185 | } 186 | if user.Email != email { 187 | user.Registration = nil 188 | user.Email = email 189 | err := registerUser(&user, path) 190 | if err != nil { 191 | return nil, err 192 | } 193 | } 194 | } else { 195 | user.Email = email 196 | err := registerUser(&user, path) 197 | if err != nil { 198 | return nil, err 199 | } 200 | } 201 | return &user, nil 202 | } 203 | 204 | func registerUser(user *User, path string) error { 205 | privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 206 | if err != nil { 207 | return fmt.Errorf("generate key error: %s", err) 208 | } 209 | user.key = privateKey 210 | c := lego.NewConfig(user) 211 | client, err := lego.NewClient(c) 212 | if err != nil { 213 | return fmt.Errorf("create lego client error: %s", err) 214 | } 215 | reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) 216 | if err != nil { 217 | return err 218 | } 219 | user.Registration = reg 220 | err = user.Save(path) 221 | if err != nil { 222 | return fmt.Errorf("save user error: %s", err) 223 | } 224 | return nil 225 | } 226 | 227 | func EncodePrivate(privKey *ecdsa.PrivateKey) (string, error) { 228 | encoded, err := x509.MarshalECPrivateKey(privKey) 229 | if err != nil { 230 | return "", err 231 | } 232 | pemEncoded := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: encoded}) 233 | return string(pemEncoded), nil 234 | } 235 | func (u *User) Save(path string) error { 236 | err := checkPath(path) 237 | if err != nil { 238 | return fmt.Errorf("check path error: %s", err) 239 | } 240 | u.KeyEncoded, _ = EncodePrivate(u.key.(*ecdsa.PrivateKey)) 241 | f, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) 242 | if err != nil { 243 | return err 244 | } 245 | err = json.NewEncoder(f).Encode(u) 246 | if err != nil { 247 | return fmt.Errorf("marshal json error: %s", err) 248 | } 249 | u.KeyEncoded = "" 250 | return nil 251 | } 252 | 253 | func (u *User) DecodePrivate(pemEncodedPriv string) (*ecdsa.PrivateKey, error) { 254 | blockPriv, _ := pem.Decode([]byte(pemEncodedPriv)) 255 | x509EncodedPriv := blockPriv.Bytes 256 | privateKey, err := x509.ParseECPrivateKey(x509EncodedPriv) 257 | return privateKey, err 258 | } 259 | func (u *User) Load(path string) error { 260 | f, err := os.Open(path) 261 | if err != nil { 262 | return fmt.Errorf("open file error: %s", err) 263 | } 264 | err = json.NewDecoder(f).Decode(u) 265 | if err != nil { 266 | return fmt.Errorf("unmarshal json error: %s", err) 267 | } 268 | u.key, err = u.DecodePrivate(u.KeyEncoded) 269 | if err != nil { 270 | return fmt.Errorf("decode private key error: %s", err) 271 | } 272 | return nil 273 | } 274 | -------------------------------------------------------------------------------- /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 | if newN != nil { 72 | c.info = newN 73 | // nodeInfo changed 74 | if newU != nil { 75 | c.userList = newU 76 | } 77 | c.traffic = make(map[string]int64) 78 | // Remove old node 79 | log.WithField("tag", c.tag).Info("Node changed, reload") 80 | err = c.server.DelNode(c.tag) 81 | if err != nil { 82 | log.WithFields(log.Fields{ 83 | "tag": c.tag, 84 | "err": err, 85 | }).Error("Delete node failed") 86 | return nil 87 | } 88 | 89 | // Update limiter 90 | if len(c.Options.Name) == 0 { 91 | c.tag = c.buildNodeTag(newN) 92 | // Remove Old limiter 93 | limiter.DeleteLimiter(c.tag) 94 | // Add new Limiter 95 | l := limiter.AddLimiter(c.tag, &c.LimitConfig, c.userList) 96 | c.limiter = l 97 | } 98 | // Update rule 99 | err = c.limiter.UpdateRule(&newN.Rules) 100 | if err != nil { 101 | log.WithFields(log.Fields{ 102 | "tag": c.tag, 103 | "err": err, 104 | }).Error("Update Rule failed") 105 | return nil 106 | } 107 | 108 | // check cert 109 | if newN.Security == panel.Tls { 110 | err = c.requestCert() 111 | if err != nil { 112 | log.WithFields(log.Fields{ 113 | "tag": c.tag, 114 | "err": err, 115 | }).Error("Request cert failed") 116 | return nil 117 | } 118 | } 119 | // add new node 120 | err = c.server.AddNode(c.tag, newN, c.Options) 121 | if err != nil { 122 | log.WithFields(log.Fields{ 123 | "tag": c.tag, 124 | "err": err, 125 | }).Error("Add node failed") 126 | return nil 127 | } 128 | _, err = c.server.AddUsers(&vCore.AddUsersParams{ 129 | Tag: c.tag, 130 | Users: c.userList, 131 | NodeInfo: newN, 132 | }) 133 | if err != nil { 134 | log.WithFields(log.Fields{ 135 | "tag": c.tag, 136 | "err": err, 137 | }).Error("Add users failed") 138 | return nil 139 | } 140 | // Check interval 141 | if c.nodeInfoMonitorPeriodic.Interval != newN.PullInterval && 142 | newN.PullInterval != 0 { 143 | c.nodeInfoMonitorPeriodic.Interval = newN.PullInterval 144 | c.nodeInfoMonitorPeriodic.Close() 145 | _ = c.nodeInfoMonitorPeriodic.Start(false) 146 | } 147 | if c.userReportPeriodic.Interval != newN.PushInterval && 148 | newN.PushInterval != 0 { 149 | c.userReportPeriodic.Interval = newN.PullInterval 150 | c.userReportPeriodic.Close() 151 | _ = c.userReportPeriodic.Start(false) 152 | } 153 | log.WithField("tag", c.tag).Infof("Added %d new users", len(c.userList)) 154 | // exit 155 | return nil 156 | } 157 | 158 | // node no changed, check users 159 | if len(newU) == 0 { 160 | return nil 161 | } 162 | deleted, added := compareUserList(c.userList, newU) 163 | if len(deleted) > 0 { 164 | // have deleted users 165 | err = c.server.DelUsers(deleted, c.tag) 166 | if err != nil { 167 | log.WithFields(log.Fields{ 168 | "tag": c.tag, 169 | "err": err, 170 | }).Error("Delete users failed") 171 | return nil 172 | } 173 | } 174 | if len(added) > 0 { 175 | // have added users 176 | _, err = c.server.AddUsers(&vCore.AddUsersParams{ 177 | Tag: c.tag, 178 | NodeInfo: c.info, 179 | Users: added, 180 | }) 181 | if err != nil { 182 | log.WithFields(log.Fields{ 183 | "tag": c.tag, 184 | "err": err, 185 | }).Error("Add users failed") 186 | return nil 187 | } 188 | } 189 | if len(added) > 0 || len(deleted) > 0 { 190 | // update Limiter 191 | c.limiter.UpdateUser(c.tag, added, deleted) 192 | if err != nil { 193 | log.WithFields(log.Fields{ 194 | "tag": c.tag, 195 | "err": err, 196 | }).Error("limiter users failed") 197 | return nil 198 | } 199 | // clear traffic record 200 | if c.LimitConfig.EnableDynamicSpeedLimit { 201 | for i := range deleted { 202 | delete(c.traffic, deleted[i].Uuid) 203 | } 204 | } 205 | } 206 | c.userList = newU 207 | if len(added)+len(deleted) != 0 { 208 | log.WithField("tag", c.tag). 209 | Infof("%d user deleted, %d user added", len(deleted), len(added)) 210 | } 211 | return nil 212 | } 213 | 214 | func (c *Controller) SpeedChecker() error { 215 | for u, t := range c.traffic { 216 | if t >= c.LimitConfig.DynamicSpeedLimitConfig.Traffic { 217 | err := c.limiter.UpdateDynamicSpeedLimit(c.tag, u, 218 | c.LimitConfig.DynamicSpeedLimitConfig.SpeedLimit, 219 | time.Now().Add(time.Duration(c.LimitConfig.DynamicSpeedLimitConfig.ExpireTime)*time.Minute)) 220 | log.WithField("err", err).Error("Update dynamic speed limit failed") 221 | delete(c.traffic, u) 222 | } 223 | } 224 | return nil 225 | } 226 | -------------------------------------------------------------------------------- /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 | if _, ok := c.traffic[c.userList[i].Uuid]; ok { 18 | c.traffic[c.userList[i].Uuid] += up + down 19 | } else { 20 | c.traffic[c.userList[i].Uuid] = up + down 21 | } 22 | } 23 | userTraffic = append(userTraffic, panel.UserTraffic{ 24 | UID: (c.userList)[i].Id, 25 | Upload: up, 26 | Download: down}) 27 | } 28 | } 29 | if len(userTraffic) > 0 { 30 | err = c.apiClient.ReportUserTraffic(userTraffic) 31 | if err != nil { 32 | log.WithFields(log.Fields{ 33 | "tag": c.tag, 34 | "err": err, 35 | }).Info("Report user traffic failed") 36 | } else { 37 | log.WithField("tag", c.tag).Infof("Report %d online users", len(userTraffic)) 38 | } 39 | } 40 | userTraffic = nil 41 | return nil 42 | } 43 | 44 | func compareUserList(old, new []panel.UserInfo) (deleted, added []panel.UserInfo) { 45 | tmp := map[string]struct{}{} 46 | tmp2 := map[string]struct{}{} 47 | for i := range old { 48 | tmp[old[i].Uuid+strconv.Itoa(old[i].SpeedLimit)] = struct{}{} 49 | } 50 | l := len(tmp) 51 | for i := range new { 52 | e := new[i].Uuid + strconv.Itoa(new[i].SpeedLimit) 53 | tmp[e] = struct{}{} 54 | tmp2[e] = struct{}{} 55 | if l != len(tmp) { 56 | added = append(added, new[i]) 57 | l++ 58 | } 59 | } 60 | tmp = nil 61 | l = len(tmp2) 62 | for i := range old { 63 | tmp2[old[i].Uuid+strconv.Itoa(old[i].SpeedLimit)] = struct{}{} 64 | if l != len(tmp2) { 65 | deleted = append(deleted, old[i]) 66 | l++ 67 | } 68 | } 69 | return deleted, added 70 | } 71 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------