Embedded HandbookПриложения
Все разделы › XII. Сети и DevOps для инженера
STM32F103, STM32F407 · уровень средний · 9 мин чтения
Открыть в интерактивном справочнике

107. Что происходит, когда вводишь адрес: DNS, DHCP, HTTP, TLS

Между адресом в строке браузера и страницей — шесть шагов, и прибор, который шлёт данные на сервер по имени, проходит их все. Раздел собирает и разбирает сообщения каждого протокола своим кодом на C. В опыте код спрашивает встроенный DNS Docker об имени соседнего контейнера и забирает страницу с его сервера, а приветствие TLS собирает настоящий OpenSSL — и функция раздела читает из него имя сервера, открытым текстом.

Кадры и ARP — раздел 105, TCP — раздел 106, часы прибора — раздел 12.

Как это называется в документации

Оригинал Перевод
dynamic host configuration protocol (DHCP) раздача адресов в сети
lease аренда адреса
renewal (T1), rebinding (T2) продление аренды, поиск любого сервера
resolver распознаватель имён
resource record запись DNS
name compression сжатие имён указателями
time to live (TTL) срок хранения ответа в кэше
NXDOMAIN «такого имени нет»
status line строка состояния HTTP
header field заголовок HTTP
chunked transfer encoding передача тела кусками
ClientHello приветствие клиента TLS
server name indication (SNI) имя сервера в приветствии
application-layer protocol negotiation (ALPN) выбор протокола поверх TLS
certificate сертификат

107.1 Шесть шагов

От адреса в строке до страницы: шесть шагов и протоколы под ними

  1. DHCP: при включении прибор получает адрес, маску, шлюз и адрес DNS.
  2. ARP: MAC-адрес шлюза.
  3. DNS: имя сервера превращается в IP-адрес.
  4. TCP: рукопожатие с сервером.
  5. TLS: обмен ключами и проверка сертификата.
  6. HTTP: запрос и ответ.

Первые три шага прибор делает редко: адрес живёт срок аренды, ответ DNS — свой срок в кэше. Последние три — на каждое соединение.


107.2 DHCP: адрес на время

Сообщений четыре: DISCOVER — «есть тут сервер?», OFFER — «возьми этот адрес», REQUEST — «беру», ACK — «твой». У клиента адреса ещё нет, поэтому он шлёт с 0.0.0.0 на 255.255.255.255 и просит отвечать широковещательно:

unsigned dhcp_discover(uint8_t *d, uint32_t xid, const uint8_t mac[6])
{
    memset(d, 0, 240);
    d[0] = 1;                           /* сообщение клиента */
    d[1] = 1;                           /* Ethernet */
    d[2] = 6;                           /* длина MAC */
    pu32(d + 4, xid);                   /* номер обмена: ответ несёт тот же */
    pu16(d + 10, 0x8000);               /* адреса ещё нет — отвечать всем */
    memcpy(d + 28, mac, 6);
    pu32(d + 236, DHCP_COOKIE);
    static const uint8_t opt[] = { 53, 1, DHCP_DISCOVER,   /* тип сообщения */
                                   55, 4, 1, 3, 6, 51,    /* просим маску, шлюз, DNS и срок */
                                   255 };                 /* конец */
    memcpy(d + 240, opt, sizeof opt);
    return 240 + sizeof opt;
}

DHCPDISCOVER — 250 байт: 236 байт заголовка, «волшебное число» 0x63825363 и 10 байт параметров. На практике сообщение дополняют нулями хотя бы до 300 байт, как было принято у предшественника DHCP — BOOTP: часть серверов и посредников короткие сообщения отбрасывает. lwIP, например, добивает параметры до 68 байт, и сообщение выходит в 308. Предложение сервера в модели разобрано так: адрес 10.107.107.50, маска 255.255.255.0, шлюз и DNS 10.107.107.1, срок 86 400 с — сутки. Продлевать аренду у того же сервера начинают через 43 200 с, половину срока, а искать любой сервер — через 75 600 с, 7/8 срока. Ответ с чужим номером обмена отвергнут: номер связывает вопрос с ответом.

