Add Ethernet transport with beacon discovery

Implement raw Ethernet transport using AF_PACKET SOCK_DGRAM on Linux
with EtherType 0x88B5 (IEEE experimental range) and 1-byte frame type
prefix (0x00=data, 0x01=beacon).

Transport implementation:
- EthernetConfig with interface, ethertype, MTU, buffer sizes, and
  four independent discovery knobs (discovery, announce, auto_connect,
  accept_connections)
- PacketSocket/AsyncPacketSocket wrappers with ioctl helpers for
  interface index, MAC address, and MTU queries
- EthernetTransport with Transport trait impl, async start/stop/send,
  receive loop dispatching data frames and discovery beacons
- Discovery beacons (34 bytes: type + version + x-only pubkey) with
  DiscoveryBuffer for peer accumulation and dedup
- Atomic statistics counters (frames, bytes, errors, beacons)
- Platform-gated with #[cfg(target_os = "linux")]

Transport-layer discovery integration:
- Promote auto_connect() and accept_connections() to Transport trait
  with default implementations and TransportHandle dispatch
- Extract initiate_connection() so both static peer config and
  discovery auto-connect share the same handshake initiation path
- Add poll_transport_discovery() to the tick handler to drain
  discovery buffers and auto-connect to discovered peers
- Enforce accept_connections() in handle_msg1() — transports with
  accept_connections=false silently drop inbound handshakes

Node integration:
- create_transports() handles Ethernet named instances
- resolve_ethernet_addr() parses "interface/mac" address format
- transport_mtu() generalized for multi-transport operation

Test harness:
- VethPair RAII struct for veth pair lifecycle management
- Three #[ignore] integration tests requiring root/CAP_NET_RAW:
  two-node handshake, data exchange, mixed transport coexistence
- Chaos harness: transport-aware topology model, VethManager for
  veth pairs between Docker containers, Ethernet-aware config gen,
  netem split (HTB+u32 for UDP, root netem for veth), transport-aware
  link flaps and node churn with veth re-setup
- Container entrypoint waits for configured Ethernet interfaces
  before starting FIPS (handles veth creation timing)
- New scenarios: ethernet-only (4-node ring), ethernet-mesh (6-node
  mixed UDP+Ethernet with netem and link flaps)

Documentation:
- fips-transport-layer.md: Ethernet section, beacon discovery, WiFi
  compatibility, updated discovery state, trait surface additions,
  implementation status table
- fips-configuration.md: Ethernet parameter table, named instances,
  peer address format, mixed UDP+Ethernet example, complete reference
