/* * instance_lite.c — lightweight uTun instance lifecycle for Android * * Kotlin sends INI config text → parse_config_from_buf → * utun_instance_create_from_config → sqlite3_open → utun_instance_init → * pthread with uasync_poll loop. * * Mirrors UtunNode::runLoop() from tools/chatgui/transport/utun_node.cpp:221-372. */ #include "instance_lite.h" #include "utun_instance.h" #include "config_parser.h" #include "config_updater.h" #include "secure_channel.h" #include "etcp_router.h" #include "topo_node_sqlite.h" #include "chat/chat_core.h" #include "chat/chat_sync.h" #include "chat/chat_event.h" #include "u_async.h" #include "sqlite3.h" #include "mem.h" #include "debug_config.h" #include "../jni_bridge/android_udp_log.h" #include #include #include #include #include #include #ifdef __ANDROID__ #include #define IL_LOGV(fmt, ...) __android_log_print(ANDROID_LOG_VERBOSE, "utun-inst", fmt, ##__VA_ARGS__) #define IL_LOGI(fmt, ...) __android_log_print(ANDROID_LOG_INFO, "utun-inst", fmt, ##__VA_ARGS__) #define IL_LOGE(fmt, ...) __android_log_print(ANDROID_LOG_ERROR, "utun-inst", fmt, ##__VA_ARGS__) #else #define IL_LOGV(fmt, ...) fprintf(stderr, fmt "\n", ##__VA_ARGS__) #define IL_LOGI(fmt, ...) fprintf(stderr, fmt "\n", ##__VA_ARGS__) #define IL_LOGE(fmt, ...) fprintf(stderr, fmt "\n", ##__VA_ARGS__) #endif /* ── Static state ── */ static struct UASYNC* g_ua = NULL; static struct UTUN_INSTANCE* g_inst = NULL; static pthread_t g_thread; static volatile int g_running = 0; static volatile int g_stop = 0; static char g_db_path[512]; static instance_lite_event_fn g_event_handler = NULL; static char g_generated_pub[65]; static char g_generated_priv[65]; /* ── Generate X25519 key pair, hex-encode to out buffers ── */ static int generate_x25519_keys(char pub_hex[65], char priv_hex[65]) { EVP_PKEY* pkey = NULL; EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_X25519, NULL); if (!ctx) return -1; if (EVP_PKEY_keygen_init(ctx) <= 0 || EVP_PKEY_generate(ctx, &pkey) <= 0) { EVP_PKEY_CTX_free(ctx); return -1; } EVP_PKEY_CTX_free(ctx); size_t pkLen = 32, skLen = 32; uint8_t pk[32], sk[32]; EVP_PKEY_get_raw_public_key(pkey, pk, &pkLen); EVP_PKEY_get_raw_private_key(pkey, sk, &skLen); EVP_PKEY_free(pkey); for (int i = 0; i < 32; i++) snprintf(pub_hex + i * 2, 3, "%02x", pk[i]); for (int i = 0; i < 32; i++) snprintf(priv_hex + i * 2, 3, "%02x", sk[i]); pub_hex[64] = '\0'; priv_hex[64] = '\0'; return 0; } static void save_keys_to_file(const char* pub, const char* priv) { if (!g_db_path[0]) { IL_LOGE("save_keys_to_file: g_db_path is empty, keys not saved to disk"); return; } char fname[512]; snprintf(fname, sizeof(fname), "%s/keys.conf", g_db_path); FILE* f = fopen(fname, "w"); if (!f) { IL_LOGE("cannot write %s: %s", fname, strerror(errno)); return; } fchmod(fileno(f), 0600); fprintf(f, "public_key=%s\nprivate_key=%s\n", pub, priv); fclose(f); IL_LOGI("keys saved to %s", fname); } /* ── Ensure valid keys in config, generate if missing ── */ static void ensure_keys(struct utun_config* config) { if (strlen(config->global.my_private_key_hex) != 64 || strlen(config->global.my_public_key_hex) != 64) { IL_LOGI("config keys missing or invalid, generating new X25519 keypair"); if (generate_x25519_keys(g_generated_pub, g_generated_priv) == 0) { memcpy(config->global.my_public_key_hex, g_generated_pub, 65); memcpy(config->global.my_private_key_hex, g_generated_priv, 65); IL_LOGI("new keys generated pub=%s", g_generated_pub); save_keys_to_file(g_generated_pub, g_generated_priv); } else { IL_LOGE("key generation failed"); } } } /* ── Helper: extract single config value by key ── */ static const char* cfg_get(const char* text, const char* key) { size_t klen = strlen(key); const char* p = text; while (*p) { if (strncmp(p, key, klen) == 0 && p[klen] == '=') { return p + klen + 1; } while (*p && *p != '\n') p++; if (*p == '\n') p++; } return NULL; } static void cfg_get_val(const char* text, const char* key, char* out, size_t out_sz) { const char* v = cfg_get(text, key); if (!v) { out[0] = '\0'; return; } size_t i = 0; while (*v && *v != '\n' && *v != '\r' && i < out_sz - 1) out[i++] = *v++; out[i] = '\0'; } /* ── Chat event forward → Kotlin callback ── */ static void chat_event_forward(int type, const uint8_t* data, int len) { if (g_event_handler) g_event_handler(type, data, len); } static void nodeinfo_event_cb(struct TOPO_GROUP* group, struct TOPO_GROUP_NODE* nq, void* arg) { (void)group; (void)arg; if (!nq || !g_event_handler) return; uint16_t best_rtt = node_best_rtt(nq); uint8_t data[12]; memcpy(data, &nq->node_id, 8); data[8] = nq->conn_presence; data[9] = nq->conn_up; memcpy(data + 10, &best_rtt, 2); g_event_handler(CHAT_EVT_NODEINFO_UPDATED, data, sizeof(data)); } /* ── Thread function ── */ static void* instance_thread(void* arg) { char* config_text = (char*)arg; struct utun_config* config = parse_config_from_buf(config_text, strlen(config_text), "android"); u_free(config_text); if (!config) { IL_LOGE("parse_config_from_buf failed"); return NULL; } g_ua = uasync_create(); if (!g_ua) { IL_LOGE("uasync_create failed"); free_config(config); return NULL; } if (config->global.log_udp_ip[0] && config->global.log_udp_port > 0) { udp_log_set_target(config->global.log_udp_ip, config->global.log_udp_port); uasync_set_timeout(g_ua, 2000, NULL, udp_trampoline, "udp_log_flush"); } chat_event_set_handler(chat_event_forward); ensure_keys(config); g_inst = utun_instance_create_from_config(g_ua, config); if (!g_inst) { IL_LOGE("utun_instance_create_from_config failed"); uasync_destroy(g_ua, 0); g_ua = NULL; u_report_unfreed_blocks(); return NULL; } IL_LOGI("instance created, node_id=0x%016llx", (unsigned long long)g_inst->node_id); /* Open shared SQLite DB */ if (g_db_path[0] && !g_inst->topo_sqlite_db) { char db_file[512]; snprintf(db_file, sizeof(db_file), "%s/chats.db", g_db_path); int rc = sqlite3_open_v2(db_file, &g_inst->topo_sqlite_db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX, NULL); if (rc == SQLITE_OK && g_inst->topo_sqlite_db) { sqlite3_exec(g_inst->topo_sqlite_db, "PRAGMA journal_mode=WAL", NULL, NULL, NULL); sqlite3_exec(g_inst->topo_sqlite_db, "PRAGMA foreign_keys=ON", NULL, NULL, NULL); sqlite3_exec(g_inst->topo_sqlite_db, "PRAGMA synchronous=NORMAL", NULL, NULL, NULL); sqlite3_exec(g_inst->topo_sqlite_db, "PRAGMA wal_autocheckpoint=10000", NULL, NULL, NULL); topo_node_sqlite_init(g_inst->topo_sqlite_db); IL_LOGI("SQLite opened %s", db_file); } else { IL_LOGE("SQLite open failed rc=%d", rc); if (g_inst->topo_sqlite_db) { sqlite3_close(g_inst->topo_sqlite_db); g_inst->topo_sqlite_db = NULL; } } } /* Full init: db_sync, chat_core, chat_sync, init_connections */ if (utun_instance_init(g_inst) != 0) { IL_LOGE("utun_instance_init failed"); utun_instance_destroy(g_inst); g_inst = NULL; uasync_destroy(g_ua, 0); g_ua = NULL; u_report_unfreed_blocks(); return NULL; } /* Bind chat sync via etcp_router */ etcp_router_bind(g_inst, ETCP_RT_ID_CHAT_SYNC, NULL); utun_add_nodeinfo_cbk(g_inst, nodeinfo_event_cb, NULL); /* Set my_name from config */ if (g_inst->config->global.name[0]) { chat_core_update_my_name(g_inst->config->global.name); IL_LOGI("my_name set to '%s'", g_inst->config->global.name); } IL_LOGI("all init done, entering poll loop"); /* Notify GUI of current public key (initial generation or existing) */ if (strlen(config->global.my_public_key_hex) == 64) { chat_event_post(CHAT_EVT_KEYS_GENERATED, (const uint8_t*)config->global.my_public_key_hex, 64); } g_running = 1; chat_event_post(CHAT_EVT_SERVICE_STARTED, NULL, 0); while (!g_stop) { uasync_poll(g_ua, 100); } IL_LOGI("poll loop exit, cleaning up"); if (g_inst) { utun_instance_destroy(g_inst); g_inst = NULL; } if (g_ua) { uasync_destroy(g_ua, 0); g_ua = NULL; } u_report_unfreed_blocks(); chat_event_post(CHAT_EVT_SERVICE_STOPPED, NULL, 0); g_event_handler = NULL; pthread_detach(pthread_self()); IL_LOGI("cleanup complete"); return NULL; } /* ── Public API ── */ int instance_lite_start(const char* config_text) { if (!config_text) return -1; if (g_inst) return 0; debug_config_init(); debug_set_level(DEBUG_LEVEL_INFO); debug_enable_function_name(0); cfg_get_val(config_text, "db_path", g_db_path, sizeof(g_db_path)); char* config_copy = u_strdup(config_text); if (!config_copy) return -1; g_stop = 0; int rc = pthread_create(&g_thread, NULL, instance_thread, config_copy); if (rc != 0) { u_free(config_copy); IL_LOGE("pthread_create failed rc=%d", rc); return -1; } IL_LOGI("started OK"); return 0; } void instance_lite_stop(void) { if (!g_inst) return; IL_LOGI("stopping..."); g_stop = 1; if (g_ua) uasync_wakeup(g_ua); for (int i = 0; i < 500 && g_inst != NULL; i++) usleep(10000); if (g_inst) { IL_LOGE("stop timeout, force cleanup"); g_inst = NULL; g_ua = NULL; } g_running = 0; } static void restart_trampoline(void* arg) { char* config_text = (char*)arg; IL_LOGI("restart: begin"); chat_sync_destroy(g_inst); chat_core_destroy(g_inst); if (g_inst->topo_sqlite_db) { sqlite3_close(g_inst->topo_sqlite_db); g_inst->topo_sqlite_db = NULL; } struct utun_config* new_config = parse_config_from_buf(config_text, strlen(config_text), "android"); u_free(config_text); if (!new_config) { IL_LOGE("restart: parse_config failed"); g_running = 0; return; } free_config(g_inst->config); g_inst->config = new_config; ensure_keys(new_config); uint8_t pk_bin[32]; for (int i = 0; i < 32; i++) { unsigned int b; sscanf(new_config->global.my_public_key_hex + i * 2, "%2x", &b); pk_bin[i] = (uint8_t)b; } g_inst->node_id = sc_derive_node_id_from_pubkey(pk_bin); sc_init_local_keys(&g_inst->my_keys, new_config->global.my_public_key_hex, new_config->global.my_private_key_hex); if (new_config->global.log_udp_ip[0] && new_config->global.log_udp_port > 0) { udp_log_set_target(new_config->global.log_udp_ip, new_config->global.log_udp_port); uasync_set_timeout(g_ua, 2000, NULL, udp_trampoline, "udp_log_flush"); } else { udp_log_stop(); } char db_file[512]; snprintf(db_file, sizeof(db_file), "%s/chats.db", g_db_path); int rc = sqlite3_open_v2(db_file, &g_inst->topo_sqlite_db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX, NULL); if (rc == SQLITE_OK && g_inst->topo_sqlite_db) { sqlite3_exec(g_inst->topo_sqlite_db, "PRAGMA journal_mode=WAL", NULL, NULL, NULL); sqlite3_exec(g_inst->topo_sqlite_db, "PRAGMA foreign_keys=ON", NULL, NULL, NULL); sqlite3_exec(g_inst->topo_sqlite_db, "PRAGMA synchronous=NORMAL", NULL, NULL, NULL); topo_node_sqlite_init(g_inst->topo_sqlite_db); IL_LOGI("restart: SQLite reopened %s", db_file); } else { IL_LOGE("restart: SQLite reopen failed rc=%d", rc); g_running = 0; return; } if (utun_instance_init(g_inst) != 0) { IL_LOGE("restart: utun_instance_init failed"); g_running = 0; return; } if (g_inst->config->global.name[0]) { chat_core_update_my_name(g_inst->config->global.name); IL_LOGI("restart: my_name set to '%s'", g_inst->config->global.name); } IL_LOGI("restart: done"); } void instance_lite_restart(const char* new_config_text) { if (!g_ua || !g_inst || !new_config_text) return; IL_LOGI("restart: posting to uasync"); char* copy = u_strdup(new_config_text); if (!copy) return; uasync_post(g_ua, restart_trampoline, copy); } /* ── Health check ping ── */ static volatile int g_ping_id = 0; static volatile int g_pong_id = 0; static void ping_trampoline(void* arg) { (void)arg; g_pong_id = g_ping_id; } void instance_lite_ping(void) { if (!g_ua || !g_inst) return; g_ping_id++; uasync_post(g_ua, ping_trampoline, NULL); } int instance_lite_is_responsive(void) { return (g_pong_id == g_ping_id) ? 1 : 0; } int instance_lite_is_running(void) { return g_inst != NULL ? 1 : 0; } struct UASYNC* instance_lite_get_uasync(void) { return g_ua; } static void regenerate_trampoline(void* arg) { (void)arg; if (!g_inst || !g_inst->config) { IL_LOGE("regenerate_keys: instance not running"); return; } struct utun_config* config = g_inst->config; IL_LOGI("regenerating keys..."); config->global.my_public_key_hex[0] = '\0'; config->global.my_private_key_hex[0] = '\0'; ensure_keys(config); if (g_generated_pub[0]) { uint8_t pk_bin[32]; for (int i = 0; i < 32; i++) { unsigned int b; sscanf(g_generated_pub + i * 2, "%2x", &b); pk_bin[i] = (uint8_t)b; } g_inst->node_id = sc_derive_node_id_from_pubkey(pk_bin); sc_init_local_keys(&g_inst->my_keys, g_generated_pub, g_generated_priv); IL_LOGI("keys regenerated, node_id=0x%016llx", (unsigned long long)g_inst->node_id); chat_event_post(CHAT_EVT_KEYS_GENERATED, (const uint8_t*)g_generated_pub, 64); } } void instance_lite_regenerate_keys(void) { if (!g_ua || !g_inst) return; uasync_post(g_ua, regenerate_trampoline, NULL); } void instance_lite_set_event_handler(instance_lite_event_fn handler) { g_event_handler = handler; if (g_running) chat_event_set_handler(chat_event_forward); }