feat(core): RDP client on IronRDP

clients::rdp connects over any caller-dialled TCP stream (direct, proxy
or SSH tunnel), upgrades to TLS and logs on with NLA (CredSSP/NTLM),
optionally falling back to TLS-only logon for servers without NLA such
as xrdp (credentials are passed so they log straight in). The server
certificate is pinned through a caller check on its SHA256 fingerprint
before any credentials are sent. The desktop decodes into the VNC
client's RGBA framebuffer; input (IronRDP's scancode/Unicode/mouse
operations), Display Control resizes and the deactivation-reactivation
sequence run on the session thread.

IronRDP's releases need three vendored patches (vendor/README.md):
picky and sspi with their RustCrypto release-candidate pins relaxed so
they share russh's stable versions (their own suites, 402 tests, pass
against them), and ironrdp-session with two unreleased upstream bitmap
fixes (#1486 stride, #1524 RDP6 orientation) without which xrdp desktops
render sheared.

Verified live against xrdp (scottyhardy/docker-remote-desktop): logon,
full desktop rendering, keyboard and mouse input.
This commit is contained in:
Andy Dixon
2026-10-12 10:50:24 +01:00
parent 6a4d7381d9
commit 50c0104d57
227 changed files with 66687 additions and 20 deletions
+276
View File
@@ -0,0 +1,276 @@
#[cfg(test)]
mod tests;
use std::io;
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
#[repr(u8)]
#[allow(unused)]
pub(crate) enum Pc {
Primitive = 0x00,
Construct = 0x20,
}
#[repr(u8)]
#[allow(unused)]
enum Class {
Universal = 0x00,
Application = 0x40,
ContextSpecific = 0x80,
Private = 0xC0,
}
#[repr(u8)]
#[allow(unused)]
enum Tag {
Mask = 0x1F,
Boolean = 0x01,
Integer = 0x02,
BitString = 0x03,
OctetString = 0x04,
ObjectIdentifier = 0x06,
Enumerated = 0x0A,
Sequence = 0x10,
}
const TAG_MASK: u8 = 0x1F;
pub(crate) fn sizeof_sequence(length: u16) -> u16 {
1 + sizeof_length(length) + length
}
pub(crate) fn sizeof_sequence_tag(length: u16) -> u16 {
1 + sizeof_length(length)
}
pub(crate) fn sizeof_contextual_tag(length: u16) -> u16 {
1 + sizeof_length(length)
}
pub(crate) fn sizeof_octet_string(length: u16) -> u16 {
1 + sizeof_length(length) + length
}
pub(crate) fn sizeof_sequence_octet_string(length: u16) -> u16 {
sizeof_contextual_tag(sizeof_octet_string(length)) + sizeof_octet_string(length)
}
pub(crate) fn sizeof_integer(value: u32) -> u16 {
if value < 0x80 {
3
} else if value < 0x8000 {
4
} else if value < 0x0080_0000 {
5
} else {
6
}
}
pub(crate) fn write_sequence_tag(mut stream: impl io::Write, length: u16) -> io::Result<usize> {
write_universal_tag(&mut stream, Tag::Sequence, Pc::Construct)?;
write_length(stream, length).map(|length| length + 1)
}
pub(crate) fn read_sequence_tag(mut stream: impl io::Read) -> io::Result<u16> {
let identifier = stream.read_u8()?;
if identifier != Class::Universal as u8 | Pc::Construct as u8 | (TAG_MASK & Tag::Sequence as u8) {
Err(io::Error::new(
io::ErrorKind::InvalidData,
"invalid sequence tag identifier",
))
} else {
read_length(stream)
}
}
pub(crate) fn write_contextual_tag(mut stream: impl io::Write, tagnum: u8, length: u16, pc: Pc) -> io::Result<usize> {
let identifier = Class::ContextSpecific as u8 | pc as u8 | (TAG_MASK & tagnum);
stream.write_u8(identifier)?;
write_length(stream, length).map(|length| length + 1)
}
pub(crate) fn read_contextual_tag(mut stream: impl io::Read, tagnum: u8, pc: Pc) -> io::Result<u16> {
let identifier = stream.read_u8()?;
if identifier != Class::ContextSpecific as u8 | pc as u8 | (TAG_MASK & tagnum) {
Err(io::Error::new(
io::ErrorKind::InvalidData,
"invalid contextual tag identifier",
))
} else {
read_length(stream)
}
}
pub(crate) fn read_contextual_tag_or_unwind(
mut stream: impl io::Read + io::Seek,
tagnum: u8,
pc: Pc,
) -> io::Result<Option<u16>> {
match read_contextual_tag(&mut stream, tagnum, pc) {
Ok(contextual_tag_len) => Ok(Some(contextual_tag_len)),
Err(_) => {
stream.seek(io::SeekFrom::Current(-1))?;
Ok(None)
}
}
}
pub(crate) fn write_integer(mut stream: impl io::Write, value: u32) -> io::Result<usize> {
write_universal_tag(&mut stream, Tag::Integer, Pc::Primitive)?;
if value < 0x80 {
write_length(&mut stream, 1)?;
stream.write_u8(value as u8)?;
Ok(3)
} else if value < 0x8000 {
write_length(&mut stream, 2)?;
stream.write_u16::<BigEndian>(value as u16)?;
Ok(4)
} else if value < 0x0080_0000 {
write_length(&mut stream, 3)?;
stream.write_u8((value >> 16) as u8)?;
stream.write_u16::<BigEndian>((value & 0xFFFF) as u16)?;
Ok(5)
} else {
write_length(&mut stream, 4)?;
stream.write_u32::<BigEndian>(value)?;
Ok(6)
}
}
pub(crate) fn read_integer(mut stream: impl io::Read) -> io::Result<u64> {
read_universal_tag(&mut stream, Tag::Integer, Pc::Primitive)?;
let length = read_length(&mut stream)?;
if length == 1 {
stream.read_u8().map(u64::from)
} else if length == 2 {
stream.read_u16::<BigEndian>().map(u64::from)
} else if length == 3 {
let a = stream.read_u8()?;
let b = stream.read_u16::<BigEndian>()?;
Ok(u64::from(b) + (u64::from(a) << 16))
} else if length == 4 {
stream.read_u32::<BigEndian>().map(u64::from)
} else if length == 8 {
stream.read_u64::<BigEndian>()
} else {
Err(io::Error::new(io::ErrorKind::InvalidData, "invalid integer len"))
}
}
pub(crate) fn write_sequence_octet_string(mut stream: impl io::Write, tagnum: u8, value: &[u8]) -> io::Result<usize> {
let tag_len = write_contextual_tag(
&mut stream,
tagnum,
sizeof_octet_string(value.len() as u16),
Pc::Construct,
)?;
let string_len = write_octet_string(&mut stream, value)?;
Ok(tag_len + string_len)
}
pub(crate) fn write_octet_string(mut stream: impl io::Write, value: &[u8]) -> io::Result<usize> {
let tag_size = write_octet_string_tag(&mut stream, value.len() as u16)?;
stream.write_all(value)?;
Ok(tag_size + value.len())
}
pub(crate) fn write_octet_string_tag(mut stream: impl io::Write, length: u16) -> io::Result<usize> {
write_universal_tag(&mut stream, Tag::OctetString, Pc::Primitive)?;
write_length(&mut stream, length).map(|length| length + 1)
}
pub(crate) fn read_octet_string_tag(mut stream: impl io::Read) -> io::Result<u16> {
read_universal_tag(&mut stream, Tag::OctetString, Pc::Primitive)?;
read_length(stream)
}
fn write_universal_tag(mut stream: impl io::Write, tag: Tag, pc: Pc) -> io::Result<usize> {
let identifier = Class::Universal as u8 | pc as u8 | (TAG_MASK & tag as u8);
stream.write_u8(identifier)?;
Ok(1)
}
fn read_universal_tag(mut stream: impl io::Read, tag: Tag, pc: Pc) -> io::Result<()> {
let identifier = stream.read_u8()?;
if identifier != Class::Universal as u8 | pc as u8 | (TAG_MASK & tag as u8) {
Err(io::Error::new(
io::ErrorKind::InvalidData,
"invalid universal tag identifier",
))
} else {
Ok(())
}
}
fn write_length(mut stream: impl io::Write, length: u16) -> io::Result<usize> {
if length > 0xFF {
stream.write_u8(0x80 ^ 0x2)?;
stream.write_u16::<BigEndian>(length)?;
Ok(3)
} else if length > 0x7F {
stream.write_u8(0x80 ^ 0x1)?;
stream.write_u8(length as u8)?;
Ok(2)
} else {
stream.write_u8(length as u8)?;
Ok(1)
}
}
fn read_length(mut stream: impl io::Read) -> io::Result<u16> {
let byte = stream.read_u8()?;
if byte & 0x80 != 0 {
let len = byte & !0x80;
if len == 1 {
stream.read_u8().map(u16::from)
} else if len == 2 {
let length = stream.read_u16::<BigEndian>()?;
// u16 should be capable to hold the ASN1 structure length
// this condition checks that length is not too big for the u16 type
if length > u16::MAX - 1 /* tag byte */ - sizeof_length(length) {
return Err(io::Error::new(io::ErrorKind::InvalidData, "the length is too big"));
}
Ok(length)
} else {
Err(io::Error::new(
io::ErrorKind::InvalidData,
"invalid length of the length",
))
}
} else {
Ok(u16::from(byte))
}
}
fn sizeof_length(length: u16) -> u16 {
if length > 0xff {
3
} else if length > 0x7f {
2
} else {
1
}
}
+236
View File
@@ -0,0 +1,236 @@
use super::*;
#[test]
fn write_sequence_tag_is_correct() {
let mut buf = Vec::new();
assert_eq!(write_sequence_tag(&mut buf, 0x100).unwrap(), 4);
assert_eq!(buf, vec![0x30, 0x82, 0x01, 0x00]);
}
#[test]
fn read_sequence_tag_returns_correct_length() {
let buf = vec![0x30, 0x82, 0x01, 0x00];
assert_eq!(read_sequence_tag(&mut buf.as_slice()).unwrap(), 0x100);
}
#[test]
fn read_sequence_tag_returns_error_on_invalid_tag() {
let buf = vec![0x3a, 0x82, 0x01, 0x00];
assert_eq!(
read_sequence_tag(&mut buf.as_slice()).unwrap_err().kind(),
io::ErrorKind::InvalidData
);
}
#[test]
fn write_contextual_tag_constuct_is_correct() {
let mut buf = Vec::new();
assert_eq!(write_contextual_tag(&mut buf, 0x3, 0x100, Pc::Construct).unwrap(), 4);
assert_eq!(buf, vec![0xA3, 0x82, 0x01, 0x00]);
}
#[test]
fn write_contextual_tag_primitive_is_correct() {
let mut buf = Vec::new();
assert_eq!(write_contextual_tag(&mut buf, 0x4, 0xF0, Pc::Primitive).unwrap(), 3);
assert_eq!(buf, vec![0x84, 0x81, 0xF0]);
}
#[test]
fn read_contextual_tag_returns_correct_length() {
let buf = vec![0xA3, 0x82, 0x01, 0x00];
assert_eq!(
read_contextual_tag(&mut buf.as_slice(), 0x3, Pc::Construct).unwrap(),
0x100
);
}
#[test]
fn read_contextual_tag_returns_error_on_invalid_tag() {
let buf = vec![0xA3, 0x82, 0x01, 0x00];
assert_eq!(
read_contextual_tag(&mut buf.as_slice(), 0x2, Pc::Construct)
.unwrap_err()
.kind(),
io::ErrorKind::InvalidData
);
}
#[test]
fn write_octet_string_tag_is_correct() {
let mut buf = Vec::new();
assert_eq!(write_octet_string_tag(&mut buf, 0x0F).unwrap(), 2);
assert_eq!(buf, vec![0x04, 0x0F]);
}
#[test]
fn read_octet_string_tag_is_correct() {
let buf = vec![0x04, 0x0F];
assert_eq!(read_octet_string_tag(&mut buf.as_slice()).unwrap(), 0x0F);
}
#[test]
fn read_octet_string_tag_returns_error_on_wrong_tag() {
let buf = vec![0x05, 0x0F];
assert_eq!(
read_octet_string_tag(&mut buf.as_slice()).unwrap_err().kind(),
io::ErrorKind::InvalidData
);
}
#[test]
fn write_octet_string_is_correct() {
let mut buf = Vec::new();
let string = [0x68, 0x65, 0x6c, 0x6c, 0x6f];
let expected: Vec<_> = [0x04, 0x05].iter().chain(string.iter()).cloned().collect();
assert_eq!(write_octet_string(&mut buf, &string).unwrap(), 7);
assert_eq!(buf, expected);
}
#[test]
fn write_sequence_octet_string_is_correct() {
let mut buf = Vec::new();
let string = [0x68, 0x65, 0x6c, 0x6c, 0x6f];
let expected: Vec<_> = [0xA3, 0x07, 0x04, 0x05].iter().chain(string.iter()).cloned().collect();
assert_eq!(write_sequence_octet_string(&mut buf, 0x03, &string).unwrap(), 9);
assert_eq!(buf, expected);
}
#[test]
fn write_length_is_correct_with_3_byte_length() {
let mut buf = Vec::new();
assert_eq!(write_length(&mut buf, 0x100).unwrap(), 3);
assert_eq!(buf, vec![0x82, 0x01, 0x00]);
}
#[test]
fn write_length_is_correct_with_2_byte_length() {
let mut buf = Vec::new();
assert_eq!(write_length(&mut buf, 0xFA).unwrap(), 2);
assert_eq!(buf, vec![0x81, 0xFA]);
}
#[test]
fn write_length_is_correct_with_1_byte_length() {
let mut buf = Vec::new();
assert_eq!(write_length(&mut buf, 0x70).unwrap(), 1);
assert_eq!(buf, vec![0x70]);
}
#[test]
fn read_length_is_correct_with_3_byte_length() {
let buf = vec![0x82, 0x01, 0x00];
assert_eq!(read_length(&mut buf.as_slice()).unwrap(), 0x100);
}
#[test]
fn read_length_is_correct_with_2_byte_length() {
let buf = vec![0x81, 0xFA];
assert_eq!(read_length(&mut buf.as_slice()).unwrap(), 0xFA);
}
#[test]
fn read_length_is_correct_with_1_byte_length() {
let buf = vec![0x70];
assert_eq!(read_length(&mut buf.as_slice()).unwrap(), 0x70);
}
#[test]
fn read_length_returns_error_on_invalid_length() {
let buf = vec![0x8a, 0x1];
assert_eq!(
read_length(&mut buf.as_slice()).unwrap_err().kind(),
io::ErrorKind::InvalidData
);
}
#[test]
fn write_integer_is_correct_with_4_byte_integer() {
let mut buf = Vec::new();
assert_eq!(write_integer(&mut buf, 0x0080_0000).unwrap(), 6);
assert_eq!(buf, vec![0x02, 0x04, 0x00, 0x80, 0x00, 0x00]);
}
#[test]
fn write_integer_is_correct_with_3_byte_integer() {
let mut buf = Vec::new();
assert_eq!(write_integer(&mut buf, 0x80000).unwrap(), 5);
assert_eq!(buf, vec![0x02, 0x03, 0x08, 0x00, 0x00]);
}
#[test]
fn write_integer_is_correct_with_2_byte_integer() {
let mut buf = Vec::new();
assert_eq!(write_integer(&mut buf, 0x800).unwrap(), 4);
assert_eq!(buf, vec![0x02, 0x02, 0x08, 0x00]);
}
#[test]
fn write_integer_is_correct_with_1_byte_integer() {
let mut buf = Vec::new();
assert_eq!(write_integer(&mut buf, 0x79).unwrap(), 3);
assert_eq!(buf, vec![0x02, 0x01, 0x79]);
}
#[test]
fn read_integer_is_correct_with_8_byte_integer() {
let buf = vec![0x02, 0x08, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
assert_eq!(read_integer(&mut buf.as_slice()).unwrap(), 0x0080_0000_0000_0000);
}
#[test]
fn read_integer_is_correct_with_4_byte_integer() {
let buf = vec![0x02, 0x04, 0x00, 0x80, 0x00, 0x00];
assert_eq!(read_integer(&mut buf.as_slice()).unwrap(), 0x0080_0000);
}
#[test]
fn read_integer_is_correct_with_3_byte_integer() {
let buf = vec![0x02, 0x03, 0x08, 0x00, 0x00];
assert_eq!(read_integer(&mut buf.as_slice()).unwrap(), 0x80000);
}
#[test]
fn read_integer_is_correct_with_2_byte_integer() {
let buf = vec![0x02, 0x02, 0x08, 0x00];
assert_eq!(read_integer(&mut buf.as_slice()).unwrap(), 0x800);
}
#[test]
fn read_integer_is_correct_with_1_byte_integer() {
let buf = vec![0x02, 0x01, 0x79];
assert_eq!(read_integer(&mut buf.as_slice()).unwrap(), 0x79);
}
#[test]
fn read_integer_returns_error_on_incorrect_len() {
let buf = vec![0x02, 0x06, 0x79];
assert_eq!(
read_integer(&mut buf.as_slice()).unwrap_err().kind(),
io::ErrorKind::InvalidData
);
}
#[test]
fn write_universal_tag_primitive_integer_is_correct() {
let mut buf = Vec::new();
assert_eq!(write_universal_tag(&mut buf, Tag::Integer, Pc::Primitive).unwrap(), 1);
assert_eq!(buf, vec![0x02]);
}
#[test]
fn write_universal_tag_construct_enumerated_is_correct() {
let mut buf = Vec::new();
assert_eq!(
write_universal_tag(&mut buf, Tag::Enumerated, Pc::Construct).unwrap(),
1
);
assert_eq!(buf, vec![0x2A]);
}
#[test]
fn sizeof_length_with_long_len() {
let len = 625;
let expected = 3;
assert_eq!(sizeof_length(len), expected);
}