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