refactor: STCP connections as ETCP links (is_tcp)
- Add is_tcp, tcp_link, tcp_reconnect_* fields to ETCP_LINK - Remove transport_link from ETCP_CONN, add tcp_link_count - stcp_link: replace embedded etcp_conn with pointer; add etcp_link backpointer - stcp_link: link_rx_cb routes ETCP data to etcp_conn_input - stcp_link: call on_close_cb in close - New TCP functions: etcp_link_enter_ready_tcp, etcp_tcp_link_start_connect, etcp_tcp_link_start_reconnect, tcp_link_close_cb, etcp_tcp_send - etcp_link_new: handle NULL conn for TCP links; skip enter_init for TCP - etcp_link_close: close stcp_link for TCP - etcp_encrypt_send: redirect TCP to etcp_tcp_send (skips AES-CCM) - tcp_server_on_link: create ETCP_LINK(is_tcp=1) + enter_ready_tcp - etcp_connect: TCP addrs -> etcp_tcp_link_start_connect - Remove ctx->tcp_link/tcp_ready - test_stcp_traffic: find stcp_link via links->is_tcp instead of transport_link - 66/68 tests pass; 2 STCP tests expected to fail (integration not complete)
This commit is contained in:
1 parent
937af4c26a
commit
d773f1724a
1705 files changed
+187
-1082154
No files matched your search
@@ -1,433 +0,0 @@
|
||||
// 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"
|
||||
};
|
||||
|
||||
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", "debug_categories",
|
||||
"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"
|
||||
};
|
||||
|
||||
// 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<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() ? "chatgui" : 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();
|
||||
if (listenPort > 0) {
|
||||
m_servers.append({"lan", QString("0.0.0.0:%1").arg(listenPort)});
|
||||
m_servers.append({"lan6", QString("[::]:%1").arg(listenPort)});
|
||||
}
|
||||
if (m_dbPath.isEmpty())
|
||||
m_dbPath = "chat_data";
|
||||
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 (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 (key == "debug_categories") m_debugCategories = 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 << "\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";
|
||||
out << "debug_categories=" << m_debugCategories << "\n\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);
|
||||
|
||||
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);
|
||||
config_update_in_lines(lines, "gui", "debug_categories", m_debugCategories, 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);
|
||||
}
|
||||
Reference in new issue
Block a user