gate4agent_node_wire/mesh_underlay/
bridge_reach.rs1use 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;
24const 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#[cfg(target_os = "linux")]
58pub struct BridgeUnderlayClient {
59 session: LinuxUnderlaySession,
60 closed: bool,
61}
62
63#[cfg(target_os = "linux")]
64impl BridgeUnderlayClient {
65 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 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#[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#[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
254pub 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"); 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}