feat(infra): auto-commit con 86 cambios

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-26 19:38:15 +02:00
parent 34c27876e0
commit 621e8895c9
85 changed files with 11840 additions and 92 deletions
+68
View File
@@ -0,0 +1,68 @@
---
name: wg_client_install
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "wg_client_install(config_path_or_stdin, [interface_name]) -> json"
description: "Device-side: instala wg0.conf en /etc/wireguard/, habilita systemd wg-quick@wg0, verifica handshake con hub. Idempotente. Acepta config por path o stdin (para pipes desde wg_client_config)."
tags: [wireguard, client, install, mesh, systemd]
uses_functions: [wg_install_bash_infra]
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: config_path_or_stdin
desc: "path al archivo .conf existente, o '-' para leer de stdin (compatible con pipe desde wg_client_config)"
- name: interface_name
desc: "nombre de la interfaz WireGuard (default: wg0). Determina /etc/wireguard/<iface>.conf y la unit systemd wg-quick@<iface>"
output: "JSON {status, interface, hub_endpoint, handshake_seen}. status: installed | already-configured | installed-no-handshake | installed-no-systemd"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/wg_client_install.sh"
---
## Ejemplo
```bash
source bash/functions/infra/wg_client_install.sh
# Desde pipe (caso más común en flow 0009):
wg_client_config_go_infra | jq -r '.INI' | wg_client_install -
# {"status":"installed","interface":"wg0","hub_endpoint":"203.0.113.1:51820","handshake_seen":true}
# Desde archivo .conf generado previamente:
wg_client_install /tmp/peer_laptop.conf
# {"status":"installed","interface":"wg0","hub_endpoint":"203.0.113.1:51820","handshake_seen":true}
# Con interfaz personalizada:
wg_client_install /tmp/peer_laptop.conf wg1
# {"status":"installed","interface":"wg1","hub_endpoint":"203.0.113.1:51820","handshake_seen":true}
# Segunda ejecución con misma config (idempotente):
wg_client_install /tmp/peer_laptop.conf
# {"status":"already-configured","interface":"wg0","hub_endpoint":"203.0.113.1:51820","handshake_seen":false}
```
## Cuando usarla
Cuando necesites conectar un nuevo peer al mesh WireGuard en el flow 0009. Úsala justo después de `wg_client_config` (que genera el .conf) para instalarlo en el device peer. Es el paso final del onboarding de un nodo: config generada → instalada → verificada con handshake.
## Gotchas
- **Requiere root/sudo** para escribir en `/etc/wireguard/`, hacer `chmod 600`, y ejecutar `systemctl`. El operador debe tener `sudo` sin password para estos comandos, o ejecutar la función como root.
- **Idempotente por contenido**: si `/etc/wireguard/<iface>.conf` ya existe con el mismo contenido, retorna `status=already-configured` sin tocar nada. Si el contenido difiere, hace backup automático con timestamp antes de sobreescribir.
- **NetworkManager**: si NM gestiona la interfaz wg0, `wg-quick` puede fallar con conflicto. Solución: crear `/etc/NetworkManager/conf.d/99-wg.conf` con `[keyfile]\nunmanaged-devices=interface-name:wg0` y reiniciar NM antes de ejecutar esta función.
- **WSL2 sin systemd** (variantes antiguas o sin `/etc/wsl.conf` con `[boot] systemd=true`): `systemctl` no está disponible. La función detecta esto, emite `status=installed-no-systemd` con instrucciones en stderr para levantar la interfaz manualmente con `sudo wg-quick up wg0`. Para autostart en WSL2 sin systemd: añadir `sudo wg-quick up wg0` al final de `~/.bashrc`.
- **WSL2 con systemd**: kernel WSL2 >= 5.6 (default en distros recientes) incluye WireGuard built-in. Habilitar systemd en WSL2 con `[boot]\nsystemd=true` en `/etc/wsl.conf` y reiniciar WSL. Luego esta función funciona igual que en Linux nativo.
- **Android / Termux**: NO usar esta función. Termux no tiene systemd ni `/etc/wireguard/`. En Android usar la app WireGuard oficial (F-Droid / Play Store) e importar el .conf generado por `wg_client_config` directamente desde la app.
- **handshake_seen=false con status=installed-no-handshake**: la interfaz está activa pero el hub no ha respondido en 10s. No es un error fatal — puede tardar más si el hub está ocupado o hay NAT traversal pendiente. Verificar: endpoint accesible por UDP, hub corriendo con `wg show`, claves public/preshared coincidentes.
- Los logs van siempre a stderr con prefijo `[wg_client_install]`; stdout es exclusivamente el JSON de resultado.
## Capability growth log
<!-- Rellenar solo cuando haya version bump real -->
+116
View File
@@ -0,0 +1,116 @@
#!/usr/bin/env bash
# wg_client_install — Device-side: instala wg0.conf en /etc/wireguard/, habilita
# systemd wg-quick@<iface>, verifica handshake con hub. Idempotente.
# Acepta config por path o stdin ("-").
# Exit 0 = éxito (installed o already-configured), 1 = error fatal.
wg_client_install() {
local config_src="${1:--}"
local iface="${2:-wg0}"
local conf_dest="/etc/wireguard/${iface}.conf"
local config_content="" hub_endpoint="" handshake_seen="false"
_wg_ci_log() { echo "[wg_client_install] $*" >&2; }
# ── Prereq: wg debe estar instalado ──────────────────────────────────────
if ! command -v wg &>/dev/null; then
_wg_ci_log "ERROR: 'wg' no encontrado. Ejecuta wg_install primero."
return 1
fi
# ── Leer contenido del .conf ──────────────────────────────────────────────
if [[ "${config_src}" == "-" ]]; then
_wg_ci_log "Leyendo config desde stdin"
config_content=$(cat) || { _wg_ci_log "ERROR: fallo al leer stdin"; return 1; }
elif [[ -f "${config_src}" ]]; then
_wg_ci_log "Leyendo config desde ${config_src}"
config_content=$(cat "${config_src}") || { _wg_ci_log "ERROR: fallo al leer ${config_src}"; return 1; }
else
_wg_ci_log "ERROR: '${config_src}' no es un path existente ni '-' (stdin)"
return 1
fi
if [[ -z "${config_content}" ]]; then
_wg_ci_log "ERROR: contenido de config vacío"
return 1
fi
# ── Extraer endpoint del hub para incluirlo en el JSON de salida ──────────
hub_endpoint=$(printf '%s\n' "${config_content}" | grep -m1 '^Endpoint\s*=' | sed 's/.*=\s*//' | tr -d '[:space:]' || true)
# ── Idempotencia: comparar con conf existente ─────────────────────────────
if [[ -f "${conf_dest}" ]]; then
local existing_content
existing_content=$(sudo cat "${conf_dest}" 2>/dev/null || cat "${conf_dest}" 2>/dev/null || true)
if [[ "${existing_content}" == "${config_content}" ]]; then
_wg_ci_log "Configuración idéntica ya presente en ${conf_dest}; nada que hacer"
printf '{"status":"already-configured","interface":"%s","hub_endpoint":"%s","handshake_seen":false}\n' \
"${iface}" "${hub_endpoint}"
return 0
fi
# Contenido difiere → backup + rewrite
local backup="${conf_dest}.bak.$(date +%Y%m%d%H%M%S)"
_wg_ci_log "Configuración existente difiere; backup → ${backup}"
sudo cp "${conf_dest}" "${backup}" \
|| { _wg_ci_log "ERROR: no se pudo hacer backup de ${conf_dest}"; return 1; }
fi
# ── Crear directorio y escribir conf ─────────────────────────────────────
sudo mkdir -p "/etc/wireguard" \
|| { _wg_ci_log "ERROR: no se pudo crear /etc/wireguard"; return 1; }
printf '%s\n' "${config_content}" | sudo tee "${conf_dest}" >/dev/null \
|| { _wg_ci_log "ERROR: no se pudo escribir ${conf_dest}"; return 1; }
sudo chmod 600 "${conf_dest}" \
|| { _wg_ci_log "WARN: no se pudo chmod 600 ${conf_dest}"; }
_wg_ci_log "Config escrita en ${conf_dest} (chmod 600)"
# ── Habilitar + arrancar systemd unit ─────────────────────────────────────
if ! command -v systemctl &>/dev/null; then
_wg_ci_log "WARN: systemctl no disponible."
_wg_ci_log " En WSL2 sin systemd: ejecuta 'sudo wg-quick up ${iface}' manualmente."
_wg_ci_log " Para autostart en WSL2: añade 'sudo wg-quick up ${iface}' a ~/.bashrc o usa WSL2 con systemd habilitado."
printf '{"status":"installed-no-systemd","interface":"%s","hub_endpoint":"%s","handshake_seen":false}\n' \
"${iface}" "${hub_endpoint}"
return 0
fi
_wg_ci_log "Habilitando y arrancando wg-quick@${iface}"
if ! sudo systemctl enable --now "wg-quick@${iface}" 2>&1 | tee /dev/stderr >&2; then
_wg_ci_log "ERROR: systemctl enable --now wg-quick@${iface} falló."
_wg_ci_log " En WSL2: asegúrate de tener kernel >= 5.6 y systemd habilitado (/etc/wsl.conf: [boot] systemd=true)."
_wg_ci_log " Si NetworkManager gestiona ${iface}: añade 'unmanaged-devices=interface-name:${iface}' a /etc/NetworkManager/conf.d/99-wg.conf"
return 1
fi
_wg_ci_log "wg-quick@${iface} habilitado y activo"
# ── Esperar handshake (hasta 10 s) ────────────────────────────────────────
local deadline=$(( $(date +%s) + 10 ))
_wg_ci_log "Esperando handshake en ${iface} (timeout 10s)..."
while [[ $(date +%s) -lt ${deadline} ]]; do
local hs_output
hs_output=$(sudo wg show "${iface}" latest-handshakes 2>/dev/null || true)
# latest-handshakes devuelve "<pubkey> <unix_ts>"; ts > 0 = handshake visto
if printf '%s\n' "${hs_output}" | awk '{print $2}' | grep -qE '^[1-9][0-9]+$'; then
handshake_seen="true"
_wg_ci_log "Handshake confirmado en ${iface}"
break
fi
sleep 1
done
if [[ "${handshake_seen}" == "false" ]]; then
_wg_ci_log "WARN: timeout esperando handshake en ${iface}. La interfaz está activa pero el hub no ha respondido aún."
_wg_ci_log " Verifica: endpoint accesible, hub corriendo, claves correctas."
printf '{"status":"installed-no-handshake","interface":"%s","hub_endpoint":"%s","handshake_seen":false}\n' \
"${iface}" "${hub_endpoint}"
return 0
fi
printf '{"status":"installed","interface":"%s","hub_endpoint":"%s","handshake_seen":true}\n' \
"${iface}" "${hub_endpoint}"
return 0
}
+66
View File
@@ -0,0 +1,66 @@
---
name: wg_hub_setup
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "wg_hub_setup(private_key, subnet_cidr, listen_port) -> json"
description: "Configura el host como hub WireGuard (servidor). Crea /etc/wireguard/wg0.conf con clave privada + IP pool + ListenPort. Abre UDP en firewall (ufw o iptables), habilita ip_forward persistente en /etc/sysctl.d/99-wireguard.conf, persiste y arranca systemd unit wg-quick@wg0. Idempotente: misma PrivateKey = no-op; PrivateKey distinta = backup + rewrite."
tags: [wireguard, hub, infra, mesh, systemd]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: private_key
desc: "base64 WG private key del hub (44 chars, generada por wg_keygen o `wg genkey`)"
- name: subnet_cidr
desc: "subnet hub con bits del host, ej. 10.42.0.1/24. El hub recibe la .1"
- name: listen_port
desc: "UDP port donde escucha WireGuard (default 51820, rango 1024-65535)"
output: "JSON {status, config_path, interface, hub_ip}. status: configured | reconfigured | already-configured"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/wg_hub_setup.sh"
---
## Ejemplo
```bash
# Generar clave (o usar wg_keygen del registry)
PRIVKEY=$(wg genkey)
source bash/functions/infra/wg_hub_setup.sh
wg_hub_setup "$PRIVKEY" "10.42.0.1/24" 51820
# {"status":"configured","config_path":"/etc/wireguard/wg0.conf","interface":"wg0","hub_ip":"10.42.0.1"}
# Segunda ejecución con la misma clave → no-op
wg_hub_setup "$PRIVKEY" "10.42.0.1/24" 51820
# {"status":"already-configured","config_path":"/etc/wireguard/wg0.conf","interface":"wg0","hub_ip":"10.42.0.1"}
# Cambiar clave → backup de conf anterior + rewrite
wg_hub_setup "$NUEVA_PRIVKEY" "10.42.0.1/24" 51820
# {"status":"reconfigured","config_path":"/etc/wireguard/wg0.conf","interface":"wg0","hub_ip":"10.42.0.1"}
```
## Cuando usarla
Cuando necesites convertir un VPS/host en el nodo central (hub) de una red mesh WireGuard. Úsala inmediatamente después de `wg_install` para dejar el hub listo para recibir peers. El hub escucha en un puerto UDP público; los peers se conectan a él con su propia clave y la AllowedIPs del hub.
## Gotchas
- Requiere `sudo` con NOPASSWD para: `tee /etc/wireguard/`, `chmod`, `sysctl`, `iptables`/`ufw`, `systemctl`. Configurar antes en sudoers.
- NUNCA reusar la misma `private_key` entre hubs distintos. Cada hub tiene su propio par de claves independiente.
- El bloque `PostUp`/`PostDown` usa `eth0` como interfaz de salida para NAT. En VPS con interfaz distinta (ens3, enp3s0) editar `/etc/wireguard/wg0.conf` manualmente antes de reiniciar.
- Conflicto de subnet con docker0 si usas 172.17.0.0/16. Evitar solapamiento — usar 10.42.x.x o 192.168.200.x para WireGuard.
- `systemd-resolved` en VPS Ubuntu puede interferir con resolución DNS cuando WireGuard está activo si el conf añade `DNS =`. Esta función NO setea DNS para evitar el problema — configurarlo a nivel peer si se necesita.
- Si `systemctl start wg-quick@wg0` falla, revisar logs con `journalctl -u wg-quick@wg0 -n 50`.
- En entornos cloud (AWS/GCP/Azure) el security group / firewall de red del proveedor también debe abrir el puerto UDP, independientemente de ufw/iptables local.
## Capability growth log
<!-- Rellenar solo cuando haya version bump real -->
+171
View File
@@ -0,0 +1,171 @@
#!/usr/bin/env bash
# wg_hub_setup — Configura el host como hub WireGuard (servidor central).
# Crea /etc/wireguard/wg0.conf con [Interface] block, abre UDP en firewall,
# habilita ip_forward persistente, arranca y verifica wg-quick@wg0.
# Idempotente: si el conf existe con la misma PrivateKey -> no-op.
# Emite JSON a stdout. Logs a stderr con prefijo [wg_hub_setup].
# Exit 0 = éxito, 1 = fallo.
wg_hub_setup() {
local private_key="${1:-}"
local subnet_cidr="${2:-10.42.0.1/24}"
local listen_port="${3:-51820}"
_wg_hub_log() { echo "[wg_hub_setup] $*" >&2; }
# ── Validación de entradas ──────────────────────────────────────────────
# private_key: base64 estándar de 44 caracteres (32 bytes)
if [[ -z "${private_key}" ]]; then
_wg_hub_log "ERROR: private_key requerida (base64 44 chars, generada por wg genkey)"
return 1
fi
if ! [[ "${private_key}" =~ ^[A-Za-z0-9+/]{43}=$ ]]; then
_wg_hub_log "ERROR: private_key no parece base64 válida (se esperan 44 chars terminando en '=')"
return 1
fi
# subnet_cidr: 10.x.x.x/nn
if ! [[ "${subnet_cidr}" =~ ^10\.[0-9]+\.[0-9]+\.[0-9]+/[0-9]+$ ]]; then
_wg_hub_log "ERROR: subnet_cidr debe ser 10.x.x.x/nn, recibido: '${subnet_cidr}'"
return 1
fi
# listen_port: 1024-65535
if ! [[ "${listen_port}" =~ ^[0-9]+$ ]] || (( listen_port < 1024 || listen_port > 65535 )); then
_wg_hub_log "ERROR: listen_port debe ser un entero entre 1024 y 65535, recibido: '${listen_port}'"
return 1
fi
# ── Verificar que wireguard-tools esté instalado ────────────────────────
if ! command -v wg &>/dev/null; then
_wg_hub_log "ERROR: 'wg' no encontrado. Ejecuta wg_install primero."
return 1
fi
if ! command -v wg-quick &>/dev/null; then
_wg_hub_log "ERROR: 'wg-quick' no encontrado. Instala wireguard-tools."
return 1
fi
# ── Extraer hub_ip (parte sin CIDR prefix) y determinar config_path ────
local hub_ip="${subnet_cidr%%/*}"
local config_path="/etc/wireguard/wg0.conf"
local interface="wg0"
local action_status=""
# ── Idempotencia: comparar PrivateKey existente ─────────────────────────
if [[ -f "${config_path}" ]]; then
local existing_key
existing_key=$(sudo grep -E '^\s*PrivateKey\s*=' "${config_path}" 2>/dev/null \
| head -n1 | sed 's/.*=\s*//')
if [[ "${existing_key}" == "${private_key}" ]]; then
_wg_hub_log "Config existente con misma PrivateKey — no-op (status=already-configured)"
printf '{"status":"already-configured","config_path":"%s","interface":"%s","hub_ip":"%s"}\n' \
"${config_path}" "${interface}" "${hub_ip}"
return 0
else
_wg_hub_log "Config existente con PrivateKey DIFERENTE — haciendo backup y reescribiendo"
local backup_path="${config_path}.bak.$(date +%Y%m%d%H%M%S)"
sudo cp "${config_path}" "${backup_path}" \
|| { _wg_hub_log "ERROR: no se pudo hacer backup en ${backup_path}"; return 1; }
_wg_hub_log "Backup guardado en ${backup_path}"
action_status="reconfigured"
fi
else
action_status="configured"
fi
# ── Asegurar que /etc/wireguard existe con permisos correctos ───────────
if [[ ! -d /etc/wireguard ]]; then
sudo mkdir -p /etc/wireguard \
|| { _wg_hub_log "ERROR: no se pudo crear /etc/wireguard"; return 1; }
sudo chmod 700 /etc/wireguard
_wg_hub_log "Directorio /etc/wireguard creado"
fi
# ── Escribir /etc/wireguard/wg0.conf ────────────────────────────────────
_wg_hub_log "Escribiendo ${config_path} (Address=${subnet_cidr}, ListenPort=${listen_port})"
sudo tee "${config_path}" > /dev/null <<EOF
[Interface]
Address = ${subnet_cidr}
ListenPort = ${listen_port}
PrivateKey = ${private_key}
SaveConfig = false
# NAT: permite que los peers accedan a internet via este hub (opcional, comentar si no se desea)
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
EOF
if [[ $? -ne 0 ]]; then
_wg_hub_log "ERROR: no se pudo escribir ${config_path}"
return 1
fi
sudo chmod 600 "${config_path}" \
|| { _wg_hub_log "ERROR: chmod 600 ${config_path} falló"; return 1; }
_wg_hub_log "Permisos 600 aplicados a ${config_path}"
# ── Habilitar ip_forward persistente ────────────────────────────────────
local sysctl_file="/etc/sysctl.d/99-wireguard.conf"
if [[ ! -f "${sysctl_file}" ]] || ! grep -q "net.ipv4.ip_forward" "${sysctl_file}" 2>/dev/null; then
_wg_hub_log "Habilitando ip_forward en ${sysctl_file}"
echo "net.ipv4.ip_forward = 1" | sudo tee "${sysctl_file}" > /dev/null \
|| { _wg_hub_log "ERROR: no se pudo escribir ${sysctl_file}"; return 1; }
fi
sudo sysctl -p "${sysctl_file}" >&2 \
|| _wg_hub_log "WARN: sysctl -p falló (puede ignorarse si el kernel ya tiene ip_forward=1)"
# ── Abrir puerto en firewall ─────────────────────────────────────────────
if command -v ufw &>/dev/null && sudo ufw status 2>/dev/null | grep -q "Status: active"; then
_wg_hub_log "ufw activo — abriendo UDP/${listen_port}"
sudo ufw allow "${listen_port}/udp" >&2 \
|| _wg_hub_log "WARN: ufw allow ${listen_port}/udp falló (verificar manualmente)"
elif command -v iptables &>/dev/null; then
_wg_hub_log "ufw inactivo — usando iptables para abrir UDP/${listen_port}"
sudo iptables -C INPUT -p udp --dport "${listen_port}" -j ACCEPT 2>/dev/null \
|| sudo iptables -A INPUT -p udp --dport "${listen_port}" -j ACCEPT >&2 \
|| _wg_hub_log "WARN: iptables INPUT rule falló (verificar manualmente)"
else
_wg_hub_log "WARN: ni ufw ni iptables disponibles — abre el puerto ${listen_port}/udp manualmente"
fi
# ── Detener interfaz si estaba corriendo (para aplicar nueva config) ────
if sudo wg show "${interface}" &>/dev/null 2>&1; then
_wg_hub_log "Interfaz ${interface} activa — deteniendo antes de reconfigurar"
sudo systemctl stop "wg-quick@${interface}" 2>/dev/null \
|| sudo wg-quick down "${interface}" 2>/dev/null \
|| _wg_hub_log "WARN: no se pudo detener ${interface} (puede que no estuviera activa)"
fi
# ── Habilitar y arrancar wg-quick@wg0 ────────────────────────────────────
_wg_hub_log "Habilitando systemd unit wg-quick@${interface}"
sudo systemctl enable "wg-quick@${interface}" >&2 \
|| { _wg_hub_log "ERROR: systemctl enable wg-quick@${interface} falló"; return 1; }
_wg_hub_log "Arrancando wg-quick@${interface}"
sudo systemctl start "wg-quick@${interface}" >&2 \
|| { _wg_hub_log "ERROR: systemctl start wg-quick@${interface} falló"; return 1; }
# ── Verificar que la interfaz está UP ────────────────────────────────────
local retries=5
local up=0
for (( i=0; i<retries; i++ )); do
if sudo wg show "${interface}" &>/dev/null 2>&1; then
up=1
break
fi
sleep 1
done
if [[ "${up}" -eq 0 ]]; then
_wg_hub_log "ERROR: 'wg show ${interface}' falló tras ${retries}s — la interfaz no arrancó"
return 1
fi
_wg_hub_log "Interfaz ${interface} UP (status=${action_status})"
printf '{"status":"%s","config_path":"%s","interface":"%s","hub_ip":"%s"}\n' \
"${action_status}" "${config_path}" "${interface}" "${hub_ip}"
return 0
}
+51
View File
@@ -0,0 +1,51 @@
---
name: wg_install
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "wg_install() -> json"
description: "Instala wireguard + wireguard-tools en Linux (debian/ubuntu/fedora/arch). Idempotente. Carga modulo kernel. Emite JSON con distro detectada y version instalada."
tags: [wireguard, install, infra, mesh, deploy]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params: []
output: "JSON {status, distro, version}. status=installed o already-present."
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/wg_install.sh"
---
## Ejemplo
```bash
source bash/functions/infra/wg_install.sh
wg_install
# {"status":"installed","distro":"ubuntu","version":"wireguard-tools 1.0.20210914"}
# Si ya está instalado:
wg_install
# {"status":"already-present","distro":"ubuntu","version":"wireguard-tools 1.0.20210914"}
```
## Cuando usarla
Cuando necesites asegurarte de que wireguard-tools está disponible en un host antes de configurar un peer o hub WireGuard. Úsala como paso previo en pipelines de bootstrapping de nodos mesh (flow wireguard).
## Gotchas
- Requiere `sudo` con NOPASSWD para apt-get/dnf/pacman y para modprobe. El operador debe haberlo configurado antes.
- `modprobe wireguard` puede fallar en kernels < 5.6 sin DKMS instalado (wireguard-dkms). La función lo trata como advertencia, no como error fatal — la instalación de las herramientas igual se completa.
- En RHEL/CentOS instala `epel-release` automáticamente antes de wireguard-tools.
- Distros no reconocidas en `/etc/os-release ID` producen exit 1 con mensaje de error explícito en stderr.
- Los logs van siempre a stderr con prefijo `[wg_install]`; stdout es exclusivamente el JSON de resultado.
## Capability growth log
<!-- Rellenar solo cuando haya version bump real -->
+81
View File
@@ -0,0 +1,81 @@
#!/usr/bin/env bash
# wg_install — Instala wireguard + wireguard-tools en Linux (debian/ubuntu/fedora/arch).
# Idempotente: si wg ya está instalado emite JSON con status=already-present y sale.
# Carga módulo kernel wireguard. Emite JSON a stdout. Logs a stderr con prefijo [wg_install].
# Exit 0 = éxito, 1 = fallo.
wg_install() {
local distro="" version="" status=""
_wg_log() { echo "[wg_install] $*" >&2; }
# Detectar distro via /etc/os-release
if [[ -f /etc/os-release ]]; then
distro=$(. /etc/os-release && echo "${ID:-unknown}")
else
_wg_log "ERROR: /etc/os-release no encontrado; no se puede detectar distro"
return 1
fi
_wg_log "Distro detectada: ${distro}"
# Comprobar si wg ya está instalado (idempotencia)
if command -v wg &>/dev/null; then
version=$(wg --version 2>/dev/null | head -n1 || echo "unknown")
_wg_log "wireguard-tools ya presente (${version}); cargando módulo kernel"
# Intentar cargar módulo igualmente (no fatal)
sudo modprobe wireguard 2>/dev/null || true
printf '{"status":"already-present","distro":"%s","version":"%s"}\n' "${distro}" "${version}"
return 0
fi
# Instalar según distro
case "${distro}" in
debian|ubuntu|linuxmint|pop|kali|raspbian)
_wg_log "Usando apt-get (${distro})"
sudo apt-get update -y >&2 || { _wg_log "ERROR: apt-get update falló"; return 1; }
sudo apt-get install -y wireguard wireguard-tools >&2 \
|| { _wg_log "ERROR: apt-get install wireguard falló"; return 1; }
;;
fedora)
_wg_log "Usando dnf (fedora)"
sudo dnf install -y wireguard-tools >&2 \
|| { _wg_log "ERROR: dnf install wireguard-tools falló"; return 1; }
;;
rhel|centos|rocky|almalinux)
_wg_log "Usando dnf (rhel/centos/rocky/alma)"
sudo dnf install -y epel-release >&2 || true
sudo dnf install -y wireguard-tools >&2 \
|| { _wg_log "ERROR: dnf install wireguard-tools falló"; return 1; }
;;
arch|manjaro|endeavouros)
_wg_log "Usando pacman (arch)"
sudo pacman -S --noconfirm wireguard-tools >&2 \
|| { _wg_log "ERROR: pacman install wireguard-tools falló"; return 1; }
;;
*)
_wg_log "ERROR: distro '${distro}' no soportada (soportadas: debian/ubuntu/fedora/rhel/arch)"
return 1
;;
esac
# Verificar instalación
if ! command -v wg &>/dev/null; then
_wg_log "ERROR: 'wg' no encontrado tras la instalación"
return 1
fi
version=$(wg --version 2>/dev/null | head -n1 || echo "unknown")
_wg_log "wireguard-tools instalado: ${version}"
# Cargar módulo kernel (no fatal: kernels >=5.6 lo incluyen built-in)
if sudo modprobe wireguard 2>/dev/null; then
_wg_log "Módulo kernel wireguard cargado"
else
_wg_log "WARN: modprobe wireguard falló (puede estar built-in en el kernel o requerir DKMS)"
fi
status="installed"
printf '{"status":"%s","distro":"%s","version":"%s"}\n' "${status}" "${distro}" "${version}"
return 0
}
+79
View File
@@ -0,0 +1,79 @@
---
name: wg_status
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "wg_status([interface_name]) -> json"
description: "Parsea `wg show <iface> dump` a JSON estructurado con peers, handshake age, status (online/stale/never), bytes rx/tx. Resuelve device_id desde comentarios en wg0.conf. Para dashboards (agents_dashboard Mesh panel)."
tags: [wireguard, status, observability, json, infra]
params:
- name: interface_name
desc: "Nombre de la interface WireGuard (default wg0)"
output: "JSON con interface info + array de peers. Cada peer incluye public_key, device_id (de comentario # DeviceID:<id> en wg0.conf), endpoint, allowed_ips, latest_handshake_unix, latest_handshake_ago_s, rx_bytes, tx_bytes, persistent_keepalive, status (online/stale/never)."
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
tested: true
tests:
- "interface con 2 peers online y stale"
- "interface sin peers devuelve array vacio"
- "interface inexistente devuelve error JSON"
- "WG_FAKE_DUMP carga dump de archivo"
test_file_path: "bash/functions/infra/wg_status_test.sh"
file_path: "bash/functions/infra/wg_status.sh"
---
## Ejemplo
```bash
# Estado real de wg0
source bash/functions/infra/wg_status.sh
wg_status | jq .
# Interface distinta
wg_status wg1 | jq .peers[].status
# Sin sudo real (testing / CI)
WG_FAKE_DUMP=bash/functions/infra/wg_status_test_dump.tsv wg_status wg0 | jq .
```
Salida representativa:
```json
{
"interface": "wg0",
"public_key": "abcXYZ123...",
"listen_port": "51820",
"peers": [
{
"public_key": "peerKey1...",
"device_id": "pc-aurgi",
"endpoint": "1.2.3.4:54321",
"allowed_ips": ["10.42.0.10/32"],
"latest_handshake_unix": 1716000000,
"latest_handshake_ago_s": 42,
"rx_bytes": 12345,
"tx_bytes": 67890,
"persistent_keepalive": 25,
"status": "online"
}
]
}
```
## Cuando usarla
Cuando necesites saber el estado del mesh WireGuard desde un script, dashboard o agente. Usa antes de mostrar el panel Mesh en `agents_dashboard`. Llama cada N segundos para polling ligero desde shell sin depender de la API de WireGuard.
## Gotchas
- Requiere `CAP_NET_ADMIN` / root: `wg show` falla sin permisos. En produccion ejecutar via `sudo -n wg show wg0 dump` o dar permiso al binario. Para tests sin sudo: `WG_FAKE_DUMP=<path>` carga el dump desde archivo.
- `listen_port` se devuelve como string (tal como lo emite `wg show dump`). El campo es `"0"` si wg no esta activo pero la interface existe.
- `device_id` queda `""` si no hay comentario `# DeviceID:<id>` antes del `[Peer]` correspondiente en `/etc/wireguard/<iface>.conf`.
- Status `stale` cubre desde 180s hasta cualquier valor mayor. No hay distincion entre "hace 5 min" y "hace 3 dias" — ambos son `stale`. Para un threshold mas fino, usar `latest_handshake_ago_s` directamente.
- Si `/etc/wireguard/<iface>.conf` no existe o no es legible, `device_id` sera `""` para todos los peers (la funcion no falla, solo omite el lookup).
+161
View File
@@ -0,0 +1,161 @@
#!/usr/bin/env bash
# wg_status — Parsea `wg show <iface> dump` a JSON estructurado con peers,
# handshake age, status (online/stale/never), bytes rx/tx.
# Resuelve device_id desde comentarios # DeviceID:<id> en wg0.conf.
#
# Usage:
# wg_status [interface_name] # default: wg0
#
# Env:
# WG_FAKE_DUMP=<path> # lee dump de archivo en vez de llamar wg show (para tests)
wg_status() {
local iface="${1:-wg0}"
local conf="${WG_FAKE_CONF:-/etc/wireguard/${iface}.conf}"
local now
now=$(date +%s)
# --- obtener dump (real o fake) ---
local dump
if [[ -n "${WG_FAKE_DUMP:-}" ]]; then
if [[ ! -f "$WG_FAKE_DUMP" ]]; then
printf '{"error":"WG_FAKE_DUMP file not found: %s"}\n' "$WG_FAKE_DUMP"
return 1
fi
dump=$(cat "$WG_FAKE_DUMP")
else
if ! command -v wg &>/dev/null; then
printf '{"error":"wg command not found"}\n'
return 1
fi
if ! dump=$(wg show "$iface" dump 2>&1); then
if echo "$dump" | grep -qi "no such device\|does not exist\|unable to access interface"; then
printf '{"error":"interface not found"}\n'
return 1
fi
printf '{"error":"%s"}\n' "$(echo "$dump" | head -n1 | sed 's/"/\\"/g')"
return 1
fi
fi
# --- primera linea: info de la propia interface ---
# formato: <private_key>\t<public_key>\t<listen_port>\t<fwmark>
local iface_line
iface_line=$(echo "$dump" | head -n1)
local iface_pubkey iface_port
iface_pubkey=$(echo "$iface_line" | awk -F'\t' '{print $2}')
iface_port=$(echo "$iface_line" | awk -F'\t' '{print $3}')
# --- leer DeviceID map desde wg0.conf ---
# Busca patron:
# # DeviceID:<id>
# [Peer]
# PublicKey = <pk>
# Producimos pares "pk\tdevice_id" en un archivo temporal para lookup via awk
local device_map
device_map=$(awk '
/^#[[:space:]]*DeviceID:/ {
split($0, a, "DeviceID:")
did = a[2]
gsub(/^[[:space:]]+|[[:space:]]+$/, "", did)
pending_did = did
}
/^\[Peer\]/ {
in_peer = 1
}
in_peer && /^PublicKey[[:space:]]*=/ {
pk = $0
sub(/^PublicKey[[:space:]]*=[[:space:]]*/, "", pk)
gsub(/^[[:space:]]+|[[:space:]]+$/, "", pk)
if (pending_did != "") {
print pk "\t" pending_did
pending_did = ""
}
in_peer = 0
}
' "$conf" 2>/dev/null)
# --- parsear peers (lineas 2..N del dump) ---
# formato peer: <public_key>\t<preshared_key>\t<endpoint>\t<allowed_ips>\t<latest_handshake>\t<rx_bytes>\t<tx_bytes>\t<persistent_keepalive>
local peers_json
peers_json=$(echo "$dump" | tail -n +2 | awk -v now="$now" -v dmap="$device_map" '
BEGIN {
# construir lookup device_id
n = split(dmap, lines, "\n")
for (i = 1; i <= n; i++) {
if (lines[i] != "") {
split(lines[i], parts, "\t")
pk_to_did[parts[1]] = parts[2]
}
}
first = 1
printf "["
}
NF >= 7 {
pk = $1
endpoint = $3
allowed = $4
hs = $5 + 0
rx = $6 + 0
tx = $7 + 0
ka = $8
# device_id lookup
did = (pk in pk_to_did) ? pk_to_did[pk] : ""
# handshake age y status
if (hs == 0) {
ago = 0
status = "never"
} else {
ago = now - hs
if (ago < 180) status = "online"
else if (ago < 86400) status = "stale"
else status = "stale"
}
# persistent_keepalive
ka_val = (ka == "off" || ka == "") ? 0 : ka + 0
# endpoint null si "(none)"
ep_val = (endpoint == "(none)") ? "null" : "\"" endpoint "\""
# allowed_ips array
n_ips = split(allowed, ips_arr, ",")
ips_json = "["
for (j = 1; j <= n_ips; j++) {
gsub(/^[[:space:]]+|[[:space:]]+$/, "", ips_arr[j])
ips_json = ips_json "\"" ips_arr[j] "\""
if (j < n_ips) ips_json = ips_json ","
}
ips_json = ips_json "]"
if (!first) printf ","
first = 0
printf "{"
printf "\"public_key\":\"%s\"", pk
printf ",\"device_id\":\"%s\"", did
printf ",\"endpoint\":%s", ep_val
printf ",\"allowed_ips\":%s", ips_json
printf ",\"latest_handshake_unix\":%d", hs
printf ",\"latest_handshake_ago_s\":%d",ago
printf ",\"rx_bytes\":%d", rx
printf ",\"tx_bytes\":%d", tx
printf ",\"persistent_keepalive\":%d", ka_val
printf ",\"status\":\"%s\"", status
printf "}"
}
END { printf "]" }
' FS='\t')
# --- output final ---
printf '{"interface":"%s","public_key":"%s","listen_port":%s,"peers":%s}\n' \
"$iface" "$iface_pubkey" "$iface_port" "$peers_json"
}
# Permitir invocacion directa: bash wg_status.sh [iface]
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
wg_status "$@"
fi
+101
View File
@@ -0,0 +1,101 @@
#!/usr/bin/env bash
# Tests para wg_status
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/wg_status.sh"
PASS=0
FAIL=0
assert_contains() {
local test_name="$1" needle="$2" haystack="$3"
if echo "$haystack" | grep -qF "$needle"; then
echo "PASS: $test_name"
PASS=$((PASS+1))
else
echo "FAIL: $test_name — expected to contain '$needle'"
echo " got: $haystack"
FAIL=$((FAIL+1))
fi
}
assert_not_contains() {
local test_name="$1" needle="$2" haystack="$3"
if ! echo "$haystack" | grep -qF "$needle"; then
echo "PASS: $test_name"
PASS=$((PASS+1))
else
echo "FAIL: $test_name — expected NOT to contain '$needle'"
echo " got: $haystack"
FAIL=$((FAIL+1))
fi
}
# --- fixtures ---
FAKE_DUMP=$(mktemp)
FAKE_DUMP_EMPTY=$(mktemp)
FAKE_CONF=$(mktemp)
trap 'rm -f "$FAKE_DUMP" "$FAKE_DUMP_EMPTY" "$FAKE_CONF"' EXIT
NOW=$(date +%s)
HS_ONLINE=$(( NOW - 60 )) # 60s ago → online
HS_STALE=$(( NOW - 500 )) # 500s ago → stale
# dump con 2 peers (tabs como separador)
printf '%s\n' \
"privKeyBase64== ifacePubKey== 51820 off" \
"peerKey1== (none) 1.2.3.4:54321 10.42.0.10/32 ${HS_ONLINE} 12345 67890 25" \
"peerKey2== (none) 5.6.7.8:12345 10.42.0.20/32 ${HS_STALE} 111 222 0" \
> "$FAKE_DUMP"
# dump vacío (solo línea de interface, sin peers)
printf '%s\n' "privKeyBase64== ifacePubKey== 51820 off" > "$FAKE_DUMP_EMPTY"
# conf con DeviceID comments
cat > "$FAKE_CONF" <<'CONF'
[Interface]
PrivateKey = privKeyBase64==
Address = 10.42.0.1/24
ListenPort = 51820
# DeviceID:pc-aurgi
[Peer]
PublicKey = peerKey1==
AllowedIPs = 10.42.0.10/32
# DeviceID:home-wsl
[Peer]
PublicKey = peerKey2==
AllowedIPs = 10.42.0.20/32
CONF
# --- Test: interface con 2 peers online y stale ---
result=$(WG_FAKE_DUMP="$FAKE_DUMP" WG_FAKE_CONF="$FAKE_CONF" wg_status wg0)
assert_contains "interface con 2 peers online y stale" '"interface":"wg0"' "$result"
assert_contains "interface con 2 peers online y stale" '"listen_port":51820' "$result"
assert_contains "interface con 2 peers online y stale" '"public_key":"ifacePubKey=="' "$result"
assert_contains "interface con 2 peers online y stale" '"status":"online"' "$result"
assert_contains "interface con 2 peers online y stale" '"status":"stale"' "$result"
assert_contains "interface con 2 peers online y stale" '"device_id":"pc-aurgi"' "$result"
assert_contains "interface con 2 peers online y stale" '"device_id":"home-wsl"' "$result"
assert_contains "interface con 2 peers online y stale" '"rx_bytes":12345' "$result"
assert_contains "interface con 2 peers online y stale" '"persistent_keepalive":25' "$result"
# --- Test: interface sin peers devuelve array vacio ---
result_empty=$(WG_FAKE_DUMP="$FAKE_DUMP_EMPTY" WG_FAKE_CONF="$FAKE_CONF" wg_status wg0)
assert_contains "interface sin peers devuelve array vacio" '"peers":[]' "$result_empty"
assert_not_contains "interface sin peers devuelve array vacio" '"error"' "$result_empty"
# --- Test: interface inexistente devuelve error JSON ---
result_err=$(wg_status nonexistent_iface_xyz 2>/dev/null || true)
assert_contains "interface inexistente devuelve error JSON" '"error"' "$result_err"
# --- Test: WG_FAKE_DUMP carga dump de archivo ---
result_fake=$(WG_FAKE_DUMP="$FAKE_DUMP" WG_FAKE_CONF="$FAKE_CONF" wg_status wg0)
assert_contains "WG_FAKE_DUMP carga dump de archivo" '"public_key":"ifacePubKey=="' "$result_fake"
assert_contains "WG_FAKE_DUMP carga dump de archivo" '"peers":[{' "$result_fake"
echo "---"
echo "Results: $PASS passed, $FAIL failed"
[[ $FAIL -eq 0 ]] || exit 1