wandres.dev
P2P I · WebRTC y su peso

ICE, sondeo y relevo: descubrir, probar y rendirse

El procedimiento de conectividad enumera todas las direcciones posibles de cada extremo, prueba las parejas por orden de preferencia y termina en un servidor de relevo cuando ninguna funciona.

⏱ 23 min

Sabemos ya que la conexión directa hay que fabricarla y que hace falta un tercero para empezar. Queda la parte mecánica, que es donde vive casi toda la complejidad de WebRTC y también casi todo su coste operativo. Un dispositivo cualquiera no tiene una dirección sino varias: la de cada interfaz de red que posee, la que un servidor externo le dice que aparenta tener, y eventualmente la de un servicio que se ofrece a reenviar sus paquetes. Ninguna de ellas es fiable a priori y ninguna se puede descartar por adelantado, porque la conectividad entre dos máquinas concretas depende de una combinación de routers, cortafuegos y políticas que nadie puede consultar. La única salida es empírica: enumerar todas las direcciones de ambos lados, formar todas las parejas posibles, probarlas por orden de preferencia y quedarse con la primera que responda. Esta lección recorre esa maquinaria escalón por escalón y pone precio a cada uno, porque el último de ellos es el que aparece en la factura.

🎯 Al terminar esta lección sabrás
  • Distinguir los cuatro tipos de dirección candidata y qué información aporta cada uno.
  • Entender el sondeo como una pregunta mínima a un tercero y por qué es casi gratis.
  • Seguir el procedimiento de comprobación por parejas, su orden de preferencia y su renovación periódica.
  • Situar el relevo como último escalón y calcular lo que cuesta en dinero y en latencia.
  • Diseñar asumiendo que el tiempo de establecimiento es una distribución con cola larga.

Las cuatro clases de dirección que un extremo puede ofrecer

El procedimiento arranca con un inventario. Cada extremo recopila todas las direcciones desde las que podría recibir tráfico y las anuncia todas, sin decidir cuál es buena, porque no puede saberlo.

Las primeras son las locales, una por cada interfaz de red activa. Son gratis, se descubren al instante y funcionan perfectamente cuando los dos dispositivos comparten la red, que en un escenario local-first doméstico o de oficina es un caso frecuentísimo y el que mejor rendimiento da. Tienen un problema de privacidad conocido: exponen la topología interna de la red del usuario a cualquier página, y por eso los navegadores modernos las sustituyen por nombres efímeros que solo resuelven dentro de la propia red local. Esa protección es correcta y añade una resolución de nombres más al camino crítico.

Las segundas son las reflejadas por un servidor, que son la dirección pública que un tercero dice ver. Son la pieza que hace posible la perforación descrita en la primera lección y cuestan una pregunta diminuta a un servicio externo.

Las terceras son las reflejadas por el otro par, y son un descubrimiento accidental del propio procedimiento: cuando llega una prueba desde una dirección que no estaba anunciada, se apunta esa dirección como candidata nueva. Ocurre cuando la traducción fabrica una pareja distinta hacia este destino concreto, y es una de las razones por las que el procedimiento sigue funcionando en escenarios que sobre el papel parecían perdidos.

Las cuartas son las de relevo, que no son direcciones del dispositivo sino de un servidor que se ha comprometido a reenviar por cuenta de él. Son las que siempre funcionan y las únicas que cuestan dinero de verdad.

// La configuracion tipica declara los dos servicios externos por separado
const config = {
  iceServers: [
    { urls: "stun:sondeo.ejemplo.com:3478" },
    {
      urls: ["turn:relevo.ejemplo.com:3478?transport=udp",
             "turns:relevo.ejemplo.com:5349?transport=tcp"],
      username: "efimero-1728",       // credencial de vida corta, nunca fija
      credential: "hmac-derivado"
    }
  ],
  iceTransportPolicy: "all"           // "relay" fuerza el ultimo escalon para probar
};
ℹ️
Las credenciales de relevo nunca deben ser fijas ni ir en el código del cliente

Un servicio de relevo abierto es ancho de banda regalado a quien lo encuentre, y encontrarlo es trivial porque las credenciales viajan al navegador. La práctica correcta es emitirlas desde tu servidor con caducidad corta, derivadas de un secreto compartido con el relevo mediante un cálculo que incluye la marca de caducidad, de modo que el propio servidor pueda validarlas sin consultar ninguna base de datos y sin que sirvan de nada al día siguiente. Es un patrón estándar en las implementaciones habituales y evita el error más caro de todo el nivel.

El sondeo: una pregunta minúscula a un tercero

El servicio de sondeo hace una sola cosa y la hace muy bien: recibe una petición y responde con la dirección y el puerto desde los que la vio llegar. Nada más. No guarda estado entre peticiones, no reenvía tráfico, no participa en la conversación posterior y ni siquiera se entera de que hubo una. El intercambio completo son dos paquetes de decenas de bytes.

// Lo que ocurre conceptualmente en un sondeo
// -> peticion vacia desde 192.168.1.34:51872, que el router reescribe
// <- respuesta: te veo en 88.12.7.201:40311
// El servidor no recuerda nada. El cliente ya sabe que anunciar.

Esa levedad tiene una consecuencia económica que conviene tener presente cuando se compara con el escalón siguiente: un servicio de sondeo atiende un volumen enorme de clientes con recursos ridículos, y por eso existen instancias públicas gratuitas y por eso hospedar la propia cuesta prácticamente nada. Es el único componente externo de toda la pila cuyo coste puede ignorarse sin faltar a la verdad.

Su limitación es igual de clara. La dirección que devuelve es la que el router fabricó hacia ese servidor concreto. Si el mapeo del router es independiente del destino, esa dirección sirve para hablar con cualquiera y la perforación tiene todas las papeletas de funcionar. Si el mapeo depende del destino, la respuesta es correcta y a la vez inútil, porque el puerto que el router asignará hacia el otro extremo será distinto y no hay manera de averiguarlo antes de enviar.

Probar todas las parejas y renovar el permiso

Con las candidatas de ambos lados sobre la mesa, el procedimiento forma todas las parejas compatibles, las ordena por una prioridad que favorece a las locales sobre las reflejadas y a las reflejadas sobre las de relevo, y empieza a comprobarlas. Cada comprobación es una pequeña petición autenticada con las credenciales efímeras que viajaron por señalización, y sirve para dos cosas a la vez: confirmar que existe camino y, por el mero hecho de salir, abrir el mapeo del propio lado. La perforación no es un paso aparte, es un efecto secundario de las comprobaciones.

Uno de los dos extremos toma el papel de controlador y es quien decide cuál de las parejas que han respondido se usa finalmente. El resto no se descartan del todo: se conservan como respaldo, de modo que si la elegida deja de responder se puede promover otra sin rehacer el procedimiento entero.

flowchart TD
A[inventario de candidatas de ambos extremos] --> P[formar parejas compatibles]
P --> O[ordenar por prioridad]
O --> L[locales: misma red, coste cero]
O --> R[reflejadas: perforacion, coste casi cero]
O --> T[relevo: siempre funciona, coste real]
L --> V[comprobaciones autenticadas en ambos sentidos]
R --> V
T --> V
V --> N[el controlador nomina una pareja]
N --> K[renovacion periodica de consentimiento]
K --> F[si deja de responder se promueve el respaldo]
style L fill:#a6e3a1,color:#11111b
style R fill:#89b4fa,color:#11111b
style T fill:#f38ba8,color:#11111b

Un detalle que casi siempre se pasa por alto y que tiene consecuencias operativas: una vez establecida, la conexión no se queda quieta. Cada pocos segundos se repiten comprobaciones para confirmar que el otro extremo sigue queriendo recibir tráfico. Ese mecanismo existe para que WebRTC no pueda usarse como amplificador de ataques contra terceros, ya que sin él bastaría con anunciar la dirección de una víctima y dejar que un navegador ajeno la bombardease. El efecto lateral es que la conexión emite tráfico continuo aunque no haya datos, lo cual mantiene vivos los mapeos, impide que el dispositivo entre en reposo profundo y consume batería de forma medible en un móvil.

Cuando la red cambia por debajo, todo lo anterior queda invalidado y hay que rehacerlo con credenciales nuevas, cosa que exige volver a señalizar. Diseñar como si la conexión fuese estable es el error práctico más común: en movilidad, el reinicio del procedimiento es un suceso ordinario y hay que tratarlo como tal.

⚠️
El goteo de candidatas acorta el arranque y complica el código

Anunciar las candidatas conforme se descubren, en lugar de esperar a tenerlas todas, recorta el tiempo de establecimiento de forma muy notable, porque las locales suelen estar listas de inmediato mientras que las de relevo tardan bastante más. El precio es que la señalización deja de ser un intercambio de dos mensajes y pasa a ser un canal que debe permanecer abierto y ordenado durante todo el arranque, con la complicación añadida de que pueden llegar candidatas después de que la conexión ya funcione. Merece la pena, pero conviene saber que se está eligiendo un canal permanente y no un par de peticiones sueltas.

El último escalón y lo que cuesta cada uno

El relevo aparece cuando ninguna pareja directa responde. Su funcionamiento es simple: el cliente reserva una dirección en el servidor, autoriza a un par concreto a enviarle cosas y a partir de ahí todo el tráfico entra y sale por ahí. Siempre funciona, incluso sobre un transporte con conexión y sobre el puerto de la web cifrada, que es lo que permite atravesar redes corporativas que bloquean todo lo demás.

📡

Local: coste cero

Misma red, latencia mínima, ancho de banda de la red interna. Es el mejor caso posible y en local-first es más frecuente de lo que se supone.

📡

Reflejada: coste casi cero

Dos paquetes contra un servicio sin estado y después ni un byte más. Es el escalón que justifica la palabra directa.

📡

Relevo: coste por byte

