fips-core 0.4.15

Reusable FIPS mesh, endpoint, transport, and protocol library
Documentation
use super::*;
use crate::config::WebSocketConfig;
use crate::transport::websocket::WebSocketTransport;
use crate::transport::{TransportAddr, TransportHandle, TransportId, packet_channel};
use spanning_tree::{TestNode, cleanup_nodes, process_available_packets, run_synthetic_node_work};
use std::time::Duration;

async fn make_websocket_node(config: WebSocketConfig) -> TestNode {
    let mut node = make_node();
    node.config.node.rate_limit.handshake_resend_interval_ms = 50;
    node.config.node.rate_limit.handshake_max_resends = 20;
    let transport_id = TransportId::new(1);
    let (packet_tx, packet_rx) = packet_channel(256);
    let (tun_outbound_tx, tun_outbound_rx) = crate::upper::tun::tun_outbound_channel(256);
    node.tun_outbound_rx = Some(tun_outbound_rx);
    let mut transport =
        WebSocketTransport::new(transport_id, None, config, packet_tx, node.identity());
    transport.start_async().await.unwrap();
    let addr = transport
        .local_addr()
        .map(|addr| TransportAddr::from_string(&format!("ws://{addr}/fips")))
        .unwrap_or_else(|| TransportAddr::from_string("websocket-client"));
    node.transports.insert(
        transport_id,
        TransportHandle::WebSocket(Box::new(transport)),
    );
    TestNode {
        node,
        transport_id,
        packet_rx,
        tun_outbound_tx,
        addr,
    }
}

#[tokio::test]
async fn url_only_seed_hint_completes_noise_ik_and_datagram_exchange() {
    let server = make_websocket_node(WebSocketConfig {
        bind_addr: Some("127.0.0.1:0".into()),
        ..Default::default()
    })
    .await;
    let seed_url = server.addr.to_string();
    let client = make_websocket_node(WebSocketConfig {
        seed_urls: vec![seed_url],
        reconnect_initial_ms: Some(10),
        reconnect_max_ms: Some(50),
        ..Default::default()
    })
    .await;
    let mut nodes = vec![server, client];
    let server_addr = *nodes[0].node.node_addr();
    let client_addr = *nodes[1].node.node_addr();

    tokio::time::timeout(Duration::from_secs(5), async {
        loop {
            for node in &mut nodes {
                node.node.poll_transport_discovery().await;
                node.node.poll_pending_connects().await;
            }
            run_synthetic_node_work(&mut nodes).await;
            process_available_packets(&mut nodes).await;
            if nodes[0].node.get_peer(&client_addr).is_some()
                && nodes[1].node.get_peer(&server_addr).is_some()
            {
                break;
            }
            tokio::time::sleep(Duration::from_millis(10)).await;
        }
    })
    .await
    .expect("URL-only WebSocket seed must authenticate with Noise IK");

    assert_eq!(
        nodes[1]
            .node
            .get_peer(&server_addr)
            .and_then(|peer| peer.transport_id()),
        Some(nodes[1].transport_id)
    );
    cleanup_nodes(&mut nodes).await;
}

