This commit is contained in:
yuzuki999 2022-06-02 01:35:41 +08:00
commit 358b5888b4
76 changed files with 47129 additions and 0 deletions

27
.github/ISSUE_TEMPLATE/bug-report.md vendored Normal file
View File

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

View File

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

33
.github/build/friendly-filenames.json vendored Normal file
View File

@ -0,0 +1,33 @@
{
"android-arm64": { "friendlyName": "android-arm64-v8a" },
"darwin-amd64": { "friendlyName": "macos-64" },
"darwin-arm64": { "friendlyName": "macos-arm64-v8a" },
"dragonfly-amd64": { "friendlyName": "dragonfly-64" },
"freebsd-386": { "friendlyName": "freebsd-32" },
"freebsd-amd64": { "friendlyName": "freebsd-64" },
"freebsd-arm64": { "friendlyName": "freebsd-arm64-v8a" },
"freebsd-arm7": { "friendlyName": "freebsd-arm32-v7a" },
"linux-386": { "friendlyName": "linux-32" },
"linux-amd64": { "friendlyName": "linux-64" },
"linux-arm5": { "friendlyName": "linux-arm32-v5" },
"linux-arm64": { "friendlyName": "linux-arm64-v8a" },
"linux-arm6": { "friendlyName": "linux-arm32-v6" },
"linux-arm7": { "friendlyName": "linux-arm32-v7a" },
"linux-mips64le": { "friendlyName": "linux-mips64le" },
"linux-mips64": { "friendlyName": "linux-mips64" },
"linux-mipslesoftfloat": { "friendlyName": "linux-mips32le-softfloat" },
"linux-mipsle": { "friendlyName": "linux-mips32le" },
"linux-mipssoftfloat": { "friendlyName": "linux-mips32-softfloat" },
"linux-mips": { "friendlyName": "linux-mips32" },
"linux-ppc64le": { "friendlyName": "linux-ppc64le" },
"linux-ppc64": { "friendlyName": "linux-ppc64" },
"linux-riscv64": { "friendlyName": "linux-riscv64" },
"linux-s390x": { "friendlyName": "linux-s390x" },
"openbsd-386": { "friendlyName": "openbsd-32" },
"openbsd-amd64": { "friendlyName": "openbsd-64" },
"openbsd-arm64": { "friendlyName": "openbsd-arm64-v8a" },
"openbsd-arm7": { "friendlyName": "openbsd-arm32-v7a" },
"windows-386": { "friendlyName": "windows-32" },
"windows-amd64": { "friendlyName": "windows-64" },
"windows-arm7": { "friendlyName": "windows-arm32-v7a" }
}

11
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,11 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "gomod" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "daily"

67
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@ -0,0 +1,67 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ master ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ master ]
schedule:
- cron: '43 22 * * 3'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
language: [ 'go' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# 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:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# 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.
# 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
# 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@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ 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
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

40
.github/workflows/docker.yml vendored Normal file
View File

@ -0,0 +1,40 @@
name: Publish Docker image
on:
workflow_dispatch:
push:
tags:
- 'v*'
jobs:
push_to_registry:
name: Push Docker image to Docker Hub
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v2
-
name: Docker meta
id: docker_meta
uses: crazy-max/ghaction-docker-meta@v1
with:
images: misakano7545/xrayr
tag-semver: |
{{version}}
{{major}}.{{minor}}
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
-
name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
-
name: Build and push
uses: docker/build-push-action@v2
with:
context: .
platforms: linux/arm/v7,linux/arm64,linux/amd64,linux/s390x
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.docker_meta.outputs.tags }}
labels: ${{ steps.docker_meta.outputs.labels }}

