core-laravel-proxyTEST
This commit is contained in:
parent
75d026651a
commit
a3cf476af4
24 changed files with 628 additions and 41 deletions
6
.env
Normal file
6
.env
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
# Environment: local | production
|
||||||
|
APP_ENV=local
|
||||||
|
|
||||||
|
# API URLs
|
||||||
|
SP_API_BASE_URL_LOCAL=http://simplifiedprivacy.test/api/v1
|
||||||
|
SP_API_BASE_URL_PRODUCTION=https://api.hydraveil.net/api/v1
|
||||||
|
|
@ -1,57 +1,49 @@
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Final
|
from typing import Final
|
||||||
|
from dotenv import load_dotenv
|
||||||
import os
|
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)
|
@dataclass(frozen=True)
|
||||||
class Constants:
|
class Constants:
|
||||||
# ticketing group:
|
|
||||||
TICKET_API_BASE_URL: Final[str] = os.environ.get(
|
TICKET_API_BASE_URL: Final[str] = os.environ.get(
|
||||||
"TICKET_API_BASE_URL", "https://ticket.hydraveil.net"
|
"TICKET_API_BASE_URL", "https://ticket.hydraveil.net"
|
||||||
)
|
)
|
||||||
|
SP_API_BASE_URL: Final[str] = _sp_api_base_url
|
||||||
SP_API_BASE_URL: Final[str] = os.environ.get('SP_API_BASE_URL', 'https://api.hydraveil.net/api/v1')
|
PING_URL: Final[str] = os.environ.get('PING_URL', f'{_sp_api_base_url}/health')
|
||||||
PING_URL: Final[str] = os.environ.get('PING_URL', 'https://api.hydraveil.net/api/v1/health')
|
|
||||||
|
|
||||||
CONNECTION_RETRY_INTERVAL: Final[int] = int(os.environ.get('CONNECTION_RETRY_INTERVAL', '5'))
|
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'))
|
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_PATH: Final[str] = os.environ.get('HV_CLIENT_PATH')
|
||||||
HV_CLIENT_VERSION_NUMBER: Final[str] = os.environ.get('HV_CLIENT_VERSION_NUMBER')
|
HV_CLIENT_VERSION_NUMBER: Final[str] = os.environ.get('HV_CLIENT_VERSION_NUMBER')
|
||||||
|
|
||||||
HOME: Final[str] = os.path.expanduser('~')
|
HOME: Final[str] = os.path.expanduser('~')
|
||||||
|
|
||||||
SYSTEM_CONFIG_PATH: Final[str] = '/etc'
|
SYSTEM_CONFIG_PATH: Final[str] = '/etc'
|
||||||
|
|
||||||
CACHE_HOME: Final[str] = os.environ.get('XDG_CACHE_HOME', os.path.join(HOME, '.cache'))
|
CACHE_HOME: Final[str] = os.environ.get('XDG_CACHE_HOME', os.path.join(HOME, '.cache'))
|
||||||
CONFIG_HOME: Final[str] = os.environ.get('XDG_CONFIG_HOME', os.path.join(HOME, '.config'))
|
CONFIG_HOME: Final[str] = os.environ.get('XDG_CONFIG_HOME', os.path.join(HOME, '.config'))
|
||||||
DATA_HOME: Final[str] = os.environ.get('XDG_DATA_HOME', os.path.join(HOME, '.local/share'))
|
DATA_HOME: Final[str] = os.environ.get('XDG_DATA_HOME', os.path.join(HOME, '.local/share'))
|
||||||
STATE_HOME: Final[str] = os.environ.get('XDG_STATE_HOME', os.path.join(HOME, '.local/state'))
|
STATE_HOME: Final[str] = os.environ.get('XDG_STATE_HOME', os.path.join(HOME, '.local/state'))
|
||||||
|
|
||||||
HV_SYSTEM_CONFIG_PATH: Final[str] = f'{SYSTEM_CONFIG_PATH}/hydra-veil'
|
HV_SYSTEM_CONFIG_PATH: Final[str] = f'{SYSTEM_CONFIG_PATH}/hydra-veil'
|
||||||
|
|
||||||
HV_CACHE_HOME: Final[str] = f'{CACHE_HOME}/hydra-veil'
|
HV_CACHE_HOME: Final[str] = f'{CACHE_HOME}/hydra-veil'
|
||||||
HV_CONFIG_HOME: Final[str] = f'{CONFIG_HOME}/hydra-veil'
|
HV_CONFIG_HOME: Final[str] = f'{CONFIG_HOME}/hydra-veil'
|
||||||
HV_DATA_HOME: Final[str] = f'{DATA_HOME}/hydra-veil'
|
HV_DATA_HOME: Final[str] = f'{DATA_HOME}/hydra-veil'
|
||||||
HV_STATE_HOME: Final[str] = f'{STATE_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_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_CONFIG_HOME: Final[str] = f'{HV_CONFIG_HOME}/profiles'
|
||||||
HV_PROFILE_DATA_HOME: Final[str] = f'{HV_DATA_HOME}/profiles'
|
HV_PROFILE_DATA_HOME: Final[str] = f'{HV_DATA_HOME}/profiles'
|
||||||
|
|
||||||
# ticketing group:
|
|
||||||
HV_TICKETING_CONFIG_HOME: Final[str] = f"{HV_CONFIG_HOME}/ticketing"
|
HV_TICKETING_CONFIG_HOME: Final[str] = f"{HV_CONFIG_HOME}/ticketing"
|
||||||
HV_TICKETING_DATA_HOME: Final[str] = f"{HV_DATA_HOME}/ticket_data"
|
HV_TICKETING_DATA_HOME: Final[str] = f"{HV_DATA_HOME}/ticket_data"
|
||||||
|
|
||||||
HV_APPLICATION_DATA_HOME: Final[str] = f'{HV_DATA_HOME}/applications'
|
HV_APPLICATION_DATA_HOME: Final[str] = f'{HV_DATA_HOME}/applications'
|
||||||
HV_INCIDENT_DATA_HOME: Final[str] = f'{HV_DATA_HOME}/incidents'
|
HV_INCIDENT_DATA_HOME: Final[str] = f'{HV_DATA_HOME}/incidents'
|
||||||
HV_RUNTIME_DATA_HOME: Final[str] = f'{HV_DATA_HOME}/runtime'
|
HV_RUNTIME_DATA_HOME: Final[str] = f'{HV_DATA_HOME}/runtime'
|
||||||
|
|
||||||
HV_STORAGE_DATABASE_PATH: Final[str] = f'{HV_DATA_HOME}/storage.db'
|
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_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_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_SESSION_STATE_HOME: Final[str] = f'{HV_STATE_HOME}/sessions'
|
||||||
HV_TOR_STATE_HOME: Final[str] = f'{HV_STATE_HOME}/tor'
|
HV_TOR_STATE_HOME: Final[str] = f'{HV_STATE_HOME}/tor'
|
||||||
|
|
@ -25,7 +25,7 @@ class ConfigurationController:
|
||||||
|
|
||||||
configuration = ConfigurationController.get()
|
configuration = ConfigurationController.get()
|
||||||
|
|
||||||
if configuration is None or configuration.connection not in ('system', 'tor'):
|
if configuration is None or configuration.connection not in ('system', 'tor', 'vless', 'hysteria2'):
|
||||||
raise UnknownConnectionTypeError('The preferred connection type could not be determined.')
|
raise UnknownConnectionTypeError('The preferred connection type could not be determined.')
|
||||||
|
|
||||||
return configuration.connection
|
return configuration.connection
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import time
|
import time
|
||||||
|
from core.models.OperatorProxySession import OperatorProxySession
|
||||||
|
|
||||||
|
|
||||||
class ConnectionController:
|
class ConnectionController:
|
||||||
|
|
@ -65,6 +66,32 @@ class ConnectionController:
|
||||||
|
|
||||||
if connection.needs_wireguard_configuration() and not profile.has_wireguard_configuration():
|
if connection.needs_wireguard_configuration() and not profile.has_wireguard_configuration():
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if connection.needs_operator_proxy():
|
||||||
|
|
||||||
|
if profile.has_subscription():
|
||||||
|
|
||||||
|
if not profile.subscription.has_been_activated():
|
||||||
|
ProfileController.activate_subscription(profile, connection_observer=connection_observer)
|
||||||
|
|
||||||
|
operator_proxy_session = ConnectionController.with_preferred_connection(
|
||||||
|
profile.subscription.billing_code,
|
||||||
|
profile.subscription.operator_id,
|
||||||
|
connection.get_protocol(),
|
||||||
|
task=WebServiceApiService.post_operator_proxy,
|
||||||
|
connection_observer=connection_observer
|
||||||
|
)
|
||||||
|
|
||||||
|
if operator_proxy_session is None:
|
||||||
|
raise InvalidSubscriptionError()
|
||||||
|
|
||||||
|
profile.attach_operator_proxy_session(operator_proxy_session)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise MissingSubscriptionError()
|
||||||
|
|
||||||
if profile.has_subscription():
|
if profile.has_subscription():
|
||||||
|
|
||||||
if not profile.subscription.has_been_activated():
|
if not profile.subscription.has_been_activated():
|
||||||
|
|
@ -150,6 +177,24 @@ class ConnectionController:
|
||||||
ConnectionController.establish_wireguard_session_connection(profile, session_directory, port_number)
|
ConnectionController.establish_wireguard_session_connection(profile, session_directory, port_number)
|
||||||
session_state.network_port_numbers.wireguard.append(port_number)
|
session_state.network_port_numbers.wireguard.append(port_number)
|
||||||
|
|
||||||
|
elif profile.connection.code in ('vless', 'hysteria2'):
|
||||||
|
|
||||||
|
if not profile.has_operator_proxy_session():
|
||||||
|
raise MissingSubscriptionError()
|
||||||
|
|
||||||
|
operator_proxy_session = profile.get_operator_proxy_session()
|
||||||
|
port_number = ConnectionService.get_random_available_port_number()
|
||||||
|
|
||||||
|
if profile.connection.code == 'vless':
|
||||||
|
from core.controllers.encrypted_proxy.VlessController import VlessController
|
||||||
|
VlessController.enable(operator_proxy_session, port_number)
|
||||||
|
session_state.network_port_numbers.vless.append(port_number)
|
||||||
|
|
||||||
|
elif profile.connection.code == 'hysteria2':
|
||||||
|
from core.controllers.encrypted_proxy.HysteriaController import HysteriaController
|
||||||
|
HysteriaController.enable(operator_proxy_session, port_number)
|
||||||
|
session_state.network_port_numbers.hysteria2.append(port_number)
|
||||||
|
|
||||||
if profile.connection.masked:
|
if profile.connection.masked:
|
||||||
|
|
||||||
while proxy_port_number is None or proxy_port_number == port_number:
|
while proxy_port_number is None or proxy_port_number == port_number:
|
||||||
|
|
@ -164,7 +209,6 @@ class ConnectionController:
|
||||||
SessionStateController.update_or_create(session_state)
|
SessionStateController.update_or_create(session_state)
|
||||||
|
|
||||||
return proxy_port_number or port_number
|
return proxy_port_number or port_number
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def establish_system_connection(profile: SystemProfile, ignore: tuple[type[Exception]] = (), connection_observer: Optional[ConnectionObserver] = None):
|
def establish_system_connection(profile: SystemProfile, ignore: tuple[type[Exception]] = (), connection_observer: Optional[ConnectionObserver] = None):
|
||||||
|
|
||||||
|
|
|
||||||
17
core/controllers/encrypted_proxy/DisableController.py
Normal file
17
core/controllers/encrypted_proxy/DisableController.py
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
from pathlib import Path
|
||||||
|
from core.services.encrypted_proxy.disable_service import disable_proxy
|
||||||
|
|
||||||
|
|
||||||
|
class DisableController:
|
||||||
|
def __init__(self, tmp_dir: Path, wrapper: str, unit: str):
|
||||||
|
self.tmp_dir = tmp_dir
|
||||||
|
self.wrapper = wrapper
|
||||||
|
self.unit = unit
|
||||||
|
|
||||||
|
def disable(self, observer=None) -> bool:
|
||||||
|
return disable_proxy(
|
||||||
|
tmp_dir=self.tmp_dir,
|
||||||
|
wrapper=self.wrapper,
|
||||||
|
unit=self.unit,
|
||||||
|
observer=observer,
|
||||||
|
)
|
||||||
30
core/controllers/encrypted_proxy/HysteriaController.py
Normal file
30
core/controllers/encrypted_proxy/HysteriaController.py
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
from pathlib import Path
|
||||||
|
from core.services.encrypted_proxy.hysteria_service import enable_hysteria, disable_hysteria
|
||||||
|
|
||||||
|
|
||||||
|
class HysteriaController:
|
||||||
|
def __init__(self, config_dir: Path, socks5_port: int):
|
||||||
|
self.config_dir = config_dir
|
||||||
|
self.socks5_port = socks5_port
|
||||||
|
|
||||||
|
def enable(self, username: str, password: str,
|
||||||
|
server_host: str, observer=None) -> bool:
|
||||||
|
if not username or not isinstance(username, str):
|
||||||
|
if observer:
|
||||||
|
observer.notify("error", "Invalid username")
|
||||||
|
return False
|
||||||
|
if not password or not server_host:
|
||||||
|
if observer:
|
||||||
|
observer.notify("error", "Missing password or server_host")
|
||||||
|
return False
|
||||||
|
return enable_hysteria(
|
||||||
|
username=username,
|
||||||
|
password=password,
|
||||||
|
server_host=server_host,
|
||||||
|
config_dir=self.config_dir,
|
||||||
|
socks5_port=self.socks5_port,
|
||||||
|
observer=observer,
|
||||||
|
)
|
||||||
|
|
||||||
|
def disable(self, observer=None) -> bool:
|
||||||
|
return disable_hysteria(observer=observer)
|
||||||
28
core/controllers/encrypted_proxy/VlessController.py
Normal file
28
core/controllers/encrypted_proxy/VlessController.py
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
from pathlib import Path
|
||||||
|
from core.services.encrypted_proxy.vless_service import enable_vless, disable_vless
|
||||||
|
|
||||||
|
|
||||||
|
class VlessController:
|
||||||
|
def __init__(self, config_dir: Path, socks5_port: int):
|
||||||
|
self.config_dir = config_dir
|
||||||
|
self.socks5_port = socks5_port
|
||||||
|
|
||||||
|
def enable(self, vless_link: str, username: str, observer=None) -> bool:
|
||||||
|
if not username or not isinstance(username, str):
|
||||||
|
if observer:
|
||||||
|
observer.notify("error", "Invalid username")
|
||||||
|
return False
|
||||||
|
if not vless_link or not vless_link.startswith("vless://"):
|
||||||
|
if observer:
|
||||||
|
observer.notify("error", "Invalid vless link")
|
||||||
|
return False
|
||||||
|
return enable_vless(
|
||||||
|
vless_link=vless_link,
|
||||||
|
username=username,
|
||||||
|
config_dir=self.config_dir,
|
||||||
|
socks5_port=self.socks5_port,
|
||||||
|
observer=observer,
|
||||||
|
)
|
||||||
|
|
||||||
|
def disable(self, observer=None) -> bool:
|
||||||
|
return disable_vless(observer=observer)
|
||||||
0
core/controllers/encrypted_proxy/__init__.py
Normal file
0
core/controllers/encrypted_proxy/__init__.py
Normal file
|
|
@ -15,3 +15,5 @@ class BaseConnection:
|
||||||
|
|
||||||
def is_system_connection(self):
|
def is_system_connection(self):
|
||||||
return type(self).__name__ == 'SystemConnection'
|
return type(self).__name__ == 'SystemConnection'
|
||||||
|
def needs_operator_proxy(self):
|
||||||
|
return False
|
||||||
|
|
@ -6,6 +6,7 @@ _table_name: str = 'operators'
|
||||||
_table_definition: str = """
|
_table_definition: str = """
|
||||||
'id' int UNIQUE,
|
'id' int UNIQUE,
|
||||||
'name' varchar,
|
'name' varchar,
|
||||||
|
'type' varchar,
|
||||||
'public_key' varchar,
|
'public_key' varchar,
|
||||||
'nostr_public_key' varchar,
|
'nostr_public_key' varchar,
|
||||||
'nostr_profile_reference' varchar,
|
'nostr_profile_reference' varchar,
|
||||||
|
|
@ -17,11 +18,18 @@ _table_definition: str = """
|
||||||
class Operator(Model):
|
class Operator(Model):
|
||||||
id: int
|
id: int
|
||||||
name: str
|
name: str
|
||||||
|
type: str
|
||||||
public_key: str
|
public_key: str
|
||||||
nostr_public_key: str
|
nostr_public_key: str
|
||||||
nostr_profile_reference: str
|
nostr_profile_reference: str
|
||||||
nostr_attestation_event_reference: str
|
nostr_attestation_event_reference: str
|
||||||
|
|
||||||
|
def is_external(self) -> bool:
|
||||||
|
return self.type == 'external'
|
||||||
|
|
||||||
|
def is_internal(self) -> bool:
|
||||||
|
return self.type == 'internal'
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def find_by_id(id: int):
|
def find_by_id(id: int):
|
||||||
Model._create_table_if_not_exists(table_name=_table_name, table_definition=_table_definition)
|
Model._create_table_if_not_exists(table_name=_table_name, table_definition=_table_definition)
|
||||||
|
|
@ -44,7 +52,7 @@ class Operator(Model):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def save_many(operators):
|
def save_many(operators):
|
||||||
Model._create_table_if_not_exists(table_name=_table_name, table_definition=_table_definition)
|
Model._create_table_if_not_exists(table_name=_table_name, table_definition=_table_definition)
|
||||||
Model._insert_many('INSERT INTO operators VALUES(?, ?, ?, ?, ?, ?)', Operator.tuple_factory, operators)
|
Model._insert_many('INSERT INTO operators VALUES(?, ?, ?, ?, ?, ?, ?)', Operator.tuple_factory, operators)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def factory(cursor, row):
|
def factory(cursor, row):
|
||||||
|
|
@ -53,4 +61,4 @@ class Operator(Model):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def tuple_factory(operator):
|
def tuple_factory(operator):
|
||||||
return operator.id, operator.name, operator.public_key, operator.nostr_public_key, operator.nostr_profile_reference, operator.nostr_attestation_event_reference
|
return operator.id, operator.name, operator.type, operator.public_key, operator.nostr_public_key, operator.nostr_profile_reference, operator.nostr_attestation_event_reference
|
||||||
16
core/models/OperatorProxySession.py
Normal file
16
core/models/OperatorProxySession.py
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from dataclasses_json import dataclass_json
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass_json
|
||||||
|
@dataclass
|
||||||
|
class OperatorProxySession:
|
||||||
|
id: int
|
||||||
|
type: str
|
||||||
|
username: Optional[str]
|
||||||
|
password: Optional[str]
|
||||||
|
links: Optional[list]
|
||||||
|
subscription_url: Optional[str]
|
||||||
|
operator_id: int
|
||||||
|
operator_name: str
|
||||||
|
|
@ -10,6 +10,13 @@ import dataclasses_json
|
||||||
@dataclass
|
@dataclass
|
||||||
class Subscription:
|
class Subscription:
|
||||||
billing_code: str
|
billing_code: str
|
||||||
|
operator_id: Optional[int] = field(
|
||||||
|
default=None,
|
||||||
|
metadata=config(
|
||||||
|
undefined=dataclasses_json.Undefined.EXCLUDE,
|
||||||
|
exclude=lambda value: value is None
|
||||||
|
)
|
||||||
|
)
|
||||||
expires_at: Optional[datetime] = field(
|
expires_at: Optional[datetime] = field(
|
||||||
default=None,
|
default=None,
|
||||||
metadata=config(
|
metadata=config(
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,13 @@ class NetworkPortNumbers:
|
||||||
proxy: list[int] = field(default_factory=list)
|
proxy: list[int] = field(default_factory=list)
|
||||||
wireguard: list[int] = field(default_factory=list)
|
wireguard: list[int] = field(default_factory=list)
|
||||||
tor: list[int] = field(default_factory=list)
|
tor: list[int] = field(default_factory=list)
|
||||||
|
vless: list[int] = field(default_factory=list)
|
||||||
|
hysteria2: list[int] = field(default_factory=list)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def all(self):
|
def all(self):
|
||||||
return self.proxy + self.wireguard + self.tor
|
return self.proxy + self.wireguard + self.tor + self.vless + self.hysteria2
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def isolated(self):
|
def isolated(self):
|
||||||
return self.proxy + self.wireguard
|
return self.proxy + self.wireguard + self.vless + self.hysteria2
|
||||||
|
|
@ -1,14 +1,12 @@
|
||||||
from core.models.BaseConnection import BaseConnection
|
from core.models.BaseConnection import BaseConnection
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class SessionConnection(BaseConnection):
|
class SessionConnection(BaseConnection):
|
||||||
masked: bool = False
|
masked: bool = False
|
||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
|
if self.code not in ('system', 'tor', 'wireguard', 'vless', 'hysteria2'):
|
||||||
if self.code not in ('system', 'tor', 'wireguard'):
|
|
||||||
raise ValueError('Invalid connection code.')
|
raise ValueError('Invalid connection code.')
|
||||||
|
|
||||||
def is_unprotected(self):
|
def is_unprotected(self):
|
||||||
|
|
@ -16,3 +14,9 @@ class SessionConnection(BaseConnection):
|
||||||
|
|
||||||
def needs_proxy_configuration(self):
|
def needs_proxy_configuration(self):
|
||||||
return self.masked is True
|
return self.masked is True
|
||||||
|
|
||||||
|
def needs_operator_proxy(self):
|
||||||
|
return self.code in ('vless', 'hysteria2')
|
||||||
|
|
||||||
|
def get_protocol(self):
|
||||||
|
return self.code if self.needs_operator_proxy() else None
|
||||||
|
|
@ -116,3 +116,34 @@ class SessionProfile(BaseProfile):
|
||||||
|
|
||||||
def __delete_wireguard_configuration(self):
|
def __delete_wireguard_configuration(self):
|
||||||
Path(self.get_wireguard_configuration_path()).unlink(missing_ok=True)
|
Path(self.get_wireguard_configuration_path()).unlink(missing_ok=True)
|
||||||
|
def attach_operator_proxy_session(self, operator_proxy_session):
|
||||||
|
|
||||||
|
from core.models.OperatorProxySession import OperatorProxySession
|
||||||
|
operator_proxy_session_file_contents = f'{operator_proxy_session.to_json(indent=4)}\n'
|
||||||
|
os.makedirs(self.get_config_path(), exist_ok=True)
|
||||||
|
|
||||||
|
operator_proxy_session_file_path = self.get_operator_proxy_session_path()
|
||||||
|
|
||||||
|
with open(operator_proxy_session_file_path, 'w') as operator_proxy_session_file:
|
||||||
|
operator_proxy_session_file.write(operator_proxy_session_file_contents)
|
||||||
|
|
||||||
|
def get_operator_proxy_session_path(self):
|
||||||
|
return f'{self.get_config_path()}/operator_proxy_session.json'
|
||||||
|
|
||||||
|
def get_operator_proxy_session(self):
|
||||||
|
|
||||||
|
try:
|
||||||
|
config_file_contents = open(self.get_operator_proxy_session_path(), 'r').read()
|
||||||
|
except FileNotFoundError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = json.loads(config_file_contents)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
from core.models.OperatorProxySession import OperatorProxySession
|
||||||
|
return OperatorProxySession.from_dict(data)
|
||||||
|
|
||||||
|
def has_operator_proxy_session(self):
|
||||||
|
return os.path.isfile(self.get_operator_proxy_session_path())
|
||||||
8
core/observers/EncryptedProxyObserver.py
Normal file
8
core/observers/EncryptedProxyObserver.py
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
from core.observers.BaseObserver import BaseObserver
|
||||||
|
|
||||||
|
|
||||||
|
class EncryptedProxyObserver(BaseObserver):
|
||||||
|
def __init__(self):
|
||||||
|
self.on_connected = []
|
||||||
|
self.on_disconnected = []
|
||||||
|
self.on_error = []
|
||||||
|
|
@ -2,6 +2,7 @@ from core.Constants import Constants
|
||||||
from core.models.ClientVersion import ClientVersion
|
from core.models.ClientVersion import ClientVersion
|
||||||
from core.models.Location import Location
|
from core.models.Location import Location
|
||||||
from core.models.Operator import Operator
|
from core.models.Operator import Operator
|
||||||
|
from core.models.OperatorProxySession import OperatorProxySession
|
||||||
from core.models.Subscription import Subscription
|
from core.models.Subscription import Subscription
|
||||||
from core.models.SubscriptionPlan import SubscriptionPlan
|
from core.models.SubscriptionPlan import SubscriptionPlan
|
||||||
from core.models.invoice.Invoice import Invoice
|
from core.models.invoice.Invoice import Invoice
|
||||||
|
|
@ -67,7 +68,7 @@ class WebServiceApiService:
|
||||||
|
|
||||||
if response.status_code == status_codes.OK:
|
if response.status_code == status_codes.OK:
|
||||||
for operator in response.json()['data']:
|
for operator in response.json()['data']:
|
||||||
operators.append(Operator(operator['id'], operator['name'], operator['public_key'], operator['nostr_public_key'], operator['nostr_profile_reference'], operator['nostr_attestation']['event_reference']))
|
operators.append(Operator(operator['id'], operator['name'], operator['type'], operator['public_key'], operator['nostr_public_key'], operator['nostr_profile_reference'], operator['nostr_attestation']['event_reference']))
|
||||||
|
|
||||||
return operators
|
return operators
|
||||||
|
|
||||||
|
|
@ -100,17 +101,21 @@ class WebServiceApiService:
|
||||||
return subscription_plans
|
return subscription_plans
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def post_subscription(subscription_plan_id, location_id, proxies: Optional[dict] = None):
|
def post_subscription(subscription_plan_id, location_id=None, operator_id=None, proxies: Optional[dict] = None):
|
||||||
|
|
||||||
from requests.status_codes import codes as status_codes
|
from requests.status_codes import codes as status_codes
|
||||||
|
|
||||||
response = WebServiceApiService.__post('/subscriptions', None, {
|
body = {'subscription_plan_id': subscription_plan_id}
|
||||||
'subscription_plan_id': subscription_plan_id,
|
|
||||||
'location_id': location_id
|
if operator_id is not None:
|
||||||
}, proxies)
|
body['operator_id'] = operator_id
|
||||||
|
else:
|
||||||
|
body['location_id'] = location_id
|
||||||
|
|
||||||
|
response = WebServiceApiService.__post('/subscriptions', None, body, proxies)
|
||||||
|
|
||||||
if response.status_code == status_codes.CREATED:
|
if response.status_code == status_codes.CREATED:
|
||||||
return Subscription(response.headers['X-Billing-Code'])
|
return Subscription(response.headers['X-Billing-Code'], operator_id=operator_id)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
@ -127,9 +132,12 @@ class WebServiceApiService:
|
||||||
response = WebServiceApiService.__get('/subscriptions/current', billing_code, proxies)
|
response = WebServiceApiService.__get('/subscriptions/current', billing_code, proxies)
|
||||||
|
|
||||||
if response.status_code == status_codes.OK:
|
if response.status_code == status_codes.OK:
|
||||||
|
|
||||||
subscription = response.json()['data']
|
subscription = response.json()['data']
|
||||||
return Subscription(billing_code, Subscription.from_iso_format(subscription['expires_at']))
|
return Subscription(
|
||||||
|
billing_code,
|
||||||
|
operator_id=subscription.get('operator_id'),
|
||||||
|
expires_at=Subscription.from_iso_format(subscription['expires_at'])
|
||||||
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
@ -168,13 +176,38 @@ class WebServiceApiService:
|
||||||
response = WebServiceApiService.__get('/proxy-configurations/current', billing_code, proxies)
|
response = WebServiceApiService.__get('/proxy-configurations/current', billing_code, proxies)
|
||||||
|
|
||||||
if response.status_code == status_codes.OK:
|
if response.status_code == status_codes.OK:
|
||||||
|
|
||||||
proxy_configuration = response.json()['data']
|
proxy_configuration = response.json()['data']
|
||||||
return ProxyConfiguration(proxy_configuration['ip_address'], proxy_configuration['port'], proxy_configuration['username'], proxy_configuration['password'], proxy_configuration['location']['time_zone']['code'])
|
return ProxyConfiguration(proxy_configuration['ip_address'], proxy_configuration['port'], proxy_configuration['username'], proxy_configuration['password'], proxy_configuration['location']['time_zone']['code'])
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def post_operator_proxy(billing_code: str, operator_id: int, protocol: str, proxies: Optional[dict] = None):
|
||||||
|
|
||||||
|
from requests.status_codes import codes as status_codes
|
||||||
|
|
||||||
|
response = WebServiceApiService.__post('/subscriptions/current/operator-proxies', billing_code, {
|
||||||
|
'operator_id': operator_id,
|
||||||
|
'protocol': protocol,
|
||||||
|
}, proxies)
|
||||||
|
|
||||||
|
if response.status_code == status_codes.OK:
|
||||||
|
data = response.json()['data']
|
||||||
|
return OperatorProxySession(
|
||||||
|
data['id'],
|
||||||
|
data['type'],
|
||||||
|
data['username'],
|
||||||
|
data.get('password'),
|
||||||
|
data.get('links'),
|
||||||
|
data.get('subscription_url'),
|
||||||
|
data['operator']['id'],
|
||||||
|
data['operator']['name'],
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def post_wireguard_session(country_code: str, location_code: str, billing_code: str, public_key: str, proxies: Optional[dict] = None):
|
def post_wireguard_session(country_code: str, location_code: str, billing_code: str, public_key: str, proxies: Optional[dict] = None):
|
||||||
|
|
||||||
|
|
|
||||||
0
core/services/encrypted_proxy/__init__.py
Normal file
0
core/services/encrypted_proxy/__init__.py
Normal file
33
core/services/encrypted_proxy/disable_service.py
Normal file
33
core/services/encrypted_proxy/disable_service.py
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
import ssl
|
||||||
|
import time
|
||||||
|
import urllib.request
|
||||||
|
from pathlib import Path
|
||||||
|
from core.utils.encrypted_proxy.singbox import SingboxRunner
|
||||||
|
|
||||||
|
|
||||||
|
def get_public_ip(timeout: int = 8) -> str:
|
||||||
|
ctx = ssl.create_default_context()
|
||||||
|
try:
|
||||||
|
req = urllib.request.Request(
|
||||||
|
"https://ifconfig.me/ip",
|
||||||
|
headers={"User-Agent": "curl/7.0"}
|
||||||
|
)
|
||||||
|
with urllib.request.urlopen(req, timeout=timeout, context=ctx) as resp:
|
||||||
|
return resp.read().decode().strip()
|
||||||
|
except Exception as e:
|
||||||
|
return f"unknown ({e})"
|
||||||
|
|
||||||
|
|
||||||
|
def disable_proxy(tmp_dir: Path, wrapper: str, unit: str, observer=None) -> bool:
|
||||||
|
runner = SingboxRunner(wrapper, unit)
|
||||||
|
runner.stop()
|
||||||
|
|
||||||
|
for f in tmp_dir.glob("*-sing-box.json"):
|
||||||
|
f.unlink(missing_ok=True)
|
||||||
|
|
||||||
|
time.sleep(1)
|
||||||
|
ip = get_public_ip()
|
||||||
|
|
||||||
|
if observer:
|
||||||
|
observer.notify("disconnected", {"ip": ip})
|
||||||
|
return True
|
||||||
114
core/services/encrypted_proxy/hysteria_service.py
Normal file
114
core/services/encrypted_proxy/hysteria_service.py
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
from pathlib import Path
|
||||||
|
from core.utils.encrypted_proxy.net import get_real_ip, verify_ip
|
||||||
|
from core.utils.encrypted_proxy.singbox import SingboxRunner
|
||||||
|
import socket
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve(host: str) -> str:
|
||||||
|
return socket.gethostbyname(host)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_hy2_domain(server_host: str) -> str:
|
||||||
|
parts = server_host.split(".", 1)
|
||||||
|
if len(parts) == 2:
|
||||||
|
return f"hy2.{parts[1]}"
|
||||||
|
return server_host
|
||||||
|
|
||||||
|
|
||||||
|
def build_hysteria_config(username: str, password: str,
|
||||||
|
server_host: str, socks5_port: int) -> dict:
|
||||||
|
hy2_domain = _get_hy2_domain(server_host)
|
||||||
|
server_ip = _resolve(hy2_domain)
|
||||||
|
return {
|
||||||
|
"dns": {
|
||||||
|
"servers": [{"tag": "local", "type": "udp", "server": "1.1.1.1"}],
|
||||||
|
"final": "local",
|
||||||
|
"strategy": "ipv4_only",
|
||||||
|
},
|
||||||
|
"inbounds": [
|
||||||
|
{
|
||||||
|
"type": "tun",
|
||||||
|
"tag": "tun-in",
|
||||||
|
"interface_name": "tun0",
|
||||||
|
"address": ["172.19.0.1/30"],
|
||||||
|
"mtu": 9000,
|
||||||
|
"auto_route": True,
|
||||||
|
"stack": "system",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "socks",
|
||||||
|
"tag": "socks-in",
|
||||||
|
"listen": "127.0.0.1",
|
||||||
|
"listen_port": socks5_port,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"outbounds": [
|
||||||
|
{"type": "direct", "tag": "direct"},
|
||||||
|
{"type": "block", "tag": "block"},
|
||||||
|
{
|
||||||
|
"type": "hysteria2",
|
||||||
|
"tag": "proxy",
|
||||||
|
"server": server_ip,
|
||||||
|
"server_port": 443,
|
||||||
|
"password": f"{username}:{password}",
|
||||||
|
"tls": {
|
||||||
|
"enabled": True,
|
||||||
|
"server_name": hy2_domain,
|
||||||
|
"insecure": False,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"route": {
|
||||||
|
"rules": [
|
||||||
|
{"protocol": "dns", "action": "hijack-dns"},
|
||||||
|
{"ip_cidr": ["172.19.0.0/30"], "action": "hijack-dns"},
|
||||||
|
{"ip_cidr": [f"{server_ip}/32"], "outbound": "direct"},
|
||||||
|
{"ip_is_private": True, "outbound": "direct"},
|
||||||
|
{"ip_version": 6, "outbound": "block"},
|
||||||
|
{"inbound": ["tun-in", "socks-in"], "outbound": "proxy"},
|
||||||
|
],
|
||||||
|
"final": "proxy",
|
||||||
|
"default_domain_resolver": "local",
|
||||||
|
"auto_detect_interface": True,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def enable_hysteria(username: str, password: str, server_host: str,
|
||||||
|
config_dir: Path, socks5_port: int,
|
||||||
|
observer=None) -> bool:
|
||||||
|
real_ip = get_real_ip()
|
||||||
|
runner = SingboxRunner()
|
||||||
|
runner.stop()
|
||||||
|
|
||||||
|
config_path = config_dir / f"{username}-sing-box.json"
|
||||||
|
config = build_hysteria_config(username, password, server_host, socks5_port)
|
||||||
|
|
||||||
|
try:
|
||||||
|
runner.write_config(config_path, config)
|
||||||
|
ok = runner.start(config_path)
|
||||||
|
except Exception as e:
|
||||||
|
if observer:
|
||||||
|
observer.notify("error", str(e))
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not ok:
|
||||||
|
if observer:
|
||||||
|
observer.notify("error", "sing-box not active after start")
|
||||||
|
return False
|
||||||
|
|
||||||
|
proxy_ip = verify_ip(socks5_port)
|
||||||
|
if observer:
|
||||||
|
observer.notify("connected", {
|
||||||
|
"real_ip": real_ip,
|
||||||
|
"proxy_ip": proxy_ip,
|
||||||
|
"socks5_port": socks5_port,
|
||||||
|
})
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def disable_hysteria(observer=None) -> bool:
|
||||||
|
SingboxRunner().stop()
|
||||||
|
if observer:
|
||||||
|
observer.notify("disconnected", {})
|
||||||
|
return True
|
||||||
133
core/services/encrypted_proxy/vless_service.py
Normal file
133
core/services/encrypted_proxy/vless_service.py
Normal file
|
|
@ -0,0 +1,133 @@
|
||||||
|
from urllib.parse import unquote
|
||||||
|
from pathlib import Path
|
||||||
|
from core.utils.encrypted_proxy.net import get_real_ip, verify_ip
|
||||||
|
from core.utils.encrypted_proxy.singbox import SingboxRunner
|
||||||
|
|
||||||
|
|
||||||
|
def parse_vless_link(link: str) -> dict:
|
||||||
|
link = link.replace("vless://", "")
|
||||||
|
uuid, rest = link.split("@", 1)
|
||||||
|
hostport, qs = rest.split("?", 1)
|
||||||
|
query = qs.split("#")[0]
|
||||||
|
host, port = hostport.rsplit(":", 1)
|
||||||
|
params = {}
|
||||||
|
for part in query.split("&"):
|
||||||
|
if "=" in part:
|
||||||
|
k, v = part.split("=", 1)
|
||||||
|
params[k] = v
|
||||||
|
sni = params.get("sni", host)
|
||||||
|
ws_host = params.get("host", "").strip() or sni
|
||||||
|
return {
|
||||||
|
"uuid": uuid,
|
||||||
|
"host": host,
|
||||||
|
"port": int(port),
|
||||||
|
"path": unquote(params.get("path", "/vless")),
|
||||||
|
"sni": sni,
|
||||||
|
"ws_host": ws_host,
|
||||||
|
"security": params.get("security", "tls"),
|
||||||
|
"network": params.get("type", "ws"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _get_vpn_domain(sni: str) -> str:
|
||||||
|
parts = sni.split(".", 1)
|
||||||
|
if len(parts) == 2:
|
||||||
|
return f"vpn.{parts[1]}"
|
||||||
|
return sni
|
||||||
|
|
||||||
|
|
||||||
|
def build_vless_config(vless: dict, socks5_port: int) -> dict:
|
||||||
|
vpn_domain = _get_vpn_domain(vless["sni"])
|
||||||
|
return {
|
||||||
|
"dns": {
|
||||||
|
"servers": [{"tag": "local", "type": "udp", "server": "1.1.1.1"}],
|
||||||
|
"final": "local",
|
||||||
|
"strategy": "ipv4_only",
|
||||||
|
},
|
||||||
|
"inbounds": [
|
||||||
|
{
|
||||||
|
"type": "tun",
|
||||||
|
"tag": "tun-in",
|
||||||
|
"interface_name": "tun0",
|
||||||
|
"address": ["172.19.0.1/30"],
|
||||||
|
"mtu": 9000,
|
||||||
|
"auto_route": True,
|
||||||
|
"stack": "system",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "socks",
|
||||||
|
"tag": "socks-in",
|
||||||
|
"listen": "127.0.0.1",
|
||||||
|
"listen_port": socks5_port,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"outbounds": [
|
||||||
|
{"type": "direct", "tag": "direct"},
|
||||||
|
{"type": "block", "tag": "block"},
|
||||||
|
{
|
||||||
|
"type": "vless",
|
||||||
|
"tag": "proxy",
|
||||||
|
"server": vpn_domain,
|
||||||
|
"server_port": 443,
|
||||||
|
"uuid": vless["uuid"],
|
||||||
|
"tls": {
|
||||||
|
"enabled": True,
|
||||||
|
"server_name": vpn_domain,
|
||||||
|
"insecure": False,
|
||||||
|
},
|
||||||
|
"transport": {
|
||||||
|
"type": "ws",
|
||||||
|
"path": vless["path"],
|
||||||
|
"headers": {"Host": vpn_domain},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"route": {
|
||||||
|
"rules": [
|
||||||
|
{"protocol": "dns", "action": "hijack-dns"},
|
||||||
|
{"ip_cidr": ["172.19.0.0/30"], "action": "hijack-dns"},
|
||||||
|
{"ip_is_private": True, "outbound": "direct"},
|
||||||
|
{"ip_version": 6, "outbound": "block"},
|
||||||
|
{"inbound": ["tun-in", "socks-in"], "outbound": "proxy"},
|
||||||
|
],
|
||||||
|
"final": "proxy",
|
||||||
|
"default_domain_resolver": "local",
|
||||||
|
"auto_detect_interface": True,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def enable_vless(vless_link: str, username: str, config_dir: Path,
|
||||||
|
socks5_port: int, observer=None) -> bool:
|
||||||
|
vless = parse_vless_link(vless_link)
|
||||||
|
real_ip = get_real_ip()
|
||||||
|
runner = SingboxRunner()
|
||||||
|
runner.stop()
|
||||||
|
config_path = config_dir / f"{username}-sing-box.json"
|
||||||
|
config = build_vless_config(vless, socks5_port)
|
||||||
|
try:
|
||||||
|
runner.write_config(config_path, config)
|
||||||
|
ok = runner.start(config_path)
|
||||||
|
except Exception as e:
|
||||||
|
if observer:
|
||||||
|
observer.notify("error", str(e))
|
||||||
|
return False
|
||||||
|
if not ok:
|
||||||
|
if observer:
|
||||||
|
observer.notify("error", "sing-box not active after start")
|
||||||
|
return False
|
||||||
|
proxy_ip = verify_ip(socks5_port)
|
||||||
|
if observer:
|
||||||
|
observer.notify("connected", {
|
||||||
|
"real_ip": real_ip,
|
||||||
|
"proxy_ip": proxy_ip,
|
||||||
|
"socks5_port": socks5_port,
|
||||||
|
})
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def disable_vless(observer=None) -> bool:
|
||||||
|
SingboxRunner().stop()
|
||||||
|
if observer:
|
||||||
|
observer.notify("disconnected", {})
|
||||||
|
return True
|
||||||
0
core/utils/encrypted_proxy/__init__.py
Normal file
0
core/utils/encrypted_proxy/__init__.py
Normal file
44
core/utils/encrypted_proxy/net.py
Normal file
44
core/utils/encrypted_proxy/net.py
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
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"
|
||||||
35
core/utils/encrypted_proxy/singbox.py
Normal file
35
core/utils/encrypted_proxy/singbox.py
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
class SingboxRunner:
|
||||||
|
WRAPPER = "/usr/local/bin/hydraveil-singbox"
|
||||||
|
|
||||||
|
def start(self, config_path: Path) -> bool:
|
||||||
|
self.stop()
|
||||||
|
time.sleep(1)
|
||||||
|
self._process = subprocess.Popen(
|
||||||
|
["sudo", self.WRAPPER, str(config_path)],
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
)
|
||||||
|
time.sleep(2)
|
||||||
|
return self.is_running()
|
||||||
|
|
||||||
|
def stop(self) -> str:
|
||||||
|
subprocess.run(["sudo", self.WRAPPER, "", "stop"], capture_output=True)
|
||||||
|
subprocess.run(["sudo", "ip", "link", "delete", "tun0"],
|
||||||
|
capture_output=True)
|
||||||
|
self._process = None
|
||||||
|
return "stopped"
|
||||||
|
|
||||||
|
def is_running(self) -> bool:
|
||||||
|
r = subprocess.run(["pgrep", "-f", "sing-box"], capture_output=True)
|
||||||
|
return r.returncode == 0
|
||||||
|
|
||||||
|
def write_config(self, config_path: Path, config: dict) -> None:
|
||||||
|
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with open(config_path, "w") as f:
|
||||||
|
json.dump(config, f, indent=2)
|
||||||
Loading…
Reference in a new issue