51 changed files with 2621 additions and 2484 deletions
@ -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 не меняется. |
||||||
@ -0,0 +1,51 @@ |
|||||||
|
/* Профиль конкретного endpoint: изменение адреса и доступность входа независимы.
|
||||||
|
* checked_at — время измерения владельца в Unix microseconds, не версия NODEINFO. |
||||||
|
* Профиль помогает выбирать попытки; работоспособность подтверждает транспорт. */ |
||||||
|
#ifndef ADDRESS_PROFILE_H |
||||||
|
#define ADDRESS_PROFILE_H |
||||||
|
|
||||||
|
#include <stdint.h> |
||||||
|
|
||||||
|
enum address_mapping { |
||||||
|
ADDRESS_MAP_UNKNOWN, |
||||||
|
ADDRESS_MAP_UNCHANGED, |
||||||
|
ADDRESS_MAP_EIM, |
||||||
|
ADDRESS_MAP_DEPENDENT |
||||||
|
}; |
||||||
|
|
||||||
|
enum address_inbound { |
||||||
|
ADDRESS_IN_UNKNOWN, |
||||||
|
ADDRESS_IN_OPEN, |
||||||
|
ADDRESS_IN_CONTACT |
||||||
|
}; |
||||||
|
|
||||||
|
#define ADDRESS_PROFILE_TTL_US UINT64_C(300000000) |
||||||
|
|
||||||
|
struct address_profile { |
||||||
|
uint8_t mapping; |
||||||
|
uint8_t inbound; |
||||||
|
uint64_t evidence_id; |
||||||
|
uint64_t checked_at; |
||||||
|
}; |
||||||
|
|
||||||
|
static inline const char *address_mapping_name(uint8_t value) { |
||||||
|
static const char *const names[] = {"UNKNOWN", "UNCHANGED", "EIM", "DEPENDENT"}; |
||||||
|
return value <= ADDRESS_MAP_DEPENDENT ? names[value] : "INVALID"; |
||||||
|
} |
||||||
|
|
||||||
|
static inline const char *address_inbound_name(uint8_t value) { |
||||||
|
static const char *const names[] = {"UNKNOWN", "OPEN", "CONTACT_REQUIRED"}; |
||||||
|
return value <= ADDRESS_IN_CONTACT ? names[value] : "INVALID"; |
||||||
|
} |
||||||
|
|
||||||
|
static inline int address_profile_valid(const struct address_profile *profile) { |
||||||
|
return profile && profile->mapping <= ADDRESS_MAP_DEPENDENT && profile->inbound <= ADDRESS_IN_CONTACT && |
||||||
|
((!profile->mapping && !profile->inbound) || (profile->evidence_id && profile->checked_at)); |
||||||
|
} |
||||||
|
|
||||||
|
static inline int address_profile_fresh(const struct address_profile *profile, uint64_t now_us) { |
||||||
|
return profile && profile->evidence_id && profile->checked_at && now_us >= profile->checked_at && |
||||||
|
now_us - profile->checked_at <= ADDRESS_PROFILE_TTL_US; |
||||||
|
} |
||||||
|
|
||||||
|
#endif |
||||||
File diff suppressed because it is too large
Load Diff
@ -1,161 +1,61 @@ |
|||||||
/**
|
/* Владелец измеряет собственный endpoint. Пиры передают наблюдения и выполняют
|
||||||
* @file nat_detection.h |
* прямые пробы; управление проверяющим идёт через явно выбранного посредника. |
||||||
* @brief NAT-детекция через третьего пира (STUN-like ping). |
* Все операции и callbacks выполняются в uasync-потоке ядра. */ |
||||||
* |
|
||||||
* Самодостаточный модуль на своём ETCP_ID (0x02): |
|
||||||
* сам биндится, сам подписывается на conn_up/conn_down, |
|
||||||
* ищет третий узел прямо в instance->connections, |
|
||||||
* обновляет ETCP_SOCKET/loc_node и broadcast-ит NODEINFO. |
|
||||||
*/ |
|
||||||
#ifndef NAT_DETECTION_H |
#ifndef NAT_DETECTION_H |
||||||
#define NAT_DETECTION_H |
#define NAT_DETECTION_H |
||||||
|
|
||||||
|
#include <stddef.h> |
||||||
|
#include <stdint.h> |
||||||
|
#include "../lib/socket_compat.h" |
||||||
|
|
||||||
#ifdef __cplusplus |
#ifdef __cplusplus |
||||||
extern "C" { |
extern "C" { |
||||||
#endif |
#endif |
||||||
|
|
||||||
#include <stdint.h> |
|
||||||
#include <stddef.h> |
|
||||||
#include "secure_channel.h" |
|
||||||
|
|
||||||
struct UTUN_INSTANCE; |
struct UTUN_INSTANCE; |
||||||
struct TOPO_GROUP; |
|
||||||
struct ETCP_CONN; |
struct ETCP_CONN; |
||||||
struct ETCP_LINK; |
struct ETCP_LINK; |
||||||
struct route_ping_pending; |
struct ETCP_SOCKET; |
||||||
|
struct NAT_DETECTION; |
||||||
/* ====== Sub-команды протокола ====== */ |
|
||||||
|
|
||||||
#define NATDET_SUBCMD_PING_REQ 0x07 // сервер → третий узел: "пропингуй IP:port"
|
|
||||||
#define NATDET_SUBCMD_PING_RESP 0x08 // третий узел → сервер: результат пинга
|
|
||||||
#define NATDET_SUBCMD_NAT_INFO 0x09 // сервер → клиент: "твой NAT — EIM/STRICT/DIRECT"
|
|
||||||
#define NATDET_SUBCMD_NAT_CHECK_REQ 0x0A // клиент → сервер: "проверь мой NAT, вот мой interface_addr"
|
|
||||||
|
|
||||||
/* ====== Wire-формат сообщений ====== */ |
|
||||||
|
|
||||||
/** Сервер → третий узел: запрос серии ETCP-пингов на target_ip:target_port */ |
|
||||||
struct NATDET_PING_REQ { |
|
||||||
uint8_t cmd; // ETCP_ID_NAT_DETECTION
|
|
||||||
uint8_t subcmd; // NATDET_SUBCMD_PING_REQ
|
|
||||||
uint16_t reserved; |
|
||||||
uint32_t request_id; // для корреляции запрос/ответ
|
|
||||||
uint8_t count; // число пингов в серии
|
|
||||||
uint8_t socket_id; // сокет для отправки (на третьем узле)
|
|
||||||
uint16_t interval_ms; // интервал между пингами
|
|
||||||
uint16_t timeout_ms; // таймаут одного пинга
|
|
||||||
uint8_t target_ipv4[4]; // IP цели (network byte order)
|
|
||||||
uint16_t target_port; // порт цели (network byte order)
|
|
||||||
uint8_t pubkey[SC_PUBKEY_SIZE]; // опционально: pubkey цели для шифрования пинга
|
|
||||||
} __attribute__((packed)); |
|
||||||
|
|
||||||
/** Третий узел → сервер: агрегированная статистика серии пингов */ |
enum nat_check_reason { |
||||||
struct NATDET_PING_RESP { |
NAT_REASON_NONE, NAT_REASON_NO_OBSERVER, NAT_REASON_NO_HELPER, NAT_REASON_CONTROL_LOST, |
||||||
uint8_t cmd; |
NAT_REASON_SEND_ERROR, NAT_REASON_TIMEOUT, NAT_REASON_ADDRESS_CHANGED, NAT_REASON_CONTACT_HISTORY, |
||||||
uint8_t subcmd; // NATDET_SUBCMD_PING_RESP
|
NAT_REASON_PROXY, NAT_REASON_NO_LISTENER, NAT_REASON_CANCELLED, NAT_REASON_INVALID_OBSERVATION, NAT_REASON_DEPENDENT_MAPPING |
||||||
uint16_t reserved; |
|
||||||
uint32_t request_id; |
|
||||||
uint8_t count_sent; // всего отправлено пингов
|
|
||||||
uint8_t count_ok; // успешно (получен PONG)
|
|
||||||
uint16_t avg_rtt; // средний RTT в 0.1ms
|
|
||||||
} __attribute__((packed)); |
|
||||||
|
|
||||||
/** Сервер → клиент: результат определения типа NAT */ |
|
||||||
struct NATDET_MSG_NAT_INFO { |
|
||||||
uint8_t cmd; |
|
||||||
uint8_t subcmd; // NATDET_SUBCMD_NAT_INFO
|
|
||||||
uint8_t socket_id; // ID сокета клиента
|
|
||||||
uint8_t nat_ip[4]; // NAT-адрес клиента (network byte order)
|
|
||||||
uint16_t nat_port; // NAT-порт клиента (network byte order)
|
|
||||||
uint8_t nat_type; // NAT_TYPE_EIM / NAT_TYPE_STRICT / NAT_TYPE_DIRECT
|
|
||||||
} __attribute__((packed)); |
|
||||||
|
|
||||||
/** Клиент → сервер: запрос на проверку NAT для указанного сокета */ |
|
||||||
struct NATDET_MSG_NAT_CHECK_REQ { |
|
||||||
uint8_t cmd; |
|
||||||
uint8_t subcmd; // NATDET_SUBCMD_NAT_CHECK_REQ
|
|
||||||
uint8_t socket_id; // ID сокета который надо проверить
|
|
||||||
uint32_t interface_ip; // IP клиента на интерфейсе (network byte order)
|
|
||||||
uint16_t interface_port; // порт клиента на интерфейсе (network byte order)
|
|
||||||
} __attribute__((packed)); |
|
||||||
|
|
||||||
/* ====== Callback для завершения пинга ====== */ |
|
||||||
|
|
||||||
typedef void (*route_ping_callback_t)(int success, uint16_t avg_rtt, |
|
||||||
uint8_t count_sent, uint8_t count_ok, void* arg); |
|
||||||
|
|
||||||
/* ====== Контекст NAT-детекции (один на UTUN_INSTANCE) ====== */ |
|
||||||
|
|
||||||
struct NAT_DETECTION { |
|
||||||
struct UTUN_INSTANCE* inst; |
|
||||||
struct route_ping_pending* ping_pending; // список ожидающих PING_RESP ответов
|
|
||||||
uint32_t next_ping_req_id; // монотонный счётчик request_id
|
|
||||||
uint8_t allow_nat_check_local; // разрешить чек для loopback (тесты)
|
|
||||||
}; |
}; |
||||||
|
|
||||||
/** Аргумент, передаваемый в callback nat_link_check_cb */ |
struct nat_check_status { |
||||||
struct nat_check_arg { |
uint64_t generation, round; |
||||||
struct ETCP_LINK* link; |
uint64_t observer, helper; |
||||||
uint32_t nat_ip; |
uint32_t rounds, helper_reselections; |
||||||
uint16_t nat_port; |
uint8_t phase, reason, history_complete; |
||||||
}; |
}; |
||||||
|
|
||||||
/* ==================== ПУБЛИЧНОЕ API ==================== */ |
struct NAT_DETECTION *nat_detection_create(struct UTUN_INSTANCE *instance); |
||||||
|
void nat_detection_destroy(struct NAT_DETECTION *detector); |
||||||
/* -------- Жизненный цикл -------- */ |
void nat_detection_link_ready(struct NAT_DETECTION *detector, struct ETCP_LINK *link); |
||||||
|
void nat_detection_cancel_for_conn(struct NAT_DETECTION *detector, struct ETCP_CONN *connection); |
||||||
/** Создать модуль: etcp_bind на 0x02, подписка на new_conn (on_up + on_down). */ |
void nat_detection_socket_removed(struct NAT_DETECTION *detector, struct ETCP_SOCKET *socket); |
||||||
struct NAT_DETECTION* nat_detection_create(struct UTUN_INSTANCE* inst); |
void nat_detection_note_send(struct NAT_DETECTION *detector, struct ETCP_SOCKET *socket, |
||||||
|
const struct sockaddr_storage *destination); |
||||||
/** Уничтожить: отписаться от new_conn, etcp_unbind, очистить pending пинги.
|
void nat_detection_observed_by_peer(struct NAT_DETECTION *detector, struct ETCP_LINK *link, |
||||||
* Вызывается из topo_groups_destroy(). */ |
const struct sockaddr_storage *observed); |
||||||
|
void nat_detection_record_observation(struct NAT_DETECTION *detector, struct ETCP_SOCKET *socket, |
||||||
void nat_detection_destroy(struct NAT_DETECTION* nd); |
const uint8_t peer_key[32], const struct sockaddr_storage *source, |
||||||
|
const uint8_t *tag, size_t length); |
||||||
/* -------- Служебные: вызываются другими модулями в ответ на события ETCP -------- */ |
void nat_detection_receive_ingress(struct NAT_DETECTION *detector, struct ETCP_SOCKET *socket, |
||||||
|
const uint8_t peer_key[32], const struct sockaddr_storage *source, |
||||||
/** Линк поднялся (INIT handshake завершён) — запустить для него NAT-чек.
|
const uint8_t *tag, size_t length); |
||||||
* Вызывается из etcp_connections: handle_init_response_server(). */ |
void nat_detection_request_check_all(struct NAT_DETECTION *detector); |
||||||
|
int nat_detection_request_check_socket(struct NAT_DETECTION *detector, struct ETCP_SOCKET *socket, uint64_t observer); |
||||||
void nat_detection_link_ready(struct NAT_DETECTION* nd, struct ETCP_LINK* link); |
void nat_detection_set_allow_local(struct NAT_DETECTION *detector, int allow); |
||||||
|
/* При ручном режиме помощники продолжают работать, раунды запускает request_check_all. */ |
||||||
/** Отменить все pending NAT-пинги для соединения и сбросить nat_check_status.
|
void nat_detection_set_automatic(struct NAT_DETECTION *detector, int automatic); |
||||||
* Вызывается из etcp.c при реините и etcp_connections.c при смене адреса линка. */ |
int nat_detection_get_status(struct NAT_DETECTION *detector, struct ETCP_SOCKET *socket, struct nat_check_status *status); |
||||||
|
const char *nat_check_reason_name(uint8_t reason); |
||||||
void nat_detection_cancel_for_conn(struct NAT_DETECTION* nd, struct ETCP_CONN* conn); |
|
||||||
|
|
||||||
/** Отправить клиенту NAT_INFO — результат DIRECT detection (публичный IP без пинга).
|
|
||||||
* Вызывается из etcp_connections: handle_init_response_server(). */ |
|
||||||
|
|
||||||
void nat_detection_send_nat_info(struct NAT_DETECTION* nd, struct ETCP_CONN* to_conn, |
|
||||||
uint8_t socket_id, uint32_t nat_ip, uint16_t nat_port, |
|
||||||
uint8_t nat_type); |
|
||||||
|
|
||||||
/* -------- Control API -------- */ |
|
||||||
|
|
||||||
/** Принудительная перепроверка NAT для всех линков (etcpmon команда "nat").
|
|
||||||
* Вызывается из control_server. */ |
|
||||||
|
|
||||||
void nat_detection_request_check_all(struct NAT_DETECTION* nd); |
|
||||||
|
|
||||||
/* -------- Тестовые хелперы -------- */ |
|
||||||
|
|
||||||
/** Разрешить NAT-чек для локальных подсетей (127.x, 10.x, 192.168.x).
|
|
||||||
* По умолчанию запрещено — только для тестов где узлы на loopback. */ |
|
||||||
|
|
||||||
void nat_detection_set_allow_local(struct NAT_DETECTION* nd, int allow); |
|
||||||
|
|
||||||
/** Отправить STUN-пинг через BGP-посредника (to_conn) к target_ip:target_port.
|
|
||||||
* pubkey — опционально (NULL = взять из nodeinfo). |
|
||||||
* count пингов, интервал interval_ms, таймаут одного timeout_ms, общий wait_timeout_ms. */ |
|
||||||
|
|
||||||
int route_ping_send_req_addr(struct NAT_DETECTION* nd, struct TOPO_GROUP* group, |
|
||||||
struct ETCP_CONN* to_conn, |
|
||||||
uint32_t target_ip, uint16_t target_port, |
|
||||||
uint8_t count, uint16_t interval_ms, uint16_t timeout_ms, |
|
||||||
uint16_t wait_timeout_ms, route_ping_callback_t cb, |
|
||||||
void* arg, const uint8_t* pubkey); |
|
||||||
|
|
||||||
#ifdef __cplusplus |
#ifdef __cplusplus |
||||||
} |
} |
||||||
#endif |
#endif |
||||||
#endif /* NAT_DETECTION_H */ |
|
||||||
|
#endif |
||||||
|
|||||||
@ -1,428 +1,77 @@ |
|||||||
#include <stdlib.h> |
#include "route_ping.h" |
||||||
|
|
||||||
#include <string.h> |
#include <string.h> |
||||||
#include <stdio.h> |
|
||||||
#ifdef _WIN32 |
|
||||||
#include <winsock2.h> |
|
||||||
#include <ws2tcpip.h> |
|
||||||
#else |
|
||||||
#include <arpa/inet.h> |
|
||||||
#endif |
|
||||||
#include "../lib/platform_compat.h" |
#include "../lib/platform_compat.h" |
||||||
#include "../lib/debug_config.h" |
#include "../lib/debug_config.h" |
||||||
#include "../lib/mem.h" |
|
||||||
#include "utun_instance.h" |
|
||||||
#include "etcp_api.h" |
|
||||||
#include "etcp.h" |
|
||||||
#include "etcp_connections.h" |
|
||||||
#include "topo_node.h" |
|
||||||
#include "topo_group.h" |
|
||||||
#include "route_ping.h" |
|
||||||
#include "../lib/u_async.h" |
|
||||||
|
|
||||||
struct route_ping_series_ctx { |
|
||||||
struct ETCP_CONN* reply_conn; |
|
||||||
uint32_t request_id; |
|
||||||
struct sockaddr_storage target_addr; |
|
||||||
uint8_t pubkey[SC_PUBKEY_SIZE]; |
|
||||||
struct ETCP_SOCKET* local_sock; |
|
||||||
uint8_t count_total; |
|
||||||
uint8_t count_sent; |
|
||||||
uint8_t count_ok; |
|
||||||
uint32_t sum_rtt; /* в 0.1 ms */ |
|
||||||
uint16_t timeout_ms; |
|
||||||
}; |
|
||||||
|
|
||||||
// ==================================== блок обработки запросов на удаленные пинги
|
|
||||||
|
|
||||||
struct route_ping_pending { |
|
||||||
struct route_ping_pending* next; |
|
||||||
struct NAT_DETECTION* nd; |
|
||||||
uint32_t request_id; |
|
||||||
route_ping_callback_t callback; |
|
||||||
void* arg; |
|
||||||
void* timeout_timer; |
|
||||||
uint8_t cancelled; |
|
||||||
struct ETCP_CONN* via_conn; // через кого отправили запрос (third_conn)
|
|
||||||
}; |
|
||||||
|
|
||||||
// если удаленный узел долго не отвечает - вызываем коллбэк по таймауту
|
|
||||||
static void route_ping_pending_timeout(void* arg) { |
|
||||||
struct route_ping_pending* p = (struct route_ping_pending*)arg; |
|
||||||
if (!p) return; |
|
||||||
p->timeout_timer = NULL; |
|
||||||
if (p->cancelled) { |
|
||||||
u_free(p); |
|
||||||
return; |
|
||||||
} |
|
||||||
struct NAT_DETECTION* nd = p->nd; |
|
||||||
struct route_ping_pending** cur = &nd->ping_pending; |
|
||||||
while (*cur) { |
|
||||||
if (*cur == p) { |
|
||||||
*cur = p->next; |
|
||||||
break; |
|
||||||
} |
|
||||||
cur = &(*cur)->next; |
|
||||||
} |
|
||||||
if (p->callback) { |
|
||||||
p->callback(0, 0, 0, 0, p->arg); |
|
||||||
} |
|
||||||
u_free(p); |
|
||||||
} |
|
||||||
|
|
||||||
// прошел ответ "серия пигнов на удаленном узле завершена"
|
|
||||||
void route_ping_handle_resp(struct NAT_DETECTION* nd, struct ETCP_CONN* from_conn, |
|
||||||
const uint8_t* data, size_t len) { |
|
||||||
if (!nd || !from_conn || !data || len < sizeof(struct NATDET_PING_RESP)) { |
|
||||||
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "invalid args"); |
|
||||||
return; |
|
||||||
} |
|
||||||
const struct NATDET_PING_RESP* resp = (const struct NATDET_PING_RESP*)data; |
|
||||||
DEBUG_INFO(DEBUG_CATEGORY_BGP, "from=%s request_id=%08x sent=%u ok=%u avg_rtt=%u", |
|
||||||
from_conn->log_name, (unsigned)resp->request_id, |
|
||||||
(unsigned)resp->count_sent, (unsigned)resp->count_ok, (unsigned)resp->avg_rtt); |
|
||||||
topo_node_ping_update_rtt(nd->inst->topo_groups, from_conn->peer_node_id, resp->avg_rtt); |
|
||||||
|
|
||||||
struct route_ping_pending** cur = &nd->ping_pending; |
|
||||||
while (*cur) { |
|
||||||
struct route_ping_pending* p = *cur; |
|
||||||
if (p->request_id == resp->request_id) { |
|
||||||
*cur = p->next; |
|
||||||
if (p->timeout_timer) { |
|
||||||
uasync_cancel_timeout(nd->inst->ua, p->timeout_timer); |
|
||||||
p->timeout_timer = NULL; |
|
||||||
} |
|
||||||
if (p->callback) { |
|
||||||
int success = (resp->count_ok > 0) ? 1 : 0; |
|
||||||
p->callback(success, resp->avg_rtt, resp->count_sent, resp->count_ok, p->arg); |
|
||||||
} |
|
||||||
u_free(p); |
|
||||||
return; |
|
||||||
} |
|
||||||
cur = &(*cur)->next; |
|
||||||
} |
|
||||||
DEBUG_WARN(DEBUG_CATEGORY_BGP, "request_id=%08x not found in pending list", |
|
||||||
(unsigned)resp->request_id); |
|
||||||
} |
|
||||||
|
|
||||||
void route_ping_cancel_for_conn(struct NAT_DETECTION* nd, struct ETCP_CONN* conn) { |
|
||||||
if (!nd || !conn) return; |
|
||||||
|
|
||||||
if (!nd->ping_pending) { |
static void write64(uint8_t *out, uint64_t value) { |
||||||
DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "cancel_for_conn: ping_pending is NULL for %s", conn->log_name); |
for (unsigned i = 0; i < 8; i++) out[i] = (uint8_t)(value >> (56 - i * 8)); |
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "cancel_for_conn: checking pending for %s", conn->log_name); |
|
||||||
|
|
||||||
int cancelled = 0; |
|
||||||
struct route_ping_pending** cur = &nd->ping_pending; |
|
||||||
while (*cur) { |
|
||||||
struct route_ping_pending* p = *cur; |
|
||||||
struct nat_check_arg* na = (struct nat_check_arg*)p->arg; |
|
||||||
|
|
||||||
DEBUG_DEBUG(DEBUG_CATEGORY_BGP, " checking pending req_id=%08x via=%s link->etcp=%s", |
|
||||||
(unsigned)p->request_id, |
|
||||||
p->via_conn ? p->via_conn->log_name : "NULL", |
|
||||||
(na && na->link && na->link->etcp) ? na->link->etcp->log_name : "NULL"); |
|
||||||
|
|
||||||
if ((na && na->link && na->link->etcp == conn) || p->via_conn == conn) { |
|
||||||
*cur = p->next; |
|
||||||
uint32_t rid = p->request_id; |
|
||||||
if (p->timeout_timer) { |
|
||||||
uasync_cancel_timeout(nd->inst->ua, p->timeout_timer); |
|
||||||
p->timeout_timer = NULL; |
|
||||||
} |
|
||||||
p->cancelled = 1; |
|
||||||
if (na) na->link = NULL; |
|
||||||
u_free(na); |
|
||||||
u_free(p); |
|
||||||
cancelled++; |
|
||||||
DEBUG_INFO(DEBUG_CATEGORY_BGP, "cancel_for_conn: cancelled req_id=%08x", (unsigned)rid); |
|
||||||
} else { |
|
||||||
cur = &(*cur)->next; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if (cancelled == 0) { |
|
||||||
DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "cancel_for_conn: no pending found for %s", conn->log_name); |
|
||||||
} |
|
||||||
} |
} |
||||||
|
|
||||||
void route_ping_cancel_for_link(struct NAT_DETECTION* nd, struct ETCP_LINK* link) { |
int route_ping_addr_encode(uint8_t out[ROUTE_ADDR_WIRE_SIZE], const struct sockaddr_storage *address) { |
||||||
if (!nd || !link) return; |
if (!out || !address) { DEBUG_ERROR(DEBUG_CATEGORY_NAT, "address encode: missing argument"); return -1; } |
||||||
struct route_ping_pending** cur = &nd->ping_pending; |
memset(out, 0, ROUTE_ADDR_WIRE_SIZE); |
||||||
while (*cur) { |
if (address->ss_family == AF_INET) { |
||||||
struct route_ping_pending* p = *cur; |
const struct sockaddr_in *v4 = (const struct sockaddr_in *)address; |
||||||
struct nat_check_arg* na = (struct nat_check_arg*)p->arg; |
out[0] = 4; memcpy(out + 1, &v4->sin_addr, 4); memcpy(out + 17, &v4->sin_port, 2); |
||||||
if (na && na->link == link) { |
} else if (address->ss_family == AF_INET6) { |
||||||
*cur = p->next; |
const struct sockaddr_in6 *v6 = (const struct sockaddr_in6 *)address; |
||||||
uint32_t rid = p->request_id; |
out[0] = 6; memcpy(out + 1, &v6->sin6_addr, 16); memcpy(out + 17, &v6->sin6_port, 2); |
||||||
if (p->timeout_timer) { |
} else { DEBUG_ERROR(DEBUG_CATEGORY_NAT, "address encode: unsupported family=%d", address->ss_family); return -1; } |
||||||
uasync_cancel_timeout(nd->inst->ua, p->timeout_timer); |
return 0; |
||||||
p->timeout_timer = NULL; |
|
||||||
} |
|
||||||
p->cancelled = 1; |
|
||||||
na->link = NULL; |
|
||||||
u_free(na); |
|
||||||
u_free(p); |
|
||||||
DEBUG_INFO(DEBUG_CATEGORY_BGP, "cancel_for_link: cancelled req_id=%08x link=%p", (unsigned)rid, (void*)link); |
|
||||||
} else { |
|
||||||
cur = &(*cur)->next; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
} |
||||||
|
|
||||||
void route_ping_destroy_pending(struct NAT_DETECTION* nd) { |
int route_ping_addr_decode(const uint8_t in[ROUTE_ADDR_WIRE_SIZE], struct sockaddr_storage *address) { |
||||||
if (!nd) return; |
if (!in || !address) { DEBUG_ERROR(DEBUG_CATEGORY_NAT, "address decode: missing argument"); return -1; } |
||||||
struct route_ping_pending* p = nd->ping_pending; |
memset(address, 0, sizeof(*address)); |
||||||
while (p) { |
if (in[0] == 4) { |
||||||
struct route_ping_pending* next = p->next; |
struct sockaddr_in *v4 = (struct sockaddr_in *)address; |
||||||
if (p->timeout_timer) { |
v4->sin_family = AF_INET; memcpy(&v4->sin_addr, in + 1, 4); memcpy(&v4->sin_port, in + 17, 2); |
||||||
uasync_cancel_timeout(nd->inst->ua, p->timeout_timer); |
static const uint8_t zero[12]; |
||||||
p->timeout_timer = NULL; |
if (memcmp(in + 5, zero, 12)) { DEBUG_WARN(DEBUG_CATEGORY_NAT, "address decode: noncanonical IPv4"); return -1; } |
||||||
} |
} else if (in[0] == 6) { |
||||||
p->cancelled = 1; |
struct sockaddr_in6 *v6 = (struct sockaddr_in6 *)address; |
||||||
u_free(p); |
v6->sin6_family = AF_INET6; memcpy(&v6->sin6_addr, in + 1, 16); memcpy(&v6->sin6_port, in + 17, 2); |
||||||
p = next; |
} else { DEBUG_WARN(DEBUG_CATEGORY_NAT, "address decode: invalid family=%u", in[0]); return -1; } |
||||||
} |
if (!in[17] && !in[18]) { DEBUG_WARN(DEBUG_CATEGORY_NAT, "address decode: zero port"); return -1; } |
||||||
nd->ping_pending = NULL; |
return 0; |
||||||
} |
} |
||||||
|
|
||||||
// ========================================================================
|
int route_ping_addr_equal(const struct sockaddr_storage *a, const struct sockaddr_storage *b, int include_port) { |
||||||
|
if (!a || !b || a->ss_family != b->ss_family) return 0; |
||||||
int route_ping_send_req_addr(struct NAT_DETECTION* nd, struct TOPO_GROUP* group, |
if (a->ss_family == AF_INET) { |
||||||
struct ETCP_CONN* to_conn, |
const struct sockaddr_in *av = (const struct sockaddr_in *)a, *bv = (const struct sockaddr_in *)b; |
||||||
uint32_t target_ip, uint16_t target_port, |
return av->sin_addr.s_addr == bv->sin_addr.s_addr && (!include_port || av->sin_port == bv->sin_port); |
||||||
uint8_t count, uint16_t interval_ms, uint16_t timeout_ms, |
|
||||||
uint16_t wait_timeout_ms, route_ping_callback_t cb, void* arg, |
|
||||||
const uint8_t* pubkey) { |
|
||||||
(void)group; |
|
||||||
if (!nd || !to_conn || count == 0 || timeout_ms == 0) { |
|
||||||
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "invalid args"); |
|
||||||
return -1; |
|
||||||
} |
|
||||||
size_t pkt_size = pubkey ? sizeof(struct NATDET_PING_REQ) : offsetof(struct NATDET_PING_REQ, pubkey); |
|
||||||
struct NATDET_PING_REQ* req_pkt = u_calloc(1, pkt_size); |
|
||||||
if (!req_pkt) { |
|
||||||
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "alloc failed"); |
|
||||||
return -2; |
|
||||||
} |
|
||||||
req_pkt->cmd = ETCP_ID_NAT_DETECTION; |
|
||||||
req_pkt->subcmd = NATDET_SUBCMD_PING_REQ; |
|
||||||
uint32_t new_req_id = nd->next_ping_req_id++; |
|
||||||
req_pkt->request_id = new_req_id; |
|
||||||
req_pkt->count = count; |
|
||||||
req_pkt->interval_ms = interval_ms; |
|
||||||
req_pkt->timeout_ms = timeout_ms; |
|
||||||
/* IP уже в network byte order, порт конвертируем */ |
|
||||||
memcpy(req_pkt->target_ipv4, &target_ip, 4); |
|
||||||
req_pkt->target_port = htons(target_port); |
|
||||||
if (pubkey) { |
|
||||||
memcpy(req_pkt->pubkey, pubkey, SC_PUBKEY_SIZE); |
|
||||||
} |
|
||||||
|
|
||||||
struct ll_entry* e = queue_entry_new(0); |
|
||||||
if (!e) { |
|
||||||
u_free(req_pkt); |
|
||||||
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "queue_entry_new failed"); |
|
||||||
return -3; |
|
||||||
} |
} |
||||||
e->dgram = (uint8_t*)req_pkt; |
if (a->ss_family == AF_INET6) { |
||||||
e->len = pkt_size; |
const struct sockaddr_in6 *av = (const struct sockaddr_in6 *)a, *bv = (const struct sockaddr_in6 *)b; |
||||||
|
return !memcmp(&av->sin6_addr, &bv->sin6_addr, 16) && (!include_port || av->sin6_port == bv->sin6_port); |
||||||
DEBUG_INFO(DEBUG_CATEGORY_BGP, "request_id=%08x ip=%s port=%u pubkey=%s", |
|
||||||
(unsigned)new_req_id, |
|
||||||
ip_to_str(&target_ip, AF_INET).str, (unsigned)target_port, pubkey ? "yes" : "no"); |
|
||||||
|
|
||||||
int ret = etcp_send(to_conn, e); |
|
||||||
if (ret != 0) { |
|
||||||
u_free(req_pkt); |
|
||||||
queue_entry_free(e); |
|
||||||
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "etcp_send failed"); |
|
||||||
return -4; |
|
||||||
} |
} |
||||||
|
|
||||||
struct route_ping_pending* pending = u_calloc(1, sizeof(struct route_ping_pending)); |
|
||||||
if (!pending) { |
|
||||||
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "pending alloc failed"); |
|
||||||
return -5; |
|
||||||
} |
|
||||||
pending->nd = nd; |
|
||||||
pending->request_id = new_req_id; |
|
||||||
pending->callback = cb; |
|
||||||
pending->arg = arg; |
|
||||||
pending->via_conn = to_conn; |
|
||||||
pending->next = nd->ping_pending; |
|
||||||
nd->ping_pending = pending; |
|
||||||
|
|
||||||
pending->timeout_timer = uasync_set_timeout(nd->inst->ua, wait_timeout_ms * 10, pending, |
|
||||||
route_ping_pending_timeout, "route_ping"); |
|
||||||
return 0; |
return 0; |
||||||
} |
} |
||||||
|
|
||||||
|
void route_ping_make_tag(uint8_t out[ROUTE_OBSERVE_TAG_SIZE], const struct ETCP_SOCKET *socket, uint64_t round) { |
||||||
static void route_ping_series_finish(struct route_ping_series_ctx* ctx) { |
memcpy(out, "NAT2UDP!", 8); out[8] = socket->sock_id; |
||||||
if (!ctx || !ctx->reply_conn) { |
write64(out + 9, socket->generation); write64(out + 17, round); |
||||||
u_free(ctx); |
|
||||||
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "zero args"); |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
uint16_t avg_rtt = (ctx->count_ok > 0) |
|
||||||
? (uint16_t)(ctx->sum_rtt / ctx->count_ok) |
|
||||||
: (uint16_t)(ctx->timeout_ms * 10); /* таймаут как "плохо" */ |
|
||||||
|
|
||||||
/* Отправляем ответ по BGP */ |
|
||||||
struct NATDET_PING_RESP* resp = u_calloc(1, sizeof(struct NATDET_PING_RESP)); |
|
||||||
if (resp) { |
|
||||||
resp->cmd = ETCP_ID_NAT_DETECTION; |
|
||||||
resp->subcmd = NATDET_SUBCMD_PING_RESP; |
|
||||||
resp->request_id = ctx->request_id; |
|
||||||
resp->count_sent = ctx->count_sent; |
|
||||||
resp->count_ok = ctx->count_ok; |
|
||||||
resp->avg_rtt = avg_rtt; |
|
||||||
|
|
||||||
struct ll_entry* e = queue_entry_new(0); |
|
||||||
if (e) { |
|
||||||
e->dgram = (uint8_t*)resp; |
|
||||||
e->len = sizeof(struct NATDET_PING_RESP); |
|
||||||
if (etcp_send(ctx->reply_conn, e) != 0) { |
|
||||||
queue_dgram_free(e); queue_entry_free(e); |
|
||||||
} else { |
|
||||||
DEBUG_INFO(DEBUG_CATEGORY_BGP, |
|
||||||
"PING series done %s request_id=%08x sent=%u ok=%u avg_rtt=%u", |
|
||||||
ctx->reply_conn->log_name, |
|
||||||
(unsigned)ctx->request_id, |
|
||||||
(unsigned)ctx->count_sent, |
|
||||||
(unsigned)ctx->count_ok, |
|
||||||
(unsigned)avg_rtt); |
|
||||||
} |
|
||||||
} else { |
|
||||||
u_free(resp); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
u_free(ctx); |
|
||||||
} |
} |
||||||
|
|
||||||
/* Callback одного пинга из серии */ |
int route_ping_observe(struct ETCP_SOCKET *socket, const uint8_t peer_key[SC_PUBKEY_SIZE], |
||||||
static void route_ping_single_cb(int success, |
const struct sockaddr_storage *peer_address, uint64_t round, int timeout_ms, |
||||||
uint16_t rtt, |
etcp_ping_callback_t callback, void *arg) { |
||||||
void* arg, |
if (!socket || !socket->generation || !round) { DEBUG_ERROR(DEBUG_CATEGORY_NAT, "observe: invalid endpoint/round"); return -1; } |
||||||
uint64_t nonce, |
uint8_t tag[ROUTE_OBSERVE_TAG_SIZE]; route_ping_make_tag(tag, socket, round); |
||||||
const uint8_t* resp_data, |
return etcp_send_ping_to_socket(socket->instance, socket, peer_key, peer_address, timeout_ms, |
||||||
size_t resp_data_len) { |
callback, arg, tag, sizeof(tag), ETCP_PING_FLAG_OBSERVE); |
||||||
(void)nonce; (void)resp_data; (void)resp_data_len; |
|
||||||
|
|
||||||
struct route_ping_series_ctx* ctx = (struct route_ping_series_ctx*)arg; |
|
||||||
if (!ctx) return; |
|
||||||
|
|
||||||
/* Обновляем статистику */ |
|
||||||
ctx->count_sent++; |
|
||||||
if (success) { |
|
||||||
ctx->count_ok++; |
|
||||||
ctx->sum_rtt += rtt; |
|
||||||
} |
|
||||||
|
|
||||||
DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "ping cb: ok=%d rtt=%u sent=%d/%d nonce=%016llx", |
|
||||||
success, rtt, ctx->count_sent, ctx->count_total, (unsigned long long)nonce); |
|
||||||
|
|
||||||
/* Если ещё не все пакеты отправлены — сразу шлём следующий */ |
|
||||||
if (ctx->count_sent < ctx->count_total) { |
|
||||||
int ret = etcp_send_ping_to_socket( |
|
||||||
ctx->reply_conn->instance, |
|
||||||
ctx->local_sock, |
|
||||||
ctx->pubkey, |
|
||||||
&ctx->target_addr, |
|
||||||
ctx->timeout_ms, |
|
||||||
route_ping_single_cb, |
|
||||||
ctx, |
|
||||||
NULL, 0, 0); |
|
||||||
|
|
||||||
if (ret != 0) { |
|
||||||
/* Не смогли отправить следующий — завершаем серию досрочно */ |
|
||||||
route_ping_series_finish(ctx); |
|
||||||
} |
|
||||||
/* else: следующий пинг запущен, ждём его callback */ |
|
||||||
} else { |
|
||||||
/* Все пакеты обработаны */ |
|
||||||
route_ping_series_finish(ctx); |
|
||||||
} |
|
||||||
} |
} |
||||||
|
|
||||||
// Обработчик PING_REQ (нам запросили "пропингуй узел и верни результат")
|
int route_ping_observation(const uint8_t *data, size_t length, const struct ETCP_SOCKET *socket, |
||||||
void route_ping_handle_req(struct NAT_DETECTION* nd, struct TOPO_GROUP* group, |
uint64_t round, struct sockaddr_storage *observed) { |
||||||
struct ETCP_CONN* from_conn, |
if (!data || !socket || length != ROUTE_ADDR_WIRE_SIZE + ROUTE_OBSERVE_TAG_SIZE) { |
||||||
const uint8_t* data, |
DEBUG_WARN(DEBUG_CATEGORY_NAT, "observation: invalid size=%zu", length); return -1; |
||||||
size_t len) { |
|
||||||
if (!from_conn || !nd || !data || len < offsetof(struct NATDET_PING_REQ, pubkey)) { |
|
||||||
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "ping req: bad args len=%zu", len); |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
const struct NATDET_PING_REQ* req_pkt = (const struct NATDET_PING_REQ*)data; |
|
||||||
|
|
||||||
/* Создаём контекст серии */ |
|
||||||
struct route_ping_series_ctx* ctx = u_calloc(1, sizeof(struct route_ping_series_ctx)); |
|
||||||
if (!ctx) { |
|
||||||
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "ping req: alloc ctx failed"); |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
ctx->reply_conn = from_conn; |
|
||||||
ctx->request_id = req_pkt->request_id; |
|
||||||
ctx->count_total = req_pkt->count; |
|
||||||
ctx->timeout_ms = req_pkt->timeout_ms; |
|
||||||
|
|
||||||
{ |
|
||||||
if (group) { |
|
||||||
struct TOPO_GROUP_NODE* req_nq = topo_node_find_by_id(group, from_conn->peer_node_id); |
|
||||||
if (req_nq) req_nq->connectivity.ping_req_time = get_time_tb(); |
|
||||||
} |
|
||||||
} |
|
||||||
if (len >= sizeof(struct NATDET_PING_REQ)) { |
|
||||||
memcpy(ctx->pubkey, req_pkt->pubkey, SC_PUBKEY_SIZE); |
|
||||||
} |
} |
||||||
|
uint8_t tag[ROUTE_OBSERVE_TAG_SIZE]; route_ping_make_tag(tag, socket, round); |
||||||
/* Целевой адрес - в sockaddr храним в network byte order (как в пакете) */ |
if (memcmp(data + ROUTE_ADDR_WIRE_SIZE, tag, sizeof(tag))) { |
||||||
struct sockaddr_in* sin = (struct sockaddr_in*)&ctx->target_addr; |
DEBUG_WARN(DEBUG_CATEGORY_NAT, "observation: stale endpoint generation/round"); return -1; |
||||||
sin->sin_family = AF_INET; |
|
||||||
memcpy(&sin->sin_addr.s_addr, req_pkt->target_ipv4, 4); /* network byte order */ |
|
||||||
sin->sin_port = req_pkt->target_port; /* порт в network byte order */ |
|
||||||
|
|
||||||
/* Ищем первый IPv4-сокет */ |
|
||||||
struct ETCP_SOCKET* ls = (group ? group->instance : nd->inst)->etcp_sockets; |
|
||||||
while (ls) { |
|
||||||
if (ls->local_addr.ss_family == AF_INET) { |
|
||||||
ctx->local_sock = ls; |
|
||||||
break; |
|
||||||
} |
|
||||||
ls = ls->next; |
|
||||||
} |
|
||||||
|
|
||||||
if (!ctx->local_sock) { |
|
||||||
DEBUG_WARN(DEBUG_CATEGORY_BGP, "ping req: no IPv4 socket"); |
|
||||||
route_ping_series_finish(ctx); /* отправит 0/0 */ |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
DEBUG_INFO(DEBUG_CATEGORY_BGP, "PING series start request_id=%08x target=%s:%u count=%u timeout=%u", |
|
||||||
(unsigned)ctx->request_id, ip_to_str(&sin->sin_addr, AF_INET).str, |
|
||||||
ntohs(sin->sin_port), (unsigned)ctx->count_total, (unsigned)ctx->timeout_ms); |
|
||||||
|
|
||||||
/* Запускаем первый пинг (дальше цепочка через callback) */ |
|
||||||
int ret = etcp_send_ping_to_socket( |
|
||||||
(group ? group->instance : nd->inst), |
|
||||||
ctx->local_sock, |
|
||||||
ctx->pubkey, |
|
||||||
&ctx->target_addr, |
|
||||||
ctx->timeout_ms, |
|
||||||
route_ping_single_cb, |
|
||||||
ctx, |
|
||||||
NULL, 0, 0); |
|
||||||
|
|
||||||
if (ret != 0) { |
|
||||||
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "ping req: cannot start first ping"); |
|
||||||
route_ping_series_finish(ctx); |
|
||||||
} |
} |
||||||
|
return route_ping_addr_decode(data, observed); |
||||||
} |
} |
||||||
|
|||||||
@ -1,56 +1,33 @@ |
|||||||
/**
|
/* Прямые UDP-наблюдения с указанного рабочего сокета. Управление помощниками —
|
||||||
* @file route_ping.h |
* nat_detection; этот модуль не вычисляет NAT-тип и не выбирает чужой сокет. */ |
||||||
* @brief Удалённый пинг через BGP-посредника. |
|
||||||
* |
|
||||||
* A просит B пропинговать C и вернуть сводную статистику: |
|
||||||
* A --PING_REQ--> B --ETCP ping--> C |
|
||||||
* A <--PING_RESP-- B (sent/ok/avg_rtt) |
|
||||||
* |
|
||||||
* Промежуточный узел задаётся ЯВНО в to_conn — вызывающий сам выбирает, |
|
||||||
* через кого слать запрос. Автоподбор посредника (nat_check_find_third_node) |
|
||||||
* находится в nat_detection.c, не здесь. |
|
||||||
*/ |
|
||||||
#ifndef ROUTE_PING_H |
#ifndef ROUTE_PING_H |
||||||
#define ROUTE_PING_H |
#define ROUTE_PING_H |
||||||
|
|
||||||
|
#include <stddef.h> |
||||||
|
#include <stdint.h> |
||||||
|
#include "etcp_connections.h" |
||||||
|
|
||||||
#ifdef __cplusplus |
#ifdef __cplusplus |
||||||
extern "C" { |
extern "C" { |
||||||
#endif |
#endif |
||||||
|
|
||||||
#include <stdint.h> |
#define ROUTE_ADDR_WIRE_SIZE 19 |
||||||
#include <stddef.h> |
#define ROUTE_OBSERVE_TAG_SIZE 25 |
||||||
#include "nat_detection.h" |
|
||||||
|
|
||||||
/** Запросить у to_conn серию пингов на target_ip:target_port.
|
|
||||||
* cb вызывается всегда ровно 1 раз: |
|
||||||
* success=1 если хоть один пинг успешен (PONG получен) |
|
||||||
* success=0 при общем таймауте или все пинги провалились |
|
||||||
* @param to_conn промежуточный узел — выбирается вызывающим явно */ |
|
||||||
int route_ping_send_req_addr(struct NAT_DETECTION* nd, struct TOPO_GROUP* group, |
|
||||||
struct ETCP_CONN* to_conn, |
|
||||||
uint32_t target_ip, uint16_t target_port, |
|
||||||
uint8_t count, uint16_t interval_ms, uint16_t timeout_ms, |
|
||||||
uint16_t wait_timeout_ms, route_ping_callback_t cb, |
|
||||||
void* arg, const uint8_t* pubkey); |
|
||||||
|
|
||||||
/** Отменить все ожидающие ответы (при остановке NAT_DETECTION). */ |
int route_ping_addr_encode(uint8_t out[ROUTE_ADDR_WIRE_SIZE], const struct sockaddr_storage *address); |
||||||
void route_ping_destroy_pending(struct NAT_DETECTION* nd); |
int route_ping_addr_decode(const uint8_t in[ROUTE_ADDR_WIRE_SIZE], struct sockaddr_storage *address); |
||||||
|
int route_ping_addr_equal(const struct sockaddr_storage *a, const struct sockaddr_storage *b, int include_port); |
||||||
|
|
||||||
/** Отменить pending для conn (при разрыве/реините соединения). */ |
/* Ответ: address(19) || tag(25), все числа BE, IP — неизменённые сетевые байты. */ |
||||||
void route_ping_cancel_for_conn(struct NAT_DETECTION* nd, struct ETCP_CONN* conn); |
int route_ping_observe(struct ETCP_SOCKET *socket, const uint8_t peer_key[SC_PUBKEY_SIZE], |
||||||
|
const struct sockaddr_storage *peer_address, uint64_t round, int timeout_ms, |
||||||
/** Отменить pending для конкретного линка (при закрытии линка без разрыва conn). */ |
etcp_ping_callback_t callback, void *arg); |
||||||
void route_ping_cancel_for_link(struct NAT_DETECTION* nd, struct ETCP_LINK* link); |
int route_ping_observation(const uint8_t *data, size_t length, const struct ETCP_SOCKET *socket, |
||||||
|
uint64_t round, struct sockaddr_storage *observed); |
||||||
/** Входящий PING_REQ — мы посредник: пингуем цель, шлём PING_RESP. */ |
void route_ping_make_tag(uint8_t out[ROUTE_OBSERVE_TAG_SIZE], const struct ETCP_SOCKET *socket, uint64_t round); |
||||||
void route_ping_handle_req(struct NAT_DETECTION* nd, struct TOPO_GROUP* group, |
|
||||||
struct ETCP_CONN* from_conn, const uint8_t* data, size_t len); |
|
||||||
|
|
||||||
/** Входящий PING_RESP — мы инициатор: находим pending по request_id, вызываем cb. */ |
|
||||||
void route_ping_handle_resp(struct NAT_DETECTION* nd, |
|
||||||
struct ETCP_CONN* from_conn, const uint8_t* data, size_t len); |
|
||||||
|
|
||||||
#ifdef __cplusplus |
#ifdef __cplusplus |
||||||
} |
} |
||||||
#endif |
#endif |
||||||
#endif // ROUTE_PING_H
|
|
||||||
|
#endif |
||||||
|
|||||||
@ -1,62 +1,28 @@ |
|||||||
# route_ping — удалённый пинг через BGP (NAT-детекция, проверка живости) |
# route_ping — наблюдение рабочего UDP endpoint |
||||||
|
|
||||||
## 1. Назначение |
Модуль кодирует адреса и выполняет прямое UDP-наблюдение через конкретный |
||||||
Механизм запроса удалённого пинга через BGP-канал. Узел A отправляет узлу B TOPO_PING_REQ |
`ETCP_SOCKET`. Выбор B/C, пересылка управляющих сообщений и классификация |
||||||
с IP:port цели, узел B выполняет серию пингов (`etcp_send_ping_to_socket`) и возвращает |
профиля принадлежат `nat_detection`; алгоритм — в `doc/nat_detection.txt`. |
||||||
TOPO_PING_RESP с результатами (count_ok, avg_rtt). |
|
||||||
|
`route_ping_observe(socket, peer_key, peer_address, round, timeout_ms, callback, arg)` |
||||||
Используется: |
передаёт PING с `ETCP_PING_FLAG_OBSERVE`. PONG содержит фактически наблюдаемый |
||||||
- `route_connectivity` — для зондирования связности (прямые пинги, без BGP) |
source IP:port и исходный тег. Профиль не меняется от единственного ответа. |
||||||
- `topo_group` — для NAT-детекции через промежуточный узел (third-party ping) |
`route_ping_observation` проверяет размер, ID/поколение сокета и round. |
||||||
- Общий механизм проверки живости адресов |
|
||||||
|
Адрес занимает 19 байт: family(1: 4/6), IP(16: IPv4 + 12 нулей либо IPv6), |
||||||
## 2. Как пользоваться |
port(2, BE). Тег занимает 25 байт: `NAT2UDP!`(8), socket_id(1), |
||||||
```c |
generation(8, BE), round(8, BE). Неправильные family, скрытые IPv4-байты |
||||||
// Отправить запрос пинга адреса target_ip:target_port через промежуточный узел to_conn |
и нулевой порт отвергаются. Новый wire не использует native packed integers. |
||||||
int ret = route_ping_send_req_addr(group, to_conn, |
|
||||||
target_ip, target_port, |
В транспорте nonce генерируется CSPRNG; PONG должен соответствовать ожидаемому |
||||||
count=2, interval_ms=100, timeout_ms=500, |
ключу, source IP:port и принимающему socket generation. Pending снимается |
||||||
wait_timeout_ms=2000, callback, arg, pubkey); |
до callback. `etcp_cancel_pings(instance, callback, arg)` отменяет только |
||||||
``` |
совпадающие запросы без callback; arg остаётся собственностью вызывающего. |
||||||
|
|
||||||
Промежуточный узел (to_conn) получает запрос → выполняет серию `etcp_send_ping_to_socket()` |
`ETCP_PING_FLAG_ADDRESS_CHECK` обозначает прямое входящее испытание. Его |
||||||
→ собирает статистику → отправляет ответ. |
receipt фиксируется на рабочем сокете владельца до отправки PONG. Потеря |
||||||
|
обратного PONG поэтому не превращает полученный ingress в отрицательный результат. |
||||||
Ключевой нюанс: `wait_timeout_ms` задаёт таймаут ожидания ответа на локальной стороне. |
UDP-результаты не используются для TCP listener и SOCKS5 endpoint. |
||||||
Если ответ не приходит вовремя, `route_ping_pending_timeout` вызывает callback с `success=0`. |
|
||||||
|
Проверки: `tests/test_route_ping.c`, `tests/test_nat_detection.c`, |
||||||
## 3. API |
изолированный стенд `tests/stream_netns/run.py --nat-only`. |
||||||
|
|
||||||
### Протокольные структуры (packed, wire-format) |
|
||||||
| Структура | Поля | |
|
||||||
|-----------|------| |
|
||||||
| `TOPO_PING_REQ` | `cmd=ETCP_ID_TOPO_ENTRY`, `subcmd=0x07`, `request_id`, `count`, `interval_ms`, `timeout_ms`, `target_ipv4`, `target_port`, `pubkey` (опционально) | |
|
||||||
| `TOPO_PING_RESP` | `cmd`, `subcmd=0x08`, `request_id`, `count_sent`, `count_ok`, `avg_rtt` (0.1ms) | |
|
||||||
|
|
||||||
### Внутренние структуры |
|
||||||
| Структура | Назначение | |
|
||||||
|-----------|------------| |
|
||||||
| `route_ping_series_ctx` | Контекст серии пингов (на отвечающей стороне): `reply_conn`, `request_id`, `target_addr`, `local_sock`, счётчики, статистика | |
|
||||||
| `route_ping_pending` | Ожидающий запрос (на запрашивающей стороне): `request_id`, `callback`, `timeout_timer`, `via_conn` | |
|
||||||
|
|
||||||
### Функции |
|
||||||
| Функция | Назначение | |
|
||||||
|---------|------------| |
|
||||||
| `route_ping_send_req_addr(group, to_conn, target_ip, target_port, count, interval, timeout, wait_timeout, cb, arg, pubkey)` | Отправить TOPO_PING_REQ через BGP, зарегистрировать callback | |
|
||||||
| `route_ping_handle_req(group, from_conn, data, len)` | Обработать входящий PING_REQ: найти локальный IPv4-сокет, запустить серию `etcp_send_ping_to_socket()`, по завершении отправить PING_RESP | |
|
||||||
| `route_ping_handle_resp(group, from_conn, data, len)` | Обработать PING_RESP: найти pending по `request_id`, отменить таймер, вызвать callback | |
|
||||||
| `route_ping_destroy_pending(group)` | Очистить весь список pending (при уничтожении BGP) | |
|
||||||
| `route_ping_cancel_for_conn(group, conn)` | Отменить pending запросы, связанные с конкретным `ETCP_CONN` (при разрыве) | |
|
||||||
|
|
||||||
### Callback |
|
||||||
```c |
|
||||||
typedef void (*route_ping_callback_t)(int success, uint16_t avg_rtt, |
|
||||||
uint8_t count_sent, uint8_t count_ok, void* arg); |
|
||||||
``` |
|
||||||
Вызывается при получении ответа или по таймауту (`success=0`). |
|
||||||
|
|
||||||
### Зависимости |
|
||||||
- `src/topo_group.h` — BGP-канал (список `ping_pending`, `next_ping_req_id`) |
|
||||||
- `src/etcp_api.h` — `etcp_send()` для отправки через BGP |
|
||||||
- `src/etcp_connections.h` — `etcp_send_ping_to_socket()` для собственно пинга |
|
||||||
- `src/topo_node.h` — `topo_node_find_by_id()` |
|
||||||
|
|||||||
@ -0,0 +1,82 @@ |
|||||||
|
#ifndef NAT_FIXTURE_H |
||||||
|
#define NAT_FIXTURE_H |
||||||
|
|
||||||
|
#include <assert.h> |
||||||
|
#include <stdio.h> |
||||||
|
#include <string.h> |
||||||
|
#include "test_net.h" |
||||||
|
#include "utun_instance.h" |
||||||
|
#include "config_parser.h" |
||||||
|
#include "transport_layer/etcp.h" |
||||||
|
#include "transport_layer/node_conn_direct.h" |
||||||
|
#include "routing_layer/topo_group.h" |
||||||
|
#include "routing_layer/topo_node_sqlite.h" |
||||||
|
#include "routing_layer/nat_detection.h" |
||||||
|
#include "routing_layer/route_ping.h" |
||||||
|
#include "../lib/u_async.h" |
||||||
|
#include "../lib/mem.h" |
||||||
|
|
||||||
|
struct nat_fixture { |
||||||
|
struct UASYNC *ua; |
||||||
|
struct UTUN_INSTANCE *nodes[8]; |
||||||
|
struct NODE_CONN_DIRECT *handles[24]; |
||||||
|
unsigned node_count, handle_count; |
||||||
|
size_t allocations; |
||||||
|
}; |
||||||
|
|
||||||
|
static inline void nat_fixture_init(struct nat_fixture *fixture) { |
||||||
|
memset(fixture, 0, sizeof(*fixture)); fixture->allocations = u_get_allocated_count(); |
||||||
|
fixture->ua = uasync_create(); assert(fixture->ua); |
||||||
|
} |
||||||
|
|
||||||
|
static inline struct UTUN_INSTANCE *nat_fixture_node(struct nat_fixture *fixture, const char *ip, |
||||||
|
const struct SC_MYKEYS *keys, const char *extra) { |
||||||
|
struct SC_MYKEYS generated; |
||||||
|
if (!keys) { assert(sc_generate_keypair(&generated) == SC_OK); keys = &generated; } |
||||||
|
char private_key[65], public_key[65], config[4096]; |
||||||
|
for (unsigned i = 0; i < 32; i++) { |
||||||
|
snprintf(private_key + i * 2, 3, "%02x", keys->private_key[i]); |
||||||
|
snprintf(public_key + i * 2, 3, "%02x", keys->public_key[i]); |
||||||
|
} |
||||||
|
snprintf(config, sizeof(config), "[global]\nmy_private_key=%s\nmy_public_key=%s\nauto_sockets=no\n" |
||||||
|
"[ntp]\nenabled=no\n[server:udp]\naddr=%s:0\ntype=public\n[allowed_keys]\nallow_all=1\n%s", |
||||||
|
private_key, public_key, ip, extra ? extra : ""); |
||||||
|
struct UTUN_INSTANCE *node = utun_instance_create_from_str(fixture->ua, config); |
||||||
|
assert(node && fixture->node_count < 8 && utun_core_start(node) == 0); |
||||||
|
nat_detection_set_allow_local(node->nat_det, 1); |
||||||
|
fixture->nodes[fixture->node_count++] = node; return node; |
||||||
|
} |
||||||
|
|
||||||
|
/* Без групповой автосвязности: A контактирует только с явно указанным B.
|
||||||
|
* NCD переносит настоящую подписанную запись, handshake идёт по UDP. */ |
||||||
|
static inline struct ETCP_CONN *nat_fixture_connect(struct nat_fixture *fixture, struct UTUN_INSTANCE *from, |
||||||
|
const char *source, struct UTUN_INSTANCE *to) { |
||||||
|
assert(fixture->handle_count < 24); |
||||||
|
struct TOPO_NODE *peer = topo_node_registry_find(to->topo_groups, to->node_id); assert(peer); |
||||||
|
struct NODE_CONN_DIRECT **handle = &fixture->handles[fixture->handle_count++]; |
||||||
|
assert(node_conn_direct_open_node(from, to->node_id, NULL, NULL, handle, peer, |
||||||
|
test_etcp_bound_socket(from, source)) != NCD_ERR); |
||||||
|
uint64_t end = get_time_tb() + 50000; |
||||||
|
struct ETCP_CONN *connection = NULL; |
||||||
|
while (get_time_tb() < end) { |
||||||
|
connection = node_conn_direct_get_conn(*handle); |
||||||
|
if (connection && connection->links_up) return connection; |
||||||
|
uasync_poll(fixture->ua, 10); |
||||||
|
} |
||||||
|
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "fixture connection timed out from=%016llx to=%016llx", |
||||||
|
(unsigned long long)from->node_id, (unsigned long long)to->node_id); |
||||||
|
abort(); |
||||||
|
} |
||||||
|
|
||||||
|
static inline void nat_fixture_cleanup(struct nat_fixture *fixture) { |
||||||
|
for (unsigned i = 0; i < fixture->handle_count; i++) node_conn_direct_force_close(fixture->handles[i]); |
||||||
|
for (unsigned i = 0; i < fixture->node_count; i++) utun_instance_destroy(fixture->nodes[i]); |
||||||
|
uasync_poll(fixture->ua, 0); |
||||||
|
assert(fixture->ua->timer_alloc_count == fixture->ua->timer_free_count); |
||||||
|
assert(fixture->ua->socket_alloc_count == fixture->ua->socket_free_count); |
||||||
|
uasync_destroy(fixture->ua, 0); |
||||||
|
if (u_get_allocated_count() != fixture->allocations) u_report_unfreed_blocks(); |
||||||
|
assert(u_get_allocated_count() == fixture->allocations); |
||||||
|
} |
||||||
|
|
||||||
|
#endif |
||||||
@ -0,0 +1,107 @@ |
|||||||
|
"""Проверка детектора uTun на настоящем NAT; контроль не создаёт контакт с помощником.""" |
||||||
|
|
||||||
|
import json |
||||||
|
|
||||||
|
from topology import NATS, NODES |
||||||
|
|
||||||
|
|
||||||
|
def profile(statistics, protocol): |
||||||
|
return next(item for item in statistics["address_profiles"] if item["socket"] == protocol) |
||||||
|
|
||||||
|
|
||||||
|
def check_profiles(scenario): |
||||||
|
agents = scenario.agents |
||||||
|
public = [node for node in NODES if not node.nat] |
||||||
|
transport = "tcp" if scenario.args.tcp_only else "udp" |
||||||
|
connected = set() |
||||||
|
|
||||||
|
def connect(source, target): |
||||||
|
pair = (source.name, target.name) |
||||||
|
if pair in connected: |
||||||
|
return |
||||||
|
snapshot = agents[target.name].rpc("snapshot")["snapshot"] |
||||||
|
agents[source.name].rpc("nat_link", snapshot=snapshot, socket=transport) |
||||||
|
target_id = agents[target.name].identity |
||||||
|
scenario.wait(f"nat-link-{source.name}-{target.name}", |
||||||
|
lambda data: target_id in data[source.name]["direct_up"], 20) |
||||||
|
connected.add(pair) |
||||||
|
|
||||||
|
def store_owner(owner, observer): |
||||||
|
agents[observer.name].rpc("nat_store", snapshot=agents[owner.name].rpc("snapshot")["snapshot"]) |
||||||
|
|
||||||
|
def measure(owner, observer, protocol): |
||||||
|
before = profile(agents[owner.name].rpc("stats"), protocol) |
||||||
|
agents[owner.name].rpc("nat_check", socket=protocol, observer=agents[observer.name].identity) |
||||||
|
data = scenario.wait(f"nat-profile-{owner.name}-{protocol}", lambda statistics: |
||||||
|
profile(statistics[owner.name], protocol)["rounds"] > before["rounds"] and |
||||||
|
profile(statistics[owner.name], protocol)["phase"] == 0, 40) |
||||||
|
return profile(data[owner.name], protocol) |
||||||
|
|
||||||
|
# Отсутствие второго наблюдателя не превращается в STRICT/DIRECT. |
||||||
|
first, second = public[:2] |
||||||
|
connect(first, second) |
||||||
|
if not scenario.args.tcp_only: |
||||||
|
incomplete = measure(first, second, "udp") |
||||||
|
if (incomplete["mapping"], incomplete["inbound"], incomplete["reason"]) != ("UNKNOWN", "UNKNOWN", "no_helper"): |
||||||
|
raise RuntimeError(f"NAT no-helper test: {incomplete}") |
||||||
|
|
||||||
|
verified = {} |
||||||
|
for owner in public + [node for node in NODES if node.nat]: |
||||||
|
observer = second if owner.name == first.name else first |
||||||
|
if not scenario.args.tcp_only: |
||||||
|
contacts = set(profile(agents[owner.name].rpc("stats"), "udp")["contacts"]) |
||||||
|
helper = next((node for node in public if node.name not in {owner.name, observer.name} and node.ip not in contacts), None) |
||||||
|
if not helper: |
||||||
|
raise RuntimeError(f"{owner.name}: no independent helper remains before cold measurement; contacts={contacts}") |
||||||
|
connect(observer, helper) |
||||||
|
connect(owner, observer) |
||||||
|
store_owner(owner, observer) |
||||||
|
protocols = ["tcp"] if scenario.args.tcp_only else ["udp", "tcp"] if scenario.args.tcp else ["udp"] |
||||||
|
observed = {} |
||||||
|
for protocol in protocols: |
||||||
|
if protocol == "tcp" and owner.nat: |
||||||
|
# Приватный TCP listener не имеет обнаруженного публичного endpoint. |
||||||
|
agents[owner.name].rpc("nat_check", socket="tcp", observer=agents[observer.name].identity) |
||||||
|
data = scenario.wait(f"nat-private-tcp-{owner.name}", lambda statistics: |
||||||
|
profile(statistics[owner.name], "tcp")["reason"] == "no_listener", 5) |
||||||
|
actual = profile(data[owner.name], "tcp") |
||||||
|
expected = ("UNKNOWN", "UNKNOWN") |
||||||
|
else: |
||||||
|
actual = measure(owner, observer, protocol) |
||||||
|
if protocol == "tcp" or not owner.nat: |
||||||
|
expected = ("UNCHANGED", "OPEN") |
||||||
|
else: |
||||||
|
gateway = next(item for item in NATS if item.name == owner.nat) |
||||||
|
expected = {"cone": ("EIM", "OPEN"), "restricted": ("EIM", "CONTACT_REQUIRED"), |
||||||
|
"strict": ("DEPENDENT", "UNKNOWN")}[gateway.mode] |
||||||
|
if (actual["mapping"], actual["inbound"]) != expected: |
||||||
|
raise RuntimeError(f"{owner.name}/{protocol}: expected {expected}, detector returned {actual}") |
||||||
|
if expected[0] == "EIM": |
||||||
|
gateway = next(item for item in NATS if item.name == owner.nat) |
||||||
|
external = f"{gateway.ip}:{owner.mapped_port}" |
||||||
|
elif expected[0] == "UNCHANGED": |
||||||
|
external = f"{owner.ip}:32000" |
||||||
|
else: |
||||||
|
external = "" |
||||||
|
if actual["external"] != external: |
||||||
|
raise RuntimeError(f"{owner.name}/{protocol}: foreign/invalid external endpoint: {actual}, expected {external}") |
||||||
|
observed[protocol] = actual |
||||||
|
print(f"NAT {owner.name}/{protocol}: {actual['mapping']} / {actual['inbound']}" |
||||||
|
f" external={actual['external'] or '-'} reason={actual['reason']}", flush=True) |
||||||
|
verified[owner.name] = observed |
||||||
|
(scenario.network.directory / "nat-profiles.json").write_text(json.dumps(verified, indent=2) + "\n") |
||||||
|
|
||||||
|
# Повтор на использовавшемся порту сохраняет mapping, но не обновляет OPEN |
||||||
|
# по контактировавшим IP. Проверка запускается после всех холодных измерений. |
||||||
|
first_profile = verified[first.name].get("udp") |
||||||
|
if first_profile: |
||||||
|
again = measure(first, second, "udp") |
||||||
|
if again["mapping"] != "UNCHANGED" or again["inbound"] not in {"OPEN", "UNKNOWN"}: |
||||||
|
raise RuntimeError(f"{first.name}: warm measurement produced invalid classification: {again}") |
||||||
|
if again["inbound"] == "UNKNOWN" and again["reason"] != "contact_history": |
||||||
|
raise RuntimeError(f"{first.name}: UNKNOWN without contact-history explanation: {again}") |
||||||
|
|
||||||
|
result = {"nodes_checked": len(verified), "protocols": protocols, "no_helper_checked": not scenario.args.tcp_only, |
||||||
|
"warm_recheck_checked": bool(first_profile), "verified": verified} |
||||||
|
(scenario.network.directory / "nat-result.json").write_text(json.dumps(result, indent=2) + "\n") |
||||||
|
return result |
||||||
@ -1,527 +1,125 @@ |
|||||||
/**
|
/* Детектор владеет профилем сокета; помощник с IP наблюдателя заменяется.
|
||||||
* @file test_nat_detection.c |
* Настоящие ETCP/NCD/PING/STCP, без групповой автосвязности и без подмены времени. */ |
||||||
* @brief Интеграционный тест детекции типа NAT через три узла |
#include "nat_fixture.h" |
||||||
* |
#include "ntp_time.h" |
||||||
* Топология: C1 -> S <- C2 |
|
||||||
* - S получает NODEINFO от C1 (direct peer) |
static void wait_profile(struct nat_fixture *fixture, struct ETCP_SOCKET *socket, uint8_t mapping, uint8_t inbound) { |
||||||
* - S использует C2 как третий узел для NAT-пинга C1 |
uint64_t end = get_time_tb() + 180000; |
||||||
* - C2 пингует C1, результат возвращается S |
while (get_time_tb() < end) { |
||||||
* - S отправляет NAT_INFO C1 |
if (socket->address_profile.evidence_id && socket->address_profile.mapping == mapping && |
||||||
* - Проверяем корректность заполнения всех NAT-полей |
socket->address_profile.inbound == inbound) return; |
||||||
*/ |
uasync_poll(fixture->ua, 10); |
||||||
|
} |
||||||
#include <stdio.h> |
struct nat_check_status status = {0}; |
||||||
#include <stdlib.h> |
nat_detection_get_status(socket->instance->nat_det, socket, &status); |
||||||
#include <string.h> |
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "profile wait failed socket=%s mapping=%s inbound=%s phase=%u reason=%s", |
||||||
#include <unistd.h> |
socket->name, address_mapping_name(socket->address_profile.mapping), |
||||||
#include "test_utils.h" |
address_inbound_name(socket->address_profile.inbound), status.phase, nat_check_reason_name(status.reason)); |
||||||
#include "test_net.h" |
abort(); |
||||||
#include "etcp.h" |
|
||||||
#include "etcp_connections.h" |
|
||||||
#include "../src/config_parser.h" |
|
||||||
#include "../src/config_updater.h" |
|
||||||
#include "../src/utun_instance.h" |
|
||||||
#include "routing.h" |
|
||||||
#include "topo_group.h" |
|
||||||
#include "route_ping.h" |
|
||||||
#include "nat_detection.h" |
|
||||||
#include "topo_node.h" |
|
||||||
#include "../src/tun_if.h" |
|
||||||
#include "secure_channel.h" |
|
||||||
#include "../lib/u_async.h" |
|
||||||
#include "../lib/debug_config.h" |
|
||||||
|
|
||||||
#define TEST_TIMEOUT_MS 5000 |
|
||||||
static uint64_t g_node_id_s = 0; |
|
||||||
static uint64_t g_node_id_c1 = 0; |
|
||||||
static uint64_t g_node_id_c2 = 0; |
|
||||||
|
|
||||||
static struct UTUN_INSTANCE* inst_s = NULL; |
|
||||||
static struct UTUN_INSTANCE* inst_c1 = NULL; |
|
||||||
static struct UTUN_INSTANCE* inst_c2 = NULL; |
|
||||||
static struct UASYNC* ua = NULL; |
|
||||||
static int test_timed_out = 0; |
|
||||||
static void* test_timeout_id = NULL; |
|
||||||
|
|
||||||
static char temp_dir[] = "/tmp/utun_nat_test_XXXXXX"; |
|
||||||
static char config_s[256]; |
|
||||||
static char config_c1[256]; |
|
||||||
static char config_c2[256]; |
|
||||||
|
|
||||||
static struct { |
|
||||||
int done; |
|
||||||
int success; |
|
||||||
uint16_t avg_rtt; |
|
||||||
uint8_t count_sent; |
|
||||||
uint8_t count_ok; |
|
||||||
} nat_ping_result; |
|
||||||
|
|
||||||
static int write_config(const char* path, const char* content) { |
|
||||||
FILE* f = fopen(path, "w"); |
|
||||||
if (!f) return -1; |
|
||||||
fprintf(f, "%s", content); |
|
||||||
fclose(f); |
|
||||||
return 0; |
|
||||||
} |
|
||||||
|
|
||||||
static char* get_pubkey_from_config(const char* path) { |
|
||||||
struct utun_config* cfg = parse_config(path); |
|
||||||
if (!cfg) return NULL; |
|
||||||
char* pub = strdup(cfg->global.my_public_key_hex); |
|
||||||
free_config(cfg); |
|
||||||
return pub; |
|
||||||
} |
|
||||||
|
|
||||||
static int create_temp_configs(void) { |
|
||||||
if (test_mkdtemp(temp_dir) != 0) { |
|
||||||
fprintf(stderr, "Failed to create temp directory\n"); |
|
||||||
return -1; |
|
||||||
} |
|
||||||
snprintf(config_s, sizeof(config_s), "%s/s.conf", temp_dir); |
|
||||||
snprintf(config_c1, sizeof(config_c1), "%s/c1.conf", temp_dir); |
|
||||||
snprintf(config_c2, sizeof(config_c2), "%s/c2.conf", temp_dir); |
|
||||||
|
|
||||||
const char* tpl_s = |
|
||||||
"[global]\n" |
|
||||||
"tun_ip=10.100.0.1/24\n" |
|
||||||
"tun_ifname=tun100\n" |
|
||||||
"\n" |
|
||||||
"[server:test_s]\n" |
|
||||||
"addr=127.0.0.1:0\n" |
|
||||||
"type=public\n" |
|
||||||
"\n" |
|
||||||
"[allowed_keys]\n" |
|
||||||
"allow_all=1\n"; |
|
||||||
if (write_config(config_s, tpl_s) != 0) return -1; |
|
||||||
if (config_ensure_keys_and_node_id(config_s) != 0) return -1; |
|
||||||
char* pub_s = get_pubkey_from_config(config_s); |
|
||||||
if (!pub_s) return -1; |
|
||||||
|
|
||||||
const char* tpl_c2 = |
|
||||||
"[global]\n" |
|
||||||
"tun_ip=10.100.0.3/24\n" |
|
||||||
"tun_ifname=tun103\n" |
|
||||||
"\n" |
|
||||||
"[server:test_c2]\n" |
|
||||||
"addr=127.0.0.1:0\n" |
|
||||||
"type=public\n" |
|
||||||
"\n" |
|
||||||
"[client:to_s]\n" |
|
||||||
"keepalive=1\n" |
|
||||||
"peer_public_key=%s\n" |
|
||||||
"link=test_c2:127.0.0.1:0\n"; |
|
||||||
char tpl_c2_full[4096]; |
|
||||||
snprintf(tpl_c2_full, sizeof(tpl_c2_full), tpl_c2, pub_s); |
|
||||||
if (write_config(config_c2, tpl_c2_full) != 0) { free(pub_s); return -1; } |
|
||||||
if (config_ensure_keys_and_node_id(config_c2) != 0) { free(pub_s); return -1; } |
|
||||||
char* pub_c2 = get_pubkey_from_config(config_c2); |
|
||||||
if (!pub_c2) { free(pub_s); return -1; } |
|
||||||
|
|
||||||
const char* tpl_c1 = |
|
||||||
"[global]\n" |
|
||||||
"tun_ip=10.100.0.2/24\n" |
|
||||||
"tun_ifname=tun102\n" |
|
||||||
"\n" |
|
||||||
"[server:test_c1]\n" |
|
||||||
"addr=127.0.0.1:0\n" |
|
||||||
"type=nat\n" |
|
||||||
"\n" |
|
||||||
"[client:to_s]\n" |
|
||||||
"keepalive=1\n" |
|
||||||
"peer_public_key=%s\n" |
|
||||||
"link=test_c1:127.0.0.1:0\n"; |
|
||||||
char tpl_c1_full[4096]; |
|
||||||
snprintf(tpl_c1_full, sizeof(tpl_c1_full), tpl_c1, pub_s); |
|
||||||
free(pub_s); |
|
||||||
if (write_config(config_c1, tpl_c1_full) != 0) { free(pub_c2); return -1; } |
|
||||||
if (config_ensure_keys_and_node_id(config_c1) != 0) { free(pub_c2); return -1; } |
|
||||||
free(pub_c2); |
|
||||||
|
|
||||||
return 0; |
|
||||||
} |
|
||||||
|
|
||||||
static void cleanup_temp_configs(void) { |
|
||||||
test_unlink(config_s); |
|
||||||
test_unlink(config_c1); |
|
||||||
test_unlink(config_c2); |
|
||||||
test_rmdir(temp_dir); |
|
||||||
} |
|
||||||
|
|
||||||
static int has_initialized_link(struct UTUN_INSTANCE* inst) { |
|
||||||
if (!inst || !inst->connections) return 0; |
|
||||||
struct ll_entry* entry = inst->connections->head; |
|
||||||
while (entry) { |
|
||||||
struct conn_queue_entry* ce = (struct conn_queue_entry*)entry->data; |
|
||||||
struct ETCP_CONN* conn = ce->conn; |
|
||||||
struct ETCP_LINK* link = conn->links; |
|
||||||
while (link) { |
|
||||||
if (link->initialized) return 1; |
|
||||||
link = link->next; |
|
||||||
} |
|
||||||
entry = entry->next; |
|
||||||
} |
|
||||||
return 0; |
|
||||||
} |
} |
||||||
|
|
||||||
static struct ETCP_LINK* find_link_to_node(struct UTUN_INSTANCE* inst, uint64_t node_id) { |
static void check_snapshot(struct UTUN_INSTANCE *owner, struct ETCP_SOCKET *socket) { |
||||||
if (!inst || !inst->connections) return NULL; |
struct TOPO_NODE *node = topo_node_registry_find(owner->topo_groups, owner->node_id); |
||||||
struct ll_entry* entry = queue_find_data_by_index(inst->connections, (const uint8_t*)&node_id); |
assert(node && topo_node_verify(node) == 0); |
||||||
return entry ? ((struct conn_queue_entry*)entry->data)->conn->links : NULL; |
struct TOPO_SOCKMETA4 *meta = node->v4_sock_meta; |
||||||
} |
while (meta && meta->id != socket->sock_id) meta = meta->next; |
||||||
|
assert(meta && meta->address_profile.evidence_id == socket->address_profile.evidence_id && |
||||||
static void test_timeout_cb(void* arg) { |
meta->address_profile.checked_at == socket->address_profile.checked_at && |
||||||
(void)arg; |
meta->address_profile.mapping == socket->address_profile.mapping && |
||||||
test_timed_out = 1; |
meta->address_profile.inbound == socket->address_profile.inbound); |
||||||
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "test_nat_detection: overall test timeout"); |
struct address_profile saved = meta->address_profile; |
||||||
} |
meta->address_profile.checked_at++; |
||||||
|
assert(topo_node_verify(node) < 0); |
||||||
static void nat_ping_resp_cb(int success, uint16_t avg_rtt, uint8_t count_sent, uint8_t count_ok, void* arg) { |
meta->address_profile = saved; |
||||||
(void)arg; |
meta->address_profile.inbound = ADDRESS_IN_CONTACT; |
||||||
nat_ping_result.done = 1; |
assert(topo_node_verify(node) < 0); |
||||||
nat_ping_result.success = success; |
meta->address_profile = saved; assert(topo_node_verify(node) == 0); |
||||||
nat_ping_result.avg_rtt = avg_rtt; |
|
||||||
nat_ping_result.count_sent = count_sent; |
sqlite3_stmt *statement = NULL; |
||||||
nat_ping_result.count_ok = count_ok; |
assert(sqlite3_prepare_v2(owner->topo_sqlite_db, |
||||||
DEBUG_INFO(DEBUG_CATEGORY_BGP, "nat_ping_resp_cb: success=%d avg_rtt=%u sent=%u ok=%u", |
"SELECT nat_mapping,nat_inbound,nat_evidence,nat_checked FROM node_addresses WHERE node_id=? AND socket_id=?", |
||||||
success, (unsigned)avg_rtt, (unsigned)count_sent, (unsigned)count_ok); |
-1, &statement, NULL) == SQLITE_OK); |
||||||
|
sqlite3_bind_int64(statement, 1, (sqlite3_int64)owner->node_id); sqlite3_bind_int(statement, 2, socket->sock_id); |
||||||
|
assert(sqlite3_step(statement) == SQLITE_ROW); |
||||||
|
assert(sqlite3_column_int(statement, 0) == saved.mapping && sqlite3_column_int(statement, 1) == saved.inbound); |
||||||
|
assert((uint64_t)sqlite3_column_int64(statement, 2) == saved.evidence_id); |
||||||
|
assert((uint64_t)sqlite3_column_int64(statement, 3) == saved.checked_at); |
||||||
|
sqlite3_finalize(statement); |
||||||
} |
} |
||||||
|
|
||||||
int main(void) { |
int main(void) { |
||||||
int test_result = 1; |
debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); debug_set_category_level(DEBUG_CATEGORY_NAT, DEBUG_LEVEL_DEBUG); |
||||||
debug_config_init(); |
struct nat_fixture fixture; nat_fixture_init(&fixture); |
||||||
debug_set_level(DEBUG_LEVEL_INFO); |
struct SC_MYKEYS first, second; |
||||||
debug_set_categories(DEBUG_CATEGORY_ETCP | DEBUG_CATEGORY_BGP | DEBUG_CATEGORY_ROUTING); |
assert(sc_generate_keypair(&first) == SC_OK && sc_generate_keypair(&second) == SC_OK); |
||||||
utun_instance_set_tun_init_enabled(0); |
if (sc_derive_node_id_from_pubkey(first.public_key) > sc_derive_node_id_from_pubkey(second.public_key)) { |
||||||
|
struct SC_MYKEYS swap = first; first = second; second = swap; |
||||||
if (create_temp_configs() != 0) { |
} |
||||||
fprintf(stderr, "Failed to create temp configs\n"); |
struct UTUN_INSTANCE *owner = nat_fixture_node(&fixture, "127.0.0.2", NULL, |
||||||
return 1; |
"[server:second]\naddr=127.0.0.5:0\ntype=public\n" |
||||||
} |
"[server:tcp]\naddr=127.0.0.2:0\ntype=public\ntransport=tcp\n"); |
||||||
|
struct UTUN_INSTANCE *observer = nat_fixture_node(&fixture, "127.0.0.3", NULL, NULL); |
||||||
ua = uasync_create(); |
struct UTUN_INSTANCE *same_ip = nat_fixture_node(&fixture, "127.0.0.3", &first, NULL); |
||||||
if (!ua) { |
struct UTUN_INSTANCE *helper = nat_fixture_node(&fixture, "127.0.0.4", &second, NULL); |
||||||
cleanup_temp_configs(); |
struct ETCP_SOCKET *socket = test_etcp_bound_socket(owner, "udp"); |
||||||
return 1; |
struct ETCP_SOCKET *second_socket = test_etcp_bound_socket(owner, "second"); |
||||||
} |
struct ETCP_SOCKET *tcp = test_etcp_bound_socket(owner, "tcp"); |
||||||
|
struct ETCP_SOCKET *observer_socket = test_etcp_bound_socket(observer, "udp"); |
||||||
inst_s = utun_instance_create(ua, config_s); |
assert(socket->sock_id != observer_socket->sock_id); |
||||||
inst_c1 = utun_instance_create(ua, config_c1); |
|
||||||
inst_c2 = utun_instance_create(ua, config_c2); |
/* B уже знает обоих помощниках, но A не отправлял UDP ни одному из них. */ |
||||||
if (!inst_s || !inst_c1 || !inst_c2) { |
nat_fixture_connect(&fixture, observer, "udp", same_ip); |
||||||
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "Failed to create instances"); |
nat_fixture_connect(&fixture, observer, "udp", helper); |
||||||
goto cleanup; |
struct ETCP_CONN *connection = nat_fixture_connect(&fixture, owner, "udp", observer); |
||||||
} |
assert(connection->links && connection->links->remote_socket_id == observer_socket->sock_id); |
||||||
|
assert(connection->links->local_as_seen_by_peer.ss_family == AF_INET); |
||||||
test_etcp_set_peer(inst_c1, "to_s", inst_s, "test_s"); |
assert(route_ping_addr_equal(&connection->links->local_as_seen_by_peer, &socket->local_addr, 1)); |
||||||
test_etcp_set_peer(inst_c2, "to_s", inst_s, "test_s"); |
/* Подписанная запись A нужна независимому испытанию настоящего TCP listener. */ |
||||||
// Разрешаем NAT check для localhost (для теста)
|
nat_fixture_connect(&fixture, observer, "udp", owner); |
||||||
|
owner->ntp.reachable = 0; /* ни измерение, ни публикация не требуют интернет-NTP */ |
||||||
if (utun_instance_init(inst_s) != 0 || utun_instance_init(inst_c1) != 0 || utun_instance_init(inst_c2) != 0) { |
nat_detection_request_check_all(owner->nat_det); |
||||||
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "Failed to init connections"); |
wait_profile(&fixture, socket, ADDRESS_MAP_UNCHANGED, ADDRESS_IN_OPEN); |
||||||
goto cleanup; |
wait_profile(&fixture, second_socket, ADDRESS_MAP_UNCHANGED, ADDRESS_IN_OPEN); |
||||||
} |
wait_profile(&fixture, tcp, ADDRESS_MAP_UNCHANGED, ADDRESS_IN_OPEN); |
||||||
|
|
||||||
if (topo_groups_get_default(inst_s->topo_groups)) topo_group_set_nat_check_local(topo_groups_get_default(inst_s->topo_groups), 1); |
struct nat_check_status status; |
||||||
|
assert(nat_detection_get_status(owner->nat_det, socket, &status) == 0); |
||||||
g_node_id_s = inst_s->node_id; g_node_id_c1 = inst_c1->node_id; g_node_id_c2 = inst_c2->node_id; |
assert(status.helper_reselections > 0 && status.helper == helper->node_id && status.observer == observer->node_id); |
||||||
|
assert(socket->address_profile.evidence_id != second_socket->address_profile.evidence_id); |
||||||
test_timeout_id = uasync_set_timeout(ua, TEST_TIMEOUT_MS * 10, NULL, test_timeout_cb, "test_nat"); |
check_snapshot(owner, socket); |
||||||
|
check_snapshot(owner, second_socket); |
||||||
// 1. Wait for links C1->S and C2->S to initialize
|
check_snapshot(owner, tcp); |
||||||
DEBUG_INFO(DEBUG_CATEGORY_ETCP, "Waiting for ETCP links to initialize..."); |
|
||||||
// Wait for links to be initialized instead of fixed 100 iterations
|
/* Профиль B не заимствует профиль A, даже когда транспорт переиспользуется в обе стороны. */ |
||||||
while (!test_timed_out) { |
assert(!route_ping_addr_equal(&socket->local_addr, &observer_socket->local_addr, 1)); |
||||||
if (has_initialized_link(inst_c1) && has_initialized_link(inst_c2)) { |
assert(!route_ping_addr_equal(&socket->local_addr, &test_etcp_bound_socket(helper, "udp")->local_addr, 1)); |
||||||
DEBUG_INFO(DEBUG_CATEGORY_ETCP, "Links C1->S and C2->S initialized"); |
|
||||||
break; |
/* Повторная проверка не объявляет старый контакт независимым. */ |
||||||
} |
uint64_t old_evidence = socket->address_profile.evidence_id; |
||||||
uasync_poll(ua, 10); |
nat_detection_request_check_all(owner->nat_det); |
||||||
} |
uint64_t end = get_time_tb() + 180000; |
||||||
if (test_timed_out) { |
while (get_time_tb() < end && socket->address_profile.evidence_id == old_evidence) uasync_poll(fixture.ua, 10); |
||||||
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "Timeout waiting for links"); |
assert(socket->address_profile.evidence_id != old_evidence); |
||||||
goto cleanup; |
assert(socket->address_profile.mapping == ADDRESS_MAP_UNCHANGED && socket->address_profile.inbound == ADDRESS_IN_UNKNOWN); |
||||||
} |
|
||||||
|
/* Закрытие endpoint посреди запроса и переиспользование ID/порта: старые ответы не применяются. */ |
||||||
// 2. Wait for BGP exchange so S learns about C1 and C2
|
nat_detection_request_check_all(owner->nat_det); |
||||||
DEBUG_INFO(DEBUG_CATEGORY_BGP, "Waiting for BGP exchange (S learns C1 and C2)..."); |
end = get_time_tb() + 20000; |
||||||
int bgp_wait_cycles = 0; |
do { uasync_poll(fixture.ua, 0); nat_detection_get_status(owner->nat_det, socket, &status); } |
||||||
while (!test_timed_out && bgp_wait_cycles < 500) { |
while (!status.phase && get_time_tb() < end); |
||||||
if (topo_groups_get_default(inst_s->topo_groups) && topo_node_find_by_id(topo_groups_get_default(inst_s->topo_groups), g_node_id_c1) != NULL && |
assert(status.phase); |
||||||
topo_node_find_by_id(topo_groups_get_default(inst_s->topo_groups), g_node_id_c2) != NULL) { |
uint8_t old_id = socket->sock_id; uint64_t old_generation = socket->generation; |
||||||
DEBUG_INFO(DEBUG_CATEGORY_BGP, "S learned about C1 and C2"); |
struct CFG_SERVER recreated; memset(&recreated, 0, sizeof(recreated)); |
||||||
break; |
recreated.ip = socket->local_addr; recreated.type = CFG_SERVER_TYPE_PUBLIC; recreated.mtu = 1500; recreated.fib = -1; |
||||||
} |
snprintf(recreated.name, sizeof(recreated.name), "recreated"); |
||||||
uasync_poll(ua, 10); |
etcp_socket_remove(socket); |
||||||
bgp_wait_cycles++; |
owner->next_socket_id = old_id; |
||||||
} |
socket = etcp_socket_add(owner, &recreated); |
||||||
if (!topo_groups_get_default(inst_s->topo_groups) || topo_node_find_by_id(topo_groups_get_default(inst_s->topo_groups), g_node_id_c1) == NULL || |
assert(socket && socket->sock_id == old_id && socket->generation != old_generation); |
||||||
topo_node_find_by_id(topo_groups_get_default(inst_s->topo_groups), g_node_id_c2) == NULL) { |
end = get_time_tb() + 2000; |
||||||
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "S did not learn about C1/C2 in time"); |
while (get_time_tb() < end) uasync_poll(fixture.ua, 10); |
||||||
goto cleanup; |
assert(socket->address_profile.mapping == ADDRESS_MAP_UNKNOWN && socket->address_profile.inbound == ADDRESS_IN_UNKNOWN); |
||||||
} |
|
||||||
|
nat_fixture_cleanup(&fixture); |
||||||
// Verify C1 socket has config type before NAT detection
|
return 0; |
||||||
{ |
|
||||||
struct ETCP_SOCKET* s = inst_c1->etcp_sockets; |
|
||||||
if (!s || s->type != CFG_SERVER_TYPE_NAT) { |
|
||||||
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "FAIL: C1 socket type=%d, expected CFG_SERVER_TYPE_NAT(%d) before detection", |
|
||||||
s ? (int)s->type : -1, CFG_SERVER_TYPE_NAT); |
|
||||||
goto cleanup; |
|
||||||
} |
|
||||||
DEBUG_INFO(DEBUG_CATEGORY_BGP, "C1 socket has config NAT type before detection"); |
|
||||||
} |
|
||||||
|
|
||||||
// 3. Wait for NAT detection to complete for C1 - check link on server
|
|
||||||
struct ETCP_LINK* link_sc1 = find_link_to_node(inst_s, g_node_id_c1); |
|
||||||
if (!link_sc1) { |
|
||||||
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "No link to C1 on server"); |
|
||||||
goto cleanup; |
|
||||||
} |
|
||||||
DEBUG_INFO(DEBUG_CATEGORY_BGP, "Waiting for NAT detection to complete for C1..."); |
|
||||||
bgp_wait_cycles = 0; |
|
||||||
while (!test_timed_out && bgp_wait_cycles < 1500) { |
|
||||||
if (link_sc1->nat_check_status == NAT_CHECK_EIM) { |
|
||||||
DEBUG_INFO(DEBUG_CATEGORY_BGP, "NAT detection completed for C1: EIM"); |
|
||||||
break; |
|
||||||
} |
|
||||||
uasync_poll(ua, 10); |
|
||||||
bgp_wait_cycles++; |
|
||||||
} |
|
||||||
if (!link_sc1 || link_sc1->nat_check_status != NAT_CHECK_EIM) { |
|
||||||
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "NAT detection did not complete for C1"); |
|
||||||
goto cleanup; |
|
||||||
} |
|
||||||
|
|
||||||
// 4. Wait for C1 to receive NAT_INFO (verified type)
|
|
||||||
DEBUG_INFO(DEBUG_CATEGORY_BGP, "Waiting for C1 to receive NAT_INFO..."); |
|
||||||
bgp_wait_cycles = 0; |
|
||||||
while (!test_timed_out && bgp_wait_cycles < 500) { |
|
||||||
struct ETCP_SOCKET* sock = inst_c1->etcp_sockets; |
|
||||||
int found = 0; |
|
||||||
while (sock) { |
|
||||||
if (sock->nat_type == NAT_VERIFIED_EIM) { found = 1; break; } |
|
||||||
sock = sock->next; |
|
||||||
} |
|
||||||
if (found) { |
|
||||||
DEBUG_INFO(DEBUG_CATEGORY_BGP, "C1 received NAT_INFO"); |
|
||||||
/* Даем время для получения NODEINFO с обновленным local_node */ |
|
||||||
for (int i = 0; i < 50 && !test_timed_out; i++) { |
|
||||||
uasync_poll(ua, 10); |
|
||||||
} |
|
||||||
break; |
|
||||||
} |
|
||||||
uasync_poll(ua, 10); |
|
||||||
bgp_wait_cycles++; |
|
||||||
} |
|
||||||
|
|
||||||
// 5. Verify all NAT fields on server
|
|
||||||
struct TOPO_GROUP_NODE* node_c1 = topo_node_find_by_id(topo_groups_get_default(inst_s->topo_groups), g_node_id_c1); |
|
||||||
if (!node_c1) { |
|
||||||
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "TOPO_GROUP_NODE for C1 disappeared"); |
|
||||||
goto cleanup; |
|
||||||
} |
|
||||||
if (link_sc1->nat_check_status != NAT_CHECK_EIM) { |
|
||||||
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "FAIL: nat_check_status=%d, expected EIM(%d)", |
|
||||||
(int)link_sc1->nat_check_status, NAT_CHECK_EIM); |
|
||||||
goto cleanup; |
|
||||||
} |
|
||||||
if (link_sc1->nat_type != NAT_TYPE_EIM) { |
|
||||||
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "FAIL: nat_type=%d, expected EIM(%d)", |
|
||||||
(int)link_sc1->nat_type, NAT_TYPE_EIM); |
|
||||||
goto cleanup; |
|
||||||
} |
|
||||||
if (link_sc1->nat_ip == 0 || link_sc1->nat_port == 0) { |
|
||||||
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "FAIL: nat_ip/port not set (ip=%08x port=%u)", |
|
||||||
(unsigned)link_sc1->nat_ip, (unsigned)link_sc1->nat_port); |
|
||||||
goto cleanup; |
|
||||||
} |
|
||||||
|
|
||||||
// Verify link nat_type on server
|
|
||||||
struct ETCP_CONN* conn_sc1 = NULL; |
|
||||||
{ struct ll_entry* e = inst_s->connections->head; |
|
||||||
while (e) { |
|
||||||
struct conn_queue_entry* ce = (struct conn_queue_entry*)e->data; |
|
||||||
if (ce->conn->peer_node_id == g_node_id_c1) { conn_sc1 = ce->conn; break; } |
|
||||||
e = e->next; |
|
||||||
} } |
|
||||||
if (!link_sc1 || link_sc1->nat_type != NAT_TYPE_EIM) { |
|
||||||
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "FAIL: link nat_type not EIM on server"); |
|
||||||
goto cleanup; |
|
||||||
} |
|
||||||
|
|
||||||
// Verify socket nat_type on C1
|
|
||||||
struct ETCP_SOCKET* sock_c1 = NULL; |
|
||||||
struct ETCP_SOCKET* sock = inst_c1->etcp_sockets; |
|
||||||
while (sock) { |
|
||||||
if (sock->nat_type == NAT_VERIFIED_EIM) { |
|
||||||
sock_c1 = sock; |
|
||||||
break; |
|
||||||
} |
|
||||||
sock = sock->next; |
|
||||||
} |
|
||||||
if (!sock_c1) { |
|
||||||
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "FAIL: no socket with NAT_VERIFIED_EIM on C1"); |
|
||||||
goto cleanup; |
|
||||||
} |
|
||||||
|
|
||||||
// Verify local_node socket update on C1 (received NAT_INFO updated nodeinfo)
|
|
||||||
if (topo_groups_get_default(inst_c1->topo_groups) && topo_groups_get_default(inst_c1->topo_groups)->local_node) { |
|
||||||
struct TOPO_NODE* lni = topo_node_registry_find(inst_c1->topo_groups, topo_groups_get_default(inst_c1->topo_groups)->local_node->node_id); |
|
||||||
if (lni) { |
|
||||||
int found = 0; |
|
||||||
for (const struct TOPO_SOCKMETA4* m = lni->v4_sock_meta; m; m = m->next) { |
|
||||||
if (m->id == sock_c1->sock_id) { |
|
||||||
if (m->nat_type != NAT_VERIFIED_EIM) { |
|
||||||
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "FAIL: local_node socket meta not updated on C1: nat_type=%u expected=%u", m->nat_type, NAT_VERIFIED_EIM); |
|
||||||
goto cleanup; |
|
||||||
} |
|
||||||
found = 1; break; |
|
||||||
} |
|
||||||
} |
|
||||||
if (!found) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "FAIL: socket not found in local_node meta on C1"); goto cleanup; } |
|
||||||
// Verify NAT addr in flat address list
|
|
||||||
int nat_found = 0; |
|
||||||
for (const struct TOPO_ADDR4* a = lni->v4_addrs; a; a = a->next) { |
|
||||||
if (a->type == TOPO_ADDR_NAT && a->socket_id == sock_c1->sock_id) { |
|
||||||
uint32_t addr_ip; memcpy(&addr_ip, a->addr, 4); |
|
||||||
if (addr_ip != link_sc1->nat_ip || a->port != link_sc1->nat_port) { |
|
||||||
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "FAIL: NAT addr mismatch in local_node: addr=%08x nat_ip=%08x port=%u nat_port=%u", |
|
||||||
addr_ip, link_sc1->nat_ip, a->port, link_sc1->nat_port); |
|
||||||
goto cleanup; |
|
||||||
} |
|
||||||
nat_found = 1; break; |
|
||||||
} |
|
||||||
} |
|
||||||
if (!nat_found) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "FAIL: NAT addr not found in local_node flat addr list on C1"); goto cleanup; } |
|
||||||
DEBUG_INFO(DEBUG_CATEGORY_BGP, "local_node meta+addr check PASSED on C1"); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Wait for C2 to learn updated C1 nodeinfo (with verified NAT type)
|
|
||||||
DEBUG_INFO(DEBUG_CATEGORY_BGP, "Waiting for C2 to learn updated C1 nodeinfo..."); |
|
||||||
bgp_wait_cycles = 0; |
|
||||||
int c2_verified_nat = 0; |
|
||||||
struct TOPO_GROUP_NODE* node_c1_on_c2 = NULL; |
|
||||||
struct TOPO_NODE* cni = NULL; |
|
||||||
while (!test_timed_out && bgp_wait_cycles < 500) { |
|
||||||
node_c1_on_c2 = topo_groups_get_default(inst_c2->topo_groups) ? topo_node_find_by_id(topo_groups_get_default(inst_c2->topo_groups), g_node_id_c1) : NULL; |
|
||||||
cni = node_c1_on_c2 ? topo_node_registry_find(inst_c2->topo_groups, node_c1_on_c2->node_id) : NULL; |
|
||||||
if (node_c1_on_c2 && cni) { |
|
||||||
for (const struct TOPO_SOCKMETA4* m = cni->v4_sock_meta; m; m = m->next) { |
|
||||||
if (m->nat_type == NAT_VERIFIED_EIM) { c2_verified_nat = 1; break; } |
|
||||||
} |
|
||||||
if (c2_verified_nat) break; |
|
||||||
} |
|
||||||
uasync_poll(ua, 10); |
|
||||||
bgp_wait_cycles++; |
|
||||||
} |
|
||||||
if (!c2_verified_nat) { |
|
||||||
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "FAIL: C2 did not receive updated C1 nodeinfo with verified NAT"); |
|
||||||
goto cleanup; |
|
||||||
} |
|
||||||
DEBUG_INFO(DEBUG_CATEGORY_BGP, "C2 received updated C1 nodeinfo with verified NAT"); |
|
||||||
|
|
||||||
// 6. Separate STUN check via explicit route_ping_send_req_addr
|
|
||||||
DEBUG_INFO(DEBUG_CATEGORY_BGP, "Performing explicit STUN ping from S via C2 to C1..."); |
|
||||||
struct ETCP_CONN* conn_sc2 = NULL; |
|
||||||
struct ll_entry* entry = inst_s->connections->head; |
|
||||||
while (entry) { |
|
||||||
struct conn_queue_entry* ce = (struct conn_queue_entry*)entry->data; |
|
||||||
if (ce->conn->peer_node_id == g_node_id_c2) { conn_sc2 = ce->conn; break; } |
|
||||||
entry = entry->next; |
|
||||||
} |
|
||||||
if (!conn_sc2) { |
|
||||||
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "FAIL: S has no connection to C2"); |
|
||||||
goto cleanup; |
|
||||||
} |
|
||||||
|
|
||||||
memset(&nat_ping_result, 0, sizeof(nat_ping_result)); |
|
||||||
/* nat_ip уже в network byte order, API тоже принимает NBO */ |
|
||||||
uint32_t target_ip_net = link_sc1->nat_ip; |
|
||||||
DEBUG_INFO(DEBUG_CATEGORY_BGP, "TEST: sending ping with pubkey, nat_ip_net=0x%08x nat_port=%u", |
|
||||||
link_sc1->nat_ip, link_sc1->nat_port); |
|
||||||
int ret = route_ping_send_req_addr(inst_s->nat_det, topo_groups_get_default(inst_s->topo_groups), conn_sc2, |
|
||||||
target_ip_net, link_sc1->nat_port, |
|
||||||
3, 10, 200, 3000, nat_ping_resp_cb, NULL, |
|
||||||
node_c1_on_c2 ? topo_node_registry_find(inst_c2->topo_groups, node_c1_on_c2->node_id)->public_key : NULL); |
|
||||||
if (ret != 0) { |
|
||||||
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "route_ping_send_req_addr failed: %d", ret); |
|
||||||
goto cleanup; |
|
||||||
} |
|
||||||
|
|
||||||
bgp_wait_cycles = 0; |
|
||||||
while (!test_timed_out && bgp_wait_cycles < 500 && !nat_ping_result.done) { |
|
||||||
uasync_poll(ua, 10); |
|
||||||
bgp_wait_cycles++; |
|
||||||
} |
|
||||||
if (!nat_ping_result.done) { |
|
||||||
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "FAIL: STUN ping timeout"); |
|
||||||
goto cleanup; |
|
||||||
} |
|
||||||
if (!nat_ping_result.success || nat_ping_result.count_ok == 0) { |
|
||||||
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "FAIL: STUN ping failed success=%d ok=%u", |
|
||||||
nat_ping_result.success, (unsigned)nat_ping_result.count_ok); |
|
||||||
goto cleanup; |
|
||||||
} |
|
||||||
DEBUG_INFO(DEBUG_CATEGORY_BGP, "STUN check PASSED"); |
|
||||||
|
|
||||||
// 7. Test route_ping_send_req_addr WITHOUT embedded pubkey (C2 should resolve pubkey from node_id)
|
|
||||||
// First wait for C2 to learn C1's nodeinfo via BGP exchange from S
|
|
||||||
DEBUG_INFO(DEBUG_CATEGORY_BGP, "Waiting for C2 to learn C1 nodeinfo..."); |
|
||||||
bgp_wait_cycles = 0; |
|
||||||
while (!test_timed_out && bgp_wait_cycles < 500) { |
|
||||||
if (topo_groups_get_default(inst_c2->topo_groups) && topo_node_find_by_id(topo_groups_get_default(inst_c2->topo_groups), g_node_id_c1) != NULL) { |
|
||||||
DEBUG_INFO(DEBUG_CATEGORY_BGP, "C2 learned C1 nodeinfo"); |
|
||||||
break; |
|
||||||
} |
|
||||||
uasync_poll(ua, 10); |
|
||||||
bgp_wait_cycles++; |
|
||||||
} |
|
||||||
if (!topo_groups_get_default(inst_c2->topo_groups) || topo_node_find_by_id(topo_groups_get_default(inst_c2->topo_groups), g_node_id_c1) == NULL) { |
|
||||||
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "FAIL: C2 did not learn C1 nodeinfo in time"); |
|
||||||
goto cleanup; |
|
||||||
} |
|
||||||
|
|
||||||
DEBUG_INFO(DEBUG_CATEGORY_BGP, "Testing route ping without embedded pubkey..."); |
|
||||||
memset(&nat_ping_result, 0, sizeof(nat_ping_result)); |
|
||||||
/* nat_ip уже в network byte order, API тоже принимает NBO */ |
|
||||||
target_ip_net = link_sc1->nat_ip; |
|
||||||
DEBUG_INFO(DEBUG_CATEGORY_BGP, "TEST: sending ping NO pubkey, nat_ip_net=0x%08x nat_port=%u", |
|
||||||
link_sc1->nat_ip, link_sc1->nat_port); |
|
||||||
ret = route_ping_send_req_addr(inst_s->nat_det, topo_groups_get_default(inst_s->topo_groups), conn_sc2, |
|
||||||
target_ip_net, link_sc1->nat_port, |
|
||||||
3, 10, 200, 3000, nat_ping_resp_cb, NULL, |
|
||||||
cni->public_key); |
|
||||||
if (ret != 0) { |
|
||||||
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "route_ping_send_req_addr (no pubkey) failed: %d", ret); |
|
||||||
goto cleanup; |
|
||||||
} |
|
||||||
|
|
||||||
bgp_wait_cycles = 0; |
|
||||||
while (!test_timed_out && bgp_wait_cycles < 500 && !nat_ping_result.done) { |
|
||||||
uasync_poll(ua, 10); |
|
||||||
bgp_wait_cycles++; |
|
||||||
} |
|
||||||
if (!nat_ping_result.done) { |
|
||||||
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "FAIL: no-pubkey ping timeout"); |
|
||||||
goto cleanup; |
|
||||||
} |
|
||||||
if (!nat_ping_result.success || nat_ping_result.count_ok == 0) { |
|
||||||
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "FAIL: no-pubkey ping failed success=%d ok=%u", |
|
||||||
nat_ping_result.success, (unsigned)nat_ping_result.count_ok); |
|
||||||
goto cleanup; |
|
||||||
} |
|
||||||
DEBUG_INFO(DEBUG_CATEGORY_BGP, "No-pubkey ping check PASSED"); |
|
||||||
|
|
||||||
DEBUG_INFO(DEBUG_CATEGORY_BGP, "NAT detection test PASSED"); |
|
||||||
test_result = 0; |
|
||||||
|
|
||||||
cleanup: |
|
||||||
if (test_timeout_id) uasync_cancel_timeout(ua, test_timeout_id); |
|
||||||
if (inst_s) utun_instance_destroy(inst_s); |
|
||||||
if (inst_c1) utun_instance_destroy(inst_c1); |
|
||||||
if (inst_c2) utun_instance_destroy(inst_c2); |
|
||||||
if (ua) uasync_destroy(ua, 0); |
|
||||||
cleanup_temp_configs(); |
|
||||||
return test_result; |
|
||||||
} |
} |
||||||
|
|||||||
@ -1,341 +1,98 @@ |
|||||||
/**
|
/* Настоящие UDP-наблюдения и отклонение PONG чужого ключа/поколения. */ |
||||||
* @file test_route_ping.c |
#include "nat_fixture.h" |
||||||
* @brief Интеграционный тест route_ping через три инстанса |
|
||||||
* |
|
||||||
* Топология: A -> B -> C |
|
||||||
* - A отправляет запрос route_ping на B для узла C |
|
||||||
* - B пингует C через ETCP ping |
|
||||||
* - C отвечает PONG |
|
||||||
* - B отправляет BGP_PING_RESPONSE на A |
|
||||||
* - A получает ответ через route_ping_send_req callback |
|
||||||
*/ |
|
||||||
|
|
||||||
#include <stdio.h> |
struct result { int done, success; uint8_t data[128]; size_t length; }; |
||||||
#include <stdlib.h> |
|
||||||
#include <string.h> |
|
||||||
#include <unistd.h> |
|
||||||
#include "test_utils.h" |
|
||||||
#include "test_net.h" |
|
||||||
#include "etcp.h" |
|
||||||
#include "etcp_connections.h" |
|
||||||
#include "../src/config_parser.h" |
|
||||||
#include "../src/config_updater.h" |
|
||||||
#include "../src/utun_instance.h" |
|
||||||
#include "routing.h" |
|
||||||
#include "topo_group.h" |
|
||||||
#include "route_ping.h" |
|
||||||
#include "../src/tun_if.h" |
|
||||||
#include "secure_channel.h" |
|
||||||
#include "../lib/u_async.h" |
|
||||||
#include "../lib/debug_config.h" |
|
||||||
|
|
||||||
#define TEST_TIMEOUT_MS 10000 |
static void ping_done(int success, uint16_t rtt, void *arg, uint64_t nonce, const uint8_t *data, size_t length) { |
||||||
static uint64_t g_node_id_a = 0; |
(void)rtt; (void)nonce; |
||||||
static uint64_t g_node_id_b = 0; |
struct result *result = arg; result->done++; result->success = success; result->length = length; |
||||||
static uint64_t g_node_id_c = 0; |
assert(length <= sizeof(result->data)); if (length) memcpy(result->data, data, length); |
||||||
|
|
||||||
static struct UTUN_INSTANCE* inst_a = NULL; |
|
||||||
static struct UTUN_INSTANCE* inst_b = NULL; |
|
||||||
static struct UTUN_INSTANCE* inst_c = NULL; |
|
||||||
static struct UASYNC* ua = NULL; |
|
||||||
static int test_timed_out = 0; |
|
||||||
static void* test_timeout_id = NULL; |
|
||||||
|
|
||||||
static char temp_dir[] = "/tmp/utun_route_ping_test_XXXXXX"; |
|
||||||
static char config_a[256]; |
|
||||||
static char config_b[256]; |
|
||||||
static char config_c[256]; |
|
||||||
|
|
||||||
static struct { |
|
||||||
int done; |
|
||||||
int success; |
|
||||||
uint16_t avg_rtt; |
|
||||||
uint8_t count_sent; |
|
||||||
uint8_t count_ok; |
|
||||||
} ping_result; |
|
||||||
|
|
||||||
static int write_config(const char* path, const char* content) { |
|
||||||
FILE* f = fopen(path, "w"); |
|
||||||
if (!f) return -1; |
|
||||||
fprintf(f, "%s", content); |
|
||||||
fclose(f); |
|
||||||
return 0; |
|
||||||
} |
|
||||||
|
|
||||||
static char* get_pubkey_from_config(const char* path) { |
|
||||||
struct utun_config* cfg = parse_config(path); |
|
||||||
if (!cfg) return NULL; |
|
||||||
char* pub = strdup(cfg->global.my_public_key_hex); |
|
||||||
free_config(cfg); |
|
||||||
return pub; |
|
||||||
} |
|
||||||
|
|
||||||
static int create_temp_configs(void) { |
|
||||||
if (test_mkdtemp(temp_dir) != 0) { |
|
||||||
fprintf(stderr, "Failed to create temp directory\n"); |
|
||||||
return -1; |
|
||||||
} |
|
||||||
snprintf(config_a, sizeof(config_a), "%s/a.conf", temp_dir); |
|
||||||
snprintf(config_b, sizeof(config_b), "%s/b.conf", temp_dir); |
|
||||||
snprintf(config_c, sizeof(config_c), "%s/c.conf", temp_dir); |
|
||||||
|
|
||||||
// 1. Config C (no keys)
|
|
||||||
const char* tpl_c = |
|
||||||
"[global]\n" |
|
||||||
"tun_ip=10.100.0.3/24\n" |
|
||||||
"tun_ifname=tun102\n" |
|
||||||
"\n" |
|
||||||
"[server:test_c]\n" |
|
||||||
"addr=127.0.0.1:0\n" |
|
||||||
"type=public\n" |
|
||||||
"\n" |
|
||||||
"[allowed_keys]\n" |
|
||||||
"allow_all=1\n"; |
|
||||||
if (write_config(config_c, tpl_c) != 0) return -1; |
|
||||||
if (config_ensure_keys_and_node_id(config_c) != 0) return -1; |
|
||||||
char* pub_c = get_pubkey_from_config(config_c); |
|
||||||
if (!pub_c) return -1; |
|
||||||
|
|
||||||
// 2. Config B (no own keys, but needs peer_public_key of C)
|
|
||||||
char tpl_b[4096]; |
|
||||||
snprintf(tpl_b, sizeof(tpl_b), |
|
||||||
"[global]\n" |
|
||||||
"tun_ip=10.100.0.2/24\n" |
|
||||||
"tun_ifname=tun101\n" |
|
||||||
"\n" |
|
||||||
"[server:test_b]\n" |
|
||||||
"addr=127.0.0.1:0\n" |
|
||||||
"type=public\n" |
|
||||||
"\n" |
|
||||||
"[allowed_keys]\n" |
|
||||||
"allow_all=1\n" |
|
||||||
"\n" |
|
||||||
"[client:to_c]\n" |
|
||||||
"keepalive=1\n" |
|
||||||
"peer_public_key=%s\n" |
|
||||||
"link=test_b:127.0.0.1:0\n", |
|
||||||
pub_c); |
|
||||||
free(pub_c); |
|
||||||
if (write_config(config_b, tpl_b) != 0) return -1; |
|
||||||
if (config_ensure_keys_and_node_id(config_b) != 0) return -1; |
|
||||||
char* pub_b = get_pubkey_from_config(config_b); |
|
||||||
if (!pub_b) return -1; |
|
||||||
|
|
||||||
// 3. Config A (no own keys, but needs peer_public_key of B)
|
|
||||||
char tpl_a[4096]; |
|
||||||
snprintf(tpl_a, sizeof(tpl_a), |
|
||||||
"[global]\n" |
|
||||||
"tun_ip=10.100.0.1/24\n" |
|
||||||
"tun_ifname=tun100\n" |
|
||||||
"\n" |
|
||||||
"[server:test_a]\n" |
|
||||||
"addr=127.0.0.1:0\n" |
|
||||||
"type=public\n" |
|
||||||
"\n" |
|
||||||
"[allowed_keys]\n" |
|
||||||
"allow_all=1\n" |
|
||||||
"\n" |
|
||||||
"[client:to_b]\n" |
|
||||||
"keepalive=1\n" |
|
||||||
"peer_public_key=%s\n" |
|
||||||
"link=test_a:127.0.0.1:0\n", |
|
||||||
pub_b); |
|
||||||
free(pub_b); |
|
||||||
if (write_config(config_a, tpl_a) != 0) return -1; |
|
||||||
if (config_ensure_keys_and_node_id(config_a) != 0) return -1; |
|
||||||
|
|
||||||
return 0; |
|
||||||
} |
|
||||||
|
|
||||||
static void cleanup_temp_configs(void) { |
|
||||||
test_unlink(config_a); |
|
||||||
test_unlink(config_b); |
|
||||||
test_unlink(config_c); |
|
||||||
test_rmdir(temp_dir); |
|
||||||
} |
|
||||||
|
|
||||||
static int has_initialized_link(struct UTUN_INSTANCE* inst) { |
|
||||||
if (!inst || !inst->connections) return 0; |
|
||||||
struct ll_entry* entry = inst->connections->head; |
|
||||||
while (entry) { |
|
||||||
struct conn_queue_entry* ce = (struct conn_queue_entry*)entry->data; |
|
||||||
struct ETCP_CONN* conn = ce->conn; |
|
||||||
struct ETCP_LINK* link = conn->links; |
|
||||||
while (link) { |
|
||||||
if (link->initialized) return 1; |
|
||||||
link = link->next; |
|
||||||
} |
|
||||||
entry = entry->next; |
|
||||||
} |
|
||||||
return 0; |
|
||||||
} |
} |
||||||
|
|
||||||
static struct ETCP_CONN* find_conn_to_peer(struct UTUN_INSTANCE* inst, uint64_t peer_node_id) { |
static void wait_result(struct nat_fixture *fixture, struct result *result) { |
||||||
if (!inst || !inst->connections) return NULL; |
uint64_t end = get_time_tb() + 30000; |
||||||
struct ll_entry* entry = queue_find_data_by_index(inst->connections, (const uint8_t*)&peer_node_id); |
while (!result->done && get_time_tb() < end) uasync_poll(fixture->ua, 10); |
||||||
return entry ? ((struct conn_queue_entry*)entry->data)->conn : NULL; |
assert(result->done == 1); |
||||||
} |
} |
||||||
|
|
||||||
static void test_timeout_cb(void* arg) { |
/* Аутентичный PONG от другого узла с известным nonce. Его source совпадает с
|
||||||
(void)arg; |
* target ожидающего запроса, поэтому ключ пира обязан проверяться отдельно. */ |
||||||
test_timed_out = 1; |
static void send_foreign_pong(struct UTUN_INSTANCE *sender, struct ETCP_SOCKET *destination, uint64_t nonce) { |
||||||
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "test_route_ping: overall test timeout"); |
uint8_t plain[23] = {0}, encrypted[256], salt[8], obfuscated[32]; |
||||||
|
plain[2] = 1; plain[3] = ETCP_PONG; |
||||||
|
for (unsigned i = 0; i < 8; i++) { |
||||||
|
plain[5 + i] = (uint8_t)(sender->node_id >> (56 - i * 8)); |
||||||
|
plain[13 + i] = (uint8_t)(nonce >> (56 - i * 8)); |
||||||
|
} |
||||||
|
sc_context_t crypto; assert(sc_init_ctx(&crypto, &sender->my_keys) == SC_OK); |
||||||
|
assert(sc_set_peer_public_key(&crypto, destination->instance->my_keys.public_key, SC_PEER_PUBKEY_BIN) == SC_OK); |
||||||
|
size_t length = 0; assert(sc_encrypt(&crypto, plain, sizeof(plain), encrypted, &length) == SC_OK); |
||||||
|
assert(random_bytes(salt, sizeof(salt)) == 0); |
||||||
|
sc_obfuscate_pubkey(salt, destination->instance->my_keys.public_key, sender->my_keys.public_key, obfuscated); |
||||||
|
memcpy(encrypted + length, salt, sizeof(salt)); memcpy(encrypted + length + sizeof(salt), obfuscated, sizeof(obfuscated)); |
||||||
|
length += sizeof(salt) + sizeof(obfuscated); |
||||||
|
struct ETCP_SOCKET *socket = test_etcp_bound_socket(sender, "udp"); |
||||||
|
assert(socket_sendto(socket->fd, encrypted, length, (const struct sockaddr *)&destination->local_addr, |
||||||
|
sizeof(struct sockaddr_in)) == (ssize_t)length); |
||||||
} |
} |
||||||
|
|
||||||
static void ping_resp_cb(int success, uint16_t avg_rtt, uint8_t count_sent, uint8_t count_ok, void* arg) { |
static void address_encoding(void) { |
||||||
(void)arg; |
struct sockaddr_storage address = {0}, decoded; |
||||||
ping_result.done = 1; |
struct sockaddr_in *v4 = (struct sockaddr_in *)&address; |
||||||
ping_result.success = success; |
v4->sin_family = AF_INET; assert(inet_pton(AF_INET, "198.51.100.7", &v4->sin_addr) == 1); v4->sin_port = htons(1300); |
||||||
ping_result.avg_rtt = avg_rtt; |
uint8_t wire[ROUTE_ADDR_WIRE_SIZE]; |
||||||
ping_result.count_sent = count_sent; |
static const uint8_t expected[19] = {4,198,51,100,7,0,0,0,0,0,0,0,0,0,0,0,0,5,20}; |
||||||
ping_result.count_ok = count_ok; |
assert(route_ping_addr_encode(wire, &address) == 0 && !memcmp(wire, expected, sizeof(wire))); |
||||||
DEBUG_INFO(DEBUG_CATEGORY_BGP, "ping_resp_cb: success=%d avg_rtt=%u sent=%u ok=%u", |
assert(route_ping_addr_decode(wire, &decoded) == 0 && route_ping_addr_equal(&address, &decoded, 1)); |
||||||
success, (unsigned)avg_rtt, (unsigned)count_sent, (unsigned)count_ok); |
wire[5] = 1; assert(route_ping_addr_decode(wire, &decoded) < 0); /* скрытые байты IPv4 запрещены */ |
||||||
|
memset(&address, 0, sizeof(address)); |
||||||
|
struct sockaddr_in6 *v6 = (struct sockaddr_in6 *)&address; |
||||||
|
v6->sin6_family = AF_INET6; v6->sin6_port = htons(0xabcd); |
||||||
|
assert(inet_pton(AF_INET6, "2001:db8::1234", &v6->sin6_addr) == 1); |
||||||
|
assert(route_ping_addr_encode(wire, &address) == 0 && wire[0] == 6 && wire[17] == 0xab && wire[18] == 0xcd); |
||||||
|
assert(route_ping_addr_decode(wire, &decoded) == 0 && route_ping_addr_equal(&address, &decoded, 1)); |
||||||
|
wire[17] = wire[18] = 0; assert(route_ping_addr_decode(wire, &decoded) < 0); |
||||||
} |
} |
||||||
|
|
||||||
int main(void) { |
int main(void) { |
||||||
int test_result = 1; |
debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); debug_set_category_level(DEBUG_CATEGORY_NAT, DEBUG_LEVEL_DEBUG); |
||||||
debug_config_init(); |
address_encoding(); |
||||||
debug_set_level(DEBUG_LEVEL_INFO); |
struct nat_fixture fixture; nat_fixture_init(&fixture); |
||||||
debug_set_categories(DEBUG_CATEGORY_ETCP | DEBUG_CATEGORY_BGP | DEBUG_CATEGORY_ROUTING); |
struct UTUN_INSTANCE *owner = nat_fixture_node(&fixture, "127.0.0.2", NULL, NULL); |
||||||
utun_instance_set_tun_init_enabled(0); |
struct UTUN_INSTANCE *observer = nat_fixture_node(&fixture, "127.0.0.3", NULL, NULL); |
||||||
|
struct UTUN_INSTANCE *foreign = nat_fixture_node(&fixture, "127.0.0.4", NULL, NULL); |
||||||
if (create_temp_configs() != 0) { |
struct ETCP_SOCKET *socket = test_etcp_bound_socket(owner, "udp"); |
||||||
fprintf(stderr, "Failed to create temp configs\n"); |
struct ETCP_SOCKET *peer = test_etcp_bound_socket(observer, "udp"); |
||||||
return 1; |
const uint64_t round = UINT64_C(0x0102030405060708); |
||||||
} |
struct result result = {0}; |
||||||
|
assert(route_ping_observe(socket, observer->my_keys.public_key, &peer->local_addr, round, 500, ping_done, &result) == 0); |
||||||
ua = uasync_create(); |
wait_result(&fixture, &result); |
||||||
if (!ua) { |
struct sockaddr_storage observed; |
||||||
cleanup_temp_configs(); |
assert(result.success && route_ping_observation(result.data, result.length, socket, round, &observed) == 0); |
||||||
return 1; |
assert(route_ping_addr_equal(&observed, &socket->local_addr, 1)); |
||||||
} |
assert(route_ping_observation(result.data, result.length, socket, round + 1, &observed) < 0); |
||||||
|
assert(socket->address_profile.mapping == ADDRESS_MAP_UNKNOWN); /* один ответ не назначает NAT-тип */ |
||||||
inst_a = utun_instance_create(ua, config_a); |
|
||||||
inst_b = utun_instance_create(ua, config_b); |
memset(&result, 0, sizeof(result)); |
||||||
inst_c = utun_instance_create(ua, config_c); |
struct ETCP_SOCKET *untrusted = test_etcp_bound_socket(foreign, "udp"); |
||||||
if (!inst_a || !inst_b || !inst_c) { |
assert(route_ping_observe(socket, observer->my_keys.public_key, &untrusted->local_addr, |
||||||
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "Failed to create instances"); |
round + 1, 300, ping_done, &result) == 0); |
||||||
goto cleanup; |
assert(owner->pending_pings); |
||||||
} |
send_foreign_pong(foreign, socket, owner->pending_pings->nonce); |
||||||
|
wait_result(&fixture, &result); |
||||||
test_etcp_set_peer(inst_a, "to_b", inst_b, "test_b"); |
assert(!result.success && !owner->pending_pings); |
||||||
test_etcp_set_peer(inst_b, "to_c", inst_c, "test_c"); |
|
||||||
if (utun_instance_init(inst_a) != 0 || utun_instance_init(inst_b) != 0 || utun_instance_init(inst_c) != 0) { |
memset(&result, 0, sizeof(result)); |
||||||
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "Failed to init connections"); |
assert(route_ping_observe(socket, observer->my_keys.public_key, &peer->local_addr, round + 2, 300, ping_done, &result) == 0); |
||||||
goto cleanup; |
uint64_t generation = socket->generation; socket->generation = generation == UINT64_MAX ? generation - 1 : generation + 1; |
||||||
} |
wait_result(&fixture, &result); assert(!result.success); socket->generation = generation; |
||||||
|
|
||||||
g_node_id_a = inst_a->node_id; g_node_id_b = inst_b->node_id; g_node_id_c = inst_c->node_id; |
memset(&result, 0, sizeof(result)); |
||||||
|
assert(route_ping_observe(socket, observer->my_keys.public_key, &peer->local_addr, round + 3, 500, ping_done, &result) == 0); |
||||||
test_timeout_id = uasync_set_timeout(ua, TEST_TIMEOUT_MS * 10, NULL, test_timeout_cb, "test_route_ping"); |
etcp_cancel_pings(owner, ping_done, &result); assert(!owner->pending_pings); |
||||||
|
uint64_t end = get_time_tb() + 6000; |
||||||
// 1. Wait for links A->B and B->C to initialize
|
while (get_time_tb() < end) uasync_poll(fixture.ua, 10); |
||||||
DEBUG_INFO(DEBUG_CATEGORY_ETCP, "Waiting for ETCP links to initialize..."); |
assert(!result.done); |
||||||
while (!test_timed_out) { |
nat_fixture_cleanup(&fixture); |
||||||
if (has_initialized_link(inst_a) && has_initialized_link(inst_b)) { |
return 0; |
||||||
DEBUG_INFO(DEBUG_CATEGORY_ETCP, "Links initialized"); |
|
||||||
break; |
|
||||||
} |
|
||||||
uasync_poll(ua, 10); |
|
||||||
} |
|
||||||
if (test_timed_out) { |
|
||||||
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "Timeout waiting for links"); |
|
||||||
goto cleanup; |
|
||||||
} |
|
||||||
|
|
||||||
// 2. Wait for BGP exchange so B learns C (registry has C's nodeinfo + addresses)
|
|
||||||
DEBUG_INFO(DEBUG_CATEGORY_BGP, "Waiting for BGP exchange (B learns C)..."); |
|
||||||
int bgp_wait_cycles = 0; |
|
||||||
while (!test_timed_out && bgp_wait_cycles < 500) { |
|
||||||
struct TOPO_NODE* ni_c = topo_node_registry_find(inst_b->topo_groups, g_node_id_c); |
|
||||||
if (ni_c && ni_c->v4_addrs) { |
|
||||||
DEBUG_INFO(DEBUG_CATEGORY_BGP, "B learned about C"); |
|
||||||
break; |
|
||||||
} |
|
||||||
uasync_poll(ua, 10); |
|
||||||
bgp_wait_cycles++; |
|
||||||
} |
|
||||||
if (!topo_node_registry_find(inst_b->topo_groups, g_node_id_c)) { |
|
||||||
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "B did not learn about C in time"); |
|
||||||
goto cleanup; |
|
||||||
} |
|
||||||
|
|
||||||
// 3. Get target info for C and send route ping request from A to B
|
|
||||||
struct ETCP_CONN* conn_ab = find_conn_to_peer(inst_a, g_node_id_b); |
|
||||||
if (!conn_ab) { |
|
||||||
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "A has no connection to B"); |
|
||||||
goto cleanup; |
|
||||||
} |
|
||||||
|
|
||||||
/* Получаем target IP:port из nodeinfo узла C */ |
|
||||||
/* addr[] в network byte order, port в host byte order */ |
|
||||||
uint32_t target_ip = 0; |
|
||||||
uint16_t target_port = 0; |
|
||||||
struct TOPO_GROUP_NODE* nq = topo_groups_get_default(inst_b->topo_groups) ? topo_node_find_by_id(topo_groups_get_default(inst_b->topo_groups), g_node_id_c) : NULL; |
|
||||||
struct TOPO_NODE* ni = nq ? topo_node_registry_find(inst_b->topo_groups, nq->node_id) : NULL; |
|
||||||
if (nq && ni) { |
|
||||||
const struct TOPO_ADDR4* a = ni->v4_addrs; |
|
||||||
if (a) { |
|
||||||
memcpy(&target_ip, a->addr, 4); |
|
||||||
target_port = a->port; |
|
||||||
} |
|
||||||
} |
|
||||||
if (target_ip == 0 || target_port == 0) { |
|
||||||
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "Cannot resolve target for node C"); |
|
||||||
goto cleanup; |
|
||||||
} |
|
||||||
|
|
||||||
DEBUG_INFO(DEBUG_CATEGORY_BGP, "Sending route ping request A->B for C (%s:%u)", |
|
||||||
ip_to_str(&target_ip, AF_INET).str, (unsigned)target_port); |
|
||||||
memset(&ping_result, 0, sizeof(ping_result)); |
|
||||||
int ret = route_ping_send_req_addr(inst_a->nat_det, topo_groups_get_default(inst_a->topo_groups), conn_ab, target_ip, target_port, |
|
||||||
3, // count
|
|
||||||
10, // interval_ms
|
|
||||||
200, // timeout_ms
|
|
||||||
5000,// wait_timeout_ms
|
|
||||||
ping_resp_cb, NULL, |
|
||||||
ni ? ni->public_key : NULL); |
|
||||||
if (ret != 0) { |
|
||||||
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "route_ping_send_req_addr failed: %d", ret); |
|
||||||
goto cleanup; |
|
||||||
} |
|
||||||
|
|
||||||
// 4. Wait for response
|
|
||||||
DEBUG_INFO(DEBUG_CATEGORY_BGP, "Waiting for route ping response..."); |
|
||||||
while (!test_timed_out && !ping_result.done) { |
|
||||||
uasync_poll(ua, 10); |
|
||||||
} |
|
||||||
|
|
||||||
if (test_timed_out) { |
|
||||||
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "Timeout waiting for ping response"); |
|
||||||
goto cleanup; |
|
||||||
} |
|
||||||
|
|
||||||
// 5. Verify results
|
|
||||||
if (!ping_result.success) { |
|
||||||
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "Ping reported failure (success=0)"); |
|
||||||
goto cleanup; |
|
||||||
} |
|
||||||
if (ping_result.count_ok == 0) { |
|
||||||
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "Ping count_ok is zero"); |
|
||||||
goto cleanup; |
|
||||||
} |
|
||||||
if (ping_result.avg_rtt == 0) { |
|
||||||
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "Ping avg_rtt is zero"); |
|
||||||
goto cleanup; |
|
||||||
} |
|
||||||
|
|
||||||
DEBUG_INFO(DEBUG_CATEGORY_BGP, "Route ping test PASSED: sent=%u ok=%u avg_rtt=%u", |
|
||||||
(unsigned)ping_result.count_sent, (unsigned)ping_result.count_ok, (unsigned)ping_result.avg_rtt); |
|
||||||
test_result = 0; |
|
||||||
|
|
||||||
cleanup: |
|
||||||
if (test_timeout_id) uasync_cancel_timeout(ua, test_timeout_id); |
|
||||||
if (inst_a) utun_instance_destroy(inst_a); |
|
||||||
if (inst_b) utun_instance_destroy(inst_b); |
|
||||||
if (inst_c) utun_instance_destroy(inst_c); |
|
||||||
if (ua) uasync_destroy(ua, 0); |
|
||||||
cleanup_temp_configs(); |
|
||||||
return test_result; |
|
||||||
} |
} |
||||||
|
|||||||
@ -1,88 +1,46 @@ |
|||||||
/**
|
/* Политика не заменяет измерение; неопределённость разрешает ограниченную пробу. */ |
||||||
* @file test_sock_match.c |
#include <assert.h> |
||||||
* @brief Unit-тесты классификации сокетов и сопоставления адресов (sock_match). |
|
||||||
* |
|
||||||
* Тестирует только чистую логику sock_role_of / sock_target_class / sock_match |
|
||||||
* без сети и без UTUN_INSTANCE — с синтетическими struct sock_view и адресами. |
|
||||||
*/ |
|
||||||
#include <stdio.h> |
|
||||||
#include <string.h> |
#include <string.h> |
||||||
#include "../src/routing_layer/sock_match.h" |
#include "routing_layer/sock_match.h" |
||||||
#include "../src/config_parser.h" |
#include "routing_layer/topo_node.h" |
||||||
#include "../src/transport_layer/etcp_connections.h" |
#include "config_parser.h" |
||||||
#include "../src/routing_layer/topo_node.h" |
|
||||||
#include "../lib/platform_compat.h" |
#include "../lib/platform_compat.h" |
||||||
|
#include "../lib/debug_config.h" |
||||||
static int fails = 0; |
|
||||||
static int checks = 0; |
|
||||||
|
|
||||||
#define CHECK(cond, msg) do { checks++; if (!(cond)) { fails++; printf("FAIL: %s\n", msg); } } while (0) |
|
||||||
|
|
||||||
static struct sock_view mk_view(uint8_t role, uint8_t has_inet, uint8_t has_priv, uint8_t is_eim, uint32_t pub4) { |
|
||||||
struct sock_view v; memset(&v, 0, sizeof(v)); |
|
||||||
v.role = role; v.has_inet = has_inet; v.has_priv = has_priv; v.is_eim = is_eim; v.public_ip4 = pub4; |
|
||||||
return v; |
|
||||||
} |
|
||||||
|
|
||||||
/* Классификация IPv4-адреса (network byte order): публичный/маршрутизируемый или нет.
|
|
||||||
* Адрес задаём «логически» (старший байт = первый октет) и переводим в s_addr через htonl. */ |
|
||||||
static void test_ip_is_public(void) { |
|
||||||
CHECK(ip_is_public(htonl(0x08080808u)) == 1, "8.8.8.8 public"); |
|
||||||
CHECK(ip_is_public(htonl(0x01020304u)) == 1, "1.2.3.4 public"); |
|
||||||
CHECK(ip_is_public(htonl(0xC0A80101u)) == 0, "192.168.1.1 private"); |
|
||||||
CHECK(ip_is_public(htonl(0x0A000001u)) == 0, "10.0.0.1 private"); |
|
||||||
CHECK(ip_is_public(htonl(0xAC100001u)) == 0, "172.16.0.1 private"); |
|
||||||
CHECK(ip_is_public(htonl(0xAC1F0001u)) == 0, "172.31.0.1 private"); |
|
||||||
CHECK(ip_is_public(htonl(0x64400001u)) == 0, "100.64.0.1 CGNAT"); |
|
||||||
CHECK(ip_is_public(htonl(0x7F000001u)) == 0, "127.0.0.1 loopback"); |
|
||||||
CHECK(ip_is_public(htonl(0xA9FE0101u)) == 0, "169.254.1.1 link-local"); |
|
||||||
CHECK(ip_is_public(htonl(0x00000000u)) == 0, "0.0.0.0"); |
|
||||||
CHECK(ip_is_public(htonl(0xE0000001u)) == 0, "224.0.0.1 multicast"); |
|
||||||
CHECK(ip_is_public(htonl(0xFFFFFFFFu)) == 0, "255.255.255.255 reserved"); |
|
||||||
} |
|
||||||
|
|
||||||
static void test_target_class(void) { |
|
||||||
CHECK(sock_target_class(TOPO_ADDR_INTERFACE, CFG_SERVER_TYPE_PUBLIC, NAT_TYPE_UNKNOWN) == SOCK_TC_PUB, "public INTERFACE -> PUB"); |
|
||||||
CHECK(sock_target_class(TOPO_ADDR_INTERFACE, CFG_SERVER_TYPE_UNKNOWN, NAT_TYPE_UNKNOWN) == SOCK_TC_PUB, "unknown -> PUB"); |
|
||||||
CHECK(sock_target_class(TOPO_ADDR_INTERFACE, CFG_SERVER_TYPE_LOCAL, NAT_TYPE_UNKNOWN) == SOCK_TC_PRIV, "local -> PRIV"); |
|
||||||
CHECK(sock_target_class(TOPO_ADDR_INTERFACE, CFG_SERVER_TYPE_PRIVATE, NAT_TYPE_UNKNOWN) == SOCK_TC_PRIV, "private -> PRIV"); |
|
||||||
CHECK(sock_target_class(TOPO_ADDR_NAT, CFG_SERVER_TYPE_NAT, NAT_VERIFIED_EIM) == SOCK_TC_PUB, "nat+EIM NAT-addr -> PUB"); |
|
||||||
CHECK(sock_target_class(TOPO_ADDR_INTERFACE, CFG_SERVER_TYPE_NAT, NAT_VERIFIED_EIM) == SOCK_TC_PRIV, "nat+EIM INTERFACE -> PRIV"); |
|
||||||
CHECK(sock_target_class(TOPO_ADDR_NAT, CFG_SERVER_TYPE_NAT, NAT_VERIFIED_DIRECT) == SOCK_TC_PUB, "nat+DIRECT NAT-addr -> PUB"); |
|
||||||
CHECK(sock_target_class(TOPO_ADDR_NAT, CFG_SERVER_TYPE_NAT, NAT_VERIFIED_STRICT) == SOCK_TC_UNREACH, "nat+STRICT NAT-addr -> UNREACH"); |
|
||||||
CHECK(sock_target_class(TOPO_ADDR_INTERFACE, CFG_SERVER_TYPE_NAT, NAT_VERIFIED_STRICT) == SOCK_TC_PRIV, "nat+STRICT INTERFACE -> PRIV"); |
|
||||||
CHECK(sock_target_class(TOPO_ADDR_NAT, CFG_SERVER_TYPE_NAT, NAT_TYPE_UNKNOWN) == SOCK_TC_UNREACH, "nat+unknown NAT-addr -> UNREACH"); |
|
||||||
CHECK(sock_target_class(TOPO_ADDR_INTERFACE, CFG_SERVER_TYPE_NAT, NAT_TYPE_UNKNOWN) == SOCK_TC_PRIV, "nat+unknown INTERFACE -> PRIV"); |
|
||||||
} |
|
||||||
|
|
||||||
static void test_match(void) { |
|
||||||
uint32_t pub = 0x01020304u; /* произвольный "публичный" IP */ |
|
||||||
struct sock_view direct = mk_view(SOCK_ROLE_DIRECT, 1, 0, 0, pub); |
|
||||||
struct sock_view natout = mk_view(SOCK_ROLE_NAT, 1, 1, 0, 0); |
|
||||||
struct sock_view local = mk_view(SOCK_ROLE_LOCAL, 0, 1, 0, 0); |
|
||||||
|
|
||||||
/* наш DIRECT (публичный) */ |
|
||||||
CHECK(sock_match(&direct, TOPO_ADDR_NAT, CFG_SERVER_TYPE_PUBLIC, NAT_TYPE_UNKNOWN, pub) == 1, "DIRECT->PUB ok"); |
|
||||||
CHECK(sock_match(&direct, TOPO_ADDR_INTERFACE, CFG_SERVER_TYPE_LOCAL, NAT_TYPE_UNKNOWN, 0) == 0, "DIRECT->PRIV нет приватного интерфейса"); |
|
||||||
/* наш NAT (outbound-only) */ |
|
||||||
CHECK(sock_match(&natout, TOPO_ADDR_NAT, CFG_SERVER_TYPE_PUBLIC, NAT_TYPE_UNKNOWN, pub) == 1, "NAT->PUB ok"); |
|
||||||
CHECK(sock_match(&natout, TOPO_ADDR_INTERFACE, CFG_SERVER_TYPE_NAT, NAT_VERIFIED_STRICT, 0) == 1, "NAT->PRIV (есть приватный)"); |
|
||||||
/* наш LOCAL (без интернета) */ |
|
||||||
CHECK(sock_match(&local, TOPO_ADDR_NAT, CFG_SERVER_TYPE_PUBLIC, NAT_TYPE_UNKNOWN, pub) == 0, "LOCAL->PUB нет интернета"); |
|
||||||
CHECK(sock_match(&local, TOPO_ADDR_INTERFACE, CFG_SERVER_TYPE_LOCAL, NAT_TYPE_UNKNOWN, 0) == 1, "LOCAL->PRIV ок (LAN)"); |
|
||||||
/* UNREACH цель */ |
|
||||||
CHECK(sock_match(&direct, TOPO_ADDR_NAT, CFG_SERVER_TYPE_NAT, NAT_VERIFIED_STRICT, pub) == 0, "строгий NAT-адрес недостижим"); |
|
||||||
/* hairpin: оба EIM, одинаковый публичный IP */ |
|
||||||
struct sock_view eim = mk_view(SOCK_ROLE_DIRECT, 1, 1, 1, pub); |
|
||||||
CHECK(sock_match(&eim, TOPO_ADDR_NAT, CFG_SERVER_TYPE_NAT, NAT_VERIFIED_EIM, pub) == 0, "hairpin (одинаковый публичный IP)"); |
|
||||||
CHECK(sock_match(&eim, TOPO_ADDR_NAT, CFG_SERVER_TYPE_NAT, NAT_VERIFIED_EIM, 0x05060708u) == 1, "EIM разные публичные IP -> ok"); |
|
||||||
} |
|
||||||
|
|
||||||
int main(void) { |
int main(void) { |
||||||
test_ip_is_public(); |
debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); |
||||||
test_target_class(); |
const uint64_t now = UINT64_C(1700000000000000); |
||||||
test_match(); |
const uint32_t public_ip = htonl(UINT32_C(0xc633640b)); |
||||||
if (fails) { printf("RESULT: %d/%d failed\n", fails, checks); return 1; } |
const uint32_t private_ip = htonl(UINT32_C(0x0a400102)); |
||||||
printf("RESULT: all %d checks passed\n", checks); |
struct address_profile unknown = {0}; |
||||||
|
struct address_profile open = {.mapping = ADDRESS_MAP_EIM, .inbound = ADDRESS_IN_OPEN, .evidence_id = 1, .checked_at = now}; |
||||||
|
struct address_profile contact = open; contact.inbound = ADDRESS_IN_CONTACT; |
||||||
|
struct address_profile dependent = open; dependent.mapping = ADDRESS_MAP_DEPENDENT; dependent.inbound = ADDRESS_IN_UNKNOWN; |
||||||
|
|
||||||
|
assert(sock_target_class(TOPO_ADDR_INTERFACE, CFG_SERVER_TYPE_PUBLIC, &unknown, 1, now) == SOCK_TC_PROBE); |
||||||
|
assert(sock_target_class(TOPO_ADDR_INTERFACE, CFG_SERVER_TYPE_PUBLIC, &open, 0, now) == SOCK_TC_PRIV); |
||||||
|
assert(sock_target_class(TOPO_ADDR_NAT, CFG_SERVER_TYPE_NAT, &open, 1, now) == SOCK_TC_PUB); |
||||||
|
assert(sock_target_class(TOPO_ADDR_NAT, CFG_SERVER_TYPE_NAT, &contact, 1, now) == SOCK_TC_CONTACT); |
||||||
|
assert(sock_target_class(TOPO_ADDR_NAT, CFG_SERVER_TYPE_NAT, &unknown, 1, now) == SOCK_TC_PROBE); |
||||||
|
assert(sock_target_class(TOPO_ADDR_NAT, CFG_SERVER_TYPE_NAT, &dependent, 1, now) == SOCK_TC_UNREACH); |
||||||
|
assert(sock_target_class(TOPO_ADDR_INTERFACE, CFG_SERVER_TYPE_NAT, &dependent, 0, now) == SOCK_TC_PRIV); |
||||||
|
assert(sock_target_class(TOPO_ADDR_INTERFACE, CFG_SERVER_TYPE_LOCAL, &open, 1, now) == SOCK_TC_PRIV); |
||||||
|
assert(sock_target_class(TOPO_ADDR_INTERFACE, CFG_SERVER_TYPE_PRIVATE, &open, 1, now) == SOCK_TC_PRIV); |
||||||
|
assert(sock_target_class(TOPO_ADDR_NAT, CFG_SERVER_TYPE_NAT, &open, 1, now + ADDRESS_PROFILE_TTL_US + 1) == SOCK_TC_PROBE); |
||||||
|
assert(sock_target_class(TOPO_ADDR_NAT, CFG_SERVER_TYPE_NAT, &open, 1, now - 1) == SOCK_TC_PROBE); |
||||||
|
|
||||||
|
struct sock_view outgoing = {.has_inet = 1, .has_priv = 1, .now_us = now, .public_ip4 = public_ip}; |
||||||
|
struct sock_view local = {.has_priv = 1, .now_us = now}; |
||||||
|
assert(sock_match(&outgoing, TOPO_ADDR_NAT, CFG_SERVER_TYPE_NAT, &open, public_ip)); |
||||||
|
assert(sock_match(&outgoing, TOPO_ADDR_NAT, CFG_SERVER_TYPE_NAT, &contact, public_ip)); |
||||||
|
/* Совпадение публичного IP разрешает проверку hairpin. */ |
||||||
|
outgoing.address_profile = open; |
||||||
|
assert(sock_match(&outgoing, TOPO_ADDR_NAT, CFG_SERVER_TYPE_NAT, &open, public_ip)); |
||||||
|
assert(!sock_match(&local, TOPO_ADDR_NAT, CFG_SERVER_TYPE_NAT, &open, public_ip)); |
||||||
|
assert(sock_match(&local, TOPO_ADDR_INTERFACE, CFG_SERVER_TYPE_NAT, &dependent, private_ip)); |
||||||
|
assert(!sock_match(&outgoing, TOPO_ADDR_NAT, CFG_SERVER_TYPE_NAT, &dependent, public_ip)); |
||||||
|
outgoing.now_us = now + ADDRESS_PROFILE_TTL_US + 1; |
||||||
|
assert(sock_match(&outgoing, TOPO_ADDR_NAT, CFG_SERVER_TYPE_NAT, &open, public_ip)); |
||||||
|
/* Смена времени/давности снижает уверенность, сохраняя возможность реального handshake. */ |
||||||
return 0; |
return 0; |
||||||
} |
} |
||||||
|
|||||||
Loading…
Reference in new issue