Move the DNS socket helpers into the ipv6tun DNS module

Binding the responder's dual-stack socket, enabling IPV6_RECVPKTINFO on
it and resolving the mesh interface index are host-side socket setup for
the .fips responder. They lived on Node as associated functions that
touch no node state. Move them next to the responder they serve, in
ipv6tun::dns.

The DNS child start now calls ipv6tun::dns::bind_dns_socket directly, and
Node::mesh_ifindex keeps its reading of the live TUN name and resolves it
through the moved lookup. The code is unchanged, including the cfg(unix)
gating of the PKTINFO setsockopt and the non-unix ifindex fallback. None
of the moved functions log, so no tracing target changes.
This commit is contained in:
Johnathan Corgan
2026-09-24 14:05:33 +00:00
parent 92a3a26e67
commit 26201a4fe1
2 changed files with 79 additions and 77 deletions
+75 -2
View File
@@ -164,7 +164,7 @@ fn is_mesh_interface_query(arrival_ifindex: Option<u32>, mesh_ifindex: Option<u3
/// by the `::` bind: mesh peers can reach the listener over fips0 and
/// probe `/etc/fips/hosts` aliases via dictionary attack. The check
/// requires `IPV6_RECVPKTINFO` to be enabled on the socket (done in
/// `Node::bind_dns_socket`); if it is not, arrival ifindex is unknown
/// `bind_dns_socket`); if it is not, arrival ifindex is unknown
/// and no filter is applied.
pub async fn run_dns_responder(
socket: tokio::net::UdpSocket,
@@ -219,6 +219,79 @@ pub async fn run_dns_responder(
}
}
/// Bind a UDP socket for the DNS responder.
///
/// For IPv6 binds (including `::`), sets `IPV6_V6ONLY=0` so the socket
/// also accepts IPv4-mapped addresses. This guarantees dual-stack
/// delivery regardless of `net.ipv6.bindv6only` sysctl on the host —
/// v4 clients on 127.0.0.1 and v6 clients on the fips0 address both
/// land on the same socket.
///
/// Also enables `IPV6_RECVPKTINFO` on IPv6 sockets so the responder
/// can learn the arrival interface per packet. The responder uses that
/// to drop queries arriving on the mesh TUN, closing the hosts-file
/// probing side-channel created by the `::` bind.
pub(crate) fn bind_dns_socket(
addr: std::net::SocketAddr,
) -> Result<tokio::net::UdpSocket, std::io::Error> {
use socket2::{Domain, Protocol, Socket, Type};
let domain = if addr.is_ipv4() {
Domain::IPV4
} else {
Domain::IPV6
};
let sock = Socket::new(domain, Type::DGRAM, Some(Protocol::UDP))?;
if addr.is_ipv6() {
sock.set_only_v6(false)?;
#[cfg(unix)]
set_recv_pktinfo_v6(&sock)?;
}
sock.set_nonblocking(true)?;
sock.bind(&addr.into())?;
tokio::net::UdpSocket::from_std(sock.into())
}
/// Enable `IPV6_RECVPKTINFO` on an IPv6 UDP socket.
///
/// After this setsockopt, each `recvmsg()` call on the socket receives
/// an `IPV6_PKTINFO` control message containing the arrival interface
/// index, which the DNS responder uses for its mesh-interface filter.
#[cfg(unix)]
fn set_recv_pktinfo_v6(sock: &socket2::Socket) -> Result<(), std::io::Error> {
use std::os::fd::AsRawFd;
let enable: libc::c_int = 1;
let ret = unsafe {
libc::setsockopt(
sock.as_raw_fd(),
libc::IPPROTO_IPV6,
libc::IPV6_RECVPKTINFO,
&enable as *const _ as *const libc::c_void,
std::mem::size_of::<libc::c_int>() as libc::socklen_t,
)
};
if ret < 0 {
return Err(std::io::Error::last_os_error());
}
Ok(())
}
/// Resolve an interface index by name.
///
/// Returns `None` if the interface does not exist.
pub(crate) fn lookup_mesh_ifindex(name: &str) -> Option<u32> {
#[cfg(unix)]
{
let c_name = std::ffi::CString::new(name).ok()?;
let idx = unsafe { libc::if_nametoindex(c_name.as_ptr()) };
if idx == 0 { None } else { Some(idx) }
}
#[cfg(not(unix))]
{
let _ = name;
None
}
}
/// Receive a UDP datagram with arrival-interface info via `IPV6_PKTINFO`.
///
/// Returns `(len, src, arrival_ifindex)`. The ifindex is `Some` when the
@@ -819,7 +892,7 @@ mod tests {
}
/// Build a socket bound to `[::1]:0` with `IPV6_RECVPKTINFO` enabled,
/// mirroring the setup done in `Node::bind_dns_socket`.
/// mirroring the setup done in `bind_dns_socket`.
#[cfg(unix)]
fn bind_loopback_v6_with_pktinfo() -> tokio::net::UdpSocket {
use socket2::{Domain, Protocol, Socket, Type};
+4 -75
View File
@@ -1864,7 +1864,7 @@ impl Node {
match addr_str.parse::<std::net::IpAddr>() {
Ok(ip) => {
let bind = std::net::SocketAddr::new(ip, self.config().dns.port());
match Self::bind_dns_socket(bind) {
match crate::ipv6tun::dns::bind_dns_socket(bind) {
Ok(socket) => {
// Read the bound address back off the socket
// rather than reusing `bind`: a port-0 config
@@ -2072,62 +2072,6 @@ impl Node {
Ok(())
}
/// Bind a UDP socket for the DNS responder.
///
/// For IPv6 binds (including `::`), sets `IPV6_V6ONLY=0` so the socket
/// also accepts IPv4-mapped addresses. This guarantees dual-stack
/// delivery regardless of `net.ipv6.bindv6only` sysctl on the host —
/// v4 clients on 127.0.0.1 and v6 clients on the fips0 address both
/// land on the same socket.
///
/// Also enables `IPV6_RECVPKTINFO` on IPv6 sockets so the responder
/// can learn the arrival interface per packet. The responder uses that
/// to drop queries arriving on the mesh TUN, closing the hosts-file
/// probing side-channel created by the `::` bind.
fn bind_dns_socket(
addr: std::net::SocketAddr,
) -> Result<tokio::net::UdpSocket, std::io::Error> {
use socket2::{Domain, Protocol, Socket, Type};
let domain = if addr.is_ipv4() {
Domain::IPV4
} else {
Domain::IPV6
};
let sock = Socket::new(domain, Type::DGRAM, Some(Protocol::UDP))?;
if addr.is_ipv6() {
sock.set_only_v6(false)?;
#[cfg(unix)]
Self::set_recv_pktinfo_v6(&sock)?;
}
sock.set_nonblocking(true)?;
sock.bind(&addr.into())?;
tokio::net::UdpSocket::from_std(sock.into())
}
/// Enable `IPV6_RECVPKTINFO` on an IPv6 UDP socket.
///
/// After this setsockopt, each `recvmsg()` call on the socket receives
/// an `IPV6_PKTINFO` control message containing the arrival interface
/// index, which the DNS responder uses for its mesh-interface filter.
#[cfg(unix)]
fn set_recv_pktinfo_v6(sock: &socket2::Socket) -> Result<(), std::io::Error> {
use std::os::fd::AsRawFd;
let enable: libc::c_int = 1;
let ret = unsafe {
libc::setsockopt(
sock.as_raw_fd(),
libc::IPPROTO_IPV6,
libc::IPV6_RECVPKTINFO,
&enable as *const _ as *const libc::c_void,
std::mem::size_of::<libc::c_int>() as libc::socklen_t,
)
};
if ret < 0 {
return Err(std::io::Error::last_os_error());
}
Ok(())
}
/// Resolve the index of the mesh TUN device this node actually created.
///
/// Reads the device name recorded when the TUN was brought up, which is
@@ -2137,24 +2081,9 @@ impl Node {
/// An app-owned TUN also leaves the name unset, so the filter stays off
/// there even though a mesh interface exists.
pub(crate) fn mesh_ifindex(&self) -> Option<u32> {
self.tun_name.as_deref().and_then(Self::lookup_mesh_ifindex)
}
/// Resolve an interface index by name.
///
/// Returns `None` if the interface does not exist.
fn lookup_mesh_ifindex(name: &str) -> Option<u32> {
#[cfg(unix)]
{
let c_name = std::ffi::CString::new(name).ok()?;
let idx = unsafe { libc::if_nametoindex(c_name.as_ptr()) };
if idx == 0 { None } else { Some(idx) }
}
#[cfg(not(unix))]
{
let _ = name;
None
}
self.tun_name
.as_deref()
.and_then(crate::ipv6tun::dns::lookup_mesh_ifindex)
}
/// Stop the node.