675 lines
24 KiB
Rust
675 lines
24 KiB
Rust
//! 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)
|
|
));
|
|
}
|
|
}
|