server/lib/src/sync/invited.rs
//! **The invite an upstream gave this downstream** (lane HOL-WGDS; direct invites since
//! HOL-PRODUCTS-7).
//!
//! The downstream's RON names its upstream with the invite it was given
//! (`sync.upstreams[].invite`). At its first start the invite is verified ONCE under that
//! upstream's key (signer, kind, this downstream, its key, not expired) and kept
//! (`holger-sync/enrolled-<up>.json`, written once, 0600); every later start reads the kept
//! copy, and another invite in its place is refused by name.
//!
//! * A **WireGuard** invite (schema v2, the link's pre-shared key in it) is for a downstream
//! behind the upstream's front: `tunnel::start` brings the link up from it.
//! * A **direct** invite (a plain server's, schema v1, no tunnel) is for a downstream that
//! reaches its upstream as it is (`via: Front`): its `door` is the upstream's sync door, so
//! the RON needs no `url:` ([`door_of`]). It is bound to the downstream's SYNC key.
use std::path::{Path, PathBuf};
use holger_sync::invite::{open_invite, DownstreamInvite};
use super::*;
/// The kept invite of upstream `<name>`, under [`STATE_DIR`].
pub fn enrolled_file(dir: &Path, upstream: &str) -> PathBuf {
dir.join(format!(
"enrolled-{}.json",
upstream
.chars()
.map(|c| if c.is_ascii_alphanumeric() || c == '-' || c == '_' {
c
} else {
'_'
})
.collect::<String>()
))
}
/// ★ **The invite this downstream runs under**: the kept one, else `invite_path` verified
/// once under `upstream`'s key and kept. Refused by name: a signer the RON does not trust, an
/// invite for another downstream or key, an expired one, a second invite that differs from
/// the kept one. With no file at `invite_path`, the kept one is the invite (RA5-20).
pub fn enrol(
data_root: &Path,
me: &str,
own_key: &str,
upstream: &SyncPeer,
invite_path: &Path,
now: i64,
) -> Result<DownstreamInvite, SyncFailure> {
let given = match std::fs::read(invite_path) {
Ok(b) => Some(b),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => None,
Err(e) => {
return Err(SyncError::Io(format!("the invite {}: {e}", invite_path.display())).into())
}
};
enrol_with(
data_root,
me,
own_key,
upstream,
given.as_deref(),
&invite_path.display().to_string(),
now,
)?
.ok_or_else(|| {
SyncFailure::new(
&holger_errcode::SYNC_DOWNSTREAM_REFUSED,
404,
format!(
"upstream '{}': no invite at {} and none kept: this downstream is not invited yet",
upstream.name,
invite_path.display()
),
)
})
}
/// ★ **The invite of `upstream`, from the bytes `given` (where they came from is `from`)**:
/// the kept one when this downstream enrolled already (`given` must then be that same
/// invite, else 409 by name), else `given` verified once and kept. `Ok(None)`: nothing given
/// and nothing kept — not invited yet.
pub fn enrol_with(
data_root: &Path,
me: &str,
own_key: &str,
upstream: &SyncPeer,
given: Option<&[u8]>,
from: &str,
now: i64,
) -> Result<Option<DownstreamInvite>, SyncFailure> {
let dir = data_root.join(STATE_DIR);
let kept = enrolled_file(&dir, &upstream.name);
let trusted = [upstream.trusted()];
if let Ok(k) = std::fs::read(&kept) {
if given.is_some_and(|g| g != k.as_slice()) {
return Err(SyncFailure::new(
&holger_errcode::SYNC_DOWNSTREAM_REFUSED,
409,
format!("this downstream enrolled with upstream '{}' once ({}); the invite {from} is another one — remove the kept invite by hand to enrol again", upstream.name, kept.display()),
));
}
// Enrolled: the signature, the names and the key still hold; the expiry was the enrolment's.
let s = Signed::from_bytes(&k)?;
let issued: DownstreamInvite = s.open(holger_sync::Kind::Invite, &trusted)?;
return Ok(Some(pin_its_endpoint(open_invite(
&k,
&trusted,
me,
own_key,
issued.issued_at,
)?)));
}
let Some(given) = given else { return Ok(None) };
let invite = open_invite(given, &trusted, me, own_key, now)?;
std::fs::create_dir_all(&dir)
.map_err(|e| SyncError::Io(format!("create {}: {e}", dir.display())))?;
// 0600: an invite of schema v2 holds the link's pre-shared key (lane WG-PSK).
use std::os::unix::fs::OpenOptionsExt as _;
let mut f = std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.mode(0o600)
.open(&kept)
.map_err(|e| SyncError::Io(format!("keep {}: {e}", kept.display())))?;
use std::io::Write as _;
f.write_all(given)
.and_then(|()| f.sync_all())
.map_err(|e| SyncError::Io(format!("keep {}: {e}", kept.display())))?;
Ok(Some(pin_its_endpoint(invite)))
}
/// HOL-EGRESS-WG: a WireGuard invite this downstream took (verified, given or kept) pins its
/// endpoint as the line to its upstream's door ([`crate::egress::pin_invite_endpoint`]).
fn pin_its_endpoint(invite: DownstreamInvite) -> DownstreamInvite {
if !invite.is_direct() {
crate::egress::pin_invite_endpoint(&invite.door, &invite.endpoint);
}
invite
}
/// The invite at `path` (or the kept one) opened under `upstream`'s key alone, to tell a
/// direct invite from a WireGuard one before either is verified for this downstream.
fn peek(data_root: &Path, upstream: &SyncPeer, path: &Path) -> Option<DownstreamInvite> {
let kept = enrolled_file(&data_root.join(STATE_DIR), &upstream.name);
let bytes = std::fs::read(&kept).or_else(|_| std::fs::read(path)).ok()?;
let s = Signed::from_bytes(&bytes).ok()?;
s.open(holger_sync::Kind::Invite, &[upstream.trusted()])
.ok()
}
/// Is the invite `upstream` names (or the one this downstream kept) a direct one?
pub fn is_direct_at(data_root: &Path, upstream: &SyncPeer, path: &Path) -> bool {
peek(data_root, upstream, path).is_some_and(|i| i.is_direct())
}
/// ★ **The start's direct invites** (HOL-PRODUCTS-7): every upstream whose `invite:` is a
/// direct one is verified for this downstream's sync key `own_key` and kept once. Nothing here
/// fails the start: a refused or missing invite is a line said by name, and the box serves on
/// locally. Returns the lines.
pub fn start_direct(
data_root: &Path,
me: &str,
own_key: &str,
upstreams: &[SyncPeer],
) -> Vec<String> {
let mut said = Vec::new();
for u in upstreams.iter().filter(|u| u.invite.is_some()) {
let path = PathBuf::from(u.invite.clone().unwrap_or_default());
let given = std::fs::read(&path).ok();
let kept = enrolled_file(&data_root.join(STATE_DIR), &u.name);
// A WireGuard invite is the tunnel's; an unreadable or untrusted one is refused below.
if let Some(i) = peek(data_root, u, &path) {
if !i.is_direct() {
if cfg!(not(feature = "downstream-wg")) {
said.push(format!("sync: upstream '{}': REFUSED its invite is for a WireGuard link and this build has no tunnel (build with `downstream-wg`)", u.name));
}
continue;
}
}
if given.is_none() && !kept.exists() {
said.push(format!("sync: upstream '{}': its invite {} is not here yet (nor kept): ask its admin for one (`holger-cli downstream invite --name {me}`)", u.name, path.display()));
continue;
}
let was_kept = kept.exists();
match enrol_with(
data_root,
me,
own_key,
u,
given.as_deref(),
&format!("at {}", path.display()),
holger_sync::now_secs(),
) {
Ok(Some(i)) if i.is_direct() => said.push(format!(
"sync: upstream '{}' invited this downstream directly: its door {} ({} invite)",
u.name,
i.door,
if was_kept { "the kept" } else { "the given" }
)),
Ok(Some(_)) => said.push(format!(
"sync: upstream '{}': REFUSED the invite is for a WireGuard link, not a direct one",
u.name
)),
Ok(None) => said.push(format!("sync: upstream '{}': no invite", u.name)),
Err(e) => said.push(format!(
"sync: upstream '{}': REFUSED {}",
u.name,
e.tagged()
)),
}
}
said
}
/// **Where this downstream reaches `upstream`**: its `url:`, else the door of the direct
/// invite it kept (verified again under the upstream's key, for this downstream and `own_key`).
pub fn door_of(data_root: &Path, me: &str, own_key: &str, upstream: &SyncPeer) -> Option<String> {
if let Some(u) = &upstream.url {
return Some(u.clone());
}
let kept = std::fs::read(enrolled_file(&data_root.join(STATE_DIR), &upstream.name)).ok()?;
let trusted = [upstream.trusted()];
let s = Signed::from_bytes(&kept).ok()?;
let issued: DownstreamInvite = s.open(holger_sync::Kind::Invite, &trusted).ok()?;
let i = open_invite(&kept, &trusted, me, own_key, issued.issued_at).ok()?;
i.is_direct().then_some(i.door)
}