mirror of
https://github.com/jmcorgan/fips.git
synced 2026-10-06 03:28:24 +00:00
Adds FreeBSD as a supported platform. The daemon, fipsctl, TUN datapath and DNS integration build and run there, with a native pkg and an rc.d service. The one piece of genuinely new datapath logic is the TUN framing. FreeBSD's tun rejects every non-IPv4 packet with EAFNOSUPPORT unless TUNSIFHEAD is set, so nothing IPv6 can be sent at all; with it set, every frame carries a 4-byte network-order address-family prefix the way macOS utun does. The ioctl is issued at device creation and the prefix is stripped on read, which gives callers the same raw-IP contract as Linux and macOS. A frame carrying only the header reads as zero bytes and the reader loops treat it as nothing to do. The address family is now taken from libc rather than hardcoded, because AF_INET6 is 30 on Darwin and 28 on FreeBSD. The reader shutdown path, the writer's address-family header and the supervisor's shutdown pipe were all macOS-only and are now shared with FreeBSD, since neither platform wakes a blocked read when the interface goes down. Linux continues to rely on interface deletion. mdns-sd moves from 0.19 to 0.20 for socket-pktinfo 0.4.1, the first release that builds on FreeBSD, which uses IP_RECVDSTADDR and IP_RECVIF instead of Linux-style IP_PKTINFO. This is the only change here that affects every platform rather than just the new one. The config, ACL, hosts and keygen path constants now treat FreeBSD the same as macOS, since both install under /usr/local/etc/fips. Those constants arrived separately on maint and are merged here rather than duplicated: the predicates widen to cover FreeBSD, the platform-gated tests widen with them, and keygen keeps reading the shared SYSTEM_CONFIG_DIR constant rather than reintroducing a literal. Co-authored-by: Johnathan Corgan <johnathan@corganlabs.com>
432 lines
15 KiB
Rust
432 lines
15 KiB
Rust
//! FIPS daemon binary
|
|
//!
|
|
//! Loads configuration and creates the top-level node instance.
|
|
//! On Windows, can run as a Windows Service when invoked with `--service`.
|
|
|
|
use clap::Parser;
|
|
use fips::config::{IdentitySource, resolve_identity};
|
|
use fips::version;
|
|
use fips::{Config, Node};
|
|
use std::path::PathBuf;
|
|
use tracing::{debug, error, info};
|
|
use tracing_subscriber::{EnvFilter, fmt};
|
|
|
|
/// FIPS mesh network daemon
|
|
#[derive(Parser, Debug)]
|
|
#[command(
|
|
name = "fips",
|
|
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>,
|
|
|
|
/// Run as a Windows service (internal use by service control manager)
|
|
#[cfg(windows)]
|
|
#[arg(long, hide = true)]
|
|
service: bool,
|
|
|
|
/// Install as a Windows service
|
|
#[cfg(windows)]
|
|
#[arg(long)]
|
|
install_service: bool,
|
|
|
|
/// Uninstall the Windows service
|
|
#[cfg(windows)]
|
|
#[arg(long)]
|
|
uninstall_service: bool,
|
|
}
|
|
|
|
/// Run the FIPS daemon (shared between foreground and service modes).
|
|
///
|
|
/// `config_path` overrides the default config search. `shutdown_signal`
|
|
/// is awaited to trigger a graceful stop — in foreground mode this is
|
|
/// Ctrl+C / SIGTERM, in service mode it's the service stop event.
|
|
async fn run_daemon(
|
|
config_path: Option<PathBuf>,
|
|
shutdown_signal: impl std::future::Future<Output = ()>,
|
|
) {
|
|
// Load configuration before initializing logging so we can use
|
|
// the config's log_level as the tracing filter default.
|
|
let (config, loaded_paths) = if let Some(config_path) = &config_path {
|
|
match Config::load_file(config_path) {
|
|
Ok(config) => (config, vec![config_path.clone()]),
|
|
Err(e) => {
|
|
eprintln!(
|
|
"Failed to load configuration from {}: {}",
|
|
config_path.display(),
|
|
e
|
|
);
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
} else {
|
|
match Config::load() {
|
|
Ok(result) => result,
|
|
Err(e) => {
|
|
eprintln!("Failed to load configuration: {}", e);
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
};
|
|
|
|
// Initialize logging: RUST_LOG env var overrides config if set.
|
|
//
|
|
// The nostr-sdk relay pool emits the full JSON of every event it
|
|
// sends and receives at DEBUG level. At our DEBUG level that drowns
|
|
// out everything else, so suppress it unless the operator has
|
|
// explicitly asked for TRACE — at which point the raw frames come
|
|
// back.
|
|
let log_level = config.node.log_level();
|
|
let nostr_directive = if log_level == tracing::Level::TRACE {
|
|
"trace"
|
|
} else {
|
|
"info"
|
|
};
|
|
let default_directive = format!(
|
|
"{log_level},nostr_relay_pool={nostr_directive},nostr_sdk={nostr_directive},nostr={nostr_directive}"
|
|
);
|
|
let filter = EnvFilter::builder()
|
|
.with_default_directive(log_level.into())
|
|
.parse_lossy(default_directive);
|
|
let filter = match std::env::var("RUST_LOG") {
|
|
Ok(env) if !env.is_empty() => EnvFilter::builder()
|
|
.with_default_directive(log_level.into())
|
|
.parse_lossy(env),
|
|
_ => filter,
|
|
};
|
|
|
|
// ANSI color only when stdout is a terminal — under a supervisor
|
|
// (daemon(8), systemd) escape codes would litter the log file.
|
|
fmt()
|
|
.with_env_filter(filter)
|
|
.with_target(true)
|
|
.with_ansi(std::io::IsTerminal::is_terminal(&std::io::stdout()))
|
|
.init();
|
|
|
|
info!("FIPS {} starting", version::short_version());
|
|
|
|
if loaded_paths.is_empty() {
|
|
info!("No config files found, using defaults");
|
|
} else {
|
|
for path in &loaded_paths {
|
|
info!(path = %path.display(), "Loaded config file");
|
|
}
|
|
}
|
|
|
|
// The hosts/ACL defaults on these platforms moved from /etc/fips to
|
|
// /usr/local/etc/fips; flag files stranded at the old location.
|
|
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
|
|
fips::node::warn_on_legacy_config_paths();
|
|
|
|
// Identity provisioning: config nsec > key file > generate ephemeral
|
|
let resolved = match resolve_identity(&config, &loaded_paths) {
|
|
Ok(r) => r,
|
|
Err(e) => {
|
|
error!("Failed to resolve identity: {}", e);
|
|
std::process::exit(1);
|
|
}
|
|
};
|
|
match &resolved.source {
|
|
IdentitySource::Config => info!("Using identity from configuration"),
|
|
IdentitySource::KeyFile(p) => {
|
|
info!(path = %p.display(), "Loaded persistent identity from key file")
|
|
}
|
|
IdentitySource::Generated(p) => {
|
|
info!(path = %p.display(), "Generated persistent identity, saved to key file")
|
|
}
|
|
IdentitySource::Ephemeral => info!("Using ephemeral identity (new keypair each start)"),
|
|
}
|
|
|
|
// Create node with resolved identity
|
|
let mut config = config;
|
|
config.node.identity.nsec = Some(resolved.nsec);
|
|
debug!("Creating node");
|
|
let mut node = match Node::new(config) {
|
|
Ok(node) => node,
|
|
Err(e) => {
|
|
error!("Failed to create node: {}", e);
|
|
std::process::exit(1);
|
|
}
|
|
};
|
|
|
|
info!("Node created:");
|
|
info!(" npub: {}", node.npub());
|
|
info!(" node_addr: {}", hex::encode(node.node_addr().as_bytes()));
|
|
info!(" address: {}", node.identity().address());
|
|
info!(" state: {}", node.state());
|
|
info!(" leaf_only: {}", node.is_leaf_only());
|
|
|
|
// Start the node (initializes TUN, spawns I/O threads)
|
|
if let Err(e) = node.start().await {
|
|
error!("Failed to start node: {}", e);
|
|
std::process::exit(1);
|
|
}
|
|
|
|
info!("FIPS running");
|
|
|
|
// Serve until the shutdown signal, then drain in place before returning.
|
|
// The rx loop observes the signal directly, so its channels are never
|
|
// destructively cancelled — they live in the loop's locals across serve and
|
|
// drain, and are dropped only on clean exit (after which teardown does not
|
|
// need them). On the signal the loop broadcasts a shutdown Disconnect and
|
|
// waits (bounded by node.drain_timeout_secs) for peers to clear.
|
|
match node.run_rx_loop_with_shutdown(shutdown_signal).await {
|
|
Ok(()) => info!("RX loop exited"),
|
|
Err(e) => error!("RX loop error: {}", e),
|
|
}
|
|
|
|
info!("FIPS shutting down");
|
|
|
|
// Close the drain window (if the loop drained) and tear down. A drained
|
|
// loop tears down without re-broadcasting; a loop that exited some other
|
|
// way falls back to the immediate stop().
|
|
node.finish_shutdown().await;
|
|
|
|
info!("FIPS shutdown complete");
|
|
}
|
|
|
|
/// Build a shutdown future for foreground mode (Ctrl+C / SIGTERM).
|
|
async fn foreground_shutdown_signal() {
|
|
#[cfg(unix)]
|
|
{
|
|
use tokio::signal::unix::{SignalKind, signal};
|
|
let mut sigterm =
|
|
signal(SignalKind::terminate()).expect("failed to register SIGTERM handler");
|
|
tokio::select! {
|
|
_ = tokio::signal::ctrl_c() => {},
|
|
_ = sigterm.recv() => {},
|
|
}
|
|
}
|
|
#[cfg(not(unix))]
|
|
{
|
|
let _ = tokio::signal::ctrl_c().await;
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Unix entry point
|
|
// ============================================================================
|
|
|
|
#[cfg(not(windows))]
|
|
#[tokio::main(flavor = "current_thread")]
|
|
async fn main() {
|
|
let args = Args::parse();
|
|
run_daemon(args.config, foreground_shutdown_signal()).await;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Windows entry point and service support
|
|
// ============================================================================
|
|
|
|
#[cfg(windows)]
|
|
fn main() {
|
|
let args = Args::parse();
|
|
|
|
if args.install_service {
|
|
if let Err(e) = service::install_service() {
|
|
eprintln!("Failed to install service: {}", e);
|
|
std::process::exit(1);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if args.uninstall_service {
|
|
if let Err(e) = service::uninstall_service() {
|
|
eprintln!("Failed to uninstall service: {}", e);
|
|
std::process::exit(1);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if args.service {
|
|
// Running as a Windows service (invoked by the service control manager)
|
|
if let Err(e) = service::run_as_service() {
|
|
eprintln!("Failed to start as service: {}", e);
|
|
std::process::exit(1);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Foreground mode: build a manual tokio runtime since we can't use
|
|
// #[tokio::main] with platform-conditional main functions.
|
|
let rt = tokio::runtime::Builder::new_current_thread()
|
|
.enable_all()
|
|
.build()
|
|
.expect("Failed to create tokio runtime");
|
|
|
|
rt.block_on(run_daemon(args.config, foreground_shutdown_signal()));
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
mod service {
|
|
use std::ffi::OsString;
|
|
use std::path::PathBuf;
|
|
use std::time::Duration;
|
|
use windows_service::{
|
|
define_windows_service,
|
|
service::{
|
|
ServiceAccess, ServiceControl, ServiceControlAccept, ServiceErrorControl,
|
|
ServiceExitCode, ServiceInfo, ServiceStartType, ServiceState, ServiceStatus,
|
|
ServiceType,
|
|
},
|
|
service_control_handler::{self, ServiceControlHandlerResult},
|
|
service_dispatcher,
|
|
service_manager::{ServiceManager, ServiceManagerAccess},
|
|
};
|
|
|
|
const SERVICE_NAME: &str = "fips";
|
|
const SERVICE_DISPLAY_NAME: &str = "FIPS Mesh Network Daemon";
|
|
const SERVICE_DESCRIPTION: &str =
|
|
"Free Internetworking Peering System - distributed mesh networking protocol";
|
|
|
|
define_windows_service!(ffi_service_main, service_main);
|
|
|
|
/// Start the service dispatcher, which blocks until the service stops.
|
|
pub fn run_as_service() -> Result<(), windows_service::Error> {
|
|
service_dispatcher::start(SERVICE_NAME, ffi_service_main)
|
|
}
|
|
|
|
/// Entry point called by the Windows service control manager.
|
|
fn service_main(arguments: Vec<OsString>) {
|
|
if let Err(e) = run_service(arguments) {
|
|
eprintln!("Service error: {:?}", e);
|
|
}
|
|
}
|
|
|
|
/// Core service logic: register control handler, run daemon, report status.
|
|
fn run_service(_arguments: Vec<OsString>) -> Result<(), windows_service::Error> {
|
|
let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>();
|
|
let shutdown_tx = std::sync::Mutex::new(Some(shutdown_tx));
|
|
|
|
let event_handler = move |control_event| -> ServiceControlHandlerResult {
|
|
match control_event {
|
|
ServiceControl::Stop | ServiceControl::Shutdown => {
|
|
if let Ok(mut guard) = shutdown_tx.lock()
|
|
&& let Some(tx) = guard.take()
|
|
{
|
|
let _ = tx.send(());
|
|
}
|
|
ServiceControlHandlerResult::NoError
|
|
}
|
|
ServiceControl::Interrogate => ServiceControlHandlerResult::NoError,
|
|
_ => ServiceControlHandlerResult::NotImplemented,
|
|
}
|
|
};
|
|
|
|
let status_handle = service_control_handler::register(SERVICE_NAME, event_handler)?;
|
|
|
|
// Report running
|
|
status_handle.set_service_status(ServiceStatus {
|
|
service_type: ServiceType::OWN_PROCESS,
|
|
current_state: ServiceState::Running,
|
|
controls_accepted: ServiceControlAccept::STOP | ServiceControlAccept::SHUTDOWN,
|
|
exit_code: ServiceExitCode::Win32(0),
|
|
checkpoint: 0,
|
|
wait_hint: Duration::default(),
|
|
process_id: None,
|
|
})?;
|
|
|
|
let rt = tokio::runtime::Builder::new_current_thread()
|
|
.enable_all()
|
|
.build()
|
|
.expect("Failed to create tokio runtime");
|
|
|
|
// Look for config file path from FIPS_CONFIG env var
|
|
let config_path: Option<PathBuf> = std::env::var("FIPS_CONFIG").ok().map(PathBuf::from);
|
|
|
|
rt.block_on(super::run_daemon(config_path, async {
|
|
let _ = shutdown_rx.await;
|
|
}));
|
|
|
|
// Report stopped
|
|
status_handle.set_service_status(ServiceStatus {
|
|
service_type: ServiceType::OWN_PROCESS,
|
|
current_state: ServiceState::Stopped,
|
|
controls_accepted: ServiceControlAccept::empty(),
|
|
exit_code: ServiceExitCode::Win32(0),
|
|
checkpoint: 0,
|
|
wait_hint: Duration::default(),
|
|
process_id: None,
|
|
})?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Install FIPS as a Windows service (requires Administrator).
|
|
pub fn install_service() -> Result<(), Box<dyn std::error::Error>> {
|
|
let manager = ServiceManager::local_computer(
|
|
None::<&str>,
|
|
ServiceManagerAccess::CREATE_SERVICE | ServiceManagerAccess::CONNECT,
|
|
)?;
|
|
|
|
let exe_path = std::env::current_exe()?;
|
|
let service_info = ServiceInfo {
|
|
name: OsString::from(SERVICE_NAME),
|
|
display_name: OsString::from(SERVICE_DISPLAY_NAME),
|
|
service_type: ServiceType::OWN_PROCESS,
|
|
start_type: ServiceStartType::AutoStart,
|
|
error_control: ServiceErrorControl::Normal,
|
|
executable_path: exe_path,
|
|
launch_arguments: vec![OsString::from("--service")],
|
|
dependencies: vec![],
|
|
account_name: None,
|
|
account_password: None,
|
|
};
|
|
|
|
let service = match manager.create_service(&service_info, ServiceAccess::CHANGE_CONFIG) {
|
|
Ok(s) => s,
|
|
Err(windows_service::Error::Winapi(ref e)) if e.raw_os_error() == Some(0x431) => {
|
|
// ERROR_SERVICE_EXISTS (1073) — open the existing service instead
|
|
println!(
|
|
"Service '{}' already exists, updating configuration...",
|
|
SERVICE_NAME
|
|
);
|
|
manager.open_service(SERVICE_NAME, ServiceAccess::CHANGE_CONFIG)?
|
|
}
|
|
Err(e) => return Err(format!("Failed to create service: {}", e).into()),
|
|
};
|
|
|
|
// set_description is non-critical — don't fail the install over it
|
|
if let Err(e) = service.set_description(SERVICE_DESCRIPTION) {
|
|
eprintln!("Warning: could not set service description: {}", e);
|
|
}
|
|
|
|
println!("Service '{}' installed successfully.", SERVICE_NAME);
|
|
println!("Start it with: sc start {}", SERVICE_NAME);
|
|
println!();
|
|
println!("Configuration: place fips.yaml in one of:");
|
|
println!(" - Current directory");
|
|
println!(" - %APPDATA%\\fips\\fips.yaml");
|
|
println!(" - Set FIPS_CONFIG environment variable");
|
|
Ok(())
|
|
}
|
|
|
|
/// Uninstall the FIPS Windows service (requires Administrator).
|
|
pub fn uninstall_service() -> Result<(), Box<dyn std::error::Error>> {
|
|
let manager = ServiceManager::local_computer(None::<&str>, ServiceManagerAccess::CONNECT)?;
|
|
|
|
let service = manager.open_service(
|
|
SERVICE_NAME,
|
|
ServiceAccess::STOP | ServiceAccess::DELETE | ServiceAccess::QUERY_STATUS,
|
|
)?;
|
|
|
|
// Stop the service if running
|
|
if let Ok(status) = service.query_status()
|
|
&& status.current_state != ServiceState::Stopped
|
|
{
|
|
println!("Stopping service...");
|
|
let _ = service.stop();
|
|
// Wait briefly for the service to stop
|
|
std::thread::sleep(Duration::from_secs(2));
|
|
}
|
|
|
|
service.delete()?;
|
|
println!("Service '{}' uninstalled.", SERVICE_NAME);
|
|
Ok(())
|
|
}
|
|
}
|