feat(mysqlx): add MysqlxBackendTlsMode enum + mysqlx_tls_backend_mode runtime variable

Wires up the configuration plumbing for issue #5693 (P1: asymmetric TLS
/ AsClient mode parity gap with MySQL Router 8.0). This commit is
behaviour-neutral: the new variable is parsed, validated, persisted and
exposed via MysqlxConfigStore::get_backend_tls_mode(), but the per-
session backend-TLS decision still uses the legacy
target_use_ssl_ || client_ds_.is_encrypted() expression. The decision
site is rewritten in the next commit.

What this adds:

* MysqlxBackendTlsMode enum with four values matching MySQL Router's
  client_ssl_mode / server_ssl_mode taxonomy: disabled, preferred,
  required, as_client. Default is as_client because that most closely
  matches the legacy implicit behaviour where the backend leg
  encryption was tied to the frontend leg's encryption.

* mysqlx_backend_tls_mode_from_string() / mysqlx_backend_tls_mode_to_string()
  for case-insensitive parsing and canonical lower-case rendering.
  The parser returns std::optional so the install path can surface a
  useful error to the operator on a typo instead of silently coercing
  to a default.

* MysqlxConfigStore now reads the mysqlx_tls_backend_mode key from
  mysqlx_variables in install_variables_from_admin(), persists it via
  save_variables_to_admin_table(), and projects it in
  project_variables_to_runtime_view(). install fails atomically with a
  descriptive error when the value is unrecognised; an absent row
  leaves the cached mode untouched (matches how the other tunables
  already behave).

* MysqlxBackendEndpoint.use_ssl=1 remains an operator-controlled
  override that forces TLS regardless of the mode (per existing
  comment at handler_connecting_server). The mode interacts with that
  flag in the next commit.

Tests:

  test/tap/tests/unit/mysqlx_config_store_unit-t: 16 -> 24 assertions.
  New coverage: parser accepts all four documented values
  case-insensitively, parser rejects unknown values, default mode is
  as_client, LOAD round-trip caches the parsed mode, invalid value
  fails install with descriptive error, store retains last-good mode
  after rejected install, absent row leaves cached mode untouched.

Tested under NOJEMALLOC=1 WITHASAN=1 PROXYSQLGENAI=1.

Stacks on PR #5706 (mysqlx response state machines), which itself
stacks on PR #5704 (mysqlx observability P0). Refs #5693.
feature/mysqlx-asymmetric-tls
Rene Cannao 2 months ago
parent b99f9cc538
commit f051c89f75

