Архитектура P2P Direct Connection (установка оптимального подключения между узлами) Цель: в большой сети узлов каждый узел умеет находить быстрые подключения к любому другому узлу — либо прямое (direct), либо через минимальное число промежуточных узлов с минимальным суммарным RTT. Оба узла пробуют доступные варианты пингом и выбирают лучший. === Что уже есть в коде (используем, не дублируем) === 1. NODEINFO уже содержит поле tranzit_nodes + структуру NODEINFO_TRANZIT_NODE (node_id, rtt, link_q) — "лучшие транзитные узлы, выбирается/обновляется узлом", но пока не заполняется и не используется. 2. Пинг-инфраструктура: route_ping_send_req_addr (запрос удалённого пинга через третий узел), route_ping_handle_resp (приём результата). Также есть etcp_send_ping_to_socket — прямой пинг с конкретного сокета на конкретный адрес с pubkey для шифрования. 3. NAT-детекция уже работает: проверка типа NAT через третий узел, обмен NAT_INFO. 4. Path system: NODEINFO_PATH с hop_count, routes → ROUTE_ENTRY → NODEINFO_Q → paths — уже выбирается путь для маршрутизации пакетов. 5. Константа ROUTE_SUBCMD_CONN_REQ (0x0B) зарезервирована в route_bgp.h, но не реализована. 6. NODEINFO_Q уже имеет best_socket, last_ping_time, last_rtt. === Общий алгоритм (на примере узлов A и B) === A ──(ETCP через релейные узлы)──> B [текущий путь, hop_count > 1] 1. A замечает большой объём трафика к B → запускает CONN_REQ 2. A шлёт B через существующий ETCP-путь: свои адреса-кандидаты + лучших транзитных соседей 3. B получает CONN_REQ, сразу начинает пинговать адреса A, собирает свои данные 4. B шлёт CONN_RESP: свои адреса + транзитных соседей + результаты пробных пингов 5. A получает CONN_RESP, пингует все адреса B 6. Оба вычисляют лучший вариант (прямой или релейный с минимальным RTT) 7. Оба шлют друг другу CONN_RESULT с выбором 8. Оба пытаются создать ETCP link к выбранному адресу (если прямой) 9. Лишние/худшие линки закрываются позже — все линки в рамках одного ETCP_CONN === Фаза 1: Обмен кандидатами (CONN_REQ → CONN_RESP) === Узел-инициатор A собирает и отправляет: - Свои адреса-кандидаты: из своих ETCP_SOCKET (interface_addr + nat_addr). Включаются только сокеты с NAT_VERIFIED_* / EIM / PUBLIC (type >= NAT_VERIFIED_UNKNOWN). Для каждого: ip, port, type, socket_id. - Лучших транзитных соседей: top-N directly-connected узлов (hop_count == 1) отсортированных по NODEINFO_Q.last_rtt (лучший RTT первым). Узел B при получении CONN_REQ: - Сохраняет кандидатов A - Сразу запускает пробные пинги ко всем адресам A (etcp_send_ping_to_socket с pubkey) - Собирает свои адреса-кандидаты - Собирает своих транзитных соседей - Шлёт CONN_RESP со своими данными + уже готовыми результатами проб === Фаза 2: Пробы (CONN_RESP → Пинги) === Узел A при получении CONN_RESP: - Запускает пинги ко всем адресам B: каждый адрес B пингуется с каждого своего сокета (N×M проб) - Использует etcp_send_ping_to_socket с pubkey узла B (из NODEINFO) - Пинг идёт напрямую по UDP (не через ETCP) — проверяет реальную достижимость включая NAT traversal - Ждёт все результаты либо таймаут Параллельно B тоже завершает свои пробы (запущенные на шаге 3). === Фаза 3: Выбор лучшего пути (→ CONN_RESULT) === Каждая сторона независимо вычисляет: best = {mode: none, rtt: 65535} Прямые варианты: for each (мой_сокет, peer_addr) где probe.ok: if probe.rtt < best.rtt: best = {direct, my_sock_idx, peer_addr_idx, probe.rtt} Релейные варианты: for each общий_транзитный_узел R (доступен и A и B): // RTT(A↔R) берём из своих замеров (A знает RTT до своих соседей) // RTT(R↔B) берём из транзитных соседей B (присланы в CONN_RESP) total_rtt = A→R_rtt + R→B_rtt if total_rtt < best.rtt: best = {relay, R, total_rtt} Обе стороны приходят к одинаковому выводу (информация симметрична). Шлют CONN_RESULT с выбором. Если прямой вариант — оба пытаются создать ETCP link к адресу пира. Если релейный — используют существующий путь (уже работает). === Триггер запуска negotiation === В route_pkt (routing.c) при отправке пакета узлу с hop_count > 1: - Накапливаем счётчик байт к этому узлу (per-node counter в bgp) - При превышении порога (например 64KB) и если negotiation ещё не запущен — стартуем - Cooldown: не чаще чем раз в 30 секунд для одной пары узлов - Если negotiation уже в процессе для этой пары — не дублируем === Tranzit nodes — автоматическое заполнение === В route_bgp_update_my_nodeinfo (route_node.c) при каждом обновлении NODEINFO: - Сканируем directly-connected соседей (из bgp->nodes, где hop_count == 1) - Сортируем по NODEINFO_Q.last_rtt (лучший RTT первым) - Берём top-N (до 4) и упаковываем в NODEINFO.tranzit_nodes как массив NODEINFO_TRANZIT_NODE (node_id, rtt, link_q) Плюс периодический refill (раз в ~10 сек) для свежести RTT замеров. === Структуры данных (новые, route_p2pconn.h) === #define P2P_MAX_ADDRS 8 #define P2P_MAX_RELAYS 8 #define P2P_MAX_PROBES (P2P_MAX_ADDRS * P2P_MAX_ADDRS) // до 64 #define P2P_PHASE_WAIT_RESP 0 // ждём CONN_RESP от пира #define P2P_PHASE_PROBING 1 // пингуем адреса пира #define P2P_PHASE_SELECTING 2 // выбор лучшего, отправка CONN_RESULT #define P2P_PHASE_DONE 3 // завершено // Один адрес-кандидат struct P2P_ADDR { uint32_t ip; // network byte order uint16_t port; uint8_t type; // NAT_VERIFIED_* uint8_t socket_id; }; // Один транзитный узел struct P2P_RELAY { uint64_t node_id; uint16_t rtt; // x0.1ms uint16_t link_q; }; // Результат одного пробного пинга struct P2P_PROBE_RESULT { uint8_t my_sock_idx; // индекс в my_addrs[] uint8_t peer_addr_idx; // индекс в peer_addrs[] uint16_t rtt; // x0.1ms, 0 = fail }; // Состояние одних переговоров (хранится в хеш-таблице bgp->p2p_negotiations) struct P2P_NEGOTIATION { struct ll_entry ll; uint32_t request_id; uint64_t peer_node_id; uint8_t phase; // Мои данные struct P2P_ADDR my_addrs[P2P_MAX_ADDRS]; uint8_t my_addr_count; struct P2P_RELAY my_relays[P2P_MAX_RELAYS]; uint8_t my_relay_count; // Данные пира (заполняются из CONN_RESP) struct P2P_ADDR peer_addrs[P2P_MAX_ADDRS]; uint8_t peer_addr_count; struct P2P_RELAY peer_relays[P2P_MAX_RELAYS]; uint8_t peer_relay_count; // Результаты проб struct P2P_PROBE_RESULT probes[P2P_MAX_PROBES]; uint8_t probe_total; // сколько всего запланировано uint8_t probe_done; // сколько завершилось (ok + fail) // Лучший выбор uint8_t best_mode; // 1=direct, 2=relay uint64_t best_relay; // node_id релея (если mode=relay) uint16_t best_rtt; // x0.1ms uint8_t best_my_sock_idx; // индекс в my_addrs (если direct) uint8_t best_peer_addr_idx;// индекс в peer_addrs (если direct) void* timeout_timer; // общий таймаут на всю negotiation }; // Счётчик трафика per-node (для триггера, хранится в bgp) struct P2P_TRAFFIC_COUNTER { uint64_t node_id; uint64_t bytes_sent; // накоплено байт uint64_t last_negotiation_time; // время последней попытки (0 = не было) }; === Протокольные пакеты (добавляются в route_bgp.h) === ROUTE_SUBCMD_CONN_REQ 0x0B // запрос прямого подключения (уже зарезервирован) ROUTE_SUBCMD_CONN_RESP 0x0C // ответ с адресами + результаты проб ROUTE_SUBCMD_CONN_RESULT 0x0E // финальный выбор // Пакет CONN_REQ (A → B) struct BGP_CONN_REQ { uint8_t cmd; // ETCP_ID_ROUTE_ENTRY uint8_t subcmd; // ROUTE_SUBCMD_CONN_REQ uint32_t request_id; // для корреляции uint8_t addr_count; // число адресов-кандидатов uint8_t relay_count; // число транзитных узлов uint8_t reserved[2]; // далее динамически (размер = addr_count*8 + relay_count*12): // [addr_count × {ip[4] port[2] type[1] socket_id[1]}] // [relay_count × {node_id[8] rtt[2] link_q[2]}] }; // Пакет CONN_RESP (B → A) struct BGP_CONN_RESP { uint8_t cmd; // ETCP_ID_ROUTE_ENTRY uint8_t subcmd; // ROUTE_SUBCMD_CONN_RESP uint32_t request_id; uint8_t addr_count; // адреса B uint8_t relay_count; // транзитные узлы B uint8_t probe_count; // готовые результаты проб B→A uint8_t reserved; // [addr_count × {ip[4] port[2] type[1] socket_id[1]}] // [relay_count × {node_id[8] rtt[2] link_q[2]}] // [probe_count × {peer_addr_idx[1] my_sock_idx[1] rtt[2] ok[1]}] }; // Пакет CONN_RESULT (A ↔ B, финальный) struct BGP_CONN_RESULT { uint8_t cmd; uint8_t subcmd; // ROUTE_SUBCMD_CONN_RESULT uint32_t request_id; uint8_t chosen_mode; // 1=direct, 2=relay uint8_t reserved; uint16_t chosen_rtt; // x0.1ms // Если direct: uint32_t peer_ip; // IP пира к которому подключаемся uint16_t peer_port; uint8_t my_socket_id; // свой сокет для подключения uint8_t peer_socket_id; // сокет пира // Если relay (оверлей тех же байт): // uint64_t relay_node_id; }; === API модуля route_p2pconn === // Запуск negotiation к узлу peer_node_id. // Вызывается из route_pkt при превышении порога трафика. int p2p_start_negotiation(struct ROUTE_BGP* bgp, uint64_t peer_node_id); // Проверка: запущена ли уже negotiation для этой пары int p2p_is_negotiating(struct ROUTE_BGP* bgp, uint64_t peer_node_id); // Сбор своих адресов-кандидатов (из etcp_sockets) int p2p_collect_my_addrs(struct ROUTE_BGP* bgp, struct P2P_ADDR* out, uint8_t max); // Сбор лучших транзитных соседей (из directly-connected nodes) int p2p_collect_my_relays(struct ROUTE_BGP* bgp, struct P2P_RELAY* out, uint8_t max); // Обработчики входящих пакетов (вызываются из route_bgp_receive_cbk): void p2p_handle_conn_req(struct ROUTE_BGP* bgp, struct ETCP_CONN* from, const uint8_t* data, size_t len); void p2p_handle_conn_resp(struct ROUTE_BGP* bgp, struct ETCP_CONN* from, const uint8_t* data, size_t len); void p2p_handle_conn_result(struct ROUTE_BGP* bgp, struct ETCP_CONN* from, const uint8_t* data, size_t len); // Отмена negotiation при удалении conn void p2p_cancel_for_conn(struct ROUTE_BGP* bgp, struct ETCP_CONN* conn); // Очистка всех negotiation (при destroy bgp) void p2p_destroy_all(struct ROUTE_BGP* bgp); === Схема состояний negotiation === IDLE ──(трафик > порог)──→ WAIT_RESP ──(CONN_RESP получен)──→ PROBING ↑ ↑ │ │ (таймаут) (все пробы готовы) │ ↓ │ └──────────────────────────── DONE ←──(CONN_RESULT отправлен)── SELECTING === Интеграция (какие файлы меняются) === Новые файлы: src/route_p2pconn.h — структуры, константы, прототипы src/route_p2pconn.c — вся логика negotiation Изменения в существующих: src/route_bgp.h — добавить ROUTE_SUBCMD_CONN_RESP 0x0C, ROUTE_SUBCMD_CONN_RESULT 0x0E, структуры пакетов BGP_CONN_REQ/RESP/RESULT src/route_bgp.c — в route_bgp_receive_cbk добавить обработку новых subcmd. В struct ROUTE_BGP добавить поле p2p_negotiations (очередь/хеш negotiation) и p2p_traffic_counters (per-node counters) src/route_node.c — в route_bgp_update_my_nodeinfo заполнять tranzit_nodes из RTT directly-connected соседей src/routing.c — в route_pkt добавить накопление счётчика трафика и вызов p2p_start_negotiation при превышении порога src/Makefile.am — добавить route_p2pconn.c в сборку === Внутренняя логика route_p2pconn.c === p2p_start_negotiation(bgp, peer_node_id): 1. Проверить что нет активной negotiation для этой пары 2. Проверить cooldown (30 сек) 3. Создать struct P2P_NEGOTIATION, заполнить my_addrs, my_relays 4. Сформировать BGP_CONN_REQ и отправить через существующий путь к пиру 5. Поставить таймаут на всю negotiation (например 5 сек) p2p_handle_conn_req(bgp, from_conn, data, len): 1. Распарсить BGP_CONN_REQ 2. Сохранить адреса и релеи инициатора в P2P_NEGOTIATION 3. Собрать свои my_addrs, my_relays 4. Запустить пробные пинги к адресам инициатора (p2p_start_probes) 5. Когда пробы готовы — отправить CONN_RESP с результатами p2p_start_probes(neg): 1. Для каждой пары (мой_сокет, peer_addr) запланировать пинг 2. Вызвать etcp_send_ping_to_socket с pubkey пира 3. В коллбэке сохранить результат в probes[], инкрементировать probe_done 4. Когда probe_done == probe_total → вызвать p2p_select_best p2p_select_best(neg): 1. Прямые: найти пару (my_sock, peer_addr) с минимальным rtt > 0 2. Релейные: для каждого общего транзитного узла посчитать суммарный rtt, найти минимум 3. Сравнить прямой vs релейный лучший rtt 4. Записать выбор в neg->best_* 5. Отправить CONN_RESULT пиру 6. Вызвать p2p_establish_link(neg) p2p_establish_link(neg): 1. Если best_mode == direct: - Найти/создать ETCP_CONN к пиру (по node_id) - Вызвать etcp_link_new с chosen address 2. Если best_mode == relay: - Ничего не делаем, существующий путь уже работает 3. Пометить negotiation как DONE