wandres.dev
P2P II · libp2p, iroh y QUIC

Perforar NAT en la práctica: la coreografía, el porcentaje y el plan B

Perforar la traducción de direcciones consiste en que ambos extremos marquen exactamente a la vez, acierta en torno al setenta por ciento de los casos en libp2p, y por eso el relevo de reserva no es un lujo sino parte del diseño.

⏱ 22 min

Hay una asimetría en el corazón de internet que ninguna biblioteca puede derogar: casi cualquier máquina puede iniciar una conexión hacia una máquina pública, y casi ninguna máquina pública puede iniciar una conexión hacia una máquina doméstica. La documentación de libp2p lo formula dividiendo los nodos en públicos y no públicos, y observando que la mayoría de los nodos de casa, de oficina y todos los móviles caen del lado no público. Perforar es el conjunto de trucos que convierte esa asimetría en una simetría temporal, y funciona por una razón sorprendentemente frágil: el router de cada lado guarda una entrada en su tabla de estado cuando sale un paquete, y si el paquete del otro extremo llega mientras esa entrada sigue viva, lo deja pasar creyendo que es una respuesta. Todo lo demás —sondeos, reservas, sincronización de relojes— existe para conseguir que esos dos paquetes salgan al mismo tiempo. Y todo lo demás falla lo bastante a menudo como para que el plan de reserva no sea una nota al pie.

🎯 Al terminar esta lección sabrás
  • Explicar el mecanismo de la perforación en términos de la tabla de estado del router.
  • Reconstruir la coreografía completa de libp2p, desde el sondeo de alcanzabilidad hasta la sincronización.
  • Identificar las configuraciones de red donde la perforación falla de forma sistemática.
  • Dimensionar el relevo de reserva como componente de diseño y no como excepción.

El router dice que no, y tiene sus razones

Un router doméstico funciona como una puerta de un solo sentido: lo de dentro sale, lo de fuera no entra salvo que parezca respuesta a algo que salió. La documentación de libp2p describe el mecanismo con precisión: cuando un paquete atraviesa el router, este añade una quíntupla a su tabla de estado —dirección de origen, puerto de origen, dirección de destino, puerto de destino y protocolo de transporte—. Cualquier paquete entrante que coincida con una quíntupla registrada se entrega al equipo interno. Cualquier otro se descarta sin ceremonia.

Las soluciones clásicas atacan el problema desde fuera de la aplicación y las dos envejecen mal. El protocolo de configuración automática entre equipos y router permite pedir la apertura de un puerto, pero la propia documentación advierte de que en muchos escenarios está desactivado por el router o el cortafuegos, y de que puede no funcionar según el firmware. Abrir un puerto a mano funciona siempre y exige conocimientos técnicos que ningún producto de consumo puede presuponer, además de no llevar consigo ninguna autenticación ni autorización.

Queda entonces el retransmisor puro: el nodo A mantiene una conexión permanente con un nodo relevo R, y cuando B quiere hablar con A se conecta a R, que reenvía todos los paquetes de la conversación. Funciona siempre y por eso es la red de seguridad, pero la documentación es franca sobre su precio: añade latencia y consume recursos, porque R tiene que manejar todo el tráfico de todos. La pregunta que da origen a la perforación está escrita casi con esas palabras en el manual: ¿y si usáramos R para facilitar una conexión directa entre A y B en lugar de para transportarla?

La coreografía completa: sondear, reservar, sincronizar

libp2p se inspira declaradamente en el protocolo de establecimiento interactivo de conectividad, pero con una diferencia que define su carácter: perfora sin servidores centrales de coordinación, sin la infraestructura clásica de sondeo y retransmisión. El procedimiento se divide en dos fases, y la primera ocurre mucho antes de que exista intención de hablar con nadie en concreto.

En la fase de preparación, el nodo empieza por averiguar si acaso necesita perforar. El protocolo de autodiagnóstico de alcanzabilidad —equivalente funcional del sondeo clásico— consiste en que el nodo pide a otros pares que intenten marcarle en las direcciones que sospecha que son suyas, direcciones que ha aprendido sobre todo mediante identify, y esos pares le informan del resultado. Con esos informes el nodo se hace una idea de si es marcable desde fuera. Si no lo es, descubre relevos de forma dinámica; en la red de ficheros distribuidos que le dio origen, esto se hace consultando en la tabla de Kademlia los relevos públicos más cercanos a su propio identificador. Con cada relevo descubierto negocia una reserva: le pide que escuche conexiones en su nombre, y si el relevo acepta, el nodo puede anunciarse como alcanzable a través de él.

