wandres.dev
P2P I · WebRTC y su peso

Dos clientes que quieren hablarse y no pueden

Dos navegadores detrás de NAT no tienen dirección pública estable ni pueden ponerse a escuchar en un puerto, y de esa doble imposibilidad nace la pila entera que WebRTC arrastra.

⏱ 20 min

Suena a problema resuelto hace medio siglo: dos programas en dos ordenadores quieren intercambiar bytes. La sorpresa llega cuando los dos programas son dos pestañas de navegador en dos casas distintas, porque entonces ninguna de las premisas que sostienen una conexión ordinaria se cumple. Ninguno de los dos extremos tiene una dirección a la que el otro pueda dirigirse, porque ambos viven detrás de una traducción de direcciones que fabrica identidades temporales y las inventa por conversación. Y ninguno de los dos puede quedarse esperando una llamada, porque el navegador no expone forma alguna de escuchar en un puerto ni la va a exponer nunca. La consecuencia no es que conectar sea difícil: es que la operación, enunciada así, es imposible, y lo que WebRTC ofrece no es una conexión sino un procedimiento largo para fabricar las condiciones en las que una conexión llega a ser posible. Esta lección se ocupa de ver el hueco con precisión, porque cada capa que estudiaremos después existe para tapar una parte concreta de él y no se entiende bien sin haberlo mirado primero.

🎯 Al terminar esta lección sabrás
  • Explicar por qué una dirección detrás de NAT no es una dirección a la que se pueda llamar.
  • Separar comportamiento de mapeo y comportamiento de filtrado, y por qué la taxonomía de conos confunde.
  • Enumerar con precisión qué puede y qué no puede hacer un navegador en materia de red.
  • Reconstruir el truco de la perforación y las condiciones exactas bajo las que funciona.
  • Situar cada capa de la pila frente al hueco concreto que viene a tapar.

Una dirección que no es tuya y que además caduca

Un dispositivo doméstico no tiene una dirección pública. Tiene una dirección privada dentro de su red y, cuando envía un paquete hacia fuera, el router reescribe la dirección y el puerto de origen por una pareja tomada de su propio espacio público, y anota esa correspondencia en una tabla para saber a quién devolver lo que llegue de vuelta. Esa reescritura no es una anomalía de las redes mal configuradas: es cómo funciona prácticamente todo el acceso residencial y móvil desde que las direcciones de la cuarta versión del protocolo se agotaron, y en las redes de operador móvil suele haber además una segunda traducción por encima que afecta a miles de abonados a la vez.

De aquí salen dos hechos incómodos. El primero es que la entrada de la tabla solo se crea desde dentro: nace cuando sale un paquete y nadie de fuera puede provocar su creación. Desde el exterior, la pareja pública asignada a un dispositivo simplemente no existe hasta que ese dispositivo la ha estrenado. El segundo es que la entrada caduca, y para tráfico sin conexión los plazos habituales van de decenas de segundos a unos pocos minutos, con la recomendación de la especificación situada en dos minutos como mínimo. Una conexión que se queda callada deja de existir sin que ninguno de los dos extremos se entere, y por eso todo protocolo que atraviese una traducción necesita tráfico de mantenimiento aunque no tenga nada que decir.

// La misma conversacion vista desde los dos lados de la traduccion
const dentro = { ip: "192.168.1.34", puerto: 51872 };  // lo que asigna el sistema
const fuera  = { ip: "88.12.7.201",  puerto: 40311 };  // lo que fabrica el router

// La tabla del router es lo unico que une las dos vistas, y es efimera:
// clave -> [ip interna, puerto interno, destino, caducidad]
// Nadie dentro de la maquina puede leerla. Nadie fuera puede escribirla.

Lo que de verdad hay que entender es que hay dos comportamientos independientes y que confundirlos es el origen de casi todos los malentendidos sobre este tema. El primero es el de mapeo: si el router reutiliza la misma pareja pública para todos los destinos, el mapeo es independiente del extremo remoto y la dirección pública que descubras hablando con un tercero sirve para hablar con cualquiera. Si en cambio fabrica una pareja distinta según a dónde vayas, el mapeo depende de la dirección y del puerto de destino, y entonces averiguar tu dirección pública frente a un tercero no te dice absolutamente nada sobre la que tendrás frente a un cuarto. El segundo comportamiento es el de filtrado: cuál es el criterio para dejar entrar lo que llega a una pareja ya existente, que puede ser cualquier origen, solo la dirección con la que hablaste o solo esa dirección y ese puerto exactos.

La vieja terminología de conos, que sigue circulando, mezcla ambos ejes en una sola etiqueta y por eso pierde información. Conviene abandonarla y hablar siempre de las dos dimensiones por separado, porque las combinaciones reales no se agotan en cuatro casos y porque la única que hace verdaderamente imposible la conexión directa es la primera dimensión: si el mapeo depende del destino en los dos extremos, no hay forma de que ninguno adivine a qué pareja debe escribir.

