#!/bin/sh
# Helper de tunel OpenVPN de PBI (root). Alternativa a WireGuard para el modo
# "NAS ligero" -- muchos NAS traen integracion nativa de cliente OpenVPN en su
# propia interfaz, sin instalar nada aparte (a diferencia de WireGuard, que en
# algunos NAS exige Docker/paquetes de terceros). Mismo principio que
# pbi-netconf: el servicio web (sin privilegios) NUNCA escala -- solo escribe
# una solicitud en $DATA, y este script (root, disparado por
# pbi-openvpn.path) hace el trabajo real.
#
# Modo CLAVE ESTATICA (no PKI/CA completa): un tunel punto a punto por peer,
# igual de simple que WireGuard -- una sola clave compartida, sin jerarquia de
# certificados que gestionar/revocar. Suficiente porque cada peer ya es su
# propio tunel independiente (mismo diseño que WireGuard: un peer, una
# interfaz, nunca comparten nada entre si).
#
# Tres solicitudes por fichero, cada una consume su fichero al procesarlo:
#   .ovpn-request         crear/actualizar el tunel de un peer (4 lineas:
#                          peerId, puerto UDP, direccion propia, direccion
#                          del peer -- direcciones IPv4 sueltas, no CIDR)
#   .ovpn-client-request   generar (o regenerar) el fichero .ovpn descargable
#                          para ese peer (2 lineas: peerId, endpoint publico
#                          "ip-o-host:puerto" tal como debe conectar el NAS)
#   .ovpn-remove-request   desmontar el tunel de un peer y borrar su clave
#
# IMPORTANTE -- por que esto no acepta un .ovpn ya escrito por el lado sin
# privilegios: igual que con WireGuard, un fichero de config de OpenVPN
# admite directivas (--up/--down/--route-up) que ejecutan comandos de shell
# como root al levantar la interfaz. Por eso aqui solo se aceptan VALORES
# individuales validados, y es este script el que construye tanto la config
# del servidor como el .ovpn del cliente el mismo -- nunca copiando nada.
set -u

DATA=${PBI_NETCONF_ROOT:-/var/lib/pbi}
REQ_TUNNEL="$DATA/.ovpn-request"
REQ_CLIENT="$DATA/.ovpn-client-request"
REQ_RM="$DATA/.ovpn-remove-request"
STATUS="$DATA/.ovpn-status"
LOG="$DATA/.ovpn.log"
CONFDIR=/etc/openvpn/server

RE_PEERID='^[a-z0-9][a-z0-9-]{1,30}$'
RE_PORT='^[0-9]{1,5}$'
RE_IPV4='^([0-9]{1,3}\.){3}[0-9]{1,3}$'
RE_ENDPOINT='^[A-Za-z0-9][A-Za-z0-9.-]{0,253}:[0-9]{1,5}$'

validar() { printf '%s' "$2" | grep -Eq "$1"; }

