1use crate::uplink_journal::UplinkJournal;
17use hatchery_core::inner::{decode_inner, encode_inner, InnerKind};
18use hatchery_core::nested::{node_seal_outer, NoiseInitiator, NoiseTransport, PublicKeyBytes};
19use hatchery_core::{grow_read_buf, ChannelKind, Frame, FrameError, FrameType, OpenPayload};
20use serde_json::{json, Value};
21use std::collections::{BTreeMap, VecDeque};
22use std::net::SocketAddr;
23use std::sync::atomic::{AtomicBool, Ordering};
24use std::sync::{Arc, Mutex};
25use std::time::Duration;
26use tokio::io::{AsyncReadExt, AsyncWriteExt};
27use tokio::net::TcpStream;
28use tokio::sync::{mpsc, Notify};
29use tokio::task::JoinHandle;
30
31#[derive(Clone, Copy, Debug)]
33pub struct UpstreamKeys {
34 pub c2_public: PublicKeyBytes,
35 pub recipient_public: PublicKeyBytes,
36}
37
38#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
40pub enum SendPrio {
41 Control = 0,
43 Pty = 1,
45 Mail = 2,
47 Browser = 3,
49}
50
51const WRITE_TIMEOUT: Duration = Duration::from_secs(5);
52const CHAN_CAP_CONTROL: usize = 64;
53const CHAN_CAP_PTY: usize = 128;
54const CHAN_CAP_MAIL: usize = 16;
55const CHAN_CAP_BROWSER: usize = 8;
56
57struct Queued {
58 prio: SendPrio,
59 frame: Frame,
60}
61
62struct ChanQ {
63 q: VecDeque<Queued>,
64 credit: Option<u32>,
66}
67
68impl ChanQ {
69 fn new() -> Self {
70 Self { q: VecDeque::new(), credit: None }
71 }
72
73 fn cap_for(prio: SendPrio) -> usize {
74 match prio {
75 SendPrio::Control => CHAN_CAP_CONTROL,
76 SendPrio::Pty => CHAN_CAP_PTY,
77 SendPrio::Mail => CHAN_CAP_MAIL,
78 SendPrio::Browser => CHAN_CAP_BROWSER,
79 }
80 }
81}
82
83struct OutState {
84 chans: BTreeMap<u32, ChanQ>,
85}
86
87pub struct NodeUplink {
89 enqueue: mpsc::UnboundedSender<(u32, SendPrio, Frame)>,
90 routes: Arc<Mutex<BTreeMap<u32, mpsc::UnboundedSender<Vec<u8>>>>>,
91 credits: Arc<Mutex<BTreeMap<u32, u32>>>,
93 credit_pulse: Arc<Notify>,
94 journal: Option<Arc<UplinkJournal>>,
95 alive: Arc<AtomicBool>,
96 dead: Arc<Notify>,
97 tasks: Vec<JoinHandle<()>>,
98}
99
100impl Drop for NodeUplink {
101 fn drop(&mut self) {
102 self.alive.store(false, Ordering::SeqCst);
103 for t in &self.tasks {
104 t.abort();
105 }
106 }
107}
108
109impl NodeUplink {
110 pub async fn connect(uplink: SocketAddr, journal: Option<Arc<UplinkJournal>>) -> Result<Arc<Self>, String> {
111 let stream = TcpStream::connect(uplink).await.map_err(|e| e.to_string())?;
112 let _ = stream.set_nodelay(true);
113 let (mut rd, mut wr) = stream.into_split();
114 let (enq_tx, mut enq_rx) = mpsc::unbounded_channel::<(u32, SendPrio, Frame)>();
115 let routes: Arc<Mutex<BTreeMap<u32, mpsc::UnboundedSender<Vec<u8>>>>> = Arc::default();
116 let credits: Arc<Mutex<BTreeMap<u32, u32>>> = Arc::default();
117 let credit_pulse = Arc::new(Notify::new());
118 let alive = Arc::new(AtomicBool::new(true));
119 let dead = Arc::new(Notify::new());
120 let wake = Arc::new(Notify::new());
121
122 let out_state = Arc::new(Mutex::new(OutState { chans: BTreeMap::new() }));
123
124 let state_in = Arc::clone(&out_state);
126 let wake_in = Arc::clone(&wake);
127 let alive_in = Arc::clone(&alive);
128 let ingress = tokio::spawn(async move {
129 while let Some((ch, prio, frame)) = enq_rx.recv().await {
130 if !alive_in.load(Ordering::SeqCst) {
131 break;
132 }
133 let mut st = state_in.lock().expect("out");
134 let q = st.chans.entry(ch).or_insert_with(ChanQ::new);
135 let cap = ChanQ::cap_for(prio);
136 if prio == SendPrio::Browser {
137 q.q.retain(|x| x.prio != SendPrio::Browser);
139 if q.q.len() >= cap {
140 continue;
142 }
143 } else {
144 while q.q.len() >= cap {
145 if let Some(i) = q
147 .q
148 .iter()
149 .enumerate()
150 .filter(|(_, x)| x.prio >= SendPrio::Mail)
151 .max_by_key(|(_, x)| x.prio)
152 .map(|(i, _)| i)
153 {
154 q.q.remove(i);
155 } else if prio <= SendPrio::Pty {
156 q.q.pop_front();
158 } else {
159 break;
160 }
161 }
162 if q.q.len() >= cap {
163 continue;
164 }
165 }
166 q.q.push_back(Queued { prio, frame });
167 drop(st);
168 wake_in.notify_one();
169 }
170 });
171
172 let state_wr = Arc::clone(&out_state);
175 let wake_wr = Arc::clone(&wake);
176 let alive_wr = Arc::clone(&alive);
177 let dead_wr = Arc::clone(&dead);
178 let routes_wr = Arc::clone(&routes);
179 let writer = tokio::spawn(async move {
180 loop {
181 if !alive_wr.load(Ordering::SeqCst) {
182 break;
183 }
184 let next = {
185 let mut st = state_wr.lock().expect("out");
186 pick_next(&mut st)
187 };
188 let Some((ch, is_data, frame)) = next else {
189 wake_wr.notified().await;
190 continue;
191 };
192 let Ok(bytes) = frame.encode() else {
193 kill(&alive_wr, &dead_wr, &routes_wr);
194 break;
195 };
196 match tokio::time::timeout(WRITE_TIMEOUT, wr.write_all(&bytes)).await {
197 Ok(Ok(())) => {
198 if is_data {
199 let mut st = state_wr.lock().expect("out");
200 if let Some(q) = st.chans.get_mut(&ch) {
201 if let Some(c) = q.credit.as_mut() {
202 *c = c.saturating_sub(1);
203 }
204 }
205 }
206 }
207 Ok(Err(_)) | Err(_) => {
208 tracing::warn!(%ch, "uplink write timed out or failed; reconnecting");
209 kill(&alive_wr, &dead_wr, &routes_wr);
210 break;
211 }
212 }
213 }
214 });
215
216 let enq_reader = enq_tx.clone();
217 let reader_journal = journal.clone();
218 let reader_routes = Arc::clone(&routes);
219 let alive_rd = Arc::clone(&alive);
220 let dead_rd = Arc::clone(&dead);
221 let state_rd = Arc::clone(&out_state);
222 let wake_rd = Arc::clone(&wake);
223 let credits_rd = Arc::clone(&credits);
224 let credit_pulse_rd = Arc::clone(&credit_pulse);
225 let reader = tokio::spawn(async move {
226 let mut buf = vec![0u8; 128 * 1024];
227 let mut filled = 0usize;
228 loop {
229 let Ok(n) = rd.read(&mut buf[filled..]).await else { break };
230 if n == 0 {
231 break;
232 }
233 filled += n;
234 loop {
235 match Frame::decode(&buf[..filled]) {
236 Ok((frame, used)) => {
237 buf.copy_within(used..filled, 0);
238 filled -= used;
239 match frame.frame_type {
240 FrameType::Data => {
241 let tx = reader_routes.lock().expect("routes").get(&frame.channel_id).cloned();
242 if let Some(tx) = tx {
243 let _ = tx.send(frame.payload);
244 }
245 }
246 FrameType::Window => {
247 let credit = frame.seq.min(u64::from(u32::MAX)) as u32;
248 {
249 let mut st = state_rd.lock().expect("out");
250 st.chans.entry(frame.channel_id).or_insert_with(ChanQ::new).credit = Some(credit);
251 }
252 credits_rd.lock().expect("credits").insert(frame.channel_id, credit);
253 credit_pulse_rd.notify_waiters();
254 wake_rd.notify_one();
255 }
256 FrameType::JournalReq => {
257 let Some(j) = reader_journal.as_ref() else { continue };
258 let limit = frame.journal_req_limit().unwrap_or(64) as usize;
259 if let Ok(body) = j.journal().reply_body(frame.channel_id, frame.seq, limit) {
260 if let Ok(reply) = Frame::journal_reply(frame.channel_id, &body) {
261 let _ = enq_reader.send((frame.channel_id, SendPrio::Control, reply));
262 }
263 }
264 }
265 FrameType::GoAway | FrameType::Close => {
266 kill(&alive_rd, &dead_rd, &reader_routes);
267 return;
268 }
269 _ => {}
270 }
271 }
272 Err(FrameError::Truncated) => break,
273 Err(_) => {
274 kill(&alive_rd, &dead_rd, &reader_routes);
275 return;
276 }
277 }
278 }
279 if filled == buf.len() {
280 if grow_read_buf(&mut buf, filled).is_err() {
283 break;
284 }
285 }
286 }
287 kill(&alive_rd, &dead_rd, &reader_routes);
288 });
289
290 Ok(Arc::new(Self {
291 enqueue: enq_tx,
292 routes,
293 credits,
294 credit_pulse,
295 journal,
296 alive,
297 dead,
298 tasks: vec![ingress, writer, reader],
299 }))
300 }
301
302 pub fn is_alive(&self) -> bool {
303 self.alive.load(Ordering::SeqCst)
304 }
305
306 pub async fn closed(&self) {
308 if !self.is_alive() {
309 return;
310 }
311 self.dead.notified().await;
312 }
313
314 fn push(&self, channel_id: u32, prio: SendPrio, frame: Frame) -> Result<(), String> {
315 if !self.is_alive() {
316 return Err("uplink closed".into());
317 }
318 self.enqueue
319 .send((channel_id, prio, frame))
320 .map_err(|_| "uplink closed".to_owned())
321 }
322
323 pub async fn open(
325 self: &Arc<Self>,
326 node_id: &str,
327 channel_id: u32,
328 kind: ChannelKind,
329 keys: UpstreamKeys,
330 handshake_timeout: Duration,
331 ) -> Result<NodeSession, String> {
332 let (down_tx, mut down_rx) = mpsc::unbounded_channel::<Vec<u8>>();
333 self.routes.lock().expect("routes").insert(channel_id, down_tx);
334 self.credits.lock().expect("credits").remove(&channel_id);
335 let open = Frame::open(channel_id, &OpenPayload { channel_kind: kind, node_id: node_id.to_owned() })
336 .map_err(|e| e.to_string())?;
337 self.push(channel_id, SendPrio::Control, open)?;
338
339 tokio::time::timeout(handshake_timeout, async {
342 loop {
343 if !self.is_alive() {
344 return Err("uplink closed".to_owned());
345 }
346 if self.credits.lock().expect("credits").get(&channel_id).copied().unwrap_or(0) > 0 {
347 return Ok(());
348 }
349 tokio::select! {
350 _ = self.credit_pulse.notified() => {}
351 _ = self.closed() => return Err("uplink closed".to_owned()),
352 }
353 }
354 })
355 .await
356 .map_err(|_| "window credit timeout after OPEN".to_owned())??;
357
358 let mut initiator = NoiseInitiator::new(keys.recipient_public).map_err(|e| e.to_string())?;
359 let msg1 = initiator.write_message1().map_err(|e| e.to_string())?;
360 self.send_inner(channel_id, keys, SendPrio::Control, &encode_inner(InnerKind::Handshake1, &msg1))?;
361
362 let msg2 = tokio::time::timeout(handshake_timeout, async {
363 loop {
364 let Some(bytes) = down_rx.recv().await else { return Err("uplink closed".to_owned()) };
365 if let Some((InnerKind::Handshake2, body)) = decode_inner(&bytes) {
366 return Ok(body.to_vec());
367 }
368 }
369 })
370 .await
371 .map_err(|_| "handshake timeout".to_owned())??;
372 let transport = initiator.read_message2(&msg2).map_err(|e| e.to_string())?;
373 Ok(NodeSession {
374 channel_id,
375 keys,
376 default_prio: match kind {
377 ChannelKind::M4a => SendPrio::Mail,
378 ChannelKind::G4aControl | ChannelKind::G4aPty | ChannelKind::G4aEvents => SendPrio::Pty,
379 ChannelKind::Vault | ChannelKind::Observation => SendPrio::Mail,
380 },
381 down: down_rx,
382 transport,
383 journal: self.journal.clone(),
384 uplink: Arc::clone(self),
385 })
386 }
387
388 fn send_inner(&self, channel_id: u32, keys: UpstreamKeys, prio: SendPrio, inner: &[u8]) -> Result<(), String> {
389 if let Some(j) = &self.journal {
390 j.append(channel_id, inner)?;
391 }
392 let sealed = node_seal_outer(keys.c2_public, inner).map_err(|e| e.to_string())?;
393 let frame = Frame::data(channel_id, 0, sealed).map_err(|e| e.to_string())?;
394 self.push(channel_id, prio, frame)
395 }
396}
397
398fn kill(alive: &AtomicBool, dead: &Notify, routes: &Mutex<BTreeMap<u32, mpsc::UnboundedSender<Vec<u8>>>>) {
399 if alive.swap(false, Ordering::SeqCst) {
400 routes.lock().expect("routes").clear();
401 dead.notify_waiters();
402 }
403}
404
405fn pick_next(st: &mut OutState) -> Option<(u32, bool, Frame)> {
407 let mut best: Option<(SendPrio, u32, usize)> = None;
409 for (&ch, cq) in &st.chans {
410 for (i, item) in cq.q.iter().enumerate() {
411 let needs_credit = matches!(item.frame.frame_type, FrameType::Data);
412 if needs_credit {
413 match cq.credit {
414 Some(0) => continue,
415 None => {
416 if item.prio > SendPrio::Control {
419 continue;
420 }
421 }
422 Some(_) => {}
423 }
424 }
425 let cand = (item.prio, ch, i);
426 if best.is_none_or(|b| cand.0 < b.0 || (cand.0 == b.0 && cand.1 < b.1)) {
427 best = Some(cand);
428 }
429 }
430 }
431 let (prio, ch, idx) = best?;
432 let cq = st.chans.get_mut(&ch)?;
433 let item = cq.q.remove(idx)?;
434 debug_assert_eq!(item.prio, prio);
435 let is_data = matches!(item.frame.frame_type, FrameType::Data);
436 Some((ch, is_data, item.frame))
437}
438
439pub struct NodeSession {
440 channel_id: u32,
441 keys: UpstreamKeys,
442 default_prio: SendPrio,
443 down: mpsc::UnboundedReceiver<Vec<u8>>,
444 transport: NoiseTransport,
445 journal: Option<Arc<UplinkJournal>>,
446 uplink: Arc<NodeUplink>,
447}
448
449impl NodeSession {
450 pub async fn connect(
451 uplink: SocketAddr,
452 node_id: &str,
453 channel_id: u32,
454 kind: ChannelKind,
455 keys: UpstreamKeys,
456 journal: Option<Arc<UplinkJournal>>,
457 handshake_timeout: Duration,
458 ) -> Result<Self, String> {
459 let link = NodeUplink::connect(uplink, journal).await?;
460 link.open(node_id, channel_id, kind, keys, handshake_timeout).await
461 }
462
463 pub fn uplink(&self) -> Arc<NodeUplink> {
464 Arc::clone(&self.uplink)
465 }
466
467 pub async fn send(&mut self, plaintext: &[u8]) -> Result<(), String> {
468 self.send_prio(plaintext, self.default_prio).await
469 }
470
471 pub async fn send_prio(&mut self, plaintext: &[u8], prio: SendPrio) -> Result<(), String> {
472 if !self.uplink.is_alive() {
473 return Err("uplink closed".into());
474 }
475 let ct = self.transport.encrypt(plaintext).map_err(|e| e.to_string())?;
476 let inner = encode_inner(InnerKind::Transport, &ct);
477 self.uplink.send_inner(self.channel_id, self.keys, prio, &inner)
478 }
479
480 pub async fn recv(&mut self, timeout: Duration) -> Result<Vec<u8>, String> {
481 tokio::time::timeout(timeout, async {
482 loop {
483 tokio::select! {
484 bytes = self.down.recv() => {
485 let Some(bytes) = bytes else { return Err("uplink closed".to_owned()) };
486 if let Some((InnerKind::Transport, ct)) = decode_inner(&bytes) {
487 return self.transport.decrypt(ct).map_err(|e| e.to_string());
488 }
489 }
490 _ = self.uplink.closed() => return Err("uplink closed".to_owned()),
491 }
492 }
493 })
494 .await
495 .map_err(|_| "reply timeout".to_owned())?
496 }
497
498 pub async fn call(&mut self, op: &str, payload: &Value, timeout: Duration) -> Result<Value, String> {
499 let req = serde_json::to_vec(&json!({ "op": op, "payload": payload })).map_err(|e| e.to_string())?;
500 self.send_prio(&req, SendPrio::Mail).await?;
501 let reply = self.recv(timeout).await?;
502 let reply: Value = serde_json::from_slice(&reply).map_err(|e| e.to_string())?;
503 let status = reply.get("status").and_then(Value::as_u64).unwrap_or(500);
504 let body = reply.get("body").cloned().unwrap_or(Value::Null);
505 if status == 200 {
506 Ok(body)
507 } else {
508 let msg = body.get("error").and_then(Value::as_str).unwrap_or("error");
509 Err(format!("{status}: {msg}"))
510 }
511 }
512}
513
514#[cfg(test)]
515mod tests {
516 use super::*;
517
518 #[test]
519 fn pick_prefers_control_over_browser() {
520 let mut st = OutState { chans: BTreeMap::new() };
521 let q = st.chans.entry(1).or_insert_with(ChanQ::new);
522 q.credit = Some(10);
523 q.q.push_back(Queued {
524 prio: SendPrio::Browser,
525 frame: Frame::data(1, 0, vec![1]).unwrap(),
526 });
527 q.q.push_back(Queued {
528 prio: SendPrio::Control,
529 frame: Frame::data(1, 0, vec![2]).unwrap(),
530 });
531 q.q.push_back(Queued {
532 prio: SendPrio::Pty,
533 frame: Frame::data(1, 0, vec![3]).unwrap(),
534 });
535 let (_, _, f) = pick_next(&mut st).unwrap();
536 assert_eq!(f.payload, vec![2]);
537 let (_, _, f) = pick_next(&mut st).unwrap();
538 assert_eq!(f.payload, vec![3]);
539 let (_, _, f) = pick_next(&mut st).unwrap();
540 assert_eq!(f.payload, vec![1]);
541 }
542
543 #[test]
544 fn pick_holds_bulk_until_window() {
545 let mut st = OutState { chans: BTreeMap::new() };
546 let q = st.chans.entry(2).or_insert_with(ChanQ::new);
547 q.credit = None;
548 q.q.push_back(Queued {
549 prio: SendPrio::Mail,
550 frame: Frame::data(2, 0, vec![9]).unwrap(),
551 });
552 q.q.push_back(Queued {
553 prio: SendPrio::Control,
554 frame: Frame::data(2, 0, vec![1]).unwrap(),
555 });
556 let (_, _, f) = pick_next(&mut st).unwrap();
557 assert_eq!(f.payload, vec![1]);
558 assert!(pick_next(&mut st).is_none());
559 st.chans.get_mut(&2).unwrap().credit = Some(4);
560 let (_, _, f) = pick_next(&mut st).unwrap();
561 assert_eq!(f.payload, vec![9]);
562 }
563}