You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
344 lines
18 KiB
344 lines
18 KiB
Архитектура 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
|
|
|