Skip to main content

hatchery_c2/restreamer/
runtime.rs

1//! Restreamer runtime: node uplink + core drain, no push to core.
2//!
3//! Core opens a long-lived stream to C2 and drains envelopes. C2 never dials core.
4//! Nodes speak multiplex frames; payloads stay opaque after the outer crypto layer.
5
6use std::collections::BTreeMap;
7use std::net::SocketAddr;
8use std::sync::{Arc, Mutex};
9use std::time::Instant;
10
11use hatchery_core::{
12    ChannelKind, DrainEnvelope, Frame, FrameError, FrameType, JournalReplyBody, OpenPayload,
13    MAX_DATA_PAYLOAD,
14};
15use tokio::io::{AsyncReadExt, AsyncWriteExt};
16use tokio::net::{TcpListener, TcpStream};
17use tokio::sync::watch;
18
19use super::buffer::{EnqueueOutcome, ReplayBuffer, StreamKey};
20use super::crypto::{c2_strip_outer, RestreamerKeys};
21use super::journal::{recover_gap, NodeJournal};
22
23#[derive(Clone, Debug)]
24pub struct RestreamerConfig {
25    /// Node uplink listen (edge / WG tunnel address in production).
26    pub node_listen: SocketAddr,
27    /// Core drain listen (reachable only from the core tunnel).
28    pub drain_listen: SocketAddr,
29    pub buffer: super::buffer::BufferLimits,
30    /// Auth token core must present as the first line before draining.
31    /// Never logged.
32    pub core_token: String,
33}
34
35#[derive(Debug)]
36pub enum RestreamerError {
37    Io(std::io::Error),
38    Config(&'static str),
39    Frame(FrameError),
40}
41
42impl std::fmt::Display for RestreamerError {
43    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44        match self {
45            Self::Io(e) => write!(f, "{e}"),
46            Self::Config(m) => write!(f, "{m}"),
47            Self::Frame(e) => write!(f, "{e}"),
48        }
49    }
50}
51
52impl std::error::Error for RestreamerError {}
53
54impl From<std::io::Error> for RestreamerError {
55    fn from(value: std::io::Error) -> Self {
56        Self::Io(value)
57    }
58}
59
60/// Live node uplinks so C2 can send JournalReq when recovering gaps.
61#[derive(Clone, Default)]
62pub struct UplinkPeers {
63    inner: Arc<Mutex<BTreeMap<String, PeerSlot>>>,
64}
65
66struct PeerSlot {
67    out: tokio::sync::mpsc::Sender<Frame>,
68    pending: Option<tokio::sync::oneshot::Sender<JournalReplyBody>>,
69}
70
71impl UplinkPeers {
72    pub fn new() -> Self {
73        Self::default()
74    }
75
76    pub fn register(&self, node_id: String, tx: tokio::sync::mpsc::Sender<Frame>) {
77        self.inner.lock().expect("peers").insert(
78            node_id,
79            PeerSlot {
80                out: tx,
81                pending: None,
82            },
83        );
84    }
85
86    pub fn unregister(&self, node_id: &str) {
87        self.inner.lock().expect("peers").remove(node_id);
88    }
89
90    pub fn deliver_reply(&self, node_id: &str, body: JournalReplyBody) {
91        let mut guard = self.inner.lock().expect("peers");
92        if let Some(slot) = guard.get_mut(node_id) {
93            if let Some(tx) = slot.pending.take() {
94                let _ = tx.send(body);
95            }
96        }
97    }
98
99    /// Relay one opaque downlink frame (core -> node). C2 does not read the payload.
100    pub async fn send_to(&self, node_id: &str, frame: Frame) -> Result<(), RestreamerError> {
101        let out = {
102            let guard = self.inner.lock().expect("peers");
103            guard.get(node_id).map(|slot| slot.out.clone())
104        };
105        let Some(out) = out else {
106            return Err(RestreamerError::Frame(FrameError::Io("node uplink absent".into())));
107        };
108        out.send(frame)
109            .await
110            .map_err(|_| RestreamerError::Frame(FrameError::Io("node uplink closed".into())))
111    }
112
113    pub fn is_registered(&self, node_id: &str) -> bool {
114        self.inner.lock().expect("peers").contains_key(node_id)
115    }
116
117    pub async fn request_journal(
118        &self,
119        node_id: &str,
120        channel_id: u32,
121        after: u64,
122        limit: u32,
123        timeout: std::time::Duration,
124    ) -> Result<JournalReplyBody, RestreamerError> {
125        let (tx, rx) = tokio::sync::oneshot::channel();
126        let out = {
127            let mut guard = self.inner.lock().expect("peers");
128            let Some(slot) = guard.get_mut(node_id) else {
129                return Err(RestreamerError::Frame(FrameError::Io(
130                    "node uplink absent".into(),
131                )));
132            };
133            slot.pending = Some(tx);
134            slot.out.clone()
135        };
136        out.send(Frame::journal_req(channel_id, after, limit))
137            .await
138            .map_err(|_| RestreamerError::Frame(FrameError::Io("node uplink closed".into())))?;
139        match tokio::time::timeout(timeout, rx).await {
140            Ok(Ok(body)) => Ok(body),
141            Ok(Err(_)) => Err(RestreamerError::Frame(FrameError::Io(
142                "journal reply dropped".into(),
143            ))),
144            Err(_) => Err(RestreamerError::Frame(FrameError::Io(
145                "journal reply timeout".into(),
146            ))),
147        }
148    }
149}
150
151struct Inner {
152    buffer: ReplayBuffer,
153    /// channel_id -> kind, for OPEN bookkeeping (metadata only).
154    channels: BTreeMap<(String, u32), ChannelKind>,
155    keys: Option<RestreamerKeys>,
156    /// True once any outbound dial toward a "core" address was attempted — must stay false.
157    pushed_to_core: bool,
158}
159
160/// Shared restreamer state.
161#[derive(Clone)]
162pub struct Restreamer {
163    inner: Arc<Mutex<Inner>>,
164    journal: Arc<dyn NodeJournal>,
165    peers: UplinkPeers,
166}
167
168impl Restreamer {
169    pub fn new(
170        buffer_limits: super::buffer::BufferLimits,
171        journal: Arc<dyn NodeJournal>,
172        core_token: impl Into<String>,
173        keys: Option<RestreamerKeys>,
174    ) -> Self {
175        let _ = core_token; // validated by callers / RestreamerConfig
176        Self {
177            inner: Arc::new(Mutex::new(Inner {
178                buffer: ReplayBuffer::new(buffer_limits).with_default_credit(64),
179                channels: BTreeMap::new(),
180                keys,
181                pushed_to_core: false,
182            })),
183            journal,
184            peers: UplinkPeers::new(),
185        }
186    }
187
188    pub fn set_keys(&self, keys: RestreamerKeys) {
189        self.inner.lock().expect("lock").keys = Some(keys);
190    }
191
192    pub fn peers(&self) -> &UplinkPeers {
193        &self.peers
194    }
195
196    pub fn upstream_public_keys(&self) -> Option<(super::crypto::PublicKeyBytes, super::crypto::PublicKeyBytes)> {
197        self.inner
198            .lock()
199            .expect("lock")
200            .keys
201            .as_ref()
202            .map(|k| k.upstream_public_keys())
203    }
204
205    /// Invariant: C2 never opens connections to core.
206    pub fn ever_pushed_to_core(&self) -> bool {
207        self.inner.lock().expect("lock").pushed_to_core
208    }
209
210    /// Admit one opaque DATA payload from a node (outer layer already optional).
211    /// If keys are configured, `payload` is treated as outer-sealed and stripped.
212    pub fn admit_from_node(
213        &self,
214        node_id: &str,
215        channel_id: u32,
216        payload: Vec<u8>,
217    ) -> Result<EnqueueOutcome, RestreamerError> {
218        if payload.len() > MAX_DATA_PAYLOAD {
219            return Err(RestreamerError::Frame(FrameError::PayloadTooLarge(payload.len())));
220        }
221        let mut guard = self.inner.lock().expect("lock");
222        let opaque = if let Some(keys) = guard.keys.clone() {
223            c2_strip_outer(&keys, &payload).map_err(|e| {
224                RestreamerError::Frame(FrameError::Io(format!("outer layer: {e}")))
225            })?
226        } else {
227            payload
228        };
229        // Also record in journal view for gap recovery tests / local nodes.
230        let outcome = guard
231            .buffer
232            .enqueue(node_id, channel_id, opaque.clone(), Instant::now());
233        if let EnqueueOutcome::Accepted { .. } = outcome {
234            let seq = guard.buffer.next_seq(node_id, channel_id) - 1;
235            drop(guard);
236            // best-effort journal mirror when the journal is a MemoryJournal-compatible append —
237            // production journals live on the node; C2 asks via NodeJournal trait.
238            let _ = (seq, opaque);
239        }
240        Ok(outcome)
241    }
242
243    pub fn open_channel(&self, node_id: &str, channel_id: u32, kind: ChannelKind) {
244        self.inner
245            .lock()
246            .expect("lock")
247            .channels
248            .insert((node_id.to_owned(), channel_id), kind);
249    }
250
251    pub fn ack(&self, node_id: &str, channel_id: u32, seq: u64) {
252        self.inner
253            .lock()
254            .expect("lock")
255            .buffer
256            .ack(node_id, channel_id, seq);
257    }
258
259    pub fn window_credit(&self, node_id: &str, channel_id: u32) -> u32 {
260        let key = StreamKey {
261            node_id: node_id.to_owned(),
262            channel_id,
263        };
264        self.inner.lock().expect("lock").buffer.credit(&key)
265    }
266
267    /// Produce drain envelopes for RESUME(after=N). Recovers gaps from the node journal.
268    pub fn resume(
269        &self,
270        node_id: &str,
271        channel_id: u32,
272        after: u64,
273    ) -> Result<Vec<DrainEnvelope>, RestreamerError> {
274        let (gap, oldest, buffered) = {
275            let guard = self.inner.lock().expect("lock");
276            let gap = guard.buffer.gap_before_buffer(node_id, channel_id, after);
277            let oldest = guard.buffer.oldest_seq(node_id, channel_id);
278            let buffered = guard.buffer.events_after(node_id, channel_id, after);
279            (gap, oldest, buffered)
280        };
281        let mut out = Vec::new();
282        if gap {
283            let until = oldest;
284            let recovered = recover_gap(
285                self.journal.as_ref(),
286                node_id,
287                channel_id,
288                after,
289                until,
290                1024,
291            )
292            .map_err(|e| RestreamerError::Frame(FrameError::Io(e.to_string())))?;
293            for ev in recovered {
294                out.push(DrainEnvelope {
295                    node_id: ev.node_id.clone(),
296                    frame: Frame::data(ev.channel_id, ev.seq, ev.payload)
297                        .map_err(RestreamerError::Frame)?,
298                });
299            }
300        }
301        for ev in buffered {
302            // skip duplicates already recovered
303            if out.iter().any(|e| e.frame.seq == ev.seq) {
304                continue;
305            }
306            out.push(DrainEnvelope {
307                node_id: ev.node_id.clone(),
308                frame: Frame::data(ev.channel_id, ev.seq, ev.payload)
309                    .map_err(RestreamerError::Frame)?,
310            });
311        }
312        Ok(out)
313    }
314
315    /// Prefer live uplink journal; fall back to the configured NodeJournal.
316    pub async fn resume_with_uplink(
317        &self,
318        node_id: &str,
319        channel_id: u32,
320        after: u64,
321    ) -> Result<Vec<DrainEnvelope>, RestreamerError> {
322        let (gap, oldest, buffered) = {
323            let guard = self.inner.lock().expect("lock");
324            let gap = guard.buffer.gap_before_buffer(node_id, channel_id, after);
325            let oldest = guard.buffer.oldest_seq(node_id, channel_id);
326            let buffered = guard.buffer.events_after(node_id, channel_id, after);
327            (gap, oldest, buffered)
328        };
329        let mut out = Vec::new();
330        if gap {
331            let until = oldest;
332            let recovered = if self.peers.is_registered(node_id) {
333                let body = self
334                    .peers
335                    .request_journal(
336                        node_id,
337                        channel_id,
338                        after,
339                        1024,
340                        std::time::Duration::from_secs(5),
341                    )
342                    .await?;
343                body.entries
344                    .into_iter()
345                    .filter(|e| until.map(|u| e.seq < u).unwrap_or(true))
346                    .map(|e| super::buffer::BufferedEvent {
347                        seq: e.seq,
348                        node_id: node_id.to_owned(),
349                        channel_id,
350                        payload: e.payload,
351                        enqueued_at: Instant::now(),
352                    })
353                    .collect()
354            } else {
355                recover_gap(
356                    self.journal.as_ref(),
357                    node_id,
358                    channel_id,
359                    after,
360                    until,
361                    1024,
362                )
363                .map_err(|e| RestreamerError::Frame(FrameError::Io(e.to_string())))?
364            };
365            for ev in recovered {
366                out.push(DrainEnvelope {
367                    node_id: ev.node_id.clone(),
368                    frame: Frame::data(ev.channel_id, ev.seq, ev.payload)
369                        .map_err(RestreamerError::Frame)?,
370                });
371            }
372        }
373        for ev in buffered {
374            if out.iter().any(|e| e.frame.seq == ev.seq) {
375                continue;
376            }
377            out.push(DrainEnvelope {
378                node_id: ev.node_id.clone(),
379                frame: Frame::data(ev.channel_id, ev.seq, ev.payload)
380                    .map_err(RestreamerError::Frame)?,
381            });
382        }
383        Ok(out)
384    }
385
386    /// Snapshot of buffered events for a stream (no journal).
387    pub fn buffered_after(&self, node_id: &str, channel_id: u32, after: u64) -> Vec<DrainEnvelope> {
388        let guard = self.inner.lock().expect("lock");
389        guard
390            .buffer
391            .events_after(node_id, channel_id, after)
392            .into_iter()
393            .filter_map(|ev| {
394                Frame::data(ev.channel_id, ev.seq, ev.payload)
395                    .ok()
396                    .map(|frame| DrainEnvelope {
397                        node_id: ev.node_id,
398                        frame,
399                    })
400            })
401            .collect()
402    }
403}
404
405/// Bind node uplink + core drain listeners and serve until shutdown.
406pub async fn serve(
407    config: RestreamerConfig,
408    restreamer: Restreamer,
409    mut shutdown: watch::Receiver<bool>,
410) -> Result<(), RestreamerError> {
411    if config.node_listen == config.drain_listen {
412        return Err(RestreamerError::Config(
413            "node_listen and drain_listen must differ",
414        ));
415    }
416    if config.core_token.len() < 16 {
417        return Err(RestreamerError::Config("core_token too short"));
418    }
419    let node_listener = TcpListener::bind(config.node_listen).await?;
420    let drain_listener = TcpListener::bind(config.drain_listen).await?;
421    loop {
422        tokio::select! {
423            _ = shutdown.changed() => {
424                if *shutdown.borrow() { break; }
425            }
426            accepted = node_listener.accept() => {
427                let (stream, _) = accepted?;
428                let rs = restreamer.clone();
429                tokio::spawn(async move {
430                    if let Err(err) = handle_node_uplink(stream, rs).await {
431                        tracing::debug!(error = %err, "node uplink closed");
432                    }
433                });
434            }
435            accepted = drain_listener.accept() => {
436                let (stream, _) = accepted?;
437                let rs = restreamer.clone();
438                let token = config.core_token.clone();
439                tokio::spawn(async move {
440                    if let Err(err) = handle_core_drain(stream, rs, &token).await {
441                        tracing::debug!(error = %err, "core drain closed");
442                    }
443                });
444            }
445        }
446    }
447    Ok(())
448}
449
450async fn handle_node_uplink(mut stream: TcpStream, rs: Restreamer) -> Result<(), RestreamerError> {
451    let mut buf = vec![0u8; 64 * 1024];
452    let mut filled = 0usize;
453    let mut node_id: Option<String> = None;
454    let (out_tx, mut out_rx) = tokio::sync::mpsc::channel::<Frame>(16);
455    loop {
456        tokio::select! {
457            biased;
458            maybe_out = out_rx.recv() => {
459                match maybe_out {
460                    Some(frame) => {
461                        stream
462                            .write_all(&frame.encode().map_err(RestreamerError::Frame)?)
463                            .await?;
464                    }
465                    None => break,
466                }
467            }
468            read = stream.read(&mut buf[filled..]) => {
469                let n = read?;
470                if n == 0 {
471                    break;
472                }
473                filled += n;
474                while filled > 0 {
475                    match Frame::decode(&buf[..filled]) {
476                        Ok((frame, consumed)) => {
477                            buf.copy_within(consumed..filled, 0);
478                            filled -= consumed;
479                            match frame.frame_type {
480                                FrameType::Open => {
481                                    let open = frame.parse_open().map_err(RestreamerError::Frame)?;
482                                    if let Some(prev) = node_id.replace(open.node_id.clone()) {
483                                        rs.peers().unregister(&prev);
484                                    }
485                                    rs.peers().register(open.node_id.clone(), out_tx.clone());
486                                    rs.open_channel(&open.node_id, frame.channel_id, open.channel_kind);
487                                    let credit = rs.window_credit(&open.node_id, frame.channel_id);
488                                    let win = Frame::window(frame.channel_id, credit);
489                                    stream
490                                        .write_all(&win.encode().map_err(RestreamerError::Frame)?)
491                                        .await?;
492                                }
493                                FrameType::Data => {
494                                    let Some(ref nid) = node_id else {
495                                        let go = Frame::goaway(1);
496                                        stream
497                                            .write_all(&go.encode().map_err(RestreamerError::Frame)?)
498                                            .await?;
499                                        return Ok(());
500                                    };
501                                    match rs.admit_from_node(nid, frame.channel_id, frame.payload)? {
502                                        EnqueueOutcome::Accepted { window_credit } => {
503                                            let win = Frame::window(frame.channel_id, window_credit);
504                                            stream
505                                                .write_all(
506                                                    &win.encode().map_err(RestreamerError::Frame)?,
507                                                )
508                                                .await?;
509                                        }
510                                        EnqueueOutcome::Backpressure => {
511                                            let win = Frame::window(frame.channel_id, 0);
512                                            stream
513                                                .write_all(
514                                                    &win.encode().map_err(RestreamerError::Frame)?,
515                                                )
516                                                .await?;
517                                        }
518                                    }
519                                }
520                                FrameType::JournalReply => {
521                                    if let Some(ref nid) = node_id {
522                                        let body = JournalReplyBody::decode(&frame.payload)
523                                            .map_err(RestreamerError::Frame)?;
524                                        rs.peers().deliver_reply(nid, body);
525                                    }
526                                }
527                                FrameType::Ping => {
528                                    let pong = Frame::pong(frame.seq);
529                                    stream
530                                        .write_all(&pong.encode().map_err(RestreamerError::Frame)?)
531                                        .await?;
532                                }
533                                FrameType::Close | FrameType::GoAway => {
534                                    if let Some(ref nid) = node_id {
535                                        rs.peers().unregister(nid);
536                                    }
537                                    return Ok(());
538                                }
539                                _ => {}
540                            }
541                        }
542                        Err(FrameError::Truncated) => break,
543                        Err(err) => return Err(RestreamerError::Frame(err)),
544                    }
545                }
546                if filled == buf.len() {
547                    return Err(RestreamerError::Frame(FrameError::PayloadTooLarge(filled)));
548                }
549            }
550        }
551    }
552    if let Some(ref nid) = node_id {
553        rs.peers().unregister(nid);
554    }
555    Ok(())
556}
557
558async fn handle_core_drain(
559    mut stream: TcpStream,
560    rs: Restreamer,
561    expected_token: &str,
562) -> Result<(), RestreamerError> {
563    // Auth: first line "TOKEN\n" (constant-time compare).
564    let mut auth = Vec::with_capacity(128);
565    loop {
566        let mut byte = [0u8; 1];
567        let n = stream.read(&mut byte).await?;
568        if n == 0 {
569            return Err(RestreamerError::Config("drain auth closed"));
570        }
571        if byte[0] == b'\n' {
572            break;
573        }
574        auth.push(byte[0]);
575        if auth.len() > 512 {
576            return Err(RestreamerError::Config("drain auth too long"));
577        }
578    }
579    if !tokens_equal(&auth, expected_token.as_bytes()) {
580        let go = Frame::goaway(403);
581        let _ = stream.write_all(&go.encode().unwrap_or_default()).await;
582        return Err(RestreamerError::Config("drain auth refused"));
583    }
584
585    let mut buf = vec![0u8; 64 * 1024];
586    let mut filled = 0usize;
587    // Default stream selection via OPEN from core (opaque node id).
588    let mut selected: Option<(String, u32)> = None;
589    // Highest seq already written on this drain session (per selected stream).
590    let mut sent_through: u64 = 0;
591    // Core must RESUME once before we drain; reconnects always start with RESUME.
592    let mut resumed = false;
593    loop {
594        // After the first RESUME, opportunistically drain newly buffered events.
595        if resumed {
596            if let Some((ref nid, ch)) = selected {
597                let envelopes = rs.buffered_after(nid, ch, sent_through);
598                for env in envelopes {
599                    stream
600                        .write_all(&env.encode().map_err(RestreamerError::Frame)?)
601                        .await?;
602                    sent_through = sent_through.max(env.frame.seq);
603                }
604            }
605        }
606
607        let read = tokio::time::timeout(
608            std::time::Duration::from_millis(50),
609            stream.read(&mut buf[filled..]),
610        )
611        .await;
612        match read {
613            Ok(Ok(0)) => break,
614            Ok(Ok(n)) => {
615                filled += n;
616                while filled > 0 {
617                    match Frame::decode(&buf[..filled]) {
618                        Ok((frame, consumed)) => {
619                            buf.copy_within(consumed..filled, 0);
620                            filled -= consumed;
621                            match frame.frame_type {
622                                FrameType::Open => {
623                                    let open =
624                                        frame.parse_open().map_err(RestreamerError::Frame)?;
625                                    selected = Some((open.node_id, frame.channel_id));
626                                    sent_through = 0;
627                                    resumed = false;
628                                }
629                                FrameType::Ack => {
630                                    if let Some((ref nid, ch)) = selected {
631                                        rs.ack(nid, ch, frame.seq);
632                                    }
633                                }
634                                FrameType::Resume => {
635                                    if let Some((ref nid, ch)) = selected {
636                                        let after = frame.seq;
637                                        let envelopes = rs.resume_with_uplink(nid, ch, after).await?;
638                                        for env in envelopes {
639                                            stream
640                                                .write_all(
641                                                    &env.encode().map_err(RestreamerError::Frame)?,
642                                                )
643                                                .await?;
644                                            sent_through = sent_through.max(env.frame.seq);
645                                        }
646                                        // Even with an empty replay, do not resend <= after.
647                                        sent_through = sent_through.max(after);
648                                        resumed = true;
649                                    }
650                                }
651                                FrameType::Data => {
652                                    // Downlink: opaque bytes for the selected node, relayed as-is.
653                                    if let Some((ref nid, ch)) = selected {
654                                        let down = Frame::data(ch, frame.seq, frame.payload)
655                                            .map_err(RestreamerError::Frame)?;
656                                        if let Err(err) = rs.peers().send_to(nid, down).await {
657                                            tracing::debug!(error = %err, "downlink dropped");
658                                        }
659                                    }
660                                }
661                                FrameType::Ping => {
662                                    let pong = Frame::pong(frame.seq);
663                                    stream
664                                        .write_all(
665                                            &pong.encode().map_err(RestreamerError::Frame)?,
666                                        )
667                                        .await?;
668                                }
669                                FrameType::Close | FrameType::GoAway => return Ok(()),
670                                _ => {}
671                            }
672                        }
673                        Err(FrameError::Truncated) => break,
674                        Err(err) => return Err(RestreamerError::Frame(err)),
675                    }
676                }
677            }
678            Ok(Err(e)) => return Err(e.into()),
679            Err(_) => {
680                // idle tick — loop to flush buffer
681            }
682        }
683    }
684    Ok(())
685}
686
687fn tokens_equal(a: &[u8], b: &[u8]) -> bool {
688    if a.len() != b.len() {
689        return false;
690    }
691    let mut diff = 0u8;
692    for (x, y) in a.iter().zip(b.iter()) {
693        diff |= x ^ y;
694    }
695    diff == 0
696}
697
698/// Core-side drain client: dials C2, authenticates, RESUMEs, ACKs.
699pub struct DrainClient {
700    addr: SocketAddr,
701    token: String,
702}
703
704impl DrainClient {
705    pub fn new(addr: SocketAddr, token: impl Into<String>) -> Self {
706        Self {
707            addr,
708            token: token.into(),
709        }
710    }
711
712    pub async fn connect(&self) -> Result<DrainSession, RestreamerError> {
713        let mut stream = TcpStream::connect(self.addr).await?;
714        stream.write_all(self.token.as_bytes()).await?;
715        stream.write_all(b"\n").await?;
716        Ok(DrainSession { stream })
717    }
718}
719
720pub struct DrainSession {
721    stream: TcpStream,
722}
723
724impl DrainSession {
725    pub async fn open(&mut self, node_id: &str, channel_id: u32, kind: ChannelKind) -> Result<(), RestreamerError> {
726        let frame = Frame::open(
727            channel_id,
728            &OpenPayload {
729                channel_kind: kind,
730                node_id: node_id.to_owned(),
731            },
732        )
733        .map_err(RestreamerError::Frame)?;
734        self.stream
735            .write_all(&frame.encode().map_err(RestreamerError::Frame)?)
736            .await?;
737        Ok(())
738    }
739
740    pub async fn resume(&mut self, after: u64) -> Result<(), RestreamerError> {
741        let frame = Frame::resume(after);
742        self.stream
743            .write_all(&frame.encode().map_err(RestreamerError::Frame)?)
744            .await?;
745        Ok(())
746    }
747
748    pub async fn ack(&mut self, seq: u64) -> Result<(), RestreamerError> {
749        let frame = Frame::ack(seq);
750        self.stream
751            .write_all(&frame.encode().map_err(RestreamerError::Frame)?)
752            .await?;
753        Ok(())
754    }
755
756    pub async fn recv_envelope(
757        &mut self,
758        deadline: std::time::Duration,
759    ) -> Result<Option<DrainEnvelope>, RestreamerError> {
760        let mut buf = vec![0u8; 64 * 1024];
761        let n = match tokio::time::timeout(deadline, self.stream.read(&mut buf)).await {
762            Ok(Ok(0)) => return Ok(None),
763            Ok(Ok(n)) => n,
764            Ok(Err(e)) => return Err(e.into()),
765            Err(_) => return Ok(None),
766        };
767        let (env, _) = DrainEnvelope::decode(&buf[..n]).map_err(RestreamerError::Frame)?;
768        Ok(Some(env))
769    }
770}
771
772#[cfg(test)]
773mod tests {
774    use super::*;
775    use crate::restreamer::crypto::{core_open_inner, noise_session_seal, HqRecipientSecret};
776    use crate::restreamer::journal::MemoryJournal;
777    use hatchery_core::ChannelKind;
778    use std::time::Duration;
779
780    fn token() -> String {
781        "test-core-token-32bytes-minimum!".to_owned()
782    }
783
784    #[tokio::test]
785    async fn core_drains_and_acks_without_c2_push() {
786        let journal = Arc::new(MemoryJournal::new());
787        let rs = Restreamer::new(
788            super::super::buffer::BufferLimits {
789                max_events: 100,
790                max_bytes: 1_000_000,
791                max_age: Duration::from_secs(60),
792            },
793            journal,
794            token(),
795            None,
796        );
797        rs.open_channel("node-a", 1, ChannelKind::M4a);
798        assert!(matches!(
799            rs.admit_from_node("node-a", 1, b"cipher-1".to_vec()).unwrap(),
800            EnqueueOutcome::Accepted { .. }
801        ));
802        assert!(matches!(
803            rs.admit_from_node("node-a", 1, b"cipher-2".to_vec()).unwrap(),
804            EnqueueOutcome::Accepted { .. }
805        ));
806        let batch = rs.buffered_after("node-a", 1, 0);
807        assert_eq!(batch.len(), 2);
808        assert_eq!(batch[0].frame.payload, b"cipher-1");
809        rs.ack("node-a", 1, 1);
810        let batch = rs.buffered_after("node-a", 1, 0);
811        assert_eq!(batch.len(), 1);
812        assert_eq!(batch[0].frame.seq, 2);
813        assert!(!rs.ever_pushed_to_core());
814    }
815
816    #[tokio::test]
817    async fn live_drain_session_resume_and_ack() {
818        let journal = Arc::new(MemoryJournal::new());
819        // Pre-populate journal for gap recovery.
820        for seq in 1..=5u64 {
821            journal.append("node-a", 1, seq, format!("j{seq}").into_bytes());
822        }
823        let rs = Restreamer::new(
824            super::super::buffer::BufferLimits {
825                max_events: 2,
826                max_bytes: 1024,
827                max_age: Duration::from_secs(60),
828            },
829            journal.clone(),
830            token(),
831            None,
832        );
833        // Fill buffer with seq 4,5 only (simulating 1..3 already acked/expired).
834        // Manually: enqueue three then ack to leave two? limits max_events=2 so third backpressures.
835        rs.admit_from_node("node-a", 1, b"b1".to_vec()).unwrap();
836        rs.admit_from_node("node-a", 1, b"b2".to_vec()).unwrap();
837        // ack both so buffer empty but next_seq advances; then we cannot get old seqs from buffer.
838        rs.ack("node-a", 1, 2);
839        // Produce seq 3,4 in buffer
840        rs.admit_from_node("node-a", 1, b"b3".to_vec()).unwrap();
841        rs.admit_from_node("node-a", 1, b"b4".to_vec()).unwrap();
842
843        // RESUME after 0: gap for 1..2 from journal, then 3..4 from buffer.
844        // But journal has 1..5 with different payloads; buffer has its own seq 3,4 with b3/b4.
845        // For a clean gap test: ack everything, empty buffer, resume after 0 with journal only.
846        let journal2 = Arc::new(MemoryJournal::new());
847        for seq in 1..=4u64 {
848            journal2.append("node-b", 1, seq, format!("evt-{seq}").into_bytes());
849        }
850        let rs2 = Restreamer::new(
851            super::super::buffer::BufferLimits {
852                max_events: 10,
853                max_bytes: 10_000,
854                max_age: Duration::from_secs(60),
855            },
856            journal2,
857            token(),
858            None,
859        );
860        // Advance next_seq without retaining: enqueue + ack
861        for i in 1..=4u8 {
862            rs2.admit_from_node("node-b", 1, vec![i]).unwrap();
863        }
864        rs2.ack("node-b", 1, 4);
865        assert!(rs2.buffered_after("node-b", 1, 0).is_empty());
866        let recovered = rs2.resume("node-b", 1, 0).unwrap();
867        assert_eq!(recovered.len(), 4);
868        assert_eq!(recovered[0].frame.payload, b"evt-1");
869        assert_eq!(recovered[3].frame.seq, 4);
870        assert!(!rs2.ever_pushed_to_core());
871    }
872
873    #[tokio::test]
874    async fn tcp_drain_end_to_end() {
875        let journal = Arc::new(MemoryJournal::new());
876        let rs = Restreamer::new(
877            super::super::buffer::BufferLimits::default(),
878            journal,
879            token(),
880            None,
881        );
882        let node_l = TcpListener::bind("127.0.0.1:0").await.unwrap();
883        let drain_l = TcpListener::bind("127.0.0.1:0").await.unwrap();
884        let node_addr = node_l.local_addr().unwrap();
885        let drain_addr = drain_l.local_addr().unwrap();
886        drop(node_l);
887        drop(drain_l);
888        let (shutdown_tx, shutdown_rx) = watch::channel(false);
889        let cfg = RestreamerConfig {
890            node_listen: node_addr,
891            drain_listen: drain_addr,
892            buffer: super::super::buffer::BufferLimits::default(),
893            core_token: token(),
894        };
895        let serve_rs = rs.clone();
896        let handle = tokio::spawn(async move { serve(cfg, serve_rs, shutdown_rx).await });
897
898        // Give the server a moment to bind.
899        tokio::time::sleep(Duration::from_millis(20)).await;
900
901        // Node uplink
902        let mut node = TcpStream::connect(node_addr).await.unwrap();
903        let open = Frame::open(
904            1,
905            &OpenPayload {
906                channel_kind: ChannelKind::G4aEvents,
907                node_id: "node-a".into(),
908            },
909        )
910        .unwrap();
911        node.write_all(&open.encode().unwrap()).await.unwrap();
912        // read WINDOW
913        let mut rb = [0u8; 64];
914        let _ = node.read(&mut rb).await.unwrap();
915        let data = Frame::data(1, 0, b"opaque-event".to_vec()).unwrap();
916        // Note: node-assigned seq on wire is ignored; C2 assigns its own.
917        node.write_all(&data.encode().unwrap()).await.unwrap();
918        let _ = node.read(&mut rb).await.unwrap();
919
920        // Core drain
921        let client = DrainClient::new(drain_addr, token());
922        let mut session = client.connect().await.unwrap();
923        session
924            .open("node-a", 1, ChannelKind::G4aEvents)
925            .await
926            .unwrap();
927        session.resume(0).await.unwrap();
928        let env = session
929            .recv_envelope(Duration::from_secs(2))
930            .await
931            .unwrap()
932            .expect("envelope");
933        assert_eq!(env.node_id, "node-a");
934        assert_eq!(env.frame.payload, b"opaque-event");
935        session.ack(env.frame.seq).await.unwrap();
936
937        let _ = shutdown_tx.send(true);
938        let _ = handle.await;
939        assert!(!rs.ever_pushed_to_core());
940    }
941
942    #[test]
943    fn nested_crypto_on_admit_path() {
944        let hq = HqRecipientSecret::from_seed([11u8; 32]);
945        let keys = RestreamerKeys::from_seeds([13u8; 32], hq.public_bytes().0);
946        let journal = Arc::new(MemoryJournal::new());
947        let rs = Restreamer::new(
948            super::super::buffer::BufferLimits::default(),
949            journal,
950            token(),
951            Some(keys.clone()),
952        );
953        let sealed = noise_session_seal(&keys, &hq, b"visible-only-to-core").unwrap();
954        rs.admit_from_node("node-a", 1, sealed.outer).unwrap();
955        let env = &rs.buffered_after("node-a", 1, 0)[0];
956        assert!(!env
957            .frame
958            .payload
959            .windows(b"visible-only-to-core".len())
960            .any(|w| w == b"visible-only-to-core"));
961        let mut hq_tx = sealed.hq_transport;
962        let plain = core_open_inner(&mut hq_tx, &env.frame.payload).unwrap();
963        assert_eq!(plain, b"visible-only-to-core");
964    }
965
966    #[tokio::test]
967    async fn uplink_journal_req_served_from_file_journal() {
968        use crate::restreamer::FileJournal;
969        use hatchery_core::FrameType;
970
971        let dir = std::env::temp_dir().join(format!("upj-{}", std::process::id()));
972        let _ = std::fs::remove_dir_all(&dir);
973        let fj = FileJournal::open(dir.join("n.journal")).unwrap();
974        for i in 0..5u8 {
975            fj.append(1, &[i]).unwrap();
976        }
977        let journal = Arc::new(MemoryJournal::new());
978        let rs = Restreamer::new(
979            super::super::buffer::BufferLimits::default(),
980            journal,
981            token(),
982            None,
983        );
984        let node_l = TcpListener::bind("127.0.0.1:0").await.unwrap();
985        let drain_l = TcpListener::bind("127.0.0.1:0").await.unwrap();
986        let node_addr = node_l.local_addr().unwrap();
987        let drain_addr = drain_l.local_addr().unwrap();
988        drop(node_l);
989        drop(drain_l);
990        let (shutdown_tx, shutdown_rx) = watch::channel(false);
991        let cfg = RestreamerConfig {
992            node_listen: node_addr,
993            drain_listen: drain_addr,
994            buffer: super::super::buffer::BufferLimits::default(),
995            core_token: token(),
996        };
997        let serve_rs = rs.clone();
998        let handle = tokio::spawn(async move { serve(cfg, serve_rs, shutdown_rx).await });
999        tokio::time::sleep(Duration::from_millis(20)).await;
1000
1001        let fj_node = fj.clone();
1002        let node_task = tokio::spawn(async move {
1003            let mut node = TcpStream::connect(node_addr).await.unwrap();
1004            let open = Frame::open(
1005                1,
1006                &OpenPayload {
1007                    channel_kind: ChannelKind::G4aEvents,
1008                    node_id: "node-a".into(),
1009                },
1010            )
1011            .unwrap();
1012            node.write_all(&open.encode().unwrap()).await.unwrap();
1013            let mut rb = vec![0u8; 64 * 1024];
1014            let mut filled = 0usize;
1015            loop {
1016                let n = node.read(&mut rb[filled..]).await.unwrap();
1017                if n == 0 {
1018                    break;
1019                }
1020                filled += n;
1021                while filled > 0 {
1022                    match Frame::decode(&rb[..filled]) {
1023                        Ok((frame, consumed)) => {
1024                            rb.copy_within(consumed..filled, 0);
1025                            filled -= consumed;
1026                            if frame.frame_type == FrameType::JournalReq {
1027                                let limit = frame.journal_req_limit().unwrap_or(64) as usize;
1028                                let body = fj_node
1029                                    .reply_body(frame.channel_id, frame.seq, limit)
1030                                    .unwrap();
1031                                let reply = Frame::journal_reply(frame.channel_id, &body).unwrap();
1032                                node.write_all(&reply.encode().unwrap()).await.unwrap();
1033                            }
1034                        }
1035                        Err(FrameError::Truncated) => break,
1036                        Err(e) => panic!("{e}"),
1037                    }
1038                }
1039            }
1040        });
1041
1042        for _ in 0..50 {
1043            if rs.peers().is_registered("node-a") {
1044                break;
1045            }
1046            tokio::time::sleep(Duration::from_millis(10)).await;
1047        }
1048        assert!(rs.peers().is_registered("node-a"));
1049        let body = rs
1050            .peers()
1051            .request_journal("node-a", 1, 2, 10, Duration::from_secs(2))
1052            .await
1053            .unwrap();
1054        assert_eq!(
1055            body.entries.iter().map(|e| e.seq).collect::<Vec<_>>(),
1056            vec![3, 4, 5]
1057        );
1058        assert_eq!(body.entries[0].payload, &[2]);
1059
1060        let _ = shutdown_tx.send(true);
1061        node_task.abort();
1062        let _ = handle.await;
1063        let _ = std::fs::remove_dir_all(&dir);
1064    }
1065}