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:
Sergio
2026-05-09 12:51:43 +00:00
parent ad0d475a2e
commit 73dadbb166
9 changed files with 723 additions and 281 deletions
+368 -269
View File
@@ -7,6 +7,7 @@ use std::time::{SystemTime, UNIX_EPOCH};
use brahman_broker::{Broker, Endpoint};
use brahman_card::{Card, ResolvedCard, WitInterface, CARD_SCHEMA_VERSION};
use tokio::io::{split, AsyncRead, AsyncWrite, WriteHalf};
use tokio::net::{UnixListener, UnixStream};
use tokio::sync::{mpsc, Mutex};
use tracing::{debug, warn};
@@ -93,17 +94,34 @@ impl Server {
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.
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?;
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,
sessions: self.sessions.clone(),
push_table: self.push_table.clone(),
last_matches: self.last_matches.clone(),
config: self.config.clone(),
})
}
}
/// Loop de aceptación: cada conexión se despacha en una task separada.
@@ -131,310 +149,391 @@ impl Drop for Server {
}
}
/// Conexión individual aceptada por el servidor.
pub struct Session {
stream: UnixStream,
/// Conexión individual aceptada por el servidor. Genérica sobre el
/// transport — funciona indistinguiblemente sobre `UnixStream` (modo
/// local), libp2p stream wrapped con `tokio_util::compat`, in-memory
/// duplex (tests), etc.
pub struct Session<S> {
stream: S,
sessions: SessionRegistry,
push_table: SessionTxTable,
last_matches: LastMatches,
config: ServerConfig,
}
impl Session {
/// Procesa la conexión hasta `Farewell` o EOF: handshake + loop de pings.
/// Garantiza cleanup (sessions + broker) sin importar la rama de salida.
pub async fn handle(mut self) -> std::io::Result<()> {
let session_id = match self.do_handshake().await? {
impl<S> Session<S>
where
S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
{
/// 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,
None => return Ok(()), // Hello rechazado, no se registró nada
};
let result = self.run_post_handshake(session_id).await;
self.cleanup(session_id).await;
let result = run_post_handshake(
stream,
session_id,
push_table.clone(),
last_matches.clone(),
config.clone(),
)
.await;
cleanup(
session_id,
&sessions,
&push_table,
&last_matches,
&config,
)
.await;
result
}
async fn run_post_handshake(&mut self, session_id: SessionId) -> std::io::Result<()> {
// Canal por donde el server inyecta frames push (MatchEvent, etc.).
let (tx, mut rx) = mpsc::channel::<Frame>(PUSH_CHANNEL_CAPACITY);
self.push_table.lock().await.insert(session_id, tx);
}
// Tras registrar el canal, recomputar matches y emitir diffs a
// todas las sesiones afectadas (incluida ésta, si tiene inputs).
self.broadcast_match_diffs().await;
// ============================================================================
// 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.
// ============================================================================
loop {
tokio::select! {
// Frame entrante del cliente.
res = read_frame(&mut self.stream) => {
match res {
Ok(frame) => {
if !self.handle_inbound_frame(session_id, frame).await? {
return Ok(());
}
}
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
debug!(session = %session_id, "cliente cerró sin Farewell");
return Ok(());
}
Err(e) => return Err(e),
}
}
// Frame push desde el server (MatchEvent).
Some(frame) = rx.recv() => {
write_frame(&mut self.stream, &frame).await?;
}
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.).
let (tx, mut rx) = mpsc::channel::<Frame>(PUSH_CHANNEL_CAPACITY);
push_table.lock().await.insert(session_id, tx);
// Tras registrar el canal, recomputar matches y emitir diffs.
broadcast_match_diffs(&push_table, &last_matches, &config).await;
// Split: reader en el hilo principal, writer compartido bajo Mutex
// entre la writer task (push channel) y el handler de inbound
// (que escribe Pong/Error). Mutex serializa writes; es OK porque
// la frecuencia de writes por sesión es baja.
let (mut reader, writer) = split(stream);
let writer = Arc::new(Mutex::new(writer));
// 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;
}
}
}
});
/// Maneja un frame entrante. Devuelve `Ok(false)` si la sesión debe
/// cerrarse limpiamente (Farewell con session-id correcto).
async fn handle_inbound_frame(
&mut self,
session_id: SessionId,
frame: Frame,
) -> std::io::Result<bool> {
match frame {
Frame::Ping(Ping { session }) if session == session_id => {
let pong = Pong {
timestamp_ms: now_ms(),
};
write_frame(&mut self.stream, &Frame::Pong(pong)).await?;
Ok(true)
}
Frame::Ping(_) => {
write_frame(
&mut self.stream,
&Frame::Error(HandshakeError::Unauthorized(
"session-id no coincide".into(),
)),
)
.await?;
Ok(true)
}
Frame::Farewell(Farewell { session }) if session == session_id => Ok(false),
Frame::Farewell(_) => {
write_frame(
&mut self.stream,
&Frame::Error(HandshakeError::Unauthorized(
"session-id no coincide".into(),
)),
)
.await?;
Ok(true)
}
_ => {
write_frame(
&mut self.stream,
&Frame::Error(HandshakeError::Rejected(
"frame inesperado tras handshake".into(),
)),
)
.await?;
Ok(true)
// 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 => {
debug!(session = %session_id, "cliente cerró sin Farewell");
break Ok(());
}
Err(e) => break Err(e),
}
}
};
/// Limpieza atómica de TRES vistas: registro de sesiones + broker +
/// canal push. Se ejecuta tanto si la sesión cierra por Farewell, EOF,
/// o error. Tras desregistrar, emite diffs a las sesiones que perdieron
/// el match contra ésta.
async fn cleanup(&self, session_id: SessionId) {
self.sessions.lock().await.remove(&session_id);
self.push_table.lock().await.remove(&session_id);
self.last_matches.lock().await.remove(&session_id);
if let Some(broker) = &self.config.broker {
broker.lock().await.unregister(session_id);
}
self.broadcast_match_diffs().await;
}
// 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;
/// Recomputa los matches para todas las sesiones registradas y empuja
/// `MatchEvent::Available` / `MatchEvent::Lost` por las que cambiaron
/// respecto al último estado conocido.
///
/// Se llama tras cada `register_session` y `cleanup`. Las inserciones
/// al canal usan `try_send` (non-blocking); si el cliente está lento
/// y se llena el buffer, los eventos extra se pierden — es ephemeral
/// y el cliente puede re-consultar el estado vía `brahman-status`.
async fn broadcast_match_diffs(&self) {
let broker = match &self.config.broker {
Some(b) => b,
None => return,
};
result
}
let b = broker.lock().await;
let push_table = self.push_table.lock().await;
let mut last = self.last_matches.lock().await;
debug!(
target: "brahman_handshake::broadcast",
cards = b.len(),
push_subscribers = push_table.len(),
"broadcast_match_diffs"
);
// Snapshot de cards para no tener que sostener el lock del broker.
let cards: Vec<_> = b.cards().cloned().collect();
for cons in &cards {
let cons_session = cons.session;
let tx = match push_table.get(&cons_session) {
Some(tx) => tx,
None => continue, // todavía no tiene canal push
async fn handle_inbound_frame<S>(
session_id: SessionId,
frame: Frame,
writer: &Arc<Mutex<WriteHalf<S>>>,
) -> std::io::Result<bool>
where
S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
{
match frame {
Frame::Ping(Ping { session }) if session == session_id => {
let pong = Pong {
timestamp_ms: now_ms(),
};
let cons_last = last.entry(cons_session).or_default();
let mut w = writer.lock().await;
write_frame(&mut *w, &Frame::Pong(pong)).await?;
Ok(true)
}
Frame::Ping(_) => {
let mut w = writer.lock().await;
write_frame(
&mut *w,
&Frame::Error(HandshakeError::Unauthorized(
"session-id no coincide".into(),
)),
)
.await?;
Ok(true)
}
Frame::Farewell(Farewell { session }) if session == session_id => Ok(false),
Frame::Farewell(_) => {
let mut w = writer.lock().await;
write_frame(
&mut *w,
&Frame::Error(HandshakeError::Unauthorized(
"session-id no coincide".into(),
)),
)
.await?;
Ok(true)
}
_ => {
let mut w = writer.lock().await;
write_frame(
&mut *w,
&Frame::Error(HandshakeError::Rejected(
"frame inesperado tras handshake".into(),
)),
)
.await?;
Ok(true)
}
}
}
for input in &cons.inputs {
let new_match = b.find_producer_for(cons_session, &input.name);
let new_endpoint = new_match.as_ref().map(|m| m.producer.clone());
let old_endpoint = cons_last.get(&input.name).cloned();
/// Limpieza atómica de TRES vistas: registro de sesiones + broker +
/// canal push. Se ejecuta tanto si la sesión cierra por Farewell, EOF,
/// o error. Tras desregistrar, emite diffs a las sesiones que perdieron
/// el match contra ésta.
async fn cleanup(
session_id: SessionId,
sessions: &SessionRegistry,
push_table: &SessionTxTable,
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);
}
broadcast_match_diffs(push_table, last_matches, config).await;
}
if old_endpoint == new_endpoint {
continue;
}
/// Recomputa los matches para todas las sesiones registradas y empuja
/// `MatchEvent::Available` / `MatchEvent::Lost` por las que cambiaron
/// respecto al último estado conocido.
async fn broadcast_match_diffs(
push_table: &SessionTxTable,
last_matches: &LastMatches,
config: &ServerConfig,
) {
let broker = match &config.broker {
Some(b) => b,
None => return,
};
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
.cards()
.find(|c| c.session == m.producer.session)
.and_then(|c| c.service_socket.clone());
let event = MatchEvent {
kind: MatchEventKind::Available,
consumer_flow: input.name.clone(),
producer_session: m.producer.session,
producer_label: m.producer_label.clone(),
producer_flow: m.producer.flow_name.clone(),
ty: m.ty.clone(),
via: m.via,
pinned: m.pinned,
producer_service_socket,
};
let send_res = tx.try_send(Frame::MatchEvent(event));
debug!(
target: "brahman_handshake::broadcast",
consumer = %cons_session,
flow = %input.name,
producer = %m.producer_label,
result = ?send_res.as_ref().map(|_| "ok").unwrap_or_else(|e| match e {
tokio::sync::mpsc::error::TrySendError::Full(_) => "full",
tokio::sync::mpsc::error::TrySendError::Closed(_) => "closed",
}),
"Available pushed"
);
} else {
// Tenía match, ahora no.
let event = MatchEvent {
kind: MatchEventKind::Lost,
consumer_flow: input.name.clone(),
producer_session: Ulid::nil(),
producer_label: String::new(),
producer_flow: String::new(),
ty: input.ty.clone(),
via: brahman_broker::MatchStrategy::Exact,
pinned: false,
producer_service_socket: None,
};
let _ = tx.try_send(Frame::MatchEvent(event));
}
let b = broker.lock().await;
let push_table = push_table.lock().await;
let mut last = last_matches.lock().await;
if let Some(ep) = new_endpoint {
cons_last.insert(input.name.clone(), ep);
} else {
cons_last.remove(&input.name);
}
debug!(
target: "brahman_handshake::broadcast",
cards = b.len(),
push_subscribers = push_table.len(),
"broadcast_match_diffs"
);
let cards: Vec<_> = b.cards().cloned().collect();
for cons in &cards {
let cons_session = cons.session;
let tx = match push_table.get(&cons_session) {
Some(tx) => tx,
None => continue,
};
let cons_last = last.entry(cons_session).or_default();
for input in &cons.inputs {
let new_match = b.find_producer_for(cons_session, &input.name);
let new_endpoint = new_match.as_ref().map(|m| m.producer.clone());
let old_endpoint = cons_last.get(&input.name).cloned();
if old_endpoint == new_endpoint {
continue;
}
if let Some(m) = &new_match {
let producer_service_socket = b
.cards()
.find(|c| c.session == m.producer.session)
.and_then(|c| c.service_socket.clone());
let event = MatchEvent {
kind: MatchEventKind::Available,
consumer_flow: input.name.clone(),
producer_session: m.producer.session,
producer_label: m.producer_label.clone(),
producer_flow: m.producer.flow_name.clone(),
ty: m.ty.clone(),
via: m.via,
pinned: m.pinned,
producer_service_socket,
};
let send_res = tx.try_send(Frame::MatchEvent(event));
debug!(
target: "brahman_handshake::broadcast",
consumer = %cons_session,
flow = %input.name,
producer = %m.producer_label,
result = ?send_res.as_ref().map(|_| "ok").unwrap_or_else(|e| match e {
tokio::sync::mpsc::error::TrySendError::Full(_) => "full",
tokio::sync::mpsc::error::TrySendError::Closed(_) => "closed",
}),
"Available pushed"
);
} else {
let event = MatchEvent {
kind: MatchEventKind::Lost,
consumer_flow: input.name.clone(),
producer_session: Ulid::nil(),
producer_label: String::new(),
producer_flow: String::new(),
ty: input.ty.clone(),
via: brahman_broker::MatchStrategy::Exact,
pinned: false,
producer_service_socket: None,
};
let _ = tx.try_send(Frame::MatchEvent(event));
}
if let Some(ep) = new_endpoint {
cons_last.insert(input.name.clone(), ep);
} else {
cons_last.remove(&input.name);
}
}
}
}
/// Lee el Hello, valida, registra la sesión y emite HelloAck.
/// Devuelve `Some(session_id)` si el handshake fue exitoso.
async fn do_handshake(&mut self) -> std::io::Result<Option<SessionId>> {
let frame = read_frame(&mut self.stream).await?;
let hello = match frame {
Frame::Hello(h) => h,
_ => {
write_frame(
&mut self.stream,
&Frame::Error(HandshakeError::Rejected(
"primer frame debe ser Hello".into(),
)),
)
.await?;
return Ok(None);
}
};
if let Some(err) = self.validate_hello(&hello) {
write_frame(&mut self.stream, &Frame::Error(err)).await?;
/// Lee el Hello, valida, registra la sesión y emite HelloAck.
async fn do_handshake<S>(
stream: &mut S,
config: &ServerConfig,
sessions: &SessionRegistry,
) -> std::io::Result<Option<SessionId>>
where
S: AsyncRead + AsyncWrite + Unpin,
{
let frame = read_frame(stream).await?;
let hello = match frame {
Frame::Hello(h) => h,
_ => {
write_frame(
stream,
&Frame::Error(HandshakeError::Rejected(
"primer frame debe ser Hello".into(),
)),
)
.await?;
return Ok(None);
}
};
let session_id = Ulid::new();
// WireCard → Card: extensiones quedan vacías post-wire (es el contrato).
let card: Card = hello.card.into();
self.register_session(session_id, card, hello.wit).await;
let ack = HelloAck {
server_version: crate::HANDSHAKE_VERSION.to_string(),
protocol_version: brahman_card::PROTOCOL_VERSION.to_string(),
session: session_id,
init_attached: self.config.init_attached,
};
write_frame(&mut self.stream, &Frame::HelloAck(ack)).await?;
debug!(session = %session_id, "handshake completado");
Ok(Some(session_id))
if let Some(err) = validate_hello(&hello) {
write_frame(stream, &Frame::Error(err)).await?;
return Ok(None);
}
/// 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(
&self,
session_id: SessionId,
card: Card,
wit: Option<WitInterface>,
) {
if let Some(broker) = &self.config.broker {
broker
.lock()
.await
.register(session_id, &card, wit.clone());
}
let resolved = match wit {
Some(w) => ResolvedCard::from_conscious(card, w),
None => ResolvedCard::from_agnostic(card),
};
self.sessions.lock().await.insert(session_id, resolved);
}
let session_id = Ulid::new();
let card: Card = hello.card.into();
register_session(session_id, card, hello.wit, config, sessions).await;
/// Validaciones que el servidor aplica al Hello del cliente.
fn validate_hello(&self, hello: &Hello) -> Option<HandshakeError> {
if hello.schema_version != CARD_SCHEMA_VERSION {
return Some(HandshakeError::SchemaMismatch {
client: hello.schema_version,
server: CARD_SCHEMA_VERSION,
});
}
if hello.protocol_version != brahman_card::PROTOCOL_VERSION {
return Some(HandshakeError::ProtocolMismatch(format!(
"cliente={}, servidor={}",
hello.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());
if let Err(e) = as_card.validate() {
return Some(HandshakeError::InvalidCard(e.to_string()));
}
None
let ack = HelloAck {
server_version: crate::HANDSHAKE_VERSION.to_string(),
protocol_version: brahman_card::PROTOCOL_VERSION.to_string(),
session: session_id,
init_attached: config.init_attached,
};
write_frame(stream, &Frame::HelloAck(ack)).await?;
debug!(session = %session_id, "handshake completado");
Ok(Some(session_id))
}
async fn register_session(
session_id: SessionId,
card: Card,
wit: Option<WitInterface>,
config: &ServerConfig,
sessions: &SessionRegistry,
) {
if let Some(broker) = &config.broker {
broker
.lock()
.await
.register(session_id, &card, wit.clone());
}
let resolved = match wit {
Some(w) => ResolvedCard::from_conscious(card, w),
None => ResolvedCard::from_agnostic(card),
};
sessions.lock().await.insert(session_id, resolved);
}
fn validate_hello(hello: &Hello) -> Option<HandshakeError> {
if hello.schema_version != CARD_SCHEMA_VERSION {
return Some(HandshakeError::SchemaMismatch {
client: hello.schema_version,
server: CARD_SCHEMA_VERSION,
});
}
if hello.protocol_version != brahman_card::PROTOCOL_VERSION {
return Some(HandshakeError::ProtocolMismatch(format!(
"cliente={}, servidor={}",
hello.protocol_version,
brahman_card::PROTOCOL_VERSION
)));
}
let as_card: Card = Card::from(hello.card.clone());
if let Err(e) = as_card.validate() {
return Some(HandshakeError::InvalidCard(e.to_string()));
}
None
}
fn now_ms() -> u64 {