Initial commit: xray-manager v0.2 with Debian 13 support
This commit is contained in:
+221
@@ -0,0 +1,221 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[codz]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
.pybuild
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py.cover
|
||||
*.lcov
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
# Pipfile.lock
|
||||
|
||||
# UV
|
||||
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# uv.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
# poetry.lock
|
||||
# poetry.toml
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
||||
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
||||
# pdm.lock
|
||||
# pdm.toml
|
||||
.pdm-python
|
||||
.pdm-build/
|
||||
|
||||
# pixi
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
||||
# pixi.lock
|
||||
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
||||
# in the .venv directory. It is recommended not to include this directory in version control.
|
||||
.pixi/*
|
||||
!.pixi/config.toml
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule*
|
||||
celerybeat.pid
|
||||
|
||||
# Redis
|
||||
*.rdb
|
||||
*.aof
|
||||
*.pid
|
||||
|
||||
# RabbitMQ
|
||||
mnesia/
|
||||
rabbitmq/
|
||||
rabbitmq-data/
|
||||
|
||||
# ActiveMQ
|
||||
activemq-data/
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.envrc
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
# .idea/
|
||||
|
||||
# Abstra
|
||||
# Abstra is an AI-powered process automation framework.
|
||||
# Ignore directories containing user credentials, local state, and settings.
|
||||
# Learn more at https://abstra.io/docs
|
||||
.abstra/
|
||||
|
||||
# Visual Studio Code
|
||||
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
||||
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
||||
# you could uncomment the following to ignore the entire vscode folder
|
||||
# .vscode/
|
||||
# Temporary file for partial code execution
|
||||
tempCodeRunnerFile.py
|
||||
|
||||
# Ruff stuff:
|
||||
.ruff_cache/
|
||||
|
||||
# PyPI configuration file
|
||||
.pypirc
|
||||
|
||||
# Marimo
|
||||
marimo/_static/
|
||||
marimo/_lsp/
|
||||
__marimo__/
|
||||
|
||||
# Streamlit
|
||||
.streamlit/secrets.toml
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"host": "127.0.0.1",
|
||||
"xray_config_folder": "/usr/local/etc/xray/default",
|
||||
"profiles": {
|
||||
"host": "profiles-site-dns.com",
|
||||
"base_path": "/var/lib/xray-manager/profiles",
|
||||
"file_name": "list.txt",
|
||||
"folder_mapping": {}
|
||||
}
|
||||
}
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
python3-xray-manager (0.2-1) stable; urgency=medium
|
||||
|
||||
* Initial release.
|
||||
|
||||
-- Vladimir Khvan <thisisnotablownfuse@gmail.com> Thu, 28 Jun 2026 20:00:00 +0300
|
||||
Vendored
+16
@@ -0,0 +1,16 @@
|
||||
Source: python3-xray-manager
|
||||
Section: python
|
||||
Priority: optional
|
||||
Maintainer: vkh <vkh@casket.ddns.net>
|
||||
Build-Depends: debhelper-compat (=13),
|
||||
dh-python,
|
||||
pybuild-plugin-pyproject,
|
||||
python3-all,
|
||||
python3-setuptools,
|
||||
python3-build
|
||||
Standards-Version: 4.6.2
|
||||
|
||||
Package: python3-xray-manager
|
||||
Architecture: all
|
||||
Depends: ${python3:Depends}, ${misc:Depends}
|
||||
Description: Utility for managing Xray configuration and user profiles
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
data/config.json etc/xray-manager/
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/make -f
|
||||
|
||||
%:
|
||||
dh $@ --with python3 --buildsystem=pybuild
|
||||
@@ -0,0 +1,16 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=61.0.0"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "xray-manager"
|
||||
version = "0.2"
|
||||
description = "Utility for managing Xray configuration and user profiles"
|
||||
dependencies = [
|
||||
"cryptography>=43.0.0",
|
||||
"qrcode>=8.2",
|
||||
"tabulate>=0.9.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
xray-manager = "xray_manager.cli:main"
|
||||
@@ -0,0 +1,3 @@
|
||||
cryptography==43.0.0
|
||||
qrcode==8.2
|
||||
tabulate==0.9.0
|
||||
@@ -0,0 +1,123 @@
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
from . import handlers
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(prog="xray-manager")
|
||||
|
||||
subparsers = parser.add_subparsers(dest="group", required=True)
|
||||
|
||||
# ---------- user ----------
|
||||
user_parser = subparsers.add_parser("user")
|
||||
user_subparser = user_parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
# user add
|
||||
user_add_parser = user_subparser.add_parser("add")
|
||||
user_add_parser.add_argument("username")
|
||||
user_add_parser.add_argument(
|
||||
"-c",
|
||||
"--connection",
|
||||
nargs="+",
|
||||
default=[],
|
||||
help="Connections to add, e.g. shadowsocks:ru1 vless:us1",
|
||||
)
|
||||
user_add_parser.set_defaults(handler=handlers.user_add)
|
||||
|
||||
# user show
|
||||
user_show_parser = user_subparser.add_parser("show")
|
||||
user_show_parser.add_argument("username", nargs="?")
|
||||
user_show_parser.add_argument("-a", "--all", action="store_true")
|
||||
user_show_parser.set_defaults(handler=handlers.user_show)
|
||||
|
||||
# user modify
|
||||
user_modify_parser = user_subparser.add_parser("modify")
|
||||
user_modify_parser.add_argument("username")
|
||||
user_modify_parser.add_argument(
|
||||
"-a",
|
||||
"--add",
|
||||
dest="to_add",
|
||||
nargs="+",
|
||||
default=[],
|
||||
metavar="SPEC",
|
||||
help="Connections to add",
|
||||
)
|
||||
user_modify_parser.add_argument(
|
||||
"-d",
|
||||
"--delete",
|
||||
dest="to_delete",
|
||||
nargs="+",
|
||||
default=[],
|
||||
metavar="SPEC",
|
||||
help="Connections to delete",
|
||||
)
|
||||
user_modify_parser.set_defaults(handler=handlers.user_modify)
|
||||
|
||||
# user delete
|
||||
user_delete_parser = user_subparser.add_parser("delete")
|
||||
group = user_delete_parser.add_mutually_exclusive_group(required=True)
|
||||
group.add_argument("username", nargs="?")
|
||||
group.add_argument("-a", "--all", action="store_true")
|
||||
user_delete_parser.set_defaults(handler=handlers.user_delete)
|
||||
|
||||
# -------- profile ---------
|
||||
profile_parser = subparsers.add_parser("profile")
|
||||
profile_subparser = profile_parser.add_subparsers(
|
||||
dest="command", required=True
|
||||
)
|
||||
|
||||
# profile build
|
||||
profile_build_parser = profile_subparser.add_parser("build")
|
||||
group = profile_build_parser.add_mutually_exclusive_group(required=True)
|
||||
group.add_argument("username", nargs="?")
|
||||
group.add_argument("-a", "--all", action="store_true")
|
||||
profile_build_parser.set_defaults(handler=handlers.profile_build)
|
||||
|
||||
# profile show
|
||||
profile_show_parser = profile_subparser.add_parser("show")
|
||||
group = profile_show_parser.add_mutually_exclusive_group(required=True)
|
||||
group.add_argument("username", nargs="?")
|
||||
group.add_argument("-a", "--all", action="store_true")
|
||||
profile_show_parser.set_defaults(handler=handlers.profile_show)
|
||||
|
||||
# profile clear
|
||||
profile_clear_parser = profile_subparser.add_parser("clear")
|
||||
group = profile_clear_parser.add_mutually_exclusive_group(required=True)
|
||||
group.add_argument("username", nargs="?")
|
||||
group.add_argument("-a", "--all", action="store_true")
|
||||
profile_clear_parser.set_defaults(handler=handlers.profile_clear)
|
||||
|
||||
# profile delete
|
||||
profile_delete_parser = profile_subparser.add_parser("delete")
|
||||
group = profile_delete_parser.add_mutually_exclusive_group(required=True)
|
||||
group.add_argument("username", nargs="?")
|
||||
group.add_argument("-a", "--all", action="store_true")
|
||||
profile_delete_parser.set_defaults(handler=handlers.profile_delete)
|
||||
|
||||
# profile qrcode
|
||||
profile_qrcode_parser = profile_subparser.add_parser("qrcode")
|
||||
group = profile_qrcode_parser.add_mutually_exclusive_group(required=True)
|
||||
group.add_argument("username", nargs="?")
|
||||
group.add_argument("-a", "--all", action="store_true")
|
||||
profile_qrcode_parser.set_defaults(handler=handlers.profile_qrcode)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def main():
|
||||
parser = build_parser()
|
||||
|
||||
try:
|
||||
args = parser.parse_args()
|
||||
func = args.handler
|
||||
kwargs = vars(args)
|
||||
kwargs.pop("handler")
|
||||
func(**kwargs)
|
||||
except ValueError as e:
|
||||
print(e, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,202 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from .core import (
|
||||
ConnectionFactory,
|
||||
ConnectionKey,
|
||||
Profile,
|
||||
ProfileFactory,
|
||||
ProfileStorage,
|
||||
User,
|
||||
UserFactory,
|
||||
XrayConfig,
|
||||
XrayManagerConfig,
|
||||
)
|
||||
from .utils import print_qrcode, print_table
|
||||
|
||||
DEFAULT_CONFIG_PATH = "/etc/xray-manager/config.json"
|
||||
DEV_CONFIG_PATH = "tests/mock_data/etc/xray-manager/config.json"
|
||||
|
||||
|
||||
def _resolve_config_path() -> str:
|
||||
env_path = os.getenv("XRAY_MANAGER_CONFIG")
|
||||
if env_path:
|
||||
return env_path
|
||||
|
||||
if os.getenv("XRAY_ENV") == "development":
|
||||
return DEV_CONFIG_PATH
|
||||
|
||||
if os.path.exists(DEFAULT_CONFIG_PATH):
|
||||
return DEFAULT_CONFIG_PATH
|
||||
|
||||
return DEV_CONFIG_PATH
|
||||
|
||||
|
||||
xray_manager_config = XrayManagerConfig(_resolve_config_path())
|
||||
|
||||
xray_config = XrayConfig(
|
||||
xray_manager_config.xray_config_folder, xray_manager_config.host
|
||||
)
|
||||
|
||||
storage = ProfileStorage(xray_manager_config)
|
||||
|
||||
|
||||
def _resolve_users(username: str | None, all: bool) -> list[User]:
|
||||
if all:
|
||||
return xray_config.get_users()
|
||||
if username:
|
||||
user = xray_config.get_user(username)
|
||||
if not isinstance(user, User):
|
||||
raise ValueError(f"User not found: {username}")
|
||||
return [user]
|
||||
raise ValueError("Either username or --all must be specified")
|
||||
|
||||
|
||||
def _resolve_profiles(username: str | None, all: bool) -> list[Profile]:
|
||||
if all:
|
||||
return storage.load_profiles()
|
||||
if username:
|
||||
try:
|
||||
profile = storage.load_profile(username)
|
||||
except (ValueError, FileNotFoundError) as e:
|
||||
raise ValueError(f"Cannot load profile for {username}: {e}")
|
||||
return [profile]
|
||||
raise ValueError("Either username or --all must be specified")
|
||||
|
||||
|
||||
def _parse_connection_spec(spec: str) -> ConnectionKey:
|
||||
protocol, exit_point = spec.split(":", 1)
|
||||
if protocol not in ConnectionFactory.get_protocols():
|
||||
raise ValueError(f"Unsupported protocol: {protocol}")
|
||||
if exit_point not in xray_config.get_exit_points():
|
||||
raise ValueError(f"Unsupported exit point: {exit_point}")
|
||||
return ConnectionKey(protocol, exit_point)
|
||||
|
||||
|
||||
def _build_user_rows(users: list[User]) -> list[list[str]]:
|
||||
rows = []
|
||||
for index, user in enumerate(users, start=1):
|
||||
first = True
|
||||
for conn in user.connections:
|
||||
if first:
|
||||
row = [index, user.username, conn.exit_point, conn.protocol]
|
||||
first = False
|
||||
else:
|
||||
row = ["", "", conn.exit_point, conn.protocol]
|
||||
rows.append(row)
|
||||
return rows
|
||||
|
||||
|
||||
def _build_profile_rows(profiles: list[Profile]) -> list[list[str]]:
|
||||
rows = []
|
||||
for index, profile in enumerate(profiles, start=1):
|
||||
first = True
|
||||
for conn in profile.user.connections:
|
||||
if first:
|
||||
row = [
|
||||
index,
|
||||
profile.user.username,
|
||||
profile.folder_name,
|
||||
conn.exit_point,
|
||||
conn.protocol,
|
||||
]
|
||||
first = False
|
||||
else:
|
||||
row = ["", "", "", conn.exit_point, conn.protocol]
|
||||
rows.append(row)
|
||||
return rows
|
||||
|
||||
|
||||
def user_add(username: str, connection: list[str], **_):
|
||||
user = xray_config.get_user(username)
|
||||
|
||||
if isinstance(user, User):
|
||||
raise ValueError(f"User already exists: {username}")
|
||||
|
||||
keys: list[ConnectionKey] = [
|
||||
_parse_connection_spec(spec) for spec in connection
|
||||
]
|
||||
|
||||
user = UserFactory.from_spec(
|
||||
username=username, keys=keys, xray_config=xray_config
|
||||
)
|
||||
|
||||
xray_config.add_user(user)
|
||||
|
||||
|
||||
def user_show(username: str | None, all: bool, **_):
|
||||
users = _resolve_users(username, all)
|
||||
|
||||
headers = ["#", "Username", "Exit Point", "Protocol"]
|
||||
rows = _build_user_rows(users)
|
||||
|
||||
print_table(headers, rows)
|
||||
|
||||
|
||||
def user_modify(username: str, to_add: list[str], to_delete: list[str], **_):
|
||||
user = xray_config.get_user(username)
|
||||
|
||||
if not isinstance(user, User):
|
||||
raise ValueError(f"User not found: {username}")
|
||||
|
||||
add_key = [_parse_connection_spec(spec) for spec in to_add]
|
||||
del_key = [_parse_connection_spec(spec) for spec in to_delete]
|
||||
|
||||
user.modify_connection_by_key(xray_config, add_key, del_key)
|
||||
xray_config.modify_user(user)
|
||||
|
||||
|
||||
def user_delete(username: str | None, all: bool, **_):
|
||||
users = _resolve_users(username, all)
|
||||
|
||||
for user in users:
|
||||
xray_config.delete_user(user.username)
|
||||
|
||||
|
||||
def profile_build(username: str | None, all: bool, **_):
|
||||
users = _resolve_users(username, all)
|
||||
|
||||
for user in users:
|
||||
try:
|
||||
profile = storage.load_profile(user.username)
|
||||
profile = ProfileFactory.from_user(user, profile.folder_name)
|
||||
except (ValueError, FileNotFoundError) as e:
|
||||
print(f"Cannot load profile for {user.username}: {e}")
|
||||
profile = ProfileFactory.from_user(user)
|
||||
|
||||
storage.save_profile(profile)
|
||||
|
||||
|
||||
def profile_show(username: str | None, all: bool, **_):
|
||||
profiles = _resolve_profiles(username, all)
|
||||
|
||||
headers = ["#", "Username", "Profile", "Exit Point", "Protocol"]
|
||||
rows = _build_profile_rows(profiles)
|
||||
|
||||
print_table(headers, rows)
|
||||
|
||||
|
||||
def profile_clear(username: str | None, all: bool, **_):
|
||||
profiles = _resolve_profiles(username, all)
|
||||
|
||||
for profile in profiles:
|
||||
storage.clear_profile(profile.user.username)
|
||||
|
||||
|
||||
def profile_delete(username: str | None, all: bool, **_):
|
||||
profiles = _resolve_profiles(username, all)
|
||||
|
||||
for profile in profiles:
|
||||
storage.delete_profile(profile.user.username)
|
||||
|
||||
|
||||
def profile_qrcode(username: str | None, all: bool, **_):
|
||||
profiles = _resolve_profiles(username, all)
|
||||
|
||||
for profile in profiles:
|
||||
host = xray_manager_config.profile_host
|
||||
folder_name = profile.folder_name
|
||||
filename = xray_manager_config.profile_file_name
|
||||
|
||||
url = f"https://{host}/{folder_name}/{filename}"
|
||||
print_qrcode(url)
|
||||
@@ -0,0 +1,44 @@
|
||||
import base64
|
||||
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import x25519
|
||||
from qrcode import ERROR_CORRECT_L, QRCode
|
||||
from tabulate import tabulate
|
||||
|
||||
|
||||
def print_table(headers: list[str], rows: list[list[str]]) -> None:
|
||||
table = tabulate(rows, headers=headers, tablefmt="rounded_grid")
|
||||
print(table)
|
||||
|
||||
|
||||
def print_qrcode(url: str) -> None:
|
||||
qr = QRCode(version=1, error_correction=ERROR_CORRECT_L)
|
||||
qr.add_data(url)
|
||||
qr.make()
|
||||
|
||||
print(f"{url}\n")
|
||||
qr.print_ascii()
|
||||
|
||||
|
||||
def private_to_public(private_key_b64: str) -> str:
|
||||
def b64decode(s: str) -> bytes:
|
||||
padding = "=" * (-len(s) % 4)
|
||||
return base64.urlsafe_b64decode(s + padding)
|
||||
|
||||
def b64encode(b: bytes) -> str:
|
||||
return base64.urlsafe_b64encode(b).decode().rstrip("=")
|
||||
|
||||
private_bytes = b64decode(private_key_b64)
|
||||
|
||||
if len(private_bytes) != 32:
|
||||
raise ValueError("Invalid private key length (expected 32 bytes)")
|
||||
|
||||
private_key = x25519.X25519PrivateKey.from_private_bytes(private_bytes)
|
||||
public_key = private_key.public_key()
|
||||
|
||||
public_bytes = public_key.public_bytes(
|
||||
encoding=serialization.Encoding.Raw,
|
||||
format=serialization.PublicFormat.Raw,
|
||||
)
|
||||
|
||||
return b64encode(public_bytes)
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"host": "127.0.0.1",
|
||||
"xray_config_folder": "tests/mock_data/usr/local/etc/xray/default",
|
||||
"profiles": {
|
||||
"host": "localhost.internal",
|
||||
"base_path": "tests/mock_data/var/lib/xray-manager/profiles",
|
||||
"file_name": "list.txt",
|
||||
"folder_mapping": {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"log": {
|
||||
"loglevel": "info"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"inbounds": [
|
||||
{
|
||||
"tag": "shadowsocks",
|
||||
"xrm": true,
|
||||
"listen": "0.0.0.0",
|
||||
"port": 8443,
|
||||
"protocol": "shadowsocks",
|
||||
"settings": {
|
||||
"method": "2022-blake3-aes-256-gcm",
|
||||
"password": "Z8WtMeWzZVh1F6/5URGrs8vWdB3CLN5y7A9D1U0Q65E=",
|
||||
"clients": [],
|
||||
"network": "tcp,udp"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"inbounds": [
|
||||
{
|
||||
"tag": "in-vless",
|
||||
"xrm": true,
|
||||
"listen": "127.0.0.1",
|
||||
"port": 443,
|
||||
"protocol": "vless",
|
||||
"settings": {
|
||||
"clients": [],
|
||||
"decryption": "none"
|
||||
},
|
||||
"streamSettings": {
|
||||
"network": "tcp",
|
||||
"security": "reality",
|
||||
"realitySettings": {
|
||||
"show": false,
|
||||
"dest": "google.com:443",
|
||||
"xver": 0,
|
||||
"serverNames": [
|
||||
"google.com"
|
||||
],
|
||||
"privateKey": "yHJQq57MNRi-0UQm7ymiy2DI17iO_Ovu2HF5EV1ViGg",
|
||||
"minClientVer": "",
|
||||
"maxClientVer": "",
|
||||
"maxTimeDiff": 0,
|
||||
"shortIds": []
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"outbounds": [
|
||||
{
|
||||
"tag": "out-relay",
|
||||
"protocol": "freedom"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"outbounds": [
|
||||
{
|
||||
"tag": "out-us1",
|
||||
"protocol": "vless",
|
||||
"settings": {
|
||||
"vnext": [
|
||||
{
|
||||
"address": "127.0.0.1",
|
||||
"port": 443,
|
||||
"users": [
|
||||
{
|
||||
"id": "-",
|
||||
"encryption": "none",
|
||||
"flow": "xtls-rprx-vision"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"streamSettings": {
|
||||
"network": "tcp",
|
||||
"security": "reality",
|
||||
"realitySettings": {
|
||||
"fingerprint": "chrome",
|
||||
"serverName": "google.com",
|
||||
"publicKey": "-",
|
||||
"spiderX": "",
|
||||
"shortId": "-"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"outbounds": [
|
||||
{
|
||||
"tag": "out-us2",
|
||||
"protocol": "vless",
|
||||
"settings": {
|
||||
"vnext": [
|
||||
{
|
||||
"address": "127.0.0.1",
|
||||
"port": 443,
|
||||
"users": [
|
||||
{
|
||||
"id": "-",
|
||||
"encryption": "none",
|
||||
"flow": "xtls-rprx-vision"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"streamSettings": {
|
||||
"network": "tcp",
|
||||
"security": "reality",
|
||||
"realitySettings": {
|
||||
"fingerprint": "chrome",
|
||||
"serverName": "google.com",
|
||||
"publicKey": "-",
|
||||
"spiderX": "",
|
||||
"shortId": "-"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"outbounds": [
|
||||
{
|
||||
"tag": "out-fr1",
|
||||
"protocol": "socks",
|
||||
"settings": {
|
||||
"address": "127.0.0.1",
|
||||
"port": 1080
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"routing": {
|
||||
"domainStrategy": "AsIs",
|
||||
"rules": [
|
||||
{
|
||||
"type": "field",
|
||||
"user": [
|
||||
],
|
||||
"outboundTag": "out-fr1"
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"user": [
|
||||
],
|
||||
"outboundTag": "out-us1"
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"user": [
|
||||
],
|
||||
"outboundTag": "out-us2"
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"user": [
|
||||
],
|
||||
"outboundTag": "out-relay"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user