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.
154 lines
5.7 KiB
Rust
154 lines
5.7 KiB
Rust
//! Protocol clients. Conventions, shared by every client so the session manager treats them alike:
|
|
//! - `connect()` blocks the calling thread (the manager calls it from a worker thread, as Go
|
|
//! called it from a goroutine) and returns once the connection is usable.
|
|
//! - After connecting, the client owns its own reader thread(s) and reports through callbacks:
|
|
//! data, a fatal error, and close (exactly once, whether remote or local).
|
|
//! - `write`/`resize`/`close` are callable from any thread; `close` is idempotent and must not
|
|
//! fire `on_close` re-entrantly while holding locks.
|
|
|
|
pub mod github;
|
|
pub mod irc;
|
|
pub mod local;
|
|
pub mod sftp;
|
|
pub mod ssh;
|
|
|
|
use std::sync::{Arc, OnceLock};
|
|
|
|
/// The shared tokio runtime for async protocol stacks (russh). Blocking clients use threads.
|
|
pub fn rt() -> &'static tokio::runtime::Runtime {
|
|
static RT: OnceLock<tokio::runtime::Runtime> = OnceLock::new();
|
|
RT.get_or_init(|| tokio::runtime::Builder::new_multi_thread().worker_threads(2).enable_all().thread_name("nexus-net").build().expect("tokio runtime"))
|
|
}
|
|
|
|
pub type DataFn = Arc<dyn Fn(&[u8]) + Send + Sync>;
|
|
pub type ErrorFn = Arc<dyn Fn(String) + Send + Sync>;
|
|
pub type CloseFn = Arc<dyn Fn() + Send + Sync>;
|
|
|
|
#[derive(Clone)]
|
|
pub struct Callbacks {
|
|
pub on_data: DataFn,
|
|
pub on_error: ErrorFn,
|
|
pub on_close: CloseFn,
|
|
}
|
|
|
|
impl Callbacks {
|
|
pub fn noop() -> Callbacks {
|
|
Callbacks { on_data: Arc::new(|_| {}), on_error: Arc::new(|_| {}), on_close: Arc::new(|| {}) }
|
|
}
|
|
}
|
|
|
|
pub mod proxy;
|
|
pub mod raw;
|
|
pub mod rdp;
|
|
pub mod serial;
|
|
pub mod telnet;
|
|
pub mod vnc;
|
|
|
|
/// Called with protocol negotiation debug messages (Go's `onDebug`).
|
|
pub type DebugFn = Arc<dyn Fn(String) + Send + Sync>;
|
|
|
|
/// TCP dial shared by the Telnet and RAW clients: Go's `net.DialTimeout` with a 30 s default.
|
|
/// Returns the stream and the `host:port` string used in error messages.
|
|
pub(crate) fn dial(host: &str, port: i64, timeout_secs: i64) -> (String, std::io::Result<std::net::TcpStream>) {
|
|
use std::net::{TcpStream, ToSocketAddrs};
|
|
let timeout = std::time::Duration::from_secs(if timeout_secs > 0 { timeout_secs as u64 } else { 30 });
|
|
// Go's net.JoinHostPort brackets IPv6 literals.
|
|
let addr = if host.contains(':') { format!("[{host}]:{port}") } else { format!("{host}:{port}") };
|
|
let result = (|| {
|
|
let mut last = std::io::Error::new(std::io::ErrorKind::NotFound, "no addresses found");
|
|
for sa in addr.to_socket_addrs()? {
|
|
match TcpStream::connect_timeout(&sa, timeout) {
|
|
Ok(s) => return Ok(s),
|
|
Err(e) => last = e,
|
|
}
|
|
}
|
|
Err(last)
|
|
})();
|
|
(addr, result)
|
|
}
|
|
|
|
/// The TCP connection lifecycle shared by the Telnet and RAW clients: one writer-side stream behind
|
|
/// a lock, a reader thread that feeds `on_read`, and Go's error/close semantics (a read error or EOF
|
|
/// reports `read error: …` then closes; `close` is idempotent and fires `on_close` once).
|
|
pub(crate) struct TcpConn {
|
|
stream: parking_lot::Mutex<Option<std::net::TcpStream>>,
|
|
on_error: ErrorFn,
|
|
on_close: CloseFn,
|
|
}
|
|
|
|
impl TcpConn {
|
|
pub(crate) fn new(on_error: ErrorFn, on_close: CloseFn) -> TcpConn {
|
|
TcpConn { stream: parking_lot::Mutex::new(None), on_error, on_close }
|
|
}
|
|
|
|
pub(crate) fn connect(self: &Arc<Self>, stream: std::net::TcpStream, mut on_read: impl FnMut(&[u8]) + Send + 'static) -> crate::Result<()> {
|
|
let mut reader = stream.try_clone()?;
|
|
{
|
|
let mut guard = self.stream.lock();
|
|
if guard.is_some() {
|
|
return Err("already connected".into());
|
|
}
|
|
*guard = Some(stream);
|
|
}
|
|
let me = self.clone();
|
|
std::thread::spawn(move || {
|
|
use std::io::Read;
|
|
let mut buf = [0u8; 4096];
|
|
loop {
|
|
let err = match reader.read(&mut buf) {
|
|
Ok(0) => "EOF".to_string(),
|
|
Ok(n) => {
|
|
on_read(&buf[..n]);
|
|
continue;
|
|
}
|
|
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
|
|
Err(e) => e.to_string(),
|
|
};
|
|
// A local close also ends the read; Go reported that as an error too, we don't.
|
|
if me.is_connected() {
|
|
(me.on_error)(format!("read error: {err}"));
|
|
me.close();
|
|
}
|
|
return;
|
|
}
|
|
});
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn write(&self, data: &[u8]) -> crate::Result<()> {
|
|
use std::io::Write;
|
|
let guard = self.stream.lock();
|
|
let mut s = guard.as_ref().ok_or("not connected")?;
|
|
s.write_all(data)?;
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn is_connected(&self) -> bool {
|
|
self.stream.lock().is_some()
|
|
}
|
|
|
|
pub(crate) fn close(&self) {
|
|
let taken = self.stream.lock().take();
|
|
if let Some(s) = taken {
|
|
let _ = s.shutdown(std::net::Shutdown::Both);
|
|
(self.on_close)();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// True for the encoding names Go mapped to ISO 8859-1.
|
|
pub(crate) fn is_latin1(encoding: &str) -> bool {
|
|
matches!(encoding.trim().to_ascii_lowercase().as_str(), "iso-8859-1" | "latin1" | "latin-1")
|
|
}
|
|
|
|
/// UTF-8 → ISO 8859-1. Like Go's charmap encoder, input that isn't valid UTF-8 or holds a
|
|
/// character above U+00FF is returned unchanged.
|
|
pub(crate) fn utf8_to_latin1(data: &[u8]) -> Vec<u8> {
|
|
std::str::from_utf8(data).ok().and_then(|s| s.chars().map(|c| u8::try_from(c).ok()).collect()).unwrap_or_else(|| data.to_vec())
|
|
}
|
|
|
|
/// ISO 8859-1 → UTF-8 (every byte maps to the code point of the same value).
|
|
pub(crate) fn latin1_to_utf8(data: &[u8]) -> String {
|
|
data.iter().map(|&b| b as char).collect()
|
|
}
|