From 5f2eb2ef70949fb72865e238a67b558701c6cc37 Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 3 Nov 2026 18:53:50 +0100 Subject: [PATCH] Enforce authenticated service admission in the router --- src/routing_layer/etcp_router.c | 62 +++++++++++++++++++++++++++- src/routing_layer/etcp_router.h | 15 ++++++- src/routing_layer/topo_group.c | 13 ++++-- src/routing_layer/topo_group.h | 3 ++ src/routing_layer/topo_node_sqlite.c | 4 +- 5 files changed, 90 insertions(+), 7 deletions(-) diff --git a/src/routing_layer/etcp_router.c b/src/routing_layer/etcp_router.c index b7086a760..b2090fc55 100644 --- a/src/routing_layer/etcp_router.c +++ b/src/routing_layer/etcp_router.c @@ -70,6 +70,15 @@ static uint32_t router_effective_max_inflight(struct ETCP_ROUTER_CONN* rconn); static void router_update_inflight_limit(struct ETCP_ROUTER_CONN* rconn); static void router_update_minrtt(struct ETCP_ROUTER_CONN* rconn); +/* Политика не изменяет router; отказ не закрывает канал из чужого стека отправки. */ +static int router_allowed(struct UTUN_INSTANCE* inst, uint64_t group, uint64_t peer, uint8_t svc) { + int (*authorize)(struct UTUN_INSTANCE*, uint64_t, uint64_t) = inst->router_bindings.authorize[svc]; + if (!authorize || authorize(inst, group, peer) == 1) return 1; + DEBUG_WARN(DEBUG_CATEGORY_ETCPROUTE, "router access denied: group=%016llx peer=%016llx svc=%u", + (unsigned long long)group, (unsigned long long)peer, svc); + return 0; +} + /* Трейлер меняется независимо от подписанного тела. Не realloc: входной буфер * может принадлежать пулу normalizer. При ошибке entry остаётся у вызывающего. */ static int router_path_append(struct ll_entry* e, uint64_t node_id, uint8_t count) { @@ -86,6 +95,14 @@ static int router_path_append(struct ll_entry* e, uint64_t node_id, uint8_t coun } static int router_source_send(struct ETCP_CONN* conn, struct ll_entry* e) { + struct UTUN_INSTANCE* inst = conn->instance; + const struct SVC_ROUTE_HDR* header = (const struct SVC_ROUTE_HDR*)e->dgram; + if (!router_allowed(inst, header->group_id, header->dst_node_id, header->svc_id)) return -1; + if (e->len == SVC_ROUTE_HDR_SIZE && (inst->router_bindings.required_crypto[header->svc_id] & ROUTER_FLAG_SIGNED)) { + uint8_t* signed_wire = NULL; size_t size = 0; + if (route_crypto_encode(inst, header->dst_node_id, e->dgram, e->len, ROUTE_CRYPTO_SIGN, &signed_wire, &size)) return -1; + queue_dgram_free(e); e->dgram = signed_wire; e->len = size; + } if (router_path_append(e, conn->instance->node_id, 0) < 0) return -1; const struct SVC_ROUTE_HDR* h = (const struct SVC_ROUTE_HDR*)e->dgram; DEBUG_TRACE(DEBUG_CATEGORY_TRAFFIC, "route TX conn=%s svc=%u flags=%02x seq=%u len=%u src=%016llx dst=%016llx group=%016llx epoch=%016llx", @@ -641,6 +658,9 @@ static void router_send_drain_cb(struct ll_queue* q, void* arg) { struct ETCP_ROUTER_CONN* rconn = arg; rconn->send_waiter_q = NULL; if (rconn->closed || rconn->no_route) return; + if (!router_allowed(rconn->inst, rconn->group_id, rconn->remote_node_id, rconn->svc_id)) { + router_close_and_notify(rconn); return; + } struct ETCP_CONN* conn = router_send_conn(rconn); if (!conn || conn->send_input_q != q) { router_send_kick(rconn); return; } int retry_pending = rconn->retrans_seq != rconn->retrans_end; @@ -892,6 +912,7 @@ static void router_retrans_timer_cb(void* arg) { // Скопировать plaintext и mode в send_q, выделить seq; кодирование выполняется при отправке после handshake. static int router_enqueue_send(struct ETCP_ROUTER_CONN* rconn, const uint8_t* payload, size_t len, int force, int mode) { + if (!router_allowed(rconn->inst, rconn->group_id, rconn->remote_node_id, rconn->svc_id)) return -1; uint8_t required = rconn->inst->router_bindings.required_crypto[rconn->svc_id]; if ((mode & required) != required) { DEBUG_ERROR(DEBUG_CATEGORY_ETCPROUTE, "router send: insufficient crypto svc=%u peer=%016llx flags=%02x required=%02x", @@ -1128,6 +1149,7 @@ static void router_idle_ack_timer_cb(void* arg) { // Доставка сервисной кодограммы в едином формате [svc_id][src][dst][rx_flags][group_id][payload]. // Для loopback (dst == self) src и dst оба = self, rx_flags = 0. static int router_deliver_loopback(struct UTUN_INSTANCE* inst, uint64_t group_id, uint8_t svc_id, struct ll_entry* entry, int mode) { + if (!router_allowed(inst, group_id, inst->node_id, svc_id)) { free_entry(entry); return -1; } size_t payload_len = entry->len - 1; if (!payload_len || payload_len > UINT16_MAX - ROUTER_SVC_HDR_SIZE || (mode & ~(ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT))) { DEBUG_ERROR(DEBUG_CATEGORY_ETCPROUTE, "router loopback: invalid payload svc=%u bytes=%zu mode=%x", svc_id, payload_len, mode); @@ -1195,6 +1217,9 @@ static int router_deliver(struct ETCP_ROUTER_CONN* rconn, struct ETCP_CONN* conn // Собрать доставку из recv_q: последовательно доставлять пакеты по возрастанию rx_seq, пока есть следующий. static void router_try_assembly(struct ETCP_ROUTER_CONN* rconn, struct ETCP_CONN* conn) { + if (!router_allowed(rconn->inst, rconn->group_id, rconn->remote_node_id, rconn->svc_id)) { + router_close_and_notify(rconn); return; + } struct ETCP_CONN local = {.instance = rconn->inst, .peer_node_id = rconn->inst->node_id}; if (!conn) conn = &local; uint32_t next = rconn->rx_seq; @@ -1477,7 +1502,7 @@ static void etcp_router_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) } entry->len = body; uint8_t required = inst->router_bindings.required_crypto[h->svc_id]; - if (body > SVC_ROUTE_HDR_SIZE && (h->flags & required) != required) { + if ((h->flags & (required & ROUTER_FLAG_SIGNED)) != (required & ROUTER_FLAG_SIGNED)) { DEBUG_WARN(DEBUG_CATEGORY_ETCPROUTE, "router receive: insufficient crypto svc=%u peer=%016llx flags=%02x required=%02x", h->svc_id, (unsigned long long)h->src_node_id, h->flags, required); free_entry(entry); return; @@ -1494,6 +1519,11 @@ static void etcp_router_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) } uint8_t control = h->flags & ~(ROUTER_FLAG_ENCRYPTED | ROUTER_FLAG_SIGNED); size_t payload_len = entry->len - SVC_ROUTE_HDR_SIZE; + if (payload_len && (h->flags & required) != required) { + DEBUG_WARN(DEBUG_CATEGORY_ETCPROUTE, "router data crypto rejected: svc=%u flags=%02x required=%02x", h->svc_id, h->flags, required); + free_entry(entry); return; + } + if (!router_allowed(inst, h->group_id, h->src_node_id, h->svc_id)) { free_entry(entry); return; } if (!h->reset_id || (payload_len && (control || h->challenge)) || (!payload_len && control != 0 && control != ROUTER_FLAG_START && control != ROUTER_FLAG_RST && control != (ROUTER_FLAG_START | ROUTER_FLAG_RST) && control != ROUTER_FLAG_CLOSE && control != ROUTER_FLAG_RECEIVE_BUSY)) { @@ -1586,6 +1616,7 @@ int etcp_router_bind(struct UTUN_INSTANCE* inst, uint8_t svc_id, etcp_recv_fn ca if (!callback) { DEBUG_DEBUG(DEBUG_CATEGORY_ETCPROUTE, "unbind svc_id=%u", svc_id); inst->router_bindings.callbacks[svc_id] = NULL; + inst->router_bindings.authorize[svc_id] = NULL; inst->router_bindings.receive_ready[svc_id] = NULL; inst->router_bindings.required_crypto[svc_id] = 0; return 0; @@ -1605,6 +1636,7 @@ int etcp_router_unbind(struct UTUN_INSTANCE* inst, uint8_t svc_id) { return -1; } inst->router_bindings.callbacks[svc_id] = NULL; + inst->router_bindings.authorize[svc_id] = NULL; inst->router_bindings.receive_ready[svc_id] = NULL; inst->router_bindings.required_crypto[svc_id] = 0; DEBUG_INFO(DEBUG_CATEGORY_ETCPROUTE, "svc_id=%u", svc_id); @@ -1612,7 +1644,8 @@ int etcp_router_unbind(struct UTUN_INSTANCE* inst, uint8_t svc_id) { } int etcp_router_require_crypto(struct UTUN_INSTANCE* inst, uint8_t svc_id, uint8_t flags) { - if (!inst || !inst->router_bindings.callbacks[svc_id] || (flags & ~(ROUTER_FLAG_ENCRYPTED | ROUTER_FLAG_SIGNED))) { + if (!inst || !inst->router_bindings.callbacks[svc_id] || (flags & ~(ROUTER_FLAG_ENCRYPTED | ROUTER_FLAG_SIGNED)) || + (inst->router_bindings.authorize[svc_id] && !(flags & ROUTER_FLAG_SIGNED))) { DEBUG_ERROR(DEBUG_CATEGORY_ETCPROUTE, "router crypto policy invalid inst=%p svc=%u flags=%02x", (void*)inst, svc_id, flags); return -1; } @@ -1621,6 +1654,30 @@ int etcp_router_require_crypto(struct UTUN_INSTANCE* inst, uint8_t svc_id, uint8 return 0; } +int etcp_router_set_authorize(struct UTUN_INSTANCE* inst, uint8_t svc_id, + int (*authorize)(struct UTUN_INSTANCE*, uint64_t, uint64_t)) { + if (!inst || !inst->router_bindings.callbacks[svc_id] || + (authorize && !(inst->router_bindings.required_crypto[svc_id] & ROUTER_FLAG_SIGNED))) { + DEBUG_ERROR(DEBUG_CATEGORY_ETCPROUTE, "router access policy requires bound signed service svc=%u", svc_id); return -1; + } + inst->router_bindings.authorize[svc_id] = authorize; + DEBUG_INFO(DEBUG_CATEGORY_ETCPROUTE, "router access policy svc=%u enabled=%d", svc_id, authorize != NULL); + return 0; +} + +void etcp_router_revalidate_group(struct UTUN_INSTANCE* inst, uint64_t group_id) { + if (!inst) return; + for (;;) { + struct ETCP_ROUTER_CONN* denied = NULL; + for (struct ll_entry* e = inst->router_conns ? inst->router_conns->head : NULL; e; e = e->next) { + struct ETCP_ROUTER_CONN* c = (struct ETCP_ROUTER_CONN*)e; + if (c->group_id == group_id && !router_allowed(inst, group_id, c->remote_node_id, c->svc_id)) { denied = c; break; } + } + if (!denied) return; + router_close_and_notify(denied); + } +} + void etcp_router_set_receive_ready(struct UTUN_INSTANCE* inst, uint8_t svc_id, int (*ready)(struct UTUN_INSTANCE*, uint64_t, uint64_t, const uint8_t*, size_t)) { if (!inst) { DEBUG_ERROR(DEBUG_CATEGORY_ETCPROUTE, "router receive readiness: NULL instance"); return; } @@ -1778,6 +1835,7 @@ struct ETCP_ROUTER_CONN* etcp_router_conn_get(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t remote_node_id, uint8_t svc_id) { if (!inst || !inst->router_conns) return NULL; + if (!router_allowed(inst, group_id, remote_node_id, svc_id)) return NULL; struct ETCP_ROUTER_CONN* rconn = router_conn_find(inst, group_id, remote_node_id, svc_id); if (rconn) return rconn; diff --git a/src/routing_layer/etcp_router.h b/src/routing_layer/etcp_router.h index 5c6b2b852..f580a6ae7 100644 --- a/src/routing_layer/etcp_router.h +++ b/src/routing_layer/etcp_router.h @@ -258,6 +258,7 @@ struct ROUTER_INFLIGHT { // Bindings для сервисов внутри etcp_router (аналогично ETCP_BINDINGS) struct ETCP_ROUTER_BINDINGS { etcp_recv_fn callbacks[SVC_ROUTE_MAX_BINDINGS]; + int (*authorize[SVC_ROUTE_MAX_BINDINGS])(struct UTUN_INSTANCE*, uint64_t, uint64_t); // Допуск group/peer; 1 — разрешено. int (*receive_ready[SVC_ROUTE_MAX_BINDINGS])(struct UTUN_INSTANCE*, uint64_t, uint64_t, const uint8_t*, size_t); uint8_t required_crypto[SVC_ROUTE_MAX_BINDINGS]; // Обязательные ENCRYPTED/SIGNED для сервисных данных; 0 — без требования. }; @@ -284,11 +285,23 @@ int etcp_router_bind(struct UTUN_INSTANCE* inst, uint8_t svc_id, etcp_recv_fn c int etcp_router_unbind(struct UTUN_INSTANCE* inst, uint8_t svc_id); // Задать обязательные ROUTER_FLAG_ENCRYPTED/SIGNED для зарегистрированного сервиса, в обе стороны сетевого обмена. -// Входящие данные проверяются до состояния/очередей/receive_ready, на конечном узле; header-only control исключён. +// Входящие данные проверяются до состояния/очередей/receive_ready; SIGNED обязателен также для header-only control. +// Control подписывается без шифрования пустого payload; для данных применяются оба заданных требования. // Доверенная синхронная доставка dst=self обходит crypto policy и даёт rx_flags=0. // Работает в uasync; unbind очищает требование. Возвращает 0 при успехе, -1 при неверном instance/сервисе/маске. int etcp_router_require_crypto(struct UTUN_INSTANCE* inst, uint8_t svc_id, uint8_t flags); +/* Допуск конечных участников сервиса; NULL снимает. Требует зарегистрированный сервис с обязательным SIGNED. + * authorize(inst,group,peer) возвращает строго 1 при разрешении; остальные значения запрещают доступ. + * Вызывается в uasync: разрешены чтения БД, запрещены callbacks, мутации и закрытие router. + * Проверяется при открытии, сетевом приёме, доставке очереди и отправке/ретрансмите, также при dst=self. + * Транзит не проверяется. Unbind очищает callback. 0/-1 с диагностикой. */ +int etcp_router_set_authorize(struct UTUN_INSTANCE* inst, uint8_t svc_id, + int (*authorize)(struct UTUN_INSTANCE*, uint64_t, uint64_t)); +/* После commit изменения допуска: закрыть запрещённые каналы группы, уведомить сервисы, очистить очереди. + * Не затрагивает общий ETCP и сервисы без authorize; callbacks могут менять список каналов. */ +void etcp_router_revalidate_group(struct UTUN_INSTANCE* inst, uint64_t group_id); + // Проверяется до доставки/ACK. 0 оставляет пакет в recv_q, сохраняя порядок; CLOSE не блокируется. // ready получает заимствованный plaintext, работает в uasync без I/O и изменения состояния/закрытия роутера. // NULL снимает ограничение; для уже приостановленных каналов вызвать resume_receive отдельно. Локальная доставка не проверяется. diff --git a/src/routing_layer/topo_group.c b/src/routing_layer/topo_group.c index f27737dd1..19adf61e6 100644 --- a/src/routing_layer/topo_group.c +++ b/src/routing_layer/topo_group.c @@ -407,13 +407,20 @@ static int topo_group_has_sender(struct TOPO_GROUP* group, struct ETCP_CONN* con /* Транспорт не даёт членства: CHAT сверяет локальную таблицу участников. */ static int topo_group_peer_allowed(struct TOPO_GROUP* group, uint64_t node_id) { if (group->group_type != TOPO_GROUP_TYPE_CHAT) return 1; - sqlite3* db = group->instance->topo_sqlite_db; - return db && group->channel_id[0] && topo_node_sqlite_member_allowed(db, group->channel_id, group->instance->node_id) && - topo_node_sqlite_member_allowed(db, group->channel_id, node_id); + return topo_group_chat_allowed(group->instance, group->group_id, node_id); +} + +int topo_group_chat_allowed(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t peer) { + struct TOPO_GROUP* group = inst && inst->topo_groups ? topo_groups_find(inst->topo_groups, group_id) : NULL; + if (!group || group->stopping || group->group_type != TOPO_GROUP_TYPE_CHAT || !group->channel_id[0] || !inst->topo_sqlite_db) + return 0; + return topo_node_sqlite_member_allowed(inst->topo_sqlite_db, group->channel_id, inst->node_id) && + (peer == inst->node_id || topo_node_sqlite_member_allowed(inst->topo_sqlite_db, group->channel_id, peer)); } void topo_group_revalidate_members(struct TOPO_GROUP* group) { if (!group || group->group_type != TOPO_GROUP_TYPE_CHAT || group->stopping) return; + etcp_router_revalidate_group(group->instance, group->group_id); /* Удаление вызывает callbacks и меняет список; каждый раз ищем следующего заново. */ for (;;) { struct TOPO_GROUP_CONN_ITEM* invalid = NULL; diff --git a/src/routing_layer/topo_group.h b/src/routing_layer/topo_group.h index 333eb9ca5..b7c7e65a1 100644 --- a/src/routing_layer/topo_group.h +++ b/src/routing_layer/topo_group.h @@ -389,6 +389,9 @@ enum topo_group_remove_reason { void topo_group_remove_conn(struct TOPO_GROUP* group, struct ETCP_CONN* conn, enum topo_group_remove_reason reason); /* После пересчёта admitted: закрыть недопущенные сессии/запросы только этой группы. Общий транспорт сохраняется. */ void topo_group_revalidate_members(struct TOPO_GROUP* group); +/* Допуск сервиса CHAT: существующая работающая группа, self и peer имеют активное admitted-членство. + * Готовность прямой сессии не требуется. Только чтение БД в uasync; 1 — разрешено, 0 — отказ/ошибка. */ +int topo_group_chat_allowed(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t peer); /** * @brief Обрабатывает пакет NODEINFO. diff --git a/src/routing_layer/topo_node_sqlite.c b/src/routing_layer/topo_node_sqlite.c index 5a0c4d1c4..436addee7 100644 --- a/src/routing_layer/topo_node_sqlite.c +++ b/src/routing_layer/topo_node_sqlite.c @@ -976,7 +976,9 @@ static int member_access(sqlite3* db, const char* channel_id, uint64_t node_id, if (rc != SQLITE_ROW && rc != SQLITE_DONE) DEBUG_ERROR(DEBUG_CATEGORY_BGP, "member admission read failed channel=%s node=%016llx rc=%d: %s", channel_id, (unsigned long long)node_id, rc, sqlite3_errmsg(db)); - sqlite3_finalize(statement); return allowed; + int final = sqlite3_finalize(statement); + if (final != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_BGP, "member admission finalize failed channel=%s rc=%d", channel_id, final); + return allowed && final == SQLITE_OK; } int topo_node_sqlite_member_allowed(sqlite3* db, const char* channel_id, uint64_t node_id) {