From eb3dd7f2376a1d26baacb59a51bc8a499d2d2ae5 Mon Sep 17 00:00:00 2001 From: evgeny Date: Sat, 17 Oct 2026 17:06:04 +0200 Subject: [PATCH] docs: explain NAT detection through its main idea and steps --- doc/nat_detection.txt | 195 +++++++++++------------------------------- 1 file changed, 50 insertions(+), 145 deletions(-) diff --git a/doc/nat_detection.txt b/doc/nat_detection.txt index a58ad18a..95aca4b5 100644 --- a/doc/nat_detection.txt +++ b/doc/nat_detection.txt @@ -1,161 +1,66 @@ -Детектор рабочего UDP endpoint -============================== +Как работает детектор NAT +======================== -Владелец сокета единственный выбирает и публикует его профиль. Он относится -к протоколу, семейству, фактическому локальному IP:port и поколению сокета. -PUBLIC/NAT/PRIVATE/LOCAL — политика конфигурации, не измеренный NAT-тип. +Детектор выясняет две вещи: какой внешний IP:порт получает рабочий UDP-сокет +при обращении к разным IP и проходят ли к нему пакеты от незнакомого IP. +Проверяется именно сокет, через который работает приложение. -Независимые факты в src/routing_layer/address_profile.h: - mapping: UNKNOWN, UNCHANGED, EIM, DEPENDENT; - inbound: UNKNOWN, OPEN, CONTACT_REQUIRED; - source: ETCP — измерен рабочий endpoint. +Нужны три узла: A проверяет себя, B наблюдает его внешний адрес, +C помогает проверить входящие пакеты. Команды и результаты передаются +по уже работающей сети uTun, в том числе через TCP или промежуточные узлы. +Сами тестовые пакеты идут напрямую по UDP. -Практические классы: strict — стабильные разные mapping к разным IP; -eim open — одинаковый mapping и полученная чистая проба незнакомого входа; -eim filtered — одинаковый mapping и вход, полученный только после контакта. -UNCHANGED отличается от EIM отсутствием трансляции IP:port. -DEPENDENT не означает запрета всех входящих. Дополнительные порты одного IP -не сравниваются. Разные Node ID за одним IP не дают независимого испытания. -OPEN подтверждается пакетом; CONTACT_REQUIRED — оценка по серии до/после: -потерю первых UDP-проб нельзя абсолютно отличить от их блокировки. -Таймаут, отсутствие помощника и противоречия не означают strict. +Порядок проверки +--------------- -Участники и каналы ------------------- +1. A отправляет пакет узлу B. Тот сообщает, с какого IP:порта его получил. + Ответ идёт по управляющему соединению, поэтому блокировка прямого + UDP-ответа не мешает узнать внешний адрес A. -S — рабочий сокет владельца A; B — наблюдатель; C — помощник с другим IP. -Выбираются все активные UTUN/CHAT-группы с READY-маршрутами управления; -прямые UDP-линки A-C не обязательны. Управление допускает транзит и TCP. -Перебираются все подходящие UDP endpoint узла, включая адрес работающего линка. -Таймаут одного адреса не исключает остальные адреса того же узла. +2. Через B узел A просит C отправить несколько пакетов на этот внешний адрес. + До этого A не должен отправлять ничего на IP узла C с проверяемого сокета. + Приложение хранит историю таких отправок и учитывает новые во время проверки. + На входящие тестовые пакеты A автоматически не отвечает. -OBSERVE принимается по UDP, фактический source IP:port возвращается через -подписанный зашифрованный router-отчёт OBSERVATION. Прямой PONG не требуется. -Получатель проверяет публичность source IP: локальный/немаршрутизируемый -источник не подходит для интернет-проверки. OBSERVATION возвращает отказ, -наблюдение не сохраняется; владелец переходит к следующему кандидату. -Отдельный короткий UDP-обмен с B поддерживает mapping и контролирует входящий путь. -Его отсутствие запрещает отрицательный вывод о filtering, но не стирает mapping. -Обычное LAN-наблюдение INIT_RESPONSE сохраняется в линке и не сбрасывает -интернет-профиль сокета. Локальный путь управления сам по себе допустим. +3. Получатель проверяет настоящий адрес отправителя каждого тестового пакета. + Локальный или другой непубличный IP не подходит для интернет-проверки. + Для проверки незнакомого входа также не подходят IP узла B, собственный + внешний IP узла A и IP, на который A уже отправлял пакеты. + Если помощник не подходит, A выбирает другого. Разные узлы с одним IP + не считаются разными источниками. Проверяются также подлинность пакета + и принадлежность текущей попытке, чтобы чужой или старый пакет не влиял на результат. -У source-сокета C один отдельный probe_only UDP-сокет помощника. Он не входит -в NODEINFO/NCD и не открывает фильтр собственного рабочего endpoint C. -Дополнительного сравнительного порта нет. +4. Затем A сам отправляет пакет C и узнаёт, какой внешний IP:порт видит C. + Если входящие до этого не проходили и внешний адрес совпал с увиденным B, + C повторяет отправку с того же IP:порта на исходный внешний адрес A. + Так проверяется, открыл ли исходящий контакт путь для входящих. + Параллельный обмен с B подтверждает, что UDP-связь в целом работает. -UDP-раунд ---------- +5. A повторяет измерение внешнего адреса через B и C. Результат принимается, + только если адрес для каждого направления остаётся стабильным. + Сравниваются разные IP назначения; другие порты того же IP не проверяются. -1. S отправляет B односторонний OBSERVE; через управление получает M_B. -2. A просит B делегировать C входящее испытание на M_B. B разрешает делегацию - только для действительного записанного наблюдения этого владельца и раунда. -3. C отправляет начальную серию входящих проб, без автоматического UDP-ответа. - Затем через B измеряет mapping своего отдельного сокета; отчёт также идёт - через router. Начальная серия могла уже подтвердить OPEN. -4. Публичность и незнакомость проверяет получатель S по фактическому source IP. - Локальный/немаршрутизируемый source немедленно исключает помощника из этой попытки. - Проверка идентичности, round и token предшествует отказу; чужой или поздний - пакет не может переключить кандидата. IP B, - собственный внешний IP S (hairpin) и прежний исходящий контакт не подходят: - A запрашивает другого помощника. Объявленный IP и Node ID не заменяют проверку. -5. Если начального входа нет, C выполняет полную серию PRE до исходящего - контакта S с C. После отчёта выдерживается окно доставки последней пробы: - управляющее сообщение может обогнать UDP. -6. S фиксирует границу контакта и отправляет OBSERVE на C. Через управление - получает M_C. Source полученной аутентичной входящей пробы имеет приоритет - перед предварительно объявленным endpoint помощника. -7. При M_C = M_B, полном чистом PRE без входа и исправном контроле B выполняется - POST: серия с того же endpoint C на исходный M_B. Source POST должен совпасть - с endpoint, к которому обращался S. История проверяется до намеренного контакта, - включая посторонние отправки во время PRE. Смена source не подтверждает filtering. -8. S повторяет OBSERVE к B и C. Изменение наблюдений одного направления - инвалидирует профиль и даёт address_changed. Этапы разных состояний не смешиваются. -9. Стабильные M_B != M_C дают DEPENDENT; равенство — UNCHANGED/EIM. - Чистый вход до контакта даёт OPEN. Полный PRE без входа, успешный POST - и свежий прямой контроль B дают CONTACT_REQUIRED. Незавершённый ingress - оставляет inbound UNKNOWN, сохраняя измеренный mapping. - -Новый помощник получает новый round/token. Поздние пакеты предыдущей попытки -не используются. После исчерпания свежих IP допускается mapping-раунд с прежним -помощником, без обновления незнакомой входящей доступности. - -История, время и отмена +Что означает результат ---------------------- -История по IP учитывает INIT, DATA, ACK, keepalive, PING/PONG и другие отправки -в двух общих точках UDP-send. Проверяется на момент первого ingress, не только -на старте. Случайная отправка к C во время PRE делает опыт загрязнённым. -Для POST сохраняется граница истории до намеренного OBSERVE к C. +strict — B и C стабильно видят разные внешние IP:порты узла A. +Единого внешнего адреса для подключения с любого IP нет. -nat_contacts общей SQLite хранит семейство и фактический локальный IP:port -и один элемент на IP назначения. Перезапуск на том же endpoint историю не очищает. -Максимум 256 IP; переполнение, повреждение и ошибка записи отмечают её неполной. -Неполная история запрещает доказательство незнакомости. История описывает только -наблюдаемый приложением трафик: удаление БД или чужое использование tuple -не доказывает отсутствия состояния NAT. +eim open — B и C видят одинаковый внешний IP:порт, и пакет от C пришёл +ещё до исходящего контакта A с его IP. -Начальный раунд имеет короткий приоритет перед фоновыми NCD-пробами. -Уже известные IP не блокируются; работающий транспорт сохраняется. -Все операции выполняются в uasync-потоке. Один активный раунд на S. -Ожидание пробы — max(1 секунда, 4*RTT к B), серия — три отправки. -Цикл перебора ограничен 30 секундами. Истечение бюджета не назначает NAT-тип. -Повтор неполного EIM/inbound — через 30 секунд; новый подходящий endpoint -может разбудить раньше. Полный профиль обновляется через 30 минут. -Неудачная перепроверка сохраняет прежний успех. Частичный результат того же -mapping/endpoint сохраняет прежний inbound, но не переносит его с другого endpoint. +eim filtered — внешний IP:порт одинаковый, серия пакетов до контакта не прошла, +а после исходящего контакта пакеты с того же адреса C начали приходить. +Это оценка: потерю UDP-пакетов нельзя абсолютно отличить от блокировки. -Смена адреса сбрасывает профиль и отменяет раунд. Закрытие сокета, удаление группы -и остановка ядра отменяют UDP-пробы, heartbeat и helper jobs до закрытия транспорта. -Generation и токены исключают применение позднего ответа к новому socket ID/fd. -Неудачные реально отправленные native INIT к подписанному OPEN назначают recheck; -CONTACT допускает его после исчерпания reverse. Ошибки объединяются, минимум -пять минут между такими перепроверками рабочего сокета. +Если внешний IP:порт совпадает с локальным, отдельно отмечается отсутствие +трансляции адреса. Доступность входящих всё равно проверяется. -Wire v4 -------- +Если данных недостаточно, соответствующая часть результата остаётся UNKNOWN. +Известный тип NAT можно сохранить, даже если входящие проверить не удалось. +Таймаут и отсутствие подходящих узлов сами по себе не означают strict. -Команды: NAT_RUN, NAT_RESULT, NAT_RECHECK, NAT_OBSERVATION. -SOURCE/group берутся из проверенного router-заголовка, не из физического next hop. -Разрешены только участники выбранной группы; управление подписано и зашифровано. - -Тег OBSERVE: "NAT4UDP!", socket ID, generation, round, group. -Односторонняя проба добавляет этап (1 байт) и token (8 байт). -OBSERVATION проверяется по ожидаемому наблюдателю, socket ID/generation, -group, round, этапу и token. Каждый повторный UDP-запрос получает новый token. -Ingress дополнительно проверяет ключ C и фактический source endpoint. -Wire содержит только uint8_t; числа BE, адреса — сетевые байты. - -ETCP_PING_FLAG_NO_REPLY разрешён для OBSERVE/ADDRESS_CHECK с полным NAT-тегом. -Он запрещает автоматический PONG: приём пробы не создаёт исходящий контакт. -Обычные транспортные PING/PONG сохраняются. v3 несовместим с v4: -участвующие в измерении узлы обновляются вместе. - -Публикация, STUN и TCP ---------------------- - -Владелец публикует профиль в подписанном NODEINFO. Универсальный внешний -endpoint рекламируется после EIM; UNCHANGED использует собственный адрес. -DEPENDENT не рекламирует универсальный NAT endpoint. NCD использует профиль -для выбора попыток; работоспособность конкретной связи подтверждает handshake. - -STUN другого порта остаётся диагностикой IP/netif и никогда не назначает -профиль рабочему S. Результаты доступны через stun_nat API. -UDP-профиль не переносится на TCP. TCP inbound проверяется STCP handshake -к объявленному listener с проверкой ключа. Без публичного кандидата TCP -остаётся UNKNOWN/no_listener. SOCKS UDP не классифицируется как native endpoint. - -Проверки --------- - -test_route_ping: UDP-наблюдения, IPv4/IPv6 encoding, чужой ключ, неверный round, -старое поколение и отмена без callback. -test_nat_detection: несколько endpoint, одинаковые IP узлов, подпись/SQLite, -TCP listener, транзитная CHAT-группа, recheck и teardown. -test_nat_detection_faults (Linux): реальные UDP/router-пакеты, потерянные PONG, -PRE/POST, поздний PRE после контакта, посторонняя отправка, пропавший отчёт, -разные/нестабильные mapping, совпавший IP B, hairpin и перебор IP наблюдателя; -локальные source OBSERVE/ingress, переход к другому помощнику, чужой token -и сохранение интернет-профиля после наблюдения LAN. -Только часы тестового poll ускоряются; сокеты, шифрование и протокол настоящие. -tests/stream_netns/run.py --nat-only: 16 ядер, 6 настоящих NAT-шлюзов, -EIM open/filtered/strict. Главный network namespace проверяется на неизменность. +Соединения по LAN работают независимо и не сбрасывают интернет-профиль. +Если доступны только локальные узлы, определить интернет-NAT нельзя. +Результат относится к проверенному UDP-сокету: проверка STUN с другого порта +и работа TCP не заменяют это измерение.