185
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,185 @@
name: Build and Release
on:
workflow_dispatch:
push:
branches:
- master
paths:
- "**/*.go"
- "go.mod"
- "go.sum"
- ".github/workflows/*.yml"
pull_request:
types: [opened, synchronize, reopened]
paths:
- "**/*.go"
- "go.mod"
- "go.sum"
- ".github/workflows/*.yml"
release:
types: [published]
jobs:
build:
strategy:
matrix:
# Include amd64 on all platforms.
goos: [windows, freebsd, linux, dragonfly, darwin]
goarch: [amd64, 386]
exclude:
# Exclude i386 on darwin and dragonfly.
- goarch: 386
goos: dragonfly
- goarch: 386
goos: darwin
include:
# BEIGIN MacOS ARM64
- goos: darwin
goarch: arm64
# END MacOS ARM64
# BEGIN Linux ARM 5 6 7
- goos: linux
goarch: arm
goarm: 7
- goos: linux
goarch: arm
goarm: 6
- goos: linux
goarch: arm
goarm: 5
# END Linux ARM 5 6 7
# BEGIN Android ARM 8
- goos: android
goarch: arm64
# END Android ARM 8
# BEGIN Other architectures
# BEGIN riscv64 & ARM64
- goos: linux
goarch: arm64
- goos: linux
goarch: riscv64
# END riscv64 & ARM64
# BEGIN MIPS
- goos: linux
goarch: mips64
- goos: linux
goarch: mips64le
- goos: linux
goarch: mipsle
- goos: linux
goarch: mips
# END MIPS
# BEGIN PPC
- goos: linux
goarch: ppc64
- goos: linux
goarch: ppc64le
# END PPC
# BEGIN FreeBSD ARM
- goos: freebsd
goarch: arm64
- goos: freebsd
goarch: arm
goarm: 7
# END FreeBSD ARM
# BEGIN S390X
- goos: linux
goarch: s390x
# END S390X
# END Other architectures
fail-fast: false
runs-on: ubuntu-latest
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
GOARM: ${{ matrix.goarm }}
CGO_ENABLED: 0
steps:
- name: Checkout codebase
uses: actions/checkout@v2
- name: Show workflow information
id: get_filename
run: |
export _NAME=$(jq ".[\"$GOOS-$GOARCH$GOARM$GOMIPS\"].friendlyName" -r < .github/build/friendly-filenames.json)
echo "GOOS: $GOOS, GOARCH: $GOARCH, GOARM: $GOARM, GOMIPS: $GOMIPS, RELEASE_NAME: $_NAME"
echo "::set-output name=ASSET_NAME::$_NAME"
echo "ASSET_NAME=$_NAME" >> $GITHUB_ENV
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: ^1.18
- name: Get project dependencies
run: go mod download
- name: Build XrayR
run: |
mkdir -p build_assets
go build -v -o build_assets/XrayR -trimpath -ldflags "-s -w -buildid=" ./main
- name: Build Mips softfloat XrayR
if: matrix.goarch == 'mips' || matrix.goarch == 'mipsle'
run: |
GOMIPS=softfloat go build -v -o build_assets/XrayR_softfloat -trimpath -ldflags "-s -w -buildid=" ./main
- name: Rename Windows XrayR
if: matrix.goos == 'windows'
run: |
cd ./build_assets || exit 1
mv XrayR XrayR.exe
- name: Prepare to release
run: |
cp ${GITHUB_WORKSPACE}/README.md ./build_assets/README.md
cp ${GITHUB_WORKSPACE}/LICENSE ./build_assets/LICENSE
cp ${GITHUB_WORKSPACE}/main/dns.json ./build_assets/dns.json
cp ${GITHUB_WORKSPACE}/main/route.json ./build_assets/route.json
cp ${GITHUB_WORKSPACE}/main/custom_outbound.json ./build_assets/custom_outbound.json
cp ${GITHUB_WORKSPACE}/main/custom_inbound.json ./build_assets/custom_inbound.json
cp ${GITHUB_WORKSPACE}/main/rulelist ./build_assets/rulelist
cp ${GITHUB_WORKSPACE}/main/config.yml.example ./build_assets/config.yml
LIST=('geoip geoip geoip' 'domain-list-community dlc geosite')
for i in "${LIST[@]}"
do
INFO=($(echo $i | awk 'BEGIN{FS=" ";OFS=" "} {print $1,$2,$3}'))
LASTEST_TAG="$(curl -sL "https://api.github.com/repos/v2fly/${INFO[0]}/releases" | jq -r ".[0].tag_name" || echo "latest")"
FILE_NAME="${INFO[2]}.dat"
echo -e "Downloading ${FILE_NAME}..."
curl -L "https://github.com/v2fly/${INFO[0]}/releases/download/${LASTEST_TAG}/${INFO[1]}.dat" -o ./build_assets/${FILE_NAME}
echo -e "Verifying HASH key..."
HASH="$(curl -sL "https://github.com/v2fly/${INFO[0]}/releases/download/${LASTEST_TAG}/${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
- name: Create ZIP archive
shell: bash
run: |
pushd build_assets || exit 1
touch -mt $(date +%Y01010000) *
zip -9vr ../XrayR-$ASSET_NAME.zip .
popd || exit 1
FILE=./XrayR-$ASSET_NAME.zip
DGST=$FILE.dgst
for METHOD in {"md5","sha1","sha256","sha512"}
do
openssl dgst -$METHOD $FILE | sed 's/([^)]*)//g' >>$DGST
done
- name: Change the name
run: |
mv build_assets XrayR-$ASSET_NAME
- name: Upload files to Artifacts
uses: actions/upload-artifact@v2
with:
name: XrayR-${{ steps.get_filename.outputs.ASSET_NAME }}
path: |
./XrayR-${{ steps.get_filename.outputs.ASSET_NAME }}/*
- name: Upload binaries to release
uses: svenstaro/upload-release-action@v2
if: github.event_name == 'release'
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: ./XrayR-${{ steps.get_filename.outputs.ASSET_NAME }}.zip*
tag: ${{ github.ref }}
file_glob: true

16
.gitignore vendored Normal file
View File

@ -0,0 +1,16 @@
main/config.yml
main/main
main/XrayR
main/XrayR*
main/mytest
main/access.logo
main/error.log
api/chooseparser.go.bak
common/Inboundbuilder/.lego/
common/legocmd/.lego/
.vscode/launch.json
main/.lego
main/cert
main/config.yml
./vscode
.idea/*

18
Dockerfile Normal file
View File

@ -0,0 +1,18 @@
# Build go
FROM golang:1.18.1-alpine AS builder
WORKDIR /app
COPY . .
ENV CGO_ENABLED=0
RUN go mod download && \
go env -w GOFLAGS=-buildvcs=false && \
go build -v -o XrayR -trimpath -ldflags "-s -w -buildid=" ./main
# Release
FROM alpine
# 安装必要的工具包
RUN apk --update --no-cache add tzdata ca-certificates && \
cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
mkdir /etc/XrayR/
COPY --from=builder /app/XrayR /usr/local/bin
ENTRYPOINT [ "XrayR", "--config", "/etc/XrayR/config.yml"]

373
LICENSE Normal file
View File

@ -0,0 +1,373 @@
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.

101
README.md Normal file
View File

@ -0,0 +1,101 @@
# XrayR
[![](https://img.shields.io/badge/TgChat-@XrayR讨论-blue.svg)](https://t.me/XrayR_project)
[![](https://img.shields.io/badge/Channel-@XrayR通知-blue.svg)](https://t.me/XrayR_channel)
A Xray backend framework that can easily support many panels.
一个基于Xray的后端框架支持V2ay,Trojan,Shadowsocks协议极易扩展支持多面板对接。
如果您喜欢本项目可以右上角点个star+watch持续关注本项目的进展。
使用教程:[详细使用教程](https://crackair.gitbook.io/xrayr-project/)
如对脚本不放心可使用此沙箱先测一遍再使用https://killercoda.com/playgrounds/scenario/ubuntu
## 免责声明
本项目只是本人个人学习开发并维护,本人不保证任何可用性,也不对使用本软件造成的任何后果负责。
## 特点
* 永久开源且免费。
* 支持V2rayTrojan Shadowsocks多种协议。
* 支持Vless和XTLS等新特性。
* 支持单实例对接多面板、多节点,无需重复启动。
* 支持限制在线IP
* 支持节点端口级别、用户级别限速。
* 配置简单明了。
* 修改配置自动重启实例。
* 方便编译和升级,可以快速更新核心版本, 支持Xray-core新特性。
## 功能介绍
| 功能 | v2ray | trojan | shadowsocks |
| --------------- | ----- | ------ | ----------- |
| 获取节点信息 | √ | √ | √ |
| 获取用户信息 | √ | √ | √ |
| 用户流量统计 | √ | √ | √ |
| 服务器信息上报 | √ | √ | √ |
| 自动申请tls证书 | √ | √ | √ |
| 自动续签tls证书 | √ | √ | √ |
| 在线人数统计 | √ | √ | √ |
| 在线用户限制 | √ | √ | √ |
| 审计规则 | √ | √ | √ |
| 节点端口限速 | √ | √ | √ |
| 按照用户限速 | √ | √ | √ |
| 自定义DNS | √ | √ | √ |
## 支持前端
| 前端 | v2ray | trojan | shadowsocks |
| ------------------------------------------------------ | ----- | ------ | ------------------------------ |
| sspanel-uim | √ | √ | √ (单端口多用户和V2ray-Plugin) |
| v2board | √ | √ | √ |
| [PMPanel](https://github.com/ByteInternetHK/PMPanel) | √ | √ | √ |
| [ProxyPanel](https://github.com/ProxyPanel/ProxyPanel) | √ | √ | √ |
## TODO
* 支持WARP Socks5代理模式分流
## 软件安装
### 一键安装
```
wget -N https://raw.githubusercontents.com/Yuzuki616/V2bX-script/master/install.sh && bash install.sh
```
### 使用Docker部署
[Docker部署教程](https://crackair.gitbook.io/xrayr-project/xrayr-xia-zai-he-an-zhuang/install/docker)
### 手动安装
[手动安装教程](https://crackair.gitbook.io/xrayr-project/xrayr-xia-zai-he-an-zhuang/install/manual)
## 配置文件及详细使用教程
[详细使用教程](https://crackair.gitbook.io/xrayr-project/)
## Thanks
* [Project X](https://github.com/XTLS/)
* [V2Fly](https://github.com/v2fly)
* [VNet-V2ray](https://github.com/ProxyPanel/VNet-V2ray)
* [Air-Universe](https://github.com/crossfw/Air-Universe)
## Licence
[Mozilla Public License Version 2.0](https://github.com/XrayR-project/XrayR/blob/master/LICENSE)
## Telgram
[XrayR后端讨论](https://t.me/XrayR_project)
[XrayR通知](https://t.me/XrayR_channel)
## Stars 增长记录
[![Stargazers over time](https://starchart.cc/Yuzuki616/V2bX.svg)](https://starchart.cc/Yuzuki616/V2bX)

14
api/api.go Normal file
View File

@ -0,0 +1,14 @@
// Package api contains all the api used by XrayR
// To implement an api , one needs to implement the interface below.
package api
// API is the interface for different panel's api.
type API interface {
GetNodeInfo() (nodeInfo *NodeInfo, err error)
GetUserList() (userList *[]UserInfo, err error)
ReportUserTraffic(userTraffic *[]UserTraffic) (err error)
Describe() ClientInfo
GetNodeRule() (ruleList *[]DetectRule, err error)
Debug()
}

119
api/apimodel.go Normal file
View File

@ -0,0 +1,119 @@
package api
import (
"github.com/xtls/xray-core/infra/conf"
"regexp"
)
// API config
type Config struct {
APIHost string `mapstructure:"ApiHost"`
NodeID int `mapstructure:"NodeID"`
Key string `mapstructure:"ApiKey"`
NodeType string `mapstructure:"NodeType"`
EnableVless bool `mapstructure:"EnableVless"`
EnableXTLS bool `mapstructure:"EnableXTLS"`
EnableSS2022 bool `mapstructure:"EnableSS2022"`
Timeout int `mapstructure:"Timeout"`
SpeedLimit float64 `mapstructure:"SpeedLimit"`
DeviceLimit int `mapstructure:"DeviceLimit"`
RuleListPath string `mapstructure:"RuleListPath"`
DisableCustomConfig bool `mapstructure:"DisableCustomConfig"`
}
type OnlineUser struct {
UID int
IP string
}
type UserTraffic struct {
UID int
Email string
Upload int64
Download int64
}
type ClientInfo struct {
APIHost string
NodeID int
Key string
NodeType string
}
type DetectRule struct {
ID int
Pattern *regexp.Regexp
}
type DetectResult struct {
UID int
RuleID int
}
type V2RayUserInfo struct {
Uuid string `json:"uuid"`
Email string `json:"email"`
AlterId int `json:"alter_id"`
}
type TrojanUserInfo struct {
Password string `json:"password"`
}
type UserInfo struct {
UID int `json:"id"`
DeviceLimit int `json:"device_limit"`
SpeedLimit uint64 `json:"speed_limit"`
Port int `json:"port"`
Cipher string `json:"cipher"`
Secret string `json:"secret"`
V2rayUser *V2RayUserInfo `json:"v2ray_user"`
TrojanUser *TrojanUserInfo `json:"trojan_user"`
}
func (p *UserInfo) GetUserEmail() string {
if p.V2rayUser != nil {
return p.V2rayUser.Email
} else if p.TrojanUser != nil {
return p.TrojanUser.Password
}
return p.Cipher
}
type NodeInfo struct {
NodeType string
NodeId int
TLSType string
EnableVless bool
EnableTls bool
V2ray *V2rayConfig
Trojan *TrojanConfig
SS *SSConfig
}
type SSConfig struct {
Port int `json:"port"`
TransportProtocol string `json:"transportProtocol"`
CypherMethod string `json:"cypher"`
}
type V2rayConfig struct {
Inbounds []conf.InboundDetourConfig `json:"inbounds"`
Routing *struct {
Rules []Rule `json:"rules"`
} `json:"routing"`
}
type Rule struct {
Type string `json:"type"`
InboundTag string `json:"inboundTag,omitempty"`
OutboundTag string `json:"outboundTag"`
Domain []string `json:"domain,omitempty"`
Protocol []string `json:"protocol,omitempty"`
}
type TrojanConfig struct {
LocalPort int `json:"local_port"`
Password []interface{} `json:"password"`
TransportProtocol string
Ssl struct {
Sni string `json:"sni"`
} `json:"ssl"`
}

7
api/v2board/model.go Normal file
View File

@ -0,0 +1,7 @@
package v2board
type UserTraffic struct {
UID int `json:"user_id"`
Upload int64 `json:"u"`
Download int64 `json:"d"`
}

315
api/v2board/v2board.go Normal file
View File

@ -0,0 +1,315 @@
package v2board
import (
"bufio"
"fmt"
"github.com/Yuzuki616/V2bX/api"
"github.com/go-resty/resty/v2"
json "github.com/goccy/go-json"
"log"
"os"
"regexp"
"strconv"
"sync"
"time"
)
// APIClient create an api client to the panel.
type APIClient struct {
client *resty.Client
APIHost string
NodeID int
Key string
NodeType string
EnableVless bool
EnableXTLS bool
SpeedLimit float64
DeviceLimit int
LocalRuleList []api.DetectRule
RemoteRuleCache *api.Rule
access sync.Mutex
}
// New create an api instance
func New(apiConfig *api.Config) *APIClient {
client := resty.New()
client.SetRetryCount(3)
if apiConfig.Timeout > 0 {
client.SetTimeout(time.Duration(apiConfig.Timeout) * time.Second)
} else {
client.SetTimeout(5 * time.Second)
}
client.OnError(func(req *resty.Request, err error) {
if v, ok := err.(*resty.ResponseError); ok {
// v.Response contains the last response from the server
// v.Err contains the original error
log.Print(v.Err)
}
})
client.SetBaseURL(apiConfig.APIHost)
// Create Key for each requests
client.SetQueryParams(map[string]string{
"node_id": strconv.Itoa(apiConfig.NodeID),
"token": apiConfig.Key,
})
// Read local rule list
localRuleList := readLocalRuleList(apiConfig.RuleListPath)
apiClient := &APIClient{
client: client,
NodeID: apiConfig.NodeID,
Key: apiConfig.Key,
APIHost: apiConfig.APIHost,
NodeType: apiConfig.NodeType,
EnableVless: apiConfig.EnableVless,
EnableXTLS: apiConfig.EnableXTLS,
SpeedLimit: apiConfig.SpeedLimit,
DeviceLimit: apiConfig.DeviceLimit,
LocalRuleList: localRuleList,
}
return apiClient
}
// readLocalRuleList reads the local rule list file
func readLocalRuleList(path string) (LocalRuleList []api.DetectRule) {
LocalRuleList = make([]api.DetectRule, 0)
if path != "" {
// open the file
file, err := os.Open(path)
//handle errors while opening
if err != nil {
log.Printf("Error when opening file: %s", err)
return LocalRuleList
}
fileScanner := bufio.NewScanner(file)
// read line by line
for fileScanner.Scan() {
LocalRuleList = append(LocalRuleList, api.DetectRule{
ID: -1,
Pattern: regexp.MustCompile(fileScanner.Text()),
})
}
// handle first encountered error while reading
if err := fileScanner.Err(); err != nil {
log.Fatalf("Error while reading file: %s", err)
return make([]api.DetectRule, 0)
}
file.Close()
}
return LocalRuleList
}
// Describe return a description of the client
func (c *APIClient) Describe() api.ClientInfo {
return api.ClientInfo{APIHost: c.APIHost, NodeID: c.NodeID, Key: c.Key, NodeType: c.NodeType}
}
// Debug set the client debug for client
func (c *APIClient) Debug() {
c.client.SetDebug(true)
}
func (c *APIClient) assembleURL(path string) string {
return c.APIHost + path
}
func (c *APIClient) checkResponse(res *resty.Response, path string, err error) error {
if err != nil {
return fmt.Errorf("request %s failed: %s", c.assembleURL(path), err)
}
if res.StatusCode() > 400 {
body := res.Body()
return fmt.Errorf("request %s failed: %s, %s", c.assembleURL(path), string(body), err)
}
return nil
}
// GetNodeInfo will pull NodeInfo Config from sspanel
func (c *APIClient) GetNodeInfo() (nodeInfo *api.NodeInfo, err error) {
var path string
var res *resty.Response
switch c.NodeType {
case "V2ray":
path = "/api/v1/server/Deepbwork/config"
res, err = c.client.R().
SetQueryParam("local_port", "1").
ForceContentType("application/json").
Get(path)
case "Trojan":
path = "/api/v1/server/TrojanTidalab/config"
case "Shadowsocks":
if nodeInfo, err = c.ParseSSNodeResponse(); err == nil {
return nodeInfo, nil
} else {
return nil, err
}
default:
return nil, fmt.Errorf("unsupported Node type: %s", c.NodeType)
}
res, err = c.client.R().
SetQueryParam("local_port", "1").
ForceContentType("application/json").
Get(path)
err = c.checkResponse(res, path, err)
if err != nil {
return nil, err
}
c.access.Lock()
defer c.access.Unlock()
switch c.NodeType {
case "V2ray":
nodeInfo, err = c.ParseV2rayNodeResponse(res.Body())
case "Trojan":
nodeInfo, err = c.ParseTrojanNodeResponse(res.Body())
case "Shadowsocks":
nodeInfo, err = c.ParseSSNodeResponse()
default:
return nil, fmt.Errorf("unsupported Node type: %s", c.NodeType)
}
return nodeInfo, nil
}
// GetUserList will pull user form sspanel
func (c *APIClient) GetUserList() (UserList *[]api.UserInfo, err error) {
var path string
switch c.NodeType {
case "V2ray":
path = "/api/v1/server/Deepbwork/user"
case "Trojan":
path = "/api/v1/server/TrojanTidalab/user"
case "Shadowsocks":
path = "/api/v1/server/ShadowsocksTidalab/user"
default:
return nil, fmt.Errorf("unsupported Node type: %s", c.NodeType)
}
res, err := c.client.R().
ForceContentType("application/json").
Get(path)
var userList []api.UserInfo
err = c.checkResponse(res, path, err)
if err != nil {
return nil, err
}
err = json.Unmarshal(res.Body(), &userList)
if err != nil {
return nil, fmt.Errorf("unmarshal userlist error: %s", err)
}
return &userList, nil
}
// ReportUserTraffic reports the user traffic
func (c *APIClient) ReportUserTraffic(userTraffic *[]api.UserTraffic) error {
var path string
switch c.NodeType {
case "V2ray":
path = "/api/v1/server/Deepbwork/submit"
case "Trojan":
path = "/api/v1/server/TrojanTidalab/submit"
case "Shadowsocks":
path = "/api/v1/server/ShadowsocksTidalab/submit"
}
data := make([]UserTraffic, len(*userTraffic))
for i, traffic := range *userTraffic {
data[i] = UserTraffic{
UID: traffic.UID,
Upload: traffic.Upload,
Download: traffic.Download}
}
res, err := c.client.R().
SetQueryParam("node_id", strconv.Itoa(c.NodeID)).
SetBody(data).
ForceContentType("application/json").
Post(path)
err = c.checkResponse(res, path, err)
if err != nil {
return err
}
return nil
}
// GetNodeRule implements the API interface
func (c *APIClient) GetNodeRule() (*[]api.DetectRule, error) {
ruleList := c.LocalRuleList
if c.NodeType != "V2ray" {
return &ruleList, nil
}
// V2board only support the rule for v2ray
// fix: reuse config response
c.access.Lock()
defer c.access.Unlock()
for i, rule := range c.RemoteRuleCache.Domain {
ruleListItem := api.DetectRule{
ID: i,
Pattern: regexp.MustCompile(rule),
}
ruleList = append(ruleList, ruleListItem)
}
return &ruleList, nil
}
// ParseTrojanNodeResponse parse the response for the given nodeinfor format
func (c *APIClient) ParseTrojanNodeResponse(body []byte) (*api.NodeInfo, error) {
node := &api.NodeInfo{Trojan: &api.TrojanConfig{}}
err := json.Unmarshal(body, node.Trojan)
if err != nil {
return nil, fmt.Errorf("unmarshal nodeinfo error: %s", err)
}
node.NodeId = c.NodeID
node.NodeType = c.NodeType
return node, nil
}
// ParseSSNodeResponse parse the response for the given nodeinfor format
func (c *APIClient) ParseSSNodeResponse() (*api.NodeInfo, error) {
var port int
var method string
userInfo, err := c.GetUserList()
if err != nil {
return nil, err
}
if len(*userInfo) > 0 {
port = (*userInfo)[0].Port
method = (*userInfo)[0].Cipher
}
if err != nil {
return nil, err
}
node := &api.NodeInfo{
NodeType: c.NodeType,
NodeId: c.NodeID,
SS: &api.SSConfig{
Port: port,
TransportProtocol: "tcp",
CypherMethod: method,
},
}
return node, nil
}
// ParseV2rayNodeResponse parse the response for the given nodeinfor format
func (c *APIClient) ParseV2rayNodeResponse(body []byte) (*api.NodeInfo, error) {
node := &api.NodeInfo{V2ray: &api.V2rayConfig{}}
node.NodeType = c.NodeType
node.NodeId = c.NodeID
c.RemoteRuleCache = &node.V2ray.Routing.Rules[0]
node.V2ray.Routing = nil
if c.EnableXTLS {
node.TLSType = "xtls"
} else {
node.TLSType = "tls"
}
node.EnableVless = c.EnableVless
node.EnableTls = node.V2ray.Inbounds[0].StreamSetting.Security == "tls"
return node, nil
}

101
api/v2board/v2board_test.go Normal file
View File

@ -0,0 +1,101 @@
package v2board_test
import (
"testing"
"github.com/Yuzuki616/V2bX/api"
"github.com/Yuzuki616/V2bX/api/v2board"
)
func CreateClient() api.API {
apiConfig := &api.Config{
APIHost: "http://localhost:9897",
Key: "qwertyuiopasdfghjkl",
NodeID: 1,
NodeType: "V2ray",
}
client := v2board.New(apiConfig)
return client
}
func TestGetV2rayNodeinfo(t *testing.T) {
client := CreateClient()
nodeInfo, err := client.GetNodeInfo()
if err != nil {
t.Error(err)
}
t.Log(nodeInfo)
}
func TestGetSSNodeinfo(t *testing.T) {
apiConfig := &api.Config{
APIHost: "http://127.0.0.1:668",
Key: "qwertyuiopasdfghjkl",
NodeID: 1,
NodeType: "Shadowsocks",
}
client := v2board.New(apiConfig)
nodeInfo, err := client.GetNodeInfo()
if err != nil {
t.Error(err)
}
t.Log(nodeInfo)
}
func TestGetTrojanNodeinfo(t *testing.T) {
apiConfig := &api.Config{
APIHost: "http://127.0.0.1:668",
Key: "qwertyuiopasdfghjkl",
NodeID: 1,
NodeType: "Trojan",
}
client := v2board.New(apiConfig)
nodeInfo, err := client.GetNodeInfo()
if err != nil {
t.Error(err)
}
t.Log(nodeInfo)
}
func TestGetUserList(t *testing.T) {
client := CreateClient()
userList, err := client.GetUserList()
if err != nil {
t.Error(err)
}
t.Log(userList)
}
func TestReportReportUserTraffic(t *testing.T) {
client := CreateClient()
userList, err := client.GetUserList()
if err != nil {
t.Error(err)
}
generalUserTraffic := make([]api.UserTraffic, len(*userList))
for i, userInfo := range *userList {
generalUserTraffic[i] = api.UserTraffic{
UID: userInfo.UID,
Upload: 114514,
Download: 114514,
}
}
//client.Debug()
err = client.ReportUserTraffic(&generalUserTraffic)
if err != nil {
t.Error(err)
}
}
func TestGetNodeRule(t *testing.T) {
client := CreateClient()
client.Debug()
ruleList, err := client.GetNodeRule()
if err != nil {
t.Error(err)
}
t.Log(ruleList)
}

2
app/app.go Normal file
View File

@ -0,0 +1,2 @@
// Package app contains the third-party app used to replace the default app in xray-core
package app

View File

@ -0,0 +1,205 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.28.0
// protoc v3.19.4
// source: app/mydispatcher/config.proto
package mydispatcher
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type SessionConfig struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
}
func (x *SessionConfig) Reset() {
*x = SessionConfig{}
if protoimpl.UnsafeEnabled {
mi := &file_app_mydispatcher_config_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *SessionConfig) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SessionConfig) ProtoMessage() {}
func (x *SessionConfig) ProtoReflect() protoreflect.Message {
mi := &file_app_mydispatcher_config_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SessionConfig.ProtoReflect.Descriptor instead.
func (*SessionConfig) Descriptor() ([]byte, []int) {
return file_app_mydispatcher_config_proto_rawDescGZIP(), []int{0}
}
type Config struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Settings *SessionConfig `protobuf:"bytes,1,opt,name=settings,proto3" json:"settings,omitempty"`
}
func (x *Config) Reset() {
*x = Config{}
if protoimpl.UnsafeEnabled {
mi := &file_app_mydispatcher_config_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Config) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Config) ProtoMessage() {}
func (x *Config) ProtoReflect() protoreflect.Message {
mi := &file_app_mydispatcher_config_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Config.ProtoReflect.Descriptor instead.
func (*Config) Descriptor() ([]byte, []int) {
return file_app_mydispatcher_config_proto_rawDescGZIP(), []int{1}
}
func (x *Config) GetSettings() *SessionConfig {
if x != nil {
return x.Settings
}
return nil
}
var File_app_mydispatcher_config_proto protoreflect.FileDescriptor
var file_app_mydispatcher_config_proto_rawDesc = []byte{
0x0a, 0x1d, 0x61, 0x70, 0x70, 0x2f, 0x6d, 0x79, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68,
0x65, 0x72, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12,
0x16, 0x78, 0x72, 0x61, 0x79, 0x72, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x6d, 0x79, 0x64, 0x69, 0x73,
0x70, 0x61, 0x74, 0x63, 0x68, 0x65, 0x72, 0x22, 0x15, 0x0a, 0x0d, 0x53, 0x65, 0x73, 0x73, 0x69,
0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x22, 0x4b,
0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x41, 0x0a, 0x08, 0x73, 0x65, 0x74, 0x74,
0x69, 0x6e, 0x67, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x78, 0x72, 0x61,
0x79, 0x72, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x6d, 0x79, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63,
0x68, 0x65, 0x72, 0x2e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69,
0x67, 0x52, 0x08, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x42, 0x67, 0x0a, 0x1a, 0x63,
0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x72, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x6d, 0x79, 0x64,
0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x65, 0x72, 0x50, 0x01, 0x5a, 0x2f, 0x67, 0x69, 0x74,
0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x58, 0x72, 0x61, 0x79, 0x52, 0x2d, 0x70, 0x72,
0x6f, 0x6a, 0x65, 0x63, 0x74, 0x2f, 0x58, 0x72, 0x61, 0x79, 0x52, 0x2f, 0x61, 0x70, 0x70, 0x2f,
0x6d, 0x79, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x65, 0x72, 0xaa, 0x02, 0x15, 0x58,
0x72, 0x61, 0x79, 0x52, 0x2e, 0x41, 0x70, 0x70, 0x2e, 0x4d, 0x79, 0x69, 0x73, 0x70, 0x61, 0x74,
0x63, 0x68, 0x65, 0x72, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_app_mydispatcher_config_proto_rawDescOnce sync.Once
file_app_mydispatcher_config_proto_rawDescData = file_app_mydispatcher_config_proto_rawDesc
)
func file_app_mydispatcher_config_proto_rawDescGZIP() []byte {
file_app_mydispatcher_config_proto_rawDescOnce.Do(func() {
file_app_mydispatcher_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_app_mydispatcher_config_proto_rawDescData)
})
return file_app_mydispatcher_config_proto_rawDescData
}
var file_app_mydispatcher_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_app_mydispatcher_config_proto_goTypes = []interface{}{
(*SessionConfig)(nil), // 0: xrayr.app.mydispatcher.SessionConfig
(*Config)(nil), // 1: xrayr.app.mydispatcher.Config
}
var file_app_mydispatcher_config_proto_depIdxs = []int32{
0, // 0: xrayr.app.mydispatcher.Config.settings:type_name -> xrayr.app.mydispatcher.SessionConfig
1, // [1:1] is the sub-list for method output_type
1, // [1:1] is the sub-list for method input_type
1, // [1:1] is the sub-list for extension type_name
1, // [1:1] is the sub-list for extension extendee
0, // [0:1] is the sub-list for field type_name
}
func init() { file_app_mydispatcher_config_proto_init() }
func file_app_mydispatcher_config_proto_init() {
if File_app_mydispatcher_config_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_app_mydispatcher_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_app_mydispatcher_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{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_app_mydispatcher_config_proto_rawDesc,
NumEnums: 0,
NumMessages: 2,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_app_mydispatcher_config_proto_goTypes,
DependencyIndexes: file_app_mydispatcher_config_proto_depIdxs,
MessageInfos: file_app_mydispatcher_config_proto_msgTypes,
}.Build()
File_app_mydispatcher_config_proto = out.File
file_app_mydispatcher_config_proto_rawDesc = nil
file_app_mydispatcher_config_proto_goTypes = nil
file_app_mydispatcher_config_proto_depIdxs = nil
}

View File

@ -0,0 +1,15 @@
syntax = "proto3";
package xrayr.app.mydispatcher;
option csharp_namespace = "XrayR.App.Myispatcher";
option go_package = "github.com/Yuzuki616/V2bX/app/mydispatcher";
option java_package = "com.xrayr.app.mydispatcher";
option java_multiple_files = true;
message SessionConfig {
reserved 1;
}
message Config {
SessionConfig settings = 1;
}

566
app/mydispatcher/default.go Normal file
View File

@ -0,0 +1,566 @@
package mydispatcher
//go:generate go run github.com/xtls/xray-core/common/errors/errorgen
import (
"context"
"fmt"
"strings"
"sync"
"time"
"github.com/Yuzuki616/V2bX/common/limiter"
"github.com/Yuzuki616/V2bX/common/rule"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/log"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/protocol"
"github.com/xtls/xray-core/common/session"
"github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/features/dns"
"github.com/xtls/xray-core/features/outbound"
"github.com/xtls/xray-core/features/policy"
"github.com/xtls/xray-core/features/routing"
routing_session "github.com/xtls/xray-core/features/routing/session"
"github.com/xtls/xray-core/features/stats"
"github.com/xtls/xray-core/transport"
"github.com/xtls/xray-core/transport/pipe"
)
var errSniffingTimeout = newError("timeout on sniffing")
type cachedReader struct {
sync.Mutex
reader *pipe.Reader
cache buf.MultiBuffer
}
func (r *cachedReader) Cache(b *buf.Buffer) {
mb, _ := r.reader.ReadMultiBufferTimeout(time.Millisecond * 100)
r.Lock()
if !mb.IsEmpty() {
r.cache, _ = buf.MergeMulti(r.cache, mb)
}
b.Clear()
rawBytes := b.Extend(buf.Size)
n := r.cache.Copy(rawBytes)
b.Resize(0, int32(n))
r.Unlock()
}
func (r *cachedReader) readInternal() buf.MultiBuffer {
r.Lock()
defer r.Unlock()
if r.cache != nil && !r.cache.IsEmpty() {
mb := r.cache
r.cache = nil
return mb
}
return nil
}
func (r *cachedReader) ReadMultiBuffer() (buf.MultiBuffer, error) {
mb := r.readInternal()
if mb != nil {
return mb, nil
}
return r.reader.ReadMultiBuffer()
}
func (r *cachedReader) ReadMultiBufferTimeout(timeout time.Duration) (buf.MultiBuffer, error) {
mb := r.readInternal()
if mb != nil {
return mb, nil
}
return r.reader.ReadMultiBufferTimeout(timeout)
}
func (r *cachedReader) Interrupt() {
r.Lock()
if r.cache != nil {
r.cache = buf.ReleaseMulti(r.cache)
}
r.Unlock()
r.reader.Interrupt()
}
// DefaultDispatcher is a default implementation of Dispatcher.
type DefaultDispatcher struct {
ohm outbound.Manager
router routing.Router
policy policy.Manager
stats stats.Manager
dns dns.Client
fdns dns.FakeDNSEngine
Limiter *limiter.Limiter
RuleManager *rule.RuleManager
}
func init() {
common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
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 {
core.RequireFeatures(ctx, func(fdns dns.FakeDNSEngine) {
d.fdns = fdns
})
return d.Init(config.(*Config), om, router, pm, sm, dc)
}); err != nil {
return nil, err
}
return d, nil
}))
}
// 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 {
d.ohm = om
d.router = router
d.policy = pm
d.stats = sm
d.Limiter = limiter.New()
d.RuleManager = rule.New()
d.dns = dns
return nil
}
// Type implements common.HasType.
func (*DefaultDispatcher) Type() interface{} {
return routing.DispatcherType()
}
// Start implements common.Runnable.
func (*DefaultDispatcher) Start() error {
return nil
}
// Close implements common.Closable.
func (*DefaultDispatcher) Close() error { return nil }
func (d *DefaultDispatcher) getLink(ctx context.Context, network net.Network, sniffing session.SniffingRequest) (*transport.Link, *transport.Link, error) {
downOpt := pipe.OptionsFromContext(ctx)
upOpt := downOpt
if network == net.Network_UDP {
var ip2domain *sync.Map // net.IP.String() => domain, this map is used by server side when client turn on fakedns
// Client will send domain address in the buffer.UDP.Address, server record all possible target IP addrs.
// When target replies, server will restore the domain and send back to client.
// Note: this map is not global but per connection context
upOpt = append(upOpt, pipe.OnTransmission(func(mb buf.MultiBuffer) buf.MultiBuffer {
for i, buffer := range mb {
if buffer.UDP == nil {
continue
}
addr := buffer.UDP.Address
if addr.Family().IsIP() {
if fkr0, ok := d.fdns.(dns.FakeDNSEngineRev0); ok && fkr0.IsIPInIPPool(addr) && sniffing.Enabled {
domain := fkr0.GetDomainFromFakeDNS(addr)
if len(domain) > 0 {
buffer.UDP.Address = net.DomainAddress(domain)
newError("[fakedns client] override with domain: ", domain, " for xUDP buffer at ", i).WriteToLog(session.ExportIDToError(ctx))
} else {
newError("[fakedns client] failed to find domain! :", addr.String(), " for xUDP buffer at ", i).AtWarning().WriteToLog(session.ExportIDToError(ctx))
}
}
} else {
if ip2domain == nil {
ip2domain = new(sync.Map)
newError("[fakedns client] create a new map").WriteToLog(session.ExportIDToError(ctx))
}
domain := addr.Domain()
ips, err := d.dns.LookupIP(domain, dns.IPOption{true, true, false})
if err == nil {
for _, ip := range ips {
ip2domain.Store(ip.String(), domain)
}
newError("[fakedns client] candidate ip: "+fmt.Sprintf("%v", ips), " for xUDP buffer at ", i).WriteToLog(session.ExportIDToError(ctx))
} else {
newError("[fakedns client] failed to look up IP for ", domain, " for xUDP buffer at ", i).Base(err).WriteToLog(session.ExportIDToError(ctx))
}
}
}
return mb
}))
downOpt = append(downOpt, pipe.OnTransmission(func(mb buf.MultiBuffer) buf.MultiBuffer {
for i, buffer := range mb {
if buffer.UDP == nil {
continue
}
addr := buffer.UDP.Address
if addr.Family().IsIP() {
if ip2domain == nil {
continue
}
if domain, found := ip2domain.Load(addr.IP().String()); found {
buffer.UDP.Address = net.DomainAddress(domain.(string))
newError("[fakedns client] restore domain: ", domain.(string), " for xUDP buffer at ", i).WriteToLog(session.ExportIDToError(ctx))
}
} else {
if fkr0, ok := d.fdns.(dns.FakeDNSEngineRev0); ok {
fakeIp := fkr0.GetFakeIPForDomain(addr.Domain())
buffer.UDP.Address = fakeIp[0]
newError("[fakedns client] restore FakeIP: ", buffer.UDP, fmt.Sprintf("%v", fakeIp), " for xUDP buffer at ", i).WriteToLog(session.ExportIDToError(ctx))
}
}
}
return mb
}))
}
uplinkReader, uplinkWriter := pipe.New(upOpt...)
downlinkReader, downlinkWriter := pipe.New(downOpt...)
inboundLink := &transport.Link{
Reader: downlinkReader,
Writer: uplinkWriter,
}
outboundLink := &transport.Link{
Reader: uplinkReader,
Writer: downlinkWriter,
}
sessionInbound := session.InboundFromContext(ctx)
var user *protocol.MemoryUser
if sessionInbound != nil {
user = sessionInbound.User
}
if user != nil && len(user.Email) > 0 {
// Speed Limit and Device Limit
bucket, ok, reject := d.Limiter.GetUserBucket(sessionInbound.Tag, user.Email, sessionInbound.Source.Address.IP().String())
if reject {
newError("Devices reach the limit: ", user.Email).AtError().WriteToLog()
common.Close(outboundLink.Writer)
common.Close(inboundLink.Writer)
common.Interrupt(outboundLink.Reader)
common.Interrupt(inboundLink.Reader)
return nil, nil, newError("Devices reach the limit: ", user.Email)
}
if ok {
inboundLink.Writer = d.Limiter.RateWriter(inboundLink.Writer, bucket)
outboundLink.Writer = d.Limiter.RateWriter(outboundLink.Writer, bucket)
}
p := d.policy.ForLevel(user.Level)
if p.Stats.UserUplink {
name := "user>>>" + user.Email + ">>>traffic>>>uplink"
if c, _ := stats.GetOrRegisterCounter(d.stats, name); c != nil {
inboundLink.Writer = &SizeStatWriter{
Counter: c,
Writer: inboundLink.Writer,
}
}
}
if p.Stats.UserDownlink {
name := "user>>>" + user.Email + ">>>traffic>>>downlink"
if c, _ := stats.GetOrRegisterCounter(d.stats, name); c != nil {
outboundLink.Writer = &SizeStatWriter{
Counter: c,
Writer: outboundLink.Writer,
}
}
}
}
return inboundLink, outboundLink, nil
}
func (d *DefaultDispatcher) shouldOverride(ctx context.Context, result SniffResult, request session.SniffingRequest, destination net.Destination) bool {
domain := result.Domain()
for _, d := range request.ExcludeForDomain {
if strings.ToLower(domain) == d {
return false
}
}
protocolString := result.Protocol()
if resComp, ok := result.(SnifferResultComposite); ok {
protocolString = resComp.ProtocolForDomainResult()
}
for _, p := range request.OverrideDestinationForProtocol {
if strings.HasPrefix(protocolString, p) {
return true
}
if fkr0, ok := d.fdns.(dns.FakeDNSEngineRev0); ok && protocolString != "bittorrent" && p == "fakedns" &&
destination.Address.Family().IsIP() && fkr0.IsIPInIPPool(destination.Address) {
newError("Using sniffer ", protocolString, " since the fake DNS missed").WriteToLog(session.ExportIDToError(ctx))
return true
}
if resultSubset, ok := result.(SnifferIsProtoSubsetOf); ok {
if resultSubset.IsProtoSubsetOf(p) {
return true
}
}
}
return false
}
// Dispatch implements routing.Dispatcher.
func (d *DefaultDispatcher) Dispatch(ctx context.Context, destination net.Destination) (*transport.Link, error) {
if !destination.IsValid() {
panic("Dispatcher: Invalid destination.")
}
ob := &session.Outbound{
Target: destination,
}
ctx = session.ContextWithOutbound(ctx, ob)
content := session.ContentFromContext(ctx)
if content == nil {
content = new(session.Content)
ctx = session.ContextWithContent(ctx, content)
}
sniffingRequest := content.SniffingRequest
inbound, outbound, err := d.getLink(ctx, destination.Network, sniffingRequest)
if err != nil {
return nil, err
}
switch {
case !sniffingRequest.Enabled:
go d.routedDispatch(ctx, outbound, destination)
case destination.Network != net.Network_TCP:
// Only metadata sniff will be used for non tcp connection
result, err := sniffer(ctx, nil, true)
if err == nil {
content.Protocol = result.Protocol()
if d.shouldOverride(ctx, result, sniffingRequest, destination) {
domain := result.Domain()
newError("sniffed domain: ", domain).WriteToLog(session.ExportIDToError(ctx))
destination.Address = net.ParseAddress(domain)
if sniffingRequest.RouteOnly && result.Protocol() != "fakedns" {
ob.RouteTarget = destination
} else {
ob.Target = destination
}
}
}
go d.routedDispatch(ctx, outbound, destination)
default:
go func() {
cReader := &cachedReader{
reader: outbound.Reader.(*pipe.Reader),
}
outbound.Reader = cReader
result, err := sniffer(ctx, cReader, sniffingRequest.MetadataOnly)
if err == nil {
content.Protocol = result.Protocol()
}
if err == nil && d.shouldOverride(ctx, result, sniffingRequest, destination) {
domain := result.Domain()
newError("sniffed domain: ", domain).WriteToLog(session.ExportIDToError(ctx))
destination.Address = net.ParseAddress(domain)
if sniffingRequest.RouteOnly && result.Protocol() != "fakedns" {
ob.RouteTarget = destination
} else {
ob.Target = destination
}
}
d.routedDispatch(ctx, outbound, destination)
}()
}
return inbound, nil
}
// DispatchLink implements routing.Dispatcher.
func (d *DefaultDispatcher) DispatchLink(ctx context.Context, destination net.Destination, outbound *transport.Link) error {
if !destination.IsValid() {
return newError("Dispatcher: Invalid destination.")
}
ob := &session.Outbound{
Target: destination,
}
ctx = session.ContextWithOutbound(ctx, ob)
content := session.ContentFromContext(ctx)
if content == nil {
content = new(session.Content)
ctx = session.ContextWithContent(ctx, content)
}
sniffingRequest := content.SniffingRequest
switch {
case !sniffingRequest.Enabled:
go d.routedDispatch(ctx, outbound, destination)
case destination.Network != net.Network_TCP:
// Only metadata sniff will be used for non tcp connection
result, err := sniffer(ctx, nil, true)
if err == nil {
content.Protocol = result.Protocol()
if d.shouldOverride(ctx, result, sniffingRequest, destination) {
domain := result.Domain()
newError("sniffed domain: ", domain).WriteToLog(session.ExportIDToError(ctx))
destination.Address = net.ParseAddress(domain)
if sniffingRequest.RouteOnly && result.Protocol() != "fakedns" {
ob.RouteTarget = destination
} else {
ob.Target = destination
}
}
}
go d.routedDispatch(ctx, outbound, destination)
default:
go func() {
cReader := &cachedReader{
reader: outbound.Reader.(*pipe.Reader),
}
outbound.Reader = cReader
result, err := sniffer(ctx, cReader, sniffingRequest.MetadataOnly)
if err == nil {
content.Protocol = result.Protocol()
}
if err == nil && d.shouldOverride(ctx, result, sniffingRequest, destination) {
domain := result.Domain()
newError("sniffed domain: ", domain).WriteToLog(session.ExportIDToError(ctx))
destination.Address = net.ParseAddress(domain)
if sniffingRequest.RouteOnly && result.Protocol() != "fakedns" {
ob.RouteTarget = destination
} else {
ob.Target = destination
}
}
d.routedDispatch(ctx, outbound, destination)
}()
}
return nil
}
func sniffer(ctx context.Context, cReader *cachedReader, metadataOnly bool) (SniffResult, error) {
payload := buf.New()
defer payload.Release()
sniffer := NewSniffer(ctx)
metaresult, metadataErr := sniffer.SniffMetadata(ctx)
if metadataOnly {
return metaresult, metadataErr
}
contentResult, contentErr := func() (SniffResult, error) {
totalAttempt := 0
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
totalAttempt++
if totalAttempt > 2 {
return nil, errSniffingTimeout
}
cReader.Cache(payload)
if !payload.IsEmpty() {
result, err := sniffer.Sniff(ctx, payload.Bytes())
if err != common.ErrNoClue {
return result, err
}
}
if payload.IsFull() {
return nil, errUnknownContent
}
}
}
}()
if contentErr != nil && metadataErr == nil {
return metaresult, nil
}
if contentErr == nil && metadataErr == nil {
return CompositeResult(metaresult, contentResult), nil
}
return contentResult, contentErr
}
func (d *DefaultDispatcher) routedDispatch(ctx context.Context, link *transport.Link, destination net.Destination) {
ob := session.OutboundFromContext(ctx)
if hosts, ok := d.dns.(dns.HostsLookup); ok && destination.Address.Family().IsDomain() {
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
}
}
}
var handler outbound.Handler
// Check if domain and protocol hit the rule
sessionInbound := session.InboundFromContext(ctx)
// Whether the inbound connection contains a user
if sessionInbound.User != nil {
if d.RuleManager.Detect(sessionInbound.Tag, destination.String(), sessionInbound.User.Email) {
newError(fmt.Sprintf("User %s access %s reject by rule", sessionInbound.User.Email, destination.String())).AtError().WriteToLog()
newError("destination is reject by rule")
common.Close(link.Writer)
common.Interrupt(link.Reader)
return
}
}
routingLink := routing_session.AsRoutingContext(ctx)
inTag := routingLink.GetInboundTag()
isPickRoute := 0
if forcedOutboundTag := session.GetForcedOutboundTagFromContext(ctx); forcedOutboundTag != "" {
ctx = session.SetForcedOutboundTagToContext(ctx, "")
if h := d.ohm.GetHandler(forcedOutboundTag); h != nil {
isPickRoute = 1
newError("taking platform initialized detour [", forcedOutboundTag, "] for [", destination, "]").WriteToLog(session.ExportIDToError(ctx))
handler = h
} else {
newError("non existing tag for platform initialized detour: ", forcedOutboundTag).AtError().WriteToLog(session.ExportIDToError(ctx))
common.Close(link.Writer)
common.Interrupt(link.Reader)
return
}
} else if d.router != nil {
if route, err := d.router.PickRoute(routingLink); err == nil {
outTag := route.GetOutboundTag()
if h := d.ohm.GetHandler(outTag); h != nil {
isPickRoute = 2
newError("taking detour [", outTag, "] for [", destination, "]").WriteToLog(session.ExportIDToError(ctx))
handler = h
} else {
newError("non existing outTag: ", outTag).AtWarning().WriteToLog(session.ExportIDToError(ctx))
}
} else {
newError("default route for ", destination).WriteToLog(session.ExportIDToError(ctx))
}
}
if handler == nil {
handler = d.ohm.GetHandler(inTag) // Default outbound hander tag should be as same as the inbound tag
}
// If there is no outbound with tag as same as the inbound tag
if handler == nil {
handler = d.ohm.GetDefaultHandler()
}
if handler == nil {
newError("default outbound handler not exist").WriteToLog(session.ExportIDToError(ctx))
common.Close(link.Writer)
common.Interrupt(link.Reader)
return
}
if accessMessage := log.AccessMessageFromContext(ctx); accessMessage != nil {
if tag := handler.Tag(); tag != "" {
if inTag == "" {
accessMessage.Detour = tag
} else if isPickRoute == 1 {
accessMessage.Detour = inTag + " ==> " + tag
} else if isPickRoute == 2 {
accessMessage.Detour = inTag + " -> " + tag
} else {
accessMessage.Detour = inTag + " >> " + tag
}
}
log.Record(accessMessage)
}
handler.Dispatch(ctx, link)
}

View File

@ -0,0 +1,4 @@
// Package dispather implement the rate limiter and the onlie device counter
package mydispatcher
//go:generate go run github.com/xtls/xray-core/common/errors/errorgen

View File

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

View File

@ -0,0 +1,118 @@
package mydispatcher
import (
"context"
"strings"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/session"
"github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/features/dns"
)
// newFakeDNSSniffer Create a Fake DNS metadata sniffer
func newFakeDNSSniffer(ctx context.Context) (protocolSnifferWithMetadata, error) {
var fakeDNSEngine dns.FakeDNSEngine
{
fakeDNSEngineFeat := core.MustFromContext(ctx).GetFeature((*dns.FakeDNSEngine)(nil))
if fakeDNSEngineFeat != nil {
fakeDNSEngine = fakeDNSEngineFeat.(dns.FakeDNSEngine)
}
}
if fakeDNSEngine == nil {
errNotInit := newError("FakeDNSEngine is not initialized, but such a sniffer is used").AtError()
return protocolSnifferWithMetadata{}, errNotInit
}
return protocolSnifferWithMetadata{protocolSniffer: func(ctx context.Context, bytes []byte) (SniffResult, error) {
Target := session.OutboundFromContext(ctx).Target
if Target.Network == net.Network_TCP || Target.Network == net.Network_UDP {
domainFromFakeDNS := fakeDNSEngine.GetDomainFromFakeDNS(Target.Address)
if domainFromFakeDNS != "" {
newError("fake dns got domain: ", domainFromFakeDNS, " for ip: ", Target.Address.String()).WriteToLog(session.ExportIDToError(ctx))
return &fakeDNSSniffResult{domainName: domainFromFakeDNS}, nil
}
}
if ipAddressInRangeValueI := ctx.Value(ipAddressInRange); ipAddressInRangeValueI != nil {
ipAddressInRangeValue := ipAddressInRangeValueI.(*ipAddressInRangeOpt)
if fkr0, ok := fakeDNSEngine.(dns.FakeDNSEngineRev0); ok {
inPool := fkr0.IsIPInIPPool(Target.Address)
ipAddressInRangeValue.addressInRange = &inPool
}
}
return nil, common.ErrNoClue
}, metadataSniffer: true}, nil
}
type fakeDNSSniffResult struct {
domainName string
}
func (fakeDNSSniffResult) Protocol() string {
return "fakedns"
}
func (f fakeDNSSniffResult) Domain() string {
return f.domainName
}
type fakeDNSExtraOpts int
const ipAddressInRange fakeDNSExtraOpts = 1
type ipAddressInRangeOpt struct {
addressInRange *bool
}
type DNSThenOthersSniffResult struct {
domainName string
protocolOriginalName string
}
func (f DNSThenOthersSniffResult) IsProtoSubsetOf(protocolName string) bool {
return strings.HasPrefix(protocolName, f.protocolOriginalName)
}
func (DNSThenOthersSniffResult) Protocol() string {
return "fakedns+others"
}
func (f DNSThenOthersSniffResult) Domain() string {
return f.domainName
}
func newFakeDNSThenOthers(ctx context.Context, fakeDNSSniffer protocolSnifferWithMetadata, others []protocolSnifferWithMetadata) (
protocolSnifferWithMetadata, error) { // nolint: unparam
// ctx may be used in the future
_ = ctx
return protocolSnifferWithMetadata{
protocolSniffer: func(ctx context.Context, bytes []byte) (SniffResult, error) {
ipAddressInRangeValue := &ipAddressInRangeOpt{}
ctx = context.WithValue(ctx, ipAddressInRange, ipAddressInRangeValue)
result, err := fakeDNSSniffer.protocolSniffer(ctx, bytes)
if err == nil {
return result, nil
}
if ipAddressInRangeValue.addressInRange != nil {
if *ipAddressInRangeValue.addressInRange {
for _, v := range others {
if v.metadataSniffer || bytes != nil {
if result, err := v.protocolSniffer(ctx, bytes); err == nil {
return DNSThenOthersSniffResult{domainName: result.Domain(), protocolOriginalName: result.Protocol()}, nil
}
}
}
return nil, common.ErrNoClue
}
newError("ip address not in fake dns range, return as is").AtDebug().WriteToLog()
return nil, common.ErrNoClue
}
newError("fake dns sniffer did not set address in range option, assume false.").AtWarning().WriteToLog()
return nil, common.ErrNoClue
},
metadataSniffer: false,
}, nil
}

132
app/mydispatcher/sniffer.go Normal file
View File

@ -0,0 +1,132 @@
package mydispatcher
import (
"context"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/protocol/bittorrent"
"github.com/xtls/xray-core/common/protocol/http"
"github.com/xtls/xray-core/common/protocol/tls"
)
type SniffResult interface {
Protocol() string
Domain() string
}
type protocolSniffer func(context.Context, []byte) (SniffResult, error)
type protocolSnifferWithMetadata struct {
protocolSniffer protocolSniffer
// A Metadata sniffer will be invoked on connection establishment only, with nil body,
// for both TCP and UDP connections
// It will not be shown as a traffic type for routing unless there is no other successful sniffing.
metadataSniffer bool
}
type Sniffer struct {
sniffer []protocolSnifferWithMetadata
}
func NewSniffer(ctx context.Context) *Sniffer {
ret := &Sniffer{
sniffer: []protocolSnifferWithMetadata{
{func(c context.Context, b []byte) (SniffResult, error) { return http.SniffHTTP(b) }, false},
{func(c context.Context, b []byte) (SniffResult, error) { return tls.SniffTLS(b) }, false},
{func(c context.Context, b []byte) (SniffResult, error) { return bittorrent.SniffBittorrent(b) }, false},
},
}
if sniffer, err := newFakeDNSSniffer(ctx); err == nil {
others := ret.sniffer
ret.sniffer = append(ret.sniffer, sniffer)
fakeDNSThenOthers, err := newFakeDNSThenOthers(ctx, sniffer, others)
if err == nil {
ret.sniffer = append([]protocolSnifferWithMetadata{fakeDNSThenOthers}, ret.sniffer...)
}
}
return ret
}
var errUnknownContent = newError("unknown content")
func (s *Sniffer) Sniff(c context.Context, payload []byte) (SniffResult, error) {
var pendingSniffer []protocolSnifferWithMetadata
for _, si := range s.sniffer {
s := si.protocolSniffer
if si.metadataSniffer {
continue
}
result, err := s(c, payload)
if err == common.ErrNoClue {
pendingSniffer = append(pendingSniffer, si)
continue
}
if err == nil && result != nil {
return result, nil
}
}
if len(pendingSniffer) > 0 {
s.sniffer = pendingSniffer
return nil, common.ErrNoClue
}
return nil, errUnknownContent
}
func (s *Sniffer) SniffMetadata(c context.Context) (SniffResult, error) {
var pendingSniffer []protocolSnifferWithMetadata
for _, si := range s.sniffer {
s := si.protocolSniffer
if !si.metadataSniffer {
pendingSniffer = append(pendingSniffer, si)
continue
}
result, err := s(c, nil)
if err == common.ErrNoClue {
pendingSniffer = append(pendingSniffer, si)
continue
}
if err == nil && result != nil {
return result, nil
}
}
if len(pendingSniffer) > 0 {
s.sniffer = pendingSniffer
return nil, common.ErrNoClue
}
return nil, errUnknownContent
}
func CompositeResult(domainResult SniffResult, protocolResult SniffResult) SniffResult {
return &compositeResult{domainResult: domainResult, protocolResult: protocolResult}
}
type compositeResult struct {
domainResult SniffResult
protocolResult SniffResult
}
func (c compositeResult) Protocol() string {
return c.protocolResult.Protocol()
}
func (c compositeResult) Domain() string {
return c.domainResult.Domain()
}
func (c compositeResult) ProtocolForDomainResult() string {
return c.domainResult.Protocol()
}
type SnifferResultComposite interface {
ProtocolForDomainResult() string
}
type SnifferIsProtoSubsetOf interface {
IsProtoSubsetOf(protocolName string) bool
}

25
app/mydispatcher/stats.go Normal file
View File

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

View File

@ -0,0 +1,44 @@
package mydispatcher_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())
}
}

2
common/common.go Normal file
View File

@ -0,0 +1,2 @@
// Package common contains common utilities that are shared among other packages.
package common

View File

@ -0,0 +1,33 @@
package cmd
import (
"crypto"
"github.com/go-acme/lego/v4/registration"
)
// Account represents a users local saved credentials.
type Account struct {
Email string `json:"email"`
Registration *registration.Resource `json:"registration"`
key crypto.PrivateKey
}
/** Implementation of the registration.User interface **/
// GetEmail returns the email address for the account.
func (a *Account) GetEmail() string {
return a.Email
}
// GetPrivateKey returns the private RSA account key.
func (a *Account) GetPrivateKey() crypto.PrivateKey {
return a.key
}
// GetRegistration returns the server registration.
func (a *Account) GetRegistration() *registration.Resource {
return a.Registration
}
/** End **/

