1use mysql::Pool;
12use mysql::prelude::*;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum MysqlProxyKind {
28 Direct,
30 ProxySql,
33 MaxScale,
38 Multiplexed,
47}
48
49impl MysqlProxyKind {
50 #[allow(dead_code)]
57 pub fn is_proxy(self) -> bool {
58 !matches!(self, MysqlProxyKind::Direct)
59 }
60
61 pub fn log_label(self) -> &'static str {
64 match self {
65 MysqlProxyKind::Direct => "direct",
66 MysqlProxyKind::ProxySql => "proxysql",
67 MysqlProxyKind::MaxScale => "maxscale",
68 MysqlProxyKind::Multiplexed => "proxy-multiplexed",
69 }
70 }
71
72 fn warn_message(self) -> Option<&'static str> {
75 match self {
76 MysqlProxyKind::Direct => None,
77 MysqlProxyKind::ProxySql => Some(
78 "MySQL proxy multiplexer detected (ProxySQL) — session variables \
79 set per-connection may not survive multiplexing; use direct connections \
80 for production exports",
81 ),
82 MysqlProxyKind::MaxScale => Some(
83 "MySQL proxy detected (MaxScale) — queries may be rewritten or routed; \
84 verify SQL is accepted by the active MaxScale router (readwritesplit, \
85 readconnroute) and that session timeouts apply on the backend",
86 ),
87 MysqlProxyKind::Multiplexed => Some(
88 "MySQL connection multiplexing detected (CONNECTION_ID() differs across \
89 queries) — session variables and temporary state may not persist across \
90 statements; use direct connections for production exports",
91 ),
92 }
93 }
94}
95
96fn classify_mysql_proxy(
121 proxysql_internal_accepted: bool,
122 version_comment: Option<&str>,
123 proxy_version: Option<&str>,
124 connection_id_pair: Option<(u64, u64)>,
125) -> MysqlProxyKind {
126 if proxysql_internal_accepted {
127 return MysqlProxyKind::ProxySql;
128 }
129 if let Some(v) = version_comment {
130 let l = v.to_ascii_lowercase();
131 if l.contains("proxysql") {
132 return MysqlProxyKind::ProxySql;
133 }
134 if l.contains("maxscale") {
135 return MysqlProxyKind::MaxScale;
136 }
137 }
138 if proxy_version.is_some() {
139 return MysqlProxyKind::ProxySql;
140 }
141 if let Some((a, b)) = connection_id_pair
142 && a != b
143 {
144 return MysqlProxyKind::Multiplexed;
145 }
146 MysqlProxyKind::Direct
147}
148
149pub(super) fn detect_mysql_proxy_kind(pool: &Pool) -> MysqlProxyKind {
154 let mut conn = match pool.get_conn() {
155 Ok(c) => c,
156 Err(_) => return MysqlProxyKind::Direct,
157 };
158 let proxysql_internal_accepted: bool = conn.query_drop("PROXYSQL INTERNAL SESSION").is_ok();
167 let version_comment: Option<String> =
168 conn.query_first("SELECT @@version_comment").unwrap_or(None);
169 let proxy_version: Option<String> = conn.query_first("SELECT @@proxy_version").unwrap_or(None);
170 let cid1: Option<u64> = conn.query_first("SELECT CONNECTION_ID()").unwrap_or(None);
174 let cid2: Option<u64> = conn.query_first("SELECT CONNECTION_ID()").unwrap_or(None);
175 let pair = match (cid1, cid2) {
176 (Some(a), Some(b)) => Some((a, b)),
177 _ => None,
178 };
179 classify_mysql_proxy(
180 proxysql_internal_accepted,
181 version_comment.as_deref(),
182 proxy_version.as_deref(),
183 pair,
184 )
185}
186
187pub(super) fn warn_proxy_kind(kind: MysqlProxyKind) {
191 if let Some(msg) = kind.warn_message() {
192 log::warn!("{msg}");
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::{MysqlProxyKind, classify_mysql_proxy};
199
200 #[test]
201 fn proxy_classify_direct_when_no_signals() {
202 let kind = classify_mysql_proxy(
203 false,
204 Some("MySQL Community Server - GPL"),
205 None,
206 Some((42, 42)),
207 );
208 assert_eq!(kind, MysqlProxyKind::Direct);
209 }
210
211 #[test]
212 fn proxy_classify_direct_when_all_signals_missing() {
213 let kind = classify_mysql_proxy(false, None, None, None);
214 assert_eq!(kind, MysqlProxyKind::Direct);
215 }
216
217 #[test]
218 fn proxy_classify_proxysql_via_internal_command() {
219 let kind = classify_mysql_proxy(
220 true,
221 Some("MySQL Community Server - GPL"),
222 None,
223 Some((7, 7)),
224 );
225 assert_eq!(kind, MysqlProxyKind::ProxySql);
226 }
227
228 #[test]
229 fn proxy_classify_proxysql_via_banner() {
230 let kind = classify_mysql_proxy(
231 false,
232 Some("(ProxySQL) High Performance MySQL Proxy"),
233 None,
234 None,
235 );
236 assert_eq!(kind, MysqlProxyKind::ProxySql);
237 }
238
239 #[test]
240 fn proxy_classify_proxysql_via_banner_lowercase() {
241 let kind = classify_mysql_proxy(false, Some("(proxysql) hpmp"), None, None);
242 assert_eq!(kind, MysqlProxyKind::ProxySql);
243 }
244
245 #[test]
246 fn proxy_classify_proxysql_via_proxy_version_only() {
247 let kind = classify_mysql_proxy(
248 false,
249 Some("MySQL Community Server - GPL"),
250 Some("2.5.5-percona-1.1"),
251 Some((1, 1)),
252 );
253 assert_eq!(kind, MysqlProxyKind::ProxySql);
254 }
255
256 #[test]
257 fn proxy_classify_maxscale_via_banner() {
258 let kind = classify_mysql_proxy(
259 false,
260 Some("MariaDB MaxScale 22.08.4-ge6a8d35ec source distribution"),
261 None,
262 Some((1, 1)),
263 );
264 assert_eq!(kind, MysqlProxyKind::MaxScale);
265 }
266
267 #[test]
268 fn proxy_classify_proxysql_internal_takes_precedence_over_banner_maxscale() {
269 let kind = classify_mysql_proxy(
270 true,
271 Some("MariaDB MaxScale 22.08.4 source distribution"),
272 None,
273 None,
274 );
275 assert_eq!(kind, MysqlProxyKind::ProxySql);
276 }
277
278 #[test]
279 fn proxy_classify_proxysql_takes_precedence_over_maxscale_in_banner() {
280 let kind = classify_mysql_proxy(
281 false,
282 Some("ProxySQL bridging MaxScale upstream"),
283 None,
284 None,
285 );
286 assert_eq!(kind, MysqlProxyKind::ProxySql);
287 }
288
289 #[test]
290 fn proxy_classify_multiplexed_via_connection_id_drift() {
291 let kind = classify_mysql_proxy(
292 false,
293 Some("MySQL Community Server"),
294 None,
295 Some((100, 200)),
296 );
297 assert_eq!(kind, MysqlProxyKind::Multiplexed);
298 }
299
300 #[test]
301 fn proxy_classify_direct_when_connection_id_pair_missing() {
302 let kind = classify_mysql_proxy(false, Some("MySQL Community Server"), None, None);
303 assert_eq!(kind, MysqlProxyKind::Direct);
304 }
305
306 #[test]
307 fn proxy_classify_direct_when_connection_ids_match() {
308 let kind = classify_mysql_proxy(false, Some("MySQL Community Server"), None, Some((7, 7)));
309 assert_eq!(kind, MysqlProxyKind::Direct);
310 }
311
312 #[test]
313 fn proxy_classify_proxysql_pool_size_one_still_caught_by_banner() {
314 let kind = classify_mysql_proxy(false, Some("(ProxySQL) HPMP"), None, Some((5, 5)));
315 assert_eq!(kind, MysqlProxyKind::ProxySql);
316 }
317
318 #[test]
319 fn proxy_classify_proxysql_default_config_still_detected_via_internal() {
320 let kind = classify_mysql_proxy(
321 true,
322 Some("MySQL Community Server - GPL"),
323 None,
324 Some((42, 42)),
325 );
326 assert_eq!(
327 kind,
328 MysqlProxyKind::ProxySql,
329 "default-config ProxySQL must be detectable via PROXYSQL INTERNAL signal alone"
330 );
331 }
332
333 #[test]
336 fn proxy_kind_is_proxy_helper_matches_variants() {
337 assert!(!MysqlProxyKind::Direct.is_proxy());
338 assert!(MysqlProxyKind::ProxySql.is_proxy());
339 assert!(MysqlProxyKind::MaxScale.is_proxy());
340 assert!(MysqlProxyKind::Multiplexed.is_proxy());
341 }
342
343 #[test]
344 fn proxy_kind_direct_has_no_warning() {
345 assert!(MysqlProxyKind::Direct.warn_message().is_none());
346 }
347
348 #[test]
349 fn proxy_kind_non_direct_variants_have_warnings() {
350 for k in [
351 MysqlProxyKind::ProxySql,
352 MysqlProxyKind::MaxScale,
353 MysqlProxyKind::Multiplexed,
354 ] {
355 assert!(
356 k.warn_message().is_some(),
357 "{k:?} must emit a warning at connect time"
358 );
359 }
360 }
361
362 #[test]
363 fn proxy_kind_log_labels_are_stable_and_distinct() {
364 let labels = [
365 MysqlProxyKind::Direct.log_label(),
366 MysqlProxyKind::ProxySql.log_label(),
367 MysqlProxyKind::MaxScale.log_label(),
368 MysqlProxyKind::Multiplexed.log_label(),
369 ];
370 assert_eq!(
371 labels,
372 ["direct", "proxysql", "maxscale", "proxy-multiplexed"]
373 );
374 }
375}