Redes

IPv6 en tu homelab: SLAAC, NAT66 y primeros pasos para sysadmins Linux

Cuando monté mi primer homelab en serio, el router de la casa era el que hacía todo el trabajo de IPv6: recibía el prefijo del ISP, repartía direcciones y levantaba las rutas. Funcionaba, claro, hasta que quise meter un servidor Linux en el medio para exponer un servicio sin abrir puertos en el router. Ahí descubrí que el servidor no entendía nada de IPv6, que el firewall que tenía tirado abajo de la IPv4 se comía el Neighbor Discovery, y que la mitad de mis contenedores no tenía dirección IPv6 aunque el host sí.

Esta es la guía para hacer ese salto: convertir una máquina Linux en el router IPv6 de tu homelab, con autoconfiguración SLAAC para los clientes, un firewall con estado que no rompa el protocolo, y sin caer en la trampa de meter NAT66 porque es lo que “suena” a IPv4. La decisión de fondo no es técnica: es entender que IPv6 no necesita traducción de direcciones, y que el firewall —no el NAT— es lo que debe proteger tu red.

TL;DR — Lo que vas a tener al final

  • Tiempo de setup: 45-60 minutos la primera vez: 10 en confirmar el prefijo que te delegó el ISP, 15 en el sysctl y radvd, 20 en el ruleset de nftables y el resto probando desde un cliente.
  • Requisitos: un /64 libre dentro de tu prefijo delegado (o un ULA fd00::/64 propio), root en el servidor Linux, el paquete radvd del repositorio y nftables con la familia inet disponible.
  • Resultado: tus clientes obtienen dirección global por SLAAC sin tocar su configuración, el forwarding IPv6 funciona y el firewall filtra por estado. Publicas servicios IPv6 sin NAT y sin abrir puertos a ciegas.
  • Troubleshooting: si un cliente se queda con dirección link-local, casi siempre falta accept_ra=2 porque activaste forwarding. Si navega IPv4 pero no IPv6, estás bloqueando el ICMPv6 de Neighbor Discovery en la regla final.

Por qué IPv6 cambia las reglas del homelab

Hasta hace poco, la respuesta estándar para “quiero exponer mi servicio en mi homelab” era el NAT o el reenvío de puertos en el router: un puerto público que apunta a una IP privada. Ese modelo no tiene equivalente en IPv6, y ese es el punto que más confunde a quien viene de IPv4.

En IPv6 cada interfaz tiene una dirección global y cada servicio puede escuchar directamente en ella. No necesitas traducir direcciones porque no hay escasez de espacio: un /64 son 2^64 direcciones, unas 4.300 millones de veces más que todo el espacio de direcciones del IPv4. Lo que sí necesitas es una política de acceso, y eso es un firewall, no un NAT.

Aspecto IPv4 IPv6
Espacio de direcciones Escaso, obliga a compartir Por diseño no escasea (un /64 por subred)
Privacidad NAT oculta la IP interna Sin NAT: cada host es direccionable
Firewall Simples, se traduce la conexión Obligatorio: la dirección no protege nada
Resolución de vecino ARP, tabla aparte Neighbor Discovery, sobre ICMPv6 (RFC 4861)
Control del tráfico Puerto o IP de origen Ambos, y además dirección de destino

La consecuencia práctica: cuando enciendes IPv6, tu host queda alcanzable desde internet en todos los puertos que noFILTRES. Un firewall que en IPv4 podías dejar con una regla laxa porque el NAT te cubría, en IPv6 tiene que ser explícito desde el primer día.

Qué es SLAAC y por qué no es lo mismo que DHCPv6

SLAAC es el mecanismo de autoconfiguración sin estado definido en la RFC 4862. El router de tu LAN manda periódicamente un Router Advertisement, un paquete ICMPv6 que lleva el prefijo que debe usar la red y otros parámetros. El cliente que lo recibe se asigna solo una dirección, componiendo el prefijo con su propia dirección de enlace, y de paso aprende la ruta por defecto.

