Browse Source

docs: specify NAT v4 independent mapping reports and receiver-verified ingress

stream-multiview
evgeny 1 week ago
parent
commit
7673316dc8
  1. 446
      doc/nat_detection.txt
  2. 10
      doc/stun_nat.md
  3. 21
      tests/test_nat_detection_faults.c

446
doc/nat_detection.txt

@ -1,315 +1,151 @@
Детектор свойств endpoint
========================
Детектор рабочего UDP endpoint
==============================
Владелец рабочего сокета единственный выбирает и публикует его профиль.
Помощники возвращают наблюдения и выполняют прямые испытания. Профиль относится
Владелец сокета единственный выбирает и публикует его профиль. Он относится
к протоколу, семейству, фактическому локальному IP:port и поколению сокета.
PUBLIC/NAT/PRIVATE/LOCAL — политика конфигурации, а не результат измерения.
source=ETCP означает измерение рабочего endpoint; source=STUN — резервную оценку
по его IP/netif через другой порт. Успешный ETCP имеет приоритет до явного сброса endpoint.
Модель: src/routing_layer/address_profile.h.
mapping: UNKNOWN, UNCHANGED, EIM, DEPENDENT.
inbound: UNKNOWN, OPEN, CONTACT_REQUIRED.
source: NONE, ETCP, STUN.
round/generation: локальные ID раунда и жизненного цикла endpoint; в NODEINFO не входят.
UNCHANGED означает совпадение наблюдаемых и локального адресов на испытанных
направлениях. Оно не доказывает свободный вход. EIM означает одинаковый внешний
endpoint на испытанных разных IP и портах; оно также не доказывает свободный вход.
DEPENDENT означает различие mapping на испытанных назначениях, без общего
внешнего адреса. Дальнейшее разделение ADM/APDM не используется для выбора NCD.
PUBLIC/NAT/PRIVATE/LOCAL — политика конфигурации, не измеренный NAT-тип.
Независимые факты в src/routing_layer/address_profile.h:
mapping: UNKNOWN, UNCHANGED, EIM, DEPENDENT;
inbound: UNKNOWN, OPEN, CONTACT_REQUIRED;
source: ETCP — измерен рабочий endpoint.
Практические классы: strict — стабильные разные mapping к разным IP;
eim open — одинаковый mapping и полученная чистая проба незнакомого входа;
eim filtered — одинаковый mapping и вход, полученный только после контакта.
UNCHANGED отличается от EIM отсутствием трансляции IP:port.
DEPENDENT не означает запрета всех входящих. Дополнительные порты одного IP
не сравниваются. Разные Node ID за одним IP не дают независимого испытания.
OPEN подтверждается пакетом; CONTACT_REQUIRED — оценка по серии до/после:
потерю первых UDP-проб нельзя абсолютно отличить от их блокировки.
Таймаут, отсутствие помощника и противоречия не означают strict.
Участники и каналы
------------------
S — рабочий сокет владельца A; B — наблюдатель; C — помощник с другим IP.
Выбираются все активные UTUN/CHAT-группы с READY-маршрутами управления;
прямые UDP-линки A-C не обязательны. Управление допускает транзит и TCP.
Перебираются все подходящие UDP endpoint узла, включая адрес работающего линка.
Таймаут одного адреса не исключает остальные адреса того же узла.
OBSERVE принимается по UDP, фактический source IP:port возвращается через
подписанный зашифрованный router-отчёт OBSERVATION. Прямой PONG не требуется.
Отдельный короткий UDP-обмен с B поддерживает mapping и контролирует входящий путь.
Его отсутствие запрещает отрицательный вывод о filtering, но не стирает mapping.
У source-сокета C один отдельный probe_only UDP-сокет помощника. Он не входит
в NODEINFO/NCD и не открывает фильтр собственного рабочего endpoint C.
Дополнительного сравнительного порта нет.
UDP-раунд
---------
1. Владелец A рассматривает узлы всех активных UTUN/CHAT-групп и выбирает одну группу
с маршрутами к наблюдателю B и отдельному помощнику C. Следующие хопы управления
должны быть READY. Прямые ETCP/UDP-линки к B/C не обязательны; B нужен доступный UDP endpoint.
Через проверяемый сокет S выполняет OBSERVE. Ответ содержит фактический
source IP:port, наблюдаемый B, и исходный тег S/generation/round/group.
2. A выбирает C по подписанному NODEINFO выбранной группы. Его рабочий UDP-сокет
задаёт интерфейс/семейство; C использует два отдельных probe_only порта,
чтобы помощь другому узлу не открывала фильтр его собственного рабочего endpoint.
C может находиться за NAT, независимо от политики PUBLIC/NAT и собственного профиля.
3. Управление RUN/RESULT идёт A -> B -> C через router в одной группе с подписью
и шифрованием. C сначала отправляет ADDRESS_CHECK с primary-порта к endpoint A,
реально наблюдавшемуся B. Аутентичный ingress на S до исходящего контакта подтверждает OPEN.
PONG возвращает наблюдаемый внешний endpoint C. При отсутствии PONG C узнаёт mapping
своего primary-порта через B, после чего выполняется контролируемый PRE из трёх проб.
4. Если внешний IP C совпадает с IP B или S ранее контактировал с C, A пробует
другие пары и группы. Совпадающие Node ID и разные порты одного IP не дают
независимого IP-наблюдения. При отсутствии второго IP результат неполный.
При выборе пары сначала сохраняется возможность свежего помощника; ранее контактировавшая
пара используется для mapping после исчерпания свежих. Отчёт о PONG не заменяет ingress на S.
5. После первого испытания S выполняет OBSERVE к primary C. При различии mapping
с B уже известен DEPENDENT. Иначе C отдельно измеряет внешний endpoint второго порта
через A; если фильтр A мешает — через B. S выполняет OBSERVE к этому endpoint.
Оба порта C скрыты из NODEINFO и исключены из NCD. Проверяемый S не меняется.
6. Если первый ingress отсутствовал, C здоров и mapping к C совпал с mapping к B,
после исходящего контакта выполняется второе входящее испытание от того же C.
Контролируемый переход до/после контакта даёт CONTACT_REQUIRED.
7. S повторяет исходное наблюдение у B. Изменение этого mapping во время раунда
даёт UNKNOWN/address_changed и инвалидирует прежний общий внешний адрес.
8. При совпадении наблюдений B/C/C-alt выбирается UNCHANGED или EIM; различие
назначений даёт DEPENDENT. Первый чистый ingress даёт OPEN; подтверждённый
переход после контакта при неизменном endpoint — CONTACT_REQUIRED.
Если независимых от истории C больше нет, уже контактировавший C может измерить
mapping. Входящая часть остаётся UNKNOWN/contact_history. Отказ проверки одного
facet не превращается в ложный STRICT. Потери, ошибка отправки, отсутствие
помощника, разрыв управления и неправильный ответ имеют отдельную причину.
Контакты и повторы
-----------------
История IP назначения учитывает INIT, DATA, ACK, keepalive, PING/PONG и другие
отправки с S в двух общих точках UDP-send. Любая неожиданная отправка к C до
получения первого ingress делает входящее испытание загрязнённым.
История записывается в nat_contacts общей SQLite один раз на новый IP. Ключ —
семейство и фактический локальный IP:port. Перезапуск на том же endpoint её не
очищает. Максимум 256 IP на endpoint; переполнение/ошибка сохранения отмечаются
постоянным маркером неполной истории. Неизвестная история запрещает вывод OPEN
по испытанию, которое может использовать старое разрешение фильтра.
История характеризует наблюдаемый приложением трафик. Удаление БД, использование
того же tuple другим процессом или изменение NAT вне наблюдаемого раунда нельзя
превратить в доказательство полного отсутствия прежнего состояния. Результаты
остаются подсказками для попыток, подтверждение связи даёт handshake с нужным ключом.
Один активный раунд на S. Таймаут пробы — max(1 секунда, 4*RTT), не более 3 секунд;
три попытки, бюджет раунда 30 секунд. До первого результата повтор после ошибки
через 30 секунд или после появления подходящего транспорта; штатный refresh через 30 минут.
Успешный профиль не протухает по таймеру. Ошибка перепроверки сохраняет прежний результат
и редкий период; неизвестный inbound при том же mapping/endpoint сохраняет прежний ETCP inbound.
Свойства разных источников не объединяются в один ETCP-профиль. Смена адреса отменяет
старый раунд, сбрасывает профиль и назначает скорую проверку. Таймеры используют monotonic timebase.
После таймаута NCD перепроверка назначается только для реально отправленных native UDP INIT
к OPEN endpoint из текущего подписанного NODEINFO. CONTACT допускает такой повод после
исчерпания цикла CM вместе с reverse, а UNKNOWN/приватный адрес и SOCKS5 его не дают.
Активный раунд не перезапускается; повторные ошибки объединяются, минимум пять минут
между такими перепроверками одного рабочего сокета. Последний успех сохраняется.
Начальные измерения имеют короткий приоритет перед фоновыми проверками; завершённый
частичный результат освобождает его, чтобы соседи могли выполнить свои наблюдения.
Если выбранная группа ещё доставляет управление пиру, ему передаётся подписанный
зашифрованный RECHECK размером 23 байта (команда/версия/socket ID/endpoint).
Пир проверяет членство отправителя и совпадение endpoint со своим текущим сокетом;
тот же лимит действует на удалённые запросы. ID раунда и время в NODEINFO не возвращаются.
TCP и прокси
-------------
TCP inbound проверяется отдельным STCP handshake к объявленному настоящему
listener с проверкой ключа владельца. Его target должен присутствовать в
подписанном snapshot. Внешний порт исходящего TCP-соединения не рекламируется
как listener. Без публичного кандидата профиль остаётся UNKNOWN/no_listener.
UDP-факты не переносятся на TCP. Входящий TCP source port не определяет профиль
listener удалённого клиента. SOCKS5 UDP не классифицируется как native endpoint.
Корреляция и владение
--------------------
Транспортный PONG сопоставляется по криптографически случайному nonce, ожидаемому
ключу, source IP:port, ID и поколению принимающего сокета. Входящее испытание
дополнительно проверяет владельца, round, step и token. Перед callback PING
удаляется из pending. Отмена не вызывает callback и не интерпретирует void* arg
как контекст другого типа. Произвольный выбор первого IPv4/TCP-сокета запрещён.
Broker принимает RUN только для записанного им UDP-наблюдения данного владельца.
Участники и ключи берутся из подписанного реестра выбранной группы. SOURCE читается
из проверенного router-заголовка, не из ETCP next hop. Управление занимает 136 байт,
не содержит списков исключений и копий ключей. Размер, версия, enum и ненулевые
group/generation/round/token проверяются до обработки. Числа BE, IP — сетевые байты.
На смене адреса, закрытии сокета, reset/DELETE транспорта и остановке ядра
отменяются собственные раунды и связанные helper jobs. Поздние ответы не
применяются к новому socket generation даже при повторном ID и IP:port.
Между попытками разных пар локальный статус cycling отличает паузу от завершения измерения.
Удаление выбранной группы отменяет её раунды, jobs и наблюдения до закрытия router-каналов;
другие группы и последний успешный профиль сохраняются. Детектор останавливается
до разборки групп, сокетов и соединений.
Публикация и NCD
---------------
Профиль публикуется только владельцем в общей подписанной записи NODEINFO.
Списки и их границы входят в подпись; mapping/inbound/source
сохраняются и загружаются вместе. node_addresses — производная проекция:
nat_mapping, nat_inbound, nat_source, address_kind.
При изменении формата старый кэш адресов пересоздаётся; старый NAT_TYPE не
интерпретируется как новая модель. При сокращении SOCKMETA старые snapshots инвалидируются
с сохранением timestamp; адреса и REALITY остаются bootstrap-сведениями. Записи чата не изменяются.
Общий NAT-адрес рекламируется только после EIM. DEPENDENT не рекламирует
универсальный внешний endpoint. NTP.reachable не управляет рекламой адресов.
Ошибки публикации логируются; детектор повторяет сохранение/публикацию.
NCD различает OPEN, CONTACT и непроверенные публичные кандидаты. UNKNOWN допускает
ограниченные попытки. Контактирующие EIM используют обычные встречные INIT-пробы
NCD и reverse. Одинаковый внешний IP не запрещает hairpin и не доказывает общую
LAN; частные адреса проверяются маршрутизацией и аутентификацией нужного пира.
Профиль не закрывает уже работающий транспорт. Признак PUBLIC конфигурации
может влиять на очередность попыток CM, но не назначает подтверждённый NAT-тип.
Стриминг сохраняет raw NCD + REVERSE_REQ через группу, без routed DATA fallback.
1. S отправляет B односторонний OBSERVE; через управление получает M_B.
2. A просит B делегировать C входящее испытание на M_B. B разрешает делегацию
только для действительного записанного наблюдения этого владельца и раунда.
3. C отправляет начальную серию входящих проб, без автоматического UDP-ответа.
Затем через B измеряет mapping своего отдельного сокета; отчёт также идёт
через router. Начальная серия могла уже подтвердить OPEN.
4. Незнакомость проверяет получатель S по фактическому source IP. 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.
nat_contacts общей SQLite хранит семейство и фактический локальный IP:port
и один элемент на IP назначения. Перезапуск на том же endpoint историю не очищает.
Максимум 256 IP; переполнение, повреждение и ошибка записи отмечают её неполной.
Неполная история запрещает доказательство незнакомости. История описывает только
наблюдаемый приложением трафик: удаление БД или чужое использование tuple
не доказывает отсутствия состояния NAT.
Начальный раунд имеет короткий приоритет перед фоновыми NCD-пробами.
Уже известные IP не блокируются; работающий транспорт сохраняется.
Все операции выполняются в uasync-потоке. Один активный раунд на S.
Ожидание пробы — max(1 секунда, 4*RTT к B), серия — три отправки.
Цикл перебора ограничен 30 секундами. Истечение бюджета не назначает NAT-тип.
Повтор неполного EIM/inbound — через 30 секунд; новый подходящий endpoint
может разбудить раньше. Полный профиль обновляется через 30 минут.
Неудачная перепроверка сохраняет прежний успех. Частичный результат того же
mapping/endpoint сохраняет прежний inbound, но не переносит его с другого endpoint.
Смена адреса сбрасывает профиль и отменяет раунд. Закрытие сокета, удаление группы
и остановка ядра отменяют UDP-пробы, heartbeat и helper jobs до закрытия транспорта.
Generation и токены исключают применение позднего ответа к новому socket ID/fd.
Неудачные реально отправленные native INIT к подписанному OPEN назначают recheck;
CONTACT допускает его после исчерпания reverse. Ошибки объединяются, минимум
пять минут между такими перепроверками рабочего сокета.
Wire v4
-------
Команды: 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, неправильный
round, аутентичный PONG чужого ключа, старое поколение, отмена без callback.
test_nat_detection: разные socket ID, два рабочих UDP endpoint, TCP listener,
замена помощника с тем же IP, повтор по старому контакту, подпись/SQLite профиля,
закрытие S посреди раунда и повторный ID/порт. Утечки/таймеры/сокеты проверяются.
Недостаточная UTUN-группа и достаточная CHAT-группа проверяются отдельно с TCP next hop
и отсутствием прямого транспорта A-C. Неудачные native INIT проверяют отсутствие
лишних перепроверок для UNKNOWN, ожидание reverse для CONTACT и пятиминутный лимит.
test_sock_match: независимость политики от фактов, UNKNOWN, CONTACT, давность,
частные адреса и разрешённая попытка hairpin; профили не протухают по времени.
tests/stream_netns --nat-only: 16 настоящих uTun с 10 NAT, проверка всех UDP/TCP
профилей и точного собственного внешнего адреса. --nat-check включает эту фазу
до обычных JOIN/Merkle/BGP/streaming. Главный network namespace не меняется.
Резервное определение типа NAT через STUN: реализация IPv4 2026-10-12
------------------------------------------------------------------
Уточнение пользователя: STUN группирует рабочие сокеты по одинаковым IP/netif,
создаёт аналогичный UDP-сокет с другим портом и определяет тип NAT для группы.
Когда ETCP не может выполнить проверку из-за нехватки узлов, используется STUN.
Это заменяет прежнее предложение использовать STUN непосредственно на ETCP fd.
Резервная схема реализована для native IPv4 UDP. IPv6 остаётся отдельным этапом.
1. Реестр уникальных путей
Ключ группы — (address family, фактический local IP, netif_index), без порта,
имени и socket ID. Несколько рабочих портов одного IP/netif дают одну группу.
Одинаковый IP на разных netif и разные IP одного netif — разные группы.
netif=0 сохраняет смысл автоматической маршрутизации, не приравнивается
произвольно к явно привязанному интерфейсу. Wildcard не становится отдельным IP:
сначала нужен фактический адрес пути. Неопределённый путь получает UNKNOWN.
Сканировать native UDP, исключать probe_only, SOCKS5 и only_local. TCP listener
сохраняет отдельную проверку STCP: UDP-результат ему не присваивается.
Начать с IPv4, уже поддержанного stun_nat; IPv6 требует отдельного этапа:
XOR-MAPPED IPv6, IPv6 server/config/DNS и scope/interface, без переноса IPv4-фактов.
Выбрать представителя группы, скопировать фактические bind IP/device/mark/FIB.
Порты исходных сокетов не копировать; проба владеет собственным fd/reader.
Разные mark/FIB/device внутри одного ключа означают неоднозначный путь:
группа получает UNKNOWN/incompatible_route_policy с логом, а не случайный
результат первого сокета. Ключ IP/netif при этом не усложняется.
2. Измерение и кэш STUN
Одна активная проба и один результат на группу, независимо от числа её сокетов.
Тестовый порт случайный, отличается от рабочих портов. В пределах жизни ядра
не переиспользовать ранее измеренные порты на этом IP/netif; при исчерпании
не выполнять заведомо загрязнённое входящее испытание, вернуть явную причину.
Самостоятельно закрывать только пробный fd, отменять его DNS и таймеры.
STUN запускается при появлении группы, при наличии enabled и настроенных серверов.
Разные имена серверов с одинаковым IP не считаются независимыми наблюдателями.
На чистом тестовом порту сначала проверять filtering, потом mapping и стабильность
исходного наблюдения. Переносить в кэш только завершённые пригодные свойства;
потери, ошибки, отсутствие RFC5780 и сомнительный CHANGE дают UNKNOWN/причину.
В результате хранить ключ группы, её generation, случайный evidence ID,
test IP:port, фактические серверы, mapped адреса, mapping/filtering, причины,
время измерения monotonic и Unix microseconds. TTL=300с, refresh=180с,
повтор неполной проверки через 30с; темп новых транзакций остаётся ограниченным.
Применение готового кэша к новому сокету не обновляет время измерения.
3. Выбор результата NAT_DETECTION
NAT_DETECTION остаётся единственным владельцем эффективного socket profile,
nat_addr и публикации NODEINFO. STUN предоставляет кэш по ключу группы,
NAT_DETECTION проверяет его готовность/инвалидацию раз в секунду; сам STUN
не меняет профили рабочих сокетов.
Реестр групп не держит указателей на рабочие сокеты: потребитель проверяет
собственные socket generation и актуальную принадлежность группе перед применением.
Хранить отдельно собственный результат ETCP и выбранный эффективный профиль.
Добавить profile source: NONE / ETCP / STUN, в том числе в подпись, wire,
SQLite и диагностические API. STUN означает резервную оценку по интерфейсу,
ETCP — измерение рабочего endpoint. Подписывает публикацию только сам узел.
Приоритет: действующий измеренный ETCP -> пригодный STUN при невозможности
ETCP из-за отсутствия наблюдателя/независимого помощника -> UNKNOWN.
Для STUN использовать реальные NO_OBSERVER/NO_HELPER, не общее число узлов:
третий узел может быть offline, несовместимым или иметь тот же IP наблюдателя.
После исчерпания замены таких помощников разрешать резервное применение.
Пока STUN не готов, оставить UNKNOWN или ещё действующий профиль ETCP.
Частичный, но измеренный ETCP не заменять целиком STUN: DEPENDENT с неизвестным
ingress является полезным результатом. В первой версии не смешивать facets
разных источников. Неудача нового раунда не стирает ещё действующий ETCP.
Появление подходящего помощника ускоряет повтор ETCP; его успешный результат
заменяет резервный без ожидания TTL. Не сбрасывать ETCP при завершении STUN.
4. Преобразование и использование типа
RFC5780 EIM -> EIM; отсутствие трансляции в согласованном измерении -> UNCHANGED;
ADM/APDM и устойчивое расхождение обычных серверов -> DEPENDENT.
EIF -> OPEN; ADF/APDF -> CONTACT_REQUIRED; неизвестный facet остаётся UNKNOWN.
Это преобразование применяется с source=STUN и описывает резервную оценку.
Обычный MATCH остаётся неполной диагностикой, сам по себе не назначает EIM.
Для полного определения типа необходимы доступные серверы с RFC5780.
Тип STUN помогает выбирать прямые/reverse попытки, но не доказывает их успех.
Различное поведение NAT для рабочих и тестовых портов допустимо: источник
оценки виден в логах и метаданных, реальные факты ETCP имеют приоритет.
Не запрещать LAN/hairpin или закрывать работающую связь по резервной оценке.
NCD не объявляет рабочий endpoint недоступным только на основании STUN.
mapped IP:port тестового сокета никогда не копировать в nat_addr рабочего.
Не подставлять туда рабочий local port: NAT может изменить его независимо.
Общий внешний адрес рабочего сокета публиковать только с самостоятельным
подтверждением его EIM/endpoint средствами ETCP. При наличии одного наблюдателя
его адрес остаётся наблюдением для данного назначения, не общим NAT-адресом.
5. Жизненный цикл и диагностика
Добавление/удаление дубликата не повторяет STUN для неизменившейся группы.
Исчезновение последнего рабочего сокета удаляет группу и отменяет пробу.
Смена IP/interface/параметров маршрутизации инвалидирует соответствующий кэш;
смена маршрута без смены IP и явный recheck запускают новое измерение.
Generation относится к пути и раунду STUN, а не к имени представителя.
Поздние ответы не применяются к пересозданной группе. Reload/stop отменяют всё
до освобождения config/сокетов; порядок уничтожения не вызывает публикацию.
INFO: создание/удаление группы, начало/результат пробы, выбор source, возврат
к ETCP. DEBUG: объединённые дубликаты, transaction/generation/время/причины.
WARN/ERROR: неподходящие помощники, неоднозначный путь, timeout/ошибка/несогласованность.
В статусе доступны эффективный профиль, оба результата и причина выбора STUN.
Этапы реализации: (1) группировка и API STUN по ключу IP/netif,
(2) generation/TTL/обновление и завершение проб,
(3) source в модели/подписи/wire/SQLite/статусе,
(4) выбор резерва и возврат к ETCP, независимое хранение внешних адресов,
(5) локальные и изолированные сетевые тесты. Изменять ETCP UDP demux не требуется.
План тестов
-----------
- Два порта одного IP/netif: одна проба, применение резерва обоим сокетам.
Разные IP/netif: отдельные пробы; разные mark/FIB внутри группы: явный UNKNOWN.
- Смена представителя и удаление одного дубликата не теряют результат; удаление
последнего, новая группа и поздний ответ проверяют владение и generation.
- Нет узлов / один наблюдатель / все помощники за одним IP: STUN fallback.
Живой независимый помощник: ETCP; появление/исчезновение, TTL и обе очередности
завершения проверяют приоритет. Частичный ETCP не затирается полным STUN.
- RFC5780 EIM/EIF, EIM/ADF, EIM/APDF, ADM/APDM; обычный MATCH; недоступные
альтернативы, ignored CHANGE, DNS, потери, нестабильность и ограничение повторов.
- NAT по-разному обрабатывает тестовый и рабочий порты: резерв помечен STUN,
реальный ETCP его заменяет; тестовый mapped port не попадает в NODEINFO/NCD.
- Смена маршрута без смены IP, refresh, reload, stop, callbacks и утечки.
Общий evidence/time кэша не омолаживается при новом socket или NODEINFO.
- В tests/stream_netns разместить RFC5780-серверы с двумя IP/двумя портами,
сценарий двух uTun, затем добавить третьего. Проверить переключение источника
типа при RTT/jitter/loss; всю сетевую настройку выполнять в отдельных namespace.
Основания: https://www.rfc-editor.org/rfc/rfc5780#section-4.1 (выбор тестового порта
и ограниченность переноса результата), #section-4.3/#section-4.4 (классы).
Приоритет и применение резерва — согласованный проект uTun, не гарантия RFC.
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 наблюдателя.
Только часы тестового poll ускоряются; сокеты, шифрование и протокол настоящие.
tests/stream_netns/run.py --nat-only: 16 ядер, 6 настоящих NAT-шлюзов,
EIM open/filtered/strict. Главный network namespace проверяется на неизменность.