set_status() {
  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"; }

# Nombre corto y determinista, igual que en pbi-netconf (ifname) -- aqui no
# hay limite de 15 caracteres (no es una interfaz de red con nombre fijo,
# "dev tun" deja que el kernel asigne tunN), pero se mantiene el mismo
# criterio por consistencia y para evitar caracteres raros en rutas de fichero.
name_for() {
  h=$(printf '%s' "$1" | sha256sum | cut -c1-10)
  printf 'pbi%s' "$h"
}
conf_path() { printf '%s/%s.conf' "$CONFDIR" "$(name_for "$1")"; }
key_path() { printf '%s/%s.key' "$CONFDIR" "$(name_for "$1")"; }
client_ovpn_path() { printf '%s/.ovpn-client-%s.ovpn' "$DATA" "$1"; }
unit_name() { printf 'openvpn-server@%s' "$(name_for "$1")"; }

hay_openvpn() { command -v openvpn >/dev/null 2>&1; }

# --------------------------------------------------------- desmontar/limpiar
if [ -f "$REQ_RM" ]; then
  PEER=$(sed -n '1p' "$REQ_RM" 2>/dev/null || true)
  rm -f "$REQ_RM"
  {
    echo "== $(date) == desmontar tunel OpenVPN"
    validar "$RE_PEERID" "$PEER" || fail 2 "peerId invalido" "$PEER"
    set_status running "$PEER" "desmontando"
    systemctl disable --now "$(unit_name "$PEER")" >/dev/null 2>&1 || true
    rm -f "$(conf_path "$PEER")" "$(key_path "$PEER")" "$(client_ovpn_path "$PEER")"
    echo "Tunel OpenVPN de '$PEER' desmontado."
    set_status ok "$PEER" "desmontado"
  } >>"$LOG" 2>&1
  exit 0
fi

# ------------------------------------------------------- crear/actualizar tunel
if [ -f "$REQ_TUNNEL" ]; then
  PEER=$(sed -n '1p' "$REQ_TUNNEL" 2>/dev/null || true)
  PORT=$(sed -n '2p' "$REQ_TUNNEL" 2>/dev/null || true)
  OURADDR=$(sed -n '3p' "$REQ_TUNNEL" 2>/dev/null || true)
  PEERADDR=$(sed -n '4p' "$REQ_TUNNEL" 2>/dev/null || true)
  rm -f "$REQ_TUNNEL"

  {
    echo "== $(date) == crear/actualizar tunel OpenVPN"
    hay_openvpn || fail 10 "openvpn no esta instalado (apt install openvpn)" "$PEER"
    validar "$RE_PEERID" "$PEER"    || fail 2 "peerId invalido" "$PEER"
    validar "$RE_PORT" "$PORT"      || fail 3 "puerto invalido" "$PEER"
    [ "$PORT" -ge 1024 ] && [ "$PORT" -le 65535 ] || fail 3 "puerto fuera de rango (1024-65535)" "$PEER"
    validar "$RE_IPV4" "$OURADDR"   || fail 4 "direccion propia invalida" "$PEER"
    validar "$RE_IPV4" "$PEERADDR"  || fail 5 "direccion del peer invalida" "$PEER"

    set_status running "$PEER" "configurando"
    mkdir -p "$CONFDIR"
    KEY=$(key_path "$PEER")
    [ -s "$KEY" ] || openvpn --genkey secret "$KEY" || fail 11 "no se pudo generar la clave" "$PEER"
    chmod 600 "$KEY"

    CONF=$(conf_path "$PEER")
    TMP="$CONF.tmp.$$"
    {
      printf 'dev tun\n'
      printf 'ifconfig %s %s\n' "$OURADDR" "$PEERADDR"
      printf 'secret %s\n' "$KEY"
      printf 'port %s\n' "$PORT"
      printf 'proto udp\n'
      printf 'cipher AES-256-CBC\n'
      printf 'auth SHA256\n'
      printf 'keepalive 10 60\n'
      printf 'persist-key\n'
      printf 'persist-tun\n'
    } > "$TMP"
    chmod 600 "$TMP"
    mv -f "$TMP" "$CONF"

    UNIT=$(unit_name "$PEER")
    systemctl enable "$UNIT" >/dev/null 2>&1 || true
    if ! systemctl restart "$UNIT"; then
      fail 12 "openvpn fallo al levantar el tunel (revisa el puerto $PORT, ¿en uso?)" "$PEER"
    fi

    echo "Tunel OpenVPN de '$PEER' activo (puerto $PORT)."
    set_status ok "$PEER" "activo"
  } >>"$LOG" 2>&1
  exit 0
fi

# --------------------------------------------------- generar .ovpn del cliente
if [ -f "$REQ_CLIENT" ]; then
  PEER=$(sed -n '1p' "$REQ_CLIENT" 2>/dev/null || true)
  ENDPOINT=$(sed -n '2p' "$REQ_CLIENT" 2>/dev/null || true)
  rm -f "$REQ_CLIENT"

  {
    echo "== $(date) == generar .ovpn del cliente"
    validar "$RE_PEERID" "$PEER" || fail 2 "peerId invalido" "$PEER"
    validar "$RE_ENDPOINT" "$ENDPOINT" || fail 6 "endpoint invalido (formato host:puerto)" "$PEER"
    HOST=${ENDPOINT%:*}
    PORT=${ENDPOINT##*:}

    CONF=$(conf_path "$PEER")
    KEY=$(key_path "$PEER")
    [ -f "$CONF" ] && [ -s "$KEY" ] || fail 7 "el tunel de '$PEER' no esta creado todavia" "$PEER"

    # Direcciones invertidas respecto al servidor: lo que aqui es "propio" en
    # el servidor es "del peer" en el cliente, y viceversa.
    OURADDR=$(grep '^ifconfig ' "$CONF" | awk '{print $2}')
    PEERADDR=$(grep '^ifconfig ' "$CONF" | awk '{print $3}')

    OUT=$(client_ovpn_path "$PEER")
    TMP="$OUT.tmp.$$"
    {
      printf 'dev tun\n'
      printf 'remote %s %s\n' "$HOST" "$PORT"
      printf 'ifconfig %s %s\n' "$PEERADDR" "$OURADDR"
      printf 'proto udp\n'
      printf 'cipher AES-256-CBC\n'
      printf 'auth SHA256\n'
      printf 'keepalive 10 60\n'
      printf 'persist-key\n'
      printf 'persist-tun\n'
      printf '<secret>\n'
      cat "$KEY"
      printf '</secret>\n'
    } > "$TMP"
    chown pbi:pbi "$TMP"
    chmod 640 "$TMP"
    mv -f "$TMP" "$OUT"

    echo "Fichero .ovpn de '$PEER' generado."
    set_status ok "$PEER" "ovpn-generado"
  } >>"$LOG" 2>&1
  exit 0
fi
