From e68f8bb74a194ed2831c085fe5f126894cc96cb1 Mon Sep 17 00:00:00 2001 From: Rahim Kanji Date: Thu, 9 Apr 2026 02:33:22 +0500 Subject: [PATCH] refactor: simplify pgsql_servers_ssl_params schema and pre-parse TLS range - Drop ssl_capath and ssl_cipher columns (no libpq equivalents) - Pre-parse ssl_protocol_version_range into ssl_min/max_protocol_version at construction time instead of on every backend connection - Fix Servers_SslParams 3-arg delegating constructor - Update tests, docs, and all consumers (Connection, Monitor, Kill, Admin) --- docs/pgsql_servers_ssl_params.md | 10 +-- include/PgSQL_Connection.h | 4 +- include/PgSQL_HostGroups_Manager.h | 3 +- include/ProxySQL_Admin_Tables_Definitions.h | 4 +- include/Servers_SslParams.h | 51 ++++++++++- lib/PgSQL_Connection.cpp | 73 +++++++-------- lib/PgSQL_HostGroups_Manager.cpp | 25 +++--- lib/PgSQL_Monitor.cpp | 18 +++- lib/ProxySQL_Admin.cpp | 12 ++- test/tap/tests/pgsql-servers_ssl_params-t.cpp | 76 ++++++++++++++-- .../unit/pgsql_servers_ssl_params_unit-t.cpp | 88 +++++++++++++++---- 11 files changed, 265 insertions(+), 99 deletions(-) diff --git a/docs/pgsql_servers_ssl_params.md b/docs/pgsql_servers_ssl_params.md index be3b845b6..bb2d629b9 100644 --- a/docs/pgsql_servers_ssl_params.md +++ b/docs/pgsql_servers_ssl_params.md @@ -16,10 +16,8 @@ CREATE TABLE pgsql_servers_ssl_params ( ssl_ca VARCHAR NOT NULL DEFAULT '', ssl_cert VARCHAR NOT NULL DEFAULT '', ssl_key VARCHAR NOT NULL DEFAULT '', - ssl_capath VARCHAR NOT NULL DEFAULT '', ssl_crl VARCHAR NOT NULL DEFAULT '', ssl_crlpath VARCHAR NOT NULL DEFAULT '', - ssl_cipher VARCHAR NOT NULL DEFAULT '', ssl_protocol_version_range VARCHAR NOT NULL DEFAULT '', comment VARCHAR NOT NULL DEFAULT '', PRIMARY KEY (hostname, port, username) @@ -33,13 +31,11 @@ CREATE TABLE pgsql_servers_ssl_params ( | `hostname` | Backend server hostname. Must match the `hostname` in `pgsql_servers`. | | `port` | Backend server port. Default: `5432`. Must match the `port` in `pgsql_servers`. | | `username` | ProxySQL username. Empty string `''` acts as a wildcard fallback (see Lookup Hierarchy). | -| `ssl_ca` | Path to the CA certificate file. Maps to libpq `sslrootcert`. | +| `ssl_ca` | Path to the CA certificate file (PEM). May contain multiple concatenated CA certs. Maps to libpq `sslrootcert`. | | `ssl_cert` | Path to the client certificate file. Maps to libpq `sslcert`. | | `ssl_key` | Path to the client private key file. Maps to libpq `sslkey`. | -| `ssl_capath` | Path to directory containing CA certificates. | | `ssl_crl` | Path to the certificate revocation list file. Maps to libpq `sslcrl`. | | `ssl_crlpath` | Path to directory containing CRL files. Maps to libpq `sslcrldir` (PostgreSQL 14+). | -| `ssl_cipher` | SSL cipher specification. | | `ssl_protocol_version_range` | TLS protocol version constraint. See format below. | | `comment` | Free-form comment. | @@ -86,6 +82,8 @@ When ProxySQL opens a new connection to a PostgreSQL backend, it looks up SSL pa This allows you to set a default SSL configuration for a server (empty username) while overriding it for specific users. +> **Important — matching is all-or-nothing.** Once a row in `pgsql_servers_ssl_params` matches (either at step 1 or step 2), ProxySQL uses **only** the SSL fields from that row. Empty columns in the matched row are passed through as empty (libpq defaults), they are **not** silently filled in from `pgsql-ssl_p2s_*`. The global variables are consulted **only** when no row matches at all (step 3). If you want a per-server row to inherit some defaults from the globals, you must copy those values into the row explicitly. + ## Usage ### Basic: Same SSL cert for all users connecting to a server @@ -190,4 +188,4 @@ If `use_ssl=0`, the backend connection does not use SSL regardless of `pgsql_ser - Empty fields in `pgsql_servers_ssl_params` are omitted from the libpq connection string (libpq defaults apply for those fields). - Per-server SSL params only affect **new** backend connections. Existing pooled connections continue using their original SSL settings. Use the `/* create_new_connection=1 */` query annotation to force ProxySQL to create a new backend connection. -- The `ssl_cipher` column is reserved for future use. libpq does not expose a direct connection string parameter for cipher selection. +- Per-server SSL params apply on the data path (`PgSQL_Connection`), the monitor path (`PgSQL_Monitor`), and the cancel/terminate path (`PgSQL_Backend_Kill_Args`). diff --git a/include/PgSQL_Connection.h b/include/PgSQL_Connection.h index 46a6b7024..9a5365800 100644 --- a/include/PgSQL_Connection.h +++ b/include/PgSQL_Connection.h @@ -61,7 +61,7 @@ enum PgSQL_Param_Name { PG_SSLROOTCERT, // Specifies the name of a file containing SSL certificate authority (CA) certificate(s) PG_SSLCRL, // Specifies the file name of the SSL server certificate revocation list (CRL) PG_SSLCRLDIR, // Specifies the directory name of the SSL server certificate revocation list (CRL) - PG_SSLSNI, // Sets the TLS extension “Server Name Indication” (SNI) on SSL-enabled connections + PG_SSLSNI, // Sets the TLS extension Server Name Indication (SNI) on SSL-enabled connections PG_REQUIREPEER, // Specifies the operating-system user name of the server PG_SSL_MIN_PROTOCOL_VERSION, // Specifies the minimum SSL/TLS protocol version to allow for the connection PG_SSL_MAX_PROTOCOL_VERSION, // Specifies the maximum SSL/TLS protocol version to allow for the connection @@ -729,6 +729,8 @@ public: char* sslrootcert; char* sslcrl; char* sslcrldir; + char* ssl_min_protocol_version; + char* ssl_max_protocol_version; } ssl_config; PgSQL_Backend_Kill_Args(PGconn* conn, const char* user, const char* pass, const char* db, const char* host, diff --git a/include/PgSQL_HostGroups_Manager.h b/include/PgSQL_HostGroups_Manager.h index 2109dad2b..73a9e67db 100644 --- a/include/PgSQL_HostGroups_Manager.h +++ b/include/PgSQL_HostGroups_Manager.h @@ -46,7 +46,7 @@ #define MYHGM_PgSQL_SERVERS "CREATE TABLE pgsql_servers ( hostgroup_id INT NOT NULL DEFAULT 0 , hostname VARCHAR NOT NULL , port INT NOT NULL DEFAULT 5432 , weight INT NOT NULL DEFAULT 1 , status INT NOT NULL DEFAULT 0 , compression INT NOT NULL DEFAULT 0 , max_connections INT NOT NULL DEFAULT 1000 , max_replication_lag INT NOT NULL DEFAULT 0 , use_ssl INT NOT NULL DEFAULT 0 , max_latency_ms INT UNSIGNED NOT NULL DEFAULT 0 , comment VARCHAR NOT NULL DEFAULT '' , mem_pointer INT NOT NULL DEFAULT 0 , PRIMARY KEY (hostgroup_id, hostname, port) )" #define MYHGM_PgSQL_SERVERS_INCOMING "CREATE TABLE pgsql_servers_incoming ( hostgroup_id INT NOT NULL DEFAULT 0 , hostname VARCHAR NOT NULL , port INT NOT NULL DEFAULT 5432 , weight INT NOT NULL DEFAULT 1 , status INT NOT NULL DEFAULT 0 , compression INT NOT NULL DEFAULT 0 , max_connections INT NOT NULL DEFAULT 1000 , max_replication_lag INT NOT NULL DEFAULT 0 , use_ssl INT NOT NULL DEFAULT 0 , max_latency_ms INT UNSIGNED NOT NULL DEFAULT 0 , comment VARCHAR NOT NULL DEFAULT '' , PRIMARY KEY (hostgroup_id, hostname, port))" #endif /* DEBUG */ -#define MYHGM_PgSQL_SERVERS_SSL_PARAMS "CREATE TABLE pgsql_servers_ssl_params (hostname VARCHAR NOT NULL , port INT CHECK (port >= 0 AND port <= 65535) NOT NULL DEFAULT 5432 , username VARCHAR NOT NULL DEFAULT '' , ssl_ca VARCHAR NOT NULL DEFAULT '' , ssl_cert VARCHAR NOT NULL DEFAULT '' , ssl_key VARCHAR NOT NULL DEFAULT '' , ssl_capath VARCHAR NOT NULL DEFAULT '' , ssl_crl VARCHAR NOT NULL DEFAULT '' , ssl_crlpath VARCHAR NOT NULL DEFAULT '' , ssl_cipher VARCHAR NOT NULL DEFAULT '' , ssl_protocol_version_range VARCHAR NOT NULL DEFAULT '' , comment VARCHAR NOT NULL DEFAULT '' , PRIMARY KEY (hostname, port, username) )" +#define MYHGM_PgSQL_SERVERS_SSL_PARAMS "CREATE TABLE pgsql_servers_ssl_params (hostname VARCHAR NOT NULL , port INT CHECK (port >= 0 AND port <= 65535) NOT NULL DEFAULT 5432 , username VARCHAR NOT NULL DEFAULT '' , ssl_ca VARCHAR NOT NULL DEFAULT '' , ssl_cert VARCHAR NOT NULL DEFAULT '' , ssl_key VARCHAR NOT NULL DEFAULT '' , ssl_crl VARCHAR NOT NULL DEFAULT '' , ssl_crlpath VARCHAR NOT NULL DEFAULT '' , ssl_protocol_version_range VARCHAR NOT NULL DEFAULT '' , comment VARCHAR NOT NULL DEFAULT '' , PRIMARY KEY (hostname, port, username) )" #define MYHGM_PgSQL_REPLICATION_HOSTGROUPS "CREATE TABLE pgsql_replication_hostgroups (writer_hostgroup INT CHECK (writer_hostgroup>=0) NOT NULL PRIMARY KEY , reader_hostgroup INT NOT NULL CHECK (reader_hostgroup<>writer_hostgroup AND reader_hostgroup>=0) , check_type VARCHAR CHECK (LOWER(check_type) IN ('read_only')) NOT NULL DEFAULT 'read_only' , comment VARCHAR NOT NULL DEFAULT '' , UNIQUE (reader_hostgroup))" #define PGHGM_GEN_ADMIN_RUNTIME_SERVERS "SELECT hostgroup_id, hostname, port, CASE status WHEN 0 THEN \"ONLINE\" WHEN 1 THEN \"SHUNNED\" WHEN 2 THEN \"OFFLINE_SOFT\" WHEN 3 THEN \"OFFLINE_HARD\" WHEN 4 THEN \"SHUNNED\" END status, weight, compression, max_connections, max_replication_lag, use_ssl, max_latency_ms, comment FROM pgsql_servers ORDER BY hostgroup_id, hostname, port" @@ -404,6 +404,7 @@ class PgSQL_HostGroups_Manager : public Base_HostGroups_Manager { PgSQL_GALERA_HOSTGROUPS, PgSQL_AWS_AURORA_HOSTGROUPS, PgSQL_HOSTGROUP_ATTRIBUTES, + PgSQL_SERVERS_SSL_PARAMS, PgSQL_SERVERS, __HGM_TABLES_SIZE diff --git a/include/ProxySQL_Admin_Tables_Definitions.h b/include/ProxySQL_Admin_Tables_Definitions.h index 61ac4d3bc..b26aec745 100644 --- a/include/ProxySQL_Admin_Tables_Definitions.h +++ b/include/ProxySQL_Admin_Tables_Definitions.h @@ -315,9 +315,9 @@ #define ADMIN_SQLITE_TABLE_RUNTIME_PGSQL_SERVERS "CREATE TABLE runtime_pgsql_servers (hostgroup_id INT CHECK (hostgroup_id>=0) NOT NULL DEFAULT 0 , hostname VARCHAR NOT NULL , port INT CHECK (port >= 0 AND port <= 65535) NOT NULL DEFAULT 5432 , status VARCHAR CHECK (UPPER(status) IN ('ONLINE','SHUNNED','OFFLINE_SOFT', 'OFFLINE_HARD')) NOT NULL DEFAULT 'ONLINE' , weight INT CHECK (weight >= 0 AND weight <=10000000) NOT NULL DEFAULT 1 , compression INT CHECK (compression IN(0,1)) NOT NULL DEFAULT 0 , max_connections INT CHECK (max_connections >=0) NOT NULL DEFAULT 1000 , max_replication_lag INT CHECK (max_replication_lag >= 0 AND max_replication_lag <= 126144000) NOT NULL DEFAULT 0 , use_ssl INT CHECK (use_ssl IN(0,1)) NOT NULL DEFAULT 0 , max_latency_ms INT UNSIGNED CHECK (max_latency_ms>=0) NOT NULL DEFAULT 0 , comment VARCHAR NOT NULL DEFAULT '' , PRIMARY KEY (hostgroup_id, hostname, port) )" -#define ADMIN_SQLITE_TABLE_PGSQL_SERVERS_SSL_PARAMS "CREATE TABLE pgsql_servers_ssl_params (hostname VARCHAR NOT NULL , port INT CHECK (port >= 0 AND port <= 65535) NOT NULL DEFAULT 5432 , username VARCHAR NOT NULL DEFAULT '' , ssl_ca VARCHAR NOT NULL DEFAULT '' , ssl_cert VARCHAR NOT NULL DEFAULT '' , ssl_key VARCHAR NOT NULL DEFAULT '' , ssl_capath VARCHAR NOT NULL DEFAULT '' , ssl_crl VARCHAR NOT NULL DEFAULT '' , ssl_crlpath VARCHAR NOT NULL DEFAULT '' , ssl_cipher VARCHAR NOT NULL DEFAULT '' , ssl_protocol_version_range VARCHAR NOT NULL DEFAULT '' , comment VARCHAR NOT NULL DEFAULT '' , PRIMARY KEY (hostname, port, username) )" +#define ADMIN_SQLITE_TABLE_PGSQL_SERVERS_SSL_PARAMS "CREATE TABLE pgsql_servers_ssl_params (hostname VARCHAR NOT NULL , port INT CHECK (port >= 0 AND port <= 65535) NOT NULL DEFAULT 5432 , username VARCHAR NOT NULL DEFAULT '' , ssl_ca VARCHAR NOT NULL DEFAULT '' , ssl_cert VARCHAR NOT NULL DEFAULT '' , ssl_key VARCHAR NOT NULL DEFAULT '' , ssl_crl VARCHAR NOT NULL DEFAULT '' , ssl_crlpath VARCHAR NOT NULL DEFAULT '' , ssl_protocol_version_range VARCHAR NOT NULL DEFAULT '' , comment VARCHAR NOT NULL DEFAULT '' , PRIMARY KEY (hostname, port, username) )" -#define ADMIN_SQLITE_TABLE_RUNTIME_PGSQL_SERVERS_SSL_PARAMS "CREATE TABLE runtime_pgsql_servers_ssl_params (hostname VARCHAR NOT NULL , port INT CHECK (port >= 0 AND port <= 65535) NOT NULL DEFAULT 5432 , username VARCHAR NOT NULL DEFAULT '' , ssl_ca VARCHAR NOT NULL DEFAULT '' , ssl_cert VARCHAR NOT NULL DEFAULT '' , ssl_key VARCHAR NOT NULL DEFAULT '' , ssl_capath VARCHAR NOT NULL DEFAULT '' , ssl_crl VARCHAR NOT NULL DEFAULT '' , ssl_crlpath VARCHAR NOT NULL DEFAULT '' , ssl_cipher VARCHAR NOT NULL DEFAULT '' , ssl_protocol_version_range VARCHAR NOT NULL DEFAULT '' , comment VARCHAR NOT NULL DEFAULT '' , PRIMARY KEY (hostname, port, username) )" +#define ADMIN_SQLITE_TABLE_RUNTIME_PGSQL_SERVERS_SSL_PARAMS "CREATE TABLE runtime_pgsql_servers_ssl_params (hostname VARCHAR NOT NULL , port INT CHECK (port >= 0 AND port <= 65535) NOT NULL DEFAULT 5432 , username VARCHAR NOT NULL DEFAULT '' , ssl_ca VARCHAR NOT NULL DEFAULT '' , ssl_cert VARCHAR NOT NULL DEFAULT '' , ssl_key VARCHAR NOT NULL DEFAULT '' , ssl_crl VARCHAR NOT NULL DEFAULT '' , ssl_crlpath VARCHAR NOT NULL DEFAULT '' , ssl_protocol_version_range VARCHAR NOT NULL DEFAULT '' , comment VARCHAR NOT NULL DEFAULT '' , PRIMARY KEY (hostname, port, username) )" #define ADMIN_SQLITE_TABLE_RUNTIME_PGSQL_USERS "CREATE TABLE runtime_pgsql_users (username VARCHAR NOT NULL , password VARCHAR , active INT CHECK (active IN (0,1)) NOT NULL DEFAULT 1 , use_ssl INT CHECK (use_ssl IN (0,1)) NOT NULL DEFAULT 0 , default_hostgroup INT NOT NULL DEFAULT 0 , transaction_persistent INT CHECK (transaction_persistent IN (0,1)) NOT NULL DEFAULT 1 , fast_forward INT CHECK (fast_forward IN (0,1)) NOT NULL DEFAULT 0 , backend INT CHECK (backend IN (0,1)) NOT NULL DEFAULT 1 , frontend INT CHECK (frontend IN (0,1)) NOT NULL DEFAULT 1 , max_connections INT CHECK (max_connections >=0) NOT NULL DEFAULT 10000 , attributes VARCHAR CHECK (JSON_VALID(attributes) OR attributes = '') NOT NULL DEFAULT '', comment VARCHAR NOT NULL DEFAULT '' , PRIMARY KEY (username, backend) , UNIQUE (username, frontend))" #define ADMIN_SQLITE_TABLE_RUNTIME_PGSQL_LDAP_MAPPING "CREATE TABLE runtime_pgsql_ldap_mapping (priority INTEGER PRIMARY KEY NOT NULL , frontend_entity VARCHAR NOT NULL , backend_entity VARCHAR NOT NULL , comment VARCHAR NOT NULL DEFAULT '' , UNIQUE (frontend_entity))" diff --git a/include/Servers_SslParams.h b/include/Servers_SslParams.h index 92edeb3d8..1adc5530c 100644 --- a/include/Servers_SslParams.h +++ b/include/Servers_SslParams.h @@ -57,9 +57,8 @@ class Servers_SslParams { comment = string(c); MapKey = ""; } - Servers_SslParams(string _h, int _p, string _u) { - Servers_SslParams(_h, _p, _u, "", "", "", "", "", "", "", "", ""); - } + Servers_SslParams(string _h, int _p, string _u) + : Servers_SslParams(_h, _p, _u, "", "", "", "", "", "", "", "", "") {} virtual ~Servers_SslParams() = default; string getMapKey(const char *del) { if (MapKey == "") { @@ -76,7 +75,51 @@ class MySQLServers_SslParams : public Servers_SslParams { class PgSQLServers_SslParams : public Servers_SslParams { public: - using Servers_SslParams::Servers_SslParams; + // Pre-parsed from tls_version (= the SQL column ssl_protocol_version_range). + // Populated once at construction so the data path does not have to re-parse + // the range string on every backend connection. Empty when tls_version is + // empty or malformed (in which case libpq defaults apply). + string ssl_min_protocol_version; + string ssl_max_protocol_version; + + // PgSQL-specific constructors. libpq has no equivalent for the base class + // ssl_capath / ssl_cipher fields, so they are not exposed here — the base + // class members are forwarded as empty strings and stay unused on the + // PgSQL backend path. + PgSQLServers_SslParams(string _h, int _p, string _u, + string ca, string cert, string key, + string crl, string crlpath, string tls, string c) + : Servers_SslParams(_h, _p, _u, ca, cert, key, "", crl, crlpath, "", tls, c) { + parse_tls_version(); + } + PgSQLServers_SslParams(char * _h, int _p, char * _u, + char * ca, char * cert, char * key, + char * crl, char * crlpath, char * tls, char * c) + : Servers_SslParams(_h, _p, _u, ca, cert, key, (char*)"", crl, crlpath, (char*)"", tls, c) { + parse_tls_version(); + } + PgSQLServers_SslParams(string _h, int _p, string _u) + : Servers_SslParams(_h, _p, _u) {} + + private: + // Parse tls_version into ssl_min_protocol_version / ssl_max_protocol_version. + // Format: "MIN-MAX" for a range, or a single token to pin both ends. + // Empty or malformed values leave both fields empty. + void parse_tls_version() { + if (tls_version.empty()) return; + size_t dash_pos = tls_version.find('-'); + if (dash_pos == string::npos) { + ssl_min_protocol_version = tls_version; + ssl_max_protocol_version = tls_version; + return; + } + string min_ver = tls_version.substr(0, dash_pos); + string max_ver = tls_version.substr(dash_pos + 1); + if (!min_ver.empty() && !max_ver.empty()) { + ssl_min_protocol_version = min_ver; + ssl_max_protocol_version = max_ver; + } + } }; #endif // __CLASS_SERVERS_SSL_PARAMS_H diff --git a/lib/PgSQL_Connection.cpp b/lib/PgSQL_Connection.cpp index 7e33f47e7..6fc0a1025 100644 --- a/lib/PgSQL_Connection.cpp +++ b/lib/PgSQL_Connection.cpp @@ -979,21 +979,13 @@ void PgSQL_Connection::connect_start() { append_conninfo_param(conninfo, "sslcrl", (char*)ssl_params->ssl_crl.c_str()); if (ssl_params->ssl_crlpath.length() > 0) append_conninfo_param(conninfo, "sslcrldir", (char*)ssl_params->ssl_crlpath.c_str()); - // Parse ssl_protocol_version_range (format: "TLSv1.2-TLSv1.3" or "TLSv1.3") - if (ssl_params->tls_version.length() > 0) { - string tls_ver = ssl_params->tls_version; - size_t dash_pos = tls_ver.find('-'); - if (dash_pos != string::npos) { - string min_ver = tls_ver.substr(0, dash_pos); - string max_ver = tls_ver.substr(dash_pos + 1); - append_conninfo_param(conninfo, "ssl_min_protocol_version", (char*)min_ver.c_str()); - append_conninfo_param(conninfo, "ssl_max_protocol_version", (char*)max_ver.c_str()); - } else { - // Single version = pin to that version - append_conninfo_param(conninfo, "ssl_min_protocol_version", (char*)tls_ver.c_str()); - append_conninfo_param(conninfo, "ssl_max_protocol_version", (char*)tls_ver.c_str()); - } - } + // ssl_protocol_version_range was pre-parsed at PgSQLServers_SslParams + // construction time (see parse_tls_version()). Empty min/max means + // either unset or malformed — in both cases libpq defaults apply. + if (ssl_params->ssl_min_protocol_version.length() > 0) + append_conninfo_param(conninfo, "ssl_min_protocol_version", (char*)ssl_params->ssl_min_protocol_version.c_str()); + if (ssl_params->ssl_max_protocol_version.length() > 0) + append_conninfo_param(conninfo, "ssl_max_protocol_version", (char*)ssl_params->ssl_max_protocol_version.c_str()); } else { // Fall back to global SSL settings append_conninfo_param(conninfo, "sslkey", pgsql_thread___ssl_p2s_key); @@ -2972,36 +2964,25 @@ PgSQL_Backend_Kill_Args::PgSQL_Backend_Kill_Args(PGconn* conn, const char* user, backend_pid = PQbackendPID(conn); ssl_config.use_ssl = ssl; if (ssl) { - ssl_config.sslkey = pgsql_thread___ssl_p2s_key ? strdup(pgsql_thread___ssl_p2s_key) : nullptr; - ssl_config.sslcert = pgsql_thread___ssl_p2s_cert ? strdup(pgsql_thread___ssl_p2s_cert) : nullptr; - ssl_config.sslrootcert = pgsql_thread___ssl_p2s_ca ? strdup(pgsql_thread___ssl_p2s_ca) : nullptr; - ssl_config.sslcrl = pgsql_thread___ssl_p2s_crl ? strdup(pgsql_thread___ssl_p2s_crl) : nullptr; - ssl_config.sslcrldir = pgsql_thread___ssl_p2s_crlpath ? strdup(pgsql_thread___ssl_p2s_crlpath) : nullptr; - // Override with per-server SSL params if available std::unique_ptr params { PgHGM->get_Server_SSL_Params(hostname, port, username) }; if (params != nullptr) { - if (params->ssl_key.length() > 0) { - free(ssl_config.sslkey); - ssl_config.sslkey = strdup(params->ssl_key.c_str()); - } - if (params->ssl_cert.length() > 0) { - free(ssl_config.sslcert); - ssl_config.sslcert = strdup(params->ssl_cert.c_str()); - } - if (params->ssl_ca.length() > 0) { - free(ssl_config.sslrootcert); - ssl_config.sslrootcert = strdup(params->ssl_ca.c_str()); - } - if (params->ssl_crl.length() > 0) { - free(ssl_config.sslcrl); - ssl_config.sslcrl = strdup(params->ssl_crl.c_str()); - } - if (params->ssl_crlpath.length() > 0) { - free(ssl_config.sslcrldir); - ssl_config.sslcrldir = strdup(params->ssl_crlpath.c_str()); - } + ssl_config.sslkey = params->ssl_key.length() > 0 ? strdup(params->ssl_key.c_str()) : nullptr; + ssl_config.sslcert = params->ssl_cert.length() > 0 ? strdup(params->ssl_cert.c_str()) : nullptr; + ssl_config.sslrootcert = params->ssl_ca.length() > 0 ? strdup(params->ssl_ca.c_str()) : nullptr; + ssl_config.sslcrl = params->ssl_crl.length() > 0 ? strdup(params->ssl_crl.c_str()) : nullptr; + ssl_config.sslcrldir = params->ssl_crlpath.length() > 0 ? strdup(params->ssl_crlpath.c_str()) : nullptr; + ssl_config.ssl_min_protocol_version = params->ssl_min_protocol_version.length() > 0 ? strdup(params->ssl_min_protocol_version.c_str()) : nullptr; + ssl_config.ssl_max_protocol_version = params->ssl_max_protocol_version.length() > 0 ? strdup(params->ssl_max_protocol_version.c_str()) : nullptr; + } else { + ssl_config.sslkey = pgsql_thread___ssl_p2s_key ? strdup(pgsql_thread___ssl_p2s_key) : nullptr; + ssl_config.sslcert = pgsql_thread___ssl_p2s_cert ? strdup(pgsql_thread___ssl_p2s_cert) : nullptr; + ssl_config.sslrootcert = pgsql_thread___ssl_p2s_ca ? strdup(pgsql_thread___ssl_p2s_ca) : nullptr; + ssl_config.sslcrl = pgsql_thread___ssl_p2s_crl ? strdup(pgsql_thread___ssl_p2s_crl) : nullptr; + ssl_config.sslcrldir = pgsql_thread___ssl_p2s_crlpath ? strdup(pgsql_thread___ssl_p2s_crlpath) : nullptr; + ssl_config.ssl_min_protocol_version = nullptr; + ssl_config.ssl_max_protocol_version = nullptr; } } else { ssl_config.sslkey = nullptr; @@ -3009,6 +2990,8 @@ PgSQL_Backend_Kill_Args::PgSQL_Backend_Kill_Args(PGconn* conn, const char* user, ssl_config.sslrootcert = nullptr; ssl_config.sslcrl = nullptr; ssl_config.sslcrldir = nullptr; + ssl_config.ssl_min_protocol_version = nullptr; + ssl_config.ssl_max_protocol_version = nullptr; } } @@ -3022,7 +3005,9 @@ PgSQL_Backend_Kill_Args::~PgSQL_Backend_Kill_Args() { free(ssl_config.sslrootcert); free(ssl_config.sslcrl); free(ssl_config.sslcrldir); - if (cancel_conn) + free(ssl_config.ssl_min_protocol_version); + free(ssl_config.ssl_max_protocol_version); + if (cancel_conn) PQfreeCancel(cancel_conn); } @@ -3068,6 +3053,10 @@ void* PgSQL_backend_kill_thread(void* arg) { append_conninfo_param(conninfo, "sslrootcert", backend_kill_args->ssl_config.sslrootcert); append_conninfo_param(conninfo, "sslcrl", backend_kill_args->ssl_config.sslcrl); append_conninfo_param(conninfo, "sslcrldir", backend_kill_args->ssl_config.sslcrldir); + // Per-server TLS protocol pinning was pre-parsed from + // ssl_protocol_version_range when the Kill_Args struct was built. + append_conninfo_param(conninfo, "ssl_min_protocol_version", backend_kill_args->ssl_config.ssl_min_protocol_version); + append_conninfo_param(conninfo, "ssl_max_protocol_version", backend_kill_args->ssl_config.ssl_max_protocol_version); } else { conninfo << "sslmode='disable' "; // not supporting SSL } diff --git a/lib/PgSQL_HostGroups_Manager.cpp b/lib/PgSQL_HostGroups_Manager.cpp index 660eac1d8..052376825 100644 --- a/lib/PgSQL_HostGroups_Manager.cpp +++ b/lib/PgSQL_HostGroups_Manager.cpp @@ -1051,6 +1051,7 @@ void PgSQL_HostGroups_Manager::commit_update_checksums_from_tables(SpookyHash& m CUCFT1(myhash,init,"pgsql_replication_hostgroups","writer_hostgroup", table_resultset_checksum[HGM_TABLES::PgSQL_REPLICATION_HOSTGROUPS]); CUCFT1(myhash,init,"pgsql_hostgroup_attributes","hostgroup_id", table_resultset_checksum[HGM_TABLES::PgSQL_HOSTGROUP_ATTRIBUTES]); + CUCFT1(myhash,init,"pgsql_servers_ssl_params","hostname,port,username", table_resultset_checksum[HGM_TABLES::PgSQL_SERVERS_SSL_PARAMS]); } /** @@ -1848,7 +1849,7 @@ SQLite3_result * PgSQL_HostGroups_Manager::dump_table_pgsql(const string& name) } else if (name == "cluster_pgsql_servers") { query = (char *)PGHGM_GEN_CLUSTER_ADMIN_RUNTIME_SERVERS; } else if (name == "pgsql_servers_ssl_params") { - query=(char *)"SELECT hostname, port, username, ssl_ca, ssl_cert, ssl_key, ssl_capath, ssl_crl, ssl_crlpath, ssl_cipher, ssl_protocol_version_range, comment FROM pgsql_servers_ssl_params ORDER BY hostname, port, username"; + query=(char *)"SELECT hostname, port, username, ssl_ca, ssl_cert, ssl_key, ssl_crl, ssl_crlpath, ssl_protocol_version_range, comment FROM pgsql_servers_ssl_params ORDER BY hostname, port, username"; } else { assert(0); } @@ -3977,9 +3978,9 @@ void PgSQL_HostGroups_Manager::generate_pgsql_servers_ssl_params_table() { int rc; const char * query = (const char *)"INSERT INTO pgsql_servers_ssl_params (" - "hostname, port, username, ssl_ca, ssl_cert, ssl_key, ssl_capath, " - "ssl_crl, ssl_crlpath, ssl_cipher, ssl_protocol_version_range, comment) VALUES " - "(?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)"; + "hostname, port, username, ssl_ca, ssl_cert, ssl_key, " + "ssl_crl, ssl_crlpath, ssl_protocol_version_range, comment) VALUES " + "(?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)"; auto [rc1, statement_unique] = mydb->prepare_v2(query); ASSERT_SQLITE_OK(rc1, mydb); @@ -4000,12 +4001,10 @@ void PgSQL_HostGroups_Manager::generate_pgsql_servers_ssl_params_table() { rc=(*proxy_sqlite3_bind_text)(statement, 4, r->fields[3] , -1, SQLITE_TRANSIENT); ASSERT_SQLITE_OK(rc, mydb); // ssl_ca rc=(*proxy_sqlite3_bind_text)(statement, 5, r->fields[4] , -1, SQLITE_TRANSIENT); ASSERT_SQLITE_OK(rc, mydb); // ssl_cert rc=(*proxy_sqlite3_bind_text)(statement, 6, r->fields[5] , -1, SQLITE_TRANSIENT); ASSERT_SQLITE_OK(rc, mydb); // ssl_key - rc=(*proxy_sqlite3_bind_text)(statement, 7, r->fields[6] , -1, SQLITE_TRANSIENT); ASSERT_SQLITE_OK(rc, mydb); // ssl_capath - rc=(*proxy_sqlite3_bind_text)(statement, 8, r->fields[7] , -1, SQLITE_TRANSIENT); ASSERT_SQLITE_OK(rc, mydb); // ssl_crl - rc=(*proxy_sqlite3_bind_text)(statement, 9, r->fields[8] , -1, SQLITE_TRANSIENT); ASSERT_SQLITE_OK(rc, mydb); // ssl_crlpath - rc=(*proxy_sqlite3_bind_text)(statement, 10, r->fields[9] , -1, SQLITE_TRANSIENT); ASSERT_SQLITE_OK(rc, mydb); // ssl_cipher - rc=(*proxy_sqlite3_bind_text)(statement, 11, r->fields[10] , -1, SQLITE_TRANSIENT); ASSERT_SQLITE_OK(rc, mydb); // ssl_protocol_version_range - rc=(*proxy_sqlite3_bind_text)(statement, 12, r->fields[11] , -1, SQLITE_TRANSIENT); ASSERT_SQLITE_OK(rc, mydb); // comment + rc=(*proxy_sqlite3_bind_text)(statement, 7, r->fields[6] , -1, SQLITE_TRANSIENT); ASSERT_SQLITE_OK(rc, mydb); // ssl_crl + rc=(*proxy_sqlite3_bind_text)(statement, 8, r->fields[7] , -1, SQLITE_TRANSIENT); ASSERT_SQLITE_OK(rc, mydb); // ssl_crlpath + rc=(*proxy_sqlite3_bind_text)(statement, 9, r->fields[8] , -1, SQLITE_TRANSIENT); ASSERT_SQLITE_OK(rc, mydb); // ssl_protocol_version_range + rc=(*proxy_sqlite3_bind_text)(statement, 10, r->fields[9] , -1, SQLITE_TRANSIENT); ASSERT_SQLITE_OK(rc, mydb); // comment SAFE_SQLITE3_STEP2(statement); rc=(*proxy_sqlite3_clear_bindings)(statement); ASSERT_SQLITE_OK(rc, mydb); @@ -4013,9 +4012,9 @@ void PgSQL_HostGroups_Manager::generate_pgsql_servers_ssl_params_table() { PgSQLServers_SslParams PSSP( r->fields[0], atoi(r->fields[1]), r->fields[2], - r->fields[3], r->fields[4], r->fields[5], - r->fields[6], r->fields[7], r->fields[8], - r->fields[9], r->fields[10], r->fields[11] + r->fields[3], r->fields[4], r->fields[5], + r->fields[6], r->fields[7], + r->fields[8], r->fields[9] ); string MapKey = PSSP.getMapKey(rand_del); PgSQL_Servers_SSL_Params_map.emplace(MapKey, PSSP); diff --git a/lib/PgSQL_Monitor.cpp b/lib/PgSQL_Monitor.cpp index 39ef010e3..30fab6727 100644 --- a/lib/PgSQL_Monitor.cpp +++ b/lib/PgSQL_Monitor.cpp @@ -301,6 +301,9 @@ struct mon_srv_t { string ssl_p2s_ca; string ssl_p2s_crl; string ssl_p2s_crlpath; + // Pre-parsed from ssl_protocol_version_range; empty when unset/malformed. + string ssl_min_protocol_version; + string ssl_max_protocol_version; } ssl_opt; }; @@ -456,7 +459,9 @@ vector ext_srvs(const unique_ptr& srvs_info) { ssl_params->ssl_cert, ssl_params->ssl_ca, ssl_params->ssl_crl, - ssl_params->ssl_crlpath + ssl_params->ssl_crlpath, + ssl_params->ssl_min_protocol_version, + ssl_params->ssl_max_protocol_version }; } } @@ -465,7 +470,9 @@ vector ext_srvs(const unique_ptr& srvs_info) { string { pgsql_thread___ssl_p2s_cert ? pgsql_thread___ssl_p2s_cert : "" }, string { pgsql_thread___ssl_p2s_ca ? pgsql_thread___ssl_p2s_ca : "" }, string { pgsql_thread___ssl_p2s_crl ? pgsql_thread___ssl_p2s_crl : "" }, - string { pgsql_thread___ssl_p2s_crlpath ? pgsql_thread___ssl_p2s_crlpath : ""} + string { pgsql_thread___ssl_p2s_crlpath ? pgsql_thread___ssl_p2s_crlpath : ""}, + string { "" }, + string { "" } }; }() }); @@ -1103,6 +1110,13 @@ string build_conn_str(const task_st_t& task_st) { append_conninfo_param(conninfo, "sslrootcert", srv_info.ssl_opt.ssl_p2s_ca); append_conninfo_param(conninfo, "sslcrl", srv_info.ssl_opt.ssl_p2s_crl); append_conninfo_param(conninfo, "sslcrldir", srv_info.ssl_opt.ssl_p2s_crlpath); + // Per-server TLS protocol pinning was pre-parsed from + // ssl_protocol_version_range when the row was loaded into + // PgSQLServers_SslParams. Empty fields => libpq defaults. + if (!srv_info.ssl_opt.ssl_min_protocol_version.empty()) + append_conninfo_param(conninfo, "ssl_min_protocol_version", srv_info.ssl_opt.ssl_min_protocol_version); + if (!srv_info.ssl_opt.ssl_max_protocol_version.empty()) + append_conninfo_param(conninfo, "ssl_max_protocol_version", srv_info.ssl_opt.ssl_max_protocol_version); } else { conninfo << "sslmode='disable' "; // not supporting SSL } diff --git a/lib/ProxySQL_Admin.cpp b/lib/ProxySQL_Admin.cpp index 3b8a2a077..8ac1b0309 100644 --- a/lib/ProxySQL_Admin.cpp +++ b/lib/ProxySQL_Admin.cpp @@ -7730,7 +7730,7 @@ void ProxySQL_Admin::save_pgsql_servers_runtime_to_database(bool _runtime) { StrQuery = "INSERT INTO "; if (_runtime) StrQuery += "runtime_"; - StrQuery += "pgsql_servers_ssl_params (hostname, port, username, ssl_ca, ssl_cert, ssl_key, ssl_capath, ssl_crl, ssl_crlpath, ssl_cipher, ssl_protocol_version_range, comment) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)"; + StrQuery += "pgsql_servers_ssl_params (hostname, port, username, ssl_ca, ssl_cert, ssl_key, ssl_crl, ssl_crlpath, ssl_protocol_version_range, comment) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)"; auto [rc1, statement_unique] = admindb->prepare_v2(StrQuery.c_str()); rc = rc1; statement = statement_unique.get(); @@ -7744,12 +7744,10 @@ void ProxySQL_Admin::save_pgsql_servers_runtime_to_database(bool _runtime) { rc=(*proxy_sqlite3_bind_text)(statement, 4, r->fields[3], -1, SQLITE_TRANSIENT); ASSERT_SQLITE_OK(rc, admindb); // ssl_ca rc=(*proxy_sqlite3_bind_text)(statement, 5, r->fields[4], -1, SQLITE_TRANSIENT); ASSERT_SQLITE_OK(rc, admindb); // ssl_cert rc=(*proxy_sqlite3_bind_text)(statement, 6, r->fields[5], -1, SQLITE_TRANSIENT); ASSERT_SQLITE_OK(rc, admindb); // ssl_key - rc=(*proxy_sqlite3_bind_text)(statement, 7, r->fields[6], -1, SQLITE_TRANSIENT); ASSERT_SQLITE_OK(rc, admindb); // ssl_capath - rc=(*proxy_sqlite3_bind_text)(statement, 8, r->fields[7], -1, SQLITE_TRANSIENT); ASSERT_SQLITE_OK(rc, admindb); // ssl_crl - rc=(*proxy_sqlite3_bind_text)(statement, 9, r->fields[8], -1, SQLITE_TRANSIENT); ASSERT_SQLITE_OK(rc, admindb); // ssl_crlpath - rc=(*proxy_sqlite3_bind_text)(statement, 10, r->fields[9], -1, SQLITE_TRANSIENT); ASSERT_SQLITE_OK(rc, admindb); // ssl_cipher - rc=(*proxy_sqlite3_bind_text)(statement, 11, r->fields[10], -1, SQLITE_TRANSIENT); ASSERT_SQLITE_OK(rc, admindb); // ssl_protocol_version_range - rc=(*proxy_sqlite3_bind_text)(statement, 12, r->fields[11], -1, SQLITE_TRANSIENT); ASSERT_SQLITE_OK(rc, admindb); // comment + rc=(*proxy_sqlite3_bind_text)(statement, 7, r->fields[6], -1, SQLITE_TRANSIENT); ASSERT_SQLITE_OK(rc, admindb); // ssl_crl + rc=(*proxy_sqlite3_bind_text)(statement, 8, r->fields[7], -1, SQLITE_TRANSIENT); ASSERT_SQLITE_OK(rc, admindb); // ssl_crlpath + rc=(*proxy_sqlite3_bind_text)(statement, 9, r->fields[8], -1, SQLITE_TRANSIENT); ASSERT_SQLITE_OK(rc, admindb); // ssl_protocol_version_range + rc=(*proxy_sqlite3_bind_text)(statement, 10, r->fields[9], -1, SQLITE_TRANSIENT); ASSERT_SQLITE_OK(rc, admindb); // comment SAFE_SQLITE3_STEP2(statement); rc=(*proxy_sqlite3_clear_bindings)(statement); ASSERT_SQLITE_OK(rc, admindb); diff --git a/test/tap/tests/pgsql-servers_ssl_params-t.cpp b/test/tap/tests/pgsql-servers_ssl_params-t.cpp index b915a27f9..d79b4ad11 100644 --- a/test/tap/tests/pgsql-servers_ssl_params-t.cpp +++ b/test/tap/tests/pgsql-servers_ssl_params-t.cpp @@ -151,10 +151,10 @@ static void test_insert_and_select(PGconn* admin) { ok(exec_ok(admin, "INSERT INTO pgsql_servers_ssl_params " - "(hostname, port, username, ssl_ca, ssl_cert, ssl_key, ssl_capath, " - "ssl_crl, ssl_crlpath, ssl_cipher, ssl_protocol_version_range, comment) " + "(hostname, port, username, ssl_ca, ssl_cert, ssl_key, " + "ssl_crl, ssl_crlpath, ssl_protocol_version_range, comment) " "VALUES ('testhost', 5432, 'testuser', '/ca.crt', '/cert.crt', '/key.pem', " - "'/capath', '/crl.pem', '/crlpath', 'AES256', 'TLSv1.2-TLSv1.3', 'test row')"), + "'/crl.pem', '/crlpath', 'TLSv1.2-TLSv1.3', 'test row')"), "INSERT into pgsql_servers_ssl_params succeeds"); int count = exec_count(admin, "SELECT * FROM pgsql_servers_ssl_params"); @@ -438,7 +438,71 @@ static void test_monitor_ssl_with_per_server_params(PGconn* admin) { diag("After PgSQL_Monitor_ssl_connections_OK: %ld", after_ssl); ok(after_ssl > initial_ssl, - "Monitor SSL counter increased with use_ssl=1"); + "Monitor SSL counter increased with use_ssl=1 and no per-server row"); +} + +/** + * @brief Verify the monitor path actually consults pgsql_servers_ssl_params. + * + * Inserts a per-server row matching the actual backend with username='' + * (so the monitor's empty-username fallback in get_Server_SSL_Params hits + * it) and an ssl_protocol_version_range pinned to TLSv1 — disabled in + * modern PostgreSQL builds. If the monitor honors per-server params and + * propagates tls_version into its libpq conninfo, monitor SSL connections + * must fail and the OK counter must NOT advance over the wait window. + * If the monitor were ignoring per-server params (or dropping tls_version), + * the counter would keep climbing, which is the regression we want to catch. + */ +static void test_monitor_uses_per_server_row(PGconn* admin) { + std::string hostname; + int port; + if (!get_backend_server(admin, hostname, port)) { + ok(0, "Monitor per-server: no backend server found"); + ok(0, "Monitor per-server: cleanup restores monitor SSL OK"); + return; + } + + cleanup_ssl_params(admin); + exec_ok(admin, "UPDATE pgsql_servers SET use_ssl=1"); + exec_ok(admin, "LOAD PGSQL SERVERS TO RUNTIME"); + + // Insert a per-server row matching the backend, with TLSv1 pin so the + // monitor's connection attempt must fail if tls_version flows through. + std::stringstream q; + q << "INSERT INTO pgsql_servers_ssl_params " + "(hostname, port, username, ssl_protocol_version_range) VALUES ('" + << hostname << "', " << port << ", '', 'TLSv1')"; + exec_ok(admin, q.str().c_str()); + exec_ok(admin, "LOAD PGSQL SERVERS TO RUNTIME"); + + long ok_before = getMonitorValue(admin, "PgSQL_Monitor_ssl_connections_OK"); + diag("With TLSv1 per-server pin, ssl OK before wait: %ld", ok_before); + + usleep(3000000); // 3 seconds — multiple monitor cycles + + long ok_after = getMonitorValue(admin, "PgSQL_Monitor_ssl_connections_OK"); + diag("With TLSv1 per-server pin, ssl OK after wait: %ld (delta=%ld)", + ok_after, ok_after - ok_before); + + ok(ok_after == ok_before, + "Monitor per-server: SSL OK counter does NOT advance when " + "per-server row pins ssl_protocol_version_range to TLSv1"); + + // Phase 2: remove the row and confirm the monitor recovers, proving + // the previous failure was caused by the per-server row and not by + // some unrelated breakage. + cleanup_ssl_params(admin); + exec_ok(admin, "LOAD PGSQL SERVERS TO RUNTIME"); + + long recover_before = getMonitorValue(admin, "PgSQL_Monitor_ssl_connections_OK"); + usleep(3000000); + long recover_after = getMonitorValue(admin, "PgSQL_Monitor_ssl_connections_OK"); + diag("After cleanup, ssl OK recovered from %ld to %ld", + recover_before, recover_after); + + ok(recover_after > recover_before, + "Monitor per-server: SSL OK counter resumes advancing after " + "removing the per-server row"); } // ============================================================================ @@ -446,7 +510,7 @@ static void test_monitor_ssl_with_per_server_params(PGconn* admin) { // ============================================================================ int main(int argc, char** argv) { - plan(31); + plan(34); if (cl.getEnv()) { BAIL_OUT("Failed to get environment variables"); @@ -482,6 +546,8 @@ int main(int argc, char** argv) { test_tls_version_pin_causes_failure(a); test_per_server_overrides_global(a); test_remove_per_server_fallback_to_global(a); + test_monitor_ssl_with_per_server_params(a); + test_monitor_uses_per_server_row(a); // Cleanup remove_bogus_cert_file(); cleanup_ssl_params(a); diff --git a/test/tap/tests/unit/pgsql_servers_ssl_params_unit-t.cpp b/test/tap/tests/unit/pgsql_servers_ssl_params_unit-t.cpp index 2d74fa086..e1a453363 100644 --- a/test/tap/tests/unit/pgsql_servers_ssl_params_unit-t.cpp +++ b/test/tap/tests/unit/pgsql_servers_ssl_params_unit-t.cpp @@ -47,6 +47,23 @@ static void test_base_constructor_string() { ok(p.comment == "test comment", "string ctor: comment"); } +static void test_base_constructor_3arg() { + Servers_SslParams p(string("db3.example.com"), 5434, string("readonly")); + + ok(p.hostname == "db3.example.com", "3-arg ctor: hostname"); + ok(p.port == 5434, "3-arg ctor: port"); + ok(p.username == "readonly", "3-arg ctor: username"); + ok(p.ssl_ca == "", "3-arg ctor: ssl_ca defaults to empty"); + ok(p.ssl_cert == "", "3-arg ctor: ssl_cert defaults to empty"); + ok(p.ssl_key == "", "3-arg ctor: ssl_key defaults to empty"); + ok(p.ssl_capath == "", "3-arg ctor: ssl_capath defaults to empty"); + ok(p.ssl_crl == "", "3-arg ctor: ssl_crl defaults to empty"); + ok(p.ssl_crlpath == "", "3-arg ctor: ssl_crlpath defaults to empty"); + ok(p.ssl_cipher == "", "3-arg ctor: ssl_cipher defaults to empty"); + ok(p.tls_version == "", "3-arg ctor: tls_version defaults to empty"); + ok(p.comment == "", "3-arg ctor: comment defaults to empty"); +} + static void test_base_constructor_charptr() { char h[] = "db2.example.com"; char u[] = "admin"; @@ -110,16 +127,48 @@ static void test_pgsql_derived_class() { PgSQLServers_SslParams p( string("pghost"), 5432, string("pguser"), string("/pg/ca.crt"), string("/pg/cert.crt"), string("/pg/key.pem"), - string(""), string(""), string(""), - string(""), string("TLSv1.2-TLSv1.3"), string("pg comment") + string(""), string(""), + string("TLSv1.2-TLSv1.3"), string("pg comment") ); ok(p.hostname == "pghost", "PgSQL derived: hostname inherited"); ok(p.ssl_ca == "/pg/ca.crt", "PgSQL derived: ssl_ca inherited"); ok(p.tls_version == "TLSv1.2-TLSv1.3", "PgSQL derived: tls_version inherited"); + ok(p.ssl_min_protocol_version == "TLSv1.2", "PgSQL derived: parsed min from range"); + ok(p.ssl_max_protocol_version == "TLSv1.3", "PgSQL derived: parsed max from range"); string key = p.getMapKey("|"); ok(key == "pghost|5432|pguser", "PgSQL derived: getMapKey inherited"); + + // Single token pins both ends + PgSQLServers_SslParams pin( + string("pinhost"), 5432, string(""), + string(""), string(""), string(""), + string(""), string(""), + string("TLSv1.3"), string("") + ); + ok(pin.ssl_min_protocol_version == "TLSv1.3", "PgSQL derived: single-token pin -> min"); + ok(pin.ssl_max_protocol_version == "TLSv1.3", "PgSQL derived: single-token pin -> max"); + + // Empty tls_version leaves both empty + PgSQLServers_SslParams empty( + string("eh"), 5432, string(""), + string(""), string(""), string(""), + string(""), string(""), + string(""), string("") + ); + ok(empty.ssl_min_protocol_version == "", "PgSQL derived: empty tls_version -> empty min"); + ok(empty.ssl_max_protocol_version == "", "PgSQL derived: empty tls_version -> empty max"); + + // Malformed (one side missing) leaves both empty + PgSQLServers_SslParams bad( + string("bh"), 5432, string(""), + string(""), string(""), string(""), + string(""), string(""), + string("TLSv1.2-"), string("") + ); + ok(bad.ssl_min_protocol_version == "", "PgSQL derived: malformed -> empty min"); + ok(bad.ssl_max_protocol_version == "", "PgSQL derived: malformed -> empty max"); } static void test_pgsql_storable_in_map() { @@ -127,13 +176,15 @@ static void test_pgsql_storable_in_map() { PgSQLServers_SslParams p1( string("host1"), 5432, string("user1"), - string("/ca1"), string(""), string(""), string(""), - string(""), string(""), string(""), string(""), string("") + string("/ca1"), string(""), string(""), + string(""), string(""), + string(""), string("") ); PgSQLServers_SslParams p2( string("host2"), 5433, string("user2"), - string("/ca2"), string(""), string(""), string(""), - string(""), string(""), string(""), string(""), string("") + string("/ca2"), string(""), string(""), + string(""), string(""), + string(""), string("") ); m.emplace("key1", p1); @@ -149,29 +200,31 @@ static void test_pgsql_storable_in_map() { // ============================================================================ static void populate_ssl_params() { - SQLite3_result *result = new SQLite3_result(12); + // Schema columns: hostname, port, username, ssl_ca, ssl_cert, ssl_key, + // ssl_crl, ssl_crlpath, ssl_protocol_version_range, comment + SQLite3_result *result = new SQLite3_result(10); char *row1[] = { (char*)"host1", (char*)"5432", (char*)"testuser", (char*)"/certs/ca1.crt", (char*)"/certs/cert1.crt", (char*)"/certs/key1.pem", - (char*)"", (char*)"", (char*)"", - (char*)"", (char*)"TLSv1.3", (char*)"exact match row" + (char*)"", (char*)"", + (char*)"TLSv1.3", (char*)"exact match row" }; result->add_row(row1); char *row2[] = { (char*)"host1", (char*)"5432", (char*)"", (char*)"/certs/ca_fallback.crt", (char*)"/certs/cert_fb.crt", (char*)"/certs/key_fb.pem", - (char*)"", (char*)"", (char*)"", - (char*)"", (char*)"", (char*)"fallback row" + (char*)"", (char*)"", + (char*)"", (char*)"fallback row" }; result->add_row(row2); char *row3[] = { (char*)"host2", (char*)"5433", (char*)"admin", (char*)"/certs/ca2.crt", (char*)"", (char*)"", - (char*)"", (char*)"", (char*)"", - (char*)"AES256", (char*)"TLSv1.2-TLSv1.3", (char*)"host2 row" + (char*)"", (char*)"", + (char*)"TLSv1.2-TLSv1.3", (char*)"host2 row" }; result->add_row(row3); @@ -246,11 +299,13 @@ static void test_lookup_different_host() { ); ok(p != NULL, "different host: found host2:5433:admin"); if (p) { - ok(p->ssl_cipher == "AES256", "different host: ssl_cipher correct"); + ok(p->ssl_ca == "/certs/ca2.crt", "different host: ssl_ca correct"); ok(p->tls_version == "TLSv1.2-TLSv1.3", "different host: tls_version correct"); + ok(p->ssl_min_protocol_version == "TLSv1.2", "different host: parsed min"); + ok(p->ssl_max_protocol_version == "TLSv1.3", "different host: parsed max"); delete p; } else { - skip(2, "host2 not found"); + skip(4, "host2 not found"); } } @@ -259,11 +314,12 @@ static void test_lookup_different_host() { // ============================================================================ int main() { - plan(45); + plan(67); test_init_minimal(); test_base_constructor_string(); + test_base_constructor_3arg(); test_base_constructor_charptr(); test_getMapKey(); test_getMapKey_empty_username();