Encrypted proxy killswitch + TUN optimization #1

Open
zenaku wants to merge 54 commits from feature/merge-encrypted-proxy into master
14 changed files with 27 additions and 941 deletions
Showing only changes of commit ed0de4b2d9 - Show all commits

View file

@ -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

View file

@ -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')

View file

@ -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 ""

View file

@ -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
}

View file

@ -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"
}

View 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
}

View file

@ -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

View file

@ -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)"
}

View file

@ -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
}

View file

@ -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}"
}

View 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)"
}

View file

@ -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
}

View file

@ -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
View file

@ -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"
}