Encrypted proxy killswitch + TUN optimization #1
14 changed files with 27 additions and 941 deletions
|
|
@ -1,102 +1,54 @@
|
|||
class CommandNotFoundError(OSError):
|
||||
|
||||
def __init__(self, subject):
|
||||
|
||||
self.subject = subject
|
||||
super().__init__(f"Command '{subject}' could not be found.")
|
||||
|
||||
|
||||
class UnknownClientPathError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class UnknownClientVersionError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class UnknownConnectionTypeError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class UnknownTimeZoneError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ConnectionTerminationError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class PolicyAssignmentError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class PolicyInstatementError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class PolicyRevocationError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ProfileDeletionError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ProfileModificationError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ProfileStateConflictError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ProfileActivationError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ProfileDeactivationError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class UnsupportedApplicationVersionError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ApplicationAlreadyInstalledError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class MissingLocationError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class MissingSubscriptionError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidSubscriptionError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class InvoiceNotFoundError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class InvoiceExpiredError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class InvoicePaymentFailedError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ConnectionUnprotectedError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class FileIntegrityError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class EndpointVerificationError(Exception):
|
||||
pass
|
||||
class SingboxNotInstalledException(Exception):
|
||||
pass
|
||||
|
|
@ -44,9 +44,10 @@ class ConnectionController:
|
|||
|
||||
@staticmethod
|
||||
def establish_connection(profile: Union[SessionProfile, SystemProfile], ignore: tuple[type[Exception]] = (), connection_observer: Optional[ConnectionObserver] = None):
|
||||
|
||||
from core.controllers.ConnectionController import ConnectionController
|
||||
|
||||
|
||||
ConnectionController.verify_singbox_installation()
|
||||
|
||||
connection = profile.connection
|
||||
|
||||
if connection.needs_proxy_configuration() and not profile.has_proxy_configuration():
|
||||
|
|
@ -543,3 +544,25 @@ class ConnectionController:
|
|||
return True
|
||||
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def verify_singbox_installation():
|
||||
import subprocess
|
||||
import os
|
||||
from core.Errors import SingboxNotInstalledException
|
||||
|
||||
singbox_binary = '/usr/bin/sing-box'
|
||||
singbox_wrapper = '/usr/local/bin/hydraveil-singbox'
|
||||
|
||||
if not os.path.exists(singbox_binary):
|
||||
raise SingboxNotInstalledException(f'sing-box binary not found at {singbox_binary}')
|
||||
|
||||
if not os.path.exists(singbox_wrapper):
|
||||
raise SingboxNotInstalledException(f'sing-box wrapper not found at {singbox_wrapper}')
|
||||
|
||||
try:
|
||||
subprocess.run([singbox_binary, 'version'], capture_output=True, timeout=5, check=True)
|
||||
except subprocess.CalledProcessError:
|
||||
raise SingboxNotInstalledException('sing-box binary check failed')
|
||||
except subprocess.TimeoutExpired:
|
||||
raise SingboxNotInstalledException('sing-box verification timeout')
|
||||
98
install.sh
98
install.sh
|
|
@ -1,98 +0,0 @@
|
|||
#!/bin/bash
|
||||
# install.sh — hydra-veil core installer
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
LIB_DIR="$SCRIPT_DIR/lib"
|
||||
|
||||
trap 'echo ""; echo " [ERROR] Installer failed at line $LINENO — check: cat $LOG_FILE" >&2' ERR
|
||||
|
||||
for _lib in ui log input network steps; do
|
||||
_path="$LIB_DIR/${_lib}.sh"
|
||||
if [ ! -f "$_path" ]; then
|
||||
echo "[ERROR] Missing lib: $_path" >&2
|
||||
exit 1
|
||||
fi
|
||||
source "$_path"
|
||||
done
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--no-log) LOG_ENABLED="false" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
_log_init
|
||||
|
||||
clear
|
||||
print_header "HYDRA-VEIL CORE INSTALLER" "v2.0.0"
|
||||
printf " ${DIM}%s${RESET}\n" "Simplified Privacy"
|
||||
printf " ${DIM}%s${RESET}\n" "$(date '+%Y-%m-%d %H:%M:%S') — $(uname -n)"
|
||||
print_divider
|
||||
|
||||
print_section "Pre-flight"
|
||||
log_info "User: ${USER}"
|
||||
log_info "Home: ${HOME}"
|
||||
log_info "Install dir: ${HV_BASE}"
|
||||
require_internet
|
||||
print_divider
|
||||
|
||||
echo ""
|
||||
label_info "hydra-veil se instalará en:"
|
||||
printf " ${BCYAN}%s${RESET}\n\n" "$HV_BASE"
|
||||
label_warn "sudo es requerido para sing-box, wrapper y sudoers."
|
||||
echo ""
|
||||
if ! ask_confirm "Continuar con la instalación?"; then
|
||||
log_warn "Instalación cancelada."
|
||||
exit 0
|
||||
fi
|
||||
echo ""
|
||||
print_divider
|
||||
|
||||
step_preflight
|
||||
step_repos
|
||||
step_python_env
|
||||
step_singbox
|
||||
step_wrapper
|
||||
step_sudoers
|
||||
|
||||
echo ""
|
||||
print_divider
|
||||
printf "${BGREEN}"
|
||||
cat << 'EOF'
|
||||
_
|
||||
_ _|_| _
|
||||
| \_ _| | _/ |
|
||||
| \__|___|__/ |
|
||||
| | _ _ | |
|
||||
| || | | || |
|
||||
\_ || | | || _/
|
||||
\_||_| |_||_/
|
||||
______| |______
|
||||
_/ \_
|
||||
_/ S I M P L I F I E D \_
|
||||
_/ P R I V A C Y \_
|
||||
| _____ _____ |
|
||||
| _/ |_ _| \_ |
|
||||
|_/ |_ _| \_|
|
||||
\_____/
|
||||
|
||||
EOF
|
||||
printf "${RESET}"
|
||||
print_divider
|
||||
echo ""
|
||||
label_ok "hydra-veil instalado correctamente"
|
||||
label_ok "Todos los steps completados"
|
||||
echo ""
|
||||
label_info "Activar entorno:"
|
||||
printf " ${CYAN}source %s/core/.venv/bin/activate${RESET}\n" "$HV_BASE"
|
||||
echo ""
|
||||
label_info "CLI disponible:"
|
||||
printf " ${CYAN}python -m cli${RESET}\n"
|
||||
echo ""
|
||||
label_info "Wrapper disponible:"
|
||||
printf " ${CYAN}sudo /usr/local/bin/hydraveil-singbox <config.json> start${RESET}\n"
|
||||
echo ""
|
||||
log_file_path
|
||||
echo ""
|
||||
print_divider
|
||||
echo ""
|
||||
32
lib/input.sh
32
lib/input.sh
|
|
@ -1,32 +0,0 @@
|
|||
#!/bin/bash
|
||||
ask_input() {
|
||||
local label="$1"
|
||||
local default="${2:-}"
|
||||
local prompt_default=""
|
||||
[ -n "$default" ] && prompt_default=" ${DIM}[${default}]${RESET}"
|
||||
|
||||
printf " ${BCYAN}?${RESET} ${BWHITE}%s${RESET}%b " "$label" "$prompt_default"
|
||||
read -r INPUT_RESULT
|
||||
[ -z "$INPUT_RESULT" ] && INPUT_RESULT="$default"
|
||||
}
|
||||
|
||||
ask_confirm() {
|
||||
local label="$1"
|
||||
local default="${2:-y}"
|
||||
local hint
|
||||
if [ "$default" = "y" ]; then hint="${BGREEN}Y${RESET}${DIM}/n${RESET}"; else hint="${DIM}y/${RESET}${BRED}N${RESET}"; fi
|
||||
|
||||
printf " ${BYELLOW}?${RESET} ${BWHITE}%s${RESET} [%b] " "$label" "$hint"
|
||||
read -r _confirm
|
||||
_confirm="${_confirm:-$default}"
|
||||
case "${_confirm,,}" in
|
||||
y|yes) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
ask_pause() {
|
||||
local msg="${1:-Press ENTER to continue...}"
|
||||
printf " ${DIM}%s${RESET}" "$msg"
|
||||
read -r
|
||||
}
|
||||
52
lib/log.sh
52
lib/log.sh
|
|
@ -1,52 +0,0 @@
|
|||
#!/bin/bash
|
||||
LOG_FILE="${LOG_FILE:-/tmp/hydra-veil-core.log}"
|
||||
LOG_ENABLED="${LOG_ENABLED:-true}"
|
||||
|
||||
_log_init() {
|
||||
if [ "$LOG_ENABLED" = "true" ]; then
|
||||
mkdir -p "$(dirname "$LOG_FILE")"
|
||||
echo "──────────────────────────────────────────" >> "$LOG_FILE"
|
||||
echo " LOG START: $(date '+%Y-%m-%d %H:%M:%S')" >> "$LOG_FILE"
|
||||
echo "──────────────────────────────────────────" >> "$LOG_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
_log_write() {
|
||||
local level="$1"
|
||||
local msg="$2"
|
||||
if [ "$LOG_ENABLED" = "true" ]; then
|
||||
printf "[%s] [%s] %s\n" "$(date '+%H:%M:%S')" "$level" "$msg" >> "$LOG_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
log_info() {
|
||||
label_info "$1"
|
||||
_log_write "INFO " "$1"
|
||||
}
|
||||
|
||||
log_ok() {
|
||||
label_ok "$1"
|
||||
_log_write "OK " "$1"
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
label_warn "$1"
|
||||
_log_write "WARN " "$1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
label_error "$1"
|
||||
_log_write "ERROR" "$1"
|
||||
}
|
||||
|
||||
log_step() {
|
||||
local msg="$1"
|
||||
local current="$2"
|
||||
local total="$3"
|
||||
label_step "$msg" "$current" "$total"
|
||||
_log_write "STEP " "[$current/$total] $msg"
|
||||
}
|
||||
|
||||
log_file_path() {
|
||||
printf " ${DIM}Log file: ${CYAN}%s${RESET}\n" "$LOG_FILE"
|
||||
}
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
#!/bin/bash
|
||||
NET_CHECK_HOST="${NET_CHECK_HOST:-8.8.8.8}"
|
||||
NET_CHECK_TIMEOUT="${NET_CHECK_TIMEOUT:-3}"
|
||||
|
||||
check_internet() {
|
||||
local host="${1:-$NET_CHECK_HOST}"
|
||||
if ping -c 1 -W "$NET_CHECK_TIMEOUT" "$host" &>/dev/null; then
|
||||
label_connected "Internet reachable ${DIM}(${host})${RESET}"
|
||||
_log_write "NET " "Connected: $host"
|
||||
return 0
|
||||
else
|
||||
label_disconnect "No internet connection ${DIM}(${host})${RESET}"
|
||||
_log_write "NET " "Disconnected: $host"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
check_host() {
|
||||
local host="$1"
|
||||
local label="${2:-$host}"
|
||||
if ping -c 1 -W "$NET_CHECK_TIMEOUT" "$host" &>/dev/null 2>&1; then
|
||||
label_connected "$label"
|
||||
return 0
|
||||
else
|
||||
label_disconnect "$label"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
check_url() {
|
||||
local url="$1"
|
||||
local label="${2:-$url}"
|
||||
local http_code
|
||||
http_code=$(curl -s -o /dev/null -w "%{http_code}" \
|
||||
--max-time "$NET_CHECK_TIMEOUT" "$url" 2>/dev/null || echo "000")
|
||||
|
||||
if [[ "$http_code" =~ ^[23] ]]; then
|
||||
label_connected "$label ${DIM}(HTTP ${http_code})${RESET}"
|
||||
_log_write "NET " "URL OK [$http_code]: $url"
|
||||
return 0
|
||||
else
|
||||
label_disconnect "$label ${DIM}(HTTP ${http_code})${RESET}"
|
||||
_log_write "NET " "URL FAIL [$http_code]: $url"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
require_internet() {
|
||||
if ! check_internet; then
|
||||
log_error "Internet connection required. Aborting."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
57
lib/steps.sh
57
lib/steps.sh
|
|
@ -1,57 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
TOTAL_STEPS=6
|
||||
|
||||
HV_BASE="$HOME/.local/share/hydra-veil"
|
||||
HV_CORE="$HV_BASE/core"
|
||||
HV_CLI="$HV_BASE/cli"
|
||||
HV_GUI="$HV_BASE/gui"
|
||||
HV_ESSENTIALS="$HV_BASE/essentials"
|
||||
HV_VENV="$HV_CORE/.venv"
|
||||
|
||||
INSTALL_DIR="$HOME"
|
||||
HV_DATA_DIR="$HOME/data/hydra-veil"
|
||||
HV_CONFIG_DIR="$HOME/.config/hydra-veil"
|
||||
HV_CACHE_DIR="$HOME/.cache/hydra-veil"
|
||||
HV_STATE_DIR="$HOME/.state/hydra-veil"
|
||||
|
||||
SINGBOX_BIN="/usr/bin/sing-box"
|
||||
SINGBOX_WRAPPER="/usr/local/bin/hydraveil-singbox"
|
||||
SINGBOX_VERSION="1.13.5"
|
||||
SINGBOX_CONFIG_DIR="$HV_DATA_DIR/configs"
|
||||
SINGBOX_PID_FILE="$HV_DATA_DIR/runtime/singbox.pid"
|
||||
SINGBOX_LOG_FILE="$HV_DATA_DIR/runtime/singbox.log"
|
||||
|
||||
SUDOERS_PATH="/etc/sudoers.d/hydraveil"
|
||||
|
||||
declare -A REPO_URLS=(
|
||||
["core"]="https://git.simplifiedprivacy.com/Support/sp-hydra-veil-core"
|
||||
["cli"]="https://git.simplifiedprivacy.com/Support/sp-hydra-veil-cli"
|
||||
["essentials"]="https://git.simplifiedprivacy.com/Support/sp-essentials"
|
||||
["gui"]="https://git.simplifiedprivacy.com/Support/sp-hydra-veil-gui"
|
||||
)
|
||||
|
||||
declare -A REPO_BRANCHES=(
|
||||
["core"]="deploy"
|
||||
["cli"]="zenaku"
|
||||
["essentials"]="master"
|
||||
["gui"]="master"
|
||||
)
|
||||
|
||||
declare -A REPO_DIRS=(
|
||||
["core"]="$HV_CORE"
|
||||
["cli"]="$HV_CLI"
|
||||
["essentials"]="$HV_ESSENTIALS"
|
||||
["gui"]="$HV_GUI"
|
||||
)
|
||||
|
||||
_STEPS_DIR="$SCRIPT_DIR/lib/steps"
|
||||
|
||||
for _step in preflight repos python_env singbox wrapper sudoers; do
|
||||
_step_path="${_STEPS_DIR}/${_step}.sh"
|
||||
if [ ! -f "$_step_path" ]; then
|
||||
echo "[ERROR] Missing step: $_step_path" >&2
|
||||
exit 1
|
||||
fi
|
||||
source "$_step_path"
|
||||
done
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
#!/bin/bash
|
||||
# lib/steps/preflight.sh — verificaciones previas
|
||||
step_preflight() {
|
||||
print_section "Pre-flight"
|
||||
log_step "System checks" 1 $TOTAL_STEPS
|
||||
|
||||
_preflight_user
|
||||
_preflight_python
|
||||
_preflight_git
|
||||
}
|
||||
|
||||
_preflight_user() {
|
||||
log_info "Running as: ${USER} (uid=$(id -u))"
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
log_error "No correr el installer como root"
|
||||
exit 1
|
||||
fi
|
||||
log_ok "User OK"
|
||||
}
|
||||
|
||||
_preflight_python() {
|
||||
local found=""
|
||||
for candidate in python3.12 python3.13; do
|
||||
if command -v "$candidate" &>/dev/null; then
|
||||
found="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ -z "$found" ]; then
|
||||
log_error "Se necesita Python 3.12 o 3.13"
|
||||
log_info "Arch: sudo pacman -S python312"
|
||||
log_info "Ubuntu: sudo apt install python3.12"
|
||||
exit 1
|
||||
fi
|
||||
PYTHON_BIN="$found"
|
||||
log_ok "Python: $($PYTHON_BIN --version 2>&1)"
|
||||
}
|
||||
|
||||
_preflight_git() {
|
||||
if ! command -v git &>/dev/null; then
|
||||
log_error "git no encontrado"
|
||||
exit 1
|
||||
fi
|
||||
log_ok "git: $(git --version)"
|
||||
}
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
#!/bin/bash
|
||||
# lib/steps/python_env.sh — venv + pip install editable
|
||||
|
||||
step_python_env() {
|
||||
print_section "Python Environment"
|
||||
log_step "Virtual environment + dependencies" 3 $TOTAL_STEPS
|
||||
|
||||
_create_venv
|
||||
_pip_install
|
||||
}
|
||||
|
||||
_create_venv() {
|
||||
if [ -d "$HV_VENV" ]; then
|
||||
log_warn "venv ya existe: ${HV_VENV}"
|
||||
if ask_confirm "Recrear venv?"; then
|
||||
rm -rf "$HV_VENV"
|
||||
else
|
||||
log_skip "venv existente conservado"
|
||||
source "$HV_VENV/bin/activate"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
start_spinner "Creando virtual environment (${PYTHON_BIN})..."
|
||||
"$PYTHON_BIN" -m venv "$HV_VENV"
|
||||
stop_spinner "venv creado → ${HV_VENV}"
|
||||
|
||||
source "$HV_VENV/bin/activate"
|
||||
|
||||
start_spinner "Actualizando pip..."
|
||||
pip install --quiet --upgrade pip
|
||||
stop_spinner "pip actualizado"
|
||||
}
|
||||
|
||||
_pip_install() {
|
||||
source "$HV_VENV/bin/activate"
|
||||
|
||||
if [ -f "$HV_GUI/pyproject.toml" ]; then
|
||||
sed -i 's/sp-hydra-veil-core == [0-9.]*/sp-hydra-veil-core >= 2.3.0/' \
|
||||
"$HV_GUI/pyproject.toml" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
for name in essentials core cli gui; do
|
||||
local dir
|
||||
case "$name" in
|
||||
essentials) dir="$HV_ESSENTIALS" ;;
|
||||
core) dir="$HV_CORE" ;;
|
||||
cli) dir="$HV_CLI" ;;
|
||||
gui) dir="$HV_GUI" ;;
|
||||
esac
|
||||
|
||||
if [ ! -d "$dir" ]; then
|
||||
log_warn "${name} no encontrado en ${dir} — omitiendo"
|
||||
continue
|
||||
fi
|
||||
|
||||
start_spinner "Instalando ${name} (editable)..."
|
||||
if pip install --quiet -e "$dir" 2>/tmp/pip_err_${name}; then
|
||||
stop_spinner "${name} instalado"
|
||||
else
|
||||
stop_spinner
|
||||
log_error "pip install falló para ${name}:"
|
||||
tail -5 /tmp/pip_err_${name}
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
step_repos() {
|
||||
print_section "Repositories"
|
||||
log_step "Cloning repositories & configuring .env" 2 $TOTAL_STEPS
|
||||
|
||||
mkdir -p "$HV_BASE"
|
||||
chmod 700 "$HV_BASE"
|
||||
|
||||
_clone_repos
|
||||
_setup_env
|
||||
}
|
||||
|
||||
_clone_repos() {
|
||||
for name in essentials cli gui core; do
|
||||
local url="${REPO_URLS[$name]}"
|
||||
local branch="${REPO_BRANCHES[$name]}"
|
||||
local dest="${REPO_DIRS[$name]}"
|
||||
log_ok "HIJUEPUTA"
|
||||
|
||||
if [ -d "$dest" ]; then
|
||||
log_warn "Repo ya existe: ${dest}"
|
||||
rm -rf "$dest"
|
||||
fi
|
||||
|
||||
if ! git clone --branch "$branch" "$url" "$dest" 2>&1; then
|
||||
log_error "git clone FAILED: $name"
|
||||
log_error " URL: $url"
|
||||
log_error " Branch: $branch"
|
||||
log_error " Dest: $dest"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
chmod 700 "$dest"
|
||||
log_ok "${name} clonado → ${dest}"
|
||||
done
|
||||
}
|
||||
|
||||
_setup_env() {
|
||||
local env_file="$HV_CORE/.env"
|
||||
local example_file="$HV_CORE/.env.example"
|
||||
|
||||
if [ ! -f "$example_file" ]; then
|
||||
log_error ".env.example no encontrado: ${example_file}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -f "$env_file" ]; then
|
||||
log_warn ".env ya existe — sobreescribiendo"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
printf " ${DIM}Fill in the variables. ENTER = leave empty.${RESET}\n"
|
||||
print_divider
|
||||
echo ""
|
||||
|
||||
select_option \
|
||||
"APP_ENV — application environment" \
|
||||
"local development / testing" \
|
||||
"production production / real domain"
|
||||
|
||||
case "$SELECT_RESULT" in
|
||||
0) _app_env="local" ;;
|
||||
1) _app_env="production" ;;
|
||||
*) _app_env="production" ;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
printf " ${BCYAN}┌─${RESET} ${BWHITE}SP_API_BASE_URL_PRODUCTION${RESET} ${DIM}e.g. https://fake.simplifiedprivacy.org/api/v1${RESET}\n"
|
||||
printf " ${BCYAN}›${RESET} "
|
||||
read -r _api_url < /dev/tty
|
||||
|
||||
cat > "$env_file" << ENVEOF
|
||||
APP_ENV=${_app_env}
|
||||
SP_API_BASE_URL_LOCAL=http://simplifiedprivacy.test/api/v1
|
||||
SP_API_BASE_URL_PRODUCTION=${_api_url}
|
||||
HV_CORE_HOME=${HV_CORE}
|
||||
INSTALL_DIR=${HOME}
|
||||
CORE_DIR=${HV_CORE}
|
||||
ENVEOF
|
||||
|
||||
chmod 600 "$env_file"
|
||||
|
||||
log_ok "APP_ENV=${_app_env}"
|
||||
[ -n "$_api_url" ] && log_ok "SP_API_BASE_URL_PRODUCTION=${_api_url}" || label_skip "SP_API_BASE_URL_PRODUCTION — vacío"
|
||||
log_ok "INSTALL_DIR=${HOME}"
|
||||
log_ok ".env escrito → ${env_file}"
|
||||
}
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
#!/bin/bash
|
||||
# lib/steps/singbox.sh — instalar/verificar sing-box
|
||||
|
||||
step_singbox() {
|
||||
print_section "sing-box"
|
||||
log_step "Checking sing-box binary" 4 $TOTAL_STEPS
|
||||
|
||||
local found=""
|
||||
for candidate in /usr/bin/sing-box /usr/local/bin/sing-box /bin/sing-box; do
|
||||
[ -x "$candidate" ] && found="$candidate" && break
|
||||
done
|
||||
[ -z "$found" ] && found=$(command -v sing-box 2>/dev/null || true)
|
||||
|
||||
if [ -n "$found" ]; then
|
||||
local ver
|
||||
ver=$("$found" version 2>/dev/null | head -1 || echo "unknown")
|
||||
log_ok "Encontrado: ${found} (${ver})"
|
||||
if [ "$found" != "$SINGBOX_BIN" ]; then
|
||||
sudo ln -sf "$found" "$SINGBOX_BIN"
|
||||
log_ok "Symlink → ${SINGBOX_BIN}"
|
||||
fi
|
||||
else
|
||||
_install_singbox
|
||||
fi
|
||||
|
||||
# setcap — sing-box crea TUN sin necesitar root completo
|
||||
if command -v setcap &>/dev/null; then
|
||||
sudo setcap cap_net_admin,cap_net_raw+ep "$SINGBOX_BIN"
|
||||
log_ok "setcap cap_net_admin,cap_net_raw+ep → ${SINGBOX_BIN}"
|
||||
else
|
||||
log_warn "setcap no disponible — sing-box necesitará sudo para TUN"
|
||||
fi
|
||||
|
||||
log_ok "sing-box listo → ${SINGBOX_BIN}"
|
||||
}
|
||||
|
||||
_install_singbox() {
|
||||
log_info "sing-box no encontrado — instalando v${SINGBOX_VERSION}..."
|
||||
|
||||
local arch
|
||||
arch=$(uname -m)
|
||||
case "$arch" in
|
||||
x86_64) arch="amd64" ;;
|
||||
aarch64) arch="arm64" ;;
|
||||
*)
|
||||
log_error "Arquitectura no soportada: ${arch}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
local url="https://github.com/SagerNet/sing-box/releases/download/v${SINGBOX_VERSION}/sing-box-${SINGBOX_VERSION}-linux-${arch}.tar.gz"
|
||||
local tmp_tar="/tmp/sing-box-$$.tar.gz"
|
||||
local tmp_dir="/tmp/sing-box-$$"
|
||||
|
||||
start_spinner "Descargando sing-box v${SINGBOX_VERSION}..."
|
||||
if wget -q --timeout=60 "$url" -O "$tmp_tar"; then
|
||||
stop_spinner "Descarga completa"
|
||||
else
|
||||
stop_spinner
|
||||
rm -f "$tmp_tar"
|
||||
log_error "Descarga falló: ${url}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
start_spinner "Extrayendo..."
|
||||
mkdir -p "$tmp_dir"
|
||||
tar -xzf "$tmp_tar" -C "$tmp_dir" --strip-components=1
|
||||
stop_spinner "Extraído"
|
||||
|
||||
sudo cp "$tmp_dir/sing-box" "$SINGBOX_BIN"
|
||||
sudo chmod 755 "$SINGBOX_BIN"
|
||||
rm -rf "$tmp_tar" "$tmp_dir"
|
||||
log_ok "sing-box instalado: $($SINGBOX_BIN version | head -1)"
|
||||
}
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
#!/bin/bash
|
||||
# lib/steps/sudoers.sh — sudoers + directorios XDG
|
||||
|
||||
step_sudoers() {
|
||||
print_section "Permissions & Directories"
|
||||
log_step "sudoers + XDG directories" 6 $TOTAL_STEPS
|
||||
|
||||
_setup_sudoers
|
||||
_create_directories
|
||||
}
|
||||
|
||||
_setup_sudoers() {
|
||||
local tmp
|
||||
tmp=$(mktemp)
|
||||
|
||||
cat > "$tmp" << EOF
|
||||
# hydraveil — generado por installer $(date '+%Y-%m-%d')
|
||||
# NO editar manualmente
|
||||
Defaults!${SINGBOX_WRAPPER} !requiretty
|
||||
${USER} ALL=(root) NOPASSWD: ${SINGBOX_WRAPPER}
|
||||
EOF
|
||||
|
||||
if ! sudo visudo -c -f "$tmp" &>/dev/null; then
|
||||
rm -f "$tmp"
|
||||
log_error "sudoers inválido — abortando para evitar lockout"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sudo cp "$tmp" "$SUDOERS_PATH"
|
||||
sudo chmod 440 "$SUDOERS_PATH"
|
||||
rm -f "$tmp"
|
||||
log_ok "sudoers instalado → ${SUDOERS_PATH}"
|
||||
}
|
||||
|
||||
_create_directories() {
|
||||
local -a dirs=(
|
||||
# XDG data
|
||||
"$HV_DATA_DIR/configs"
|
||||
"$HV_DATA_DIR/runtime"
|
||||
"$HV_DATA_DIR/applications"
|
||||
"$HV_DATA_DIR/profiles"
|
||||
"$HV_DATA_DIR/ticket_data"
|
||||
"$HV_DATA_DIR/incidents"
|
||||
# XDG config
|
||||
"$HV_CONFIG_DIR/profiles"
|
||||
"$HV_CONFIG_DIR/ticketing"
|
||||
# XDG state
|
||||
"$HV_STATE_DIR/sessions"
|
||||
"$HV_STATE_DIR/tor"
|
||||
# XDG cache
|
||||
"$HV_CACHE_DIR"
|
||||
)
|
||||
|
||||
for dir in "${dirs[@]}"; do
|
||||
mkdir -p "$dir"
|
||||
chmod 700 "$dir"
|
||||
log_ok "→ ${dir}"
|
||||
done
|
||||
|
||||
# Log file con permisos de usuario ANTES de que el wrapper lo toque
|
||||
local log_file="$SINGBOX_LOG_FILE"
|
||||
if [ ! -f "$log_file" ]; then
|
||||
touch "$log_file"
|
||||
chmod 600 "$log_file"
|
||||
log_ok "Log file creado → ${log_file}"
|
||||
else
|
||||
log_ok "Log file ya existe → ${log_file}"
|
||||
fi
|
||||
}
|
||||
|
|
@ -1,140 +0,0 @@
|
|||
#!/bin/bash
|
||||
# lib/steps/wrapper.sh — generar wrapper de privilegios para sing-box
|
||||
|
||||
step_wrapper() {
|
||||
print_section "Security Wrapper"
|
||||
log_step "Installing hydraveil-singbox wrapper" 5 $TOTAL_STEPS
|
||||
|
||||
[ -f "$SINGBOX_WRAPPER" ] && sudo rm -f "$SINGBOX_WRAPPER" && log_ok "Wrapper anterior eliminado"
|
||||
|
||||
sudo tee "$SINGBOX_WRAPPER" > /dev/null << WRAPPER
|
||||
#!/bin/bash
|
||||
# hydraveil-singbox — wrapper de privilegios para sing-box
|
||||
# Generado por el installer. NO editar manualmente.
|
||||
# Solo este binario tiene NOPASSWD en sudoers.
|
||||
#
|
||||
# Uso:
|
||||
# sudo hydraveil-singbox <config.json> start
|
||||
# sudo hydraveil-singbox "" stop
|
||||
# sudo hydraveil-singbox "" log
|
||||
set -euo pipefail
|
||||
|
||||
CONFIG="\${1:-}"
|
||||
ACTION="\${2:-}"
|
||||
|
||||
# ── Paths fijos (rutas XDG correctas — INSTALL_DIR=\$HOME) ────────────────────
|
||||
SINGBOX_BIN="${SINGBOX_BIN}"
|
||||
PID_FILE="${SINGBOX_PID_FILE}"
|
||||
LOG_FILE="${SINGBOX_LOG_FILE}"
|
||||
CONFIG_DIR="${SINGBOX_CONFIG_DIR}"
|
||||
STOP_TIMEOUT=8
|
||||
|
||||
# ── Validar acción ────────────────────────────────────────────────────────────
|
||||
if [[ "\$ACTION" != "start" && "\$ACTION" != "stop" && "\$ACTION" != "log" ]]; then
|
||||
echo "Error: acción inválida '\$ACTION'. Uso: start|stop|log" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
_pid_running() {
|
||||
local pid="\$1"
|
||||
[[ "\$pid" =~ ^[0-9]+$ ]] && kill -0 "\$pid" 2>/dev/null
|
||||
}
|
||||
|
||||
_delete_tun() {
|
||||
if ip link show tun0 &>/dev/null 2>&1; then
|
||||
ip link delete tun0 2>/dev/null || true
|
||||
echo "[wrapper] tun0 eliminada" >> "\$LOG_FILE" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
_kill_by_pidfile() {
|
||||
[ -f "\$PID_FILE" ] || return 0
|
||||
local pid
|
||||
pid=\$(cat "\$PID_FILE" 2>/dev/null || echo "")
|
||||
if ! _pid_running "\$pid"; then
|
||||
rm -f "\$PID_FILE"
|
||||
return 0
|
||||
fi
|
||||
kill -TERM "\$pid" 2>/dev/null || true
|
||||
echo "[wrapper] SIGTERM → PID \$pid" >> "\$LOG_FILE" 2>/dev/null || true
|
||||
local elapsed=0
|
||||
while _pid_running "\$pid" && (( elapsed < STOP_TIMEOUT * 2 )); do
|
||||
sleep 0.5
|
||||
(( elapsed++ )) || true
|
||||
done
|
||||
if _pid_running "\$pid"; then
|
||||
kill -KILL "\$pid" 2>/dev/null || true
|
||||
echo "[wrapper] SIGKILL → PID \$pid" >> "\$LOG_FILE" 2>/dev/null || true
|
||||
sleep 0.3
|
||||
fi
|
||||
rm -f "\$PID_FILE"
|
||||
}
|
||||
|
||||
# ── log ───────────────────────────────────────────────────────────────────────
|
||||
if [[ "\$ACTION" == "log" ]]; then
|
||||
[ -f "\$LOG_FILE" ] && tail -80 "\$LOG_FILE" || echo "Sin logs disponibles"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── stop ──────────────────────────────────────────────────────────────────────
|
||||
if [[ "\$ACTION" == "stop" ]]; then
|
||||
_kill_by_pidfile
|
||||
_delete_tun
|
||||
echo "[wrapper] sing-box detenido — \$(date '+%Y-%m-%d %H:%M:%S')" >> "\$LOG_FILE" 2>/dev/null || true
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── start ─────────────────────────────────────────────────────────────────────
|
||||
[[ -z "\$CONFIG" ]] && { echo "Error: config requerido para start" >&2; exit 1; }
|
||||
[[ ! -f "\$CONFIG" ]] && { echo "Error: config no encontrado: \$CONFIG" >&2; exit 1; }
|
||||
[[ ! -r "\$CONFIG" ]] && { echo "Error: config no legible: \$CONFIG" >&2; exit 1; }
|
||||
|
||||
_real_config=\$(realpath "\$CONFIG")
|
||||
_real_config_dir=\$(realpath "\$CONFIG_DIR")
|
||||
if [[ "\$_real_config" != "\$_real_config_dir"/* ]]; then
|
||||
echo "Error: config fuera del directorio permitido" >&2
|
||||
echo " Config: \$_real_config" >&2
|
||||
echo " Permitido: \$_real_config_dir" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! python3 -c "import json,sys; json.load(open(sys.argv[1]))" "\$CONFIG" 2>/dev/null; then
|
||||
echo "Error: JSON inválido: \$CONFIG" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
[[ ! -x "\$SINGBOX_BIN" ]] && { echo "Error: sing-box no encontrado: \$SINGBOX_BIN" >&2; exit 1; }
|
||||
|
||||
_kill_by_pidfile
|
||||
_delete_tun
|
||||
|
||||
mkdir -p "\$(dirname "\$PID_FILE")" && chmod 700 "\$(dirname "\$PID_FILE")"
|
||||
mkdir -p "\$(dirname "\$LOG_FILE")" && chmod 700 "\$(dirname "\$LOG_FILE")"
|
||||
|
||||
"\$SINGBOX_BIN" run -c "\$CONFIG" >> "\$LOG_FILE" 2>&1 &
|
||||
_new_pid=\$!
|
||||
|
||||
sleep 0.3
|
||||
if ! _pid_running "\$_new_pid"; then
|
||||
echo "Error: sing-box terminó inmediatamente. Ver: \$LOG_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "\$_new_pid" > "\$PID_FILE"
|
||||
chmod 600 "\$PID_FILE"
|
||||
echo "[wrapper] sing-box iniciado — PID \$_new_pid — \$(date '+%Y-%m-%d %H:%M:%S')" >> "\$LOG_FILE"
|
||||
echo "[wrapper] config: \$_real_config" >> "\$LOG_FILE"
|
||||
exit 0
|
||||
WRAPPER
|
||||
|
||||
sudo chmod 755 "$SINGBOX_WRAPPER"
|
||||
log_ok "Wrapper instalado → ${SINGBOX_WRAPPER}"
|
||||
|
||||
# Verificar que el wrapper funciona
|
||||
if sudo "$SINGBOX_WRAPPER" "" stop &>/dev/null; then
|
||||
log_ok "Wrapper verificado (stop OK)"
|
||||
else
|
||||
log_warn "Wrapper instalado pero verificación falló — revisar manualmente"
|
||||
fi
|
||||
}
|
||||
114
lib/ui.sh
114
lib/ui.sh
|
|
@ -1,114 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
RESET="\033[0m"
|
||||
BOLD="\033[1m"
|
||||
DIM="\033[2m"
|
||||
|
||||
BLACK="\033[0;30m"
|
||||
RED="\033[0;31m"
|
||||
GREEN="\033[0;32m"
|
||||
YELLOW="\033[0;33m"
|
||||
BLUE="\033[0;34m"
|
||||
CYAN="\033[0;36m"
|
||||
WHITE="\033[0;37m"
|
||||
|
||||
BRED="\033[1;31m"
|
||||
BGREEN="\033[1;32m"
|
||||
BYELLOW="\033[1;33m"
|
||||
BBLUE="\033[1;34m"
|
||||
BCYAN="\033[1;36m"
|
||||
BWHITE="\033[1;37m"
|
||||
|
||||
print_header() {
|
||||
local title="${1:-INSTALLER}"
|
||||
local version="${2:-}"
|
||||
local width=60
|
||||
local line
|
||||
printf -v line '%*s' "$width" ''
|
||||
line="${line// /─}"
|
||||
|
||||
echo ""
|
||||
printf "${BCYAN}${line}${RESET}\n"
|
||||
printf "${BCYAN} %-56s${RESET}\n" ""
|
||||
printf "${BCYAN} ${BWHITE}%-54s${BCYAN} ${RESET}\n" "$title ${DIM}${version}${RESET}"
|
||||
printf "${BCYAN} %-56s${RESET}\n" ""
|
||||
printf "${BCYAN}${line}${RESET}\n"
|
||||
echo ""
|
||||
}
|
||||
|
||||
print_section() {
|
||||
local label="$1"
|
||||
echo ""
|
||||
printf "${DIM}${CYAN}┌─${RESET} ${BWHITE}${label}${RESET}\n"
|
||||
}
|
||||
|
||||
print_divider() {
|
||||
printf "${DIM}────────────────────────────────────────────────────────────${RESET}\n"
|
||||
}
|
||||
|
||||
label_ok() { printf " ${BGREEN}[ OK ]${RESET} %s\n" "$1"; }
|
||||
label_warn() { printf " ${BYELLOW}[ WARN ]${RESET} %s\n" "$1"; }
|
||||
label_error() { printf " ${BRED}[ ERROR]${RESET} %s\n" "$1"; }
|
||||
label_info() { printf " ${BCYAN}[ INFO ]${RESET} %s\n" "$1"; }
|
||||
label_skip() { printf " ${DIM}[ SKIP ]${RESET} %s\n" "$1"; }
|
||||
label_simple() { printf " ${DIM}[SIMPLE]${RESET} %s\n" "$1"; }
|
||||
label_connected() { printf " ${BGREEN}[CONNECTED ]${RESET} %s\n" "$1"; }
|
||||
label_disconnect() { printf " ${BRED}[DISCONNECTED]${RESET} %s\n" "$1"; }
|
||||
label_step() { printf " ${BBLUE}[ %-2s/%-2s ]${RESET} %s\n" "$2" "$3" "$1"; }
|
||||
|
||||
progress_bar() {
|
||||
local current=$1
|
||||
local total=$2
|
||||
local label="${3:-}"
|
||||
local width=40
|
||||
local filled=$(( current * width / total ))
|
||||
local empty=$(( width - filled ))
|
||||
local pct=$(( current * 100 / total ))
|
||||
|
||||
local bar_filled bar_empty
|
||||
printf -v bar_filled '%*s' "$filled" ''
|
||||
printf -v bar_empty '%*s' "$empty" ''
|
||||
bar_filled="${bar_filled// /#}"
|
||||
bar_empty="${bar_empty// /·}"
|
||||
|
||||
printf "\r ${DIM}[${RESET}${BGREEN}${bar_filled}${RESET}${DIM}${bar_empty}${RESET}${DIM}]${RESET} ${BWHITE}%3d%%${RESET} ${DIM}%s${RESET} " \
|
||||
"$pct" "$label"
|
||||
}
|
||||
|
||||
progress_simulate() {
|
||||
local steps="${1:-20}"
|
||||
local label="${2:-loading}"
|
||||
for i in $(seq 1 "$steps"); do
|
||||
progress_bar "$i" "$steps" "$label"
|
||||
sleep 0.04
|
||||
done
|
||||
echo ""
|
||||
}
|
||||
|
||||
SPINNER_PID=""
|
||||
|
||||
start_spinner() {
|
||||
local msg="${1:-Processing...}"
|
||||
local frames=('─' '\\' '│' '/')
|
||||
(
|
||||
local i=0
|
||||
while true; do
|
||||
printf "\r ${BCYAN}${frames[$i]}${RESET} ${DIM}%s${RESET} " "$msg"
|
||||
i=$(( (i + 1) % 4 ))
|
||||
sleep 0.1
|
||||
done
|
||||
) &
|
||||
SPINNER_PID=$!
|
||||
disown "$SPINNER_PID" 2>/dev/null
|
||||
}
|
||||
|
||||
stop_spinner() {
|
||||
local result_msg="${1:-}"
|
||||
if [ -n "$SPINNER_PID" ]; then
|
||||
kill "$SPINNER_PID" 2>/dev/null
|
||||
wait "$SPINNER_PID" 2>/dev/null
|
||||
SPINNER_PID=""
|
||||
fi
|
||||
printf "\r%*s\r" 60 ""
|
||||
[ -n "$result_msg" ] && label_ok "$result_msg"
|
||||
}
|
||||
Loading…
Reference in a new issue