1use crate::llm::actions::{
9 protocol_trait::{ActionResult, Protocol, Server},
10 ActionDefinition, Parameter,
11};
12use crate::protocol::log_template::LogTemplate;
13use crate::protocol::EventType;
14use crate::server::connection::ConnectionId;
15use crate::state::app_state::AppState;
16use anyhow::{Context, Result};
17use serde_json::json;
18use std::sync::{Arc, LazyLock};
19use tokio::sync::mpsc;
20use tracing::debug;
21
22pub struct Db2Protocol {
24 _connection_id: ConnectionId,
25 _app_state: Arc<AppState>,
26 status_tx: mpsc::UnboundedSender<String>,
27}
28
29impl Db2Protocol {
30 pub fn new(
31 connection_id: ConnectionId,
32 app_state: Arc<AppState>,
33 status_tx: mpsc::UnboundedSender<String>,
34 ) -> Self {
35 Self {
36 _connection_id: connection_id,
37 _app_state: app_state,
38 status_tx,
39 }
40 }
41}
42
43impl Protocol for Db2Protocol {
44 fn get_startup_parameters(&self) -> Vec<crate::llm::actions::ParameterDefinition> {
45 vec![]
48 }
49
50 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
51 vec![]
52 }
53
54 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
55 vec![
56 db2_accept_connection_action(),
57 db2_reject_connection_action(),
58 db2_query_ok_action(),
59 db2_query_error_action(),
60 ]
61 }
62
63 fn protocol_name(&self) -> &'static str {
64 "Db2"
65 }
66
67 fn get_event_types(&self) -> Vec<EventType> {
68 get_db2_event_types()
69 }
70
71 fn stack_name(&self) -> &'static str {
72 "ETH>IP>TCP>DRDA>Db2"
73 }
74
75 fn keywords(&self) -> Vec<&'static str> {
76 vec!["db2", "ibm db2", "drda"]
78 }
79
80 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
81 use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
82
83 ProtocolMetadataV2::builder()
84 .state(DevelopmentState::Experimental)
85 .well_known_port(446)
86 .implementation(
87 "Hand-rolled DRDA/DDM codec (src/server/db2/drda.rs) against the public DRDA spec — \
88 no maintained Rust DRDA crate exists",
89 )
90 .llm_control("Login accept/reject decision, and the SQLCA (SQLCODE/SQLSTATE) for a statement")
91 .e2e_testing(
92 "tests/server/db2/real_client_test.rs: two independent drivers - Apache Derby \
93 10.17.1.0's JDBC client (through ij) and IBM's CLI driver (ibm_db 3.3.0) - log \
94 in, are refused when the model refuses, run statements and read back the \
95 model's row count and SQLSTATE; IBM's driver's default encrypted security \
96 mechanism is declined. tshark's drda dissector reads both conversations. \
97 Byte-literal tests of the codec beside it",
98 )
99 .notes(
100 "Scope: the connection handshake (EXCSAT/ACCSEC/SECCHK/ACCRDB) and the \
101 EXCSQLIMM -> SQLCARD basic-query path (statement text extracted, model decides the \
102 SQLCA). NOT implemented: SELECT result-set retrieval (OPNQRY/QRYDSC/QRYDTA/FDOCA \
103 rows), PRPSQLSTT and prepared-statement parameter marshalling (refused with \
104 CMDNSPRM), message text in the SQLCA, and TLS. RDBCMM/RDBRLLBCK are acknowledged \
105 (there is no storage to commit). Experimental: statements only, no result \
106 sets. Fail-closed: an LLM outage during login is a refusal (SECCHKRM \
107 severity ERROR), never an accept.",
108 )
109 .build()
110 }
111
112 fn description(&self) -> &'static str {
113 "IBM Db2 database server (DRDA wire protocol: handshake + basic query)"
114 }
115
116 fn example_prompt(&self) -> &'static str {
117 "Start an IBM Db2 server on port 50000 that accepts logins and answers statements"
118 }
119
120 fn group_name(&self) -> &'static str {
121 "Database"
122 }
123
124 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
125 use crate::llm::actions::StartupExamples;
126
127 let script = r#"import json, sys
130data = json.load(sys.stdin)
131if data["event_type_id"] == "db2_query":
132 actions = [{"type": "db2_query_ok", "rows_affected": 0}]
133else:
134 actions = []
135print(json.dumps({"actions": actions}))"#;
136
137 StartupExamples::new(
138 json!({
140 "type": "open_server",
141 "port": 50000,
142 "base_stack": "db2",
143 "instruction": "IBM Db2 server: accept logins for user db2inst1 and answer statements"
144 }),
145 json!({
147 "type": "open_server",
148 "port": 50000,
149 "base_stack": "db2",
150 "event_handlers": [{
151 "event_pattern": "db2_query",
152 "handler": {
153 "type": "script",
154 "language": "python",
155 "code": script
156 }
157 }]
158 }),
159 json!({
161 "type": "open_server",
162 "port": 50000,
163 "base_stack": "db2",
164 "event_handlers": [{
165 "event_pattern": "db2_connect",
166 "handler": {
167 "type": "static",
168 "actions": [{"type": "db2_accept_connection"}]
169 }
170 }]
171 }),
172 )
173 }
174}
175
176impl Server for Db2Protocol {
177 fn spawn(
178 &self,
179 ctx: crate::protocol::SpawnContext,
180 ) -> std::pin::Pin<
181 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
182 > {
183 Box::pin(async move {
184 use crate::server::db2::Db2Server;
185 Db2Server::spawn_with_llm_actions(
186 ctx.legacy_listen_addr(),
187 ctx.llm_client,
188 ctx.state,
189 ctx.status_tx,
190 ctx.server_id,
191 )
192 .await
193 })
194 }
195
196 fn execute_action(&self, action: serde_json::Value) -> Result<ActionResult> {
197 let action_type = action
198 .get("type")
199 .and_then(|v| v.as_str())
200 .context("Missing 'type' field in action")?;
201
202 match action_type {
203 "db2_accept_connection" => Ok(ActionResult::Custom {
204 name: "db2_accept_connection".to_string(),
205 data: json!({}),
206 }),
207 "db2_reject_connection" => self.execute_reject(action),
208 "db2_query_ok" => self.execute_query_ok(action),
209 "db2_query_error" => self.execute_query_error(action),
210 "close_connection" => Ok(ActionResult::CloseConnection),
214 _ => Err(anyhow::anyhow!("Unknown Db2 action: {}", action_type)),
215 }
216 }
217}
218
219impl Db2Protocol {
220 fn execute_reject(&self, action: serde_json::Value) -> Result<ActionResult> {
221 let reason = action
222 .get("reason")
223 .and_then(|v| v.as_str())
224 .unwrap_or("authentication failed")
225 .to_string();
226 let sec_check_code = action
227 .get("sec_check_code")
228 .and_then(|v| v.as_str())
229 .unwrap_or("password_invalid")
230 .to_string();
231 debug!("Db2 reject connection: {} ({})", reason, sec_check_code);
232 let _ = self
233 .status_tx
234 .send(format!("[DEBUG] Db2 ✗ reject: {}", reason));
235 Ok(ActionResult::Custom {
236 name: "db2_reject_connection".to_string(),
237 data: json!({ "reason": reason, "sec_check_code": sec_check_code }),
238 })
239 }
240
241 fn execute_query_ok(&self, action: serde_json::Value) -> Result<ActionResult> {
242 let rows_affected = action.get("rows_affected").and_then(|v| v.as_i64());
243 let sqlcode = action.get("sqlcode").and_then(|v| v.as_i64()).unwrap_or(0);
244 debug!(
245 "Db2 query ok: sqlcode={}, rows={:?}",
246 sqlcode, rows_affected
247 );
248 let _ = self.status_tx.send(format!(
249 "[DEBUG] Db2 → OK sqlcode={} rows={:?}",
250 sqlcode, rows_affected
251 ));
252 Ok(ActionResult::Custom {
253 name: "db2_query_ok".to_string(),
254 data: json!({ "rows_affected": rows_affected, "sqlcode": sqlcode }),
255 })
256 }
257
258 fn execute_query_error(&self, action: serde_json::Value) -> Result<ActionResult> {
259 let sqlcode = action
260 .get("sqlcode")
261 .and_then(|v| v.as_i64())
262 .unwrap_or(-104);
263 let sqlstate = action
264 .get("sqlstate")
265 .and_then(|v| v.as_str())
266 .unwrap_or("42601")
267 .to_string();
268 let message = action
269 .get("message")
270 .and_then(|v| v.as_str())
271 .unwrap_or("SQL error")
272 .to_string();
273 debug!(
274 "Db2 query error: sqlcode={}, sqlstate={}",
275 sqlcode, sqlstate
276 );
277 let _ = self.status_tx.send(format!(
278 "[DEBUG] Db2 ✗ SQL error sqlcode={} sqlstate={}",
279 sqlcode, sqlstate
280 ));
281 Ok(ActionResult::Custom {
282 name: "db2_query_error".to_string(),
283 data: json!({ "sqlcode": sqlcode, "sqlstate": sqlstate, "message": message }),
284 })
285 }
286}
287
288pub fn db2_accept_connection_action() -> ActionDefinition {
293 ActionDefinition {
294 name: "db2_accept_connection".to_string(),
295 description:
296 "Accept the client's login (the security check succeeds). Emit this only when \
297 the credentials should be accepted; to refuse, use db2_reject_connection. Leaving the \
298 db2_connect event unanswered is treated as a refusal."
299 .to_string(),
300 parameters: vec![],
301 example: json!({ "type": "db2_accept_connection" }),
302 log_template: Some(
303 LogTemplate::new()
304 .with_info("-> Db2 login accepted")
305 .with_debug("Db2 db2_accept_connection"),
306 ),
307 }
308}
309
310pub fn db2_reject_connection_action() -> ActionDefinition {
311 ActionDefinition {
312 name: "db2_reject_connection".to_string(),
313 description: "Refuse the client's login. The server replies with a SECCHKRM (security \
314 check reply message) at severity ERROR."
315 .to_string(),
316 parameters: vec![
317 Parameter {
318 name: "reason".to_string(),
319 type_hint: "string".to_string(),
320 description: "Human-readable reason (for logs)".to_string(),
321 required: false,
322 },
323 Parameter {
324 name: "sec_check_code".to_string(),
325 type_hint: "string".to_string(),
326 description: "Which security-check failure to report: \"password_invalid\" \
327 (default), \"userid_unknown\", or \"userid_missing\"."
328 .to_string(),
329 required: false,
330 },
331 ],
332 example: json!({
333 "type": "db2_reject_connection",
334 "reason": "Invalid password",
335 "sec_check_code": "password_invalid"
336 }),
337 log_template: Some(
338 LogTemplate::new()
339 .with_info("-> Db2 login rejected: {reason}")
340 .with_debug("Db2 db2_reject_connection: {sec_check_code}"),
341 ),
342 }
343}
344
345pub fn db2_query_ok_action() -> ActionDefinition {
346 ActionDefinition {
347 name: "db2_query_ok".to_string(),
348 description: "Report a successful statement. The server replies with an SQLCARD carrying \
349 the SQLCA. With sqlcode 0 and no rows_affected the SQLCA is sent NULL (the normal \
350 success reply); with rows_affected it carries that count in SQLERRD3, which a JDBC \
351 driver reports as the update count. NOTE: row data is not returned — SELECT \
352 result-set retrieval is not implemented; use this for INSERT/UPDATE/DELETE/DDL-style \
353 statements."
354 .to_string(),
355 parameters: vec![
356 Parameter {
357 name: "sqlcode".to_string(),
358 type_hint: "number".to_string(),
359 description: "SQLCODE to report (default 0 = success; e.g. 100 = no rows found)"
360 .to_string(),
361 required: false,
362 },
363 Parameter {
364 name: "rows_affected".to_string(),
365 type_hint: "number".to_string(),
366 description: "Rows the statement inserted, updated or deleted; sent in SQLERRD3 \
367 (with sqlcode 0)"
368 .to_string(),
369 required: false,
370 },
371 ],
372 example: json!({ "type": "db2_query_ok", "sqlcode": 0, "rows_affected": 1 }),
373 log_template: Some(
374 LogTemplate::new()
375 .with_info("-> Db2 OK sqlcode={sqlcode}")
376 .with_debug("Db2 db2_query_ok: sqlcode={sqlcode}, rows_affected={rows_affected}"),
377 ),
378 }
379}
380
381pub fn db2_query_error_action() -> ActionDefinition {
382 ActionDefinition {
383 name: "db2_query_error".to_string(),
384 description: "Report a statement failure. The server replies with an SQLCARD whose SQLCA \
385 carries the SQLCODE and SQLSTATE you provide."
386 .to_string(),
387 parameters: vec![
388 Parameter {
389 name: "sqlcode".to_string(),
390 type_hint: "number".to_string(),
391 description:
392 "Negative SQLCODE, e.g. -204 (object not found), -104 (syntax error), \
393 -911 (deadlock/timeout)."
394 .to_string(),
395 required: true,
396 },
397 Parameter {
398 name: "sqlstate".to_string(),
399 type_hint: "string".to_string(),
400 description: "5-character SQLSTATE, e.g. \"42704\", \"42601\", \"40001\""
401 .to_string(),
402 required: true,
403 },
404 Parameter {
405 name: "message".to_string(),
406 type_hint: "string".to_string(),
407 description: "Error message (logged; the SQLCA message text field is not encoded)"
408 .to_string(),
409 required: false,
410 },
411 ],
412 example: json!({
413 "type": "db2_query_error",
414 "sqlcode": -204,
415 "sqlstate": "42704",
416 "message": "DB2INST1.USERS is an undefined name"
417 }),
418 log_template: Some(
419 LogTemplate::new()
420 .with_info("-> Db2 ERR sqlcode={sqlcode} sqlstate={sqlstate}")
421 .with_debug("Db2 db2_query_error: sqlcode={sqlcode}, sqlstate={sqlstate}"),
422 ),
423 }
424}
425
426pub static DB2_ACCEPT_CONNECTION_ACTION: LazyLock<ActionDefinition> =
431 LazyLock::new(db2_accept_connection_action);
432pub static DB2_REJECT_CONNECTION_ACTION: LazyLock<ActionDefinition> =
433 LazyLock::new(db2_reject_connection_action);
434pub static DB2_QUERY_OK_ACTION: LazyLock<ActionDefinition> = LazyLock::new(db2_query_ok_action);
435pub static DB2_QUERY_ERROR_ACTION: LazyLock<ActionDefinition> =
436 LazyLock::new(db2_query_error_action);
437
438pub static DB2_CONNECT_EVENT: LazyLock<EventType> = LazyLock::new(|| {
444 EventType::new(
445 "db2_connect",
446 "Db2 client login (security check) received",
447 json!({ "type": "db2_accept_connection" }),
448 )
449 .with_parameters(vec![
450 Parameter {
451 name: "user_id".to_string(),
452 type_hint: "string".to_string(),
453 description: "The user id the client is authenticating as (EBCDIC-decoded)".to_string(),
454 required: false,
455 },
456 Parameter {
457 name: "rdb_name".to_string(),
458 type_hint: "string".to_string(),
459 description: "The relational database (RDBNAM) the client wants to access".to_string(),
460 required: false,
461 },
462 Parameter {
463 name: "has_password".to_string(),
464 type_hint: "boolean".to_string(),
465 description: "True if a password was supplied in the security check".to_string(),
466 required: false,
467 },
468 ])
469 .with_actions(vec![
470 DB2_ACCEPT_CONNECTION_ACTION.clone(),
471 DB2_REJECT_CONNECTION_ACTION.clone(),
472 ])
473 .with_log_template(
474 LogTemplate::new()
475 .with_info("Db2 login: {user_id}@{rdb_name}")
476 .with_debug("Db2 connect: user={user_id} rdb={rdb_name} has_password={has_password}"),
477 )
478});
479
480pub static DB2_QUERY_EVENT: LazyLock<EventType> = LazyLock::new(|| {
482 EventType::new(
483 "db2_query",
484 "Db2 SQL statement received",
485 json!({ "type": "db2_query_ok", "sqlcode": 0 }),
486 )
487 .with_parameters(vec![
488 Parameter {
489 name: "sql_text".to_string(),
490 type_hint: "string".to_string(),
491 description: "The SQL statement text, decoded from the requester's SQLSTT (UTF-8; bare EBCDIC is also read)".to_string(),
492 required: true,
493 },
494 Parameter {
495 name: "statement_type".to_string(),
496 type_hint: "string".to_string(),
497 description: "The DRDA command, e.g. \"execute_immediate\"".to_string(),
498 required: false,
499 },
500 ])
501 .with_actions(vec![
502 DB2_QUERY_OK_ACTION.clone(),
503 DB2_QUERY_ERROR_ACTION.clone(),
504 ])
505 .with_log_template(
506 LogTemplate::new()
507 .with_info("Db2 query: {preview(sql_text,80)}")
508 .with_debug("Db2 query: {sql_text}"),
509 )
510});
511
512pub fn get_db2_event_types() -> Vec<EventType> {
514 vec![DB2_CONNECT_EVENT.clone(), DB2_QUERY_EVENT.clone()]
515}