gate4agent_node_wire/mesh_underlay/
linux.rs1use super::{
7 assert_accept_allowed, authorize_probe, MeshUnderlayError, MeshUnderlayRole, UnderlayAuthToken,
8 UnderlayTransportKey,
9};
10use ring::aead::{Aad, LessSafeKey, Nonce, UnboundKey, CHACHA20_POLY1305};
11use std::net::SocketAddr;
12use std::path::Path;
13use tokio::net::UdpSocket;
14
15const NONCE_LEN: usize = 12;
16const TAG_LEN: usize = 16;
17const MAX_PLAINTEXT: usize = 4_096;
18
19pub struct LinuxUnderlayListener {
21 socket: UdpSocket,
22 key: LessSafeKey,
23 auth: UnderlayAuthToken,
24 send_counter: u64,
26}
27
28pub struct LinuxUnderlaySession {
30 socket: UdpSocket,
31 #[allow(dead_code)]
32 peer: SocketAddr,
33 key: LessSafeKey,
34 auth: UnderlayAuthToken,
35 send_counter: u64,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct TunSurfaceReport {
41 pub device_present: bool,
42 pub open_attempt: &'static str,
43 pub note: &'static str,
44}
45
46pub fn probe_tun_surface() -> TunSurfaceReport {
47 let device_present = Path::new("/dev/net/tun").exists();
48 if !device_present {
49 return TunSurfaceReport {
50 device_present: false,
51 open_attempt: "missing-device",
52 note: "tip 5 uses userspace UDP+AEAD; kernel TUN/WG deferred",
53 };
54 }
55 match std::fs::OpenOptions::new()
57 .read(true)
58 .write(true)
59 .open("/dev/net/tun")
60 {
61 Ok(_f) => TunSurfaceReport {
62 device_present: true,
63 open_attempt: "ok",
64 note: "tip 5 does not configure WireGuard; CAP_NET_ADMIN may still be needed for iface",
65 },
66 Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => TunSurfaceReport {
67 device_present: true,
68 open_attempt: "permission-denied",
69 note: "CAP_NET_ADMIN / device cgroup likely required for TUN; tip 5 UDP path does not need it",
70 },
71 Err(_) => TunSurfaceReport {
72 device_present: true,
73 open_attempt: "error",
74 note: "TUN present but open failed; tip 5 continues on userspace UDP+AEAD",
75 },
76 }
77}
78
79fn sealing_key(transport: &UnderlayTransportKey) -> Result<LessSafeKey, MeshUnderlayError> {
80 let unbound = UnboundKey::new(&CHACHA20_POLY1305, transport.as_bytes())
81 .map_err(|_| MeshUnderlayError::InvalidTransportKey)?;
82 Ok(LessSafeKey::new(unbound))
83}
84
85
86pub async fn accept_peer(
87 role: MeshUnderlayRole,
88 transport_key: &UnderlayTransportKey,
89 auth_token: &UnderlayAuthToken,
90) -> Result<LinuxUnderlayListener, MeshUnderlayError> {
91 accept_peer_on(
92 role,
93 transport_key,
94 auth_token,
95 "127.0.0.1:0".parse().expect("loopback ephemeral"),
96 )
97 .await
98}
99
100pub async fn accept_peer_on(
104 role: MeshUnderlayRole,
105 transport_key: &UnderlayTransportKey,
106 auth_token: &UnderlayAuthToken,
107 bind: SocketAddr,
108) -> Result<LinuxUnderlayListener, MeshUnderlayError> {
109 assert_accept_allowed(role)?;
110 let key = sealing_key(transport_key)?;
111 let socket = UdpSocket::bind(bind)
112 .await
113 .map_err(|e| MeshUnderlayError::Path(e.to_string()))?;
114 Ok(LinuxUnderlayListener {
115 socket,
116 key,
117 auth: auth_token.clone(),
118 send_counter: 0,
119 })
120}
121
122pub async fn dial_peer(
123 role: MeshUnderlayRole,
124 transport_key: &UnderlayTransportKey,
125 auth_token: &UnderlayAuthToken,
126 peer: &str,
127) -> Result<LinuxUnderlaySession, MeshUnderlayError> {
128 let _ = role;
130 let key = sealing_key(transport_key)?;
131 let peer: SocketAddr = peer
132 .parse()
133 .map_err(|e: std::net::AddrParseError| MeshUnderlayError::Path(e.to_string()))?;
134 let socket = UdpSocket::bind("127.0.0.1:0")
135 .await
136 .map_err(|e| MeshUnderlayError::Path(e.to_string()))?;
137 socket
138 .connect(peer)
139 .await
140 .map_err(|e| MeshUnderlayError::Path(e.to_string()))?;
141
142 let mut session = LinuxUnderlaySession {
144 socket,
145 peer,
146 key,
147 auth: auth_token.clone(),
148 send_counter: 0,
149 };
150 session.send_encrypted(b"mesh-underlay-v1-hello").await?;
151 let reply = session.recv_encrypted().await?;
152 if reply != b"mesh-underlay-v1-hello-ack" {
153 return Err(MeshUnderlayError::Path(
154 "underlay hello ack mismatch".into(),
155 ));
156 }
157 Ok(session)
158}
159
160impl LinuxUnderlayListener {
161 pub fn local_addr(&self) -> Result<SocketAddr, MeshUnderlayError> {
162 self.socket
163 .local_addr()
164 .map_err(|e| MeshUnderlayError::Path(e.to_string()))
165 }
166
167 pub async fn accept(self) -> Result<LinuxUnderlaySession, MeshUnderlayError> {
168 let mut buf = vec![0u8; NONCE_LEN + MAX_PLAINTEXT + TAG_LEN];
170 let (n, peer) = self
171 .socket
172 .recv_from(&mut buf)
173 .await
174 .map_err(|e| MeshUnderlayError::Path(e.to_string()))?;
175 buf.truncate(n);
176
177 let mut session = LinuxUnderlaySession {
178 socket: self.socket,
179 peer,
180 key: self.key,
181 auth: self.auth,
182 send_counter: self.send_counter,
183 };
184 session
186 .socket
187 .connect(peer)
188 .await
189 .map_err(|e| MeshUnderlayError::Path(e.to_string()))?;
190
191 let plain = decrypt(&session.key, &buf)?;
192 if plain != b"mesh-underlay-v1-hello" {
193 return Err(MeshUnderlayError::Path(
194 "underlay hello mismatch".into(),
195 ));
196 }
197 session
198 .send_encrypted(b"mesh-underlay-v1-hello-ack")
199 .await?;
200 Ok(session)
201 }
202}
203
204impl LinuxUnderlaySession {
205 pub async fn send_encrypted(&mut self, plaintext: &[u8]) -> Result<(), MeshUnderlayError> {
206 if plaintext.len() > MAX_PLAINTEXT {
207 return Err(MeshUnderlayError::Path("plaintext too large".into()));
208 }
209 let counter = self.send_counter;
210 self.send_counter = self
211 .send_counter
212 .checked_add(1)
213 .ok_or_else(|| MeshUnderlayError::Path("nonce counter overflow".into()))?;
214 let mut nonce_bytes = [0u8; NONCE_LEN];
215 nonce_bytes[4..].copy_from_slice(&counter.to_be_bytes());
216 let mut out = Vec::with_capacity(NONCE_LEN + plaintext.len() + TAG_LEN);
217 out.extend_from_slice(&nonce_bytes);
218 let nonce = Nonce::assume_unique_for_key(nonce_bytes);
219 let mut body = plaintext.to_vec();
220 self.key
221 .seal_in_place_append_tag(nonce, Aad::empty(), &mut body)
222 .map_err(|_| MeshUnderlayError::Path("seal failed".into()))?;
223 out.extend_from_slice(&body);
224 self.socket
225 .send(&out)
226 .await
227 .map_err(|e| MeshUnderlayError::Path(e.to_string()))?;
228 Ok(())
229 }
230
231 pub async fn recv_encrypted(&self) -> Result<Vec<u8>, MeshUnderlayError> {
232 let mut buf = vec![0u8; NONCE_LEN + MAX_PLAINTEXT + TAG_LEN];
233 let n = self
234 .socket
235 .recv(&mut buf)
236 .await
237 .map_err(|e| MeshUnderlayError::Path(e.to_string()))?;
238 buf.truncate(n);
239 decrypt(&self.key, &buf)
240 }
241
242 pub fn probe(&self, provided_token: Option<&str>) -> Result<(), MeshUnderlayError> {
244 authorize_probe(&self.auth, provided_token)
245 }
246}
247
248fn decrypt(key: &LessSafeKey, packet: &[u8]) -> Result<Vec<u8>, MeshUnderlayError> {
249 if packet.len() < NONCE_LEN + TAG_LEN {
250 return Err(MeshUnderlayError::Path("datagram too short".into()));
251 }
252 let (nonce_bytes, ct) = packet.split_at(NONCE_LEN);
253 let mut nonce_arr = [0u8; NONCE_LEN];
254 nonce_arr.copy_from_slice(nonce_bytes);
255 let nonce = Nonce::assume_unique_for_key(nonce_arr);
256 let mut body = ct.to_vec();
257 let plain = key
258 .open_in_place(nonce, Aad::empty(), &mut body)
259 .map_err(|_| MeshUnderlayError::Path("open failed".into()))?;
260 Ok(plain.to_vec())
261}