docs: explain NAT detection through its main idea and steps

This commit is contained in:
evgeny committed 2026-10-17 17:06:04 +02:00
1 parent d70d6cb66a
commit eb3dd7f237
1 file changed
+50 -145
+50 -145
View File
@@ -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 не заменяют это измерение.