mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-09 08:14:42 +00:00
Remove the duplicated immutable fields (config, identity, startup_epoch, started_at, is_leaf_only, max_connections/peers/links) from the Node struct so the Arc<NodeContext> bundle is the single source of truth. Previously Node owned these fields and a parallel context copy, kept in lockstep by rebuild_context() at every mutation site — pure overhead that existed only because of the duplication. - Replace rebuild_context() with replace_context(): a clone-edit-swap of the whole Arc. The per-instance context stays immutable; mutation swaps the Arc. This is the sole runtime mutation path (constructors, leaf_only, update_peers). - Add Copy-returning accessors startup_epoch() and max_connections()/ max_peers()/max_links(); migrate the remaining direct field readers onto the accessors. node_addr()/npub()/Debug now read identity/is_leaf_only from the context. - update_peers reads the pre-update peer set from the live context Arc before building a fresh Config + context and swapping — preserving the read-before-write ordering its mutation-window test depends on. - Remove the test-only set_max_* setters; tests set the limits on Config at construction instead (new make_node_with_max_peers/links helpers). - Add a ci-local guard that fails if the Node struct re-declares a bundled field, so the single-store invariant can't silently regress. cargo test --lib 1291/0; clippy -D warnings and release build clean.
181 lines
6.1 KiB
Rust
181 lines
6.1 KiB
Rust
use super::*;
|
|
use crate::ReceivedPacket;
|
|
use crate::node::acl::PeerAclReloader;
|
|
use crate::node::reloadable::HostMapReloadable;
|
|
use crate::node::wire::{build_msg1, build_msg2};
|
|
use crate::upper::hosts::HostMap;
|
|
use crate::utils::index::SessionIndex;
|
|
use std::path::PathBuf;
|
|
use std::time::Duration;
|
|
|
|
fn make_acl_node() -> (tempfile::TempDir, Node) {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let mut node = Node::new(Config::new()).unwrap();
|
|
node.peer_acl = PeerAclReloader::with_paths(
|
|
dir.path().join("peers.allow"),
|
|
dir.path().join("peers.deny"),
|
|
);
|
|
(dir, node)
|
|
}
|
|
|
|
fn allow_path(dir: &tempfile::TempDir) -> PathBuf {
|
|
dir.path().join("peers.allow")
|
|
}
|
|
|
|
fn deny_path(dir: &tempfile::TempDir) -> PathBuf {
|
|
dir.path().join("peers.deny")
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_outbound_connect_denied_by_denylist() {
|
|
let (dir, mut node) = make_acl_node();
|
|
let denied = Identity::generate();
|
|
std::fs::write(deny_path(&dir), format!("{}\n", denied.npub())).unwrap();
|
|
node.reload_peer_acl().await;
|
|
|
|
let result = node
|
|
.initiate_connection(
|
|
TransportId::new(1),
|
|
TransportAddr::from_string("127.0.0.1:9000"),
|
|
PeerIdentity::from_pubkey_full(denied.pubkey_full()),
|
|
)
|
|
.await;
|
|
|
|
assert!(matches!(result, Err(NodeError::AccessDenied(_))));
|
|
assert_eq!(node.link_count(), 0);
|
|
assert_eq!(node.connection_count(), 0);
|
|
assert_eq!(node.peer_count(), 0);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_inbound_msg1_denied_by_acl() {
|
|
let (dir, mut node_b) = make_acl_node();
|
|
let node_a = make_node();
|
|
|
|
std::fs::write(deny_path(&dir), format!("{}\n", node_a.npub())).unwrap();
|
|
node_b.reload_peer_acl().await;
|
|
|
|
let peer_b_identity = PeerIdentity::from_pubkey_full(node_b.identity().pubkey_full());
|
|
let mut conn_a = PeerConnection::outbound(LinkId::new(1), peer_b_identity, 1000);
|
|
let noise_msg1 = conn_a
|
|
.start_handshake(node_a.identity().keypair(), node_a.startup_epoch(), 1000)
|
|
.unwrap();
|
|
let wire_msg1 = build_msg1(SessionIndex::new(7), &noise_msg1);
|
|
let packet = ReceivedPacket::with_timestamp(
|
|
TransportId::new(1),
|
|
TransportAddr::from_string("127.0.0.1:5000"),
|
|
wire_msg1,
|
|
1000,
|
|
);
|
|
|
|
node_b.handle_msg1(packet).await;
|
|
|
|
assert_eq!(node_b.peer_count(), 0);
|
|
assert_eq!(node_b.connection_count(), 0);
|
|
assert_eq!(node_b.link_count(), 0);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_outbound_msg2_denied_after_acl_reload() {
|
|
let (dir, mut node_a) = make_acl_node();
|
|
let node_b = make_node();
|
|
let transport_id = TransportId::new(1);
|
|
let remote_addr = TransportAddr::from_string("127.0.0.1:5001");
|
|
let peer_b_identity = PeerIdentity::from_pubkey_full(node_b.identity().pubkey_full());
|
|
|
|
let link_id_a = node_a.allocate_link_id();
|
|
let mut conn_a = PeerConnection::outbound(link_id_a, peer_b_identity, 1000);
|
|
let our_index_a = node_a.index_allocator.allocate().unwrap();
|
|
let noise_msg1 = conn_a
|
|
.start_handshake(node_a.identity().keypair(), node_a.startup_epoch(), 1000)
|
|
.unwrap();
|
|
conn_a.set_our_index(our_index_a);
|
|
conn_a.set_transport_id(transport_id);
|
|
conn_a.set_source_addr(remote_addr.clone());
|
|
|
|
let link_a = Link::connectionless(
|
|
link_id_a,
|
|
transport_id,
|
|
remote_addr.clone(),
|
|
LinkDirection::Outbound,
|
|
Duration::from_millis(100),
|
|
);
|
|
node_a.links.insert(link_id_a, link_a);
|
|
node_a
|
|
.addr_to_link
|
|
.insert((transport_id, remote_addr.clone()), link_id_a);
|
|
node_a.connections.insert(link_id_a, conn_a);
|
|
node_a
|
|
.pending_outbound
|
|
.insert((transport_id, our_index_a.as_u32()), link_id_a);
|
|
|
|
let mut conn_b = PeerConnection::inbound(LinkId::new(2), 1000);
|
|
let responder_epoch = [0x11; 8];
|
|
let noise_msg2 = conn_b
|
|
.receive_handshake_init(
|
|
node_b.identity().keypair(),
|
|
responder_epoch,
|
|
&noise_msg1,
|
|
1000,
|
|
)
|
|
.unwrap();
|
|
let our_index_b = SessionIndex::new(9);
|
|
let wire_msg2 = build_msg2(our_index_b, our_index_a, &noise_msg2);
|
|
|
|
std::fs::write(deny_path(&dir), format!("{}\n", node_b.npub())).unwrap();
|
|
assert!(node_a.reload_peer_acl().await);
|
|
|
|
let packet = ReceivedPacket::with_timestamp(transport_id, remote_addr, wire_msg2, 1100);
|
|
node_a.handle_msg2(packet).await;
|
|
|
|
assert_eq!(node_a.peer_count(), 0);
|
|
assert_eq!(node_a.connection_count(), 0);
|
|
assert_eq!(node_a.link_count(), 0);
|
|
assert!(node_a.pending_outbound.is_empty());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_host_map_hot_reloads_from_tick() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let hosts_path = dir.path().join("hosts");
|
|
|
|
let mut node = Node::new(Config::new()).unwrap();
|
|
node.host_map = HostMapReloadable::new(HostMap::new(), hosts_path.clone());
|
|
|
|
let peer = Identity::generate();
|
|
let peer_addr = *PeerIdentity::from_pubkey_full(peer.pubkey_full()).node_addr();
|
|
|
|
// No hosts file yet: the display name is not the alias.
|
|
assert_ne!(node.peer_display_name(&peer_addr), "gateway");
|
|
assert!(!node.reload_host_map().await);
|
|
|
|
// Write a hosts entry and let the tick-driven reload pick it up.
|
|
std::thread::sleep(Duration::from_millis(50));
|
|
std::fs::write(&hosts_path, format!("gateway {}\n", peer.npub())).unwrap();
|
|
|
|
assert!(node.reload_host_map().await);
|
|
assert_eq!(node.peer_display_name(&peer_addr), "gateway");
|
|
|
|
// No further change: reload reports nothing replaced.
|
|
assert!(!node.reload_host_map().await);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_outbound_connect_not_denied_by_allowlist_miss() {
|
|
let (dir, mut node) = make_acl_node();
|
|
let denied = Identity::generate();
|
|
let allowed = Identity::generate();
|
|
std::fs::write(allow_path(&dir), format!("{}\n", allowed.npub())).unwrap();
|
|
node.reload_peer_acl().await;
|
|
|
|
let result = node
|
|
.initiate_connection(
|
|
TransportId::new(1),
|
|
TransportAddr::from_string("127.0.0.1:9000"),
|
|
PeerIdentity::from_pubkey_full(denied.pubkey_full()),
|
|
)
|
|
.await;
|
|
|
|
assert!(!matches!(result, Err(NodeError::AccessDenied(_))));
|
|
}
|