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

This commit is contained in:
evgeny committed 2026-10-17 16:19:47 +02:00
1 parent 4fab849917
commit 7673316dc8
3 files changed
+147 -300

No files matched your search

+126 -290
View File
@@ -1,315 +1,151 @@
Детектор свойств endpoint Детектор рабочего UDP endpoint
======================== ==============================
Владелец рабочего сокета единственный выбирает и публикует его профиль. Владелец сокета единственный выбирает и публикует его профиль. Он относится
Помощники возвращают наблюдения и выполняют прямые испытания. Профиль относится
к протоколу, семейству, фактическому локальному IP:port и поколению сокета. к протоколу, семейству, фактическому локальному IP:port и поколению сокета.
PUBLIC/NAT/PRIVATE/LOCAL — политика конфигурации, а не результат измерения. PUBLIC/NAT/PRIVATE/LOCAL — политика конфигурации, не измеренный NAT-тип.
source=ETCP означает измерение рабочего endpoint; source=STUN — резервную оценку
по его IP/netif через другой порт. Успешный ETCP имеет приоритет до явного сброса endpoint.
Модель: src/routing_layer/address_profile.h. Независимые факты в src/routing_layer/address_profile.h:
mapping: UNKNOWN, UNCHANGED, EIM, DEPENDENT. mapping: UNKNOWN, UNCHANGED, EIM, DEPENDENT;
inbound: UNKNOWN, OPEN, CONTACT_REQUIRED. inbound: UNKNOWN, OPEN, CONTACT_REQUIRED;
source: NONE, ETCP, STUN. source: ETCP — измерен рабочий endpoint.
round/generation: локальные ID раунда и жизненного цикла endpoint; в NODEINFO не входят.
UNCHANGED означает совпадение наблюдаемых и локального адресов на испытанных Практические классы: strict — стабильные разные mapping к разным IP;
направлениях. Оно не доказывает свободный вход. EIM означает одинаковый внешний eim open — одинаковый mapping и полученная чистая проба незнакомого входа;
endpoint на испытанных разных IP и портах; оно также не доказывает свободный вход. eim filtered — одинаковый mapping и вход, полученный только после контакта.
DEPENDENT означает различие mapping на испытанных назначениях, без общего UNCHANGED отличается от EIM отсутствием трансляции IP:port.
внешнего адреса. Дальнейшее разделение ADM/APDM не используется для выбора NCD. 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-раунд UDP-раунд
--------- ---------
1. Владелец A рассматривает узлы всех активных UTUN/CHAT-групп и выбирает одну группу 1. S отправляет B односторонний OBSERVE; через управление получает M_B.
с маршрутами к наблюдателю B и отдельному помощнику C. Следующие хопы управления 2. A просит B делегировать C входящее испытание на M_B. B разрешает делегацию
должны быть READY. Прямые ETCP/UDP-линки к B/C не обязательны; B нужен доступный UDP endpoint. только для действительного записанного наблюдения этого владельца и раунда.
Через проверяемый сокет S выполняет OBSERVE. Ответ содержит фактический 3. C отправляет начальную серию входящих проб, без автоматического UDP-ответа.
source IP:port, наблюдаемый B, и исходный тег S/generation/round/group. Затем через B измеряет mapping своего отдельного сокета; отчёт также идёт
2. A выбирает C по подписанному NODEINFO выбранной группы. Его рабочий UDP-сокет через router. Начальная серия могла уже подтвердить OPEN.
задаёт интерфейс/семейство; C использует два отдельных probe_only порта, 4. Незнакомость проверяет получатель S по фактическому source IP. IP B,
чтобы помощь другому узлу не открывала фильтр его собственного рабочего endpoint. собственный внешний IP S (hairpin) и прежний исходящий контакт не подходят:
C может находиться за NAT, независимо от политики PUBLIC/NAT и собственного профиля. A запрашивает другого помощника. Объявленный IP и Node ID не заменяют проверку.
3. Управление RUN/RESULT идёт A -> B -> C через router в одной группе с подписью 5. Если начального входа нет, C выполняет полную серию PRE до исходящего
и шифрованием. C сначала отправляет ADDRESS_CHECK с primary-порта к endpoint A, контакта S с C. После отчёта выдерживается окно доставки последней пробы:
реально наблюдавшемуся B. Аутентичный ingress на S до исходящего контакта подтверждает OPEN. управляющее сообщение может обогнать UDP.
PONG возвращает наблюдаемый внешний endpoint C. При отсутствии PONG C узнаёт mapping 6. S фиксирует границу контакта и отправляет OBSERVE на C. Через управление
своего primary-порта через B, после чего выполняется контролируемый PRE из трёх проб. получает M_C. Source полученной аутентичной входящей пробы имеет приоритет
4. Если внешний IP C совпадает с IP B или S ранее контактировал с C, A пробует перед предварительно объявленным endpoint помощника.
другие пары и группы. Совпадающие Node ID и разные порты одного IP не дают 7. При M_C = M_B, полном чистом PRE без входа и исправном контроле B выполняется
независимого IP-наблюдения. При отсутствии второго IP результат неполный. POST: серия с того же endpoint C на исходный M_B. Source POST должен совпасть
При выборе пары сначала сохраняется возможность свежего помощника; ранее контактировавшая с endpoint, к которому обращался S. История проверяется до намеренного контакта,
пара используется для mapping после исчерпания свежих. Отчёт о PONG не заменяет ingress на S. включая посторонние отправки во время PRE. Смена source не подтверждает filtering.
5. После первого испытания S выполняет OBSERVE к primary C. При различии mapping 8. S повторяет OBSERVE к B и C. Изменение наблюдений одного направления
с B уже известен DEPENDENT. Иначе C отдельно измеряет внешний endpoint второго порта инвалидирует профиль и даёт address_changed. Этапы разных состояний не смешиваются.
через A; если фильтр A мешает — через B. S выполняет OBSERVE к этому endpoint. 9. Стабильные M_B != M_C дают DEPENDENT; равенство — UNCHANGED/EIM.
Оба порта C скрыты из NODEINFO и исключены из NCD. Проверяемый S не меняется. Чистый вход до контакта даёт OPEN. Полный PRE без входа, успешный POST
6. Если первый ingress отсутствовал, C здоров и mapping к C совпал с mapping к B, и свежий прямой контроль B дают CONTACT_REQUIRED. Незавершённый ingress
после исходящего контакта выполняется второе входящее испытание от того же C. оставляет inbound UNKNOWN, сохраняя измеренный mapping.
Контролируемый переход до/после контакта даёт CONTACT_REQUIRED.
7. S повторяет исходное наблюдение у B. Изменение этого mapping во время раунда
даёт UNKNOWN/address_changed и инвалидирует прежний общий внешний адрес.
8. При совпадении наблюдений B/C/C-alt выбирается UNCHANGED или EIM; различие
назначений даёт DEPENDENT. Первый чистый ingress даёт OPEN; подтверждённый
переход после контакта при неизменном endpoint — CONTACT_REQUIRED.
Если независимых от истории C больше нет, уже контактировавший C может измерить Новый помощник получает новый round/token. Поздние пакеты предыдущей попытки
mapping. Входящая часть остаётся UNKNOWN/contact_history. Отказ проверки одного не используются. После исчерпания свежих IP допускается mapping-раунд с прежним
facet не превращается в ложный STRICT. Потери, ошибка отправки, отсутствие помощником, без обновления незнакомой входящей доступности.
помощника, разрыв управления и неправильный ответ имеют отдельную причину.
Контакты и повторы История, время и отмена
----------------- ----------------------
История IP назначения учитывает INIT, DATA, ACK, keepalive, PING/PONG и другие История по IP учитывает INIT, DATA, ACK, keepalive, PING/PONG и другие отправки
отправки с S в двух общих точках UDP-send. Любая неожиданная отправка к C до в двух общих точках UDP-send. Проверяется на момент первого ingress, не только
получения первого ingress делает входящее испытание загрязнённым. на старте. Случайная отправка к C во время PRE делает опыт загрязнённым.
Для POST сохраняется граница истории до намеренного OBSERVE к C.
История записывается в nat_contacts общей SQLite один раз на новый IP. Ключ — nat_contacts общей SQLite хранит семейство и фактический локальный IP:port
семейство и фактический локальный IP:port. Перезапуск на том же endpoint её не и один элемент на IP назначения. Перезапуск на том же endpoint историю не очищает.
очищает. Максимум 256 IP на endpoint; переполнение/ошибка сохранения отмечаются Максимум 256 IP; переполнение, повреждение и ошибка записи отмечают её неполной.
постоянным маркером неполной истории. Неизвестная история запрещает вывод OPEN Неполная история запрещает доказательство незнакомости. История описывает только
по испытанию, которое может использовать старое разрешение фильтра. наблюдаемый приложением трафик: удаление БД или чужое использование tuple
не доказывает отсутствия состояния NAT.
История характеризует наблюдаемый приложением трафик. Удаление БД, использование Начальный раунд имеет короткий приоритет перед фоновыми NCD-пробами.
того же tuple другим процессом или изменение NAT вне наблюдаемого раунда нельзя Уже известные IP не блокируются; работающий транспорт сохраняется.
превратить в доказательство полного отсутствия прежнего состояния. Результаты Все операции выполняются в uasync-потоке. Один активный раунд на S.
остаются подсказками для попыток, подтверждение связи даёт handshake с нужным ключом. Ожидание пробы — max(1 секунда, 4*RTT к B), серия — три отправки.
Цикл перебора ограничен 30 секундами. Истечение бюджета не назначает NAT-тип.
Повтор неполного EIM/inbound — через 30 секунд; новый подходящий endpoint
может разбудить раньше. Полный профиль обновляется через 30 минут.
Неудачная перепроверка сохраняет прежний успех. Частичный результат того же
mapping/endpoint сохраняет прежний inbound, но не переносит его с другого endpoint.
Один активный раунд на S. Таймаут пробы — max(1 секунда, 4*RTT), не более 3 секунд; Смена адреса сбрасывает профиль и отменяет раунд. Закрытие сокета, удаление группы
три попытки, бюджет раунда 30 секунд. До первого результата повтор после ошибки и остановка ядра отменяют UDP-пробы, heartbeat и helper jobs до закрытия транспорта.
через 30 секунд или после появления подходящего транспорта; штатный refresh через 30 минут. Generation и токены исключают применение позднего ответа к новому socket ID/fd.
Успешный профиль не протухает по таймеру. Ошибка перепроверки сохраняет прежний результат Неудачные реально отправленные native INIT к подписанному OPEN назначают recheck;
и редкий период; неизвестный inbound при том же mapping/endpoint сохраняет прежний ETCP inbound. CONTACT допускает его после исчерпания reverse. Ошибки объединяются, минимум
Свойства разных источников не объединяются в один ETCP-профиль. Смена адреса отменяет пять минут между такими перепроверками рабочего сокета.
старый раунд, сбрасывает профиль и назначает скорую проверку. Таймеры используют monotonic timebase.
После таймаута NCD перепроверка назначается только для реально отправленных native UDP INIT Wire v4
к OPEN endpoint из текущего подписанного NODEINFO. CONTACT допускает такой повод после -------
исчерпания цикла CM вместе с reverse, а UNKNOWN/приватный адрес и SOCKS5 его не дают.
Активный раунд не перезапускается; повторные ошибки объединяются, минимум пять минут
между такими перепроверками одного рабочего сокета. Последний успех сохраняется.
Начальные измерения имеют короткий приоритет перед фоновыми проверками; завершённый
частичный результат освобождает его, чтобы соседи могли выполнить свои наблюдения.
Если выбранная группа ещё доставляет управление пиру, ему передаётся подписанный
зашифрованный RECHECK размером 23 байта (команда/версия/socket ID/endpoint).
Пир проверяет членство отправителя и совпадение endpoint со своим текущим сокетом;
тот же лимит действует на удалённые запросы. ID раунда и время в NODEINFO не возвращаются.
TCP и прокси Команды: NAT_RUN, NAT_RESULT, NAT_RECHECK, NAT_OBSERVATION.
------------- SOURCE/group берутся из проверенного router-заголовка, не из физического next hop.
Разрешены только участники выбранной группы; управление подписано и зашифровано.
TCP inbound проверяется отдельным STCP handshake к объявленному настоящему Тег OBSERVE: "NAT4UDP!", socket ID, generation, round, group.
listener с проверкой ключа владельца. Его target должен присутствовать в Односторонняя проба добавляет этап (1 байт) и token (8 байт).
подписанном snapshot. Внешний порт исходящего TCP-соединения не рекламируется OBSERVATION проверяется по ожидаемому наблюдателю, socket ID/generation,
как listener. Без публичного кандидата профиль остаётся UNKNOWN/no_listener. group, round, этапу и token. Каждый повторный UDP-запрос получает новый token.
UDP-факты не переносятся на TCP. Входящий TCP source port не определяет профиль Ingress дополнительно проверяет ключ C и фактический source endpoint.
listener удалённого клиента. SOCKS5 UDP не классифицируется как native endpoint. Wire содержит только uint8_t; числа BE, адреса — сетевые байты.
Корреляция и владение ETCP_PING_FLAG_NO_REPLY разрешён для OBSERVE/ADDRESS_CHECK с полным NAT-тегом.
-------------------- Он запрещает автоматический PONG: приём пробы не создаёт исходящий контакт.
Обычные транспортные PING/PONG сохраняются. v3 несовместим с v4:
участвующие в измерении узлы обновляются вместе.
Транспортный PONG сопоставляется по криптографически случайному nonce, ожидаемому Публикация, STUN и TCP
ключу, source IP:port, ID и поколению принимающего сокета. Входящее испытание ---------------------
дополнительно проверяет владельца, round, step и token. Перед callback PING
удаляется из pending. Отмена не вызывает callback и не интерпретирует void* arg
как контекст другого типа. Произвольный выбор первого IPv4/TCP-сокета запрещён.
Broker принимает RUN только для записанного им UDP-наблюдения данного владельца. Владелец публикует профиль в подписанном NODEINFO. Универсальный внешний
Участники и ключи берутся из подписанного реестра выбранной группы. SOURCE читается endpoint рекламируется после EIM; UNCHANGED использует собственный адрес.
из проверенного router-заголовка, не из ETCP next hop. Управление занимает 136 байт, DEPENDENT не рекламирует универсальный NAT endpoint. NCD использует профиль
не содержит списков исключений и копий ключей. Размер, версия, enum и ненулевые для выбора попыток; работоспособность конкретной связи подтверждает handshake.
group/generation/round/token проверяются до обработки. Числа BE, IP — сетевые байты.
На смене адреса, закрытии сокета, reset/DELETE транспорта и остановке ядра STUN другого порта остаётся диагностикой IP/netif и никогда не назначает
отменяются собственные раунды и связанные helper jobs. Поздние ответы не профиль рабочему S. Результаты доступны через stun_nat API.
применяются к новому socket generation даже при повторном ID и IP:port. UDP-профиль не переносится на TCP. TCP inbound проверяется STCP handshake
Между попытками разных пар локальный статус cycling отличает паузу от завершения измерения. к объявленному listener с проверкой ключа. Без публичного кандидата TCP
Удаление выбранной группы отменяет её раунды, jobs и наблюдения до закрытия router-каналов; остаётся UNKNOWN/no_listener. SOCKS UDP не классифицируется как native endpoint.
другие группы и последний успешный профиль сохраняются. Детектор останавливается
до разборки групп, сокетов и соединений.
Публикация и 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.
Проверки Проверки
-------- --------
test_route_ping: реальные UDP-наблюдения, эталонные байты IPv4/IPv6, неправильный test_route_ping: UDP-наблюдения, IPv4/IPv6 encoding, чужой ключ, неверный round,
round, аутентичный PONG чужого ключа, старое поколение, отмена без callback. старое поколение и отмена без callback.
test_nat_detection: разные socket ID, два рабочих UDP endpoint, TCP listener, test_nat_detection: несколько endpoint, одинаковые IP узлов, подпись/SQLite,
замена помощника с тем же IP, повтор по старому контакту, подпись/SQLite профиля, TCP listener, транзитная CHAT-группа, recheck и teardown.
закрытие S посреди раунда и повторный ID/порт. Утечки/таймеры/сокеты проверяются. test_nat_detection_faults (Linux): реальные UDP/router-пакеты, потерянные PONG,
Недостаточная UTUN-группа и достаточная CHAT-группа проверяются отдельно с TCP next hop PRE/POST, поздний PRE после контакта, посторонняя отправка, пропавший отчёт,
и отсутствием прямого транспорта A-C. Неудачные native INIT проверяют отсутствие разные/нестабильные mapping, совпавший IP B, hairpin и перебор IP наблюдателя.
лишних перепроверок для UNKNOWN, ожидание reverse для CONTACT и пятиминутный лимит. Только часы тестового poll ускоряются; сокеты, шифрование и протокол настоящие.
test_sock_match: независимость политики от фактов, UNKNOWN, CONTACT, давность, tests/stream_netns/run.py --nat-only: 16 ядер, 6 настоящих NAT-шлюзов,
частные адреса и разрешённая попытка hairpin; профили не протухают по времени. EIM open/filtered/strict. Главный network namespace проверяется на неизменность.
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.
+5 -5
View File
@@ -5,11 +5,11 @@
Для каждой группы создаётся один собственный неблокирующий UDP-сокет с тем же локальным IP, Для каждой группы создаётся один собственный неблокирующий UDP-сокет с тем же локальным IP,
привязкой к устройству и Linux SO_MARK / FreeBSD FIB. Тестовый порт выбирается случайно из 49152–65535. привязкой к устройству и Linux SO_MARK / FreeBSD FIB. Тестовый порт выбирается случайно из 49152–65535.
Модуль сам регистрирует чтение в uasync, принимает STUN и закрывает свой сокет после измерения. Модуль сам регистрирует чтение в uasync, принимает STUN и закрывает свой сокет после измерения.
STUN не меняет исходный ETCP fd, его порт и обработчики. NAT_DETECTION использует его STUN не меняет исходный ETCP fd, его порт и обработчики. Результат остаётся независимой
оценку типа при NO_OBSERVER/NO_HELPER, помечает source=STUN и публикует подписанный профиль. диагностикой IP/netif через `stun_nat` API. Он не назначает mapping/inbound рабочему сокету
Действующий результат source=ETCP имеет приоритет, включая частичный. Мapped endpoint даже при NO_OBSERVER/NO_HELPER: поведение NAT на другом порту может отличаться.
тестового порта не переносится в nat_addr или NODEINFO рабочего сокета. Ни профиль, ни mapped endpoint тестового порта не переносятся в `nat_addr` или NODEINFO
Поведение NAT на рабочем порту может отличаться; source показывает происхождение оценки. рабочего сокета. Детектор рабочего endpoint описан в `doc/nat_detection.txt`.
## Конфигурация ## Конфигурация
+16 -5
View File
@@ -8,8 +8,8 @@ enum fault_mode { FAULT_NONE, FAULT_NO_PONG, FAULT_FILTERED, FAULT_LATE_PRE, FAU
static struct { static struct {
struct nat_fixture *fixture; struct nat_fixture *fixture;
struct UTUN_INSTANCE *owner, *observer, *helper; struct UTUN_INSTANCE *owner, *observer, *helper, *alias;
struct ETCP_SOCKET *socket; struct ETCP_SOCKET *socket, *alias_source;
enum fault_mode mode; enum fault_mode mode;
unsigned dropped, altered, direct_received, observations; unsigned dropped, altered, direct_received, observations;
int contacted, injected; 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)); struct sockaddr_storage destination = {0}; assert(target_length <= sizeof(destination));
memcpy(&destination, target, target_length); memcpy(&destination, target, target_length);
int to_owner = route_ping_addr_equal(&destination, &fault.socket->local_addr, 1); 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) { if ((fault.mode == FAULT_NO_PONG || fault.mode == FAULT_NO_ANCHOR) && to_owner && plain[3] == ETCP_PONG) {
fault.dropped++; return length; 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 *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 *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); 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(observer->nat_det, 0); nat_detection_set_automatic(alias->nat_det, 0);
nat_detection_set_automatic(helper->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, 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); 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"); 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)); 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; 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)); } 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)); 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(socket->address_profile.mapping == ADDRESS_MAP_UNCHANGED && socket->address_profile.inbound == ADDRESS_IN_OPEN);
assert(status.helper == helper->node_id && status.helper_reselections); DEBUG_INFO(DEBUG_CATEGORY_NAT, "alias fixture result source=%s helper=%016llx expected=%016llx reselections=%u remapped=%u",
nat_fixture_cleanup(&fixture); 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); DEBUG_INFO(DEBUG_CATEGORY_NAT, "PASS NAT receiver rejects helper source=%s", alias_ip);
} }