Implement SSH X11 forwarding Phase 1
- x11_forward.rs module with X11ForwardContext - parse_display() to parse DISPLAY env variable - read_xauthority_cookie() to read MIT-MAGIC-COOKIE-1 - X11Connection for socket forwarding - Unit tests: parse_display/disabled/display_env All tests pass.
This commit is contained in:
@@ -22,6 +22,7 @@ pub mod sshbuf;
|
|||||||
pub mod upload_hook;
|
pub mod upload_hook;
|
||||||
pub mod version;
|
pub mod version;
|
||||||
pub mod window_manager;
|
pub mod window_manager;
|
||||||
|
pub mod x11_forward; // SSH X11 forwarding (RFC 4254 §7.2)
|
||||||
|
|
||||||
pub use packet::{PacketType, SshPacket};
|
pub use packet::{PacketType, SshPacket};
|
||||||
pub use server::SshServer;
|
pub use server::SshServer;
|
||||||
|
|||||||
341
markbase-core/src/ssh_server/x11_forward.rs
Normal file
341
markbase-core/src/ssh_server/x11_forward.rs
Normal file
@@ -0,0 +1,341 @@
|
|||||||
|
//! SSH X11 forwarding support (RFC 4254 §7.2).
|
||||||
|
//!
|
||||||
|
//! OpenSSH supports X11 forwarding for remote graphical applications.
|
||||||
|
//! X11 connections are forwarded through SSH channel type "x11".
|
||||||
|
|
||||||
|
use anyhow::{Result, anyhow};
|
||||||
|
use log::info;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::net::TcpStream;
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
|
||||||
|
/// X11 authentication cookie type (RFC 4254 §7.2).
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum X11AuthType {
|
||||||
|
/// MIT-MAGIC-COOKIE-1 (most common)
|
||||||
|
MitMagicCookie1,
|
||||||
|
/// XDM-AUTHORIZATION-1 (less common)
|
||||||
|
XdmAuthorization1,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// X11 forwarding context (RFC 4254 §7.2).
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct X11ForwardContext {
|
||||||
|
/// X11 display number (e.g., 0 for :0)
|
||||||
|
display_number: u16,
|
||||||
|
/// X11 screen number (e.g., 0 for :0.0)
|
||||||
|
screen_number: u16,
|
||||||
|
/// Authentication type
|
||||||
|
auth_type: X11AuthType,
|
||||||
|
/// Authentication cookie (16 bytes for MIT-MAGIC-COOKIE-1)
|
||||||
|
auth_cookie: Vec<u8>,
|
||||||
|
/// X11 socket path (e.g., /tmp/.X11-unix/X0)
|
||||||
|
socket_path: PathBuf,
|
||||||
|
/// Whether X11 forwarding is enabled
|
||||||
|
enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl X11ForwardContext {
|
||||||
|
/// Create new X11 forwarding context.
|
||||||
|
pub fn new(display: &str) -> Result<Self> {
|
||||||
|
// Parse DISPLAY environment variable (e.g., ":0", "localhost:10.0")
|
||||||
|
let (display_number, screen_number, socket_path) = parse_display(display)?;
|
||||||
|
|
||||||
|
// Read Xauthority file to get authentication cookie
|
||||||
|
let auth_cookie = read_xauthority_cookie(display_number)?;
|
||||||
|
|
||||||
|
info!("X11 forwarding enabled: display={}, socket={}",
|
||||||
|
display_number, socket_path.display());
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
display_number,
|
||||||
|
screen_number,
|
||||||
|
auth_type: X11AuthType::MitMagicCookie1,
|
||||||
|
auth_cookie,
|
||||||
|
socket_path,
|
||||||
|
enabled: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create disabled X11 forwarding context.
|
||||||
|
pub fn disabled() -> Self {
|
||||||
|
Self {
|
||||||
|
display_number: 0,
|
||||||
|
screen_number: 0,
|
||||||
|
auth_type: X11AuthType::MitMagicCookie1,
|
||||||
|
auth_cookie: vec![0; 16],
|
||||||
|
socket_path: PathBuf::new(),
|
||||||
|
enabled: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if X11 forwarding is enabled.
|
||||||
|
pub fn is_enabled(&self) -> bool {
|
||||||
|
self.enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get display number.
|
||||||
|
pub fn display_number(&self) -> u16 {
|
||||||
|
self.display_number
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get screen number.
|
||||||
|
pub fn screen_number(&self) -> u16 {
|
||||||
|
self.screen_number
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get authentication cookie.
|
||||||
|
pub fn auth_cookie(&self) -> &[u8] {
|
||||||
|
&self.auth_cookie
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get socket path.
|
||||||
|
pub fn socket_path(&self) -> &PathBuf {
|
||||||
|
&self.socket_path
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Connect to local X11 display.
|
||||||
|
pub fn connect(&self) -> Result<X11Connection> {
|
||||||
|
if !self.enabled {
|
||||||
|
return Err(anyhow!("X11 forwarding disabled"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect to X11 Unix socket
|
||||||
|
let socket = TcpStream::connect("127.0.0.1:6000")?;
|
||||||
|
|
||||||
|
info!("Connected to X11 display: {}", self.display_number);
|
||||||
|
|
||||||
|
Ok(X11Connection {
|
||||||
|
socket,
|
||||||
|
display_number: self.display_number,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get DISPLAY environment variable value.
|
||||||
|
pub fn display_env(&self) -> String {
|
||||||
|
format!(":{}", self.display_number)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// X11 connection (forwarded through SSH channel).
|
||||||
|
pub struct X11Connection {
|
||||||
|
socket: TcpStream,
|
||||||
|
display_number: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl X11Connection {
|
||||||
|
/// Get socket reference.
|
||||||
|
pub fn socket(&self) -> &TcpStream {
|
||||||
|
&self.socket
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get mutable socket reference.
|
||||||
|
pub fn socket_mut(&mut self) -> &mut TcpStream {
|
||||||
|
&mut self.socket
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse DISPLAY environment variable.
|
||||||
|
/// Examples: ":0", ":0.0", "localhost:10.0"
|
||||||
|
fn parse_display(display: &str) -> Result<(u16, u16, PathBuf)> {
|
||||||
|
// Remove leading ":" if present
|
||||||
|
let display = display.trim_start_matches(':');
|
||||||
|
|
||||||
|
// Split display/screen number
|
||||||
|
let parts: Vec<&str> = display.split('.').collect();
|
||||||
|
|
||||||
|
// Parse display number
|
||||||
|
let display_number = parts[0]
|
||||||
|
.parse::<u16>()
|
||||||
|
.map_err(|_| anyhow!("Invalid display number: {}", display))?;
|
||||||
|
|
||||||
|
// Parse screen number (default 0)
|
||||||
|
let screen_number = if parts.len() > 1 {
|
||||||
|
parts[1].parse::<u16>()
|
||||||
|
.map_err(|_| anyhow!("Invalid screen number: {}", parts[1]))?
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
|
// X11 Unix socket path (e.g., /tmp/.X11-unix/X0)
|
||||||
|
let socket_path = PathBuf::from(format!("/tmp/.X11-unix/X{}", display_number));
|
||||||
|
|
||||||
|
Ok((display_number, screen_number, socket_path))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read Xauthority file to get authentication cookie.
|
||||||
|
/// Xauthority file: ~/.Xauthority
|
||||||
|
/// Format: family address number name data
|
||||||
|
fn read_xauthority_cookie(display_number: u16) -> Result<Vec<u8>> {
|
||||||
|
// Try to read ~/.Xauthority
|
||||||
|
let home = std::env::var("HOME")
|
||||||
|
.map_err(|_| anyhow!("HOME environment variable not set"))?;
|
||||||
|
let xauthority_path = PathBuf::from(home).join(".Xauthority");
|
||||||
|
|
||||||
|
if !xauthority_path.exists() {
|
||||||
|
// Generate random cookie if no .Xauthority file
|
||||||
|
info!("No .Xauthority file found, generating random cookie");
|
||||||
|
let mut cookie = vec![0u8; 16];
|
||||||
|
use rand::RngCore;
|
||||||
|
rand::thread_rng().fill_bytes(&mut cookie);
|
||||||
|
return Ok(cookie);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read .Xauthority file (binary format)
|
||||||
|
// Entry format: family(2) address_length(2) address number_length(2) number name_length(2) name data_length(2) data
|
||||||
|
let mut file = std::fs::File::open(&xauthority_path)?;
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
file.read_to_end(&mut buffer)?;
|
||||||
|
|
||||||
|
// Parse entries to find cookie for our display
|
||||||
|
// This is simplified - proper parsing would handle all entry types
|
||||||
|
// For MIT-MAGIC-COOKIE-1, data is 16 bytes
|
||||||
|
|
||||||
|
// Search for FamilyLocal (256) entries with display number matching
|
||||||
|
let mut cursor = 0;
|
||||||
|
while cursor < buffer.len() {
|
||||||
|
if cursor + 2 > buffer.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let family = u16::from_be_bytes([buffer[cursor], buffer[cursor + 1]]);
|
||||||
|
cursor += 2;
|
||||||
|
|
||||||
|
// FamilyLocal (256) or FamilyWild (65535)
|
||||||
|
if family == 256 || family == 65535 {
|
||||||
|
// Skip address
|
||||||
|
if cursor + 2 > buffer.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let addr_len = u16::from_be_bytes([buffer[cursor], buffer[cursor + 1]]) as usize;
|
||||||
|
cursor += 2 + addr_len;
|
||||||
|
|
||||||
|
// Read number (display number)
|
||||||
|
if cursor + 2 > buffer.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let num_len = u16::from_be_bytes([buffer[cursor], buffer[cursor + 1]]) as usize;
|
||||||
|
cursor += 2;
|
||||||
|
|
||||||
|
if cursor + num_len > buffer.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let number_str = String::from_utf8_lossy(&buffer[cursor..cursor + num_len]);
|
||||||
|
cursor += num_len;
|
||||||
|
|
||||||
|
// Check if display number matches
|
||||||
|
if number_str.parse::<u16>().ok() == Some(display_number) || family == 65535 {
|
||||||
|
// Read name (authentication protocol name)
|
||||||
|
if cursor + 2 > buffer.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let name_len = u16::from_be_bytes([buffer[cursor], buffer[cursor + 1]]) as usize;
|
||||||
|
cursor += 2;
|
||||||
|
|
||||||
|
if cursor + name_len > buffer.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let name = String::from_utf8_lossy(&buffer[cursor..cursor + name_len]);
|
||||||
|
cursor += name_len;
|
||||||
|
|
||||||
|
// Check if MIT-MAGIC-COOKIE-1
|
||||||
|
if name == "MIT-MAGIC-COOKIE-1" {
|
||||||
|
// Read data (cookie)
|
||||||
|
if cursor + 2 > buffer.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let data_len = u16::from_be_bytes([buffer[cursor], buffer[cursor + 1]]) as usize;
|
||||||
|
cursor += 2;
|
||||||
|
|
||||||
|
if cursor + data_len > buffer.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let cookie = buffer[cursor..cursor + data_len].to_vec();
|
||||||
|
info!("Found MIT-MAGIC-COOKIE-1: {} bytes", cookie.len());
|
||||||
|
return Ok(cookie);
|
||||||
|
} else {
|
||||||
|
// Skip data
|
||||||
|
if cursor + 2 > buffer.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let data_len = u16::from_be_bytes([buffer[cursor], buffer[cursor + 1]]) as usize;
|
||||||
|
cursor += 2 + data_len;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Skip name and data
|
||||||
|
if cursor + 2 > buffer.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let name_len = u16::from_be_bytes([buffer[cursor], buffer[cursor + 1]]) as usize;
|
||||||
|
cursor += 2 + name_len;
|
||||||
|
|
||||||
|
if cursor + 2 > buffer.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let data_len = u16::from_be_bytes([buffer[cursor], buffer[cursor + 1]]) as usize;
|
||||||
|
cursor += 2 + data_len;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Skip other family types
|
||||||
|
if cursor + 2 > buffer.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let addr_len = u16::from_be_bytes([buffer[cursor], buffer[cursor + 1]]) as usize;
|
||||||
|
cursor += 2 + addr_len;
|
||||||
|
|
||||||
|
if cursor + 2 > buffer.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let num_len = u16::from_be_bytes([buffer[cursor], buffer[cursor + 1]]) as usize;
|
||||||
|
cursor += 2 + num_len;
|
||||||
|
|
||||||
|
if cursor + 2 > buffer.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let name_len = u16::from_be_bytes([buffer[cursor], buffer[cursor + 1]]) as usize;
|
||||||
|
cursor += 2 + name_len;
|
||||||
|
|
||||||
|
if cursor + 2 > buffer.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let data_len = u16::from_be_bytes([buffer[cursor], buffer[cursor + 1]]) as usize;
|
||||||
|
cursor += 2 + data_len;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No cookie found, generate random
|
||||||
|
info!("No matching Xauthority entry found, generating random cookie");
|
||||||
|
let mut cookie = vec![0u8; 16];
|
||||||
|
use rand::RngCore;
|
||||||
|
rand::thread_rng().fill_bytes(&mut cookie);
|
||||||
|
Ok(cookie)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_display() {
|
||||||
|
let (display, screen, path) = parse_display(":0").unwrap();
|
||||||
|
assert_eq!(display, 0);
|
||||||
|
assert_eq!(screen, 0);
|
||||||
|
assert_eq!(path.to_str().unwrap(), "/tmp/.X11-unix/X0");
|
||||||
|
|
||||||
|
let (display, screen, _) = parse_display(":10.0").unwrap();
|
||||||
|
assert_eq!(display, 10);
|
||||||
|
assert_eq!(screen, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_x11_disabled() {
|
||||||
|
let ctx = X11ForwardContext::disabled();
|
||||||
|
assert!(!ctx.is_enabled());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_display_env() {
|
||||||
|
let ctx = X11ForwardContext::new(":0").unwrap();
|
||||||
|
assert_eq!(ctx.display_env(), ":0");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user