mirror of
https://github.com/jmcorgan/fips.git
synced 2026-07-22 07:48:26 +00:00
Apply rustfmt to master-only code
This commit is contained in:
@@ -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,10 +182,7 @@ async fn main() {
|
||||
}
|
||||
|
||||
let mut buf = [0u8; 512];
|
||||
match tokio::time::timeout(
|
||||
std::time::Duration::from_secs(3),
|
||||
sock.recv_from(&mut buf),
|
||||
)
|
||||
match tokio::time::timeout(std::time::Duration::from_secs(3), sock.recv_from(&mut buf))
|
||||
.await
|
||||
{
|
||||
Ok(Ok(_)) => {
|
||||
@@ -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 ---
|
||||
|
||||
@@ -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() => {},
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
assert!(
|
||||
paths
|
||||
.iter()
|
||||
.any(|p| p.starts_with("/etc/fips") && p.ends_with("fips.yaml")));
|
||||
.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")));
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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()
|
||||
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()
|
||||
}
|
||||
.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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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,18 +241,17 @@ 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
|
||||
{
|
||||
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");
|
||||
|
||||
@@ -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,
|
||||
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,9 +174,7 @@ 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,
|
||||
))
|
||||
HighLevelPayload::Network(NetworkHeaderField::IPv6(IPv6HeaderField::Daddr))
|
||||
.build(),
|
||||
)
|
||||
.with_expr(Cmp::new(CmpOp::Eq, mapping.virtual_ip.octets()))
|
||||
@@ -193,9 +194,7 @@ 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,
|
||||
))
|
||||
HighLevelPayload::Network(NetworkHeaderField::IPv6(IPv6HeaderField::Saddr))
|
||||
.build(),
|
||||
)
|
||||
.with_expr(Cmp::new(CmpOp::Eq, mapping.mesh_addr.octets()))
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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?;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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,8 +676,10 @@ 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) {
|
||||
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");
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,13 +70,15 @@ 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,7 +306,8 @@ 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) {
|
||||
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
|
||||
@@ -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(
|
||||
if let Err(e) = self
|
||||
.start_handshake(
|
||||
pending.link_id,
|
||||
pending.transport_id,
|
||||
pending.remote_addr.clone(),
|
||||
pending.peer_identity,
|
||||
).await {
|
||||
)
|
||||
.await
|
||||
{
|
||||
warn!(
|
||||
link_id = %pending.link_id,
|
||||
error = %e,
|
||||
@@ -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(
|
||||
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) {
|
||||
|
||||
136
src/node/mod.rs
136
src/node/mod.rs
@@ -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,7 +1608,10 @@ impl Node {
|
||||
node_addr: *node_addr,
|
||||
reason: "no transport_id".into(),
|
||||
})?;
|
||||
let remote_addr = peer.current_addr().cloned().ok_or_else(|| NodeError::SendFailed {
|
||||
let remote_addr = peer
|
||||
.current_addr()
|
||||
.cloned()
|
||||
.ok_or_else(|| NodeError::SendFailed {
|
||||
node_addr: *node_addr,
|
||||
reason: "no current_addr".into(),
|
||||
})?;
|
||||
@@ -1598,9 +1620,7 @@ impl Node {
|
||||
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,7 +1629,9 @@ impl Node {
|
||||
flags |= FLAG_KEY_EPOCH;
|
||||
}
|
||||
|
||||
let session = peer.noise_session_mut().ok_or_else(|| NodeError::SendFailed {
|
||||
let session = peer
|
||||
.noise_session_mut()
|
||||
.ok_or_else(|| NodeError::SendFailed {
|
||||
node_addr: *node_addr,
|
||||
reason: "no noise session".into(),
|
||||
})?;
|
||||
@@ -1623,7 +1645,9 @@ 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 {
|
||||
let ciphertext = session
|
||||
.encrypt_with_aad(&inner_plaintext, &header)
|
||||
.map_err(|e| NodeError::SendFailed {
|
||||
node_addr: *node_addr,
|
||||
reason: format!("encryption failed: {}", e),
|
||||
})?;
|
||||
@@ -1631,10 +1655,14 @@ impl Node {
|
||||
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,
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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};
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)) => {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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) };
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user