#!/bin/sh
# Helper de red de PBI (root). Lo dispara pbi-netconf.path cuando el servicio web
# (sin privilegios) deja una solicitud en /var/lib/pbi. Dos tipos de solicitud,
# mas un modo de solo-lectura para el timer de estado:
#
#   .wg-request         crear/actualizar un tunel WireGuard hacia un peer
#                        (6 lineas: peerId, clave-privada-propia, direccion-CIDR
#                        propia, puerto de escucha, clave-publica del peer,
#                        IPs-permitidas del peer en CIDR)
#   .wg-remove-request   desmontar el tunel de un peer (1 linea: peerId)
#   --dump-status        (invocado por el timer, no por un fichero de solicitud)
#                        vuelca `wg show ... dump` a un fichero legible por pbi
#
# IMPORTANTE — por que esto no puede aceptar un fichero de configuracion ya
# escrito por el lado sin privilegios: un .conf de wg-quick admite directivas
# PostUp/PostDown/PreUp/PreDown que ejecutan comandos de shell COMO ROOT al
# levantar/bajar la interfaz. Si este script copiara un fichero recibido tal
# cual, cualquier fallo en el lado sin privilegios (o un plugin comprometido)
# se convertiria en ejecucion arbitraria como root. Por eso aqui solo se
# aceptan VALORES individuales, cada uno validado con una expresion regular
# estricta, y es este script el que construye el texto de la configuracion
# el mismo — sin PostUp/PostDown/PreUp/PreDown, nunca, pase lo que pase en
# los valores recibidos.
#
# El servicio web NUNCA escala privilegios: solo escribe la solicitud.
set -u

DATA=${PBI_NETCONF_ROOT:-/var/lib/pbi}
REQ_WG="$DATA/.wg-request"
REQ_WG_RM="$DATA/.wg-remove-request"
STATUS="$DATA/.wg-status"
IFSTATUS="$DATA/.wg-interfaces-status"
LOG="$DATA/.wg.log"
WGDIR=/etc/wireguard

set_status() {
  # "estado|peerId|mensaje|fecha-utc" -- estado: running | ok | error
  printf '%s|%s|%s|%s\n' "$1" "$2" "${3:-}" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$STATUS" 2>/dev/null || true
  chown pbi:pbi "$STATUS" 2>/dev/null || true
  chmod 640 "$STATUS" 2>/dev/null || true
}
fail() { echo "$2"; set_status error "${3:-}" "$2"; exit "$1"; }

hay_wg() { command -v wg-quick >/dev/null 2>&1 && command -v wg >/dev/null 2>&1; }

# --- validaciones -------------------------------------------------------------
# peerId: slug seguro para usar en nombre de fichero y de unidad systemd.
validar_peer_id() {
  printf '%s' "$1" | grep -Eq '^[a-z0-9][a-z0-9-]{1,30}$'
}
# Clave WireGuard: 44 caracteres base64 terminados en "=", que decodifiquen a
# exactamente 32 bytes (formato Curve25519 de wg).
validar_clave_wg() {
  printf '%s' "$1" | grep -Eq '^[A-Za-z0-9+/]{43}=$' || return 1
  n=$(printf '%s' "$1" | base64 -d 2>/dev/null | wc -c)
  [ "$n" = "32" ]
}
# CIDR IPv4 basico (no valida rango 0-255 por octeto al detalle: si el valor es
# absurdo, `ip`/`wg-quick` lo rechazaran igualmente al aplicarlo).
validar_cidr() {
  printf '%s' "$1" | grep -Eq '^([0-9]{1,3}\.){3}[0-9]{1,3}/[0-9]{1,2}$'
}
validar_puerto() {
  printf '%s' "$1" | grep -Eq '^[0-9]{1,5}$' || return 1
  [ "$1" -ge 1024 ] && [ "$1" -le 65535 ]
}
# host:puerto del peer -- SOLO lo necesita el lado que marca (destino), para
# saber a que direccion mandar el primer paquete. El lado que escucha
# (origen) no lo necesita: WireGuard aprende la direccion del otro lado solo
# al recibir el primer paquete (roaming). Vacio = valido (no se escribe
# Endpoint en el [Peer]).
validar_endpoint() {
  [ -z "$1" ] && return 0
  printf '%s' "$1" | grep -Eq '^[A-Za-z0-9.-]+:[0-9]{1,5}$'
}

