Measure owned endpoint mapping and inbound reachability separately

This commit is contained in:
evgeny committed 2026-10-12 00:01:40 +02:00
1 parent b54307a0df
commit ff69552a61
51 files changed
+2620 -2483

No files matched your search

+7 -1
View File
@@ -159,7 +159,7 @@ link_state: 0=just init, 1=handshake, 2=reconnect, 3=connected.
- session_id: 32 бита, big-endian (должен совпасть с запросом клиента; если нет — клиент игнорирует)
- mtu_local: uint16_t локальный MTU сервера
- local_link_id: uint8_t идентификатор канала сервера
- remote_socket_id: uint8_t (копируется из запроса)
- remote_socket_id: uint8_t (ID серверного сокета; ID клиентского сокета известен серверу из запроса)
- only_local: uint8_t флаг сервера
- type: uint8_t тип сокета сервера
- peer_ipv4: 4 байта (IPv4 адрес клиента как его видит сервер — для NAT traversal)
@@ -176,6 +176,12 @@ link_state: 0=just init, 1=handshake, 2=reconnect, 3=connected.
[0x07] [flags(1)] [node_id(8 BE)] [nonce(8 BE)] [resp_data_len(2 BE)] [response_data...] [padding...]
[salt(SC_PUBKEY_ENC_SALT_SIZE)] [obfuscated_pubkey(SC_PUBKEY_SIZE)]
Ответ на PING. nonce копируется из запроса для сопоставления.
Nonce генерируется CSPRNG. Проверяются также ключ отправителя, source IP:port,
ID и поколение принимающего сокета. Флаг OBSERVE (0x04) запрашивает ответ
address(19) || tag(25): family(4/6), IP(16), port(2, BE), NAT2UDP!(8),
socket_id(1), generation(8, BE), round(8, BE).
ADDRESS_CHECK (0x08) — аутентифицированное входящее испытание endpoint.
Модель NAT и алгоритм владельца: doc/nat_detection.txt.
При получении init получатель пакета должен:
- reset ETCP_LINK с этим ip_port если он есть
+145
View File
@@ -0,0 +1,145 @@
Детектор свойств endpoint
========================
Владелец рабочего сокета единственный вычисляет и публикует его профиль.
Помощники возвращают наблюдения и выполняют прямые испытания. Профиль относится
к протоколу, семейству, фактическому локальному IP:port и поколению сокета.
PUBLIC/NAT/PRIVATE/LOCAL — политика конфигурации, а не результат измерения.
Модель: src/routing_layer/address_profile.h.
mapping: UNKNOWN, UNCHANGED, EIM, DEPENDENT.
inbound: UNKNOWN, OPEN, CONTACT_REQUIRED.
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 не меняется.
+12
View File
@@ -13,12 +13,24 @@
8-битный `ver` сохраняется как диагностический счётчик; свежесть определяется
только timestamp.
Профиль endpoint в SOCKMETA содержит независимые `mapping` и `inbound`,
`evidence_id` и `checked_at`. Последние относятся к измерению владельца,
а не к версии всего NODEINFO: повтор записи или изменение имени не продлевает
измерение. Новые поля профиля на проводе кодируются явными BE-байтами;
границы списков metadata/адресов/REALITY также входят в каноническую подпись.
Алгоритм и условия неопределённости описаны в `doc/nat_detection.txt`.
Таблица `node_snapshots` хранит полную сериализованную запись без группового
окружения. `node_addresses` остаётся проекцией для поиска и отображения, а не
источником для восстановления подписанной записи. При наличии сохранённого
snapshot загрузка использует его. Локальная публикация сохраняется до рассылки;
ошибка сохранения не разрешает публикацию новой версии.
В проекции адресов сохраняются `nat_mapping`, `nat_inbound`, `nat_evidence`,
`nat_checked`, `address_kind`. Уникальность определяется endpoint и видом адреса,
а не отображаемым классом NAT. Старый производный формат пересоздаётся;
bootstrap-загрузка без подписанного snapshot не подтверждает NAT-профиль.
Timestamp 0 обозначает bootstrap-сведения без подписи (конфиг, приглашение,
начальные адреса). Они могут помочь установить первое соединение, но не
заменяют принятую подписанную запись. По BGP принимаются подписанные записи