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 detect_proxy_on_conn(&mut conn)
159}
160
161pub(super) fn detect_proxy_on_conn<C: Queryable>(conn: &mut C) -> MysqlProxyKind {
165 let proxysql_internal_accepted: bool = conn.query_drop("PROXYSQL INTERNAL SESSION").is_ok();
174 let version_comment: Option<String> =
175 conn.query_first("SELECT @@version_comment").unwrap_or(None);
176 let proxy_version: Option<String> = conn.query_first("SELECT @@proxy_version").unwrap_or(None);
177 let cid1: Option<u64> = conn.query_first("SELECT CONNECTION_ID()").unwrap_or(None);
181 let cid2: Option<u64> = conn.query_first("SELECT CONNECTION_ID()").unwrap_or(None);
182 let pair = match (cid1, cid2) {
183 (Some(a), Some(b)) => Some((a, b)),
184 _ => None,
185 };
186 classify_mysql_proxy(
187 proxysql_internal_accepted,
188 version_comment.as_deref(),
189 proxy_version.as_deref(),
190 pair,
191 )
192}
193
194pub(super) fn warn_proxy_kind(kind: MysqlProxyKind) {
198 if let Some(msg) = kind.warn_message() {
199 log::warn!("{msg}");
200 }
201}
202
203#[cfg(test)]
204mod tests {
205 use super::{MysqlProxyKind, classify_mysql_proxy};
206
207 #[test]
208 fn proxy_classify_direct_when_no_signals() {
209 let kind = classify_mysql_proxy(
210 false,
211 Some("MySQL Community Server - GPL"),
212 None,
213 Some((42, 42)),
214 );
215 assert_eq!(kind, MysqlProxyKind::Direct);
216 }
217
218 #[test]
219 fn proxy_classify_direct_when_all_signals_missing() {
220 let kind = classify_mysql_proxy(false, None, None, None);
221 assert_eq!(kind, MysqlProxyKind::Direct);
222 }
223
224 #[test]
225 fn proxy_classify_proxysql_via_internal_command() {
226 let kind = classify_mysql_proxy(
227 true,
228 Some("MySQL Community Server - GPL"),
229 None,
230 Some((7, 7)),
231 );
232 assert_eq!(kind, MysqlProxyKind::ProxySql);
233 }
234
235 #[test]
236 fn proxy_classify_proxysql_via_banner() {
237 let kind = classify_mysql_proxy(
238 false,
239 Some("(ProxySQL) High Performance MySQL Proxy"),
240 None,
241 None,
242 );
243 assert_eq!(kind, MysqlProxyKind::ProxySql);
244 }
245
246 #[test]
247 fn proxy_classify_proxysql_via_banner_lowercase() {
248 let kind = classify_mysql_proxy(false, Some("(proxysql) hpmp"), None, None);
249 assert_eq!(kind, MysqlProxyKind::ProxySql);
250 }
251
252 #[test]
253 fn proxy_classify_proxysql_via_proxy_version_only() {
254 let kind = classify_mysql_proxy(
255 false,
256 Some("MySQL Community Server - GPL"),
257 Some("2.5.5-percona-1.1"),
258 Some((1, 1)),
259 );
260 assert_eq!(kind, MysqlProxyKind::ProxySql);
261 }
262
263 #[test]
264 fn proxy_classify_maxscale_via_banner() {
265 let kind = classify_mysql_proxy(
266 false,
267 Some("MariaDB MaxScale 22.08.4-ge6a8d35ec source distribution"),
268 None,
269 Some((1, 1)),
270 );
271 assert_eq!(kind, MysqlProxyKind::MaxScale);
272 }
273
274 #[test]
275 fn proxy_classify_proxysql_internal_takes_precedence_over_banner_maxscale() {
276 let kind = classify_mysql_proxy(
277 true,
278 Some("MariaDB MaxScale 22.08.4 source distribution"),
279 None,
280 None,
281 );
282 assert_eq!(kind, MysqlProxyKind::ProxySql);
283 }
284
285 #[test]
286 fn proxy_classify_proxysql_takes_precedence_over_maxscale_in_banner() {
287 let kind = classify_mysql_proxy(
288 false,
289 Some("ProxySQL bridging MaxScale upstream"),
290 None,
291 None,
292 );
293 assert_eq!(kind, MysqlProxyKind::ProxySql);
294 }
295
296 #[test]
297 fn proxy_classify_multiplexed_via_connection_id_drift() {
298 let kind = classify_mysql_proxy(
299 false,
300 Some("MySQL Community Server"),
301 None,
302 Some((100, 200)),
303 );
304 assert_eq!(kind, MysqlProxyKind::Multiplexed);
305 }
306
307 #[test]
308 fn proxy_classify_direct_when_connection_id_pair_missing() {
309 let kind = classify_mysql_proxy(false, Some("MySQL Community Server"), None, None);
310 assert_eq!(kind, MysqlProxyKind::Direct);
311 }
312
313 #[test]
314 fn proxy_classify_direct_when_connection_ids_match() {
315 let kind = classify_mysql_proxy(false, Some("MySQL Community Server"), None, Some((7, 7)));
316 assert_eq!(kind, MysqlProxyKind::Direct);
317 }
318
319 #[test]
320 fn proxy_classify_proxysql_pool_size_one_still_caught_by_banner() {
321 let kind = classify_mysql_proxy(false, Some("(ProxySQL) HPMP"), None, Some((5, 5)));
322 assert_eq!(kind, MysqlProxyKind::ProxySql);
323 }
324
325 #[test]
326 fn proxy_classify_proxysql_default_config_still_detected_via_internal() {
327 let kind = classify_mysql_proxy(
328 true,
329 Some("MySQL Community Server - GPL"),
330 None,
331 Some((42, 42)),
332 );
333 assert_eq!(
334 kind,
335 MysqlProxyKind::ProxySql,
336 "default-config ProxySQL must be detectable via PROXYSQL INTERNAL signal alone"
337 );
338 }
339
340 #[test]
343 fn proxy_kind_is_proxy_helper_matches_variants() {
344 assert!(!MysqlProxyKind::Direct.is_proxy());
345 assert!(MysqlProxyKind::ProxySql.is_proxy());
346 assert!(MysqlProxyKind::MaxScale.is_proxy());
347 assert!(MysqlProxyKind::Multiplexed.is_proxy());
348 }
349
350 #[test]
351 fn proxy_kind_direct_has_no_warning() {
352 assert!(MysqlProxyKind::Direct.warn_message().is_none());
353 }
354
355 #[test]
356 fn proxy_kind_non_direct_variants_have_warnings() {
357 for k in [
358 MysqlProxyKind::ProxySql,
359 MysqlProxyKind::MaxScale,
360 MysqlProxyKind::Multiplexed,
361 ] {
362 assert!(
363 k.warn_message().is_some(),
364 "{k:?} must emit a warning at connect time"
365 );
366 }
367 }
368
369 #[test]
370 fn proxy_kind_log_labels_are_stable_and_distinct() {
371 let labels = [
372 MysqlProxyKind::Direct.log_label(),
373 MysqlProxyKind::ProxySql.log_label(),
374 MysqlProxyKind::MaxScale.log_label(),
375 MysqlProxyKind::Multiplexed.log_label(),
376 ];
377 assert_eq!(
378 labels,
379 ["direct", "proxysql", "maxscale", "proxy-multiplexed"]
380 );
381 }
382}