3 changed files with 720 additions and 245 deletions
@ -0,0 +1,344 @@
|
||||
Архитектура 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 |
||||
Loading…
Reference in new issue