You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
484 lines
18 KiB
484 lines
18 KiB
// node_config.cpp — Simplified INI config manager for embedded uTun node |
|
#include "node_config.h" |
|
#include "config_updater.h" |
|
#include <QHostInfo> |
|
|
|
extern "C" { |
|
#include <openssl/evp.h> |
|
} |
|
#include <QFile> |
|
#include <QTextStream> |
|
#include <QFileInfo> |
|
#include <QSet> |
|
|
|
// ===================================================================== |
|
// Whitelists — union of chatgui + uTun config_parser keys |
|
// ===================================================================== |
|
|
|
static const QSet<QString> 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<QString> SERVER_KEYS = { |
|
"addr", "so_mark", "fib", "netif", "type", "mtu", "only_local", "transport" |
|
}; |
|
|
|
static const QSet<QString> CLIENT_KEYS = { |
|
"peer_public_key", "link", "keepalive", |
|
"reality", "server_name", "short_id", "public_key", "version", "fingerprint" |
|
}; |
|
|
|
static const QSet<QString> CONTROL_KEYS = { |
|
"ip", "control_ip", "port", "control_port", "allow", "control_allow" |
|
}; |
|
|
|
static const QSet<QString> GUI_KEYS = { |
|
"db_path", "debug_file", "debug_level", |
|
"sound_dir", "sound_msg_incoming", "sound_msg_outgoing", |
|
"sound_connect", "sound_disconnect" |
|
}; |
|
|
|
static const QSet<QString> ALLOWED_KEYS_KEYS = { |
|
"allow_all", "key" |
|
}; |
|
|
|
static const QSet<QString> NAT_KEYS = { |
|
"enabled", "tun_ifname", "tun_ip", "nat_via", "port_start", "port_end", "forward" |
|
}; |
|
|
|
static const QSet<QString> FIREWALL_KEYS = { |
|
"allow" |
|
}; |
|
|
|
static const QSet<QString> ROUTING_KEYS = { |
|
"route_subnet", "my_subnet" |
|
}; |
|
|
|
static const QSet<QString> 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<QString> TCP_PROXY_SERVER_KEYS = { |
|
"enabled", "tcp_recv_buf" |
|
}; |
|
|
|
static const QSet<QString> NETWORK_KEYS = { |
|
"id", "pubkey", "signing_key" |
|
}; |
|
|
|
static const QSet<QString> 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; |
|
if (section == "chat" || section == "chatserver") return true; |
|
return false; |
|
} |
|
|
|
static const QSet<QString>* 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<const char*>(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<QString>* 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<quint16>(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); |
|
}
|
|
|