netget/server/netbios_ns/mod.rs
1//! NetBIOS Name Service server (RFC 1001 / RFC 1002).
2//!
3//! UDP, default port 137. The model decides which NetBIOS names exist, what they resolve to,
4//! and what a node status listing says. This file owns the socket, the request decoding and —
5//! most importantly — the guarantee that **no decision means no datagram**.
6//!
7//! # Why silence, and not an error reply
8//!
9//! NBNS is a caching name service. A querier that receives a POSITIVE NAME QUERY RESPONSE
10//! stores the address for the TTL and uses it for every subsequent connection to that name, so
11//! a fabricated answer does not merely fail once: it redirects the peer's traffic for hours.
12//! NetGet therefore never synthesises an answer about a name. If the model is unreachable, or
13//! returns nothing usable, or its action fails to encode, this server sends **nothing** on a
14//! broadcast or node-directed request, and the querier falls back exactly as it would if no
15//! NBNS server were listening — which, on a broadcast query, is the normal behaviour of every
16//! node that does not hold the name.
17//!
18//! The one exception is a request addressed to this server *as its name server* (RD set, B
19//! clear): that gets a NEGATIVE response with RCODE SRV_ERR, the NBNS's own "I cannot process
20//! this", which asserts nothing about the name. See `server_failure_reply`.
21//!
22//! The distinction the wire mostly cannot carry lives in the log. Every request is logged
23//! with a `decision=` token so an operator can tell a refusal from an outage after the fact:
24//! `model_answer`, `model_reject`, `model_silent`, `fail_closed_no_action`,
25//! `fail_closed_llm_error`, `fail_closed_action_error`. This is the discipline `src/server/radius`
26//! established; outside the SRV_ERR case every fail-closed path is byte-for-byte identical to a
27//! dead server.
28
29pub mod actions;
30pub mod packet;
31
32pub use actions::NetbiosNsProtocol;
33
34use crate::llm::action_helper::call_llm;
35use crate::logging::emit::Log;
36use crate::protocol::{Event, SpawnContext};
37use crate::server::connection::ConnectionId;
38use crate::state::app_state::AppState;
39use crate::state::ServerId;
40use anyhow::{Context, Result};
41use std::net::SocketAddr;
42use std::sync::Arc;
43use tokio::net::UdpSocket;
44use tokio::sync::mpsc;
45use tracing::{debug, trace, warn};
46
47use actions::{
48 RequestContext, DEFAULT_TTL_SECONDS, NETBIOS_NAME_QUERY_EVENT, NETBIOS_NAME_REGISTRATION_EVENT,
49 NETBIOS_NODE_STATUS_REQUEST_EVENT,
50};
51use packet::{NbnsRequest, NodeType};
52
53/// How a request was disposed of.
54///
55/// The point of this enum is that "the model refused" and "the model could not be reached"
56/// produce *identical* silence on the wire, so they must not be allowed to collapse into one
57/// another in the log as well. That conflation is the OAuth2 defect recorded in the root
58/// `CLAUDE.md`; here it would mean an operator cannot tell a working deny-by-default server
59/// from a broken one.
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum Decision {
62 /// The model produced a response and it went on the wire.
63 ModelAnswer,
64 /// The model explicitly refused with `send_netbios_negative_response`.
65 ModelReject,
66 /// The model explicitly chose silence with `no_response`.
67 ModelSilent,
68 /// The model returned no actions at all. Nothing is sent.
69 FailClosedNoAction,
70 /// The LLM call itself failed. Nothing is sent.
71 FailClosedLlmError,
72 /// The model's action could not be encoded. Nothing is sent.
73 FailClosedActionError,
74}
75
76impl Decision {
77 /// Stable, grep-able token. `decision=fail_closed_` finds every request the model did not
78 /// actually answer.
79 pub fn as_str(&self) -> &'static str {
80 match self {
81 Decision::ModelAnswer => "model_answer",
82 Decision::ModelReject => "model_reject",
83 Decision::ModelSilent => "model_silent",
84 Decision::FailClosedNoAction => "fail_closed_no_action",
85 Decision::FailClosedLlmError => "fail_closed_llm_error",
86 Decision::FailClosedActionError => "fail_closed_action_error",
87 }
88 }
89
90 /// True when the server, not the model, decided to say nothing.
91 pub fn is_fail_closed(&self) -> bool {
92 matches!(
93 self,
94 Decision::FailClosedNoAction
95 | Decision::FailClosedLlmError
96 | Decision::FailClosedActionError
97 )
98 }
99}
100
101pub struct NetbiosNsServer;
102
103impl NetbiosNsServer {
104 /// Bind the socket and start serving.
105 ///
106 /// Awaits the bind and returns `Err` on failure so `server_startup` reports
107 /// `ServerStatus::Error` rather than leaving a server that claims to be up while holding
108 /// no socket.
109 pub async fn spawn_with_llm_actions(ctx: SpawnContext) -> Result<SocketAddr> {
110 let listen_addr = ctx.legacy_listen_addr();
111 let SpawnContext {
112 llm_client,
113 state,
114 status_tx,
115 server_id,
116 startup_params,
117 ..
118 } = ctx;
119
120 // Both parameters are optional and both are really read; a declared knob that turns
121 // nothing is the `startup_param_drift_test` defect.
122 let (default_ttl, default_node_type) = match &startup_params {
123 None => (DEFAULT_TTL_SECONDS, NodeType::B),
124 Some(params) => {
125 let ttl = match params.get_optional_u64("default_ttl")? {
126 None => DEFAULT_TTL_SECONDS,
127 Some(n) => u32::try_from(n)
128 .context("default_ttl exceeds 32 bits; it is a number of seconds")?,
129 };
130 let node_type = match params.get_optional_string("node_type")? {
131 None => NodeType::B,
132 Some(s) => NodeType::parse(&s).with_context(|| {
133 format!("node_type must be one of b, p, m, h — got '{}'", s)
134 })?,
135 };
136 (ttl, node_type)
137 }
138 };
139
140 let socket = Arc::new(
141 UdpSocket::bind(listen_addr)
142 .await
143 .with_context(|| format!("NetBIOS-NS failed to bind {}", listen_addr))?,
144 );
145 let local_addr = socket.local_addr()?;
146
147 Log::new(Some(&status_tx)).info(format!(
148 "NetBIOS-NS server listening on {} (default_ttl={}s, node_type={})",
149 local_addr,
150 default_ttl,
151 default_node_type.as_str()
152 ));
153
154 let task_registrar = state.clone();
155 let accept_handle = tokio::spawn(async move {
156 // One octet over the RFC 1002 §4.1 limit, so an over-long datagram is detected
157 // rather than silently truncated into something that happens to parse.
158 let mut buffer = vec![0u8; packet::MAX_DATAGRAM + 1];
159
160 loop {
161 let (n, peer_addr) = match socket.recv_from(&mut buffer).await {
162 Ok(pair) => pair,
163 Err(e) => {
164 Log::new(Some(&status_tx))
165 .error(format!("NetBIOS-NS receive error: {}", e));
166 break;
167 }
168 };
169
170 let data = buffer[..n].to_vec();
171 trace!(
172 "NetBIOS-NS {} bytes from {}: {}",
173 n,
174 peer_addr,
175 hex::encode(&data)
176 );
177
178 let request = match packet::parse_request(&data) {
179 Ok(r) => r,
180 Err(e) => {
181 // A datagram we cannot parse gets no reply. RFC 1002 defines an
182 // FMT_ERR rcode, but building any response needs the question's NAME
183 // field, which is exactly the part that failed to parse — and
184 // answering unparsed input from a spoofable transport is how a
185 // reflector is built.
186 Log::new(Some(&status_tx)).warn(format!(
187 "NetBIOS-NS dropped datagram from {}: {}",
188 peer_addr, e
189 ));
190 continue;
191 }
192 };
193
194 let connection_id =
195 Self::record_connection(&state, server_id, local_addr, peer_addr, n).await;
196 let _ = status_tx.send("__UPDATE_UI__".to_string());
197
198 debug!(
199 "NetBIOS-NS {} {}<{:#04x}> type={} from {} trn_id={:#06x}",
200 packet::opcode_name(request.header.opcode()),
201 request.question_name.name,
202 request.question_name.suffix,
203 packet::qtype_name(request.qtype),
204 peer_addr,
205 request.header.trn_id
206 );
207
208 let llm = llm_client.clone();
209 let st = state.clone();
210 let tx = status_tx.clone();
211 let sock = socket.clone();
212
213 let handle = tokio::spawn(async move {
214 Self::handle_request(
215 request,
216 peer_addr,
217 connection_id,
218 sock,
219 llm,
220 st,
221 tx,
222 server_id,
223 default_ttl,
224 default_node_type,
225 )
226 .await;
227 });
228
229 // Every spawned task is registered, not just the accept loop: aborting a task
230 // does not abort what it spawned, so an in-flight LLM call would otherwise
231 // outlive `stop_server` and could still write to the socket. `register_server_task`
232 // prunes finished handles on each call, so this does not grow without bound.
233 state.register_server_task(server_id, handle).await;
234 }
235 });
236
237 task_registrar
238 .register_server_task(server_id, accept_handle)
239 .await;
240
241 Ok(local_addr)
242 }
243
244 /// Record the datagram against a per-remote-address connection entry.
245 ///
246 /// The protocol declares `.connectionless()`, so these entries are reaped by
247 /// `AppState::cleanup_old_connections` after ten idle seconds — nothing else would ever
248 /// close them.
249 ///
250 /// Returns the id so the reply can be counted against the request that provoked it: the
251 /// rail's `↑` counter reads `bytes_sent`, and an entry that only ever counts the inbound
252 /// side shows every answered query as a datagram received and nothing sent.
253 async fn record_connection(
254 state: &Arc<AppState>,
255 server_id: ServerId,
256 local_addr: SocketAddr,
257 peer_addr: SocketAddr,
258 bytes: usize,
259 ) -> ConnectionId {
260 use crate::state::server::{
261 ConnectionState as ServerConnectionState, ConnectionStatus, ProtocolConnectionInfo,
262 };
263 let connection_id = ConnectionId::new(state.get_next_unified_id().await);
264 let now = crate::utils::clock::Instant::now();
265 let conn_state = ServerConnectionState {
266 id: connection_id,
267 remote_addr: peer_addr,
268 local_addr,
269 bytes_sent: 0,
270 bytes_received: bytes as u64,
271 packets_sent: 0,
272 packets_received: 1,
273 last_activity: now,
274 status: ConnectionStatus::Active,
275 status_changed_at: now,
276 protocol_info: ProtocolConnectionInfo::empty(),
277 };
278 state.add_connection_to_server(server_id, conn_state).await;
279 connection_id
280 }
281
282 /// Raise the event, ask the model, then apply the silence rule.
283 #[allow(clippy::too_many_arguments)]
284 async fn handle_request(
285 request: NbnsRequest,
286 peer_addr: SocketAddr,
287 connection_id: ConnectionId,
288 socket: Arc<UdpSocket>,
289 llm_client: crate::llm::ollama_client::OllamaClient,
290 state: Arc<AppState>,
291 status_tx: mpsc::UnboundedSender<String>,
292 server_id: ServerId,
293 default_ttl: u32,
294 default_node_type: NodeType,
295 ) {
296 let header = request.header;
297 let opcode = header.opcode();
298
299 let Some(event) = Self::event_for(&request, peer_addr) else {
300 // Releases, refreshes and anything else this server does not serve are dropped.
301 // Answering an opcode we have no event for would mean answering without ever
302 // asking the model.
303 debug!(
304 "NetBIOS-NS ignoring {} type={} from {}: not a request this server serves",
305 packet::opcode_name(opcode),
306 packet::qtype_name(request.qtype),
307 peer_addr
308 );
309 return;
310 };
311
312 let protocol = NetbiosNsProtocol::for_request(RequestContext {
313 trn_id: header.trn_id,
314 opcode,
315 recursion_desired: header.recursion_desired(),
316 name_field: request.question_name.raw.clone(),
317 question_name: request.question_name.name.clone(),
318 question_suffix: request.question_name.suffix,
319 default_ttl,
320 default_node_type,
321 });
322
323 let llm_outcome = call_llm(&llm_client, &state, server_id, None, &event, &protocol).await;
324
325 let (decision, mut reply) = Self::decide(llm_outcome, &status_tx, peer_addr);
326 let mut server_failure_sent = false;
327 if reply.is_none() && decision.is_fail_closed() {
328 reply = Self::server_failure_reply(&request);
329 server_failure_sent = reply.is_some();
330 }
331
332 let summary = format!(
333 "NetBIOS-NS {} {}<{:#04x}> from {} decision={}",
334 packet::opcode_name(opcode),
335 request.question_name.name,
336 request.question_name.suffix,
337 peer_addr,
338 decision.as_str()
339 );
340 let log = Log::new(Some(&status_tx));
341 if server_failure_sent {
342 log.error(format!(
343 "{} (answered SRV_ERR: the querier addressed this server as its NBNS, and \
344 SRV_ERR says the name server failed - it asserts nothing about the name)",
345 summary
346 ));
347 } else if decision.is_fail_closed() {
348 // Loud, because on the wire this is indistinguishable from the server being down.
349 log.error(format!(
350 "{} (nothing sent: no usable answer was produced, and a fabricated NetBIOS \
351 answer would be cached by the querier)",
352 summary
353 ));
354 } else {
355 log.info(&summary);
356 }
357
358 let Some(reply) = reply else {
359 return;
360 };
361
362 match socket.send_to(&reply, peer_addr).await {
363 Ok(sent) => {
364 trace!(
365 "NetBIOS-NS sent {} bytes to {}: {}",
366 sent,
367 peer_addr,
368 hex::encode(&reply)
369 );
370 state
371 .update_connection_stats(
372 server_id,
373 connection_id,
374 None,
375 Some(sent as u64),
376 None,
377 Some(1),
378 )
379 .await;
380 let _ = status_tx.send("__UPDATE_UI__".to_string());
381 }
382 Err(e) => {
383 Log::new(Some(&status_tx)).error(format!(
384 "NetBIOS-NS failed to reply to {}: {}",
385 peer_addr, e
386 ));
387 }
388 }
389 }
390
391 /// The one reply this server writes without the model: a NEGATIVE response carrying
392 /// RCODE SRV_ERR, for a request that addressed this server **as its name server**.
393 ///
394 /// RFC 1002 §4.2.1.1: the RD flag "may only be set on a request to the NBNS". A request
395 /// with RD set and B clear is a querier asking its configured NBNS, and RFC 1002 gives that
396 /// server a word for "I cannot process this": SRV_ERR (§4.2.14 for a query, §4.2.6 for a
397 /// registration) — "the NBNS has a problem and is unable to process the query". It is
398 /// the NBNS's SERVFAIL. It asserts nothing about the name, which is what separates it from
399 /// NAM_ERR and from a positive answer, and it saves the querier its retransmission
400 /// timeouts before it falls back to its next resolution method.
401 ///
402 /// Everything else stays silent:
403 /// - a **broadcast** query (B set) is answered only by the node that owns the name; a
404 /// bystander answering anything, even an error, races the owner;
405 /// - a query **without RD** is addressed to a node, not a name server, and a node that
406 /// does not hold the name says nothing;
407 /// - a **node status** request has no negative form at all (§4.2.18 defines only the
408 /// positive listing).
409 fn server_failure_reply(request: &NbnsRequest) -> Option<Vec<u8>> {
410 let header = request.header;
411 if !header.recursion_desired() || header.broadcast() {
412 return None;
413 }
414 let opcode = header.opcode();
415 let has_negative_form = matches!(
416 (opcode, request.qtype),
417 (packet::OPCODE_QUERY, packet::QTYPE_NB) | (packet::OPCODE_REGISTRATION, _)
418 );
419 if !has_negative_form {
420 return None;
421 }
422 packet::encode_negative_response(
423 header.trn_id,
424 opcode,
425 true,
426 &request.question_name.raw,
427 packet::RCODE_SRV_ERR,
428 )
429 .ok()
430 }
431
432 /// Which event, if any, a decoded request raises.
433 fn event_for(request: &NbnsRequest, peer_addr: SocketAddr) -> Option<Event> {
434 let name = &request.question_name;
435 let opcode = request.header.opcode();
436
437 match (opcode, request.qtype) {
438 (packet::OPCODE_QUERY, packet::QTYPE_NB) => Some(Event::new(
439 &NETBIOS_NAME_QUERY_EVENT,
440 serde_json::json!({
441 "name": name.name,
442 "suffix": name.suffix,
443 "question_type": packet::qtype_name(request.qtype),
444 "source_address": peer_addr.to_string(),
445 "transaction_id": request.header.trn_id,
446 }),
447 )),
448 (packet::OPCODE_QUERY, packet::QTYPE_NBSTAT) => Some(Event::new(
449 &NETBIOS_NODE_STATUS_REQUEST_EVENT,
450 serde_json::json!({
451 "name": name.name,
452 "suffix": name.suffix,
453 "source_address": peer_addr.to_string(),
454 "transaction_id": request.header.trn_id,
455 }),
456 )),
457 (packet::OPCODE_REGISTRATION, _) => {
458 let claimed = request.addresses.first();
459 Some(Event::new(
460 &NETBIOS_NAME_REGISTRATION_EVENT,
461 serde_json::json!({
462 "name": name.name,
463 "suffix": name.suffix,
464 "address": claimed.map(|a| a.address.to_string()),
465 "group": claimed.map(|a| a.is_group()).unwrap_or(false),
466 "source_address": peer_addr.to_string(),
467 "transaction_id": request.header.trn_id,
468 }),
469 ))
470 }
471 _ => None,
472 }
473 }
474
475 /// **The silence rule.**
476 ///
477 /// Returns what happened and the bytes to send, if any. There is no branch that
478 /// synthesises a response: every `Vec<u8>` returned here came out of an action the model
479 /// named.
480 fn decide(
481 llm_outcome: Result<crate::llm::ExecutionResult>,
482 status_tx: &mpsc::UnboundedSender<String>,
483 peer_addr: SocketAddr,
484 ) -> (Decision, Option<Vec<u8>>) {
485 let log = Log::new(Some(status_tx));
486
487 let execution = match llm_outcome {
488 Ok(result) => {
489 for message in &result.messages {
490 log.info(message);
491 }
492 result
493 }
494 Err(e) => {
495 // The category is worth having even though the wire cannot carry it: an
496 // overloaded backend and a dead one call for different operator responses.
497 // The error itself is logged here and never leaves the process.
498 let category = match crate::utils::wire_failure::WireFailure::classify(&e) {
499 crate::utils::wire_failure::WireFailure::Overloaded => "overloaded",
500 crate::utils::wire_failure::WireFailure::Unavailable => "unavailable",
501 };
502 log.error(format!(
503 "NetBIOS-NS LLM call failed for {} (category={}): {}",
504 peer_addr, category, e
505 ));
506 return (Decision::FailClosedLlmError, None);
507 }
508 };
509
510 let action_types: Vec<&str> = execution
511 .raw_actions
512 .iter()
513 .filter_map(|a| a.get("type").and_then(|v| v.as_str()))
514 .collect();
515
516 let mut outputs: Vec<Vec<u8>> = Vec::new();
517 for result in &execution.protocol_results {
518 outputs.extend(result.get_all_output());
519 }
520
521 if outputs.len() > 1 {
522 warn!(
523 "NetBIOS-NS ignored {} extra response(s) for {}; one request gets one datagram",
524 outputs.len() - 1,
525 peer_addr
526 );
527 }
528
529 if let Some(bytes) = outputs.into_iter().next() {
530 let decision = if action_types.contains(&"send_netbios_negative_response") {
531 Decision::ModelReject
532 } else {
533 Decision::ModelAnswer
534 };
535 return (decision, Some(bytes));
536 }
537
538 // Nothing to send. Which of the three silences is it?
539 if action_types.contains(&"no_response") {
540 // The model was asked and chose to say nothing. That is a real answer here, and
541 // must not be reported as a failure.
542 return (Decision::ModelSilent, None);
543 }
544 if execution.raw_actions.is_empty() {
545 return (Decision::FailClosedNoAction, None);
546 }
547 (Decision::FailClosedActionError, None)
548 }
549}