1use std::fmt;
20
21#[cfg(target_os = "linux")]
22mod linux;
23mod bridge_reach;
24#[cfg(target_os = "linux")]
25pub use linux::{
26 accept_peer, accept_peer_on, dial_peer, probe_tun_surface, LinuxUnderlayListener,
27 LinuxUnderlaySession, TunSurfaceReport,
28};
29pub use bridge_reach::{serve_bridge_tcp_relay, underlay_to_io, BridgeUnderlayClient};
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
33pub enum MeshUnderlayRole {
34 C2Peer,
35 NodePeer,
36 HqClientAdmin,
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
42pub enum MeshUnderlayDialCapability {
43 DialOnly,
44 DialOrAccept,
45}
46
47impl MeshUnderlayRole {
48 pub const fn dial_capability(self) -> MeshUnderlayDialCapability {
49 match self {
50 Self::C2Peer | Self::NodePeer => MeshUnderlayDialCapability::DialOrAccept,
51 Self::HqClientAdmin => MeshUnderlayDialCapability::DialOnly,
52 }
53 }
54
55 pub const fn may_accept_underlay(self) -> bool {
56 matches!(
57 self.dial_capability(),
58 MeshUnderlayDialCapability::DialOrAccept
59 )
60 }
61}
62
63impl fmt::Display for MeshUnderlayRole {
64 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65 f.write_str(match self {
66 Self::C2Peer => "c2-peer",
67 Self::NodePeer => "node-peer",
68 Self::HqClientAdmin => "hq-client-admin",
69 })
70 }
71}
72
73#[derive(Debug, Clone, PartialEq, Eq)]
75pub enum MeshUnderlayError {
76 HqMustNotAcceptUnderlay,
78 PlatformUnsupported {
80 os: &'static str,
81 hint: &'static str,
82 },
83 Unauthorized,
85 InvalidTransportKey,
87 InvalidAuthToken,
89 Path(String),
91 WireGuardDaemonNotInThisTip,
93}
94
95impl fmt::Display for MeshUnderlayError {
96 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97 match self {
98 Self::HqMustNotAcceptUnderlay => {
99 f.write_str("HQ mesh role is dial-only; underlay accept is refused")
100 }
101 Self::PlatformUnsupported { os, hint } => {
102 write!(f, "mesh underlay unsupported on {os}: {hint}")
103 }
104 Self::Unauthorized => {
105 f.write_str("underlay probe unauthorized: token barrier failed (crypto ≠ auth)")
106 }
107 Self::InvalidTransportKey => {
108 f.write_str("underlay transport key must be exactly 32 bytes")
109 }
110 Self::InvalidAuthToken => {
111 f.write_str("underlay auth token empty or longer than 4096 bytes")
112 }
113 Self::Path(msg) => write!(f, "underlay path error: {msg}"),
114 Self::WireGuardDaemonNotInThisTip => f.write_str(
115 "no WireGuard/kernel TUN daemon in tips 5–6; userspace UDP+AEAD + bridge TCP relay only",
116 ),
117 }
118 }
119}
120
121impl std::error::Error for MeshUnderlayError {}
122
123#[derive(Clone)]
125pub struct UnderlayTransportKey {
126 bytes: [u8; 32],
127}
128
129impl UnderlayTransportKey {
130 pub fn from_bytes(bytes: [u8; 32]) -> Self {
131 Self { bytes }
132 }
133
134 pub fn try_from_slice(bytes: &[u8]) -> Result<Self, MeshUnderlayError> {
135 let Ok(array) = <[u8; 32]>::try_from(bytes) else {
136 return Err(MeshUnderlayError::InvalidTransportKey);
137 };
138 Ok(Self { bytes: array })
139 }
140
141 pub(crate) fn as_bytes(&self) -> &[u8; 32] {
142 &self.bytes
143 }
144}
145
146impl fmt::Debug for UnderlayTransportKey {
147 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
148 f.write_str("UnderlayTransportKey([redacted])")
149 }
150}
151
152#[derive(Clone)]
155pub struct UnderlayAuthToken {
156 value: String,
157}
158
159impl UnderlayAuthToken {
160 pub fn new(value: impl Into<String>) -> Result<Self, MeshUnderlayError> {
161 let value = value.into();
162 if value.is_empty() || value.len() > 4_096 {
163 return Err(MeshUnderlayError::InvalidAuthToken);
164 }
165 Ok(Self { value })
166 }
167
168 pub(crate) fn as_str(&self) -> &str {
169 &self.value
170 }
171}
172
173impl fmt::Debug for UnderlayAuthToken {
174 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
175 f.write_str("UnderlayAuthToken([redacted])")
176 }
177}
178
179pub fn tokens_match(provided: &str, expected: &str) -> bool {
181 let left = provided.as_bytes();
182 let right = expected.as_bytes();
183 if left.len() != right.len() {
184 return false;
185 }
186 left.iter()
187 .zip(right)
188 .fold(0_u8, |acc, (a, b)| acc | (a ^ b))
189 == 0
190}
191
192pub fn authorize_probe(
194 configured: &UnderlayAuthToken,
195 provided: Option<&str>,
196) -> Result<(), MeshUnderlayError> {
197 match provided {
198 Some(got) if tokens_match(got, configured.as_str()) => Ok(()),
199 _ => Err(MeshUnderlayError::Unauthorized),
200 }
201}
202
203pub fn assert_accept_allowed(role: MeshUnderlayRole) -> Result<(), MeshUnderlayError> {
205 if role.may_accept_underlay() {
206 Ok(())
207 } else {
208 Err(MeshUnderlayError::HqMustNotAcceptUnderlay)
209 }
210}
211
212#[cfg(not(target_os = "linux"))]
214pub fn accept_peer(
215 role: MeshUnderlayRole,
216 _transport_key: &UnderlayTransportKey,
217 _auth_token: &UnderlayAuthToken,
218) -> Result<UnsupportedUnderlayHandle, MeshUnderlayError> {
219 assert_accept_allowed(role)?;
220 Err(platform_unsupported())
221}
222
223#[cfg(not(target_os = "linux"))]
224pub fn accept_peer_on(
225 role: MeshUnderlayRole,
226 transport_key: &UnderlayTransportKey,
227 auth_token: &UnderlayAuthToken,
228 _bind: std::net::SocketAddr,
229) -> Result<UnsupportedUnderlayHandle, MeshUnderlayError> {
230 accept_peer(role, transport_key, auth_token)
231}
232
233#[cfg(not(target_os = "linux"))]
234pub fn dial_peer(
235 _role: MeshUnderlayRole,
236 _transport_key: &UnderlayTransportKey,
237 _auth_token: &UnderlayAuthToken,
238 _peer: &str,
239) -> Result<UnsupportedUnderlayHandle, MeshUnderlayError> {
240 Err(platform_unsupported())
241}
242
243#[cfg(not(target_os = "linux"))]
244pub fn probe_tun_surface() -> TunSurfaceReport {
245 TunSurfaceReport {
246 device_present: false,
247 open_attempt: "skipped-non-linux",
248 note: "Win/mac underlay deferred; see recon mesh-underlay-linux-tun-wg-vs-win-mac",
249 }
250}
251
252#[cfg(not(target_os = "linux"))]
253fn platform_unsupported() -> MeshUnderlayError {
254 MeshUnderlayError::PlatformUnsupported {
255 os: std::env::consts::OS,
256 hint: "Linux-first tip 5; Win/mac underlay later (WireGuardNT/utun) — see recon doc",
257 }
258}
259
260#[cfg(not(target_os = "linux"))]
262#[derive(Debug)]
263pub struct UnsupportedUnderlayHandle;
264
265#[cfg(not(target_os = "linux"))]
267#[derive(Debug, Clone, PartialEq, Eq)]
268pub struct TunSurfaceReport {
269 pub device_present: bool,
270 pub open_attempt: &'static str,
271 pub note: &'static str,
272}
273
274pub fn open_wireguard_daemon_stub() -> Result<(), MeshUnderlayError> {
276 Err(MeshUnderlayError::WireGuardDaemonNotInThisTip)
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282
283 #[test]
284 fn hq_is_dial_only_accept_refused() {
285 assert!(!MeshUnderlayRole::HqClientAdmin.may_accept_underlay());
286 assert_eq!(
287 assert_accept_allowed(MeshUnderlayRole::HqClientAdmin),
288 Err(MeshUnderlayError::HqMustNotAcceptUnderlay)
289 );
290 for role in [MeshUnderlayRole::C2Peer, MeshUnderlayRole::NodePeer] {
291 assert!(role.may_accept_underlay());
292 assert_eq!(assert_accept_allowed(role), Ok(()));
293 }
294 }
295
296 #[test]
297 fn token_barrier_independent_of_transport_key() {
298 let token = UnderlayAuthToken::new("probe-secret").unwrap();
299 assert!(authorize_probe(&token, Some("probe-secret")).is_ok());
300 assert_eq!(
301 authorize_probe(&token, Some("wrong-secret!!")),
302 Err(MeshUnderlayError::Unauthorized)
303 );
304 assert_eq!(
305 authorize_probe(&token, None),
306 Err(MeshUnderlayError::Unauthorized)
307 );
308 let _key = UnderlayTransportKey::from_bytes([7u8; 32]);
310 assert_eq!(
311 authorize_probe(&token, Some("still-wrong")),
312 Err(MeshUnderlayError::Unauthorized)
313 );
314 }
315
316 #[test]
317 fn wireguard_daemon_stub_refuses() {
318 assert_eq!(
319 open_wireguard_daemon_stub(),
320 Err(MeshUnderlayError::WireGuardDaemonNotInThisTip)
321 );
322 }
323
324 #[test]
325 fn secrets_redacted_in_debug() {
326 let key = UnderlayTransportKey::from_bytes([1u8; 32]);
327 let token = UnderlayAuthToken::new("super-secret-token").unwrap();
328 let key_dbg = format!("{key:?}");
329 let token_dbg = format!("{token:?}");
330 assert!(!key_dbg.contains("1, 1, 1"));
331 assert!(!token_dbg.contains("super-secret"));
332 assert!(key_dbg.contains("redacted"));
333 assert!(token_dbg.contains("redacted"));
334 }
335
336 #[cfg(target_os = "linux")]
337 #[tokio::test]
338 async fn linux_peer_encrypted_path_still_requires_token_for_probe() {
339 let transport = UnderlayTransportKey::from_bytes([9u8; 32]);
340 let auth = UnderlayAuthToken::new("underlay-probe-token").unwrap();
341
342 assert_eq!(
344 accept_peer(MeshUnderlayRole::HqClientAdmin, &transport, &auth)
345 .await
346 .err(),
347 Some(MeshUnderlayError::HqMustNotAcceptUnderlay)
348 );
349
350 let listener = accept_peer(MeshUnderlayRole::C2Peer, &transport, &auth)
351 .await
352 .expect("c2 peer may accept");
353 let addr = listener.local_addr().expect("bound");
354
355 let dial_task = {
356 let transport = transport.clone();
357 let auth = auth.clone();
358 tokio::spawn(async move {
359 dial_peer(
360 MeshUnderlayRole::NodePeer,
361 &transport,
362 &auth,
363 &addr.to_string(),
364 )
365 .await
366 })
367 };
368
369 let server = listener
370 .accept()
371 .await
372 .expect("accept encrypted peer session");
373 let mut client = dial_task.await.expect("join").expect("dial ok");
374
375 client
377 .send_encrypted(b"hello-from-node")
378 .await
379 .expect("client send");
380 let got = server.recv_encrypted().await.expect("server recv");
381 assert_eq!(got, b"hello-from-node");
382
383 assert_eq!(
385 client.probe(None),
386 Err(MeshUnderlayError::Unauthorized)
387 );
388 assert_eq!(
389 client.probe(Some("wrong")),
390 Err(MeshUnderlayError::Unauthorized)
391 );
392 client
393 .probe(Some("underlay-probe-token"))
394 .expect("probe authorized");
395 server
396 .probe(Some("underlay-probe-token"))
397 .expect("server probe authorized");
398 }
399
400 #[cfg(target_os = "linux")]
401 #[test]
402 fn linux_tun_surface_report_is_informative() {
403 let report = probe_tun_surface();
404 assert!(
406 report.open_attempt == "ok"
407 || report.open_attempt == "permission-denied"
408 || report.open_attempt == "error"
409 || report.open_attempt == "missing-device"
410 );
411 assert!(report.note.contains("tip 5") || report.note.contains("CAP_NET_ADMIN") || report.note.contains("userspace"));
412 }
413
414
415 #[cfg(target_os = "linux")]
416 #[tokio::test]
417 async fn tip6_bridge_health_over_underlay_requires_token_barriers() {
418 use crate::mesh_underlay::{serve_bridge_tcp_relay, BridgeUnderlayClient};
419 use tokio::io::{AsyncReadExt, AsyncWriteExt};
420 use tokio::net::TcpListener;
421
422 let transport = UnderlayTransportKey::from_bytes([11u8; 32]);
423 let underlay_auth = UnderlayAuthToken::new("underlay-tip6-token").unwrap();
424
425 let tcp = TcpListener::bind("127.0.0.1:0").await.unwrap();
426 let bridge_addr = tcp.local_addr().unwrap();
427 let app_task = tokio::spawn(async move {
428 let (mut stream, _) = tcp.accept().await.unwrap();
429 let mut buf = vec![0u8; 1024];
430 let _ = stream.read(&mut buf).await;
431 let body = br#"{"ok":true,"door":"node-envelope-bridge","tip":6}"#;
432 let resp = format!(
433 "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
434 body.len(),
435 std::str::from_utf8(body).unwrap()
436 );
437 stream.write_all(resp.as_bytes()).await.unwrap();
438 });
439
440 assert_eq!(
441 accept_peer(MeshUnderlayRole::HqClientAdmin, &transport, &underlay_auth)
442 .await
443 .err(),
444 Some(MeshUnderlayError::HqMustNotAcceptUnderlay)
445 );
446
447 let listener = accept_peer(MeshUnderlayRole::NodePeer, &transport, &underlay_auth)
449 .await
450 .unwrap();
451 let addr = listener.local_addr().unwrap();
452 let client_bad = {
453 let transport = transport.clone();
454 let underlay_auth = underlay_auth.clone();
455 tokio::spawn(async move {
456 let session = dial_peer(
457 MeshUnderlayRole::C2Peer,
458 &transport,
459 &underlay_auth,
460 &addr.to_string(),
461 )
462 .await
463 .unwrap();
464 BridgeUnderlayClient::open(session, "wrong-token").await
465 })
466 };
467 let session = listener.accept().await.unwrap();
468 let server_err = serve_bridge_tcp_relay(session, &underlay_auth, bridge_addr).await;
469 assert!(matches!(server_err, Err(MeshUnderlayError::Unauthorized)));
470 let client_err = client_bad.await.unwrap();
471 assert!(matches!(client_err, Err(MeshUnderlayError::Unauthorized)));
472
473 let listener = accept_peer(MeshUnderlayRole::NodePeer, &transport, &underlay_auth)
475 .await
476 .unwrap();
477 let addr = listener.local_addr().unwrap();
478 let client_ok = {
479 let transport = transport.clone();
480 let underlay_auth = underlay_auth.clone();
481 tokio::spawn(async move {
482 let session = dial_peer(
483 MeshUnderlayRole::C2Peer,
484 &transport,
485 &underlay_auth,
486 &addr.to_string(),
487 )
488 .await
489 .unwrap();
490 let mut client = BridgeUnderlayClient::open(session, "underlay-tip6-token")
491 .await
492 .unwrap();
493 client
494 .write_all(b"GET /bridge/health HTTP/1.1\r\nHost: localhost\r\n\r\n")
495 .await
496 .unwrap();
497 let resp = client.read_at_least(12).await.unwrap();
498 let text = String::from_utf8_lossy(&resp);
499 assert!(text.contains("200 OK"), "got {text}");
500 assert!(text.contains("node-envelope-bridge"));
501 client.close().await.unwrap();
502 })
503 };
504 let session = listener.accept().await.unwrap();
505 serve_bridge_tcp_relay(session, &underlay_auth, bridge_addr)
506 .await
507 .expect("authorized relay");
508 client_ok.await.unwrap();
509 app_task.await.unwrap();
510 }
511
512
513 #[cfg(target_os = "linux")]
514 #[tokio::test]
515 async fn tip6_bridge_multi_chunk_relay_over_underlay() {
516 use crate::mesh_underlay::{serve_bridge_tcp_relay, BridgeUnderlayClient};
517 use tokio::io::{AsyncReadExt, AsyncWriteExt};
518 use tokio::net::TcpListener;
519
520 let transport = UnderlayTransportKey::from_bytes([13u8; 32]);
521 let underlay_auth = UnderlayAuthToken::new("underlay-tip6-chunk").unwrap();
522
523 let tcp = TcpListener::bind("127.0.0.1:0").await.unwrap();
525 let bridge_addr = tcp.local_addr().unwrap();
526 let payload = vec![0x5Au8; 9_000]; let app_task = {
528 let payload = payload.clone();
529 tokio::spawn(async move {
530 let (mut stream, _) = tcp.accept().await.unwrap();
531 let mut buf = vec![0u8; 64];
532 let _ = stream.read(&mut buf).await;
533 stream.write_all(&payload).await.unwrap();
534 let _ = stream.shutdown().await;
535 })
536 };
537
538 let listener = accept_peer(MeshUnderlayRole::NodePeer, &transport, &underlay_auth)
539 .await
540 .unwrap();
541 let addr = listener.local_addr().unwrap();
542 let client_task = {
543 let transport = transport.clone();
544 let underlay_auth = underlay_auth.clone();
545 let expect = payload.clone();
546 tokio::spawn(async move {
547 let session = dial_peer(
548 MeshUnderlayRole::C2Peer,
549 &transport,
550 &underlay_auth,
551 &addr.to_string(),
552 )
553 .await
554 .unwrap();
555 let mut client = BridgeUnderlayClient::open(session, "underlay-tip6-chunk")
556 .await
557 .unwrap();
558 client.write_all(b"GET /echo HTTP/1.1\r\n\r\n").await.unwrap();
559 let got = client.read_at_least(expect.len()).await.unwrap();
560 assert_eq!(got, expect, "multi-chunk reassembly mismatch");
561 client.close().await.unwrap();
562 })
563 };
564 let session = listener.accept().await.unwrap();
565 serve_bridge_tcp_relay(session, &underlay_auth, bridge_addr)
566 .await
567 .expect("authorized multi-chunk relay");
568 client_task.await.unwrap();
569 app_task.await.unwrap();
570 }
571
572 #[cfg(not(target_os = "linux"))]
573 #[test]
574 fn non_linux_accept_refuses_with_clear_platform_error() {
575 let transport = UnderlayTransportKey::from_bytes([3u8; 32]);
576 let auth = UnderlayAuthToken::new("t").unwrap();
577 let err = accept_peer(MeshUnderlayRole::C2Peer, &transport, &auth).unwrap_err();
578 match err {
579 MeshUnderlayError::PlatformUnsupported { hint, .. } => {
580 assert!(hint.contains("Linux-first"));
581 }
582 other => panic!("expected PlatformUnsupported, got {other:?}"),
583 }
584 }
585}