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.
This commit is contained in:
parent
d8273171b5
commit
bd34f456b5
14 changed files with 291 additions and 184 deletions
|
|
@ -65,3 +65,8 @@ 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,6 +11,10 @@ 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
|
||||
|
|
@ -64,8 +68,8 @@ class BaseProfile(ABC):
|
|||
)
|
||||
name: str
|
||||
subscription: Optional[Subscription]
|
||||
location: Optional[Location] = field(metadata=config(exclude=Exclude.ALWAYS)) # SQLAlchemy object
|
||||
type: ProfileType
|
||||
location: Optional[Location] = field(metadata=config(exclude=Exclude.ALWAYS)) # SQLAlchemy object
|
||||
|
||||
# legacy version included it to be serialized, but now it's an SQLAlchemy object.
|
||||
# location: Optional[Location]
|
||||
|
|
@ -98,13 +102,15 @@ class BaseProfile(ABC):
|
|||
|
||||
|
||||
def save(self: Self):
|
||||
# === SERIALIZATION ===
|
||||
config_dict = self.to_dict()
|
||||
|
||||
location_dict = self.location.to_dict() # this is from SQLAlchemy, and not JSON-models, that's why it's separate.
|
||||
|
||||
# === LOCATION ===
|
||||
location_dict = self.location.convert_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,10 +11,14 @@ 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,7 +46,7 @@ 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 to_dict(self):
|
||||
def convert_to_dict(self):
|
||||
return {
|
||||
"country_code": self.country_code,
|
||||
"code": self.code,
|
||||
|
|
|
|||
|
|
@ -2,14 +2,15 @@ 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):
|
||||
code: SessionConnectionTypes
|
||||
masked: bool
|
||||
masked: bool = False
|
||||
|
||||
def __post_init__(self):
|
||||
|
||||
if self.code not in ('system', 'tor', 'wireguard'):
|
||||
raise ValueError('Invalid connection code.')
|
||||
|
||||
|
||||
# Called by: ConnectionController
|
||||
# doesn't even make sense, it's checking if the Session is code system. this will always be false.
|
||||
|
|
@ -21,12 +22,36 @@ class SessionConnection(BaseConnection):
|
|||
return self.masked is True
|
||||
|
||||
|
||||
# legacy:
|
||||
# Potential refactor
|
||||
|
||||
# from dataclasses import dataclass, field
|
||||
# from dataclasses_json import dataclass_json, config
|
||||
|
||||
# class SessionConnectionTypes(str, Enum):
|
||||
# WIREGUARD = "wireguard"
|
||||
# TOR = "tor"
|
||||
|
||||
# @dataclass_json
|
||||
# @dataclass
|
||||
# class SessionConnection(BaseConnection):
|
||||
# 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
|
||||
# )
|
||||
# )
|
||||
# masked: bool = False
|
||||
|
||||
# def __post_init__(self):
|
||||
# # Convert string to enum for deserialization
|
||||
# if isinstance(self.code, str):
|
||||
# self.code = SessionConnectionTypes(self.code)
|
||||
|
||||
# if self.code not in ('system', 'tor', 'wireguard'):
|
||||
# raise ValueError('Invalid connection code.')
|
||||
# # Validation
|
||||
# if self.code not in SessionConnectionTypes:
|
||||
# raise ValueError(f'Invalid code: {self.code}')
|
||||
|
||||
# @dataclass
|
||||
# class SessionConnection(BaseConnection):
|
||||
# code: SessionConnectionTypes
|
||||
# masked: bool
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import shutil
|
|||
class SessionProfile(BaseProfile):
|
||||
resolution: str
|
||||
application_version: Optional[ApplicationVersion]
|
||||
connection: Optional[SessionConnection]
|
||||
connection: Optional[SessionConnection] = None
|
||||
|
||||
def has_connection(self):
|
||||
return self.connection is not None
|
||||
|
|
|
|||
|
|
@ -1,28 +1,29 @@
|
|||
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):
|
||||
code: SystemConnectionTypes
|
||||
masked: Literal[False] = False
|
||||
|
||||
def __post_init__(self):
|
||||
|
||||
if self.code not in ('vless', 'hysteria2', 'wireguard'):
|
||||
raise ValueError('Invalid connection code.')
|
||||
|
||||
@staticmethod
|
||||
def needs_proxy_configuration():
|
||||
return False
|
||||
|
||||
# legacy:
|
||||
# Potential refactor:
|
||||
# from enum import Enum
|
||||
# from typing import Literal
|
||||
|
||||
# class SystemConnectionTypes(str, Enum):
|
||||
# WIREGUARD = "wireguard"
|
||||
# HYSTERIA2 = "hysteria2"
|
||||
# VLESS = "vless"
|
||||
|
||||
# @dataclass
|
||||
# class SystemConnection(BaseConnection):
|
||||
|
||||
|
||||
# def __post_init__(self):
|
||||
|
||||
# if self.code not in ('vless', 'hysteria2', 'wireguard'):
|
||||
# raise ValueError('Invalid connection code.')
|
||||
# code: SystemConnectionTypes
|
||||
# masked: Literal[False] = False
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
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,10 +1,14 @@
|
|||
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
|
||||
|
|
@ -17,8 +21,6 @@ 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]] = (),
|
||||
|
|
@ -75,7 +77,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 not in wireguard_types:
|
||||
if profile.connection.code != "wireguard":
|
||||
logger.info(f"[CONNECTION] This profile {profile.id} does NOT need a wireguard config")
|
||||
return
|
||||
|
||||
|
|
|
|||
|
|
@ -1,118 +0,0 @@
|
|||
#!/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)
|
||||
}
|
||||
}
|
||||
42
core/services/networking/systemwide/killswitch.py
Normal file
42
core/services/networking/systemwide/killswitch.py
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
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" # has 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"
|
||||
48
core/services/networking/systemwide/systemwide_utils.py
Normal file
48
core/services/networking/systemwide/systemwide_utils.py
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
|
||||
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,5 +1,11 @@
|
|||
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
|
||||
|
|
@ -37,42 +43,87 @@ def terminate_system_connection():
|
|||
else:
|
||||
raise ConnectionTerminationError('The connection could not be terminated.')
|
||||
|
||||
|
||||
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_nmcli_connection(wg_config_path: str) -> Result:
|
||||
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.')
|
||||
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 setup_ipv6_sinkhole(process_output: str) -> Result:
|
||||
error_info = "with nmcli setting up the IPv6 sinkhole, error is "
|
||||
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:
|
||||
raise ConnectionError('The connection could not be established.')
|
||||
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.')
|
||||
|
||||
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:
|
||||
raise ConnectionError('The connection could not be established.')
|
||||
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.')
|
||||
|
||||
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)
|
||||
except CalledProcessError:
|
||||
raise ConnectionError('The connection could not be established.')
|
||||
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.')
|
||||
|
||||
|
||||
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:
|
||||
|
|
@ -81,15 +132,35 @@ def __establish_system_connection(profile: SystemProfile, connection_observer: O
|
|||
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:
|
||||
__establish_system_connection(profile, connection_observer)
|
||||
connection_result = __establish_system_connection(profile, False, connection_observer)
|
||||
evaluate_connection_result(connection_result)
|
||||
|
||||
except ConnectionError:
|
||||
|
||||
|
|
@ -107,8 +178,10 @@ def establish_system_connection(profile: SystemProfile, ignore: tuple[type[Excep
|
|||
except ConnectionTerminationError:
|
||||
pass
|
||||
|
||||
# trying again..
|
||||
try:
|
||||
__establish_system_connection(profile, connection_observer)
|
||||
connection_result = __establish_system_connection(profile, False, connection_observer)
|
||||
evaluate_connection_result(connection_result)
|
||||
|
||||
except (ConnectionError, CalledProcessError):
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue