Skip to main content
Blog
Blog Learning

Cómo arreglamos la gestión de timeouts de TLS en el Edge de cside

Cómo cside mejoró la fiabilidad de TLS en el Edge escrito en Rust sacando el trabajo del handshake del camino de accept de Axum y llevándolo a tareas de Tokio acotadas.

Jun 16, 2026 6 min read
Visualización abstracta en azul oscuro de conexiones de red que atraviesan una puerta de enlace circular

En cside ponemos un fuerte énfasis en la seguridad de memoria y el rendimiento. Todos nuestros servicios principales están escritos en Rust, incluido el servicio Edge.

El Edge se sitúa en una frontera sensible. Tiene que ser rápido, seguro y resiliente, porque forma parte del camino en el que recogemos señales de alta calidad para productos como la detección de VPN y la detección de bots.

Para muchas aplicaciones web, la configuración de TLS más sencilla es dejar que un balanceador de carga en la nube termine TLS y reenvíe HTTP plano a la aplicación. Ese es un buen valor por defecto cuando la aplicación solo necesita la petición HTTP final.

El Edge tiene un trabajo distinto.

Algunas señales de detección existen antes de que la petición se convierta en HTTP corriente, en la capa 4 (la capa de transporte) en lugar de en la capa 7. Si TLS se termina por completo antes de que el tráfico llegue al Edge, esas señales dejan de estar disponibles en la misma forma. Así que, para esta parte de la plataforma, el Edge tiene que operar en la capa 4 y gestionar el camino de TLS directamente.

Eso le da a cside mejor calidad de señal para la detección, pero también significa que el Edge necesita gestionar él mismo el comportamiento real de TLS.

Este post trata sobre un pequeño arreglo de fiabilidad en ese camino: sacar el trabajo del handshake de TLS del camino de accept de Axum y llevarlo a tareas de Tokio acotadas.

Qué empezó a fallar

El parche en sí no era grande, pero entender el fallo requirió algo de investigación.

El Edge estaba sano. Los certificados se cargaban. El puerto era accesible. La mayor parte del tráfico se comportaba con normalidad.

Pero bajo más carga, algunas comprobaciones HTTPS y conexiones de clientes parecían quedarse colgadas o agotar el tiempo de espera.

Al principio, eso puede parecer un problema de TLS. En la práctica, el patrón importante era más específico: algunos clientes abrían una conexión pero no completaban el handshake de TLS.

Eso es normal en un servicio expuesto a internet. Los endpoints públicos ven conexiones incompletas todo el tiempo:

el cliente conecta y luego no envía nada
el cliente inicia TLS y luego desaparece
el cliente inicia TLS y luego se atasca

Esos fallos no eran la parte sorprendente.

La parte sorprendente era cuánto impacto podía tener un único handshake incompleto sobre el tráfico sano cercano.

Antes del arreglo

Antes del arreglo, una parte del camino de TLS del Edge hacía demasiado trabajo en un solo paso.

Conceptualmente, se comportaba así:

aceptar una conexión
terminar el trabajo de TLS para esa conexión
y luego aceptar la siguiente conexión

En Rust simplificado, la forma era más o menos esta:

impl axum::serve::Listener for EdgeTlsListener {
  async fn accept(&mut self) -> (TlsStream, SocketAddr) {
    let (mut tcp_stream, addr) = self.tcp_listener.accept().await;

    let hello = read_tls_hello(&mut tcp_stream).await;
    let tls_stream = complete_tls_handshake(hello, tcp_stream).await;

    (tls_stream, addr)
  }
}

Ese código es fácil de razonar, pero mete todo el handshake de TLS dentro de accept().

Para Axum, accept() es la puerta de entrada. Si está ocupado esperando una conexión, el servidor no está recibiendo la siguiente conexión completada de ese listener.

Eso parece simple, pero crea head-of-line blocking.

Si una conexión iniciaba TLS y luego se atascaba, el Edge esperaba el timeout de esa conexión antes de seguir adelante. Durante esa espera, las conexiones sanas podían quedarse retenidas detrás de ella.

El problema no era:

TLS no puede funcionar

Era:

un handshake de TLS incompleto puede retrasar handshakes sanos posteriores

Esa distinción importaba.

Aumentar el timeout no habría arreglado el problema. Habría hecho que el camino lento mantuviera la línea bloqueada durante más tiempo.

El arreglo

El arreglo consistió en separar la aceptación de una conexión de la finalización de su handshake de TLS.

Ahora el Edge acepta nuevas conexiones rápidamente y gestiona cada handshake de TLS de forma independiente. Un handshake atascado o incompleto todavía puede agotar su tiempo de espera, pero no impide que las conexiones sanas posteriores avancen.

Conceptualmente, el nuevo flujo se ve así:

aceptar conexiones rápidamente
gestionar cada handshake de TLS de forma independiente
devolver al manejo de peticiones solo las conexiones seguras completadas

La nueva forma usa tareas de Tokio más un canal de conexiones seguras completadas:

let (ready_tx, ready_rx) = tokio::sync::mpsc::channel(limit);

tokio::spawn(async move {
  loop {
    let (tcp_stream, addr) = tcp_listener.accept().await;
    let ready_tx = ready_tx.clone();

    tokio::spawn(async move {
      if let Some(tls_stream) = finish_tls(tcp_stream).await {
        let _ = ready_tx.send((tls_stream, addr)).await;
      }
    });
  }
});