#[tokio::test]
async fn open_discovery_listener_routes_first_contact_between_websocket_clients() {
    let seed = make_websocket_node(WebSocketConfig {
        bind_addr: Some("127.0.0.1:0".into()),
        ..Default::default()
    })
    .await;
    let seed_url = seed.addr.to_string();

    let router = make_websocket_node(WebSocketConfig {
        bind_addr: Some("127.0.0.1:0".into()),
        seed_urls: vec![seed_url.clone()],
        reconnect_initial_ms: Some(10),
        reconnect_max_ms: Some(50),
        ..Default::default()
    })
    .await;
    let router_url = router.addr.to_string();

    let guest = make_websocket_node(WebSocketConfig {
        seed_urls: vec![router_url],
        reconnect_initial_ms: Some(10),
        reconnect_max_ms: Some(50),
        ..Default::default()
    })
    .await;
    let admin = make_websocket_node(WebSocketConfig {
        seed_urls: vec![seed_url],
        reconnect_initial_ms: Some(10),
        reconnect_max_ms: Some(50),
        ..Default::default()
    })
    .await;

    let mut nodes = vec![seed, router, guest, admin];
    for node in &mut nodes {
        node.node.config.node.routing.mode = crate::config::RoutingMode::ReplyLearned;
        node.node.config.node.discovery.nostr.enabled = true;
        node.node.config.node.discovery.nostr.policy = crate::config::NostrDiscoveryPolicy::Open;
    }

    let seed_addr = *nodes[0].node.node_addr();
    let router_addr = *nodes[1].node.node_addr();
    let guest_addr = *nodes[2].node.node_addr();
    let admin_addr = *nodes[3].node.node_addr();
    let guest_npub = nodes[2].node.identity.npub();
    let guest_pubkey = nodes[2].node.identity.pubkey_full();
    nodes[3].node.config.peers.push(crate::config::PeerConfig {
        npub: guest_npub,
        alias: None,
        addresses: Vec::new(),
        connect_policy: crate::config::ConnectPolicy::AutoConnect,
        auto_reconnect: true,
        discovery_fallback_transit: true,
    });
    nodes[3].node.configured_peers = ConfiguredPeerLookup::from_config(&nodes[3].node.config);
    nodes[3].node.register_identity(guest_addr, guest_pubkey);

    tokio::time::timeout(Duration::from_secs(5), async {
        loop {
            for node in &mut nodes {
                node.node.poll_transport_discovery().await;
                node.node.poll_pending_connects().await;
            }
            run_synthetic_node_work(&mut nodes).await;
            process_available_packets(&mut nodes).await;
            let seed_ready = nodes[0].node.get_peer(&router_addr).is_some()
                && nodes[0].node.get_peer(&admin_addr).is_some();
            let router_ready = nodes[1].node.get_peer(&seed_addr).is_some()
                && nodes[1].node.get_peer(&guest_addr).is_some();
            let edge_ready = nodes[2].node.get_peer(&router_addr).is_some()
                && nodes[3].node.get_peer(&seed_addr).is_some();
            if seed_ready && router_ready && edge_ready {
                break;
            }
            tokio::time::sleep(Duration::from_millis(10)).await;
        }
    })
    .await
    .expect("WebSocket seed/router topology must authenticate");

    assert!(
        nodes[0]
            .node
            .peer_is_configured_websocket_adjacency(&admin_addr),
        "the seed must recognize an authenticated inbound admin as an operator-configured adjacency"
    );
    assert!(
        nodes[0]
            .node
            .peer_is_configured_websocket_adjacency(&router_addr),
        "the seed must recognize an authenticated inbound router as an operator-configured adjacency"
    );
    assert!(
        nodes[1]
            .node
            .peer_is_configured_websocket_adjacency(&seed_addr),
        "the router must recognize its explicitly configured outbound seed"
    );

    assert_eq!(
        nodes[3].node.initiate_lookup(&guest_addr, 8).await,
        1,
        "admin lookup should leave through its configured WebSocket seed"
    );
    for _ in 0..500 {
        run_synthetic_node_work(&mut nodes).await;
        process_available_packets(&mut nodes).await;
        if nodes[3].node.find_next_hop(&guest_addr).is_some() {
            break;
        }
        tokio::time::sleep(Duration::from_millis(10)).await;
    }
    assert!(
        !nodes[0].node.recent_requests.is_empty(),
        "the WSS seed must receive the admin lookup"
    );
    assert!(
        !nodes[1].node.recent_requests.is_empty(),
        "the WSS seed must forward the lookup to the router client"
    );
    assert!(
        !nodes[2].node.recent_requests.is_empty(),
        "the router must forward the lookup to its direct guest"
    );
    assert!(
        nodes[3].node.find_next_hop(&guest_addr).is_some(),
        "lookup should traverse the WSS listener and return a guest route"
    );

    nodes[3]
        .node
        .initiate_session(guest_addr, guest_pubkey)
        .await
        .expect("admin should initiate an end-to-end session over the learned route");
    for _ in 0..500 {
        run_synthetic_node_work(&mut nodes).await;
        process_available_packets(&mut nodes).await;
        let admin_established = nodes[3]
            .node
            .get_session(&guest_addr)
            .is_some_and(|session| session.is_established());
        let guest_established = nodes[2]
            .node
            .get_session(&admin_addr)
            .is_some_and(|session| session.is_established());
        if admin_established && guest_established {
            break;
        }
        tokio::time::sleep(Duration::from_millis(10)).await;
    }
    assert!(
        nodes[3]
            .node
            .get_session(&guest_addr)
            .is_some_and(|session| session.is_established()),
        "the admin session should establish over WSS seed/router transit"
    );
    assert!(
        nodes[2]
            .node
            .get_session(&admin_addr)
            .is_some_and(|session| session.is_established()),
        "the guest session should establish over WSS seed/router transit"
    );

    cleanup_nodes(&mut nodes).await;
}