- fips-wire-formats.md: Ethernet frame type prefix note
This commit is contained in:
Johnathan Corgan
2026-02-26 00:03:14 +00:00
parent 7260ad2878
commit d29da442ac
30 changed files with 2967 additions and 305 deletions
+1 -1
View File
@@ -33,7 +33,7 @@ pub use node::{
NodeConfig, RateLimitConfig, RetryConfig, SessionConfig, SessionMmpConfig, TreeConfig,
};
pub use peer::{ConnectPolicy, PeerAddress, PeerConfig};
pub use transport::{TransportInstances, TransportsConfig, UdpConfig};
pub use transport::{EthernetConfig, TransportInstances, TransportsConfig, UdpConfig};
/// Default config filename.
const CONFIG_FILENAME: &str = "fips.yaml";
+114 -10
View File
@@ -131,6 +131,113 @@ impl<T> Default for TransportInstances<T> {
}
}
/// Default Ethernet EtherType (IEEE 802 experimental).
const DEFAULT_ETHERNET_ETHERTYPE: u16 = 0x88B5;
/// Default Ethernet receive buffer size (2 MB).
const DEFAULT_ETHERNET_RECV_BUF: usize = 2 * 1024 * 1024;
/// Default Ethernet send buffer size (2 MB).
const DEFAULT_ETHERNET_SEND_BUF: usize = 2 * 1024 * 1024;
/// Default beacon announcement interval in seconds.
const DEFAULT_BEACON_INTERVAL_SECS: u64 = 30;
/// Minimum beacon announcement interval in seconds.
const MIN_BEACON_INTERVAL_SECS: u64 = 10;
/// Ethernet transport instance configuration.
///
/// EthernetConfig is always compiled (for config parsing on any platform),
/// but the transport runtime requires Linux (`#[cfg(target_os = "linux")]`).
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct EthernetConfig {
/// Network interface name (e.g., "eth0", "enp3s0"). Required.
pub interface: String,
/// Custom EtherType (default: 0x88B5).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ethertype: Option<u16>,
/// MTU override. Defaults to the interface's MTU minus 1 (for frame type prefix).
/// Cannot exceed the interface's actual MTU.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mtu: Option<u16>,
/// Receive buffer size in bytes. Default: 2 MB.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub recv_buf_size: Option<usize>,
/// Send buffer size in bytes. Default: 2 MB.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub send_buf_size: Option<usize>,
/// Listen for discovery beacons from other nodes. Default: true.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub discovery: Option<bool>,
/// Broadcast announcement beacons on the LAN. Default: false.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub announce: Option<bool>,
/// Auto-connect to discovered peers. Default: false.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub auto_connect: Option<bool>,
/// Accept incoming connection attempts. Default: false.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub accept_connections: Option<bool>,
/// Announcement beacon interval in seconds. Default: 30.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub beacon_interval_secs: Option<u64>,
}
impl EthernetConfig {
/// Get the EtherType, using default if not configured.
pub fn ethertype(&self) -> u16 {
self.ethertype.unwrap_or(DEFAULT_ETHERNET_ETHERTYPE)
}
/// Get the receive buffer size, using default if not configured.
pub fn recv_buf_size(&self) -> usize {
self.recv_buf_size.unwrap_or(DEFAULT_ETHERNET_RECV_BUF)
}
/// Get the send buffer size, using default if not configured.
pub fn send_buf_size(&self) -> usize {
self.send_buf_size.unwrap_or(DEFAULT_ETHERNET_SEND_BUF)
}
/// Whether to listen for discovery beacons. Default: true.
pub fn discovery(&self) -> bool {
self.discovery.unwrap_or(true)
}
/// Whether to broadcast announcement beacons. Default: false.
pub fn announce(&self) -> bool {
self.announce.unwrap_or(false)
}
/// Whether to auto-connect to discovered peers. Default: false.
pub fn auto_connect(&self) -> bool {
self.auto_connect.unwrap_or(false)
}
/// Whether to accept incoming connections. Default: false.
pub fn accept_connections(&self) -> bool {
self.accept_connections.unwrap_or(false)
}
/// Get the beacon interval, clamped to minimum. Default: 30s.
pub fn beacon_interval_secs(&self) -> u64 {
self.beacon_interval_secs
.unwrap_or(DEFAULT_BEACON_INTERVAL_SECS)
.max(MIN_BEACON_INTERVAL_SECS)
}
}
/// Transports configuration section.
///
/// Each transport type can have either a single instance (config directly
@@ -141,12 +248,9 @@ pub struct TransportsConfig {
#[serde(default, skip_serializing_if = "is_transport_empty")]
pub udp: TransportInstances<UdpConfig>,
// Future transport types:
// #[serde(default, skip_serializing_if = "is_transport_empty")]
// pub tcp: TransportInstances<TcpConfig>,
//
// #[serde(default, skip_serializing_if = "is_transport_empty")]
// pub tor: TransportInstances<TorConfig>,
/// Ethernet transport instances.
#[serde(default, skip_serializing_if = "is_transport_empty")]
pub ethernet: TransportInstances<EthernetConfig>,
}
/// Helper for skip_serializing_if on TransportInstances.
@@ -157,9 +261,7 @@ fn is_transport_empty<T>(instances: &TransportInstances<T>) -> bool {
impl TransportsConfig {
/// Check if any transports are configured.
pub fn is_empty(&self) -> bool {
self.udp.is_empty()
// && self.tcp.is_empty()
// && self.tor.is_empty()
self.udp.is_empty() && self.ethernet.is_empty()
}
/// Merge another TransportsConfig into this one.
@@ -169,6 +271,8 @@ impl TransportsConfig {
if !other.udp.is_empty() {
self.udp = other.udp;
}
// Future: same for tcp, tor, etc.
if !other.ethernet.is_empty() {
self.ethernet = other.ethernet;
}
}
}