View File

@ -0,0 +1,243 @@
package cmd
import (
"crypto"
"crypto/x509"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io/ioutil"
"net/url"
"os"
"path/filepath"
"strings"
"github.com/Yuzuki616/V2bX/common/legocmd/log"
"github.com/go-acme/lego/v4/certcrypto"
"github.com/go-acme/lego/v4/lego"
"github.com/go-acme/lego/v4/registration"
"github.com/urfave/cli"
)
const (
baseAccountsRootFolderName = "accounts"
baseKeysFolderName = "keys"
accountFileName = "account.json"
)
// AccountsStorage A storage for account data.
//
// rootPath:
//
// ./.lego/accounts/
// │ └── root accounts directory
// └── "path" option
//
// rootUserPath:
//
// ./.lego/accounts/localhost_14000/hubert@hubert.com/
// │ │ │ └── userID ("email" option)
// │ │ └── CA server ("server" option)
// │ └── root accounts directory
// └── "path" option
//
// keysPath:
//
// ./.lego/accounts/localhost_14000/hubert@hubert.com/keys/
// │ │ │ │ └── root keys directory
// │ │ │ └── userID ("email" option)
// │ │ └── CA server ("server" option)
// │ └── root accounts directory
// └── "path" option
//
// accountFilePath:
//
// ./.lego/accounts/localhost_14000/hubert@hubert.com/account.json
// │ │ │ │ └── account file
// │ │ │ └── userID ("email" option)
// │ │ └── CA server ("server" option)
// │ └── root accounts directory
// └── "path" option
//
type AccountsStorage struct {
userID string
rootPath string
rootUserPath string
keysPath string
accountFilePath string
ctx *cli.Context
}
// NewAccountsStorage Creates a new AccountsStorage.
func NewAccountsStorage(ctx *cli.Context) *AccountsStorage {
// TODO: move to account struct? Currently MUST pass email.
email := getEmail(ctx)
serverURL, err := url.Parse(ctx.GlobalString("server"))
if err != nil {
log.Panic(err)
}
rootPath := filepath.Join(ctx.GlobalString("path"), baseAccountsRootFolderName)
serverPath := strings.NewReplacer(":", "_", "/", string(os.PathSeparator)).Replace(serverURL.Host)
accountsPath := filepath.Join(rootPath, serverPath)
rootUserPath := filepath.Join(accountsPath, email)
return &AccountsStorage{
userID: email,
rootPath: rootPath,
rootUserPath: rootUserPath,
keysPath: filepath.Join(rootUserPath, baseKeysFolderName),
accountFilePath: filepath.Join(rootUserPath, accountFileName),
ctx: ctx,
}
}
func (s *AccountsStorage) ExistsAccountFilePath() bool {
accountFile := filepath.Join(s.rootUserPath, accountFileName)
if _, err := os.Stat(accountFile); os.IsNotExist(err) {
return false
} else if err != nil {
log.Panic(err)
}
return true
}
func (s *AccountsStorage) GetRootPath() string {
return s.rootPath
}
func (s *AccountsStorage) GetRootUserPath() string {
return s.rootUserPath
}
func (s *AccountsStorage) GetUserID() string {
return s.userID
}
func (s *AccountsStorage) Save(account *Account) error {
jsonBytes, err := json.MarshalIndent(account, "", "\t")
if err != nil {
return err
}
return ioutil.WriteFile(s.accountFilePath, jsonBytes, filePerm)
}
func (s *AccountsStorage) LoadAccount(privateKey crypto.PrivateKey) *Account {
fileBytes, err := ioutil.ReadFile(s.accountFilePath)
if err != nil {
log.Panicf("Could not load file for account %s: %v", s.userID, err)
}
var account Account
err = json.Unmarshal(fileBytes, &account)
if err != nil {
log.Panicf("Could not parse file for account %s: %v", s.userID, err)
}
account.key = privateKey
if account.Registration == nil || account.Registration.Body.Status == "" {
reg, err := tryRecoverRegistration(s.ctx, privateKey)
if err != nil {
log.Panicf("Could not load account for %s. Registration is nil: %#v", s.userID, err)
}
account.Registration = reg
err = s.Save(&account)
if err != nil {
log.Panicf("Could not save account for %s. Registration is nil: %#v", s.userID, err)
}
}
return &account
}
func (s *AccountsStorage) GetPrivateKey(keyType certcrypto.KeyType) crypto.PrivateKey {
accKeyPath := filepath.Join(s.keysPath, s.userID+".key")
if _, err := os.Stat(accKeyPath); os.IsNotExist(err) {
log.Printf("No key found for account %s. Generating a %s key.", s.userID, keyType)
s.createKeysFolder()
privateKey, err := generatePrivateKey(accKeyPath, keyType)
if err != nil {
log.Panicf("Could not generate RSA private account key for account %s: %v", s.userID, err)
}
log.Printf("Saved key to %s", accKeyPath)
return privateKey
}
privateKey, err := loadPrivateKey(accKeyPath)
if err != nil {
log.Panicf("Could not load RSA private key from file %s: %v", accKeyPath, err)
}
return privateKey
}
func (s *AccountsStorage) createKeysFolder() {
if err := createNonExistingFolder(s.keysPath); err != nil {
log.Panicf("Could not check/create directory for account %s: %v", s.userID, err)
}
}
func generatePrivateKey(file string, keyType certcrypto.KeyType) (crypto.PrivateKey, error) {
privateKey, err := certcrypto.GeneratePrivateKey(keyType)
if err != nil {
return nil, err
}
certOut, err := os.Create(file)
if err != nil {
return nil, err
}
defer certOut.Close()
pemKey := certcrypto.PEMBlock(privateKey)
err = pem.Encode(certOut, pemKey)
if err != nil {
return nil, err
}
return privateKey, nil
}
func loadPrivateKey(file string) (crypto.PrivateKey, error) {
keyBytes, err := ioutil.ReadFile(file)
if err != nil {
return nil, err
}
keyBlock, _ := pem.Decode(keyBytes)
switch keyBlock.Type {
case "RSA PRIVATE KEY":
return x509.ParsePKCS1PrivateKey(keyBlock.Bytes)
case "EC PRIVATE KEY":
return x509.ParseECPrivateKey(keyBlock.Bytes)
}
return nil, errors.New("unknown private key type")
}
func tryRecoverRegistration(ctx *cli.Context, privateKey crypto.PrivateKey) (*registration.Resource, error) {
// couldn't load account but got a key. Try to look the account up.
config := lego.NewConfig(&Account{key: privateKey})
config.CADirURL = ctx.GlobalString("server")
config.UserAgent = fmt.Sprintf("lego-cli/%s", ctx.App.Version)
client, err := lego.NewClient(config)
if err != nil {
return nil, err
}
reg, err := client.Registration.ResolveAccountByKey()
if err != nil {
return nil, err
}
return reg, nil
}

View File

@ -0,0 +1,205 @@
package cmd
import (
"bytes"
"crypto/x509"
"encoding/json"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/Yuzuki616/V2bX/common/legocmd/log"
"github.com/go-acme/lego/v4/certcrypto"
"github.com/go-acme/lego/v4/certificate"
"github.com/urfave/cli"
"golang.org/x/net/idna"
)
const (
baseCertificatesFolderName = "certificates"
baseArchivesFolderName = "archives"
)
// CertificatesStorage a certificates storage.
//
// rootPath:
//
// ./.lego/certificates/
// │ └── root certificates directory
// └── "path" option
//
// archivePath:
//
// ./.lego/archives/
// │ └── archived certificates directory
// └── "path" option
//
type CertificatesStorage struct {
rootPath string
archivePath string
pem bool
filename string // Deprecated
}
// NewCertificatesStorage create a new certificates storage.
func NewCertificatesStorage(ctx *cli.Context) *CertificatesStorage {
return &CertificatesStorage{
rootPath: filepath.Join(ctx.GlobalString("path"), baseCertificatesFolderName),
archivePath: filepath.Join(ctx.GlobalString("path"), baseArchivesFolderName),
pem: ctx.GlobalBool("pem"),
filename: ctx.GlobalString("filename"),
}
}
func (s *CertificatesStorage) CreateRootFolder() {
err := createNonExistingFolder(s.rootPath)
if err != nil {
log.Panicf("Could not check/create path: %v", err)
}
}
func (s *CertificatesStorage) CreateArchiveFolder() {
err := createNonExistingFolder(s.archivePath)
if err != nil {
log.Panicf("Could not check/create path: %v", err)
}
}
func (s *CertificatesStorage) GetRootPath() string {
return s.rootPath
}
func (s *CertificatesStorage) SaveResource(certRes *certificate.Resource) {
domain := certRes.Domain
// We store the certificate, private key and metadata in different files
// as web servers would not be able to work with a combined file.
err := s.WriteFile(domain, ".crt", certRes.Certificate)
if err != nil {
log.Panicf("Unable to save Certificate for domain %s\n\t%v", domain, err)
}
if certRes.IssuerCertificate != nil {
err = s.WriteFile(domain, ".issuer.crt", certRes.IssuerCertificate)
if err != nil {
log.Panicf("Unable to save IssuerCertificate for domain %s\n\t%v", domain, err)
}
}
if certRes.PrivateKey != nil {
// if we were given a CSR, we don't know the private key
err = s.WriteFile(domain, ".key", certRes.PrivateKey)
if err != nil {
log.Panicf("Unable to save PrivateKey for domain %s\n\t%v", domain, err)
}
if s.pem {
err = s.WriteFile(domain, ".pem", bytes.Join([][]byte{certRes.Certificate, certRes.PrivateKey}, nil))
if err != nil {
log.Panicf("Unable to save Certificate and PrivateKey in .pem for domain %s\n\t%v", domain, err)
}
}
} else if s.pem {
// we don't have the private key; can't write the .pem file
log.Panicf("Unable to save pem without private key for domain %s\n\t%v; are you using a CSR?", domain, err)
}
jsonBytes, err := json.MarshalIndent(certRes, "", "\t")
if err != nil {
log.Panicf("Unable to marshal CertResource for domain %s\n\t%v", domain, err)
}
err = s.WriteFile(domain, ".json", jsonBytes)
if err != nil {
log.Panicf("Unable to save CertResource for domain %s\n\t%v", domain, err)
}
}
func (s *CertificatesStorage) ReadResource(domain string) certificate.Resource {
raw, err := s.ReadFile(domain, ".json")
if err != nil {
log.Panicf("Error while loading the meta data for domain %s\n\t%v", domain, err)
}
var resource certificate.Resource
if err = json.Unmarshal(raw, &resource); err != nil {
log.Panicf("Error while marshaling the meta data for domain %s\n\t%v", domain, err)
}
return resource
}
func (s *CertificatesStorage) ExistsFile(domain, extension string) bool {
filePath := s.GetFileName(domain, extension)
if _, err := os.Stat(filePath); os.IsNotExist(err) {
return false
} else if err != nil {
log.Panic(err)
}
return true
}
func (s *CertificatesStorage) ReadFile(domain, extension string) ([]byte, error) {
return ioutil.ReadFile(s.GetFileName(domain, extension))
}
func (s *CertificatesStorage) GetFileName(domain, extension string) string {
filename := sanitizedDomain(domain) + extension
return filepath.Join(s.rootPath, filename)
}
func (s *CertificatesStorage) ReadCertificate(domain, extension string) ([]*x509.Certificate, error) {
content, err := s.ReadFile(domain, extension)
if err != nil {
return nil, err
}
// The input may be a bundle or a single certificate.
return certcrypto.ParsePEMBundle(content)
}
func (s *CertificatesStorage) WriteFile(domain, extension string, data []byte) error {
var baseFileName string
if s.filename != "" {
baseFileName = s.filename
} else {
baseFileName = sanitizedDomain(domain)
}
filePath := filepath.Join(s.rootPath, baseFileName+extension)
return ioutil.WriteFile(filePath, data, filePerm)
}
func (s *CertificatesStorage) MoveToArchive(domain string) error {
matches, err := filepath.Glob(filepath.Join(s.rootPath, sanitizedDomain(domain)+".*"))
if err != nil {
return err
}
for _, oldFile := range matches {
date := strconv.FormatInt(time.Now().Unix(), 10)
filename := date + "." + filepath.Base(oldFile)
newFile := filepath.Join(s.archivePath, filename)
err = os.Rename(oldFile, newFile)
if err != nil {
return err
}
}
return nil
}
// sanitizedDomain Make sure no funny chars are in the cert names (like wildcards ;)).
func sanitizedDomain(domain string) string {
safe, err := idna.ToASCII(strings.ReplaceAll(domain, "*", "_"))
if err != nil {
log.Panic(err)
}
return safe
}

