Skip to main content

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}