Add a settings button to open the current config in an external editor. Wait for core, UTUN and chat startup before reporting readiness; verify startup failures and restarts. Include Wintun DLL and license notices in Windows builds and installers.
304 lines
11 KiB
C++
304 lines
11 KiB
C++
// utun_node.cpp — embedded uTun node running in a dedicated thread
|
|
#include "utun_node.h"
|
|
#include "stream_bridge.h"
|
|
#include <cinttypes>
|
|
#include <utility>
|
|
#include <QDir>
|
|
#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();
|
|
}
|
|
|
|
bool UtunNode::start(const QString& configPath) {
|
|
if (m_running) return false;
|
|
m_configPath = configPath;
|
|
m_stop = false;
|
|
m_running = true;
|
|
std::promise<bool> startup;
|
|
auto ready = startup.get_future();
|
|
m_thread = std::thread(&UtunNode::runLoop, this, std::move(startup));
|
|
if (!ready.get()) {
|
|
if (m_thread.joinable()) m_thread.join();
|
|
finalize();
|
|
return false;
|
|
}
|
|
emit started();
|
|
return true;
|
|
}
|
|
|
|
void UtunNode::setDebugFile(const QString& path) {
|
|
m_debugFile = path;
|
|
}
|
|
|
|
void UtunNode::setDbPath(const QString& path) {
|
|
m_dbPath = path;
|
|
QDir(path).mkpath(".");
|
|
}
|
|
|
|
void UtunNode::setDebugLevel(const QString& level) {
|
|
m_debugLevel = level;
|
|
}
|
|
|
|
void UtunNode::setDebugCategories(const QString& categories) {
|
|
m_debugCategories = categories;
|
|
}
|
|
|
|
void UtunNode::requestStop() {
|
|
m_stop = true;
|
|
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);
|
|
if (m_ua) { uasync_destroy(m_ua, 0); m_ua = nullptr; }
|
|
m_instance = nullptr;
|
|
m_running = false;
|
|
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "finalize: cleanup done");
|
|
}
|
|
|
|
QString UtunNode::nodeIdHex() const {
|
|
if (!m_instance) return {};
|
|
char buf[32];
|
|
snprintf(buf, sizeof(buf), "%016" PRIx64, m_instance->node_id);
|
|
return QString(buf);
|
|
}
|
|
|
|
QString UtunNode::pubKeyHex() const {
|
|
if (!m_instance || !m_instance->config) return {};
|
|
return QString::fromLatin1(m_instance->config->global.my_public_key_hex, 64);
|
|
}
|
|
|
|
int64_t UtunNode::ntpOffsetUs() const {
|
|
return m_instance ? m_instance->ntp.offset_us : 0;
|
|
}
|
|
|
|
bool UtunNode::ntpSynced() const {
|
|
return m_instance ? m_instance->ntp.synced : false;
|
|
}
|
|
|
|
bool UtunNode::ntpEnabled() const {
|
|
return m_instance ? m_instance->ntp.enabled : false;
|
|
}
|
|
|
|
void UtunNode::runLoop(std::promise<bool> startup) {
|
|
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()) {
|
|
QByteArray cats = m_debugCategories.toUtf8();
|
|
char* token = strtok(cats.data(), ",");
|
|
while (token) {
|
|
while (*token == ' ') token++;
|
|
char* eq = strchr(token, '=');
|
|
if (eq) {
|
|
*eq = '\0';
|
|
char* cat_name = token;
|
|
char* lvl_name = eq + 1;
|
|
while (*lvl_name == ' ') lvl_name++;
|
|
debug_category_t cat = get_category_by_name(cat_name);
|
|
debug_level_t lvl = debug_level_from_name(lvl_name);
|
|
if (cat != DEBUG_CATEGORY_NONE && lvl != DEBUG_LEVEL_NONE) {
|
|
g_debug_config.category_levels[cat] = lvl;
|
|
DEBUG_DEBUG(DEBUG_CATEGORY_CONFIG, "debug category: %s=%s",
|
|
debug_get_category_name(cat),
|
|
lvl_name);
|
|
}
|
|
}
|
|
token = strtok(NULL, ",");
|
|
}
|
|
}
|
|
|
|
/* file output (shared with GUI via debug_config) */
|
|
if (!m_debugFile.isEmpty()) {
|
|
debug_enable_file_output(m_debugFile.toUtf8().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");
|
|
QMetaObject::invokeMethod(this, [this] { emit error("uasync_create failed"); });
|
|
startup.set_value(false);
|
|
return;
|
|
}
|
|
m_ua = ua;
|
|
|
|
/* Читаем конфиг и выставляем 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));
|
|
QMetaObject::invokeMethod(this, [this] { emit error("parse_config failed"); });
|
|
uasync_destroy(ua, 0);
|
|
m_ua = nullptr;
|
|
startup.set_value(false);
|
|
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());
|
|
|
|
m_instance = utun_instance_create_from_config(ua, cfg);
|
|
if (!m_instance) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "desktop startup: instance creation failed");
|
|
free_config(cfg);
|
|
QMetaObject::invokeMethod(this, [this] { emit error("utun_instance_create_from_config failed"); });
|
|
uasync_destroy(ua, 0);
|
|
m_ua = nullptr;
|
|
startup.set_value(false);
|
|
return;
|
|
}
|
|
|
|
m_instance->test_user_ptr = this;
|
|
|
|
if (utun_core_start(m_instance) != 0) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "desktop startup: utun_core_start failed");
|
|
QMetaObject::invokeMethod(this, [this] { emit error("utun_core_start failed"); });
|
|
utun_instance_destroy(m_instance);
|
|
m_instance = nullptr;
|
|
uasync_destroy(ua, 0);
|
|
m_ua = nullptr;
|
|
startup.set_value(false);
|
|
return;
|
|
}
|
|
|
|
if (utun_service_start(m_instance) != 0) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_TUN, "desktop startup: UTUN service failed tun_enabled=%d interface=%s",
|
|
cfg->global.tun_enabled, cfg->global.tun_ifname);
|
|
QMetaObject::invokeMethod(this, [this] { emit error("utun_service_start failed; check TUN configuration, driver and permissions"); });
|
|
utun_instance_destroy(m_instance); m_instance = nullptr;
|
|
uasync_destroy(ua, 0); m_ua = nullptr;
|
|
startup.set_value(false);
|
|
return;
|
|
}
|
|
|
|
{ int sock_count = 0; for (struct ETCP_SOCKET* s = m_instance->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(m_instance->connections)); }
|
|
|
|
utun_add_nodeinfo_cbk(m_instance, gui_nodeinfo_cb_impl, nullptr);
|
|
|
|
/* Bridge chat events to GUI via gui_bridge */
|
|
chat_event_set_handler(m_instance, [](struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len) {
|
|
(void)inst;
|
|
gui_bridge_post(type, data, len);
|
|
});
|
|
|
|
if (chat_service_start(m_instance) != 0) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "desktop startup: chat_service_start failed");
|
|
QMetaObject::invokeMethod(this, [this] { emit error("chat_service_start failed"); });
|
|
utun_instance_destroy(m_instance); m_instance = nullptr;
|
|
uasync_destroy(ua, 0); m_ua = nullptr;
|
|
startup.set_value(false);
|
|
return;
|
|
}
|
|
call_audio_init(m_instance);
|
|
call_video_port_attach(m_instance);
|
|
radio_audio_init(m_instance);
|
|
if (!gui_stream_attach(m_instance)) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "desktop stream bridge initialization failed");
|
|
QMetaObject::invokeMethod(this, [this] { emit error("stream bridge initialization failed"); });
|
|
}
|
|
chat_core_sync_my_addresses(m_instance);
|
|
|
|
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "chat_core + chat_sync initialized");
|
|
uasync_mark_running(ua);
|
|
gui_bridge_set_uasync(ua);
|
|
gui_bridge_set_inst(m_instance);
|
|
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",
|
|
m_instance->core_started, m_instance->utun_started, m_instance->chat_started,
|
|
cfg->global.tun_enabled, cfg->global.tun_ifname);
|
|
startup.set_value(true);
|
|
|
|
while (!m_stop) {
|
|
uasync_poll(ua, 100);
|
|
}
|
|
|
|
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);
|
|
|
|
if (m_instance) {
|
|
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "runLoop: destroying instance in worker thread");
|
|
gui_stream_detach(m_instance);
|
|
call_video_port_detach(m_instance);
|
|
call_audio_destroy(m_instance);
|
|
radio_audio_destroy(m_instance);
|
|
utun_instance_destroy(m_instance);
|
|
m_instance = nullptr;
|
|
}
|
|
|
|
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "runLoop: emitting stopped");
|
|
QMetaObject::invokeMethod(this, [this] { emit stopped(); });
|
|
}
|