Files
brahman/crates/protocol/brahman-handshake/tests/network_libp2p.rs
T
sergio 550c98f275 refactor(monorepo): reorganización lógica + renames + SDDs + split CHANGELOG
Reorganización física de crates/:
- core/ (mezclaba 6 propósitos) se divide en protocol/, init/, runtime/, compat/
- shared/ (3 crates) se redistribuye en protocol/ e init/
- lapaloma (sub-módulo de ui_engine) se promueve a modules/pineal/

Renames de proyectos:
- shipote → shuma (runtime de sandboxes)
- nouser → akasha (explorador de Mónadas)
- yahweh → nahual (motor GPUI, antes ui_engine/)
- lapaloma → pineal (data-viz agnóstica)

Fraccionamiento UI → core agnóstico:
- vista-core (DeckState + snap, 175 LOC, 5 tests verdes)
- barra-core (Task + render_html + sanitize, 90 LOC, 5 tests verdes)
- vista-web y barra-web ahora son thin DOM bindings

Documentación nueva:
- 16 SDDs por subdirectorio (≤80 LOC c/u): protocol/init/runtime/compat
  + 10 módulos + apps/
- docs/STATUS.md con cifras reales por proyecto
- docs/ROADMAP.md con plan a finalización (6 hitos, ~6-8 semanas)
- CHANGELOG.md particionado en docs/changelog/<proyecto>.md (7 buckets)

Automatización:
- scripts/reorg.py — script idempotente que: git mv directorios, renombra
  package names, recomputa path = refs, reescribe imports rust, actualiza
  workspace Cargo.toml. Soporta --dry-run.
- scripts/split-changelog.py — particiona CHANGELOG por componente.

Validación:
- cargo check --workspace pasa (124 crates + 2 nuevos cores).
- 10 tests adicionales (5 en vista-core + 5 en barra-core) verdes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 14:48:34 +00:00

526 lines
19 KiB
Rust