Lo importante es que SLAAC no reemplaza a DHCPv6: son complementarios. DHCPv6 (RFC 8415) puede entregar información que los Router Advertisements no transmiten, como el servidor DNS. Un diseño limpio de homelab usa SLAAC para la dirección y el prefijo, y opcionalmente DHCPv6 o la opción RDNSS para el DNS.

El mecanismo complementario es la delegación de prefijo (DHCPv6-PD), definida en la RFC 8415 e incorporada ahí desde la RFC 3633 original. Es lo que permite que tu router de casa reciba un /56 o un /48 del ISP y lo reparta en varios /64, uno por subred. Sin ese prefijo delegado, tu servidor Linux no tiene un /64 global propio que anunciar, y esta guía no aplica tal cual.

Antes de tocar nada, confirma qué tienes disponible:

# 1. Ver que el kernel tiene IPv6 y que ya hay una dirección global
ip -6 addr show scope global
# 2. Ver la ruta por defecto que injectó el Router Advertisement
ip -6 route show default
# 3. Ver el prefijo queCgrees que anuncia tu router de casa
ip -6 route show | grep -i "proto ra"
# 4. Confirmar el estado del reenvío y de la aceptación de RA
sysctl net.ipv6.conf.all.forwarding
sysctl net.ipv6.conf.all.accept_ra

Si el paso 1 no devuelve nada, tu ISP no te está dando IPv6 global y el escenario es otro (solo link-local, o CGNAT dual-stack). Si el paso 3 no muestra rutas con “proto ra”, tu router de casa no está anunciando prefijo: en ese caso necesitas configurar la delegación en el router o mirar la guía de PiVPN detrás de CGNAT.

Habilitar el reenvío IPv6 y ajustar accept_ra

Aquí está el detalle que más problemas causa, y la razón por la que mucha gente abandona IPv6 a mitad de camino.

El parámetro accept_ra controla si el host acepta Router Advertisements. Tiene tres valores: 0 no acepta ninguno, 1 los acepta solo si el reenvío está desactivado, y 2 los acepta aunque el reenvío esté activo. Y el valor por defecto funcional no es fijo: el kernel lo activa si el reenvío local está apagado y lo desactiva si el reenvío está encendido.

Traducido: en el momento en que activas el forwarding para convertir tu servidor en router, el kernel deja de aceptar Router Advertisements por su cuenta. Tu servidor deja de autoconfigurarse y, sin ruta por defecto, se queda sin salida a internet. La solución es fijar accept_ra=2 de forma explícita en la interfaz de salida.

# Activar reenvío IPv6 entre interfaces
sysctl -w net.ipv6.conf.all.forwarding=1
# Obligar al servidor a seguir aceptando RA en la interfaz WAN,
# porque ahora es un router y el default funcional lo desactiva
sysctl -w net.ipv6.conf.INTERFAZ_WAN.accept_ra=2
# No queremos que el servidor use las direcciones que le inyectan
# los RA: el prefijo lo gestionamos nosotros con radvd
sysctl -w net.ipv6.conf.INTERFAZ_WAN.autoconf=0
# Detección de direcciones duplicadas: 1 activa DAD (valor normal),
# 2 además desactiva IPv6 si encuentra una dirección link-local duplicada
sysctl -w net.ipv6.conf.all.accept_dad=1

Y para que sobreviva a un reinicio, escribe un archivo en /etc/sysctl.d/ en lugar de usar sysctl -w cada vez:

cat > /etc/sysctl.d/99-ipv6-router.conf << 'EOF'
net.ipv6.conf.all.forwarding=1
net.ipv6.conf.INTERFAZ_WAN.accept_ra=2
net.ipv6.conf.INTERFAZ_WAN.autoconf=0
net.ipv6.conf.all.accept_dad=1
EOF
sysctl --system

La heredoc con delimitador entre comillas simples es importante: sin las comillas, la shell expande las variables antes de escribir el archivo y las rutas de interfaz se deforman. Además sysctl --system recarga todo lo que hay en /etc/sysctl.d/, no solo tu archivo, así que revisa la salida: si algún otro archivo define forwarding en 0, tu valor se pierde.

Levantar radvd para anunciar tu /64

