Deep Engineering

ЗАМЕР

bench/tls/practice.py

Скрипт, которым получены числа в статье, и запись прогона. Файл читается на сборке из репозитория — это тот самый код, который запускали, а не его копия.

Цитируется в статье
/ru/interview/sre/tls-handshake
Как запустить
python3 bench/tls/handshake.py > bench/tls/runs/handshake.txt
python3 bench/tls/practice.py  > bench/tls/runs/practice.txt

Запись прогона

Замеры для урока «TLS-рукопожатие»

Файл Что делает
handshake.py четыре наблюдения: сколько кругов стоит полное рукопожатие в TLS 1.3 и TLS 1.2; что меняет возобновление сессии в каждой версии; сколько байт идёт в обе стороны при полном и возобновлённом рукопожатии; и что рукопожатие добавляет к обычному TCP
practice.py ответы к задачам урока: кратность рукопожатия в кругах для двух версий и для возобновления, а также во сколько раз возобновление сокращает поток от сервера

Запуск из корня репозитория:

python3 bench/tls/handshake.py > bench/tls/runs/handshake.txt
python3 bench/tls/practice.py  > bench/tls/runs/practice.txt

Как устроен замер

Ни один настоящий сервер здесь не участвует. Сервер стоит на loopback, а между ним и клиентом — ретранслятор, который задерживает каждую переданную порцию на 20 мс в одну сторону. Круг поэтому известен точно — 40 мс, — и время рукопожатия читается в кругах: 44 мс это один круг, 85 мс — два.

Тот же ретранслятор считает байты в обе стороны. Это второй предмет замера: чем именно дешевле возобновлённое рукопожатие там, где круг оно не экономит.

Сертификат самоподписанный, RSA-2048, ровно один в цепочке, и клиент его не проверяет. Так сделано намеренно: предмет замера — круги и байты, а не стоимость проверки подписи, которая зависит от типа ключа и длины цепочки.

Что воспроизводимо

Число кругов: полное рукопожатие TLS 1.3 — один круг, TLS 1.2 — два, возобновлённое в обеих версиях — один. Направление разницы в байтах: при возобновлении сервер не посылает сертификат, и его поток сокращается в разы.

Не воспроизводятся абсолютные байты: их меняет длина ключа, набор шифров и версия OpenSSL. Не воспроизводится и колонка «exchange» в четвёртом блоке — она больше круга потому, что в TLS 1.3 сервер присылает билеты на возобновление уже после рукопожатия, и звено задерживает этот полёт тоже.

Требования к среде

Нужен openssl в PATH: сертификат создаётся при каждом запуске во временном каталоге. Прав root не требуется, сеть не нужна вовсе — весь обмен идёт по loopback.

Числа сняты на CPython 3.11.15 и OpenSSL 3.0.13.

Скрипт

85 строк
"""Практика к уроку про TLS: три ответа и одна кратность.

ЗАЧЕМ ОТДЕЛЬНЫЙ ФАЙЛ. Задачи урока проверяются сборкой
(scripts/validate-practice.mjs): показанный читателю код обязан построчно быть
в скрипте, верный вариант — дословно встречаться в записи прогона, а
заявленное число — печататься самой программой.

ЧТО ЗДЕСЬ ПРОВЕРЯЕТСЯ. Два места, где ходячая формулировка расходится с
замером. Первое: сколько кругов стоит полное рукопожатие в двух версиях
протокола и меняет ли возобновление это число в TLS 1.3. Второе: во сколько
раз возобновление сокращает поток от сервера — то есть чем оно на самом деле
дешевле, если круг не экономит.

Сервер, звено с известной задержкой и клиент берутся из `handshake.py`, чтобы
задача и разбор урока стояли на одном и том же коде.

ЗАПУСК: python3 bench/tls/practice.py
Вывод: runs/practice.txt
"""

import os
import ssl
import sys
import tempfile
import time

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

from handshake import (  # noqa: E402
    RTT_MS,
    DelayedLink,
    Server,
    dial,
    make_cert,
)


def pair(key: str, crt: str, version: ssl.TLSVersion) -> tuple[dict, dict, DelayedLink]:
    """Два соединения к одному серверу: полное и возобновлённое."""
    server = Server(key, crt, version)
    link = DelayedLink(server.port)
    time.sleep(0.2)

    link.reset()
    full = dial(link.port)
    full["to_client"] = link.to_client

    link.reset()
    resumed = dial(link.port, full["session"])
    resumed["to_client"] = link.to_client
    return full, resumed, link


def main() -> None:
    print(f"Python {sys.version.split()[0]} · {ssl.OPENSSL_VERSION}")
    print()

    key, crt = make_cert(tempfile.mkdtemp())
    full13, resumed13, _ = pair(key, crt, ssl.TLSVersion.TLSv1_3)
    full12, resumed12, _ = pair(key, crt, ssl.TLSVersion.TLSv1_2)

    # --- Часть 1: три ответа о поведении --------------------------------
    print(f"{full13['handshake_ms'] / RTT_MS:.1f}")
    print(f"{full12['handshake_ms'] / RTT_MS:.1f}")
    print(f"{resumed13['handshake_ms'] / RTT_MS:.1f}")

    # --- Часть 2: чем дешевле возобновление ------------------------------
    #
    # ПОЧЕМУ БАЙТЫ ОТ СЕРВЕРА, А НЕ ОТ КЛИЕНТА. Сертификат посылает сервер, и
    # именно его поток меняется при возобновлении. Поток от клиента почти не
    # меняется: он и в полном рукопожатии не отправляет ничего крупного.
    #
    # ПОЧЕМУ КРАТНОСТЬ ПЕЧАТАЕТ ПРОГРАММА. Деление в уме на стороне редакции —
    # ровно тот шаг, где вкрадывается ошибка, которую потом нечем поймать.
    ratio = full12["to_client"] / resumed12["to_client"]
    print()
    print(f"round trip on this link, ms          {RTT_MS}")
    print(f"TLS 1.2, full: bytes from server     {full12['to_client']}")
    print(f"TLS 1.2, resumed: bytes from server  {resumed12['to_client']}")
    print(f"times fewer bytes from the server    {ratio:.1f}")


if __name__ == "__main__":
    main()