You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

290 lines
27 KiB

Детектор свойств endpoint
========================
Владелец рабочего сокета единственный выбирает и публикует его профиль.
Помощники возвращают наблюдения и выполняют прямые испытания. Профиль относится
к протоколу, семейству, фактическому локальному IP:port и поколению сокета.
PUBLIC/NAT/PRIVATE/LOCAL — политика конфигурации, а не результат измерения.
source=ETCP означает измерение рабочего endpoint; source=STUN — резервную оценку
по его IP/netif через другой порт. Измеренный ETCP имеет приоритет до истечения TTL.
Модель: src/routing_layer/address_profile.h.
mapping: UNKNOWN, UNCHANGED, EIM, DEPENDENT.
inbound: UNKNOWN, OPEN, CONTACT_REQUIRED.
source: NONE, ETCP, STUN.
evidence_id: случайный 64-битный ID завершённого раунда владельца.
checked_at: время измерения владельца, Unix microseconds.
generation: случайный 64-битный ID жизненного цикла локального endpoint.
UNCHANGED означает совпадение наблюдаемых и локального адресов на испытанных
направлениях. Оно не доказывает свободный вход. EIM означает одинаковый внешний
endpoint на испытанных разных IP и портах; оно также не доказывает свободный вход.
DEPENDENT означает различие mapping на испытанных назначениях, без общего
внешнего адреса. Дальнейшее разделение ADM/APDM не используется для выбора NCD.
UDP-раунд
---------
1. Владелец A выбирает живого наблюдателя B с доступным native UDP endpoint.
Через проверяемый сокет S выполняет OBSERVE. Ответ содержит фактический
source IP:port, наблюдаемый B, и исходный тег S/generation/round.
2. Через уже установленный ETCP к B запрашивает поиск помощника C.
B выбирает C из своих работающих UDP-пиров и возвращает его ключ и endpoint.
Собственный подтверждённый NAT-профиль B/C не требуется: иначе bootstrap
зависел бы от уже завершённой детекции. Пригодность подтверждают реальные пробы.
3. Если внешний IP C совпадает с IP B, A исключает C и командует B искать
следующего помощника. Совпадающие Node ID и разные порты одного IP не дают
независимого IP-наблюдения. При отсутствии второго IP результат неполный.
4. Для входящего испытания выбирается C, с IP которого S ранее не контактировал.
Все команды C идут A -> B -> C. До первого испытания A не шлёт UDP к C.
C отправляет три аутентифицированные ADDRESS_CHECK к endpoint из наблюдения B.
A регистрирует фактический ingress на S; один отчёт о PONG не заменяет ingress.
5. После завершения первого испытания S выполняет OBSERVE к основному порту C,
затем к другому UDP-порту того же помощника. Вспомогательный порт принадлежит
наблюдателю, скрыт из 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 через 180 секунд.
Срок фактов — 300 секунд. Таймеры владельца используют monotonic timebase;
checked_at не обновляется повторным NODEINFO и не зависит от ответа сервера NTP.
При несогласованных часах получатель использует осторожные попытки, не отказ.
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-наблюдения данного владельца.
Размер wire, версия, enum, границы списка исключений и ненулевые поколения/раунды
проверяются до обработки. Числа новых сообщений — BE, IP — сетевые байты.
owner_key и helper_key имеют разные поля и никогда не меняют значение по роли.
На смене адреса, закрытии сокета, reset/DELETE транспорта и остановке ядра
отменяются собственные раунды и связанные helper jobs. Поздние ответы не
применяются к новому socket generation даже при повторном ID и IP:port.
Детектор останавливается до разборки групп, сокетов и соединений.
Публикация и NCD
---------------
Профиль публикуется только владельцем в общей подписанной записи NODEINFO.
Списки и их границы входят в подпись; mapping/inbound/evidence/checked_at
сохраняются и загружаются вместе. node_addresses — производная проекция:
nat_mapping, nat_inbound, nat_evidence, nat_checked, address_kind.
При изменении формата старый кэш адресов пересоздаётся; старый NAT_TYPE не
интерпретируется как новая модель. Подписанные snapshots и записи чата не удаляются.
Общий 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, неправильный
round, аутентичный PONG чужого ключа, старое поколение, отмена без callback.
test_nat_detection: разные socket ID, два рабочих UDP endpoint, TCP listener,
замена помощника с тем же IP, повтор по старому контакту, подпись/SQLite профиля,
закрытие S посреди раунда и повторный ID/порт. Утечки/таймеры/сокеты проверяются.
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.