From 4605f1b9cc89b6b2cd520e425ffd82c3153434c2 Mon Sep 17 00:00:00 2001 From: zenaku Date: Wed, 3 Jun 2026 18:00:00 -0500 Subject: [PATCH] Core configuration synchronized with the CLI --- README.md | 290 +++++++++++++++++++++++++++++++++++++--- cli/commands/profile.py | 31 ++++- 2 files changed, 293 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 6809ff0..4727c54 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,282 @@ -# sp-hydra-veil-cli +# hydra-veil CLI -The `sp-hydra-veil-cli` command line interface is the reference implementation of the `sp-hydra-veil-core` library. +> [English](#english) · [Español](#español) -## Build Instructions +--- -### Presumptions +### What is this? -* Your system is configured to use the Simplified Privacy package registry [1]. +A command-line tool to manage VPN profiles and encrypted proxy connections. It supports two connection types: -### Prerequisites +- **WireGuard** — system-level VPN tied to a specific location +- **Operator Proxy** — encrypted proxy (vless or hysteria2) tied to an operator -* `build` -* `twine` +--- -To install them, activate your `venv`, if necessary, and run: +### Installation ```bash -pip install build twine -``` - -### Build Steps - -* Activate your `venv`, if necessary, and run: - -```bash -rm -rf ./dist/* -python3 -m build -python3 -m twine upload --repository forgejo ./dist/* +pip install -e . +python -m cli --version ``` --- -[1] https://forgejo.org/docs/v14.0/user/packages/pypi/#configuring-the-package-registry +### Commands overview + +| Command | Description | +|---|---| +| `profile` | Manage connection profiles | +| `application` | Manage supported applications | +| `operator` | List available operators | +| `policy` | Manage system policies | +| `get` / `set` | Read / write configuration | +| `sync` | Sync data from the server | +| `update` | Update the client | + +--- + +### Creating a profile + +#### WireGuard (system-level VPN) + +```bash +python -m cli profile create system -i 2 -n "my-vpn" -l de:be +``` + +| Argument | Description | +|---|---| +| `-i` | Profile ID (unique integer) | +| `-n` | Profile name (any label) | +| `-l` | Location in `country:city` format | + +#### Operator Proxy + +```bash +python -m cli profile create operator -i 8 -n "zenaku-vless" -o 3 -pr vless +``` + +| Argument | Description | +|---|---| +| `-i` | Profile ID (unique integer) | +| `-n` | Profile name (any label) | +| `-o` | Operator ID (see `operator list`) | +| `-pr` | Protocol: `vless` or `hysteria2` | + +To see available operators: + +```bash +python -m cli operator list +``` + +--- + +### Enabling a profile + +```bash +python -m cli profile enable -i 8 +``` + +On first run, the profile has no subscription. The CLI will prompt: + +``` +1) Request new subscription +2) Enter billing code +3) Exit +``` + +Choose **1** to generate a billing code. The profile needs to be paid and activated before the connection is established. + +--- + +### Dev / staging: bypass BTCPay payment + +If the BTCPay server is not set up, manually activate the latest subscription: + +```bash +sudo docker compose exec laravel php artisan tinker --execute=" +\$sub = \App\Models\Subscription::orderBy('id', 'desc')->first(); +\$sub->expires_at = '2027-12-31 23:59:59'; +\$sub->duration = 720; +\$sub->payment_reference = 'bypass_' . uniqid(); +\$sub->save(); +echo 'OK' . PHP_EOL; +" +``` + +Then enable again: + +```bash +python -m cli profile enable -i 8 +``` + +--- + +### Other useful commands + +```bash +# List all profiles +python -m cli profile list + +# Show a specific profile +python -m cli profile show -i 8 + +# Disable a profile +python -m cli profile disable -i 8 + +# Destroy a profile +python -m cli profile destroy -i 8 + +# List available applications +python -m cli application list + +# Sync data from server +python -m cli sync + +# Get current connection type +python -m cli get connection + +# Set connection type +python -m cli set connection tor +python -m cli set connection system +``` +--- + +## Español + +### ¿Qué es esto? + +Una herramienta de línea de comandos para gestionar perfiles VPN y conexiones proxy encriptadas. Soporta dos tipos de conexión: + +- **WireGuard** — VPN a nivel de sistema ligada a una ubicación específica +- **Operator Proxy** — proxy encriptado (vless o hysteria2) ligado a un operador + +--- + +### Instalación + +```bash +pip install -e . +python -m cli --version +``` + +--- + +### Resumen de comandos + +| Comando | Descripción | +|---|---| +| `profile` | Gestionar perfiles de conexión | +| `application` | Gestionar aplicaciones soportadas | +| `operator` | Listar operadores disponibles | +| `policy` | Gestionar políticas del sistema | +| `get` / `set` | Leer / escribir configuración | +| `sync` | Sincronizar datos del servidor | +| `update` | Actualizar el cliente | + +--- + +### Crear un perfil + +#### WireGuard (VPN a nivel de sistema) + +```bash +python -m cli profile create system -i 2 -n "mi-vpn" -l de:be +``` + +| Argumento | Descripción | +|---|---| +| `-i` | ID del perfil (número único) | +| `-n` | Nombre del perfil (cualquier etiqueta) | +| `-l` | Ubicación en formato `país:ciudad` | + +#### Operator Proxy + +```bash +python -m cli profile create operator -i 8 -n "zenaku-vless" -o 3 -pr vless +``` + +| Argumento | Descripción | +|---|---| +| `-i` | ID del perfil (número único) | +| `-n` | Nombre del perfil (cualquier etiqueta) | +| `-o` | ID del operador (ver `operator list`) | +| `-pr` | Protocolo: `vless` o `hysteria2` | + +Para ver los operadores disponibles: + +```bash +python -m cli operator list +``` + +--- + +### Activar un perfil + +```bash +python -m cli profile enable -i 8 +``` + +La primera vez el perfil no tiene suscripción. El CLI mostrará: + +``` +1) Request new subscription +2) Enter billing code +3) Exit +``` + +Elige **1** para generar un billing code. El perfil debe ser pagado y activado antes de que se establezca la conexión. + +--- + +### Dev / staging: saltarse el pago de BTCPay + +Si el servidor BTCPay no está configurado, activa manualmente la última suscripción: + +```bash +sudo docker compose exec laravel php artisan tinker --execute=" +\$sub = \App\Models\Subscription::orderBy('id', 'desc')->first(); +\$sub->expires_at = '2027-12-31 23:59:59'; +\$sub->duration = 720; +\$sub->payment_reference = 'bypass_' . uniqid(); +\$sub->save(); +echo 'OK' . PHP_EOL; +" +``` + +Luego activa el perfil de nuevo: + +```bash +python -m cli profile enable -i 8 +``` + +--- + +### Otros comandos útiles + +```bash +# Listar todos los perfiles +python -m cli profile list + +# Ver un perfil específico +python -m cli profile show -i 8 + +# Desactivar un perfil +python -m cli profile disable -i 8 + +# Eliminar un perfil +python -m cli profile destroy -i 8 + +# Listar aplicaciones disponibles +python -m cli application list + +# Sincronizar datos del servidor +python -m cli sync + +# Ver tipo de conexión actual +python -m cli get connection + +# Cambiar tipo de conexión +python -m cli set connection tor +python -m cli set connection system +``` \ No newline at end of file diff --git a/cli/commands/profile.py b/cli/commands/profile.py index 3ac7184..1556d12 100644 --- a/cli/commands/profile.py +++ b/cli/commands/profile.py @@ -48,6 +48,11 @@ def register(subparsers): system_parser.add_argument('--location', '-l', default='') system_parser.add_argument('--connection', '-c', dest='connection_type', choices=['wireguard'], default='wireguard') + operator_parser = create_subs.add_parser('operator', parents=[base]) + operator_parser.add_argument('--name', '-n', default='') + operator_parser.add_argument('--operator-id', '-o', type=int, required=True) + operator_parser.add_argument('--protocol', '-pr', choices=['vless', 'hysteria2'], required=True) + return parser @@ -68,6 +73,13 @@ def handle(arguments, main_parser): pprint.pp(ProfileController.get(arguments.id)) elif arguments.subcommand == 'create': + + if arguments.profile_type == 'operator': + connection = SystemConnection('operator', arguments.operator_id, arguments.protocol) + profile = SystemProfile(arguments.id, arguments.name, None, None, connection) + ProfileController.create(profile, profile_observer=profile_observer) + return + location_details = parse_location_argument(arguments.location) location = LocationController.get(location_details.get('country_code'), location_details.get('code')) @@ -110,7 +122,6 @@ def handle(arguments, main_parser): else: main_parser.error('the following argument should be a valid reference: --id/-i') - def _handle_subscription_error(exception, profile, ignore, arguments, main_parser): if type(exception).__name__ == 'InvalidSubscriptionError': print('The profile\'s subscription appears to be invalid.\n') @@ -129,17 +140,24 @@ def _handle_subscription_error(exception, profile, ignore, arguments, main_parse if manage_subscription_input in ('1', ''): print('\nCreating subscription...\n') + subscription_plan = SubscriptionPlanController.get(profile.connection, 720) if subscription_plan is None: raise RuntimeError('No compatible subscription plan was found. Please contact support.') + potential_subscription = SubscriptionController.create(subscription_plan, profile, connection_observer=connection_observer) if potential_subscription is None: raise RuntimeError('The subscription could not be created. Please try again later.') + ProfileController.attach_subscription(profile, potential_subscription) - subscription = InvoiceController.handle_payment(potential_subscription.billing_code, invoice_observer=invoice_observer, connection_observer=connection_observer) - if subscription is None: - raise RuntimeError('The subscription could not be activated. Please try again later.') - ProfileController.attach_subscription(profile, subscription) + + # Operator no necesita invoice + if potential_subscription.operator_id is None: + subscription = InvoiceController.handle_payment(potential_subscription.billing_code, invoice_observer=invoice_observer, connection_observer=connection_observer) + if subscription is None: + raise RuntimeError('The subscription could not be activated. Please try again later.') + ProfileController.attach_subscription(profile, subscription) + ProfileController.enable(profile, ignore=ignore, pristine=arguments.pristine, asynchronous=True, profile_observer=profile_observer, application_version_observer=application_version_observer, connection_observer=connection_observer) elif manage_subscription_input == '2': @@ -157,8 +175,7 @@ def _handle_subscription_error(exception, profile, ignore, arguments, main_parse pass else: print('\nInput appears to be invalid. Please try again.\n') - - + def _build_ignore(arguments): ignore = [] if getattr(arguments, 'without_connection_protection', False):