💡
La reserva con límites es lo que hace que haya relevos suficientes

El detalle que sostiene todo el edificio es que el relevo puede acotar los recursos que dedica a cada reserva por número de conexiones, por tiempo y por bytes. Esa acotación es la razón por la que en una red como la de ficheros distribuidos cualquier nodo público puede actuar de relevo sin consumo elevado. Sin límites, ser relevo sería un acto de caridad que solo unos pocos podrían permitirse y la red tendría cuellos de botella escasos y frágiles; con límites, el relevo pasa a ser un servicio barato y abundante que se presta de paso. Cuando evalúes cualquier diseño de perforación, busca primero dónde están los límites de recurso del intermediario: si no los encuentras, el diseño no escala aunque el protocolo sea correcto.

La fase de perforación empieza cuando A quiere hablar con B y solo dispone de la dirección anunciada a través del relevo. A conecta directamente con el relevo, le pide un canal hacia B, el relevo reenvía la petición, B la acepta, y ambos extremos quedan unidos por un canal bidireccional que acto seguido se protege con TLS. Sobre ese canal ya seguro corre el protocolo de promoción a conexión directa, que es donde ocurre la sincronización fina.

sequenceDiagram
participant A
participant R as relevo
participant B
A->>R: conectar y pedir canal hacia B
R->>B: reenviar la solicitud de canal
B->>R: aceptar
R->>A: canal establecido y protegido con TLS
A->>B: mensaje de conexion con mis direcciones directas
B->>A: mensaje de conexion con sus direcciones directas
Note over A: mide el tiempo de ida y vuelta a traves del relevo
A->>B: mensaje de sincronizacion
Note over A: espera la mitad del tiempo medido
A->>B: marcado directo
B->>A: marcado directo al recibir la sincronizacion
Note over A,B: los dos paquetes se cruzan y perforan a la vez

Ese cálculo de la mitad del tiempo de ida y vuelta es la pieza más bonita del protocolo y la más fácil de pasar por alto. A no puede decirle a B marca ahora, porque el mensaje tarda en llegar. Lo que hace es medir cuánto tarda un viaje completo a través del relevo, enviar la señal de sincronización y esperar exactamente la mitad de ese tiempo antes de marcar. B marca en cuanto recibe la señal. Si la medida fue buena, los dos marcados salen prácticamente a la vez y cada paquete encuentra al llegar la quíntupla que el otro acaba de crear. Toda la perforación se juega en esa estimación.

Por qué falla, y a quién le falla

Conviene dar cifras y darlas con su origen, porque el género abunda en optimismo. En libp2p, la tasa de éxito de la perforación ronda el setenta por ciento en condiciones reales. La documentación de iroh, para su propia implementación sobre QUIC, sitúa la cifra más arriba: en torno a nueve de cada diez configuraciones de red permiten establecer una conexión directa. Ambas cifras dicen lo mismo con distinta severidad: la perforación funciona en la mayoría de los casos y falla en una minoría que no es despreciable en absoluto.

La documentación de iroh añade dos observaciones que valen más que la cifra. La primera es dónde falla: en algunos cortafuegos corporativos y en redes de telefonía móvil. La segunda es una propiedad del comportamiento que cambia por completo cómo se depura esto: su implementación es determinista, de modo que si la perforación funciona una vez entre dos dispositivos, seguirá funcionando mientras su configuración de red no cambie. Es decir, el fallo no es aleatorio; es estructural, y por tanto reproducible.

// Los dos comportamientos que hacen falta viven juntos y no se pueden separar
#[derive(NetworkBehaviour)]
struct Conectividad {
    identify: identify::Behaviour,   // aprender que direcciones tengo
    autonat: autonat::Behaviour,     // averiguar si soy marcable
    relay: relay::client::Behaviour, // reservar plaza en un relevo
    dcutr: dcutr::Behaviour,         // promocionar a conexion directa
}

Merece la pena entender por qué se resiste ese diez o treinta por ciento restante. La perforación asume que el router asigna a una misma conexión saliente un puerto externo predecible. Cuando la traducción de direcciones asigna un puerto distinto por cada destino, la dirección que el relevo observó no sirve para que el otro extremo entre, porque el router creará una quíntupla diferente. A eso se suman las traducciones a nivel de operador, donde miles de abonados comparten dirección pública y el equipo del operador aplica políticas que el usuario no controla, y los cortafuegos corporativos que sencillamente descartan el tráfico no solicitado por política.

