From 2ba011d3fda5a568dcd7db209eef56957c15f459 Mon Sep 17 00:00:00 2001 From: evgeny Date: Mon, 12 Oct 2026 20:07:49 +0200 Subject: [PATCH] Document grouped control, isolated helper ports and ingress correlation --- doc/nat_detection.txt | 47 ++++++++++++++++++------------- src/routing_layer/nat_detection.c | 9 ++++-- 2 files changed, 34 insertions(+), 22 deletions(-) diff --git a/doc/nat_detection.txt b/doc/nat_detection.txt index 0d0237af..a61013a5 100644 --- a/doc/nat_detection.txt +++ b/doc/nat_detection.txt @@ -23,23 +23,29 @@ DEPENDENT означает различие mapping на испытанных н UDP-раунд --------- -1. Владелец A выбирает живого наблюдателя B с доступным native UDP endpoint. +1. Владелец A рассматривает узлы всех активных UTUN/CHAT-групп и выбирает одну группу + с маршрутами к наблюдателю B и отдельному помощнику C. Следующие хопы управления + должны быть READY. Прямые ETCP/UDP-линки к B/C не обязательны; B нужен доступный 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 не дают + 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 результат неполный. -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 не меняется. + При выборе пары сначала сохраняется возможность свежего помощника; ранее контактировавшая + пара используется для 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. @@ -100,14 +106,17 @@ listener удалённого клиента. SOCKS5 UDP не классифиц как контекст другого типа. Произвольный выбор первого IPv4/TCP-сокета запрещён. Broker принимает RUN только для записанного им UDP-наблюдения данного владельца. -Размер wire, версия, enum, границы списка исключений и ненулевые поколения/раунды -проверяются до обработки. Числа новых сообщений — BE, IP — сетевые байты. -owner_key и helper_key имеют разные поля и никогда не меняют значение по роли. +Участники и ключи берутся из подписанного реестра выбранной группы. SOURCE читается +из проверенного router-заголовка, не из ETCP next hop. Управление занимает 136 байт, +не содержит списков исключений и копий ключей. Размер, версия, enum и ненулевые +group/generation/round/token проверяются до обработки. Числа BE, IP — сетевые байты. На смене адреса, закрытии сокета, reset/DELETE транспорта и остановке ядра отменяются собственные раунды и связанные helper jobs. Поздние ответы не применяются к новому socket generation даже при повторном ID и IP:port. -Детектор останавливается до разборки групп, сокетов и соединений. +Удаление выбранной группы отменяет её раунды, jobs и наблюдения до закрытия router-каналов; +другие группы и последний успешный профиль сохраняются. Детектор останавливается +до разборки групп, сокетов и соединений. Публикация и NCD --------------- diff --git a/src/routing_layer/nat_detection.c b/src/routing_layer/nat_detection.c index a8ad3c93..c49af52e 100644 --- a/src/routing_layer/nat_detection.c +++ b/src/routing_layer/nat_detection.c @@ -1,6 +1,6 @@ -/* NAT-детектор владельца endpoint. Один раунд на рабочий сокет: - * S->B observation, B->C ingress(S), S->C/C-alt observations, S->B repeat. - * Команды к C проходят через B: контроль не открывает фильтр S->C. */ +/* Владелец выбирает пару из всех групп; управление A->B->C идёт через router. + * S->B observation, C->S controlled ingress, S->C/C-alt observations, S->B repeat. + * Два отдельных порта C и короткий приоритет холодного owner сохраняют независимость ingress. */ #include "nat_detection.h" #include @@ -735,6 +735,9 @@ static void helper_remove(struct nat_probe *probe) { static void helper_finish(struct nat_probe *probe, uint8_t reason) { probe->message.command = NAT_RESULT; probe->message.reason = reason; + if (reason) DEBUG_WARN(DEBUG_CATEGORY_NAT, "NAT helper failed group=%016llx owner=%016llx round=%016llx step=%u reason=%s", + (unsigned long long)probe->group_id, (unsigned long long)read64(probe->message.owner), + (unsigned long long)read64(probe->message.round), probe->message.step, nat_check_reason_name(reason)); DEBUG_DEBUG(DEBUG_CATEGORY_NAT, "NAT helper done owner=%016llx round=%016llx step=%u sent=%u received=%u errors=%u reason=%s", (unsigned long long)read64(probe->message.owner), (unsigned long long)read64(probe->message.round), probe->message.step, probe->message.sent, probe->message.received, probe->message.errors, nat_check_reason_name(reason));