Compare commits
No commits in common. "9ba6d7e783da650d525575e8996ffe134a089a85" and "d8273171b5d7832e3b3b087576a1bd62945554a7" have entirely different histories.
9ba6d7e783
...
d8273171b5
16 changed files with 184 additions and 297 deletions
|
|
@ -1,12 +1,6 @@
|
|||
# Major Change Log:
|
||||
|
||||
|
||||
# 2.4.3
|
||||
### Systemwide Killswitch Introduced for Enable
|
||||
### July 20, 2026
|
||||
Introduced IP-interface-based killswitch support for systemwide wireguard, on enable only. This version is stable and working for enable only, with a "test" script in the fileslot. And fixed GUI's object save creation. Additionally rolled back Connection data models to previous non-enum version.
|
||||
<br/>
|
||||
|
||||
# 2.4.2
|
||||
### Surgery on ConnectionController
|
||||
Huge separation of ConnectionController & ProfileController into naked function modules for enabling a connection, registering wireguard keys, coordinating proxy configs, subscriptions, endpoint verification, and the logistical flow. Additionally enums were used for type checking, instead of polymorphic functions. This version is stable and confirmed working for systemwide and session wireguard, but not yet proxies.
|
||||
|
|
|
|||
|
|
@ -65,8 +65,3 @@ class Constants:
|
|||
|
||||
HV_SESSION_STATE_HOME: Final[str] = f'{HV_STATE_HOME}/sessions'
|
||||
HV_TOR_STATE_HOME: Final[str] = f'{HV_STATE_HOME}/tor'
|
||||
|
||||
# ── killswitch / dns wrappers ─────────────────────────────────────────────
|
||||
KILLSWITCH_WRAPPER: Final[str] = os.environ.get(
|
||||
'KILLSWITCH_WRAPPER', '/opt/hydra-veil/killswitch'
|
||||
)
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ def get_unused_tickets(ticket_observer: TicketObserver) -> dict:
|
|||
does_the_file_exist, path_of_file = does_ticket_tracker_exist()
|
||||
if does_the_file_exist == False:
|
||||
error_msg = f"The ticket tracker organizer file does not exist. Check the folder {path_of_file}"
|
||||
# ticket_observer.notify("failed_input", subject=error_msg)
|
||||
ticket_observer.notify("failed_input", subject=error_msg)
|
||||
return {"valid": False, "message": error_msg}
|
||||
|
||||
# Use the Model:
|
||||
|
|
|
|||
|
|
@ -11,10 +11,6 @@ from core.Helpers import write_atomically
|
|||
from core.models.manage.wrapper import safe_db_operation, WrapperRollback
|
||||
from core.models.DatabaseOperation import DatabaseOperation, DBErrorType
|
||||
|
||||
# If switching to enums for Connection:
|
||||
# from core.models.system.SystemConnection import SystemConnection, SystemConnectionTypes
|
||||
# from core.models.session.SessionConnection import SessionConnection, SessionConnectionTypes
|
||||
|
||||
from core.controllers.LocationController import LocationController
|
||||
from core.models.orm_models.Location import Location
|
||||
from core.models.orm_models.Operator import Operator
|
||||
|
|
@ -68,8 +64,8 @@ class BaseProfile(ABC):
|
|||
)
|
||||
name: str
|
||||
subscription: Optional[Subscription]
|
||||
type: ProfileType
|
||||
location: Optional[Location] = field(metadata=config(exclude=Exclude.ALWAYS)) # SQLAlchemy object
|
||||
type: ProfileType
|
||||
|
||||
# legacy version included it to be serialized, but now it's an SQLAlchemy object.
|
||||
# location: Optional[Location]
|
||||
|
|
@ -102,15 +98,13 @@ class BaseProfile(ABC):
|
|||
|
||||
|
||||
def save(self: Self):
|
||||
# === SERIALIZATION ===
|
||||
config_dict = self.to_dict()
|
||||
|
||||
# === LOCATION ===
|
||||
location_dict = self.location.convert_to_dict() # this is from SQLAlchemy, and not JSON-models, that's why it's separate.
|
||||
location_dict = self.location.to_dict() # this is from SQLAlchemy, and not JSON-models, that's why it's separate.
|
||||
|
||||
if self.location:
|
||||
config_dict["location"] = location_dict
|
||||
|
||||
# === FILE I/O ===
|
||||
config_file_contents = json.dumps(config_dict, indent=4) + '\n'
|
||||
|
||||
# legacy version:
|
||||
|
|
|
|||
|
|
@ -11,14 +11,10 @@ class ResultError(Enum):
|
|||
INVALID_INPUT = "invalid_input"
|
||||
CONNECTION = "connection"
|
||||
DATABASE = "database"
|
||||
PERMISSION = "permission"
|
||||
SUBSCRIPTION = "subscription"
|
||||
NMCLI = "nmcli_issues"
|
||||
FIREWALL = "firewall"
|
||||
UNKNOWN = "unknown"
|
||||
|
||||
@dataclass
|
||||
class Result():
|
||||
class Result:
|
||||
valid: bool
|
||||
error_type: ResultError = ResultError.SUCCESS
|
||||
data: Optional[Any] = None
|
||||
|
|
|
|||
|
|
@ -46,12 +46,12 @@ class Location(BaseModel):
|
|||
is_hysteria2_capable: Mapped[Optional[bool]] = mapped_column(String, nullable=True, default=None)
|
||||
is_vless_capable: Mapped[Optional[bool]] = mapped_column(String, nullable=True, default=None)
|
||||
|
||||
def convert_to_dict(self):
|
||||
def to_dict(self):
|
||||
return {
|
||||
"country_code": self.country_code,
|
||||
"code": self.code,
|
||||
"time_zone": self.time_zone
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# to use:
|
||||
|
|
|
|||
|
|
@ -2,15 +2,14 @@ from core.models.BaseConnection import BaseConnection
|
|||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
|
||||
class SessionConnectionTypes(str, Enum):
|
||||
WIREGUARD = "wireguard"
|
||||
TOR = "tor"
|
||||
|
||||
@dataclass
|
||||
class SessionConnection(BaseConnection):
|
||||
masked: bool = False
|
||||
|
||||
def __post_init__(self):
|
||||
|
||||
if self.code not in ('system', 'tor', 'wireguard'):
|
||||
raise ValueError('Invalid connection code.')
|
||||
|
||||
code: SessionConnectionTypes
|
||||
masked: bool
|
||||
|
||||
# Called by: ConnectionController
|
||||
# doesn't even make sense, it's checking if the Session is code system. this will always be false.
|
||||
|
|
@ -22,36 +21,12 @@ class SessionConnection(BaseConnection):
|
|||
return self.masked is True
|
||||
|
||||
|
||||
# Potential refactor
|
||||
|
||||
# from dataclasses import dataclass, field
|
||||
# from dataclasses_json import dataclass_json, config
|
||||
|
||||
# class SessionConnectionTypes(str, Enum):
|
||||
# WIREGUARD = "wireguard"
|
||||
# TOR = "tor"
|
||||
|
||||
# @dataclass_json
|
||||
# legacy:
|
||||
# @dataclass
|
||||
# class SessionConnection:
|
||||
# code: SessionConnectionTypes = field(
|
||||
# metadata=config(
|
||||
# encoder=lambda x: x.value, # Convert enum to string on save
|
||||
# decoder=lambda x: SessionConnectionTypes(x) # Convert string to enum on load
|
||||
# )
|
||||
# )
|
||||
# class SessionConnection(BaseConnection):
|
||||
# masked: bool = False
|
||||
|
||||
# def __post_init__(self):
|
||||
# # Convert string to enum for deserialization
|
||||
# if isinstance(self.code, str):
|
||||
# self.code = SessionConnectionTypes(self.code)
|
||||
|
||||
# # Validation
|
||||
# if self.code not in SessionConnectionTypes:
|
||||
# raise ValueError(f'Invalid code: {self.code}')
|
||||
|
||||
# @dataclass
|
||||
# class SessionConnection(BaseConnection):
|
||||
# code: SessionConnectionTypes
|
||||
# masked: bool
|
||||
# if self.code not in ('system', 'tor', 'wireguard'):
|
||||
# raise ValueError('Invalid connection code.')
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import shutil
|
|||
class SessionProfile(BaseProfile):
|
||||
resolution: str
|
||||
application_version: Optional[ApplicationVersion]
|
||||
connection: Optional[SessionConnection] = None
|
||||
connection: Optional[SessionConnection]
|
||||
|
||||
def has_connection(self):
|
||||
return self.connection is not None
|
||||
|
|
|
|||
|
|
@ -1,29 +1,28 @@
|
|||
from core.models.BaseConnection import BaseConnection
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import Literal
|
||||
|
||||
class SystemConnectionTypes(str, Enum):
|
||||
WIREGUARD = "wireguard"
|
||||
HYSTERIA2 = "hysteria2"
|
||||
VLESS = "vless"
|
||||
|
||||
# legacy:
|
||||
@dataclass
|
||||
class SystemConnection (BaseConnection):
|
||||
|
||||
def __post_init__(self):
|
||||
|
||||
if self.code not in ('vless', 'hysteria2', 'wireguard'):
|
||||
raise ValueError('Invalid connection code.')
|
||||
class SystemConnection(BaseConnection):
|
||||
code: SystemConnectionTypes
|
||||
masked: Literal[False] = False
|
||||
|
||||
@staticmethod
|
||||
def needs_proxy_configuration():
|
||||
return False
|
||||
|
||||
# Potential refactor:
|
||||
# from enum import Enum
|
||||
# from typing import Literal
|
||||
|
||||
# class SystemConnectionTypes(str, Enum):
|
||||
# WIREGUARD = "wireguard"
|
||||
# HYSTERIA2 = "hysteria2"
|
||||
# VLESS = "vless"
|
||||
|
||||
# legacy:
|
||||
# @dataclass
|
||||
# class SystemConnection(BaseConnection):
|
||||
# code: SystemConnectionTypes
|
||||
# masked: Literal[False] = False
|
||||
# class SystemConnection (BaseConnection):
|
||||
|
||||
|
||||
# def __post_init__(self):
|
||||
|
||||
# if self.code not in ('vless', 'hysteria2', 'wireguard'):
|
||||
# raise ValueError('Invalid connection code.')
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
from core.Constants import Constants
|
||||
import os
|
||||
|
||||
def extract_endpoint_ip(profile_id: str):
|
||||
try:
|
||||
profile_path = Constants.HV_PROFILE_CONFIG_HOME + f'/{profile_id}'
|
||||
wg_conf_path = f'{profile_path}/wg.conf.bak'
|
||||
print(f"wg_conf_path is {wg_conf_path}")
|
||||
if not os.path.exists(wg_conf_path):
|
||||
return None
|
||||
with open(wg_conf_path, 'r') as f:
|
||||
content = f.read()
|
||||
for line in content.split('\n'):
|
||||
if line.strip().startswith('Endpoint = '):
|
||||
endpoint = line.strip().split(' = ')[1]
|
||||
return endpoint.split(':')[0]
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
|
@ -1,14 +1,10 @@
|
|||
from core.services.networking.general_connection_tools.testing_evaluating import system_uses_wireguard_interface
|
||||
from core.services.networking.systemwide.systemwide_wireguard import establish_system_connection, terminate_system_connection
|
||||
from core.services.keys_and_verifications.wireguard_keys import register_wireguard_session
|
||||
from core.models.session.SessionConnection import SessionConnectionTypes
|
||||
from core.models.system.SystemConnection import SystemConnectionTypes
|
||||
from core.services.subscriptions.subscriptions import activate_subscription
|
||||
|
||||
# If refactored to enums:
|
||||
# from core.models.session.SessionConnection import SessionConnectionTypes
|
||||
# from core.models.system.SystemConnection import SystemConnectionTypes
|
||||
# wireguard_types = [SystemConnectionTypes.WIREGUARD, SessionConnectionTypes.WIREGUARD]
|
||||
|
||||
|
||||
from core.errors.logger import logger
|
||||
from core.errors.exceptions import *
|
||||
from typing import Union, Optional, Callable
|
||||
|
|
@ -21,6 +17,8 @@ from core.models.BaseProfile import ProfileType
|
|||
from core.observers.ConnectionObserver import ConnectionObserver
|
||||
from core.controllers.SystemStateController import SystemStateController
|
||||
|
||||
wireguard_types = [SystemConnectionTypes.WIREGUARD, SessionConnectionTypes.WIREGUARD]
|
||||
|
||||
def establish_connection(
|
||||
profile: Union[SessionProfile, SystemProfile],
|
||||
ignore: tuple[type[Exception]] = (),
|
||||
|
|
@ -77,7 +75,7 @@ def _ensure_wireguard_configured(
|
|||
"""Setup WireGuard config if needed."""
|
||||
|
||||
logger.info(f"[CONNECTION] Checking if this profile needs a wg config. The profile.connection.code is {profile.connection.code}")
|
||||
if profile.connection.code != "wireguard":
|
||||
if profile.connection.code not in wireguard_types:
|
||||
logger.info(f"[CONNECTION] This profile {profile.id} does NOT need a wireguard config")
|
||||
return
|
||||
|
||||
|
|
|
|||
118
core/services/networking/systemwide/firewall_isolated.sh
Normal file
118
core/services/networking/systemwide/firewall_isolated.sh
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# root check
|
||||
if [[ "$EUID" -ne 0 ]]; then
|
||||
echo "[killswitch] ERROR: must be run as root" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ACTION="${1:-}"
|
||||
SERVER_IP="${2:-}"
|
||||
TUNNEL_IF="${3:-}"
|
||||
TABLE="hydraveil"
|
||||
|
||||
# Confirm the action is among the valid choices:
|
||||
if [[ "$ACTION" != "arm" && "$ACTION" != "disarm" && "$ACTION" != "status" ]]; then
|
||||
echo "[killswitch] ERROR: invalid action '$ACTION'. Usage: arm <server_ip> <tunnel_if> | disarm | status" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# This function gets the default route. The function is used during the arm phase later on.
|
||||
_default_iface() {
|
||||
ip route show default 2>/dev/null | awk 'NR==1{print $5}'
|
||||
}
|
||||
|
||||
# Log changes in the rules:
|
||||
_log() {
|
||||
local level="$1"; shift
|
||||
echo "[killswitch] [$level] $* — $(date '+%Y-%m-%d %H:%M:%S')" >&2
|
||||
}
|
||||
|
||||
# use the presence of the table to detect if the rules are active, which we define as "armed"
|
||||
if [[ "$ACTION" == "status" ]]; then
|
||||
if nft list table inet "$TABLE" &>/dev/null 2>&1; then
|
||||
echo "armed"
|
||||
else
|
||||
echo "disarmed"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# If disarm is chosen, then check if the table rules exist, and if so, delete the table. Then log it.
|
||||
if [[ "$ACTION" == "disarm" ]]; then
|
||||
if nft list table inet "$TABLE" &>/dev/null 2>&1; then
|
||||
nft delete table inet "$TABLE"
|
||||
_log "INFO" "disarmed"
|
||||
else
|
||||
_log "INFO" "already disarmed"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check the pre-reqs exist. We need a server IP and a tunnel interface.
|
||||
[[ -z "$SERVER_IP" ]] && { _log "ERROR" "server_ip required for arm"; exit 1; }
|
||||
[[ -z "$TUNNEL_IF" ]] && { _log "ERROR" "tunnel_if required for arm"; exit 1; }
|
||||
|
||||
# Confirm the IPv4 address is a valid format. But does NOT check if it's actually a live server.
|
||||
# Just that it looks like an IPv4 address.
|
||||
if ! [[ "$SERVER_IP" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
|
||||
_log "ERROR" "invalid IPv4: $SERVER_IP"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
IFS='.' read -r o1 o2 o3 o4 <<< "$SERVER_IP"
|
||||
for oct in "$o1" "$o2" "$o3" "$o4"; do
|
||||
if (( oct > 255 )); then
|
||||
_log "ERROR" "invalid IPv4 octet ($oct) in $SERVER_IP"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# This uses the function at the top of the file to get the default WAN interface, and then checks that it got a result.
|
||||
WAN_IFACE=$(_default_iface)
|
||||
if [[ -z "$WAN_IFACE" ]]; then
|
||||
_log "ERROR" "could not detect primary network interface"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Either list out the table rules and delete them, or return True.
|
||||
nft list table inet "$TABLE" &>/dev/null 2>&1 && nft delete table inet "$TABLE" || true
|
||||
|
||||
table inet ${TABLE} {
|
||||
chain output {
|
||||
type filter hook output priority filter; policy drop;
|
||||
# Allow all loopback traffic (localhost)
|
||||
oifname "lo" accept
|
||||
|
||||
# Drop all IPv6 except loopback (::1). This blocks IPv6 from leaving the system entirely
|
||||
ip6 daddr != ::1 drop
|
||||
|
||||
# Allow IPv4 traffic to the server IP on the WAN interface
|
||||
oifname "${WAN_IFACE}" ip daddr ${SERVER_IP} accept
|
||||
|
||||
# Allow all traffic (IPv4 and IPv6) on the WireGuard tunnel interface
|
||||
oifname "${TUNNEL_IF}" accept
|
||||
}
|
||||
chain input {
|
||||
type filter hook input priority filter; policy drop;
|
||||
# Allow all loopback traffic (localhost)
|
||||
iifname "lo" accept
|
||||
|
||||
# Drop all IPv6 except loopback (::1). This blocks IPv6 from entering the system entirely
|
||||
ip6 saddr != ::1 drop
|
||||
|
||||
# Allow established and related connections (stateful firewall tracking)
|
||||
ct state established,related accept
|
||||
|
||||
# Allow IPv4 traffic from the server IP on the WAN interface
|
||||
iifname "${WAN_IFACE}" ip saddr ${SERVER_IP} accept
|
||||
|
||||
# Allow all traffic (IPv4 and IPv6) from the WireGuard tunnel interface
|
||||
iifname "${TUNNEL_IF}" accept
|
||||
}
|
||||
chain forward {
|
||||
type filter hook forward priority filter; policy drop;
|
||||
# Drop all forwarded traffic (nothing is routed through this system)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
import subprocess
|
||||
from core.Constants import Constants
|
||||
from core.models.Result import Result, ResultError
|
||||
from core.errors.logger import logger
|
||||
|
||||
_C = Constants()
|
||||
# _C.KILLSWITCH_WRAPPER
|
||||
|
||||
TESTING_KILLSWITCH = "/opt/hydra-veil/test" # needs chmod 755
|
||||
|
||||
def arm(server_ip: str, tunnel_if: str, internal_subnet: str = None) -> Result:
|
||||
cmd = ["sudo", TESTING_KILLSWITCH, "arm", server_ip, tunnel_if]
|
||||
if internal_subnet:
|
||||
cmd.append(internal_subnet)
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
if result.returncode != 0:
|
||||
error_log = f"Failed to arm firewall: {result.stderr.strip()}"
|
||||
logger.error(f"[KILLSWITCH] {error_log}")
|
||||
return Result(valid=False, error_type=ResultError.FIREWALL, message=error_log)
|
||||
else:
|
||||
return Result(valid=True)
|
||||
|
||||
|
||||
def disarm() -> bool:
|
||||
result = subprocess.run(
|
||||
["sudo", _C.KILLSWITCH_WRAPPER, "disarm"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
print(f"[killswitch] Failed to disarm: {result.stderr.strip()}")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def status() -> bool:
|
||||
result = subprocess.run(
|
||||
["sudo", _C.KILLSWITCH_WRAPPER, "status"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
return result.returncode == 0 and result.stdout.strip() == "armed"
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
|
||||
from core.models.Result import Result, ResultError
|
||||
import re
|
||||
import shutil
|
||||
from core.Errors import CommandNotFoundError
|
||||
|
||||
def check_system_prereqs():
|
||||
if shutil.which('dbus-send') is None:
|
||||
raise CommandNotFoundError('dbus-send')
|
||||
|
||||
if shutil.which('nmcli') is None:
|
||||
raise CommandNotFoundError('nmcli')
|
||||
|
||||
|
||||
|
||||
def extract_wg_interface_name(text: str) -> Result:
|
||||
"""
|
||||
Purpose:
|
||||
Extracts the wg interface name, which should be 'wg'
|
||||
|
||||
Pattern:
|
||||
"Connection" followed by text in single quotes
|
||||
|
||||
Example:
|
||||
"Connection 'wg' (jgfdgfdjgfdjfdg"
|
||||
"""
|
||||
|
||||
pattern = r"Connection '([^']*)'"
|
||||
|
||||
match = re.search(pattern, text)
|
||||
|
||||
if match:
|
||||
# Return what's between the quotes
|
||||
result = match.group(1)
|
||||
return Result(valid=True, data=result)
|
||||
else:
|
||||
error_msg = "Error: String doesn't contain 'Connection' and text within single quotes"
|
||||
return Result(valid=True, error_type=ResultError.NMCLI, message=error_msg)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
######### ISOLATION TESTING ########
|
||||
# result1 = extract_connection_name("Connection 'wg' (jgfdgfdjgfdjfdg")
|
||||
# print(result1) # Output: wg
|
||||
|
|
@ -1,11 +1,5 @@
|
|||
from core.services.networking.general_connection_tools.testing_evaluating import await_connection, system_uses_wireguard_interface, terminate_tor_connection
|
||||
from core.services.keys_and_verifications.endpoint_verification import verify_wireguard_endpoint
|
||||
from core.models.Result import Result, ResultError
|
||||
from core.services.networking.systemwide.systemwide_utils import extract_wg_interface_name, check_system_prereqs
|
||||
from core.services.networking.general_connection_tools.config_tools import extract_endpoint_ip
|
||||
from core.services.networking.systemwide import killswitch
|
||||
from core.errors.logger import logger
|
||||
|
||||
|
||||
from core.Constants import Constants
|
||||
from core.Errors import ConnectionTerminationError, CommandNotFoundError
|
||||
|
|
@ -43,87 +37,42 @@ def terminate_system_connection():
|
|||
else:
|
||||
raise ConnectionTerminationError('The connection could not be terminated.')
|
||||
|
||||
def setup_nmcli_connection(wg_config_path: str) -> Result:
|
||||
try:
|
||||
process_output = subprocess.check_output(('nmcli', 'connection', 'import', '--temporary', 'type', 'wireguard', 'file', wg_config_path), text=True)
|
||||
return Result(valid=True, data=process_output)
|
||||
|
||||
except CalledProcessError as e:
|
||||
return Result(valid=False, error_type=ResultError.NMCLI, message=f"Called Process error with nmcli: {e}")
|
||||
# raise ConnectionError('The connection could not be established.')
|
||||
def __establish_system_connection(profile: SystemProfile, connection_observer: Optional[ConnectionObserver] = None):
|
||||
|
||||
if shutil.which('dbus-send') is None:
|
||||
raise CommandNotFoundError('dbus-send')
|
||||
|
||||
if shutil.which('nmcli') is None:
|
||||
raise CommandNotFoundError('nmcli')
|
||||
|
||||
terminate_system_connection()
|
||||
|
||||
def setup_ipv6_sinkhole(process_output: str) -> Result:
|
||||
error_info = "with nmcli setting up the IPv6 sinkhole, error is "
|
||||
try:
|
||||
process_output = subprocess.check_output(('nmcli', 'connection', 'import', '--temporary', 'type', 'wireguard', 'file', profile.get_wireguard_configuration_path()), text=True)
|
||||
except CalledProcessError:
|
||||
raise ConnectionError('The connection could not be established.')
|
||||
|
||||
try:
|
||||
|
||||
connection_id = (m := re.search(r'(?<=\()([a-f0-9-]+?)(?=\))', process_output)) and m.group(1)
|
||||
ipv6_method = subprocess.check_output(('nmcli', '-g', 'ipv6.method', 'connection', 'show', connection_id), text=True).strip()
|
||||
except CalledProcessError as e:
|
||||
return Result(valid=False, error_type=ResultError.NMCLI, message=f"Called Process error {error_info}: {e}")
|
||||
# raise ConnectionError('The connection could not be established.')
|
||||
|
||||
except CalledProcessError:
|
||||
raise ConnectionError('The connection could not be established.')
|
||||
|
||||
if ipv6_method in ('disabled', 'ignore'):
|
||||
|
||||
try:
|
||||
subprocess.run(('dbus-send', '--system', '--print-reply', '--dest=org.freedesktop.NetworkManager', '/org/freedesktop/NetworkManager', 'org.freedesktop.DBus.Properties.Set', 'string:org.freedesktop.NetworkManager', 'string:ConnectivityCheckEnabled', 'variant:boolean:false'), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
|
||||
except CalledProcessError as e:
|
||||
return Result(valid=False, error_type=ResultError.NMCLI, message=f"Called Process error {error_info}: {e}")
|
||||
# raise ConnectionError('The connection could not be established.')
|
||||
except CalledProcessError:
|
||||
raise ConnectionError('The connection could not be established.')
|
||||
|
||||
try:
|
||||
subprocess.run(('nmcli', 'connection', 'add', 'type', 'dummy', 'save', 'no', 'con-name', 'hv-ipv6-sink', 'ifname', 'hvipv6sink0', 'ipv6.method', 'manual', 'ipv6.addresses', 'fd7a:fd4b:54e3:077c::/64', 'ipv6.gateway', 'fd7a:fd4b:54e3:077c::1', 'ipv6.dns', '::1', 'ipv6.route-metric', '72'), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
|
||||
return Result(valid=True)
|
||||
|
||||
except CalledProcessError:
|
||||
return Result(valid=False, error_type=ResultError.NMCLI, message=f"Called Process error {error_info}")
|
||||
# raise ConnectionError('The connection could not be established.')
|
||||
raise ConnectionError('The connection could not be established.')
|
||||
|
||||
|
||||
def __establish_system_connection(profile: SystemProfile, firewall_setting: bool = False, connection_observer: Optional[ConnectionObserver] = None):
|
||||
|
||||
check_system_prereqs()
|
||||
terminate_system_connection()
|
||||
|
||||
# ============= INITIAL NMCLI CONNECTION =============
|
||||
wg_config_path = profile.get_wireguard_configuration_path()
|
||||
nmcli_result = setup_nmcli_connection(wg_config_path)
|
||||
|
||||
if nmcli_result.valid:
|
||||
process_output = nmcli_result.data
|
||||
else:
|
||||
logger.error(f"[SYSTEMWIDE WG] Critical issue, the initial nmcli command failed {nmcli_result.message}.")
|
||||
return nmcli_result
|
||||
|
||||
wg_interface_result = extract_wg_interface_name(process_output)
|
||||
|
||||
if not wg_interface_result.valid:
|
||||
logger.error(f"[SYSTEMWIDE WG] Critical issue, the wg interface is in the wrong format. This is the raw output: {process_output}")
|
||||
raise ConnectionError('The connection could not be established.')
|
||||
# handle the interface being wrong
|
||||
|
||||
# ============= IPv6 SINKHOLE =============
|
||||
sinkhole_result = setup_ipv6_sinkhole(process_output)
|
||||
if not sinkhole_result:
|
||||
raise ConnectionError('IPv6 Could not be protected.')
|
||||
|
||||
# ============= FIREWALL =============
|
||||
if firewall_setting:
|
||||
logger.info(f"[SYSTEMWIDE WG] Turning on Firewall")
|
||||
|
||||
wg_interface_name = wg_interface_result.data
|
||||
server_ip_address = extract_endpoint_ip(str(profile.id))
|
||||
print(f"WG Inteface Name: {wg_interface_name}")
|
||||
print(f"Server IP address: {server_ip_address}")
|
||||
|
||||
firewall_results = killswitch.arm(
|
||||
server_ip=server_ip_address,
|
||||
tunnel_if=wg_interface_name,
|
||||
internal_subnet=None
|
||||
)
|
||||
|
||||
if not firewall_results.valid:
|
||||
print(f"FAILED to setup Firewall. firewall_results is {firewall_results.message}")
|
||||
# raise ConnectionError('Firewall could not be enabled.')
|
||||
|
||||
# ============= SETUP STATE & TEST =============
|
||||
SystemStateController.create(profile.id)
|
||||
|
||||
try:
|
||||
|
|
@ -132,35 +81,15 @@ def __establish_system_connection(profile: SystemProfile, firewall_setting: bool
|
|||
except ConnectionError:
|
||||
raise ConnectionError('The connection could not be established.')
|
||||
|
||||
return Result(valid=True)
|
||||
|
||||
|
||||
def evaluate_connection_result(connection_result: Result):
|
||||
if not connection_result.valid:
|
||||
logger.error(f"[SYSTEMWIDE WG] Critical issue, could not establish a connection: {connection_result.message}")
|
||||
try:
|
||||
terminate_system_connection()
|
||||
except ConnectionTerminationError:
|
||||
pass
|
||||
|
||||
|
||||
def establish_system_connection(profile: SystemProfile, ignore: tuple[type[Exception]] = (), connection_observer: Optional[ConnectionObserver] = None):
|
||||
"""
|
||||
Rank:
|
||||
Public Interface Orchestrator
|
||||
|
||||
Purpose:
|
||||
Tries to enable the systemwide connection, retries on failure.
|
||||
"""
|
||||
|
||||
# ================= VERIFY ENDPOINT =================
|
||||
if ConfigurationController.get_endpoint_verification_enabled():
|
||||
verify_wireguard_endpoint(profile, ignore=ignore)
|
||||
|
||||
# ================= CONNECT WG =================
|
||||
try:
|
||||
connection_result = __establish_system_connection(profile, False, connection_observer)
|
||||
evaluate_connection_result(connection_result)
|
||||
__establish_system_connection(profile, connection_observer)
|
||||
|
||||
except ConnectionError:
|
||||
|
||||
|
|
@ -178,10 +107,8 @@ def establish_system_connection(profile: SystemProfile, ignore: tuple[type[Excep
|
|||
except ConnectionTerminationError:
|
||||
pass
|
||||
|
||||
# trying again..
|
||||
try:
|
||||
connection_result = __establish_system_connection(profile, False, connection_observer)
|
||||
evaluate_connection_result(connection_result)
|
||||
__establish_system_connection(profile, connection_observer)
|
||||
|
||||
except (ConnectionError, CalledProcessError):
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[project]
|
||||
name = "sp-hydra-veil-core"
|
||||
version = "2.4.3"
|
||||
version = "2.4.2"
|
||||
authors = [
|
||||
{ name = "Simplified Privacy" },
|
||||
]
|
||||
|
|
|
|||
Loading…
Reference in a new issue