El plan de reserva forma parte del diseño

De todo lo anterior se deduce una regla que conviene escribir en el documento de arquitectura antes que ninguna otra: el relevo no es el modo degradado, es un modo de operación normal para una fracción conocida de tus usuarios. Si la perforación acierta en el setenta por ciento de los casos, tres de cada diez conversaciones de tu producto irán por relevo de forma permanente, no transitoria. Diseñar como si eso fuera una excepción produce sistemas que funcionan de maravilla en la mesa del equipo y son inservibles en una oficina con cortafuegos.

🚪

La quíntupla es todo

Origen, destino, puertos y protocolo. La perforación existe porque esa entrada de la tabla dura un rato y deja pasar lo que parezca respuesta.

🤝

Sincronizar es el arte

Medir el viaje completo por el relevo y esperar la mitad. Sin esa estimación no hay simultaneidad y sin simultaneidad no hay agujero.

📉

Setenta por ciento

La cifra de libp2p en condiciones reales. iroh reporta cerca de nueve de cada diez sobre QUIC. Ninguna de las dos es cien.

🛟

Relevo permanente

Para la minoría que no perfora, el relevo no es transitorio. Presupuesta su ancho de banda como gasto recurrente, no como incidencia.

La perforación no vence al cortafuegos: obtiene su permiso, y por eso siempre habrá un resto irreductible

Casi todo el material divulgativo sobre este tema usa el verbo equivocado y ese verbo contamina el razonamiento posterior. Se habla de atravesar la traducción de direcciones, de saltarse el cortafuegos, de perforar —el propio nombre de la técnica— como si hubiera algo que romper. No hay nada que romper. Lo que ocurre es exactamente lo contrario y el matiz es decisivo: la perforación consigue que el cortafuegos autorice el tráfico aplicando su regla normal, no una excepción. El router de B deja entrar el paquete de A porque encuentra una quíntupla que el propio B creó al enviar su paquete; desde el punto de vista del router, no ha ocurrido nada extraordinario, ha entregado lo que parecía una respuesta a una petición legítima de su usuario. La técnica no vulnera la política de seguridad, la satisface. De esa reformulación se derivan tres consecuencias que ordenan todo el nivel. La primera es que el éxito depende por entero de la política del router, no de tu habilidad: si el equipo del otro lado asigna puertos impredecibles o descarta por norma lo no solicitado, no hay astucia posible, y por eso el resto que no perfora es irreductible y no un defecto de implementación pendiente de arreglar. La segunda es que la sincronización es el único parámetro que controlas, y de ahí que protocolos enteros existan solo para estimar bien un tiempo de ida y vuelta; toda la ingeniería seria de perforación es, en el fondo, ingeniería de relojes. Y la tercera, la que más consecuencias tiene sobre el producto: si la perforación es una autorización y no un asalto, entonces el relevo no es el fracaso de la perforación sino su complemento estructural, la parte del diseño que atiende a los usuarios cuya política de red simplemente no concede ese permiso. Los sistemas que tratan el relevo como plan de emergencia acaban descubriendo, con el producto ya en manos de usuarios, que su plan de emergencia soporta a un tercio de la base instalada y no está dimensionado ni presupuestado para ello. Los que lo tratan desde el principio como un modo de operación de primera clase construyen algo que funciona en la oficina, en el móvil y en la red del hotel. La lección siguiente estudia una pila que hizo justo eso, y la posterior explica por qué su relevo puede permitirse ser barato.

⚔️ Mide tu propia tasa de perforación antes de creerte ninguna cifra
  1. Instrumenta tu aplicación para registrar, en cada conexión, si acabó siendo directa o por relevo, y guarda el dato con la clase de red.
  2. Recoge una semana de datos reales de usuarios y calcula tu porcentaje: casi con seguridad no coincidirá con el de ningún manual.
  3. Prueba la misma pareja de dispositivos varias veces en la misma red y comprueba la propiedad de determinismo que documenta iroh.
  4. Repite la prueba en una red móvil y en una red corporativa, que son los dos escenarios donde la documentación anticipa fallos.
  5. Estima el ancho de banda mensual de relevo que necesitarías si tu tasa de perforación cayera diez puntos.
  6. Escribe qué hace tu interfaz durante los segundos en que la conexión todavía va por relevo y aún no ha promocionado a directa.