Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions native/engine/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ mod drive;
mod encode;
mod rail;
mod rdp;
mod rdstls;
mod redirect;
mod sound;
mod surface;
Expand Down
94 changes: 94 additions & 0 deletions native/engine/src/rdp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ pub struct Config {
pub initial_program: String,
pub work_dir: String,
pub load_balance_info: String,
/// From a Server Redirection PDU: present, the next hop logs on with
/// RDSTLS instead of NLA.
pub redirection_guid: Vec<u8>,
pub password_cookie: Vec<u8>,
pub timezone_bias: i32,
pub timezone_name: String,
pub kdc_url: String,
Expand All @@ -104,6 +108,8 @@ enum Phase {
/// Waiting for JS to upgrade the socket to TLS.
AwaitTls(Box<ClientConnector>),
Credssp(Box<ClientConnector>, Box<CredsspSequence>),
/// RDSTLS after TLS: true once the Authentication Request went out.
Rdstls(Box<ClientConnector>, bool),
/// VMConnect: waiting for TLS right after the preconnection blob.
VmTls(Box<ClientConnector>),
/// VMConnect: CredSSP with the Hyper-V host, before X.224.
Expand Down Expand Up @@ -246,6 +252,13 @@ impl RdpClient {
self.phase = phase;
return;
};
if let ClientConnectorState::EnhancedSecurityUpgrade { selected_protocol } = &connector.state {
if selected_protocol.contains(ironrdp::pdu::nego::SecurityProtocol::RDSTLS) {
self.phase = Phase::Rdstls(connector, false);
self.pump();
return;
}
}
connector.mark_security_upgrade_as_done();
if connector.should_perform_credssp() {
match self.credssp_init(&connector, public_key.to_vec()) {
Expand Down Expand Up @@ -691,6 +704,7 @@ impl RdpClient {
let progressed = match std::mem::replace(&mut self.phase, Phase::Closed) {
Phase::Connecting(connector) => self.pump_connecting(connector),
Phase::Credssp(connector, sequence) => self.pump_credssp(connector, sequence),
Phase::Rdstls(connector, sent) => self.pump_rdstls(connector, sent),
Phase::VmCredssp(connector, sequence) => self.pump_vm_credssp(connector, sequence),
Phase::Active(session) => self.pump_active(session),
other => {
Expand Down Expand Up @@ -722,6 +736,11 @@ impl RdpClient {
self.phase = Phase::Connecting(connector);
return true;
}
if !self.config.password_cookie.is_empty()
&& matches!(connector.state, ClientConnectorState::ConnectionInitiationSendRequest)
{
return self.request_rdstls(connector);
}
if connector.should_perform_security_upgrade() {
self.phase = Phase::AwaitTls(connector);
self.events.push(obj("tls").into());
Expand Down Expand Up @@ -943,6 +962,16 @@ impl RdpClient {
}
keep
}
// Data on a dynamic channel this client refused or closed, which
// GNOME Remote Desktop sends after a refused audio channel. FreeRDP
// drops it too; the rest of the session is fine.
Err(e) if format!("{e:?}").contains("access to non existing DVC channel") => {
let e2 = obj("log");
set(&e2, "message", format!("dropped data for a closed dynamic channel: {e}"));
self.events.push(e2.into());
self.phase = Phase::Active(session);
true
}
Err(e) => {
self.fail("protocol", &format!("{e:?}"));
false
Expand Down Expand Up @@ -1229,11 +1258,76 @@ impl RdpClient {
set(&e, "username", r.username);
set(&e, "domain", r.domain);
set(&e, "password", r.password);
set(&e, "redirectionGuid", Uint8Array::from(r.redirection_guid.as_slice()));
set(&e, "passwordCookie", Uint8Array::from(r.password_cookie.as_slice()));
self.events.push(e.into());
self.phase = Phase::Closed;
true
}

/// The X.224 request for a redirected hop that logs on with RDSTLS, which
/// the connector cannot ask for itself.
fn request_rdstls(&mut self, mut connector: Box<ClientConnector>) -> bool {
use ironrdp::pdu::nego;
let protocol = nego::SecurityProtocol::RDSTLS;
let request = nego::ConnectionRequest {
nego_data: None,
flags: nego::RequestFlags::empty(),
protocol,
correlation_info: None,
};
let token = &self.config.load_balance_info;
let encoded = if token.is_empty() {
ironrdp_core::encode_vec(&ironrdp::pdu::x224::X224(request))
} else {
ironrdp_core::encode_vec(&ironrdp::pdu::x224::X224(nego::ConnectionRequestWithOpaqueRoutingToken {
request,
routing_token: nego::OpaqueRoutingToken(token.clone()),
}))
};
match encoded {
Ok(bytes) => self.push_send(bytes),
Err(e) => {
self.fail("protocol", &format!("{e:?}"));
return false;
}
}
connector.state = ClientConnectorState::ConnectionInitiationWaitConfirm {
requested_protocol: protocol,
};
self.phase = Phase::Connecting(connector);
true
}

/// Reads the server's capabilities, logs on, then hands back to the
/// connector, which skips CredSSP for RDSTLS.
fn pump_rdstls(&mut self, mut connector: Box<ClientConnector>, sent: bool) -> bool {
let need = if sent { crate::rdstls::RESPONSE_LEN } else { crate::rdstls::CAPABILITIES_LEN };
if self.inbuf.len() < need {
self.phase = Phase::Rdstls(connector, sent);
return false;
}
let pdu: Vec<u8> = self.inbuf.drain(..need).collect();
if !sent {
if let Err(e) = crate::rdstls::read_capabilities(&pdu) {
self.fail("protocol", &e);
return false;
}
let c = &self.config;
let request = crate::rdstls::auth_request(&c.redirection_guid, &c.username, &c.domain, &c.password_cookie);
self.push_send(request);
self.phase = Phase::Rdstls(connector, true);
return true;
}
if let Err(e) = crate::rdstls::read_response(&pdu) {
self.fail("auth", &e);
return false;
}
connector.mark_security_upgrade_as_done();
self.phase = Phase::Connecting(connector);
true
}

fn fail(&mut self, kind: &str, message: &str) {
let e = obj("error");
set(&e, "kind", kind);
Expand Down
80 changes: 80 additions & 0 deletions native/engine/src/rdstls.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
//! RDSTLS (MS-RDPBCGR 2.2.17): after TLS, the client proves itself with the
//! password cookie a Server Redirection PDU handed it. GNOME Remote Login
//! takes it for the user's session, whose one-time account name NLA cannot
//! always carry. IronRDP negotiates the protocol but does not run it.

const VERSION_1: u16 = 0x01;
const TYPE_CAPABILITIES: u16 = 0x01;
const TYPE_AUTHREQ: u16 = 0x02;
const TYPE_AUTHRSP: u16 = 0x04;
const DATA_CAPABILITIES: u16 = 0x01;
const DATA_PASSWORD_CREDS: u16 = 0x01;
const DATA_RESULT_CODE: u16 = 0x01;

/// The server's Capabilities PDU size.
pub const CAPABILITIES_LEN: usize = 8;
/// The server's Authentication Response PDU size.
pub const RESPONSE_LEN: usize = 10;

fn u16_at(b: &[u8], at: usize) -> u16 {
u16::from_le_bytes([b[at], b[at + 1]])
}

/// Checks the server's Capabilities PDU offers version 1.
pub fn read_capabilities(pdu: &[u8]) -> Result<(), String> {
let ok = pdu.len() == CAPABILITIES_LEN
&& u16_at(pdu, 2) == TYPE_CAPABILITIES
&& u16_at(pdu, 4) == DATA_CAPABILITIES
&& u16_at(pdu, 6) & VERSION_1 != 0;
ok.then_some(()).ok_or_else(|| "RDSTLS capabilities not understood".to_owned())
}

/// The Authentication Request with the redirect's GUID, account and cookie.
pub fn auth_request(guid: &[u8], username: &str, domain: &str, cookie: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
for value in [VERSION_1, TYPE_AUTHREQ, DATA_PASSWORD_CREDS] {
out.extend(value.to_le_bytes());
}
let wide = |s: &str| s.encode_utf16().chain([0]).flat_map(u16::to_le_bytes).collect::<Vec<u8>>();
for data in [guid, &wide(username), &wide(domain), cookie] {
out.extend((data.len() as u16).to_le_bytes());
out.extend(data);
}
out
}

/// The server's verdict on the request: Ok, or the result code it sent.
pub fn read_response(pdu: &[u8]) -> Result<(), String> {
if pdu.len() != RESPONSE_LEN || u16_at(pdu, 2) != TYPE_AUTHRSP || u16_at(pdu, 4) != DATA_RESULT_CODE {
return Err("RDSTLS response not understood".to_owned());
}
match u32::from_le_bytes([pdu[6], pdu[7], pdu[8], pdu[9]]) {
0 => Ok(()),
code => Err(format!("RDSTLS authentication refused (0x{code:08x})")),
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn reads_capabilities_and_responses() {
assert!(read_capabilities(&[1, 0, 1, 0, 1, 0, 3, 0]).is_ok());
assert!(read_capabilities(&[1, 0, 4, 0, 1, 0, 1, 0]).is_err());
assert!(read_response(&[1, 0, 4, 0, 1, 0, 0, 0, 0, 0]).is_ok());
assert_eq!(
read_response(&[1, 0, 4, 0, 1, 0, 0x2e, 5, 0, 0]),
Err("RDSTLS authentication refused (0x0000052e)".to_owned())
);
}

#[test]
fn writes_the_account_name_whole() {
let pdu = auth_request(&[7, 7], "a\\b", "", &[1, 2]);
let mut expected = vec![1, 0, 2, 0, 1, 0, 2, 0, 7, 7, 8, 0];
expected.extend([b'a', 0, b'\\', 0, b'b', 0, 0, 0]);
expected.extend([2, 0, 0, 0, 2, 0, 1, 2]);
assert_eq!(pdu, expected);
}
}
51 changes: 44 additions & 7 deletions native/engine/src/redirect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ const LB_TARGET_NETBIOS: u32 = 0x200;
const LB_TARGET_NET_ADDRESSES: u32 = 0x800;
const LB_CLIENT_TSV_URL: u32 = 0x1000;
const LB_PASSWORD_IS_PK_ENCRYPTED: u32 = 0x4000;
const LB_REDIRECTION_GUID: u32 = 0x8000;
const LB_TARGET_CERTIFICATE: u32 = 0x10000;

#[derive(Debug, Default, PartialEq, Eq)]
pub struct Redirect {
Expand All @@ -23,6 +25,9 @@ pub struct Redirect {
pub username: String,
pub domain: String,
pub password: String,
/// For RDSTLS: the redirection GUID and the password cookie, as sent.
pub redirection_guid: Vec<u8>,
pub password_cookie: Vec<u8>,
}

/// Reads the redirect out of a whole TPKT frame, or None when the frame is
Expand Down Expand Up @@ -74,9 +79,11 @@ fn parse_packet(data: &[u8]) -> Option<Redirect> {
}
if flags & LB_PASSWORD != 0 {
let raw = r.field()?;
// A password cookie is opaque; only a plain one is usable.
// A password cookie is opaque: it goes back as is over RDSTLS.
if flags & LB_PASSWORD_IS_PK_ENCRYPTED == 0 {
out.password = utf16(raw);
} else {
out.password_cookie = raw.to_vec();
}
}
if flags & LB_TARGET_FQDN != 0 {
Expand All @@ -88,10 +95,16 @@ fn parse_packet(data: &[u8]) -> Option<Redirect> {
if flags & LB_CLIENT_TSV_URL != 0 {
r.field()?;
}
if flags & LB_TARGET_NET_ADDRESSES != 0 && out.address.is_empty() {
if flags & LB_REDIRECTION_GUID != 0 {
out.redirection_guid = r.field()?.to_vec();
}
if flags & LB_TARGET_CERTIFICATE != 0 {
r.field()?;
}
if flags & LB_TARGET_NET_ADDRESSES != 0 {
// Count, then a length and an address each; the first one is enough.
let list = r.field()?;
if list.len() >= 8 {
if out.address.is_empty() && list.len() >= 8 {
let len = u32::from_le_bytes([list[4], list[5], list[6], list[7]]) as usize;
if let Some(first) = list.get(8..8 + len) {
out.address = utf16(first);
Expand Down Expand Up @@ -126,7 +139,8 @@ impl<'a> Reader<'a> {

fn field(&mut self) -> Option<&'a [u8]> {
let len = self.u32()? as usize;
if len > 4096 {
// A target certificate can outgrow 4 KB; the PDU is at most 64 KB.
if len > 0xffff {
return None;
}
self.take(len)
Expand Down Expand Up @@ -160,7 +174,11 @@ mod tests {
let mut share = vec![0, 0, 0x1a, 0x00, 0xea, 0x03, 0, 0];
share.extend(&packet);
let mut mcs = vec![0x68, 0x00, 0x01, 0x03, 0xeb, 0x70];
mcs.push(share.len() as u8);
if share.len() < 0x80 {
mcs.push(share.len() as u8);
} else {
mcs.extend([0x80 | (share.len() >> 8) as u8, share.len() as u8]);
}
mcs.extend(&share);
let mut out = vec![3, 0, 0, 0, 0x02, 0xf0, 0x80];
out.extend(&mcs);
Expand All @@ -183,11 +201,30 @@ mod tests {
}

#[test]
fn skips_an_encrypted_password() {
fn keeps_an_encrypted_password_as_a_cookie() {
let mut body = Vec::new();
field(&mut body, &[1, 2, 3, 4]);
let f = frame(LB_PASSWORD | LB_PASSWORD_IS_PK_ENCRYPTED, &body);
assert_eq!(parse_frame(&f).expect("redirect").password, "");
let r = parse_frame(&f).expect("redirect");
assert_eq!(r.password, "");
assert_eq!(r.password_cookie, [1, 2, 3, 4]);
}

#[test]
fn reads_the_guid_past_the_certificate_and_addresses_last() {
let mut body = Vec::new();
field(&mut body, &wide("10.0.0.5"));
field(&mut body, &wide("guid"));
field(&mut body, &[9; 5000]);
let mut list = 1u32.to_le_bytes().to_vec();
let other = wide("10.0.0.6");
list.extend((other.len() as u32).to_le_bytes());
list.extend(&other);
field(&mut body, &list);
let flags = LB_TARGET_NET_ADDRESS | LB_REDIRECTION_GUID | LB_TARGET_CERTIFICATE | LB_TARGET_NET_ADDRESSES;
let r = parse_frame(&frame(flags, &body)).expect("redirect");
assert_eq!(r.address, "10.0.0.5");
assert_eq!(r.redirection_guid, wide("guid"));
}

#[test]
Expand Down
Binary file modified src/backend/engine/wasm/engine_bg.wasm
Binary file not shown.
2 changes: 1 addition & 1 deletion src/backend/engine/wasm/source-hash.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0ca6af3c0479b0f76f4faadb0ed52459096060d12713b03af59a1605cac55885
cb025d5b5d28affc3baf16d6139e941937df4d3c2d9bece949e79cbba3d23785
Loading