O QUIC é um protocolo de transporte que roda sobre UDP e faz, dentro de uma única conexão criptografada, o que o TCP e o TLS faziam como pilha: entrega confiável, controle de fluxo e de congestionamento, e o handshake do TLS 1.3 (RFC 9000). O lado do handshake está contado em TLS 1.2 vs TLS 1.3 vs DTLS vs QUIC. Este artigo é sobre as três propriedades que mais se atribuem ao QUIC, streams que não se bloqueiam, recuperação mais rápida de perdas e conexões que sobrevivem a uma troca de endereço, e sobre onde cada uma delas termina.

Streams: ordenados por dentro, independentes entre si

Uma conexão QUIC carrega muitos streams. Dentro de um stream, a entrega é rigorosa: o endpoint tem de conseguir entregar à aplicação os dados do stream como um fluxo de bytes ordenado, guardando em buffer o que chegar fora de ordem (RFC 9000, seção 2.2). Entre streams não existe essa promessa, e esse é o ponto. Quando um pacote se perde, só os streams que tinham dados naquele pacote esperam a retransmissão, enquanto os outros continuam avançando (RFC 9000, seção 13). Sobre TCP, a mesma perda travaria todos os streams que dividem a conexão, porque o TCP entrega uma única sequência ordenada de bytes; é o head-of-line blocking de transporte que o manteve e o HTTP/3 elimina.

A mesma seção da especificação traz a ressalva: quando dados de vários streams viajam num mesmo pacote QUIC, perder esse pacote bloqueia todos esses streams, e a recomendação às implementações é colocar o menor número possível de streams num pacote sem desperdiçar espaço. E a independência entre streams acaba onde começam as dependências da própria aplicação. Se uma página só consegue usar o recurso B depois que o recurso A chegou, B espera por A, seja qual for o transporte; se uma atualização de compressão de headers viaja num stream e uma requisição em outro depende dela, essa requisição também espera. Streams independentes eliminam uma ordem que o transporte impunha, não a ordem de que a aplicação precisa.

Perdas: sinais mais claros, a mesma física

O QUIC nunca retransmite um pacote. Os números de pacote só crescem, então registram a ordem em que os pacotes foram enviados, e a ordem de entrega dos dados vem dos offsets dos streams; quando um pacote com dados se perde, os frames que ainda precisam chegar são enviados de novo em pacotes novos, com números novos (RFC 9002, seção 4.2). Isso elimina a ambiguidade que o TCP tem quando um ACK pode se referir tanto à transmissão original quanto à retransmissão, o que deixa mais precisos a medição do RTT e a detecção de perdas.

Isso não cria capacidade. O controle de congestionamento no QUIC funciona por caminho, não por stream: o controlador descrito na especificação é parecido com o NewReno do TCP, e todos os streams de uma conexão no mesmo caminho dividem esse controlador (RFC 9002, seção 7). Um caminho congestionado deixa lentos todos os streams que passam por ele, e um link com perdas continua custando o que custa.

Migração: uma conexão identificada por IDs

Uma conexão TCP é identificada pelos dois endereços e portas, então quando um celular passa do para a rede móvel a conexão acabou. O QUIC identifica a conexão por connection IDs que cada lado escolhe para o outro, justamente para que mudanças de endereçamento abaixo dele não entreguem pacotes no lugar errado (RFC 9000, seção 5.1). Isso torna a migração possível, sob regras (RFC 9000, seção 9):

  • Só depois do handshake. Um endpoint não pode começar a migrar antes de o handshake estar confirmado.
  • Só o cliente se move. A especificação limita a migração a endereços novos do cliente, fora o endereço preferido anunciado por um servidor, e é o cliente que inicia toda migração.
  • O caminho novo é conferido. Um endpoint que percebe qualquer mudança no endereço do outro lado, proposital ou causada por um (network address translation) que remapeou o fluxo, tem de validar o caminho novo antes de confiar nele, com um desafio que o outro lado precisa devolver por aquele caminho (RFC 9000, seção 8.2).
  • O servidor pode recusar. Um servidor que envia o transport parameter disable_active_migration avisa o cliente para não mandar pacotes de um endereço local novo para o endereço usado no handshake.

Nada disso ajuda numa rede que nem leva UDP até o servidor: ali a conexão não chega a ser estabelecida, e a própria especificação do diz que, quando problemas de conectividade como UDP bloqueado impedem uma conexão QUIC, os clientes devem tentar uma versão do HTTP sobre TCP (RFC 9114, seção 3.1). E um caminho que muda porque o tráfego agora atravessa outro load balancer, firewall ou NAT só continua funcionando se esse equipamento encaminhar pacotes por connection ID e deixar o UDP passar; o protocolo dá os meios, a implantação decide se os usa.

Como ler o QUIC em operação

O resumo prático: um stream lento não trava mais os vizinhos, a menos que eles tenham dividido o pacote perdido ou dependam dele na aplicação; a perda é detectada com mais precisão, mas o congestionamento continua compartilhado por caminho; e uma conexão consegue acompanhar o cliente até um endereço novo depois que o handshake foi confirmado e o caminho novo respondeu ao desafio. Quando uma captura ou um log contradiz essas expectativas, as perguntas são qual dessas condições falhou, e se a rede no caminho leva UDP e connection IDs do jeito que o QUIC precisa.

Fontes