crates/wg-appliance-core/src/lib.rs
//! **wg-appliance-core** — the ONE userspace WireGuard implementation in the
//! nordisk suite.
//!
//! # Why userspace, and why this crate exists at all
//!
//! An OS-less appliance is a single statically linked binary and a kernel it did
//! not choose. Kernel WireGuard makes the tunnel a property of **that kernel**:
//! `CONFIG_WIREGUARD` has to be `y` or `m`, and when it is `m` the four `.ko`
//! files have to be staged somewhere, matched to the exact kernel release, and
//! loaded before the link kind exists. A released image cannot carry them —
//! they belong to a kernel the image is packed with at build time on somebody
//! else's box — so a kernel-WireGuard appliance ships with a tunnel that cannot
//! come up until an operator rebuilds it.
//!
//! boringtun links the whole data plane into the binary instead. The only kernel
//! feature needed is `CONFIG_TUN`, which is **built in** on every ordinary
//! distribution kernel (`=y` on the kernel gunnar's ISO packs, measured
//! 2026-08-12), so the tunnel works regardless of what the packed kernel thinks
//! about WireGuard. OS-LESS means everything is in the compiled heap.
//!
//! # What is here, and what is deliberately NOT
//!
//! Here — the parts that would otherwise be written twice:
//!
//! * [`keys`] — base64 ⇄ 32 raw bytes, x25519 public-key derivation, and
//! **keygen** (`wg genkey` without a shell).
//! * [`ini`] — the `wg-quick(8)` INI **tokenizer**: comments, sections,
//! `Key = value`, line numbers. Tokens only.
//! * [`tun`] — `/dev/net/tun` + `TUNSETIFF`, and the MTU.
//! * [`dataplane`] — the boringtun pump: one thread, `poll(2)` over the tun fd
//! and one UDP socket, `encapsulate` out, `decapsulate` in, `update_timers`
//! on a tick, and AllowedIPs longest-prefix routing to pick the peer.
//!
//! * [`ceremony`] — the appliance **identity ceremony**: load-or-mint the
//! keypair at a path (idempotent, fail-closed), the ONE canonical identity
//! line every product prints and every watcher greps, and the hub-file
//! dialect (`hub.conf`: one `[Peer]`, public data only).
//!
//! NOT here, on purpose:
//!
//! * **A config schema.** tunnr's payload accepts `DNS`/`MTU`/IPv6/multiple
//! addresses and infers a server-or-client role; gunnar refuses every
//! wg-quick key it does not implement *by name* and types its addresses as
//! `Ipv4Addr`. Those are two different declared products, not two copies of
//! one — merging them would silently change both. What they genuinely shared
//! was the tokenizer, and the tokenizer is what moved.
//!
//! **Amendment (the ceremony's dialect is not an exception):** `hub.conf` is
//! not gunnar's schema and not tunnr's — it is the ceremony's OWN, single,
//! product-identical dialect (one `[Peer]`, `PublicKey`/`AllowedIPs`/
//! `Endpoint`/`PersistentKeepalive`, everything else refused by name). It
//! lives here precisely BECAUSE it must be byte-identical for holger, gunnar
//! and korp across both the ISO and the container shape — a per-product copy
//! is the drift this crate exists to prevent. The products' full wg-quick
//! surfaces remain their own.
//! * **A route writer.** gunnar has a netlink writer with an acknowledged
//! `NLMSG_ERROR` on every message and one `apply` every address on the
//! machine goes through; that stays gunnar's own declared surface.
//!
//! **Amendment (2026-08-13, the [`addr`] module):** the *ioctl address
//! writer* — `SIOCSIFADDR`/`SIOCSIFNETMASK`/`IFF_UP`, LIFTED from tunnr's
//! `linux.rs` exactly as `keys` was lifted — now lives here, because new
//! consumers (holger first) had NO existing answer and each growing its own
//! would be the third copy this crate exists to prevent. One writer for
//! consumers without one; gunnar keeps its netlink writer by prior ruling;
//! tunnr's fold-in is pending alongside its keys fold-in. Routes BEYOND the
//! connected subnet (netlink `RTM_NEWROUTE`) remain deliberately absent —
//! declare the appliance address with its real site prefix and the kernel's
//! connected route answers the hub without one.
//!
//! # The pump's shape, stated once
//!
//! One OS thread, not a task pool: a WireGuard `Tunn` is a state machine that
//! must be stepped in order, and every packet touches it. Concurrency would buy
//! nothing and cost the ordering. (Suite law: concurrency is `gatling`, never
//! rayon; this crate is single-threaded and pulls neither.)
pub mod addr;
pub mod appliance;
pub mod ceremony;
pub mod dataplane;
/// The hub **test responder** — behind the `hubfixture` feature (default ON).
///
/// It mints a keypair at runtime and binds `0.0.0.0`, which is right for a
/// proof harness and wrong for an appliance image. Until 2026-09-05 it was an
/// unconditional module and the only thing keeping it out of `wg-init` was the
/// linker dropping code nothing called. That is an optimisation, not a
/// posture. A payload compiles it out with `default-features = false`.
#[cfg(feature = "hubfixture")]
pub mod hubfixture;
/// The N-peer RELAY beside the one-peer fixture — the same posture: a proof
/// harness's hub (binds `0.0.0.0`, holds a hub key), compiled out of an image.
#[cfg(feature = "hubfixture")]
pub mod hubrelay;
pub mod ini;
pub mod keys;
/// The podman/docker ceremony robot: a test tool, behind the `robot` feature
/// (not a default), so a runtime build never compiles a process spawner.
#[cfg(any(feature = "robot", test))]
pub mod robot;
pub mod tun;
pub use ceremony::{
identity_line, load_or_mint_identity, minted_line, parse_hub_config, parse_identity_line,
read_hub_config, CeremonyError, HubPeer, Identity, HUB_CONF_FILE, IDENTITY_KEY_FILE,
IDENTITY_LINE_REGEX, IDENTITY_PUB_FILE,
};
pub use dataplane::{PeerSpec, PeerStats, Router, TunnelHandle, TunnelSpec};
#[cfg(feature = "hubfixture")]
pub use hubfixture::{HubFixture, HubVerdict};
pub use keys::{
decode_key, encode_key, generate_keypair, public_from_private, KeyError, KEY_LEN, REDACTED,
};
#[cfg(any(feature = "robot", test))]
pub use robot::{ArmOutcome, ContainerCeremony};
pub use tun::TunDevice;