use crate::seed::TunnelKind;
use crate::tunnel::TunnelError;
use std::io::{Read, Write};
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4, TcpStream};
use std::time::Duration;
pub const LINK_INSTALLER: Ipv4Addr = Ipv4Addr::new(10, 45, 0, 1);
pub const LINK_VM: Ipv4Addr = Ipv4Addr::new(10, 45, 0, 2);
pub const LINK_PREFIX: u8 = 30;
pub const LINK_PORT: u16 = 51821;
pub const LINK_IFACE: &str = "wg-installer";
pub const LINK_MTU: usize = 1380;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Reach {
Tunnel,
}
pub fn reach(kind: TunnelKind) -> Result<Reach, TunnelError> {
match kind {
TunnelKind::None => Err(TunnelError::InstallerLinkNone),
TunnelKind::Wireguard | TunnelKind::Mullvad => Ok(Reach::Tunnel),
TunnelKind::Tailscale => Err(TunnelError::Unimplemented {
kind: TunnelKind::Tailscale,
missing: "a one-time installer node on the tailnet (a pure-Rust Tailscale coordination client does not exist in the suite)",
}),
}
}
pub fn vm_bind(r: Reach) -> Ipv4Addr {
match r {
Reach::Tunnel => LINK_VM,
}
}
pub trait Wire: Read + Write {}
impl<T: Read + Write> Wire for T {}
pub trait Path: Send {
fn connect(&mut self, timeout: Duration) -> Result<Box<dyn Wire + '_>, String>;
fn gone(&mut self, wait: Duration) -> Result<bool, String>;
fn describe(&self) -> String;
}
pub struct DirectPath {
pub at: SocketAddrV4,
}
impl Path for DirectPath {
fn connect(&mut self, timeout: Duration) -> Result<Box<dyn Wire + '_>, String> {
let s = TcpStream::connect_timeout(&SocketAddr::V4(self.at), timeout).map_err(|e| format!("could not be reached: {}: {e}", self.at))?;
let _ = s.set_read_timeout(Some(timeout));
let _ = s.set_write_timeout(Some(timeout));
Ok(Box::new(s))
}
fn gone(&mut self, wait: Duration) -> Result<bool, String> {
let t0 = std::time::Instant::now();
while t0.elapsed() < wait {
if TcpStream::connect_timeout(&SocketAddr::V4(self.at), Duration::from_secs(2)).is_err() {
return Ok(true);
}
std::thread::sleep(Duration::from_millis(250));
}
Ok(false)
}
fn describe(&self) -> String {
format!("{} (direct mutual TLS)", self.at)
}
}
#[derive(Clone, PartialEq, Eq)]
pub struct LinkKeys {
pub installer_private: [u8; 32],
pub vm_public: [u8; 32],
}
impl std::fmt::Debug for LinkKeys {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("LinkKeys { .. }")
}
}
#[cfg(feature = "installer-wireguard")]
pub use wg::WgPath;
#[cfg(feature = "installer-wireguard")]
mod wg {
use super::*;
use boringtun::noise::{Tunn, TunnResult};
use boringtun::x25519::{PublicKey, StaticSecret};
use smoltcp::iface::{Config, Interface, SocketHandle, SocketSet};
use smoltcp::phy::{Device, DeviceCapabilities, Medium};
use smoltcp::socket::tcp;
use smoltcp::wire::{HardwareAddress, IpAddress, IpCidr};
use std::collections::VecDeque;
use std::net::UdpSocket;
use std::time::Instant;
#[derive(Default)]
struct Queues {
rx: VecDeque<Vec<u8>>,
tx: VecDeque<Vec<u8>>,
}
struct Rx(Vec<u8>);
struct Tx<'a>(&'a mut VecDeque<Vec<u8>>);
impl smoltcp::phy::RxToken for Rx {
fn consume<R, F: FnOnce(&[u8]) -> R>(self, f: F) -> R {
f(&self.0)
}
}
impl smoltcp::phy::TxToken for Tx<'_> {
fn consume<R, F: FnOnce(&mut [u8]) -> R>(self, len: usize, f: F) -> R {
let mut buf = vec![0u8; len];
let r = f(&mut buf);
self.0.push_back(buf);
r
}
}
impl Device for Queues {
type RxToken<'a> = Rx;
type TxToken<'a> = Tx<'a>;
fn receive(&mut self, _t: smoltcp::time::Instant) -> Option<(Rx, Tx<'_>)> {
let p = self.rx.pop_front()?;
Some((Rx(p), Tx(&mut self.tx)))
}
fn transmit(&mut self, _t: smoltcp::time::Instant) -> Option<Tx<'_>> {
Some(Tx(&mut self.tx))
}
fn capabilities(&self) -> DeviceCapabilities {
let mut c = DeviceCapabilities::default();
c.medium = Medium::Ip;
c.max_transmission_unit = LINK_MTU;
c
}
}
pub struct WgPath {
endpoint: SocketAddrV4,
keys: LinkKeys,
udp: UdpSocket,
tunn: Tunn,
iface: Interface,
dev: Queues,
sockets: SocketSet<'static>,
epoch: Instant,
next_port: u16,
net: Vec<u8>,
}
fn now(epoch: Instant) -> smoltcp::time::Instant {
smoltcp::time::Instant::from_millis(epoch.elapsed().as_millis() as i64)
}
impl WgPath {
pub fn new(endpoint: SocketAddrV4, keys: LinkKeys) -> Result<WgPath, String> {
let udp = UdpSocket::bind("0.0.0.0:0").map_err(|e| format!("the installer's link socket: {e}"))?;
udp.connect(endpoint).map_err(|e| format!("the installer's link to {endpoint}: {e}"))?;
udp.set_read_timeout(Some(Duration::from_millis(5))).map_err(|e| e.to_string())?;
let epoch = Instant::now();
let mut dev = Queues::default();
let mut cfg = Config::new(HardwareAddress::Ip);
cfg.random_seed = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_nanos() as u64).unwrap_or(7);
let mut iface = Interface::new(cfg, &mut dev, now(epoch));
iface.update_ip_addrs(|a| {
let _ = a.push(IpCidr::new(IpAddress::Ipv4(LINK_INSTALLER), LINK_PREFIX));
});
let tunn = Self::tunn(&keys, 1);
let seed_port = 40000 + (cfg_port_seed() % 20000) as u16;
Ok(WgPath { endpoint, keys, udp, tunn, iface, dev, sockets: SocketSet::new(Vec::new()), epoch, next_port: seed_port, net: vec![0u8; 65536] })
}
fn tunn(keys: &LinkKeys, index: u32) -> Tunn {
Tunn::new(StaticSecret::from(keys.installer_private), PublicKey::from(keys.vm_public), None, Some(15), index, None)
}
fn pump(&mut self) {
if let TunnResult::WriteToNetwork(p) = self.tunn.update_timers(&mut self.net) {
let _ = self.udp.send(p);
}
let mut dgram = [0u8; 2048];
for _ in 0..64 {
let n = match self.udp.recv(&mut dgram) {
Ok(n) => n,
Err(_) => break,
};
let mut first = true;
loop {
let r = self.tunn.decapsulate(if first { Some(std::net::IpAddr::V4(*self.endpoint.ip())) } else { None }, if first { &dgram[..n] } else { &[] }, &mut self.net);
first = false;
match r {
TunnResult::WriteToNetwork(p) => {
let _ = self.udp.send(p);
}
TunnResult::WriteToTunnelV4(p, src) => {
if src == LINK_VM {
self.dev.rx.push_back(p.to_vec());
}
break;
}
_ => break,
}
}
}
let t = now(self.epoch);
self.iface.poll(t, &mut self.dev, &mut self.sockets);
while let Some(p) = self.dev.tx.pop_front() {
if let TunnResult::WriteToNetwork(out) = self.tunn.encapsulate(&p, &mut self.net) {
let _ = self.udp.send(out);
}
}
}
}
fn cfg_port_seed() -> u64 {
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.subsec_nanos() as u64).unwrap_or(1)
}
struct LinkStream<'a> {
path: &'a mut WgPath,
h: SocketHandle,
timeout: Duration,
}
impl Read for LinkStream<'_> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let t0 = Instant::now();
loop {
self.path.pump();
let s = self.path.sockets.get_mut::<tcp::Socket>(self.h);
if s.can_recv() {
let n = s.recv_slice(buf).map_err(|e| std::io::Error::other(format!("{e:?}")))?;
if n > 0 {
return Ok(n);
}
} else if !s.may_recv() {
return Ok(0);
}
if t0.elapsed() > self.timeout {
return Err(std::io::Error::new(std::io::ErrorKind::TimedOut, "nothing from the VM over the link"));
}
}
}
}
impl Write for LinkStream<'_> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let t0 = Instant::now();
loop {
self.path.pump();
let s = self.path.sockets.get_mut::<tcp::Socket>(self.h);
if s.can_send() {
let n = s.send_slice(buf).map_err(|e| std::io::Error::other(format!("{e:?}")))?;
if n > 0 {
return Ok(n);
}
} else if !s.may_send() {
return Err(std::io::Error::new(std::io::ErrorKind::BrokenPipe, "the VM closed the connection"));
}
if t0.elapsed() > self.timeout {
return Err(std::io::Error::new(std::io::ErrorKind::TimedOut, "the link would not take the bytes"));
}
}
}
fn flush(&mut self) -> std::io::Result<()> {
let t0 = Instant::now();
loop {
self.path.pump();
let s = self.path.sockets.get_mut::<tcp::Socket>(self.h);
if s.send_queue() == 0 || !s.may_send() {
return Ok(());
}
if t0.elapsed() > self.timeout {
return Err(std::io::Error::new(std::io::ErrorKind::TimedOut, "the link would not drain"));
}
}
}
}
impl Drop for LinkStream<'_> {
fn drop(&mut self) {
self.path.sockets.get_mut::<tcp::Socket>(self.h).close();
for _ in 0..20 {
self.path.pump();
if !self.path.sockets.get_mut::<tcp::Socket>(self.h).is_open() {
break;
}
}
self.path.sockets.remove(self.h);
}
}
impl Path for WgPath {
fn connect(&mut self, timeout: Duration) -> Result<Box<dyn Wire + '_>, String> {
let sock = tcp::Socket::new(tcp::SocketBuffer::new(vec![0u8; 65536]), tcp::SocketBuffer::new(vec![0u8; 65536]));
let h = self.sockets.add(sock);
self.next_port = if self.next_port >= 60000 { 40000 } else { self.next_port + 1 };
let port = self.next_port;
{
let cx = self.iface.context();
let s = self.sockets.get_mut::<tcp::Socket>(h);
if let Err(e) = s.connect(cx, (IpAddress::Ipv4(LINK_VM), crate::bootstrap::PORT), port) {
self.sockets.remove(h);
return Err(format!("the link's TCP to {LINK_VM}: {e:?}"));
}
}
let t0 = Instant::now();
loop {
self.pump();
let s = self.sockets.get_mut::<tcp::Socket>(h);
match s.state() {
tcp::State::Established => break,
tcp::State::Closed | tcp::State::TimeWait => {
self.sockets.remove(h);
return Err(format!("could not be reached: the VM at {} refused the bootstrap listener inside the link", self.endpoint));
}
_ => {}
}
if t0.elapsed() > timeout {
self.sockets.get_mut::<tcp::Socket>(h).abort();
self.pump();
self.sockets.remove(h);
let hs = match self.tunn.stats().0 {
Some(ago) => format!("the link handshook {} s ago", ago.as_secs()),
None => "no WireGuard handshake yet".to_string(),
};
return Err(format!("could not be reached: {} over the installer's link ({hs})", self.endpoint));
}
}
Ok(Box::new(LinkStream { path: self, h, timeout }))
}
fn gone(&mut self, wait: Duration) -> Result<bool, String> {
let t0 = Instant::now();
let mut index = 2u32;
let mut dgram = [0u8; 2048];
while t0.elapsed() < wait {
index += 1;
let mut t = Self::tunn(&self.keys, index);
let mut buf = [0u8; 256];
let init = match t.format_handshake_initiation(&mut buf, true) {
TunnResult::WriteToNetwork(p) => p.to_vec(),
other => return Err(format!("boringtun: {other:?}")),
};
let _ = self.udp.send(&init);
let round = Instant::now();
let mut answered = false;
while round.elapsed() < Duration::from_secs(3) {
if let Ok(n) = self.udp.recv(&mut dgram) {
if matches!(t.decapsulate(Some(std::net::IpAddr::V4(*self.endpoint.ip())), &dgram[..n], &mut self.net), TunnResult::WriteToNetwork(_) | TunnResult::Done) {
answered = true;
break;
}
}
}
if !answered {
return Ok(true);
}
std::thread::sleep(Duration::from_millis(500));
}
Ok(false)
}
fn describe(&self) -> String {
format!("{} (the installer's WireGuard link, {LINK_INSTALLER} → {LINK_VM})", self.endpoint)
}
}
}
#[cfg(feature = "wireguard")]
pub struct VmLink {
handle: Option<wg_appliance_core::TunnelHandle>,
pub bind: Ipv4Addr,
}
#[cfg(feature = "wireguard")]
impl VmLink {
pub fn up(&self) -> bool {
self.handle.is_some()
}
pub fn handshaken(&self) -> bool {
self.handle.as_ref().map(|h| h.handshaken()).unwrap_or(false)
}
pub fn drop_installer(&mut self) {
if let Some(mut h) = self.handle.take() {
h.shutdown();
}
}
}
#[cfg(feature = "wireguard")]
pub fn vm_link(seed: &crate::seed::Seed) -> Result<VmLink, TunnelError> {
use wg_appliance_core::{addr, dataplane, decode_key, PeerSpec, TunDevice, TunnelSpec};
match reach(seed.tunnel)? {
Reach::Tunnel => {
let l = seed.installer_link.as_ref().ok_or_else(|| TunnelError::NotConfigured { kind: seed.tunnel, what: "the seed carries no [installer_link]".into() })?;
let private = decode_key(&l.vm_private).map_err(|e| TunnelError::Failed(format!("installer_link.vm_private: {}", e.0)))?;
let installer = decode_key(&l.installer_pubkey).map_err(|e| TunnelError::Failed(format!("installer_link.installer_pubkey: {}", e.0)))?;
let tun = TunDevice::create(LINK_IFACE, LINK_MTU as u32 + 40).map_err(|e| TunnelError::Failed(format!("{LINK_IFACE}: {e}")))?;
addr::assign_ipv4(LINK_IFACE, LINK_VM, LINK_PREFIX).map_err(|e| TunnelError::Failed(format!("{LINK_IFACE} {LINK_VM}/{LINK_PREFIX}: {e}")))?;
addr::link_up(LINK_IFACE).map_err(|e| TunnelError::Failed(format!("{LINK_IFACE} up: {e}")))?;
let spec = TunnelSpec {
private_key: private,
listen_port: Some(l.port),
peers: vec![PeerSpec { public_key: installer, preshared_key: None, endpoint: None, allowed_ips: vec![(LINK_INSTALLER, 32)], persistent_keepalive: None }],
};
let handle = dataplane::spawn(tun, &spec).map_err(|e| TunnelError::Failed(format!("{LINK_IFACE}: {e}")))?;
Ok(VmLink { handle: Some(handle), bind: LINK_VM })
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_path_is_the_tunnel_kinds_and_tailscale_is_refused_by_name() {
assert!(reach(TunnelKind::None).unwrap_err().to_string().starts_with("REFUSED [installer-link-none]"), "WireGuard + mutual TLS always");
assert_eq!(reach(TunnelKind::Wireguard).unwrap(), Reach::Tunnel);
assert_eq!(reach(TunnelKind::Mullvad).unwrap(), Reach::Tunnel, "mullvad is only the exit; the mesh is WireGuard");
let e = reach(TunnelKind::Tailscale).unwrap_err().to_string();
assert!(e.starts_with("REFUSED [tunnel-tailscale-unimplemented]"), "{e}");
assert_eq!(vm_bind(Reach::Tunnel), LINK_VM, "never the public interface in tunnel mode");
assert!(!crate::in_overlay(LINK_VM) && !crate::in_overlay(LINK_INSTALLER), "the link is not the mesh");
assert!(format!("{:?}", LinkKeys { installer_private: [7; 32], vm_public: [1; 32] }).contains(".."), "no key in Debug");
}
}