14
common/legocmd/cmd/cmd.go Normal file
View File

@ -0,0 +1,14 @@
package cmd
import "github.com/urfave/cli"
// CreateCommands Creates all CLI commands.
func CreateCommands() []cli.Command {
return []cli.Command{
createRun(),
createRevoke(),
createRenew(),
createDNSHelp(),
createList(),
}
}

View File

@ -0,0 +1,23 @@
package cmd
import (
"github.com/Yuzuki616/V2bX/common/legocmd/log"
"github.com/urfave/cli"
)
func Before(ctx *cli.Context) error {
if ctx.GlobalString("path") == "" {
log.Panic("Could not determine current working directory. Please pass --path.")
}
err := createNonExistingFolder(ctx.GlobalString("path"))
if err != nil {
log.Panicf("Could not check/create path: %v", err)
}
if ctx.GlobalString("server") == "" {
log.Panic("Could not determine current working server. Please pass --server.")
}
return nil
}

View File

@ -0,0 +1,73 @@
package cmd
import (
"fmt"
"io"
"os"
"strings"
"text/tabwriter"
"github.com/urfave/cli"
)
func createDNSHelp() cli.Command {
return cli.Command{
Name: "dnshelp",
Usage: "Shows additional help for the '--dns' global option",
Action: dnsHelp,
Flags: []cli.Flag{
cli.StringFlag{
Name: "code, c",
Usage: fmt.Sprintf("DNS code: %s", allDNSCodes()),
},
},
}
}
func dnsHelp(ctx *cli.Context) error {
code := ctx.String("code")
if code == "" {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
ew := &errWriter{w: w}
ew.writeln(`Credentials for DNS providers must be passed through environment variables.`)
ew.writeln()
ew.writeln(`To display the documentation for a DNS providers:`)
ew.writeln()
ew.writeln("\t$ lego dnshelp -c code")
ew.writeln()
ew.writeln("All DNS codes:")
ew.writef("\t%s\n", allDNSCodes())
ew.writeln()
ew.writeln("More information: https://go-acme.github.io/lego/dns")
if ew.err != nil {
return ew.err
}
return w.Flush()
}
return displayDNSHelp(strings.ToLower(code))
}
type errWriter struct {
w io.Writer
err error
}
func (ew *errWriter) writeln(a ...interface{}) {
if ew.err != nil {
return
}
_, ew.err = fmt.Fprintln(ew.w, a...)
}
func (ew *errWriter) writef(format string, a ...interface{}) {
if ew.err != nil {
return
}
_, ew.err = fmt.Fprintf(ew.w, format, a...)
}

View File

@ -0,0 +1,136 @@
package cmd
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/url"
"path/filepath"
"strings"
"github.com/go-acme/lego/v4/certcrypto"
"github.com/urfave/cli"
)
func createList() cli.Command {
return cli.Command{
Name: "list",
Usage: "Display certificates and accounts information.",
Action: list,
Flags: []cli.Flag{
cli.BoolFlag{
Name: "accounts, a",
Usage: "Display accounts.",
},
cli.BoolFlag{
Name: "names, n",
Usage: "Display certificate common names only.",
},
},
}
}
func list(ctx *cli.Context) error {
if ctx.Bool("accounts") && !ctx.Bool("names") {
if err := listAccount(ctx); err != nil {
return err
}
}
return listCertificates(ctx)
}
func listCertificates(ctx *cli.Context) error {
certsStorage := NewCertificatesStorage(ctx)
matches, err := filepath.Glob(filepath.Join(certsStorage.GetRootPath(), "*.crt"))
if err != nil {
return err
}
names := ctx.Bool("names")
if len(matches) == 0 {
if !names {
fmt.Println("No certificates found.")
}
return nil
}
if !names {
fmt.Println("Found the following certs:")
}
for _, filename := range matches {
if strings.HasSuffix(filename, ".issuer.crt") {
continue
}
data, err := ioutil.ReadFile(filename)
if err != nil {
return err
}
pCert, err := certcrypto.ParsePEMCertificate(data)
if err != nil {
return err
}
if names {
fmt.Println(pCert.Subject.CommonName)
} else {
fmt.Println(" Certificate Name:", pCert.Subject.CommonName)
fmt.Println(" Domains:", strings.Join(pCert.DNSNames, ", "))
fmt.Println(" Expiry Date:", pCert.NotAfter)
fmt.Println(" Certificate Path:", filename)
fmt.Println()
}
}
return nil
}
func listAccount(ctx *cli.Context) error {
// fake email, needed by NewAccountsStorage
if err := ctx.GlobalSet("email", "unknown"); err != nil {
return err
}
accountsStorage := NewAccountsStorage(ctx)
matches, err := filepath.Glob(filepath.Join(accountsStorage.GetRootPath(), "*", "*", "*.json"))
if err != nil {
return err
}
if len(matches) == 0 {
fmt.Println("No accounts found.")
return nil
}
fmt.Println("Found the following accounts:")
for _, filename := range matches {
data, err := ioutil.ReadFile(filename)
if err != nil {
return err
}
var account Account
err = json.Unmarshal(data, &account)
if err != nil {
return err
}
uri, err := url.Parse(account.Registration.URI)
if err != nil {
return err
}
fmt.Println(" Email:", account.Email)
fmt.Println(" Server:", uri.Host)
fmt.Println(" Path:", filepath.Dir(filename))
fmt.Println()
}
return nil
}

View File

@ -0,0 +1,225 @@
package cmd
import (
"crypto"
"crypto/x509"
"time"
"github.com/Yuzuki616/V2bX/common/legocmd/log"
"github.com/go-acme/lego/v4/certcrypto"
"github.com/go-acme/lego/v4/certificate"
"github.com/go-acme/lego/v4/lego"
"github.com/urfave/cli"
)
const (
renewEnvAccountEmail = "LEGO_ACCOUNT_EMAIL"
renewEnvCertDomain = "LEGO_CERT_DOMAIN"
renewEnvCertPath = "LEGO_CERT_PATH"
renewEnvCertKeyPath = "LEGO_CERT_KEY_PATH"
)
func createRenew() cli.Command {
return cli.Command{
Name: "renew",
Usage: "Renew a certificate",
Action: renew,
Before: func(ctx *cli.Context) error {
// we require either domains or csr, but not both
hasDomains := len(ctx.GlobalStringSlice("domains")) > 0
hasCsr := len(ctx.GlobalString("csr")) > 0
if hasDomains && hasCsr {
log.Panic("Please specify either --domains/-d or --csr/-c, but not both")
}
if !hasDomains && !hasCsr {
log.Panic("Please specify --domains/-d (or --csr/-c if you already have a CSR)")
}
return nil
},
Flags: []cli.Flag{
cli.IntFlag{
Name: "days",
Value: 30,
Usage: "The number of days left on a certificate to renew it.",
},
cli.BoolFlag{
Name: "reuse-key",
Usage: "Used to indicate you want to reuse your current private key for the new certificate.",
},
cli.BoolFlag{
Name: "no-bundle",
Usage: "Do not create a certificate bundle by adding the issuers certificate to the new certificate.",
},
cli.BoolFlag{
Name: "must-staple",
Usage: "Include the OCSP must staple TLS extension in the CSR and generated certificate. Only works if the CSR is generated by lego.",
},
cli.StringFlag{
Name: "renew-hook",
Usage: "Define a hook. The hook is executed only when the certificates are effectively renewed.",
},
cli.StringFlag{
Name: "preferred-chain",
Usage: "If the CA offers multiple certificate chains, prefer the chain with an issuer matching this Subject Common Name. If no match, the default offered chain will be used.",
},
},
}
}
func renew(ctx *cli.Context) error {
account, client := setup(ctx, NewAccountsStorage(ctx))
setupChallenges(ctx, client)
if account.Registration == nil {
log.Panicf("Account %s is not registered. Use 'run' to register a new account.\n", account.Email)
}
certsStorage := NewCertificatesStorage(ctx)
bundle := !ctx.Bool("no-bundle")
meta := map[string]string{renewEnvAccountEmail: account.Email}
// CSR
if ctx.GlobalIsSet("csr") {
return renewForCSR(ctx, client, certsStorage, bundle, meta)
}
// Domains
return renewForDomains(ctx, client, certsStorage, bundle, meta)
}
func renewForDomains(ctx *cli.Context, client *lego.Client, certsStorage *CertificatesStorage, bundle bool, meta map[string]string) error {
domains := ctx.GlobalStringSlice("domains")
domain := domains[0]
// load the cert resource from files.
// We store the certificate, private key and metadata in different files
// as web servers would not be able to work with a combined file.
certificates, err := certsStorage.ReadCertificate(domain, ".crt")
if err != nil {
log.Panicf("Error while loading the certificate for domain %s\n\t%v", domain, err)
}
cert := certificates[0]
if !needRenewal(cert, domain, ctx.Int("days")) {
return nil
}
// This is just meant to be informal for the user.
timeLeft := cert.NotAfter.Sub(time.Now().UTC())
log.Infof("[%s] acme: Trying renewal with %d hours remaining", domain, int(timeLeft.Hours()))
certDomains := certcrypto.ExtractDomains(cert)
var privateKey crypto.PrivateKey
if ctx.Bool("reuse-key") {
keyBytes, errR := certsStorage.ReadFile(domain, ".key")
if errR != nil {
log.Panicf("Error while loading the private key for domain %s\n\t%v", domain, errR)
}
privateKey, errR = certcrypto.ParsePEMPrivateKey(keyBytes)
if errR != nil {
return errR
}
}
request := certificate.ObtainRequest{
Domains: merge(certDomains, domains),
Bundle: bundle,
PrivateKey: privateKey,
MustStaple: ctx.Bool("must-staple"),
PreferredChain: ctx.String("preferred-chain"),
}
certRes, err := client.Certificate.Obtain(request)
if err != nil {
log.Panic(err)
}
certsStorage.SaveResource(certRes)
meta[renewEnvCertDomain] = domain
meta[renewEnvCertPath] = certsStorage.GetFileName(domain, ".crt")
meta[renewEnvCertKeyPath] = certsStorage.GetFileName(domain, ".key")
return launchHook(ctx.String("renew-hook"), meta)
}
func renewForCSR(ctx *cli.Context, client *lego.Client, certsStorage *CertificatesStorage, bundle bool, meta map[string]string) error {
csr, err := readCSRFile(ctx.GlobalString("csr"))
if err != nil {
log.Panic(err)
}
domain := csr.Subject.CommonName
// load the cert resource from files.
// We store the certificate, private key and metadata in different files
// as web servers would not be able to work with a combined file.
certificates, err := certsStorage.ReadCertificate(domain, ".crt")
if err != nil {
log.Panicf("Error while loading the certificate for domain %s\n\t%v", domain, err)
}
cert := certificates[0]
if !needRenewal(cert, domain, ctx.Int("days")) {
return nil
}
// This is just meant to be informal for the user.
timeLeft := cert.NotAfter.Sub(time.Now().UTC())
log.Infof("[%s] acme: Trying renewal with %d hours remaining", domain, int(timeLeft.Hours()))
certRes, err := client.Certificate.ObtainForCSR(certificate.ObtainForCSRRequest{
CSR: csr,
Bundle: bundle,
PreferredChain: ctx.String("preferred-chain"),
})
if err != nil {
log.Panic(err)
}
certsStorage.SaveResource(certRes)
meta[renewEnvCertDomain] = domain
meta[renewEnvCertPath] = certsStorage.GetFileName(domain, ".crt")
meta[renewEnvCertKeyPath] = certsStorage.GetFileName(domain, ".key")
return launchHook(ctx.String("renew-hook"), meta)
}
func needRenewal(x509Cert *x509.Certificate, domain string, days int) bool {
if x509Cert.IsCA {
log.Panicf("[%s] Certificate bundle starts with a CA certificate", domain)
}
if days >= 0 {
notAfter := int(time.Until(x509Cert.NotAfter).Hours() / 24.0)
if notAfter > days {
log.Printf("[%s] The certificate expires in %d days, the number of days defined to perform the renewal is %d: no renewal.",
domain, notAfter, days)
return false
}
}
return true
}
func merge(prevDomains, nextDomains []string) []string {
for _, next := range nextDomains {
var found bool
for _, prev := range prevDomains {
if prev == next {
found = true
break
}
}
if !found {
prevDomains = append(prevDomains, next)
}
}
return prevDomains
}

View File

@ -0,0 +1,118 @@
package cmd
import (
"crypto/x509"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func Test_merge(t *testing.T) {
testCases := []struct {
desc string
prevDomains []string
nextDomains []string
expected []string
}{
{
desc: "all empty",
prevDomains: []string{},
nextDomains: []string{},
expected: []string{},
},
{
desc: "next empty",
prevDomains: []string{"a", "b", "c"},
nextDomains: []string{},
expected: []string{"a", "b", "c"},
},
{
desc: "prev empty",
prevDomains: []string{},
nextDomains: []string{"a", "b", "c"},
expected: []string{"a", "b", "c"},
},
{
desc: "merge append",
prevDomains: []string{"a", "b", "c"},
nextDomains: []string{"a", "c", "d"},
expected: []string{"a", "b", "c", "d"},
},
{
desc: "merge same",
prevDomains: []string{"a", "b", "c"},
nextDomains: []string{"a", "b", "c"},
expected: []string{"a", "b", "c"},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
actual := merge(test.prevDomains, test.nextDomains)
assert.Equal(t, test.expected, actual)
})
}
}
func Test_needRenewal(t *testing.T) {
testCases := []struct {
desc string
x509Cert *x509.Certificate
days int
expected bool
}{
{
desc: "30 days, NotAfter now",
x509Cert: &x509.Certificate{
NotAfter: time.Now(),
},
days: 30,
expected: true,
},
{
desc: "30 days, NotAfter 31 days",
x509Cert: &x509.Certificate{
NotAfter: time.Now().Add(31*24*time.Hour + 1*time.Second),
},
days: 30,
expected: false,
},
{
desc: "30 days, NotAfter 30 days",
x509Cert: &x509.Certificate{
NotAfter: time.Now().Add(30 * 24 * time.Hour),
},
days: 30,
expected: true,
},
{
desc: "0 days, NotAfter 30 days: only the day of the expiration",
x509Cert: &x509.Certificate{
NotAfter: time.Now().Add(30 * 24 * time.Hour),
},
days: 0,
expected: false,
},
{
desc: "-1 days, NotAfter 30 days: always renew",
x509Cert: &x509.Certificate{
NotAfter: time.Now().Add(30 * 24 * time.Hour),
},
days: -1,
expected: true,
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
actual := needRenewal(test.x509Cert, "foo.com", test.days)
assert.Equal(t, test.expected, actual)
})
}
}

View File

@ -0,0 +1,62 @@
package cmd
import (
"github.com/Yuzuki616/V2bX/common/legocmd/log"
"github.com/urfave/cli"
)
func createRevoke() cli.Command {
return cli.Command{
Name: "revoke",
Usage: "Revoke a certificate",
Action: revoke,
Flags: []cli.Flag{
cli.BoolFlag{
Name: "keep, k",
Usage: "Keep the certificates after the revocation instead of archiving them.",
},
},
}
}
func revoke(ctx *cli.Context) error {
acc, client := setup(ctx, NewAccountsStorage(ctx))
if acc.Registration == nil {
log.Panicf("Account %s is not registered. Use 'run' to register a new account.\n", acc.Email)
}
certsStorage := NewCertificatesStorage(ctx)
certsStorage.CreateRootFolder()
for _, domain := range ctx.GlobalStringSlice("domains") {
log.Printf("Trying to revoke certificate for domain %s", domain)
certBytes, err := certsStorage.ReadFile(domain, ".crt")
if err != nil {
log.Panicf("Error while revoking the certificate for domain %s\n\t%v", domain, err)
}
err = client.Certificate.Revoke(certBytes)
if err != nil {
log.Panicf("Error while revoking the certificate for domain %s\n\t%v", domain, err)
}
log.Println("Certificate was revoked.")
if ctx.Bool("keep") {
return nil
}
certsStorage.CreateArchiveFolder()
err = certsStorage.MoveToArchive(domain)
if err != nil {
return err
}
log.Println("Certificate was archived for domain:", domain)
}
return nil
}

View File

@ -0,0 +1,186 @@
package cmd
import (
"bufio"
"fmt"
"os"
"strings"
"github.com/Yuzuki616/V2bX/common/legocmd/log"
"github.com/go-acme/lego/v4/certificate"
"github.com/go-acme/lego/v4/lego"
"github.com/go-acme/lego/v4/registration"
"github.com/urfave/cli"
)
func createRun() cli.Command {
return cli.Command{
Name: "run",
Usage: "Register an account, then create and install a certificate",
Before: func(ctx *cli.Context) error {
// we require either domains or csr, but not both
hasDomains := len(ctx.GlobalStringSlice("domains")) > 0
hasCsr := len(ctx.GlobalString("csr")) > 0
if hasDomains && hasCsr {
log.Panic("Please specify either --domains/-d or --csr/-c, but not both")
}
if !hasDomains && !hasCsr {
log.Panic("Please specify --domains/-d (or --csr/-c if you already have a CSR)")
}
return nil
},
Action: run,
Flags: []cli.Flag{
cli.BoolFlag{
Name: "no-bundle",
Usage: "Do not create a certificate bundle by adding the issuers certificate to the new certificate.",
},
cli.BoolFlag{
Name: "must-staple",
Usage: "Include the OCSP must staple TLS extension in the CSR and generated certificate. Only works if the CSR is generated by lego.",
},
cli.StringFlag{
Name: "run-hook",
Usage: "Define a hook. The hook is executed when the certificates are effectively created.",
},
cli.StringFlag{
Name: "preferred-chain",
Usage: "If the CA offers multiple certificate chains, prefer the chain with an issuer matching this Subject Common Name. If no match, the default offered chain will be used.",
},
},
}
}
const rootPathWarningMessage = `!!!! HEADS UP !!!!
Your account credentials have been saved in your Let's Encrypt
configuration directory at "%s".
You should make a secure backup of this folder now. This
configuration directory will also contain certificates and
private keys obtained from Let's Encrypt so making regular
backups of this folder is ideal.
`
func run(ctx *cli.Context) error {
accountsStorage := NewAccountsStorage(ctx)
account, client := setup(ctx, accountsStorage)
setupChallenges(ctx, client)
if account.Registration == nil {
reg, err := register(ctx, client)
if err != nil {
log.Panicf("Could not complete registration\n\t%v", err)
}
account.Registration = reg
if err = accountsStorage.Save(account); err != nil {
log.Panic(err)
}
fmt.Printf(rootPathWarningMessage, accountsStorage.GetRootPath())
}
certsStorage := NewCertificatesStorage(ctx)
certsStorage.CreateRootFolder()
cert, err := obtainCertificate(ctx, client)
if err != nil {
// Make sure to return a non-zero exit code if ObtainSANCertificate returned at least one error.
// Due to us not returning partial certificate we can just exit here instead of at the end.
log.Panicf("Could not obtain certificates:\n\t%v", err)
}
certsStorage.SaveResource(cert)
meta := map[string]string{
renewEnvAccountEmail: account.Email,
renewEnvCertDomain: cert.Domain,
renewEnvCertPath: certsStorage.GetFileName(cert.Domain, ".crt"),
renewEnvCertKeyPath: certsStorage.GetFileName(cert.Domain, ".key"),
}
return launchHook(ctx.String("run-hook"), meta)
}
func handleTOS(ctx *cli.Context, client *lego.Client) bool {
// Check for a global accept override
if ctx.GlobalBool("accept-tos") {
return true
}
reader := bufio.NewReader(os.Stdin)
log.Printf("Please review the TOS at %s", client.GetToSURL())
for {
fmt.Println("Do you accept the TOS? Y/n")
text, err := reader.ReadString('\n')
if err != nil {
log.Panicf("Could not read from console: %v", err)
}
text = strings.Trim(text, "\r\n")
switch text {
case "", "y", "Y":
return true
case "n", "N":
return false
default:
fmt.Println("Your input was invalid. Please answer with one of Y/y, n/N or by pressing enter.")
}
}
}
func register(ctx *cli.Context, client *lego.Client) (*registration.Resource, error) {
accepted := handleTOS(ctx, client)
if !accepted {
log.Panic("You did not accept the TOS. Unable to proceed.")
}
if ctx.GlobalBool("eab") {
kid := ctx.GlobalString("kid")
hmacEncoded := ctx.GlobalString("hmac")
if kid == "" || hmacEncoded == "" {
log.Panicf("Requires arguments --kid and --hmac.")
}
return client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
TermsOfServiceAgreed: accepted,
Kid: kid,
HmacEncoded: hmacEncoded,
})
}
return client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
}
func obtainCertificate(ctx *cli.Context, client *lego.Client) (*certificate.Resource, error) {
bundle := !ctx.Bool("no-bundle")
domains := ctx.GlobalStringSlice("domains")
if len(domains) > 0 {
// obtain a certificate, generating a new private key
request := certificate.ObtainRequest{
Domains: domains,
Bundle: bundle,
MustStaple: ctx.Bool("must-staple"),
PreferredChain: ctx.String("preferred-chain"),
}
return client.Certificate.Obtain(request)
}
// read the CSR
csr, err := readCSRFile(ctx.GlobalString("csr"))
if err != nil {
return nil, err
}
// obtain a certificate for this CSR
return client.Certificate.ObtainForCSR(certificate.ObtainForCSRRequest{
CSR: csr,
Bundle: bundle,
PreferredChain: ctx.String("preferred-chain"),
})
}

120
common/legocmd/cmd/flags.go Normal file
View File

@ -0,0 +1,120 @@
package cmd
import (
"github.com/go-acme/lego/v4/lego"
"github.com/urfave/cli"
)
func CreateFlags(defaultPath string) []cli.Flag {
return []cli.Flag{
cli.StringSliceFlag{
Name: "domains, d",
Usage: "Add a domain to the process. Can be specified multiple times.",
},
cli.StringFlag{
Name: "server, s",
Usage: "CA hostname (and optionally :port). The server certificate must be trusted in order to avoid further modifications to the client.",
Value: lego.LEDirectoryProduction,
},
cli.BoolFlag{
Name: "accept-tos, a",
Usage: "By setting this flag to true you indicate that you accept the current Let's Encrypt terms of service.",
},
cli.StringFlag{
Name: "email, m",
Usage: "Email used for registration and recovery contact.",
},
cli.StringFlag{
Name: "csr, c",
Usage: "Certificate signing request filename, if an external CSR is to be used.",
},
cli.BoolFlag{
Name: "eab",
Usage: "Use External Account Binding for account registration. Requires --kid and --hmac.",
},
cli.StringFlag{
Name: "kid",
Usage: "Key identifier from External CA. Used for External Account Binding.",
},
cli.StringFlag{
Name: "hmac",
Usage: "MAC key from External CA. Should be in Base64 URL Encoding without padding format. Used for External Account Binding.",
},
cli.StringFlag{
Name: "key-type, k",
Value: "ec256",
Usage: "Key type to use for private keys. Supported: rsa2048, rsa4096, rsa8192, ec256, ec384.",
},
cli.StringFlag{
Name: "filename",
Usage: "(deprecated) Filename of the generated certificate.",
},
cli.StringFlag{
Name: "path",
EnvVar: "LEGO_PATH",
Usage: "Directory to use for storing the data.",
Value: defaultPath,
},
cli.BoolFlag{
Name: "http",
Usage: "Use the HTTP challenge to solve challenges. Can be mixed with other types of challenges.",
},
cli.StringFlag{
Name: "http.port",
Usage: "Set the port and interface to use for HTTP based challenges to listen on.Supported: interface:port or :port.",
Value: ":80",
},
cli.StringFlag{
Name: "http.proxy-header",
Usage: "Validate against this HTTP header when solving HTTP based challenges behind a reverse proxy.",
Value: "Host",
},
cli.StringFlag{
Name: "http.webroot",
Usage: "Set the webroot folder to use for HTTP based challenges to write directly in a file in .well-known/acme-challenge. This disables the built-in server and expects the given directory to be publicly served with access to .well-known/acme-challenge",
},
cli.StringSliceFlag{
Name: "http.memcached-host",
Usage: "Set the memcached host(s) to use for HTTP based challenges. Challenges will be written to all specified hosts.",
},
cli.BoolFlag{
Name: "tls",
Usage: "Use the TLS challenge to solve challenges. Can be mixed with other types of challenges.",
},
cli.StringFlag{
Name: "tls.port",
Usage: "Set the port and interface to use for TLS based challenges to listen on. Supported: interface:port or :port.",
Value: ":443",
},
cli.StringFlag{
Name: "dns",
Usage: "Solve a DNS challenge using the specified provider. Can be mixed with other types of challenges. Run 'lego dnshelp' for help on usage.",
},
cli.BoolFlag{
Name: "dns.disable-cp",
Usage: "By setting this flag to true, disables the need to wait the propagation of the TXT record to all authoritative name servers.",
},
cli.StringSliceFlag{
Name: "dns.resolvers",
Usage: "Set the resolvers to use for performing recursive DNS queries. Supported: host:port. The default is to use the system resolvers, or Google's DNS resolvers if the system's cannot be determined.",
},
cli.IntFlag{
Name: "http-timeout",
Usage: "Set the HTTP timeout value to a specific value in seconds.",
},
cli.IntFlag{
Name: "dns-timeout",
Usage: "Set the DNS timeout value to a specific value in seconds. Used only when performing authoritative name servers queries.",
Value: 10,
},
cli.BoolFlag{
Name: "pem",
Usage: "Generate a .pem file by concatenating the .key and .crt files together.",
},
cli.IntFlag{
Name: "cert.timeout",
Usage: "Set the certificate timeout value to a specific value in seconds. Only used when obtaining certificates.",
Value: 30,
},
}
}