10
doc/stun_nat.md

@ -5,11 +5,11 @@
Для каждой группы создаётся один собственный неблокирующий UDP-сокет с тем же локальным IP,
привязкой к устройству и Linux SO_MARK / FreeBSD FIB. Тестовый порт выбирается случайно из 49152–65535.
Модуль сам регистрирует чтение в uasync, принимает STUN и закрывает свой сокет после измерения.
STUN не меняет исходный ETCP fd, его порт и обработчики. NAT_DETECTION использует его
оценку типа при NO_OBSERVER/NO_HELPER, помечает source=STUN и публикует подписанный профиль.
Действующий результат source=ETCP имеет приоритет, включая частичный. Мapped endpoint
тестового порта не переносится в nat_addr или NODEINFO рабочего сокета.
Поведение NAT на рабочем порту может отличаться; source показывает происхождение оценки.
STUN не меняет исходный ETCP fd, его порт и обработчики. Результат остаётся независимой
диагностикой IP/netif через `stun_nat` API. Он не назначает mapping/inbound рабочему сокету
даже при NO_OBSERVER/NO_HELPER: поведение NAT на другом порту может отличаться.
Ни профиль, ни mapped endpoint тестового порта не переносятся в `nat_addr` или NODEINFO
рабочего сокета. Детектор рабочего endpoint описан в `doc/nat_detection.txt`.
## Конфигурация

