Анализ pcap — что реально летит в проводе при curl https
Если первый урок capstone был про дизайн, то этот — про наблюдение. Прежде чем строить свою инфраструктуру, полезно один раз увидеть, что происходит, когда вы делаете обычный curl https://example.com. Какой DNS-запрос ушёл? Сколько пакетов потребовалось на TCP-handshake? Что внутри ClientHello? Где границы между TLS-record’ами?
Это упражнение — из тех, что разделяет инженеров до и после: до него сети ощущаются магией, после — набором конкретных, поддающихся анализу пакетов. Никакой магии нет: всё, что происходит, можно увидеть в tcpdump или Wireshark, разобрать по байтам и понять.
В этом уроке мы захватим один HTTPS-запрос с нуля, разберём четыре фазы (DNS -> TCP -> TLS -> HTTP), найдём SNI, проследим, какие пакеты потерялись или ретрансмитились. Команды одинаковые для macOS и Linux.
Подготовка — инструменты и привилегии
tcpdump требует root-прав, потому что захват трафика — это privileged операция (можно подслушать чужой трафик на shared-интерфейсе). На Linux вы запускаете через sudo tcpdump, на macOS — то же самое.
Сначала проверим, что инструменты установлены:
tcpdump --version
# tcpdump version 4.99.x
wireshark --version 2>/dev/null || tshark --version
# Wireshark 4.x.x или TShark
Если wireshark нет — ничего страшного, мы будем работать через CLI. Для визуального просмотра можно потом открыть .pcap в Wireshark.
Найдём правильный интерфейс. На macOS обычно en0 (Wi-Fi) или en1 (Ethernet), на Linux — eth0, ens33, wlan0:
# macOS
ifconfig | grep -E "^[a-z]" | head -5
# en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
# Linux
ip -br addr show
# eth0 UP 192.168.1.42/24 ...
Дальше — запускаем захват и в другом терминале делаем запрос.
Захват — одна команда, один curl
В одном терминале запускаем tcpdump на нужный интерфейс с фильтром port 53 or port 443 (DNS + HTTPS), записывая в файл:
sudo tcpdump -i en0 -w trace.pcap '(port 53 or port 443) and host example.com'
Флаги:
-i en0— интерфейс (замените на свой)-w trace.pcap— запись в файл (без-wпишет в stdout в человекочитаемом виде)'port 53 or port 443'— BPF-фильтр: DNS + HTTPSand host example.com— только трафик к/от example.com
В другом терминале:
curl https://example.com/ -o /dev/null -s
Вернёмся в первый терминал, нажмём Ctrl+C — tcpdump остановится. Должно быть от 10 до 30 пакетов.
Проверим, что файл записался:
ls -la trace.pcap
# -rw-r--r-- 1 root staff 8234 May 18 14:32 trace.pcap
sudo chown $(whoami) trace.pcap # чтобы открывать без sudo
Теперь у нас есть pcap — бинарный файл с метаданными каждого пакета (timestamp, размер, флаги) и payload’ом.
Чтение pcap в человеческом виде
tcpdump -r trace.pcap -nn -tttt | head -30
Флаги:
-r trace.pcap— читать из файла, не из интерфейса-nn— не резолвить ни IP в имена, ни порты в названия сервисов-tttt— timestamps в человекочитаемом формате
Что мы увидим — разберём по фазам.
Каждая фаза состоит из нескольких пакетов; вместе они занимают ~100-300 мс
Phase 1 — DNS query
В начале pcap должны быть два пакета UDP на порт 53:
2026-05-18 14:32:01.123456 IP 192.168.1.42.54321 > 192.168.1.1.53: 12345+ A? example.com. (29)
2026-05-18 14:32:01.156789 IP 192.168.1.1.53 > 192.168.1.42.54321: 12345 1/0/0 A 93.184.216.34 (45)
Разбираем первый пакет:
192.168.1.42.54321— наш хост, порт 54321 (эфемерный, выбран ядром)>— направление (от/к)192.168.1.1.53— наш router (или DNS-сервер из/etc/resolv.conf), порт 5312345+— ID запроса 12345, флаг+означает рекурсивныйA? example.com.— спрашиваем A-record дляexample.com(29)— 29 байт DNS-payload
Второй пакет — ответ:
12345 1/0/0— тот же ID (так клиент сматчит запрос-ответ), 1 answer + 0 authority + 0 additionalA 93.184.216.34— ответ: A-record93.184.216.34
Это базовый кусок, который вы должны узнать в любом DNS-трафике. На реальном Wi-Fi первый DNS-запрос может занять 5-50 мс, последующие — из кэша, мгновенно.
Если в pcap нет DNS-запросов, это нормально — значит, ответ был в кэше. Запустите sudo killall -HUP mDNSResponder на macOS или sudo resolvectl flush-caches на Linux, чтобы очистить DNS-кэш, и повторите. Тогда DNS-пакеты появятся.
Phase 2 — TCP handshake
После DNS curl открывает TCP-соединение на 93.184.216.34:443. Три пакета:
2026-05-18 14:32:01.200001 IP 192.168.1.42.50001 > 93.184.216.34.443: Flags [S], seq 1234567890, win 65535
2026-05-18 14:32:01.245678 IP 93.184.216.34.443 > 192.168.1.42.50001: Flags [S.], seq 987654321, ack 1234567891, win 65535
2026-05-18 14:32:01.245789 IP 192.168.1.42.50001 > 93.184.216.34.443: Flags [.], ack 987654322, win 65535
Это 3-way handshake:
В tcpdump флаги в скобках:
[S]— SYN[S.]— SYN + ACK (точка = ACK)[.]— только ACK[P.]— PSH + ACK (данные)[F.]— FIN + ACK (закрытие)[R]— RST (форсированный разрыв)
Между SYN и SYN-ACK — ~45 мс. Это round-trip time (RTT) до сервера. Для example.com (CDN на Fastly) обычно 20-100 мс. Если в pcap RTT 300+ мс — сервер далёкий или сеть с проблемами.
Phase 3 — TLS handshake
После TCP начинается TLS. В TLS 1.3 handshake занимает 1 RTT (1 round trip). Видим примерно такие пакеты:
2026-05-18 14:32:01.300000 IP 192.168.1.42.50001 > 93.184.216.34.443: Flags [P.], seq 1:518, ack 1, length 517
2026-05-18 14:32:01.350000 IP 93.184.216.34.443 > 192.168.1.42.50001: Flags [P.], seq 1:3500, ack 518, length 3499
2026-05-18 14:32:01.355000 IP 192.168.1.42.50001 > 93.184.216.34.443: Flags [P.], seq 518:582, ack 3500, length 64
Первый пакет (517 байт) — ClientHello. Это часть, которую можно посмотреть в plaintext, потому что шифрование ещё не установлено. Внутри:
- TLS-версия (
0x0303для TLS 1.2 framing совместимости) - random (32 байта)
- список cipher suites (что мы поддерживаем)
- extensions: SNI, ALPN, supported_groups, и так далее
Где увидеть SNI
SNI (Server Name Indication) — это TLS-extension, в которой клиент сообщает серверу, к какому домену он подключается. Это нужно, потому что один IP может хостить много доменов под разными сертификатами (так работает CDN).
Чтобы извлечь SNI из pcap, удобнее всего tshark:
tshark -r trace.pcap -Y 'tls.handshake.type == 1' \
-T fields -e tls.handshake.extensions_server_name
# example.com
Расшифровка флагов:
-Y 'tls.handshake.type == 1'— фильтр на ClientHello (type 1)-T fields -e ...— вывести только указанное поле
SNI приходит в plaintext. Это означает, что любой посредник на пути (provider, корпоративный proxy, MITM-злоумышленник) знает, к какому домену вы идёте, хотя сам трафик зашифрован. Это известная privacy-проблема, и поэтому TLS 1.3 разрабатывает Encrypted ClientHello (ECH) — но он пока в draft и поддерживается только Cloudflare + Firefox.
Если вы анализируете пакет от незнакомого хоста и видите SNI — значит он использует обычный TLS, без ECH. По SNI можно определить домен даже если IP принадлежит CDN (один IP = много доменов). Это используется и в обе стороны: для аналитики, для блокировки, для firewall’ов.
Server response — ServerHello + Certificate
Второй TLS-пакет (3499 байт в нашем примере) большой, потому что содержит certificate chain — 1-3 сертификата по 1-2 КБ каждый. Также там ServerHello (выбран cipher suite, ключи Диффи-Хеллмана) и Finished.
Чтобы посмотреть сертификат:
tshark -r trace.pcap -Y 'tls.handshake.type == 11' \
-T fields -e x509sat.printableString
# Fastly, Inc.
# *.example.com
# CN=Fastly Inc., O=Fastly, ...
tls.handshake.type == 11 — это Certificate handshake. Если в pcap его не видно, значит сервер использует TLS resumption: клиент подключался раньше, есть session ticket, certificate не пересылается. Это нормально.
Phase 4 — HTTP request (зашифрован)
После TLS handshake идут пакеты с данными. В tcpdump они выглядят как обычные TCP-сегменты с PSH-флагом:
2026-05-18 14:32:01.360000 IP 192.168.1.42.50001 > 93.184.216.34.443: Flags [P.], seq 582:660, ack 3500, length 78
2026-05-18 14:32:01.410000 IP 93.184.216.34.443 > 192.168.1.42.50001: Flags [P.], seq 3500:5000, ack 660, length 1500
Первый пакет (78 байт) — наш GET / HTTP/1.1\r\nHost: example.com\r\n\r\n, зашифрованный. Без приватного ключа сервера расшифровать payload нельзя. Но можно видеть:
- Размер (~80 байт) — типичный для маленького GET без cookies.
- Time-to-first-byte (TTFB) — разница между отправкой запроса и первым ответом. 50 мс в нашем примере. На медленном сервере было бы 200-500 мс.
- Размер ответа (1500 байт первого пакета + последующие, в сумме ~1300-1400 байт payload).
Чтобы реально увидеть содержимое HTTP, есть два пути:
- Запустить с
SSLKEYLOGFILE, который выгрузит ключи сессии в файл, и Wireshark расшифрует pcap. - Использовать
curl --trace-asciiилиmitmproxy, которые видят HTTP до шифрования.
Первый путь:
export SSLKEYLOGFILE=$HOME/sslkeys.log
curl https://example.com/ -o /dev/null -s
# Wireshark: Preferences -> Protocols -> TLS -> Pre-Master-Secret log -> $HOME/sslkeys.log
В Wireshark теперь pcap покажет расшифрованный HTTP. Полезно для дебага, но в продакшене никогда не делайте этого — ключи сессии могут компрометировать всю историю запросов.
Анализ ретрансмиссий и потерь
В реальной сети не всё идеально. Пакеты теряются, ретрансмитятся, прибывают не по порядку. tcpdump покажет это в флагах:
tcpdump -r trace.pcap -nn | grep -i retransmission
Если в нашем pcap есть retransmission — это значит TCP-стек обнаружил потерю (через duplicate ACKs или timeout) и переслал данные. Не критично, но если retransmissions много — сеть перегружена.
Также полезно посмотреть статистику:
tshark -r trace.pcap -q -z io,stat,0
Это покажет общее число пакетов, размер payload, длительность сессии.
Для глубокого анализа: открыть pcap в Wireshark, Analyze -> Expert Information. Wireshark подсветит проблемы: out-of-order packets, retransmissions, zero windows, large delays.
Что искать — чек-лист
Когда вы анализируете pcap своего HTTPS-запроса, вы должны увидеть:
Если чего-то нет -- это сигнал к копанию
После handshake — зашифрованные данные. Структура: PSH+ACK с TCP payload >40 байт.
В конце — FIN или RST. Если запрос завершён нормально, обе стороны шлют FIN, потом ACK — это graceful close. Если соединение оборвалось (timeout, network glitch) — RST.
Попробуй сам
Захвати свой собственный pcap по схеме выше. Открой его в tshark, найди следующее:
# 1. Сколько DNS-запросов в pcap?
tshark -r trace.pcap -Y 'dns.qry.name' -T fields -e dns.qry.name | sort -u
# 2. Какой SNI в ClientHello?
tshark -r trace.pcap -Y 'tls.handshake.type==1' -T fields -e tls.handshake.extensions_server_name
# 3. Сколько байт занял TLS handshake (от SYN до первого PSH с данными)?
tshark -r trace.pcap -q -z conv,tcp
# 4. Сколько пакетов всего? Каков их совокупный размер?
tshark -r trace.pcap -q -z io,stat,0
# 5. Есть ли retransmissions?
tshark -r trace.pcap -Y 'tcp.analysis.retransmission'
Если в pcap слишком много пакетов (background-трафик от других процессов), уточни фильтр при захвате:
sudo tcpdump -i en0 -w trace2.pcap 'host 93.184.216.34'
# (использовать тот же IP, что выдал dig example.com)
И сравни: один и тот же curl можно запустить, изменив curl —tls-max 1.2, и увидеть разницу в TLS handshake — 2 RTT вместо 1.
Сравнение HTTP/1.1 vs HTTP/2 vs HTTP/3
Если вы делаете curl --http2 https://example.com или curl --http3 https://example.com, pcap будет отличаться:
- HTTP/1.1 — то, что мы разбирали. TLS over TCP, в один поток.
- HTTP/2 — то же самое поверх TLS, но frames вместо текста, multiplexing. В pcap не видно разницы на уровне пакетов (всё зашифровано), но размер response может быть меньше из-за HPACK-сжатия заголовков.
- HTTP/3 — QUIC, UDP! Никакого TCP handshake. В pcap: только UDP пакеты на :443. Initial QUIC packet содержит ClientHello внутри.
Захватите три pcap для одного и того же URL с разными --http версиями. Сравните количество пакетов, время handshake, тип transport. Это упражнение примерно за час даёт интуицию того, как современный web эволюционирует.
HTTP/2 frames и HTTP/3 QUIC: что видит pcap и что происходит внутри
Итог
В этом уроке мы научились снимать «магию» с HTTPS:
- Захват —
sudo tcpdump -i <iface> -w trace.pcap 'port 53 or port 443'. - Чтение —
tcpdump -r trace.pcap -nn -ttttилиtsharkдля структурированного вывода. - Четыре фазы — DNS, TCP handshake, TLS handshake, HTTP request.
- SNI в ClientHello — доступен в plaintext, виден через
tshark -Y 'tls.handshake.type==1'. - Certificate chain — виден в server response,
tls.handshake.type==11. - Зашифрованный HTTP — можно расшифровать через
SSLKEYLOGFILE+ Wireshark, только в dev. - Retransmissions и проблемы — видны через
tcp.analysis.retransmissionфильтр.
В следующем уроке — собираем HTTPS-сервер на Python с self-signed cert. Это маленький ssl.SSLContext и http.server, но с правильно сделанным сертификатом, который curl будет принимать без --insecure.