mirror of
https://github.com/wyx2685/V2bX.git
synced 2026-02-06 13:40:10 +00:00
Compare commits
129 Commits
v0.0.0-202
...
v0.3.9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9bbccf772b | ||
|
|
9483526f3c | ||
|
|
65f2de55ea | ||
|
|
4ed6bc6d87 | ||
|
|
af7b473c13 | ||
|
|
10f66b57ea | ||
|
|
0824bf7a4e | ||
|
|
6e8297c553 | ||
|
|
7d52a8932d | ||
|
|
06441afa79 | ||
|
|
f7b588fb45 | ||
|
|
9be082ede6 | ||
|
|
dce3ec1079 | ||
|
|
2990999f7b | ||
|
|
fe003fcb19 | ||
|
|
ea26985d7c | ||
|
|
32437f5e48 | ||
|
|
39dfd8b6dd | ||
|
|
eb51d3e13c | ||
|
|
dadeb6304b | ||
|
|
4c7b9f5eb9 | ||
|
|
63d88843b6 | ||
|
|
8d225f811b | ||
|
|
f6b587b275 | ||
|
|
d9b3d24465 | ||
|
|
fc284b3b9f | ||
|
|
eb92c4912d | ||
|
|
a68378670f | ||
|
|
d200a3336e | ||
|
|
6a95d576f1 | ||
|
|
dbe529bd48 | ||
|
|
8254e49297 | ||
|
|
9e8ad2619a | ||
|
|
d5fff6c433 | ||
|
|
fe896a61a3 | ||
|
|
a63198c20b | ||
|
|
180fb14dd1 | ||
|
|
44db7512d7 | ||
|
|
95263fea99 | ||
|
|
0b155bbf89 | ||
|
|
1c8c17b067 | ||
|
|
61606646b9 | ||
|
|
e2904ad126 | ||
|
|
903aef1fb5 | ||
|
|
2c43704090 | ||
|
|
d71df3a0df | ||
|
|
96baa0a99c | ||
|
|
e502624fe4 | ||
|
|
484faaf0c3 | ||
|
|
ec5dcc3c8a | ||
|
|
2f1362067b | ||
|
|
c755e9800b | ||
|
|
29e0d7e56e | ||
|
|
08ebbed9fb | ||
|
|
792841d073 | ||
|
|
981e59b836 | ||
|
|
7dbe5fda85 | ||
|
|
4f9ccdf8db | ||
|
|
c2d5861d7d | ||
|
|
0e29c19f0e | ||
|
|
a1c40bb1c8 | ||
|
|
a0de94efff | ||
|
|
29928a1135 | ||
|
|
ab1ca837de | ||
|
|
1f61446fa9 | ||
|
|
c0325227db | ||
|
|
ba3036a7ac | ||
|
|
f99e2b4489 | ||
|
|
0af952be10 | ||
|
|
ad5971f164 | ||
|
|
f7d5d891c3 | ||
|
|
6936a76724 | ||
|
|
7184e49650 | ||
|
|
ea0b7d8f40 | ||
|
|
12fbcb1460 | ||
|
|
c6d48e1edf | ||
|
|
8d7168c6a4 | ||
|
|
173c48a76f | ||
|
|
130e94cf45 | ||
|
|
89ddfff060 | ||
|
|
07d49293d8 | ||
|
|
9e8f87740e | ||
|
|
29a99985c8 | ||
|
|
248ff3764f | ||
|
|
3dfeba7e68 | ||
|
|
8eb623b3f0 | ||
|
|
cdcbddd464 | ||
|
|
e81d47321b | ||
|
|
4d82eff518 | ||
|
|
b96545649b | ||
|
|
8757b955a6 | ||
|
|
33d9ab4b0a | ||
|
|
a7637d436f | ||
|
|
4dda65a636 | ||
|
|
6c725b424f | ||
|
|
a052a1f1e8 | ||
|
|
85ad40d098 | ||
|
|
a85352c402 | ||
|
|
206af0216c | ||
|
|
ed5edda28a | ||
|
|
995e606694 | ||
|
|
96b457d679 | ||
|
|
55b20f5550 | ||
|
|
c0b31837e4 | ||
|
|
bf4a52df4d | ||
|
|
423ac622b5 | ||
|
|
625265148f | ||
|
|
73f9b19222 | ||
|
|
0d274bcf61 | ||
|
|
77ec03016c | ||
|
|
1d4945af8d | ||
|
|
f92c5b37d5 | ||
|
|
91e78fbc20 | ||
|
|
e292b3b0e7 | ||
|
|
c4d404d979 | ||
|
|
16221d17fb | ||
|
|
f33d7a3bf9 | ||
|
|
50183e70b3 | ||
|
|
d19ca6863e | ||
|
|
8d116b19d1 | ||
|
|
4ec6426234 | ||
|
|
73b5c37d94 | ||
|
|
e7997f9896 | ||
|
|
1f6cccbb9f | ||
|
|
a25c3d2759 | ||
|
|
5aef9cf0de | ||
|
|
39972f5cf9 | ||
|
|
0af24e4646 | ||
|
|
ea6ef41c60 |
22
.github/ISSUE_TEMPLATE/bug-report.md
vendored
22
.github/ISSUE_TEMPLATE/bug-report.md
vendored
@@ -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不会得到答复并且会被直接关闭
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
**额外的内容**
|
**额外的内容**
|
||||||
在这里添加关于问题的任何其他内容
|
在这里添加关于问题的任何其他内容
|
||||||
19
.github/ISSUE_TEMPLATE/feature-request.md
vendored
19
.github/ISSUE_TEMPLATE/feature-request.md
vendored
@@ -1,19 +0,0 @@
|
|||||||
---
|
|
||||||
name: "功能建议"
|
|
||||||
about: 给XrayR提出建议,让我们做得更好
|
|
||||||
title: ''
|
|
||||||
labels: awaiting reply, feature-request
|
|
||||||
assignees: ''
|
|
||||||
---
|
|
||||||
|
|
||||||
**描述您想要的功能**
|
|
||||||
|
|
||||||
清晰简洁的功能描述。
|
|
||||||
|
|
||||||
**描述您考虑过的替代方案**
|
|
||||||
|
|
||||||
是否有任何替代方案可以解决这个问题?
|
|
||||||
|
|
||||||
**附加上下文**
|
|
||||||
|
|
||||||
在此处添加有关功能请求的任何其他上下文或截图。
|
|
||||||
113
.github/workflows/Publish Docker image.yml
vendored
Normal file
113
.github/workflows/Publish Docker image.yml
vendored
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
name: Publish Docker image
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- 'dev_new'
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: ghcr.io
|
||||||
|
IMAGE_NAME: ${{ github.repository_owner }}/v2bx
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
platform:
|
||||||
|
- linux/amd64
|
||||||
|
- linux/arm64
|
||||||
|
steps:
|
||||||
|
- name: Prepare
|
||||||
|
run: |
|
||||||
|
platform=${{ matrix.platform }}
|
||||||
|
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Docker meta
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Login to GitHub Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push by digest
|
||||||
|
id: build
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
platforms: ${{ matrix.platform }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
|
||||||
|
|
||||||
|
- name: Export digest
|
||||||
|
run: |
|
||||||
|
mkdir -p /tmp/digests
|
||||||
|
digest="${{ steps.build.outputs.digest }}"
|
||||||
|
echo "${digest#sha256:}" > "/tmp/digests/${digest#sha256:}"
|
||||||
|
|
||||||
|
- name: Upload digest
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: digests-${{ env.PLATFORM_PAIR }}
|
||||||
|
path: /tmp/digests/*
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
merge:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- build
|
||||||
|
steps:
|
||||||
|
- name: Download digests
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: /tmp/digests
|
||||||
|
pattern: digests-*
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Docker meta
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
|
||||||
|
- name: Login to GitHub Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Create manifest list and push
|
||||||
|
run: |
|
||||||
|
cd /tmp/digests
|
||||||
|
tags=$(echo '${{ steps.meta.outputs.json }}' | jq -cr '.tags | map("-t " + .) | join(" ")')
|
||||||
|
images=$(printf "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s " $(find . -type f -exec cat {} \;))
|
||||||
|
echo "Creating manifest with tags: $tags"
|
||||||
|
echo "Using images: $images"
|
||||||
|
docker buildx imagetools create $tags $images
|
||||||
|
|
||||||
|
- name: Inspect image
|
||||||
|
run: |
|
||||||
|
docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }}
|
||||||
54
.github/workflows/codeql-analysis.yml
vendored
54
.github/workflows/codeql-analysis.yml
vendored
@@ -34,34 +34,38 @@ jobs:
|
|||||||
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v1
|
uses: github/codeql-action/init@v2
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
# By default, queries listed here will override any specified in a config file.
|
# By default, queries listed here will override any specified in a config file.
|
||||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||||
|
|
||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
- name: Install Go
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
uses: actions/setup-go@v4
|
||||||
- name: Autobuild
|
with:
|
||||||
uses: github/codeql-action/autobuild@v1
|
go-version-file: go.mod
|
||||||
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
|
- name: Autobuild
|
||||||
|
uses: github/codeql-action/autobuild@v2
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# 📚 https://git.io/JvXDl
|
# 📚 https://git.io/JvXDl
|
||||||
|
|
||||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||||
# and modify them (or add more) to build your code if your project
|
# and modify them (or add more) to build your code if your project
|
||||||
# uses a compiled language
|
# uses a compiled language
|
||||||
|
|
||||||
#- run: |
|
#- run: |
|
||||||
# make bootstrap
|
# make bootstrap
|
||||||
# make release
|
# make release
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v1
|
uses: github/codeql-action/analyze@v2
|
||||||
33
.github/workflows/release.yml
vendored
33
.github/workflows/release.yml
vendored
@@ -5,18 +5,19 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
|
- dev_new
|
||||||
paths:
|
paths:
|
||||||
- "**/*.go"
|
- "**/*.go"
|
||||||
- "go.mod"
|
- "go.mod"
|
||||||
- "go.sum"
|
- "go.sum"
|
||||||
- ".github/workflows/*.yml"
|
- ".github/workflows/release.yml"
|
||||||
pull_request:
|
pull_request:
|
||||||
types: [opened, synchronize, reopened]
|
types: [opened, synchronize, reopened]
|
||||||
paths:
|
paths:
|
||||||
- "**/*.go"
|
- "**/*.go"
|
||||||
- "go.mod"
|
- "go.mod"
|
||||||
- "go.sum"
|
- "go.sum"
|
||||||
- ".github/workflows/*.yml"
|
- ".github/workflows/release.yml"
|
||||||
release:
|
release:
|
||||||
types: [published]
|
types: [published]
|
||||||
|
|
||||||
@@ -97,7 +98,7 @@ jobs:
|
|||||||
CGO_ENABLED: 0
|
CGO_ENABLED: 0
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout codebase
|
- name: Checkout codebase
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
- name: Show workflow information
|
- name: Show workflow information
|
||||||
id: get_filename
|
id: get_filename
|
||||||
run: |
|
run: |
|
||||||
@@ -106,12 +107,13 @@ jobs:
|
|||||||
echo "ASSET_NAME=$_NAME" >> $GITHUB_OUTPUT
|
echo "ASSET_NAME=$_NAME" >> $GITHUB_OUTPUT
|
||||||
echo "ASSET_NAME=$_NAME" >> $GITHUB_ENV
|
echo "ASSET_NAME=$_NAME" >> $GITHUB_ENV
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v3
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: '1.21.4'
|
go-version: '1.25.0'
|
||||||
|
|
||||||
- name: Get project dependencies
|
- name: Get project dependencies
|
||||||
run: go mod download
|
run: |
|
||||||
|
go mod download
|
||||||
- name: Get release version
|
- name: Get release version
|
||||||
if: ${{ github.event_name == 'release' }}
|
if: ${{ github.event_name == 'release' }}
|
||||||
run: |
|
run: |
|
||||||
@@ -124,13 +126,13 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo "version: $version"
|
echo "version: $version"
|
||||||
mkdir -p build_assets
|
mkdir -p build_assets
|
||||||
go build -v -o build_assets/V2bX -tags "sing xray with_reality_server with_quic with_grpc with_utls with_wireguard with_acme" -trimpath -ldflags "-X 'github.com/InazumaV/V2bX/cmd.version=$version' -s -w -buildid="
|
GOEXPERIMENT=jsonv2 go build -v -o build_assets/V2bX -tags "sing xray hysteria2 with_quic with_grpc with_utls with_wireguard with_acme with_gvisor" -trimpath -ldflags "-X 'github.com/InazumaV/V2bX/cmd.version=$version' -s -w -buildid="
|
||||||
|
|
||||||
- name: Build Mips softfloat V2bX
|
- name: Build Mips softfloat V2bX
|
||||||
if: matrix.goarch == 'mips' || matrix.goarch == 'mipsle'
|
if: matrix.goarch == 'mips' || matrix.goarch == 'mipsle'
|
||||||
run: |
|
run: |
|
||||||
echo "version: $version"
|
echo "version: $version"
|
||||||
GOMIPS=softfloat go build -v -o build_assets/V2bX_softfloat -tags "sing xray with_reality_server with_quic with_grpc with_utls with_wireguard with_acme" -trimpath -ldflags "-X 'github.com/InazumaV/V2bX/cmd.version=$version' -s -w -buildid="
|
GOEXPERIMENT=jsonv2 GOMIPS=softfloat go build -v -o build_assets/V2bX_softfloat -tags "sing xray hysteria2 with_quic with_grpc with_utls with_wireguard with_acme with_gvisor" -trimpath -ldflags "-X 'github.com/InazumaV/V2bX/cmd.version=$version' -s -w -buildid="
|
||||||
- name: Rename Windows V2bX
|
- name: Rename Windows V2bX
|
||||||
if: matrix.goos == 'windows'
|
if: matrix.goos == 'windows'
|
||||||
run: |
|
run: |
|
||||||
@@ -141,16 +143,13 @@ jobs:
|
|||||||
cp ${GITHUB_WORKSPACE}/README.md ./build_assets/README.md
|
cp ${GITHUB_WORKSPACE}/README.md ./build_assets/README.md
|
||||||
cp ${GITHUB_WORKSPACE}/LICENSE ./build_assets/LICENSE
|
cp ${GITHUB_WORKSPACE}/LICENSE ./build_assets/LICENSE
|
||||||
cp ${GITHUB_WORKSPACE}/example/*.json ./build_assets/
|
cp ${GITHUB_WORKSPACE}/example/*.json ./build_assets/
|
||||||
LIST=('geoip geoip geoip' 'domain-list-community dlc geosite')
|
LIST=('geoip' 'geosite')
|
||||||
for i in "${LIST[@]}"
|
for i in "${LIST[@]}"
|
||||||
do
|
do
|
||||||
INFO=($(echo $i | awk 'BEGIN{FS=" ";OFS=" "} {print $1,$2,$3}'))
|
DOWNLOAD_URL="https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/${i}.dat"
|
||||||
FILE_NAME="${INFO[2]}.dat"
|
FILE_NAME="${i}.dat"
|
||||||
echo -e "Downloading ${FILE_NAME}..."
|
echo -e "Downloading ${DOWNLOAD_URL}..."
|
||||||
curl -L "https://github.com/v2fly/${INFO[0]}/releases/latest/download/${INFO[1]}.dat" -o ./build_assets/${FILE_NAME}
|
curl -L "${DOWNLOAD_URL}" -o ./build_assets/${FILE_NAME}
|
||||||
echo -e "Verifying HASH key..."
|
|
||||||
HASH="$(curl -sL "https://github.com/v2fly/${INFO[0]}/releases/latest/download/${INFO[1]}.dat.sha256sum" | awk -F ' ' '{print $1}')"
|
|
||||||
[ "$(sha256sum "./build_assets/${FILE_NAME}" | awk -F ' ' '{print $1}')" == "${HASH}" ] || { echo -e "The HASH key of ${FILE_NAME} does not match cloud one."; exit 1; }
|
|
||||||
done
|
done
|
||||||
- name: Create ZIP archive
|
- name: Create ZIP archive
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -169,7 +168,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
mv build_assets V2bX-$ASSET_NAME
|
mv build_assets V2bX-$ASSET_NAME
|
||||||
- name: Upload files to Artifacts
|
- name: Upload files to Artifacts
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: V2bX-${{ steps.get_filename.outputs.ASSET_NAME }}
|
name: V2bX-${{ steps.get_filename.outputs.ASSET_NAME }}
|
||||||
path: |
|
path: |
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -12,4 +12,6 @@ app/legocmd/.lego/
|
|||||||
example/.lego
|
example/.lego
|
||||||
example/cert
|
example/cert
|
||||||
./vscode
|
./vscode
|
||||||
|
output/*
|
||||||
.idea/*
|
.idea/*
|
||||||
|
newV2bX.sh
|
||||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "sing-box_mod"]
|
||||||
|
path = sing-box_mod
|
||||||
|
url = https://github.com/wyx2685/sing-box_mod.git
|
||||||
17
Dockerfile
Normal file
17
Dockerfile
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Build go
|
||||||
|
FROM golang:1.25.0-alpine AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY . .
|
||||||
|
ENV CGO_ENABLED=0
|
||||||
|
RUN GOEXPERIMENT=jsonv2 go mod download
|
||||||
|
RUN GOEXPERIMENT=jsonv2 go build -v -o V2bX -tags "sing xray hysteria2 with_quic with_grpc with_utls with_wireguard with_acme with_gvisor"
|
||||||
|
|
||||||
|
# Release
|
||||||
|
FROM alpine
|
||||||
|
# 安装必要的工具包
|
||||||
|
RUN apk --update --no-cache add tzdata ca-certificates \
|
||||||
|
&& cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
|
||||||
|
RUN mkdir /etc/V2bX/
|
||||||
|
COPY --from=builder /app/V2bX /usr/local/bin
|
||||||
|
|
||||||
|
ENTRYPOINT [ "V2bX", "server", "--config", "/etc/V2bX/config.json"]
|
||||||
28
README.md
28
README.md
@@ -1,16 +1,17 @@
|
|||||||
# V2bX
|
# V2bX
|
||||||
|
|
||||||
[](https://t.me/YuzukiProjects)
|
[](https://t.me/unofficialV2board)
|
||||||
|
[](https://t.me/YuzukiProjects)
|
||||||
|
|
||||||
A V2board node server based on multi core, modified from XrayR.
|
A V2board node server based on multi core, modified from XrayR.
|
||||||
一个基于多种内核的V2board节点服务端,修改自XrayR,支持V2ay,Trojan,Shadowsocks协议。
|
一个基于多种内核的V2board节点服务端,修改自XrayR,支持V2ay,Trojan,Shadowsocks协议。
|
||||||
|
|
||||||
**注意: 本项目需要V2board版本 >= 1.7.0**
|
**注意: 本项目需要搭配[修改版V2board](https://github.com/wyx2685/v2board)**
|
||||||
|
|
||||||
## 特点
|
## 特点
|
||||||
|
|
||||||
* 永久开源且免费。
|
* 永久开源且免费。
|
||||||
* 支持Vmess/Vless, Trojan, Shadowsocks, Hysteria多种协议。
|
* 支持Vmess/Vless, Trojan, Shadowsocks, Hysteria1/2多种协议。
|
||||||
* 支持Vless和XTLS等新特性。
|
* 支持Vless和XTLS等新特性。
|
||||||
* 支持单实例对接多节点,无需重复启动。
|
* 支持单实例对接多节点,无需重复启动。
|
||||||
* 支持限制在线IP。
|
* 支持限制在线IP。
|
||||||
@@ -23,7 +24,7 @@ A V2board node server based on multi core, modified from XrayR.
|
|||||||
|
|
||||||
## 功能介绍
|
## 功能介绍
|
||||||
|
|
||||||
| 功能 | v2ray | trojan | shadowsocks | hysteria |
|
| 功能 | v2ray | trojan | shadowsocks | hysteria1/2 |
|
||||||
|-----------|-------|--------|-------------|----------|
|
|-----------|-------|--------|-------------|----------|
|
||||||
| 自动申请tls证书 | √ | √ | √ | √ |
|
| 自动申请tls证书 | √ | √ | √ | √ |
|
||||||
| 自动续签tls证书 | √ | √ | √ | √ |
|
| 自动续签tls证书 | √ | √ | √ | √ |
|
||||||
@@ -32,14 +33,13 @@ A V2board node server based on multi core, modified from XrayR.
|
|||||||
| 自定义DNS | √ | √ | √ | √ |
|
| 自定义DNS | √ | √ | √ | √ |
|
||||||
| 在线IP数限制 | √ | √ | √ | √ |
|
| 在线IP数限制 | √ | √ | √ | √ |
|
||||||
| 连接数限制 | √ | √ | √ | √ |
|
| 连接数限制 | √ | √ | √ | √ |
|
||||||
| 跨节点IP数限制 | | | | |
|
| 跨节点IP数限制 |√ |√ |√ |√ |
|
||||||
| 按照用户限速 | √ | √ | √ | √ |
|
| 按照用户限速 | √ | √ | √ | √ |
|
||||||
| 动态限速(未测试) | √ | √ | √ | √ |
|
| 动态限速(未测试) | √ | √ | √ | √ |
|
||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
|
|
||||||
- [ ] 重新实现动态限速
|
- [ ] 重新实现动态限速
|
||||||
- [ ] 重新实现在线IP同步(跨节点在线IP限制)
|
|
||||||
- [ ] 完善使用文档
|
- [ ] 完善使用文档
|
||||||
|
|
||||||
## 软件安装
|
## 软件安装
|
||||||
@@ -47,22 +47,22 @@ A V2board node server based on multi core, modified from XrayR.
|
|||||||
### 一键安装
|
### 一键安装
|
||||||
|
|
||||||
```
|
```
|
||||||
wget -N https://raw.githubusercontents.com/wyx2685/V2bX-script/master/install.sh && bash install.sh
|
wget -N https://raw.githubusercontent.com/wyx2685/V2bX-script/master/install.sh && bash install.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
### 手动安装
|
### 手动安装
|
||||||
|
|
||||||
[手动安装教程(过时待更新)](https://yuzuki-1.gitbook.io/v2bx-doc/xrayr-xia-zai-he-an-zhuang/install/manual)
|
[手动安装教程](https://v2bx.v-50.me/v2bx/v2bx-xia-zai-he-an-zhuang/install/manual)
|
||||||
|
|
||||||
## 构建
|
## 构建
|
||||||
``` bash
|
``` bash
|
||||||
# 通过-tags选项指定要编译的内核, 可选 xray, sing
|
# 通过-tags选项指定要编译的内核, 可选 xray, sing, hysteria2
|
||||||
go build -o V2bX -ldflags '-s -w' -gcflags="all=-trimpath=${PWD}" -asmflags="all=-trimpath=${PWD} -tags "xray sing"
|
GOEXPERIMENT=jsonv2 go build -v -o build_assets/V2bX -tags "sing xray hysteria2 with_quic with_grpc with_utls with_wireguard with_acme with_gvisor" -trimpath -ldflags "-X 'github.com/InazumaV/V2bX/cmd.version=$version' -s -w -buildid="
|
||||||
```
|
```
|
||||||
|
|
||||||
## 配置文件及详细使用教程
|
## 配置文件及详细使用教程
|
||||||
|
|
||||||
[详细使用教程](https://yuzuki-1.gitbook.io/v2bx-doc/)
|
[详细使用教程](https://v2bx.v-50.me/)
|
||||||
|
|
||||||
## 免责声明
|
## 免责声明
|
||||||
|
|
||||||
@@ -71,6 +71,10 @@ go build -o V2bX -ldflags '-s -w' -gcflags="all=-trimpath=${PWD}" -asmflags="all
|
|||||||
* 本人不对任何人使用本项目造成的任何后果承担责任。
|
* 本人不对任何人使用本项目造成的任何后果承担责任。
|
||||||
* 本人比较多变,因此本项目可能会随想法或思路的变动随性更改项目结构或大规模重构代码,若不能接受请勿使用。
|
* 本人比较多变,因此本项目可能会随想法或思路的变动随性更改项目结构或大规模重构代码,若不能接受请勿使用。
|
||||||
|
|
||||||
|
## 赞助
|
||||||
|
|
||||||
|
[赞助链接](https://v-50.me/)
|
||||||
|
|
||||||
## Thanks
|
## Thanks
|
||||||
|
|
||||||
* [Project X](https://github.com/XTLS/)
|
* [Project X](https://github.com/XTLS/)
|
||||||
@@ -82,4 +86,4 @@ go build -o V2bX -ldflags '-s -w' -gcflags="all=-trimpath=${PWD}" -asmflags="all
|
|||||||
|
|
||||||
## Stars 增长记录
|
## Stars 增长记录
|
||||||
|
|
||||||
[](https://starchart.cc/InazumaV/V2bX)
|
[](https://starchart.cc/wyx2685/V2bX)
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
package panel
|
package panel
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/InazumaV/V2bX/common/crypt"
|
"encoding/json"
|
||||||
"github.com/goccy/go-json"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Security type
|
// Security type
|
||||||
@@ -32,6 +32,8 @@ type NodeInfo struct {
|
|||||||
VAllss *VAllssNode
|
VAllss *VAllssNode
|
||||||
Shadowsocks *ShadowsocksNode
|
Shadowsocks *ShadowsocksNode
|
||||||
Trojan *TrojanNode
|
Trojan *TrojanNode
|
||||||
|
Tuic *TuicNode
|
||||||
|
AnyTls *AnyTlsNode
|
||||||
Hysteria *HysteriaNode
|
Hysteria *HysteriaNode
|
||||||
Hysteria2 *Hysteria2Node
|
Hysteria2 *Hysteria2Node
|
||||||
Common *CommonNode
|
Common *CommonNode
|
||||||
@@ -65,6 +67,8 @@ type VAllssNode struct {
|
|||||||
Network string `json:"network"`
|
Network string `json:"network"`
|
||||||
NetworkSettings json.RawMessage `json:"network_settings"`
|
NetworkSettings json.RawMessage `json:"network_settings"`
|
||||||
NetworkSettingsBack json.RawMessage `json:"networkSettings"`
|
NetworkSettingsBack json.RawMessage `json:"networkSettings"`
|
||||||
|
Encryption string `json:"encryption"`
|
||||||
|
EncryptionSettings EncSettings `json:"encryption_settings"`
|
||||||
ServerName string `json:"server_name"`
|
ServerName string `json:"server_name"`
|
||||||
|
|
||||||
// vless only
|
// vless only
|
||||||
@@ -73,12 +77,20 @@ type VAllssNode struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type TlsSettings struct {
|
type TlsSettings struct {
|
||||||
ServerName string `json:"server_name"`
|
ServerName string `json:"server_name"`
|
||||||
Dest string `json:"dest"`
|
Dest string `json:"dest"`
|
||||||
ServerPort string `json:"server_port"`
|
ServerPort string `json:"server_port"`
|
||||||
ShortId string `json:"short_id"`
|
ShortId string `json:"short_id"`
|
||||||
PrivateKey string `json:"private_key"`
|
PrivateKey string `json:"private_key"`
|
||||||
Xver uint8 `json:"xver,string"`
|
Mldsa65Seed string `json:"mldsa65Seed"`
|
||||||
|
Xver uint64 `json:"xver,string"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type EncSettings struct {
|
||||||
|
Mode string `json:"mode"`
|
||||||
|
Ticket string `json:"ticket"`
|
||||||
|
ServerPadding string `json:"server_padding"`
|
||||||
|
PrivateKey string `json:"private_key"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RealityConfig struct {
|
type RealityConfig struct {
|
||||||
@@ -94,7 +106,22 @@ type ShadowsocksNode struct {
|
|||||||
ServerKey string `json:"server_key"`
|
ServerKey string `json:"server_key"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TrojanNode CommonNode
|
type TrojanNode struct {
|
||||||
|
CommonNode
|
||||||
|
Network string `json:"network"`
|
||||||
|
NetworkSettings json.RawMessage `json:"networkSettings"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TuicNode struct {
|
||||||
|
CommonNode
|
||||||
|
CongestionControl string `json:"congestion_control"`
|
||||||
|
ZeroRTTHandshake bool `json:"zero_rtt_handshake"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AnyTlsNode struct {
|
||||||
|
CommonNode
|
||||||
|
PaddingScheme []string `json:"padding_scheme,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type HysteriaNode struct {
|
type HysteriaNode struct {
|
||||||
CommonNode
|
CommonNode
|
||||||
@@ -105,10 +132,11 @@ type HysteriaNode struct {
|
|||||||
|
|
||||||
type Hysteria2Node struct {
|
type Hysteria2Node struct {
|
||||||
CommonNode
|
CommonNode
|
||||||
UpMbps int `json:"up_mbps"`
|
Ignore_Client_Bandwidth bool `json:"ignore_client_bandwidth"`
|
||||||
DownMbps int `json:"down_mbps"`
|
UpMbps int `json:"up_mbps"`
|
||||||
ObfsType string `json:"obfs"`
|
DownMbps int `json:"down_mbps"`
|
||||||
ObfsPassword string `json:"obfs-password"`
|
ObfsType string `json:"obfs"`
|
||||||
|
ObfsPassword string `json:"obfs-password"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RawDNS struct {
|
type RawDNS struct {
|
||||||
@@ -126,12 +154,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 +208,6 @@ func (c *Client) GetNodeInfo() (node *NodeInfo, err error) {
|
|||||||
cm = &rsp.CommonNode
|
cm = &rsp.CommonNode
|
||||||
node.VAllss = rsp
|
node.VAllss = rsp
|
||||||
node.Security = node.VAllss.Tls
|
node.Security = node.VAllss.Tls
|
||||||
if len(rsp.NetworkSettings) > 0 {
|
|
||||||
err = json.Unmarshal(rsp.NetworkSettings, &rsp.RealityConfig)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("decode reality config error: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if node.Security == Reality {
|
|
||||||
if rsp.TlsSettings.PrivateKey == "" {
|
|
||||||
key := crypt.GenX25519Private([]byte("vless" + c.Token))
|
|
||||||
rsp.TlsSettings.PrivateKey = base64.RawURLEncoding.EncodeToString(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case "shadowsocks":
|
case "shadowsocks":
|
||||||
rsp := &ShadowsocksNode{}
|
rsp := &ShadowsocksNode{}
|
||||||
err = json.Unmarshal(r.Body(), rsp)
|
err = json.Unmarshal(r.Body(), rsp)
|
||||||
@@ -188,9 +223,27 @@ func (c *Client) GetNodeInfo() (node *NodeInfo, err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("decode trojan params error: %s", err)
|
return nil, fmt.Errorf("decode trojan params error: %s", err)
|
||||||
}
|
}
|
||||||
cm = (*CommonNode)(rsp)
|
cm = &rsp.CommonNode
|
||||||
node.Trojan = rsp
|
node.Trojan = rsp
|
||||||
node.Security = Tls
|
node.Security = Tls
|
||||||
|
case "tuic":
|
||||||
|
rsp := &TuicNode{}
|
||||||
|
err = json.Unmarshal(r.Body(), rsp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("decode tuic params error: %s", err)
|
||||||
|
}
|
||||||
|
cm = &rsp.CommonNode
|
||||||
|
node.Tuic = rsp
|
||||||
|
node.Security = Tls
|
||||||
|
case "anytls":
|
||||||
|
rsp := &AnyTlsNode{}
|
||||||
|
err = json.Unmarshal(r.Body(), rsp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("decode anytls params error: %s", err)
|
||||||
|
}
|
||||||
|
cm = &rsp.CommonNode
|
||||||
|
node.AnyTls = rsp
|
||||||
|
node.Security = Tls
|
||||||
case "hysteria":
|
case "hysteria":
|
||||||
rsp := &HysteriaNode{}
|
rsp := &HysteriaNode{}
|
||||||
err = json.Unmarshal(r.Body(), rsp)
|
err = json.Unmarshal(r.Body(), rsp)
|
||||||
@@ -260,8 +313,7 @@ func (c *Client) GetNodeInfo() (node *NodeInfo, err error) {
|
|||||||
cm.Routes = nil
|
cm.Routes = nil
|
||||||
cm.BaseConfig = nil
|
cm.BaseConfig = nil
|
||||||
|
|
||||||
c.nodeEtag = r.Header().Get("ETag")
|
return node, nil
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func intervalToTime(i interface{}) time.Duration {
|
func intervalToTime(i interface{}) time.Duration {
|
||||||
|
|||||||
@@ -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) {
|
||||||
@@ -54,6 +56,8 @@ func New(c *conf.ApiConfig) (*Client, error) {
|
|||||||
"shadowsocks",
|
"shadowsocks",
|
||||||
"hysteria",
|
"hysteria",
|
||||||
"hysteria2",
|
"hysteria2",
|
||||||
|
"tuic",
|
||||||
|
"anytls",
|
||||||
"vless":
|
"vless":
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unsupported Node type: %s", c.NodeType)
|
return nil, fmt.Errorf("unsupported Node type: %s", c.NodeType)
|
||||||
@@ -70,5 +74,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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,12 @@ package panel
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/goccy/go-json"
|
"encoding/json/jsontext"
|
||||||
|
"encoding/json/v2"
|
||||||
|
|
||||||
|
"github.com/vmihailenco/msgpack/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
type OnlineUser struct {
|
type OnlineUser struct {
|
||||||
@@ -12,62 +16,103 @@ type OnlineUser struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UserInfo struct {
|
type UserInfo struct {
|
||||||
Id int `json:"id"`
|
Id int `json:"id" msgpack:"id"`
|
||||||
Uuid string `json:"uuid"`
|
Uuid string `json:"uuid" msgpack:"uuid"`
|
||||||
SpeedLimit int `json:"speed_limit"`
|
SpeedLimit int `json:"speed_limit" msgpack:"speed_limit"`
|
||||||
DeviceLimit int `json:"device_limit"`
|
DeviceLimit int `json:"device_limit" msgpack:"device_limit"`
|
||||||
AliveIp int `json:"alive_ip"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserListBody struct {
|
type UserListBody struct {
|
||||||
//Msg string `json:"msg"`
|
Users []UserInfo `json:"users" msgpack:"users"`
|
||||||
Users []UserInfo `json:"users"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserList will pull user form sspanel
|
type AliveMap struct {
|
||||||
func (c *Client) GetUserList() (UserList []UserInfo, err error) {
|
Alive map[int]int `json:"alive"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserList will pull user from v2board
|
||||||
|
func (c *Client) GetUserList() ([]UserInfo, error) {
|
||||||
const path = "/api/v1/server/UniProxy/user"
|
const path = "/api/v1/server/UniProxy/user"
|
||||||
r, err := c.client.R().
|
r, err := c.client.R().
|
||||||
SetHeader("If-None-Match", c.userEtag).
|
SetHeader("If-None-Match", c.userEtag).
|
||||||
|
SetHeader("X-Response-Format", "msgpack").
|
||||||
|
SetDoNotParseResponse(true).
|
||||||
Get(path)
|
Get(path)
|
||||||
err = c.checkResponse(r, path, err)
|
if r == nil || r.RawResponse == nil {
|
||||||
if err != nil {
|
return nil, fmt.Errorf("received nil response or raw response")
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
defer r.RawResponse.Body.Close()
|
||||||
|
|
||||||
if r.StatusCode() == 304 {
|
if r.StatusCode() == 304 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
var userList *UserListBody
|
|
||||||
err = json.Unmarshal(r.Body(), &userList)
|
if err = c.checkResponse(r, path, err); err != nil {
|
||||||
if err != nil {
|
return nil, err
|
||||||
return nil, fmt.Errorf("unmarshal userlist error: %s", err)
|
|
||||||
}
|
}
|
||||||
c.userEtag = r.Header().Get("ETag")
|
userlist := &UserListBody{}
|
||||||
|
if strings.Contains(r.Header().Get("Content-Type"), "application/x-msgpack") {
|
||||||
var userinfos []UserInfo
|
decoder := msgpack.NewDecoder(r.RawResponse.Body)
|
||||||
var localDeviceLimit int = 0
|
if err := decoder.Decode(userlist); err != nil {
|
||||||
for _, user := range userList.Users {
|
return nil, fmt.Errorf("decode user list error: %w", err)
|
||||||
// If there is still device available, add the user
|
}
|
||||||
if user.DeviceLimit > 0 && user.AliveIp > 0 {
|
} else {
|
||||||
lastOnline := 0
|
dec := jsontext.NewDecoder(r.RawResponse.Body)
|
||||||
if v, ok := c.LastReportOnline[user.Id]; ok {
|
for {
|
||||||
lastOnline = v
|
tok, err := dec.ReadToken()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("decode user list error: %w", err)
|
||||||
}
|
}
|
||||||
// If there are any available device.
|
if tok.Kind() == '"' && tok.String() == "users" {
|
||||||
localDeviceLimit = user.DeviceLimit - user.AliveIp + lastOnline
|
break
|
||||||
if localDeviceLimit > 0 {
|
|
||||||
|
|
||||||
} else if lastOnline > 0 {
|
|
||||||
|
|
||||||
} else {
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
userinfos = append(userinfos, user)
|
tok, err := dec.ReadToken()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("decode user list error: %w", err)
|
||||||
|
}
|
||||||
|
if tok.Kind() != '[' {
|
||||||
|
return nil, fmt.Errorf(`decode user list error: expected "users" array`)
|
||||||
|
}
|
||||||
|
for dec.PeekKind() != ']' {
|
||||||
|
val, err := dec.ReadValue()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("decode user list error: read user object: %w", err)
|
||||||
|
}
|
||||||
|
var u UserInfo
|
||||||
|
if err := json.Unmarshal(val, &u); err != nil {
|
||||||
|
return nil, fmt.Errorf("decode user list error: unmarshal user error: %w", err)
|
||||||
|
}
|
||||||
|
userlist.Users = append(userlist.Users, u)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.userEtag = r.Header().Get("ETag")
|
||||||
|
return userlist.Users, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserAlive will fetch the alive_ip count for users
|
||||||
|
func (c *Client) GetUserAlive() (map[int]int, error) {
|
||||||
|
c.AliveMap = &AliveMap{}
|
||||||
|
const path = "/api/v1/server/UniProxy/alivelist"
|
||||||
|
r, err := c.client.R().
|
||||||
|
ForceContentType("application/json").
|
||||||
|
Get(path)
|
||||||
|
if err != nil || r.StatusCode() >= 399 {
|
||||||
|
c.AliveMap.Alive = make(map[int]int)
|
||||||
|
return c.AliveMap.Alive, nil
|
||||||
|
}
|
||||||
|
if r == nil || r.RawResponse == nil {
|
||||||
|
fmt.Printf("received nil response or raw response")
|
||||||
|
c.AliveMap.Alive = make(map[int]int)
|
||||||
|
return c.AliveMap.Alive, nil
|
||||||
|
}
|
||||||
|
defer r.RawResponse.Body.Close()
|
||||||
|
if err := json.Unmarshal(r.Body(), c.AliveMap); err != nil {
|
||||||
|
fmt.Printf("unmarshal user alive list error: %s", err)
|
||||||
|
c.AliveMap.Alive = make(map[int]int)
|
||||||
}
|
}
|
||||||
|
|
||||||
return userinfos, nil
|
return c.AliveMap.Alive, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserTraffic struct {
|
type UserTraffic struct {
|
||||||
@@ -94,8 +139,7 @@ func (c *Client) ReportUserTraffic(userTraffic []UserTraffic) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) ReportNodeOnlineUsers(data *map[int][]string, reportOnline *map[int]int) error {
|
func (c *Client) ReportNodeOnlineUsers(data *map[int][]string) error {
|
||||||
c.LastReportOnline = *reportOnline
|
|
||||||
const path = "/api/v1/server/UniProxy/alive"
|
const path = "/api/v1/server/UniProxy/alive"
|
||||||
r, err := c.client.R().
|
r, err := c.client.R().
|
||||||
SetBody(data).
|
SetBody(data).
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
"github.com/InazumaV/V2bX/node"
|
"github.com/InazumaV/V2bX/node"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"gopkg.in/natefinch/lumberjack.v2"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -56,14 +55,11 @@ func serverHandle(_ *cobra.Command, _ []string) {
|
|||||||
log.SetLevel(log.ErrorLevel)
|
log.SetLevel(log.ErrorLevel)
|
||||||
}
|
}
|
||||||
if c.LogConfig.Output != "" {
|
if c.LogConfig.Output != "" {
|
||||||
w := &lumberjack.Logger{
|
f, err := os.OpenFile(c.LogConfig.Output, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
||||||
Filename: c.LogConfig.Output,
|
if err != nil {
|
||||||
MaxSize: 100,
|
log.WithField("err", err).Error("Open log file failed, using stdout instead")
|
||||||
MaxBackups: 3,
|
|
||||||
MaxAge: 28,
|
|
||||||
Compress: true,
|
|
||||||
}
|
}
|
||||||
log.SetOutput(w)
|
log.SetOutput(f)
|
||||||
}
|
}
|
||||||
limiter.Init()
|
limiter.Init()
|
||||||
log.Info("Start V2bX...")
|
log.Info("Start V2bX...")
|
||||||
@@ -96,7 +92,6 @@ func serverHandle(_ *cobra.Command, _ []string) {
|
|||||||
log.WithField("err", err).Error("Restart node failed")
|
log.WithField("err", err).Error("Restart node failed")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
runtime.GC()
|
|
||||||
vc, err = vCore.NewCore(c.CoresConfig)
|
vc, err = vCore.NewCore(c.CoresConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithField("err", err).Error("New core failed")
|
log.WithField("err", err).Error("New core failed")
|
||||||
@@ -114,6 +109,7 @@ func serverHandle(_ *cobra.Command, _ []string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Info("Nodes restarted")
|
log.Info("Nodes restarted")
|
||||||
|
runtime.GC()
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithField("err", err).Error("start watch failed")
|
log.WithField("err", err).Error("start watch failed")
|
||||||
@@ -125,7 +121,7 @@ func serverHandle(_ *cobra.Command, _ []string) {
|
|||||||
// wait exit signal
|
// wait exit signal
|
||||||
{
|
{
|
||||||
osSignals := make(chan os.Signal, 1)
|
osSignals := make(chan os.Signal, 1)
|
||||||
signal.Notify(osSignals, syscall.SIGINT, syscall.SIGKILL, syscall.SIGTERM)
|
signal.Notify(osSignals, syscall.SIGINT, syscall.SIGTERM)
|
||||||
<-osSignals
|
<-osSignals
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type TrafficCounter struct {
|
type TrafficCounter struct {
|
||||||
counters map[string]*TrafficStorage
|
Counters sync.Map
|
||||||
lock sync.RWMutex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type TrafficStorage struct {
|
type TrafficStorage struct {
|
||||||
@@ -16,76 +15,60 @@ type TrafficStorage struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewTrafficCounter() *TrafficCounter {
|
func NewTrafficCounter() *TrafficCounter {
|
||||||
return &TrafficCounter{
|
return &TrafficCounter{}
|
||||||
counters: map[string]*TrafficStorage{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *TrafficCounter) GetCounter(id string) *TrafficStorage {
|
func (c *TrafficCounter) GetCounter(uuid string) *TrafficStorage {
|
||||||
c.lock.RLock()
|
if cts, ok := c.Counters.Load(uuid); ok {
|
||||||
cts, ok := c.counters[id]
|
return cts.(*TrafficStorage)
|
||||||
c.lock.RUnlock()
|
|
||||||
if !ok {
|
|
||||||
cts = &TrafficStorage{}
|
|
||||||
c.counters[id] = cts
|
|
||||||
}
|
}
|
||||||
return cts
|
newStorage := &TrafficStorage{}
|
||||||
|
if cts, loaded := c.Counters.LoadOrStore(uuid, newStorage); loaded {
|
||||||
|
return cts.(*TrafficStorage)
|
||||||
|
}
|
||||||
|
return newStorage
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *TrafficCounter) GetUpCount(id string) int64 {
|
func (c *TrafficCounter) GetUpCount(uuid string) int64 {
|
||||||
c.lock.RLock()
|
if cts, ok := c.Counters.Load(uuid); ok {
|
||||||
cts, ok := c.counters[id]
|
return cts.(*TrafficStorage).UpCounter.Load()
|
||||||
c.lock.RUnlock()
|
|
||||||
if ok {
|
|
||||||
return cts.UpCounter.Load()
|
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *TrafficCounter) GetDownCount(id string) int64 {
|
func (c *TrafficCounter) GetDownCount(uuid string) int64 {
|
||||||
c.lock.RLock()
|
if cts, ok := c.Counters.Load(uuid); ok {
|
||||||
cts, ok := c.counters[id]
|
return cts.(*TrafficStorage).DownCounter.Load()
|
||||||
c.lock.RUnlock()
|
|
||||||
if ok {
|
|
||||||
return cts.DownCounter.Load()
|
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *TrafficCounter) Len() int {
|
func (c *TrafficCounter) Len() int {
|
||||||
c.lock.RLock()
|
length := 0
|
||||||
defer c.lock.RUnlock()
|
c.Counters.Range(func(_, _ interface{}) bool {
|
||||||
return len(c.counters)
|
length++
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
return length
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *TrafficCounter) Reset(id string) {
|
func (c *TrafficCounter) Reset(uuid string) {
|
||||||
c.lock.RLock()
|
if cts, ok := c.Counters.Load(uuid); ok {
|
||||||
cts := c.GetCounter(id)
|
cts.(*TrafficStorage).UpCounter.Store(0)
|
||||||
c.lock.RUnlock()
|
cts.(*TrafficStorage).DownCounter.Store(0)
|
||||||
cts.UpCounter.Store(0)
|
}
|
||||||
cts.DownCounter.Store(0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *TrafficCounter) Delete(id string) {
|
func (c *TrafficCounter) Delete(uuid string) {
|
||||||
c.lock.Lock()
|
c.Counters.Delete(uuid)
|
||||||
delete(c.counters, id)
|
|
||||||
c.lock.Unlock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *TrafficCounter) Rx(id string, n int) {
|
func (c *TrafficCounter) Rx(uuid string, n int) {
|
||||||
cts := c.GetCounter(id)
|
cts := c.GetCounter(uuid)
|
||||||
cts.DownCounter.Add(int64(n))
|
cts.DownCounter.Add(int64(n))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *TrafficCounter) Tx(id string, n int) {
|
func (c *TrafficCounter) Tx(uuid string, n int) {
|
||||||
cts := c.GetCounter(id)
|
cts := c.GetCounter(uuid)
|
||||||
cts.UpCounter.Add(int64(n))
|
cts.UpCounter.Add(int64(n))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *TrafficCounter) IncConn(auth string) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *TrafficCounter) DecConn(auth string) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|||||||
25
common/counter/xraytraffic.go
Normal file
25
common/counter/xraytraffic.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package counter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
|
fstats "github.com/xtls/xray-core/features/stats"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ fstats.Counter = (*XrayTrafficCounter)(nil)
|
||||||
|
|
||||||
|
type XrayTrafficCounter struct {
|
||||||
|
V *atomic.Int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *XrayTrafficCounter) Value() int64 {
|
||||||
|
return c.V.Load()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *XrayTrafficCounter) Set(newValue int64) int64 {
|
||||||
|
return c.V.Swap(newValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *XrayTrafficCounter) Add(delta int64) int64 {
|
||||||
|
return c.V.Add(delta)
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|||||||
20
conf/conf.go
20
conf/conf.go
@@ -2,10 +2,12 @@ package conf
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/InazumaV/V2bX/common/json5"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/goccy/go-json"
|
"github.com/InazumaV/V2bX/common/json5"
|
||||||
|
|
||||||
|
"encoding/json/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Conf struct {
|
type Conf struct {
|
||||||
@@ -29,5 +31,17 @@ func (p *Conf) LoadFromPath(filePath string) error {
|
|||||||
return fmt.Errorf("open config file error: %s", err)
|
return fmt.Errorf("open config file error: %s", err)
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
return json.NewDecoder(json5.NewTrimNodeReader(f)).Decode(p)
|
|
||||||
|
reader := json5.NewTrimNodeReader(f)
|
||||||
|
data, err := io.ReadAll(reader)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read config file error: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(data, p)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unmarshal config error: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
12
conf/core.go
12
conf/core.go
@@ -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
17
conf/hy.go
Normal 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",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,8 +7,9 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
"github.com/InazumaV/V2bX/common/json5"
|
"github.com/InazumaV/V2bX/common/json5"
|
||||||
"github.com/goccy/go-json"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type NodeConfig struct {
|
type NodeConfig struct {
|
||||||
@@ -109,10 +110,12 @@ type Options struct {
|
|||||||
ListenIP string `json:"ListenIP"`
|
ListenIP string `json:"ListenIP"`
|
||||||
SendIP string `json:"SendIP"`
|
SendIP string `json:"SendIP"`
|
||||||
DeviceOnlineMinTraffic int64 `json:"DeviceOnlineMinTraffic"`
|
DeviceOnlineMinTraffic int64 `json:"DeviceOnlineMinTraffic"`
|
||||||
|
ReportMinTraffic int64 `json:"ReportMinTraffic"`
|
||||||
LimitConfig LimitConfig `json:"LimitConfig"`
|
LimitConfig LimitConfig `json:"LimitConfig"`
|
||||||
RawOptions json.RawMessage `json:"RawOptions"`
|
RawOptions json.RawMessage `json:"RawOptions"`
|
||||||
XrayOptions *XrayOptions `json:"XrayOptions"`
|
XrayOptions *XrayOptions `json:"XrayOptions"`
|
||||||
SingOptions *SingOptions `json:"SingOptions"`
|
SingOptions *SingOptions `json:"SingOptions"`
|
||||||
|
Hysteria2ConfigPath string `json:"Hysteria2ConfigPath"`
|
||||||
CertConfig *CertConfig `json:"CertConfig"`
|
CertConfig *CertConfig `json:"CertConfig"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,6 +132,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
|
||||||
|
|||||||
26
conf/sing.go
26
conf/sing.go
@@ -5,11 +5,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type SingConfig struct {
|
type SingConfig struct {
|
||||||
LogConfig SingLogConfig `json:"Log"`
|
LogConfig SingLogConfig `json:"Log"`
|
||||||
NtpConfig SingNtpConfig `json:"NTP"`
|
NtpConfig SingNtpConfig `json:"NTP"`
|
||||||
EnableConnClear bool `json:"EnableConnClear"`
|
OriginalPath string `json:"OriginalPath"`
|
||||||
DnsConfigPath string `json:"DnsConfigPath"`
|
|
||||||
OriginalPath string `json:"OriginalPath"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type SingLogConfig struct {
|
type SingLogConfig struct {
|
||||||
@@ -34,13 +32,13 @@ func NewSingConfig() *SingConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type SingOptions struct {
|
type SingOptions struct {
|
||||||
EnableProxyProtocol bool `json:"EnableProxyProtocol"`
|
|
||||||
TCPFastOpen bool `json:"EnableTFO"`
|
TCPFastOpen bool `json:"EnableTFO"`
|
||||||
SniffEnabled bool `json:"EnableSniff"`
|
SniffEnabled bool `json:"EnableSniff"`
|
||||||
|
SniffOverrideDestination bool `json:"SniffOverrideDestination"`
|
||||||
EnableDNS bool `json:"EnableDNS"`
|
EnableDNS bool `json:"EnableDNS"`
|
||||||
DomainStrategy option.DomainStrategy `json:"DomainStrategy"`
|
DomainStrategy option.DomainStrategy `json:"DomainStrategy"`
|
||||||
SniffOverrideDestination bool `json:"SniffOverrideDestination"`
|
|
||||||
FallBackConfigs *FallBackConfigForSing `json:"FallBackConfigs"`
|
FallBackConfigs *FallBackConfigForSing `json:"FallBackConfigs"`
|
||||||
|
Multiplex *MultiplexConfig `json:"MultiplexConfig"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SingNtpConfig struct {
|
type SingNtpConfig struct {
|
||||||
@@ -60,13 +58,25 @@ type FallBack struct {
|
|||||||
ServerPort string `json:"ServerPort"`
|
ServerPort string `json:"ServerPort"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MultiplexConfig struct {
|
||||||
|
Enabled bool `json:"Enable"`
|
||||||
|
Padding bool `json:"Padding"`
|
||||||
|
Brutal BrutalOptions `json:"Brutal"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BrutalOptions struct {
|
||||||
|
Enabled bool `json:"Enable"`
|
||||||
|
UpMbps int `json:"UpMbps"`
|
||||||
|
DownMbps int `json:"DownMbps"`
|
||||||
|
}
|
||||||
|
|
||||||
func NewSingOptions() *SingOptions {
|
func NewSingOptions() *SingOptions {
|
||||||
return &SingOptions{
|
return &SingOptions{
|
||||||
EnableDNS: false,
|
EnableDNS: false,
|
||||||
EnableProxyProtocol: false,
|
|
||||||
TCPFastOpen: false,
|
TCPFastOpen: false,
|
||||||
SniffEnabled: true,
|
SniffEnabled: true,
|
||||||
SniffOverrideDestination: true,
|
SniffOverrideDestination: true,
|
||||||
FallBackConfigs: &FallBackConfigForSing{},
|
FallBackConfigs: &FallBackConfigForSing{},
|
||||||
|
Multiplex: &MultiplexConfig{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func NewCore(c []conf.CoreConfig) (Core, error) {
|
func NewCore(c []conf.CoreConfig) (Core, error) {
|
||||||
if len(c) < 0 {
|
if len(c) == 0 {
|
||||||
return nil, errors.New("no have vail core")
|
return nil, errors.New("no have vail core")
|
||||||
}
|
}
|
||||||
// multi core
|
// multi core
|
||||||
|
|||||||
461
core/hy2/config.go
Normal file
461
core/hy2/config.go
Normal file
@@ -0,0 +1,461 @@
|
|||||||
|
package hy2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httputil"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/InazumaV/V2bX/api/panel"
|
||||||
|
"github.com/InazumaV/V2bX/conf"
|
||||||
|
"github.com/apernet/hysteria/core/v2/server"
|
||||||
|
"github.com/apernet/hysteria/extras/v2/correctnet"
|
||||||
|
"github.com/apernet/hysteria/extras/v2/masq"
|
||||||
|
"github.com/apernet/hysteria/extras/v2/obfs"
|
||||||
|
"github.com/apernet/hysteria/extras/v2/outbounds"
|
||||||
|
"github.com/apernet/hysteria/extras/v2/sniff"
|
||||||
|
eUtils "github.com/apernet/hysteria/extras/v2/utils"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type masqHandlerLogWrapper struct {
|
||||||
|
H http.Handler
|
||||||
|
QUIC bool
|
||||||
|
Logger *zap.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *masqHandlerLogWrapper) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
m.Logger.Debug("masquerade request",
|
||||||
|
zap.String("addr", r.RemoteAddr),
|
||||||
|
zap.String("method", r.Method),
|
||||||
|
zap.String("host", r.Host),
|
||||||
|
zap.String("url", r.URL.String()),
|
||||||
|
zap.Bool("quic", m.QUIC))
|
||||||
|
m.H.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
Byte = 1
|
||||||
|
Kilobyte = Byte * 1000
|
||||||
|
Megabyte = Kilobyte * 1000
|
||||||
|
Gigabyte = Megabyte * 1000
|
||||||
|
Terabyte = Gigabyte * 1000
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultStreamReceiveWindow = 8388608 // 8MB
|
||||||
|
defaultConnReceiveWindow = defaultStreamReceiveWindow * 5 / 2 // 20MB
|
||||||
|
defaultMaxIdleTimeout = 30 * time.Second
|
||||||
|
defaultMaxIncomingStreams = 4096
|
||||||
|
defaultUDPIdleTimeout = 60 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
func (n *Hysteria2node) getTLSConfig(config *conf.Options) (*server.TLSConfig, error) {
|
||||||
|
if config.CertConfig == nil {
|
||||||
|
return nil, fmt.Errorf("the CertConfig is not vail")
|
||||||
|
}
|
||||||
|
switch config.CertConfig.CertMode {
|
||||||
|
case "none", "":
|
||||||
|
return nil, fmt.Errorf("the CertMode cannot be none")
|
||||||
|
default:
|
||||||
|
var certs []tls.Certificate
|
||||||
|
cert, err := tls.LoadX509KeyPair(config.CertConfig.CertFile, config.CertConfig.KeyFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
certs = append(certs, cert)
|
||||||
|
return &server.TLSConfig{
|
||||||
|
Certificates: certs,
|
||||||
|
GetCertificate: func(tlsinfo *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||||
|
cert, err := tls.LoadX509KeyPair(config.CertConfig.CertFile, config.CertConfig.KeyFile)
|
||||||
|
return &cert, err
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Hysteria2node) getQUICConfig(config *serverConfig) (*server.QUICConfig, error) {
|
||||||
|
quic := &server.QUICConfig{}
|
||||||
|
if config.QUIC.InitStreamReceiveWindow == 0 {
|
||||||
|
quic.InitialStreamReceiveWindow = defaultStreamReceiveWindow
|
||||||
|
} else if config.QUIC.InitStreamReceiveWindow < 16384 {
|
||||||
|
return nil, fmt.Errorf("QUICConfig.InitialStreamReceiveWindowf must be at least 16384")
|
||||||
|
} else {
|
||||||
|
quic.InitialConnectionReceiveWindow = config.QUIC.InitConnectionReceiveWindow
|
||||||
|
}
|
||||||
|
if config.QUIC.MaxStreamReceiveWindow == 0 {
|
||||||
|
quic.MaxStreamReceiveWindow = defaultStreamReceiveWindow
|
||||||
|
} else if config.QUIC.MaxStreamReceiveWindow < 16384 {
|
||||||
|
return nil, fmt.Errorf("QUICConfig.MaxStreamReceiveWindowf must be at least 16384")
|
||||||
|
} else {
|
||||||
|
quic.MaxStreamReceiveWindow = config.QUIC.MaxStreamReceiveWindow
|
||||||
|
}
|
||||||
|
if config.QUIC.InitConnectionReceiveWindow == 0 {
|
||||||
|
quic.InitialConnectionReceiveWindow = defaultConnReceiveWindow
|
||||||
|
} else if config.QUIC.InitConnectionReceiveWindow < 16384 {
|
||||||
|
return nil, fmt.Errorf("QUICConfig.InitialConnectionReceiveWindowf must be at least 16384")
|
||||||
|
} else {
|
||||||
|
quic.InitialConnectionReceiveWindow = config.QUIC.InitConnectionReceiveWindow
|
||||||
|
}
|
||||||
|
if config.QUIC.MaxConnectionReceiveWindow == 0 {
|
||||||
|
quic.MaxConnectionReceiveWindow = defaultConnReceiveWindow
|
||||||
|
} else if config.QUIC.MaxConnectionReceiveWindow < 16384 {
|
||||||
|
return nil, fmt.Errorf("QUICConfig.MaxConnectionReceiveWindowf must be at least 16384")
|
||||||
|
} else {
|
||||||
|
quic.MaxConnectionReceiveWindow = config.QUIC.MaxConnectionReceiveWindow
|
||||||
|
}
|
||||||
|
if config.QUIC.MaxIdleTimeout == 0 {
|
||||||
|
quic.MaxIdleTimeout = defaultMaxIdleTimeout
|
||||||
|
} else if config.QUIC.MaxIdleTimeout < 4*time.Second || config.QUIC.MaxIdleTimeout > 120*time.Second {
|
||||||
|
return nil, fmt.Errorf("QUICConfig.MaxIdleTimeoutf must be between 4s and 120s")
|
||||||
|
} else {
|
||||||
|
quic.MaxIdleTimeout = config.QUIC.MaxIdleTimeout
|
||||||
|
}
|
||||||
|
if config.QUIC.MaxIncomingStreams == 0 {
|
||||||
|
quic.MaxIncomingStreams = defaultMaxIncomingStreams
|
||||||
|
} else if config.QUIC.MaxIncomingStreams < 8 {
|
||||||
|
return nil, fmt.Errorf("QUICConfig.MaxIncomingStreamsf must be at least 8")
|
||||||
|
} else {
|
||||||
|
quic.MaxIncomingStreams = config.QUIC.MaxIncomingStreams
|
||||||
|
}
|
||||||
|
// todo fix !linux && !windows && !darwin
|
||||||
|
quic.DisablePathMTUDiscovery = false
|
||||||
|
|
||||||
|
return quic, nil
|
||||||
|
}
|
||||||
|
func (n *Hysteria2node) getConn(info *panel.NodeInfo, config *conf.Options) (net.PacketConn, error) {
|
||||||
|
uAddr, err := net.ResolveUDPAddr("udp", formatAddress(config.ListenIP, info.Common.ServerPort))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
conn, err := correctnet.ListenUDP("udp", uAddr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
switch strings.ToLower(info.Hysteria2.ObfsType) {
|
||||||
|
case "", "plain":
|
||||||
|
return conn, nil
|
||||||
|
case "salamander":
|
||||||
|
ob, err := obfs.NewSalamanderObfuscator([]byte(info.Hysteria2.ObfsPassword))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return obfs.WrapPacketConn(conn, ob), nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported obfuscation type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Hysteria2node) getBandwidthConfig(info *panel.NodeInfo) *server.BandwidthConfig {
|
||||||
|
band := &server.BandwidthConfig{}
|
||||||
|
if info.Hysteria2.UpMbps != 0 {
|
||||||
|
band.MaxTx = (uint64)(info.Hysteria2.UpMbps * Megabyte / 8)
|
||||||
|
}
|
||||||
|
if info.Hysteria2.DownMbps != 0 {
|
||||||
|
band.MaxRx = (uint64)(info.Hysteria2.DownMbps * Megabyte / 8)
|
||||||
|
|
||||||
|
}
|
||||||
|
return band
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Hysteria2node) getRequestHook(c *serverConfig) (server.RequestHook, error) {
|
||||||
|
if c.Sniff.Enable {
|
||||||
|
s := &sniff.Sniffer{
|
||||||
|
Timeout: c.Sniff.Timeout,
|
||||||
|
RewriteDomain: c.Sniff.RewriteDomain,
|
||||||
|
}
|
||||||
|
if c.Sniff.TCPPorts != "" {
|
||||||
|
s.TCPPorts = eUtils.ParsePortUnion(c.Sniff.TCPPorts)
|
||||||
|
if s.TCPPorts == nil {
|
||||||
|
return nil, fmt.Errorf("sniff.tcpPorts: invalid port union")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if c.Sniff.UDPPorts != "" {
|
||||||
|
s.UDPPorts = eUtils.ParsePortUnion(c.Sniff.UDPPorts)
|
||||||
|
if s.UDPPorts == nil {
|
||||||
|
return nil, fmt.Errorf("sniff.udpPorts: invalid port union")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Hysteria2node) getOutboundConfig(c *serverConfig) (server.Outbound, error) {
|
||||||
|
// Resolver, ACL, actual outbound are all implemented through the Outbound interface.
|
||||||
|
// Depending on the config, we build a chain like this:
|
||||||
|
// Resolver(ACL(Outbounds...))
|
||||||
|
|
||||||
|
// Outbounds
|
||||||
|
var obs []outbounds.OutboundEntry
|
||||||
|
if len(c.Outbounds) == 0 {
|
||||||
|
// Guarantee we have at least one outbound
|
||||||
|
obs = []outbounds.OutboundEntry{{
|
||||||
|
Name: "default",
|
||||||
|
Outbound: outbounds.NewDirectOutboundSimple(outbounds.DirectOutboundModeAuto),
|
||||||
|
}}
|
||||||
|
} else {
|
||||||
|
obs = make([]outbounds.OutboundEntry, len(c.Outbounds))
|
||||||
|
for i, entry := range c.Outbounds {
|
||||||
|
if entry.Name == "" {
|
||||||
|
return nil, fmt.Errorf("empty outbound name")
|
||||||
|
}
|
||||||
|
var ob outbounds.PluggableOutbound
|
||||||
|
var err error
|
||||||
|
switch strings.ToLower(entry.Type) {
|
||||||
|
case "direct":
|
||||||
|
ob, err = serverConfigOutboundDirectToOutbound(entry.Direct)
|
||||||
|
case "socks5":
|
||||||
|
ob, err = serverConfigOutboundSOCKS5ToOutbound(entry.SOCKS5)
|
||||||
|
case "http":
|
||||||
|
ob, err = serverConfigOutboundHTTPToOutbound(entry.HTTP)
|
||||||
|
default:
|
||||||
|
err = fmt.Errorf("outbounds.type unsupported outbound type")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
obs[i] = outbounds.OutboundEntry{Name: entry.Name, Outbound: ob}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var uOb outbounds.PluggableOutbound // "unified" outbound
|
||||||
|
|
||||||
|
// ACL
|
||||||
|
hasACL := false
|
||||||
|
if c.ACL.File != "" && len(c.ACL.Inline) > 0 {
|
||||||
|
return nil, fmt.Errorf("cannot set both acl.file and acl.inline")
|
||||||
|
}
|
||||||
|
gLoader := &GeoLoader{
|
||||||
|
GeoIPFilename: c.ACL.GeoIP,
|
||||||
|
GeoSiteFilename: c.ACL.GeoSite,
|
||||||
|
UpdateInterval: c.ACL.GeoUpdateInterval,
|
||||||
|
Logger: n.Logger,
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.ACL.File != "" {
|
||||||
|
hasACL = true
|
||||||
|
acl, err := outbounds.NewACLEngineFromFile(c.ACL.File, obs, gLoader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
uOb = acl
|
||||||
|
} else if len(c.ACL.Inline) > 0 {
|
||||||
|
n.Logger.Debug("found ACL Inline:", zap.Strings("Inline", c.ACL.Inline))
|
||||||
|
hasACL = true
|
||||||
|
acl, err := outbounds.NewACLEngineFromString(strings.Join(c.ACL.Inline, "\n"), obs, gLoader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
uOb = acl
|
||||||
|
} else {
|
||||||
|
// No ACL, use the first outbound
|
||||||
|
uOb = obs[0].Outbound
|
||||||
|
}
|
||||||
|
|
||||||
|
switch strings.ToLower(c.Resolver.Type) {
|
||||||
|
case "", "system":
|
||||||
|
if hasACL {
|
||||||
|
// If the user uses ACL, we must put a resolver in front of it,
|
||||||
|
// for IP rules to work on domain requests.
|
||||||
|
uOb = outbounds.NewSystemResolver(uOb)
|
||||||
|
}
|
||||||
|
// Otherwise we can just rely on outbound handling on its own.
|
||||||
|
case "tcp":
|
||||||
|
if c.Resolver.TCP.Addr == "" {
|
||||||
|
return nil, fmt.Errorf("empty resolver address")
|
||||||
|
}
|
||||||
|
uOb = outbounds.NewStandardResolverTCP(c.Resolver.TCP.Addr, c.Resolver.TCP.Timeout, uOb)
|
||||||
|
case "udp":
|
||||||
|
if c.Resolver.UDP.Addr == "" {
|
||||||
|
return nil, fmt.Errorf("empty resolver address")
|
||||||
|
}
|
||||||
|
uOb = outbounds.NewStandardResolverUDP(c.Resolver.UDP.Addr, c.Resolver.UDP.Timeout, uOb)
|
||||||
|
case "tls", "tcp-tls":
|
||||||
|
if c.Resolver.TLS.Addr == "" {
|
||||||
|
return nil, fmt.Errorf("empty resolver address")
|
||||||
|
}
|
||||||
|
uOb = outbounds.NewStandardResolverTLS(c.Resolver.TLS.Addr, c.Resolver.TLS.Timeout, c.Resolver.TLS.SNI, c.Resolver.TLS.Insecure, uOb)
|
||||||
|
case "https", "http":
|
||||||
|
if c.Resolver.HTTPS.Addr == "" {
|
||||||
|
return nil, fmt.Errorf("empty resolver address")
|
||||||
|
}
|
||||||
|
uOb = outbounds.NewDoHResolver(c.Resolver.HTTPS.Addr, c.Resolver.HTTPS.Timeout, c.Resolver.HTTPS.SNI, c.Resolver.HTTPS.Insecure, uOb)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported resolver type")
|
||||||
|
}
|
||||||
|
Outbound := &outbounds.PluggableOutboundAdapter{PluggableOutbound: uOb}
|
||||||
|
|
||||||
|
return Outbound, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Hysteria2node) getMasqHandler(tlsconfig *server.TLSConfig, conn net.PacketConn, c *serverConfig) (http.Handler, error) {
|
||||||
|
var handler http.Handler
|
||||||
|
switch strings.ToLower(c.Masquerade.Type) {
|
||||||
|
case "", "404":
|
||||||
|
handler = http.NotFoundHandler()
|
||||||
|
case "file":
|
||||||
|
if c.Masquerade.File.Dir == "" {
|
||||||
|
return nil, fmt.Errorf("masquerade.file.dir empty file directory")
|
||||||
|
}
|
||||||
|
handler = http.FileServer(http.Dir(c.Masquerade.File.Dir))
|
||||||
|
case "proxy":
|
||||||
|
if c.Masquerade.Proxy.URL == "" {
|
||||||
|
return nil, fmt.Errorf("masquerade.proxy.url empty proxy url")
|
||||||
|
}
|
||||||
|
u, err := url.Parse(c.Masquerade.Proxy.URL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("masquerade.proxy.url %s", err)
|
||||||
|
}
|
||||||
|
handler = &httputil.ReverseProxy{
|
||||||
|
Director: func(req *http.Request) {
|
||||||
|
req.URL.Scheme = u.Scheme
|
||||||
|
req.URL.Host = u.Host
|
||||||
|
|
||||||
|
if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil {
|
||||||
|
xff := req.Header.Get("X-Forwarded-For")
|
||||||
|
if xff != "" {
|
||||||
|
clientIP = xff + ", " + clientIP
|
||||||
|
}
|
||||||
|
req.Header.Set("X-Forwarded-For", clientIP)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Masquerade.Proxy.RewriteHost {
|
||||||
|
req.Host = req.URL.Host
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) {
|
||||||
|
n.Logger.Error("HTTP reverse proxy error", zap.Error(err))
|
||||||
|
w.WriteHeader(http.StatusBadGateway)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
case "string":
|
||||||
|
if c.Masquerade.String.Content == "" {
|
||||||
|
return nil, fmt.Errorf("masquerade.string.content empty string content")
|
||||||
|
}
|
||||||
|
if c.Masquerade.String.StatusCode != 0 &&
|
||||||
|
(c.Masquerade.String.StatusCode < 200 ||
|
||||||
|
c.Masquerade.String.StatusCode > 599 ||
|
||||||
|
c.Masquerade.String.StatusCode == 233) {
|
||||||
|
// 233 is reserved for Hysteria authentication
|
||||||
|
return nil, fmt.Errorf("masquerade.string.statusCode invalid status code (must be 200-599, except 233)")
|
||||||
|
}
|
||||||
|
handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
for k, v := range c.Masquerade.String.Headers {
|
||||||
|
w.Header().Set(k, v)
|
||||||
|
}
|
||||||
|
if c.Masquerade.String.StatusCode != 0 {
|
||||||
|
w.WriteHeader(c.Masquerade.String.StatusCode)
|
||||||
|
} else {
|
||||||
|
w.WriteHeader(http.StatusOK) // Use 200 OK by default
|
||||||
|
}
|
||||||
|
_, _ = w.Write([]byte(c.Masquerade.String.Content))
|
||||||
|
})
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("masquerade.type unsupported masquerade type")
|
||||||
|
}
|
||||||
|
MasqHandler := &masqHandlerLogWrapper{H: handler, QUIC: true, Logger: n.Logger}
|
||||||
|
|
||||||
|
if c.Masquerade.ListenHTTP != "" || c.Masquerade.ListenHTTPS != "" {
|
||||||
|
if c.Masquerade.ListenHTTP != "" && c.Masquerade.ListenHTTPS == "" {
|
||||||
|
return nil, fmt.Errorf("masquerade.listenHTTPS having only HTTP server without HTTPS is not supported")
|
||||||
|
}
|
||||||
|
s := masq.MasqTCPServer{
|
||||||
|
QUICPort: extractPortFromAddr(conn.LocalAddr().String()),
|
||||||
|
HTTPSPort: extractPortFromAddr(c.Masquerade.ListenHTTPS),
|
||||||
|
Handler: &masqHandlerLogWrapper{H: handler, QUIC: false, Logger: n.Logger},
|
||||||
|
TLSConfig: &tls.Config{
|
||||||
|
Certificates: tlsconfig.Certificates,
|
||||||
|
GetCertificate: tlsconfig.GetCertificate,
|
||||||
|
},
|
||||||
|
ForceHTTPS: c.Masquerade.ForceHTTPS,
|
||||||
|
}
|
||||||
|
go runMasqTCPServer(&s, c.Masquerade.ListenHTTP, c.Masquerade.ListenHTTPS, n.Logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
return MasqHandler, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Hysteria2node) getHyConfig(info *panel.NodeInfo, config *conf.Options, c *serverConfig) (*server.Config, error) {
|
||||||
|
tls, err := n.getTLSConfig(config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
quic, err := n.getQUICConfig(c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
conn, err := n.getConn(info, config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sniff, err := n.getRequestHook(c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
Outbound, err := n.getOutboundConfig(c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
Masq, err := n.getMasqHandler(tls, conn, c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &server.Config{
|
||||||
|
TLSConfig: *tls,
|
||||||
|
QUICConfig: *quic,
|
||||||
|
Conn: conn,
|
||||||
|
RequestHook: sniff,
|
||||||
|
Outbound: Outbound,
|
||||||
|
BandwidthConfig: *n.getBandwidthConfig(info),
|
||||||
|
IgnoreClientBandwidth: info.Hysteria2.Ignore_Client_Bandwidth,
|
||||||
|
DisableUDP: c.DisableUDP,
|
||||||
|
UDPIdleTimeout: c.UDPIdleTimeout,
|
||||||
|
EventLogger: n.EventLogger,
|
||||||
|
TrafficLogger: n.TrafficLogger,
|
||||||
|
MasqHandler: Masq,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runMasqTCPServer(s *masq.MasqTCPServer, httpAddr, httpsAddr string, logger *zap.Logger) {
|
||||||
|
errChan := make(chan error, 2)
|
||||||
|
if httpAddr != "" {
|
||||||
|
go func() {
|
||||||
|
logger.Info("masquerade HTTP server up and running", zap.String("listen", httpAddr))
|
||||||
|
errChan <- s.ListenAndServeHTTP(httpAddr)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
if httpsAddr != "" {
|
||||||
|
go func() {
|
||||||
|
logger.Info("masquerade HTTPS server up and running", zap.String("listen", httpsAddr))
|
||||||
|
errChan <- s.ListenAndServeHTTPS(httpsAddr)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
err := <-errChan
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatal("failed to serve masquerade HTTP(S)", zap.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractPortFromAddr(addr string) int {
|
||||||
|
_, portStr, err := net.SplitHostPort(addr)
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
port, err := strconv.Atoi(portStr)
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return port
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatAddress(ip string, port int) string {
|
||||||
|
if strings.Contains(ip, ":") {
|
||||||
|
return fmt.Sprintf("[%s]:%d", ip, port)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s:%d", ip, port)
|
||||||
|
}
|
||||||
181
core/hy2/geoloader.go
Normal file
181
core/hy2/geoloader.go
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
63
core/hy2/hook.go
Normal file
63
core/hy2/hook.go
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
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
|
||||||
|
ReportMinTrafficBytes int64
|
||||||
|
}
|
||||||
|
|
||||||
|
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
61
core/hy2/hy2.go
Normal 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
156
core/hy2/logger.go
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
package hy2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/InazumaV/V2bX/common/format"
|
||||||
|
"github.com/InazumaV/V2bX/limiter"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"go.uber.org/zap/zapcore"
|
||||||
|
)
|
||||||
|
|
||||||
|
type serverLogger struct {
|
||||||
|
Tag string
|
||||||
|
logger *zap.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
var logLevelMap = map[string]zapcore.Level{
|
||||||
|
"debug": zapcore.DebugLevel,
|
||||||
|
"info": zapcore.InfoLevel,
|
||||||
|
"warn": zapcore.WarnLevel,
|
||||||
|
"error": zapcore.ErrorLevel,
|
||||||
|
}
|
||||||
|
|
||||||
|
var logFormatMap = map[string]zapcore.EncoderConfig{
|
||||||
|
"console": {
|
||||||
|
TimeKey: "time",
|
||||||
|
LevelKey: "level",
|
||||||
|
NameKey: "logger",
|
||||||
|
MessageKey: "msg",
|
||||||
|
LineEnding: zapcore.DefaultLineEnding,
|
||||||
|
EncodeLevel: zapcore.CapitalColorLevelEncoder,
|
||||||
|
EncodeTime: zapcore.RFC3339TimeEncoder,
|
||||||
|
EncodeDuration: zapcore.SecondsDurationEncoder,
|
||||||
|
},
|
||||||
|
"json": {
|
||||||
|
TimeKey: "time",
|
||||||
|
LevelKey: "level",
|
||||||
|
NameKey: "logger",
|
||||||
|
MessageKey: "msg",
|
||||||
|
LineEnding: zapcore.DefaultLineEnding,
|
||||||
|
EncodeLevel: zapcore.LowercaseLevelEncoder,
|
||||||
|
EncodeTime: zapcore.EpochMillisTimeEncoder,
|
||||||
|
EncodeDuration: zapcore.SecondsDurationEncoder,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *serverLogger) Connect(addr net.Addr, uuid string, tx uint64) {
|
||||||
|
limiterinfo, err := limiter.GetLimiter(l.Tag)
|
||||||
|
if err != nil {
|
||||||
|
l.logger.Panic("Get limiter error", zap.String("tag", l.Tag), zap.Error(err))
|
||||||
|
}
|
||||||
|
if _, r := limiterinfo.CheckLimit(format.UserTag(l.Tag, uuid), extractIPFromAddr(addr), addr.Network() == "tcp", true); r {
|
||||||
|
if userLimit, ok := limiterinfo.UserLimitInfo.Load(format.UserTag(l.Tag, uuid)); ok {
|
||||||
|
userLimit.(*limiter.UserLimitInfo).OverLimit = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if userLimit, ok := limiterinfo.UserLimitInfo.Load(format.UserTag(l.Tag, uuid)); ok {
|
||||||
|
userLimit.(*limiter.UserLimitInfo).OverLimit = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
l.logger.Info("client connected", zap.String("addr", addr.String()), zap.String("uuid", uuid), zap.Uint64("tx", tx))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *serverLogger) Disconnect(addr net.Addr, uuid string, err error) {
|
||||||
|
l.logger.Info("client disconnected", zap.String("addr", addr.String()), zap.String("uuid", uuid), zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *serverLogger) TCPRequest(addr net.Addr, uuid, reqAddr string) {
|
||||||
|
limiterinfo, err := limiter.GetLimiter(l.Tag)
|
||||||
|
if err != nil {
|
||||||
|
l.logger.Panic("Get limiter error", zap.String("tag", l.Tag), zap.Error(err))
|
||||||
|
}
|
||||||
|
if _, r := limiterinfo.CheckLimit(format.UserTag(l.Tag, uuid), extractIPFromAddr(addr), addr.Network() == "tcp", true); r {
|
||||||
|
if userLimit, ok := limiterinfo.UserLimitInfo.Load(format.UserTag(l.Tag, uuid)); ok {
|
||||||
|
userLimit.(*limiter.UserLimitInfo).OverLimit = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if userLimit, ok := limiterinfo.UserLimitInfo.Load(format.UserTag(l.Tag, uuid)); ok {
|
||||||
|
userLimit.(*limiter.UserLimitInfo).OverLimit = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
l.logger.Debug("TCP request", zap.String("addr", addr.String()), zap.String("uuid", uuid), zap.String("reqAddr", reqAddr))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *serverLogger) TCPError(addr net.Addr, uuid, reqAddr string, err error) {
|
||||||
|
if err == nil {
|
||||||
|
l.logger.Debug("TCP closed", zap.String("addr", addr.String()), zap.String("uuid", uuid), zap.String("reqAddr", reqAddr))
|
||||||
|
} else {
|
||||||
|
l.logger.Debug("TCP error", zap.String("addr", addr.String()), zap.String("uuid", uuid), zap.String("reqAddr", reqAddr), zap.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *serverLogger) UDPRequest(addr net.Addr, uuid string, sessionId uint32, reqAddr string) {
|
||||||
|
limiterinfo, err := limiter.GetLimiter(l.Tag)
|
||||||
|
if err != nil {
|
||||||
|
l.logger.Panic("Get limiter error", zap.String("tag", l.Tag), zap.Error(err))
|
||||||
|
}
|
||||||
|
if _, r := limiterinfo.CheckLimit(format.UserTag(l.Tag, uuid), extractIPFromAddr(addr), addr.Network() == "tcp", true); r {
|
||||||
|
if userLimit, ok := limiterinfo.UserLimitInfo.Load(format.UserTag(l.Tag, uuid)); ok {
|
||||||
|
userLimit.(*limiter.UserLimitInfo).OverLimit = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if userLimit, ok := limiterinfo.UserLimitInfo.Load(format.UserTag(l.Tag, uuid)); ok {
|
||||||
|
userLimit.(*limiter.UserLimitInfo).OverLimit = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
l.logger.Debug("UDP request", zap.String("addr", addr.String()), zap.String("uuid", uuid), zap.Uint32("sessionId", sessionId), zap.String("reqAddr", reqAddr))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *serverLogger) UDPError(addr net.Addr, uuid string, sessionId uint32, err error) {
|
||||||
|
if err == nil {
|
||||||
|
l.logger.Debug("UDP closed", zap.String("addr", addr.String()), zap.String("uuid", uuid), zap.Uint32("sessionId", sessionId))
|
||||||
|
} else {
|
||||||
|
l.logger.Debug("UDP error", zap.String("addr", addr.String()), zap.String("uuid", uuid), zap.Uint32("sessionId", sessionId), zap.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func initLogger(logLevel string, logFormat string) (*zap.Logger, error) {
|
||||||
|
level, ok := logLevelMap[strings.ToLower(logLevel)]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("unsupported log level: %s", logLevel)
|
||||||
|
}
|
||||||
|
enc, ok := logFormatMap[strings.ToLower(logFormat)]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("unsupported log format: %s", logFormat)
|
||||||
|
}
|
||||||
|
c := zap.Config{
|
||||||
|
Level: zap.NewAtomicLevelAt(level),
|
||||||
|
DisableCaller: true,
|
||||||
|
DisableStacktrace: true,
|
||||||
|
Encoding: strings.ToLower(logFormat),
|
||||||
|
EncoderConfig: enc,
|
||||||
|
OutputPaths: []string{"stderr"},
|
||||||
|
ErrorOutputPaths: []string{"stderr"},
|
||||||
|
}
|
||||||
|
logger, err := c.Build()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to initialize logger: %s", err)
|
||||||
|
}
|
||||||
|
return logger, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractIPFromAddr(addr net.Addr) string {
|
||||||
|
switch v := addr.(type) {
|
||||||
|
case *net.TCPAddr:
|
||||||
|
return v.IP.String()
|
||||||
|
case *net.UDPAddr:
|
||||||
|
return v.IP.String()
|
||||||
|
case *net.IPAddr:
|
||||||
|
return v.IP.String()
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
77
core/hy2/node.go
Normal file
77
core/hy2/node.go
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
package hy2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/InazumaV/V2bX/api/panel"
|
||||||
|
"github.com/InazumaV/V2bX/conf"
|
||||||
|
"github.com/apernet/hysteria/core/v2/server"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Hysteria2node struct {
|
||||||
|
Hy2server server.Server
|
||||||
|
Tag string
|
||||||
|
Logger *zap.Logger
|
||||||
|
EventLogger server.EventLogger
|
||||||
|
TrafficLogger server.TrafficLogger
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hysteria2) AddNode(tag string, info *panel.NodeInfo, config *conf.Options) error {
|
||||||
|
var err error
|
||||||
|
hyconfig := &server.Config{}
|
||||||
|
var c serverConfig
|
||||||
|
v := viper.New()
|
||||||
|
if len(config.Hysteria2ConfigPath) != 0 {
|
||||||
|
v.SetConfigFile(config.Hysteria2ConfigPath)
|
||||||
|
if err := v.ReadInConfig(); err != nil {
|
||||||
|
h.Logger.Fatal("failed to read server config", zap.Error(err))
|
||||||
|
}
|
||||||
|
if err := v.Unmarshal(&c); err != nil {
|
||||||
|
h.Logger.Fatal("failed to parse server config", zap.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
n := Hysteria2node{
|
||||||
|
Tag: tag,
|
||||||
|
Logger: h.Logger,
|
||||||
|
EventLogger: &serverLogger{
|
||||||
|
Tag: tag,
|
||||||
|
logger: h.Logger,
|
||||||
|
},
|
||||||
|
TrafficLogger: &HookServer{
|
||||||
|
Tag: tag,
|
||||||
|
logger: h.Logger,
|
||||||
|
ReportMinTrafficBytes: config.ReportMinTraffic * 1024,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
hyconfig, err = n.getHyConfig(info, config, &c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
hyconfig.Authenticator = h.Auth
|
||||||
|
s, err := server.NewServer(hyconfig)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
n.Hy2server = s
|
||||||
|
h.Hy2nodes[tag] = n
|
||||||
|
go func() {
|
||||||
|
if err := s.Serve(); err != nil {
|
||||||
|
if !strings.Contains(err.Error(), "quic: server closed") {
|
||||||
|
h.Logger.Error("Server Error", zap.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hysteria2) DelNode(tag string) error {
|
||||||
|
err := h.Hy2nodes[tag].Hy2server.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
delete(h.Hy2nodes, tag)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
238
core/hy2/serverConfig.go
Normal file
238
core/hy2/serverConfig.go
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
package hy2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/apernet/hysteria/extras/v2/outbounds"
|
||||||
|
)
|
||||||
|
|
||||||
|
type serverConfig struct {
|
||||||
|
Listen string `mapstructure:"listen"`
|
||||||
|
Obfs serverConfigObfs `mapstructure:"obfs"`
|
||||||
|
TLS *serverConfigTLS `mapstructure:"tls"`
|
||||||
|
ACME *serverConfigACME `mapstructure:"acme"`
|
||||||
|
QUIC serverConfigQUIC `mapstructure:"quic"`
|
||||||
|
Bandwidth serverConfigBandwidth `mapstructure:"bandwidth"`
|
||||||
|
IgnoreClientBandwidth bool `mapstructure:"ignoreClientBandwidth"`
|
||||||
|
SpeedTest bool `mapstructure:"speedTest"`
|
||||||
|
DisableUDP bool `mapstructure:"disableUDP"`
|
||||||
|
UDPIdleTimeout time.Duration `mapstructure:"udpIdleTimeout"`
|
||||||
|
Auth serverConfigAuth `mapstructure:"auth"`
|
||||||
|
Resolver serverConfigResolver `mapstructure:"resolver"`
|
||||||
|
Sniff serverConfigSniff `mapstructure:"sniff"`
|
||||||
|
ACL serverConfigACL `mapstructure:"acl"`
|
||||||
|
Outbounds []serverConfigOutboundEntry `mapstructure:"outbounds"`
|
||||||
|
TrafficStats serverConfigTrafficStats `mapstructure:"trafficStats"`
|
||||||
|
Masquerade serverConfigMasquerade `mapstructure:"masquerade"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type serverConfigObfsSalamander struct {
|
||||||
|
Password string `mapstructure:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type serverConfigObfs struct {
|
||||||
|
Type string `mapstructure:"type"`
|
||||||
|
Salamander serverConfigObfsSalamander `mapstructure:"salamander"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type serverConfigTLS struct {
|
||||||
|
Cert string `mapstructure:"cert"`
|
||||||
|
Key string `mapstructure:"key"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type serverConfigACME struct {
|
||||||
|
Domains []string `mapstructure:"domains"`
|
||||||
|
Email string `mapstructure:"email"`
|
||||||
|
CA string `mapstructure:"ca"`
|
||||||
|
DisableHTTP bool `mapstructure:"disableHTTP"`
|
||||||
|
DisableTLSALPN bool `mapstructure:"disableTLSALPN"`
|
||||||
|
AltHTTPPort int `mapstructure:"altHTTPPort"`
|
||||||
|
AltTLSALPNPort int `mapstructure:"altTLSALPNPort"`
|
||||||
|
Dir string `mapstructure:"dir"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type serverConfigQUIC struct {
|
||||||
|
InitStreamReceiveWindow uint64 `mapstructure:"initStreamReceiveWindow"`
|
||||||
|
MaxStreamReceiveWindow uint64 `mapstructure:"maxStreamReceiveWindow"`
|
||||||
|
InitConnectionReceiveWindow uint64 `mapstructure:"initConnReceiveWindow"`
|
||||||
|
MaxConnectionReceiveWindow uint64 `mapstructure:"maxConnReceiveWindow"`
|
||||||
|
MaxIdleTimeout time.Duration `mapstructure:"maxIdleTimeout"`
|
||||||
|
MaxIncomingStreams int64 `mapstructure:"maxIncomingStreams"`
|
||||||
|
DisablePathMTUDiscovery bool `mapstructure:"disablePathMTUDiscovery"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type serverConfigBandwidth struct {
|
||||||
|
Up string `mapstructure:"up"`
|
||||||
|
Down string `mapstructure:"down"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type serverConfigAuthHTTP struct {
|
||||||
|
URL string `mapstructure:"url"`
|
||||||
|
Insecure bool `mapstructure:"insecure"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type serverConfigAuth struct {
|
||||||
|
Type string `mapstructure:"type"`
|
||||||
|
Password string `mapstructure:"password"`
|
||||||
|
UserPass map[string]string `mapstructure:"userpass"`
|
||||||
|
HTTP serverConfigAuthHTTP `mapstructure:"http"`
|
||||||
|
Command string `mapstructure:"command"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type serverConfigResolverTCP struct {
|
||||||
|
Addr string `mapstructure:"addr"`
|
||||||
|
Timeout time.Duration `mapstructure:"timeout"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type serverConfigResolverUDP struct {
|
||||||
|
Addr string `mapstructure:"addr"`
|
||||||
|
Timeout time.Duration `mapstructure:"timeout"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type serverConfigResolverTLS struct {
|
||||||
|
Addr string `mapstructure:"addr"`
|
||||||
|
Timeout time.Duration `mapstructure:"timeout"`
|
||||||
|
SNI string `mapstructure:"sni"`
|
||||||
|
Insecure bool `mapstructure:"insecure"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type serverConfigResolverHTTPS struct {
|
||||||
|
Addr string `mapstructure:"addr"`
|
||||||
|
Timeout time.Duration `mapstructure:"timeout"`
|
||||||
|
SNI string `mapstructure:"sni"`
|
||||||
|
Insecure bool `mapstructure:"insecure"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type serverConfigResolver struct {
|
||||||
|
Type string `mapstructure:"type"`
|
||||||
|
TCP serverConfigResolverTCP `mapstructure:"tcp"`
|
||||||
|
UDP serverConfigResolverUDP `mapstructure:"udp"`
|
||||||
|
TLS serverConfigResolverTLS `mapstructure:"tls"`
|
||||||
|
HTTPS serverConfigResolverHTTPS `mapstructure:"https"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type serverConfigSniff struct {
|
||||||
|
Enable bool `mapstructure:"enable"`
|
||||||
|
Timeout time.Duration `mapstructure:"timeout"`
|
||||||
|
RewriteDomain bool `mapstructure:"rewriteDomain"`
|
||||||
|
TCPPorts string `mapstructure:"tcpPorts"`
|
||||||
|
UDPPorts string `mapstructure:"udpPorts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type serverConfigACL struct {
|
||||||
|
File string `mapstructure:"file"`
|
||||||
|
Inline []string `mapstructure:"inline"`
|
||||||
|
GeoIP string `mapstructure:"geoip"`
|
||||||
|
GeoSite string `mapstructure:"geosite"`
|
||||||
|
GeoUpdateInterval time.Duration `mapstructure:"geoUpdateInterval"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type serverConfigOutboundDirect struct {
|
||||||
|
Mode string `mapstructure:"mode"`
|
||||||
|
BindIPv4 string `mapstructure:"bindIPv4"`
|
||||||
|
BindIPv6 string `mapstructure:"bindIPv6"`
|
||||||
|
BindDevice string `mapstructure:"bindDevice"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type serverConfigOutboundSOCKS5 struct {
|
||||||
|
Addr string `mapstructure:"addr"`
|
||||||
|
Username string `mapstructure:"username"`
|
||||||
|
Password string `mapstructure:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type serverConfigOutboundHTTP struct {
|
||||||
|
URL string `mapstructure:"url"`
|
||||||
|
Insecure bool `mapstructure:"insecure"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type serverConfigOutboundEntry struct {
|
||||||
|
Name string `mapstructure:"name"`
|
||||||
|
Type string `mapstructure:"type"`
|
||||||
|
Direct serverConfigOutboundDirect `mapstructure:"direct"`
|
||||||
|
SOCKS5 serverConfigOutboundSOCKS5 `mapstructure:"socks5"`
|
||||||
|
HTTP serverConfigOutboundHTTP `mapstructure:"http"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type serverConfigTrafficStats struct {
|
||||||
|
Listen string `mapstructure:"listen"`
|
||||||
|
Secret string `mapstructure:"secret"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type serverConfigMasqueradeFile struct {
|
||||||
|
Dir string `mapstructure:"dir"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type serverConfigMasqueradeProxy struct {
|
||||||
|
URL string `mapstructure:"url"`
|
||||||
|
RewriteHost bool `mapstructure:"rewriteHost"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type serverConfigMasqueradeString struct {
|
||||||
|
Content string `mapstructure:"content"`
|
||||||
|
Headers map[string]string `mapstructure:"headers"`
|
||||||
|
StatusCode int `mapstructure:"statusCode"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type serverConfigMasquerade struct {
|
||||||
|
Type string `mapstructure:"type"`
|
||||||
|
File serverConfigMasqueradeFile `mapstructure:"file"`
|
||||||
|
Proxy serverConfigMasqueradeProxy `mapstructure:"proxy"`
|
||||||
|
String serverConfigMasqueradeString `mapstructure:"string"`
|
||||||
|
ListenHTTP string `mapstructure:"listenHTTP"`
|
||||||
|
ListenHTTPS string `mapstructure:"listenHTTPS"`
|
||||||
|
ForceHTTPS bool `mapstructure:"forceHTTPS"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func serverConfigOutboundDirectToOutbound(c serverConfigOutboundDirect) (outbounds.PluggableOutbound, error) {
|
||||||
|
var mode outbounds.DirectOutboundMode
|
||||||
|
switch strings.ToLower(c.Mode) {
|
||||||
|
case "", "auto":
|
||||||
|
mode = outbounds.DirectOutboundModeAuto
|
||||||
|
case "64":
|
||||||
|
mode = outbounds.DirectOutboundMode64
|
||||||
|
case "46":
|
||||||
|
mode = outbounds.DirectOutboundMode46
|
||||||
|
case "6":
|
||||||
|
mode = outbounds.DirectOutboundMode6
|
||||||
|
case "4":
|
||||||
|
mode = outbounds.DirectOutboundMode4
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("outbounds.direct.mode unsupported mode")
|
||||||
|
}
|
||||||
|
bindIP := len(c.BindIPv4) > 0 || len(c.BindIPv6) > 0
|
||||||
|
bindDevice := len(c.BindDevice) > 0
|
||||||
|
if bindIP && bindDevice {
|
||||||
|
return nil, fmt.Errorf("outbounds.direct cannot bind both IP and device")
|
||||||
|
}
|
||||||
|
if bindIP {
|
||||||
|
ip4, ip6 := net.ParseIP(c.BindIPv4), net.ParseIP(c.BindIPv6)
|
||||||
|
if len(c.BindIPv4) > 0 && ip4 == nil {
|
||||||
|
return nil, fmt.Errorf("outbounds.direct.bindIPv4 invalid IPv4 address")
|
||||||
|
}
|
||||||
|
if len(c.BindIPv6) > 0 && ip6 == nil {
|
||||||
|
return nil, fmt.Errorf("outbounds.direct.bindIPv6 invalid IPv6 address")
|
||||||
|
}
|
||||||
|
return outbounds.NewDirectOutboundBindToIPs(mode, ip4, ip6)
|
||||||
|
}
|
||||||
|
if bindDevice {
|
||||||
|
return outbounds.NewDirectOutboundBindToDevice(mode, c.BindDevice)
|
||||||
|
}
|
||||||
|
return outbounds.NewDirectOutboundSimple(mode), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func serverConfigOutboundSOCKS5ToOutbound(c serverConfigOutboundSOCKS5) (outbounds.PluggableOutbound, error) {
|
||||||
|
if c.Addr == "" {
|
||||||
|
return nil, fmt.Errorf("outbounds.socks5.addr empty socks5 address")
|
||||||
|
}
|
||||||
|
return outbounds.NewSOCKS5Outbound(c.Addr, c.Username, c.Password), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func serverConfigOutboundHTTPToOutbound(c serverConfigOutboundHTTP) (outbounds.PluggableOutbound, error) {
|
||||||
|
if c.URL == "" {
|
||||||
|
return nil, fmt.Errorf("outbounds.http.url empty http address")
|
||||||
|
}
|
||||||
|
return outbounds.NewHTTPOutbound(c.URL, c.Insecure)
|
||||||
|
}
|
||||||
93
core/hy2/user.go
Normal file
93
core/hy2/user.go
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
package hy2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/InazumaV/V2bX/api/panel"
|
||||||
|
"github.com/InazumaV/V2bX/common/counter"
|
||||||
|
vCore "github.com/InazumaV/V2bX/core"
|
||||||
|
"github.com/apernet/hysteria/core/v2/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ server.Authenticator = &V2bX{}
|
||||||
|
|
||||||
|
type V2bX struct {
|
||||||
|
usersMap map[string]int
|
||||||
|
mutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *V2bX) Authenticate(addr net.Addr, auth string, tx uint64) (ok bool, id string) {
|
||||||
|
v.mutex.RLock()
|
||||||
|
defer v.mutex.RUnlock()
|
||||||
|
if _, exists := v.usersMap[auth]; exists {
|
||||||
|
return true, auth
|
||||||
|
}
|
||||||
|
return false, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hysteria2) AddUsers(p *vCore.AddUsersParams) (added int, err error) {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
for _, user := range p.Users {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(u panel.UserInfo) {
|
||||||
|
defer wg.Done()
|
||||||
|
h.Auth.mutex.Lock()
|
||||||
|
h.Auth.usersMap[u.Uuid] = u.Id
|
||||||
|
h.Auth.mutex.Unlock()
|
||||||
|
}(user)
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
return len(p.Users), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hysteria2) DelUsers(users []panel.UserInfo, tag string, _ *panel.NodeInfo) error {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
for _, user := range users {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(u panel.UserInfo) {
|
||||||
|
defer wg.Done()
|
||||||
|
h.Auth.mutex.Lock()
|
||||||
|
delete(h.Auth.usersMap, u.Uuid)
|
||||||
|
h.Auth.mutex.Unlock()
|
||||||
|
}(user)
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hysteria2) GetUserTrafficSlice(tag string, reset bool) ([]panel.UserTraffic, error) {
|
||||||
|
trafficSlice := make([]panel.UserTraffic, 0)
|
||||||
|
h.Auth.mutex.RLock()
|
||||||
|
defer h.Auth.mutex.RUnlock()
|
||||||
|
if _, ok := h.Hy2nodes[tag]; !ok {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
hook := h.Hy2nodes[tag].TrafficLogger.(*HookServer)
|
||||||
|
if v, ok := hook.Counter.Load(tag); ok {
|
||||||
|
c := v.(*counter.TrafficCounter)
|
||||||
|
c.Counters.Range(func(key, value interface{}) bool {
|
||||||
|
uuid := key.(string)
|
||||||
|
traffic := value.(*counter.TrafficStorage)
|
||||||
|
up := traffic.UpCounter.Load()
|
||||||
|
down := traffic.DownCounter.Load()
|
||||||
|
if up+down > hook.ReportMinTrafficBytes {
|
||||||
|
if reset {
|
||||||
|
traffic.UpCounter.Store(0)
|
||||||
|
traffic.DownCounter.Store(0)
|
||||||
|
}
|
||||||
|
trafficSlice = append(trafficSlice, panel.UserTraffic{
|
||||||
|
UID: h.Auth.usersMap[uuid],
|
||||||
|
Upload: up,
|
||||||
|
Download: down,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
if len(trafficSlice) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return trafficSlice, nil
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
5
core/imports/hy2.go
Normal file
5
core/imports/hy2.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
//go:build hysteria2
|
||||||
|
|
||||||
|
package imports
|
||||||
|
|
||||||
|
import _ "github.com/InazumaV/V2bX/core/hy2"
|
||||||
@@ -17,8 +17,8 @@ type Core interface {
|
|||||||
AddNode(tag string, info *panel.NodeInfo, config *conf.Options) error
|
AddNode(tag string, info *panel.NodeInfo, config *conf.Options) error
|
||||||
DelNode(tag string) error
|
DelNode(tag string) error
|
||||||
AddUsers(p *AddUsersParams) (added int, err error)
|
AddUsers(p *AddUsersParams) (added int, err error)
|
||||||
GetUserTraffic(tag, uuid string, reset bool) (up int64, down int64)
|
GetUserTrafficSlice(tag string, reset bool) ([]panel.UserTraffic, error)
|
||||||
DelUsers(users []panel.UserInfo, tag string) error
|
DelUsers(users []panel.UserInfo, tag string, info *panel.NodeInfo) error
|
||||||
Protocols() []string
|
Protocols() []string
|
||||||
Type() string
|
Type() string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package core
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/hashicorp/go-multierror"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
@@ -49,14 +48,13 @@ func (s *Selector) Start() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Selector) Close() error {
|
func (s *Selector) Close() error {
|
||||||
var errs error
|
var errs []error
|
||||||
for i := range s.cores {
|
for i := range s.cores {
|
||||||
err := s.cores[i].Close()
|
if err := s.cores[i].Close(); err != nil {
|
||||||
if err != nil {
|
errs = append(errs, err)
|
||||||
errs = multierror.Append(errs, err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return errs
|
return errors.Join(errs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func isSupported(protocol string, protocols []string) bool {
|
func isSupported(protocol string, protocols []string) bool {
|
||||||
@@ -127,20 +125,20 @@ func (s *Selector) AddUsers(p *AddUsersParams) (added int, err error) {
|
|||||||
return t.(Core).AddUsers(p)
|
return t.(Core).AddUsers(p)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Selector) GetUserTraffic(tag, uuid string, reset bool) (up int64, down int64) {
|
func (s *Selector) GetUserTrafficSlice(tag string, reset bool) ([]panel.UserTraffic, error) {
|
||||||
t, e := s.nodes.Load(tag)
|
t, e := s.nodes.Load(tag)
|
||||||
if !e {
|
if !e {
|
||||||
return 0, 0
|
return nil, errors.New("the node is not have")
|
||||||
}
|
}
|
||||||
return t.(Core).GetUserTraffic(tag, uuid, reset)
|
return t.(Core).GetUserTrafficSlice(tag, reset)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Selector) DelUsers(users []panel.UserInfo, tag string) error {
|
func (s *Selector) DelUsers(users []panel.UserInfo, tag string, info *panel.NodeInfo) error {
|
||||||
t, e := s.nodes.Load(tag)
|
t, e := s.nodes.Load(tag)
|
||||||
if !e {
|
if !e {
|
||||||
return errors.New("the node is not have")
|
return errors.New("the node is not have")
|
||||||
}
|
}
|
||||||
return t.(Core).DelUsers(users, tag)
|
return t.(Core).DelUsers(users, tag, info)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Selector) Protocols() []string {
|
func (s *Selector) Protocols() []string {
|
||||||
|
|||||||
@@ -1,85 +0,0 @@
|
|||||||
package sing
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"github.com/InazumaV/V2bX/api/panel"
|
|
||||||
"github.com/goccy/go-json"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func updateDNSConfig(node *panel.NodeInfo) (err error) {
|
|
||||||
dnsPath := os.Getenv("SING_DNS_PATH")
|
|
||||||
if len(node.RawDNS.DNSJson) != 0 {
|
|
||||||
var prettyJSON bytes.Buffer
|
|
||||||
if err := json.Indent(&prettyJSON, node.RawDNS.DNSJson, "", " "); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = saveDnsConfig(prettyJSON.Bytes(), dnsPath)
|
|
||||||
} else if len(node.RawDNS.DNSMap) != 0 {
|
|
||||||
dnsConfig := DNSConfig{
|
|
||||||
Servers: []map[string]interface{}{
|
|
||||||
{
|
|
||||||
"tag": "default",
|
|
||||||
"address": "https://8.8.8.8/dns-query",
|
|
||||||
"detour": "direct",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for id, value := range node.RawDNS.DNSMap {
|
|
||||||
dnsConfig.Servers = append(dnsConfig.Servers,
|
|
||||||
map[string]interface{}{
|
|
||||||
"tag": id,
|
|
||||||
"address": value["address"],
|
|
||||||
"address_resolver": "default",
|
|
||||||
"detour": "direct",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
rule := map[string]interface{}{
|
|
||||||
"server": id,
|
|
||||||
"disable_cache": true,
|
|
||||||
}
|
|
||||||
for _, ruleType := range []string{"domain_suffix", "domain_keyword", "domain_regex", "geosite"} {
|
|
||||||
var domains []string
|
|
||||||
for _, v := range value["domains"].([]string) {
|
|
||||||
split := strings.SplitN(v, ":", 2)
|
|
||||||
prefix := strings.ToLower(split[0])
|
|
||||||
if prefix == ruleType || (prefix == "domain" && ruleType == "domain_suffix") {
|
|
||||||
if len(split) > 1 {
|
|
||||||
domains = append(domains, split[1])
|
|
||||||
}
|
|
||||||
if len(domains) > 0 {
|
|
||||||
rule[ruleType] = domains
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dnsConfig.Rules = append(dnsConfig.Rules, rule)
|
|
||||||
}
|
|
||||||
dnsConfigJSON, err := json.MarshalIndent(dnsConfig, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
log.WithField("err", err).Error("Error marshaling dnsConfig to JSON")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = saveDnsConfig(dnsConfigJSON, dnsPath)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func saveDnsConfig(dns []byte, dnsPath string) (err error) {
|
|
||||||
currentData, err := os.ReadFile(dnsPath)
|
|
||||||
if err != nil {
|
|
||||||
log.WithField("err", err).Error("Failed to read SING_DNS_PATH")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !bytes.Equal(currentData, dns) {
|
|
||||||
if err = os.Truncate(dnsPath, 0); err != nil {
|
|
||||||
log.WithField("err", err).Error("Failed to clear SING DNS PATH file")
|
|
||||||
}
|
|
||||||
if err = os.WriteFile(dnsPath, dns, 0644); err != nil {
|
|
||||||
log.WithField("err", err).Error("Failed to write DNS to SING DNS PATH file")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
@@ -2,12 +2,11 @@ package sing
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"io"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/urltest"
|
"github.com/InazumaV/V2bX/common/format"
|
||||||
|
|
||||||
"github.com/InazumaV/V2bX/common/rate"
|
"github.com/InazumaV/V2bX/common/rate"
|
||||||
|
|
||||||
"github.com/InazumaV/V2bX/limiter"
|
"github.com/InazumaV/V2bX/limiter"
|
||||||
@@ -18,204 +17,108 @@ import (
|
|||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var _ adapter.ConnectionTracker = (*HookServer)(nil)
|
||||||
|
|
||||||
type HookServer struct {
|
type HookServer struct {
|
||||||
EnableConnClear bool
|
counter sync.Map //map[string]*counter.TrafficCounter
|
||||||
counter sync.Map
|
|
||||||
connClears sync.Map
|
|
||||||
}
|
|
||||||
|
|
||||||
type ConnClear struct {
|
|
||||||
lock sync.RWMutex
|
|
||||||
conns map[int]io.Closer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ConnClear) AddConn(cn io.Closer) (key int) {
|
|
||||||
c.lock.Lock()
|
|
||||||
defer c.lock.Unlock()
|
|
||||||
key = len(c.conns)
|
|
||||||
c.conns[key] = cn
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ConnClear) DelConn(key int) {
|
|
||||||
c.lock.Lock()
|
|
||||||
defer c.lock.Unlock()
|
|
||||||
delete(c.conns, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ConnClear) ClearConn() {
|
|
||||||
c.lock.Lock()
|
|
||||||
defer c.lock.Unlock()
|
|
||||||
for _, c := range c.conns {
|
|
||||||
c.Close()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HookServer) ModeList() []string {
|
func (h *HookServer) ModeList() []string {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHookServer(enableClear bool) *HookServer {
|
func (h *HookServer) RoutedConnection(_ context.Context, conn net.Conn, m adapter.InboundContext, _ adapter.Rule, _ adapter.Outbound) net.Conn {
|
||||||
return &HookServer{
|
|
||||||
EnableConnClear: enableClear,
|
|
||||||
counter: sync.Map{},
|
|
||||||
connClears: sync.Map{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *HookServer) Start() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *HookServer) Close() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *HookServer) PreStart() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *HookServer) RoutedConnection(_ context.Context, conn net.Conn, m adapter.InboundContext, _ adapter.Rule) (net.Conn, adapter.Tracker) {
|
|
||||||
t := &Tracker{}
|
|
||||||
l, err := limiter.GetLimiter(m.Inbound)
|
l, err := limiter.GetLimiter(m.Inbound)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("get limiter for ", m.Inbound, " error: ", err)
|
log.Warn("get limiter for ", m.Inbound, " error: ", err)
|
||||||
return conn, t
|
return conn
|
||||||
}
|
|
||||||
if l.CheckDomainRule(m.Domain) {
|
|
||||||
conn.Close()
|
|
||||||
log.Error("[", m.Inbound, "] ",
|
|
||||||
"Limited ", m.User, " access to ", m.Domain, " by domain rule")
|
|
||||||
return conn, t
|
|
||||||
}
|
|
||||||
if l.CheckProtocolRule(m.Protocol) {
|
|
||||||
conn.Close()
|
|
||||||
log.Error("[", m.Inbound, "] ",
|
|
||||||
"Limited ", m.User, " use ", m.Domain, " by protocol rule")
|
|
||||||
return conn, t
|
|
||||||
}
|
}
|
||||||
|
taguuid := format.UserTag(m.Inbound, m.User)
|
||||||
ip := m.Source.Addr.String()
|
ip := m.Source.Addr.String()
|
||||||
if b, r := l.CheckLimit(m.User, ip, true); r {
|
if b, r := l.CheckLimit(taguuid, ip, true, true); r {
|
||||||
conn.Close()
|
conn.Close()
|
||||||
log.Error("[", m.Inbound, "] ", "Limited ", m.User, " by ip or conn")
|
log.Error("[", m.Inbound, "] ", "Limited ", m.User, " by ip or conn")
|
||||||
return conn, t
|
return conn
|
||||||
} else if b != nil {
|
} else if b != nil {
|
||||||
conn = rate.NewConnRateLimiter(conn, b)
|
conn = rate.NewConnRateLimiter(conn, b)
|
||||||
}
|
}
|
||||||
t.AddLeave(func() {
|
if l != nil {
|
||||||
l.ConnLimiter.DelConnCount(m.User, ip)
|
destStr := m.Destination.AddrString()
|
||||||
})
|
protocol := m.Protocol
|
||||||
if h.EnableConnClear {
|
if l.CheckDomainRule(destStr) {
|
||||||
var key int
|
log.Error(fmt.Sprintf(
|
||||||
cc := &ConnClear{
|
"User %s access domain %s reject by rule",
|
||||||
conns: map[int]io.Closer{
|
m.User,
|
||||||
0: conn,
|
destStr))
|
||||||
},
|
conn.Close()
|
||||||
|
return conn
|
||||||
}
|
}
|
||||||
if v, ok := h.connClears.LoadOrStore(m.Inbound+m.User, cc); ok {
|
if len(protocol) != 0 {
|
||||||
cc = v.(*ConnClear)
|
if l.CheckProtocolRule(protocol) {
|
||||||
key = cc.AddConn(conn)
|
log.Error(fmt.Sprintf(
|
||||||
|
"User %s access protocol %s reject by rule",
|
||||||
|
m.User,
|
||||||
|
protocol))
|
||||||
|
conn.Close()
|
||||||
|
return conn
|
||||||
|
}
|
||||||
}
|
}
|
||||||
t.AddLeave(func() {
|
|
||||||
cc.DelConn(key)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
if c, ok := h.counter.Load(m.Inbound); ok {
|
var t *counter.TrafficCounter
|
||||||
return counter.NewConnCounter(conn, c.(*counter.TrafficCounter).GetCounter(m.User)), t
|
if c, ok := h.counter.Load(m.Inbound); !ok {
|
||||||
|
t = counter.NewTrafficCounter()
|
||||||
|
h.counter.Store(m.Inbound, t)
|
||||||
} else {
|
} else {
|
||||||
c := counter.NewTrafficCounter()
|
t = c.(*counter.TrafficCounter)
|
||||||
h.counter.Store(m.Inbound, c)
|
|
||||||
return counter.NewConnCounter(conn, c.GetCounter(m.User)), t
|
|
||||||
}
|
}
|
||||||
|
conn = counter.NewConnCounter(conn, t.GetCounter(m.User))
|
||||||
|
return conn
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HookServer) RoutedPacketConnection(_ context.Context, conn N.PacketConn, m adapter.InboundContext, _ adapter.Rule) (N.PacketConn, adapter.Tracker) {
|
func (h *HookServer) RoutedPacketConnection(_ context.Context, conn N.PacketConn, m adapter.InboundContext, _ adapter.Rule, _ adapter.Outbound) N.PacketConn {
|
||||||
t := &Tracker{}
|
|
||||||
l, err := limiter.GetLimiter(m.Inbound)
|
l, err := limiter.GetLimiter(m.Inbound)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("get limiter for ", m.Inbound, " error: ", err)
|
log.Warn("get limiter for ", m.Inbound, " error: ", err)
|
||||||
return conn, t
|
return conn
|
||||||
}
|
|
||||||
if l.CheckDomainRule(m.Domain) {
|
|
||||||
conn.Close()
|
|
||||||
log.Error("[", m.Inbound, "] ",
|
|
||||||
"Limited ", m.User, " access to ", m.Domain, " by domain rule")
|
|
||||||
return conn, t
|
|
||||||
}
|
|
||||||
if l.CheckProtocolRule(m.Protocol) {
|
|
||||||
conn.Close()
|
|
||||||
log.Error("[", m.Inbound, "] ",
|
|
||||||
"Limited ", m.User, " use ", m.Domain, " by protocol rule")
|
|
||||||
return conn, t
|
|
||||||
}
|
}
|
||||||
ip := m.Source.Addr.String()
|
ip := m.Source.Addr.String()
|
||||||
if b, r := l.CheckLimit(m.User, ip, true); r {
|
taguuid := format.UserTag(m.Inbound, m.User)
|
||||||
|
if b, r := l.CheckLimit(taguuid, ip, false, false); r {
|
||||||
conn.Close()
|
conn.Close()
|
||||||
log.Error("[", m.Inbound, "] ", "Limited ", m.User, " by ip or conn")
|
log.Error("[", m.Inbound, "] ", "Limited ", m.User, " by ip or conn")
|
||||||
return conn, t
|
return conn
|
||||||
} else if b != nil {
|
} else if b != nil {
|
||||||
conn = rate.NewPacketConnCounter(conn, b)
|
//conn = rate.NewPacketConnCounter(conn, b)
|
||||||
}
|
}
|
||||||
if h.EnableConnClear {
|
if l != nil {
|
||||||
var key int
|
destStr := m.Destination.AddrString()
|
||||||
cc := &ConnClear{
|
protocol := m.Destination.Network()
|
||||||
conns: map[int]io.Closer{
|
if l.CheckDomainRule(destStr) {
|
||||||
0: conn,
|
log.Error(fmt.Sprintf(
|
||||||
},
|
"User %s access domain %s reject by rule",
|
||||||
|
m.User,
|
||||||
|
destStr))
|
||||||
|
conn.Close()
|
||||||
|
return conn
|
||||||
}
|
}
|
||||||
if v, ok := h.connClears.LoadOrStore(m.Inbound+m.User, cc); ok {
|
if len(protocol) != 0 {
|
||||||
cc = v.(*ConnClear)
|
if l.CheckProtocolRule(protocol) {
|
||||||
key = cc.AddConn(conn)
|
log.Error(fmt.Sprintf(
|
||||||
|
"User %s access protocol %s reject by rule",
|
||||||
|
m.User,
|
||||||
|
protocol))
|
||||||
|
conn.Close()
|
||||||
|
return conn
|
||||||
|
}
|
||||||
}
|
}
|
||||||
t.AddLeave(func() {
|
|
||||||
cc.DelConn(key)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
if c, ok := h.counter.Load(m.Inbound); ok {
|
var t *counter.TrafficCounter
|
||||||
return counter.NewPacketConnCounter(conn, c.(*counter.TrafficCounter).GetCounter(m.User)), t
|
if c, ok := h.counter.Load(m.Inbound); !ok {
|
||||||
|
t = counter.NewTrafficCounter()
|
||||||
|
h.counter.Store(m.Inbound, t)
|
||||||
} else {
|
} else {
|
||||||
c := counter.NewTrafficCounter()
|
t = c.(*counter.TrafficCounter)
|
||||||
h.counter.Store(m.Inbound, c)
|
|
||||||
return counter.NewPacketConnCounter(conn, c.GetCounter(m.User)), t
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// not need
|
|
||||||
|
|
||||||
func (h *HookServer) Mode() string {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
func (h *HookServer) StoreSelected() bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
func (h *HookServer) CacheFile() adapter.CacheFile {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (h *HookServer) HistoryStorage() *urltest.HistoryStorage {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *HookServer) StoreFakeIP() bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *HookServer) ClearConn(inbound string, user string) {
|
|
||||||
if v, ok := h.connClears.Load(inbound + user); ok {
|
|
||||||
v.(*ConnClear).ClearConn()
|
|
||||||
h.connClears.Delete(inbound + user)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Tracker struct {
|
|
||||||
l []func()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Tracker) AddLeave(f func()) {
|
|
||||||
t.l = append(t.l, f)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Tracker) Leave() {
|
|
||||||
for i := range t.l {
|
|
||||||
t.l[i]()
|
|
||||||
}
|
}
|
||||||
|
conn = counter.NewPacketConnCounter(conn, t.GetCounter(m.User))
|
||||||
|
return conn
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,12 +10,13 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
"github.com/InazumaV/V2bX/api/panel"
|
"github.com/InazumaV/V2bX/api/panel"
|
||||||
"github.com/InazumaV/V2bX/conf"
|
"github.com/InazumaV/V2bX/conf"
|
||||||
"github.com/goccy/go-json"
|
|
||||||
"github.com/sagernet/sing-box/inbound"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
F "github.com/sagernet/sing/common/format"
|
F "github.com/sagernet/sing/common/format"
|
||||||
|
"github.com/sagernet/sing/common/json/badoption"
|
||||||
)
|
)
|
||||||
|
|
||||||
type HttpNetworkConfig struct {
|
type HttpNetworkConfig struct {
|
||||||
@@ -26,30 +27,51 @@ type HttpNetworkConfig struct {
|
|||||||
} `json:"header"`
|
} `json:"header"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type HttpRequest struct {
|
||||||
|
Version string `json:"version"`
|
||||||
|
Method string `json:"method"`
|
||||||
|
Path []string `json:"path"`
|
||||||
|
Headers struct {
|
||||||
|
Host []string `json:"Host"`
|
||||||
|
} `json:"headers"`
|
||||||
|
}
|
||||||
|
|
||||||
type WsNetworkConfig struct {
|
type WsNetworkConfig struct {
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
Headers map[string]string `json:"headers"`
|
Headers map[string]string `json:"headers"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GrpcNetworkConfig struct {
|
||||||
|
ServiceName string `json:"serviceName"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HttpupgradeNetworkConfig struct {
|
||||||
|
Path string `json:"path"`
|
||||||
|
Host string `json:"host"`
|
||||||
|
}
|
||||||
|
|
||||||
func getInboundOptions(tag string, info *panel.NodeInfo, c *conf.Options) (option.Inbound, error) {
|
func getInboundOptions(tag string, info *panel.NodeInfo, c *conf.Options) (option.Inbound, error) {
|
||||||
addr, err := netip.ParseAddr(c.ListenIP)
|
addr, err := netip.ParseAddr(c.ListenIP)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return option.Inbound{}, fmt.Errorf("the listen ip not vail")
|
return option.Inbound{}, fmt.Errorf("the listen ip not vail")
|
||||||
}
|
}
|
||||||
var domainStrategy option.DomainStrategy
|
|
||||||
if c.SingOptions.EnableDNS {
|
|
||||||
domainStrategy = c.SingOptions.DomainStrategy
|
|
||||||
}
|
|
||||||
listen := option.ListenOptions{
|
listen := option.ListenOptions{
|
||||||
Listen: (*option.ListenAddress)(&addr),
|
Listen: (*badoption.Addr)(&addr),
|
||||||
ListenPort: uint16(info.Common.ServerPort),
|
ListenPort: uint16(info.Common.ServerPort),
|
||||||
ProxyProtocol: c.SingOptions.EnableProxyProtocol,
|
TCPFastOpen: c.SingOptions.TCPFastOpen,
|
||||||
TCPFastOpen: c.SingOptions.TCPFastOpen,
|
}
|
||||||
InboundOptions: option.InboundOptions{
|
var multiplex *option.InboundMultiplexOptions
|
||||||
SniffEnabled: c.SingOptions.SniffEnabled,
|
if c.SingOptions.Multiplex != nil {
|
||||||
SniffOverrideDestination: c.SingOptions.SniffOverrideDestination,
|
multiplexOption := option.InboundMultiplexOptions{
|
||||||
DomainStrategy: domainStrategy,
|
Enabled: c.SingOptions.Multiplex.Enabled,
|
||||||
},
|
Padding: c.SingOptions.Multiplex.Padding,
|
||||||
|
Brutal: &option.BrutalOptions{
|
||||||
|
Enabled: c.SingOptions.Multiplex.Brutal.Enabled,
|
||||||
|
UpMbps: c.SingOptions.Multiplex.Brutal.UpMbps,
|
||||||
|
DownMbps: c.SingOptions.Multiplex.Brutal.DownMbps,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
multiplex = &multiplexOption
|
||||||
}
|
}
|
||||||
var tls option.InboundTLSOptions
|
var tls option.InboundTLSOptions
|
||||||
switch info.Security {
|
switch info.Security {
|
||||||
@@ -82,14 +104,14 @@ func getInboundOptions(tag string, info *panel.NodeInfo, c *conf.Options) (optio
|
|||||||
Enabled: true,
|
Enabled: true,
|
||||||
ShortID: []string{v.TlsSettings.ShortId},
|
ShortID: []string{v.TlsSettings.ShortId},
|
||||||
PrivateKey: v.TlsSettings.PrivateKey,
|
PrivateKey: v.TlsSettings.PrivateKey,
|
||||||
Xver: v.TlsSettings.Xver,
|
Xver: uint8(v.TlsSettings.Xver),
|
||||||
Handshake: option.InboundRealityHandshakeOptions{
|
Handshake: option.InboundRealityHandshakeOptions{
|
||||||
ServerOptions: option.ServerOptions{
|
ServerOptions: option.ServerOptions{
|
||||||
Server: dest,
|
Server: dest,
|
||||||
ServerPort: uint16(port),
|
ServerPort: uint16(port),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
MaxTimeDifference: option.Duration(mtd),
|
MaxTimeDifference: badoption.Duration(mtd),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
in := option.Inbound{
|
in := option.Inbound{
|
||||||
@@ -109,10 +131,19 @@ func getInboundOptions(tag string, info *panel.NodeInfo, c *conf.Options) (optio
|
|||||||
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)
|
||||||
}
|
}
|
||||||
|
//Todo fix http options
|
||||||
if network.Header.Type == "http" {
|
if network.Header.Type == "http" {
|
||||||
t.Type = network.Header.Type
|
t.Type = network.Header.Type
|
||||||
//Todo fix http options
|
var request HttpRequest
|
||||||
//t.HTTPOptions.Host =
|
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 {
|
} else {
|
||||||
t.Type = ""
|
t.Type = ""
|
||||||
}
|
}
|
||||||
@@ -123,7 +154,7 @@ func getInboundOptions(tag string, info *panel.NodeInfo, c *conf.Options) (optio
|
|||||||
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{}
|
||||||
@@ -138,9 +169,9 @@ func getInboundOptions(tag string, info *panel.NodeInfo, c *conf.Options) (optio
|
|||||||
}
|
}
|
||||||
path = u.Path
|
path = u.Path
|
||||||
ed, _ = strconv.Atoi(u.Query().Get("ed"))
|
ed, _ = strconv.Atoi(u.Query().Get("ed"))
|
||||||
headers = make(map[string]option.Listable[string], len(network.Headers))
|
headers = make(map[string]badoption.Listable[string], len(network.Headers))
|
||||||
for k, v := range network.Headers {
|
for k, v := range network.Headers {
|
||||||
headers[k] = option.Listable[string]{
|
headers[k] = badoption.Listable[string]{
|
||||||
v,
|
v,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -152,30 +183,48 @@ func getInboundOptions(tag string, info *panel.NodeInfo, c *conf.Options) (optio
|
|||||||
Headers: headers,
|
Headers: headers,
|
||||||
}
|
}
|
||||||
case "grpc":
|
case "grpc":
|
||||||
|
network := GrpcNetworkConfig{}
|
||||||
if len(n.NetworkSettings) != 0 {
|
if len(n.NetworkSettings) != 0 {
|
||||||
err := json.Unmarshal(n.NetworkSettings, &t.GRPCOptions)
|
err := json.Unmarshal(n.NetworkSettings, &network)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return option.Inbound{}, fmt.Errorf("decode NetworkSettings error: %s", err)
|
return option.Inbound{}, fmt.Errorf("decode NetworkSettings error: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
t.GRPCOptions = option.V2RayGRPCOptions{
|
||||||
|
ServiceName: network.ServiceName,
|
||||||
|
}
|
||||||
|
case "httpupgrade":
|
||||||
|
network := HttpupgradeNetworkConfig{}
|
||||||
|
if len(n.NetworkSettings) != 0 {
|
||||||
|
err := json.Unmarshal(n.NetworkSettings, &network)
|
||||||
|
if err != nil {
|
||||||
|
return option.Inbound{}, fmt.Errorf("decode NetworkSettings error: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.HTTPUpgradeOptions = option.V2RayHTTPUpgradeOptions{
|
||||||
|
Path: network.Path,
|
||||||
|
Host: network.Host,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if info.Type == "vless" {
|
if info.Type == "vless" {
|
||||||
in.Type = "vless"
|
in.Type = "vless"
|
||||||
in.VLESSOptions = option.VLESSInboundOptions{
|
in.Options = &option.VLESSInboundOptions{
|
||||||
ListenOptions: listen,
|
ListenOptions: listen,
|
||||||
InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{
|
InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{
|
||||||
TLS: &tls,
|
TLS: &tls,
|
||||||
},
|
},
|
||||||
Transport: &t,
|
Transport: &t,
|
||||||
|
Multiplex: multiplex,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
in.Type = "vmess"
|
in.Type = "vmess"
|
||||||
in.VMessOptions = option.VMessInboundOptions{
|
in.Options = &option.VMessInboundOptions{
|
||||||
ListenOptions: listen,
|
ListenOptions: listen,
|
||||||
InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{
|
InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{
|
||||||
TLS: &tls,
|
TLS: &tls,
|
||||||
},
|
},
|
||||||
Transport: &t,
|
Transport: &t,
|
||||||
|
Multiplex: multiplex,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case "shadowsocks":
|
case "shadowsocks":
|
||||||
@@ -185,39 +234,96 @@ func getInboundOptions(tag string, info *panel.NodeInfo, c *conf.Options) (optio
|
|||||||
switch n.Cipher {
|
switch n.Cipher {
|
||||||
case "2022-blake3-aes-128-gcm":
|
case "2022-blake3-aes-128-gcm":
|
||||||
keyLength = 16
|
keyLength = 16
|
||||||
case "2022-blake3-aes-256-gcm":
|
case "2022-blake3-aes-256-gcm", "2022-blake3-chacha20-poly1305":
|
||||||
keyLength = 32
|
keyLength = 32
|
||||||
default:
|
default:
|
||||||
keyLength = 16
|
keyLength = 16
|
||||||
}
|
}
|
||||||
in.ShadowsocksOptions = option.ShadowsocksInboundOptions{
|
ssoption := &option.ShadowsocksInboundOptions{
|
||||||
ListenOptions: listen,
|
ListenOptions: listen,
|
||||||
Method: n.Cipher,
|
Method: n.Cipher,
|
||||||
|
Multiplex: multiplex,
|
||||||
}
|
}
|
||||||
p := make([]byte, keyLength)
|
p := make([]byte, keyLength)
|
||||||
_, _ = rand.Read(p)
|
_, _ = rand.Read(p)
|
||||||
randomPasswd := string(p)
|
randomPasswd := string(p)
|
||||||
if strings.Contains(n.Cipher, "2022") {
|
if strings.Contains(n.Cipher, "2022") {
|
||||||
in.ShadowsocksOptions.Password = n.ServerKey
|
ssoption.Password = n.ServerKey
|
||||||
randomPasswd = base64.StdEncoding.EncodeToString([]byte(randomPasswd))
|
randomPasswd = base64.StdEncoding.EncodeToString([]byte(randomPasswd))
|
||||||
}
|
}
|
||||||
in.ShadowsocksOptions.Users = []option.ShadowsocksUser{{
|
ssoption.Users = []option.ShadowsocksUser{{
|
||||||
Password: randomPasswd,
|
Password: randomPasswd,
|
||||||
}}
|
}}
|
||||||
|
in.Options = ssoption
|
||||||
case "trojan":
|
case "trojan":
|
||||||
|
n := info.Trojan
|
||||||
|
t := option.V2RayTransportOptions{
|
||||||
|
Type: n.Network,
|
||||||
|
}
|
||||||
|
switch n.Network {
|
||||||
|
case "tcp":
|
||||||
|
t.Type = ""
|
||||||
|
case "ws":
|
||||||
|
var (
|
||||||
|
path string
|
||||||
|
ed int
|
||||||
|
headers map[string]badoption.Listable[string]
|
||||||
|
)
|
||||||
|
if len(n.NetworkSettings) != 0 {
|
||||||
|
network := WsNetworkConfig{}
|
||||||
|
err := json.Unmarshal(n.NetworkSettings, &network)
|
||||||
|
if err != nil {
|
||||||
|
return option.Inbound{}, fmt.Errorf("decode NetworkSettings error: %s", err)
|
||||||
|
}
|
||||||
|
var u *url.URL
|
||||||
|
u, err = url.Parse(network.Path)
|
||||||
|
if err != nil {
|
||||||
|
return option.Inbound{}, fmt.Errorf("parse path error: %s", err)
|
||||||
|
}
|
||||||
|
path = u.Path
|
||||||
|
ed, _ = strconv.Atoi(u.Query().Get("ed"))
|
||||||
|
headers = make(map[string]badoption.Listable[string], len(network.Headers))
|
||||||
|
for k, v := range network.Headers {
|
||||||
|
headers[k] = badoption.Listable[string]{
|
||||||
|
v,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.WebsocketOptions = option.V2RayWebsocketOptions{
|
||||||
|
Path: path,
|
||||||
|
EarlyDataHeaderName: "Sec-WebSocket-Protocol",
|
||||||
|
MaxEarlyData: uint32(ed),
|
||||||
|
Headers: headers,
|
||||||
|
}
|
||||||
|
case "grpc":
|
||||||
|
network := GrpcNetworkConfig{}
|
||||||
|
if len(n.NetworkSettings) != 0 {
|
||||||
|
err := json.Unmarshal(n.NetworkSettings, &network)
|
||||||
|
if err != nil {
|
||||||
|
return option.Inbound{}, fmt.Errorf("decode NetworkSettings error: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.GRPCOptions = option.V2RayGRPCOptions{
|
||||||
|
ServiceName: network.ServiceName,
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
t.Type = ""
|
||||||
|
}
|
||||||
in.Type = "trojan"
|
in.Type = "trojan"
|
||||||
in.TrojanOptions = option.TrojanInboundOptions{
|
trojanoption := &option.TrojanInboundOptions{
|
||||||
ListenOptions: listen,
|
ListenOptions: listen,
|
||||||
InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{
|
InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{
|
||||||
TLS: &tls,
|
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),
|
||||||
}
|
}
|
||||||
@@ -225,12 +331,33 @@ func getInboundOptions(tag string, info *panel.NodeInfo, c *conf.Options) (optio
|
|||||||
fallbackForALPNMap := c.SingOptions.FallBackConfigs.FallBackForALPN
|
fallbackForALPNMap := c.SingOptions.FallBackConfigs.FallBackForALPN
|
||||||
fallbackForALPN := make(map[string]*option.ServerOptions, len(fallbackForALPNMap))
|
fallbackForALPN := make(map[string]*option.ServerOptions, len(fallbackForALPNMap))
|
||||||
if err := processFallback(c, fallbackForALPN); err == nil {
|
if err := processFallback(c, fallbackForALPN); err == nil {
|
||||||
in.TrojanOptions.FallbackForALPN = fallbackForALPN
|
trojanoption.FallbackForALPN = fallbackForALPN
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
in.Options = trojanoption
|
||||||
|
case "tuic":
|
||||||
|
in.Type = "tuic"
|
||||||
|
tls.ALPN = append(tls.ALPN, "h3")
|
||||||
|
in.Options = &option.TUICInboundOptions{
|
||||||
|
ListenOptions: listen,
|
||||||
|
CongestionControl: info.Tuic.CongestionControl,
|
||||||
|
ZeroRTTHandshake: info.Tuic.ZeroRTTHandshake,
|
||||||
|
InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{
|
||||||
|
TLS: &tls,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
case "anytls":
|
||||||
|
in.Type = "anytls"
|
||||||
|
in.Options = &option.AnyTLSInboundOptions{
|
||||||
|
ListenOptions: listen,
|
||||||
|
PaddingScheme: info.AnyTls.PaddingScheme,
|
||||||
|
InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{
|
||||||
|
TLS: &tls,
|
||||||
|
},
|
||||||
|
}
|
||||||
case "hysteria":
|
case "hysteria":
|
||||||
in.Type = "hysteria"
|
in.Type = "hysteria"
|
||||||
in.HysteriaOptions = option.HysteriaInboundOptions{
|
in.Options = &option.HysteriaInboundOptions{
|
||||||
ListenOptions: listen,
|
ListenOptions: listen,
|
||||||
UpMbps: info.Hysteria.UpMbps,
|
UpMbps: info.Hysteria.UpMbps,
|
||||||
DownMbps: info.Hysteria.DownMbps,
|
DownMbps: info.Hysteria.DownMbps,
|
||||||
@@ -253,11 +380,12 @@ func getInboundOptions(tag string, info *panel.NodeInfo, c *conf.Options) (optio
|
|||||||
Password: info.Hysteria2.ObfsType,
|
Password: info.Hysteria2.ObfsType,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
in.Hysteria2Options = option.Hysteria2InboundOptions{
|
in.Options = &option.Hysteria2InboundOptions{
|
||||||
ListenOptions: listen,
|
ListenOptions: listen,
|
||||||
UpMbps: info.Hysteria2.UpMbps,
|
UpMbps: info.Hysteria2.UpMbps,
|
||||||
DownMbps: info.Hysteria2.DownMbps,
|
DownMbps: info.Hysteria2.DownMbps,
|
||||||
Obfs: obfs,
|
IgnoreClientBandwidth: info.Hysteria2.Ignore_Client_Bandwidth,
|
||||||
|
Obfs: obfs,
|
||||||
InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{
|
InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{
|
||||||
TLS: &tls,
|
TLS: &tls,
|
||||||
},
|
},
|
||||||
@@ -267,31 +395,21 @@ func getInboundOptions(tag string, info *panel.NodeInfo, c *conf.Options) (optio
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *Sing) AddNode(tag string, info *panel.NodeInfo, config *conf.Options) error {
|
func (b *Sing) AddNode(tag string, info *panel.NodeInfo, config *conf.Options) error {
|
||||||
err := updateDNSConfig(info)
|
b.nodeReportMinTrafficBytes[tag] = config.ReportMinTraffic * 1024
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("build dns error: %s", err)
|
|
||||||
}
|
|
||||||
c, err := getInboundOptions(tag, info, config)
|
c, err := getInboundOptions(tag, info, config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
in := b.box.Inbound()
|
||||||
in, err := inbound.New(
|
err = in.Create(
|
||||||
b.ctx,
|
b.ctx,
|
||||||
b.box.Router(),
|
b.box.Router(),
|
||||||
b.logFactory.NewLogger(F.ToString("inbound/", c.Type, "[", tag, "]")),
|
b.logFactory.NewLogger(F.ToString("inbound/", c.Type, "[", tag, "]")),
|
||||||
c,
|
tag,
|
||||||
nil,
|
c.Type,
|
||||||
|
c.Options,
|
||||||
)
|
)
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("init inbound error: %s", err)
|
|
||||||
}
|
|
||||||
err = in.Start()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("start inbound error: %s", err)
|
|
||||||
}
|
|
||||||
b.inbounds[tag] = in
|
|
||||||
err = b.router.AddInbound(in)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("add inbound error: %s", err)
|
return fmt.Errorf("add inbound error: %s", err)
|
||||||
}
|
}
|
||||||
@@ -299,11 +417,8 @@ func (b *Sing) AddNode(tag string, info *panel.NodeInfo, config *conf.Options) e
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *Sing) DelNode(tag string) error {
|
func (b *Sing) DelNode(tag string) error {
|
||||||
err := b.inbounds[tag].Close()
|
in := b.box.Inbound()
|
||||||
if err != nil {
|
err := in.Remove(tag)
|
||||||
return fmt.Errorf("close inbound error: %s", err)
|
|
||||||
}
|
|
||||||
err = b.router.DelInbound(tag)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("delete inbound error: %s", err)
|
return fmt.Errorf("delete inbound error: %s", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,15 +4,17 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/include"
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
|
|
||||||
"github.com/InazumaV/V2bX/conf"
|
"github.com/InazumaV/V2bX/conf"
|
||||||
vCore "github.com/InazumaV/V2bX/core"
|
vCore "github.com/InazumaV/V2bX/core"
|
||||||
"github.com/goccy/go-json"
|
|
||||||
box "github.com/sagernet/sing-box"
|
box "github.com/sagernet/sing-box"
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
|
"github.com/sagernet/sing/common/json"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ vCore.Core = (*Sing)(nil)
|
var _ vCore.Core = (*Sing)(nil)
|
||||||
@@ -23,12 +25,18 @@ type DNSConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Sing struct {
|
type Sing struct {
|
||||||
box *box.Box
|
box *box.Box
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
hookServer *HookServer
|
hookServer *HookServer
|
||||||
router adapter.Router
|
router adapter.Router
|
||||||
logFactory log.Factory
|
logFactory log.Factory
|
||||||
inbounds map[string]adapter.Inbound
|
users *UserMap
|
||||||
|
nodeReportMinTrafficBytes map[string]int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserMap struct {
|
||||||
|
uidMap map[string]int
|
||||||
|
mapLock sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -36,16 +44,17 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func New(c *conf.CoreConfig) (vCore.Core, error) {
|
func New(c *conf.CoreConfig) (vCore.Core, error) {
|
||||||
|
ctx := context.Background()
|
||||||
|
ctx = box.Context(ctx, include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), include.DNSTransportRegistry(), include.ServiceRegistry())
|
||||||
options := option.Options{}
|
options := option.Options{}
|
||||||
if len(c.SingConfig.OriginalPath) != 0 {
|
if len(c.SingConfig.OriginalPath) != 0 {
|
||||||
f, err := os.Open(c.SingConfig.OriginalPath)
|
data, err := os.ReadFile(c.SingConfig.OriginalPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("open original config error: %s", err)
|
return nil, fmt.Errorf("read original config error: %s", err)
|
||||||
}
|
}
|
||||||
defer f.Close()
|
options, err = json.UnmarshalExtendedContext[option.Options](ctx, data)
|
||||||
err = json.NewDecoder(f).Decode(&options)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("decode original config error: %s", err)
|
return nil, fmt.Errorf("unmarshal original config error: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
options.Log = &option.LogOptions{
|
options.Log = &option.LogOptions{
|
||||||
@@ -57,26 +66,12 @@ func New(c *conf.CoreConfig) (vCore.Core, error) {
|
|||||||
options.NTP = &option.NTPOptions{
|
options.NTP = &option.NTPOptions{
|
||||||
Enabled: c.SingConfig.NtpConfig.Enable,
|
Enabled: c.SingConfig.NtpConfig.Enable,
|
||||||
WriteToSystem: true,
|
WriteToSystem: true,
|
||||||
Server: c.SingConfig.NtpConfig.Server,
|
ServerOptions: option.ServerOptions{
|
||||||
ServerPort: c.SingConfig.NtpConfig.ServerPort,
|
Server: c.SingConfig.NtpConfig.Server,
|
||||||
|
ServerPort: c.SingConfig.NtpConfig.ServerPort,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
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,
|
||||||
@@ -84,15 +79,20 @@ func New(c *conf.CoreConfig) (vCore.Core, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
hs := NewHookServer(c.SingConfig.EnableConnClear)
|
hs := &HookServer{
|
||||||
b.Router().SetClashServer(hs)
|
counter: sync.Map{},
|
||||||
|
}
|
||||||
|
b.Router().AppendTracker(hs)
|
||||||
return &Sing{
|
return &Sing{
|
||||||
ctx: b.Router().GetCtx(),
|
ctx: b.Router().GetCtx(),
|
||||||
box: b,
|
box: b,
|
||||||
hookServer: hs,
|
hookServer: hs,
|
||||||
router: b.Router(),
|
router: b.Router(),
|
||||||
logFactory: b.LogFactory(),
|
logFactory: b.LogFactory(),
|
||||||
inbounds: make(map[string]adapter.Inbound),
|
users: &UserMap{
|
||||||
|
uidMap: make(map[string]int),
|
||||||
|
},
|
||||||
|
nodeReportMinTrafficBytes: make(map[string]int64),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,6 +110,8 @@ func (b *Sing) Protocols() []string {
|
|||||||
"vless",
|
"vless",
|
||||||
"shadowsocks",
|
"shadowsocks",
|
||||||
"trojan",
|
"trojan",
|
||||||
|
"tuic",
|
||||||
|
"anytls",
|
||||||
"hysteria",
|
"hysteria",
|
||||||
"hysteria2",
|
"hysteria2",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,33 +7,47 @@ import (
|
|||||||
"github.com/InazumaV/V2bX/api/panel"
|
"github.com/InazumaV/V2bX/api/panel"
|
||||||
"github.com/InazumaV/V2bX/common/counter"
|
"github.com/InazumaV/V2bX/common/counter"
|
||||||
"github.com/InazumaV/V2bX/core"
|
"github.com/InazumaV/V2bX/core"
|
||||||
"github.com/sagernet/sing-box/inbound"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
|
"github.com/sagernet/sing-box/protocol/anytls"
|
||||||
|
"github.com/sagernet/sing-box/protocol/hysteria"
|
||||||
|
"github.com/sagernet/sing-box/protocol/hysteria2"
|
||||||
|
"github.com/sagernet/sing-box/protocol/shadowsocks"
|
||||||
|
"github.com/sagernet/sing-box/protocol/trojan"
|
||||||
|
"github.com/sagernet/sing-box/protocol/tuic"
|
||||||
|
"github.com/sagernet/sing-box/protocol/vless"
|
||||||
|
"github.com/sagernet/sing-box/protocol/vmess"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (b *Sing) AddUsers(p *core.AddUsersParams) (added int, err error) {
|
func (b *Sing) AddUsers(p *core.AddUsersParams) (added int, err error) {
|
||||||
|
in, found := b.box.Inbound().Get(p.Tag)
|
||||||
|
if !found {
|
||||||
|
return 0, errors.New("the inbound not found")
|
||||||
|
}
|
||||||
|
b.users.mapLock.Lock()
|
||||||
|
defer b.users.mapLock.Unlock()
|
||||||
|
for i := range p.Users {
|
||||||
|
b.users.uidMap[p.Users[i].Uuid] = p.Users[i].Id
|
||||||
|
}
|
||||||
switch p.NodeInfo.Type {
|
switch p.NodeInfo.Type {
|
||||||
case "vmess", "vless":
|
case "vless":
|
||||||
if p.NodeInfo.Type == "vless" {
|
us := make([]option.VLESSUser, len(p.Users))
|
||||||
us := make([]option.VLESSUser, len(p.Users))
|
for i := range p.Users {
|
||||||
for i := range p.Users {
|
us[i] = option.VLESSUser{
|
||||||
us[i] = option.VLESSUser{
|
Name: p.Users[i].Uuid,
|
||||||
Name: p.Users[i].Uuid,
|
Flow: p.VAllss.Flow,
|
||||||
Flow: p.VAllss.Flow,
|
UUID: p.Users[i].Uuid,
|
||||||
UUID: p.Users[i].Uuid,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
err = b.inbounds[p.Tag].(*inbound.VLESS).AddUsers(us)
|
|
||||||
} else {
|
|
||||||
us := make([]option.VMessUser, len(p.Users))
|
|
||||||
for i := range p.Users {
|
|
||||||
us[i] = option.VMessUser{
|
|
||||||
Name: p.Users[i].Uuid,
|
|
||||||
UUID: p.Users[i].Uuid,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
err = b.inbounds[p.Tag].(*inbound.VMess).AddUsers(us)
|
|
||||||
}
|
}
|
||||||
|
err = in.(*vless.Inbound).AddUsers(us)
|
||||||
|
case "vmess":
|
||||||
|
us := make([]option.VMessUser, len(p.Users))
|
||||||
|
for i := range p.Users {
|
||||||
|
us[i] = option.VMessUser{
|
||||||
|
Name: p.Users[i].Uuid,
|
||||||
|
UUID: p.Users[i].Uuid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = in.(*vmess.Inbound).AddUsers(us)
|
||||||
case "shadowsocks":
|
case "shadowsocks":
|
||||||
us := make([]option.ShadowsocksUser, len(p.Users))
|
us := make([]option.ShadowsocksUser, len(p.Users))
|
||||||
for i := range p.Users {
|
for i := range p.Users {
|
||||||
@@ -49,7 +63,7 @@ func (b *Sing) AddUsers(p *core.AddUsersParams) (added int, err error) {
|
|||||||
Password: password,
|
Password: password,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
err = b.inbounds[p.Tag].(*inbound.ShadowsocksMulti).AddUsers(us)
|
err = in.(*shadowsocks.MultiInbound).AddUsers(us)
|
||||||
case "trojan":
|
case "trojan":
|
||||||
us := make([]option.TrojanUser, len(p.Users))
|
us := make([]option.TrojanUser, len(p.Users))
|
||||||
for i := range p.Users {
|
for i := range p.Users {
|
||||||
@@ -58,7 +72,19 @@ func (b *Sing) AddUsers(p *core.AddUsersParams) (added int, err error) {
|
|||||||
Password: p.Users[i].Uuid,
|
Password: p.Users[i].Uuid,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
err = b.inbounds[p.Tag].(*inbound.Trojan).AddUsers(us)
|
err = in.(*trojan.Inbound).AddUsers(us)
|
||||||
|
case "tuic":
|
||||||
|
us := make([]option.TUICUser, len(p.Users))
|
||||||
|
id := make([]int, len(p.Users))
|
||||||
|
for i := range p.Users {
|
||||||
|
us[i] = option.TUICUser{
|
||||||
|
Name: p.Users[i].Uuid,
|
||||||
|
UUID: p.Users[i].Uuid,
|
||||||
|
Password: p.Users[i].Uuid,
|
||||||
|
}
|
||||||
|
id[i] = p.Users[i].Id
|
||||||
|
}
|
||||||
|
err = in.(*tuic.Inbound).AddUsers(us, id)
|
||||||
case "hysteria":
|
case "hysteria":
|
||||||
us := make([]option.HysteriaUser, len(p.Users))
|
us := make([]option.HysteriaUser, len(p.Users))
|
||||||
for i := range p.Users {
|
for i := range p.Users {
|
||||||
@@ -67,16 +93,27 @@ func (b *Sing) AddUsers(p *core.AddUsersParams) (added int, err error) {
|
|||||||
AuthString: p.Users[i].Uuid,
|
AuthString: p.Users[i].Uuid,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
err = b.inbounds[p.Tag].(*inbound.Hysteria).AddUsers(us)
|
err = in.(*hysteria.Inbound).AddUsers(us)
|
||||||
case "hysteria2":
|
case "hysteria2":
|
||||||
us := make([]option.Hysteria2User, len(p.Users))
|
us := make([]option.Hysteria2User, len(p.Users))
|
||||||
|
id := make([]int, len(p.Users))
|
||||||
for i := range p.Users {
|
for i := range p.Users {
|
||||||
us[i] = option.Hysteria2User{
|
us[i] = option.Hysteria2User{
|
||||||
Name: p.Users[i].Uuid,
|
Name: p.Users[i].Uuid,
|
||||||
Password: p.Users[i].Uuid,
|
Password: p.Users[i].Uuid,
|
||||||
}
|
}
|
||||||
|
id[i] = p.Users[i].Id
|
||||||
}
|
}
|
||||||
err = b.inbounds[p.Tag].(*inbound.Hysteria2).AddUsers(us)
|
err = in.(*hysteria2.Inbound).AddUsers(us, id)
|
||||||
|
case "anytls":
|
||||||
|
us := make([]option.AnyTLSUser, len(p.Users))
|
||||||
|
for i := range p.Users {
|
||||||
|
us[i] = option.AnyTLSUser{
|
||||||
|
Name: p.Users[i].Uuid,
|
||||||
|
Password: p.Users[i].Uuid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = in.(*anytls.Inbound).AddUsers(us)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
@@ -97,33 +134,72 @@ func (b *Sing) GetUserTraffic(tag, uuid string, reset bool) (up int64, down int6
|
|||||||
return 0, 0
|
return 0, 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *Sing) GetUserTrafficSlice(tag string, reset bool) ([]panel.UserTraffic, error) {
|
||||||
|
trafficSlice := make([]panel.UserTraffic, 0)
|
||||||
|
hook := b.hookServer
|
||||||
|
b.users.mapLock.RLock()
|
||||||
|
defer b.users.mapLock.RUnlock()
|
||||||
|
if v, ok := hook.counter.Load(tag); ok {
|
||||||
|
c := v.(*counter.TrafficCounter)
|
||||||
|
c.Counters.Range(func(key, value interface{}) bool {
|
||||||
|
uuid := key.(string)
|
||||||
|
traffic := value.(*counter.TrafficStorage)
|
||||||
|
up := traffic.UpCounter.Load()
|
||||||
|
down := traffic.DownCounter.Load()
|
||||||
|
if up+down > b.nodeReportMinTrafficBytes[tag] {
|
||||||
|
if reset {
|
||||||
|
traffic.UpCounter.Store(0)
|
||||||
|
traffic.DownCounter.Store(0)
|
||||||
|
}
|
||||||
|
trafficSlice = append(trafficSlice, panel.UserTraffic{
|
||||||
|
UID: b.users.uidMap[uuid],
|
||||||
|
Upload: up,
|
||||||
|
Download: down,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
if len(trafficSlice) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return trafficSlice, nil
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
type UserDeleter interface {
|
type UserDeleter interface {
|
||||||
DelUsers(uuid []string) error
|
DelUsers(uuid []string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Sing) DelUsers(users []panel.UserInfo, tag string) error {
|
func (b *Sing) DelUsers(users []panel.UserInfo, tag string, info *panel.NodeInfo) error {
|
||||||
var del UserDeleter
|
var del UserDeleter
|
||||||
if i, ok := b.inbounds[tag]; ok {
|
if i, found := b.box.Inbound().Get(tag); found {
|
||||||
switch i.Type() {
|
switch info.Type {
|
||||||
case "vmess":
|
case "vmess":
|
||||||
del = i.(*inbound.VMess)
|
del = i.(*vmess.Inbound)
|
||||||
case "vless":
|
case "vless":
|
||||||
del = i.(*inbound.VLESS)
|
del = i.(*vless.Inbound)
|
||||||
case "shadowsocks":
|
case "shadowsocks":
|
||||||
del = i.(*inbound.ShadowsocksMulti)
|
del = i.(*shadowsocks.MultiInbound)
|
||||||
case "trojan":
|
case "trojan":
|
||||||
del = i.(*inbound.Trojan)
|
del = i.(*trojan.Inbound)
|
||||||
|
case "tuic":
|
||||||
|
del = i.(*tuic.Inbound)
|
||||||
case "hysteria":
|
case "hysteria":
|
||||||
del = i.(*inbound.Hysteria)
|
del = i.(*hysteria.Inbound)
|
||||||
case "hysteria2":
|
case "hysteria2":
|
||||||
del = i.(*inbound.Hysteria2)
|
del = i.(*hysteria2.Inbound)
|
||||||
|
case "anytls":
|
||||||
|
del = i.(*anytls.Inbound)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return errors.New("the inbound not found")
|
return errors.New("the inbound not found")
|
||||||
}
|
}
|
||||||
uuids := make([]string, len(users))
|
uuids := make([]string, len(users))
|
||||||
|
b.users.mapLock.Lock()
|
||||||
|
defer b.users.mapLock.Unlock()
|
||||||
for i := range users {
|
for i := range users {
|
||||||
b.hookServer.ClearConn(tag, users[i].Uuid)
|
delete(b.users.uidMap, users[i].Uuid)
|
||||||
uuids[i] = users[i].Uuid
|
uuids[i] = users[i].Uuid
|
||||||
}
|
}
|
||||||
err := del.DelUsers(uuids)
|
err := del.DelUsers(uuids)
|
||||||
|
|||||||
@@ -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{
|
||||||
|
|||||||
37
core/xray/app/dispatcher/countreader.go
Normal file
37
core/xray/app/dispatcher/countreader.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package dispatcher
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/xtls/xray-core/common/buf"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ buf.TimeoutReader = (*CounterReader)(nil)
|
||||||
|
|
||||||
|
type CounterReader struct {
|
||||||
|
Reader buf.TimeoutReader
|
||||||
|
Counter *atomic.Int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CounterReader) ReadMultiBufferTimeout(time.Duration) (buf.MultiBuffer, error) {
|
||||||
|
mb, err := c.Reader.ReadMultiBufferTimeout(time.Second)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if mb.Len() > 0 {
|
||||||
|
c.Counter.Add(int64(mb.Len()))
|
||||||
|
}
|
||||||
|
return mb, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CounterReader) ReadMultiBuffer() (buf.MultiBuffer, error) {
|
||||||
|
mb, err := c.Reader.ReadMultiBuffer()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if mb.Len() > 0 {
|
||||||
|
c.Counter.Add(int64(mb.Len()))
|
||||||
|
}
|
||||||
|
return mb, nil
|
||||||
|
}
|
||||||
@@ -5,15 +5,19 @@ package dispatcher
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/InazumaV/V2bX/common/counter"
|
||||||
"github.com/InazumaV/V2bX/common/rate"
|
"github.com/InazumaV/V2bX/common/rate"
|
||||||
"github.com/InazumaV/V2bX/limiter"
|
"github.com/InazumaV/V2bX/limiter"
|
||||||
|
|
||||||
|
"github.com/xtls/xray-core/app/dispatcher"
|
||||||
"github.com/xtls/xray-core/common"
|
"github.com/xtls/xray-core/common"
|
||||||
"github.com/xtls/xray-core/common/buf"
|
"github.com/xtls/xray-core/common/buf"
|
||||||
|
"github.com/xtls/xray-core/common/errors"
|
||||||
"github.com/xtls/xray-core/common/log"
|
"github.com/xtls/xray-core/common/log"
|
||||||
"github.com/xtls/xray-core/common/net"
|
"github.com/xtls/xray-core/common/net"
|
||||||
"github.com/xtls/xray-core/common/protocol"
|
"github.com/xtls/xray-core/common/protocol"
|
||||||
@@ -29,25 +33,29 @@ import (
|
|||||||
"github.com/xtls/xray-core/transport/pipe"
|
"github.com/xtls/xray-core/transport/pipe"
|
||||||
)
|
)
|
||||||
|
|
||||||
var errSniffingTimeout = newError("timeout on sniffing")
|
var errSniffingTimeout = errors.New("timeout on sniffing")
|
||||||
|
|
||||||
type cachedReader struct {
|
type cachedReader struct {
|
||||||
sync.Mutex
|
sync.Mutex
|
||||||
reader *pipe.Reader
|
reader buf.TimeoutReader
|
||||||
cache buf.MultiBuffer
|
cache buf.MultiBuffer
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *cachedReader) Cache(b *buf.Buffer) {
|
func (r *cachedReader) Cache(b *buf.Buffer, deadline time.Duration) error {
|
||||||
mb, _ := r.reader.ReadMultiBufferTimeout(time.Millisecond * 100)
|
mb, err := r.reader.ReadMultiBufferTimeout(deadline)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
r.Lock()
|
r.Lock()
|
||||||
if !mb.IsEmpty() {
|
if !mb.IsEmpty() {
|
||||||
r.cache, _ = buf.MergeMulti(r.cache, mb)
|
r.cache, _ = buf.MergeMulti(r.cache, mb)
|
||||||
}
|
}
|
||||||
b.Clear()
|
b.Clear()
|
||||||
rawBytes := b.Extend(buf.Size)
|
rawBytes := b.Extend(min(r.cache.Len(), b.Cap()))
|
||||||
n := r.cache.Copy(rawBytes)
|
n := r.cache.Copy(rawBytes)
|
||||||
b.Resize(0, int32(n))
|
b.Resize(0, int32(n))
|
||||||
r.Unlock()
|
r.Unlock()
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *cachedReader) readInternal() buf.MultiBuffer {
|
func (r *cachedReader) readInternal() buf.MultiBuffer {
|
||||||
@@ -87,27 +95,30 @@ func (r *cachedReader) Interrupt() {
|
|||||||
r.cache = buf.ReleaseMulti(r.cache)
|
r.cache = buf.ReleaseMulti(r.cache)
|
||||||
}
|
}
|
||||||
r.Unlock()
|
r.Unlock()
|
||||||
r.reader.Interrupt()
|
if p, ok := r.reader.(*pipe.Reader); ok {
|
||||||
|
p.Interrupt()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultDispatcher is a default implementation of Dispatcher.
|
// DefaultDispatcher is a default implementation of Dispatcher.
|
||||||
type DefaultDispatcher struct {
|
type DefaultDispatcher struct {
|
||||||
ohm outbound.Manager
|
ohm outbound.Manager
|
||||||
router routing.Router
|
router routing.Router
|
||||||
policy policy.Manager
|
policy policy.Manager
|
||||||
stats stats.Manager
|
stats stats.Manager
|
||||||
dns dns.Client
|
fdns dns.FakeDNSEngine
|
||||||
fdns dns.FakeDNSEngine
|
Counter sync.Map
|
||||||
|
LinkManagers sync.Map // map[string]*LinkManager
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
|
common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
|
||||||
d := new(DefaultDispatcher)
|
d := new(DefaultDispatcher)
|
||||||
if err := core.RequireFeatures(ctx, func(om outbound.Manager, router routing.Router, pm policy.Manager, sm stats.Manager, dc dns.Client) error {
|
if err := core.RequireFeatures(ctx, func(om outbound.Manager, router routing.Router, pm policy.Manager, sm stats.Manager, dc dns.Client) error {
|
||||||
core.RequireFeatures(ctx, func(fdns dns.FakeDNSEngine) {
|
core.OptionalFeatures(ctx, func(fdns dns.FakeDNSEngine) {
|
||||||
d.fdns = fdns
|
d.fdns = fdns
|
||||||
})
|
})
|
||||||
return d.Init(config.(*Config), om, router, pm, sm, dc)
|
return d.Init(config.(*Config), om, router, pm, sm)
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -116,12 +127,11 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Init initializes DefaultDispatcher.
|
// Init initializes DefaultDispatcher.
|
||||||
func (d *DefaultDispatcher) Init(config *Config, om outbound.Manager, router routing.Router, pm policy.Manager, sm stats.Manager, dns dns.Client) error {
|
func (d *DefaultDispatcher) Init(config *Config, om outbound.Manager, router routing.Router, pm policy.Manager, sm stats.Manager) error {
|
||||||
d.ohm = om
|
d.ohm = om
|
||||||
d.router = router
|
d.router = router
|
||||||
d.policy = pm
|
d.policy = pm
|
||||||
d.stats = sm
|
d.stats = sm
|
||||||
d.dns = dns
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,47 +174,64 @@ func (d *DefaultDispatcher) getLink(ctx context.Context, network net.Network) (*
|
|||||||
if user != nil && len(user.Email) > 0 {
|
if user != nil && len(user.Email) > 0 {
|
||||||
limit, err = limiter.GetLimiter(sessionInbound.Tag)
|
limit, err = limiter.GetLimiter(sessionInbound.Tag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
newError("get limiter ", sessionInbound.Tag, " error: ", err).AtError().WriteToLog()
|
errors.LogInfo(ctx, "get limiter ", sessionInbound.Tag, " error: ", err)
|
||||||
common.Close(outboundLink.Writer)
|
common.Close(outboundLink.Writer)
|
||||||
common.Close(inboundLink.Writer)
|
common.Close(inboundLink.Writer)
|
||||||
common.Interrupt(outboundLink.Reader)
|
common.Interrupt(outboundLink.Reader)
|
||||||
common.Interrupt(inboundLink.Reader)
|
common.Interrupt(inboundLink.Reader)
|
||||||
return nil, nil, nil, newError("get limiter ", sessionInbound.Tag, " error: ", err)
|
return nil, nil, nil, errors.New("get limiter ", sessionInbound.Tag, " error: ", err)
|
||||||
}
|
}
|
||||||
// Speed Limit and Device Limit
|
// Speed Limit and Device Limit
|
||||||
w, reject := limit.CheckLimit(user.Email,
|
w, reject := limit.CheckLimit(user.Email,
|
||||||
sessionInbound.Source.Address.IP().String(),
|
sessionInbound.Source.Address.IP().String(),
|
||||||
network == net.Network_TCP)
|
network == net.Network_TCP,
|
||||||
|
sessionInbound.Source.Network == net.Network_TCP)
|
||||||
if reject {
|
if reject {
|
||||||
newError("Limited ", user.Email, " by conn or ip").AtWarning().WriteToLog()
|
errors.LogInfo(ctx, "Limited ", user.Email, " by conn or ip")
|
||||||
common.Close(outboundLink.Writer)
|
common.Close(outboundLink.Writer)
|
||||||
common.Close(inboundLink.Writer)
|
common.Close(inboundLink.Writer)
|
||||||
common.Interrupt(outboundLink.Reader)
|
common.Interrupt(outboundLink.Reader)
|
||||||
common.Interrupt(inboundLink.Reader)
|
common.Interrupt(inboundLink.Reader)
|
||||||
return nil, nil, nil, newError("Limited ", user.Email, " by conn or ip")
|
return nil, nil, nil, errors.New("Limited ", user.Email, " by conn or ip")
|
||||||
}
|
}
|
||||||
|
var lm *LinkManager
|
||||||
|
if lmloaded, ok := d.LinkManagers.Load(user.Email); !ok {
|
||||||
|
lm = &LinkManager{
|
||||||
|
links: make(map[*ManagedWriter]buf.Reader),
|
||||||
|
}
|
||||||
|
d.LinkManagers.Store(user.Email, lm)
|
||||||
|
} else {
|
||||||
|
lm = lmloaded.(*LinkManager)
|
||||||
|
}
|
||||||
|
managedWriter := &ManagedWriter{
|
||||||
|
writer: uplinkWriter,
|
||||||
|
manager: lm,
|
||||||
|
}
|
||||||
|
lm.AddLink(managedWriter, outboundLink.Reader)
|
||||||
|
inboundLink.Writer = managedWriter
|
||||||
if w != nil {
|
if w != nil {
|
||||||
|
sessionInbound.CanSpliceCopy = 3
|
||||||
inboundLink.Writer = rate.NewRateLimitWriter(inboundLink.Writer, w)
|
inboundLink.Writer = rate.NewRateLimitWriter(inboundLink.Writer, w)
|
||||||
outboundLink.Writer = rate.NewRateLimitWriter(outboundLink.Writer, w)
|
outboundLink.Writer = rate.NewRateLimitWriter(outboundLink.Writer, w)
|
||||||
}
|
}
|
||||||
p := d.policy.ForLevel(user.Level)
|
var t *counter.TrafficCounter
|
||||||
if p.Stats.UserUplink {
|
if c, ok := d.Counter.Load(sessionInbound.Tag); !ok {
|
||||||
name := "user>>>" + user.Email + ">>>traffic>>>uplink"
|
t = counter.NewTrafficCounter()
|
||||||
if c, _ := stats.GetOrRegisterCounter(d.stats, name); c != nil {
|
d.Counter.Store(sessionInbound.Tag, t)
|
||||||
inboundLink.Writer = &SizeStatWriter{
|
} else {
|
||||||
Counter: c,
|
t = c.(*counter.TrafficCounter)
|
||||||
Writer: inboundLink.Writer,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if p.Stats.UserDownlink {
|
|
||||||
name := "user>>>" + user.Email + ">>>traffic>>>downlink"
|
ts := t.GetCounter(user.Email)
|
||||||
if c, _ := stats.GetOrRegisterCounter(d.stats, name); c != nil {
|
upcounter := &counter.XrayTrafficCounter{V: &ts.UpCounter}
|
||||||
outboundLink.Writer = &SizeStatWriter{
|
downcounter := &counter.XrayTrafficCounter{V: &ts.DownCounter}
|
||||||
Counter: c,
|
inboundLink.Writer = &dispatcher.SizeStatWriter{
|
||||||
Writer: outboundLink.Writer,
|
Counter: upcounter,
|
||||||
}
|
Writer: inboundLink.Writer,
|
||||||
}
|
}
|
||||||
|
outboundLink.Writer = &dispatcher.SizeStatWriter{
|
||||||
|
Counter: downcounter,
|
||||||
|
Writer: outboundLink.Writer,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,8 +244,20 @@ func (d *DefaultDispatcher) shouldOverride(ctx context.Context, result SniffResu
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
for _, d := range request.ExcludeForDomain {
|
for _, d := range request.ExcludeForDomain {
|
||||||
if strings.ToLower(domain) == d {
|
if strings.HasPrefix(d, "regexp:") {
|
||||||
return false
|
pattern := d[7:]
|
||||||
|
re, err := regexp.Compile(pattern)
|
||||||
|
if err != nil {
|
||||||
|
errors.LogInfo(ctx, "Unable to compile regex")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if re.MatchString(domain) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if strings.ToLower(domain) == d {
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
protocolString := result.Protocol()
|
protocolString := result.Protocol()
|
||||||
@@ -226,12 +265,12 @@ func (d *DefaultDispatcher) shouldOverride(ctx context.Context, result SniffResu
|
|||||||
protocolString = resComp.ProtocolForDomainResult()
|
protocolString = resComp.ProtocolForDomainResult()
|
||||||
}
|
}
|
||||||
for _, p := range request.OverrideDestinationForProtocol {
|
for _, p := range request.OverrideDestinationForProtocol {
|
||||||
if strings.HasPrefix(protocolString, p) || strings.HasPrefix(protocolString, p) {
|
if strings.HasPrefix(protocolString, p) || strings.HasPrefix(p, protocolString) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if fkr0, ok := d.fdns.(dns.FakeDNSEngineRev0); ok && protocolString != "bittorrent" && p == "fakedns" &&
|
if fkr0, ok := d.fdns.(dns.FakeDNSEngineRev0); ok && protocolString != "bittorrent" && p == "fakedns" &&
|
||||||
destination.Address.Family().IsIP() && fkr0.IsIPInIPPool(destination.Address) {
|
destination.Address.Family().IsIP() && fkr0.IsIPInIPPool(destination.Address) {
|
||||||
newError("Using sniffer ", protocolString, " since the fake DNS missed").WriteToLog(session.ExportIDToError(ctx))
|
errors.LogInfo(ctx, "Using sniffer ", protocolString, " since the fake DNS missed")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if resultSubset, ok := result.(SnifferIsProtoSubsetOf); ok {
|
if resultSubset, ok := result.(SnifferIsProtoSubsetOf); ok {
|
||||||
@@ -249,11 +288,14 @@ func (d *DefaultDispatcher) Dispatch(ctx context.Context, destination net.Destin
|
|||||||
if !destination.IsValid() {
|
if !destination.IsValid() {
|
||||||
panic("Dispatcher: Invalid destination.")
|
panic("Dispatcher: Invalid destination.")
|
||||||
}
|
}
|
||||||
ob := &session.Outbound{
|
outbounds := session.OutboundsFromContext(ctx)
|
||||||
OriginalTarget: destination,
|
if len(outbounds) == 0 {
|
||||||
Target: destination,
|
outbounds = []*session.Outbound{{}}
|
||||||
|
ctx = session.ContextWithOutbounds(ctx, outbounds)
|
||||||
}
|
}
|
||||||
ctx = session.ContextWithOutbound(ctx, ob)
|
ob := outbounds[len(outbounds)-1]
|
||||||
|
ob.OriginalTarget = destination
|
||||||
|
ob.Target = destination
|
||||||
content := session.ContentFromContext(ctx)
|
content := session.ContentFromContext(ctx)
|
||||||
if content == nil {
|
if content == nil {
|
||||||
content = new(session.Content)
|
content = new(session.Content)
|
||||||
@@ -278,7 +320,7 @@ func (d *DefaultDispatcher) Dispatch(ctx context.Context, destination net.Destin
|
|||||||
}
|
}
|
||||||
if err == nil && d.shouldOverride(ctx, result, sniffingRequest, destination) {
|
if err == nil && d.shouldOverride(ctx, result, sniffingRequest, destination) {
|
||||||
domain := result.Domain()
|
domain := result.Domain()
|
||||||
newError("sniffed domain: ", domain).WriteToLog(session.ExportIDToError(ctx))
|
errors.LogInfo(ctx, "sniffed domain: ", domain)
|
||||||
destination.Address = net.ParseAddress(domain)
|
destination.Address = net.ParseAddress(domain)
|
||||||
protocol := result.Protocol()
|
protocol := result.Protocol()
|
||||||
if resComp, ok := result.(SnifferResultComposite); ok {
|
if resComp, ok := result.(SnifferResultComposite); ok {
|
||||||
@@ -303,24 +345,94 @@ func (d *DefaultDispatcher) Dispatch(ctx context.Context, destination net.Destin
|
|||||||
// DispatchLink implements routing.Dispatcher.
|
// DispatchLink implements routing.Dispatcher.
|
||||||
func (d *DefaultDispatcher) DispatchLink(ctx context.Context, destination net.Destination, outbound *transport.Link) error {
|
func (d *DefaultDispatcher) DispatchLink(ctx context.Context, destination net.Destination, outbound *transport.Link) error {
|
||||||
if !destination.IsValid() {
|
if !destination.IsValid() {
|
||||||
return newError("Dispatcher: Invalid destination.")
|
return errors.New("Dispatcher: Invalid destination.")
|
||||||
}
|
}
|
||||||
ob := &session.Outbound{
|
outbounds := session.OutboundsFromContext(ctx)
|
||||||
OriginalTarget: destination,
|
if len(outbounds) == 0 {
|
||||||
Target: destination,
|
outbounds = []*session.Outbound{{}}
|
||||||
|
ctx = session.ContextWithOutbounds(ctx, outbounds)
|
||||||
}
|
}
|
||||||
ctx = session.ContextWithOutbound(ctx, ob)
|
ob := outbounds[len(outbounds)-1]
|
||||||
|
ob.OriginalTarget = destination
|
||||||
|
ob.Target = destination
|
||||||
content := session.ContentFromContext(ctx)
|
content := session.ContentFromContext(ctx)
|
||||||
if content == nil {
|
if content == nil {
|
||||||
content = new(session.Content)
|
content = new(session.Content)
|
||||||
ctx = session.ContextWithContent(ctx, content)
|
ctx = session.ContextWithContent(ctx, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sessionInbound := session.InboundFromContext(ctx)
|
||||||
|
var user *protocol.MemoryUser
|
||||||
|
if sessionInbound != nil {
|
||||||
|
user = sessionInbound.User
|
||||||
|
}
|
||||||
|
|
||||||
|
var limit *limiter.Limiter
|
||||||
|
var err error
|
||||||
|
if user != nil && len(user.Email) > 0 {
|
||||||
|
limit, err = limiter.GetLimiter(sessionInbound.Tag)
|
||||||
|
if err != nil {
|
||||||
|
errors.LogInfo(ctx, "get limiter ", sessionInbound.Tag, " error: ", err)
|
||||||
|
common.Close(outbound.Writer)
|
||||||
|
common.Interrupt(outbound.Reader)
|
||||||
|
return errors.New("get limiter ", sessionInbound.Tag, " error: ", err)
|
||||||
|
}
|
||||||
|
// Speed Limit and Device Limit
|
||||||
|
w, reject := limit.CheckLimit(user.Email,
|
||||||
|
sessionInbound.Source.Address.IP().String(),
|
||||||
|
destination.Network == net.Network_TCP,
|
||||||
|
sessionInbound.Source.Network == net.Network_TCP)
|
||||||
|
if reject {
|
||||||
|
errors.LogInfo(ctx, "Limited ", user.Email, " by conn or ip")
|
||||||
|
common.Close(outbound.Writer)
|
||||||
|
common.Interrupt(outbound.Reader)
|
||||||
|
return errors.New("Limited ", user.Email, " by conn or ip")
|
||||||
|
}
|
||||||
|
var lm *LinkManager
|
||||||
|
if lmloaded, ok := d.LinkManagers.Load(user.Email); !ok {
|
||||||
|
lm = &LinkManager{
|
||||||
|
links: make(map[*ManagedWriter]buf.Reader),
|
||||||
|
}
|
||||||
|
d.LinkManagers.Store(user.Email, lm)
|
||||||
|
} else {
|
||||||
|
lm = lmloaded.(*LinkManager)
|
||||||
|
}
|
||||||
|
managedWriter := &ManagedWriter{
|
||||||
|
writer: outbound.Writer,
|
||||||
|
manager: lm,
|
||||||
|
}
|
||||||
|
outbound.Writer = managedWriter
|
||||||
|
if w != nil {
|
||||||
|
sessionInbound.CanSpliceCopy = 3
|
||||||
|
outbound.Writer = rate.NewRateLimitWriter(outbound.Writer, w)
|
||||||
|
}
|
||||||
|
var t *counter.TrafficCounter
|
||||||
|
if c, ok := d.Counter.Load(sessionInbound.Tag); !ok {
|
||||||
|
t = counter.NewTrafficCounter()
|
||||||
|
d.Counter.Store(sessionInbound.Tag, t)
|
||||||
|
} else {
|
||||||
|
t = c.(*counter.TrafficCounter)
|
||||||
|
}
|
||||||
|
|
||||||
|
ts := t.GetCounter(user.Email)
|
||||||
|
downcounter := &counter.XrayTrafficCounter{V: &ts.DownCounter}
|
||||||
|
outbound.Reader = &CounterReader{
|
||||||
|
Reader: &buf.TimeoutWrapperReader{Reader: outbound.Reader},
|
||||||
|
Counter: &ts.UpCounter,
|
||||||
|
}
|
||||||
|
lm.AddLink(managedWriter, outbound.Reader)
|
||||||
|
outbound.Writer = &dispatcher.SizeStatWriter{
|
||||||
|
Counter: downcounter,
|
||||||
|
Writer: outbound.Writer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sniffingRequest := content.SniffingRequest
|
sniffingRequest := content.SniffingRequest
|
||||||
if !sniffingRequest.Enabled {
|
if !sniffingRequest.Enabled {
|
||||||
d.routedDispatch(ctx, outbound, destination, nil, "")
|
d.routedDispatch(ctx, outbound, destination, limit, "")
|
||||||
} else {
|
} else {
|
||||||
cReader := &cachedReader{
|
cReader := &cachedReader{
|
||||||
reader: outbound.Reader.(*pipe.Reader),
|
reader: outbound.Reader.(buf.TimeoutReader),
|
||||||
}
|
}
|
||||||
outbound.Reader = cReader
|
outbound.Reader = cReader
|
||||||
result, err := sniffer(ctx, cReader, sniffingRequest.MetadataOnly, destination.Network)
|
result, err := sniffer(ctx, cReader, sniffingRequest.MetadataOnly, destination.Network)
|
||||||
@@ -329,14 +441,14 @@ func (d *DefaultDispatcher) DispatchLink(ctx context.Context, destination net.De
|
|||||||
}
|
}
|
||||||
if err == nil && d.shouldOverride(ctx, result, sniffingRequest, destination) {
|
if err == nil && d.shouldOverride(ctx, result, sniffingRequest, destination) {
|
||||||
domain := result.Domain()
|
domain := result.Domain()
|
||||||
newError("sniffed domain: ", domain).WriteToLog(session.ExportIDToError(ctx))
|
errors.LogInfo(ctx, "sniffed domain: ", domain)
|
||||||
destination.Address = net.ParseAddress(domain)
|
destination.Address = net.ParseAddress(domain)
|
||||||
protocol := result.Protocol()
|
protocol := result.Protocol()
|
||||||
if resComp, ok := result.(SnifferResultComposite); ok {
|
if resComp, ok := result.(SnifferResultComposite); ok {
|
||||||
protocol = resComp.ProtocolForDomainResult()
|
protocol = resComp.ProtocolForDomainResult()
|
||||||
}
|
}
|
||||||
isFakeIP := false
|
isFakeIP := false
|
||||||
if fkr0, ok := d.fdns.(dns.FakeDNSEngineRev0); ok && ob.Target.Address.Family().IsIP() && fkr0.IsIPInIPPool(ob.Target.Address) {
|
if fkr0, ok := d.fdns.(dns.FakeDNSEngineRev0); ok && fkr0.IsIPInIPPool(ob.Target.Address) {
|
||||||
isFakeIP = true
|
isFakeIP = true
|
||||||
}
|
}
|
||||||
if sniffingRequest.RouteOnly && protocol != "fakedns" && protocol != "fakedns+others" && !isFakeIP {
|
if sniffingRequest.RouteOnly && protocol != "fakedns" && protocol != "fakedns+others" && !isFakeIP {
|
||||||
@@ -344,16 +456,15 @@ func (d *DefaultDispatcher) DispatchLink(ctx context.Context, destination net.De
|
|||||||
} else {
|
} else {
|
||||||
ob.Target = destination
|
ob.Target = destination
|
||||||
}
|
}
|
||||||
destination.Address.Family()
|
|
||||||
}
|
}
|
||||||
d.routedDispatch(ctx, outbound, destination, nil, content.Protocol)
|
d.routedDispatch(ctx, outbound, destination, limit, content.Protocol)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func sniffer(ctx context.Context, cReader *cachedReader, metadataOnly bool, network net.Network) (SniffResult, error) {
|
func sniffer(ctx context.Context, cReader *cachedReader, metadataOnly bool, network net.Network) (SniffResult, error) {
|
||||||
payload := buf.New()
|
payload := buf.NewWithSize(32767)
|
||||||
defer payload.Release()
|
defer payload.Release()
|
||||||
|
|
||||||
sniffer := NewSniffer(ctx)
|
sniffer := NewSniffer(ctx)
|
||||||
@@ -365,26 +476,36 @@ func sniffer(ctx context.Context, cReader *cachedReader, metadataOnly bool, netw
|
|||||||
}
|
}
|
||||||
|
|
||||||
contentResult, contentErr := func() (SniffResult, error) {
|
contentResult, contentErr := func() (SniffResult, error) {
|
||||||
|
cacheDeadline := 200 * time.Millisecond
|
||||||
totalAttempt := 0
|
totalAttempt := 0
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return nil, ctx.Err()
|
return nil, ctx.Err()
|
||||||
default:
|
default:
|
||||||
totalAttempt++
|
cachingStartingTimeStamp := time.Now()
|
||||||
if totalAttempt > 2 {
|
err := cReader.Cache(payload, cacheDeadline)
|
||||||
return nil, errSniffingTimeout
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
cachingTimeElapsed := time.Since(cachingStartingTimeStamp)
|
||||||
|
cacheDeadline -= cachingTimeElapsed
|
||||||
|
|
||||||
cReader.Cache(payload)
|
|
||||||
if !payload.IsEmpty() {
|
if !payload.IsEmpty() {
|
||||||
result, err := sniffer.Sniff(ctx, payload.Bytes(), network)
|
result, err := sniffer.Sniff(ctx, payload.Bytes(), network)
|
||||||
if err != common.ErrNoClue {
|
switch err {
|
||||||
|
case common.ErrNoClue: // No Clue: protocol not matches, and sniffer cannot determine whether there will be a match or not
|
||||||
|
totalAttempt++
|
||||||
|
case protocol.ErrProtoNeedMoreData: // Protocol Need More Data: protocol matches, but need more data to complete sniffing
|
||||||
|
// in this case, do not add totalAttempt(allow to read until timeout)
|
||||||
|
default:
|
||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
totalAttempt++
|
||||||
}
|
}
|
||||||
if payload.IsFull() {
|
if totalAttempt >= 2 || cacheDeadline <= 0 {
|
||||||
return nil, errUnknownContent
|
return nil, errSniffingTimeout
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -399,34 +520,16 @@ func sniffer(ctx context.Context, cReader *cachedReader, metadataOnly bool, netw
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *DefaultDispatcher) routedDispatch(ctx context.Context, link *transport.Link, destination net.Destination, l *limiter.Limiter, protocol string) {
|
func (d *DefaultDispatcher) routedDispatch(ctx context.Context, link *transport.Link, destination net.Destination, l *limiter.Limiter, protocol string) {
|
||||||
ob := session.OutboundFromContext(ctx)
|
outbounds := session.OutboundsFromContext(ctx)
|
||||||
if hosts, ok := d.dns.(dns.HostsLookup); ok && destination.Address.Family().IsDomain() {
|
ob := outbounds[len(outbounds)-1]
|
||||||
proxied := hosts.LookupHosts(ob.Target.String())
|
|
||||||
if proxied != nil {
|
|
||||||
ro := ob.RouteTarget == destination
|
|
||||||
destination.Address = *proxied
|
|
||||||
if ro {
|
|
||||||
ob.RouteTarget = destination
|
|
||||||
} else {
|
|
||||||
ob.Target = destination
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sessionInbound := session.InboundFromContext(ctx)
|
sessionInbound := session.InboundFromContext(ctx)
|
||||||
if sessionInbound.User != nil {
|
if sessionInbound.User != nil {
|
||||||
if l != nil {
|
if l == nil {
|
||||||
// del connect count
|
|
||||||
if destination.Network == net.Network_TCP {
|
|
||||||
defer func() {
|
|
||||||
l.ConnLimiter.DelConnCount(sessionInbound.User.Email, sessionInbound.Source.Address.IP().String())
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
var err error
|
var err error
|
||||||
l, err = limiter.GetLimiter(sessionInbound.Tag)
|
l, err = limiter.GetLimiter(sessionInbound.Tag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
newError("get limiter ", sessionInbound.Tag, " error: ", err).AtWarning().WriteToLog(session.ExportIDToError(ctx))
|
errors.LogError(ctx, "get limiter ", sessionInbound.Tag, " error: ", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if l != nil {
|
if l != nil {
|
||||||
@@ -437,20 +540,20 @@ func (d *DefaultDispatcher) routedDispatch(ctx context.Context, link *transport.
|
|||||||
destStr = destination.Address.IP().String()
|
destStr = destination.Address.IP().String()
|
||||||
}
|
}
|
||||||
if l.CheckDomainRule(destStr) {
|
if l.CheckDomainRule(destStr) {
|
||||||
newError(fmt.Sprintf(
|
errors.LogError(ctx, fmt.Sprintf(
|
||||||
"User %s access domain %s reject by rule",
|
"User %s access domain %s reject by rule",
|
||||||
sessionInbound.User.Email,
|
sessionInbound.User.Email,
|
||||||
destStr)).AtWarning().WriteToLog(session.ExportIDToError(ctx))
|
destStr))
|
||||||
common.Close(link.Writer)
|
common.Close(link.Writer)
|
||||||
common.Interrupt(link.Reader)
|
common.Interrupt(link.Reader)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if len(protocol) != 0 {
|
if len(protocol) != 0 {
|
||||||
if l.CheckProtocolRule(protocol) {
|
if l.CheckProtocolRule(protocol) {
|
||||||
newError(fmt.Sprintf(
|
errors.LogError(ctx, fmt.Sprintf(
|
||||||
"User %s access protocol %s reject by rule",
|
"User %s access protocol %s reject by rule",
|
||||||
sessionInbound.User.Email,
|
sessionInbound.User.Email,
|
||||||
protocol)).AtWarning().WriteToLog(session.ExportIDToError(ctx))
|
protocol))
|
||||||
common.Close(link.Writer)
|
common.Close(link.Writer)
|
||||||
common.Interrupt(link.Reader)
|
common.Interrupt(link.Reader)
|
||||||
return
|
return
|
||||||
@@ -468,10 +571,10 @@ func (d *DefaultDispatcher) routedDispatch(ctx context.Context, link *transport.
|
|||||||
ctx = session.SetForcedOutboundTagToContext(ctx, "")
|
ctx = session.SetForcedOutboundTagToContext(ctx, "")
|
||||||
if h := d.ohm.GetHandler(forcedOutboundTag); h != nil {
|
if h := d.ohm.GetHandler(forcedOutboundTag); h != nil {
|
||||||
isPickRoute = 1
|
isPickRoute = 1
|
||||||
newError("taking platform initialized detour [", forcedOutboundTag, "] for [", destination, "]").WriteToLog(session.ExportIDToError(ctx))
|
errors.LogInfo(ctx, "taking platform initialized detour [", forcedOutboundTag, "] for [", destination, "]")
|
||||||
handler = h
|
handler = h
|
||||||
} else {
|
} else {
|
||||||
newError("non existing tag for platform initialized detour: ", forcedOutboundTag).AtError().WriteToLog(session.ExportIDToError(ctx))
|
errors.LogError(ctx, "non existing tag for platform initialized detour: ", forcedOutboundTag)
|
||||||
common.Close(link.Writer)
|
common.Close(link.Writer)
|
||||||
common.Interrupt(link.Reader)
|
common.Interrupt(link.Reader)
|
||||||
return
|
return
|
||||||
@@ -481,31 +584,35 @@ func (d *DefaultDispatcher) routedDispatch(ctx context.Context, link *transport.
|
|||||||
outTag := route.GetOutboundTag()
|
outTag := route.GetOutboundTag()
|
||||||
if h := d.ohm.GetHandler(outTag); h != nil {
|
if h := d.ohm.GetHandler(outTag); h != nil {
|
||||||
isPickRoute = 2
|
isPickRoute = 2
|
||||||
newError("taking detour [", outTag, "] for [", destination, "]").WriteToLog(session.ExportIDToError(ctx))
|
if route.GetRuleTag() == "" {
|
||||||
|
errors.LogInfo(ctx, "taking detour [", outTag, "] for [", destination, "]")
|
||||||
|
} else {
|
||||||
|
errors.LogInfo(ctx, "Hit route rule: [", route.GetRuleTag(), "] so taking detour [", outTag, "] for [", destination, "]")
|
||||||
|
}
|
||||||
handler = h
|
handler = h
|
||||||
} else {
|
} else {
|
||||||
newError("non existing outTag: ", outTag).AtWarning().WriteToLog(session.ExportIDToError(ctx))
|
errors.LogWarning(ctx, "non existing outTag: ", outTag)
|
||||||
|
common.Close(link.Writer)
|
||||||
|
common.Interrupt(link.Reader)
|
||||||
|
return // DO NOT CHANGE: the traffic shouldn't be processed by default outbound if the specified outbound tag doesn't exist (yet), e.g., VLESS Reverse Proxy
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
newError("default route for ", destination).WriteToLog(session.ExportIDToError(ctx))
|
errors.LogInfo(ctx, "default route for ", destination)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if handler == nil {
|
|
||||||
handler = d.ohm.GetHandler(inTag)
|
|
||||||
}
|
|
||||||
|
|
||||||
if handler == nil {
|
if handler == nil {
|
||||||
handler = d.ohm.GetDefaultHandler()
|
handler = d.ohm.GetDefaultHandler()
|
||||||
}
|
}
|
||||||
|
|
||||||
if handler == nil {
|
if handler == nil {
|
||||||
newError("default outbound handler not exist").WriteToLog(session.ExportIDToError(ctx))
|
errors.LogInfo(ctx, "default outbound handler not exist")
|
||||||
common.Close(link.Writer)
|
common.Close(link.Writer)
|
||||||
common.Interrupt(link.Reader)
|
common.Interrupt(link.Reader)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ob.Tag = handler.Tag()
|
||||||
if accessMessage := log.AccessMessageFromContext(ctx); accessMessage != nil {
|
if accessMessage := log.AccessMessageFromContext(ctx); accessMessage != nil {
|
||||||
if tag := handler.Tag(); tag != "" {
|
if tag := handler.Tag(); tag != "" {
|
||||||
if inTag == "" {
|
if inTag == "" {
|
||||||
|
|||||||
@@ -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{})
|
|
||||||
}
|
|
||||||
@@ -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,
|
||||||
|
|||||||
46
core/xray/app/dispatcher/linkmanager.go
Normal file
46
core/xray/app/dispatcher/linkmanager.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package dispatcher
|
||||||
|
|
||||||
|
import (
|
||||||
|
sync "sync"
|
||||||
|
|
||||||
|
"github.com/xtls/xray-core/common"
|
||||||
|
"github.com/xtls/xray-core/common/buf"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ManagedWriter struct {
|
||||||
|
writer buf.Writer
|
||||||
|
manager *LinkManager
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *ManagedWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
|
||||||
|
return w.writer.WriteMultiBuffer(mb)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *ManagedWriter) Close() error {
|
||||||
|
w.manager.RemoveWriter(w)
|
||||||
|
return common.Close(w.writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
type LinkManager struct {
|
||||||
|
links map[*ManagedWriter]buf.Reader
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *LinkManager) AddLink(writer *ManagedWriter, reader buf.Reader) {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
m.links[writer] = reader
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *LinkManager) RemoveWriter(writer *ManagedWriter) {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
delete(m.links, writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *LinkManager) CloseAll() {
|
||||||
|
for w, r := range m.links {
|
||||||
|
common.Close(w)
|
||||||
|
common.Interrupt(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
package dispatcher
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/xtls/xray-core/common"
|
|
||||||
"github.com/xtls/xray-core/common/buf"
|
|
||||||
"github.com/xtls/xray-core/features/stats"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SizeStatWriter struct {
|
|
||||||
Counter stats.Counter
|
|
||||||
Writer buf.Writer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *SizeStatWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
|
|
||||||
w.Counter.Add(int64(mb.Len()))
|
|
||||||
return w.Writer.WriteMultiBuffer(mb)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *SizeStatWriter) Close() error {
|
|
||||||
return common.Close(w.Writer)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *SizeStatWriter) Interrupt() {
|
|
||||||
common.Interrupt(w.Writer)
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
package dispatcher_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
. "github.com/xtls/xray-core/app/dispatcher"
|
|
||||||
"github.com/xtls/xray-core/common"
|
|
||||||
"github.com/xtls/xray-core/common/buf"
|
|
||||||
)
|
|
||||||
|
|
||||||
type TestCounter int64
|
|
||||||
|
|
||||||
func (c *TestCounter) Value() int64 {
|
|
||||||
return int64(*c)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *TestCounter) Add(v int64) int64 {
|
|
||||||
x := int64(*c) + v
|
|
||||||
*c = TestCounter(x)
|
|
||||||
return x
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *TestCounter) Set(v int64) int64 {
|
|
||||||
*c = TestCounter(v)
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStatsWriter(t *testing.T) {
|
|
||||||
var c TestCounter
|
|
||||||
writer := &SizeStatWriter{
|
|
||||||
Counter: &c,
|
|
||||||
Writer: buf.Discard,
|
|
||||||
}
|
|
||||||
|
|
||||||
mb := buf.MergeBytes(nil, []byte("abcd"))
|
|
||||||
common.Must(writer.WriteMultiBuffer(mb))
|
|
||||||
|
|
||||||
mb = buf.MergeBytes(nil, []byte("efg"))
|
|
||||||
common.Must(writer.WriteMultiBuffer(mb))
|
|
||||||
|
|
||||||
if c.Value() != 7 {
|
|
||||||
t.Fatal("unexpected counter value. want 7, but got ", c.Value())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,14 +2,16 @@ package xray
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"github.com/InazumaV/V2bX/api/panel"
|
|
||||||
"github.com/goccy/go-json"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
coreConf "github.com/xtls/xray-core/infra/conf"
|
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/InazumaV/V2bX/api/panel"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
coreConf "github.com/xtls/xray-core/infra/conf"
|
||||||
)
|
)
|
||||||
|
|
||||||
func updateDNSConfig(node *panel.NodeInfo) (err error) {
|
func updateDNSConfig(node *panel.NodeInfo) (err error) {
|
||||||
@@ -62,7 +64,7 @@ func saveDnsConfig(dns []byte, dnsPath string) (err error) {
|
|||||||
}
|
}
|
||||||
if !bytes.Equal(currentData, dns) {
|
if !bytes.Equal(currentData, dns) {
|
||||||
coreDnsConfig := &coreConf.DNSConfig{}
|
coreDnsConfig := &coreConf.DNSConfig{}
|
||||||
if err = json.NewDecoder(bytes.NewReader(dns)).Decode(coreDnsConfig); err != nil {
|
if err = json.Unmarshal(dns, coreDnsConfig); err != nil {
|
||||||
log.WithField("err", err).Error("Failed to unmarshal DNS config")
|
log.WithField("err", err).Error("Failed to unmarshal DNS config")
|
||||||
}
|
}
|
||||||
_, err := coreDnsConfig.Build()
|
_, err := coreDnsConfig.Build()
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
"github.com/InazumaV/V2bX/api/panel"
|
"github.com/InazumaV/V2bX/api/panel"
|
||||||
"github.com/InazumaV/V2bX/conf"
|
"github.com/InazumaV/V2bX/conf"
|
||||||
"github.com/goccy/go-json"
|
|
||||||
"github.com/xtls/xray-core/common/net"
|
"github.com/xtls/xray-core/common/net"
|
||||||
"github.com/xtls/xray-core/core"
|
"github.com/xtls/xray-core/core"
|
||||||
coreConf "github.com/xtls/xray-core/infra/conf"
|
coreConf "github.com/xtls/xray-core/infra/conf"
|
||||||
@@ -26,8 +28,12 @@ func buildInbound(option *conf.Options, nodeInfo *panel.NodeInfo, tag string) (*
|
|||||||
err = buildV2ray(option, nodeInfo, in)
|
err = buildV2ray(option, nodeInfo, in)
|
||||||
network = nodeInfo.VAllss.Network
|
network = nodeInfo.VAllss.Network
|
||||||
case "trojan":
|
case "trojan":
|
||||||
err = buildTrojan(option, in)
|
err = buildTrojan(option, nodeInfo, in)
|
||||||
network = "tcp"
|
if nodeInfo.Trojan.Network != "" {
|
||||||
|
network = nodeInfo.Trojan.Network
|
||||||
|
} else {
|
||||||
|
network = "tcp"
|
||||||
|
}
|
||||||
case "shadowsocks":
|
case "shadowsocks":
|
||||||
err = buildShadowsocks(option, nodeInfo, in)
|
err = buildShadowsocks(option, nodeInfo, in)
|
||||||
network = "tcp"
|
network = "tcp"
|
||||||
@@ -69,8 +75,13 @@ func buildInbound(option *conf.Options, nodeInfo *panel.NodeInfo, tag string) (*
|
|||||||
in.StreamSetting.TCPSettings = tcpSetting
|
in.StreamSetting.TCPSettings = tcpSetting
|
||||||
}
|
}
|
||||||
case "ws":
|
case "ws":
|
||||||
in.StreamSetting.WSSettings = &coreConf.WebSocketConfig{
|
if in.StreamSetting.WSSettings != nil {
|
||||||
AcceptProxyProtocol: option.XrayOptions.EnableProxyProtocol} //Enable proxy protocol
|
in.StreamSetting.WSSettings.AcceptProxyProtocol = option.XrayOptions.EnableProxyProtocol
|
||||||
|
} else {
|
||||||
|
in.StreamSetting.WSSettings = &coreConf.WebSocketConfig{
|
||||||
|
AcceptProxyProtocol: option.XrayOptions.EnableProxyProtocol,
|
||||||
|
} //Enable proxy protocol
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
socketConfig := &coreConf.SocketConfig{
|
socketConfig := &coreConf.SocketConfig{
|
||||||
AcceptProxyProtocol: option.XrayOptions.EnableProxyProtocol,
|
AcceptProxyProtocol: option.XrayOptions.EnableProxyProtocol,
|
||||||
@@ -105,9 +116,17 @@ func buildInbound(option *conf.Options, nodeInfo *panel.NodeInfo, tag string) (*
|
|||||||
// Reality
|
// Reality
|
||||||
in.StreamSetting.Security = "reality"
|
in.StreamSetting.Security = "reality"
|
||||||
v := nodeInfo.VAllss
|
v := nodeInfo.VAllss
|
||||||
|
dest := v.TlsSettings.Dest
|
||||||
|
if dest == "" {
|
||||||
|
dest = v.TlsSettings.ServerName
|
||||||
|
}
|
||||||
|
xver := v.TlsSettings.Xver
|
||||||
|
if xver == 0 {
|
||||||
|
xver = v.RealityConfig.Xver
|
||||||
|
}
|
||||||
d, err := json.Marshal(fmt.Sprintf(
|
d, err := json.Marshal(fmt.Sprintf(
|
||||||
"%s:%s",
|
"%s:%s",
|
||||||
v.TlsSettings.ServerName,
|
dest,
|
||||||
v.TlsSettings.ServerPort))
|
v.TlsSettings.ServerPort))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("marshal reality dest error: %s", err)
|
return nil, fmt.Errorf("marshal reality dest error: %s", err)
|
||||||
@@ -115,14 +134,17 @@ func buildInbound(option *conf.Options, nodeInfo *panel.NodeInfo, tag string) (*
|
|||||||
mtd, _ := time.ParseDuration(v.RealityConfig.MaxTimeDiff)
|
mtd, _ := time.ParseDuration(v.RealityConfig.MaxTimeDiff)
|
||||||
in.StreamSetting.REALITYSettings = &coreConf.REALITYConfig{
|
in.StreamSetting.REALITYSettings = &coreConf.REALITYConfig{
|
||||||
Dest: d,
|
Dest: d,
|
||||||
Xver: v.RealityConfig.Xver,
|
Xver: xver,
|
||||||
|
Show: false,
|
||||||
ServerNames: []string{v.TlsSettings.ServerName},
|
ServerNames: []string{v.TlsSettings.ServerName},
|
||||||
PrivateKey: v.TlsSettings.PrivateKey,
|
PrivateKey: v.TlsSettings.PrivateKey,
|
||||||
MinClientVer: v.RealityConfig.MinClientVer,
|
MinClientVer: v.RealityConfig.MinClientVer,
|
||||||
MaxClientVer: v.RealityConfig.MaxClientVer,
|
MaxClientVer: v.RealityConfig.MaxClientVer,
|
||||||
MaxTimeDiff: uint64(mtd.Microseconds()),
|
MaxTimeDiff: uint64(mtd.Microseconds()),
|
||||||
ShortIds: []string{v.TlsSettings.ShortId},
|
ShortIds: []string{v.TlsSettings.ShortId},
|
||||||
|
Mldsa65Seed: v.TlsSettings.Mldsa65Seed,
|
||||||
}
|
}
|
||||||
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
in.Tag = tag
|
in.Tag = tag
|
||||||
@@ -150,8 +172,27 @@ func buildV2ray(config *conf.Options, nodeInfo *panel.NodeInfo, inbound *coreCon
|
|||||||
inbound.Settings = (*json.RawMessage)(&s)
|
inbound.Settings = (*json.RawMessage)(&s)
|
||||||
} else {
|
} else {
|
||||||
var err error
|
var err error
|
||||||
|
decryption := "none"
|
||||||
|
if nodeInfo.VAllss.Encryption != "" {
|
||||||
|
switch nodeInfo.VAllss.Encryption {
|
||||||
|
case "mlkem768x25519plus":
|
||||||
|
encSettings := nodeInfo.VAllss.EncryptionSettings
|
||||||
|
parts := []string{
|
||||||
|
| ||||||