Add runtime observation and systemd restart adapter
This commit is contained in:
+674
@@ -0,0 +1,674 @@
|
||||
//! Public, read-only runtime observations and pure saved-peer verification.
|
||||
//!
|
||||
//! The transport performs bounded read-only Linux access; comparison remains
|
||||
//! pure and accepts caller-supplied observations.
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use ipnet::IpNet;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
use crate::domain::PublicKey;
|
||||
use crate::wireguard::{ConfigDocument, DocumentIssue, PeerIssue};
|
||||
|
||||
#[allow(dead_code)] // Integrated by the M5 orchestration step.
|
||||
pub(crate) mod netlink;
|
||||
#[allow(dead_code)] // Consumed by the M5 application integration step.
|
||||
pub(crate) mod transport;
|
||||
|
||||
/// A value that could not be observed is distinct from a valid zero or empty value.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum Observed<T> {
|
||||
Available(T),
|
||||
Unavailable,
|
||||
}
|
||||
|
||||
/// Current statistics for a peer, from the server interface's perspective.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct RuntimePeer {
|
||||
pub allowed_ips: Observed<Vec<IpNet>>,
|
||||
/// `Available(None)` means WireGuard has never recorded a handshake.
|
||||
pub last_handshake: Observed<Option<OffsetDateTime>>,
|
||||
/// These are current counters and may reset when the interface restarts.
|
||||
pub received_bytes: Observed<u64>,
|
||||
pub transmitted_bytes: Observed<u64>,
|
||||
}
|
||||
|
||||
/// Whether the peer map is exhaustive enough to establish absence and equality.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum PeerSetCompleteness {
|
||||
Unknown,
|
||||
Unavailable,
|
||||
Partial,
|
||||
Complete,
|
||||
}
|
||||
|
||||
/// Runtime peers keyed by their public key, with duplicate identities retained
|
||||
/// as an explicit ambiguity signal rather than silently overwritten.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct RuntimePeerSet {
|
||||
pub peers: BTreeMap<PublicKey, RuntimePeer>,
|
||||
pub duplicate_public_keys: BTreeSet<PublicKey>,
|
||||
pub completeness: PeerSetCompleteness,
|
||||
}
|
||||
|
||||
impl RuntimePeerSet {
|
||||
/// Collect records while preserving evidence that the adapter returned a
|
||||
/// duplicate public key. The first record remains available for diagnostics.
|
||||
pub fn from_records(
|
||||
completeness: PeerSetCompleteness,
|
||||
records: impl IntoIterator<Item = (PublicKey, RuntimePeer)>,
|
||||
) -> Self {
|
||||
let mut peers = BTreeMap::new();
|
||||
let mut duplicate_public_keys = BTreeSet::new();
|
||||
for (identity, peer) in records {
|
||||
if let std::collections::btree_map::Entry::Vacant(entry) = peers.entry(identity) {
|
||||
entry.insert(peer);
|
||||
} else {
|
||||
duplicate_public_keys.insert(identity);
|
||||
}
|
||||
}
|
||||
Self {
|
||||
peers,
|
||||
duplicate_public_keys,
|
||||
completeness,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Interface presence and peer observation are independent facts.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum InterfaceObservation {
|
||||
Unknown,
|
||||
Unavailable,
|
||||
Missing,
|
||||
Present {
|
||||
/// Interface returned by the runtime adapter.
|
||||
name: String,
|
||||
peers: RuntimePeerSet,
|
||||
},
|
||||
}
|
||||
|
||||
/// One read-only sample. A missing timestamp means the sample time is unknown.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct RuntimeObservation {
|
||||
pub observed_at: Option<OffsetDateTime>,
|
||||
pub interface: InterfaceObservation,
|
||||
}
|
||||
|
||||
impl RuntimeObservation {
|
||||
pub const fn unknown() -> Self {
|
||||
Self {
|
||||
observed_at: None,
|
||||
interface: InterfaceObservation::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One saved peer's complete technical routes, independent of alias metadata
|
||||
/// and the narrower `domain::Client` address representation.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct SavedPeer {
|
||||
pub identity: PublicKey,
|
||||
pub allowed_ips: BTreeSet<IpNet>,
|
||||
}
|
||||
|
||||
/// Technical peer projection from the authoritative byte-preserving document.
|
||||
/// Invalid or ambiguous records remain represented as unverifiable entries.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct SavedPeerProjection {
|
||||
pub peers: BTreeMap<PublicKey, SavedPeer>,
|
||||
pub duplicate_public_keys: BTreeSet<PublicKey>,
|
||||
pub unverifiable_peer_indices: BTreeSet<usize>,
|
||||
pub document_unverifiable: bool,
|
||||
}
|
||||
|
||||
impl SavedPeerProjection {
|
||||
/// Project every `[Peer]` section, retaining valid but unsupported route
|
||||
/// shapes that cannot fit the UI's ordinary client address model.
|
||||
pub fn from_document(document: &ConfigDocument) -> Self {
|
||||
let mut peers = BTreeMap::new();
|
||||
let mut duplicate_public_keys = BTreeSet::new();
|
||||
let mut unverifiable_peer_indices = BTreeSet::new();
|
||||
|
||||
for peer in document.peers() {
|
||||
let Some(identity) = peer.identity else {
|
||||
unverifiable_peer_indices.insert(peer.index);
|
||||
continue;
|
||||
};
|
||||
let invalid_route = peer
|
||||
.issues
|
||||
.iter()
|
||||
.any(|issue| matches!(issue, PeerIssue::MalformedAllowedIp { .. }));
|
||||
let duplicate_key_field = peer.issues.iter().any(|issue| {
|
||||
matches!(
|
||||
issue,
|
||||
PeerIssue::MalformedPublicKey { .. }
|
||||
| PeerIssue::DuplicatePublicKeyField { .. }
|
||||
| PeerIssue::DuplicatePeerIdentity { .. }
|
||||
)
|
||||
});
|
||||
if invalid_route || duplicate_key_field {
|
||||
unverifiable_peer_indices.insert(peer.index);
|
||||
}
|
||||
|
||||
let routes = peer
|
||||
.allowed_ips
|
||||
.iter()
|
||||
.map(|network| network.trunc())
|
||||
.collect();
|
||||
if peers
|
||||
.insert(
|
||||
identity,
|
||||
SavedPeer {
|
||||
identity,
|
||||
allowed_ips: routes,
|
||||
},
|
||||
)
|
||||
.is_some()
|
||||
{
|
||||
duplicate_public_keys.insert(identity);
|
||||
unverifiable_peer_indices.insert(peer.index);
|
||||
}
|
||||
}
|
||||
|
||||
let document_unverifiable = document
|
||||
.issues()
|
||||
.iter()
|
||||
.any(|issue| !matches!(issue, DocumentIssue::ManagedMetadata(_)));
|
||||
Self {
|
||||
peers,
|
||||
duplicate_public_keys,
|
||||
unverifiable_peer_indices,
|
||||
document_unverifiable,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A technical discrepancy or a reason verification could not safely decide.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum ComparisonFinding {
|
||||
InterfaceUnknown,
|
||||
InterfaceUnavailable,
|
||||
InterfaceMissing,
|
||||
InterfaceMismatch { expected: String, observed: String },
|
||||
PeerSetUnknown,
|
||||
PeerSetUnavailable,
|
||||
PeerSetPartial,
|
||||
SavedDocumentUnverifiable,
|
||||
SavedPeerUnverifiable { peer_index: usize },
|
||||
DuplicateSavedPublicKey { identity: PublicKey },
|
||||
DuplicateRuntimePublicKey { identity: PublicKey },
|
||||
MissingRuntimePeer { identity: PublicKey },
|
||||
ExtraRuntimePeer { identity: PublicKey },
|
||||
RuntimeRoutesUnavailable { identity: PublicKey },
|
||||
AllowedIpsMismatch { identity: PublicKey },
|
||||
}
|
||||
|
||||
/// Outcome does not depend on handshake, counters, aliases, or record ordering.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum TechnicalMatch {
|
||||
Match,
|
||||
Mismatch(Vec<ComparisonFinding>),
|
||||
Unverifiable(Vec<ComparisonFinding>),
|
||||
}
|
||||
|
||||
/// Compare the complete saved peer set with a runtime sample, requiring the
|
||||
/// observed interface to be the interface selected by current configuration.
|
||||
pub fn compare_peers(
|
||||
saved: &SavedPeerProjection,
|
||||
expected_interface: &str,
|
||||
runtime: &RuntimeObservation,
|
||||
) -> TechnicalMatch {
|
||||
let mut findings = Vec::new();
|
||||
let runtime_peers = match &runtime.interface {
|
||||
InterfaceObservation::Unknown => {
|
||||
return TechnicalMatch::Unverifiable(vec![ComparisonFinding::InterfaceUnknown]);
|
||||
}
|
||||
InterfaceObservation::Unavailable => {
|
||||
return TechnicalMatch::Unverifiable(vec![ComparisonFinding::InterfaceUnavailable]);
|
||||
}
|
||||
InterfaceObservation::Missing => {
|
||||
return TechnicalMatch::Mismatch(vec![ComparisonFinding::InterfaceMissing]);
|
||||
}
|
||||
InterfaceObservation::Present { name, peers } => {
|
||||
if name != expected_interface {
|
||||
return TechnicalMatch::Mismatch(vec![ComparisonFinding::InterfaceMismatch {
|
||||
expected: expected_interface.to_owned(),
|
||||
observed: name.clone(),
|
||||
}]);
|
||||
}
|
||||
peers
|
||||
}
|
||||
};
|
||||
|
||||
if saved.document_unverifiable {
|
||||
findings.push(ComparisonFinding::SavedDocumentUnverifiable);
|
||||
}
|
||||
findings.extend(saved.unverifiable_peer_indices.iter().map(|peer_index| {
|
||||
ComparisonFinding::SavedPeerUnverifiable {
|
||||
peer_index: *peer_index,
|
||||
}
|
||||
}));
|
||||
findings.extend(saved.duplicate_public_keys.iter().map(|identity| {
|
||||
ComparisonFinding::DuplicateSavedPublicKey {
|
||||
identity: *identity,
|
||||
}
|
||||
}));
|
||||
findings.extend(runtime_peers.duplicate_public_keys.iter().map(|identity| {
|
||||
ComparisonFinding::DuplicateRuntimePublicKey {
|
||||
identity: *identity,
|
||||
}
|
||||
}));
|
||||
|
||||
match runtime_peers.completeness {
|
||||
PeerSetCompleteness::Unknown => findings.push(ComparisonFinding::PeerSetUnknown),
|
||||
PeerSetCompleteness::Unavailable => findings.push(ComparisonFinding::PeerSetUnavailable),
|
||||
PeerSetCompleteness::Partial => findings.push(ComparisonFinding::PeerSetPartial),
|
||||
PeerSetCompleteness::Complete => {}
|
||||
}
|
||||
|
||||
for (identity, expected) in &saved.peers {
|
||||
match runtime_peers.peers.get(identity) {
|
||||
None if runtime_peers.completeness == PeerSetCompleteness::Complete => {
|
||||
findings.push(ComparisonFinding::MissingRuntimePeer {
|
||||
identity: *identity,
|
||||
});
|
||||
}
|
||||
None => {}
|
||||
Some(actual) => match &actual.allowed_ips {
|
||||
Observed::Unavailable => {
|
||||
findings.push(ComparisonFinding::RuntimeRoutesUnavailable {
|
||||
identity: *identity,
|
||||
})
|
||||
}
|
||||
Observed::Available(routes) => {
|
||||
let normalized: BTreeSet<IpNet> =
|
||||
routes.iter().map(|network| network.trunc()).collect();
|
||||
if normalized != expected.allowed_ips {
|
||||
findings.push(ComparisonFinding::AllowedIpsMismatch {
|
||||
identity: *identity,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
if runtime_peers.completeness == PeerSetCompleteness::Complete {
|
||||
for identity in runtime_peers.peers.keys() {
|
||||
if !saved.peers.contains_key(identity) {
|
||||
findings.push(ComparisonFinding::ExtraRuntimePeer {
|
||||
identity: *identity,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if findings.is_empty() {
|
||||
TechnicalMatch::Match
|
||||
} else if findings.iter().any(is_unverifiable_finding) {
|
||||
TechnicalMatch::Unverifiable(findings)
|
||||
} else {
|
||||
TechnicalMatch::Mismatch(findings)
|
||||
}
|
||||
}
|
||||
|
||||
fn is_unverifiable_finding(finding: &ComparisonFinding) -> bool {
|
||||
matches!(
|
||||
finding,
|
||||
ComparisonFinding::InterfaceUnknown
|
||||
| ComparisonFinding::InterfaceUnavailable
|
||||
| ComparisonFinding::PeerSetUnknown
|
||||
| ComparisonFinding::PeerSetUnavailable
|
||||
| ComparisonFinding::PeerSetPartial
|
||||
| ComparisonFinding::SavedDocumentUnverifiable
|
||||
| ComparisonFinding::SavedPeerUnverifiable { .. }
|
||||
| ComparisonFinding::DuplicateSavedPublicKey { .. }
|
||||
| ComparisonFinding::DuplicateRuntimePublicKey { .. }
|
||||
| ComparisonFinding::RuntimeRoutesUnavailable { .. }
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::str::FromStr;
|
||||
|
||||
use super::*;
|
||||
|
||||
const KEY_A: &str = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
|
||||
const KEY_B: &str = "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE=";
|
||||
|
||||
fn key(value: &str) -> PublicKey {
|
||||
PublicKey::from_str(value).unwrap()
|
||||
}
|
||||
|
||||
fn runtime_peer(routes: Observed<Vec<IpNet>>) -> RuntimePeer {
|
||||
RuntimePeer {
|
||||
allowed_ips: routes,
|
||||
last_handshake: Observed::Available(None),
|
||||
received_bytes: Observed::Available(0),
|
||||
transmitted_bytes: Observed::Available(0),
|
||||
}
|
||||
}
|
||||
|
||||
fn observation(records: Vec<(PublicKey, RuntimePeer)>) -> RuntimeObservation {
|
||||
RuntimeObservation {
|
||||
observed_at: None,
|
||||
interface: InterfaceObservation::Present {
|
||||
name: "wg0".to_owned(),
|
||||
peers: RuntimePeerSet::from_records(PeerSetCompleteness::Complete, records),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn compare(saved: &SavedPeerProjection, runtime: &RuntimeObservation) -> TechnicalMatch {
|
||||
compare_peers(saved, "wg0", runtime)
|
||||
}
|
||||
|
||||
fn projection(source: &str) -> SavedPeerProjection {
|
||||
SavedPeerProjection::from_document(&ConfigDocument::parse(source).unwrap())
|
||||
}
|
||||
|
||||
fn route(value: &str) -> IpNet {
|
||||
value.parse().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn observation_distinguishes_unknown_missing_unavailable_zero_and_no_handshake() {
|
||||
assert_eq!(
|
||||
RuntimeObservation::unknown().interface,
|
||||
InterfaceObservation::Unknown
|
||||
);
|
||||
assert_eq!(
|
||||
RuntimeObservation {
|
||||
observed_at: None,
|
||||
interface: InterfaceObservation::Unavailable,
|
||||
}
|
||||
.interface,
|
||||
InterfaceObservation::Unavailable
|
||||
);
|
||||
assert!(matches!(
|
||||
runtime_peer(Observed::Available(Vec::new())).last_handshake,
|
||||
Observed::Available(None)
|
||||
));
|
||||
assert_eq!(
|
||||
runtime_peer(Observed::Available(Vec::new())).received_bytes,
|
||||
Observed::Available(0)
|
||||
);
|
||||
assert_eq!(
|
||||
runtime_peer(Observed::Unavailable).allowed_ips,
|
||||
Observed::Unavailable
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn comparison_accepts_empty_set_and_normalized_reordered_dual_stack_routes() {
|
||||
let empty = projection("");
|
||||
assert_eq!(
|
||||
compare(&empty, &observation(Vec::new())),
|
||||
TechnicalMatch::Match
|
||||
);
|
||||
|
||||
let source =
|
||||
format!("[Peer]\nPublicKey = {KEY_A}\nAllowedIPs = fd00::2/128, 10.0.0.2/32\n");
|
||||
let expected = projection(&source);
|
||||
let actual = observation(vec![(
|
||||
key(KEY_A),
|
||||
runtime_peer(Observed::Available(vec![
|
||||
route("10.0.0.2/32"),
|
||||
route("fd00::2/128"),
|
||||
route("10.0.0.2/32"),
|
||||
])),
|
||||
)]);
|
||||
assert_eq!(compare(&expected, &actual), TechnicalMatch::Match);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn comparison_requires_the_configured_interface_identity() {
|
||||
let expected = projection(&format!(
|
||||
"[Peer]\nPublicKey = {KEY_A}\nAllowedIPs = 10.0.0.2/32\n"
|
||||
));
|
||||
let wrong_interface = RuntimeObservation {
|
||||
observed_at: None,
|
||||
interface: InterfaceObservation::Present {
|
||||
name: "wg1".to_owned(),
|
||||
peers: RuntimePeerSet::from_records(
|
||||
PeerSetCompleteness::Complete,
|
||||
[(
|
||||
key(KEY_A),
|
||||
runtime_peer(Observed::Available(vec![route("10.0.0.2/32")])),
|
||||
)],
|
||||
),
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
compare_peers(&expected, "wg0", &wrong_interface),
|
||||
TechnicalMatch::Mismatch(vec![ComparisonFinding::InterfaceMismatch {
|
||||
expected: "wg0".to_owned(),
|
||||
observed: "wg1".to_owned(),
|
||||
}])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn counters_and_handshake_changes_do_not_change_technical_match() {
|
||||
let expected = projection(&format!(
|
||||
"[Peer]\nPublicKey = {KEY_A}\nAllowedIPs = 10.0.0.2/32\n"
|
||||
));
|
||||
let mut before_restart_peer = runtime_peer(Observed::Available(vec![route("10.0.0.2/32")]));
|
||||
before_restart_peer.last_handshake = Observed::Available(Some(
|
||||
OffsetDateTime::from_unix_timestamp(1_700_000_000).unwrap(),
|
||||
));
|
||||
before_restart_peer.received_bytes = Observed::Available(1_000);
|
||||
before_restart_peer.transmitted_bytes = Observed::Available(300);
|
||||
let before_restart = observation(vec![(key(KEY_A), before_restart_peer)]);
|
||||
let mut after_restart_peer = runtime_peer(Observed::Available(vec![route("10.0.0.2/32")]));
|
||||
after_restart_peer.last_handshake = Observed::Available(None);
|
||||
after_restart_peer.received_bytes = Observed::Available(0);
|
||||
after_restart_peer.transmitted_bytes = Observed::Available(7);
|
||||
let after_restart = observation(vec![(key(KEY_A), after_restart_peer)]);
|
||||
|
||||
assert_eq!(compare(&expected, &before_restart), TechnicalMatch::Match);
|
||||
assert_eq!(compare(&expected, &after_restart), TechnicalMatch::Match);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn projection_keeps_valid_unsupported_routes_and_ipv4_only_peers() {
|
||||
let source = format!(
|
||||
"[Peer]\nPublicKey = {KEY_A}\nAllowedIPs = 10.0.0.2/32, 10.20.0.0/16\n[Peer]\nPublicKey = {KEY_B}\nAllowedIPs = 10.0.0.3/32\n"
|
||||
);
|
||||
let expected = projection(&source);
|
||||
assert_eq!(expected.peers.len(), 2);
|
||||
assert!(expected.unverifiable_peer_indices.is_empty());
|
||||
let actual = observation(vec![
|
||||
(
|
||||
key(KEY_A),
|
||||
runtime_peer(Observed::Available(vec![
|
||||
route("10.20.0.0/16"),
|
||||
route("10.0.0.2/32"),
|
||||
])),
|
||||
),
|
||||
(
|
||||
key(KEY_B),
|
||||
runtime_peer(Observed::Available(vec![route("10.0.0.3/32")])),
|
||||
),
|
||||
]);
|
||||
assert_eq!(compare(&expected, &actual), TechnicalMatch::Match);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn comparison_reports_missing_extra_and_route_mismatch() {
|
||||
let expected = projection(&format!(
|
||||
"[Peer]\nPublicKey = {KEY_A}\nAllowedIPs = 10.0.0.2/32\n"
|
||||
));
|
||||
let actual = observation(vec![(
|
||||
key(KEY_B),
|
||||
runtime_peer(Observed::Available(vec![route("10.0.0.3/32")])),
|
||||
)]);
|
||||
let TechnicalMatch::Mismatch(findings) = compare(&expected, &actual) else {
|
||||
panic!("expected concrete set differences");
|
||||
};
|
||||
assert!(findings.contains(&ComparisonFinding::MissingRuntimePeer {
|
||||
identity: key(KEY_A)
|
||||
}));
|
||||
assert!(findings.contains(&ComparisonFinding::ExtraRuntimePeer {
|
||||
identity: key(KEY_B)
|
||||
}));
|
||||
|
||||
let actual = observation(vec![(
|
||||
key(KEY_A),
|
||||
runtime_peer(Observed::Available(vec![route("10.0.0.9/32")])),
|
||||
)]);
|
||||
assert!(matches!(
|
||||
compare(&expected, &actual),
|
||||
TechnicalMatch::Mismatch(findings)
|
||||
if findings.contains(&ComparisonFinding::AllowedIpsMismatch { identity: key(KEY_A) })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partial_duplicate_and_unavailable_records_never_pass() {
|
||||
let expected = projection(&format!(
|
||||
"[Peer]\nPublicKey = {KEY_A}\nAllowedIPs = 10.0.0.2/32\n"
|
||||
));
|
||||
let duplicate = RuntimePeerSet::from_records(
|
||||
PeerSetCompleteness::Complete,
|
||||
[
|
||||
(
|
||||
key(KEY_A),
|
||||
runtime_peer(Observed::Available(vec![route("10.0.0.2/32")])),
|
||||
),
|
||||
(
|
||||
key(KEY_A),
|
||||
runtime_peer(Observed::Available(vec![route("10.0.0.2/32")])),
|
||||
),
|
||||
],
|
||||
);
|
||||
assert!(matches!(
|
||||
compare(
|
||||
&expected,
|
||||
&RuntimeObservation {
|
||||
observed_at: None,
|
||||
interface: InterfaceObservation::Present {
|
||||
name: "wg0".to_owned(),
|
||||
peers: duplicate,
|
||||
},
|
||||
}
|
||||
),
|
||||
TechnicalMatch::Unverifiable(findings)
|
||||
if findings.contains(&ComparisonFinding::DuplicateRuntimePublicKey { identity: key(KEY_A) })
|
||||
));
|
||||
|
||||
for completeness in [
|
||||
PeerSetCompleteness::Unknown,
|
||||
PeerSetCompleteness::Unavailable,
|
||||
PeerSetCompleteness::Partial,
|
||||
] {
|
||||
let partial = RuntimeObservation {
|
||||
observed_at: None,
|
||||
interface: InterfaceObservation::Present {
|
||||
name: "wg0".to_owned(),
|
||||
peers: RuntimePeerSet {
|
||||
peers: BTreeMap::from([(
|
||||
key(KEY_A),
|
||||
runtime_peer(Observed::Available(vec![route("10.0.0.2/32")])),
|
||||
)]),
|
||||
duplicate_public_keys: BTreeSet::new(),
|
||||
completeness,
|
||||
},
|
||||
},
|
||||
};
|
||||
let TechnicalMatch::Unverifiable(findings) = compare(&expected, &partial) else {
|
||||
panic!("an incomplete peer set cannot verify");
|
||||
};
|
||||
if completeness != PeerSetCompleteness::Complete {
|
||||
assert!(!findings.iter().any(|finding| matches!(
|
||||
finding,
|
||||
ComparisonFinding::MissingRuntimePeer { .. }
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
let no_routes = observation(vec![(key(KEY_A), runtime_peer(Observed::Unavailable))]);
|
||||
assert!(matches!(
|
||||
compare(&expected, &no_routes),
|
||||
TechnicalMatch::Unverifiable(_)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_saved_identities_are_unverifiable_even_if_the_map_would_match() {
|
||||
let source = format!(
|
||||
"[Peer]\nPublicKey = {KEY_A}\nAllowedIPs = 10.0.0.2/32\n[Peer]\nPublicKey = {KEY_A}\nAllowedIPs = 10.0.0.3/32\n"
|
||||
);
|
||||
let expected = projection(&source);
|
||||
let actual = observation(vec![(
|
||||
key(KEY_A),
|
||||
runtime_peer(Observed::Available(vec![route("10.0.0.2/32")])),
|
||||
)]);
|
||||
assert!(matches!(
|
||||
compare(&expected, &actual),
|
||||
TechnicalMatch::Unverifiable(findings)
|
||||
if findings.contains(&ComparisonFinding::DuplicateSavedPublicKey { identity: key(KEY_A) })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_saved_peer_and_interface_states_fail_closed_but_metadata_does_not() {
|
||||
let invalid = projection("[Peer]\nAllowedIPs = 10.0.0.2/32\n");
|
||||
assert!(matches!(
|
||||
compare(&invalid, &observation(Vec::new())),
|
||||
TechnicalMatch::Unverifiable(_)
|
||||
));
|
||||
let valid_with_bad_alias = projection(&format!(
|
||||
"# wireguard-admin:v1 alias=bad%0A date=unknown\n[Peer]\nPublicKey = {KEY_A}\nAllowedIPs = 10.0.0.2/32\n"
|
||||
));
|
||||
assert!(!valid_with_bad_alias.document_unverifiable);
|
||||
|
||||
for interface in [
|
||||
InterfaceObservation::Unknown,
|
||||
InterfaceObservation::Unavailable,
|
||||
InterfaceObservation::Missing,
|
||||
] {
|
||||
let result = compare(
|
||||
&projection(""),
|
||||
&RuntimeObservation {
|
||||
observed_at: None,
|
||||
interface,
|
||||
},
|
||||
);
|
||||
assert!(!matches!(result, TechnicalMatch::Match));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_routes_and_structural_saved_issues_cannot_be_omitted_for_a_match() {
|
||||
let malformed_route = projection(&format!(
|
||||
"[Peer]\nPublicKey = {KEY_A}\nAllowedIPs = 10.0.0.2/32, not-a-network\n"
|
||||
));
|
||||
assert!(!malformed_route.unverifiable_peer_indices.is_empty());
|
||||
let apparent_subset_match = observation(vec![(
|
||||
key(KEY_A),
|
||||
runtime_peer(Observed::Available(vec![route("10.0.0.2/32")])),
|
||||
)]);
|
||||
assert!(matches!(
|
||||
compare(&malformed_route, &apparent_subset_match),
|
||||
TechnicalMatch::Unverifiable(findings)
|
||||
if findings.iter().any(|finding| matches!(finding, ComparisonFinding::SavedPeerUnverifiable { .. }))
|
||||
));
|
||||
|
||||
let structural = projection(&format!(
|
||||
"[Interface]\nMalformed Directive\n[Peer]\nPublicKey = {KEY_A}\nAllowedIPs = 10.0.0.2/32\n"
|
||||
));
|
||||
assert!(structural.document_unverifiable);
|
||||
let exact_runtime = observation(vec![(
|
||||
key(KEY_A),
|
||||
runtime_peer(Observed::Available(vec![route("10.0.0.2/32")])),
|
||||
)]);
|
||||
assert!(matches!(
|
||||
compare(&structural, &exact_runtime),
|
||||
TechnicalMatch::Unverifiable(findings)
|
||||
if findings.contains(&ComparisonFinding::SavedDocumentUnverifiable)
|
||||
));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user