# Linux limita el nombre de una interfaz de red a 15 caracteres (IFNAMSIZ-1).
# wg-quick usa el nombre del fichero .conf TAL CUAL como nombre de interfaz --
# el peerId humano (slug del nombre que le puso el usuario, p.ej.
# "oficina-sur-2") puede ser mucho mas largo que eso, y "pbi-remote-" + ese
# peerId lo supera casi siempre. Por eso la interfaz real usa un nombre corto
# derivado por hash del peerId (determinista: mismo peerId -> mismo nombre
# siempre), nunca el peerId human-readable directamente.
ifname() {
  h=$(printf '%s' "$1" | sha256sum | cut -c1-10)
  printf 'pbi%s' "$h"
}
conf_path() { printf '%s/%s.conf' "$WGDIR" "$(ifname "$1")"; }
# Nombre de la interfaz/fichero de configuracion (coincide con lo que espera
# `wg-quick up/down <nombre>` al resolver /etc/wireguard/<nombre>.conf).
unit_name() { ifname "$1"; }
# Unidad systemd real para esa interfaz -- el patron es siempre wg-quick@<nombre>,
# nunca <nombre> a secas (systemctl sobre <nombre> a secas busca una unidad que
# no existe y falla en silencio via el "|| true" de sus llamadas: la interfaz
# nunca llega a levantarse aunque el script informe estado "ok").
systemd_unit() { printf 'wg-quick@%s' "$(unit_name "$1")"; }
hay_systemd() { [ -d /run/systemd/system ]; }
# Arranca/reinicia la interfaz -- via systemd si esta disponible (bare metal),
# o llamando a wg-quick directamente si no (p.ej. dentro de un contenedor
# Docker sin systemd). Devuelve 0 solo si la interfaz queda de verdad activa.
levantar_tunel() {
  peer="$1"
  if hay_systemd; then
    unit=$(systemd_unit "$peer")
    systemctl enable "$unit" >/dev/null 2>&1 || true
    systemctl restart "$unit"
    return $?
  fi
  nombre=$(unit_name "$peer")
  wg-quick down "$nombre" >/dev/null 2>&1 || true
  wg-quick up "$nombre"
}
desmontar_tunel() {
  peer="$1"
  if hay_systemd; then
    systemctl disable --now "$(systemd_unit "$peer")" >/dev/null 2>&1 || true
    return 0
  fi
  wg-quick down "$(unit_name "$peer")" >/dev/null 2>&1 || true
}

# ------------------------------------------------------------- volcado de estado
if [ "${1:-}" = "--dump-status" ]; then
  if hay_wg; then
    wg show all dump > "$IFSTATUS.tmp" 2>/dev/null || true
    mv -f "$IFSTATUS.tmp" "$IFSTATUS" 2>/dev/null || true
  else
    : > "$IFSTATUS"
  fi
  chown pbi:pbi "$IFSTATUS" 2>/dev/null || true
  chmod 640 "$IFSTATUS" 2>/dev/null || true
  exit 0
fi

# --------------------------------------------------------- desmontar un tunel
if [ -f "$REQ_WG_RM" ]; then
  PEER=$(sed -n '1p' "$REQ_WG_RM" 2>/dev/null || true)
  rm -f "$REQ_WG_RM"
  {
    echo "== $(date) == desmontar tunel"
    validar_peer_id "$PEER" || fail 2 "peerId invalido" "$PEER"
    set_status running "$PEER" "desmontando"
    desmontar_tunel "$PEER"
    rm -f "$(conf_path "$PEER")"
    echo "Tunel de '$PEER' desmontado."
    set_status ok "$PEER" "desmontado"
  } >>"$LOG" 2>&1
  exit 0
