diff --git a/deps/Makefile b/deps/Makefile index bfb9421de..29be815d9 100644 --- a/deps/Makefile +++ b/deps/Makefile @@ -370,6 +370,7 @@ postgresql/postgresql/src/interfaces/libpq/libpq.a: cd postgresql/postgresql && patch -p0 < ../fmt_err_msg.patch cd postgresql/postgresql && patch -p0 < ../bind_fmt_text.patch cd postgresql/postgresql && patch -p0 < ../pqsendpipelinesync.patch + cd postgresql/postgresql && patch -p0 < ../sslkeylogfile.patch ifeq ($(UNAME_S),Darwin) cd postgresql/postgresql && LDFLAGS="-L$$(brew --prefix icu4c)/lib" CPPFLAGS="-I$$(brew --prefix icu4c)/include" PKG_CONFIG_PATH="$$(brew --prefix icu4c)/lib/pkgconfig:$$PKG_CONFIG_PATH" DYLD_LIBRARY_PATH="$(SSL_LDIR):$$DYLD_LIBRARY_PATH" ./configure --with-ssl=openssl --with-includes="$(SSL_IDIR)" --with-libraries="$(SSL_LDIR)" --without-readline --with-icu else diff --git a/deps/postgresql/sslkeylogfile.patch b/deps/postgresql/sslkeylogfile.patch new file mode 100644 index 000000000..5c4752b6e --- /dev/null +++ b/deps/postgresql/sslkeylogfile.patch @@ -0,0 +1,63 @@ +diff -ruN ../tmp/src/interfaces/libpq/fe-secure-openssl.c ./src/interfaces/libpq/fe-secure-openssl.c +--- ../tmp/src/interfaces/libpq/fe-secure-openssl.c 2025-08-11 21:06:43.000000000 +0000 ++++ ./src/interfaces/libpq/fe-secure-openssl.c 2026-04-03 00:00:00.000000000 +0000 +@@ -97,6 +97,8 @@ + + static PQsslKeyPassHook_OpenSSL_type PQsslKeyPassHook = NULL; + static int ssl_protocol_version_to_openssl(const char *protocol); ++ ++static PQsslKeyLogCallback_type PQsslKeyLogCB = NULL; + + /* ------------------------------------------------------------ */ + /* Procedures common to all secure sessions */ +@@ -972,6 +974,10 @@ + /* Disable old protocol versions */ + SSL_CTX_set_options(SSL_context, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3); + ++ /* Set SSL keylog callback if configured (for TLS traffic decryption) */ ++ if (PQsslKeyLogCB) ++ SSL_CTX_set_keylog_callback(SSL_context, (void(*)(const SSL*, const char*))PQsslKeyLogCB); ++ + /* Set the minimum and maximum protocol versions if necessary */ + if (conn->ssl_min_protocol_version && + strlen(conn->ssl_min_protocol_version) != 0) +@@ -1758,6 +1764,24 @@ + return NULL; + } + ++/* ++ * SSL Key Log callback support ++ * ++ * Global callback for writing TLS secrets to a keylog file. ++ * Follows the same pattern as PQsslKeyPassHook. ++ */ ++PQsslKeyLogCallback_type ++PQgetSSLKeyLogCallback(void) ++{ ++ return PQsslKeyLogCB; ++} ++ ++void ++PQsetSSLKeyLogCallback(PQsslKeyLogCallback_type cb) ++{ ++ PQsslKeyLogCB = cb; ++} ++ + const char *const * + PQsslAttributeNames(PGconn *conn) + { +diff -ruN ../tmp/src/interfaces/libpq/libpq-fe.h ./src/interfaces/libpq/libpq-fe.h +--- ../tmp/src/interfaces/libpq/libpq-fe.h 2025-08-11 21:06:43.000000000 +0000 ++++ ./src/interfaces/libpq/libpq-fe.h 2026-04-03 00:00:00.000000000 +0000 +@@ -669,6 +669,11 @@ + extern void PQsetSSLKeyPassHook_OpenSSL(PQsslKeyPassHook_OpenSSL_type hook); + extern int PQdefaultSSLKeyPassHook_OpenSSL(char *buf, int size, PGconn *conn); + ++/* Support for SSL key log callback (TLS traffic decryption) */ ++typedef void (*PQsslKeyLogCallback_type)(const void *ssl, const char *line); ++extern PQsslKeyLogCallback_type PQgetSSLKeyLogCallback(void); ++extern void PQsetSSLKeyLogCallback(PQsslKeyLogCallback_type cb); ++ + #ifdef __cplusplus + } + #endif diff --git a/doc/ssl_keylog/ssl_keylog_user_guide.md b/doc/ssl_keylog/ssl_keylog_user_guide.md index 92282428e..e050e244d 100644 --- a/doc/ssl_keylog/ssl_keylog_user_guide.md +++ b/doc/ssl_keylog/ssl_keylog_user_guide.md @@ -8,8 +8,8 @@ SSL/TLS key logging is a debugging feature that allows ProxySQL to write TLS enc This feature is primarily useful for: -- **Debugging TLS connection issues** between clients and ProxySQL -- **Analyzing encrypted traffic** without modifying application code +- **Debugging TLS connection issues** between clients and ProxySQL, or between ProxySQL and MySQL/PostgreSQL backends +- **Analyzing encrypted backend traffic** to MySQL and PostgreSQL servers without modifying application code - **Troubleshooting TLS handshake problems** - **Performance analysis** of TLS connections - **Security auditing** of TLS configurations @@ -222,11 +222,16 @@ In production environments, you typically don't run Wireshark directly on the se On the ProxySQL server, capture network traffic to a pcap file: ```bash -# Capture on the interface ProxySQL is listening on (e.g., eth0) -# Replace 6033 with your ProxySQL MySQL port +# Capture MySQL frontend traffic (client → ProxySQL) sudo tcpdump -i eth0 -w /tmp/proxysql_debug.pcap port 6033 -# Or capture traffic between specific hosts +# Capture PgSQL frontend traffic (client → ProxySQL) +sudo tcpdump -i eth0 -w /tmp/proxysql_debug.pcap port 6133 + +# Capture PgSQL backend traffic (ProxySQL → PostgreSQL server) +sudo tcpdump -i eth0 -w /tmp/proxysql_debug.pcap port 5432 + +# Capture traffic between specific hosts sudo tcpdump -i eth0 -w /tmp/proxysql_debug.pcap host client_ip and host proxysql_ip # Run for a specific duration @@ -271,6 +276,9 @@ On your analysis system with Wireshark installed: # Show only MySQL packets mysql + # Show only PostgreSQL packets + pgsql + # Show TLS handshake tls.handshake.type == 1 @@ -407,11 +415,21 @@ mysql_variables= **Solutions:** 1. Verify TLS is actually being used: ```sql - -- Check if connections are using TLS + -- Check MySQL backend connections SELECT * FROM stats_mysql_connection_pool; + -- Check PgSQL backend connections + SELECT * FROM stats_pgsql_connection_pool; + ``` +2. For PgSQL backends, ensure `use_ssl=1` is set on the servers: + ```sql + SELECT hostgroup_id, hostname, port, use_ssl FROM pgsql_servers; + ``` +3. Keylog entries are only written during **new** SSL handshakes. Existing pooled connections won't generate entries. To force new handshakes, reload servers: + ```sql + LOAD PGSQL SERVERS TO RUNTIME; ``` -2. Make sure clients are connecting with SSL/TLS -3. Check that `admin-ssl_keylog_file` is loaded into runtime: +4. Make sure clients are connecting with SSL/TLS +5. Check that `admin-ssl_keylog_file` is loaded into runtime: ```sql LOAD ADMIN VARIABLES TO RUNTIME; ``` @@ -476,9 +494,26 @@ sudo tcpdump -i eth0 -w /tmp/capture.pcap port 6033 --- +## Supported Connection Types + +When `admin-ssl_keylog_file` is configured, TLS secrets are captured from **all** SSL/TLS connection types: + +| Connection Type | Direction | Protocol | +|----------------|-----------|----------| +| Frontend (client) | Client → ProxySQL | MySQL | +| Frontend (client) | Client → ProxySQL | PostgreSQL | +| Backend | ProxySQL → MySQL server | MySQL | +| Backend | ProxySQL → PostgreSQL server | PostgreSQL | +| Monitor | ProxySQL → MySQL server | MySQL | +| Monitor | ProxySQL → PostgreSQL server | PostgreSQL | +| Cluster | ProxySQL → ProxySQL peer | MySQL | + +No additional configuration is needed per connection type — the single `admin-ssl_keylog_file` variable enables logging for all types. + +--- + ## Additional Resources -- **Developer Documentation:** See `ssl_keylog_developer_guide.md` for implementation details - **NSS Key Log Format:** https://developer.mozilla.org/en-US/docs/Mozilla/Projects/NSS/Key_Log_Format - **Wireshark TLS Decryption:** https://wiki.wireshark.org/TLS - **tshark Manual:** `man tshark` or https://www.wireshark.org/docs/man-pages/tshark.html diff --git a/include/proxysql_sslkeylog.h b/include/proxysql_sslkeylog.h index 5ec1793b0..21c5d15e9 100644 --- a/include/proxysql_sslkeylog.h +++ b/include/proxysql_sslkeylog.h @@ -103,4 +103,19 @@ void proxysql_keylog_attach_callback(SSL_CTX* ssl_ctx); */ void proxysql_keylog_write_line_callback(const SSL* ssl, const char* line); +/** + * @brief Register the ProxySQL keylog callback with libpq + * + * Sets the global SSL keylog callback in libpq so that all PostgreSQL + * backend connections (regular, monitor, kill, harvester) will write + * TLS secrets to the keylog file. + * + * Must be called once at ProxySQL startup after proxysql_keylog_init(). + * + * Thread-safety: Safe (should only be called during single-threaded initialization) + * + * @see PQsetSSLKeyLogCallback + */ +void proxysql_keylog_set_pgsql_callback(); + #endif // __PROXYSQL_SSLKEYLOG_H diff --git a/lib/ProxySQL_GloVars.cpp b/lib/ProxySQL_GloVars.cpp index e6f271246..bd119cbc4 100644 --- a/lib/ProxySQL_GloVars.cpp +++ b/lib/ProxySQL_GloVars.cpp @@ -487,6 +487,7 @@ void ProxySQL_GlobalVariables::process_opts_pre() { init_coredump_struct(); proxysql_keylog_init(); + proxysql_keylog_set_pgsql_callback(); }; void ProxySQL_GlobalVariables::process_opts_post() { diff --git a/lib/proxysql_sslkeylog.cpp b/lib/proxysql_sslkeylog.cpp index 18281e950..7b4041dcd 100644 --- a/lib/proxysql_sslkeylog.cpp +++ b/lib/proxysql_sslkeylog.cpp @@ -23,6 +23,7 @@ */ #include "proxysql_sslkeylog.h" +#include "libpq-fe.h" // NSS Key Log Format reference: // https://developer.mozilla.org/en-US/docs/Mozilla/Projects/NSS/Key_Log_Format @@ -229,3 +230,19 @@ void proxysql_keylog_attach_callback(SSL_CTX* ssl_ctx) { SSL_CTX_set_keylog_callback(ssl_ctx, proxysql_keylog_write_line_callback); } } + +/** + * @brief Register the ProxySQL keylog callback with libpq + * + * Sets the global SSL keylog callback in libpq using PQsetSSLKeyLogCallback(). + * This ensures all PostgreSQL backend connections write TLS secrets to the + * keylog file, matching the behavior already implemented for MySQL backends + * via MARIADB_OPT_SSL_KEYLOG_CALLBACK. + * + * The callback is set globally (not per-connection) because libpq creates + * a new SSL_CTX per connection internally. A global callback ensures coverage + * for both async (PQconnectStart) and sync (PQconnectdb) connection paths. + */ +void proxysql_keylog_set_pgsql_callback() { + PQsetSSLKeyLogCallback((PQsslKeyLogCallback_type)proxysql_keylog_write_line_callback); +} diff --git a/test/tap/groups/groups.json b/test/tap/groups/groups.json index 285624a84..9e0523e8e 100644 --- a/test/tap/groups/groups.json +++ b/test/tap/groups/groups.json @@ -146,6 +146,7 @@ "pgsql-reg_test_5415_copy_error_recovery-t" : [ "legacy-g4","mysql-auto_increment_delay_multiplex=0-g4","mysql-multiplexing=false-g4","mysql-query_digests=0-g4","mysql-query_digests_keep_comment=1-g4" ], "pgsql-set_parameter_validation_test-t" : [ "legacy-g1","mysql-auto_increment_delay_multiplex=0-g1","mysql-multiplexing=false-g1","mysql-query_digests=0-g1","mysql-query_digests_keep_comment=1-g1" ], "pgsql-set_statement_test-t" : [ "legacy-g4","mysql-auto_increment_delay_multiplex=0-g4","mysql-multiplexing=false-g4","mysql-query_digests=0-g4","mysql-query_digests_keep_comment=1-g4" ], + "pgsql-ssl_keylog-t" : [ "legacy-g4","mysql-auto_increment_delay_multiplex=0-g4","mysql-multiplexing=false-g4","mysql-query_digests=0-g4","mysql-query_digests_keep_comment=1-g4" ], "pgsql-test_malformed_packet-t" : [ "legacy-g4","mysql-auto_increment_delay_multiplex=0-g4","mysql-multiplexing=false-g4","mysql-query_digests=0-g4","mysql-query_digests_keep_comment=1-g4" ], "pgsql-transaction_state_comprehensive-t" : [ "legacy-g4","mysql-auto_increment_delay_multiplex=0-g4","mysql-multiplexing=false-g4","mysql-query_digests=0-g4","mysql-query_digests_keep_comment=1-g4" ], "pgsql-transaction_variable_state_tracking-t" : [ "legacy-g4","mysql-auto_increment_delay_multiplex=0-g4","mysql-multiplexing=false-g4","mysql-query_digests=0-g4","mysql-query_digests_keep_comment=1-g4" ], diff --git a/test/tap/tests/pgsql-ssl_keylog-t.cpp b/test/tap/tests/pgsql-ssl_keylog-t.cpp new file mode 100644 index 000000000..89939a871 --- /dev/null +++ b/test/tap/tests/pgsql-ssl_keylog-t.cpp @@ -0,0 +1,458 @@ +/** + * @file pgsql-ssl_keylog-t.cpp + * @brief Integration test for PgSQL backend SSL keylog support. + * + * @details Validates that TLS secrets from PostgreSQL backend SSL connections + * are written to the keylog file when admin-ssl_keylog_file is set. + * + * ProxySQL patches libpq with a global SSL keylog callback + * (PQsetSSLKeyLogCallback). When admin-ssl_keylog_file is set, ALL + * TLS connections write secrets to the keylog file in NSS Key Log Format. + * + * Test cases: + * 1. Basic keylog creation — set admin-ssl_keylog_file, make PgSQL SSL connection, + * verify file is non-empty + * 2. NSS format validation — parse keylog lines, verify regex match for NSS Key Log Format + * 3. TLS version label check — verify keylog contains TLS 1.2 or 1.3 secret labels + * 4. Concurrent connections — 8 threads making PgSQL SSL connections, verify no corruption + * 5. Disable keylog — set empty, make new connections, verify no new lines + * 6. Log rotation — PROXYSQL FLUSH LOGS, verify new secrets appended + * 7. Monitor SSL keylog — enable use_ssl=1, wait for monitor, verify keylog entries + * 8. Non-SSL no keylog — use_ssl=0, verify no new entries + * 9. Invalid keylog path — set /nonexistent/dir/file, verify graceful handling + * + * Test infrastructure: docker-pgsql16-single (PgSQL backend with SSL enabled) + * Admin connection: PgSQL protocol on pgsql_admin_host:pgsql_admin_port + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "libpq-fe.h" +#include "command_line.h" +#include "tap.h" +#include "utils.h" + +CommandLine cl; + +using PGConnPtr = std::unique_ptr; + +static const char* KEYLOG_PATH = "/tmp/pgsql_ssl_keylog_test.log"; + +// ============================================================ +// Helper: open PgSQL admin connection +// ============================================================ +static PGConnPtr open_admin() { + std::stringstream ss; + ss << "host=" << cl.pgsql_admin_host + << " port=" << cl.pgsql_admin_port + << " user=" << cl.admin_username + << " password=" << cl.admin_password + << " sslmode=disable"; + PGconn* conn = PQconnectdb(ss.str().c_str()); + if (PQstatus(conn) != CONNECTION_OK) { + fprintf(stderr, "Admin connection failed: %s", PQerrorMessage(conn)); + PQfinish(conn); + return PGConnPtr(nullptr, &PQfinish); + } + return PGConnPtr(conn, &PQfinish); +} + +// ============================================================ +// Helper: run admin SQL, return true on success +// ============================================================ +static bool admin_exec(PGconn* admin, const char* sql) { + PGresult* res = PQexec(admin, sql); + ExecStatusType st = PQresultStatus(res); + bool ok = (st == PGRES_COMMAND_OK || st == PGRES_TUPLES_OK); + if (!ok) { + diag("Admin query failed: '%s' err='%s'", sql, PQerrorMessage(admin)); + } + PQclear(res); + return ok; +} + +// ============================================================ +// Helper: run admin SQL and return first column of first row +// ============================================================ +static std::string admin_query_value(PGconn* admin, const char* sql) { + std::string result; + PGresult* res = PQexec(admin, sql); + if (PQresultStatus(res) == PGRES_TUPLES_OK && PQntuples(res) > 0) { + const char* val = PQgetvalue(res, 0, 0); + if (val) result = val; + } else { + diag("Admin query failed: '%s' err='%s'", sql, PQerrorMessage(admin)); + } + PQclear(res); + return result; +} + +// ============================================================ +// Helper: set admin-ssl_keylog_file and load to runtime +// ============================================================ +static bool set_keylog_file(PGconn* admin, const char* path) { + std::string sql = "SET admin-ssl_keylog_file='"; + if (path) sql += path; + sql += "'"; + if (!admin_exec(admin, sql.c_str())) return false; + return admin_exec(admin, "LOAD ADMIN VARIABLES TO RUNTIME"); +} + +// ============================================================ +// Helper: make a PgSQL backend connection with SSL +// ============================================================ +static PGConnPtr make_pgsql_ssl_conn() { + std::stringstream ss; + ss << "host=" << cl.pgsql_host + << " port=" << cl.pgsql_port + << " user=" << cl.pgsql_root_username + << " password=" << cl.pgsql_root_password + << " sslmode=require"; + PGconn* conn = PQconnectdb(ss.str().c_str()); + return PGConnPtr(conn, &PQfinish); +} + +// ============================================================ +// Helper: make a PgSQL backend connection without SSL +// ============================================================ +static PGConnPtr make_pgsql_nossl_conn() { + std::stringstream ss; + ss << "host=" << cl.pgsql_host + << " port=" << cl.pgsql_port + << " user=" << cl.pgsql_root_username + << " password=" << cl.pgsql_root_password + << " sslmode=disable"; + PGconn* conn = PQconnectdb(ss.str().c_str()); + return PGConnPtr(conn, &PQfinish); +} + +// ============================================================ +// Helper: count non-empty lines in a file +// ============================================================ +static long count_file_lines(const char* path) { + std::ifstream f(path); + if (!f.is_open()) return -1; + long n = 0; + std::string line; + while (std::getline(f, line)) { + if (!line.empty()) ++n; + } + return n; +} + +// ============================================================ +// Helper: get file size in bytes, returns -1 if file doesn't exist +// ============================================================ +static long file_size(const char* path) { + std::ifstream f(path, std::ios::ate | std::ios::binary); + if (!f.is_open()) return -1; + return (long)f.tellg(); +} + +// ============================================================ +// Helper: check whether any line in the file matches a regex +// ============================================================ +static bool file_has_regex_match(const char* path, const std::regex& re) { + std::ifstream f(path); + if (!f.is_open()) return false; + std::string line; + while (std::getline(f, line)) { + if (std::regex_search(line, re)) return true; + } + return false; +} + +// ============================================================ +// Helper: set pgsql_servers use_ssl and reload +// ============================================================ +static bool set_pgsql_use_ssl(PGconn* admin, int value) { + std::string sql = "UPDATE pgsql_servers SET use_ssl=" + std::to_string(value); + if (!admin_exec(admin, sql.c_str())) return false; + return admin_exec(admin, "LOAD PGSQL SERVERS TO RUNTIME"); +} + +// ============================================================ +// Helper: get pgsql-monitor_connect_interval (ms) +// ============================================================ +static long get_monitor_interval(PGconn* admin) { + std::string v = admin_query_value(admin, + "SELECT Variable_Value FROM global_variables " + "WHERE Variable_Name='pgsql-monitor_connect_interval'"); + return v.empty() ? 1000 : atol(v.c_str()); +} + +int main(int argc, char** argv) { + plan(9); + + if (cl.getEnv()) { + diag("Failed to get required environment variables"); + return exit_status(); + } + + // Remove any leftover keylog file from a prior run + ::remove(KEYLOG_PATH); + + // Open PgSQL admin connection + auto admin_conn = open_admin(); + if (!admin_conn) { + BAIL_OUT("Admin connection failed — cannot continue"); + return exit_status(); + } + PGconn* admin = admin_conn.get(); + diag("PgSQL admin connected to %s:%d", cl.pgsql_admin_host, cl.pgsql_admin_port); + + // Save original keylog file setting so we can restore it at the end + std::string original_keylog = admin_query_value(admin, + "SELECT Variable_Value FROM global_variables " + "WHERE Variable_Name='admin-ssl_keylog_file'"); + diag("Original admin-ssl_keylog_file='%s'", original_keylog.c_str()); + + // ================================================================ + // Test 1: Basic keylog creation + // ================================================================ + diag("---- Test 1: Basic keylog creation ----"); + + bool set_ok = set_keylog_file(admin, KEYLOG_PATH); + diag("Set admin-ssl_keylog_file='%s' result=%s", KEYLOG_PATH, set_ok ? "ok" : "FAIL"); + + if (set_ok) { + auto conn1 = make_pgsql_ssl_conn(); + if (PQstatus(conn1.get()) != CONNECTION_OK) { + diag("PgSQL SSL connection failed: %s", PQerrorMessage(conn1.get())); + } + usleep(100000); + } + + long sz = file_size(KEYLOG_PATH); + diag("Keylog file size after SSL PgSQL connection: %ld bytes", sz); + ok(set_ok && sz > 0, + "Test 1: Keylog file is non-empty after PgSQL SSL connection (size=%ld)", sz); + + // ================================================================ + // Test 2: NSS format validation + // ================================================================ + diag("---- Test 2: NSS Key Log Format validation ----"); + + std::regex nss_re(R"(^[A-Z_]+ [0-9a-fA-F]{64} [0-9a-fA-F]+$)"); + bool has_valid_line = file_has_regex_match(KEYLOG_PATH, nss_re); + ok(has_valid_line, + "Test 2: Keylog file contains valid NSS Key Log Format lines"); + + // ================================================================ + // Test 3: TLS version label check + // ================================================================ + diag("---- Test 3: TLS 1.2/1.3 secret label check ----"); + + std::regex tls_label_re( + R"(^(CLIENT_RANDOM|CLIENT_HANDSHAKE_TRAFFIC_SECRET|SERVER_HANDSHAKE_TRAFFIC_SECRET|)" + R"(CLIENT_TRAFFIC_SECRET_\d+|SERVER_TRAFFIC_SECRET_\d+) )"); + bool has_tls_label = file_has_regex_match(KEYLOG_PATH, tls_label_re); + ok(has_tls_label, + "Test 3: Keylog contains TLS 1.2 CLIENT_RANDOM or TLS 1.3 traffic secret labels"); + + // ================================================================ + // Test 4: Concurrent connections — no corruption + // ================================================================ + diag("---- Test 4: Concurrent SSL connections ----"); + + long lines_before = count_file_lines(KEYLOG_PATH); + diag("Lines before concurrent test: %ld", lines_before); + + const int NUM_THREADS = 8; + std::atomic conn_ok_count{0}; + std::vector threads; + threads.reserve(NUM_THREADS); + + for (int i = 0; i < NUM_THREADS; ++i) { + threads.emplace_back([&conn_ok_count]() { + auto c = make_pgsql_ssl_conn(); + if (PQstatus(c.get()) == CONNECTION_OK) { + ++conn_ok_count; + } + }); + } + for (auto& t : threads) t.join(); + usleep(200000); + + long lines_after = count_file_lines(KEYLOG_PATH); + diag("Lines after concurrent test: %ld (conn_ok=%d)", lines_after, (int)conn_ok_count); + + bool all_valid = true; + { + std::ifstream f(KEYLOG_PATH); + std::string line; + while (std::getline(f, line)) { + if (line.empty()) continue; + if (!std::regex_search(line, nss_re)) { + diag("Corrupt/unexpected line: '%s'", line.c_str()); + all_valid = false; + } + } + } + ok(all_valid && lines_after > lines_before, + "Test 4: Concurrent SSL connections produced valid keylog lines without corruption " + "(before=%ld after=%ld)", lines_before, lines_after); + + // ================================================================ + // Test 5: Disable keylog — no new lines after clearing + // ================================================================ + diag("---- Test 5: Disable keylog ----"); + + set_keylog_file(admin, ""); + usleep(50000); + + long lines_disabled = count_file_lines(KEYLOG_PATH); + diag("Lines before disable connection: %ld", lines_disabled); + + for (int i = 0; i < 3; ++i) { + auto c = make_pgsql_ssl_conn(); + (void)c; + } + usleep(200000); + + long lines_after_disable = count_file_lines(KEYLOG_PATH); + diag("Lines after connections with keylog disabled: %ld", lines_after_disable); + ok(lines_after_disable == lines_disabled, + "Test 5: No new keylog lines written after disabling keylog " + "(before=%ld after=%ld)", lines_disabled, lines_after_disable); + + // ================================================================ + // Test 6: Log rotation — PROXYSQL FLUSH LOGS appends new secrets + // ================================================================ + diag("---- Test 6: Log rotation via PROXYSQL FLUSH LOGS ----"); + + set_keylog_file(admin, KEYLOG_PATH); + usleep(50000); + + { + auto c = make_pgsql_ssl_conn(); + (void)c; + } + usleep(100000); + + long lines_pre_flush = count_file_lines(KEYLOG_PATH); + diag("Lines before PROXYSQL FLUSH LOGS: %ld", lines_pre_flush); + + bool flush_ok = admin_exec(admin, "PROXYSQL FLUSH LOGS"); + diag("PROXYSQL FLUSH LOGS result=%s", flush_ok ? "ok" : "FAIL"); + + { + auto c = make_pgsql_ssl_conn(); + (void)c; + } + usleep(100000); + + long lines_post_flush = count_file_lines(KEYLOG_PATH); + diag("Lines after PROXYSQL FLUSH LOGS + new connection: %ld", lines_post_flush); + ok(flush_ok && lines_post_flush > lines_pre_flush, + "Test 6: New keylog secrets appended after PROXYSQL FLUSH LOGS " + "(before=%ld after=%ld)", lines_pre_flush, lines_post_flush); + + // ================================================================ + // Test 7: Monitor SSL keylog — enable use_ssl, wait, verify entries + // ================================================================ + diag("---- Test 7: Monitor SSL keylog ----"); + + ::remove(KEYLOG_PATH); + set_keylog_file(admin, KEYLOG_PATH); + usleep(50000); + + long lines_before_monitor = count_file_lines(KEYLOG_PATH); + diag("Lines before enabling monitor SSL: %ld", lines_before_monitor); + + long monitor_interval_ms = get_monitor_interval(admin); + diag("pgsql-monitor_connect_interval=%ld ms", monitor_interval_ms); + + bool use_ssl_ok = set_pgsql_use_ssl(admin, 1); + diag("Set pgsql_servers use_ssl=1 result=%s", use_ssl_ok ? "ok" : "FAIL"); + + useconds_t wait_us = (useconds_t)(monitor_interval_ms * 2 * 1000); + if (wait_us > 10000000) wait_us = 10000000; + diag("Waiting %u us for monitor cycles...", wait_us); + usleep(wait_us); + + long lines_after_monitor = count_file_lines(KEYLOG_PATH); + diag("Lines after monitor SSL cycles: %ld", lines_after_monitor); + ok(use_ssl_ok && lines_after_monitor > lines_before_monitor, + "Test 7: Monitor SSL connections write keylog entries " + "(before=%ld after=%ld)", lines_before_monitor, lines_after_monitor); + + set_pgsql_use_ssl(admin, 0); + usleep(50000); + + // ================================================================ + // Test 8: Non-SSL PgSQL connection — no keylog entries added + // ================================================================ + diag("---- Test 8: Non-SSL connection produces no keylog entries ----"); + + admin_exec(admin, "SET pgsql-monitor_enabled='false'"); + admin_exec(admin, "LOAD PGSQL VARIABLES TO RUNTIME"); + usleep(500000); + + ::remove(KEYLOG_PATH); + set_keylog_file(admin, KEYLOG_PATH); + usleep(50000); + + long lines_nossl_before = count_file_lines(KEYLOG_PATH); + diag("Lines before non-SSL connections: %ld", lines_nossl_before); + + for (int i = 0; i < 3; ++i) { + auto c = make_pgsql_nossl_conn(); + if (PQstatus(c.get()) != CONNECTION_OK) { + diag("Non-SSL PgSQL connection failed (may be expected): %s", + PQerrorMessage(c.get())); + } + } + usleep(200000); + + long lines_nossl_after = count_file_lines(KEYLOG_PATH); + diag("Lines after non-SSL connections: %ld", lines_nossl_after); + ok(lines_nossl_after == lines_nossl_before, + "Test 8: Non-SSL PgSQL connections do not add keylog entries " + "(before=%ld after=%ld)", lines_nossl_before, lines_nossl_after); + + admin_exec(admin, "SET pgsql-monitor_enabled='true'"); + admin_exec(admin, "LOAD PGSQL VARIABLES TO RUNTIME"); + + // ================================================================ + // Test 9: Invalid keylog path — graceful handling + // ================================================================ + diag("---- Test 9: Invalid keylog path ----"); + + const char* bad_path = "/nonexistent/dir/proxysql_keylog.log"; + set_keylog_file(admin, bad_path); + + { + auto c = make_pgsql_ssl_conn(); + (void)c; + } + usleep(100000); + + long bad_sz = file_size(bad_path); + diag("bad_path size=%ld (expected -1)", bad_sz); + + ok(bad_sz < 0, + "Test 9: Invalid keylog path handled gracefully — file not created at bad path"); + + // ================================================================ + // Cleanup + // ================================================================ + diag("---- Cleanup ----"); + + set_keylog_file(admin, original_keylog.empty() ? "" : original_keylog.c_str()); + + if (::remove(KEYLOG_PATH) == 0) { + diag("Removed keylog file: %s", KEYLOG_PATH); + } + + return exit_status(); +} diff --git a/test/tap/tests/unit/genai_query_handler_unit-t.cpp b/test/tap/tests/unit/genai_query_handler_unit-t.cpp index b4062d5bd..ea1439255 100644 --- a/test/tap/tests/unit/genai_query_handler_unit-t.cpp +++ b/test/tap/tests/unit/genai_query_handler_unit-t.cpp @@ -14,9 +14,10 @@ * Compiled only when PROXYSQLGENAI=1 (auto-detected from libproxysql.a). */ +#include "tap.h" + #ifdef PROXYSQLGENAI -#include "tap.h" #include "test_globals.h" #include "test_init.h" #include "proxysql.h"