Skip to main content
Blog
Blog Learning

Corrigir o tratamento de timeouts de TLS na Edge da cside

Como a cside melhorou a fiabilidade do TLS na Edge em Rust ao retirar o trabalho de handshake do caminho de accept do Axum e colocá-lo em tasks Tokio limitadas.

Jun 16, 2026 6 min read
Visualização abstrata em azul-escuro de ligações de rede a atravessar um gateway circular

Na cside, damos uma forte ênfase à segurança de memória e ao desempenho. Todos os nossos serviços centrais são escritos em Rust, incluindo o serviço Edge.

A Edge situa-se numa fronteira sensível. Tem de ser rápida, segura e resiliente, porque faz parte do caminho onde recolhemos sinais de alta qualidade para produtos como a deteção de VPN e a deteção de bots.

Para muitas aplicações web, a configuração de TLS mais simples é deixar um balanceador de carga na cloud terminar o TLS e encaminhar HTTP simples para a aplicação. Esse é um bom valor por omissão quando a aplicação só precisa do pedido HTTP final.

A Edge tem uma função diferente.

Alguns sinais de deteção existem antes de o pedido se tornar HTTP comum, na camada 4 (a camada de transporte) e não na camada 7. Se o TLS for totalmente terminado antes de o tráfego chegar à Edge, esses sinais deixam de estar disponíveis na mesma forma. Por isso, para esta parte da plataforma, a Edge tem de operar na camada 4 e gerir o caminho do TLS diretamente.

Isso dá à cside melhor qualidade de sinal para a deteção, mas também significa que a Edge precisa de lidar ela própria com o comportamento real do TLS.

Este artigo é sobre uma pequena correção de fiabilidade nesse caminho: retirar o trabalho de handshake de TLS do caminho de accept do Axum e colocá-lo em tasks Tokio limitadas.

O que começou a falhar

O patch em si não era grande, mas compreender a falha deu algum trabalho.

A Edge estava saudável. Os certificados estavam a carregar. A porta estava acessível. A maior parte do tráfego comportava-se normalmente.

Mas sob mais carga, algumas verificações HTTPS e ligações de clientes pareciam ficar penduradas ou expirar.

À primeira vista, isso pode parecer um problema de TLS. Na prática, o padrão importante era mais específico: alguns clientes abriam uma ligação mas não completavam o handshake de TLS.

Isso é normal num serviço exposto à internet. Os endpoints públicos veem ligações incompletas a toda a hora:

o cliente liga-se e depois não envia nada
o cliente inicia o TLS e depois desaparece
o cliente inicia o TLS e depois bloqueia

Essas falhas não eram a parte surpreendente.

A parte surpreendente era o quanto um único handshake incompleto podia afetar o tráfego saudável próximo.

Antes da correção

Antes da correção, uma parte do caminho de TLS da Edge fazia demasiado trabalho num único passo.

Conceptualmente, comportava-se assim:

aceitar uma ligação
terminar o trabalho de TLS dessa ligação
depois aceitar a ligação seguinte

Em Rust simplificado, a forma era aproximadamente 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)
  }
}

Esse código é fácil de raciocinar, mas coloca todo o handshake de TLS dentro de accept().

Para o Axum, accept() é a porta de entrada. Se estiver ocupada à espera de uma ligação, o servidor não está a receber a ligação completada seguinte desse listener.

Isso parece simples, mas cria head-of-line blocking.

Se uma ligação iniciava o TLS e depois bloqueava, a Edge esperava pelo timeout dessa ligação antes de avançar. Durante essa espera, as ligações saudáveis podiam ficar atrasadas atrás dela.

O problema não era:

o TLS não funciona

Era:

um handshake de TLS incompleto pode atrasar handshakes saudáveis posteriores

Essa distinção importava.

Aumentar o timeout não teria resolvido o problema. Teria feito o caminho lento segurar a fila durante mais tempo.

A correção

A correção foi separar a aceitação de uma ligação da conclusão do seu handshake de TLS.

A Edge agora aceita novas ligações rapidamente e trata de cada handshake de TLS de forma independente. Um handshake bloqueado ou incompleto ainda pode expirar, mas não impede que as ligações saudáveis posteriores progridam.

Conceptualmente, o novo fluxo é assim:

aceitar ligações rapidamente
tratar de cada handshake de TLS de forma independente
devolver ao tratamento de pedidos apenas as ligações seguras concluídas

