Apply rustfmt to master-only code

This commit is contained in:
Johnathan Corgan
2026-04-10 08:47:02 +00:00
parent 6196307f0e
commit e9da598f8a
33 changed files with 926 additions and 557 deletions

View File

@@ -4,18 +4,18 @@
//! DNS-allocated virtual IPs and kernel nftables NAT.
use clap::Parser;
use fips::Config;
#[cfg(target_os = "linux")]
use fips::gateway::{control, dns, nat, net, pool};
use fips::version;
use fips::Config;
use std::path::PathBuf;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Instant;
use tokio::signal::unix::{signal, SignalKind};
use tokio::sync::{mpsc, watch, Mutex};
use tokio::signal::unix::{SignalKind, signal};
use tokio::sync::{Mutex, mpsc, watch};
use tracing::{error, info, warn};
use tracing_subscriber::{fmt, EnvFilter};
use tracing_subscriber::{EnvFilter, fmt};
/// FIPS outbound LAN gateway
#[derive(Parser, Debug)]
@@ -60,7 +60,11 @@ async fn main() {
config
}
Err(e) => {
error!("Failed to load config from {}: {}", config_path.display(), e);
error!(
"Failed to load config from {}: {}",
config_path.display(),
e
);
std::process::exit(1);
}
}
@@ -151,12 +155,16 @@ async fn main() {
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)
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 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) => {
@@ -174,11 +182,8 @@ async fn main() {
}
let mut buf = [0u8; 512];
match tokio::time::timeout(
std::time::Duration::from_secs(3),
sock.recv_from(&mut buf),
)
.await
match tokio::time::timeout(std::time::Duration::from_secs(3), sock.recv_from(&mut buf))
.await
{
Ok(Ok(_)) => {
info!(upstream = %upstream, "DNS upstream is reachable");
@@ -225,10 +230,7 @@ async fn main() {
};
// Network setup
let mut net_setup = net::NetSetup::new(
gw_config.lan_interface.clone(),
gw_config.pool.clone(),
);
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 {
@@ -272,8 +274,7 @@ async fn main() {
// --- Snapshot channel for control socket ---
let (snapshot_tx, snapshot_rx) =
watch::channel::<Option<control::GatewaySnapshot>>(None);
let (snapshot_tx, snapshot_rx) = watch::channel::<Option<control::GatewaySnapshot>>(None);
let start_time = Instant::now();
// --- Start control socket ---

View File

@@ -3,12 +3,12 @@
//! Loads configuration and creates the top-level node instance.
use clap::Parser;
use fips::config::{resolve_identity, IdentitySource};
use fips::config::{IdentitySource, resolve_identity};
use fips::version;
use fips::{Config, Node};
use std::path::PathBuf;
use tracing::{debug, error, info, warn};
use tracing_subscriber::{fmt, EnvFilter};
use tracing_subscriber::{EnvFilter, fmt};
/// FIPS mesh network daemon
#[derive(Parser, Debug)]
@@ -34,7 +34,11 @@ async fn main() {
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);
eprintln!(
"Failed to load configuration from {}: {}",
config_path.display(),
e
);
std::process::exit(1);
}
}
@@ -54,10 +58,7 @@ async fn main() {
.with_default_directive(log_level.into())
.from_env_lossy();
fmt()
.with_env_filter(filter)
.with_target(true)
.init();
fmt().with_env_filter(filter).with_target(true).init();
info!("FIPS {} starting", version::short_version());
@@ -79,8 +80,12 @@ async fn main() {
};
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::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)"),
}
@@ -116,8 +121,9 @@ async fn main() {
// stop() drops the packet channel, causing run_rx_loop to exit.
#[cfg(unix)]
let shutdown = async {
use tokio::signal::unix::{signal, SignalKind};
let mut sigterm = signal(SignalKind::terminate()).expect("failed to register SIGTERM handler");
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() => {},

View File

@@ -193,7 +193,10 @@ impl App {
return;
}
let state = self.table_states.entry(self.active_tab).or_default();
let i = state.selected().map(|s| (s + 1).min(count - 1)).unwrap_or(0);
let i = state
.selected()
.map(|s| (s + 1).min(count - 1))
.unwrap_or(0);
state.select(Some(i));
}

View File

@@ -154,7 +154,9 @@ fn main() {
let cli = Cli::parse();
let socket_path = cli.socket.unwrap_or_else(default_socket_path);
let gateway_socket_path = cli.gateway_socket.unwrap_or_else(default_gateway_socket_path);
let gateway_socket_path = cli
.gateway_socket
.unwrap_or_else(default_gateway_socket_path);
let refresh = Duration::from_secs(cli.refresh);
// Install panic hook that restores terminal before printing panic

View File

@@ -34,8 +34,8 @@ fn draw_summary(frame: &mut Frame, app: &App, area: Rect) {
let data = match app.data.get(&Tab::Gateway) {
Some(d) => d,
None => {
let msg = Paragraph::new(" Waiting for data...")
.style(Style::default().fg(Color::DarkGray));
let msg =
Paragraph::new(" Waiting for data...").style(Style::default().fg(Color::DarkGray));
frame.render_widget(msg, area);
return;
}
@@ -49,25 +49,41 @@ fn draw_summary(frame: &mut Frame, app: &App, area: Rect) {
.split(area);
// Pool and info section
let block = Block::default()
.borders(Borders::ALL)
.title(" Gateway ");
let block = Block::default().borders(Borders::ALL).title(" Gateway ");
let inner = block.inner(chunks[0]);
frame.render_widget(block, chunks[0]);
let total = data.get("pool_total").and_then(|v| v.as_u64()).unwrap_or(0);
let allocated = data.get("pool_allocated").and_then(|v| v.as_u64()).unwrap_or(0);
let active = data.get("pool_active").and_then(|v| v.as_u64()).unwrap_or(0);
let draining = data.get("pool_draining").and_then(|v| v.as_u64()).unwrap_or(0);
let allocated = data
.get("pool_allocated")
.and_then(|v| v.as_u64())
.unwrap_or(0);
let active = data
.get("pool_active")
.and_then(|v| v.as_u64())
.unwrap_or(0);
let draining = data
.get("pool_draining")
.and_then(|v| v.as_u64())
.unwrap_or(0);
let free = data.get("pool_free").and_then(|v| v.as_u64()).unwrap_or(0);
let nat = data.get("nat_mappings").and_then(|v| v.as_u64()).unwrap_or(0);
let nat = data
.get("nat_mappings")
.and_then(|v| v.as_u64())
.unwrap_or(0);
let dns = helpers::str_field(data, "dns_listen");
let uptime_secs = data.get("uptime_secs").and_then(|v| v.as_u64()).unwrap_or(0);
let uptime_secs = data
.get("uptime_secs")
.and_then(|v| v.as_u64())
.unwrap_or(0);
let pool_cidr = helpers::str_field(data, "pool_cidr");
let lan_iface = helpers::str_field(data, "lan_interface");
let dns_upstream = helpers::str_field(data, "dns_upstream");
let dns_ttl = data.get("dns_ttl").and_then(|v| v.as_u64()).unwrap_or(0);
let grace = data.get("pool_grace_period").and_then(|v| v.as_u64()).unwrap_or(0);
let grace = data
.get("pool_grace_period")
.and_then(|v| v.as_u64())
.unwrap_or(0);
let label = Style::default().fg(Color::DarkGray);
let count = Style::default()
@@ -201,9 +217,9 @@ fn draw_mappings(frame: &mut Frame, app: &mut App, area: Rect) {
.collect();
let widths = [
Constraint::Min(18), // Virtual IP
Constraint::Min(30), // DNS Name
Constraint::Min(26), // Mesh Addr
Constraint::Min(18), // Virtual IP
Constraint::Min(30), // DNS Name
Constraint::Min(26), // Mesh Addr
Constraint::Length(10), // State
Constraint::Length(10), // Sessions
Constraint::Length(10), // Age

View File

@@ -30,15 +30,18 @@ use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use thiserror::Error;
#[cfg(feature = "gateway")]
pub use gateway::{ConntrackConfig, GatewayConfig, GatewayDnsConfig};
pub use node::{
BloomConfig, BuffersConfig, CacheConfig, ControlConfig, DiscoveryConfig, LimitsConfig,
NodeConfig, RateLimitConfig, RekeyConfig, RetryConfig, SessionConfig, SessionMmpConfig,
TreeConfig,
};
#[cfg(feature = "gateway")]
pub use gateway::{ConntrackConfig, GatewayConfig, GatewayDnsConfig};
pub use peer::{ConnectPolicy, PeerAddress, PeerConfig};
pub use transport::{BleConfig, DirectoryServiceConfig, EthernetConfig, TcpConfig, TorConfig, TransportInstances, TransportsConfig, UdpConfig};
pub use transport::{
BleConfig, DirectoryServiceConfig, EthernetConfig, TcpConfig, TorConfig, TransportInstances,
TransportsConfig, UdpConfig,
};
/// Default config filename.
const CONFIG_FILENAME: &str = "fips.yaml";
@@ -553,10 +556,7 @@ node:
override_config.node.identity.nsec = Some("override_nsec".to_string());
base.merge(override_config);
assert_eq!(
base.node.identity.nsec,
Some("override_nsec".to_string())
);
assert_eq!(base.node.identity.nsec, Some("override_nsec".to_string()));
}
#[test]
@@ -573,9 +573,8 @@ node:
#[test]
fn test_create_identity_from_nsec() {
let mut config = Config::new();
config.node.identity.nsec = Some(
"0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20".to_string(),
);
config.node.identity.nsec =
Some("0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20".to_string());
let identity = config.create_identity().unwrap();
assert!(!identity.npub().is_empty());
@@ -674,9 +673,11 @@ node:
assert!(paths.iter().any(|p| p.ends_with("fips.yaml")));
// Should include /etc/fips
assert!(paths
.iter()
.any(|p| p.starts_with("/etc/fips") && p.ends_with("fips.yaml")));
assert!(
paths
.iter()
.any(|p| p.starts_with("/etc/fips") && p.ends_with("fips.yaml"))
);
}
#[test]
@@ -761,16 +762,21 @@ node:
#[test]
fn test_key_file_path_derivation() {
let config_path = PathBuf::from("/etc/fips/fips.yaml");
assert_eq!(key_file_path(&config_path), PathBuf::from("/etc/fips/fips.key"));
assert_eq!(pub_file_path(&config_path), PathBuf::from("/etc/fips/fips.pub"));
assert_eq!(
key_file_path(&config_path),
PathBuf::from("/etc/fips/fips.key")
);
assert_eq!(
pub_file_path(&config_path),
PathBuf::from("/etc/fips/fips.pub")
);
}
#[test]
fn test_resolve_identity_from_config() {
let mut config = Config::new();
config.node.identity.nsec = Some(
"0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20".to_string(),
);
config.node.identity.nsec =
Some("0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20".to_string());
let resolved = resolve_identity(&config, &[]).unwrap();
assert!(matches!(resolved.source, IdentitySource::Config));
@@ -817,11 +823,7 @@ node:
let config_path = temp_dir.path().join("fips.yaml");
let key_path = temp_dir.path().join("fips.key");
fs::write(
&config_path,
"node:\n identity:\n persistent: true\n",
)
.unwrap();
fs::write(&config_path, "node:\n identity:\n persistent: true\n").unwrap();
// Write a key file
let identity = crate::Identity::generate();
@@ -841,11 +843,7 @@ node:
let temp_dir = TempDir::new().unwrap();
let config_path = temp_dir.path().join("fips.yaml");
fs::write(
&config_path,
"node:\n identity:\n persistent: true\n",
)
.unwrap();
fs::write(&config_path, "node:\n identity:\n persistent: true\n").unwrap();
let config = Config::load_file(&config_path).unwrap();
let resolved = resolve_identity(&config, std::slice::from_ref(&config_path)).unwrap();
@@ -906,8 +904,7 @@ transports:
assert_eq!(config.transports.udp.len(), 2);
let instances: std::collections::HashMap<_, _> =
config.transports.udp.iter().collect();
let instances: std::collections::HashMap<_, _> = config.transports.udp.iter().collect();
// Named instances have Some(name)
assert!(instances.contains_key(&Some("main")));

View File

@@ -7,7 +7,7 @@
use serde::{Deserialize, Serialize};
use super::IdentityConfig;
use crate::mmp::{MmpConfig, MmpMode, DEFAULT_LOG_INTERVAL_SECS, DEFAULT_OWD_WINDOW_SIZE};
use crate::mmp::{DEFAULT_LOG_INTERVAL_SECS, DEFAULT_OWD_WINDOW_SIZE, MmpConfig, MmpMode};
// ============================================================================
// Node Configuration Subsections
@@ -42,10 +42,18 @@ impl Default for LimitsConfig {
}
impl LimitsConfig {
fn default_max_connections() -> usize { 256 }
fn default_max_peers() -> usize { 128 }
fn default_max_links() -> usize { 256 }
fn default_max_pending_inbound() -> usize { 1000 }
fn default_max_connections() -> usize {
256
}
fn default_max_peers() -> usize {
128
}
fn default_max_links() -> usize {
256
}
fn default_max_pending_inbound() -> usize {
1000
}
}
/// Rate limiting (`node.rate_limit.*`).
@@ -86,12 +94,24 @@ impl Default for RateLimitConfig {
}
impl RateLimitConfig {
fn default_handshake_burst() -> u32 { 100 }
fn default_handshake_rate() -> f64 { 10.0 }
fn default_handshake_timeout_secs() -> u64 { 30 }
fn default_handshake_resend_interval_ms() -> u64 { 1000 }
fn default_handshake_resend_backoff() -> f64 { 2.0 }
fn default_handshake_max_resends() -> u32 { 5 }
fn default_handshake_burst() -> u32 {
100
}
fn default_handshake_rate() -> f64 {
10.0
}
fn default_handshake_timeout_secs() -> u64 {
30
}
fn default_handshake_resend_interval_ms() -> u64 {
1000
}
fn default_handshake_resend_backoff() -> f64 {
2.0
}
fn default_handshake_max_resends() -> u32 {
5
}
}
/// Retry/backoff configuration (`node.retry.*`).
@@ -119,9 +139,15 @@ impl Default for RetryConfig {
}
impl RetryConfig {
fn default_max_retries() -> u32 { 5 }
fn default_base_interval_secs() -> u64 { 5 }
fn default_max_backoff_secs() -> u64 { 300 }
fn default_max_retries() -> u32 {
5
}
fn default_base_interval_secs() -> u64 {
5
}
fn default_max_backoff_secs() -> u64 {
300
}
}
/// Cache parameters (`node.cache.*`).
@@ -149,9 +175,15 @@ impl Default for CacheConfig {
}
impl CacheConfig {
fn default_coord_size() -> usize { 50_000 }
fn default_coord_ttl_secs() -> u64 { 300 }
fn default_identity_size() -> usize { 10_000 }
fn default_coord_size() -> usize {
50_000
}
fn default_coord_ttl_secs() -> u64 {
300
}
fn default_identity_size() -> usize {
10_000
}
}
/// Discovery protocol (`node.discovery.*`).
@@ -205,14 +237,30 @@ impl Default for DiscoveryConfig {
}
impl DiscoveryConfig {
fn default_ttl() -> u8 { 64 }
fn default_timeout_secs() -> u64 { 10 }
fn default_recent_expiry_secs() -> u64 { 10 }
fn default_backoff_base_secs() -> u64 { 30 }
fn default_backoff_max_secs() -> u64 { 300 }
fn default_forward_min_interval_secs() -> u64 { 2 }
fn default_retry_interval_secs() -> u64 { 5 }
fn default_max_attempts() -> u8 { 2 }
fn default_ttl() -> u8 {
64
}
fn default_timeout_secs() -> u64 {
10
}
fn default_recent_expiry_secs() -> u64 {
10
}
fn default_backoff_base_secs() -> u64 {
30
}
fn default_backoff_max_secs() -> u64 {
300
}
fn default_forward_min_interval_secs() -> u64 {
2
}
fn default_retry_interval_secs() -> u64 {
5
}
fn default_max_attempts() -> u8 {
2
}
}
/// Spanning tree (`node.tree.*`).
@@ -267,13 +315,27 @@ impl Default for TreeConfig {
}
impl TreeConfig {
fn default_announce_min_interval_ms() -> u64 { 500 }
fn default_parent_hysteresis() -> f64 { 0.2 }
fn default_hold_down_secs() -> u64 { 30 }
fn default_reeval_interval_secs() -> u64 { 60 }
fn default_flap_threshold() -> u32 { 4 }
fn default_flap_window_secs() -> u64 { 60 }
fn default_flap_dampening_secs() -> u64 { 120 }
fn default_announce_min_interval_ms() -> u64 {
500
}
fn default_parent_hysteresis() -> f64 {
0.2
}
fn default_hold_down_secs() -> u64 {
30
}
fn default_reeval_interval_secs() -> u64 {
60
}
fn default_flap_threshold() -> u32 {
4
}
fn default_flap_window_secs() -> u64 {
60
}
fn default_flap_dampening_secs() -> u64 {
120
}
}
/// Bloom filter (`node.bloom.*`).
@@ -286,12 +348,16 @@ pub struct BloomConfig {
impl Default for BloomConfig {
fn default() -> Self {
Self { update_debounce_ms: 500 }
Self {
update_debounce_ms: 500,
}
}
}
impl BloomConfig {
fn default_update_debounce_ms() -> u64 { 500 }
fn default_update_debounce_ms() -> u64 {
500
}
}
/// Session/data plane (`node.session.*`).
@@ -337,12 +403,24 @@ impl Default for SessionConfig {
}
impl SessionConfig {
fn default_ttl() -> u8 { 64 }
fn default_pending_packets_per_dest() -> usize { 16 }
fn default_pending_max_destinations() -> usize { 256 }
fn default_idle_timeout_secs() -> u64 { 90 }
fn default_coords_warmup_packets() -> u8 { 5 }
fn default_coords_response_interval_ms() -> u64 { 2000 }
fn default_ttl() -> u8 {
64
}
fn default_pending_packets_per_dest() -> usize {
16
}
fn default_pending_max_destinations() -> usize {
256
}
fn default_idle_timeout_secs() -> u64 {
90
}
fn default_coords_warmup_packets() -> u8 {
5
}
fn default_coords_response_interval_ms() -> u64 {
2000
}
}
/// Session-layer Metrics Measurement Protocol (`node.session_mmp.*`).
@@ -377,8 +455,12 @@ impl Default for SessionMmpConfig {
}
impl SessionMmpConfig {
fn default_log_interval_secs() -> u64 { DEFAULT_LOG_INTERVAL_SECS }
fn default_owd_window_size() -> usize { DEFAULT_OWD_WINDOW_SIZE }
fn default_log_interval_secs() -> u64 {
DEFAULT_LOG_INTERVAL_SECS
}
fn default_owd_window_size() -> usize {
DEFAULT_OWD_WINDOW_SIZE
}
}
/// Control socket configuration (`node.control.*`).
@@ -402,7 +484,9 @@ impl Default for ControlConfig {
}
impl ControlConfig {
fn default_enabled() -> bool { true }
fn default_enabled() -> bool {
true
}
fn default_socket_path() -> String {
if let Ok(runtime_dir) = std::env::var("XDG_RUNTIME_DIR") {
@@ -440,9 +524,15 @@ impl Default for BuffersConfig {
}
impl BuffersConfig {
fn default_packet_channel() -> usize { 1024 }
fn default_tun_channel() -> usize { 1024 }
fn default_dns_channel() -> usize { 64 }
fn default_packet_channel() -> usize {
1024
}
fn default_tun_channel() -> usize {
1024
}
fn default_dns_channel() -> usize {
64
}
}
// ============================================================================
@@ -480,9 +570,15 @@ impl Default for RekeyConfig {
}
impl RekeyConfig {
fn default_enabled() -> bool { true }
fn default_after_secs() -> u64 { 120 }
fn default_after_messages() -> u64 { 1 << 16 }
fn default_enabled() -> bool {
true
}
fn default_after_secs() -> u64 {
120
}
fn default_after_messages() -> u64 {
1 << 16
}
}
/// ECN congestion signaling configuration (`node.ecn.*`).
@@ -520,9 +616,15 @@ impl Default for EcnConfig {
}
impl EcnConfig {
fn default_enabled() -> bool { true }
fn default_loss_threshold() -> f64 { 0.05 }
fn default_etx_threshold() -> f64 { 3.0 }
fn default_enabled() -> bool {
true
}
fn default_loss_threshold() -> f64 {
0.05
}
fn default_etx_threshold() -> f64 {
3.0
}
}
// ============================================================================
@@ -650,7 +752,12 @@ impl Default for NodeConfig {
impl NodeConfig {
/// Get the log level as a tracing Level. Default: INFO.
pub fn log_level(&self) -> tracing::Level {
match self.log_level.as_deref().map(|s| s.to_lowercase()).as_deref() {
match self
.log_level
.as_deref()
.map(|s| s.to_lowercase())
.as_deref()
{
Some("trace") => tracing::Level::TRACE,
Some("debug") => tracing::Level::DEBUG,
Some("warn") | Some("warning") => tracing::Level::WARN,
@@ -659,10 +766,18 @@ impl NodeConfig {
}
}
fn default_tick_interval_secs() -> u64 { 1 }
fn default_base_rtt_ms() -> u64 { 100 }
fn default_heartbeat_interval_secs() -> u64 { 10 }
fn default_link_dead_timeout_secs() -> u64 { 30 }
fn default_tick_interval_secs() -> u64 {
1
}
fn default_base_rtt_ms() -> u64 {
100
}
fn default_heartbeat_interval_secs() -> u64 {
10
}
fn default_link_dead_timeout_secs() -> u64 {
30
}
}
#[cfg(test)]

View File

@@ -112,15 +112,12 @@ impl<T> TransportInstances<T> {
/// Named instances have `Some(name)`.
pub fn iter(&self) -> impl Iterator<Item = (Option<&str>, &T)> {
match self {
TransportInstances::Single(config) => {
vec![(None, config)].into_iter()
}
TransportInstances::Named(map) => {
map.iter()
.map(|(k, v)| (Some(k.as_str()), v))
.collect::<Vec<_>>()
.into_iter()
}
TransportInstances::Single(config) => vec![(None, config)].into_iter(),
TransportInstances::Named(map) => map
.iter()
.map(|(k, v)| (Some(k.as_str()), v))
.collect::<Vec<_>>()
.into_iter(),
}
}
}
@@ -306,7 +303,8 @@ impl TcpConfig {
/// Get the connect timeout in milliseconds.
pub fn connect_timeout_ms(&self) -> u64 {
self.connect_timeout_ms.unwrap_or(DEFAULT_TCP_CONNECT_TIMEOUT_MS)
self.connect_timeout_ms
.unwrap_or(DEFAULT_TCP_CONNECT_TIMEOUT_MS)
}
/// Whether TCP_NODELAY is enabled. Default: true.
@@ -331,7 +329,8 @@ impl TcpConfig {
/// Get the maximum number of inbound connections. Default: 256.
pub fn max_inbound_connections(&self) -> usize {
self.max_inbound_connections.unwrap_or(DEFAULT_TCP_MAX_INBOUND)
self.max_inbound_connections
.unwrap_or(DEFAULT_TCP_MAX_INBOUND)
}
}
@@ -447,12 +446,16 @@ pub struct DirectoryServiceConfig {
impl DirectoryServiceConfig {
/// Path to the hostname file. Default: "/var/lib/tor/fips_onion_service/hostname".
pub fn hostname_file(&self) -> &str {
self.hostname_file.as_deref().unwrap_or(DEFAULT_HOSTNAME_FILE)
self.hostname_file
.as_deref()
.unwrap_or(DEFAULT_HOSTNAME_FILE)
}
/// Local bind address for the listener. Default: "127.0.0.1:8443".
pub fn bind_addr(&self) -> &str {
self.bind_addr.as_deref().unwrap_or(DEFAULT_DIRECTORY_BIND_ADDR)
self.bind_addr
.as_deref()
.unwrap_or(DEFAULT_DIRECTORY_BIND_ADDR)
}
}
@@ -464,12 +467,16 @@ impl TorConfig {
/// Get the SOCKS5 proxy address. Default: "127.0.0.1:9050".
pub fn socks5_addr(&self) -> &str {
self.socks5_addr.as_deref().unwrap_or(DEFAULT_TOR_SOCKS5_ADDR)
self.socks5_addr
.as_deref()
.unwrap_or(DEFAULT_TOR_SOCKS5_ADDR)
}
/// Get the control port address. Default: "/run/tor/control".
pub fn control_addr(&self) -> &str {
self.control_addr.as_deref().unwrap_or(DEFAULT_TOR_CONTROL_ADDR)
self.control_addr
.as_deref()
.unwrap_or(DEFAULT_TOR_CONTROL_ADDR)
}
/// Get the control auth string. Default: "cookie".
@@ -479,12 +486,15 @@ impl TorConfig {
/// Get the cookie file path. Default: "/var/run/tor/control.authcookie".
pub fn cookie_path(&self) -> &str {
self.cookie_path.as_deref().unwrap_or(DEFAULT_TOR_COOKIE_PATH)
self.cookie_path
.as_deref()
.unwrap_or(DEFAULT_TOR_COOKIE_PATH)
}
/// Get the connect timeout in milliseconds. Default: 120000.
pub fn connect_timeout_ms(&self) -> u64 {
self.connect_timeout_ms.unwrap_or(DEFAULT_TOR_CONNECT_TIMEOUT_MS)
self.connect_timeout_ms
.unwrap_or(DEFAULT_TOR_CONNECT_TIMEOUT_MS)
}
/// Get the default MTU. Default: 1400.
@@ -494,7 +504,8 @@ impl TorConfig {
/// Get the max inbound connections. Default: 64.
pub fn max_inbound_connections(&self) -> usize {
self.max_inbound_connections.unwrap_or(DEFAULT_TOR_MAX_INBOUND)
self.max_inbound_connections
.unwrap_or(DEFAULT_TOR_MAX_INBOUND)
}
}

View File

@@ -107,7 +107,10 @@ impl GatewayControlSocket {
let group_name = CString::new("fips").unwrap();
let grp = unsafe { libc::getgrnam(group_name.as_ptr()) };
if grp.is_null() {
debug!("'fips' group not found, skipping chown for {}", path.display());
debug!(
"'fips' group not found, skipping chown for {}",
path.display()
);
return;
}
let gid = unsafe { (*grp).gr_gid };

View File

@@ -7,7 +7,7 @@
//! The daemon resolver populates its identity cache as a side effect
//! of resolution, which is required for fips0 routing to work.
use simple_dns::{Packet, PacketFlag, RCODE, ResourceRecord, CLASS, rdata};
use simple_dns::{CLASS, Packet, PacketFlag, RCODE, ResourceRecord, rdata};
use simple_dns::{QTYPE, TYPE};
use std::net::{Ipv6Addr, SocketAddr};
@@ -241,28 +241,27 @@ async fn handle_query(
}
};
if let Err(e) = upstream_socket.send_to(&upstream_query_bytes, upstream).await {
if let Err(e) = upstream_socket
.send_to(&upstream_query_bytes, upstream)
.await
{
warn!(error = %e, "Failed to forward query to daemon");
return build_servfail(&query);
}
let mut resp_buf = vec![0u8; MAX_DNS_SIZE];
let resp_len = match tokio::time::timeout(
UPSTREAM_TIMEOUT,
upstream_socket.recv(&mut resp_buf),
)
.await
{
Ok(Ok(len)) => len,
Ok(Err(e)) => {
warn!(error = %e, "Upstream recv error");
return build_servfail(&query);
}
Err(_) => {
warn!("Upstream DNS timeout");
return build_servfail(&query);
}
};
let resp_len =
match tokio::time::timeout(UPSTREAM_TIMEOUT, upstream_socket.recv(&mut resp_buf)).await {
Ok(Ok(len)) => len,
Ok(Err(e)) => {
warn!(error = %e, "Upstream recv error");
return build_servfail(&query);
}
Err(_) => {
warn!("Upstream DNS timeout");
return build_servfail(&query);
}
};
let upstream_response = match Packet::parse(&resp_buf[..resp_len]) {
Ok(p) => p,

View File

@@ -11,9 +11,7 @@ use rustables::expr::{
Cmp, CmpOp, HighLevelPayload, IPv6HeaderField, Immediate, Masquerade, Meta, MetaType, Nat,
NatType, NetworkHeaderField, Register,
};
use rustables::{
Batch, Chain, ChainType, Hook, HookClass, MsgType, ProtocolFamily, Rule, Table,
};
use rustables::{Batch, Chain, ChainType, Hook, HookClass, MsgType, ProtocolFamily, Rule, Table};
const TABLE_NAME: &str = "fips_gateway";
const PREROUTING_CHAIN: &str = "prerouting";
@@ -100,10 +98,13 @@ impl NatManager {
virtual_ip: Ipv6Addr,
mesh_addr: Ipv6Addr,
) -> Result<(), NatError> {
self.mappings.insert(virtual_ip, NatMapping {
self.mappings.insert(
virtual_ip,
mesh_addr,
});
NatMapping {
virtual_ip,
mesh_addr,
},
);
self.rebuild()?;
debug!(
@@ -129,7 +130,9 @@ impl NatManager {
pub fn cleanup(self) -> Result<(), NatError> {
let mut batch = Batch::new();
batch.add(&self.table, MsgType::Del);
batch.send().map_err(|e| NatError::Nftables(e.to_string()))?;
batch
.send()
.map_err(|e| NatError::Nftables(e.to_string()))?;
info!("Deleted nftables table '{TABLE_NAME}'");
Ok(())
@@ -171,10 +174,8 @@ impl NatManager {
.with_expr(Meta::new(MetaType::NfProto))
.with_expr(Cmp::new(CmpOp::Eq, [libc::NFPROTO_IPV6 as u8]))
.with_expr(
HighLevelPayload::Network(NetworkHeaderField::IPv6(
IPv6HeaderField::Daddr,
))
.build(),
HighLevelPayload::Network(NetworkHeaderField::IPv6(IPv6HeaderField::Daddr))
.build(),
)
.with_expr(Cmp::new(CmpOp::Eq, mapping.virtual_ip.octets()))
.with_expr(Immediate::new_data(
@@ -193,10 +194,8 @@ impl NatManager {
.with_expr(Meta::new(MetaType::NfProto))
.with_expr(Cmp::new(CmpOp::Eq, [libc::NFPROTO_IPV6 as u8]))
.with_expr(
HighLevelPayload::Network(NetworkHeaderField::IPv6(
IPv6HeaderField::Saddr,
))
.build(),
HighLevelPayload::Network(NetworkHeaderField::IPv6(IPv6HeaderField::Saddr))
.build(),
)
.with_expr(Cmp::new(CmpOp::Eq, mapping.mesh_addr.octets()))
.with_expr(Immediate::new_data(
@@ -212,7 +211,9 @@ impl NatManager {
batch.add(&snat_rule, MsgType::Add);
}
batch.send().map_err(|e| NatError::Nftables(e.to_string()))?;
batch
.send()
.map_err(|e| NatError::Nftables(e.to_string()))?;
Ok(())
}
}

View File

@@ -30,9 +30,7 @@ pub fn check_ipv6_forwarding() {
}
/// Check that a network interface exists using rtnetlink.
pub async fn check_interface_exists(
name: &str,
) -> Result<u32, std::io::Error> {
pub async fn check_interface_exists(name: &str) -> Result<u32, std::io::Error> {
let index = rustables::iface_index(name)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::NotFound, e.to_string()))?;
debug!(interface = %name, index, "Interface found");
@@ -80,9 +78,9 @@ impl NetSetup {
debug!(cidr = %self.pool_cidr, "Pool route already exists");
return Ok(());
}
return Err(std::io::Error::other(
format!("Failed to add pool route: {stderr}"),
));
return Err(std::io::Error::other(format!(
"Failed to add pool route: {stderr}"
)));
}
self.route_added = true;
@@ -94,7 +92,15 @@ impl NetSetup {
pub async fn add_proxy_ndp(&mut self, addr: Ipv6Addr) -> Result<(), std::io::Error> {
let addr_str = addr.to_string();
let output = tokio::process::Command::new("ip")
.args(["-6", "neigh", "add", "proxy", &addr_str, "dev", &self.lan_interface])
.args([
"-6",
"neigh",
"add",
"proxy",
&addr_str,
"dev",
&self.lan_interface,
])
.output()
.await?;
@@ -104,9 +110,9 @@ impl NetSetup {
debug!(addr = %addr, "Proxy NDP entry already exists");
return Ok(());
}
return Err(std::io::Error::other(
format!("Failed to add proxy NDP: {stderr}"),
));
return Err(std::io::Error::other(format!(
"Failed to add proxy NDP: {stderr}"
)));
}
self.proxy_entries.push(addr);
@@ -118,7 +124,15 @@ impl NetSetup {
pub async fn remove_proxy_ndp(&mut self, addr: Ipv6Addr) -> Result<(), std::io::Error> {
let addr_str = addr.to_string();
let output = tokio::process::Command::new("ip")
.args(["-6", "neigh", "del", "proxy", &addr_str, "dev", &self.lan_interface])
.args([
"-6",
"neigh",
"del",
"proxy",
&addr_str,
"dev",
&self.lan_interface,
])
.output()
.await?;

View File

@@ -215,11 +215,7 @@ impl VirtualIpPool {
/// Periodic tick — drives state transitions. Returns events for
/// the NAT and network modules.
pub fn tick(
&mut self,
now: Instant,
conntrack: &dyn ConntrackQuerier,
) -> Vec<PoolEvent> {
pub fn tick(&mut self, now: Instant, conntrack: &dyn ConntrackQuerier) -> Vec<PoolEvent> {
let mut events = Vec::new();
let mut to_free = Vec::new();
let ttl = std::time::Duration::from_secs(self.ttl_secs);
@@ -227,9 +223,7 @@ impl VirtualIpPool {
for (node_addr, mapping) in &mut self.mappings {
// Query conntrack for active sessions
let sessions = conntrack
.active_sessions(mapping.virtual_ip)
.unwrap_or(0);
let sessions = conntrack.active_sessions(mapping.virtual_ip).unwrap_or(0);
mapping.session_count = sessions;
match mapping.state {
@@ -462,7 +456,10 @@ mod tests {
pool.allocate(make_node_addr(i), make_mesh_addr(i), "test.fips")
.unwrap();
}
assert!(pool.allocate(make_node_addr(4), make_mesh_addr(4), "test.fips").is_err());
assert!(
pool.allocate(make_node_addr(4), make_mesh_addr(4), "test.fips")
.is_err()
);
}
#[test]
@@ -485,7 +482,10 @@ mod tests {
let events = pool.tick(later, &ct);
assert!(events.is_empty());
assert_eq!(pool.mappings.len(), 1);
assert_eq!(pool.mappings.values().next().unwrap().state, MappingState::Draining);
assert_eq!(
pool.mappings.values().next().unwrap().state,
MappingState::Draining
);
// Tick after grace period — freed
let after_grace = later + std::time::Duration::from_secs(2);
@@ -541,7 +541,8 @@ mod tests {
assert_eq!(status.free, 255);
assert_eq!(status.allocated, 0);
pool.allocate(make_node_addr(1), make_mesh_addr(1), "test.fips").unwrap();
pool.allocate(make_node_addr(1), make_mesh_addr(1), "test.fips")
.unwrap();
let status = pool.status();
assert_eq!(status.allocated, 1);
assert_eq!(status.free, 254);

View File

@@ -7,9 +7,10 @@ use std::time::{Duration, Instant};
use crate::mmp::algorithms::{JitterEstimator, OwdTrendDetector};
use crate::mmp::report::ReceiverReport;
use crate::mmp::{COLD_START_SAMPLES, DEFAULT_COLD_START_INTERVAL_MS,
DEFAULT_OWD_WINDOW_SIZE, MAX_REPORT_INTERVAL_MS,
MIN_REPORT_INTERVAL_MS};
use crate::mmp::{
COLD_START_SAMPLES, DEFAULT_COLD_START_INTERVAL_MS, DEFAULT_OWD_WINDOW_SIZE,
MAX_REPORT_INTERVAL_MS, MIN_REPORT_INTERVAL_MS,
};
/// Grace period after rekey before resuming jitter calculation.
///
@@ -234,8 +235,7 @@ impl ReceiverState {
self.owd_seq = 0;
self.last_sender_timestamp = 0;
self.last_recv_time = None;
self.rekey_jitter_grace_until =
Some(now + Duration::from_secs(REKEY_JITTER_GRACE_SECS));
self.rekey_jitter_grace_until = Some(now + Duration::from_secs(REKEY_JITTER_GRACE_SECS));
self.ecn_ce_count = 0;
self.interval_has_data = false;
// Keep cumulative_packets_recv, cumulative_bytes_recv (lifetime stats)
@@ -287,14 +287,14 @@ impl ReceiverState {
// We can't get absolute µs from Instant, but we can compute the delta
// between consecutive transits using relative Instant differences.
// Skip during post-rekey grace period to avoid drain-window spikes.
let in_grace = self.rekey_jitter_grace_until
let in_grace = self
.rekey_jitter_grace_until
.is_some_and(|deadline| now < deadline);
if !in_grace {
self.rekey_jitter_grace_until = None; // clear expired grace
if let Some(prev_recv) = self.last_recv_time {
let recv_delta_us = now.duration_since(prev_recv).as_micros() as i64;
let send_delta_us =
sender_us - (self.last_sender_timestamp as i64 * 1000);
let send_delta_us = sender_us - (self.last_sender_timestamp as i64 * 1000);
let transit_delta = (recv_delta_us - send_delta_us) as i32;
self.jitter.update(transit_delta);
}
@@ -324,7 +324,8 @@ impl ReceiverState {
}
// Dwell time: ms between last frame reception and report generation
let dwell_time = self.last_recv_time
let dwell_time = self
.last_recv_time
.map(|t| now.duration_since(t).as_millis() as u16)
.unwrap_or(0);
@@ -604,7 +605,10 @@ mod tests {
// 6th sample: steady state, floor is MIN_REPORT_INTERVAL_MS (1000ms)
// 50ms SRTT → 50ms receiver interval (1× SRTT), clamped to 1000ms
r.update_report_interval_from_srtt(50_000);
assert_eq!(r.report_interval(), Duration::from_millis(MIN_REPORT_INTERVAL_MS));
assert_eq!(
r.report_interval(),
Duration::from_millis(MIN_REPORT_INTERVAL_MS)
);
// 3s SRTT → 3000ms, within [1000, 5000]
r.update_report_interval_from_srtt(3_000_000);
@@ -634,8 +638,8 @@ mod tests {
assert_eq!(r.jitter_us(), 0);
// After grace expires, jitter updates resume
let after_grace = t0 + Duration::from_secs(2)
+ Duration::from_secs(REKEY_JITTER_GRACE_SECS + 1);
let after_grace =
t0 + Duration::from_secs(2) + Duration::from_secs(REKEY_JITTER_GRACE_SECS + 1);
r.record_recv(2, 200, 100, false, after_grace);
r.record_recv(3, 300, 100, false, after_grace + Duration::from_millis(100));
// Now jitter should be updating (non-zero or zero depending on timing)

View File

@@ -6,8 +6,10 @@
use std::time::{Duration, Instant};
use crate::mmp::report::SenderReport;
use crate::mmp::{COLD_START_SAMPLES, DEFAULT_COLD_START_INTERVAL_MS,
MAX_REPORT_INTERVAL_MS, MIN_REPORT_INTERVAL_MS};
use crate::mmp::{
COLD_START_SAMPLES, DEFAULT_COLD_START_INTERVAL_MS, MAX_REPORT_INTERVAL_MS,
MIN_REPORT_INTERVAL_MS,
};
/// Per-peer sender-side MMP state.
///
@@ -123,7 +125,9 @@ impl SenderState {
match self.last_report_time {
None => true, // Never sent a report — send immediately
Some(last) => {
let effective = self.report_interval.mul_f64(self.send_failure_backoff_multiplier());
let effective = self
.report_interval
.mul_f64(self.send_failure_backoff_multiplier());
now.duration_since(last) >= effective
}
}
@@ -327,11 +331,17 @@ mod tests {
// 6th sample: now in steady state, floor is MIN_REPORT_INTERVAL_MS (1000ms)
// 50ms RTT → 100ms sender interval (2× SRTT), clamped to 1000ms
s.update_report_interval_from_srtt(50_000);
assert_eq!(s.report_interval(), Duration::from_millis(MIN_REPORT_INTERVAL_MS));
assert_eq!(
s.report_interval(),
Duration::from_millis(MIN_REPORT_INTERVAL_MS)
);
// 3s RTT → 6s, clamped to max 5s
s.update_report_interval_from_srtt(3_000_000);
assert_eq!(s.report_interval(), Duration::from_millis(MAX_REPORT_INTERVAL_MS));
assert_eq!(
s.report_interval(),
Duration::from_millis(MAX_REPORT_INTERVAL_MS)
);
}
#[test]

View File

@@ -1,12 +1,10 @@
//! Handshake handlers and connection promotion.
use crate::node::{Node, NodeError};
use crate::peer::{
cross_connection_winner, ActivePeer, PeerConnection, PromotionResult,
};
use crate::transport::{Link, LinkDirection, LinkId, ReceivedPacket};
use crate::node::wire::{build_msg2, Msg1Header, Msg2Header};
use crate::PeerIdentity;
use crate::node::wire::{Msg1Header, Msg2Header, build_msg2};
use crate::node::{Node, NodeError};
use crate::peer::{ActivePeer, PeerConnection, PromotionResult, cross_connection_winner};
use crate::transport::{Link, LinkDirection, LinkId, ReceivedPacket};
use std::time::Duration;
use tracing::{debug, info, warn};
@@ -63,8 +61,7 @@ impl Node {
{
if link.direction() == LinkDirection::Inbound {
// Check if this link belongs to an already-promoted active peer
let is_active_peer = self.peers.values()
.any(|p| p.link_id() == existing_link_id);
let is_active_peer = self.peers.values().any(|p| p.link_id() == existing_link_id);
if is_active_peer {
// Possible restart — fall through to decrypt and check epoch
@@ -100,8 +97,7 @@ impl Node {
// peer, this may be a rekey msg1 (same epoch) or a
// restart (different epoch). Set possible_restart to enable
// the epoch/rekey check below.
let is_active_peer = self.peers.values()
.any(|p| p.link_id() == existing_link_id);
let is_active_peer = self.peers.values().any(|p| p.link_id() == existing_link_id);
if is_active_peer {
possible_restart = true;
} else {
@@ -126,7 +122,12 @@ impl Node {
let our_keypair = self.identity.keypair();
let noise_msg1 = &packet.data[header.noise_msg1_offset..];
let msg2_response = match conn.receive_handshake_init(our_keypair, self.startup_epoch, noise_msg1, packet.timestamp_ms) {
let msg2_response = match conn.receive_handshake_init(
our_keypair,
self.startup_epoch,
noise_msg1,
packet.timestamp_ms,
) {
Ok(m) => m,
Err(e) => {
self.msg1_rate_limiter.complete_handshake();
@@ -162,9 +163,7 @@ impl Node {
// If we fell through from the addr_to_link check above with
// possible_restart=true, we now have the decrypted epoch from msg1.
// Compare it against the stored epoch for this peer.
if possible_restart
&& let Some(existing_peer) = self.peers.get(&peer_node_addr)
{
if possible_restart && let Some(existing_peer) = self.peers.get(&peer_node_addr) {
let new_epoch = conn.remote_epoch();
let existing_epoch = existing_peer.remote_epoch();
@@ -192,10 +191,8 @@ impl Node {
// During simultaneous connection, both sides promote
// within the same tick and the peer's msg1 arrives
// immediately — a genuine rekey can't fire that fast.
let session_age_secs = existing_peer
.session_established_at()
.elapsed()
.as_secs();
let session_age_secs =
existing_peer.session_established_at().elapsed().as_secs();
if self.config.node.rekey.enabled
&& existing_peer.has_session()
&& existing_peer.is_healthy()
@@ -272,7 +269,8 @@ impl Node {
};
// Send msg2 response using the new handshake
let wire_msg2 = build_msg2(our_new_index, header.sender_idx, &msg2_response);
let wire_msg2 =
build_msg2(our_new_index, header.sender_idx, &msg2_response);
if let Some(transport) = self.transports.get(&packet.transport_id) {
match transport.send(&packet.remote_addr, &wire_msg2).await {
Ok(_) => {
@@ -401,7 +399,8 @@ impl Node {
// Clean up on failure
self.connections.remove(&link_id);
self.links.remove(&link_id);
self.addr_to_link.remove(&(packet.transport_id, packet.remote_addr));
self.addr_to_link
.remove(&(packet.transport_id, packet.remote_addr));
let _ = self.index_allocator.free(our_index);
self.msg1_rate_limiter.complete_handshake();
return;
@@ -434,7 +433,10 @@ impl Node {
self.bloom_state.mark_update_needed(node_addr);
self.reset_discovery_backoff();
}
PromotionResult::CrossConnectionWon { loser_link_id, node_addr } => {
PromotionResult::CrossConnectionWon {
loser_link_id,
node_addr,
} => {
// Store msg2 on peer for resend on duplicate msg1
if let Some(peer) = self.peers.get_mut(&node_addr) {
peer.set_handshake_msg2(wire_msg2.clone());
@@ -552,9 +554,7 @@ impl Node {
// Find peer with rekey in progress for this index
let peer_addr = self.peers.iter().find_map(|(addr, peer)| {
if peer.rekey_in_progress()
&& peer.rekey_our_index() == Some(header.receiver_idx)
{
if peer.rekey_in_progress() && peer.rekey_our_index() == Some(header.receiver_idx) {
Some(*addr)
} else {
None
@@ -568,15 +568,12 @@ impl Node {
if let Some(peer) = self.peers.get_mut(&peer_node_addr) {
match peer.complete_rekey_msg2(noise_msg2) {
Ok(session) => {
let our_index = peer.rekey_our_index()
.unwrap_or(header.receiver_idx);
let our_index = peer.rekey_our_index().unwrap_or(header.receiver_idx);
peer.set_pending_session(session, our_index, header.sender_idx);
if let Some(transport_id) = peer.transport_id() {
self.peers_by_index.insert(
(transport_id, our_index.as_u32()),
peer_node_addr,
);
self.peers_by_index
.insert((transport_id, our_index.as_u32()), peer_node_addr);
}
debug!(
@@ -679,15 +676,17 @@ impl Node {
let outbound_our_index = conn.our_index();
let outbound_session = conn.take_session();
let (outbound_session, outbound_our_index) =
match (outbound_session, outbound_our_index) {
(Some(s), Some(idx)) => (s, idx),
_ => {
warn!(peer = %self.peer_display_name(&peer_node_addr), "Incomplete outbound connection");
self.pending_outbound.remove(&key);
return;
}
};
let (outbound_session, outbound_our_index) = match (
outbound_session,
outbound_our_index,
) {
(Some(s), Some(idx)) => (s, idx),
_ => {
warn!(peer = %self.peer_display_name(&peer_node_addr), "Incomplete outbound connection");
self.pending_outbound.remove(&key);
return;
}
};
if let Some(peer) = self.peers.get_mut(&peer_node_addr) {
let suppressed = peer.replay_suppressed_count();
@@ -700,13 +699,12 @@ impl Node {
// Update peers_by_index: remove old inbound index, add outbound
let transport_id = peer.transport_id().unwrap();
if let Some(old_idx) = old_our_index {
self.peers_by_index.remove(&(transport_id, old_idx.as_u32()));
self.peers_by_index
.remove(&(transport_id, old_idx.as_u32()));
let _ = self.index_allocator.free(old_idx);
}
self.peers_by_index.insert(
(transport_id, outbound_our_index.as_u32()),
peer_node_addr,
);
self.peers_by_index
.insert((transport_id, outbound_our_index.as_u32()), peer_node_addr);
if suppressed > 0 {
debug!(
@@ -791,7 +789,10 @@ impl Node {
self.bloom_state.mark_update_needed(node_addr);
self.reset_discovery_backoff();
}
PromotionResult::CrossConnectionWon { loser_link_id, node_addr } => {
PromotionResult::CrossConnectionWon {
loser_link_id,
node_addr,
} => {
// Close the losing TCP connection (no-op for connectionless)
if let Some(loser_link) = self.links.get(&loser_link_id) {
let loser_tid = loser_link.transport_id();
@@ -803,10 +804,8 @@ impl Node {
// Clean up the losing connection's link
self.remove_link(&loser_link_id);
// Ensure addr_to_link points to the winning link
self.addr_to_link.insert(
(packet.transport_id, packet.remote_addr.clone()),
link_id,
);
self.addr_to_link
.insert((packet.transport_id, packet.remote_addr.clone()), link_id);
debug!(
peer = %self.peer_display_name(&node_addr),
loser_link_id = %loser_link_id,
@@ -873,30 +872,31 @@ impl Node {
.take_session()
.ok_or(NodeError::NoSession(link_id))?;
let our_index = connection.our_index().ok_or_else(|| {
NodeError::PromotionFailed {
let our_index = connection
.our_index()
.ok_or_else(|| NodeError::PromotionFailed {
link_id,
reason: "missing our_index".into(),
}
})?;
let their_index = connection.their_index().ok_or_else(|| {
NodeError::PromotionFailed {
})?;
let their_index = connection
.their_index()
.ok_or_else(|| NodeError::PromotionFailed {
link_id,
reason: "missing their_index".into(),
}
})?;
let transport_id = connection.transport_id().ok_or_else(|| {
NodeError::PromotionFailed {
})?;
let transport_id = connection
.transport_id()
.ok_or_else(|| NodeError::PromotionFailed {
link_id,
reason: "missing transport_id".into(),
}
})?;
let current_addr = connection.source_addr().ok_or_else(|| {
NodeError::PromotionFailed {
})?;
let current_addr = connection
.source_addr()
.ok_or_else(|| NodeError::PromotionFailed {
link_id,
reason: "missing source_addr".into(),
}
})?.clone();
})?
.clone();
let link_stats = connection.link_stats().clone();
let remote_epoch = connection.remote_epoch();
@@ -908,11 +908,8 @@ impl Node {
let existing_link_id = existing_peer.link_id();
// Determine which connection wins
let this_wins = cross_connection_winner(
self.identity.node_addr(),
&peer_node_addr,
is_outbound,
);
let this_wins =
cross_connection_winner(self.identity.node_addr(), &peer_node_addr, is_outbound);
if this_wins {
// This connection wins, replace the existing peer
@@ -923,8 +920,7 @@ impl Node {
if let (Some(old_tid), Some(old_idx)) =
(old_peer.transport_id(), old_peer.our_index())
{
self.peers_by_index
.remove(&(old_tid, old_idx.as_u32()));
self.peers_by_index.remove(&(old_tid, old_idx.as_u32()));
let _ = self.index_allocator.free(old_idx);
}
@@ -942,7 +938,9 @@ impl Node {
&self.config.node.mmp,
remote_epoch,
);
new_peer.set_tree_announce_min_interval_ms(self.config.node.tree.announce_min_interval_ms);
new_peer.set_tree_announce_min_interval_ms(
self.config.node.tree.announce_min_interval_ms,
);
self.peers.insert(peer_node_addr, new_peer);
self.peers_by_index
@@ -1008,13 +1006,17 @@ impl Node {
// Normal promotion
if self.max_peers > 0 && self.peers.len() >= self.max_peers {
let _ = self.index_allocator.free(our_index);
return Err(NodeError::MaxPeersExceeded { max: self.max_peers });
return Err(NodeError::MaxPeersExceeded {
max: self.max_peers,
});
}
// Preserve tree announce rate-limit state from old peer (if reconnecting).
// Without this, reconnection resets the rate limit window to zero,
// allowing an immediate announce that can feed an announce loop.
let old_announce_ts = self.peers.get(&peer_node_addr)
let old_announce_ts = self
.peers
.get(&peer_node_addr)
.map(|p| p.last_tree_announce_sent_ms());
let mut new_peer = ActivePeer::with_session(
@@ -1031,7 +1033,8 @@ impl Node {
&self.config.node.mmp,
remote_epoch,
);
new_peer.set_tree_announce_min_interval_ms(self.config.node.tree.announce_min_interval_ms);
new_peer
.set_tree_announce_min_interval_ms(self.config.node.tree.announce_min_interval_ms);
if let Some(ts) = old_announce_ts {
new_peer.set_last_tree_announce_sent_ms(ts);
}

View File

@@ -5,11 +5,11 @@
//! 2. Drain window expiry (clean up previous session after cutover)
//! 3. Initiator-side cutover (first send after handshake completion)
use crate::NodeAddr;
use crate::node::Node;
use crate::node::wire::build_msg1;
use crate::noise::HandshakeState;
use crate::protocol::{SessionDatagram, SessionSetup};
use crate::NodeAddr;
use tracing::{debug, trace, warn};
/// Keep previous session alive for this long after cutover.
@@ -69,7 +69,8 @@ impl Node {
}
let elapsed = peer.session_established_at().elapsed().as_secs();
let counter = peer.noise_session()
let counter = peer
.noise_session()
.map(|s| s.current_send_counter())
.unwrap_or(0);
@@ -88,9 +89,10 @@ impl Node {
debug_assert!(
peer.transport_id().is_some()
&& peer.our_index().is_some()
&& self.peers_by_index.contains_key(
&(peer.transport_id().unwrap(), peer.our_index().unwrap().as_u32())
),
&& self.peers_by_index.contains_key(&(
peer.transport_id().unwrap(),
peer.our_index().unwrap().as_u32()
)),
"peers_by_index should contain pre-registered new index after cutover"
);
debug!(
@@ -106,7 +108,8 @@ impl Node {
&& let Some(old_our_index) = peer.complete_drain()
{
if let Some(transport_id) = peer.transport_id() {
self.peers_by_index.remove(&(transport_id, old_our_index.as_u32()));
self.peers_by_index
.remove(&(transport_id, old_our_index.as_u32()));
}
let _ = self.index_allocator.free(old_our_index);
trace!(
@@ -208,7 +211,8 @@ impl Node {
}
// Register in pending_outbound for msg2 dispatch (maps to existing link)
self.pending_outbound.insert((transport_id, our_index.as_u32()), link_id);
self.pending_outbound
.insert((transport_id, our_index.as_u32()), link_id);
}
/// Resend pending rekey msg1s and abandon timed-out rekeys.

View File

@@ -1,11 +1,11 @@
//! Node lifecycle management: start, stop, and peer connection initiation.
use super::{Node, NodeError, NodeState};
use crate::node::wire::build_msg1;
use crate::peer::PeerConnection;
use crate::protocol::{Disconnect, DisconnectReason};
use crate::transport::{packet_channel, Link, LinkDirection, LinkId, TransportAddr, TransportId};
use crate::upper::tun::{run_tun_reader, shutdown_tun_interface, TunDevice, TunState};
use crate::node::wire::build_msg1;
use crate::transport::{Link, LinkDirection, LinkId, TransportAddr, TransportId, packet_channel};
use crate::upper::tun::{TunDevice, TunState, run_tun_reader, shutdown_tun_interface};
use crate::{NodeAddr, PeerIdentity};
use std::thread;
use std::time::Duration;
@@ -50,7 +50,10 @@ impl Node {
return;
}
debug!(count = peer_configs.len(), "Initiating static peer connections");
debug!(
count = peer_configs.len(),
"Initiating static peer connections"
);
for peer_config in peer_configs {
if let Err(e) = self.initiate_peer_connection(&peer_config).await {
@@ -67,14 +70,16 @@ impl Node {
/// Initiate a connection to a single peer.
///
/// Creates a link, starts the Noise handshake, and sends the first message.
pub(super) async fn initiate_peer_connection(&mut self, peer_config: &crate::config::PeerConfig) -> Result<(), NodeError> {
pub(super) async fn initiate_peer_connection(
&mut self,
peer_config: &crate::config::PeerConfig,
) -> Result<(), NodeError> {
// Parse the peer's npub to get their identity
let peer_identity = PeerIdentity::from_npub(&peer_config.npub).map_err(|e| {
NodeError::InvalidPeerNpub {
let peer_identity =
PeerIdentity::from_npub(&peer_config.npub).map_err(|e| NodeError::InvalidPeerNpub {
npub: peer_config.npub.clone(),
reason: e.to_string(),
}
})?;
})?;
let peer_node_addr = *peer_identity.node_addr();
@@ -158,7 +163,10 @@ impl Node {
(tid, TransportAddr::from_string(&addr.addr))
};
match self.initiate_connection(transport_id, remote_addr, peer_identity).await {
match self
.initiate_connection(transport_id, remote_addr, peer_identity)
.await
{
Ok(()) => return Ok(()),
Err(e) => {
debug!(
@@ -197,7 +205,9 @@ impl Node {
) -> Result<(), NodeError> {
let peer_node_addr = *peer_identity.node_addr();
let is_connection_oriented = self.transports.get(&transport_id)
let is_connection_oriented = self
.transports
.get(&transport_id)
.map(|t| t.transport_type().connection_oriented)
.unwrap_or(false);
@@ -258,7 +268,8 @@ impl Node {
Ok(())
} else {
// Connectionless: proceed with immediate handshake
self.start_handshake(link_id, transport_id, remote_addr, peer_identity).await
self.start_handshake(link_id, transport_id, remote_addr, peer_identity)
.await
}
}
@@ -295,16 +306,17 @@ impl Node {
// Start the Noise handshake and get message 1
let our_keypair = self.identity.keypair();
let noise_msg1 = match connection.start_handshake(our_keypair, self.startup_epoch, current_time_ms) {
Ok(msg) => msg,
Err(e) => {
// Clean up the index and link
let _ = self.index_allocator.free(our_index);
self.links.remove(&link_id);
self.addr_to_link.remove(&(transport_id, remote_addr));
return Err(NodeError::HandshakeFailed(e.to_string()));
}
};
let noise_msg1 =
match connection.start_handshake(our_keypair, self.startup_epoch, current_time_ms) {
Ok(msg) => msg,
Err(e) => {
// Clean up the index and link
let _ = self.index_allocator.free(our_index);
self.links.remove(&link_id);
self.addr_to_link.remove(&(transport_id, remote_addr));
return Err(NodeError::HandshakeFailed(e.to_string()));
}
};
// Set index and transport info on the connection
connection.set_our_index(our_index);
@@ -328,7 +340,8 @@ impl Node {
connection.set_handshake_msg1(wire_msg1.clone(), current_time_ms + resend_interval);
// Track in pending_outbound for msg2 dispatch
self.pending_outbound.insert((transport_id, our_index.as_u32()), link_id);
self.pending_outbound
.insert((transport_id, our_index.as_u32()), link_id);
self.connections.insert(link_id, connection);
// Send the wire format handshake message
@@ -419,7 +432,10 @@ impl Node {
remote_addr = %remote_addr,
"Auto-connecting to discovered peer"
);
if let Err(e) = self.initiate_connection(transport_id, remote_addr, identity).await {
if let Err(e) = self
.initiate_connection(transport_id, remote_addr, identity)
.await
{
warn!(error = %e, "Failed to auto-connect to discovered peer");
}
}
@@ -481,12 +497,15 @@ impl Node {
);
// Start the handshake now that the transport is connected
if let Err(e) = self.start_handshake(
pending.link_id,
pending.transport_id,
pending.remote_addr.clone(),
pending.peer_identity,
).await {
if let Err(e) = self
.start_handshake(
pending.link_id,
pending.transport_id,
pending.remote_addr.clone(),
pending.peer_identity,
)
.await
{
warn!(
link_id = %pending.link_id,
error = %e,
@@ -583,7 +602,7 @@ impl Node {
// Calculate max MSS for TCP clamping
let effective_mtu = self.effective_ipv6_mtu();
let max_mss = effective_mtu.saturating_sub(40).saturating_sub(20); // IPv6 + TCP headers
info!("effective MTU: {} bytes", effective_mtu);
debug!(" max TCP MSS: {} bytes", max_mss);
@@ -594,11 +613,9 @@ impl Node {
let (shutdown_read_fd, shutdown_write_fd) = {
let mut fds = [0i32; 2];
if unsafe { libc::pipe(fds.as_mut_ptr()) } < 0 {
return Err(NodeError::Tun(
crate::upper::tun::TunError::Configure(
"failed to create shutdown pipe".into(),
),
));
return Err(NodeError::Tun(crate::upper::tun::TunError::Configure(
"failed to create shutdown pipe".into(),
)));
}
(fds[0], fds[1])
};
@@ -622,11 +639,26 @@ impl Node {
let transport_mtu = self.transport_mtu();
#[cfg(target_os = "macos")]
let reader_handle = thread::spawn(move || {
run_tun_reader(device, mtu, our_addr, reader_tun_tx, outbound_tx, transport_mtu, shutdown_read_fd);
run_tun_reader(
device,
mtu,
our_addr,
reader_tun_tx,
outbound_tx,
transport_mtu,
shutdown_read_fd,
);
});
#[cfg(not(target_os = "macos"))]
let reader_handle = thread::spawn(move || {
run_tun_reader(device, mtu, our_addr, reader_tun_tx, outbound_tx, transport_mtu);
run_tun_reader(
device,
mtu,
our_addr,
reader_tun_tx,
outbound_tx,
transport_mtu,
);
});
self.tun_state = TunState::Active;
@@ -636,7 +668,9 @@ impl Node {
self.tun_reader_handle = Some(reader_handle);
self.tun_writer_handle = Some(writer_handle);
#[cfg(target_os = "macos")]
{ self.tun_shutdown_fd = Some(shutdown_write_fd); }
{
self.tun_shutdown_fd = Some(shutdown_write_fd);
}
}
Err(e) => {
self.tun_state = TunState::Failed;
@@ -653,11 +687,19 @@ impl Node {
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);
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));
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);
}
@@ -693,7 +735,8 @@ impl Node {
}
// Send disconnect notifications to all active peers before closing transports
self.send_disconnect_to_all_peers(DisconnectReason::Shutdown).await;
self.send_disconnect_to_all_peers(DisconnectReason::Shutdown)
.await;
// Shutdown transports (they're packet producers)
let transport_ids: Vec<_> = self.transports.keys().cloned().collect();
@@ -767,7 +810,9 @@ impl Node {
let plaintext = disconnect.encode();
// Collect node_addrs to avoid borrow conflict with send helper
let peer_addrs: Vec<NodeAddr> = self.peers.iter()
let peer_addrs: Vec<NodeAddr> = self
.peers
.iter()
.filter(|(_, peer)| peer.can_send() && peer.has_session())
.map(|(addr, _)| *addr)
.collect();
@@ -782,7 +827,10 @@ impl Node {
let mut sent = 0usize;
for node_addr in &peer_addrs {
match self.send_encrypted_link_message(node_addr, &plaintext).await {
match self
.send_encrypted_link_message(node_addr, &plaintext)
.await
{
Ok(()) => sent += 1,
Err(e) => {
debug!(
@@ -847,8 +895,8 @@ impl Node {
///
/// Removes the peer and suppresses auto-reconnect.
pub(crate) fn api_disconnect(&mut self, npub: &str) -> Result<serde_json::Value, String> {
let peer_identity = PeerIdentity::from_npub(npub)
.map_err(|e| format!("invalid npub '{npub}': {e}"))?;
let peer_identity =
PeerIdentity::from_npub(npub).map_err(|e| format!("invalid npub '{npub}': {e}"))?;
let node_addr = *peer_identity.node_addr();
if !self.peers.contains_key(&node_addr) {

View File

@@ -5,40 +5,43 @@
//! Bloom filters, coordinate caches, transports, links, and peers.
mod bloom;
mod discovery_rate_limit;
mod handlers;
mod lifecycle;
mod retry;
mod discovery_rate_limit;
mod rate_limit;
mod retry;
mod routing_error_rate_limit;
pub(crate) mod session;
pub(crate) mod session_wire;
pub(crate) mod wire;
pub(crate) mod stats;
mod tree;
#[cfg(test)]
mod tests;
mod tree;
pub(crate) mod wire;
use crate::bloom::BloomState;
use crate::cache::CoordCache;
use crate::utils::index::IndexAllocator;
use crate::node::session::SessionEntry;
use crate::peer::{ActivePeer, PeerConnection};
use self::discovery_rate_limit::{DiscoveryBackoff, DiscoveryForwardRateLimiter};
use self::rate_limit::HandshakeRateLimiter;
use self::routing_error_rate_limit::RoutingErrorRateLimiter;
use self::wire::{
FLAG_CE, FLAG_KEY_EPOCH, FLAG_SP, build_encrypted, build_established_header,
prepend_inner_header,
};
use crate::bloom::BloomState;
use crate::cache::CoordCache;
use crate::node::session::SessionEntry;
use crate::peer::{ActivePeer, PeerConnection};
use crate::transport::ethernet::EthernetTransport;
use crate::transport::tcp::TcpTransport;
use crate::transport::tor::TorTransport;
use crate::transport::udp::UdpTransport;
use crate::transport::{
Link, LinkId, PacketRx, PacketTx, TransportAddr, TransportError, TransportHandle, TransportId,
};
use crate::transport::udp::UdpTransport;
use crate::transport::tcp::TcpTransport;
use crate::transport::tor::TorTransport;
use crate::transport::ethernet::EthernetTransport;
use crate::tree::TreeState;
use crate::upper::hosts::HostMap;
use crate::upper::icmp_rate_limit::IcmpRateLimiter;
use crate::upper::tun::{TunError, TunOutboundRx, TunState, TunTx};
use self::wire::{build_encrypted, build_established_header, prepend_inner_header, FLAG_CE, FLAG_KEY_EPOCH, FLAG_SP};
use crate::utils::index::IndexAllocator;
use crate::{Config, ConfigError, Identity, IdentityError, NodeAddr, PeerIdentity};
use rand::Rng;
use std::collections::{HashMap, VecDeque};
@@ -105,7 +108,11 @@ pub enum NodeError {
SendFailed { node_addr: NodeAddr, reason: String },
#[error("mtu exceeded forwarding to {node_addr}: packet {packet_size} > mtu {mtu}")]
MtuExceeded { node_addr: NodeAddr, packet_size: usize, mtu: u16 },
MtuExceeded {
node_addr: NodeAddr,
packet_size: usize,
mtu: u16,
},
#[error("config error: {0}")]
Config(#[from] ConfigError),
@@ -546,10 +553,7 @@ impl Node {
coords_response_rate_limiter: RoutingErrorRateLimiter::with_interval(
std::time::Duration::from_millis(coords_response_interval_ms),
),
discovery_backoff: DiscoveryBackoff::with_params(
backoff_base_secs,
backoff_max_secs,
),
discovery_backoff: DiscoveryBackoff::with_params(backoff_base_secs, backoff_max_secs),
discovery_forward_limiter: DiscoveryForwardRateLimiter::with_interval(
std::time::Duration::from_secs(forward_min_interval_secs),
),
@@ -784,7 +788,9 @@ impl Node {
#[cfg(any(not(feature = "ble"), test))]
if !ble_instances.is_empty() {
#[cfg(not(test))]
tracing::warn!("BLE transport configured but 'ble' feature not enabled at compile time");
tracing::warn!(
"BLE transport configured but 'ble' feature not enabled at compile time"
);
}
}
@@ -845,13 +851,9 @@ impl Node {
/// (TransportId, TransportAddr) pair by finding the BLE transport
/// instance matching the adapter name.
#[cfg(target_os = "linux")]
fn resolve_ble_addr(
&self,
addr_str: &str,
) -> Result<(TransportId, TransportAddr), NodeError> {
fn resolve_ble_addr(&self, addr_str: &str) -> Result<(TransportId, TransportAddr), NodeError> {
let ta = TransportAddr::from_string(addr_str);
let adapter = crate::transport::ble::addr::adapter_from_addr(&ta)
.ok_or_else(|| {
let adapter = crate::transport::ble::addr::adapter_from_addr(&ta).ok_or_else(|| {
NodeError::NoTransportForType(format!(
"invalid BLE address format '{}': expected 'adapter/mac'",
addr_str
@@ -862,9 +864,7 @@ impl Node {
let transport_id = self
.transports
.iter()
.find(|(_, handle)| {
handle.transport_type().name == "ble" && handle.is_operational()
})
.find(|(_, handle)| handle.transport_type().name == "ble" && handle.is_operational())
.map(|(id, _)| *id)
.ok_or_else(|| {
NodeError::NoTransportForType(format!(
@@ -1060,9 +1060,10 @@ impl Node {
let now = std::time::Instant::now();
let should_log = match self.last_mesh_size_log {
None => true,
Some(last) => now.duration_since(last) >= std::time::Duration::from_secs(
self.config.node.mmp.log_interval_secs,
),
Some(last) => {
now.duration_since(last)
>= std::time::Duration::from_secs(self.config.node.mmp.log_interval_secs)
}
};
if should_log {
tracing::debug!(
@@ -1111,7 +1112,6 @@ impl Node {
self.tun_name.as_deref()
}
// === Resource Limits ===
/// Set the maximum number of connections (handshake phase).
@@ -1192,14 +1192,17 @@ impl Node {
/// Add a link.
pub fn add_link(&mut self, link: Link) -> Result<(), NodeError> {
if self.max_links > 0 && self.links.len() >= self.max_links {
return Err(NodeError::MaxLinksExceeded { max: self.max_links });
return Err(NodeError::MaxLinksExceeded {
max: self.max_links,
});
}
let link_id = link.link_id();
let transport_id = link.transport_id();
let remote_addr = link.remote_addr().clone();
self.links.insert(link_id, link);
self.addr_to_link.insert((transport_id, remote_addr), link_id);
self.addr_to_link
.insert((transport_id, remote_addr), link_id);
Ok(())
}
@@ -1214,8 +1217,14 @@ impl Node {
}
/// Find link ID by transport address.
pub fn find_link_by_addr(&self, transport_id: TransportId, addr: &TransportAddr) -> Option<LinkId> {
self.addr_to_link.get(&(transport_id, addr.clone())).copied()
pub fn find_link_by_addr(
&self,
transport_id: TransportId,
addr: &TransportAddr,
) -> Option<LinkId> {
self.addr_to_link
.get(&(transport_id, addr.clone()))
.copied()
}
/// Remove a link.
@@ -1361,11 +1370,14 @@ impl Node {
pub(crate) fn register_identity(&mut self, node_addr: NodeAddr, pubkey: secp256k1::PublicKey) {
let mut prefix = [0u8; 15];
prefix.copy_from_slice(&node_addr.as_bytes()[0..15]);
self.identity_cache.insert(prefix, (node_addr, pubkey, Self::now_ms()));
self.identity_cache
.insert(prefix, (node_addr, pubkey, Self::now_ms()));
// LRU eviction
let max = self.config.node.cache.identity_size;
if self.identity_cache.len() > max
&& let Some(oldest_key) = self.identity_cache.iter()
&& let Some(oldest_key) = self
.identity_cache
.iter()
.min_by_key(|(_, (_, _, ts))| *ts)
.map(|(k, _)| *k)
{
@@ -1374,7 +1386,10 @@ impl Node {
}
/// Look up a destination by FipsAddress prefix (bytes 1-15 of the IPv6 address).
pub(crate) fn lookup_by_fips_prefix(&mut self, prefix: &[u8; 15]) -> Option<(NodeAddr, secp256k1::PublicKey)> {
pub(crate) fn lookup_by_fips_prefix(
&mut self,
prefix: &[u8; 15],
) -> Option<(NodeAddr, secp256k1::PublicKey)> {
if let Some(entry) = self.identity_cache.get_mut(prefix) {
entry.2 = Self::now_ms(); // LRU touch
Some((entry.0, entry.1))
@@ -1413,9 +1428,7 @@ impl Node {
/// has declared us as their parent (making them our child).
pub(crate) fn is_tree_peer(&self, peer_addr: &NodeAddr) -> bool {
// Peer is our parent
if !self.tree_state.is_root()
&& self.tree_state.my_declaration().parent_id() == peer_addr
{
if !self.tree_state.is_root() && self.tree_state.my_declaration().parent_id() == peer_addr {
return true;
}
// Peer is our child (their declaration names us as parent)
@@ -1464,7 +1477,10 @@ impl Node {
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0);
let dest_coords = self.coord_cache.get_and_touch(dest_node_addr, now_ms)?.clone();
let dest_coords = self
.coord_cache
.get_and_touch(dest_node_addr, now_ms)?
.clone();
// 3. Bloom filter candidates — requires dest_coords for loop-free selection.
// If no candidate is strictly closer, fall through to tree routing.
@@ -1566,7 +1582,8 @@ impl Node {
node_addr: &NodeAddr,
plaintext: &[u8],
) -> Result<(), NodeError> {
self.send_encrypted_link_message_with_ce(node_addr, plaintext, false).await
self.send_encrypted_link_message_with_ce(node_addr, plaintext, false)
.await
}
/// Like `send_encrypted_link_message` but allows setting the FMP CE flag.
@@ -1578,7 +1595,9 @@ impl Node {
plaintext: &[u8],
ce_flag: bool,
) -> Result<(), NodeError> {
let peer = self.peers.get_mut(node_addr)
let peer = self
.peers
.get_mut(node_addr)
.ok_or(NodeError::PeerNotFound(*node_addr))?;
let their_index = peer.their_index().ok_or_else(|| NodeError::SendFailed {
@@ -1589,18 +1608,19 @@ impl Node {
node_addr: *node_addr,
reason: "no transport_id".into(),
})?;
let remote_addr = peer.current_addr().cloned().ok_or_else(|| NodeError::SendFailed {
node_addr: *node_addr,
reason: "no current_addr".into(),
})?;
let remote_addr = peer
.current_addr()
.cloned()
.ok_or_else(|| NodeError::SendFailed {
node_addr: *node_addr,
reason: "no current_addr".into(),
})?;
// Prepend 4-byte session-relative timestamp (inner header)
let timestamp_ms = peer.session_elapsed_ms();
// MMP: read spin bit value before entering session borrow
let sp_flag = peer.mmp()
.map(|mmp| mmp.spin_bit.tx_bit())
.unwrap_or(false);
let sp_flag = peer.mmp().map(|mmp| mmp.spin_bit.tx_bit()).unwrap_or(false);
let mut flags = if sp_flag { FLAG_SP } else { 0 };
if ce_flag {
flags |= FLAG_CE;
@@ -1609,10 +1629,12 @@ impl Node {
flags |= FLAG_KEY_EPOCH;
}
let session = peer.noise_session_mut().ok_or_else(|| NodeError::SendFailed {
node_addr: *node_addr,
reason: "no noise session".into(),
})?;
let session = peer
.noise_session_mut()
.ok_or_else(|| NodeError::SendFailed {
node_addr: *node_addr,
reason: "no noise session".into(),
})?;
// Inner plaintext: [timestamp:4 LE][msg_type][payload...]
let inner_plaintext = prepend_inner_header(timestamp_ms, plaintext);
@@ -1623,18 +1645,24 @@ impl Node {
let header = build_established_header(their_index, counter, flags, payload_len);
// Encrypt with AAD binding to the outer header
let ciphertext = session.encrypt_with_aad(&inner_plaintext, &header).map_err(|e| NodeError::SendFailed {
node_addr: *node_addr,
reason: format!("encryption failed: {}", e),
})?;
let ciphertext = session
.encrypt_with_aad(&inner_plaintext, &header)
.map_err(|e| NodeError::SendFailed {
node_addr: *node_addr,
reason: format!("encryption failed: {}", e),
})?;
let wire_packet = build_encrypted(&header, &ciphertext);
// Re-borrow peer for stats update after sending
let transport = self.transports.get(&transport_id)
let transport = self
.transports
.get(&transport_id)
.ok_or(NodeError::TransportNotFound(transport_id))?;
let bytes_sent = transport.send(&remote_addr, &wire_packet).await
let bytes_sent = transport
.send(&remote_addr, &wire_packet)
.await
.map_err(|e| match e {
TransportError::MtuExceeded { packet_size, mtu } => NodeError::MtuExceeded {
node_addr: *node_addr,

View File

@@ -284,7 +284,14 @@ mod tests {
let hi = (expected * 1.2).max(expected + 0.5).min(10.0);
let tokens = bucket.tokens();
assert!((lo..=hi).contains(&tokens), "tokens: {}, expected ~{:.2} (range {:.2}..={:.2})", tokens, expected, lo, hi);
assert!(
(lo..=hi).contains(&tokens),
"tokens: {}, expected ~{:.2} (range {:.2}..={:.2})",
tokens,
expected,
lo,
hi
);
}
#[test]

View File

@@ -6,12 +6,12 @@
use super::*;
use crate::config::BleConfig;
use crate::transport::ble::BleTransport;
use crate::transport::ble::addr::BleAddr;
use crate::transport::ble::io::{MockBleIo, MockBleStream};
use crate::transport::ble::BleTransport;
use crate::transport::{packet_channel, Transport, TransportHandle, TransportId};
use crate::transport::{Transport, TransportHandle, TransportId, packet_channel};
use spanning_tree::{
cleanup_nodes, drain_all_packets, initiate_handshake, verify_tree_convergence, TestNode,
TestNode, cleanup_nodes, drain_all_packets, initiate_handshake, verify_tree_convergence,
};
use std::collections::HashMap;
use std::sync::{Arc, Mutex as StdMutex};
@@ -44,8 +44,8 @@ async fn make_test_node_ble(node_num: u8) -> TestNode {
adapter: Some("hci0".to_string()),
mtu: Some(2048),
accept_connections: Some(true),
scan: Some(false), // no auto-scan in tests
advertise: Some(false), // no advertising in tests
scan: Some(false), // no auto-scan in tests
advertise: Some(false), // no advertising in tests
auto_connect: Some(false),
..Default::default()
};
@@ -91,7 +91,11 @@ async fn wire_ble_connection(nodes: &[TestNode], i: usize, j: usize, bank: &Stre
bank.lock().unwrap().insert(key, stream_i);
// Inject stream_j into node j's accept loop so it sees the inbound.
let transport_j = nodes[j].node.transports.get(&nodes[j].transport_id).unwrap();
let transport_j = nodes[j]
.node
.transports
.get(&nodes[j].transport_id)
.unwrap();
match transport_j {
TransportHandle::Ble(t) => {
t.io().inject_inbound(stream_j).await;
@@ -103,7 +107,11 @@ async fn wire_ble_connection(nodes: &[TestNode], i: usize, j: usize, bank: &Stre
/// Install a connect handler on node `i` that draws from the stream bank.
fn install_connect_handler(nodes: &[TestNode], i: usize, bank: &StreamBank) {
let bank = Arc::clone(bank);
let transport_i = nodes[i].node.transports.get(&nodes[i].transport_id).unwrap();
let transport_i = nodes[i]
.node
.transports
.get(&nodes[i].transport_id)
.unwrap();
match transport_i {
TransportHandle::Ble(t) => {
t.io().set_connect_handler(move |addr, _psm| {
@@ -125,7 +133,11 @@ fn install_connect_handler(nodes: &[TestNode], i: usize, bank: &StreamBank) {
/// BLE send_async fails fast if no connection exists, so connections must
/// be pre-established before initiating handshakes.
async fn establish_ble_connection(nodes: &[TestNode], i: usize, j: usize) {
let transport = nodes[i].node.transports.get(&nodes[i].transport_id).unwrap();
let transport = nodes[i]
.node
.transports
.get(&nodes[i].transport_id)
.unwrap();
transport.connect(&nodes[j].addr).await.unwrap();
// Let the background connect task complete
tokio::task::yield_now().await;
@@ -288,6 +300,11 @@ async fn test_ble_discovery() {
node.transports
.insert(transport_id, TransportHandle::Ble(transport));
let mut nodes = vec![TestNode { node, transport_id, packet_rx, addr: ta }];
let mut nodes = vec![TestNode {
node,
transport_id,
packet_rx,
addr: ta,
}];
cleanup_nodes(&mut nodes).await;
}

View File

@@ -4,9 +4,9 @@ use crate::transport::{LinkDirection, TransportAddr, packet_channel};
use crate::utils::index::SessionIndex;
use std::time::Duration;
mod bloom;
#[cfg(target_os = "linux")]
mod ble;
mod bloom;
mod disconnect;
mod discovery;
mod ethernet;

View File

@@ -16,9 +16,9 @@ pub struct BleAddr {
impl BleAddr {
/// Parse a BLE address from the `"adapter/AA:BB:CC:DD:EE:FF"` format.
pub fn parse(s: &str) -> Result<Self, TransportError> {
let (adapter, mac_str) = s
.split_once('/')
.ok_or_else(|| TransportError::InvalidAddress(format!("missing '/' in BLE address: {s}")))?;
let (adapter, mac_str) = s.split_once('/').ok_or_else(|| {
TransportError::InvalidAddress(format!("missing '/' in BLE address: {s}"))
})?;
if adapter.is_empty() {
return Err(TransportError::InvalidAddress("empty adapter name".into()));
@@ -75,11 +75,7 @@ impl BleAddr {
/// Convert to a bluer L2CAP `SocketAddr` with the given PSM.
pub fn to_socket_addr(&self, psm: u16) -> bluer::l2cap::SocketAddr {
bluer::l2cap::SocketAddr::new(
self.to_bluer_address(),
bluer::AddressType::LePublic,
psm,
)
bluer::l2cap::SocketAddr::new(self.to_bluer_address(), bluer::AddressType::LePublic, psm)
}
}

View File

@@ -54,9 +54,7 @@ pub trait BleScanner: Send {
/// Wait for the next discovered device.
///
/// Returns `None` when scanning is stopped.
fn next(
&mut self,
) -> impl std::future::Future<Output = Option<BleAddr>> + Send;
fn next(&mut self) -> impl std::future::Future<Output = Option<BleAddr>> + Send;
}
/// Core BLE I/O operations.
@@ -117,7 +115,9 @@ mod bluer_impl {
use crate::transport::TransportError;
use bluer::l2cap::{SeqPacket, SeqPacketListener, Socket, SocketAddr};
use bluer::{adv::Advertisement, AdapterEvent, AddressType, DiscoveryFilter, DiscoveryTransport};
use bluer::{
AdapterEvent, AddressType, DiscoveryFilter, DiscoveryTransport, adv::Advertisement,
};
use futures::StreamExt;
use std::collections::{BTreeSet, HashSet};
use std::pin::Pin;
@@ -138,10 +138,7 @@ mod bluer_impl {
/// Map a std::io::Error to a TransportError.
fn map_io_err(context: &str, e: std::io::Error) -> TransportError {
TransportError::Io(std::io::Error::new(
e.kind(),
format!("{}: {}", context, e),
))
TransportError::Io(std::io::Error::new(e.kind(), format!("{}: {}", context, e)))
}
// ----------------------------------------------------------------
@@ -159,20 +156,25 @@ mod bluer_impl {
impl BluerStream {
/// Construct from a connected SeqPacket, querying MTU values.
pub fn new(conn: SeqPacket, remote: BleAddr) -> Result<Self, TransportError> {
let send_mtu = conn
.send_mtu()
.map_err(|e| map_io_err("send_mtu", e))? as u16;
let recv_mtu = conn
.recv_mtu()
.map_err(|e| map_io_err("recv_mtu", e))? as u16;
let send_mtu = conn.send_mtu().map_err(|e| map_io_err("send_mtu", e))? as u16;
let recv_mtu = conn.recv_mtu().map_err(|e| map_io_err("recv_mtu", e))? as u16;
// Log negotiated PHY for diagnostics (2M vs 1M)
match conn.as_ref().phy() {
Ok(phy) => debug!(addr = %remote, phy, send_mtu, recv_mtu, "BLE connection established"),
Err(_) => debug!(addr = %remote, send_mtu, recv_mtu, "BLE connection established (PHY query unsupported)"),
Ok(phy) => {
debug!(addr = %remote, phy, send_mtu, recv_mtu, "BLE connection established")
}
Err(_) => {
debug!(addr = %remote, send_mtu, recv_mtu, "BLE connection established (PHY query unsupported)")
}
}
Ok(Self { conn, remote, send_mtu, recv_mtu })
Ok(Self {
conn,
remote,
send_mtu,
recv_mtu,
})
}
}
@@ -250,8 +252,7 @@ mod bluer_impl {
if let Ok(device) = self.adapter.device(addr) {
match device.uuids().await {
Ok(Some(uuids)) if uuids.contains(&FIPS_SERVICE_UUID) => {
let ble_addr =
BleAddr::from_bluer(addr, &self.adapter_name);
let ble_addr = BleAddr::from_bluer(addr, &self.adapter_name);
debug!(addr = %ble_addr, "BLE scanner: FIPS peer found");
return Some(ble_addr);
}
@@ -359,11 +360,7 @@ mod bluer_impl {
})
}
async fn connect(
&self,
addr: &BleAddr,
psm: u16,
) -> Result<Self::Stream, TransportError> {
async fn connect(&self, addr: &BleAddr, psm: u16) -> Result<Self::Stream, TransportError> {
let target_sa = addr.to_socket_addr(psm);
let socket = Socket::<SeqPacket>::new_seq_packet()
@@ -507,11 +504,7 @@ pub struct MockBleStream {
impl MockBleStream {
/// Create a linked pair of mock streams simulating an L2CAP connection.
pub fn pair(
addr_a: BleAddr,
addr_b: BleAddr,
mtu: u16,
) -> (Self, Self) {
pub fn pair(addr_a: BleAddr, addr_b: BleAddr, mtu: u16) -> (Self, Self) {
let (tx_a, rx_a) = tokio::sync::mpsc::channel(64);
let (tx_b, rx_b) = tokio::sync::mpsc::channel(64);
let stream_a = Self {

View File

@@ -36,11 +36,11 @@ use io::{BleIo, BleScanner, BleStream};
use pool::{BleConnection, ConnectionPool};
use stats::BleStats;
use secp256k1::XOnlyPublicKey;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::Mutex;
use tokio::task::JoinHandle;
use secp256k1::XOnlyPublicKey;
use tracing::{debug, info, trace, warn};
/// Default FIPS L2CAP PSM (Protocol Service Multiplexer).
@@ -58,7 +58,6 @@ pub type DefaultBleTransport = BleTransport<io::BluerIo>;
#[cfg(any(not(feature = "ble"), test))]
pub type DefaultBleTransport = BleTransport<io::MockBleIo>;
// ============================================================================
// BLE Transport
// ============================================================================
@@ -490,8 +489,7 @@ impl<I: BleIo> BleTransport<I> {
match pubkey_exchange(&stream, our_pubkey).await {
Ok(peer_pubkey) => {
debug!(addr = %addr_clone, "BLE outbound pubkey exchange complete");
discovery_buffer
.add_peer_with_pubkey(&ble_addr, peer_pubkey);
discovery_buffer.add_peer_with_pubkey(&ble_addr, peer_pubkey);
}
Err(e) => {
warn!(
@@ -1077,16 +1075,13 @@ mod tests {
fn make_transport(
io: MockBleIo,
) -> (BleTransport<MockBleIo>, tokio::sync::mpsc::Receiver<ReceivedPacket>) {
) -> (
BleTransport<MockBleIo>,
tokio::sync::mpsc::Receiver<ReceivedPacket>,
) {
let (tx, rx) = tokio::sync::mpsc::channel(64);
let config = BleConfig::default();
let transport = BleTransport::new(
TransportId::new(1),
None,
config,
io,
tx,
);
let transport = BleTransport::new(TransportId::new(1), None, config, io, tx);
(transport, rx)
}
@@ -1177,7 +1172,10 @@ mod tests {
let io = MockBleIo::new("hci0", test_addr(1));
let (transport, _rx) = make_transport(io);
let addr = test_addr(2).to_transport_addr();
assert_eq!(transport.connection_state_sync(&addr), ConnectionState::None);
assert_eq!(
transport.connection_state_sync(&addr),
ConnectionState::None
);
}
/// Verify that the cross-probe tie-breaker follows the same convention

View File

@@ -151,9 +151,7 @@ impl<S> ConnectionPool<S> {
.min_by_key(|(_, c)| c.established_at)
.map(|(addr, _)| addr.clone())
.ok_or_else(|| {
TransportError::NotSupported(
"BLE pool full: all connections are static".into(),
)
TransportError::NotSupported("BLE pool full: all connections are static".into())
})
} else {
// Non-static peer evicts oldest non-static
@@ -163,9 +161,7 @@ impl<S> ConnectionPool<S> {
.min_by_key(|(_, c)| c.established_at)
.map(|(addr, _)| addr.clone())
.ok_or_else(|| {
TransportError::NotSupported(
"BLE pool full: all connections are static".into(),
)
TransportError::NotSupported("BLE pool full: all connections are static".into())
})
}
}

View File

@@ -108,7 +108,6 @@ impl BleStats {
self.scan_results.fetch_add(1, Ordering::Relaxed);
}
/// Take a snapshot of all counters.
pub fn snapshot(&self) -> BleStatsSnapshot {
BleStatsSnapshot {

View File

@@ -253,12 +253,16 @@ impl EthernetTransport {
if let Some(task) = self.beacon_task.take() {
task.abort();
#[cfg(not(target_os = "macos"))]
{ let _ = task.await; }
{
let _ = task.await;
}
}
if let Some(task) = self.recv_task.take() {
task.abort();
#[cfg(not(target_os = "macos"))]
{ let _ = task.await; }
{
let _ = task.await;
}
}
// Drop socket

View File

@@ -41,7 +41,11 @@ mod async_impl {
Ok(Self { inner: async_fd })
}
pub async fn send_to(&self, data: &[u8], dest_mac: &[u8; 6]) -> Result<usize, TransportError> {
pub async fn send_to(
&self,
data: &[u8],
dest_mac: &[u8; 6],
) -> Result<usize, TransportError> {
loop {
let mut guard = self
.inner
@@ -57,10 +61,7 @@ mod async_impl {
}
}
pub async fn recv_from(
&self,
buf: &mut [u8],
) -> Result<(usize, [u8; 6]), TransportError> {
pub async fn recv_from(&self, buf: &mut [u8]) -> Result<(usize, [u8; 6]), TransportError> {
loop {
let mut guard = self
.inner
@@ -136,7 +137,10 @@ mod async_impl {
loop {
// Drain any buffered frames from the previous read
while let Some(result) = super::platform::parse_next_frame(
&parse_buf, &mut parse_offset, parse_len, &mut read_buf,
&parse_buf,
&mut parse_offset,
parse_len,
&mut read_buf,
) {
match result {
Ok((n, mac)) => {
@@ -207,22 +211,24 @@ mod async_impl {
})
}
pub async fn send_to(&self, data: &[u8], dest_mac: &[u8; 6]) -> Result<usize, TransportError> {
pub async fn send_to(
&self,
data: &[u8],
dest_mac: &[u8; 6],
) -> Result<usize, TransportError> {
let socket = Arc::clone(&self.inner);
let data = data.to_vec();
let dest = *dest_mac;
tokio::task::spawn_blocking(move || {
socket.send_to(&data, &dest)
socket
.send_to(&data, &dest)
.map_err(|e| TransportError::SendFailed(format!("{}", e)))
})
.await
.map_err(|e| TransportError::SendFailed(format!("spawn_blocking: {}", e)))?
}
pub async fn recv_from(
&self,
buf: &mut [u8],
) -> Result<(usize, [u8; 6]), TransportError> {
pub async fn recv_from(&self, buf: &mut [u8]) -> Result<(usize, [u8; 6]), TransportError> {
let mut rx = self.rx.lock().await;
match rx.recv().await {
Some((data, mac)) => {

View File

@@ -248,7 +248,10 @@ fn get_mac_addr(fd: RawFd, if_index: i32) -> Result<[u8; 6], TransportError> {
// Use if_indextoname to get the name
let mut name_buf = [0u8; libc::IFNAMSIZ];
let ret = unsafe {
libc::if_indextoname(if_index as libc::c_uint, name_buf.as_mut_ptr() as *mut libc::c_char)
libc::if_indextoname(
if_index as libc::c_uint,
name_buf.as_mut_ptr() as *mut libc::c_char,
)
};
if ret.is_null() {
return Err(TransportError::StartFailed(format!(
@@ -259,7 +262,10 @@ fn get_mac_addr(fd: RawFd, if_index: i32) -> Result<[u8; 6], TransportError> {
}
// Copy name into ifreq
let name_len = name_buf.iter().position(|&b| b == 0).unwrap_or(name_buf.len());
let name_len = name_buf
.iter()
.position(|&b| b == 0)
.unwrap_or(name_buf.len());
let copy_len = name_len.min(libc::IFNAMSIZ - 1);
unsafe {
std::ptr::copy_nonoverlapping(
@@ -299,7 +305,10 @@ fn get_if_mtu(fd: RawFd, if_index: i32) -> Result<u16, TransportError> {
// Get the interface name from index
let mut name_buf = [0u8; libc::IFNAMSIZ];
let ret = unsafe {
libc::if_indextoname(if_index as libc::c_uint, name_buf.as_mut_ptr() as *mut libc::c_char)
libc::if_indextoname(
if_index as libc::c_uint,
name_buf.as_mut_ptr() as *mut libc::c_char,
)
};
if ret.is_null() {
return Err(TransportError::StartFailed(format!(
@@ -309,7 +318,10 @@ fn get_if_mtu(fd: RawFd, if_index: i32) -> Result<u16, TransportError> {
)));
}
let name_len = name_buf.iter().position(|&b| b == 0).unwrap_or(name_buf.len());
let name_len = name_buf
.iter()
.position(|&b| b == 0)
.unwrap_or(name_buf.len());
let copy_len = name_len.min(libc::IFNAMSIZ - 1);
unsafe {
std::ptr::copy_nonoverlapping(

View File

@@ -42,7 +42,9 @@ impl FdGuard {
impl Drop for FdGuard {
fn drop(&mut self) {
unsafe { libc::close(self.0); }
unsafe {
libc::close(self.0);
}
}
}
@@ -99,7 +101,8 @@ impl PacketSocket {
unsafe { libc::close(guard.0) };
std::mem::forget(guard); // don't double-close via FdGuard
return Err(TransportError::StartFailed(format!(
"pipe() failed: {}", std::io::Error::last_os_error()
"pipe() failed: {}",
std::io::Error::last_os_error()
)));
}
@@ -157,7 +160,11 @@ impl PacketSocket {
/// Signal the reader thread to stop by writing to the shutdown pipe.
pub fn request_shutdown(&self) {
unsafe {
libc::write(self.shutdown_write_fd, b"x".as_ptr() as *const libc::c_void, 1);
libc::write(
self.shutdown_write_fd,
b"x".as_ptr() as *const libc::c_void,
1,
);
}
}
@@ -387,7 +394,9 @@ fn bind_to_interface(fd: RawFd, interface: &str) -> Result<(), TransportError> {
let ret = unsafe { libc::ioctl(fd, BIOCSETIF, ifreq.as_ptr()) };
if ret < 0 {
return Err(TransportError::StartFailed(format!(
"BIOCSETIF({}) failed: {}", interface, std::io::Error::last_os_error()
"BIOCSETIF({}) failed: {}",
interface,
std::io::Error::last_os_error()
)));
}
Ok(())
@@ -399,7 +408,8 @@ fn set_bpf_immediate(fd: RawFd) -> Result<(), TransportError> {
let ret = unsafe { libc::ioctl(fd, BIOCIMMEDIATE, &enable) };
if ret < 0 {
return Err(TransportError::StartFailed(format!(
"BIOCIMMEDIATE failed: {}", std::io::Error::last_os_error()
"BIOCIMMEDIATE failed: {}",
std::io::Error::last_os_error()
)));
}
Ok(())
@@ -411,7 +421,8 @@ fn set_bpf_hdrcmplt(fd: RawFd) -> Result<(), TransportError> {
let ret = unsafe { libc::ioctl(fd, BIOCSHDRCMPLT, &enable) };
if ret < 0 {
return Err(TransportError::StartFailed(format!(
"BIOCSHDRCMPLT failed: {}", std::io::Error::last_os_error()
"BIOCSHDRCMPLT failed: {}",
std::io::Error::last_os_error()
)));
}
Ok(())
@@ -425,10 +436,30 @@ fn install_ethertype_filter(fd: RawFd, ethertype: u16) -> Result<(), TransportEr
// accept: ret #65535 ; accept entire packet
// reject: ret #0 ; drop
let filter = [
BpfInsn { code: 0x28, jt: 0, jf: 0, k: 12 }, // ldh [12]
BpfInsn { code: 0x15, jt: 0, jf: 1, k: ethertype as u32 }, // jeq #ethertype
BpfInsn { code: 0x06, jt: 0, jf: 0, k: 0xFFFF }, // ret #65535
BpfInsn { code: 0x06, jt: 0, jf: 0, k: 0 }, // ret #0
BpfInsn {
code: 0x28,
jt: 0,
jf: 0,
k: 12,
}, // ldh [12]
BpfInsn {
code: 0x15,
jt: 0,
jf: 1,
k: ethertype as u32,
}, // jeq #ethertype
BpfInsn {
code: 0x06,
jt: 0,
jf: 0,
k: 0xFFFF,
}, // ret #65535
BpfInsn {
code: 0x06,
jt: 0,
jf: 0,
k: 0,
}, // ret #0
];
let prog = BpfProgram {
@@ -439,7 +470,8 @@ fn install_ethertype_filter(fd: RawFd, ethertype: u16) -> Result<(), TransportEr
let ret = unsafe { libc::ioctl(fd, BIOCSETF, &prog) };
if ret < 0 {
return Err(TransportError::StartFailed(format!(
"BIOCSETF failed: {}", std::io::Error::last_os_error()
"BIOCSETF failed: {}",
std::io::Error::last_os_error()
)));
}
Ok(())
@@ -451,7 +483,8 @@ fn get_bpf_buflen(fd: RawFd) -> Result<usize, TransportError> {
let ret = unsafe { libc::ioctl(fd, BIOCGBLEN, &mut buflen) };
if ret < 0 {
return Err(TransportError::StartFailed(format!(
"BIOCGBLEN failed: {}", std::io::Error::last_os_error()
"BIOCGBLEN failed: {}",
std::io::Error::last_os_error()
)));
}
Ok(buflen as usize)
@@ -480,7 +513,8 @@ fn get_mac_addr(interface: &str) -> Result<[u8; 6], TransportError> {
let ret = unsafe { libc::getifaddrs(&mut addrs) };
if ret != 0 {
return Err(TransportError::StartFailed(format!(
"getifaddrs() failed: {}", std::io::Error::last_os_error()
"getifaddrs() failed: {}",
std::io::Error::last_os_error()
)));
}
@@ -509,7 +543,8 @@ fn get_mac_addr(interface: &str) -> Result<[u8; 6], TransportError> {
cur = unsafe { (*cur).ifa_next };
}
Err(TransportError::StartFailed(format!(
"MAC address not found for interface: {}", interface
"MAC address not found for interface: {}",
interface
)))
})();
@@ -756,7 +791,10 @@ mod tests {
libc::close(read_fd);
libc::close(write_fd);
}
assert!(!readable, "pipe read end should not be readable before write");
assert!(
!readable,
"pipe read end should not be readable before write"
);
}
#[test]
@@ -790,23 +828,19 @@ fn get_if_mtu(if_index: i32) -> Result<u16, TransportError> {
// Get interface name from index
let mut name_buf = [0i8; libc::IFNAMSIZ];
let ret = unsafe {
libc::if_indextoname(if_index as libc::c_uint, name_buf.as_mut_ptr())
};
let ret = unsafe { libc::if_indextoname(if_index as libc::c_uint, name_buf.as_mut_ptr()) };
if ret.is_null() {
unsafe { libc::close(sock) };
return Err(TransportError::StartFailed(format!(
"if_indextoname({}) failed: {}", if_index, std::io::Error::last_os_error()
"if_indextoname({}) failed: {}",
if_index,
std::io::Error::last_os_error()
)));
}
let mut ifr: libc::ifreq = unsafe { std::mem::zeroed() };
unsafe {
std::ptr::copy_nonoverlapping(
name_buf.as_ptr(),
ifr.ifr_name.as_mut_ptr(),
libc::IFNAMSIZ,
);
std::ptr::copy_nonoverlapping(name_buf.as_ptr(), ifr.ifr_name.as_mut_ptr(), libc::IFNAMSIZ);
}
let ret = unsafe { libc::ioctl(sock, SIOCGIFMTU, &mut ifr) };

View File

@@ -4,27 +4,27 @@
//! underlying communication mechanisms (UDP, Ethernet, Tor, etc.) over
//! which FIPS links are established.
pub mod udp;
pub mod tcp;
pub mod tor;
pub mod udp;
pub mod ethernet;
#[cfg(target_os = "linux")]
pub mod ble;
use secp256k1::XOnlyPublicKey;
use udp::UdpTransport;
use tcp::TcpTransport;
use tor::control::TorMonitoringInfo;
use tor::TorTransport;
use ethernet::EthernetTransport;
#[cfg(target_os = "linux")]
use ble::DefaultBleTransport;
use ethernet::EthernetTransport;
use secp256k1::XOnlyPublicKey;
use std::fmt;
use std::net::SocketAddr;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tcp::TcpTransport;
use thiserror::Error;
use tor::TorTransport;
use tor::control::TorMonitoringInfo;
use udp::UdpTransport;
// ============================================================================
// Packet Channel Types
@@ -1066,7 +1066,7 @@ impl TransportHandle {
/// Poll `connection_state()` to check when the connection is ready.
pub async fn connect(&self, addr: &TransportAddr) -> Result<(), TransportError> {
match self {
TransportHandle::Udp(_) => Ok(()), // connectionless
TransportHandle::Udp(_) => Ok(()), // connectionless
TransportHandle::Ethernet(_) => Ok(()), // connectionless
TransportHandle::Tcp(t) => t.connect_async(addr).await,
TransportHandle::Tor(t) => t.connect_async(addr).await,
@@ -1600,10 +1600,8 @@ mod tests {
let addr_b = TransportAddr::from_string("10.0.0.1:5000");
let addr_unknown = TransportAddr::from_string("172.16.0.1:6000");
let transport = PerLinkMtuTransport::new(
1280,
vec![(addr_a.clone(), 512), (addr_b.clone(), 247)],
);
let transport =
PerLinkMtuTransport::new(1280, vec![(addr_a.clone(), 512), (addr_b.clone(), 247)]);
// Known addresses return their per-link MTU
assert_eq!(transport.link_mtu(&addr_a), 512);

View File

@@ -123,7 +123,9 @@ impl TunDevice {
// Read the actual device name (on macOS this is the kernel-assigned utun* name)
let actual_name = {
use tun::AbstractDevice;
device.tun_name().map_err(|e| TunError::Configure(format!("failed to get device name: {}", e)))?
device
.tun_name()
.map_err(|e| TunError::Configure(format!("failed to get device name: {}", e)))?
};
// Configure address and bring up via platform-specific method
@@ -329,7 +331,14 @@ pub fn run_tun_reader(
loop {
match device.read_packet(&mut buf) {
Ok(n) if n > 0 => {
if !handle_tun_packet(&mut buf[..n], max_mss, &name, our_addr, &tun_tx, &outbound_tx) {
if !handle_tun_packet(
&mut buf[..n],
max_mss,
&name,
our_addr,
&tun_tx,
&outbound_tx,
) {
break;
}
}
@@ -355,7 +364,9 @@ struct ShutdownFd(std::os::unix::io::RawFd);
#[cfg(target_os = "macos")]
impl Drop for ShutdownFd {
fn drop(&mut self) {
unsafe { libc::close(self.0); }
unsafe {
libc::close(self.0);
}
}
}
@@ -397,7 +408,13 @@ pub fn run_tun_reader(
libc::FD_SET(tun_fd, &mut read_fds);
libc::FD_SET(shutdown_fd, &mut read_fds);
let ret = libc::select(nfds, &mut read_fds, std::ptr::null_mut(), std::ptr::null_mut(), std::ptr::null_mut());
let ret = libc::select(
nfds,
&mut read_fds,
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
);
if ret < 0 {
let err = std::io::Error::last_os_error();
if err.kind() == std::io::ErrorKind::Interrupted {
@@ -418,7 +435,14 @@ pub fn run_tun_reader(
loop {
match device.read_packet(&mut buf) {
Ok(n) if n > 0 => {
if !handle_tun_packet(&mut buf[..n], max_mss, &name, our_addr, &tun_tx, &outbound_tx) {
if !handle_tun_packet(
&mut buf[..n],
max_mss,
&name,
our_addr,
&tun_tx,
&outbound_tx,
) {
return; // _shutdown_fd closes on drop
}
}
@@ -474,7 +498,7 @@ fn handle_tun_packet(
tun_tx: &TunTx,
outbound_tx: &TunOutboundTx,
) -> bool {
use super::icmp::{build_dest_unreachable, should_send_icmp_error, DestUnreachableCode};
use super::icmp::{DestUnreachableCode, build_dest_unreachable, should_send_icmp_error};
use super::tcp_mss::clamp_tcp_mss;
log_ipv6_packet(packet);
@@ -495,11 +519,8 @@ fn handle_tun_packet(
} else {
// Non-FIPS destination: send ICMPv6 Destination Unreachable
if should_send_icmp_error(packet)
&& let Some(response) = build_dest_unreachable(
packet,
DestUnreachableCode::NoRoute,
our_addr.to_ipv6(),
)
&& let Some(response) =
build_dest_unreachable(packet, DestUnreachableCode::NoRoute, our_addr.to_ipv6())
{
trace!(name = %name, len = response.len(), "Sending ICMPv6 Destination Unreachable (non-FIPS destination)");
if tun_tx.send(response).is_err() {
@@ -574,7 +595,7 @@ impl std::fmt::Debug for TunDevice {
mod platform {
use super::TunError;
use futures::TryStreamExt;
use rtnetlink::{new_connection, Handle, LinkUnspec, RouteMessageBuilder};
use rtnetlink::{Handle, LinkUnspec, RouteMessageBuilder, new_connection};
use std::net::Ipv6Addr;
use tracing::debug;
@@ -652,7 +673,13 @@ mod platform {
// Add ip6 rule to ensure fd00::/8 uses the main table, preventing other
// routing software (e.g. Tailscale) from intercepting FIPS traffic via
// catch-all rules in auxiliary routing tables.
let mut rule_req = handle.rule().add().v6().destination_prefix(fd_prefix, 8).table_id(254).priority(5265);
let mut rule_req = handle
.rule()
.add()
.v6()
.destination_prefix(fd_prefix, 8)
.table_id(254)
.priority(5265);
rule_req.message_mut().header.action = 1.into(); // FR_ACT_TO_TBL
if let Err(e) = rule_req.execute().await {
debug!("ip6 rule for fd00::/8 not added (may already exist): {e}");
@@ -713,7 +740,11 @@ mod platform {
/// Configure a network interface with an IPv6 address using ifconfig/route.
pub async fn configure_interface(name: &str, addr: Ipv6Addr, mtu: u16) -> Result<(), TunError> {
// Add IPv6 address with /128 prefix
run_cmd("ifconfig", &[name, "inet6", &addr.to_string(), "prefixlen", "128"]).await?;
run_cmd(
"ifconfig",
&[name, "inet6", &addr.to_string(), "prefixlen", "128"],
)
.await?;
// Set MTU
run_cmd("ifconfig", &[name, "mtu", &mtu.to_string()]).await?;
@@ -722,7 +753,19 @@ mod platform {
run_cmd("ifconfig", &[name, "up"]).await?;
// Add route for fd00::/8 (FIPS address space) via this interface
run_cmd("route", &["add", "-inet6", "-prefixlen", "8", "fd00::", "-interface", name]).await?;
run_cmd(
"route",
&[
"add",
"-inet6",
"-prefixlen",
"8",
"fd00::",
"-interface",
name,
],
)
.await?;
Ok(())
}