use std::path::Path;
use std::sync::{Mutex, OnceLock, RwLock};
use nordisk_pairing::feedbus::{self, Feed};
pub const FEED_SMTP_URL: &str = "secret.smtp_url";
pub const FEED_EGRESS: &str = nordisk_pairing::egress::FEED_EGRESS;
pub const FEED_SMTP_FROM: &str = "secret.smtp_from";
pub const FEED_SMTP_CA: &str = "secret.smtp_ca";
pub const FEED_MONETIZE: &str = "monetize_pubkey";
pub const FEED_CLOCK_KEY: &str = "clock_key";
pub const FEED_CLOCK_PEER: &str = "clock_peer";
pub const FEED_SSO_PRIVATE_IDP_CIDRS: &str = "sso_private_idp_cidrs";
pub const FEED_SSO_EGRESS: &str = "sso_egress";
pub const FEED_SSO_DNS_RESOLVER: &str = "sso_dns_resolver";
pub const FEED_SELLS_ROOM: &str = "sells_room";
pub const FEED_FLEET: &str = "fleet";
pub const FEED_FLEET_INSTANCE: &str = "fleet_instance";
pub const FEED_FLEET_AT: &str = "fleet_at";
pub const FEED_FLEET_CA: &str = "fleet_ca";
pub const FEED_BILLING_URL: &str = "refusal_link";
pub const FEED_REPAIR_AUTO: &str = "repair_auto";
pub const FEED_REPAIR_KEY: &str = "secret.repair_key";
pub const FEED_MISMATCH: &str = "pairing.feed_mismatch";
pub const FEED_NOT_APPLIED: &str = "pairing.feed_not_applied";
pub fn offered_by<'a>(
feed: &'a std::collections::BTreeMap<String, String>,
vm: &str,
key: &str,
) -> Option<&'a str> {
feed.get(&format!("offer.{vm}.{key}"))
.map(|v| v.trim())
.filter(|v| !v.is_empty())
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Fed {
pub monetize_pubkey: Option<String>,
pub clock_key: Option<String>,
pub clock_peer: Option<String>,
pub sso_private_idp_cidrs: Option<String>,
pub sso_egress: bool,
pub sso_dns_resolver: Option<String>,
pub sells_room: bool,
pub fleet: Option<String>,
pub fleet_instance: Option<String>,
pub fleet_at: Option<String>,
pub fleet_ca: Option<String>,
pub smtp_url: Option<Withheld>,
pub smtp_from: Option<String>,
pub smtp_ca: Option<String>,
pub billing_url: Option<String>,
pub repair_auto: bool,
pub repair_key: Option<Withheld>,
pub egress: Option<String>,
}
#[derive(Clone, Default, PartialEq, Eq)]
pub struct Withheld(pub String);
impl std::fmt::Debug for Withheld {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("<withheld>")
}
}
impl Fed {
pub fn from_feed(feed: &std::collections::BTreeMap<String, String>) -> Fed {
let get = |k: &str| {
feed.get(k)
.map(|v| v.trim().to_string())
.filter(|v| !v.is_empty())
.or_else(|| nordisk_pairing::feedbus::offered(feed, k))
};
Fed {
monetize_pubkey: get(FEED_MONETIZE),
clock_key: get(FEED_CLOCK_KEY),
clock_peer: get(FEED_CLOCK_PEER),
sso_private_idp_cidrs: get(FEED_SSO_PRIVATE_IDP_CIDRS),
sso_egress: get(FEED_SSO_EGRESS).as_deref() == Some("on"),
sso_dns_resolver: get(FEED_SSO_DNS_RESOLVER),
sells_room: get(FEED_SELLS_ROOM).as_deref() == Some("true"),
fleet: get(FEED_FLEET),
fleet_instance: get(FEED_FLEET_INSTANCE),
fleet_at: get(FEED_FLEET_AT),
fleet_ca: get(FEED_FLEET_CA),
smtp_url: get(FEED_SMTP_URL).map(Withheld),
smtp_from: get(FEED_SMTP_FROM),
smtp_ca: get(FEED_SMTP_CA),
billing_url: get(FEED_BILLING_URL),
repair_auto: get(FEED_REPAIR_AUTO).as_deref() == Some("true"),
repair_key: get(FEED_REPAIR_KEY).map(Withheld),
egress: get(FEED_EGRESS).filter(|d| d.starts_with("http://")),
}
}
pub fn is_empty(&self) -> bool {
self == &Fed::default()
}
pub fn read(root: &Path) -> anyhow::Result<Option<Fed>> {
Ok(kept_feed(root)?.as_ref().map(Fed::from_feed))
}
pub fn current() -> Option<Fed> {
feedbus::current().map(|feed| Fed::from_feed(&feed))
}
}
pub fn take_egress(feed: &std::collections::BTreeMap<String, String>) {
if let Some(door) = Fed::from_feed(feed).egress {
if nordisk_pairing::egress::proxy().is_none() {
tracing::info!(target: "gunnar::audit", event = "egress_door", door = %door, "this box leaves through its front's egress door (no public IPv4 of its own)");
}
nordisk_pairing::egress::set_proxy(&door);
}
}
#[cfg(any(feature = "mirror-http", feature = "sso-egress"))]
pub fn ureq_proxy() -> Option<ureq::Proxy> {
nordisk_pairing::egress::proxy().and_then(|d| ureq::Proxy::new(d).ok())
}
#[cfg(feature = "wg-client")]
pub fn kept_feed(root: &Path) -> anyhow::Result<Option<Feed>> {
let p = root.join(crate::pairing::JOINED_REL);
let Some(text) = crate::fed_at_rest::read_checked(root, crate::pairing::JOINED_REL)? else {
return Ok(None);
};
let reply = nordisk_pairing::join::JoinReply::parse(&text)
.map_err(|e| anyhow::anyhow!("{}: {e}", p.display()))?;
Ok(Some(reply.feed))
}
#[cfg(not(feature = "wg-client"))]
pub fn kept_feed(_root: &Path) -> anyhow::Result<Option<Feed>> {
Ok(None)
}
pub fn sso_private_nets(fed: Option<&Fed>) -> Result<gunnar_grpc::sso::PrivateNets, String> {
match fed.and_then(|f| f.sso_private_idp_cidrs.as_deref()) {
Some(text) => gunnar_grpc::sso::PrivateNets::parse(text)
.map_err(|why| format!("`{FEED_SSO_PRIVATE_IDP_CIDRS}`: {why}")),
None => Ok(gunnar_grpc::sso::PrivateNets::default()),
}
}
pub fn sso_egress_on(fed: Option<&Fed>, env: Option<&str>) -> bool {
fed.is_some_and(|f| f.sso_egress) || env.is_some_and(|v| v.trim() == "on")
}
pub fn sso_dns_resolver(fed: Option<&Fed>, env: Option<&str>) -> Option<String> {
fed.and_then(|f| f.sso_dns_resolver.clone()).or_else(|| {
env.map(str::trim)
.filter(|v| !v.is_empty())
.map(str::to_owned)
})
}
pub fn load_at_start(root: &Path) -> Option<Fed> {
match kept_feed(root) {
Ok(Some(feed)) => {
let fed = Fed::from_feed(&feed);
if !fed.is_empty() {
tracing::info!(monetize = fed.monetize_pubkey.is_some(), clock_peer = ?fed.clock_peer, clock_key = fed.clock_key.is_some(), "feed: the installer's values from {}", "<data>/pairing/joined.toml");
}
feedbus::publish(feed);
Some(fed)
}
Ok(None) => None,
Err(e) => {
tracing::error!(error = %format!("{e:#}"), "feed: the persisted join reply is unreadable — this start uses the medium's values only");
None
}
}
}
fn in_force_slot() -> &'static RwLock<Option<Option<[u8; 32]>>> {
static IN_FORCE: OnceLock<RwLock<Option<Option<[u8; 32]>>>> = OnceLock::new();
IN_FORCE.get_or_init(|| RwLock::new(None))
}
pub fn record_monetize_in_force(key: Option<[u8; 32]>) {
if let Ok(mut s) = in_force_slot().write() {
*s = Some(key);
}
}
pub fn monetize_in_force() -> Option<[u8; 32]> {
in_force_slot().read().ok().and_then(|s| s.flatten())
}
pub type MonetizeTaker = Box<dyn Fn(&[u8; 32]) -> Result<(), String> + Send + Sync>;
fn takers() -> &'static Mutex<Vec<(&'static str, MonetizeTaker)>> {
static TAKERS: OnceLock<Mutex<Vec<(&'static str, MonetizeTaker)>>> = OnceLock::new();
TAKERS.get_or_init(|| Mutex::new(Vec::new()))
}
pub fn on_monetize_key(name: &'static str, take: MonetizeTaker) {
if let Ok(mut t) = takers().lock() {
t.push((name, take));
}
}
fn hex(k: &[u8; 32]) -> String {
k.iter().map(|b| format!("{b:02x}")).collect()
}
pub fn resolve_monetize(
medium: Option<[u8; 32]>,
fed: Option<&str>,
) -> Result<Option<[u8; 32]>, String> {
let fed = match fed {
None => return Ok(medium),
Some(text) => gunnar_common::monetize_key::parse_hex32(text.trim()).map_err(|why| {
format!("REFUSED [pairing-feed-monetize-malformed] the installer fed `{FEED_MONETIZE}` = `{text}`, which is not a monetize key: {why}")
})?,
};
match medium {
None => Ok(Some(fed)),
Some(m) if m == fed => Ok(Some(m)),
Some(m) => Err(format!(
"REFUSED [{FEED_MISMATCH}] the medium trusts monetize key {} and the installer's door fed {} — \
this box will not pick one silently. Compose the medium without `monetize_pubkey` (the door \
feeds it), or with the front's live key",
hex(&m),
hex(&fed)
)),
}
}
#[cfg(feature = "metered")]
const NO_TAKER_WHY: &str =
"the metered start opened no entitlement slot (no --billing-url on a `fed` medium)";
#[cfg(not(feature = "metered"))]
const NO_TAKER_WHY: &str =
"this build carries no entitlement module, so no start opens a slot to take it";
pub fn monetize_arrived(fed: Option<&str>) -> Option<MonetizeArrival> {
let in_force = in_force_slot().read().ok().and_then(|s| *s)?;
let fed = fed?;
match resolve_monetize(in_force, Some(fed)) {
Ok(Some(k)) if Some(k) == in_force => None,
Ok(Some(k)) => {
let takers = takers().lock().ok()?;
if takers.is_empty() {
let detail = format!(
"the installer fed monetize key {} and nothing on this box can enforce it: {NO_TAKER_WHY}",
hex(&k)
);
raise(FEED_NOT_APPLIED, &detail);
return Some(MonetizeArrival::NotApplied(detail));
}
let failed: Vec<String> = takers
.iter()
.filter_map(|(name, take)| take(&k).err().map(|e| format!("{name}: {e}")))
.collect();
if !failed.is_empty() {
let detail = format!(
"the installer fed monetize key {}, and it could not be applied: {}",
hex(&k),
failed.join("; ")
);
raise(FEED_NOT_APPLIED, &detail);
return Some(MonetizeArrival::NotApplied(detail));
}
record_monetize_in_force(Some(k));
let detail = format!("the installer fed monetize key {}; it is IN FORCE now (applied in place, no restart)", hex(&k));
tracing::info!(event = "pairing.feed_applied", "feed: {detail}");
Some(MonetizeArrival::Applied(detail))
}
Ok(None) => None,
Err(why) => {
raise(FEED_MISMATCH, &why);
Some(MonetizeArrival::Mismatch(why))
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MonetizeArrival {
Applied(String),
NotApplied(String),
Mismatch(String),
}
pub use estate_clock::offer::resolve_clock;
pub fn clock_config() -> Result<estate_clock::config::Config, String> {
use estate_clock::config::{var, Config, Role, CLOCK_ENV, CLOCK_KEY_ENV};
let env_clock = var(CLOCK_ENV);
let env_key = var(CLOCK_KEY_ENV);
let fed = Fed::current().unwrap_or_default();
let (clock, key) = resolve_clock(
env_clock.as_deref(),
env_key.as_deref(),
fed.clock_peer.as_deref(),
fed.clock_key.as_deref(),
)?;
Config::parse(clock.as_deref(), key.as_deref(), Role::Appliance)
}
pub fn clock_fed() -> bool {
Fed::current().is_some_and(|f| f.clock_peer.is_some())
}
fn alarm_slot() -> &'static Mutex<Vec<gunnar_proto::HealthAlarm>> {
static ALARMS: OnceLock<Mutex<Vec<gunnar_proto::HealthAlarm>>> = OnceLock::new();
ALARMS.get_or_init(|| Mutex::new(Vec::new()))
}
pub fn raise(event: &str, detail: &str) {
tracing::error!(event, "feed: {detail}");
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as i64)
.unwrap_or(0);
if let Ok(mut alarms) = alarm_slot().lock() {
match alarms.iter_mut().find(|a| a.event == event) {
Some(a) => {
a.count += 1;
a.detail = detail.to_string();
}
None => alarms.push(gunnar_proto::HealthAlarm {
event: event.into(),
count: 1,
since_unix_ms: now,
detail: detail.to_string(),
}),
}
}
}
pub fn alarms() -> Vec<gunnar_proto::HealthAlarm> {
alarm_slot().lock().map(|a| a.clone()).unwrap_or_default()
}
pub fn seam_refusal(at: Option<&str>, ca: bool) -> Option<String> {
const WORD: &str = "fleet-seam-bench-only";
match at {
None if ca => Some(format!(
"REFUSED [{WORD}] the installer fed `{}` with no `{}`: a door's own CA is a bench's, beside a bench address only — a public door is checked under the public roots",
crate::feed::FEED_FLEET_CA,
crate::feed::FEED_FLEET_AT
)),
None => None,
Some(at) => match at.trim().parse::<std::net::SocketAddr>() {
Ok(a) if bench_address(a.ip()) => None,
_ => Some(format!(
"REFUSED [{WORD}] the installer fed `{}` = {at}: the pull channel is dialled elsewhere than its door's name only on a bench (a private, loopback or link-local `ip:port`)",
crate::feed::FEED_FLEET_AT
)),
},
}
}
fn bench_address(ip: std::net::IpAddr) -> bool {
match ip {
std::net::IpAddr::V4(v) => {
v.is_private()
|| v.is_loopback()
|| v.is_link_local()
|| (v.octets()[0] == 100 && (v.octets()[1] & 0xc0) == 64)
}
std::net::IpAddr::V6(v) => {
v.is_loopback()
|| (v.segments()[0] & 0xfe00) == 0xfc00
|| (v.segments()[0] & 0xffc0) == 0xfe80
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_fed_relay_is_read_and_its_url_withheld() {
let feed: std::collections::BTreeMap<String, String> = [
(FEED_SMTP_URL, "smtp://u:TOKENvalue@smtp.example:587"),
(FEED_SMTP_FROM, "u@smtp.example"),
]
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
let f = Fed::from_feed(&feed);
assert_eq!(
f.smtp_url,
Some(Withheld("smtp://u:TOKENvalue@smtp.example:587".into()))
);
assert_eq!(f.smtp_from.as_deref(), Some("u@smtp.example"));
assert!(!format!("{f:?}").contains("TOKENvalue"), "{f:?}");
assert!(Fed::from_feed(&Default::default()).smtp_url.is_none());
}
#[test]
fn the_fed_egress_door_is_read_and_only_an_http_one() {
let feed = |pairs: &[(&str, &str)]| {
pairs
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect::<std::collections::BTreeMap<_, _>>()
};
assert_eq!(FEED_EGRESS, nordisk_pairing::egress::FEED_EGRESS);
assert_eq!(
Fed::from_feed(&feed(&[(FEED_EGRESS, " http://10.44.0.1:3128 ")]))
.egress
.as_deref(),
Some("http://10.44.0.1:3128")
);
assert_eq!(
Fed::from_feed(&feed(&[(FEED_EGRESS, "socks5://10.44.0.1:1080")])).egress,
None,
"only an http:// door is one"
);
assert_eq!(
Fed::from_feed(&feed(&[])).egress,
None,
"a box with its own way out is fed none"
);
}
const A: &str = "43cb00000000000000000000000000000000000000000000000000000000842a";
const B: &str = "43cb00000000000000000000000000000000000000000000000000000000842b";
fn k(s: &str) -> [u8; 32] {
gunnar_common::monetize_key::parse_hex32(s).unwrap()
}
#[test]
fn a_value_offered_by_a_vm_is_taken_when_the_installer_feeds_none_itself() {
let feed = |pairs: &[(&str, &str)]| {
pairs
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect::<std::collections::BTreeMap<_, _>>()
};
let f = Fed::from_feed(&feed(&[
("offer.front.monetize_pubkey", A),
("offer.front.clock_key", "ssh-ed25519 AAAAk"),
("clock_peer", "peer://10.13.8.1:50123"),
("offer.front.xmonetize_pubkey", B),
]));
assert_eq!(
f,
Fed {
monetize_pubkey: Some(A.into()),
clock_key: Some("ssh-ed25519 AAAAk".into()),
clock_peer: Some("peer://10.13.8.1:50123".into()),
..Fed::default()
}
);
assert_eq!(
Fed::from_feed(&feed(&[
(FEED_MONETIZE, B),
("offer.front.monetize_pubkey", A)
]))
.monetize_pubkey
.as_deref(),
Some(B),
"the plain key wins"
);
assert_eq!(
nordisk_pairing::feedbus::offered(
&feed(&[
("offer..monetize_pubkey", A),
("offer.a.b.monetize_pubkey", A)
]),
FEED_MONETIZE
),
None,
"not a VM name"
);
}
#[test]
fn the_fleet_door_and_the_estate_are_taken_from_the_feed_or_the_fronts_offer() {
let feed = |pairs: &[(&str, &str)]| {
pairs
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect::<std::collections::BTreeMap<_, _>>()
};
let f = Fed::from_feed(&feed(&[
("offer.front.fleet", "https://monetize.ignalina.dk:8443"),
("offer.front.fleet_instance", "acme"),
("offer.front.monetize_pubkey", A),
]));
assert_eq!(
(
f.fleet.as_deref(),
f.fleet_instance.as_deref(),
f.monetize_pubkey.as_deref()
),
(
Some("https://monetize.ignalina.dk:8443"),
Some("acme"),
Some(A)
)
);
let f = Fed::from_feed(&feed(&[
(FEED_FLEET, "https://one:8443"),
("offer.front.fleet", "https://other:8443"),
(FEED_FLEET_INSTANCE, " "),
("offer.front.fleet_instance", "acme"),
]));
assert_eq!(
(f.fleet.as_deref(), f.fleet_instance.as_deref()),
(Some("https://one:8443"), Some("acme")),
"the plain key wins; blank is absent"
);
assert!(Fed::from_feed(&feed(&[])).fleet.is_none());
}
#[test]
fn sells_room_is_fed_as_exactly_true() {
let feed = |pairs: &[(&str, &str)]| {
pairs
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect::<std::collections::BTreeMap<_, _>>()
};
assert!(Fed::from_feed(&feed(&[(FEED_SELLS_ROOM, "true")])).sells_room);
assert!(
Fed::from_feed(&feed(&[(FEED_SELLS_ROOM, " true\n")])).sells_room,
"trimmed, as fed"
);
assert!(!Fed::from_feed(&feed(&[(FEED_SELLS_ROOM, "yes")])).sells_room);
assert!(
!Fed::from_feed(&feed(&[])).sells_room,
"a sold estate is fed nothing"
);
}
#[test]
fn the_sites_pay_page_is_read_from_the_feed() {
let feed = |pairs: &[(&str, &str)]| {
pairs
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect::<std::collections::BTreeMap<_, _>>()
};
assert_eq!(FEED_BILLING_URL, "refusal_link");
assert_eq!(
Fed::from_feed(&feed(&[(
FEED_BILLING_URL,
" https://test2.gunnar.rs/billing/{tenant}\n"
)]))
.billing_url
.as_deref(),
Some("https://test2.gunnar.rs/billing/{tenant}")
);
assert_eq!(
Fed::from_feed(&feed(&[(FEED_BILLING_URL, " ")])).billing_url,
None
);
assert_eq!(Fed::from_feed(&feed(&[])).billing_url, None);
}
#[test]
fn sso_egress_is_off_unless_the_operator_turns_it_on() {
let feed = |pairs: &[(&str, &str)]| {
pairs
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect::<std::collections::BTreeMap<_, _>>()
};
let on = Fed::from_feed(&feed(&[(FEED_SSO_EGRESS, " on ")]));
assert!(on.sso_egress);
assert!(sso_egress_on(Some(&on), None));
for word in ["off", "true", "yes", "ON?", ""] {
let fed = Fed::from_feed(&feed(&[(FEED_SSO_EGRESS, word)]));
assert!(!sso_egress_on(Some(&fed), None), "{word:?} turned it on");
}
assert!(!sso_egress_on(None, None), "nothing said is airgap");
assert!(
sso_egress_on(None, Some("on")),
"a box fed nothing takes the operator's environment"
);
assert!(!sso_egress_on(None, Some("1")));
let off = Fed::from_feed(&feed(&[]));
assert!(
sso_egress_on(Some(&off), Some("on")),
"the environment holds when the feed says nothing"
);
}
#[test]
fn the_domain_proofs_resolver_is_fed_or_the_operators_environment() {
let feed = |pairs: &[(&str, &str)]| {
pairs
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect::<std::collections::BTreeMap<_, _>>()
};
let fed = Fed::from_feed(&feed(&[(
FEED_SSO_DNS_RESOLVER,
"https://doh.example/resolve",
)]));
assert_eq!(
sso_dns_resolver(Some(&fed), None).as_deref(),
Some("https://doh.example/resolve")
);
assert_eq!(
sso_dns_resolver(Some(&fed), Some("http://127.0.0.1:9/resolve")).as_deref(),
Some("https://doh.example/resolve"),
"the feed's resolver wins over the environment"
);
let none = Fed::from_feed(&feed(&[]));
assert_eq!(
sso_dns_resolver(Some(&none), Some(" http://127.0.0.1:9/resolve ")).as_deref(),
Some("http://127.0.0.1:9/resolve")
);
assert_eq!(
sso_dns_resolver(None, Some("http://127.0.0.1:9/resolve")).as_deref(),
Some("http://127.0.0.1:9/resolve"),
"a box fed nothing"
);
assert_eq!(sso_dns_resolver(None, Some(" ")), None, "blank is none");
assert_eq!(
sso_dns_resolver(None, None),
None,
"nothing named is no resolver (RA5-24)"
);
}
#[test]
fn the_operators_idp_networks_are_fed_and_applied_live() {
let feed = |pairs: &[(&str, &str)]| {
pairs
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect::<std::collections::BTreeMap<_, _>>()
};
let fed = Fed::from_feed(&feed(&[(FEED_SSO_PRIVATE_IDP_CIDRS, " 10.0.2.100/32 ")]));
assert_eq!(fed.sso_private_idp_cidrs.as_deref(), Some("10.0.2.100/32"));
assert_eq!(
sso_private_nets(Some(&fed))
.expect("a private network")
.to_string(),
"10.0.2.100/32"
);
assert!(sso_private_nets(None).expect("nothing fed").is_empty());
let public = Fed::from_feed(&feed(&[(FEED_SSO_PRIVATE_IDP_CIDRS, "8.8.8.0/24")]));
assert!(
sso_private_nets(Some(&public)).is_err(),
"a public range is refused"
);
let seen = std::sync::Arc::new(Mutex::new(Vec::<String>::new()));
let into = std::sync::Arc::clone(&seen);
feedbus::on_published("test: the idp networks", move |f: &Feed| {
into.lock().unwrap().push(
Fed::from_feed(f)
.sso_private_idp_cidrs
.clone()
.unwrap_or_default(),
)
});
feedbus::publish(feed(&[(FEED_SSO_PRIVATE_IDP_CIDRS, " 10.0.2.100/32 ")]));
feedbus::publish(feed(&[]));
let got = seen.lock().unwrap().clone();
let at = got
.iter()
.position(|v| v == "10.0.2.100/32")
.expect("the list reached the taker");
assert!(
got[at + 1..].iter().any(String::is_empty),
"every publish reaches the taker: {got:?}"
);
}
#[test]
fn an_offer_is_read_from_the_named_vm_not_the_first_by_name() {
let feed: std::collections::BTreeMap<String, String> = [
("offer.vm2.ticket_pubkey", A),
("offer.vm3.ticket_pubkey", B),
("offer.vm4.ticket_pubkey", " "),
]
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
assert_eq!(
nordisk_pairing::feedbus::offered(&feed, "ticket_pubkey").as_deref(),
Some(A),
"the first by name"
);
assert_eq!(
offered_by(&feed, "vm3", "ticket_pubkey"),
Some(B),
"the primary's own"
);
assert_eq!(
offered_by(&feed, "vm4", "ticket_pubkey"),
None,
"blank is absent"
);
assert_eq!(offered_by(&feed, "vm9", "ticket_pubkey"), None);
}
#[test]
fn monetize_door_alone_medium_alone_and_equal_are_taken() {
assert_eq!(
resolve_monetize(None, Some(A)),
Ok(Some(k(A))),
"one medium, any estate: the door's key"
);
assert_eq!(
resolve_monetize(Some(k(A)), None),
Ok(Some(k(A))),
"no feed: today's behaviour"
);
assert_eq!(resolve_monetize(Some(k(A)), Some(A)), Ok(Some(k(A))));
assert_eq!(
resolve_monetize(None, None),
Ok(None),
"a metered build with no key from either source: today's behaviour"
);
}
#[test]
fn monetize_medium_and_a_different_door_key_is_refused_by_name() {
let why = resolve_monetize(Some(k(A)), Some(B)).unwrap_err();
assert!(
why.contains(&format!("[{FEED_MISMATCH}]")) && why.contains(A) && why.contains(B),
"{why}"
);
let why = resolve_monetize(None, Some("not-a-key")).unwrap_err();
assert!(why.contains("[pairing-feed-monetize-malformed]"), "{why}");
}
#[test]
fn the_feed_table_reads_the_contract_keys_and_blank_is_absent() {
let mut feed = std::collections::BTreeMap::new();
feed.insert(FEED_MONETIZE.to_string(), A.to_string());
feed.insert(
FEED_CLOCK_PEER.to_string(),
"peer://10.13.8.179:50123".to_string(),
);
feed.insert(FEED_CLOCK_KEY.to_string(), " ".to_string());
feed.insert(
"admin_pubkey".to_string(),
"ssh-ed25519 AAAAconsole".to_string(),
);
let fed = Fed::from_feed(&feed);
assert_eq!(
fed,
Fed {
monetize_pubkey: Some(A.into()),
clock_key: None,
clock_peer: Some("peer://10.13.8.179:50123".into()),
..Fed::default()
}
);
}
#[cfg(feature = "wg-client")]
#[test]
fn a_persisted_join_reply_is_read_back_and_absent_is_none() {
let dir = std::env::temp_dir().join(format!("gunnar-feed-{}", std::process::id()));
let _ = std::fs::create_dir_all(dir.join("pairing"));
assert_eq!(Fed::read(&dir.join("nowhere")).unwrap(), None);
let mut feed = std::collections::BTreeMap::new();
feed.insert(FEED_MONETIZE.to_string(), A.to_string());
feed.insert(FEED_CLOCK_KEY.to_string(), "ssh-ed25519 AAAAk".to_string());
let reply = nordisk_pairing::join::JoinReply {
estate: "gunnar.rs".into(),
me: nordisk_pairing::join::Assigned {
name: "main".into(),
overlay: nordisk_pairing::OVERLAY_BOX_A,
},
cert_pem: None,
role: None,
primary_overlay: None,
primary_utility: None,
peers: vec![],
feed,
};
std::fs::write(
dir.join(crate::pairing::JOINED_REL),
reply.render().unwrap(),
)
.unwrap();
let fed = Fed::read(&dir).unwrap().unwrap();
assert_eq!(fed.monetize_pubkey.as_deref(), Some(A));
assert_eq!(fed.clock_key.as_deref(), Some("ssh-ed25519 AAAAk"));
let _ = std::fs::remove_dir_all(&dir);
}
}