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