View File

@ -0,0 +1,47 @@
package cmd
import (
"context"
"errors"
"fmt"
"os"
"os/exec"
"strings"
"time"
)
func launchHook(hook string, meta map[string]string) error {
if hook == "" {
return nil
}
ctxCmd, cancel := context.WithTimeout(context.Background(), 120*time.Second)
defer cancel()
parts := strings.Fields(hook)
cmdCtx := exec.CommandContext(ctxCmd, parts[0], parts[1:]...)
cmdCtx.Env = append(os.Environ(), metaToEnv(meta)...)
output, err := cmdCtx.CombinedOutput()
if len(output) > 0 {
fmt.Println(string(output))
}
if errors.Is(ctxCmd.Err(), context.DeadlineExceeded) {
return errors.New("hook timed out")
}
return err
}
func metaToEnv(meta map[string]string) []string {
var envs []string
for k, v := range meta {
envs = append(envs, k+"="+v)
}
return envs
}

129
common/legocmd/cmd/setup.go Normal file
View File

@ -0,0 +1,129 @@
package cmd
import (
"crypto/x509"
"encoding/pem"
"fmt"
"io/ioutil"
"os"
"strings"
"time"
"github.com/Yuzuki616/V2bX/common/legocmd/log"
"github.com/go-acme/lego/v4/certcrypto"
"github.com/go-acme/lego/v4/lego"
"github.com/go-acme/lego/v4/registration"
"github.com/urfave/cli"
)
const filePerm os.FileMode = 0o600
func setup(ctx *cli.Context, accountsStorage *AccountsStorage) (*Account, *lego.Client) {
keyType := getKeyType(ctx)
privateKey := accountsStorage.GetPrivateKey(keyType)
var account *Account
if accountsStorage.ExistsAccountFilePath() {
account = accountsStorage.LoadAccount(privateKey)
} else {
account = &Account{Email: accountsStorage.GetUserID(), key: privateKey}
}
client := newClient(ctx, account, keyType)
return account, client
}
func newClient(ctx *cli.Context, acc registration.User, keyType certcrypto.KeyType) *lego.Client {
config := lego.NewConfig(acc)
config.CADirURL = ctx.GlobalString("server")
config.Certificate = lego.CertificateConfig{
KeyType: keyType,
Timeout: time.Duration(ctx.GlobalInt("cert.timeout")) * time.Second,
}
config.UserAgent = fmt.Sprintf("lego-cli/%s", ctx.App.Version)
if ctx.GlobalIsSet("http-timeout") {
config.HTTPClient.Timeout = time.Duration(ctx.GlobalInt("http-timeout")) * time.Second
}
client, err := lego.NewClient(config)
if err != nil {
log.Panicf("Could not create client: %v", err)
}
if client.GetExternalAccountRequired() && !ctx.GlobalIsSet("eab") {
log.Panic("Server requires External Account Binding. Use --eab with --kid and --hmac.")
}
return client
}
// getKeyType the type from which private keys should be generated.
func getKeyType(ctx *cli.Context) certcrypto.KeyType {
keyType := ctx.GlobalString("key-type")
switch strings.ToUpper(keyType) {
case "RSA2048":
return certcrypto.RSA2048
case "RSA4096":
return certcrypto.RSA4096
case "RSA8192":
return certcrypto.RSA8192
case "EC256":
return certcrypto.EC256
case "EC384":
return certcrypto.EC384
}
log.Panicf("Unsupported KeyType: %s", keyType)
return ""
}
func getEmail(ctx *cli.Context) string {
email := ctx.GlobalString("email")
if email == "" {
log.Panic("You have to pass an account (email address) to the program using --email or -m")
}
return email
}
func createNonExistingFolder(path string) error {
if _, err := os.Stat(path); os.IsNotExist(err) {
return os.MkdirAll(path, 0o700)
} else if err != nil {
return err
}
return nil
}
func readCSRFile(filename string) (*x509.CertificateRequest, error) {
bytes, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
raw := bytes
// see if we can find a PEM-encoded CSR
var p *pem.Block
rest := bytes
for {
// decode a PEM block
p, rest = pem.Decode(rest)
// did we fail?
if p == nil {
break
}
// did we get a CSR?
if p.Type == "CERTIFICATE REQUEST" {
raw = p.Bytes
}
}
// no PEM-encoded CSR
// assume we were given a DER-encoded ASN.1 CSR
// (if this assumption is wrong, parsing these bytes will fail)
return x509.ParseCertificateRequest(raw)
}

View File

@ -0,0 +1,126 @@
package cmd
import (
"net"
"strings"
"time"
"github.com/Yuzuki616/V2bX/common/legocmd/log"
"github.com/go-acme/lego/v4/challenge"
"github.com/go-acme/lego/v4/challenge/dns01"
"github.com/go-acme/lego/v4/challenge/http01"
"github.com/go-acme/lego/v4/challenge/tlsalpn01"
"github.com/go-acme/lego/v4/lego"
"github.com/go-acme/lego/v4/providers/dns"
"github.com/go-acme/lego/v4/providers/http/memcached"
"github.com/go-acme/lego/v4/providers/http/webroot"
"github.com/urfave/cli"
)
func setupChallenges(ctx *cli.Context, client *lego.Client) {
if !ctx.GlobalBool("http") && !ctx.GlobalBool("tls") && !ctx.GlobalIsSet("dns") {
log.Panic("No challenge selected. You must specify at least one challenge: `--http`, `--tls`, `--dns`.")
}
if ctx.GlobalBool("http") {
err := client.Challenge.SetHTTP01Provider(setupHTTPProvider(ctx))
if err != nil {
log.Panic(err)
}
}
if ctx.GlobalBool("tls") {
err := client.Challenge.SetTLSALPN01Provider(setupTLSProvider(ctx))
if err != nil {
log.Panic(err)
}
}
if ctx.GlobalIsSet("dns") {
setupDNS(ctx, client)
}
}
func setupHTTPProvider(ctx *cli.Context) challenge.Provider {
switch {
case ctx.GlobalIsSet("http.webroot"):
ps, err := webroot.NewHTTPProvider(ctx.GlobalString("http.webroot"))
if err != nil {
log.Panic(err)
}
return ps
case ctx.GlobalIsSet("http.memcached-host"):
ps, err := memcached.NewMemcachedProvider(ctx.GlobalStringSlice("http.memcached-host"))
if err != nil {
log.Panic(err)
}
return ps
case ctx.GlobalIsSet("http.port"):
iface := ctx.GlobalString("http.port")
if !strings.Contains(iface, ":") {
log.Panicf("The --http switch only accepts interface:port or :port for its argument.")
}
host, port, err := net.SplitHostPort(iface)
if err != nil {
log.Panic(err)
}
srv := http01.NewProviderServer(host, port)
if header := ctx.GlobalString("http.proxy-header"); header != "" {
srv.SetProxyHeader(header)
}
return srv
case ctx.GlobalBool("http"):
srv := http01.NewProviderServer("", "")
if header := ctx.GlobalString("http.proxy-header"); header != "" {
srv.SetProxyHeader(header)
}
return srv
default:
log.Panic("Invalid HTTP challenge options.")
return nil
}
}
func setupTLSProvider(ctx *cli.Context) challenge.Provider {
switch {
case ctx.GlobalIsSet("tls.port"):
iface := ctx.GlobalString("tls.port")
if !strings.Contains(iface, ":") {
log.Panicf("The --tls switch only accepts interface:port or :port for its argument.")
}
host, port, err := net.SplitHostPort(iface)
if err != nil {
log.Panic(err)
}
return tlsalpn01.NewProviderServer(host, port)
case ctx.GlobalBool("tls"):
return tlsalpn01.NewProviderServer("", "")
default:
log.Panic("Invalid HTTP challenge options.")
return nil
}
}
func setupDNS(ctx *cli.Context, client *lego.Client) {
provider, err := dns.NewDNSChallengeProviderByName(ctx.GlobalString("dns"))
if err != nil {
log.Panic(err)
}
servers := ctx.GlobalStringSlice("dns.resolvers")
err = client.Challenge.SetDNS01Provider(provider,
dns01.CondOption(len(servers) > 0,
dns01.AddRecursiveNameservers(dns01.ParseNameservers(ctx.GlobalStringSlice("dns.resolvers")))),
dns01.CondOption(ctx.GlobalBool("dns.disable-cp"),
dns01.DisableCompletePropagationRequirement()),
dns01.CondOption(ctx.GlobalIsSet("dns-timeout"),
dns01.AddDNSTimeout(time.Duration(ctx.GlobalInt("dns-timeout"))*time.Second)),
)
if err != nil {
log.Panic(err)
}
}

File diff suppressed because it is too large Load Diff

189
common/legocmd/lego.go Normal file
View File

@ -0,0 +1,189 @@
// Let's Encrypt client to go!
// CLI application for generating Let's Encrypt certificates using the ACME package.
package legocmd
import (
"errors"
"fmt"
"os"
"path"
"path/filepath"
"runtime"
"strings"
"github.com/Yuzuki616/V2bX/common/legocmd/cmd"
"github.com/urfave/cli"
)
var version = "dev"
var defaultPath string
type LegoCMD struct {
cmdClient *cli.App
}
func New() (*LegoCMD, error) {
app := cli.NewApp()
app.Name = "lego"
app.HelpName = "lego"
app.Usage = "Let's Encrypt client written in Go"
app.EnableBashCompletion = true
app.Version = version
cli.VersionPrinter = func(c *cli.Context) {
fmt.Printf("lego version %s %s/%s\n", c.App.Version, runtime.GOOS, runtime.GOARCH)
}
// Set default path to configPath/cert
var path string = ""
configPath := os.Getenv("XRAY_LOCATION_CONFIG")
if configPath != "" {
path = configPath
} else if cwd, err := os.Getwd(); err == nil {
path = cwd
} else {
path = "."
}
defaultPath = filepath.Join(path, "cert")
app.Flags = cmd.CreateFlags(defaultPath)
app.Before = cmd.Before
app.Commands = cmd.CreateCommands()
lego := &LegoCMD{
cmdClient: app,
}
return lego, nil
}
// DNSCert cert a domain using DNS API
func (l *LegoCMD) DNSCert(domain, email, provider string, DNSEnv map[string]string) (CertPath string, KeyPath string, err error) {
defer func() (string, string, error) {
// Handle any error
if r := recover(); r != nil {
switch x := r.(type) {
case string:
err = errors.New(x)
case error:
err = x
default:
err = errors.New("unknow panic")
}
return "", "", err
}
return CertPath, KeyPath, nil
}()
// Set Env for DNS configuration
for key, value := range DNSEnv {
os.Setenv(key, value)
}
// First check if the certificate exists
CertPath, KeyPath, err = checkCertfile(domain)
if err == nil {
return CertPath, KeyPath, err
}
argstring := fmt.Sprintf("lego -a -d %s -m %s --dns %s run", domain, email, provider)
err = l.cmdClient.Run(strings.Split(argstring, " "))
if err != nil {
return "", "", err
}
CertPath, KeyPath, err = checkCertfile(domain)
if err != nil {
return "", "", err
}
return CertPath, KeyPath, nil
}
// HTTPCert cert a domain using http methods
func (l *LegoCMD) HTTPCert(domain, email string) (CertPath string, KeyPath string, err error) {
defer func() (string, string, error) {
// Handle any error
if r := recover(); r != nil {
switch x := r.(type) {
case string:
err = errors.New(x)
case error:
err = x
default:
err = errors.New("unknow panic")
}
return "", "", err
}
return CertPath, KeyPath, nil
}()
// First check if the certificate exists
CertPath, KeyPath, err = checkCertfile(domain)
if err == nil {
return CertPath, KeyPath, err
}
argstring := fmt.Sprintf("lego -a -d %s -m %s --http run", domain, email)
err = l.cmdClient.Run(strings.Split(argstring, " "))
if err != nil {
return "", "", err
}
CertPath, KeyPath, err = checkCertfile(domain)
if err != nil {
return "", "", err
}
return CertPath, KeyPath, nil
}
//RenewCert renew a domain cert
func (l *LegoCMD) RenewCert(domain, email, certMode, provider string, DNSEnv map[string]string) (CertPath string, KeyPath string, err error) {
var argstring string
defer func() (string, string, error) {
// Handle any error
if r := recover(); r != nil {
switch x := r.(type) {
case string:
err = errors.New(x)
case error:
err = x
default:
err = errors.New("unknow panic")
}
return "", "", err
}
return CertPath, KeyPath, nil
}()
if certMode == "http" {
argstring = fmt.Sprintf("lego -a -d %s -m %s --http renew --days 30", domain, email)
} else if certMode == "dns" {
// Set Env for DNS configuration
for key, value := range DNSEnv {
os.Setenv(key, value)
}
argstring = fmt.Sprintf("lego -a -d %s -m %s --dns %s renew --days 30", domain, email, provider)
} else {
return "", "", fmt.Errorf("Unsupport cert mode: %s", certMode)
}
err = l.cmdClient.Run(strings.Split(argstring, " "))
if err != nil {
return "", "", err
}
CertPath, KeyPath, err = checkCertfile(domain)
if err != nil {
return "", "", err
}
return CertPath, KeyPath, nil
}
func checkCertfile(domain string) (string, string, error) {
keyPath := path.Join(defaultPath, "certificates", fmt.Sprintf("%s.key", domain))
certPath := path.Join(defaultPath, "certificates", fmt.Sprintf("%s.crt", domain))
if _, err := os.Stat(keyPath); os.IsNotExist(err) {
return "", "", fmt.Errorf("Cert key failed: %s", domain)
}
if _, err := os.Stat(certPath); os.IsNotExist(err) {
return "", "", fmt.Errorf("Cert cert failed: %s", domain)
}
absKeyPath, _ := filepath.Abs(keyPath)
absCertPath, _ := filepath.Abs(certPath)
return absCertPath, absKeyPath, nil
}

View File

@ -0,0 +1,82 @@
package legocmd_test
import (
"testing"
"github.com/Yuzuki616/V2bX/common/legocmd"
)
func TestLegoClient(t *testing.T) {
_, err := legocmd.New()
if err != nil {
t.Error(err)
}
}
func TestLegoDNSCert(t *testing.T) {
lego, err := legocmd.New()
if err != nil {
t.Error(err)
}
var (
domain string = "node1.test.com"
email string = "test@gmail.com"
provider string = "alidns"
DNSEnv map[string]string
)
DNSEnv = make(map[string]string)
DNSEnv["ALICLOUD_ACCESS_KEY"] = "aaa"
DNSEnv["ALICLOUD_SECRET_KEY"] = "bbb"
certPath, keyPath, err := lego.DNSCert(domain, email, provider, DNSEnv)
if err != nil {
t.Error(err)
}
t.Log(certPath)
t.Log(keyPath)
}
func TestLegoHTTPCert(t *testing.T) {
lego, err := legocmd.New()
if err != nil {
t.Error(err)
}
var (
domain string = "node1.test.com"
email string = "test@gmail.com"
)
certPath, keyPath, err := lego.HTTPCert(domain, email)
if err != nil {
t.Error(err)
}
t.Log(certPath)
t.Log(keyPath)
}
func TestLegoRenewCert(t *testing.T) {
lego, err := legocmd.New()
if err != nil {
t.Error(err)
}
var (
domain string = "node1.test.com"
email string = "test@gmail.com"
provider string = "alidns"
DNSEnv map[string]string
)
DNSEnv = make(map[string]string)
DNSEnv["ALICLOUD_ACCESS_KEY"] = "aaa"
DNSEnv["ALICLOUD_SECRET_KEY"] = "bbb"
certPath, keyPath, err := lego.RenewCert(domain, email, "dns", provider, DNSEnv)
if err != nil {
t.Error(err)
}
t.Log(certPath)
t.Log(keyPath)
certPath, keyPath, err = lego.RenewCert(domain, email, "http", provider, DNSEnv)
if err != nil {
t.Error(err)
}
t.Log(certPath)
t.Log(keyPath)
}

60
common/legocmd/log/log.go Normal file
View File

@ -0,0 +1,60 @@
package log
import (
"log"
"os"
)
// Logger is an optional custom logger.
var Logger StdLogger = log.New(os.Stdout, "", log.LstdFlags)
// StdLogger interface for Standard Logger.
type StdLogger interface {
Panic(args ...interface{})
Fatalln(args ...interface{})
Panicf(format string, args ...interface{})
Print(args ...interface{})
Println(args ...interface{})
Printf(format string, args ...interface{})
}
// Panic writes a log entry.
// It uses Logger if not nil, otherwise it uses the default log.Logger.
func Panic(args ...interface{}) {
Logger.Panic(args...)
}
// Panicf writes a log entry.
// It uses Logger if not nil, otherwise it uses the default log.Logger.
func Panicf(format string, args ...interface{}) {
Logger.Panicf(format, args...)
}
// Print writes a log entry.
// It uses Logger if not nil, otherwise it uses the default log.Logger.
func Print(args ...interface{}) {
Logger.Print(args...)
}
// Println writes a log entry.
// It uses Logger if not nil, otherwise it uses the default log.Logger.
func Println(args ...interface{}) {
Logger.Println(args...)
}
// Printf writes a log entry.
// It uses Logger if not nil, otherwise it uses the default log.Logger.
func Printf(format string, args ...interface{}) {
Logger.Printf(format, args...)
}
// Warnf writes a log entry.
func Warnf(format string, args ...interface{}) {
Printf("[WARN] "+format, args...)
}
// Infof writes a log entry.
func Infof(format string, args ...interface{}) {
Printf("[INFO] "+format, args...)
}

9
common/limiter/errors.go Normal file
View File

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

180
common/limiter/limiter.go Normal file
View File

@ -0,0 +1,180 @@
// Package limiter is to control the links that go into the dispather
package limiter
import (
"fmt"
"sync"
"time"
"github.com/Yuzuki616/V2bX/api"
"github.com/juju/ratelimit"
)
type UserInfo struct {
UID int
SpeedLimit uint64
DeviceLimit int
}
type InboundInfo struct {
Tag string
NodeSpeedLimit uint64
UserInfo *sync.Map // Key: Email value: UserInfo
BucketHub *sync.Map // key: Email, value: *ratelimit.Bucket
UserOnlineIP *sync.Map // Key: Email Value: *sync.Map: Key: IP, Value: UID
}
type Limiter struct {
InboundInfo *sync.Map // Key: Tag, Value: *InboundInfo
}
func New() *Limiter {
return &Limiter{
InboundInfo: new(sync.Map),
}
}
func (l *Limiter) AddInboundLimiter(tag string, nodeSpeedLimit uint64, userList *[]api.UserInfo) error {
inboundInfo := &InboundInfo{
Tag: tag,
NodeSpeedLimit: nodeSpeedLimit,
BucketHub: new(sync.Map),
UserOnlineIP: new(sync.Map),
}
userMap := new(sync.Map)
for _, u := range *userList {
userMap.Store(fmt.Sprintf("%s|%d|%d", tag, u.Port, u.UID), UserInfo{
UID: u.UID,
SpeedLimit: u.SpeedLimit,
DeviceLimit: u.DeviceLimit,
})
}
inboundInfo.UserInfo = userMap
l.InboundInfo.Store(tag, inboundInfo) // Replace the old inbound info
return nil
}
func (l *Limiter) UpdateInboundLimiter(tag string, updatedUserList *[]api.UserInfo) error {
if value, ok := l.InboundInfo.Load(tag); ok {
inboundInfo := value.(*InboundInfo)
// Update User info
for _, u := range *updatedUserList {
inboundInfo.UserInfo.Store(fmt.Sprintf("%s|%s|%d", tag, u.GetUserEmail(), u.UID), UserInfo{
UID: u.UID,
SpeedLimit: u.SpeedLimit,
DeviceLimit: u.DeviceLimit,
})
inboundInfo.BucketHub.Delete(fmt.Sprintf("%s|%s|%d", tag, u.GetUserEmail(), u.UID)) // Delete old limiter bucket
}
} else {
return fmt.Errorf("no such inbound in limiter: %s", tag)
}
return nil
}
func (l *Limiter) DeleteInboundLimiter(tag string) error {
l.InboundInfo.Delete(tag)
return nil
}
func (l *Limiter) GetOnlineDevice(tag string) (*[]api.OnlineUser, error) {
onlineUser := make([]api.OnlineUser, 0)
if value, ok := l.InboundInfo.Load(tag); ok {
inboundInfo := value.(*InboundInfo)
// Clear Speed Limiter bucket for users who are not online
inboundInfo.BucketHub.Range(func(key, value interface{}) bool {
email := key.(string)
if _, exists := inboundInfo.UserOnlineIP.Load(email); !exists {
inboundInfo.BucketHub.Delete(email)
}
return true
})
inboundInfo.UserOnlineIP.Range(func(key, value interface{}) bool {
ipMap := value.(*sync.Map)
ipMap.Range(func(key, value interface{}) bool {
ip := key.(string)
uid := value.(int)
onlineUser = append(onlineUser, api.OnlineUser{UID: uid, IP: ip})
return true
})
email := key.(string)
inboundInfo.UserOnlineIP.Delete(email) // Reset online device
return true
})
} else {
return nil, fmt.Errorf("no such inbound in limiter: %s", tag)
}
return &onlineUser, nil
}
func (l *Limiter) GetUserBucket(tag string, email string, ip string) (limiter *ratelimit.Bucket, SpeedLimit bool, Reject bool) {
if value, ok := l.InboundInfo.Load(tag); ok {
inboundInfo := value.(*InboundInfo)
nodeLimit := inboundInfo.NodeSpeedLimit
var userLimit uint64 = 0
var deviceLimit int = 0
var uid int = 0
if v, ok := inboundInfo.UserInfo.Load(email); ok {
u := v.(UserInfo)
uid = u.UID
userLimit = u.SpeedLimit
deviceLimit = u.DeviceLimit
}
// Report online device
ipMap := new(sync.Map)
ipMap.Store(ip, uid)
// If any device is online
if v, ok := inboundInfo.UserOnlineIP.LoadOrStore(email, ipMap); ok {
ipMap := v.(*sync.Map)
// If this ip is a new device
if _, ok := ipMap.LoadOrStore(ip, uid); !ok {
counter := 0
ipMap.Range(func(key, value interface{}) bool {
counter++
return true
})
if counter > deviceLimit && deviceLimit > 0 {
ipMap.Delete(ip)
return nil, false, true
}
}
}
limit := determineRate(nodeLimit, userLimit) // If need the Speed limit
if limit > 0 {
limiter := ratelimit.NewBucketWithQuantum(time.Duration(time.Second), int64(limit), int64(limit)) // Byte/s
if v, ok := inboundInfo.BucketHub.LoadOrStore(email, limiter); ok {
bucket := v.(*ratelimit.Bucket)
return bucket, true, false
} else {
return limiter, true, false
}
} else {
return nil, false, false
}
} else {
newError("Get Inbound Limiter information failed").AtDebug().WriteToLog()
return nil, false, false
}
}
// determineRate returns the minimum non-zero rate
func determineRate(nodeLimit, userLimit uint64) (limit uint64) {
if nodeLimit == 0 || userLimit == 0 {
if nodeLimit > userLimit {
return nodeLimit
} else if nodeLimit < userLimit {
return userLimit
} else {
return 0
}
} else {
if nodeLimit > userLimit {
return userLimit
} else if nodeLimit < userLimit {
return nodeLimit
} else {
return nodeLimit
}
}
}

