// node_config.cpp — Simplified INI config manager for embedded uTun node #include "node_config.h" #include "config_updater.h" #include extern "C" { #include } #include #include #include #include // ===================================================================== // Whitelists — union of chatgui + uTun config_parser keys // ===================================================================== static const QSet GLOBAL_KEYS = { "my_node_name", "my_private_key", "my_public_key", "my_node_id", "tun_enabled", "tun_ifname", "tun_ip", "mtu", "keepalive_timeout", "keepalive_interval", "keepalive_adaptive", "bbr_max_cwnd", "auto_sockets", "debug_level", "log_file", "db_path", "db_sync_enabled", "db_sync_ttl", "enable_timestamp", "enable_function_names", "enable_file_lines", "enable_colors", "tun_test_mode" }; static const QSet SERVER_KEYS = { "addr", "so_mark", "fib", "netif", "type", "mtu", "only_local", "transport" }; static const QSet CLIENT_KEYS = { "peer_public_key", "link", "keepalive" }; static const QSet CONTROL_KEYS = { "ip", "control_ip", "port", "control_port", "allow", "control_allow" }; static const QSet GUI_KEYS = { "db_path", "debug_file", "debug_level", "sound_dir", "sound_msg_incoming", "sound_msg_outgoing", "sound_connect", "sound_disconnect" }; static const QSet ALLOWED_KEYS_KEYS = { "allow_all", "key" }; static const QSet NAT_KEYS = { "enabled", "tun_ifname", "tun_ip", "nat_via", "port_start", "port_end", "forward" }; static const QSet FIREWALL_KEYS = { "allow" }; static const QSet ROUTING_KEYS = { "route_subnet", "my_subnet" }; static const QSet TCP_PROXY_CLIENT_KEYS = { "enabled", "tun_name", "tun_ip", "mtu", "via_node", "forward", "socks_enabled", "socks_addr", "http_proxy_enabled", "http_proxy_addr" }; static const QSet TCP_PROXY_SERVER_KEYS = { "enabled", "tcp_recv_buf" }; static const QSet NETWORK_KEYS = { "id", "pubkey", "signing_key" }; static const QSet NTP_KEYS = { "enabled", "server", "interval" }; static const QStringList VALID_DEBUG_LEVELS = { "none", "error", "warn", "info", "debug", "trace" }; // Основные debug-категории для подсказки в секции [debug] (комментарием). // Имена соответствуют lib/debug_config.c (g_debug_categories), уровень — разумный дефолт. static const struct { const char* name; const char* level; } DEBUG_CATEGORY_HINTS[] = { {"sys", "error"}, {"connection", "info"}, {"etcp", "trace"}, {"crypto", "error"}, {"config", "info"}, {"tun", "error"}, {"routing", "info"}, {"timers", "error"}, {"bgp", "info"}, {"chat", "info"}, {"chat_sync", "info"}, {"member_sync", "info"}, {"socket", "error"}, {"control", "info"}, {"dump", "trace"}, {"traffic", "trace"}, {"debug", "trace"}, {"general", "info"}, {"nat", "info"}, {"keepalive", "error"}, {"media", "info"}, {"etcp_route", "info"}, {"etcp_dump", "trace"}, {"proxy", "info"}, }; // Sections: "" = global, use prefix match for dynamic sections static bool isSectionValid(const QString& section) { if (section.isEmpty() || section == "global") return true; if (section.startsWith("server:")) return true; if (section.startsWith("client:")) return true; if (section.startsWith("network:")) return true; if (section == "control") return true; if (section == "gui") return true; if (section == "allowed_keys") return true; if (section == "nat") return true; if (section == "firewall") return true; if (section == "debug") return true; if (section == "routing" || section == "route") return true; if (section == "tcp_proxy_client") return true; if (section == "tcp_proxy_server") return true; if (section == "ntp") return true; return false; } static const QSet* keysForSection(const QString& section) { if (section.isEmpty() || section == "global") return &GLOBAL_KEYS; if (section.startsWith("server:")) return &SERVER_KEYS; if (section.startsWith("client:")) return &CLIENT_KEYS; if (section.startsWith("network:")) return &NETWORK_KEYS; if (section == "control") return &CONTROL_KEYS; if (section == "gui") return &GUI_KEYS; if (section == "allowed_keys") return &ALLOWED_KEYS_KEYS; if (section == "nat") return &NAT_KEYS; if (section == "firewall") return &FIREWALL_KEYS; if (section == "routing" || section == "route") return &ROUTING_KEYS; if (section == "tcp_proxy_client") return &TCP_PROXY_CLIENT_KEYS; if (section == "tcp_proxy_server") return &TCP_PROXY_SERVER_KEYS; if (section == "ntp") return &NTP_KEYS; return nullptr; // [debug] — free-form, no key validation } static QString formatKeyHex(const uint8_t* bin, size_t len) { return QByteArray(reinterpret_cast(bin), len).toHex(); } static bool parseKeyHex(const QString& hex, uint8_t* bin, size_t len) { QByteArray ba = QByteArray::fromHex(hex.toLatin1()); if (ba.size() != (int)len) return false; memcpy(bin, ba.constData(), len); return true; } // ===================================================================== // NodeConfig // ===================================================================== NodeConfig::NodeConfig(const QString& path) : m_path(path) {} bool NodeConfig::exists() const { return QFileInfo::exists(m_path); } void NodeConfig::generateIdentity() { EVP_PKEY* pkey = nullptr; EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_X25519, nullptr); if (ctx) { EVP_PKEY_keygen_init(ctx); EVP_PKEY_generate(ctx, &pkey); EVP_PKEY_CTX_free(ctx); } if (pkey) { 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); m_pubKey = formatKeyHex(pk, 32); m_privKey = formatKeyHex(sk, 32); EVP_PKEY_free(pkey); uint8_t hash[32]; unsigned int hashLen = 32; EVP_MD_CTX* mdctx = EVP_MD_CTX_new(); EVP_DigestInit_ex(mdctx, EVP_sha256(), NULL); EVP_DigestUpdate(mdctx, pk, 32); EVP_DigestFinal_ex(mdctx, hash, &hashLen); EVP_MD_CTX_free(mdctx); uint64_t id; memcpy(&id, hash, 8); id &= 0x7FFFFFFFFFFFFFFFULL; m_nodeId = QString("%1").arg(id, 16, 16, QChar('0')).right(16); } } bool NodeConfig::create(const QString& nodeName, quint16 listenPort) { if (nodeName.isEmpty()) { QString host = QHostInfo::localHostName(); m_nodeName = host.isEmpty() ? "vibechat" : host; } else { m_nodeName = nodeName; } if (m_pubKey.isEmpty()) generateIdentity(); if (m_controlAddr.isEmpty()) m_controlAddr = "127.0.0.1:9999"; m_servers.clear(); m_clients.clear(); (void)listenPort; /* автосокеты сами создают UDP+TCP на случайных портах */ m_autoSockets = true; if (m_dbPath.isEmpty()) m_dbPath = "chat_data"; if (m_debugFile.isEmpty()) m_debugFile = "chatgui.log"; return save(); } bool NodeConfig::load() { QFile f(m_path); if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) return false; m_servers.clear(); m_clients.clear(); m_errors.clear(); QString section, controlIp, controlPort; QTextStream in(&f); int lineNum = 0; bool sectionValid = true; // global/empty is valid bool debugSection = false; while (!in.atEnd()) { QString line = in.readLine().trimmed(); lineNum++; if (line.isEmpty() || line.startsWith('#')) continue; if (line.startsWith('[') && line.endsWith(']')) { section = line.mid(1, line.length() - 2); sectionValid = isSectionValid(section); debugSection = (section == "debug"); if (!sectionValid) { m_errors.append(QString("line %1: unknown section [%2]") .arg(lineNum).arg(section)); } continue; } if (!sectionValid) continue; // skip keys in unknown sections (already reported) int eq = line.indexOf('='); if (eq < 0) continue; QString key = line.left(eq).trimmed(); QString val = line.mid(eq + 1).trimmed(); // Validate key unless [debug] section (free-form category=level) if (!debugSection) { const QSet* validKeys = keysForSection(section); if (validKeys && !validKeys->contains(key)) { QString knownList; for (const auto& k : *validKeys) { if (!knownList.isEmpty()) knownList += ", "; knownList += k; } m_errors.append(QString("line %1: [%2] unknown option '%3'. Valid: %4") .arg(lineNum).arg(section.isEmpty() ? "global" : section) .arg(key).arg(knownList)); } } // Value validation for known keys if (!debugSection && key == "debug_level" && !val.isEmpty()) { if (!VALID_DEBUG_LEVELS.contains(val.toLower())) { m_errors.append(QString("line %1: [%2] debug_level='%3' is invalid. Valid: %4") .arg(lineNum).arg(section.isEmpty() ? "global" : section) .arg(val).arg(VALID_DEBUG_LEVELS.join(", "))); } } if ((key == "port" || key == "control_port") && !val.isEmpty()) { bool ok; int p = val.toInt(&ok); if (!ok || p < 1 || p > 65535) { m_errors.append(QString("line %1: [%2] %3='%4' is invalid (must be 1-65535)") .arg(lineNum).arg(section).arg(key).arg(val)); } } if ((key == "my_public_key" || key == "my_private_key" || key == "peer_public_key") && !val.isEmpty() && val.length() != 64) { m_errors.append(QString("line %1: [%2] %3 must be 64 hex chars (got %4)") .arg(lineNum).arg(section.isEmpty() ? "global" : section) .arg(key).arg(val.length())); } if (key == "my_node_id" && !val.isEmpty() && val.length() != 16) { m_errors.append(QString("line %1: [%2] my_node_id must be 16 hex chars (got %3)") .arg(lineNum).arg(section.isEmpty() ? "global" : section) .arg(val.length())); } // --- store known values (unchanged) --- if (section == "global" || section.isEmpty()) { if (key == "my_node_name") m_nodeName = val; else if (key == "my_node_id") m_nodeId = val; else if (key == "my_public_key") m_pubKey = val; else if (key == "my_private_key") m_privKey = val; else if (key == "auto_sockets") m_autoSockets = (val.compare("yes", Qt::CaseInsensitive) == 0 || val == "1"); } else if (section.startsWith("server:")) { QString srvName = section.mid(7); if (key == "addr") m_servers.append({srvName, val}); } else if (section.startsWith("client:")) { QString cliName = section.mid(7); if (key == "peer_public_key") { NodeClient nc; nc.name = cliName; nc.pubKeyHex = val; m_clients.append(nc); } else if (key == "link") { if (!m_clients.isEmpty() && m_clients.last().name == cliName) { int sep1 = val.indexOf(':'); int sep2 = val.lastIndexOf(':'); if (sep1 > 0 && sep2 > sep1) { m_clients.last().serverName = val.left(sep1); m_clients.last().remoteAddr = val.mid(sep1 + 1); } } } } else if (section == "control") { if (key == "ip" || key == "control_ip") controlIp = val; else if (key == "port" || key == "control_port") controlPort = val; } else if (section == "gui") { if (key == "db_path") m_dbPath = val; else if (key == "debug_file") m_debugFile = val; else if (key == "debug_level") m_debugLevel = val; } else if (debugSection) { if (!m_debugCategories.isEmpty()) m_debugCategories += ','; m_debugCategories += key + '=' + val; } } f.close(); if (!controlIp.isEmpty() && !controlPort.isEmpty()) m_controlAddr = controlIp + ":" + controlPort; return true; } bool NodeConfig::saveFull() { QFile f(m_path); if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) return false; QTextStream out(&f); out << "[global]\n"; if (!m_nodeName.isEmpty()) out << "my_node_name=" << m_nodeName << "\n"; if (!m_nodeId.isEmpty()) out << "my_node_id=" << m_nodeId << "\n"; if (!m_privKey.isEmpty()) out << "my_private_key=" << m_privKey << "\n"; if (!m_pubKey.isEmpty()) out << "my_public_key=" << m_pubKey << "\n"; out << "auto_sockets=" << (m_autoSockets ? "yes" : "no") << "\n"; out << "\n"; for (auto& srv : m_servers) { out << "[server:" << srv.first << "]\n"; out << "addr=" << srv.second << "\n"; out << "type=public\n\n"; } for (auto& cli : m_clients) { out << "[client:" << cli.name << "]\n"; out << "peer_public_key=" << cli.pubKeyHex << "\n"; out << "link=" << cli.serverName << ":" << cli.remoteAddr << "\n\n"; } if (!m_controlAddr.isEmpty()) { out << "[control]\n"; int sep = m_controlAddr.lastIndexOf(':'); if (sep > 0) { out << "ip=" << m_controlAddr.left(sep) << "\n"; out << "port=" << m_controlAddr.mid(sep + 1) << "\n\n"; } } out << "[gui]\n"; out << "db_path=" << (m_dbPath.isEmpty() ? "chat_data" : m_dbPath) << "\n"; out << "debug_file=" << m_debugFile << "\n"; out << "debug_level=" << (m_debugLevel.isEmpty() ? "info" : m_debugLevel) << "\n\n"; out << "[debug]\n"; for (const auto& h : DEBUG_CATEGORY_HINTS) out << '#' << h.name << '=' << h.level << '\n'; for (const QString& d : m_debugCategories.split(',', Qt::SkipEmptyParts)) { int eq = d.indexOf('='); if (eq > 0) out << d.left(eq).trimmed() << '=' << d.mid(eq + 1).trimmed() << '\n'; } out << '\n'; out << "[allowed_keys]\n"; out << "allow_all=yes\n\n"; out << "[ntp]\n"; out << "enabled=yes\n"; out << "server=pool.ntp.org\n"; out << "server=time.google.com\n"; out << "server=time.cloudflare.com\n"; out << "server=time.windows.com\n"; out << "server=ntp1.vniiftri.ru\n"; out << "server=ntp2.vniiftri.ru\n"; out << "interval=3600\n\n"; f.close(); return true; } bool NodeConfig::save() { if (!QFileInfo::exists(m_path)) return saveFull(); QFile f(m_path); if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) return false; QStringList lines; QTextStream in(&f); while (!in.atEnd()) { QString line = in.readLine(0); if (line.endsWith('\r')) line.chop(1); lines.append(line); } f.close(); config_update_in_lines(lines, "global", "my_node_name", m_nodeName, true); config_update_in_lines(lines, "global", "my_node_id", m_nodeId, true); config_update_in_lines(lines, "global", "my_private_key", m_privKey, true); config_update_in_lines(lines, "global", "my_public_key", m_pubKey, true); config_update_in_lines(lines, "global", "auto_sockets", m_autoSockets ? "yes" : "no", true); if (!m_controlAddr.isEmpty()) { int sep = m_controlAddr.lastIndexOf(':'); if (sep > 0) { config_update_in_lines(lines, "control", "ip", m_controlAddr.left(sep), true); config_update_in_lines(lines, "control", "port", m_controlAddr.mid(sep + 1), true); } } config_update_in_lines(lines, "gui", "db_path", m_dbPath.isEmpty() ? "chat_data" : m_dbPath, true); config_update_in_lines(lines, "gui", "debug_file", m_debugFile, true); config_update_in_lines(lines, "gui", "debug_level", m_debugLevel.isEmpty() ? "info" : m_debugLevel, true); /* [debug] section: each category=level as separate line */ QStringList dbg = m_debugCategories.split(',', Qt::SkipEmptyParts); for (const QString& d : dbg) { int eq = d.indexOf('='); if (eq > 0) config_update_in_lines(lines, "debug", d.left(eq).trimmed(), d.mid(eq + 1).trimmed(), true); } if (!f.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) return false; QTextStream out(&f); for (int i = 0; i < lines.size(); i++) out << lines[i] << '\n'; f.close(); return true; } uint64_t NodeConfig::nodeIdU64() const { if (m_nodeId.length() != 16) return 0; return m_nodeId.toULongLong(nullptr, 16); } quint16 NodeConfig::controlPort() const { if (m_controlAddr.isEmpty()) return 0; int sep = m_controlAddr.lastIndexOf(':'); if (sep < 0) return 0; return static_cast(m_controlAddr.mid(sep + 1).toUInt()); } void NodeConfig::setControlPort(quint16 p) { int sep = m_controlAddr.lastIndexOf(':'); if (sep >= 0) m_controlAddr = m_controlAddr.left(sep) + ":" + QString::number(p); else m_controlAddr = "127.0.0.1:" + QString::number(p); }