Resumo

  • Em TLS 1.3, close_notify é uma declaração autenticada sobre uma direção: o emissor não enviará mais mensagens TLS naquela conexão. A outra direção continua independente, e o alerta não confirma requisição, resposta, stream ou efeito durável.
  • EOF de transporte sem o alerta mantém a hipótese de truncamento. Só pode ser aceito por compatibilidade quando o protocolo superior detecta completude de forma independente e a aplicação demonstra que executou essa verificação.
  • Autoridade para repetir ou atribuir uma operação exige quatro limites separados: fim da mensagem de aplicação, encerramento TLS local e remoto, terminação do transporte e resultado persistente. Um único campo “closed” não preserva essa prova.

A última linha do log não era o fim

O serviço montou a resposta de sucesso antes do commit definitivo. Os registros TLS saíram, o processo enviou close_notify e o wrapper anotou shutdown=success. Milissegundos depois, uma restrição de unicidade rejeitou a atualização.

Para o cliente, a sequência criptográfica era válida. Os dados escolhidos pelo servidor vieram antes do alerta autenticado. Ainda assim, nenhum desses bytes era um identificador de commit emitido pelo sistema de registro.

O erro operacional foi entregar autoridade de negócio ao último evento de rede. TLS ordena e protege mensagens; ele não observa se um ledger, fila ou banco confirmou a consequência anunciada.

Cada direção termina por conta própria

Quem envia close_notify declara que não mandará mais mensagens TLS, e dados posteriores ao alerta devem ser ignorados. Isso fecha a escrita daquele lado. A leitura pode continuar enquanto o par termina sua própria saída.

Cada extremo deve enviar o alerta antes de fechar seu lado de escrita, salvo se já emitiu um alerta de erro. TLS 1.3 removeu a antiga obrigação de resposta imediata que poderia descartar escritas pendentes. Encerramento ordenado não é um único instante para a conexão inteira.

TCP também permite meia conexão fechada, mas um FIN é evidência de sequência de bytes, não um alerta TLS autenticado. Ele não indica o último registro protegido completo nem o significado da mensagem de aplicação.

EOF não ganha autenticidade por parecer normal

Se o transporte some antes de close_notify, o receptor não sabe se chegou tudo que o emissor pretendia enviar. Processo interrompido, proxy expirado e implementação incompatível produzem o mesmo formato externo. A ausência não prova ataque; preserva a incerteza de truncamento.

Alerta fatal, RST, timeout, EOF inesperado, alerta remoto recebido e tentativa local de encerramento devem permanecer estados diferentes. O rótulo genérico “desconectado” apaga a direção e a ordem necessárias para decidir o que usar.

O registro da IANA associa o valor 0 a close_notify. Ele prova como interpretar o código, não que um caminho de produção o emitiu ou que a mensagem anterior estava completa.

A aplicação precisa de seu próprio ponto final

TLS considera opaco o conteúdo dos registros. Receber o alerta depois deles demonstra ordem, não que formavam documento inteiro, resposta final, recibo ou commit.

O protocolo superior deve definir seu término: comprimento declarado, delimitador, chunk final, FIN de stream, código final, confirmação, identificador de solicitação ou registro durável. Mensagem completa e operação bem-sucedida são fatos distintos; um pode existir sem o outro.

Buffers criam estados adicionais. Uma chamada de escrita aceita pode ter colocado bytes apenas no TLS ou no BIO. Em E/S não bloqueante, a tentativa de alerta pode ser registrada antes da entrega ao transporte. Nenhum desses marcos prova durabilidade fora da conexão.

SSL_shutdown() descreve uma transição

O retorno 0 normalmente significa que o close_notify local foi enviado e o alerta do par ainda falta. Não é erro, mas tampouco é encerramento bilateral. O retorno 1 indica que os dois alertas foram enviados e recebidos.

A primeira chamada encerra a escrita TLS, mantém a leitura disponível e deixa TCP aberto. OpenSSL recomenda continuar lendo para processar dados finais e mensagens posteriores ao handshake. Chamar novamente sem drenar dados de aplicação pendentes pode falhar.

