Merge origin/master with encrypted attachment ownership fixes

Validation: ./build.sh --no-gui -j4 and make check -j4 passed; 186 tests, no failures.
This commit is contained in:
evgeny committed 2026-10-24 15:11:50 +02:00
commit bafda5ae3a
38 files changed
+2854 -984

No files matched your search

+5 -2
View File
@@ -22,11 +22,12 @@ enum address_inbound {
enum address_profile_source { ADDRESS_SOURCE_NONE, ADDRESS_SOURCE_ETCP, ADDRESS_SOURCE_STUN };
struct address_profile {
uint8_t mapping;
uint8_t inbound;
uint8_t mapping; /* enum address_mapping: UNKNOWN, без трансляции, EIM или зависимый mapping. */
uint8_t inbound; /* enum address_inbound: UNKNOWN, OPEN либо нужен исходящий контакт. */
uint8_t source; /* ETCP — измерение рабочего endpoint; STUN — предварительный EIM? с неизвестным входом. */
};
/* Постоянные строки для диагностики; неизвестное значение возвращает INVALID. */
static inline const char *address_mapping_name(uint8_t value) {
static const char *const names[] = {"UNKNOWN", "UNCHANGED", "EIM", "DEPENDENT"};
return value <= ADDRESS_MAP_DEPENDENT ? names[value] : "INVALID";
@@ -42,6 +43,8 @@ static inline const char *address_profile_source_name(uint8_t value) {
return value <= ADDRESS_SOURCE_STUN ? names[value] : "INVALID";
}
/* 1 — поля входят в enum и при source=NONE оба свойства UNKNOWN; 0 — иначе/NULL.
* Проверяет представление, а не достоверность измерения или сочетания свойств. */
static inline int address_profile_valid(const struct address_profile *profile) {
return profile && profile->mapping <= ADDRESS_MAP_DEPENDENT && profile->inbound <= ADDRESS_IN_CONTACT &&
profile->source <= ADDRESS_SOURCE_STUN &&
+7
View File
@@ -39,10 +39,17 @@ struct ROUTE6_TABLE {
struct ll_queue *queue; // indexed by node_id for delete
};
/* Создать таблицу в ua (его время жизни длиннее таблицы); NULL при ошибке. */
struct ROUTE6_TABLE *route6_table_create(struct UASYNC *ua);
/* Освободить таблицу и записи, сохраняя заимствованные узлы; NULL допустим. */
void route6_table_destroy(struct ROUTE6_TABLE *table);
/* Вставить IPv6-подсети node. Вложенные префиксы допустимы; дубликаты radix пропускает.
* true означает завершённый обход, даже если ничего не добавлено. false — неверный вход/нет подсетей/OOM;
* при OOM ранее вставленные подсети остаются. Адреса и /0..128 должен проверить вызывающий. */
bool route6_insert(struct ROUTE6_TABLE *table, struct TOPO_GROUP_NODE *node);
/* Удалить все записи с node->node_id (не по равенству указателей); NULL table/node допустимы. */
void route6_delete(struct ROUTE6_TABLE *table, struct TOPO_GROUP_NODE *node);
/* Найти наиболее длинный подходящий префикс; addr — ненулевой указатель на 16 сетевых байт. NULL — нет маршрута. */
const struct ROUTE6_DATA *route6_lookup(struct ROUTE6_TABLE *table, const uint8_t addr[16]);
+9 -4
View File
@@ -16,18 +16,23 @@ extern "C" {
struct UTUN_INSTANCE;
struct TOPO_GROUP;
// Запускает зондирование связности ко всем адресам удалённого узла
/* Запустить асинхронные пробы известных адресов nq из registry; group — его группа.
* UDP: до 8 сокетов, серия CONN_PROBE_COUNT с каждого до первого успешного сокета;
* TCP: один STCP ping с REALITY-параметрами при наличии. RTT — 0.1 мс, UINT16_MAX неизвестен.
* До 16 уникальных IP:port на семейство. Собственный/спящий узел и текущие пробы пропускаются;
* NAT/standby могут отложить старт. Результат — в nq->connectivity, завершение уведомляет topo callback. */
void route_connectivity_probe_node(struct UTUN_INSTANCE* instance, struct TOPO_GROUP* group,
struct TOPO_GROUP_NODE* nq);
// Отменяет все pending пробы для узла (при удалении / withdraw)
/* Отменить UDP/TCP-пробы и отложенный старт, освободить контексты; NULL допустим.
* Сбрасывает probe_status/pending_count, сохранённые RTT/status остаются. */
void route_connectivity_cancel_node(struct TOPO_GROUP_NODE* nq);
// Отменяет все pending пробы для всех узлов (при destroy)
// Отменяет pending пробы всех узлов во всех группах instance (при destroy)
void route_connectivity_cancel_all(struct UTUN_INSTANCE* instance);
// Устанавливает таймаут одного probe-пинга (по умолчанию CONN_PROBE_TIMEOUT_MS).
// Используется тестами для ускорения probe-циклов.
// Используется тестами для ускорения probe-циклов; настройка общая для процесса, 0 игнорируется.
void route_connectivity_set_probe_timeout_ms(uint16_t ms);
+35 -57
View File
@@ -1,64 +1,42 @@
# route_connectivity — зондирование прямой связности с удалённым узлом
# route_connectivity — прямые пробы адресов узла
## 1. Назначение
Проверяет прямую IP-достижимость всех адресов удалённого узла через `etcp_send_ping_to_socket()`.
Для каждого адреса перебирает локальные сокеты-кандидаты (до 8), с каждого делает серию из 3 пингов
(CONN_PROBE_COUNT=3, CONN_PROBE_TIMEOUT_MS=1000). Результаты (reachable/unreachable, min_rtt)
сохраняются в `TOPO_CONNECTIVITY` узла и используются `conn_mgr` для выбора оптимального пути.
`route_connectivity_probe_node(instance, group, nq)` берёт общую запись узла из
registry и запускает асинхронные пробы. Собственный узел, пир в SLEEP-фазе и уже
идущие/отложенные пробы пропускаются. NAT-окно сохраняет незнакомые IP для проверки
входящих; Android standby откладывает старт до активного интервала.
В отличие от `route_ping` (пинг через BGP-посредника), здесь пинги идут напрямую с локальных сокетов
на адреса удалённого узла — без промежуточных узлов.
Для каждого семейства перебираются до 16 уникальных IP:port. Нулевые адреса/порты
пропускаются. Дедупликация выполняется по IP:port, без учёта типа адреса/протокола.
## 2. Как пользоваться
```c
// Запустить зондирование узла (вызывается conn_mgr при обнаружении нового узла):
route_connectivity_probe_node(instance, nodeq);
UDP проверяется с обычных локальных сокетов того же семейства (до 8).
IPv4-кандидаты фильтруются через `sock_match` в порядке списка сокетов; предпочтений
по /24 или рейтинга DIRECT/EIM здесь нет. Для IPv6 сначала идут сокеты той же /64,
затем остальные IPv6. TCP-сокеты и `probe_only` для UDP исключены.
С каждого UDP-сокета идёт последовательная серия из `CONN_PROBE_COUNT=3` ping;
достаточно одного ответа в серии, чтобы завершить этот адрес успешно. Следующий
сокет пробуется, если текущая серия не дала успеха. RTT — минимум успешной серии,
а не обязательный минимум по всем доступным сокетам.
// Отменить зондирование (при удалении узла / withdraw):
route_connectivity_cancel_node(instance, nodeq);
TCP-адрес проверяется одним `etcp_send_tcp_ping` через STCP-клиент, независимо
от набора UDP-сокетов. Используются REALITY-параметры из записи узла; адрес с
REALITY-флагом без параметров пропускается. Проба не создаёт READY-сессию группы.
Таймаут по умолчанию 1000 мс; `route_connectivity_set_probe_timeout_ms(ms)` меняет
его для новых контекстов всего процесса, 0 игнорируется.
// Отменить все зондирования (при destroy):
route_connectivity_cancel_all(instance);
```
Результаты находятся в `nq->connectivity`: `probe_status`, `pending_count`,
`interface/nat/real_status`, соответствующие `*_min_rtt` и `*_probe_time`.
RTT и время — timebase (0.1 мс), UINT16_MAX означает неизвестный RTT.
INTERFACE обновляет также исторические поля REAL; отдельного третьего адреса
для этого не требуется. Успех одного адреса не затирается неудачей другого.
Завершение последней pending-пробы устанавливает DONE, обновляет БД и вызывает
`topo_fire_nodeinfo_cbk`. Если пригодных адресов/сокетов нет, DONE означает только
конец обхода: UNKNOWN не превращается автоматически в UNREACHABLE.
Результаты доступны через `nodeq->connectivity`:
- `probe_status` — `PROBE_STATUS_NONE` / `PROBE_STATUS_IN_PROGRESS` / `PROBE_STATUS_DONE`
- `interface_status/nat_status/real_status` — `PROBE_RESULT_UNKNOWN` / `REACHABLE` / `UNREACHABLE`
- `interface_min_rtt/nat_min_rtt/real_min_rtt` — минимальный RTT по всем сокетам (0.1ms)
- `pending_count` — счётчик незавершённых серий (когда 0 → PROBE_STATUS_DONE)
`route_connectivity_cancel_node(nq)` отменяет все UDP/TCP-пробы и отложенный старт,
освобождает контексты и сбрасывает probe_status/pending_count. Последние результаты
RTT/status сохраняются. Отмена нужна до удаления узла, группы или рабочих сокетов.
`route_connectivity_cancel_all(instance)` обходит все группы, не только UTUN.
Все вызовы выполняются в потоке ядра; group/nq заимствованы до завершения или отмены.
Ключевой нюанс: зондирование пропускает собственный узел (`nq->node->node_id == instance->node_id`).
Если зондирование уже в процессе, повторный вызов игнорируется.
## 3. API
### Внутренние структуры
| Структура | Назначение |
|-----------|------------|
| `conn_probe_ctx` | Контекст одной серии проб: `addr_type` (INTERFACE/NAT/REAL), `target_addr`, `candidate_sockets[8]`, `best_across_sockets` (min RTT по всем опробованным сокетам), `min_rtt` (в текущей серии), счётчики |
### Подбор сокетов-кандидатов (`conn_match_candidate_sockets`)
Для каждого адреса узла подбирает локальные сокеты в порядке предпочтения:
1. **INTERFACE-адрес**: PRIVATE/LOCAL сокеты в той же /24 подсети
2. **NAT/REAL-адрес**: DIRECT > EIM > PUBLIC/UNKNOWN сокеты
3. Fallback: остальные подходящие сокеты, затем все IPv4
Для IPv6: предпочитает сокеты в той же /64 подсети.
### Функции
| Функция | Назначение |
|---------|------------|
| `route_connectivity_probe_node(instance, nq)` | Перебрать все v4/v6 адреса узла, для каждого подобрать сокеты-кандидаты, запустить серии пингов по 3. Заполняет `nq->connectivity` |
| `route_connectivity_cancel_node(instance, nq)` | Сбросить `probe_status=NONE`, `pending_count=0`, занулить `nq` в ctx (чтобы callback не обновлял статистику) |
| `route_connectivity_cancel_all(instance)` | Сбросить состояние всех узлов в default-группе |
### Логика завершения
- Серия из 3 пингов с одного сокета: если все 3 потеряны → переход к следующему сокету
- Если хоть 1 пинг успешен → серия считается успешной для данного адреса, обновляется `*_status=REACHABLE` и `*_min_rtt`
- Когда `pending_count` доходит до 0 → `probe_status = PROBE_STATUS_DONE`
### Зависимости
- `src/etcp_connections.h` — `etcp_send_ping_to_socket()` (прямой пинг)
- `src/topo_node.h` — `TOPO_GROUP_NODE`, `TOPO_CONNECTIVITY`, `TOPO_ADDR4`, `TOPO_ADDR6`
- `src/topo_group.h` — `topo_groups_get_default()` (для cancel_all)
- `lib/u_async.h` — get_time_tb() для отметок времени
`route_ping` также выполняет прямые UDP-пробы, но для измерения наблюдаемого endpoint;
выбор наблюдателей/помощников и вычисление NAT-профиля находятся в `nat_detection`.
+5
View File
@@ -9,6 +9,9 @@
// Порядок кодирования: encrypt-then-sign (подпись заголовка и payload, включая шифртекст).
// Список пройденных узлов добавляет/снимает etcp_router снаружи этого формата.
// При ошибке декодирования (плохая подпись / не удалось расшифровать) пакет дропается.
// Все вызовы — в потоке ядра; inst и его registry/ключи должны быть готовы.
// Вход заимствован и не изменяется; при успехе *out принадлежит вызывающему (u_free).
// При ошибке out/out_len не устанавливаются. Буфер выделяется и при mode=0/отсутствии crypto-флагов.
#ifndef ROUTE_CRYPTO_H
#define ROUTE_CRYPTO_H
@@ -28,6 +31,7 @@ struct UTUN_INSTANCE;
// Кодировать пакет: base = [SVC_ROUTE_HDR][plaintext payload].
// mode — битовая маска ROUTE_CRYPTO_SIGN / ROUTE_CRYPTO_ENCRYPT.
// peer_node_id — получатель ECDH; hdr.src_node_id должен быть локальным, исходные crypto-флаги очищены.
// Возвращает новый буфер (u_malloc) с финальным пакетом. 0 = успех, <0 = ошибка.
int route_crypto_encode(struct UTUN_INSTANCE* inst, uint64_t peer_node_id,
const uint8_t* base, size_t base_len, int mode,
@@ -36,6 +40,7 @@ int route_crypto_encode(struct UTUN_INSTANCE* inst, uint64_t peer_node_id,
// Декодировать пакет: wire = [SVC_ROUTE_HDR][payload][sig?].
// Проверяет подпись и/или расшифровывает. Через *rx_flags сообщает, что было на wire
// (ROUTE_CRYPTO_SIGN / ROUTE_CRYPTO_ENCRYPT). Возвращает [SVC_ROUTE_HDR][plaintext] с очищенными флагами.
// Ключ отправителя берётся по hdr.src_node_id из registry. rx_flags обязателен; может быть заполнен и при ошибке.
// 0 = успех, <0 = ошибка (пакет дропается).
int route_crypto_decode(struct UTUN_INSTANCE* inst,
const uint8_t* wire, size_t wire_len,
+30 -54
View File
@@ -21,22 +21,12 @@ static uint32_t prefix_to_mask(uint8_t prefix) {
}
static bool routes_overlap(uint32_t net1, uint8_t pre1, uint32_t net2, uint8_t pre2) {
if (pre1 == pre2 && net1 == net2) return true;
return false;
}
static bool check_route_overlap_in_table(uint32_t network, uint8_t prefix,
const struct ROUTE_ENTRY *entries, size_t count) {
for (size_t i = 0; i < count; i++) {
if (routes_overlap(network, prefix, entries[i].network, entries[i].prefix_length)) {
return true;
}
}
return false;
uint32_t mask = prefix_to_mask(pre1 < pre2 ? pre1 : pre2);
return (net1 & mask) == (net2 & mask);
}
// ============================================================================
// Поиск (LPM)
// Поиск по непересекающимся диапазонам
// ============================================================================
static int binary_search_insert_pos(struct ROUTE_ENTRY *entries, size_t count,
@@ -56,51 +46,19 @@ static int binary_search_insert_pos(struct ROUTE_ENTRY *entries, size_t count,
return left;
}
static struct ROUTE_ENTRY* binary_search_lpm(struct ROUTE_TABLE *table, uint32_t dest_ip) {
if (table->count == 0) return NULL;
/* В отсортированной таблице без пересечений подходящий диапазон может быть только один. */
static struct ROUTE_ENTRY* binary_search_route(struct ROUTE_TABLE *table, uint32_t dest_ip) {
int left = 0;
int right = (int)table->count - 1;
struct ROUTE_ENTRY *best = NULL;
uint8_t best_prefix = 0;
while (left <= right) {
int mid = left + (right - left) / 2;
struct ROUTE_ENTRY *entry = &table->entries[mid];
uint32_t mask = prefix_to_mask(entry->prefix_length);
if ((dest_ip & mask) == (entry->network & mask)) {
if (entry->prefix_length > best_prefix) {
best_prefix = entry->prefix_length;
best = entry;
}
if (mid + 1 < (int)table->count) {
struct ROUTE_ENTRY *next = &table->entries[mid + 1];
uint32_t next_mask = prefix_to_mask(next->prefix_length);
if ((dest_ip & next_mask) == (next->network & next_mask)) {
left = mid + 1;
} else {
break;
}
} else {
break;
}
} else if (dest_ip > entry->network) {
left = mid + 1;
} else {
right = mid - 1;
}
if ((dest_ip & mask) == entry->network) return entry;
if (dest_ip > entry->network) left = mid + 1;
else right = mid - 1;
}
// fallback проверка предыдущей записи
if (!best && left > 0) {
struct ROUTE_ENTRY *entry = &table->entries[left - 1];
uint32_t mask = prefix_to_mask(entry->prefix_length);
if ((dest_ip & mask) == (entry->network & mask)) {
best = entry;
}
}
return best;
return NULL;
}
// ============================================================================
@@ -160,8 +118,25 @@ bool route_insert(struct ROUTE_TABLE *table, struct TOPO_GROUP_NODE *node) {
for (const struct TOPO_SUBNET4* s = sub; s; s = s->next) {
uint32_t network; memcpy(&network, s->addr, 4); network = ntohl(network);
if (check_route_overlap_in_table(network, s->prefix_length, table->entries, table->count)) {
DEBUG_WARN(DEBUG_CATEGORY_ROUTING, "overlap detected for %s/%d", ip_to_string(network).a, s->prefix_length);
if (s->prefix_length > 32) {
DEBUG_WARN(DEBUG_CATEGORY_ROUTING, "reject node=%016llx subnet=%s/%u: invalid IPv4 prefix",
(unsigned long long)node->node_id, ip_to_string(network).a, s->prefix_length);
return false;
}
for (size_t i = 0; i < table->count; i++) {
const struct ROUTE_ENTRY *e = &table->entries[i];
if (!routes_overlap(network, s->prefix_length, e->network, e->prefix_length)) continue;
DEBUG_WARN(DEBUG_CATEGORY_ROUTING, "reject node=%016llx subnet=%s/%u: overlaps node=%016llx subnet=%s/%u",
(unsigned long long)node->node_id, ip_to_string(network).a, s->prefix_length,
(unsigned long long)e->v_node_info->node_id, ip_to_string(e->network).a, e->prefix_length);
return false;
}
for (const struct TOPO_SUBNET4 *previous = sub; previous != s; previous = previous->next) {
uint32_t other; memcpy(&other, previous->addr, 4); other = ntohl(other);
if (!routes_overlap(network, s->prefix_length, other, previous->prefix_length)) continue;
DEBUG_WARN(DEBUG_CATEGORY_ROUTING, "reject node=%016llx subnet=%s/%u: overlaps own subnet=%s/%u",
(unsigned long long)node->node_id, ip_to_string(network).a, s->prefix_length,
ip_to_string(other).a, previous->prefix_length);
return false;
}
}
@@ -176,6 +151,7 @@ bool route_insert(struct ROUTE_TABLE *table, struct TOPO_GROUP_NODE *node) {
for (const struct TOPO_SUBNET4* s = sub; s; s = s->next) {
uint32_t network; memcpy(&network, s->addr, 4); network = ntohl(network);
network &= prefix_to_mask(s->prefix_length);
int pos = binary_search_insert_pos(table->entries, table->count, network, s->prefix_length);
if (pos < (int)table->count) memmove(&table->entries[pos + 1], &table->entries[pos], (table->count - pos) * sizeof(struct ROUTE_ENTRY));
struct ROUTE_ENTRY* e = &table->entries[pos];
@@ -210,7 +186,7 @@ struct ROUTE_ENTRY* route_lookup(struct ROUTE_TABLE *table, uint32_t dest_ip) {
DEBUG_TRACE(DEBUG_CATEGORY_ROUTING, "dest_ip=%s", ip_to_string(dest_ip).a);
struct ROUTE_ENTRY *result = binary_search_lpm(table, dest_ip);
struct ROUTE_ENTRY *result = binary_search_route(table, dest_ip);
if (result) {
table->stats.routes_lookup_hits++;
+14 -12
View File
@@ -1,5 +1,7 @@
/* route_lib — локальная IPv4-таблица подсетей UTUN, отсортированный массив без пересечения маршрутов.
* create → insert/add_local_subnet → lookup → delete/destroy; все операции в одном uasync-потоке.
* create → insert → lookup → delete/destroy; все операции в одном uasync-потоке.
* Пересечения и дубликаты запрещены, в том числе внутри одного набора подсетей; /0 занимает всю таблицу.
* Вставка нормализует адреса по маске; при отказе таблица и её статистика не меняются.
* network/dest_ip/parse_subnet — host order; только is_local_subnet принимает network order.
* TOPO_GROUP_NODE заимствован: удалить его маршруты до освобождения узла.
* lookup возвращает элемент массива, действительный до следующего изменения таблицы. */
@@ -39,24 +41,24 @@ typedef enum {
struct ROUTE_ENTRY {
uint32_t network; // Сетевой адрес в host order
uint8_t prefix_length; // Длина префикса подсети
struct TOPO_GROUP_NODE* v_node_info; // узел владелец этих маршрутов. null если - локальный маршрут.
struct TOPO_GROUP_NODE* v_node_info; // заимствованный владелец; локальный маршрут тоже содержит узел
};
/**
* @brief Таблица маршрутизации
*
* Структура представляет собой таблицу маршрутизации, содержащую записи маршрутов, подсети и статистику.
* маршруты не должны пересекаться. т.е. адрес 192.168.1.1/30 нельзя добавить если есть 192.168.1.2/32 или 192.168.1.1/24
* Подсети не пересекаются: например, 192.168.1.0/24 исключает 192.168.1.0/30 и 192.168.1.2/32.
*/
struct ROUTE_TABLE {
struct ROUTE_ENTRY *entries; /**< Массив записей маршрутов */
size_t count; /**< Текущее количество записей */
size_t capacity; /**< Максимальная емкость таблицы */
uint64_t my_node_id; /**< ID локального узла */
uint32_t *dynamic_subnets; /**< Динамические подсети (массив пар: сеть, префикс) */
size_t dynamic_subnet_count; /**< Количество динамических подсетей */
uint32_t *local_subnets; /**< Локальные подсети (массив пар: сеть, префикс) */
size_t local_subnet_count; /**< Количество локальных подсетей */
uint32_t *dynamic_subnets; /**< Не используется текущей реализацией; destroy освобождает память */
size_t dynamic_subnet_count; /**< Не используется текущей реализацией */
uint32_t *local_subnets; /**< Не используется текущей реализацией; destroy освобождает память */
size_t local_subnet_count; /**< Не используется текущей реализацией */
struct {
uint64_t local_routes; /**< Количество локальных маршрутов */
uint64_t learned_routes; /**< Количество изученных маршрутов */
@@ -80,10 +82,12 @@ struct ROUTE_TABLE *route_table_create(uint64_t my_node_id);
void route_table_destroy(struct ROUTE_TABLE *table);
/**
* @brief Вставляет в таблицу маршрутизации все подсети для указанного узла
* @brief Атомарно вставляет все IPv4-подсети узла; /0..32, адреса нормализуются по маске
* @param table Таблица без пересечений; перед заменой маршрутов узла вызвать route_delete
* @param node узел, все маршруты которого нужно добавить
*
* @return true если подсети добавлены; false при отсутствии подсетей, пересечении или ошибке выделения памяти
* @return true если весь набор добавлен; false при неверных аргументах/префиксе, отсутствии подсетей,
* пересечении с таблицей/внутри набора или нехватке памяти. При false таблица не меняется.
*/
bool route_insert(struct ROUTE_TABLE *table, struct TOPO_GROUP_NODE *node);
@@ -112,7 +116,7 @@ struct ROUTE_ENTRY* route_lookup(struct ROUTE_TABLE *table, uint32_t dest_ip);
void route_table_print(const struct ROUTE_TABLE *table);
/**
* @brief Парсит строку подсети в сетевой адрес и длину префикса
* @brief Разбирает IPv4/префикс; host-биты адреса сохраняются, префикс читается через atoi
*
* @param subnet_str Строка подсети (например, "192.168.1.0/24")
* @param network Указатель для сохранения адреса в host order
@@ -121,8 +125,6 @@ void route_table_print(const struct ROUTE_TABLE *table);
*/
int parse_subnet(const char *subnet_str, uint32_t *network, uint8_t *prefix_length);
bool route_add_local_subnet(struct ROUTE_TABLE *table, uint32_t network, uint8_t prefix_length);
/**
* @brief Проверяет что IP адрес НЕ является публичным (роутится в интернете)
*
+34 -31
View File
@@ -1,31 +1,34 @@
В таблице роутинга маршруты не пересекаются. Т.е. не может быть одновременно 192.168.1.1/24 и 192.168.1.100/30
Как работает роутинг:
1. типов маршрута бывает два: learned (роутятся в ETCP instance) и local (роутятся в TUN).
local - локальные маршруты из конфига (опция конфига my_subnet=IP/Mask)
при инициализации local сразу добавляются в роутинг таблицу (ипользуя запись BGP_NODEINFO_PACKET* my_nodeinfo).
BGP_NODEINFO собирается на узле владельце node, далее распространяется по остальным узлам.
2. При установке подключения к новому узлу мы отправляем этому узлу все nodeinfo. Если несколько доступных линков - отправляем только один - preferred_conn
3. При изменении preferred_conn рассылаем reroute (например старый preferred_conn удален)
3. При удалении всех подключений рассылаем withdraw.
3. При получении BGP_NODEINFO_PACKET мы смотрим есть ли такой узел уже в таблице. Если узла нет - добавляем, если есть - обновляем информацию о узле включая его маршруты и список next_hop через которые доступен узел (nb_routes).
Добавляем так:
инкрементируем hop_count
устанавливая etcp линк с которого приняли как next_hop, добавляем его в hop_list.
рассылаем по всем активным линкам кроме линка с которого получили (обязательно)
Обновление: обновляем hop_list если поменялся
4. При отключении от узла (если у node не осталось nb_routes) мы:
Удаляем node и вложенные структуры: все маршруты узла
Рассылаем withdraw для узла hop_id.
Логика рассылки withdraw:
Если получен withdraw - удаляем этот маршрут и распространяем withdraw или reroute по всем линкам кроме того с которого получили. В зависимости от изменений (остались ли резервные линки или изменился preferred_conn).
Метрик маршрута пока нет. используется первый доступный.
Как работаеут маршрутизация:
destination IP -> route table lookup -> route entry (ip/mask, node_info*) -> выбор лучшего next_hop -> отправка в next_hop
route_lib — локальная таблица IPv4-подсетей UTUN
================================================
Подсети в таблице не пересекаются. Запрещены одинаковые и вложенные диапазоны,
как между узлами, так и внутри одного переданного набора. Например,
10.0.0.0/8 исключает 10.1.0.0/24; соседние 10.1.0.0/24 и 10.1.1.0/24 допустимы.
Маршрут /0 занимает всё адресное пространство и может быть только единственным.
route_insert(table, node) добавляет все node->subnets->v4_subnets атомарно.
Префиксы должны быть в диапазоне 0..32; адреса нормализуются маской перед хранением.
При неверных аргументах, отсутствии подсетей, пересечении или OOM возвращает false,
не меняя таблицу и её статистику. Перед заменой маршрутов узла вызывают route_delete.
Таблица — отсортированный массив. route_lookup получает IPv4 в host byte order
и бинарным поиском возвращает единственный подходящий диапазон либо NULL.
Это не LPM-таблица: вложенные префиксы исключены при вставке.
Указатель на ROUTE_ENTRY действителен только до следующего изменения таблицы.
Адреса TOPO_SUBNET4 хранятся в сетевых байтах, parse_subnet возвращает host order,
is_local_subnet принимает network order. parse_subnet сохраняет host-биты и читает
префикс через atoi; это не строгий валидатор всей строки CIDR.
TOPO_GROUP_NODE заимствован, в том числе для локальных маршрутов.
Маршруты удаляют до освобождения узла. destroy освобождает массив, но не узлы.
Все вызовы выполняются в одном потоке ядра.
Наполнение таблицы и поиск следующего узла — разные задачи:
IPv4 destination -> route_lookup -> TOPO_GROUP_NODE -> etcp_route_send
-> выбор живого next hop через CM/BGP -> ETCP.
Для локального владельца routing доставляет пакет в TUN после firewall.
Групповые пути, WITHDRAW, выбор по числу переходов и READY описаны в topo_group.h;
общая подписанная запись узла — в topo_node.h и doc/node_snapshot.md.
IPv6-таблица route6_lib использует radix и разрешает вложенные префиксы.
Основной routing.c пока доставляет только IPv4.
+12 -2
View File
@@ -1,5 +1,6 @@
/* Прямые UDP-наблюдения с указанного рабочего сокета. Управление помощниками —
* nat_detection; этот модуль не вычисляет NAT-тип и не выбирает чужой сокет. */
* nat_detection; этот модуль не вычисляет NAT-тип и не выбирает чужой сокет.
* Вызовы с ETCP_SOCKET выполняются в потоке его ядра; буферы адресов/тегов предоставляет вызывающий. */
#ifndef ROUTE_PING_H
#define ROUTE_PING_H
@@ -14,16 +15,25 @@ extern "C" {
#define ROUTE_ADDR_WIRE_SIZE 19
#define ROUTE_OBSERVE_TAG_SIZE 33
/* family(1:4/6), IP(16: IPv4 + 12 нулей либо IPv6), port(2 BE), без IPv6 scope_id.
* encode/decode: 0/-1; decode также отвергает нулевой порт и ненулевой IPv4-padding.
* При ошибке выход может быть частично заполнен. encode сам по себе нулевой порт не запрещает. */
int route_ping_addr_encode(uint8_t out[ROUTE_ADDR_WIRE_SIZE], const struct sockaddr_storage *address);
int route_ping_addr_decode(const uint8_t in[ROUTE_ADDR_WIRE_SIZE], struct sockaddr_storage *address);
/* 1 — совпадают семейство/IP и (при include_port!=0) порт; 0 — иначе. IPv6 scope_id не сравнивается. */
int route_ping_addr_equal(const struct sockaddr_storage *a, const struct sockaddr_storage *b, int include_port);
/* Ответ: address(19) || tag(33); tag связывает socket/generation/round/group. Числа BE, IP — сетевые байты. */
/* Отправить OBSERVE PING: group/round и generation сокета ненулевые, timeout_ms — мс.
* peer_key — 32 байта ключа наблюдателя, peer_address — его UDP endpoint.
* Возвращает 0 при запуске, отрицательный код ошибки ETCP при отказе; callback/arg — контракт ETCP.
* Ответ: address(19) || tag(33); профиль сокета здесь не изменяется. */
int route_ping_observe(struct ETCP_SOCKET *socket, const uint8_t peer_key[SC_PUBKEY_SIZE],
const struct sockaddr_storage *peer_address, uint64_t group, uint64_t round, int timeout_ms,
etcp_ping_callback_t callback, void *arg);
/* Проверить точные размер/тег и декодировать observed; 0/-1. Подлинность ответа проверяет ETCP. */
int route_ping_observation(const uint8_t *data, size_t length, const struct ETCP_SOCKET *socket,
uint64_t group, uint64_t round, struct sockaddr_storage *observed);
/* NAT4UDP!(8) || socket_id(1) || generation(8 BE) || round(8 BE) || group(8 BE); out/socket обязательны. */
void route_ping_make_tag(uint8_t out[ROUTE_OBSERVE_TAG_SIZE], const struct ETCP_SOCKET *socket, uint64_t group, uint64_t round);
#ifdef __cplusplus
+8 -7
View File
@@ -4,15 +4,16 @@
`ETCP_SOCKET`. Выбор B/C, пересылка управляющих сообщений и классификация
профиля принадлежат `nat_detection`; алгоритм — в `doc/nat_detection.txt`.
`route_ping_observe(socket, peer_key, peer_address, round, timeout_ms, callback, arg)`
`route_ping_observe(socket, peer_key, peer_address, group, round, timeout_ms, callback, arg)`
передаёт PING с `ETCP_PING_FLAG_OBSERVE`. PONG содержит фактически наблюдаемый
source IP:port и исходный тег. Профиль не меняется от единственного ответа.
`route_ping_observation` проверяет размер, ID/поколение сокета и round.
`route_ping_observation` проверяет размер, ID/поколение сокета, round и group.
Адрес занимает 19 байт: family(1: 4/6), IP(16: IPv4 + 12 нулей либо IPv6),
port(2, BE). Тег занимает 25 байт: `NAT2UDP!`(8), socket_id(1),
generation(8, BE), round(8, BE). Неправильные family, скрытые IPv4-байты
и нулевой порт отвергаются. Новый wire не использует native packed integers.
port(2, BE). Тег занимает 33 байта: `NAT4UDP!`(8), socket_id(1),
generation(8, BE), round(8, BE), group(8, BE). Декодирование отвергает неправильные
family, скрытые IPv4-байты и нулевой порт. IPv6 scope_id в wire не передаётся.
Кодирование нулевой порт не запрещает. Числа тега передаются в BE.
В транспорте nonce генерируется CSPRNG; PONG должен соответствовать ожидаемому
ключу, source IP:port и принимающему socket generation. Pending снимается
@@ -20,8 +21,8 @@ generation(8, BE), round(8, BE). Неправильные family, скрытые
совпадающие запросы без callback; arg остаётся собственностью вызывающего.
`ETCP_PING_FLAG_ADDRESS_CHECK` обозначает прямое входящее испытание. Его
receipt фиксируется на рабочем сокете владельца до отправки PONG. Потеря
обратного PONG поэтому не превращает полученный ingress в отрицательный результат.
приём передаётся `nat_detection_receive_ingress` без UDP-ответа: ответ мог бы
испортить историю контактов. Результат передаётся по защищённому управляющему каналу.
UDP-результаты не используются для TCP listener и SOCKS5 endpoint.
Проверки: `tests/test_route_ping.c`, `tests/test_nat_detection.c`,
+9 -3
View File
@@ -1,6 +1,7 @@
/* Учёт первой доставки payload логического канала. Один uasync-поток.
* Десять завершённых интервалов по 50мс и текущий интервал; reset начинает новую историю.
* Повторы учитывает вызывающий: add вызывается только при снятии подтверждённого inflight. */
* Повторы учитывает вызывающий: ack вызывается только при снятии подтверждённого inflight.
* Время — монотонные микросекунды; память всех структур принадлежит вызывающему. */
#ifndef ROUTE_TX_STATS_H
#define ROUTE_TX_STATS_H
@@ -14,16 +15,21 @@ extern "C" {
#define ROUTE_TX_WINDOW_US 500000u
#define ROUTE_TX_BUCKETS 11
/* tick_plus_one=0 означает пустую ячейку; иначе это номер 50-мс интервала + 1. */
struct route_tx_bucket { uint64_t tick_plus_one, bytes, packets; };
struct route_tx_meter {
struct route_tx_bucket buckets[ROUTE_TX_BUCKETS];
uint64_t started_us, last_us, bytes, packets;
uint64_t started_us, last_us, bytes, packets; /* reset/последний ACK и накопленные итоги эпохи */
};
/* end_us — правая граница окна; valid=1 после накопления полных 500 мс с момента reset. */
struct route_tx_window { uint64_t end_us, bytes, packets; int valid; };
/* Обнулить историю и начать эпоху в now_us; meter обязателен. */
void route_tx_meter_reset(struct route_tx_meter *meter, uint64_t now_us);
/* Учесть один впервые подтверждённый payload (bytes>0); 0/-1, при ошибке история не меняется. */
int route_tx_meter_ack(struct route_tx_meter *meter, uint64_t now_us, uint64_t bytes);
/* Неполное окно после reset возвращает valid=0; текущая незавершённая ячейка исключается. */
/* Заполнить out суммой завершённых ячеек; 0/-1 (при ошибке out не меняется).
* Неполное окно после reset возвращает valid=0; текущая незавершённая ячейка исключается. */
int route_tx_meter_window(const struct route_tx_meter *meter, uint64_t now_us, struct route_tx_window *out);
#ifdef __cplusplus
+4 -2
View File
@@ -33,7 +33,7 @@ int routing_create(struct UTUN_INSTANCE* instance);
int routing_bind(struct UTUN_INSTANCE* instance);
/**
* @brief Destroy routing module for instance
* @brief Снять DATA binding и освободить IPv4-таблицу; callback TUN снимается при остановке самого TUN
* @param instance UTUN instance
*/
void routing_destroy(struct UTUN_INSTANCE* instance);
@@ -58,7 +58,9 @@ void routing_del_conn(struct ETCP_CONN* etcp);
void routing_set_tun(struct UTUN_INSTANCE* instance);
/**
* @brief Route a single packet (exposed for NAT module interception)
* @brief Маршрутизировать [cmd:1][IPv4] в TUN либо через TOPO_GROUP_UTUN; требует готовых instance->rt/tun
* @param entry Передаваемые entry и dgram: модуль забирает владение при любом исходе
* @param src_node_id Исходный узел для диагностики, для выхода из TUN — instance->node_id
*/
void route_pkt(struct UTUN_INSTANCE* instance, struct ll_entry* entry, uint64_t src_node_id);
+6 -1
View File
@@ -20,8 +20,13 @@ IPv6-пакеты на данный момент дропаются на эта
**Ключевые нюансы:**
- `routing_add_conn` / `routing_del_conn` — **deprecated** (no-op), маршруты теперь управляются через BGP и таблицу `route_lib`
- Все не-IPv4 пакеты (IPv6, unknown) дропаются в `extract_dst_ip`
- Multicast (224.0.0.0/4) и limited broadcast (255.255.255.255) дропаются без логирования
- Multicast (224.0.0.0/4) и limited broadcast (255.255.255.255) отбрасываются с DEBUG-логом routing
- Пакеты без маршрута дропаются с инкрементом `instance->dropped_packets`
- `route_pkt` забирает entry и dgram при любом исходе; ожидает `[cmd:1][IPv4]` и готовые `rt`/`tun`.
- Удалённая доставка идёт через `TOPO_GROUP_UTUN`; локальная — в `tun->input_queue`.
Firewall проверяется перед маршрутизацией выхода из TUN и перед локальной доставкой в TUN.
- Все вызовы выполняются в потоке ядра. `routing_destroy` снимает DATA binding и освобождает таблицу;
callback очереди TUN снимается при остановке самого TUN/UTUN-сервиса.
## 3. API
+10 -5
View File
@@ -21,17 +21,22 @@ struct ETCP_SOCKET;
#define SOCK_TC_PROBE 4
struct sock_view {
struct ETCP_SOCKET *sock;
uint8_t role, has_inet, has_priv, family, is_tcp;
uint32_t local_ip4, public_ip4;
struct address_profile address_profile;
struct ETCP_SOCKET *sock; /* заимствован; не переживает удаление сокета */
uint8_t role, has_inet, has_priv, family, is_tcp; /* SOCK_ROLE_*, доступ к Internet/LAN, AF_INET, TCP-флаг */
uint32_t local_ip4, public_ip4; /* network order; public_ip4=0, если публичный адрес неизвестен */
struct address_profile address_profile; /* копия текущего профиля */
};
/* public_address — классификация IP в соответствующем семействе, не доказательство входа. */
/* Возвращает SOCK_TC_*; address_type=TOPO_ADDR_*, config_type=CFG_SERVER_TYPE_*.
* public_address — классификация IP в соответствующем семействе, не доказательство входа. */
uint8_t sock_target_class(uint8_t address_type, uint8_t config_type, const struct address_profile *profile,
int public_address);
/* 1 — разрешена попытка по target_ip4 (network order), 0 — нет; проверяет has_priv/has_inet.
* Семейство/протокол и выбор маршрута проверяет вызывающий; profile может быть NULL. */
int sock_match(const struct sock_view *view, uint8_t address_type, uint8_t config_type,
const struct address_profile *profile, uint32_t target_ip4);
/* В потоке ядра заполнить до max views обычных IPv4-сокетов, включая TCP; probe_only исключены.
* Возвращает число заполненных (0 также при неверных аргументах); специальной сортировки нет. */
int sock_collect_views(struct UTUN_INSTANCE *instance, struct sock_view *out, int max);
#endif
+6 -1
View File
@@ -1265,7 +1265,12 @@ int topo_group_process_nodeinfo(struct TOPO_GROUP* group, struct ETCP_CONN* from
}
int dyn = topo_node_dyn_size(ni);
if ((size_t)dyn + sizeof(struct TOPOMSG_NODEINFO_PKT) != len) { DEBUG_WARN(DEBUG_CATEGORY_BGP, "Incorrect packet size (%s)", from->log_name); return -1; }
if (dyn < 0) return -1; // Причина уже записана в topo_node_dyn_size.
if ((size_t)dyn != len - sizeof(struct TOPOMSG_NODEINFO_PKT)) {
DEBUG_WARN(DEBUG_CATEGORY_BGP, "NODEINFO size mismatch from=%s node=%016llx length=%zu expected=%zu",
from->log_name, (unsigned long long)ni->node_id, len, sizeof(struct TOPOMSG_NODEINFO_PKT) + (size_t)dyn);
return -1;
}
uint64_t node_id = ni->node_id;
if (ni->hop_count >= MAX_HOPS) { DEBUG_WARN(DEBUG_CATEGORY_BGP, "NODEINFO from %s dropped: too many hops (%d)", from->log_name, ni->hop_count); return -1; }
+322 -214
View File
@@ -1,6 +1,7 @@
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <limits.h>
#include "../lib/ll_queue.h"
#include "../lib/debug_config.h"
@@ -475,15 +476,20 @@ int topo_node_verify(const struct TOPO_NODE* ni) {
/* Вычисляет суммарный размер динамической части NODEINFO по счётчикам заголовка. */
int topo_node_dyn_size(const struct TOPOMSG_NODE* msg) {
if (!msg) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "NODEINFO size: missing header"); return -1; }
if (!(msg->flags & TOPO_FLAG_SEND_SUBNETS) && (msg->local_v4_subnets || msg->local_v6_subnets)) {
DEBUG_WARN(DEBUG_CATEGORY_BGP, "NODEINFO size: subnets without flag node=%016llx flags=%02x v4=%u v6=%u",
(unsigned long long)msg->node_id, msg->flags, msg->local_v4_subnets, msg->local_v6_subnets);
return -1;
}
return msg->node_name_len
+ msg->local_v4_sockets * sizeof(struct TOPOMSG_SOCKMETA4)
+ msg->local_v4_addrs * sizeof(struct TOPOMSG_ADDR4)
+ msg->local_v6_sockets * sizeof(struct TOPOMSG_SOCKMETA6)
+ msg->local_v6_addrs * sizeof(struct TOPOMSG_ADDR6)
+ msg->local_reality_socks * sizeof(struct TOPOMSG_REALITY_SOCK)
+ ((msg->flags & TOPO_FLAG_SEND_SUBNETS)
? (msg->local_v4_subnets * sizeof(struct TOPOMSG_SUBNET4) + msg->local_v6_subnets * sizeof(struct TOPOMSG_SUBNET6))
: 0)
+ msg->local_v4_subnets * sizeof(struct TOPOMSG_SUBNET4)
+ msg->local_v6_subnets * sizeof(struct TOPOMSG_SUBNET6)
+ msg->hop_count * 8;
}
@@ -491,7 +497,7 @@ int topo_node_dyn_size(const struct TOPOMSG_NODE* msg) {
int topo_node_serialize(struct TOPO_NODE* ni, struct TOPO_GROUP_NODE* nq,
uint64_t group_id, uint8_t flags,
uint8_t* out, size_t out_max, uint16_t cumulative_rtt) {
if (!ni || !nq || !out) return -1;
if (!ni || !nq || !out) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "NODEINFO encode: missing node/group/output"); return -1; }
struct TOPOMSG_NODE msg;
memset(&msg, 0, sizeof(msg));
@@ -510,7 +516,8 @@ int topo_node_serialize(struct TOPO_NODE* ni, struct TOPO_GROUP_NODE* nq,
topo_list_count((struct _topo_head*)ni->v6_sock_meta) > UINT8_MAX ||
topo_list_count((struct _topo_head*)ni->v6_addrs) > UINT8_MAX ||
topo_list_count((struct _topo_head*)ni->reality_socks) > UINT8_MAX ||
(nq->subnets && (topo_list_count((struct _topo_head*)nq->subnets->v4_subnets) > UINT8_MAX ||
((flags & TOPO_FLAG_SEND_SUBNETS) && nq->subnets &&
(topo_list_count((struct _topo_head*)nq->subnets->v4_subnets) > UINT8_MAX ||
topo_list_count((struct _topo_head*)nq->subnets->v6_subnets) > UINT8_MAX))) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "node exceeds wire format limits"); return -1;
}
@@ -519,18 +526,25 @@ int topo_node_serialize(struct TOPO_NODE* ni, struct TOPO_GROUP_NODE* nq,
msg.local_v6_sockets = topo_list_count((struct _topo_head*)ni->v6_sock_meta);
msg.local_v6_addrs = topo_list_count((struct _topo_head*)ni->v6_addrs);
msg.local_reality_socks = topo_list_count((struct _topo_head*)ni->reality_socks);
msg.local_v4_subnets = nq->subnets ? topo_list_count((struct _topo_head*)nq->subnets->v4_subnets) : 0;
msg.local_v6_subnets = nq->subnets ? topo_list_count((struct _topo_head*)nq->subnets->v6_subnets) : 0;
{ uint8_t hc; uint64_t* hl = topo_node_best_hop_list(nq, &hc, NULL);
msg.hop_count = hc; }
if ((flags & TOPO_FLAG_SEND_SUBNETS) && nq->subnets) {
msg.local_v4_subnets = topo_list_count((struct _topo_head*)nq->subnets->v4_subnets);
msg.local_v6_subnets = topo_list_count((struct _topo_head*)nq->subnets->v6_subnets);
}
uint64_t *hop_list = topo_node_best_hop_list(nq, &msg.hop_count, NULL);
msg.client_type = ni->client_type;
msg.client_activity = ni->client_activity;
msg.cumulative_rtt = cumulative_rtt;
if (nq->radio) msg.flags |= TOPO_FLAG_RADIO;
msg.flags |= nq->phone_status;
size_t total = TOPOMSG_NODE_HDR_SIZE + (size_t)topo_node_dyn_size(&msg);
if (total > out_max) return -1;
int dyn = topo_node_dyn_size(&msg);
if (dyn < 0) return -1; // Причина уже записана в topo_node_dyn_size.
size_t total = TOPOMSG_NODE_HDR_SIZE + (size_t)dyn;
if (total > out_max) {
DEBUG_WARN(DEBUG_CATEGORY_BGP, "NODEINFO encode: output too small node=%016llx need=%zu capacity=%zu",
(unsigned long long)ni->node_id, total, out_max);
return -1;
}
memcpy(out, &msg, TOPOMSG_NODE_HDR_SIZE);
uint8_t* dp = out + TOPOMSG_NODE_HDR_SIZE;
@@ -574,8 +588,9 @@ int topo_node_serialize(struct TOPO_NODE* ni, struct TOPO_GROUP_NODE* nq,
}}
}
}
{ uint8_t hc; uint64_t* hl = topo_node_best_hop_list(nq, &hc, NULL);
if (hc && hl) { size_t sz = hc * 8; memcpy(dp, hl, sz); dp += sz; } }
if (msg.hop_count) { size_t size = msg.hop_count * sizeof(*hop_list); memcpy(dp, hop_list, size); dp += size; }
DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "NODEINFO encoded: node=%016llx bytes=%zu flags=%02x subnets=%u/%u hops=%u",
(unsigned long long)ni->node_id, total, msg.flags, msg.local_v4_subnets, msg.local_v6_subnets, msg.hop_count);
return (int)(dp - out);
}
@@ -585,129 +600,190 @@ int topo_node_serialize(struct TOPO_NODE* ni, struct TOPO_GROUP_NODE* nq,
int topo_node_deserialize(struct TOPO_GROUP* group, const uint8_t* data, size_t len, struct TOPO_NODE** out_ni, struct TOPO_NODESUBNETS** out_subnets,
uint64_t** out_hop_list, uint8_t* out_hop_count,
uint16_t* out_cumulative_rtt) {
if (!group || !data || len < TOPOMSG_NODE_HDR_SIZE || !out_ni) return -1;
if (out_ni) *out_ni = NULL;
if (out_subnets) *out_subnets = NULL;
if (out_hop_list) *out_hop_list = NULL;
if (out_hop_count) *out_hop_count = 0;
if (out_cumulative_rtt) *out_cumulative_rtt = 0;
if (!group || !group->instance || !group->instance->topo_groups || !data || !out_ni) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "NODEINFO decode: missing group/instance/registry/data/output");
return -1;
}
DEBUG_TRACE(DEBUG_CATEGORY_BGP, "NODEINFO decode: length=%zu", len);
if (len < TOPOMSG_NODE_HDR_SIZE) {
DEBUG_WARN(DEBUG_CATEGORY_BGP, "NODEINFO decode: short header length=%zu need=%zu", len, TOPOMSG_NODE_HDR_SIZE);
return -1;
}
const struct TOPOMSG_NODE* msg = (const struct TOPOMSG_NODE*)data;
int dyn = topo_node_dyn_size(msg);
if ((size_t)dyn + TOPOMSG_NODE_HDR_SIZE != len) return -1;
if (dyn < 0) return -1; // Причина уже записана в topo_node_dyn_size.
if ((size_t)dyn != len - TOPOMSG_NODE_HDR_SIZE) {
DEBUG_WARN(DEBUG_CATEGORY_BGP, "NODEINFO decode: size mismatch node=%016llx length=%zu expected=%zu flags=%02x subnets=%u/%u hops=%u",
(unsigned long long)msg->node_id, len, TOPOMSG_NODE_HDR_SIZE + (size_t)dyn,
msg->flags, msg->local_v4_subnets, msg->local_v6_subnets, msg->hop_count);
return -1;
}
struct TOPO_NODE* ni = u_calloc(1, sizeof(struct TOPO_NODE));
if (!ni) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "alloc TOPO_NODE failed"); return -1; }
ni->group_ref_count = 0;
ni->node_id = msg->node_id;
ni->ver = msg->ver;
ni->timestamp = msg->timestamp;
struct TOPO_GROUPS* groups = group->instance->topo_groups;
struct TOPO_NODE* ni = u_calloc(1, sizeof(*ni));
struct TOPO_NODESUBNETS* subnets = NULL;
uint64_t* hops = NULL;
const uint8_t* dp = data + TOPOMSG_NODE_HDR_SIZE;
const char* section = "node";
unsigned index = 0;
if (!ni) goto no_memory;
ni->node_id = msg->node_id; ni->ver = msg->ver; ni->timestamp = msg->timestamp;
memcpy(ni->public_key, msg->public_key, SC_PUBKEY_SIZE);
memcpy(ni->ed25519_public_key, msg->ed25519_public_key, SC_PUBKEY_SIZE);
memcpy(ni->x25519_self_sig, msg->x25519_self_sig, 64);
ni->client_type = msg->client_type;
ni->client_activity = msg->client_activity;
const uint8_t* dp = data + TOPOMSG_NODE_HDR_SIZE;
ni->client_type = msg->client_type; ni->client_activity = msg->client_activity;
section = "name";
if (msg->node_name_len) {
ni->node_name = u_malloc(msg->node_name_len + 1);
if (ni->node_name) { memcpy(ni->node_name, dp, msg->node_name_len); ni->node_name[msg->node_name_len] = 0; }
dp += msg->node_name_len;
if (!ni->node_name) goto no_memory;
memcpy(ni->node_name, dp, msg->node_name_len); ni->node_name[msg->node_name_len] = 0;
}
dp += msg->node_name_len;
{ struct TOPO_SOCKMETA4* head = NULL; struct TOPO_SOCKMETA4* tail = NULL;
for (int i = 0; i < msg->local_v4_sockets; i++) {
/* Списки сразу принадлежат ni: общий выход ошибки видит и незавершённую секцию. */
section = "v4 socket metadata";
{ struct TOPO_SOCKMETA4** tail = &ni->v4_sock_meta;
for (index = 0; index < msg->local_v4_sockets; index++) {
const struct TOPOMSG_SOCKMETA4* w = (const struct TOPOMSG_SOCKMETA4*)dp; dp += sizeof(*w);
struct TOPO_SOCKMETA4* e = memory_pool_alloc(group->instance->topo_groups->v4_sock_meta_pool);
if (!e) { topo_node_free_raw(group->instance->topo_groups, ni); return -1; }
e->id = w->id; e->config_type = w->config_type; e->next = NULL;
if (sockmeta_wire_read((const uint8_t *)w, &e->address_profile)) {
memory_pool_free(group->instance->topo_groups->v4_sock_meta_pool, e);
ni->v4_sock_meta = head; topo_node_free_raw(group->instance->topo_groups, ni); return -1;
}
if (!head) head = tail = e; else { tail->next = e; tail = e; }
struct TOPO_SOCKMETA4* e = memory_pool_alloc(groups->v4_sock_meta_pool);
if (!e) goto no_memory;
*tail = e; tail = &e->next;
e->id = w->id; e->config_type = w->config_type;
if (sockmeta_wire_read((const uint8_t*)w, &e->address_profile)) goto invalid_profile;
}
ni->v4_sock_meta = head; }
{ struct TOPO_ADDR4* head = NULL; struct TOPO_ADDR4* tail = NULL;
for (int i = 0; i < msg->local_v4_addrs; i++) {
}
section = "v4 addresses";
{ struct TOPO_ADDR4** tail = &ni->v4_addrs;
for (index = 0; index < msg->local_v4_addrs; index++) {
const struct TOPOMSG_ADDR4* w = (const struct TOPOMSG_ADDR4*)dp; dp += sizeof(*w);
struct TOPO_ADDR4* e = memory_pool_alloc(group->instance->topo_groups->v4_addr_pool);
if (!e) { topo_node_free_raw(group->instance->topo_groups, ni); return -1; }
memcpy(e->addr, w->addr, 4); e->port = w->port; e->type = w->type; e->socket_id = w->socket_id; e->protocol = w->protocol; e->next = NULL;
if (!head) head = tail = e; else { tail->next = e; tail = e; }
struct TOPO_ADDR4* e = memory_pool_alloc(groups->v4_addr_pool);
if (!e) goto no_memory;
*tail = e; tail = &e->next;
memcpy(e->addr, w->addr, 4); e->port = w->port; e->type = w->type;
e->socket_id = w->socket_id; e->protocol = w->protocol;
}
ni->v4_addrs = head; }
{ struct TOPO_SOCKMETA6* head = NULL; struct TOPO_SOCKMETA6* tail = NULL;
for (int i = 0; i < msg->local_v6_sockets; i++) {
}
section = "v6 socket metadata";
{ struct TOPO_SOCKMETA6** tail = &ni->v6_sock_meta;
for (index = 0; index < msg->local_v6_sockets; index++) {
const struct TOPOMSG_SOCKMETA6* w = (const struct TOPOMSG_SOCKMETA6*)dp; dp += sizeof(*w);
struct TOPO_SOCKMETA6* e = memory_pool_alloc(group->instance->topo_groups->v6_sock_meta_pool);
if (!e) { topo_node_free_raw(group->instance->topo_groups, ni); return -1; }
e->id = w->id; e->config_type = w->config_type; e->next = NULL;
if (sockmeta_wire_read((const uint8_t *)w, &e->address_profile)) {
memory_pool_free(group->instance->topo_groups->v6_sock_meta_pool, e);
ni->v6_sock_meta = head; topo_node_free_raw(group->instance->topo_groups, ni); return -1;
}
if (!head) head = tail = e; else { tail->next = e; tail = e; }
struct TOPO_SOCKMETA6* e = memory_pool_alloc(groups->v6_sock_meta_pool);
if (!e) goto no_memory;
*tail = e; tail = &e->next;
e->id = w->id; e->config_type = w->config_type;
if (sockmeta_wire_read((const uint8_t*)w, &e->address_profile)) goto invalid_profile;
}
ni->v6_sock_meta = head; }
{ struct TOPO_ADDR6* head = NULL; struct TOPO_ADDR6* tail = NULL;
for (int i = 0; i < msg->local_v6_addrs; i++) {
}
section = "v6 addresses";
{ struct TOPO_ADDR6** tail = &ni->v6_addrs;
for (index = 0; index < msg->local_v6_addrs; index++) {
const struct TOPOMSG_ADDR6* w = (const struct TOPOMSG_ADDR6*)dp; dp += sizeof(*w);
struct TOPO_ADDR6* e = memory_pool_alloc(group->instance->topo_groups->v6_addr_pool);
if (!e) { topo_node_free_raw(group->instance->topo_groups, ni); return -1; }
memcpy(e->addr, w->addr, 16); e->port = w->port; e->type = w->type; e->socket_id = w->socket_id; e->protocol = w->protocol; e->next = NULL;
if (!head) head = tail = e; else { tail->next = e; tail = e; }
struct TOPO_ADDR6* e = memory_pool_alloc(groups->v6_addr_pool);
if (!e) goto no_memory;
*tail = e; tail = &e->next;
memcpy(e->addr, w->addr, 16); e->port = w->port; e->type = w->type;
e->socket_id = w->socket_id; e->protocol = w->protocol;
}
ni->v6_addrs = head; }
{ struct TOPO_REALITY_SOCK* head = NULL; struct TOPO_REALITY_SOCK* tail = NULL;
for (int i = 0; i < msg->local_reality_socks; i++) {
}
section = "REALITY sockets";
{ struct TOPO_REALITY_SOCK** tail = &ni->reality_socks;
for (index = 0; index < msg->local_reality_socks; index++) {
const struct TOPOMSG_REALITY_SOCK* w = (const struct TOPOMSG_REALITY_SOCK*)dp; dp += sizeof(*w);
struct TOPO_REALITY_SOCK* e = u_calloc(1, sizeof(struct TOPO_REALITY_SOCK));
if (!e) { topo_node_free_raw(group->instance->topo_groups, ni); return -1; }
struct TOPO_REALITY_SOCK* e = u_calloc(1, sizeof(*e));
if (!e) goto no_memory;
*tail = e; tail = &e->next;
e->socket_id = w->socket_id;
memcpy(e->short_id, w->short_id, 8);
memcpy(e->server_pubkey, w->server_pubkey, 32);
memcpy(e->version, w->version, 3);
memcpy(e->server_name, w->server_name, TOPO_REALITY_SNAME_MAX);
memcpy(e->short_id, w->short_id, 8); memcpy(e->server_pubkey, w->server_pubkey, 32);
memcpy(e->version, w->version, 3); memcpy(e->server_name, w->server_name, TOPO_REALITY_SNAME_MAX);
e->server_name[TOPO_REALITY_SNAME_MAX - 1] = 0;
e->next = NULL;
if (!head) head = tail = e; else { tail->next = e; tail = e; }
}
ni->reality_socks = head; }
}
if (out_subnets && (msg->flags & TOPO_FLAG_SEND_SUBNETS) && (msg->local_v4_subnets || msg->local_v6_subnets)) {
struct TOPO_NODESUBNETS* r = u_calloc(1, sizeof(struct TOPO_NODESUBNETS));
if (!r) { topo_node_free_raw(group->instance->topo_groups, ni); return -1; }
for (int i = 0; i < msg->local_v4_subnets; i++) {
section = "subnets"; index = 0;
if (out_subnets && (msg->local_v4_subnets || msg->local_v6_subnets)) {
subnets = u_calloc(1, sizeof(*subnets));
if (!subnets) goto no_memory;
}
/* Курсор проходит все присутствующие секции; out_subnets управляет только выделением памяти. */
section = "v4 subnets";
{ struct TOPO_SUBNET4** tail = subnets ? &subnets->v4_subnets : NULL;
for (index = 0; index < msg->local_v4_subnets; index++) {
const struct TOPOMSG_SUBNET4* w = (const struct TOPOMSG_SUBNET4*)dp; dp += sizeof(*w);
struct TOPO_SUBNET4* e = memory_pool_alloc(group->instance->topo_groups->v4_subnet_pool);
if (!e) { u_free(r); topo_node_free_raw(group->instance->topo_groups, ni); return -1; }
if (w->prefix_length > 32) {
DEBUG_WARN(DEBUG_CATEGORY_BGP, "NODEINFO decode: invalid IPv4 prefix node=%016llx index=%u prefix=%u",
(unsigned long long)msg->node_id, index, w->prefix_length);
goto fail;
}
if (!subnets) continue;
struct TOPO_SUBNET4* e = memory_pool_alloc(groups->v4_subnet_pool);
if (!e) goto no_memory;
*tail = e; tail = &e->next;
memcpy(e->addr, w->addr, 4); e->prefix_length = w->prefix_length;
e->next = r->v4_subnets; r->v4_subnets = e;
}
for (int i = 0; i < msg->local_v6_subnets; i++) {
}
section = "v6 subnets";
{ struct TOPO_SUBNET6** tail = subnets ? &subnets->v6_subnets : NULL;
for (index = 0; index < msg->local_v6_subnets; index++) {
const struct TOPOMSG_SUBNET6* w = (const struct TOPOMSG_SUBNET6*)dp; dp += sizeof(*w);
struct TOPO_SUBNET6* e = memory_pool_alloc(group->instance->topo_groups->v6_subnet_pool);
if (!e) { u_free(r); topo_node_free_raw(group->instance->topo_groups, ni); return -1; }
if (w->prefix_length > 128) {
DEBUG_WARN(DEBUG_CATEGORY_BGP, "NODEINFO decode: invalid IPv6 prefix node=%016llx index=%u prefix=%u",
(unsigned long long)msg->node_id, index, w->prefix_length);
goto fail;
}
if (!subnets) continue;
struct TOPO_SUBNET6* e = memory_pool_alloc(groups->v6_subnet_pool);
if (!e) goto no_memory;
*tail = e; tail = &e->next;
memcpy(e->addr, w->addr, 16); e->prefix_length = w->prefix_length;
e->next = r->v6_subnets; r->v6_subnets = e;
}
*out_subnets = r;
} else if (!(msg->flags & TOPO_FLAG_SEND_SUBNETS)) {
dp += msg->local_v4_subnets * sizeof(struct TOPOMSG_SUBNET4)
+ msg->local_v6_subnets * sizeof(struct TOPOMSG_SUBNET6);
if (out_subnets) *out_subnets = NULL;
} else {
if (out_subnets) *out_subnets = NULL;
}
if (out_hop_list && msg->hop_count > 0) {
size_t hsz = msg->hop_count * 8;
*out_hop_list = u_malloc(hsz);
if (!*out_hop_list) { topo_node_free_raw(group->instance->topo_groups, ni); return -1; }
memcpy(*out_hop_list, dp, hsz); dp += hsz;
*out_hop_count = msg->hop_count;
section = "hops"; index = 0;
size_t hop_bytes = msg->hop_count * sizeof(*hops);
if (out_hop_list && hop_bytes) {
hops = u_malloc(hop_bytes);
if (!hops) goto no_memory;
memcpy(hops, dp, hop_bytes);
}
dp += hop_bytes;
if ((size_t)(dp - data) != len) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "NODEINFO decode: cursor mismatch node=%016llx consumed=%zu length=%zu",
(unsigned long long)msg->node_id, (size_t)(dp - data), len);
goto fail;
}
if (out_cumulative_rtt) *out_cumulative_rtt = msg->cumulative_rtt;
*out_ni = ni;
if (out_subnets) *out_subnets = subnets;
if (out_hop_list) *out_hop_list = hops;
if (out_hop_count) *out_hop_count = msg->hop_count;
if (out_cumulative_rtt) *out_cumulative_rtt = msg->cumulative_rtt;
DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "NODEINFO decoded: node=%016llx bytes=%zu flags=%02x subnets=%u/%u hops=%u keep_subnets=%d keep_hops=%d",
(unsigned long long)msg->node_id, len, msg->flags, msg->local_v4_subnets, msg->local_v6_subnets,
msg->hop_count, out_subnets != NULL, out_hop_list != NULL);
return 0;
invalid_profile:
DEBUG_WARN(DEBUG_CATEGORY_BGP, "NODEINFO decode: invalid profile node=%016llx section=%s index=%u",
(unsigned long long)msg->node_id, section, index);
goto fail;
no_memory:
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "NODEINFO decode: allocation failed node=%016llx section=%s index=%u offset=%zu",
(unsigned long long)msg->node_id, section, index, (size_t)(dp - data));
fail:
topo_node_free_raw(groups, ni);
if (subnets) {
free_v4_sub_list(groups->v4_subnet_pool, subnets->v4_subnets);
free_v6_sub_list(groups->v6_subnet_pool, subnets->v6_subnets);
u_free(subnets);
}
u_free(hops);
return -1;
}
/* Ищет per-group узел TOPO_GROUP_NODE в группе по node_id (NULL, если нет). */
@@ -896,23 +972,36 @@ void topo_node_dump_all(struct TOPO_GROUP* group) {
/* То же, что dump_all, но в строковый буфер (для control-сервера). */
int topo_node_format_all(struct TOPO_GROUP* group, char* buf, size_t buf_size) {
if (!group || !group->nodes || !buf || buf_size == 0) return 0;
int pos = 0;
if (buf && buf_size) buf[0] = '\0';
if (!group || !group->nodes || !buf || buf_size == 0) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "node dump: invalid arguments group=%p buf=%p capacity=%zu", (void*)group, (void*)buf, buf_size);
return 0;
}
if (buf_size > INT_MAX) buf_size = INT_MAX;
size_t pos = 0;
pos += snprintf(buf + pos, buf_size > (size_t)pos ? buf_size - pos : 0,
"=== BGP Nodes dump: %d nodes ===\n", queue_entry_count(group->nodes));
#define FMT_ADD(...) do { \
int added = snprintf(buf + pos, buf_size - pos, __VA_ARGS__); \
if (added < 0) { \
buf[pos] = '\0'; \
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "node dump: formatting failed after %zu bytes", pos); \
return -1; \
} \
if ((size_t)added >= buf_size - pos) { pos = buf_size - 1; goto truncated; } \
pos += (size_t)added; \
} while (0)
FMT_ADD("=== BGP Nodes dump: %d nodes ===\n", queue_entry_count(group->nodes));
int idx = 0;
struct ll_entry* e = group->nodes->head;
while (e && (size_t)pos < buf_size) {
while (e) {
struct TOPO_GROUP_NODE* nq = (struct TOPO_GROUP_NODE*)e;
struct TOPO_NODE* ni = topo_node_registry_find(group->instance->topo_groups, nq->node_id);
if (!ni) { e = e->next; continue; }
int is_local = (group->local_node == nq);
const char* name_s = ni->node_name ? ni->node_name : "";
#define FMT_ADD(fmt, ...) pos += snprintf(buf + pos, buf_size > (size_t)pos ? buf_size - pos : 0, fmt, ##__VA_ARGS__)
FMT_ADD("--- Node #%d: id=%016llx name=\"%s\" ver=%u grp=%llu%s ---\n",
idx, (unsigned long long)ni->node_id, name_s, ni->ver,
(unsigned long long)group->group_id, is_local ? " [LOCAL]" : "");
@@ -977,15 +1066,24 @@ int topo_node_format_all(struct TOPO_GROUP* group, char* buf, size_t buf_size) {
FMT_ADD("]\n");
}
#undef FMT_ADD
e = e->next; idx++;
}
#undef FMT_ADD
if ((size_t)pos >= buf_size) {
pos = (int)buf_size - 14; if (pos < 0) pos = 0;
pos += snprintf(buf + pos, buf_size - pos, "\n[TRUNCATED]\n");
DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "node dump: group=%016llx nodes=%d written=%zu capacity=%zu",
(unsigned long long)group->group_id, idx, pos, buf_size);
return (int)pos;
truncated: {
const char marker[] = "\n[TRUNCATED]\n";
size_t marker_len = sizeof(marker) - 1;
if (marker_len > pos) marker_len = pos;
memcpy(buf + pos - marker_len, marker, marker_len);
buf[pos] = '\0';
DEBUG_WARN(DEBUG_CATEGORY_BGP, "node dump truncated: group=%016llx written=%zu capacity=%zu",
(unsigned long long)group->group_id, pos, buf_size);
return (int)pos;
}
return pos;
}
// ===== update my nodeinfo =====
@@ -1009,114 +1107,124 @@ int topo_node_init_self(struct UTUN_INSTANCE* instance) {
return 0;
}
/* Пересоздаёт local_node группы при изменении имени/подсетей/ключей, инкрементируя версию. */
/* Готовит local_node целиком; отказ подготовки/публикации сохраняет прежний узел и маршруты. */
int topo_group_update_my_nodeinfo(struct UTUN_INSTANCE* instance, struct TOPO_GROUP* group) {
if (!instance || !group) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "invalid args"); return -1; }
size_t name_len = 0;
if (instance->name[0]) { name_len = strlen(instance->name); if (name_len > 63) name_len = 63; }
int vc = 0, vc6 = 0;
struct CFG_ROUTE_ENTRY* s = instance->config->my_subnets;
while (s) { if (s->ip.family == AF_INET) vc++; else if (s->ip.family == AF_INET6) vc6++; s = s->next; }
// Для chat групп подсети не передаём
if (group->group_type == TOPO_GROUP_TYPE_CHAT) { vc = 0; vc6 = 0; }
int changed = 1; uint8_t old_ver = 0;
if (group->local_node) {
struct TOPO_NODE* oni = topo_node_registry_find(instance->topo_groups, group->local_node->node_id);
if (oni) {
old_ver = oni->ver;
changed = (vc != (group->local_node->subnets ? topo_list_count((struct _topo_head*)group->local_node->subnets->v4_subnets) : 0))
|| (name_len != (oni->node_name ? strlen(oni->node_name) : 0))
|| (name_len && strncmp(instance->name, oni->node_name ? oni->node_name : "", name_len))
|| (memcmp(oni->public_key, instance->my_keys.public_key, SC_PUBKEY_SIZE) != 0)
|| (vc6 != (group->local_node->subnets ? topo_list_count((struct _topo_head*)group->local_node->subnets->v6_subnets) : 0))
|| (oni->client_type != instance->client_type)
|| (oni->client_activity != instance->client_activity)
|| (group->local_node->radio != group->radio_active)
|| (group->local_node->phone_status != call_phone_status(instance))
|| (memcmp(oni->ed25519_public_key, group->ed25519_public_key, SC_PUBKEY_SIZE) != 0);
}
if (!instance || !group || group->instance != instance || !instance->config || !instance->topo_groups) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "local node update: invalid instance/group/config"); return -1;
}
if (!changed && group->local_node)
DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "my nodeinfo unchanged (identity), ver=%d grp=%016llx",
group->local_node->last_ver, (unsigned long long)group->group_id);
if (changed) {
struct TOPO_NODE* ni = topo_node_registry_find(instance->topo_groups, instance->node_id);
if (ni) topo_node_ref(ni); /* новая группа удерживает запись до освобождения старой */
if (group->local_node) {
if (instance->rt) route_delete(instance->rt, group->local_node);
topo_nodeq_free_group_fields(instance->topo_groups, group->local_node);
u_free(group->local_node);
group->local_node = NULL;
}
group->local_node = u_calloc(1, sizeof(struct TOPO_GROUP_NODE));
if (!group->local_node) {
if (ni) topo_node_registry_unref(instance->topo_groups, instance->node_id);
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "local group node allocation failed"); return -1;
}
struct TOPO_GROUP_NODE* lq = group->local_node;
int new_identity = ni == NULL;
if (!ni) ni = u_calloc(1, sizeof(struct TOPO_NODE));
if (!ni) { u_free(lq); group->local_node = NULL; return -1; }
struct TOPO_NODE old_identity = *ni;
ni->node_id = instance->node_id;
ni->ver = (old_ver % 255) + 1;
ni->client_type = instance->client_type;
ni->client_activity = instance->client_activity;
lq->radio = group->radio_active;
lq->phone_status = call_phone_status(instance);
memcpy(ni->public_key, instance->my_keys.public_key, SC_PUBKEY_SIZE);
memcpy(ni->ed25519_public_key, group->ed25519_public_key, SC_PUBKEY_SIZE);
ni->node_name = u_strdup(instance->name);
if (!ni->node_name || topo_node_sign_self(instance, ni) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "cannot publish local identity");
u_free(ni->node_name); *ni = old_identity;
if (new_identity) topo_node_free_raw(instance->topo_groups, ni);
else topo_node_registry_unref(instance->topo_groups, instance->node_id);
u_free(lq); group->local_node = NULL; return -1;
}
u_free(old_identity.node_name);
if (new_identity) ni = topo_node_registry_store(instance->topo_groups, ni);
if (!ni) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "cannot register local identity"); return -1; }
lq->node_id = ni->node_id;
lq->last_ver = ni->ver;
lq->last_timestamp = ni->timestamp;
if (group->group_type != TOPO_GROUP_TYPE_CHAT && (vc || vc6)) {
struct TOPO_NODESUBNETS* r = u_calloc(1, sizeof(struct TOPO_NODESUBNETS));
s = instance->config->my_subnets;
while (s) {
if (s->ip.family == AF_INET) {
struct TOPO_SUBNET4* sub = memory_pool_alloc(group->instance->topo_groups->v4_subnet_pool);
memcpy(sub->addr, &s->ip.addr.v4, 4); sub->prefix_length = s->netmask;
sub->next = r->v4_subnets; r->v4_subnets = sub;
} else if (s->ip.family == AF_INET6) {
struct TOPO_SUBNET6* sub6 = memory_pool_alloc(group->instance->topo_groups->v6_subnet_pool);
memcpy(sub6->addr, &s->ip.addr.v6, 16); sub6->prefix_length = s->netmask;
sub6->next = r->v6_subnets; r->v6_subnets = sub6;
}
s = s->next;
struct TOPO_GROUPS* groups = instance->topo_groups;
struct TOPO_GROUP_NODE* old = group->local_node;
struct TOPO_GROUP_NODE* lq = u_calloc(1, sizeof(*lq));
if (!lq) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "local node update: allocation failed"); return -1; }
struct TOPO_NODE* ni = NULL;
char* name = NULL;
int new_identity = 0, retained = 0, routes_changed = 0, changed = 1, vc = 0, vc6 = 0;
lq->node_id = instance->node_id; lq->radio = group->radio_active; lq->phone_status = call_phone_status(instance);
if (group->group_type != TOPO_GROUP_TYPE_CHAT) {
struct TOPO_SUBNET4** tail4 = NULL; struct TOPO_SUBNET6** tail6 = NULL;
for (struct CFG_ROUTE_ENTRY* s = instance->config->my_subnets; s; s = s->next) {
if ((s->ip.family != AF_INET && s->ip.family != AF_INET6) ||
s->netmask > (s->ip.family == AF_INET ? 32 : 128)) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "local node update: invalid subnet family=%d prefix=%u", s->ip.family, s->netmask);
goto fail;
}
if (!lq->subnets) {
lq->subnets = u_calloc(1, sizeof(*lq->subnets));
if (!lq->subnets) goto allocation_failed;
tail4 = &lq->subnets->v4_subnets; tail6 = &lq->subnets->v6_subnets;
}
if (s->ip.family == AF_INET) {
struct TOPO_SUBNET4* sub = memory_pool_alloc(groups->v4_subnet_pool);
if (!sub) goto allocation_failed;
*sub = (struct TOPO_SUBNET4){.prefix_length = s->netmask}; memcpy(sub->addr, &s->ip.addr.v4, 4);
*tail4 = sub; tail4 = &sub->next; vc++;
} else {
struct TOPO_SUBNET6* sub = memory_pool_alloc(groups->v6_subnet_pool);
if (!sub) goto allocation_failed;
*sub = (struct TOPO_SUBNET6){.prefix_length = s->netmask}; memcpy(sub->addr, &s->ip.addr.v6, 16);
*tail6 = sub; tail6 = &sub->next; vc6++;
}
lq->subnets = r;
}
if (group->group_type == TOPO_GROUP_TYPE_CHAT)
DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "my_nodeinfo updated: v4sub=%d v6sub=%d ver=%d grp=%016llx",
vc, vc6, ni->ver, (unsigned long long)group->group_id);
else
DEBUG_INFO(DEBUG_CATEGORY_BGP, "my_nodeinfo updated: v4sub=%d v6sub=%d ver=%d grp=%016llx",
vc, vc6, ni->ver, (unsigned long long)group->group_id);
if (group->group_type != TOPO_GROUP_TYPE_CHAT && instance->rt && !route_insert(instance->rt, lq))
DEBUG_WARN(DEBUG_CATEGORY_ROUTING, "failed to insert local routes");
} else {
struct TOPO_NODE* lni = topo_node_registry_find(instance->topo_groups, group->local_node->node_id);
if (lni) group->local_node->last_ver = lni->ver;
if (vc > UINT8_MAX || vc6 > UINT8_MAX) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "local node update: subnet counters exceed wire limit v4=%d v6=%d", vc, vc6); goto fail;
}
}
ni = topo_node_registry_find(groups, instance->node_id);
changed = !old || !ni || strcmp(ni->node_name ? ni->node_name : "", instance->name) ||
memcmp(ni->public_key, instance->my_keys.public_key, SC_PUBKEY_SIZE) ||
memcmp(ni->ed25519_public_key, instance->my_ed25519_pubkey, SC_PUBKEY_SIZE) ||
ni->client_type != instance->client_type || ni->client_activity != instance->client_activity ||
old->radio != lq->radio || old->phone_status != lq->phone_status;
if (!changed) {
struct TOPO_SUBNET4* a4 = old->subnets ? old->subnets->v4_subnets : NULL;
struct TOPO_SUBNET4* b4 = lq->subnets ? lq->subnets->v4_subnets : NULL;
while (a4 && b4 && a4->prefix_length == b4->prefix_length && !memcmp(a4->addr, b4->addr, 4)) { a4 = a4->next; b4 = b4->next; }
struct TOPO_SUBNET6* a6 = old->subnets ? old->subnets->v6_subnets : NULL;
struct TOPO_SUBNET6* b6 = lq->subnets ? lq->subnets->v6_subnets : NULL;
while (a6 && b6 && a6->prefix_length == b6->prefix_length && !memcmp(a6->addr, b6->addr, 16)) { a6 = a6->next; b6 = b6->next; }
changed = a4 || b4 || a6 || b6;
}
if (!changed) {
old->last_ver = ni->ver; old->last_timestamp = ni->timestamp;
DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "local node unchanged: group=%016llx ver=%u", (unsigned long long)group->group_id, ni->ver);
goto unchanged;
}
name = u_strdup(instance->name);
if (!name) goto allocation_failed;
if (!ni) {
ni = u_calloc(1, sizeof(*ni)); new_identity = 1;
if (!ni) goto allocation_failed;
} else { topo_node_registry_ref(groups, instance->node_id); retained = 1; }
struct TOPO_NODE candidate = *ni;
candidate.node_id = instance->node_id; candidate.ver = (ni->ver % 255) + 1;
candidate.node_name = name; candidate.client_type = instance->client_type; candidate.client_activity = instance->client_activity;
memcpy(candidate.public_key, instance->my_keys.public_key, SC_PUBKEY_SIZE);
memcpy(candidate.ed25519_public_key, instance->my_ed25519_pubkey, SC_PUBKEY_SIZE);
if (group->group_type != TOPO_GROUP_TYPE_CHAT && instance->rt) {
routes_changed = 1;
route_delete(instance->rt, old);
if (vc && !route_insert(instance->rt, lq)) {
DEBUG_ERROR(DEBUG_CATEGORY_ROUTING, "local node update: route conflict/allocation group=%016llx",
(unsigned long long)group->group_id); goto fail;
}
}
if (topo_node_sign_self(instance, &candidate) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "local node update: cannot publish group=%016llx", (unsigned long long)group->group_id); goto fail;
}
char* old_name = ni->node_name;
*ni = candidate; name = NULL; u_free(old_name);
if (new_identity) {
struct TOPO_NODE* stored = topo_node_registry_store(groups, ni);
if (!stored) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "local node update: cannot register identity"); goto fail; }
ni = stored; new_identity = 0; retained = 1;
}
lq->last_ver = ni->ver; lq->last_timestamp = ni->timestamp;
if (old) { topo_nodeq_clear_paths(old); topo_nodeq_free_group_fields(groups, old); u_free(old); }
group->local_node = lq;
DEBUG_INFO(DEBUG_CATEGORY_BGP, "local node published: group=%016llx v4=%d v6=%d ver=%u timestamp=%llu",
(unsigned long long)group->group_id, vc, vc6, ni->ver, (unsigned long long)ni->timestamp);
return vc;
allocation_failed:
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "local node update: preparation allocation failed group=%016llx",
(unsigned long long)group->group_id);
fail:
if (routes_changed) {
route_delete(instance->rt, lq);
if (old && old->subnets && old->subnets->v4_subnets && !route_insert(instance->rt, old))
DEBUG_ERROR(DEBUG_CATEGORY_ROUTING, "local node update: previous routes restoration failed group=%016llx",
(unsigned long long)group->group_id);
}
if (new_identity) topo_node_destroy(groups, ni);
if (retained) topo_node_registry_unref(groups, instance->node_id);
u_free(name);
unchanged:
if (lq->subnets) {
free_v4_sub_list(groups->v4_subnet_pool, lq->subnets->v4_subnets);
free_v6_sub_list(groups->v6_subnet_pool, lq->subnets->v6_subnets);
u_free(lq->subnets);
}
u_free(lq);
return changed ? -1 : vc;
}
/* Пересобирает списки адресов своего узла из ETCP-сокетов; при изменении — подпись, БД, рассылка. */
+39 -16
View File
@@ -130,6 +130,9 @@ struct TOPOMSG_REALITY_SOCK {
uint8_t server_name[TOPO_REALITY_SNAME_MAX];
} __attribute__((packed));
/* NODEINFO: этот заголовок, имя без NUL, SOCKMETA4/ADDR4/SOCKMETA6/ADDR6/REALITY,
* опциональные SUBNET4/SUBNET6, затем hop_count ID. Счётчики — число элементов, имя до 63 байт.
* Многобайтные числа wire копируются в нативном порядке; IP — сетевые байты, port в памяти — host order. */
struct TOPOMSG_NODE {
uint8_t flags; uint64_t group_id, node_id; uint8_t ver;
uint64_t timestamp; // версия всей подписанной записи, Unix microseconds
@@ -206,13 +209,13 @@ struct TOPO_NODEPATH {
uint64_t exchange; // local_epoch сессии, в которой путь подтверждён NODEINFO
};
/** Узел в контексте группы (per-group данные). Хранится в TOPO_GROUP->nodes (ll_queue). */
/** Узел группы: удалённый хранится в TOPO_GROUP->nodes (ll_queue), локальный — отдельно в local_node. */
struct TOPO_GROUP_NODE {
struct ll_entry ll; // queue entry, хеш-ключ 8 байт → node_id
uint64_t node_id; // идентификатор узла в этой группе
struct TOPO_NODESUBNETS* subnets; // подсети узла (v4/v6), для route_insert()
struct ll_queue* paths; // TOPO_NODEPATH{conn,hop_count,rtt} — маршруты
uint8_t last_ver; // последняя версия NODEINFO (защита от stale, BGP)
uint8_t last_ver; // диагностический циклический ver; свежесть определяется timestamp
uint64_t last_timestamp; // последняя принятая версия записи в группе
uint8_t conn_mgr_type; // CONN_TYPE_DIRECT/REVERSE/INDIRECT/NONE
uint64_t conn_mgr_intermediaries[CONN_MGR_MAX_INTERMEDIARIES]; // посредники для INDIRECT
@@ -228,8 +231,10 @@ struct TOPO_GROUP_NODE {
// API — глобальный реестр TOPO_NODE
/* Заимствованный указатель; ref нужен, если запись должна пережить удаление из группы. */
struct TOPO_NODE* topo_node_registry_find(struct TOPO_GROUPS* groups, uint64_t node_id);
/* Принять ni и вернуть запись с одной новой ссылкой. Может вернуть существующий объект.
* При NULL входной ni остаётся у вызывающего. Подписанные записи сравниваются по timestamp. */
/* Принять ni и вернуть запись с одной новой ссылкой. При NULL входной ni остаётся у вызывающего.
* Более свежий подписанный снимок заменяет содержимое целиком, сохраняя адрес существующего объекта.
* Старый/равный вход поглощается, возвращается прежняя запись; при двух timestamp=0 сливаются непустые bootstrap-списки.
* Новая запись может быть заменена более свежим снимком SQLite перед регистрацией. */
struct TOPO_NODE* topo_node_registry_store(struct TOPO_GROUPS* groups, struct TOPO_NODE* ni);
/* Добавить/снять ссылку; последняя unref удаляет запись из реестра и освобождает её. */
void topo_node_registry_ref(struct TOPO_GROUPS* groups, uint64_t node_id);
@@ -244,17 +249,23 @@ struct TOPO_GROUP_NODE* topo_node_find_by_id(struct TOPO_GROUP* group, uint64_t
void topo_nodeq_free_group_fields(struct TOPO_GROUPS* groups, struct TOPO_GROUP_NODE* nq);
/* Освободить принадлежащие узлу записи путей и их контейнер; ETCP-соединения заимствованы. */
void topo_nodeq_clear_paths(struct TOPO_GROUP_NODE* nq);
/* Полное удаление узла из группы: отменяет влётные пробы, освобождает paths/subnets,
* убирает из group->nodes и освобождает сам nq. Единственный корректный способ удаления узла. */
/* Полное удаление узла из group->nodes: отменяет пробы, освобождает paths/subnets,
* убирает из очереди и освобождает nq. local_node освобождает владелец группы отдельно. */
void topo_nodeq_remove_node(struct TOPO_GROUP* group, struct TOPO_GROUP_NODE* nq);
/* Размер переменной части NODEINFO по счётчикам заголовка. */
/* Размер переменной части NODEINFO или -1 при NULL/счётчиках подсетей без TOPO_FLAG_SEND_SUBNETS. */
int topo_node_dyn_size(const struct TOPOMSG_NODE* msg);
/* Собрать NODEINFO в out; длина или -1. Входные структуры остаются у вызывающего. */
/* Собрать NODEINFO в out[out_max]; длина или -1. Входные структуры остаются у вызывающего.
* Без TOPO_FLAG_SEND_SUBNETS обе секции подсетей отсутствуют, их счётчики равны 0. */
int topo_node_serialize(struct TOPO_NODE* ni, struct TOPO_GROUP_NODE* nq,
uint64_t group_id, uint8_t flags,
uint8_t* out, size_t out_max, uint16_t cumulative_rtt);
/* Разобрать NODEINFO; 0/-1. Выходные объекты выделены отдельно и передаются вызывающему. */
/* Разобрать ровно len байт NODEINFO; 0/-1. group->instance->topo_groups содержит пулы списков.
* out_ni обязателен, остальные выходы независимы и необязательны. Отсутствие выхода не меняет обход/проверки секций.
* Перед разбором все переданные выходы обнуляются, прежние объекты не освобождаются.
* При успехе node уничтожается topo_node_destroy, подсети возвращаются в пулы groups, hops освобождается u_free.
* Пустые секции дают NULL/0; при ошибке выходы остаются NULL/0, частичные объекты освобождены.
* Проверяет размер, профили и /0..32, /0..128; подпись и допустимость BGP-пути проверяет вызывающий. */
int topo_node_deserialize(struct TOPO_GROUP* group, const uint8_t* data, size_t len, struct TOPO_NODE** out_ni, struct TOPO_NODESUBNETS** out_subnets,
uint64_t** out_hop_list, uint8_t* out_hop_count,
uint16_t* out_cumulative_rtt);
@@ -265,15 +276,24 @@ uint64_t* topo_node_best_hop_list(struct TOPO_GROUP_NODE* nq, uint8_t* out_count
/* Лучший живой путь: минимум переходов, затем ID соседа. Указатель заимствованный. */
struct TOPO_NODEPATH* topo_node_best_path(const struct TOPO_GROUP_NODE* nq);
/* Обновить локальный узел группы и его анонс. 0/-1. */
/* Подготовить и опубликовать local_node с актуальными подсетями/состоянием; число IPv4-подсетей или -1.
* Учитывает адреса и префиксы, а не только счётчики; CHAT не анонсирует подсети и возвращает 0.
* При отказе подготовки/подписи или конфликте IPv4 сохраняет прежние local_node и маршруты.
* Вызывает обновление общей подписанной записи; рассылку NODEINFO выполняет вызывающий. */
int topo_group_update_my_nodeinfo(struct UTUN_INSTANCE* instance, struct TOPO_GROUP* group);
/* Создать подписанную локальную идентичность с отдельной ссылкой ядра. 0/-1. */
int topo_node_init_self(struct UTUN_INSTANCE* instance);
int topo_node_update_my_addresses(struct UTUN_INSTANCE* instance); /* 1=changed, 0=unchanged, -1=error */
/* Пересобрать общие адреса из рабочих UDP/TCP-сокетов; 1=изменились, 0=прежние, -1=ошибка.
* Новая запись подписывается/сохраняется перед заменой, затем рассылается во все группы.
* Ошибка SQL-проекции даёт -1 и после применённого изменения; при неизменных адресах проекция также обновляется. */
int topo_node_update_my_addresses(struct UTUN_INSTANCE* instance);
/* Обработчик изменений локального сокета: обновляет адреса узла. */
void topo_node_on_socket_changed(struct ETCP_SOCKET* sock, int event, void* arg);
/* Вывести диагностику узлов группы в лог / текстовый буфер. */
/* Вывести group->nodes в лог категории BGP на уровне INFO; отдельный local_node не добавляется. */
void topo_node_dump_all(struct TOPO_GROUP* group);
/* Записать дамп в buf: длина без NUL или -1 при ошибке форматирования. Ёмкость ограничена INT_MAX.
* При усечении завершает запись сразу, заменяет хвост доступной частью "\n[TRUNCATED]\n" и возвращает реальную длину.
* При buf != NULL и buf_size > 0 строка всегда завершена NUL. Некорректные аргументы дают 0 и пустую строку, если есть буфер. */
int topo_node_format_all(struct TOPO_GROUP* group, char* buf, size_t buf_size);
/* 1, если хотя бы в одной группе пора повторить ping этого узла; иначе 0. */
int topo_node_ping_request_cbk(struct TOPO_GROUPS* groups, uint64_t node_id);
@@ -286,15 +306,18 @@ uint16_t topo_get_chain_rtt(struct TOPO_GROUP_NODE* nq);
/* Лимиты буфера подписываемых данных и полного сетевого NODEINFO. */
#define TOPO_SIG_MSG_MAX_SIZE 8192
#define TOPO_NODE_WIRE_MAX_SIZE 16384
/* Канонические байты подписи: node_id, ver, timestamp, X25519-ключ, имя,
* тип/активность и полные списки сокетов/адресов/Reality. Длина или -1. */
/* Канонические байты: node_id(native), ver, timestamp(BE), X25519-ключ, имя до 63 байт + NUL,
* тип/активность, пять счётчиков, SOCKMETA4/ADDR4/SOCKMETA6/ADDR6/REALITY. Порты здесь LE, SNI с NUL.
* Групповые flags/id, подсети, hops и RTT не подписываются. Длина или -1, буфер при ошибке может быть заполнен частично. */
int topo_node_build_sig_msg(struct TOPO_NODE* ni, uint8_t* buf, size_t buf_size);
/* Увеличить timestamp, подписать локальный NODEINFO и сохранить снимок в SQLite. 0/-1. */
/* Увеличить timestamp (выше текущего/сохранённого), подписать локальный NODEINFO; при наличии SQLite сохранить снимок.
* 0/-1; при ошибке возвращает прежние timestamp/подпись, остальные изменённые вызывающим поля не откатывает. */
int topo_node_sign_self(struct UTUN_INSTANCE* instance, struct TOPO_NODE* ni);
/* Проверить timestamp, привязку node_id к ключу и подпись. 0/-1. */
/* Проверить 0 < timestamp <= INT64_MAX, node_id=derive(X25519) и подпись предоставленным Ed25519-ключом. 0/-1. */
int topo_node_verify(const struct TOPO_NODE* ni);
/* Заимствованные списки: ni обязателен; подсетевые аксессоры допускают r=NULL. */
static inline const struct TOPO_SOCKMETA4* topo_v4_sock_meta(const struct TOPO_NODE* ni) { return ni->v4_sock_meta; }
static inline const struct TOPO_ADDR4* topo_v4_addrs(const struct TOPO_NODE* ni) { return ni->v4_addrs; }
static inline const struct TOPO_SOCKMETA6* topo_v6_sock_meta(const struct TOPO_NODE* ni) { return ni->v6_sock_meta; }
+119 -145
View File
@@ -1,178 +1,152 @@
# topo_node
# topo_node — модель, снимок и локальное состояние узла
## 1. Назначение
## Два уровня записи
Модель данных узла топологии — структуры для представления узла в памяти, сериализации/
десериализации в бинарный wire-формат для BGP-обмена, глобальный реестр идентичностей
(refcounting) и диагностический дамп.
TOPO_NODE содержит общую идентичность: node_id, диагностический ver, timestamp, ключи,
Ed25519-подпись, имя, тип/активность, списки SOCKMETA4/ADDR4/SOCKMETA6/ADDR6/REALITY.
Групповых flags, group_id, подсетей и путей в нём нет.
Модуль определяет два слоя:
- **Память:** связанные списки (`TOPO_ADDR4`, `TOPO_SOCKMETA4`, `TOPO_SUBNET4` и др.) и две
структуры узла — `TOPO_NODE` (глобальная идентичность) и `TOPO_GROUP_NODE` (per-group данные).
- **Wire-формат:** packed-структуры (`TOPOMSG_NODE`, `TOPOMSG_ADDR4` и др.) для передачи по сети.
TOPO_GROUP_NODE содержит node_id, подсети, пути, last_ver/last_timestamp, состояние conn_mgr,
пробы связности, presence/up, radio/phone_status и заимствованный NCD handle.
Удалённые записи находятся в group->nodes; локальная запись хранится отдельно в group->local_node.
TOPO_NODEPATH содержит заимствованный ETCP_CONN, hop_count, cumulative_rtt, exchange
и массив uint64_t[hop_count] непосредственно после структуры.
Сам модуль не содержит сетевой логики BGP-обмена — этим занимается `topo_group`. Здесь только
данные и их упаковка/распаковка. Персистентность — в `topo_node_sqlite`.
Все операции выполняются в uasync-потоке ядра. BGP-сессией/обменом управляет topo_group,
хранением в общей SQLite — topo_node_sqlite. Этот модуль также готовит локальную запись,
обновляет адреса и инициирует рассылку после их изменения.
Два уровня представления узла:
- **TOPO_NODE** — глобальная идентичность (pubkeys, имя, адреса, self-signature). Хранится один
раз в глобальном реестре `TOPO_GROUPS->node_registry` (hash по node_id), разделяется между
группами через `group_ref_count`.
- **TOPO_GROUP_NODE** — узел в контексте группы: `node_id`, `paths`, `subnets`, `last_ver`,
`connectivity`, `conn_presence/conn_up`, `handle`. Хранится в `TOPO_GROUP->nodes`.
## Реестр и память
## 2. Как пользоваться
node_registry хранит отдельный ll_entry с node_id и указателем TOPO_NODE.
registry_find возвращает заимствованный указатель. Для удержания записи берётся registry_ref;
последний registry_unref удаляет registry entry, списки, имя и сам объект.
### Типовые сценарии
registry_store принимает входной объект при успехе и возвращает запись с одной новой ссылкой:
**Сборка информации о себе:**
```c
topo_group_update_my_nodeinfo(instance, group);
```
Строит `group->local_node` (`TOPO_GROUP_NODE`) из конфига: сокеты, адреса (interface/NAT/real),
подсети; при изменениях инкрементирует `ver`. Для chat-групп (`TOPO_GROUP_TYPE_CHAT`) подсети
не включаются.
- Более свежий подписанный timestamp заменяет всю запись, включая пустые списки, но адрес существующего объекта сохраняется.
- Старый/равный вход освобождается, возвращается прежняя запись. Конфликт одинаковых timestamp логируется.
- Для двух bootstrap-записей с timestamp=0 сливаются присутствующие списки; полноценная идентичность может обновить скалярные поля.
- При новой регистрации более свежий/равный сохранённый снимок SQLite может заменить вход.
- При NULL-результате вход остаётся у вызывающего.
**Сериализация для отправки по BGP:**
```c
struct TOPO_NODE* ni = topo_node_registry_find(groups, nq->node_id);
uint8_t buf[4096];
int len = topo_node_serialize(ni, nq, group_id, flags, buf, sizeof(buf), cumulative_rtt);
```
Запаковывает `TOPO_NODE` + per-group данные (`TOPO_GROUP_NODE`) в буфер: заголовок
`TOPOMSG_NODE`, затем имя узла, v4/v6 sock_meta, v4/v6 адреса, подсети (если
`TOPO_FLAG_SEND_SUBNETS`) и hop-лист (через `topo_node_best_hop_list`).
У зарегистрированной записи используется registry_unref. Незарегистрированный результат parser/SQL
уничтожается topo_node_destroy(groups, ni). Списки сокетов/адресов/подсетей принадлежат пулам groups,
имя, REALITY, контейнер подсетей и отдельно возвращённый hop-list — u_malloc/u_calloc.
Для освобождения pooled-списков нужен тот же контейнер groups.
**Десериализация при получении NODEINFO:**
```c
struct TOPO_NODE* ni = NULL;
struct TOPO_NODESUBNETS* subnets = NULL;
uint64_t* hop_list = NULL; uint8_t hop_count = 0;
uint16_t cumulative_rtt = 0;
topo_node_deserialize(group, data, data_len, &ni, &subnets, &hop_list, &hop_count, &cumulative_rtt);
```
Восстанавливает `TOPO_NODE` (через `u_calloc`), подсети и hop-лист. Освобождение:
- `topo_node_destroy(groups, ni)` — освободить `TOPO_NODE` (списки адресов + имя).
- `topo_nodeq_free_group_fields(groups, nq)` — освободить per-group поля (`subnets`, `paths`-элементы).
topo_nodeq_free_group_fields снимает ссылку реестра, освобождает подсети и обнуляет handle;
сам nq и пути остаются. topo_nodeq_clear_paths освобождает пути/контейнер, транспорт не закрывает.
Удалённый узел удаляется через topo_nodeq_remove_node: отмена проб, удаление IPv4-маршрутов,
удаление из очереди и освобождение полей/записи. local_node освобождает владелец группы отдельно.
**Поиск узла по ID:**
```c
struct TOPO_GROUP_NODE* nq = topo_node_find_by_id(group, node_id); // per-group
struct TOPO_NODE* ni = topo_node_registry_find(groups, node_id); // глобальная идентичность
```
## Timestamp и подпись
**Дамп всех узлов:**
```c
topo_node_dump_all(group); // в лог
topo_node_format_all(group, buf, size); // в строку
```
timestamp — Unix microseconds, версия всей подписанной записи; 0 обозначает bootstrap.
ver — циклический счётчик 1..255, не фильтр свежести. Групповой last_timestamp хранит последнюю
принятую версию в этой группе, last_ver используется для диагностики.
### Ключевые концепции
topo_node_build_sig_msg создаёт канонические байты в таком порядке:
- **Глобальный реестр**: `TOPO_NODE` хранится один раз в `node_registry`; `topo_node_registry_store`
дедуплицирует по `node_id` (возвращает существующий при совпадении). `registry_ref/unref`
управляют `group_ref_count`; при 0 — `topo_node_destroy`.
- **Путь**: `TOPO_NODEPATH = {conn, hop_count, cumulative_rtt}` + inline `hop_list[hop_count]`
сразу после структуры. Полный hop-лист для отправки — `topo_node_best_hop_list`.
- **Self-signature**: `x25519_self_sig` — Ed25519-подпись над каноническим сообщением
(`topo_node_build_sig_msg`: pubkey || имя || client_type || client_activity || адреса).
Ставится `topo_node_sign_self`, проверяется в `topo_group_process_nodeinfo`.
- **Адреса трёх типов**: `TOPO_ADDR_INTERFACE` (LAN), `TOPO_ADDR_NAT` (после детекции),
`TOPO_ADDR_REAL` (подтверждённый прямой). Для каждого сокета — до 3 адресов.
- **TOPO_CONNECTIVITY** — локальное состояние связности (не передаётся по BGP): probe_status
(NONE/IN_PROGRESS/DONE/DEFERRED), RTT и статусы interface/nat/real, `probe_deferred_wait`.
- **TOPO_FLAG_SEND_SUBNETS** — если установлен, подсети сериализуются; для chat-групп сброшен.
- **Wire-протокол**: заголовок `TOPOMSG_NODE` (packed) + динамика: `node_name`, `SOCKMETA4*N`,
`ADDR4*N`, `SOCKMETA6*N`, `ADDR6*N`, опционально подсети, hop-лист. Размер динамики — `topo_node_dyn_size()`.
| Поле/секция | Представление подписи |
| --- | --- |
| node_id, ver | uint64 в нативном порядке, затем байт ver |
| timestamp | 8 байт BE |
| public_key | X25519, 32 байта |
| node_name | До 63 байт и NUL |
| client_type, client_activity | По одному байту |
| Счётчики | По байту: v4 sockets, v4 addrs, v6 sockets, v6 addrs, REALITY |
| SOCKMETA4, ADDR4, SOCKMETA6, ADDR6 | Полные списки в текущем порядке; адреса сетевыми байтами, порты LE |
| REALITY | socket_id, short_id[8], server_pubkey[32], version[3], SNI с NUL |
## 3. API
SOCKMETA содержит id/config_type/mapping/inbound/source, каждый по байту.
Групповые flags/id, подсети, hops и cumulative_rtt не входят в подпись.
Ed25519-ключ передаётся в записи и используется при verify; он не добавляется отдельным полем канонического сообщения.
### Основные структуры
topo_node_sign_self назначает timestamp строго выше текущего и сохранённого в SQLite,
подписывает ключом instance->my_ed25519_privkey и сохраняет снимок, если БД есть.
При ошибке восстанавливаются timestamp/подпись; другие изменённые вызывающим поля не откатываются.
Поэтому локальные обновления подписывают временный candidate до замены общей записи.
| Структура | Назначение |
|-----------|-----------|
| `struct TOPO_NODE` | Глобальная идентичность узла: `flags`, `group_id`, `node_id`, `ver`, `public_key`, `ed25519_public_key`, `x25519_self_sig`, `client_type`, `client_activity`, `node_name`, списки v4/v6 sock_meta и addrs, `group_ref_count` |
| `struct TOPO_GROUP_NODE` | Per-group запись: `node_id`, `subnets`, `paths` (queue TOPO_NODEPATH), `last_ver`, `conn_mgr_*`, `connectivity`, `conn_presence`, `conn_up`, `handle` (ncd) |
| `struct TOPO_NODEPATH` | Путь через ETCP_CONN: `{conn, hop_count, cumulative_rtt}` + inline `hop_list[]` |
| `struct TOPO_NODESUBNETS` | v4/v6 подсети узла (отдельный malloc, может быть NULL) |
| `struct TOPO_CONNECTIVITY` | Связность: probe_status, RTT, статусы interface/nat/real, probe-тайминги |
| `struct TOPOMSG_NODE` | Packed-заголовок wire-формата: счётчики, `hop_count`, `client_type/activity`, `cumulative_rtt` |
| `struct TOPOMSG_ADDR4/6` | Packed wire одного адреса (addr, port, type, socket_id, protocol) |
| `struct TOPOMSG_SOCKMETA4/6` | Packed wire сокет-метаданных (id, config_type, nat_type) |
| `struct TOPOMSG_SUBNET4/6` | Packed wire подсети (addr, prefix_length) |
topo_node_verify проверяет диапазон timestamp, node_id=derive(X25519 public_key)
и Ed25519-подпись предоставленным ключом; возвращает 0/-1.
Проверку права участия в группе и допустимости пути выполняет вызывающий BGP-слой.
### Вспомогательные типы в памяти (связанные списки)
## NODEINFO wire
| Тип | Назначение |
|-----|-----------|
| `struct TOPO_SOCKMETA4/6` | Элемент списка сокет-метаданных (next*, id, config_type, nat_type) |
| `struct TOPO_ADDR4/6` | Элемент списка адресов (next*, addr, port, type, socket_id, protocol) |
| `struct TOPO_SUBNET4/6` | Элемент списка подсетей (next*, addr, prefix_length) |
Сериализация ручная, без lib/serialize. Packed-заголовок TOPOMSG_NODE содержит поля записи,
group_id/flags, счётчики, hop_count и cumulative_rtt. Далее идут:
Элементы списков аллоцируются из memory pool'ов (`topo_groups`) через `memory_pool_alloc/free`.
| Порядок | Секция |
| --- | --- |
| 1 | node_name_len байт имени без NUL |
| 2 | SOCKMETA4, затем ADDR4 |
| 3 | SOCKMETA6, затем ADDR6 |
| 4 | REALITY с фиксированным полем server_name[256] |
| 5 | SUBNET4, затем SUBNET6, только с TOPO_FLAG_SEND_SUBNETS |
| 6 | hop_count идентификаторов uint64_t |
### Константы
Числа wire копируются в нативном порядке; IP — сетевые байты. Это представление отличается
от канонического сообщения подписи (timestamp BE, порты LE, строки с NUL).
Счётчики ограничены 255, имя — 63 байтами. dyn_size возвращает размер динамики или -1.
| Константа | Значение | Назначение |
|-----------|---------|-----------|
| `TOPO_ADDR_INTERFACE` / `TOPO_ADDR_NAT` / `TOPO_ADDR_REAL` | 0/1/2 | Тип адреса |
| `TOPO_PROTO_UDP` / `TOPO_PROTO_TCP` | 0x01/0x02 | Транспорт |
| `ADDR_TYPE_NETIF/DIRECT/NAT_EIM/NAT_STRICT` | 0/1/2/3 | Классификация адреса |
| `PROBE_STATUS_NONE/IN_PROGRESS/DONE/DEFERRED` | 0/1/2/3 | Состояние пробы |
| `PROBE_RESULT_UNKNOWN/REACHABLE/UNREACHABLE` | 0/1/2 | Результат пробы |
| `CONN_TYPE_NONE/DIRECT/REVERSE/INDIRECT` | 0/1/2/3 | Стратегия соединения |
| `CLIENT_TYPE_SERVER/DESKTOP/MOBILE` | 0/1/2 | Тип клиента |
| `CLIENT_ACTIVITY_STANDBY/ACTIVE` | 0/1 | Режим |
| `NCONN_DIRECT/INDIRECT/BGP` | 1/2/4 | Биты наличия соединений |
| `TOPO_FLAG_SEND_SUBNETS` | 0x01 | Отправлять подсети |
| `TOPO_GROUP_UTUN` | `0x8000000000000000ULL` | ID utun-группы по умолчанию |
| `TOPO_GROUP_TYPE_UTUN/CHAT` | 1/2 | Тип группы |
| `CONN_MGR_MAX_INTERMEDIARIES` | 3 | Максимум посредников |
serialize получает ni и nq, оставляет владение у вызывающего и возвращает длину либо -1.
Входные поля должны быть пригодны для публикации; serialize не проверяет подпись.
Hop-list берётся один раз из выбранного живого пути. Radio и phone_status дополняют flags.
Без TOPO_FLAG_SEND_SUBNETS счётчики подсетей равны 0; секции не записываются.
### Функции
deserialize требует group->instance->topo_groups с пулами и обязательный out_ni.
Остальные out_* независимы и необязательны. Проверяются точная длина, флаг/счётчики,
профили сокетов и IPv4 /0..32, IPv6 /0..128. Все секции обходятся и проверяются независимо
от запрошенных выходов. Подпись и допустимость BGP-пути проверяются отдельно.
**Реестр и refcounting:**
- `topo_node_registry_find(groups, node_id)` — найти `TOPO_NODE` в глобальном реестре.
- `topo_node_registry_store(groups, ni)` — сохранить с дедупликацией (возвращает существующий при совпадении).
- `topo_node_registry_ref/unref(groups, node_id)` — инкремент/декремент `group_ref_count`.
- `topo_node_destroy(groups, ni)` — освободить `TOPO_NODE` (списки адресов + имя).
Перед разбором переданные выходы обнуляются, прежние объекты не освобождаются.
Успех — 0 и атомарная выдача результата; ошибка — -1 и NULL/0 без частичных объектов.
Пустые секции дают NULL/0. ni уничтожается topo_node_destroy, подсети передаются владельцу
TOPO_GROUP_NODE, hop-list освобождается u_free.
**Per-group:**
- `topo_node_find_by_id(group, node_id)` — найти `TOPO_GROUP_NODE` по node_id (hash).
- `topo_nodeq_free_group_fields(groups, nq)` — освободить per-group поля (subnets, paths) + unref глобального TOPO_NODE.
## Локальная публикация
**Сериализация:**
- `topo_node_dyn_size(msg)` — размер динамической части по счётчикам заголовка.
- `topo_node_serialize(ni, nq, group_id, flags, out, out_max, cumulative_rtt)` — сериализовать `TOPO_NODE` + per-group в буфер.
- `topo_node_deserialize(group, data, len, &out_ni, &out_subnets, &out_hop_list, &out_hop_count, &out_cumulative_rtt)` — восстановить из wire-формата.
topo_node_init_self создаёт подписанную идентичность с отдельной ссылкой ядра; 0/-1.
**Пути:**
- `topo_node_best_hop_list(nq, &out_count, &out_rtt)` — лучший hop-лист (по min hop_count).
topo_group_update_my_nodeinfo готовит local_node из имени, ключей, типа/активности,
radio/phone_status и конфигурационных подсетей. Сравниваются адреса и префиксы,
включая замену подсети при прежнем количестве. CHAT не анонсирует подсети.
**Подпись:**
- `topo_node_build_sig_msg(ni, buf, buf_size)` — каноническое сообщение для Ed25519-подписи.
- `topo_node_sign_self(instance, ni)` — подписать свой NODEINFO (`x25519_self_sig`).
Новая запись/подсети готовятся до удаления прежнего local_node. IPv4 проходит route_insert
с запретом пересечений; при конфликте или отказе подготовки/подписи прежние узел и маршруты сохраняются.
Общая подписанная идентичность заменяется после успешного sign_self.
Возврат — число IPv4-подсетей (CHAT: 0), при ошибке -1. Рассылку запускает вызывающий.
**Обновление о себе:**
- `topo_group_update_my_nodeinfo(instance, group)` — перестроить `local_node`.
- `topo_node_update_my_addresses(instance)` — обновить адреса.
- `topo_node_on_socket_changed(sock, event, arg)` — обработчик изменений сокетов.
topo_node_update_my_addresses отдельно собирает адреса рабочих UDP/TCP-сокетов.
PRIVATE исключаются, UDP probe_only исключаются; LOCAL остаются доступными для локальных подключений.
UDP публикует интерфейс и допустимый наблюдаемый NAT endpoint, IPv6 интерфейс — отдельным списком.
TCP может публиковать внешний IP с port=0, если входящий порт неизвестен.
REALITY-параметры берутся из глобальной конфигурации и связываются с socket_id.
**Диагностика:**
- `topo_node_dump_all(group)` / `topo_node_format_all(group, buf, size)` — дамп узлов.
Изменение подписывается/сохраняется до замены списков и рассылается во все группы.
Неизменный набор не переподписывается, но SQL-проекция обновляется повторно.
Возврат 1/0/-1; ошибка проекции может дать -1 уже после применённой подписанной записи.
on_socket_changed инициирует этот пересчёт при событии сокета.
**Inline-аксессоры (.h):**
- `topo_v4_sock_meta/addr`, `topo_v6_sock_meta/addr`, `topo_v4_subnets/topo_v6_subnets`, `topo_list_count`.
## Пути, RTT и диагностика
### Зависимости
best_path выбирает путь с conn->links_up, без close_requested, по минимуму hop_count,
при равенстве — меньшему peer_node_id. READY проверяется отдельно владельцем групповой сессии.
best_hop_list возвращает заимствованный inline-массив; out_count обязателен,
out_rtt необязателен и получает RTT без локального линка. При отсутствии пути — NULL/0.
| Модуль | Использование |
|--------|--------------|
| `ll_queue.h` | `TOPO_GROUP_NODE` — запись в очереди (hash по node_id), `paths` — вложенная очередь |
| `secure_channel.h` | `SC_PUBKEY_SIZE` (32) |
| `mem.h` | `u_malloc`, `u_calloc`, `u_free` |
| `memory_pool.h` | Элементы списков из пулов `topo_groups` |
| `debug_config.h` | Логирование (BGP) |
| `topo_group.h` | `struct TOPO_GROUP`, `TOPO_GROUPS` |
| `route_lib.h` | `route_insert/route_delete` |
| `utun_instance.h` | `struct UTUN_INSTANCE` |
topo_get_chain_rtt складывает rtt_last локального соединения и cumulative_rtt.
UINT16_MAX обозначает неизвестный путь/локальный RTT либо насыщение суммы.
Единицы RTT и timebase-таймингов связности — 0.1 ms.
ping_request_cbk сообщает, пора ли повторить ping хотя бы в одной группе (TTL 24 часа).
ping_update_rtt обновляет время ping и минимальные RTT во всех группах, SQL и callbacks;
сам по себе не переводит статусы достижимости в REACHABLE.
dump_all пишет group->nodes в BGP/INFO. format_all форматирует ту же очередь в буфер:
реальная длина без NUL, -1 при ошибке форматирования, некорректные аргументы дают 0.
При наличии буфера строка завершена NUL; после первого усечения запись прекращается,
хвост заменяется доступной частью маркера TRUNCATED. Ёмкость ограничена INT_MAX.
Полные объявления и контракты — в topo_node.h; общий жизненный цикл снимка — doc/node_snapshot.md.
+380 -320
View File
@@ -206,24 +206,35 @@ int topo_node_sqlite_member_exists(sqlite3* db, uint64_t node_id) {
int topo_node_sqlite_get_member_channels(sqlite3* db, uint64_t node_id,
uint64_t** out_ch_ids, int* out_count) {
if (!db || !out_ch_ids || !out_count) return -1;
*out_ch_ids = NULL; *out_count = 0;
if (out_ch_ids) *out_ch_ids = NULL;
if (out_count) *out_count = 0;
if (!db || !out_ch_ids || !out_count) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "member channels: invalid arguments"); return -1; }
sqlite3_stmt* s = NULL;
if (sqlite3_prepare_v2(db,
"SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'peers\\_%' ESCAPE '\\'",
-1, &s, NULL) != SQLITE_OK) return -1;
-1, &s, NULL) != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "member channels: enumerate tables: %s", sqlite3_errmsg(db)); return -1;
}
uint64_t* ids = NULL; int count = 0, cap = 0;
char sql[256];
while (sqlite3_step(s) == SQLITE_ROW) {
int rc;
while ((rc = sqlite3_step(s)) == SQLITE_ROW) {
const char* tbl = (const char*)sqlite3_column_text(s, 0);
if (!tbl || strncmp(tbl, "peers_", 6) != 0) continue;
snprintf(sql, sizeof(sql), "SELECT 1 FROM \"%s\" WHERE node_id=?", tbl);
sqlite3_stmt* m = NULL;
if (sqlite3_prepare_v2(db, sql, -1, &m, NULL) == SQLITE_OK) {
sqlite3_bind_int64(m, 1, (sqlite3_int64)node_id);
int is_member = (sqlite3_step(m) == SQLITE_ROW);
int member_rc = sqlite3_prepare_v2(db, sql, -1, &m, NULL);
if (member_rc == SQLITE_OK) member_rc = sqlite3_bind_int64(m, 1, (sqlite3_int64)node_id);
if (member_rc == SQLITE_OK) member_rc = sqlite3_step(m);
if (member_rc != SQLITE_ROW && member_rc != SQLITE_DONE) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "member channels: table=%s node=%016llx rc=%d: %s",
tbl, (unsigned long long)node_id, member_rc, sqlite3_errmsg(db));
sqlite3_finalize(m); u_free(ids); sqlite3_finalize(s); return -1;
}
{
int is_member = member_rc == SQLITE_ROW;
sqlite3_finalize(m);
if (is_member) {
uint64_t ch = strtoull(tbl + 6, NULL, 10);
@@ -231,7 +242,10 @@ int topo_node_sqlite_get_member_channels(sqlite3* db, uint64_t node_id,
if (count == cap) {
cap = cap ? cap * 2 : 8;
uint64_t* tmp = u_realloc(ids, (size_t)cap * sizeof(uint64_t));
if (!tmp) { u_free(ids); sqlite3_finalize(s); return -1; }
if (!tmp) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "member channels: allocation failed count=%d", count);
u_free(ids); sqlite3_finalize(s); return -1;
}
ids = tmp;
}
ids[count++] = ch;
@@ -239,6 +253,11 @@ int topo_node_sqlite_get_member_channels(sqlite3* db, uint64_t node_id,
}
}
}
if (rc != SQLITE_DONE) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "member channels: enumeration failed node=%016llx rc=%d: %s",
(unsigned long long)node_id, rc, sqlite3_errmsg(db));
u_free(ids); sqlite3_finalize(s); return -1;
}
sqlite3_finalize(s);
*out_ch_ids = ids; *out_count = count;
return 0;
@@ -586,44 +605,47 @@ int topo_node_sqlite_channel_put(sqlite3* db, const char* channel_id,
return rc == SQLITE_DONE ? 0 : -1;
}
/* Удаляет канал и производные таблицы целиком; savepoint сохраняет внешнюю транзакцию. */
int topo_node_sqlite_channel_delete(sqlite3* db, const char* channel_id) {
if (!db || !channel_id || !channel_id[0]) return -1;
char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl));
char msg_tbl[80]; sanitize_ch_id(channel_id, msg_tbl, sizeof(msg_tbl));
/* msg_<san> */
char msg_full[80]; snprintf(msg_full, sizeof(msg_full), "msg_%s", msg_tbl);
/* 1. drop per-channel tables (индексы дропятся вместе с таблицей) */
char sql[128];
snprintf(sql, sizeof(sql), "DROP TABLE IF EXISTS \"%s\"", peers_tbl);
sqlite3_exec(db, sql, NULL, NULL, NULL);
snprintf(sql, sizeof(sql), "DROP TABLE IF EXISTS \"%s\"", msg_full);
sqlite3_exec(db, sql, NULL, NULL, NULL);
snprintf(sql, sizeof(sql), "DROP TABLE IF EXISTS \"feedback_%s\"", msg_tbl);
if (sqlite3_exec(db, sql, NULL, NULL, NULL) != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "feedback: delete channel=%s: %s", channel_id, sqlite3_errmsg(db)); return -1;
if (!db || !channel_id || !channel_id[0]) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "channel delete: invalid arguments"); return -1;
}
int rc = sqlite3_exec(db, "SAVEPOINT delete_topo_channel", NULL, NULL, NULL);
if (rc != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "channel delete savepoint: channel=%s rc=%d: %s", channel_id, rc, sqlite3_errmsg(db)); return -1;
}
/* 2. delete channels row (вместе с канальными privkey) */
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(db, "DELETE FROM channels WHERE channel_id=?", -1, &st, NULL) == SQLITE_OK) {
sqlite3_bind_text(st, 1, channel_id, -1, SQLITE_STATIC);
sqlite3_step(st);
sqlite3_finalize(st);
char san[64], sql[512]; sanitize_ch_id(channel_id, san, sizeof(san));
snprintf(sql, sizeof(sql), "DROP TABLE IF EXISTS \"peers_%s\"; DROP TABLE IF EXISTS \"msg_%s\";"
" DROP TABLE IF EXISTS \"feedback_%s\"", san, san, san);
if ((rc = sqlite3_exec(db, sql, NULL, NULL, NULL)) != SQLITE_OK) goto fail;
rc = sqlite3_prepare_v2(db, "DELETE FROM channels WHERE channel_id=?", -1, &st, NULL);
if (rc == SQLITE_OK) rc = sqlite3_bind_text(st, 1, channel_id, -1, SQLITE_STATIC);
if (rc == SQLITE_OK) rc = sqlite3_step(st);
if (rc != SQLITE_DONE) goto fail;
sqlite3_finalize(st); st = NULL;
rc = sqlite3_prepare_v2(db, "SELECT 1 FROM sqlite_master WHERE type='table' AND name='merkle_tree_hash'", -1, &st, NULL);
if (rc != SQLITE_OK) goto fail;
rc = sqlite3_step(st);
if (rc != SQLITE_ROW && rc != SQLITE_DONE) goto fail;
int has_merkle = rc == SQLITE_ROW;
sqlite3_finalize(st); st = NULL;
if (has_merkle) {
rc = sqlite3_prepare_v2(db, "DELETE FROM merkle_tree_hash WHERE substr(namespace,instr(namespace,':')+1)=?", -1, &st, NULL);
if (rc == SQLITE_OK) rc = sqlite3_bind_text(st, 1, channel_id, -1, SQLITE_STATIC);
if (rc == SQLITE_OK) rc = sqlite3_step(st);
if (rc != SQLITE_DONE) goto fail;
sqlite3_finalize(st); st = NULL;
}
/* 3. delete merkle tree hashes for this namespace */
sqlite3_stmt* mh = NULL;
if (sqlite3_prepare_v2(db, "DELETE FROM merkle_tree_hash WHERE substr(namespace,instr(namespace,':')+1)=?", -1, &mh, NULL) == SQLITE_OK) {
sqlite3_bind_text(mh, 1, channel_id, -1, SQLITE_STATIC);
sqlite3_step(mh);
sqlite3_finalize(mh);
}
DEBUG_INFO(DEBUG_CATEGORY_BGP, "topo_node_sqlite_channel_delete: ch=%s peers=%s msg=%s",
channel_id, peers_tbl, msg_full);
if ((rc = sqlite3_exec(db, "RELEASE delete_topo_channel", NULL, NULL, NULL)) != SQLITE_OK) goto fail;
DEBUG_INFO(DEBUG_CATEGORY_BGP, "channel deleted: channel=%s tables=peers/msg/feedback merkle=%d", channel_id, has_merkle);
return 0;
fail:
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "channel delete failed: channel=%s rc=%d: %s", channel_id, rc, sqlite3_errmsg(db));
sqlite3_finalize(st);
if (sqlite3_exec(db, "ROLLBACK TO delete_topo_channel; RELEASE delete_topo_channel", NULL, NULL, NULL) != SQLITE_OK)
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "channel delete rollback failed: channel=%s: %s", channel_id, sqlite3_errmsg(db));
return -1;
}
/* Упрощённый плейсхолдер мембера от topo_group: только node_id + pubkeys, source=1.
@@ -724,11 +746,16 @@ int topo_node_sqlite_member_owner_put(sqlite3* db, const char* channel_id, uint6
return 0;
}
/* Записывает уже проверенную вызывающим идентичность; любой отказ SQL возвращается вызывающему. */
int topo_node_sqlite_node_update_verified(sqlite3* db, uint64_t node_id,
const char* name, const uint8_t* x25519, const uint8_t* ed25519, uint64_t update_ts, time_t created_at) {
if (!db || !x25519 || !ed25519) return -1;
if (!db || !x25519 || !ed25519) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "verified node write: invalid arguments node=%016llx", (unsigned long long)node_id);
return -1;
}
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(db,
const char* stage = "prepare";
int rc = sqlite3_prepare_v2(db,
"INSERT INTO nodes(node_id, name, x25519_pubkey, ed25519_pubkey, created_at, update_ts, online)"
" VALUES(?,?,?,?,?,?,1)"
" ON CONFLICT(node_id) DO UPDATE SET"
@@ -739,15 +766,25 @@ int topo_node_sqlite_node_update_verified(sqlite3* db, uint64_t node_id,
" created_at=COALESCE(nodes.created_at, excluded.created_at),"
" update_ts=CASE WHEN nodes.update_ts IS NULL OR excluded.update_ts > nodes.update_ts"
" THEN excluded.update_ts ELSE nodes.update_ts END",
-1, &stmt, NULL) != SQLITE_OK) return -1;
sqlite3_bind_int64(stmt, 1, (sqlite3_int64)node_id);
sqlite3_bind_text(stmt, 2, name && name[0] ? name : "", -1, SQLITE_STATIC);
sqlite3_bind_blob(stmt, 3, x25519, 32, SQLITE_STATIC);
sqlite3_bind_blob(stmt, 4, ed25519, 32, SQLITE_STATIC);
sqlite3_bind_int64(stmt, 5, (sqlite3_int64)created_at);
sqlite3_bind_int64(stmt, 6, (sqlite3_int64)update_ts);
sqlite3_step(stmt);
sqlite3_finalize(stmt);
-1, &stmt, NULL);
if (rc == SQLITE_OK) {
stage = "bind";
rc = sqlite3_bind_int64(stmt, 1, (sqlite3_int64)node_id);
if (rc == SQLITE_OK) rc = sqlite3_bind_text(stmt, 2, name && name[0] ? name : "", -1, SQLITE_STATIC);
if (rc == SQLITE_OK) rc = sqlite3_bind_blob(stmt, 3, x25519, 32, SQLITE_STATIC);
if (rc == SQLITE_OK) rc = sqlite3_bind_blob(stmt, 4, ed25519, 32, SQLITE_STATIC);
if (rc == SQLITE_OK) rc = sqlite3_bind_int64(stmt, 5, (sqlite3_int64)created_at);
if (rc == SQLITE_OK) rc = sqlite3_bind_int64(stmt, 6, (sqlite3_int64)update_ts);
}
if (rc == SQLITE_OK) { stage = "step"; rc = sqlite3_step(stmt); }
if (rc != SQLITE_DONE) DEBUG_ERROR(DEBUG_CATEGORY_BGP, "verified node write: node=%016llx stage=%s rc=%d: %s",
(unsigned long long)node_id, stage, rc, sqlite3_errmsg(db));
int final_rc = sqlite3_finalize(stmt);
if (final_rc != SQLITE_OK && rc == SQLITE_DONE) DEBUG_ERROR(DEBUG_CATEGORY_BGP,
"verified node write: node=%016llx stage=finalize rc=%d: %s", (unsigned long long)node_id, final_rc, sqlite3_errmsg(db));
if (rc != SQLITE_DONE || final_rc != SQLITE_OK) return -1;
DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "verified node stored: node=%016llx timestamp=%llu changed=%d",
(unsigned long long)node_id, (unsigned long long)update_ts, sqlite3_changes(db));
return 0;
}
@@ -763,7 +800,10 @@ int topo_node_sqlite_member_get_join(sqlite3* db, const char* channel_id, uint64
int rc = -1;
if (sqlite3_step(stmt) == SQLITE_ROW) {
const uint8_t* sig = (const uint8_t*)sqlite3_column_blob(stmt, 0);
if (sig) { memcpy(join_sig_out, sig, PEERS_JOIN_SIG_SIZE); *join_ts_out = (uint64_t)sqlite3_column_int64(stmt, 1); rc = 0; }
if (sig && sqlite3_column_bytes(stmt, 0) == PEERS_JOIN_SIG_SIZE) {
memcpy(join_sig_out, sig, PEERS_JOIN_SIG_SIZE); *join_ts_out = (uint64_t)sqlite3_column_int64(stmt, 1); rc = 0;
} else DEBUG_WARN(DEBUG_CATEGORY_BGP, "invalid stored join signature channel=%s node=%016llx bytes=%d",
channel_id, (unsigned long long)node_id, sqlite3_column_bytes(stmt, 0));
}
sqlite3_finalize(stmt);
return rc;
@@ -802,15 +842,18 @@ int topo_node_sqlite_member_in_channel(sqlite3* db, const char* channel_id, uint
}
int topo_node_sqlite_member_del(sqlite3* db, const char* channel_id, uint64_t node_id) {
if (!db || !channel_id) return -1;
if (!db || !channel_id) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "member delete: invalid arguments"); return -1; }
char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl));
char sql[256];
snprintf(sql, sizeof(sql), "DELETE FROM \"%s\" WHERE node_id=?", peers_tbl);
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
sqlite3_bind_int64(stmt, 1, (sqlite3_int64)node_id);
sqlite3_step(stmt); sqlite3_finalize(stmt);
return 0;
int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) rc = sqlite3_bind_int64(stmt, 1, (sqlite3_int64)node_id);
if (rc == SQLITE_OK) rc = sqlite3_step(stmt);
if (rc != SQLITE_DONE) DEBUG_ERROR(DEBUG_CATEGORY_BGP, "member delete: channel=%s node=%016llx rc=%d: %s",
channel_id, (unsigned long long)node_id, rc, sqlite3_errmsg(db));
sqlite3_finalize(stmt);
return rc == SQLITE_DONE ? 0 : -1;
}
int topo_node_sqlite_channel_get(sqlite3* db, const char* channel_id,
@@ -823,6 +866,12 @@ int topo_node_sqlite_channel_get(sqlite3* db, const char* channel_id,
" FROM channels WHERE channel_id=?", -1, &stmt, NULL) != SQLITE_OK) return -1;
sqlite3_bind_text(stmt, 1, channel_id, -1, SQLITE_STATIC);
if (sqlite3_step(stmt) != SQLITE_ROW) { sqlite3_finalize(stmt); return -1; }
if ((x25519_pub && sqlite3_column_bytes(stmt, 2) != 32) ||
(ed25519_pub && sqlite3_column_bytes(stmt, 3) != 32) || (signature && sqlite3_column_bytes(stmt, 4) != 64)) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "invalid stored channel keys/signature channel=%s sizes=%d/%d/%d", channel_id,
sqlite3_column_bytes(stmt, 2), sqlite3_column_bytes(stmt, 3), sqlite3_column_bytes(stmt, 4));
sqlite3_finalize(stmt); return -1;
}
if (name_out && name_sz > 0) {
const unsigned char* txt = sqlite3_column_text(stmt, 0);
if (txt) snprintf(name_out, name_sz, "%s", txt); else name_out[0] = '\0';
@@ -884,14 +933,16 @@ int topo_node_sqlite_channel_set_privs(sqlite3* db, const char* channel_id,
}
int topo_node_sqlite_node_set_online(sqlite3* db, uint64_t node_id, int online) {
if (!db) return -1;
if (!db) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "node online: database is NULL"); return -1; }
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(db, "UPDATE nodes SET online=? WHERE node_id=?", -1, &stmt, NULL) != SQLITE_OK) return -1;
sqlite3_bind_int(stmt, 1, online ? 1 : 0);
sqlite3_bind_int64(stmt, 2, (sqlite3_int64)node_id);
sqlite3_step(stmt);
int rc = sqlite3_prepare_v2(db, "UPDATE nodes SET online=? WHERE node_id=?", -1, &stmt, NULL);
if (rc == SQLITE_OK) rc = sqlite3_bind_int(stmt, 1, online ? 1 : 0);
if (rc == SQLITE_OK) rc = sqlite3_bind_int64(stmt, 2, (sqlite3_int64)node_id);
if (rc == SQLITE_OK) rc = sqlite3_step(stmt);
if (rc != SQLITE_DONE) DEBUG_ERROR(DEBUG_CATEGORY_BGP, "node online: node=%016llx online=%d rc=%d: %s",
(unsigned long long)node_id, online, rc, sqlite3_errmsg(db));
sqlite3_finalize(stmt);
return 0;
return rc == SQLITE_DONE ? 0 : -1;
}
int topo_node_sqlite_node_get_online(sqlite3* db, uint64_t node_id) {
@@ -928,75 +979,80 @@ int topo_node_sqlite_get_ed25519_pubkey(sqlite3* db, uint64_t node_id, uint8_t p
return rc;
}
/* Пересчитывает канальные роли/хранение/удаление; при отказе откатывает всю проекцию узла. */
int topo_node_sqlite_nodeinfo_updated(sqlite3* db, uint64_t node_id) {
if (!db) return -1;
int best_at = -1;
{
sqlite3_stmt* s = NULL;
if (sqlite3_prepare_v2(db,
"SELECT addr_type FROM node_addresses WHERE node_id=?", -1, &s, NULL) == SQLITE_OK) {
sqlite3_bind_int64(s, 1, (sqlite3_int64)node_id);
while (sqlite3_step(s) == SQLITE_ROW) {
int at = sqlite3_column_int(s, 0);
if (at == ADDR_TYPE_DIRECT) { best_at = ADDR_TYPE_DIRECT; break; }
if (best_at < 0 || at < best_at) best_at = at;
}
sqlite3_finalize(s);
}
if (!db) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "node classification: database is NULL"); return -1; }
int rc = sqlite3_exec(db, "SAVEPOINT node_classification", NULL, NULL, NULL);
if (rc != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "node classification savepoint: rc=%d: %s", rc, sqlite3_errmsg(db)); return -1;
}
sqlite3_stmt *scan = NULL, *read = NULL, *write = NULL;
const char* stage = "addresses";
int best_at = -1;
rc = sqlite3_prepare_v2(db, "SELECT addr_type FROM node_addresses WHERE node_id=?", -1, &read, NULL);
if (rc == SQLITE_OK) rc = sqlite3_bind_int64(read, 1, (sqlite3_int64)node_id);
if (rc != SQLITE_OK) goto fail;
while ((rc = sqlite3_step(read)) == SQLITE_ROW) {
int at = sqlite3_column_int(read, 0);
if (at == ADDR_TYPE_DIRECT) { best_at = ADDR_TYPE_DIRECT; break; }
if (best_at < 0 || at < best_at) best_at = at;
}
if (rc != SQLITE_DONE && rc != SQLITE_ROW) goto fail;
sqlite3_finalize(read); read = NULL;
static const int at2type[] = { [ADDR_TYPE_NETIF]=1, [ADDR_TYPE_DIRECT]=1, [ADDR_TYPE_NAT_EIM]=2,
[ADDR_TYPE_NAT_STRICT]=1, [ADDR_TYPE_NAT]=2, [ADDR_TYPE_CONTACT]=2, [ADDR_TYPE_NAT_EIM_UNVERIFIED]=2 };
int base_type = best_at < 0 || (unsigned)best_at >= sizeof(at2type) / sizeof(at2type[0]) ? 0 : at2type[best_at];
if (best_at >= 0 && (unsigned)best_at >= sizeof(at2type) / sizeof(at2type[0]))
DEBUG_WARN(DEBUG_CATEGORY_BGP, "invalid projected address type=%d node=%016llx", best_at, (unsigned long long)node_id);
sqlite3_stmt* s = NULL;
if (sqlite3_prepare_v2(db,
"SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'peers\\_%' ESCAPE '\\'",
-1, &s, NULL) != SQLITE_OK) return -1;
char sql[256];
while (sqlite3_step(s) == SQLITE_ROW) {
const char* tbl = (const char*)sqlite3_column_text(s, 0);
int node_type = base_type; int is_storage = 0; int is_deleted = 0;
snprintf(sql, sizeof(sql),
"SELECT adm_tags FROM \"%s\" WHERE node_id=?", tbl);
sqlite3_stmt* a = NULL;
if (sqlite3_prepare_v2(db, sql, -1, &a, NULL) == SQLITE_OK) {
sqlite3_bind_int64(a, 1, (sqlite3_int64)node_id);
if (sqlite3_step(a) == SQLITE_ROW) {
const char* tags = (const char*)sqlite3_column_text(a, 0);
if (tags) {
char buf[64];
if (json_flat_get(tags, "supernode", buf, sizeof(buf)) == 0 && strcmp(buf, "yes") == 0) node_type = 4;
if (best_at == ADDR_TYPE_DIRECT && base_type == 1) {
if (json_flat_get(tags, "storage", buf, sizeof(buf)) == 0 && strcmp(buf, "yes") == 0) is_storage = 1;
}
is_deleted = adm_tags_deleted(tags);
}
}
sqlite3_finalize(a);
}
snprintf(sql, sizeof(sql),
"UPDATE \"%s\" SET node_type=?, storage=?, deleted=? WHERE node_id=?", tbl);
sqlite3_stmt* u = NULL;
if (sqlite3_prepare_v2(db, sql, -1, &u, NULL) == SQLITE_OK) {
sqlite3_bind_int(u, 1, node_type);
sqlite3_bind_int(u, 2, is_storage);
sqlite3_bind_int(u, 3, is_deleted);
sqlite3_bind_int64(u, 4, (sqlite3_int64)node_id);
sqlite3_step(u); sqlite3_finalize(u);
}
stage = "membership tables";
rc = sqlite3_prepare_v2(db, "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'peers\\_%' ESCAPE '\\'",
-1, &scan, NULL);
if (rc != SQLITE_OK) goto fail;
while ((rc = sqlite3_step(scan)) == SQLITE_ROW) {
const char* table = (const char*)sqlite3_column_text(scan, 0);
int node_type = base_type, storage = 0, deleted = 0;
char sql[256]; snprintf(sql, sizeof(sql), "SELECT adm_tags FROM \"%s\" WHERE node_id=?", table);
stage = "member tags";
rc = sqlite3_prepare_v2(db, sql, -1, &read, NULL);
if (rc == SQLITE_OK) rc = sqlite3_bind_int64(read, 1, (sqlite3_int64)node_id);
if (rc != SQLITE_OK) goto fail;
rc = sqlite3_step(read);
if (rc == SQLITE_ROW) {
const char* tags = (const char*)sqlite3_column_text(read, 0);
char value[64];
if (json_flat_get(tags, "supernode", value, sizeof(value)) == 0 && !strcmp(value, "yes")) node_type = 4;
if (best_at == ADDR_TYPE_DIRECT && json_flat_get(tags, "storage", value, sizeof(value)) == 0 && !strcmp(value, "yes"))
storage = 1;
deleted = adm_tags_deleted(tags);
} else if (rc != SQLITE_DONE) goto fail;
sqlite3_finalize(read); read = NULL;
snprintf(sql, sizeof(sql), "UPDATE \"%s\" SET node_type=?,storage=?,deleted=? WHERE node_id=?", table);
stage = "member projection";
rc = sqlite3_prepare_v2(db, sql, -1, &write, NULL);
if (rc == SQLITE_OK) rc = sqlite3_bind_int(write, 1, node_type);
if (rc == SQLITE_OK) rc = sqlite3_bind_int(write, 2, storage);
if (rc == SQLITE_OK) rc = sqlite3_bind_int(write, 3, deleted);
if (rc == SQLITE_OK) rc = sqlite3_bind_int64(write, 4, (sqlite3_int64)node_id);
if (rc == SQLITE_OK) rc = sqlite3_step(write);
if (rc != SQLITE_DONE) goto fail;
sqlite3_finalize(write); write = NULL;
stage = "membership tables";
}
sqlite3_finalize(s);
DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "nodeinfo_updated: node=%016llx best_at=%d base_type=%d",
if (rc != SQLITE_DONE) goto fail;
sqlite3_finalize(scan); scan = NULL;
stage = "release";
if ((rc = sqlite3_exec(db, "RELEASE node_classification", NULL, NULL, NULL)) != SQLITE_OK) goto fail;
DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "node classification stored: node=%016llx best_at=%d base_type=%d",
(unsigned long long)node_id, best_at, base_type);
return 0;
fail:
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "node classification failed: node=%016llx stage=%s rc=%d: %s",
(unsigned long long)node_id, stage, rc, sqlite3_errmsg(db));
sqlite3_finalize(write); sqlite3_finalize(read); sqlite3_finalize(scan);
if (sqlite3_exec(db, "ROLLBACK TO node_classification; RELEASE node_classification", NULL, NULL, NULL) != SQLITE_OK)
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "node classification rollback failed: node=%016llx: %s",
(unsigned long long)node_id, sqlite3_errmsg(db));
return -1;
}
void topo_node_sqlite_update_rtt(sqlite3* db, uint64_t node_id, uint16_t rtt) {
@@ -1111,157 +1167,157 @@ struct TOPO_NODE* topo_node_sqlite_snapshot_load(sqlite3* db, struct TOPO_GROUPS
}
struct TOPO_NODE* topo_node_sqlite_node_load(sqlite3* db, struct TOPO_GROUPS* groups, uint64_t node_id) {
if (!db || !groups) return NULL;
if (!db || !groups || !groups->instance || groups->instance->topo_groups != groups) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "node load: invalid database/groups node=%016llx", (unsigned long long)node_id); return NULL;
}
struct TOPO_NODE* snapshot = topo_node_sqlite_snapshot_load(db, groups, node_id);
if (snapshot) return snapshot;
struct TOPO_NODE* ni = u_calloc(1, sizeof(*ni));
if (!ni) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "node load: allocation failed node=%016llx", (unsigned long long)node_id); return NULL; }
sqlite3_stmt* s = NULL;
uint8_t pubkey[32] = {0}, ed_pubkey[32] = {0};
char* name = NULL;
uint8_t client_type = 0;
int found = 0;
{
sqlite3_stmt* s = NULL;
if (sqlite3_prepare_v2(db, "SELECT name,x25519_pubkey,ed25519_pubkey,client_type FROM nodes WHERE node_id=?",
-1, &s, NULL) == SQLITE_OK) {
sqlite3_bind_int64(s, 1, (sqlite3_int64)node_id);
if (sqlite3_step(s) == SQLITE_ROW) {
const char* n = (const char*)sqlite3_column_text(s, 0);
if (n) name = u_strdup(n);
const void* pk = sqlite3_column_blob(s, 1);
if (pk && sqlite3_column_bytes(s, 1) >= 32) memcpy(pubkey, pk, 32);
const void* ep = sqlite3_column_blob(s, 2);
if (ep && sqlite3_column_bytes(s, 2) >= 32) memcpy(ed_pubkey, ep, 32);
client_type = (uint8_t)sqlite3_column_int(s, 3);
found = 1;
}
sqlite3_finalize(s);
}
}
if (!found) { u_free(name); return NULL; }
struct TOPO_ADDR4* v4_head = NULL; struct TOPO_ADDR4* v4_tail = NULL;
struct TOPO_SOCKMETA4* m4_head = NULL; struct TOPO_SOCKMETA4* m4_tail = NULL;
struct TOPO_ADDR6* v6_head = NULL; struct TOPO_ADDR6* v6_tail = NULL;
struct TOPO_SOCKMETA6* m6_head = NULL; struct TOPO_SOCKMETA6* m6_tail = NULL;
struct TOPO_REALITY_SOCK* r_head = NULL; struct TOPO_REALITY_SOCK* r_tail = NULL;
{
sqlite3_stmt* s = NULL;
if (sqlite3_prepare_v2(db, "SELECT name,x25519_pubkey,ed25519_pubkey,client_type FROM nodes WHERE node_id=?",
-1, &s, NULL) != SQLITE_OK) goto sql_failed;
if (sqlite3_bind_int64(s, 1, (sqlite3_int64)node_id) != SQLITE_OK) goto sql_failed;
int rc = sqlite3_step(s);
if (rc == SQLITE_ROW) {
const char* n = (const char*)sqlite3_column_text(s, 0);
if (n) { name = u_strdup(n); if (!name) goto allocation_failed; }
const void* pk = sqlite3_column_blob(s, 1);
if (pk && sqlite3_column_bytes(s, 1) >= 32) memcpy(pubkey, pk, 32);
const void* ep = sqlite3_column_blob(s, 2);
if (ep && sqlite3_column_bytes(s, 2) >= 32) memcpy(ed_pubkey, ep, 32);
client_type = (uint8_t)sqlite3_column_int(s, 3);
found = 1;
} else if (rc != SQLITE_DONE) goto sql_failed;
sqlite3_finalize(s); s = NULL;
}
if (!found) { DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "node load: missing node=%016llx", (unsigned long long)node_id); goto failed; }
{
if (sqlite3_prepare_v2(db,
"SELECT address,port,addr_type,socket_id,protocol,config_type,nat_mapping,options,address_kind FROM node_addresses"
" WHERE node_id=? AND family=4 ORDER BY addr_type",
-1, &s, NULL) == SQLITE_OK) {
sqlite3_bind_int64(s, 1, (sqlite3_int64)node_id);
while (sqlite3_step(s) == SQLITE_ROW) {
const void* addr = sqlite3_column_blob(s, 0);
if (!addr || sqlite3_column_bytes(s, 0) < 4) continue;
int port = sqlite3_column_int(s, 1);
int sock_id_raw = sqlite3_column_int(s, 3);
uint8_t sock_id = (uint8_t)sock_id_raw;
uint8_t type = (uint8_t)sqlite3_column_int(s, 8);
struct TOPO_ADDR4* a4 = memory_pool_alloc(groups->v4_addr_pool);
if (!a4) continue;
memset(a4, 0, sizeof(*a4));
int proto = sqlite3_column_int(s, 4);
memcpy(a4->addr, addr, 4); a4->port = (uint16_t)port;
a4->type = type; a4->socket_id = sock_id; a4->protocol = (uint8_t)proto;
if (!v4_head) v4_head = v4_tail = a4;
else { v4_tail->next = a4; v4_tail = a4; }
-1, &s, NULL) != SQLITE_OK) goto sql_failed;
if (sqlite3_bind_int64(s, 1, (sqlite3_int64)node_id) != SQLITE_OK) goto sql_failed;
int rc;
while ((rc = sqlite3_step(s)) == SQLITE_ROW) {
const void* addr = sqlite3_column_blob(s, 0);
if (!addr || sqlite3_column_bytes(s, 0) < 4) continue;
int port = sqlite3_column_int(s, 1);
int sock_id_raw = sqlite3_column_int(s, 3);
uint8_t sock_id = (uint8_t)sock_id_raw;
uint8_t type = (uint8_t)sqlite3_column_int(s, 8);
struct TOPO_ADDR4* a4 = memory_pool_alloc(groups->v4_addr_pool);
if (!a4) goto allocation_failed;
memset(a4, 0, sizeof(*a4));
int proto = sqlite3_column_int(s, 4);
memcpy(a4->addr, addr, 4); a4->port = (uint16_t)port;
a4->type = type; a4->socket_id = sock_id; a4->protocol = (uint8_t)proto;
if (!v4_head) v4_head = v4_tail = a4;
else { v4_tail->next = a4; v4_tail = a4; }
struct TOPO_SOCKMETA4* ex = m4_head;
while (ex && ex->id != sock_id) ex = ex->next;
if (!ex) {
struct TOPO_SOCKMETA4* m4 = memory_pool_alloc(groups->v4_sock_meta_pool);
if (m4) {
m4->id = sock_id;
m4->config_type = (uint8_t)sqlite3_column_int(s, 5);
/* Без подписанного snapshot профиль из SQL-проекции не считается доказательством. */
memset(&m4->address_profile, 0, sizeof(m4->address_profile));
m4->next = NULL;
if (!m4_head) m4_head = m4_tail = m4;
else { m4_tail->next = m4; m4_tail = m4; }
}
}
if (sqlite3_column_type(s, 7) == SQLITE_BLOB
&& sqlite3_column_bytes(s, 7) == TOPO_ADDR_REALITY_OPTS_SIZE
&& (proto & TOPO_PROTO_REALITY)) {
const struct TOPO_ADDR_REALITY_OPTS* o = (const struct TOPO_ADDR_REALITY_OPTS*)sqlite3_column_blob(s, 7);
struct TOPO_REALITY_SOCK* r = u_calloc(1, sizeof(*r));
if (r) {
r->socket_id = sock_id;
memcpy(r->short_id, o->short_id, 8);
memcpy(r->server_pubkey, o->server_pubkey, 32);
memcpy(r->version, o->version, 3);
memcpy(r->server_name, o->server_name, TOPO_REALITY_SNAME_MAX);
r->server_name[TOPO_REALITY_SNAME_MAX - 1] = 0;
if (!r_head) r_head = r_tail = r;
else { r_tail->next = r; r_tail = r; }
}
}
struct TOPO_SOCKMETA4* ex = m4_head;
while (ex && ex->id != sock_id) ex = ex->next;
if (!ex) {
struct TOPO_SOCKMETA4* m4 = memory_pool_alloc(groups->v4_sock_meta_pool);
if (!m4) goto allocation_failed;
m4->id = sock_id;
m4->config_type = (uint8_t)sqlite3_column_int(s, 5);
/* Без подписанного snapshot профиль из SQL-проекции не считается доказательством. */
memset(&m4->address_profile, 0, sizeof(m4->address_profile));
m4->next = NULL;
if (!m4_head) m4_head = m4_tail = m4;
else { m4_tail->next = m4; m4_tail = m4; }
}
if (sqlite3_column_type(s, 7) == SQLITE_BLOB
&& sqlite3_column_bytes(s, 7) == TOPO_ADDR_REALITY_OPTS_SIZE
&& (proto & TOPO_PROTO_REALITY)) {
const struct TOPO_ADDR_REALITY_OPTS* o = (const struct TOPO_ADDR_REALITY_OPTS*)sqlite3_column_blob(s, 7);
struct TOPO_REALITY_SOCK* r = u_calloc(1, sizeof(*r));
if (!r) goto allocation_failed;
r->socket_id = sock_id;
memcpy(r->short_id, o->short_id, 8);
memcpy(r->server_pubkey, o->server_pubkey, 32);
memcpy(r->version, o->version, 3);
memcpy(r->server_name, o->server_name, TOPO_REALITY_SNAME_MAX);
r->server_name[TOPO_REALITY_SNAME_MAX - 1] = 0;
if (!r_head) r_head = r_tail = r;
else { r_tail->next = r; r_tail = r; }
}
sqlite3_finalize(s);
}
if (rc != SQLITE_DONE) goto sql_failed;
sqlite3_finalize(s); s = NULL;
}
/* IPv6 addresses */
struct TOPO_ADDR6* v6_head = NULL; struct TOPO_ADDR6* v6_tail = NULL;
struct TOPO_SOCKMETA6* m6_head = NULL; struct TOPO_SOCKMETA6* m6_tail = NULL;
{
sqlite3_stmt* s = NULL;
if (sqlite3_prepare_v2(db,
"SELECT address,port,addr_type,socket_id,protocol,config_type,nat_mapping,options,address_kind FROM node_addresses"
" WHERE node_id=? AND family=6 ORDER BY addr_type",
-1, &s, NULL) == SQLITE_OK) {
sqlite3_bind_int64(s, 1, (sqlite3_int64)node_id);
while (sqlite3_step(s) == SQLITE_ROW) {
const void* addr = sqlite3_column_blob(s, 0);
if (!addr || sqlite3_column_bytes(s, 0) < 16) continue;
int port = sqlite3_column_int(s, 1);
int sock_id_raw = sqlite3_column_int(s, 3);
uint8_t sock_id = (uint8_t)sock_id_raw;
uint8_t type = (uint8_t)sqlite3_column_int(s, 8);
struct TOPO_ADDR6* a6 = memory_pool_alloc(groups->v6_addr_pool);
if (!a6) continue;
memset(a6, 0, sizeof(*a6));
int proto6 = sqlite3_column_int(s, 4);
memcpy(a6->addr, addr, 16); a6->port = (uint16_t)port;
a6->type = type; a6->socket_id = sock_id; a6->protocol = (uint8_t)proto6;
if (!v6_head) v6_head = v6_tail = a6;
else { v6_tail->next = a6; v6_tail = a6; }
-1, &s, NULL) != SQLITE_OK) goto sql_failed;
if (sqlite3_bind_int64(s, 1, (sqlite3_int64)node_id) != SQLITE_OK) goto sql_failed;
int rc;
while ((rc = sqlite3_step(s)) == SQLITE_ROW) {
const void* addr = sqlite3_column_blob(s, 0);
if (!addr || sqlite3_column_bytes(s, 0) < 16) continue;
int port = sqlite3_column_int(s, 1);
int sock_id_raw = sqlite3_column_int(s, 3);
uint8_t sock_id = (uint8_t)sock_id_raw;
uint8_t type = (uint8_t)sqlite3_column_int(s, 8);
struct TOPO_ADDR6* a6 = memory_pool_alloc(groups->v6_addr_pool);
if (!a6) goto allocation_failed;
memset(a6, 0, sizeof(*a6));
int proto6 = sqlite3_column_int(s, 4);
memcpy(a6->addr, addr, 16); a6->port = (uint16_t)port;
a6->type = type; a6->socket_id = sock_id; a6->protocol = (uint8_t)proto6;
if (!v6_head) v6_head = v6_tail = a6;
else { v6_tail->next = a6; v6_tail = a6; }
struct TOPO_SOCKMETA6* ex = m6_head;
while (ex && ex->id != sock_id) ex = ex->next;
if (!ex) {
struct TOPO_SOCKMETA6* m6 = memory_pool_alloc(groups->v6_sock_meta_pool);
if (m6) {
m6->id = sock_id;
m6->config_type = (uint8_t)sqlite3_column_int(s, 5);
memset(&m6->address_profile, 0, sizeof(m6->address_profile));
m6->next = NULL;
if (!m6_head) m6_head = m6_tail = m6;
else { m6_tail->next = m6; m6_tail = m6; }
}
}
if (sqlite3_column_type(s, 7) == SQLITE_BLOB
&& sqlite3_column_bytes(s, 7) == TOPO_ADDR_REALITY_OPTS_SIZE
&& (proto6 & TOPO_PROTO_REALITY)) {
const struct TOPO_ADDR_REALITY_OPTS* o = (const struct TOPO_ADDR_REALITY_OPTS*)sqlite3_column_blob(s, 7);
struct TOPO_REALITY_SOCK* r = u_calloc(1, sizeof(*r));
if (r) {
r->socket_id = sock_id;
memcpy(r->short_id, o->short_id, 8);
memcpy(r->server_pubkey, o->server_pubkey, 32);
memcpy(r->version, o->version, 3);
memcpy(r->server_name, o->server_name, TOPO_REALITY_SNAME_MAX);
r->server_name[TOPO_REALITY_SNAME_MAX - 1] = 0;
if (!r_head) r_head = r_tail = r;
else { r_tail->next = r; r_tail = r; }
}
}
struct TOPO_SOCKMETA6* ex = m6_head;
while (ex && ex->id != sock_id) ex = ex->next;
if (!ex) {
struct TOPO_SOCKMETA6* m6 = memory_pool_alloc(groups->v6_sock_meta_pool);
if (!m6) goto allocation_failed;
m6->id = sock_id;
m6->config_type = (uint8_t)sqlite3_column_int(s, 5);
memset(&m6->address_profile, 0, sizeof(m6->address_profile));
m6->next = NULL;
if (!m6_head) m6_head = m6_tail = m6;
else { m6_tail->next = m6; m6_tail = m6; }
}
if (sqlite3_column_type(s, 7) == SQLITE_BLOB
&& sqlite3_column_bytes(s, 7) == TOPO_ADDR_REALITY_OPTS_SIZE
&& (proto6 & TOPO_PROTO_REALITY)) {
const struct TOPO_ADDR_REALITY_OPTS* o = (const struct TOPO_ADDR_REALITY_OPTS*)sqlite3_column_blob(s, 7);
struct TOPO_REALITY_SOCK* r = u_calloc(1, sizeof(*r));
if (!r) goto allocation_failed;
r->socket_id = sock_id;
memcpy(r->short_id, o->short_id, 8);
memcpy(r->server_pubkey, o->server_pubkey, 32);
memcpy(r->version, o->version, 3);
memcpy(r->server_name, o->server_name, TOPO_REALITY_SNAME_MAX);
r->server_name[TOPO_REALITY_SNAME_MAX - 1] = 0;
if (!r_head) r_head = r_tail = r;
else { r_tail->next = r; r_tail = r; }
}
sqlite3_finalize(s);
}
if (rc != SQLITE_DONE) goto sql_failed;
sqlite3_finalize(s); s = NULL;
}
if (!v4_head && !v6_head) {
DEBUG_WARN(DEBUG_CATEGORY_BGP, "node load: no bootstrap addresses node=%016llx", (unsigned long long)node_id); goto failed;
}
if (!v4_head && !v6_head) { u_free(name); DEBUG_WARN(DEBUG_CATEGORY_BGP, "node_load: node 0x%016llx has no addresses (may be stub/seed — will retry when addresses arrive)", (unsigned long long)node_id); return NULL; }
struct TOPO_NODE* ni = u_calloc(1, sizeof(struct TOPO_NODE));
if (!ni) { u_free(name); return NULL; }
ni->node_id = node_id; ni->ver = 1; ni->group_ref_count = 0;
ni->client_type = client_type;
memcpy(ni->public_key, pubkey, 32); memcpy(ni->ed25519_public_key, ed_pubkey, 32);
@@ -1277,6 +1333,18 @@ struct TOPO_NODE* topo_node_sqlite_node_load(sqlite3* db, struct TOPO_GROUPS* gr
topo_list_count((struct _topo_head*)v4_head),
topo_list_count((struct _topo_head*)v6_head));
return ni;
sql_failed:
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "node load SQL failed node=%016llx rc=%d: %s",
(unsigned long long)node_id, sqlite3_extended_errcode(db), sqlite3_errmsg(db));
goto failed;
allocation_failed:
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "node load: bootstrap allocation failed node=%016llx", (unsigned long long)node_id);
failed:
sqlite3_finalize(s);
ni->node_name = name; ni->v4_addrs = v4_head; ni->v4_sock_meta = m4_head;
ni->v6_addrs = v6_head; ni->v6_sock_meta = m6_head; ni->reality_socks = r_head;
topo_node_destroy(groups, ni);
return NULL;
}
int topo_node_sqlite_set_connected(sqlite3* db, const char* channel_id, uint64_t node_id, int connected) {
@@ -1346,98 +1414,90 @@ int topo_node_sqlite_get_connect_peers(sqlite3* db, const char* channel_id,
sqlite3_finalize(stmt); *out = peers; *out_count = count; return 0;
}
int topo_node_sqlite_get_connected_peers(sqlite3* db, const char* channel_id,
uint64_t** out_ids, int* out_count) {
if (!db || !channel_id || !out_ids || !out_count) return -1;
/* Один проход SQL: частичный массив никогда не публикуется при ошибке чтения/памяти. */
static int node_ids_query(sqlite3* db, const char* sql, int exclude_self, uint64_t self,
uint64_t** out_ids, int* out_count) {
*out_ids = NULL; *out_count = 0;
char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl));
char sql[192]; snprintf(sql, sizeof(sql), "SELECT node_id FROM \"%s\" WHERE connected=1 AND deleted=0", peers_tbl);
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) return -1;
int cnt = 0;
while (sqlite3_step(st) == SQLITE_ROW) cnt++;
if (cnt == 0) { sqlite3_finalize(st); return 0; }
uint64_t* ids = u_malloc((size_t)cnt * sizeof(uint64_t));
if (!ids) { sqlite3_finalize(st); return -1; }
sqlite3_reset(st); int i = 0;
while (sqlite3_step(st) == SQLITE_ROW) ids[i++] = (uint64_t)sqlite3_column_int64(st, 0);
uint64_t* ids = NULL;
size_t count = 0, capacity = 0;
int rc = sqlite3_prepare_v2(db, sql, -1, &st, NULL);
if (rc == SQLITE_OK && exclude_self) rc = sqlite3_bind_int64(st, 1, (sqlite3_int64)self);
if (rc != SQLITE_OK) goto fail;
while ((rc = sqlite3_step(st)) == SQLITE_ROW) {
if (count == capacity) {
size_t next = capacity ? capacity * 2 : 16;
if (next > UINT32_MAX / sizeof(*ids)) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "peer ID list too large: count=%zu", count); goto fail;
}
uint64_t* resized = u_realloc(ids, next * sizeof(*ids));
if (!resized) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "peer ID list allocation failed: count=%zu", count); goto fail; }
ids = resized; capacity = next;
}
ids[count++] = (uint64_t)sqlite3_column_int64(st, 0);
}
if (rc != SQLITE_DONE) goto fail;
sqlite3_finalize(st);
*out_ids = ids; *out_count = cnt;
*out_ids = ids; *out_count = (int)count;
DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "peer ID list read: count=%zu exclude_self=%d", count, exclude_self);
return 0;
fail:
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "peer ID query failed: rc=%d rows=%zu sql=%s: %s", rc, count, sql, sqlite3_errmsg(db));
sqlite3_finalize(st); u_free(ids); return -1;
}
int topo_node_sqlite_get_connected_peers(sqlite3* db, const char* channel_id, uint64_t** out_ids, int* out_count) {
if (out_ids) *out_ids = NULL;
if (out_count) *out_count = 0;
if (!db || !channel_id || !out_ids || !out_count) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "connected peers: invalid arguments"); return -1;
}
char table[80], sql[192]; peers_table_name(channel_id, table, sizeof(table));
snprintf(sql, sizeof(sql), "SELECT node_id FROM \"%s\" WHERE connected=1 AND deleted=0", table);
return node_ids_query(db, sql, 0, 0, out_ids, out_count);
}
int topo_node_sqlite_get_public_peers(sqlite3* db, const char* channel_id,
uint64_t** out_ids, int* out_count, uint64_t self_node_id) {
if (!db || !channel_id || !out_ids || !out_count) return -1;
*out_ids = NULL; *out_count = 0;
char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl));
char sql[384]; snprintf(sql, sizeof(sql),
uint64_t** out_ids, int* out_count, uint64_t self_node_id) {
if (out_ids) *out_ids = NULL;
if (out_count) *out_count = 0;
if (!db || !channel_id || !out_ids || !out_count) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "public peers: invalid arguments"); return -1;
}
char table[80], sql[512]; peers_table_name(channel_id, table, sizeof(table));
snprintf(sql, sizeof(sql),
"SELECT DISTINCT na.node_id FROM node_addresses na"
" WHERE na.node_id!=?1 AND na.family IN (4,6) AND na.addr_type IN (1,2)"
" AND EXISTS (SELECT 1 FROM nodes n WHERE n.node_id=na.node_id)"
" AND NOT EXISTS (SELECT 1 FROM \"%s\" p WHERE p.node_id=na.node_id AND p.deleted=1)",
peers_tbl);
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) return -1;
sqlite3_bind_int64(st, 1, (sqlite3_int64)self_node_id);
int cnt = 0;
while (sqlite3_step(st) == SQLITE_ROW) cnt++;
if (cnt == 0) { sqlite3_finalize(st); return 0; }
uint64_t* ids = u_malloc((size_t)cnt * sizeof(uint64_t));
if (!ids) { sqlite3_finalize(st); return -1; }
sqlite3_reset(st); int i = 0;
while (sqlite3_step(st) == SQLITE_ROW) ids[i++] = (uint64_t)sqlite3_column_int64(st, 0);
sqlite3_finalize(st);
*out_ids = ids; *out_count = cnt;
return 0;
" AND NOT EXISTS (SELECT 1 FROM \"%s\" p WHERE p.node_id=na.node_id AND p.deleted=1)", table);
return node_ids_query(db, sql, 1, self_node_id, out_ids, out_count);
}
int topo_node_sqlite_get_supernode_peers(sqlite3* db, const char* channel_id,
uint64_t** out_ids, int* out_count) {
if (!db || !channel_id || !out_ids || !out_count) return -1;
*out_ids = NULL; *out_count = 0;
char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl));
char sql[256]; snprintf(sql, sizeof(sql),
"SELECT p.node_id FROM \"%s\" p WHERE p.node_type=4 AND p.deleted=0",
peers_tbl);
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) return -1;
int cnt = 0;
while (sqlite3_step(st) == SQLITE_ROW) cnt++;
if (cnt == 0) { sqlite3_finalize(st); return 0; }
uint64_t* ids = u_malloc((size_t)cnt * sizeof(uint64_t));
if (!ids) { sqlite3_finalize(st); return -1; }
sqlite3_reset(st); int i = 0;
while (sqlite3_step(st) == SQLITE_ROW) ids[i++] = (uint64_t)sqlite3_column_int64(st, 0);
sqlite3_finalize(st);
*out_ids = ids; *out_count = cnt;
return 0;
int topo_node_sqlite_get_supernode_peers(sqlite3* db, const char* channel_id, uint64_t** out_ids, int* out_count) {
if (out_ids) *out_ids = NULL;
if (out_count) *out_count = 0;
if (!db || !channel_id || !out_ids || !out_count) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "supernode peers: invalid arguments"); return -1;
}
char table[80], sql[192]; peers_table_name(channel_id, table, sizeof(table));
snprintf(sql, sizeof(sql), "SELECT node_id FROM \"%s\" WHERE node_type=4 AND deleted=0", table);
return node_ids_query(db, sql, 0, 0, out_ids, out_count);
}
int topo_node_sqlite_get_local_peers(sqlite3* db, const char* channel_id,
uint64_t** out_ids, int* out_count, uint64_t self_node_id) {
if (!db || !channel_id || !out_ids || !out_count) return -1;
*out_ids = NULL; *out_count = 0;
char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl));
char sql[384]; snprintf(sql, sizeof(sql),
uint64_t** out_ids, int* out_count, uint64_t self_node_id) {
if (out_ids) *out_ids = NULL;
if (out_count) *out_count = 0;
if (!db || !channel_id || !out_ids || !out_count) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "local peers: invalid arguments"); return -1;
}
char table[80], sql[512]; peers_table_name(channel_id, table, sizeof(table));
snprintf(sql, sizeof(sql),
"SELECT DISTINCT na.node_id FROM node_addresses na"
" WHERE na.node_id!=?1 AND na.family IN (4,6) AND na.addr_type IN (0,3)"
" AND EXISTS (SELECT 1 FROM nodes n WHERE n.node_id=na.node_id)"
" AND NOT EXISTS (SELECT 1 FROM \"%s\" p WHERE p.node_id=na.node_id AND p.deleted=1)",
peers_tbl);
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) return -1;
sqlite3_bind_int64(st, 1, (sqlite3_int64)self_node_id);
int cnt = 0;
while (sqlite3_step(st) == SQLITE_ROW) cnt++;
if (cnt == 0) { sqlite3_finalize(st); return 0; }
uint64_t* ids = u_malloc((size_t)cnt * sizeof(uint64_t));
if (!ids) { sqlite3_finalize(st); return -1; }
sqlite3_reset(st); int i = 0;
while (sqlite3_step(st) == SQLITE_ROW) ids[i++] = (uint64_t)sqlite3_column_int64(st, 0);
sqlite3_finalize(st);
*out_ids = ids; *out_count = cnt;
return 0;
" AND NOT EXISTS (SELECT 1 FROM \"%s\" p WHERE p.node_id=na.node_id AND p.deleted=1)", table);
return node_ids_query(db, sql, 1, self_node_id, out_ids, out_count);
}
int topo_node_sqlite_get_node_type(sqlite3* db, const char* channel_id, uint64_t node_id) {
+50 -15
View File
@@ -1,6 +1,7 @@
/* topo_node_sqlite — хранение подписанных снимков узлов, адресов, каналов и блоков мемберов в общей SQLite.
* Модель/проверка подписей/registry — topo_node и member_sync; этот модуль реализует SQL-операции в uasync-потоке.
* snapshot_put проверяет подпись и сохраняет только более свежий timestamp; node_load предпочитает snapshot.
* Только snapshot_put/load проверяют подпись; остальные записи требуют проверенных вызывающим данных.
* snapshot_put сохраняет более свежий timestamp; node_load предпочитает snapshot.
* db/groups заимствованы. Возвращённый TOPO_NODE без registry ref: передать registry либо уничтожить через topo_node_destroy.
* Массивы out_ids/out_ch_ids/out_peers принадлежат вызывающему и освобождаются u_free. */
#ifndef TOPO_NODE_SQLITE_H
@@ -19,41 +20,55 @@
/* flags byte in wire format */
#define PEERS_FLAG_HAS_JOIN 0x01
/* Создать/обновить nodes, node_addresses, node_snapshots, channels; 0/-1. db открывает и закрывает вызывающий. */
int topo_node_sqlite_init(sqlite3* db);
/* Проверить и сохранить полный подписанный NODEINFO без групповых полей; старый/равный timestamp даёт 0 без замены. */
int topo_node_sqlite_snapshot_put(sqlite3* db, struct TOPO_NODE* ni);
/* Максимальный сохранённый timestamp (Unix microseconds), в том числе после инвалидирования record; 0 — нет/ошибка. */
uint64_t topo_node_sqlite_snapshot_timestamp(sqlite3* db, uint64_t node_id);
/* Незарегистрированный проверенный снимок, включая снимок без адресов; NULL — нет/невалиден/ошибка.
* groups и groups->instance->topo_groups должны обозначать один контейнер с пулами. */
struct TOPO_NODE* topo_node_sqlite_snapshot_load(sqlite3* db, struct TOPO_GROUPS* groups, uint64_t node_id);
/* Персистентная запись адресов узла в node_addresses (источник истины — BGP/topo_group).
* DELETE + INSERT из ni->v4_addrs/v6_addrs; mapping/inbound/source из подписанного SOCKMETA. */
* DELETE + INSERT из ni->v4_addrs/v6_addrs под savepoint, включая пустой набор; 0/-1, отказ откатывает замену.
* mapping/inbound/source берутся из SOCKMETA; проверку ni выполняет вызывающий. RTT при замене не сохраняется. */
int topo_node_sqlite_addrs_put(sqlite3* db, uint64_t node_id, struct TOPO_NODE* ni);
/* Есть ли у узла запись мембера хотя бы в одной peers_<ch> (реальный мембер с ключами). */
/* Есть ли строка хотя бы в одной peers_*: включая source=1 и deleted=1; 1/0, при ошибке также 0. */
int topo_node_sqlite_member_exists(sqlite3* db, uint64_t node_id);
/* Список channel_id (uint64) всех peers_<ch>, где узел является членом. 0=ок. */
/* Числовые ненулевые channel_id таблиц peers_*, содержащих строку узла; включает placeholders/deleted.
* 0/-1; пустой результат NULL/0, при ошибке частичный массив не выдаётся. */
int topo_node_sqlite_get_member_channels(sqlite3* db, uint64_t node_id,
uint64_t** out_ch_ids, int* out_count);
/* Создать peers_<id> и сохранить канал; 0/-1. Ключи — по 32, signature — 64 байта, privkey необязательны.
* При обновлении публичные ключи обязаны совпасть; имя/владелец/подпись заменяются, NULL privkey сохраняет прежний.
* Подпись/пары ключей не проверяет. Табличный суффикс: первые 63 символа, вне [A-Za-z0-9_] заменены на '_'. */
int topo_node_sqlite_channel_put(sqlite3* db, const char* channel_id,
const char* name, uint64_t owner_node_id,
const uint8_t* x25519_pub, const uint8_t* x25519_priv,
const uint8_t* ed25519_pub, const uint8_t* ed25519_priv,
const uint8_t* signature);
/* Подготовить локальные поля участника при создании и загрузке канала. */
/* Добавить radio_muted=0 в существующую peers_<id>, если поля ещё нет; 0/-1, идемпотентно. */
int topo_node_sqlite_member_local_init(sqlite3* db, const char* channel_id);
/* Полное локальное удаление канала: DROP peers_<id>/msg_<id>, DELETE channels,
* Удаляются feedback_<channel_id> и merkle_tree_hash всех datasets этого канала. 0=ок. */
* Удаляются feedback_<id> и merkle_tree_hash всех datasets канала (если таблица есть).
* 0/-1; savepoint откатывает в том числе DROP TABLE, внешняя транзакция сохраняется. */
int topo_node_sqlite_channel_delete(sqlite3* db, const char* channel_id);
/* Упрощённый плейсхолдер мембера от topo_group (source=1, не участвует в merkle-хеше). */
/* Вставить source=1 с нулевой join_sig; существующую строку не менять. 0/-1; ключи по 32 байта.
* Плейсхолдер не участвует в Merkle-хеше; канал/peers_<id> должны уже существовать. */
int topo_node_sqlite_member_placeholder_put(sqlite3* db, const char* channel_id, uint64_t node_id,
const uint8_t* x25519_pubkey, const uint8_t* ed25519_pubkey);
/* Обновляет только блок мембера (keys/join/update_sig/update_ts/userinfo), не трогая блок владельца.
* signed_by/signature — дерево приглашений: кто подписал pubkey узла (0 = корень, ключом группы). */
* signed_by/signature — дерево приглашений: кто подписал pubkey узла (0 = корень, ключом группы).
* 0/-1; подписи по 64, ключи по 32 байта. NULL join_sig даёт нулевую подпись, userinfo=NULL даёт ''.
* Сбрасывает source=0; свежесть/подписи проверяет вызывающий, owner/local-поля сохраняются. */
int topo_node_sqlite_member_block_put(sqlite3* db, const char* channel_id, uint64_t node_id,
const uint8_t* join_sig, uint64_t join_ts,
const uint8_t* update_sig, uint64_t update_ts,
@@ -64,25 +79,33 @@ int topo_node_sqlite_member_block_put(sqlite3* db, const char* channel_id, uint6
int topo_node_sqlite_member_get_sign(sqlite3* db, const char* channel_id, uint64_t node_id,
uint64_t* signed_by_out, uint8_t signature_out[64]);
/* Есть ли узел в таблице мемберов канала peers_<ch_id> (source 0 или 1). 1=да, 0=нет. */
/* Есть ли строка в peers_<id>, включая placeholders/deleted. 1/0; ошибка также даёт 0. */
int topo_node_sqlite_member_in_channel(sqlite3* db, const char* channel_id, uint64_t node_id);
/* Обновляет только блок владельца (adm_tags/adm_tags_sig/storage). */
/* Обновить owner-блок и deleted из adm_tags; 0/-1, отсутствующая строка даёт 0 без вставки.
* adm_tags/adm_tags_sig могут быть NULL; ненулевая подпись — 64 байта, проверяется вызывающим. */
int topo_node_sqlite_member_owner_put(sqlite3* db, const char* channel_id, uint64_t node_id,
const char* adm_tags, const uint8_t* adm_tags_sig, int storage);
/* Прочитать ровно 64 байта join_sig и join_ts; 0/-1, выходы обязательны и при неуспехе не меняются. */
int topo_node_sqlite_member_get_join(sqlite3* db, const char* channel_id, uint64_t node_id,
uint8_t* join_sig_out, uint64_t* join_ts_out);
/* Физически удалить строку участника; 0/-1. Отсутствующая строка даёт 0; tombstone/Merkle не создаёт. */
int topo_node_sqlite_member_del(sqlite3* db, const char* channel_id, uint64_t node_id);
/* Записать проверенные вызывающим ключи/имя в nodes; 0/-1. Пустое имя сохраняет прежнее при UPDATE.
* Ключи заменяются независимо от update_ts; update_ts хранит максимум, created_at сохраняется, если не NULL.
* Новая строка online=1, при UPDATE online сохраняется. Подпись и свежесть здесь не проверяются. */
int topo_node_sqlite_node_update_verified(sqlite3* db, uint64_t node_id,
const char* name, const uint8_t* x25519, const uint8_t* ed25519, uint64_t update_ts, time_t created_at);
/* Метаданные канала; 0/-1. Все выходы необязательны; name_sz включает NUL, ключи по 32, подпись 64 байта. */
int topo_node_sqlite_channel_get(sqlite3* db, const char* channel_id,
char* name_out, int name_sz, uint64_t* owner_node_id,
uint8_t* x25519_pub, uint8_t* ed25519_pub, uint8_t* signature);
/* Прочитать первые 32 байта Ed25519 privkey; 0/-1, отсутствующий/короткий ключ даёт -1. */
int topo_node_sqlite_channel_get_priv(sqlite3* db, const char* channel_id,
uint8_t* ed25519_priv_out);
@@ -90,32 +113,40 @@ int topo_node_sqlite_channel_get_priv(sqlite3* db, const char* channel_id,
int topo_node_sqlite_channel_get_privs(sqlite3* db, const char* channel_id,
uint8_t* x25519_priv_out, uint8_t* ed25519_priv_out);
/* Записать оба канальных privkey (импорт прав админа). */
/* Записать оба канальных privkey по 32 байта (импорт прав админа); 0/-1, без проверки пары/подписи.
* Отсутствующая строка даёт 0 без вставки; наличие канала проверяет вызывающий. */
int topo_node_sqlite_channel_set_privs(sqlite3* db, const char* channel_id,
const uint8_t* x25519_priv, const uint8_t* ed25519_priv);
/* Установить online=0/1 существующей строки; 0/-1. Отсутствующая строка даёт 0 без вставки. */
int topo_node_sqlite_node_set_online(sqlite3* db, uint64_t node_id, int online);
/* online из nodes; 0, если записи нет или запрос не выполнен. */
int topo_node_sqlite_node_get_online(sqlite3* db, uint64_t node_id);
/* Скопировать ровно 32 байта Ed25519 из nodes: 0 — найден, -1 — отсутствует/другая длина/ошибка SQLite.
* Значение байтов не определяет наличие ключа; криптографическую пригодность проверяет операция verify. */
int topo_node_sqlite_get_ed25519_pubkey(sqlite3* db, uint64_t node_id, uint8_t pubkey_out[32]);
/** Пересчитывает node_type во всех peers_* таблицах на основе addr_type из node_addresses */
/* Атомарно пересчитать node_type/storage/deleted во всех peers_* по адресам и adm_tags; 0/-1.
* node_type: 0 нет адресов, 1 прямой/локальный/strict, 2 NAT/contact, 4 supernode=yes.
* storage=yes действует только при наличии ADDR_TYPE_DIRECT, deleted берётся из adm_tags.deleted=yes. */
int topo_node_sqlite_nodeinfo_updated(sqlite3* db, uint64_t node_id);
/** Обновляет RTT узла: node_addresses.rtt и все peers_*.node_RTT */
/* Обновить node_addresses.rtt и все peers_*.node_RTT, единицы 0.1 ms; ошибки логируются, обновление неатомарно. */
void topo_node_sqlite_update_rtt(sqlite3* db, uint64_t node_id, uint16_t rtt);
/**
* @brief Загружает TOPO_NODE из SQLite (ref_count=0).
* @param groups нужен для выделения адресов из memory_pool
* @return TOPO_NODE* или NULL если узел не найден/нет адресов
* @return TOPO_NODE* или NULL, если нет пригодной записи/адресов bootstrap либо при ошибке
*
* Сначала загружает и проверяет подписанный snapshot; при его отсутствии собирает сведения nodes/node_addresses.
* Вызывающий передаёт владение через topo_node_registry_acquire() либо вызывает topo_node_destroy().
* Snapshot может быть без адресов. Bootstrap требует адресов и получает timestamp=0, профиль UNKNOWN.
* При отказе SQL/памяти частичная запись освобождается. groups->instance->topo_groups должен совпадать с groups.
* Вызывающий передаёт владение через topo_node_registry_store() либо вызывает topo_node_destroy().
*/
struct TOPO_NODE* topo_node_sqlite_node_load(sqlite3* db, struct TOPO_GROUPS* groups, uint64_t node_id);
/* Записать connected=0/1 существующего участника; 0/-1, без вставки отсутствующей строки. */
int topo_node_sqlite_set_connected(sqlite3* db, const char* channel_id, uint64_t node_id, int connected);
/* Кандидат CHAT-подключения: роль канальная, тип устройства из подписанного snapshot.
@@ -129,6 +160,10 @@ struct topo_connect_peer {
* 0/-1; массив принадлежит вызывающему и освобождается u_free(). */
int topo_node_sqlite_get_connect_peers(sqlite3* db, const char* channel_id,
struct topo_connect_peer** out, size_t* out_count);
/* Нижеследующие выборки: 0/-1, пусто/ошибка — NULL/0, частичный массив не выдаётся; освобождение u_free.
* connected/supernode ограничены участниками канала, self не исключают, deleted=1 исключают.
* public/local — глобальные адреса с записью nodes, исключены self и deleted=1 в указанном канале;
* наличие членства в канале не требуется. public: addr_type 1/2, local: 0/3; фильтра port!=0 нет. */
int topo_node_sqlite_get_connected_peers(sqlite3* db, const char* channel_id, uint64_t** out_ids, int* out_count);
int topo_node_sqlite_get_public_peers(sqlite3* db, const char* channel_id, uint64_t** out_ids, int* out_count, uint64_t self_node_id);
int topo_node_sqlite_get_local_peers(sqlite3* db, const char* channel_id, uint64_t** out_ids, int* out_count, uint64_t self_node_id);
+117 -31
View File
@@ -1,38 +1,124 @@
# topo_node_sqlite — SQLite-персистентность топологии
# topo_node_sqlite — хранение топологии в SQLite
## 1. Назначение
Обеспечивает сохранение и восстановление топологии P2P-сети (ноды, адреса, каналы, участники каналов) в SQLite-базе. База используется как основной источник данных для chatgui и восстановления состояния между запусками utun.
## Назначение и владение
В отличие от LMDB-слоя (`topo_node_lmdb`), который хранит данные в бинарном формате BGP-сообщений, SQLite-слой работает с нормализованными таблицами и используется для клиентских целей — чтение данных chatgui, получение списка участников канала, проверка online-статуса.
Модуль работает с общей SQLite ядра: подписанные снимки узлов, производные адреса/состояния,
описания каналов и строки участников. Базу открывает и закрывает владелец ядра; все операции
этого модуля выполняются в его uasync-потоке. Отдельного LMDB-хранилища топологии здесь нет.
## 2. Как пользоваться
1. Получить готовый `sqlite3* db` (открывается извне, обычно в `chat_sync.c`)
2. Вызвать `topo_node_sqlite_init(db)` для создания таблиц
3. Сохранять/обновлять ноды через `topo_node_sqlite_node_put()`
4. Сохранять каналы через `topo_node_sqlite_channel_put()` (автоматически создаёт таблицу `peers_<channel_id>`)
5. Добавлять участников в канал через `topo_node_sqlite_member_put()`
6. Читать участников из `peers_<channel_id>`; постраничная сетевая сериализация принадлежит `member_sync`.
Проверку подписи выполняют только topo_node_sqlite_snapshot_put/load.
Канальные метаданные, ключи участников и их версии проверяют вызывающие chat_sync/member_sync.
SQL-функции записи блоков сами не проверяют подписи и не отбрасывают старые версии.
**Нюансы:**
- Имена таблиц `peers_<channel_id>` санитизируются: все символы кроме `[a-zA-Z0-9_]` заменяются на `_`
- Сетевой формат участников описан в `src/chat/member_sync_doc.md`; SQLite-слой не дублирует сериализатор.
- Ключи в BLOB-полях всегда 32 байта (x25519/ed25519), подписи — 64 байта
- Функции `online` и `get_ed25519_pubkey` — простые SELECT-запросы без JOIN
Возвращённый TOPO_NODE не зарегистрирован и имеет нулевой refcount.
Передайте владение через topo_node_registry_store либо вызовите topo_node_destroy(groups, node).
Массивы идентификаторов и topo_connect_peer освобождаются через u_free.
db, groups и входные строки/объекты остаются у вызывающего.
## 3. API
## Таблицы и версии
| Функция | Описание |
|---------|----------|
| `topo_node_sqlite_init(db)` | Создаёт таблицы `nodes`, `node_addresses`, `channels` (если не существуют) |
| `topo_node_sqlite_node_put(db, nq)` | Сохраняет ноду и её адреса (IPv4/IPv6) в транзакции; старые адреса удаляются |
| `topo_node_sqlite_channel_put(...)` | Сохраняет канал в `channels` + создаёт таблицу `peers_<ch_id>` |
| `topo_node_sqlite_member_put(...)` | Добавляет/обновляет участника в `peers_<ch_id>` (INSERT OR REPLACE) |
| `topo_node_sqlite_member_get_join(db, ch_id, node_id, ...)` | Читает join_sig и join_ts участника канала |
| `topo_node_sqlite_member_del(db, ch_id, node_id)` | Удаляет участника из `peers_<ch_id>` |
| `topo_node_sqlite_node_update_verified(...)` | Обновляет ключи и имя ноды по условию `update_ts < новое_значение` (защита от stale-данных) |
| `topo_node_sqlite_channel_get(...)` | Читает метаданные канала (имя, владелец, ключи, подпись) |
| `topo_node_sqlite_node_set_online(db, id, online)` | Устанавливает флаг `online` для ноды |
| `topo_node_sqlite_node_get_online(db, id)` | Возвращает 1 если нода online |
| `topo_node_sqlite_get_ed25519_pubkey(db, id, out)` | Извлекает ed25519-публичный ключ ноды (32 байта) |
| Таблица | Назначение |
| --- | --- |
| node_snapshots | node_id, максимальный timestamp, полный подписанный NODEINFO в BLOB record |
| nodes | Проекция имени и ключей, online/last_seen, update_ts, created_at; не замена подписанному снимку |
| node_addresses | IPv4/IPv6 endpoints, protocol, socket_id, address_kind, NAT-профиль, классификация, RTT и REALITY options |
| channels | Имя, владелец, публичные/необязательные приватные ключи, подпись канала и локальные поля |
| peers_&lt;id&gt; | Блок участника, блок владельца и локальные поля конкретного канала |
**Структуры:** внешние — `sqlite3*`, `struct TOPO_GROUP_NODE`, `struct TOPO_NODE`; собственных структур нет.
topo_node_sqlite_init создаёт общие таблицы/индексы, обрабатывает прежние форматы проекции адресов
и восстанавливает deleted из adm_tags. peers_&lt;id&gt; создаётся при channel_put;
member_local_init идемпотентно добавляет radio_muted.
В снимке timestamp — Unix microseconds и версия всей подписанной записи.
snapshot_put проверяет подпись и заменяет BLOB только при большем timestamp.
Старый или равный timestamp даёт успех без замены. При смене формата BLOB может инвалидироваться
без сброса сохранённого timestamp, чтобы новая локальная публикация оставалась новее.
node_update_verified записывает уже проверенные ключи и непустое имя независимо от update_ts.
В nodes.update_ts сохраняется максимум прежнего и входного значения; это не фильтр замены ключей.
created_at сохраняется, если уже не NULL. Новая строка получает online=1,
обновление существующей строки online не меняет.
## Снимок и bootstrap
topo_node_sqlite_node_load сначала загружает и проверяет снимок. Валидный снимок может содержать
пустой список адресов: это полноценное опубликованное состояние, а не повод вернуть старые адреса.
Если пригодного снимка нет, функция собирает bootstrap из nodes и node_addresses.
Для этого требуется хотя бы один адрес. Ключи/имя копируются из проекции, timestamp остаётся 0,
профили сокетов сбрасываются в UNKNOWN; сохранённая SQL-классификация не считается подписанным доказательством.
IPv4/IPv6 сохраняются отдельно, REALITY восстанавливается из options, порт 0 сохраняется как известный TCP IP.
Пулы берутся из groups; groups->instance->topo_groups должен указывать на тот же контейнер.
При ошибке SQL или памяти частичные списки, имя, REALITY и statement освобождаются; возвращается NULL.
## Адреса, классификация и RTT
addrs_put заменяет все адреса узла одним savepoint: DELETE, затем INSERT IPv4/IPv6.
Пустой список очищает проекцию. При отказе замена откатывается.
Профиль mapping/inbound/source берётся из SOCKMETA, REALITY — из записи соответствующего socket_id.
Функция не проверяет подпись входного узла. Замена адресов не сохраняет прежний RTT.
address_kind — INTERFACE/NAT/REAL, отдельно от вычисляемого addr_type.
Классификация учитывает IP, protocol, конфигурационный тип сокета и профиль доступности.
Адрес с port=0 хранится, но не является готовым endpoint для подключения.
nodeinfo_updated под savepoint обновляет node_type, storage, deleted во всех peers_*:
| node_type | Основание |
| --- | --- |
| 0 | Нет адресов или классификация неизвестна |
| 1 | NETIF, DIRECT или NAT_STRICT |
| 2 | NAT_EIM, NAT, CONTACT или EIM_UNVERIFIED |
| 4 | adm_tags.supernode=yes |
При выборе адреса DIRECT имеет приоритет, иначе берётся минимальный addr_type.
storage=yes применяется только при DIRECT; deleted выводится из adm_tags.deleted=yes.
При отказе одной канальной записи весь пересчёт узла откатывается, функция возвращает -1.
update_rtt обновляет node_addresses.rtt и peers_*.node_RTT в единицах 0.1 ms.
Обновление этих таблиц неатомарно; ошибки логируются. Online и connected меняются отдельными функциями.
## Каналы и участники
channel_put требует публичные ключи по 32 байта и подпись 64 байта.
При конфликте channel_id публичные ключи должны совпасть: меняются имя/владелец/подпись,
переданные privkey заменяют прежние, NULL сохраняет их. Криптографическую проверку делает вызывающий.
Суффикс таблиц строится из первых 63 символов channel_id: вне [A-Za-z0-9_] заменяются на '_'.
Штатные channel_id — десятичные uint64. Строка channels.channel_id хранит исходное значение.
member_placeholder_put вставляет source=1 с нулевой join_sig; существующую строку не меняет.
member_block_put заменяет ключи/join/update/userinfo/дерево приглашений и устанавливает source=0,
сохраняя owner-блок и локальные поля. NULL join_sig записывается как 64 нулевых байта.
member_owner_put меняет adm_tags/подпись/storage/deleted без вставки отсутствующей строки.
Placeholder не участвует в Merkle-хеше; сетевой формат и проверка версий принадлежат member_sync.
member_exists, member_in_channel, get_member_channels учитывают любое наличие строки,
в том числе source=1 и deleted=1. Это не проверка полной подписанной записи участника.
member_get_join требует ровно 64 байта подписи; channel_get проверяет размер запрошенных ключей/подписи.
member_del физически удаляет строку без tombstone и пересчёта Merkle.
channel_delete удаляет peers/msg/feedback, канал с приватными ключами и хеши datasets его namespace
(если таблица Merkle существует). Все действия, включая DROP TABLE, защищены savepoint:
ошибка возвращает -1 и откатывает удаление, сохраняя внешнюю транзакцию.
## Выбор пиров
Все выборки возвращают принадлежащий вызывающему массив. Пустой результат — NULL/0.
Ошибка чтения/памяти — -1 без публикации частичного массива.
| API | Выборка |
| --- | --- |
| get_connect_peers | Все неудалённые участники канала, включая self; connected, затем известный RTT/RTT/node_id |
| get_connected_peers | connected=1, deleted=0 в указанном канале |
| get_supernode_peers | node_type=4, deleted=0 в указанном канале |
| get_public_peers | Глобальные адреса addr_type 1/2 с записью nodes, кроме self и удалённых в указанном канале |
| get_local_peers | Аналогично public, но addr_type 0/3 |
Public/local не требуют членства в указанном канале и не фильтруют port=0.
Для get_connect_peers роль supernode берётся из adm_tags, client_type — из сохранённого
подписанного snapshot; отсутствие снимка даёт UINT8_MAX. Адреса выборку не ограничивают.
Сам запрос не повторяет криптографическую проверку BLOB; подпись проверяется при сохранении снимка.
Полный список функций, размеры буферов и возвраты — в topo_node_sqlite.h.
+67 -19
View File
@@ -29,7 +29,6 @@
// Constants from spec (adjusted for completeness)
#define MAX_INFLIGHT_BYTES 65536 // Initial window
#define ACK_DELAY_TB 20 // ACK timer delay (2ms in 0.1ms units)
#define BURST_DELAY_FACTOR 4 // Delay before burst
#define BURST_SIZE 5 // Packets in burst (1 delayed + 4 burst)
#define RTT_HISTORY_SIZE 10 // For jitter calc
@@ -56,7 +55,7 @@ static void etcp_schedule_send(struct ETCP_CONN* conn, int delay_tb) {
if (!conn || conn->close_requested || conn->state == 2 || conn->tx_token) return;
if (!conn->initialized || conn->session_required || conn->reinit_pending) return;
if (conn->tx_running && !delay_tb) return;
int work = conn->input_send_q->count || conn->input_queue->count || conn->ack_till_pending || conn->ack_request_pending;
int work = conn->input_send_q->count || conn->input_queue->count || conn->ack_till_deadline_us || conn->ack_request_pending;
for (struct ETCP_LINK* link = conn->links; !work && link; link = link->next)
work = link->burst_active || link->ack_reply_pending || (link->ack_q && link->ack_q->count);
if (!work) return;
@@ -463,6 +462,7 @@ void etcp_connection_close(struct ETCP_CONN* etcp) {
// Cancel active timers
if (etcp->ack_resp_timer) { uasync_cancel_timeout(etcp->instance->ua, etcp->ack_resp_timer); etcp->ack_resp_timer = NULL; }
etcp->ack_resp_deadline_us = etcp->ack_till_deadline_us = 0;
if (etcp->ack_window_timer) { uasync_cancel_timeout(etcp->instance->ua, etcp->ack_window_timer); etcp->ack_window_timer = NULL; }
routing_del_conn(etcp);
@@ -522,11 +522,12 @@ void etcp_conn_reset(struct ETCP_CONN* conn) {
for (struct ETCP_LINK* link = conn->links; link; link = link->next) etcp_tx_link_reset(link, 0);
etcp_cancel_send(conn);
if (conn->ack_resp_timer) { uasync_cancel_timeout(conn->instance->ua, conn->ack_resp_timer); conn->ack_resp_timer = NULL; }
conn->ack_resp_deadline_us = 0;
if (conn->ack_window_timer) { uasync_cancel_timeout(conn->instance->ua, conn->ack_window_timer); conn->ack_window_timer = NULL; }
queue_clear(conn->input_queue); queue_clear(conn->output_queue);
queue_clear(conn->input_send_q); queue_clear(conn->input_wait_ack); queue_clear(conn->recv_q);
if (conn->normalizer) pn_reset(conn->normalizer);
conn->ack_till_pending = conn->ack_request_pending = 0;
conn->ack_till_deadline_us = conn->ack_request_pending = 0;
conn->next_tx_id = 1; conn->last_rx_id = conn->last_delivered_id = conn->rx_ack_till = 0;
conn->got_initial_pkt = 0;
conn->unacked_bytes = conn->rtt_last = conn->rtt_avg_10 = conn->jitter = conn->last_rtt_cb_time = 0;
@@ -794,7 +795,7 @@ static void input_queue_try_resume(struct ETCP_CONN* etcp) {// при ACK
if (etcp->unacked_bytes < etcp->optimal_inflight) {
DEBUG_TRACE(DEBUG_CATEGORY_ETCP, "[%s] resume input queue: unacked_bytes=%u input_len=%zu", etcp->log_name,
etcp->unacked_bytes, etcp->input_queue->total_bytes);
queue_resume_callback(etcp->input_queue);// и только когда больше нечего отправлять - забираем новый пакет
queue_resume_callback(etcp->input_queue); // освобождённый общий бюджет допускает новые DATA и при непустой send_q
}
}
@@ -962,16 +963,48 @@ void etcp_on_link_down(struct ETCP_CONN* etcp, struct ETCP_LINK* down_link) {
static void ack_response_timer_cb(void* arg) {
struct ETCP_CONN* conn = arg;
conn->ack_resp_timer = NULL;
etcp_conn_wake_sender(conn);
DEBUG_TRACE(DEBUG_CATEGORY_ETCP, "[%s] ACK timer fired due_us=%llu now_us=%llu", conn->log_name,
(unsigned long long)conn->ack_resp_deadline_us, (unsigned long long)get_time_us());
conn->ack_resp_timer = NULL; conn->ack_resp_deadline_us = 0;
etcp_conn_schedule_ack(conn);
}
static void ack_schedule(struct ETCP_CONN* conn) {
if (conn->ack_resp_timer || conn->close_requested) return;
conn->ack_resp_timer = uasync_set_timeout(conn->instance->ua, ACK_DELAY_TB, conn, ack_response_timer_cb, "etcp_ack_resp");
/* Один таймер обслуживает ближайший абсолютный срок: новый RX не отодвигает старые ACK. */
void etcp_conn_schedule_ack(struct ETCP_CONN* conn) {
if (!conn) return;
uint64_t now = get_time_us(), deadline = 0;
if (!conn->close_requested && conn->state != 2 && conn->initialized && !conn->session_required && !conn->reinit_pending) {
deadline = conn->ack_till_deadline_us;
for (struct ETCP_LINK* link = conn->links; link; link = link->next) {
if (!link->initialized || !link->link_status) continue;
if (link->ack_reply_pending) { deadline = now; break; }
const struct ACK_PACKET* first = link->ack_q ? (struct ACK_PACKET*)link->ack_q->head : NULL;
if (first && (!deadline || first->received_at_us + ETCP_ACK_DELAY_US < deadline))
deadline = first->received_at_us + ETCP_ACK_DELAY_US;
}
}
if (conn->ack_resp_timer && conn->ack_resp_deadline_us == deadline && deadline > now) return;
if (conn->ack_resp_timer) {
DEBUG_TRACE(DEBUG_CATEGORY_ETCP, "[%s] ACK timer replace old_us=%llu next_us=%llu", conn->log_name,
(unsigned long long)conn->ack_resp_deadline_us, (unsigned long long)deadline);
uasync_cancel_timeout(conn->instance->ua, conn->ack_resp_timer); conn->ack_resp_timer = NULL;
}
conn->ack_resp_deadline_us = 0;
if (!deadline) return;
if (deadline <= now) { etcp_conn_wake_sender(conn); return; }
int delay = (int)((deadline - now + 99) / 100);
conn->ack_resp_timer = uasync_set_timeout(conn->instance->ua, delay, conn, ack_response_timer_cb, "etcp_ack_resp");
if (!conn->ack_resp_timer) {
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "[%s] ACK delay timer allocation failed, requesting immediate send", conn->log_name);
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "[%s] ACK timer OOM due_us=%llu delay_tb=%d; forcing urgent ACK", conn->log_name,
(unsigned long long)deadline, delay);
for (struct ETCP_LINK* link = conn->links; link; link = link->next)
if (link->initialized && link->link_status && link->ack_q && link->ack_q->count) link->ack_reply_pending = 1;
if (conn->ack_till_deadline_us) conn->ack_till_deadline_us = now;
etcp_conn_wake_sender(conn);
} else {
conn->ack_resp_deadline_us = deadline;
DEBUG_TRACE(DEBUG_CATEGORY_ETCP, "[%s] ACK timer arm due_us=%llu now_us=%llu delay_tb=%d", conn->log_name,
(unsigned long long)deadline, (unsigned long long)now, delay);
}
}
@@ -1099,6 +1132,8 @@ struct ETCP_DGRAM* etcp_request_pkt(struct ETCP_CONN* conn) {
if (conn->tx_state) conn->tx_state = ETCP_TX_STATE_LINK_WAIT;
return NULL;
}
etcp_conn_schedule_ack(conn);
if (conn->close_requested) return NULL;
struct ETCP_LINK* link = NULL;
struct INFLIGHT_PACKET* packet = send_queue_select(conn, &link);
if (!packet) {
@@ -1109,14 +1144,26 @@ struct ETCP_DGRAM* etcp_request_pkt(struct ETCP_CONN* conn) {
conn->session_required || conn->reinit_pending) return NULL;
if (pulled) packet = send_queue_select(conn, &link);
}
if (!link) {
for (struct ETCP_LINK* candidate = conn->links; candidate; candidate = candidate->next)
if (candidate->initialized && candidate->link_status &&
(candidate->ack_reply_pending || (candidate->ack_q && candidate->ack_q->count))) {
link = candidate; packet = NULL; break;
}
if (!link || !link->burst_active) {
uint64_t now = get_time_us();
struct ETCP_LINK* ack_link = NULL;
uint64_t first_due = UINT64_MAX;
for (struct ETCP_LINK* candidate = conn->links; candidate; candidate = candidate->next) {
if (!candidate->initialized || !candidate->link_status) continue;
const struct ACK_PACKET* first = candidate->ack_q ? (struct ACK_PACKET*)candidate->ack_q->head : NULL;
if (!candidate->ack_reply_pending && !first) continue;
uint64_t due = candidate->ack_reply_pending ? 0 : first->received_at_us + ETCP_ACK_DELAY_US;
if (due <= now && (!ack_link || due < first_due)) { ack_link = candidate; first_due = due; }
}
if (ack_link) {
DEBUG_TRACE(DEBUG_CATEGORY_ETCP, "[%s] ACK due path=%u deadline_us=%llu now_us=%llu piggyback=%u", conn->log_name,
ack_link->local_link_id, (unsigned long long)first_due, (unsigned long long)now, ack_link == link && packet != NULL);
if (ack_link != link) packet = NULL;
link = ack_link;
}
}
if (!link && (conn->ack_till_pending || conn->ack_request_pending)) {
if (!link && (conn->ack_request_pending ||
(conn->ack_till_deadline_us && conn->ack_till_deadline_us <= get_time_us()))) {
link = etcp_loadbalancer_select_link(conn, 0, NULL); packet = NULL;
}
if (!link) {
@@ -1272,7 +1319,8 @@ void etcp_output_try_assembly(struct ETCP_CONN* etcp) {
}
if (delivered_count > 0 && !etcp->close_requested && !etcp->reinit_pending) {
etcp->ack_till_pending = 1; ack_schedule(etcp);
if (!etcp->ack_till_deadline_us) etcp->ack_till_deadline_us = get_time_us() + ETCP_ACK_DELAY_US;
etcp_conn_schedule_ack(etcp);
DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "[%s] delivered %u fragments (%u bytes), till=%u recv_q=%d",
etcp->log_name, delivered_count, delivered_bytes, etcp->last_delivered_id, etcp->recv_q->count);
}
@@ -1475,7 +1523,7 @@ void etcp_conn_input(struct ETCP_DGRAM* pkt) {
pkt->link->rx_data_bytes += payload_len; pkt->link->rx_data_time_us = get_time_us();
}
etcp->got_initial_pkt = 1;
if (etcp_tx_queue_ack(pkt->link, seq, attempt_id) == 0) ack_schedule(etcp);
if (etcp_tx_queue_ack(pkt->link, seq, attempt_id) == 0) etcp_conn_schedule_ack(etcp);
DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "[%s] accepted DATA seq=%u attempt=%llu path=%u bytes=%u duplicate=%d",
etcp->log_name, seq, (unsigned long long)attempt_no, pkt->link->local_link_id, payload_len, duplicate);
if (distance == 1) etcp_output_try_assembly(etcp);
+12 -3
View File
@@ -66,6 +66,7 @@ uint16_t get_current_timestamp(void);
#define INFLIGHT_STATE_WAIT_SEND 1
#define INFLIGHT_INITIAL_HASH_SIZE 1024
#define ETCP_LINK_HISTORY_BYTES 32 // по одному биту для локального слота линка 0..255
#define ETCP_ACK_DELAY_US 2000 // обычный ACK-only: срок от старейшей записи; piggyback и срочный ответ не ждут
#define MAX_INFLIGHT_SIZE 16384 // максимальное число элементов в inflight приёмной очереди (для предотвращения атак)
#define ETCP_ACK_GAP_MAX_PACKETS 8192 // предел кумулятивного ACK за приём: 8192 × PACKET_DATA_SIZE(2048) = 16 MB (защита от DoS)
@@ -197,9 +198,10 @@ struct ETCP_CONN {
uint32_t optimal_inflight; // сумма индивидуальных окон; общий бюджет подгрузки новых DATA с учётом unacked_bytes
// Timers
void* ack_resp_timer; // ACK send timer
void* ack_resp_timer; // один таймер ближайшего срока ACK всех линков/логической границы
uint64_t ack_resp_deadline_us; // абсолютный срок установленного таймера; 0 без таймера
void* ack_window_timer; // запрос потерянного till, только при заполненном приёмном окне
uint8_t ack_till_pending; // нужно передать текущую логическую границу по рабочему пути
uint64_t ack_till_deadline_us; // срок передачи текущей логической границы по рабочему пути; 0 — нет запроса
uint8_t ack_request_pending; // нужно запросить границу у пира
void* down_timer; // удаление после непрерывного DOWN, включая первоначальное ожидание
uint64_t down_deadline_tb; // сохраняется при наличии NCD handles после истечения таймера
@@ -342,7 +344,9 @@ void etcp_conn_input(struct ETCP_DGRAM* pkt);
int etcp_int_send(struct ETCP_CONN* etcp, const void* data, uint16_t len);
/* Подготовить следующий пакет из pkt_pool либо NULL при ожидании/ошибке/отсутствии работы.
* Сканирует send_q: готовые повторы имеют приоритет, заблокированные записи пропускаются.
* Наступивший/срочный ACK своего пути имеет приоритет; готовый DATA этого пути забирает его piggyback.
* Сканирует send_q: готовые повторы идут перед новыми DATA, заблокированные записи пропускаются.
* Короткая измерительная burst-серия завершается до перехода к ACK другого пути.
* При отсутствии кандидата допускает одну подгрузку в рамках общего/приёмного окна; callback может сделать reset/close.
* Пакет с DATA содержит резерв попытки: etcp_loadbalancer_send завершает send/rollback и возвращает пакет в пул. */
struct ETCP_DGRAM* etcp_request_pkt(struct ETCP_CONN* etcp);
@@ -351,6 +355,11 @@ struct ETCP_DGRAM* etcp_request_pkt(struct ETCP_CONN* etcp);
* Используется очередями, ACK/RTO, изменением окна и подтверждением сессии. close/reset отменяет запуск. */
void etcp_conn_wake_sender(struct ETCP_CONN* conn);
/* Пересчитать единственный ACK-таймер по головам очередей и ack_till_deadline_us; просроченную работу передать sender.
* Вызвать после RX/продвижения till, завершения send и смены доступных путей. NULL/закрытый conn допустимы.
* Piggyback не ждёт сроков. При OOM таймера существующие флаги срочного ACK обеспечивают немедленный fallback. */
void etcp_conn_schedule_ack(struct ETCP_CONN* conn);
// Process ACK receipt - remove acknowledged packet from inflight queues
void etcp_ack_recv(struct ETCP_CONN* etcp, uint32_t seq);
+1 -1
View File
@@ -264,7 +264,7 @@ struct ETCP_LINK {
void* shaper_timer;
struct ll_queue* attempts; // Физические попытки, внутренний ключ (seq<<16)|attempt_no; ll.len учитывается в окне
struct ll_queue* ack_q; // ACK только DATA этого линка, тот же составной ключ
uint8_t ack_reply_pending; // Ответ на запрос till возвращается тем же путём, даже при пустой ACK-очереди
uint8_t ack_reply_pending; // срочный ACK: ответ till тем же путём либо fallback при OOM таймера; обходит ACK_DELAY
void* retrans_timer; // Срок старейшей принятой транспортом попытки этого канала
uint64_t retrans_deadline_us; // Уже запланированная проверка: более ранний таймер можно сохранить
uint32_t data_rtt_us; // Сглаженный RTT DATA->ACK этого пути; 0 до первого измерения
+3 -2
View File
@@ -186,16 +186,17 @@ struct ETCP_TX_ATTEMPT* etcp_tx_prepare(struct ETCP_LINK* link, struct INFLIGHT_
}
void etcp_tx_complete(struct ETCP_DGRAM* dgram, int success) {
struct ETCP_CONN* conn = dgram->link->etcp;
if (success) {
dgram->link->ack_reply_pending = 0;
dgram->link->etcp->ack_till_pending = 0;
conn->ack_till_deadline_us = 0;
if (dgram->ack_request) dgram->link->etcp->ack_request_pending = 0;
}
etcp_conn_schedule_ack(conn);
struct ETCP_TX_ATTEMPT* attempt = dgram->attempt;
if (!attempt) return;
dgram->attempt = NULL;
struct ETCP_LINK* link = dgram->link;
struct ETCP_CONN* conn = link->etcp;
struct INFLIGHT_PACKET* packet = logical_find(conn, attempt->seq);
if (!success) { attempt_retire(link, attempt, 0, 0); return; }
if (!packet || packet->state != INFLIGHT_STATE_WAIT_SEND) {
+3 -1
View File
@@ -54,6 +54,7 @@ uint32_t etcp_tx_rto_us(const struct ETCP_LINK* link);
struct ETCP_TX_ATTEMPT* etcp_tx_prepare(struct ETCP_LINK* link, struct INFLIGHT_PACKET* packet);
/* Завершить синхронный send: success=1 принят транспортом, 0 — снять резерв без сетевой потери.
* Принятие DATA фиксирует историю каналов и RR; повторно использованный слот начинает новый обход.
* После потребления ACK пересчитывает ближайший срок оставшихся очередей; успешный пакет снимает pending till/urgent этого пути.
* Вызвать ровно один раз до освобождения dgram и до следующего poll; dgram->attempt обнуляется. */
void etcp_tx_complete(struct ETCP_DGRAM* dgram, int success);
/* Принятый ACK: seq подтверждает логическую доставку; внутренний attempt_id=(seq<<16)|номер обновляет только свой линк.
@@ -66,7 +67,8 @@ void etcp_tx_complete(struct ETCP_DGRAM* dgram, int success);
* Непредставимый интервал не создаёт оценку BW. Пустой flight заново начинает приёмную базу, исключая простой. */
void etcp_tx_ack(struct ETCP_LINK* link, uint32_t seq, uint64_t attempt_id, uint32_t receiver_delay_us,
uint64_t peer_rx_bytes, uint64_t peer_rx_time_us);
/* Поставить ACK уже сохранённого/доставленного DATA в очередь этого линка; 0/-1, ошибка логируется. */
/* Поставить ACK уже сохранённого/доставленного DATA в очередь этого линка; 0/-1, ошибка логируется.
* received_at_us сохраняется для RTT и срока ACK-only; вызывающий затем запускает etcp_conn_schedule_ack(). */
int etcp_tx_queue_ack(struct ETCP_LINK* link, uint32_t seq, uint64_t attempt_id);
#endif