Compare commits

..

80 Commits

Author SHA1 Message Date
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
55 changed files with 3204 additions and 1257 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,94 @@
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: 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@v5
with:
context: .
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 }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v3
with:
name: digests
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@v3
with:
name: digests
path: /tmp/digests
- 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
working-directory: /tmp/digests
run: |
ls -al
echo docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *)
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *)
- 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
- name: Install Go
uses: actions/setup-go@v4
with:
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
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # Command-line programs to run using the OS shell.
# If this step fails, then you should remove it and run the build manually (see below) # 📚 https://git.io/JvXDl
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell. # If the Autobuild fails above, remove it and uncomment the following three lines
# 📚 https://git.io/JvXDl # and modify them (or add more) to build your code if your project
# uses a compiled language
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines #- run: |
# and modify them (or add more) to build your code if your project # make bootstrap
# uses a compiled language # make release
#- run: | - name: Perform CodeQL Analysis
# make bootstrap uses: github/codeql-action/analyze@v2
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View File

@@ -5,6 +5,7 @@ on:
push: push:
branches: branches:
- master - master
- dev_new
paths: paths:
- "**/*.go" - "**/*.go"
- "go.mod" - "go.mod"
@@ -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,9 +107,9 @@ 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.23.2'
- name: Get project dependencies - name: Get project dependencies
run: go mod download run: go mod download
@@ -124,13 +125,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=" go build -v -o build_assets/V2bX -tags "sing xray hysteria2 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="
- 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=" GOMIPS=softfloat go build -v -o build_assets/V2bX_softfloat -tags "sing xray hysteria2 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="
- name: Rename Windows V2bX - name: Rename Windows V2bX
if: matrix.goos == 'windows' if: matrix.goos == 'windows'
run: | run: |
@@ -141,16 +142,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 +167,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: |

4
.gitignore vendored
View File

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

17
Dockerfile Normal file
View File

