diff --git a/core/assets/sudo_scripts/dns b/core/assets/sudo_scripts/dns new file mode 100644 index 0000000..d102c29 --- /dev/null +++ b/core/assets/sudo_scripts/dns @@ -0,0 +1,53 @@ +#!/bin/bash +set -euo pipefail + +if [[ "$EUID" -ne 0 ]]; then + echo "[killswitch] ERROR: must be run as root" >&2 + exit 1 +fi + +ACTION="${1:-}" +IFACE="${2:-}" +DNS="${3:-}" + + +_check_prereqs() { + [[ -z "$DNS" ]] && { echo "Error: DNS IP required" >&2; exit 1; } + + # ✓ Check it's a valid IPv4 address: + if ! [[ "$DNS" =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}$ ]]; then + echo "Error: Invalid IP: $DNS" >&2 + exit 1 + fi + + # ✓ checks interface exists before configuring + if ! ip link show "$IFACE" &>/dev/null; then + echo "Error: Interface $IFACE not found" >&2 + exit 1 + fi +} + +_set_dns() { + resolvectl dns "$IFACE" "$DNS" || { + echo "ERROR: Failed to set DNS to $DNS" >&2 + return 1 + } + resolvectl domain "$IFACE" '~.' || { + echo "ERROR: Failed to set domain routing" >&2 + return 1 + } + resolvectl default-route "$IFACE" true || { + echo "ERROR: Failed to set default route" >&2 + return 1 + } +} + +if [[ "$ACTION" == "set" ]]; then + _check_prereqs + _set_dns +fi + +if [[ "$ACTION" == "revert" ]]; then + resolvectl revert "$IFACE" 2>/dev/null || true + +fi diff --git a/core/assets/sudo_scripts/firewall b/core/assets/sudo_scripts/firewall new file mode 100644 index 0000000..910dbc2 --- /dev/null +++ b/core/assets/sudo_scripts/firewall @@ -0,0 +1,120 @@ +#!/bin/bash +set -euo pipefail + +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:-}" +INTERNAL_SUBNET="${4:-}" +TABLE="hydraveil" + +if [[ "$ACTION" != "arm" && "$ACTION" != "disarm" && "$ACTION" != "status" ]]; then + echo "[killswitch] ERROR: invalid action '$ACTION'. Usage: arm [internal_subnet] | disarm | status" >&2 + exit 1 +fi + +_default_iface() { + ip route show default 2>/dev/null | awk 'NR==1{print $5}' +} + +_log() { + local level="$1"; shift + echo "[killswitch] [$level] $* — $(date '+%Y-%m-%d %H:%M:%S')" >&2 +} + +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 [[ "$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 + +[[ -z "$SERVER_IP" ]] && { _log "ERROR" "server_ip required for arm"; exit 1; } +[[ -z "$TUNNEL_IF" ]] && { _log "ERROR" "tunnel_if required for arm"; exit 1; } + +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 + +WAN_IFACE=$(_default_iface) +if [[ -z "$WAN_IFACE" ]]; then + _log "ERROR" "could not detect primary network interface" + exit 1 +fi + +INTERNAL_RULE="" +if [[ -n "$INTERNAL_SUBNET" ]]; then + INTERNAL_RULE=" ip daddr ${INTERNAL_SUBNET} accept" +fi + +nft list table inet "$TABLE" &>/dev/null 2>&1 && nft delete table inet "$TABLE" || true + +nft -f - << NFTEOF +table inet ${TABLE} { + chain output { + type filter hook output priority filter; policy drop; + oifname "lo" accept + ether type arp drop + ip6 daddr != ::1 drop + ip daddr 224.0.0.0/4 drop + ip daddr 255.255.255.255 drop + ip daddr 192.168.1.0/24 drop + + oifname "${WAN_IFACE}" ip daddr ${SERVER_IP} accept + oifname "${TUNNEL_IF}" accept + log prefix "hydraveil-drop " drop + } + + chain input { + type filter hook input priority filter; policy drop; + iifname "lo" accept + + ether type arp drop + ip6 saddr != ::1 drop + ip daddr 224.0.0.0/4 drop + ip daddr 255.255.255.255 drop + + ct state established,related accept + iifname "${WAN_IFACE}" ip saddr ${SERVER_IP} accept + iifname "${TUNNEL_IF}" accept + + log prefix "hydraveil-drop " drop + } + chain forward { + type filter hook forward priority filter; policy drop; + } +} +NFTEOF + + +_log "INFO" "armed — wan=${WAN_IFACE} server=${SERVER_IP} tunnel=${TUNNEL_IF} internal=${INTERNAL_SUBNET:-none}" +exit 0 + + + + diff --git a/core/assets/sudo_scripts/setup.sh b/core/assets/sudo_scripts/setup.sh new file mode 100644 index 0000000..28be6b3 --- /dev/null +++ b/core/assets/sudo_scripts/setup.sh @@ -0,0 +1,102 @@ +#!/bin/bash + +ORIGINAL_FOLDER="${ORIGINAL_FOLDER:-$(pwd)}" +TARGET_FOLDER="${TARGET_FOLDER:-/opt/hydra-veil}" +LINUX_USER="${LINUX_USER:-$SUDO_USER}" + +add_sudoers_rule() { + sudo tee /etc/sudoers.d/zzzzzzzzzzzzzzzzzzz > /dev/null <&2 + exit 1 +fi + +# Validate environment variables +if [[ -z "$ORIGINAL_FOLDER" ]]; then + echo "Error: ORIGINAL_FOLDER environment variable not set" >&2 + exit 1 +fi + +if [[ -z "$TARGET_FOLDER" ]]; then + echo "Error: TARGET_FOLDER environment variable not set" >&2 + exit 1 +fi + +# Validate that source folder exists +if [[ ! -d "$ORIGINAL_FOLDER" ]]; then + echo "Error: ORIGINAL_FOLDER does not exist: $ORIGINAL_FOLDER" >&2 + exit 1 +fi + +# Create target folder if it doesn't exist +if [[ ! -d "$TARGET_FOLDER" ]]; then + echo "Creating target folder: $TARGET_FOLDER" + mkdir -p "$TARGET_FOLDER" +fi + +# Get the setup script's own filename, because we'll exclude it from being copied, +SCRIPT_NAME="$(basename "$0")" + +echo "Starting file copy from $ORIGINAL_FOLDER to $TARGET_FOLDER" +echo "Script name to exclude: $SCRIPT_NAME" +echo "" + +# Counter for tracking progress +COPIED=0 +SKIPPED=0 + +# Loop through all files in source folder +while IFS= read -r -d '' file; do + filename="$(basename "$file")" + + # Skip if this is the script itself + if [[ "$filename" == "$SCRIPT_NAME" ]]; then + echo "⊘ Skipping: $filename (this script)" + ((SKIPPED++)) + continue + fi + + # Copy the file + if cp "$file" "$TARGET_FOLDER/"; then + echo "✓ Copied: $filename" + ((COPIED++)) + + # Set permissions to 755 + if chmod 755 "$TARGET_FOLDER/$filename"; then + echo " └─ chmod 755 applied" + else + echo " └─ WARNING: chmod 755 failed for $filename" >&2 + fi + else + echo "✗ ERROR: Failed to copy $filename" >&2 + ((SKIPPED++)) + fi + +done < <(find "$ORIGINAL_FOLDER" -maxdepth 1 -type f -print0) + +echo "" +echo "==========================================" +echo "Copy complete!" +echo "Files copied: $COPIED" +echo "Files skipped: $SKIPPED" +echo "Target folder: $TARGET_FOLDER" +echo "==========================================" + +# Try to apply the rules, if not emit 1 +if add_sudoers_rule; then + echo "Operation successful" +else + echo "Operation failed - sudoers rule not applied" + exit 1 +fi + +exit 0 + + + diff --git a/core/models/Configuration.py b/core/models/Configuration.py index 9def285..3dfded5 100644 --- a/core/models/Configuration.py +++ b/core/models/Configuration.py @@ -65,6 +65,14 @@ class Configuration: ) ) + did_sudo_setup: Optional[bool] = field( + default=False, + metadata=config( + undefined=dataclasses_json.Undefined.EXCLUDE, + exclude=lambda value: value is None + ) + ) + def save(self: Self): config_file_contents = f'{self.to_json(indent=4)}\n' diff --git a/core/services/helpers/manage_assets.py b/core/services/helpers/manage_assets.py index ff26392..d707b0c 100644 --- a/core/services/helpers/manage_assets.py +++ b/core/services/helpers/manage_assets.py @@ -1,16 +1,28 @@ from core.Constants import Constants from core.errors.logger import logger +# generic +from pathlib import Path import shutil import os def assets_folder_setup(): initial_appimage_assets = f"{Constants.APPDIR_HOME}/assets" current_assets_folder = f"{Constants.HV_DATA_HOME}/assets" - if os.path.exists(current_assets_folder): return True + return copy_folders(initial_appimage_assets, current_assets_folder) + + +def sudo_assets_folder_setup() -> bool: + # initial_appimage_assets = f"{Constants.APPDIR_HOME}/assets/sudo_scripts" + initial_appimage_assets = f"/home/a/VPN/code/DEV/sp-hydra-veil-core/core/assets/sudo_scripts" + current_assets_folder = f"{Constants.HOME}/Downloads/hydraveil_sudo_scripts" + return copy_folders(initial_appimage_assets, current_assets_folder) + + +def copy_folders(initial_appimage_assets: str, current_assets_folder: str) -> bool: try: os.makedirs(current_assets_folder, exist_ok=True) for item in os.listdir(initial_appimage_assets): @@ -20,9 +32,22 @@ def assets_folder_setup(): shutil.copytree(src, dst) else: shutil.copy2(src, dst) + logger.info(f"[MANAGE ASSETS] Successfully copied over the folder.") return True except Exception as e: - logger.error(f"initial_appimage_assets is {initial_appimage_assets}") - logger.error(f"current_assets_folder is {current_assets_folder}") - logger.error(f"ERROR: {e}") + logger.error(f"[MANAGE ASSETS] initial_appimage_assets is {initial_appimage_assets}") + logger.error(f"[MANAGE ASSETS] current_assets_folder is {current_assets_folder}") + logger.error(f"[MANAGE ASSETS] ERROR: {e}") + return False + + +def cleanup_setup_dir(): + """After install completes""" + delete_this_folder = f"{Constants.HOME}/Downloads/hydraveil_sudo_scripts" + try: + shutil.rmtree(delete_this_folder) + logger.info(f"[MANAGE ASSETS] Cleaned up {delete_this_folder}") + return True + except Exception as e: + logger.error(f"[MANAGE ASSETS] We could not delete {delete_this_folder} because {e}") return False diff --git a/core/services/helpers/setup_sudo_scripts.py b/core/services/helpers/setup_sudo_scripts.py new file mode 100644 index 0000000..af8fe20 --- /dev/null +++ b/core/services/helpers/setup_sudo_scripts.py @@ -0,0 +1,67 @@ +from core.services.helpers.manage_assets import sudo_assets_folder_setup +from core.utils.basic_operations.confirm_files_exist import confirm_files_and_folders_exist +from core.models.Result import Result, ResultError +from core.Constants import Constants +from core.errors.logger import logger + +import subprocess +from pathlib import Path +import shutil +import os +from subprocess import CalledProcessError +import copy + +FILES_TO_CHECK = ["setup.sh", "firewall", "dns"] +TARGET_FOLDER = "/opt/hydra-veil" + +def auto_install_sudo_script() -> Result: + setup_dir = Path(f"{Constants.HOME}/Downloads/hydraveil_sudo_scripts") + setup_script = Path(setup_dir, 'setup.sh') + original_linux_user = os.getlogin() + + # Make sure the folder exists + files_exist = confirm_files_and_folders_exist( + folder_path=setup_dir, files_to_check=FILES_TO_CHECK + ) + + if not files_exist: + copied = sudo_assets_folder_setup() + if not copied: + return Result(valid=False, message="Could not copy scripts to begin.") + + try: + result = subprocess.run( + ['pkexec', 'env', f'ORIGINAL_FOLDER={setup_dir}', f'TARGET_FOLDER={TARGET_FOLDER}', f'LINUX_USER={original_linux_user}', 'bash', setup_script], + capture_output=True, + text=True, + ) + if result.returncode == 0: + logger.info("Sudo Setup Ran. Now testing..") + return Result(valid=True) + else: + logger.error(f"Sudo setup setup failed: {result.stderr.strip()}") + return Result(valid=False, message=result.stderr.strip()) + except CalledProcessError as e: + return Result(valid=False, message=e) + except Exception as e: + return Result(valid=False, message=e) + +def test_if_in_sudo_folder() -> Result: + final_sudo_folder = Path("/opt/hydra-veil/") + _files_to_check = FILES_TO_CHECK.copy() # to not affect the other functions. + + # setup script itself doesn't get copied over: + _files_to_check.remove("setup.sh") + + # Now verify it copied them: + sudo_files_copied = confirm_files_and_folders_exist( + folder_path=final_sudo_folder, files_to_check=_files_to_check + ) + + if sudo_files_copied: + return Result(valid=True, message="Sudo Script Setup Completed Successfully!") + else: + error_msg = "We failed to find the files in the sudo folder." + logger.error(error_msg) + return Result(valid=False, message=error_msg) + diff --git a/core/services/networking/systemwide/dns.py b/core/services/networking/systemwide/dns.py index 25ce206..85f9dd5 100644 --- a/core/services/networking/systemwide/dns.py +++ b/core/services/networking/systemwide/dns.py @@ -3,12 +3,10 @@ from core.Constants import Constants from core.models.Result import Result, ResultError from core.errors.logger import logger -# generic import subprocess from subprocess import CalledProcessError -# temp location: -DNS_SCRIPT = f"/opt/hydra-veil/dns_script" +DNS_SCRIPT = f"/opt/hydra-veil/dns" # requires script to have sudo diff --git a/core/services/networking/systemwide/killswitch.py b/core/services/networking/systemwide/killswitch.py index a94ab76..a12318c 100644 --- a/core/services/networking/systemwide/killswitch.py +++ b/core/services/networking/systemwide/killswitch.py @@ -1,15 +1,12 @@ 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 +KILLSWITCH_WRAPPER = "/opt/hydra-veil/firewall" # 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] + cmd = ["sudo", KILLSWITCH_WRAPPER, "arm", server_ip, tunnel_if] if internal_subnet: cmd.append(internal_subnet) result = subprocess.run(cmd, capture_output=True, text=True) @@ -23,7 +20,7 @@ def arm(server_ip: str, tunnel_if: str, internal_subnet: str = None) -> Result: def disarm() -> bool: result = subprocess.run( - ["sudo", TESTING_KILLSWITCH, "disarm"], + ["sudo", KILLSWITCH_WRAPPER, "disarm"], capture_output=True, text=True, ) @@ -37,7 +34,7 @@ def disarm() -> bool: def status() -> bool: result = subprocess.run( - ["sudo", TESTING_KILLSWITCH, "status"], + ["sudo", KILLSWITCH_WRAPPER, "status"], capture_output=True, text=True, ) diff --git a/core/utils/basic_operations/confirm_files_exist.py b/core/utils/basic_operations/confirm_files_exist.py new file mode 100644 index 0000000..27139a9 --- /dev/null +++ b/core/utils/basic_operations/confirm_files_exist.py @@ -0,0 +1,20 @@ +from core.errors.logger import logger +from pathlib import Path + +def confirm_files_and_folders_exist(folder_path, files_to_check): + if folder_path.is_dir(): + logger.info(f"Folder exists: {folder_path}") + + # Check each file + for filename in files_to_check: + file_path = folder_path / filename + if file_path.is_file(): + logger.info(f"✓ {filename} exists") + return True + else: + logger.error(f"✗ {filename} NOT found") + return False + else: + logger.error("Folder does not exist") + return False +