Browse Source

fix: NCD UAF, conn_mgr invite protocol, node_id derivation, link reuse

node_conn_direct:
- ncd_init_cb: skip UP when links_up==0, ncd_up_cb delivers when link comes up
- close/force_close: handle_count safety checks, callback clearing
- open/open_node case 2: create links on reused dead conn
- ncd_create_links: skip duplicate link on same conn (stale->etcp==conn)
- set_callback: optional callback change on live handle

conn_mgr:
- cm_ncd_callback: DOWN only delivers event (handle stays alive)
- cm_ncd_callback: TIMEOUT force_close BEFORE cm_deliver_event
- cm_deliver_event: read handle fields to locals before callback (reentrany-safe)
- INVITE_INFO_REQ/RESP: add duplicate cmd byte for direct-send protocol
- cm_handle_invite_info_resp: search invite by group_id in channel conn_mgr

Android:
- node_id computed by sc_derive_node_id_from_pubkey in C, not Kotlin
- utun_bridge_join_channel: recompute node_id, log mismatch
- invite_link_c: compute nodeId from pubkey via sc_derive_node_id_from_pubkey
- NativeLib.deriveNodeId, InviteLink.kt uses C function
topo_upd
evgeny 2 months ago
parent
commit
740ed2a93a
  1. 8
      src/routing_layer/conn_mgr.h
  2. 65
      src/routing_layer/conn_mgr_core.c
  3. 222
      src/transport_layer/node_conn_direct.c
  4. 28
      src/transport_layer/node_conn_direct.h
  5. 11
      tools/chatgui-android/app/src/main/java/com/utun/chat/data/InviteLink.kt
  6. 20
      tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt
  7. 2
      tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt
  8. 29
      tools/chatgui-android/jni_bridge/android_jni_bridge.c
  9. 15
      tools/chatgui-android/libutun_lite/instance_lite.c
  10. 4
      tools/chatgui-android/libutun_lite/invite_link_c.c

8
src/routing_layer/conn_mgr.h

