pub mod actions;
use crate::llm::action_helper::call_llm;
use crate::llm::actions::protocol_trait::ActionResult;
use crate::llm::ollama_client::OllamaClient;
use crate::protocol::Event;
use crate::server::connection::ConnectionId;
use crate::state::app_state::AppState;
use crate::state::server::{ConnectionState, ConnectionStatus, ProtocolConnectionInfo};
use crate::{console_debug, console_info, console_warn};
use actions::{
build_negotiation_failure, protocol_value_to_names, RdpProtocol, DEFAULT_FAILURE_CODE,
RDP_CONNECTION_REQUEST_EVENT, TYPE_RDP_NEG_FAILURE, TYPE_RDP_NEG_REQ, TYPE_RDP_NEG_RSP,
X224_TPDU_CONNECTION_REQUEST,
};
use anyhow::{anyhow, Result};
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::sync::{mpsc, Mutex};
use tracing::{debug, error, info, warn};
pub const MAX_X224_LEN: usize = 2048;
struct ConnectionRequest {
cookie_username: String,
requested_protocols: u32,
flags: u8,
}
pub struct RdpServer;
const FIRST_BYTE_READ_TIMEOUT: Duration = Duration::from_secs(30);
const IN_FRAME_READ_TIMEOUT: Duration = Duration::from_secs(15);
const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
const CONNECTION_CAP_REFUSAL: &[u8] = b"";
type RdpReader = crate::server::accept_bounded::IdleTimeoutReader<tokio::io::ReadHalf<TcpStream>>;
impl RdpServer {
pub async fn spawn_with_llm_actions(
listen_addr: SocketAddr,
llm_client: OllamaClient,
app_state: Arc<AppState>,
status_tx: mpsc::UnboundedSender<String>,
server_id: crate::state::ServerId,
) -> Result<SocketAddr> {
let listener =
crate::server::socket_helpers::create_reusable_tcp_listener(listen_addr).await?;
let local_addr = listener.local_addr()?;
console_info!(
status_tx,
"RDP server (negotiation slice) listening on {}",
local_addr
);
let task_registrar = app_state.clone();
let limiter = crate::server::accept_bounded::ConnectionLimiter::new(MAX_CONNECTIONS);
let accept_handle = tokio::spawn(async move {
loop {
match crate::server::accept_bounded::accept_bounded(
&listener,
&limiter,
CONNECTION_CAP_REFUSAL,
"RDP",
Some(&status_tx),
)
.await
{
Ok((stream, remote_addr, permit)) => {
let connection_id =
ConnectionId::new(app_state.get_next_unified_id().await);
let local_addr_conn = stream.local_addr().unwrap_or(local_addr);
info!("RDP client connected from {}", remote_addr);
let _ = status_tx
.send(format!("[INFO] RDP client connected from {}", remote_addr));
let llm_clone = llm_client.clone();
let state_clone = app_state.clone();
let status_clone = status_tx.clone();
let task_owner = app_state.clone();
task_owner
.spawn_server_task(server_id, async move {
let _permit = permit;
if let Err(e) = Self::handle_connection(
stream,
connection_id,
remote_addr,
local_addr_conn,
server_id,
state_clone,
status_clone,
llm_clone,
)
.await
{
error!("RDP connection error: {}", e);
}
})
.await;
}
Err(e) => {
error!("RDP accept failed: {}", e);
let _ = status_tx.send(format!("✗ RDP accept failed, stopping: {}", e));
break;
}
}
}
});
task_registrar
.register_server_task(server_id, accept_handle)
.await;
Ok(local_addr)
}
#[allow(clippy::too_many_arguments)]
async fn handle_connection(
stream: TcpStream,
connection_id: ConnectionId,
remote_addr: SocketAddr,
local_addr: SocketAddr,
server_id: crate::state::ServerId,
app_state: Arc<AppState>,
status_tx: mpsc::UnboundedSender<String>,
llm_client: OllamaClient,
) -> Result<()> {
let (read_half, write_half) = tokio::io::split(stream);
let mut read_half: RdpReader = crate::server::accept_bounded::IdleTimeoutReader::with_first(
read_half,
FIRST_BYTE_READ_TIMEOUT,
IN_FRAME_READ_TIMEOUT,
);
let write_half = Arc::new(Mutex::new(write_half));
let now = crate::utils::clock::Instant::now();
let conn_state = ConnectionState {
id: connection_id,
remote_addr,
local_addr,
bytes_sent: 0,
bytes_received: 0,
packets_sent: 0,
packets_received: 0,
last_activity: now,
status: ConnectionStatus::Active,
status_changed_at: now,
protocol_info: ProtocolConnectionInfo::empty(),
};
app_state
.add_connection_to_server(server_id, conn_state)
.await;
let _ = status_tx.send("__UPDATE_UI__".to_string());
let protocol: Arc<RdpProtocol> = Arc::new(RdpProtocol::new());
let peer_rx = crate::server::peer_support::register_peer_channel(
&app_state,
server_id,
connection_id.as_u32(),
)
.await;
crate::server::peer_support::spawn_peer_command_task(
peer_rx,
protocol.clone(),
app_state.clone(),
server_id,
connection_id.as_u32(),
write_half.clone(),
status_tx.clone(),
);
let result = Self::negotiate(
&mut read_half,
&write_half,
connection_id,
server_id,
&app_state,
&status_tx,
&llm_client,
&protocol,
)
.await;
app_state
.remove_peer_handle(server_id, connection_id.as_u32())
.await;
app_state
.remove_connection_from_server(server_id, connection_id)
.await;
let _ = status_tx.send("__UPDATE_UI__".to_string());
result
}
#[allow(clippy::too_many_arguments)]
async fn negotiate(
read_half: &mut RdpReader,
write_half: &Arc<Mutex<tokio::io::WriteHalf<TcpStream>>>,
connection_id: ConnectionId,
server_id: crate::state::ServerId,
app_state: &Arc<AppState>,
status_tx: &mpsc::UnboundedSender<String>,
llm_client: &OllamaClient,
protocol: &Arc<RdpProtocol>,
) -> Result<()> {
let (request, bytes_in) = match read_connection_request(read_half).await {
Ok(req) => req,
Err(e) => {
console_warn!(status_tx, "RDP malformed connection request: {}", e);
return Ok(());
}
};
app_state
.update_connection_stats(
server_id,
connection_id,
Some(bytes_in as u64),
None,
Some(1),
None,
)
.await;
let protocol_names = protocol_value_to_names(request.requested_protocols);
console_debug!(
status_tx,
"RDP CR: user='{}', requested={:?}, flags={:#04x}",
request.cookie_username,
protocol_names,
request.flags
);
let event = Event::new(
&RDP_CONNECTION_REQUEST_EVENT,
serde_json::json!({
"cookie_username": request.cookie_username,
"requested_protocols": protocol_names,
"requested_protocols_flags": request.flags,
}),
);
let response_bytes = match call_llm(
llm_client,
app_state,
server_id,
Some(connection_id),
&event,
protocol.as_ref(),
)
.await
{
Ok(execution) => {
for message in execution.messages {
let _ = status_tx.send(message);
}
let mut chosen = None;
for result in execution.protocol_results {
if let ActionResult::Output(bytes) = result {
chosen = Some(bytes);
break;
}
}
match chosen {
Some(bytes) => {
let decision = match negotiation_kind(&bytes) {
Some(TYPE_RDP_NEG_RSP) => "model_accept",
Some(TYPE_RDP_NEG_FAILURE) => "model_reject",
_ => "model_other",
};
debug!(
"RDP {} decision={} ({} bytes)",
connection_id,
decision,
bytes.len()
);
console_debug!(status_tx, "RDP {} decision={}", connection_id, decision);
bytes
}
None => {
let tag = if execution.failures.is_empty() {
"fail_closed_model_silent"
} else {
"fail_closed_action_error"
};
warn!(
"RDP {} decision={}: no usable negotiation action; \
answering SSL_REQUIRED_BY_SERVER",
connection_id, tag
);
console_warn!(
status_tx,
"RDP {} decision={}: no usable negotiation action, failing closed (SSL_REQUIRED_BY_SERVER)",
connection_id,
tag
);
build_negotiation_failure(DEFAULT_FAILURE_CODE)
}
}
}
Err(e) => {
warn!(
"RDP {} decision=fail_closed_llm_error ({:?}): {}",
connection_id,
crate::utils::WireFailure::classify(&e),
e
);
console_warn!(
status_tx,
"RDP {} decision=fail_closed_llm_error, failing closed (SSL_REQUIRED_BY_SERVER): {}",
connection_id,
e
);
build_negotiation_failure(DEFAULT_FAILURE_CODE)
}
};
{
let mut writer = write_half.lock().await;
writer.write_all(&response_bytes).await?;
writer.flush().await?;
}
app_state
.update_connection_stats(
server_id,
connection_id,
None,
Some(response_bytes.len() as u64),
None,
Some(1),
)
.await;
console_debug!(
status_tx,
"RDP sent {}-byte Connection Confirm to {}",
response_bytes.len(),
connection_id
);
{
let mut writer = write_half.lock().await;
let _ = writer.shutdown().await;
}
debug!("RDP negotiation complete for {}, closing", connection_id);
Ok(())
}
}
fn negotiation_kind(confirm: &[u8]) -> Option<u8> {
confirm.get(11).copied()
}
async fn read_connection_request(read_half: &mut RdpReader) -> Result<(ConnectionRequest, usize)> {
let mut tpkt = [0u8; 4];
read_half.read_exact(&mut tpkt).await?;
if tpkt[0] != 0x03 {
return Err(anyhow!("not a TPKT frame (first byte {:#04x})", tpkt[0]));
}
let total_len = u16::from_be_bytes([tpkt[2], tpkt[3]]) as usize;
if total_len < 11 || total_len > MAX_X224_LEN {
return Err(anyhow!("implausible TPKT length {total_len}"));
}
let x224_len = total_len - 4;
let mut x224 = vec![0u8; x224_len];
read_half.read_exact(&mut x224).await?;
if x224.len() < 7 {
return Err(anyhow!("X.224 CR too short ({} bytes)", x224.len()));
}
let code = x224[1];
if code != X224_TPDU_CONNECTION_REQUEST {
return Err(anyhow!(
"X.224 TPDU is not a Connection Request (code {:#04x})",
code
));
}
let variable = &x224[7..];
let (cookie_username, after_cookie) = parse_routing_cookie(variable);
let (requested_protocols, flags) = parse_rdp_neg_req(after_cookie);
let bytes_in = 4 + x224.len();
Ok((
ConnectionRequest {
cookie_username,
requested_protocols,
flags,
},
bytes_in,
))
}
fn parse_routing_cookie(data: &[u8]) -> (String, &[u8]) {
const COOKIE_PREFIX: &[u8] = b"Cookie: ";
if !data.starts_with(COOKIE_PREFIX) {
return (String::new(), data);
}
let Some(crlf) = data.windows(2).position(|w| w == b"\r\n") else {
return (String::new(), data);
};
let line = &data[COOKIE_PREFIX.len()..crlf];
let rest = &data[crlf + 2..];
const MSTSHASH: &[u8] = b"mstshash=";
let username = if line.starts_with(MSTSHASH) {
String::from_utf8_lossy(&line[MSTSHASH.len()..]).to_string()
} else {
String::new()
};
(username, rest)
}
fn parse_rdp_neg_req(data: &[u8]) -> (u32, u8) {
if data.len() < 8 || data[0] != TYPE_RDP_NEG_REQ {
return (0, 0);
}
let flags = data[1];
let requested = u32::from_le_bytes([data[4], data[5], data[6], data[7]]);
(requested, flags)
}