pub mod actions;
use crate::server::connection::ConnectionId;
use anyhow::Result;
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::mpsc;
use crate::llm::action_helper::call_llm;
use crate::llm::actions::protocol_trait::ActionResult;
use crate::llm::ollama_client::OllamaClient;
use crate::logging::emit::Log;
use crate::protocol::Event;
use crate::server::TelnetProtocol;
use crate::state::app_state::AppState;
use actions::{TELNET_CONNECTION_OPENED_EVENT, TELNET_MESSAGE_RECEIVED_EVENT};
fn preview(text: &str, max: usize) -> String {
if text.len() <= max {
return text.to_string();
}
let mut end = max;
while end > 0 && !text.is_char_boundary(end) {
end -= 1;
}
format!("{}...", &text[..end])
}
pub const MAX_LINE_BYTES: usize = 8192;
const FIRST_LINE_READ_TIMEOUT: Duration = Duration::from_secs(300);
#[derive(Clone, Copy)]
struct ReadDeadlines {
first_byte: Duration,
idle: Duration,
}
impl ReadDeadlines {
fn resolve(first_byte_secs: Option<u64>, idle_secs: Option<u64>) -> Self {
Self {
first_byte: first_byte_secs
.map(Duration::from_secs)
.unwrap_or(FIRST_LINE_READ_TIMEOUT),
idle: idle_secs
.map(Duration::from_secs)
.unwrap_or(IDLE_BETWEEN_LINES_TIMEOUT),
}
}
}
const IDLE_BETWEEN_LINES_TIMEOUT: Duration = Duration::from_secs(600);
const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
const CONNECTION_CAP_REFUSAL: &[u8] = b"\r\n[netget] too many connections\r\n";
mod iac {
pub const IAC: u8 = 255;
pub const SE: u8 = 240;
pub const SB: u8 = 250;
pub const WILL: u8 = 251;
pub const DONT: u8 = 254;
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum IacState {
Data,
Command,
Option,
Subnegotiation,
SubnegotiationIac,
}
enum LineRead {
Line(Vec<u8>, usize),
Eof,
TooLong,
TimedOut,
Failed(std::io::Error),
}
struct TelnetLineReader<R> {
reader: R,
pending: Vec<u8>,
chunk: Vec<u8>,
state: IacState,
}
impl<R: tokio::io::AsyncRead + Unpin> TelnetLineReader<R> {
fn new(reader: R) -> Self {
Self {
reader,
pending: Vec::new(),
chunk: vec![0u8; 4096],
state: IacState::Data,
}
}
fn absorb(&mut self, raw: &[u8]) {
for &byte in raw {
self.state = match self.state {
IacState::Data => {
if byte == iac::IAC {
IacState::Command
} else {
self.pending.push(byte);
IacState::Data
}
}
IacState::Command => match byte {
iac::IAC => {
self.pending.push(iac::IAC);
IacState::Data
}
iac::SB => IacState::Subnegotiation,
iac::WILL..=iac::DONT => IacState::Option,
_ => IacState::Data,
},
IacState::Option => IacState::Data,
IacState::Subnegotiation => {
if byte == iac::IAC {
IacState::SubnegotiationIac
} else {
IacState::Subnegotiation
}
}
IacState::SubnegotiationIac => match byte {
iac::SE => IacState::Data,
_ => IacState::Subnegotiation,
},
};
}
}
async fn next_line(&mut self, bound: Duration) -> LineRead {
use tokio::io::AsyncReadExt;
loop {
if let Some(idx) = self.pending.iter().position(|b| *b == b'\n') {
let mut line: Vec<u8> = self.pending.drain(..=idx).collect();
let consumed = line.len();
line.pop();
if line.last() == Some(&b'\r') {
line.pop();
}
return LineRead::Line(line, consumed);
}
if self.pending.len() > MAX_LINE_BYTES {
return LineRead::TooLong;
}
let read = match tokio::time::timeout(bound, self.reader.read(&mut self.chunk)).await {
Err(_) => return LineRead::TimedOut,
Ok(read) => read,
};
let n = match read {
Ok(0) => {
if self.pending.is_empty() {
return LineRead::Eof;
}
let mut line = std::mem::take(&mut self.pending);
let consumed = line.len();
if line.last() == Some(&b'\r') {
line.pop();
}
return LineRead::Line(line, consumed);
}
Ok(n) => n,
Err(e) => return LineRead::Failed(e),
};
let raw = self.chunk[..n].to_vec();
self.absorb(&raw);
}
}
}
pub struct TelnetServer;
impl TelnetServer {
#[allow(clippy::too_many_arguments)]
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,
send_first: bool,
first_byte_timeout_secs: Option<u64>,
idle_timeout_secs: Option<u64>,
) -> Result<SocketAddr> {
let deadlines = ReadDeadlines::resolve(first_byte_timeout_secs, idle_timeout_secs);
let listener =
crate::server::socket_helpers::create_reusable_tcp_listener(listen_addr).await?;
let local_addr = listener.local_addr()?;
Log::new(Some(&status_tx)).info(format!("Telnet server listening on {}", local_addr));
let protocol = Arc::new(TelnetProtocol::new());
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,
"Telnet",
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);
let llm_clone = llm_client.clone();
let state_clone = app_state.clone();
let status_clone = status_tx.clone();
let protocol_clone = protocol.clone();
let task_owner = app_state.clone();
task_owner.spawn_server_task(server_id, async move {
let _permit = permit;
let (read_half, write_half) = tokio::io::split(stream);
let write_half_arc = Arc::new(tokio::sync::Mutex::new(write_half));
use crate::state::server::{
ConnectionState as ServerConnectionState, ConnectionStatus,
ProtocolConnectionInfo,
};
let now = crate::utils::clock::Instant::now();
let conn_state = ServerConnectionState {
id: connection_id,
remote_addr,
local_addr: local_addr_conn,
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(),
};
state_clone
.add_connection_to_server(server_id, conn_state)
.await;
let _ = status_clone.send("__UPDATE_UI__".to_string());
let log = Log::new(Some(&status_clone));
let peer_rx = crate::server::peer_support::register_peer_channel(
&state_clone,
server_id,
connection_id.as_u32(),
)
.await;
crate::server::peer_support::spawn_peer_command_task(
peer_rx,
protocol_clone.clone(),
state_clone.clone(),
server_id,
connection_id.as_u32(),
write_half_arc.clone(),
status_clone.clone(),
);
{
let event = Event::new(
&TELNET_CONNECTION_OPENED_EVENT,
crate::protocol::event_type::connect_event_data(),
);
match call_llm(
&llm_clone,
&state_clone,
server_id,
Some(connection_id),
&event,
protocol_clone.as_ref(),
)
.await
{
Ok(execution_result) => {
let mut greeted = 0usize;
for protocol_result in execution_result.protocol_results {
if let ActionResult::Output(data) = protocol_result {
greeted += 1;
use tokio::io::AsyncWriteExt;
let mut write = write_half_arc.lock().await;
let _ = write.write_all(&data).await;
let _ = write.flush().await;
drop(write);
state_clone
.update_connection_stats(
server_id,
connection_id,
None,
Some(data.len() as u64),
None,
Some(1),
)
.await;
log.debug(format!(
"Telnet sent greeting ({} bytes) on connection {}",
data.len(),
connection_id
));
}
}
if greeted > 0 {
log.info(format!(
"Telnet greeting for {} (connection {}) \
decision=model_answer ({} write(s))",
remote_addr, connection_id, greeted
));
} else if !send_first {
log.debug(format!(
"Telnet connect for {} (connection {}) \
decision=model_no_actions",
remote_addr, connection_id
));
} else {
log.warn(format!(
"Telnet greeting for {} (connection {}) \
decision=model_silent: send_first was requested \
but the model produced no banner",
remote_addr, connection_id
));
}
}
Err(e) if !send_first => {
log.warn(format!(
"Telnet connect event for {} (connection {}) \
decision=connect_event_failed: {}",
remote_addr, connection_id, e
));
}
Err(e) => {
let decision = match crate::utils::WireFailure::classify(&e)
{
crate::utils::WireFailure::Overloaded => {
"fail_closed_llm_overloaded"
}
crate::utils::WireFailure::Unavailable => {
"fail_closed_llm_error"
}
};
log.error(format!(
"Telnet greeting for {} (connection {}) decision={}: {}",
remote_addr, connection_id, decision, e
));
let notice = telnet_failure_notice(&e);
use tokio::io::AsyncWriteExt;
let mut write = write_half_arc.lock().await;
let _ = write.write_all(notice.as_bytes()).await;
let _ = write.flush().await;
}
}
}
let mut reader = TelnetLineReader::new(read_half);
let mut close_requested = false;
let mut answered_one = false;
loop {
let read_timeout = if answered_one {
deadlines.idle
} else {
deadlines.first_byte
};
let (line_bytes, n) = match reader.next_line(read_timeout).await {
LineRead::Line(bytes, n) => (bytes, n),
LineRead::Eof => break,
LineRead::TimedOut => {
log.info(format!(
"Telnet client {} (connection {}) sent nothing for \
{}s; closing idle connection",
remote_addr,
connection_id,
read_timeout.as_secs()
));
break;
}
LineRead::TooLong => {
log.warn(format!(
"Telnet line from {} (connection {}) \
decision=refused_line_too_long: more than {} bytes \
with no newline; closing",
remote_addr, connection_id, MAX_LINE_BYTES
));
use tokio::io::AsyncWriteExt;
let mut write = write_half_arc.lock().await;
let _ = write
.write_all(b"\r\n[netget] line too long\r\n")
.await;
let _ = write.flush().await;
let _ = write.shutdown().await;
break;
}
LineRead::Failed(e) => {
log.debug(format!(
"Telnet read error on connection {}: {}",
connection_id, e
));
break;
}
};
let line = String::from_utf8_lossy(&line_bytes).into_owned();
answered_one = true;
state_clone
.update_connection_stats(
server_id,
connection_id,
Some(n as u64),
None,
Some(1),
None,
)
.await;
let line_preview = preview(&line, 100);
log.debug(format!(
"Telnet received {} bytes on connection {}: {}",
n,
connection_id,
line_preview.trim()
));
log.trace(format!("Telnet data (text): {:?}", line.trim()));
let event = Event::new(
&TELNET_MESSAGE_RECEIVED_EVENT,
serde_json::json!({
"message": line.trim()
}),
);
log.debug(format!(
"Telnet calling LLM for connection {}",
connection_id
));
match call_llm(
&llm_clone,
&state_clone,
server_id,
Some(connection_id),
&event,
protocol_clone.as_ref(),
)
.await
{
Ok(execution_result) => {
for message in &execution_result.messages {
log.info(message);
}
log.debug(format!(
"Telnet got {} protocol results",
execution_result.protocol_results.len()
));
let waited = execution_result
.protocol_results
.iter()
.any(|r| matches!(r, ActionResult::WaitForMore));
let failure_summary = execution_result
.failures
.iter()
.map(|f| format!("{}: {}", f.action, f.error))
.collect::<Vec<_>>()
.join("; ");
let mut wrote = 0usize;
let mut closed = false;
for protocol_result in execution_result.protocol_results {
match protocol_result {
ActionResult::Output(data) => {
wrote += 1;
let mut write = write_half_arc.lock().await;
use tokio::io::AsyncWriteExt;
let _ = write.write_all(&data).await;
let _ = write.flush().await;
drop(write);
state_clone
.update_connection_stats(
server_id,
connection_id,
None,
Some(data.len() as u64),
None,
Some(1),
)
.await;
let response = String::from_utf8_lossy(&data);
let response_preview = preview(&response, 100);
log.debug(format!(
"Telnet sent {} bytes on connection {}: {}",
data.len(),
connection_id,
response_preview.trim()
));
log.trace(format!(
"Telnet sent (text): {:?}",
response.trim()
));
}
ActionResult::CloseConnection => {
close_requested = true;
closed = true;
}
_ => {}
}
}
let subject = format!(
"Telnet line from {} (connection {})",
remote_addr, connection_id
);
if wrote > 0 {
log.info(format!(
"{} decision=model_answer ({} write(s){})",
subject,
wrote,
if closed { ", then close" } else { "" }
));
} else if closed {
log.info(format!(
"{} decision=model_reject (close_connection with \
no reply: the model hung up rather than answer)",
subject
));
} else if !failure_summary.is_empty() {
log.error(format!(
"{} decision=fail_closed_bad_action: nothing was \
written because the model's action(s) could not \
be executed ({})",
subject, failure_summary
));
} else if waited {
log.info(format!(
"{} decision=model_wait_for_more (deliberately \
nothing yet; the session stays open)",
subject
));
} else {
log.warn(format!(
"{} decision=model_silent: the model was asked and \
returned no usable action, so nothing was written \
and the peer sees an idle session",
subject
));
}
}
Err(e) => {
let decision = match crate::utils::WireFailure::classify(&e)
{
crate::utils::WireFailure::Overloaded => {
"fail_closed_llm_overloaded"
}
crate::utils::WireFailure::Unavailable => {
"fail_closed_llm_error"
}
};
log.error(format!(
"Telnet line from {} (connection {}) decision={}: {}",
remote_addr, connection_id, decision, e
));
let notice = telnet_failure_notice(&e);
use tokio::io::AsyncWriteExt;
let mut write = write_half_arc.lock().await;
let _ = write.write_all(notice.as_bytes()).await;
let _ = write.flush().await;
}
}
if close_requested {
log.info(format!(
"Telnet connection {} closed by handler",
connection_id
));
use tokio::io::AsyncWriteExt;
let mut write = write_half_arc.lock().await;
let _ = write.shutdown().await;
break;
}
}
state_clone
.remove_peer_handle(server_id, connection_id.as_u32())
.await;
state_clone
.close_connection_on_server(server_id, connection_id)
.await;
let _ = status_clone.send("__UPDATE_UI__".to_string());
}).await;
}
Err(e) => {
Log::new(Some(&status_tx))
.error(format!("Failed to accept Telnet connection: {}", e));
break;
}
}
}
});
task_registrar
.register_server_task(server_id, accept_handle)
.await;
Ok(local_addr)
}
}
fn telnet_failure_notice(err: &anyhow::Error) -> String {
format!(
"\r\n[netget] {}\r\n",
crate::utils::WireFailure::classify(err).text()
)
}