pub mod actions;
pub mod packet;
pub use actions::NetbiosNsProtocol;
use crate::llm::action_helper::call_llm;
use crate::logging::emit::Log;
use crate::protocol::{Event, SpawnContext};
use crate::server::connection::ConnectionId;
use crate::state::app_state::AppState;
use crate::state::ServerId;
use anyhow::{Context, Result};
use std::net::SocketAddr;
use std::sync::Arc;
use tokio::net::UdpSocket;
use tokio::sync::mpsc;
use tracing::{debug, trace, warn};
use actions::{
RequestContext, DEFAULT_TTL_SECONDS, NETBIOS_NAME_QUERY_EVENT, NETBIOS_NAME_REGISTRATION_EVENT,
NETBIOS_NODE_STATUS_REQUEST_EVENT,
};
use packet::{NbnsRequest, NodeType};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Decision {
ModelAnswer,
ModelReject,
ModelSilent,
FailClosedNoAction,
FailClosedLlmError,
FailClosedActionError,
}
impl Decision {
pub fn as_str(&self) -> &'static str {
match self {
Decision::ModelAnswer => "model_answer",
Decision::ModelReject => "model_reject",
Decision::ModelSilent => "model_silent",
Decision::FailClosedNoAction => "fail_closed_no_action",
Decision::FailClosedLlmError => "fail_closed_llm_error",
Decision::FailClosedActionError => "fail_closed_action_error",
}
}
pub fn is_fail_closed(&self) -> bool {
matches!(
self,
Decision::FailClosedNoAction
| Decision::FailClosedLlmError
| Decision::FailClosedActionError
)
}
}
pub struct NetbiosNsServer;
impl NetbiosNsServer {
pub async fn spawn_with_llm_actions(ctx: SpawnContext) -> Result<SocketAddr> {
let listen_addr = ctx.legacy_listen_addr();
let SpawnContext {
llm_client,
state,
status_tx,
server_id,
startup_params,
..
} = ctx;
let (default_ttl, default_node_type) = match &startup_params {
None => (DEFAULT_TTL_SECONDS, NodeType::B),
Some(params) => {
let ttl = match params.get_optional_u64("default_ttl")? {
None => DEFAULT_TTL_SECONDS,
Some(n) => u32::try_from(n)
.context("default_ttl exceeds 32 bits; it is a number of seconds")?,
};
let node_type = match params.get_optional_string("node_type")? {
None => NodeType::B,
Some(s) => NodeType::parse(&s).with_context(|| {
format!("node_type must be one of b, p, m, h — got '{}'", s)
})?,
};
(ttl, node_type)
}
};
let socket = Arc::new(
UdpSocket::bind(listen_addr)
.await
.with_context(|| format!("NetBIOS-NS failed to bind {}", listen_addr))?,
);
let local_addr = socket.local_addr()?;
Log::new(Some(&status_tx)).info(format!(
"NetBIOS-NS server listening on {} (default_ttl={}s, node_type={})",
local_addr,
default_ttl,
default_node_type.as_str()
));
let task_registrar = state.clone();
let accept_handle = tokio::spawn(async move {
let mut buffer = vec![0u8; packet::MAX_DATAGRAM + 1];
loop {
let (n, peer_addr) = match socket.recv_from(&mut buffer).await {
Ok(pair) => pair,
Err(e) => {
Log::new(Some(&status_tx))
.error(format!("NetBIOS-NS receive error: {}", e));
break;
}
};
let data = buffer[..n].to_vec();
trace!(
"NetBIOS-NS {} bytes from {}: {}",
n,
peer_addr,
hex::encode(&data)
);
let request = match packet::parse_request(&data) {
Ok(r) => r,
Err(e) => {
Log::new(Some(&status_tx)).warn(format!(
"NetBIOS-NS dropped datagram from {}: {}",
peer_addr, e
));
continue;
}
};
let connection_id =
Self::record_connection(&state, server_id, local_addr, peer_addr, n).await;
let _ = status_tx.send("__UPDATE_UI__".to_string());
debug!(
"NetBIOS-NS {} {}<{:#04x}> type={} from {} trn_id={:#06x}",
packet::opcode_name(request.header.opcode()),
request.question_name.name,
request.question_name.suffix,
packet::qtype_name(request.qtype),
peer_addr,
request.header.trn_id
);
let llm = llm_client.clone();
let st = state.clone();
let tx = status_tx.clone();
let sock = socket.clone();
let handle = tokio::spawn(async move {
Self::handle_request(
request,
peer_addr,
connection_id,
sock,
llm,
st,
tx,
server_id,
default_ttl,
default_node_type,
)
.await;
});
state.register_server_task(server_id, handle).await;
}
});
task_registrar
.register_server_task(server_id, accept_handle)
.await;
Ok(local_addr)
}
async fn record_connection(
state: &Arc<AppState>,
server_id: ServerId,
local_addr: SocketAddr,
peer_addr: SocketAddr,
bytes: usize,
) -> ConnectionId {
use crate::state::server::{
ConnectionState as ServerConnectionState, ConnectionStatus, ProtocolConnectionInfo,
};
let connection_id = ConnectionId::new(state.get_next_unified_id().await);
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: bytes as u64,
packets_sent: 0,
packets_received: 1,
last_activity: now,
status: ConnectionStatus::Active,
status_changed_at: now,
protocol_info: ProtocolConnectionInfo::empty(),
};
state.add_connection_to_server(server_id, conn_state).await;
connection_id
}
#[allow(clippy::too_many_arguments)]
async fn handle_request(
request: NbnsRequest,
peer_addr: SocketAddr,
connection_id: ConnectionId,
socket: Arc<UdpSocket>,
llm_client: crate::llm::ollama_client::OllamaClient,
state: Arc<AppState>,
status_tx: mpsc::UnboundedSender<String>,
server_id: ServerId,
default_ttl: u32,
default_node_type: NodeType,
) {
let header = request.header;
let opcode = header.opcode();
let Some(event) = Self::event_for(&request, peer_addr) else {
debug!(
"NetBIOS-NS ignoring {} type={} from {}: not a request this server serves",
packet::opcode_name(opcode),
packet::qtype_name(request.qtype),
peer_addr
);
return;
};
let protocol = NetbiosNsProtocol::for_request(RequestContext {
trn_id: header.trn_id,
opcode,
recursion_desired: header.recursion_desired(),
name_field: request.question_name.raw.clone(),
question_name: request.question_name.name.clone(),
question_suffix: request.question_name.suffix,
default_ttl,
default_node_type,
});
let llm_outcome = call_llm(&llm_client, &state, server_id, None, &event, &protocol).await;
let (decision, mut reply) = Self::decide(llm_outcome, &status_tx, peer_addr);
let mut server_failure_sent = false;
if reply.is_none() && decision.is_fail_closed() {
reply = Self::server_failure_reply(&request);
server_failure_sent = reply.is_some();
}
let summary = format!(
"NetBIOS-NS {} {}<{:#04x}> from {} decision={}",
packet::opcode_name(opcode),
request.question_name.name,
request.question_name.suffix,
peer_addr,
decision.as_str()
);
let log = Log::new(Some(&status_tx));
if server_failure_sent {
log.error(format!(
"{} (answered SRV_ERR: the querier addressed this server as its NBNS, and \
SRV_ERR says the name server failed - it asserts nothing about the name)",
summary
));
} else if decision.is_fail_closed() {
log.error(format!(
"{} (nothing sent: no usable answer was produced, and a fabricated NetBIOS \
answer would be cached by the querier)",
summary
));
} else {
log.info(&summary);
}
let Some(reply) = reply else {
return;
};
match socket.send_to(&reply, peer_addr).await {
Ok(sent) => {
trace!(
"NetBIOS-NS sent {} bytes to {}: {}",
sent,
peer_addr,
hex::encode(&reply)
);
state
.update_connection_stats(
server_id,
connection_id,
None,
Some(sent as u64),
None,
Some(1),
)
.await;
let _ = status_tx.send("__UPDATE_UI__".to_string());
}
Err(e) => {
Log::new(Some(&status_tx)).error(format!(
"NetBIOS-NS failed to reply to {}: {}",
peer_addr, e
));
}
}
}
fn server_failure_reply(request: &NbnsRequest) -> Option<Vec<u8>> {
let header = request.header;
if !header.recursion_desired() || header.broadcast() {
return None;
}
let opcode = header.opcode();
let has_negative_form = matches!(
(opcode, request.qtype),
(packet::OPCODE_QUERY, packet::QTYPE_NB) | (packet::OPCODE_REGISTRATION, _)
);
if !has_negative_form {
return None;
}
packet::encode_negative_response(
header.trn_id,
opcode,
true,
&request.question_name.raw,
packet::RCODE_SRV_ERR,
)
.ok()
}
fn event_for(request: &NbnsRequest, peer_addr: SocketAddr) -> Option<Event> {
let name = &request.question_name;
let opcode = request.header.opcode();
match (opcode, request.qtype) {
(packet::OPCODE_QUERY, packet::QTYPE_NB) => Some(Event::new(
&NETBIOS_NAME_QUERY_EVENT,
serde_json::json!({
"name": name.name,
"suffix": name.suffix,
"question_type": packet::qtype_name(request.qtype),
"source_address": peer_addr.to_string(),
"transaction_id": request.header.trn_id,
}),
)),
(packet::OPCODE_QUERY, packet::QTYPE_NBSTAT) => Some(Event::new(
&NETBIOS_NODE_STATUS_REQUEST_EVENT,
serde_json::json!({
"name": name.name,
"suffix": name.suffix,
"source_address": peer_addr.to_string(),
"transaction_id": request.header.trn_id,
}),
)),
(packet::OPCODE_REGISTRATION, _) => {
let claimed = request.addresses.first();
Some(Event::new(
&NETBIOS_NAME_REGISTRATION_EVENT,
serde_json::json!({
"name": name.name,
"suffix": name.suffix,
"address": claimed.map(|a| a.address.to_string()),
"group": claimed.map(|a| a.is_group()).unwrap_or(false),
"source_address": peer_addr.to_string(),
"transaction_id": request.header.trn_id,
}),
))
}
_ => None,
}
}
fn decide(
llm_outcome: Result<crate::llm::ExecutionResult>,
status_tx: &mpsc::UnboundedSender<String>,
peer_addr: SocketAddr,
) -> (Decision, Option<Vec<u8>>) {
let log = Log::new(Some(status_tx));
let execution = match llm_outcome {
Ok(result) => {
for message in &result.messages {
log.info(message);
}
result
}
Err(e) => {
let category = match crate::utils::wire_failure::WireFailure::classify(&e) {
crate::utils::wire_failure::WireFailure::Overloaded => "overloaded",
crate::utils::wire_failure::WireFailure::Unavailable => "unavailable",
};
log.error(format!(
"NetBIOS-NS LLM call failed for {} (category={}): {}",
peer_addr, category, e
));
return (Decision::FailClosedLlmError, None);
}
};
let action_types: Vec<&str> = execution
.raw_actions
.iter()
.filter_map(|a| a.get("type").and_then(|v| v.as_str()))
.collect();
let mut outputs: Vec<Vec<u8>> = Vec::new();
for result in &execution.protocol_results {
outputs.extend(result.get_all_output());
}
if outputs.len() > 1 {
warn!(
"NetBIOS-NS ignored {} extra response(s) for {}; one request gets one datagram",
outputs.len() - 1,
peer_addr
);
}
if let Some(bytes) = outputs.into_iter().next() {
let decision = if action_types.contains(&"send_netbios_negative_response") {
Decision::ModelReject
} else {
Decision::ModelAnswer
};
return (decision, Some(bytes));
}
if action_types.contains(&"no_response") {
return (Decision::ModelSilent, None);
}
if execution.raw_actions.is_empty() {
return (Decision::FailClosedNoAction, None);
}
(Decision::FailClosedActionError, None)
}
}