Files
fips/src/bin/fips-gateway.rs
T
Johnathan Corgan b2ad24363f Remove unresolvable internal references from source comments
Fifteen comments in src/ cited internal identifiers that a reader of the
published source cannot resolve. Each now states the thing the identifier
stood for, or drops the citation where the surrounding text already carries
the meaning.

The MMP report group needed more than a rewording. It claimed the payload
content was undefined and both variants were stubs, and neither is true: the
link dispatcher routes 0x01 and 0x02 to real handlers, and both report types
implement encode and decode. The group comment now names the types, and the
two stale "(stub)" doc comments below it are corrected with it.
2026-08-16 06:37:21 +00:00

506 lines
17 KiB
Rust

//! FIPS outbound LAN gateway binary.
//!
//! Allows unmodified LAN hosts to reach FIPS mesh destinations via
//! DNS-allocated virtual IPs and kernel nftables NAT.
#[cfg(target_os = "linux")]
use clap::Parser;
#[cfg(target_os = "linux")]
use fips::Config;
#[cfg(target_os = "linux")]
use fips::gateway::{control, dns, nat, net, pool};
#[cfg(target_os = "linux")]
use fips::version;
#[cfg(target_os = "linux")]
use std::path::PathBuf;
#[cfg(target_os = "linux")]
use std::sync::Arc;
#[cfg(target_os = "linux")]
use std::sync::atomic::{AtomicUsize, Ordering};
#[cfg(target_os = "linux")]
use std::time::Instant;
#[cfg(target_os = "linux")]
use tokio::signal::unix::{SignalKind, signal};
#[cfg(target_os = "linux")]
use tokio::sync::{Mutex, mpsc, watch};
#[cfg(target_os = "linux")]
use tracing::{error, info, warn};
#[cfg(target_os = "linux")]
use tracing_subscriber::{EnvFilter, fmt};
/// FIPS outbound LAN gateway
#[cfg(target_os = "linux")]
#[derive(Parser, Debug)]
#[command(
name = "fips-gateway",
version = version::short_version(),
long_version = version::long_version(),
about
)]
struct Args {
/// Path to configuration file (overrides default search paths).
#[arg(short, long, value_name = "FILE")]
config: Option<PathBuf>,
/// Log level (trace, debug, info, warn, error).
#[arg(short, long, default_value = "info")]
log_level: String,
}
#[cfg(not(target_os = "linux"))]
fn main() {
eprintln!("fips-gateway requires Linux (nftables unavailable on this platform)");
std::process::exit(1);
}
#[cfg(target_os = "linux")]
#[tokio::main(flavor = "current_thread")]
async fn main() {
let args = Args::parse();
// Initialize logging
let filter = EnvFilter::builder()
.with_default_directive(
args.log_level
.parse()
.unwrap_or_else(|_| tracing::level_filters::LevelFilter::INFO.into()),
)
.from_env_lossy();
// As in the daemon: a failed log write must not panic whoever logged. The
// default reports write failures with `eprintln!`, which panics when stderr
// fails too, and both units send stdout and stderr to journald. Here the
// casualty is a spawned task — the DNS resolver or the pool tick — whose
// handle nothing observes until shutdown.
fmt()
.with_env_filter(filter)
.with_target(true)
.log_internal_errors(false)
.init();
info!("fips-gateway {} starting", version::short_version());
// Load configuration
let config = if let Some(config_path) = &args.config {
match Config::load_file(config_path) {
Ok(config) => {
info!(path = %config_path.display(), "Loaded config file");
config
}
Err(e) => {
error!(
"Failed to load config from {}: {}",
config_path.display(),
e
);
std::process::exit(1);
}
}
} else {
match Config::load() {
Ok((config, paths)) => {
if paths.is_empty() {
warn!("No config files found, using defaults");
} else {
for path in &paths {
info!(path = %path.display(), "Loaded config file");
}
}
config
}
Err(e) => {
error!("Failed to load config: {}", e);
std::process::exit(1);
}
}
};
// Validate gateway config
let gw_config = match &config.gateway {
Some(gw) if gw.enabled => gw.clone(),
Some(_) => {
error!("Gateway section exists but is not enabled (gateway.enabled = false)");
std::process::exit(1);
}
None => {
error!("No gateway section in configuration");
std::process::exit(1);
}
};
if let Err(e) = gw_config.validate_port_forwards() {
error!("Invalid gateway.port_forwards: {e}");
std::process::exit(1);
}
info!(
pool = %gw_config.pool,
lan_interface = %gw_config.lan_interface,
port_forwards = gw_config.port_forwards.len(),
"Gateway config loaded"
);
// --- Prerequisites ---
// Check IPv6 forwarding
net::check_ipv6_forwarding();
// Check fips0 interface exists
if let Err(e) = net::check_interface_exists("fips0").await {
error!(error = %e, "fips0 interface not found — is the FIPS daemon running?");
std::process::exit(1);
}
// Check LAN interface exists
if let Err(e) = net::check_interface_exists(&gw_config.lan_interface).await {
error!(
error = %e,
interface = %gw_config.lan_interface,
"LAN interface not found"
);
std::process::exit(1);
}
// Check DNS upstream reachability (proves the FIPS daemon is running)
{
let upstream = gw_config.dns.upstream();
info!(upstream = %upstream, "Checking DNS upstream reachability");
use std::net::ToSocketAddrs;
let upstream_addr = match upstream.to_socket_addrs() {
Ok(mut addrs) => match addrs.next() {
Some(addr) => addr,
None => {
error!(upstream = %upstream, "DNS upstream address resolved to nothing");
std::process::exit(1);
}
},
Err(e) => {
error!(upstream = %upstream, error = %e, "Invalid DNS upstream address");
std::process::exit(1);
}
};
// Build a minimal DNS query for "test.fips" AAAA
// Header: ID=0x1234, flags=0x0100 (standard query, RD=1),
// QDCOUNT=1, ANCOUNT=0, NSCOUNT=0, ARCOUNT=0
// Question: 4test4fips0 QTYPE=AAAA(28) QCLASS=IN(1)
let query: Vec<u8> = vec![
0x12, 0x34, // ID
0x01, 0x00, // Flags: standard query, RD=1
0x00, 0x01, // QDCOUNT = 1
0x00, 0x00, // ANCOUNT = 0
0x00, 0x00, // NSCOUNT = 0
0x00, 0x00, // ARCOUNT = 0
// QNAME: "test.fips"
0x04, b't', b'e', b's', b't', 0x04, b'f', b'i', b'p', b's', 0x00, 0x00,
0x1C, // QTYPE = AAAA (28)
0x00, 0x01, // QCLASS = IN (1)
];
let bind_addr = if upstream_addr.is_ipv4() {
"0.0.0.0:0"
} else {
"[::]:0"
};
let sock = match tokio::net::UdpSocket::bind(bind_addr).await {
Ok(s) => s,
Err(e) => {
error!(error = %e, "Failed to bind UDP socket for DNS check");
std::process::exit(1);
}
};
// Retry the upstream probe up to MAX_PROBE_ATTEMPTS times with a
// 1-second per-attempt timeout and a 1-second sleep between
// attempts. Total worst-case wait: ~10 seconds.
//
// Bounded retry covers the cold-boot race where this gateway and
// the fips daemon start at approximately the same time: the
// daemon's TUN may be up (the systemd ExecStartPre wait already
// gates on that) while its DNS responder is still binding
// [::1]:5354. Without this retry, the gateway hard-failed after
// a single 3-second probe and relied on Restart=on-failure for
// recovery.
const MAX_PROBE_ATTEMPTS: u32 = 5;
const PROBE_TIMEOUT_SECS: u64 = 1;
const PROBE_RETRY_DELAY_SECS: u64 = 1;
let mut buf = [0u8; 512];
let mut last_failure: Option<String> = None;
let mut succeeded = false;
for attempt in 1..=MAX_PROBE_ATTEMPTS {
if let Err(e) = sock.send_to(&query, upstream_addr).await {
last_failure = Some(format!("send_to failed: {}", e));
} else {
match tokio::time::timeout(
std::time::Duration::from_secs(PROBE_TIMEOUT_SECS),
sock.recv_from(&mut buf),
)
.await
{
Ok(Ok(_)) => {
info!(
upstream = %upstream, attempt = attempt,
"DNS upstream is reachable"
);
succeeded = true;
break;
}
Ok(Err(e)) => {
last_failure = Some(format!("recv_from failed: {}", e));
}
Err(_) => {
last_failure = Some(format!("no response within {}s", PROBE_TIMEOUT_SECS));
}
}
}
if attempt < MAX_PROBE_ATTEMPTS {
info!(
upstream = %upstream, attempt = attempt,
last = ?last_failure,
"DNS upstream probe attempt failed; retrying"
);
tokio::time::sleep(std::time::Duration::from_secs(PROBE_RETRY_DELAY_SECS)).await;
}
}
if !succeeded {
error!(
upstream = %upstream,
attempts = MAX_PROBE_ATTEMPTS,
last = ?last_failure,
"DNS upstream did not become reachable after exhausting retries — is the FIPS daemon running?"
);
std::process::exit(1);
}
}
// --- Initialize components ---
// Virtual IP pool
let ip_pool = match pool::VirtualIpPool::new(
&gw_config.pool,
gw_config.dns.ttl() as u64,
gw_config.grace_period(),
) {
Ok(p) => Arc::new(Mutex::new(p)),
Err(e) => {
error!(error = %e, "Failed to create virtual IP pool");
std::process::exit(1);
}
};
// NAT manager
let mut nat_mgr = match nat::NatManager::new(gw_config.lan_interface.clone()) {
Ok(n) => n,
Err(e) => {
error!(error = %e, "Failed to create nftables table — do you have CAP_NET_ADMIN?");
std::process::exit(1);
}
};
// Install inbound port-forward rules.
if let Err(e) = nat_mgr.set_port_forwards(&gw_config.port_forwards) {
error!(error = %e, "Failed to install port-forward rules");
let _ = nat_mgr.cleanup();
std::process::exit(1);
}
// Network setup
let mut net_setup = net::NetSetup::new(gw_config.lan_interface.clone(), gw_config.pool.clone());
// Add pool route
if let Err(e) = net_setup.add_pool_route().await {
error!(error = %e, "Failed to add pool route");
// Clean up NAT table before exit
let _ = nat_mgr.cleanup();
std::process::exit(1);
}
// --- Channels ---
// Pool events (new/removed mappings) → NAT + net modules
let (event_tx, mut event_rx) = mpsc::channel::<pool::PoolEvent>(64);
// Shutdown signal
let (shutdown_tx, shutdown_rx) = watch::channel(false);
// --- Start DNS resolver task ---
let dns_pool = Arc::clone(&ip_pool);
let dns_event_tx = event_tx.clone();
let dns_shutdown = shutdown_rx.clone();
let dns_listen = gw_config.dns.listen().to_string();
let dns_upstream = gw_config.dns.upstream().to_string();
let dns_ttl = gw_config.dns.ttl();
let dns_task = tokio::spawn(async move {
if let Err(e) = dns::run_dns_resolver(
&dns_listen,
&dns_upstream,
dns_ttl,
dns_pool,
dns_event_tx,
dns_shutdown,
)
.await
{
error!(error = %e, "DNS resolver error");
}
});
// --- Snapshot channel for control socket ---
let (snapshot_tx, snapshot_rx) = watch::channel::<Option<control::GatewaySnapshot>>(None);
let start_time = Instant::now();
// --- Start control socket ---
let control_task = match control::GatewayControlSocket::bind() {
Ok(socket) => {
let rx = snapshot_rx.clone();
Some(tokio::spawn(async move {
socket.accept_loop(rx).await;
}))
}
Err(e) => {
warn!(error = %e, "Failed to bind gateway control socket — continuing without it");
None
}
};
// --- NAT mapping counter (shared with tick task for snapshots) ---
let nat_count = Arc::new(AtomicUsize::new(0));
// --- Start pool tick task ---
let tick_pool = Arc::clone(&ip_pool);
let tick_event_tx = event_tx;
let tick_nat_count = Arc::clone(&nat_count);
let mut tick_shutdown = shutdown_rx.clone();
let conntrack = pool::ProcConntrack;
let snap_config = control::SnapshotConfig {
pool_cidr: gw_config.pool.clone(),
lan_interface: gw_config.lan_interface.clone(),
dns_upstream: gw_config.dns.upstream().to_string(),
dns_listen: gw_config.dns.listen().to_string(),
dns_ttl: gw_config.dns.ttl(),
pool_grace_period: gw_config.grace_period(),
};
let tick_task = tokio::spawn(async move {
let mut interval = tokio::time::interval(std::time::Duration::from_secs(10));
loop {
tokio::select! {
_ = interval.tick() => {
let now = Instant::now();
let mut pool_guard = tick_pool.lock().await;
let events = pool_guard.tick(now, &conntrack);
// Build snapshot for control socket
let pool_status = pool_guard.status();
let mappings = pool_guard.mapping_info(now);
drop(pool_guard);
let snapshot = control::build_snapshot(
pool_status,
mappings,
tick_nat_count.load(Ordering::Relaxed),
start_time,
&snap_config,
);
let _ = snapshot_tx.send(Some(snapshot));
for event in events {
let _ = tick_event_tx.send(event).await;
}
}
_ = tick_shutdown.changed() => break,
}
}
});
// --- Event processing loop ---
let mut sigterm = signal(SignalKind::terminate()).expect("failed to register SIGTERM handler");
info!("fips-gateway running");
loop {
tokio::select! {
Some(event) = event_rx.recv() => {
match event {
pool::PoolEvent::MappingCreated { virtual_ip, mesh_addr } => {
// Add NAT rules
if let Err(e) = nat_mgr.add_mapping(virtual_ip, mesh_addr) {
error!(error = %e, virtual_ip = %virtual_ip, "Failed to add NAT rules");
}
nat_count.store(nat_mgr.mapping_count(), Ordering::Relaxed);
// Add proxy NDP entry
if let Err(e) = net_setup.add_proxy_ndp(virtual_ip).await {
error!(error = %e, virtual_ip = %virtual_ip, "Failed to add proxy NDP");
}
}
pool::PoolEvent::MappingRemoved { virtual_ip, mesh_addr: _ } => {
// Remove NAT rules
if let Err(e) = nat_mgr.remove_mapping(virtual_ip) {
warn!(error = %e, virtual_ip = %virtual_ip, "Failed to remove NAT rules");
}
nat_count.store(nat_mgr.mapping_count(), Ordering::Relaxed);
// Remove proxy NDP entry
if let Err(e) = net_setup.remove_proxy_ndp(virtual_ip).await {
warn!(error = %e, virtual_ip = %virtual_ip, "Failed to remove proxy NDP");
}
}
}
}
_ = tokio::signal::ctrl_c() => {
info!("Received SIGINT, shutting down");
break;
}
_ = sigterm.recv() => {
info!("Received SIGTERM, shutting down");
break;
}
}
}
// --- Shutdown ---
info!("fips-gateway shutting down");
// Signal all tasks to stop
let _ = shutdown_tx.send(true);
// Wait for tasks (control task is cancelled by dropping the listener)
if let Some(task) = control_task {
task.abort();
let _ = task.await;
}
let _ = dns_task.await;
let _ = tick_task.await;
// Log final pool status
{
let pool_guard = ip_pool.lock().await;
let status = pool_guard.status();
info!(
total = status.total,
allocated = status.allocated,
active = status.active,
draining = status.draining,
free = status.free,
"Final pool status"
);
}
// Clean up network and NAT
net_setup.cleanup().await;
if let Err(e) = nat_mgr.cleanup() {
warn!(error = %e, "Failed to clean up nftables table");
}
info!("fips-gateway shutdown complete");
}