mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-09 08:14:42 +00:00
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:
+1
-1
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,14 @@ impl Node {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this transport accepts inbound connections
|
||||
if let Some(transport) = self.transports.get(&packet.transport_id)
|
||||
&& !transport.accept_connections()
|
||||
{
|
||||
self.msg1_rate_limiter.complete_handshake();
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse header
|
||||
let header = match Msg1Header::parse(&packet.data) {
|
||||
Some(h) => h,
|
||||
|
||||
@@ -117,6 +117,7 @@ impl Node {
|
||||
self.check_session_mmp_reports().await;
|
||||
self.check_link_heartbeats().await;
|
||||
self.purge_stale_lookups(now_ms);
|
||||
self.poll_transport_discovery().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+218
-122
@@ -3,7 +3,7 @@
|
||||
use super::{Node, NodeError, NodeState};
|
||||
use crate::peer::PeerConnection;
|
||||
use crate::protocol::{Disconnect, DisconnectReason};
|
||||
use crate::transport::{packet_channel, Link, LinkDirection, TransportAddr};
|
||||
use crate::transport::{packet_channel, Link, LinkDirection, TransportAddr, TransportId};
|
||||
use crate::upper::tun::{run_tun_reader, shutdown_tun_interface, TunDevice, TunState};
|
||||
use crate::node::wire::build_msg1;
|
||||
use crate::{NodeAddr, PeerIdentity};
|
||||
@@ -89,133 +89,49 @@ impl Node {
|
||||
|
||||
// Try addresses in priority order until one works
|
||||
for addr in peer_config.addresses_by_priority() {
|
||||
// Find a transport matching this address type
|
||||
let transport_id = match self.find_transport_for_type(&addr.transport) {
|
||||
Some(id) => id,
|
||||
None => {
|
||||
debug!(
|
||||
transport = %addr.transport,
|
||||
addr = %addr.addr,
|
||||
"No operational transport for address type"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Allocate link ID and create link
|
||||
let link_id = self.allocate_link_id();
|
||||
let remote_addr = TransportAddr::from_string(&addr.addr);
|
||||
|
||||
// For UDP, links are immediately "connected" (connectionless)
|
||||
// TODO: For connection-oriented transports, state would be Connecting
|
||||
let link = Link::connectionless(
|
||||
link_id,
|
||||
transport_id,
|
||||
remote_addr.clone(),
|
||||
LinkDirection::Outbound,
|
||||
Duration::from_millis(self.config.node.base_rtt_ms),
|
||||
);
|
||||
|
||||
self.links.insert(link_id, link);
|
||||
|
||||
// Add reverse lookup for packet dispatch
|
||||
self.addr_to_link
|
||||
.insert((transport_id, remote_addr.clone()), link_id);
|
||||
|
||||
// Create connection in handshake phase (outbound knows expected identity)
|
||||
let current_time_ms = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_millis() as u64)
|
||||
.unwrap_or(0);
|
||||
let mut connection = PeerConnection::outbound(link_id, peer_identity, current_time_ms);
|
||||
|
||||
// Allocate a session index for this handshake
|
||||
let our_index = match self.index_allocator.allocate() {
|
||||
Ok(idx) => idx,
|
||||
Err(e) => {
|
||||
warn!(
|
||||
npub = %peer_config.npub,
|
||||
error = %e,
|
||||
"Failed to allocate session index"
|
||||
);
|
||||
// Clean up the link we just created
|
||||
self.links.remove(&link_id);
|
||||
self.addr_to_link.remove(&(transport_id, remote_addr));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Start the Noise handshake and get message 1
|
||||
let our_keypair = self.identity.keypair();
|
||||
let noise_msg1 = match connection.start_handshake(our_keypair, self.startup_epoch, current_time_ms) {
|
||||
Ok(msg) => msg,
|
||||
Err(e) => {
|
||||
warn!(
|
||||
npub = %peer_config.npub,
|
||||
error = %e,
|
||||
"Failed to start handshake"
|
||||
);
|
||||
// Clean up the index and link
|
||||
let _ = self.index_allocator.free(our_index);
|
||||
self.links.remove(&link_id);
|
||||
self.addr_to_link.remove(&(transport_id, remote_addr));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Set index and transport info on the connection
|
||||
connection.set_our_index(our_index);
|
||||
connection.set_transport_id(transport_id);
|
||||
connection.set_source_addr(remote_addr.clone());
|
||||
|
||||
// Build wire format msg1: [0x01][sender_idx:4 LE][noise_msg1:82]
|
||||
let wire_msg1 = build_msg1(our_index, &noise_msg1);
|
||||
|
||||
debug!(
|
||||
peer = %self.peer_display_name(&peer_node_addr),
|
||||
transport = %addr.transport,
|
||||
addr = %addr.addr,
|
||||
link_id = %link_id,
|
||||
our_index = %our_index,
|
||||
"Peer connection initiated"
|
||||
);
|
||||
|
||||
// Store msg1 for resend and schedule first resend
|
||||
let resend_interval = self.config.node.rate_limit.handshake_resend_interval_ms;
|
||||
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.connections.insert(link_id, connection);
|
||||
|
||||
// Send the wire format handshake message
|
||||
if let Some(transport) = self.transports.get(&transport_id) {
|
||||
match transport.send(&remote_addr, &wire_msg1).await {
|
||||
Ok(bytes) => {
|
||||
debug!(
|
||||
link_id = %link_id,
|
||||
our_index = %our_index,
|
||||
bytes,
|
||||
"Sent Noise handshake message 1 (wire format)"
|
||||
);
|
||||
}
|
||||
// For Ethernet addresses ("interface/mac"), find the transport
|
||||
// instance matching the interface name and parse the MAC.
|
||||
let (transport_id, remote_addr) = if addr.transport == "ethernet" {
|
||||
match self.resolve_ethernet_addr(&addr.addr) {
|
||||
Ok(result) => result,
|
||||
Err(e) => {
|
||||
warn!(
|
||||
link_id = %link_id,
|
||||
debug!(
|
||||
transport = %addr.transport,
|
||||
addr = %addr.addr,
|
||||
error = %e,
|
||||
"Failed to send handshake message"
|
||||
"Failed to resolve Ethernet address"
|
||||
);
|
||||
// Mark connection as failed but don't remove it yet
|
||||
// The event loop can handle retry logic
|
||||
if let Some(conn) = self.connections.get_mut(&link_id) {
|
||||
conn.mark_failed();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Find a transport matching this address type
|
||||
let tid = match self.find_transport_for_type(&addr.transport) {
|
||||
Some(id) => id,
|
||||
None => {
|
||||
debug!(
|
||||
transport = %addr.transport,
|
||||
addr = %addr.addr,
|
||||
"No operational transport for address type"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
(tid, TransportAddr::from_string(&addr.addr))
|
||||
};
|
||||
|
||||
match self.initiate_connection(transport_id, remote_addr, peer_identity).await {
|
||||
Ok(()) => return Ok(()),
|
||||
Err(e) => {
|
||||
debug!(
|
||||
npub = %peer_config.npub,
|
||||
transport_id = %transport_id,
|
||||
error = %e,
|
||||
"Connection attempt failed, trying next address"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Successfully initiated connection via this address
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// No address worked
|
||||
@@ -225,6 +141,186 @@ impl Node {
|
||||
)))
|
||||
}
|
||||
|
||||
/// Initiate a connection to a peer on a specific transport and address.
|
||||
///
|
||||
/// Allocates a link, starts the Noise IK handshake, sends msg1, and
|
||||
/// registers the connection for msg2 dispatch. Used by both static peer
|
||||
/// config and transport discovery auto-connect paths.
|
||||
pub(super) async fn initiate_connection(
|
||||
&mut self,
|
||||
transport_id: TransportId,
|
||||
remote_addr: TransportAddr,
|
||||
peer_identity: PeerIdentity,
|
||||
) -> Result<(), NodeError> {
|
||||
let peer_node_addr = *peer_identity.node_addr();
|
||||
|
||||
// Allocate link ID and create link
|
||||
let link_id = self.allocate_link_id();
|
||||
|
||||
let link = Link::connectionless(
|
||||
link_id,
|
||||
transport_id,
|
||||
remote_addr.clone(),
|
||||
LinkDirection::Outbound,
|
||||
Duration::from_millis(self.config.node.base_rtt_ms),
|
||||
);
|
||||
|
||||
self.links.insert(link_id, link);
|
||||
|
||||
// Add reverse lookup for packet dispatch
|
||||
self.addr_to_link
|
||||
.insert((transport_id, remote_addr.clone()), link_id);
|
||||
|
||||
// Create connection in handshake phase (outbound knows expected identity)
|
||||
let current_time_ms = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_millis() as u64)
|
||||
.unwrap_or(0);
|
||||
let mut connection = PeerConnection::outbound(link_id, peer_identity, current_time_ms);
|
||||
|
||||
// Allocate a session index for this handshake
|
||||
let our_index = match self.index_allocator.allocate() {
|
||||
Ok(idx) => idx,
|
||||
Err(e) => {
|
||||
// Clean up the link we just created
|
||||
self.links.remove(&link_id);
|
||||
self.addr_to_link.remove(&(transport_id, remote_addr));
|
||||
return Err(NodeError::IndexAllocationFailed(e.to_string()));
|
||||
}
|
||||
};
|
||||
|
||||
// Start the Noise handshake and get message 1
|
||||
let our_keypair = self.identity.keypair();
|
||||
let noise_msg1 = match connection.start_handshake(our_keypair, self.startup_epoch, current_time_ms) {
|
||||
Ok(msg) => msg,
|
||||
Err(e) => {
|
||||
// Clean up the index and link
|
||||
let _ = self.index_allocator.free(our_index);
|
||||
self.links.remove(&link_id);
|
||||
self.addr_to_link.remove(&(transport_id, remote_addr));
|
||||
return Err(NodeError::HandshakeFailed(e.to_string()));
|
||||
}
|
||||
};
|
||||
|
||||
// Set index and transport info on the connection
|
||||
connection.set_our_index(our_index);
|
||||
connection.set_transport_id(transport_id);
|
||||
connection.set_source_addr(remote_addr.clone());
|
||||
|
||||
// Build wire format msg1: [0x01][sender_idx:4 LE][noise_msg1:82]
|
||||
let wire_msg1 = build_msg1(our_index, &noise_msg1);
|
||||
|
||||
debug!(
|
||||
peer = %self.peer_display_name(&peer_node_addr),
|
||||
transport_id = %transport_id,
|
||||
remote_addr = %remote_addr,
|
||||
link_id = %link_id,
|
||||
our_index = %our_index,
|
||||
"Connection initiated"
|
||||
);
|
||||
|
||||
// Store msg1 for resend and schedule first resend
|
||||
let resend_interval = self.config.node.rate_limit.handshake_resend_interval_ms;
|
||||
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.connections.insert(link_id, connection);
|
||||
|
||||
// Send the wire format handshake message
|
||||
if let Some(transport) = self.transports.get(&transport_id) {
|
||||
match transport.send(&remote_addr, &wire_msg1).await {
|
||||
Ok(bytes) => {
|
||||
debug!(
|
||||
link_id = %link_id,
|
||||
our_index = %our_index,
|
||||
bytes,
|
||||
"Sent Noise handshake message 1 (wire format)"
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(
|
||||
link_id = %link_id,
|
||||
error = %e,
|
||||
"Failed to send handshake message"
|
||||
);
|
||||
// Mark connection as failed but don't remove it yet
|
||||
// The event loop can handle retry logic
|
||||
if let Some(conn) = self.connections.get_mut(&link_id) {
|
||||
conn.mark_failed();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Poll all transports for discovered peers and auto-connect.
|
||||
///
|
||||
/// Called from the tick handler. Iterates operational transports,
|
||||
/// drains their discovery buffers, and initiates connections to
|
||||
/// newly discovered peers (if auto_connect is enabled).
|
||||
pub(super) async fn poll_transport_discovery(&mut self) {
|
||||
// Collect discoveries first to avoid borrow conflict with self
|
||||
let mut to_connect = Vec::new();
|
||||
|
||||
for (transport_id, transport) in &self.transports {
|
||||
if !transport.is_operational() {
|
||||
continue;
|
||||
}
|
||||
if !transport.auto_connect() {
|
||||
// Still drain the buffer so it doesn't grow unbounded
|
||||
let _ = transport.discover();
|
||||
continue;
|
||||
}
|
||||
let discovered = match transport.discover() {
|
||||
Ok(peers) => peers,
|
||||
Err(_) => continue,
|
||||
};
|
||||
for peer in discovered {
|
||||
let pubkey = match peer.pubkey_hint {
|
||||
Some(pk) => pk,
|
||||
None => continue,
|
||||
};
|
||||
let identity = PeerIdentity::from_pubkey(pubkey);
|
||||
let node_addr = *identity.node_addr();
|
||||
|
||||
// Skip self
|
||||
if node_addr == *self.identity.node_addr() {
|
||||
continue;
|
||||
}
|
||||
// Skip if already connected
|
||||
if self.peers.contains_key(&node_addr) {
|
||||
continue;
|
||||
}
|
||||
// Skip if connection already in progress
|
||||
let connecting = self.connections.values().any(|c| {
|
||||
c.expected_identity()
|
||||
.map(|id| id.node_addr() == &node_addr)
|
||||
.unwrap_or(false)
|
||||
});
|
||||
if connecting {
|
||||
continue;
|
||||
}
|
||||
|
||||
to_connect.push((*transport_id, peer.addr, identity));
|
||||
}
|
||||
}
|
||||
|
||||
for (transport_id, remote_addr, identity) in to_connect {
|
||||
info!(
|
||||
peer = %self.peer_display_name(identity.node_addr()),
|
||||
transport_id = %transport_id,
|
||||
remote_addr = %remote_addr,
|
||||
"Auto-connecting to discovered peer"
|
||||
);
|
||||
if let Err(e) = self.initiate_connection(transport_id, remote_addr, identity).await {
|
||||
warn!(error = %e, "Failed to auto-connect to discovered peer");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === State Transitions ===
|
||||
|
||||
/// Start the node.
|
||||
|
||||
+92
-12
@@ -28,6 +28,8 @@ use crate::transport::{
|
||||
Link, LinkId, PacketRx, PacketTx, TransportAddr, TransportError, TransportHandle, TransportId,
|
||||
};
|
||||
use crate::transport::udp::UdpTransport;
|
||||
#[cfg(target_os = "linux")]
|
||||
use crate::transport::ethernet::EthernetTransport;
|
||||
use crate::tree::TreeState;
|
||||
use crate::upper::icmp_rate_limit::IcmpRateLimiter;
|
||||
use crate::upper::tun::{TunError, TunOutboundRx, TunState, TunTx};
|
||||
@@ -107,6 +109,12 @@ pub enum NodeError {
|
||||
|
||||
#[error("TUN error: {0}")]
|
||||
Tun(#[from] TunError),
|
||||
|
||||
#[error("index allocation failed: {0}")]
|
||||
IndexAllocationFailed(String),
|
||||
|
||||
#[error("handshake failed: {0}")]
|
||||
HandshakeFailed(String),
|
||||
}
|
||||
|
||||
/// Node operational state.
|
||||
@@ -570,8 +578,25 @@ impl Node {
|
||||
transports.push(TransportHandle::Udp(udp));
|
||||
}
|
||||
|
||||
// Future transports follow same pattern:
|
||||
// for (name, tcp_config) in self.config.transports.tcp.iter() { ... }
|
||||
// Create Ethernet transport instances
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let eth_instances: Vec<_> = self
|
||||
.config
|
||||
.transports
|
||||
.ethernet
|
||||
.iter()
|
||||
.map(|(name, config)| (name.map(|s| s.to_string()), config.clone()))
|
||||
.collect();
|
||||
|
||||
let xonly = self.identity.pubkey();
|
||||
for (name, eth_config) in eth_instances {
|
||||
let transport_id = self.allocate_transport_id();
|
||||
let mut eth = EthernetTransport::new(transport_id, name, eth_config, packet_tx.clone());
|
||||
eth.set_local_pubkey(xonly);
|
||||
transports.push(TransportHandle::Ethernet(eth));
|
||||
}
|
||||
}
|
||||
|
||||
transports
|
||||
}
|
||||
@@ -586,6 +611,55 @@ impl Node {
|
||||
.map(|(id, _)| *id)
|
||||
}
|
||||
|
||||
/// Resolve an Ethernet peer address ("interface/mac") to a transport ID
|
||||
/// and binary TransportAddr.
|
||||
///
|
||||
/// Finds the Ethernet transport instance bound to the named interface
|
||||
/// and parses the MAC portion into a 6-byte TransportAddr.
|
||||
fn resolve_ethernet_addr(
|
||||
&self,
|
||||
addr_str: &str,
|
||||
) -> Result<(TransportId, TransportAddr), NodeError> {
|
||||
let (iface, mac_str) = addr_str.split_once('/').ok_or_else(|| {
|
||||
NodeError::NoTransportForType(format!(
|
||||
"invalid Ethernet address format '{}': expected 'interface/mac'",
|
||||
addr_str
|
||||
))
|
||||
})?;
|
||||
|
||||
// Find the Ethernet transport bound to this interface
|
||||
let transport_id = self
|
||||
.transports
|
||||
.iter()
|
||||
.find(|(_, handle)| {
|
||||
handle.transport_type().name == "ethernet"
|
||||
&& handle.is_operational()
|
||||
&& handle.interface_name() == Some(iface)
|
||||
})
|
||||
.map(|(id, _)| *id)
|
||||
.ok_or_else(|| {
|
||||
NodeError::NoTransportForType(format!(
|
||||
"no operational Ethernet transport for interface '{}'",
|
||||
iface
|
||||
))
|
||||
})?;
|
||||
|
||||
// Parse the MAC address
|
||||
#[cfg(target_os = "linux")]
|
||||
let mac = crate::transport::ethernet::parse_mac_string(mac_str).map_err(|e| {
|
||||
NodeError::NoTransportForType(format!("invalid MAC in '{}': {}", addr_str, e))
|
||||
})?;
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let mac: [u8; 6] = {
|
||||
let _ = mac_str;
|
||||
return Err(NodeError::NoTransportForType(
|
||||
"Ethernet transport not available on this platform".into(),
|
||||
));
|
||||
};
|
||||
|
||||
Ok((transport_id, TransportAddr::from_bytes(&mac)))
|
||||
}
|
||||
|
||||
// === Identity Accessors ===
|
||||
|
||||
/// Get this node's identity.
|
||||
@@ -640,18 +714,24 @@ impl Node {
|
||||
crate::upper::icmp::effective_ipv6_mtu(self.transport_mtu())
|
||||
}
|
||||
|
||||
/// Get the transport MTU from configuration.
|
||||
/// Get the transport MTU for a specific transport.
|
||||
///
|
||||
/// Returns the MTU of the first configured UDP transport, or 1280
|
||||
/// (IPv6 minimum) as fallback.
|
||||
/// When called without a specific transport context, returns the MTU
|
||||
/// of the first operational transport, or 1280 (IPv6 minimum) as
|
||||
/// fallback. This is used for initial TUN configuration where a
|
||||
/// specific transport isn't yet known.
|
||||
pub fn transport_mtu(&self) -> u16 {
|
||||
self.config
|
||||
.transports
|
||||
.udp
|
||||
.iter()
|
||||
.next()
|
||||
.map(|(_, cfg)| cfg.mtu())
|
||||
.unwrap_or(1280)
|
||||
// Prefer the MTU from the first operational transport
|
||||
for handle in self.transports.values() {
|
||||
if handle.is_operational() {
|
||||
return handle.mtu();
|
||||
}
|
||||
}
|
||||
// Fallback to config: try UDP first, then Ethernet
|
||||
if let Some((_, cfg)) = self.config.transports.udp.iter().next() {
|
||||
return cfg.mtu();
|
||||
}
|
||||
1280
|
||||
}
|
||||
|
||||
// === State ===
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
//! Ethernet transport integration tests.
|
||||
//!
|
||||
//! Tests that the Ethernet transport works end-to-end using veth pairs.
|
||||
//! All tests require root or CAP_NET_RAW and are marked `#[ignore]`.
|
||||
|
||||
use super::*;
|
||||
use crate::config::EthernetConfig;
|
||||
use crate::transport::ethernet::EthernetTransport;
|
||||
use crate::transport::{packet_channel, TransportAddr, TransportHandle, TransportId};
|
||||
use spanning_tree::{cleanup_nodes, drain_all_packets, initiate_handshake, TestNode};
|
||||
|
||||
use std::process::Command;
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
|
||||
/// Atomic counter for unique veth names across tests.
|
||||
static VETH_COUNTER: AtomicU32 = AtomicU32::new(0);
|
||||
|
||||
/// RAII wrapper for a veth pair.
|
||||
///
|
||||
/// Creates a pair of connected virtual Ethernet interfaces. Destroying
|
||||
/// one end automatically destroys the other.
|
||||
struct VethPair {
|
||||
name_a: String,
|
||||
name_b: String,
|
||||
}
|
||||
|
||||
impl VethPair {
|
||||
/// Create a new veth pair with unique interface names.
|
||||
///
|
||||
/// Names are kept under 15 chars (IFNAMSIZ limit). Format: `ftXXa`/`ftXXb`
|
||||
/// where XX is an atomic counter combined with PID for cross-process uniqueness.
|
||||
fn create() -> Self {
|
||||
let id = VETH_COUNTER.fetch_add(1, Ordering::Relaxed);
|
||||
let pid = std::process::id() % 10000;
|
||||
let name_a = format!("ft{}{}a", pid, id);
|
||||
let name_b = format!("ft{}{}b", pid, id);
|
||||
|
||||
assert!(name_a.len() <= 15, "veth name too long: {}", name_a);
|
||||
assert!(name_b.len() <= 15, "veth name too long: {}", name_b);
|
||||
|
||||
// Create veth pair
|
||||
let status = Command::new("ip")
|
||||
.args(["link", "add", &name_a, "type", "veth", "peer", "name", &name_b])
|
||||
.status()
|
||||
.expect("failed to run 'ip link add'");
|
||||
assert!(status.success(), "failed to create veth pair");
|
||||
|
||||
// Bring both ends up
|
||||
let status = Command::new("ip")
|
||||
.args(["link", "set", &name_a, "up"])
|
||||
.status()
|
||||
.expect("failed to run 'ip link set up'");
|
||||
assert!(status.success(), "failed to bring up {}", name_a);
|
||||
|
||||
let status = Command::new("ip")
|
||||
.args(["link", "set", &name_b, "up"])
|
||||
.status()
|
||||
.expect("failed to run 'ip link set up'");
|
||||
assert!(status.success(), "failed to bring up {}", name_b);
|
||||
|
||||
VethPair { name_a, name_b }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for VethPair {
|
||||
fn drop(&mut self) {
|
||||
// Deleting one end destroys both
|
||||
let _ = Command::new("ip")
|
||||
.args(["link", "delete", &self.name_a])
|
||||
.status();
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a test node with a live Ethernet transport on the given interface.
|
||||
///
|
||||
/// Parallel to `make_test_node()` in spanning_tree.rs but uses
|
||||
/// EthernetTransport instead of UDP.
|
||||
async fn make_test_node_ethernet(interface: &str) -> TestNode {
|
||||
let mut node = make_node();
|
||||
let transport_id = TransportId::new(1);
|
||||
|
||||
let config = EthernetConfig {
|
||||
interface: interface.to_string(),
|
||||
discovery: Some(false),
|
||||
announce: Some(false),
|
||||
accept_connections: Some(true),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let (packet_tx, packet_rx) = packet_channel(256);
|
||||
let mut transport = EthernetTransport::new(transport_id, None, config, packet_tx);
|
||||
transport.start_async().await.unwrap();
|
||||
|
||||
let mac = transport.local_mac().expect("transport should have MAC after start");
|
||||
let addr = TransportAddr::from_bytes(&mac);
|
||||
|
||||
node.transports
|
||||
.insert(transport_id, TransportHandle::Ethernet(transport));
|
||||
|
||||
TestNode {
|
||||
node,
|
||||
transport_id,
|
||||
packet_rx,
|
||||
addr,
|
||||
}
|
||||
}
|
||||
|
||||
/// Two nodes on a veth pair complete a Noise handshake and establish peering.
|
||||
#[tokio::test]
|
||||
#[ignore] // Requires root or CAP_NET_RAW
|
||||
async fn test_ethernet_two_node_handshake() {
|
||||
let veth = VethPair::create();
|
||||
|
||||
let mut nodes = vec![
|
||||
make_test_node_ethernet(&veth.name_a).await,
|
||||
make_test_node_ethernet(&veth.name_b).await,
|
||||
];
|
||||
|
||||
// Initiate handshake from node 0 to node 1
|
||||
initiate_handshake(&mut nodes, 0, 1).await;
|
||||
|
||||
// Drain all packets (handshake + tree announce)
|
||||
let total = drain_all_packets(&mut nodes, false).await;
|
||||
assert!(total > 0, "should have processed packets");
|
||||
|
||||
// Verify bidirectional peering
|
||||
let addr_0 = *nodes[0].node.node_addr();
|
||||
let addr_1 = *nodes[1].node.node_addr();
|
||||
assert!(
|
||||
nodes[0].node.get_peer(&addr_1).is_some(),
|
||||
"node 0 should have node 1 as peer"
|
||||
);
|
||||
assert!(
|
||||
nodes[1].node.get_peer(&addr_0).is_some(),
|
||||
"node 1 should have node 0 as peer"
|
||||
);
|
||||
|
||||
cleanup_nodes(&mut nodes).await;
|
||||
}
|
||||
|
||||
/// Two Ethernet nodes converge to a correct spanning tree (2-node tree).
|
||||
#[tokio::test]
|
||||
#[ignore] // Requires root or CAP_NET_RAW
|
||||
async fn test_ethernet_data_exchange() {
|
||||
use spanning_tree::verify_tree_convergence;
|
||||
|
||||
let veth = VethPair::create();
|
||||
|
||||
let mut nodes = vec![
|
||||
make_test_node_ethernet(&veth.name_a).await,
|
||||
make_test_node_ethernet(&veth.name_b).await,
|
||||
];
|
||||
|
||||
initiate_handshake(&mut nodes, 0, 1).await;
|
||||
let total = drain_all_packets(&mut nodes, false).await;
|
||||
assert!(total > 0);
|
||||
|
||||
// Verify spanning tree convergence
|
||||
verify_tree_convergence(&nodes);
|
||||
|
||||
// The root should be the node with the smallest NodeAddr
|
||||
let expected_root = std::cmp::min(*nodes[0].node.node_addr(), *nodes[1].node.node_addr());
|
||||
assert_eq!(*nodes[0].node.tree_state().root(), expected_root);
|
||||
assert_eq!(*nodes[1].node.tree_state().root(), expected_root);
|
||||
|
||||
cleanup_nodes(&mut nodes).await;
|
||||
}
|
||||
|
||||
/// Mixed transport: 2 Ethernet nodes + 2 UDP nodes coexist.
|
||||
///
|
||||
/// Each transport forms its own connected component. Validates that
|
||||
/// `process_available_packets()` handles heterogeneous transport types.
|
||||
#[tokio::test]
|
||||
#[ignore] // Requires root or CAP_NET_RAW
|
||||
async fn test_mixed_transport_coexistence() {
|
||||
use spanning_tree::{make_test_node, verify_tree_convergence_components};
|
||||
|
||||
let veth = VethPair::create();
|
||||
|
||||
// Create 2 Ethernet nodes and 2 UDP nodes
|
||||
let eth_0 = make_test_node_ethernet(&veth.name_a).await;
|
||||
let eth_1 = make_test_node_ethernet(&veth.name_b).await;
|
||||
let udp_0 = make_test_node().await;
|
||||
let udp_1 = make_test_node().await;
|
||||
|
||||
let mut nodes = vec![eth_0, eth_1, udp_0, udp_1];
|
||||
|
||||
// Handshake within each component
|
||||
initiate_handshake(&mut nodes, 0, 1).await; // Ethernet pair
|
||||
initiate_handshake(&mut nodes, 2, 3).await; // UDP pair
|
||||
|
||||
// Drain all packets across both transports
|
||||
let total = drain_all_packets(&mut nodes, false).await;
|
||||
assert!(total > 0);
|
||||
|
||||
// Verify each component converges independently
|
||||
verify_tree_convergence_components(&nodes, &[vec![0, 1], vec![2, 3]]);
|
||||
|
||||
// Ethernet component has its own root
|
||||
let eth_root = std::cmp::min(*nodes[0].node.node_addr(), *nodes[1].node.node_addr());
|
||||
assert_eq!(*nodes[0].node.tree_state().root(), eth_root);
|
||||
assert_eq!(*nodes[1].node.tree_state().root(), eth_root);
|
||||
|
||||
// UDP component has its own root
|
||||
let udp_root = std::cmp::min(*nodes[2].node.node_addr(), *nodes[3].node.node_addr());
|
||||
assert_eq!(*nodes[2].node.tree_state().root(), udp_root);
|
||||
assert_eq!(*nodes[3].node.tree_state().root(), udp_root);
|
||||
|
||||
cleanup_nodes(&mut nodes).await;
|
||||
}
|
||||
@@ -7,6 +7,8 @@ use std::time::Duration;
|
||||
mod bloom;
|
||||
mod disconnect;
|
||||
mod discovery;
|
||||
#[cfg(target_os = "linux")]
|
||||
mod ethernet;
|
||||
mod forwarding;
|
||||
mod handshake;
|
||||
mod routing;
|
||||
|
||||
+4
-4
@@ -302,10 +302,10 @@ impl Node {
|
||||
let now = std::time::Instant::now();
|
||||
let interval = std::time::Duration::from_secs(interval_secs);
|
||||
|
||||
if let Some(last) = self.last_parent_reeval {
|
||||
if now.duration_since(last) < interval {
|
||||
return;
|
||||
}
|
||||
if let Some(last) = self.last_parent_reeval
|
||||
&& now.duration_since(last) < interval
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
self.last_parent_reeval = Some(now);
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
//! Ethernet LAN discovery via broadcast beacons.
|
||||
//!
|
||||
//! Beacon format (34 bytes total):
|
||||
//! - `0x01` (1 byte): frame type = discovery announcement
|
||||
//! - `0x01` (1 byte): discovery protocol version
|
||||
//! - x-only public key (32 bytes): node's Nostr identity
|
||||
|
||||
use crate::transport::{DiscoveredPeer, TransportAddr, TransportId};
|
||||
use secp256k1::XOnlyPublicKey;
|
||||
use std::sync::Mutex;
|
||||
|
||||
/// Discovery protocol version.
|
||||
pub const DISCOVERY_VERSION: u8 = 0x01;
|
||||
|
||||
/// Frame type prefix for discovery announcement beacons.
|
||||
pub const FRAME_TYPE_BEACON: u8 = 0x01;
|
||||
|
||||
/// Frame type prefix for FIPS data frames.
|
||||
pub const FRAME_TYPE_DATA: u8 = 0x00;
|
||||
|
||||
/// Total beacon payload size: type(1) + version(1) + pubkey(32).
|
||||
pub const BEACON_SIZE: usize = 34;
|
||||
|
||||
/// Build a discovery announcement beacon payload.
|
||||
pub fn build_beacon(pubkey: &XOnlyPublicKey) -> [u8; BEACON_SIZE] {
|
||||
let mut buf = [0u8; BEACON_SIZE];
|
||||
buf[0] = FRAME_TYPE_BEACON;
|
||||
buf[1] = DISCOVERY_VERSION;
|
||||
buf[2..BEACON_SIZE].copy_from_slice(&pubkey.serialize());
|
||||
buf
|
||||
}
|
||||
|
||||
/// Parse a discovery announcement beacon payload.
|
||||
///
|
||||
/// Returns the sender's public key, or None if the payload is invalid.
|
||||
pub fn parse_beacon(data: &[u8]) -> Option<XOnlyPublicKey> {
|
||||
if data.len() < BEACON_SIZE {
|
||||
return None;
|
||||
}
|
||||
if data[0] != FRAME_TYPE_BEACON {
|
||||
return None;
|
||||
}
|
||||
if data[1] != DISCOVERY_VERSION {
|
||||
return None;
|
||||
}
|
||||
XOnlyPublicKey::from_slice(&data[2..34]).ok()
|
||||
}
|
||||
|
||||
/// Buffer for discovered peers, drained by `discover()`.
|
||||
pub struct DiscoveryBuffer {
|
||||
transport_id: TransportId,
|
||||
peers: Mutex<Vec<DiscoveredPeer>>,
|
||||
}
|
||||
|
||||
impl DiscoveryBuffer {
|
||||
/// Create a new empty discovery buffer.
|
||||
pub fn new(transport_id: TransportId) -> Self {
|
||||
Self {
|
||||
transport_id,
|
||||
peers: Mutex::new(Vec::new()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a discovered peer from a received beacon.
|
||||
pub fn add_peer(&self, src_mac: [u8; 6], pubkey: XOnlyPublicKey) {
|
||||
let addr = TransportAddr::from_bytes(&src_mac);
|
||||
let peer = DiscoveredPeer::with_hint(self.transport_id, addr, pubkey);
|
||||
let mut peers = self.peers.lock().unwrap();
|
||||
// Deduplicate by MAC address — keep the latest
|
||||
peers.retain(|p| p.addr.as_bytes() != src_mac);
|
||||
peers.push(peer);
|
||||
}
|
||||
|
||||
/// Drain all discovered peers since the last call.
|
||||
pub fn take(&self) -> Vec<DiscoveredPeer> {
|
||||
let mut peers = self.peers.lock().unwrap();
|
||||
std::mem::take(&mut *peers)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Tests
|
||||
// ============================================================================
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use secp256k1::{Secp256k1, SecretKey};
|
||||
|
||||
fn test_pubkey() -> XOnlyPublicKey {
|
||||
let secp = Secp256k1::new();
|
||||
let sk = SecretKey::from_slice(&[0x42; 32]).unwrap();
|
||||
let (xonly, _) = sk.public_key(&secp).x_only_public_key();
|
||||
xonly
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_parse_beacon() {
|
||||
let pubkey = test_pubkey();
|
||||
let beacon = build_beacon(&pubkey);
|
||||
|
||||
assert_eq!(beacon.len(), BEACON_SIZE);
|
||||
assert_eq!(beacon[0], FRAME_TYPE_BEACON);
|
||||
assert_eq!(beacon[1], DISCOVERY_VERSION);
|
||||
|
||||
let parsed = parse_beacon(&beacon).unwrap();
|
||||
assert_eq!(parsed, pubkey);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_beacon_too_short() {
|
||||
assert!(parse_beacon(&[0x01, 0x01]).is_none());
|
||||
assert!(parse_beacon(&[]).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_beacon_wrong_type() {
|
||||
let mut beacon = build_beacon(&test_pubkey());
|
||||
beacon[0] = 0x00; // data frame, not beacon
|
||||
assert!(parse_beacon(&beacon).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_beacon_wrong_version() {
|
||||
let mut beacon = build_beacon(&test_pubkey());
|
||||
beacon[1] = 0xFF;
|
||||
assert!(parse_beacon(&beacon).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_frame_type_prefix() {
|
||||
assert_eq!(FRAME_TYPE_DATA, 0x00);
|
||||
assert_eq!(FRAME_TYPE_BEACON, 0x01);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_discovery_buffer() {
|
||||
let buffer = DiscoveryBuffer::new(TransportId::new(1));
|
||||
let pubkey = test_pubkey();
|
||||
let mac = [0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff];
|
||||
|
||||
buffer.add_peer(mac, pubkey);
|
||||
|
||||
let peers = buffer.take();
|
||||
assert_eq!(peers.len(), 1);
|
||||
assert_eq!(peers[0].addr.as_bytes(), &mac);
|
||||
assert_eq!(peers[0].pubkey_hint, Some(pubkey));
|
||||
|
||||
// Second take should be empty
|
||||
let peers = buffer.take();
|
||||
assert!(peers.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_discovery_buffer_dedup() {
|
||||
let buffer = DiscoveryBuffer::new(TransportId::new(1));
|
||||
let pubkey = test_pubkey();
|
||||
let mac = [0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff];
|
||||
|
||||
buffer.add_peer(mac, pubkey);
|
||||
buffer.add_peer(mac, pubkey); // same MAC again
|
||||
|
||||
let peers = buffer.take();
|
||||
assert_eq!(peers.len(), 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,629 @@
|
||||
//! Ethernet Transport Implementation
|
||||
//!
|
||||
//! Provides raw Ethernet transport for FIPS peer communication using
|
||||
//! AF_PACKET sockets with SOCK_DGRAM. Works on wired Ethernet and WiFi
|
||||
//! interfaces (kernel mac80211 abstracts 802.11 transparently).
|
||||
|
||||
pub mod discovery;
|
||||
pub mod socket;
|
||||
pub mod stats;
|
||||
|
||||
use super::{
|
||||
DiscoveredPeer, PacketTx, ReceivedPacket, Transport, TransportAddr, TransportError,
|
||||
TransportId, TransportState, TransportType,
|
||||
};
|
||||
use crate::config::EthernetConfig;
|
||||
use discovery::{
|
||||
build_beacon, parse_beacon, DiscoveryBuffer, FRAME_TYPE_BEACON, FRAME_TYPE_DATA,
|
||||
};
|
||||
use socket::{AsyncPacketSocket, PacketSocket, ETHERNET_BROADCAST};
|
||||
use stats::EthernetStats;
|
||||
|
||||
use secp256k1::XOnlyPublicKey;
|
||||
use std::sync::Arc;
|
||||
use tokio::task::JoinHandle;
|
||||
use tracing::{debug, info, trace, warn};
|
||||
|
||||
/// Ethernet transport for FIPS.
|
||||
///
|
||||
/// Uses AF_PACKET with SOCK_DGRAM for raw Ethernet frame I/O. A single
|
||||
/// socket per interface serves all peers; links are virtual tuples of
|
||||
/// (transport_id, remote_mac).
|
||||
pub struct EthernetTransport {
|
||||
/// Unique transport identifier.
|
||||
transport_id: TransportId,
|
||||
/// Optional instance name (for named instances in config).
|
||||
name: Option<String>,
|
||||
/// Configuration.
|
||||
config: EthernetConfig,
|
||||
/// Current state.
|
||||
state: TransportState,
|
||||
/// Async socket (None until started).
|
||||
socket: Option<Arc<AsyncPacketSocket>>,
|
||||
/// Channel for delivering received packets to Node.
|
||||
packet_tx: PacketTx,
|
||||
/// Receive loop task handle.
|
||||
recv_task: Option<JoinHandle<()>>,
|
||||
/// Beacon sender task handle.
|
||||
beacon_task: Option<JoinHandle<()>>,
|
||||
/// Local MAC address (after start).
|
||||
local_mac: Option<[u8; 6]>,
|
||||
/// Interface name (from config).
|
||||
interface: String,
|
||||
/// Effective MTU (interface MTU - 1 for frame type prefix).
|
||||
effective_mtu: u16,
|
||||
/// Discovery buffer for discovered peers.
|
||||
discovery_buffer: Arc<DiscoveryBuffer>,
|
||||
/// Transport-level statistics.
|
||||
stats: Arc<EthernetStats>,
|
||||
/// Node's public key for beacon construction.
|
||||
local_pubkey: Option<XOnlyPublicKey>,
|
||||
}
|
||||
|
||||
impl EthernetTransport {
|
||||
/// Create a new Ethernet transport.
|
||||
pub fn new(
|
||||
transport_id: TransportId,
|
||||
name: Option<String>,
|
||||
config: EthernetConfig,
|
||||
packet_tx: PacketTx,
|
||||
) -> Self {
|
||||
let interface = config.interface.clone();
|
||||
let discovery_buffer = Arc::new(DiscoveryBuffer::new(transport_id));
|
||||
let stats = Arc::new(EthernetStats::new());
|
||||
|
||||
Self {
|
||||
transport_id,
|
||||
name,
|
||||
config,
|
||||
state: TransportState::Configured,
|
||||
socket: None,
|
||||
packet_tx,
|
||||
recv_task: None,
|
||||
beacon_task: None,
|
||||
local_mac: None,
|
||||
interface,
|
||||
effective_mtu: 1499, // default, updated on start
|
||||
discovery_buffer,
|
||||
stats,
|
||||
local_pubkey: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the instance name (if configured as a named instance).
|
||||
pub fn name(&self) -> Option<&str> {
|
||||
self.name.as_deref()
|
||||
}
|
||||
|
||||
/// Get the interface name.
|
||||
pub fn interface_name(&self) -> &str {
|
||||
&self.interface
|
||||
}
|
||||
|
||||
/// Get the local MAC address (only valid after start).
|
||||
pub fn local_mac(&self) -> Option<[u8; 6]> {
|
||||
self.local_mac
|
||||
}
|
||||
|
||||
/// Set the node's public key for beacon construction.
|
||||
///
|
||||
/// Must be called before start if announce is enabled.
|
||||
pub fn set_local_pubkey(&mut self, pubkey: XOnlyPublicKey) {
|
||||
self.local_pubkey = Some(pubkey);
|
||||
}
|
||||
|
||||
/// Get a reference to the statistics.
|
||||
pub fn stats(&self) -> &Arc<EthernetStats> {
|
||||
&self.stats
|
||||
}
|
||||
|
||||
/// Start the transport asynchronously.
|
||||
///
|
||||
/// Creates the AF_PACKET socket, spawns the receive loop, and
|
||||
/// optionally spawns the beacon sender task.
|
||||
pub async fn start_async(&mut self) -> Result<(), TransportError> {
|
||||
if !self.state.can_start() {
|
||||
return Err(TransportError::AlreadyStarted);
|
||||
}
|
||||
|
||||
self.state = TransportState::Starting;
|
||||
|
||||
// Create and bind AF_PACKET socket
|
||||
let raw_socket = PacketSocket::open(&self.config.interface, self.config.ethertype())?;
|
||||
|
||||
// Get local MAC and MTU
|
||||
let local_mac = raw_socket.local_mac()?;
|
||||
let if_mtu = raw_socket.interface_mtu()?;
|
||||
|
||||
// Effective MTU: interface MTU minus 1 byte for frame type prefix
|
||||
let effective_mtu = if let Some(configured_mtu) = self.config.mtu {
|
||||
// Config MTU cannot exceed interface MTU - 1
|
||||
configured_mtu.min(if_mtu.saturating_sub(1))
|
||||
} else {
|
||||
if_mtu.saturating_sub(1)
|
||||
};
|
||||
self.effective_mtu = effective_mtu;
|
||||
self.local_mac = Some(local_mac);
|
||||
|
||||
// Set buffer sizes
|
||||
raw_socket.set_recv_buffer_size(self.config.recv_buf_size())?;
|
||||
raw_socket.set_send_buffer_size(self.config.send_buf_size())?;
|
||||
|
||||
// Wrap in async
|
||||
let async_socket = raw_socket.into_async()?;
|
||||
let socket = Arc::new(async_socket);
|
||||
self.socket = Some(socket.clone());
|
||||
|
||||
// Spawn receive loop
|
||||
let transport_id = self.transport_id;
|
||||
let packet_tx = self.packet_tx.clone();
|
||||
let mtu = self.effective_mtu;
|
||||
let discovery_enabled = self.config.discovery();
|
||||
let discovery_buffer = self.discovery_buffer.clone();
|
||||
let stats = self.stats.clone();
|
||||
let recv_socket = socket.clone();
|
||||
|
||||
let recv_task = tokio::spawn(async move {
|
||||
ethernet_receive_loop(
|
||||
recv_socket,
|
||||
transport_id,
|
||||
packet_tx,
|
||||
mtu,
|
||||
discovery_enabled,
|
||||
discovery_buffer,
|
||||
stats,
|
||||
)
|
||||
.await;
|
||||
});
|
||||
self.recv_task = Some(recv_task);
|
||||
|
||||
// Spawn beacon sender if announce is enabled
|
||||
if self.config.announce() {
|
||||
if let Some(pubkey) = self.local_pubkey {
|
||||
let beacon_socket = socket.clone();
|
||||
let interval_secs = self.config.beacon_interval_secs();
|
||||
let beacon_stats = self.stats.clone();
|
||||
let beacon_transport_id = self.transport_id;
|
||||
|
||||
let beacon_task = tokio::spawn(async move {
|
||||
beacon_sender_loop(
|
||||
beacon_socket,
|
||||
pubkey,
|
||||
interval_secs,
|
||||
beacon_stats,
|
||||
beacon_transport_id,
|
||||
)
|
||||
.await;
|
||||
});
|
||||
self.beacon_task = Some(beacon_task);
|
||||
} else {
|
||||
warn!(
|
||||
transport_id = %self.transport_id,
|
||||
"Announce enabled but no local pubkey set; beacons disabled"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
self.state = TransportState::Up;
|
||||
|
||||
if let Some(ref name) = self.name {
|
||||
info!(
|
||||
name = %name,
|
||||
interface = %self.interface,
|
||||
mac = %format_mac(&local_mac),
|
||||
mtu = effective_mtu,
|
||||
if_mtu = if_mtu,
|
||||
"Ethernet transport started"
|
||||
);
|
||||
} else {
|
||||
info!(
|
||||
interface = %self.interface,
|
||||
mac = %format_mac(&local_mac),
|
||||
mtu = effective_mtu,
|
||||
if_mtu = if_mtu,
|
||||
"Ethernet transport started"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stop the transport asynchronously.
|
||||
pub async fn stop_async(&mut self) -> Result<(), TransportError> {
|
||||
if !self.state.is_operational() {
|
||||
return Err(TransportError::NotStarted);
|
||||
}
|
||||
|
||||
// Abort beacon task
|
||||
if let Some(task) = self.beacon_task.take() {
|
||||
task.abort();
|
||||
let _ = task.await;
|
||||
}
|
||||
|
||||
// Abort receive task
|
||||
if let Some(task) = self.recv_task.take() {
|
||||
task.abort();
|
||||
let _ = task.await;
|
||||
}
|
||||
|
||||
// Drop socket
|
||||
self.socket.take();
|
||||
self.local_mac = None;
|
||||
|
||||
self.state = TransportState::Down;
|
||||
|
||||
info!(
|
||||
transport_id = %self.transport_id,
|
||||
interface = %self.interface,
|
||||
"Ethernet transport stopped"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send a packet asynchronously.
|
||||
///
|
||||
/// The data is prepended with a FRAME_TYPE_DATA prefix byte before
|
||||
/// transmission.
|
||||
pub async fn send_async(
|
||||
&self,
|
||||
addr: &TransportAddr,
|
||||
data: &[u8],
|
||||
) -> Result<usize, TransportError> {
|
||||
if !self.state.is_operational() {
|
||||
return Err(TransportError::NotStarted);
|
||||
}
|
||||
|
||||
if data.len() > self.effective_mtu as usize {
|
||||
return Err(TransportError::MtuExceeded {
|
||||
packet_size: data.len(),
|
||||
mtu: self.effective_mtu,
|
||||
});
|
||||
}
|
||||
|
||||
let dest_mac = parse_mac_addr(addr)?;
|
||||
let socket = self.socket.as_ref().ok_or(TransportError::NotStarted)?;
|
||||
|
||||
// Prepend frame type prefix
|
||||
let mut frame = Vec::with_capacity(1 + data.len());
|
||||
frame.push(FRAME_TYPE_DATA);
|
||||
frame.extend_from_slice(data);
|
||||
|
||||
let bytes_sent = socket.send_to(&frame, &dest_mac).await?;
|
||||
self.stats.record_send(bytes_sent);
|
||||
|
||||
trace!(
|
||||
transport_id = %self.transport_id,
|
||||
remote_mac = %format_mac(&dest_mac),
|
||||
bytes = bytes_sent,
|
||||
"Ethernet frame sent"
|
||||
);
|
||||
|
||||
// Return the data bytes sent (excluding frame type prefix)
|
||||
Ok(bytes_sent.saturating_sub(1))
|
||||
}
|
||||
}
|
||||
|
||||
impl Transport for EthernetTransport {
|
||||
fn transport_id(&self) -> TransportId {
|
||||
self.transport_id
|
||||
}
|
||||
|
||||
fn transport_type(&self) -> &TransportType {
|
||||
&TransportType::ETHERNET
|
||||
}
|
||||
|
||||
fn state(&self) -> TransportState {
|
||||
self.state
|
||||
}
|
||||
|
||||
fn mtu(&self) -> u16 {
|
||||
self.effective_mtu
|
||||
}
|
||||
|
||||
fn start(&mut self) -> Result<(), TransportError> {
|
||||
Err(TransportError::NotSupported(
|
||||
"use start_async() for Ethernet transport".into(),
|
||||
))
|
||||
}
|
||||
|
||||
fn stop(&mut self) -> Result<(), TransportError> {
|
||||
Err(TransportError::NotSupported(
|
||||
"use stop_async() for Ethernet transport".into(),
|
||||
))
|
||||
}
|
||||
|
||||
fn send(&self, _addr: &TransportAddr, _data: &[u8]) -> Result<(), TransportError> {
|
||||
Err(TransportError::NotSupported(
|
||||
"use send_async() for Ethernet transport".into(),
|
||||
))
|
||||
}
|
||||
|
||||
fn discover(&self) -> Result<Vec<DiscoveredPeer>, TransportError> {
|
||||
Ok(self.discovery_buffer.take())
|
||||
}
|
||||
|
||||
fn auto_connect(&self) -> bool {
|
||||
self.config.auto_connect()
|
||||
}
|
||||
|
||||
fn accept_connections(&self) -> bool {
|
||||
self.config.accept_connections()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Receive Loop
|
||||
// ============================================================================
|
||||
|
||||
/// Ethernet receive loop — runs as a spawned task.
|
||||
async fn ethernet_receive_loop(
|
||||
socket: Arc<AsyncPacketSocket>,
|
||||
transport_id: TransportId,
|
||||
packet_tx: PacketTx,
|
||||
mtu: u16,
|
||||
discovery_enabled: bool,
|
||||
discovery_buffer: Arc<DiscoveryBuffer>,
|
||||
stats: Arc<EthernetStats>,
|
||||
) {
|
||||
// Buffer with headroom: frame type prefix + MTU + some extra
|
||||
let mut buf = vec![0u8; mtu as usize + 100];
|
||||
|
||||
debug!(transport_id = %transport_id, "Ethernet receive loop starting");
|
||||
|
||||
loop {
|
||||
match socket.recv_from(&mut buf).await {
|
||||
Ok((len, src_mac)) => {
|
||||
if len == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
stats.record_recv(len);
|
||||
|
||||
let frame_type = buf[0];
|
||||
match frame_type {
|
||||
FRAME_TYPE_DATA => {
|
||||
// Strip the frame type prefix, deliver payload
|
||||
let data = buf[1..len].to_vec();
|
||||
let addr = TransportAddr::from_bytes(&src_mac);
|
||||
let packet = ReceivedPacket::new(transport_id, addr, data);
|
||||
|
||||
trace!(
|
||||
transport_id = %transport_id,
|
||||
remote_mac = %format_mac(&src_mac),
|
||||
bytes = len - 1,
|
||||
"Ethernet data frame received"
|
||||
);
|
||||
|
||||
if packet_tx.send(packet).await.is_err() {
|
||||
info!(
|
||||
transport_id = %transport_id,
|
||||
"Packet channel closed, stopping receive loop"
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
FRAME_TYPE_BEACON => {
|
||||
stats.record_beacon_recv();
|
||||
|
||||
if discovery_enabled
|
||||
&& let Some(pubkey) = parse_beacon(&buf[..len])
|
||||
{
|
||||
discovery_buffer.add_peer(src_mac, pubkey);
|
||||
trace!(
|
||||
transport_id = %transport_id,
|
||||
remote_mac = %format_mac(&src_mac),
|
||||
"Discovery beacon received"
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Unknown frame type, ignore
|
||||
trace!(
|
||||
transport_id = %transport_id,
|
||||
frame_type = frame_type,
|
||||
"Unknown frame type, dropping"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
stats.record_recv_error();
|
||||
warn!(
|
||||
transport_id = %transport_id,
|
||||
error = %e,
|
||||
"Ethernet receive error"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
debug!(transport_id = %transport_id, "Ethernet receive loop stopped");
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Beacon Sender
|
||||
// ============================================================================
|
||||
|
||||
/// Periodic beacon sender loop.
|
||||
async fn beacon_sender_loop(
|
||||
socket: Arc<AsyncPacketSocket>,
|
||||
pubkey: XOnlyPublicKey,
|
||||
interval_secs: u64,
|
||||
stats: Arc<EthernetStats>,
|
||||
transport_id: TransportId,
|
||||
) {
|
||||
let beacon = build_beacon(&pubkey);
|
||||
let interval = tokio::time::Duration::from_secs(interval_secs);
|
||||
|
||||
debug!(
|
||||
transport_id = %transport_id,
|
||||
interval_secs,
|
||||
"Beacon sender starting"
|
||||
);
|
||||
|
||||
// Send an initial beacon immediately at startup
|
||||
if let Err(e) = socket.send_to(&beacon, ÐERNET_BROADCAST).await {
|
||||
warn!(
|
||||
transport_id = %transport_id,
|
||||
error = %e,
|
||||
"Failed to send initial beacon"
|
||||
);
|
||||
} else {
|
||||
stats.record_beacon_sent();
|
||||
}
|
||||
|
||||
let mut interval_timer = tokio::time::interval(interval);
|
||||
interval_timer.tick().await; // consume the immediate first tick
|
||||
|
||||
loop {
|
||||
interval_timer.tick().await;
|
||||
|
||||
match socket.send_to(&beacon, ÐERNET_BROADCAST).await {
|
||||
Ok(_) => {
|
||||
stats.record_beacon_sent();
|
||||
trace!(
|
||||
transport_id = %transport_id,
|
||||
"Beacon sent"
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
stats.record_send_error();
|
||||
warn!(
|
||||
transport_id = %transport_id,
|
||||
error = %e,
|
||||
"Failed to send beacon"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MAC Address Helpers
|
||||
// ============================================================================
|
||||
|
||||
/// Parse a TransportAddr as a 6-byte MAC address.
|
||||
fn parse_mac_addr(addr: &TransportAddr) -> Result<[u8; 6], TransportError> {
|
||||
let bytes = addr.as_bytes();
|
||||
if bytes.len() != 6 {
|
||||
return Err(TransportError::InvalidAddress(format!(
|
||||
"expected 6-byte MAC, got {} bytes",
|
||||
bytes.len()
|
||||
)));
|
||||
}
|
||||
if bytes == [0, 0, 0, 0, 0, 0] {
|
||||
return Err(TransportError::InvalidAddress(
|
||||
"destination MAC is all zeros".into(),
|
||||
));
|
||||
}
|
||||
let mut mac = [0u8; 6];
|
||||
mac.copy_from_slice(bytes);
|
||||
Ok(mac)
|
||||
}
|
||||
|
||||
/// Format a MAC address as colon-separated hex for display.
|
||||
pub fn format_mac(mac: &[u8; 6]) -> String {
|
||||
format!(
|
||||
"{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
|
||||
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]
|
||||
)
|
||||
}
|
||||
|
||||
/// Parse a colon-separated MAC string (e.g., "aa:bb:cc:dd:ee:ff") into bytes.
|
||||
pub fn parse_mac_string(s: &str) -> Result<[u8; 6], TransportError> {
|
||||
let parts: Vec<&str> = s.split(':').collect();
|
||||
if parts.len() != 6 {
|
||||
return Err(TransportError::InvalidAddress(format!(
|
||||
"invalid MAC format: expected 6 colon-separated hex bytes, got '{}'",
|
||||
s
|
||||
)));
|
||||
}
|
||||
let mut mac = [0u8; 6];
|
||||
for (i, part) in parts.iter().enumerate() {
|
||||
mac[i] = u8::from_str_radix(part, 16).map_err(|_| {
|
||||
TransportError::InvalidAddress(format!("invalid hex byte '{}' in MAC address", part))
|
||||
})?;
|
||||
}
|
||||
Ok(mac)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Tests
|
||||
// ============================================================================
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_mac_addr_valid() {
|
||||
let addr = TransportAddr::from_bytes(&[0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff]);
|
||||
let mac = parse_mac_addr(&addr).unwrap();
|
||||
assert_eq!(mac, [0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_mac_addr_wrong_length() {
|
||||
let addr = TransportAddr::from_bytes(&[0xaa, 0xbb, 0xcc]);
|
||||
assert!(parse_mac_addr(&addr).is_err());
|
||||
|
||||
let addr = TransportAddr::from_string("192.168.1.1:4000");
|
||||
assert!(parse_mac_addr(&addr).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_mac_addr_all_zeros() {
|
||||
let addr = TransportAddr::from_bytes(&[0, 0, 0, 0, 0, 0]);
|
||||
assert!(parse_mac_addr(&addr).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_mac() {
|
||||
let mac = [0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff];
|
||||
assert_eq!(format_mac(&mac), "aa:bb:cc:dd:ee:ff");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_mac_leading_zeros() {
|
||||
let mac = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06];
|
||||
assert_eq!(format_mac(&mac), "01:02:03:04:05:06");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_mac_string_valid() {
|
||||
let mac = parse_mac_string("aa:bb:cc:dd:ee:ff").unwrap();
|
||||
assert_eq!(mac, [0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_mac_string_uppercase() {
|
||||
let mac = parse_mac_string("AA:BB:CC:DD:EE:FF").unwrap();
|
||||
assert_eq!(mac, [0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_mac_string_invalid() {
|
||||
assert!(parse_mac_string("aa:bb:cc").is_err());
|
||||
assert!(parse_mac_string("not:a:mac:at:all:x").is_err());
|
||||
assert!(parse_mac_string("").is_err());
|
||||
assert!(parse_mac_string("aa-bb-cc-dd-ee-ff").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_frame_type_data_prefix() {
|
||||
// Verify data frames are prefixed with 0x00
|
||||
let data = vec![1, 2, 3, 4];
|
||||
let mut frame = Vec::with_capacity(1 + data.len());
|
||||
frame.push(FRAME_TYPE_DATA);
|
||||
frame.extend_from_slice(&data);
|
||||
|
||||
assert_eq!(frame[0], 0x00);
|
||||
assert_eq!(&frame[1..], &[1, 2, 3, 4]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_beacon_size() {
|
||||
assert_eq!(discovery::BEACON_SIZE, 34);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,388 @@
|
||||
//! AF_PACKET socket creation, binding, and ioctl helpers.
|
||||
|
||||
use crate::transport::TransportError;
|
||||
use std::os::unix::io::{AsRawFd, RawFd};
|
||||
use tokio::io::unix::AsyncFd;
|
||||
|
||||
/// Broadcast MAC address.
|
||||
pub const ETHERNET_BROADCAST: [u8; 6] = [0xff; 6];
|
||||
|
||||
/// Wrapper around an AF_PACKET SOCK_DGRAM file descriptor.
|
||||
///
|
||||
/// Owns the fd and closes it on drop. Provides synchronous send/recv
|
||||
/// methods used by the async wrappers via `AsyncFd`.
|
||||
pub struct PacketSocket {
|
||||
fd: RawFd,
|
||||
if_index: i32,
|
||||
ethertype: u16,
|
||||
}
|
||||
|
||||
impl PacketSocket {
|
||||
/// Create and bind an AF_PACKET SOCK_DGRAM socket.
|
||||
///
|
||||
/// Returns an error with a clear message if CAP_NET_RAW is missing.
|
||||
pub fn open(interface: &str, ethertype: u16) -> Result<Self, TransportError> {
|
||||
let fd = unsafe {
|
||||
libc::socket(
|
||||
libc::AF_PACKET,
|
||||
libc::SOCK_DGRAM,
|
||||
(ethertype).to_be() as i32,
|
||||
)
|
||||
};
|
||||
if fd < 0 {
|
||||
let err = std::io::Error::last_os_error();
|
||||
if err.raw_os_error() == Some(libc::EPERM) {
|
||||
return Err(TransportError::StartFailed(
|
||||
"AF_PACKET requires CAP_NET_RAW capability \
|
||||
(run as root or use: setcap cap_net_raw=ep <binary>)"
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
return Err(TransportError::StartFailed(format!(
|
||||
"socket(AF_PACKET) failed: {}",
|
||||
err
|
||||
)));
|
||||
}
|
||||
|
||||
// Look up interface index
|
||||
let if_index = get_if_index(fd, interface)?;
|
||||
|
||||
// Bind to the interface
|
||||
let mut sll: libc::sockaddr_ll = unsafe { std::mem::zeroed() };
|
||||
sll.sll_family = libc::AF_PACKET as u16;
|
||||
sll.sll_protocol = ethertype.to_be();
|
||||
sll.sll_ifindex = if_index;
|
||||
|
||||
let ret = unsafe {
|
||||
libc::bind(
|
||||
fd,
|
||||
&sll as *const libc::sockaddr_ll as *const libc::sockaddr,
|
||||
std::mem::size_of::<libc::sockaddr_ll>() as libc::socklen_t,
|
||||
)
|
||||
};
|
||||
if ret < 0 {
|
||||
let err = std::io::Error::last_os_error();
|
||||
unsafe { libc::close(fd) };
|
||||
return Err(TransportError::StartFailed(format!(
|
||||
"bind(AF_PACKET, {}) failed: {}",
|
||||
interface, err
|
||||
)));
|
||||
}
|
||||
|
||||
// Set non-blocking for async integration
|
||||
let flags = unsafe { libc::fcntl(fd, libc::F_GETFL) };
|
||||
if flags < 0 {
|
||||
let err = std::io::Error::last_os_error();
|
||||
unsafe { libc::close(fd) };
|
||||
return Err(TransportError::StartFailed(format!(
|
||||
"fcntl(F_GETFL) failed: {}",
|
||||
err
|
||||
)));
|
||||
}
|
||||
let ret = unsafe { libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) };
|
||||
if ret < 0 {
|
||||
let err = std::io::Error::last_os_error();
|
||||
unsafe { libc::close(fd) };
|
||||
return Err(TransportError::StartFailed(format!(
|
||||
"fcntl(F_SETFL, O_NONBLOCK) failed: {}",
|
||||
err
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
fd,
|
||||
if_index,
|
||||
ethertype,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the interface index.
|
||||
pub fn if_index(&self) -> i32 {
|
||||
self.if_index
|
||||
}
|
||||
|
||||
/// Get the local MAC address of the bound interface.
|
||||
pub fn local_mac(&self) -> Result<[u8; 6], TransportError> {
|
||||
get_mac_addr(self.fd, self.if_index)
|
||||
}
|
||||
|
||||
/// Get the interface MTU.
|
||||
pub fn interface_mtu(&self) -> Result<u16, TransportError> {
|
||||
get_if_mtu(self.fd, self.if_index)
|
||||
}
|
||||
|
||||
/// Set the socket receive buffer size.
|
||||
pub fn set_recv_buffer_size(&self, size: usize) -> Result<(), TransportError> {
|
||||
let size = size as libc::c_int;
|
||||
let ret = unsafe {
|
||||
libc::setsockopt(
|
||||
self.fd,
|
||||
libc::SOL_SOCKET,
|
||||
libc::SO_RCVBUF,
|
||||
&size as *const libc::c_int as *const libc::c_void,
|
||||
std::mem::size_of::<libc::c_int>() as libc::socklen_t,
|
||||
)
|
||||
};
|
||||
if ret < 0 {
|
||||
return Err(TransportError::StartFailed(format!(
|
||||
"setsockopt(SO_RCVBUF) failed: {}",
|
||||
std::io::Error::last_os_error()
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set the socket send buffer size.
|
||||
pub fn set_send_buffer_size(&self, size: usize) -> Result<(), TransportError> {
|
||||
let size = size as libc::c_int;
|
||||
let ret = unsafe {
|
||||
libc::setsockopt(
|
||||
self.fd,
|
||||
libc::SOL_SOCKET,
|
||||
libc::SO_SNDBUF,
|
||||
&size as *const libc::c_int as *const libc::c_void,
|
||||
std::mem::size_of::<libc::c_int>() as libc::socklen_t,
|
||||
)
|
||||
};
|
||||
if ret < 0 {
|
||||
return Err(TransportError::StartFailed(format!(
|
||||
"setsockopt(SO_SNDBUF) failed: {}",
|
||||
std::io::Error::last_os_error()
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send a payload to a destination MAC address.
|
||||
///
|
||||
/// Returns the number of bytes sent, or an io::Error.
|
||||
pub fn send_to(&self, data: &[u8], dest_mac: &[u8; 6]) -> std::io::Result<usize> {
|
||||
let mut sll: libc::sockaddr_ll = unsafe { std::mem::zeroed() };
|
||||
sll.sll_family = libc::AF_PACKET as u16;
|
||||
sll.sll_protocol = self.ethertype.to_be();
|
||||
sll.sll_ifindex = self.if_index;
|
||||
sll.sll_halen = 6;
|
||||
sll.sll_addr[..6].copy_from_slice(dest_mac);
|
||||
|
||||
let ret = unsafe {
|
||||
libc::sendto(
|
||||
self.fd,
|
||||
data.as_ptr() as *const libc::c_void,
|
||||
data.len(),
|
||||
0,
|
||||
&sll as *const libc::sockaddr_ll as *const libc::sockaddr,
|
||||
std::mem::size_of::<libc::sockaddr_ll>() as libc::socklen_t,
|
||||
)
|
||||
};
|
||||
if ret < 0 {
|
||||
Err(std::io::Error::last_os_error())
|
||||
} else {
|
||||
Ok(ret as usize)
|
||||
}
|
||||
}
|
||||
|
||||
/// Receive a payload and source MAC address.
|
||||
///
|
||||
/// Returns (bytes_read, source_mac), or an io::Error.
|
||||
pub fn recv_from(&self, buf: &mut [u8]) -> std::io::Result<(usize, [u8; 6])> {
|
||||
let mut sll: libc::sockaddr_ll = unsafe { std::mem::zeroed() };
|
||||
let mut sll_len = std::mem::size_of::<libc::sockaddr_ll>() as libc::socklen_t;
|
||||
|
||||
let ret = unsafe {
|
||||
libc::recvfrom(
|
||||
self.fd,
|
||||
buf.as_mut_ptr() as *mut libc::c_void,
|
||||
buf.len(),
|
||||
0,
|
||||
&mut sll as *mut libc::sockaddr_ll as *mut libc::sockaddr,
|
||||
&mut sll_len,
|
||||
)
|
||||
};
|
||||
if ret < 0 {
|
||||
return Err(std::io::Error::last_os_error());
|
||||
}
|
||||
|
||||
let mut src_mac = [0u8; 6];
|
||||
src_mac.copy_from_slice(&sll.sll_addr[..6]);
|
||||
|
||||
Ok((ret as usize, src_mac))
|
||||
}
|
||||
|
||||
/// Wrap this socket in a tokio AsyncFd for async I/O.
|
||||
pub fn into_async(self) -> Result<AsyncPacketSocket, TransportError> {
|
||||
let async_fd = AsyncFd::new(self)
|
||||
.map_err(|e| TransportError::StartFailed(format!("AsyncFd::new failed: {}", e)))?;
|
||||
Ok(AsyncPacketSocket { inner: async_fd })
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRawFd for PacketSocket {
|
||||
fn as_raw_fd(&self) -> RawFd {
|
||||
self.fd
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for PacketSocket {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
libc::close(self.fd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Async wrapper around PacketSocket using tokio's AsyncFd.
|
||||
pub struct AsyncPacketSocket {
|
||||
inner: AsyncFd<PacketSocket>,
|
||||
}
|
||||
|
||||
impl AsyncPacketSocket {
|
||||
/// Send a payload to a destination MAC address.
|
||||
pub async fn send_to(&self, data: &[u8], dest_mac: &[u8; 6]) -> Result<usize, TransportError> {
|
||||
loop {
|
||||
let mut guard = self
|
||||
.inner
|
||||
.writable()
|
||||
.await
|
||||
.map_err(|e| TransportError::SendFailed(format!("writable wait: {}", e)))?;
|
||||
|
||||
match guard.try_io(|inner| inner.get_ref().send_to(data, dest_mac)) {
|
||||
Ok(Ok(n)) => return Ok(n),
|
||||
Ok(Err(e)) => return Err(TransportError::SendFailed(format!("{}", e))),
|
||||
Err(_would_block) => continue,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Receive a payload and source MAC address.
|
||||
pub async fn recv_from(
|
||||
&self,
|
||||
buf: &mut [u8],
|
||||
) -> Result<(usize, [u8; 6]), TransportError> {
|
||||
loop {
|
||||
let mut guard = self
|
||||
.inner
|
||||
.readable()
|
||||
.await
|
||||
.map_err(|e| TransportError::RecvFailed(format!("readable wait: {}", e)))?;
|
||||
|
||||
match guard.try_io(|inner| inner.get_ref().recv_from(buf)) {
|
||||
Ok(Ok(result)) => return Ok(result),
|
||||
Ok(Err(e)) => return Err(TransportError::RecvFailed(format!("{}", e))),
|
||||
Err(_would_block) => continue,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a reference to the inner PacketSocket.
|
||||
pub fn get_ref(&self) -> &PacketSocket {
|
||||
self.inner.get_ref()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ioctl helpers
|
||||
// ============================================================================
|
||||
|
||||
/// Get the interface index by name.
|
||||
fn get_if_index(_fd: RawFd, interface: &str) -> Result<i32, TransportError> {
|
||||
let c_name = std::ffi::CString::new(interface).map_err(|_| {
|
||||
TransportError::StartFailed(format!("invalid interface name: {}", interface))
|
||||
})?;
|
||||
|
||||
let idx = unsafe { libc::if_nametoindex(c_name.as_ptr()) };
|
||||
if idx == 0 {
|
||||
return Err(TransportError::StartFailed(format!(
|
||||
"interface not found: {} ({})",
|
||||
interface,
|
||||
std::io::Error::last_os_error()
|
||||
)));
|
||||
}
|
||||
Ok(idx as i32)
|
||||
}
|
||||
|
||||
/// Get the MAC address of an interface by its index.
|
||||
fn get_mac_addr(fd: RawFd, if_index: i32) -> Result<[u8; 6], TransportError> {
|
||||
// First get the interface name from the index
|
||||
let mut ifr: libc::ifreq = unsafe { std::mem::zeroed() };
|
||||
|
||||
// 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)
|
||||
};
|
||||
if ret.is_null() {
|
||||
return Err(TransportError::StartFailed(format!(
|
||||
"if_indextoname({}) failed: {}",
|
||||
if_index,
|
||||
std::io::Error::last_os_error()
|
||||
)));
|
||||
}
|
||||
|
||||
// Copy name into ifreq
|
||||
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(
|
||||
name_buf.as_ptr(),
|
||||
ifr.ifr_name.as_mut_ptr() as *mut u8,
|
||||
copy_len,
|
||||
);
|
||||
}
|
||||
|
||||
let ret = unsafe { libc::ioctl(fd, libc::SIOCGIFHWADDR as libc::c_ulong, &ifr) };
|
||||
if ret < 0 {
|
||||
return Err(TransportError::StartFailed(format!(
|
||||
"ioctl(SIOCGIFHWADDR) failed: {}",
|
||||
std::io::Error::last_os_error()
|
||||
)));
|
||||
}
|
||||
|
||||
let mut mac = [0u8; 6];
|
||||
unsafe {
|
||||
let sa_data = ifr.ifr_ifru.ifru_hwaddr.sa_data;
|
||||
for (i, byte) in mac.iter_mut().enumerate() {
|
||||
*byte = sa_data[i] as u8;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(mac)
|
||||
}
|
||||
|
||||
/// Get the MTU of an interface by its index.
|
||||
fn get_if_mtu(fd: RawFd, if_index: i32) -> Result<u16, TransportError> {
|
||||
let mut ifr: libc::ifreq = unsafe { std::mem::zeroed() };
|
||||
|
||||
// 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)
|
||||
};
|
||||
if ret.is_null() {
|
||||
return Err(TransportError::StartFailed(format!(
|
||||
"if_indextoname({}) failed: {}",
|
||||
if_index,
|
||||
std::io::Error::last_os_error()
|
||||
)));
|
||||
}
|
||||
|
||||
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(
|
||||
name_buf.as_ptr(),
|
||||
ifr.ifr_name.as_mut_ptr() as *mut u8,
|
||||
copy_len,
|
||||
);
|
||||
}
|
||||
|
||||
let ret = unsafe { libc::ioctl(fd, libc::SIOCGIFMTU as libc::c_ulong, &ifr) };
|
||||
if ret < 0 {
|
||||
return Err(TransportError::StartFailed(format!(
|
||||
"ioctl(SIOCGIFMTU) failed: {}",
|
||||
std::io::Error::last_os_error()
|
||||
)));
|
||||
}
|
||||
|
||||
let mtu = unsafe { ifr.ifr_ifru.ifru_mtu } as u16;
|
||||
Ok(mtu)
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
//! Ethernet transport statistics.
|
||||
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
/// Statistics for an Ethernet transport instance.
|
||||
///
|
||||
/// Uses atomic counters for lock-free updates from the receive loop
|
||||
/// and send path concurrently.
|
||||
pub struct EthernetStats {
|
||||
pub frames_sent: AtomicU64,
|
||||
pub frames_recv: AtomicU64,
|
||||
pub bytes_sent: AtomicU64,
|
||||
pub bytes_recv: AtomicU64,
|
||||
pub send_errors: AtomicU64,
|
||||
pub recv_errors: AtomicU64,
|
||||
pub beacons_sent: AtomicU64,
|
||||
pub beacons_recv: AtomicU64,
|
||||
pub frames_too_short: AtomicU64,
|
||||
pub frames_too_long: AtomicU64,
|
||||
}
|
||||
|
||||
impl EthernetStats {
|
||||
/// Create a new stats instance with all counters at zero.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
frames_sent: AtomicU64::new(0),
|
||||
frames_recv: AtomicU64::new(0),
|
||||
bytes_sent: AtomicU64::new(0),
|
||||
bytes_recv: AtomicU64::new(0),
|
||||
send_errors: AtomicU64::new(0),
|
||||
recv_errors: AtomicU64::new(0),
|
||||
beacons_sent: AtomicU64::new(0),
|
||||
beacons_recv: AtomicU64::new(0),
|
||||
frames_too_short: AtomicU64::new(0),
|
||||
frames_too_long: AtomicU64::new(0),
|
||||
}
|
||||
}
|
||||
|
||||
/// Record a successful send.
|
||||
pub fn record_send(&self, bytes: usize) {
|
||||
self.frames_sent.fetch_add(1, Ordering::Relaxed);
|
||||
self.bytes_sent.fetch_add(bytes as u64, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Record a successful receive.
|
||||
pub fn record_recv(&self, bytes: usize) {
|
||||
self.frames_recv.fetch_add(1, Ordering::Relaxed);
|
||||
self.bytes_recv.fetch_add(bytes as u64, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Record a send error.
|
||||
pub fn record_send_error(&self) {
|
||||
self.send_errors.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Record a receive error.
|
||||
pub fn record_recv_error(&self) {
|
||||
self.recv_errors.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Record a sent beacon.
|
||||
pub fn record_beacon_sent(&self) {
|
||||
self.beacons_sent.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Record a received beacon.
|
||||
pub fn record_beacon_recv(&self) {
|
||||
self.beacons_recv.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Take a snapshot of all counters.
|
||||
pub fn snapshot(&self) -> EthernetStatsSnapshot {
|
||||
EthernetStatsSnapshot {
|
||||
frames_sent: self.frames_sent.load(Ordering::Relaxed),
|
||||
frames_recv: self.frames_recv.load(Ordering::Relaxed),
|
||||
bytes_sent: self.bytes_sent.load(Ordering::Relaxed),
|
||||
bytes_recv: self.bytes_recv.load(Ordering::Relaxed),
|
||||
send_errors: self.send_errors.load(Ordering::Relaxed),
|
||||
recv_errors: self.recv_errors.load(Ordering::Relaxed),
|
||||
beacons_sent: self.beacons_sent.load(Ordering::Relaxed),
|
||||
beacons_recv: self.beacons_recv.load(Ordering::Relaxed),
|
||||
frames_too_short: self.frames_too_short.load(Ordering::Relaxed),
|
||||
frames_too_long: self.frames_too_long.load(Ordering::Relaxed),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for EthernetStats {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Point-in-time snapshot of Ethernet stats (non-atomic, copyable).
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct EthernetStatsSnapshot {
|
||||
pub frames_sent: u64,
|
||||
pub frames_recv: u64,
|
||||
pub bytes_sent: u64,
|
||||
pub bytes_recv: u64,
|
||||
pub send_errors: u64,
|
||||
pub recv_errors: u64,
|
||||
pub beacons_sent: u64,
|
||||
pub beacons_recv: u64,
|
||||
pub frames_too_short: u64,
|
||||
pub frames_too_long: u64,
|
||||
}
|
||||
+77
-2
@@ -6,8 +6,13 @@
|
||||
|
||||
pub mod udp;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod ethernet;
|
||||
|
||||
use secp256k1::XOnlyPublicKey;
|
||||
use udp::UdpTransport;
|
||||
#[cfg(target_os = "linux")]
|
||||
use ethernet::EthernetTransport;
|
||||
use std::fmt;
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
use thiserror::Error;
|
||||
@@ -761,6 +766,18 @@ pub trait Transport {
|
||||
|
||||
/// Discover potential peers (if supported).
|
||||
fn discover(&self) -> Result<Vec<DiscoveredPeer>, TransportError>;
|
||||
|
||||
/// Whether to auto-connect to peers returned by discover().
|
||||
/// Default: false. Concrete transports read from their own config.
|
||||
fn auto_connect(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Whether to accept inbound handshake initiations on this transport.
|
||||
/// Default: true (preserves UDP's current implicit behavior).
|
||||
fn accept_connections(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -774,7 +791,9 @@ pub trait Transport {
|
||||
pub enum TransportHandle {
|
||||
/// UDP/IP transport.
|
||||
Udp(UdpTransport),
|
||||
// Future: Tcp(TcpTransport), Tor(TorTransport), etc.
|
||||
/// Raw Ethernet transport.
|
||||
#[cfg(target_os = "linux")]
|
||||
Ethernet(EthernetTransport),
|
||||
}
|
||||
|
||||
impl TransportHandle {
|
||||
@@ -782,6 +801,8 @@ impl TransportHandle {
|
||||
pub async fn start(&mut self) -> Result<(), TransportError> {
|
||||
match self {
|
||||
TransportHandle::Udp(t) => t.start_async().await,
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ethernet(t) => t.start_async().await,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -789,6 +810,8 @@ impl TransportHandle {
|
||||
pub async fn stop(&mut self) -> Result<(), TransportError> {
|
||||
match self {
|
||||
TransportHandle::Udp(t) => t.stop_async().await,
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ethernet(t) => t.stop_async().await,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -796,6 +819,8 @@ impl TransportHandle {
|
||||
pub async fn send(&self, addr: &TransportAddr, data: &[u8]) -> Result<usize, TransportError> {
|
||||
match self {
|
||||
TransportHandle::Udp(t) => t.send_async(addr, data).await,
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ethernet(t) => t.send_async(addr, data).await,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -803,6 +828,8 @@ impl TransportHandle {
|
||||
pub fn transport_id(&self) -> TransportId {
|
||||
match self {
|
||||
TransportHandle::Udp(t) => t.transport_id(),
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ethernet(t) => t.transport_id(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -810,6 +837,8 @@ impl TransportHandle {
|
||||
pub fn name(&self) -> Option<&str> {
|
||||
match self {
|
||||
TransportHandle::Udp(t) => t.name(),
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ethernet(t) => t.name(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -817,6 +846,8 @@ impl TransportHandle {
|
||||
pub fn transport_type(&self) -> &TransportType {
|
||||
match self {
|
||||
TransportHandle::Udp(t) => t.transport_type(),
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ethernet(t) => t.transport_type(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -824,6 +855,8 @@ impl TransportHandle {
|
||||
pub fn state(&self) -> TransportState {
|
||||
match self {
|
||||
TransportHandle::Udp(t) => t.state(),
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ethernet(t) => t.state(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -831,6 +864,8 @@ impl TransportHandle {
|
||||
pub fn mtu(&self) -> u16 {
|
||||
match self {
|
||||
TransportHandle::Udp(t) => t.mtu(),
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ethernet(t) => t.mtu(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -841,13 +876,53 @@ impl TransportHandle {
|
||||
pub fn link_mtu(&self, addr: &TransportAddr) -> u16 {
|
||||
match self {
|
||||
TransportHandle::Udp(t) => t.link_mtu(addr),
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ethernet(t) => t.link_mtu(addr),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the local bound address (only valid after start).
|
||||
/// Get the local bound address (UDP only, returns None for other transports).
|
||||
pub fn local_addr(&self) -> Option<std::net::SocketAddr> {
|
||||
match self {
|
||||
TransportHandle::Udp(t) => t.local_addr(),
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ethernet(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the interface name (Ethernet only, returns None for other transports).
|
||||
pub fn interface_name(&self) -> Option<&str> {
|
||||
match self {
|
||||
TransportHandle::Udp(_) => None,
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ethernet(t) => Some(t.interface_name()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Drain discovered peers from this transport.
|
||||
pub fn discover(&self) -> Result<Vec<DiscoveredPeer>, TransportError> {
|
||||
match self {
|
||||
TransportHandle::Udp(t) => t.discover(),
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ethernet(t) => t.discover(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this transport auto-connects to discovered peers.
|
||||
pub fn auto_connect(&self) -> bool {
|
||||
match self {
|
||||
TransportHandle::Udp(t) => t.auto_connect(),
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ethernet(t) => t.auto_connect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this transport accepts inbound connections.
|
||||
pub fn accept_connections(&self) -> bool {
|
||||
match self {
|
||||
TransportHandle::Udp(t) => t.accept_connections(),
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ethernet(t) => t.accept_connections(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user