fi

# ------------------------------------------------------- crear/actualizar tunel
[ -f "$REQ_WG" ] || exit 0
PEER=$(sed -n '1p' "$REQ_WG" 2>/dev/null || true)
PRIVKEY=$(sed -n '2p' "$REQ_WG" 2>/dev/null || true)
ADDR=$(sed -n '3p' "$REQ_WG" 2>/dev/null || true)
PORT=$(sed -n '4p' "$REQ_WG" 2>/dev/null || true)
PEER_PUBKEY=$(sed -n '5p' "$REQ_WG" 2>/dev/null || true)
PEER_ALLOWED=$(sed -n '6p' "$REQ_WG" 2>/dev/null || true)
PEER_ENDPOINT=$(sed -n '7p' "$REQ_WG" 2>/dev/null || true)
rm -f "$REQ_WG"   # consumir la solicitud (evita reprocesos)

{
  echo "== $(date) == crear/actualizar tunel"
  # Igual que cifs-utils/rsync en el helper de replica ligera -- viene en
  # "Recommends" del .deb, que un "dpkg -i" a mano no instala solo.
  hay_wg || { apt-get update -qq && apt-get install -y -qq wireguard-tools; } \
    || fail 10 "no se pudo instalar wireguard-tools (apt install wireguard-tools)" "$PEER"
  validar_peer_id "$PEER"       || fail 2 "peerId invalido" "$PEER"
  validar_clave_wg "$PRIVKEY"    || fail 3 "clave privada invalida" "$PEER"
  validar_cidr "$ADDR"           || fail 4 "direccion CIDR invalida" "$PEER"
  validar_puerto "$PORT"         || fail 5 "puerto de escucha invalido" "$PEER"
  validar_clave_wg "$PEER_PUBKEY" || fail 6 "clave publica del peer invalida" "$PEER"
  validar_cidr "$PEER_ALLOWED"   || fail 7 "IPs permitidas del peer invalidas" "$PEER"
  validar_endpoint "$PEER_ENDPOINT" || fail 8 "endpoint del peer invalido (formato host:puerto)" "$PEER"

  set_status running "$PEER" "configurando"
  mkdir -p "$WGDIR"
  CONF=$(conf_path "$PEER")
  TMP="$CONF.tmp.$$"

  # El texto de la configuracion lo construye ESTE script, campo a campo, ya
  # validado -- nunca se copia nada escrito por el lado sin privilegios. Sin
  # PostUp/PostDown/PreUp/PreDown a proposito (ver cabecera de este fichero).
  {
    printf '[Interface]\n'
    printf 'PrivateKey = %s\n' "$PRIVKEY"
    printf 'Address = %s\n' "$ADDR"
    printf 'ListenPort = %s\n' "$PORT"
    printf '\n[Peer]\n'
    printf 'PublicKey = %s\n' "$PEER_PUBKEY"
    printf 'AllowedIPs = %s\n' "$PEER_ALLOWED"
    [ -n "$PEER_ENDPOINT" ] && printf 'Endpoint = %s\n' "$PEER_ENDPOINT"
  } > "$TMP"
  chmod 600 "$TMP"
  mv -f "$TMP" "$CONF"

  # Si ya estaba levantada con una config anterior, se reinicia para aplicar la
  # nueva; si no, arranca por primera vez -- levantar_tunel cubre ambos casos.
  if ! levantar_tunel "$PEER"; then
    fail 11 "wg-quick fallo al levantar la interfaz (revisa $ADDR/$PORT, ¿en uso?)" "$PEER"
  fi

  echo "Tunel de '$PEER' activo ($ADDR, puerto $PORT)."
  set_status ok "$PEER" "activo"
} >>"$LOG" 2>&1