Un servidor que transporta todo lo que se envía, en los dos sentidos, durante toda la sesión. Se paga en tráfico y se factura por gigabyte.

📡

Relevo: coste en latencia

El camino deja de ser directo y pasa por un tercer punto, con lo que la ida y vuelta se alarga según lo lejos que quede el relevo de ambos.

La proporción de conexiones que terminan en relevo depende de la población de usuarios y de sus redes, y las cifras que se manejan en la industria suelen situarla en una fracción menor pero nada despreciable, del orden de una de cada diez o una de cada cinco en poblaciones generalistas, y bastante peor en entornos corporativos con cortafuegos restrictivos o en algunas redes móviles. Lo importante para el diseño no es acertar el número exacto, sino aceptar dos consecuencias que se siguen de que no sea cero: tienes que operar un relevo o aceptar que ese porcentaje de usuarios no conecta, y el relevo transporta bytes reales, así que su coste crece con el uso exactamente igual que el de un servidor ordinario.

// El coste del ultimo escalon, estimado sin misterio
const sesiones = 10_000;             // sesiones al mes
const fraccionRelevada = 0.15;       // las que no logran camino directo
const minutosPorSesion = 40;
const kbPorSegundo = 8;              // sincronizacion ligera, no video

const mb = sesiones * fraccionRelevada * minutosPorSesion * 60 * kbPorSegundo / 1024;
// El servidor cuenta el trafico dos veces: entra y sale.
const gbFacturados = (mb * 2) / 1024;

Ese cálculo es el que conviene hacer antes de decidir nada, y su resultado sorprende en las dos direcciones. Para cargas ligeras de sincronización, la factura del relevo es modesta. Para tráfico continuo o para ficheros grandes, se dispara. Y en ambos casos hay que ponerlo al lado de una cifra que casi nadie calcula: lo que costaría reenviar todo el tráfico por un servidor propio, sin pila de conectividad, sin relevo y sin la mitad del código.

La conectividad no es una propiedad consultable: solo se puede descubrir probando, y eso convierte el arranque en una distribución con cola

Toda la complejidad de este procedimiento se sigue de un hecho que parece técnico y es en realidad epistemológico: no existe forma de averiguar si dos máquinas pueden hablarse salvo intentándolo. No hay consulta que responda esa pregunta, no hay registro que la publique, no hay cálculo que la derive de los datos disponibles. La respuesta depende de una combinación de routers, cortafuegos, políticas de operador y estados de tablas efímeras que ninguno de los dos extremos puede observar y que además cambia con el tiempo. Por eso el procedimiento no es un algoritmo de decisión sino un experimento sistemático: enumera hipótesis, las ordena por plausibilidad y las contrasta una a una hasta que alguna sobrevive. Aceptar eso tiene consecuencias de diseño que van mucho más allá de WebRTC y que conviene interiorizar, porque reaparecen en cualquier sistema que opere sobre redes que no controla. La primera es que el tiempo de establecimiento no es un número sino una distribución, y una con cola muy larga: el caso favorable se resuelve en decenas de milisegundos sobre la red local, el caso medio en cientos, y el caso desfavorable tarda segundos porque hay que agotar las hipótesis buenas antes de bajar al relevo. Diseñar la interfaz contra la media garantiza que una parte de los usuarios vea algo roto. La segunda es que el fallo no es un error sino un resultado válido del experimento, y por tanto no puede tratarse con la lógica de la excepción: no hay nada que reintentar ni nada que arreglar, simplemente esas dos máquinas no pueden hablarse hoy y el sistema debe seguir siendo útil bajo esa condición. La tercera, y la más importante para local-first, es que un experimento cuyo resultado no controlas no puede estar en el camino crítico de la funcionalidad principal. Si tu aplicación solo sincroniza cuando el experimento sale bien, has puesto tu propuesta de valor a merced del router de un desconocido. La arquitectura sana coloca el camino que siempre funciona debajo, como suelo garantizado, y trata el resultado del experimento como una mejora que llega cuando llega: cuando aparece, se aprovecha su latencia y su ancho de banda; cuando no aparece, nadie se entera. Esa inversión de la relación entre lo directo y lo indirecto es el contenido práctico de la última lección del nivel, y sale directamente de aquí.

⚔️ Ponle números a tu propio procedimiento
  1. Instrumenta una conexión de prueba y registra cuántas candidatas de cada clase se generan en tu red habitual.
  2. Fuerza el uso exclusivo de relevo y mide la diferencia de latencia frente al camino directo.
  3. Mide el tiempo hasta el primer byte útil en la red local, en dos redes domésticas distintas y desde una red móvil.
  4. Calcula el coste mensual de relevo para tu volumen esperado y compáralo con el de reenviarlo todo por servidor.
  5. Diseña la emisión de credenciales efímeras para tu relevo y comprueba que caducan de verdad.
  6. Provoca un cambio de red durante una sesión activa y comprueba cuánto tarda tu aplicación en recuperarse.