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
This commit is contained in:
evgeny committed 2026-08-03 09:51:06 +03:00
1 parent 4b07d59043
commit 740ed2a93a
10 files changed
+351 -53

No files matched your search

@@ -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))
@@ -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?
@@ -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()
@@ -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;
@@ -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) {
@@ -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;