/* Реальный desktop-генератор и общий строгий парсер; --validate проверяет также конфиги Android. */ #include "../transport/node_config.h" #include "../transport/config_updater.h" #include #include #include #include #ifdef Q_OS_UNIX #include #include #endif extern "C" { #include "config_parser.h" #include "chat/chat_setting.h" #include "socket_compat.h" #include "debug_config.h" #include "../../../src/config_updater.h" } #ifdef UTUN_TEST_NODE_CONFIG_FAILURES static bool failKeyGeneration = false, failNodeId = false; extern "C" sc_status_t __real_sc_generate_keypair(struct SC_MYKEYS *keys); extern "C" sc_status_t __wrap_sc_generate_keypair(struct SC_MYKEYS *keys) { return failKeyGeneration ? SC_ERR_CRYPTO : __real_sc_generate_keypair(keys); } extern "C" uint64_t __real_sc_derive_node_id_from_pubkey(const uint8_t *publicKey); extern "C" uint64_t __wrap_sc_derive_node_id_from_pubkey(const uint8_t *publicKey) { return failNodeId ? 0 : __real_sc_derive_node_id_from_pubkey(publicKey); } #endif using Config = std::unique_ptr; class NodeConfigTest : public QObject { Q_OBJECT private slots: void identityFailureKeepsPreviousValues_data() { QTest::addColumn("nodeIdFailure"); QTest::newRow("keypair") << false; QTest::newRow("node-id") << true; } void identityFailureKeepsPreviousValues() { #ifdef UTUN_TEST_NODE_CONFIG_FAILURES QFETCH(bool, nodeIdFailure); QTemporaryDir directory; QVERIFY(directory.isValid()); NodeConfig config(directory.filePath("identity.cfg")); QVERIFY(config.create("Original")); const QString privateKey = config.privKey(), publicKey = config.pubKey(), id = config.nodeId(); QScopedValueRollback failure(nodeIdFailure ? failNodeId : failKeyGeneration, true); QVERIFY(!config.generateIdentity()); QCOMPARE(config.privKey(), privateKey); QCOMPARE(config.pubKey(), publicKey); QCOMPARE(config.nodeId(), id); NodeConfig fresh(directory.filePath("failed.cfg")); QVERIFY(!fresh.create("Failure")); QVERIFY(!fresh.exists()); #else QSKIP("Linker fault injection is available on Linux"); #endif } void repairKeepsExistingPrivateKey() { QTemporaryDir directory; QVERIFY(directory.isValid()); const QString path = directory.filePath("repair.cfg"); NodeConfig config(path); QVERIFY(config.create("Original")); const QString privateKey = config.privKey(), publicKey = config.pubKey(), id = config.nodeId(); QVERIFY(config_update_option(path, "global", "my_node_id", "")); QVERIFY(config_update_option(path, "global", "my_public_key", "")); QCOMPARE(config_ensure_keys_and_node_id(path.toUtf8().constData()), 0); QVERIFY(config.load()); QVERIFY(!config.hasErrors()); QCOMPARE(config.privKey(), privateKey); QCOMPARE(config.pubKey(), publicKey); QCOMPARE(config.nodeId(), id); } void zeroIpv6ControlPortAndAndroidModeRoundTrip() { QTemporaryDir directory; QVERIFY(directory.isValid()); const QString path = directory.filePath("control.cfg"); NodeConfig config(path); QVERIFY(config.create("Control")); QVERIFY(config_update_option(path, "global", "auto_sockets", "android")); QVERIFY(config_update_option(path, "control", "control_ip", "::1")); QVERIFY(config_update_option(path, "control", "control_port", "0")); QVERIFY(config.load()); QVERIFY(!config.hasErrors()); QCOMPARE(config.controlAddr(), QString("[::1]:0")); QCOMPARE(config.controlPort(), quint16(0)); QVERIFY(config.autoSockets()); QVERIFY(config.save()); Config parsed(parse_config(qPrintable(path)), free_config); QVERIFY(parsed); QCOMPARE(parsed->global.auto_sockets, CFG_AUTO_SOCKETS_ANDROID); QCOMPARE(parsed->global.control_sock.ss_family, AF_INET6); config.setControlPort(9999); QVERIFY(config.save()); QVERIFY(config.load()); QCOMPARE(config.controlAddr(), QString("[::1]:9999")); } void failedLoadKeepsPreviousProjection() { QTemporaryDir directory; QVERIFY(directory.isValid()); const QString path = directory.filePath("missing.cfg"); NodeConfig config(path); QVERIFY(config.create("Previous")); QVERIFY(QFile::remove(path)); QVERIFY(!config.load()); QCOMPARE(config.nodeName(), QString("Previous")); QVERIFY(config.nodeIdU64() != 0); } void lastEmptyIdentityAndInvalidLanguageReplacePreviousValues() { QTemporaryDir directory; QVERIFY(directory.isValid()); const QString path = directory.filePath("last.cfg"); NodeConfig config(path); QVERIFY(config.create("Previous")); QFile file(path); QVERIFY(file.open(QIODevice::Append)); const QByteArray tail("[global]\nmy_node_id=\n[gui]\nlanguage=ru\nlanguage=invalid\n"); QCOMPARE(file.write(tail), qint64(tail.size())); file.close(); QVERIFY(config.load()); QVERIFY(config.nodeId().isEmpty()); QCOMPARE(config.language(), QString("system")); } void failedWriteKeepsOriginalFile() { #ifdef Q_OS_UNIX QTemporaryDir directory; QVERIFY(directory.isValid()); const QString path = directory.filePath("write.cfg"); NodeConfig config(path); QVERIFY(config.create("Previous")); QFile file(path); QVERIFY(file.open(QIODevice::ReadOnly)); const auto original = file.readAll(); file.close(); config.setNodeName("Unsaved"); bool saved; { struct LimitGuard { struct rlimit previous{}; using Handler = void (*)(int); Handler handler = SIG_DFL; bool active = false; ~LimitGuard() { if (active) { setrlimit(RLIMIT_FSIZE, &previous); std::signal(SIGXFSZ, handler); } } } guard; QVERIFY(getrlimit(RLIMIT_FSIZE, &guard.previous) == 0); struct rlimit limited = guard.previous; limited.rlim_cur = 64; guard.handler = std::signal(SIGXFSZ, SIG_IGN); guard.active = true; QVERIFY(setrlimit(RLIMIT_FSIZE, &limited) == 0); saved = config.save(); } QVERIFY(!saved); QVERIFY(file.open(QIODevice::ReadOnly)); QCOMPARE(file.readAll(), original); #else QSKIP("RLIMIT_FSIZE is available on Unix"); #endif } void reloadResetsRemovedFields() { QTemporaryDir directory; QVERIFY(directory.isValid()); const QString path = directory.filePath("reload.cfg"); NodeConfig config(path); QVERIFY(config.create("Previous profile")); QVERIFY(config.load()); QFile file(path); QVERIFY(file.open(QIODevice::WriteOnly)); const QByteArray minimal("[global]\nauto_socket_protocols=udp\n[gui]\nlanguage=en\n"); QCOMPARE(file.write(minimal), qint64(minimal.size())); file.close(); QVERIFY(config.load()); QVERIFY(!config.hasErrors()); QVERIFY(config.nodeName().isEmpty() && config.nodeId().isEmpty()); QVERIFY(config.pubKey().isEmpty() && config.privKey().isEmpty()); QVERIFY(config.controlAddr().isEmpty() && config.dbPath().isEmpty() && config.debugFile().isEmpty()); QVERIFY(config.debugLevel().isEmpty() && !config.autoSockets()); QCOMPARE(config.autoSocketProtocols(), QString("udp")); QCOMPARE(config.language(), QString("en")); } void guiProjectionAcceptsNativeSettingsAndComments() { QTemporaryDir directory; QVERIFY(directory.isValid()); const QString path = directory.filePath("native.cfg"); NodeConfig config(path); QVERIFY(config.create("Native options")); QFile file(path); QVERIFY(file.open(QIODevice::Append)); const QByteArray options("[ GLOBAL ]\nclient_type=1\nauto_socket_skip_no_default_route=no\n" "my_node_name=Commented # note\nauto_sockets=true # enabled\n" "[reality]\nenabled=no\n[socks]\nenabled=no\n[stun]\nenabled=no\n[log_udp]\nip=127.0.0.1\nport=9\n"); QCOMPARE(file.write(options), qint64(options.size())); file.close(); Config parsed(parse_config(qPrintable(path)), free_config); QVERIFY(parsed); QVERIFY(config.load()); QVERIFY2(!config.hasErrors(), qPrintable(config.errors().join('\n'))); QCOMPARE(config.nodeName(), QString("Commented")); QVERIFY(config.autoSockets()); QVERIFY(config.save()); QVERIFY(file.open(QIODevice::ReadOnly)); const auto saved = file.readAll(); QVERIFY(saved.contains("[reality]\nenabled=no\n[socks]\nenabled=no\n[stun]\nenabled=no")); QVERIFY(saved.contains("client_type=1\nauto_socket_skip_no_default_route=no")); } void rejectsNonHexIdentity() { QTemporaryDir directory; QVERIFY(directory.isValid()); const QString path = directory.filePath("identity.cfg"); NodeConfig config(path); QVERIFY(config.create("Identity")); QVERIFY(config_update_option(path, "global", "my_private_key", QString(64, 'z'))); QVERIFY(config.load()); QVERIFY(config.hasErrors()); } void rejectedEditDoesNotPublishPartialChanges() { QTemporaryDir directory; QVERIFY(directory.isValid()); const QString path = directory.filePath("edit.cfg"); NodeConfig config(path); QVERIFY(config.create("Original")); QFile file(path); QVERIFY(file.open(QIODevice::ReadOnly)); const auto original = file.readAll(); file.close(); config.setNodeName("Injected\n[gui]"); config.setLanguage("ru"); QVERIFY(!config.save()); QVERIFY(file.open(QIODevice::ReadOnly)); QCOMPARE(file.readAll(), original); } void generatedConfigurations_data() { QTest::addColumn("protocols"); for (const QString& protocols : {QString("tcp"), QString("udp"), QString("tcp+udp")}) QTest::newRow(qPrintable(protocols)) << protocols; } void generatedConfigurations() { QFETCH(QString, protocols); QTemporaryDir directory; QVERIFY(directory.isValid()); const QString path = directory.filePath("vibechat.cfg"); NodeConfig config(path); QVERIFY(config.create(QString::fromUtf8("Тест конфигурации"))); config.setAutoSocketProtocols(protocols); for (bool utun : {false, true}) { config.setUtunEnabled(utun); for (int pass = 0; pass < 3; ++pass) { QVERIFY(config.save()); QVERIFY(config.load()); QVERIFY2(!config.hasErrors(), qPrintable(config.errors().join('\n'))); Config parsed(parse_config(qPrintable(path)), free_config); QVERIFY(parsed); QCOMPARE(QString::fromUtf8(parsed->global.name), config.nodeName()); QCOMPARE(QString::fromLatin1(parsed->global.my_public_key_hex), config.pubKey()); QCOMPARE(QString::fromLatin1(parsed->global.my_private_key_hex), config.privKey()); QCOMPARE(parsed->global.gui_utun_enabled, int(utun)); QCOMPARE(parsed->global.auto_sockets, 1); const int expected = protocols == "tcp" ? CFG_AUTO_SOCKET_TCP : protocols == "udp" ? CFG_AUTO_SOCKET_UDP : CFG_AUTO_SOCKET_BOTH; QCOMPARE(int(parsed->global.auto_socket_protocols), expected); } } } void settingsUpdatesPreserveValidConfiguration() { QTemporaryDir directory; QVERIFY(directory.isValid()); const QString path = directory.filePath("settings.cfg"); NodeConfig config(path); QVERIFY(config.create("Settings test")); QVERIFY(config_rewrite_servers(path, {{"tcp", "127.0.0.1:1333", "tcp", "private", ""}, {"udp", "[::1]:1333", "udp", "local", ""}})); QVERIFY(config_update_option(path, "chatserver", "storage_unit_size", "23M")); QVERIFY(config_update_option(path, "chatserver", "storage_total_size", "7G")); QVERIFY(config_update_option(path, "chatserver", "radio_auto_enabled", "1")); QVERIFY(config_update_option(path, "chatserver", "radio_auto_mode", "2")); QVERIFY(config_update_option(path, "client:seed", "fingerprint", "openssl")); QVERIFY(config_update_option(path, "client:seed", "peer_public_key", config.pubKey())); QVERIFY(config_update_option(path, "client:seed", "link", "tcp:127.0.0.2:443")); QVERIFY(config.load()); QVERIFY2(!config.hasErrors(), qPrintable(config.errors().join('\n'))); QVERIFY(config.save()); Config parsed(parse_config(qPrintable(path)), free_config); QVERIFY(parsed); QCOMPARE(parsed->global.chatserver_storage_unit_size, UINT64_C(23) * 1024 * 1024); QCOMPARE(parsed->global.chatserver_storage_total_size, UINT64_C(7) * 1024 * 1024 * 1024); QCOMPARE(chat_setting_get_int_state(&parsed->global.chat_settings, "radio_auto_mode", 0), 2); QVERIFY(parsed->clients && parsed->clients->links && parsed->servers && parsed->servers->next); } void repeatedLoadDoesNotDuplicateDebugOptions() { QTemporaryDir directory; QVERIFY(directory.isValid()); const QString path = directory.filePath("debug.cfg"); NodeConfig config(path); QVERIFY(config.create("Debug settings")); QVERIFY(config_update_option(path, "debug", "stream", "debug")); for (int attempt = 0; attempt < 3; ++attempt) { QVERIFY(config.load()); QCOMPARE(config.debugCategories(), QString("stream=debug")); QVERIFY(config.save()); Config parsed(parse_config(qPrintable(path)), free_config); QVERIFY(parsed); } } }; int main(int argc, char** argv) { QCoreApplication app(argc, argv); debug_config_init(); debug_set_level(DEBUG_LEVEL_INFO); if (socket_platform_init()) return 1; int result = 0; if (argc >= 3 && !strcmp(argv[1], "--validate")) { for (int i = 2; i < argc; ++i) { Config parsed(parse_config(argv[i]), free_config); if (!parsed) { result = 1; continue; } DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Config accepted: %s storage_unit=%llu storage_total=%llu", argv[i], (unsigned long long)parsed->global.chatserver_storage_unit_size, (unsigned long long)parsed->global.chatserver_storage_total_size); } } else { NodeConfigTest test; result = QTest::qExec(&test, argc, argv); } socket_platform_cleanup(); return result; } #include "test_node_config.moc"