A nova forma usa tasks Tokio mais um channel de ligações seguras concluídas:

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;
      }
    });
  }
});

Depois, o listener voltado para o Axum torna-se muito mais pequeno:

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

A parte importante é a fronteira: accept() já não executa ele próprio o trabalho lento de handshake. Recebe handshakes que já foram concluídos.

Assim, o modo de falha mudou disto:

um handshake bloqueado
-> atrasa a ligação seguinte

para isto:

um handshake bloqueado
-> expira de forma independente
-> as ligações saudáveis continuam

Esta é a melhoria de fiabilidade importante.

A correção não eliminou os timeouts. Os timeouts continuam a ser necessários. Um handshake incompleto não deve viver para sempre.

A correção mudou onde o timeout é pago. Uma ligação má agora paga o seu próprio timeout em vez de fazer com que outras ligações o paguem por ela.

Mantê-lo limitado

Há uma segunda parte importante da correção.

Se cada nova ligação puder criar trabalho ilimitado, então o serviço torna-se responsivo mas não seguro sob pressão. Por isso, a Edge também limita o número de handshakes de TLS que podem estar em curso ao mesmo tempo.

A versão simplificada é assim:

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;
    }
  });
}

O permit pertence à task. Quando a task termina, o Rust faz drop do permit e devolve capacidade ao semáforo. Isso mantém o limite de concorrência ligado ao tempo de vida do trabalho de handshake real.

Isso dá-nos as duas propriedades que queríamos:

os handshakes lentos não bloqueiam os handshakes saudáveis

e:

os handshakes lentos não podem criar trabalho ilimitado

Este é o tipo de compromisso que nos importa na Edge: melhorar a fiabilidade sem abdicar de uma utilização de recursos previsível.

Tornar a falha visível

Também apertámos um caminho de falha interno.

Se a parte da Edge responsável por aceitar ligações seguras alguma vez parar inesperadamente, o serviço não deve esperar silenciosamente para sempre. Os bloqueios silenciosos são difíceis de operar e difíceis de raciocinar.

O channel torna este estado explícito. Se todos os emissores desaparecerem, receber do channel devolve None. Isso não deve ser tratado como tempo inativo normal, por isso o corpo final de accept() substitui o anterior .expect() por uma falha explícita e registada:

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

O caminho de falha agora torna-se visível imediatamente em vez de se transformar numa espera oculta.

Isso não altera o tráfego normal dos clientes, mas torna o sistema mais fácil de confiar durante incidentes.

O que aprendemos

A principal lição é que os timeouts não chegam se o timeout for pago no sítio errado.

Um timeout à volta do trabalho de TLS parece razoável. Mas se uma ligação lenta puder fazer com que ligações não relacionadas esperem atrás dela, o timeout torna-se uma dor partilhada.

O melhor modelo é:

aceitar rapidamente
isolar o trabalho lento
limitar a concorrência
tornar a falha inesperada visível

Outra lição é que os serviços expostos à internet devem tratar as ligações incompletas como normais. Os clientes desligam-se. As verificações de saúde voltam a tentar. As redes oscilam. Alguns handshakes nunca terminam.

A Edge não deve assumir que a internet é arrumada.

Antes da correção:

um handshake incompleto
-> o tráfego saudável próximo pode esperar

Depois da correção:

um handshake incompleto
-> timeout isolado
-> o tráfego saudável continua

O patch final não fez as ligações más desaparecer.

Fez com que a Edge as tratasse no sítio certo, com os limites certos, mantendo as garantias de desempenho e segurança de memória que esperamos dos nossos serviços Rust.

Taym Haddadi
Software Engineer

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

FAQ

Frequently Asked Questions

Alguns produtos de deteção da cside dependem de sinais ao nível da ligação que só estão disponíveis antes de um pedido ter sido achatado em HTTP comum pela terminação de TLS gerida. A Edge trata desse caminho diretamente para que esses sinais possam ser usados de forma segura e consistente.

Não. O tratamento do TLS continua dentro do serviço Edge. A alteração melhorou a forma como os handshakes incompletos são isolados, para que não afetem o tráfego saudável.

Monitore e Proteja Seus Scripts de Terceiros

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

Comece grátis, ou experimente o Business com um teste de 14 dias.

Interface do painel cside mostrando monitoramento de scripts e análises de segurança
Related Articles
Agende uma demonstração