Skip to content

Commit 02b9f4e

Browse files
authored
fix(acceptor): send RDP_NEG_FAILURE on security protocol mismatch (#1152)
When the client and server have no common security protocol, the acceptor now sends a proper `RDP_NEG_FAILURE` PDU before returning an error, instead of dropping the TCP connection.
1 parent ff2b567 commit 02b9f4e

File tree

5 files changed

+155
-1
lines changed

5 files changed

+155
-1
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/ironrdp-acceptor/src/connection.rs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,31 @@ impl Sequence for Acceptor {
324324
} else if self.security.is_empty() {
325325
SecurityProtocol::empty()
326326
} else {
327-
return Err(ConnectorError::general("failed to negotiate security protocol"));
327+
// No common security protocol. Send RDP_NEG_FAILURE so the client
328+
// gets a well-formed response instead of a TCP reset (MS-RDPBCGR 2.2.1.2.2).
329+
let failure_code = if self.security.intersects(SecurityProtocol::SSL) {
330+
nego::FailureCode::SSL_REQUIRED_BY_SERVER
331+
} else if self
332+
.security
333+
.intersects(SecurityProtocol::HYBRID | SecurityProtocol::HYBRID_EX)
334+
{
335+
nego::FailureCode::HYBRID_REQUIRED_BY_SERVER
336+
} else {
337+
nego::FailureCode::SSL_REQUIRED_BY_SERVER
338+
};
339+
340+
let failure = nego::ConnectionConfirm::Failure { code: failure_code };
341+
342+
debug!(message = ?failure, "Send");
343+
344+
ironrdp_core::encode_buf(&X224(failure), output).map_err(ConnectorError::encode)?;
345+
346+
return Err(reason_err!(
347+
"security protocol mismatch",
348+
"server requires {:?} but client only offered {:?}",
349+
self.security,
350+
requested_protocol,
351+
));
328352
};
329353
let connection_confirm = nego::ConnectionConfirm::Response {
330354
flags: nego::ResponseFlags::empty(),

crates/ironrdp-testsuite-core/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ expect-test.workspace = true
3535
hex = "0.4"
3636
ironrdp-cliprdr-format.path = "../ironrdp-cliprdr-format"
3737
ironrdp-cliprdr.path = "../ironrdp-cliprdr"
38+
ironrdp-acceptor.path = "../ironrdp-acceptor"
3839
ironrdp-connector.path = "../ironrdp-connector"
3940
ironrdp-displaycontrol.path = "../ironrdp-displaycontrol"
4041
ironrdp-dvc.path = "../ironrdp-dvc"
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
use ironrdp_acceptor::Acceptor;
2+
use ironrdp_connector::{DesktopSize, Sequence as _, Written};
3+
use ironrdp_core::{decode, WriteBuf};
4+
use ironrdp_pdu::nego::{self, SecurityProtocol};
5+
use ironrdp_pdu::x224::X224;
6+
7+
/// Build a minimal ConnectionRequest with the given protocols and encode it.
8+
fn encode_connection_request(protocol: SecurityProtocol) -> Vec<u8> {
9+
let request = nego::ConnectionRequest {
10+
nego_data: None,
11+
flags: nego::RequestFlags::empty(),
12+
protocol,
13+
};
14+
let mut buf = WriteBuf::new();
15+
ironrdp_core::encode_buf(&X224(request), &mut buf).unwrap();
16+
buf.filled().to_vec()
17+
}
18+
19+
/// When server requires TLS but client only offers HYBRID|HYBRID_EX,
20+
/// the acceptor must write an RDP_NEG_FAILURE PDU and return an error.
21+
#[test]
22+
fn neg_failure_on_protocol_mismatch() {
23+
let mut acceptor = Acceptor::new(
24+
SecurityProtocol::SSL,
25+
DesktopSize {
26+
width: 1920,
27+
height: 1080,
28+
},
29+
Vec::new(),
30+
None,
31+
);
32+
33+
// Step 1: feed the connection request (HYBRID | HYBRID_EX, no SSL)
34+
let request_bytes = encode_connection_request(SecurityProtocol::HYBRID | SecurityProtocol::HYBRID_EX);
35+
let mut output = WriteBuf::new();
36+
let written = acceptor.step(&request_bytes, &mut output).unwrap();
37+
assert!(matches!(written, Written::Nothing));
38+
39+
// Step 2: acceptor tries to send confirm, finds no common protocol
40+
let mut output = WriteBuf::new();
41+
let result = acceptor.step(&[], &mut output);
42+
43+
// Must be an error
44+
assert!(result.is_err(), "expected error on protocol mismatch");
45+
46+
// Must have written an RDP_NEG_FAILURE PDU to the output buffer
47+
let response_bytes = output.filled();
48+
assert!(!response_bytes.is_empty(), "expected RDP_NEG_FAILURE PDU in output");
49+
50+
// Decode the response and verify it's a Failure with the right code
51+
let confirm = decode::<X224<nego::ConnectionConfirm>>(response_bytes).unwrap().0;
52+
match confirm {
53+
nego::ConnectionConfirm::Failure { code } => {
54+
assert_eq!(code, nego::FailureCode::SSL_REQUIRED_BY_SERVER);
55+
}
56+
nego::ConnectionConfirm::Response { .. } => {
57+
panic!("expected Failure, got Response");
58+
}
59+
}
60+
}
61+
62+
/// When server and client agree on SSL, negotiation succeeds normally.
63+
#[test]
64+
fn neg_success_when_protocols_match() {
65+
let mut acceptor = Acceptor::new(
66+
SecurityProtocol::SSL,
67+
DesktopSize {
68+
width: 1920,
69+
height: 1080,
70+
},
71+
Vec::new(),
72+
None,
73+
);
74+
75+
let request_bytes = encode_connection_request(SecurityProtocol::SSL | SecurityProtocol::HYBRID);
76+
let mut output = WriteBuf::new();
77+
acceptor.step(&request_bytes, &mut output).unwrap();
78+
79+
let mut output = WriteBuf::new();
80+
let written = acceptor.step(&[], &mut output).unwrap();
81+
assert!(!matches!(written, Written::Nothing));
82+
83+
let response_bytes = output.filled();
84+
let confirm = decode::<X224<nego::ConnectionConfirm>>(response_bytes).unwrap().0;
85+
match confirm {
86+
nego::ConnectionConfirm::Response { protocol, .. } => {
87+
assert_eq!(protocol, SecurityProtocol::SSL);
88+
}
89+
nego::ConnectionConfirm::Failure { .. } => {
90+
panic!("expected Response, got Failure");
91+
}
92+
}
93+
}
94+
95+
/// When server requires HYBRID but client only offers SSL, the failure code
96+
/// should be HYBRID_REQUIRED_BY_SERVER.
97+
#[test]
98+
fn neg_failure_hybrid_required() {
99+
let mut acceptor = Acceptor::new(
100+
SecurityProtocol::HYBRID | SecurityProtocol::HYBRID_EX,
101+
DesktopSize {
102+
width: 1920,
103+
height: 1080,
104+
},
105+
Vec::new(),
106+
None,
107+
);
108+
109+
let request_bytes = encode_connection_request(SecurityProtocol::SSL);
110+
let mut output = WriteBuf::new();
111+
acceptor.step(&request_bytes, &mut output).unwrap();
112+
113+
let mut output = WriteBuf::new();
114+
let result = acceptor.step(&[], &mut output);
115+
assert!(result.is_err());
116+
117+
let response_bytes = output.filled();
118+
let confirm = decode::<X224<nego::ConnectionConfirm>>(response_bytes).unwrap().0;
119+
match confirm {
120+
nego::ConnectionConfirm::Failure { code } => {
121+
assert_eq!(code, nego::FailureCode::HYBRID_REQUIRED_BY_SERVER);
122+
}
123+
nego::ConnectionConfirm::Response { .. } => {
124+
panic!("expected Failure, got Response");
125+
}
126+
}
127+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
mod acceptor;
12
mod fast_path;

0 commit comments

Comments
 (0)