mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-09 16:24:45 +00:00
The previous default configured systemd-resolved with `resolvectl dns fips0 [<fips0_addr>]:5354`, intended to bypass an Ubuntu 22 systemd 249 interface-scoping bug. That target collides with the daemon's mesh-interface filter on Linux: when an IPv6 packet's destination belongs to a non-loopback interface, the kernel attributes the packet to that interface in IPV6_PKTINFO (ipi6_ifindex == fips0) even though loopback delivery is used (tcpdump shows lo). The mesh-interface filter sees arrival_ifindex == mesh_ifindex and silently drops every query at trace level — invisible to operators at the default debug level. Net effect on stock deployments: every .fips query on systemd-resolved hosts was silently dropped. Daemon side ----------- - Default `dns.bind_addr` changes from "::" to "::1" (IPv6 loopback only). The mesh-interface filter is then defanged on the default path because loopback isn't reachable from mesh peers. The filter remains in place defensively for operators who explicitly bind "::" to expose a mesh-reachable responder. fips-dns-setup backend unification ---------------------------------- - New `try_global_drop_in` backend writes /etc/systemd/resolved.conf.d/fips.conf with DNS=[::1]:5354 and Domains=~fips. Inserted ahead of `try_resolvectl` in the dispatch chain. The standard loopback path has no interface scoping, so ipi6_ifindex reports lo and the filter passes. - All other backends now target [::1]:5354 to match the daemon's default IPv6-loopback bind: - try_dns_delegate writes DNS=[::1]:5354 - try_dnsmasq writes server=/fips/::1#5354 - try_nm_dnsmasq writes server=/fips/::1#5354 - Fixed dns-delegate file path: was /etc/systemd/dns-delegate/, must be /etc/systemd/dns-delegate.d/ (with .d suffix). systemd-resolved silently ignored the previous path. - fips-dns-teardown handles the new global-drop-in backend in cleanup. - The legacy resolvectl per-link backend stays as a fallback, documented to require careful daemon bind_addr coordination. fips-gateway upstream pairing ----------------------------- - gateway.dns.upstream default changes from 127.0.0.1:5354 to [::1]:5354 to match the daemon's default bind. Linux IPv6 sockets bound to explicit ::1 do not accept v4-mapped traffic, so the old default would have caused the gateway's startup DNS reachability probe to time out and systemd to restart-loop the service. - Operators who set a non-default daemon `dns.bind_addr` must also set `gateway.dns.upstream` to match — documented inline. Documentation ------------- - packaging/common/fips.yaml and packaging/openwrt-ipk fips.yaml examples updated; rationale for the bind_addr choice and the daemon/gateway pairing recorded inline. Test coverage ------------- - testing/dns-resolver/test.sh: real-fipsd end-to-end scenario added. Builds fipsd in a Debian 12 builder image (cached), runs the daemon with a real TUN in a privileged container, configures DNS via the setup script, and asserts `dig @127.0.0.53 AAAA <npub>.fips` returns AAAA. Refactored as a parameterized helper running across Debian 12/13 and Ubuntu 22/24/26 (5 e2e scenarios). Backend-aware assertions: on systemd >= 258 the expected backend is dns-delegate; on older systemd it's global-drop-in. Strict content checks fail CI on any [::1]:5354 drift. fips-gateway also exercised in the debian12 scenario to lock the gateway-upstream pairing. Renamed all "fipsd" references to "fips" (project convention). - testing/deb-install/ (new harness): builds the actual .deb via cargo-deb in a Debian 12 builder image (cached), installs via apt across each target distro, verifies maintainer scripts, conffile placement, binary placement, and end-to-end .fips resolution after start. Also exercises fips-gateway against the installed daemon to verify the gateway/daemon default pairing on a real .deb path. - This is the test layer that was missing — the previous harness only verified config files were written, never that queries reached the daemon. Verified: dns-resolver 78/78 assertions, deb-install 55/55 assertions across all 5 distros (debian:12, debian:trixie, ubuntu:22.04, ubuntu:24.04, ubuntu:26.04).
114 lines
3.3 KiB
Rust
114 lines
3.3 KiB
Rust
//! Upper layer configuration types.
|
|
//!
|
|
//! Configuration for the IPv6 adaptation layer components: TUN interface
|
|
//! and DNS responder.
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
/// Default TUN device name.
|
|
const DEFAULT_TUN_NAME: &str = "fips0";
|
|
|
|
/// Default TUN MTU (IPv6 minimum).
|
|
const DEFAULT_TUN_MTU: u16 = 1280;
|
|
|
|
/// Default DNS responder bind address.
|
|
///
|
|
/// Loopback by default. The shipped `fips-dns-setup` configures
|
|
/// systemd-resolved with a global drop-in pointing at `[::1]:5354`
|
|
/// (instead of a per-link `resolvectl dns fips0 [<fips0_addr>]:5354`),
|
|
/// which avoids a Linux IPV6_PKTINFO behaviour where self-destined
|
|
/// traffic to a TUN address is attributed to the TUN's ifindex —
|
|
/// causing the mesh-interface filter to silently drop every query.
|
|
///
|
|
/// To expose the responder to mesh peers, set `bind_addr: "::"` in
|
|
/// fips.yaml. The `is_mesh_interface_query` filter in `src/upper/dns.rs`
|
|
/// is still in place to prevent hosts-file alias enumeration in that
|
|
/// mode. See `packaging/common/fips-dns-setup` for backend selection.
|
|
const DEFAULT_DNS_BIND_ADDR: &str = "::1";
|
|
|
|
/// Default DNS responder port.
|
|
const DEFAULT_DNS_PORT: u16 = 5354;
|
|
|
|
/// Default DNS record TTL in seconds (5 minutes).
|
|
const DEFAULT_DNS_TTL: u32 = 300;
|
|
|
|
fn default_true() -> bool {
|
|
true
|
|
}
|
|
|
|
/// DNS responder configuration (`dns.*`).
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct DnsConfig {
|
|
/// Enable DNS responder (`dns.enabled`, default: true).
|
|
#[serde(default = "default_true")]
|
|
pub enabled: bool,
|
|
|
|
/// Bind address (`dns.bind_addr`).
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub bind_addr: Option<String>,
|
|
|
|
/// Port (`dns.port`).
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub port: Option<u16>,
|
|
|
|
/// Record TTL in seconds (`dns.ttl`).
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub ttl: Option<u32>,
|
|
}
|
|
|
|
impl Default for DnsConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
enabled: true,
|
|
bind_addr: None,
|
|
port: None,
|
|
ttl: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl DnsConfig {
|
|
/// Get the bind address (default: `::1`, IPv6 loopback only).
|
|
pub fn bind_addr(&self) -> &str {
|
|
self.bind_addr.as_deref().unwrap_or(DEFAULT_DNS_BIND_ADDR)
|
|
}
|
|
|
|
/// Get the port (default: 5354).
|
|
pub fn port(&self) -> u16 {
|
|
self.port.unwrap_or(DEFAULT_DNS_PORT)
|
|
}
|
|
|
|
/// Get the TTL in seconds (default: 300).
|
|
pub fn ttl(&self) -> u32 {
|
|
self.ttl.unwrap_or(DEFAULT_DNS_TTL)
|
|
}
|
|
}
|
|
|
|
/// TUN interface configuration (`tun.*`).
|
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
|
pub struct TunConfig {
|
|
/// Enable TUN interface (`tun.enabled`).
|
|
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
|
pub enabled: bool,
|
|
|
|
/// Device name (`tun.name`).
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub name: Option<String>,
|
|
|
|
/// MTU (`tun.mtu`).
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub mtu: Option<u16>,
|
|
}
|
|
|
|
impl TunConfig {
|
|
/// Get the device name (default: "fips0").
|
|
pub fn name(&self) -> &str {
|
|
self.name.as_deref().unwrap_or(DEFAULT_TUN_NAME)
|
|
}
|
|
|
|
/// Get the MTU (default: 1280).
|
|
pub fn mtu(&self) -> u16 {
|
|
self.mtu.unwrap_or(DEFAULT_TUN_MTU)
|
|
}
|
|
}
|