Red, routing y túneles Todo lo que mueve paquetes o decide por dónde pasan. Headscale Trucos, líos y soluciones varias para cuando Headscale decide hacerse el interesante. Cambiar direcciones IP asignadas a los nodos en Headscale Introducción Modificar la IP de un nodo en Headscale puede ser útil para tener un orden lógico o asignar direcciones específicas según el uso. Esto se puede hacer directamente desde la base de datos SQLite que usa Headscale. Pasos para cambiar la IP 1. Parar Headscale Con Docker: docker-compose down Con systemd: sudo systemctl stop headscale 2. Entrar en la base de datos sudo sqlite3 /ruta/a/la/base/de/datos/db.sqlite 3. Cambiar la IP de un nodo Consulta SQL genérica: update nodes set ipv4 = 'nueva-ipv4' where ipv4 = 'ip-actual'; Ejemplo concreto: update nodes set ipv4 = '100.64.0.10' where ipv4 = '100.64.0.14'; 4. Salir de SQLite .quit 5. Volver a levantar Headscale Con Docker: docker-compose up -d Con systemd: sudo systemctl start headscale 6. Actualizar los clientes Es recomendable que los clientes se reconecten para recibir la nueva IP. Puedes apagar y encender Tailscale en cada dispositivo. Referencias Discusión en GitHub sobre cambio de IP Guía de instalación de Headscale + Headplane Error DNS al usar Tailscale con Headscale Esto pasa cuando usas Tailscale (con servidor Headscale autohospedado) en Linux y al hacer tailscale status ves este tipo de error en el health check: # Health check: # - running /usr/sbin/resolvconf -m 0 -x -a tailscale: Failed to resolve interface "tailscale": No such device # - Tailscale failed to set the DNS configuration of your device: running /usr/sbin/resolvconf -m 0 -x -a tailscale: Failed to resolve interface "tailscale": No such device Causa Tailscale espera que /etc/resolv.conf sea un enlace simbólico a systemd-resolved, pero a veces no lo es. Si ese enlace no está bien, no puede gestionar la configuración DNS correctamente y da ese error. Solución Forzar el enlace correcto: sudo ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf sudo systemctl restart tailscaled Después de eso, el error desaparece y vuelve a funcionar el estado DNS como toca. Fuente original Esto está documentado en la propia página de Tailscale, en la sección de problemas comunes con DNS en Linux: Tailscale – Linux DNS Headscale v0.26 – Consideraciones tras la actualización Introducción Notas sobre el cambio a Headscale v0.26.0 y los ajustes necesarios tras el salto a Policy v2. Este artículo recoge los dos puntos críticos que rompían la configuración previa: base_domain y los identificadores de usuario. Requisitos previos Tener ya configurado Headscale con una versión anterior funcionando. Usar Caddy como proxy inverso con TLS (por eso no se usan las opciones de TLS internas de Headscale). Haber actualizado a la versión 0.26.0 o superior. Cambios importantes en Headscale 0.26.0 Usuarios con @ obligatorio en Policy v2 Con la nueva implementación de políticas (Policy v2), todos los identificadores de usuario deben contener un @. Si no lo tienen, Headscale lanza un error como este: "Invalid Owner 'xxx'. An alias must be one of the following types..." También puedes ver este error ejecutando: docker logs headscale # o el nombre del contenedor que uses Ejemplo del policy.json antes (v1): { "tagOwners": { "tag:admin": ["usuario1"], "tag:srvadmin": ["usuario1"] }, "acls": [ { "action": "accept", "src": ["tag:admin"], "dst": ["*:*"] }, { "action": "accept", "src": ["tag:srvadmin"], "dst": [ "100.64.0.35:15134", "100.64.0.35:13515", "100.64.0.35:57000", "100.64.0.35:5134" ] } ] } Y después (v2): { "tagOwners": { "tag:admin": ["usuario1@midominio.local"], "tag:srvadmin": ["usuario1@midominio.local"] }, "acls": [ { "action": "accept", "src": ["tag:admin"], "dst": ["*:*"] }, { "action": "accept", "src": ["tag:srvadmin"], "dst": [ "100.64.0.35:15134", "100.64.0.35:13515", "100.64.0.35:57000", "100.64.0.35:5134" ] } ] } Para ver los usuarios válidos ya creados: headscale users list Migración de Policy v1 a v2 (pasos seguidos) Esto fue lo que se hizo para realizar la migración correctamente: Iniciar Headscale 0.26.0 con la variable de entorno HEADSCALE_POLICY_V1=1 o HEADSCALE_POLICY_V1: '1' activada. Se puede confirmar que ha funcionado si aparece este mensaje al arrancar: Using policy manager version: 1 Volcar la política actual a un archivo: headscale policy get > policy.json Editar policy.json y migrarlo manualmente al formato v2. Se puede comprobar si hay errores con: headscale policy check --file policy.json Cargar la política migrada: Si estás usando Headscale en contenedor Docker, el archivo debe estar en una ruta accesible desde dentro del contenedor. Por ejemplo: - './Headscale:/etc/headscale' En ese caso, el comando sería: hs policy set --file /etc/headscale/policy.json Donde hs es un alias que apunta a: docker exec Headscale headscale Reiniciar Headscale sin la variable HEADSCALE_POLICY_V1. Si todo va bien, el mensaje será: Using policy manager version: 2 base_domain no puede coincidir con server_url Otro breaking change relevante es que base_domain no puede ser igual (ni estar contenido) en el server_url. Si coinciden, MagicDNS deja de funcionar o se comporta de forma errática. Ejemplo incorrecto: server_url: https://headscale.midominio.local base_domain: midominio.local Ejemplo correcto: server_url: https://headscale.midominio.local base_domain: redprivada.lan En este caso se usó un dominio inventado, corto y sin registrar: redprivada.lan. Errores comunes o decisiones importantes Si olvidaste poner @ a los usuarios, el error no es muy claro. Hay que revisar el policy.json y asegurarse de que todos los identificadores lo llevan. Aunque el config-example.yaml trae bloques de Let's Encrypt, se ignoraron porque ya se gestiona TLS con Caddy. Puedes registrar un dominio personalizado si lo ves útil, pero por ahora solo se usa como base de resolución DNS local. La migración de v1 a v2 puede hacerse sin sobresaltos si se siguen los pasos indicados, especialmente si estás usando Docker. Resumen breve Todos los usuarios deben tener @ en su identificador. base_domain no puede coincidir con server_url. MagicDNS depende de que estos dos valores estén bien puestos. La política ahora se valida al cargar, no en tiempo de ejecución. La migración a Policy v2 se puede hacer conservando primero la configuración previa. Si usas Docker, asegúrate de que el archivo esté en una ruta montada dentro del contenedor. Notas personales Anotar bien estos dos cambios porque son minas antipersona: si se olvidan, te explota la configuración en la cara sin piedad. Referencias Changelog oficial de Headscale 0.26.0 Headscale + Headplane: tu propio servidor Tailscale autohospedado Hysteria2 Ajustes de QUIC, certificados y demás rituales para que el proxy deje de quejarse. Configuración de acceso de Hysteria a certificados de Caddy Propósito: dejar a Hysteria2 leer certificados gestionados por Caddy sin aflojar permisos globales, usando un grupo dedicado, ACLs y un drop‑in de systemd. Reproducible y reversible. Introducción Hysteria2 necesita acceso de solo lectura a las claves y certificados que Caddy guarda en su árbol de datos. Se documenta el estado que ya funciona: grupo tlsreaders, ACLs en el árbol de Caddy para permitir traversal y lectura, y SupplementaryGroups en el servicio de systemd. Características Acceso mínimo y acotado al árbol de Caddy. Renovaciones futuras cubiertas con ACLs por defecto. Sin cambiar propietario ni permisos base de Caddy. Drop‑in de systemd auditable y fácil de revertir. Requisitos previos Linux con systemd y soporte de ACL (ext4/xfs suelen tenerlo por defecto; si el FS monta sin acl, activarlo). Ruta de datos de Caddy en: /var/lib/caddy/.local/share/caddy. Servicio de Hysteria: hysteria-server.service. Usuario de servicio: hysteria. Herramientas: setfacl, getfacl, find. Privilegios de root. Desarrollo/pasos 1) Crear grupo y añadir usuarios Grupo dedicado para lectores TLS y membresías necesarias. # Grupo del sistema (si no existe) groupadd -r tlsreaders || true # Añadir miembros relevantes usermod -aG tlsreaders hysteria || true # (Opcional) si interesa que caddy herede políticas del grupo en otros contextos usermod -aG tlsreaders caddy || true Nota: la membresía del grupo surte efecto en el próximo arranque del servicio; este artículo reinicia el servicio al final. 2) ACLs de traversal en toda la cadena hasta certificados Permisos rx para el grupo en cada directorio padre, para poder atravesar rutas profundamente anidadas. # Cadena de directorios hasta el almacén de certificados de Caddy for p in \ /var \ /var/lib \ /var/lib/caddy \ /var/lib/caddy/.local \ /var/lib/caddy/.local/share \ /var/lib/caddy/.local/share/caddy \ /var/lib/caddy/.local/share/caddy/certificates do [ -d "$p" ] && setfacl -m g:tlsreaders:rx "$p" || true done 3) ACLs en el árbol de Caddy (dirs y ficheros) Directorios: rx + ACL por defecto rx para heredar en nuevas rutas. Ficheros de claves y certificados: lectura r. BASE="/var/lib/caddy/.local/share/caddy" # Directorios existentes: rx y default rx find "$BASE" -type d -exec setfacl -m g:tlsreaders:rx {} \; \ -exec setfacl -d -m g:tlsreaders:rx {} \; # Ficheros sensibles: lectura find "$BASE" -type f \( -name "*.key" -o -name "*.crt" -o -name "*.pem" \) \ -exec setfacl -m g:tlsreaders:r {} \; La default ACL en directorios hace que nuevos ficheros hereden permisos adecuados. En ficheros, la x no aplica y se traduce en lectura efectiva. 4) Drop‑in de systemd con SupplementaryGroups Añadir el grupo como suplementario en el servicio para que el demonio lo tenga activo al arrancar. install -d -m 0755 /etc/systemd/system/hysteria-server.service.d cat >/etc/systemd/system/hysteria-server.service.d/acl.conf <<'EOF' [Service] SupplementaryGroups=tlsreaders # Opcional si se endurece el servicio en el futuro: # ReadOnlyPaths=/var/lib/caddy/.local/share/caddy EOF 5) Recargar systemd y reiniciar el servicio systemctl daemon-reload systemctl restart hysteria-server.service 6) Verificación rápida # Confirmar membresía del usuario de servicio id hysteria | sed 's/\((/\n(/g' # Ver ACL efectiva sobre una clave concreta getfacl /var/lib/caddy/.local/share/caddy/certificates/*/*/*.key | sed -n '1,40p' # Ver grupos suplementarios aplicados al servicio systemctl show hysteria-server.service -p SupplementaryGroups # Log de arranque por si hay denegaciones journalctl -u hysteria-server.service -b --no-pager | tail -n 50 Script usado El script completo se mantiene en Gitea para versionado y trazabilidad: FixHysteriaCaddy.sh Errores comunes o decisiones importantes FS sin ACL: si el sistema de ficheros no monta con acl, las órdenes de setfacl no surten efecto. Revisar mount | grep -E ' / ( |,).*acl' o tune2fs -l en ext4. Ruta distinta: si Caddy usa otro XDG_DATA_HOME, ajustar BASE. Aquí se asume /var/lib/caddy/.local/share/caddy. Nombre del servicio: en algunas distros el servicio puede llamarse hysteria.service. Ajustar el drop‑in y los reinicios. Endurecimiento de systemd: si se habilitan ProtectSystem=strict, ProtectHome=yes u otras llaves, añadir ReadOnlyPaths=/var/lib/caddy/.local/share/caddy en el drop‑in para garantizar acceso. SELinux/AppArmor: en entornos con MAC, revisar denegaciones ( auditd, ausearch, aa-status). Las ACL no saltan políticas MAC. Contenedores: si Hysteria corre en contenedor, las ACL del host no aplican dentro. Exponer el directorio como volumen con permisos adecuados o usar init containers para fijarlos. Membresías mínimas: mantener el grupo tlsreaders lo más pequeño posible. Solo procesos que realmente necesiten leer claves. ⚠️ Nota crítica sobre ACL y mask Al verificar permisos con getfacl, no basta con ver que un usuario o grupo tiene r o rx: es imprescindible revisar el campo #effective. Si aparece mask::---, la ACL está anulada en la práctica, aunque exista una entrada como group:tlsreaders:r-x. En ese caso, debe corregirse explícitamente con: setfacl -m m:r /var/lib/caddy/.local/share/caddy/certificates/… Tras ajustar la máscara, #effective debe reflejar al menos r-- para los lectores TLS. Este fallo es silencioso y provoca errores de tipo permission denied aunque las ACL “parezcan” correctas. Resumen breve Grupo tlsreaders + membresía para hysteria. ACLs rx en padres y árbol de Caddy, default ACL en dirs. Lectura r en *.key/*.crt/*.pem. Drop‑in con SupplementaryGroups=tlsreaders. daemon-reload + restart. Verificar con id, getfacl y systemctl show -p SupplementaryGroups. Notas personales Mantener una tarea de verificación tras cada renovación de certificados para detectar rutas nuevas no cubiertas por herencia. Para revertir: setfacl -bR en el árbol, borrar el drop‑in y reiniciar. Quitar hysteria de tlsreaders y, si queda vacío, eliminar el grupo. Enlaces de interés Hysteria2: proxy QUIC de alto rendimiento contra bloqueos y restricciones de red NetBird Trucos, arreglos y configuraciones reales con NetBird: desde líos con relays hasta integraciones finas con Caddy, Authentik y compañía. Lo que no cuenta la documentación, contado con mala leche y comandos que funcionan. Relay de NetBird con TLS usando los certificados de Caddy Introducción Este artículo aclara cómo habilitar correctamente un relay de NetBird con TLS válido reutilizando los certificados emitidos por Caddy, sin ocupar el puerto 443 (ya reservado por el propio proxy). El objetivo principal es permitir el uso de QUIC y que el endpoint rels:// sea considerado válido por NetBird, manteniendo una separación clara entre el proxy HTTP(S) y el servicio de relay. No se trata de un despliegue completo, sino de una aclaración técnica sobre un punto concreto que suele generar confusión cuando Caddy y NetBird conviven en el mismo host. Contexto del problema Con una configuración estándar de Caddy —como la utilizada inicialmente en el Caddyfile del repositorio— el relay no quedaba operativo, aun cuando el contenedor estaba levantado y los certificados eran válidos. El estado podía comprobarse desde cualquier cliente con: netbird status -d La salida mostraba el relay TLS como no disponible: Relays: [stun:tu.dominio.netbird:3478] is Available [turn:tu.dominio.netbird:3478?transport=udp] is Available [rels://tu.dominio.netbird:443/relay] is Unavailable, reason: relay client not connected Esto indicaba que el relay TLS existía a nivel de configuración, pero no era alcanzable ni funcional desde los peers. Tras aplicar los ajustes descritos en este artículo, el estado pasó a ser: Relays: [stun:tu.dominio.netbird:3478] is Available [turn:tu.dominio.netbird:3478?transport=udp] is Available [rels://tu.dominio.netbird:33443/relay] is Available La diferencia clave es que el relay TLS deja de competir con Caddy en el puerto 443 y pasa a exponerse de forma explícita en un puerto alternativo. Requisitos previos Para que este enfoque funcione, deben cumplirse las siguientes condiciones: Caddy funcionando como reverse proxy y gestor de TLS Certificados válidos emitidos por Caddy (Let's Encrypt) Puerto TCP alternativo accesible desde Internet (ejemplo: 33443) Docker operativo en el host Enfoque adoptado El relay de NetBird no se expone a través de Caddy. En su lugar: Se reutilizan directamente los certificados generados por Caddy El contenedor del relay escucha en un puerto dedicado El endpoint rels:// apunta a ese puerto específico Este enfoque evita conflictos con el proxy HTTP y respeta el diseño de NetBird para tráfico QUIC. Configuración del contenedor Relay El contenedor del relay se lanza indicando explícitamente: Puerto de escucha alternativo Dominio TLS Ruta a los certificados emitidos por Caddy Secreto compartido con el management Ejemplo de servicio: relay: image: netbirdio/relay:latest container_name: NetBirdRelay restart: unless-stopped environment: - NB_LOG_LEVEL=debug - NB_LISTEN_ADDRESS=:33443 - NB_EXPOSED_ADDRESS=rels://tu.dominio.netbird:33443/relay - NB_AUTH_SECRET=22222222222nvxxxxxxxxxxxxxh666666So - NB_TLS_CERT_FILE=/certs/tu.dominio.netbird.crt - NB_TLS_KEY_FILE=/certs/tu.dominio.netbird.key - NB_RELAY_TLS_DOMAIN=tu.dominio.netbird ports: - 33443:33443 volumes: - /var/lib/caddy/.local/share/caddy/certificates/acme-v02.api.letsencrypt.org-directory/tu.dominio.netbird:/certs:ro El relay queda así completamente desacoplado de Caddy, pero sigue beneficiándose de su gestión automática de certificados. Registro manual del relay en NetBird El relay no se autodetecta. Debe declararse explícitamente en el management.json del servidor de NetBird: "Relay": { "Addresses": [ "rels://tu.dominio.netbird:33443/relay" ], "CredentialsTTL": "24h0m0s", "Secret": "22222222222nvxxxxxxxxxxxxxh666666So" } El valor de Secret debe coincidir exactamente con NB_AUTH_SECRET definido en el contenedor del relay. Consideraciones de red (NAT / firewall) El puerto TCP 33443 debe estar abierto y redirigido correctamente hacia el host Si el puerto no es accesible externamente, el relay nunca será utilizable, aunque todo lo demás esté bien configurado Este punto suele ser la causa más frecuente de fallos silenciosos. Errores comunes y decisiones clave No usar el puerto 443: ya está ocupado por Caddy QUIC exige TLS válido: sin certificados correctos, rels:// se descarta El relay no es automático: debe declararse en management.json Caddy no actúa como proxy del relay: solo se reutilizan sus certificados Resumen breve El relay TLS de NetBird se expone en un puerto dedicado Se reutilizan los certificados emitidos por Caddy El endpoint rels:// apunta explícitamente a ese puerto El relay debe registrarse manualmente en NetBird El puerto debe estar accesible desde Internet Enlaces de interés NetBird: Implementación de una VPN de malla basada en WireGuard y confianza cero (Parte I) NetBird: Implementación de una VPN de malla basada en WireGuard y confianza cero (Parte II) Integración de NetBird con Authentik Documentación general de NetBird Documentación avanzada para autohospedar NetBird OpenWRT Tips que no vienen en el manual (ni en los foros, a veces). OpenWRT puede ser tu mejor amigo o una puñetera caja de sorpresas. Aquí tienes atajos, trucos y salvavidas que me han evitado tirar el router por la ventana. Conexiones fallan desde LAN pero funcionan desde WAN Introducción Resumen rápido de un problema que rompía WireGuard y RustDesk al acceder desde LAN con dominios públicos. La solución fue más simple de lo esperado, pero no tan obvia si no conoces cómo funciona OpenWrt. ⚠️ Este fue mi caso concreto. Dependiendo de la configuración de cada red o router, el comportamiento puede variar. Problema Desde la red local (LAN), al acceder a servicios expuestos mediante redirección de puertos (DNAT) usando el dominio público o la IP WAN del router, la conexión fallaba. Ejemplo: mi.dominio.net:51820 no funcionaba desde LAN, pero sí desde fuera (móvil con datos). Causa OpenWrt tiene activado por defecto el loopback NAT para las reglas DNAT (también llamado reflection), lo que permite acceder desde LAN usando la IP pública del router. Pero si el loopback usa como origen la IP interna del router ( 192.168.X.X), algunos servicios lo rechazan, porque esperan que la conexión venga de una IP pública (como haría cualquier cliente externo real). Solución aplicada En el panel LuCI, dentro de la sección de redirección de puertos: Mantener activado el NAT loopback (reflection). Cambiar el parámetro "IP de origen del bucle de retorno" de: Dirección IP interna a Dirección IP externa ¿Qué hace esto? Obliga al router a simular el tráfico como si viniera realmente del exterior (usando su IP pública). Así, los servicios como WireGuard o RustDesk no lo rechazan. Se evita tener que hacer apaños como hijack DNS, split-horizon o reglas extra de firewall. Resultado ✔️ WireGuard conecta sin problemas desde LAN usando el dominio externo. ✔️ RustDesk también funciona correctamente desde dentro. ✔️ No hizo falta tocar reglas, ni montar DNS especial ni nada raro. Resumen rápido Fallo al acceder desde LAN usando dominios públicos con redirección de puertos. Loopback NAT activado, pero usaba IP interna como origen. Cambio a IP externa como origen: todo arreglado. Solución limpia sin DNS ni reglas extra. Notas personales No hacía falta desactivar el loopback, ni liarse con split-DNS. El fallo fue por no fijarse en la IP de origen del loopback. Referencias Documentación de OpenWrt sobre el firewall para mas información.