Compare commits
No commits in common. "5377b7348b74093bd12b3f82c6394763c63c3b0e" and "bae5bfe7f34882e44d0858d4ae5de65d74e606e1" have entirely different histories.
5377b7348b
...
bae5bfe7f3
12 changed files with 17 additions and 306 deletions
|
|
@ -1,26 +0,0 @@
|
|||
#!/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,8 +1,7 @@
|
|||
from core.services.networking.systemwide.systemwide_wireguard import terminate_system_connection
|
||||
from core.services.networking.general_connection_tools.testing_evaluating import system_uses_wireguard_interface
|
||||
from core.services.networking.general_connection_tools.connection_enable import establish_connection
|
||||
from core.services.networking.systemwide.systemwide_utils import get_firewall_setting, get_dns_setting
|
||||
from core.services.networking.systemwide import killswitch
|
||||
# from core.services.networking.systemwide.systemwide_utils import get_firewall_setting, get_dns_setting
|
||||
from core.errors.exceptions import FirewallError
|
||||
|
||||
from core.Errors import InvalidSubscriptionError, MissingSubscriptionError, ConnectionTerminationError, ProfileActivationError, ProfileDeactivationError, MissingLocationError, ConnectionUnprotectedError, EndpointVerificationError, ProfileStateConflictError
|
||||
|
|
@ -146,16 +145,7 @@ class ProfileController:
|
|||
raise ProfileDeactivationError('The profile could not be disabled.')
|
||||
|
||||
try:
|
||||
# ================= SETTINGS =================
|
||||
firewall_setting = get_firewall_setting()
|
||||
dns_setting = get_dns_setting()
|
||||
# ======= KILL SYSTEMWIDE =============
|
||||
terminate_system_connection(
|
||||
firewall_setting=firewall_setting,
|
||||
dns_setting=dns_setting
|
||||
)
|
||||
# ================= UPDATE UI ================
|
||||
# if it made it this far, it worked in theory.
|
||||
terminate_system_connection()
|
||||
if profile_observer is not None:
|
||||
profile_observer.notify('disabled', profile, dict(
|
||||
explicitly=explicitly,
|
||||
|
|
|
|||
|
|
@ -1,20 +1,6 @@
|
|||
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,8 +15,6 @@ 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, timeout=5)
|
||||
return run_generic_command(command, human_readable_goal)
|
||||
|
||||
|
||||
# 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, timeout=7)
|
||||
return run_generic_command(command, human_readable_goal)
|
||||
|
||||
|
||||
# does NOT need sudo
|
||||
|
|
|
|||
|
|
@ -1,34 +0,0 @@
|
|||
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)
|
||||
|
|
@ -1,136 +0,0 @@
|
|||
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)
|
||||
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
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, timeout=5)
|
||||
return run_generic_command(command, human_readable_goal)
|
||||
|
||||
@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, timeout=5)
|
||||
return run_generic_command(command, human_readable_goal)
|
||||
|
||||
def status() -> bool:
|
||||
command = ["sudo", KILLSWITCH_WRAPPER, "status"]
|
||||
|
|
|
|||
|
|
@ -29,23 +29,16 @@ import subprocess
|
|||
import time
|
||||
|
||||
def terminate_system_connection(
|
||||
firewall_setting: Optional[bool] = None,
|
||||
dns_setting: Optional[bool] = None
|
||||
firewall_setting: Optional[bool] = get_firewall_setting(),
|
||||
dns_setting: Optional[bool] = get_dns_setting()
|
||||
):
|
||||
|
||||
# ====== PREREQS =======
|
||||
if firewall_setting is None:
|
||||
firewall_setting = get_firewall_setting()
|
||||
|
||||
if dns_setting is None:
|
||||
dns_setting = get_dns_setting()
|
||||
|
||||
if shutil.which('nmcli') is None:
|
||||
raise CommandNotFoundError('nmcli')
|
||||
|
||||
# ====== FIREWALL =======
|
||||
if firewall_setting:
|
||||
disable_result_object = killswitch.disarm() # raw blind kill
|
||||
check_and_kill_firewall() # on failure, this raises errors, which then bubble up
|
||||
|
||||
# ====== DNS =======
|
||||
if dns_setting:
|
||||
|
|
@ -61,9 +54,9 @@ def terminate_system_connection(
|
|||
subprocess.run(('nmcli', 'connection', 'delete', 'hv-ipv6-sink'), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
terminate_tor_connection()
|
||||
|
||||
# confirm firewall is off.
|
||||
if firewall_setting:
|
||||
check_and_kill_firewall() # on failure, this raises errors, which then bubble up
|
||||
# confirm check on dns:
|
||||
# did_dns_revert = confirm_dns_is_reverted('wg')
|
||||
# print(f"did_dns_revert is {did_dns_revert}")
|
||||
|
||||
# finally end that profile's JSON state:
|
||||
SystemState.dissolve()
|
||||
|
|
@ -169,10 +162,9 @@ 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)
|
||||
current_state.firewalled = firewall_tracker
|
||||
current_state.dns_set = did_dns_work
|
||||
SystemStateController.update_or_create(current_state)
|
||||
|
||||
return Result(valid=True, data=current_state)
|
||||
|
||||
|
|
|
|||
|
|
@ -80,8 +80,7 @@ def check_and_kill_firewall():
|
|||
logger.error(error_msg)
|
||||
|
||||
# if the systemwide_raiser didn't catch it with the reason, then..
|
||||
raise FirewallError(disable_result_object)
|
||||
|
||||
raise ConnectionTerminationError(error_msg)
|
||||
else:
|
||||
logger.info("We are skipping disabling the firewall, because it's already off.")
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
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