use std::io::{ErrorKind, Read, Write};
use std::net::{Shutdown, TcpStream};
use std::path::PathBuf;
use std::sync::Arc;
use std::thread;
#[cfg(feature = "server")]
use std::net::{SocketAddr, TcpListener};
#[cfg(feature = "server")]
use std::sync::atomic::{AtomicBool, Ordering};
#[cfg(feature = "server")]
use std::thread::JoinHandle;
#[cfg(feature = "server")]
use std::time::Duration;
#[cfg(feature = "server")]
use crate::error::{Error, Result};
#[cfg(feature = "server")]
use crate::server::{X11ForwardContext, X11ForwardHandle, X11ForwardHandler};
use crate::stream::{ChannelEgress, ChannelStream};
#[cfg(feature = "server")]
const ACCEPT_POLL_INTERVAL: Duration = Duration::from_millis(100);
const X_BASE_PORT: u16 = 6000;
#[cfg(feature = "server")]
const MIT_MAGIC_COOKIE_1: &str = "MIT-MAGIC-COOKIE-1";
#[cfg(feature = "server")]
const MAX_X11_SETUP_PRELUDE: usize = 1 << 16;
#[cfg(feature = "server")]
enum CookiePolicy {
PermitUnauthenticated,
RequireCookie(Arc<Vec<u8>>),
}
#[cfg(feature = "server")]
const DEFAULT_MIN_DISPLAY: u16 = 10;
#[cfg(feature = "server")]
const DEFAULT_MAX_DISPLAY: u16 = 999;
#[cfg(feature = "server")]
struct X11Binding {
stop: Arc<AtomicBool>,
handle: Option<JoinHandle<()>>,
}
#[cfg(feature = "server")]
impl Drop for X11Binding {
fn drop(&mut self) {
self.stop.store(true, Ordering::SeqCst);
if let Some(h) = self.handle.take() {
let _ = h.join();
}
}
}
#[cfg(feature = "server")]
pub struct DefaultX11ForwardHandler {
min_display: u16,
max_display: u16,
permit_unauthenticated: bool,
}
#[cfg(feature = "server")]
impl Default for DefaultX11ForwardHandler {
fn default() -> Self {
Self::new()
}
}
#[cfg(feature = "server")]
impl DefaultX11ForwardHandler {
pub fn new() -> Self {
Self {
min_display: DEFAULT_MIN_DISPLAY,
max_display: DEFAULT_MAX_DISPLAY,
permit_unauthenticated: false,
}
}
pub fn with_display_range(min_display: u16, max_display: u16) -> Self {
Self {
min_display,
max_display,
permit_unauthenticated: false,
}
}
pub fn permit_unauthenticated(mut self) -> Self {
self.permit_unauthenticated = true;
self
}
fn bind_first_free(&self) -> Result<(TcpListener, u16)> {
for n in self.min_display..=self.max_display {
let port = X_BASE_PORT.saturating_add(n);
let addr: SocketAddr = ([127u8, 0, 0, 1], port).into();
if let Ok(listener) = TcpListener::bind(addr) {
return Ok((listener, n));
}
}
Err(Error::Io(std::io::Error::new(
ErrorKind::AddrInUse,
"x11-forward: no free display number in configured range",
)))
}
}
#[cfg(feature = "server")]
impl X11ForwardHandler for DefaultX11ForwardHandler {
fn setup(
&self,
_user: &str,
single_connection: bool,
auth_protocol: &str,
auth_cookie: &str,
screen: u32,
ctx: X11ForwardContext,
) -> Result<X11ForwardHandle> {
let policy = if self.permit_unauthenticated {
CookiePolicy::PermitUnauthenticated
} else {
if !auth_protocol.eq_ignore_ascii_case(MIT_MAGIC_COOKIE_1) {
return Err(Error::Io(std::io::Error::new(
ErrorKind::InvalidInput,
"x11-forward: unsupported authorization protocol (only MIT-MAGIC-COOKIE-1)",
)));
}
let Some(cookie) = hex_decode(auth_cookie) else {
return Err(Error::Io(std::io::Error::new(
ErrorKind::InvalidInput,
"x11-forward: x11-req cookie is not valid hex",
)));
};
if cookie.is_empty() {
return Err(Error::Io(std::io::Error::new(
ErrorKind::InvalidInput,
"x11-forward: x11-req carried an empty cookie",
)));
}
CookiePolicy::RequireCookie(Arc::new(cookie))
};
let (listener, display_number) = self.bind_first_free()?;
listener.set_nonblocking(true)?;
let stop = Arc::new(AtomicBool::new(false));
let stop_thread = Arc::clone(&stop);
let handle = thread::spawn(move || {
while !stop_thread.load(Ordering::SeqCst) {
match listener.accept() {
Ok((mut conn, peer)) => {
let prelude = match &policy {
CookiePolicy::PermitUnauthenticated => Vec::new(),
CookiePolicy::RequireCookie(cookie) => {
match read_and_check_cookie(&mut conn, cookie) {
Ok(bytes) => bytes,
Err(_) => {
let _ = conn.shutdown(Shutdown::Both);
continue;
}
}
}
};
let orig_host = match peer.ip() {
std::net::IpAddr::V4(v4) => v4.to_string(),
std::net::IpAddr::V6(v6) => v6.to_string(),
};
let orig_port = peer.port() as u32;
match ctx.open_x11(orig_host, orig_port) {
Ok(channel_stream) => {
spawn_tcp_splice_with_prelude(conn, channel_stream, prelude);
}
Err(_) => {
let _ = conn.shutdown(Shutdown::Both);
}
}
if single_connection {
break;
}
}
Err(e) if e.kind() == ErrorKind::WouldBlock => {
thread::sleep(ACCEPT_POLL_INTERVAL);
}
Err(_) => break,
}
}
});
let display_env = format!("localhost:{display_number}.{screen}");
let binding = X11Binding {
stop,
handle: Some(handle),
};
Ok(X11ForwardHandle {
display_env,
display_number,
stopper: Box::new(binding),
})
}
}
fn spawn_tcp_splice(tcp: TcpStream, stream: ChannelStream) {
let (chan_rx, chan_tx) = stream.into_raw();
let Ok(tcp_in) = tcp.try_clone() else {
let _ = chan_tx.send(ChannelEgress::Eof);
let _ = chan_tx.send(ChannelEgress::Close);
return;
};
let tcp_out = tcp;
let chan_tx_a = chan_tx.clone();
let mut tcp_in_a = tcp_in;
let a = thread::spawn(move || {
let mut buf = [0u8; 32 * 1024];
loop {
match tcp_in_a.read(&mut buf) {
Ok(0) => break,
Ok(n) => {
if chan_tx_a
.send(ChannelEgress::Data(buf[..n].to_vec()))
.is_err()
{
break;
}
}
Err(e) if e.kind() == ErrorKind::Interrupted => continue,
Err(_) => break,
}
}
let _ = chan_tx_a.send(ChannelEgress::Eof);
});
let mut tcp_out_b = tcp_out;
let b = thread::spawn(move || {
while let Ok(Some(chunk)) = chan_rx.recv() {
if tcp_out_b.write_all(&chunk).is_err() {
break;
}
}
let _ = tcp_out_b.shutdown(Shutdown::Read);
});
thread::spawn(move || {
let _ = a.join();
let _ = b.join();
let _ = chan_tx.send(ChannelEgress::Close);
});
}
#[cfg(feature = "server")]
fn spawn_tcp_splice_with_prelude(tcp: TcpStream, stream: ChannelStream, prelude: Vec<u8>) {
let (chan_rx, chan_tx) = stream.into_raw();
if !prelude.is_empty() && chan_tx.send(ChannelEgress::Data(prelude)).is_err() {
let _ = tcp.shutdown(Shutdown::Both);
return;
}
let Ok(tcp_in) = tcp.try_clone() else {
let _ = chan_tx.send(ChannelEgress::Eof);
let _ = chan_tx.send(ChannelEgress::Close);
return;
};
let tcp_out = tcp;
let chan_tx_a = chan_tx.clone();
let mut tcp_in_a = tcp_in;
let a = thread::spawn(move || {
let mut buf = [0u8; 32 * 1024];
loop {
match tcp_in_a.read(&mut buf) {
Ok(0) => break,
Ok(n) => {
if chan_tx_a
.send(ChannelEgress::Data(buf[..n].to_vec()))
.is_err()
{
break;
}
}
Err(e) if e.kind() == ErrorKind::Interrupted => continue,
Err(_) => break,
}
}
let _ = chan_tx_a.send(ChannelEgress::Eof);
});
let mut tcp_out_b = tcp_out;
let b = thread::spawn(move || {
while let Ok(Some(chunk)) = chan_rx.recv() {
if tcp_out_b.write_all(&chunk).is_err() {
break;
}
}
let _ = tcp_out_b.shutdown(Shutdown::Read);
});
thread::spawn(move || {
let _ = a.join();
let _ = b.join();
let _ = chan_tx.send(ChannelEgress::Close);
});
}
#[cfg(feature = "server")]
fn read_and_check_cookie(conn: &mut TcpStream, expected: &[u8]) -> std::io::Result<Vec<u8>> {
use std::io::{Error as IoError, Read};
let prev_timeout = conn.read_timeout().ok().flatten();
conn.set_read_timeout(Some(Duration::from_secs(10)))?;
let result = (|| {
let mut buf = Vec::with_capacity(64);
let mut read_until = |buf: &mut Vec<u8>, need: usize| -> std::io::Result<()> {
if need > MAX_X11_SETUP_PRELUDE {
return Err(IoError::new(
ErrorKind::InvalidData,
"x11-forward: setup packet exceeds maximum size",
));
}
let mut chunk = [0u8; 4096];
while buf.len() < need {
let want = (need - buf.len()).min(chunk.len());
let n = conn.read(&mut chunk[..want])?;
if n == 0 {
return Err(IoError::new(
ErrorKind::UnexpectedEof,
"x11-forward: connection closed during setup packet",
));
}
buf.extend_from_slice(&chunk[..n]);
}
Ok(())
};
read_until(&mut buf, 12)?;
let big_endian = match buf[0] {
0x42 => true, 0x6c => false, _ => {
return Err(IoError::new(
ErrorKind::InvalidData,
"x11-forward: bad byte-order byte in setup packet",
));
}
};
let rd16 = |hi: u8, lo: u8| -> usize {
if big_endian {
((hi as usize) << 8) | lo as usize
} else {
((lo as usize) << 8) | hi as usize
}
};
let name_len = rd16(buf[6], buf[7]);
let data_len = rd16(buf[8], buf[9]);
let pad = |x: usize| -> usize { (x + 3) & !3 };
let name_off = 12;
let data_off = name_off + pad(name_len);
let total = data_off + pad(data_len);
read_until(&mut buf, total)?;
let name = &buf[name_off..name_off + name_len];
let data = &buf[data_off..data_off + data_len];
if name != MIT_MAGIC_COOKIE_1.as_bytes() {
return Err(IoError::new(
ErrorKind::PermissionDenied,
"x11-forward: unsupported authorization protocol on connection",
));
}
if !constant_time_eq(data, expected) {
return Err(IoError::new(
ErrorKind::PermissionDenied,
"x11-forward: authorization cookie mismatch",
));
}
Ok(buf)
})();
let _ = conn.set_read_timeout(prev_timeout);
result
}
#[cfg(feature = "server")]
fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
let mut diff = 0u8;
for (x, y) in a.iter().zip(b.iter()) {
diff |= x ^ y;
}
diff == 0
}
#[cfg(feature = "server")]
fn hex_decode(s: &str) -> Option<Vec<u8>> {
let bytes = s.as_bytes();
if !bytes.len().is_multiple_of(2) {
return None;
}
let nib = |c: u8| -> Option<u8> {
match c {
b'0'..=b'9' => Some(c - b'0'),
b'a'..=b'f' => Some(c - b'a' + 10),
b'A'..=b'F' => Some(c - b'A' + 10),
_ => None,
}
};
let mut out = Vec::with_capacity(bytes.len() / 2);
for pair in bytes.chunks_exact(2) {
out.push((nib(pair[0])? << 4) | nib(pair[1])?);
}
Some(out)
}
pub fn splice_to_unix_display_callback(
path: PathBuf,
) -> Option<Arc<dyn Fn(ChannelStream) + Send + Sync + 'static>> {
if !path.exists() {
return None;
}
Some(Arc::new(
move |stream: ChannelStream| match std::os::unix::net::UnixStream::connect(&path) {
Ok(uds) => spawn_unix_splice(uds, stream),
Err(_) => {
let (_rx, tx) = stream.into_raw();
let _ = tx.send(ChannelEgress::Eof);
let _ = tx.send(ChannelEgress::Close);
}
},
))
}
pub fn splice_to_tcp_display_callback(
host: String,
port: u16,
) -> Arc<dyn Fn(ChannelStream) + Send + Sync + 'static> {
Arc::new(
move |stream: ChannelStream| match TcpStream::connect((host.as_str(), port)) {
Ok(tcp) => spawn_tcp_splice(tcp, stream),
Err(_) => {
let (_rx, tx) = stream.into_raw();
let _ = tx.send(ChannelEgress::Eof);
let _ = tx.send(ChannelEgress::Close);
}
},
)
}
pub fn splice_to_local_display_callback()
-> Option<Arc<dyn Fn(ChannelStream) + Send + Sync + 'static>> {
let raw = std::env::var("DISPLAY").ok()?;
if raw.is_empty() {
return None;
}
let (host_part, display_part) = raw.rsplit_once(':')?;
let n_str = display_part.split('.').next()?;
let n: u16 = n_str.parse().ok()?;
if host_part.is_empty() || host_part == "unix" {
let path = PathBuf::from(format!("/tmp/.X11-unix/X{n}"));
return splice_to_unix_display_callback(path);
}
let port = X_BASE_PORT.saturating_add(n);
Some(splice_to_tcp_display_callback(host_part.to_string(), port))
}
fn spawn_unix_splice(uds: std::os::unix::net::UnixStream, stream: ChannelStream) {
use std::os::unix::net::UnixStream;
let (chan_rx, chan_tx) = stream.into_raw();
let Ok(uds_in) = uds.try_clone() else {
let _ = chan_tx.send(ChannelEgress::Eof);
let _ = chan_tx.send(ChannelEgress::Close);
return;
};
let uds_out = uds;
let chan_tx_a = chan_tx.clone();
let mut uds_in_a: UnixStream = uds_in;
let a = thread::spawn(move || {
let mut buf = [0u8; 32 * 1024];
loop {
match uds_in_a.read(&mut buf) {
Ok(0) => break,
Ok(n) => {
if chan_tx_a
.send(ChannelEgress::Data(buf[..n].to_vec()))
.is_err()
{
break;
}
}
Err(e) if e.kind() == ErrorKind::Interrupted => continue,
Err(_) => break,
}
}
let _ = chan_tx_a.send(ChannelEgress::Eof);
});
let mut uds_out_b: UnixStream = uds_out;
let b = thread::spawn(move || {
while let Ok(Some(chunk)) = chan_rx.recv() {
if uds_out_b.write_all(&chunk).is_err() {
break;
}
}
let _ = uds_out_b.shutdown(Shutdown::Read);
});
thread::spawn(move || {
let _ = a.join();
let _ = b.join();
let _ = chan_tx.send(ChannelEgress::Close);
});
}
#[cfg(all(test, feature = "server"))]
mod tests {
use super::*;
#[test]
fn setup_binds_a_display_port() {
let h = DefaultX11ForwardHandler::with_display_range(900, 920);
let ctx = X11ForwardContext::for_test_no_opens();
let handle = h
.setup("u", false, "MIT-MAGIC-COOKIE-1", "deadbeef", 0, ctx)
.expect("setup");
let n = handle.display_number;
assert!((900..=920).contains(&n), "n out of range: {n}");
assert_eq!(handle.display_env, format!("localhost:{n}.0"));
let addr: SocketAddr = ([127u8, 0, 0, 1], 6000 + n).into();
assert!(
TcpListener::bind(addr).is_err(),
"port should be in use while the handle is alive"
);
drop(handle);
for _ in 0..50 {
if TcpListener::bind(addr).is_ok() {
break;
}
thread::sleep(Duration::from_millis(50));
}
assert!(
TcpListener::bind(addr).is_ok(),
"port should be free after the handle is dropped"
);
}
#[test]
fn accepted_connection_is_closed_when_open_fails() {
let h = DefaultX11ForwardHandler::with_display_range(800, 820);
let ctx = X11ForwardContext::for_test_no_opens();
let handle = h
.setup("u", false, "MIT-MAGIC-COOKIE-1", "deadbeef", 0, ctx)
.expect("setup");
let addr: SocketAddr = ([127u8, 0, 0, 1], 6000 + handle.display_number).into();
let mut peer = TcpStream::connect_timeout(&addr, Duration::from_secs(2)).expect("connect");
peer.set_read_timeout(Some(Duration::from_secs(2)))
.expect("read timeout");
let pkt = x11_setup_packet(MIT_MAGIC_COOKIE_1.as_bytes(), &[0xde, 0xad, 0xbe, 0xef]);
peer.write_all(&pkt).expect("write setup packet");
let mut buf = [0u8; 1];
let _ = peer.read(&mut buf);
}
#[test]
fn tcp_display_callback_constructs() {
let _cb = splice_to_tcp_display_callback("127.0.0.1".to_string(), 65000);
}
#[test]
fn hex_decode_roundtrips_and_rejects_garbage() {
assert_eq!(hex_decode("deadbeef"), Some(vec![0xde, 0xad, 0xbe, 0xef]));
assert_eq!(hex_decode(""), Some(vec![]));
assert_eq!(hex_decode("DEADbeef"), Some(vec![0xde, 0xad, 0xbe, 0xef]));
assert_eq!(hex_decode("abc"), None, "odd length must fail");
assert_eq!(hex_decode("zz"), None, "non-hex must fail");
}
#[test]
fn constant_time_eq_basic() {
assert!(constant_time_eq(b"abc", b"abc"));
assert!(!constant_time_eq(b"abc", b"abd"));
assert!(!constant_time_eq(b"abc", b"ab"));
assert!(constant_time_eq(b"", b""));
}
fn x11_setup_packet(name: &[u8], cookie: &[u8]) -> Vec<u8> {
let pad = |x: usize| (x + 3) & !3;
let mut p = Vec::new();
p.push(0x42); p.push(0); p.extend_from_slice(&11u16.to_be_bytes()); p.extend_from_slice(&0u16.to_be_bytes()); p.extend_from_slice(&(name.len() as u16).to_be_bytes());
p.extend_from_slice(&(cookie.len() as u16).to_be_bytes());
p.extend_from_slice(&0u16.to_be_bytes()); p.extend_from_slice(name);
p.resize(12 + pad(name.len()), 0);
p.extend_from_slice(cookie);
let want = 12 + pad(name.len()) + pad(cookie.len());
p.resize(want, 0);
p
}
fn check_over_loopback(packet: &[u8], expected: &[u8]) -> std::io::Result<Vec<u8>> {
let listener = TcpListener::bind(("127.0.0.1", 0)).unwrap();
let addr = listener.local_addr().unwrap();
let pkt = packet.to_vec();
let writer = thread::spawn(move || {
let mut c = TcpStream::connect(addr).unwrap();
let _ = c.write_all(&pkt);
thread::sleep(Duration::from_millis(50));
});
let (mut server_side, _peer) = listener.accept().unwrap();
let res = read_and_check_cookie(&mut server_side, expected);
let _ = writer.join();
res
}
#[test]
fn cookie_check_accepts_matching_packet() {
let cookie = vec![0xde, 0xad, 0xbe, 0xef, 0x01, 0x02];
let pkt = x11_setup_packet(MIT_MAGIC_COOKIE_1.as_bytes(), &cookie);
let consumed = check_over_loopback(&pkt, &cookie).expect("should accept");
assert_eq!(consumed, pkt);
}
#[test]
fn cookie_check_rejects_wrong_cookie() {
let cookie = vec![0xde, 0xad, 0xbe, 0xef];
let wrong = vec![0x00, 0x00, 0x00, 0x00];
let pkt = x11_setup_packet(MIT_MAGIC_COOKIE_1.as_bytes(), &cookie);
let err = check_over_loopback(&pkt, &wrong).expect_err("should reject");
assert_eq!(err.kind(), ErrorKind::PermissionDenied);
}
#[test]
fn cookie_check_rejects_wrong_protocol() {
let cookie = vec![0xaa, 0xbb];
let pkt = x11_setup_packet(b"XDM-AUTHORIZATION-1", &cookie);
let err = check_over_loopback(&pkt, &cookie).expect_err("should reject");
assert_eq!(err.kind(), ErrorKind::PermissionDenied);
}
#[test]
fn bad_cookie_connection_is_dropped_then_loop_continues() {
let h = DefaultX11ForwardHandler::with_display_range(640, 660);
let ctx = X11ForwardContext::for_test_no_opens();
let handle = h
.setup("u", false, "MIT-MAGIC-COOKIE-1", "deadbeef", 0, ctx)
.expect("setup");
let addr: SocketAddr = ([127u8, 0, 0, 1], 6000 + handle.display_number).into();
let mut bad = TcpStream::connect_timeout(&addr, Duration::from_secs(2)).expect("connect");
bad.set_read_timeout(Some(Duration::from_secs(3)))
.expect("read timeout");
let pkt = x11_setup_packet(MIT_MAGIC_COOKIE_1.as_bytes(), &[0x00, 0x11, 0x22]);
bad.write_all(&pkt).expect("write bad packet");
let mut buf = [0u8; 1];
let _ = bad.read(&mut buf); drop(bad);
let still_bound = TcpListener::bind(addr).is_err();
assert!(
still_bound,
"non-single_connection: port should remain bound after a rejected connection"
);
drop(handle);
}
#[test]
fn permit_unauthenticated_skips_cookie_check() {
let h = DefaultX11ForwardHandler::with_display_range(620, 639).permit_unauthenticated();
let ctx = X11ForwardContext::for_test_no_opens();
let handle = h
.setup("u", false, "MIT-MAGIC-COOKIE-1", "deadbeef", 0, ctx)
.expect("setup");
let addr: SocketAddr = ([127u8, 0, 0, 1], 6000 + handle.display_number).into();
let mut peer = TcpStream::connect_timeout(&addr, Duration::from_secs(2)).expect("connect");
peer.set_read_timeout(Some(Duration::from_secs(2)))
.expect("read timeout");
let mut buf = [0u8; 1];
let _ = peer.read(&mut buf);
drop(handle);
}
#[test]
fn setup_rejects_unsupported_protocol() {
let h = DefaultX11ForwardHandler::with_display_range(600, 619);
let ctx = X11ForwardContext::for_test_no_opens();
match h.setup("u", false, "XDM-AUTHORIZATION-1", "deadbeef", 0, ctx) {
Err(Error::Io(e)) => assert_eq!(e.kind(), ErrorKind::InvalidInput),
Err(other) => panic!("unexpected error: {other:?}"),
Ok(_) => panic!("unsupported protocol must fail setup"),
}
}
#[test]
fn setup_rejects_non_hex_cookie() {
let h = DefaultX11ForwardHandler::with_display_range(580, 599);
let ctx = X11ForwardContext::for_test_no_opens();
match h.setup("u", false, "MIT-MAGIC-COOKIE-1", "nothex!!", 0, ctx) {
Err(Error::Io(e)) => assert_eq!(e.kind(), ErrorKind::InvalidInput),
Err(other) => panic!("unexpected error: {other:?}"),
Ok(_) => panic!("non-hex cookie must fail setup"),
}
}
#[test]
fn single_connection_releases_listener_after_first_accept() {
let h = DefaultX11ForwardHandler::with_display_range(700, 720);
let ctx = X11ForwardContext::for_test_no_opens();
let handle = h
.setup("u", true, "MIT-MAGIC-COOKIE-1", "deadbeef", 0, ctx)
.expect("setup");
let addr: SocketAddr = ([127u8, 0, 0, 1], 6000 + handle.display_number).into();
let mut first =
TcpStream::connect_timeout(&addr, Duration::from_secs(2)).expect("first connect");
let pkt = x11_setup_packet(MIT_MAGIC_COOKIE_1.as_bytes(), &[0xde, 0xad, 0xbe, 0xef]);
first.write_all(&pkt).expect("write setup packet");
drop(first);
let mut released = false;
for _ in 0..50 {
thread::sleep(Duration::from_millis(50));
if TcpListener::bind(addr).is_ok() {
released = true;
break;
}
}
assert!(
released,
"single_connection: listener should be released after the first accept"
);
drop(handle);
}
}