@@ -0,0 +1,17 @@
# Build go
FROM golang:1.23.2-alpine AS builder
WORKDIR /app
COPY . .
ENV CGO_ENABLED=0
RUN go mod download
RUN go build -v -o V2bX -tags "sing xray hysteria2 with_reality_server with_quic with_grpc with_utls with_wireguard with_acme"
# 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,7 +33,7 @@ A V2board node server based on multi core, modified from XrayR.
| 自定义DNS | √ | √ | √ | √ | | 自定义DNS | √ | √ | √ | √ |
| 在线IP数限制 | √ | √ | √ | √ | | 在线IP数限制 | √ | √ | √ | √ |
| 连接数限制 | √ | √ | √ | √ | | 连接数限制 | √ | √ | √ | √ |
| 跨节点IP数限制 | | | | | | 跨节点IP数限制 | | | | |
| 按照用户限速 | √ | √ | √ | √ | | 按照用户限速 | √ | √ | √ | √ |
| 动态限速(未测试) | √ | √ | √ | √ | | 动态限速(未测试) | √ | √ | √ | √ |
@@ -47,22 +48,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" go build -v -o ./V2bX -tags "xray sing hysteria2 with_reality_server with_quic with_grpc with_utls with_wireguard with_acme" -trimpath -ldflags "-s -w -buildid="
``` ```
## 配置文件及详细使用教程 ## 配置文件及详细使用教程
[详细使用教程](https://yuzuki-1.gitbook.io/v2bx-doc/) [详细使用教程](https://v2bx.v-50.me/)
## 免责声明 ## 免责声明
@@ -82,4 +83,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,14 +1,14 @@
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"
"github.com/goccy/go-json" "github.com/goccy/go-json"
) )
@@ -78,7 +78,7 @@ type TlsSettings struct {
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,string"` Xver uint64 `json:"xver,string"`
} }
type RealityConfig struct { type RealityConfig struct {
@@ -94,7 +94,11 @@ 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 HysteriaNode struct { type HysteriaNode struct {
CommonNode CommonNode
@@ -126,12 +130,31 @@ 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 r.StatusCode() == 304 {
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 { if err = c.checkResponse(r, path, err); err != nil {
return nil, err return nil, err
} }
if r.StatusCode() == 304 {
return nil, nil 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,
@@ -161,18 +184,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,7 +199,7 @@ 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 "hysteria": case "hysteria":
@@ -260,8 +271,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

@@ -23,7 +23,9 @@ type Client struct {
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) {
@@ -70,5 +72,7 @@ func New(c *conf.ApiConfig) (*Client, error) {
APIHost: c.APIHost, APIHost: c.APIHost,
NodeType: c.NodeType, NodeType: c.NodeType,
NodeId: c.NodeID, NodeId: c.NodeID,
UserList: &UserListBody{},
AliveMap: &AliveMap{},
}, nil }, nil
} }

View File

@@ -16,7 +16,6 @@ type UserInfo struct {
Uuid string `json:"uuid"` Uuid string `json:"uuid"`
SpeedLimit int `json:"speed_limit"` SpeedLimit int `json:"speed_limit"`
DeviceLimit int `json:"device_limit"` DeviceLimit int `json:"device_limit"`
AliveIp int `json:"alive_ip"`
} }
type UserListBody struct { type UserListBody struct {
@@ -24,50 +23,58 @@ type UserListBody struct {
Users []UserInfo `json:"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).
ForceContentType("application/json").
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) }
userlist := &UserListBody{}
if err := json.Unmarshal(r.Body(), userlist); err != nil {
return nil, fmt.Errorf("unmarshal user list error: %w", err)
} }
c.userEtag = r.Header().Get("ETag") c.userEtag = r.Header().Get("ETag")
return userlist.Users, nil
}
var userinfos []UserInfo // GetUserAlive will fetch the alive_ip count for users
var localDeviceLimit int = 0 func (c *Client) GetUserAlive() (map[int]int, error) {
for _, user := range userList.Users { c.AliveMap = &AliveMap{}
// If there is still device available, add the user const path = "/api/v1/server/UniProxy/alivelist"
if user.DeviceLimit > 0 && user.AliveIp > 0 { r, err := c.client.R().
lastOnline := 0 ForceContentType("application/json").
if v, ok := c.LastReportOnline[user.Id]; ok { Get(path)
lastOnline = v if err != nil || r.StatusCode() >= 399 {
} c.AliveMap.Alive = make(map[int]int)
// If there are any available device. }
localDeviceLimit = user.DeviceLimit - user.AliveIp + lastOnline if r == nil || r.RawResponse == nil {
if localDeviceLimit > 0 { fmt.Printf("received nil response or raw response")
c.AliveMap.Alive = make(map[int]int)
} else if lastOnline > 0 { }
defer r.RawResponse.Body.Close()
} else { if err := json.Unmarshal(r.Body(), c.AliveMap); err != nil {
continue fmt.Printf("unmarshal user alive list error: %s", err)
} c.AliveMap.Alive = make(map[int]int)
}
userinfos = append(userinfos, user)
} }
return userinfos, nil return c.AliveMap.Alive, nil
} }
type UserTraffic struct { type UserTraffic struct {
@@ -94,8 +101,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,6 +1,11 @@
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"
@@ -8,10 +13,6 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"gopkg.in/natefinch/lumberjack.v2" "gopkg.in/natefinch/lumberjack.v2"
"os"
"os/signal"
"runtime"
"syscall"
) )
var ( var (

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,60 +15,52 @@ 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(id string) *TrafficStorage {
c.lock.RLock() if cts, ok := c.counters.Load(id); 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(id, newStorage); loaded {
return cts.(*TrafficStorage)
}
return newStorage
} }
func (c *TrafficCounter) GetUpCount(id string) int64 { func (c *TrafficCounter) GetUpCount(id string) int64 {
c.lock.RLock() if cts, ok := c.counters.Load(id); 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(id string) int64 {
c.lock.RLock() if cts, ok := c.counters.Load(id); 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(id string) {
c.lock.RLock() if cts, ok := c.counters.Load(id); 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(id string) {
c.lock.Lock() c.counters.Delete(id)
delete(c.counters, id)
c.lock.Unlock()
} }
func (c *TrafficCounter) Rx(id string, n int) { func (c *TrafficCounter) Rx(id string, n int) {
@@ -81,11 +72,3 @@ func (c *TrafficCounter) Tx(id string, n int) {
cts := c.GetCounter(id) cts := c.GetCounter(id)
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

@@ -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 {
@@ -31,6 +28,7 @@ func (c *Conn) Write(b []byte) (n int, err error) {
return c.Conn.Write(b) return c.Conn.Write(b)
} }
/*
type PacketConnCounter struct { type PacketConnCounter struct {
network.PacketConn network.PacketConn
limiter *ratelimit.Bucket limiter *ratelimit.Bucket
@@ -47,10 +45,11 @@ func (p *PacketConnCounter) ReadPacket(buff *buf.Buffer) (destination M.Socksadd
pLen := buff.Len() pLen := buff.Len()
destination, err = p.PacketConn.ReadPacket(buff) destination, err = p.PacketConn.ReadPacket(buff)
p.limiter.Wait(int64(buff.Len() - pLen)) p.limiter.Wait(int64(buff.Len() - pLen))
return return destination, err
} }
func (p *PacketConnCounter) WritePacket(buff *buf.Buffer, destination M.Socksaddr) (err error) { func (p *PacketConnCounter) WritePacket(buff *buf.Buffer, destination M.Socksaddr) (err error) {
p.limiter.Wait(int64(buff.Len())) p.limiter.Wait(int64(buff.Len()))
return p.PacketConn.WritePacket(buff, destination) return p.PacketConn.WritePacket(buff, destination)
} }
*/

View File

@@ -2,9 +2,11 @@ package conf
import ( import (
"fmt" "fmt"
"github.com/InazumaV/V2bX/common/json5" "io"
"os" "os"
"github.com/InazumaV/V2bX/common/json5"
"github.com/goccy/go-json" "github.com/goccy/go-json"
) )
@@ -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

@@ -113,6 +113,7 @@ type Options struct {
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 +130,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

@@ -8,7 +8,6 @@ type SingConfig struct {
LogConfig SingLogConfig `json:"Log"` LogConfig SingLogConfig `json:"Log"`
NtpConfig SingNtpConfig `json:"NTP"` NtpConfig SingNtpConfig `json:"NTP"`
EnableConnClear bool `json:"EnableConnClear"` EnableConnClear bool `json:"EnableConnClear"`
DnsConfigPath string `json:"DnsConfigPath"`
OriginalPath string `json:"OriginalPath"` OriginalPath string `json:"OriginalPath"`
} }
@@ -34,13 +33,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 +59,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)
} }

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

@@ -0,0 +1,431 @@
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"
"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) 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},
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
}
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,
Outbound: Outbound,
BandwidthConfig: *n.getBandwidthConfig(info),
IgnoreClientBandwidth: c.IgnoreClientBandwidth,
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"
quic "github.com/apernet/quic-go"
"go.uber.org/zap"
)
var _ server.TrafficLogger = (*HookServer)(nil)
type HookServer struct {
Tag string
logger *zap.Logger
Counter sync.Map
}
func (h *HookServer) TraceStream(stream quic.Stream, stats *server.StreamStats) {
}
func (h *HookServer) UntraceStream(stream quic.Stream) {
}
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(fmt.Sprintf("unsupported log level: %s\n", logLevel))
}
enc, ok := logFormatMap[strings.ToLower(logFormat)]
if !ok {
return nil, fmt.Errorf(fmt.Sprintf("unsupported log format: %s\n", 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(fmt.Sprintf("failed to initialize logger: %s\n", 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 ""
}
}

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

@@ -0,0 +1,76 @@
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,
},
}
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
}

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

@@ -0,0 +1,228 @@
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"`
DisableUDP bool `mapstructure:"disableUDP"`
UDPIdleTimeout time.Duration `mapstructure:"udpIdleTimeout"`
Auth serverConfigAuth `mapstructure:"auth"`
Resolver serverConfigResolver `mapstructure:"resolver"`
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 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)
}

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

@@ -0,0 +1,70 @@
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.Mutex
}
func (v *V2bX) Authenticate(addr net.Addr, auth string, tx uint64) (ok bool, id string) {
v.mutex.Lock()
defer v.mutex.Unlock()
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) error {
var wg sync.WaitGroup
for _, user := range users {
wg.Add(1)
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) GetUserTraffic(tag string, uuid string, reset bool) (up int64, down int64) {
if v, ok := h.Hy2nodes[tag].TrafficLogger.(*HookServer).Counter.Load(tag); ok {
c := v.(*counter.TrafficCounter)
up = c.GetUpCount(uuid)
down = c.GetDownCount(uuid)
if reset {
c.Reset(uuid)
}
return up, down
}
return 0, 0
}

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

@@ -6,8 +6,7 @@ import (
"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,6 +17,8 @@ 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 EnableConnClear bool
counter sync.Map counter sync.Map
@@ -56,57 +57,29 @@ func (h *HookServer) ModeList() []string {
} }
func NewHookServer(enableClear bool) *HookServer { func NewHookServer(enableClear bool) *HookServer {
return &HookServer{ server := &HookServer{
EnableConnClear: enableClear, EnableConnClear: enableClear,
counter: sync.Map{}, counter: sync.Map{},
connClears: sync.Map{}, connClears: sync.Map{},
} }
return server
} }
func (h *HookServer) Start() error { func (h *HookServer) RoutedConnection(_ context.Context, conn net.Conn, m adapter.InboundContext, _ adapter.Rule, _ adapter.Outbound) net.Conn {
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
} }
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(format.UserTag(m.Inbound, m.User), 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() {
l.ConnLimiter.DelConnCount(m.User, ip)
})
if h.EnableConnClear { if h.EnableConnClear {
var key int
cc := &ConnClear{ cc := &ConnClear{
conns: map[int]io.Closer{ conns: map[int]io.Closer{
0: conn, 0: conn,
@@ -114,50 +87,32 @@ func (h *HookServer) RoutedConnection(_ context.Context, conn net.Conn, m adapte
} }
if v, ok := h.connClears.LoadOrStore(m.Inbound+m.User, cc); ok { if v, ok := h.connClears.LoadOrStore(m.Inbound+m.User, cc); ok {
cc = v.(*ConnClear) cc = v.(*ConnClear)
key = cc.AddConn(conn)
} }
t.AddLeave(func() {
cc.DelConn(key)
})
} }
if c, ok := h.counter.Load(m.Inbound); ok { if c, ok := h.counter.Load(m.Inbound); ok {
return counter.NewConnCounter(conn, c.(*counter.TrafficCounter).GetCounter(m.User)), t return counter.NewConnCounter(conn, c.(*counter.TrafficCounter).GetCounter(m.User))
} else { } else {
c := counter.NewTrafficCounter() c := counter.NewTrafficCounter()
h.counter.Store(m.Inbound, c) h.counter.Store(m.Inbound, c)
return counter.NewConnCounter(conn, c.GetCounter(m.User)), t return counter.NewConnCounter(conn, c.GetCounter(m.User))
} }
} }
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 { if b, r := l.CheckLimit(format.UserTag(m.Inbound, m.User), 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 h.EnableConnClear {
var key int
cc := &ConnClear{ cc := &ConnClear{
conns: map[int]io.Closer{ conns: map[int]io.Closer{
0: conn, 0: conn,
@@ -165,57 +120,20 @@ func (h *HookServer) RoutedPacketConnection(_ context.Context, conn N.PacketConn
} }
if v, ok := h.connClears.LoadOrStore(m.Inbound+m.User, cc); ok { if v, ok := h.connClears.LoadOrStore(m.Inbound+m.User, cc); ok {
cc = v.(*ConnClear) cc = v.(*ConnClear)
key = cc.AddConn(conn)
} }
t.AddLeave(func() {
cc.DelConn(key)
})
} }
if c, ok := h.counter.Load(m.Inbound); ok { if c, ok := h.counter.Load(m.Inbound); ok {
return counter.NewPacketConnCounter(conn, c.(*counter.TrafficCounter).GetCounter(m.User)), t return counter.NewPacketConnCounter(conn, c.(*counter.TrafficCounter).GetCounter(m.User))
} else { } else {
c := counter.NewTrafficCounter() c := counter.NewTrafficCounter()
h.counter.Store(m.Inbound, c) h.counter.Store(m.Inbound, c)
return counter.NewPacketConnCounter(conn, c.GetCounter(m.User)), t return counter.NewPacketConnCounter(conn, c.GetCounter(m.User))
} }
} }
// 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) { func (h *HookServer) ClearConn(inbound string, user string) {
if v, ok := h.connClears.Load(inbound + user); ok { if v, ok := h.connClears.Load(inbound + user); ok {
v.(*ConnClear).ClearConn() v.(*ConnClear).ClearConn()
h.connClears.Delete(inbound + user) 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]()
}
}

View File

@@ -13,16 +13,42 @@ import (
"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/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 {
@@ -33,16 +59,28 @@ func getInboundOptions(tag string, info *panel.NodeInfo, c *conf.Options) (optio
domainStrategy = c.SingOptions.DomainStrategy 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{ InboundOptions: option.InboundOptions{
SniffEnabled: c.SingOptions.SniffEnabled, SniffEnabled: c.SingOptions.SniffEnabled,
SniffOverrideDestination: c.SingOptions.SniffOverrideDestination, SniffOverrideDestination: c.SingOptions.SniffOverrideDestination,
DomainStrategy: domainStrategy, DomainStrategy: domainStrategy,
}, },
} }
var multiplex *option.InboundMultiplexOptions
if c.SingOptions.Multiplex != nil {
multiplexOption := option.InboundMultiplexOptions{
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 {
case panel.Tls: case panel.Tls:
@@ -74,14 +112,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 +133,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 +177,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 +191,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 +242,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 +339,20 @@ 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 "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 +368,34 @@ 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, Obfs: obfs,
TLS: &tls, 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)
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 +403,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

@@ -5,14 +5,15 @@ import (
"fmt" "fmt"
"os" "os"
"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)
@@ -36,16 +37,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())
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 +65,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,
@@ -87,14 +73,13 @@ func New(c *conf.CoreConfig) (vCore.Core, error) {
return nil, err return nil, err
} }
hs := NewHookServer(c.SingConfig.EnableConnClear) hs := NewHookServer(c.SingConfig.EnableConnClear)
b.Router().SetClashServer(hs) b.Router().SetTracker(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),
}, nil }, nil
} }

View File

@@ -7,11 +7,20 @@ 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/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/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")
}
switch p.NodeInfo.Type { switch p.NodeInfo.Type {
case "vmess", "vless": case "vmess", "vless":
if p.NodeInfo.Type == "vless" { if p.NodeInfo.Type == "vless" {
@@ -23,7 +32,7 @@ func (b *Sing) AddUsers(p *core.AddUsersParams) (added int, err error) {
UUID: p.Users[i].Uuid, UUID: p.Users[i].Uuid,
} }
} }
err = b.inbounds[p.Tag].(*inbound.VLESS).AddUsers(us) err = in.(*vless.Inbound).AddUsers(us)
} else { } else {
us := make([]option.VMessUser, len(p.Users)) us := make([]option.VMessUser, len(p.Users))
for i := range p.Users { for i := range p.Users {
@@ -32,7 +41,7 @@ func (b *Sing) AddUsers(p *core.AddUsersParams) (added int, err error) {
UUID: p.Users[i].Uuid, UUID: p.Users[i].Uuid,
} }
} }
err = b.inbounds[p.Tag].(*inbound.VMess).AddUsers(us) err = in.(*vmess.Inbound).AddUsers(us)
} }
case "shadowsocks": case "shadowsocks":
us := make([]option.ShadowsocksUser, len(p.Users)) us := make([]option.ShadowsocksUser, len(p.Users))
@@ -49,7 +58,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 +67,7 @@ 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 "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 +76,18 @@ 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)
} }
if err != nil { if err != nil {
return 0, err return 0, err
@@ -106,17 +117,17 @@ func (b *Sing) DelUsers(users []panel.UserInfo, tag string) error {
if i, ok := b.inbounds[tag]; ok { if i, ok := b.inbounds[tag]; ok {
switch i.Type() { switch i.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 "hysteria": case "hysteria":
del = i.(*inbound.Hysteria) del = i.(*hysteria.Inbound)
case "hysteria2": case "hysteria2":
del = i.(*inbound.Hysteria2) del = i.(*hysteria2.Inbound)
} }
} else { } else {
return errors.New("the inbound not found") return errors.New("the inbound not found")

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

@@ -5,6 +5,7 @@ package dispatcher
import ( import (
"context" "context"
"fmt" "fmt"
"regexp"
"strings" "strings"
"sync" "sync"
"time" "time"
@@ -14,6 +15,7 @@ import (
"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,7 +31,7 @@ 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
@@ -157,6 +159,7 @@ func (d *DefaultDispatcher) getLink(ctx context.Context, network net.Network) (*
var user *protocol.MemoryUser var user *protocol.MemoryUser
if sessionInbound != nil { if sessionInbound != nil {
user = sessionInbound.User user = sessionInbound.User
sessionInbound.CanSpliceCopy = 3
} }
var limit *limiter.Limiter var limit *limiter.Limiter
@@ -164,24 +167,25 @@ 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")
} }
if w != nil { if w != nil {
inboundLink.Writer = rate.NewRateLimitWriter(inboundLink.Writer, w) inboundLink.Writer = rate.NewRateLimitWriter(inboundLink.Writer, w)
@@ -217,8 +221,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 +242,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 +265,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 +297,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,13 +322,16 @@ 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)
@@ -329,7 +351,7 @@ 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 {
@@ -399,7 +421,8 @@ 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)
ob := outbounds[len(outbounds)-1]
if hosts, ok := d.dns.(dns.HostsLookup); ok && destination.Address.Family().IsDomain() { if hosts, ok := d.dns.(dns.HostsLookup); ok && destination.Address.Family().IsDomain() {
proxied := hosts.LookupHosts(ob.Target.String()) proxied := hosts.LookupHosts(ob.Target.String())
if proxied != nil { if proxied != nil {
@@ -426,7 +449,7 @@ func (d *DefaultDispatcher) routedDispatch(ctx context.Context, link *transport.
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 +460,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 +491,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,13 +504,13 @@ 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)) errors.LogInfo(ctx, "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)
} }
} else { } else {
newError("default route for ", destination).WriteToLog(session.ExportIDToError(ctx)) errors.LogInfo(ctx, "default route for ", destination)
} }
} }
@@ -500,7 +523,7 @@ func (d *DefaultDispatcher) routedDispatch(ctx context.Context, link *transport.
} }
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

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

@@ -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

@@ -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,15 @@ 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"
"github.com/InazumaV/V2bX/api/panel"
"github.com/goccy/go-json"
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 +63,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

@@ -26,8 +26,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 +73,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 +114,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,7 +132,7 @@ 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,
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,
@@ -123,6 +140,7 @@ func buildInbound(option *conf.Options, nodeInfo *panel.NodeInfo, tag string) (*
MaxTimeDiff: uint64(mtd.Microseconds()), MaxTimeDiff: uint64(mtd.Microseconds()),
ShortIds: []string{v.TlsSettings.ShortId}, ShortIds: []string{v.TlsSettings.ShortId},
} }
default:
break break
} }
in.Tag = tag in.Tag = tag
@@ -172,7 +190,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 +204,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 +244,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

@@ -5,7 +5,6 @@ import (
conf2 "github.com/InazumaV/V2bX/conf" conf2 "github.com/InazumaV/V2bX/conf"
"github.com/goccy/go-json" "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 +17,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

@@ -58,22 +58,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 +86,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 +115,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")
} }
} }

