// utun_node.cpp — embedded uTun node running in a dedicated thread #include "utun_node.h" #include "stream_bridge.h" #include #include #include #include #include #include "../../lib/socket_compat.h" // === C headers === extern "C" { #include "utun_instance.h" #include "etcp_router.h" #include "etcp_api.h" #include "etcp.h" #include "config_parser.h" #include "config_updater.h" #include "control_server.h" #include "secure_channel.h" #include "../lib/u_async.h" #include "../lib/ll_queue.h" #include "../lib/memory_pool.h" #include "../lib/debug_config.h" #include "../lib/mem.h" #include "chat/chat_sync.h" #include "chat/chat_core.h" #include "chat/chat_event.h" #include "chat/db_sync.h" #include "call/call.h" #include "call/call_audio.h" #include "call/call_video_port.h" #include "radio/radio_audio.h" #include "dm/dm_core.h" #include "dm/dm_mailbox.h" #include "gui_bridge.h" #include "topo_node_sqlite.h" #include "topo_node.h" #include "topo_group.h" } UtunNode::UtunNode(QObject* parent) : QObject(parent) {} UtunNode::~UtunNode() { stop(); if (!m_debugLogPath.isEmpty() && g_debug_config.output_file == m_debugLogPath.constData()) debug_disable_file_output(); } bool UtunNode::start(const QString& configPath) { if (m_running) { m_startupError = QStringLiteral("Node is already running"); DEBUG_WARN(DEBUG_CATEGORY_SYS, "desktop startup rejected: node already running"); emit error(m_startupError); return false; } m_configPath = configPath; m_stop = false; m_running = true; ++m_generation; std::promise startup; auto ready = startup.get_future(); try { m_thread = std::thread(&UtunNode::runLoop, this, std::move(startup)); } catch (const std::system_error& e) { m_running = false; m_startupError = QStringLiteral("Worker thread creation failed: %1").arg(QString::fromLocal8Bit(e.what())); DEBUG_ERROR(DEBUG_CATEGORY_SYS, "desktop startup: %s", qPrintable(m_startupError)); emit error(m_startupError); return false; } m_startupError = ready.get(); if (!m_startupError.isEmpty()) { if (m_thread.joinable()) m_thread.join(); finalize(); emit error(m_startupError); return false; } emit started(); return true; } void UtunNode::setDebugFile(const QString& path) { m_debugFile = path; } void UtunNode::setDbPath(const QString& path) { m_dbPath = path; if (!QDir(path).mkpath(".")) DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "desktop database directory creation failed: %s", qPrintable(path)); } void UtunNode::setDebugLevel(const QString& level) { m_debugLevel = level; } void UtunNode::setDebugCategories(const QString& categories) { m_debugCategories = categories; } void UtunNode::requestStop() { m_stop = true; std::lock_guard guard(m_uaMutex); if (m_ua) uasync_wakeup(m_ua); } void UtunNode::stop() { if (!m_running) return; requestStop(); if (m_thread.joinable()) m_thread.join(); finalize(); m_running = false; } void UtunNode::finalize() { gui_bridge_set_inst(nullptr); gui_bridge_set_uasync(nullptr); { std::lock_guard guard(m_uaMutex); if (m_ua) { uasync_destroy(m_ua, 0); m_ua = nullptr; } } m_instance = nullptr; updateSnapshot(nullptr); m_running = false; DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "finalize: cleanup done"); } QString UtunNode::nodeIdHex() const { std::lock_guard guard(m_snapshotMutex); return m_snapshot.nodeId; } QString UtunNode::pubKeyHex() const { std::lock_guard guard(m_snapshotMutex); return m_snapshot.publicKey; } int64_t UtunNode::ntpOffsetUs() const { std::lock_guard guard(m_snapshotMutex); return m_snapshot.ntpOffsetUs; } bool UtunNode::ntpSynced() const { std::lock_guard guard(m_snapshotMutex); return m_snapshot.ntpSynced; } bool UtunNode::ntpEnabled() const { std::lock_guard guard(m_snapshotMutex); return m_snapshot.ntpEnabled; } /* GUI получает только собственные строки/числа; изменяемые поля ядра читает его поток. */ void UtunNode::updateSnapshot(const UTUN_INSTANCE* inst) { std::lock_guard guard(m_snapshotMutex); if (!inst) { m_snapshot = {}; return; } if (m_snapshot.nodeId.isEmpty()) { char id[17]; snprintf(id, sizeof(id), "%016" PRIx64, inst->node_id); m_snapshot.nodeId = QString::fromLatin1(id); m_snapshot.publicKey = QString::fromLatin1(inst->config->global.my_public_key_hex, 64); } m_snapshot.ntpOffsetUs = inst->ntp.offset_us; m_snapshot.ntpSynced = inst->ntp.synced; m_snapshot.ntpEnabled = inst->ntp.enabled; } void UtunNode::runLoop(std::promise startup) { // Повторный init не закрывает прежний FILE; путь прошлого запуска пока ещё жив. debug_disable_file_output(); debug_config_init(); debug_enable_function_name(0); /* ── debug level from config ── */ if (!m_debugLevel.isEmpty()) { QString lvl = m_debugLevel.toLower(); debug_level_t dl = DEBUG_LEVEL_INFO; if (lvl == "none") dl = DEBUG_LEVEL_NONE; else if (lvl == "error") dl = DEBUG_LEVEL_ERROR; else if (lvl == "warn") dl = DEBUG_LEVEL_WARN; else if (lvl == "info") dl = DEBUG_LEVEL_INFO; else if (lvl == "debug") dl = DEBUG_LEVEL_DEBUG; else if (lvl == "trace") dl = DEBUG_LEVEL_TRACE; g_debug_config.level = dl; } else { g_debug_config.level = DEBUG_LEVEL_INFO; } /* Системные ошибки и жизненный цикл ядра должны быть видны без специальной настройки. */ g_debug_config.category_levels[DEBUG_CATEGORY_SYS] = DEBUG_LEVEL_INFO; g_debug_config.category_levels[DEBUG_CATEGORY_BGP] = DEBUG_LEVEL_INFO; /* per-category overrides from config: cat=level,cat=level,... */ if (!m_debugCategories.isEmpty() && debug_parse_config(m_debugCategories.toUtf8().constData()) < 0) DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "desktop debug categories contain invalid settings: %s", qPrintable(m_debugCategories)); /* file output (shared with GUI via debug_config) */ if (!m_debugFile.isEmpty()) { m_debugLogPath = m_debugFile.toUtf8(); debug_enable_file_output(m_debugLogPath.constData(), 0); debug_enable_console(1); g_debug_config.thread_marker = 2; } utun_instance_set_tun_init_enabled(1); struct UASYNC* ua = uasync_create(); if (!ua) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "desktop startup: uasync_create failed"); startup.set_value(QStringLiteral("uasync_create failed")); return; } { std::lock_guard guard(m_uaMutex); m_ua = ua; } UTUN_INSTANCE* inst = nullptr; // Один откат для любого частичного запуска, до публикации готовности в GUI. auto failed = [&](const QString& reason) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "desktop startup rollback: reason=%s instance=%p", qPrintable(reason), (void*)inst); if (inst) { gui_stream_detach(inst); call_video_port_detach(inst); call_audio_destroy(inst); radio_audio_destroy(inst); utun_instance_destroy(inst); } { std::lock_guard guard(m_uaMutex); uasync_destroy(ua, 0); m_ua = nullptr; } startup.set_value(reason); }; /* Читаем конфиг и выставляем db_path ДО создания инстанса, чтобы topo_groups_init * (внутри utun_instance_create_from_config) открыл общую SQLite-БД раньше auto_socket_init — * иначе auto_socket не сможет сохранить/переиспользовать порты (БД ещё не открыта). */ struct utun_config* cfg = parse_config(m_configPath.toUtf8().constData()); if (!cfg) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "desktop startup: parse_config failed path=%s", qPrintable(m_configPath)); failed(QCoreApplication::translate("ChatGui", "Invalid configuration:\n%1").arg(m_configPath)); return; } cfg->global.db_sync_enabled = 1; cfg->global.client_type = CLIENT_TYPE_DESKTOP; DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "chatgui: device=desktop; supernode role is channel-specific"); if (!m_dbPath.isEmpty()) snprintf(cfg->global.db_path, sizeof(cfg->global.db_path), "%s", m_dbPath.toUtf8().constData()); inst = utun_instance_create_from_config(ua, cfg); if (!inst) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "desktop startup: instance creation failed"); free_config(cfg); failed(QStringLiteral("utun_instance_create_from_config failed")); return; } inst->test_user_ptr = this; if (utun_core_start(inst) != 0) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "desktop startup: utun_core_start failed"); failed(QStringLiteral("utun_core_start failed")); return; } DEBUG_INFO(DEBUG_CATEGORY_SYS, "desktop service policy: utun_enabled=%d tun_enabled=%d", cfg->global.gui_utun_enabled, cfg->global.tun_enabled); if (cfg->global.gui_utun_enabled && utun_service_start(inst) != 0) { DEBUG_ERROR(DEBUG_CATEGORY_TUN, "desktop startup: UTUN service failed tun_enabled=%d interface=%s", cfg->global.tun_enabled, cfg->global.tun_ifname); failed(QStringLiteral("utun_service_start failed; check TUN configuration, driver and permissions")); return; } { int sock_count = 0; for (struct ETCP_SOCKET* s = inst->etcp_sockets; s; s = s->next) sock_count++; DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "utun_node: after instance_init, etcp_sockets=%d conns=%d", sock_count, queue_entry_count(inst->connections)); } utun_add_nodeinfo_cbk(inst, gui_nodeinfo_cb_impl, nullptr); /* Bridge chat events to GUI via gui_bridge */ chat_event_set_handler(inst, [](struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len) { (void)inst; gui_bridge_post(type, data, len); }); if (chat_service_start(inst) != 0) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "desktop startup: chat_service_start failed"); failed(QStringLiteral("chat_service_start failed")); return; } if (call_audio_init(inst) != 0) { failed(QStringLiteral("call_audio_init failed")); return; } if (call_video_port_attach(inst) != 0) { failed(QStringLiteral("call_video_port_attach failed")); return; } if (radio_audio_init(inst) != 0) { failed(QStringLiteral("radio_audio_init failed")); return; } if (!gui_stream_attach(inst)) { failed(QStringLiteral("stream bridge initialization failed")); return; } chat_core_sync_my_addresses(inst); DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "chat_core + chat_sync initialized"); uasync_mark_running(ua); updateSnapshot(inst); m_instance = inst; gui_bridge_set_uasync(ua); gui_bridge_set_inst(inst); DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "utun_node: poll started ua=%p running=%d", (void*)ua, uasync_is_running(ua)); DEBUG_INFO(DEBUG_CATEGORY_SYS, "desktop services ready: core=%d utun=%d chat=%d tun=%d interface=%s", inst->core_started, inst->utun_started, inst->chat_started, cfg->global.tun_enabled, cfg->global.tun_ifname); startup.set_value(QString()); const uint64_t generation = m_generation; while (!m_stop && inst->running) { uasync_poll(ua, 100); updateSnapshot(inst); } if (!m_stop) DEBUG_ERROR(DEBUG_CATEGORY_SYS, "desktop core requested stop; releasing services outside callbacks"); gui_bridge_set_inst(nullptr); m_instance = nullptr; uasync_mark_stopped(ua); DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "utun_node: poll stopped ua=%p running=%d requested=%d", (void*)ua, uasync_is_running(ua), (int)m_stop); DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "runLoop: destroying instance in worker thread"); gui_stream_detach(inst); call_video_port_detach(inst); call_audio_destroy(inst); radio_audio_destroy(inst); utun_instance_destroy(inst); DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "runLoop: emitting stopped"); QMetaObject::invokeMethod(this, [this, generation] { if (generation == m_generation) emit stopped(); else DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "desktop stale stopped ignored: generation=%llu current=%llu", (unsigned long long)generation, (unsigned long long)m_generation); }); }