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::server::DotProtocol;
use crate::state::app_state::AppState;
use crate::state::ServerId;
use actions::DOT_QUERY_EVENT;
use anyhow::{Context, Result};
use hickory_proto::op::Message as DnsMessage;
use serde_json::json;
use std::net::SocketAddr;
use std::sync::Arc;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::mpsc;
use tokio::time::{timeout, Duration};
use tokio_rustls::TlsAcceptor;
use tracing::error;
const TLS_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(10);
const IDLE_READ_TIMEOUT: Duration = Duration::from_secs(300);
const MESSAGE_READ_TIMEOUT: Duration = Duration::from_secs(10);
const ACCEPT_ERROR_BACKOFF: Duration = Duration::from_millis(100);
const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
const CONNECTION_CAP_REFUSAL: &[u8] = b"";
pub struct DotServer;
impl DotServer {
pub async fn spawn(
bind_addr: SocketAddr,
llm_client: OllamaClient,
app_state: Arc<AppState>,
server_id: ServerId,
status_tx: mpsc::UnboundedSender<String>,
) -> Result<SocketAddr> {
let tls_config = crate::server::tls_cert_manager::generate_default_tls_config()
.context("Failed to generate TLS configuration")?;
Log::new(Some(&status_tx)).info(format!("Starting DoT server on {}", bind_addr));
let listener = TcpListener::bind(bind_addr)
.await
.context("Failed to bind DoT TCP listener")?;
let local_addr = listener
.local_addr()
.context("Failed to get DoT listener local address")?;
Log::new(Some(&status_tx)).info(format!("DoT server listening on {}", local_addr));
let task_registrar = app_state.clone();
let handle = tokio::spawn(async move {
if let Err(e) = Self::run(
listener, tls_config, llm_client, app_state, server_id, status_tx,
)
.await
{
error!("DoT server error: {}", e);
}
});
task_registrar.register_server_task(server_id, handle).await;
Ok(local_addr)
}
async fn run(
listener: TcpListener,
tls_config: Arc<rustls::ServerConfig>,
llm_client: OllamaClient,
app_state: Arc<AppState>,
server_id: ServerId,
status_tx: mpsc::UnboundedSender<String>,
) -> Result<()> {
let acceptor = TlsAcceptor::from(tls_config);
let local_addr = listener
.local_addr()
.context("Failed to get DoT listener local address")?;
let limiter = crate::server::accept_bounded::ConnectionLimiter::new(MAX_CONNECTIONS);
loop {
match crate::server::accept_bounded::accept_bounded(
&listener,
&limiter,
CONNECTION_CAP_REFUSAL,
"DOT",
Some(&status_tx),
)
.await
{
Ok((stream, peer_addr, permit)) => {
Log::new(Some(&status_tx))
.debug(format!("DoT TCP connection from {}", peer_addr));
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();
app_state
.add_connection_to_server(
server_id,
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(),
},
)
.await;
}
let _ = status_tx.send("__UPDATE_UI__".to_string());
let acceptor = acceptor.clone();
let llm_client = llm_client.clone();
let conn_state = app_state.clone();
let app_state = app_state.clone();
let status_tx = status_tx.clone();
let handle = tokio::spawn(async move {
let _permit = permit;
if let Err(e) = Self::handle_connection(
stream,
peer_addr,
connection_id,
acceptor,
llm_client,
app_state,
server_id,
status_tx,
)
.await
{
error!("DoT connection error from {}: {}", peer_addr, e);
}
});
conn_state.register_server_task(server_id, handle).await;
}
Err(e) => {
Log::new(Some(&status_tx))
.warn(format!("Failed to accept DoT TCP connection: {}", e));
tokio::time::sleep(ACCEPT_ERROR_BACKOFF).await;
}
}
}
}
#[allow(clippy::too_many_arguments)]
async fn handle_connection(
stream: TcpStream,
peer_addr: SocketAddr,
connection_id: ConnectionId,
acceptor: TlsAcceptor,
llm_client: OllamaClient,
app_state: Arc<AppState>,
server_id: ServerId,
status_tx: mpsc::UnboundedSender<String>,
) -> Result<()> {
let outcome = Self::serve_connection(
stream,
peer_addr,
connection_id,
acceptor,
llm_client,
&app_state,
server_id,
&status_tx,
)
.await;
app_state
.close_connection_on_server(server_id, connection_id)
.await;
let _ = status_tx.send("__UPDATE_UI__".to_string());
outcome
}
#[allow(clippy::too_many_arguments)]
async fn serve_connection(
stream: TcpStream,
peer_addr: SocketAddr,
connection_id: ConnectionId,
acceptor: TlsAcceptor,
llm_client: OllamaClient,
app_state: &Arc<AppState>,
server_id: ServerId,
status_tx: &mpsc::UnboundedSender<String>,
) -> Result<()> {
let status_tx = status_tx.clone();
let app_state = app_state.clone();
let mut tls_stream = timeout(TLS_HANDSHAKE_TIMEOUT, acceptor.accept(stream))
.await
.map_err(|_| {
anyhow::anyhow!(
"TLS handshake with {peer_addr} did not complete within {:?}",
TLS_HANDSHAKE_TIMEOUT
)
})?
.context("TLS handshake failed")?;
Log::new(Some(&status_tx)).debug(format!("DoT TLS handshake complete with {}", peer_addr));
Log::new(Some(&status_tx)).info(format!("DoT connection from {}", peer_addr));
loop {
let mut len_buf = [0u8; 2];
let read_len =
match timeout(IDLE_READ_TIMEOUT, tls_stream.read_exact(&mut len_buf)).await {
Ok(r) => r,
Err(_) => {
Log::new(Some(&status_tx)).debug(format!(
"DoT connection from {} idle for {:?}, closing",
peer_addr, IDLE_READ_TIMEOUT
));
break;
}
};
match read_len {
Ok(_) => {}
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
Log::new(Some(&status_tx))
.debug(format!("DoT connection from {} closed", peer_addr));
break;
}
Err(e) => {
Log::new(Some(&status_tx))
.error(format!("Failed to read DoT length prefix: {}", e));
break;
}
}
let dns_len = u16::from_be_bytes(len_buf) as usize;
if dns_len == 0 {
Log::new(Some(&status_tx))
.warn(format!("Invalid DoT DNS message length: {}", dns_len));
break;
}
let mut dns_buf = vec![0u8; dns_len];
match timeout(MESSAGE_READ_TIMEOUT, tls_stream.read_exact(&mut dns_buf)).await {
Ok(Ok(_)) => {}
Ok(Err(e)) => {
Log::new(Some(&status_tx))
.error(format!("Failed to read DoT DNS message: {}", e));
break;
}
Err(_) => {
Log::new(Some(&status_tx)).warn(format!(
"DoT message from {peer_addr} incomplete after {MESSAGE_READ_TIMEOUT:?} \
decision=fail_closed_read_timeout"
));
break;
}
}
app_state
.update_connection_stats(
server_id,
connection_id,
Some((dns_len + 2) as u64),
None,
Some(1),
None,
)
.await;
Log::new(Some(&status_tx))
.debug(format!("DoT received {} bytes from {}", dns_len, peer_addr));
let dns_message = match DnsMessage::from_vec(&dns_buf) {
Ok(msg) => msg,
Err(e) => {
Log::new(Some(&status_tx))
.warn(format!("Failed to parse DoT DNS message: {}", e));
continue;
}
};
let queries = dns_message.queries();
if queries.is_empty() {
Log::new(Some(&status_tx)).warn("DoT DNS message has no queries");
continue;
}
let query = &queries[0];
let domain = query.name().to_utf8();
let query_type = query.query_type().to_string();
let query_id = dns_message.id();
Log::new(Some(&status_tx)).info(format!(
"DoT query: {} {} (ID: {})",
domain, query_type, query_id
));
Log::new(Some(&status_tx))
.trace(format!("DoT DNS query hex: {}", hex::encode(&dns_buf)));
let event = Event::new(
&DOT_QUERY_EVENT,
json!({
"query_id": query_id,
"domain": domain,
"query_type": query_type,
"peer_addr": peer_addr.to_string(),
}),
);
let protocol = Arc::new(DotProtocol::new());
Log::new(Some(&status_tx))
.debug(format!("DoT 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::new(Some(&status_tx)).info(format!("{}", message));
}
Log::new(Some(&status_tx)).debug(format!(
"DoT got {} protocol results",
execution_result.protocol_results.len()
));
for protocol_result in &execution_result.protocol_results {
use crate::llm::actions::protocol_trait::ActionResult;
match protocol_result {
ActionResult::Output(bytes) => {
Self::send_framed(
&mut tls_stream,
bytes,
peer_addr,
connection_id,
server_id,
&app_state,
&status_tx,
)
.await;
}
ActionResult::Custom { data, .. } => {
if let Some(output_data) =
data.get("output_data").and_then(|v| v.as_str())
{
if let Ok(response_bytes) = hex::decode(output_data) {
Self::send_framed(
&mut tls_stream,
&response_bytes,
peer_addr,
connection_id,
server_id,
&app_state,
&status_tx,
)
.await;
}
}
}
ActionResult::CloseConnection => {
Log::new(Some(&status_tx)).info(format!(
"DoT connection from {} closed by LLM",
peer_addr
));
return Ok(());
}
ActionResult::NoAction => {
Log::new(Some(&status_tx)).debug("DoT query ignored by LLM");
}
_ => {}
}
}
}
Err(e) => {
let decision = if crate::llm::is_overload_error(&e) {
"fail_closed_llm_overload"
} else {
"fail_closed_llm_error"
};
Log::new(Some(&status_tx)).warn(format!(
"DoT answering SERVFAIL to {} decision={}: {}",
peer_addr, decision, e
));
match crate::server::dns::actions::build_servfail(&dns_message) {
Ok(packet) => {
Self::send_framed(
&mut tls_stream,
&packet,
peer_addr,
connection_id,
server_id,
&app_state,
&status_tx,
)
.await;
}
Err(build_err) => {
Log::new(Some(&status_tx)).error(format!(
"DoT failed to build SERVFAIL for {}: {}",
peer_addr, build_err
));
}
}
continue;
}
}
}
Log::new(Some(&status_tx)).info(format!("DoT connection from {} closed", peer_addr));
Ok(())
}
#[allow(clippy::too_many_arguments)]
async fn send_framed(
tls_stream: &mut tokio_rustls::server::TlsStream<TcpStream>,
message: &[u8],
peer_addr: SocketAddr,
connection_id: ConnectionId,
server_id: ServerId,
app_state: &Arc<AppState>,
status_tx: &mpsc::UnboundedSender<String>,
) {
let Ok(len) = u16::try_from(message.len()) else {
Log::new(Some(status_tx)).error(format!(
"DoT refusing to send a {}-byte message to {}: RFC 7858 framing carries a \
16-bit length, so this cannot be put on the wire without desynchronising \
the connection",
message.len(),
peer_addr
));
return;
};
let mut framed = Vec::with_capacity(message.len() + 2);
framed.extend_from_slice(&len.to_be_bytes());
framed.extend_from_slice(message);
if let Err(e) = tls_stream.write_all(&framed).await {
Log::new(Some(status_tx)).error(format!(
"Failed to send DoT response to {}: {}",
peer_addr, e
));
return;
}
app_state
.update_connection_stats(
server_id,
connection_id,
None,
Some(framed.len() as u64),
None,
Some(1),
)
.await;
let log = Log::new(Some(status_tx));
log.debug(format!("DoT sent {} bytes to {}", message.len(), peer_addr));
log.trace(format!("DoT response hex: {}", hex::encode(message)));
}
}