fips-core 0.3.1

Reusable FIPS mesh, endpoint, transport, and protocol library
Documentation
//! 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::{TransportAddr, TransportHandle, TransportId, packet_channel};
use spanning_tree::{TestNode, cleanup_nodes, drain_all_packets, initiate_handshake};

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;
}