diff --git a/vm/devices/net/net_consomme/consomme/src/dns_resolver/dns_tcp.rs b/vm/devices/net/net_consomme/consomme/src/dns_resolver/dns_tcp.rs new file mode 100644 index 0000000000..a2c3237e29 --- /dev/null +++ b/vm/devices/net/net_consomme/consomme/src/dns_resolver/dns_tcp.rs @@ -0,0 +1,473 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! DNS over TCP handler for consomme. +//! +//! Implements DNS TCP framing per RFC 1035 §4.2.2: each DNS message is +//! preceded by a 2-byte big-endian length prefix. This module intercepts +//! TCP connections to the gateway on port 53 and resolves queries using +//! the shared `DnsBackend`. +use super::DnsBackend; +use super::DnsFlow; +use super::DnsRequest; +use super::DnsResolver; +use super::DnsResponse; +use mesh_channel_core::Receiver; +use std::io::IoSliceMut; +use std::task::Context; +use std::task::Poll; +use std::task::ready; +use thiserror::Error; + +// Maximum allowed DNS message size over TCP: 65535 bytes for the message +// plus 2 bytes for the TCP length prefix. This is a sanity check to prevent +// unbounded memory growth. +const MAX_DNS_TCP_PAYLOAD_SIZE: usize = (u16::MAX as usize) + 2; + +/// Errors returned by [`DnsTcpHandler::ingest`] and [`DnsTcpHandler::poll_read`] +/// when the DNS TCP framing is invalid or the query cannot be processed. +#[derive(Debug, Error)] +pub enum DnsTcpError { + /// The TCP length prefix specified a message size too small for a valid DNS header. + #[error("invalid DNS TCP message length")] + InvalidMessageLength, + /// The query was rate-limited by the resolver backend. + #[error("DNS TCP query rate-limited")] + RateLimited, + /// The DNS response exceeded the maximum allowed TCP message size. + #[error("DNS TCP response too large")] + ResponseTooLarge, + /// The resolver backend dropped the query without sending a response. + #[error("DNS TCP query cancelled")] + QueryCancelled, +} + +/// Current phase of the DNS TCP handler state machine. +enum Phase { + /// Accumulating an incoming TCP-framed DNS request. + Receiving, + /// Query submitted to the backend; awaiting response. + InFlight, + /// Writing a TCP-framed response back to the caller. + Responding, +} + +pub struct DnsTcpHandler { + receiver: Receiver, + flow: DnsFlow, + /// Shared buffer used for both the incoming request and the outgoing + /// response. During [`Phase::Receiving`] it accumulates one TCP-framed + /// DNS message from the guest. During [`Phase::Responding`] it holds + /// the TCP-framed response being drained to the caller. + buf: Vec, + /// Write offset into `buf` while draining a response to the caller. + /// Only meaningful during [`Phase::Responding`]. + tx_offset: usize, + phase: Phase, + /// The guest has sent FIN; no more data will arrive. + guest_fin: bool, +} + +impl DnsTcpHandler { + pub fn new(flow: DnsFlow) -> Self { + let receiver = Receiver::new(); + Self { + receiver, + flow, + buf: Vec::new(), + tx_offset: 0, + phase: Phase::Receiving, + guest_fin: false, + } + } + + /// Feed data received from the guest into the handler. + /// + /// Consumes bytes from `data` to assemble one complete TCP-framed DNS + /// message. When a complete message is assembled, it is submitted to the + /// backend for resolution and no further data is accepted until the + /// response has been fully written out by [`poll_read`]. + /// + /// Returns the number of bytes consumed from `data`. The caller should + /// only drain this many bytes from its receive buffer. + /// + /// Returns an error if the TCP framing is invalid or the query cannot be + /// submitted, in which case the caller should reset the connection. + pub fn ingest( + &mut self, + data: &[&[u8]], + dns: &mut DnsResolver, + ) -> Result { + // Don't accept data while a query is in-flight or a response is pending. + if !matches!(self.phase, Phase::Receiving) { + return Ok(0); + } + + let mut total_consumed = 0; + for chunk in data { + let mut pos = 0; + while pos < chunk.len() { + let need = self.bytes_needed(); + if need == 0 { + // Complete message already in rx_buf but not yet submitted + // (should not happen in practice). + break; + } + let accept = (chunk.len() - pos).min(need); + self.buf.extend_from_slice(&chunk[pos..pos + accept]); + pos += accept; + total_consumed += accept; + + match self.try_submit(dns) { + Ok(true) => return Ok(total_consumed), + Ok(false) => {} + Err(e) => return Err(e), + } + } + } + + Ok(total_consumed) + } + + /// How many more bytes are needed to complete the current message. + fn bytes_needed(&self) -> usize { + if self.buf.len() < 2 { + return 2 - self.buf.len(); + } + let msg_len = u16::from_be_bytes([self.buf[0], self.buf[1]]) as usize; + (2 + msg_len).saturating_sub(self.buf.len()) + } + + /// If a complete TCP-framed DNS message is in `buf`, submit it to the + /// resolver via [`DnsResolver::submit_tcp_query`]. + /// + /// Returns `Ok(true)` if the query was submitted, `Ok(false)` if the + /// message is still incomplete, or `Err` if the framing is invalid or + /// the query was rejected. + fn try_submit(&mut self, dns: &mut DnsResolver) -> Result { + if self.buf.len() < 2 { + return Ok(false); + } + let msg_len = u16::from_be_bytes([self.buf[0], self.buf[1]]) as usize; + if msg_len <= super::DNS_HEADER_SIZE { + return Err(DnsTcpError::InvalidMessageLength); + } + if self.buf.len() < 2 + msg_len { + return Ok(false); + } + + // Submit the raw DNS query (without the TCP length prefix). + let request = DnsRequest { + flow: self.flow.clone(), + dns_query: &self.buf[2..2 + msg_len], + }; + if !dns.submit_tcp_query(&request, self.receiver.sender()) { + tracelimit::warn_ratelimited!( + msg_len, + src_port = self.flow.src_port, + "dns_tcp: query rate-limited, closing connection" + ); + return Err(DnsTcpError::RateLimited); + } + self.buf.clear(); + self.phase = Phase::InFlight; + Ok(true) + } + + /// Poll for the next chunk of response data. + /// + /// Models the socket `poll_read_vectored` contract: + /// - `Poll::Ready(Ok(n))` where `n > 0`: wrote `n` bytes of response data. + /// - `Poll::Ready(Ok(0))`: EOF — the guest sent FIN and all responses have + /// been drained. The caller should close the connection. + /// - `Poll::Ready(Err(_))`: a protocol error occurred; the caller should + /// reset the connection. + /// - `Poll::Pending`: waiting for a DNS response or for [`ingest`] to + /// submit a new query. + pub fn poll_read( + &mut self, + cx: &mut Context<'_>, + bufs: &mut [IoSliceMut<'_>], + dns: &mut DnsResolver, + ) -> Poll> { + match self.phase { + Phase::InFlight => match ready!(self.receiver.poll_recv(cx)) { + Ok(response) => { + dns.complete_tcp_query(); + let payload_len = response.response_data.len(); + if payload_len > MAX_DNS_TCP_PAYLOAD_SIZE { + tracelimit::warn_ratelimited!( + size = payload_len, + "DNS TCP response exceeds maximum message size" + ); + return Poll::Ready(Err(DnsTcpError::ResponseTooLarge)); + } + + self.buf.clear(); + self.buf + .reserve((2 + payload_len).saturating_sub(self.buf.capacity())); + self.buf + .extend_from_slice(&(payload_len as u16).to_be_bytes()); + self.buf.extend(response.response_data); + self.tx_offset = 0; + self.phase = Phase::Responding; + + let n = self.drain_tx(bufs); + return Poll::Ready(Ok(n)); + } + Err(_) => { + dns.complete_tcp_query(); + return Poll::Ready(Err(DnsTcpError::QueryCancelled)); + } + }, + Phase::Responding => { + let n = self.drain_tx(bufs); + return Poll::Ready(Ok(n)); + } + Phase::Receiving => {} + } + + // No in-flight query and no pending response. + if self.guest_fin { + Poll::Ready(Ok(0)) + } else { + Poll::Pending + } + } + + /// Write as much of `buf[tx_offset..]` into `bufs` as possible. + /// Clears `buf` when fully drained so it can be reused for the next + /// incoming request. + fn drain_tx(&mut self, bufs: &mut [IoSliceMut<'_>]) -> usize { + let remaining = &self.buf[self.tx_offset..]; + let mut written = 0; + for buf in bufs.iter_mut() { + let left = remaining.len() - written; + if left == 0 { + break; + } + let n = buf.len().min(left); + buf[..n].copy_from_slice(&remaining[written..written + n]); + written += n; + } + self.tx_offset += written; + if self.tx_offset >= self.buf.len() { + self.buf.clear(); + self.tx_offset = 0; + self.phase = Phase::Receiving; + } + written + } + + pub fn guest_fin(&self) -> bool { + self.guest_fin + } + + pub fn set_guest_fin(&mut self) { + self.guest_fin = true; + } + + pub fn is_in_flight(&self) -> bool { + matches!(self.phase, Phase::InFlight) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::dns_resolver::DnsBackend; + use crate::dns_resolver::DnsRequest; + use crate::dns_resolver::DnsResponse; + use std::sync::Arc; + + /// A test DNS backend that echoes the query back as the response. + struct EchoBackend; + + impl DnsBackend for EchoBackend { + fn query( + &self, + request: &DnsRequest<'_>, + response_sender: mesh_channel_core::Sender, + ) { + response_sender.send(DnsResponse { + flow: request.flow.clone(), + response_data: request.dns_query.to_vec(), + }); + } + } + + fn test_flow() -> DnsFlow { + use smoltcp::wire::EthernetAddress; + use smoltcp::wire::IpAddress; + use smoltcp::wire::Ipv4Address; + DnsFlow { + src_addr: IpAddress::Ipv4(Ipv4Address::new(10, 0, 0, 2)), + dst_addr: IpAddress::Ipv4(Ipv4Address::new(10, 0, 0, 1)), + src_port: 12345, + dst_port: 53, + gateway_mac: EthernetAddress([0x52, 0x55, 10, 0, 0, 1]), + client_mac: EthernetAddress([0, 0, 0, 0, 1, 0]), + transport: crate::dns_resolver::DnsTransport::Tcp, + } + } + + fn make_tcp_dns_message(payload: &[u8]) -> Vec { + let len = payload.len() as u16; + let mut msg = len.to_be_bytes().to_vec(); + msg.extend_from_slice(payload); + msg + } + + /// A 16-byte fake DNS query payload (>= 12-byte header minimum). + fn sample_query() -> Vec { + vec![ + 0xAB, 0xCD, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x66, + 0x6F, 0x6F, + ] + } + + struct NoopWaker; + impl std::task::Wake for NoopWaker { + fn wake(self: Arc) {} + } + + #[test] + fn single_query_response() { + let mut dns = DnsResolver::new_for_test(Arc::new(EchoBackend)); + let mut handler = DnsTcpHandler::new(test_flow()); + + let query = sample_query(); + let msg = make_tcp_dns_message(&query); + + let consumed = handler.ingest(&[&msg], &mut dns).unwrap(); + assert_eq!(consumed, msg.len()); + + let waker = std::task::Waker::from(Arc::new(NoopWaker)); + let mut cx = Context::from_waker(&waker); + + let mut buf = vec![0u8; 256]; + match handler.poll_read(&mut cx, &mut [IoSliceMut::new(&mut buf)], &mut dns) { + Poll::Ready(Ok(n)) => { + assert!(n > 0); + // First 2 bytes are the TCP length prefix. + let resp_len = u16::from_be_bytes([buf[0], buf[1]]) as usize; + assert_eq!(resp_len, query.len()); + // Response payload should match the query (echo backend). + assert_eq!(&buf[2..2 + resp_len], &query); + } + Poll::Ready(Err(e)) => panic!("unexpected error: {e}"), + Poll::Pending => panic!("expected Ready"), + } + } + + #[test] + fn partial_message_buffering() { + let mut dns = DnsResolver::new_for_test(Arc::new(EchoBackend)); + let mut handler = DnsTcpHandler::new(test_flow()); + + let query = sample_query(); + let msg = make_tcp_dns_message(&query); + + // Feed just the length prefix. + let consumed = handler.ingest(&[&msg[..2]], &mut dns).unwrap(); + assert_eq!(consumed, 2); + + let waker = std::task::Waker::from(Arc::new(NoopWaker)); + let mut cx = Context::from_waker(&waker); + let mut buf = vec![0u8; 256]; + assert!(matches!( + handler.poll_read(&mut cx, &mut [IoSliceMut::new(&mut buf)], &mut dns), + Poll::Pending + )); + + // Feed the rest. + let consumed = handler.ingest(&[&msg[2..]], &mut dns).unwrap(); + assert_eq!(consumed, msg.len() - 2); + + match handler.poll_read(&mut cx, &mut [IoSliceMut::new(&mut buf)], &mut dns) { + Poll::Ready(Ok(n)) => assert!(n > 0), + Poll::Ready(Err(e)) => panic!("unexpected error: {e}"), + Poll::Pending => panic!("expected Ready after completing message"), + } + } + + #[test] + fn backpressure_one_at_a_time() { + let mut dns = DnsResolver::new_for_test(Arc::new(EchoBackend)); + let mut handler = DnsTcpHandler::new(test_flow()); + + let q1 = sample_query(); + let q2 = vec![ + 0x00, 0x02, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x62, + 0x62, 0x62, + ]; + let mut combined = make_tcp_dns_message(&q1); + combined.extend(make_tcp_dns_message(&q2)); + + // Only the first message should be consumed. + let consumed = handler.ingest(&[&combined], &mut dns).unwrap(); + assert_eq!(consumed, make_tcp_dns_message(&q1).len()); + + let waker = std::task::Waker::from(Arc::new(NoopWaker)); + let mut cx = Context::from_waker(&waker); + + // Drain the first response. + let mut buf = vec![0u8; 256]; + match handler.poll_read(&mut cx, &mut [IoSliceMut::new(&mut buf)], &mut dns) { + Poll::Ready(Ok(n)) => assert!(n > 0), + Poll::Ready(Err(e)) => panic!("unexpected error: {e}"), + Poll::Pending => panic!("expected Ready for first response"), + } + + // Now the second message can be ingested. + let remaining = &combined[consumed..]; + let consumed2 = handler.ingest(&[remaining], &mut dns).unwrap(); + assert_eq!(consumed2, make_tcp_dns_message(&q2).len()); + + match handler.poll_read(&mut cx, &mut [IoSliceMut::new(&mut buf)], &mut dns) { + Poll::Ready(Ok(n)) => assert!(n > 0), + Poll::Ready(Err(e)) => panic!("unexpected error: {e}"), + Poll::Pending => panic!("expected Ready for second response"), + } + } + + #[test] + fn eof_after_fin_and_drain() { + let mut dns = DnsResolver::new_for_test(Arc::new(EchoBackend)); + let mut handler = DnsTcpHandler::new(test_flow()); + + let query = sample_query(); + handler + .ingest(&[&make_tcp_dns_message(&query)], &mut dns) + .unwrap(); + + let waker = std::task::Waker::from(Arc::new(NoopWaker)); + let mut cx = Context::from_waker(&waker); + + // Drain the response. + let mut buf = vec![0u8; 256]; + let _ = handler.poll_read(&mut cx, &mut [IoSliceMut::new(&mut buf)], &mut dns); + + handler.set_guest_fin(); + + // Should now report EOF. + assert!(matches!( + handler.poll_read(&mut cx, &mut [IoSliceMut::new(&mut buf)], &mut dns), + Poll::Ready(Ok(0)) + )); + } + + #[test] + fn protocol_error_on_invalid_length() { + let mut dns = DnsResolver::new_for_test(Arc::new(EchoBackend)); + let mut handler = DnsTcpHandler::new(test_flow()); + + // Craft a message with msg_len <= DNS_HEADER_SIZE (12). + // Length prefix says 4 bytes, which is too small for a DNS header. + let bad_msg = [0x00, 0x04, 0x01, 0x02, 0x03, 0x04]; + assert!(matches!( + handler.ingest(&[&bad_msg], &mut dns), + Err(DnsTcpError::InvalidMessageLength) + )); + } +} diff --git a/vm/devices/net/net_consomme/consomme/src/dns_resolver/mod.rs b/vm/devices/net/net_consomme/consomme/src/dns_resolver/mod.rs index d3bae3265f..acecb02c13 100644 --- a/vm/devices/net/net_consomme/consomme/src/dns_resolver/mod.rs +++ b/vm/devices/net/net_consomme/consomme/src/dns_resolver/mod.rs @@ -6,19 +6,35 @@ use mesh_channel_core::Receiver; use mesh_channel_core::Sender; use smoltcp::wire::EthernetAddress; use smoltcp::wire::IpAddress; +use std::sync::Arc; use std::task::Context; use std::task::Poll; use crate::DropReason; +pub mod dns_tcp; + #[cfg(unix)] mod unix; #[cfg(windows)] mod windows; +#[cfg(unix)] +type PlatformDnsBackend = unix::UnixDnsResolverBackend; + +#[cfg(windows)] +type PlatformDnsBackend = windows::WindowsDnsResolverBackend; + static DNS_HEADER_SIZE: usize = 12; +/// Transport protocol for a DNS query. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DnsTransport { + Udp, + Tcp, +} + #[derive(Debug, Clone)] pub struct DnsFlow { pub src_addr: IpAddress, @@ -27,6 +43,11 @@ pub struct DnsFlow { pub dst_port: u16, pub gateway_mac: EthernetAddress, pub client_mac: EthernetAddress, + // Used by the glibc and Windows DNS backends. The musl resolver + // implementation handles TCP internally, so this field is not + // used in the musl backend. + #[allow(dead_code)] + pub transport: DnsTransport, } #[derive(Debug, Clone)] @@ -42,16 +63,26 @@ pub struct DnsResponse { pub response_data: Vec, } +/// Backend trait for resolving DNS queries. +/// +/// Both `dns_query` in [`DnsRequest`] and `response_data` in [`DnsResponse`] +/// carry **raw DNS message bytes** with no transport-layer framing (e.g. no +/// TCP 2-byte length prefix). Transport framing is the responsibility of the +/// caller (see [`dns_tcp::DnsTcpHandler`]). pub(crate) trait DnsBackend: Send + Sync { fn query(&self, request: &DnsRequest<'_>, response_sender: Sender); } #[derive(Inspect)] -pub struct DnsResolver { +pub struct DnsResolver { #[inspect(skip)] - backend: Box, + backend: Arc, + /// Channel receiver for UDP DNS responses. Each call to + /// [`Self::submit_udp_query`] sends the response back through this + /// channel so that [`Self::poll_udp_response`] can retrieve it. + /// The TCP path uses its own per-connection channel instead. #[inspect(skip)] - receiver: Receiver, + udp_receiver: Receiver, pending_requests: usize, max_pending_requests: usize, } @@ -68,10 +99,10 @@ impl DnsResolver { pub fn new(max_pending_requests: usize) -> Result { use crate::dns_resolver::windows::WindowsDnsResolverBackend; - let receiver = Receiver::new(); + let udp_receiver = Receiver::new(); Ok(Self { - backend: Box::new(WindowsDnsResolverBackend::new()?), - receiver, + backend: Arc::new(WindowsDnsResolverBackend::new()?), + udp_receiver, pending_requests: 0, max_pending_requests, }) @@ -85,36 +116,60 @@ impl DnsResolver { pub fn new(max_pending_requests: usize) -> Result { use crate::dns_resolver::unix::UnixDnsResolverBackend; - let receiver = Receiver::new(); + let udp_receiver = Receiver::new(); Ok(Self { - backend: Box::new(UnixDnsResolverBackend::new()?), - receiver, + backend: Arc::new(UnixDnsResolverBackend::new()?), + udp_receiver, pending_requests: 0, max_pending_requests, }) } +} - pub fn handle_dns(&mut self, request: &DnsRequest<'_>) -> Result<(), DropReason> { - if request.dns_query.len() <= DNS_HEADER_SIZE { - return Err(DropReason::Packet(smoltcp::wire::Error)); - } - +impl DnsResolver { + // ── Shared ─────────────────────────────────────────────────────── + + /// Submit a DNS query to the backend with a caller-supplied response + /// sender. Returns `true` if accepted, `false` if the pending-request + /// limit has been reached. + fn submit_query( + &mut self, + request: &DnsRequest<'_>, + response_sender: Sender, + ) -> bool { if self.pending_requests < self.max_pending_requests { self.pending_requests += 1; - self.backend.query(request, self.receiver.sender()); + self.backend.query(request, response_sender); + true } else { tracelimit::warn_ratelimited!( current = self.pending_requests, max = self.max_pending_requests, "DNS request limit reached" ); + false } + } + /// Validate and submit a DNS query received over UDP. + /// + /// The response will be delivered through [`Self::poll_udp_response`]. + pub fn submit_udp_query(&mut self, request: &DnsRequest<'_>) -> Result<(), DropReason> { + if request.dns_query.len() <= DNS_HEADER_SIZE { + return Err(DropReason::Packet(smoltcp::wire::Error)); + } + + let sender = self.udp_receiver.sender(); + self.submit_query(request, sender); Ok(()) } - pub fn poll_response(&mut self, cx: &mut Context<'_>) -> Poll> { - match self.receiver.poll_recv(cx) { + /// Poll for the next completed UDP DNS response. + /// + /// This drains `self.udp_receiver`; it must **not** be used for TCP + /// responses (the TCP path has its own per-connection channel). + pub fn poll_udp_response(&mut self, cx: &mut Context<'_>) -> Poll> { + match self.udp_receiver.poll_recv(cx) { Poll::Ready(Ok(response)) => { self.pending_requests -= 1; Poll::Ready(Some(response)) @@ -122,6 +177,39 @@ impl DnsResolver { Poll::Ready(Err(_)) | Poll::Pending => Poll::Pending, } } + + /// Submit a DNS query with a caller-supplied response sender. + /// + /// Returns `true` if the query was accepted, or `false` if the + /// pending-request limit has been reached. + /// + /// The TCP handler calls this with its own [`Sender`] so responses + /// arrive on the per-connection channel rather than `udp_receiver`. + pub fn submit_tcp_query( + &mut self, + request: &DnsRequest<'_>, + response_sender: Sender, + ) -> bool { + self.submit_query(request, response_sender) + } + + /// Decrement the pending-request counter after a TCP response has + /// been consumed by [`dns_tcp::DnsTcpHandler`]. + pub fn complete_tcp_query(&mut self) { + self.pending_requests = self.pending_requests.saturating_sub(1); + } + + /// Create a resolver with a test backend (for unit tests only). + #[cfg(test)] + pub(crate) fn new_for_test(backend: Arc) -> Self { + let udp_receiver = Receiver::new(); + Self { + backend, + udp_receiver, + pending_requests: 0, + max_pending_requests: DEFAULT_MAX_PENDING_DNS_REQUESTS, + } + } } /// Internal DNS request structure used by backend implementations. diff --git a/vm/devices/net/net_consomme/consomme/src/dns_resolver/unix/glibc.rs b/vm/devices/net/net_consomme/consomme/src/dns_resolver/unix/glibc.rs index 397b410da7..3ec63e5018 100644 --- a/vm/devices/net/net_consomme/consomme/src/dns_resolver/unix/glibc.rs +++ b/vm/devices/net/net_consomme/consomme/src/dns_resolver/unix/glibc.rs @@ -10,6 +10,15 @@ use super::DnsRequestInternal; use super::DnsResponse; use super::build_servfail_response; use libc::c_int; +use libc::c_ulong; +use zerocopy::FromZeros; +use zerocopy::Immutable; +use zerocopy::IntoBytes; +use zerocopy::KnownLayout; + +/// RES_USEVC option flag - use TCP (virtual circuit) instead of UDP. +/// From glibc resolv/resolv.h: https://sourceware.org/git/?p=glibc.git;a=blob_plain;f=resolv/resolv.h;hb=HEAD +const RES_USEVC: c_ulong = 0x00000040; /// Size of the `res_state` structure for different platforms. /// These values were derived from including resolv.h and using sizeof(struct __res_state). @@ -18,16 +27,44 @@ const RES_STATE_SIZE: usize = 552; #[cfg(target_os = "linux")] const RES_STATE_SIZE: usize = 568; +/// The prefix of the glibc `struct __res_state` that we need to access. +/// This matches the layout defined in glibc resolv/bits/types/res_state.h: +/// See: https://sourceware.org/git/?p=glibc.git;a=blob_plain;f=resolv/bits/types/res_state.h;hb=HEAD +/// See: https://github.com/apple-oss-distributions/libresolv/blob/main/resolv.h +/// +/// ```c +/// struct __res_state { +/// int retrans; /* retransmission time interval */ +/// int retry; /* number of times to retransmit */ +/// unsigned long options; /* option flags */ +/// ... +/// } +/// ``` +#[repr(C)] +#[derive(IntoBytes, Immutable, KnownLayout, FromZeros)] +struct ResStatePrefix { + retrans: c_int, + retry: c_int, + options: c_ulong, +} + +/// Wrapper around the glibc/macOS resolver state structure. #[repr(C)] +#[derive(IntoBytes, Immutable, KnownLayout, FromZeros)] pub struct ResState { - _data: [u8; RES_STATE_SIZE], + prefix: ResStatePrefix, + _rest: [u8; RES_STATE_SIZE - size_of::()], } impl ResState { - pub fn zeroed() -> Self { - Self { - _data: [0u8; RES_STATE_SIZE], - } + /// Set the options field in the resolver state. + pub fn set_options(&mut self, options: c_ulong) { + self.prefix.options = options; + } + + /// Get the options field from the resolver state. + pub fn options(&self) -> c_ulong { + self.prefix.options } } @@ -59,7 +96,7 @@ unsafe extern "C" { /// Handle a DNS query using reentrant resolver functions (macOS and GNU libc). pub fn handle_dns_query(request: DnsRequestInternal) { let mut answer = vec![0u8; 4096]; - let mut state = ResState::zeroed(); + let mut state = ResState::new_zeroed(); // SAFETY: res_ninit initializes the resolver state by reading /etc/resolv.conf. // The state is properly sized and aligned. @@ -74,6 +111,10 @@ pub fn handle_dns_query(request: DnsRequestInternal) { return; } + // Set RES_USEVC to force TCP for DNS queries. + if request.flow.transport == crate::dns_resolver::DnsTransport::Tcp { + state.set_options(state.options() | RES_USEVC); + } // SAFETY: res_nsend is called with valid state, query buffer and answer buffer. // All buffers are properly sized and aligned. The state was initialized above. let answer_len = unsafe { @@ -110,17 +151,9 @@ pub fn handle_dns_query(request: DnsRequestInternal) { mod tests { use super::*; - #[test] - fn test_res_ninit_and_res_nsend_callable() { - // Test that the reentrant resolver functions are callable - let mut state = ResState::zeroed(); - - // SAFETY: res_ninit initializes the resolver state - let init_result = unsafe { res_ninit(&mut state) }; - assert_eq!(init_result, 0, "res_ninit() should succeed"); - - // Example DNS query buffer for google.com A record - let dns_query: Vec = vec![ + /// Example DNS query buffer for google.com A record. + fn sample_dns_query() -> Vec { + vec![ 0x12, 0x34, // Transaction ID 0x01, 0x00, // Flags: standard query 0x00, 0x01, // Questions: 1 @@ -131,23 +164,66 @@ mod tests { 0x00, // null terminator 0x00, 0x01, // Type: A 0x00, 0x01, // Class: IN - ]; - - let mut answer = vec![0u8; 4096]; - - // SAFETY: res_nsend is called with valid state, query buffer and answer buffer. - let _answer_len = unsafe { - res_nsend( - &mut state, - dns_query.as_ptr(), - dns_query.len() as c_int, - answer.as_mut_ptr(), - answer.len() as c_int, - ) - }; - - // Clean up - // SAFETY: res_nclose frees resources associated with the resolver state. - unsafe { res_nclose(&mut state) }; + ] + } + + /// RAII wrapper for ResState that ensures proper cleanup. + struct InitializedResState { + state: ResState, + } + + impl InitializedResState { + fn new() -> Self { + let mut state = ResState::new_zeroed(); + // SAFETY: res_ninit initializes the resolver state + let result = unsafe { res_ninit(&mut state) }; + assert_eq!(result, 0, "res_ninit() should succeed"); + Self { state } + } + + /// Send a DNS query and return the response length. + fn send_query(&mut self, query: &[u8]) -> c_int { + let mut answer = vec![0u8; 4096]; + // SAFETY: res_nsend is called with valid state, query buffer and answer buffer. + unsafe { + res_nsend( + &mut self.state, + query.as_ptr(), + query.len() as c_int, + answer.as_mut_ptr(), + answer.len() as c_int, + ) + } + } + } + + impl Drop for InitializedResState { + fn drop(&mut self) { + // SAFETY: res_nclose frees resources associated with the resolver state. + unsafe { res_nclose(&mut self.state) }; + } + } + + #[test] + fn test_res_ninit_and_res_nsend_callable() { + let mut state = InitializedResState::new(); + let _answer_len = state.send_query(&sample_dns_query()); + } + + #[test] + fn test_res_usevc_flag_for_tcp() { + let mut state = InitializedResState::new(); + + // Verify we can read and modify the options field + let original_options = state.state.options(); + state.state.set_options(original_options | RES_USEVC); + assert_ne!( + state.state.options() & RES_USEVC, + 0, + "RES_USEVC flag should be set" + ); + + // With RES_USEVC set, this should use TCP instead of UDP. + let _answer_len = state.send_query(&sample_dns_query()); } } diff --git a/vm/devices/net/net_consomme/consomme/src/dns_resolver/windows/mod.rs b/vm/devices/net/net_consomme/consomme/src/dns_resolver/windows/mod.rs index 3a60e98bf8..58e3822d5f 100644 --- a/vm/devices/net/net_consomme/consomme/src/dns_resolver/windows/mod.rs +++ b/vm/devices/net/net_consomme/consomme/src/dns_resolver/windows/mod.rs @@ -21,6 +21,7 @@ use std::ptr::null_mut; use std::sync::Arc; use windows_sys::Win32::Foundation::DNS_REQUEST_PENDING; use windows_sys::Win32::Foundation::NO_ERROR; +use windows_sys::Win32::NetworkManagement::Dns::DNS_PROTOCOL_TCP; use windows_sys::Win32::NetworkManagement::Dns::DNS_PROTOCOL_UDP; use windows_sys::Win32::NetworkManagement::Dns::DNS_QUERY_NO_MULTICAST; use windows_sys::Win32::NetworkManagement::Dns::DNS_QUERY_RAW_CANCEL; @@ -74,15 +75,30 @@ impl DnsBackend for WindowsDnsResolverBackend { // Clone the sender for error handling let response_sender_clone = response_sender.clone(); - // Create internal request + // For TCP, DnsQueryRaw expects the 2-byte TCP length prefix in the + // query buffer. Prepend it here so that the DnsTcpHandler can remain + // platform-agnostic and always pass raw DNS bytes. + let wire_query = match request.flow.transport { + super::DnsTransport::Tcp => { + let len = request.dns_query.len() as u16; + let mut buf = Vec::with_capacity(2 + request.dns_query.len()); + buf.extend_from_slice(&len.to_be_bytes()); + buf.extend_from_slice(request.dns_query); + buf + } + super::DnsTransport::Udp => request.dns_query.to_vec(), + }; + + // Create internal request with raw DNS bytes (no TCP prefix) so that + // SERVFAIL generation works correctly. let internal_request = DnsRequestInternal { flow: request.flow.clone(), query: request.dns_query.to_vec(), response_sender, }; - let dns_query_size = internal_request.query.len() as u32; - let dns_query = internal_request.query.as_ptr().cast_mut(); + let dns_query_size = wire_query.len() as u32; + let dns_query = wire_query.as_ptr().cast_mut(); // Pre-insert placeholder before calling DnsQueryRaw to avoid race condition // where callback fires before we can insert the cancel handle. @@ -117,7 +133,10 @@ impl DnsBackend for WindowsDnsResolverBackend { queryRawOptions: 0, customServersSize: 0, customServers: null_mut(), - protocol: DNS_PROTOCOL_UDP, + protocol: match request.flow.transport { + super::DnsTransport::Tcp => DNS_PROTOCOL_TCP, + super::DnsTransport::Udp => DNS_PROTOCOL_UDP, + }, Anonymous: DNS_QUERY_RAW_REQUEST_0::default(), }; @@ -229,10 +248,20 @@ unsafe extern "system" fn dns_query_raw_callback( // SAFETY: query_results is provided by Windows and will be freed after processing let response = match unsafe { process_dns_results(query_results) } { - Ok(response_data) => Some(DnsResponse { - flow: context.request.flow.clone(), - response_data, - }), + Ok(mut response_data) => { + // For TCP, DnsQueryRaw returns the response with a 2-byte TCP + // length prefix. Strip it so the DnsTcpHandler can add its own + // framing. + if context.request.flow.transport == super::DnsTransport::Tcp + && response_data.len() >= 2 + { + response_data.drain(..2); + } + Some(DnsResponse { + flow: context.request.flow.clone(), + response_data, + }) + } Err(DnsResultError::QueryFailed(status)) => { tracelimit::warn_ratelimited!(status, "DNS query failed, returning SERVFAIL"); None diff --git a/vm/devices/net/net_consomme/consomme/src/lib.rs b/vm/devices/net/net_consomme/consomme/src/lib.rs index db10fcd51a..806516ce91 100644 --- a/vm/devices/net/net_consomme/consomme/src/lib.rs +++ b/vm/devices/net/net_consomme/consomme/src/lib.rs @@ -29,6 +29,9 @@ mod udp; mod unix; mod windows; +/// Standard DNS port number. +const DNS_PORT: u16 = 53; + use inspect::Inspect; use inspect::InspectMut; use pal_async::driver::Driver; diff --git a/vm/devices/net/net_consomme/consomme/src/tcp.rs b/vm/devices/net/net_consomme/consomme/src/tcp.rs index 4c11940c8e..03eb39fbd4 100644 --- a/vm/devices/net/net_consomme/consomme/src/tcp.rs +++ b/vm/devices/net/net_consomme/consomme/src/tcp.rs @@ -9,6 +9,8 @@ use super::DropReason; use crate::ChecksumState; use crate::ConsommeState; use crate::IpAddresses; +use crate::dns_resolver::DnsResolver; +use crate::dns_resolver::dns_tcp::DnsTcpHandler; use futures::AsyncRead; use futures::AsyncWrite; use inspect::Inspect; @@ -121,10 +123,28 @@ enum LoopbackPortInfo { ProxyForGuestPort { sending_port: u16, guest_port: u16 }, } +/// The I/O backend for a TCP connection. +/// +/// A connection is either backed by a real host socket or a virtual DNS +/// handler that resolves DNS queries without a real socket. +enum TcpBackend { + /// A real host socket. The socket may be `None` while the connection is + /// being constructed, or after both ends have closed. + Socket(Option>), + /// A virtual DNS TCP handler (no real socket). + Dns(DnsTcpHandler), +} + #[derive(Inspect)] struct TcpConnection { #[inspect(skip)] - socket: Option>, + backend: TcpBackend, + #[inspect(flatten)] + inner: TcpConnectionInner, +} + +#[derive(Inspect)] +struct TcpConnectionInner { loopback_port: LoopbackPortInfo, state: TcpState, @@ -227,8 +247,8 @@ impl Access<'_, T> { // This supports a guest owning both the sending and receiving ports. if other_addr.ip().is_loopback() { for (other_ft, connection) in self.inner.tcp.connections.iter() { - if connection.state == TcpState::Connecting && other_ft.dst.port() == *port { - if let LoopbackPortInfo::ProxyForGuestPort{sending_port, guest_port} = connection.loopback_port { + if connection.inner.state == TcpState::Connecting && other_ft.dst.port() == *port { + if let LoopbackPortInfo::ProxyForGuestPort{sending_port, guest_port} = connection.inner.loopback_port { if sending_port == other_addr.port() { other_addr.set_port(guest_port); break; @@ -293,26 +313,41 @@ impl Access<'_, T> { }); // Check for any new incoming data self.inner.tcp.connections.retain(|ft, conn| { - conn.poll_conn( - cx, - &mut Sender { - ft, - state: &mut self.inner.state, - client: self.client, + let mut sender = Sender { + ft, + state: &mut self.inner.state, + client: self.client, + }; + match &mut conn.backend { + TcpBackend::Dns(dns_handler) => match &mut self.inner.dns { + Some(dns) => conn + .inner + .poll_dns_backend(cx, &mut sender, dns_handler, dns), + None => { + tracing::warn!("DNS TCP connection without DNS resolver, dropping"); + false + } }, - ) + TcpBackend::Socket(opt_socket) => { + conn.inner.poll_socket_backend(cx, &mut sender, opt_socket) + } + } }) } pub(crate) fn refresh_tcp_driver(&mut self) { self.inner.tcp.connections.retain(|_, conn| { - let Some(socket) = conn.socket.take() else { + let TcpBackend::Socket(opt_socket) = &mut conn.backend else { + // DNS connections have no real socket to refresh. + return true; + }; + let Some(socket) = opt_socket.take() else { return true; }; let socket = socket.into_inner(); match PolledSocket::new(self.client.driver(), socket) { Ok(socket) => { - conn.socket = Some(socket); + *opt_socket = Some(socket); true } Err(err) => { @@ -352,6 +387,9 @@ impl Access<'_, T> { }; tracing::trace!(?tcp, "tcp packet"); + let is_dns_tcp = + is_gateway_dns_tcp(&ft, &self.inner.state.params, self.inner.dns.is_some()); + let mut sender = Sender { ft: &ft, client: self.client, @@ -360,9 +398,18 @@ impl Access<'_, T> { match self.inner.tcp.connections.entry(ft) { hash_map::Entry::Occupied(mut e) => { - let conn = e.get_mut(); - if !conn.handle_packet(&mut sender, &tcp)? { + let keep = e.get_mut().inner.handle_packet(&mut sender, &tcp)?; + if !keep { + let dns_in_flight = matches!( + e.get().backend, + TcpBackend::Dns(ref h) if h.is_in_flight() + ); e.remove(); + if dns_in_flight { + if let Some(dns) = &mut self.inner.dns { + dns.complete_tcp_query(); + } + } } } hash_map::Entry::Vacant(e) => { @@ -372,8 +419,15 @@ impl Access<'_, T> { // This is for an old connection. Send reset. sender.rst(ack, None); } else if tcp.control == TcpControl::Syn { - let conn = - TcpConnection::new(&mut sender, &tcp, &self.inner.tcp.connection_params)?; + let conn = if is_dns_tcp { + TcpConnection::new_dns( + &mut sender, + &tcp, + &self.inner.tcp.connection_params, + )? + } else { + TcpConnection::new(&mut sender, &tcp, &self.inner.tcp.connection_params)? + }; e.insert(conn); } else { // Ignore the packet. @@ -527,7 +581,7 @@ impl Sender<'_, T> { } impl TcpConnection { - fn new_base(params: &ConnectionParams) -> Self { + fn new_base(params: &ConnectionParams) -> TcpConnectionInner { let mut rx_tx_seq = [0; 8]; getrandom::fill(&mut rx_tx_seq[..]).expect("prng failure"); let rx_seq = TcpSeqNumber(i32::from_ne_bytes( @@ -546,8 +600,7 @@ impl TcpConnection { .clamp(16384, 4 << 20) .next_power_of_two(); - Self { - socket: None, + TcpConnectionInner { loopback_port: LoopbackPortInfo::None, state: TcpState::Connecting, rx_buffer: VecDeque::new(), @@ -576,8 +629,8 @@ impl TcpConnection { tcp: &TcpRepr<'_>, params: &ConnectionParams, ) -> Result { - let mut this = Self::new_base(params); - this.initialize_from_first_client_packet(tcp)?; + let mut inner = Self::new_base(params); + inner.initialize_from_first_client_packet(tcp)?; let socket = Socket::new( match sender.ft.dst { @@ -619,7 +672,7 @@ impl TcpConnection { } Some(addr) => { if addr.ip().is_loopback() { - this.loopback_port = LoopbackPortInfo::ProxyForGuestPort { + inner.loopback_port = LoopbackPortInfo::ProxyForGuestPort { sending_port: addr.port(), guest_port: sender.ft.src.port(), }; @@ -627,8 +680,10 @@ impl TcpConnection { } } } - this.socket = Some(socket); - Ok(this) + Ok(Self { + backend: TcpBackend::Socket(Some(socket)), + inner, + }) } fn new_from_accept( @@ -636,17 +691,52 @@ impl TcpConnection { socket: Socket, params: &ConnectionParams, ) -> Result { - let mut this = Self { - socket: Some( - PolledSocket::new(sender.client.driver(), socket).map_err(DropReason::Io)?, - ), + let mut inner = TcpConnectionInner { state: TcpState::SynSent, ..Self::new_base(params) }; - this.send_syn(sender, None); - Ok(this) + inner.send_syn(sender, None); + Ok(Self { + backend: TcpBackend::Socket(Some( + PolledSocket::new(sender.client.driver(), socket).map_err(DropReason::Io)?, + )), + inner, + }) } + /// Create a virtual DNS TCP connection (no real host socket). + /// The connection completes the TCP handshake with the guest and + /// routes DNS queries through the provided resolver backend. + fn new_dns( + sender: &mut Sender<'_, impl Client>, + tcp: &TcpRepr<'_>, + params: &ConnectionParams, + ) -> Result { + let mut inner = Self::new_base(params); + inner.initialize_from_first_client_packet(tcp)?; + + let flow = crate::dns_resolver::DnsFlow { + src_addr: sender.ft.src.ip().into(), + dst_addr: sender.ft.dst.ip().into(), + src_port: sender.ft.src.port(), + dst_port: sender.ft.dst.port(), + gateway_mac: sender.state.params.gateway_mac, + client_mac: sender.state.params.client_mac, + transport: crate::dns_resolver::DnsTransport::Tcp, + }; + + // Immediately transition to SynReceived so the handshake SYN-ACK is sent. + inner.state = TcpState::SynReceived; + inner.send_syn(sender, Some(inner.rx_seq)); + + Ok(Self { + backend: TcpBackend::Dns(DnsTcpHandler::new(flow)), + inner, + }) + } +} + +impl TcpConnectionInner { fn initialize_from_first_client_packet(&mut self, tcp: &TcpRepr<'_>) -> Result<(), DropReason> { // The TCPv4 default maximum segment size is 536. This can be bigger for // IPv6. @@ -673,17 +763,85 @@ impl TcpConnection { Ok(()) } - fn poll_conn(&mut self, cx: &mut Context<'_>, sender: &mut Sender<'_, impl Client>) -> bool { + /// Poll the DNS TCP virtual connection backend. + /// + /// There is no real socket; data flows through the [`DnsTcpHandler`]. + fn poll_dns_backend( + &mut self, + cx: &mut Context<'_>, + sender: &mut Sender<'_, impl Client>, + dns_handler: &mut DnsTcpHandler, + dns: &mut DnsResolver, + ) -> bool { + // Propagate guest FIN before the tx path so that poll_read can + // detect EOF on the same iteration. + if self.state.rx_fin() && !dns_handler.guest_fin() { + dns_handler.set_guest_fin(); + } + + // tx path first: drain DNS responses into tx_buffer. + // This frees up backpressure so that ingest can make progress. + while !self.tx_buffer.is_full() { + let (a, b) = self.tx_buffer.unwritten_slices_mut(); + let mut bufs = [IoSliceMut::new(a), IoSliceMut::new(b)]; + match dns_handler.poll_read(cx, &mut bufs, dns) { + Poll::Ready(Ok(n)) => { + if n == 0 { + // EOF — close the connection. + if !self.state.tx_fin() { + self.close(); + } + break; + } + self.tx_buffer.extend_by(n); + } + Poll::Ready(Err(_)) => { + sender.rst(self.tx_send, Some(self.rx_seq)); + return false; + } + Poll::Pending => break, + } + } + + // rx path: feed guest data into the DNS handler for query extraction. + let (a, b) = self.rx_buffer.as_slices(); + match dns_handler.ingest(&[a, b], dns) { + Ok(consumed) if consumed > 0 => { + self.rx_buffer.drain(..consumed); + } + Ok(_) => {} + Err(_) => { + // Invalid DNS TCP framing; reset the connection. + sender.rst(self.tx_send, Some(self.rx_seq)); + return false; + } + } + + self.send_next(sender); + !(self.state == TcpState::TimeWait + || self.state == TcpState::LastAck + || (self.state.tx_fin() && self.state.rx_fin() && self.tx_buffer.is_empty())) + } + + /// Poll the real-socket TCP connection backend. + /// + /// Reads data from the host socket into the tx buffer (host -> guest) and + /// writes guest rx data into the host socket (guest -> host). + fn poll_socket_backend( + &mut self, + cx: &mut Context<'_>, + sender: &mut Sender<'_, impl Client>, + opt_socket: &mut Option>, + ) -> bool { + // Wait for the outbound connection to complete. if self.state == TcpState::Connecting { - match self - .socket - .as_mut() - .unwrap() - .poll_ready(cx, PollEvents::OUT) - { + let Some(socket) = opt_socket.as_mut() else { + return false; + }; + match socket.poll_ready(cx, PollEvents::OUT) { Poll::Ready(r) => { if r.has_err() { - self.handle_connect_error(sender); + self.handle_connect_error(sender, socket); return false; } @@ -698,7 +856,7 @@ impl TcpConnection { } // Handle the tx path. - if let Some(socket) = &mut self.socket { + if let Some(socket) = opt_socket.as_mut() { if self.state.tx_fin() { if let Poll::Ready(events) = socket.poll_ready(cx, PollEvents::EMPTY) { if events.has_err() { @@ -715,7 +873,7 @@ impl TcpConnection { } // Both ends are closed. Close the actual socket. - self.socket = None; + *opt_socket = None; } } else { while !self.tx_buffer.is_full() { @@ -750,7 +908,7 @@ impl TcpConnection { } // Handle the rx path. - if let Some(socket) = &mut self.socket { + if let Some(socket) = opt_socket.as_mut() { while !self.rx_buffer.is_empty() { let (a, b) = self.rx_buffer.as_slices(); let bufs = [IoSlice::new(a), IoSlice::new(b)]; @@ -789,8 +947,12 @@ impl TcpConnection { true } - fn handle_connect_error(&mut self, sender: &mut Sender<'_, impl Client>) { - let err = take_socket_error(self.socket.as_mut().unwrap()); + fn handle_connect_error( + &mut self, + sender: &mut Sender<'_, impl Client>, + socket: &mut PolledSocket, + ) { + let err = take_socket_error(socket); let reset = match err.kind() { ErrorKind::TimedOut => { // Avoid resetting so that the guest doesn't @@ -1297,3 +1459,14 @@ fn seq_min(seqs: [TcpSeqNumber; N]) -> TcpSeqNumber { } min } + +/// Check if a TCP connection targets the gateway's DNS port. +fn is_gateway_dns_tcp(ft: &FourTuple, params: &crate::ConsommeParams, dns_available: bool) -> bool { + if !dns_available || ft.dst.port() != crate::DNS_PORT { + return false; + } + match ft.dst.ip() { + IpAddr::V4(ip) => params.gateway_ip == ip, + IpAddr::V6(ip) => params.gateway_link_local_ipv6 == ip, + } +} diff --git a/vm/devices/net/net_consomme/consomme/src/tcp/ring.rs b/vm/devices/net/net_consomme/consomme/src/tcp/ring.rs index 7402535700..f1fa24e3dd 100644 --- a/vm/devices/net/net_consomme/consomme/src/tcp/ring.rs +++ b/vm/devices/net/net_consomme/consomme/src/tcp/ring.rs @@ -38,7 +38,6 @@ impl Ring { self.view(0..self.len()).as_slices() } - #[cfg(test)] pub fn is_empty(&self) -> bool { self.len() == 0 } diff --git a/vm/devices/net/net_consomme/consomme/src/udp.rs b/vm/devices/net/net_consomme/consomme/src/udp.rs index 536a3fb27e..645ab7f216 100644 --- a/vm/devices/net/net_consomme/consomme/src/udp.rs +++ b/vm/devices/net/net_consomme/consomme/src/udp.rs @@ -53,7 +53,7 @@ use std::task::Poll; use std::time::Duration; use std::time::Instant; -pub const DNS_PORT: u16 = 53; +use crate::DNS_PORT; pub(crate) struct Udp { connections: HashMap, @@ -204,7 +204,7 @@ impl Access<'_, T> { self.inner .dns .as_mut() - .and_then(|dns| match dns.poll_response(cx) { + .and_then(|dns| match dns.poll_udp_response(cx) { Poll::Ready(resp) => resp, Poll::Pending => None, }) @@ -442,13 +442,14 @@ impl Access<'_, T> { dst_port: udp.dst_port(), gateway_mac: self.inner.state.params.gateway_mac, client_mac: frame.src_addr, + transport: crate::dns_resolver::DnsTransport::Udp, }, dns_query: udp.payload(), }; // Submit the DNS query with addressing information // The response will be queued and sent later in poll_udp - dns.handle_dns(&request).map_err(|e| { + dns.submit_udp_query(&request).map_err(|e| { tracelimit::error_ratelimited!(error = ?e, "Failed to start DNS query"); DropReason::Packet(smoltcp::wire::Error) })?;