Con el forwarding activo, tu servidor ya reenvía paquetes. Ahora necesita anunciarles a los clientes qué prefijo usar. Eso lo hace radvd, el daemon que responde a las Router Solicitations y emite los Router Advertisements.

La configuración vive en /etc/radvd.conf y tiene una sección por interfaz. Lo mínimo para un /64 propio:

# Instalar el daemon
apt update && apt install -y radvd
# Habilitar el reenvío de IPv6 en el kernel del servidor
sysctl -w net.ipv6.conf.all.forwarding=1

Y el contenido de /etc/radvd.conf:

interface INTERFAZ_LAN
{
    AdvSendAdvert on;
    MinRtrAdvInterval 3;
    MaxRtrAdvInterval 10;
    prefix TU_PREFIJO_DELEGADO:SUBDIRECCION::/64
    {
        AdvOnLink on;
        AdvAutonomous on;
        AdvRouterAddr off;
    };
};

Dos flags deciden todo aquí. AdvAutonomous on es el que le dice al cliente “puedes autoconfigurar una dirección con este prefijo”, es decir, activa SLAAC propiamente dicho. Con AdvAutonomous off el cliente recibe el prefijo para rutas pero su dirección tendrá que venir de DHCPv6. Y AdvOnLink on le indica que ese prefijo está en la red local, para que el cliente lo use en la tabla de rutas.

Activa el servicio y verifica que quedó escuchando:

systemctl enable --now radvd
systemctl status radvd --no-pager
# Ver las Router Advertisements que se están emitiendo en el enlace
radvdump

radvdump viene en el paquete ndisc6, no en radvd: es un conjunto de herramientas de diagnóstico IPv6 aparte. Si te dice “command not found”, instálalo con apt install ndisc6.

La salida esperada de radvdump muestra la interfaz, el Router Advertisement con el Managed Flag apagado (porque no usas DHCPv6 para la dirección) y el prefijo con AdvAutonomous: on. Si el prefijo que sale no es el que configuraste, revisa que el prefijo esté realmente asignado a la interfaz LAN:

# Asignar el /64 a la interfaz LAN
ip -6 addr add TU_PREFIJO_DELEGADO:SUBDIRECCION::1/64 dev INTERFAZ_LAN
# Confirmar que quedó con vida
ip -6 addr show dev INTERFAZ_LAN

Filtrar con nftables sin romper el Neighbor Discovery

Aquí está la segunda mitad del problema y la razón por la que mucha gente concluye que “IPv6 no funciona” en su máquina. Si tu reglaset de nftables termina con un drop y no tiene una regla explícita para el ICMPv6 que el protocolo necesita, la red se apaga en silencio.

El Neighbor Discovery (RFC 4861) funciona sobre ICMPv6, y el kernel usa ese mismo protocolo para otras cosas que no son opcionales: descubrimiento de routers, resolución de vecinos y, sobre todo, los mensajes de error que mantienen vivo el Path MTU Discovery. Bloquearlos no rompe solo la resolución de vecinos: rompe la conexión y produce timeouts que parecen “IPv6 lento”.

La base mínima es una regla de estado primero y las excepciones de ICMPv6 después:

cat > /etc/nftables.conf << 'EOF'
#!/usr/sbin/nft -f
table inet filter
{
    chain input
    {
        type filter hook input priority 0; policy drop;
        iifname lo accept
        ct state established,related accept
        ct state invalid drop
        # Neighbor Discovery y mensajes de control obligatorios.
        # Los Router Advertisements llegan con hop limit 255: ese limite
        # es lo que impide que un atacante en la LAN los falsifique.
        ip6 hoplimit 255 icmpv6 type {
            nd-router-solicit,
            nd-router-advert,
            nd-neighbor-solicit,
            nd-neighbor-advert,
            nd-redirect
        } accept
        # Errores de ICMPv6: sin ellos se rompe el Path MTU Discovery
        icmpv6 type {
            destination-unreachable,
            packet-too-big,
            time-exceeded,
            parameter-problem
        } accept
        # Multicast listener discovery, para los grupos multicast de la LAN
        icmpv6 type {
            mld-listener-query,
            mld-listener-report,
            mld-listener-reduction,
            mld2-listener-report
        } ip6 saddr fe80::/10 accept
        # Lo que tú decidas abrir, explícitamente
        tcp dport 22 accept
    }
    chain forward
    {
        type filter hook forward priority 0; policy drop;
        ct state established,related accept
        ip6 hoplimit 255 icmpv6 type {
            nd-router-solicit,
            nd-router-advert,
            nd-neighbor-solicit,
            nd-neighbor-advert
        } accept
        icmpv6 type { packet-too-big, time-exceeded } accept
    }
    chain output
    {
        type filter hook output priority 0; policy accept;
    }
}
EOF
nft -c -f /etc/nftables.conf
nft -f /etc/nftables.conf
nft list ruleset

