diff --git a/doc/nat_detection.txt b/doc/nat_detection.txt index 98b06057..1d41b70e 100644 --- a/doc/nat_detection.txt +++ b/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 проверяется на неизменность. diff --git a/doc/stun_nat.md b/doc/stun_nat.md index 98a35e95..3efbe4db 100644 --- a/doc/stun_nat.md +++ b/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`. ## Конфигурация diff --git a/tests/test_nat_detection_faults.c b/tests/test_nat_detection_faults.c index 46f892a3..49145dca 100644 --- a/tests/test_nat_detection_faults.c +++ b/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); }