feat(brahman-handshake): Fase 1 — handshake brahman sobre stream libp2p
Segundo paso del plan "el encuentro entre Entes no se restringe a
local". El protocolo brahman (Hello / HelloAck / Ping / Pong /
MatchEvent / Farewell, frames postcard length-prefixed) ahora tambien
viaja sobre streams libp2p de la malla brahman-net — el mismo Init
acepta sesiones por Unix socket Y por libp2p indistintamente, y un
consumer remoto puede dial-ar al multiaddr y completar handshake.
Cambios:
- Session<S> y Client<S> genericos: ambos dejan de estar atados a
UnixStream y pasan a ser genericos sobre S: AsyncRead + AsyncWrite
+ Unpin + Send + 'static. El path Unix queda como
Client = Client<UnixStream> (default generico). Constructores
nuevos: Server::session_from_stream(stream),
Client::connect_with_stream(stream, card, wit).
- Refactor del post-handshake con split: tokio::select! sobre
&mut self.stream requeria S: Sync indirectamente, y libp2p::Stream
no es Sync. Reemplazado por tokio::io::split(stream) -> reader loop
principal + writer task separada que drena el push channel. Writer
compartido bajo Arc<Mutex<WriteHalf<S>>> para serializar Pong/Error
inline con los MatchEvents pusheados. Cleanup garantizado en todas
las ramas. La logica del post-handshake migra a funciones libres
(run_post_handshake, handle_inbound_frame, cleanup,
broadcast_match_diffs, do_handshake, register_session,
validate_hello).
- Nuevo modulo brahman-handshake::network:
- BRAHMAN_HANDSHAKE_PROTOCOL = "/brahman/handshake/1.0.0"
- LibP2pHandshakeStream = Compat<libp2p::Stream>
- run_libp2p_accept_loop(server, net): accept loop que delega cada
stream entrante a session_from_stream(stream.compat()). Sesiones
libp2p y Unix conviven en el mismo Server — comparten broker,
push table, last_matches.
- connect_libp2p(net, peer, card, wit): abre stream libp2p al peer
y arranca handshake.
- NetworkError tipado.
Deps: brahman-handshake gana brahman-net, futures, tokio-util.
brahman-net re-exporta Multiaddr, PeerId, Stream, StreamProtocol,
Protocol, OpenStreamError para que callers no necesiten dep directa
a libp2p.
Tests: 9 verdes en el path Unix (sin regresion). Nuevo
tests/network_libp2p.rs E2E que arma server con BrahmanNet, hace
listen TCP, monta accept loop; cliente con su propio BrahmanNet
dial-ea al peer_id, completa handshake remoto, ping, farewell.
Verifica que la sesion se registro durante la conversacion y se
removio tras farewell.
This commit is contained in:
@@ -6,6 +6,61 @@ ratio/diff ver `git show <sha>`.
|
|||||||
|
|
||||||
## 2026-05-09
|
## 2026-05-09
|
||||||
|
|
||||||
|
### feat(brahman-handshake): Fase 1 — handshake brahman sobre stream libp2p
|
||||||
|
Segundo paso del plan "el encuentro entre Entes no se restringe a
|
||||||
|
local". El protocolo brahman (Hello / HelloAck / Ping / Pong /
|
||||||
|
MatchEvent / Farewell, frames postcard length-prefixed) ahora también
|
||||||
|
viaja sobre streams libp2p de la malla `brahman-net` — el mismo Init
|
||||||
|
acepta sesiones por Unix socket Y por libp2p indistintamente, y un
|
||||||
|
consumer remoto puede dial-ar al multiaddr y completar handshake.
|
||||||
|
|
||||||
|
Cambios:
|
||||||
|
- **`Session<S>` y `Client<S>` genéricos**: ambos dejan de estar atados
|
||||||
|
a `UnixStream` y pasan a ser genéricos sobre `S: AsyncRead +
|
||||||
|
AsyncWrite + Unpin + Send + 'static`. El path Unix queda como
|
||||||
|
`Client = Client<UnixStream>` (default genérico). Constructores
|
||||||
|
nuevos: `Server::session_from_stream(stream)`,
|
||||||
|
`Client::connect_with_stream(stream, card, wit)`.
|
||||||
|
- **Refactor del post-handshake con split**: `tokio::select!` sobre
|
||||||
|
`&mut self.stream` requería `S: Sync` indirectamente, y
|
||||||
|
`libp2p::Stream` no es Sync. Reemplazado por
|
||||||
|
`tokio::io::split(stream)` → reader loop principal + writer task
|
||||||
|
separada que drena el push channel. Writer compartido bajo
|
||||||
|
`Arc<Mutex<WriteHalf<S>>>` para serializar Pong/Error inline con
|
||||||
|
los MatchEvents pusheados. Cleanup garantizado en todas las ramas.
|
||||||
|
La lógica del post-handshake migra a funciones libres
|
||||||
|
(`run_post_handshake`, `handle_inbound_frame`, `cleanup`,
|
||||||
|
`broadcast_match_diffs`, `do_handshake`, `register_session`,
|
||||||
|
`validate_hello`).
|
||||||
|
- **Nuevo módulo `brahman-handshake::network`**:
|
||||||
|
- `BRAHMAN_HANDSHAKE_PROTOCOL = "/brahman/handshake/1.0.0"`.
|
||||||
|
- `LibP2pHandshakeStream = Compat<libp2p::Stream>` (alias del
|
||||||
|
stream una vez convertido al mundo `tokio::io`).
|
||||||
|
- `run_libp2p_accept_loop(server, net)`: bucle accept sobre el
|
||||||
|
protocolo que delega cada stream entrante a una `Session`
|
||||||
|
construida vía `server.session_from_stream(stream.compat())`.
|
||||||
|
Sesiones libp2p y Unix conviven en el mismo `Server` —
|
||||||
|
comparten broker, push table, last_matches.
|
||||||
|
- `connect_libp2p(net, peer, card, wit)`: abre stream libp2p al
|
||||||
|
`peer` y arranca handshake.
|
||||||
|
- `NetworkError` tipado (`OpenStream`, `Handshake`, `AcceptStream`).
|
||||||
|
|
||||||
|
Deps: `brahman-handshake` gana `brahman-net`, `futures`, `tokio-util`.
|
||||||
|
`brahman-net` re-exporta `Multiaddr`, `PeerId`, `Stream`,
|
||||||
|
`StreamProtocol`, `Protocol`, `OpenStreamError` para que callers no
|
||||||
|
necesiten dep directa a libp2p.
|
||||||
|
|
||||||
|
Tests:
|
||||||
|
- 9 tests unit + integration verdes (sin regresión en el path Unix).
|
||||||
|
- Nuevo `tests/network_libp2p.rs`: test E2E que arma server con
|
||||||
|
Unix socket + BrahmanNet, hace listen TCP, monta el accept loop;
|
||||||
|
cliente con su propio BrahmanNet dial-ea al peer_id, completa
|
||||||
|
handshake remoto, pinguea, farewell. Verifica que la sesión se
|
||||||
|
registró durante la conversación y se removió tras farewell.
|
||||||
|
|
||||||
|
Próximo: Fase 2 (discovery remoto vía DHT — anunciar Cards bajo
|
||||||
|
flow type, broker query local + remoto).
|
||||||
|
|
||||||
### feat(brahman-net): capa P2P compartida — Fase 0 (extracción del swarm libp2p)
|
### feat(brahman-net): capa P2P compartida — Fase 0 (extracción del swarm libp2p)
|
||||||
Primer paso del plan "el encuentro entre Entes no se restringe a local".
|
Primer paso del plan "el encuentro entre Entes no se restringe a local".
|
||||||
El swarm libp2p que vivía dentro de `minga-p2p::network` (282 LOC) sale
|
El swarm libp2p que vivía dentro de `minga-p2p::network` (282 LOC) sale
|
||||||
|
|||||||
Generated
+3
@@ -1201,11 +1201,14 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"brahman-broker",
|
"brahman-broker",
|
||||||
"brahman-card",
|
"brahman-card",
|
||||||
|
"brahman-net",
|
||||||
|
"futures",
|
||||||
"postcard",
|
"postcard",
|
||||||
"serde",
|
"serde",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
"tracing",
|
"tracing",
|
||||||
"ulid",
|
"ulid",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -6,14 +6,17 @@ rust-version.workspace = true
|
|||||||
license.workspace = true
|
license.workspace = true
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
publish.workspace = true
|
publish.workspace = true
|
||||||
description = "Brahman — handshake runtime Init↔módulo sobre Unix socket (postcard frames)."
|
description = "Brahman — handshake runtime Init↔módulo. Local sobre Unix socket; remoto sobre stream libp2p (brahman-net)."
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
brahman-card = { path = "../brahman-card" }
|
brahman-card = { path = "../brahman-card" }
|
||||||
brahman-broker = { path = "../brahman-broker" }
|
brahman-broker = { path = "../brahman-broker" }
|
||||||
|
brahman-net = { path = "../../shared/brahman-net" }
|
||||||
|
futures = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
postcard = { workspace = true }
|
postcard = { workspace = true }
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
|
tokio-util = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
ulid = { workspace = true }
|
ulid = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use std::time::Duration;
|
|||||||
|
|
||||||
use brahman_card::{Card, WitInterface, CARD_SCHEMA_VERSION};
|
use brahman_card::{Card, WitInterface, CARD_SCHEMA_VERSION};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
use tokio::io::{AsyncRead, AsyncWrite};
|
||||||
use tokio::net::UnixStream;
|
use tokio::net::UnixStream;
|
||||||
|
|
||||||
use crate::codec::{read_frame, write_frame};
|
use crate::codec::{read_frame, write_frame};
|
||||||
@@ -34,33 +35,55 @@ pub enum ClientError {
|
|||||||
/// y tiene su `SessionId`. Los `MatchEvent` recibidos durante operaciones
|
/// y tiene su `SessionId`. Los `MatchEvent` recibidos durante operaciones
|
||||||
/// request/response se buferean en `pending_events` y se obtienen vía
|
/// request/response se buferean en `pending_events` y se obtienen vía
|
||||||
/// [`Client::take_event`] o [`Client::await_event`].
|
/// [`Client::take_event`] o [`Client::await_event`].
|
||||||
|
///
|
||||||
|
/// Genérico sobre el transport (`AsyncRead + AsyncWrite + Unpin + Send`):
|
||||||
|
/// funciona indistintamente sobre `UnixStream` (path local) o sobre un
|
||||||
|
/// stream libp2p wrapped con `tokio_util::compat` (path remoto, vía
|
||||||
|
/// `brahman_handshake::network`).
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Client {
|
pub struct Client<S = UnixStream> {
|
||||||
stream: UnixStream,
|
stream: S,
|
||||||
session: SessionId,
|
session: SessionId,
|
||||||
server_info: HelloAck,
|
server_info: HelloAck,
|
||||||
pending_events: VecDeque<MatchEvent>,
|
pending_events: VecDeque<MatchEvent>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Client {
|
impl Client<UnixStream> {
|
||||||
/// Conecta como módulo agnóstico (sin WIT). Equivalente a
|
/// Conecta como módulo agnóstico (sin WIT) sobre Unix socket.
|
||||||
/// `connect_with(path, card, None)`.
|
/// Equivalente a `connect_with(path, card, None)`.
|
||||||
pub async fn connect(path: impl AsRef<Path>, card: Card) -> Result<Self, ClientError> {
|
pub async fn connect(path: impl AsRef<Path>, card: Card) -> Result<Self, ClientError> {
|
||||||
Self::connect_with(path, card, None).await
|
Self::connect_with(path, card, None).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Conecta al socket enviando Hello con la Card dada y opcionalmente
|
/// Conecta al socket Unix enviando Hello con la Card dada y
|
||||||
/// una `WitInterface` ya extraída. Si `wit` es `Some`, el server
|
/// opcionalmente una `WitInterface` ya extraída. Si `wit` es `Some`,
|
||||||
/// registra el módulo como "consciente".
|
/// el server registra el módulo como "consciente".
|
||||||
pub async fn connect_with(
|
pub async fn connect_with(
|
||||||
path: impl AsRef<Path>,
|
path: impl AsRef<Path>,
|
||||||
card: Card,
|
card: Card,
|
||||||
wit: Option<WitInterface>,
|
wit: Option<WitInterface>,
|
||||||
|
) -> Result<Self, ClientError> {
|
||||||
|
let stream = UnixStream::connect(path).await?;
|
||||||
|
Self::connect_with_stream(stream, card, wit).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> Client<S>
|
||||||
|
where
|
||||||
|
S: AsyncRead + AsyncWrite + Unpin + Send,
|
||||||
|
{
|
||||||
|
/// Constructor genérico: arranca el handshake sobre un stream
|
||||||
|
/// arbitrario que ya esté abierto. Es el punto de entrada para
|
||||||
|
/// transports alternativos (libp2p, in-memory para tests, etc.)
|
||||||
|
/// que reusan toda la lógica del handshake post-stream-open.
|
||||||
|
pub async fn connect_with_stream(
|
||||||
|
mut stream: S,
|
||||||
|
card: Card,
|
||||||
|
wit: Option<WitInterface>,
|
||||||
) -> Result<Self, ClientError> {
|
) -> Result<Self, ClientError> {
|
||||||
card.validate()
|
card.validate()
|
||||||
.map_err(|e| ClientError::InvalidCard(e.to_string()))?;
|
.map_err(|e| ClientError::InvalidCard(e.to_string()))?;
|
||||||
|
|
||||||
let mut stream = UnixStream::connect(path).await?;
|
|
||||||
let hello = Hello {
|
let hello = Hello {
|
||||||
schema_version: CARD_SCHEMA_VERSION,
|
schema_version: CARD_SCHEMA_VERSION,
|
||||||
protocol_version: brahman_card::PROTOCOL_VERSION.to_string(),
|
protocol_version: brahman_card::PROTOCOL_VERSION.to_string(),
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ pub mod codec;
|
|||||||
pub mod messages;
|
pub mod messages;
|
||||||
pub mod server;
|
pub mod server;
|
||||||
pub mod client;
|
pub mod client;
|
||||||
|
pub mod network;
|
||||||
pub mod transport;
|
pub mod transport;
|
||||||
|
|
||||||
pub use brahman_card::PROTOCOL_VERSION;
|
pub use brahman_card::PROTOCOL_VERSION;
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
//! Backend libp2p del handshake brahman: el mismo protocolo (Hello /
|
||||||
|
//! HelloAck / Ping / Pong / MatchEvent / Farewell, frames postcard
|
||||||
|
//! length-prefixed) ahora también viaja sobre streams libp2p de la
|
||||||
|
//! malla `brahman-net`.
|
||||||
|
//!
|
||||||
|
//! El servidor expone el bucle [`run_libp2p_accept_loop`] que acepta
|
||||||
|
//! streams del protocolo `BRAHMAN_HANDSHAKE_PROTOCOL` y los delega al
|
||||||
|
//! mismo `Server` que ya escucha por Unix socket — la `Session` es
|
||||||
|
//! genérica sobre el transporte, así que ambas vías comparten broker,
|
||||||
|
//! tablas de sesiones, push de MatchEvents, todo.
|
||||||
|
//!
|
||||||
|
//! El cliente se conecta vía [`connect_libp2p`]: abre un stream
|
||||||
|
//! libp2p hacia un `PeerId` ya conocido y arranca el handshake como
|
||||||
|
//! cualquier `Client`.
|
||||||
|
//!
|
||||||
|
//! Identidad: cada nodo libp2p tiene su `PeerId` (ed25519 derivado).
|
||||||
|
//! La identidad del Ente (Card.id ULID + futura firma) viaja en el
|
||||||
|
//! Hello, como en el path Unix. Trust remoto (verificación de firma
|
||||||
|
//! antes de aceptar el Hello) es Fase 3.
|
||||||
|
//!
|
||||||
|
//! Ejemplo (servidor — Arje):
|
||||||
|
//! ```ignore
|
||||||
|
//! let server = Arc::new(Server::bind("/run/brahman-init.sock", config)?);
|
||||||
|
//! let net = Arc::new(BrahmanNet::new()?);
|
||||||
|
//! net.listen("/ip4/0.0.0.0/tcp/4101".parse()?).await;
|
||||||
|
//!
|
||||||
|
//! tokio::spawn(brahman_handshake::network::run_libp2p_accept_loop(
|
||||||
|
//! server.clone(),
|
||||||
|
//! net.clone(),
|
||||||
|
//! ));
|
||||||
|
//! // Server::run sigue escuchando Unix en paralelo.
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! Ejemplo (cliente — sidecar de un Ente remoto):
|
||||||
|
//! ```ignore
|
||||||
|
//! let net = BrahmanNet::new()?;
|
||||||
|
//! net.dial(remote_multiaddr);
|
||||||
|
//! let mut client = brahman_handshake::network::connect_libp2p(
|
||||||
|
//! &net, peer_id, my_card, None,
|
||||||
|
//! ).await?;
|
||||||
|
//! client.ping().await?;
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use brahman_card::{Card, WitInterface};
|
||||||
|
use brahman_net::{BrahmanNet, OpenStreamError, PeerId, Stream, StreamProtocol};
|
||||||
|
use futures::StreamExt;
|
||||||
|
use tokio_util::compat::{Compat, FuturesAsyncReadCompatExt};
|
||||||
|
use tracing::warn;
|
||||||
|
|
||||||
|
use crate::client::{Client, ClientError};
|
||||||
|
use crate::server::Server;
|
||||||
|
|
||||||
|
/// Sub-protocolo del handshake brahman sobre streams libp2p.
|
||||||
|
pub const BRAHMAN_HANDSHAKE_PROTOCOL: StreamProtocol =
|
||||||
|
StreamProtocol::new("/brahman/handshake/1.0.0");
|
||||||
|
|
||||||
|
/// Tipo del stream que ve la lógica del handshake una vez convertido
|
||||||
|
/// del mundo `futures::AsyncRead/Write` (libp2p) al mundo
|
||||||
|
/// `tokio::io::AsyncRead/Write` (resto del crate).
|
||||||
|
pub type LibP2pHandshakeStream = Compat<Stream>;
|
||||||
|
|
||||||
|
/// Errores específicos del backend libp2p.
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum NetworkError {
|
||||||
|
#[error("abrir stream libp2p: {0}")]
|
||||||
|
OpenStream(#[from] OpenStreamError),
|
||||||
|
|
||||||
|
#[error("handshake: {0}")]
|
||||||
|
Handshake(#[from] ClientError),
|
||||||
|
|
||||||
|
#[error("aceptar stream libp2p: {0}")]
|
||||||
|
AcceptStream(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loop de aceptación de streams libp2p del protocolo handshake.
|
||||||
|
/// Cada stream entrante se construye como `Session` reutilizando las
|
||||||
|
/// tablas compartidas del `Server`, así que conviven con sesiones
|
||||||
|
/// Unix indistinguibles.
|
||||||
|
///
|
||||||
|
/// Vive hasta que el control libp2p se cierre o el caller drop el
|
||||||
|
/// future. Errores por sesión se loggean (no tumban el loop).
|
||||||
|
pub async fn run_libp2p_accept_loop(
|
||||||
|
server: Arc<Server>,
|
||||||
|
net: Arc<BrahmanNet>,
|
||||||
|
) -> Result<(), NetworkError> {
|
||||||
|
let mut control = net.control.clone();
|
||||||
|
let mut incoming = control
|
||||||
|
.accept(BRAHMAN_HANDSHAKE_PROTOCOL)
|
||||||
|
.map_err(|e| NetworkError::AcceptStream(e.to_string()))?;
|
||||||
|
|
||||||
|
while let Some((peer, stream)) = incoming.next().await {
|
||||||
|
let server = server.clone();
|
||||||
|
// .compat() debe pasar al spawn ADENTRO; si lo hacemos afuera
|
||||||
|
// y capturamos `Compat<Stream>` en la closure, el future
|
||||||
|
// resultante hereda traits que dyn AsyncReadWrite no satisface
|
||||||
|
// (compatibility con thread-safety de tokio::spawn).
|
||||||
|
tokio::spawn(handle_libp2p_session(server, stream, peer));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_libp2p_session(
|
||||||
|
server: Arc<Server>,
|
||||||
|
stream: Stream,
|
||||||
|
peer: PeerId,
|
||||||
|
) {
|
||||||
|
let session = server.session_from_stream(stream.compat());
|
||||||
|
if let Err(e) = session.handle().await {
|
||||||
|
warn!(
|
||||||
|
target: "brahman_handshake::network",
|
||||||
|
peer = %peer,
|
||||||
|
error = %e,
|
||||||
|
"sesión libp2p terminó con error"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Conecta como cliente a un Ente remoto vía libp2p y completa el
|
||||||
|
/// handshake. Requiere que `net` ya tenga conexión (o pueda dial-ar)
|
||||||
|
/// al `peer`; típicamente el caller hace `net.dial(multiaddr)` antes.
|
||||||
|
///
|
||||||
|
/// Devuelve un `Client` típico — los métodos `ping`, `await_event`,
|
||||||
|
/// `farewell` funcionan idéntico al path Unix. El stream subyacente
|
||||||
|
/// es libp2p convertido vía `tokio_util::compat`.
|
||||||
|
pub async fn connect_libp2p(
|
||||||
|
net: &BrahmanNet,
|
||||||
|
peer: PeerId,
|
||||||
|
card: Card,
|
||||||
|
wit: Option<WitInterface>,
|
||||||
|
) -> Result<Client<LibP2pHandshakeStream>, NetworkError> {
|
||||||
|
let mut control = net.control.clone();
|
||||||
|
let stream = control
|
||||||
|
.open_stream(peer, BRAHMAN_HANDSHAKE_PROTOCOL)
|
||||||
|
.await?;
|
||||||
|
let client = Client::connect_with_stream(stream.compat(), card, wit).await?;
|
||||||
|
Ok(client)
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ use std::time::{SystemTime, UNIX_EPOCH};
|
|||||||
|
|
||||||
use brahman_broker::{Broker, Endpoint};
|
use brahman_broker::{Broker, Endpoint};
|
||||||
use brahman_card::{Card, ResolvedCard, WitInterface, CARD_SCHEMA_VERSION};
|
use brahman_card::{Card, ResolvedCard, WitInterface, CARD_SCHEMA_VERSION};
|
||||||
|
use tokio::io::{split, AsyncRead, AsyncWrite, WriteHalf};
|
||||||
use tokio::net::{UnixListener, UnixStream};
|
use tokio::net::{UnixListener, UnixStream};
|
||||||
use tokio::sync::{mpsc, Mutex};
|
use tokio::sync::{mpsc, Mutex};
|
||||||
use tracing::{debug, warn};
|
use tracing::{debug, warn};
|
||||||
@@ -93,17 +94,34 @@ impl Server {
|
|||||||
self.sessions.clone()
|
self.sessions.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Acepta UNA conexión, devuelve la `Session` lista para `handle()`.
|
/// Acepta UNA conexión Unix, devuelve la `Session` lista para `handle()`.
|
||||||
/// No corre el handler — eso es responsabilidad del llamante.
|
/// No corre el handler — eso es responsabilidad del llamante.
|
||||||
pub async fn accept_one(&self) -> std::io::Result<Session> {
|
pub async fn accept_one(&self) -> std::io::Result<Session<UnixStream>> {
|
||||||
let (stream, _addr) = self.listener.accept().await?;
|
let (stream, _addr) = self.listener.accept().await?;
|
||||||
Ok(Session {
|
Ok(self.session_from_stream(stream))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construye una `Session` a partir de un stream arbitrario que
|
||||||
|
/// implemente `AsyncRead + AsyncWrite + Unpin + Send`. Es el
|
||||||
|
/// punto de entrada para transports alternativos (libp2p en
|
||||||
|
/// `brahman_handshake::network`, in-memory para tests, etc.) que
|
||||||
|
/// quieren reutilizar la lógica del handshake sin venir por el
|
||||||
|
/// listener Unix.
|
||||||
|
///
|
||||||
|
/// Las tablas compartidas (sessions/push/last_matches/broker) se
|
||||||
|
/// clonan, así que sesiones construidas por esta vía conviven
|
||||||
|
/// indistinguibles en el mismo `Server`.
|
||||||
|
pub fn session_from_stream<S>(&self, stream: S) -> Session<S>
|
||||||
|
where
|
||||||
|
S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
|
||||||
|
{
|
||||||
|
Session {
|
||||||
stream,
|
stream,
|
||||||
sessions: self.sessions.clone(),
|
sessions: self.sessions.clone(),
|
||||||
push_table: self.push_table.clone(),
|
push_table: self.push_table.clone(),
|
||||||
last_matches: self.last_matches.clone(),
|
last_matches: self.last_matches.clone(),
|
||||||
config: self.config.clone(),
|
config: self.config.clone(),
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Loop de aceptación: cada conexión se despacha en una task separada.
|
/// Loop de aceptación: cada conexión se despacha en una task separada.
|
||||||
@@ -131,81 +149,160 @@ impl Drop for Server {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Conexión individual aceptada por el servidor.
|
/// Conexión individual aceptada por el servidor. Genérica sobre el
|
||||||
pub struct Session {
|
/// transport — funciona indistinguiblemente sobre `UnixStream` (modo
|
||||||
stream: UnixStream,
|
/// local), libp2p stream wrapped con `tokio_util::compat`, in-memory
|
||||||
|
/// duplex (tests), etc.
|
||||||
|
pub struct Session<S> {
|
||||||
|
stream: S,
|
||||||
sessions: SessionRegistry,
|
sessions: SessionRegistry,
|
||||||
push_table: SessionTxTable,
|
push_table: SessionTxTable,
|
||||||
last_matches: LastMatches,
|
last_matches: LastMatches,
|
||||||
config: ServerConfig,
|
config: ServerConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Session {
|
impl<S> Session<S>
|
||||||
/// Procesa la conexión hasta `Farewell` o EOF: handshake + loop de pings.
|
where
|
||||||
/// Garantiza cleanup (sessions + broker) sin importar la rama de salida.
|
S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
|
||||||
pub async fn handle(mut self) -> std::io::Result<()> {
|
{
|
||||||
let session_id = match self.do_handshake().await? {
|
/// Procesa la conexión hasta `Farewell` o EOF.
|
||||||
|
///
|
||||||
|
/// Estructura: handshake (sobre el stream entero) → split en
|
||||||
|
/// halves (read + write) → reader loop principal + writer task
|
||||||
|
/// que drena el push channel. Garantiza cleanup (sessions + broker
|
||||||
|
/// + canales) sin importar la rama de salida.
|
||||||
|
///
|
||||||
|
/// El split es necesario para soportar streams `!Sync` como
|
||||||
|
/// `libp2p::Stream`: `tokio::select!` sobre `&mut self.stream`
|
||||||
|
/// requeriría `S: Sync`. Con `tokio::io::split` cada mitad va a
|
||||||
|
/// su propia task, eliminando el requirement y permitiendo que
|
||||||
|
/// la misma `Session` sirva indistinta sobre Unix socket o
|
||||||
|
/// stream libp2p remoto.
|
||||||
|
pub async fn handle(self) -> std::io::Result<()> {
|
||||||
|
let Self {
|
||||||
|
mut stream,
|
||||||
|
sessions,
|
||||||
|
push_table,
|
||||||
|
last_matches,
|
||||||
|
config,
|
||||||
|
} = self;
|
||||||
|
|
||||||
|
let session_id = match do_handshake(&mut stream, &config, &sessions).await? {
|
||||||
Some(id) => id,
|
Some(id) => id,
|
||||||
None => return Ok(()), // Hello rechazado, no se registró nada
|
None => return Ok(()), // Hello rechazado, no se registró nada
|
||||||
};
|
};
|
||||||
|
|
||||||
let result = self.run_post_handshake(session_id).await;
|
let result = run_post_handshake(
|
||||||
self.cleanup(session_id).await;
|
stream,
|
||||||
|
session_id,
|
||||||
|
push_table.clone(),
|
||||||
|
last_matches.clone(),
|
||||||
|
config.clone(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
cleanup(
|
||||||
|
session_id,
|
||||||
|
&sessions,
|
||||||
|
&push_table,
|
||||||
|
&last_matches,
|
||||||
|
&config,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run_post_handshake(&mut self, session_id: SessionId) -> std::io::Result<()> {
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Free functions (post-refactor): la lógica del post-handshake corre sobre
|
||||||
|
// halves del stream; no necesita más `&mut Session<S>` y por eso vive afuera.
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
async fn run_post_handshake<S>(
|
||||||
|
stream: S,
|
||||||
|
session_id: SessionId,
|
||||||
|
push_table: SessionTxTable,
|
||||||
|
last_matches: LastMatches,
|
||||||
|
config: ServerConfig,
|
||||||
|
) -> std::io::Result<()>
|
||||||
|
where
|
||||||
|
S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
|
||||||
|
{
|
||||||
// Canal por donde el server inyecta frames push (MatchEvent, etc.).
|
// Canal por donde el server inyecta frames push (MatchEvent, etc.).
|
||||||
let (tx, mut rx) = mpsc::channel::<Frame>(PUSH_CHANNEL_CAPACITY);
|
let (tx, mut rx) = mpsc::channel::<Frame>(PUSH_CHANNEL_CAPACITY);
|
||||||
self.push_table.lock().await.insert(session_id, tx);
|
push_table.lock().await.insert(session_id, tx);
|
||||||
|
|
||||||
// Tras registrar el canal, recomputar matches y emitir diffs a
|
// Tras registrar el canal, recomputar matches y emitir diffs.
|
||||||
// todas las sesiones afectadas (incluida ésta, si tiene inputs).
|
broadcast_match_diffs(&push_table, &last_matches, &config).await;
|
||||||
self.broadcast_match_diffs().await;
|
|
||||||
|
|
||||||
loop {
|
// Split: reader en el hilo principal, writer compartido bajo Mutex
|
||||||
tokio::select! {
|
// entre la writer task (push channel) y el handler de inbound
|
||||||
// Frame entrante del cliente.
|
// (que escribe Pong/Error). Mutex serializa writes; es OK porque
|
||||||
res = read_frame(&mut self.stream) => {
|
// la frecuencia de writes por sesión es baja.
|
||||||
match res {
|
let (mut reader, writer) = split(stream);
|
||||||
Ok(frame) => {
|
let writer = Arc::new(Mutex::new(writer));
|
||||||
if !self.handle_inbound_frame(session_id, frame).await? {
|
|
||||||
return Ok(());
|
// Writer task: drena el push channel.
|
||||||
|
let writer_for_push = writer.clone();
|
||||||
|
let writer_task = tokio::spawn(async move {
|
||||||
|
while let Some(frame) = rx.recv().await {
|
||||||
|
let mut w = writer_for_push.lock().await;
|
||||||
|
if write_frame(&mut *w, &frame).await.is_err() {
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reader loop principal.
|
||||||
|
let result: std::io::Result<()> = loop {
|
||||||
|
match read_frame(&mut reader).await {
|
||||||
|
Ok(frame) => match handle_inbound_frame(session_id, frame, &writer).await {
|
||||||
|
Ok(true) => continue,
|
||||||
|
Ok(false) => break Ok(()),
|
||||||
|
Err(e) => break Err(e),
|
||||||
|
},
|
||||||
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
|
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
|
||||||
debug!(session = %session_id, "cliente cerró sin Farewell");
|
debug!(session = %session_id, "cliente cerró sin Farewell");
|
||||||
return Ok(());
|
break Ok(());
|
||||||
}
|
|
||||||
Err(e) => return Err(e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Frame push desde el server (MatchEvent).
|
|
||||||
Some(frame) = rx.recv() => {
|
|
||||||
write_frame(&mut self.stream, &frame).await?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
Err(e) => break Err(e),
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cerrar writer: drop nuestro Arc y abortar la task. La task
|
||||||
|
// saldrá igual cuando rx se cierre por drop del último Sender,
|
||||||
|
// pero abortarla es más rápido que esperar a que próximo recv()
|
||||||
|
// observe el cierre.
|
||||||
|
drop(writer);
|
||||||
|
writer_task.abort();
|
||||||
|
let _ = writer_task.await;
|
||||||
|
|
||||||
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Maneja un frame entrante. Devuelve `Ok(false)` si la sesión debe
|
async fn handle_inbound_frame<S>(
|
||||||
/// cerrarse limpiamente (Farewell con session-id correcto).
|
|
||||||
async fn handle_inbound_frame(
|
|
||||||
&mut self,
|
|
||||||
session_id: SessionId,
|
session_id: SessionId,
|
||||||
frame: Frame,
|
frame: Frame,
|
||||||
) -> std::io::Result<bool> {
|
writer: &Arc<Mutex<WriteHalf<S>>>,
|
||||||
|
) -> std::io::Result<bool>
|
||||||
|
where
|
||||||
|
S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
|
||||||
|
{
|
||||||
match frame {
|
match frame {
|
||||||
Frame::Ping(Ping { session }) if session == session_id => {
|
Frame::Ping(Ping { session }) if session == session_id => {
|
||||||
let pong = Pong {
|
let pong = Pong {
|
||||||
timestamp_ms: now_ms(),
|
timestamp_ms: now_ms(),
|
||||||
};
|
};
|
||||||
write_frame(&mut self.stream, &Frame::Pong(pong)).await?;
|
let mut w = writer.lock().await;
|
||||||
|
write_frame(&mut *w, &Frame::Pong(pong)).await?;
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
Frame::Ping(_) => {
|
Frame::Ping(_) => {
|
||||||
|
let mut w = writer.lock().await;
|
||||||
write_frame(
|
write_frame(
|
||||||
&mut self.stream,
|
&mut *w,
|
||||||
&Frame::Error(HandshakeError::Unauthorized(
|
&Frame::Error(HandshakeError::Unauthorized(
|
||||||
"session-id no coincide".into(),
|
"session-id no coincide".into(),
|
||||||
)),
|
)),
|
||||||
@@ -215,8 +312,9 @@ impl Session {
|
|||||||
}
|
}
|
||||||
Frame::Farewell(Farewell { session }) if session == session_id => Ok(false),
|
Frame::Farewell(Farewell { session }) if session == session_id => Ok(false),
|
||||||
Frame::Farewell(_) => {
|
Frame::Farewell(_) => {
|
||||||
|
let mut w = writer.lock().await;
|
||||||
write_frame(
|
write_frame(
|
||||||
&mut self.stream,
|
&mut *w,
|
||||||
&Frame::Error(HandshakeError::Unauthorized(
|
&Frame::Error(HandshakeError::Unauthorized(
|
||||||
"session-id no coincide".into(),
|
"session-id no coincide".into(),
|
||||||
)),
|
)),
|
||||||
@@ -225,8 +323,9 @@ impl Session {
|
|||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
|
let mut w = writer.lock().await;
|
||||||
write_frame(
|
write_frame(
|
||||||
&mut self.stream,
|
&mut *w,
|
||||||
&Frame::Error(HandshakeError::Rejected(
|
&Frame::Error(HandshakeError::Rejected(
|
||||||
"frame inesperado tras handshake".into(),
|
"frame inesperado tras handshake".into(),
|
||||||
)),
|
)),
|
||||||
@@ -241,33 +340,38 @@ impl Session {
|
|||||||
/// canal push. Se ejecuta tanto si la sesión cierra por Farewell, EOF,
|
/// canal push. Se ejecuta tanto si la sesión cierra por Farewell, EOF,
|
||||||
/// o error. Tras desregistrar, emite diffs a las sesiones que perdieron
|
/// o error. Tras desregistrar, emite diffs a las sesiones que perdieron
|
||||||
/// el match contra ésta.
|
/// el match contra ésta.
|
||||||
async fn cleanup(&self, session_id: SessionId) {
|
async fn cleanup(
|
||||||
self.sessions.lock().await.remove(&session_id);
|
session_id: SessionId,
|
||||||
self.push_table.lock().await.remove(&session_id);
|
sessions: &SessionRegistry,
|
||||||
self.last_matches.lock().await.remove(&session_id);
|
push_table: &SessionTxTable,
|
||||||
if let Some(broker) = &self.config.broker {
|
last_matches: &LastMatches,
|
||||||
|
config: &ServerConfig,
|
||||||
|
) {
|
||||||
|
sessions.lock().await.remove(&session_id);
|
||||||
|
push_table.lock().await.remove(&session_id);
|
||||||
|
last_matches.lock().await.remove(&session_id);
|
||||||
|
if let Some(broker) = &config.broker {
|
||||||
broker.lock().await.unregister(session_id);
|
broker.lock().await.unregister(session_id);
|
||||||
}
|
}
|
||||||
self.broadcast_match_diffs().await;
|
broadcast_match_diffs(push_table, last_matches, config).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Recomputa los matches para todas las sesiones registradas y empuja
|
/// Recomputa los matches para todas las sesiones registradas y empuja
|
||||||
/// `MatchEvent::Available` / `MatchEvent::Lost` por las que cambiaron
|
/// `MatchEvent::Available` / `MatchEvent::Lost` por las que cambiaron
|
||||||
/// respecto al último estado conocido.
|
/// respecto al último estado conocido.
|
||||||
///
|
async fn broadcast_match_diffs(
|
||||||
/// Se llama tras cada `register_session` y `cleanup`. Las inserciones
|
push_table: &SessionTxTable,
|
||||||
/// al canal usan `try_send` (non-blocking); si el cliente está lento
|
last_matches: &LastMatches,
|
||||||
/// y se llena el buffer, los eventos extra se pierden — es ephemeral
|
config: &ServerConfig,
|
||||||
/// y el cliente puede re-consultar el estado vía `brahman-status`.
|
) {
|
||||||
async fn broadcast_match_diffs(&self) {
|
let broker = match &config.broker {
|
||||||
let broker = match &self.config.broker {
|
|
||||||
Some(b) => b,
|
Some(b) => b,
|
||||||
None => return,
|
None => return,
|
||||||
};
|
};
|
||||||
|
|
||||||
let b = broker.lock().await;
|
let b = broker.lock().await;
|
||||||
let push_table = self.push_table.lock().await;
|
let push_table = push_table.lock().await;
|
||||||
let mut last = self.last_matches.lock().await;
|
let mut last = last_matches.lock().await;
|
||||||
|
|
||||||
debug!(
|
debug!(
|
||||||
target: "brahman_handshake::broadcast",
|
target: "brahman_handshake::broadcast",
|
||||||
@@ -276,14 +380,13 @@ impl Session {
|
|||||||
"broadcast_match_diffs"
|
"broadcast_match_diffs"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Snapshot de cards para no tener que sostener el lock del broker.
|
|
||||||
let cards: Vec<_> = b.cards().cloned().collect();
|
let cards: Vec<_> = b.cards().cloned().collect();
|
||||||
|
|
||||||
for cons in &cards {
|
for cons in &cards {
|
||||||
let cons_session = cons.session;
|
let cons_session = cons.session;
|
||||||
let tx = match push_table.get(&cons_session) {
|
let tx = match push_table.get(&cons_session) {
|
||||||
Some(tx) => tx,
|
Some(tx) => tx,
|
||||||
None => continue, // todavía no tiene canal push
|
None => continue,
|
||||||
};
|
};
|
||||||
let cons_last = last.entry(cons_session).or_default();
|
let cons_last = last.entry(cons_session).or_default();
|
||||||
|
|
||||||
@@ -297,9 +400,6 @@ impl Session {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(m) = &new_match {
|
if let Some(m) = &new_match {
|
||||||
// Resolvemos el service_socket del productor desde
|
|
||||||
// la BrokeredCard; pasarlo en el evento permite al
|
|
||||||
// consumer conectar directo sin discovery extra.
|
|
||||||
let producer_service_socket = b
|
let producer_service_socket = b
|
||||||
.cards()
|
.cards()
|
||||||
.find(|c| c.session == m.producer.session)
|
.find(|c| c.session == m.producer.session)
|
||||||
@@ -328,7 +428,6 @@ impl Session {
|
|||||||
"Available pushed"
|
"Available pushed"
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Tenía match, ahora no.
|
|
||||||
let event = MatchEvent {
|
let event = MatchEvent {
|
||||||
kind: MatchEventKind::Lost,
|
kind: MatchEventKind::Lost,
|
||||||
consumer_flow: input.name.clone(),
|
consumer_flow: input.name.clone(),
|
||||||
@@ -353,14 +452,20 @@ impl Session {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Lee el Hello, valida, registra la sesión y emite HelloAck.
|
/// Lee el Hello, valida, registra la sesión y emite HelloAck.
|
||||||
/// Devuelve `Some(session_id)` si el handshake fue exitoso.
|
async fn do_handshake<S>(
|
||||||
async fn do_handshake(&mut self) -> std::io::Result<Option<SessionId>> {
|
stream: &mut S,
|
||||||
let frame = read_frame(&mut self.stream).await?;
|
config: &ServerConfig,
|
||||||
|
sessions: &SessionRegistry,
|
||||||
|
) -> std::io::Result<Option<SessionId>>
|
||||||
|
where
|
||||||
|
S: AsyncRead + AsyncWrite + Unpin,
|
||||||
|
{
|
||||||
|
let frame = read_frame(stream).await?;
|
||||||
let hello = match frame {
|
let hello = match frame {
|
||||||
Frame::Hello(h) => h,
|
Frame::Hello(h) => h,
|
||||||
_ => {
|
_ => {
|
||||||
write_frame(
|
write_frame(
|
||||||
&mut self.stream,
|
stream,
|
||||||
&Frame::Error(HandshakeError::Rejected(
|
&Frame::Error(HandshakeError::Rejected(
|
||||||
"primer frame debe ser Hello".into(),
|
"primer frame debe ser Hello".into(),
|
||||||
)),
|
)),
|
||||||
@@ -370,36 +475,34 @@ impl Session {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(err) = self.validate_hello(&hello) {
|
if let Some(err) = validate_hello(&hello) {
|
||||||
write_frame(&mut self.stream, &Frame::Error(err)).await?;
|
write_frame(stream, &Frame::Error(err)).await?;
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
let session_id = Ulid::new();
|
let session_id = Ulid::new();
|
||||||
// WireCard → Card: extensiones quedan vacías post-wire (es el contrato).
|
|
||||||
let card: Card = hello.card.into();
|
let card: Card = hello.card.into();
|
||||||
self.register_session(session_id, card, hello.wit).await;
|
register_session(session_id, card, hello.wit, config, sessions).await;
|
||||||
|
|
||||||
let ack = HelloAck {
|
let ack = HelloAck {
|
||||||
server_version: crate::HANDSHAKE_VERSION.to_string(),
|
server_version: crate::HANDSHAKE_VERSION.to_string(),
|
||||||
protocol_version: brahman_card::PROTOCOL_VERSION.to_string(),
|
protocol_version: brahman_card::PROTOCOL_VERSION.to_string(),
|
||||||
session: session_id,
|
session: session_id,
|
||||||
init_attached: self.config.init_attached,
|
init_attached: config.init_attached,
|
||||||
};
|
};
|
||||||
write_frame(&mut self.stream, &Frame::HelloAck(ack)).await?;
|
write_frame(stream, &Frame::HelloAck(ack)).await?;
|
||||||
debug!(session = %session_id, "handshake completado");
|
debug!(session = %session_id, "handshake completado");
|
||||||
Ok(Some(session_id))
|
Ok(Some(session_id))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Indexa la sesión: ResolvedCard en sessions + Card en broker (si hay).
|
|
||||||
/// Si `wit` está presente, el módulo se registra como "consciente".
|
|
||||||
async fn register_session(
|
async fn register_session(
|
||||||
&self,
|
|
||||||
session_id: SessionId,
|
session_id: SessionId,
|
||||||
card: Card,
|
card: Card,
|
||||||
wit: Option<WitInterface>,
|
wit: Option<WitInterface>,
|
||||||
|
config: &ServerConfig,
|
||||||
|
sessions: &SessionRegistry,
|
||||||
) {
|
) {
|
||||||
if let Some(broker) = &self.config.broker {
|
if let Some(broker) = &config.broker {
|
||||||
broker
|
broker
|
||||||
.lock()
|
.lock()
|
||||||
.await
|
.await
|
||||||
@@ -409,11 +512,10 @@ impl Session {
|
|||||||
Some(w) => ResolvedCard::from_conscious(card, w),
|
Some(w) => ResolvedCard::from_conscious(card, w),
|
||||||
None => ResolvedCard::from_agnostic(card),
|
None => ResolvedCard::from_agnostic(card),
|
||||||
};
|
};
|
||||||
self.sessions.lock().await.insert(session_id, resolved);
|
sessions.lock().await.insert(session_id, resolved);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Validaciones que el servidor aplica al Hello del cliente.
|
fn validate_hello(hello: &Hello) -> Option<HandshakeError> {
|
||||||
fn validate_hello(&self, hello: &Hello) -> Option<HandshakeError> {
|
|
||||||
if hello.schema_version != CARD_SCHEMA_VERSION {
|
if hello.schema_version != CARD_SCHEMA_VERSION {
|
||||||
return Some(HandshakeError::SchemaMismatch {
|
return Some(HandshakeError::SchemaMismatch {
|
||||||
client: hello.schema_version,
|
client: hello.schema_version,
|
||||||
@@ -427,15 +529,12 @@ impl Session {
|
|||||||
brahman_card::PROTOCOL_VERSION
|
brahman_card::PROTOCOL_VERSION
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
// Validamos contra Card (la rica) — convertir es barato y centraliza
|
|
||||||
// la lógica de validación en un solo lugar.
|
|
||||||
let as_card: Card = Card::from(hello.card.clone());
|
let as_card: Card = Card::from(hello.card.clone());
|
||||||
if let Err(e) = as_card.validate() {
|
if let Err(e) = as_card.validate() {
|
||||||
return Some(HandshakeError::InvalidCard(e.to_string()));
|
return Some(HandshakeError::InvalidCard(e.to_string()));
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fn now_ms() -> u64 {
|
fn now_ms() -> u64 {
|
||||||
SystemTime::now()
|
SystemTime::now()
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
//! 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::network::{connect_libp2p, run_libp2p_accept_loop};
|
||||||
|
use brahman_handshake::server::{Server, ServerConfig};
|
||||||
|
use brahman_net::{BrahmanNet, 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()),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.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 mut client = connect_libp2p(&client_net, server_peer_id, card, None)
|
||||||
|
.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();
|
||||||
|
}
|
||||||
@@ -48,12 +48,13 @@ use futures::StreamExt;
|
|||||||
use libp2p::{
|
use libp2p::{
|
||||||
identify, identity, kad, noise,
|
identify, identity, kad, noise,
|
||||||
swarm::{NetworkBehaviour, SwarmEvent},
|
swarm::{NetworkBehaviour, SwarmEvent},
|
||||||
tcp, yamux, Multiaddr, PeerId, Swarm, SwarmBuilder,
|
tcp, yamux, Swarm, SwarmBuilder,
|
||||||
};
|
};
|
||||||
use libp2p_stream as stream;
|
use libp2p_stream as stream;
|
||||||
use tokio::sync::{mpsc, oneshot, Mutex};
|
use tokio::sync::{mpsc, oneshot, Mutex};
|
||||||
|
|
||||||
pub use libp2p::{Stream, StreamProtocol};
|
pub use libp2p::{multiaddr::Protocol, Multiaddr, PeerId, Stream, StreamProtocol};
|
||||||
|
pub use libp2p_stream::OpenStreamError;
|
||||||
|
|
||||||
const IDENTIFY_PROTOCOL: &str = "/brahman-net/0.1.0";
|
const IDENTIFY_PROTOCOL: &str = "/brahman-net/0.1.0";
|
||||||
const IDLE_CONNECTION_TIMEOUT: Duration = Duration::from_secs(60);
|
const IDLE_CONNECTION_TIMEOUT: Duration = Duration::from_secs(60);
|
||||||
|
|||||||
Reference in New Issue
Block a user