31
common/limiter/rate.go Normal file
View File

@ -0,0 +1,31 @@
package limiter
import (
"io"
"github.com/juju/ratelimit"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf"
)
type Writer struct {
writer buf.Writer
limiter *ratelimit.Bucket
w io.Writer
}
func (l *Limiter) RateWriter(writer buf.Writer, limiter *ratelimit.Bucket) buf.Writer {
return &Writer{
writer: writer,
limiter: limiter,
}
}
func (w *Writer) Close() error {
return common.Close(w.writer)
}
func (w *Writer) WriteMultiBuffer(mb buf.MultiBuffer) error {
w.limiter.Wait(int64(mb.Len()))
return w.writer.WriteMultiBuffer(mb)
}

9
common/rule/errors.go Normal file
View File

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

82
common/rule/rule.go Normal file
View File

@ -0,0 +1,82 @@
// Package rule is to control the audit rule behaviors
package rule
import (
"fmt"
"reflect"
"strconv"
"strings"
"sync"
"github.com/Yuzuki616/V2bX/api"
mapset "github.com/deckarep/golang-set"
)
type RuleManager struct {
InboundRule *sync.Map // Key: Tag, Value: []api.DetectRule
InboundDetectResult *sync.Map // key: Tag, Value: mapset.NewSet []api.DetectResult
}
func New() *RuleManager {
return &RuleManager{
InboundRule: new(sync.Map),
InboundDetectResult: new(sync.Map),
}
}
func (r *RuleManager) UpdateRule(tag string, newRuleList []api.DetectRule) error {
if value, ok := r.InboundRule.LoadOrStore(tag, newRuleList); ok {
oldRuleList := value.([]api.DetectRule)
if !reflect.DeepEqual(oldRuleList, newRuleList) {
r.InboundRule.Store(tag, newRuleList)
}
}
return nil
}
func (r *RuleManager) GetDetectResult(tag string) (*[]api.DetectResult, error) {
detectResult := make([]api.DetectResult, 0)
if value, ok := r.InboundDetectResult.LoadAndDelete(tag); ok {
resultSet := value.(mapset.Set)
it := resultSet.Iterator()
for result := range it.C {
detectResult = append(detectResult, result.(api.DetectResult))
}
}
return &detectResult, nil
}
func (r *RuleManager) Detect(tag string, destination string, email string) (reject bool) {
reject = false
var hitRuleID int = -1
// If we have some rule for this inbound
if value, ok := r.InboundRule.Load(tag); ok {
ruleList := value.([]api.DetectRule)
for _, r := range ruleList {
if r.Pattern.Match([]byte(destination)) {
hitRuleID = r.ID
reject = true
break
}
}
// If we hit some rule
if reject && hitRuleID != -1 {
l := strings.Split(email, "|")
uid, err := strconv.Atoi(l[len(l)-1])
if err != nil {
newError(fmt.Sprintf("Record illegal behavior failed! Cannot find user's uid: %s", email)).AtDebug().WriteToLog()
return reject
}
newSet := mapset.NewSetWith(api.DetectResult{UID: uid, RuleID: hitRuleID})
// If there are any hit history
if v, ok := r.InboundDetectResult.LoadOrStore(tag, newSet); ok {
resultSet := v.(mapset.Set)
// If this is a new record
if resultSet.Add(api.DetectResult{UID: uid, RuleID: hitRuleID}) {
r.InboundDetectResult.Store(tag, resultSet)
}
}
}
}
return reject
}

View File

@ -0,0 +1,41 @@
// Package serverstatus generate the server system status
package serverstatus
import (
"fmt"
"time"
"github.com/shirou/gopsutil/cpu"
"github.com/shirou/gopsutil/disk"
"github.com/shirou/gopsutil/mem"
)
// GetSystemInfo get the system info of a given periodic
func GetSystemInfo() (Cpu float64, Mem float64, Disk float64, Uptime int, err error) {
upTime := time.Now()
cpuPercent, err := cpu.Percent(0, false)
// Check if cpuPercent is empty
if len(cpuPercent) > 0 {
Cpu = cpuPercent[0]
} else {
Cpu = 0
}
if err != nil {
return 0, 0, 0, 0, fmt.Errorf("get cpu usage failed: %s", err)
}
memUsage, err := mem.VirtualMemory()
if err != nil {
return 0, 0, 0, 0, fmt.Errorf("get mem usage failed: %s", err)
}
diskUsage, err := disk.Usage("/")
if err != nil {
return 0, 0, 0, 0, fmt.Errorf("et disk usage failed: %s", err)
}
Uptime = int(time.Since(upTime).Seconds())
return Cpu, memUsage.UsedPercent, diskUsage.UsedPercent, Uptime, nil
}

176
go.mod Normal file
View File

@ -0,0 +1,176 @@
module github.com/Yuzuki616/V2bX
go 1.18
require (
github.com/bitly/go-simplejson v0.5.0
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect
github.com/deckarep/golang-set v1.8.0
github.com/fsnotify/fsnotify v1.5.4
github.com/go-acme/lego/v4 v4.6.0
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-resty/resty/v2 v2.7.0
github.com/golang/protobuf v1.5.2 // indirect
github.com/imdario/mergo v0.3.13
github.com/juju/ratelimit v1.0.1
github.com/r3labs/diff/v2 v2.15.1
github.com/shirou/gopsutil v3.21.11+incompatible
github.com/spf13/viper v1.12.0
github.com/stretchr/testify v1.7.1
github.com/tklauser/go-sysconf v0.3.10 // indirect
github.com/urfave/cli v1.22.9
github.com/xtls/xray-core v1.5.6
golang.org/x/net v0.0.0-20220526153639-5463443f8c37
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.28.0
)
require github.com/goccy/go-json v0.9.6
require (
cloud.google.com/go/compute v1.6.1 // indirect
github.com/Azure/azure-sdk-for-go v63.4.0+incompatible // indirect
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
github.com/Azure/go-autorest/autorest v0.11.27 // indirect
github.com/Azure/go-autorest/autorest/adal v0.9.19 // indirect
github.com/Azure/go-autorest/autorest/azure/auth v0.5.11 // indirect
github.com/Azure/go-autorest/autorest/azure/cli v0.4.5 // indirect
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect
github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect
github.com/Azure/go-autorest/logger v0.2.1 // indirect
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 // indirect
github.com/akamai/AkamaiOPEN-edgegrid-golang v1.1.1 // indirect
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1583 // indirect
github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect
github.com/aws/aws-sdk-go v1.44.7 // indirect
github.com/boombuler/barcode v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
github.com/cheekybits/genny v1.0.0 // indirect
github.com/cloudflare/cloudflare-go v0.38.0 // indirect
github.com/cpu/goacmedns v0.1.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/deepmap/oapi-codegen v1.10.1 // indirect
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 // indirect
github.com/dimchansky/utfbom v1.1.1 // indirect
github.com/dnsimple/dnsimple-go v0.71.1 // indirect
github.com/exoscale/egoscale v1.19.0 // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/francoispqt/gojay v1.2.13 // indirect
github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 // indirect
github.com/go-errors/errors v1.4.2 // indirect
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
github.com/gofrs/uuid v4.2.0+incompatible // indirect
github.com/golang-jwt/jwt/v4 v4.4.1 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/googleapis/gax-go/v2 v2.4.0 // indirect
github.com/gophercloud/gophercloud v0.24.0 // indirect
github.com/gophercloud/utils v0.0.0-20220307143606-8e7800759d16 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.1 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // indirect
github.com/infobloxopen/infoblox-go-client v1.1.1 // indirect
github.com/jarcoal/httpmock v1.1.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 // indirect
github.com/klauspost/cpuid/v2 v2.0.12 // indirect
github.com/kolo/xmlrpc v0.0.0-20201022064351-38db28db192b // indirect
github.com/labbsr0x/bindman-dns-webhook v1.0.2 // indirect
github.com/labbsr0x/goh v1.0.1 // indirect
github.com/linode/linodego v1.4.1 // indirect
github.com/liquidweb/go-lwApi v0.0.5 // indirect
github.com/liquidweb/liquidweb-cli v0.6.10 // indirect
github.com/liquidweb/liquidweb-go v1.6.3 // indirect
github.com/lucas-clemente/quic-go v0.27.1 // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/marten-seemann/qtls-go1-16 v0.1.5 // indirect
github.com/marten-seemann/qtls-go1-17 v0.1.1 // indirect
github.com/marten-seemann/qtls-go1-18 v0.1.1 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/miekg/dns v1.1.49 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 // indirect
github.com/nrdcg/auroradns v1.0.1 // indirect
github.com/nrdcg/desec v0.6.0 // indirect
github.com/nrdcg/dnspod-go v0.4.0 // indirect
github.com/nrdcg/freemyip v0.2.0 // indirect
github.com/nrdcg/goinwx v0.8.1 // indirect
github.com/nrdcg/namesilo v0.2.1 // indirect
github.com/nrdcg/porkbun v0.1.1 // indirect
github.com/nxadm/tail v1.4.8 // indirect
github.com/onsi/ginkgo v1.16.5 // indirect
github.com/oracle/oci-go-sdk v24.3.0+incompatible // indirect
github.com/ovh/go-ovh v1.1.0 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.1 // indirect
github.com/pires/go-proxyproto v0.6.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/pquerna/otp v1.3.0 // indirect
github.com/rainycape/memcache v0.0.0-20150622160815-1031fa0ce2f2 // indirect
github.com/refraction-networking/utls v1.1.0 // indirect
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
github.com/rogpeppe/go-internal v1.8.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sacloud/libsacloud v1.36.2 // indirect
github.com/sagernet/sing v0.0.0-20220528022605-7ba6439364fa // indirect
github.com/sagernet/sing-shadowsocks v0.0.0-20220528022643-c8403614f554 // indirect
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.9 // indirect
github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 // indirect
github.com/softlayer/softlayer-go v1.0.4 // indirect
github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect
github.com/spf13/afero v1.8.2 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/objx v0.3.0 // indirect
github.com/subosito/gotenv v1.3.0 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.392 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.392 // indirect
github.com/tklauser/numcpus v0.4.0 // indirect
github.com/transip/gotransip/v6 v6.17.0 // indirect
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e // indirect
github.com/vinyldns/go-vinyldns v0.9.16 // indirect
github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
github.com/vultr/govultr/v2 v2.16.0 // indirect
github.com/xtls/go v0.0.0-20210920065950-d4af136d3672 // indirect
github.com/yusufpapurcu/wmi v1.2.2 // indirect
go.opencensus.io v0.23.0 // indirect
go.starlark.net v0.0.0-20220328144851-d1966c6b9fcd // indirect
go.uber.org/ratelimit v0.2.0 // indirect
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 // indirect
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20220411224347-583f2d630306 // indirect
golang.org/x/tools v0.1.11-0.20220325154526-54af36eca237 // indirect
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df // indirect
google.golang.org/api v0.81.0 // indirect
google.golang.org/genproto v0.0.0-20220527130721-00d5c0f3be58 // indirect
google.golang.org/grpc v1.46.2 // indirect
gopkg.in/ini.v1 v1.66.4 // indirect
gopkg.in/ns1/ns1-go.v2 v2.6.5 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/blake3 v1.1.7 // indirect
)
replace github.com/linode/linodego => github.com/linode/linodego v0.31.1
replace github.com/exoscale/egoscale => github.com/exoscale/egoscale v0.67.0

1449
go.sum Normal file

File diff suppressed because it is too large Load Diff

80
main/config.yml.example Normal file
View File

@ -0,0 +1,80 @@
Log:
Level: warning # Log level: none, error, warning, info, debug
AccessPath: # /etc/XrayR/access.Log
ErrorPath: # /etc/XrayR/error.log
DnsConfigPath: # /etc/XrayR/dns.json # Path to dns config, check https://xtls.github.io/config/dns.html for help
RouteConfigPath: # /etc/XrayR/route.json # Path to route config, check https://xtls.github.io/config/routing.html for help
InboundConfigPath: # /etc/XrayR/custom_inbound.json # Path to custom inbound config, check https://xtls.github.io/config/inbound.html for help
OutboundConfigPath: # /etc/XrayR/custom_outbound.json # Path to custom outbound config, check https://xtls.github.io/config/outbound.html for help
ConnetionConfig:
Handshake: 4 # Handshake time limit, Second
ConnIdle: 30 # Connection idle time limit, Second
UplinkOnly: 2 # Time limit when the connection downstream is closed, Second
DownlinkOnly: 4 # Time limit when the connection is closed after the uplink is closed, Second
BufferSize: 64 # The internal cache size of each connection, kB
Nodes:
-
PanelType: "SSpanel" # Panel type: SSpanel, V2board, PMpanel, Proxypanel
ApiConfig:
ApiHost: "http://127.0.0.1:667"
ApiKey: "123"
NodeID: 41
NodeType: V2ray # Node type: V2ray, Shadowsocks, Trojan, Shadowsocks-Plugin
Timeout: 30 # Timeout for the api request
EnableVless: false # Enable Vless for V2ray Type
EnableXTLS: false # Enable XTLS for V2ray and Trojan
SpeedLimit: 0 # Mbps, Local settings will replace remote settings, 0 means disable
DeviceLimit: 0 # Local settings will replace remote settings, 0 means disable
RuleListPath: # /etc/XrayR/rulelist Path to local rulelist file
ControllerConfig:
ListenIP: 0.0.0.0 # IP address you want to listen
SendIP: 0.0.0.0 # IP address you want to send pacakage
UpdatePeriodic: 60 # Time to update the nodeinfo, how many sec.
EnableDNS: false # Use custom DNS config, Please ensure that you set the dns.json well
DNSType: AsIs # AsIs, UseIP, UseIPv4, UseIPv6, DNS strategy
EnableProxyProtocol: false # Only works for WebSocket and TCP
EnableFallback: false # Only support for Trojan and Vless
FallBackConfigs: # Support multiple fallbacks
-
SNI: # TLS SNI(Server Name Indication), Empty for any
Alpn: # Alpn, Empty for any
Path: # HTTP PATH, Empty for any
Dest: 80 # Required, Destination of fallback, check https://xtls.github.io/config/features/fallback.html for details.
ProxyProtocolVer: 0 # Send PROXY protocol version, 0 for dsable
CertConfig:
CertMode: dns # Option about how to get certificate: none, file, http, dns. Choose "none" will forcedly disable the tls config.
CertDomain: "node1.test.com" # Domain to cert
CertFile: /etc/XrayR/cert/node1.test.com.cert # Provided if the CertMode is file
KeyFile: /etc/XrayR/cert/node1.test.com.key
Provider: alidns # DNS cert provider, Get the full support list here: https://go-acme.github.io/lego/dns/
Email: test@me.com
DNSEnv: # DNS ENV option used by DNS provider
ALICLOUD_ACCESS_KEY: aaa
ALICLOUD_SECRET_KEY: bbb
# -
# PanelType: "V2board" # Panel type: SSpanel, V2board
# ApiConfig:
# ApiHost: "http://127.0.0.1:668"
# ApiKey: "123"
# NodeID: 4
# NodeType: Shadowsocks # Node type: V2ray, Shadowsocks, Trojan
# Timeout: 30 # Timeout for the api request
# EnableVless: false # Enable Vless for V2ray Type
# EnableXTLS: false # Enable XTLS for V2ray and Trojan
# SpeedLimit: 0 # Mbps, Local settings will replace remote settings
# DeviceLimit: 0 # Local settings will replace remote settings
# ControllerConfig:
# ListenIP: 0.0.0.0 # IP address you want to listen
# UpdatePeriodic: 10 # Time to update the nodeinfo, how many sec.
# EnableDNS: false # Use custom DNS config, Please ensure that you set the dns.json well
# CertConfig:
# CertMode: dns # Option about how to get certificate: none, file, http, dns
# CertDomain: "node1.test.com" # Domain to cert
# CertFile: /etc/XrayR/cert/node1.test.com.cert # Provided if the CertMode is file
# KeyFile: /etc/XrayR/cert/node1.test.com.pem
# Provider: alidns # DNS cert provider, Get the full support list here: https://go-acme.github.io/lego/dns/
# Email: test@me.com
# DNSEnv: # DNS ENV option used by DNS provider
# ALICLOUD_ACCESS_KEY: aaa
# ALICLOUD_SECRET_KEY: bbb

19
main/custom_inbound.json Normal file
View File

@ -0,0 +1,19 @@
[
{
"listen": "0.0.0.0",
"port": 1234,
"protocol": "socks",
"settings": {
"auth": "noauth",
"accounts": [
{
"user": "my-username",
"pass": "my-password"
}
],
"udp": false,
"ip": "127.0.0.1",
"userLevel": 0
}
}
]

28
main/custom_outbound.json Normal file
View File

@ -0,0 +1,28 @@
[
{
"tag": "IPv4_out",
"protocol": "freedom",
"settings": {}
},
{
"tag": "IPv6_out",
"protocol": "freedom",
"settings": {
"domainStrategy": "UseIPv6"
}
},
{
"tag": "socks5-warp",
"protocol": "socks",
"settings": {
"servers": [{
"address": "127.0.0.1",
"port": 40000
}]
}
},
{
"protocol": "blackhole",
"tag": "block"
}
]

72
main/distro/all/all.go Normal file
View File

@ -0,0 +1,72 @@
package all
import (
// The following are necessary as they register handlers in their init functions.
// Required features. Can't remove unless there is replacements.
// _ "github.com/xtls/xray-core/app/dispatcher"
_ "github.com/Yuzuki616/V2bX/app/mydispatcher"
_ "github.com/xtls/xray-core/app/proxyman/inbound"
_ "github.com/xtls/xray-core/app/proxyman/outbound"
// Default commander and all its services. This is an optional feature.
_ "github.com/xtls/xray-core/app/commander"
_ "github.com/xtls/xray-core/app/log/command"
_ "github.com/xtls/xray-core/app/proxyman/command"
_ "github.com/xtls/xray-core/app/stats/command"
// Other optional features.
_ "github.com/xtls/xray-core/app/dns"
_ "github.com/xtls/xray-core/app/log"
_ "github.com/xtls/xray-core/app/metrics"
_ "github.com/xtls/xray-core/app/policy"
_ "github.com/xtls/xray-core/app/reverse"
_ "github.com/xtls/xray-core/app/router"
_ "github.com/xtls/xray-core/app/stats"
// Inbound and outbound proxies.
_ "github.com/xtls/xray-core/proxy/blackhole"
_ "github.com/xtls/xray-core/proxy/dns"
_ "github.com/xtls/xray-core/proxy/dokodemo"
_ "github.com/xtls/xray-core/proxy/freedom"
_ "github.com/xtls/xray-core/proxy/http"
_ "github.com/xtls/xray-core/proxy/mtproto"
_ "github.com/xtls/xray-core/proxy/shadowsocks"
_ "github.com/xtls/xray-core/proxy/socks"
_ "github.com/xtls/xray-core/proxy/trojan"
_ "github.com/xtls/xray-core/proxy/vless/inbound"
_ "github.com/xtls/xray-core/proxy/vless/outbound"
_ "github.com/xtls/xray-core/proxy/vmess/inbound"
_ "github.com/xtls/xray-core/proxy/vmess/outbound"
// Transports
_ "github.com/xtls/xray-core/transport/internet/domainsocket"
_ "github.com/xtls/xray-core/transport/internet/http"
_ "github.com/xtls/xray-core/transport/internet/kcp"
_ "github.com/xtls/xray-core/transport/internet/quic"
_ "github.com/xtls/xray-core/transport/internet/tcp"
_ "github.com/xtls/xray-core/transport/internet/tls"
_ "github.com/xtls/xray-core/transport/internet/udp"
_ "github.com/xtls/xray-core/transport/internet/websocket"
_ "github.com/xtls/xray-core/transport/internet/xtls"
// Transport headers
_ "github.com/xtls/xray-core/transport/internet/headers/http"
_ "github.com/xtls/xray-core/transport/internet/headers/noop"
_ "github.com/xtls/xray-core/transport/internet/headers/srtp"
_ "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/wechat"
_ "github.com/xtls/xray-core/transport/internet/headers/wireguard"
// JSON & TOML & YAML
_ "github.com/xtls/xray-core/main/json"
_ "github.com/xtls/xray-core/main/toml"
_ "github.com/xtls/xray-core/main/yaml"
// Load config from file or http(s)
_ "github.com/xtls/xray-core/main/confloader/external"
// Commands
_ "github.com/xtls/xray-core/main/commands/all"
)

8
main/dns.json Normal file
View File

@ -0,0 +1,8 @@
{
"servers": [
"1.1.1.1",
"8.8.8.8",
"localhost"
],
"tag": "dns_inbound"
}

BIN
main/geoip.dat Normal file

Binary file not shown.

36810
main/geosite.dat Normal file

File diff suppressed because one or more lines are too long

103
main/main.go Normal file
View File

@ -0,0 +1,103 @@
package main
import (
"flag"
"fmt"
"log"
"os"
"os/signal"
"path"
"runtime"
"strings"
"syscall"
"time"
"github.com/Yuzuki616/V2bX/panel"
"github.com/fsnotify/fsnotify"
"github.com/spf13/viper"
)
var (
configFile = flag.String("config", "", "Config file for XrayR.")
printVersion = flag.Bool("version", false, "show version")
)
var (
version = "0.8.2.6"
codename = "XrayR"
intro = "A Xray backend that supports many panels"
)
func showVersion() {
fmt.Printf("%s %s (%s) \n", codename, version, intro)
}
func getConfig() *viper.Viper {
config := viper.New()
// Set custom path and name
if *configFile != "" {
configName := path.Base(*configFile)
configFileExt := path.Ext(*configFile)
configNameOnly := strings.TrimSuffix(configName, configFileExt)
configPath := path.Dir(*configFile)
config.SetConfigName(configNameOnly)
config.SetConfigType(strings.TrimPrefix(configFileExt, "."))
config.AddConfigPath(configPath)
// Set ASSET Path and Config Path for XrayR
os.Setenv("XRAY_LOCATION_ASSET", configPath)
os.Setenv("XRAY_LOCATION_CONFIG", configPath)
} else {
// Set default config path
config.SetConfigName("config")
config.SetConfigType("yml")
config.AddConfigPath(".")
}
if err := config.ReadInConfig(); err != nil {
log.Panicf("Fatal error config file: %s \n", err)
}
config.WatchConfig() // Watch the config
return config
}
func main() {
flag.Parse()
showVersion()
if *printVersion {
return
}
config := getConfig()
panelConfig := &panel.Config{}
config.Unmarshal(panelConfig)
p := panel.New(panelConfig)
lastTime := time.Now()
config.OnConfigChange(func(e fsnotify.Event) {
// Discarding event received within a short period of time after receiving an event.
if time.Now().After(lastTime.Add(3 * time.Second)) {
// Hot reload function
fmt.Println("Config file changed:", e.Name)
p.Close()
// Delete old instance and trigger GC
runtime.GC()
config.Unmarshal(panelConfig)
p.Start()
lastTime = time.Now()
}
})
p.Start()
defer p.Close()
//Explicitly triggering GC to remove garbage from config loading.
runtime.GC()
// Running backend
{
osSignals := make(chan os.Signal, 1)
signal.Notify(osSignals, os.Interrupt, os.Kill, syscall.SIGTERM)
<-osSignals
}
}

36
main/route.json Normal file
View File

@ -0,0 +1,36 @@
{
"domainStrategy": "IPOnDemand",
"rules": [
{
"type": "field",
"outboundTag": "block",
"ip": [
"geoip:private"
]
},
{
"type": "field",
"outboundTag": "block",
"protocol": [
"bittorrent"
]
},
{
"type": "field",
"outboundTag": "socks5-warp",
"domain": [""]
},
{
"type": "field",
"outboundTag": "IPv6_out",
"domain": [
"geosite:netflix"
]
},
{
"type": "field",
"outboundTag": "IPv4_out",
"network": "udp,tcp"
}
]
}

3
main/rulelist Normal file
View File

@ -0,0 +1,3 @@
(.+\.|^)(360|so)\.(cn|com)
baidu.com
google.com

36
panel/config.go Normal file
View File

