mirror of
https://github.com/wyx2685/V2bX.git
synced 2025-01-22 09:58:14 -05:00
update
This commit is contained in:
commit
358b5888b4
27
.github/ISSUE_TEMPLATE/bug-report.md
vendored
Normal file
27
.github/ISSUE_TEMPLATE/bug-report.md
vendored
Normal 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`查看并添加日志,以帮助解释你的问题
|
||||||
|
|
||||||
|
**额外的内容**
|
||||||
|
在这里添加关于问题的任何其他内容
|
19
.github/ISSUE_TEMPLATE/feature-request.md
vendored
Normal file
19
.github/ISSUE_TEMPLATE/feature-request.md
vendored
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
name: "功能建议"
|
||||||
|
about: 给XrayR提出建议,让我们做得更好
|
||||||
|
title: ''
|
||||||
|
labels: awaiting reply, feature-request
|
||||||
|
assignees: ''
|
||||||
|
---
|
||||||
|
|
||||||
|
**描述您想要的功能**
|
||||||
|
|
||||||
|
清晰简洁的功能描述。
|
||||||
|
|
||||||
|
**描述您考虑过的替代方案**
|
||||||
|
|
||||||
|
是否有任何替代方案可以解决这个问题?
|
||||||
|
|
||||||
|
**附加上下文**
|
||||||
|
|
||||||
|
在此处添加有关功能请求的任何其他上下文或截图。
|
33
.github/build/friendly-filenames.json
vendored
Normal file
33
.github/build/friendly-filenames.json
vendored
Normal 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
11
.github/dependabot.yml
vendored
Normal 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
67
.github/workflows/codeql-analysis.yml
vendored
Normal 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
40
.github/workflows/docker.yml
vendored
Normal 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
185
.github/workflows/release.yml
vendored
Normal 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
16
.gitignore
vendored
Normal 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
18
Dockerfile
Normal 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
373
LICENSE
Normal 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
101
README.md
Normal 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
|
||||||
|
|
||||||
|
## 免责声明
|
||||||
|
|
||||||
|
本项目只是本人个人学习开发并维护,本人不保证任何可用性,也不对使用本软件造成的任何后果负责。
|
||||||
|
|
||||||
|
## 特点
|
||||||
|
|
||||||
|
* 永久开源且免费。
|
||||||
|
* 支持V2ray,Trojan, 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
14
api/api.go
Normal 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
119
api/apimodel.go
Normal 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
7
api/v2board/model.go
Normal 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
315
api/v2board/v2board.go
Normal 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
101
api/v2board/v2board_test.go
Normal 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
2
app/app.go
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
// Package app contains the third-party app used to replace the default app in xray-core
|
||||||
|
package app
|
205
app/mydispatcher/config.pb.go
Normal file
205
app/mydispatcher/config.pb.go
Normal 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
|
||||||
|
}
|
15
app/mydispatcher/config.proto
Normal file
15
app/mydispatcher/config.proto
Normal 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
566
app/mydispatcher/default.go
Normal 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)
|
||||||
|
}
|
4
app/mydispatcher/dispatcher.go
Normal file
4
app/mydispatcher/dispatcher.go
Normal 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
|
9
app/mydispatcher/errors.generated.go
Normal file
9
app/mydispatcher/errors.generated.go
Normal 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{})
|
||||||
|
}
|
118
app/mydispatcher/fakednssniffer.go
Normal file
118
app/mydispatcher/fakednssniffer.go
Normal 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
132
app/mydispatcher/sniffer.go
Normal 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
25
app/mydispatcher/stats.go
Normal 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)
|
||||||
|
}
|
44
app/mydispatcher/stats_test.go
Normal file
44
app/mydispatcher/stats_test.go
Normal 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
2
common/common.go
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
// Package common contains common utilities that are shared among other packages.
|
||||||
|
package common
|
33
common/legocmd/cmd/account.go
Normal file
33
common/legocmd/cmd/account.go
Normal 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 **/
|
243
common/legocmd/cmd/accounts_storage.go
Normal file
243
common/legocmd/cmd/accounts_storage.go
Normal 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
|
||||||
|
}
|
205
common/legocmd/cmd/certs_storage.go
Normal file
205
common/legocmd/cmd/certs_storage.go
Normal 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
14
common/legocmd/cmd/cmd.go
Normal 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(),
|
||||||
|
}
|
||||||
|
}
|
23
common/legocmd/cmd/cmd_before.go
Normal file
23
common/legocmd/cmd/cmd_before.go
Normal 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
|
||||||
|
}
|
73
common/legocmd/cmd/cmd_dnshelp.go
Normal file
73
common/legocmd/cmd/cmd_dnshelp.go
Normal 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...)
|
||||||
|
}
|
136
common/legocmd/cmd/cmd_list.go
Normal file
136
common/legocmd/cmd/cmd_list.go
Normal 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
|
||||||
|
}
|
225
common/legocmd/cmd/cmd_renew.go
Normal file
225
common/legocmd/cmd/cmd_renew.go
Normal 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
|
||||||
|
}
|
118
common/legocmd/cmd/cmd_renew_test.go
Normal file
118
common/legocmd/cmd/cmd_renew_test.go
Normal 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
62
common/legocmd/cmd/cmd_revoke.go
Normal file
62
common/legocmd/cmd/cmd_revoke.go
Normal 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
|
||||||
|
}
|
186
common/legocmd/cmd/cmd_run.go
Normal file
186
common/legocmd/cmd/cmd_run.go
Normal 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
120
common/legocmd/cmd/flags.go
Normal 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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
47
common/legocmd/cmd/hook.go
Normal file
47
common/legocmd/cmd/hook.go
Normal 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
129
common/legocmd/cmd/setup.go
Normal 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)
|
||||||
|
}
|
126
common/legocmd/cmd/setup_challenges.go
Normal file
126
common/legocmd/cmd/setup_challenges.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
1884
common/legocmd/cmd/zz_gen_cmd_dnshelp.go
Normal file
1884
common/legocmd/cmd/zz_gen_cmd_dnshelp.go
Normal file
File diff suppressed because it is too large
Load Diff
189
common/legocmd/lego.go
Normal file
189
common/legocmd/lego.go
Normal 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
|
||||||
|
}
|
82
common/legocmd/lego_test.go
Normal file
82
common/legocmd/lego_test.go
Normal 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
60
common/legocmd/log/log.go
Normal 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
9
common/limiter/errors.go
Normal 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
180
common/limiter/limiter.go
Normal 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
31
common/limiter/rate.go
Normal 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
9
common/rule/errors.go
Normal 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
82
common/rule/rule.go
Normal 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
|
||||||
|
}
|
41
common/serverstatus/serverstatus.go
Normal file
41
common/serverstatus/serverstatus.go
Normal 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
176
go.mod
Normal 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
|
80
main/config.yml.example
Normal file
80
main/config.yml.example
Normal 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
19
main/custom_inbound.json
Normal 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
28
main/custom_outbound.json
Normal 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
72
main/distro/all/all.go
Normal 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
8
main/dns.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"servers": [
|
||||||
|
"1.1.1.1",
|
||||||
|
"8.8.8.8",
|
||||||
|
"localhost"
|
||||||
|
],
|
||||||
|
"tag": "dns_inbound"
|
||||||
|
}
|
BIN
main/geoip.dat
Normal file
BIN
main/geoip.dat
Normal file
Binary file not shown.
36810
main/geosite.dat
Normal file
36810
main/geosite.dat
Normal file
File diff suppressed because one or more lines are too long
103
main/main.go
Normal file
103
main/main.go
Normal 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
36
main/route.json
Normal 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
3
main/rulelist
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
(.+\.|^)(360|so)\.(cn|com)
|
||||||
|
baidu.com
|
||||||
|
google.com
|
36
panel/config.go
Normal file
36
panel/config.go
Normal 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
30
panel/defaultConfig.go
Normal 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
220
panel/panel.go
Normal 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
|
||||||
|
}
|
36
service/controller/config.go
Normal file
36
service/controller/config.go
Normal 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"`
|
||||||
|
}
|
164
service/controller/control.go
Normal file
164
service/controller/control.go
Normal 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)
|
||||||
|
}
|
357
service/controller/controller.go
Normal file
357
service/controller/controller.go
Normal 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)
|
||||||
|
}
|
82
service/controller/controller_test.go
Normal file
82
service/controller/controller_test.go
Normal 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
|
||||||
|
}
|
255
service/controller/inboundbuilder.go
Normal file
255
service/controller/inboundbuilder.go
Normal 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
|
||||||
|
}
|
100
service/controller/inboundbuilder_test.go
Normal file
100
service/controller/inboundbuilder_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
48
service/controller/outboundbuilder.go
Normal file
48
service/controller/outboundbuilder.go
Normal 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()
|
||||||
|
}
|
126
service/controller/userbuilder.go
Normal file
126
service/controller/userbuilder.go
Normal 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
16
service/service.go
Normal 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
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user