@ -20,6 +20,47 @@ enum class MysqlxBackendAuthMode : uint8_t {
MysqlxBackendAuthMode mysqlx_backend_auth_mode_from_string(const std::string& value);
// MysqlxBackendTlsMode mirrors MySQL Router 8.0's `client_ssl_mode` /
// `server_ssl_mode` family for the proxy->backend leg of a session.
//
// * disabled -- never wrap the backend connection in TLS, regardless of
// whether the client is using TLS. Plaintext only.
// * preferred -- send `CapabilitiesSet(tls=true)` to the backend; on
// Mysqlx::Error, fall back to plaintext authentication.
// Lets ProxySQL adapt to a mixed fleet where some
// backends have TLS configured and some do not.
// * required -- send `CapabilitiesSet(tls=true)`; on Mysqlx::Error
// fail the backend connect. Use when policy mandates
// encryption proxy<->backend.
// * as_client -- mirror the client's TLS choice. If the client
// connected over TLS, encrypt the backend leg too;
// if the client connected in plaintext, leave the
// backend leg in plaintext. Matches MySQL Router's
// AsClient semantics. This is the default because it
// most closely matches the previous (pre-mode-aware)
// ProxySQL behaviour where backend TLS was implicitly
// driven by `client_ds_.is_encrypted()`.
//
// `mysqlx_backend_endpoints.use_ssl=1` remains an operator-controlled
// override that forces TLS regardless of the mode (so an operator can
// pin a single sensitive backend to TLS even under mode=disabled).
enum class MysqlxBackendTlsMode : uint8_t {
disabled = 0,
preferred = 1,
required = 2,
as_client = 3
};
// Parses the string form of MysqlxBackendTlsMode (case-insensitive).
// Returns std::nullopt on an unrecognised value so the caller can
// surface a useful error to the operator instead of silently coercing
// to a default. Accepted values: "disabled", "preferred", "required",
// "as_client".
std::optional<MysqlxBackendTlsMode> mysqlx_backend_tls_mode_from_string(const std::string& value);
// Canonical lower-case rendering for SAVE / runtime-view projection.
const char* mysqlx_backend_tls_mode_to_string(MysqlxBackendTlsMode m);
struct MysqlxResolvedIdentity {
std::string username {};
std::string password {};
@ -139,6 +180,11 @@ public:
int get_connect_timeout() const;
std::string get_tls_mode() const;
int get_max_cached_connections() const;
// Returns the parsed mysqlx_tls_backend_mode currently in effect.
// Defaults to MysqlxBackendTlsMode::as_client (the legacy implicit
// behaviour) until install_variables_from_admin parses a different
// value.
MysqlxBackendTlsMode get_backend_tls_mode() const;
private:
MysqlxBackendEndpoint pick_from_hostgroup(int hostgroup_id, const std::string& strategy) const;
@ -160,6 +206,11 @@ private:
int connect_timeout_ { 10000 };
std::string tls_mode_ { "DISABLED" };
int max_cached_connections_ { 100 };
// Default `as_client` matches the pre-modeaware behaviour where
// backend TLS was implicitly tied to client_ds_.is_encrypted() at
// resolve time, so existing deployments see no behavioural change
// after upgrading.
MysqlxBackendTlsMode backend_tls_mode_ { MysqlxBackendTlsMode::as_client };
};
#endif /* PROXYSQL_MYSQLX_CONFIG_STORE_H */

@ -169,12 +169,29 @@ void load_backend_servers(
}
}
// load_variables walks `mysqlx_variables` rows and writes the
// recognised tunables back through reference parameters. The
// backend-TLS-mode field is captured as a raw string + a parsed enum +
// a "did the operator set it" flag so that:
//
// * an unrecognised value can be reported back as a useful error
// instead of silently coerced to the default,
// * an absent row leaves the existing `backend_tls_mode_` cached on
// the store untouched (rather than resetting to as_client on every
// LOAD), matching how the other variables behave.
//
// `parse_err` is populated with the first malformed row encountered;
// the caller is responsible for surfacing it back to the operator and
// aborting the install if it is non-empty.
void load_variables(
SQLite3_result& rows,
int& thread_pool_size,
int& connect_timeout,
std::string& tls_mode,
int& max_cached_connections
int& max_cached_connections,
MysqlxBackendTlsMode& backend_tls_mode,
bool& backend_tls_mode_set,
std::string& parse_err
) {
for (auto* row : rows.rows) {
if (row == nullptr || row->fields[0] == nullptr) {
@ -190,6 +207,18 @@ void load_variables(
tls_mode = value;
} else if (name == "mysqlx_max_cached_connections_per_thread") {
max_cached_connections = std::atoi(value);
} else if (name == "mysqlx_tls_backend_mode") {
auto parsed = mysqlx_backend_tls_mode_from_string(value);
if (!parsed) {
if (parse_err.empty()) {
parse_err = "invalid mysqlx_tls_backend_mode '";
parse_err += value;
parse_err += "' (expected one of: disabled, preferred, required, as_client)";
}
continue;
}
backend_tls_mode = *parsed;
backend_tls_mode_set = true;
}
}
}
@ -232,6 +261,32 @@ MysqlxBackendAuthMode mysqlx_backend_auth_mode_from_string(const std::string& va
return MysqlxBackendAuthMode::mapped;
}
std::optional<MysqlxBackendTlsMode> mysqlx_backend_tls_mode_from_string(const std::string& value) {
if (strcasecmp(value.c_str(), "disabled") == 0) {
return MysqlxBackendTlsMode::disabled;
}
if (strcasecmp(value.c_str(), "preferred") == 0) {
return MysqlxBackendTlsMode::preferred;
}
if (strcasecmp(value.c_str(), "required") == 0) {
return MysqlxBackendTlsMode::required;
}
if (strcasecmp(value.c_str(), "as_client") == 0) {
return MysqlxBackendTlsMode::as_client;
}
return std::nullopt;
}
const char* mysqlx_backend_tls_mode_to_string(MysqlxBackendTlsMode m) {
switch (m) {
case MysqlxBackendTlsMode::disabled: return "disabled";
case MysqlxBackendTlsMode::preferred: return "preferred";
case MysqlxBackendTlsMode::required: return "required";
case MysqlxBackendTlsMode::as_client: return "as_client";
}
return "as_client";
}
// install_users_from_admin
//
// Reads two admin-side tables and atomically swaps `identities_` under
@ -374,6 +429,11 @@ bool MysqlxConfigStore::install_variables_from_admin(SQLite3DB& db, std::string&
int new_connect_timeout = connect_timeout_;
std::string new_tls_mode = tls_mode_;
int new_max_cached = max_cached_connections_;
// Seed with the currently-installed value so an absent
// mysqlx_tls_backend_mode row leaves the cached mode untouched.
MysqlxBackendTlsMode new_backend_tls_mode = backend_tls_mode_;
bool backend_tls_mode_set = false;
std::string parse_err;
std::unique_ptr<SQLite3_result> result {};
if (!fetch_result(
@ -383,13 +443,21 @@ bool MysqlxConfigStore::install_variables_from_admin(SQLite3DB& db, std::string&
err)) {
return false;
}
load_variables(*result, new_pool_size, new_connect_timeout, new_tls_mode, new_max_cached);
load_variables(*result, new_pool_size, new_connect_timeout, new_tls_mode, new_max_cached,
new_backend_tls_mode, backend_tls_mode_set, parse_err);
if (!parse_err.empty()) {
err = std::move(parse_err);
return false;
}
std::unique_lock<std::shared_mutex> lock(mutex_);
thread_pool_size_ = new_pool_size;
connect_timeout_ = new_connect_timeout;
tls_mode_ = std::move(new_tls_mode);
max_cached_connections_ = new_max_cached;
if (backend_tls_mode_set) {
backend_tls_mode_ = new_backend_tls_mode;
}
return true;
}
@ -514,6 +582,9 @@ bool MysqlxConfigStore::save_variables_to_admin_table(SQLite3DB& db) const {
if (!put("mysqlx_max_cached_connections_per_thread", std::to_string(max_cached_connections_))) {
db.execute("ROLLBACK"); return false;
}
if (!put("mysqlx_tls_backend_mode", mysqlx_backend_tls_mode_to_string(backend_tls_mode_))) {
db.execute("ROLLBACK"); return false;
}
return db.execute("COMMIT");
}
@ -609,7 +680,8 @@ void MysqlxConfigStore::project_variables_to_runtime_view(SQLite3DB& db) const {
if (!put("mysqlx_thread_pool_size", std::to_string(thread_pool_size_)) ||
!put("mysqlx_connect_timeout", std::to_string(connect_timeout_)) ||
!put("mysqlx_tls_mode", tls_mode_) ||
!put("mysqlx_max_cached_connections_per_thread", std::to_string(max_cached_connections_))) {
!put("mysqlx_max_cached_connections_per_thread", std::to_string(max_cached_connections_)) ||
!put("mysqlx_tls_backend_mode", mysqlx_backend_tls_mode_to_string(backend_tls_mode_))) {
db.execute("ROLLBACK"); return;
}
db.execute("COMMIT");
@ -723,3 +795,8 @@ int MysqlxConfigStore::get_max_cached_connections() const {
std::shared_lock<std::shared_mutex> lock(mutex_);
return max_cached_connections_;
}
MysqlxBackendTlsMode MysqlxConfigStore::get_backend_tls_mode() const {
std::shared_lock<std::shared_mutex> lock(mutex_);
return backend_tls_mode_;
}

@ -62,7 +62,7 @@ std::unique_ptr<SQLite3DB> create_test_db() {
int main() {
setvbuf(stdout, nullptr, _IOLBF, 0);
plan(16);
plan(24);
diag("=== mysqlx_config_store_unit-t starting ===");
MysqlxResolvedIdentity identity {};
@ -141,5 +141,55 @@ int main() {
ok(store.topology_generation() == 1,
"topology generation increments on demand");
// --- mysqlx_tls_backend_mode (asymmetric TLS / AsClient mode) ---
// String parser exercise: each documented value parses, unknown
// values produce nullopt so the install path can surface a useful
// error to the operator instead of silently coercing to default.
ok(mysqlx_backend_tls_mode_from_string("disabled") ==
std::optional<MysqlxBackendTlsMode>(MysqlxBackendTlsMode::disabled) &&
mysqlx_backend_tls_mode_from_string("PREFERRED") ==
std::optional<MysqlxBackendTlsMode>(MysqlxBackendTlsMode::preferred) &&
mysqlx_backend_tls_mode_from_string("Required") ==
std::optional<MysqlxBackendTlsMode>(MysqlxBackendTlsMode::required) &&
mysqlx_backend_tls_mode_from_string("as_client") ==
std::optional<MysqlxBackendTlsMode>(MysqlxBackendTlsMode::as_client),
"backend tls mode parser accepts all four documented values case-insensitively");
ok(!mysqlx_backend_tls_mode_from_string("nonsense").has_value() &&
!mysqlx_backend_tls_mode_from_string("").has_value(),
"backend tls mode parser rejects unknown values");
ok(std::string(mysqlx_backend_tls_mode_to_string(MysqlxBackendTlsMode::as_client)) == "as_client",
"backend tls mode renders to canonical lowercase string");
// Default value: as_client matches the legacy implicit behaviour
// where backend TLS was tied to client TLS at resolve time.
ok(store.get_backend_tls_mode() == MysqlxBackendTlsMode::as_client,
"store defaults backend tls mode to as_client (matches legacy behaviour)");
// LOAD round-trip: an explicit row should be parsed and cached.
ok(db->execute("INSERT INTO mysqlx_variables (variable_name, variable_value) VALUES "
"('mysqlx_tls_backend_mode', 'required')") &&
store.install_variables_from_admin(*db, err) && err.empty() &&
store.get_backend_tls_mode() == MysqlxBackendTlsMode::required,
"store parses explicit mysqlx_tls_backend_mode='required' from admin");
// Invalid value: install must fail with a non-empty err describing the bad value.
ok(db->execute("UPDATE mysqlx_variables SET variable_value='garbage' "
"WHERE variable_name='mysqlx_tls_backend_mode'") &&
!store.install_variables_from_admin(*db, err) &&
err.find("garbage") != std::string::npos,
"store rejects invalid mysqlx_tls_backend_mode with descriptive error");
ok(store.get_backend_tls_mode() == MysqlxBackendTlsMode::required,
"store retains last-good backend tls mode after rejected install");
// Absent row: removing the variable leaves the cached value alone.
// Reset `err` first because the previous failing call left a message
// in it; install_variables_from_admin only writes on failure.
err.clear();
ok(db->execute("DELETE FROM mysqlx_variables WHERE variable_name='mysqlx_tls_backend_mode'") &&
store.install_variables_from_admin(*db, err) && err.empty() &&
store.get_backend_tls_mode() == MysqlxBackendTlsMode::required,
"absent mysqlx_tls_backend_mode row leaves cached mode untouched");
return exit_status();
}

Loading…
Cancel
Save