@ -0,0 +1,36 @@
package panel
import (
"github.com/Yuzuki616/V2bX/api"
"github.com/Yuzuki616/V2bX/service/controller"
)
type Config struct {
LogConfig *LogConfig `mapstructure:"Log"`
DnsConfigPath string `mapstructure:"DnsConfigPath"`
InboundConfigPath string `mapstructure:"InboundConfigPath"`
OutboundConfigPath string `mapstructure:"OutboundConfigPath"`
RouteConfigPath string `mapstructure:"RouteConfigPath"`
ConnetionConfig *ConnetionConfig `mapstructure:"ConnetionConfig"`
NodesConfig []*NodesConfig `mapstructure:"Nodes"`
}
type NodesConfig struct {
PanelType string `mapstructure:"PanelType"`
ApiConfig *api.Config `mapstructure:"ApiConfig"`
ControllerConfig *controller.Config `mapstructure:"ControllerConfig"`
}
type LogConfig struct {
Level string `mapstructure:"Level"`
AccessPath string `mapstructure:"AccessPath"`
ErrorPath string `mapstructure:"ErrorPath"`
}
type ConnetionConfig struct {
Handshake uint32 `mapstructure:"handshake"`
ConnIdle uint32 `mapstructure:"connIdle"`
UplinkOnly uint32 `mapstructure:"uplinkOnly"`
DownlinkOnly uint32 `mapstructure:"downlinkOnly"`
BufferSize int32 `mapstructure:"bufferSize"`
}

30
panel/defaultConfig.go Normal file
View File

@ -0,0 +1,30 @@
package panel
import "github.com/Yuzuki616/V2bX/service/controller"
func getDefaultLogConfig() *LogConfig {
return &LogConfig{
Level: "none",
AccessPath: "",
ErrorPath: "",
}
}
func getDefaultConnetionConfig() *ConnetionConfig {
return &ConnetionConfig{
Handshake: 4,
ConnIdle: 30,
UplinkOnly: 2,
DownlinkOnly: 4,
BufferSize: 64,
}
}
func getDefaultControllerConfig() *controller.Config {
return &controller.Config{
ListenIP: "0.0.0.0",
SendIP: "0.0.0.0",
UpdatePeriodic: 60,
DNSType: "AsIs",
}
}

220
panel/panel.go Normal file
View File

@ -0,0 +1,220 @@
package panel
import (
"encoding/json"
"github.com/Yuzuki616/V2bX/app/mydispatcher"
io "io/ioutil"
"log"
"sync"
"github.com/Yuzuki616/V2bX/api"
"github.com/Yuzuki616/V2bX/api/v2board"
_ "github.com/Yuzuki616/V2bX/main/distro/all"
"github.com/Yuzuki616/V2bX/service"
"github.com/Yuzuki616/V2bX/service/controller"
"github.com/imdario/mergo"
"github.com/r3labs/diff/v2"
"github.com/xtls/xray-core/app/proxyman"
"github.com/xtls/xray-core/app/stats"
"github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/infra/conf"
)
// Panel Structure
type Panel struct {
access sync.Mutex
panelConfig *Config
Server *core.Instance
Service []service.Service
Running bool
}
func New(panelConfig *Config) *Panel {
p := &Panel{panelConfig: panelConfig}
return p
}
func (p *Panel) loadCore(panelConfig *Config) *core.Instance {
// Log Config
coreLogConfig := &conf.LogConfig{}
logConfig := getDefaultLogConfig()
if panelConfig.LogConfig != nil {
if _, err := diff.Merge(logConfig, panelConfig.LogConfig, logConfig); err != nil {
log.Panicf("Read Log config failed: %s", err)
}
}
coreLogConfig.LogLevel = logConfig.Level
coreLogConfig.AccessLog = logConfig.AccessPath
coreLogConfig.ErrorLog = logConfig.ErrorPath
// DNS config
coreDnsConfig := &conf.DNSConfig{}
if panelConfig.DnsConfigPath != "" {
if data, err := io.ReadFile(panelConfig.DnsConfigPath); err != nil {
log.Panicf("Failed to read DNS config file at: %s", panelConfig.DnsConfigPath)
} else {
if err = json.Unmarshal(data, coreDnsConfig); err != nil {
log.Panicf("Failed to unmarshal DNS config: %s", panelConfig.DnsConfigPath)
}
}
}
dnsConfig, err := coreDnsConfig.Build()
if err != nil {
log.Panicf("Failed to understand DNS config, Please check: https://xtls.github.io/config/dns.html for help: %s", err)
}
// Routing config
coreRouterConfig := &conf.RouterConfig{}
if panelConfig.RouteConfigPath != "" {
if data, err := io.ReadFile(panelConfig.RouteConfigPath); err != nil {
log.Panicf("Failed to read Routing config file at: %s", panelConfig.RouteConfigPath)
} else {
if err = json.Unmarshal(data, coreRouterConfig); err != nil {
log.Panicf("Failed to unmarshal Routing config: %s", panelConfig.RouteConfigPath)
}
}
}
routeConfig, err := coreRouterConfig.Build()
if err != nil {
log.Panicf("Failed to understand Routing config Please check: https://xtls.github.io/config/routing.html for help: %s", err)
}
// Custom Inbound config
var coreCustomInboundConfig []conf.InboundDetourConfig
if panelConfig.InboundConfigPath != "" {
if data, err := io.ReadFile(panelConfig.InboundConfigPath); err != nil {
log.Panicf("Failed to read Custom Inbound config file at: %s", panelConfig.OutboundConfigPath)
} else {
if err = json.Unmarshal(data, &coreCustomInboundConfig); err != nil {
log.Panicf("Failed to unmarshal Custom Inbound config: %s", panelConfig.OutboundConfigPath)
}
}
}
var inBoundConfig []*core.InboundHandlerConfig
for _, config := range coreCustomInboundConfig {
oc, err := config.Build()
if err != nil {
log.Panicf("Failed to understand Inbound config, Please check: https://xtls.github.io/config/inbound.html for help: %s", err)
}
inBoundConfig = append(inBoundConfig, oc)
}
// Custom Outbound config
var coreCustomOutboundConfig []conf.OutboundDetourConfig
if panelConfig.OutboundConfigPath != "" {
if data, err := io.ReadFile(panelConfig.OutboundConfigPath); err != nil {
log.Panicf("Failed to read Custom Outbound config file at: %s", panelConfig.OutboundConfigPath)
} else {
if err = json.Unmarshal(data, &coreCustomOutboundConfig); err != nil {
log.Panicf("Failed to unmarshal Custom Outbound config: %s", panelConfig.OutboundConfigPath)
}
}
}
var outBoundConfig []*core.OutboundHandlerConfig
for _, config := range coreCustomOutboundConfig {
oc, err := config.Build()
if err != nil {
log.Panicf("Failed to understand Outbound config, Please check: https://xtls.github.io/config/outbound.html for help: %s", err)
}
outBoundConfig = append(outBoundConfig, oc)
}
// Policy config
levelPolicyConfig := parseConnectionConfig(panelConfig.ConnetionConfig)
corePolicyConfig := &conf.PolicyConfig{}
corePolicyConfig.Levels = map[uint32]*conf.Policy{0: levelPolicyConfig}
policyConfig, _ := corePolicyConfig.Build()
// Build Core Config
config := &core.Config{
App: []*serial.TypedMessage{
serial.ToTypedMessage(coreLogConfig.Build()),
serial.ToTypedMessage(&mydispatcher.Config{}),
serial.ToTypedMessage(&stats.Config{}),
serial.ToTypedMessage(&proxyman.InboundConfig{}),
serial.ToTypedMessage(&proxyman.OutboundConfig{}),
serial.ToTypedMessage(policyConfig),
serial.ToTypedMessage(dnsConfig),
serial.ToTypedMessage(routeConfig),
},
Inbound: inBoundConfig,
Outbound: outBoundConfig,
}
server, err := core.New(config)
if err != nil {
log.Panicf("failed to create instance: %s", err)
}
log.Printf("Xray Core Version: %s", core.Version())
return server
}
// Start Start the panel
func (p *Panel) Start() {
p.access.Lock()
defer p.access.Unlock()
log.Print("Start the panel..")
// Load Core
server := p.loadCore(p.panelConfig)
if err := server.Start(); err != nil {
log.Panicf("Failed to start instance: %s", err)
}
p.Server = server
// Load Nodes config
for _, nodeConfig := range p.panelConfig.NodesConfig {
var apiClient api.API = v2board.New(nodeConfig.ApiConfig)
var controllerService service.Service
// Register controller service
controllerConfig := getDefaultControllerConfig()
if nodeConfig.ControllerConfig != nil {
if err := mergo.Merge(controllerConfig, nodeConfig.ControllerConfig, mergo.WithOverride); err != nil {
log.Panicf("Read Controller Config Failed")
}
}
controllerService = controller.New(server, apiClient, controllerConfig)
p.Service = append(p.Service, controllerService)
}
// Start all the service
for _, s := range p.Service {
err := s.Start()
if err != nil {
log.Panicf("Panel Start fialed: %s", err)
}
}
p.Running = true
return
}
// Close Close the panel
func (p *Panel) Close() {
p.access.Lock()
defer p.access.Unlock()
for _, s := range p.Service {
err := s.Close()
if err != nil {
log.Panicf("Panel Close fialed: %s", err)
}
}
p.Service = nil
p.Server.Close()
p.Running = false
return
}
func parseConnectionConfig(c *ConnetionConfig) (policy *conf.Policy) {
connetionConfig := getDefaultConnetionConfig()
if c != nil {
if _, err := diff.Merge(connetionConfig, c, connetionConfig); err != nil {
log.Panicf("Read ConnetionConfig failed: %s", err)
}
}
policy = &conf.Policy{
StatsUserUplink: true,
StatsUserDownlink: true,
Handshake: &connetionConfig.Handshake,
ConnectionIdle: &connetionConfig.ConnIdle,
UplinkOnly: &connetionConfig.UplinkOnly,
DownlinkOnly: &connetionConfig.DownlinkOnly,
BufferSize: &connetionConfig.BufferSize,
}
return
}

View File

@ -0,0 +1,36 @@
package controller
type Config struct {
ListenIP string `mapstructure:"ListenIP"`
SendIP string `mapstructure:"SendIP"`
UpdatePeriodic int `mapstructure:"UpdatePeriodic"`
CertConfig *CertConfig `mapstructure:"CertConfig"`
EnableDNS bool `mapstructure:"EnableDNS"`
DNSType string `mapstructure:"DNSType"`
DisableUploadTraffic bool `mapstructure:"DisableUploadTraffic"`
DisableGetRule bool `mapstructure:"DisableGetRule"`
EnableProxyProtocol bool `mapstructure:"EnableProxyProtocol"`
EnableFallback bool `mapstructure:"EnableFallback"`
DisableIVCheck bool `mapstructure:"DisableIVCheck"`
DisableSniffing bool `mapstructure:"DisableSniffing"`
FallBackConfigs []*FallBackConfig `mapstructure:"FallBackConfigs"`
}
type CertConfig struct {
CertMode string `mapstructure:"CertMode"` // none, file, http, dns
RejectUnknownSni bool `mapstructure:"RejectUnknownSni"`
CertDomain string `mapstructure:"CertDomain"`
CertFile string `mapstructure:"CertFile"`
KeyFile string `mapstructure:"KeyFile"`
Provider string `mapstructure:"Provider"` // alidns, cloudflare, gandi, godaddy....
Email string `mapstructure:"Email"`
DNSEnv map[string]string `mapstructure:"DNSEnv"`
}
type FallBackConfig struct {
SNI string `mapstructure:"SNI"`
Alpn string `mapstructure:"Alpn"`
Path string `mapstructure:"Path"`
Dest string `mapstructure:"Dest"`
ProxyProtocolVer uint64 `mapstructure:"ProxyProtocolVer"`
}

View File

@ -0,0 +1,164 @@
package controller
import (
"context"
"fmt"
"github.com/Yuzuki616/V2bX/api"
"github.com/Yuzuki616/V2bX/app/mydispatcher"
"github.com/xtls/xray-core/common/protocol"
"github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/features/inbound"
"github.com/xtls/xray-core/features/outbound"
"github.com/xtls/xray-core/features/routing"
"github.com/xtls/xray-core/features/stats"
"github.com/xtls/xray-core/proxy"
)
func (c *Controller) removeInbound(tag string) error {
inboundManager := c.server.GetFeature(inbound.ManagerType()).(inbound.Manager)
err := inboundManager.RemoveHandler(context.Background(), tag)
return err
}
func (c *Controller) removeOutbound(tag string) error {
outboundManager := c.server.GetFeature(outbound.ManagerType()).(outbound.Manager)
err := outboundManager.RemoveHandler(context.Background(), tag)
return err
}
func (c *Controller) addInbound(config *core.InboundHandlerConfig) error {
inboundManager := c.server.GetFeature(inbound.ManagerType()).(inbound.Manager)
rawHandler, err := core.CreateObject(c.server, config)
if err != nil {
return err
}
handler, ok := rawHandler.(inbound.Handler)
if !ok {
return fmt.Errorf("not an InboundHandler: %s", err)
}
if err := inboundManager.AddHandler(context.Background(), handler); err != nil {
return err
}
return nil
}
func (c *Controller) addOutbound(config *core.OutboundHandlerConfig) error {
outboundManager := c.server.GetFeature(outbound.ManagerType()).(outbound.Manager)
rawHandler, err := core.CreateObject(c.server, config)
if err != nil {
return err
}
handler, ok := rawHandler.(outbound.Handler)
if !ok {
return fmt.Errorf("not an InboundHandler: %s", err)
}
if err := outboundManager.AddHandler(context.Background(), handler); err != nil {
return err
}
return nil
}
func (c *Controller) addUsers(users []*protocol.User, tag string) error {
inboundManager := c.server.GetFeature(inbound.ManagerType()).(inbound.Manager)
handler, err := inboundManager.GetHandler(context.Background(), tag)
if err != nil {
return fmt.Errorf("No such inbound tag: %s", err)
}
inboundInstance, ok := handler.(proxy.GetInbound)
if !ok {
return fmt.Errorf("handler %s is not implement proxy.GetInbound", tag)
}
userManager, ok := inboundInstance.GetInbound().(proxy.UserManager)
if !ok {
return fmt.Errorf("handler %s is not implement proxy.UserManager", err)
}
for _, item := range users {
mUser, err := item.ToMemoryUser()
if err != nil {
return err
}
err = userManager.AddUser(context.Background(), mUser)
if err != nil {
return err
}
}
return nil
}
func (c *Controller) removeUsers(users []string, tag string) error {
inboundManager := c.server.GetFeature(inbound.ManagerType()).(inbound.Manager)
handler, err := inboundManager.GetHandler(context.Background(), tag)
if err != nil {
return fmt.Errorf("No such inbound tag: %s", err)
}
inboundInstance, ok := handler.(proxy.GetInbound)
if !ok {
return fmt.Errorf("handler %s is not implement proxy.GetInbound", tag)
}
userManager, ok := inboundInstance.GetInbound().(proxy.UserManager)
if !ok {
return fmt.Errorf("handler %s is not implement proxy.UserManager", err)
}
for _, email := range users {
err = userManager.RemoveUser(context.Background(), email)
if err != nil {
return err
}
}
return nil
}
func (c *Controller) getTraffic(email string) (up int64, down int64) {
upName := "user>>>" + email + ">>>traffic>>>uplink"
downName := "user>>>" + email + ">>>traffic>>>downlink"
statsManager := c.server.GetFeature(stats.ManagerType()).(stats.Manager)
upCounter := statsManager.GetCounter(upName)
downCounter := statsManager.GetCounter(downName)
if upCounter != nil {
up = upCounter.Value()
upCounter.Set(0)
}
if downCounter != nil {
down = downCounter.Value()
downCounter.Set(0)
}
return up, down
}
func (c *Controller) AddInboundLimiter(tag string, nodeSpeedLimit uint64, userList *[]api.UserInfo) error {
dispather := c.server.GetFeature(routing.DispatcherType()).(*mydispatcher.DefaultDispatcher)
err := dispather.Limiter.AddInboundLimiter(tag, nodeSpeedLimit, userList)
return err
}
func (c *Controller) UpdateInboundLimiter(tag string, updatedUserList *[]api.UserInfo) error {
dispather := c.server.GetFeature(routing.DispatcherType()).(*mydispatcher.DefaultDispatcher)
err := dispather.Limiter.UpdateInboundLimiter(tag, updatedUserList)
return err
}
func (c *Controller) DeleteInboundLimiter(tag string) error {
dispather := c.server.GetFeature(routing.DispatcherType()).(*mydispatcher.DefaultDispatcher)
err := dispather.Limiter.DeleteInboundLimiter(tag)
return err
}
func (c *Controller) GetOnlineDevice(tag string) (*[]api.OnlineUser, error) {
dispather := c.server.GetFeature(routing.DispatcherType()).(*mydispatcher.DefaultDispatcher)
return dispather.Limiter.GetOnlineDevice(tag)
}
func (c *Controller) UpdateRule(tag string, newRuleList []api.DetectRule) error {
dispather := c.server.GetFeature(routing.DispatcherType()).(*mydispatcher.DefaultDispatcher)
err := dispather.RuleManager.UpdateRule(tag, newRuleList)
return err
}
func (c *Controller) GetDetectResult(tag string) (*[]api.DetectResult, error) {
dispather := c.server.GetFeature(routing.DispatcherType()).(*mydispatcher.DefaultDispatcher)
return dispather.RuleManager.GetDetectResult(tag)
}

View File

@ -0,0 +1,357 @@
package controller
import (
"fmt"
"log"
"math"
"reflect"
"time"
"github.com/Yuzuki616/V2bX/api"
"github.com/Yuzuki616/V2bX/common/legocmd"
"github.com/xtls/xray-core/common/protocol"
"github.com/xtls/xray-core/common/task"
"github.com/xtls/xray-core/core"
)
type Controller struct {
server *core.Instance
config *Config
clientInfo api.ClientInfo
apiClient api.API
nodeInfo *api.NodeInfo
Tag string
userList *[]api.UserInfo
nodeInfoMonitorPeriodic *task.Periodic
userReportPeriodic *task.Periodic
panelType string
}
// New return a Controller service with default parameters.
func New(server *core.Instance, api api.API, config *Config) *Controller {
controller := &Controller{
server: server,
config: config,
apiClient: api,
}
return controller
}
// Start implement the Start() function of the service interface
func (c *Controller) Start() error {
c.clientInfo = c.apiClient.Describe()
// First fetch Node Info
newNodeInfo, err := c.apiClient.GetNodeInfo()
if err != nil {
return err
}
c.nodeInfo = newNodeInfo
c.Tag = c.buildNodeTag()
// Add new tag
err = c.addNewTag(newNodeInfo)
if err != nil {
log.Panic(err)
return err
}
// Update user
userInfo, err := c.apiClient.GetUserList()
if err != nil {
return err
}
err = c.addNewUser(userInfo, newNodeInfo)
if err != nil {
return err
}
//sync controller userList
c.userList = userInfo
// Add Rule Manager
if !c.config.DisableGetRule {
if ruleList, err := c.apiClient.GetNodeRule(); err != nil {
log.Printf("Get rule list filed: %s", err)
} else if len(*ruleList) > 0 {
if err := c.UpdateRule(c.Tag, *ruleList); err != nil {
log.Print(err)
}
}
}
c.nodeInfoMonitorPeriodic = &task.Periodic{
Interval: time.Duration(c.config.UpdatePeriodic) * time.Second,
Execute: c.nodeInfoMonitor,
}
c.userReportPeriodic = &task.Periodic{
Interval: time.Duration(c.config.UpdatePeriodic) * time.Second,
Execute: c.userInfoMonitor,
}
log.Printf("[%s: %d] Start monitor node status", c.nodeInfo.NodeType, c.nodeInfo.NodeId)
// delay to start nodeInfoMonitor
go func() {
time.Sleep(time.Duration(c.config.UpdatePeriodic) * time.Second)
_ = c.nodeInfoMonitorPeriodic.Start()
}()
log.Printf("[%s: %d] Start report node status", c.nodeInfo.NodeType, c.nodeInfo.NodeId)
// delay to start userReport
go func() {
time.Sleep(time.Duration(c.config.UpdatePeriodic) * time.Second)
_ = c.userReportPeriodic.Start()
}()
return nil
}
// Close implement the Close() function of the service interface
func (c *Controller) Close() error {
if c.nodeInfoMonitorPeriodic != nil {
err := c.nodeInfoMonitorPeriodic.Close()
if err != nil {
log.Panicf("node info periodic close failed: %s", err)
}
}
if c.nodeInfoMonitorPeriodic != nil {
err := c.userReportPeriodic.Close()
if err != nil {
log.Panicf("user report periodic close failed: %s", err)
}
}
return nil
}
func (c *Controller) nodeInfoMonitor() (err error) {
// First fetch Node Info
newNodeInfo, err := c.apiClient.GetNodeInfo()
if err != nil {
log.Print(err)
return nil
}
// Update User
newUserInfo, err := c.apiClient.GetUserList()
if err != nil {
log.Print(err)
return nil
}
var nodeInfoChanged = false
// If nodeInfo changed
if !reflect.DeepEqual(c.nodeInfo, newNodeInfo) {
// Remove old tag
oldtag := c.Tag
err := c.removeOldTag(oldtag)
if err != nil {
log.Print(err)
return nil
}
if c.nodeInfo.NodeType == "Shadowsocks-Plugin" {
err = c.removeOldTag(fmt.Sprintf("dokodemo-door_%s+1", c.Tag))
}
if err != nil {
log.Print(err)
return nil
}
// Add new tag
c.nodeInfo = newNodeInfo
c.Tag = c.buildNodeTag()
err = c.addNewTag(newNodeInfo)
if err != nil {
log.Print(err)
return nil
}
nodeInfoChanged = true
// Remove Old limiter
if err = c.DeleteInboundLimiter(oldtag); err != nil {
log.Print(err)
return nil
}
}
// Check Rule
if !c.config.DisableGetRule {
if ruleList, err := c.apiClient.GetNodeRule(); err != nil {
log.Printf("Get rule list filed: %s", err)
} else if len(*ruleList) > 0 {
if err := c.UpdateRule(c.Tag, *ruleList); err != nil {
log.Print(err)
}
}
}
// Check Cert
if c.nodeInfo.V2ray.Inbounds[0].StreamSetting.Security == "tls" && (c.config.CertConfig.CertMode == "dns" || c.config.CertConfig.CertMode == "http") {
lego, err := legocmd.New()
if err != nil {
log.Print(err)
}
// Xray-core supports the OcspStapling certification hot renew
_, _, err = lego.RenewCert(c.config.CertConfig.CertDomain, c.config.CertConfig.Email, c.config.CertConfig.CertMode, c.config.CertConfig.Provider, c.config.CertConfig.DNSEnv)
if err != nil {
log.Print(err)
}
}
if nodeInfoChanged {
err = c.addNewUser(newUserInfo, newNodeInfo)
if err != nil {
log.Print(err)
return nil
}
} else {
deleted, added := compareUserList(c.userList, newUserInfo)
if len(deleted) > 0 {
deletedEmail := make([]string, len(deleted))
for i, u := range deleted {
deletedEmail[i] = fmt.Sprintf("%s|%d|%d", c.Tag, c.nodeInfo.NodeId, u.UID)
}
err := c.removeUsers(deletedEmail, c.Tag)
if err != nil {
log.Print(err)
}
}
if len(added) > 0 {
err = c.addNewUser(&added, c.nodeInfo)
if err != nil {
log.Print(err)
}
// Update Limiter
if err := c.UpdateInboundLimiter(c.Tag, &added); err != nil {
log.Print(err)
}
}
log.Printf("[%s: %d] %d user deleted, %d user added", c.nodeInfo.NodeType, c.nodeInfo.NodeId,
len(deleted), len(added))
}
c.userList = newUserInfo
return nil
}
func (c *Controller) removeOldTag(oldtag string) (err error) {
err = c.removeInbound(oldtag)
if err != nil {
return err
}
err = c.removeOutbound(oldtag)
if err != nil {
return err
}
return nil
}
func (c *Controller) addNewTag(newNodeInfo *api.NodeInfo) (err error) {
inboundConfig, err := InboundBuilder(c.config, newNodeInfo, c.Tag)
if err != nil {
return err
}
err = c.addInbound(inboundConfig)
if err != nil {
return err
}
outBoundConfig, err := OutboundBuilder(c.config, newNodeInfo, c.Tag)
if err != nil {
return err
}
err = c.addOutbound(outBoundConfig)
if err != nil {
return err
}
return nil
}
func (c *Controller) addNewUser(userInfo *[]api.UserInfo, nodeInfo *api.NodeInfo) (err error) {
users := make([]*protocol.User, 0)
if nodeInfo.NodeType == "V2ray" {
if nodeInfo.EnableVless {
users = c.buildVlessUser(userInfo)
} else {
alterID := 0
alterID = (*userInfo)[0].V2rayUser.AlterId
if alterID >= 0 && alterID < math.MaxUint16 {
users = c.buildVmessUser(userInfo, uint16(alterID))
} else {
users = c.buildVmessUser(userInfo, 0)
return fmt.Errorf("AlterID should between 0 to 1<<16 - 1, set it to 0 for now")
}
}
} else if nodeInfo.NodeType == "Trojan" {
users = c.buildTrojanUser(userInfo)
} else if nodeInfo.NodeType == "Shadowsocks" {
users = c.buildSSUser(userInfo, nodeInfo.SS.CypherMethod)
} else {
return fmt.Errorf("unsupported node type: %s", nodeInfo.NodeType)
}
err = c.addUsers(users, c.Tag)
if err != nil {
return err
}
log.Printf("[%s: %d] Added %d new users", c.nodeInfo.NodeType, c.nodeInfo.NodeId, len(*userInfo))
return nil
}
func compareUserList(old, new *[]api.UserInfo) (deleted, added []api.UserInfo) {
tmp := map[int]int{}
for i := range *old {
tmp[(*old)[i].UID] = i
}
l := len(tmp)
for i := range *new {
tmp[(*new)[i].UID] = i
if l != len(tmp) {
tmp[(*new)[i].UID] = i
added = append(added, (*new)[i])
l++
} else {
delete(tmp, (*new)[i].UID)
l--
}
}
for i := range *old {
tmp[(*old)[i].UID] = i
if l == len(tmp) {
deleted = append(deleted, (*old)[i])
} else {
l++
}
}
return deleted, added
}
func (c *Controller) userInfoMonitor() (err error) {
// Get User traffic
userTraffic := make([]api.UserTraffic, 0)
for _, user := range *c.userList {
up, down := c.getTraffic(c.buildUserTag(&user))
if up > 0 || down > 0 {
userTraffic = append(userTraffic, api.UserTraffic{
UID: user.UID,
Email: user.V2rayUser.Email,
Upload: up,
Download: down})
}
}
if len(userTraffic) > 0 && !c.config.DisableUploadTraffic {
err = c.apiClient.ReportUserTraffic(&userTraffic)
if err != nil {
log.Print(err)
}
}
// Report Online info
if onlineDevice, err := c.GetOnlineDevice(c.Tag); err != nil {
log.Print(err)
} else {
log.Printf("[%s: %d] Report %d online users", c.nodeInfo.NodeType, c.nodeInfo.NodeId, len(*onlineDevice))
}
// Report Illegal user
if detectResult, err := c.GetDetectResult(c.Tag); err != nil {
log.Print(err)
} else {
log.Printf("[%s: %d] Report %d illegal behaviors", c.nodeInfo.NodeType, c.nodeInfo.NodeId, len(*detectResult))
}
return nil
}
func (c *Controller) buildNodeTag() string {
return fmt.Sprintf("%s_%s_%d", c.nodeInfo.NodeType, c.config.ListenIP, c.nodeInfo.NodeId)
}

