Skip to main content

gate4agent_node_wire/mesh_underlay/
bridge_reach.rs

1//! Tip 6: browser-bridge TCP reachability over the tip-5 underlay path.
2//!
3//! Application dialect stays HTTP+WS over TCP. This module only carries
4//! length-framed TCP bytes inside the encrypted UDP underlay session after
5//! the **underlay auth token** barrier passes. It does not invent a second
6//! observe/drive dialect and is not a WireGuard daemon.
7//!
8//! Cite: mesh-connectivity-daemon-design tip 6; crypto ≠ authorization.
9
10use super::{authorize_probe, MeshUnderlayError, UnderlayAuthToken};
11use std::io;
12use std::net::SocketAddr;
13use tokio::io::{AsyncReadExt, AsyncWriteExt};
14use tokio::net::TcpStream;
15
16#[cfg(target_os = "linux")]
17use super::linux::LinuxUnderlaySession;
18
19const OPEN_REQ: &[u8] = b"bridge-tcp-open-v1";
20const OPEN_ACK: &[u8] = b"bridge-tcp-open-ack-v1";
21const OPEN_NACK: &[u8] = b"bridge-tcp-open-nack-v1";
22const KIND_DATA: u8 = 1;
23const KIND_CLOSE: u8 = 2;
24/// Leave room for kind+len header inside tip-5 MAX_PLAINTEXT (4096).
25const CHUNK_MAX: usize = 4_090;
26
27fn encode_open_with_token(token: &str) -> Result<Vec<u8>, MeshUnderlayError> {
28    let token_bytes = token.as_bytes();
29    if token_bytes.len() > 4_096 {
30        return Err(MeshUnderlayError::InvalidAuthToken);
31    }
32    let mut out = Vec::with_capacity(OPEN_REQ.len() + 2 + token_bytes.len());
33    out.extend_from_slice(OPEN_REQ);
34    out.extend_from_slice(&(token_bytes.len() as u16).to_be_bytes());
35    out.extend_from_slice(token_bytes);
36    Ok(out)
37}
38
39fn decode_open_with_token(packet: &[u8]) -> Result<&str, MeshUnderlayError> {
40    if packet.len() < OPEN_REQ.len() + 2 || !packet.starts_with(OPEN_REQ) {
41        return Err(MeshUnderlayError::Path(
42            "expected bridge-tcp-open-v1 with token".into(),
43        ));
44    }
45    let rest = &packet[OPEN_REQ.len()..];
46    let len = u16::from_be_bytes([rest[0], rest[1]]) as usize;
47    if rest.len() != 2 + len {
48        return Err(MeshUnderlayError::Path(
49            "bridge-tcp-open token length mismatch".into(),
50        ));
51    }
52    std::str::from_utf8(&rest[2..])
53        .map_err(|_| MeshUnderlayError::Path("bridge-tcp-open token not utf8".into()))
54}
55
56/// Client-side stream: HTTP+WS bytes in, framed underlay out (tip 6).
57#[cfg(target_os = "linux")]
58pub struct BridgeUnderlayClient {
59    session: LinuxUnderlaySession,
60    closed: bool,
61}
62
63#[cfg(target_os = "linux")]
64impl BridgeUnderlayClient {
65    /// Dial an already tip-5-handshaken session, send underlay token with OPEN,
66    /// then carry application TCP bytes. Application `GATE4AGENT_BRIDGE_TOKEN`
67    /// is still checked by the HTTP+WS door itself (second barrier).
68    pub async fn open(
69        mut session: LinuxUnderlaySession,
70        provided_underlay_token: &str,
71    ) -> Result<Self, MeshUnderlayError> {
72        let open = encode_open_with_token(provided_underlay_token)?;
73        session.send_encrypted(&open).await?;
74        let ack = session.recv_encrypted().await?;
75        if ack == OPEN_NACK {
76            return Err(MeshUnderlayError::Unauthorized);
77        }
78        if ack != OPEN_ACK {
79            return Err(MeshUnderlayError::Path(
80                "bridge-tcp-open ack mismatch".into(),
81            ));
82        }
83        Ok(Self {
84            session,
85            closed: false,
86        })
87    }
88
89    pub async fn write_all(&mut self, mut data: &[u8]) -> Result<(), MeshUnderlayError> {
90        if self.closed {
91            return Err(MeshUnderlayError::Path(
92                "bridge underlay client closed".into(),
93            ));
94        }
95        while !data.is_empty() {
96            let n = data.len().min(CHUNK_MAX);
97            let mut frame = Vec::with_capacity(1 + 4 + n);
98            frame.push(KIND_DATA);
99            frame.extend_from_slice(&(n as u32).to_be_bytes());
100            frame.extend_from_slice(&data[..n]);
101            self.session.send_encrypted(&frame).await?;
102            data = &data[n..];
103        }
104        Ok(())
105    }
106
107    pub async fn read_some(&mut self) -> Result<Vec<u8>, MeshUnderlayError> {
108        if self.closed {
109            return Ok(Vec::new());
110        }
111        let packet = self.session.recv_encrypted().await?;
112        if packet.is_empty() {
113            return Err(MeshUnderlayError::Path("empty bridge frame".into()));
114        }
115        match packet[0] {
116            KIND_CLOSE => {
117                self.closed = true;
118                Ok(Vec::new())
119            }
120            KIND_DATA => {
121                if packet.len() < 5 {
122                    return Err(MeshUnderlayError::Path("short data frame".into()));
123                }
124                let len = u32::from_be_bytes(packet[1..5].try_into().unwrap()) as usize;
125                if packet.len() != 5 + len {
126                    return Err(MeshUnderlayError::Path(
127                        "data frame length mismatch".into(),
128                    ));
129                }
130                Ok(packet[5..].to_vec())
131            }
132            _ => Err(MeshUnderlayError::Path("unknown bridge frame kind".into())),
133        }
134    }
135
136    /// Read until `min_bytes` accumulated or peer close (HTTP one-shots).
137    pub async fn read_at_least(&mut self, min_bytes: usize) -> Result<Vec<u8>, MeshUnderlayError> {
138        let mut out = Vec::new();
139        while out.len() < min_bytes {
140            let chunk = self.read_some().await?;
141            if chunk.is_empty() {
142                break;
143            }
144            out.extend_from_slice(&chunk);
145        }
146        Ok(out)
147    }
148
149    pub async fn close(mut self) -> Result<(), MeshUnderlayError> {
150        if !self.closed {
151            self.session.send_encrypted(&[KIND_CLOSE]).await?;
152            self.closed = true;
153        }
154        Ok(())
155    }
156}
157
158/// Server side: wait for OPEN+token, authorize underlay token, dial local
159/// `--bridge-listen`, relay until close. HQ must never call this (accept
160/// refused upstream). Application BRIDGE_TOKEN remains a separate barrier.
161#[cfg(target_os = "linux")]
162pub async fn serve_bridge_tcp_relay(
163    mut session: LinuxUnderlaySession,
164    underlay_auth: &UnderlayAuthToken,
165    local_bridge: SocketAddr,
166) -> Result<(), MeshUnderlayError> {
167    let open = session.recv_encrypted().await?;
168    let provided = decode_open_with_token(&open)?;
169    if let Err(err) = authorize_probe(underlay_auth, Some(provided)) {
170        let _ = session.send_encrypted(OPEN_NACK).await;
171        return Err(err);
172    }
173
174    let mut tcp = TcpStream::connect(local_bridge)
175        .await
176        .map_err(|e| MeshUnderlayError::Path(format!("local bridge dial: {e}")))?;
177    session.send_encrypted(OPEN_ACK).await?;
178
179    let mut tcp_buf = vec![0u8; CHUNK_MAX];
180    loop {
181        tokio::select! {
182            biased;
183            read = tcp.read(&mut tcp_buf) => {
184                match read {
185                    Ok(0) => {
186                        let _ = session.send_encrypted(&[KIND_CLOSE]).await;
187                        break;
188                    }
189                    Ok(n) => {
190                        let mut frame = Vec::with_capacity(1 + 4 + n);
191                        frame.push(KIND_DATA);
192                        frame.extend_from_slice(&(n as u32).to_be_bytes());
193                        frame.extend_from_slice(&tcp_buf[..n]);
194                        session.send_encrypted(&frame).await?;
195                    }
196                    Err(err) => {
197                        let _ = session.send_encrypted(&[KIND_CLOSE]).await;
198                        return Err(MeshUnderlayError::Path(format!("tcp read: {err}")));
199                    }
200                }
201            }
202            packet = session.recv_encrypted() => {
203                let packet = packet?;
204                if packet.is_empty() {
205                    return Err(MeshUnderlayError::Path("empty bridge frame".into()));
206                }
207                match packet[0] {
208                    KIND_CLOSE => {
209                        let _ = tcp.shutdown().await;
210                        break;
211                    }
212                    KIND_DATA => {
213                        if packet.len() < 5 {
214                            return Err(MeshUnderlayError::Path("short data frame".into()));
215                        }
216                        let len = u32::from_be_bytes(packet[1..5].try_into().unwrap()) as usize;
217                        if packet.len() != 5 + len {
218                            return Err(MeshUnderlayError::Path(
219                                "data frame length mismatch".into(),
220                            ));
221                        }
222                        tcp.write_all(&packet[5..])
223                            .await
224                            .map_err(|e| MeshUnderlayError::Path(format!("tcp write: {e}")))?;
225                    }
226                    _ => {
227                        return Err(MeshUnderlayError::Path(
228                            "unknown bridge frame kind".into(),
229                        ));
230                    }
231                }
232            }
233        }
234    }
235    Ok(())
236}
237
238/// Non-Linux refuse (Win/mac deferred).
239#[cfg(not(target_os = "linux"))]
240pub async fn serve_bridge_tcp_relay(
241    _session: super::UnsupportedUnderlayHandle,
242    _underlay_auth: &UnderlayAuthToken,
243    _local_bridge: SocketAddr,
244) -> Result<(), MeshUnderlayError> {
245    Err(MeshUnderlayError::PlatformUnsupported {
246        os: std::env::consts::OS,
247        hint: "Linux-first tip 6 bridge-over-underlay; Win/mac later",
248    })
249}
250
251#[cfg(not(target_os = "linux"))]
252pub struct BridgeUnderlayClient;
253
254/// Map underlay path errors that look like I/O into std::io::Error for node loops.
255pub fn underlay_to_io(err: MeshUnderlayError) -> io::Error {
256    io::Error::new(io::ErrorKind::Other, err.to_string())
257}
258
259#[cfg(test)]
260mod tests {
261    use super::*;
262
263    #[test]
264    fn open_encode_decode_round_trip() {
265        let packet = encode_open_with_token("underlay-tip6-token").unwrap();
266        assert!(packet.starts_with(OPEN_REQ));
267        assert_eq!(decode_open_with_token(&packet).unwrap(), "underlay-tip6-token");
268    }
269
270    #[test]
271    fn open_decode_rejects_truncated_and_bad_length() {
272        assert!(decode_open_with_token(b"nope").is_err());
273        assert!(decode_open_with_token(OPEN_REQ).is_err());
274        let mut packet = OPEN_REQ.to_vec();
275        packet.extend_from_slice(&5u16.to_be_bytes());
276        packet.extend_from_slice(b"abcd"); // claims 5, has 4
277        assert!(decode_open_with_token(&packet).is_err());
278    }
279
280    #[test]
281    fn open_encode_rejects_oversized_token() {
282        let huge = "x".repeat(5_000);
283        assert_eq!(
284            encode_open_with_token(&huge),
285            Err(MeshUnderlayError::InvalidAuthToken)
286        );
287    }
288
289    #[test]
290    fn underlay_to_io_preserves_message_without_secrets() {
291        let err = underlay_to_io(MeshUnderlayError::Unauthorized);
292        let msg = err.to_string();
293        assert!(msg.contains("unauthorized") || msg.contains("Unauthorized") || msg.contains("token"));
294        assert!(!msg.contains("super-secret"));
295    }
296}