Son aktivite 16 hours ago

Scripts de demostración del episodio «Podman, IPs reales y La Liga» (atareao con Linux): diagnóstico del motor de contenedores, la prueba del ciudadano ante un bloqueo, la corrección de Traefik y Umami, y el borde con HAProxy.

Revizyon cca5f6f3c8f35e4d29832b5626a839114ef7cac8

demo-01-diagnostico-de-tu-motor.sh Ham
1#!/usr/bin/env bash
2# =============================================================================
3# demo-01 · Diagnóstico de tu motor
4# =============================================================================
5# A qué punto del guion sirve: puntos 1, 2, 10 y 11
6# · 1 «Dos mundos: Podman en mi portátil, Docker en el servidor»
7# → `podman --version` y `docker --version`: dos binarios, dos diseños.
8# · 2 «Y ahora quiero el servidor sin root»
9# → cómo se sabe si tu contenedor corre rootless: `podman info` /
10# `docker info`, y el mapa de identificadores del espacio de usuario.
11# · 10 «Qué cambia rootless de verdad»
12# → la línea `rootlessPortForwarder` de `podman info`: el reenviador
13# que hace de proxy y se come la IP de origen.
14# · 11 «No es cosa de Podman: es cosa de rootless»
15# → el mismo dato, buscado en `docker info`.
16#
17# Qué se ve en pantalla: cuatro bloques, uno por idea. Cada bloque lanza el
18# comando exacto y comenta en una línea lo que significa lo que ha salido.
19#
20# Estado:
21# · Podman (versiones, rootless, mapa de UID, reenviador) → VERIFICADO en
22# local con Podman 6.1.4 rootless. La salida real está en
23# `verificacion/demo-01-salida.txt`.
24# · Docker → NO VERIFICADO aquí: en esta máquina no hay binario `docker`.
25# El bloque está escrito para que funcione si lo hay, y si no, imprime la
26# salida ESPERADA y qué línea hay que buscar.
27#
28# Seguridad de la demo: solo LEE. No crea contenedores, no toca la red del
29# sistema y no modifica nada. `podman unshare` abre un espacio de nombres de
30# usuario, ejecuta `cat` y sale.
31# =============================================================================
32
33set -u
34
35titulo() { printf '\n\033[1m── %s\033[0m\n' "$1"; }
36orden() { printf '\n\033[1m$ %s\033[0m\n' "$1"; }
37nota() { printf ' ↳ %s\n' "$1"; }
38esperado() { printf '\n\033[1m[ESPERADO, no ejecutado aquí]\033[0m\n'; }
39
40printf '\033[1mDiagnóstico de tu motor de contenedores\033[0m (puntos 1, 2, 10 y 11)\n'
41printf 'Máquina: %s · %s\n' "$(hostname)" "$(date -Is)"
42
43# -----------------------------------------------------------------------------
44titulo "1 · ¿Qué motor tengo? · punto 1"
45# -----------------------------------------------------------------------------
46# La versión dice mucho más de lo que parece: aquí no se elige "Podman o
47# Docker" por gusto, son dos arquitecturas. Podman no tiene demonio; Docker sí.
48if command -v podman >/dev/null 2>&1; then
49 orden "podman --version"
50 podman --version
51 nota "Un binario más que contesta. No hay servicio con privilegios detrás:"
52 nota "el contenedor es un hijo de esta sesión y muere con ella."
53else
54 nota "no hay 'podman' en el PATH de esta máquina."
55fi
56
57if command -v docker >/dev/null 2>&1; then
58 orden "docker --version"
59 docker --version
60 nota "Mismo software por debajo, distinto modelo: hay un 'dockerd'"
61 nota "arrancado como root al que le pides el contenedor."
62else
63 esperado
64 printf '$ docker --version\n'
65 printf 'Docker version 28.x.y, build xxxxxxx\n'
66 nota "La versión en sí no te dice si es rootless: para eso, el bloque 2."
67fi
68
69# -----------------------------------------------------------------------------
70titulo "2 · ¿Mi motor es rootless? · punto 2"
71# -----------------------------------------------------------------------------
72# Pregunta útil, sobre todo en el servidor: si es rootless, el usuario cero
73# dentro del contenedor NO es el root de la máquina.
74if command -v podman >/dev/null 2>&1; then
75 orden "podman info --format '{{.Host.Security.Rootless}}'"
76 podman info --format '{{.Host.Security.Rootless}}'
77 nota "'true' = rootless. 'false' = los contenedores corren como root de verdad."
78
79 orden "podman info | grep -i rootless"
80 podman info 2>/dev/null | grep -i rootless
81 nota "Tres líneas, y ninguna sobra: 'rootlessNetworkCmd' (con qué se monta la"
82 nota "red: aquí 'pasta'), 'rootlessPortForwarder' (quiénes publican los puertos:"
83 nota "aquí 'rootlessport') y 'rootless: true', que es la tercera y la misma"
84 nota "respuesta del comando anterior. El reenviador del bloque 10 es la segunda."
85else
86 nota "no hay 'podman' en el PATH: se salta la comprobación rootless."
87fi
88
89# -----------------------------------------------------------------------------
90titulo "3 · Qué significa rootless por dentro · punto 2"
91# -----------------------------------------------------------------------------
92# El truco es del kernel: espacios de nombres de usuario. Dentro, el proceso se
93# cree root; fuera, es un usuario sin privilegios con un rango de identificadores
94# prestados. Se ve en el mapa de UID.
95# Se usa 'podman unshare' para NO tener que crear ningún contenedor: abre el
96# espacio de nombres, ejecuta 'cat' y sale.
97if command -v podman >/dev/null 2>&1; then
98 orden "podman unshare cat /proc/self/uid_map"
99 podman unshare cat /proc/self/uid_map 2>&1
100 nota "El 0 de la izquierda (dentro) es un 1000 cualquiera (fuera):"
101 nota "quien escape del contenedor no encuentra el root del host, encuentra un don nadie."
102 nota "En un contenedor ya en marcha, el mismo dato se ve con:"
103 nota " cat /proc/self/uid_map (dentro del contenedor)"
104else
105 nota "sin podman no hay espacio de nombres que enseñar."
106fi
107
108# -----------------------------------------------------------------------------
109titulo "4 · El reenviador: donde se pierde la IP · punto 10"
110# -----------------------------------------------------------------------------
111# En rootless nadie abre el puerto en el kernel por ti: lo abre un proceso tuyo
112# que escucha y reenvía hacia el contenedor. Ese proceso es un proxy, y por eso
113# dentro del contenedor la conexión no llega del visitante sino del mensajero.
114if command -v podman >/dev/null 2>&1; then
115 orden "podman info --format '{{.Host.RootlessPortForwarder}}'"
116 podman info --format '{{.Host.RootlessPortForwarder}}' 2>&1
117 orden "podman info --format '{{.Host.RootlessNetworkCmd}}'"
118 podman info --format '{{.Host.RootlessNetworkCmd}}' 2>&1
119 nota "Esos dos nombres son la pieza de la que habla el guion: el reenvío"
120 nota "de puertos ocurre en el espacio de usuario, no en el NAT del kernel."
121else
122 nota "sin podman no hay reenviador que enseñar."
123fi
124
125# -----------------------------------------------------------------------------
126titulo "5 · ¿Y en Docker? Exactamente lo mismo · punto 11"
127# -----------------------------------------------------------------------------
128# La frase del episodio: no es cosa de Podman, es cosa de rootless. Docker
129# rootless arrastra el mismo comportamiento porque se apoya en RootlessKit.
130if command -v docker >/dev/null 2>&1; then
131 orden "docker info --format '{{.SecurityOptions}}'"
132 docker info --format '{{.SecurityOptions}}' 2>&1
133 nota "Si aparece 'rootless' en la lista, es rootless. Si no aparece, no lo es."
134
135 orden "docker context inspect --format '{{.Endpoints.docker.Host}}'"
136 docker context inspect --format '{{.Endpoints.docker.Host}}' 2>&1
137 nota "Segunda pista: un socket en /run/user/<uid>/docker.sock es rootless;"
138 nota "unix:///var/run/docker.sock es el demonio de root de toda la vida."
139else
140 esperado
141 printf '$ docker info --format '"'"'{{.SecurityOptions}}'"'"'\n'
142 printf '[name=seccomp,profile=builtin name=rootless name=cgroupns]\n\n'
143 printf '$ docker context inspect --format '"'"'{{.Endpoints.docker.Host}}'"'"'\n'
144 printf 'unix:///run/user/1000/docker.sock\n'
145 nota "Docker rootless se instala aparte y se arranca como usuario;"
146 nota "si no lo has hecho, tu Docker es rootful aunque no te lo diga la versión."
147fi
148
149# -----------------------------------------------------------------------------
150titulo "Cierre del bloque"
151# -----------------------------------------------------------------------------
152printf ' · Dos binarios, dos diseños: sin demonio (Podman) / con demonio (Docker).\n'
153printf ' · Rootless se comprueba con %s, no con la versión.\n' "'info'"
154printf ' · Y en rootless los puertos los publica un reenviador: ese es el que\n'
155printf ' hace que dentro del contenedor veas al mensajero y no al visitante.\n\n'
156exit 0
157
demo-02-correcciones-traefik-y-umami.sh Ham
1#!/usr/bin/env bash
2# =============================================================================
3# demo-02 · Las correcciones de configuración
4# =============================================================================
5# A qué punto del guion sirve: punto 13
6# «Y se puede tratar en Traefik y en Umami»
7#
8# Qué se ve en pantalla: los dos fragmentos correctos, con la línea que importa
9# señalada, y una comprobación de que lo son de verdad.
10# 1. Traefik → `config/traefik-entrypoints.yml`
11# forwardedHeaders.trustedIPs y proxyProtocol.trustedIPs
12# apuntando SOLO al proxy correcto.
13# 2. Umami → `config/umami.env`
14# CLIENT_IP_HEADER=x-real-ip
15#
16# La frase que resume las dos: una cabecera que el proxy REESCRIBE frente a una
17# cabecera que CUALQUIERA PUEDE ESCRIBIR.
18#
19# Estado: VERIFICADO en local. El YAML se parsea y el fichero de entorno se
20# comprueba con comandos; la salida real está en
21# `verificacion/demo-02-salida.txt`.
22#
23# Seguridad: solo lee los ficheros de `config/`. No hay ningún secreto en ellos
24# (el token de Cloudflare, si algún día se usa, no vive aquí: va en un `podman
25# secret`). La única dirección de la confianza es la del borde de este montaje
26# (loopback); donde se usa un valor de mentira se marca como rango de
27# documentación (RFC 5737).
28# =============================================================================
29
30set -u
31
32AQUI="$(cd "$(dirname "$0")" && pwd)"
33TRAEFIK="$AQUI/config/traefik-entrypoints.yml"
34UMAMI="$AQUI/config/umami.env"
35
36titulo() { printf '\n\033[1m── %s\033[0m\n' "$1"; }
37orden() { printf '\n\033[1m$ %s\033[0m\n' "$1"; }
38nota() { printf ' ↳ %s\n' "$1"; }
39ok() { printf ' \033[1m✔ %s\033[0m\n' "$1"; }
40fallo() { printf ' \033[1m✘ %s\033[0m\n' "$1"; }
41
42rc_final=0
43
44printf '\033[1mLas dos correcciones del punto 13\033[0m\n'
45
46# -----------------------------------------------------------------------------
47titulo "1 · Traefik: de quién se fía el proxy"
48# -----------------------------------------------------------------------------
49# El error de partida (lo que se cuenta en voz alta, no hace falta fichero):
50# proxyProtocol: trustedIPs: [el rango de tu vecindario de contenedores]
51# ← confía en TODO el vecindario: cualquiera
52# que alcance el puerto puede contar que
53# viene de donde le apetezca
54# forwardedHeaders: insecure: true ← se cree cualquier cabecera
55# Con eso, cualquiera que alcance el puerto de Traefik entra diciendo que viene de
56# donde quiera.
57orden "cat config/traefik-entrypoints.yml"
58cat "$TRAEFIK"
59
60# ¿Se parsea? Un YAML roto en el arranque deja el proxy sin servicios.
61if command -v python3 >/dev/null 2>&1 && python3 -c 'import yaml' 2>/dev/null; then
62 orden "python3 -c 'yaml.safe_load(...)' # ¿el YAML es válido y dice lo que creemos?"
63 python3 - "$TRAEFIK" <<'PY'
64import sys, yaml
65with open(sys.argv[1]) as f:
66 cfg = yaml.safe_load(f)
67ep = cfg["entryPoints"]["websecure"]
68print("address :", ep["address"])
69print("proxyProtocol.trustedIPs :", ep["proxyProtocol"]["trustedIPs"])
70print("forwardedHeaders.insecure :", ep.get("forwardedHeaders", {}).get("insecure", False))
71print("forwardedHeaders.trustedIPs :", ep["forwardedHeaders"]["trustedIPs"])
72PY
73 ok "YAML válido y con las listas presentes."
74else
75 nota "python3 + PyYAML no disponibles: se salta la comprobación de sintaxis."
76fi
77
78# Auditoría: lo que NO debe haber. Solo miran las líneas ACTIVAS (sin '#').
79orden "grep -vE '^[[:space:]]*#' config/traefik-entrypoints.yml | grep -nE 'insecure: true|/[0-9]{1,2}\"' "
80if grep -vE '^[[:space:]]*#' "$TRAEFIK" | grep -E 'insecure: true|/(0|[0-9]|1[0-9]|2[0-3])"'; then
81 fallo "hay un rango demasiado ancho o un 'insecure' activo."
82 rc_final=1
83else
84 ok "ni 'insecure: true' ni rangos anchos activos: solo la IP del borde."
85fi
86
87# La misma auditoría, pero entendiendo el YAML (si hay python3). Es la que manda.
88if command -v python3 >/dev/null 2>&1 && python3 -c 'import yaml' 2>/dev/null; then
89 orden "python3 -c 'auditar el YAML: insecure y prefijos de red'"
90 if python3 - "$TRAEFIK" <<'PY'
91import sys, ipaddress, yaml
92ep = yaml.safe_load(open(sys.argv[1]))["entryPoints"]["websecure"]
93problemas = []
94for nombre in ("forwardedHeaders", "proxyProtocol"):
95 bloque = ep.get(nombre) or {}
96 if bloque.get("insecure"):
97 problemas.append(f"{nombre}.insecure: true ← se cree a cualquiera")
98 for cidr in bloque.get("trustedIPs") or []:
99 red = ipaddress.ip_network(cidr, strict=False)
100 minimo = 24 if red.version == 4 else 64
101 if red.prefixlen < minimo:
102 problemas.append(f"{nombre}.trustedIPs demasiado ancho: {cidr}")
103 else:
104 print(f" {nombre}.trustedIPs → {cidr} (red pequeña, correcto)")
105if problemas:
106 for p in problemas:
107 print(" ✘", p)
108 sys.exit(1)
109print(" ✔ sin 'insecure' y sin rangos anchos.")
110PY
111 then
112 ok "el YAML auditado no tiene confianzas excesivas."
113 else
114 fallo "el YAML confía en más gente de la que debería."
115 rc_final=1
116 fi
117fi
118
119# -----------------------------------------------------------------------------
120titulo "2 · Umami: qué cabecera lee"
121# -----------------------------------------------------------------------------
122# Mismo razonamiento, al revés: aquí no se restringe QUIÉN puede llegar, se elige
123# QUÉ cabecera creerse. Se elige la que el proxy reescribe.
124orden "cat config/umami.env"
125cat "$UMAMI"
126
127orden "grep -n '^CLIENT_IP_HEADER' config/umami.env"
128if grep -q '^CLIENT_IP_HEADER=x-real-ip' "$UMAMI"; then
129 ok "CLIENT_IP_HEADER=x-real-ip → la cabecera que Traefik reconstruye."
130else
131 fallo "CLIENT_IP_HEADER no apunta a x-real-ip."
132 rc_final=1
133fi
134
135orden "grep -nE 'cf-connecting-ip|true-client-ip' config/umami.env # no debe haber ninguna activa"
136if grep -E '^[[:space:]]*CLIENT_IP_HEADER=.*(cf-connecting-ip|true-client-ip)' "$UMAMI"; then
137 fallo "queda una cabecera que cualquiera puede escribir."
138 rc_final=1
139else
140 ok "ninguna cabecera de cliente (cf-connecting-ip / true-client-ip) activa."
141fi
142
143# -----------------------------------------------------------------------------
144titulo "Por qué, en una frase (para decir en el aire)"
145# -----------------------------------------------------------------------------
146cat <<'TEXTO'
147 · En el proxy: no te fíes de todos. 'trustedIPs' con la IP exacta del borde;
148 si te fías del rango entero, cualquiera que llegue te cuenta de dónde viene.
149 · En la analítica: lee la cabecera que el proxy reescribe (x-real-ip), no una
150 que pueda rellenar el primero que pase (cf-connecting-ip, x-forwarded-for).
151 · Y recuerda el límite: esto ordena la casa, no tapa el agujero. Si el 8443
152 está abierto al mundo, la fiabilidad la da el firewall, no la cabecera.
153 · Y ojo con el alcance: esto funcionaba… hasta que un sábado por la tarde el
154 bloqueo judicial tumbó el rango entero del proxy que había delante.
155TEXTO
156
157printf '\n'
158if [ "$rc_final" -eq 0 ]; then
159 printf '\033[1mResultado: las dos correcciones están en su sitio.\033[0m\n\n'
160else
161 printf '\033[1mResultado: hay algo que corregir (ver arriba).\033[0m\n\n'
162fi
163exit "$rc_final"
164
demo-03-prueba-del-ciudadano.sh Ham
1#!/usr/bin/env bash
2# =============================================================================
3# demo-03 · La prueba del ciudadano
4# =============================================================================
5# A qué punto del guion sirve: punto 15
6# «Qué puedes hacer tú: prueba, mide y denuncia»
7#
8# Qué hace: cuando un sitio legítimo te falla, guarda la prueba CON FECHA en un
9# fichero fechado. Dominio, operador, hora exacta y el error tal cual.
10# · curl → ¿responde? ¿con qué código? ¿cuánto tarda? y el error completo.
11# · dig → ¿resuelve? ¿a qué direcciones? ¿con qué servidor de nombres?
12# · whois → ¿de quién es esa dirección? (si es de un rango compartido, no se
13# ha caído «tu» servidor: se ha caído un vector que usan cientos de
14# miles de sitios). Si no hay `whois` instalado, se usa RDAP con
15# `curl`, que da la misma información sin instalar nada.
16#
17# Uso:
18# ./demo-03-prueba-del-ciudadano.sh <dominio> [operador]
19# ./demo-03-prueba-del-ciudadano.sh tudominio.example "Mi operador"
20#
21# El informe se guarda en:
22# $PRUEBAS_DIR/<AAAA-MM-DD_HHMMSS>-prueba-<dominio>.txt
23# (por defecto $PRUEBAS_DIR=/tmp/opencode/pruebas-ciudadano)
24#
25# Códigos de salida (para que sirva también en un cron o en un script):
26# 0 = el sitio responde
27# 1 = el sitio NO responde (conexión, TLS o tiempo agotado)
28# 2 = el nombre no resuelve
29# 3 = no se ha podido escribir el informe
30#
31# Estado: VERIFICADO en local (cuatro casos: responde, no resuelve, no responde
32# y bloqueo detectado). Salidas reales en `verificacion/demo-03-*.txt`.
33#
34# Seguridad de la demo: solo lectura. Nada de `ssh`, nada de tocar servidores
35# ajenos, nada que modifique el sistema. El único tráfico que sale es el de tus
36# propias consultas DNS/HTTP, que es justo lo que se está probando.
37# =============================================================================
38
39set -u
40
41DOMINIO="${1:-example.com}"
42OPERADOR="${2:-${OPERADOR:-(anota aquí tu operador: Movistar, Vodafone, Digi…)}}"
43DESTINO="${PRUEBAS_DIR:-/tmp/opencode/pruebas-ciudadano}"
44
45if [ "$DOMINIO" = "-h" ] || [ "$DOMINIO" = "--help" ]; then
46 sed -n '2,32p' "$0" | sed 's/^# \{0,1\}//'
47 exit 0
48fi
49
50mkdir -p "$DESTINO" || { printf 'no se puede crear %s\n' "$DESTINO" >&2; exit 3; }
51FECHA="$(date +%Y-%m-%d_%H%M%S)"
52INFORME="$DESTINO/${FECHA}-prueba-${DOMINIO}.txt"
53: > "$INFORME" || exit 3
54
55ULTIMO_RC=0
56salida() { printf '%s\n' "$*" | tee -a "$INFORME"; }
57orden() { printf '\n$ %s\n' "$*" | tee -a "$INFORME"; }
58run() { # run <comando…> → lanza, guarda la salida y el código, nunca aborta
59 local cabecera salida_cmd rc=0
60 cabecera="$(printf '$'; for a in "$@"; do
61 case "$a" in
62 *" "*) printf " '%s'" "$a" ;;
63 *) printf ' %s' "$a" ;;
64 esac
65 done)"
66 printf '\n%s\n' "$cabecera" | tee -a "$INFORME"
67 salida_cmd="$("$@" 2>&1)" || rc=$?
68 printf '%s\n' "$salida_cmd" | tee -a "$INFORME"
69 if [ "$rc" -ne 0 ]; then printf '[exit %s]\n' "$rc" | tee -a "$INFORME"; fi
70 ULTIMO_RC=$rc
71 return 0
72}
73
74# ------------------------------------------------------------------ cabecera
75salida "================================================================"
76salida " PRUEBA DE ACCESO — prueba del ciudadano"
77salida "================================================================"
78salida " Dominio : $DOMINIO"
79salida " Operador : $OPERADOR"
80salida " Fecha : $(date -Is)"
81salida " Equipo : $(hostname) · $(uname -sr)"
82salida "================================================================"
83salida ""
84salida "Nota: esta prueba NO dice quién bloquea ni por qué. Solo deja constancia"
85salida "fechada de lo que ha pasado desde tu conexión. Una prueba, o varias, es"
86salida "lo que se adjunta a una reclamación."
87
88# --------------------------------------------------------------- 1 · curl
89salida ""
90salida "--- 1 · ¿El sitio responde? -------------------------------------"
91if command -v curl >/dev/null 2>&1; then
92 run curl -sS -o /dev/null \
93 --connect-timeout 10 --max-time 20 \
94 -w 'HTTP %{http_code} · connect %{time_connect}s · TLS %{time_appconnect}s · total %{time_total}s · IP destino %{remote_ip}\n' \
95 "https://$DOMINIO/"
96 RC_CURL=$ULTIMO_RC
97 orden "curl -v https://$DOMINIO/ # el error tal cual, para adjuntar"
98 curl -v -o /dev/null --connect-timeout 10 --max-time 20 "https://$DOMINIO/" 2>&1 \
99 | tee -a "$INFORME"
100else
101 salida "curl no está instalado: sin esta parte no hay prueba de acceso."
102 RC_CURL=127
103fi
104
105# --------------------------------------------------------------- 2 · dig
106salida ""
107salida "--- 2 · ¿Resuelve el nombre? ------------------------------------"
108IP_A=""
109if command -v dig >/dev/null 2>&1; then
110 run dig "$DOMINIO" A
111 IP_A="$(dig +short A "$DOMINIO" | grep -E '^[0-9.]+$' | head -1)"
112 run dig +short "$DOMINIO" AAAA
113 run dig +short "$DOMINIO" NS
114 if [ -n "$IP_A" ]; then
115 salida ""
116 salida "Primera dirección IPv4 anotada: $IP_A"
117 fi
118else
119 salida "dig no está instalado (paquete bind-utils / dnsutils)."
120fi
121
122# --------------------------------------------------------------- 3 · whois
123salida ""
124salida "--- 3 · ¿De quién es esa dirección? -----------------------------"
125salida "(si es de un rango compartido, no está caído «tu» servidor:"
126salida " está caído un vector que usan cientos de miles de sitios)"
127if [ -z "$IP_A" ]; then
128 salida "Sin dirección IPv4 no hay nada que consultar."
129elif command -v whois >/dev/null 2>&1; then
130 run whois "$IP_A"
131else
132 salida "No hay 'whois' instalado en este equipo. Alternativa sin instalar nada:"
133 salida "RDAP, el registro de direcciones que habla HTTP."
134 if command -v curl >/dev/null 2>&1; then
135 orden "curl -sSL https://rdap.org/ip/$IP_A"
136 RDAP="$(curl -sSL --connect-timeout 10 --max-time 20 "https://rdap.org/ip/$IP_A" 2>&1)" \
137 || salida "(la consulta RDAP ha fallado)"
138 if command -v jq >/dev/null 2>&1 && printf '%s' "$RDAP" | jq -e . >/dev/null 2>&1; then
139 printf '%s' "$RDAP" | jq -r '
140 " titular : " + (.name // .handle // (.entities[0].handle // "?")),
141 " registro : " + (.handle // "?"),
142 " país : " + (.country // "?"),
143 " rango : " + (
144 if .["cidr0_cidrs"] then
145 (.["cidr0_cidrs"][0] | "\(.v4prefix // .v6prefix)/\(.length)")
146 elif .startAddress then "\(.startAddress) - \(.endAddress)"
147 else "?" end
148 )
149 ' | tee -a "$INFORME"
150 else
151 printf '%s\n' "$RDAP" | head -40 | tee -a "$INFORME"
152 fi
153 fi
154fi
155
156# ------------------------------------------------------------- veredicto
157salida ""
158salida "--- Resultado ---------------------------------------------------"
159if [ -n "$IP_A" ] && [ "${RC_CURL:-1}" -eq 0 ]; then
160 salida " ALCANZABLE: el sitio ha respondido desde esta conexión."
161 salida " Si aún así a ti te falla a ratos, repite esta prueba cada vez que"
162 salida " falle: lo que vale es la serie con fecha y hora, no una sola foto."
163 VEREDICTO=0
164elif [ -z "$IP_A" ]; then
165 salida " NO RESUELVE EL NOMBRE: el dominio no se ha podido traducir a dirección."
166 salida " Prueba desde otra red (datos móviles, otra casa) para saber si es"
167 salida " cosa de tu operador o del dominio."
168 VEREDICTO=2
169else
170 salida " NO ALCANZABLE: el nombre resuelve ($IP_A) pero la conexión no llega."
171 salida " Guarda este fichero: es exactamente la prueba que hay que adjuntar."
172 VEREDICTO=1
173fi
174
175# ------------------------------------------------------- qué hacer ahora
176cat <<'TEXTO' | tee -a "$INFORME"
177
178--- Y ahora, los otros dos pasos ---------------------------------
179 2 · MEDIR. Instala OONI Probe (libre, sin registro) y lanza la prueba
180 desde tu propia red: las mediciones se publican solas en OONI
181 Explorer. Una anomalía sola no prueba nada, pero suma.
182 App e instrucciones : https://ooni.org/install/
183 Mediciones públicas : https://explorer.ooni.org/
184
185 3 · DENUNCIAR (esto deja constancia; no hay caso documentado de una
186 reclamación individual que levante uno de estos bloqueos):
187 · Tu operador, primero, y pide respuesta por escrito.
188 · Oficina de Atención al Usuario de Telecomunicaciones (SETELECO):
189 https://usuariosteleco.digital.gob.es/reclamaciones/telefonia-e-internet/tramitacion-electronica
190 · AEPD, solo si hubo inspección de comunicaciones o tratamiento
191 indebido de datos:
192 https://www.aepd.es/
193 · CNMC, como coordinador de servicios digitales (DSA):
194 https://www.cnmc.es/sectores-que-regulamos/servicios-digitales-dsa/reclamaciones
195
196 Guarda también: capturas con la fecha visible, tu operador y, si puedes,
197 la misma prueba desde otra conexión.
198TEXTO
199
200salida ""
201salida "Informe guardado en: $INFORME"
202# Sin códigos ANSI a propósito: la salida se guarda tal cual y se puede pegar en
203# un informe o un tique de incidencia sin que aparezcan escapes de color.
204printf '\nInforme guardado en: %s\n' "$INFORME"
205printf 'Código de salida: %s (0 responde · 1 no responde · 2 no resuelve)\n\n' "$VEREDICTO"
206exit "$VEREDICTO"
207
demo-04-borde-haproxy.sh Ham
1#!/usr/bin/env bash
2# =============================================================================
3# demo-04 · El borde con HAProxy
4# =============================================================================
5# A qué punto del guion sirve: puntos 16, 17, 18 y 19
6# · 16 «HAProxy en bare metal» → el borde corre en el HOST,
7# fuera del contenedor.
8# · 17 «Directo al 443» → el puerto privilegiado lo abre
9# HAProxy, en el metal.
10# · 18 «Traefik se levanta en el 8443» → el contenedor solo publica un
11# puerto alto.
12# · 19 «Y no necesito reglas especiales» → sin ip_unprivileged_port_start,
13# sin setcap, sin iptables.
14#
15# Qué se ve en pantalla:
16# 1. `config/haproxy.cfg` entero, con las tres líneas clave señaladas.
17# 2. El recorrido: :443 → HAProxy → `send-proxy-v2` → Traefik :8443.
18# 3. La validación de sintaxis de verdad, con `haproxy -c` y un control
19# negativo (un fichero roto a propósito) para demostrar que el chequeo
20# no dice «OK» a todo.
21#
22# Estado: VERIFICADO en local. La validación se ejecuta con HAProxy 3.4.7
23# dentro de un contenedor EFÍMERO (`--rm`, sin puertos publicados y sin tocar
24# el host). Salida real en `verificacion/demo-04-salida.txt`.
25# En esta máquina NO hay binario `haproxy` ni `docker` instalados: por eso la
26# validación va por contenedor y ese camino es el que está verificado.
27#
28# Si no hay ni `haproxy` ni `podman` ni `docker`, el script NO falla: avisa de
29# que la sintaxis queda sin validar y muestra el comando exacto para hacerlo.
30# =============================================================================
31
32set -u
33
34AQUI="$(cd "$(dirname "$0")" && pwd)"
35CFG="$AQUI/config/haproxy.cfg"
36# Imagen del validador. Se mira primero la caché local y, si no hay ninguna, se
37# descarga. Son las mismas versiones que el binario oficial de HAProxy:
38# · docker.io/library/haproxy:lts-alpine → la canónica
39# · mirror.gcr.io/library/haproxy:lts-alpine → espejo público (el Hub devolvió
40# error de autenticación al preparar este material, así que el espejo es lo
41# que acabó funcionando)
42# · docker.io/library/haproxy:latest → por si ya la tienes por casa
43CANDIDATAS=(
44 "docker.io/library/haproxy:lts-alpine"
45 "mirror.gcr.io/library/haproxy:lts-alpine"
46 "docker.io/library/haproxy:latest"
47)
48IMAGEN="${HAPROXY_IMAGEN:-${CANDIDATAS[0]}}"
49TMP="${TMPDIR:-/tmp/opencode}"
50
51resolver_imagen() { # elige la primera imagen disponible (caché primero, luego descarga)
52 local img
53 for img in "${CANDIDATAS[@]}"; do
54 if podman image exists "$img" >/dev/null 2>&1; then IMAGEN="$img"; return 0; fi
55 done
56 for img in "${CANDIDATAS[@]}"; do
57 printf ' (descargando %s…)\n' "$img"
58 if podman pull "$img" >/dev/null 2>&1; then IMAGEN="$img"; return 0; fi
59 done
60 return 1
61}
62
63titulo() { printf '\n\033[1m── %s\033[0m\n' "$1"; }
64orden() { printf '\n\033[1m$ %s\033[0m\n' "$1"; }
65nota() { printf ' ↳ %s\n' "$1"; }
66ok() { printf ' \033[1m✔ %s\033[0m\n' "$1"; }
67fallo() { printf ' \033[1m✘ %s\033[0m\n' "$1"; }
68
69rc_final=0
70
71printf '\033[1mEl borde en bare metal\033[0m (puntos 16, 17, 18 y 19)\n'
72
73# -----------------------------------------------------------------------------
74titulo "1 · El fichero del borde"
75# -----------------------------------------------------------------------------
76orden "cat config/haproxy.cfg"
77cat "$CFG"
78
79# -----------------------------------------------------------------------------
80titulo "2 · Las cuatro líneas que sostienen el diseño"
81# -----------------------------------------------------------------------------
82orden "grep -nE 'mode tcp|bind \\*:443|send-proxy-v2|127.0.0.1:8443' config/haproxy.cfg"
83grep -nE 'mode tcp|bind \*:443|send-proxy-v2|127.0.0.1:8443' "$CFG"
84nota "'bind *:443' → el puerto privilegiado lo abre el HOST (punto 17)."
85nota "'mode tcp' → no descifra: pasa la conexión tal cual."
86nota "'send-proxy-v2' → avisa al siguiente salto de quién llama."
87nota "'127.0.0.1:8443' → Traefik, en rootless, en un puerto alto (punto 18)."
88
89# Comprobación de que están de verdad (no solo de que el grep los encuentre).
90for patron in 'bind \*:443' 'send-proxy-v2' '127\.0\.0\.1:8443'; do
91 if grep -qE "$patron" "$CFG"; then
92 ok "presente: $patron"
93 else
94 fallo "falta: $patron"
95 rc_final=1
96 fi
97done
98
99# -----------------------------------------------------------------------------
100titulo "3 · Los tres apaños que NO hacen falta (punto 19)"
101# -----------------------------------------------------------------------------
102orden "grep -nE 'ip_unprivileged_port_start|setcap|iptables|nftables' config/haproxy.cfg # solo aparecen en los comentarios"
103if grep -nE 'ip_unprivileged_port_start|setcap|iptables|nftables' "$CFG"; then
104 nota "aparecen mencionados en los comentarios, para explicar por qué no se usan;"
105 nota "ninguno está como directiva activa (en HAProxy no existen como directivas)."
106else
107 nota "ni siquiera se mencionan."
108fi
109nota "El motivo, en una frase: el contenedor no abre ningún puerto < 1024,"
110nota "así que no hay nada que habilitar dentro del espacio de nombres."
111
112# -----------------------------------------------------------------------------
113titulo "4 · Validar la sintaxis con HAProxy de verdad"
114# -----------------------------------------------------------------------------
115validar() { # validar <fichero> → 0 si HAProxy acepta la configuración
116 if command -v haproxy >/dev/null 2>&1; then
117 haproxy -c -f "$1"
118 elif command -v podman >/dev/null 2>&1; then
119 podman run --rm -v "$1:/usr/local/etc/haproxy/haproxy.cfg:ro" \
120 "$IMAGEN" haproxy -c -f /usr/local/etc/haproxy/haproxy.cfg
121 elif command -v docker >/dev/null 2>&1; then
122 docker run --rm -v "$1:/usr/local/etc/haproxy/haproxy.cfg:ro" \
123 "$IMAGEN" haproxy -c -f /usr/local/etc/haproxy/haproxy.cfg
124 else
125 return 127
126 fi
127}
128
129if command -v haproxy >/dev/null 2>&1; then
130 orden "haproxy -v"
131 haproxy -v | head -2
132 VALIDADOR="haproxy (binario del sistema)"
133elif command -v podman >/dev/null 2>&1; then
134 [ -n "${HAPROXY_IMAGEN:-}" ] || resolver_imagen || true
135 if podman run --rm "$IMAGEN" haproxy -v >/dev/null 2>&1; then
136 orden "podman run --rm $IMAGEN haproxy -v"
137 podman run --rm "$IMAGEN" haproxy -v 2>&1 | head -2
138 VALIDADOR="HAProxy en contenedor efímero (podman)"
139 nota "El contenedor se borra solo (--rm): no deja nada en el host."
140 nota "Imagen usada: $IMAGEN"
141 else
142 VALIDADOR=""
143 fi
144elif command -v docker >/dev/null 2>&1; then
145 if docker run --rm "$IMAGEN" haproxy -v >/dev/null 2>&1; then
146 orden "docker run --rm $IMAGEN haproxy -v"
147 docker run --rm "$IMAGEN" haproxy -v 2>&1 | head -2
148 VALIDADOR="HAProxy en contenedor efímero (docker)"
149 else
150 VALIDADOR=""
151 fi
152else
153 VALIDADOR=""
154fi
155
156if [ -n "$VALIDADOR" ]; then
157 orden "haproxy -c -f config/haproxy.cfg"
158 if validar "$CFG"; then
159 ok "configuración válida (exit 0). Validador: $VALIDADOR"
160 else
161 fallo "HAProxy NO acepta la configuración."
162 rc_final=1
163 fi
164
165 # --- control negativo: si el chequeo dijera «OK» a todo, no valdría nada ---
166 orden "control negativo: una copia con 'send-proxy-v9' (no existe)"
167 ROTO="$(mktemp "$TMP/haproxy-roto-XXXXXX.cfg")"
168 sed 's/send-proxy-v2/send-proxy-v9/' "$CFG" > "$ROTO"
169 if validar "$ROTO" >/dev/null 2>&1; then
170 fallo "ha aceptado una directiva inventada: la validación no vale."
171 rc_final=1
172 else
173 ok "la copia rota se rechaza → el chequeo es real (exit != 0)."
174 fi
175 rm -f "$ROTO"
176else
177 nota "No hay 'haproxy', ni 'podman', ni 'docker' en esta máquina:"
178 nota "la sintaxis queda SIN VALIDAR. Para validarla, en cualquier equipo:"
179 nota " podman run --rm -v \"\$PWD/config/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro\" \\"
180 nota " $IMAGEN haproxy -c -f /usr/local/etc/haproxy/haproxy.cfg"
181fi
182
183# -----------------------------------------------------------------------------
184titulo "5 · El recorrido, para contarlo en voz alta"
185# -----------------------------------------------------------------------------
186cat <<'TEXTO'
187 visitante ──:443──► HAProxy (HOST, servicio del sistema)
188 │ mode tcp: no descifra nada
189 │ send-proxy-v2: antepone quién llama
190 ▼
191 Traefik :8443 (rootless, en su contenedor)
192 │ proxyProtocol.trustedIPs: solo ese borde
193 │ forwardedHeaders: lee la IP de verdad
194 ▼
195 tu web, tus logs y la analítica
196
197 · El 443 no lo pelea el contenedor: lo abre el host. Por eso no hacen falta
198 ip_unprivileged_port_start, ni setcap, ni reglas de iptables.
199 · Sin certificados duplicados: HAProxy no termina TLS, solo lo atraviesa.
200 · Cada cosa en la capa que le toca: el puerto en el metal, el enrutado en el
201 contenedor rootless.
202TEXTO
203
204printf '\n'
205if [ "$rc_final" -eq 0 ]; then
206 printf '\033[1mResultado: el diseño del borde está completo y validado.\033[0m\n\n'
207else
208 printf '\033[1mResultado: hay algo que revisar (ver arriba).\033[0m\n\n'
209fi
210exit "$rc_final"
211