Аренда DHCP на сутки: когда продлевать, когда искать другой сервер, когда адреса уже нет


107.3 DNS: имя → адрес

Запрос — заголовок 12 байт, имя метками, тип и класс:

unsigned dns_query(uint8_t *q, uint32_t id, const char *name, uint32_t type)
{
    pu16(q, id);
    pu16(q + 2, 0x0100);                /* обычный запрос, «нужна рекурсия» */
    pu16(q + 4, 1);                     /* один вопрос */
    pu16(q + 6, 0);
    pu16(q + 8, 0);
    pu16(q + 10, 0);
    unsigned n = 12;
    while (*name) {                     /* «stm.example» → 3 stm 7 example 0 */
        const char *dot = strchr(name, '.');
        unsigned len = dot ? (unsigned)(dot - name) : (unsigned)strlen(name);
        q[n++] = (uint8_t)len;
        memcpy(q + n, name, len);
        n += len;
        name += len + (dot != 0);
    }
    q[n++] = 0;
    pu16(q + n, type);
    pu16(q + n + 2, DNS_IN);
    return n + 4;
}

Имя stm107-peer в запросе — 0b 73 74 6d 31 30 37 2d 70 65 65 72 00: длина метки, её буквы и ноль в конце; весь запрос — 29 байт. В ответе сервер повторяет вопрос, а имя в записи второй раз не пишет — вместо него указатель c0 0c, «имя с 12-го байта». Весь ответ — 45 байт. Разбор пропускает имена обоих видов:

static int dns_skip(const uint8_t *m, unsigned n, unsigned *p)
{
    while (*p < n) {
        unsigned l = m[*p];
        if (l == 0) {
            *p += 1;
            return 1;
        }
        if ((l & 0xC0u) == 0xC0u) {
            *p += 2;
            return *p <= n;
        }
        *p += 1u + l;
    }
    return 0;
}

Запись несёт срок: столько секунд ответ можно держать в кэше. Код 3 в заголовке — «такого имени нет», и адреса из такого ответа не берут.


107.4 HTTP/1.1: текст, пустая строка, тело

Запрос — строки текста, каждая с \r\n, в конце пустая строка:

GET / HTTP/1.1
Host: stm107-peer
Connection: close

Заголовок Host в HTTP/1.1 обязателен: на одном адресе живёт много сайтов. Весь запрос — 56 байт текста. Ответ — строка состояния, заголовки, пустая строка и тело; имена заголовков сравнивают без учёта регистра, длина тела — в Content-Length:

unsigned http_response(const char *r, unsigned n, unsigned *status, unsigned *clen)
{
    const char *end = 0;
    for (unsigned i = 0; i + 3 < n && !end; i++)
        if (memcmp(r + i, "\r\n\r\n", 4) == 0)
            end = r + i + 4;
    if (!end || memcmp(r, "HTTP/1.", 7) != 0)
        return 0;
    *status = (unsigned)(r[9] - '0') * 100u + (unsigned)(r[10] - '0') * 10u + (unsigned)(r[11] - '0');
    const char *cl = http_header(r, end, "content-length");
    *clen = 0;
    while (cl && *cl >= '0' && *cl <= '9')
        *clen = *clen * 10u + (unsigned)(*cl++ - '0');
    return (unsigned)(end - r);
}

Тело читают по длине, а не «до закрытия»: соединение может остаться открытым для следующего запроса. Нет длины, а есть Transfer-Encoding: chunked — тело идёт кусками, у каждого своя длина.


107.5 TLS: что видно до шифрования

