1use crate::llm::actions::{
10 protocol_trait::{ActionResult, Protocol, Server},
11 ActionDefinition, Parameter,
12};
13use crate::protocol::log_template::LogTemplate;
14use crate::protocol::EventType;
15use crate::state::app_state::AppState;
16use anyhow::{Context, Result};
17use serde_json::json;
18use std::sync::LazyLock;
19
20#[derive(Default)]
22pub struct RdpProtocol;
23
24impl RdpProtocol {
25 pub fn new() -> Self {
26 Self
27 }
28}
29
30impl Protocol for RdpProtocol {
31 fn get_startup_parameters(&self) -> Vec<crate::llm::actions::ParameterDefinition> {
32 Vec::new()
34 }
35
36 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
37 Vec::new()
38 }
39
40 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
41 vec![
42 send_negotiation_response_action(),
43 reject_connection_action(),
44 ]
45 }
46
47 fn protocol_name(&self) -> &'static str {
48 "RDP"
49 }
50
51 fn get_event_types(&self) -> Vec<EventType> {
52 get_rdp_event_types()
53 }
54
55 fn stack_name(&self) -> &'static str {
56 "ETH>IP>TCP>TPKT>X224>RDP"
57 }
58
59 fn keywords(&self) -> Vec<&'static str> {
60 vec!["rdp", "mstsc", "ms-rdpbcgr", "freerdp"]
63 }
64
65 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
66 use crate::protocol::metadata::{
67 DevelopmentState, PrivilegeRequirement, ProtocolMetadataV2,
68 };
69
70 ProtocolMetadataV2::builder()
71 .state(DevelopmentState::Experimental)
72 .privilege_requirement(PrivilegeRequirement::None)
73 .well_known_port(3389)
74 .implementation(
75 "Hand-written TPKT + X.224 with the [MS-RDPBCGR] RDP negotiation (CR/CC); no crate",
76 )
77 .llm_control("The negotiation decision: which security protocol to select, or reject with which failure code")
78 .e2e_testing("Raw TCP client sending a real X.224 Connection Request; asserts CC bytes vs MS-RDPBCGR literals")
79 .notes(
80 "SLICE: TPKT + X.224 connection negotiation ONLY ([MS-RDPBCGR] 2.2.1.1/2.2.1.2). \
81 The server parses the client's Connection Request (routing cookie / mstshash \
82 username, requestedProtocols) and answers a Connection Confirm the model chooses. \
83 It STOPS before MCS/GCC, the security exchange, the capability exchange and any \
84 bitmap output — so no desktop frame is rendered and a client does not reach a \
85 session. No real RDP client (xfreerdp/mstsc) was available in this environment to \
86 test past negotiation; correctness is pinned against RFC-derived literal bytes.",
87 )
88 .max_inbound_bytes(crate::server::rdp::MAX_X224_LEN)
89 .build()
90 }
91
92 fn description(&self) -> &'static str {
93 "RDP server: the TPKT/X.224 connection-negotiation handshake (model picks the response)"
94 }
95
96 fn example_prompt(&self) -> &'static str {
97 "Act as an RDP server on port 3389 that requires TLS security during negotiation"
98 }
99
100 fn group_name(&self) -> &'static str {
101 "Network Services"
102 }
103
104 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
105 use crate::llm::actions::StartupExamples;
106
107 let script = r#"import json, sys
110data = json.load(sys.stdin)
111event = data["event"]
112if data["event_type_id"] == "rdp_connection_request":
113 actions = [{"type": "send_rdp_negotiation_response", "selected_protocol": "TLS"}]
114else:
115 actions = []
116print(json.dumps({"actions": actions}))"#;
117
118 StartupExamples::new(
119 json!({
121 "type": "open_server",
122 "port": 3389,
123 "base_stack": "rdp",
124 "instruction": "RDP server that selects TLS security during negotiation"
125 }),
126 json!({
128 "type": "open_server",
129 "port": 3389,
130 "base_stack": "rdp",
131 "event_handlers": [{
132 "event_pattern": "rdp_connection_request",
133 "handler": {
134 "type": "script",
135 "language": "python",
136 "code": script
137 }
138 }]
139 }),
140 json!({
142 "type": "open_server",
143 "port": 3389,
144 "base_stack": "rdp",
145 "event_handlers": [{
146 "event_pattern": "rdp_connection_request",
147 "handler": {
148 "type": "static",
149 "actions": [{
150 "type": "send_rdp_negotiation_response",
151 "selected_protocol": "TLS"
152 }]
153 }
154 }]
155 }),
156 )
157 }
158}
159
160impl Server for RdpProtocol {
161 fn spawn(
162 &self,
163 ctx: crate::protocol::SpawnContext,
164 ) -> std::pin::Pin<
165 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
166 > {
167 Box::pin(async move {
168 if let Some(params) = ctx.startup_params.as_ref() {
169 let _ = params.allowed_parameters();
170 }
171 use crate::server::rdp::RdpServer;
172 let listen_addr = ctx.legacy_listen_addr();
173 RdpServer::spawn_with_llm_actions(
174 listen_addr,
175 ctx.llm_client,
176 ctx.state,
177 ctx.status_tx,
178 ctx.server_id,
179 )
180 .await
181 })
182 }
183
184 fn execute_action(&self, action: serde_json::Value) -> Result<ActionResult> {
185 let action_type = action
186 .get("type")
187 .and_then(|v| v.as_str())
188 .context("Missing 'type' field in action")?;
189
190 match action_type {
191 "send_rdp_negotiation_response" => {
192 let name = action
193 .get("selected_protocol")
194 .and_then(|v| v.as_str())
195 .context("Missing 'selected_protocol' parameter")?;
196 let selected = protocol_name_to_value(name).ok_or_else(|| {
197 anyhow::anyhow!(
198 "Invalid 'selected_protocol' {name:?}. Valid values: RDP, TLS, HYBRID \
199 (CredSSP/NLA), RDSTLS, HYBRID_EX."
200 )
201 })?;
202 let extended = action
203 .get("extended_client_data")
204 .and_then(|v| v.as_bool())
205 .unwrap_or(false);
206 let flags = if extended {
207 NEG_RSP_EXTENDED_CLIENT_DATA_SUPPORTED
208 } else {
209 0
210 };
211 Ok(ActionResult::Output(build_negotiation_response(
212 selected, flags,
213 )))
214 }
215 "reject_rdp_connection" => {
216 let name = action
217 .get("failure_code")
218 .and_then(|v| v.as_str())
219 .context("Missing 'failure_code' parameter")?;
220 let code = failure_name_to_value(name).ok_or_else(|| {
221 anyhow::anyhow!(
222 "Invalid 'failure_code' {name:?}. Valid values: SSL_REQUIRED_BY_SERVER, \
223 SSL_NOT_ALLOWED_BY_SERVER, SSL_CERT_NOT_ON_SERVER, INCONSISTENT_FLAGS, \
224 HYBRID_REQUIRED_BY_SERVER, SSL_WITH_USER_AUTH_REQUIRED_BY_SERVER."
225 )
226 })?;
227 Ok(ActionResult::Output(build_negotiation_failure(code)))
228 }
229 "close_connection" => Ok(ActionResult::CloseConnection),
233 other => Err(anyhow::anyhow!("Unknown RDP action: {other}")),
234 }
235 }
236}
237
238pub const X224_TPDU_CONNECTION_REQUEST: u8 = 0xE0;
244pub const X224_TPDU_CONNECTION_CONFIRM: u8 = 0xD0;
246
247pub const TYPE_RDP_NEG_REQ: u8 = 0x01;
249pub const TYPE_RDP_NEG_RSP: u8 = 0x02;
251pub const TYPE_RDP_NEG_FAILURE: u8 = 0x03;
253
254pub const NEG_RSP_EXTENDED_CLIENT_DATA_SUPPORTED: u8 = 0x01;
256
257pub fn protocol_name_to_value(name: &str) -> Option<u32> {
259 match name.trim().to_ascii_uppercase().as_str() {
260 "RDP" | "PROTOCOL_RDP" | "STANDARD" => Some(0x0000_0000),
261 "TLS" | "SSL" | "PROTOCOL_SSL" => Some(0x0000_0001),
262 "HYBRID" | "CREDSSP" | "NLA" | "PROTOCOL_HYBRID" => Some(0x0000_0002),
263 "RDSTLS" | "PROTOCOL_RDSTLS" => Some(0x0000_0004),
264 "HYBRID_EX" | "PROTOCOL_HYBRID_EX" => Some(0x0000_0008),
265 "RDSAAD" | "PROTOCOL_RDSAAD" => Some(0x0000_0010),
266 _ => None,
267 }
268}
269
270pub fn protocol_value_to_names(value: u32) -> Vec<&'static str> {
275 if value == 0 {
276 return vec!["RDP"];
277 }
278 let mut names = Vec::new();
279 if value & 0x0000_0001 != 0 {
280 names.push("TLS");
281 }
282 if value & 0x0000_0002 != 0 {
283 names.push("HYBRID");
284 }
285 if value & 0x0000_0004 != 0 {
286 names.push("RDSTLS");
287 }
288 if value & 0x0000_0008 != 0 {
289 names.push("HYBRID_EX");
290 }
291 if value & 0x0000_0010 != 0 {
292 names.push("RDSAAD");
293 }
294 if names.is_empty() {
295 names.push("UNKNOWN");
296 }
297 names
298}
299
300pub fn failure_name_to_value(name: &str) -> Option<u32> {
302 match name.trim().to_ascii_uppercase().as_str() {
303 "SSL_REQUIRED_BY_SERVER" => Some(0x0000_0001),
304 "SSL_NOT_ALLOWED_BY_SERVER" => Some(0x0000_0002),
305 "SSL_CERT_NOT_ON_SERVER" => Some(0x0000_0003),
306 "INCONSISTENT_FLAGS" => Some(0x0000_0004),
307 "HYBRID_REQUIRED_BY_SERVER" => Some(0x0000_0005),
308 "SSL_WITH_USER_AUTH_REQUIRED_BY_SERVER" => Some(0x0000_0006),
309 _ => None,
310 }
311}
312
313pub const DEFAULT_FAILURE_CODE: u32 = 0x0000_0001; pub fn build_negotiation_response(selected_protocol: u32, flags: u8) -> Vec<u8> {
320 let nego = build_rdp_neg(TYPE_RDP_NEG_RSP, flags, selected_protocol);
321 wrap_connection_confirm(&nego)
322}
323
324pub fn build_negotiation_failure(failure_code: u32) -> Vec<u8> {
326 let nego = build_rdp_neg(TYPE_RDP_NEG_FAILURE, 0, failure_code);
328 wrap_connection_confirm(&nego)
329}
330
331fn build_rdp_neg(neg_type: u8, flags: u8, payload: u32) -> [u8; 8] {
333 let mut buf = [0u8; 8];
334 buf[0] = neg_type;
335 buf[1] = flags;
336 buf[2..4].copy_from_slice(&8u16.to_le_bytes());
337 buf[4..8].copy_from_slice(&payload.to_le_bytes());
338 buf
339}
340
341fn wrap_connection_confirm(nego: &[u8]) -> Vec<u8> {
343 debug_assert_eq!(nego.len(), 8);
344 let li: u8 = 6 + nego.len() as u8; let total_len: u16 = 4 + 1 + li as u16; let mut out = Vec::with_capacity(total_len as usize);
349 out.push(0x03); out.push(0x00); out.extend_from_slice(&total_len.to_be_bytes());
353 out.push(li);
355 out.push(X224_TPDU_CONNECTION_CONFIRM);
356 out.extend_from_slice(&[0x00, 0x00]); out.extend_from_slice(&[0x00, 0x00]); out.push(0x00); out.extend_from_slice(nego);
360 out
361}
362
363fn send_negotiation_response_action() -> ActionDefinition {
368 ActionDefinition {
369 name: "send_rdp_negotiation_response".to_string(),
370 description: "Accept the RDP connection by sending an X.224 Connection Confirm with an \
371 RDP_NEG_RSP that selects one security protocol. Choose from the protocols the client \
372 offered in the event's requested_protocols."
373 .to_string(),
374 parameters: vec![
375 Parameter {
376 name: "selected_protocol".to_string(),
377 type_hint: "string".to_string(),
378 description: "Security protocol to select: \"RDP\" (standard RDP security), \
379 \"TLS\" (SSL/TLS), \"HYBRID\" (CredSSP/NLA), \"RDSTLS\", or \"HYBRID_EX\"."
380 .to_string(),
381 required: true,
382 },
383 Parameter {
384 name: "extended_client_data".to_string(),
385 type_hint: "boolean".to_string(),
386 description: "Set the EXTENDED_CLIENT_DATA_SUPPORTED flag. Default false."
387 .to_string(),
388 required: false,
389 },
390 ],
391 example: json!({
392 "type": "send_rdp_negotiation_response",
393 "selected_protocol": "TLS"
394 }),
395 log_template: Some(
396 LogTemplate::new()
397 .with_info("-> RDP NEG_RSP {selected_protocol}")
398 .with_debug("RDP negotiation response: {selected_protocol}"),
399 ),
400 }
401}
402
403fn reject_connection_action() -> ActionDefinition {
404 ActionDefinition {
405 name: "reject_rdp_connection".to_string(),
406 description: "Reject the RDP connection by sending an X.224 Connection Confirm with an \
407 RDP_NEG_FAILURE. Use this to demand a protocol the client did not offer (e.g. reject \
408 with SSL_REQUIRED_BY_SERVER when the client only offered standard RDP)."
409 .to_string(),
410 parameters: vec![Parameter {
411 name: "failure_code".to_string(),
412 type_hint: "string".to_string(),
413 description: "One of: SSL_REQUIRED_BY_SERVER, SSL_NOT_ALLOWED_BY_SERVER, \
414 SSL_CERT_NOT_ON_SERVER, INCONSISTENT_FLAGS, HYBRID_REQUIRED_BY_SERVER, \
415 SSL_WITH_USER_AUTH_REQUIRED_BY_SERVER."
416 .to_string(),
417 required: true,
418 }],
419 example: json!({
420 "type": "reject_rdp_connection",
421 "failure_code": "SSL_REQUIRED_BY_SERVER"
422 }),
423 log_template: Some(
424 LogTemplate::new()
425 .with_info("-> RDP NEG_FAILURE {failure_code}")
426 .with_debug("RDP negotiation failure: {failure_code}"),
427 ),
428 }
429}
430
431pub static SEND_NEGOTIATION_RESPONSE_ACTION: LazyLock<ActionDefinition> =
432 LazyLock::new(send_negotiation_response_action);
433pub static REJECT_CONNECTION_ACTION: LazyLock<ActionDefinition> =
434 LazyLock::new(reject_connection_action);
435
436pub static RDP_CONNECTION_REQUEST_EVENT: LazyLock<EventType> = LazyLock::new(|| {
442 EventType::new(
443 "rdp_connection_request",
444 "An RDP client sent its X.224 Connection Request. Decide how to negotiate: accept with \
445 send_rdp_negotiation_response selecting one of the offered security protocols, or reject \
446 with reject_rdp_connection.",
447 json!({
448 "type": "send_rdp_negotiation_response",
449 "selected_protocol": "TLS"
450 }),
451 )
452 .with_parameters(vec![
453 Parameter {
454 name: "cookie_username".to_string(),
455 type_hint: "string".to_string(),
456 description: "The username from the client's `Cookie: mstshash=...` routing token, or \
457 empty if none was sent."
458 .to_string(),
459 required: true,
460 },
461 Parameter {
462 name: "requested_protocols".to_string(),
463 type_hint: "array".to_string(),
464 description: "Security protocols the client offered, as names (e.g. [\"RDP\"], \
465 [\"TLS\",\"HYBRID\"]). \"RDP\" alone means standard RDP security (no RDP_NEG_REQ)."
466 .to_string(),
467 required: true,
468 },
469 Parameter {
470 name: "requested_protocols_flags".to_string(),
471 type_hint: "number".to_string(),
472 description: "The raw flags octet from the client's RDP_NEG_REQ (0 if none)."
473 .to_string(),
474 required: true,
475 },
476 ])
477 .with_actions(vec![
478 SEND_NEGOTIATION_RESPONSE_ACTION.clone(),
479 REJECT_CONNECTION_ACTION.clone(),
480 ])
481 .with_alternative_example(json!({
482 "type": "reject_rdp_connection",
483 "failure_code": "SSL_REQUIRED_BY_SERVER"
484 }))
485 .with_log_template(
486 LogTemplate::new()
487 .with_info("RDP CR from {client_ip}:{client_port} user='{cookie_username}'")
488 .with_debug("RDP connection request: {json_pretty(.)}"),
489 )
490});
491
492pub fn get_rdp_event_types() -> Vec<EventType> {
494 vec![RDP_CONNECTION_REQUEST_EVENT.clone()]
495}