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

Архитектура 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