9.1Базовый уровень
Свяжи DNS, IP, маршрутизацию, ARP, NAT и порт с этапами одного запроса. Пойми, какие сведения есть у процесса, какие у ядра и какие появляются только после разрешения имени.
Зачем вообще уровни
Идея слоёной модели одна: каждый уровень решает свою задачу и не знает, что внутри у соседа. Ethernet понятия не имеет об IP-адресах, он доставляет кадр соседу по проводу. IP не знает про соединения и доставляет отдельный пакет «куда-то туда, по возможности». TCP даёт поток байт, а про HTTP не знает. Самому HTTP неважно, что под ним: TCP или QUIC.
OSI живёт в учебниках и в разговорах («L7-балансировщик», «L3-связность»). В интернете реально работает четырёхуровневая TCP/IP (она же DoD-модель). На собесе называют обе и показывают соответствие: уровни OSI 5–6–7 в реальном стеке слиты в «прикладной».
Инкапсуляция похожа на упаковку в конверт. Уровень берёт всё, что пришло сверху, целиком считает это полезной нагрузкой, приписывает спереди свой заголовок (служебные поля: адреса, флаги, длины) и передаёт получившееся вниз. HTTP-запрос оказывается внутри TCP-сегмента, сегмент внутри IP-пакета, а пакет внутри Ethernet-кадра. Матрёшка. На приёме всё разбирается в обратном порядке, это декапсуляция: каждый уровень снимает свой заголовок и отдаёт остаток наверх.
На практике отсюда следует одно, и всплывает оно постоянно: уровень видит только свой заголовок, а всё остальное считает непрозрачными байтами. Поэтому L4-балансировщик физически не может выбрать бэкенд по URL: URL лежит в чужой полезной нагрузке, да ещё и зашифрованной, если сверху TLS.
«L2 доставляет кадр внутри одного сегмента сети по MAC-адресу. L3 доставляет пакет между сетями по IP-адресу, без гарантий. L4 добавляет понятие процесса (порт) и, в случае TCP, надёжность и порядок. На L7 появляется смысл: методы, ресурсы, заголовки». Дальше можно прицепить практику: L4-балансировщик (LVS, AWS NLB) видит только пятёрку и молотит пакеты, L7-балансировщик (nginx, Envoy, ALB) терминирует TCP+TLS и распределяет по URL, заголовкам, куки, но стоит дороже.
IP-адреса, маски и подсети
IPv4-адрес занимает 32 бита и записывается четырьмя октетами. Логически он делится на сетевую часть
и хостовую, границу задаёт маска. Запись CIDR 10.0.5.17/24 означает: под сеть
отведены первые 24 бита, под хост оставшиеся 8. Маска /24 = 255.255.255.0.
10.0.5.17/24
адрес сети 10.0.5.0 все хостовые биты в 0
broadcast 10.0.5.255 все хостовые биты в 1
хостов 2^8 - 2 = 254 минус адрес сети и broadcast
диапазон 10.0.5.1 - 10.0.5.254
Маска нужна вот для чего: хост смотрит на адрес назначения и по маске решает, сосед ли это
по канальному сегменту или пакет надо отдать шлюзу. Если dst & mask == my & mask,
ищем MAC соседа через ARP и шлём напрямую. Иначе шлём на MAC шлюза по умолчанию,
а IP-адрес назначения при этом не меняется.
| Диапазон | Что это | Где встречается |
|---|---|---|
10.0.0.0/8 | приватная сеть (RFC 1918) | корпоративные сети, VPC, поды Kubernetes |
172.16.0.0/12 | приватная (RFC 1918) | сеть Docker по умолчанию - 172.17.0.0/16 |
192.168.0.0/16 | приватная (RFC 1918) | домашние роутеры |
127.0.0.0/8 | loopback | 127.0.0.1, весь /8 - тоже loopback |
169.254.0.0/16 | link-local (APIPA) | DHCP не ответил; 169.254.169.254 - метадата облака |
100.64.0.0/10 | CGNAT (RFC 6598) | операторский NAT, Tailscale |
224.0.0.0/4 | multicast | OSPF, mDNS, IPTV |
0.0.0.0/0 | маршрут по умолчанию; как bind - «все интерфейсы» | ListenAndServe(":8080") |
Сервис слушает 127.0.0.1:8080 внутри контейнера, а снаружи ничего не работает, хотя «порт
же проброшен». Loopback виден только внутри сетевого namespace. Слушать надо 0.0.0.0:8080
(в Go достаточно ":8080"). Бывает и обратный вариант вопроса: в поде Kubernetes два
контейнера, и один ходит к другому по localhost. Вот это работает, потому что
сетевой namespace у них общий.
NAT: как приватные адреса выходят в интернет
Приватные адреса не маршрутизируются в интернете. Роутер на границе делает NAPT
(он же PAT, он же «NAT overload»): подменяет в исходящем пакете
src IP:port на свой публичный адрес и свободный порт, а соответствие
записывает в таблицу трансляций. По этой таблице роутер узнаёт ответный пакет и подменяет адрес обратно.
внутри таблица NAT на роутере снаружи
192.168.1.5:51514 192.168.1.5:51514 -> 203.0.113.8:40001 203.0.113.8:40001 -> 142.250.74.68:443
192.168.1.9:51514 192.168.1.9:51514 -> 203.0.113.8:40002 203.0.113.8:40002 -> 142.250.74.68:443
Отсюда три следствия, о них и спрашивают. Первое: соединение может начать только внутренняя сторона. Снаружи в таблице записи нет, поэтому входящие блокируются (нужен port forwarding, UPnP или hole punching через STUN/TURN). Второе: записи протухают по таймауту - обычно через 30–120 секунд для UDP и через десятки минут для TCP. Из-за этого рвутся долгие простаивающие соединения, и ровно поэтому включают TCP keep-alive. Наконец, число портов ограничено: за одним публичным адресом больше ~64 тысяч одновременных исходящих соединений на один dst IP:port не спрячешь.
DNS: как имя превращается в адрес
DNS устроен как распределённая иерархическая база. Полное имя читается справа налево:
в www.google.com. точка в конце обозначает корень, за ней зона com,
потом google.com. Клиент (stub resolver внутри libc или Go) сам иерархию не обходит:
он задаёт рекурсивный вопрос ближайшему резолверу (провайдерскому, 8.8.8.8,
1.1.1.1, корпоративному). Серию итеративных запросов делает уже резолвер.
| Тип | Что возвращает | Зачем на практике |
|---|---|---|
| A | IPv4-адрес | основной тип; несколько A-записей = примитивная балансировка round-robin |
| AAAA | IPv6-адрес | Happy Eyeballs: клиент пробует v6 и v4 параллельно и берёт, что быстрее |
| CNAME | алиас на другое имя | указать на CDN/балансировщик. Нельзя на вершине зоны рядом с SOA/NS |
| MX | почтовый сервер + приоритет | маршрутизация почты |
| TXT | произвольный текст | SPF, DKIM, DMARC, подтверждение владения доменом (ACME/Let's Encrypt) |
| SRV | имя + порт + приоритет + вес | service discovery: Consul, Kubernetes headless-сервисы, SIP |
| NS | авторитативные серверы зоны | делегирование зоны |
| SOA | параметры зоны | серийник, TTL отрицательного кэширования |
| PTR | имя по адресу | обратная зона in-addr.arpa, антиспам-проверки |
У Go два резолвера. Чистый Go-резолвер сам читает /etc/resolv.conf и шлёт UDP;
cgo-резолвер зовёт getaddrinfo из libc и умеет NSS, mDNS, sssd. Go выбирает сам,
а форсировать можно через GODEBUG=netdns=go / netdns=cgo.
Практическое следствие: Go не кэширует DNS сам - кэш есть только у ОС и резолвера. А о
смене DNS соединение из пула keep-alive не узнаёт вовсе: адрес спрашивают только при dial, и
долгоживущий http.Client может месяцами держать соединение к IP, который давно
выведен из ротации. Transport.IdleConnTimeout и CloseIdleConnections()
закрывают только простаивающие соединения, а ограничения на возраст занятого у
http.Transport нет - его делают обёрткой в DialContext или на сервере.
ARP: от IP-адреса к MAC
IP-пакет нельзя просто положить в провод: туда кладут кадр Ethernet, а у кадра адреса MAC, не IP.
Этот разрыв закрывает ARP: «у кого IP 10.0.5.1, сообщите свой MAC».
Запрос идёт широковещательно на ff:ff:ff:ff:ff:ff, а отвечают юникастом.
Результат живёт в ARP-кэше десятки секунд (ip neigh в Linux).
$ ip neigh
10.0.5.1 dev eth0 lladdr 02:42:ac:11:00:01 REACHABLE
10.0.5.23 dev eth0 lladdr 02:42:ac:11:00:17 STALE
$ arping -I eth0 10.0.5.1 # ручной ARP-запрос
$ tcpdump -n -i eth0 arp # посмотреть широковещательные who-has
Для ответа запомни одно: ARP работает только внутри одного канального сегмента. Если адрес назначения в другой сети, хост никогда не спрашивает про него ARP - он спрашивает MAC шлюза. IP-адреса в пакете при этом не меняются от источника до получателя (если нет NAT), а MAC-адреса переписываются на каждом роутере. В IPv6 роль ARP играет NDP поверх ICMPv6.
Порт против сокета
Порт всего лишь 16-битное число (0–65535) в заголовке TCP/UDP, метка «какому процессу отдать».
Сокет принадлежит операционной системе. Это файловый дескриптор, за которым стоят буферы,
состояние и пятёрка: {протокол, локальный IP, локальный порт, удалённый IP,
удалённый порт}. Уникальна именно пятёрка, а не порт.
- У сервера кончаются не порты, а file descriptors (
ulimit -n), память под сокеты и очередь accept (somaxconn,backlog). - У клиента кончаются эфемерные порты: в Linux по умолчанию
net.ipv4.ip_local_port_range = 32768 60999, это ~28 тысяч. Но лимит считается на пару(dst IP, dst port), поэтому к одному бэкенду выйдет 28 тысяч, а к десяти разным 280 тысяч. - Порты 0–1023 привилегированные: для них нужен root или
CAP_NET_BIND_SERVICE.
Маршрутизация: как пакет находит дорогу
У каждого хоста есть таблица маршрутов. Маршрут выбирают по правилу longest prefix match:
из всех подходящих побеждает тот, у которого длиннее маска. У 0.0.0.0/0 префикс
самый короткий, поэтому он означает «всё остальное», то есть маршрут по умолчанию.
$ ip route
default via 192.168.1.1 dev wlan0 # 0.0.0.0/0 - шлюз по умолчанию
192.168.1.0/24 dev wlan0 proto kernel # своя подсеть, без шлюза
172.17.0.0/16 dev docker0 # мост докера
$ ip route get 142.250.74.68
142.250.74.68 via 192.168.1.1 dev wlan0 src 192.168.1.5
Дальше пакет идёт по цепочке роутеров. В IPv4-заголовке есть поле TTL (в IPv6 это
Hop Limit): каждый роутер уменьшает его на 1, а на нуле пакет уничтожают и
шлют источнику ICMP Time Exceeded. Так сеть защищается от петель, и на этом же
работает traceroute: он шлёт пакеты с TTL 1, 2, 3… и собирает адреса тех, кто
ответил ICMP-ошибкой.
Что происходит, когда вводишь google.com и жмёшь Enter
Вопрос-каркас: по нему интервьюер смотрит, насколько глубоко ты можешь зайти и где остановишься. Отвечай крупными блоками, называй, что дорого по времени, а потом по просьбе углубляйся в любой из них.
- Разбор ввода. Браузер решает, это URL или поисковый запрос. Нормализует ввод и сверяется со списком HSTS preload: если домен там есть, схема принудительно становится https ещё до какого-либо трафика.
- Проверка кэшей. HTTP-кэш браузера (может отдать страницу вообще без сети),
Service Worker, кэш DNS браузера, кэш DNS ОС,
/etc/hosts. - DNS. Рекурсивный запрос к резолверу, дальше корень → TLD → авторитативный (см. схему выше). Обычно по UDP/53, а большой ответ приходит усечённым с флагом TC, и запрос повторяют по TCP. В ответе набор A/AAAA; клиент по Happy Eyeballs пробует v6, а через ~250 мс и v4 (в Go - через 300 мс).
- Маршрутизация и ARP. Хост по маске понимает, что адрес чужой, берёт шлюз по умолчанию, узнаёт его MAC по ARP, кладёт IP-пакет в Ethernet-кадр.
- TCP handshake. SYN → SYN-ACK → ACK, один RTT. Здесь же согласуются MSS, window scale, SACK. По дороге домашний роутер делает NAT.
- TLS handshake. ClientHello с SNI и ALPN, ответ сервера, проверка цепочки сертификатов, вывод общего секрета через ECDHE. В TLS 1.3 это один RTT, при возобновлении с early data - ноль.
- HTTP-запрос.
GET / HTTP/1.1плюс заголовки (или HEADERS-фрейм в HTTP/2). На пути стоят CDN, балансировщик, реверс-прокси, и каждый может ответить из кэша. - Ответ и рендер. Браузер строит DOM из HTML и CSSOM из CSS, объединяет в render tree,
считает layout (геометрию), paint (пиксели слоёв) и composite (сборку слоёв на GPU).
Параллельно preload scanner уже тянет подресурсы, а синхронный
<script>безdefer/asyncостанавливает парсер.
Помогают три приёма. Назови стоимость в RTT: «до первого байта три круга - TCP, TLS и сам запрос; QUIC схлопывает первые два». Перечисли места, где ответ может прийти раньше: кэш браузера, Service Worker, кэш DNS, CDN edge, кэш реверс-прокси. И скажи, что ломается: DNS отдал протухший IP, у сервера нет промежуточного сертификата, SNI не совпал с именем, MTU меньше 1500, а PMTUD заблокирован файрволом.
Вопросы
6Уровни и что на них живёт
- L1 физический - вольты, свет, радио; по нему бегут биты.
- L2 канальный - Ethernet, Wi-Fi (802.11), PPP. Адресует по MAC, передаёт кадры и не выходит за пределы одного широковещательного сегмента. Здесь же VLAN и коммутаторы.
- L3 сетевой - IP, ICMP, IPsec, протоколы маршрутизации (OSPF, BGP). Адресует по IP, передаёт пакеты. Гарантий никаких, только best effort: пакет может пропасть, задублироваться, прийти не в том порядке. Здесь работают роутеры.
- L4 транспортный - TCP, UDP, QUIC (формально поверх UDP), SCTP. Здесь появляется порт, то есть адрес конкретного процесса на хосте. TCP добавляет надёжность, порядок, управление потоком и перегрузкой, UDP не добавляет ничего.
- L5/L6 в OSI - сеансовый и представления (сессии, шифрование, кодировки). В реальном стеке их отдельно нет: TLS формально «L6», а на деле библиотека между TCP и HTTP.
- L7 прикладной - HTTP, gRPC, DNS, SMTP, SSH, AMQP, Redis-протокол, Postgres wire protocol.
Зачем это знать на практике
Почти любой вопрос про инфраструктуру задают в терминах уровней.
L4-балансировщик (LVS/IPVS, AWS NLB, HAProxy в режиме tcp) видит только пятёрку
адрес/порт и перекладывает пакеты. Он дешёвый и держит миллионы соединений, но не знает ни URL,
ни заголовка Host, ни куки. L7-балансировщик (nginx, Envoy, ALB, Traefik)
терминирует TCP и TLS, разбирает HTTP и умеет маршрутизировать по пути, хосту, заголовкам,
делать ретраи отдельных запросов и circuit breaking - но ест процессор и добавляет задержку.
Из той же оперы правило безопасности «фильтруем на L3/L4 файрволом, на L7 - WAF».
Про инкапсуляцию - обязательно проговорить
На отправке каждый уровень дописывает свой заголовок вокруг данных верхнего: HTTP-текст оборачивается в TCP-сегмент, сегмент в IP-пакет, пакет в Ethernet-кадр. На приёме всё разбирается обратно. Отсюда MTU: в Ethernet-кадр влезает 1500 байт полезной нагрузки, IP-заголовок 20 и TCP-заголовок 20 съедают 40, остаётся MSS 1460. Если у кого-то на пути MTU меньше (VPN, PPPoE, туннели в облаке), а ICMP-сообщения «Fragmentation Needed» режет файрвол, получаешь классический баг: рукопожатие проходит, мелкие запросы работают, а большой POST висит намертво. Это чёрная дыра PMTU.
«На каком уровне работает TLS?» Правильный ответ не «на шестом», а «формально между транспортом и приложением; в реальном стеке TCP/IP это часть прикладного уровня, отдельного уровня для него нет». «А ARP на каком?» - на стыке L2 и L3: он использует L2-широковещание, чтобы разрешить L3-адрес, поэтому его относят то к L2, то к L2.5.
Маска и подсеть
10.0.5.17/24: 24 бита - сеть, 8 - хост. В адресе сети все хостовые биты нулевые
(10.0.5.0), в broadcast единичные (10.0.5.255), полезных адресов
2^8 - 2 = 254. Чем короче префикс, тем больше сеть: в /16 65534 хоста,
в /30 два (классический линк между роутерами), /32 означает один конкретный адрес.
$ ipcalc 10.0.5.17/24
Address: 10.0.5.17 00001010.00000000.00000101. 00010001
Netmask: 255.255.255.0 = 24
Network: 10.0.5.0/24
Broadcast: 10.0.5.255
HostMin: 10.0.5.1 HostMax: 10.0.5.254
Маршрут выбирают по правилу longest prefix match: если адрес назначения после наложения маски совпадает с собственной сетью, шлём напрямую соседу (узнав его MAC через ARP), иначе отдаём кадр на MAC шлюза по умолчанию, не трогая IP назначения.
Приватные диапазоны
Диапазоны RFC 1918 (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)
в интернете не маршрутизируются. Ещё полезно помнить 127.0.0.0/8 (loopback целиком,
не только 127.0.0.1), 169.254.0.0/16 (link-local; сюда входит и
169.254.169.254, метадата-сервис в AWS/GCP/Yandex Cloud, откуда при SSRF утекают креды)
и 100.64.0.0/10 (CGNAT).
NAT
Почти всегда это NAPT: роутер подменяет src IP:port на свой публичный
адрес и свободный порт, запись кладёт в таблицу трансляций и по ней разворачивает обратный трафик.
Услышать хотят три следствия:
- Соединение может инициировать только внутренняя сторона - снаружи записи в таблице нет. Поэтому p2p требует port forwarding, UPnP или hole punching через STUN/TURN.
- Записи протухают: у UDP обычно через 30–120 с, у TCP через минуты и десятки минут. Из-за этого простаивающее соединение «внезапно» перестаёт работать; лечат это TCP keep-alive с интервалом меньше таймаута NAT.
- Порты кончаются: за одним публичным IP на одну пару (dst IP, dst port) больше ~64 тысяч одновременных соединений не спрячешь. На NAT-шлюзе перед большим кластером это реальная беда: SNAT port exhaustion в Kubernetes случается сплошь и рядом.
Потому что NAT оказался достаточно хорошим костылём и снял остроту дефицита. Заплатили за это сквозной адресуемостью, а вокруг обхода NAT вырос целый пласт технологий. В IPv6 адресов хватает всем, NAT не нужен, а роль ARP играет NDP поверх ICMPv6.
Порядок разрешения имени
- Кэш браузера (десятки секунд), затем кэш ОС (
systemd-resolved,nscd), затем/etc/hosts- до сети дело может и не дойти. - Stub resolver шлёт вопрос с флагом RD=1 («сделай за меня всю работу») на резолвер из
/etc/resolv.conf. Ходит он по UDP/53; если ответ не влез, приходит флаг TC (truncated), и клиент повторяет запрос по TCP/53. С EDNS0 UDP-ответ может быть больше 512 байт. - Резолвер идёт по иерархии: корневой сервер отдаёт NS для
com, TLD-сервер отвечает NS дляgoogle.com, а авторитативный возвращает саму A-запись с флагом AA=1. - Ответ кладётся в кэши на всём обратном пути на время TTL.
TTL и отрицательное кэширование
TTL задаёт владелец зоны: у A-записей обычно 60–300 секунд, у NS часы. Перед миграцией TTL заранее опускают до 60 секунд, переключают запись, ждут и потом поднимают обратно. NXDOMAIN тоже кэшируется, срок задаёт поле minimum в SOA. Поэтому «создал запись, а её не видно» часто означает, что ты успел спросить имя до создания и отрицательный ответ осел в кэше.
Типы записей
- A / AAAA хранят IPv4 / IPv6. Несколько A-записей дают round-robin-балансировку «для бедных»: порядок ротируется, но клиент может закэшировать одну и сидеть на ней.
- CNAME - алиас на другое имя, разрешение продолжается по цепочке. Нельзя на вершине зоны (рядом с SOA и NS), поэтому провайдеры придумали ALIAS/ANAME.
- MX указывает почтовый сервер с приоритетом (чем меньше число, тем выше приоритет).
- В TXT кладут произвольный текст: SPF, DKIM, DMARC, подтверждение владения доменом для
ACME/Let's Encrypt (
_acme-challenge). - SRV:
_service._proto.name→ приоритет, вес, порт и хост. Порт отдают она и записи SVCB/HTTPS (RFC 9460), а на SRV держится service discovery в Consul и headless-сервисы Kubernetes. - NS - делегирование зоны, SOA - параметры зоны, PTR - обратное разрешение.
Go не кэширует DNS сам: кэш только у ОС и резолвера. А соединение из пула
http.Transport о смене DNS не узнаёт вовсе: адрес спрашивают только при dial, и
клиент может месяцами держать соединение к IP, который давно выведен из ротации.
IdleConnTimeout закрывает только простаивающие соединения; возраст занятых
ограничивают обёрткой в DialContext или на сервере. И ещё: у Go два резолвера, чистый Go (сам читает
/etc/resolv.conf) и cgo (getaddrinfo, умеет NSS/mDNS), а переключают их
через GODEBUG=netdns=go|cgo.
Решает он вот что: в провод кладут не IP-пакет, а Ethernet-кадр, и адреса у кадра
MAC. Значит, перед отправкой хосту нужно знать MAC следующего узла. ARP-запрос уходит на
ff:ff:ff:ff:ff:ff, его видят все в сегменте, а отвечает только владелец адреса.
$ ip neigh # ARP-кэш в Linux
10.0.5.1 dev eth0 lladdr 02:42:ac:11:00:01 REACHABLE
10.0.5.23 dev eth0 lladdr 02:42:ac:11:00:17 STALE
$ tcpdump -n -i eth0 arp
ARP, Request who-has 10.0.5.1 tell 10.0.5.17, length 28
ARP, Reply 10.0.5.1 is-at 02:42:ac:11:00:01, length 46
Главная мысль ответа
ARP не работает через роутер. Если адрес назначения в другой сети, хост никогда не спрашивает про него ARP, он спрашивает MAC своего шлюза. По дороге IP-адреса источника и назначения не меняются (пока нет NAT), а MAC-адреса переписываются на каждом роутере. Так коротко и объясняют разницу между L2 и L3.
Что ещё бывает вокруг ARP
- При Gratuitous ARP хост сам объявляет «этот IP теперь мой». Так переезжает виртуальный IP при failover (keepalived/VRRP), чтобы коммутаторы и соседи обновили кэш.
- Proxy ARP - роутер отвечает за чужой адрес.
- ARP spoofing возможен, потому что у протокола нет аутентификации: кто угодно в сегменте может ответить «это я» и устроить MITM. Защищаются Dynamic ARP Inspection, статическими записями, 802.1X.
- В IPv6 ARP заменён на NDP поверх ICMPv6 (Neighbor Solicitation/Advertisement) и использует multicast вместо broadcast.
Отсюда вывод: сервер обслуживает тысячи клиентов на одном порту. Порт 443 один, а сокетов столько, сколько соединений, и ядро различает их по пятёрке. Классический неверный ответ на собесе: «на каждого клиента сервер выделяет новый порт» - нет, новый порт выделяет клиент, а серверный остаётся тем же.
ln, _ := net.Listen("tcp", ":8080") // один слушающий сокет
for {
conn, err := ln.Accept() // каждый Accept -> новый fd на том же :8080
if err != nil { continue }
go handle(conn) // conn.RemoteAddr() у всех разный
}
Два типа сокетов у TCP
- Слушающий: пятёрка неполная (
{tcp, *, 8080, *, *}), у него две очереди - SYN queue (полуоткрытые) и accept queue (готовые, длина =backlog, ограниченаnet.core.somaxconn). Когда accept queue переполнена, SYN молча отбрасываются, а клиент ловит «непонятные» таймауты. - Установленный: полная пятёрка, свои буферы приёма и отправки в ядре.
Где реально кончаются ресурсы
- Сервер упирается в файловые дескрипторы (
ulimit -n), память ядра под буферы и длину accept-очереди, но не в порты. - Клиент упирается в эфемерные порты:
net.ipv4.ip_local_port_range = 32768 60999, то есть ~28 тысяч. Лимит считается на пару (dst IP, dst port): к одному бэкенду 28 тысяч, к десяти разным уже 280 тысяч. Этот лимит и выжигаетTIME_WAIT, когда нет keep-alive. - Порты 0–1023 привилегированные: нужен root или
CAP_NET_BIND_SERVICE.
Сокеты бывают не только сетевые. У unix domain socket (net.Dial("unix", "/var/run/x.sock"))
тот же API, но нет сетевого стека, портов и checksum: на одной машине он ощутимо быстрее,
а доступ к нему разграничивают правами файловой системы. Это и есть ответ на «как ускорить
локальный вызов, если оба процесса на одном хосте».
1. Разбор строки и HSTS
Браузер решает, это URL или поисковый запрос, нормализует (punycode для кириллицы, схема, порт
по умолчанию). Затем проверяет HSTS: если домен есть в preload-списке или в локальном
кэше HSTS, схема принудительно становится https ещё до какого-либо трафика. Так
браузер защищается от downgrade-атаки, и редиректа 301 не будет.
2. Кэши: сети может и не быть вообще
По очереди: HTTP-кэш браузера (свежий ответ отдаётся мгновенно), Service Worker (может ответить
из своего кэша офлайн), кэш DNS браузера, кэш DNS операционной системы, /etc/hosts.
Скажи это вслух: здесь впервые отвечают на вопрос «почему быстро/медленно».
3. DNS
Stub resolver шлёт рекурсивный запрос (RD=1) резолверу по UDP/53. Резолвер обходит иерархию:
корень → NS зоны com → NS зоны google.com → авторитативный сервер отдаёт
A/AAAA с TTL. Если ответ не влез в датаграмму, ставится флаг TC и запрос повторяют по TCP. Клиент получает набор
адресов и по Happy Eyeballs (RFC 8305) начинает соединяться по IPv6, а через четверть секунды и по IPv4,
оставляя того, кто ответил первым. В жизни ответ для google.com придёт из кэша резолвера за единицы
миллисекунд, а адрес будет ближайшим GeoDNS/anycast-узлом.
4. Маршрутизация, ARP, NAT
Хост накладывает маску: адрес не свой - берём default gateway. ARP даёт MAC шлюза, IP-пакет уходит в Ethernet-кадре. Домашний роутер делает NAPT: подменяет src на публичный адрес и свободный порт. Дальше пакет идёт по цепочке роутеров, каждый уменьшает TTL и переписывает MAC-адреса, а IP остаются прежними.
5. TCP-рукопожатие - 1 RTT
SYN (со своим ISN, опциями MSS, window scale, SACK permitted, timestamps) → SYN-ACK → ACK. После третьего пакета сокет в состоянии ESTABLISHED, и данные можно слать прямо с ним. Если сервер за CDN и находится в соседней стране, это уже 20–40 мс.
6. TLS-рукопожатие - 1 RTT (TLS 1.3)
ClientHello везёт версии, шифронаборы, SNI (по нему сервер выбирает сертификат, когда на
одном IP много доменов), ALPN (h2, http/1.1; h3 бывает только в TLS внутри QUIC) и
уже свою долю ключа ECDHE. Сервер отвечает ServerHello, сертификатом и цепочкой, подписью и
Finished - дальше трафик шифруется. Клиент проверяет цепочку до доверенного корня, срок действия,
соответствие имени в SAN, отзыв (OCSP stapling). Асимметрика нужна, только чтобы
аутентифицировать сервер и договориться об общем секрете, а сами данные шифруются симметрично
(AES-GCM или ChaCha20-Poly1305). При возобновлении сессии по билету бывает 0-RTT.
7. HTTP-запрос и путь до приложения
Браузер шлёт GET / HTTP/1.1 с Host, User-Agent,
Accept, Accept-Encoding, Cookie, а если ALPN
договорился о h2, то HEADERS-фрейм с заголовками, сжатыми HPACK. По пути обычно стоят CDN edge (может
ответить из кэша), L4-балансировщик, реверс-прокси/L7-балансировщик, который терминирует TLS и
выбирает бэкенд, и только потом само приложение. Каждый прокси добавляет
X-Forwarded-For/Forwarded и, если всё сделано правильно,
X-Request-ID для сквозной трассировки.
8. Ответ и рендер
Приходят статус, заголовки и тело (обычно Transfer-Encoding: chunked или
DATA-фреймы в h2, часто со сжатием gzip/br). Браузер потоково парсит HTML в DOM и CSS
в CSSOM, объединяет их в render tree, считает layout (геометрию), paint
(растеризацию слоёв) и composite (сборку слоёв на GPU). Параллельно preload scanner уже
тянет подресурсы. Синхронный <script> без defer/async
блокирует парсер, CSS блокирует рендер, и отсюда растут все метрики FCP/LCP.
- Стоимость в RTT: «на холодную до первого байта три круга: TCP, TLS, сам запрос. QUIC схлопывает первые два в один, а по билету в ноль».
- Где ответ может прийти раньше: кэш браузера, Service Worker, кэш DNS, CDN edge, кэш реверс-прокси.
- Что ломается: протухший IP в кэше при живом keep-alive; сервер не отдал промежуточный
сертификат (в браузере работает, в
curlиз контейнера - нет); имя не совпало с SAN; MTU меньше 1500 и PMTUD зарезан файрволом; IPv6-адрес есть, а связности нет - спасает Happy Eyeballs.
Что стоит запомнить
- DNS превращает имя в набор адресов; IP доставляет пакеты между сетями; порт выбирает транспортную конечную точку на узле.
- ARP разрешает локальный IPv4 адрес следующего узла в MAC, а не адрес удалённого сервера через интернет.
- TCP-соединение различают локальный/удалённый IP и порт; несколько соединений могут слушаться на одном server port.
Частые ошибки
- Не смешивай DNS имя, IP-адрес, порт и сокет: это разные уровни адресации.
- NAT меняет адреса/порты на границе сети и требует состояния соответствия для ответного трафика.
- DNS lookup не гарантирует, что найденный адрес доступен или TLS-сертификат ему соответствует.
Самопроверка
Сначала реши задачу самостоятельно, затем открой ответ и сравни ход рассуждений.
Чтение кода
1. Разбери результат DNS
Что именно гарантирует вызов и произойдёт ли TLS handshake?
d := net.Dialer{Timeout: 3 * time.Second}
conn, err := d.DialContext(ctx, "tcp", "api.example:443")
Ответ и разбор
Из snippet нельзя узнать, сколько адресов вернёт resolver и какие именно: DNS lookup зависит от текущего resolver-ответа. DialContext разрешает имя при необходимости и пытается установить TCP-соединение к адресу на порту 443; при успешном вызове вернётся TCP conn. Сам вызов не выполняет TLS handshake и не отправляет HTTP-запрос. Таймаут Dialer и ctx ограничивают установление соединения.
Код-ревью
2. Найди ошибку модели сокета
Автор говорит: "На :8080 может быть только одно соединение, потому что порт уже занят слушателем". Проверь код и исправь объяснение.
ln, _ := net.Listen("tcp", ":8080")
for {
conn, err := ln.Accept()
if err != nil { continue }
go handle(conn)
}
Ответ и разбор
Listen занимает локальный listening socket на :8080, а Accept создаёт отдельное установленное соединение с собственным удалённым адресом/портом. Один listener принимает много соединений. В production также нужно ограничить обработку ошибок, закрывать соединения и завершать accept loop при shutdown.
Применение
3. Проследи Enter до первого запроса
Пользователь вводит https://shop.example:8443/orders. Перечисли основные сетевые шаги до получения HTTP-ответа и назови два независимых места, где запрос может завершиться ошибкой.
Ответ и разбор
Клиент разбирает URL, получает адрес через DNS, выбирает маршрут и next hop, устанавливает TCP к адресу на порту 8443, выполняет TLS handshake с проверкой сертификата/имени, затем отправляет HTTP-запрос и читает ответ. Ошибки возможны, например, при DNS lookup, маршруте/таймауте TCP, проверке TLS или на уровне HTTP; успех предыдущего шага не гарантирует следующий.
Применение
4. Посчитай сокетные адреса
К одному server listener 10.0.0.5:8080 подключились два клиента: 192.0.2.4:51000 и 192.0.2.7:51001. Запиши 4-кортеж каждого TCP-соединения и объясни, что у них общее.
Ответ и разбор
Кортежи: (192.0.2.4, 51000, 10.0.0.5, 8080) и (192.0.2.7, 51001, 10.0.0.5, 8080). У них общий адрес/порт сервера, но разные удалённые пары, поэтому ядро различает соединения, принятые одним listener.