La validación con nft -c antes de aplicar es la que te salva de dejarte sin red: si la sintaxis está mal, el comando no toca nada y te dice exactamente qué línea rechazó. La regla ip6 hoplimit 255 es la que impide que alguien en tu LAN falsifique un Router Advertisement para inyectar rutas falsas.

Sobre el estado: no uses reglas que filtren por puerto de origen para el tráfico de respuesta, es la forma clásica de abrir un agujero. La regla ct state established,related accept deja pasar el tráfico de vuelta y los errores relacionados, que es exactamente lo que el Neighbor Discovery necesita para funcionar.

NAT66: cómo se hace y por qué no te lo recomiendo

Si llegaste hasta aquí, probablemente te hayan dicho que “IPv6 es como IPv4 pero más grande” y que el siguiente paso es un NAT66. Voy a darte el cómo, porque algún día lo vas a necesitar, y luego el por qué no deberías.

Primero, un malentendido frecuente que hay que aclarar: el NAT66 de tipo masquerade con estado, el que hace el kernel de Linux, no está estandarizado en ninguna RFC. El documento del IETF más cercano es la RFC 6296, “IPv6-to-IPv6 Network Prefix Translation”, de junio de 2011 y con estado Experimental, que describe una traducción stateless de prefijos 1:1, no un masquerade con estado. Y el propio texto de esa RFC recuerda, citando la RFC 2993, que el IETF no recomienda usar traducción de direcciones en IPv6.

El soporte del kernel sí existe y es serio: tanto el módulo de NAT para IPv6 (nf_nat_ipv6) como la tabla nat de ip6tables (CONFIG_IP6_NF_NAT, que compila el módulo ip6table_nat) llegaron en el kernel 3.7, en 2012. En nftables, masquerade existe desde el kernel 3.18, y el NAT con estado en la familia inet (IPv4 e IPv6 en la misma cadena) desde el 5.2.

# Con ip6tables (ruta clásica, requiere CONFIG_IP6_NF_NAT en el kernel)
ip6tables -t nat -A POSTROUTING -o INTERFAZ_WAN -j MASQUERADE
ip6tables -t nat -L POSTROUTING -n -v
# Con nftables, tabla propia para la familia ip6
cat > /etc/nftables.nat66.conf << 'EOF'
table ip6 nat66
{
    # La cadena prerouting debe existir aunque no tenga reglas:
    # es la que invoca el motor NAT para el trafico de respuesta.
    chain prerouting
    {
        type nat hook prerouting priority -100; policy accept;
    }
    chain postrouting
    {
        type nat hook postrouting priority 100; policy accept;
        oifname "INTERFAZ_WAN" masquerade
    }
}
EOF
nft -c -f /etc/nftables.nat66.conf
nft -f /etc/nftables.nat66.conf

Ahora el por qué no te lo recomiendo, que es la parte que le da valor a todo lo anterior:

  • Rompe el extremo a extremo. Con NAT66 el cliente no es alcanzable desde internet con su dirección real. Vuelves al modelo IPv4 y a la necesidad de abrir puertos uno por uno, que es justo lo que querías evitar.
  • Complica la movilidad. Cambiar de interfaz o de prefijo significa que las sesiones vivas se rompen, porque la traducción depende del prefijo de origen.
  • Se lleva por delante IPsec. Las direcciones IP entran en el cálculo del HMAC de IPsec, así que un NAT intermedio rompe la autenticación de los paquetes. Es un problema conocido y con consecuencias de seguridad.
  • El punto fuerte de IPv6 desaparece. Que un host sea direccionable es una propiedad, no un defecto. El problema real es la exposición, y ese problema lo resuelve un firewall con estado, que ya necesitas tener de todos modos.

