feat(rdp): RD Gateway transport (experimental)
RDP profiles can go through a Remote Desktop Gateway (MS-TSGU over an
HTTPS WebSocket) with its own username and password, stored like other
credentials (rdp-gateway-password). The gateway stream is bridged to a
loopback socket so the blocking RDP client uses it like any transport.
IronRDP's gateway crate (0.0.1) is vendored with fixes: it used
ironrdp-tls, which accepts any certificate, before sending Basic
credentials — the gateway is now verified against the web PKI; its
asserts on gateway-supplied lengths are errors instead of panics; and
the server port is passed through. Checked against a self-signed
openssl s_server: the connection is refused ('invalid peer certificate')
before any request or credential is sent. Not tested against a real
gateway; Basic-auth gateways only (no NTLM).
This commit is contained in:
+1
-1
@@ -144,7 +144,7 @@ Use Nexus Terminal when you need to:
|
||||
|
||||
- Native RDP viewer (IronRDP) with Network Level Authentication (CredSSP/NTLM) and optional TLS-only logon for servers such as xrdp.
|
||||
- Server certificate pinned by fingerprint in Known Hosts, checked before credentials are sent.
|
||||
- Direct, SOCKS5, HTTP CONNECT or SSH-tunnelled connections.
|
||||
- Direct, SOCKS5, HTTP CONNECT, SSH-tunnelled or (experimental) RD Gateway connections.
|
||||
- Scale-to-fit or 1:1, a fixed resolution or one that follows the tab (Display Control).
|
||||
- Keyboard (scancodes for keys and shortcuts, Unicode for text), mouse and wheel; Ctrl+Alt+Del; text clipboard sharing in both directions.
|
||||
- Password storage modes and domain logon.
|
||||
|
||||
@@ -166,6 +166,7 @@ Use SSH tunnelling for VNC whenever possible. If a VNC profile has no tunnel, Ne
|
||||
|
||||
- **RDP**: domain (optional), password storage mode, **Certificate Policy** and **Allow servers without NLA**. The username, host and port (3389) are the profile's own.
|
||||
- **Display**: **Scale to Fit**, a fixed **Width** and **Height** (0 × 0 fits the tab) and **Resize With Tab**.
|
||||
- **RD Gateway** (experimental): reach the desktop through a Remote Desktop Gateway (`host` or `host:port`) with its own username and password. The gateway's certificate must be valid for its name (public CA); only gateways that accept Basic authentication over HTTPS work.
|
||||
- **Proxy**: **SOCKS5** or **HTTP CONNECT**, with an optional username and password.
|
||||
- **Devices**: **Play Remote Audio**; **Redirect Printer**, which adds a *Nexus Terminal* printer on the remote desktop whose jobs are saved as PostScript files in your Downloads folder (up to 256 MiB per job); and on Linux and macOS a **Shared Folder** that appears on the remote desktop as a drive named Nexus. The server can read and change everything in that folder and nothing outside it: paths that try to leave it (`..`, or symlinks pointing elsewhere) are refused.
|
||||
- **SSH Tunnel**: carry RDP through SSH, as for VNC. Use either a proxy or a tunnel, not both.
|
||||
|
||||
Generated
+131
@@ -2631,6 +2631,29 @@ dependencies = [
|
||||
"itoa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-body"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-body-util"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"http",
|
||||
"http-body",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httparse"
|
||||
version = "1.10.1"
|
||||
@@ -2655,6 +2678,39 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c3e324da4c95177d6291d4c8730197c0d1822f8a9766814a4a44fa5ab797c9c"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"http",
|
||||
"http-body",
|
||||
"httparse",
|
||||
"itoa",
|
||||
"pin-project-lite",
|
||||
"smallvec",
|
||||
"tokio",
|
||||
"want",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http",
|
||||
"http-body",
|
||||
"hyper",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.65"
|
||||
@@ -2978,6 +3034,27 @@ dependencies = [
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ironrdp-mstsgu"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bitflags 2.13.2",
|
||||
"futures-util",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"ironrdp-core",
|
||||
"ironrdp-error",
|
||||
"log",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-tungstenite",
|
||||
"tokio-util",
|
||||
"uuid",
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ironrdp-pdu"
|
||||
version = "0.9.0"
|
||||
@@ -3651,6 +3728,7 @@ dependencies = [
|
||||
"image",
|
||||
"ironrdp",
|
||||
"ironrdp-blocking",
|
||||
"ironrdp-mstsgu",
|
||||
"ironrdp-rdpdr-native",
|
||||
"ironrdp-rdpsnd-native",
|
||||
"keyring",
|
||||
@@ -6366,6 +6444,28 @@ dependencies = [
|
||||
"syn 3.0.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.26.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9cc2678c2cdd569ef8215e2afd7954ada2ae20b4fdd2c5fe6139a3b02d105db"
|
||||
dependencies = [
|
||||
"rustls",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-tungstenite"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"log",
|
||||
"tokio",
|
||||
"tungstenite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.20"
|
||||
@@ -6479,6 +6579,28 @@ dependencies = [
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "try-lock"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||
|
||||
[[package]]
|
||||
name = "tungstenite"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"data-encoding",
|
||||
"http",
|
||||
"httparse",
|
||||
"log",
|
||||
"rand 0.9.5",
|
||||
"sha1 0.10.7",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "type-map"
|
||||
version = "0.5.1"
|
||||
@@ -6698,6 +6820,15 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "want"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec4cdd0dd910afe868b7ef477227d8d538b46b3075031afee8a9f2acb0a2ed0b"
|
||||
dependencies = [
|
||||
"try-lock",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
|
||||
@@ -22,3 +22,4 @@ lto = "thin"
|
||||
picky = { path = "vendor/picky" }
|
||||
sspi = { path = "vendor/sspi" }
|
||||
ironrdp-session = { path = "vendor/ironrdp-session" }
|
||||
ironrdp-mstsgu = { path = "vendor/ironrdp-mstsgu" }
|
||||
|
||||
@@ -739,6 +739,19 @@ impl ProfileEditor {
|
||||
check(ui, "Resize With Tab", &mut c.dynamic_resolution, "Ask the server to match the tab's size as it changes");
|
||||
}
|
||||
});
|
||||
section(ui, "RD Gateway", |ui| {
|
||||
text(ui, "Gateway", &mut c.gateway_host, "gateway.example.com (empty: none)");
|
||||
err(ui, errs, "rdp.gatewayHost");
|
||||
if !c.gateway_host.trim().is_empty() {
|
||||
text(ui, "Gateway Username", &mut c.gateway_username, "DOMAIN\\user");
|
||||
err(ui, errs, "rdp.gatewayUsername");
|
||||
mode(ui, "Gateway Password Storage", &mut c.gateway_secret_mode);
|
||||
if c.gateway_secret_mode == SecretMode::Saved {
|
||||
secret(ui, "Gateway Password", secrets, SecretKind::RdpGatewayPassword, "Leave blank to keep the saved password");
|
||||
}
|
||||
widgets::hint(ui, "Experimental: gateways that accept Basic authentication over HTTPS (the gateway's certificate is verified).");
|
||||
}
|
||||
});
|
||||
section(ui, "Proxy", |ui| {
|
||||
combo_s(ui, "Proxy Type", &mut c.proxy_type, &[("none", "None (direct connection)"), ("socks5", "SOCKS5"), ("http", "HTTP CONNECT")]);
|
||||
err(ui, errs, "rdp.proxyType");
|
||||
|
||||
@@ -39,6 +39,7 @@ webpki-roots = "1"
|
||||
ironrdp = { version = "0.17", features = ["connector", "session", "graphics", "pdu", "svc", "dvc", "displaycontrol", "input", "cliprdr", "rdpsnd", "rdpdr"] }
|
||||
ironrdp-rdpsnd-native = { version = "0.7", default-features = false }
|
||||
ironrdp-blocking = "0.10"
|
||||
ironrdp-mstsgu = "0.0.1"
|
||||
x509-cert = { version = "0.2", default-features = false, features = ["std"] }
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
|
||||
@@ -467,6 +467,34 @@ fn copy_rect(image: &DecodedImage, fb: &mut Framebuffer, left: u16, top: u16, ri
|
||||
fb.dirty = true;
|
||||
}
|
||||
|
||||
/// A transport through an RD Gateway (MS-TSGU over HTTPS WebSocket, Basic authentication). The
|
||||
/// gateway's certificate is verified against the web PKI. The async gateway stream is bridged to
|
||||
/// a loopback socket so the blocking RDP client can use it like any other connection.
|
||||
pub fn gateway_dial(endpoint: String, user: String, password: zeroize::Zeroizing<String>, server: String, port: u16) -> Dial {
|
||||
Arc::new(move || {
|
||||
let endpoint = if endpoint.contains(':') { endpoint.clone() } else { format!("{endpoint}:443") };
|
||||
let target = ironrdp_mstsgu::GwConnectTarget {
|
||||
gw_endpoint: endpoint,
|
||||
gw_user: user.clone(),
|
||||
gw_pass: password.to_string(),
|
||||
server: server.clone(),
|
||||
server_port: port,
|
||||
};
|
||||
let (gw, _) = super::rt()
|
||||
.block_on(ironrdp_mstsgu::GwClient::connect(&target, "Nexus Terminal"))
|
||||
.map_err(|e| format!("RD Gateway {}: {}", target.gw_endpoint, error_chain(&e)))?;
|
||||
let (client, server_end) = super::ssh::loopback_pair()?;
|
||||
server_end.set_nonblocking(true)?;
|
||||
super::rt().spawn(async move {
|
||||
if let Ok(mut local) = tokio::net::TcpStream::from_std(server_end) {
|
||||
let mut gw = gw;
|
||||
let _ = tokio::io::copy_bidirectional(&mut local, &mut gw).await;
|
||||
}
|
||||
});
|
||||
Ok(client)
|
||||
})
|
||||
}
|
||||
|
||||
/// `SHA256:<unpadded base64>` of the certificate, the format SSH host keys use.
|
||||
pub fn fingerprint(cert_der: &[u8]) -> String {
|
||||
use base64::Engine;
|
||||
@@ -544,10 +572,14 @@ impl connector::sspi::network_client::NetworkClient for NoNetwork {
|
||||
}
|
||||
}
|
||||
|
||||
/// Connector errors carry their cause in a chain; surface the useful part.
|
||||
/// IronRDP errors carry their cause in a chain; surface the useful part.
|
||||
fn describe(e: &connector::ConnectorError) -> String {
|
||||
error_chain(e)
|
||||
}
|
||||
|
||||
fn error_chain(e: &dyn std::error::Error) -> String {
|
||||
let mut s = e.to_string();
|
||||
let mut src = std::error::Error::source(e);
|
||||
let mut src = e.source();
|
||||
while let Some(c) = src {
|
||||
s = format!("{s}: {c}");
|
||||
src = c.source();
|
||||
|
||||
@@ -418,6 +418,10 @@ pub struct RdpConfig {
|
||||
/// Trust policy for the server's TLS certificate (pinned like an SSH host key).
|
||||
#[serde(deserialize_with = "lenient")]
|
||||
pub host_key_policy: HostKeyPolicy,
|
||||
/// RD Gateway (`host` or `host:port`, HTTPS); empty connects without one. Basic auth only.
|
||||
pub gateway_host: String,
|
||||
pub gateway_username: String,
|
||||
pub gateway_secret_mode: SecretMode,
|
||||
/// "none", "socks5" or "http"; the password is a stored secret.
|
||||
pub proxy_type: String,
|
||||
pub proxy_host: String,
|
||||
|
||||
@@ -34,12 +34,13 @@ pub enum SecretKind {
|
||||
RdpPassword,
|
||||
RdpSshPassword,
|
||||
RdpSshPassphrase,
|
||||
RdpGatewayPassword,
|
||||
}
|
||||
|
||||
impl SecretKind {
|
||||
/// Every kind, so deleting a profile can purge them all from the keychain. A new variant
|
||||
/// must be added here as well as to `as_str`.
|
||||
pub const ALL: [SecretKind; 15] = [
|
||||
pub const ALL: [SecretKind; 16] = [
|
||||
Self::Password,
|
||||
Self::Passphrase,
|
||||
Self::Totp,
|
||||
@@ -55,6 +56,7 @@ impl SecretKind {
|
||||
Self::RdpPassword,
|
||||
Self::RdpSshPassword,
|
||||
Self::RdpSshPassphrase,
|
||||
Self::RdpGatewayPassword,
|
||||
];
|
||||
|
||||
/// The storage name, shared with the Go app.
|
||||
@@ -75,6 +77,7 @@ impl SecretKind {
|
||||
Self::RdpPassword => "rdp-password",
|
||||
Self::RdpSshPassword => "rdp-ssh-password",
|
||||
Self::RdpSshPassphrase => "rdp-ssh-passphrase",
|
||||
Self::RdpGatewayPassword => "rdp-gateway-password",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -315,12 +315,17 @@ impl Manager {
|
||||
) -> Result<Arc<clients::rdp::Client>> {
|
||||
let timeout = Duration::from_secs(p.connect_timeout.max(1) as u64);
|
||||
let host = p.host.clone();
|
||||
let dial: clients::rdp::Dial = match tunnel {
|
||||
Some(t) => {
|
||||
let gateway = (!rc.gateway_host.trim().is_empty()).then(|| rc.gateway_host.trim().to_string());
|
||||
let dial: clients::rdp::Dial = match (tunnel, gateway) {
|
||||
(_, Some(gw)) => {
|
||||
let pw = self.secret_fn(id, &p.id, SecretKind::RdpGatewayPassword, rc.gateway_secret_mode, prompt.clone())()?;
|
||||
clients::rdp::gateway_dial(gw, rc.gateway_username.clone(), pw, host.clone(), port)
|
||||
}
|
||||
(Some(t), None) => {
|
||||
let h = host.clone();
|
||||
Arc::new(move || t.open_tunnel(&h, port))
|
||||
}
|
||||
None => {
|
||||
(None, None) => {
|
||||
let proxy = clients::proxy::Proxy {
|
||||
kind: rc.proxy_type.clone(),
|
||||
host: rc.proxy_host.clone(),
|
||||
|
||||
@@ -334,6 +334,16 @@ pub fn validate_rdp_config(cfg: &RdpConfig) -> Result<(), ValidationErrors> {
|
||||
}
|
||||
t => errs.add("rdp.proxyType", format!("unsupported proxy type {t:?}")),
|
||||
}
|
||||
if !cfg.gateway_host.trim().is_empty() {
|
||||
let host = cfg.gateway_host.trim().rsplit_once(':').filter(|(_, p)| p.parse::<u16>().is_ok()).map_or(cfg.gateway_host.trim(), |(h, _)| h);
|
||||
errs.check("rdp.gatewayHost", validate_host(host));
|
||||
if cfg.gateway_username.trim().is_empty() {
|
||||
errs.add("rdp.gatewayUsername", "the gateway needs a username");
|
||||
}
|
||||
if cfg.ssh_tunnel || !matches!(cfg.proxy_type.as_str(), "" | "none") {
|
||||
errs.add("rdp.gatewayHost", "use a gateway, a proxy or an SSH tunnel — only one");
|
||||
}
|
||||
}
|
||||
let share = cfg.shared_folder.trim();
|
||||
if !share.is_empty() && !std::path::Path::new(share).is_dir() {
|
||||
errs.add("rdp.sharedFolder", "the shared folder must be an existing folder");
|
||||
|
||||
@@ -20,6 +20,7 @@ members = ["."]
|
||||
picky = { path = "../vendor/picky" }
|
||||
sspi = { path = "../vendor/sspi" }
|
||||
ironrdp-session = { path = "../vendor/ironrdp-session" }
|
||||
ironrdp-mstsgu = { path = "../vendor/ironrdp-mstsgu" }
|
||||
|
||||
[profile.release]
|
||||
debug = 1
|
||||
|
||||
Vendored
+1
@@ -7,6 +7,7 @@ IronRDP release includes the fix it carries.
|
||||
|---|---|---|
|
||||
| `picky` | 7.0.0-rc.25 (crates.io) | `=x.y.z-rc.N` RustCrypto pins relaxed to `^x.y.z-rc.N` (so the stable releases russh uses satisfy them); one unused import removed. |
|
||||
| `sspi` | 0.21.3 (crates.io) | Same pin relaxation. |
|
||||
| `ironrdp-mstsgu` | 0.0.1 (crates.io) | RD Gateway client. Upstream connects with `ironrdp-tls`, which accepts any certificate, and then sends Basic credentials; the patch verifies the gateway against the web PKI (rustls/ring + webpki-roots). Its `assert!`s and unchecked length subtractions on gateway packets become errors, and the RDP server's port is passed through instead of a fixed 3389. |
|
||||
| `ironrdp-session` | 0.11.0 (crates.io) | Upstream fixes not yet released: #1486 "preserve bitmap source stride" (commit `80bb81b344`, applied as-is) and the RDP6 orientation flip from #1524 (`8f1737a287`). Without them xrdp desktops render sheared and upside-down tile by tile. |
|
||||
|
||||
Why: IronRDP pins `picky = "=7.0.0-rc.25"`, which pins `curve25519-dalek = "=5.0.0-rc.1"` and
|
||||
|
||||
+1242
File diff suppressed because it is too large
Load Diff
+223
@@ -0,0 +1,223 @@
|
||||
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
|
||||
#
|
||||
# When uploading crates to the registry Cargo will automatically
|
||||
# "normalize" Cargo.toml files for maximal compatibility
|
||||
# with all versions of Cargo and also rewrite `path` dependencies
|
||||
# to registry (e.g., crates.io) dependencies.
|
||||
#
|
||||
# If you are reading this file be aware that the original Cargo.toml
|
||||
# will likely look very different (and much more reasonable).
|
||||
# See Cargo.toml.orig for the original contents.
|
||||
|
||||
[package]
|
||||
edition = "2024"
|
||||
rust-version = "1.89"
|
||||
name = "ironrdp-mstsgu"
|
||||
version = "0.0.1"
|
||||
authors = [
|
||||
"Devolutions Inc. <infos@devolutions.net>",
|
||||
"Teleport <goteleport.com>",
|
||||
]
|
||||
build = false
|
||||
autolib = false
|
||||
autobins = false
|
||||
autoexamples = false
|
||||
autotests = false
|
||||
autobenches = false
|
||||
description = "Terminal Services Gateway Server Protocol"
|
||||
homepage = "https://github.com/Devolutions/IronRDP"
|
||||
readme = "README.md"
|
||||
keywords = [
|
||||
"rdp",
|
||||
"remote-desktop",
|
||||
"network",
|
||||
"client",
|
||||
"protocol",
|
||||
]
|
||||
categories = ["network-programming"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/Devolutions/IronRDP"
|
||||
resolver = "2"
|
||||
|
||||
# Nexus patch: TLS is always rustls (ring) with web PKI verification; see src/lib.rs.
|
||||
[features]
|
||||
default = []
|
||||
|
||||
[lib]
|
||||
name = "ironrdp_mstsgu"
|
||||
path = "src/lib.rs"
|
||||
test = false
|
||||
doctest = false
|
||||
|
||||
[dependencies.base64]
|
||||
version = "0.22"
|
||||
|
||||
[dependencies.bitflags]
|
||||
version = "2.11"
|
||||
|
||||
[dependencies.futures-util]
|
||||
version = "0.3"
|
||||
|
||||
[dependencies.http-body-util]
|
||||
version = "0.1"
|
||||
|
||||
[dependencies.hyper]
|
||||
version = "1.9"
|
||||
features = [
|
||||
"client",
|
||||
"http1",
|
||||
]
|
||||
|
||||
[dependencies.hyper-util]
|
||||
version = "0.1"
|
||||
features = ["tokio"]
|
||||
|
||||
[dependencies.ironrdp-core]
|
||||
version = "0.2"
|
||||
features = ["std"]
|
||||
|
||||
[dependencies.ironrdp-error]
|
||||
version = "0.2"
|
||||
|
||||
[dependencies.tokio-rustls]
|
||||
version = "0.26"
|
||||
default-features = false
|
||||
features = ["ring", "tls12"]
|
||||
|
||||
[dependencies.webpki-roots]
|
||||
version = "1"
|
||||
|
||||
[dependencies.log]
|
||||
version = "0.4"
|
||||
|
||||
[dependencies.tokio]
|
||||
version = "1.52"
|
||||
features = [
|
||||
"macros",
|
||||
"rt",
|
||||
]
|
||||
|
||||
[dependencies.tokio-tungstenite]
|
||||
version = "0.29"
|
||||
|
||||
[dependencies.tokio-util]
|
||||
version = "0.7"
|
||||
|
||||
[dependencies.uuid]
|
||||
version = "1"
|
||||
features = ["v4"]
|
||||
|
||||
[lints.clippy]
|
||||
alloc_instead_of_core = "warn"
|
||||
allow_attributes = "warn"
|
||||
as_conversions = "warn"
|
||||
as_pointer_underscore = "warn"
|
||||
as_ptr_cast_mut = "warn"
|
||||
as_underscore = "warn"
|
||||
cast_lossless = "warn"
|
||||
cast_possible_truncation = "warn"
|
||||
cast_possible_wrap = "warn"
|
||||
cast_ptr_alignment = "warn"
|
||||
cast_sign_loss = "warn"
|
||||
cfg_not_test = "warn"
|
||||
checked_conversions = "warn"
|
||||
clone_on_ref_ptr = "warn"
|
||||
cloned_instead_of_copied = "warn"
|
||||
collection_is_never_read = "warn"
|
||||
copy_iterator = "warn"
|
||||
dbg_macro = "warn"
|
||||
deref_by_slicing = "warn"
|
||||
disallowed_script_idents = "warn"
|
||||
doc_include_without_cfg = "warn"
|
||||
empty_drop = "warn"
|
||||
empty_enum_variants_with_brackets = "warn"
|
||||
expl_impl_clone_on_copy = "warn"
|
||||
filetype_is_file = "warn"
|
||||
float_cmp = "warn"
|
||||
float_cmp_const = "warn"
|
||||
fn_to_numeric_cast_any = "warn"
|
||||
get_unwrap = "warn"
|
||||
implicit_clone = "warn"
|
||||
infinite_loop = "warn"
|
||||
inline_always = "warn"
|
||||
large_futures = "warn"
|
||||
large_include_file = "warn"
|
||||
large_stack_frames = "warn"
|
||||
large_types_passed_by_value = "warn"
|
||||
lossy_float_literal = "warn"
|
||||
map_with_unused_argument_over_ranges = "warn"
|
||||
mem_forget = "warn"
|
||||
missing_panics_doc = "warn"
|
||||
missing_safety_doc = "warn"
|
||||
mixed_read_write_in_expression = "warn"
|
||||
multiple_inherent_impl = "warn"
|
||||
multiple_unsafe_ops_per_block = "warn"
|
||||
needless_raw_strings = "warn"
|
||||
non_ascii_literal = "warn"
|
||||
non_zero_suggestions = "warn"
|
||||
or_fun_call = "warn"
|
||||
panic = "warn"
|
||||
partial_pub_fields = "warn"
|
||||
precedence_bits = "warn"
|
||||
print_stderr = "warn"
|
||||
print_stdout = "warn"
|
||||
ptr_cast_constness = "warn"
|
||||
pub_without_shorthand = "warn"
|
||||
range_plus_one = "warn"
|
||||
rc_buffer = "warn"
|
||||
rc_mutex = "warn"
|
||||
redundant_clone = "warn"
|
||||
redundant_type_annotations = "warn"
|
||||
renamed_function_params = "warn"
|
||||
rest_pat_in_fully_bound_structs = "warn"
|
||||
return_self_not_must_use = "warn"
|
||||
same_name_method = "warn"
|
||||
self_named_module_files = "warn"
|
||||
semicolon_outside_block = "warn"
|
||||
separated_literal_suffix = "warn"
|
||||
similar_names = "warn"
|
||||
std_instead_of_core = "warn"
|
||||
str_to_string = "warn"
|
||||
string_add = "warn"
|
||||
string_lit_chars_any = "warn"
|
||||
string_slice = "warn"
|
||||
suspicious_xor_used_as_pow = "warn"
|
||||
todo = "warn"
|
||||
trait_duplication_in_bounds = "warn"
|
||||
transmute_ptr_to_ptr = "warn"
|
||||
try_err = "warn"
|
||||
type_repetition_in_bounds = "warn"
|
||||
undocumented_unsafe_blocks = "warn"
|
||||
unnecessary_box_returns = "warn"
|
||||
unnecessary_safety_comment = "warn"
|
||||
unnecessary_self_imports = "warn"
|
||||
unused_result_ok = "warn"
|
||||
unused_self = "warn"
|
||||
unused_trait_names = "warn"
|
||||
unwrap_used = "warn"
|
||||
useless_let_if_seq = "warn"
|
||||
wildcard_dependencies = "warn"
|
||||
wildcard_imports = "warn"
|
||||
|
||||
[lints.rust]
|
||||
absolute_paths_not_starting_with_crate = "warn"
|
||||
ambiguous_negative_literals = "warn"
|
||||
elided_lifetimes_in_paths = "warn"
|
||||
invalid_reference_casting = "warn"
|
||||
keyword_idents = "warn"
|
||||
macro_use_extern_crate = "warn"
|
||||
missing_unsafe_on_extern = "warn"
|
||||
noop_method_call = "warn"
|
||||
redundant_imports = "warn"
|
||||
redundant_lifetimes = "warn"
|
||||
single_use_lifetimes = "warn"
|
||||
trivial_numeric_casts = "warn"
|
||||
unit_bindings = "warn"
|
||||
unreachable_pub = "warn"
|
||||
unsafe_attr_outside_unsafe = "warn"
|
||||
unsafe_op_in_unsafe_fn = "warn"
|
||||
unused_crate_dependencies = "warn"
|
||||
unused_lifetimes = "warn"
|
||||
unused_macro_rules = "warn"
|
||||
unused_qualifications = "warn"
|
||||
unused_unsafe = "warn"
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
[package]
|
||||
name = "ironrdp-mstsgu"
|
||||
version = "0.0.1"
|
||||
readme = "README.md"
|
||||
description = "Terminal Services Gateway Server Protocol"
|
||||
edition.workspace = true
|
||||
rust-version = "1.89"
|
||||
license.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
authors.workspace = true
|
||||
keywords.workspace = true
|
||||
categories.workspace = true
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
test = false
|
||||
|
||||
[features]
|
||||
default = []
|
||||
rustls = ["ironrdp-tls/rustls", "tokio-tungstenite/rustls-tls-native-roots"]
|
||||
native-tls = ["ironrdp-tls/native-tls", "tokio-tungstenite/native-tls"]
|
||||
|
||||
[dependencies]
|
||||
base64 = "0.22"
|
||||
bitflags = "2.11"
|
||||
futures-util = "0.3"
|
||||
http-body-util = { version = "0.1" }
|
||||
hyper-util = { version = "0.1", features = ["tokio"] }
|
||||
hyper = { version = "1.9", features = ["client", "http1"] }
|
||||
ironrdp-core = { path = "../ironrdp-core", version = "0.2", features = ["std"] }
|
||||
ironrdp-error = { path = "../ironrdp-error", version = "0.2" }
|
||||
ironrdp-tls = { path = "../ironrdp-tls", version = "0.2" }
|
||||
log = "0.4"
|
||||
tokio-tungstenite = { version = "0.29" }
|
||||
tokio-util = { version = "0.7" }
|
||||
tokio = { version = "1.52", features = ["macros", "rt"] }
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
# IronRDP MS-TSGU
|
||||
|
||||
[Terminal Services Gateway Server Protocol][MS-TSGU] implementation for IronRDP.
|
||||
|
||||
This crate
|
||||
- implements an MVP state needed to connect through Microsoft RD Gateway,
|
||||
- only supports the HTTPS protocol with WebSocket (and not the legacy HTTP, HTTP-RPC or UDP protocols),
|
||||
- does not implement reconnection/reauthentication, and
|
||||
- only supports basic auth.
|
||||
|
||||
[MS-TSGU]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsgu/0007d661-a86d-4e8f-89f7-7f77f8824188
|
||||
+487
@@ -0,0 +1,487 @@
|
||||
#![cfg_attr(doc, doc = include_str!("../README.md"))]
|
||||
#![doc(html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg")]
|
||||
|
||||
#[macro_use]
|
||||
mod macros;
|
||||
|
||||
mod proto;
|
||||
|
||||
use core::fmt;
|
||||
use core::fmt::Display;
|
||||
use core::pin::Pin;
|
||||
use core::task::Poll;
|
||||
use core::time::Duration;
|
||||
use std::io;
|
||||
|
||||
use base64::Engine as _;
|
||||
use base64::engine::general_purpose::STANDARD;
|
||||
use futures_util::stream::{SplitSink, SplitStream};
|
||||
use futures_util::{FutureExt as _, SinkExt as _, StreamExt as _};
|
||||
use hyper::body::Bytes;
|
||||
use ironrdp_core::{Decode as _, Encode, ReadCursor, WriteCursor};
|
||||
type TlsStream<S> = tokio_rustls::client::TlsStream<S>;
|
||||
use log::{error, warn};
|
||||
use tokio::io::{AsyncRead, AsyncWrite};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::sync::oneshot;
|
||||
use tokio_tungstenite::WebSocketStream;
|
||||
use tokio_tungstenite::tungstenite::handshake::client::generate_key;
|
||||
use tokio_tungstenite::tungstenite::protocol::Role;
|
||||
use tokio_tungstenite::tungstenite::{Message, http};
|
||||
use tokio_util::sync::PollSender;
|
||||
|
||||
use self::proto::{
|
||||
ChannelPkt, ChannelResp, DataPkt, HandshakeReqPkt, HandshakeRespPkt, HttpCapsTy, KeepalivePkt, PktHdr, PktTy,
|
||||
TunnelAuthPkt, TunnelAuthRespPkt, TunnelReqPkt, TunnelRespPkt,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GwConnectTarget {
|
||||
pub gw_endpoint: String,
|
||||
pub gw_user: String,
|
||||
pub gw_pass: String,
|
||||
|
||||
pub server: String,
|
||||
/// The RDP server's port behind the gateway (Nexus patch: was always 3389).
|
||||
pub server_port: u16,
|
||||
}
|
||||
|
||||
type Error = ironrdp_error::Error<GwErrorKind>;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum GwErrorKind {
|
||||
InvalidGwTarget,
|
||||
Connect,
|
||||
PacketEof,
|
||||
UnsupportedFeature,
|
||||
Custom,
|
||||
Encode,
|
||||
Decode,
|
||||
}
|
||||
|
||||
trait GwErrorExt {
|
||||
fn custom<E>(context: &'static str, e: E) -> Self
|
||||
where
|
||||
E: core::error::Error + Sync + Send + 'static;
|
||||
}
|
||||
|
||||
impl GwErrorExt for ironrdp_error::Error<GwErrorKind> {
|
||||
#[track_caller]
|
||||
fn custom<E>(context: &'static str, e: E) -> Self
|
||||
where
|
||||
E: core::error::Error + Sync + Send + 'static,
|
||||
{
|
||||
Self::new(context, GwErrorKind::Custom).with_source(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for GwErrorKind {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let x = match self {
|
||||
GwErrorKind::InvalidGwTarget => "invalid GW Target",
|
||||
GwErrorKind::Connect => "connection error",
|
||||
GwErrorKind::PacketEof => "PacketEOF",
|
||||
GwErrorKind::UnsupportedFeature => "unsupported feature",
|
||||
GwErrorKind::Custom => "custom",
|
||||
GwErrorKind::Encode => "encode",
|
||||
GwErrorKind::Decode => "decode",
|
||||
};
|
||||
f.write_str(x)
|
||||
}
|
||||
}
|
||||
|
||||
impl core::error::Error for GwErrorKind {}
|
||||
|
||||
struct GwConn {
|
||||
client_name: String,
|
||||
target: GwConnectTarget,
|
||||
ws_sink: SplitSink<WebSocketStream<TlsStream<TcpStream>>, Message>,
|
||||
ws_stream: SplitStream<WebSocketStream<TlsStream<TcpStream>>>,
|
||||
}
|
||||
|
||||
pub struct GwClient {
|
||||
work: tokio::task::JoinHandle<Result<(), Error>>,
|
||||
rx: tokio::sync::mpsc::Receiver<Bytes>,
|
||||
rx_bufs: Vec<Bytes>,
|
||||
tx: PollSender<Bytes>,
|
||||
}
|
||||
|
||||
impl Drop for GwClient {
|
||||
fn drop(&mut self) {
|
||||
self.work.abort();
|
||||
}
|
||||
}
|
||||
|
||||
impl GwClient {
|
||||
pub async fn connect(
|
||||
target: &GwConnectTarget,
|
||||
client_name: &str,
|
||||
) -> Result<(GwClient, core::net::SocketAddr), Error> {
|
||||
let gw_host = target
|
||||
.gw_endpoint
|
||||
.split(":")
|
||||
.nth(0)
|
||||
.ok_or_else(|| Error::new("Connect", GwErrorKind::InvalidGwTarget))?;
|
||||
|
||||
let stream = TcpStream::connect(&target.gw_endpoint)
|
||||
.await
|
||||
.map_err(|e| custom_err!("TCP connect", e))?;
|
||||
let client_addr = stream
|
||||
.local_addr()
|
||||
.map_err(|e| custom_err!("get socket local address", e))?;
|
||||
|
||||
// Nexus patch: the gateway is a public HTTPS endpoint, so its certificate is verified
|
||||
// against the web PKI (upstream used ironrdp-tls, which accepts any certificate, before
|
||||
// sending Basic credentials).
|
||||
let mut roots = tokio_rustls::rustls::RootCertStore::empty();
|
||||
roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
|
||||
let config = tokio_rustls::rustls::ClientConfig::builder_with_provider(std::sync::Arc::new(
|
||||
tokio_rustls::rustls::crypto::ring::default_provider(),
|
||||
))
|
||||
.with_safe_default_protocol_versions()
|
||||
.map_err(|e| custom_err!("TLS config", e))?
|
||||
.with_root_certificates(roots)
|
||||
.with_no_client_auth();
|
||||
let name = tokio_rustls::rustls::pki_types::ServerName::try_from(gw_host.to_owned())
|
||||
.map_err(|e| custom_err!("gateway host name", e))?;
|
||||
let stream = tokio_rustls::TlsConnector::from(std::sync::Arc::new(config))
|
||||
.connect(name, stream)
|
||||
.await
|
||||
.map_err(|e| custom_err!("TLS connect", e))?;
|
||||
|
||||
let auth_val: String = STANDARD.encode(format!("{}:{}", target.gw_user, target.gw_pass));
|
||||
let req = http::Request::builder()
|
||||
.method("RDG_OUT_DATA")
|
||||
.header(hyper::header::HOST, gw_host)
|
||||
.header("Rdg-Connection-Id", format!("{{{}}}", uuid::Uuid::new_v4()))
|
||||
.uri("/remoteDesktopGateway/")
|
||||
.header(hyper::header::AUTHORIZATION, format!("Basic {auth_val}"))
|
||||
.header(hyper::header::CONNECTION, "Upgrade")
|
||||
.header(hyper::header::UPGRADE, "websocket")
|
||||
.header(hyper::header::SEC_WEBSOCKET_VERSION, "13")
|
||||
.header(hyper::header::SEC_WEBSOCKET_KEY, generate_key())
|
||||
.body(http_body_util::Empty::<Bytes>::new())
|
||||
.map_err(|e| custom_err!("failed to build request", e))?;
|
||||
|
||||
let stream = hyper_util::rt::tokio::TokioIo::new(stream);
|
||||
let (mut sender, mut conn) = hyper::client::conn::http1::handshake(stream)
|
||||
.await
|
||||
.map_err(|e| custom_err!("H1 Handshake", e))?;
|
||||
let (tx, rx) = oneshot::channel();
|
||||
|
||||
let jh = tokio::task::spawn(async move {
|
||||
tokio::select! {
|
||||
Err(e) = &mut conn => error!("Handshake error: {:?}", e),
|
||||
_ = rx => (),
|
||||
}
|
||||
conn.into_parts()
|
||||
});
|
||||
let resp = sender
|
||||
.send_request(req)
|
||||
.await
|
||||
.map_err(|e| custom_err!("WS Upgrade Send error", e))?;
|
||||
|
||||
if resp.status() != http::StatusCode::SWITCHING_PROTOCOLS {
|
||||
return Err(Error::new("WS Upgrade", GwErrorKind::Connect));
|
||||
}
|
||||
|
||||
let _ = tx.send(()); // TODO: Not needed since it doesnt keep alive conn?
|
||||
let stream = jh.await.map_err(|e| custom_err!("WS join", e))?.io.into_inner();
|
||||
|
||||
Self::connect_ws(target.clone(), client_name, stream)
|
||||
.await
|
||||
.map(|x| (x, client_addr))
|
||||
}
|
||||
|
||||
async fn connect_ws(
|
||||
target: GwConnectTarget,
|
||||
client_name: &str,
|
||||
tls_stream: TlsStream<TcpStream>,
|
||||
) -> Result<GwClient, Error> {
|
||||
let ws_stream: WebSocketStream<_> = WebSocketStream::from_raw_socket(tls_stream, Role::Client, None).await;
|
||||
let (ws_sink, ws_stream) = ws_stream.split();
|
||||
let mut gw = GwConn {
|
||||
client_name: client_name.to_owned(),
|
||||
target,
|
||||
ws_sink,
|
||||
ws_stream,
|
||||
};
|
||||
|
||||
gw.handshake().await?;
|
||||
gw.tunnel().await?;
|
||||
gw.tunnel_auth().await?;
|
||||
gw.channel().await?;
|
||||
|
||||
let (in_tx, in_rx) = tokio::sync::mpsc::channel(4);
|
||||
let (out_tx, mut out_rx) = tokio::sync::mpsc::channel::<Bytes>(4);
|
||||
|
||||
let work = tokio::spawn(async move {
|
||||
let iv = Duration::from_secs(15 * 60);
|
||||
let mut keepalive_interval = tokio::time::interval_at(tokio::time::Instant::now() + iv, iv);
|
||||
|
||||
loop {
|
||||
let mut wsbuf = [0u8; 8192];
|
||||
|
||||
tokio::select!(
|
||||
_ = keepalive_interval.tick() => {
|
||||
let pos = {
|
||||
let mut cur = WriteCursor::new(&mut wsbuf);
|
||||
KeepalivePkt.encode(&mut cur).map_err(|e| custom_err!("PktEncode", e))?;
|
||||
cur.pos()
|
||||
};
|
||||
|
||||
gw.ws_sink.send(Message::Binary(Bytes::copy_from_slice(&wsbuf[..pos]))).await.map_err(|e| custom_err!("ws send", e))?;
|
||||
},
|
||||
next = gw.ws_stream.next() => {
|
||||
let tmp = next.ok_or_else(|| Error::new("WS Stream Dead", GwErrorKind::Connect))?;
|
||||
let msg = tmp.map_err(|e| custom_err!("Stream", e))?.into_data();
|
||||
let mut cur = ReadCursor::new(&msg);
|
||||
let hdr = PktHdr::decode(&mut cur).map_err(|e| custom_err!("Header Decode", e))?;
|
||||
|
||||
let header_length = usize::try_from(hdr.length).map_err(|_| Error::new("PktHdr too big", GwErrorKind::Decode))?;
|
||||
// Nexus patch: a short or inconsistent packet is an error, not a panic.
|
||||
if header_length.checked_sub(hdr.size()).is_none_or(|body| cur.len() < body) {
|
||||
return Err(Error::new("PktHdr length", GwErrorKind::Decode));
|
||||
}
|
||||
match hdr.ty {
|
||||
PktTy::Keepalive => {
|
||||
continue;
|
||||
},
|
||||
PktTy::Data => {
|
||||
let p = DataPkt::decode(&mut cur).map_err(|e| custom_err!("PktDecode", e))?;
|
||||
in_tx.send(Bytes::from(p.data.to_vec())).await.map_err(|e| custom_err!("in_tx dead", e))?;
|
||||
},
|
||||
x => {
|
||||
warn!("Unhandled gw packet type {x:?}");
|
||||
}
|
||||
}
|
||||
},
|
||||
next = out_rx.recv() => {
|
||||
let next = next.ok_or_else(|| Error::new("WS Sink Dead", GwErrorKind::Connect))?;
|
||||
let pkt = DataPkt { data: &next };
|
||||
|
||||
let pos = {
|
||||
let mut cur = WriteCursor::new(&mut wsbuf);
|
||||
pkt.encode(&mut cur).map_err(|e| custom_err!("PktEncode", e))?;
|
||||
cur.pos()
|
||||
};
|
||||
gw.ws_sink.send(Message::Binary(Bytes::copy_from_slice(&wsbuf[..pos]))).await.map_err(|e| custom_err!("ws send", e))?;
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
Ok(GwClient {
|
||||
work,
|
||||
rx: in_rx,
|
||||
rx_bufs: vec![],
|
||||
tx: PollSender::new(out_tx),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl GwConn {
|
||||
async fn send_packet<E: Encode>(&mut self, payload: &E) -> Result<(), Error> {
|
||||
let mut buf = [0u8; 4096];
|
||||
let pos = {
|
||||
let mut cur = WriteCursor::new(&mut buf);
|
||||
payload
|
||||
.encode(&mut cur)
|
||||
.map_err(|e| Error::new("packet encode", GwErrorKind::Encode).with_source(e))?;
|
||||
cur.pos()
|
||||
};
|
||||
self.ws_sink
|
||||
.send(Message::Binary(Bytes::copy_from_slice(&buf[..pos])))
|
||||
.await
|
||||
.map_err(|e| custom_err!("WebSocket send error", e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn read_packet(&mut self) -> Result<(PktHdr, Bytes), Error> {
|
||||
let mut msg = self
|
||||
.ws_stream
|
||||
.next()
|
||||
.await
|
||||
.ok_or_else(|| Error::new("Stream closed", GwErrorKind::Connect))?
|
||||
.map_err(|e| custom_err!("WS err", e))?
|
||||
.into_data();
|
||||
let mut cur = ReadCursor::new(&msg);
|
||||
|
||||
let hdr = PktHdr::decode(&mut cur).map_err(|_| Error::new("PktHdr", GwErrorKind::Decode))?;
|
||||
|
||||
let header_length =
|
||||
usize::try_from(hdr.length).map_err(|_| Error::new("PktHdr too big", GwErrorKind::Decode))?;
|
||||
if header_length.checked_sub(hdr.size()) != Some(cur.len()) {
|
||||
return Err(Error::new("read_packet", GwErrorKind::PacketEof));
|
||||
}
|
||||
|
||||
Ok((hdr, msg.split_off(cur.pos())))
|
||||
}
|
||||
|
||||
async fn handshake(&mut self) -> Result<(), Error> {
|
||||
// For NTLM we would include extended_auth: NTLM_SSPI in this handshake req here.
|
||||
let hs = HandshakeReqPkt {
|
||||
ver_major: 1,
|
||||
ver_minor: 0,
|
||||
..HandshakeReqPkt::default()
|
||||
};
|
||||
self.send_packet(&hs).await?;
|
||||
let (_hdr, bytes) = self.read_packet().await?;
|
||||
|
||||
let mut cur = ReadCursor::new(&bytes);
|
||||
let resp = HandshakeRespPkt::decode(&mut cur).map_err(|_| Error::new("Handshake", GwErrorKind::Decode))?;
|
||||
if resp.error_code != 0 || resp.ver_major != 1 || resp.ver_minor != 0 || resp.server_version != 0 {
|
||||
return Err(Error::new("Handshake", GwErrorKind::Connect));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn tunnel(&mut self) -> Result<(), Error> {
|
||||
let req = TunnelReqPkt {
|
||||
// Havent seen any server working without this.
|
||||
caps: HttpCapsTy::MessagingConsentSign.as_u32(),
|
||||
fields_present: 0,
|
||||
..TunnelReqPkt::default()
|
||||
};
|
||||
self.send_packet(&req).await?;
|
||||
|
||||
let (_hdr, bytes) = self.read_packet().await?;
|
||||
let mut cur = ReadCursor::new(&bytes);
|
||||
|
||||
let resp = TunnelRespPkt::decode(&mut cur).map_err(|_| Error::new("TunnelDecode", GwErrorKind::Decode))?;
|
||||
if resp.status_code != 0 {
|
||||
return Err(Error::new("Tunnel", GwErrorKind::Connect));
|
||||
}
|
||||
if !cur.eof() {
|
||||
return Err(Error::new("Tunnel", GwErrorKind::Decode));
|
||||
}
|
||||
if !resp.consent_msg.is_empty() {
|
||||
return Err(Error::new(
|
||||
"Received consent message but showing it not implemented",
|
||||
GwErrorKind::UnsupportedFeature,
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn tunnel_auth(&mut self) -> Result<(), Error> {
|
||||
let req = TunnelAuthPkt {
|
||||
fields_present: 0,
|
||||
client_name: self.client_name.clone(),
|
||||
};
|
||||
self.send_packet(&req).await?;
|
||||
|
||||
let (_hdr, bytes) = self.read_packet().await?;
|
||||
let mut cur = ReadCursor::new(&bytes);
|
||||
let resp: TunnelAuthRespPkt =
|
||||
TunnelAuthRespPkt::decode(&mut cur).map_err(|_| Error::new("TunnelAuth", GwErrorKind::Decode))?;
|
||||
|
||||
if resp.error_code() != 0 {
|
||||
return Err(Error::new("TunnelAuth", GwErrorKind::Connect));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn channel(&mut self) -> Result<ChannelResp, Error> {
|
||||
let req = ChannelPkt {
|
||||
resources: vec![self.target.server.clone()],
|
||||
port: self.target.server_port,
|
||||
protocol: 3,
|
||||
};
|
||||
self.send_packet(&req).await?;
|
||||
|
||||
let (hdr, bytes) = self.read_packet().await?;
|
||||
if hdr.ty != PktTy::ChannelResp {
|
||||
return Err(Error::new("ChannelResp", GwErrorKind::Decode));
|
||||
}
|
||||
let mut cur: ReadCursor<'_> = ReadCursor::new(&bytes);
|
||||
let resp: ChannelResp =
|
||||
ChannelResp::decode(&mut cur).map_err(|_| Error::new("ChannelResp", GwErrorKind::Decode))?;
|
||||
if resp.error_code() != 0 {
|
||||
return Err(Error::new("ChannelCreate", GwErrorKind::Connect));
|
||||
}
|
||||
if !cur.eof() {
|
||||
return Err(Error::new("ChannelResp", GwErrorKind::Decode));
|
||||
}
|
||||
Ok(resp)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncRead for GwClient {
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut core::task::Context<'_>,
|
||||
buf: &mut tokio::io::ReadBuf<'_>,
|
||||
) -> Poll<io::Result<()>> {
|
||||
// Propagate error or premature exit (?)
|
||||
match self.work.poll_unpin(cx) {
|
||||
Poll::Ready(Err(e)) => return Poll::Ready(Err(io::Error::other(e))),
|
||||
Poll::Ready(Ok(Err(e))) => return Poll::Ready(Err(io::Error::other(e))),
|
||||
Poll::Ready(_) => return Poll::Ready(Err(io::Error::other("Premature Work Task end?"))),
|
||||
_ => (),
|
||||
}
|
||||
|
||||
// Get new bufs
|
||||
if let Poll::Ready(Some(new_buf)) = self.rx.poll_recv(cx) {
|
||||
self.rx_bufs.push(new_buf);
|
||||
}
|
||||
|
||||
// Read from all queued bufs
|
||||
let mut n = 0;
|
||||
self.rx_bufs.retain_mut(|rx_buf| {
|
||||
let rem = buf.remaining();
|
||||
if rem == 0 {
|
||||
return true;
|
||||
}
|
||||
let max = core::cmp::min(rem, rx_buf.len());
|
||||
buf.put_slice(&rx_buf[..max]);
|
||||
n += max;
|
||||
let _ = rx_buf.split_to(max);
|
||||
|
||||
!rx_buf.is_empty()
|
||||
});
|
||||
|
||||
if n > 0 { Poll::Ready(Ok(())) } else { Poll::Pending }
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncWrite for GwClient {
|
||||
fn poll_write(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut core::task::Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<Result<usize, io::Error>> {
|
||||
// Propagate error or premature exit (?)
|
||||
match self.work.poll_unpin(cx) {
|
||||
Poll::Ready(Err(e)) => return Poll::Ready(Err(io::Error::other(e))),
|
||||
Poll::Ready(Ok(Err(e))) => return Poll::Ready(Err(io::Error::other(e))),
|
||||
Poll::Ready(_) => return Poll::Ready(Err(io::Error::other("Premature Work Task end?"))),
|
||||
Poll::Pending => (),
|
||||
}
|
||||
|
||||
match self.tx.poll_reserve(cx) {
|
||||
Poll::Ready(Ok(())) => {
|
||||
if self.tx.send_item(Bytes::from(buf.to_vec())).is_err() {
|
||||
return Poll::Ready(Err(io::Error::other("Sender closed")));
|
||||
}
|
||||
return Poll::Ready(Ok(buf.len()));
|
||||
}
|
||||
Poll::Ready(Err(err)) => {
|
||||
return Poll::Ready(Err(io::Error::other(err)));
|
||||
}
|
||||
Poll::Pending => (),
|
||||
}
|
||||
|
||||
Poll::Pending
|
||||
}
|
||||
|
||||
fn poll_flush(self: Pin<&mut Self>, _cx: &mut core::task::Context<'_>) -> Poll<Result<(), io::Error>> {
|
||||
// TODO: call flush on the backing sink (e.g. websocket, but atleast for that backend doesnt seem to matter)?
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
|
||||
fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut core::task::Context<'_>) -> Poll<Result<(), io::Error>> {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
/// Creates a [`crate::Error`] with `Custom` kind and a source error attached to it
|
||||
#[macro_export]
|
||||
macro_rules! custom_err {
|
||||
( $context:expr, $source:expr $(,)? ) => {{ <$crate::Error as $crate::GwErrorExt>::custom($context, $source) }};
|
||||
}
|
||||
+601
@@ -0,0 +1,601 @@
|
||||
use bitflags::bitflags;
|
||||
use ironrdp_core::{
|
||||
Decode, Encode, ReadCursor, WriteCursor, cast_int, cast_length, ensure_fixed_part_size, ensure_size,
|
||||
unsupported_value_err,
|
||||
};
|
||||
|
||||
bitflags! {
|
||||
/// 2.2.5.3.2 HTTP_EXTENDED_AUTH Enumeration
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub(crate) struct HttpExtendedAuth: u16 {
|
||||
const HTTP_EXTENDED_AUTH_NONE = 0x01;
|
||||
const HTTP_EXTENDED_AUTH_SC = 0x01;
|
||||
const HTTP_EXTENDED_AUTH_PAA = 0x02;
|
||||
const HTTP_EXTENDED_AUTH_SSPI_NTLM = 0x04;
|
||||
}
|
||||
}
|
||||
|
||||
/// 2.2.5.3.3 HTTP_PACKET_TYPE Enumeration
|
||||
#[repr(u16)]
|
||||
#[derive(Eq, PartialEq, Copy, Clone, Debug, Default)]
|
||||
pub(crate) enum PktTy {
|
||||
#[default]
|
||||
Invalid,
|
||||
HandshakeReq = 0x01,
|
||||
HandshakeResp = 0x02,
|
||||
ExtendedAuth = 0x03,
|
||||
TunnelCreate = 0x04,
|
||||
TunnelResp = 0x05,
|
||||
TunnelAuth = 0x06,
|
||||
TunnelAuthResponse = 0x07,
|
||||
ChannelCreate = 0x08,
|
||||
ChannelResp = 0x09,
|
||||
ChannelClose = 0x10,
|
||||
Data = 0x0A,
|
||||
ServiceMessage = 0x0B,
|
||||
ReauthMessage = 0x0C,
|
||||
Keepalive = 0x0D,
|
||||
}
|
||||
|
||||
impl PktTy {
|
||||
#[expect(
|
||||
clippy::as_conversions,
|
||||
reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive"
|
||||
)]
|
||||
fn as_u16(self) -> u16 {
|
||||
self as u16
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<u16> for PktTy {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(val: u16) -> Result<Self, Self::Error> {
|
||||
let mapped = match val {
|
||||
0x01 => PktTy::HandshakeReq,
|
||||
0x02 => PktTy::HandshakeResp,
|
||||
0x03 => PktTy::ExtendedAuth,
|
||||
0x04 => PktTy::TunnelCreate,
|
||||
0x05 => PktTy::TunnelResp,
|
||||
0x06 => PktTy::TunnelAuth,
|
||||
0x07 => PktTy::TunnelAuthResponse,
|
||||
0x08 => PktTy::ChannelCreate,
|
||||
0x09 => PktTy::ChannelResp,
|
||||
0x0A => PktTy::Data,
|
||||
0x0B => PktTy::ServiceMessage,
|
||||
0x0C => PktTy::ReauthMessage,
|
||||
0x0D => PktTy::Keepalive,
|
||||
0x10 => PktTy::ChannelClose,
|
||||
_ => return Err(()),
|
||||
};
|
||||
Ok(mapped)
|
||||
}
|
||||
}
|
||||
|
||||
/// 2.2.10.9 HTTP_PACKET_HEADER Structure
|
||||
#[derive(Default, Debug)]
|
||||
pub(crate) struct PktHdr {
|
||||
pub ty: PktTy,
|
||||
pub _reserved: u16,
|
||||
pub length: u32,
|
||||
}
|
||||
|
||||
impl PktHdr {
|
||||
const FIXED_PART_SIZE: usize = 4 /* ty */ + 2 /* _reserved */ + 2 /* length */;
|
||||
}
|
||||
|
||||
impl Encode for PktHdr {
|
||||
fn encode(&self, dst: &mut WriteCursor<'_>) -> ironrdp_core::EncodeResult<()> {
|
||||
ensure_size!(in: dst, size: self.size());
|
||||
|
||||
dst.write_u16(self.ty.as_u16());
|
||||
dst.write_u16(self._reserved);
|
||||
dst.write_u32(self.length);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"HTTP_PACKET_HEADER"
|
||||
}
|
||||
|
||||
fn size(&self) -> usize {
|
||||
Self::FIXED_PART_SIZE
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Decode<'a> for PktHdr {
|
||||
fn decode(src: &mut ReadCursor<'a>) -> ironrdp_core::DecodeResult<Self> {
|
||||
ensure_fixed_part_size!(in: src);
|
||||
|
||||
let ty = src.read_u16();
|
||||
let mty = PktTy::try_from(ty).map_err(|_| unsupported_value_err("PktHdr::ty", "ty", format!("0x{ty:x}")))?;
|
||||
|
||||
Ok(PktHdr {
|
||||
ty: mty,
|
||||
_reserved: src.read_u16(),
|
||||
length: src.read_u32(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// 2.2.10.10 HTTP_HANDSHAKE_REQUEST_PACKET Structure
|
||||
#[derive(Default)]
|
||||
pub(crate) struct HandshakeReqPkt {
|
||||
pub ver_major: u8,
|
||||
pub ver_minor: u8,
|
||||
pub client_version: u16,
|
||||
pub extended_auth: HttpExtendedAuth,
|
||||
}
|
||||
|
||||
impl Encode for HandshakeReqPkt {
|
||||
fn encode(&self, dst: &mut WriteCursor<'_>) -> ironrdp_core::EncodeResult<()> {
|
||||
ensure_size!(in: dst, size: self.size());
|
||||
|
||||
let hdr = PktHdr {
|
||||
ty: PktTy::HandshakeReq,
|
||||
length: u32::try_from(self.size()).expect("handshake packet size fits in u32"),
|
||||
..PktHdr::default()
|
||||
};
|
||||
hdr.encode(dst)?;
|
||||
|
||||
dst.write_u8(self.ver_major);
|
||||
dst.write_u8(self.ver_minor);
|
||||
dst.write_u16(self.client_version);
|
||||
dst.write_u16(self.extended_auth.bits());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"HTTP_HANDSHAKE_REQUEST_PACKET"
|
||||
}
|
||||
|
||||
fn size(&self) -> usize {
|
||||
PktHdr::FIXED_PART_SIZE + 6
|
||||
}
|
||||
}
|
||||
|
||||
/// 2.2.10.11 HTTP_HANDSHAKE_RESPONSE_PACKET Structure
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct HandshakeRespPkt {
|
||||
pub error_code: u32,
|
||||
pub ver_major: u8,
|
||||
pub ver_minor: u8,
|
||||
pub server_version: u16,
|
||||
pub _extended_auth: HttpExtendedAuth,
|
||||
}
|
||||
|
||||
impl HandshakeRespPkt {
|
||||
const FIXED_PART_SIZE: usize = 4 /* error_code */ + 1 /* ver_major */ + 1 /* ver_minor */ + 2 /* server_auth */ + 1 /*extended_auth*/;
|
||||
}
|
||||
|
||||
impl Decode<'_> for HandshakeRespPkt {
|
||||
fn decode(src: &mut ReadCursor<'_>) -> ironrdp_core::DecodeResult<Self> {
|
||||
ensure_fixed_part_size!(in: src);
|
||||
|
||||
Ok(HandshakeRespPkt {
|
||||
error_code: src.read_u32(),
|
||||
ver_major: src.read_u8(),
|
||||
ver_minor: src.read_u8(),
|
||||
server_version: src.read_u16(),
|
||||
_extended_auth: {
|
||||
let raw = src.read_u16();
|
||||
HttpExtendedAuth::from_bits(raw)
|
||||
.ok_or_else(|| unsupported_value_err("HandshakeResp", "extended_auth", format!("0x{raw:x}")))?
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// 2.2.10.18 HTTP_TUNNEL_PACKET
|
||||
#[derive(Default)]
|
||||
pub(crate) struct TunnelReqPkt {
|
||||
pub caps: u32,
|
||||
pub fields_present: u16,
|
||||
pub _reserved: u16,
|
||||
}
|
||||
|
||||
impl Encode for TunnelReqPkt {
|
||||
fn encode(&self, dst: &mut WriteCursor<'_>) -> ironrdp_core::EncodeResult<()> {
|
||||
ensure_size!(in: dst, size: self.size());
|
||||
|
||||
let hdr = PktHdr {
|
||||
ty: PktTy::TunnelCreate,
|
||||
length: u32::try_from(self.size()).expect("tunnel request packet size fits in u32"),
|
||||
..PktHdr::default()
|
||||
};
|
||||
hdr.encode(dst)?;
|
||||
|
||||
dst.write_u32(self.caps);
|
||||
dst.write_u16(self.fields_present);
|
||||
dst.write_u16(self._reserved);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"HTTP_TUNNEL_PACKET"
|
||||
}
|
||||
|
||||
fn size(&self) -> usize {
|
||||
PktHdr::default().size() + 8
|
||||
}
|
||||
}
|
||||
|
||||
/// 2.2.5.3.9 HTTP_CAPABILITY_TYPE Enumeration
|
||||
#[repr(u32)]
|
||||
#[expect(dead_code)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub(crate) enum HttpCapsTy {
|
||||
QuarSOH = 1,
|
||||
IdleTimeout = 2,
|
||||
MessagingConsentSign = 4,
|
||||
MessagingServiceMsg = 8,
|
||||
Reauth = 0x10,
|
||||
UdpTransport = 0x20,
|
||||
}
|
||||
|
||||
impl HttpCapsTy {
|
||||
#[expect(
|
||||
clippy::as_conversions,
|
||||
reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive"
|
||||
)]
|
||||
pub(crate) fn as_u32(self) -> u32 {
|
||||
self as u32
|
||||
}
|
||||
}
|
||||
|
||||
/// 2.2.5.3.8 HTTP_TUNNEL_RESPONSE_FIELDS_PRESENT_FLAGS
|
||||
#[repr(u16)]
|
||||
#[derive(Copy, Clone)]
|
||||
enum HttpTunnelResponseFields {
|
||||
TunnelID = 1,
|
||||
Caps = 2,
|
||||
/// nonce & server_cert
|
||||
Soh = 4,
|
||||
Consent = 0x10,
|
||||
}
|
||||
|
||||
impl HttpTunnelResponseFields {
|
||||
#[expect(
|
||||
clippy::as_conversions,
|
||||
reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive"
|
||||
)]
|
||||
fn as_u16(self) -> u16 {
|
||||
self as u16
|
||||
}
|
||||
}
|
||||
|
||||
/// 2.2.10.20 HTTP_TUNNEL_RESPONSE Structure
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct TunnelRespPkt {
|
||||
pub _server_version: u16,
|
||||
pub status_code: u32,
|
||||
pub fields_present: u16,
|
||||
pub _reserved: u16,
|
||||
|
||||
// 2.2.10.21 HTTP_TUNNEL_RESPONSE_OPTIONAL
|
||||
pub tunnel_id: Option<u32>,
|
||||
pub caps_flags: Option<u32>,
|
||||
pub nonce: Option<u16>,
|
||||
pub server_cert: Vec<u8>,
|
||||
pub consent_msg: Vec<u8>,
|
||||
}
|
||||
|
||||
impl TunnelRespPkt {
|
||||
const FIXED_PART_SIZE: usize = 2 /* server_version */ + 4 /* status_code */ + 2 /* fields_present */ + 2 /* reserved */;
|
||||
}
|
||||
|
||||
impl Decode<'_> for TunnelRespPkt {
|
||||
fn decode(src: &mut ReadCursor<'_>) -> ironrdp_core::DecodeResult<Self> {
|
||||
ensure_fixed_part_size!(in: src);
|
||||
|
||||
let mut pkt = TunnelRespPkt {
|
||||
_server_version: src.read_u16(),
|
||||
status_code: src.read_u32(),
|
||||
fields_present: src.read_u16(),
|
||||
_reserved: src.read_u16(),
|
||||
..TunnelRespPkt::default()
|
||||
};
|
||||
|
||||
if pkt.fields_present & (HttpTunnelResponseFields::TunnelID.as_u16()) != 0 {
|
||||
ensure_size!(in: src, size: 4);
|
||||
pkt.tunnel_id = Some(src.read_u32());
|
||||
}
|
||||
if pkt.fields_present & (HttpTunnelResponseFields::Caps.as_u16()) != 0 {
|
||||
ensure_size!(in: src, size: 4);
|
||||
pkt.caps_flags = Some(src.read_u32());
|
||||
}
|
||||
if pkt.fields_present & (HttpTunnelResponseFields::Soh.as_u16()) != 0 {
|
||||
ensure_size!(in: src, size: 2 + 2);
|
||||
pkt.nonce = Some(src.read_u16());
|
||||
let len = usize::from(src.read_u16());
|
||||
ensure_size!(in: src, size: len);
|
||||
pkt.server_cert = src.read_slice(len).to_vec();
|
||||
}
|
||||
if pkt.fields_present & (HttpTunnelResponseFields::Consent.as_u16()) != 0 {
|
||||
ensure_size!(in: src, size: 2);
|
||||
let len = usize::from(src.read_u16());
|
||||
ensure_size!(in: src, size: len);
|
||||
pkt.consent_msg = src.read_slice(len).to_vec();
|
||||
}
|
||||
|
||||
Ok(pkt)
|
||||
}
|
||||
}
|
||||
|
||||
/// 2.2.10.7 HTTP_EXTENDED_AUTH_PACKET Structure
|
||||
#[expect(dead_code, reason = "defined for completeness per spec; not yet used")]
|
||||
pub(crate) struct ExtendedAuthPkt {
|
||||
error_code: u32,
|
||||
blob: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Encode for ExtendedAuthPkt {
|
||||
fn encode(&self, dst: &mut WriteCursor<'_>) -> ironrdp_core::EncodeResult<()> {
|
||||
ensure_size!(in: dst, size: self.size());
|
||||
|
||||
let hdr = PktHdr {
|
||||
ty: PktTy::ExtendedAuth,
|
||||
length: cast_int!("packet length", self.size())?,
|
||||
..PktHdr::default()
|
||||
};
|
||||
hdr.encode(dst)?;
|
||||
|
||||
dst.write_u32(self.error_code);
|
||||
let blob_len: u16 = cast_int!("blob length", self.blob.len())?;
|
||||
dst.write_u16(blob_len);
|
||||
dst.write_slice(&self.blob);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"HTTP_EXTENDED_AUTH_PACKET"
|
||||
}
|
||||
|
||||
fn size(&self) -> usize {
|
||||
PktHdr::default().size() + 6 + self.blob.len()
|
||||
}
|
||||
}
|
||||
|
||||
impl Decode<'_> for ExtendedAuthPkt {
|
||||
fn decode(src: &mut ReadCursor<'_>) -> ironrdp_core::DecodeResult<Self> {
|
||||
ensure_size!(in: src, size: 4 + 2);
|
||||
let error_code = src.read_u32();
|
||||
let len = usize::from(src.read_u16());
|
||||
ensure_size!(in: src, size: len);
|
||||
|
||||
Ok(ExtendedAuthPkt {
|
||||
error_code,
|
||||
blob: src.read_slice(len).to_vec(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// 2.2.10.14 HTTP_TUNNEL_AUTH_PACKET Structure
|
||||
pub(crate) struct TunnelAuthPkt {
|
||||
pub fields_present: u16,
|
||||
pub client_name: String,
|
||||
}
|
||||
|
||||
impl Encode for TunnelAuthPkt {
|
||||
fn encode(&self, dst: &mut WriteCursor<'_>) -> ironrdp_core::EncodeResult<()> {
|
||||
ensure_size!(in: dst, size: self.size());
|
||||
|
||||
let hdr = PktHdr {
|
||||
ty: PktTy::TunnelAuth,
|
||||
length: cast_int!("packet length", self.size())?,
|
||||
..PktHdr::default()
|
||||
};
|
||||
hdr.encode(dst)?;
|
||||
|
||||
dst.write_u16(self.fields_present);
|
||||
|
||||
let client_name_len = self.client_name.encode_utf16().count() * 2 + 2; // Add 2 to account for a null terminator (0x0000).
|
||||
let client_name_len: u16 = cast_int!("client name length", client_name_len)?;
|
||||
dst.write_u16(client_name_len);
|
||||
|
||||
for c in self.client_name.encode_utf16() {
|
||||
dst.write_u16(c);
|
||||
}
|
||||
|
||||
dst.write_u16(0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"HTTP_TUNNEL_AUTH_PACKET"
|
||||
}
|
||||
|
||||
fn size(&self) -> usize {
|
||||
PktHdr::default().size() + 4 + 2 * (self.client_name.len() + 1)
|
||||
}
|
||||
}
|
||||
|
||||
/// 2.2.10.16 HTTP_TUNNEL_AUTH_RESPONSE Structure
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct TunnelAuthRespPkt {
|
||||
error_code: u32,
|
||||
_fields_present: u16,
|
||||
_reserved: u16,
|
||||
}
|
||||
|
||||
impl TunnelAuthRespPkt {
|
||||
const FIXED_PART_SIZE: usize = 4 /* error_code */ + 2 /* fields_present */ + 2 /* _reserved */;
|
||||
|
||||
pub(crate) fn error_code(&self) -> u32 {
|
||||
self.error_code
|
||||
}
|
||||
}
|
||||
|
||||
impl Decode<'_> for TunnelAuthRespPkt {
|
||||
fn decode(src: &mut ReadCursor<'_>) -> ironrdp_core::DecodeResult<Self> {
|
||||
ensure_fixed_part_size!(in: src);
|
||||
|
||||
Ok(TunnelAuthRespPkt {
|
||||
error_code: src.read_u32(),
|
||||
_fields_present: src.read_u16(),
|
||||
_reserved: src.read_u16(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// 2.2.10.2 HTTP_CHANNEL_PACKET
|
||||
pub(crate) struct ChannelPkt {
|
||||
pub resources: Vec<String>,
|
||||
pub port: u16,
|
||||
pub protocol: u16,
|
||||
}
|
||||
|
||||
impl Encode for ChannelPkt {
|
||||
fn encode(&self, dst: &mut WriteCursor<'_>) -> ironrdp_core::EncodeResult<()> {
|
||||
ensure_size!(in: dst, size: self.size());
|
||||
|
||||
let hdr = PktHdr {
|
||||
ty: PktTy::ChannelCreate,
|
||||
length: cast_int!("packet length", self.size())?,
|
||||
..PktHdr::default()
|
||||
};
|
||||
hdr.encode(dst)?;
|
||||
|
||||
let resources_count: u8 = cast_length!("resources count", self.resources.len())?;
|
||||
dst.write_u8(resources_count);
|
||||
dst.write_u8(0); // alt_names
|
||||
dst.write_u16(self.port);
|
||||
dst.write_u16(self.protocol);
|
||||
|
||||
// 2.2.10.3 HTTP_CHANNEL_PACKET_VARIABLE
|
||||
for res in &self.resources {
|
||||
let res_utf16_len = res.encode_utf16().count() * 2 + 2; // Add 2 to account for a null terminator (0x0000).
|
||||
let res_len: u16 = cast_int!("resource name UTF-16 length", res_utf16_len)?;
|
||||
dst.write_u16(res_len);
|
||||
for b in res.encode_utf16() {
|
||||
dst.write_u16(b);
|
||||
}
|
||||
dst.write_u16(0);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"HTTP_CHANNEL_PACKET"
|
||||
}
|
||||
|
||||
fn size(&self) -> usize {
|
||||
PktHdr::default().size() + 6 + self.resources.iter().map(|x| 2 + 2 * (x.len() + 1)).sum::<usize>()
|
||||
}
|
||||
}
|
||||
|
||||
/// 2.2.10.4 HTTP_CHANNEL_RESPONSE
|
||||
#[derive(Default, Debug)]
|
||||
pub(crate) struct ChannelResp {
|
||||
error_code: u32,
|
||||
fields_present: u16,
|
||||
_reserved: u16,
|
||||
|
||||
/// 2.2.10.5 HTTP_CHANNEL_RESPONSE_OPTIONAL
|
||||
chan_id: Option<u32>,
|
||||
udp_port: u16,
|
||||
authn_cookie: Vec<u8>,
|
||||
}
|
||||
|
||||
impl ChannelResp {
|
||||
const FIXED_PART_SIZE: usize = 4 /* error_code */ + 2 /* fields_present */ + 2 /* _reserved */;
|
||||
|
||||
pub(crate) fn error_code(&self) -> u32 {
|
||||
self.error_code
|
||||
}
|
||||
}
|
||||
|
||||
impl Decode<'_> for ChannelResp {
|
||||
fn decode(src: &mut ReadCursor<'_>) -> ironrdp_core::DecodeResult<Self> {
|
||||
ensure_fixed_part_size!(in: src);
|
||||
|
||||
let mut resp = ChannelResp {
|
||||
error_code: src.read_u32(),
|
||||
fields_present: src.read_u16(),
|
||||
_reserved: src.read_u16(),
|
||||
..ChannelResp::default()
|
||||
};
|
||||
if resp.fields_present & 1 != 0 {
|
||||
ensure_size!(in: src, size: 4);
|
||||
resp.chan_id = Some(src.read_u32());
|
||||
}
|
||||
if resp.fields_present & 2 != 0 {
|
||||
ensure_size!(in: src, size: 2);
|
||||
resp.udp_port = src.read_u16();
|
||||
}
|
||||
if resp.fields_present & 4 != 0 {
|
||||
ensure_size!(in: src, size: 2);
|
||||
let len = usize::from(src.read_u16());
|
||||
ensure_size!(in: src, size: len);
|
||||
resp.authn_cookie = src.read_slice(len).to_vec();
|
||||
}
|
||||
Ok(resp)
|
||||
}
|
||||
}
|
||||
|
||||
/// 2.2.10.6 HTTP_DATA_PACKET
|
||||
pub(crate) struct DataPkt<'a> {
|
||||
pub data: &'a [u8],
|
||||
}
|
||||
|
||||
impl Encode for DataPkt<'_> {
|
||||
fn encode(&self, dst: &mut WriteCursor<'_>) -> ironrdp_core::EncodeResult<()> {
|
||||
ensure_size!(in: dst, size: self.size());
|
||||
|
||||
let hdr = PktHdr {
|
||||
ty: PktTy::Data,
|
||||
length: cast_int!("packet length", self.size())?,
|
||||
..PktHdr::default()
|
||||
};
|
||||
hdr.encode(dst)?;
|
||||
let data_len: u16 = cast_int!("data payload length", self.data.len())?;
|
||||
dst.write_u16(data_len);
|
||||
dst.write_slice(self.data);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"HTTP_DATA_PACKET"
|
||||
}
|
||||
|
||||
fn size(&self) -> usize {
|
||||
PktHdr::default().size() + 2 + self.data.len()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Decode<'a> for DataPkt<'a> {
|
||||
fn decode(src: &mut ReadCursor<'a>) -> ironrdp_core::DecodeResult<Self> {
|
||||
ensure_size!(in: src, size: 2);
|
||||
let len = usize::from(src.read_u16());
|
||||
ensure_size!(in: src, size: len);
|
||||
Ok(DataPkt {
|
||||
data: src.read_slice(len),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct KeepalivePkt;
|
||||
|
||||
impl Encode for KeepalivePkt {
|
||||
fn encode(&self, dst: &mut WriteCursor<'_>) -> ironrdp_core::EncodeResult<()> {
|
||||
let hdr = PktHdr {
|
||||
ty: PktTy::Keepalive,
|
||||
length: u32::try_from(self.size()).expect("keepalive packet size fits in u32"),
|
||||
..PktHdr::default()
|
||||
};
|
||||
hdr.encode(dst)
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"KEEPALIVE"
|
||||
}
|
||||
|
||||
fn size(&self) -> usize {
|
||||
PktHdr::default().size()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user