Кратко

  • В TLS 1.3 close_notify — аутентифицированное заявление для одного направления: отправитель больше не передаст сообщений TLS по этому соединению. Обратное направление остаётся независимым, а запрос, ответ, поток или долговечный эффект не получают подтверждения.
  • EOF транспорта без такого alert сохраняет риск усечения. Считать его приемлемым можно только тогда, когда верхний протокол независимо определяет полноту сообщения, а приложение действительно выполнило эту проверку.
  • Право повторить операцию требует четырёх раздельных границ: завершённого сообщения приложения, локального и удалённого завершения TLS, конца транспорта и долговечного результата. Один флаг closed уничтожает нужный порядок.

Штатное закрытие нештатной операции

Сервер сформировал положительный ответ до окончательного commit. Он записал TLS records, отправил close_notify, а обёртка сохранила shutdown=success. Затем ограничение уникальности отклонило изменение в базе.

Сетевая последовательность была настоящей: выбранные сервером данные предшествовали аутентифицированному alert. Не было другого факта — идентификатора commit, выпущенного системой, которая отвечает за состояние операции.

Ошибка наблюдаемости состояла в том, что последнее событие соединения стало решением о бизнес-процессе. TLS защищает порядок байтов, но не видит транзакцию базы, состояние очереди или решение следующего сервиса.

У соединения два направления завершения

Отправитель close_notify обещает больше не посылать TLS-сообщений; данные после alert должны игнорироваться. Его запись завершена, но чтение может продолжаться, пока другая сторона отправляет последние данные и закрывает своё направление.

Каждая сторона должна передать alert до закрытия своей записи, если ранее не отправила alert ошибки. TLS 1.3 отказался от немедленного ответа, способного отбросить ожидающие записи. Поэтому корректное завершение не является одним атомарным состоянием всего соединения.

TCP FIN тоже относится к одному направлению, но это доказательство конца байтовой последовательности, а не аутентифицированный TLS alert. Оно не указывает последний полный TLS record и не знает границы приложения.

EOF оставляет вопрос открытым

Если транспорт исчез до close_notify, получатель не знает, дошло ли всё задуманное отправителем. Сбой process, timeout proxy, несовместимый peer и намеренное усечение могут выглядеть одинаково. Отсутствие alert не доказывает атаку, но лишает более сильной криптографической границы.

Фатальный alert, RST, timeout, неожиданный EOF, полученный alert завершения и локальная попытка отправки — разные состояния. Общая метка «disconnected» стирает направление и порядок, необходимые для решения.

IANA присвоила close_notify значение alert 0. Реестр доказывает codepoint и нормативную ссылку, но не путь конкретного соединения и не полноту предшествующего сообщения.

Конец сообщения определяет приложение

TLS считает защищённое содержимое непрозрачным. Records перед alert имеют доказанный порядок, но протокол не различает полный документ, конечный chunk, квитанцию и половину структуры.

Верхний протокол обязан задать собственную границу: объявленную длину, delimiter, последний chunk, FIN потока, финальный статус, подтверждение, ID запроса или долговечную запись. Полное сообщение и успешная операция — отдельные факты; один возможен без другого.

Буферы добавляют промежуточные состояния. Успешный вызов write может лишь поместить байты в TLS или BIO. В неблокирующем процессе попытка alert может попасть в журнал до выхода в транспорт. Ни один такой момент не доказывает внешнюю долговечность.

OpenSSL возвращает состояние, а не вердикт

Значение 0 от SSL_shutdown() обычно означает: локальный close_notify отправлен, alert peer ещё ожидается. Это не ошибка, но и не двустороннее завершение. Значение 1 означает, что оба alert были отправлены и получены.

Первый вызов закрывает запись TLS, оставляет чтение и TCP открытыми. OpenSSL советует продолжать чтение, чтобы обработать последние данные и post-handshake сообщения. Второй shutdown без чтения ожидающих данных может завершиться ошибкой.

Флаг sent следует за попыткой отправки, received — за alert другой стороны. Quiet shutdown меняет локальное состояние без alert и не соответствует протоколу. Начиная с OpenSSL 3.0 неожиданный EOF имеет самостоятельный смысл ошибки. SSL_OP_IGNORE_UNEXPECTED_EOF допустим лишь при однозначном обнаружении усечения верхним протоколом и фактической проверке.

GnuTLS различает GNUTLS_SHUT_WR и GNUTLS_SHUT_RDWR, а BoringSSL отдельно хранит состояния чтения и записи. Библиотеки сохраняют разницу; телеметрия не должна её сворачивать.

HTTP располагает собственной границей полноты

В HTTP/1.1 Content-Length требует заявленное число октетов, а chunked encoding — нулевой завершающий chunk. Закрытие соединения не делает неполное тело завершённым.

Ответ, ограниченный только закрытием, слабее: его конец зависит от корректного конца соединения. Неполное завершение TLS оставляет неполным и HTTP-ответ. RFC 9112 рекомендует явную длину или кодирование, поскольку сетевой сбой может выглядеть как нормальный конец.

Если длина или последний chunk уже проверены, эта граница остаётся доказательством даже при последующем отсутствии peer alert. Именно такой независимый тест нужен для совместимости с EOF, а не просто включённая опция.

Мультиплексированию нужна граница запроса

HTTP/2 переносит множество streams по одному TLS-соединению. close_notify не сообщает, какие запросы сервер начал обрабатывать. GOAWAY задаёт последний stream и ограничивает множество потенциально обработанных запросов.

Без GOAWAY неидемпотентный POST в пути остаётся неопределённым. HTTP/3 использует ту же прикладную идею над QUIC и ограничивает GOAWAY принятые запросы. Завершение соединения не создаёт подтверждение для каждого запроса.

Безусловный повтор может удвоить платёж; отказ от повтора может потерять работу. Полномочия дают семантика метода, ключ идемпотентности, запрос результата и сверка.

QUIC завершает иначе

QUIC применяет TLS для handshake, но не защищает прикладные данные TLS records. Alerts TLS становятся ошибками соединения QUIC, а сам QUIC определяет CONNECTION_CLOSE, stream FIN, reset, closing и draining. Warning-семантика close_notify сюда не переносится.

HTTP/3 всё равно нуждается в GOAWAY, потому что конец QUIC не указывает принятые запросы. Метрика должна называть механизм: TLS alert, TCP FIN/RST, QUIC stream FIN, CONNECTION_CLOSE, idle timeout, HTTP GOAWAY или подтверждение приложения.

Сохранять доказательства каждого конца

Записывать роль endpoint, направление, версию TLS, correlation ID и последнее полное сообщение или stream. Хранить правило framing, ожидаемые и полученные байты, ID запроса, ключ идемпотентности и класс повтора.

Для TLS разнести локальный alert в очереди и на проводе, полученный peer alert, последовательность возвратов библиотеки, флаги чтения и записи, ожидающие данные и точную ошибку. Добавить quiet mode, политику EOF, версию библиотеки, kTLS и границу proxy.

В транспорте различать FIN, RST, EOF, timeout и half-close. При мультиплексировании хранить GOAWAY и конец каждого stream. В бизнес-слое сохранять подтверждение, commit ID, долговечное время и выдавший их источник полномочий.

Негативные тесты должны обрывать TCP до alert, убирать конечный chunk, закрывать после полного frame, но до commit, фиксировать возврат 0, оставлять данные peer непрочитанными, включать quiet shutdown, менять EOF-политику и завершать HTTP/2 или HTTP/3 с неидемпотентным запросом в пути.

Источники