#!/usr/bin/env bash
# =========================================================================
# vpnpro89-red — ayudante privilegiado de VPN PRO 89 en Linux.
#
# Lo invoca polkit segun las acciones declaradas en
# tech.veritasdigital.vpnpro89.policy, que ya existian apuntando a este
# fichero. Se instala en /usr/lib/vpnpro89/vpnpro89-red.
#
# POR QUE UN AYUDANTE SEPARADO
#
# La aplicacion corre sin privilegios. Manipular el cortafuegos requiere
# root, y darle root a toda la aplicacion —o pedir la contrasena para
# arrancar— seria desproporcionado. Este script es la unica parte que se
# eleva, hace una cosa y termina.
#
# LA LECCION QUE YA COSTO CARA
#
# sysproxy.rs:13 documenta que en el prototipo Electron la precedencia de las
# reglas de cortafuegos dejo el equipo sin red. Un kill switch que falla
# cerrado y no sabe volver atras no es una proteccion: es un ladrillo.
#
# De ahi las tres salvaguardas de este script:
#   1. Tabla propia (inet vpnpro89). Nunca se tocan las reglas del usuario,
#      y retirar la nuestra no puede romper las suyas.
#   2. Centinela en disco antes de aplicar. Si la aplicacion muere con el
#      corte puesto, al siguiente arranque se revierte solo.
#   3. `desactivar` idempotente: funciona aunque la tabla no exista, aunque
#      se llame dos veces, y aunque nunca se hubiera activado.
#
# Y la salida manual, documentada tambien en la interfaz:
#     sudo nft delete table inet vpnpro89
# =========================================================================
set -euo pipefail

TABLA="vpnpro89"
CENTINELA="/run/vpnpro89/cortafuegos.activo"
NFT="$(command -v nft || true)"

morir() { echo "vpnpro89-red: $*" >&2; exit 1; }

exigir_nft() {
  [ -n "$NFT" ] || morir "nftables no esta instalado. El kill switch lo necesita."
}

# --- validacion de argumentos -------------------------------------------
# Este script corre como root. Todo lo que llegue de fuera se valida antes de
# acercarse a una regla: una IP sin comprobar es una inyeccion esperando.
ip_valida() {
  [[ "$1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || return 1
  local o
  for o in ${1//./ }; do [ "$o" -le 255 ] || return 1; done
  return 0
}

puerto_valido() {
  [[ "$1" =~ ^[0-9]{1,5}$ ]] && [ "$1" -ge 1 ] && [ "$1" -le 65535 ]
}

# =========================================================================
# cortafuegos activar <ip-nodo> <puerto-nodo> [ip-dns]
#
# Deja salir SOLO: loopback, el nodo, y el DNS indicado. Todo lo demas se
# rechaza, IPv6 incluido y por completo.
#
# Se usa `reject` y no `drop` a proposito: una aplicacion que recibe un
# rechazo falla al instante y dice algo. Con `drop` se queda esperando hasta
# agotar el tiempo, y la persona cree que su conexion va lenta en vez de
# entender que el corte esta puesto.
# =========================================================================
cortafuegos_activar() {
  exigir_nft
  local ip="${1:-}" puerto="${2:-}" dns="${3:-}"

  ip_valida "$ip"        || morir "IP de nodo no valida"
  puerto_valido "$puerto" || morir "puerto de nodo no valido"
  if [ -n "$dns" ]; then ip_valida "$dns" || morir "IP de DNS no valida"; fi

  # El centinela se escribe ANTES de aplicar. Si el equipo se apaga a mitad,
  # el rastro queda y el siguiente arranque puede limpiar.
  mkdir -p "$(dirname "$CENTINELA")"
  printf '%s %s %s\n' "$ip" "$puerto" "${dns:-}" > "$CENTINELA"

  # Idempotente: si quedaba una tabla previa, se sustituye entera.
  "$NFT" delete table inet "$TABLA" 2>/dev/null || true

  "$NFT" -f - <<EOF
table inet ${TABLA} {
  chain salida {
    type filter hook output priority filter; policy accept;

    # Loopback siempre. Sin esto el propio Xray no podria hablar con su
    # inbound local y el corte se convertiria en un apagon.
    oifname "lo" accept

    # Lo ya establecido sigue. Cortar conexiones vivas al activar no
    # protege de nada y rompe descargas en curso.
    ct state established,related accept

    # El nodo.
    ip daddr ${ip} tcp dport ${puerto} accept
    ip daddr ${ip} udp dport ${puerto} accept
$( [ -n "$dns" ] && printf '    ip daddr %s udp dport 53 accept\n    ip daddr %s tcp dport 53 accept' "$dns" "$dns" )

    # IPv6 entero. El nodo habla IPv4: cualquier salida por v6 seria una
    # fuga fuera del tunel.
    meta nfproto ipv6 reject

    # Lo demas, fuera.
    reject
  }
}
EOF
  echo "activo"
}

cortafuegos_desactivar() {
  # Idempotente por diseno: si no hay nft, si no hay tabla, o si nunca se
  # activo, esto tiene que terminar bien igualmente. Una funcion de
  # recuperacion que puede fallar no sirve para recuperarse.
  if [ -n "$NFT" ]; then
    "$NFT" delete table inet "$TABLA" 2>/dev/null || true
  fi
  rm -f "$CENTINELA" 2>/dev/null || true
  echo "inactivo"
}

cortafuegos_estado() {
  if [ -n "$NFT" ] && "$NFT" list table inet "$TABLA" >/dev/null 2>&1; then
    echo "activo"
  else
    echo "inactivo"
  fi
}

# Se llama al arrancar la aplicacion. Si quedo un centinela de una sesion que
# murio, se limpia el corte: nadie debe encontrarse sin red por un cierre
# inesperado de la vez anterior.
cortafuegos_restaurar_si_quedo() {
  if [ -f "$CENTINELA" ]; then
    cortafuegos_desactivar
    echo "revertido"
  else
    echo "nada que revertir"
  fi
}

uso() {
  cat >&2 <<'AYUDA'
Uso:
  vpnpro89-red cortafuegos activar <ip> <puerto> [ip-dns]
  vpnpro89-red cortafuegos desactivar
  vpnpro89-red cortafuegos estado
  vpnpro89-red cortafuegos restaurar-si-quedo

Salida manual, si algo va mal:
  sudo nft delete table inet vpnpro89
AYUDA
  exit 2
}

case "${1:-}" in
  cortafuegos)
    case "${2:-}" in
      activar)              shift 2; cortafuegos_activar "$@" ;;
      desactivar)           cortafuegos_desactivar ;;
      estado)               cortafuegos_estado ;;
      restaurar-si-quedo)   cortafuegos_restaurar_si_quedo ;;
      *) uso ;;
    esac
    ;;
  # `rutas` y `tun` estan declaradas en la politica polkit pero pertenecen a
  # la etapa del tunel completo. Se responde explicitamente en vez de callar:
  # un subcomando que no hace nada y termina con exito es peor que uno que
  # dice que todavia no existe.
  rutas|tun)
    morir "'$1' pertenece a la etapa de tunel completo y no esta implementado."
    ;;
  *) uso ;;
esac