La recomendación honesta: usa un firewall con estado que filtre por puertos y por rangos de direcciones de origen. Si de verdad necesitas traducción, por ejemplo porque un servicio interno expone una dirección que no debería salir, úsala como excepción local y no como el mecanismo central de tu red.

Verificar que todo funciona de punta a punta

El paso que más se salta la gente y que más falsos positivos genera. curl sin -6 puede salir por IPv4 y darte una falsa sensación de que todo está bien: vas a ver una dirección IPv4 en la respuesta y asumiste que probaste IPv6.

# 1. El cliente tiene que tener dirección GLOBAL, no solo link-local
ip -6 addr show scope global
# 2. La ruta por defecto tiene que apuntar a tu router interno
ip -6 route show default
# 3. Ping forzado a IPv6, con una dirección de prueba pública
ping -6 -c 3 2606:4700:4700::1111
# 4. HTTP real por IPv6, forzando el protocolo
curl -6 -s https://ifconfig.co
# 5. Ver qué devuelve el DNS para ese dominio
dig AAAA soylinux.com
# 6. Recorrido de saltos para ver por dónde sale el tráfico
traceroute -6 -n 2606:4700:4700::1111
# 7. Ver los vecinos descubiertos por Neighbor Discovery
ip -6 neigh show

El paso 4 es el que da la respuesta honesta. Si curl -6 devuelve una dirección IPv6, el camino completo funciona: resolución, vecino, reenvío y salida. Si devuelve un error de “Network is unreachable” pero ping -6 sí funciona, el problema está en el DNS por IPv6, que es el siguiente punto.

Un detalle sobre el MTU: sobre PPPoE el valor correcto es 1492, no los 1500 de Ethernet. Si configuras 1500 sobre una línea PPPoE, la conexión se corta con los paquetes grandes y el síntoma es desconexiones intermitentes en HTTPS, no un error claro de MTU.

Los dos problemas reales que vas a encontrar

El DNS resuelto por IPv6

En Debian y Ubuntu el resolvedor por defecto es systemd-resolved, y funciona bien con registros AAAA. El detalle importante: las consultas DNS se envían en paralelo a los servidores configurados y a los que cada enlace entrega por DHCPv6 o por RDNSS. Eso significa que un servidor DNS IPv6 inalcanzable no tumba la resolución por completo, pero sí añade los tiempos de espera de ese intento a cada resolución, y se nota como “todo va lento” sin que ningún componente falle visiblemente.

El caso patológico es cuando el único DNS que conoce tu sistema es una dirección IPv6 link-local del router y esa ruta está bloqueada por el firewall que acabas de escribir. Verifícalo así:

# Qué servidores DNS conoce el sistema
resolvectl status
# Comprobar la resolución forzando por IPv6
dig AAAA @DIRECCION_IPV6_DEL_RESOLVER soylinux.com
# Ver si las consultas salen por IPv6 o por IPv4
resolvectl statistics

Docker sin IPv6

El daemon de Docker corre con IPv6 desactivado por defecto. Y el detalle que hace perder tiempo a todo el mundo: enable_ipv6 no es la clave del daemon. En /etc/docker/daemon.json las claves son ipv6 y fixed-cidr-v6; enable_ipv6 es la opción de las redes creadas a mano y de los archivos de Compose.

# Revisar la configuración actual del daemon
cat /etc/docker/daemon.json
# Habilitar IPv6 en la red bridge por defecto, con un ULA propio
cat > /etc/docker/daemon.json << 'EOF'
{
  "ipv6": true,
  "fixed-cidr-v6": "fd00:dead:beef::/64"
}
EOF
systemctl restart docker
# Crear una red de usuario con IPv6 propio y rango ULA
docker network create --ipv6 --subnet fd00:beef:1::/64 red_ipv6
# Comprobar que el contenedor ya tiene dirección IPv6
docker run --rm --network red_ipv6 alpine:latest ip -6 addr show scope global

