diff --git a/README.md b/README.md index 7a0250f..23e1148 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,29 @@ +# hydra-veil — usage + +## 1. Create billing code / Crear billing code + +```python python3 -c " from core.services.WebServiceApiService import WebServiceApiService -subscription = WebServiceApiService.post_subscription(2, operator_id=3) +subscription = WebServiceApiService.post_subscription(2, operator_id=) print('billing code:', subscription.billing_code) " +``` -parchear +> Copy the printed `billing_code` — you'll need it in the connect step. +> Copiá el `billing_code` impreso, lo vas a necesitar en el paso de conexión. + +--- +consultar estado de invoive +python3 -c " +from core.services.WebServiceApiService import WebServiceApiService + +invoice = WebServiceApiService.get_invoice('BILLING_CODE_HERE') +print('status:', invoice.status) +" +## 2. Patch subscription / Parchear suscripción + +```bash sudo docker compose exec laravel.test php artisan tinker --execute=" \$sub = \App\Models\Subscription::orderBy('id', 'desc')->first(); \$sub->expires_at = '2027-12-31 23:59:59'; @@ -13,7 +32,15 @@ sudo docker compose exec laravel.test php artisan tinker --execute=" \$sub->save(); echo 'OK' . PHP_EOL; " -conectar +``` + +--- + +## 3. Connect / Conectar + +### VLESS + +```python python3 -c " from core.services.WebServiceApiService import WebServiceApiService from core.controllers.encrypted_proxy.VlessController import VlessController @@ -24,11 +51,48 @@ observer.subscribe('connected', lambda e: print('connected:', e.subject)) observer.subscribe('error', lambda e: print('error:', e.subject)) observer.subscribe('disconnected', lambda e: print('disconnected:', e.subject)) -session = WebServiceApiService.post_operator_proxy('617N-LPKB-MVPT-YRQM', 3, 'vless') +session = WebServiceApiService.post_operator_proxy('', , 'vless') print('session:', session) controller = VlessController(1080) controller.enable(session.links[0], session.username, observer) " +``` +### Hysteria2 +```python +python3 -c " +from core.services.WebServiceApiService import WebServiceApiService +from core.controllers.encrypted_proxy.HysteriaController import HysteriaController +from core.observers.EncryptedProxyObserver import EncryptedProxyObserver + +observer = EncryptedProxyObserver() +observer.subscribe('connected', lambda e: print('connected:', e.subject)) +observer.subscribe('error', lambda e: print('error:', e.subject)) +observer.subscribe('disconnected', lambda e: print('disconnected:', e.subject)) + +session = WebServiceApiService.post_operator_proxy('', , 'hysteria2') +print('session:', session) + +controller = HysteriaController(1080) +controller.enable(session.username, session.password, session.operator_hysteria2_host, observer) +" +``` + +--- + +## 4. Disconnect / Desconectar + +```python +python3 -c " +from core.controllers.encrypted_proxy.HysteriaController import HysteriaController +from core.observers.EncryptedProxyObserver import EncryptedProxyObserver + +observer = EncryptedProxyObserver() +observer.subscribe('disconnected', lambda e: print('disconnected')) + +controller = HysteriaController(1080) +controller.disable(observer) +" +``` diff --git a/app/Constants.py b/app/Constants.py deleted file mode 100644 index 8c2d82e..0000000 --- a/app/Constants.py +++ /dev/null @@ -1,50 +0,0 @@ -from dataclasses import dataclass -from typing import Final -from dotenv import load_dotenv -import os - -load_dotenv(os.path.join(os.path.dirname(__file__), '../.env')) - -_env = os.environ.get('APP_ENV', 'production') -_sp_api_base_url = ( - os.environ.get('SP_API_BASE_URL_LOCAL', 'http://simplifiedprivacy.test/api/v1') - if _env == 'local' - else os.environ.get('SP_API_BASE_URL_PRODUCTION', 'https://api.hydraveil.net/api/v1') -) - -@dataclass(frozen=True) -class Constants: - TICKET_API_BASE_URL: Final[str] = os.environ.get( - "TICKET_API_BASE_URL", "https://ticket.hydraveil.net" - ) - SP_API_BASE_URL: Final[str] = _sp_api_base_url - PING_URL: Final[str] = os.environ.get('PING_URL', f'{_sp_api_base_url}/health') - CONNECTION_RETRY_INTERVAL: Final[int] = int(os.environ.get('CONNECTION_RETRY_INTERVAL', '5')) - MAX_CONNECTION_ATTEMPTS: Final[int] = int(os.environ.get('MAX_CONNECTION_ATTEMPTS', '2')) - HV_CLIENT_PATH: Final[str] = os.environ.get('HV_CLIENT_PATH') - HV_CLIENT_VERSION_NUMBER: Final[str] = os.environ.get('HV_CLIENT_VERSION_NUMBER') - HOME: Final[str] = os.path.expanduser('~') - SYSTEM_CONFIG_PATH: Final[str] = '/etc' - INSTALL_DIR: Final[str] = os.environ.get('INSTALL_DIR', os.path.expanduser('~')) - CACHE_HOME: Final[str] = os.environ.get('XDG_CACHE_HOME', f'{INSTALL_DIR}/.cache') - CONFIG_HOME: Final[str] = os.environ.get('XDG_CONFIG_HOME', f'{INSTALL_DIR}/.config') - DATA_HOME: Final[str] = os.environ.get('XDG_DATA_HOME', f'{INSTALL_DIR}/data') - STATE_HOME: Final[str] = os.environ.get('XDG_STATE_HOME', f'{INSTALL_DIR}/.state') - HV_SYSTEM_CONFIG_PATH: Final[str] = f'{SYSTEM_CONFIG_PATH}/hydra-veil' - HV_CACHE_HOME: Final[str] = f'{CACHE_HOME}/hydra-veil' - HV_CONFIG_HOME: Final[str] = f'{CONFIG_HOME}/hydra-veil' - HV_DATA_HOME: Final[str] = f'{DATA_HOME}/hydra-veil' - HV_STATE_HOME: Final[str] = f'{STATE_HOME}/hydra-veil' - HV_SYSTEM_PROFILE_CONFIG_PATH: Final[str] = f'{HV_SYSTEM_CONFIG_PATH}/profiles' - HV_PROFILE_CONFIG_HOME: Final[str] = f'{HV_CONFIG_HOME}/profiles' - HV_PROFILE_DATA_HOME: Final[str] = f'{HV_DATA_HOME}/profiles' - HV_TICKETING_CONFIG_HOME: Final[str] = f"{HV_CONFIG_HOME}/ticketing" - HV_TICKETING_DATA_HOME: Final[str] = f"{HV_DATA_HOME}/ticket_data" - HV_APPLICATION_DATA_HOME: Final[str] = f'{HV_DATA_HOME}/applications' - HV_INCIDENT_DATA_HOME: Final[str] = f'{HV_DATA_HOME}/incidents' - HV_RUNTIME_DATA_HOME: Final[str] = f'{HV_DATA_HOME}/runtime' - HV_STORAGE_DATABASE_PATH: Final[str] = f'{HV_DATA_HOME}/storage.db' - HV_CAPABILITY_POLICY_PATH: Final[str] = f'{SYSTEM_CONFIG_PATH}/apparmor.d/hydra-veil' - HV_PRIVILEGE_POLICY_PATH: Final[str] = f'{SYSTEM_CONFIG_PATH}/sudoers.d/hydra-veil' - HV_SESSION_STATE_HOME: Final[str] = f'{HV_STATE_HOME}/sessions' - HV_TOR_STATE_HOME: Final[str] = f'{HV_STATE_HOME}/tor' \ No newline at end of file diff --git a/app/utils/encrypted_proxy/net.py b/app/utils/encrypted_proxy/net.py deleted file mode 100644 index 167c10d..0000000 --- a/app/utils/encrypted_proxy/net.py +++ /dev/null @@ -1,44 +0,0 @@ -import socket -import subprocess -import time - - -def resolve(host: str) -> str | None: - try: - return socket.gethostbyname(host) - except Exception: - return None - - -def get_real_ip(timeout: int = 10) -> str: - try: - r = subprocess.run( - ["curl", "-s", "--max-time", str(timeout), "https://ifconfig.me/ip"], - capture_output=True, timeout=timeout + 2, - ) - return r.stdout.decode().strip() or "unknown (empty)" - except Exception as e: - return f"unknown ({e})" - - -def get_proxied_ip(socks5_port: int, timeout: int = 10) -> str: - try: - r = subprocess.run( - ["curl", "-s", "--max-time", str(timeout), - "-x", f"socks5h://127.0.0.1:{socks5_port}", - "https://ifconfig.me/ip"], - capture_output=True, timeout=timeout + 2, - ) - return r.stdout.decode().strip() or "unknown (empty)" - except Exception as e: - return f"unknown ({e})" - - -def verify_ip(socks5_port: int, retries: int = 3, delay: float = 2.0) -> str: - for attempt in range(1, retries + 1): - ip = get_proxied_ip(socks5_port) - if not ip.startswith("unknown"): - return ip - if attempt < retries: - time.sleep(delay) - return "unknown" \ No newline at end of file diff --git a/core/Constants.py b/core/Constants.py new file mode 100644 index 0000000..f45ffdd --- /dev/null +++ b/core/Constants.py @@ -0,0 +1,70 @@ +from dataclasses import dataclass +from typing import Final +from dotenv import load_dotenv +import os + +load_dotenv(os.path.join(os.path.dirname(__file__), '../.env')) + +_env = os.environ.get('APP_ENV', 'production') + +_sp_api_base_url = ( + os.environ.get('SP_API_BASE_URL_LOCAL', 'http://simplifiedprivacy.test/api/v1') + if _env == 'local' + else os.environ.get('SP_API_BASE_URL_PRODUCTION', 'https://api.hydraveil.net/api/v1') +) + + +@dataclass(frozen=True) +class Constants: + TICKET_API_BASE_URL: Final[str] = os.environ.get( + "TICKET_API_BASE_URL", "https://ticket.hydraveil.net" + ) + SP_API_BASE_URL: Final[str] = _sp_api_base_url + PING_URL: Final[str] = os.environ.get('PING_URL', f'{_sp_api_base_url}/health') + CONNECTION_RETRY_INTERVAL: Final[int] = int(os.environ.get('CONNECTION_RETRY_INTERVAL', '5')) + MAX_CONNECTION_ATTEMPTS: Final[int] = int(os.environ.get('MAX_CONNECTION_ATTEMPTS', '2')) + HV_CLIENT_PATH: Final[str] = os.environ.get('HV_CLIENT_PATH') + HV_CLIENT_VERSION_NUMBER: Final[str] = os.environ.get('HV_CLIENT_VERSION_NUMBER') + + # ── Paths base ──────────────────────────────────────────────────────────── + HOME: Final[str] = os.path.expanduser('~') + SYSTEM_CONFIG_PATH: Final[str] = '/etc' + INSTALL_DIR: Final[str] = os.environ.get('INSTALL_DIR', os.path.expanduser('~')) + + # ── XDG ─────────────────────────────────────────────────────────────────── + CACHE_HOME: Final[str] = os.environ.get('XDG_CACHE_HOME', f'{INSTALL_DIR}/.cache') + CONFIG_HOME: Final[str] = os.environ.get('XDG_CONFIG_HOME', f'{INSTALL_DIR}/.config') + DATA_HOME: Final[str] = os.environ.get('XDG_DATA_HOME', f'{INSTALL_DIR}/data') + STATE_HOME: Final[str] = os.environ.get('XDG_STATE_HOME', f'{INSTALL_DIR}/.state') + + # ── hydra-veil dirs ─────────────────────────────────────────────────────── + HV_SYSTEM_CONFIG_PATH: Final[str] = f'{SYSTEM_CONFIG_PATH}/hydra-veil' + HV_CACHE_HOME: Final[str] = f'{CACHE_HOME}/hydra-veil' + HV_CONFIG_HOME: Final[str] = f'{CONFIG_HOME}/hydra-veil' + HV_DATA_HOME: Final[str] = f'{DATA_HOME}/hydra-veil' + HV_STATE_HOME: Final[str] = f'{STATE_HOME}/hydra-veil' + + HV_SYSTEM_PROFILE_CONFIG_PATH: Final[str] = f'{HV_SYSTEM_CONFIG_PATH}/profiles' + HV_PROFILE_CONFIG_HOME: Final[str] = f'{HV_CONFIG_HOME}/profiles' + HV_PROFILE_DATA_HOME: Final[str] = f'{HV_DATA_HOME}/profiles' + HV_TICKETING_CONFIG_HOME: Final[str] = f'{HV_CONFIG_HOME}/ticketing' + HV_TICKETING_DATA_HOME: Final[str] = f'{HV_DATA_HOME}/ticket_data' + HV_APPLICATION_DATA_HOME: Final[str] = f'{HV_DATA_HOME}/applications' + HV_INCIDENT_DATA_HOME: Final[str] = f'{HV_DATA_HOME}/incidents' + HV_RUNTIME_DATA_HOME: Final[str] = f'{HV_DATA_HOME}/runtime' + HV_STORAGE_DATABASE_PATH: Final[str] = f'{HV_DATA_HOME}/storage.db' + HV_CAPABILITY_POLICY_PATH: Final[str] = f'{SYSTEM_CONFIG_PATH}/apparmor.d/hydra-veil' + HV_PRIVILEGE_POLICY_PATH: Final[str] = f'{SYSTEM_CONFIG_PATH}/sudoers.d/hydra-veil' + HV_SESSION_STATE_HOME: Final[str] = f'{HV_STATE_HOME}/sessions' + HV_TOR_STATE_HOME: Final[str] = f'{HV_STATE_HOME}/tor' + + # ── sing-box ────────────────────────────────────────────────────────────── + SINGBOX_WRAPPER: Final[str] = os.environ.get( + 'SINGBOX_WRAPPER', '/usr/local/bin/hydraveil-singbox' + ) + SINGBOX_BIN: Final[str] = os.environ.get( + 'SINGBOX_BIN', '/usr/bin/sing-box' + ) + SINGBOX_CONFIG_DIR: Final[str] = f'{HV_DATA_HOME}/configs' + SINGBOX_PID_FILE: Final[str] = f'{HV_RUNTIME_DATA_HOME}/singbox.pid' + SINGBOX_LOG_FILE: Final[str] = f'{HV_RUNTIME_DATA_HOME}/singbox.log' diff --git a/app/Errors.py b/core/Errors.py similarity index 100% rename from app/Errors.py rename to core/Errors.py diff --git a/app/Helpers.py b/core/Helpers.py similarity index 100% rename from app/Helpers.py rename to core/Helpers.py diff --git a/app/__init__.py b/core/__init__.py similarity index 100% rename from app/__init__.py rename to core/__init__.py diff --git a/app/controllers/ApplicationController.py b/core/controllers/ApplicationController.py similarity index 100% rename from app/controllers/ApplicationController.py rename to core/controllers/ApplicationController.py diff --git a/app/controllers/ApplicationVersionController.py b/core/controllers/ApplicationVersionController.py similarity index 100% rename from app/controllers/ApplicationVersionController.py rename to core/controllers/ApplicationVersionController.py diff --git a/app/controllers/ClientController.py b/core/controllers/ClientController.py similarity index 100% rename from app/controllers/ClientController.py rename to core/controllers/ClientController.py diff --git a/app/controllers/ClientVersionController.py b/core/controllers/ClientVersionController.py similarity index 100% rename from app/controllers/ClientVersionController.py rename to core/controllers/ClientVersionController.py diff --git a/app/controllers/ConfigurationController.py b/core/controllers/ConfigurationController.py similarity index 100% rename from app/controllers/ConfigurationController.py rename to core/controllers/ConfigurationController.py diff --git a/app/controllers/ConnectionController.py b/core/controllers/ConnectionController.py similarity index 100% rename from app/controllers/ConnectionController.py rename to core/controllers/ConnectionController.py diff --git a/app/controllers/InvoiceController.py b/core/controllers/InvoiceController.py similarity index 100% rename from app/controllers/InvoiceController.py rename to core/controllers/InvoiceController.py diff --git a/app/controllers/LocationController.py b/core/controllers/LocationController.py similarity index 100% rename from app/controllers/LocationController.py rename to core/controllers/LocationController.py diff --git a/app/controllers/OperatorController.py b/core/controllers/OperatorController.py similarity index 100% rename from app/controllers/OperatorController.py rename to core/controllers/OperatorController.py diff --git a/app/controllers/PolicyController.py b/core/controllers/PolicyController.py similarity index 100% rename from app/controllers/PolicyController.py rename to core/controllers/PolicyController.py diff --git a/app/controllers/ProfileController.py b/core/controllers/ProfileController.py similarity index 100% rename from app/controllers/ProfileController.py rename to core/controllers/ProfileController.py diff --git a/app/controllers/SessionStateController.py b/core/controllers/SessionStateController.py similarity index 100% rename from app/controllers/SessionStateController.py rename to core/controllers/SessionStateController.py diff --git a/app/controllers/SubscriptionController.py b/core/controllers/SubscriptionController.py similarity index 100% rename from app/controllers/SubscriptionController.py rename to core/controllers/SubscriptionController.py diff --git a/app/controllers/SubscriptionPlanController.py b/core/controllers/SubscriptionPlanController.py similarity index 100% rename from app/controllers/SubscriptionPlanController.py rename to core/controllers/SubscriptionPlanController.py diff --git a/app/controllers/SystemStateController.py b/core/controllers/SystemStateController.py similarity index 100% rename from app/controllers/SystemStateController.py rename to core/controllers/SystemStateController.py diff --git a/app/controllers/encrypted_proxy/DisableController.py b/core/controllers/encrypted_proxy/DisableController.py similarity index 100% rename from app/controllers/encrypted_proxy/DisableController.py rename to core/controllers/encrypted_proxy/DisableController.py diff --git a/app/controllers/encrypted_proxy/HysteriaController.py b/core/controllers/encrypted_proxy/HysteriaController.py similarity index 100% rename from app/controllers/encrypted_proxy/HysteriaController.py rename to core/controllers/encrypted_proxy/HysteriaController.py diff --git a/app/controllers/encrypted_proxy/VlessController.py b/core/controllers/encrypted_proxy/VlessController.py similarity index 100% rename from app/controllers/encrypted_proxy/VlessController.py rename to core/controllers/encrypted_proxy/VlessController.py diff --git a/app/controllers/encrypted_proxy/__init__.py b/core/controllers/encrypted_proxy/__init__.py similarity index 100% rename from app/controllers/encrypted_proxy/__init__.py rename to core/controllers/encrypted_proxy/__init__.py diff --git a/app/controllers/tickets/FailedVerificationController.py b/core/controllers/tickets/FailedVerificationController.py similarity index 100% rename from app/controllers/tickets/FailedVerificationController.py rename to core/controllers/tickets/FailedVerificationController.py diff --git a/app/controllers/tickets/TicketPayController.py b/core/controllers/tickets/TicketPayController.py similarity index 100% rename from app/controllers/tickets/TicketPayController.py rename to core/controllers/tickets/TicketPayController.py diff --git a/app/controllers/tickets/TicketPrepController.py b/core/controllers/tickets/TicketPrepController.py similarity index 100% rename from app/controllers/tickets/TicketPrepController.py rename to core/controllers/tickets/TicketPrepController.py diff --git a/app/controllers/tickets/TicketSyncController.py b/core/controllers/tickets/TicketSyncController.py similarity index 100% rename from app/controllers/tickets/TicketSyncController.py rename to core/controllers/tickets/TicketSyncController.py diff --git a/app/controllers/tickets/UseTicketController.py b/core/controllers/tickets/UseTicketController.py similarity index 100% rename from app/controllers/tickets/UseTicketController.py rename to core/controllers/tickets/UseTicketController.py diff --git a/app/errors/exceptions.py b/core/errors/exceptions.py similarity index 100% rename from app/errors/exceptions.py rename to core/errors/exceptions.py diff --git a/app/errors/get_error_msg.py b/core/errors/get_error_msg.py similarity index 100% rename from app/errors/get_error_msg.py rename to core/errors/get_error_msg.py diff --git a/app/errors/logger.py b/core/errors/logger.py similarity index 100% rename from app/errors/logger.py rename to core/errors/logger.py diff --git a/app/models/BaseConnection.py b/core/models/BaseConnection.py similarity index 100% rename from app/models/BaseConnection.py rename to core/models/BaseConnection.py diff --git a/app/models/BasePolicy.py b/core/models/BasePolicy.py similarity index 100% rename from app/models/BasePolicy.py rename to core/models/BasePolicy.py diff --git a/app/models/BaseProfile.py b/core/models/BaseProfile.py similarity index 100% rename from app/models/BaseProfile.py rename to core/models/BaseProfile.py diff --git a/app/models/ClientVersion.py b/core/models/ClientVersion.py similarity index 100% rename from app/models/ClientVersion.py rename to core/models/ClientVersion.py diff --git a/app/models/Configuration.py b/core/models/Configuration.py similarity index 100% rename from app/models/Configuration.py rename to core/models/Configuration.py diff --git a/app/models/Event.py b/core/models/Event.py similarity index 100% rename from app/models/Event.py rename to core/models/Event.py diff --git a/app/models/Location.py b/core/models/Location.py similarity index 100% rename from app/models/Location.py rename to core/models/Location.py diff --git a/app/models/Model.py b/core/models/Model.py similarity index 100% rename from app/models/Model.py rename to core/models/Model.py diff --git a/app/models/Operator.py b/core/models/Operator.py similarity index 100% rename from app/models/Operator.py rename to core/models/Operator.py diff --git a/app/models/OperatorProxySession.py b/core/models/OperatorProxySession.py similarity index 100% rename from app/models/OperatorProxySession.py rename to core/models/OperatorProxySession.py diff --git a/app/models/Subscription.py b/core/models/Subscription.py similarity index 100% rename from app/models/Subscription.py rename to core/models/Subscription.py diff --git a/app/models/SubscriptionPlan.py b/core/models/SubscriptionPlan.py similarity index 100% rename from app/models/SubscriptionPlan.py rename to core/models/SubscriptionPlan.py diff --git a/app/models/invoice/Invoice.py b/core/models/invoice/Invoice.py similarity index 100% rename from app/models/invoice/Invoice.py rename to core/models/invoice/Invoice.py diff --git a/app/models/invoice/PaymentMethod.py b/core/models/invoice/PaymentMethod.py similarity index 100% rename from app/models/invoice/PaymentMethod.py rename to core/models/invoice/PaymentMethod.py diff --git a/app/models/invoice/TicketInvoice.py b/core/models/invoice/TicketInvoice.py similarity index 100% rename from app/models/invoice/TicketInvoice.py rename to core/models/invoice/TicketInvoice.py diff --git a/app/models/policy/CapabilityPolicy.py b/core/models/policy/CapabilityPolicy.py similarity index 100% rename from app/models/policy/CapabilityPolicy.py rename to core/models/policy/CapabilityPolicy.py diff --git a/app/models/policy/PrivilegePolicy.py b/core/models/policy/PrivilegePolicy.py similarity index 100% rename from app/models/policy/PrivilegePolicy.py rename to core/models/policy/PrivilegePolicy.py diff --git a/app/models/session/Application.py b/core/models/session/Application.py similarity index 100% rename from app/models/session/Application.py rename to core/models/session/Application.py diff --git a/app/models/session/ApplicationVersion.py b/core/models/session/ApplicationVersion.py similarity index 100% rename from app/models/session/ApplicationVersion.py rename to core/models/session/ApplicationVersion.py diff --git a/app/models/session/NetworkPortNumbers.py b/core/models/session/NetworkPortNumbers.py similarity index 100% rename from app/models/session/NetworkPortNumbers.py rename to core/models/session/NetworkPortNumbers.py diff --git a/app/models/session/ProxyConfiguration.py b/core/models/session/ProxyConfiguration.py similarity index 100% rename from app/models/session/ProxyConfiguration.py rename to core/models/session/ProxyConfiguration.py diff --git a/app/models/session/SessionConnection.py b/core/models/session/SessionConnection.py similarity index 100% rename from app/models/session/SessionConnection.py rename to core/models/session/SessionConnection.py diff --git a/app/models/session/SessionProfile.py b/core/models/session/SessionProfile.py similarity index 100% rename from app/models/session/SessionProfile.py rename to core/models/session/SessionProfile.py diff --git a/app/models/session/SessionState.py b/core/models/session/SessionState.py similarity index 100% rename from app/models/session/SessionState.py rename to core/models/session/SessionState.py diff --git a/app/models/system/SystemConnection.py b/core/models/system/SystemConnection.py similarity index 100% rename from app/models/system/SystemConnection.py rename to core/models/system/SystemConnection.py diff --git a/app/models/system/SystemProfile.py b/core/models/system/SystemProfile.py similarity index 100% rename from app/models/system/SystemProfile.py rename to core/models/system/SystemProfile.py diff --git a/app/models/system/SystemState.py b/core/models/system/SystemState.py similarity index 100% rename from app/models/system/SystemState.py rename to core/models/system/SystemState.py diff --git a/app/observers/ApplicationVersionObserver.py b/core/observers/ApplicationVersionObserver.py similarity index 100% rename from app/observers/ApplicationVersionObserver.py rename to core/observers/ApplicationVersionObserver.py diff --git a/app/observers/BaseObserver.py b/core/observers/BaseObserver.py similarity index 100% rename from app/observers/BaseObserver.py rename to core/observers/BaseObserver.py diff --git a/app/observers/ClientObserver.py b/core/observers/ClientObserver.py similarity index 100% rename from app/observers/ClientObserver.py rename to core/observers/ClientObserver.py diff --git a/app/observers/ConnectionObserver.py b/core/observers/ConnectionObserver.py similarity index 100% rename from app/observers/ConnectionObserver.py rename to core/observers/ConnectionObserver.py diff --git a/app/observers/EncryptedProxyObserver.py b/core/observers/EncryptedProxyObserver.py similarity index 100% rename from app/observers/EncryptedProxyObserver.py rename to core/observers/EncryptedProxyObserver.py diff --git a/app/observers/InvoiceObserver.py b/core/observers/InvoiceObserver.py similarity index 100% rename from app/observers/InvoiceObserver.py rename to core/observers/InvoiceObserver.py diff --git a/app/observers/ProfileObserver.py b/core/observers/ProfileObserver.py similarity index 100% rename from app/observers/ProfileObserver.py rename to core/observers/ProfileObserver.py diff --git a/app/observers/TicketObserver.py b/core/observers/TicketObserver.py similarity index 100% rename from app/observers/TicketObserver.py rename to core/observers/TicketObserver.py diff --git a/app/services/WebServiceApiService.py b/core/services/WebServiceApiService.py similarity index 100% rename from app/services/WebServiceApiService.py rename to core/services/WebServiceApiService.py diff --git a/app/services/crypto/TicketCustomer.py b/core/services/crypto/TicketCustomer.py similarity index 100% rename from app/services/crypto/TicketCustomer.py rename to core/services/crypto/TicketCustomer.py diff --git a/app/services/crypto/make_commitments.py b/core/services/crypto/make_commitments.py similarity index 100% rename from app/services/crypto/make_commitments.py rename to core/services/crypto/make_commitments.py diff --git a/app/services/encrypted_proxy/__init__.py b/core/services/encrypted_proxy/__init__.py similarity index 100% rename from app/services/encrypted_proxy/__init__.py rename to core/services/encrypted_proxy/__init__.py diff --git a/app/services/encrypted_proxy/disable_service.py b/core/services/encrypted_proxy/disable_service.py similarity index 100% rename from app/services/encrypted_proxy/disable_service.py rename to core/services/encrypted_proxy/disable_service.py diff --git a/app/services/encrypted_proxy/hysteria_service.py b/core/services/encrypted_proxy/hysteria_service.py similarity index 91% rename from app/services/encrypted_proxy/hysteria_service.py rename to core/services/encrypted_proxy/hysteria_service.py index 7695070..42bf09a 100644 --- a/app/services/encrypted_proxy/hysteria_service.py +++ b/core/services/encrypted_proxy/hysteria_service.py @@ -1,4 +1,5 @@ import socket +import time from pathlib import Path from core.Constants import Constants from core.utils.encrypted_proxy.net import get_real_ip, verify_ip @@ -12,6 +13,7 @@ def _resolve(host: str) -> str: def build_hysteria_config(username: str, password: str, server_host: str, socks5_port: int) -> dict: server_ip = _resolve(server_host) + return { "dns": { "servers": [{"tag": "local", "type": "udp", "server": "9.9.9.9"}], @@ -55,6 +57,8 @@ def build_hysteria_config(username: str, password: str, "rules": [ {"protocol": "dns", "action": "hijack-dns"}, {"ip_cidr": ["172.19.0.0/30"], "action": "hijack-dns"}, + # DNS y servidor salen directo — nunca por el TUN + {"ip_cidr": ["9.9.9.9/32"], "outbound": "direct"}, {"ip_cidr": [f"{server_ip}/32"], "outbound": "direct"}, {"ip_is_private": True, "outbound": "direct"}, {"ip_version": 6, "outbound": "block"}, @@ -85,7 +89,11 @@ def enable_hysteria(username: str, password: str, server_host: str, if observer: observer.notify("error", "sing-box not active after start") return False - proxy_ip = verify_ip(socks5_port) + + # Esperar que el proxy esté listo para rutear tráfico + time.sleep(3) + + proxy_ip = verify_ip(socks5_port, retries=5, delay=3.0) if observer: observer.notify("connected", { "real_ip": real_ip, diff --git a/app/services/encrypted_proxy/vless_service.py b/core/services/encrypted_proxy/vless_service.py similarity index 88% rename from app/services/encrypted_proxy/vless_service.py rename to core/services/encrypted_proxy/vless_service.py index da45a73..ac721c5 100644 --- a/app/services/encrypted_proxy/vless_service.py +++ b/core/services/encrypted_proxy/vless_service.py @@ -4,6 +4,7 @@ from core.Constants import Constants from core.utils.encrypted_proxy.net import get_real_ip, verify_ip from core.utils.encrypted_proxy.singbox import SingboxRunner import socket +import time def parse_vless_link(link: str) -> dict: @@ -25,18 +26,24 @@ def parse_vless_link(link: str) -> dict: "port": int(port), "path": unquote(params.get("path", "/vless")), "sni": sni, - "ws_host": ws_host, + "ws_host": ws_host, "security": params.get("security", "tls"), "network": params.get("type", "ws"), } def build_vless_config(vless: dict, socks5_port: int) -> dict: - server_ip = socket.gethostbyname(vless["host"]) + server_ip = socket.gethostbyname(vless["host"]) return { "dns": { - "servers": [{"tag": "local", "type": "udp", "server": "9.9.9.9"}], + "servers": [ + { + "tag": "local", + "type": "udp", + "server": "9.9.9.9", + }, + ], "final": "local", "strategy": "ipv4_only", }, @@ -59,7 +66,7 @@ def build_vless_config(vless: dict, socks5_port: int) -> dict: ], "outbounds": [ {"type": "direct", "tag": "direct"}, - {"type": "block", "tag": "block"}, + {"type": "block", "tag": "block"}, { "type": "vless", "tag": "proxy", @@ -74,7 +81,7 @@ def build_vless_config(vless: dict, socks5_port: int) -> dict: "transport": { "type": "ws", "path": vless["path"], - "headers": {"Host": vless["ws_host"]}, + "headers": {"Host": vless["ws_host"]}, }, }, ], @@ -82,6 +89,7 @@ def build_vless_config(vless: dict, socks5_port: int) -> dict: "rules": [ {"protocol": "dns", "action": "hijack-dns"}, {"ip_cidr": ["172.19.0.0/30"], "action": "hijack-dns"}, + {"ip_cidr": ["9.9.9.9/32"], "outbound": "direct"}, {"ip_cidr": [f"{server_ip}/32"], "outbound": "direct"}, {"ip_is_private": True, "outbound": "direct"}, {"ip_version": 6, "outbound": "block"}, @@ -113,6 +121,10 @@ def enable_vless(vless_link: str, username: str, if observer: observer.notify("error", "sing-box not active after start") return False + + # Esperar que el proxy esté listo para rutear tráfico + time.sleep(3) + proxy_ip = verify_ip(socks5_port, retries=5, delay=3.0) if observer: observer.notify("connected", { diff --git a/app/services/failed_verification/is_the_key_to_blame.py b/core/services/failed_verification/is_the_key_to_blame.py similarity index 100% rename from app/services/failed_verification/is_the_key_to_blame.py rename to core/services/failed_verification/is_the_key_to_blame.py diff --git a/app/services/failed_verification/prep_with_previously_saved_blind_sigs.py b/core/services/failed_verification/prep_with_previously_saved_blind_sigs.py similarity index 100% rename from app/services/failed_verification/prep_with_previously_saved_blind_sigs.py rename to core/services/failed_verification/prep_with_previously_saved_blind_sigs.py diff --git a/app/services/failed_verification/test_if_new_key_works.py b/core/services/failed_verification/test_if_new_key_works.py similarity index 100% rename from app/services/failed_verification/test_if_new_key_works.py rename to core/services/failed_verification/test_if_new_key_works.py diff --git a/app/services/helpers/does_ticket_file_exist.py b/core/services/helpers/does_ticket_file_exist.py similarity index 100% rename from app/services/helpers/does_ticket_file_exist.py rename to core/services/helpers/does_ticket_file_exist.py diff --git a/app/services/helpers/get_how_many_profiles_were_ordered.py b/core/services/helpers/get_how_many_profiles_were_ordered.py similarity index 100% rename from app/services/helpers/get_how_many_profiles_were_ordered.py rename to core/services/helpers/get_how_many_profiles_were_ordered.py diff --git a/app/services/helpers/get_plan_data.py b/core/services/helpers/get_plan_data.py similarity index 100% rename from app/services/helpers/get_plan_data.py rename to core/services/helpers/get_plan_data.py diff --git a/app/services/helpers/get_value_from_config.py b/core/services/helpers/get_value_from_config.py similarity index 100% rename from app/services/helpers/get_value_from_config.py rename to core/services/helpers/get_value_from_config.py diff --git a/app/services/helpers/get_which_billing_key.py b/core/services/helpers/get_which_billing_key.py similarity index 100% rename from app/services/helpers/get_which_billing_key.py rename to core/services/helpers/get_which_billing_key.py diff --git a/app/services/helpers/save_sync_results.py b/core/services/helpers/save_sync_results.py similarity index 100% rename from app/services/helpers/save_sync_results.py rename to core/services/helpers/save_sync_results.py diff --git a/app/services/helpers/valid_profile_quantity.py b/core/services/helpers/valid_profile_quantity.py similarity index 100% rename from app/services/helpers/valid_profile_quantity.py rename to core/services/helpers/valid_profile_quantity.py diff --git a/app/services/helpers/validate_number_format.py b/core/services/helpers/validate_number_format.py similarity index 100% rename from app/services/helpers/validate_number_format.py rename to core/services/helpers/validate_number_format.py diff --git a/app/services/networking/evaluate_networking_problem.py b/core/services/networking/evaluate_networking_problem.py similarity index 100% rename from app/services/networking/evaluate_networking_problem.py rename to core/services/networking/evaluate_networking_problem.py diff --git a/app/services/networking/extract_domain.py b/core/services/networking/extract_domain.py similarity index 100% rename from app/services/networking/extract_domain.py rename to core/services/networking/extract_domain.py diff --git a/app/services/networking/get_connection_type.py b/core/services/networking/get_connection_type.py similarity index 100% rename from app/services/networking/get_connection_type.py rename to core/services/networking/get_connection_type.py diff --git a/app/services/networking/get_data_from_server.py b/core/services/networking/get_data_from_server.py similarity index 100% rename from app/services/networking/get_data_from_server.py rename to core/services/networking/get_data_from_server.py diff --git a/app/services/networking/internet_test.py b/core/services/networking/internet_test.py similarity index 100% rename from app/services/networking/internet_test.py rename to core/services/networking/internet_test.py diff --git a/app/services/networking/is_dns_problem.py b/core/services/networking/is_dns_problem.py similarity index 100% rename from app/services/networking/is_dns_problem.py rename to core/services/networking/is_dns_problem.py diff --git a/app/services/networking/is_tor_working.py b/core/services/networking/is_tor_working.py similarity index 100% rename from app/services/networking/is_tor_working.py rename to core/services/networking/is_tor_working.py diff --git a/app/services/networking/make_url.py b/core/services/networking/make_url.py similarity index 100% rename from app/services/networking/make_url.py rename to core/services/networking/make_url.py diff --git a/app/services/networking/regular_get_request.py b/core/services/networking/regular_get_request.py similarity index 100% rename from app/services/networking/regular_get_request.py rename to core/services/networking/regular_get_request.py diff --git a/app/services/networking/regular_post_request.py b/core/services/networking/regular_post_request.py similarity index 100% rename from app/services/networking/regular_post_request.py rename to core/services/networking/regular_post_request.py diff --git a/app/services/networking/send_data_to_server.py b/core/services/networking/send_data_to_server.py similarity index 100% rename from app/services/networking/send_data_to_server.py rename to core/services/networking/send_data_to_server.py diff --git a/app/services/networking/use_tor.py b/core/services/networking/use_tor.py similarity index 100% rename from app/services/networking/use_tor.py rename to core/services/networking/use_tor.py diff --git a/app/services/payment_phase/check_if_paid.py b/core/services/payment_phase/check_if_paid.py similarity index 100% rename from app/services/payment_phase/check_if_paid.py rename to core/services/payment_phase/check_if_paid.py diff --git a/app/services/payment_phase/extract_payment_details.py b/core/services/payment_phase/extract_payment_details.py similarity index 100% rename from app/services/payment_phase/extract_payment_details.py rename to core/services/payment_phase/extract_payment_details.py diff --git a/app/services/payment_phase/save_and_send_intitial_billing.py b/core/services/payment_phase/save_and_send_intitial_billing.py similarity index 100% rename from app/services/payment_phase/save_and_send_intitial_billing.py rename to core/services/payment_phase/save_and_send_intitial_billing.py diff --git a/app/services/payment_phase/save_billing_choices.py b/core/services/payment_phase/save_billing_choices.py similarity index 100% rename from app/services/payment_phase/save_billing_choices.py rename to core/services/payment_phase/save_billing_choices.py diff --git a/app/services/prepare_tickets/get_pub_key.py b/core/services/prepare_tickets/get_pub_key.py similarity index 100% rename from app/services/prepare_tickets/get_pub_key.py rename to core/services/prepare_tickets/get_pub_key.py diff --git a/app/services/prepare_tickets/get_public_key_by_config.py b/core/services/prepare_tickets/get_public_key_by_config.py similarity index 100% rename from app/services/prepare_tickets/get_public_key_by_config.py rename to core/services/prepare_tickets/get_public_key_by_config.py diff --git a/app/services/prepare_tickets/make_sure_pub_key_exists.py b/core/services/prepare_tickets/make_sure_pub_key_exists.py similarity index 100% rename from app/services/prepare_tickets/make_sure_pub_key_exists.py rename to core/services/prepare_tickets/make_sure_pub_key_exists.py diff --git a/app/services/prepare_tickets/save_ALL_blind_sigs.py b/core/services/prepare_tickets/save_ALL_blind_sigs.py similarity index 100% rename from app/services/prepare_tickets/save_ALL_blind_sigs.py rename to core/services/prepare_tickets/save_ALL_blind_sigs.py diff --git a/app/services/prepare_tickets/send_blind_commitments.py b/core/services/prepare_tickets/send_blind_commitments.py similarity index 100% rename from app/services/prepare_tickets/send_blind_commitments.py rename to core/services/prepare_tickets/send_blind_commitments.py diff --git a/app/services/prepare_tickets/setup_ticket_tracker.py b/core/services/prepare_tickets/setup_ticket_tracker.py similarity index 100% rename from app/services/prepare_tickets/setup_ticket_tracker.py rename to core/services/prepare_tickets/setup_ticket_tracker.py diff --git a/app/services/prepare_tickets/ticket_prep_orchestrator.py b/core/services/prepare_tickets/ticket_prep_orchestrator.py similarity index 100% rename from app/services/prepare_tickets/ticket_prep_orchestrator.py rename to core/services/prepare_tickets/ticket_prep_orchestrator.py diff --git a/app/services/prepare_tickets/ticket_tracker.py b/core/services/prepare_tickets/ticket_tracker.py similarity index 100% rename from app/services/prepare_tickets/ticket_tracker.py rename to core/services/prepare_tickets/ticket_tracker.py diff --git a/app/services/prepare_tickets/unblind_all_tickets.py b/core/services/prepare_tickets/unblind_all_tickets.py similarity index 100% rename from app/services/prepare_tickets/unblind_all_tickets.py rename to core/services/prepare_tickets/unblind_all_tickets.py diff --git a/app/services/prepare_tickets/validate_blind_signatures.py b/core/services/prepare_tickets/validate_blind_signatures.py similarity index 100% rename from app/services/prepare_tickets/validate_blind_signatures.py rename to core/services/prepare_tickets/validate_blind_signatures.py diff --git a/app/services/using_tickets/send_unblinded.py b/core/services/using_tickets/send_unblinded.py similarity index 100% rename from app/services/using_tickets/send_unblinded.py rename to core/services/using_tickets/send_unblinded.py diff --git a/app/services/using_tickets/use_ticket_orchestrator.py b/core/services/using_tickets/use_ticket_orchestrator.py similarity index 100% rename from app/services/using_tickets/use_ticket_orchestrator.py rename to core/services/using_tickets/use_ticket_orchestrator.py diff --git a/app/utils/basic_operations/does_file_exist.py b/core/utils/basic_operations/does_file_exist.py similarity index 100% rename from app/utils/basic_operations/does_file_exist.py rename to core/utils/basic_operations/does_file_exist.py diff --git a/app/utils/basic_operations/filter_data.py b/core/utils/basic_operations/filter_data.py similarity index 100% rename from app/utils/basic_operations/filter_data.py rename to core/utils/basic_operations/filter_data.py diff --git a/app/utils/basic_operations/get_json_keys.py b/core/utils/basic_operations/get_json_keys.py similarity index 100% rename from app/utils/basic_operations/get_json_keys.py rename to core/utils/basic_operations/get_json_keys.py diff --git a/app/utils/basic_operations/write_or_read_from_json.py b/core/utils/basic_operations/write_or_read_from_json.py similarity index 100% rename from app/utils/basic_operations/write_or_read_from_json.py rename to core/utils/basic_operations/write_or_read_from_json.py diff --git a/app/utils/basic_operations/write_string_to_text_file.py b/core/utils/basic_operations/write_string_to_text_file.py similarity index 100% rename from app/utils/basic_operations/write_string_to_text_file.py rename to core/utils/basic_operations/write_string_to_text_file.py diff --git a/app/utils/confirm_its_a_valid_key_choice.py b/core/utils/confirm_its_a_valid_key_choice.py similarity index 100% rename from app/utils/confirm_its_a_valid_key_choice.py rename to core/utils/confirm_its_a_valid_key_choice.py diff --git a/app/utils/convert_time.py b/core/utils/convert_time.py similarity index 100% rename from app/utils/convert_time.py rename to core/utils/convert_time.py diff --git a/app/utils/encrypted_proxy/__init__.py b/core/utils/encrypted_proxy/__init__.py similarity index 100% rename from app/utils/encrypted_proxy/__init__.py rename to core/utils/encrypted_proxy/__init__.py diff --git a/core/utils/encrypted_proxy/net.py b/core/utils/encrypted_proxy/net.py new file mode 100644 index 0000000..48f4fd9 --- /dev/null +++ b/core/utils/encrypted_proxy/net.py @@ -0,0 +1,75 @@ +import socket +import subprocess +import time + + +def resolve(host: str) -> str | None: + try: + return socket.gethostbyname(host) + except Exception: + return None + + +def get_real_ip(timeout: int = 10) -> str: + endpoints = [ + "https://api.ipify.org", + "https://ifconfig.me/ip", + "https://icanhazip.com", + ] + for url in endpoints: + try: + r = subprocess.run( + ["curl", "-s", "--max-time", str(timeout), url], + capture_output=True, + timeout=timeout + 2, + ) + ip = r.stdout.decode().strip() + if ip and not ip.startswith("unknown"): + return ip + except Exception: + continue + return "unknown" + + +def get_proxied_ip(socks5_port: int, timeout: int = 15) -> str: + endpoints = [ + "https://api.ipify.org", + "https://ifconfig.me/ip", + "https://icanhazip.com", + ] + for url in endpoints: + try: + r = subprocess.run( + [ + "curl", "-s", + "--max-time", str(timeout), + "--connect-timeout", "10", + "-x", f"socks5h://127.0.0.1:{socks5_port}", + url, + ], + capture_output=True, + timeout=timeout + 2, + ) + ip = r.stdout.decode().strip() + stderr = r.stderr.decode().strip() + if ip and not ip.startswith("<") and "." in ip: + return ip + # Log silencioso para debug + if stderr: + print(f"[net] curl stderr ({url}): {stderr[:120]}") + except subprocess.TimeoutExpired: + print(f"[net] timeout via socks5 → {url}") + except Exception as e: + print(f"[net] error via socks5 → {url}: {e}") + return "unknown" + + +def verify_ip(socks5_port: int, retries: int = 5, delay: float = 3.0) -> str: + for attempt in range(1, retries + 1): + print(f"[net] verify_ip intento {attempt}/{retries}...") + ip = get_proxied_ip(socks5_port) + if ip != "unknown": + return ip + if attempt < retries: + time.sleep(delay) + return "unknown" diff --git a/app/utils/encrypted_proxy/singbox.py b/core/utils/encrypted_proxy/singbox.py similarity index 85% rename from app/utils/encrypted_proxy/singbox.py rename to core/utils/encrypted_proxy/singbox.py index e315ac3..76df3c0 100644 --- a/app/utils/encrypted_proxy/singbox.py +++ b/core/utils/encrypted_proxy/singbox.py @@ -23,8 +23,10 @@ class SingboxRunner: _LOG_FILE = Path(Constants.SINGBOX_LOG_FILE) _CONFIG_DIR = Path(Constants.SINGBOX_CONFIG_DIR) - _START_TIMEOUT_S = 10.0 - _START_POLL_S = 0.5 + _START_TIMEOUT_S = 15.0 # tiempo total de espera + _START_POLL_S = 0.5 # intervalo de polling + _START_INITIAL_S = 2.0 # espera inicial antes de empezar a verificar + # (el wrapper necesita ~1s para escribir el PID file) # ── Público ─────────────────────────────────────────────────────────────── @@ -41,7 +43,7 @@ class SingboxRunner: subprocess.Popen( ['sudo', self._WRAPPER, str(config_path), 'start'], stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, # el wrapper escribe el log como root + stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL, env={**os.environ, 'SUDO_ASKPASS': '/bin/false'}, ) @@ -51,12 +53,14 @@ class SingboxRunner: 'Ejecutá el installer primero.' ) - # Esperar confirmación por PID file, no por tiempo fijo + # Esperar que el wrapper escriba el PID file antes de empezar polling + time.sleep(self._START_INITIAL_S) + deadline = time.monotonic() + self._START_TIMEOUT_S while time.monotonic() < deadline: - time.sleep(self._START_POLL_S) if self.is_running(): return True + time.sleep(self._START_POLL_S) raise RuntimeError( f'sing-box no levantó en {self._START_TIMEOUT_S}s.\n' @@ -78,16 +82,29 @@ class SingboxRunner: timeout=15, ) except (subprocess.TimeoutExpired, FileNotFoundError, OSError): - # Wrapper no responde — kill directo por PID file como último recurso self._emergency_kill() def is_running(self) -> bool: """ Verifica por PID file + señal 0. PermissionError significa que el proceso VIVE (corre como root). + Si el PID file no existe todavía, verifica por pgrep como fallback + para cubrir la ventana entre que el proceso arranca y el wrapper + escribe el PID file. """ if not self._PID_FILE.exists(): - return False + # Fallback: el proceso puede estar vivo pero el PID file + # aún no fue escrito por el wrapper + try: + result = subprocess.run( + ['pgrep', '-x', 'sing-box'], + capture_output=True, + text=True, + ) + return result.returncode == 0 + except OSError: + return False + try: pid = int(self._PID_FILE.read_text().strip()) if pid <= 0: @@ -95,7 +112,6 @@ class SingboxRunner: os.kill(pid, 0) return True except (ValueError, ProcessLookupError): - # PID corrupto o proceso muerto — limpiar self._PID_FILE.unlink(missing_ok=True) return False except PermissionError: @@ -118,7 +134,6 @@ class SingboxRunner: ) return result.stdout.strip() except Exception: - # Fallback: leer directo si tenemos permisos try: lines = self._LOG_FILE.read_text(errors='replace').splitlines() return '\n'.join(lines[-80:]) @@ -183,3 +198,4 @@ class SingboxRunner: pass finally: self._PID_FILE.unlink(missing_ok=True) + diff --git a/app/utils/get_data.py b/core/utils/get_data.py similarity index 100% rename from app/utils/get_data.py rename to core/utils/get_data.py diff --git a/app/utils/get_raw_string.py b/core/utils/get_raw_string.py similarity index 100% rename from app/utils/get_raw_string.py rename to core/utils/get_raw_string.py diff --git a/app/utils/save_data.py b/core/utils/save_data.py similarity index 100% rename from app/utils/save_data.py rename to core/utils/save_data.py diff --git a/data/hydra-veil/storage.db b/data/hydra-veil/storage.db new file mode 100644 index 0000000..628bceb Binary files /dev/null and b/data/hydra-veil/storage.db differ diff --git a/data/hydra-veil/ticket_data/logs/errors.log b/data/hydra-veil/ticket_data/logs/errors.log new file mode 100644 index 0000000..e69de29 diff --git a/install.sh b/install.sh index 1bd0de5..e90c2aa 100755 --- a/install.sh +++ b/install.sh @@ -1,11 +1,11 @@ #!/bin/bash # install.sh — hydra-veil core installer -set -euo pipefail - SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" LIB_DIR="$SCRIPT_DIR/lib" +trap 'echo ""; echo " [ERROR] Installer failed at line $LINENO — check: cat $LOG_FILE" >&2' ERR + for _lib in ui log input network steps; do _path="$LIB_DIR/${_lib}.sh" if [ ! -f "$_path" ]; then @@ -18,14 +18,13 @@ done for arg in "$@"; do case "$arg" in --no-log) LOG_ENABLED="false" ;; - --local) FORCE_ENV="local" ;; esac done _log_init clear -print_header "HYDRA-VEIL CORE INSTALLER" "v1.0.0" +print_header "HYDRA-VEIL CORE INSTALLER" "v2.0.0" printf " ${DIM}%s${RESET}\n" "Simplified Privacy" printf " ${DIM}%s${RESET}\n" "$(date '+%Y-%m-%d %H:%M:%S') — $(uname -n)" print_divider @@ -33,13 +32,13 @@ print_divider print_section "Pre-flight" log_info "User: ${USER}" log_info "Home: ${HOME}" -log_info "Install dir: ${HV_CORE_HOME}" +log_info "Install dir: ${HV_BASE}" require_internet print_divider echo "" -label_info "hydra-veil core se instalará en:" -printf " ${BCYAN}%s${RESET}\n\n" "$HV_CORE_HOME" +label_info "hydra-veil se instalará en:" +printf " ${BCYAN}%s${RESET}\n\n" "$HV_BASE" label_warn "sudo es requerido para sing-box, wrapper y sudoers." echo "" if ! ask_confirm "Continuar con la instalación?"; then @@ -49,8 +48,8 @@ fi echo "" print_divider -step_clone_repo -step_create_env +step_preflight +step_repos step_python_env step_singbox step_wrapper @@ -81,13 +80,16 @@ EOF printf "${RESET}" print_divider echo "" -label_ok "hydra-veil core instalado correctamente" +label_ok "hydra-veil instalado correctamente" label_ok "Todos los steps completados" echo "" label_info "Activar entorno:" -printf " ${CYAN}source %s/.venv/bin/activate${RESET}\n" "$HV_CORE_HOME" +printf " ${CYAN}source %s/core/.venv/bin/activate${RESET}\n" "$HV_BASE" echo "" -label_info "Wrapper disponible en:" +label_info "CLI disponible:" +printf " ${CYAN}python -m cli${RESET}\n" +echo "" +label_info "Wrapper disponible:" printf " ${CYAN}sudo /usr/local/bin/hydraveil-singbox start${RESET}\n" echo "" log_file_path diff --git a/lib/steps.sh b/lib/steps.sh index fb93727..49cc7c8 100755 --- a/lib/steps.sh +++ b/lib/steps.sh @@ -1,536 +1,68 @@ #!/bin/bash -# lib/steps.sh — Install steps for hydra-veil core -# Requiere: lib/ui.sh, lib/log.sh, lib/input.sh, lib/network.sh - -# ─── Ruta canónica fija — todo vive aquí ────────────────────────────────────── -HV_CORE_HOME="$HOME/.local/share/hydra-veil/core" - -# Subdirectorios -HV_REPO_DIR="$HV_CORE_HOME" -HV_VENV_DIR="$HV_CORE_HOME/.venv" -HV_DATA_DIR="$HV_CORE_HOME/data/hydra-veil" -HV_LOG_DIR="$HV_CORE_HOME/logs/hydra-veil" -HV_RUN_DIR="$HV_CORE_HOME/run" -HV_CONFIG_DIR="$HV_DATA_DIR/configs" - -# sing-box -SINGBOX_VERSION="1.13.5" -SINGBOX_BIN="/usr/bin/sing-box" -SINGBOX_PID_FILE="$HV_RUN_DIR/singbox.pid" -SINGBOX_LOG_FILE="$HV_LOG_DIR/singbox.log" - -# Sistema -WRAPPER_PATH="/usr/local/bin/hydraveil-singbox" -SUDOERS_PATH="/etc/sudoers.d/hydraveil" - -REPO_URL="https://git.simplifiedprivacy.com/Support/sp-hydra-veil-core" -BRANCH="core-laravel-proxyTEST" +# lib/steps.sh — orquestador de steps del core installer TOTAL_STEPS=6 -# ─── Helper: crear log file con permisos del usuario ───────────────────────── -# El wrapper corre como root y appendea con >>. Si root crea el archivo primero, -# Python (que corre como usuario) no puede leerlo. Esta función lo crea con -# permisos del usuario ANTES de que el wrapper lo toque por primera vez. -_ensure_log_file() { - local log_file="$1" - local log_dir - log_dir="$(dirname "$log_file")" +# ─── Variables globales ─────────────────────────────────────────────────────── +# Directorio base de hydra-veil +HV_BASE="$HOME/.local/share/hydra-veil" - # Crear directorio si no existe (permisos de usuario) - if [ ! -d "$log_dir" ]; then - mkdir -p "$log_dir" - chmod 700 "$log_dir" - fi +# Componentes +HV_CORE="$HV_BASE/core" +HV_CLI="$HV_BASE/cli" +HV_GUI="$HV_BASE/gui" +HV_ESSENTIALS="$HV_BASE/essentials" - # Crear el archivo solo si no existe — nunca sobreescribir - if [ ! -f "$log_file" ]; then - touch "$log_file" - chmod 600 "$log_file" - fi +# Venv — siempre en el core +HV_VENV="$HV_CORE/.venv" - # Verificar que el usuario actual puede leerlo - if [ ! -r "$log_file" ]; then - log_warn "Log file existe pero no es legible por el usuario actual: $log_file" - log_warn "Intentando corregir permisos..." - chmod 600 "$log_file" 2>/dev/null || { - log_error "No se pudieron corregir los permisos de: $log_file" - return 1 - } - fi -} +# XDG — INSTALL_DIR es $HOME, no el repo +INSTALL_DIR="$HOME" +HV_DATA_DIR="$HOME/data/hydra-veil" +HV_CONFIG_DIR="$HOME/.config/hydra-veil" +HV_CACHE_DIR="$HOME/.cache/hydra-veil" +HV_STATE_DIR="$HOME/.state/hydra-veil" -# ─── Step 1: Repo ───────────────────────────────────────────────────────────── -step_clone_repo() { - print_section "Repository" - log_step "Cloning repository" 1 $TOTAL_STEPS +# sing-box +SINGBOX_BIN="/usr/bin/sing-box" +SINGBOX_WRAPPER="/usr/local/bin/hydraveil-singbox" +SINGBOX_VERSION="1.13.5" +SINGBOX_CONFIG_DIR="$HV_DATA_DIR/configs" +SINGBOX_PID_FILE="$HV_DATA_DIR/runtime/singbox.pid" +SINGBOX_LOG_FILE="$HV_DATA_DIR/runtime/singbox.log" - log_info "Destino: ${HV_REPO_DIR}" +# sudoers +SUDOERS_PATH="/etc/sudoers.d/hydraveil" - if [ -d "$HV_REPO_DIR/.git" ]; then - log_warn "Repo ya existe en: $HV_REPO_DIR" - if ask_confirm "Eliminar y re-clonar?"; then - start_spinner "Eliminando repo anterior..." - rm -rf "$HV_REPO_DIR" - stop_spinner "Eliminado" - else - log_skip "Re-clone omitido — usando repo existente" - return 0 - fi - fi +# Repos +declare -A REPO_URLS=( + ["core"]="https://git.simplifiedprivacy.com/Support/sp-hydra-veil-core " + ["cli"]="https://git.simplifiedprivacy.com/Support/sp-hydra-veil-cli" + ["essentials"]="https://git.simplifiedprivacy.com/Support/sp-essentials" + ["gui"]="https://git.simplifiedprivacy.com/Support/sp-hydra-veil-gui" +) - mkdir -p "$HV_CORE_HOME" - chmod 700 "$HV_CORE_HOME" +declare -A REPO_BRANCHES=( + ["core"]="deploy" + ["cli"]="zenaku" + ["essentials"]="master" + ["gui"]="master" +) - start_spinner "Clonando ${REPO_URL}..." - if git clone --branch "$BRANCH" "$REPO_URL" "$HV_REPO_DIR" &>/dev/null; then - stop_spinner "Repositorio clonado → ${HV_REPO_DIR}" - else - stop_spinner - log_error "git clone falló" +declare -A REPO_DIRS=( + ["cli"]="$HV_CLI" + ["essentials"]="$HV_ESSENTIALS" + ["gui"]="$HV_GUI" +) + +# ─── Importar steps atómicos ────────────────────────────────────────────────── +_STEPS_DIR="$SCRIPT_DIR/lib/steps" + +for _step in preflight repos python_env singbox wrapper sudoers; do + _step_path="${_STEPS_DIR}/${_step}.sh" + if [ ! -f "$_step_path" ]; then + echo "[ERROR] Missing step: $_step_path" >&2 exit 1 fi - - chmod 700 "$HV_REPO_DIR" - log_ok "Permisos aplicados" -} - -# ─── Step 2: .env ───────────────────────────────────────────────────────────── -step_create_env() { - print_section ".env Configuration" - log_step "Building .env from .env.example" 2 $TOTAL_STEPS - - local example_file="$HV_REPO_DIR/.env.example" - local env_file="$HV_REPO_DIR/.env" - - if [ ! -f "$example_file" ]; then - log_error ".env.example no encontrado: $example_file" - exit 1 - fi - - if [ -f "$env_file" ]; then - log_warn ".env ya existe" - if ! ask_confirm "Sobreescribir .env existente?"; then - label_skip ".env sin cambios — omitiendo" - return 0 - fi - fi - - echo "" - printf " ${DIM}Completá cada variable. Enter para dejar vacío.${RESET}\n" - print_divider - - local -a env_lines=() - - while IFS= read -r line || [ -n "$line" ]; do - - # Comentarios y líneas vacías — pasar directo - if [[ -z "$line" || "$line" == \#* ]]; then - env_lines+=("$line") - continue - fi - - local key default_val value - - if [[ "$line" =~ ^([A-Za-z_][A-Za-z0-9_]*)=(.*)$ ]]; then - key="${BASH_REMATCH[1]}" - default_val="${BASH_REMATCH[2]}" - else - env_lines+=("$line") - continue - fi - - if [ "$key" = "APP_ENV" ]; then - echo "" - printf " ${BCYAN}APP_ENV${RESET} ${DIM}— entorno de la aplicación${RESET}\n" - printf " ${BWHITE}1)${RESET} ${GREEN}local${RESET} ${BWHITE}2)${RESET} ${CYAN}production${RESET}\n" - printf " ${BWHITE}Opción${RESET} ${DIM}[1/2, default=2]:${RESET} " - read -r _choice < /dev/tty - case "$_choice" in - 1) value="local" ;; - *) value="production" ;; - esac - log_ok "APP_ENV=${value}" - - else - local hint="" - [ -n "$default_val" ] && hint="${DIM}(default: ${default_val})${RESET}" - echo "" - printf " ${BWHITE}%s${RESET} %b\n" "$key" "$hint" - printf " ${BCYAN}›${RESET} " - read -r value < /dev/tty - [ -z "$value" ] && value="$default_val" - [ -n "$value" ] && log_ok "${key}=${value}" || label_skip "${key} — vacío" - fi - - env_lines+=("${key}=${value}") - - done < "$example_file" - - # Escribir .env - printf '%s\n' "${env_lines[@]}" > "$env_file" - chmod 600 "$env_file" - - # Agregar paths del installer automáticamente - cat >> "$env_file" << EOF - -# ── Generado por el installer ────────────────────────────────────────────── -HV_CORE_HOME=${HV_CORE_HOME} -INSTALL_DIR=${HV_REPO_DIR} -CORE_DIR=${HV_CORE_HOME} -EOF - - log_ok ".env escrito → ${env_file}" - echo "" - - # Preview ocultando secrets - printf " ${DIM}Preview:${RESET}\n" - while IFS= read -r ln; do - if [[ -z "$ln" || "$ln" == \#* ]]; then - printf " ${DIM}%s${RESET}\n" "$ln" - else - local pk pv - pk="${ln%%=*}" - pv="${ln#*=}" - echo "$pk" | grep -qiE '(secret|token|key|pass)' && pv="****" - printf " ${DIM}%s${RESET}=${CYAN}%s${RESET}\n" "$pk" "$pv" - fi - done < "$env_file" - echo "" -} - -# ─── Step 3: Python venv ────────────────────────────────────────────────────── -step_python_env() { - print_section "Python Environment" - log_step "Setting up virtualenv + dependencies" 3 $TOTAL_STEPS - - if ! command -v python3 &>/dev/null; then - log_error "python3 no encontrado. Instalalo primero." - exit 1 - fi - log_info "Python: $(python3 --version 2>&1)" - - # Crear venv en HV_VENV_DIR (separado del repo) - start_spinner "Creando virtual environment..." - python3 -m venv "$HV_VENV_DIR" - stop_spinner "Virtual environment creado → ${HV_VENV_DIR}" - - source "$HV_VENV_DIR/bin/activate" - - start_spinner "Actualizando pip..." - pip install --quiet --upgrade pip - stop_spinner "pip actualizado" - - local req_file="$HV_REPO_DIR/requirements.txt" - - if [ ! -f "$req_file" ]; then - log_warn "requirements.txt no encontrado — instalando dependencias base" - start_spinner "Instalando dependencias..." - pip install --quiet \ - python-dotenv \ - "cryptography~=46.0.3" \ - "dataclasses-json~=0.6.7" \ - "psutil~=7.1.3" \ - "pysocks~=1.7.1" \ - "requests~=2.32.5" \ - "pydantic==2.13.3" - stop_spinner "Dependencias instaladas" - return 0 - fi - - # Instalar todo de una vez (no paquete por paquete) - # pip resuelve dependencias mejor en un solo lote - log_info "Instalando desde requirements.txt..." - start_spinner "Instalando paquetes..." - if pip install --quiet -r "$req_file"; then - stop_spinner "Paquetes instalados" - else - stop_spinner - log_warn "pip reportó errores — intentando instalar de a uno para identificar el fallo" - local failed=() - while IFS= read -r pkg || [ -n "$pkg" ]; do - pkg="${pkg%%#*}" - pkg="${pkg// /}" - [[ -z "$pkg" ]] && continue - pip install --quiet "$pkg" 2>/dev/null || failed+=("$pkg") - done < "$req_file" - [ "${#failed[@]}" -gt 0 ] && log_warn "Fallaron: ${failed[*]}" - fi - - # Instalar el paquete en modo editable - start_spinner "Instalando paquete (editable)..." - pip install --quiet -e "$HV_REPO_DIR" --no-deps - stop_spinner "Paquete instalado en modo editable" -} - -# ─── Step 4: sing-box ───────────────────────────────────────────────────────── -step_singbox() { - print_section "sing-box" - log_step "Checking sing-box binary" 4 $TOTAL_STEPS - - local found="" - for candidate in /usr/bin/sing-box /usr/local/bin/sing-box /bin/sing-box; do - [ -x "$candidate" ] && found="$candidate" && break - done - [ -z "$found" ] && found=$(command -v sing-box 2>/dev/null || true) - - if [ -n "$found" ]; then - local ver - ver=$("$found" version 2>/dev/null | head -1 || echo "unknown") - log_ok "Encontrado: ${found} (${ver})" - if [ "$found" != "$SINGBOX_BIN" ]; then - sudo ln -sf "$found" "$SINGBOX_BIN" - log_ok "Symlink → ${SINGBOX_BIN}" - fi - else - log_info "sing-box no encontrado — instalando v${SINGBOX_VERSION}..." - local url="https://github.com/SagerNet/sing-box/releases/download/v${SINGBOX_VERSION}/sing-box-${SINGBOX_VERSION}-linux-amd64.tar.gz" - local tmp_tar="/tmp/sing-box-$$.tar.gz" - local tmp_dir="/tmp/sing-box-$$" - - start_spinner "Descargando sing-box v${SINGBOX_VERSION}..." - if wget -q --timeout=60 "$url" -O "$tmp_tar"; then - stop_spinner "Descarga completa" - else - stop_spinner - rm -f "$tmp_tar" - log_error "Descarga falló: ${url}" - exit 1 - fi - - start_spinner "Extrayendo..." - mkdir -p "$tmp_dir" - tar -xzf "$tmp_tar" -C "$tmp_dir" --strip-components=1 - stop_spinner "Extraído" - - sudo cp "$tmp_dir/sing-box" "$SINGBOX_BIN" - sudo chmod 755 "$SINGBOX_BIN" - rm -rf "$tmp_tar" "$tmp_dir" - log_ok "sing-box instalado: $($SINGBOX_BIN version | head -1)" - fi - - # setcap — sing-box crea TUN sin necesitar root completo - if command -v setcap &>/dev/null; then - sudo setcap cap_net_admin,cap_net_raw+ep "$SINGBOX_BIN" - log_ok "setcap cap_net_admin,cap_net_raw+ep → ${SINGBOX_BIN}" - else - log_warn "setcap no disponible — instalá libcap2-bin para hardening completo" - log_warn "sing-box necesitará sudo completo para TUN hasta que se instale" - fi - - log_ok "sing-box listo → ${SINGBOX_BIN}" -} - -# ─── Step 5: Wrapper ────────────────────────────────────────────────────────── -step_wrapper() { - print_section "Security Wrapper" - log_step "Installing hydraveil-singbox wrapper" 5 $TOTAL_STEPS - - [ -f "$WRAPPER_PATH" ] && sudo rm -f "$WRAPPER_PATH" && log_ok "Wrapper anterior eliminado" - - sudo tee "$WRAPPER_PATH" > /dev/null << WRAPPER -#!/bin/bash -# hydraveil-singbox — wrapper de privilegios para sing-box -# ───────────────────────────────────────────────────────── -# Generado por el installer. NO editar manualmente. -# Solo este binario tiene NOPASSWD en sudoers. -# -# Uso: -# sudo hydraveil-singbox start -# sudo hydraveil-singbox "" stop -# sudo hydraveil-singbox "" log -set -euo pipefail - -CONFIG="\${1:-}" -ACTION="\${2:-}" - -# ── Paths fijos (escritos al instalar, nunca buscados en runtime) ───────────── -SINGBOX_BIN="${SINGBOX_BIN}" -PID_FILE="${SINGBOX_PID_FILE}" -LOG_FILE="${SINGBOX_LOG_FILE}" -CONFIG_DIR="${HV_CONFIG_DIR}" -STOP_TIMEOUT=8 - -# ── Validar acción ──────────────────────────────────────────────────────────── -if [[ "\$ACTION" != "start" && "\$ACTION" != "stop" && "\$ACTION" != "log" ]]; then - echo "Error: acción inválida '\$ACTION'. Uso: start|stop|log" >&2 - exit 1 -fi - -# ── Helpers ─────────────────────────────────────────────────────────────────── -_pid_running() { - local pid="\$1" - [[ "\$pid" =~ ^[0-9]+$ ]] && kill -0 "\$pid" 2>/dev/null -} - -_delete_tun() { - if ip link show tun0 &>/dev/null 2>&1; then - ip link delete tun0 2>/dev/null || true - echo "[wrapper] tun0 eliminada" >> "\$LOG_FILE" 2>/dev/null || true - fi -} - -_kill_by_pidfile() { - [ -f "\$PID_FILE" ] || return 0 - - local pid - pid=\$(cat "\$PID_FILE" 2>/dev/null || echo "") - - if ! _pid_running "\$pid"; then - rm -f "\$PID_FILE" - return 0 - fi - - # SIGTERM → esperar → SIGKILL si no murió - kill -TERM "\$pid" 2>/dev/null || true - echo "[wrapper] SIGTERM → PID \$pid" >> "\$LOG_FILE" 2>/dev/null || true - - local elapsed=0 - while _pid_running "\$pid" && (( elapsed < STOP_TIMEOUT * 2 )); do - sleep 0.5 - (( elapsed++ )) || true - done - - if _pid_running "\$pid"; then - kill -KILL "\$pid" 2>/dev/null || true - echo "[wrapper] SIGKILL → PID \$pid (no respondió a SIGTERM)" >> "\$LOG_FILE" 2>/dev/null || true - sleep 0.3 - fi - - rm -f "\$PID_FILE" -} - -# ── log ─────────────────────────────────────────────────────────────────────── -if [[ "\$ACTION" == "log" ]]; then - [ -f "\$LOG_FILE" ] && tail -80 "\$LOG_FILE" || echo "Sin logs disponibles" - exit 0 -fi - -# ── stop ────────────────────────────────────────────────────────────────────── -if [[ "\$ACTION" == "stop" ]]; then - _kill_by_pidfile - _delete_tun - echo "[wrapper] sing-box detenido — \$(date '+%Y-%m-%d %H:%M:%S')" >> "\$LOG_FILE" 2>/dev/null || true - exit 0 -fi - -# ── start ───────────────────────────────────────────────────────────────────── - -# 1. Config requerido -if [[ -z "\$CONFIG" ]]; then - echo "Error: config requerido para start" >&2 - exit 1 -fi - -# 2. Existe y es legible -if [[ ! -f "\$CONFIG" ]]; then - echo "Error: config no encontrado: \$CONFIG" >&2 - exit 1 -fi -if [[ ! -r "\$CONFIG" ]]; then - echo "Error: config no legible: \$CONFIG" >&2 - exit 1 -fi - -# 3. Path dentro del directorio permitido (evita path traversal) -_real_config=\$(realpath "\$CONFIG") -_real_config_dir=\$(realpath "\$CONFIG_DIR") -if [[ "\$_real_config" != "\$_real_config_dir"/* ]]; then - echo "Error: config fuera del directorio permitido" >&2 - echo " Config: \$_real_config" >&2 - echo " Permitido: \$_real_config_dir" >&2 - exit 1 -fi - -# 4. JSON válido -if ! python3 -c "import json,sys; json.load(open(sys.argv[1]))" "\$CONFIG" 2>/dev/null; then - echo "Error: JSON inválido: \$CONFIG" >&2 - exit 1 -fi - -# 5. sing-box ejecutable -if [[ ! -x "\$SINGBOX_BIN" ]]; then - echo "Error: sing-box no encontrado: \$SINGBOX_BIN" >&2 - exit 1 -fi - -# 6. Parar instancia previa -_kill_by_pidfile -_delete_tun - -# 7. Directorios — solo mkdir/chmod, nunca tocar el log file existente -mkdir -p "\$(dirname "\$PID_FILE")" && chmod 700 "\$(dirname "\$PID_FILE")" -mkdir -p "\$(dirname "\$LOG_FILE")" && chmod 700 "\$(dirname "\$LOG_FILE")" -# NOTA: el log file fue creado por el installer con permisos del usuario. -# El wrapper solo appendea (>>) — nunca lo recrea ni cambia sus permisos. - -# 8. Lanzar sing-box -"\$SINGBOX_BIN" run -c "\$CONFIG" >> "\$LOG_FILE" 2>&1 & -_new_pid=\$! - -# 9. Verificar arranque inmediato -sleep 0.3 -if ! _pid_running "\$_new_pid"; then - echo "Error: sing-box terminó inmediatamente. Ver: \$LOG_FILE" >&2 - exit 1 -fi - -# 10. PID file -echo "\$_new_pid" > "\$PID_FILE" -chmod 600 "\$PID_FILE" - -echo "[wrapper] sing-box iniciado — PID \$_new_pid — \$(date '+%Y-%m-%d %H:%M:%S')" >> "\$LOG_FILE" -echo "[wrapper] config: \$_real_config" >> "\$LOG_FILE" -exit 0 -WRAPPER - - sudo chmod 755 "$WRAPPER_PATH" - log_ok "Wrapper instalado → ${WRAPPER_PATH}" -} - -# ─── Step 6: sudoers + directorios ──────────────────────────────────────────── -step_sudoers() { - print_section "Permissions" - log_step "Configurando sudoers + directorios" 6 $TOTAL_STEPS - - local _tmp_sudoers - _tmp_sudoers=$(mktemp) - - cat > "$_tmp_sudoers" << EOF -# hydraveil — generado por installer $(date '+%Y-%m-%d') -# NO editar manualmente -Defaults!${WRAPPER_PATH} !requiretty -${USER} ALL=(root) NOPASSWD: ${WRAPPER_PATH} -EOF - - # Validar con visudo ANTES de instalar — nunca instalar un sudoers roto - if ! sudo visudo -c -f "$_tmp_sudoers" &>/dev/null; then - rm -f "$_tmp_sudoers" - log_error "sudoers inválido — abortando para evitar lockout" - exit 1 - fi - - sudo cp "$_tmp_sudoers" "$SUDOERS_PATH" - sudo chmod 440 "$SUDOERS_PATH" - rm -f "$_tmp_sudoers" - log_ok "sudoers validado e instalado → ${SUDOERS_PATH}" - - # Directorios con permisos correctos - local -a _dirs=( - "$HV_CONFIG_DIR" - "$HV_RUN_DIR" - "$HV_LOG_DIR" - "$HV_DATA_DIR" - ) - for _dir in "${_dirs[@]}"; do - mkdir -p "$_dir" - chmod 700 "$_dir" - log_ok "Directorio → ${_dir}" - done - - # ── Crear log file con permisos del usuario ANTES de que root lo toque ──── - # Crítico: si root crea el archivo primero (via wrapper), Python no puede - # leerlo. _ensure_log_file() lo crea como usuario con chmod 600 ahora. - _ensure_log_file "$SINGBOX_LOG_FILE" - log_ok "Log file preparado con permisos de usuario → ${SINGBOX_LOG_FILE}" -} + source "$_step_path" +done \ No newline at end of file diff --git a/lib/steps/preflight.sh b/lib/steps/preflight.sh new file mode 100644 index 0000000..72747a3 --- /dev/null +++ b/lib/steps/preflight.sh @@ -0,0 +1,46 @@ +#!/bin/bash +# lib/steps/preflight.sh — verificaciones previas + +step_preflight() { + print_section "Pre-flight" + log_step "System checks" 1 $TOTAL_STEPS + + _preflight_user + _preflight_python + _preflight_git +} + +_preflight_user() { + log_info "Running as: ${USER} (uid=$(id -u))" + if [ "$(id -u)" -eq 0 ]; then + log_error "No correr el installer como root" + exit 1 + fi + log_ok "User OK" +} + +_preflight_python() { + local found="" + for candidate in python3.12 python3.13; do + if command -v "$candidate" &>/dev/null; then + found="$candidate" + break + fi + done + if [ -z "$found" ]; then + log_error "Se necesita Python 3.12 o 3.13" + log_info "Arch: sudo pacman -S python312" + log_info "Ubuntu: sudo apt install python3.12" + exit 1 + fi + PYTHON_BIN="$found" + log_ok "Python: $($PYTHON_BIN --version 2>&1)" +} + +_preflight_git() { + if ! command -v git &>/dev/null; then + log_error "git no encontrado" + exit 1 + fi + log_ok "git: $(git --version)" +} \ No newline at end of file diff --git a/lib/steps/python_env.sh b/lib/steps/python_env.sh new file mode 100644 index 0000000..ee51652 --- /dev/null +++ b/lib/steps/python_env.sh @@ -0,0 +1,69 @@ +#!/bin/bash +# lib/steps/python_env.sh — venv + pip install editable + +step_python_env() { + print_section "Python Environment" + log_step "Virtual environment + dependencies" 3 $TOTAL_STEPS + + _create_venv + _pip_install +} + +_create_venv() { + if [ -d "$HV_VENV" ]; then + log_warn "venv ya existe: ${HV_VENV}" + if ask_confirm "Recrear venv?"; then + rm -rf "$HV_VENV" + else + log_skip "venv existente conservado" + source "$HV_VENV/bin/activate" + return 0 + fi + fi + + start_spinner "Creando virtual environment (${PYTHON_BIN})..." + "$PYTHON_BIN" -m venv "$HV_VENV" + stop_spinner "venv creado → ${HV_VENV}" + + source "$HV_VENV/bin/activate" + + start_spinner "Actualizando pip..." + pip install --quiet --upgrade pip + stop_spinner "pip actualizado" +} + +_pip_install() { + source "$HV_VENV/bin/activate" + + # Fix: la GUI puede pedir core==2.3.4 pero core puede ser 2.3.x + if [ -f "$HV_GUI/pyproject.toml" ]; then + sed -i 's/sp-hydra-veil-core == [0-9.]*/sp-hydra-veil-core >= 2.3.0/' \ + "$HV_GUI/pyproject.toml" 2>/dev/null || true + fi + + # Orden de instalación: dependencias primero + for name in essentials core cli gui; do + local dir + case "$name" in + essentials) dir="$HV_ESSENTIALS" ;; + core) dir="$HV_CORE" ;; + cli) dir="$HV_CLI" ;; + gui) dir="$HV_GUI" ;; + esac + + if [ ! -d "$dir" ]; then + log_warn "${name} no encontrado en ${dir} — omitiendo" + continue + fi + + start_spinner "Instalando ${name} (editable)..." + if pip install --quiet -e "$dir" 2>/tmp/pip_err_${name}; then + stop_spinner "${name} instalado" + else + stop_spinner + log_error "pip install falló para ${name}:" + tail -5 /tmp/pip_err_${name} + exit 1 + fi + done +} \ No newline at end of file diff --git a/lib/steps/repos.sh b/lib/steps/repos.sh new file mode 100644 index 0000000..07710d1 --- /dev/null +++ b/lib/steps/repos.sh @@ -0,0 +1,96 @@ +#!/bin/bash +# lib/steps/repos.sh — clonar repositorios + generar .env + +step_repos() { + print_section "Repositories" + log_step "Cloning repositories & configuring .env" 2 $TOTAL_STEPS + + mkdir -p "$HV_BASE" + chmod 700 "$HV_BASE" + + _clone_repos + _setup_env +} + +_clone_repos() { + for name in essentials cli gui; do + local url="${REPO_URLS[$name]}" + local branch="${REPO_BRANCHES[$name]}" + local dest="${REPO_DIRS[$name]}" + + if [ -d "$dest/.git" ]; then + log_warn "Repo ya existe: ${dest}" + if ask_confirm "Eliminar y re-clonar ${name}?"; then + rm -rf "$dest" + else + log_skip "Re-clone omitido — usando ${dest}" + continue + fi + fi + + start_spinner "Clonando ${name} (branch: ${branch})..." + if git clone --branch "$branch" "$url" "$dest" &>/dev/null; then + stop_spinner "${name} clonado → ${dest}" + else + stop_spinner + log_error "git clone falló: ${url}" + exit 1 + fi + chmod 700 "$dest" + done +} + +_setup_env() { + local env_file="$HV_CORE/.env" + local example_file="$HV_CORE/.env.example" + + if [ ! -f "$example_file" ]; then + log_error ".env.example no encontrado: ${example_file}" + exit 1 + fi + + if [ -f "$env_file" ]; then + log_warn ".env ya existe — sobreescribiendo" + fi + + echo "" + printf " ${DIM}Completá las variables. ENTER = dejar vacío.${RESET}\n" + print_divider + echo "" + + # APP_ENV + select_option \ + "APP_ENV — entorno de la aplicación" \ + "local desarrollo / testing" \ + "production producción / dominio real" + + case "$SELECT_RESULT" in + 0) _app_env="local" ;; + 1) _app_env="production" ;; + *) _app_env="production" ;; + esac + + # SP_API_BASE_URL_PRODUCTION + echo "" + printf " ${BCYAN}┌─${RESET} ${BWHITE}SP_API_BASE_URL_PRODUCTION${RESET} ${DIM}e.g. https://fake.simplifiedprivacy.org/api/v1${RESET}\n" + printf " ${BCYAN}›${RESET} " + read -r _api_url < /dev/tty + + # Escribir .env + cat > "$env_file" << EOF +APP_ENV=${_app_env} +SP_API_BASE_URL_LOCAL=http://simplifiedprivacy.test/api/v1 +SP_API_BASE_URL_PRODUCTION=${_api_url} + +# ── Generado por el installer ───────────────────────────────────────────── +HV_CORE_HOME=${HV_CORE} +INSTALL_DIR=${HOME} +CORE_DIR=${HV_CORE} +EOF + + chmod 600 "$env_file" + log_ok "APP_ENV=${_app_env}" + [ -n "$_api_url" ] && log_ok "SP_API_BASE_URL_PRODUCTION=${_api_url}" || label_skip "SP_API_BASE_URL_PRODUCTION — vacío" + log_ok "INSTALL_DIR=${HOME}" + log_ok ".env escrito → ${env_file}" +} \ No newline at end of file diff --git a/lib/steps/singbox.sh b/lib/steps/singbox.sh new file mode 100644 index 0000000..07710d1 --- /dev/null +++ b/lib/steps/singbox.sh @@ -0,0 +1,96 @@ +#!/bin/bash +# lib/steps/repos.sh — clonar repositorios + generar .env + +step_repos() { + print_section "Repositories" + log_step "Cloning repositories & configuring .env" 2 $TOTAL_STEPS + + mkdir -p "$HV_BASE" + chmod 700 "$HV_BASE" + + _clone_repos + _setup_env +} + +_clone_repos() { + for name in essentials cli gui; do + local url="${REPO_URLS[$name]}" + local branch="${REPO_BRANCHES[$name]}" + local dest="${REPO_DIRS[$name]}" + + if [ -d "$dest/.git" ]; then + log_warn "Repo ya existe: ${dest}" + if ask_confirm "Eliminar y re-clonar ${name}?"; then + rm -rf "$dest" + else + log_skip "Re-clone omitido — usando ${dest}" + continue + fi + fi + + start_spinner "Clonando ${name} (branch: ${branch})..." + if git clone --branch "$branch" "$url" "$dest" &>/dev/null; then + stop_spinner "${name} clonado → ${dest}" + else + stop_spinner + log_error "git clone falló: ${url}" + exit 1 + fi + chmod 700 "$dest" + done +} + +_setup_env() { + local env_file="$HV_CORE/.env" + local example_file="$HV_CORE/.env.example" + + if [ ! -f "$example_file" ]; then + log_error ".env.example no encontrado: ${example_file}" + exit 1 + fi + + if [ -f "$env_file" ]; then + log_warn ".env ya existe — sobreescribiendo" + fi + + echo "" + printf " ${DIM}Completá las variables. ENTER = dejar vacío.${RESET}\n" + print_divider + echo "" + + # APP_ENV + select_option \ + "APP_ENV — entorno de la aplicación" \ + "local desarrollo / testing" \ + "production producción / dominio real" + + case "$SELECT_RESULT" in + 0) _app_env="local" ;; + 1) _app_env="production" ;; + *) _app_env="production" ;; + esac + + # SP_API_BASE_URL_PRODUCTION + echo "" + printf " ${BCYAN}┌─${RESET} ${BWHITE}SP_API_BASE_URL_PRODUCTION${RESET} ${DIM}e.g. https://fake.simplifiedprivacy.org/api/v1${RESET}\n" + printf " ${BCYAN}›${RESET} " + read -r _api_url < /dev/tty + + # Escribir .env + cat > "$env_file" << EOF +APP_ENV=${_app_env} +SP_API_BASE_URL_LOCAL=http://simplifiedprivacy.test/api/v1 +SP_API_BASE_URL_PRODUCTION=${_api_url} + +# ── Generado por el installer ───────────────────────────────────────────── +HV_CORE_HOME=${HV_CORE} +INSTALL_DIR=${HOME} +CORE_DIR=${HV_CORE} +EOF + + chmod 600 "$env_file" + log_ok "APP_ENV=${_app_env}" + [ -n "$_api_url" ] && log_ok "SP_API_BASE_URL_PRODUCTION=${_api_url}" || label_skip "SP_API_BASE_URL_PRODUCTION — vacío" + log_ok "INSTALL_DIR=${HOME}" + log_ok ".env escrito → ${env_file}" +} \ No newline at end of file diff --git a/lib/steps/sudoers.sh b/lib/steps/sudoers.sh new file mode 100644 index 0000000..5753297 --- /dev/null +++ b/lib/steps/sudoers.sh @@ -0,0 +1,69 @@ +#!/bin/bash +# lib/steps/sudoers.sh — sudoers + directorios XDG + +step_sudoers() { + print_section "Permissions & Directories" + log_step "sudoers + XDG directories" 6 $TOTAL_STEPS + + _setup_sudoers + _create_directories +} + +_setup_sudoers() { + local tmp + tmp=$(mktemp) + + cat > "$tmp" << EOF +# hydraveil — generado por installer $(date '+%Y-%m-%d') +# NO editar manualmente +Defaults!${SINGBOX_WRAPPER} !requiretty +${USER} ALL=(root) NOPASSWD: ${SINGBOX_WRAPPER} +EOF + + if ! sudo visudo -c -f "$tmp" &>/dev/null; then + rm -f "$tmp" + log_error "sudoers inválido — abortando para evitar lockout" + exit 1 + fi + + sudo cp "$tmp" "$SUDOERS_PATH" + sudo chmod 440 "$SUDOERS_PATH" + rm -f "$tmp" + log_ok "sudoers instalado → ${SUDOERS_PATH}" +} + +_create_directories() { + local -a dirs=( + # XDG data + "$HV_DATA_DIR/configs" + "$HV_DATA_DIR/runtime" + "$HV_DATA_DIR/applications" + "$HV_DATA_DIR/profiles" + "$HV_DATA_DIR/ticket_data" + "$HV_DATA_DIR/incidents" + # XDG config + "$HV_CONFIG_DIR/profiles" + "$HV_CONFIG_DIR/ticketing" + # XDG state + "$HV_STATE_DIR/sessions" + "$HV_STATE_DIR/tor" + # XDG cache + "$HV_CACHE_DIR" + ) + + for dir in "${dirs[@]}"; do + mkdir -p "$dir" + chmod 700 "$dir" + log_ok "→ ${dir}" + done + + # Log file con permisos de usuario ANTES de que el wrapper lo toque + local log_file="$SINGBOX_LOG_FILE" + if [ ! -f "$log_file" ]; then + touch "$log_file" + chmod 600 "$log_file" + log_ok "Log file creado → ${log_file}" + else + log_ok "Log file ya existe → ${log_file}" + fi +} \ No newline at end of file diff --git a/lib/steps/wrapper.sh b/lib/steps/wrapper.sh new file mode 100644 index 0000000..0ca3fb0 --- /dev/null +++ b/lib/steps/wrapper.sh @@ -0,0 +1,140 @@ +#!/bin/bash +# lib/steps/wrapper.sh — generar wrapper de privilegios para sing-box + +step_wrapper() { + print_section "Security Wrapper" + log_step "Installing hydraveil-singbox wrapper" 5 $TOTAL_STEPS + + [ -f "$SINGBOX_WRAPPER" ] && sudo rm -f "$SINGBOX_WRAPPER" && log_ok "Wrapper anterior eliminado" + + sudo tee "$SINGBOX_WRAPPER" > /dev/null << WRAPPER +#!/bin/bash +# hydraveil-singbox — wrapper de privilegios para sing-box +# Generado por el installer. NO editar manualmente. +# Solo este binario tiene NOPASSWD en sudoers. +# +# Uso: +# sudo hydraveil-singbox start +# sudo hydraveil-singbox "" stop +# sudo hydraveil-singbox "" log +set -euo pipefail + +CONFIG="\${1:-}" +ACTION="\${2:-}" + +# ── Paths fijos (rutas XDG correctas — INSTALL_DIR=\$HOME) ──────────────────── +SINGBOX_BIN="${SINGBOX_BIN}" +PID_FILE="${SINGBOX_PID_FILE}" +LOG_FILE="${SINGBOX_LOG_FILE}" +CONFIG_DIR="${SINGBOX_CONFIG_DIR}" +STOP_TIMEOUT=8 + +# ── Validar acción ──────────────────────────────────────────────────────────── +if [[ "\$ACTION" != "start" && "\$ACTION" != "stop" && "\$ACTION" != "log" ]]; then + echo "Error: acción inválida '\$ACTION'. Uso: start|stop|log" >&2 + exit 1 +fi + +# ── Helpers ─────────────────────────────────────────────────────────────────── +_pid_running() { + local pid="\$1" + [[ "\$pid" =~ ^[0-9]+$ ]] && kill -0 "\$pid" 2>/dev/null +} + +_delete_tun() { + if ip link show tun0 &>/dev/null 2>&1; then + ip link delete tun0 2>/dev/null || true + echo "[wrapper] tun0 eliminada" >> "\$LOG_FILE" 2>/dev/null || true + fi +} + +_kill_by_pidfile() { + [ -f "\$PID_FILE" ] || return 0 + local pid + pid=\$(cat "\$PID_FILE" 2>/dev/null || echo "") + if ! _pid_running "\$pid"; then + rm -f "\$PID_FILE" + return 0 + fi + kill -TERM "\$pid" 2>/dev/null || true + echo "[wrapper] SIGTERM → PID \$pid" >> "\$LOG_FILE" 2>/dev/null || true + local elapsed=0 + while _pid_running "\$pid" && (( elapsed < STOP_TIMEOUT * 2 )); do + sleep 0.5 + (( elapsed++ )) || true + done + if _pid_running "\$pid"; then + kill -KILL "\$pid" 2>/dev/null || true + echo "[wrapper] SIGKILL → PID \$pid" >> "\$LOG_FILE" 2>/dev/null || true + sleep 0.3 + fi + rm -f "\$PID_FILE" +} + +# ── log ─────────────────────────────────────────────────────────────────────── +if [[ "\$ACTION" == "log" ]]; then + [ -f "\$LOG_FILE" ] && tail -80 "\$LOG_FILE" || echo "Sin logs disponibles" + exit 0 +fi + +# ── stop ────────────────────────────────────────────────────────────────────── +if [[ "\$ACTION" == "stop" ]]; then + _kill_by_pidfile + _delete_tun + echo "[wrapper] sing-box detenido — \$(date '+%Y-%m-%d %H:%M:%S')" >> "\$LOG_FILE" 2>/dev/null || true + exit 0 +fi + +# ── start ───────────────────────────────────────────────────────────────────── +[[ -z "\$CONFIG" ]] && { echo "Error: config requerido para start" >&2; exit 1; } +[[ ! -f "\$CONFIG" ]] && { echo "Error: config no encontrado: \$CONFIG" >&2; exit 1; } +[[ ! -r "\$CONFIG" ]] && { echo "Error: config no legible: \$CONFIG" >&2; exit 1; } + +_real_config=\$(realpath "\$CONFIG") +_real_config_dir=\$(realpath "\$CONFIG_DIR") +if [[ "\$_real_config" != "\$_real_config_dir"/* ]]; then + echo "Error: config fuera del directorio permitido" >&2 + echo " Config: \$_real_config" >&2 + echo " Permitido: \$_real_config_dir" >&2 + exit 1 +fi + +if ! python3 -c "import json,sys; json.load(open(sys.argv[1]))" "\$CONFIG" 2>/dev/null; then + echo "Error: JSON inválido: \$CONFIG" >&2 + exit 1 +fi + +[[ ! -x "\$SINGBOX_BIN" ]] && { echo "Error: sing-box no encontrado: \$SINGBOX_BIN" >&2; exit 1; } + +_kill_by_pidfile +_delete_tun + +mkdir -p "\$(dirname "\$PID_FILE")" && chmod 700 "\$(dirname "\$PID_FILE")" +mkdir -p "\$(dirname "\$LOG_FILE")" && chmod 700 "\$(dirname "\$LOG_FILE")" + +"\$SINGBOX_BIN" run -c "\$CONFIG" >> "\$LOG_FILE" 2>&1 & +_new_pid=\$! + +sleep 0.3 +if ! _pid_running "\$_new_pid"; then + echo "Error: sing-box terminó inmediatamente. Ver: \$LOG_FILE" >&2 + exit 1 +fi + +echo "\$_new_pid" > "\$PID_FILE" +chmod 600 "\$PID_FILE" +echo "[wrapper] sing-box iniciado — PID \$_new_pid — \$(date '+%Y-%m-%d %H:%M:%S')" >> "\$LOG_FILE" +echo "[wrapper] config: \$_real_config" >> "\$LOG_FILE" +exit 0 +WRAPPER + + sudo chmod 755 "$SINGBOX_WRAPPER" + log_ok "Wrapper instalado → ${SINGBOX_WRAPPER}" + + # Verificar que el wrapper funciona + if sudo "$SINGBOX_WRAPPER" "" stop &>/dev/null; then + log_ok "Wrapper verificado (stop OK)" + else + log_warn "Wrapper instalado pero verificación falló — revisar manualmente" + fi +} \ No newline at end of file diff --git a/logs/hydra-veil/singbox.log b/logs/hydra-veil/singbox.log new file mode 100644 index 0000000..b946dfe --- /dev/null +++ b/logs/hydra-veil/singbox.log @@ -0,0 +1,2 @@ +[wrapper] sing-box detenido — 2026-05-30 07:44:53 +[wrapper] sing-box detenido — 2026-05-30 07:44:53