From 0c2eeb88206b385b4a93f9da9b2d5c0e946f4100 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Mon, 13 Jul 2026 15:46:20 +0300 Subject: [PATCH] chatgui: validate config options on startup, refuse to start on errors --- tools/chatgui/src/main.cpp | 6 + tools/chatgui/transport/node_config.cpp | 168 +++++++++++++++++++++++- tools/chatgui/transport/node_config.h | 6 + 3 files changed, 178 insertions(+), 2 deletions(-) diff --git a/tools/chatgui/src/main.cpp b/tools/chatgui/src/main.cpp index da76266a..cc77851d 100644 --- a/tools/chatgui/src/main.cpp +++ b/tools/chatgui/src/main.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include "mainwindow.h" #include "../db/db_manager.h" #include "../transport/node_config.h" @@ -45,6 +46,11 @@ int main(int argc, char *argv[]) { qCritical("Cannot open/read config file: %s", qPrintable(cfgPath)); return 1; } + if (cfg.hasErrors()) { + for (const auto& err : cfg.errors()) + fprintf(stderr, "CONFIG ERROR: %s\n", qPrintable(err)); + return 1; + } if (cfg.privKey().length() != 64 || cfg.pubKey().length() != 64) { qWarning("Config: keys missing/invalid, regenerating identity"); cfg.generateIdentity(); diff --git a/tools/chatgui/transport/node_config.cpp b/tools/chatgui/transport/node_config.cpp index 08d77fd4..62dfd581 100644 --- a/tools/chatgui/transport/node_config.cpp +++ b/tools/chatgui/transport/node_config.cpp @@ -8,6 +8,109 @@ extern "C" { #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", + "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", "debug_categories" +}; + +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 MSG_TRANSPORT_KEYS = { + "port" +}; + +static const QSet NETWORK_KEYS = { + "id", "pubkey", "signing_key" +}; + +static const QStringList VALID_DEBUG_LEVELS = { + "none", "error", "warn", "info", "debug", "trace" +}; + +// 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 == "msg_transport") 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 == "msg_transport") return &MSG_TRANSPORT_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(); @@ -76,24 +179,86 @@ bool NodeConfig::load() { 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(); - if (section == "global") { + // 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; @@ -110,7 +275,6 @@ bool NodeConfig::load() { m_clients.append(nc); } else if (key == "link") { if (!m_clients.isEmpty() && m_clients.last().name == cliName) { - // link=server:addr:port int sep1 = val.indexOf(':'); int sep2 = val.lastIndexOf(':'); if (sep1 > 0 && sep2 > sep1) { diff --git a/tools/chatgui/transport/node_config.h b/tools/chatgui/transport/node_config.h index aef29291..358f2b09 100644 --- a/tools/chatgui/transport/node_config.h +++ b/tools/chatgui/transport/node_config.h @@ -3,6 +3,7 @@ #define NODE_CONFIG_H #include +#include #include #include @@ -51,6 +52,10 @@ public: QString debugLevel() const { return m_debugLevel; } QString debugCategories() const { return m_debugCategories; } + /* Config validation errors (collected during load()) */ + bool hasErrors() const { return !m_errors.isEmpty(); } + const QStringList& errors() const { return m_errors; } + /* Identity generation (public for fixing invalid keys) */ void generateIdentity(); @@ -68,5 +73,6 @@ private: QString m_debugFile; QString m_debugLevel; QString m_debugCategories; + QStringList m_errors; }; #endif // NODE_CONFIG_H