Компьютерные сети: от IPC к C/Winsock
DNS: имя становится списком адресов
От getaddrinfo до recursive resolver и authoritative answer, затем перебор IPv4/IPv6 candidates без смешения DNS и connect errors.
DNS: имя становится списком адресов
До сих пор процесс A знал destination IP процесса B. Пользователь так не работает: он вводит имя. DNS решает задачу именования, но не открывает socket, не проверяет порт и не гарантирует доступность приложения.
Задача
Клиент должен отправить Привет сервису server.example на порту 27015.
На входе есть две строки; на выходе getaddrinfo может дать ноль, один или
несколько адресов разных семейств:
node = server.example
service = 27015
candidate 1 = [2001:db8::20]:27015
candidate 2 = 192.0.2.20:27015
Даже идеальный DNS answer не сообщает, какой candidate установит TCP соединение. Поэтому функция разрешения имени возвращает список, а network client должен пройти его по явной политике.
От приложения к authoritative answer
Обычный процесс не начинает обход root servers самостоятельно. Он вызывает system resolver. Stub resolver на хосте обращается к настроенному recursive resolver. Тот сначала проверяет cache, а при miss проходит delegation до authoritative server зоны.
У каждой границы свой результат:
- cache hit возвращает ещё действующий answer без полного обхода hierarchy;
- referral указывает, где продолжать поиск, но не является конечным адресом;
NXDOMAINозначает, что имя не существует согласно authoritative answer;- timeout означает отсутствие ответа до deadline, а не отсутствие имени;
- A и AAAA records дают candidates, но не выбирают один connection.
TTL ограничивает срок использования cached record. Он не означает, что клиент обязан держать connection столько же, и не обещает мгновенную смену адреса после изменения authoritative zone.
C/Winsock2 checkpoint: список, а не один sockaddr
Современный клиент использует getaddrinfo и AF_UNSPEC:
struct addrinfo hints = {0};
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
struct addrinfo *results = NULL;
int rc = getaddrinfo("server.example", "27015", &hints, &results);
if (rc != 0) {
fprintf(stderr, "getaddrinfo: %d\n", rc);
return EXIT_FAILURE;
}
SOCKET connected = INVALID_SOCKET;
for (const struct addrinfo *it = results; it != NULL; it = it->ai_next) {
SOCKET candidate = socket(it->ai_family, it->ai_socktype, it->ai_protocol);
if (candidate == INVALID_SOCKET) {
continue;
}
if (connect(candidate, it->ai_addr, (int)it->ai_addrlen) == 0) {
connected = candidate;
break;
}
closesocket(candidate);
}
freeaddrinfo(results);
Здесь две разные error surfaces. Ошибка getaddrinfo относится к разрешению
имени. Ошибка connect относится к конкретному address:port. Нельзя вывести
«DNS сломан» из WSAECONNREFUSED, как нельзя вывести «порт открыт» из
успешного A/AAAA answer.
Dual stack — это несколько попыток
Узел может иметь и IPv6, и IPv4 address. Последовательный клиент, который ждёт полный timeout первого недоступного candidate, создаёт долгую искусственную задержку. Production libraries часто используют staggered attempts, но базовый инвариант проще: не объявлять весь service недоступным, пока политика не исчерпала допустимые candidates.
Схема разделяет три события, которые часто ошибочно называют «DNS»:
- resolver вернул candidates;
- route существует для конкретного address family;
- handshake завершился на конкретном address и port.
Логируйте выбранный family, numeric address, port, duration и error code. Не логируйте токены и чувствительный payload. Имя без фактически выбранного адреса плохо объясняет инцидент, потому что два клиента могли получить один DNS answer, но подключиться к разным endpoints.
Предсказание и контролируемое наблюдение
Перед командами запишите:
- какие A/AAAA records ожидаете;
- какой resolver отвечает;
- какой TTL увидите;
- означает ли answer доступность порта 443.
Затем выполните:
Resolve-DnsName example.com -Type A
Resolve-DnsName example.com -Type AAAA
Test-NetConnection example.com -Port 443 -InformationLevel Detailed
Если аудитория работает без внешней сети, преподаватель подставляет имя из
контролируемой зоны. В evidence сохраните command, timestamp, DNS server,
records/TTL и RemoteAddress, который реально выбрал TCP probe.
Сравните два независимых эксперимента:
| Эксперимент | Что доказывает успех | Чего не доказывает |
|---|---|---|
Resolve-DnsName | resolver вернул record для имени и типа | port открыт, route работает, endpoint честный |
Test-NetConnection -Port | попытка к выбранному address завершилась | HTTP/TLS/application исправны |
Negative answers и cache
NXDOMAIN, пустой набор нужного record type и SERVFAIL — разные исходы.
Negative answers тоже могут кешироваться. Повтор команды через секунду поэтому
не обязан снова дойти до authoritative server.
Не «лечите» DNS заменой hostname на случайный найденный IP в production code. Такой обход теряет политику адресов, может сломать TLS hostname validation и зафиксировать устаревший endpoint. Для диагностики numeric address полезен как контролируемая переменная, но не как скрытая постоянная.
Частая ошибка: первый candidate считают единственным
Такой код выглядит рабочим на машине с одним IPv4 answer:
connect(s, results->ai_addr, (int)results->ai_addrlen);
Он ломается, когда первым приходит недоступный IPv6 address, когда family не поддерживается локальным route или когда service опубликован на нескольких адресах. Correctness требует пройти список и закрыть socket каждой неудачной попытки. Performance policy определяет, делать это последовательно или с разумным overlap.
Итого
- DNS превращает имя в records/candidates; connection создаёт отдельный этап.
getaddrinfo(AF_UNSPEC)требует перебора результатов иfreeaddrinfoна всех завершённых путях.- Диагноз содержит DNS outcome, выбранный numeric endpoint и результат connect, а не одно слово «сеть».
Теперь у процесса A есть рабочий TCP connection. Следующая глава добавит поверх него TLS и HTTP и покажет, почему certificate error возникает после доступного порта, а HTTP status — только после успешной защищённой сессии.