O flag de envio acompanha a tentativa local; o de recebimento acompanha o alerta remoto. Quiet shutdown altera estado sem enviar alerta e não é compatível com o protocolo. Desde OpenSSL 3.0, EOF inesperado tem erro próprio. SSL_OP_IGNORE_UNEXPECTED_EOF só cabe quando o protocolo de aplicação detecta truncamento sem ambiguidade e realmente executa a checagem.

GnuTLS separa GNUTLS_SHUT_WR de GNUTLS_SHUT_RDWR, e BoringSSL mantém estados de leitura e escrita independentes. A API permite conservar a diferença; a observabilidade precisa fazer o mesmo.

HTTP mostra onde a mensagem acaba

HTTP/1.1 possui regras explícitas. Content-Length exige todos os octetos declarados. Transferência chunked exige o chunk zero terminal. Fechar a conexão não completa um corpo que falhou em qualquer regra.

Uma resposta delimitada apenas pelo fechamento é mais frágil. Seu fim depende de um término válido da conexão; fechamento TLS incompleto deixa a resposta incompleta. RFC 9112 prefere comprimentos ou codificações explícitas porque falha de rede pode imitar um fim regular.

Se comprimento ou chunk terminal já foi verificado, aquela fronteira continua válida mesmo que o alerta remoto falte depois. Esse é o teste preciso para tolerar EOF: toda mensagem usada deve ter limite independente comprovado.

Multiplexação requer fronteira por solicitação

HTTP/2 leva muitos streams em uma conexão TLS. close_notify não informa quais solicitações o servidor começou a processar. GOAWAY acrescenta um último stream e limita o conjunto possivelmente tratado.

Sem GOAWAY, um POST não idempotente em trânsito continua ambíguo. HTTP/3 aplica a mesma ideia sobre QUIC e usa GOAWAY para delimitar solicitações aceitas. Encerrar a conexão não cria confirmação por requisição.

Repetir tudo pode cobrar duas vezes; nunca repetir pode perder trabalho. Método, chave de idempotência, consulta de resultado e reconciliação fornecem a autoridade que o adjetivo “graceful” não oferece.

QUIC termina de outra maneira

QUIC usa TLS no handshake, mas não protege dados de aplicação com registros TLS. Alertas TLS tornam-se erros de conexão QUIC, que tem CONNECTION_CLOSE, FIN de stream, reset, closing e draining. A semântica warning de close_notify não se transfere.

HTTP/3 mantém GOAWAY porque o fechamento QUIC também não informa sozinho quais requisições foram aceitas. Métricas devem nomear o mecanismo: alerta TLS, TCP FIN ou RST, QUIC stream FIN, CONNECTION_CLOSE, idle timeout, HTTP GOAWAY ou confirmação de aplicação.

Um inventário de evidências de término

Registrar papel do endpoint, direção, versão TLS, correlação e última mensagem ou stream completo. Guardar regra de framing, bytes esperados e recebidos, ID da solicitação, chave de idempotência e classe de repetição.

Em TLS, separar alerta local enfileirado e despachado, alerta remoto recebido, sequência de retornos, flags de leitura e escrita, dados pendentes e erro exato. Incluir quiet mode, política de EOF, versão da biblioteca, kTLS e limite dos proxies.

No transporte, diferenciar FIN, RST, EOF, timeout e meio fechamento. Na aplicação, preservar confirmação, ID de commit, horário durável e autoridade emissora. GOAWAY e fim de cada stream devem permanecer consultáveis nos protocolos multiplexados.

Testes negativos precisam cortar TCP antes do alerta, omitir chunk terminal, fechar depois de quadro completo e antes do commit, capturar retorno 0, deixar dados remotos pendentes, usar quiet shutdown, alternar EOF e terminar HTTP/2 ou HTTP/3 com operação não idempotente em trânsito.

Fontes