//! Test E2E: handshake brahman remoto sobre libp2p stream.
//!
//! Pipeline:
//! 1. Server: bind Unix socket (necesario aunque no lo use el cliente);
//! crear `BrahmanNet` y escuchar en `/ip4/127.0.0.1/tcp/0`;
//! montar `run_libp2p_accept_loop`.
//! 2. Client: crear su propio `BrahmanNet`; dial al multiaddr del
//! server; `connect_libp2p` con su Card; `ping`; `farewell`.
//! 3. Verificar: el server registró la sesión; sessions.len() == 1
//! durante la sesión, == 0 después del farewell.
use std::collections::BTreeSet;
use std::sync::Arc;
use std::time::Duration;
use brahman_broker::{Broker, BrokerConfig};
use brahman_card::{
ulid::Ulid, Card, CardKind, Lifecycle, Payload, Priority, Supervision,
CARD_SCHEMA_VERSION,
};
use brahman_handshake::identity::{Identity, DEFAULT_SESSION_TTL};
use brahman_handshake::network::{connect_libp2p, connect_libp2p_with_cert, run_libp2p_accept_loop};
use brahman_handshake::peer_policy::PeerPolicy;
use brahman_handshake::server::{Server, ServerConfig};
use brahman_net::{BrahmanNet, Keypair, Multiaddr, PeerId, Protocol};
use tempfile::TempDir;
use tokio::sync::Mutex;
fn sample_card(label: &str) -> Card {
Card {
schema_version: CARD_SCHEMA_VERSION,
id: Ulid::new(),
label: label.into(),
provides: BTreeSet::new(),
requires: BTreeSet::new(),
permissions: Default::default(),
soma: Default::default(),
payload: Payload::Virtual,
supervision: Supervision::OneShot,
lifecycle: Lifecycle::default(),
priority: Priority::default(),
kind: CardKind::Ente,
..Default::default()
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn libp2p_handshake_roundtrip() {
// ---- Server side ----
let tmp = TempDir::new().unwrap();
let unix_socket = tmp.path().join("brahman-init.sock");
let broker = Arc::new(Mutex::new(Broker::new(BrokerConfig::default())));
let server = Arc::new(
Server::bind(
&unix_socket,
ServerConfig {
init_attached: true,
broker: Some(broker.clone()),
net: None,
policy: None,
},
)
.unwrap(),
);
let sessions = server.sessions();
let server_net = Arc::new(BrahmanNet::new().unwrap());
let server_peer_id = server_net.peer_id;
// Listen on a random TCP port.
let listen_addr: Multiaddr = "/ip4/127.0.0.1/tcp/0".parse().unwrap();
let actual_addr = server_net.listen(listen_addr).await;
// Inject the libp2p PeerId into the multiaddr so the client knows
// who to dial.
let mut full_addr = actual_addr.clone();
full_addr.push(Protocol::P2p(server_peer_id));
// Spawn the libp2p accept loop.
tokio::spawn(run_libp2p_accept_loop(server.clone(), server_net.clone()));
// ---- Client side ----
let client_net = BrahmanNet::new().unwrap();
client_net.dial(full_addr.clone());
// Pequeña espera para que el dial conecte. En un entorno real el
// caller usaría un mecanismo de barrier, pero para tests un sleep
// corto es suficiente y deterministic en localhost.
tokio::time::sleep(Duration::from_millis(200)).await;
let card = sample_card("test.remote_ente");
let client_kp = client_net.keypair();
let mut client = connect_libp2p(&client_net, server_peer_id, card, None, &client_kp)
.await
.expect("handshake remoto debería completar");
// Verificación: el server vio la sesión.
{
let s = sessions.lock().await;
assert_eq!(s.len(), 1, "una sesión registrada");
let resolved = s.values().next().unwrap();
assert_eq!(resolved.card.label, "test.remote_ente");
}
// Ping roundtrip.
let ts = client.ping().await.expect("ping debería responder");
assert!(ts > 0, "timestamp del Pong > 0");
// Farewell limpio.
client.farewell().await.expect("farewell debería completar");
// Tras el farewell, el cleanup remueve la sesión.
// Damos un tick para que el handler procese el frame.
tokio::time::sleep(Duration::from_millis(100)).await;
{
let s = sessions.lock().await;
assert_eq!(s.len(), 0, "sesión removida tras farewell");
}
// peer_id no usado aquí, pero validamos que la API existe.
let _ = PeerId::random();
}
/// Fase 3 negativo: el cliente intenta firmar el Hello con una keypair
/// distinta a la del peer libp2p. El server (que verifica que la
/// public key del Hello derive al peer_id autenticado por Noise) debe
/// rechazar con `Unauthorized`.
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn libp2p_handshake_rejects_mismatched_signing_key() {
let tmp = TempDir::new().unwrap();
let unix_socket = tmp.path().join("brahman-init.sock");
let server = Arc::new(
Server::bind(
&unix_socket,
ServerConfig {
init_attached: true,
broker: None,
net: None,
policy: None,
},
)
.unwrap(),
);
let sessions = server.sessions();
let server_net = Arc::new(BrahmanNet::new().unwrap());
let server_peer = server_net.peer_id;
let listen_addr: Multiaddr = "/ip4/127.0.0.1/tcp/0".parse().unwrap();
let actual = server_net.listen(listen_addr).await;
let mut full = actual.clone();
full.push(Protocol::P2p(server_peer));
tokio::spawn(run_libp2p_accept_loop(server.clone(), server_net.clone()));
let client_net = BrahmanNet::new().unwrap();
client_net.dial(full);
tokio::time::sleep(Duration::from_millis(200)).await;
// Keypair fraudulenta: NO es la del client_net.
let evil_keypair = Keypair::generate_ed25519();
let card = sample_card("test.evil");
let result = connect_libp2p(&client_net, server_peer, card, None, &evil_keypair).await;
assert!(
result.is_err(),
"handshake con keypair fraudulenta debe fallar"
);
// Sanidad: ninguna sesión registrada.
let s = sessions.lock().await;
assert_eq!(s.len(), 0, "no debería haber sesión registrada");
}
/// Allowlist gate: A configura `allowlist = [client_authorized_peer]`.
/// Un cliente con peer_id en la lista pasa el handshake; otro con
/// peer_id distinto es rechazado con `Unauthorized` ANTES de la
/// verificación de firma (la allowlist se chequea primero, es más
/// barata).
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn libp2p_handshake_allowlist_admits_listed_rejects_others() {
// Pre-generamos las dos identidades cliente para que A pueda
// construir la allowlist conociendo cuál es la "permitida".
let allowed_kp = Keypair::generate_ed25519();
let allowed_peer = allowed_kp.public().to_peer_id();
let denied_kp = Keypair::generate_ed25519();
// (denied_peer no se necesita para la lista — sólo para clarity)
let _ = denied_kp.public().to_peer_id();
// ---- Server con allowlist activa ----
let tmp = TempDir::new().unwrap();
let unix_socket = tmp.path().join("brahman-init.sock");
let server = Arc::new(
Server::bind(
&unix_socket,
ServerConfig {
init_attached: true,
broker: None,
net: None,
policy: Some(PeerPolicy::from_sets(
Some([allowed_peer].into_iter().collect()),
std::collections::BTreeSet::new(),
)),
},
)
.unwrap(),
);
let sessions = server.sessions();
let server_net = Arc::new(BrahmanNet::new().unwrap());
let server_peer = server_net.peer_id;
let actual = server_net.listen("/ip4/127.0.0.1/tcp/0".parse().unwrap()).await;
let mut full = actual.clone();
full.push(Protocol::P2p(server_peer));
tokio::spawn(run_libp2p_accept_loop(server.clone(), server_net.clone()));
// ---- Cliente PERMITIDO ----
let allowed_net = BrahmanNet::with_keypair(allowed_kp.clone()).unwrap();
allowed_net.dial(full.clone());
tokio::time::sleep(Duration::from_millis(200)).await;
let card_ok = sample_card("test.allowed");
let mut allowed_client = connect_libp2p(&allowed_net, server_peer, card_ok, None, &allowed_kp)
.await
.expect("peer en allowlist debe pasar");
{
let s = sessions.lock().await;
assert_eq!(s.len(), 1, "sesión del peer permitido registrada");
}
allowed_client.farewell().await.ok();
tokio::time::sleep(Duration::from_millis(100)).await;
// ---- Cliente DENEGADO ----
let denied_net = BrahmanNet::with_keypair(denied_kp.clone()).unwrap();
denied_net.dial(full.clone());
tokio::time::sleep(Duration::from_millis(200)).await;
let card_no = sample_card("test.denied");
let result = connect_libp2p(&denied_net, server_peer, card_no, None, &denied_kp).await;
assert!(
result.is_err(),
"peer fuera de allowlist debe ser rechazado, got: {:?}",
result.is_ok()
);
{
let s = sessions.lock().await;
assert_eq!(s.len(), 0, "ninguna sesión adicional registrada tras intento denegado");
}
}
/// Denylist gate: A configura `policy` con un peer en la denylist.
/// Modo abierto para todo lo demás (sin allowlist), pero el peer
/// baneado es rechazado aún teniendo Ed25519 válida y peer_id que
/// derivaría limpio del Noise handshake.
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn libp2p_handshake_denylist_blocks_listed_peer() {
let banned_kp = Keypair::generate_ed25519();
let banned_peer = banned_kp.public().to_peer_id();
let other_kp = Keypair::generate_ed25519();
let tmp = TempDir::new().unwrap();
let unix_socket = tmp.path().join("brahman-init.sock");
let server = Arc::new(
Server::bind(
&unix_socket,
ServerConfig {
init_attached: true,
broker: None,
net: None,
policy: Some(PeerPolicy::from_sets(
None, // sin allowlist (abierto)
[banned_peer].into_iter().collect(),
)),
},
)
.unwrap(),
);
let sessions = server.sessions();
let server_net = Arc::new(BrahmanNet::new().unwrap());
let server_peer = server_net.peer_id;
let actual = server_net
.listen("/ip4/127.0.0.1/tcp/0".parse().unwrap())
.await;
let mut full = actual.clone();
full.push(Protocol::P2p(server_peer));
tokio::spawn(run_libp2p_accept_loop(server.clone(), server_net.clone()));
// Cliente baneado: connect debe fallar.
let banned_net = BrahmanNet::with_keypair(banned_kp.clone()).unwrap();
banned_net.dial(full.clone());
tokio::time::sleep(Duration::from_millis(200)).await;
let card_x = sample_card("test.banned");
let result = connect_libp2p(&banned_net, server_peer, card_x, None, &banned_kp).await;
assert!(
result.is_err(),
"peer en denylist debe ser rechazado, got Ok"
);
{
let s = sessions.lock().await;
assert_eq!(s.len(), 0, "el peer baneado no debería tener sesión");
}
// Cliente no-baneado pasa.
let other_net = BrahmanNet::with_keypair(other_kp.clone()).unwrap();
other_net.dial(full.clone());
tokio::time::sleep(Duration::from_millis(200)).await;
let card_ok = sample_card("test.other");
let mut other_client = connect_libp2p(&other_net, server_peer, card_ok, None, &other_kp)
.await
.expect("peer fuera de denylist debe pasar");
{
let s = sessions.lock().await;
assert_eq!(s.len(), 1, "sesión del peer no-baneado registrada");
}
other_client.farewell().await.ok();
}
/// Swarm-level deny via `PeerPolicy::attach_to_net`: cuando la deny
/// se aplica al swarm vía `block_list`, el peer baneado es rechazado
/// en el dial — la conexión TCP/Noise nunca completa, así que el
/// cliente nunca llega siquiera a mandar el Hello. Más eficiente que
/// el handshake-level deny.
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn swarm_level_deny_blocks_before_noise() {
let banned_kp = Keypair::generate_ed25519();
let banned_peer = banned_kp.public().to_peer_id();
let tmp = TempDir::new().unwrap();
let unix_socket = tmp.path().join("brahman-init.sock");
let policy = brahman_handshake::peer_policy::PeerPolicy::from_sets(
None,
[banned_peer].into_iter().collect(),
);
let server = Arc::new(
Server::bind(
&unix_socket,
ServerConfig {
init_attached: true,
broker: None,
net: None,
policy: Some(policy.clone()),
},
)
.unwrap(),
);
let server_net = Arc::new(BrahmanNet::new().unwrap());
let server_peer = server_net.peer_id;
// ATTACH: la deny se proyecta al swarm. Es lo nuevo de este
// commit — sin esta llamada, el deny seguiría aplicando sólo
// al nivel de handshake brahman (lo que también funciona pero
// gasta un round-trip Noise).
policy.attach_to_net(server_net.clone());
let actual = server_net
.listen("/ip4/127.0.0.1/tcp/0".parse().unwrap())
.await;
let mut full = actual.clone();
full.push(Protocol::P2p(server_peer));
tokio::spawn(run_libp2p_accept_loop(server.clone(), server_net.clone()));
// Cliente baneado intenta dial + handshake. Con swarm-level
// deny, la conexión libp2p ni siquiera completa: `connect_libp2p`
// falla con error de open_stream (peer inalcanzable / connection
// refused) en lugar del Unauthorized del handshake-level path.
let banned_net = BrahmanNet::with_keypair(banned_kp.clone()).unwrap();
banned_net.dial(full.clone());
let card = sample_card("test.swarm_banned");
// Timeout corto: si el block falla, el handshake completaría
// rápido en localhost. Si funciona, debería fallar el dial casi
// instantáneo o colgarse hasta el timeout.
let result = tokio::time::timeout(
Duration::from_secs(3),
connect_libp2p(&banned_net, server_peer, card, None, &banned_kp),
)
.await;
match result {
Ok(Ok(_)) => panic!("peer baneado a nivel swarm NO debería completar handshake"),
Ok(Err(e)) => {
// Esperado: error de transporte/stream, no de handshake.
tracing::info!(error = %e, "swarm-level deny rechazó como esperado");
}
Err(_) => {
// También aceptable: timeout porque el dial nunca completa.
tracing::info!("swarm-level deny → connect timeout (también OK)");
}
}
}
/// Multi-key identity: la propiedad fundamental que cierra el
/// proyecto. El cliente B tiene una identity master estable; el
/// server A le permite el master_peer en allowlist. B se conecta con
/// **session1**; pasa. B "rota": genera **session2** distinta, emite
/// un nuevo cert con la misma identity, se conecta de nuevo. Pasa
/// también — sin que A toque su allowlist.
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn identity_cert_allows_session_rotation_without_policy_change() {
// Master de B (estable, persistente).
let master_kp = Keypair::generate_ed25519();
let master_peer = master_kp.public().to_peer_id();
let identity = Identity::from_keypair(master_kp);
// A configura policy: allowlist con master_peer (NO sessions).
let tmp = TempDir::new().unwrap();
let unix_socket = tmp.path().join("brahman-init.sock");
let server = Arc::new(
Server::bind(
&unix_socket,
ServerConfig {
init_attached: true,
broker: None,
net: None,
policy: Some(PeerPolicy::from_sets(
Some([master_peer].into_iter().collect()),
std::collections::BTreeSet::new(),
)),
},
)
.unwrap(),
);
let sessions = server.sessions();
let server_net = Arc::new(BrahmanNet::new().unwrap());
let server_peer = server_net.peer_id;
let actual = server_net
.listen("/ip4/127.0.0.1/tcp/0".parse().unwrap())
.await;
let mut full = actual.clone();
full.push(Protocol::P2p(server_peer));
tokio::spawn(run_libp2p_accept_loop(server.clone(), server_net.clone()));
// ---- Conexión 1: session1 ----
let session1_kp = Keypair::generate_ed25519();
let cert1 = identity
.issue_session_cert(&session1_kp, DEFAULT_SESSION_TTL)
.unwrap();
let net1 = BrahmanNet::with_keypair(session1_kp.clone()).unwrap();
net1.dial(full.clone());
tokio::time::sleep(Duration::from_millis(200)).await;
let mut client1 = connect_libp2p_with_cert(
&net1,
server_peer,
sample_card("test.session1"),
None,
&session1_kp,
cert1,
)
.await
.expect("session1 con cert válido del master allowlisted debe pasar");
{
let s = sessions.lock().await;
assert_eq!(s.len(), 1, "session1 registrada");
}
client1.farewell().await.ok();
tokio::time::sleep(Duration::from_millis(100)).await;
// ---- ROTACIÓN: session2 distinta, mismo master ----
let session2_kp = Keypair::generate_ed25519();
assert_ne!(
session1_kp.public().to_peer_id(),
session2_kp.public().to_peer_id(),
"test inválido si las sessions son iguales"
);
let cert2 = identity
.issue_session_cert(&session2_kp, DEFAULT_SESSION_TTL)
.unwrap();
let net2 = BrahmanNet::with_keypair(session2_kp.clone()).unwrap();
net2.dial(full.clone());
tokio::time::sleep(Duration::from_millis(200)).await;
let mut client2 = connect_libp2p_with_cert(
&net2,
server_peer,
sample_card("test.session2"),
None,
&session2_kp,
cert2,
)
.await
.expect(
"session2 (rotada) con cert del MISMO master debe pasar sin tocar allowlist",
);
{
let s = sessions.lock().await;
assert_eq!(s.len(), 1, "session2 registrada");
}
client2.farewell().await.ok();
// Sanity: una session sin cert (path Fase 3) cuyo session_peer_id
// NO está en la allowlist (porque la allowlist tiene master, no
// sessions) DEBE ser rechazada.
let session_other = Keypair::generate_ed25519();
let net_other = BrahmanNet::with_keypair(session_other.clone()).unwrap();
net_other.dial(full.clone());
tokio::time::sleep(Duration::from_millis(200)).await;
let result = connect_libp2p(
&net_other,
server_peer,
sample_card("test.no_cert"),
None,
&session_other,
)
.await;
assert!(
result.is_err(),
"sin cert, session_peer_id (no listado) debe ser rechazado"
);
}