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, AtomicU64, 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
31const UPLINK_PING_EVERY: Duration = Duration::from_secs(10);
33const UPLINK_DEAD_AFTER: Duration = Duration::from_secs(35);
35
36#[derive(Clone, Copy, Debug)]
38pub struct UpstreamKeys {
39 pub c2_public: PublicKeyBytes,
40 pub recipient_public: PublicKeyBytes,
41}
42
43#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
45pub enum SendPrio {
46 Control = 0,
48 Pty = 1,
50 Mail = 2,
52 Browser = 3,
54}
55
56const WRITE_TIMEOUT: Duration = Duration::from_secs(5);
57const CHAN_CAP_CONTROL: usize = 64;
58const CHAN_CAP_PTY: usize = 128;
59const CHAN_CAP_MAIL: usize = 16;
60const CHAN_CAP_BROWSER: usize = 8;
61
62struct Queued {
63 prio: SendPrio,
64 frame: Frame,
65}
66
67struct ChanQ {
68 q: VecDeque<Queued>,
69 credit: Option<u32>,
71}
72
73impl ChanQ {
74 fn new() -> Self {
75 Self { q: VecDeque::new(), credit: None }
76 }
77
78 fn cap_for(prio: SendPrio) -> usize {
79 match prio {
80 SendPrio::Control => CHAN_CAP_CONTROL,
81 SendPrio::Pty => CHAN_CAP_PTY,
82 SendPrio::Mail => CHAN_CAP_MAIL,
83 SendPrio::Browser => CHAN_CAP_BROWSER,
84 }
85 }
86}
87
88struct OutState {
89 chans: BTreeMap<u32, ChanQ>,
90}
91
92pub struct NodeUplink {
94 enqueue: mpsc::UnboundedSender<(u32, SendPrio, Frame)>,
95 routes: Arc<Mutex<BTreeMap<u32, mpsc::UnboundedSender<Vec<u8>>>>>,
96 credits: Arc<Mutex<BTreeMap<u32, u32>>>,
98 credit_pulse: Arc<Notify>,
99 journal: Option<Arc<UplinkJournal>>,
100 alive: Arc<AtomicBool>,
101 dead: Arc<Notify>,
102 tasks: Vec<JoinHandle<()>>,
103}
104
105impl Drop for NodeUplink {
106 fn drop(&mut self) {
107 self.alive.store(false, Ordering::SeqCst);
108 for t in &self.tasks {
109 t.abort();
110 }
111 }
112}
113
114fn dial_uplink(addr: SocketAddr) -> std::io::Result<std::net::TcpStream> {
118 #[cfg(feature = "wireguard")]
119 {
120 gate4agent_node_core::kit_wireguard::tcp_connect(addr)
121 }
122 #[cfg(not(feature = "wireguard"))]
123 {
124 std::net::TcpStream::connect(addr)
125 }
126}
127
128impl NodeUplink {
129 pub async fn connect(uplink: SocketAddr, journal: Option<Arc<UplinkJournal>>) -> Result<Arc<Self>, String> {
130 let dial = uplink;
131 let started = std::time::Instant::now();
132 crate::node_log::info(
133 "uplink_connect_attempt",
134 &[("addr_port", dial.port().to_string())],
135 );
136 let std_stream = match tokio::task::spawn_blocking(move || dial_uplink(dial)).await {
137 Ok(Ok(s)) => {
138 crate::node_log::info(
139 "uplink_connect_ok",
140 &[
141 ("addr_port", dial.port().to_string()),
142 ("ms", started.elapsed().as_millis().to_string()),
143 ],
144 );
145 s
146 }
147 Ok(Err(e)) => {
148 crate::node_log::warn(
149 "uplink_connect_fail",
150 &[
151 ("addr_port", dial.port().to_string()),
152 ("error", e.to_string()),
153 ("ms", started.elapsed().as_millis().to_string()),
154 ],
155 );
156 return Err(e.to_string());
157 }
158 Err(e) => {
159 crate::node_log::warn(
160 "uplink_connect_fail",
161 &[
162 ("addr_port", dial.port().to_string()),
163 ("error", e.to_string()),
164 ("ms", started.elapsed().as_millis().to_string()),
165 ],
166 );
167 return Err(e.to_string());
168 }
169 };
170 let _ = std_stream.set_nodelay(true);
171 if let Err(error) = crate::server::enable_tcp_keepalive(&std_stream) {
175 tracing::warn!(cause = %error, "could not enable TCP keepalive on the uplink");
176 }
177 std_stream.set_nonblocking(true).map_err(|e| e.to_string())?;
181 let stream = TcpStream::from_std(std_stream).map_err(|e| e.to_string())?;
182 let (mut rd, mut wr) = stream.into_split();
183 let (enq_tx, mut enq_rx) = mpsc::unbounded_channel::<(u32, SendPrio, Frame)>();
184 let routes: Arc<Mutex<BTreeMap<u32, mpsc::UnboundedSender<Vec<u8>>>>> = Arc::default();
185 let credits: Arc<Mutex<BTreeMap<u32, u32>>> = Arc::default();
186 let credit_pulse = Arc::new(Notify::new());
187 let alive = Arc::new(AtomicBool::new(true));
188 let dead = Arc::new(Notify::new());
189 let wake = Arc::new(Notify::new());
190
191 let out_state = Arc::new(Mutex::new(OutState { chans: BTreeMap::new() }));
192 let born = std::time::Instant::now();
197 let last_rx = Arc::new(AtomicU64::new(0));
198
199 let state_in = Arc::clone(&out_state);
201 let wake_in = Arc::clone(&wake);
202 let alive_in = Arc::clone(&alive);
203 let ingress = tokio::spawn(async move {
204 while let Some((ch, prio, frame)) = enq_rx.recv().await {
205 if !alive_in.load(Ordering::SeqCst) {
206 break;
207 }
208 let mut st = state_in.lock().expect("out");
209 let q = st.chans.entry(ch).or_insert_with(ChanQ::new);
210 let cap = ChanQ::cap_for(prio);
211 if prio == SendPrio::Browser {
212 q.q.retain(|x| x.prio != SendPrio::Browser);
214 if q.q.len() >= cap {
215 continue;
217 }
218 } else {
219 while q.q.len() >= cap {
220 if let Some(i) = q
222 .q
223 .iter()
224 .enumerate()
225 .filter(|(_, x)| x.prio >= SendPrio::Mail)
226 .max_by_key(|(_, x)| x.prio)
227 .map(|(i, _)| i)
228 {
229 q.q.remove(i);
230 } else if prio <= SendPrio::Pty {
231 q.q.pop_front();
233 } else {
234 break;
235 }
236 }
237 if q.q.len() >= cap {
238 continue;
239 }
240 }
241 q.q.push_back(Queued { prio, frame });
242 drop(st);
243 wake_in.notify_one();
244 }
245 });
246
247 let state_wr = Arc::clone(&out_state);
250 let wake_wr = Arc::clone(&wake);
251 let alive_wr = Arc::clone(&alive);
252 let dead_wr = Arc::clone(&dead);
253 let routes_wr = Arc::clone(&routes);
254 let writer = tokio::spawn(async move {
255 loop {
256 if !alive_wr.load(Ordering::SeqCst) {
257 break;
258 }
259 let next = {
260 let mut st = state_wr.lock().expect("out");
261 pick_next(&mut st)
262 };
263 let Some((ch, is_data, frame)) = next else {
264 wake_wr.notified().await;
265 continue;
266 };
267 let Ok(bytes) = frame.encode() else {
268 kill(&alive_wr, &dead_wr, &routes_wr);
269 break;
270 };
271 match tokio::time::timeout(WRITE_TIMEOUT, wr.write_all(&bytes)).await {
272 Ok(Ok(())) => {
273 if is_data {
274 let mut st = state_wr.lock().expect("out");
275 if let Some(q) = st.chans.get_mut(&ch) {
276 if let Some(c) = q.credit.as_mut() {
277 *c = c.saturating_sub(1);
278 }
279 }
280 }
281 }
282 Ok(Err(_)) | Err(_) => {
283 tracing::warn!(%ch, "uplink write timed out or failed; reconnecting");
284 kill(&alive_wr, &dead_wr, &routes_wr);
285 break;
286 }
287 }
288 }
289 });
290
291 let enq_reader = enq_tx.clone();
292 let reader_journal = journal.clone();
293 let reader_routes = Arc::clone(&routes);
294 let alive_rd = Arc::clone(&alive);
295 let dead_rd = Arc::clone(&dead);
296 let state_rd = Arc::clone(&out_state);
297 let wake_rd = Arc::clone(&wake);
298 let credits_rd = Arc::clone(&credits);
299 let credit_pulse_rd = Arc::clone(&credit_pulse);
300 let last_rx_rd = Arc::clone(&last_rx);
301 let reader = tokio::spawn(async move {
302 let mut buf = vec![0u8; 128 * 1024];
303 let mut filled = 0usize;
304 loop {
305 let Ok(n) = rd.read(&mut buf[filled..]).await else { break };
306 if n == 0 {
307 break;
308 }
309 filled += n;
310 last_rx_rd.store(born.elapsed().as_millis() as u64, Ordering::SeqCst);
311 loop {
312 match Frame::decode(&buf[..filled]) {
313 Ok((frame, used)) => {
314 buf.copy_within(used..filled, 0);
315 filled -= used;
316 match frame.frame_type {
317 FrameType::Data => {
318 let tx = reader_routes.lock().expect("routes").get(&frame.channel_id).cloned();
319 if let Some(tx) = tx {
320 let _ = tx.send(frame.payload);
321 }
322 }
323 FrameType::Window => {
324 let credit = frame.seq.min(u64::from(u32::MAX)) as u32;
325 {
326 let mut st = state_rd.lock().expect("out");
327 st.chans.entry(frame.channel_id).or_insert_with(ChanQ::new).credit = Some(credit);
328 }
329 credits_rd.lock().expect("credits").insert(frame.channel_id, credit);
330 credit_pulse_rd.notify_waiters();
331 wake_rd.notify_one();
332 }
333 FrameType::JournalReq => {
334 let Some(j) = reader_journal.as_ref() else { continue };
335 let limit = frame.journal_req_limit().unwrap_or(64) as usize;
336 if let Ok(body) = j.journal().reply_body(frame.channel_id, frame.seq, limit) {
337 if let Ok(reply) = Frame::journal_reply(frame.channel_id, &body) {
338 let _ = enq_reader.send((frame.channel_id, SendPrio::Control, reply));
339 }
340 }
341 }
342 FrameType::GoAway | FrameType::Close => {
343 kill(&alive_rd, &dead_rd, &reader_routes);
344 return;
345 }
346 _ => {}
347 }
348 }
349 Err(FrameError::Truncated) => break,
350 Err(_) => {
351 kill(&alive_rd, &dead_rd, &reader_routes);
352 return;
353 }
354 }
355 }
356 if filled == buf.len() {
357 if grow_read_buf(&mut buf, filled).is_err() {
360 break;
361 }
362 }
363 }
364 kill(&alive_rd, &dead_rd, &reader_routes);
365 });
366
367 let ka_tx = enq_tx.clone();
368 let alive_ka = Arc::clone(&alive);
369 let dead_ka = Arc::clone(&dead);
370 let routes_ka = Arc::clone(&routes);
371 let keepalive = tokio::spawn(async move {
372 let mut nonce = 0u64;
373 let mut tick = tokio::time::interval(UPLINK_PING_EVERY);
374 tick.tick().await;
375 loop {
376 tick.tick().await;
377 if !alive_ka.load(Ordering::SeqCst) {
378 break;
379 }
380 let now = born.elapsed().as_millis() as u64;
381 let idle = now.saturating_sub(last_rx.load(Ordering::SeqCst));
382 if idle > UPLINK_DEAD_AFTER.as_millis() as u64 {
383 tracing::warn!(idle_ms = idle, "uplink silent past keepalive deadline; reconnecting");
384 kill(&alive_ka, &dead_ka, &routes_ka);
385 break;
386 }
387 nonce = nonce.wrapping_add(1);
388 if ka_tx.send((0, SendPrio::Control, Frame::ping(nonce))).is_err() {
389 break;
390 }
391 }
392 });
393
394 Ok(Arc::new(Self {
395 enqueue: enq_tx,
396 routes,
397 credits,
398 credit_pulse,
399 journal,
400 alive,
401 dead,
402 tasks: vec![ingress, writer, reader, keepalive],
403 }))
404 }
405
406 pub fn is_alive(&self) -> bool {
407 self.alive.load(Ordering::SeqCst)
408 }
409
410 pub async fn closed(&self) {
412 if !self.is_alive() {
413 return;
414 }
415 self.dead.notified().await;
416 }
417
418 fn push(&self, channel_id: u32, prio: SendPrio, frame: Frame) -> Result<(), String> {
419 if !self.is_alive() {
420 return Err("uplink closed".into());
421 }
422 self.enqueue
423 .send((channel_id, prio, frame))
424 .map_err(|_| "uplink closed".to_owned())
425 }
426
427 pub async fn open(
429 self: &Arc<Self>,
430 node_id: &str,
431 channel_id: u32,
432 kind: ChannelKind,
433 keys: UpstreamKeys,
434 handshake_timeout: Duration,
435 ) -> Result<NodeSession, String> {
436 let (down_tx, mut down_rx) = mpsc::unbounded_channel::<Vec<u8>>();
437 self.routes.lock().expect("routes").insert(channel_id, down_tx);
438 self.credits.lock().expect("credits").remove(&channel_id);
439 let open = Frame::open(channel_id, &OpenPayload { channel_kind: kind, node_id: node_id.to_owned() })
440 .map_err(|e| e.to_string())?;
441 self.push(channel_id, SendPrio::Control, open)?;
442
443 tokio::time::timeout(handshake_timeout, async {
446 loop {
447 if !self.is_alive() {
448 return Err("uplink closed".to_owned());
449 }
450 if self.credits.lock().expect("credits").get(&channel_id).copied().unwrap_or(0) > 0 {
451 return Ok(());
452 }
453 tokio::select! {
454 _ = self.credit_pulse.notified() => {}
455 _ = self.closed() => return Err("uplink closed".to_owned()),
456 }
457 }
458 })
459 .await
460 .map_err(|_| "window credit timeout after OPEN".to_owned())??;
461
462 let mut initiator = NoiseInitiator::new(keys.recipient_public).map_err(|e| e.to_string())?;
463 let msg1 = initiator.write_message1().map_err(|e| e.to_string())?;
464 self.send_inner(channel_id, keys, SendPrio::Control, &encode_inner(InnerKind::Handshake1, &msg1))?;
465
466 let msg2 = tokio::time::timeout(handshake_timeout, async {
467 loop {
468 let Some(bytes) = down_rx.recv().await else { return Err("uplink closed".to_owned()) };
469 if let Some((InnerKind::Handshake2, body)) = decode_inner(&bytes) {
470 return Ok(body.to_vec());
471 }
472 }
473 })
474 .await
475 .map_err(|_| "handshake timeout".to_owned())??;
476 let transport = initiator.read_message2(&msg2).map_err(|e| e.to_string())?;
477 Ok(NodeSession {
478 channel_id,
479 keys,
480 default_prio: match kind {
481 ChannelKind::M4a => SendPrio::Mail,
482 ChannelKind::G4aControl | ChannelKind::G4aPty | ChannelKind::G4aEvents => SendPrio::Pty,
483 ChannelKind::Vault | ChannelKind::Observation => SendPrio::Mail,
484 },
485 down: down_rx,
486 transport,
487 journal: self.journal.clone(),
488 uplink: Arc::clone(self),
489 })
490 }
491
492 fn send_inner(&self, channel_id: u32, keys: UpstreamKeys, prio: SendPrio, inner: &[u8]) -> Result<(), String> {
493 if let Some(j) = &self.journal {
494 j.append(channel_id, inner)?;
495 }
496 let sealed = node_seal_outer(keys.c2_public, inner).map_err(|e| e.to_string())?;
497 let frame = Frame::data(channel_id, 0, sealed).map_err(|e| e.to_string())?;
498 self.push(channel_id, prio, frame)
499 }
500}
501
502fn kill(alive: &AtomicBool, dead: &Notify, routes: &Mutex<BTreeMap<u32, mpsc::UnboundedSender<Vec<u8>>>>) {
503 if alive.swap(false, Ordering::SeqCst) {
504 routes.lock().expect("routes").clear();
505 dead.notify_waiters();
506 }
507}
508
509fn pick_next(st: &mut OutState) -> Option<(u32, bool, Frame)> {
511 let mut best: Option<(SendPrio, u32, usize)> = None;
515 for (&ch, cq) in &st.chans {
516 let first_data = cq.q.iter().position(|it| matches!(it.frame.frame_type, FrameType::Data));
517 for (i, item) in cq.q.iter().enumerate() {
518 let needs_credit = matches!(item.frame.frame_type, FrameType::Data);
519 if needs_credit && Some(i) != first_data {
520 continue;
521 }
522 if needs_credit {
523 match cq.credit {
524 Some(0) => continue,
525 None => {
526 if item.prio > SendPrio::Control {
529 continue;
530 }
531 }
532 Some(_) => {}
533 }
534 }
535 let cand = (item.prio, ch, i);
536 if best.is_none_or(|b| cand.0 < b.0 || (cand.0 == b.0 && cand.1 < b.1)) {
537 best = Some(cand);
538 }
539 }
540 }
541 let (prio, ch, idx) = best?;
542 let cq = st.chans.get_mut(&ch)?;
543 let item = cq.q.remove(idx)?;
544 debug_assert_eq!(item.prio, prio);
545 let is_data = matches!(item.frame.frame_type, FrameType::Data);
546 Some((ch, is_data, item.frame))
547}
548
549pub struct NodeSession {
550 channel_id: u32,
551 keys: UpstreamKeys,
552 default_prio: SendPrio,
553 down: mpsc::UnboundedReceiver<Vec<u8>>,
554 transport: NoiseTransport,
555 journal: Option<Arc<UplinkJournal>>,
556 uplink: Arc<NodeUplink>,
557}
558
559impl NodeSession {
560 pub async fn connect(
561 uplink: SocketAddr,
562 node_id: &str,
563 channel_id: u32,
564 kind: ChannelKind,
565 keys: UpstreamKeys,
566 journal: Option<Arc<UplinkJournal>>,
567 handshake_timeout: Duration,
568 ) -> Result<Self, String> {
569 let link = NodeUplink::connect(uplink, journal).await?;
570 link.open(node_id, channel_id, kind, keys, handshake_timeout).await
571 }
572
573 pub fn uplink(&self) -> Arc<NodeUplink> {
574 Arc::clone(&self.uplink)
575 }
576
577 pub async fn send(&mut self, plaintext: &[u8]) -> Result<(), String> {
578 self.send_prio(plaintext, self.default_prio).await
579 }
580
581 pub async fn send_prio(&mut self, plaintext: &[u8], prio: SendPrio) -> Result<(), String> {
582 if !self.uplink.is_alive() {
583 return Err("uplink closed".into());
584 }
585 let ct = self.transport.encrypt(plaintext).map_err(|e| e.to_string())?;
586 let inner = encode_inner(InnerKind::Transport, &ct);
587 self.uplink.send_inner(self.channel_id, self.keys, prio, &inner)
588 }
589
590 pub async fn recv(&mut self, timeout: Duration) -> Result<Vec<u8>, String> {
591 tokio::time::timeout(timeout, async {
592 loop {
593 tokio::select! {
594 bytes = self.down.recv() => {
595 let Some(bytes) = bytes else { return Err("uplink closed".to_owned()) };
596 if let Some((InnerKind::Transport, ct)) = decode_inner(&bytes) {
597 return self.transport.decrypt(ct).map_err(|e| e.to_string());
598 }
599 }
600 _ = self.uplink.closed() => return Err("uplink closed".to_owned()),
601 }
602 }
603 })
604 .await
605 .map_err(|_| "reply timeout".to_owned())?
606 }
607
608 pub async fn call(&mut self, op: &str, payload: &Value, timeout: Duration) -> Result<Value, String> {
609 let req = serde_json::to_vec(&json!({ "op": op, "payload": payload })).map_err(|e| e.to_string())?;
610 self.send_prio(&req, SendPrio::Mail).await?;
611 let reply = self.recv(timeout).await?;
612 let reply: Value = serde_json::from_slice(&reply).map_err(|e| e.to_string())?;
613 let status = reply.get("status").and_then(Value::as_u64).unwrap_or(500);
614 let body = reply.get("body").cloned().unwrap_or(Value::Null);
615 if status == 200 {
616 Ok(body)
617 } else {
618 let msg = body.get("error").and_then(Value::as_str).unwrap_or("error");
619 Err(format!("{status}: {msg}"))
620 }
621 }
622}
623
624#[cfg(test)]
625mod tests {
626 use super::*;
627
628 #[test]
629 fn pick_prefers_control_across_channels_but_keeps_data_order_within_one() {
630 let mut st = OutState { chans: BTreeMap::new() };
631 let a = st.chans.entry(1).or_insert_with(ChanQ::new);
632 a.credit = Some(10);
633 a.q.push_back(Queued { prio: SendPrio::Browser, frame: Frame::data(1, 0, vec![1]).unwrap() });
634 a.q.push_back(Queued { prio: SendPrio::Control, frame: Frame::data(1, 0, vec![2]).unwrap() });
635 let b = st.chans.entry(2).or_insert_with(ChanQ::new);
636 b.credit = Some(10);
637 b.q.push_back(Queued { prio: SendPrio::Pty, frame: Frame::data(2, 0, vec![3]).unwrap() });
638 let order: Vec<u8> = std::iter::from_fn(|| pick_next(&mut st).map(|(_, _, f)| f.payload[0])).collect();
640 assert_eq!(order, vec![3, 1, 2]);
641 }
642
643 #[test]
644 fn pick_holds_bulk_until_window() {
645 let mut st = OutState { chans: BTreeMap::new() };
646 let q = st.chans.entry(2).or_insert_with(ChanQ::new);
647 q.credit = None;
648 q.q.push_back(Queued {
649 prio: SendPrio::Control,
650 frame: Frame::data(2, 0, vec![1]).unwrap(),
651 });
652 q.q.push_back(Queued {
653 prio: SendPrio::Mail,
654 frame: Frame::data(2, 0, vec![9]).unwrap(),
655 });
656 let (_, _, f) = pick_next(&mut st).unwrap();
657 assert_eq!(f.payload, vec![1]);
658 assert!(pick_next(&mut st).is_none());
659 st.chans.get_mut(&2).unwrap().credit = Some(4);
660 let (_, _, f) = pick_next(&mut st).unwrap();
661 assert_eq!(f.payload, vec![9]);
662 }
663}