pub mod actions;
use crate::llm::action_helper::call_llm;
use crate::llm::ollama_client::OllamaClient;
use crate::logging::emit::Log;
use crate::protocol::Event;
use crate::server::connection::ConnectionId;
use crate::state::app_state::AppState;
use actions::WHOIS_QUERY_EVENT;
use anyhow::Result;
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;
use tokio::sync::{mpsc, Mutex};
pub const MAX_QUERY_BYTES: usize = 4096;
const FIRST_QUERY_READ_TIMEOUT: Duration = Duration::from_secs(300);
const IDLE_AFTER_REPLY_TIMEOUT: Duration = Duration::from_secs(15);
const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
const CONNECTION_CAP_REFUSAL: &[u8] = b"% netget: too many connections, retry later\r\n";
pub struct WhoisServer;
impl WhoisServer {
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,
first_byte_timeout_secs: Option<u64>,
idle_timeout_secs: Option<u64>,
) -> Result<SocketAddr> {
let first_query_timeout = first_byte_timeout_secs
.map(Duration::from_secs)
.unwrap_or(FIRST_QUERY_READ_TIMEOUT);
let idle_timeout = idle_timeout_secs
.map(Duration::from_secs)
.unwrap_or(IDLE_AFTER_REPLY_TIMEOUT);
let listener = TcpListener::bind(listen_addr).await?;
let local_addr = listener.local_addr()?;
Log::new(Some(&status_tx)).info(format!("WHOIS server listening on {}", local_addr));
let protocol = Arc::new(actions::WhoisProtocol::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,
"WHOIS",
Some(&status_tx),
)
.await
{
Ok((socket, peer_addr, permit)) => {
let connection_id =
ConnectionId::new(app_state.get_next_unified_id().await);
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: peer_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());
Log::new(Some(&status_tx))
.info(format!("WHOIS client connected from {}", peer_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 connection_id_clone = connection_id;
let task_owner = app_state.clone();
task_owner
.spawn_server_task(server_id, async move {
let _permit = permit;
handle_whois_connection(
socket,
peer_addr,
llm_clone,
state_clone,
status_clone,
server_id,
protocol_clone,
connection_id_clone,
first_query_timeout,
idle_timeout,
)
.await
})
.await;
}
Err(e) => {
Log::new(Some(&status_tx)).error(format!("WHOIS accept error: {}", e));
break;
}
}
}
});
task_registrar
.register_server_task(server_id, accept_handle)
.await;
Ok(local_addr)
}
}
async fn write_counted<W>(
write_half: &Arc<Mutex<W>>,
data: &[u8],
app_state: &AppState,
server_id: crate::state::ServerId,
connection_id: ConnectionId,
) -> std::io::Result<()>
where
W: tokio::io::AsyncWrite + Unpin,
{
{
let mut writer = write_half.lock().await;
writer.write_all(data).await?;
writer.flush().await?;
}
app_state
.update_connection_stats(
server_id,
connection_id,
None,
Some(data.len() as u64),
None,
Some(1),
)
.await;
Ok(())
}
#[allow(clippy::too_many_arguments)]
async fn handle_whois_connection(
socket: tokio::net::TcpStream,
peer_addr: SocketAddr,
llm_client: OllamaClient,
app_state: Arc<AppState>,
status_tx: mpsc::UnboundedSender<String>,
server_id: crate::state::ServerId,
protocol: Arc<actions::WhoisProtocol>,
connection_id: ConnectionId,
first_query_timeout: Duration,
idle_timeout: Duration,
) {
let (reader, write_half) = tokio::io::split(socket);
let write_half = Arc::new(Mutex::new(write_half));
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(),
);
run_whois_session(
reader,
&write_half,
peer_addr,
&llm_client,
&app_state,
&status_tx,
server_id,
&protocol,
connection_id,
first_query_timeout,
idle_timeout,
)
.await;
app_state
.remove_peer_handle(server_id, connection_id.as_u32())
.await;
let _ = write_half.lock().await.shutdown().await;
use crate::state::server::ConnectionStatus;
app_state
.update_connection_status(server_id, connection_id, ConnectionStatus::Closed)
.await;
let _ = status_tx.send("__UPDATE_UI__".to_string());
}
#[allow(clippy::too_many_arguments)]
async fn run_whois_session<R, W>(
mut reader: R,
write_half: &Arc<Mutex<W>>,
peer_addr: SocketAddr,
llm_client: &OllamaClient,
app_state: &Arc<AppState>,
status_tx: &mpsc::UnboundedSender<String>,
server_id: crate::state::ServerId,
protocol: &Arc<actions::WhoisProtocol>,
connection_id: ConnectionId,
first_query_timeout: Duration,
idle_timeout: Duration,
) where
R: tokio::io::AsyncRead + Unpin,
W: tokio::io::AsyncWrite + Unpin,
{
let log = Log::new(Some(status_tx));
let mut lines = QueryLineReader::new(&mut reader);
let mut answered_one = false;
loop {
let read_timeout = if answered_one {
idle_timeout
} else {
first_query_timeout
};
let (query, n) = match lines.next_query(read_timeout).await {
QueryRead::Line(line, n) => (line, n),
QueryRead::Eof => {
log.info(format!("WHOIS client {} disconnected", peer_addr));
break;
}
QueryRead::TimedOut => {
log.info(format!(
"WHOIS client {} sent nothing further within {}s; closing",
peer_addr,
read_timeout.as_secs()
));
break;
}
QueryRead::TooLong => {
log.warn(format!(
"WHOIS query from {} exceeded {} bytes with no newline; refusing",
peer_addr, MAX_QUERY_BYTES
));
let _ = write_counted(
write_half,
b"% netget: query too long\r\n",
app_state,
server_id,
connection_id,
)
.await;
break;
}
QueryRead::Failed(e) => {
log.error(format!("WHOIS read error from {}: {}", peer_addr, e));
break;
}
};
app_state
.update_connection_stats(
server_id,
connection_id,
Some(n as u64),
None,
Some(1),
None,
)
.await;
log.debug(format!("WHOIS received {} bytes from {}", n, peer_addr));
log.trace(format!("WHOIS query data: {}", query));
let event = Event::new(
&WHOIS_QUERY_EVENT,
serde_json::json!({
"query": query,
}),
);
log.debug(format!("WHOIS calling LLM for query from {}", peer_addr));
match call_llm(
llm_client,
app_state,
server_id,
Some(connection_id),
&event,
protocol.as_ref(),
)
.await
{
Ok(execution_result) => {
for message in &execution_result.messages {
log.info(message);
}
log.debug(format!(
"WHOIS got {} protocol results",
execution_result.protocol_results.len()
));
let mut should_close = false;
let mut wrote_output = false;
for protocol_result in execution_result.protocol_results {
match protocol_result {
crate::llm::actions::protocol_trait::ActionResult::Output(output_data) => {
if let Err(e) = write_counted(
write_half,
&output_data,
app_state,
server_id,
connection_id,
)
.await
{
log.error(format!("WHOIS write error: {}", e));
return;
}
wrote_output = true;
log.debug(format!(
"WHOIS sent {} bytes to {}",
output_data.len(),
peer_addr
));
log.trace(format!(
"WHOIS response: {}",
String::from_utf8_lossy(&output_data)
));
log.info(format!(
"WHOIS response to {} ({} bytes)",
peer_addr,
output_data.len()
));
}
crate::llm::actions::protocol_trait::ActionResult::CloseConnection => {
should_close = true;
log.debug("WHOIS closing connection per LLM request");
}
_ => {} }
}
if !wrote_output {
log.warn(format!(
"WHOIS {:?} from {} decision=model_silent ({} failed action(s)); \
answering with a comment instead of closing silently",
query,
peer_addr,
execution_result.failures.len()
));
let notice = b"% netget: no data was produced for this query\r\n";
if write_counted(write_half, notice, app_state, server_id, connection_id)
.await
.is_err()
{
return;
}
}
answered_one = true;
if should_close {
break;
}
}
Err(e) => {
let (category, notice): (&str, &[u8]) =
match crate::utils::WireFailure::classify(&e) {
crate::utils::WireFailure::Overloaded => (
"overloaded",
b"% netget: backend at capacity, retry later\r\n",
),
crate::utils::WireFailure::Unavailable => (
"unavailable",
b"% netget: the query could not be answered\r\n",
),
};
log.warn(format!(
"WHOIS {:?} from {} decision=fail_closed_llm_error category={}",
query, peer_addr, category
));
log.debug(format!("WHOIS LLM call failed: {}", e));
let _ =
write_counted(write_half, notice, app_state, server_id, connection_id).await;
break;
}
}
}
}
enum QueryRead {
Line(String, usize),
Eof,
TimedOut,
TooLong,
Failed(std::io::Error),
}
struct QueryLineReader<'a, R> {
reader: &'a mut R,
pending: Vec<u8>,
chunk: Vec<u8>,
}
impl<'a, R: tokio::io::AsyncRead + Unpin> QueryLineReader<'a, R> {
fn new(reader: &'a mut R) -> Self {
Self {
reader,
pending: Vec::new(),
chunk: vec![0u8; 1024],
}
}
async fn next_query(&mut self, timeout: Duration) -> QueryRead {
loop {
if let Some(idx) = self.pending.iter().position(|b| *b == b'\n') {
let consumed = idx + 1;
let line: Vec<u8> = self.pending.drain(..consumed).collect();
return QueryRead::Line(
String::from_utf8_lossy(&line[..idx]).trim().to_string(),
consumed,
);
}
if self.pending.len() > MAX_QUERY_BYTES {
return QueryRead::TooLong;
}
let read = match tokio::time::timeout(timeout, self.reader.read(&mut self.chunk)).await
{
Err(_) => return QueryRead::TimedOut,
Ok(read) => read,
};
match read {
Ok(0) => {
if self.pending.is_empty() {
return QueryRead::Eof;
}
let line = std::mem::take(&mut self.pending);
let consumed = line.len();
return QueryRead::Line(
String::from_utf8_lossy(&line).trim().to_string(),
consumed,
);
}
Ok(n) => self.pending.extend_from_slice(&self.chunk[..n]),
Err(e) => return QueryRead::Failed(e),
}
}
}
}