Compare commits

..

144 Commits

Author SHA1 Message Date
wyx2685
71277de69e fix: compile 2025-12-03 06:26:10 +09:00
wyx2685
f32c672c4d Merge branch 'dev_new' of github.com:wyx2685/V2bX into dev_new 2025-12-03 05:24:57 +09:00
LinboLen
33b7faba53 Merge pull request #120 from LinboLen/fix_limit_problem
fix: adjust rate limiting to use actual bytes transferred
2025-12-03 04:23:31 +08:00
wyx2685
8615620ce9 update xray v25.12.2 hysteria2 v2.6.4 2025-12-03 05:23:08 +09:00
wyx2685
98777eb1b5 Merge pull request #116 from darthstren/dev_new
添加 APISendIP 属性
2025-10-28 16:01:28 +08:00
darthstren
c656877311 添加 APISendIP 属性 2025-10-28 15:53:54 +08:00
wyx2685
3deccaae00 fix: sing/hy2内核流量用户为空 2025-09-20 23:15:58 +09:00
wyx2685
9bbccf772b fix: xray内核删除用户时重复加锁导致卡死 2025-09-19 01:05:50 +09:00
wyx2685
9483526f3c fix: xray内核开启splice导致限速失效 2025-09-19 00:44:21 +09:00
wyx2685
65f2de55ea fix: 流量用户为null,xray无法断开失效用户连接 2025-09-18 20:39:18 +09:00
wyx2685
4ed6bc6d87 fix mux panic 2025-09-12 16:53:21 +09:00
wyx2685
af7b473c13 update hysteria2 v2.6.3 2025-09-12 14:28:02 +09:00
wyx2685
10f66b57ea test: Add vless encryption 2025-09-12 14:26:58 +09:00
wyx2685
0824bf7a4e update README: go1.25构建指令 2025-08-23 01:10:51 +09:00
wyx2685
6e8297c553 test: 尝试启用xray splice拷贝 2025-08-22 05:22:47 +09:00
wyx2685
7d52a8932d test: update go 1.25.0 2025-08-13 21:37:42 +09:00
wyx2685
06441afa79 fix: 流量判断阈值去除=号 2025-08-07 15:46:12 +09:00
wyx2685
f7b588fb45 test: 增加MinReportTraffic最低流量上报阈值 2025-08-07 14:18:12 +09:00
wyx2685
9be082ede6 update xray 25.8.3 & sing-box 1.12.0 2025-08-05 00:28:47 +09:00
wyx2685
dce3ec1079 fix: update xray-core 25.7.24 2025-07-24 11:52:31 +09:00
wyx2685
2990999f7b 发布临时修复版本 2025-07-24 02:10:57 +09:00
wyx2685
fe003fcb19 回滚等待xray修复 2025-07-24 01:04:15 +09:00
wyx2685
ea26985d7c fix: hy2和tuic处理错误 2025-07-24 00:07:44 +09:00
wyx2685
32437f5e48 fix: 缺少mldsa65参数导致无法启动 2025-07-24 00:07:19 +09:00
wyx2685
39dfd8b6dd fix: xray内核使用自有连接时报空指针BUG 2025-07-23 22:02:48 +09:00
wyx2685
eb51d3e13c fix: sing内核reality server不能正常连接 2025-07-23 21:40:07 +09:00
wyx2685
dadeb6304b update xray core 2025-07-23 02:06:11 +09:00
wyx2685
4c7b9f5eb9 remove trash example 2025-07-19 04:05:59 +09:00
wyx2685
63d88843b6 test: Xray内核删除用户时尝试关闭连接 2025-07-16 15:49:17 +09:00
wyx2685
8d225f811b fix: 用户限速信息不能同步更新BUG 2025-07-13 09:23:06 +09:00
wyx2685
f6b587b275 prefer msgpack for userlist 2025-07-04 11:37:23 +09:00
wyx2685
d9b3d24465 update cores 2025-06-10 01:42:31 +09:00
wyx2685
fc284b3b9f fix: 设备数限制某些情况误拒绝旧IP链接BUG 2025-05-29 00:23:21 +09:00
wyx2685
eb92c4912d fix: reality 2025-05-24 18:53:11 +09:00
wyx2685
a68378670f test: add anytls 2025-05-24 08:33:36 +09:00
wyx2685
d200a3336e test: Add Tuic 2025-04-30 07:50:41 +09:00
wyx2685
6a95d576f1 Merge branch 'dev_new' of github.com:wyx2685/V2bX into dev_new 2025-04-01 06:03:05 +09:00
wyx2685
dbe529bd48 update xray-core v25.3.31 2025-04-01 06:01:52 +09:00
wyx2685
8254e49297 Update Dockerfile 2025-03-06 23:50:24 +08:00
wyx2685
9e8ad2619a Update Dockerfile 2025-03-06 23:48:14 +08:00
wyx2685
d5fff6c433 Update Dockerfile 2025-03-06 23:45:34 +08:00
wyx2685
fe896a61a3 update Dockerfile 2025-03-07 00:38:36 +09:00
wyx2685
a63198c20b Update release.yml 2025-03-06 23:34:14 +08:00
wyx2685
180fb14dd1 update xray core 2025-03-07 00:29:14 +09:00
wyx2685
44db7512d7 use submodule 2025-03-07 00:28:11 +09:00
wyx2685
95263fea99 update sing-box core 2025-03-06 23:54:00 +09:00
wyx2685
0b155bbf89 test: Singbox内核定期释放TCP会话列表内存 2025-03-06 15:57:56 +09:00
wyx2685
1c8c17b067 test: Singbox内核删除用户时尝试关闭该用户所有TCP会话 2025-03-05 19:54:06 +09:00
wyx2685
61606646b9 fix docker build 2025-03-04 21:53:05 +09:00
wyx2685
e2904ad126 fix workflow 2025-03-04 21:28:24 +09:00
wyx2685
903aef1fb5 Update Dockerfile 2025-03-04 19:08:18 +08:00
wyx2685
2c43704090 update xray-core v25.3.3 2025-03-04 05:05:02 +09:00
wyx2685
d71df3a0df hy2内核支持sniff设置 2025-03-02 23:19:27 +09:00
wyx2685
96baa0a99c fix docker workflows 2025-02-20 21:02:37 +09:00
wyx2685
e502624fe4 读取ignore_client_bandwidth参数 2025-02-15 22:09:56 +09:00
wyx2685
484faaf0c3 update sing-box core v1.12.0 2025-02-08 13:38:05 +09:00
wyx2685
ec5dcc3c8a update cores 2025-01-15 14:31:12 +09:00
wyx2685
2f1362067b sing内核支持面板rule规则 2025-01-10 16:33:05 +09:00
wyx2685
c755e9800b fix: sing内核删除用户逻辑不正确BUG 2025-01-05 20:57:18 +09:00
wyx2685
29e0d7e56e fix hy2 core logger nil pointer 2024-12-20 16:46:05 +09:00
wyx2685
08ebbed9fb update xray-core 2024-12-15 10:13:39 +09:00
wyx2685
792841d073 Fix reality 2024-12-13 20:31:06 +09:00
wyx2685
981e59b836 update sing-box core v1.11 2024-12-13 06:22:44 +09:00
wyx2685
7dbe5fda85 fix import 2024-12-02 08:54:47 +09:00
wyx2685
4f9ccdf8db update xray-core&hy2-core 2024-12-02 08:38:23 +09:00
wyx2685
c2d5861d7d splithttp传输方式更名为xhttp 2024-11-26 10:29:12 +09:00
wyx2685
0e29c19f0e fix compile 2024-10-31 17:57:06 +09:00
wyx2685
a1c40bb1c8 update cores 2024-10-31 17:46:31 +09:00
wyx2685
a0de94efff Merge pull request #30 from KorenKrita/dev_new
add sing mux config support
2024-10-31 16:21:22 +08:00
KorenKrita
29928a1135 add sing mux config support 2024-10-31 14:21:02 +08:00
wyx2685
ab1ca837de update action 2024-10-06 18:19:58 +09:00
wyx2685
1f61446fa9 update core 2024-10-06 18:13:16 +09:00
wyx2685
c0325227db update xray-core v24.9.19 2024-09-19 10:27:13 +09:00
wyx2685
ba3036a7ac 更新 bug-report.md 2024-09-18 09:57:58 +08:00
wyx2685
f99e2b4489 移除fr模板 2024-09-18 10:53:35 +09:00
wyx2685
0af952be10 Update README.md 2024-09-16 23:17:49 +08:00
wyx2685
ad5971f164 update xray-core v24.9.16 2024-09-16 22:00:08 +09:00
wyx2685
f7d5d891c3 Fix:修复获取用户列表和在线设备列表时未正常重置资源的BUG 2024-09-12 00:10:31 +09:00
wyx2685
6936a76724 更新xray-core版本,回退sing-box版本 2024-09-12 00:08:15 +09:00
wyx2685
7184e49650 尝试修复其他面板兼容问题 2024-09-03 01:03:37 +09:00
wyx2685
ea0b7d8f40 面板无设备数列表接口时暂不报错 2024-09-01 05:27:11 +09:00
wyx2685
12fbcb1460 修复MAP未初始化 2024-08-31 17:19:13 +09:00
wyx2685
c6d48e1edf 升级内核 2024-08-31 17:06:00 +09:00
wyx2685
8d7168c6a4 尝试修复在线设备数上报后有概率被限制链接 2024-08-31 16:02:50 +09:00
wyx2685
173c48a76f 调整设备数限制实现方式 2024-08-30 06:48:41 +09:00
wyx2685
130e94cf45 Merge branch 'InazumaV:dev_new' into dev_new 2024-08-29 09:49:15 +08:00
wyx2685
89ddfff060 升级sing-box内核 2024-08-14 01:48:33 +09:00
wyx2685
07d49293d8 BUG修复前不对UDP链接进行限速 2024-08-14 01:35:30 +09:00
wyx2685
9e8f87740e update xray-core 1.8.23 2024-07-30 01:28:39 +09:00
wyx2685
29a99985c8 Fix:优化流量统计并发情况可能存在的冲突 2024-07-28 13:54:47 +09:00
wyx2685
248ff3764f update sing-box core 2024-07-24 19:50:55 +09:00
wyx2685
3dfeba7e68 xray-core 1.8.21 2024-07-22 07:49:04 +09:00
wyx2685
8eb623b3f0 update xray core 2024-07-21 20:56:35 +09:00
wyx2685
cdcbddd464 update core 2024-07-21 06:14:16 +09:00
wyx2685
e81d47321b xray-core 1.8.19 2024-07-17 23:26:09 +09:00
wyx2685
4d82eff518 update Xray-core 1.8.18 2024-07-17 09:21:57 +09:00
wyx2685
b96545649b Fix:修复无法使用Sing-box内核urltest出口的问题 2024-07-15 00:33:54 +09:00
wyx2685
8757b955a6 修复hy2内核设备数统计错误的BUG 2024-07-08 20:32:52 +09:00
wyx2685
33d9ab4b0a 适应xray1.8.16更改 2024-07-08 18:12:09 +09:00
wyx2685
a7637d436f 更新所有内核 2024-07-08 17:44:22 +09:00
wyx2685
4dda65a636 Fix:修复Xray内核VMess/VLess的Websocket传输方式参数设置错误 2024-06-24 18:55:04 +09:00
wyx2685
6c725b424f 更新文档链接 2024-06-24 04:48:15 +09:00
wyx2685
a052a1f1e8 升级Xray内核,完善singbox内核自定义配置 2024-06-24 04:41:56 +09:00
wyx2685
85ad40d098 升级全部内核 2024-06-13 00:06:22 +09:00
riolu.rs
a85352c402 fix(xray.ss) ss2022 users 2024-05-02 22:53:09 +08:00
wyx2685
206af0216c 修复xray内核vless协议bug,升级xray/sing内核 2024-03-13 22:15:04 +09:00
wyx2685
ed5edda28a 修复Sing内核trojan用户列表BUG、修复用户设备限制不生效 2024-03-04 22:25:06 +09:00
wyx2685
995e606694 hy2内核增加自定义配置 2024-02-26 22:00:55 +09:00
wyx2685
96b457d679 修正singbox内核hy2用户列表问题 2024-02-21 13:02:13 +09:00
wyx2685
55b20f5550 升级内核. 2024-02-20 06:27:31 +09:00
wyx2685
c0b31837e4 update singbox 2024-02-16 22:00:06 +09:00
wyx2685
bf4a52df4d Merge branch 'dev_new' of github.com:wyx2685/V2bX into dev_new 2024-02-16 21:46:37 +09:00
wyx2685
423ac622b5 fix traffic counting 2024-02-16 21:46:23 +09:00
wyx2685
625265148f 更新 Dockerfile
Fix hysteria2 core
2024-02-08 13:52:37 +08:00
wyx2685
73f9b19222 增加本地节点Hash辅助判断信息 2024-02-06 20:26:30 +09:00
wyx2685
0d274bcf61 测试:增加hysteria2内核 2024-02-06 10:59:37 +09:00
wyx2685
77ec03016c 尝试解决内存泄漏问题 2024-01-28 01:04:47 +09:00
wyx2685
1d4945af8d 更新内核,尝试优化代码 2024-01-27 13:19:55 +09:00
wyx2685
f92c5b37d5 尝试优化代码 2024-01-27 01:55:43 +09:00
wyx2685
91e78fbc20 尝试优化代码 2024-01-25 23:06:37 +09:00
wyx2685
e292b3b0e7 升级sing和xray内核 2024-01-22 13:38:55 +09:00
wyx2685
c4d404d979 update Sing and Xray core 2024-01-08 00:58:21 +09:00
wyx2685
16221d17fb update singbox and xray 2024-01-02 00:33:13 +09:00
wyx2685
f33d7a3bf9 fix:trojan修复Trojan未应用传输方式 2023-12-19 00:21:07 +09:00
wyx2685
50183e70b3 Merge branch 'dev_new' of github.com:wyx2685/V2bX into dev_new 2023-12-19 00:17:28 +09:00
wyx2685
d19ca6863e 尝试添加trojan的ws/grpc传输 2023-12-19 00:17:16 +09:00
wyx2685
8d116b19d1 更新 Dockerfile 尝试修复启动问题 2023-12-18 13:36:22 +08:00
wyx2685
4ec6426234 Merge pull request #2 from rebecca554owen/dev_new
增加支持docker。
2023-12-17 22:56:11 +08:00
rebecca554owen
73b5c37d94 Create Publish Docker image.yml 2023-12-17 14:01:52 +08:00
rebecca554owen
e7997f9896 Create Dockerfile 2023-12-17 14:01:08 +08:00
wyx2685
1f6cccbb9f Try to fix vmess/vless tcp http 2023-12-17 02:58:13 +09:00
wyx2685
a25c3d2759 geoip和geosite更换为增强版 2023-12-15 03:31:50 +09:00
wyx2685
5aef9cf0de 增加dev_new分支push时编译 2023-12-15 03:25:21 +09:00
wyx2685
39972f5cf9 Merge pull request #1 from TheMrJC/patch-1
Update README.md typo
2023-12-14 18:09:02 +08:00
wyx2685
0af24e4646 更新sing依赖 2023-12-14 18:35:20 +09:00
JC
ea6ef41c60 Update README.md typo
remove the typo of the installation link
2023-12-13 12:43:22 +08:00
wyx2685
6d09332654 修复xray编译不通过 2023-12-12 20:10:13 +09:00
wyx2685
34d268dd6d 更新上游xray和singbox依赖 2023-12-12 09:06:55 +09:00
wyx2685
3416284953 升级xray和sing版本 2023-12-02 01:48:15 +09:00
wyx2685
00d581d823 更新sing-box依赖 2023-11-26 02:57:50 +09:00
wyx2685
41b7cf8eb9 尝试修复自动重载配置不成功的BUG 2023-11-24 18:16:44 +09:00
wyx2685
330f3b1449 尝试修复节点删除后仍可使用的BUG 2023-11-21 04:13:14 +09:00
wyx2685
c1f36bb2ae 尝试修复xray客户端使用xtls-rprx-vision时的udp行为 2023-11-19 19:43:52 +09:00
wyx2685
dd6a16402c fix:修复无法读取ProxyProtocol版本 2023-11-19 17:53:18 +09:00
74 changed files with 4411 additions and 2426 deletions

View File

@@ -1,27 +1,25 @@
--- ---
name: "Bug 反馈" name: "Bug 反馈"
about: 创建一个报告以帮助我们修复并改进XrayR about: 创建一个报告以帮助我们修复并改进V2bX
title: '' title: ''
labels: awaiting reply, bug labels:
assignees: '' assignees: ''
--- ---
**描述该错误** **描述该错误**
简单地描述一下这个bug是什么 简单地描述一下这个bug是什么
**复现**
复现该bug的步骤
**环境和版本**
- 系统 [例如Debian 11] **复现**
- 架构 [例如AMD64] 请自行复现,并贴出详细步骤操作过程
- 面板 [例如V2board]
- 协议 [例如vmess]
- 版本 [例如0.8.2.2]
- 部署方式 [例如:一键脚本]
**日志和错误** **日志和错误**
请使用`xrayr log`查看并添加日志,以帮助解释你的问题 请使用`v2bx log`查看并添加日志,没有日志的issue不会得到答复并且会被直接关闭
**额外的内容** **额外的内容**
在这里添加关于问题的任何其他内容 在这里添加关于问题的任何其他内容

View File

@@ -1,19 +0,0 @@
---
name: "功能建议"
about: 给XrayR提出建议让我们做得更好
title: ''
labels: awaiting reply, feature-request
assignees: ''
---
**描述您想要的功能**
清晰简洁的功能描述。
**描述您考虑过的替代方案**
是否有任何替代方案可以解决这个问题?
**附加上下文**
在此处添加有关功能请求的任何其他上下文或截图。

View File

@@ -0,0 +1,113 @@
name: Publish Docker image
on:
workflow_dispatch:
release:
types: [published]
pull_request:
branches:
- 'dev_new'
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository_owner }}/v2bx
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
platform:
- linux/amd64
- linux/arm64
steps:
- name: Prepare
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Checkout
uses: actions/checkout@v4
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push by digest
id: build
uses: docker/build-push-action@v6
with:
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
- name: Export digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
echo "${digest#sha256:}" > "/tmp/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: digests-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
merge:
runs-on: ubuntu-latest
needs:
- build
steps:
- name: Download digests
uses: actions/download-artifact@v4
with:
path: /tmp/digests
pattern: digests-*
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create manifest list and push
run: |
cd /tmp/digests
tags=$(echo '${{ steps.meta.outputs.json }}' | jq -cr '.tags | map("-t " + .) | join(" ")')
images=$(printf "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s " $(find . -type f -exec cat {} \;))
echo "Creating manifest with tags: $tags"
echo "Using images: $images"
docker buildx imagetools create $tags $images
- name: Inspect image
run: |
docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }}

View File

@@ -34,34 +34,38 @@ jobs:
# 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 # 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
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v1 uses: github/codeql-action/init@v2
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file. # By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file. # Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main # queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - name: Install Go
# If this step fails, then you should remove it and run the build manually (see below) uses: actions/setup-go@v4
- name: Autobuild with:
uses: github/codeql-action/autobuild@v1 go-version-file: go.mod
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell. # Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl # 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project # and modify them (or add more) to build your code if your project
# uses a compiled language # uses a compiled language
#- run: | #- run: |
# make bootstrap # make bootstrap
# make release # make release
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1 uses: github/codeql-action/analyze@v2

View File

