mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-09 08:14:42 +00:00
Fix DNS resolution on Ubuntu 22 with systemd-resolved (#77)
On Ubuntu 22 (systemd 249), systemd-resolved applies interface-scoped routing to per-link DNS servers. Configuring `resolvectl dns fips0 127.0.0.1:5354` caused resolved to attempt reaching 127.0.0.1 through fips0 (a TUN with only fd00::/8 routes), silently failing. The DNS responder never received queries. Newer systemd versions (250+) have explicit handling for loopback servers on non-loopback interfaces. Changes: - DNS responder default bind_addr changed from "127.0.0.1" to "::" so it listens on all interfaces, including fips0. Bind logic in lifecycle.rs now parses bind_addr as IpAddr and constructs a SocketAddr, handling IPv6 literal formatting. Factored into Node::bind_dns_socket with explicit IPV6_V6ONLY=0 via socket2, so IPv4 clients on 127.0.0.1:5354 still reach the responder regardless of the kernel's net.ipv6.bindv6only sysctl. - fips-dns-setup resolvectl backend now waits for fips0 to have a global IPv6 address, then configures resolved with [<fips0-addr>]:5354. That address is locally delivered by the kernel regardless of which interface resolved tries to route through. The dnsmasq and NetworkManager backends still use 127.0.0.1 (they don't have the interface-scoping issue). - Dropped hardcoded `bind_addr: "127.0.0.1"` from the packaged fips.yaml (Debian + OpenWrt). The shipped config was overriding the new default. - DNS queries are only accepted from the localhost. Verified end-to-end in a privileged Ubuntu 22.04 systemd container: dig @127.0.0.53 AAAA <npub>.fips resolves cleanly through systemd-resolved. The dns-delegate backend (systemd 258+) still uses 127.0.0.1; it has not been verified whether that backend has the same routing issue.
This commit is contained in:
+132
-23
@@ -688,32 +688,65 @@ impl Node {
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize DNS responder (independent of TUN)
|
||||
// Initialize DNS responder (independent of TUN).
|
||||
//
|
||||
// The default bind_addr is "::" (all interfaces, dual-stack). This
|
||||
// matters on Ubuntu 22 (systemd 249): systemd-resolved applies
|
||||
// interface-scoped routing to per-link DNS servers — when resolvectl
|
||||
// points fips0 at an address, resolved tries to reach it through
|
||||
// fips0. Binding to "::" ensures the responder is reachable via fips0
|
||||
// as well as loopback (v4 and v6). `IPV6_V6ONLY=0` is set explicitly
|
||||
// so IPv4 clients on 127.0.0.1 still reach us regardless of kernel
|
||||
// sysctl defaults.
|
||||
if self.config.dns.enabled {
|
||||
let bind = format!("{}:{}", self.config.dns.bind_addr(), self.config.dns.port());
|
||||
match tokio::net::UdpSocket::bind(&bind).await {
|
||||
Ok(socket) => {
|
||||
let dns_channel_size = self.config.node.buffers.dns_channel;
|
||||
let (identity_tx, identity_rx) = tokio::sync::mpsc::channel(dns_channel_size);
|
||||
let dns_ttl = self.config.dns.ttl();
|
||||
let base_hosts =
|
||||
crate::upper::hosts::HostMap::from_peer_configs(self.config.peers());
|
||||
let hosts_path =
|
||||
std::path::PathBuf::from(crate::upper::hosts::DEFAULT_HOSTS_PATH);
|
||||
let reloader =
|
||||
crate::upper::hosts::HostMapReloader::new(base_hosts, hosts_path);
|
||||
info!(bind = %bind, hosts = reloader.hosts().len(), "DNS responder started for .fips domain (auto-reload enabled)");
|
||||
let handle = tokio::spawn(crate::upper::dns::run_dns_responder(
|
||||
socket,
|
||||
identity_tx,
|
||||
dns_ttl,
|
||||
reloader,
|
||||
));
|
||||
self.dns_identity_rx = Some(identity_rx);
|
||||
self.dns_task = Some(handle);
|
||||
let addr_str = self.config.dns.bind_addr();
|
||||
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) {
|
||||
Ok(socket) => {
|
||||
let dns_channel_size = self.config.node.buffers.dns_channel;
|
||||
let (identity_tx, identity_rx) =
|
||||
tokio::sync::mpsc::channel(dns_channel_size);
|
||||
let dns_ttl = self.config.dns.ttl();
|
||||
let base_hosts = crate::upper::hosts::HostMap::from_peer_configs(
|
||||
self.config.peers(),
|
||||
);
|
||||
let hosts_path =
|
||||
std::path::PathBuf::from(crate::upper::hosts::DEFAULT_HOSTS_PATH);
|
||||
let reloader =
|
||||
crate::upper::hosts::HostMapReloader::new(base_hosts, hosts_path);
|
||||
// Resolve the TUN ifindex so the responder can
|
||||
// drop queries arriving on the mesh interface
|
||||
// (fips0). Without this, the `::` bind exposes
|
||||
// /etc/fips/hosts alias probing to any mesh peer.
|
||||
// When TUN isn't enabled or the name can't be
|
||||
// resolved, `None` disables the filter (there
|
||||
// is no mesh surface to defend anyway).
|
||||
let mesh_ifindex = Self::lookup_mesh_ifindex(self.config.tun.name());
|
||||
info!(
|
||||
bind = %bind,
|
||||
hosts = reloader.hosts().len(),
|
||||
mesh_ifindex = ?mesh_ifindex,
|
||||
"DNS responder started for .fips domain (auto-reload enabled)"
|
||||
);
|
||||
let handle = tokio::spawn(crate::upper::dns::run_dns_responder(
|
||||
socket,
|
||||
identity_tx,
|
||||
dns_ttl,
|
||||
reloader,
|
||||
mesh_ifindex,
|
||||
));
|
||||
self.dns_identity_rx = Some(identity_rx);
|
||||
self.dns_task = Some(handle);
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(bind = %bind, error = %e, "Failed to start DNS responder");
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(bind = %bind, error = %e, "Failed to start DNS responder");
|
||||
warn!(addr = %addr_str, error = %e, "Invalid dns.bind_addr; DNS responder not started");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -726,6 +759,82 @@ 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 mesh TUN interface index by name.
|
||||
///
|
||||
/// Returns `None` if the interface does not exist (e.g. TUN disabled
|
||||
/// or not yet created). A `None` result disables the DNS responder's
|
||||
/// mesh-interface filter — safe, because if there is no fips0 there
|
||||
/// is no mesh exposure to defend against.
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/// Stop the node.
|
||||
///
|
||||
/// Shuts down TUN interface, stops I/O threads, and transitions to
|
||||
|
||||
Reference in New Issue
Block a user