mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-09 08:14:42 +00:00
`Node::adopt_established_traversal` was constructing the adopted UDP transport with `UdpConfig::default()` — MTU 1280, default recv/send buffer sizes, default accept/advertise flags. If the operator had configured a higher MTU on the primary `[transports.udp]` listener (e.g. 1500 on a path where larger frames are known viable), full-sized tunnel datagrams sent over the NAT-traversed link would exceed the adopted socket's MTU and get dropped at the socket layer with no visibility into why throughput collapsed. Inherit the primary UDP config (MTU + recv/send buffer sizes + accept / advertise flags) and clear the bind / external-address fields since the adopted socket is already bound. Lookup tries `transport_name` first so operators with multiple named `[transports.udp.<name>]` listeners pick up inheritance from the matching listener, and falls back to the unnamed `Single` listener so single-instance configs work unchanged. The previous default of MTU 1280 was deliberately the IPv6 minimum, the only value guaranteed to survive arbitrary middlebox paths. With this change, operators who set their primary listener higher (based on known-clean LAN topology) will have NAT-traversed flows initially attempting that higher MTU and possibly black-holing on tighter paths until reactive `MtuExceeded` recovery kicks in. Documented in the adoption call-site comment so future readers understand why the conservative default went away. Discovered in a downstream consumer where a `MESH_TUNNEL_MTU=1320` / encrypted wire ~1426B produced silent packet drop on every session that had been promoted onto a NAT-traversed link. Adds two sibling tests in `src/node/tests/bootstrap.rs` pinning the new behaviour for the `Single` and `Named` config variants.
334 lines
11 KiB
Rust
334 lines
11 KiB
Rust
//! Integration tests for bootstrap handoff into the FIPS node.
|
|
|
|
use super::*;
|
|
use crate::EstablishedTraversal;
|
|
use crate::config::{TransportInstances, UdpConfig};
|
|
use crate::node::wire::{PHASE_MSG1, PHASE_MSG2};
|
|
use crate::transport::udp::UdpTransport;
|
|
use crate::utils::index::IndexAllocator;
|
|
use std::collections::HashMap;
|
|
use tokio::time::{Duration, timeout, timeout_at};
|
|
|
|
#[tokio::test]
|
|
async fn test_adopted_udp_traversal_completes_handshake() {
|
|
let mut node_a = make_node();
|
|
let mut node_b = make_node();
|
|
|
|
let transport_id_b = TransportId::new(1);
|
|
let udp_config = UdpConfig {
|
|
bind_addr: Some("127.0.0.1:0".to_string()),
|
|
mtu: Some(1280),
|
|
..Default::default()
|
|
};
|
|
|
|
let (packet_tx_a, packet_rx_a) = packet_channel(64);
|
|
let (packet_tx_b, packet_rx_b) = packet_channel(64);
|
|
|
|
node_a.packet_tx = Some(packet_tx_a.clone());
|
|
node_a.packet_rx = Some(packet_rx_a);
|
|
node_a.state = NodeState::Running;
|
|
|
|
let mut transport_b = UdpTransport::new(transport_id_b, None, udp_config, packet_tx_b.clone());
|
|
transport_b.start_async().await.unwrap();
|
|
|
|
let addr_b = transport_b.local_addr().unwrap();
|
|
node_b.packet_tx = Some(packet_tx_b.clone());
|
|
node_b.packet_rx = Some(packet_rx_b);
|
|
node_b.state = NodeState::Running;
|
|
node_b
|
|
.transports
|
|
.insert(transport_id_b, TransportHandle::Udp(transport_b));
|
|
|
|
let adopted_socket = std::net::UdpSocket::bind("127.0.0.1:0").unwrap();
|
|
let handoff = EstablishedTraversal::new("sess-1", node_b.npub(), addr_b, adopted_socket)
|
|
.with_transport_name("nostr-punched");
|
|
|
|
let result = node_a.adopt_established_traversal(handoff).await.unwrap();
|
|
assert_eq!(result.remote_addr, addr_b);
|
|
assert!(node_a.get_transport(&result.transport_id).is_some());
|
|
|
|
tokio::select! {
|
|
result = node_b.run_rx_loop() => {
|
|
panic!("node_b rx loop exited unexpectedly: {:?}", result);
|
|
}
|
|
_ = tokio::time::sleep(Duration::from_millis(500)) => {}
|
|
}
|
|
|
|
tokio::select! {
|
|
result = node_a.run_rx_loop() => {
|
|
panic!("node_a rx loop exited unexpectedly: {:?}", result);
|
|
}
|
|
_ = tokio::time::sleep(Duration::from_millis(500)) => {}
|
|
}
|
|
|
|
let peer_a_node_addr =
|
|
*PeerIdentity::from_pubkey_full(node_a.identity.pubkey_full()).node_addr();
|
|
let peer_b_node_addr =
|
|
*PeerIdentity::from_pubkey_full(node_b.identity.pubkey_full()).node_addr();
|
|
|
|
assert_eq!(
|
|
node_a.peer_count(),
|
|
1,
|
|
"node_a should promote node_b after handoff"
|
|
);
|
|
assert_eq!(
|
|
node_b.peer_count(),
|
|
1,
|
|
"node_b should promote node_a after receiving msg1"
|
|
);
|
|
assert!(node_a.get_peer(&peer_b_node_addr).unwrap().has_session());
|
|
assert!(node_b.get_peer(&peer_a_node_addr).unwrap().has_session());
|
|
|
|
for (_, transport) in node_a.transports.iter_mut() {
|
|
transport.stop().await.ok();
|
|
}
|
|
for (_, transport) in node_b.transports.iter_mut() {
|
|
transport.stop().await.ok();
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_failed_adopted_traversal_cleans_up_transport() {
|
|
let mut node = make_node();
|
|
let (packet_tx, packet_rx) = packet_channel(64);
|
|
node.packet_tx = Some(packet_tx);
|
|
node.packet_rx = Some(packet_rx);
|
|
node.state = NodeState::Running;
|
|
node.index_allocator = IndexAllocator::with_max_attempts(0);
|
|
|
|
let peer = make_node();
|
|
let adopted_socket = std::net::UdpSocket::bind("127.0.0.1:0").unwrap();
|
|
let handoff = EstablishedTraversal::new(
|
|
"sess-fail",
|
|
peer.npub(),
|
|
"127.0.0.1:9".parse().unwrap(),
|
|
adopted_socket,
|
|
)
|
|
.with_transport_name("nostr-punched");
|
|
|
|
let result = node.adopt_established_traversal(handoff).await;
|
|
assert!(
|
|
result.is_err(),
|
|
"handoff should fail when handshake setup cannot allocate a session index"
|
|
);
|
|
assert!(
|
|
node.transports.is_empty(),
|
|
"failed handoff should remove the adopted transport"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_third_peer_can_handshake_via_adopted_transport_socket() {
|
|
let mut node_a = make_node(); // Existing traversal peer (Alice)
|
|
let mut node_b = make_node(); // Node with adopted socket (Bob)
|
|
let mut node_c = make_node(); // New peer onboarding via Bob socket (Colin)
|
|
|
|
let transport_id_a = TransportId::new(1);
|
|
let transport_id_c = TransportId::new(1);
|
|
let udp_config = UdpConfig {
|
|
bind_addr: Some("127.0.0.1:0".to_string()),
|
|
mtu: Some(1280),
|
|
..Default::default()
|
|
};
|
|
|
|
let (packet_tx_a, packet_rx_a) = packet_channel(64);
|
|
let (packet_tx_b, packet_rx_b) = packet_channel(64);
|
|
let (packet_tx_c, packet_rx_c) = packet_channel(64);
|
|
|
|
node_a.packet_tx = Some(packet_tx_a.clone());
|
|
node_a.packet_rx = Some(packet_rx_a);
|
|
node_a.state = NodeState::Running;
|
|
|
|
node_b.packet_tx = Some(packet_tx_b.clone());
|
|
node_b.packet_rx = Some(packet_rx_b);
|
|
node_b.state = NodeState::Running;
|
|
|
|
node_c.packet_tx = Some(packet_tx_c.clone());
|
|
node_c.packet_rx = Some(packet_rx_c);
|
|
node_c.state = NodeState::Running;
|
|
|
|
let mut transport_a = UdpTransport::new(transport_id_a, None, udp_config.clone(), packet_tx_a);
|
|
transport_a.start_async().await.unwrap();
|
|
let addr_a = transport_a.local_addr().unwrap();
|
|
node_a
|
|
.transports
|
|
.insert(transport_id_a, TransportHandle::Udp(transport_a));
|
|
|
|
// Bob adopts a traversal socket already "established" to Alice.
|
|
let adopted_socket = std::net::UdpSocket::bind("127.0.0.1:0").unwrap();
|
|
let handoff = EstablishedTraversal::new("sess-existing", node_a.npub(), addr_a, adopted_socket)
|
|
.with_transport_name("nostr-nat");
|
|
let handoff_result = node_b.adopt_established_traversal(handoff).await.unwrap();
|
|
|
|
// Drive Alice/Bob handshake manually (msg1 -> msg2).
|
|
let mut rx_a = node_a.packet_rx.take().expect("node_a packet_rx");
|
|
let mut rx_b = node_b.packet_rx.take().expect("node_b packet_rx");
|
|
|
|
let pkt_at_a = timeout(Duration::from_secs(1), rx_a.recv())
|
|
.await
|
|
.expect("timeout waiting for Bob->Alice msg1")
|
|
.expect("node_a channel closed");
|
|
assert_eq!(pkt_at_a.data[0] & 0x0f, PHASE_MSG1);
|
|
node_a.handle_msg1(pkt_at_a).await;
|
|
|
|
let pkt_at_b = timeout(Duration::from_secs(1), rx_b.recv())
|
|
.await
|
|
.expect("timeout waiting for Alice->Bob msg2")
|
|
.expect("node_b channel closed");
|
|
assert_eq!(pkt_at_b.data[0] & 0x0f, PHASE_MSG2);
|
|
node_b.handle_msg2(pkt_at_b).await;
|
|
|
|
let node_a_addr = *PeerIdentity::from_pubkey_full(node_a.identity.pubkey_full()).node_addr();
|
|
assert!(
|
|
node_b.get_peer(&node_a_addr).is_some(),
|
|
"node_b should first be connected to node_a via adopted transport"
|
|
);
|
|
|
|
// Start Colin UDP transport and connect to Bob's adopted socket address.
|
|
let mut transport_c = UdpTransport::new(transport_id_c, None, udp_config, packet_tx_c);
|
|
transport_c.start_async().await.unwrap();
|
|
let addr_c = transport_c.local_addr().unwrap();
|
|
node_c
|
|
.transports
|
|
.insert(transport_id_c, TransportHandle::Udp(transport_c));
|
|
|
|
let peer_b_identity = PeerIdentity::from_pubkey_full(node_b.identity.pubkey_full());
|
|
let adopted_addr = TransportAddr::from_string(&handoff_result.local_addr.to_string());
|
|
node_c
|
|
.initiate_connection(transport_id_c, adopted_addr, peer_b_identity)
|
|
.await
|
|
.unwrap();
|
|
|
|
// Drive Bob/Colin handshake manually (msg1 -> msg2).
|
|
let mut rx_c = node_c.packet_rx.take().expect("node_c packet_rx");
|
|
|
|
let deadline = tokio::time::Instant::now() + Duration::from_secs(1);
|
|
let pkt_at_b = loop {
|
|
let pkt = timeout_at(deadline, rx_b.recv())
|
|
.await
|
|
.expect("timeout waiting for Colin->Bob msg1")
|
|
.expect("node_b channel closed");
|
|
if pkt.remote_addr.as_str() == Some(&addr_c.to_string())
|
|
&& pkt.data.first().map(|b| b & 0x0f) == Some(PHASE_MSG1)
|
|
{
|
|
break pkt;
|
|
}
|
|
};
|
|
node_b.handle_msg1(pkt_at_b).await;
|
|
|
|
let deadline = tokio::time::Instant::now() + Duration::from_secs(1);
|
|
let pkt_at_c = loop {
|
|
let pkt = timeout_at(deadline, rx_c.recv())
|
|
.await
|
|
.expect("timeout waiting for Bob->Colin msg2")
|
|
.expect("node_c channel closed");
|
|
if pkt.data.first().map(|b| b & 0x0f) == Some(PHASE_MSG2) {
|
|
break pkt;
|
|
}
|
|
};
|
|
node_c.handle_msg2(pkt_at_c).await;
|
|
|
|
let node_c_addr = *PeerIdentity::from_pubkey_full(node_c.identity.pubkey_full()).node_addr();
|
|
assert!(
|
|
node_b.get_peer(&node_c_addr).is_some(),
|
|
"node_b should promote node_c when node_c handshakes via adopted socket"
|
|
);
|
|
|
|
for (_, transport) in node_a.transports.iter_mut() {
|
|
transport.stop().await.ok();
|
|
}
|
|
for (_, transport) in node_b.transports.iter_mut() {
|
|
transport.stop().await.ok();
|
|
}
|
|
for (_, transport) in node_c.transports.iter_mut() {
|
|
transport.stop().await.ok();
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_adopted_udp_inherits_mtu_from_single_primary_config() {
|
|
let mut node = make_node();
|
|
node.config.transports.udp = TransportInstances::Single(UdpConfig {
|
|
mtu: Some(1500),
|
|
..Default::default()
|
|
});
|
|
|
|
let (packet_tx, packet_rx) = packet_channel(64);
|
|
node.packet_tx = Some(packet_tx);
|
|
node.packet_rx = Some(packet_rx);
|
|
node.state = NodeState::Running;
|
|
|
|
let peer = make_node();
|
|
let adopted_socket = std::net::UdpSocket::bind("127.0.0.1:0").unwrap();
|
|
let handoff = EstablishedTraversal::new(
|
|
"sess-inherit-single",
|
|
peer.npub(),
|
|
"127.0.0.1:9".parse().unwrap(),
|
|
adopted_socket,
|
|
);
|
|
|
|
let result = node.adopt_established_traversal(handoff).await.unwrap();
|
|
let adopted = node
|
|
.get_transport(&result.transport_id)
|
|
.expect("adopted transport present");
|
|
assert_eq!(
|
|
adopted.mtu(),
|
|
1500,
|
|
"adopted UDP transport should inherit MTU from the primary [transports.udp] config",
|
|
);
|
|
|
|
for (_, transport) in node.transports.iter_mut() {
|
|
transport.stop().await.ok();
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_adopted_udp_inherits_mtu_from_named_primary_config() {
|
|
let mut node = make_node();
|
|
let mut named = HashMap::new();
|
|
named.insert(
|
|
"primary".to_string(),
|
|
UdpConfig {
|
|
mtu: Some(1500),
|
|
..Default::default()
|
|
},
|
|
);
|
|
named.insert(
|
|
"secondary".to_string(),
|
|
UdpConfig {
|
|
mtu: Some(1280),
|
|
..Default::default()
|
|
},
|
|
);
|
|
node.config.transports.udp = TransportInstances::Named(named);
|
|
|
|
let (packet_tx, packet_rx) = packet_channel(64);
|
|
node.packet_tx = Some(packet_tx);
|
|
node.packet_rx = Some(packet_rx);
|
|
node.state = NodeState::Running;
|
|
|
|
let peer = make_node();
|
|
let adopted_socket = std::net::UdpSocket::bind("127.0.0.1:0").unwrap();
|
|
let handoff = EstablishedTraversal::new(
|
|
"sess-inherit-named",
|
|
peer.npub(),
|
|
"127.0.0.1:9".parse().unwrap(),
|
|
adopted_socket,
|
|
)
|
|
.with_transport_name("primary");
|
|
|
|
let result = node.adopt_established_traversal(handoff).await.unwrap();
|
|
let adopted = node
|
|
.get_transport(&result.transport_id)
|
|
.expect("adopted transport present");
|
|
assert_eq!(
|
|
adopted.mtu(),
|
|
1500,
|
|
"adopted UDP transport should inherit MTU from the named [transports.udp.<name>] config matching transport_name",
|
|
);
|
|
|
|
for (_, transport) in node.transports.iter_mut() {
|
|
transport.stop().await.ok();
|
|
}
|
|
}
|