netget/server/llmnr/mod.rs
1//! LLMNR (RFC 4795) responder.
2//!
3//! LLMNR is DNS's message format on a link-local multicast group, with one rule that inverts
4//! everything the DNS server in this repo does: **a responder answers only for the names it
5//! owns, and says nothing at all for anything else.** RFC 4795 §2.1.1 spells out why NXDOMAIN
6//! is not available — "LLMNR responders MUST NOT respond with an RCODE of 3; instead, they
7//! should not respond at all" — because on a shared link the host that *does* own the name
8//! still has to be able to answer, and a negative answer from a bystander would race it.
9//!
10//! That makes this protocol a member of the deliberately-silent class the root `CLAUDE.md`
11//! catalogues, and its case is the strongest of them. An LLMNR response is not a message the
12//! peer reads and discards: it is a name-to-address binding written straight into the
13//! querier's resolver cache. **A fabricated answer is cache poisoning.** So when the model
14//! cannot be reached, or answers with nothing usable, this server writes zero bytes — and puts
15//! the distinction in the log instead, exactly as `src/server/radius/` does, so an operator can
16//! tell a refusal from an outage after the fact.
17//!
18//! See `src/server/llmnr/CLAUDE.md` for the design notes, the RFC deviations and what is
19//! unverified.
20
21pub mod actions;
22
23use crate::llm::action_helper::call_llm;
24use crate::llm::ollama_client::OllamaClient;
25use crate::logging::emit::Log;
26use crate::protocol::{Event, SpawnContext};
27use crate::server::connection::ConnectionId;
28use crate::state::app_state::AppState;
29use crate::state::ServerId;
30use actions::{LlmnrProtocol, LLMNR_IPV4_GROUP, LLMNR_IPV6_GROUP, LLMNR_QUERY_EVENT};
31use anyhow::{Context, Result};
32use hickory_proto::op::{Message as DnsMessage, MessageType, OpCode};
33use std::net::{IpAddr, Ipv4Addr, SocketAddr};
34use std::sync::Arc;
35use tokio::io::{AsyncReadExt, AsyncWriteExt};
36use tokio::net::{TcpListener, TcpStream, UdpSocket};
37use tokio::sync::mpsc;
38use tracing::{debug, error, info, trace, warn};
39
40/// Largest LLMNR message this server will read.
41///
42/// LLMNR has no EDNS0 negotiation, but the DNS message format allows up to 64 KiB over TCP.
43/// 4 KiB covers every query a name resolver produces and bounds a hostile datagram.
44pub const MAX_MESSAGE_LEN: usize = 4096;
45
46/// How long a TCP querier may hold a connection open without sending anything.
47///
48/// RFC 4795 §2.4's TCP path is a single question and a single answer; there is no session to
49/// keep alive. Without a deadline, a peer that connects and then says nothing parks a task
50/// that `register_server_task` can never prune, because it never finishes — so N idle
51/// connections cost N permanent tasks and N file descriptors for a protocol that has no
52/// concept of an idle connection. Thirty seconds is far longer than any real querier waits
53/// and short enough that the cost of a stalled peer is bounded.
54const TCP_IDLE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
55
56/// Concurrent TCP connections this responder admits before it starts refusing.
57///
58/// [`crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS`]. [`TCP_IDLE_TIMEOUT`] bounds how
59/// long one silent querier holds a slot; only a cap bounds how many of them there can be at
60/// once, and thirty seconds times an unbounded arrival rate is still unbounded.
61///
62/// The house default is *generous* here rather than tight, and deliberately so. LLMNR's TCP
63/// path is the rare one — RFC 4795 §2.4 reaches for it only for a unicast query or a response
64/// that would not fit a datagram — so this listener should normally have a handful of
65/// connections at most, and a number like 16 would be defensible on traffic alone. It is not
66/// defensible on *failure*: the cap's job is to bound what a stranger can pin, and a low cap
67/// on a rarely-used side channel is a cheap way for that stranger to deny the TCP path to the
68/// legitimate querier who actually needed it. 256 bounded connections is still bounded, and it
69/// does not turn the cap into the outage.
70const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
71
72/// A peer over [`MAX_CONNECTIONS`] gets a **plain close**, with nothing written.
73///
74/// LLMNR's only server message is a DNS response, and every DNS response is an answer to a
75/// question it must echo back: RFC 4795 §2.4 requires the responder to copy the query's ID and
76/// its question section, and §2.1 has the querier silently discard anything whose ID and
77/// question do not match one it has outstanding. A peer refused at the accept has sent no
78/// query, so a fabricated response would be dropped as unmatched — the querier would learn
79/// nothing and wait out its own timeout, which is strictly worse than the immediate EOF it
80/// gets here.
81///
82/// Silence is also what this protocol does everywhere else: RFC 4795 has responders *silently*
83/// discard queries they cannot or must not answer, so an LLMNR querier already treats "nothing
84/// came back" as the normal negative outcome and falls back to its next resolution mechanism.
85/// The refusal is logged with `decision=fail_closed_connection_cap`, which is the diagnosis.
86const CONNECTION_CAP_REFUSAL: &[u8] = b"";
87
88/// How this responder arrived at what it did (or did not) put on the wire.
89///
90/// The whole point of this enum is that **"the model said stay silent" and "the model could
91/// not be reached" produce identical bytes on the wire — nothing — and must never be
92/// identical in the log.** That collapse is the OAuth2 failure the root `CLAUDE.md` records,
93/// and here it would hide a backend outage as protocol-correct silence forever.
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub enum Decision {
96 /// The model answered with a record. Bytes went out.
97 ModelResponse,
98 /// The model chose `no_response`: this host does not own the name. Correct silence.
99 ModelSilent,
100 /// The model chose `send_llmnr_error`, i.e. an explicit non-zero RCODE refusal.
101 ModelReject,
102 /// The model's `send_llmnr_error` could not be sent because the query arrived over UDP,
103 /// where RFC 4795 §2.1.1 requires RCODE 0. Silence, and the model's intent recorded.
104 ModelRejectSuppressed,
105 /// The model returned no usable action at all. The server stays silent.
106 FailClosedNoAction,
107 /// The model produced actions but none of them encoded. The server stays silent.
108 FailClosedActionError,
109 /// The LLM call itself failed — outage, overload, unusable output. The server stays silent.
110 FailClosedLlmError,
111}
112
113impl Decision {
114 /// Stable, grep-able token. An operator looking for requests the model did not actually
115 /// answer greps `decision=fail_closed_`.
116 pub fn as_str(&self) -> &'static str {
117 match self {
118 Decision::ModelResponse => "model_response",
119 Decision::ModelSilent => "model_silent",
120 Decision::ModelReject => "model_reject",
121 Decision::ModelRejectSuppressed => "model_reject_suppressed_udp",
122 Decision::FailClosedNoAction => "fail_closed_no_action",
123 Decision::FailClosedActionError => "fail_closed_action_error",
124 Decision::FailClosedLlmError => "fail_closed_llm_error",
125 }
126 }
127
128 /// True when the *server*, not the model, chose to say nothing.
129 pub fn is_fail_closed(&self) -> bool {
130 matches!(
131 self,
132 Decision::FailClosedNoAction
133 | Decision::FailClosedActionError
134 | Decision::FailClosedLlmError
135 )
136 }
137}
138
139/// Which transport a query arrived on. It decides whether a non-zero RCODE may be returned.
140#[derive(Debug, Clone, Copy, PartialEq, Eq)]
141enum Transport {
142 Udp,
143 Tcp,
144}
145
146impl Transport {
147 fn as_str(self) -> &'static str {
148 match self {
149 Transport::Udp => "udp",
150 Transport::Tcp => "tcp",
151 }
152 }
153}
154
155/// Everything a query handler needs, gathered so the handler is not an eight-argument
156/// function repeated on two code paths.
157#[derive(Clone)]
158struct Responder {
159 llm_client: OllamaClient,
160 state: Arc<AppState>,
161 status_tx: mpsc::UnboundedSender<String>,
162 server_id: ServerId,
163 local_addr: SocketAddr,
164}
165
166pub struct LlmnrServer;
167
168impl LlmnrServer {
169 /// Bind the responder and start serving.
170 ///
171 /// Returns `Err` if the UDP socket cannot be bound, so `server_startup` marks the instance
172 /// `ServerStatus::Error` rather than showing a responder that is not listening. The two
173 /// optional extras — the multicast group join and the TCP listener — are best-effort and
174 /// logged: neither is required to answer a query sent straight to this port, and on macOS
175 /// a loopback multicast join routinely fails for reasons that have nothing to do with
176 /// whether the server works.
177 pub async fn spawn_with_llm_actions(ctx: SpawnContext) -> Result<SocketAddr> {
178 let listen_addr = ctx.legacy_listen_addr();
179 let SpawnContext {
180 llm_client,
181 state,
182 status_tx,
183 server_id,
184 startup_params,
185 ..
186 } = ctx;
187
188 // Startup parameters. Every accessor is fallible because the values come from the
189 // model or an MCP client; propagate with `?` so an undeclared key or a wrong type
190 // produces a clean startup error instead of a panic in the spawning task.
191 let (join_multicast, multicast_interface, enable_tcp) = match startup_params {
192 Some(params) => (
193 params.get_optional_bool("join_multicast")?.unwrap_or(true),
194 params.get_optional_string("multicast_interface")?,
195 params.get_optional_bool("enable_tcp")?.unwrap_or(true),
196 ),
197 None => (true, None, true),
198 };
199
200 let socket = Arc::new(
201 UdpSocket::bind(listen_addr)
202 .await
203 .with_context(|| format!("LLMNR failed to bind UDP {}", listen_addr))?,
204 );
205 let local_addr = socket
206 .local_addr()
207 .context("LLMNR could not read its own UDP address")?;
208
209 let log = Log::new(Some(&status_tx));
210 log.info(format!("LLMNR responder listening on {}", local_addr));
211 info!("LLMNR responder listening on UDP {}", local_addr);
212
213 if join_multicast {
214 join_llmnr_group(
215 &socket,
216 local_addr,
217 multicast_interface.as_deref(),
218 &status_tx,
219 );
220 } else {
221 debug!("LLMNR multicast join skipped (join_multicast=false)");
222 }
223
224 let responder = Responder {
225 llm_client,
226 state: state.clone(),
227 status_tx: status_tx.clone(),
228 server_id,
229 local_addr,
230 };
231
232 // UDP receive loop.
233 let udp_responder = responder.clone();
234 let udp_socket = socket.clone();
235 let udp_handle = tokio::spawn(async move {
236 let mut buffer = vec![0u8; MAX_MESSAGE_LEN];
237 loop {
238 let (n, peer_addr) = match udp_socket.recv_from(&mut buffer).await {
239 Ok(pair) => pair,
240 Err(e) => {
241 Log::new(Some(&udp_responder.status_tx))
242 .error(format!("LLMNR UDP receive error: {}", e));
243 break;
244 }
245 };
246
247 let data = buffer[..n].to_vec();
248 let sink = UdpSink {
249 socket: udp_socket.clone(),
250 peer: peer_addr,
251 };
252 let r = udp_responder.clone();
253 let task = tokio::spawn(async move {
254 r.handle_query(data, peer_addr, Transport::Udp, Some(sink))
255 .await;
256 });
257 // Register even these short-lived tasks: `register_server_task` prunes the
258 // finished ones on every call, so the list stays bounded, and `stop_server`
259 // cancels an in-flight LLM call instead of letting it answer after the socket
260 // is gone. Aborting the accept loop alone does not abort what it spawned.
261 udp_responder
262 .state
263 .register_server_task(server_id, task)
264 .await;
265 }
266 });
267 state.register_server_task(server_id, udp_handle).await;
268
269 // TCP listener. RFC 4795 §2.4 requires responders to support TCP queries, and it is
270 // the only transport on which a non-zero RCODE may be returned. Best-effort: the UDP
271 // responder is the protocol's main path and must not be lost to a port collision on
272 // the TCP side (the ephemeral port UDP was given may already be held by a TCP socket).
273 if enable_tcp {
274 match TcpListener::bind(SocketAddr::new(local_addr.ip(), local_addr.port())).await {
275 Ok(listener) => {
276 info!("LLMNR also listening on TCP {}", local_addr);
277 let tcp_responder = responder.clone();
278 let limiter =
279 crate::server::accept_bounded::ConnectionLimiter::new(MAX_CONNECTIONS);
280 let tcp_handle = tokio::spawn(async move {
281 loop {
282 let (stream, peer_addr, permit) =
283 match crate::server::accept_bounded::accept_bounded(
284 &listener,
285 &limiter,
286 CONNECTION_CAP_REFUSAL,
287 "LLMNR",
288 Some(&tcp_responder.status_tx),
289 )
290 .await
291 {
292 Ok(triple) => triple,
293 Err(e) => {
294 Log::new(Some(&tcp_responder.status_tx))
295 .error(format!("LLMNR TCP accept error: {}", e));
296 break;
297 }
298 };
299 let r = tcp_responder.clone();
300 let task = tokio::spawn(async move {
301 // Released when this task ends, and this task is the whole of
302 // the connection — `serve_tcp_connection` spawns nothing — so
303 // `MAX_CONNECTIONS` caps live connections rather than accepts.
304 let _permit = permit;
305 r.serve_tcp_connection(stream, peer_addr).await;
306 });
307 tcp_responder
308 .state
309 .register_server_task(server_id, task)
310 .await;
311 }
312 });
313 state.register_server_task(server_id, tcp_handle).await;
314 }
315 Err(e) => {
316 // Not fatal, but not hidden either: a responder that silently lacks TCP
317 // cannot serve the queries RFC 4795 §2.4 sends over TCP, and the operator
318 // should know which half is missing.
319 warn!(
320 "LLMNR could not bind TCP {}: {}; UDP queries are still answered, \
321 TCP queries are not",
322 local_addr, e
323 );
324 log.warn(format!(
325 "LLMNR TCP listener unavailable on {} ({}); UDP only",
326 local_addr, e
327 ));
328 }
329 }
330 } else {
331 debug!("LLMNR TCP listener disabled (enable_tcp=false)");
332 }
333
334 Ok(local_addr)
335 }
336}
337
338/// Where a UDP response goes back to. `None` means TCP, whose reply is written by the
339/// connection loop because that is what owns the stream.
340struct UdpSink {
341 socket: Arc<UdpSocket>,
342 peer: SocketAddr,
343}
344
345impl Responder {
346 /// Serve one TCP connection.
347 ///
348 /// RFC 4795 §2.4 carries unicast queries over TCP with the RFC 1035 §4.2.2 two-byte
349 /// length prefix, and requires the response to go back on the same connection.
350 async fn serve_tcp_connection(&self, mut stream: TcpStream, peer_addr: SocketAddr) {
351 loop {
352 let mut len_buf = [0u8; 2];
353 // Every read is bounded. A peer that opens a connection and then goes quiet —
354 // between frames, or half way through one — must not hold this task forever; see
355 // TCP_IDLE_TIMEOUT.
356 match tokio::time::timeout(TCP_IDLE_TIMEOUT, stream.read_exact(&mut len_buf)).await {
357 Ok(Ok(_)) => {}
358 // EOF or a half-written frame: the querier is done. Not an error.
359 Ok(Err(_)) => break,
360 Err(_) => {
361 debug!(
362 "LLMNR closing an idle TCP connection from {} after {:?}",
363 peer_addr, TCP_IDLE_TIMEOUT
364 );
365 break;
366 }
367 }
368 let len = u16::from_be_bytes(len_buf) as usize;
369 if len == 0 || len > MAX_MESSAGE_LEN {
370 warn!(
371 "LLMNR dropping TCP query from {}: length prefix {} is out of range",
372 peer_addr, len
373 );
374 break;
375 }
376
377 let mut data = vec![0u8; len];
378 match tokio::time::timeout(TCP_IDLE_TIMEOUT, stream.read_exact(&mut data)).await {
379 Ok(Ok(_)) => {}
380 Ok(Err(_)) => break,
381 Err(_) => {
382 warn!(
383 "LLMNR closing a TCP connection from {}: {} octets were announced but \
384 did not arrive within {:?}",
385 peer_addr, len, TCP_IDLE_TIMEOUT
386 );
387 break;
388 }
389 }
390
391 let outcome = self
392 .handle_query(data, peer_addr, Transport::Tcp, None)
393 .await;
394
395 let Some((reply, connection_id)) = outcome else {
396 continue;
397 };
398 let mut framed = Vec::with_capacity(reply.len() + 2);
399 // A message longer than u16::MAX cannot be framed; the encoder never produces
400 // one from a single answer record, but truncating the prefix would desynchronise
401 // the connection permanently rather than fail visibly.
402 let Ok(prefix) = u16::try_from(reply.len()) else {
403 error!(
404 "LLMNR TCP response to {} is {} bytes, too long to frame",
405 peer_addr,
406 reply.len()
407 );
408 break;
409 };
410 framed.extend_from_slice(&prefix.to_be_bytes());
411 framed.extend_from_slice(&reply);
412
413 if let Err(e) = stream.write_all(&framed).await {
414 Log::new(Some(&self.status_tx)).error(format!(
415 "LLMNR failed to reply over TCP to {}: {}",
416 peer_addr, e
417 ));
418 break;
419 }
420 trace!(
421 "LLMNR sent {} framed bytes to {} over TCP",
422 framed.len(),
423 peer_addr
424 );
425 self.count_sent_for(connection_id, framed.len()).await;
426 }
427 }
428
429 /// Parse, validate, ask the model, decide, and — when `sink` is a UDP peer — send.
430 ///
431 /// Returns the bytes to write, plus the connection entry to count them against, for the
432 /// TCP path, which owns its stream.
433 async fn handle_query(
434 &self,
435 data: Vec<u8>,
436 peer_addr: SocketAddr,
437 transport: Transport,
438 sink: Option<UdpSink>,
439 ) -> Option<(Vec<u8>, ConnectionId)> {
440 let log = Log::new(Some(&self.status_tx));
441 trace!(
442 "LLMNR {} bytes from {} over {}: {}",
443 data.len(),
444 peer_addr,
445 transport.as_str(),
446 hex::encode(&data)
447 );
448
449 let query = match DnsMessage::from_vec(&data) {
450 Ok(m) => m,
451 Err(e) => {
452 // A malformed datagram on a multicast group is normal background noise, not
453 // an error worth the status stream.
454 debug!(
455 "LLMNR dropped unparseable message from {}: {}",
456 peer_addr, e
457 );
458 return None;
459 }
460 };
461
462 // RFC 4795 §2.1.1 discard rules. Each of these is "silently discard", so none of them
463 // reaches the model and none of them produces a packet.
464 if query.message_type() != MessageType::Query {
465 debug!("LLMNR dropped a response (not a query) from {}", peer_addr);
466 return None;
467 }
468 if query.op_code() != OpCode::Query {
469 debug!(
470 "LLMNR dropped query from {}: unsupported OPCODE {:?}",
471 peer_addr,
472 query.op_code()
473 );
474 return None;
475 }
476 if query.queries().len() != 1 {
477 debug!(
478 "LLMNR dropped query from {}: QDCOUNT is {}, must be 1",
479 peer_addr,
480 query.queries().len()
481 );
482 return None;
483 }
484
485 let question = &query.queries()[0];
486 let name = question.name().to_string();
487 let query_type = question.query_type().to_string();
488 let query_class = question.query_class().to_string();
489 let transaction_id = query.id();
490
491 let connection_id = self.record_connection(peer_addr, data.len()).await;
492 let _ = self.status_tx.send("__UPDATE_UI__".to_string());
493
494 debug!(
495 "LLMNR query id={} {} {} {} from {} over {}",
496 transaction_id,
497 name,
498 query_class,
499 query_type,
500 peer_addr,
501 transport.as_str()
502 );
503
504 let event = Event::new(
505 &LLMNR_QUERY_EVENT,
506 serde_json::json!({
507 "transaction_id": transaction_id,
508 "name": name,
509 "query_type": query_type,
510 "query_class": query_class,
511 "source_address": peer_addr.to_string(),
512 "transport": transport.as_str(),
513 "conflict": actions::conflict_bit(&query),
514 "tentative": actions::tentative_bit(&query),
515 }),
516 );
517
518 let protocol = LlmnrProtocol::new();
519 let llm_outcome = call_llm(
520 &self.llm_client,
521 &self.state,
522 self.server_id,
523 None,
524 &event,
525 &protocol,
526 )
527 .await;
528
529 let (decision, reply) = self.decide(llm_outcome, transport, peer_addr);
530
531 // Log the decision before acting on it. `model_silent` and every `fail_closed_*`
532 // produce the same zero bytes on the wire, so this line is the only place the
533 // difference survives.
534 let summary = format!(
535 "LLMNR {} {} from {} decision={}",
536 query_type,
537 name,
538 peer_addr,
539 decision.as_str()
540 );
541 if decision.is_fail_closed() {
542 // Loud: nothing was written, and the reason was ours, not the protocol's.
543 error!(
544 "{} (nothing sent: no usable answer was produced; a guessed LLMNR record \
545 would poison the querier's name cache)",
546 summary
547 );
548 log.error(format!(
549 "{} - staying silent rather than guessing a name binding",
550 summary
551 ));
552 } else {
553 log.info(&summary);
554 }
555
556 let reply = reply?;
557
558 let Some(udp) = sink else {
559 // TCP: the connection loop writes it, framed, on the same connection.
560 return Some((reply, connection_id));
561 };
562
563 match udp.socket.send_to(&reply, udp.peer).await {
564 Ok(sent) => {
565 trace!(
566 "LLMNR sent {} bytes to {}: {}",
567 sent,
568 udp.peer,
569 hex::encode(&reply)
570 );
571 self.count_sent_for(connection_id, sent).await;
572 log.debug(format!("LLMNR sent {} bytes to {}", sent, udp.peer));
573 }
574 Err(e) => {
575 Log::new(Some(&self.status_tx))
576 .error(format!("LLMNR failed to reply to {}: {}", udp.peer, e));
577 }
578 }
579 None
580 }
581
582 /// **The fail-closed rule: nothing usable means nothing on the wire.**
583 ///
584 /// Every other UDP protocol in this repo answers a failure with an error frame, because
585 /// silence costs the client its own timeout. LLMNR is the case where that trade inverts.
586 /// Its only error frame is a non-zero RCODE, which RFC 4795 forbids in response to a
587 /// multicast query — and its success frame is a cache entry, so inventing one on a backend
588 /// outage would hand every querier on the link a binding that no host actually claims.
589 /// A querier that hears nothing simply asks the next responder, which is exactly what the
590 /// protocol is designed for.
591 fn decide(
592 &self,
593 llm_outcome: Result<crate::llm::ExecutionResult>,
594 transport: Transport,
595 peer_addr: SocketAddr,
596 ) -> (Decision, Option<Vec<u8>>) {
597 let log = Log::new(Some(&self.status_tx));
598
599 let execution = match llm_outcome {
600 Ok(result) => {
601 for message in &result.messages {
602 log.info(message);
603 }
604 result
605 }
606 Err(e) => {
607 // The error is classified for the log and never rendered onto the wire —
608 // there is no wire text here at all, but the category is still what tells an
609 // operator whether to wait or to look.
610 let category = match crate::utils::wire_failure::WireFailure::classify(&e) {
611 crate::utils::wire_failure::WireFailure::Overloaded => "overloaded",
612 crate::utils::wire_failure::WireFailure::Unavailable => "unavailable",
613 };
614 error!(
615 "LLMNR LLM call failed for {} (category={}): {}",
616 peer_addr, category, e
617 );
618 return (Decision::FailClosedLlmError, None);
619 }
620 };
621
622 // Take the first output that parses as an LLMNR response. Extra outputs are a model
623 // error: one query gets one response, and a second datagram would look to the querier
624 // like a second responder claiming the same name (which sets its C bit).
625 let mut chosen: Option<Vec<u8>> = None;
626 let mut extra = 0usize;
627 for protocol_result in &execution.protocol_results {
628 for output in protocol_result.get_all_output() {
629 if chosen.is_none() {
630 chosen = Some(output);
631 } else {
632 extra += 1;
633 }
634 }
635 }
636 if extra > 0 {
637 warn!(
638 "LLMNR ignored {} extra output(s) for the query from {}; one query gets one \
639 response",
640 extra, peer_addr
641 );
642 }
643
644 if let Some(bytes) = chosen {
645 // Classify by what is actually in the packet rather than by which action name the
646 // model used: the bytes are what the querier will act on.
647 let rcode = bytes.get(3).map(|b| b & 0x0F).unwrap_or(0);
648 if rcode == 0 {
649 return (Decision::ModelResponse, Some(bytes));
650 }
651 // RFC 4795 §2.1.1: "The response to a multicast LLMNR query MUST have RCODE set
652 // to zero." This server cannot distinguish a unicast UDP datagram from a
653 // multicast one without IP_PKTINFO, so it applies the stricter rule to all UDP
654 // and drops the error rather than risk an illegal response.
655 if transport == Transport::Udp {
656 warn!(
657 "LLMNR suppressed an RCODE {} response to {}: a response to a UDP query \
658 must carry RCODE 0 (RFC 4795 2.1.1); use no_response instead",
659 rcode, peer_addr
660 );
661 return (Decision::ModelRejectSuppressed, None);
662 }
663 return (Decision::ModelReject, Some(bytes));
664 }
665
666 // No bytes. Three different reasons, and they must stay apart.
667 if execution.raw_actions.is_empty() {
668 return (Decision::FailClosedNoAction, None);
669 }
670 if execution
671 .raw_actions
672 .iter()
673 .any(|a| a.get("type").and_then(|t| t.as_str()) == Some("no_response"))
674 {
675 // The model looked at the name and decided this host does not own it. This is the
676 // protocol working, not failing.
677 return (Decision::ModelSilent, None);
678 }
679 (Decision::FailClosedActionError, None)
680 }
681
682 /// Record the query as a connection entry so the dashboard shows it.
683 ///
684 /// Nothing ever closes these — LLMNR has no connection — which is why `metadata()`
685 /// declares `.connectionless()`: the 10-second idle sweep is what reaps them.
686 async fn record_connection(&self, peer_addr: SocketAddr, bytes: usize) -> ConnectionId {
687 use crate::state::server::{
688 ConnectionState as ServerConnectionState, ConnectionStatus, ProtocolConnectionInfo,
689 };
690 let connection_id = ConnectionId::new(self.state.get_next_unified_id().await);
691 let now = crate::utils::clock::Instant::now();
692 let conn_state = ServerConnectionState {
693 id: connection_id,
694 remote_addr: peer_addr,
695 local_addr: self.local_addr,
696 bytes_sent: 0,
697 bytes_received: bytes as u64,
698 packets_sent: 0,
699 packets_received: 1,
700 last_activity: now,
701 status: ConnectionStatus::Active,
702 status_changed_at: now,
703 protocol_info: ProtocolConnectionInfo::empty(),
704 };
705 self.state
706 .add_connection_to_server(self.server_id, conn_state)
707 .await;
708 connection_id
709 }
710
711 /// Keep the rail's `↑` counter in step with what actually went out. Each query — each TCP
712 /// frame included — got its own entry in `record_connection`, so the write is counted
713 /// against the entry the query created.
714 async fn count_sent_for(&self, connection_id: ConnectionId, bytes: usize) {
715 self.state
716 .update_connection_stats(
717 self.server_id,
718 connection_id,
719 None,
720 Some(bytes as u64),
721 None,
722 Some(1),
723 )
724 .await;
725 }
726}
727
728/// Join the LLMNR multicast group, non-fatally.
729///
730/// Deliberately best-effort. On macOS a join on loopback fails routinely, and a responder
731/// that refused to start because of it would be unusable for exactly the local testing this
732/// repo does — while still being perfectly able to answer a query sent to its port. The
733/// failure is logged on both channels so it is never *silently* absent.
734fn join_llmnr_group(
735 socket: &UdpSocket,
736 local_addr: SocketAddr,
737 multicast_interface: Option<&str>,
738 status_tx: &mpsc::UnboundedSender<String>,
739) {
740 let log = Log::new(Some(status_tx));
741
742 match local_addr.ip() {
743 IpAddr::V4(_) => {
744 let interface = match multicast_interface {
745 Some(value) => match value.parse::<Ipv4Addr>() {
746 Ok(ip) => ip,
747 Err(e) => {
748 warn!(
749 "LLMNR multicast_interface '{}' is not an IPv4 address ({}); \
750 letting the host choose",
751 value, e
752 );
753 Ipv4Addr::UNSPECIFIED
754 }
755 },
756 None => Ipv4Addr::UNSPECIFIED,
757 };
758 match socket.join_multicast_v4(LLMNR_IPV4_GROUP, interface) {
759 Ok(()) => {
760 info!(
761 "LLMNR joined {} on interface {}",
762 LLMNR_IPV4_GROUP, interface
763 );
764 log.info(format!("LLMNR joined multicast group {}", LLMNR_IPV4_GROUP));
765 }
766 Err(e) => {
767 warn!(
768 "LLMNR could not join {} on interface {}: {}; queries sent directly to \
769 this port are still answered",
770 LLMNR_IPV4_GROUP, interface, e
771 );
772 log.warn(format!(
773 "LLMNR multicast join failed ({}); answering direct queries only",
774 e
775 ));
776 }
777 }
778 }
779 IpAddr::V6(_) => {
780 // Interface index 0 lets the kernel pick. There is no address-based selector for
781 // IPv6 joins, so `multicast_interface` (an IPv4 address) has no meaning here.
782 match socket.join_multicast_v6(&LLMNR_IPV6_GROUP, 0) {
783 Ok(()) => {
784 info!("LLMNR joined {}", LLMNR_IPV6_GROUP);
785 log.info(format!("LLMNR joined multicast group {}", LLMNR_IPV6_GROUP));
786 }
787 Err(e) => {
788 warn!(
789 "LLMNR could not join {}: {}; queries sent directly to this port are \
790 still answered",
791 LLMNR_IPV6_GROUP, e
792 );
793 log.warn(format!(
794 "LLMNR multicast join failed ({}); answering direct queries only",
795 e
796 ));
797 }
798 }
799 }
800 }
801}