TLS начинается с приветствия клиента, и оно идёт открытым текстом: версии, наборы шифров, ключ для обмена и расширения. Одно из них — имя сервера, чтобы сервер со многими сайтами знал, какой сертификат показать:

        if (type == 0 && len >= 5 && v[2] == 0) {           /* server_name */
            unsigned k = be16(v + 3);
            if (k < sizeof h->sni && 5 + k <= len) {
                memcpy(h->sni, v + 5, k);
                h->sni[k] = 0;
            }
        } else if (type == 43 && len >= 1) {                /* supported_versions */
            for (unsigned i = 1; i + 1 < 1u + v[0] && i + 1 < len; i += 2) {
                uint32_t ver = be16(v + i);
                if ((ver & 0x0F0Fu) != 0x0A0Au && ver > h->max_version)
                    h->max_version = ver;                   /* 0x?A?A — пустышки GREASE */
            }

Приветствие собрал OpenSSL — тот же, что в браузерах и на серверах: приветствие — 517 байт, тело записи OpenSSL дополнил до 512. Функция раздела нашла в нём имя «stm32.example», наибольшую версию 0x0304 — TLS 1.3 — и протоколы h2 и http/1.1 для ALPN. Имя сайта видит каждый на пути, хотя всё остальное зашифровано, — если не включено шифрованное приветствие (ECH). Да и запрос DNS обычно уходит открытым текстом.

Сертификат проверяют и по сроку действия, поэтому прибору нужны верные часы: без часов реального времени и синхронизации сертификат окажется «ещё не действительным».


107.6 Опыт: DNS Docker и страница

Два контейнера в сети 10.107.107.0/24: в соседнем — сервер на Python (python -m http.server), в нашем — код раздела на обычных сокетах.


107.7 На приборе

DHCP, DNS и HTTP умеют и маленькие стеки: это разбор байтов, как в этом разделе. TLS — самая дорогая часть: память под ключи и сертификаты и время на рукопожатие. Опыт: клиент TLS 1.2 на mbedTLS 3.6.4, урезанный до одного набора шифров (ECDHE-ECDSA с AES-128-GCM) и одной кривой P-256, собран под Cortex-M4 с -Os и прогнан в QEMU против сервера в той же программе:

Что Сколько
флеш-память клиента 58 580 байт — 57 КБ
куча с буферами записей по 16 КБ 36 458 байт
то же с буферами по 4 КБ 11 882 байта
стек 1,3 КБ
рукопожатие со стороны клиента 85 млн команд
запрос и ответ на 1000 байт после него 185 тысяч команд

На F407 при 168 МГц рукопожатие — не меньше полусекунды, но оно одно на соединение. Буферы по 4 КБ годятся, только если сервер согласен на короткие записи (расширение max_fragment_length), иначе запись на 16 КБ не примется. Больше всех флеш-памяти берут AES с таблицами, ядро TLS, арифметика кривых и разбор сертификатов. F103C8 с его 64 КБ флеш-памяти и 20 КБ ОЗУ TLS не вмещает: одна библиотека почти заполняет флеш-память. Там TLS отдают модулю связи.


107.8 Грабли

Прибор не получает адрес по DHCP. Не выставлен флаг «отвечать всем» — ответ ушёл на адрес, которого у прибора ещё нет.

Адрес «пропал» через сутки. Аренду не продлили: продлевать с половины срока.

DNS отвечает, а адреса нет. Первой в ответе идёт запись CNAME — псевдоним, адрес — в следующей записи.

Сервер сменил адрес, а прибор ходит на старый. Ответ DNS держат в кэше не дольше срока из записи.

Сервер отвечает 400. Нет заголовка Host.

Приём ответа висит. Тело читают до закрытия, а сервер держит соединение: читать по Content-Length или просить Connection: close.

TLS не проходит проверку сертификата. Часы прибора стоят на 2000 году: сертификат «ещё не действителен».


107.9 Итоги

Дальше: раздел 108 «Маршрутизация, VLAN, Wi-Fi и VPN» — как пакет находит дорогу между сетями.

Открыть в интерактивном справочнике