Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions include/PgSQL_Monitor.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ struct PgSQL_Monitor {
uint64_t ping_check_OK { 0 };
uint64_t readonly_check_ERR { 0 };
uint64_t readonly_check_OK { 0 };
uint64_t ssl_connections_OK { 0 };
uint64_t non_ssl_connections_OK { 0 };
///////////////////////////////////////////////////////////////////////////

std::vector<table_def_t> tables_defs_monitor {
Expand Down
68 changes: 57 additions & 11 deletions lib/PgSQL_Monitor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,13 @@ struct mon_srv_t {
string addr;
uint16_t port;
bool ssl;
struct ssl_opts_t {
string ssl_p2s_key;
string ssl_p2s_cert;
string ssl_p2s_ca;
string ssl_p2s_crl;
string ssl_p2s_crlpath;
} ssl_opt;
};

struct mon_user_t {
Expand Down Expand Up @@ -353,15 +360,21 @@ unique_ptr<SQLite3_result> fetch_hgm_srvs_conf(PgSQL_HostGroups_Manager* hgm, co

vector<mon_srv_t> ext_srvs(const unique_ptr<SQLite3_result>& srvs_info) {
vector<mon_srv_t> srvs {};

srvs.reserve(srvs_info->rows.size());
for (const auto& row : srvs_info->rows) {
srvs.push_back({
string { row->fields[0] },
static_cast<uint16_t>(std::atoi(row->fields[1])),
static_cast<bool>(std::atoi(row->fields[2]))
static_cast<bool>(std::atoi(row->fields[2])),
mon_srv_t::ssl_opts_t {
string { pgsql_thread___ssl_p2s_key ? pgsql_thread___ssl_p2s_key : ""},
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 : ""}
}
});
}

return srvs;
}

Expand Down Expand Up @@ -624,6 +637,13 @@ pair<short,bool> handle_async_connect_cont(state_t& st, short revent) {
case PGRES_POLLING_OK:
pgconn.state = ASYNC_ST::ASYNC_CONNECT_END;

// connection successful, update SSL stats
if (PQsslInUse(pgconn.conn)) {
__sync_fetch_and_add(&GloPgMon->ssl_connections_OK, 1);
} else {
__sync_fetch_and_add(&GloPgMon->non_ssl_connections_OK, 1);
}

if (st.task.type == task_type_t::connect) {
st.task.end = monotonic_time();
} else if (st.task.type == task_type_t::ping) {
Expand Down Expand Up @@ -870,18 +890,44 @@ pair<bool,pgsql_conn_t> get_task_conn(conn_pool_t& conn_pool, task_st_t& task_st
}
}

static void append_conninfo_param(std::ostringstream& conninfo, const std::string& key, const std::string& val) {
if (val.empty()) return;

std::string escaped_val;
escaped_val.reserve(val.length() * 2); // Reserve maximum possible size

for (char c : val) {
if (c == '\'' || c == '\\') {
escaped_val.push_back('\\');
}
escaped_val.push_back(c);
}

conninfo << key << "='" << escaped_val << "' ";
}

string build_conn_str(const task_st_t& task_st) {
const mon_srv_t& srv_info { task_st.op_st.srv_info };
const mon_user_t& user_info { task_st.op_st.user_info };

return string {
"host='" + srv_info.addr + "' "
+ "port='" + std::to_string(srv_info.port) + "' "
+ "user='" + user_info.user + "' "
+ "password='" + user_info.pass + "' "
+ "dbname='" + user_info.dbname + "' "
+ "application_name=ProxySQL-Monitor"
};
std::ostringstream conninfo;
append_conninfo_param(conninfo, "user", user_info.user); // username
append_conninfo_param(conninfo, "password", user_info.pass); // password
append_conninfo_param(conninfo, "dbname", user_info.dbname); // dbname
append_conninfo_param(conninfo, "host", srv_info.addr); // backend address
conninfo << "port=" << srv_info.port << " "; // backend port
conninfo << "application_name=ProxySQL-Monitor "; // application name
if (srv_info.ssl) {
conninfo << "sslmode='require' "; // SSL required
append_conninfo_param(conninfo, "sslkey", srv_info.ssl_opt.ssl_p2s_key);
append_conninfo_param(conninfo, "sslcert", srv_info.ssl_opt.ssl_p2s_cert);
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);
} else {
conninfo << "sslmode='disable' "; // not supporting SSL
}
return conninfo.str();
}

pgsql_conn_t create_new_conn(task_st_t& task_st) {
Expand Down
12 changes: 12 additions & 0 deletions lib/PgSQL_Thread.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4482,6 +4482,18 @@ SQLite3_result* PgSQL_Threads_Handler::SQL3_GlobalStatus(bool _memory) {
pta[1] = buf;
result->add_row(pta);
}
{
pta[0] = (char*)"PgSQL_Monitor_ssl_connections_OK";
sprintf(buf, "%lu", GloPgMon->ssl_connections_OK);
pta[1] = buf;
result->add_row(pta);
}
{
pta[0] = (char*)"PgSQL_Monitor_non_ssl_connections_OK";
sprintf(buf, "%lu", GloPgMon->non_ssl_connections_OK);
pta[1] = buf;
result->add_row(pta);
}
/*
{
pta[0] = (char*)"MySQL_Monitor_replication_lag_check_OK";
Expand Down
3 changes: 2 additions & 1 deletion test/tap/groups/groups.json
Original file line number Diff line number Diff line change
Expand Up @@ -234,5 +234,6 @@
"test_ssl_fast_forward-2_libmysql-t": [ "default-g4", "mysql-auto_increment_delay_multiplex=0-g4", "mysql-multiplexing=false-g4", "mysql-query_digests=0-g4", "mysql-query_digests_keep_comment=1-g4" ],
"test_ssl_fast_forward-3_libmariadb-t": [ "default-g4", "mysql-auto_increment_delay_multiplex=0-g4", "mysql-multiplexing=false-g4", "mysql-query_digests=0-g4", "mysql-query_digests_keep_comment=1-g4" ],
"test_ssl_fast_forward-3_libmysql-t": [ "default-g4", "mysql-auto_increment_delay_multiplex=0-g4", "mysql-multiplexing=false-g4", "mysql-query_digests=0-g4", "mysql-query_digests_keep_comment=1-g4" ],
"test_ignore_min_gtid-t": [ "default-g4", "mysql-auto_increment_delay_multiplex=0-g4", "mysql-multiplexing=false-g4", "mysql-query_digests=0-g4", "mysql-query_digests_keep_comment=1-g4" ]
"test_ignore_min_gtid-t": [ "default-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-monitor_ssl_connections_test-t": [ "default-g4", "mysql-auto_increment_delay_multiplex=0-g4", "mysql-multiplexing=false-g4", "mysql-query_digests=0-g4", "mysql-query_digests_keep_comment=1-g4" ]
}
191 changes: 191 additions & 0 deletions test/tap/tests/pgsql-monitor_ssl_connections_test-t.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
/**
* @file pgsql-monitor_ssl_connections_test-t.cpp
* @brief Intention: validate that ProxySQL's PostgreSQL monitor correctly establishes SSL and non-SSL
* connections depending on server configuration. The test runs in two phases: first with `use_ssl=1`
* to ensure only SSL connection counters increase, and then with `use_ssl=0` to ensure only non-SSL
* counters increase.
*/

#include <unistd.h>
#include <string>
#include <sstream>
#include <chrono>
#include <thread>
#include "libpq-fe.h"
#include "command_line.h"
#include "tap.h"
#include "utils.h"

CommandLine cl;

using PGConnPtr = std::unique_ptr<PGconn, decltype(&PQfinish)>;

enum ConnType {
ADMIN,
BACKEND
};

PGConnPtr createNewConnection(ConnType conn_type, const std::string& options = "", bool with_ssl = false) {

const char* host = (conn_type == BACKEND) ? cl.pgsql_host : cl.pgsql_admin_host;
int port = (conn_type == BACKEND) ? cl.pgsql_port : cl.pgsql_admin_port;
const char* username = (conn_type == BACKEND) ? cl.pgsql_root_username : cl.admin_username;
const char* password = (conn_type == BACKEND) ? cl.pgsql_root_password : cl.admin_password;

std::stringstream ss;

ss << "host=" << host << " port=" << port;
ss << " user=" << username << " password=" << password;
ss << (with_ssl ? " sslmode=require" : " sslmode=disable");

if (options.empty() == false) {
ss << " options='" << options << "'";
}

PGconn* conn = PQconnectdb(ss.str().c_str());
if (PQstatus(conn) != CONNECTION_OK) {
fprintf(stderr, "Connection failed to '%s': %s", (conn_type == BACKEND ? "Backend" : "Admin"), PQerrorMessage(conn));
PQfinish(conn);
return PGConnPtr(nullptr, &PQfinish);
}
return PGConnPtr(conn, &PQfinish);
}

static long getMonitorValue(PGConnPtr& admin, const char* varname) {
std::stringstream q;
q << "SELECT Variable_Value FROM stats_pgsql_global "
"WHERE Variable_Name='" << varname << "';";

PGresult* res = PQexec(admin.get(), q.str().c_str());
if (PQresultStatus(res) != PGRES_TUPLES_OK || PQntuples(res) == 0) {
PQclear(res);
return -1;
}
long v = atol(PQgetvalue(res, 0, 0));
PQclear(res);
return v;
}

static long getConnectInterval(PGConnPtr& admin) {
PGresult* res = PQexec(admin.get(),
"SELECT Variable_Value FROM global_variables WHERE Variable_Name='pgsql-monitor_connect_interval';"
);
if (PQresultStatus(res) != PGRES_TUPLES_OK || PQntuples(res) == 0) {
PQclear(res);
return 1000; // default fallback
}
long v = atol(PQgetvalue(res, 0, 0));
PQclear(res);
return v;
}

static bool setUseSSL(PGConnPtr& admin, int value) {
std::stringstream q;
q << "UPDATE pgsql_servers SET use_ssl=" << value << ";";
PGresult* res = PQexec(admin.get(), q.str().c_str());
bool ok = PQresultStatus(res) == PGRES_COMMAND_OK;
PQclear(res);

PGresult* load = PQexec(admin.get(), "LOAD PGSQL SERVERS TO RUNTIME;");
bool ok2 = PQresultStatus(load) == PGRES_COMMAND_OK;
PQclear(load);
usleep(10000);
return ok && ok2;
}

static bool setConnectInterval(PGConnPtr& admin, int value) {
std::stringstream q;
q << "SET pgsql-monitor_connect_interval=" << value << ";";
PGresult* res = PQexec(admin.get(), q.str().c_str());
bool ok = PQresultStatus(res) == PGRES_COMMAND_OK;
PQclear(res);

PGresult* load = PQexec(admin.get(), "LOAD PGSQL VARIABLES TO RUNTIME;");
bool ok2 = PQresultStatus(load) == PGRES_COMMAND_OK;
PQclear(load);
usleep(10000);
return ok && ok2;
}

int main(int argc, char** argv) {

plan(7);

if (cl.getEnv())
return exit_status();

// -----------------------------------------
// Connect to ADMIN
// -----------------------------------------
auto admin = createNewConnection(ADMIN);
ok(admin != nullptr, "ADMIN connection created");

long original_connect_interval_ms = getConnectInterval(admin);
diag("Original pgsql-monitor_connect_interval = %ld ms", original_connect_interval_ms);

setConnectInterval(admin, 2000); // set to 2 second for faster test
usleep(original_connect_interval_ms * 1000); // microseconds

long connect_interval_ms = getConnectInterval(admin);
diag("Updated pgsql-monitor_connect_interval = %ld ms", connect_interval_ms);

// ###############################################################
// PHASE 1: TEST SSL (use_ssl = 1)
// ###############################################################
diag("---- PHASE 1: Checking SSL monitoring ----");

ok(setUseSSL(admin, 1), "Set pgsql_server -> use_ssl = 1");

long initial_ssl = getMonitorValue(admin, "PgSQL_Monitor_ssl_connections_OK");
long initial_non = getMonitorValue(admin, "PgSQL_Monitor_non_ssl_connections_OK");

diag("Initial SSL OK: %ld", initial_ssl);
diag("Initial NON-SSL OK: %ld", initial_non);

usleep((connect_interval_ms * 2) * 1000); // microseconds

long after_ssl = getMonitorValue(admin, "PgSQL_Monitor_ssl_connections_OK");
long after_non = getMonitorValue(admin, "PgSQL_Monitor_non_ssl_connections_OK");

diag("After SSL mode -> SSL OK: %ld", after_ssl);
diag("After SSL mode -> NON-SSL OK: %ld", after_non);

ok(after_ssl > initial_ssl,
"SSL monitoring increased when use_ssl=1");

ok(after_non == initial_non,
"NON-SSL monitoring unchanged when use_ssl=1");

// ###############################################################
// PHASE 2: TEST NON-SSL (use_ssl = 0)
// ###############################################################
diag("---- PHASE 2: Checking NON-SSL monitoring ----");

ok(setUseSSL(admin, 0), "Set pgsql_server -> use_ssl = 0");

long initial_ssl2 = getMonitorValue(admin, "PgSQL_Monitor_ssl_connections_OK");
long initial_non2 = getMonitorValue(admin, "PgSQL_Monitor_non_ssl_connections_OK");

diag("Initial SSL OK (phase2): %ld", initial_ssl2);
diag("Initial NON-SSL OK (phase2): %ld", initial_non2);

usleep((connect_interval_ms * 2) * 1000); // microseconds

long after_ssl2 = getMonitorValue(admin, "PgSQL_Monitor_ssl_connections_OK");
long after_non2 = getMonitorValue(admin, "PgSQL_Monitor_non_ssl_connections_OK");

diag("After NON-SSL mode -> SSL OK: %ld", after_ssl2);
diag("After NON-SSL mode -> NON-SSL OK: %ld", after_non2);

ok(after_non2 > initial_non2,
"NON-SSL monitoring increased when use_ssl=0");

ok(after_ssl2 == initial_ssl2,
"SSL monitoring unchanged when use_ssl=0");

diag("SSL + NON-SSL monitoring test completed successfully");

setConnectInterval(admin, original_connect_interval_ms); // reset to original value

return exit_status();
}