ℹ️
El filtrado se abre solo; el mapeo impredecible no se arregla

Merece la pena fijar la asimetría porque ordena todo lo que viene después. Un filtrado estricto no es un obstáculo grave: basta con que el paquete de salida haya ocurrido antes de que llegue el de entrada, y eso es exactamente lo que el procedimiento de perforación provoca a propósito. Un mapeo dependiente del destino sí es un obstáculo estructural, porque el número de puerto que el router asignará hacia el otro extremo no es conocible por nadie antes de que ese primer paquete salga, y para entonces el otro lado ya necesitaba saberlo. Cuando leas que una conexión ha necesitado relevo, la causa casi siempre está en esta segunda dimensión.

Lo que un navegador puede hacer con la red

Aun suponiendo resuelto lo anterior, queda un segundo muro que es puramente de plataforma. Todas las interfaces de red que el navegador expone comparten una forma: son iniciadas por el cliente contra un nombre que resuelve a un servidor que ya estaba escuchando. Pides un recurso, abres un flujo de eventos, estableces un canal bidireccional contra una dirección conocida. En ninguna de ellas existe la operación de reservar un puerto y quedarse esperando, y esa ausencia no es un descuido pendiente de arreglo.

// Todo lo que el navegador ofrece va en la misma direccion: hacia fuera.
await fetch("https://ejemplo.com/recurso");          // peticion y respuesta
new EventSource("https://ejemplo.com/eventos");      // flujo del servidor al cliente
new WebSocket("wss://ejemplo.com/canal");            // bidireccional, pero yo llamo
new WebTransport("https://ejemplo.com/wt");          // bidireccional, pero yo llamo

// No existe, y no va a existir, nada parecido a esto:
// escuchar en el puerto 9000 y aceptar lo que llegue.

La razón es de seguridad y es sólida. Una página que visitas durante tres segundos no debe poder abrir un servicio accesible en tu máquina, ni escanear tu red interna probando puertos, ni convertir tu portátil en un nodo de algo que no has autorizado. El modelo de la web se apoya en que el código no confiable solo puede hablar hacia afuera y solo con destinos que se identifican y consienten. Cualquier interfaz que rompiera esa regla se convertiría en una herramienta de abuso el mismo día de su publicación.

De aquí sale la conclusión que gobierna el nivel entero. Como el navegador no puede escuchar, la única salida posible es que los dos extremos hablen hacia afuera a la vez y sus paquetes se crucen en el camino, y como para eso hace falta saber hacia dónde hablar y coordinar el momento, hace falta un tercero. No hay una versión más simple de esto. Todo lo que WebRTC contiene es la maquinaria para ejecutar esa idea con fiabilidad razonable sobre redes reales.

Perforar: el único truco disponible

El procedimiento se llama perforación y su núcleo cabe en un párrafo. Cada extremo averigua, preguntándoselo a un servidor externo, con qué dirección pública se le ve. Los dos se intercambian esa información por un canal aparte. Entonces los dos empiezan a enviar paquetes hacia la dirección pública del otro. Los primeros se pierden, porque llegan a un router que todavía no tiene entrada para ese origen, pero cada uno de esos envíos fallidos ha creado la entrada del lado del que envía. Cuando ambos han enviado al menos una vez, los envíos siguientes encuentran ya el camino abierto en el otro extremo y la conversación queda establecida.

sequenceDiagram
participant A as Par A
participant NA as NAT de A
participant NB as NAT de B
participant B as Par B
A->>NA: paquete hacia la direccion publica de B
NA->>NB: crea mapeo de salida y viaja
NB--xNA: descartado, no hay entrada para ese origen
B->>NB: paquete hacia la direccion publica de A
NB->>NA: crea mapeo de salida y viaja
NA->>A: aceptado, el mapeo ya existia
A->>B: a partir de aqui el camino esta abierto
B->>A: y se mantiene con trafico periodico

Tres condiciones tienen que cumplirse a la vez y conviene tenerlas escritas. La primera es que exista un tercero capaz de decirle a cada uno cómo se le ve desde fuera, que es el papel del servidor de sondeo que veremos en la tercera lección. La segunda es que exista un canal previo por el que intercambiar esa información, que es la señalización de la lección siguiente y el punto donde reaparece la infraestructura que se pretendía evitar. La tercera es que al menos uno de los dos routers tenga mapeo independiente del destino, porque si ambos fabrican puertos distintos por destino, la dirección que cada uno anunció no es la que el otro necesita.

Añade a eso que el intercambio es una carrera con reintentos, que hay que probar varias parejas de direcciones porque cada extremo suele tener varias, que la conexión establecida hay que refrescarla para que no caduque y que un cambio de red la invalida entera, y ya tienes el esbozo de por qué esto no lo resuelve una función de veinte líneas sino una máquina de estados con nombre propio.

Por qué hace falta una pila entera