21
tests/test_nat_detection_faults.c

@ -8,8 +8,8 @@ enum fault_mode { FAULT_NONE, FAULT_NO_PONG, FAULT_FILTERED, FAULT_LATE_PRE, FAU
static struct {
struct nat_fixture *fixture;
struct UTUN_INSTANCE *owner, *observer, *helper;
struct ETCP_SOCKET *socket;
struct UTUN_INSTANCE *owner, *observer, *helper, *alias;
struct ETCP_SOCKET *socket, *alias_source;
enum fault_mode mode;
unsigned dropped, altered, direct_received, observations;
int contacted, injected;
@ -57,6 +57,11 @@ ssize_t __wrap_socket_sendto(socket_t fd, const void *packet, size_t length, con
struct sockaddr_storage destination = {0}; assert(target_length <= sizeof(destination));
memcpy(&destination, target, target_length);
int to_owner = route_ping_addr_equal(&destination, &fault.socket->local_addr, 1);
if (to_owner && source->instance == fault.alias && plain[3] == ETCP_PING &&
(plain[4] & ETCP_PING_FLAG_ADDRESS_CHECK)) {
fault.altered++;
return __real_socket_sendto(fault.alias_source->fd, packet, length, target, target_length);
}
if ((fault.mode == FAULT_NO_PONG || fault.mode == FAULT_NO_ANCHOR) && to_owner && plain[3] == ETCP_PONG) {
fault.dropped++; return length;
}
@ -148,20 +153,26 @@ static void run_alias(const char *alias_ip) {
}
struct UTUN_INSTANCE *owner = nat_fixture_node(&fixture, "127.0.0.2", NULL, NULL);
struct UTUN_INSTANCE *observer = nat_fixture_node(&fixture, "127.0.0.3", NULL, NULL);
struct UTUN_INSTANCE *alias = nat_fixture_node(&fixture, alias_ip, &first, NULL);
char extra[160]; snprintf(extra, sizeof(extra), "[server:alias]\naddr=%s:0\ntype=private\n", alias_ip);
struct UTUN_INSTANCE *alias = nat_fixture_node(&fixture, "127.0.0.8", &first, extra);
struct UTUN_INSTANCE *helper = nat_fixture_node(&fixture, "127.0.0.4", &second, NULL);
nat_detection_set_automatic(observer->nat_det, 0); nat_detection_set_automatic(alias->nat_det, 0);
nat_detection_set_automatic(helper->nat_det, 0);
nat_fixture_connect(&fixture, observer, "udp", alias); nat_fixture_connect(&fixture, observer, "udp", helper);
nat_fixture_connect(&fixture, owner, "udp", observer); nat_detection_set_automatic(owner->nat_det, 0);
struct ETCP_SOCKET *socket = test_etcp_bound_socket(owner, "udp");
memset(&fault, 0, sizeof(fault)); fault.fixture = &fixture; fault.owner = owner; fault.socket = socket;
fault.observer = observer; fault.helper = helper; fault.alias = alias; fault.alias_source = test_etcp_bound_socket(alias, "alias");
assert(!nat_detection_request_check_socket(owner->nat_det, socket, observer->node_id));
struct nat_check_status status = {0}; uint64_t end = get_time_tb() + 400000;
do { test_clock_poll(fixture.ua, 1000); assert(!nat_detection_get_status(owner->nat_det, socket, &status)); }
while (get_time_tb() < end && (!status.rounds || status.phase || status.cycling));
assert(socket->address_profile.mapping == ADDRESS_MAP_UNCHANGED && socket->address_profile.inbound == ADDRESS_IN_OPEN);
assert(status.helper == helper->node_id && status.helper_reselections);
nat_fixture_cleanup(&fixture);
DEBUG_INFO(DEBUG_CATEGORY_NAT, "alias fixture result source=%s helper=%016llx expected=%016llx reselections=%u remapped=%u",
alias_ip, (unsigned long long)status.helper, (unsigned long long)helper->node_id,
status.helper_reselections, fault.altered);
assert(status.helper == helper->node_id && status.helper_reselections && fault.altered);
fault.fixture = NULL; nat_fixture_cleanup(&fixture);
DEBUG_INFO(DEBUG_CATEGORY_NAT, "PASS NAT receiver rejects helper source=%s", alias_ip);
}

Loading…
Cancel
Save