use super::{
assert_accept_allowed, authorize_probe, MeshUnderlayError, MeshUnderlayRole, UnderlayAuthToken,
UnderlayTransportKey,
};
use ring::aead::{Aad, LessSafeKey, Nonce, UnboundKey, CHACHA20_POLY1305};
use std::net::SocketAddr;
use std::path::Path;
use tokio::net::UdpSocket;
const NONCE_LEN: usize = 12;
const TAG_LEN: usize = 16;
const MAX_PLAINTEXT: usize = 4_096;
pub struct LinuxUnderlayListener {
socket: UdpSocket,
key: LessSafeKey,
auth: UnderlayAuthToken,
send_counter: u64,
}
pub struct LinuxUnderlaySession {
socket: UdpSocket,
#[allow(dead_code)]
peer: SocketAddr,
key: LessSafeKey,
auth: UnderlayAuthToken,
send_counter: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TunSurfaceReport {
pub device_present: bool,
pub open_attempt: &'static str,
pub note: &'static str,
}
pub fn probe_tun_surface() -> TunSurfaceReport {
let device_present = Path::new("/dev/net/tun").exists();
if !device_present {
return TunSurfaceReport {
device_present: false,
open_attempt: "missing-device",
note: "tip 5 uses userspace UDP+AEAD; kernel TUN/WG deferred",
};
}
match std::fs::OpenOptions::new()
.read(true)
.write(true)
.open("/dev/net/tun")
{
Ok(_f) => TunSurfaceReport {
device_present: true,
open_attempt: "ok",
note: "tip 5 does not configure WireGuard; CAP_NET_ADMIN may still be needed for iface",
},
Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => TunSurfaceReport {
device_present: true,
open_attempt: "permission-denied",
note: "CAP_NET_ADMIN / device cgroup likely required for TUN; tip 5 UDP path does not need it",
},
Err(_) => TunSurfaceReport {
device_present: true,
open_attempt: "error",
note: "TUN present but open failed; tip 5 continues on userspace UDP+AEAD",
},
}
}
fn sealing_key(transport: &UnderlayTransportKey) -> Result<LessSafeKey, MeshUnderlayError> {
let unbound = UnboundKey::new(&CHACHA20_POLY1305, transport.as_bytes())
.map_err(|_| MeshUnderlayError::InvalidTransportKey)?;
Ok(LessSafeKey::new(unbound))
}
pub async fn accept_peer(
role: MeshUnderlayRole,
transport_key: &UnderlayTransportKey,
auth_token: &UnderlayAuthToken,
) -> Result<LinuxUnderlayListener, MeshUnderlayError> {
accept_peer_on(
role,
transport_key,
auth_token,
"127.0.0.1:0".parse().expect("loopback ephemeral"),
)
.await
}
pub async fn accept_peer_on(
role: MeshUnderlayRole,
transport_key: &UnderlayTransportKey,
auth_token: &UnderlayAuthToken,
bind: SocketAddr,
) -> Result<LinuxUnderlayListener, MeshUnderlayError> {
assert_accept_allowed(role)?;
let key = sealing_key(transport_key)?;
let socket = UdpSocket::bind(bind)
.await
.map_err(|e| MeshUnderlayError::Path(e.to_string()))?;
Ok(LinuxUnderlayListener {
socket,
key,
auth: auth_token.clone(),
send_counter: 0,
})
}
pub async fn dial_peer(
role: MeshUnderlayRole,
transport_key: &UnderlayTransportKey,
auth_token: &UnderlayAuthToken,
peer: &str,
) -> Result<LinuxUnderlaySession, MeshUnderlayError> {
let _ = role;
let key = sealing_key(transport_key)?;
let peer: SocketAddr = peer
.parse()
.map_err(|e: std::net::AddrParseError| MeshUnderlayError::Path(e.to_string()))?;
let socket = UdpSocket::bind("127.0.0.1:0")
.await
.map_err(|e| MeshUnderlayError::Path(e.to_string()))?;
socket
.connect(peer)
.await
.map_err(|e| MeshUnderlayError::Path(e.to_string()))?;
let mut session = LinuxUnderlaySession {
socket,
peer,
key,
auth: auth_token.clone(),
send_counter: 0,
};
session.send_encrypted(b"mesh-underlay-v1-hello").await?;
let reply = session.recv_encrypted().await?;
if reply != b"mesh-underlay-v1-hello-ack" {
return Err(MeshUnderlayError::Path(
"underlay hello ack mismatch".into(),
));
}
Ok(session)
}
impl LinuxUnderlayListener {
pub fn local_addr(&self) -> Result<SocketAddr, MeshUnderlayError> {
self.socket
.local_addr()
.map_err(|e| MeshUnderlayError::Path(e.to_string()))
}
pub async fn accept(self) -> Result<LinuxUnderlaySession, MeshUnderlayError> {
let mut buf = vec![0u8; NONCE_LEN + MAX_PLAINTEXT + TAG_LEN];
let (n, peer) = self
.socket
.recv_from(&mut buf)
.await
.map_err(|e| MeshUnderlayError::Path(e.to_string()))?;
buf.truncate(n);
let mut session = LinuxUnderlaySession {
socket: self.socket,
peer,
key: self.key,
auth: self.auth,
send_counter: self.send_counter,
};
session
.socket
.connect(peer)
.await
.map_err(|e| MeshUnderlayError::Path(e.to_string()))?;
let plain = decrypt(&session.key, &buf)?;
if plain != b"mesh-underlay-v1-hello" {
return Err(MeshUnderlayError::Path(
"underlay hello mismatch".into(),
));
}
session
.send_encrypted(b"mesh-underlay-v1-hello-ack")
.await?;
Ok(session)
}
}
impl LinuxUnderlaySession {
pub async fn send_encrypted(&mut self, plaintext: &[u8]) -> Result<(), MeshUnderlayError> {
if plaintext.len() > MAX_PLAINTEXT {
return Err(MeshUnderlayError::Path("plaintext too large".into()));
}
let counter = self.send_counter;
self.send_counter = self
.send_counter
.checked_add(1)
.ok_or_else(|| MeshUnderlayError::Path("nonce counter overflow".into()))?;
let mut nonce_bytes = [0u8; NONCE_LEN];
nonce_bytes[4..].copy_from_slice(&counter.to_be_bytes());
let mut out = Vec::with_capacity(NONCE_LEN + plaintext.len() + TAG_LEN);
out.extend_from_slice(&nonce_bytes);
let nonce = Nonce::assume_unique_for_key(nonce_bytes);
let mut body = plaintext.to_vec();
self.key
.seal_in_place_append_tag(nonce, Aad::empty(), &mut body)
.map_err(|_| MeshUnderlayError::Path("seal failed".into()))?;
out.extend_from_slice(&body);
self.socket
.send(&out)
.await
.map_err(|e| MeshUnderlayError::Path(e.to_string()))?;
Ok(())
}
pub async fn recv_encrypted(&self) -> Result<Vec<u8>, MeshUnderlayError> {
let mut buf = vec![0u8; NONCE_LEN + MAX_PLAINTEXT + TAG_LEN];
let n = self
.socket
.recv(&mut buf)
.await
.map_err(|e| MeshUnderlayError::Path(e.to_string()))?;
buf.truncate(n);
decrypt(&self.key, &buf)
}
pub fn probe(&self, provided_token: Option<&str>) -> Result<(), MeshUnderlayError> {
authorize_probe(&self.auth, provided_token)
}
}
fn decrypt(key: &LessSafeKey, packet: &[u8]) -> Result<Vec<u8>, MeshUnderlayError> {
if packet.len() < NONCE_LEN + TAG_LEN {
return Err(MeshUnderlayError::Path("datagram too short".into()));
}
let (nonce_bytes, ct) = packet.split_at(NONCE_LEN);
let mut nonce_arr = [0u8; NONCE_LEN];
nonce_arr.copy_from_slice(nonce_bytes);
let nonce = Nonce::assume_unique_for_key(nonce_arr);
let mut body = ct.to_vec();
let plain = key
.open_in_place(nonce, Aad::empty(), &mut body)
.map_err(|_| MeshUnderlayError::Path("open failed".into()))?;
Ok(plain.to_vec())
}