@@ -5,18 +5,19 @@ on:
push: push:
branches: branches:
- master - master
- dev_new
paths: paths:
- "**/*.go" - "**/*.go"
- "go.mod" - "go.mod"
- "go.sum" - "go.sum"
- ".github/workflows/*.yml" - ".github/workflows/release.yml"
pull_request: pull_request:
types: [opened, synchronize, reopened] types: [opened, synchronize, reopened]
paths: paths:
- "**/*.go" - "**/*.go"
- "go.mod" - "go.mod"
- "go.sum" - "go.sum"
- ".github/workflows/*.yml" - ".github/workflows/release.yml"
release: release:
types: [published] types: [published]
@@ -97,7 +98,7 @@ jobs:
CGO_ENABLED: 0 CGO_ENABLED: 0
steps: steps:
- name: Checkout codebase - name: Checkout codebase
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Show workflow information - name: Show workflow information
id: get_filename id: get_filename
run: | run: |
@@ -106,12 +107,13 @@ jobs:
echo "ASSET_NAME=$_NAME" >> $GITHUB_OUTPUT echo "ASSET_NAME=$_NAME" >> $GITHUB_OUTPUT
echo "ASSET_NAME=$_NAME" >> $GITHUB_ENV echo "ASSET_NAME=$_NAME" >> $GITHUB_ENV
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v3 uses: actions/setup-go@v5
with: with:
go-version: '1.21.4' go-version: '1.25.0'
- name: Get project dependencies - name: Get project dependencies
run: go mod download run: |
go mod download
- name: Get release version - name: Get release version
if: ${{ github.event_name == 'release' }} if: ${{ github.event_name == 'release' }}
run: | run: |
@@ -124,13 +126,13 @@ jobs:
run: | run: |
echo "version: $version" echo "version: $version"
mkdir -p build_assets mkdir -p build_assets
go build -v -o build_assets/V2bX -tags "sing xray with_reality_server with_quic with_grpc with_utls with_wireguard with_acme" -trimpath -ldflags "-X 'github.com/InazumaV/V2bX/cmd.version=$version' -s -w -buildid=" GOEXPERIMENT=jsonv2 go build -v -o build_assets/V2bX -tags "sing xray hysteria2 with_quic with_grpc with_utls with_wireguard with_acme with_gvisor" -trimpath -ldflags "-X 'github.com/InazumaV/V2bX/cmd.version=$version' -s -w -buildid="
- name: Build Mips softfloat V2bX - name: Build Mips softfloat V2bX
if: matrix.goarch == 'mips' || matrix.goarch == 'mipsle' if: matrix.goarch == 'mips' || matrix.goarch == 'mipsle'
run: | run: |
echo "version: $version" echo "version: $version"
GOMIPS=softfloat go build -v -o build_assets/V2bX_softfloat -tags "sing xray with_reality_server with_quic with_grpc with_utls with_wireguard with_acme" -trimpath -ldflags "-X 'github.com/InazumaV/V2bX/cmd.version=$version' -s -w -buildid=" GOEXPERIMENT=jsonv2 GOMIPS=softfloat go build -v -o build_assets/V2bX_softfloat -tags "sing xray hysteria2 with_quic with_grpc with_utls with_wireguard with_acme with_gvisor" -trimpath -ldflags "-X 'github.com/InazumaV/V2bX/cmd.version=$version' -s -w -buildid="
- name: Rename Windows V2bX - name: Rename Windows V2bX
if: matrix.goos == 'windows' if: matrix.goos == 'windows'
run: | run: |
@@ -141,16 +143,13 @@ jobs:
cp ${GITHUB_WORKSPACE}/README.md ./build_assets/README.md cp ${GITHUB_WORKSPACE}/README.md ./build_assets/README.md
cp ${GITHUB_WORKSPACE}/LICENSE ./build_assets/LICENSE cp ${GITHUB_WORKSPACE}/LICENSE ./build_assets/LICENSE
cp ${GITHUB_WORKSPACE}/example/*.json ./build_assets/ cp ${GITHUB_WORKSPACE}/example/*.json ./build_assets/
LIST=('geoip geoip geoip' 'domain-list-community dlc geosite') LIST=('geoip' 'geosite')
for i in "${LIST[@]}" for i in "${LIST[@]}"
do do
INFO=($(echo $i | awk 'BEGIN{FS=" ";OFS=" "} {print $1,$2,$3}')) DOWNLOAD_URL="https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/${i}.dat"
FILE_NAME="${INFO[2]}.dat" FILE_NAME="${i}.dat"
echo -e "Downloading ${FILE_NAME}..." echo -e "Downloading ${DOWNLOAD_URL}..."
curl -L "https://github.com/v2fly/${INFO[0]}/releases/latest/download/${INFO[1]}.dat" -o ./build_assets/${FILE_NAME} curl -L "${DOWNLOAD_URL}" -o ./build_assets/${FILE_NAME}
echo -e "Verifying HASH key..."
HASH="$(curl -sL "https://github.com/v2fly/${INFO[0]}/releases/latest/download/${INFO[1]}.dat.sha256sum" | awk -F ' ' '{print $1}')"
[ "$(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; }
done done
- name: Create ZIP archive - name: Create ZIP archive
shell: bash shell: bash
@@ -169,7 +168,7 @@ jobs:
run: | run: |
mv build_assets V2bX-$ASSET_NAME mv build_assets V2bX-$ASSET_NAME
- name: Upload files to Artifacts - name: Upload files to Artifacts
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: V2bX-${{ steps.get_filename.outputs.ASSET_NAME }} name: V2bX-${{ steps.get_filename.outputs.ASSET_NAME }}
path: | path: |

3
.gitignore vendored
View File

@@ -12,4 +12,7 @@ app/legocmd/.lego/
example/.lego example/.lego
example/cert example/cert
./vscode ./vscode
output/*
.idea/* .idea/*
newV2bX.sh
quic_singbox.sh

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "sing-box_mod"]
path = sing-box_mod
url = https://github.com/wyx2685/sing-box_mod.git

17
Dockerfile Normal file
View File

@@ -0,0 +1,17 @@
# Build go
FROM golang:1.25.0-alpine AS builder
WORKDIR /app
COPY . .
ENV CGO_ENABLED=0
RUN GOEXPERIMENT=jsonv2 go mod download
RUN GOEXPERIMENT=jsonv2 go build -v -o V2bX -tags "sing xray hysteria2 with_quic with_grpc with_utls with_wireguard with_acme with_gvisor"
# Release
FROM alpine
# 安装必要的工具包
RUN apk --update --no-cache add tzdata ca-certificates \
&& cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
RUN mkdir /etc/V2bX/
COPY --from=builder /app/V2bX /usr/local/bin
ENTRYPOINT [ "V2bX", "server", "--config", "/etc/V2bX/config.json"]

View File

@@ -1,16 +1,17 @@
# V2bX # V2bX
[![](https://img.shields.io/badge/TgChat-%E4%BA%A4%E6%B5%81%E7%BE%A4-blue)](https://t.me/YuzukiProjects) [![](https://img.shields.io/badge/TgChat-UnOfficialV2Board%E4%BA%A4%E6%B5%81%E7%BE%A4-green)](https://t.me/unofficialV2board)
[![](https://img.shields.io/badge/TgChat-YuzukiProjects%E4%BA%A4%E6%B5%81%E7%BE%A4-blue)](https://t.me/YuzukiProjects)
A V2board node server based on multi core, modified from XrayR. A V2board node server based on multi core, modified from XrayR.
一个基于多种内核的V2board节点服务端修改自XrayR支持V2ay,Trojan,Shadowsocks协议。 一个基于多种内核的V2board节点服务端修改自XrayR支持V2ay,Trojan,Shadowsocks协议。
**注意: 本项目需要V2board版本 >= 1.7.0** **注意: 本项目需要搭配[修改版V2board](https://github.com/wyx2685/v2board)**
## 特点 ## 特点
* 永久开源且免费。 * 永久开源且免费。
* 支持Vmess/Vless, Trojan Shadowsocks, Hysteria多种协议。 * 支持Vmess/Vless, Trojan Shadowsocks, Hysteria1/2多种协议。
* 支持Vless和XTLS等新特性。 * 支持Vless和XTLS等新特性。
* 支持单实例对接多节点,无需重复启动。 * 支持单实例对接多节点,无需重复启动。
* 支持限制在线IP。 * 支持限制在线IP。
@@ -23,7 +24,7 @@ A V2board node server based on multi core, modified from XrayR.
## 功能介绍 ## 功能介绍
| 功能 | v2ray | trojan | shadowsocks | hysteria | | 功能 | v2ray | trojan | shadowsocks | hysteria1/2 |
|-----------|-------|--------|-------------|----------| |-----------|-------|--------|-------------|----------|
| 自动申请tls证书 | √ | √ | √ | √ | | 自动申请tls证书 | √ | √ | √ | √ |
| 自动续签tls证书 | √ | √ | √ | √ | | 自动续签tls证书 | √ | √ | √ | √ |
@@ -32,14 +33,13 @@ A V2board node server based on multi core, modified from XrayR.
| 自定义DNS | √ | √ | √ | √ | | 自定义DNS | √ | √ | √ | √ |
| 在线IP数限制 | √ | √ | √ | √ | | 在线IP数限制 | √ | √ | √ | √ |
| 连接数限制 | √ | √ | √ | √ | | 连接数限制 | √ | √ | √ | √ |
| 跨节点IP数限制 | | | | | | 跨节点IP数限制 | | | | |
| 按照用户限速 | √ | √ | √ | √ | | 按照用户限速 | √ | √ | √ | √ |
| 动态限速(未测试) | √ | √ | √ | √ | | 动态限速(未测试) | √ | √ | √ | √ |
## TODO ## TODO
- [ ] 重新实现动态限速 - [ ] 重新实现动态限速
- [ ] 重新实现在线IP同步跨节点在线IP限制
- [ ] 完善使用文档 - [ ] 完善使用文档
## 软件安装 ## 软件安装
@@ -47,22 +47,22 @@ A V2board node server based on multi core, modified from XrayR.
### 一键安装 ### 一键安装
``` ```
wget -N https://raw.githubusercontents.com/wyx2685/V2bX-script/master/install.sh && bash install.sh wget -N https://raw.githubusercontent.com/wyx2685/V2bX-script/master/install.sh && bash install.sh
``` ```
### 手动安装 ### 手动安装
[手动安装教程(过时待更新)](https://yuzuki-1.gitbook.io/v2bx-doc/xrayr-xia-zai-he-an-zhuang/install/manual) [手动安装教程](https://v2bx.v-50.me/v2bx/v2bx-xia-zai-he-an-zhuang/install/manual)
## 构建 ## 构建
``` bash ``` bash
# 通过-tags选项指定要编译的内核 可选 xray sing # 通过-tags选项指定要编译的内核 可选 xray sing, hysteria2
go build -o V2bX -ldflags '-s -w' -gcflags="all=-trimpath=${PWD}" -asmflags="all=-trimpath=${PWD} -tags "xray sing" GOEXPERIMENT=jsonv2 go build -v -o build_assets/V2bX -tags "sing xray hysteria2 with_quic with_grpc with_utls with_wireguard with_acme with_gvisor" -trimpath -ldflags "-X 'github.com/InazumaV/V2bX/cmd.version=$version' -s -w -buildid="
``` ```
## 配置文件及详细使用教程 ## 配置文件及详细使用教程
[详细使用教程](https://yuzuki-1.gitbook.io/v2bx-doc/) [详细使用教程](https://v2bx.v-50.me/)
## 免责声明 ## 免责声明
@@ -71,6 +71,10 @@ go build -o V2bX -ldflags '-s -w' -gcflags="all=-trimpath=${PWD}" -asmflags="all
* 本人不对任何人使用本项目造成的任何后果承担责任。 * 本人不对任何人使用本项目造成的任何后果承担责任。
* 本人比较多变,因此本项目可能会随想法或思路的变动随性更改项目结构或大规模重构代码,若不能接受请勿使用。 * 本人比较多变,因此本项目可能会随想法或思路的变动随性更改项目结构或大规模重构代码,若不能接受请勿使用。
## 赞助
[赞助链接](https://v-50.me/)
## Thanks ## Thanks
* [Project X](https://github.com/XTLS/) * [Project X](https://github.com/XTLS/)
@@ -82,4 +86,4 @@ go build -o V2bX -ldflags '-s -w' -gcflags="all=-trimpath=${PWD}" -asmflags="all
## Stars 增长记录 ## Stars 增长记录
[![Stargazers over time](https://starchart.cc/InazumaV/V2bX.svg)](https://starchart.cc/InazumaV/V2bX) [![Stargazers over time](https://starchart.cc/wyx2685/V2bX.svg)](https://starchart.cc/wyx2685/V2bX)

View File

@@ -1,15 +1,15 @@
package panel package panel
import ( import (
"encoding/base64" "crypto/sha256"
"encoding/hex"
"fmt" "fmt"
"reflect" "reflect"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/InazumaV/V2bX/common/crypt" "encoding/json"
"github.com/goccy/go-json"
) )
// Security type // Security type
@@ -32,6 +32,8 @@ type NodeInfo struct {
VAllss *VAllssNode VAllss *VAllssNode
Shadowsocks *ShadowsocksNode Shadowsocks *ShadowsocksNode
Trojan *TrojanNode Trojan *TrojanNode
Tuic *TuicNode
AnyTls *AnyTlsNode
Hysteria *HysteriaNode Hysteria *HysteriaNode
Hysteria2 *Hysteria2Node Hysteria2 *Hysteria2Node
Common *CommonNode Common *CommonNode
@@ -65,6 +67,8 @@ type VAllssNode struct {
Network string `json:"network"` Network string `json:"network"`
NetworkSettings json.RawMessage `json:"network_settings"` NetworkSettings json.RawMessage `json:"network_settings"`
NetworkSettingsBack json.RawMessage `json:"networkSettings"` NetworkSettingsBack json.RawMessage `json:"networkSettings"`
Encryption string `json:"encryption"`
EncryptionSettings EncSettings `json:"encryption_settings"`
ServerName string `json:"server_name"` ServerName string `json:"server_name"`
// vless only // vless only
@@ -73,12 +77,20 @@ type VAllssNode struct {
} }
type TlsSettings struct { type TlsSettings struct {
ServerName string `json:"server_name"` ServerName string `json:"server_name"`
Dest string `json:"dest"` Dest string `json:"dest"`
ServerPort string `json:"server_port"` ServerPort string `json:"server_port"`
ShortId string `json:"short_id"` ShortId string `json:"short_id"`
PrivateKey string `json:"private_key"` PrivateKey string `json:"private_key"`
Xver uint8 `json:"xver"` Mldsa65Seed string `json:"mldsa65Seed"`
Xver uint64 `json:"xver,string"`
}
type EncSettings struct {
Mode string `json:"mode"`
Ticket string `json:"ticket"`
ServerPadding string `json:"server_padding"`
PrivateKey string `json:"private_key"`
} }
type RealityConfig struct { type RealityConfig struct {
@@ -94,7 +106,22 @@ type ShadowsocksNode struct {
ServerKey string `json:"server_key"` ServerKey string `json:"server_key"`
} }
type TrojanNode CommonNode type TrojanNode struct {
CommonNode
Network string `json:"network"`
NetworkSettings json.RawMessage `json:"networkSettings"`
}
type TuicNode struct {
CommonNode
CongestionControl string `json:"congestion_control"`
ZeroRTTHandshake bool `json:"zero_rtt_handshake"`
}
type AnyTlsNode struct {
CommonNode
PaddingScheme []string `json:"padding_scheme,omitempty"`
}
type HysteriaNode struct { type HysteriaNode struct {
CommonNode CommonNode
@@ -105,10 +132,11 @@ type HysteriaNode struct {
type Hysteria2Node struct { type Hysteria2Node struct {
CommonNode CommonNode
UpMbps int `json:"up_mbps"` Ignore_Client_Bandwidth bool `json:"ignore_client_bandwidth"`
DownMbps int `json:"down_mbps"` UpMbps int `json:"up_mbps"`
ObfsType string `json:"obfs"` DownMbps int `json:"down_mbps"`
ObfsPassword string `json:"obfs-password"` ObfsType string `json:"obfs"`
ObfsPassword string `json:"obfs-password"`
} }
type RawDNS struct { type RawDNS struct {
@@ -126,13 +154,32 @@ func (c *Client) GetNodeInfo() (node *NodeInfo, err error) {
r, err := c.client. r, err := c.client.
R(). R().
SetHeader("If-None-Match", c.nodeEtag). SetHeader("If-None-Match", c.nodeEtag).
ForceContentType("application/json").
Get(path) Get(path)
if err = c.checkResponse(r, path, err); err != nil {
return
}
if r.StatusCode() == 304 { if r.StatusCode() == 304 {
return nil, nil return nil, nil
} }
hash := sha256.Sum256(r.Body())
newBodyHash := hex.EncodeToString(hash[:])
if c.responseBodyHash == newBodyHash {
return nil, nil
}
c.responseBodyHash = newBodyHash
c.nodeEtag = r.Header().Get("ETag")
if err = c.checkResponse(r, path, err); err != nil {
return nil, err
}
if r != nil {
defer func() {
if r.RawBody() != nil {
r.RawBody().Close()
}
}()
} else {
return nil, fmt.Errorf("received nil response")
}
node = &NodeInfo{ node = &NodeInfo{
Id: c.NodeId, Id: c.NodeId,
Type: c.NodeType, Type: c.NodeType,
@@ -161,18 +208,6 @@ func (c *Client) GetNodeInfo() (node *NodeInfo, err error) {
cm = &rsp.CommonNode cm = &rsp.CommonNode
node.VAllss = rsp node.VAllss = rsp
node.Security = node.VAllss.Tls node.Security = node.VAllss.Tls
if len(rsp.NetworkSettings) > 0 {
err = json.Unmarshal(rsp.NetworkSettings, &rsp.RealityConfig)
if err != nil {
return nil, fmt.Errorf("decode reality config error: %s", err)
}
}
if node.Security == Reality {
if rsp.TlsSettings.PrivateKey == "" {
key := crypt.GenX25519Private([]byte("vless" + c.Token))
rsp.TlsSettings.PrivateKey = base64.RawURLEncoding.EncodeToString(key)
}
}
case "shadowsocks": case "shadowsocks":
rsp := &ShadowsocksNode{} rsp := &ShadowsocksNode{}
err = json.Unmarshal(r.Body(), rsp) err = json.Unmarshal(r.Body(), rsp)
@@ -188,9 +223,27 @@ func (c *Client) GetNodeInfo() (node *NodeInfo, err error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("decode trojan params error: %s", err) return nil, fmt.Errorf("decode trojan params error: %s", err)
} }
cm = (*CommonNode)(rsp) cm = &rsp.CommonNode
node.Trojan = rsp node.Trojan = rsp
node.Security = Tls node.Security = Tls
case "tuic":
rsp := &TuicNode{}
err = json.Unmarshal(r.Body(), rsp)
if err != nil {
return nil, fmt.Errorf("decode tuic params error: %s", err)
}
cm = &rsp.CommonNode
node.Tuic = rsp
node.Security = Tls
case "anytls":
rsp := &AnyTlsNode{}
err = json.Unmarshal(r.Body(), rsp)
if err != nil {
return nil, fmt.Errorf("decode anytls params error: %s", err)
}
cm = &rsp.CommonNode
node.AnyTls = rsp
node.Security = Tls
case "hysteria": case "hysteria":
rsp := &HysteriaNode{} rsp := &HysteriaNode{}
err = json.Unmarshal(r.Body(), rsp) err = json.Unmarshal(r.Body(), rsp)
@@ -238,9 +291,7 @@ func (c *Client) GetNodeInfo() (node *NodeInfo, err error) {
} }
case "dns": case "dns":
var domains []string var domains []string
for _, v := range matchs { domains = append(domains, matchs...)
domains = append(domains, v)
}
if matchs[0] != "main" { if matchs[0] != "main" {
node.RawDNS.DNSMap[strconv.Itoa(i)] = map[string]interface{}{ node.RawDNS.DNSMap[strconv.Itoa(i)] = map[string]interface{}{
"address": cm.Routes[i].ActionValue, "address": cm.Routes[i].ActionValue,
@@ -249,7 +300,6 @@ func (c *Client) GetNodeInfo() (node *NodeInfo, err error) {
} else { } else {
dns := []byte(strings.Join(matchs[1:], "")) dns := []byte(strings.Join(matchs[1:], ""))
node.RawDNS.DNSJson = dns node.RawDNS.DNSJson = dns
break
} }
} }
} }
@@ -263,8 +313,7 @@ func (c *Client) GetNodeInfo() (node *NodeInfo, err error) {
cm.Routes = nil cm.Routes = nil
cm.BaseConfig = nil cm.BaseConfig = nil
c.nodeEtag = r.Header().Get("ETag") return node, nil
return
} }
func intervalToTime(i interface{}) time.Duration { func intervalToTime(i interface{}) time.Duration {

View File

@@ -3,6 +3,7 @@ package panel
import ( import (
"errors" "errors"
"fmt" "fmt"
"net"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@@ -18,16 +19,26 @@ import (
type Client struct { type Client struct {
client *resty.Client client *resty.Client
APIHost string APIHost string
APISendIP string
Token string Token string
NodeType string NodeType string
NodeId int NodeId int
nodeEtag string nodeEtag string
userEtag string userEtag string
LastReportOnline map[int]int responseBodyHash string
UserList *UserListBody
AliveMap *AliveMap
} }
func New(c *conf.ApiConfig) (*Client, error) { func New(c *conf.ApiConfig) (*Client, error) {
client := resty.New() var client *resty.Client
if c.APISendIP != "" {
client = resty.NewWithLocalAddr(&net.TCPAddr{
IP: net.ParseIP(c.APISendIP),
})
} else {
client = resty.New()
}
client.SetRetryCount(3) client.SetRetryCount(3)
if c.Timeout > 0 { if c.Timeout > 0 {
client.SetTimeout(time.Duration(c.Timeout) * time.Second) client.SetTimeout(time.Duration(c.Timeout) * time.Second)
@@ -54,6 +65,8 @@ func New(c *conf.ApiConfig) (*Client, error) {
"shadowsocks", "shadowsocks",
"hysteria", "hysteria",
"hysteria2", "hysteria2",
"tuic",
"anytls",
"vless": "vless":
default: default:
return nil, fmt.Errorf("unsupported Node type: %s", c.NodeType) return nil, fmt.Errorf("unsupported Node type: %s", c.NodeType)
@@ -65,10 +78,13 @@ func New(c *conf.ApiConfig) (*Client, error) {
"token": c.Key, "token": c.Key,
}) })
return &Client{ return &Client{
client: client, client: client,
Token: c.Key, Token: c.Key,
APIHost: c.APIHost, APIHost: c.APIHost,
NodeType: c.NodeType, APISendIP: c.APISendIP,
NodeId: c.NodeID, NodeType: c.NodeType,
NodeId: c.NodeID,
UserList: &UserListBody{},
AliveMap: &AliveMap{},
}, nil }, nil
} }

View File

@@ -2,8 +2,12 @@ package panel
import ( import (
"fmt" "fmt"
"strings"
"github.com/goccy/go-json" "encoding/json/jsontext"
"encoding/json/v2"
"github.com/vmihailenco/msgpack/v5"
) )
type OnlineUser struct { type OnlineUser struct {
@@ -12,62 +16,103 @@ type OnlineUser struct {
} }
type UserInfo struct { type UserInfo struct {
Id int `json:"id"` Id int `json:"id" msgpack:"id"`
Uuid string `json:"uuid"` Uuid string `json:"uuid" msgpack:"uuid"`
SpeedLimit int `json:"speed_limit"` SpeedLimit int `json:"speed_limit" msgpack:"speed_limit"`
DeviceLimit int `json:"device_limit"` DeviceLimit int `json:"device_limit" msgpack:"device_limit"`
AliveIp int `json:"alive_ip"`
} }
type UserListBody struct { type UserListBody struct {
//Msg string `json:"msg"` Users []UserInfo `json:"users" msgpack:"users"`
Users []UserInfo `json:"users"`
} }
// GetUserList will pull user form sspanel type AliveMap struct {
func (c *Client) GetUserList() (UserList []UserInfo, err error) { Alive map[int]int `json:"alive"`
}
// GetUserList will pull user from v2board
func (c *Client) GetUserList() ([]UserInfo, error) {
const path = "/api/v1/server/UniProxy/user" const path = "/api/v1/server/UniProxy/user"
r, err := c.client.R(). r, err := c.client.R().
SetHeader("If-None-Match", c.userEtag). SetHeader("If-None-Match", c.userEtag).
SetHeader("X-Response-Format", "msgpack").
SetDoNotParseResponse(true).
Get(path) Get(path)
err = c.checkResponse(r, path, err) if r == nil || r.RawResponse == nil {
if err != nil { return nil, fmt.Errorf("received nil response or raw response")
return nil, err
} }
defer r.RawResponse.Body.Close()
if r.StatusCode() == 304 { if r.StatusCode() == 304 {
return nil, nil return nil, nil
} }
var userList *UserListBody
err = json.Unmarshal(r.Body(), &userList) if err = c.checkResponse(r, path, err); err != nil {
if err != nil { return nil, err
return nil, fmt.Errorf("unmarshal userlist error: %s", err)
} }
c.userEtag = r.Header().Get("ETag") userlist := &UserListBody{}
if strings.Contains(r.Header().Get("Content-Type"), "application/x-msgpack") {
var userinfos []UserInfo decoder := msgpack.NewDecoder(r.RawResponse.Body)
var localDeviceLimit int = 0 if err := decoder.Decode(userlist); err != nil {
for _, user := range userList.Users { return nil, fmt.Errorf("decode user list error: %w", err)
// If there is still device available, add the user }
if user.DeviceLimit > 0 && user.AliveIp > 0 { } else {
lastOnline := 0 dec := jsontext.NewDecoder(r.RawResponse.Body)
if v, ok := c.LastReportOnline[user.Id]; ok { for {
lastOnline = v tok, err := dec.ReadToken()
if err != nil {
return nil, fmt.Errorf("decode user list error: %w", err)
} }
// If there are any available device. if tok.Kind() == '"' && tok.String() == "users" {
localDeviceLimit = user.DeviceLimit - user.AliveIp + lastOnline break
if localDeviceLimit > 0 {
} else if lastOnline > 0 {
} else {
continue
} }
} }
userinfos = append(userinfos, user) tok, err := dec.ReadToken()
if err != nil {
return nil, fmt.Errorf("decode user list error: %w", err)
}
if tok.Kind() != '[' {
return nil, fmt.Errorf(`decode user list error: expected "users" array`)
}
for dec.PeekKind() != ']' {
val, err := dec.ReadValue()
if err != nil {
return nil, fmt.Errorf("decode user list error: read user object: %w", err)
}
var u UserInfo
if err := json.Unmarshal(val, &u); err != nil {
return nil, fmt.Errorf("decode user list error: unmarshal user error: %w", err)
}
userlist.Users = append(userlist.Users, u)
}
}
c.userEtag = r.Header().Get("ETag")
return userlist.Users, nil
}
// GetUserAlive will fetch the alive_ip count for users
func (c *Client) GetUserAlive() (map[int]int, error) {
c.AliveMap = &AliveMap{}
const path = "/api/v1/server/UniProxy/alivelist"
r, err := c.client.R().
ForceContentType("application/json").
Get(path)
if err != nil || r.StatusCode() >= 399 {
c.AliveMap.Alive = make(map[int]int)
return c.AliveMap.Alive, nil
}
if r == nil || r.RawResponse == nil {
fmt.Printf("received nil response or raw response")
c.AliveMap.Alive = make(map[int]int)
return c.AliveMap.Alive, nil
}
defer r.RawResponse.Body.Close()
if err := json.Unmarshal(r.Body(), c.AliveMap); err != nil {
fmt.Printf("unmarshal user alive list error: %s", err)
c.AliveMap.Alive = make(map[int]int)
} }
return userinfos, nil return c.AliveMap.Alive, nil
} }
type UserTraffic struct { type UserTraffic struct {
@@ -94,8 +139,7 @@ func (c *Client) ReportUserTraffic(userTraffic []UserTraffic) error {
return nil return nil
} }
func (c *Client) ReportNodeOnlineUsers(data *map[int][]string, reportOnline *map[int]int) error { func (c *Client) ReportNodeOnlineUsers(data *map[int][]string) error {
c.LastReportOnline = *reportOnline
const path = "/api/v1/server/UniProxy/alive" const path = "/api/v1/server/UniProxy/alive"
r, err := c.client.R(). r, err := c.client.R().
SetBody(data). SetBody(data).

View File

@@ -1,17 +1,17 @@
package cmd package cmd
import ( import (
"os"
"os/signal"
"runtime"
"syscall"
"github.com/InazumaV/V2bX/conf" "github.com/InazumaV/V2bX/conf"
vCore "github.com/InazumaV/V2bX/core" vCore "github.com/InazumaV/V2bX/core"
"github.com/InazumaV/V2bX/limiter" "github.com/InazumaV/V2bX/limiter"
"github.com/InazumaV/V2bX/node" "github.com/InazumaV/V2bX/node"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"gopkg.in/natefinch/lumberjack.v2"
"os"
"os/signal"
"runtime"
"syscall"
) )
var ( var (
@@ -55,14 +55,11 @@ func serverHandle(_ *cobra.Command, _ []string) {
log.SetLevel(log.ErrorLevel) log.SetLevel(log.ErrorLevel)
} }
if c.LogConfig.Output != "" { if c.LogConfig.Output != "" {
w := &lumberjack.Logger{ f, err := os.OpenFile(c.LogConfig.Output, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
Filename: c.LogConfig.Output, if err != nil {
MaxSize: 100, log.WithField("err", err).Error("Open log file failed, using stdout instead")
MaxBackups: 3,
MaxAge: 28,
Compress: true,
} }
log.SetOutput(w) log.SetOutput(f)
} }
limiter.Init() limiter.Init()
log.Info("Start V2bX...") log.Info("Start V2bX...")
@@ -124,7 +121,7 @@ func serverHandle(_ *cobra.Command, _ []string) {
// wait exit signal // wait exit signal
{ {
osSignals := make(chan os.Signal, 1) osSignals := make(chan os.Signal, 1)
signal.Notify(osSignals, syscall.SIGINT, syscall.SIGKILL, syscall.SIGTERM) signal.Notify(osSignals, syscall.SIGINT, syscall.SIGTERM)
<-osSignals <-osSignals
} }
} }

View File

@@ -2,9 +2,7 @@ package cmd
import ( import (
"fmt" "fmt"
"strings"
vCore "github.com/InazumaV/V2bX/core"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -35,8 +33,8 @@ _/ _/ _/ _/ _/ _/
_/ _/_/_/_/ _/_/_/ _/ _/ _/ _/_/_/_/ _/_/_/ _/ _/
`) `)
fmt.Printf("%s %s (%s) \n", codename, version, intro) fmt.Printf("%s %s (%s) \n", codename, version, intro)
fmt.Printf("Supported cores: %s\n", strings.Join(vCore.RegisteredCore(), ", ")) //fmt.Printf("Supported cores: %s\n", strings.Join(vCore.RegisteredCore(), ", "))
// Warning // Warning
fmt.Println(Warn("This version need V2board version >= 1.7.0.")) //fmt.Println(Warn("This version need V2board version >= 1.7.0."))
fmt.Println(Warn("The version have many changed for config, please check your config file")) //fmt.Println(Warn("The version have many changed for config, please check your config file"))
} }

View File

@@ -6,8 +6,7 @@ import (
) )
type TrafficCounter struct { type TrafficCounter struct {
counters map[string]*TrafficStorage Counters sync.Map
lock sync.RWMutex
} }
type TrafficStorage struct { type TrafficStorage struct {
@@ -16,76 +15,60 @@ type TrafficStorage struct {
} }
func NewTrafficCounter() *TrafficCounter { func NewTrafficCounter() *TrafficCounter {
return &TrafficCounter{ return &TrafficCounter{}
counters: map[string]*TrafficStorage{},
}
} }
func (c *TrafficCounter) GetCounter(id string) *TrafficStorage { func (c *TrafficCounter) GetCounter(uuid string) *TrafficStorage {
c.lock.RLock() if cts, ok := c.Counters.Load(uuid); ok {
cts, ok := c.counters[id] return cts.(*TrafficStorage)
c.lock.RUnlock()
if !ok {
cts = &TrafficStorage{}
c.counters[id] = cts
} }
return cts newStorage := &TrafficStorage{}
if cts, loaded := c.Counters.LoadOrStore(uuid, newStorage); loaded {
return cts.(*TrafficStorage)
}
return newStorage
} }
func (c *TrafficCounter) GetUpCount(id string) int64 { func (c *TrafficCounter) GetUpCount(uuid string) int64 {
c.lock.RLock() if cts, ok := c.Counters.Load(uuid); ok {
cts, ok := c.counters[id] return cts.(*TrafficStorage).UpCounter.Load()
c.lock.RUnlock()
if ok {
return cts.UpCounter.Load()
} }
return 0 return 0
} }
func (c *TrafficCounter) GetDownCount(id string) int64 { func (c *TrafficCounter) GetDownCount(uuid string) int64 {
c.lock.RLock() if cts, ok := c.Counters.Load(uuid); ok {
cts, ok := c.counters[id] return cts.(*TrafficStorage).DownCounter.Load()
c.lock.RUnlock()
if ok {
return cts.DownCounter.Load()
} }
return 0 return 0
} }
func (c *TrafficCounter) Len() int { func (c *TrafficCounter) Len() int {
c.lock.RLock() length := 0
defer c.lock.RUnlock() c.Counters.Range(func(_, _ interface{}) bool {
return len(c.counters) length++
return true
})
return length
} }
func (c *TrafficCounter) Reset(id string) { func (c *TrafficCounter) Reset(uuid string) {
c.lock.RLock() if cts, ok := c.Counters.Load(uuid); ok {
cts := c.GetCounter(id) cts.(*TrafficStorage).UpCounter.Store(0)
c.lock.RUnlock() cts.(*TrafficStorage).DownCounter.Store(0)
cts.UpCounter.Store(0) }
cts.DownCounter.Store(0)
} }
func (c *TrafficCounter) Delete(id string) { func (c *TrafficCounter) Delete(uuid string) {
c.lock.Lock() c.Counters.Delete(uuid)
delete(c.counters, id)
c.lock.Unlock()
} }
func (c *TrafficCounter) Rx(id string, n int) { func (c *TrafficCounter) Rx(uuid string, n int) {
cts := c.GetCounter(id) cts := c.GetCounter(uuid)
cts.DownCounter.Add(int64(n)) cts.DownCounter.Add(int64(n))
} }
func (c *TrafficCounter) Tx(id string, n int) { func (c *TrafficCounter) Tx(uuid string, n int) {
cts := c.GetCounter(id) cts := c.GetCounter(uuid)
cts.UpCounter.Add(int64(n)) cts.UpCounter.Add(int64(n))
} }
func (c *TrafficCounter) IncConn(auth string) {
return
}
func (c *TrafficCounter) DecConn(auth string) {
return
}

View File

@@ -0,0 +1,25 @@
package counter
import (
"sync/atomic"
fstats "github.com/xtls/xray-core/features/stats"
)
var _ fstats.Counter = (*XrayTrafficCounter)(nil)
type XrayTrafficCounter struct {
V *atomic.Int64
}
func (c *XrayTrafficCounter) Value() int64 {
return c.V.Load()
}
func (c *XrayTrafficCounter) Set(newValue int64) int64 {
return c.V.Swap(newValue)
}
func (c *XrayTrafficCounter) Add(delta int64) int64 {
return c.V.Add(delta)
}

View File

@@ -4,9 +4,6 @@ import (
"net" "net"
"github.com/juju/ratelimit" "github.com/juju/ratelimit"
"github.com/sagernet/sing/common/buf"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/common/network"
) )
func NewConnRateLimiter(c net.Conn, l *ratelimit.Bucket) *Conn { func NewConnRateLimiter(c net.Conn, l *ratelimit.Bucket) *Conn {
@@ -22,35 +19,17 @@ type Conn struct {
} }
func (c *Conn) Read(b []byte) (n int, err error) { func (c *Conn) Read(b []byte) (n int, err error) {
c.limiter.Wait(int64(len(b))) n, err = c.Conn.Read(b)
return c.Conn.Read(b) if n > 0 {
c.limiter.Wait(int64(n))
}
return n, err
} }
func (c *Conn) Write(b []byte) (n int, err error) { func (c *Conn) Write(b []byte) (n int, err error) {
c.limiter.Wait(int64(len(b))) n, err = c.Conn.Write(b)
return c.Conn.Write(b) if n > 0 {
} c.limiter.Wait(int64(n))
type PacketConnCounter struct {
network.PacketConn
limiter *ratelimit.Bucket
}
func NewPacketConnCounter(conn network.PacketConn, l *ratelimit.Bucket) network.PacketConn {
return &PacketConnCounter{
PacketConn: conn,
limiter: l,
} }
} return n, err
func (p *PacketConnCounter) ReadPacket(buff *buf.Buffer) (destination M.Socksaddr, err error) {
pLen := buff.Len()
destination, err = p.PacketConn.ReadPacket(buff)
p.limiter.Wait(int64(buff.Len() - pLen))
return
}
func (p *PacketConnCounter) WritePacket(buff *buf.Buffer, destination M.Socksaddr) (err error) {
p.limiter.Wait(int64(buff.Len()))
return p.PacketConn.WritePacket(buff, destination)
} }

View File

@@ -5,49 +5,14 @@ import (
"time" "time"
) )
// Task is a task that runs periodically.
type Task struct { type Task struct {
// Interval of the task being run
Interval time.Duration Interval time.Duration
// Execute is the task function Execute func() error
Execute func() error access sync.Mutex
running bool
access sync.Mutex stop chan struct{}
timer *time.Timer
running bool
} }
func (t *Task) hasClosed() bool {
t.access.Lock()
defer t.access.Unlock()
return !t.running
}
func (t *Task) checkedExecute(first bool) error {
if t.hasClosed() {
return nil
}
t.access.Lock()
defer t.access.Unlock()
if first {
if err := t.Execute(); err != nil {
t.running = false
return err
}
}
if !t.running {
return nil
}
t.timer = time.AfterFunc(t.Interval, func() {
t.checkedExecute(true)
})
return nil
}
// Start implements common.Runnable.
func (t *Task) Start(first bool) error { func (t *Task) Start(first bool) error {
t.access.Lock() t.access.Lock()
if t.running { if t.running {
@@ -55,24 +20,45 @@ func (t *Task) Start(first bool) error {
return nil return nil
} }
t.running = true t.running = true
t.stop = make(chan struct{})
t.access.Unlock() t.access.Unlock()
if err := t.checkedExecute(first); err != nil {
t.access.Lock() go func() {
t.running = false if first {
t.access.Unlock() if err := t.Execute(); err != nil {
return err t.access.Lock()
} t.running = false
close(t.stop)
t.access.Unlock()
return
}
}
for {
select {
case <-time.After(t.Interval):
case <-t.stop:
return
}
if err := t.Execute(); err != nil {
t.access.Lock()
t.running = false
close(t.stop)
t.access.Unlock()
return
}
}
}()
return nil return nil
} }
// Close implements common.Closable.
func (t *Task) Close() { func (t *Task) Close() {
t.access.Lock() t.access.Lock()
defer t.access.Unlock() if t.running {
t.running = false
t.running = false close(t.stop)
if t.timer != nil {
t.timer.Stop()
t.timer = nil
} }
t.access.Unlock()
} }

View File

@@ -2,10 +2,12 @@ package conf
import ( import (
"fmt" "fmt"
"github.com/InazumaV/V2bX/common/json5" "io"
"os" "os"
"github.com/goccy/go-json" "github.com/InazumaV/V2bX/common/json5"
"encoding/json/v2"
) )
type Conf struct { type Conf struct {
@@ -29,5 +31,17 @@ func (p *Conf) LoadFromPath(filePath string) error {
return fmt.Errorf("open config file error: %s", err) return fmt.Errorf("open config file error: %s", err)
} }
defer f.Close() defer f.Close()
return json.NewDecoder(json5.NewTrimNodeReader(f)).Decode(p)
reader := json5.NewTrimNodeReader(f)
data, err := io.ReadAll(reader)
if err != nil {
return fmt.Errorf("read config file error: %s", err)
}
err = json.Unmarshal(data, p)
if err != nil {
return fmt.Errorf("unmarshal config error: %s", err)
}
return nil
} }

View File

@@ -5,10 +5,11 @@ import (
) )
type CoreConfig struct { type CoreConfig struct {
Type string `json:"Type"` Type string `json:"Type"`
Name string `json:"Name"` Name string `json:"Name"`
XrayConfig *XrayConfig `json:"-"` XrayConfig *XrayConfig `json:"-"`
SingConfig *SingConfig `json:"-"` SingConfig *SingConfig `json:"-"`
Hysteria2Config *Hysteria2Config `json:"-"`
} }
type _CoreConfig CoreConfig type _CoreConfig CoreConfig
@@ -25,6 +26,9 @@ func (c *CoreConfig) UnmarshalJSON(b []byte) error {
case "sing": case "sing":
c.SingConfig = NewSingConfig() c.SingConfig = NewSingConfig()
return json.Unmarshal(b, c.SingConfig) return json.Unmarshal(b, c.SingConfig)
case "hysteria2":
c.Hysteria2Config = NewHysteria2Config()
return json.Unmarshal(b, c.Hysteria2Config)
} }
return nil return nil
} }

17
conf/hy.go Normal file
View File

@@ -0,0 +1,17 @@
package conf
type Hysteria2Config struct {
LogConfig Hysteria2LogConfig `json:"Log"`
}
type Hysteria2LogConfig struct {
Level string `json:"Level"`
}
func NewHysteria2Config() *Hysteria2Config {
return &Hysteria2Config{
LogConfig: Hysteria2LogConfig{
Level: "error",
},
}
}

View File

@@ -7,8 +7,9 @@ import (
"os" "os"
"strings" "strings"
"encoding/json"
"github.com/InazumaV/V2bX/common/json5" "github.com/InazumaV/V2bX/common/json5"
"github.com/goccy/go-json"
) )
type NodeConfig struct { type NodeConfig struct {
@@ -24,6 +25,7 @@ type rawNodeConfig struct {
type ApiConfig struct { type ApiConfig struct {
APIHost string `json:"ApiHost"` APIHost string `json:"ApiHost"`
APISendIP string `json:"ApiSendIP"`
NodeID int `json:"NodeID"` NodeID int `json:"NodeID"`
Key string `json:"ApiKey"` Key string `json:"ApiKey"`
NodeType string `json:"NodeType"` NodeType string `json:"NodeType"`
@@ -109,10 +111,12 @@ type Options struct {
ListenIP string `json:"ListenIP"` ListenIP string `json:"ListenIP"`
SendIP string `json:"SendIP"` SendIP string `json:"SendIP"`
DeviceOnlineMinTraffic int64 `json:"DeviceOnlineMinTraffic"` DeviceOnlineMinTraffic int64 `json:"DeviceOnlineMinTraffic"`
ReportMinTraffic int64 `json:"ReportMinTraffic"`
LimitConfig LimitConfig `json:"LimitConfig"` LimitConfig LimitConfig `json:"LimitConfig"`
RawOptions json.RawMessage `json:"RawOptions"` RawOptions json.RawMessage `json:"RawOptions"`
XrayOptions *XrayOptions `json:"XrayOptions"` XrayOptions *XrayOptions `json:"XrayOptions"`
SingOptions *SingOptions `json:"SingOptions"` SingOptions *SingOptions `json:"SingOptions"`
Hysteria2ConfigPath string `json:"Hysteria2ConfigPath"`
CertConfig *CertConfig `json:"CertConfig"` CertConfig *CertConfig `json:"CertConfig"`
} }
@@ -129,6 +133,9 @@ func (o *Options) UnmarshalJSON(data []byte) error {
case "sing": case "sing":
o.SingOptions = NewSingOptions() o.SingOptions = NewSingOptions()
return json.Unmarshal(data, o.SingOptions) return json.Unmarshal(data, o.SingOptions)
case "hysteria2":
o.RawOptions = data
return nil
default: default:
o.Core = "" o.Core = ""
o.RawOptions = data o.RawOptions = data

View File

@@ -5,11 +5,9 @@ import (
) )
type SingConfig struct { type SingConfig struct {
LogConfig SingLogConfig `json:"Log"` LogConfig SingLogConfig `json:"Log"`
NtpConfig SingNtpConfig `json:"NTP"` NtpConfig SingNtpConfig `json:"NTP"`
EnableConnClear bool `json:"EnableConnClear"` OriginalPath string `json:"OriginalPath"`
DnsConfigPath string `json:"DnsConfigPath"`
OriginalPath string `json:"OriginalPath"`
} }
type SingLogConfig struct { type SingLogConfig struct {
@@ -34,13 +32,13 @@ func NewSingConfig() *SingConfig {
} }
type SingOptions struct { type SingOptions struct {
EnableProxyProtocol bool `json:"EnableProxyProtocol"`
TCPFastOpen bool `json:"EnableTFO"` TCPFastOpen bool `json:"EnableTFO"`
SniffEnabled bool `json:"EnableSniff"` SniffEnabled bool `json:"EnableSniff"`
SniffOverrideDestination bool `json:"SniffOverrideDestination"`
EnableDNS bool `json:"EnableDNS"` EnableDNS bool `json:"EnableDNS"`
DomainStrategy option.DomainStrategy `json:"DomainStrategy"` DomainStrategy option.DomainStrategy `json:"DomainStrategy"`
SniffOverrideDestination bool `json:"SniffOverrideDestination"`
FallBackConfigs *FallBackConfigForSing `json:"FallBackConfigs"` FallBackConfigs *FallBackConfigForSing `json:"FallBackConfigs"`
Multiplex *MultiplexConfig `json:"MultiplexConfig"`
} }
type SingNtpConfig struct { type SingNtpConfig struct {
@@ -60,13 +58,25 @@ type FallBack struct {
ServerPort string `json:"ServerPort"` ServerPort string `json:"ServerPort"`
} }
type MultiplexConfig struct {
Enabled bool `json:"Enable"`
Padding bool `json:"Padding"`
Brutal BrutalOptions `json:"Brutal"`
}
type BrutalOptions struct {
Enabled bool `json:"Enable"`
UpMbps int `json:"UpMbps"`
DownMbps int `json:"DownMbps"`
}
func NewSingOptions() *SingOptions { func NewSingOptions() *SingOptions {
return &SingOptions{ return &SingOptions{
EnableDNS: false, EnableDNS: false,
EnableProxyProtocol: false,
TCPFastOpen: false, TCPFastOpen: false,
SniffEnabled: true, SniffEnabled: true,
SniffOverrideDestination: true, SniffOverrideDestination: true,
FallBackConfigs: &FallBackConfigForSing{}, FallBackConfigs: &FallBackConfigForSing{},
Multiplex: &MultiplexConfig{},
} }
} }

View File

@@ -2,12 +2,12 @@ package conf
import ( import (
"fmt" "fmt"
"github.com/fsnotify/fsnotify"
"log" "log"
"path"
"path/filepath" "path/filepath"
"strings" "strings"
"time" "time"
"github.com/fsnotify/fsnotify"
) )
func (p *Conf) Watch(filePath, xDnsPath string, sDnsPath string, reload func()) error { func (p *Conf) Watch(filePath, xDnsPath string, sDnsPath string, reload func()) error {
@@ -34,7 +34,7 @@ func (p *Conf) Watch(filePath, xDnsPath string, sDnsPath string, reload func())
case filepath.Base(xDnsPath), filepath.Base(sDnsPath): case filepath.Base(xDnsPath), filepath.Base(sDnsPath):
log.Println("DNS file changed, reloading...") log.Println("DNS file changed, reloading...")
default: default:
log.Println("config dir changed, reloading...") log.Println("config file changed, reloading...")
} }
*p = *New() *p = *New()
err := p.LoadFromPath(filePath) err := p.LoadFromPath(filePath)
@@ -51,18 +51,18 @@ func (p *Conf) Watch(filePath, xDnsPath string, sDnsPath string, reload func())
} }
} }
}() }()
err = watcher.Add(path.Dir(filePath)) err = watcher.Add(filePath)
if err != nil { if err != nil {
return fmt.Errorf("watch file error: %s", err) return fmt.Errorf("watch file error: %s", err)
} }
if xDnsPath != "" { if xDnsPath != "" {
err = watcher.Add(path.Dir(xDnsPath)) err = watcher.Add(xDnsPath)
if err != nil { if err != nil {
return fmt.Errorf("watch dns file error: %s", err) return fmt.Errorf("watch dns file error: %s", err)
} }
} }
if sDnsPath != "" { if sDnsPath != "" {
err = watcher.Add(path.Dir(sDnsPath)) err = watcher.Add(sDnsPath)
if err != nil { if err != nil {
return fmt.Errorf("watch dns file error: %s", err) return fmt.Errorf("watch dns file error: %s", err)
} }

View File

@@ -11,7 +11,7 @@ var (
) )
func NewCore(c []conf.CoreConfig) (Core, error) { func NewCore(c []conf.CoreConfig) (Core, error) {
if len(c) < 0 { if len(c) == 0 {
return nil, errors.New("no have vail core") return nil, errors.New("no have vail core")
} }
// multi core // multi core

461
core/hy2/config.go Normal file
View File

@@ -0,0 +1,461 @@
package hy2
import (
"crypto/tls"
"fmt"
"net"
"net/http"
"net/http/httputil"
"net/url"
"strconv"
"strings"
"time"
"github.com/InazumaV/V2bX/api/panel"
"github.com/InazumaV/V2bX/conf"
"github.com/apernet/hysteria/core/v2/server"
"github.com/apernet/hysteria/extras/v2/correctnet"
"github.com/apernet/hysteria/extras/v2/masq"
"github.com/apernet/hysteria/extras/v2/obfs"
"github.com/apernet/hysteria/extras/v2/outbounds"
"github.com/apernet/hysteria/extras/v2/sniff"
eUtils "github.com/apernet/hysteria/extras/v2/utils"
"go.uber.org/zap"
)
type masqHandlerLogWrapper struct {
H http.Handler
QUIC bool
Logger *zap.Logger
}
func (m *masqHandlerLogWrapper) ServeHTTP(w http.ResponseWriter, r *http.Request) {
m.Logger.Debug("masquerade request",
zap.String("addr", r.RemoteAddr),
zap.String("method", r.Method),
zap.String("host", r.Host),
zap.String("url", r.URL.String()),
zap.Bool("quic", m.QUIC))
m.H.ServeHTTP(w, r)
}
const (
Byte = 1
Kilobyte = Byte * 1000
Megabyte = Kilobyte * 1000
Gigabyte = Megabyte * 1000
Terabyte = Gigabyte * 1000
)
const (
defaultStreamReceiveWindow = 8388608 // 8MB
defaultConnReceiveWindow = defaultStreamReceiveWindow * 5 / 2 // 20MB
defaultMaxIdleTimeout = 30 * time.Second
defaultMaxIncomingStreams = 4096
defaultUDPIdleTimeout = 60 * time.Second
)
func (n *Hysteria2node) getTLSConfig(config *conf.Options) (*server.TLSConfig, error) {
if config.CertConfig == nil {
return nil, fmt.Errorf("the CertConfig is not vail")
}
switch config.CertConfig.CertMode {
case "none", "":
return nil, fmt.Errorf("the CertMode cannot be none")
default:
var certs []tls.Certificate
cert, err := tls.LoadX509KeyPair(config.CertConfig.CertFile, config.CertConfig.KeyFile)
if err != nil {
return nil, err
}
certs = append(certs, cert)
return &server.TLSConfig{
Certificates: certs,
GetCertificate: func(tlsinfo *tls.ClientHelloInfo) (*tls.Certificate, error) {
cert, err := tls.LoadX509KeyPair(config.CertConfig.CertFile, config.CertConfig.KeyFile)
return &cert, err
},
}, nil
}
}
func (n *Hysteria2node) getQUICConfig(config *serverConfig) (*server.QUICConfig, error) {
quic := &server.QUICConfig{}
if config.QUIC.InitStreamReceiveWindow == 0 {
quic.InitialStreamReceiveWindow = defaultStreamReceiveWindow
} else if config.QUIC.InitStreamReceiveWindow < 16384 {
return nil, fmt.Errorf("QUICConfig.InitialStreamReceiveWindowf must be at least 16384")
} else {
quic.InitialConnectionReceiveWindow = config.QUIC.InitConnectionReceiveWindow
}
if config.QUIC.MaxStreamReceiveWindow == 0 {
quic.MaxStreamReceiveWindow = defaultStreamReceiveWindow
} else if config.QUIC.MaxStreamReceiveWindow < 16384 {
return nil, fmt.Errorf("QUICConfig.MaxStreamReceiveWindowf must be at least 16384")
} else {
quic.MaxStreamReceiveWindow = config.QUIC.MaxStreamReceiveWindow
}
if config.QUIC.InitConnectionReceiveWindow == 0 {
quic.InitialConnectionReceiveWindow = defaultConnReceiveWindow
} else if config.QUIC.InitConnectionReceiveWindow < 16384 {
return nil, fmt.Errorf("QUICConfig.InitialConnectionReceiveWindowf must be at least 16384")
} else {
quic.InitialConnectionReceiveWindow = config.QUIC.InitConnectionReceiveWindow
}
if config.QUIC.MaxConnectionReceiveWindow == 0 {
quic.MaxConnectionReceiveWindow = defaultConnReceiveWindow
} else if config.QUIC.MaxConnectionReceiveWindow < 16384 {
return nil, fmt.Errorf("QUICConfig.MaxConnectionReceiveWindowf must be at least 16384")
} else {
quic.MaxConnectionReceiveWindow = config.QUIC.MaxConnectionReceiveWindow
}
if config.QUIC.MaxIdleTimeout == 0 {
quic.MaxIdleTimeout = defaultMaxIdleTimeout
} else if config.QUIC.MaxIdleTimeout < 4*time.Second || config.QUIC.MaxIdleTimeout > 120*time.Second {
return nil, fmt.Errorf("QUICConfig.MaxIdleTimeoutf must be between 4s and 120s")
} else {
quic.MaxIdleTimeout = config.QUIC.MaxIdleTimeout
}
if config.QUIC.MaxIncomingStreams == 0 {
quic.MaxIncomingStreams = defaultMaxIncomingStreams
} else if config.QUIC.MaxIncomingStreams < 8 {
return nil, fmt.Errorf("QUICConfig.MaxIncomingStreamsf must be at least 8")
} else {
quic.MaxIncomingStreams = config.QUIC.MaxIncomingStreams
}
// todo fix !linux && !windows && !darwin
quic.DisablePathMTUDiscovery = false
return quic, nil
}
func (n *Hysteria2node) getConn(info *panel.NodeInfo, config *conf.Options) (net.PacketConn, error) {
uAddr, err := net.ResolveUDPAddr("udp", formatAddress(config.ListenIP, info.Common.ServerPort))
if err != nil {
return nil, err
}
conn, err := correctnet.ListenUDP("udp", uAddr)
if err != nil {
return nil, err
}
switch strings.ToLower(info.Hysteria2.ObfsType) {
case "", "plain":
return conn, nil
case "salamander":
ob, err := obfs.NewSalamanderObfuscator([]byte(info.Hysteria2.ObfsPassword))
if err != nil {
return nil, err
}
return obfs.WrapPacketConn(conn, ob), nil
default:
return nil, fmt.Errorf("unsupported obfuscation type")
}
}
func (n *Hysteria2node) getBandwidthConfig(info *panel.NodeInfo) *server.BandwidthConfig {
band := &server.BandwidthConfig{}
if info.Hysteria2.UpMbps != 0 {
band.MaxTx = (uint64)(info.Hysteria2.UpMbps * Megabyte / 8)
}
if info.Hysteria2.DownMbps != 0 {
band.MaxRx = (uint64)(info.Hysteria2.DownMbps * Megabyte / 8)
}
return band
}
func (n *Hysteria2node) getRequestHook(c *serverConfig) (server.RequestHook, error) {
if c.Sniff.Enable {
s := &sniff.Sniffer{
Timeout: c.Sniff.Timeout,
RewriteDomain: c.Sniff.RewriteDomain,
}
if c.Sniff.TCPPorts != "" {
s.TCPPorts = eUtils.ParsePortUnion(c.Sniff.TCPPorts)
if s.TCPPorts == nil {
return nil, fmt.Errorf("sniff.tcpPorts: invalid port union")
}
}
if c.Sniff.UDPPorts != "" {
s.UDPPorts = eUtils.ParsePortUnion(c.Sniff.UDPPorts)
if s.UDPPorts == nil {
return nil, fmt.Errorf("sniff.udpPorts: invalid port union")
}
}
return s, nil
}
return nil, nil
}
func (n *Hysteria2node) getOutboundConfig(c *serverConfig) (server.Outbound, error) {
// Resolver, ACL, actual outbound are all implemented through the Outbound interface.
// Depending on the config, we build a chain like this:
// Resolver(ACL(Outbounds...))
// Outbounds
var obs []outbounds.OutboundEntry
if len(c.Outbounds) == 0 {
// Guarantee we have at least one outbound
obs = []outbounds.OutboundEntry{{
Name: "default",
Outbound: outbounds.NewDirectOutboundSimple(outbounds.DirectOutboundModeAuto),
}}
} else {
obs = make([]outbounds.OutboundEntry, len(c.Outbounds))
for i, entry := range c.Outbounds {
if entry.Name == "" {
return nil, fmt.Errorf("empty outbound name")
}
var ob outbounds.PluggableOutbound
var err error
switch strings.ToLower(entry.Type) {
case "direct":
ob, err = serverConfigOutboundDirectToOutbound(entry.Direct)
case "socks5":
ob, err = serverConfigOutboundSOCKS5ToOutbound(entry.SOCKS5)
case "http":
ob, err = serverConfigOutboundHTTPToOutbound(entry.HTTP)
default:
err = fmt.Errorf("outbounds.type unsupported outbound type")
}
if err != nil {
return nil, err
}
obs[i] = outbounds.OutboundEntry{Name: entry.Name, Outbound: ob}
}
}
var uOb outbounds.PluggableOutbound // "unified" outbound
// ACL
hasACL := false
if c.ACL.File != "" && len(c.ACL.Inline) > 0 {
return nil, fmt.Errorf("cannot set both acl.file and acl.inline")
}
gLoader := &GeoLoader{
GeoIPFilename: c.ACL.GeoIP,
GeoSiteFilename: c.ACL.GeoSite,
UpdateInterval: c.ACL.GeoUpdateInterval,
Logger: n.Logger,
}
if c.ACL.File != "" {
hasACL = true
acl, err := outbounds.NewACLEngineFromFile(c.ACL.File, obs, gLoader)
if err != nil {
return nil, err
}
uOb = acl
} else if len(c.ACL.Inline) > 0 {
n.Logger.Debug("found ACL Inline:", zap.Strings("Inline", c.ACL.Inline))
hasACL = true
acl, err := outbounds.NewACLEngineFromString(strings.Join(c.ACL.Inline, "\n"), obs, gLoader)
if err != nil {
return nil, err
}
uOb = acl
} else {
// No ACL, use the first outbound
uOb = obs[0].Outbound
}
switch strings.ToLower(c.Resolver.Type) {
case "", "system":
if hasACL {
// If the user uses ACL, we must put a resolver in front of it,
// for IP rules to work on domain requests.
uOb = outbounds.NewSystemResolver(uOb)
}
// Otherwise we can just rely on outbound handling on its own.
case "tcp":
if c.Resolver.TCP.Addr == "" {
return nil, fmt.Errorf("empty resolver address")
}
uOb = outbounds.NewStandardResolverTCP(c.Resolver.TCP.Addr, c.Resolver.TCP.Timeout, uOb)
case "udp":
if c.Resolver.UDP.Addr == "" {
return nil, fmt.Errorf("empty resolver address")
}
uOb = outbounds.NewStandardResolverUDP(c.Resolver.UDP.Addr, c.Resolver.UDP.Timeout, uOb)
case "tls", "tcp-tls":
if c.Resolver.TLS.Addr == "" {
return nil, fmt.Errorf("empty resolver address")
}
uOb = outbounds.NewStandardResolverTLS(c.Resolver.TLS.Addr, c.Resolver.TLS.Timeout, c.Resolver.TLS.SNI, c.Resolver.TLS.Insecure, uOb)
case "https", "http":
if c.Resolver.HTTPS.Addr == "" {
return nil, fmt.Errorf("empty resolver address")
}
uOb = outbounds.NewDoHResolver(c.Resolver.HTTPS.Addr, c.Resolver.HTTPS.Timeout, c.Resolver.HTTPS.SNI, c.Resolver.HTTPS.Insecure, uOb)
default:
return nil, fmt.Errorf("unsupported resolver type")
}
Outbound := &outbounds.PluggableOutboundAdapter{PluggableOutbound: uOb}
return Outbound, nil
}
func (n *Hysteria2node) getMasqHandler(tlsconfig *server.TLSConfig, conn net.PacketConn, c *serverConfig) (http.Handler, error) {
var handler http.Handler
switch strings.ToLower(c.Masquerade.Type) {
case "", "404":
handler = http.NotFoundHandler()
case "file":
if c.Masquerade.File.Dir == "" {
return nil, fmt.Errorf("masquerade.file.dir empty file directory")
}
handler = http.FileServer(http.Dir(c.Masquerade.File.Dir))
case "proxy":
if c.Masquerade.Proxy.URL == "" {
return nil, fmt.Errorf("masquerade.proxy.url empty proxy url")
}
u, err := url.Parse(c.Masquerade.Proxy.URL)
if err != nil {
return nil, fmt.Errorf("masquerade.proxy.url %s", err)
}
handler = &httputil.ReverseProxy{
Director: func(req *http.Request) {
req.URL.Scheme = u.Scheme
req.URL.Host = u.Host
if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil {
xff := req.Header.Get("X-Forwarded-For")
if xff != "" {
clientIP = xff + ", " + clientIP
}
req.Header.Set("X-Forwarded-For", clientIP)
}
if c.Masquerade.Proxy.RewriteHost {
req.Host = req.URL.Host
}
},
ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) {
n.Logger.Error("HTTP reverse proxy error", zap.Error(err))
w.WriteHeader(http.StatusBadGateway)
},
}
case "string":
if c.Masquerade.String.Content == "" {
return nil, fmt.Errorf("masquerade.string.content empty string content")
}
if c.Masquerade.String.StatusCode != 0 &&
(c.Masquerade.String.StatusCode < 200 ||
c.Masquerade.String.StatusCode > 599 ||
c.Masquerade.String.StatusCode == 233) {
// 233 is reserved for Hysteria authentication
return nil, fmt.Errorf("masquerade.string.statusCode invalid status code (must be 200-599, except 233)")
}
handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
for k, v := range c.Masquerade.String.Headers {
w.Header().Set(k, v)
}
if c.Masquerade.String.StatusCode != 0 {
w.WriteHeader(c.Masquerade.String.StatusCode)
} else {
w.WriteHeader(http.StatusOK) // Use 200 OK by default
}
_, _ = w.Write([]byte(c.Masquerade.String.Content))
})
default:
return nil, fmt.Errorf("masquerade.type unsupported masquerade type")
}
MasqHandler := &masqHandlerLogWrapper{H: handler, QUIC: true, Logger: n.Logger}
if c.Masquerade.ListenHTTP != "" || c.Masquerade.ListenHTTPS != "" {
if c.Masquerade.ListenHTTP != "" && c.Masquerade.ListenHTTPS == "" {
return nil, fmt.Errorf("masquerade.listenHTTPS having only HTTP server without HTTPS is not supported")
}
s := masq.MasqTCPServer{
QUICPort: extractPortFromAddr(conn.LocalAddr().String()),
HTTPSPort: extractPortFromAddr(c.Masquerade.ListenHTTPS),
Handler: &masqHandlerLogWrapper{H: handler, QUIC: false, Logger: n.Logger},
TLSConfig: &tls.Config{
Certificates: tlsconfig.Certificates,
GetCertificate: tlsconfig.GetCertificate,
},
ForceHTTPS: c.Masquerade.ForceHTTPS,
}
go runMasqTCPServer(&s, c.Masquerade.ListenHTTP, c.Masquerade.ListenHTTPS, n.Logger)
}
return MasqHandler, nil
}
func (n *Hysteria2node) getHyConfig(info *panel.NodeInfo, config *conf.Options, c *serverConfig) (*server.Config, error) {
tls, err := n.getTLSConfig(config)
if err != nil {
return nil, err
}
quic, err := n.getQUICConfig(c)
if err != nil {
return nil, err
}
conn, err := n.getConn(info, config)
if err != nil {
return nil, err
}
sniff, err := n.getRequestHook(c)
if err != nil {
return nil, err
}
Outbound, err := n.getOutboundConfig(c)
if err != nil {
return nil, err
}
Masq, err := n.getMasqHandler(tls, conn, c)
if err != nil {
return nil, err
}
return &server.Config{
TLSConfig: *tls,
QUICConfig: *quic,
Conn: conn,
RequestHook: sniff,
Outbound: Outbound,
BandwidthConfig: *n.getBandwidthConfig(info),
IgnoreClientBandwidth: info.Hysteria2.Ignore_Client_Bandwidth,
DisableUDP: c.DisableUDP,
UDPIdleTimeout: c.UDPIdleTimeout,
EventLogger: n.EventLogger,
TrafficLogger: n.TrafficLogger,
MasqHandler: Masq,
}, nil
}
func runMasqTCPServer(s *masq.MasqTCPServer, httpAddr, httpsAddr string, logger *zap.Logger) {
errChan := make(chan error, 2)
if httpAddr != "" {
go func() {
logger.Info("masquerade HTTP server up and running", zap.String("listen", httpAddr))
errChan <- s.ListenAndServeHTTP(httpAddr)
}()
}
if httpsAddr != "" {
go func() {
logger.Info("masquerade HTTPS server up and running", zap.String("listen", httpsAddr))
errChan <- s.ListenAndServeHTTPS(httpsAddr)
}()
}
err := <-errChan
if err != nil {
logger.Fatal("failed to serve masquerade HTTP(S)", zap.Error(err))
}
}
func extractPortFromAddr(addr string) int {
_, portStr, err := net.SplitHostPort(addr)
if err != nil {
return 0
}
port, err := strconv.Atoi(portStr)
if err != nil {
return 0
}
return port
}
func formatAddress(ip string, port int) string {
if strings.Contains(ip, ":") {
return fmt.Sprintf("[%s]:%d", ip, port)
}
return fmt.Sprintf("%s:%d", ip, port)
}

181
core/hy2/geoloader.go Normal file
View File

@@ -0,0 +1,181 @@
package hy2
import (
"fmt"
"io"
"net/http"
"os"
"time"
"github.com/apernet/hysteria/extras/v2/outbounds/acl"
"github.com/apernet/hysteria/extras/v2/outbounds/acl/v2geo"
"go.uber.org/zap"
)
const (
geoipFilename = "geoip.dat"
geoipURL = "https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/geoip.dat"
geositeFilename = "geosite.dat"
geositeURL = "https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/geosite.dat"
geoDlTmpPattern = ".hysteria-geoloader.dlpart.*"
geoDefaultUpdateInterval = 7 * 24 * time.Hour // 7 days
)
var _ acl.GeoLoader = (*GeoLoader)(nil)
// GeoLoader provides the on-demand GeoIP/GeoSite database
// loading functionality required by the ACL engine.
// Empty filenames = automatic download from built-in URLs.
type GeoLoader struct {
GeoIPFilename string
GeoSiteFilename string
UpdateInterval time.Duration
geoipMap map[string]*v2geo.GeoIP
geositeMap map[string]*v2geo.GeoSite
Logger *zap.Logger
}
func (l *GeoLoader) shouldDownload(filename string) bool {
info, err := os.Stat(filename)
if os.IsNotExist(err) {
return true
}
if info.Size() == 0 {
// empty files are loadable by v2geo, but we consider it broken
return true
}
dt := time.Since(info.ModTime())
if l.UpdateInterval == 0 {
return dt > geoDefaultUpdateInterval
} else {
return dt > l.UpdateInterval
}
}
func (l *GeoLoader) downloadAndCheck(filename, url string, checkFunc func(filename string) error) error {
l.geoDownloadFunc(filename, url)
resp, err := http.Get(url)
if err != nil {
l.geoDownloadErrFunc(err)
return err
}
defer resp.Body.Close()
f, err := os.CreateTemp(".", geoDlTmpPattern)
if err != nil {
l.geoDownloadErrFunc(err)
return err
}
defer os.Remove(f.Name())
_, err = io.Copy(f, resp.Body)
if err != nil {
f.Close()
l.geoDownloadErrFunc(err)
return err
}
f.Close()
err = checkFunc(f.Name())
if err != nil {
l.geoDownloadErrFunc(fmt.Errorf("integrity check failed: %w", err))
return err
}
err = os.Rename(f.Name(), filename)
if err != nil {
l.geoDownloadErrFunc(fmt.Errorf("rename failed: %w", err))
return err
}
return nil
}
func (l *GeoLoader) LoadGeoIP() (map[string]*v2geo.GeoIP, error) {
if l.geoipMap != nil {
return l.geoipMap, nil
}
autoDL := false
filename := l.GeoIPFilename
if filename == "" {
autoDL = true
filename = geoipFilename
}
if autoDL {
if !l.shouldDownload(filename) {
m, err := v2geo.LoadGeoIP(filename)
if err == nil {
l.geoipMap = m
return m, nil
}
// file is broken, download it again
}
err := l.downloadAndCheck(filename, geoipURL, func(filename string) error {
_, err := v2geo.LoadGeoIP(filename)
return err
})
if err != nil {
// as long as the previous download exists, fallback to it
if _, serr := os.Stat(filename); os.IsNotExist(serr) {
return nil, err
}
}
}
m, err := v2geo.LoadGeoIP(filename)
if err != nil {
return nil, err
}
l.geoipMap = m
return m, nil
}
func (l *GeoLoader) LoadGeoSite() (map[string]*v2geo.GeoSite, error) {
if l.geositeMap != nil {
return l.geositeMap, nil
}
autoDL := false
filename := l.GeoSiteFilename
if filename == "" {
autoDL = true
filename = geositeFilename
}
if autoDL {
if !l.shouldDownload(filename) {
m, err := v2geo.LoadGeoSite(filename)
if err == nil {
l.geositeMap = m
return m, nil
}
// file is broken, download it again
}
err := l.downloadAndCheck(filename, geositeURL, func(filename string) error {
_, err := v2geo.LoadGeoSite(filename)
return err
})
if err != nil {
// as long as the previous download exists, fallback to it
if _, serr := os.Stat(filename); os.IsNotExist(serr) {
return nil, err
}
}
}
m, err := v2geo.LoadGeoSite(filename)
if err != nil {
return nil, err
}
l.geositeMap = m
return m, nil
}
func (l *GeoLoader) geoDownloadFunc(filename, url string) {
l.Logger.Info("downloading database", zap.String("filename", filename), zap.String("url", url))
}
func (l *GeoLoader) geoDownloadErrFunc(err error) {
if err != nil {
l.Logger.Error("failed to download database", zap.Error(err))
}
}

62
core/hy2/hook.go Normal file
View File

@@ -0,0 +1,62 @@
package hy2
import (
"sync"
"github.com/InazumaV/V2bX/common/counter"
"github.com/InazumaV/V2bX/common/format"
"github.com/InazumaV/V2bX/limiter"
"github.com/apernet/hysteria/core/v2/server"
"go.uber.org/zap"
)
var _ server.TrafficLogger = (*HookServer)(nil)
type HookServer struct {
Tag string
logger *zap.Logger
Counter sync.Map
ReportMinTrafficBytes int64
}
func (h *HookServer) TraceStream(stream server.HyStream, stats *server.StreamStats) {
}
func (h *HookServer) UntraceStream(stream server.HyStream) {
}
func (h *HookServer) LogTraffic(id string, tx, rx uint64) (ok bool) {
var c interface{}
var exists bool
limiterinfo, err := limiter.GetLimiter(h.Tag)
if err != nil {
h.logger.Error("Get limiter error", zap.String("tag", h.Tag), zap.Error(err))
return false
}
userLimit, ok := limiterinfo.UserLimitInfo.Load(format.UserTag(h.Tag, id))
if ok {
userlimitInfo := userLimit.(*limiter.UserLimitInfo)
if userlimitInfo.OverLimit {
userlimitInfo.OverLimit = false
return false
}
}
if c, exists = h.Counter.Load(h.Tag); !exists {
c = counter.NewTrafficCounter()
h.Counter.Store(h.Tag, c)
}
if tc, ok := c.(*counter.TrafficCounter); ok {
tc.Rx(id, int(rx))
tc.Tx(id, int(tx))
return true
}
return false
}
func (s *HookServer) LogOnlineState(id string, online bool) {
}

61
core/hy2/hy2.go Normal file
View File

@@ -0,0 +1,61 @@
package hy2
import (
"github.com/InazumaV/V2bX/conf"
vCore "github.com/InazumaV/V2bX/core"
"go.uber.org/zap"
)
var _ vCore.Core = (*Hysteria2)(nil)
type Hysteria2 struct {
Hy2nodes map[string]Hysteria2node
Auth *V2bX
Logger *zap.Logger
}
func init() {
vCore.RegisterCore("hysteria2", New)
}
func New(c *conf.CoreConfig) (vCore.Core, error) {
loglever := "error"
if c.Hysteria2Config.LogConfig.Level != "" {
loglever = c.Hysteria2Config.LogConfig.Level
}
log, err := initLogger(loglever, "console")
if err != nil {
return nil, err
}
return &Hysteria2{
Hy2nodes: make(map[string]Hysteria2node),
Auth: &V2bX{
usersMap: make(map[string]int),
},
Logger: log,
}, nil
}
func (h *Hysteria2) Protocols() []string {
return []string{
"hysteria2",
}
}
func (h *Hysteria2) Start() error {
return nil
}
func (h *Hysteria2) Close() error {
for _, n := range h.Hy2nodes {
err := n.Hy2server.Close()
if err != nil {
return err
}
}
return nil
}
func (h *Hysteria2) Type() string {
return "hysteria2"
}

156
core/hy2/logger.go Normal file
View File

@@ -0,0 +1,156 @@
package hy2
import (
"fmt"
"net"
"strings"
"github.com/InazumaV/V2bX/common/format"
"github.com/InazumaV/V2bX/limiter"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
type serverLogger struct {
Tag string
logger *zap.Logger
}
var logLevelMap = map[string]zapcore.Level{
"debug": zapcore.DebugLevel,
"info": zapcore.InfoLevel,
"warn": zapcore.WarnLevel,
"error": zapcore.ErrorLevel,
}
var logFormatMap = map[string]zapcore.EncoderConfig{
"console": {
TimeKey: "time",
LevelKey: "level",
NameKey: "logger",
MessageKey: "msg",
LineEnding: zapcore.DefaultLineEnding,
EncodeLevel: zapcore.CapitalColorLevelEncoder,
EncodeTime: zapcore.RFC3339TimeEncoder,
EncodeDuration: zapcore.SecondsDurationEncoder,
},
"json": {
TimeKey: "time",
LevelKey: "level",
NameKey: "logger",
MessageKey: "msg",
LineEnding: zapcore.DefaultLineEnding,
EncodeLevel: zapcore.LowercaseLevelEncoder,
EncodeTime: zapcore.EpochMillisTimeEncoder,
EncodeDuration: zapcore.SecondsDurationEncoder,
},
}
func (l *serverLogger) Connect(addr net.Addr, uuid string, tx uint64) {
limiterinfo, err := limiter.GetLimiter(l.Tag)
if err != nil {
l.logger.Panic("Get limiter error", zap.String("tag", l.Tag), zap.Error(err))
}
if _, r := limiterinfo.CheckLimit(format.UserTag(l.Tag, uuid), extractIPFromAddr(addr), addr.Network() == "tcp", true); r {
if userLimit, ok := limiterinfo.UserLimitInfo.Load(format.UserTag(l.Tag, uuid)); ok {
userLimit.(*limiter.UserLimitInfo).OverLimit = true
}
} else {
if userLimit, ok := limiterinfo.UserLimitInfo.Load(format.UserTag(l.Tag, uuid)); ok {
userLimit.(*limiter.UserLimitInfo).OverLimit = false
}
}
l.logger.Info("client connected", zap.String("addr", addr.String()), zap.String("uuid", uuid), zap.Uint64("tx", tx))
}
func (l *serverLogger) Disconnect(addr net.Addr, uuid string, err error) {
l.logger.Info("client disconnected", zap.String("addr", addr.String()), zap.String("uuid", uuid), zap.Error(err))
}
func (l *serverLogger) TCPRequest(addr net.Addr, uuid, reqAddr string) {
limiterinfo, err := limiter.GetLimiter(l.Tag)
if err != nil {
l.logger.Panic("Get limiter error", zap.String("tag", l.Tag), zap.Error(err))
}
if _, r := limiterinfo.CheckLimit(format.UserTag(l.Tag, uuid), extractIPFromAddr(addr), addr.Network() == "tcp", true); r {
if userLimit, ok := limiterinfo.UserLimitInfo.Load(format.UserTag(l.Tag, uuid)); ok {
userLimit.(*limiter.UserLimitInfo).OverLimit = true
}
} else {
if userLimit, ok := limiterinfo.UserLimitInfo.Load(format.UserTag(l.Tag, uuid)); ok {
userLimit.(*limiter.UserLimitInfo).OverLimit = false
}
}
l.logger.Debug("TCP request", zap.String("addr", addr.String()), zap.String("uuid", uuid), zap.String("reqAddr", reqAddr))
}
func (l *serverLogger) TCPError(addr net.Addr, uuid, reqAddr string, err error) {
if err == nil {
l.logger.Debug("TCP closed", zap.String("addr", addr.String()), zap.String("uuid", uuid), zap.String("reqAddr", reqAddr))
} else {
l.logger.Debug("TCP error", zap.String("addr", addr.String()), zap.String("uuid", uuid), zap.String("reqAddr", reqAddr), zap.Error(err))
}
}
func (l *serverLogger) UDPRequest(addr net.Addr, uuid string, sessionId uint32, reqAddr string) {
limiterinfo, err := limiter.GetLimiter(l.Tag)
if err != nil {
l.logger.Panic("Get limiter error", zap.String("tag", l.Tag), zap.Error(err))
}
if _, r := limiterinfo.CheckLimit(format.UserTag(l.Tag, uuid), extractIPFromAddr(addr), addr.Network() == "tcp", true); r {
if userLimit, ok := limiterinfo.UserLimitInfo.Load(format.UserTag(l.Tag, uuid)); ok {
userLimit.(*limiter.UserLimitInfo).OverLimit = true
}
} else {
if userLimit, ok := limiterinfo.UserLimitInfo.Load(format.UserTag(l.Tag, uuid)); ok {
userLimit.(*limiter.UserLimitInfo).OverLimit = false
}
}
l.logger.Debug("UDP request", zap.String("addr", addr.String()), zap.String("uuid", uuid), zap.Uint32("sessionId", sessionId), zap.String("reqAddr", reqAddr))
}
func (l *serverLogger) UDPError(addr net.Addr, uuid string, sessionId uint32, err error) {
if err == nil {
l.logger.Debug("UDP closed", zap.String("addr", addr.String()), zap.String("uuid", uuid), zap.Uint32("sessionId", sessionId))
} else {
l.logger.Debug("UDP error", zap.String("addr", addr.String()), zap.String("uuid", uuid), zap.Uint32("sessionId", sessionId), zap.Error(err))
}
}
func initLogger(logLevel string, logFormat string) (*zap.Logger, error) {
level, ok := logLevelMap[strings.ToLower(logLevel)]
if !ok {
return nil, fmt.Errorf("unsupported log level: %s", logLevel)
}
enc, ok := logFormatMap[strings.ToLower(logFormat)]
if !ok {
return nil, fmt.Errorf("unsupported log format: %s", logFormat)
}
c := zap.Config{
Level: zap.NewAtomicLevelAt(level),
DisableCaller: true,
DisableStacktrace: true,
Encoding: strings.ToLower(logFormat),
EncoderConfig: enc,
OutputPaths: []string{"stderr"},
ErrorOutputPaths: []string{"stderr"},
}
logger, err := c.Build()
if err != nil {
return nil, fmt.Errorf("failed to initialize logger: %s", err)
}
return logger, nil
}
func extractIPFromAddr(addr net.Addr) string {
switch v := addr.(type) {
case *net.TCPAddr:
return v.IP.String()
case *net.UDPAddr:
return v.IP.String()
case *net.IPAddr:
return v.IP.String()
default:
return ""
}
}

77
core/hy2/node.go Normal file
View File

@@ -0,0 +1,77 @@
package hy2
import (
"strings"
"github.com/InazumaV/V2bX/api/panel"
"github.com/InazumaV/V2bX/conf"
"github.com/apernet/hysteria/core/v2/server"
"github.com/spf13/viper"
"go.uber.org/zap"
)
type Hysteria2node struct {
Hy2server server.Server
Tag string
Logger *zap.Logger
EventLogger server.EventLogger
TrafficLogger server.TrafficLogger
}
func (h *Hysteria2) AddNode(tag string, info *panel.NodeInfo, config *conf.Options) error {
var err error
hyconfig := &server.Config{}
var c serverConfig
v := viper.New()
if len(config.Hysteria2ConfigPath) != 0 {
v.SetConfigFile(config.Hysteria2ConfigPath)
if err := v.ReadInConfig(); err != nil {
h.Logger.Fatal("failed to read server config", zap.Error(err))
}
if err := v.Unmarshal(&c); err != nil {
h.Logger.Fatal("failed to parse server config", zap.Error(err))
}
}
n := Hysteria2node{
Tag: tag,
Logger: h.Logger,
EventLogger: &serverLogger{
Tag: tag,
logger: h.Logger,
},
TrafficLogger: &HookServer{
Tag: tag,
logger: h.Logger,
ReportMinTrafficBytes: config.ReportMinTraffic * 1024,
},
}
hyconfig, err = n.getHyConfig(info, config, &c)
if err != nil {
return err
}
hyconfig.Authenticator = h.Auth
s, err := server.NewServer(hyconfig)
if err != nil {
return err
}
n.Hy2server = s
h.Hy2nodes[tag] = n
go func() {
if err := s.Serve(); err != nil {
if !strings.Contains(err.Error(), "quic: server closed") {
h.Logger.Error("Server Error", zap.Error(err))
}
}
}()
return nil
}
func (h *Hysteria2) DelNode(tag string) error {
err := h.Hy2nodes[tag].Hy2server.Close()
if err != nil {
return err
}
delete(h.Hy2nodes, tag)
return nil
}

238
core/hy2/serverConfig.go Normal file
View File

@@ -0,0 +1,238 @@
package hy2
import (
"fmt"
"net"
"strings"
"time"
"github.com/apernet/hysteria/extras/v2/outbounds"
)
type serverConfig struct {
Listen string `mapstructure:"listen"`
Obfs serverConfigObfs `mapstructure:"obfs"`
TLS *serverConfigTLS `mapstructure:"tls"`
ACME *serverConfigACME `mapstructure:"acme"`
QUIC serverConfigQUIC `mapstructure:"quic"`
Bandwidth serverConfigBandwidth `mapstructure:"bandwidth"`
IgnoreClientBandwidth bool `mapstructure:"ignoreClientBandwidth"`
SpeedTest bool `mapstructure:"speedTest"`
DisableUDP bool `mapstructure:"disableUDP"`
UDPIdleTimeout time.Duration `mapstructure:"udpIdleTimeout"`
Auth serverConfigAuth `mapstructure:"auth"`
Resolver serverConfigResolver `mapstructure:"resolver"`
Sniff serverConfigSniff `mapstructure:"sniff"`
ACL serverConfigACL `mapstructure:"acl"`
Outbounds []serverConfigOutboundEntry `mapstructure:"outbounds"`
TrafficStats serverConfigTrafficStats `mapstructure:"trafficStats"`
Masquerade serverConfigMasquerade `mapstructure:"masquerade"`
}
type serverConfigObfsSalamander struct {
Password string `mapstructure:"password"`
}
type serverConfigObfs struct {
Type string `mapstructure:"type"`
Salamander serverConfigObfsSalamander `mapstructure:"salamander"`
}
type serverConfigTLS struct {
Cert string `mapstructure:"cert"`
Key string `mapstructure:"key"`
}
type serverConfigACME struct {
Domains []string `mapstructure:"domains"`
Email string `mapstructure:"email"`
CA string `mapstructure:"ca"`
DisableHTTP bool `mapstructure:"disableHTTP"`
DisableTLSALPN bool `mapstructure:"disableTLSALPN"`
AltHTTPPort int `mapstructure:"altHTTPPort"`
AltTLSALPNPort int `mapstructure:"altTLSALPNPort"`
Dir string `mapstructure:"dir"`
}
type serverConfigQUIC struct {
InitStreamReceiveWindow uint64 `mapstructure:"initStreamReceiveWindow"`
MaxStreamReceiveWindow uint64 `mapstructure:"maxStreamReceiveWindow"`
InitConnectionReceiveWindow uint64 `mapstructure:"initConnReceiveWindow"`
MaxConnectionReceiveWindow uint64 `mapstructure:"maxConnReceiveWindow"`
MaxIdleTimeout time.Duration `mapstructure:"maxIdleTimeout"`
MaxIncomingStreams int64 `mapstructure:"maxIncomingStreams"`
DisablePathMTUDiscovery bool `mapstructure:"disablePathMTUDiscovery"`
}
type serverConfigBandwidth struct {
Up string `mapstructure:"up"`
Down string `mapstructure:"down"`
}
type serverConfigAuthHTTP struct {
URL string `mapstructure:"url"`
Insecure bool `mapstructure:"insecure"`
}
type serverConfigAuth struct {
Type string `mapstructure:"type"`
Password string `mapstructure:"password"`
UserPass map[string]string `mapstructure:"userpass"`
HTTP serverConfigAuthHTTP `mapstructure:"http"`
Command string `mapstructure:"command"`
}
type serverConfigResolverTCP struct {
Addr string `mapstructure:"addr"`
Timeout time.Duration `mapstructure:"timeout"`
}
type serverConfigResolverUDP struct {
Addr string `mapstructure:"addr"`
Timeout time.Duration `mapstructure:"timeout"`
}
type serverConfigResolverTLS struct {
Addr string `mapstructure:"addr"`
Timeout time.Duration `mapstructure:"timeout"`
SNI string `mapstructure:"sni"`
Insecure bool `mapstructure:"insecure"`
}
type serverConfigResolverHTTPS struct {
Addr string `mapstructure:"addr"`
Timeout time.Duration `mapstructure:"timeout"`
SNI string `mapstructure:"sni"`
Insecure bool `mapstructure:"insecure"`
}
type serverConfigResolver struct {
Type string `mapstructure:"type"`
TCP serverConfigResolverTCP `mapstructure:"tcp"`
UDP serverConfigResolverUDP `mapstructure:"udp"`
TLS serverConfigResolverTLS `mapstructure:"tls"`
HTTPS serverConfigResolverHTTPS `mapstructure:"https"`
}
type serverConfigSniff struct {
Enable bool `mapstructure:"enable"`
Timeout time.Duration `mapstructure:"timeout"`
RewriteDomain bool `mapstructure:"rewriteDomain"`
TCPPorts string `mapstructure:"tcpPorts"`
UDPPorts string `mapstructure:"udpPorts"`
}
type serverConfigACL struct {
File string `mapstructure:"file"`
Inline []string `mapstructure:"inline"`
GeoIP string `mapstructure:"geoip"`
GeoSite string `mapstructure:"geosite"`
GeoUpdateInterval time.Duration `mapstructure:"geoUpdateInterval"`
}
type serverConfigOutboundDirect struct {
Mode string `mapstructure:"mode"`
BindIPv4 string `mapstructure:"bindIPv4"`
BindIPv6 string `mapstructure:"bindIPv6"`
BindDevice string `mapstructure:"bindDevice"`
}
type serverConfigOutboundSOCKS5 struct {
Addr string `mapstructure:"addr"`
Username string `mapstructure:"username"`
Password string `mapstructure:"password"`
}
type serverConfigOutboundHTTP struct {
URL string `mapstructure:"url"`
Insecure bool `mapstructure:"insecure"`
}
type serverConfigOutboundEntry struct {
Name string `mapstructure:"name"`
Type string `mapstructure:"type"`
Direct serverConfigOutboundDirect `mapstructure:"direct"`
SOCKS5 serverConfigOutboundSOCKS5 `mapstructure:"socks5"`
HTTP serverConfigOutboundHTTP `mapstructure:"http"`
}
type serverConfigTrafficStats struct {
Listen string `mapstructure:"listen"`
Secret string `mapstructure:"secret"`
}
type serverConfigMasqueradeFile struct {
Dir string `mapstructure:"dir"`
}
type serverConfigMasqueradeProxy struct {
URL string `mapstructure:"url"`
RewriteHost bool `mapstructure:"rewriteHost"`
}
type serverConfigMasqueradeString struct {
Content string `mapstructure:"content"`
Headers map[string]string `mapstructure:"headers"`
StatusCode int `mapstructure:"statusCode"`
}
type serverConfigMasquerade struct {
Type string `mapstructure:"type"`
File serverConfigMasqueradeFile `mapstructure:"file"`
Proxy serverConfigMasqueradeProxy `mapstructure:"proxy"`
String serverConfigMasqueradeString `mapstructure:"string"`
ListenHTTP string `mapstructure:"listenHTTP"`
ListenHTTPS string `mapstructure:"listenHTTPS"`
ForceHTTPS bool `mapstructure:"forceHTTPS"`
}
func serverConfigOutboundDirectToOutbound(c serverConfigOutboundDirect) (outbounds.PluggableOutbound, error) {
var mode outbounds.DirectOutboundMode
switch strings.ToLower(c.Mode) {
case "", "auto":
mode = outbounds.DirectOutboundModeAuto
case "64":
mode = outbounds.DirectOutboundMode64
case "46":
mode = outbounds.DirectOutboundMode46
case "6":
mode = outbounds.DirectOutboundMode6
case "4":
mode = outbounds.DirectOutboundMode4
default:
return nil, fmt.Errorf("outbounds.direct.mode unsupported mode")
}
bindIP := len(c.BindIPv4) > 0 || len(c.BindIPv6) > 0
bindDevice := len(c.BindDevice) > 0
if bindIP && bindDevice {
return nil, fmt.Errorf("outbounds.direct cannot bind both IP and device")
}
if bindIP {
ip4, ip6 := net.ParseIP(c.BindIPv4), net.ParseIP(c.BindIPv6)
if len(c.BindIPv4) > 0 && ip4 == nil {
return nil, fmt.Errorf("outbounds.direct.bindIPv4 invalid IPv4 address")
}
if len(c.BindIPv6) > 0 && ip6 == nil {
return nil, fmt.Errorf("outbounds.direct.bindIPv6 invalid IPv6 address")
}
return outbounds.NewDirectOutboundBindToIPs(mode, ip4, ip6)
}
if bindDevice {
return outbounds.NewDirectOutboundBindToDevice(mode, c.BindDevice)
}
return outbounds.NewDirectOutboundSimple(mode), nil
}
func serverConfigOutboundSOCKS5ToOutbound(c serverConfigOutboundSOCKS5) (outbounds.PluggableOutbound, error) {
if c.Addr == "" {
return nil, fmt.Errorf("outbounds.socks5.addr empty socks5 address")
}
return outbounds.NewSOCKS5Outbound(c.Addr, c.Username, c.Password), nil
}
func serverConfigOutboundHTTPToOutbound(c serverConfigOutboundHTTP) (outbounds.PluggableOutbound, error) {
if c.URL == "" {
return nil, fmt.Errorf("outbounds.http.url empty http address")
}
return outbounds.NewHTTPOutbound(c.URL, c.Insecure)
}

101
core/hy2/user.go Normal file
View File

@@ -0,0 +1,101 @@
package hy2
import (
"net"
"sync"
"github.com/InazumaV/V2bX/api/panel"
"github.com/InazumaV/V2bX/common/counter"
vCore "github.com/InazumaV/V2bX/core"
"github.com/apernet/hysteria/core/v2/server"
)
var _ server.Authenticator = &V2bX{}
type V2bX struct {
usersMap map[string]int
mutex sync.RWMutex
}
func (v *V2bX) Authenticate(addr net.Addr, auth string, tx uint64) (ok bool, id string) {
v.mutex.RLock()
defer v.mutex.RUnlock()
if _, exists := v.usersMap[auth]; exists {
return true, auth
}
return false, ""
}
func (h *Hysteria2) AddUsers(p *vCore.AddUsersParams) (added int, err error) {
var wg sync.WaitGroup
for _, user := range p.Users {
wg.Add(1)
go func(u panel.UserInfo) {
defer wg.Done()
h.Auth.mutex.Lock()
h.Auth.usersMap[u.Uuid] = u.Id
h.Auth.mutex.Unlock()
}(user)
}
wg.Wait()
return len(p.Users), nil
}
func (h *Hysteria2) DelUsers(users []panel.UserInfo, tag string, _ *panel.NodeInfo) error {
var wg sync.WaitGroup
for _, user := range users {
wg.Add(1)
if v, ok := h.Hy2nodes[tag].TrafficLogger.(*HookServer).Counter.Load(tag); ok {
c := v.(*counter.TrafficCounter)
c.Delete(user.Uuid)
}
go func(u panel.UserInfo) {
defer wg.Done()
h.Auth.mutex.Lock()
delete(h.Auth.usersMap, u.Uuid)
h.Auth.mutex.Unlock()
}(user)
}
wg.Wait()
return nil
}
func (h *Hysteria2) GetUserTrafficSlice(tag string, reset bool) ([]panel.UserTraffic, error) {
trafficSlice := make([]panel.UserTraffic, 0)
h.Auth.mutex.RLock()
defer h.Auth.mutex.RUnlock()
if _, ok := h.Hy2nodes[tag]; !ok {
return nil, nil
}
hook := h.Hy2nodes[tag].TrafficLogger.(*HookServer)
if v, ok := hook.Counter.Load(tag); ok {
c := v.(*counter.TrafficCounter)
c.Counters.Range(func(key, value interface{}) bool {
uuid := key.(string)
traffic := value.(*counter.TrafficStorage)
up := traffic.UpCounter.Load()
down := traffic.DownCounter.Load()
if up+down > hook.ReportMinTrafficBytes {
if reset {
traffic.UpCounter.Store(0)
traffic.DownCounter.Store(0)
}
if h.Auth.usersMap[uuid] == 0 {
c.Delete(uuid)
return true
}
trafficSlice = append(trafficSlice, panel.UserTraffic{
UID: h.Auth.usersMap[uuid],
Upload: up,
Download: down,
})
}
return true
})
if len(trafficSlice) == 0 {
return nil, nil
}
return trafficSlice, nil
}
return nil, nil
}

5
core/imports/hy2.go Normal file
View File

@@ -0,0 +1,5 @@
//go:build hysteria2
package imports
import _ "github.com/InazumaV/V2bX/core/hy2"

View File

@@ -17,8 +17,8 @@ type Core interface {
AddNode(tag string, info *panel.NodeInfo, config *conf.Options) error AddNode(tag string, info *panel.NodeInfo, config *conf.Options) error
DelNode(tag string) error DelNode(tag string) error
AddUsers(p *AddUsersParams) (added int, err error) AddUsers(p *AddUsersParams) (added int, err error)
GetUserTraffic(tag, uuid string, reset bool) (up int64, down int64) GetUserTrafficSlice(tag string, reset bool) ([]panel.UserTraffic, error)
DelUsers(users []panel.UserInfo, tag string) error DelUsers(users []panel.UserInfo, tag string, info *panel.NodeInfo) error
Protocols() []string Protocols() []string
Type() string Type() string
} }

View File

@@ -3,7 +3,6 @@ package core
import ( import (
"errors" "errors"
"fmt" "fmt"
"github.com/hashicorp/go-multierror"
"strings" "strings"
"sync" "sync"
@@ -49,14 +48,13 @@ func (s *Selector) Start() error {
} }
func (s *Selector) Close() error { func (s *Selector) Close() error {
var errs error var errs []error
for i := range s.cores { for i := range s.cores {
err := s.cores[i].Close() if err := s.cores[i].Close(); err != nil {
if err != nil { errs = append(errs, err)
errs = multierror.Append(errs, err)
} }
} }
return errs return errors.Join(errs...)
} }
func isSupported(protocol string, protocols []string) bool { func isSupported(protocol string, protocols []string) bool {
@@ -127,20 +125,20 @@ func (s *Selector) AddUsers(p *AddUsersParams) (added int, err error) {
return t.(Core).AddUsers(p) return t.(Core).AddUsers(p)
} }
func (s *Selector) GetUserTraffic(tag, uuid string, reset bool) (up int64, down int64) { func (s *Selector) GetUserTrafficSlice(tag string, reset bool) ([]panel.UserTraffic, error) {
t, e := s.nodes.Load(tag) t, e := s.nodes.Load(tag)
if !e { if !e {
return 0, 0 return nil, errors.New("the node is not have")
} }
return t.(Core).GetUserTraffic(tag, uuid, reset) return t.(Core).GetUserTrafficSlice(tag, reset)
} }
func (s *Selector) DelUsers(users []panel.UserInfo, tag string) error { func (s *Selector) DelUsers(users []panel.UserInfo, tag string, info *panel.NodeInfo) error {
t, e := s.nodes.Load(tag) t, e := s.nodes.Load(tag)
if !e { if !e {
return errors.New("the node is not have") return errors.New("the node is not have")
} }
return t.(Core).DelUsers(users, tag) return t.(Core).DelUsers(users, tag, info)
} }
func (s *Selector) Protocols() []string { func (s *Selector) Protocols() []string {

View File

@@ -1,85 +0,0 @@
package sing
import (
"bytes"
"github.com/InazumaV/V2bX/api/panel"
"github.com/goccy/go-json"
log "github.com/sirupsen/logrus"
"os"
"strings"
)
func updateDNSConfig(node *panel.NodeInfo) (err error) {
dnsPath := os.Getenv("SING_DNS_PATH")
if len(node.RawDNS.DNSJson) != 0 {
var prettyJSON bytes.Buffer
if err := json.Indent(&prettyJSON, node.RawDNS.DNSJson, "", " "); err != nil {
return err
}
err = saveDnsConfig(prettyJSON.Bytes(), dnsPath)
} else if len(node.RawDNS.DNSMap) != 0 {
dnsConfig := DNSConfig{
Servers: []map[string]interface{}{
{
"tag": "default",
"address": "https://8.8.8.8/dns-query",
"detour": "direct",
},
},
}
for id, value := range node.RawDNS.DNSMap {
dnsConfig.Servers = append(dnsConfig.Servers,
map[string]interface{}{
"tag": id,
"address": value["address"],
"address_resolver": "default",
"detour": "direct",
},
)
rule := map[string]interface{}{
"server": id,
"disable_cache": true,
}
for _, ruleType := range []string{"domain_suffix", "domain_keyword", "domain_regex", "geosite"} {
var domains []string
for _, v := range value["domains"].([]string) {
split := strings.SplitN(v, ":", 2)
prefix := strings.ToLower(split[0])
if prefix == ruleType || (prefix == "domain" && ruleType == "domain_suffix") {
if len(split) > 1 {
domains = append(domains, split[1])
}
if len(domains) > 0 {
rule[ruleType] = domains
}
}
}
}
dnsConfig.Rules = append(dnsConfig.Rules, rule)
}
dnsConfigJSON, err := json.MarshalIndent(dnsConfig, "", " ")
if err != nil {
log.WithField("err", err).Error("Error marshaling dnsConfig to JSON")
return err
}
err = saveDnsConfig(dnsConfigJSON, dnsPath)
}
return err
}
func saveDnsConfig(dns []byte, dnsPath string) (err error) {
currentData, err := os.ReadFile(dnsPath)
if err != nil {
log.WithField("err", err).Error("Failed to read SING_DNS_PATH")
return err
}
if !bytes.Equal(currentData, dns) {
if err = os.Truncate(dnsPath, 0); err != nil {
log.WithField("err", err).Error("Failed to clear SING DNS PATH file")
}
if err = os.WriteFile(dnsPath, dns, 0644); err != nil {
log.WithField("err", err).Error("Failed to write DNS to SING DNS PATH file")
}
}
return err
}

View File

@@ -2,12 +2,11 @@ package sing
import ( import (
"context" "context"
"io" "fmt"
"net" "net"
"sync" "sync"
"github.com/sagernet/sing-box/common/urltest" "github.com/InazumaV/V2bX/common/format"
"github.com/InazumaV/V2bX/common/rate" "github.com/InazumaV/V2bX/common/rate"
"github.com/InazumaV/V2bX/limiter" "github.com/InazumaV/V2bX/limiter"
@@ -18,204 +17,108 @@ import (
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
) )
var _ adapter.ConnectionTracker = (*HookServer)(nil)
type HookServer struct { type HookServer struct {
EnableConnClear bool counter sync.Map //map[string]*counter.TrafficCounter
counter sync.Map
connClears sync.Map
}
type ConnClear struct {
lock sync.RWMutex
conns map[int]io.Closer
}
func (c *ConnClear) AddConn(cn io.Closer) (key int) {
c.lock.Lock()
defer c.lock.Unlock()
key = len(c.conns)
c.conns[key] = cn
return
}
func (c *ConnClear) DelConn(key int) {
c.lock.Lock()
defer c.lock.Unlock()
delete(c.conns, key)
}
func (c *ConnClear) ClearConn() {
c.lock.Lock()
defer c.lock.Unlock()
for _, c := range c.conns {
c.Close()
}
} }
func (h *HookServer) ModeList() []string { func (h *HookServer) ModeList() []string {
return nil return nil
} }
func NewHookServer(enableClear bool) *HookServer { func (h *HookServer) RoutedConnection(_ context.Context, conn net.Conn, m adapter.InboundContext, _ adapter.Rule, _ adapter.Outbound) net.Conn {
return &HookServer{
EnableConnClear: enableClear,
counter: sync.Map{},
connClears: sync.Map{},
}
}
func (h *HookServer) Start() error {
return nil
}
func (h *HookServer) Close() error {
return nil
}
func (h *HookServer) PreStart() error {
return nil
}
func (h *HookServer) RoutedConnection(_ context.Context, conn net.Conn, m adapter.InboundContext, _ adapter.Rule) (net.Conn, adapter.Tracker) {
t := &Tracker{}
l, err := limiter.GetLimiter(m.Inbound) l, err := limiter.GetLimiter(m.Inbound)
if err != nil { if err != nil {
log.Warn("get limiter for ", m.Inbound, " error: ", err) log.Warn("get limiter for ", m.Inbound, " error: ", err)
return conn, t return conn
}
if l.CheckDomainRule(m.Domain) {
conn.Close()
log.Error("[", m.Inbound, "] ",
"Limited ", m.User, " access to ", m.Domain, " by domain rule")
return conn, t
}
if l.CheckProtocolRule(m.Protocol) {
conn.Close()
log.Error("[", m.Inbound, "] ",
"Limited ", m.User, " use ", m.Domain, " by protocol rule")
return conn, t
} }
taguuid := format.UserTag(m.Inbound, m.User)
ip := m.Source.Addr.String() ip := m.Source.Addr.String()
if b, r := l.CheckLimit(m.User, ip, true); r { if b, r := l.CheckLimit(taguuid, ip, true, true); r {
conn.Close() conn.Close()
log.Error("[", m.Inbound, "] ", "Limited ", m.User, " by ip or conn") log.Error("[", m.Inbound, "] ", "Limited ", m.User, " by ip or conn")
return conn, t return conn
} else if b != nil { } else if b != nil {
conn = rate.NewConnRateLimiter(conn, b) conn = rate.NewConnRateLimiter(conn, b)
} }
t.AddLeave(func() { if l != nil {
l.ConnLimiter.DelConnCount(m.User, ip) destStr := m.Destination.AddrString()
}) protocol := m.Protocol
if h.EnableConnClear { if l.CheckDomainRule(destStr) {
var key int log.Error(fmt.Sprintf(
cc := &ConnClear{ "User %s access domain %s reject by rule",
conns: map[int]io.Closer{ m.User,
0: conn, destStr))
}, conn.Close()
return conn
} }
if v, ok := h.connClears.LoadOrStore(m.Inbound+m.User, cc); ok { if len(protocol) != 0 {
cc = v.(*ConnClear) if l.CheckProtocolRule(protocol) {
key = cc.AddConn(conn) log.Error(fmt.Sprintf(
"User %s access protocol %s reject by rule",
m.User,
protocol))
conn.Close()
return conn
}
} }
t.AddLeave(func() {
cc.DelConn(key)
})
} }
if c, ok := h.counter.Load(m.Inbound); ok { var t *counter.TrafficCounter
return counter.NewConnCounter(conn, c.(*counter.TrafficCounter).GetCounter(m.User)), t if c, ok := h.counter.Load(m.Inbound); !ok {
t = counter.NewTrafficCounter()
h.counter.Store(m.Inbound, t)
} else { } else {
c := counter.NewTrafficCounter() t = c.(*counter.TrafficCounter)
h.counter.Store(m.Inbound, c)
return counter.NewConnCounter(conn, c.GetCounter(m.User)), t
} }
conn = counter.NewConnCounter(conn, t.GetCounter(m.User))
return conn
} }
func (h *HookServer) RoutedPacketConnection(_ context.Context, conn N.PacketConn, m adapter.InboundContext, _ adapter.Rule) (N.PacketConn, adapter.Tracker) { func (h *HookServer) RoutedPacketConnection(_ context.Context, conn N.PacketConn, m adapter.InboundContext, _ adapter.Rule, _ adapter.Outbound) N.PacketConn {
t := &Tracker{}
l, err := limiter.GetLimiter(m.Inbound) l, err := limiter.GetLimiter(m.Inbound)
if err != nil { if err != nil {
log.Warn("get limiter for ", m.Inbound, " error: ", err) log.Warn("get limiter for ", m.Inbound, " error: ", err)
return conn, t return conn
}
if l.CheckDomainRule(m.Domain) {
conn.Close()
log.Error("[", m.Inbound, "] ",
"Limited ", m.User, " access to ", m.Domain, " by domain rule")
return conn, t
}
if l.CheckProtocolRule(m.Protocol) {
conn.Close()
log.Error("[", m.Inbound, "] ",
"Limited ", m.User, " use ", m.Domain, " by protocol rule")
return conn, t
} }
ip := m.Source.Addr.String() ip := m.Source.Addr.String()
if b, r := l.CheckLimit(m.User, ip, true); r { taguuid := format.UserTag(m.Inbound, m.User)
if b, r := l.CheckLimit(taguuid, ip, false, false); r {
conn.Close() conn.Close()
log.Error("[", m.Inbound, "] ", "Limited ", m.User, " by ip or conn") log.Error("[", m.Inbound, "] ", "Limited ", m.User, " by ip or conn")
return conn, t return conn
} else if b != nil { } else if b != nil {
conn = rate.NewPacketConnCounter(conn, b) //conn = rate.NewPacketConnCounter(conn, b)
} }
if h.EnableConnClear { if l != nil {
var key int destStr := m.Destination.AddrString()
cc := &ConnClear{ protocol := m.Destination.Network()
conns: map[int]io.Closer{ if l.CheckDomainRule(destStr) {
0: conn, log.Error(fmt.Sprintf(
}, "User %s access domain %s reject by rule",
m.User,
destStr))
conn.Close()
return conn
} }
if v, ok := h.connClears.LoadOrStore(m.Inbound+m.User, cc); ok { if len(protocol) != 0 {
cc = v.(*ConnClear) if l.CheckProtocolRule(protocol) {
key = cc.AddConn(conn) log.Error(fmt.Sprintf(
"User %s access protocol %s reject by rule",
m.User,
protocol))
conn.Close()
return conn
}
} }
t.AddLeave(func() {
cc.DelConn(key)
})
} }
if c, ok := h.counter.Load(m.Inbound); ok { var t *counter.TrafficCounter
return counter.NewPacketConnCounter(conn, c.(*counter.TrafficCounter).GetCounter(m.User)), t if c, ok := h.counter.Load(m.Inbound); !ok {
t = counter.NewTrafficCounter()
h.counter.Store(m.Inbound, t)
} else { } else {
c := counter.NewTrafficCounter() t = c.(*counter.TrafficCounter)
h.counter.Store(m.Inbound, c)
return counter.NewPacketConnCounter(conn, c.GetCounter(m.User)), t
}
}
// not need
func (h *HookServer) Mode() string {
return ""
}
func (h *HookServer) StoreSelected() bool {
return false
}
func (h *HookServer) CacheFile() adapter.ClashCacheFile {
return nil
}
func (h *HookServer) HistoryStorage() *urltest.HistoryStorage {
return nil
}
func (h *HookServer) StoreFakeIP() bool {
return false
}
func (h *HookServer) ClearConn(inbound string, user string) {
if v, ok := h.connClears.Load(inbound + user); ok {
v.(*ConnClear).ClearConn()
h.connClears.Delete(inbound + user)
}
}
type Tracker struct {
l []func()
}
func (t *Tracker) AddLeave(f func()) {
t.l = append(t.l, f)
}
func (t *Tracker) Leave() {
for i := range t.l {
t.l[i]()
} }
conn = counter.NewPacketConnCounter(conn, t.GetCounter(m.User))
return conn
} }

View File

@@ -10,38 +10,68 @@ import (
"strings" "strings"
"time" "time"
"encoding/json"
"github.com/InazumaV/V2bX/api/panel" "github.com/InazumaV/V2bX/api/panel"
"github.com/InazumaV/V2bX/conf" "github.com/InazumaV/V2bX/conf"
"github.com/goccy/go-json"
"github.com/sagernet/sing-box/inbound"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
F "github.com/sagernet/sing/common/format" F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/common/json/badoption"
) )
type HttpNetworkConfig struct {
Header struct {
Type string `json:"type"`
Request *json.RawMessage `json:"request"`
Response *json.RawMessage `json:"response"`
} `json:"header"`
}
type HttpRequest struct {
Version string `json:"version"`
Method string `json:"method"`
Path []string `json:"path"`
Headers struct {
Host []string `json:"Host"`
} `json:"headers"`
}
type WsNetworkConfig struct { type WsNetworkConfig struct {
Path string `json:"path"` Path string `json:"path"`
Headers map[string]string `json:"headers"` Headers map[string]string `json:"headers"`
} }
type GrpcNetworkConfig struct {
ServiceName string `json:"serviceName"`
}
type HttpupgradeNetworkConfig struct {
Path string `json:"path"`
Host string `json:"host"`
}
func getInboundOptions(tag string, info *panel.NodeInfo, c *conf.Options) (option.Inbound, error) { func getInboundOptions(tag string, info *panel.NodeInfo, c *conf.Options) (option.Inbound, error) {
addr, err := netip.ParseAddr(c.ListenIP) addr, err := netip.ParseAddr(c.ListenIP)
if err != nil { if err != nil {
return option.Inbound{}, fmt.Errorf("the listen ip not vail") return option.Inbound{}, fmt.Errorf("the listen ip not vail")
} }
var domainStrategy option.DomainStrategy
if c.SingOptions.EnableDNS {
domainStrategy = c.SingOptions.DomainStrategy
}
listen := option.ListenOptions{ listen := option.ListenOptions{
Listen: (*option.ListenAddress)(&addr), Listen: (*badoption.Addr)(&addr),
ListenPort: uint16(info.Common.ServerPort), ListenPort: uint16(info.Common.ServerPort),
ProxyProtocol: c.SingOptions.EnableProxyProtocol, TCPFastOpen: c.SingOptions.TCPFastOpen,
TCPFastOpen: c.SingOptions.TCPFastOpen, }
InboundOptions: option.InboundOptions{ var multiplex *option.InboundMultiplexOptions
SniffEnabled: c.SingOptions.SniffEnabled, if c.SingOptions.Multiplex != nil {
SniffOverrideDestination: c.SingOptions.SniffOverrideDestination, multiplexOption := option.InboundMultiplexOptions{
DomainStrategy: domainStrategy, Enabled: c.SingOptions.Multiplex.Enabled,
}, Padding: c.SingOptions.Multiplex.Padding,
Brutal: &option.BrutalOptions{
Enabled: c.SingOptions.Multiplex.Brutal.Enabled,
UpMbps: c.SingOptions.Multiplex.Brutal.UpMbps,
DownMbps: c.SingOptions.Multiplex.Brutal.DownMbps,
},
}
multiplex = &multiplexOption
} }
var tls option.InboundTLSOptions var tls option.InboundTLSOptions
switch info.Security { switch info.Security {
@@ -74,14 +104,14 @@ func getInboundOptions(tag string, info *panel.NodeInfo, c *conf.Options) (optio
Enabled: true, Enabled: true,
ShortID: []string{v.TlsSettings.ShortId}, ShortID: []string{v.TlsSettings.ShortId},
PrivateKey: v.TlsSettings.PrivateKey, PrivateKey: v.TlsSettings.PrivateKey,
Xver: v.TlsSettings.Xver, Xver: uint8(v.TlsSettings.Xver),
Handshake: option.InboundRealityHandshakeOptions{ Handshake: option.InboundRealityHandshakeOptions{
ServerOptions: option.ServerOptions{ ServerOptions: option.ServerOptions{
Server: dest, Server: dest,
ServerPort: uint16(port), ServerPort: uint16(port),
}, },
}, },
MaxTimeDifference: option.Duration(mtd), MaxTimeDifference: badoption.Duration(mtd),
} }
} }
in := option.Inbound{ in := option.Inbound{
@@ -95,12 +125,36 @@ func getInboundOptions(tag string, info *panel.NodeInfo, c *conf.Options) (optio
} }
switch n.Network { switch n.Network {
case "tcp": case "tcp":
t.Type = "" if len(n.NetworkSettings) != 0 {
network := HttpNetworkConfig{}
err := json.Unmarshal(n.NetworkSettings, &network)
if err != nil {
return option.Inbound{}, fmt.Errorf("decode NetworkSettings error: %s", err)
}
//Todo fix http options
if network.Header.Type == "http" {
t.Type = network.Header.Type
var request HttpRequest
if network.Header.Request != nil {
err = json.Unmarshal(*network.Header.Request, &request)
if err != nil {
return option.Inbound{}, fmt.Errorf("decode HttpRequest error: %s", err)
}
t.HTTPOptions.Host = request.Headers.Host
t.HTTPOptions.Path = request.Path[0]
t.HTTPOptions.Method = request.Method
}
} else {
t.Type = ""
}
} else {
t.Type = ""
}
case "ws": case "ws":
var ( var (
path string path string
ed int ed int
headers map[string]option.Listable[string] headers map[string]badoption.Listable[string]
) )
if len(n.NetworkSettings) != 0 { if len(n.NetworkSettings) != 0 {
network := WsNetworkConfig{} network := WsNetworkConfig{}
@@ -115,9 +169,9 @@ func getInboundOptions(tag string, info *panel.NodeInfo, c *conf.Options) (optio
} }
path = u.Path path = u.Path
ed, _ = strconv.Atoi(u.Query().Get("ed")) ed, _ = strconv.Atoi(u.Query().Get("ed"))
headers = make(map[string]option.Listable[string], len(network.Headers)) headers = make(map[string]badoption.Listable[string], len(network.Headers))
for k, v := range network.Headers { for k, v := range network.Headers {
headers[k] = option.Listable[string]{ headers[k] = badoption.Listable[string]{
v, v,
} }
} }
@@ -129,26 +183,48 @@ func getInboundOptions(tag string, info *panel.NodeInfo, c *conf.Options) (optio
Headers: headers, Headers: headers,
} }
case "grpc": case "grpc":
network := GrpcNetworkConfig{}
if len(n.NetworkSettings) != 0 { if len(n.NetworkSettings) != 0 {
err := json.Unmarshal(n.NetworkSettings, &t.GRPCOptions) err := json.Unmarshal(n.NetworkSettings, &network)
if err != nil { if err != nil {
return option.Inbound{}, fmt.Errorf("decode NetworkSettings error: %s", err) return option.Inbound{}, fmt.Errorf("decode NetworkSettings error: %s", err)
} }
} }
t.GRPCOptions = option.V2RayGRPCOptions{
ServiceName: network.ServiceName,
}
case "httpupgrade":
network := HttpupgradeNetworkConfig{}
if len(n.NetworkSettings) != 0 {
err := json.Unmarshal(n.NetworkSettings, &network)
if err != nil {
return option.Inbound{}, fmt.Errorf("decode NetworkSettings error: %s", err)
}
}
t.HTTPUpgradeOptions = option.V2RayHTTPUpgradeOptions{
Path: network.Path,
Host: network.Host,
}
} }
if info.Type == "vless" { if info.Type == "vless" {
in.Type = "vless" in.Type = "vless"
in.VLESSOptions = option.VLESSInboundOptions{ in.Options = &option.VLESSInboundOptions{
ListenOptions: listen, ListenOptions: listen,
TLS: &tls, InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{
Transport: &t, TLS: &tls,
},
Transport: &t,
Multiplex: multiplex,
} }
} else { } else {
in.Type = "vmess" in.Type = "vmess"
in.VMessOptions = option.VMessInboundOptions{ in.Options = &option.VMessInboundOptions{
ListenOptions: listen, ListenOptions: listen,
TLS: &tls, InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{
Transport: &t, TLS: &tls,
},
Transport: &t,
Multiplex: multiplex,
} }
} }
case "shadowsocks": case "shadowsocks":
@@ -158,37 +234,96 @@ func getInboundOptions(tag string, info *panel.NodeInfo, c *conf.Options) (optio
switch n.Cipher { switch n.Cipher {
case "2022-blake3-aes-128-gcm": case "2022-blake3-aes-128-gcm":
keyLength = 16 keyLength = 16
case "2022-blake3-aes-256-gcm": case "2022-blake3-aes-256-gcm", "2022-blake3-chacha20-poly1305":
keyLength = 32 keyLength = 32
default: default:
keyLength = 16 keyLength = 16
} }
in.ShadowsocksOptions = option.ShadowsocksInboundOptions{ ssoption := &option.ShadowsocksInboundOptions{
ListenOptions: listen, ListenOptions: listen,
Method: n.Cipher, Method: n.Cipher,
Multiplex: multiplex,
} }
p := make([]byte, keyLength) p := make([]byte, keyLength)
_, _ = rand.Read(p) _, _ = rand.Read(p)
randomPasswd := string(p) randomPasswd := string(p)
if strings.Contains(n.Cipher, "2022") { if strings.Contains(n.Cipher, "2022") {
in.ShadowsocksOptions.Password = n.ServerKey ssoption.Password = n.ServerKey
randomPasswd = base64.StdEncoding.EncodeToString([]byte(randomPasswd)) randomPasswd = base64.StdEncoding.EncodeToString([]byte(randomPasswd))
} }
in.ShadowsocksOptions.Users = []option.ShadowsocksUser{{ ssoption.Users = []option.ShadowsocksUser{{
Password: randomPasswd, Password: randomPasswd,
}} }}
in.Options = ssoption
case "trojan": case "trojan":
n := info.Trojan
t := option.V2RayTransportOptions{
Type: n.Network,
}
switch n.Network {
case "tcp":
t.Type = ""
case "ws":
var (
path string
ed int
headers map[string]badoption.Listable[string]
)
if len(n.NetworkSettings) != 0 {
network := WsNetworkConfig{}
err := json.Unmarshal(n.NetworkSettings, &network)
if err != nil {
return option.Inbound{}, fmt.Errorf("decode NetworkSettings error: %s", err)
}
var u *url.URL
u, err = url.Parse(network.Path)
if err != nil {
return option.Inbound{}, fmt.Errorf("parse path error: %s", err)
}
path = u.Path
ed, _ = strconv.Atoi(u.Query().Get("ed"))
headers = make(map[string]badoption.Listable[string], len(network.Headers))
for k, v := range network.Headers {
headers[k] = badoption.Listable[string]{
v,
}
}
}
t.WebsocketOptions = option.V2RayWebsocketOptions{
Path: path,
EarlyDataHeaderName: "Sec-WebSocket-Protocol",
MaxEarlyData: uint32(ed),
Headers: headers,
}
case "grpc":
network := GrpcNetworkConfig{}
if len(n.NetworkSettings) != 0 {
err := json.Unmarshal(n.NetworkSettings, &network)
if err != nil {
return option.Inbound{}, fmt.Errorf("decode NetworkSettings error: %s", err)
}
}
t.GRPCOptions = option.V2RayGRPCOptions{
ServiceName: network.ServiceName,
}
default:
t.Type = ""
}
in.Type = "trojan" in.Type = "trojan"
in.TrojanOptions = option.TrojanInboundOptions{ trojanoption := &option.TrojanInboundOptions{
ListenOptions: listen, ListenOptions: listen,
TLS: &tls, InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{
TLS: &tls,
},
Transport: &t,
Multiplex: multiplex,
} }
if c.SingOptions.FallBackConfigs != nil { if c.SingOptions.FallBackConfigs != nil {
// fallback handling // fallback handling
fallback := c.SingOptions.FallBackConfigs.FallBack fallback := c.SingOptions.FallBackConfigs.FallBack
fallbackPort, err := strconv.Atoi(fallback.ServerPort) fallbackPort, err := strconv.Atoi(fallback.ServerPort)
if err == nil { if err == nil {
in.TrojanOptions.Fallback = &option.ServerOptions{ trojanoption.Fallback = &option.ServerOptions{
Server: fallback.Server, Server: fallback.Server,
ServerPort: uint16(fallbackPort), ServerPort: uint16(fallbackPort),
} }
@@ -196,17 +331,40 @@ func getInboundOptions(tag string, info *panel.NodeInfo, c *conf.Options) (optio
fallbackForALPNMap := c.SingOptions.FallBackConfigs.FallBackForALPN fallbackForALPNMap := c.SingOptions.FallBackConfigs.FallBackForALPN
fallbackForALPN := make(map[string]*option.ServerOptions, len(fallbackForALPNMap)) fallbackForALPN := make(map[string]*option.ServerOptions, len(fallbackForALPNMap))
if err := processFallback(c, fallbackForALPN); err == nil { if err := processFallback(c, fallbackForALPN); err == nil {
in.TrojanOptions.FallbackForALPN = fallbackForALPN trojanoption.FallbackForALPN = fallbackForALPN
} }
} }
in.Options = trojanoption
case "tuic":
in.Type = "tuic"
tls.ALPN = append(tls.ALPN, "h3")
in.Options = &option.TUICInboundOptions{
ListenOptions: listen,
CongestionControl: info.Tuic.CongestionControl,
ZeroRTTHandshake: info.Tuic.ZeroRTTHandshake,
InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{
TLS: &tls,
},
}
case "anytls":
in.Type = "anytls"
in.Options = &option.AnyTLSInboundOptions{
ListenOptions: listen,
PaddingScheme: info.AnyTls.PaddingScheme,
InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{
TLS: &tls,
},
}
case "hysteria": case "hysteria":
in.Type = "hysteria" in.Type = "hysteria"
in.HysteriaOptions = option.HysteriaInboundOptions{ in.Options = &option.HysteriaInboundOptions{
ListenOptions: listen, ListenOptions: listen,
UpMbps: info.Hysteria.UpMbps, UpMbps: info.Hysteria.UpMbps,
DownMbps: info.Hysteria.DownMbps, DownMbps: info.Hysteria.DownMbps,
Obfs: info.Hysteria.Obfs, Obfs: info.Hysteria.Obfs,
TLS: &tls, InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{
TLS: &tls,
},
} }
case "hysteria2": case "hysteria2":
in.Type = "hysteria2" in.Type = "hysteria2"
@@ -222,43 +380,36 @@ func getInboundOptions(tag string, info *panel.NodeInfo, c *conf.Options) (optio
Password: info.Hysteria2.ObfsType, Password: info.Hysteria2.ObfsType,
} }
} }
in.Hysteria2Options = option.Hysteria2InboundOptions{ in.Options = &option.Hysteria2InboundOptions{
ListenOptions: listen, ListenOptions: listen,
UpMbps: info.Hysteria2.UpMbps, UpMbps: info.Hysteria2.UpMbps,
DownMbps: info.Hysteria2.DownMbps, DownMbps: info.Hysteria2.DownMbps,
Obfs: obfs, IgnoreClientBandwidth: info.Hysteria2.Ignore_Client_Bandwidth,
TLS: &tls, Obfs: obfs,
InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{
TLS: &tls,
},
} }
} }
return in, nil return in, nil
} }
func (b *Sing) AddNode(tag string, info *panel.NodeInfo, config *conf.Options) error { func (b *Sing) AddNode(tag string, info *panel.NodeInfo, config *conf.Options) error {
err := updateDNSConfig(info) b.nodeReportMinTrafficBytes[tag] = config.ReportMinTraffic * 1024
if err != nil {
return fmt.Errorf("build dns error: %s", err)
}
c, err := getInboundOptions(tag, info, config) c, err := getInboundOptions(tag, info, config)
if err != nil { if err != nil {
return err return err
} }
in := b.box.Inbound()
in, err := inbound.New( err = in.Create(
b.ctx, b.ctx,
b.box.Router(), b.box.Router(),
b.logFactory.NewLogger(F.ToString("inbound/", c.Type, "[", tag, "]")), b.logFactory.NewLogger(F.ToString("inbound/", c.Type, "[", tag, "]")),
c, tag,
nil, c.Type,
c.Options,
) )
if err != nil {
return fmt.Errorf("init inbound error %s", err)
}
err = in.Start()
if err != nil {
return fmt.Errorf("start inbound error: %s", err)
}
b.inbounds[tag] = in
err = b.router.AddInbound(in)
if err != nil { if err != nil {
return fmt.Errorf("add inbound error: %s", err) return fmt.Errorf("add inbound error: %s", err)
} }
@@ -266,11 +417,8 @@ func (b *Sing) AddNode(tag string, info *panel.NodeInfo, config *conf.Options) e
} }
func (b *Sing) DelNode(tag string) error { func (b *Sing) DelNode(tag string) error {
err := b.inbounds[tag].Close() in := b.box.Inbound()
if err != nil { err := in.Remove(tag)
return fmt.Errorf("close inbound error: %s", err)
}
err = b.router.DelInbound(tag)
if err != nil { if err != nil {
return fmt.Errorf("delete inbound error: %s", err) return fmt.Errorf("delete inbound error: %s", err)
} }

View File

@@ -4,15 +4,17 @@ import (
"context" "context"
"fmt" "fmt"
"os" "os"
"sync"
"github.com/sagernet/sing-box/include"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/InazumaV/V2bX/conf" "github.com/InazumaV/V2bX/conf"
vCore "github.com/InazumaV/V2bX/core" vCore "github.com/InazumaV/V2bX/core"
"github.com/goccy/go-json"
box "github.com/sagernet/sing-box" box "github.com/sagernet/sing-box"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/json"
) )
var _ vCore.Core = (*Sing)(nil) var _ vCore.Core = (*Sing)(nil)
@@ -23,12 +25,18 @@ type DNSConfig struct {
} }
type Sing struct { type Sing struct {
box *box.Box box *box.Box
ctx context.Context ctx context.Context
hookServer *HookServer hookServer *HookServer
router adapter.Router router adapter.Router
logFactory log.Factory logFactory log.Factory
inbounds map[string]adapter.Inbound users *UserMap
nodeReportMinTrafficBytes map[string]int64
}
type UserMap struct {
uidMap map[string]int
mapLock sync.RWMutex
} }
func init() { func init() {
@@ -36,16 +44,17 @@ func init() {
} }
func New(c *conf.CoreConfig) (vCore.Core, error) { func New(c *conf.CoreConfig) (vCore.Core, error) {
ctx := context.Background()
ctx = box.Context(ctx, include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), include.DNSTransportRegistry(), include.ServiceRegistry())
options := option.Options{} options := option.Options{}
if len(c.SingConfig.OriginalPath) != 0 { if len(c.SingConfig.OriginalPath) != 0 {
f, err := os.Open(c.SingConfig.OriginalPath) data, err := os.ReadFile(c.SingConfig.OriginalPath)
if err != nil { if err != nil {
return nil, fmt.Errorf("open original config error: %s", err) return nil, fmt.Errorf("read original config error: %s", err)
} }
defer f.Close() options, err = json.UnmarshalExtendedContext[option.Options](ctx, data)
err = json.NewDecoder(f).Decode(&options)
if err != nil { if err != nil {
return nil, fmt.Errorf("decode original config error: %s", err) return nil, fmt.Errorf("unmarshal original config error: %s", err)
} }
} }
options.Log = &option.LogOptions{ options.Log = &option.LogOptions{
@@ -63,22 +72,6 @@ func New(c *conf.CoreConfig) (vCore.Core, error) {
}, },
} }
os.Setenv("SING_DNS_PATH", "") os.Setenv("SING_DNS_PATH", "")
options.DNS = &option.DNSOptions{}
if c.SingConfig.DnsConfigPath != "" {
f, err := os.OpenFile(c.SingConfig.DnsConfigPath, os.O_RDWR|os.O_CREATE, 0755)
if err != nil {
return nil, fmt.Errorf("failed to open or create sing dns config file: %s", err)
}
defer f.Close()
if err := json.NewDecoder(f).Decode(options.DNS); err != nil {
log.Warn(fmt.Sprintf(
"Failed to unmarshal sing dns config from file '%v': %v. Using default DNS options",
f.Name(), err))
options.DNS = &option.DNSOptions{}
}
os.Setenv("SING_DNS_PATH", c.SingConfig.DnsConfigPath)
}
ctx := context.Background()
b, err := box.New(box.Options{ b, err := box.New(box.Options{
Context: ctx, Context: ctx,
Options: options, Options: options,
@@ -86,15 +79,20 @@ func New(c *conf.CoreConfig) (vCore.Core, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
hs := NewHookServer(c.SingConfig.EnableConnClear) hs := &HookServer{
b.Router().SetClashServer(hs) counter: sync.Map{},
}
b.Router().AppendTracker(hs)
return &Sing{ return &Sing{
ctx: b.Router().GetCtx(), ctx: b.Router().GetCtx(),
box: b, box: b,
hookServer: hs, hookServer: hs,
router: b.Router(), router: b.Router(),
logFactory: b.LogFactory(), logFactory: b.LogFactory(),
inbounds: make(map[string]adapter.Inbound), users: &UserMap{
uidMap: make(map[string]int),
},
nodeReportMinTrafficBytes: make(map[string]int64),
}, nil }, nil
} }
@@ -112,6 +110,8 @@ func (b *Sing) Protocols() []string {
"vless", "vless",
"shadowsocks", "shadowsocks",
"trojan", "trojan",
"tuic",
"anytls",
"hysteria", "hysteria",
"hysteria2", "hysteria2",
} }

View File

@@ -7,33 +7,47 @@ import (
"github.com/InazumaV/V2bX/api/panel" "github.com/InazumaV/V2bX/api/panel"
"github.com/InazumaV/V2bX/common/counter" "github.com/InazumaV/V2bX/common/counter"
"github.com/InazumaV/V2bX/core" "github.com/InazumaV/V2bX/core"
"github.com/sagernet/sing-box/inbound"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/protocol/anytls"
"github.com/sagernet/sing-box/protocol/hysteria"
"github.com/sagernet/sing-box/protocol/hysteria2"
"github.com/sagernet/sing-box/protocol/shadowsocks"
"github.com/sagernet/sing-box/protocol/trojan"
"github.com/sagernet/sing-box/protocol/tuic"
"github.com/sagernet/sing-box/protocol/vless"
"github.com/sagernet/sing-box/protocol/vmess"
) )
func (b *Sing) AddUsers(p *core.AddUsersParams) (added int, err error) { func (b *Sing) AddUsers(p *core.AddUsersParams) (added int, err error) {
in, found := b.box.Inbound().Get(p.Tag)
if !found {
return 0, errors.New("the inbound not found")
}
b.users.mapLock.Lock()
defer b.users.mapLock.Unlock()
for i := range p.Users {
b.users.uidMap[p.Users[i].Uuid] = p.Users[i].Id
}
switch p.NodeInfo.Type { switch p.NodeInfo.Type {
case "vmess", "vless": case "vless":
if p.NodeInfo.Type == "vless" { us := make([]option.VLESSUser, len(p.Users))
us := make([]option.VLESSUser, len(p.Users)) for i := range p.Users {
for i := range p.Users { us[i] = option.VLESSUser{
us[i] = option.VLESSUser{ Name: p.Users[i].Uuid,
Name: p.Users[i].Uuid, Flow: p.VAllss.Flow,
Flow: p.VAllss.Flow, UUID: p.Users[i].Uuid,
UUID: p.Users[i].Uuid,
}
} }
err = b.inbounds[p.Tag].(*inbound.VLESS).AddUsers(us)
} else {
us := make([]option.VMessUser, len(p.Users))
for i := range p.Users {
us[i] = option.VMessUser{
Name: p.Users[i].Uuid,
UUID: p.Users[i].Uuid,
}
}
err = b.inbounds[p.Tag].(*inbound.VMess).AddUsers(us)
} }
err = in.(*vless.Inbound).AddUsers(us)
case "vmess":
us := make([]option.VMessUser, len(p.Users))
for i := range p.Users {
us[i] = option.VMessUser{
Name: p.Users[i].Uuid,
UUID: p.Users[i].Uuid,
}
}
err = in.(*vmess.Inbound).AddUsers(us)
case "shadowsocks": case "shadowsocks":
us := make([]option.ShadowsocksUser, len(p.Users)) us := make([]option.ShadowsocksUser, len(p.Users))
for i := range p.Users { for i := range p.Users {
@@ -49,7 +63,7 @@ func (b *Sing) AddUsers(p *core.AddUsersParams) (added int, err error) {
Password: password, Password: password,
} }
} }
err = b.inbounds[p.Tag].(*inbound.ShadowsocksMulti).AddUsers(us) err = in.(*shadowsocks.MultiInbound).AddUsers(us)
case "trojan": case "trojan":
us := make([]option.TrojanUser, len(p.Users)) us := make([]option.TrojanUser, len(p.Users))
for i := range p.Users { for i := range p.Users {
@@ -58,7 +72,19 @@ func (b *Sing) AddUsers(p *core.AddUsersParams) (added int, err error) {
Password: p.Users[i].Uuid, Password: p.Users[i].Uuid,
} }
} }
err = b.inbounds[p.Tag].(*inbound.Trojan).AddUsers(us) err = in.(*trojan.Inbound).AddUsers(us)
case "tuic":
us := make([]option.TUICUser, len(p.Users))
id := make([]int, len(p.Users))
for i := range p.Users {
us[i] = option.TUICUser{
Name: p.Users[i].Uuid,
UUID: p.Users[i].Uuid,
Password: p.Users[i].Uuid,
}
id[i] = p.Users[i].Id
}
err = in.(*tuic.Inbound).AddUsers(us, id)
case "hysteria": case "hysteria":
us := make([]option.HysteriaUser, len(p.Users)) us := make([]option.HysteriaUser, len(p.Users))
for i := range p.Users { for i := range p.Users {
@@ -67,16 +93,27 @@ func (b *Sing) AddUsers(p *core.AddUsersParams) (added int, err error) {
AuthString: p.Users[i].Uuid, AuthString: p.Users[i].Uuid,
} }
} }
err = b.inbounds[p.Tag].(*inbound.Hysteria).AddUsers(us) err = in.(*hysteria.Inbound).AddUsers(us)
case "hysteria2": case "hysteria2":
us := make([]option.Hysteria2User, len(p.Users)) us := make([]option.Hysteria2User, len(p.Users))
id := make([]int, len(p.Users))
for i := range p.Users { for i := range p.Users {
us[i] = option.Hysteria2User{ us[i] = option.Hysteria2User{
Name: p.Users[i].Uuid, Name: p.Users[i].Uuid,
Password: p.Users[i].Uuid, Password: p.Users[i].Uuid,
} }
id[i] = p.Users[i].Id
} }
err = b.inbounds[p.Tag].(*inbound.Hysteria2).AddUsers(us) err = in.(*hysteria2.Inbound).AddUsers(us, id)
case "anytls":
us := make([]option.AnyTLSUser, len(p.Users))
for i := range p.Users {
us[i] = option.AnyTLSUser{
Name: p.Users[i].Uuid,
Password: p.Users[i].Uuid,
}
}
err = in.(*anytls.Inbound).AddUsers(us)
} }
if err != nil { if err != nil {
return 0, err return 0, err
@@ -97,33 +134,80 @@ func (b *Sing) GetUserTraffic(tag, uuid string, reset bool) (up int64, down int6
return 0, 0 return 0, 0
} }
func (b *Sing) GetUserTrafficSlice(tag string, reset bool) ([]panel.UserTraffic, error) {
trafficSlice := make([]panel.UserTraffic, 0)
hook := b.hookServer
b.users.mapLock.RLock()
defer b.users.mapLock.RUnlock()
if v, ok := hook.counter.Load(tag); ok {
c := v.(*counter.TrafficCounter)
c.Counters.Range(func(key, value interface{}) bool {
uuid := key.(string)
traffic := value.(*counter.TrafficStorage)
up := traffic.UpCounter.Load()
down := traffic.DownCounter.Load()
if up+down > b.nodeReportMinTrafficBytes[tag] {
if reset {
traffic.UpCounter.Store(0)
traffic.DownCounter.Store(0)
}
if b.users.uidMap[uuid] == 0 {
c.Delete(uuid)
return true
}
trafficSlice = append(trafficSlice, panel.UserTraffic{
UID: b.users.uidMap[uuid],
Upload: up,
Download: down,
})
}
return true
})
if len(trafficSlice) == 0 {
return nil, nil
}
return trafficSlice, nil
}
return nil, nil
}
type UserDeleter interface { type UserDeleter interface {
DelUsers(uuid []string) error DelUsers(uuid []string) error
} }
func (b *Sing) DelUsers(users []panel.UserInfo, tag string) error { func (b *Sing) DelUsers(users []panel.UserInfo, tag string, info *panel.NodeInfo) error {
var del UserDeleter var del UserDeleter
if i, ok := b.inbounds[tag]; ok { if i, found := b.box.Inbound().Get(tag); found {
switch i.Type() { switch info.Type {
case "vmess": case "vmess":
del = i.(*inbound.VMess) del = i.(*vmess.Inbound)
case "vless": case "vless":
del = i.(*inbound.VLESS) del = i.(*vless.Inbound)
case "shadowsocks": case "shadowsocks":
del = i.(*inbound.ShadowsocksMulti) del = i.(*shadowsocks.MultiInbound)
case "trojan": case "trojan":
del = i.(*inbound.Trojan) del = i.(*trojan.Inbound)
case "tuic":
del = i.(*tuic.Inbound)
case "hysteria": case "hysteria":
del = i.(*inbound.Hysteria) del = i.(*hysteria.Inbound)
case "hysteria2": case "hysteria2":
del = i.(*inbound.Hysteria2) del = i.(*hysteria2.Inbound)
case "anytls":
del = i.(*anytls.Inbound)
} }
} else { } else {
return errors.New("the inbound not found") return errors.New("the inbound not found")
} }
uuids := make([]string, len(users)) uuids := make([]string, len(users))
b.users.mapLock.Lock()
defer b.users.mapLock.Unlock()
for i := range users { for i := range users {
b.hookServer.ClearConn(tag, users[i].Uuid) if v, ok := b.hookServer.counter.Load(tag); ok {
c := v.(*counter.TrafficCounter)
c.Delete(users[i].Uuid)
}
delete(b.users.uidMap, users[i].Uuid)
uuids[i] = users[i].Uuid uuids[i] = users[i].Uuid
} }
err := del.DelUsers(uuids) err := del.DelUsers(uuids)

View File

@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.31.0 // protoc-gen-go v1.35.1
// protoc v4.23.4 // protoc v3.21.12
// source: config.proto // source: config.proto
package dispatcher package dispatcher
@@ -28,11 +28,9 @@ type SessionConfig struct {
func (x *SessionConfig) Reset() { func (x *SessionConfig) Reset() {
*x = SessionConfig{} *x = SessionConfig{}
if protoimpl.UnsafeEnabled { mi := &file_config_proto_msgTypes[0]
mi := &file_config_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi)
ms.StoreMessageInfo(mi)
}
} }
func (x *SessionConfig) String() string { func (x *SessionConfig) String() string {
@@ -43,7 +41,7 @@ func (*SessionConfig) ProtoMessage() {}
func (x *SessionConfig) ProtoReflect() protoreflect.Message { func (x *SessionConfig) ProtoReflect() protoreflect.Message {
mi := &file_config_proto_msgTypes[0] mi := &file_config_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
@@ -68,11 +66,9 @@ type Config struct {
func (x *Config) Reset() { func (x *Config) Reset() {
*x = Config{} *x = Config{}
if protoimpl.UnsafeEnabled { mi := &file_config_proto_msgTypes[1]
mi := &file_config_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi)
ms.StoreMessageInfo(mi)
}
} }
func (x *Config) String() string { func (x *Config) String() string {
@@ -83,7 +79,7 @@ func (*Config) ProtoMessage() {}
func (x *Config) ProtoReflect() protoreflect.Message { func (x *Config) ProtoReflect() protoreflect.Message {
mi := &file_config_proto_msgTypes[1] mi := &file_config_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
@@ -140,7 +136,7 @@ func file_config_proto_rawDescGZIP() []byte {
} }
var file_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2) var file_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_config_proto_goTypes = []interface{}{ var file_config_proto_goTypes = []any{
(*SessionConfig)(nil), // 0: v2bx.core.app.dispatcher.SessionConfig (*SessionConfig)(nil), // 0: v2bx.core.app.dispatcher.SessionConfig
(*Config)(nil), // 1: v2bx.core.app.dispatcher.Config (*Config)(nil), // 1: v2bx.core.app.dispatcher.Config
} }
@@ -158,32 +154,6 @@ func file_config_proto_init() {
if File_config_proto != nil { if File_config_proto != nil {
return return
} }
if !protoimpl.UnsafeEnabled {
file_config_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*SessionConfig); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_config_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Config); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{} type x struct{}
out := protoimpl.TypeBuilder{ out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{ File: protoimpl.DescBuilder{

View File

@@ -0,0 +1,37 @@
package dispatcher
import (
"sync/atomic"
"time"
"github.com/xtls/xray-core/common/buf"
)
var _ buf.TimeoutReader = (*CounterReader)(nil)
type CounterReader struct {
Reader buf.TimeoutReader
Counter *atomic.Int64
}
func (c *CounterReader) ReadMultiBufferTimeout(time.Duration) (buf.MultiBuffer, error) {
mb, err := c.Reader.ReadMultiBufferTimeout(time.Second)
if err != nil {
return nil, err
}
if mb.Len() > 0 {
c.Counter.Add(int64(mb.Len()))
}
return mb, nil
}
func (c *CounterReader) ReadMultiBuffer() (buf.MultiBuffer, error) {
mb, err := c.Reader.ReadMultiBuffer()
if err != nil {
return nil, err
}
if mb.Len() > 0 {
c.Counter.Add(int64(mb.Len()))
}
return mb, nil
}

View File

@@ -5,15 +5,19 @@ package dispatcher
import ( import (
"context" "context"
"fmt" "fmt"
"regexp"
"strings" "strings"
"sync" "sync"
"time" "time"
"github.com/InazumaV/V2bX/common/counter"
"github.com/InazumaV/V2bX/common/rate" "github.com/InazumaV/V2bX/common/rate"
"github.com/InazumaV/V2bX/limiter" "github.com/InazumaV/V2bX/limiter"
"github.com/xtls/xray-core/app/dispatcher"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/log" "github.com/xtls/xray-core/common/log"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/protocol" "github.com/xtls/xray-core/common/protocol"
@@ -29,25 +33,29 @@ import (
"github.com/xtls/xray-core/transport/pipe" "github.com/xtls/xray-core/transport/pipe"
) )
var errSniffingTimeout = newError("timeout on sniffing") var errSniffingTimeout = errors.New("timeout on sniffing")
type cachedReader struct { type cachedReader struct {
sync.Mutex sync.Mutex
reader *pipe.Reader reader buf.TimeoutReader
cache buf.MultiBuffer cache buf.MultiBuffer
} }
func (r *cachedReader) Cache(b *buf.Buffer) { func (r *cachedReader) Cache(b *buf.Buffer, deadline time.Duration) error {
mb, _ := r.reader.ReadMultiBufferTimeout(time.Millisecond * 100) mb, err := r.reader.ReadMultiBufferTimeout(deadline)
if err != nil {
return err
}
r.Lock() r.Lock()
if !mb.IsEmpty() { if !mb.IsEmpty() {
r.cache, _ = buf.MergeMulti(r.cache, mb) r.cache, _ = buf.MergeMulti(r.cache, mb)
} }
b.Clear() b.Clear()
rawBytes := b.Extend(buf.Size) rawBytes := b.Extend(min(r.cache.Len(), b.Cap()))
n := r.cache.Copy(rawBytes) n := r.cache.Copy(rawBytes)
b.Resize(0, int32(n)) b.Resize(0, int32(n))
r.Unlock() r.Unlock()
return nil
} }
func (r *cachedReader) readInternal() buf.MultiBuffer { func (r *cachedReader) readInternal() buf.MultiBuffer {
@@ -87,27 +95,30 @@ func (r *cachedReader) Interrupt() {
r.cache = buf.ReleaseMulti(r.cache) r.cache = buf.ReleaseMulti(r.cache)
} }
r.Unlock() r.Unlock()
r.reader.Interrupt() if p, ok := r.reader.(*pipe.Reader); ok {
p.Interrupt()
}
} }
// DefaultDispatcher is a default implementation of Dispatcher. // DefaultDispatcher is a default implementation of Dispatcher.
type DefaultDispatcher struct { type DefaultDispatcher struct {
ohm outbound.Manager ohm outbound.Manager
router routing.Router router routing.Router
policy policy.Manager policy policy.Manager
stats stats.Manager stats stats.Manager
dns dns.Client fdns dns.FakeDNSEngine
fdns dns.FakeDNSEngine Counter sync.Map
LinkManagers sync.Map // map[string]*LinkManager
} }
func init() { func init() {
common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
d := new(DefaultDispatcher) d := new(DefaultDispatcher)
if err := core.RequireFeatures(ctx, func(om outbound.Manager, router routing.Router, pm policy.Manager, sm stats.Manager, dc dns.Client) error { if err := core.RequireFeatures(ctx, func(om outbound.Manager, router routing.Router, pm policy.Manager, sm stats.Manager, dc dns.Client) error {
core.RequireFeatures(ctx, func(fdns dns.FakeDNSEngine) { core.OptionalFeatures(ctx, func(fdns dns.FakeDNSEngine) {
d.fdns = fdns d.fdns = fdns
}) })
return d.Init(config.(*Config), om, router, pm, sm, dc) return d.Init(config.(*Config), om, router, pm, sm)
}); err != nil { }); err != nil {
return nil, err return nil, err
} }
@@ -116,12 +127,11 @@ func init() {
} }
// Init initializes DefaultDispatcher. // Init initializes DefaultDispatcher.
func (d *DefaultDispatcher) Init(config *Config, om outbound.Manager, router routing.Router, pm policy.Manager, sm stats.Manager, dns dns.Client) error { func (d *DefaultDispatcher) Init(config *Config, om outbound.Manager, router routing.Router, pm policy.Manager, sm stats.Manager) error {
d.ohm = om d.ohm = om
d.router = router d.router = router
d.policy = pm d.policy = pm
d.stats = sm d.stats = sm
d.dns = dns
return nil return nil
} }
@@ -164,47 +174,64 @@ func (d *DefaultDispatcher) getLink(ctx context.Context, network net.Network) (*
if user != nil && len(user.Email) > 0 { if user != nil && len(user.Email) > 0 {
limit, err = limiter.GetLimiter(sessionInbound.Tag) limit, err = limiter.GetLimiter(sessionInbound.Tag)
if err != nil { if err != nil {
newError("get limiter ", sessionInbound.Tag, " error: ", err).AtError().WriteToLog() errors.LogInfo(ctx, "get limiter ", sessionInbound.Tag, " error: ", err)
common.Close(outboundLink.Writer) common.Close(outboundLink.Writer)
common.Close(inboundLink.Writer) common.Close(inboundLink.Writer)
common.Interrupt(outboundLink.Reader) common.Interrupt(outboundLink.Reader)
common.Interrupt(inboundLink.Reader) common.Interrupt(inboundLink.Reader)
return nil, nil, nil, newError("get limiter ", sessionInbound.Tag, " error: ", err) return nil, nil, nil, errors.New("get limiter ", sessionInbound.Tag, " error: ", err)
} }
// Speed Limit and Device Limit // Speed Limit and Device Limit
w, reject := limit.CheckLimit(user.Email, w, reject := limit.CheckLimit(user.Email,
sessionInbound.Source.Address.IP().String(), sessionInbound.Source.Address.IP().String(),
network == net.Network_TCP) network == net.Network_TCP,
sessionInbound.Source.Network == net.Network_TCP)
if reject { if reject {
newError("Limited ", user.Email, " by conn or ip").AtWarning().WriteToLog() errors.LogInfo(ctx, "Limited ", user.Email, " by conn or ip")
common.Close(outboundLink.Writer) common.Close(outboundLink.Writer)
common.Close(inboundLink.Writer) common.Close(inboundLink.Writer)
common.Interrupt(outboundLink.Reader) common.Interrupt(outboundLink.Reader)
common.Interrupt(inboundLink.Reader) common.Interrupt(inboundLink.Reader)
return nil, nil, nil, newError("Limited ", user.Email, " by conn or ip") return nil, nil, nil, errors.New("Limited ", user.Email, " by conn or ip")
} }
var lm *LinkManager
if lmloaded, ok := d.LinkManagers.Load(user.Email); !ok {
lm = &LinkManager{
links: make(map[*ManagedWriter]buf.Reader),
}
d.LinkManagers.Store(user.Email, lm)
} else {
lm = lmloaded.(*LinkManager)
}
managedWriter := &ManagedWriter{
writer: uplinkWriter,
manager: lm,
}
lm.AddLink(managedWriter, outboundLink.Reader)
inboundLink.Writer = managedWriter
if w != nil { if w != nil {
sessionInbound.CanSpliceCopy = 3
inboundLink.Writer = rate.NewRateLimitWriter(inboundLink.Writer, w) inboundLink.Writer = rate.NewRateLimitWriter(inboundLink.Writer, w)
outboundLink.Writer = rate.NewRateLimitWriter(outboundLink.Writer, w) outboundLink.Writer = rate.NewRateLimitWriter(outboundLink.Writer, w)
} }
p := d.policy.ForLevel(user.Level) var t *counter.TrafficCounter
if p.Stats.UserUplink { if c, ok := d.Counter.Load(sessionInbound.Tag); !ok {
name := "user>>>" + user.Email + ">>>traffic>>>uplink" t = counter.NewTrafficCounter()
if c, _ := stats.GetOrRegisterCounter(d.stats, name); c != nil { d.Counter.Store(sessionInbound.Tag, t)
inboundLink.Writer = &SizeStatWriter{ } else {
Counter: c, t = c.(*counter.TrafficCounter)
Writer: inboundLink.Writer,
}
}
} }
if p.Stats.UserDownlink {
name := "user>>>" + user.Email + ">>>traffic>>>downlink" ts := t.GetCounter(user.Email)
if c, _ := stats.GetOrRegisterCounter(d.stats, name); c != nil { upcounter := &counter.XrayTrafficCounter{V: &ts.UpCounter}
outboundLink.Writer = &SizeStatWriter{ downcounter := &counter.XrayTrafficCounter{V: &ts.DownCounter}
Counter: c, inboundLink.Writer = &dispatcher.SizeStatWriter{
Writer: outboundLink.Writer, Counter: upcounter,
} Writer: inboundLink.Writer,
} }
outboundLink.Writer = &dispatcher.SizeStatWriter{
Counter: downcounter,
Writer: outboundLink.Writer,
} }
} }
@@ -217,8 +244,20 @@ func (d *DefaultDispatcher) shouldOverride(ctx context.Context, result SniffResu
return false return false
} }
for _, d := range request.ExcludeForDomain { for _, d := range request.ExcludeForDomain {
if strings.ToLower(domain) == d { if strings.HasPrefix(d, "regexp:") {
return false pattern := d[7:]
re, err := regexp.Compile(pattern)
if err != nil {
errors.LogInfo(ctx, "Unable to compile regex")
continue
}
if re.MatchString(domain) {
return false
}
} else {
if strings.ToLower(domain) == d {
return false
}
} }
} }
protocolString := result.Protocol() protocolString := result.Protocol()
@@ -226,12 +265,12 @@ func (d *DefaultDispatcher) shouldOverride(ctx context.Context, result SniffResu
protocolString = resComp.ProtocolForDomainResult() protocolString = resComp.ProtocolForDomainResult()
} }
for _, p := range request.OverrideDestinationForProtocol { for _, p := range request.OverrideDestinationForProtocol {
if strings.HasPrefix(protocolString, p) || strings.HasPrefix(protocolString, p) { if strings.HasPrefix(protocolString, p) || strings.HasPrefix(p, protocolString) {
return true return true
} }
if fkr0, ok := d.fdns.(dns.FakeDNSEngineRev0); ok && protocolString != "bittorrent" && p == "fakedns" && if fkr0, ok := d.fdns.(dns.FakeDNSEngineRev0); ok && protocolString != "bittorrent" && p == "fakedns" &&
destination.Address.Family().IsIP() && fkr0.IsIPInIPPool(destination.Address) { destination.Address.Family().IsIP() && fkr0.IsIPInIPPool(destination.Address) {
newError("Using sniffer ", protocolString, " since the fake DNS missed").WriteToLog(session.ExportIDToError(ctx)) errors.LogInfo(ctx, "Using sniffer ", protocolString, " since the fake DNS missed")
return true return true
} }
if resultSubset, ok := result.(SnifferIsProtoSubsetOf); ok { if resultSubset, ok := result.(SnifferIsProtoSubsetOf); ok {
@@ -249,11 +288,14 @@ func (d *DefaultDispatcher) Dispatch(ctx context.Context, destination net.Destin
if !destination.IsValid() { if !destination.IsValid() {
panic("Dispatcher: Invalid destination.") panic("Dispatcher: Invalid destination.")
} }
ob := &session.Outbound{ outbounds := session.OutboundsFromContext(ctx)
OriginalTarget: destination, if len(outbounds) == 0 {
Target: destination, outbounds = []*session.Outbound{{}}
ctx = session.ContextWithOutbounds(ctx, outbounds)
} }
ctx = session.ContextWithOutbound(ctx, ob) ob := outbounds[len(outbounds)-1]
ob.OriginalTarget = destination
ob.Target = destination
content := session.ContentFromContext(ctx) content := session.ContentFromContext(ctx)
if content == nil { if content == nil {
content = new(session.Content) content = new(session.Content)
@@ -278,7 +320,7 @@ func (d *DefaultDispatcher) Dispatch(ctx context.Context, destination net.Destin
} }
if err == nil && d.shouldOverride(ctx, result, sniffingRequest, destination) { if err == nil && d.shouldOverride(ctx, result, sniffingRequest, destination) {
domain := result.Domain() domain := result.Domain()
newError("sniffed domain: ", domain).WriteToLog(session.ExportIDToError(ctx)) errors.LogInfo(ctx, "sniffed domain: ", domain)
destination.Address = net.ParseAddress(domain) destination.Address = net.ParseAddress(domain)
protocol := result.Protocol() protocol := result.Protocol()
if resComp, ok := result.(SnifferResultComposite); ok { if resComp, ok := result.(SnifferResultComposite); ok {
@@ -303,24 +345,94 @@ func (d *DefaultDispatcher) Dispatch(ctx context.Context, destination net.Destin
// DispatchLink implements routing.Dispatcher. // DispatchLink implements routing.Dispatcher.
func (d *DefaultDispatcher) DispatchLink(ctx context.Context, destination net.Destination, outbound *transport.Link) error { func (d *DefaultDispatcher) DispatchLink(ctx context.Context, destination net.Destination, outbound *transport.Link) error {
if !destination.IsValid() { if !destination.IsValid() {
return newError("Dispatcher: Invalid destination.") return errors.New("Dispatcher: Invalid destination.")
} }
ob := &session.Outbound{ outbounds := session.OutboundsFromContext(ctx)
OriginalTarget: destination, if len(outbounds) == 0 {
Target: destination, outbounds = []*session.Outbound{{}}
ctx = session.ContextWithOutbounds(ctx, outbounds)
} }
ctx = session.ContextWithOutbound(ctx, ob) ob := outbounds[len(outbounds)-1]
ob.OriginalTarget = destination
ob.Target = destination
content := session.ContentFromContext(ctx) content := session.ContentFromContext(ctx)
if content == nil { if content == nil {
content = new(session.Content) content = new(session.Content)
ctx = session.ContextWithContent(ctx, content) ctx = session.ContextWithContent(ctx, content)
} }
sessionInbound := session.InboundFromContext(ctx)
var user *protocol.MemoryUser
if sessionInbound != nil {
user = sessionInbound.User
}
var limit *limiter.Limiter
var err error
if user != nil && len(user.Email) > 0 {
limit, err = limiter.GetLimiter(sessionInbound.Tag)
if err != nil {
errors.LogInfo(ctx, "get limiter ", sessionInbound.Tag, " error: ", err)
common.Close(outbound.Writer)
common.Interrupt(outbound.Reader)
return errors.New("get limiter ", sessionInbound.Tag, " error: ", err)
}
// Speed Limit and Device Limit
w, reject := limit.CheckLimit(user.Email,
sessionInbound.Source.Address.IP().String(),
destination.Network == net.Network_TCP,
sessionInbound.Source.Network == net.Network_TCP)
if reject {
errors.LogInfo(ctx, "Limited ", user.Email, " by conn or ip")
common.Close(outbound.Writer)
common.Interrupt(outbound.Reader)
return errors.New("Limited ", user.Email, " by conn or ip")
}
var lm *LinkManager
if lmloaded, ok := d.LinkManagers.Load(user.Email); !ok {
lm = &LinkManager{
links: make(map[*ManagedWriter]buf.Reader),
}
d.LinkManagers.Store(user.Email, lm)
} else {
lm = lmloaded.(*LinkManager)
}
managedWriter := &ManagedWriter{
writer: outbound.Writer,
manager: lm,
}
outbound.Writer = managedWriter
if w != nil {
sessionInbound.CanSpliceCopy = 3
outbound.Writer = rate.NewRateLimitWriter(outbound.Writer, w)
}
var t *counter.TrafficCounter
if c, ok := d.Counter.Load(sessionInbound.Tag); !ok {
t = counter.NewTrafficCounter()
d.Counter.Store(sessionInbound.Tag, t)
} else {
t = c.(*counter.TrafficCounter)
}
ts := t.GetCounter(user.Email)
downcounter := &counter.XrayTrafficCounter{V: &ts.DownCounter}
outbound.Reader = &CounterReader{
Reader: &buf.TimeoutWrapperReader{Reader: outbound.Reader},
Counter: &ts.UpCounter,
}
lm.AddLink(managedWriter, outbound.Reader)
outbound.Writer = &dispatcher.SizeStatWriter{
Counter: downcounter,
Writer: outbound.Writer,
}
}
sniffingRequest := content.SniffingRequest sniffingRequest := content.SniffingRequest
if !sniffingRequest.Enabled { if !sniffingRequest.Enabled {
d.routedDispatch(ctx, outbound, destination, nil, "") d.routedDispatch(ctx, outbound, destination, limit, "")
} else { } else {
cReader := &cachedReader{ cReader := &cachedReader{
reader: outbound.Reader.(*pipe.Reader), reader: outbound.Reader.(buf.TimeoutReader),
} }
outbound.Reader = cReader outbound.Reader = cReader
result, err := sniffer(ctx, cReader, sniffingRequest.MetadataOnly, destination.Network) result, err := sniffer(ctx, cReader, sniffingRequest.MetadataOnly, destination.Network)
@@ -329,14 +441,14 @@ func (d *DefaultDispatcher) DispatchLink(ctx context.Context, destination net.De
} }
if err == nil && d.shouldOverride(ctx, result, sniffingRequest, destination) { if err == nil && d.shouldOverride(ctx, result, sniffingRequest, destination) {
domain := result.Domain() domain := result.Domain()
newError("sniffed domain: ", domain).WriteToLog(session.ExportIDToError(ctx)) errors.LogInfo(ctx, "sniffed domain: ", domain)
destination.Address = net.ParseAddress(domain) destination.Address = net.ParseAddress(domain)
protocol := result.Protocol() protocol := result.Protocol()
if resComp, ok := result.(SnifferResultComposite); ok { if resComp, ok := result.(SnifferResultComposite); ok {
protocol = resComp.ProtocolForDomainResult() protocol = resComp.ProtocolForDomainResult()
} }
isFakeIP := false isFakeIP := false
if fkr0, ok := d.fdns.(dns.FakeDNSEngineRev0); ok && ob.Target.Address.Family().IsIP() && fkr0.IsIPInIPPool(ob.Target.Address) { if fkr0, ok := d.fdns.(dns.FakeDNSEngineRev0); ok && fkr0.IsIPInIPPool(ob.Target.Address) {
isFakeIP = true isFakeIP = true
} }
if sniffingRequest.RouteOnly && protocol != "fakedns" && protocol != "fakedns+others" && !isFakeIP { if sniffingRequest.RouteOnly && protocol != "fakedns" && protocol != "fakedns+others" && !isFakeIP {
@@ -344,16 +456,15 @@ func (d *DefaultDispatcher) DispatchLink(ctx context.Context, destination net.De
} else { } else {
ob.Target = destination ob.Target = destination
} }
destination.Address.Family()
} }
d.routedDispatch(ctx, outbound, destination, nil, content.Protocol) d.routedDispatch(ctx, outbound, destination, limit, content.Protocol)
} }
return nil return nil
} }
func sniffer(ctx context.Context, cReader *cachedReader, metadataOnly bool, network net.Network) (SniffResult, error) { func sniffer(ctx context.Context, cReader *cachedReader, metadataOnly bool, network net.Network) (SniffResult, error) {
payload := buf.New() payload := buf.NewWithSize(32767)
defer payload.Release() defer payload.Release()
sniffer := NewSniffer(ctx) sniffer := NewSniffer(ctx)
@@ -365,26 +476,36 @@ func sniffer(ctx context.Context, cReader *cachedReader, metadataOnly bool, netw
} }
contentResult, contentErr := func() (SniffResult, error) { contentResult, contentErr := func() (SniffResult, error) {
cacheDeadline := 200 * time.Millisecond
totalAttempt := 0 totalAttempt := 0
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
return nil, ctx.Err() return nil, ctx.Err()
default: default:
totalAttempt++ cachingStartingTimeStamp := time.Now()
if totalAttempt > 2 { err := cReader.Cache(payload, cacheDeadline)
return nil, errSniffingTimeout if err != nil {
return nil, err
} }
cachingTimeElapsed := time.Since(cachingStartingTimeStamp)
cacheDeadline -= cachingTimeElapsed
cReader.Cache(payload)
if !payload.IsEmpty() { if !payload.IsEmpty() {
result, err := sniffer.Sniff(ctx, payload.Bytes(), network) result, err := sniffer.Sniff(ctx, payload.Bytes(), network)
if err != common.ErrNoClue { switch err {
case common.ErrNoClue: // No Clue: protocol not matches, and sniffer cannot determine whether there will be a match or not
totalAttempt++
case protocol.ErrProtoNeedMoreData: // Protocol Need More Data: protocol matches, but need more data to complete sniffing
// in this case, do not add totalAttempt(allow to read until timeout)
default:
return result, err return result, err
} }
} else {
totalAttempt++
} }
if payload.IsFull() { if totalAttempt >= 2 || cacheDeadline <= 0 {
return nil, errUnknownContent return nil, errSniffingTimeout
} }
} }
} }
@@ -399,34 +520,16 @@ func sniffer(ctx context.Context, cReader *cachedReader, metadataOnly bool, netw
} }
func (d *DefaultDispatcher) routedDispatch(ctx context.Context, link *transport.Link, destination net.Destination, l *limiter.Limiter, protocol string) { func (d *DefaultDispatcher) routedDispatch(ctx context.Context, link *transport.Link, destination net.Destination, l *limiter.Limiter, protocol string) {
ob := session.OutboundFromContext(ctx) outbounds := session.OutboundsFromContext(ctx)
if hosts, ok := d.dns.(dns.HostsLookup); ok && destination.Address.Family().IsDomain() { ob := outbounds[len(outbounds)-1]
proxied := hosts.LookupHosts(ob.Target.String())
if proxied != nil {
ro := ob.RouteTarget == destination
destination.Address = *proxied
if ro {
ob.RouteTarget = destination
} else {
ob.Target = destination
}
}
}
sessionInbound := session.InboundFromContext(ctx) sessionInbound := session.InboundFromContext(ctx)
if sessionInbound.User != nil { if sessionInbound.User != nil {
if l != nil { if l == nil {
// del connect count
if destination.Network == net.Network_TCP {
defer func() {
l.ConnLimiter.DelConnCount(sessionInbound.User.Email, sessionInbound.Source.Address.IP().String())
}()
}
} else {
var err error var err error
l, err = limiter.GetLimiter(sessionInbound.Tag) l, err = limiter.GetLimiter(sessionInbound.Tag)
if err != nil { if err != nil {
newError("get limiter ", sessionInbound.Tag, " error: ", err).AtWarning().WriteToLog(session.ExportIDToError(ctx)) errors.LogError(ctx, "get limiter ", sessionInbound.Tag, " error: ", err)
} }
} }
if l != nil { if l != nil {
@@ -437,20 +540,20 @@ func (d *DefaultDispatcher) routedDispatch(ctx context.Context, link *transport.
destStr = destination.Address.IP().String() destStr = destination.Address.IP().String()
} }
if l.CheckDomainRule(destStr) { if l.CheckDomainRule(destStr) {
newError(fmt.Sprintf( errors.LogError(ctx, fmt.Sprintf(
"User %s access domain %s reject by rule", "User %s access domain %s reject by rule",
sessionInbound.User.Email, sessionInbound.User.Email,
destStr)).AtWarning().WriteToLog(session.ExportIDToError(ctx)) destStr))
common.Close(link.Writer) common.Close(link.Writer)
common.Interrupt(link.Reader) common.Interrupt(link.Reader)
return return
} }
if len(protocol) != 0 { if len(protocol) != 0 {
if l.CheckProtocolRule(protocol) { if l.CheckProtocolRule(protocol) {
newError(fmt.Sprintf( errors.LogError(ctx, fmt.Sprintf(
"User %s access protocol %s reject by rule", "User %s access protocol %s reject by rule",
sessionInbound.User.Email, sessionInbound.User.Email,
protocol)).AtWarning().WriteToLog(session.ExportIDToError(ctx)) protocol))
common.Close(link.Writer) common.Close(link.Writer)
common.Interrupt(link.Reader) common.Interrupt(link.Reader)
return return
@@ -468,10 +571,10 @@ func (d *DefaultDispatcher) routedDispatch(ctx context.Context, link *transport.
ctx = session.SetForcedOutboundTagToContext(ctx, "") ctx = session.SetForcedOutboundTagToContext(ctx, "")
if h := d.ohm.GetHandler(forcedOutboundTag); h != nil { if h := d.ohm.GetHandler(forcedOutboundTag); h != nil {
isPickRoute = 1 isPickRoute = 1
newError("taking platform initialized detour [", forcedOutboundTag, "] for [", destination, "]").WriteToLog(session.ExportIDToError(ctx)) errors.LogInfo(ctx, "taking platform initialized detour [", forcedOutboundTag, "] for [", destination, "]")
handler = h handler = h
} else { } else {
newError("non existing tag for platform initialized detour: ", forcedOutboundTag).AtError().WriteToLog(session.ExportIDToError(ctx)) errors.LogError(ctx, "non existing tag for platform initialized detour: ", forcedOutboundTag)
common.Close(link.Writer) common.Close(link.Writer)
common.Interrupt(link.Reader) common.Interrupt(link.Reader)
return return
@@ -481,31 +584,35 @@ func (d *DefaultDispatcher) routedDispatch(ctx context.Context, link *transport.
outTag := route.GetOutboundTag() outTag := route.GetOutboundTag()
if h := d.ohm.GetHandler(outTag); h != nil { if h := d.ohm.GetHandler(outTag); h != nil {
isPickRoute = 2 isPickRoute = 2
newError("taking detour [", outTag, "] for [", destination, "]").WriteToLog(session.ExportIDToError(ctx)) if route.GetRuleTag() == "" {
errors.LogInfo(ctx, "taking detour [", outTag, "] for [", destination, "]")
} else {
errors.LogInfo(ctx, "Hit route rule: [", route.GetRuleTag(), "] so taking detour [", outTag, "] for [", destination, "]")
}
handler = h handler = h
} else { } else {
newError("non existing outTag: ", outTag).AtWarning().WriteToLog(session.ExportIDToError(ctx)) errors.LogWarning(ctx, "non existing outTag: ", outTag)
common.Close(link.Writer)
common.Interrupt(link.Reader)
return // DO NOT CHANGE: the traffic shouldn't be processed by default outbound if the specified outbound tag doesn't exist (yet), e.g., VLESS Reverse Proxy
} }
} else { } else {
newError("default route for ", destination).WriteToLog(session.ExportIDToError(ctx)) errors.LogInfo(ctx, "default route for ", destination)
} }
} }
if handler == nil {
handler = d.ohm.GetHandler(inTag)
}
if handler == nil { if handler == nil {
handler = d.ohm.GetDefaultHandler() handler = d.ohm.GetDefaultHandler()
} }
if handler == nil { if handler == nil {
newError("default outbound handler not exist").WriteToLog(session.ExportIDToError(ctx)) errors.LogInfo(ctx, "default outbound handler not exist")
common.Close(link.Writer) common.Close(link.Writer)
common.Interrupt(link.Reader) common.Interrupt(link.Reader)
return return
} }
ob.Tag = handler.Tag()
if accessMessage := log.AccessMessageFromContext(ctx); accessMessage != nil { if accessMessage := log.AccessMessageFromContext(ctx); accessMessage != nil {
if tag := handler.Tag(); tag != "" { if tag := handler.Tag(); tag != "" {
if inTag == "" { if inTag == "" {

View File

@@ -1,9 +0,0 @@
package dispatcher
import "github.com/xtls/xray-core/common/errors"
type errPathObjHolder struct{}
func newError(values ...interface{}) *errors.Error {
return errors.New(values...).WithPathObj(errPathObjHolder{})
}

View File

@@ -5,6 +5,7 @@ import (
"strings" "strings"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/session" "github.com/xtls/xray-core/common/session"
"github.com/xtls/xray-core/core" "github.com/xtls/xray-core/core"
@@ -22,15 +23,16 @@ func newFakeDNSSniffer(ctx context.Context) (protocolSnifferWithMetadata, error)
} }
if fakeDNSEngine == nil { if fakeDNSEngine == nil {
errNotInit := newError("FakeDNSEngine is not initialized, but such a sniffer is used").AtError() errNotInit := errors.New("FakeDNSEngine is not initialized, but such a sniffer is used").AtError()
return protocolSnifferWithMetadata{}, errNotInit return protocolSnifferWithMetadata{}, errNotInit
} }
return protocolSnifferWithMetadata{protocolSniffer: func(ctx context.Context, bytes []byte) (SniffResult, error) { return protocolSnifferWithMetadata{protocolSniffer: func(ctx context.Context, bytes []byte) (SniffResult, error) {
Target := session.OutboundFromContext(ctx).Target outbounds := session.OutboundsFromContext(ctx)
if Target.Network == net.Network_TCP || Target.Network == net.Network_UDP { ob := outbounds[len(outbounds)-1]
domainFromFakeDNS := fakeDNSEngine.GetDomainFromFakeDNS(Target.Address) if ob.Target.Network == net.Network_TCP || ob.Target.Network == net.Network_UDP {
domainFromFakeDNS := fakeDNSEngine.GetDomainFromFakeDNS(ob.Target.Address)
if domainFromFakeDNS != "" { if domainFromFakeDNS != "" {
newError("fake dns got domain: ", domainFromFakeDNS, " for ip: ", Target.Address.String()).WriteToLog(session.ExportIDToError(ctx)) errors.LogInfo(ctx, "fake dns got domain: ", domainFromFakeDNS, " for ip: ", ob.Target.Address.String())
return &fakeDNSSniffResult{domainName: domainFromFakeDNS}, nil return &fakeDNSSniffResult{domainName: domainFromFakeDNS}, nil
} }
} }
@@ -38,7 +40,7 @@ func newFakeDNSSniffer(ctx context.Context) (protocolSnifferWithMetadata, error)
if ipAddressInRangeValueI := ctx.Value(ipAddressInRange); ipAddressInRangeValueI != nil { if ipAddressInRangeValueI := ctx.Value(ipAddressInRange); ipAddressInRangeValueI != nil {
ipAddressInRangeValue := ipAddressInRangeValueI.(*ipAddressInRangeOpt) ipAddressInRangeValue := ipAddressInRangeValueI.(*ipAddressInRangeOpt)
if fkr0, ok := fakeDNSEngine.(dns.FakeDNSEngineRev0); ok { if fkr0, ok := fakeDNSEngine.(dns.FakeDNSEngineRev0); ok {
inPool := fkr0.IsIPInIPPool(Target.Address) inPool := fkr0.IsIPInIPPool(ob.Target.Address)
ipAddressInRangeValue.addressInRange = &inPool ipAddressInRangeValue.addressInRange = &inPool
} }
} }
@@ -108,10 +110,10 @@ func newFakeDNSThenOthers(ctx context.Context, fakeDNSSniffer protocolSnifferWit
} }
return nil, common.ErrNoClue return nil, common.ErrNoClue
} }
newError("ip address not in fake dns range, return as is").AtDebug().WriteToLog() errors.LogDebug(ctx, "ip address not in fake dns range, return as is")
return nil, common.ErrNoClue return nil, common.ErrNoClue
} }
newError("fake dns sniffer did not set address in range option, assume false.").AtWarning().WriteToLog() errors.LogWarning(ctx, "fake dns sniffer did not set address in range option, assume false.")
return nil, common.ErrNoClue return nil, common.ErrNoClue
}, },
metadataSniffer: false, metadataSniffer: false,

View File

@@ -0,0 +1,46 @@
package dispatcher
import (
sync "sync"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf"
)
type ManagedWriter struct {
writer buf.Writer
manager *LinkManager
}
func (w *ManagedWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
return w.writer.WriteMultiBuffer(mb)
}
func (w *ManagedWriter) Close() error {
w.manager.RemoveWriter(w)
return common.Close(w.writer)
}
type LinkManager struct {
links map[*ManagedWriter]buf.Reader
mu sync.Mutex
}
func (m *LinkManager) AddLink(writer *ManagedWriter, reader buf.Reader) {
m.mu.Lock()
defer m.mu.Unlock()
m.links[writer] = reader
}
func (m *LinkManager) RemoveWriter(writer *ManagedWriter) {
m.mu.Lock()
defer m.mu.Unlock()
delete(m.links, writer)
}
func (m *LinkManager) CloseAll() {
for w, r := range m.links {
common.Close(w)
common.Interrupt(r)
}
}

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/protocol/bittorrent" "github.com/xtls/xray-core/common/protocol/bittorrent"
"github.com/xtls/xray-core/common/protocol/http" "github.com/xtls/xray-core/common/protocol/http"
@@ -34,7 +35,7 @@ type Sniffer struct {
func NewSniffer(ctx context.Context) *Sniffer { func NewSniffer(ctx context.Context) *Sniffer {
ret := &Sniffer{ ret := &Sniffer{
sniffer: []protocolSnifferWithMetadata{ sniffer: []protocolSnifferWithMetadata{
{func(c context.Context, b []byte) (SniffResult, error) { return http.SniffHTTP(b) }, false, net.Network_TCP}, {func(c context.Context, b []byte) (SniffResult, error) { return http.SniffHTTP(b, ctx) }, false, net.Network_TCP},
{func(c context.Context, b []byte) (SniffResult, error) { return tls.SniffTLS(b) }, false, net.Network_TCP}, {func(c context.Context, b []byte) (SniffResult, error) { return tls.SniffTLS(b) }, false, net.Network_TCP},
{func(c context.Context, b []byte) (SniffResult, error) { return bittorrent.SniffBittorrent(b) }, false, net.Network_TCP}, {func(c context.Context, b []byte) (SniffResult, error) { return bittorrent.SniffBittorrent(b) }, false, net.Network_TCP},
{func(c context.Context, b []byte) (SniffResult, error) { return quic.SniffQUIC(b) }, false, net.Network_UDP}, {func(c context.Context, b []byte) (SniffResult, error) { return quic.SniffQUIC(b) }, false, net.Network_UDP},
@@ -52,7 +53,7 @@ func NewSniffer(ctx context.Context) *Sniffer {
return ret return ret
} }
var errUnknownContent = newError("unknown content") var errUnknownContent = errors.New("unknown content")
func (s *Sniffer) Sniff(c context.Context, payload []byte, network net.Network) (SniffResult, error) { func (s *Sniffer) Sniff(c context.Context, payload []byte, network net.Network) (SniffResult, error) {
var pendingSniffer []protocolSnifferWithMetadata var pendingSniffer []protocolSnifferWithMetadata

View File

@@ -1,25 +0,0 @@
package dispatcher
import (
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/features/stats"
)
type SizeStatWriter struct {
Counter stats.Counter
Writer buf.Writer
}
func (w *SizeStatWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
w.Counter.Add(int64(mb.Len()))
return w.Writer.WriteMultiBuffer(mb)
}
func (w *SizeStatWriter) Close() error {
return common.Close(w.Writer)
}
func (w *SizeStatWriter) Interrupt() {
common.Interrupt(w.Writer)
}

View File

@@ -1,44 +0,0 @@
package dispatcher_test
import (
"testing"
. "github.com/xtls/xray-core/app/dispatcher"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf"
)
type TestCounter int64
func (c *TestCounter) Value() int64 {
return int64(*c)
}
func (c *TestCounter) Add(v int64) int64 {
x := int64(*c) + v
*c = TestCounter(x)
return x
}
func (c *TestCounter) Set(v int64) int64 {
*c = TestCounter(v)
return v
}
func TestStatsWriter(t *testing.T) {
var c TestCounter
writer := &SizeStatWriter{
Counter: &c,
Writer: buf.Discard,
}
mb := buf.MergeBytes(nil, []byte("abcd"))
common.Must(writer.WriteMultiBuffer(mb))
mb = buf.MergeBytes(nil, []byte("efg"))
common.Must(writer.WriteMultiBuffer(mb))
if c.Value() != 7 {
t.Fatal("unexpected counter value. want 7, but got ", c.Value())
}
}

View File

@@ -41,6 +41,7 @@ import (
_ "github.com/xtls/xray-core/proxy/http" _ "github.com/xtls/xray-core/proxy/http"
_ "github.com/xtls/xray-core/proxy/loopback" _ "github.com/xtls/xray-core/proxy/loopback"
_ "github.com/xtls/xray-core/proxy/shadowsocks" _ "github.com/xtls/xray-core/proxy/shadowsocks"
_ "github.com/xtls/xray-core/proxy/shadowsocks_2022"
_ "github.com/xtls/xray-core/proxy/socks" _ "github.com/xtls/xray-core/proxy/socks"
_ "github.com/xtls/xray-core/proxy/trojan" _ "github.com/xtls/xray-core/proxy/trojan"
_ "github.com/xtls/xray-core/proxy/vless/inbound" _ "github.com/xtls/xray-core/proxy/vless/inbound"
@@ -51,13 +52,10 @@ import (
//_ "github.com/xtls/xray-core/proxy/wireguard" //_ "github.com/xtls/xray-core/proxy/wireguard"
// Transports // Transports
//_ "github.com/xtls/xray-core/transport/internet/domainsocket"
_ "github.com/xtls/xray-core/transport/internet/grpc" _ "github.com/xtls/xray-core/transport/internet/grpc"
_ "github.com/xtls/xray-core/transport/internet/http" _ "github.com/xtls/xray-core/transport/internet/kcp"
//_ "github.com/xtls/xray-core/transport/internet/kcp"
//_ "github.com/xtls/xray-core/transport/internet/quic"
_ "github.com/xtls/xray-core/transport/internet/reality" _ "github.com/xtls/xray-core/transport/internet/reality"
_ "github.com/xtls/xray-core/transport/internet/splithttp"
_ "github.com/xtls/xray-core/transport/internet/tcp" _ "github.com/xtls/xray-core/transport/internet/tcp"
_ "github.com/xtls/xray-core/transport/internet/tls" _ "github.com/xtls/xray-core/transport/internet/tls"
_ "github.com/xtls/xray-core/transport/internet/udp" _ "github.com/xtls/xray-core/transport/internet/udp"
@@ -70,5 +68,5 @@ import (
_ "github.com/xtls/xray-core/transport/internet/headers/tls" _ "github.com/xtls/xray-core/transport/internet/headers/tls"
_ "github.com/xtls/xray-core/transport/internet/headers/utp" _ "github.com/xtls/xray-core/transport/internet/headers/utp"
_ "github.com/xtls/xray-core/transport/internet/headers/wechat" _ "github.com/xtls/xray-core/transport/internet/headers/wechat"
//_ "github.com/xtls/xray-core/transport/internet/headers/wireguard" _ "github.com/xtls/xray-core/transport/internet/headers/wireguard"
) )

View File

@@ -2,14 +2,16 @@ package xray
import ( import (
"bytes" "bytes"
"github.com/InazumaV/V2bX/api/panel"
"github.com/goccy/go-json"
log "github.com/sirupsen/logrus"
coreConf "github.com/xtls/xray-core/infra/conf"
"net" "net"
"os" "os"
"strconv" "strconv"
"strings" "strings"
"encoding/json"
"github.com/InazumaV/V2bX/api/panel"
log "github.com/sirupsen/logrus"
coreConf "github.com/xtls/xray-core/infra/conf"
) )
func updateDNSConfig(node *panel.NodeInfo) (err error) { func updateDNSConfig(node *panel.NodeInfo) (err error) {
@@ -62,7 +64,7 @@ func saveDnsConfig(dns []byte, dnsPath string) (err error) {
} }
if !bytes.Equal(currentData, dns) { if !bytes.Equal(currentData, dns) {
coreDnsConfig := &coreConf.DNSConfig{} coreDnsConfig := &coreConf.DNSConfig{}
if err = json.NewDecoder(bytes.NewReader(dns)).Decode(coreDnsConfig); err != nil { if err = json.Unmarshal(dns, coreDnsConfig); err != nil {
log.WithField("err", err).Error("Failed to unmarshal DNS config") log.WithField("err", err).Error("Failed to unmarshal DNS config")
} }
_, err := coreDnsConfig.Build() _, err := coreDnsConfig.Build()

View File

@@ -6,11 +6,13 @@ import (
"encoding/hex" "encoding/hex"
"errors" "errors"
"fmt" "fmt"
"strings"
"time" "time"
"encoding/json"
"github.com/InazumaV/V2bX/api/panel" "github.com/InazumaV/V2bX/api/panel"
"github.com/InazumaV/V2bX/conf" "github.com/InazumaV/V2bX/conf"
"github.com/goccy/go-json"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/core" "github.com/xtls/xray-core/core"
coreConf "github.com/xtls/xray-core/infra/conf" coreConf "github.com/xtls/xray-core/infra/conf"
@@ -26,8 +28,12 @@ func buildInbound(option *conf.Options, nodeInfo *panel.NodeInfo, tag string) (*
err = buildV2ray(option, nodeInfo, in) err = buildV2ray(option, nodeInfo, in)
network = nodeInfo.VAllss.Network network = nodeInfo.VAllss.Network
case "trojan": case "trojan":
err = buildTrojan(option, in) err = buildTrojan(option, nodeInfo, in)
network = "tcp" if nodeInfo.Trojan.Network != "" {
network = nodeInfo.Trojan.Network
} else {
network = "tcp"
}
case "shadowsocks": case "shadowsocks":
err = buildShadowsocks(option, nodeInfo, in) err = buildShadowsocks(option, nodeInfo, in)
network = "tcp" network = "tcp"
@@ -69,8 +75,13 @@ func buildInbound(option *conf.Options, nodeInfo *panel.NodeInfo, tag string) (*
in.StreamSetting.TCPSettings = tcpSetting in.StreamSetting.TCPSettings = tcpSetting
} }
case "ws": case "ws":
in.StreamSetting.WSSettings = &coreConf.WebSocketConfig{ if in.StreamSetting.WSSettings != nil {
AcceptProxyProtocol: option.XrayOptions.EnableProxyProtocol} //Enable proxy protocol in.StreamSetting.WSSettings.AcceptProxyProtocol = option.XrayOptions.EnableProxyProtocol
} else {
in.StreamSetting.WSSettings = &coreConf.WebSocketConfig{
AcceptProxyProtocol: option.XrayOptions.EnableProxyProtocol,
} //Enable proxy protocol
}
default: default:
socketConfig := &coreConf.SocketConfig{ socketConfig := &coreConf.SocketConfig{
AcceptProxyProtocol: option.XrayOptions.EnableProxyProtocol, AcceptProxyProtocol: option.XrayOptions.EnableProxyProtocol,
@@ -105,9 +116,17 @@ func buildInbound(option *conf.Options, nodeInfo *panel.NodeInfo, tag string) (*
// Reality // Reality
in.StreamSetting.Security = "reality" in.StreamSetting.Security = "reality"
v := nodeInfo.VAllss v := nodeInfo.VAllss
dest := v.TlsSettings.Dest
if dest == "" {
dest = v.TlsSettings.ServerName
}
xver := v.TlsSettings.Xver
if xver == 0 {
xver = v.RealityConfig.Xver
}
d, err := json.Marshal(fmt.Sprintf( d, err := json.Marshal(fmt.Sprintf(
"%s:%s", "%s:%s",
v.TlsSettings.ServerName, dest,
v.TlsSettings.ServerPort)) v.TlsSettings.ServerPort))
if err != nil { if err != nil {
return nil, fmt.Errorf("marshal reality dest error: %s", err) return nil, fmt.Errorf("marshal reality dest error: %s", err)
@@ -115,14 +134,17 @@ func buildInbound(option *conf.Options, nodeInfo *panel.NodeInfo, tag string) (*
mtd, _ := time.ParseDuration(v.RealityConfig.MaxTimeDiff) mtd, _ := time.ParseDuration(v.RealityConfig.MaxTimeDiff)
in.StreamSetting.REALITYSettings = &coreConf.REALITYConfig{ in.StreamSetting.REALITYSettings = &coreConf.REALITYConfig{
Dest: d, Dest: d,
Xver: v.RealityConfig.Xver, Xver: xver,
Show: false,
ServerNames: []string{v.TlsSettings.ServerName}, ServerNames: []string{v.TlsSettings.ServerName},
PrivateKey: v.TlsSettings.PrivateKey, PrivateKey: v.TlsSettings.PrivateKey,
MinClientVer: v.RealityConfig.MinClientVer, MinClientVer: v.RealityConfig.MinClientVer,
MaxClientVer: v.RealityConfig.MaxClientVer, MaxClientVer: v.RealityConfig.MaxClientVer,
MaxTimeDiff: uint64(mtd.Microseconds()), MaxTimeDiff: uint64(mtd.Microseconds()),
ShortIds: []string{v.TlsSettings.ShortId}, ShortIds: []string{v.TlsSettings.ShortId},
Mldsa65Seed: v.TlsSettings.Mldsa65Seed,
} }
default:
break break
} }
in.Tag = tag in.Tag = tag
@@ -150,8 +172,27 @@ func buildV2ray(config *conf.Options, nodeInfo *panel.NodeInfo, inbound *coreCon
inbound.Settings = (*json.RawMessage)(&s) inbound.Settings = (*json.RawMessage)(&s)
} else { } else {
var err error var err error
decryption := "none"
if nodeInfo.VAllss.Encryption != "" {
switch nodeInfo.VAllss.Encryption {
case "mlkem768x25519plus":
encSettings := nodeInfo.VAllss.EncryptionSettings
parts := []string{
"mlkem768x25519plus",
encSettings.Mode,
encSettings.Ticket,
}
if encSettings.ServerPadding != "" {
parts = append(parts, encSettings.ServerPadding)
}
parts = append(parts, encSettings.PrivateKey)
decryption = strings.Join(parts, ".")
default:
return fmt.Errorf("vless decryption method %s is not support", nodeInfo.VAllss.Encryption)
}
}
s, err := json.Marshal(&coreConf.VLessInboundConfig{ s, err := json.Marshal(&coreConf.VLessInboundConfig{
Decryption: "none", Decryption: decryption,
}) })
if err != nil { if err != nil {
return fmt.Errorf("marshal vless config error: %s", err) return fmt.Errorf("marshal vless config error: %s", err)
@@ -172,7 +213,7 @@ func buildV2ray(config *conf.Options, nodeInfo *panel.NodeInfo, inbound *coreCon
return nil return nil
} }
t := coreConf.TransportProtocol(nodeInfo.VAllss.Network) t := coreConf.TransportProtocol(v.Network)
inbound.StreamSetting = &coreConf.StreamConfig{Network: &t} inbound.StreamSetting = &coreConf.StreamConfig{Network: &t}
switch v.Network { switch v.Network {
case "tcp": case "tcp":
@@ -186,18 +227,29 @@ func buildV2ray(config *conf.Options, nodeInfo *panel.NodeInfo, inbound *coreCon
return fmt.Errorf("unmarshal ws settings error: %s", err) return fmt.Errorf("unmarshal ws settings error: %s", err)
} }
case "grpc": case "grpc":
err := json.Unmarshal(v.NetworkSettings, &inbound.StreamSetting.GRPCConfig) err := json.Unmarshal(v.NetworkSettings, &inbound.StreamSetting.GRPCSettings)
if err != nil { if err != nil {
return fmt.Errorf("unmarshal grpc settings error: %s", err) return fmt.Errorf("unmarshal grpc settings error: %s", err)
} }
case "httpupgrade":
err := json.Unmarshal(v.NetworkSettings, &inbound.StreamSetting.HTTPUPGRADESettings)
if err != nil {
return fmt.Errorf("unmarshal httpupgrade settings error: %s", err)
}
case "splithttp", "xhttp":
err := json.Unmarshal(v.NetworkSettings, &inbound.StreamSetting.SplitHTTPSettings)
if err != nil {
return fmt.Errorf("unmarshal xhttp settings error: %s", err)
}
default: default:
return errors.New("the network type is not vail") return errors.New("the network type is not vail")
} }
return nil return nil
} }
func buildTrojan(config *conf.Options, inbound *coreConf.InboundDetourConfig) error { func buildTrojan(config *conf.Options, nodeInfo *panel.NodeInfo, inbound *coreConf.InboundDetourConfig) error {
inbound.Protocol = "trojan" inbound.Protocol = "trojan"
v := nodeInfo.Trojan
if config.XrayOptions.EnableFallback { if config.XrayOptions.EnableFallback {
// Set fallback // Set fallback
fallbackConfigs, err := buildTrojanFallbacks(config.XrayOptions.FallBackConfigs) fallbackConfigs, err := buildTrojanFallbacks(config.XrayOptions.FallBackConfigs)
@@ -215,8 +267,31 @@ func buildTrojan(config *conf.Options, inbound *coreConf.InboundDetourConfig) er
s := []byte("{}") s := []byte("{}")
inbound.Settings = (*json.RawMessage)(&s) inbound.Settings = (*json.RawMessage)(&s)
} }
t := coreConf.TransportProtocol("tcp") network := v.Network
if network == "" {
network = "tcp"
}
t := coreConf.TransportProtocol(network)
inbound.StreamSetting = &coreConf.StreamConfig{Network: &t} inbound.StreamSetting = &coreConf.StreamConfig{Network: &t}
switch network {
case "tcp":
err := json.Unmarshal(v.NetworkSettings, &inbound.StreamSetting.TCPSettings)
if err != nil {
return fmt.Errorf("unmarshal tcp settings error: %s", err)
}
case "ws":
err := json.Unmarshal(v.NetworkSettings, &inbound.StreamSetting.WSSettings)
if err != nil {
return fmt.Errorf("unmarshal ws settings error: %s", err)
}
case "grpc":
err := json.Unmarshal(v.NetworkSettings, &inbound.StreamSetting.GRPCSettings)
if err != nil {
return fmt.Errorf("unmarshal grpc settings error: %s", err)
}
default:
return errors.New("the network type is not vail")
}
return nil return nil
} }

View File

@@ -17,6 +17,7 @@ type DNSConfig struct {
} }
func (c *Xray) AddNode(tag string, info *panel.NodeInfo, config *conf.Options) error { func (c *Xray) AddNode(tag string, info *panel.NodeInfo, config *conf.Options) error {
c.nodeReportMinTrafficBytes[tag] = config.ReportMinTraffic * 1024
err := updateDNSConfig(info) err := updateDNSConfig(info)
if err != nil { if err != nil {
return fmt.Errorf("build dns error: %s", err) return fmt.Errorf("build dns error: %s", err)

View File

@@ -3,9 +3,9 @@ package xray
import ( import (
"fmt" "fmt"
"encoding/json"
conf2 "github.com/InazumaV/V2bX/conf" conf2 "github.com/InazumaV/V2bX/conf"
"github.com/goccy/go-json"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/core" "github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/infra/conf" "github.com/xtls/xray-core/infra/conf"
) )
@@ -18,8 +18,7 @@ func buildOutbound(config *conf2.Options, tag string) (*core.OutboundHandlerConf
// Build Send IP address // Build Send IP address
if config.SendIP != "" { if config.SendIP != "" {
ipAddress := net.ParseAddress(config.SendIP) outboundDetourConfig.SendThrough = &config.SendIP
outboundDetourConfig.SendThrough = &conf.Address{Address: ipAddress}
} }
// Freedom Protocol setting // Freedom Protocol setting

View File

@@ -38,9 +38,11 @@ func buildSSUser(tag string, userInfo *panel.UserInfo, cypher string, serverKey
keyLength = 16 keyLength = 16
case "2022-blake3-aes-256-gcm": case "2022-blake3-aes-256-gcm":
keyLength = 32 keyLength = 32
case "2022-blake3-chacha20-poly1305":
keyLength = 32
} }
ssAccount := &shadowsocks_2022.User{ ssAccount := &shadowsocks_2022.Account{
Key: base64.StdEncoding.EncodeToString([]byte(userInfo.Uuid[:keyLength])), Key: base64.StdEncoding.EncodeToString([]byte(userInfo.Uuid[:keyLength])),
} }
return &protocol.User{ return &protocol.User{
Level: 0, Level: 0,

View File

@@ -5,8 +5,10 @@ import (
"fmt" "fmt"
"github.com/InazumaV/V2bX/api/panel" "github.com/InazumaV/V2bX/api/panel"
"github.com/InazumaV/V2bX/common/counter"
"github.com/InazumaV/V2bX/common/format" "github.com/InazumaV/V2bX/common/format"
vCore "github.com/InazumaV/V2bX/core" vCore "github.com/InazumaV/V2bX/core"
"github.com/InazumaV/V2bX/core/xray/app/dispatcher"
"github.com/xtls/xray-core/common/protocol" "github.com/xtls/xray-core/common/protocol"
"github.com/xtls/xray-core/proxy" "github.com/xtls/xray-core/proxy"
) )
@@ -27,51 +29,77 @@ func (c *Xray) GetUserManager(tag string) (proxy.UserManager, error) {
return userManager, nil return userManager, nil
} }
func (c *Xray) DelUsers(users []panel.UserInfo, tag string) error { func (c *Xray) DelUsers(users []panel.UserInfo, tag string, _ *panel.NodeInfo) error {
userManager, err := c.GetUserManager(tag) userManager, err := c.GetUserManager(tag)
if err != nil { if err != nil {
return fmt.Errorf("get user manager error: %s", err) return fmt.Errorf("get user manager error: %s", err)
} }
var up, down, user string var user string
c.users.mapLock.Lock()
defer c.users.mapLock.Unlock()
for i := range users { for i := range users {
user = format.UserTag(tag, users[i].Uuid) user = format.UserTag(tag, users[i].Uuid)
err = userManager.RemoveUser(context.Background(), user) err = userManager.RemoveUser(context.Background(), user)
if err != nil { if err != nil {
return err return err
} }
up = "user>>>" + user + ">>>traffic>>>uplink" delete(c.users.uidMap, user)
down = "user>>>" + user + ">>>traffic>>>downlink" if v, ok := c.dispatcher.Counter.Load(tag); ok {
c.shm.UnregisterCounter(up) tc := v.(*counter.TrafficCounter)
c.shm.UnregisterCounter(down) tc.Delete(user)
}
if v, ok := c.dispatcher.LinkManagers.Load(user); ok {
lm := v.(*dispatcher.LinkManager)
lm.CloseAll()
c.dispatcher.LinkManagers.Delete(user)
}
} }
return nil return nil
} }
func (c *Xray) GetUserTraffic(tag, uuid string, reset bool) (up int64, down int64) { func (x *Xray) GetUserTrafficSlice(tag string, reset bool) ([]panel.UserTraffic, error) {
upName := "user>>>" + format.UserTag(tag, uuid) + ">>>traffic>>>uplink" trafficSlice := make([]panel.UserTraffic, 0)
downName := "user>>>" + format.UserTag(tag, uuid) + ">>>traffic>>>downlink" x.users.mapLock.RLock()
upCounter := c.shm.GetCounter(upName) defer x.users.mapLock.RUnlock()
downCounter := c.shm.GetCounter(downName) if v, ok := x.dispatcher.Counter.Load(tag); ok {
if reset { c := v.(*counter.TrafficCounter)
if upCounter != nil { c.Counters.Range(func(key, value interface{}) bool {
up = upCounter.Set(0) email := key.(string)
} traffic := value.(*counter.TrafficStorage)
if downCounter != nil { up := traffic.UpCounter.Load()
down = downCounter.Set(0) down := traffic.DownCounter.Load()
} if up+down > x.nodeReportMinTrafficBytes[tag] {
} else { if reset {
if upCounter != nil { traffic.UpCounter.Store(0)
up = upCounter.Value() traffic.DownCounter.Store(0)
} }
if downCounter != nil { if x.users.uidMap[email] == 0 {
down = downCounter.Value() c.Delete(email)
return true
}
trafficSlice = append(trafficSlice, panel.UserTraffic{
UID: x.users.uidMap[email],
Upload: up,
Download: down,
})
}
return true
})
if len(trafficSlice) == 0 {
return nil, nil
} }
return trafficSlice, nil
} }
return up, down return nil, nil
} }
func (c *Xray) AddUsers(p *vCore.AddUsersParams) (added int, err error) { func (c *Xray) AddUsers(p *vCore.AddUsersParams) (added int, err error) {
users := make([]*protocol.User, 0, len(p.Users)) c.users.mapLock.Lock()
defer c.users.mapLock.Unlock()
for i := range p.Users {
c.users.uidMap[format.UserTag(p.Tag, p.Users[i].Uuid)] = p.Users[i].Id
}
var users []*protocol.User
switch p.NodeInfo.Type { switch p.NodeInfo.Type {
case "vmess": case "vmess":
users = buildVmessUsers(p.Tag, p.Users) users = buildVmessUsers(p.Tag, p.Users)

View File

@@ -5,11 +5,12 @@ import (
"os" "os"
"sync" "sync"
"encoding/json/v2"
"github.com/InazumaV/V2bX/conf" "github.com/InazumaV/V2bX/conf"
vCore "github.com/InazumaV/V2bX/core" vCore "github.com/InazumaV/V2bX/core"
"github.com/InazumaV/V2bX/core/xray/app/dispatcher" "github.com/InazumaV/V2bX/core/xray/app/dispatcher"
_ "github.com/InazumaV/V2bX/core/xray/distro/all" _ "github.com/InazumaV/V2bX/core/xray/distro/all"
"github.com/goccy/go-json"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/xtls/xray-core/app/proxyman" "github.com/xtls/xray-core/app/proxyman"
"github.com/xtls/xray-core/app/stats" "github.com/xtls/xray-core/app/stats"
@@ -18,7 +19,6 @@ import (
"github.com/xtls/xray-core/features/inbound" "github.com/xtls/xray-core/features/inbound"
"github.com/xtls/xray-core/features/outbound" "github.com/xtls/xray-core/features/outbound"
"github.com/xtls/xray-core/features/routing" "github.com/xtls/xray-core/features/routing"
statsFeature "github.com/xtls/xray-core/features/stats"
coreConf "github.com/xtls/xray-core/infra/conf" coreConf "github.com/xtls/xray-core/infra/conf"
) )
@@ -30,16 +30,28 @@ func init() {
// Xray Structure // Xray Structure
type Xray struct { type Xray struct {
access sync.Mutex access sync.Mutex
Server *core.Instance Server *core.Instance
ihm inbound.Manager ihm inbound.Manager
ohm outbound.Manager ohm outbound.Manager
shm statsFeature.Manager dispatcher *dispatcher.DefaultDispatcher
dispatcher *dispatcher.DefaultDispatcher users *UserMap
nodeReportMinTrafficBytes map[string]int64
}
type UserMap struct {
uidMap map[string]int
mapLock sync.RWMutex
} }
func New(c *conf.CoreConfig) (vCore.Core, error) { func New(c *conf.CoreConfig) (vCore.Core, error) {
return &Xray{Server: getCore(c.XrayConfig)}, nil return &Xray{
Server: getCore(c.XrayConfig),
users: &UserMap{
uidMap: make(map[string]int),
},
nodeReportMinTrafficBytes: make(map[string]int64),
}, nil
} }
func parseConnectionConfig(c *conf.XrayConnectionConfig) (policy *coreConf.Policy) { func parseConnectionConfig(c *conf.XrayConnectionConfig) (policy *coreConf.Policy) {
@@ -58,22 +70,24 @@ func parseConnectionConfig(c *conf.XrayConnectionConfig) (policy *coreConf.Polic
func getCore(c *conf.XrayConfig) *core.Instance { func getCore(c *conf.XrayConfig) *core.Instance {
os.Setenv("XRAY_LOCATION_ASSET", c.AssetPath) os.Setenv("XRAY_LOCATION_ASSET", c.AssetPath)
// Log Config // Log Config
coreLogConfig := &coreConf.LogConfig{} coreLogConfig := &coreConf.LogConfig{
coreLogConfig.LogLevel = c.LogConfig.Level LogLevel: c.LogConfig.Level,
coreLogConfig.AccessLog = c.LogConfig.AccessPath AccessLog: c.LogConfig.AccessPath,
coreLogConfig.ErrorLog = c.LogConfig.ErrorPath ErrorLog: c.LogConfig.ErrorPath,
}
// DNS config // DNS config
coreDnsConfig := &coreConf.DNSConfig{} coreDnsConfig := &coreConf.DNSConfig{}
os.Setenv("XRAY_DNS_PATH", "") os.Setenv("XRAY_DNS_PATH", "")
if c.DnsConfigPath != "" { if c.DnsConfigPath != "" {
f, err := os.OpenFile(c.DnsConfigPath, os.O_RDWR|os.O_CREATE, 0755) data, err := os.ReadFile(c.DnsConfigPath)
if err != nil { if err != nil {
log.Error("Failed to open or create xray dns config file: %v", err) log.Error(fmt.Sprintf("Failed to read xray dns config file: %v", err))
}
defer f.Close()
if err := json.NewDecoder(f).Decode(coreDnsConfig); err != nil {
log.Error(fmt.Sprintf("Failed to unmarshal xray dns config from file '%v': %v. Using default DNS options.", f.Name(), err))
coreDnsConfig = &coreConf.DNSConfig{} coreDnsConfig = &coreConf.DNSConfig{}
} else {
if err := json.Unmarshal(data, coreDnsConfig); err != nil {
log.Error(fmt.Sprintf("Failed to unmarshal xray dns config: %v. Using default DNS options.", err))
coreDnsConfig = &coreConf.DNSConfig{}
}
} }
os.Setenv("XRAY_DNS_PATH", c.DnsConfigPath) os.Setenv("XRAY_DNS_PATH", c.DnsConfigPath)
} }
@@ -84,25 +98,27 @@ func getCore(c *conf.XrayConfig) *core.Instance {
// Routing config // Routing config
coreRouterConfig := &coreConf.RouterConfig{} coreRouterConfig := &coreConf.RouterConfig{}
if c.RouteConfigPath != "" { if c.RouteConfigPath != "" {
if f, err := os.Open(c.RouteConfigPath); err != nil { data, err := os.ReadFile(c.RouteConfigPath)
if err != nil {
log.WithField("err", err).Panic("Failed to read Routing config file") log.WithField("err", err).Panic("Failed to read Routing config file")
} else { } else {
if err = json.NewDecoder(f).Decode(coreRouterConfig); err != nil { if err = json.Unmarshal(data, coreRouterConfig); err != nil {
log.WithField("err", err).Panic("Failed to unmarshal Routing config") log.WithField("err", err).Panic("Failed to unmarshal Routing config")
} }
} }
} }
routeConfig, err := coreRouterConfig.Build() routeConfig, err := coreRouterConfig.Build()
if err != nil { if err != nil {
log.WithField("err", err).Panic("Failed to understand Routing config Please check: https://xtls.github.io/config/routing.html") log.WithField("err", err).Panic("Failed to understand Routing config. Please check: https://xtls.github.io/config/routing.html for help")
} }
// Custom Inbound config // Custom Inbound config
var coreCustomInboundConfig []coreConf.InboundDetourConfig var coreCustomInboundConfig []coreConf.InboundDetourConfig
if c.InboundConfigPath != "" { if c.InboundConfigPath != "" {
if f, err := os.Open(c.InboundConfigPath); err != nil { data, err := os.ReadFile(c.InboundConfigPath)
if err != nil {
log.WithField("err", err).Panic("Failed to read Custom Inbound config file") log.WithField("err", err).Panic("Failed to read Custom Inbound config file")
} else { } else {
if err = json.NewDecoder(f).Decode(&coreCustomInboundConfig); err != nil { if err = json.Unmarshal(data, &coreCustomInboundConfig); err != nil {
log.WithField("err", err).Panic("Failed to unmarshal Custom Inbound config") log.WithField("err", err).Panic("Failed to unmarshal Custom Inbound config")
} }
} }
@@ -111,17 +127,18 @@ func getCore(c *conf.XrayConfig) *core.Instance {
for _, config := range coreCustomInboundConfig { for _, config := range coreCustomInboundConfig {
oc, err := config.Build() oc, err := config.Build()
if err != nil { if err != nil {
log.WithField("err", err).Panic("Failed to understand Inbound config, Please check: https://xtls.github.io/config/inbound.html for help") log.WithField("err", err).Panic("Failed to understand Inbound config. Please check: https://xtls.github.io/config/inbound.html for help")
} }
inBoundConfig = append(inBoundConfig, oc) inBoundConfig = append(inBoundConfig, oc)
} }
// Custom Outbound config // Custom Outbound config
var coreCustomOutboundConfig []coreConf.OutboundDetourConfig var coreCustomOutboundConfig []coreConf.OutboundDetourConfig
if c.OutboundConfigPath != "" { if c.OutboundConfigPath != "" {
if f, err := os.Open(c.OutboundConfigPath); err != nil { data, err := os.ReadFile(c.OutboundConfigPath)
if err != nil {
log.WithField("err", err).Panic("Failed to read Custom Outbound config file") log.WithField("err", err).Panic("Failed to read Custom Outbound config file")
} else { } else {
if err = json.NewDecoder(f).Decode(&coreCustomOutboundConfig); err != nil { if err = json.Unmarshal(data, &coreCustomOutboundConfig); err != nil {
log.WithField("err", err).Panic("Failed to unmarshal Custom Outbound config") log.WithField("err", err).Panic("Failed to unmarshal Custom Outbound config")
} }
} }
@@ -169,7 +186,6 @@ func (c *Xray) Start() error {
if err := c.Server.Start(); err != nil { if err := c.Server.Start(); err != nil {
return err return err
} }
c.shm = c.Server.GetFeature(statsFeature.ManagerType()).(statsFeature.Manager)
c.ihm = c.Server.GetFeature(inbound.ManagerType()).(inbound.Manager) c.ihm = c.Server.GetFeature(inbound.ManagerType()).(inbound.Manager)
c.ohm = c.Server.GetFeature(outbound.ManagerType()).(outbound.Manager) c.ohm = c.Server.GetFeature(outbound.ManagerType()).(outbound.Manager)
c.dispatcher = c.Server.GetFeature(routing.DispatcherType()).(*dispatcher.DefaultDispatcher) c.dispatcher = c.Server.GetFeature(routing.DispatcherType()).(*dispatcher.DefaultDispatcher)
@@ -182,7 +198,6 @@ func (c *Xray) Close() error {
defer c.access.Unlock() defer c.access.Unlock()
c.ihm = nil c.ihm = nil
c.ohm = nil c.ohm = nil
c.shm = nil
c.dispatcher = nil c.dispatcher = nil
err := c.Server.Close() err := c.Server.Close()
if err != nil { if err != nil {

View File

@@ -7,14 +7,15 @@
{ {
"Type": "sing", "Type": "sing",
"Log": { "Log": {
"Level": "error", "Level": "info",
"Timestamp": true "Timestamp": true
}, },
"NTP": { "NTP": {
"Enable": true, "Enable": false,
"Server": "time.apple.com", "Server": "time.apple.com",
"ServerPort": 0 "ServerPort": 0
} },
"OriginalPath": "/etc/V2bX/sing_origin.json"
} }
], ],
"Nodes": [ "Nodes": [
@@ -27,20 +28,19 @@
"Timeout": 30, "Timeout": 30,
"ListenIP": "0.0.0.0", "ListenIP": "0.0.0.0",
"SendIP": "0.0.0.0", "SendIP": "0.0.0.0",
"EnableProxyProtocol": false, "DeviceOnlineMinTraffic": 200,
"EnableDNS": true, "MinReportTraffic": 0,
"DomainStrategy": "ipv4_only", "TCPFastOpen": false,
"LimitConfig": { "SniffEnabled": true,
"EnableRealtime": false, "CertConfig": {
"SpeedLimit": 0, "CertMode": "self",
"IPLimit": 0, "RejectUnknownSni": false,
"ConnLimit": 0, "CertDomain": "example.com",
"EnableDynamicSpeedLimit": false, "CertFile": "/etc/V2bX/fullchain.cer",
"DynamicSpeedLimitConfig": { "KeyFile": "/etc/V2bX/cert.key",
"Periodic": 60, "Provider": "cloudflare",
"Traffic": 1000, "DNSEnv": {
"SpeedLimit": 100, "EnvName": "env1"
"ExpireTime": 60
} }
} }
} }

View File

@@ -1,261 +0,0 @@
{
"Log": {
// V2bX 的日志配置,独立于各 Core 的 log 配置
// 日志等级info, warn, error, none
"Level": "error",
// 日志输出路径,默认输出到标准输出
"Output": ""
},
"Cores": [
{
// Core类型
"Type": "sing",
// Core标识名可选如果需要启动多个同类型内核则必填
"Name": "sing1",
"Log": {
// 同 SingBox log 部分配置
"Level": "error",
"Timestamp": true
},
"NTP": {
// 同 SingBox ntp 部分配置
// VMess VLESS 建议开启
"Enable": true,
"Server": "time.apple.com",
"ServerPort": 0
},
"DnsConfigPath": "/etc/V2bX/dns.json",
// SingBox源配置文件目录用于引用标准SingBox配置文件
"OriginalPath": "/etc/V2bX/sing_origin.json",
// 在删除用户时清理已建立的连接
"EnableConnClear": false,
},
{
"Type": "sing",
"Name": "sing2",
"Log": {
"Level": "info",
"Timestamp": false
}
},
{
"Type": "xray",
"Log": {
// 同 Xray-core log 部分配置
"Level": "error"
},
// 静态资源文件目录
"AssetPath": "",
// DNS配置文件目录
"DnsConfigPath": "",
// 路由配置文件目录
"RouteConfigPath": "",
// 本地策略相关配置
"ConnectionConfig": {
// 详见 https://xtls.github.io/config/policy.html#levelpolicyobject
"handshake": 4,
"connIdle": 300,
"uplinkOnly": 2,
"downlinkOnly": 5,
"statsUserUplink": false,
"statsUserDownlink": false,
"bufferSize": 4
},
// Inbound配置文件目录
"InboundConfigPath": "",
// Outbound配置文件目录
"OutboundConfigPath": ""
}
],
"Nodes": [
// Node配置有两种写法
{
// 写法1
// sing内核
// Node标识名便于查看日志不填将通过下发的节点配置自动生成
// 务必注意不要重复,否则会出现问题
"Name": "sing_node1",
// 要使用的Core的类型
// 如果填写了CoreName可不填但单内核务必填写
// 建议视情况填写Core和CoreName其中一个如果均没有填写将随机选择支持的内核
"Core": "sing",
// 要使用的Core的标识名如果没有定义多个同类型内核可不填
"CoreName": "sing1",
// API接口地址
"ApiHost": "http://127.0.0.1",
// API密钥即Token
"ApiKey": "test",
// 节点ID
"NodeID": 33,
// 节点类型
"NodeType": "shadowsocks",
// 请求超时时间
"Timeout": 30,
// 监听IP
"ListenIP": "0.0.0.0",
// 发送IP
"SendIP": "0.0.0.0",
// 开启 Proxy Protocol参见 https://github.com/haproxy/haproxy/blob/master/doc/proxy-protocol.txt
"EnableProxyProtocol": false,
// 开启 TCP Fast Open
"EnableTFO": true,
// 开启 DNS
"EnableDNS" : true,
// 设置 Domain Strategy 需要开启 DNS ,默认 AsIS
// 可选 prefer_ipv4 / prefer_ipv6 / ipv4_only / ipv6_only
"DomainStrategy": "ipv4_only",
// 限制器相关配置
"LimitConfig": {
// 开启实时连接数及IP数限制
"EnableRealtime": false,
// 用户速度限制
"SpeedLimit": 0,
// 用户IP限制
"IPLimit": 0,
// 用户连接数限制
"ConnLimit": 0,
// 开启动态限速
"EnableDynamicSpeedLimit": false,
// 动态限速相关配置
"DynamicSpeedLimitConfig": {
// 检查周期
"Periodic": 60,
// 检查周期内触发限制的流量数
"Traffic": 1000,
// 触发限制后的速度限制
"SpeedLimit": 100,
// 速度限制过期时间
"ExpireTime": 60
}
},
// 证书相关配置
"CertConfig": {
// 证书申请模式none、http、dns、self
"CertMode": "none",
"RejectUnknownSni": false,
// 证书域名
"CertDomain": "test.com",
// 证书文件目录
"CertFile": "/etc/V2bX/cert/1.pem",
// 密钥文件目录
"KeyFile": "/etc/V2bX/cert/1.key",
// 申请证书时使用的用户邮箱
"Email": "1@test.com",
// DNS解析提供者
"Provider": "cloudflare",
// DNS解析提供者的环境变量详见 https://go-acme.github.io/lego/dns/
"DNSEnv": {
"EnvName": "env1"
}
}
},
{
// xray内核
"Name": "xray_node1",
"Core": "xray",
"CoreName": "",
"ApiHost": "http://127.0.0.1",
"ApiKey": "test",
"NodeID": 33,
"NodeType": "shadowsocks",
"Timeout": 30,
"ListenIP": "0.0.0.0",
"SendIP": "0.0.0.0",
"EnableProxyProtocol": true,
"EnableTFO": true,
// 以上同 sing
// 开启自定义DNS
"EnableDNS": false,
// DNS解析类型AsIs、UseIP、UseIPv4、UseIPv6
"DNSType": "AsIs",
// 开启udp over tcp
"EnableUot": false,
// 禁用IVCheck
"DisableIVCheck": false,
// 禁用嗅探
"DisableSniffing": false,
// 开启回落
"EnableFallback": false,
// 回落相关配置
"FallBackConfigs":{
// 详见 https://xtls.github.io/config/features/fallback.html#fallbackobject
"SNI": "",
"Alpn": "",
"Path": "",
"Dest": "",
"ProxyProtocolVer": 0
}
},
{
// 写法2
// 类似旧配置文件 ApiConfig 部分
"ApiConfig": {
"ApiHost": "http://127.0.0.1",
"ApiKey": "test",
"NodeID": 33,
"Timeout": 30
},
// 类似旧配置文件 ControllerConfig 部分
"Options": {
"Core": "sing",
"EnableProxyProtocol": true,
"EnableTFO": true,
"DomainStrategy": "ipv4_only"
// More
}
},
{
// 引用本地其他配置文件
"Include": "../example/config_full_node1.json"
},
{
// 通过Http引用远端配置文件
"Include": "http://127.0.0.1:11451/config_full_node1.json"
}
]
}

View File

@@ -1,13 +0,0 @@
{
"Core": "xray",
"ApiHost": "https://127.0.0.1",
"ApiKey": "key",
"NodeID": 1,
"NodeType": "vmess",
"Timeout": 30,
"ListenIP": "0.0.0.0",
"SendIP": "0.0.0.0",
"EnableProxyProtocol": false,
"EnableTFO": true,
"DNSType": "ipv4_only"
}

BIN
example/geoip.db Normal file

Binary file not shown.

BIN
example/geosite.db Normal file

Binary file not shown.

436
go.mod
View File

@@ -1,235 +1,335 @@
module github.com/InazumaV/V2bX module github.com/InazumaV/V2bX
go 1.21.4 go 1.25
toolchain go1.25.0
require ( require (
github.com/beevik/ntp v1.2.0 github.com/apernet/hysteria/core/v2 v2.6.4
github.com/fsnotify/fsnotify v1.7.0 github.com/apernet/hysteria/extras/v2 v2.6.4
github.com/go-acme/lego/v4 v4.13.2 github.com/beevik/ntp v1.4.4-0.20240716062501-06ef196b89ec
github.com/go-resty/resty/v2 v2.7.0 github.com/fsnotify/fsnotify v1.9.0
github.com/goccy/go-json v0.10.2 github.com/go-acme/lego/v4 v4.25.2
github.com/hashicorp/go-multierror v1.1.1 github.com/go-resty/resty/v2 v2.16.5
github.com/juju/ratelimit v1.0.2 github.com/juju/ratelimit v1.0.2
github.com/sagernet/sing v0.2.18-0.20231117150934-256fafcd99b6 github.com/sagernet/sing v0.8.0-beta.6
github.com/sagernet/sing-box v1.7.0-beta.3 github.com/sagernet/sing-box v1.13.0
github.com/sirupsen/logrus v1.9.3 github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.8.0 github.com/spf13/cobra v1.9.1
github.com/xtls/xray-core v1.8.6 github.com/spf13/viper v1.19.0
golang.org/x/crypto v0.15.0 github.com/vmihailenco/msgpack/v5 v5.4.1
golang.org/x/sys v0.14.0 github.com/xtls/xray-core v1.251202.0
google.golang.org/protobuf v1.31.0 go.uber.org/zap v1.27.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1 golang.org/x/crypto v0.44.0
golang.org/x/sys v0.38.0
google.golang.org/protobuf v1.36.10
) )
require ( require (
berty.tech/go-libtor v1.0.385 // indirect cloud.google.com/go/auth v0.16.2 // indirect
cloud.google.com/go/compute v1.23.2 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 // indirect github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 // indirect
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.1.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.1.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 // indirect
github.com/Azure/go-autorest v14.2.0+incompatible // indirect github.com/Azure/go-autorest v14.2.0+incompatible // indirect
github.com/Azure/go-autorest/autorest v0.11.24 // indirect github.com/Azure/go-autorest/autorest v0.11.30 // indirect
github.com/Azure/go-autorest/autorest/adal v0.9.18 // indirect github.com/Azure/go-autorest/autorest/adal v0.9.22 // indirect
github.com/Azure/go-autorest/autorest/azure/auth v0.5.12 // indirect github.com/Azure/go-autorest/autorest/azure/auth v0.5.13 // indirect
github.com/Azure/go-autorest/autorest/azure/cli v0.4.5 // indirect github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 // indirect
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect github.com/Azure/go-autorest/autorest/to v0.4.1 // indirect
github.com/Azure/go-autorest/logger v0.2.1 // indirect github.com/Azure/go-autorest/logger v0.2.1 // indirect
github.com/Azure/go-autorest/tracing v0.6.0 // indirect github.com/Azure/go-autorest/tracing v0.6.0 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect
github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 // indirect github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 // indirect
github.com/ajg/form v1.5.1 // indirect github.com/ajg/form v1.5.1 // indirect
github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2 // indirect github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2 // indirect
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1755 // indirect github.com/akutz/memconn v0.1.0 // indirect
github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect
github.com/andybalholm/brotli v1.0.6 // indirect github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
github.com/aws/aws-sdk-go v1.39.0 // indirect github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.8 // indirect
github.com/alibabacloud-go/debug v1.0.1 // indirect
github.com/alibabacloud-go/endpoint-util v1.1.0 // indirect
github.com/alibabacloud-go/openapi-util v0.1.1 // indirect
github.com/alibabacloud-go/tea v1.3.9 // indirect
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 // indirect
github.com/aliyun/credentials-go v1.4.6 // indirect
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/anthropics/anthropic-sdk-go v1.14.0 // indirect
github.com/anytls/sing-anytls v0.0.11 // indirect
github.com/apernet/quic-go v0.54.1-0.20250907230547-eb32f8aec5e2 // indirect
github.com/aws/aws-sdk-go-v2 v1.36.6 // indirect
github.com/aws/aws-sdk-go-v2/config v1.29.18 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.71 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.33 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.37 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.37 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.18 // indirect
github.com/aws/aws-sdk-go-v2/service/lightsail v1.43.5 // indirect
github.com/aws/aws-sdk-go-v2/service/route53 v1.53.1 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.25.6 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.4 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.34.1 // indirect
github.com/aws/smithy-go v1.22.4 // indirect
github.com/aziontech/azionapi-go-sdk v0.142.0 // indirect
github.com/baidubce/bce-sdk-go v0.9.235 // indirect
github.com/benbjohnson/clock v1.3.0 // indirect
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
github.com/caddyserver/certmagic v0.19.2 // indirect github.com/caddyserver/certmagic v0.23.0 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/caddyserver/zerossl v0.1.3 // indirect
github.com/civo/civogo v0.3.11 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cloudflare/circl v1.3.6 // indirect github.com/clbanning/mxj/v2 v2.7.0 // indirect
github.com/cloudflare/cloudflare-go v0.70.0 // indirect github.com/cloudflare/circl v1.6.1 // indirect
github.com/cpu/goacmedns v0.1.1 // indirect github.com/coder/websocket v1.8.13 // indirect
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 // indirect
github.com/cretz/bine v0.2.0 // indirect github.com/cretz/bine v0.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/database64128/netx-go v0.1.1 // indirect
github.com/deepmap/oapi-codegen v1.9.1 // indirect github.com/database64128/tfo-go/v2 v2.3.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 // indirect github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 // indirect
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e // indirect
github.com/dimchansky/utfbom v1.1.1 // indirect github.com/dimchansky/utfbom v1.1.1 // indirect
github.com/dnsimple/dnsimple-go v1.2.0 // indirect github.com/dnsimple/dnsimple-go/v4 v4.0.0 // indirect
github.com/exoscale/egoscale v0.100.1 // indirect github.com/exoscale/egoscale/v3 v3.1.24 // indirect
github.com/fatih/structs v1.1.0 // indirect github.com/fatih/structs v1.1.0 // indirect
github.com/francoispqt/gojay v1.2.13 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/gaukas/godicttls v0.0.4 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gaissmai/bart v0.18.0 // indirect
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 // indirect github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 // indirect
github.com/go-chi/chi/v5 v5.0.10 // indirect github.com/go-acme/alidns-20150109/v4 v4.5.10 // indirect
github.com/go-chi/cors v1.2.1 // indirect github.com/go-acme/tencentclouddnspod v1.0.1208 // indirect
github.com/go-chi/chi/v5 v5.2.2 // indirect
github.com/go-chi/render v1.0.3 // indirect github.com/go-chi/render v1.0.3 // indirect
github.com/go-errors/errors v1.0.1 // indirect github.com/go-errors/errors v1.0.1 // indirect
github.com/go-jose/go-jose/v3 v3.0.0 // indirect github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.23.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/pool v0.2.1 // indirect
github.com/gofrs/uuid/v5 v5.0.0 // indirect github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/gofrs/flock v0.12.1 // indirect
github.com/gofrs/uuid/v5 v5.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect github.com/golang/protobuf v1.5.4 // indirect
github.com/google/btree v1.1.2 // indirect github.com/google/btree v1.1.3 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect github.com/google/go-querystring v1.1.0 // indirect
github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a // indirect github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 // indirect
github.com/google/s2a-go v0.1.4 // indirect github.com/google/s2a-go v0.1.9 // indirect
github.com/google/uuid v1.3.1 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.2.4 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
github.com/googleapis/gax-go/v2 v2.12.0 // indirect github.com/googleapis/gax-go/v2 v2.14.2 // indirect
github.com/gophercloud/gophercloud v1.0.0 // indirect github.com/gophercloud/gophercloud v1.14.1 // indirect
github.com/gophercloud/utils v0.0.0-20210216074907-f6de111f2eae // indirect github.com/gophercloud/utils v0.0.0-20231010081019-80377eca5d56 // indirect
github.com/gorilla/websocket v1.5.1 // indirect github.com/gorilla/websocket v1.5.3 // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.4 // indirect github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
github.com/hashicorp/yamux v0.1.1 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.5 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hashicorp/yamux v0.1.2 // indirect
github.com/hdevalence/ed25519consensus v0.2.0 // indirect
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.159 // indirect
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // indirect github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // indirect
github.com/illarion/gonotify/v3 v3.0.2 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/infobloxopen/infoblox-go-client v1.1.1 // indirect github.com/infobloxopen/infoblox-go-client/v2 v2.10.0 // indirect
github.com/insomniacslk/dhcp v0.0.0-20231016090811-6a2c8fbdcc1c // indirect github.com/insomniacslk/dhcp v0.0.0-20250417080101-5f8cf70e8c5f // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/jsimonetti/rtnetlink v1.4.0 // indirect
github.com/josharian/native v1.1.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 // indirect github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 // indirect
github.com/klauspost/compress v1.17.2 // indirect github.com/keybase/go-keychain v0.0.1 // indirect
github.com/klauspost/cpuid/v2 v2.2.6 // indirect github.com/klauspost/compress v1.17.11 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect
github.com/labbsr0x/bindman-dns-webhook v1.0.2 // indirect github.com/labbsr0x/bindman-dns-webhook v1.0.2 // indirect
github.com/labbsr0x/goh v1.0.1 // indirect github.com/labbsr0x/goh v1.0.1 // indirect
github.com/libdns/alidns v1.0.3 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/libdns/cloudflare v0.1.0 // indirect github.com/libdns/alidns v1.0.5-libdns.v1.beta1 // indirect
github.com/libdns/libdns v0.2.1 // indirect github.com/libdns/cloudflare v0.2.2-0.20250708034226-c574dccb31a6 // indirect
github.com/linode/linodego v1.17.2 // indirect github.com/libdns/libdns v1.1.0 // indirect
github.com/liquidweb/go-lwApi v0.0.5 // indirect github.com/linode/linodego v1.53.0 // indirect
github.com/liquidweb/liquidweb-cli v0.6.9 // indirect github.com/liquidweb/liquidweb-cli v0.6.9 // indirect
github.com/liquidweb/liquidweb-go v1.6.3 // indirect github.com/liquidweb/liquidweb-go v1.6.4 // indirect
github.com/logrusorgru/aurora v2.0.3+incompatible // indirect github.com/logrusorgru/aurora v2.0.3+incompatible // indirect
github.com/mattn/go-isatty v0.0.19 // indirect github.com/magiconair/properties v1.8.7 // indirect
github.com/mholt/acmez v1.2.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/miekg/dns v1.1.57 // indirect github.com/mdlayher/genetlink v1.3.2 // indirect
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 // indirect
github.com/mdlayher/sdnotify v1.0.0 // indirect
github.com/mdlayher/socket v0.5.1 // indirect
github.com/metacubex/utls v1.8.3 // indirect
github.com/mholt/acmez/v3 v3.1.2 // indirect
github.com/miekg/dns v1.1.68 // indirect
github.com/mimuret/golang-iij-dpf v0.9.1 // indirect github.com/mimuret/golang-iij-dpf v0.9.1 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/go-ps v1.0.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 // indirect github.com/namedotcom/go/v4 v4.0.2 // indirect
github.com/nrdcg/auroradns v1.1.0 // indirect github.com/nrdcg/auroradns v1.1.0 // indirect
github.com/nrdcg/desec v0.7.0 // indirect github.com/nrdcg/bunny-go v0.0.0-20250327222614-988a091fc7ea // indirect
github.com/nrdcg/desec v0.11.0 // indirect
github.com/nrdcg/dnspod-go v0.4.0 // indirect github.com/nrdcg/dnspod-go v0.4.0 // indirect
github.com/nrdcg/freemyip v0.2.0 // indirect github.com/nrdcg/freemyip v0.3.0 // indirect
github.com/nrdcg/goinwx v0.8.2 // indirect github.com/nrdcg/goacmedns v0.2.0 // indirect
github.com/nrdcg/goinwx v0.11.0 // indirect
github.com/nrdcg/mailinabox v0.2.0 // indirect
github.com/nrdcg/namesilo v0.2.1 // indirect github.com/nrdcg/namesilo v0.2.1 // indirect
github.com/nrdcg/nodion v0.1.0 // indirect github.com/nrdcg/nodion v0.1.0 // indirect
github.com/nrdcg/porkbun v0.2.0 // indirect github.com/nrdcg/oci-go-sdk/common/v1065 v1065.95.2 // indirect
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.95.2 // indirect
github.com/nrdcg/porkbun v0.4.0 // indirect
github.com/nzdjb/go-metaname v1.0.0 // indirect github.com/nzdjb/go-metaname v1.0.0 // indirect
github.com/onsi/ginkgo/v2 v2.13.1 // indirect github.com/ovh/go-ovh v1.9.0 // indirect
github.com/ooni/go-libtor v1.1.8 // indirect
github.com/oracle/oci-go-sdk v24.3.0+incompatible // indirect
github.com/oschwald/maxminddb-golang v1.12.0 // indirect
github.com/ovh/go-ovh v1.4.1 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/pierrec/lz4/v4 v4.1.14 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/pires/go-proxyproto v0.7.0 // indirect github.com/peterhellberg/link v1.2.0 // indirect
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/pires/go-proxyproto v0.8.1 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/pquerna/otp v1.4.0 // indirect github.com/pquerna/otp v1.5.0 // indirect
github.com/quic-go/qpack v0.4.0 // indirect github.com/prometheus-community/pro-bing v0.4.0 // indirect
github.com/quic-go/qtls-go1-20 v0.4.1 // indirect github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.40.0 // indirect github.com/quic-go/quic-go v0.56.0 // indirect
github.com/refraction-networking/utls v1.5.4 // indirect github.com/refraction-networking/utls v1.8.1 // indirect
github.com/regfish/regfish-dnsapi-go v0.1.1 // indirect
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
github.com/sacloud/api-client-go v0.2.8 // indirect github.com/sacloud/api-client-go v0.3.2 // indirect
github.com/sacloud/go-http v0.1.6 // indirect github.com/sacloud/go-http v0.1.9 // indirect
github.com/sacloud/iaas-api-go v1.11.1 // indirect github.com/sacloud/iaas-api-go v1.16.1 // indirect
github.com/sacloud/packages-go v0.0.9 // indirect github.com/sacloud/packages-go v0.0.11 // indirect
github.com/safchain/ethtool v0.3.0 // indirect
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a // indirect github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a // indirect
github.com/sagernet/cloudflare-tls v0.0.0-20230829051644-4a68352d0c4a // indirect github.com/sagernet/cors v1.2.1 // indirect
github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61 // indirect github.com/sagernet/fswatch v0.1.1 // indirect
github.com/sagernet/gvisor v0.0.0-20230930141345-5fef6f2e17ab // indirect github.com/sagernet/gvisor v0.0.0-20250811-sing-box-mod.1 // indirect
github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 // indirect github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect
github.com/sagernet/quic-go v0.40.0 // indirect github.com/sagernet/nftables v0.3.0-beta.4 // indirect
github.com/sagernet/sing-dns v0.1.11-0.20231116102430-5a2133f5d358 // indirect github.com/sagernet/quic-go v0.55.0-sing-box-mod.2 // indirect
github.com/sagernet/sing-mux v0.1.5-0.20231109075101-6b086ed6bb07 // indirect github.com/sagernet/sing-mux v0.3.3 // indirect
github.com/sagernet/sing-quic v0.1.4-0.20231114135334-e2a6aab55cca // indirect github.com/sagernet/sing-quic v0.6.0-beta.4 // indirect
github.com/sagernet/sing-shadowsocks v0.2.5 // indirect github.com/sagernet/sing-shadowsocks v0.2.8 // indirect
github.com/sagernet/sing-shadowsocks2 v0.1.4 // indirect github.com/sagernet/sing-shadowsocks2 v0.2.1 // indirect
github.com/sagernet/sing-shadowtls v0.1.4 // indirect github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 // indirect
github.com/sagernet/sing-tun v0.1.20-0.20231116102736-3fa4ee409a9d // indirect github.com/sagernet/sing-tun v0.8.0-beta.11 // indirect
github.com/sagernet/sing-vmess v0.1.8 // indirect github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 // indirect
github.com/sagernet/smux v0.0.0-20230312102458-337ec2a5af37 // indirect github.com/sagernet/smux v1.5.34-mod.2 // indirect
github.com/sagernet/tfo-go v0.0.0-20230816093905-5a5c285d44a6 // indirect github.com/sagernet/tailscale v1.86.5-sing-box-1.13-mod.4 // indirect
github.com/sagernet/utls v0.0.0-20230309024959-6732c2ab36f2 // indirect github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20250917110311-16510ac47288 // indirect
github.com/sagernet/wireguard-go v0.0.0-20230807125731-5d4a7ef2dc5f // indirect github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 // indirect
github.com/sagernet/ws v0.0.0-20231030053741-7d481eb31bed // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.17 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9 // indirect github.com/scaleway/scaleway-sdk-go v1.0.0-beta.34 // indirect
github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb // indirect github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 // indirect
github.com/simplesurance/bunny-go v0.0.0-20221115111006-e11d9dc91f04 // indirect github.com/selectel/domains-go v1.1.0 // indirect
github.com/selectel/go-selvpcclient/v4 v4.1.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 // indirect github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 // indirect
github.com/softlayer/softlayer-go v1.1.2 // indirect github.com/softlayer/softlayer-go v1.1.7 // indirect
github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect
github.com/spf13/cast v1.3.1 // indirect github.com/sony/gobreaker v1.0.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/sourcegraph/conc v0.3.0 // indirect
github.com/stretchr/objx v0.5.0 // indirect github.com/spf13/afero v1.11.0 // indirect
github.com/stretchr/testify v1.8.4 // indirect github.com/spf13/cast v1.6.0 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.490 // indirect github.com/spf13/pflag v1.0.6 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.490 // indirect github.com/stretchr/objx v0.5.2 // indirect
github.com/transip/gotransip/v6 v6.20.0 // indirect github.com/stretchr/testify v1.11.1 // indirect
github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect github.com/subosito/gotenv v1.6.0 // indirect
github.com/ultradns/ultradns-go-sdk v1.5.0-20230427130837-23c9b0c // indirect github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e // indirect
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 // indirect
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a // indirect
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 // indirect
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc // indirect
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 // indirect
github.com/tailscale/wireguard-go v0.0.0-20250716170648-1d0488a3d7da // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1210 // indirect
github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect
github.com/transip/gotransip/v6 v6.26.0 // indirect
github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf // indirect
github.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301 // indirect
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect
github.com/ultradns/ultradns-go-sdk v1.8.0-20241010134910-243eeec // indirect
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e // indirect github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e // indirect
github.com/vinyldns/go-vinyldns v0.9.16 // indirect github.com/vinyldns/go-vinyldns v0.9.16 // indirect
github.com/vishvananda/netlink v1.2.1-beta.2.0.20230316163032-ced5aaba43e3 // indirect github.com/vishvananda/netlink v1.3.1 // indirect
github.com/vishvananda/netns v0.0.4 // indirect github.com/vishvananda/netns v0.0.5 // indirect
github.com/vultr/govultr/v2 v2.17.2 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/xtls/reality v0.0.0-20231112171332-de1173cf2b19 // indirect github.com/volcengine/volc-sdk-golang v1.0.216 // indirect
github.com/yandex-cloud/go-genproto v0.0.0-20220805142335-27b56ddae16f // indirect github.com/vultr/govultr/v3 v3.21.1 // indirect
github.com/yandex-cloud/go-sdk v0.0.0-20220805164847-cf028e604997 // indirect github.com/wyx2685/sing-vmess v0.0.0-20250723121437-95d5ab59ff92 // indirect
github.com/zeebo/blake3 v0.2.3 // indirect github.com/x448/float16 v0.8.4 // indirect
go.opencensus.io v0.24.0 // indirect github.com/xtls/reality v0.0.0-20251014195629-e4eec4520535 // indirect
go.uber.org/mock v0.3.0 // indirect github.com/yandex-cloud/go-genproto v0.14.0 // indirect
github.com/yandex-cloud/go-sdk/services/dns v0.0.3 // indirect
github.com/yandex-cloud/go-sdk/v2 v2.0.8 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
github.com/zeebo/blake3 v0.2.4 // indirect
go.mongodb.org/mongo-driver v1.13.1 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
go.opentelemetry.io/otel v1.38.0 // indirect
go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/otel/trace v1.38.0 // indirect
go.uber.org/atomic v1.10.0 // indirect
go.uber.org/mock v0.5.2 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
go.uber.org/ratelimit v0.2.0 // indirect go.uber.org/ratelimit v0.3.1 // indirect
go.uber.org/zap v1.26.0 // indirect go.uber.org/zap/exp v0.3.0 // indirect
go4.org/netipx v0.0.0-20230824141953-6213f710f925 // indirect go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/mod v0.14.0 // indirect golang.org/x/exp v0.0.0-20250911091902-df9299821621 // indirect
golang.org/x/net v0.18.0 // indirect golang.org/x/mod v0.29.0 // indirect
golang.org/x/oauth2 v0.11.0 // indirect golang.org/x/net v0.47.0 // indirect
golang.org/x/text v0.14.0 // indirect golang.org/x/oauth2 v0.32.0 // indirect
golang.org/x/time v0.4.0 // indirect golang.org/x/sync v0.18.0 // indirect
golang.org/x/tools v0.15.0 // indirect golang.org/x/term v0.37.0 // indirect
golang.org/x/text v0.31.0 // indirect
golang.org/x/time v0.12.0 // indirect
golang.org/x/tools v0.38.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
golang.zx2c4.com/wireguard v0.0.0-20231022001213-2e0774f246fb // indirect golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect
google.golang.org/api v0.128.0 // indirect golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
google.golang.org/appengine v1.6.7 // indirect google.golang.org/api v0.242.0 // indirect
google.golang.org/genproto v0.0.0-20231030173426-d783a09b4405 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 // indirect google.golang.org/grpc v1.77.0 // indirect
google.golang.org/grpc v1.59.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/ns1/ns1-go.v2 v2.7.6 // indirect gopkg.in/ns1/ns1-go.v2 v2.14.4 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
gvisor.dev/gvisor v0.0.0-20231104011432-48a6d7d5bd0b // indirect gvisor.dev/gvisor v0.0.0-20250428193742-2d800c3129d5 // indirect
lukechampine.com/blake3 v1.2.1 // indirect lukechampine.com/blake3 v1.4.1 // indirect
) )
replace ( replace github.com/sagernet/sing-box v1.13.0 => github.com/wyx2685/sing-box_mod v1.13.0-alpha.5.0.20251202212447-8d054dcd8bfe
github.com/sagernet/sing-box v1.7.0-beta.3 => github.com/wyx2685/sing-box_mod v0.0.0-20231119064621-2853d5fe45d7
github.com/sagernet/sing-shadowsocks v0.2.4 => github.com/inazumav/sing-shadowsocks v0.0.0-20230815111927-7c68cbaeec5c replace github.com/xtls/xray-core v1.251202.0 => github.com/wyx2685/xray-core v0.0.0-20251202200223-63db1dc9e9e2
)

1627
go.sum

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +0,0 @@
package limiter
import log "github.com/sirupsen/logrus"
func ClearOnlineIP() error {
log.WithField("Type", "Limiter").
Debug("Clear online ip...")
limitLock.RLock()
for _, l := range limiter {
l.ConnLimiter.ClearOnlineIP()
}
limitLock.RUnlock()
log.WithField("Type", "Limiter").
Debug("Clear online ip done")
return nil
}

View File

@@ -1,165 +0,0 @@
package limiter
import (
"sync"
"time"
)
type ConnLimiter struct {
realtime bool
ipLimit int
connLimit int
count sync.Map // map[string]int
ip sync.Map // map[string]map[string]int
}
func NewConnLimiter(conn int, ip int, realtime bool) *ConnLimiter {
return &ConnLimiter{
realtime: realtime,
connLimit: conn,
ipLimit: ip,
count: sync.Map{},
ip: sync.Map{},
}
}
func (c *ConnLimiter) AddConnCount(user string, ip string, isTcp bool) (limit bool) {
if c.connLimit != 0 {
if v, ok := c.count.Load(user); ok {
if v.(int) >= c.connLimit {
// over connection limit
return true
} else if isTcp {
// tcp protocol
// connection count add
c.count.Store(user, v.(int)+1)
}
} else if isTcp {
// tcp protocol
// store connection count
c.count.Store(user, 1)
}
}
if c.ipLimit == 0 {
return false
}
// first user map
ipMap := new(sync.Map)
if c.realtime {
if isTcp {
ipMap.Store(ip, 2)
} else {
ipMap.Store(ip, 1)
}
} else {
ipMap.Store(ip, time.Now())
}
// check user online ip
if v, ok := c.ip.LoadOrStore(user, ipMap); ok {
// have user
ips := v.(*sync.Map)
cn := 0
if online, ok := ips.Load(ip); ok {
// online ip
if c.realtime {
if isTcp {
// tcp count add
ips.Store(ip, online.(int)+2)
}
} else {
// update connect time for not realtime
ips.Store(ip, time.Now())
}
} else {
// not online ip
ips.Range(func(_, _ interface{}) bool {
cn++
if cn >= c.ipLimit {
limit = true
return false
}
return true
})
if limit {
// over ip limit
return
}
if c.realtime {
if isTcp {
ips.Store(ip, 2)
} else {
ips.Store(ip, 1)
}
} else {
ips.Store(ip, time.Now())
}
}
}
return
}
// DelConnCount Delete tcp connection count, no tcp do not use
func (c *ConnLimiter) DelConnCount(user string, ip string) {
if !c.realtime {
return
}
if c.connLimit != 0 {
if v, ok := c.count.Load(user); ok {
if v.(int) == 1 {
c.count.Delete(user)
} else {
c.count.Store(user, v.(int)-1)
}
}
}
if c.ipLimit == 0 {
return
}
if i, ok := c.ip.Load(user); ok {
is := i.(*sync.Map)
if i, ok := is.Load(ip); ok {
if i.(int) == 2 {
is.Delete(ip)
} else {
is.Store(user, i.(int)-2)
}
notDel := false
c.ip.Range(func(_, _ any) bool {
notDel = true
return false
})
if !notDel {
c.ip.Delete(user)
}
}
}
}
// ClearOnlineIP Clear udp,icmp and other packet protocol online ip
func (c *ConnLimiter) ClearOnlineIP() {
c.ip.Range(func(u, v any) bool {
userIp := v.(*sync.Map)
notDel := false
userIp.Range(func(ip, v any) bool {
notDel = true
if _, ok := v.(int); ok {
if v.(int) == 1 {
// clear packet ip for realtime
userIp.Delete(ip)
}
return true
} else {
// clear ip for not realtime
if v.(time.Time).Before(time.Now().Add(time.Minute)) {
// 1 minute no active
userIp.Delete(ip)
}
}
return true
})
if !notDel {
c.ip.Delete(u)
}
return true
})
}

View File

@@ -1,56 +0,0 @@
package limiter
import (
"sync"
"testing"
"time"
)
var c *ConnLimiter
func init() {
c = NewConnLimiter(1, 1, true)
}
func TestConnLimiter_AddConnCount(t *testing.T) {
t.Log(c.AddConnCount("1", "1", true))
t.Log(c.AddConnCount("1", "2", true))
}
func TestConnLimiter_DelConnCount(t *testing.T) {
t.Log(c.AddConnCount("1", "1", true))
t.Log(c.AddConnCount("1", "2", true))
c.DelConnCount("1", "1")
t.Log(c.AddConnCount("1", "2", true))
}
func TestConnLimiter_ClearOnlineIP(t *testing.T) {
t.Log(c.AddConnCount("1", "1", false))
t.Log(c.AddConnCount("1", "2", false))
c.ClearOnlineIP()
t.Log(c.AddConnCount("1", "2", true))
c.DelConnCount("1", "2")
t.Log(c.AddConnCount("1", "1", false))
// not realtime
c.realtime = false
t.Log(c.AddConnCount("3", "2", true))
c.ClearOnlineIP()
t.Log(c.ip.Load("3"))
time.Sleep(time.Minute)
c.ClearOnlineIP()
t.Log(c.ip.Load("3"))
}
func BenchmarkConnLimiter(b *testing.B) {
wg := sync.WaitGroup{}
for i := 0; i < b.N; i++ {
wg.Add(1)
go func() {
c.AddConnCount("1", "2", true)
c.DelConnCount("1", "2")
wg.Done()
}()
}
wg.Wait()
}

View File

@@ -3,6 +3,7 @@ package limiter
import ( import (
"errors" "errors"
"regexp" "regexp"
"strings"
"sync" "sync"
"time" "time"
@@ -10,8 +11,6 @@ import (
"github.com/InazumaV/V2bX/common/format" "github.com/InazumaV/V2bX/common/format"
"github.com/InazumaV/V2bX/conf" "github.com/InazumaV/V2bX/conf"
"github.com/juju/ratelimit" "github.com/juju/ratelimit"
log "github.com/sirupsen/logrus"
"github.com/xtls/xray-core/common/task"
) )
var limitLock sync.RWMutex var limitLock sync.RWMutex
@@ -19,54 +18,51 @@ var limiter map[string]*Limiter
func Init() { func Init() {
limiter = map[string]*Limiter{} limiter = map[string]*Limiter{}
c := task.Periodic{
Interval: time.Minute * 2,
Execute: ClearOnlineIP,
}
go func() {
log.WithField("Type", "Limiter").
Debug("ClearOnlineIP started")
time.Sleep(time.Minute * 2)
_ = c.Start()
}()
} }
type Limiter struct { type Limiter struct {
DomainRules []*regexp.Regexp DomainRules []*regexp.Regexp
ProtocolRules []string ProtocolRules []string
SpeedLimit int SpeedLimit int
UserOnlineIP *sync.Map // Key: Name, value: {Key: Ip, value: Uid} UserOnlineIP *sync.Map // Key: TagUUID, value: {Key: Ip, value: Uid}
UUIDtoUID map[string]int // Key: UUID, value: UID OldUserOnline *sync.Map // Key: Ip, value: Uid
UserLimitInfo *sync.Map // Key: Uid value: UserLimitInfo UUIDtoUID map[string]int // Key: UUID, value: Uid
ConnLimiter *ConnLimiter // Key: Uid value: ConnLimiter UserLimitInfo *sync.Map // Key: TagUUID value: UserLimitInfo
SpeedLimiter *sync.Map // key: Uid, value: *ratelimit.Bucket SpeedLimiter *sync.Map // key: TagUUID, value: *ratelimit.Bucket
AliveList map[int]int // Key: Uid, value: alive_ip
} }
type UserLimitInfo struct { type UserLimitInfo struct {
UID int UID int
SpeedLimit int SpeedLimit int
DeviceLimit int
DynamicSpeedLimit int DynamicSpeedLimit int
ExpireTime int64 ExpireTime int64
OverLimit bool
} }
func AddLimiter(tag string, l *conf.LimitConfig, users []panel.UserInfo) *Limiter { func AddLimiter(tag string, l *conf.LimitConfig, users []panel.UserInfo, aliveList map[int]int) *Limiter {
info := &Limiter{ info := &Limiter{
SpeedLimit: l.SpeedLimit, SpeedLimit: l.SpeedLimit,
UserOnlineIP: new(sync.Map), UserOnlineIP: new(sync.Map),
UserLimitInfo: new(sync.Map), UserLimitInfo: new(sync.Map),
ConnLimiter: NewConnLimiter(l.ConnLimit, l.IPLimit, l.EnableRealtime),
SpeedLimiter: new(sync.Map), SpeedLimiter: new(sync.Map),
AliveList: aliveList,
OldUserOnline: new(sync.Map),
} }
uuidmap := make(map[string]int) uuidmap := make(map[string]int)
for i := range users { for i := range users {
uuidmap[users[i].Uuid] = users[i].Id uuidmap[users[i].Uuid] = users[i].Id
userLimit := &UserLimitInfo{}
userLimit.UID = users[i].Id
if users[i].SpeedLimit != 0 { if users[i].SpeedLimit != 0 {
userLimit := &UserLimitInfo{ userLimit.SpeedLimit = users[i].SpeedLimit
UID: users[i].Id,
SpeedLimit: users[i].SpeedLimit,
}
info.UserLimitInfo.Store(format.UserTag(tag, users[i].Uuid), userLimit)
} }
if users[i].DeviceLimit != 0 {
userLimit.DeviceLimit = users[i].DeviceLimit
}
userLimit.OverLimit = false
info.UserLimitInfo.Store(format.UserTag(tag, users[i].Uuid), userLimit)
} }
info.UUIDtoUID = uuidmap info.UUIDtoUID = uuidmap
limitLock.Lock() limitLock.Lock()
@@ -94,17 +90,24 @@ func DeleteLimiter(tag string) {
func (l *Limiter) UpdateUser(tag string, added []panel.UserInfo, deleted []panel.UserInfo) { func (l *Limiter) UpdateUser(tag string, added []panel.UserInfo, deleted []panel.UserInfo) {
for i := range deleted { for i := range deleted {
l.UserLimitInfo.Delete(format.UserTag(tag, deleted[i].Uuid)) l.UserLimitInfo.Delete(format.UserTag(tag, deleted[i].Uuid))
l.UserOnlineIP.Delete(format.UserTag(tag, deleted[i].Uuid))
l.SpeedLimiter.Delete(format.UserTag(tag, deleted[i].Uuid))
delete(l.UUIDtoUID, deleted[i].Uuid) delete(l.UUIDtoUID, deleted[i].Uuid)
delete(l.AliveList, deleted[i].Id)
} }
for i := range added { for i := range added {
if added[i].SpeedLimit != 0 { userLimit := &UserLimitInfo{
userLimit := &UserLimitInfo{ UID: added[i].Id,
UID: added[i].Id,
SpeedLimit: added[i].SpeedLimit,
ExpireTime: 0,
}
l.UserLimitInfo.Store(format.UserTag(tag, added[i].Uuid), userLimit)
} }
if added[i].SpeedLimit != 0 {
userLimit.SpeedLimit = added[i].SpeedLimit
userLimit.ExpireTime = 0
}
if added[i].DeviceLimit != 0 {
userLimit.DeviceLimit = added[i].DeviceLimit
}
userLimit.OverLimit = false
l.UserLimitInfo.Store(format.UserTag(tag, added[i].Uuid), userLimit)
l.UUIDtoUID[added[i].Uuid] = added[i].Id l.UUIDtoUID[added[i].Uuid] = added[i].Id
} }
} }
@@ -120,53 +123,75 @@ func (l *Limiter) UpdateDynamicSpeedLimit(tag, uuid string, limit int, expire ti
return nil return nil
} }
func (l *Limiter) CheckLimit(email string, ip string, isTcp bool) (Bucket *ratelimit.Bucket, Reject bool) { func (l *Limiter) CheckLimit(taguuid string, ip string, isTcp bool, noSSUDP bool) (Bucket *ratelimit.Bucket, Reject bool) {
// ip and conn limiter // check if ipv4 mapped ipv6
if l.ConnLimiter.AddConnCount(email, ip, isTcp) { ip = strings.TrimPrefix(ip, "::ffff:")
return nil, true
}
// check and gen speed limit Bucket // check and gen speed limit Bucket
nodeLimit := l.SpeedLimit nodeLimit := l.SpeedLimit
userLimit := 0 userLimit := 0
if v, ok := l.UserLimitInfo.Load(email); ok { deviceLimit := 0
var uid int
if v, ok := l.UserLimitInfo.Load(taguuid); ok {
u := v.(*UserLimitInfo) u := v.(*UserLimitInfo)
deviceLimit = u.DeviceLimit
uid = u.UID
if u.ExpireTime < time.Now().Unix() && u.ExpireTime != 0 { if u.ExpireTime < time.Now().Unix() && u.ExpireTime != 0 {
if u.SpeedLimit != 0 { if u.SpeedLimit != 0 {
userLimit = u.SpeedLimit userLimit = u.SpeedLimit
u.DynamicSpeedLimit = 0 u.DynamicSpeedLimit = 0
u.ExpireTime = 0 u.ExpireTime = 0
} else { } else {
l.UserLimitInfo.Delete(email) l.UserLimitInfo.Delete(taguuid)
} }
} else { } else {
userLimit = determineSpeedLimit(u.SpeedLimit, u.DynamicSpeedLimit) userLimit = determineSpeedLimit(u.SpeedLimit, u.DynamicSpeedLimit)
} }
} else {
return nil, true
} }
if noSSUDP {
// Store online user for device limit // Store online user for device limit
ipMap := new(sync.Map) newipMap := new(sync.Map)
uid := l.UUIDtoUID[email] newipMap.Store(ip, uid)
ipMap.Store(ip, uid) aliveIp := l.AliveList[uid]
// If any device is online // If any device is online
if v, ok := l.UserOnlineIP.LoadOrStore(email, ipMap); ok { if v, loaded := l.UserOnlineIP.LoadOrStore(taguuid, newipMap); loaded {
ipMap := v.(*sync.Map) oldipMap := v.(*sync.Map)
// If this is a new ip // If this is a new ip
if _, ok := ipMap.LoadOrStore(ip, uid); !ok { if _, loaded := oldipMap.LoadOrStore(ip, uid); !loaded {
counter := 0 if v, loaded := l.OldUserOnline.Load(ip); loaded {
ipMap.Range(func(key, value interface{}) bool { if v.(int) == uid {
counter++ l.OldUserOnline.Delete(ip)
return true }
}) } else if deviceLimit > 0 {
if deviceLimit <= aliveIp {
oldipMap.Delete(ip)
return nil, true
}
}
}
} else if v, ok := l.OldUserOnline.Load(ip); ok {
if v.(int) == uid {
l.OldUserOnline.Delete(ip)
}
} else {
if deviceLimit > 0 {
if deviceLimit <= aliveIp {
l.UserOnlineIP.Delete(taguuid)
return nil, true
}
}
} }
} }
limit := int64(determineSpeedLimit(nodeLimit, userLimit)) * 1000000 / 8 // If you need the Speed limit limit := int64(determineSpeedLimit(nodeLimit, userLimit)) * 1000000 / 8 // If you need the Speed limit
if limit > 0 { if limit > 0 {
Bucket = ratelimit.NewBucketWithQuantum(time.Second, limit, limit) // Byte/s Bucket = ratelimit.NewBucketWithQuantum(time.Second, limit, limit) // Byte/s
if v, ok := l.SpeedLimiter.LoadOrStore(email, Bucket); ok { if v, ok := l.SpeedLimiter.LoadOrStore(taguuid, Bucket); ok {
return v.(*ratelimit.Bucket), false return v.(*ratelimit.Bucket), false
} else { } else {
l.SpeedLimiter.Store(email, Bucket) l.SpeedLimiter.Store(taguuid, Bucket)
return Bucket, false return Bucket, false
} }
} else { } else {
@@ -176,17 +201,18 @@ func (l *Limiter) CheckLimit(email string, ip string, isTcp bool) (Bucket *ratel
func (l *Limiter) GetOnlineDevice() (*[]panel.OnlineUser, error) { func (l *Limiter) GetOnlineDevice() (*[]panel.OnlineUser, error) {
var onlineUser []panel.OnlineUser var onlineUser []panel.OnlineUser
l.OldUserOnline = new(sync.Map)
l.UserOnlineIP.Range(func(key, value interface{}) bool { l.UserOnlineIP.Range(func(key, value interface{}) bool {
email := key.(string) taguuid := key.(string)
ipMap := value.(*sync.Map) ipMap := value.(*sync.Map)
ipMap.Range(func(key, value interface{}) bool { ipMap.Range(func(key, value interface{}) bool {
uid := value.(int) uid := value.(int)
ip := key.(string) ip := key.(string)
l.OldUserOnline.Store(ip, uid)
onlineUser = append(onlineUser, panel.OnlineUser{UID: uid, IP: ip}) onlineUser = append(onlineUser, panel.OnlineUser{UID: uid, IP: ip})
return true return true
}) })
l.UserOnlineIP.Delete(email) // Reset online device l.UserOnlineIP.Delete(taguuid) // Reset online device
return true return true
}) })
@@ -197,23 +223,3 @@ type UserIpList struct {
Uid int `json:"Uid"` Uid int `json:"Uid"`
IpList []string `json:"Ips"` IpList []string `json:"Ips"`
} }
func determineDeviceLimit(nodeLimit, userLimit int) (limit int) {
if nodeLimit == 0 || userLimit == 0 {
if nodeLimit > userLimit {
return nodeLimit
} else if nodeLimit < userLimit {
return userLimit
} else {
return 0
}
} else {
if nodeLimit > userLimit {
return userLimit
} else if nodeLimit < userLimit {
return nodeLimit
} else {
return nodeLimit
}
}
}

View File

@@ -1,6 +1,8 @@
package main package main
import "github.com/InazumaV/V2bX/cmd" import (
"github.com/InazumaV/V2bX/cmd"
)
func main() { func main() {
cmd.Run() cmd.Run()

View File

@@ -19,6 +19,7 @@ type Controller struct {
limiter *limiter.Limiter limiter *limiter.Limiter
traffic map[string]int64 traffic map[string]int64
userList []panel.UserInfo userList []panel.UserInfo
aliveMap map[int]int
info *panel.NodeInfo info *panel.NodeInfo
nodeInfoMonitorPeriodic *task.Task nodeInfoMonitorPeriodic *task.Task
userReportPeriodic *task.Task userReportPeriodic *task.Task
@@ -54,6 +55,10 @@ func (c *Controller) Start() error {
if len(c.userList) == 0 { if len(c.userList) == 0 {
return errors.New("add users error: not have any user") return errors.New("add users error: not have any user")
} }
c.aliveMap, err = c.apiClient.GetUserAlive()
if err != nil {
return fmt.Errorf("failed to get user alive list: %s", err)
}
if len(c.Options.Name) == 0 { if len(c.Options.Name) == 0 {
c.tag = c.buildNodeTag(node) c.tag = c.buildNodeTag(node)
} else { } else {
@@ -61,7 +66,7 @@ func (c *Controller) Start() error {
} }
// add limiter // add limiter
l := limiter.AddLimiter(c.tag, &c.LimitConfig, c.userList) l := limiter.AddLimiter(c.tag, &c.LimitConfig, c.userList, c.aliveMap)
// add rule limiter // add rule limiter
if err = l.UpdateRule(&node.Rules); err != nil { if err = l.UpdateRule(&node.Rules); err != nil {
return fmt.Errorf("update rule error: %s", err) return fmt.Errorf("update rule error: %s", err)
@@ -110,6 +115,10 @@ func (c *Controller) Close() error {
if c.onlineIpReportPeriodic != nil { if c.onlineIpReportPeriodic != nil {
c.onlineIpReportPeriodic.Close() c.onlineIpReportPeriodic.Close()
} }
err := c.server.DelNode(c.tag)
if err != nil {
return fmt.Errorf("del node error: %s", err)
}
return nil return nil
} }

View File

@@ -13,11 +13,12 @@ import (
"strings" "strings"
"time" "time"
"encoding/json"
"github.com/go-acme/lego/v4/certificate" "github.com/go-acme/lego/v4/certificate"
"github.com/go-acme/lego/v4/challenge/http01" "github.com/go-acme/lego/v4/challenge/http01"
"github.com/go-acme/lego/v4/providers/dns" "github.com/go-acme/lego/v4/providers/dns"
"github.com/go-acme/lego/v4/registration" "github.com/go-acme/lego/v4/registration"
"github.com/goccy/go-json"
"github.com/InazumaV/V2bX/common/file" "github.com/InazumaV/V2bX/common/file"
"github.com/InazumaV/V2bX/conf" "github.com/InazumaV/V2bX/conf"
@@ -262,12 +263,14 @@ func (u *User) DecodePrivate(pemEncodedPriv string) (*ecdsa.PrivateKey, error) {
privateKey, err := x509.ParseECPrivateKey(x509EncodedPriv) privateKey, err := x509.ParseECPrivateKey(x509EncodedPriv)
return privateKey, err return privateKey, err
} }
func (u *User) Load(path string) error { func (u *User) Load(path string) error {
f, err := os.Open(path) data, err := os.ReadFile(path)
if err != nil { if err != nil {
return fmt.Errorf("open file error: %s", err) return fmt.Errorf("open file error: %s", err)
} }
err = json.NewDecoder(f).Decode(u)
err = json.Unmarshal(data, u)
if err != nil { if err != nil {
return fmt.Errorf("unmarshal json error: %s", err) return fmt.Errorf("unmarshal json error: %s", err)
} }

View File

@@ -68,6 +68,15 @@ func (c *Controller) nodeInfoMonitor() (err error) {
}).Error("Get user list failed") }).Error("Get user list failed")
return nil return nil
} }
// get user alive
newA, err := c.apiClient.GetUserAlive()
if err != nil {
log.WithFields(log.Fields{
"tag": c.tag,
"err": err,
}).Error("Get alive list failed")
return nil
}
if newN != nil { if newN != nil {
c.info = newN c.info = newN
// nodeInfo changed // nodeInfo changed
@@ -82,7 +91,7 @@ func (c *Controller) nodeInfoMonitor() (err error) {
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"tag": c.tag, "tag": c.tag,
"err": err, "err": err,
}).Error("Delete node failed") }).Panic("Delete node failed")
return nil return nil
} }
@@ -92,9 +101,13 @@ func (c *Controller) nodeInfoMonitor() (err error) {
// Remove Old limiter // Remove Old limiter
limiter.DeleteLimiter(c.tag) limiter.DeleteLimiter(c.tag)
// Add new Limiter // Add new Limiter
l := limiter.AddLimiter(c.tag, &c.LimitConfig, c.userList) l := limiter.AddLimiter(c.tag, &c.LimitConfig, c.userList, newA)
c.limiter = l c.limiter = l
} }
// update alive list
if newA != nil {
c.limiter.AliveList = newA
}
// Update rule // Update rule
err = c.limiter.UpdateRule(&newN.Rules) err = c.limiter.UpdateRule(&newN.Rules)
if err != nil { if err != nil {
@@ -122,7 +135,7 @@ func (c *Controller) nodeInfoMonitor() (err error) {
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"tag": c.tag, "tag": c.tag,
"err": err, "err": err,
}).Error("Add node failed") }).Panic("Add node failed")
return nil return nil
} }
_, err = c.server.AddUsers(&vCore.AddUsersParams{ _, err = c.server.AddUsers(&vCore.AddUsersParams{
@@ -154,7 +167,10 @@ func (c *Controller) nodeInfoMonitor() (err error) {
// exit // exit
return nil return nil
} }
// update alive list
if newA != nil {
c.limiter.AliveList = newA
}
// node no changed, check users // node no changed, check users
if len(newU) == 0 { if len(newU) == 0 {
return nil return nil
@@ -162,7 +178,7 @@ func (c *Controller) nodeInfoMonitor() (err error) {
deleted, added := compareUserList(c.userList, newU) deleted, added := compareUserList(c.userList, newU)
if len(deleted) > 0 { if len(deleted) > 0 {
// have deleted users // have deleted users
err = c.server.DelUsers(deleted, c.tag) err = c.server.DelUsers(deleted, c.tag, c.info)
if err != nil { if err != nil {
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"tag": c.tag, "tag": c.tag,

View File

@@ -1,7 +1,6 @@
package node package node
import ( import (
"fmt"
"strconv" "strconv"
"github.com/InazumaV/V2bX/api/panel" "github.com/InazumaV/V2bX/api/panel"
@@ -9,24 +8,7 @@ import (
) )
func (c *Controller) reportUserTrafficTask() (err error) { func (c *Controller) reportUserTrafficTask() (err error) {
// Get User traffic userTraffic, _ := c.server.GetUserTrafficSlice(c.tag, true)
userTraffic := make([]panel.UserTraffic, 0)
for i := range c.userList {
up, down := c.server.GetUserTraffic(c.tag, c.userList[i].Uuid, true)
if up > 0 || down > 0 {
if c.LimitConfig.EnableDynamicSpeedLimit {
if _, ok := c.traffic[c.userList[i].Uuid]; ok {
c.traffic[c.userList[i].Uuid] += up + down
} else {
c.traffic[c.userList[i].Uuid] = up + down
}
}
userTraffic = append(userTraffic, panel.UserTraffic{
UID: (c.userList)[i].Id,
Upload: up,
Download: down})
}
}
if len(userTraffic) > 0 { if len(userTraffic) > 0 {
err = c.apiClient.ReportUserTraffic(userTraffic) err = c.apiClient.ReportUserTraffic(userTraffic)
if err != nil { if err != nil {
@@ -36,6 +18,7 @@ func (c *Controller) reportUserTrafficTask() (err error) {
}).Info("Report user traffic failed") }).Info("Report user traffic failed")
} else { } else {
log.WithField("tag", c.tag).Infof("Report %d users traffic", len(userTraffic)) log.WithField("tag", c.tag).Infof("Report %d users traffic", len(userTraffic))
log.WithField("tag", c.tag).Debugf("User traffic: %+v", userTraffic)
} }
} }
@@ -56,24 +39,19 @@ func (c *Controller) reportUserTrafficTask() (err error) {
result = append(result, online) result = append(result, online)
} }
} }
reportOnline := make(map[int]int)
data := make(map[int][]string) data := make(map[int][]string)
for _, onlineuser := range result { for _, onlineuser := range result {
// json structure: { UID1:["ip1","ip2"],UID2:["ip3","ip4"] } // json structure: { UID1:["ip1","ip2"],UID2:["ip3","ip4"] }
data[onlineuser.UID] = append(data[onlineuser.UID], fmt.Sprintf("%s_%d", onlineuser.IP, c.info.Id)) data[onlineuser.UID] = append(data[onlineuser.UID], onlineuser.IP)
if _, ok := reportOnline[onlineuser.UID]; ok {
reportOnline[onlineuser.UID]++
} else {
reportOnline[onlineuser.UID] = 1
}
} }
if err = c.apiClient.ReportNodeOnlineUsers(&data, &reportOnline); err != nil { if err = c.apiClient.ReportNodeOnlineUsers(&data); err != nil {
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"tag": c.tag, "tag": c.tag,
"err": err, "err": err,
}).Info("Report online users failed") }).Info("Report online users failed")
} else { } else {
log.WithField("tag", c.tag).Infof("Total %d online users, %d Reported", len(*onlineDevice), len(result)) log.WithField("tag", c.tag).Infof("Total %d online users, %d Reported", len(*onlineDevice), len(result))
log.WithField("tag", c.tag).Debugf("Online users: %+v", data)
} }
} }
@@ -82,29 +60,24 @@ func (c *Controller) reportUserTrafficTask() (err error) {
} }
func compareUserList(old, new []panel.UserInfo) (deleted, added []panel.UserInfo) { func compareUserList(old, new []panel.UserInfo) (deleted, added []panel.UserInfo) {
tmp := map[string]struct{}{} oldMap := make(map[string]int)
tmp2 := map[string]struct{}{} for i, user := range old {
for i := range old { key := user.Uuid + strconv.Itoa(user.SpeedLimit)
tmp[old[i].Uuid+strconv.Itoa(old[i].SpeedLimit)] = struct{}{} oldMap[key] = i
} }
l := len(tmp)
for i := range new { for _, user := range new {
e := new[i].Uuid + strconv.Itoa(new[i].SpeedLimit) key := user.Uuid + strconv.Itoa(user.SpeedLimit)
tmp[e] = struct{}{} if _, exists := oldMap[key]; !exists {
tmp2[e] = struct{}{} added = append(added, user)
if l != len(tmp) { } else {
added = append(added, new[i]) delete(oldMap, key)
l++
} }
} }
tmp = nil
l = len(tmp2) for _, index := range oldMap {
for i := range old { deleted = append(deleted, old[index])
tmp2[old[i].Uuid+strconv.Itoa(old[i].SpeedLimit)] = struct{}{}
if l != len(tmp2) {
deleted = append(deleted, old[i])
l++
}
} }
return deleted, added return deleted, added
} }