El rango ULA (fc00::/7, definido en la RFC 4193) es la elección correcta para la red interna de contenedores: es el equivalente IPv6 del rango privado de IPv4, y no choca con el prefijo global que publica el router. Para un contenedor con salida a internet hace falta además que el host tenga una dirección global, porque sin NAT el contenedor no puede salir por la puerta de atrás.

Resumen

El orden que funciona es fijo: primero confirmas que tienes un prefijo delegado de verdad, luego activas el forwarding con accept_ra=2 explícito, luego levantas radvd con AdvAutonomous on, después escribes el firewall de nftables con las reglas de ICMPv6 de Neighbor Discovery, y solo al final decides si necesitas NAT66. Si te saltas el paso del firewall, todo lo demás parece roto.

Y el marco mental: en IPv6 el trabajo que en IPv4 hacía el NAT lo hace el firewall. No es un cambio de sintaxis, es un cambio de responsabilidad. Tu servidor ya tiene una dirección global que lo hace alcanzable, y lo que decides es a quién le abres la puerta.

Preguntas frecuentes

¿Puedo usar SLAAC y DHCPv6 al mismo tiempo?

Sí, y es lo habitual en un homelab bien armado. SLAAC se encarga de la dirección y del prefijo; DHCPv6 aporta lo que los Router Advertisements no transmiten, sobre todo el servidor DNS. Configura radvd con AdvAutonomous on y el cliente obtiene dirección sin tocar nada, mientras el DNS sigue llegando por su vía.

¿Por qué mi cliente se queda con una dirección fe80:: y no obtiene una global?

Casi siempre es el accept_ra. Si activaste forwarding en el servidor, el valor funcional por defecto pasa a ser “no aceptar Router Advertisements” y el cliente no se autoconfigura. Comprueba con sysctl net.ipv6.conf.all.accept_ra y fíjate en el valor de la interfaz concreta, no solo en all.

¿Cómo sé si el tráfico sale de verdad por IPv6 y no por IPv4?

Usa curl -6 https://ifconfig.co y mira qué dirección devuelve. Un curl sin la opción -6 puede salir por IPv4 y darte un falso éxito, que es el error más común al verificar. Complementa con ping -6, dig AAAA y traceroute -6 para ver el recorrido completo.

¿NAT66 no es la forma “correcta” de ocultar mis direcciones internas?

La RFC 6296, el documento del IETF más cercano al NAT66, es Experimental y su propio texto recuerda que el IETF no recomienda traducción de direcciones en IPv6. Con NAT66 pierdes direccionabilidad, complicas el cambio de prefijo y rompes la autenticación de IPsec. La alternativa correcta es un firewall con estado que filtre por puertos y rangos.

¿Qué hago si el firewall bloquea mi propia red IPv6?

Revisa que el ruleset tenga las reglas de ICMPv6 de Neighbor Discovery antes de la política drop, y que la regla ct state established,related accept esté presente. Sin esas reglas el Neighbor Discovery falla y las conexiones se quedan colgadas. Valida siempre con nft -c -f antes de aplicar y revisa con nft list ruleset.

Lee también

Alfonso Moreno | Sysadmin Senior

Soy Alfonso, sysadmin senior con RHCE y más de 10 años administrando sistemas Linux para empresas y proyectos personales. Me especializo en infraestructura, redes, virtualización, monitoring, automation y self-hosting. Sobre SoyLinux.com SoyLinux.com es un blog técnico en español sobre Linux, software libre y self-hosting. Lo mantengo en mi tiempo libre. Áreas de expertise - Linux server administration: RHEL, CentOS, Rocky, Alma, Fedora, Debian, Ubuntu - Network services: Postfix, BIND, Samba, NFS, Squid - Virtualización y containers: KVM, Proxmox, Docker, Podman - Monitoring: Prometheus, Grafana, Zabbix - Automation: Ansible, Bash scripting - Self-hosting: Vaultwarden, Nextcloud, Jellyfin, Immich, Homelab Contacto - Email: [email protected]

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *