//! 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 { 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>, /// `Available(None)` means WireGuard has never recorded a handshake. pub last_handshake: Observed>, /// These are current counters and may reset when the interface restarts. pub received_bytes: Observed, pub transmitted_bytes: Observed, } /// 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, pub duplicate_public_keys: BTreeSet, 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, ) -> 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, 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, } /// 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, pub duplicate_public_keys: BTreeSet, pub unverifiable_peer_indices: BTreeSet, 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), Unverifiable(Vec), } /// 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 = 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>) -> 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) )); } }