Introduced Singbox Proxy Feature Start Utility & Script. But it's not yet integrated to be used
This commit is contained in:
parent
46a84661a0
commit
5377b7348b
10 changed files with 279 additions and 8 deletions
26
core/assets/sudo_scripts/singbox_runner
Normal file
26
core/assets/sudo_scripts/singbox_runner
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
#!/bin/bash
|
||||
set -eo pipefail
|
||||
|
||||
SINGBOX_BIN="/usr/bin/singbox"
|
||||
LOG_FILE="~/.local/share/hydra-veil/singbox.txt"
|
||||
|
||||
run_binary() {
|
||||
"$SINGBOX_BIN" run -c "$1" >> "$LOG_FILE" 2>&1 &
|
||||
_new_pid=$!
|
||||
echo "$_new_pid"
|
||||
}
|
||||
# get result in python: pid = int(result.stdout.strip())
|
||||
|
||||
gracefully_close() {
|
||||
sudo kill -TERM "$1" 2>/dev/null || true
|
||||
}
|
||||
|
||||
forcefully_kill() {
|
||||
sudo kill -TERM "$1" 2>/dev/null || true
|
||||
}
|
||||
|
||||
hello_world_test() {
|
||||
echo "hello world $1"
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1,6 +1,20 @@
|
|||
from core.models.Result import Result, ResultError
|
||||
from typing import Optional
|
||||
|
||||
class DNSError(Exception):
|
||||
"""There are issues with DNS"""
|
||||
def __init__(self, result: Optional[Result]):
|
||||
if result:
|
||||
self.result = result
|
||||
if result.message and result.message is not None:
|
||||
error_msg = result.message
|
||||
else:
|
||||
error_msg = result.error_type
|
||||
else:
|
||||
error_msg = "Failed to start DNS"
|
||||
super().__init__(error_msg)
|
||||
|
||||
|
||||
class FirewallError(Exception):
|
||||
"""There are issues with the Firewall"""
|
||||
def __init__(self, result: Optional[Result]):
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ class ResultError(Enum):
|
|||
DATABASE = "database"
|
||||
PERMISSION = "permission"
|
||||
SUBSCRIPTION = "subscription"
|
||||
PROCESS_GOT_KILLED = "process_got_killed"
|
||||
PROCESS_WONT_START = "process_wont_start"
|
||||
NMCLI = "nmcli_issues"
|
||||
FIREWALL = "firewall"
|
||||
CLIENT_DNS = "client_dns"
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ DNS_SCRIPT = f"/opt/hydra-veil/dns"
|
|||
def set_on(network_interface: str, dns_should_be: str) -> Result:
|
||||
command = ["sudo", DNS_SCRIPT, "set", network_interface, dns_should_be]
|
||||
human_readable_goal = "Set DNS"
|
||||
return run_generic_command(command, human_readable_goal)
|
||||
return run_generic_command(command, human_readable_goal, timeout=5)
|
||||
|
||||
|
||||
# requires script to have sudo
|
||||
|
|
@ -21,7 +21,7 @@ def set_on(network_interface: str, dns_should_be: str) -> Result:
|
|||
def revert() -> Result:
|
||||
command = ["sudo", DNS_SCRIPT, "revert"]
|
||||
human_readable_goal = "DNS revert"
|
||||
return run_generic_command(command, human_readable_goal)
|
||||
return run_generic_command(command, human_readable_goal, timeout=7)
|
||||
|
||||
|
||||
# does NOT need sudo
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
from core.services.networking.systemwide.systemwide_errors import systemwide_hell_raiser
|
||||
from core.utils.basic_operations.wrap_with import wrap_with
|
||||
from core.utils.basic_operations.run_generic_command import run_generic_command
|
||||
from core.models.Result import Result, ResultError
|
||||
from core.errors.logger import logger
|
||||
|
||||
SINGBOX_WRAPPER = "/opt/hydra-veil/singbox_runner" # needs chmod 755
|
||||
|
||||
@wrap_with(systemwide_hell_raiser)
|
||||
def start(config_path: str) -> Result:
|
||||
command = ['bash', '-c', f'. {SINGBOX_WRAPPER} && run_binary "{config_path}"']
|
||||
human_readable_goal = "Start running Singbox"
|
||||
return run_generic_command(command, human_readable_goal, timeout=5)
|
||||
|
||||
|
||||
@wrap_with(systemwide_hell_raiser)
|
||||
def turn_off(process_id: str) -> Result:
|
||||
command = ['bash', '-c', f'. {SINGBOX_WRAPPER} && gracefully_close "{process_id}"']
|
||||
human_readable_goal = "Gracefully Stop Singbox"
|
||||
return run_generic_command(command, human_readable_goal, timeout=5)
|
||||
|
||||
|
||||
@wrap_with(systemwide_hell_raiser)
|
||||
def force_kill(process_id: str) -> Result:
|
||||
command = ['bash', '-c', f'. {SINGBOX_WRAPPER} && forcefully_kill "{process_id}"']
|
||||
human_readable_goal = "Force Kill Singbox"
|
||||
return run_generic_command(command, human_readable_goal, timeout=5)
|
||||
|
||||
|
||||
@wrap_with(systemwide_hell_raiser)
|
||||
def hello_world_test(name: str) -> str:
|
||||
command = ['bash', '-c', f'. {SINGBOX_WRAPPER} && hello_world_test "{name}"']
|
||||
human_readable_goal = "Hello world test"
|
||||
return run_generic_command(command, human_readable_goal, timeout=5)
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
from core.services.networking.systemwide.encrypted_proxy import singbox
|
||||
from core.utils.basic_operations import process_tools
|
||||
from core.models.Result import Result, ResultError
|
||||
from core.errors.logger import logger
|
||||
from core.Constants import Constants
|
||||
from core.models.system.SystemState import SystemState
|
||||
from core.controllers.SystemStateController import SystemStateController
|
||||
from core.services.networking.systemwide.systemwide_utils import get_firewall_setting, get_dns_setting
|
||||
from core.services.networking.systemwide import dns
|
||||
from core.services.networking.systemwide.general_connection_tools.general_firewall_dns_tools import generic_enable_firewall_w_retry
|
||||
from core.errors.exceptions import FirewallError, DNSError
|
||||
|
||||
# generic
|
||||
import time
|
||||
|
||||
QUANTITY_OF_ATTEMPTS = 2
|
||||
SINGBOX_TUN_IF = 'tun0'
|
||||
SINGBOX_INTERNAL_SUBNET = '172.19.0.0/30'
|
||||
SINGBOX_INTERNAL_ADDR = '172.19.0.1/30'
|
||||
SINGBOX_DEFAULT_DNS = '9.9.9.9'
|
||||
|
||||
|
||||
def set_dns_for_singbox(current_state: SystemState):
|
||||
do_they_even_use_systemd = dns.is_systemd_enabled()
|
||||
|
||||
if not do_they_even_use_systemd.valid:
|
||||
error_msg = "You don't use SystemD, so we are skipping setting your DNS."
|
||||
logger.info(error_msg)
|
||||
return True
|
||||
|
||||
dns_result = dns.set_on(network_interface=SINGBOX_TUN_IF, dns_should_be=SINGBOX_DEFAULT_DNS)
|
||||
|
||||
if dns_result.valid:
|
||||
return True
|
||||
else:
|
||||
# Update the state for the DNS failure, but not firewall
|
||||
current_state.firewalled = True
|
||||
current_state.dns_set = False
|
||||
SystemStateController.update_or_create(current_state)
|
||||
raise DNSError(dns_result)
|
||||
|
||||
|
||||
def _attempt_start_with_retry(config_path: str, quantity_of_attempts: int = 2) -> Result:
|
||||
current_attempt = 0
|
||||
|
||||
while current_attempt < quantity_of_attempts:
|
||||
activation_result = singbox.start(config_path)
|
||||
|
||||
if activation_result.valid:
|
||||
return activation_result
|
||||
else:
|
||||
current_attempt = current_attempt + 1
|
||||
logger.error(f"[SINGBOX] Attempt {current_attempt} for Singbox Failed. Because: {activation_result.message}. Trying again..")
|
||||
|
||||
|
||||
|
||||
def start_singbox(
|
||||
profile_id: int,
|
||||
config_path: str,
|
||||
server_ip: str,
|
||||
connection_observer: Optional[ConnectionObserver] = None
|
||||
) -> Result:
|
||||
"""
|
||||
Purpose:
|
||||
Orchestrates the full flow of the Singbox connection.
|
||||
|
||||
Steps:
|
||||
1) Validate inputs
|
||||
2) Start it with a retry
|
||||
3) Evaluate if the process id is still up.
|
||||
4) If it's up, setup the State JSON
|
||||
5) Turn on Firewall
|
||||
6) Turn on DNS
|
||||
7) Update the State
|
||||
"""
|
||||
|
||||
function_name = "START_SINGBOX"
|
||||
|
||||
# ============= INPUT VALIDATION =============
|
||||
requirements = [profile_id, config_path, server_ip]
|
||||
for each_requirement in requirements:
|
||||
if each_requirement is None:
|
||||
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=f"Invalid inputs into {function_name}")
|
||||
|
||||
# ============= START PROCESS =============
|
||||
activation_result = _attempt_start_with_retry(config_path=config_path, quantity_of_attempts=QUANTITY_OF_ATTEMPTS)
|
||||
|
||||
if not activation_result.valid:
|
||||
error_msg = f"Singbox failed to start after {QUANTITY_OF_ATTEMPTS} attempts"
|
||||
logger.error(f"[{function_name}] {error_msg}")
|
||||
return Result(valid=False, error_type=ResultError=PROCESS_WONT_START, message=error_msg)
|
||||
|
||||
process_id = activation_result.data
|
||||
time.sleep(2)
|
||||
active = process_tools.is_running(process_id)
|
||||
|
||||
if not active:
|
||||
error_msg = f"While Singbox might have literally allowed the binary to begin, it's killing the process on id {process_id}"
|
||||
logger.error(f"[{function_name}] {error_msg}")
|
||||
return Result(valid=False, error_type=ResultError.PROCESS_GOT_KILLED, message=error_msg, data=process_id)
|
||||
|
||||
# ============= SETUP STATE =============
|
||||
# Even if firewall is off, we want to save the fact we turned Singbox on, before we raise errors.
|
||||
|
||||
logger.info(f"Setting State JSON with INTENDED firewall & Dns settings")
|
||||
current_state = SystemStateController.create(
|
||||
profile_id=profile.id,
|
||||
firewalled=True, # intended setting, not result yet
|
||||
dns_set=True # intended setting, not result yet
|
||||
)
|
||||
|
||||
# ============= FIREWALL =============
|
||||
logger.info(f"[{function_name}] Attempting to enable the Firewall for {SINGBOX_TUN_IF} and {SINGBOX_INTERNAL_SUBNET}...")
|
||||
# this is labeled "generic" for being protocol neutral
|
||||
firewall_result = generic_enable_firewall_w_retry(
|
||||
interface_name=SINGBOX_TUN_IF,
|
||||
server_ip=server_ip,
|
||||
internal_subnet = SINGBOX_INTERNAL_SUBNET,
|
||||
max_retries = 2,
|
||||
connection_observer=connection_observer
|
||||
)
|
||||
|
||||
if not firewall_result.valid:
|
||||
# Update the state of the failure,
|
||||
current_state.firewalled = False
|
||||
current_state.dns_set = False
|
||||
SystemStateController.update_or_create(current_state)
|
||||
raise FirewallError(firewall_result) # Now end the party.
|
||||
|
||||
# ============= DNS =============
|
||||
dns_result = set_dns_for_singbox(current_state)
|
||||
# raises error if not okay.
|
||||
|
||||
# ============= CONCLUSION =============
|
||||
return Result(valid=True, data=current_state)
|
||||
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
from core.services.networking.systemwide import killswitch
|
||||
from core.errors.logger import logger
|
||||
from core.errors.exceptions import FirewallError
|
||||
from core.models.Result import Result, ResultError
|
||||
|
||||
|
||||
def generic_enable_firewall_w_retry(
|
||||
interface_name: str,
|
||||
server_ip: str,
|
||||
internal_subnet: str = None,
|
||||
max_retries: int = 2
|
||||
) -> None:
|
||||
"""
|
||||
Purpose:
|
||||
Protocol-neutral, Enable the firewall with automatic retry on failure.
|
||||
|
||||
Called by:
|
||||
start_singbox
|
||||
|
||||
Raises Errors:
|
||||
Yes, FirewallError
|
||||
"""
|
||||
|
||||
last_result = None
|
||||
|
||||
for attempt in range(1, max_retries + 1):
|
||||
logger.info(f"Enabling firewall (attempt {attempt}/{max_retries})")
|
||||
|
||||
turn_on_result = killswitch.arm(
|
||||
server_ip=server_ip,
|
||||
tunnel_if=interface_name,
|
||||
internal_subnet=internal_subnet
|
||||
)
|
||||
|
||||
if turn_on_result is None:
|
||||
logger.error("turn_on_firewall() returned None")
|
||||
continue
|
||||
|
||||
last_result = turn_on_result
|
||||
firewall_status = killswitch.status()
|
||||
|
||||
if turn_on_result.valid and firewall_status:
|
||||
logger.info("Firewall successfully enabled")
|
||||
return last_result
|
||||
|
||||
logger.warning(f"Firewall enable failed on attempt {attempt}, because {turn_on_result.message}")
|
||||
|
||||
return last_result
|
||||
|
||||
|
|
@ -3,7 +3,7 @@ from core.utils.basic_operations.wrap_with import wrap_with
|
|||
from core.utils.basic_operations.run_generic_command import run_generic_command
|
||||
from core.models.Result import Result, ResultError
|
||||
from core.errors.logger import logger
|
||||
import subprocess
|
||||
# import subprocess
|
||||
|
||||
KILLSWITCH_WRAPPER = "/opt/hydra-veil/firewall" # needs chmod 755
|
||||
|
||||
|
|
@ -13,13 +13,13 @@ def arm(server_ip: str, tunnel_if: str, internal_subnet: str = None) -> Result:
|
|||
if internal_subnet:
|
||||
command.append(internal_subnet)
|
||||
human_readable_goal = "Arming the Firewall"
|
||||
return run_generic_command(command, human_readable_goal)
|
||||
return run_generic_command(command, human_readable_goal, timeout=5)
|
||||
|
||||
@wrap_with(systemwide_hell_raiser)
|
||||
def disarm() -> bool:
|
||||
command = ["sudo", KILLSWITCH_WRAPPER, "disarm"]
|
||||
human_readable_goal = "Disarming the Firewall"
|
||||
return run_generic_command(command, human_readable_goal)
|
||||
return run_generic_command(command, human_readable_goal, timeout=5)
|
||||
|
||||
def status() -> bool:
|
||||
command = ["sudo", KILLSWITCH_WRAPPER, "status"]
|
||||
|
|
|
|||
|
|
@ -169,6 +169,7 @@ def __establish_system_connection(
|
|||
raise ConnectionError('The connection could not be established.')
|
||||
|
||||
# ============= UPDATE STATE =============
|
||||
if dns_setting and not did_dns_work:
|
||||
current_state.firewalled = firewall_tracker
|
||||
current_state.dns_set = did_dns_work
|
||||
SystemStateController.update_or_create(current_state)
|
||||
|
|
|
|||
9
core/utils/basic_operations/process_tools.py
Normal file
9
core/utils/basic_operations/process_tools.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
from core.errors.logger import logger
|
||||
import os
|
||||
|
||||
def is_running(pid: int) -> bool | None:
|
||||
if not isinstance(pid, int):
|
||||
logger.error("[SUBPROCESS CHECKER] Invalid int passed in for process id!")
|
||||
return None
|
||||
|
||||
return os.path.exists(f"/proc/{pid}")
|
||||
Loading…
Reference in a new issue