docs: specify NAT v4 independent mapping reports and receiver-verified ingress
This commit is contained in:
1 parent
4fab849917
commit
7673316dc8
3 files changed
+147
-300
No files matched your search
+126
-290
@@ -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
@@ -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`.
|
||||||
|
|
||||||
## Конфигурация
|
## Конфигурация
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user