use crate::elect::Capacity;
use crate::seed::Kind;
use crate::Role;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::fmt;
use std::net::Ipv4Addr;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct JoinRequest {
pub estate: String,
pub kind: Kind,
pub tunnel_pubkey: String,
pub capacity: Option<Capacity>,
#[serde(default)]
pub csr_pem: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Assigned {
pub name: String,
pub overlay: Ipv4Addr,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Peer {
pub name: String,
pub role: Role,
pub tunnel_pubkey: String,
pub overlay: Ipv4Addr,
pub endpoint: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub utility: Option<Ipv4Addr>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FrontTunnel {
pub tunnel_pubkey: String,
pub endpoint: String,
}
pub const FRONT_PEER: &str = "front";
impl FrontTunnel {
pub fn as_peer(&self) -> Peer {
Peer { name: FRONT_PEER.to_string(), role: Role::Front, tunnel_pubkey: self.tunnel_pubkey.clone(), overlay: crate::OVERLAY_FRONT, endpoint: Some(self.endpoint.clone()), utility: None }
}
pub fn check(&self) -> Result<(), JoinError> {
if self.tunnel_pubkey.trim().is_empty() {
return Err(JoinError::Wire("the door's front tunnel key is empty".into()));
}
if self.endpoint.rsplit_once(':').and_then(|(_, p)| p.parse::<u16>().ok()).is_none() {
return Err(JoinError::Wire(format!("the door's front endpoint `{}` is not host:port", self.endpoint)));
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct JoinReply {
pub estate: String,
pub me: Assigned,
#[serde(default)]
pub cert_pem: Option<String>,
pub role: Option<Role>,
pub primary_overlay: Option<Ipv4Addr>,
#[serde(default)]
pub primary_utility: Option<Ipv4Addr>,
pub peers: Vec<Peer>,
#[serde(default)]
pub feed: BTreeMap<String, String>,
}
impl JoinRequest {
pub fn render(&self) -> Result<String, JoinError> {
toml::to_string(self).map_err(|e| JoinError::Wire(e.to_string()))
}
pub fn parse(text: &str) -> Result<JoinRequest, JoinError> {
toml::from_str(text).map_err(|e| JoinError::Wire(format!("the body is not a join request: {e}")))
}
}
impl JoinReply {
pub fn parse(text: &str) -> Result<JoinReply, JoinError> {
toml::from_str(text).map_err(|e| JoinError::Wire(format!("the join reply is not a join reply: {e}")))
}
pub fn front(&self) -> Option<&Peer> {
self.peers.iter().find(|p| p.role == Role::Front)
}
pub fn render(&self) -> Result<String, JoinError> {
toml::to_string(self).map_err(|e| JoinError::Wire(e.to_string()))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Presented {
Bootstrap,
Box(String),
}
impl Presented {
pub fn from_cn(cn: &str) -> Presented {
if cn == crate::mtls::BOOTSTRAP_CN {
Presented::Bootstrap
} else {
Presented::Box(cn.to_string())
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum JoinError {
Wire(String),
Unknown(Ipv4Addr),
Spent { name: String, from: Ipv4Addr },
Forged(String),
Withdrawn,
WrongEstate { want: String, got: String },
Sign(String),
NotLaid(String),
}
impl fmt::Display for JoinError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
JoinError::Wire(e) => write!(f, "REFUSED [join-wire] the join message could not be rendered or read: {e}"),
JoinError::Unknown(a) => write!(f, "REFUSED [join-unknown] {a} is not an address this estate laid a box on"),
JoinError::Spent { name, from } => write!(f, "REFUSED [join-spent] `{name}` ({from}) has already joined; the bootstrap certificate is not accepted for it again — a copied medium"),
JoinError::Forged(why) => write!(f, "REFUSED [join-forged] {why}"),
JoinError::Withdrawn => write!(f, "REFUSED [join-withdrawn] every box has joined; the bootstrap certificate is valid during bootstrap only"),
JoinError::WrongEstate { want, got } => write!(f, "REFUSED [join-estate] this is `{want}`, the request says `{got}`"),
JoinError::Sign(e) => write!(f, "REFUSED [join-sign] {e}"),
JoinError::NotLaid(n) => write!(f, "REFUSED [door-forget-unknown] `{n}` is not a box this estate laid; there is no admission to clear"),
}
}
}
impl std::error::Error for JoinError {}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Slot {
pub name: String,
pub kind: Kind,
pub overlay: Ipv4Addr,
#[serde(default)]
pub endpoint: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub utility: Option<Ipv4Addr>,
}
impl Slot {
pub fn utility_or(&self, key: Ipv4Addr) -> Ipv4Addr {
self.utility.unwrap_or(key)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Admitted {
pub from: Ipv4Addr,
pub tunnel_pubkey: String,
pub capacity: Option<Capacity>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Admission {
First(String),
Again(String),
}
impl Admission {
pub fn name(&self) -> &str {
match self {
Admission::First(n) | Admission::Again(n) => n,
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Ledger {
pub estate: String,
pub slots: BTreeMap<Ipv4Addr, Slot>,
pub admitted: BTreeMap<String, Admitted>,
}
impl Ledger {
pub fn new(estate: &str) -> Ledger {
Ledger { estate: estate.to_string(), ..Default::default() }
}
pub fn lay(&mut self, utility: Ipv4Addr, slot: Slot) {
self.slots.insert(utility, slot);
}
pub fn admit(&mut self, req: &JoinRequest, who: &Presented, from: Ipv4Addr) -> Result<Admission, JoinError> {
if req.estate != self.estate {
return Err(JoinError::WrongEstate { want: self.estate.clone(), got: req.estate.clone() });
}
if *who == Presented::Bootstrap && self.complete() {
return Err(JoinError::Withdrawn);
}
let slot = self.slots.get(&from).ok_or(JoinError::Unknown(from))?.clone();
if req.kind != slot.kind && slot.kind != Kind::Front {
return Err(JoinError::Forged(format!("{from} presents kind {:?}; the slot `{}` laid there is {:?}", req.kind, slot.name, slot.kind)));
}
let joined = self.admitted.contains_key(&slot.name);
match (who, joined) {
(Presented::Bootstrap, true) => Err(JoinError::Spent { name: slot.name, from }),
(Presented::Bootstrap, false) => {
if req.csr_pem.trim().is_empty() {
return Err(JoinError::Forged(format!("the first join from {from} carries no signing request; a box must mint its own key")));
}
self.admitted.insert(slot.name.clone(), Admitted { from, tunnel_pubkey: req.tunnel_pubkey.clone(), capacity: req.capacity });
Ok(Admission::First(slot.name))
}
(Presented::Box(cn), true) if *cn == slot.name && self.admitted[&slot.name].from == from => {
let entry = self.admitted.get_mut(&slot.name).expect("joined was just checked");
if entry.tunnel_pubkey.is_empty() && !req.tunnel_pubkey.is_empty() {
entry.tunnel_pubkey = req.tunnel_pubkey.clone();
}
Ok(Admission::Again(slot.name))
}
(Presented::Box(cn), _) => Err(JoinError::Forged(format!("{from} is `{}`'s address, and it presents the certificate of `{cn}`", slot.name))),
}
}
pub fn slot_of(&self, name: &str) -> Option<(&Ipv4Addr, &Slot)> {
self.slots.iter().find(|(_, s)| s.name == name)
}
pub fn complete(&self) -> bool {
!self.slots.is_empty() && self.slots.values().all(|s| self.admitted.contains_key(&s.name))
}
}
#[cfg(test)]
mod tests {
use super::*;
const MAIN_UTIL: Ipv4Addr = Ipv4Addr::new(10, 13, 9, 90);
const TWIN_UTIL: Ipv4Addr = Ipv4Addr::new(10, 13, 9, 87);
fn ledger() -> Ledger {
let mut l = Ledger::new("gunnar.rs");
l.lay(MAIN_UTIL, Slot { name: "main".into(), kind: Kind::Gunnar, overlay: crate::OVERLAY_BOX_A, endpoint: None, utility: None });
l.lay(TWIN_UTIL, Slot { name: "twin".into(), kind: Kind::Gunnar, overlay: crate::OVERLAY_BOX_B, endpoint: None, utility: None });
l
}
fn req() -> JoinRequest {
JoinRequest {
estate: "gunnar.rs".into(),
kind: Kind::Gunnar,
tunnel_pubkey: "K1".into(),
capacity: Some(Capacity { cores: 2, ram_bytes: 4 << 30, data_bytes: 10 << 30, disk_mib_s: 400 }),
csr_pem: "-----BEGIN CERTIFICATE REQUEST-----".into(),
}
}
#[test]
fn the_bootstrap_certificate_joins_once_then_the_box_speaks_as_itself_and_at_completion_bootstrap_is_withdrawn() {
let mut l = ledger();
assert_eq!(l.admit(&req(), &Presented::Bootstrap, MAIN_UTIL), Ok(Admission::First("main".into())));
assert_eq!(l.admit(&req(), &Presented::Bootstrap, MAIN_UTIL), Err(JoinError::Spent { name: "main".into(), from: MAIN_UTIL }), "a copied medium");
assert_eq!(l.admit(&req(), &Presented::Box("main".into()), MAIN_UTIL), Ok(Admission::Again("main".into())));
assert!(matches!(l.admit(&req(), &Presented::Box("main".into()), TWIN_UTIL), Err(JoinError::Forged(_))), "main's certificate from twin's address");
assert!(matches!(l.admit(&req(), &Presented::Box("twin".into()), TWIN_UTIL), Err(JoinError::Forged(_))), "a certificate for a slot that never joined");
assert_eq!(l.admit(&req(), &Presented::Bootstrap, TWIN_UTIL), Ok(Admission::First("twin".into())));
assert!(l.complete());
assert_eq!(l.admit(&req(), &Presented::Bootstrap, Ipv4Addr::new(10, 13, 9, 1)), Err(JoinError::Withdrawn), "bootstrap only during bootstrap");
assert_eq!(l.admit(&req(), &Presented::Box("twin".into()), TWIN_UTIL), Ok(Admission::Again("twin".into())), "a joined box still reads");
}
#[test]
fn a_box_asking_as_itself_fills_an_empty_tunnel_key_once_and_never_replaces_one() {
let mut l = ledger();
let mut first = req();
first.tunnel_pubkey.clear();
l.admit(&first, &Presented::Bootstrap, MAIN_UTIL).unwrap();
assert_eq!(l.admitted["main"].tunnel_pubkey, "");
l.admit(&req(), &Presented::Box("main".into()), MAIN_UTIL).unwrap();
assert_eq!(l.admitted["main"].tunnel_pubkey, "K1", "learned late, recorded");
let mut other = req();
other.tunnel_pubkey = "K2".into();
l.admit(&other, &Presented::Box("main".into()), MAIN_UTIL).unwrap();
assert_eq!(l.admitted["main"].tunnel_pubkey, "K1", "never replaced");
}
#[test]
fn an_unlaid_address_a_wrong_kind_another_estate_and_a_join_without_a_request_are_refused_by_name() {
let mut l = ledger();
assert_eq!(l.admit(&req(), &Presented::Bootstrap, Ipv4Addr::new(10, 13, 9, 1)), Err(JoinError::Unknown(Ipv4Addr::new(10, 13, 9, 1))));
let mut front = req();
front.kind = Kind::Front;
assert!(matches!(l.admit(&front, &Presented::Bootstrap, MAIN_UTIL), Err(JoinError::Forged(_))));
let mut other = req();
other.estate = "holger.rs".into();
assert!(matches!(l.admit(&other, &Presented::Bootstrap, MAIN_UTIL), Err(JoinError::WrongEstate { .. })));
let mut bare = req();
bare.csr_pem.clear();
assert!(matches!(l.admit(&bare, &Presented::Bootstrap, MAIN_UTIL), Err(JoinError::Forged(_))));
assert!(!l.complete());
assert_eq!(Presented::from_cn("bootstrap"), Presented::Bootstrap);
assert_eq!(Presented::from_cn("main"), Presented::Box("main".into()));
assert!(JoinError::Withdrawn.to_string().starts_with("REFUSED [join-withdrawn]"));
}
#[test]
fn the_front_tunnel_is_a_dialled_peer_and_an_undialable_one_is_refused() {
let f = FrontTunnel { tunnel_pubkey: "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=".into(), endpoint: "95.111.211.189:51820".into() };
assert_eq!(f.check(), Ok(()));
let p = f.as_peer();
assert_eq!((p.name.as_str(), p.role, p.overlay, p.endpoint.as_deref()), (FRONT_PEER, Role::Front, crate::OVERLAY_FRONT, Some("95.111.211.189:51820")));
let reply = JoinReply { estate: "e".into(), me: Assigned { name: "main".into(), overlay: crate::OVERLAY_BOX_A }, cert_pem: None, role: None, primary_overlay: None, primary_utility: None, peers: vec![p.clone()], feed: BTreeMap::new() };
assert_eq!(reply.front(), Some(&p));
assert!(FrontTunnel { tunnel_pubkey: String::new(), ..f.clone() }.check().is_err());
assert!(FrontTunnel { endpoint: "nowhere".into(), ..f }.check().is_err());
}
#[test]
fn the_messages_round_trip() {
let r = req();
assert_eq!(JoinRequest::parse(&r.render().unwrap()).unwrap(), r);
let mut feed = BTreeMap::new();
feed.insert("monetize_pubkey".to_string(), "eb16…".to_string());
let reply = JoinReply { estate: "gunnar.rs".into(), me: Assigned { name: "main".into(), overlay: crate::OVERLAY_BOX_A }, cert_pem: Some("PEM".into()), role: None, primary_overlay: None, primary_utility: None, peers: vec![], feed };
assert_eq!(JoinReply::parse(&reply.render().unwrap()).unwrap(), reply);
}
}