diff --git a/src/routing_layer/conn_mgr.h b/src/routing_layer/conn_mgr.h index bb7c3e23..afcbbdfc 100644 --- a/src/routing_layer/conn_mgr.h +++ b/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 жив */ }; /** diff --git a/src/routing_layer/conn_mgr_core.c b/src/routing_layer/conn_mgr_core.c index 46160273..28844373 100644 --- a/src/routing_layer/conn_mgr_core.c +++ b/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); } } diff --git a/src/transport_layer/node_conn_direct.c b/src/transport_layer/node_conn_direct.c index 1efa915c..d8383155 100644 --- a/src/transport_layer/node_conn_direct.c +++ b/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; diff --git a/src/transport_layer/node_conn_direct.h b/src/transport_layer/node_conn_direct.h index 9fd48541..3992dedc 100644 --- a/src/transport_layer/node_conn_direct.h +++ b/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 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) ─── */ diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/InviteLink.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/InviteLink.kt index 622e45b5..b46df4c0 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/InviteLink.kt +++ b/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)) diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt index b580fa1d..0040f0af 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt +++ b/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? diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt index 1fd5d2eb..37e3b71b 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt +++ b/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() diff --git a/tools/chatgui-android/jni_bridge/android_jni_bridge.c b/tools/chatgui-android/jni_bridge/android_jni_bridge.c index 6cdafc52..7c8039ab 100644 --- a/tools/chatgui-android/jni_bridge/android_jni_bridge.c +++ b/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 #include #include @@ -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; diff --git a/tools/chatgui-android/libutun_lite/instance_lite.c b/tools/chatgui-android/libutun_lite/instance_lite.c index db11a5cb..f912e487 100644 --- a/tools/chatgui-android/libutun_lite/instance_lite.c +++ b/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) { diff --git a/tools/chatgui-android/libutun_lite/invite_link_c.c b/tools/chatgui-android/libutun_lite/invite_link_c.c index f64703f7..8ded8d07 100644 --- a/tools/chatgui-android/libutun_lite/invite_link_c.c +++ b/tools/chatgui-android/libutun_lite/invite_link_c.c @@ -3,6 +3,8 @@ #include #include +#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;