@ -55,10 +55,10 @@ struct TOPO_GROUP;
*/
enum conn_mgr_event {
CONN_EVENT_UP = 0, /* обычное соединение установлено */
CONN_EVENT_DOWN = 1, /* соединение потеряно */
CONN_EVENT_TIMEOUT = 2, /* таймаут / ошибка */
CONN_EVENT_JOIN = 3, /* invite успешен (группа создана, соединение готово) */
CONN_EVENT_UP = 0, /* связь есть. handle жив, можно отправлять данные */
CONN_EVENT_DOWN = 1, /* временный обрыв. handle жив, само восстановится. НЕ закрывать */
CONN_EVENT_TIMEOUT = 2, /* подключение не удалось. handle жив, закройте сами */
CONN_EVENT_JOIN = 3, /* invite принят, группа создана. handle жив */
};
/**

65
src/routing_layer/conn_mgr_core.c

@ -28,7 +28,7 @@
void conn_mgr_router_recv_handler(struct ETCP_CONN* conn, struct ll_entry* entry);
static void cm_handle_invite_info_req(struct CONN_MGR* mgr, struct ETCP_CONN* conn, const uint8_t* data, size_t len);
static void cm_handle_invite_info_resp(struct CONN_MGR* mgr, const uint8_t* data, size_t len);
static void cm_handle_invite_info_resp(struct CONN_MGR* mgr, struct ETCP_CONN* conn, const uint8_t* data, size_t len);
/* ═══════ утилиты ═══════ */
@ -54,11 +54,20 @@ static void cm_deliver_up_cb(void* arg) {
}
/* Рассылает событие ВСЕМ handle'ам entry. Одно соединение — много слушателей:
* каждый conn_mgr_open создал свой handle, все получают UP/DOWN/TIMEOUT. */
* каждый conn_mgr_open создал свой handle, все получают UP/DOWN/TIMEOUT.
* Все поля handle'а читаются до вызова коллбэка — коллбэк может закрыть
* handle (conn_mgr_close), после вызова к handle не обращаемся. */
void cm_deliver_event(struct CONN_MGR_ENTRY* entry, enum conn_mgr_event event) {
uint64_t gid = entry->mgr->group->group_id;
struct CONN_MGR_HANDLE* h = entry->handles;
while (h) { struct CONN_MGR_HANDLE* next = h->next; if (h->cb) h->cb(h, h->node_id, gid, event, h->cb_arg); h = next; }
while (h) {
struct CONN_MGR_HANDLE* next = h->next;
uint64_t node_id = h->node_id;
conn_mgr_cb_t cb = h->cb;
void* cb_arg = h->cb_arg;
if (cb) cb(h, node_id, gid, event, cb_arg);
h = next;
}
}
/* ═══════ NAT / адресные утилиты ═══════ */
@ -184,7 +193,7 @@ void cm_entry_cleanup(struct CONN_MGR_ENTRY* entry) {
} else pp = &(*pp)->next;
}
}
if (entry->ncd_handle) { node_conn_direct_close(entry->ncd_handle); entry->ncd_handle = NULL; }
if (entry->ncd_handle) { node_conn_direct_force_close(entry->ncd_handle); entry->ncd_handle = NULL; }
entry->handles = NULL;
cm_clear_nodeinfo(entry->mgr, entry->node_id);
entry->state = CONN_MGR_STATE_DISCONNECTED; entry->conn_type = CONN_TYPE_NONE;
@ -228,7 +237,7 @@ void cm_cleanup_db_node(struct CONN_MGR_ENTRY* entry) {
/* ═══════ NCD коллбэки ═══════ */
/* NCD события → conn_mgr для DIRECT/REVERSE фаз:
/* NCD (node conn direct) события → conn_mgr для DIRECT/REVERSE фаз:
* UP — соединение поднялось, помечаем CONNECTED, доставляем UP всем handle'ам.
* TIMEOUT — NCD таймаут истёк, переходим к REVERSE или INDIRECT, либо фейлим db_node.
* DOWN — соединение упало, доставляем DOWN. */
@ -248,9 +257,10 @@ void cm_ncd_callback(struct NODE_CONN_DIRECT* ncd_h, enum ncd_event ncd_ev, void
case NCD_EVENT_TIMEOUT:
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "link: ETCP connect TIMEOUT to 0x%016llx — no response, trying fallback",
(unsigned long long)entry->node_id);
entry->ncd_handle = NULL;
if (entry->db_loaded) {
entry->main_connect_state = CM_TRY_FAILED;
{ struct NODE_CONN_DIRECT* ncd = entry->ncd_handle; entry->ncd_handle = NULL;
if (ncd) node_conn_direct_force_close(ncd); }
cm_cleanup_db_node(entry); cm_deliver_event(entry, CONN_EVENT_TIMEOUT);
return;
}
@ -258,17 +268,20 @@ void cm_ncd_callback(struct NODE_CONN_DIRECT* ncd_h, enum ncd_event ncd_ev, void
{ struct TOPO_GROUP* g = entry->mgr->group;
struct TOPO_GROUP_NODE* t = topo_node_find_by_id(g, entry->node_id);
if (entry->local_scan_state != CM_TRY_OK) {
{ struct NODE_CONN_DIRECT* ncd = entry->ncd_handle; entry->ncd_handle = NULL;
if (ncd) node_conn_direct_force_close(ncd); }
if (cm_has_direct_ip(entry->mgr, g->local_node) && !(t && cm_has_direct_ip(entry->mgr, t)))
{ cm_start_phase_reverse(entry); return; }
cm_start_phase_indirect(entry); return;
}
}
if (entry->local_scan_state == CM_TRY_FAILED) cm_deliver_event(entry, CONN_EVENT_TIMEOUT);
{ struct NODE_CONN_DIRECT* ncd = entry->ncd_handle; entry->ncd_handle = NULL;
if (ncd) node_conn_direct_force_close(ncd); }
break;
case NCD_EVENT_DOWN:
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "link: ETCP link DROPPED to 0x%016llx",
(unsigned long long)entry->node_id);
entry->ncd_handle = NULL;
cm_deliver_event(entry, CONN_EVENT_DOWN);
break;
}
@ -284,12 +297,13 @@ void cm_invite_ncd_callback(struct NODE_CONN_DIRECT* ncd_h, enum ncd_event ncd_e
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "invite: ETCP handshake OK with 0x%016llx — sending membership check",
(unsigned long long)inv->node_id);
inv->state = CM_INVITE_WAIT_INFO;
{ struct CM_INVITE_REQ req; memset(&req, 0, sizeof(req));
req.cmd = ETCP_RT_ID_CONN_MGR; req.subcmd = CM_SUBCMD_INVITE_INFO_REQ; req.group_id = inv->mgr->group->group_id;
{ uint8_t buf[11]; /* dgram[0]=cmd(dispatch), dgram[1]=cmd(handler skip), dgram[2]=subcmd, dgram[3..10]=group_id */
buf[0] = ETCP_RT_ID_CONN_MGR; buf[1] = ETCP_RT_ID_CONN_MGR; buf[2] = CM_SUBCMD_INVITE_INFO_REQ;
memcpy(buf + 3, &inv->mgr->group->group_id, 8);
struct ll_entry* qe = queue_entry_new(0);
if (!qe) { cm_invite_fail(inv); return; }
qe->dgram = u_malloc(sizeof(req)); if (!qe->dgram) { queue_entry_free(qe); cm_invite_fail(inv); return; }
memcpy(qe->dgram, &req, sizeof(req)); qe->len = sizeof(req);
qe->dgram = u_malloc(sizeof(buf)); if (!qe->dgram) { queue_entry_free(qe); cm_invite_fail(inv); return; }
memcpy(qe->dgram, buf, sizeof(buf)); qe->len = sizeof(buf);
etcp_send(node_conn_direct_get_conn(ncd_h), qe);
}
break;
@ -711,7 +725,7 @@ void conn_mgr_router_recv_handler(struct ETCP_CONN* conn, struct ll_entry* entry
case CM_SUBCMD_INTERM_SELECTED: if (mgr) cm_handle_interm_selected(mgr, d, len); break;
case CM_SUBCMD_DISCONNECT: if (len >= CM_DISCONNECT_SIZE && mgr) cm_handle_disconnect(mgr, ((struct CM_DISCONNECT*)d)->node_id); break;
case CM_SUBCMD_INVITE_INFO_REQ: if (mgr && len >= sizeof(struct CM_INVITE_REQ)) cm_handle_invite_info_req(mgr, conn, d, len); break;
case CM_SUBCMD_INVITE_INFO_RESP: if (mgr) cm_handle_invite_info_resp(mgr, d, len); break;
case CM_SUBCMD_INVITE_INFO_RESP: if (mgr) cm_handle_invite_info_resp(mgr, conn, d, len); break;
}
}
@ -730,17 +744,29 @@ void cm_handle_invite_info_req(struct CONN_MGR* mgr, struct ETCP_CONN* conn, con
memcpy(resp->ed25519_pubkey, sg->ed25519_public_key, SC_PUBKEY_SIZE); resp->node_name_len=(uint8_t)nl;
if (nl) memcpy(resp->node_name, nm, nl);
struct ll_entry* qe = queue_entry_new(0);
if (qe) { qe->dgram=(uint8_t*)resp; qe->len=(uint16_t)rs; etcp_send(conn, qe); } else u_free(resp);
if (qe) {
size_t new_len = rs + 1; /* +1 for duplicate cmd: dgram[0]=dispatch, dgram[1]=handler-skip, dgram[2]=subcmd, dgram[3..]=payload */
uint8_t* buf = u_malloc(new_len);
if (buf) { buf[0]=ETCP_RT_ID_CONN_MGR; buf[1]=ETCP_RT_ID_CONN_MGR; buf[2]=CM_SUBCMD_INVITE_INFO_RESP;
memcpy(buf+3, (uint8_t*)resp+2, rs-2); qe->dgram=buf; qe->len=(uint16_t)new_len; etcp_send(conn, qe); u_free(resp); }
else { queue_entry_free(qe); u_free(resp); }
} else u_free(resp);
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "invite: membership check RECEIVED from 0x%016llx — responding is_member=yes name=\"%.*s\"",
(unsigned long long)conn->peer_node_id, (int)nl, nm ? nm : "");
}
void cm_handle_invite_info_resp(struct CONN_MGR* mgr, const uint8_t* data, size_t len) {
void cm_handle_invite_info_resp(struct CONN_MGR* mgr, struct ETCP_CONN* conn, const uint8_t* data, size_t len) {
if (len < sizeof(struct CM_INVITE_RESP)) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: INVITE_INFO_RESP too short (%zu)", len); return; }
struct CM_INVITE_RESP* resp = (struct CM_INVITE_RESP*)data;
if (len < sizeof(struct CM_INVITE_RESP) + (size_t)resp->node_name_len) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: INVITE_INFO_RESP truncated"); return; }
struct cm_invite_pending* inv = NULL;
for (struct cm_invite_pending* p = mgr->invite_list; p; p = p->next) if (p->state == CM_INVITE_WAIT_INFO) { inv = p; break; }
{ struct TOPO_GROUP* grp = topo_groups_find(mgr->instance->topo_groups, resp->group_id);
if (grp && grp->conn_mgr) {
for (struct cm_invite_pending* p = grp->conn_mgr->invite_list; p; p = p->next)
if (p->state == CM_INVITE_WAIT_INFO && p->node_id == conn->peer_node_id) { inv = p; break; }
}
}
if (!inv) { for (struct cm_invite_pending* p = mgr->invite_list; p; p = p->next) if (p->state == CM_INVITE_WAIT_INFO) { inv = p; break; } }
if (!inv) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: INVITE_INFO_RESP — no pending invite in WAIT_INFO state"); return; }
if (!resp->is_member) {
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: membership DENIED by 0x%016llx — not in channel", (unsigned long long)inv->node_id);
@ -815,11 +841,12 @@ static void cm_tcp_ready_cb(struct stcp_link* link, void* arg) {
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "invite: TCP handshake OK with 0x%016llx — sending membership check",
(unsigned long long)inv->node_id);
inv->state = CM_INVITE_WAIT_INFO;
{ struct CM_INVITE_REQ req; memset(&req,0,sizeof(req));
req.cmd=ETCP_RT_ID_CONN_MGR; req.subcmd=CM_SUBCMD_INVITE_INFO_REQ; req.group_id=inv->mgr->group->group_id;
{ uint8_t buf[11]; /* dgram[0]=cmd(dispatch), dgram[1]=cmd(handler skip), dgram[2]=subcmd, dgram[3..10]=group_id */
buf[0] = ETCP_RT_ID_CONN_MGR; buf[1] = ETCP_RT_ID_CONN_MGR; buf[2] = CM_SUBCMD_INVITE_INFO_REQ;
memcpy(buf + 3, &inv->mgr->group->group_id, 8);
struct ll_entry* qe2 = queue_entry_new(0); if (!qe2) { cm_invite_fail(inv); return; }
qe2->dgram=u_malloc(sizeof(req)); if (!qe2->dgram) { queue_entry_free(qe2); cm_invite_fail(inv); return; }
memcpy(qe2->dgram,&req,sizeof(req)); qe2->len=sizeof(req); etcp_send(etcp, qe2);
qe2->dgram=u_malloc(sizeof(buf)); if (!qe2->dgram) { queue_entry_free(qe2); cm_invite_fail(inv); return; }
memcpy(qe2->dgram,buf,sizeof(buf)); qe2->len=sizeof(buf); etcp_send(etcp, qe2);
}
}

222
src/transport_layer/node_conn_direct.c

@ -66,6 +66,11 @@ static void ncd_deferred_close(void* arg);
static void ncd_deferred_close_conn(void* arg);
/* ═══════════ реестр ═══════════ */
/*
* Реестр ncd_entry по node_id — собственный связный список (не inst->connections).
* ncd_entry хранит состояние, которого нет в ETCP_CONN: handles, таймеры, fin_wait.
* Нужен чтобы при повторном open не создавать дублирующий conn для того же node_id.
*/
static struct ncd_entry* ncd_registry_find(struct UTUN_INSTANCE* inst, uint64_t node_id) {
struct ncd_entry* e = (struct ncd_entry*)inst->ncd_registry;
@ -81,6 +86,12 @@ static void ncd_registry_remove(struct UTUN_INSTANCE* inst, struct ncd_entry* en
}
/* ═══════════ поиск узла ═══════════ */
/*
* Загружает информацию об узле (адреса, pubkey) для создания линков.
* Сначала ищет в памяти (node_registry — туда попадают узлы из BGP/topo),
* если нет в памяти — подгружает из SQLite и помещает в реестр.
* Возвращает владеющую ссылку (ref++), вызывающий обязан сделать topo_node_registry_unref.
*/
static struct TOPO_NODE* ncd_lookup_node(struct UTUN_INSTANCE* inst, uint64_t node_id) {
if (inst->topo_groups && inst->topo_groups->node_registry) {
@ -99,6 +110,20 @@ static struct TOPO_NODE* ncd_lookup_node(struct UTUN_INSTANCE* inst, uint64_t no
}
/* ═══════════ создание линков (round‑robin, все сокеты кроме PRIVATE) ═══════════ */
/*
* Создаёт по одному ETCP_LINK на каждый адрес пира (IPv4 и IPv6).
*
* Все адреса пира распределяются round-robin по локальным сокетам.
* Если specific_sock задан — использует только его. PRIVATE-сокеты пропускаются.
*
* Особые случаи:
* - Нулевые адреса и нулевые порты пропускаются
* - Не-UDP протоколы пропускаются
* - IPv6 link-local: автоматически выставляется scope_id по netif_index сокета
* - Stale-линки: если на том же addr:port висит старый conn с другим pubkey
* и старый conn ещё не поднялся — stale-линк вытесняется (узел пересоздался
* с новым ключом, старый conn больше не нужен)
*/
static int ncd_create_links(struct ncd_entry* entry, struct TOPO_NODE* ni,
struct ETCP_SOCKET* specific_sock) {
@ -130,6 +155,7 @@ static int ncd_create_links(struct ncd_entry* entry, struct TOPO_NODE* ni,
struct ETCP_SOCKET* use_sock = socks[rr++ % sock_count];
{
struct ETCP_LINK* stale = etcp_link_find_by_addr(use_sock, &sa);
if (stale && stale->etcp == conn) continue;
if (stale && stale->etcp != conn
&& memcmp(conn->crypto_ctx.peer_public_key, stale->etcp->crypto_ctx.peer_public_key, SC_PUBKEY_SIZE))
{
@ -178,6 +204,7 @@ static int ncd_create_links(struct ncd_entry* entry, struct TOPO_NODE* ni,
struct sockaddr_storage sa; memcpy(&sa, &sin6, sizeof(sin6));
{
struct ETCP_LINK* stale = etcp_link_find_by_addr(use_sock, &sa);
if (stale && stale->etcp == conn) continue;
if (stale && stale->etcp != conn
&& memcmp(conn->crypto_ctx.peer_public_key, stale->etcp->crypto_ctx.peer_public_key, SC_PUBKEY_SIZE))
{
@ -203,6 +230,14 @@ static int ncd_create_links(struct ncd_entry* entry, struct TOPO_NODE* ni,
}
/* ═══════════ единая диспетчеризация событий ═══════════ */
/*
* Центральный диспетчер: принимает событие (UP/DOWN/TIMEOUT), проверяет что оно
* действительно меняет состояние (повторный UP когда уже up — игнорируется),
* обновляет entry->up/timed_out и рассылает callback всем handle'ам.
*
* Именно через эту функцию все потребители узнают об изменении состояния соединения.
* Один вызов — один проход по всем handle'ам.
*/
static void ncd_event_dispatch(struct ncd_entry* entry, enum ncd_event event) {
if (!entry) return;
@ -230,17 +265,32 @@ static void ncd_event_dispatch(struct ncd_entry* entry, enum ncd_event event) {
}
/* ═══════════ ETCP коллбэки (прокидывают в ncd_event_dispatch) ═══════════ */
/*
* ETCP сообщил что соединение работает: INIT — handshake завершён,
* UP — линки восстановились после DOWN. Транслируем в NCD_EVENT_UP.
* Не транслируем если уже сработал таймаут подключения (timed_out)
* или conn в состоянии fin_wait (ждём закрытия).
* Если INIT пришёл а линков нет — UP не доставляем: ncd_up_cb сам
* доставит когда линки поднимутся. Так гарантируем что пользователь
* получает UP только когда conn реально готов отправлять данные.
*/
static void ncd_init_cb(struct ETCP_CONN* conn, int event, void* arg) { (void)event;
struct ncd_entry* entry = (struct ncd_entry*)arg;
if (!entry || entry->timed_out || conn->fin_wait) return;
ncd_event_dispatch(entry, NCD_EVENT_UP);
if (conn->links_up > 0) ncd_event_dispatch(entry, NCD_EVENT_UP);
}
static void ncd_up_cb(struct ETCP_CONN* conn, int event, void* arg) { (void)event;
struct ncd_entry* entry = (struct ncd_entry*)arg;
if (!entry || conn->fin_wait) return;
ncd_event_dispatch(entry, NCD_EVENT_UP);
}
/*
* Отложенное (uasync_call_soon) уничтожение ncd_entry вместе с conn.
* Нельзя вызывать напрямую из ETCP-коллбэка: conn может использоваться
* после возврата из коллбэка. Поэтому очистка откладывается на следующий цикл событий.
* Снимает все ETCP-коллбэки, удаляет из реестра, закрывает conn, освобождает память.
*/
static void ncd_deferred_close(void* arg) {
struct ncd_entry* entry = (struct ncd_entry*)arg;
struct ETCP_CONN* conn = entry->conn;
@ -253,10 +303,20 @@ static void ncd_deferred_close(void* arg) {
}
u_free(entry);
}
/*
* Отложенное закрытие conn без ncd_entry.
* Используется когда пришёл CLOSE от пира, а ncd_entry для этого пира уже нет
* (закрыли раньше — например другой модуль уже удалил все handle'ы).
*/
static void ncd_deferred_close_conn(void* arg) {
struct ETCP_CONN* conn = (struct ETCP_CONN*)arg;
if (conn) etcp_connection_close(conn);
}
/*
* ETCP сообщил что все линки упали.
* Если conn в fin_wait и handle'ов нет — peer подтвердил закрытие,
* делаем немедленную очистку. Иначе транслируем NCD_EVENT_DOWN потребителям.
*/
static void ncd_down_cb(struct ETCP_CONN* conn, int event, void* arg) { (void)event;
struct ncd_entry* entry = (struct ncd_entry*)arg;
if (!entry) return;
@ -271,6 +331,11 @@ static void ncd_down_cb(struct ETCP_CONN* conn, int event, void* arg) { (void)ev
}
/* ═══════════ таймер подключения ═══════════ */
/*
* Таймер первого подключения истёк — соединение не установилось за etcp_connect_timeout_tb.
* Транслирует NCD_EVENT_TIMEOUT всем handle'ам. После этого init/up коллбэки
* больше не транслируются (entry->timed_out = 1).
*/
static void ncd_connect_timeout_cb(void* arg) {
struct ncd_entry* entry = (struct ncd_entry*)arg;
@ -282,6 +347,12 @@ static void ncd_connect_timeout_cb(void* arg) {
}
/* ═══════════ асинхронная доставка UP ═══════════ */
/*
* Доставляет NCD_EVENT_UP одному конкретному handle'у (не всем).
* Используется когда handle добавляется к уже работающему conn: доставка отложенная
* (uasync_call_soon), чтобы вызывающий успел сохранить указатель handle
* до вызова callback'а. Синхронная доставка привела бы к гонке.
*/
static void ncd_deliver_up_cb(void* arg) {
struct NODE_CONN_DIRECT* h = (struct NODE_CONN_DIRECT*)arg;
@ -289,13 +360,29 @@ static void ncd_deliver_up_cb(void* arg) {
}
/* ═══════════ FIN_WAIT ═══════════ */
/*
* fin_wait — состояние ожидания подтверждения закрытия от пира.
* Вызывающая сторона: мы отправили CLOSE и ждём ответа (KEEP_ALIVE или DOWN).
* Максимальное время ожидания — NCD_FIN_WAIT_TIMEOUT_TB (5 сек), после чего
* соединение форсированно закрывается.
*/
/*
* ETCP отменяет наш fin_wait — peer переподключился и прислал новый INIT
* (или другая причина отмены на уровне ETCP). Отменяем локальный таймер,
* соединение продолжает работать.
*/
static void ncd_fin_wait_cancelled(struct ETCP_CONN* conn, void* arg) {
struct ncd_entry* entry = (struct ncd_entry*)arg;
DEBUG_INFO(DEBUG_CATEGORY_NCD, "[ncd] fin_wait cancelled node=0x%016llx", (unsigned long long)entry->node_id);
if (entry->fin_wait_timer) { uasync_cancel_timeout(entry->ua, entry->fin_wait_timer); entry->fin_wait_timer = NULL; }
}
/*
* Таймаут fin_wait: peer не ответил на CLOSE за NCD_FIN_WAIT_TIMEOUT_TB (5 сек).
* Форсированно закрываем conn и освобождаем entry — соединение разорвано
* без подтверждения от пира.
*/
static void ncd_fin_wait_timeout_cb(void* arg) {
struct ncd_entry* entry = (struct ncd_entry*)arg;
if (!entry || !entry->conn) return;
@ -312,6 +399,17 @@ static void ncd_fin_wait_timeout_cb(void* arg) {
}
/* ═══════════ приём CLOSE / KEEP_ALIVE ═══════════ */
/*
* Обрабатывает управляющие сообщения от пира по протоколу graceful shutdown.
*
* CLOSE (пир хочет закрыть conn — у него закончились handle'ы):
* - Если у нас ещё есть handle'ы — отказываем: шлём KEEP_ALIVE, conn живёт.
* - Если handle'ов нет — соглашаемся, закрываем conn.
*
* KEEP_ALIVE (пир просит не закрывать conn — у него ещё есть handle'ы):
* - Если мы в fin_wait — отменяем его, conn продолжает работать.
* - Если у нас handle'ов нет — перезапускаем fin_wait (ждём ещё 5 сек).
*/
static void ncd_recv_control_handler(struct ETCP_CONN* conn, struct ll_entry* e) {
if (!conn || !e || e->len < NCD_CONTROL_MSG_SIZE) {
@ -363,6 +461,11 @@ static void ncd_recv_control_handler(struct ETCP_CONN* conn, struct ll_entry* e)
}
/* ═══════════ инициализация глобального обработчика ═══════════ */
/*
* Однократная привязка обработчика NCD control-сообщений к ETCP (etcp_bind).
* Вызывается при первом node_conn_direct_open, повторные вызовы безвредны
* (защита через inst->ncd_control_bound).
*/
static void ncd_init_control_binding(struct UTUN_INSTANCE* inst) {
if (inst->ncd_control_bound) return;
@ -373,6 +476,20 @@ static void ncd_init_control_binding(struct UTUN_INSTANCE* inst) {
/* ═══════════ API ═══════════ */
/*
* Открыть handle для связи с удалённым узлом.
*
* Три сценария (прозрачно для вызывающего):
* 1. Узел уже в NCD-реестре — другой модуль уже открыл соединение.
* Добавляем ещё один handle. Если conn работает — сразу шлём UP.
* 2. ETCP-соединение существует (входящее / etcp_connect), но NCD о нём не знает.
* Оборачиваем в ncd_entry, подписываемся на события.
* 3. Ничего нет — загружаем адреса/pupkey узла, создаём новый ETCP_CONN,
* инициализируем шифрование, создаём линки, запускаем таймер подключения.
*
* Возвращает NCD_NEW (новое), NCD_REUSED (переиспользовано) или NCD_ERR.
* specific_sock=NULL — авто-подбор всех локальных сокетов.
*/
int node_conn_direct_open(struct UTUN_INSTANCE* inst, uint64_t node_id,
ncd_callback cb, void* cb_arg,
struct NODE_CONN_DIRECT** out_handle,
@ -435,6 +552,9 @@ int node_conn_direct_open(struct UTUN_INSTANCE* inst, uint64_t node_id,
uasync_call_soon(inst->ua, h, ncd_deliver_up_cb);
DEBUG_INFO(DEBUG_CATEGORY_NCD, "[ncd] open REUSED new-entry (ready) node=0x%016llx conn=%p", (unsigned long long)node_id, (void*)conn);
} else {
{ struct TOPO_NODE* ni = ncd_lookup_node(inst, node_id);
if (ni) { ncd_create_links(entry, ni, specific_sock); topo_node_registry_unref(inst->topo_groups, ni->node_id); }
}
entry->connect_timer = uasync_set_timeout(inst->ua, (int)inst->etcp_connect_timeout_tb, entry, ncd_connect_timeout_cb, "ncd_connect");
DEBUG_INFO(DEBUG_CATEGORY_NCD, "[ncd] open REUSED new-entry (pending) node=0x%016llx conn=%p", (unsigned long long)node_id, (void*)conn);
}
@ -514,6 +634,12 @@ int node_conn_direct_open(struct UTUN_INSTANCE* inst, uint64_t node_id,
return NCD_NEW;
}
/*
* То же что open, но TOPO_NODE (адреса + pubkey) уже загружен вызывающим.
* Экономит поиск узла (ncd_lookup_node) — полезно когда адреса/pupkey
* известны заранее (например из BGP-анонса). ni не владеем — можно
* передать временную структуру, копия не делается.
*/
int node_conn_direct_open_node(struct UTUN_INSTANCE* inst, uint64_t node_id,
ncd_callback cb, void* cb_arg,
struct NODE_CONN_DIRECT** out_handle,
@ -575,6 +701,7 @@ int node_conn_direct_open_node(struct UTUN_INSTANCE* inst, uint64_t node_id,
uasync_call_soon(inst->ua, h, ncd_deliver_up_cb);
DEBUG_INFO(DEBUG_CATEGORY_NCD, "[ncd] open_node REUSED new-entry (ready) node=0x%016llx conn=%p", (unsigned long long)node_id, (void*)conn);
} else {
ncd_create_links(entry, ni, specific_sock);
entry->connect_timer = uasync_set_timeout(inst->ua, (int)inst->etcp_connect_timeout_tb, entry, ncd_connect_timeout_cb, "ncd_connect_node");
DEBUG_INFO(DEBUG_CATEGORY_NCD, "[ncd] open_node REUSED new-entry (pending) node=0x%016llx conn=%p", (unsigned long long)node_id, (void*)conn);
}
@ -645,6 +772,18 @@ int node_conn_direct_open_node(struct UTUN_INSTANCE* inst, uint64_t node_id,
}
}
/*
* Закрыть handle (graceful shutdown).
*
* Если остались другие handle'ы — просто удаляется из списка, conn продолжает работу.
*
* Если это был последний handle (handle_count стал 0):
* - Запускается протокол graceful shutdown: пиру отправляется CLOSE, conn
* переводится в fin_wait, ставится таймер на 5 сек.
* - Если пир ответит KEEP_ALIVE (у него ещё есть handle'ы) — conn останется жив,
* просто без наших handle'ов.
* - Если пир не ответит — conn закрывается форсированно по таймауту.
*/
void node_conn_direct_close(struct NODE_CONN_DIRECT* h) {
if (!h) return;
struct ncd_entry* entry = h->entry;
@ -658,9 +797,21 @@ void node_conn_direct_close(struct NODE_CONN_DIRECT* h) {
while (*pp) { if (*pp == h) { *pp = h->next; break; } pp = &(*pp)->next; }
entry->handle_count--;
if (entry->handle_count < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_NCD, "[ncd] FATAL: handle_count=%d < 0 node=0x%016llx",
entry->handle_count, (unsigned long long)node_id);
h->entry = NULL; u_free(h); return;
}
DEBUG_INFO(DEBUG_CATEGORY_NCD, "[ncd] close node=0x%016llx remaining=%d", (unsigned long long)node_id, entry->handle_count);
if (entry->handle_count <= 0) {
if (entry->handle_count == 0) {
if (entry->handles) {
DEBUG_ERROR(DEBUG_CATEGORY_NCD, "[ncd] FATAL: handle_count=0 but handles non-empty node=0x%016llx",
(unsigned long long)node_id);
entry->handles = NULL;
}
h->cb = NULL; h->cb_arg = NULL;
if (entry->connect_timer) { uasync_cancel_timeout(entry->ua, entry->connect_timer); entry->connect_timer = NULL; }
if (conn && conn->state != 2) {
/* устанавливаем fin_wait, отправляем CLOSE, ставим таймер */
@ -694,6 +845,73 @@ void node_conn_direct_close(struct NODE_CONN_DIRECT* h) {
u_free(h);
}
/*
* Немедленное жёсткое закрытие handle. Никакого CLOSE/KEEP_ALIVE — сразу
* снимает ETCP-коллбэки и (если последний handle) закрывает conn.
*
* Используется при разрушении CM entry чтобы гарантировать что коллбэки
* не вызовутся в уже освобождённую память. После force_close никакие
* NCD-события не будут доставлены.
*/
void node_conn_direct_force_close(struct NODE_CONN_DIRECT* h) {
if (!h) return;
struct ncd_entry* entry = h->entry;
if (!entry) { u_free(h); return; }
uint64_t node_id = entry->node_id;
struct ETCP_CONN* conn = entry->conn;
struct NODE_CONN_DIRECT** pp = &entry->handles;
while (*pp) { if (*pp == h) { *pp = h->next; break; } pp = &(*pp)->next; }
entry->handle_count--;
if (entry->handle_count < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_NCD, "[ncd] FATAL: force_close handle_count=%d < 0 node=0x%016llx",
entry->handle_count, (unsigned long long)node_id);
h->entry = NULL; u_free(h); return;
}
DEBUG_INFO(DEBUG_CATEGORY_NCD, "[ncd] force_close node=0x%016llx remaining=%d",
(unsigned long long)node_id, entry->handle_count);
if (entry->handle_count == 0) {
if (entry->handles) {
DEBUG_ERROR(DEBUG_CATEGORY_NCD, "[ncd] FATAL: force_close handle_count=0 but handles non-empty node=0x%016llx",
(unsigned long long)node_id);
entry->handles = NULL;
}
h->cb = NULL; h->cb_arg = NULL;
if (entry->connect_timer) { uasync_cancel_timeout(entry->ua, entry->connect_timer); entry->connect_timer = NULL; }
if (entry->fin_wait_timer) { uasync_cancel_timeout(entry->ua, entry->fin_wait_timer); entry->fin_wait_timer = NULL; }
if (conn) {
conn->fin_wait = 0; conn->fin_wait_clear_cb = NULL; conn->fin_wait_clear_arg = NULL;
etcp_conn_remove_cbk(conn, ncd_init_cb, entry);
etcp_conn_remove_cbk(conn, ncd_up_cb, entry);
etcp_conn_remove_cbk(conn, ncd_down_cb, entry);
if (conn->state != 2) uasync_call_soon(entry->ua, conn, ncd_deferred_close_conn);
ncd_registry_remove(conn->instance, entry);
}
u_free(entry);
}
h->entry = NULL;
u_free(h);
}
/*
* Сменить или сбросить (cb=NULL) callback на уже открытом handle.
* Не влияет на refcounting и состояние conn.
*/
void node_conn_direct_set_callback(struct NODE_CONN_DIRECT* h, ncd_callback cb, void* cb_arg) {
if (!h || !h->entry) return;
h->cb = cb;
h->cb_arg = cb_arg;
}
/*
* Прямой доступ к ETCP_CONN из handle.
* Нужен для отправки данных (etcp_send), проверки статуса, и т.д.
*/
struct ETCP_CONN* node_conn_direct_get_conn(struct NODE_CONN_DIRECT* h) {
if (!h || !h->entry) return NULL;
return h->entry->conn;

28
src/transport_layer/node_conn_direct.h

@ -1,6 +1,23 @@
#ifndef NODE_CONN_DIRECT_H
#define NODE_CONN_DIRECT_H
/*
* node_conn_direct.h — handle-based прямое подключение к удалённому узлу (node_id)
*
* Модуль управляет ETCP-соединениями через непрозрачные handle'ы (NODE_CONN_DIRECT).
* Несколько handle'ов могут разделять одно ETCP_CONN — закрытие происходит только
* когда все handle'ы закрыты (refcounting). При закрытии последнего handle
* используется graceful shutdown: CLOSE/KEEP_ALIVE протокол (fin_wait 5 сек).
*
* Кто использует:
* - conn_mgr (Connection Manager) — создаёт handle'ы для DIR/REV/IND-соединений
* - chat_core / topo_group — связь с узлами канала/группы
* - любой модуль, кому нужно надёжное ETCP-соединение с конкретным node_id
*
* События (UP/DOWN/TIMEOUT) доставляются через единый callback асинхронно
* (uasync_call_soon), что исключает рекурсию и проблемы с порядком инициализации.
*/
#include <stdint.h>
struct UTUN_INSTANCE;
@ -30,6 +47,7 @@ typedef void (*ncd_callback)(struct NODE_CONN_DIRECT* h, enum ncd_event event, v
* Node info ищется через node_registry, fallback — SQLite.
* cb вызывается при изменении статуса / таймауте.
* Если conn уже готов — cb(NCD_EVENT_UP) через uasync_call_soon (не синхронно).
* Пока не придёт событие UP - отправляения могут теряться. Поэтому перед первой отправкой всегда ждём UP.
*/
int node_conn_direct_open(struct UTUN_INSTANCE* inst, uint64_t node_id,
ncd_callback cb, void* cb_arg,
@ -48,6 +66,16 @@ int node_conn_direct_open_node(struct UTUN_INSTANCE* inst, uint64_t node_id,
void node_conn_direct_close(struct NODE_CONN_DIRECT* h);
/* Force close без fin_wait: немедленно удаляет ETCP-коллбэки и, если это
* последний handle, закрывает соединение и освобождает ncd_entry.
* Используется при уничтожении CM entry — гарантирует, что после вызова
* никакие NCD-коллбэки не доставят события в освобождённую память. */
void node_conn_direct_force_close(struct NODE_CONN_DIRECT* h);
/* Сменить или сбросить (cb=NULL) callback на уже открытом handle.
* Не влияет на refcounting и состояние conn. */
void node_conn_direct_set_callback(struct NODE_CONN_DIRECT* h, ncd_callback cb, void* cb_arg);
struct ETCP_CONN* node_conn_direct_get_conn(struct NODE_CONN_DIRECT* h);
/* ─── Протокол CLOSE / KEEP_ALIVE (ETCP_RT_ID_NCD_CONTROL = 0x12) ─── */

11
tools/chatgui-android/app/src/main/java/com/utun/chat/data/InviteLink.kt

@ -62,15 +62,6 @@ data class InviteData(
object InviteLink {
private const val PREFIX = "utun://"
fun deriveNodeId(pubkey: ByteArray): Long {
val md = MessageDigest.getInstance("SHA-256")
val hash = md.digest(pubkey)
var id = 0L
for (i in 0 until 8)
id = (id shl 8) or (hash[i].toLong() and 0xFF)
return id and 0x7FFFFFFFFFFFFFFFL
}
fun decode(link: String): InviteData? {
LogManager.addLog("DEBUG", "INVITE", "decode: ${link.take(80)}")
if (!link.startsWith(PREFIX)) {
@ -140,7 +131,7 @@ object InviteLink {
}
if (pubkey == null || pubkey.size != INVITE_PUBKEY_SIZE) return null
val nodeId = deriveNodeId(pubkey)
val nodeId = NativeLib.deriveNodeId(pubkey)
val pkHex = pubkey.take(8).joinToString("") { "%02x".format(it) }
LogManager.addLog("INFO", "INVITE", "decode: ch=0x%016x node=0x%016x pk=%s... addrs=%d".format(
chId, nodeId, pkHex, addrs.size))

20
tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt

@ -7,7 +7,6 @@ interface ChatEventCallback {
}
object NativeLib {
private var running = false
init {
System.loadLibrary("utun")
@ -24,16 +23,13 @@ object NativeLib {
/** Start full uTun instance from INI config text */
fun start(configText: String): Boolean {
if (running) return true
val ok = nativeStart(configText)
if (ok) running = true
return ok
if (isReady()) return true
return nativeStart(configText)
}
fun stop() {
if (!running) return
if (!isReady()) return
nativeStop()
running = false
}
fun setEventCallback(cb: ChatEventCallback) {
@ -42,16 +38,13 @@ object NativeLib {
})
}
fun destroy() {
nativeDestroy()
running = false
}
fun destroy() { nativeDestroy() }
fun restart(dbPath: String, controlPort: Int, configText: String): Boolean {
return nativeRestart(configText)
}
fun isReady(): Boolean = running
fun isReady(): Boolean = nativeIsRunning()
fun getMyNodeId(): Long = nativeGetMyNodeId()
@ -66,6 +59,7 @@ object NativeLib {
nativeJoinChannel(channelId, nodeId, pubkey, addrs, addrCount)
LogManager.addLog("INFO", "JNI", "joinChannel: native returned")
}
fun deriveNodeId(pubkey: ByteArray): Long = nativeDeriveNodeId(pubkey)
fun connectChannel(channelId: String) { nativeConnectChannel(channelId) }
fun getChannels(): String = nativeGetChannels() ?: "[]"
fun getMessages(channelId: String, limit: Int = 50): String = nativeGetMessages(channelId, limit) ?: "[]"
@ -124,6 +118,7 @@ object NativeLib {
private external fun nativeCreateChannel(name: String, channelId: String)
private external fun nativeConnectNode(address: String, port: Int, pubkeyHex: String)
private external fun nativeJoinChannel(channelId: Long, nodeId: Long, pubkey: ByteArray, addrs: ByteArray, addrCount: Int)
private external fun nativeDeriveNodeId(pubkey: ByteArray): Long
private external fun nativeConnectChannel(channelId: String)
private external fun nativeGetChannels(): String?
private external fun nativeGetMessages(channelId: String, limit: Int): String?
@ -138,6 +133,7 @@ object NativeLib {
private external fun nativeRestart(configText: String): Boolean
private external fun nativePing()
private external fun nativeIsResponsive(): Boolean
private external fun nativeIsRunning(): Boolean
/* ── Channel members JNI ── */
private external fun nativeGetChannelMembers(channelId: String): String?

2
tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt

@ -95,7 +95,7 @@ class ChatViewModel : ViewModel() {
viewModelScope.launch {
while (isActive) {
delay(3000)
if (!dbReady || !NativeLib.isReady()) continue
if (!dbReady || !NativeLib.isReady()) { _isStuck.value = true; continue }
NativeLib.ping()
delay(100)
_isStuck.value = !NativeLib.isResponsive()

29
tools/chatgui-android/jni_bridge/android_jni_bridge.c

@ -22,6 +22,7 @@
#include "../../../src/utun_instance.h"
#include "../../../src/transport_layer/etcp.h"
#include "../../../src/transport_layer/etcp_connections.h"
#include "../../../src/transport_layer/secure_channel.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@ -215,14 +216,18 @@ void utun_bridge_join_channel(uint64_t channel_id, uint64_t node_id,
const uint8_t* pubkey_bin,
const uint8_t* addrs_data, int addr_count,
int addrs_data_len) {
uint64_t real_node_id = sc_derive_node_id_from_pubkey(pubkey_bin);
if (real_node_id != node_id)
bridge_log(BLEV_WARN, "join: node_id Kotlin=0x%016llx != C=0x%016llx — using C value",
(unsigned long long)node_id, (unsigned long long)real_node_id);
bridge_log(BLEV_INFO, "join ch=%llu node=0x%016llx addrs=%d len=%d",
(unsigned long long)channel_id, (unsigned long long)node_id, addr_count, addrs_data_len);
(unsigned long long)channel_id, (unsigned long long)real_node_id, addr_count, addrs_data_len);
if (!pubkey_bin || !addrs_data || addr_count <= 0) {
bridge_log(BLEV_ERROR, "join invalid args");
utun_bridge_event(6, "{\"node_id\":0,\"result\":-7}"); /* CHAT_EVT_CONNECT_RESULT */
return;
}
chat_sync_connect_from_invite(channel_id, node_id, pubkey_bin, addrs_data, addr_count, addrs_data_len);
chat_sync_connect_from_invite(channel_id, real_node_id, pubkey_bin, addrs_data, addr_count, addrs_data_len);
}
void utun_bridge_connect_channel(const char* channel_id) {
@ -1131,6 +1136,20 @@ JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeJoinChannel(
(*env)->ReleaseByteArrayElements(env, pubkey, (jbyte*)pk, JNI_ABORT);
}
/* ── nativeDeriveNodeId ── */
JNIEXPORT jlong JNICALL Java_com_utun_chat_data_NativeLib_nativeDeriveNodeId(
JNIEnv* env, jobject thiz, jbyteArray pubkey) {
(void)thiz;
if (!pubkey) return 0;
jsize pk_len = (*env)->GetArrayLength(env, pubkey);
if (pk_len < 32) return 0;
uint8_t* pk = (uint8_t*)(*env)->GetByteArrayElements(env, pubkey, NULL);
uint64_t node_id = sc_derive_node_id_from_pubkey(pk);
(*env)->ReleaseByteArrayElements(env, pubkey, (jbyte*)pk, JNI_ABORT);
return (jlong)node_id;
}
/* ── nativeSetEventCallback(eventCb) ── */
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeSetEventCallback(
@ -1233,6 +1252,12 @@ JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeIsResponsive(
return utun_bridge_is_responsive() ? JNI_TRUE : JNI_FALSE;
}
JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeIsRunning(
JNIEnv* env, jobject thiz) {
(void)env; (void)thiz;
return instance_lite_is_running() ? JNI_TRUE : JNI_FALSE;
}
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativePing(
JNIEnv* env, jobject thiz) {
(void)env; (void)thiz;

15
tools/chatgui-android/libutun_lite/instance_lite.c

@ -309,6 +309,7 @@ static void restart_trampoline(void* arg) {
if (!new_config) {
IL_LOGE("restart: parse_config failed");
g_running = 0;
g_inst = NULL;
return;
}
@ -342,12 +343,14 @@ static void restart_trampoline(void* arg) {
} else {
IL_LOGE("restart: SQLite reopen failed rc=%d", rc);
g_running = 0;
g_inst = NULL;
return;
}
if (utun_instance_init(g_inst) != 0) {
IL_LOGE("restart: utun_instance_init failed");
g_running = 0;
g_inst = NULL;
return;
}
@ -360,7 +363,13 @@ static void restart_trampoline(void* arg) {
}
void instance_lite_restart(const char* new_config_text) {
if (!g_ua || !g_inst || !new_config_text) return;
if (!new_config_text) return;
if (!g_ua || !g_inst || !g_running) {
IL_LOGI("restart: instance dead, doing hard restart via stop+start");
if (g_inst) instance_lite_stop();
instance_lite_start(new_config_text);
return;
}
IL_LOGI("restart: posting to uasync");
char* copy = u_strdup(new_config_text);
if (!copy) return;
@ -384,11 +393,13 @@ void instance_lite_ping(void) {
}
int instance_lite_is_responsive(void) {
if (!g_ua || !g_inst || !g_running) return 0;
if (g_ping_id == 0) return 0;
return (g_pong_id == g_ping_id) ? 1 : 0;
}
int instance_lite_is_running(void) {
return g_inst != NULL ? 1 : 0;
return (g_inst != NULL && g_running) ? 1 : 0;
}
struct UASYNC* instance_lite_get_uasync(void) {

4
tools/chatgui-android/libutun_lite/invite_link_c.c

@ -3,6 +3,8 @@
#include <stdlib.h>
#include <stdio.h>
#include "../../../src/transport_layer/secure_channel.h"
#define INVITE_PREFIX "utun://"
#define INVITE_PREFIX_LEN 7
@ -81,7 +83,6 @@ int invite_link_decode(const char* link, size_t link_len, struct InviteDataC* ou
for (int i = 0; i < 8; i++)
chId = (chId << 8) | raw[off++];
out->channelId = chId;
out->nodeId = 0;
memset(out->pubkey, 0, INVITE_PUBKEY_SIZE);
out->addrCount = 0;
@ -99,6 +100,7 @@ int invite_link_decode(const char* link, size_t link_len, struct InviteDataC* ou
}
if (first_block) {
memcpy(out->pubkey, raw + off, INVITE_PUBKEY_SIZE);
out->nodeId = sc_derive_node_id_from_pubkey(out->pubkey);
first_block = 0;
}
off += 32;

Loading…
Cancel
Save