BIN
example/geoip.db Normal file

Binary file not shown.

BIN
example/geosite.db Normal file

Binary file not shown.

347
go.mod
View File

@@ -1,125 +1,161 @@
module github.com/InazumaV/V2bX module github.com/InazumaV/V2bX
go 1.21.4 go 1.23
toolchain go1.23.2
require ( require (
github.com/apernet/hysteria/core/v2 v2.6.0
github.com/apernet/hysteria/extras/v2 v2.6.0
github.com/apernet/quic-go v0.48.2-0.20241104191913-cb103fcecfe7
github.com/beevik/ntp v1.2.0 github.com/beevik/ntp v1.2.0
github.com/fsnotify/fsnotify v1.7.0 github.com/fsnotify/fsnotify v1.7.0
github.com/go-acme/lego/v4 v4.13.2 github.com/go-acme/lego/v4 v4.19.3-0.20241028134924-480950181787
github.com/go-resty/resty/v2 v2.7.0 github.com/go-resty/resty/v2 v2.15.3
github.com/goccy/go-json v0.10.2 github.com/goccy/go-json v0.10.3
github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/go-multierror v1.1.2-0.20240618221538-6fa16eae919d
github.com/juju/ratelimit v1.0.2 github.com/juju/ratelimit v1.0.2
github.com/sagernet/sing v0.2.18-0.20231119032432-6a556bfa50cc github.com/sagernet/sing v0.6.0-beta.6
github.com/sagernet/sing-box v1.7.0-beta.3 github.com/sagernet/sing-box v1.10.4
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.8.1
github.com/xtls/xray-core v1.8.6 github.com/spf13/viper v1.18.2
golang.org/x/crypto v0.15.0 github.com/xtls/xray-core v1.8.25-0.20241211140539-8cd9a743764a
golang.org/x/sys v0.14.0 go.uber.org/zap v1.27.0
google.golang.org/protobuf v1.31.0 golang.org/x/crypto v0.30.0
golang.org/x/sys v0.28.0
google.golang.org/protobuf v1.35.2
gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/natefinch/lumberjack.v2 v2.2.1
) )
require ( require (
berty.tech/go-libtor v1.0.385 // indirect cloud.google.com/go/auth v0.9.3 // indirect
cloud.google.com/go/compute v1.23.2 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect cloud.google.com/go/compute/metadata v0.5.1 // 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.14.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // 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.2.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.29 // 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.0 // 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.2.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/aliyun/alibaba-cloud-sdk-go v1.63.15 // indirect
github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect github.com/andybalholm/brotli v1.1.0 // indirect
github.com/andybalholm/brotli v1.0.6 // indirect github.com/aws/aws-sdk-go-v2 v1.30.5 // indirect
github.com/aws/aws-sdk-go v1.39.0 // indirect github.com/aws/aws-sdk-go-v2/config v1.27.33 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.32 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19 // indirect
github.com/aws/aws-sdk-go-v2/service/lightsail v1.40.6 // indirect
github.com/aws/aws-sdk-go-v2/service/route53 v1.43.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.22.7 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.7 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.30.7 // indirect
github.com/aws/smithy-go v1.20.4 // indirect
github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6 // 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.20.0 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/civo/civogo v0.3.11 // indirect github.com/civo/civogo v0.3.11 // indirect
github.com/cloudflare/circl v1.3.6 // indirect github.com/cloudflare/circl v1.4.0 // indirect
github.com/cloudflare/cloudflare-go v0.70.0 // indirect github.com/cloudflare/cloudflare-go v0.104.0 // indirect
github.com/cpu/goacmedns v0.1.1 // indirect github.com/cpu/goacmedns v0.1.1 // 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/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/deepmap/oapi-codegen v1.9.1 // 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/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 v1.7.0 // indirect
github.com/exoscale/egoscale v0.100.1 // indirect github.com/exoscale/egoscale/v3 v3.1.5 // 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/gabriel-vasile/mimetype v1.4.2 // 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-chi/chi/v5 v5.1.0 // indirect
github.com/go-chi/cors v1.2.1 // 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.0.4 // indirect
github.com/go-logr/logr v1.4.2 // 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.16.0 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/go-viper/mapstructure/v2 v2.1.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/gofrs/flock v0.12.1 // indirect
github.com/gofrs/uuid/v5 v5.3.0 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang-jwt/jwt/v5 v5.2.1 // 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/google/btree v1.1.3 // indirect
github.com/google/btree v1.1.2 // indirect github.com/google/go-cmp v0.6.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/pprof v0.0.0-20240528025155-186aa0362fba // indirect
github.com/google/s2a-go v0.1.4 // indirect github.com/google/s2a-go v0.1.8 // 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.4 // indirect
github.com/googleapis/gax-go/v2 v2.12.0 // indirect github.com/googleapis/gax-go/v2 v2.13.0 // indirect
github.com/gophercloud/gophercloud v1.0.0 // indirect github.com/gophercloud/gophercloud v1.14.0 // 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/errwrap v1.1.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.7 // 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/huaweicloud/huaweicloud-sdk-go-v3 v0.1.114 // indirect
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // indirect github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // 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 v1.1.1 // indirect
github.com/insomniacslk/dhcp v0.0.0-20231016090811-6a2c8fbdcc1c // indirect github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/josharian/native v1.1.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/klauspost/compress v1.17.9 // indirect
github.com/klauspost/cpuid/v2 v2.2.6 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // 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/leodido/go-urn v1.2.4 // indirect
github.com/libdns/alidns v1.0.3 // indirect github.com/libdns/alidns v1.0.3 // indirect
github.com/libdns/cloudflare v0.1.0 // indirect github.com/libdns/cloudflare v0.1.1 // indirect
github.com/libdns/libdns v0.2.1 // indirect github.com/libdns/libdns v0.2.2 // indirect
github.com/linode/linodego v1.17.2 // indirect github.com/linode/linodego v1.40.0 // indirect
github.com/liquidweb/go-lwApi v0.0.5 // 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/mattn/go-isatty v0.0.20 // indirect
github.com/mdlayher/netlink v1.7.2 // indirect
github.com/mdlayher/socket v0.4.1 // indirect
github.com/metacubex/tfo-go v0.0.0-20241006021335-daedaf0ca7aa // indirect
github.com/mholt/acmez v1.2.0 // indirect github.com/mholt/acmez v1.2.0 // indirect
github.com/miekg/dns v1.1.57 // indirect github.com/miekg/dns v1.1.62 // 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/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
@@ -127,109 +163,128 @@ require (
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 v0.0.0-20180403034216-08470befbe04 // 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-20240207213615-dde5bf4577a3 // indirect
github.com/nrdcg/desec v0.8.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.2.0 // indirect
github.com/nrdcg/goinwx v0.8.2 // indirect github.com/nrdcg/goinwx v0.10.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/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/onsi/ginkgo/v2 v2.19.0 // indirect
github.com/ooni/go-libtor v1.1.8 // indirect github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
github.com/oracle/oci-go-sdk v24.3.0+incompatible // indirect github.com/oracle/oci-go-sdk/v65 v65.73.0 // indirect
github.com/oschwald/maxminddb-golang v1.12.0 // indirect github.com/oschwald/maxminddb-golang v1.12.0 // indirect
github.com/ovh/go-ovh v1.4.1 // indirect github.com/ovh/go-ovh v1.6.0 // 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.1.0 // indirect
github.com/pires/go-proxyproto v0.7.0 // indirect github.com/pierrec/lz4/v4 v4.1.17 // indirect
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect github.com/pires/go-proxyproto v0.8.0 // 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.4.0 // indirect
github.com/quic-go/qpack v0.4.0 // indirect github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/qtls-go1-20 v0.4.1 // indirect github.com/quic-go/qtls-go1-20 v0.4.1 // indirect
github.com/quic-go/quic-go v0.40.0 // indirect github.com/quic-go/quic-go v0.46.0 // indirect
github.com/refraction-networking/utls v1.5.4 // indirect github.com/refraction-networking/utls v1.6.7 // 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.2.10 // indirect
github.com/sacloud/go-http v0.1.6 // indirect github.com/sacloud/go-http v0.1.8 // indirect
github.com/sacloud/iaas-api-go v1.11.1 // indirect github.com/sacloud/iaas-api-go v1.12.0 // indirect
github.com/sacloud/packages-go v0.0.9 // indirect github.com/sacloud/packages-go v0.0.10 // 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/cloudflare-tls v0.0.0-20231208171750-a4483c1b7cd1 // indirect
github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61 // indirect github.com/sagernet/cors v1.2.1 // indirect
github.com/sagernet/gvisor v0.0.0-20231119034329-07cfb6aaf930 // indirect github.com/sagernet/fswatch v0.1.1 // indirect
github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 // indirect github.com/sagernet/gvisor v0.0.0-20241123041152-536d05261cff // indirect
github.com/sagernet/quic-go v0.40.0 // indirect github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect
github.com/sagernet/sing-dns v0.1.11-0.20231116102430-5a2133f5d358 // indirect github.com/sagernet/nftables v0.3.0-beta.4 // indirect
github.com/sagernet/sing-mux v0.1.5-0.20231109075101-6b086ed6bb07 // indirect github.com/sagernet/quic-go v0.48.2-beta.1 // indirect
github.com/sagernet/sing-quic v0.1.4-0.20231114135334-e2a6aab55cca // indirect github.com/sagernet/sing-dns v0.4.0-beta.1 // indirect
github.com/sagernet/sing-shadowsocks v0.2.5 // indirect github.com/sagernet/sing-mux v0.3.0-alpha.1 // indirect
github.com/sagernet/sing-shadowsocks2 v0.1.4 // indirect github.com/sagernet/sing-quic v0.4.0-alpha.4 // indirect
github.com/sagernet/sing-shadowtls v0.1.4 // indirect github.com/sagernet/sing-shadowsocks v0.2.7 // indirect
github.com/sagernet/sing-tun v0.1.20-0.20231119035513-f6ea97c5af71 // indirect github.com/sagernet/sing-shadowsocks2 v0.2.0 // indirect
github.com/sagernet/sing-vmess v0.1.8 // indirect github.com/sagernet/sing-shadowtls v0.2.0-alpha.2 // indirect
github.com/sagernet/smux v0.0.0-20230312102458-337ec2a5af37 // indirect github.com/sagernet/sing-tun v0.6.0-beta.2 // indirect
github.com/sagernet/tfo-go v0.0.0-20230816093905-5a5c285d44a6 // indirect github.com/sagernet/sing-vmess v0.2.0-beta.1 // indirect
github.com/sagernet/utls v0.0.0-20230309024959-6732c2ab36f2 // indirect github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 // indirect
github.com/sagernet/wireguard-go v0.0.0-20230807125731-5d4a7ef2dc5f // indirect github.com/sagernet/utls v1.6.7 // indirect
github.com/sagernet/ws v0.0.0-20231030053741-7d481eb31bed // indirect github.com/sagernet/wireguard-go v0.0.1-beta.5 // indirect
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.17 // indirect github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 // indirect
github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/simplesurance/bunny-go v0.0.0-20221115111006-e11d9dc91f04 // indirect github.com/scaleway/scaleway-sdk-go v1.0.0-beta.30 // indirect
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 // indirect
github.com/selectel/domains-go v1.1.0 // indirect
github.com/selectel/go-selvpcclient/v3 v3.1.1 // indirect
github.com/shopspring/decimal v1.3.1 // 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.5 // 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 v0.5.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/objx v0.5.0 // indirect github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/testify v1.8.4 // indirect github.com/stretchr/testify v1.10.0 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.490 // indirect github.com/subosito/gotenv v1.6.0 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.490 // indirect github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1002 // indirect
github.com/transip/gotransip/v6 v6.20.0 // indirect github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1002 // 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-20230220225925-ffce2a382923 // indirect github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect
github.com/ultradns/ultradns-go-sdk v1.5.0-20230427130837-23c9b0c // indirect github.com/ultradns/ultradns-go-sdk v1.7.0-20240913052650-970ca9a // 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.0 // indirect
github.com/vishvananda/netns v0.0.4 // indirect github.com/vishvananda/netns v0.0.4 // indirect
github.com/vultr/govultr/v2 v2.17.2 // indirect github.com/volcengine/volc-sdk-golang v1.0.177 // indirect
github.com/xtls/reality v0.0.0-20231112171332-de1173cf2b19 // indirect github.com/vultr/govultr/v3 v3.9.1 // indirect
github.com/yandex-cloud/go-genproto v0.0.0-20220805142335-27b56ddae16f // indirect github.com/wyx2685/sing-vmess v0.0.0-20241213093025-7de21a950272 // indirect
github.com/yandex-cloud/go-sdk v0.0.0-20220805164847-cf028e604997 // indirect github.com/xtls/reality v0.0.0-20240909153216-e26ae2305463 // indirect
github.com/yandex-cloud/go-genproto v0.0.0-20240911120709-1fa0cb6f47c2 // indirect
github.com/yandex-cloud/go-sdk v0.0.0-20240911121212-e4e74d0d02f5 // indirect
github.com/zeebo/blake3 v0.2.3 // indirect github.com/zeebo/blake3 v0.2.3 // indirect
go.mongodb.org/mongo-driver v1.12.0 // indirect
go.opencensus.io v0.24.0 // indirect go.opencensus.io v0.24.0 // indirect
go.uber.org/mock v0.3.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect
go.opentelemetry.io/otel v1.29.0 // indirect
go.opentelemetry.io/otel/metric v1.29.0 // indirect
go.opentelemetry.io/otel/trace v1.29.0 // indirect
go.uber.org/mock v0.4.0 // 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.0 // indirect
go.uber.org/zap v1.26.0 // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
go4.org/netipx v0.0.0-20230824141953-6213f710f925 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect golang.org/x/mod v0.21.0 // indirect
golang.org/x/mod v0.14.0 // indirect golang.org/x/net v0.32.0 // indirect
golang.org/x/net v0.18.0 // indirect golang.org/x/oauth2 v0.23.0 // indirect
golang.org/x/oauth2 v0.11.0 // indirect golang.org/x/sync v0.10.0 // indirect
golang.org/x/text v0.14.0 // indirect golang.org/x/text v0.21.0 // indirect
golang.org/x/time v0.4.0 // indirect golang.org/x/time v0.7.0 // indirect
golang.org/x/tools v0.15.0 // indirect golang.org/x/tools v0.25.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 google.golang.org/api v0.197.0 // indirect
google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 // indirect
google.golang.org/genproto v0.0.0-20231030173426-d783a09b4405 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 // indirect google.golang.org/grpc v1.67.1 // 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.12.0 // 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-20231202080848-1f7806d17489 // indirect
lukechampine.com/blake3 v1.2.1 // indirect lukechampine.com/blake3 v1.3.0 // indirect
) )
replace ( //replace github.com/sagernet/sing-box v1.10.4 => /root/sing-box_mod
github.com/sagernet/sing-box v1.7.0-beta.3 => github.com/wyx2685/sing-box_mod v0.0.0-20231119104152-0fdf5b5ac793
github.com/sagernet/sing-shadowsocks v0.2.4 => github.com/inazumav/sing-shadowsocks v0.0.0-20230815111927-7c68cbaeec5c replace github.com/sagernet/sing-box v1.10.4 => github.com/wyx2685/sing-box_mod v0.0.0-20241213112838-c7faac61b3fe
)

1310
go.sum

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,7 @@ package limiter
import ( import (
"errors" "errors"
"regexp" "regexp"
"strings"
"sync" "sync"
"time" "time"
@@ -20,13 +21,13 @@ var limiter map[string]*Limiter
func Init() { func Init() {
limiter = map[string]*Limiter{} limiter = map[string]*Limiter{}
c := task.Periodic{ c := task.Periodic{
Interval: time.Minute * 2, Interval: time.Minute * 3,
Execute: ClearOnlineIP, Execute: ClearOnlineIP,
} }
go func() { go func() {
log.WithField("Type", "Limiter"). log.WithField("Type", "Limiter").
Debug("ClearOnlineIP started") Debug("ClearOnlineIP started")
time.Sleep(time.Minute * 2) time.Sleep(time.Minute * 3)
_ = c.Start() _ = c.Start()
}() }()
} }
@@ -36,37 +37,46 @@ type Limiter struct {
ProtocolRules []string ProtocolRules []string
SpeedLimit int SpeedLimit int
UserOnlineIP *sync.Map // Key: Name, value: {Key: Ip, value: Uid} UserOnlineIP *sync.Map // Key: Name, value: {Key: Ip, value: Uid}
UUIDtoUID map[string]int // Key: UUID, value: UID OldUserOnline *sync.Map // Key: Ip, value: Uid
UUIDtoUID map[string]int // Key: UUID, value: Uid
UserLimitInfo *sync.Map // Key: Uid value: UserLimitInfo UserLimitInfo *sync.Map // Key: Uid value: UserLimitInfo
ConnLimiter *ConnLimiter // Key: Uid value: ConnLimiter ConnLimiter *ConnLimiter // Key: Uid value: ConnLimiter
SpeedLimiter *sync.Map // key: Uid, value: *ratelimit.Bucket SpeedLimiter *sync.Map // key: Uid, 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), 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 +104,23 @@ 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))
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 +136,73 @@ 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) {
// check if ipv4 mapped ipv6
ip = strings.TrimPrefix(ip, "::ffff:")
// ip and conn limiter // ip and conn limiter
if l.ConnLimiter.AddConnCount(email, ip, isTcp) { if l.ConnLimiter.AddConnCount(taguuid, ip, isTcp) {
return nil, true 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)
} }
} }
if noSSUDP {
// Store online user for device limit // Store online user for device limit
ipMap := new(sync.Map) ipMap := new(sync.Map)
uid := l.UUIDtoUID[email] ipMap.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, ok := l.UserOnlineIP.LoadOrStore(taguuid, ipMap); ok {
ipMap := v.(*sync.Map) ipMap := v.(*sync.Map)
// If this is a new ip // If this is a new ip
if _, ok := ipMap.LoadOrStore(ip, uid); !ok { if _, ok := ipMap.LoadOrStore(ip, uid); !ok {
counter := 0 if deviceLimit > 0 {
ipMap.Range(func(key, value interface{}) bool { if deviceLimit <= aliveIp {
counter++ ipMap.Delete(ip)
return true 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 +212,17 @@ 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.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 +233,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

@@ -262,12 +262,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
@@ -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 {
@@ -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

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"
@@ -15,11 +14,7 @@ func (c *Controller) reportUserTrafficTask() (err error) {
up, down := c.server.GetUserTraffic(c.tag, c.userList[i].Uuid, true) up, down := c.server.GetUserTraffic(c.tag, c.userList[i].Uuid, true)
if up > 0 || down > 0 { if up > 0 || down > 0 {
if c.LimitConfig.EnableDynamicSpeedLimit { if c.LimitConfig.EnableDynamicSpeedLimit {
if _, ok := c.traffic[c.userList[i].Uuid]; ok { c.traffic[c.userList[i].Uuid] += up + down
c.traffic[c.userList[i].Uuid] += up + down
} else {
c.traffic[c.userList[i].Uuid] = up + down
}
} }
userTraffic = append(userTraffic, panel.UserTraffic{ userTraffic = append(userTraffic, panel.UserTraffic{
UID: (c.userList)[i].Id, UID: (c.userList)[i].Id,
@@ -56,18 +51,12 @@ 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,
@@ -82,29 +71,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
} }