crates/gunnar-server/src/tunnel_addr.rs
//! **Where the WireGuard tunnel is, for the listeners that follow it** (owner's
//! ruling 2026-09-22, amended 2026-09-24): the mutual replication listener
//! (`:50052`) answers on the outer address (eth1) for the process lifetime and,
//! once the tunnel is up, ALSO on `wg0`'s address ([`replication_extra`]).
//! `:50051` does not move.
//!
//! One value, published by whoever raises the tunnel — [`crate::wg`] at start,
//! and again when `Bootstrap.AssignTunnelAddress` raises it at RUNTIME — and
//! watched by the replication listener, which rebinds without a restart. Kept
//! outside `wg` on purpose: this module has no WireGuard in it, so a build
//! without `wg-client` still compiles the listener's rule (and its answer is
//! always "no tunnel").
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::sync::OnceLock;
use tokio::sync::watch;
fn channel() -> &'static watch::Sender<Option<Ipv4Addr>> {
static TX: OnceLock<watch::Sender<Option<Ipv4Addr>>> = OnceLock::new();
TX.get_or_init(|| watch::channel(None).0)
}
/// The tunnel came up at `address` (or went away: `None`).
pub fn publish(address: Option<Ipv4Addr>) {
channel().send_replace(address);
}
/// The tunnel address now, if there is a tunnel.
pub fn current() -> Option<Ipv4Addr> {
*channel().borrow()
}
/// A receiver that wakes when the tunnel address changes.
pub fn subscribe() -> watch::Receiver<Option<Ipv4Addr>> {
channel().subscribe()
}
/// **`--wg-only`, process-wide** (owner, 2026-10-04): set once at start, read
/// by the pairing — which address of the primary a twin joins.
static WG_ONLY: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
/// Set the posture (at start).
pub fn set_wg_only(on: bool) {
WG_ONLY.store(on, std::sync::atomic::Ordering::SeqCst);
}
/// Is this box WireGuard-only?
pub fn wg_only() -> bool {
WG_ONLY.load(std::sync::atomic::Ordering::SeqCst)
}
/// **Is a box with this pairing seed WireGuard-only?** `Some(true)`: a seed
/// whose tunnel is WireGuard — an estate box, every box-to-box connection over
/// the overlay. `Some(false)` (another tunnel kind) or `None` (no seed: a
/// standalone appliance): only if `--wg-only` says so.
pub fn estate_is_wg_only(seed_is_wireguard: Option<bool>) -> bool {
seed_is_wireguard == Some(true)
}
/// **The primary's address a twin joins** (`<it>:50051`): its utility
/// address, as it always was — or, `wg_only`, its OVERLAY address and nothing
/// else (a plain-IP connection to the primary is refused there).
pub fn primary_control(
utility: Option<Ipv4Addr>,
overlay: Option<Ipv4Addr>,
wg_only: bool,
) -> Option<Ipv4Addr> {
if wg_only {
overlay
} else {
utility.or(overlay)
}
}
/// **A fed `host:port` of a box, over the tunnel**: the installer (shared
/// with holger, whose boxes are not WireGuard-only) feeds a box's UTILITY
/// address — `recharge_from` = the twin's control plane. A WireGuard-only box
/// dials the same box's OVERLAY address: `pairs` is (utility, overlay) of
/// every fed peer. An address no peer holds is left as it is.
pub fn over_the_tunnel(at: &str, pairs: &[(Ipv4Addr, Ipv4Addr)]) -> String {
let Ok(sa) = at.parse::<SocketAddr>() else {
return at.to_string();
};
let IpAddr::V4(ip) = sa.ip() else {
return at.to_string();
};
match pairs.iter().find(|(u, _)| *u == ip) {
Some((_, o)) => SocketAddr::new(IpAddr::V4(*o), sa.port()).to_string(),
None => at.to_string(),
}
}
/// **The ONE rule for the replication listener's SECOND door** (2026-09-24).
/// The listener on `derived` (the utility address, eth1's IP, port + 1) is
/// bound for the process lifetime and never moves: a tunnel that is raised but
/// has no handshake — or goes down later — must never take away the path that
/// works. With a tunnel, a second listener answers on the tunnel address on
/// the SAME port. `None` when there is nothing to add: no tunnel; an
/// `explicit` `--grpc-replication-bind` (the operator's word, never widened);
/// or a `derived` bind that already covers the tunnel (unspecified, or the
/// tunnel address itself).
///
/// Until 2026-09-24 the rule MOVED the listener onto the tunnel the moment
/// wg0 was raised — before any handshake — and the twin was certified with
/// that host, so a tunnel that never came up left the twin nothing to follow.
pub fn replication_extra(
derived: SocketAddr,
explicit: bool,
tunnel: Option<Ipv4Addr>,
) -> Option<SocketAddr> {
let t = tunnel?;
if explicit || derived.ip().is_unspecified() || derived.ip() == IpAddr::V4(t) {
return None;
}
Some(SocketAddr::new(IpAddr::V4(t), derived.port()))
}
#[cfg(test)]
mod tests {
use super::*;
fn eth1() -> SocketAddr {
"10.0.3.15:50052".parse().unwrap()
}
/// With a tunnel address, a SECOND door on wg0's address, same port —
/// the utility one stays (the caller never unbinds it).
#[test]
fn with_a_tunnel_address_a_second_door_opens_on_wg0() {
let t: Ipv4Addr = "10.77.0.2".parse().unwrap();
assert_eq!(
replication_extra(eth1(), false, Some(t)),
Some("10.77.0.2:50052".parse().unwrap())
);
}
/// With none, only the utility door.
#[test]
fn without_a_tunnel_there_is_only_the_utility_door() {
assert_eq!(replication_extra(eth1(), false, None), None);
}
/// The operator's explicit bind is never widened; an unspecified bind
/// already answers on wg0.
#[test]
fn an_explicit_or_unspecified_bind_gets_no_second_door() {
let t = Some("10.77.0.2".parse().unwrap());
assert_eq!(replication_extra(eth1(), true, t), None);
assert_eq!(
replication_extra("0.0.0.0:50052".parse().unwrap(), false, t),
None
);
assert_eq!(
replication_extra("10.77.0.2:50052".parse().unwrap(), false, t),
None
);
}
/// A recharged main is fed the twin's UTILITY address to restore from; a
/// WireGuard-only box dials the twin's overlay address instead.
#[test]
fn a_fed_utility_address_is_dialled_over_the_tunnel() {
let pairs = [
(Ipv4Addr::new(10, 13, 8, 3), Ipv4Addr::new(10, 44, 0, 3)),
(Ipv4Addr::new(10, 13, 8, 2), Ipv4Addr::new(10, 44, 0, 2)),
];
assert_eq!(
over_the_tunnel("10.13.8.3:50051", &pairs),
"10.44.0.3:50051"
);
assert_eq!(
over_the_tunnel("10.9.9.9:50051", &pairs),
"10.9.9.9:50051",
"nobody's: as fed"
);
assert_eq!(
over_the_tunnel("twin.example:50051", &pairs),
"twin.example:50051"
);
}
/// An estate box (a WireGuard pairing seed) is WireGuard-only; a box with
/// no seed — a standalone appliance — is not, unless told.
#[test]
fn an_estate_box_is_wg_only_and_a_standalone_appliance_is_not() {
assert!(estate_is_wg_only(Some(true)));
assert!(!estate_is_wg_only(Some(false)));
assert!(!estate_is_wg_only(None));
}
/// **WireGuard-only**: a twin joins the primary's overlay address, never its
/// utility one; otherwise the utility address, as before.
#[test]
fn under_wg_only_a_twin_joins_the_primarys_overlay_address() {
let (u, o) = (
Some(Ipv4Addr::new(10, 13, 8, 2)),
Some(Ipv4Addr::new(10, 44, 0, 2)),
);
assert_eq!(primary_control(u, o, true), o);
assert_eq!(
primary_control(u, None, true),
None,
"no overlay: nothing to join, never the plain IP"
);
assert_eq!(primary_control(u, o, false), u);
assert_eq!(primary_control(None, o, false), o);
}
/// A runtime publish wakes a watcher with the new address (what the
/// listener's rebind waits on).
#[tokio::test]
async fn a_runtime_publish_wakes_the_watcher() {
let mut rx = subscribe();
let before = current();
let t: Ipv4Addr = "10.77.0.9".parse().unwrap();
publish(Some(t));
rx.changed().await.expect("the watcher is woken");
assert_eq!(*rx.borrow(), Some(t));
assert_eq!(current(), Some(t));
publish(before);
}
}