echo/src/link.rs
Jean aab64baf86
nickserv: log out the loser of a registration conflict
When a peer's registration wins a name a local session was logged into,
that session authenticated against a credential that no longer exists.
The node now clears its accountname (the ircd emits RPL_LOGGEDOUT) and
tells the user why. A new outbound channel lets the engine push these
services-initiated actions to the uplink from the gossip path, drained
by the link loop alongside incoming traffic.
2026-07-12 13:34:38 +00:00

86 lines
3.6 KiB
Rust

use std::sync::Arc;
use anyhow::Result;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::TcpStream;
use tokio::sync::{mpsc, Mutex};
use crate::engine::db::Db;
use crate::engine::Engine;
use crate::proto::{NetAction, Protocol};
// One uplink session: connect, handshake + burst, then translate lines forever.
// The engine is shared with the gossip layer, so it is locked per operation and
// never held across the registration key-stretching await.
pub async fn run(mut proto: Box<dyn Protocol>, engine: Arc<Mutex<Engine>>, addr: &str, mut irc_rx: mpsc::UnboundedReceiver<NetAction>) -> Result<()> {
let stream = TcpStream::connect(addr).await?;
let (read, mut write) = stream.into_split();
let mut lines = BufReader::new(read).lines();
for line in proto.handshake() {
send(&mut write, &line).await?;
}
let startup = engine.lock().await.startup_actions();
for action in startup {
for line in proto.serialize(&action) {
send(&mut write, &line).await?;
}
}
loop {
tokio::select! {
// A line from the uplink: translate it and answer.
line = lines.next_line() => {
let Some(line) = line? else { break };
tracing::debug!(dir = "<<", %line);
for event in proto.parse(&line) {
let actions = engine.lock().await.handle(event);
for action in actions {
let outs = match action {
// Registration: derive the password off the reactor so the
// ~1s of key stretching can't stall the link, after a cheap
// gate that rejects taken names and rate-limits floods.
NetAction::DeferRegister { account, password, email, reply } => {
let pre = engine.lock().await.pre_register_check(&account, &reply);
match pre {
Some(rejection) => rejection,
None => {
let iterations = engine.lock().await.scram_iterations();
let creds = tokio::task::spawn_blocking(move || {
Db::derive_credentials(&password, iterations)
})
.await?;
engine.lock().await.complete_register(&account, creds, email, reply)
}
}
}
action => vec![action],
};
for act in outs {
for out in proto.serialize(&act) {
send(&mut write, &out).await?;
}
}
}
}
}
// A services-initiated action (e.g. a forced logout after a lost
// registration conflict), pushed by the engine from the gossip path.
Some(action) = irc_rx.recv() => {
for out in proto.serialize(&action) {
send(&mut write, &out).await?;
}
}
}
}
tracing::warn!("uplink closed the connection");
Ok(())
}
async fn send(write: &mut (impl AsyncWriteExt + Unpin), line: &str) -> Result<()> {
tracing::debug!(dir = ">>", %line);
write.write_all(line.as_bytes()).await?;
write.write_all(b"\r\n").await?;
Ok(())
}