ЗАМЕР
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()