Entonces el listener orientado a Axum se vuelve mucho más pequeño:

impl axum::serve::Listener for EdgeTlsListener {
  async fn accept(&mut self) -> (TlsStream, SocketAddr) {
    self.ready_rx
      .recv()
      .await
      .expect("TLS accept loop terminated")
  }
}

La parte importante es la frontera: accept() ya no realiza por sí mismo el lento trabajo del handshake. Recibe handshakes que ya se han completado.

Así que el modo de fallo cambió de esto:

un handshake atascado
-> retrasa la siguiente conexión

a esto:

un handshake atascado
-> agota su tiempo de espera de forma independiente
-> las conexiones sanas continúan

Esa es la mejora de fiabilidad importante.

El arreglo no eliminó los timeouts. Los timeouts siguen siendo necesarios. Un handshake incompleto no debería vivir para siempre.

El arreglo cambió dónde se paga el timeout. Ahora una conexión defectuosa paga su propio timeout en lugar de hacer que otras conexiones lo paguen por ella.

Manteniéndolo acotado

Hay una segunda parte importante del arreglo.

Si cada nueva conexión puede crear trabajo ilimitado, entonces el servicio se vuelve receptivo pero no seguro bajo presión. Por eso el Edge también acota el número de handshakes de TLS que pueden estar en curso al mismo tiempo.

La versión simplificada se ve así:

let permits = Arc::new(tokio::sync::Semaphore::new(limit));

loop {
  let (tcp_stream, addr) = tcp_listener.accept().await;
  let permit = permits.clone().acquire_owned().await.expect("TLS semaphore closed");
  let ready_tx = ready_tx.clone();

  tokio::spawn(async move {
    let _permit = permit;

    if let Some(tls_stream) = finish_tls(tcp_stream).await {
      let _ = ready_tx.send((tls_stream, addr)).await;
    }
  });
}

El permiso es propiedad de la tarea. Cuando la tarea termina, Rust libera el permiso y devuelve capacidad al semáforo. Eso mantiene el límite de concurrencia ligado al tiempo de vida del trabajo real del handshake.

Eso nos da las dos propiedades que queríamos:

los handshakes lentos no bloquean los handshakes sanos

y:

los handshakes lentos no pueden crear trabajo ilimitado

Este es el tipo de compromiso que nos importa en el Edge: mejorar la fiabilidad sin renunciar a un uso de recursos predecible.

Haciendo visible el fallo

También reforzamos un camino de fallo interno.

Si la parte del Edge responsable de aceptar conexiones seguras llegara a detenerse de forma inesperada, el servicio no debería esperar en silencio para siempre. Los bloqueos silenciosos son difíciles de operar y difíciles de razonar.

El canal hace este estado explícito. Si todos los emisores han desaparecido, recibir del canal devuelve None. Eso no debería tratarse como tiempo de inactividad normal, así que el cuerpo final de accept() reemplaza el anterior .expect() por un fallo explícito y registrado:

self.ready_rx.recv().await.unwrap_or_else(|| {
  tracing::error!("TLS accept loop terminated");
  panic!("TLS accept loop terminated")
})

Ahora el camino de fallo se vuelve visible de inmediato en lugar de convertirse en una espera oculta.

Eso no cambia el tráfico normal de los clientes, pero hace que el sistema sea más fácil de confiar durante los incidentes.

Qué aprendimos

La lección principal es que los timeouts no bastan si el timeout se paga en el lugar equivocado.

Un timeout alrededor del trabajo de TLS suena razonable. Pero si una conexión lenta puede hacer que conexiones no relacionadas esperen detrás de ella, el timeout se convierte en un dolor compartido.

El mejor modelo es:

aceptar rápidamente
aislar el trabajo lento
acotar la concurrencia
hacer visible el fallo inesperado

Otra lección es que los servicios expuestos a internet deberían tratar las conexiones incompletas como algo normal. Los clientes se desconectan. Las comprobaciones de salud reintentan. Las redes fluctúan. Algunos handshakes nunca terminan.

El Edge no debería dar por hecho que internet es ordenado.

Antes del arreglo:

un handshake incompleto
-> el tráfico sano cercano puede esperar

Después del arreglo:

un handshake incompleto
-> timeout aislado
-> el tráfico sano continúa

El parche final no hizo que las conexiones defectuosas desaparecieran.

Hizo que el Edge las gestionara en el lugar correcto, con los límites correctos, manteniendo a la vez las garantías de rendimiento y seguridad de memoria que esperamos de nuestros servicios en Rust.

Taym Haddadi
Software Engineer

Software engineer at cside, working on the Rust services behind the cside Edge.

FAQ

Frequently Asked Questions

Algunos productos de detección de cside dependen de señales a nivel de conexión que solo están disponibles antes de que una petición haya sido aplanada en HTTP corriente por una terminación de TLS gestionada. El Edge maneja ese camino directamente para que esas señales puedan usarse de forma segura y consistente.

No. La gestión de TLS sigue dentro del servicio Edge. El cambio mejoró cómo se aíslan los handshakes incompletos para que no afecten al tráfico sano.

Monitoriza y Asegura tus Scripts de Terceros

Gain full visibility and control over every script delivered to your users to enhance site security and performance.

Comienza gratis, o prueba Business con una prueba de 14 días.

Interfaz del panel de cside mostrando monitorización de scripts y análisis de seguridad
Related Articles
Reservar una demo