Skip to main content

shadow_network_sim/
adversary.rs

1//! Adversary models — define attacker capabilities and threat models.
2
3use serde::{Serialize, Deserialize};
4use std::collections::HashSet;
5
6/// What kind of adversary we're modelling.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
8pub enum AdversaryType {
9    /// Passively observes traffic (ISP-level).
10    PassiveObserver,
11    /// Actively modifies or injects traffic (MITM).
12    ActiveAttacker,
13    /// Can observe all network links simultaneously.
14    GlobalObserver,
15    /// Controls a fraction of network relays.
16    CompromisedRelays,
17    /// Combination of capabilities.
18    AdvancedPersistent,
19}
20
21/// Capabilities an adversary may have.
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct AdversaryCapabilities {
24    /// Can observe metadata (timing, sizes, flow direction).
25    pub can_observe_metadata: bool,
26    /// Can read packet contents (no encryption).
27    pub can_read_content: bool,
28    /// Can inject packets into the network.
29    pub can_inject: bool,
30    /// Can drop packets selectively.
31    pub can_drop: bool,
32    /// Can delay packets selectively.
33    pub can_delay: bool,
34    /// Can perform traffic analysis.
35    pub can_correlate: bool,
36    /// Fraction of the network controlled (0.0–1.0).
37    pub network_fraction: f64,
38    /// Number of vantage points for observation.
39    pub vantage_points: usize,
40}
41
42impl AdversaryCapabilities {
43    /// Construct capabilities for a given adversary type.
44    pub fn for_type(adversary_type: AdversaryType) -> Self {
45        match adversary_type {
46            AdversaryType::PassiveObserver => Self {
47                can_observe_metadata: true,
48                can_read_content: false,
49                can_inject: false,
50                can_drop: false,
51                can_delay: false,
52                can_correlate: true,
53                network_fraction: 0.0,
54                vantage_points: 1,
55            },
56            AdversaryType::ActiveAttacker => Self {
57                can_observe_metadata: true,
58                can_read_content: false,
59                can_inject: true,
60                can_drop: true,
61                can_delay: true,
62                can_correlate: true,
63                network_fraction: 0.0,
64                vantage_points: 1,
65            },
66            AdversaryType::GlobalObserver => Self {
67                can_observe_metadata: true,
68                can_read_content: false,
69                can_inject: false,
70                can_drop: false,
71                can_delay: false,
72                can_correlate: true,
73                network_fraction: 0.0,
74                vantage_points: 100,
75            },
76            AdversaryType::CompromisedRelays => Self {
77                can_observe_metadata: true,
78                can_read_content: true,
79                can_inject: true,
80                can_drop: true,
81                can_delay: true,
82                can_correlate: true,
83                network_fraction: 0.1,
84                vantage_points: 5,
85            },
86            AdversaryType::AdvancedPersistent => Self {
87                can_observe_metadata: true,
88                can_read_content: false,
89                can_inject: true,
90                can_drop: true,
91                can_delay: true,
92                can_correlate: true,
93                network_fraction: 0.2,
94                vantage_points: 20,
95            },
96        }
97    }
98
99    /// Threat level (0.0–1.0) based on combined capabilities.
100    pub fn threat_level(&self) -> f64 {
101        let mut level = 0.0;
102        if self.can_observe_metadata { level += 0.1; }
103        if self.can_read_content { level += 0.2; }
104        if self.can_inject { level += 0.15; }
105        if self.can_drop { level += 0.1; }
106        if self.can_delay { level += 0.05; }
107        if self.can_correlate { level += 0.15; }
108        level += self.network_fraction * 0.25;
109        level.min(1.0)
110    }
111}
112
113/// Simulated adversary acting on network traffic.
114pub struct AdversaryModel {
115    pub adversary_type: AdversaryType,
116    pub capabilities: AdversaryCapabilities,
117    /// Set of compromised node IDs.
118    pub compromised_nodes: HashSet<[u8; 32]>,
119    /// Observed traffic flows.
120    pub observed_flows: Vec<ObservedFlow>,
121}
122
123/// A traffic flow observed by the adversary.
124#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct ObservedFlow {
126    pub source_id: [u8; 32],
127    pub dest_id: [u8; 32],
128    pub packet_sizes: Vec<usize>,
129    pub timestamps_us: Vec<u64>,
130    pub encrypted: bool,
131}
132
133impl AdversaryModel {
134    pub fn new(adversary_type: AdversaryType) -> Self {
135        Self {
136            capabilities: AdversaryCapabilities::for_type(adversary_type),
137            adversary_type,
138            compromised_nodes: HashSet::new(),
139            observed_flows: Vec::new(),
140        }
141    }
142
143    /// Compromise a specific relay node.
144    pub fn compromise_node(&mut self, node_id: [u8; 32]) {
145        self.compromised_nodes.insert(node_id);
146    }
147
148    /// Check if a node is compromised.
149    pub fn is_compromised(&self, node_id: &[u8; 32]) -> bool {
150        self.compromised_nodes.contains(node_id)
151    }
152
153    /// Observe a traffic flow.
154    pub fn observe_flow(&mut self, flow: ObservedFlow) {
155        self.observed_flows.push(flow);
156    }
157
158    /// Attempt to correlate two flows (by timing patterns).
159    pub fn correlate_flows(&self, a: &ObservedFlow, b: &ObservedFlow) -> f64 {
160        if !self.capabilities.can_correlate {
161            return 0.0;
162        }
163
164        // Simple timing correlation: compare inter-packet delays
165        let delays_a = inter_packet_delays(&a.timestamps_us);
166        let delays_b = inter_packet_delays(&b.timestamps_us);
167
168        if delays_a.is_empty() || delays_b.is_empty() {
169            return 0.0;
170        }
171
172        // Pearson correlation coefficient
173        pearson_correlation(&delays_a, &delays_b).abs()
174    }
175
176    /// Evaluate circuit anonymity: probability of deanonymization.
177    pub fn circuit_compromise_probability(&self, path_length: usize) -> f64 {
178        // Probability that both entry and exit are compromised
179        let f = self.capabilities.network_fraction;
180        // P(first AND last compromised) = f * f
181        // For path_length = 3: P = f^2
182        // For longer paths, middle nodes don't help if first+last are bad
183        f * f
184    }
185
186    /// Total observed flow count.
187    pub fn observed_count(&self) -> usize {
188        self.observed_flows.len()
189    }
190}
191
192/// Compute inter-packet delays from timestamps.
193fn inter_packet_delays(timestamps: &[u64]) -> Vec<f64> {
194    timestamps
195        .windows(2)
196        .map(|w| (w[1] as f64) - (w[0] as f64))
197        .collect()
198}
199
200/// Pearson correlation coefficient between two series.
201fn pearson_correlation(a: &[f64], b: &[f64]) -> f64 {
202    let n = a.len().min(b.len());
203    if n < 2 {
204        return 0.0;
205    }
206
207    let mean_a = a[..n].iter().sum::<f64>() / n as f64;
208    let mean_b = b[..n].iter().sum::<f64>() / n as f64;
209
210    let mut cov = 0.0;
211    let mut var_a = 0.0;
212    let mut var_b = 0.0;
213
214    for i in 0..n {
215        let da = a[i] - mean_a;
216        let db = b[i] - mean_b;
217        cov += da * db;
218        var_a += da * da;
219        var_b += db * db;
220    }
221
222    let denom = (var_a * var_b).sqrt();
223    if denom < 1e-12 {
224        0.0
225    } else {
226        cov / denom
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233
234    #[test]
235    fn test_adversary_capabilities() {
236        let caps = AdversaryCapabilities::for_type(AdversaryType::PassiveObserver);
237        assert!(caps.can_observe_metadata);
238        assert!(!caps.can_inject);
239        assert!(!caps.can_read_content);
240    }
241
242    #[test]
243    fn test_threat_levels() {
244        let passive = AdversaryCapabilities::for_type(AdversaryType::PassiveObserver);
245        let active = AdversaryCapabilities::for_type(AdversaryType::ActiveAttacker);
246        let apt = AdversaryCapabilities::for_type(AdversaryType::AdvancedPersistent);
247
248        assert!(passive.threat_level() < active.threat_level());
249        assert!(active.threat_level() < apt.threat_level());
250    }
251
252    #[test]
253    fn test_flow_correlation() {
254        let model = AdversaryModel::new(AdversaryType::GlobalObserver);
255        // Same timing pattern = high correlation
256        let flow_a = ObservedFlow {
257            source_id: [1; 32],
258            dest_id: [2; 32],
259            packet_sizes: vec![100, 200, 150],
260            timestamps_us: vec![0, 1000, 2500],
261            encrypted: true,
262        };
263        let flow_b = ObservedFlow {
264            source_id: [3; 32],
265            dest_id: [4; 32],
266            packet_sizes: vec![100, 200, 150],
267            timestamps_us: vec![50, 1050, 2550], // offset but same pattern
268            encrypted: true,
269        };
270
271        let corr = model.correlate_flows(&flow_a, &flow_b);
272        assert!(corr > 0.99, "Same-pattern flows should be highly correlated: {}", corr);
273    }
274
275    #[test]
276    fn test_circuit_compromise_probability() {
277        let mut model = AdversaryModel::new(AdversaryType::CompromisedRelays);
278        model.capabilities.network_fraction = 0.1;
279        let prob = model.circuit_compromise_probability(3);
280        assert!((prob - 0.01).abs() < 0.001); // 0.1 * 0.1 = 0.01
281    }
282}