Puesto todo junto, la lista de piezas necesarias sale sola, y cada una responde a una carencia distinta que ya hemos nombrado.

📡

Un punto de encuentro

Alguien tiene que poner en contacto a los dos extremos antes de que exista conexión. WebRTC no lo especifica, y esa omisión es el tema de la lección siguiente.

📡

Descubrir y probar direcciones

Cada extremo tiene varias direcciones posibles y ninguna es fiable a priori, así que hay que enumerarlas todas y probarlas por parejas hasta que una funcione.

📡

Un relevo para lo imposible

Cuando el mapeo depende del destino en ambos lados no hay perforación posible, y la única salida es un servidor que reenvíe los datos por cuenta de los dos.

📡

Cifrado y semántica de entrega

Lo que queda es un flujo sin conexión y sin protección, de modo que hace falta cifrarlo y montarle encima algo que ordene, reintente y trocee.

Cada tarjeta tiene su capa correspondiente y todas ellas nacieron con un propósito muy concreto: llevar voz y vídeo entre navegadores con la menor latencia posible. Esa procedencia explica decisiones que de otro modo parecen arbitrarias, como que la descripción de la sesión use un formato heredado de la telefonía sobre red, o que el canal de datos viaje sobre un protocolo de transporte pensado para señalización telefónica y encapsulado dentro de otro pensado para tráfico sin conexión. Nada de eso se diseñó para sincronizar documentos, y sin embargo es lo único que el navegador ofrece si quieres una conexión directa.

⚠️
La conexión directa no es un estado, es un resultado que puede no llegar

Un error de diseño frecuente consiste en escribir el código como si la conexión entre pares fuese algo que se abre y se tiene, igual que un canal contra un servidor. No lo es. Es el desenlace probable de un procedimiento con varios pasos que puede fallar en cualquiera de ellos, que tarda un tiempo variable con una cola larga y que se deshace por completo cuando el dispositivo cambia de red, cosa que en un móvil ocurre varias veces al día. Todo lo que construyas encima debe asumir que el transporte puede no estar disponible nunca y que, si lo estuvo, puede desaparecer sin aviso.

El obstáculo no es el NAT: es que la web se diseñó sin la idea de que un cliente pueda ser destino

Es tentador leer todo este nivel como una guerra contra la traducción de direcciones, y esa lectura deja fuera lo más importante. La traducción es solo el síntoma visible de una decisión mucho más antigua y mucho más profunda: la web se construyó sobre una asimetría en la que unas máquinas ofrecen y otras piden, y esa asimetría no vive en los routers sino en cada capa del edificio. Vive en el sistema de nombres, que resuelve servicios y no personas ni dispositivos. Vive en el modelo de certificados, que acredita autoridades sobre dominios y no tiene nada que decir sobre un portátil que se enciende a ratos. Vive en el modelo de seguridad del navegador, que permite hablar hacia fuera y prohíbe escuchar. Y vive, sobre todo, en el modelo económico: quien ofrece paga por estar disponible, quien pide no paga nada por existir. Si mañana toda la red usase la sexta versión del protocolo y hubiese direcciones públicas de sobra para cada dispositivo, el navegador seguiría sin poder aceptar una conexión, porque lo que lo impide no es la escasez de direcciones sino el papel que se le asignó. De ahí se sigue la consecuencia que conviene llevarse a cualquier diseño local-first: la conexión directa entre dos clientes no es la vuelta a un estado natural que el NAT arruinó, es una excepción cara que hay que fabricar contra la corriente de toda la pila. Los sistemas que la tratan como el caso normal y al servidor como una concesión temporal terminan descubriendo, cuando ya han escrito mucho código, que la excepción no ocurre para una fracción sustancial de sus usuarios y que no hay nada que ajustar para arreglarlo. Los que la tratan como una optimización oportunista sobre un camino que ya funciona sin ella obtienen lo mejor de las dos posturas: aprovechan la ruta directa cuando aparece, y no dependen de que aparezca. Esa distinción de encuadre, tomada antes de escribir la primera línea, decide más sobre el destino del proyecto que cualquier elección posterior de biblioteca o de protocolo.

⚔️ Mide el hueco antes de intentar taparlo
  1. Averigua desde dos redes distintas qué dirección pública se te asigna y comprueba si el puerto cambia al hablar con destinos diferentes.
  2. Enumera las interfaces de red que tu aplicación usa hoy y clasifícalas por quién inicia la conversación en cada una.
  3. Escribe en una frase cuál de las dos dimensiones de la traducción impide la conexión directa y por qué la otra no.
  4. Estima cuántos minutos de silencio soporta tu red antes de que un mapeo sin conexión desaparezca, y decide tu intervalo de mantenimiento.
  5. Describe qué haría tu aplicación si la conexión directa no llegase a establecerse nunca para un usuario concreto.
  6. Comprueba qué le ocurre a una conexión establecida cuando el dispositivo pasa de red inalámbrica a red móvil.