View File

@ -0,0 +1,82 @@
package controller_test
import (
"fmt"
"github.com/Yuzuki616/V2bX/api/v2board"
"github.com/xtls/xray-core/proxy/shadowsocks_2022"
"os"
"os/signal"
"runtime"
"syscall"
"testing"
"github.com/Yuzuki616/V2bX/api"
_ "github.com/Yuzuki616/V2bX/main/distro/all"
. "github.com/Yuzuki616/V2bX/service/controller"
"github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/infra/conf"
)
func TestController(t *testing.T) {
serverConfig := &conf.Config{
Stats: &conf.StatsConfig{},
LogConfig: &conf.LogConfig{LogLevel: "debug"},
}
policyConfig := &conf.PolicyConfig{}
policyConfig.Levels = map[uint32]*conf.Policy{0: &conf.Policy{
StatsUserUplink: true,
StatsUserDownlink: true,
}}
serverConfig.Policy = policyConfig
config, _ := serverConfig.Build()
// config := &core.Config{
// App: []*serial.TypedMessage{
// serial.ToTypedMessage(&dispatcher.Config{}),
// serial.ToTypedMessage(&proxyman.InboundConfig{}),
// serial.ToTypedMessage(&proxyman.OutboundConfig{}),
// serial.ToTypedMessage(&stats.Config{}),
// }}
server, err := core.New(config)
defer server.Close()
if err != nil {
t.Errorf("failed to create instance: %s", err)
}
if err = server.Start(); err != nil {
t.Errorf("Failed to start instance: %s", err)
}
certConfig := &CertConfig{
CertMode: "http",
CertDomain: "test.ss.tk",
Provider: "alidns",
Email: "ss@ss.com",
}
controlerconfig := &Config{
UpdatePeriodic: 5,
CertConfig: certConfig,
}
apiConfig := &api.Config{
APIHost: "http://127.0.0.1:667",
Key: "123",
NodeID: 41,
NodeType: "V2ray",
}
apiclient := v2board.New(apiConfig)
c := New(server, apiclient, controlerconfig)
fmt.Println("Sleep 1s")
err = c.Start()
if err != nil {
t.Error(err)
}
//Explicitly triggering GC to remove garbage from config loading.
runtime.GC()
{
osSignals := make(chan os.Signal, 1)
signal.Notify(osSignals, os.Interrupt, os.Kill, syscall.SIGTERM)
<-osSignals
}
test := shadowsocks_2022.MultiUserServerConfig{}
test.Network
}

View File

@ -0,0 +1,255 @@
//Package generate the InbounderConfig used by add inbound
package controller
import (
"encoding/json"
"fmt"
"github.com/Yuzuki616/V2bX/api"
"github.com/Yuzuki616/V2bX/common/legocmd"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/uuid"
"github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/infra/conf"
)
//InboundBuilder build Inbound config for different protocol
func InboundBuilder(config *Config, nodeInfo *api.NodeInfo, tag string) (*core.InboundHandlerConfig, error) {
var proxySetting interface{}
if nodeInfo.NodeType == "V2ray" {
if nodeInfo.EnableVless {
nodeInfo.V2ray.Inbounds[0].Protocol = "vless"
// Enable fallback
if config.EnableFallback {
fallbackConfigs, err := buildVlessFallbacks(config.FallBackConfigs)
if err == nil {
proxySetting = &conf.VLessInboundConfig{
Decryption: "none",
Fallbacks: fallbackConfigs,
}
} else {
return nil, err
}
} else {
proxySetting = &conf.VLessInboundConfig{
Decryption: "none",
}
}
} else {
nodeInfo.V2ray.Inbounds[0].Protocol = "vmess"
proxySetting = &conf.VMessInboundConfig{}
}
} else if nodeInfo.NodeType == "Trojan" {
nodeInfo.V2ray = &api.V2rayConfig{}
nodeInfo.V2ray.Inbounds = make([]conf.InboundDetourConfig, 1)
nodeInfo.V2ray.Inbounds[0].Protocol = "trojan"
// Enable fallback
if config.EnableFallback {
fallbackConfigs, err := buildTrojanFallbacks(config.FallBackConfigs)
if err == nil {
proxySetting = &conf.TrojanServerConfig{
Fallbacks: fallbackConfigs,
}
} else {
return nil, err
}
} else {
proxySetting = &conf.TrojanServerConfig{}
}
nodeInfo.V2ray.Inbounds[0].PortList = &conf.PortList{
Range: []conf.PortRange{{From: uint32(nodeInfo.Trojan.LocalPort), To: uint32(nodeInfo.Trojan.LocalPort)}},
}
t := conf.TransportProtocol(nodeInfo.SS.TransportProtocol)
nodeInfo.V2ray.Inbounds[0].StreamSetting = &conf.StreamConfig{Network: &t}
} else if nodeInfo.NodeType == "Shadowsocks" {
nodeInfo.V2ray = &api.V2rayConfig{}
nodeInfo.V2ray.Inbounds = make([]conf.InboundDetourConfig, 1)
nodeInfo.V2ray.Inbounds[0].Protocol = "shadowsocks"
proxySetting = &conf.ShadowsocksServerConfig{}
randomPasswd := uuid.New()
defaultSSuser := &conf.ShadowsocksUserConfig{
Cipher: "aes-128-gcm",
Password: randomPasswd.String(),
}
proxySetting, _ := proxySetting.(*conf.ShadowsocksServerConfig)
proxySetting.Users = append(proxySetting.Users, defaultSSuser)
proxySetting.NetworkList = &conf.NetworkList{"tcp", "udp"}
proxySetting.IVCheck = true
if config.DisableIVCheck {
proxySetting.IVCheck = false
}
nodeInfo.V2ray.Inbounds[0].PortList = &conf.PortList{
Range: []conf.PortRange{{From: uint32(nodeInfo.SS.Port), To: uint32(nodeInfo.SS.Port)}},
}
t := conf.TransportProtocol(nodeInfo.SS.TransportProtocol)
nodeInfo.V2ray.Inbounds[0].StreamSetting = &conf.StreamConfig{Network: &t}
} else if nodeInfo.NodeType == "dokodemo-door" {
nodeInfo.V2ray = &api.V2rayConfig{}
nodeInfo.V2ray.Inbounds = make([]conf.InboundDetourConfig, 1)
nodeInfo.V2ray.Inbounds[0].Protocol = "dokodemo-door"
proxySetting = struct {
Host string `json:"address"`
NetworkList []string `json:"network"`
}{
Host: "v1.mux.cool",
NetworkList: []string{"tcp", "udp"},
}
} else {
return nil, fmt.Errorf("unsupported node type: %s, Only support: V2ray, Trojan, Shadowsocks, and Shadowsocks-Plugin", nodeInfo.NodeType)
}
// Build Listen IP address
ipAddress := net.ParseAddress(config.ListenIP)
nodeInfo.V2ray.Inbounds[0].ListenOn = &conf.Address{Address: ipAddress}
// SniffingConfig
sniffingConfig := &conf.SniffingConfig{
Enabled: true,
DestOverride: &conf.StringList{"http", "tls"},
}
if config.DisableSniffing {
sniffingConfig.Enabled = false
}
nodeInfo.V2ray.Inbounds[0].SniffingConfig = sniffingConfig
var setting json.RawMessage
// Build Protocol and Protocol setting
setting, err := json.Marshal(proxySetting)
if err != nil {
return nil, fmt.Errorf("marshal proxy %s config fialed: %s", nodeInfo.NodeType, err)
}
if *nodeInfo.V2ray.Inbounds[0].StreamSetting.Network == "tcp" {
if nodeInfo.NodeType == "V2ray" {
nodeInfo.V2ray.Inbounds[0].StreamSetting.TCPSettings.AcceptProxyProtocol = config.EnableProxyProtocol
}
tcpSetting := &conf.TCPConfig{
AcceptProxyProtocol: config.EnableProxyProtocol,
}
nodeInfo.V2ray.Inbounds[0].StreamSetting.TCPSettings = tcpSetting
} else if *nodeInfo.V2ray.Inbounds[0].StreamSetting.Network == "websocket" {
nodeInfo.V2ray.Inbounds[0].StreamSetting.WSSettings.AcceptProxyProtocol = config.EnableProxyProtocol
}
// Build TLS and XTLS settings
if nodeInfo.EnableTls && config.CertConfig.CertMode != "none" {
nodeInfo.V2ray.Inbounds[0].StreamSetting.Security = nodeInfo.TLSType
certFile, keyFile, err := getCertFile(config.CertConfig)
if err != nil {
return nil, err
}
if nodeInfo.TLSType == "tls" {
tlsSettings := &conf.TLSConfig{
RejectUnknownSNI: config.CertConfig.RejectUnknownSni,
}
tlsSettings.Certs = append(tlsSettings.Certs, &conf.TLSCertConfig{CertFile: certFile, KeyFile: keyFile, OcspStapling: 3600})
nodeInfo.V2ray.Inbounds[0].StreamSetting.TLSSettings = tlsSettings
} else if nodeInfo.TLSType == "xtls" {
xtlsSettings := &conf.XTLSConfig{
RejectUnknownSNI: config.CertConfig.RejectUnknownSni,
}
xtlsSettings.Certs = append(xtlsSettings.Certs, &conf.XTLSCertConfig{
CertFile: certFile,
KeyFile: keyFile,
OcspStapling: 3600})
nodeInfo.V2ray.Inbounds[0].StreamSetting.XTLSSettings = xtlsSettings
}
}
// Support ProxyProtocol for any transport protocol
if *nodeInfo.V2ray.Inbounds[0].StreamSetting.Network != "tcp" &&
*nodeInfo.V2ray.Inbounds[0].StreamSetting.Network != "ws" &&
config.EnableProxyProtocol {
sockoptConfig := &conf.SocketConfig{
AcceptProxyProtocol: config.EnableProxyProtocol,
}
nodeInfo.V2ray.Inbounds[0].StreamSetting.SocketSettings = sockoptConfig
}
*nodeInfo.V2ray.Inbounds[0].Settings = setting
return nodeInfo.V2ray.Inbounds[0].Build()
}
func getCertFile(certConfig *CertConfig) (certFile string, keyFile string, err error) {
if certConfig.CertMode == "file" {
if certConfig.CertFile == "" || certConfig.KeyFile == "" {
return "", "", fmt.Errorf("cert file path or key file path not exist")
}
return certConfig.CertFile, certConfig.KeyFile, nil
} else if certConfig.CertMode == "dns" {
lego, err := legocmd.New()
if err != nil {
return "", "", err
}
certPath, keyPath, err := lego.DNSCert(certConfig.CertDomain, certConfig.Email, certConfig.Provider, certConfig.DNSEnv)
if err != nil {
return "", "", err
}
return certPath, keyPath, err
} else if certConfig.CertMode == "http" {
lego, err := legocmd.New()
if err != nil {
return "", "", err
}
certPath, keyPath, err := lego.HTTPCert(certConfig.CertDomain, certConfig.Email)
if err != nil {
return "", "", err
}
return certPath, keyPath, err
}
return "", "", fmt.Errorf("unsupported certmode: %s", certConfig.CertMode)
}
func buildVlessFallbacks(fallbackConfigs []*FallBackConfig) ([]*conf.VLessInboundFallback, error) {
if fallbackConfigs == nil {
return nil, fmt.Errorf("you must provide FallBackConfigs")
}
vlessFallBacks := make([]*conf.VLessInboundFallback, len(fallbackConfigs))
for i, c := range fallbackConfigs {
if c.Dest == "" {
return nil, fmt.Errorf("dest is required for fallback fialed")
}
var dest json.RawMessage
dest, err := json.Marshal(c.Dest)
if err != nil {
return nil, fmt.Errorf("marshal dest %s config fialed: %s", dest, err)
}
vlessFallBacks[i] = &conf.VLessInboundFallback{
Name: c.SNI,
Alpn: c.Alpn,
Path: c.Path,
Dest: dest,
Xver: c.ProxyProtocolVer,
}
}
return vlessFallBacks, nil
}
func buildTrojanFallbacks(fallbackConfigs []*FallBackConfig) ([]*conf.TrojanInboundFallback, error) {
if fallbackConfigs == nil {
return nil, fmt.Errorf("you must provide FallBackConfigs")
}
trojanFallBacks := make([]*conf.TrojanInboundFallback, len(fallbackConfigs))
for i, c := range fallbackConfigs {
if c.Dest == "" {
return nil, fmt.Errorf("dest is required for fallback fialed")
}
var dest json.RawMessage
dest, err := json.Marshal(c.Dest)
if err != nil {
return nil, fmt.Errorf("marshal dest %s config fialed: %s", dest, err)
}
trojanFallBacks[i] = &conf.TrojanInboundFallback{
Name: c.SNI,
Alpn: c.Alpn,
Path: c.Path,
Dest: dest,
Xver: c.ProxyProtocolVer,
}
}
return trojanFallBacks, nil
}

View File

@ -0,0 +1,100 @@
package controller_test
import (
"testing"
"github.com/Yuzuki616/V2bX/api"
. "github.com/Yuzuki616/V2bX/service/controller"
)
func TestBuildV2ray(t *testing.T) {
nodeInfo := &api.NodeInfo{
NodeType: "V2ray",
NodeID: 1,
Port: 1145,
SpeedLimit: 0,
AlterID: 2,
TransportProtocol: "ws",
Host: "test.test.tk",
Path: "v2ray",
EnableTLS: false,
TLSType: "tls",
}
certConfig := &CertConfig{
CertMode: "http",
CertDomain: "test.test.tk",
Provider: "alidns",
Email: "test@gmail.com",
}
config := &Config{
CertConfig: certConfig,
}
_, err := InboundBuilder(config, nodeInfo)
if err != nil {
t.Error(err)
}
}
func TestBuildTrojan(t *testing.T) {
nodeInfo := &api.NodeInfo{
NodeType: "Trojan",
NodeID: 1,
Port: 1145,
SpeedLimit: 0,
AlterID: 2,
TransportProtocol: "tcp",
Host: "trojan.test.tk",
Path: "v2ray",
EnableTLS: false,
TLSType: "tls",
}
DNSEnv := make(map[string]string)
DNSEnv["ALICLOUD_ACCESS_KEY"] = "aaa"
DNSEnv["ALICLOUD_SECRET_KEY"] = "bbb"
certConfig := &CertConfig{
CertMode: "dns",
CertDomain: "trojan.test.tk",
Provider: "alidns",
Email: "test@gmail.com",
DNSEnv: DNSEnv,
}
config := &Config{
CertConfig: certConfig,
}
_, err := InboundBuilder(config, nodeInfo)
if err != nil {
t.Error(err)
}
}
func TestBuildSS(t *testing.T) {
nodeInfo := &api.NodeInfo{
NodeType: "Shadowsocks",
NodeID: 1,
Port: 1145,
SpeedLimit: 0,
AlterID: 2,
TransportProtocol: "tcp",
Host: "test.test.tk",
Path: "v2ray",
EnableTLS: false,
TLSType: "tls",
}
DNSEnv := make(map[string]string)
DNSEnv["ALICLOUD_ACCESS_KEY"] = "aaa"
DNSEnv["ALICLOUD_SECRET_KEY"] = "bbb"
certConfig := &CertConfig{
CertMode: "dns",
CertDomain: "trojan.test.tk",
Provider: "alidns",
Email: "test@me.com",
DNSEnv: DNSEnv,
}
config := &Config{
CertConfig: certConfig,
}
_, err := InboundBuilder(config, nodeInfo)
if err != nil {
t.Error(err)
}
}

View File

@ -0,0 +1,48 @@
package controller
import (
"encoding/json"
"fmt"
"github.com/Yuzuki616/V2bX/api"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/infra/conf"
)
//OutboundBuilder build freedom outbund config for addoutbound
func OutboundBuilder(config *Config, nodeInfo *api.NodeInfo, tag string) (*core.OutboundHandlerConfig, error) {
outboundDetourConfig := &conf.OutboundDetourConfig{}
outboundDetourConfig.Protocol = "freedom"
outboundDetourConfig.Tag = tag
// Build Send IP address
if config.SendIP != "" {
ipAddress := net.ParseAddress(config.SendIP)
outboundDetourConfig.SendThrough = &conf.Address{ipAddress}
}
// Freedom Protocol setting
var domainStrategy string = "Asis"
if config.EnableDNS {
if config.DNSType != "" {
domainStrategy = config.DNSType
} else {
domainStrategy = "UseIP"
}
}
proxySetting := &conf.FreedomConfig{
DomainStrategy: domainStrategy,
}
// Used for Shadowsocks-Plugin
if nodeInfo.NodeType == "dokodemo-door" {
proxySetting.Redirect = fmt.Sprintf("127.0.0.1:%d", nodeInfo.V2ray.Inbounds[0].PortList.Range[0].From-1)
}
var setting json.RawMessage
setting, err := json.Marshal(proxySetting)
if err != nil {
return nil, fmt.Errorf("Marshal proxy %s config fialed: %s", nodeInfo.NodeType, err)
}
outboundDetourConfig.Settings = &setting
return outboundDetourConfig.Build()
}

View File

@ -0,0 +1,126 @@
package controller
import (
"fmt"
"strings"
"github.com/Yuzuki616/V2bX/api"
"github.com/xtls/xray-core/common/protocol"
"github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/infra/conf"
"github.com/xtls/xray-core/proxy/shadowsocks"
"github.com/xtls/xray-core/proxy/trojan"
"github.com/xtls/xray-core/proxy/vless"
)
var AEADMethod = []shadowsocks.CipherType{shadowsocks.CipherType_AES_128_GCM, shadowsocks.CipherType_AES_256_GCM, shadowsocks.CipherType_CHACHA20_POLY1305, shadowsocks.CipherType_XCHACHA20_POLY1305}
func (c *Controller) buildVmessUser(userInfo *[]api.UserInfo, serverAlterID uint16) (users []*protocol.User) {
users = make([]*protocol.User, len(*userInfo))
for i, user := range *userInfo {
vmessAccount := &conf.VMessAccount{
ID: user.V2rayUser.Uuid,
AlterIds: serverAlterID,
Security: "auto",
}
users[i] = &protocol.User{
Level: 0,
Email: c.buildUserTag(&user), // Email: InboundTag|email|uid
Account: serial.ToTypedMessage(vmessAccount.Build()),
}
}
return users
}
func (c *Controller) buildVlessUser(userInfo *[]api.UserInfo) (users []*protocol.User) {
users = make([]*protocol.User, len(*userInfo))
for i, user := range *userInfo {
vlessAccount := &vless.Account{
Id: user.V2rayUser.Uuid,
Flow: "xtls-rprx-direct",
}
users[i] = &protocol.User{
Level: 0,
Email: c.buildUserTag(&user),
Account: serial.ToTypedMessage(vlessAccount),
}
}
return users
}
func (c *Controller) buildTrojanUser(userInfo *[]api.UserInfo) (users []*protocol.User) {
users = make([]*protocol.User, len(*userInfo))
for i, user := range *userInfo {
trojanAccount := &trojan.Account{
Password: user.V2rayUser.Uuid,
Flow: "xtls-rprx-direct",
}
users[i] = &protocol.User{
Level: 0,
Email: c.buildUserTag(&user),
Account: serial.ToTypedMessage(trojanAccount),
}
}
return users
}
func cipherFromString(c string) shadowsocks.CipherType {
switch strings.ToLower(c) {
case "aes-128-gcm", "aead_aes_128_gcm":
return shadowsocks.CipherType_AES_128_GCM
case "aes-256-gcm", "aead_aes_256_gcm":
return shadowsocks.CipherType_AES_256_GCM
case "chacha20-poly1305", "aead_chacha20_poly1305", "chacha20-ietf-poly1305":
return shadowsocks.CipherType_CHACHA20_POLY1305
case "none", "plain":
return shadowsocks.CipherType_NONE
default:
return shadowsocks.CipherType_UNKNOWN
}
}
func (c *Controller) buildSSUser(userInfo *[]api.UserInfo, method string) (users []*protocol.User) {
users = make([]*protocol.User, 0)
cypherMethod := cipherFromString(method)
for _, user := range *userInfo {
ssAccount := &shadowsocks.Account{
Password: user.Secret,
CipherType: cypherMethod,
}
users = append(users, &protocol.User{
Level: 0,
Email: c.buildUserTag(&user),
Account: serial.ToTypedMessage(ssAccount),
})
}
return users
}
func (c *Controller) buildSSPluginUser(userInfo *[]api.UserInfo) (users []*protocol.User) {
users = make([]*protocol.User, 0)
for _, user := range *userInfo {
// Check if the cypher method is AEAD
cypherMethod := cipherFromString(user.Cipher)
for _, aeadMethod := range AEADMethod {
if aeadMethod == cypherMethod {
ssAccount := &shadowsocks.Account{
Password: user.Secret,
CipherType: cypherMethod,
}
users = append(users, &protocol.User{
Level: 0,
Email: c.buildUserTag(&user),
Account: serial.ToTypedMessage(ssAccount),
})
}
}
}
return users
}
func (c *Controller) buildUserTag(user *api.UserInfo) string {
return fmt.Sprintf("%s|%s|%d", c.Tag, user.GetUserEmail(), user.UID)
}

16
service/service.go Normal file
View File

@ -0,0 +1,16 @@
// Package service contains all the services used by XrayR
// To implement an service, one needs to implement the interface below.
package service
// Service is the interface of all the services running in the panel
type Service interface {
Start() error
Close() error
Restart
}
// Restart the service
type Restart interface {
Start() error
Close() error
}