vanguards_rs/node_selection.rs
1//! Node selection and input validation for vanguards-rs.
2//!
3//! This module provides bandwidth-weighted node selection for vanguard relay selection,
4//! along with input validation functions for fingerprints, IP addresses, and country codes.
5//!
6//! # Overview
7//!
8//! The node selection system implements:
9//!
10//! - **Input Validation**: Functions to validate relay fingerprints, IP addresses, and country codes
11//! - **Node Restrictions**: Trait-based system for filtering relays by flags and other criteria
12//! - **Bandwidth-Weighted Selection**: Random selection proportional to relay bandwidth
13//!
14//! # Bandwidth-Weighted Selection Algorithm
15//!
16//! The selection algorithm ensures relays are chosen proportionally to their bandwidth,
17//! which helps distribute load across the network while respecting Tor's consensus weights.
18//!
19//! # Validation Functions
20//!
21//! - [`is_valid_fingerprint`]: Validates 40-character hexadecimal relay fingerprints
22//! - [`is_valid_ip_or_network`]: Validates IPv4/IPv6 addresses and CIDR networks
23//! - [`is_valid_country_code`]: Validates 2-character country codes
24//!
25//! # Node Selection
26//!
27//! The [`BwWeightedGenerator`] implements bandwidth-weighted random selection:
28//!
29//! ```rust,ignore
30//! use vanguards_rs::node_selection::{BwWeightedGenerator, FlagsRestriction, NodeRestrictionList, Position};
31//!
32//! // Create restrictions requiring Fast, Stable, Valid flags
33//! let restriction = FlagsRestriction::new(
34//! vec!["Fast".to_string(), "Stable".to_string(), "Valid".to_string()],
35//! vec!["Authority".to_string()],
36//! );
37//! let restrictions = NodeRestrictionList::new(vec![Box::new(restriction)]);
38//!
39//! // Create generator with consensus weights
40//! let generator = BwWeightedGenerator::new(routers, restrictions, weights, Position::Middle)?;
41//!
42//! // Generate nodes
43//! let node = generator.generate()?;
44//! ```
45//!
46//! # What This Module Does NOT Do
47//!
48//! - **Consensus fetching**: Use [`stem_rs::descriptor::remote`] to fetch consensus data
49//! - **Guard persistence**: Use [`crate::vanguards::VanguardState`] for state management
50//! - **Circuit building**: This module only selects nodes; circuit construction is handled elsewhere
51//!
52//! # Security Considerations
53//!
54//! - Bandwidth weighting prevents attackers from easily positioning malicious relays
55//! - Flag restrictions ensure only qualified relays are selected for sensitive positions
56//! - The random selection uses a cryptographically secure random number generator
57//!
58//! # See Also
59//!
60//! - [`crate::error::Error::NoNodesRemain`] - Error when all nodes are filtered
61//! - [`crate::vanguards`] - Vanguard state management using selected nodes
62//! - [`crate::config`] - Configuration for node selection parameters
63//! - [Python vanguards NodeSelection](https://github.com/mikeperry-tor/vanguards)
64
65use std::collections::HashMap;
66use std::net::IpAddr;
67
68use ipnetwork::IpNetwork;
69use rand::Rng;
70use stem_rs::descriptor::router_status::RouterStatusEntry;
71
72use crate::error::{Error, Result};
73
74/// Validates that a string is a valid relay fingerprint.
75///
76/// A valid fingerprint is exactly 40 hexadecimal characters (case-insensitive).
77///
78/// # Arguments
79///
80/// * `s` - The string to validate
81///
82/// # Returns
83///
84/// `true` if the string is a valid fingerprint, `false` otherwise.
85///
86/// # Example
87///
88/// ```rust
89/// use vanguards_rs::node_selection::is_valid_fingerprint;
90///
91/// assert!(is_valid_fingerprint("AABBCCDD00112233445566778899AABBCCDDEEFF"));
92/// assert!(is_valid_fingerprint("aabbccdd00112233445566778899aabbccddeeff"));
93/// assert!(!is_valid_fingerprint("AABBCCDD")); // Too short
94/// assert!(!is_valid_fingerprint("GGHHIIJJ00112233445566778899AABBCCDDEEFF")); // Invalid hex
95/// ```
96pub fn is_valid_fingerprint(s: &str) -> bool {
97 s.len() == 40 && s.chars().all(|c| c.is_ascii_hexdigit())
98}
99
100/// Validates that a string is a valid IP address or CIDR network.
101///
102/// Accepts IPv4 addresses, IPv6 addresses, and CIDR notation for both.
103///
104/// # Arguments
105///
106/// * `s` - The string to validate
107///
108/// # Returns
109///
110/// `true` if the string is a valid IP address or network, `false` otherwise.
111///
112/// # Example
113///
114/// ```rust
115/// use vanguards_rs::node_selection::is_valid_ip_or_network;
116///
117/// // IPv4
118/// assert!(is_valid_ip_or_network("192.168.1.1"));
119/// assert!(is_valid_ip_or_network("192.168.1.0/24"));
120///
121/// // IPv6
122/// assert!(is_valid_ip_or_network("::1"));
123/// assert!(is_valid_ip_or_network("2001:db8::/32"));
124///
125/// // Invalid
126/// assert!(!is_valid_ip_or_network("not-an-ip"));
127/// assert!(!is_valid_ip_or_network("192.168.1.1/33")); // Invalid prefix
128/// ```
129pub fn is_valid_ip_or_network(s: &str) -> bool {
130 s.parse::<IpAddr>().is_ok() || s.parse::<IpNetwork>().is_ok()
131}
132
133/// Validates that a string is a valid 2-character country code.
134///
135/// Country codes must be exactly 2 alphabetic characters (case-insensitive).
136///
137/// # Arguments
138///
139/// * `s` - The string to validate
140///
141/// # Returns
142///
143/// `true` if the string is a valid country code, `false` otherwise.
144///
145/// # Example
146///
147/// ```rust
148/// use vanguards_rs::node_selection::is_valid_country_code;
149///
150/// assert!(is_valid_country_code("US"));
151/// assert!(is_valid_country_code("de"));
152/// assert!(!is_valid_country_code("USA")); // Too long
153/// assert!(!is_valid_country_code("U1")); // Contains digit
154/// ```
155pub fn is_valid_country_code(s: &str) -> bool {
156 s.len() == 2 && s.chars().all(|c| c.is_ascii_alphabetic())
157}
158
159/// Interface for node restriction policies.
160///
161/// Implementations of this trait define criteria for filtering relay nodes.
162/// Multiple restrictions can be combined using [`NodeRestrictionList`].
163///
164/// # Implementing Custom Restrictions
165///
166/// Create custom restrictions by implementing this trait:
167///
168/// ```rust
169/// use vanguards_rs::node_selection::NodeRestriction;
170/// use stem_rs::descriptor::router_status::RouterStatusEntry;
171///
172/// struct BandwidthRestriction {
173/// min_bandwidth: u64,
174/// }
175///
176/// impl NodeRestriction for BandwidthRestriction {
177/// fn r_is_ok(&self, router: &RouterStatusEntry) -> bool {
178/// router.bandwidth.unwrap_or(0) >= self.min_bandwidth
179/// }
180/// }
181/// ```
182///
183/// # Thread Safety
184///
185/// Implementations must be `Send + Sync` to allow use across threads.
186///
187/// # See Also
188///
189/// - [`FlagsRestriction`] - Built-in restriction for router flags
190/// - [`NodeRestrictionList`] - Combine multiple restrictions
191pub trait NodeRestriction: Send + Sync {
192 /// Returns true if the router passes this restriction.
193 fn r_is_ok(&self, router: &RouterStatusEntry) -> bool;
194}
195
196/// Restriction for mandatory and forbidden router flags.
197///
198/// This restriction filters routers based on their assigned flags.
199/// Routers must have all mandatory flags and none of the forbidden flags.
200///
201/// # Common Flag Combinations
202///
203/// | Use Case | Mandatory | Forbidden |
204/// |----------|-----------|-----------|
205/// | Vanguard Layer 2 | Fast, Stable, Valid | Authority, BadExit |
206/// | Vanguard Layer 3 | Fast, Stable, Valid | Authority, BadExit |
207/// | Exit Selection | Fast, Stable, Valid, Exit | BadExit |
208///
209/// # Example
210///
211/// ```rust
212/// use vanguards_rs::node_selection::FlagsRestriction;
213///
214/// // Require Fast, Stable, Valid; forbid Authority
215/// let restriction = FlagsRestriction::new(
216/// vec!["Fast".to_string(), "Stable".to_string(), "Valid".to_string()],
217/// vec!["Authority".to_string()],
218/// );
219/// ```
220///
221/// # See Also
222///
223/// - [`NodeRestriction`] - The trait this implements
224/// - [`NodeRestrictionList`] - Combine with other restrictions
225#[derive(Debug, Clone)]
226pub struct FlagsRestriction {
227 /// Flags that must be present on the router.
228 pub mandatory: Vec<String>,
229 /// Flags that must not be present on the router.
230 pub forbidden: Vec<String>,
231}
232
233impl FlagsRestriction {
234 /// Creates a new flags restriction.
235 ///
236 /// # Arguments
237 ///
238 /// * `mandatory` - List of flags that must be present
239 /// * `forbidden` - List of flags that must not be present
240 pub fn new(mandatory: Vec<String>, forbidden: Vec<String>) -> Self {
241 Self {
242 mandatory,
243 forbidden,
244 }
245 }
246}
247
248impl NodeRestriction for FlagsRestriction {
249 fn r_is_ok(&self, router: &RouterStatusEntry) -> bool {
250 for m in &self.mandatory {
251 if !router.flags.contains(m) {
252 return false;
253 }
254 }
255 for f in &self.forbidden {
256 if router.flags.contains(f) {
257 return false;
258 }
259 }
260 true
261 }
262}
263
264/// A list of node restrictions to apply.
265///
266/// All restrictions must pass for a router to be accepted. This allows
267/// combining multiple filtering criteria (e.g., flags + bandwidth + country).
268///
269/// # Example
270///
271/// ```rust
272/// use vanguards_rs::node_selection::{FlagsRestriction, NodeRestrictionList};
273///
274/// let flags = FlagsRestriction::new(
275/// vec!["Fast".to_string()],
276/// vec!["BadExit".to_string()],
277/// );
278/// let restrictions = NodeRestrictionList::new(vec![Box::new(flags)]);
279/// ```
280///
281/// # See Also
282///
283/// - [`NodeRestriction`] - Trait for individual restrictions
284/// - [`FlagsRestriction`] - Built-in flag-based restriction
285pub struct NodeRestrictionList {
286 restrictions: Vec<Box<dyn NodeRestriction>>,
287}
288
289impl NodeRestrictionList {
290 /// Creates a new restriction list.
291 pub fn new(restrictions: Vec<Box<dyn NodeRestriction>>) -> Self {
292 Self { restrictions }
293 }
294
295 /// Returns true if the router passes all restrictions.
296 pub fn r_is_ok(&self, router: &RouterStatusEntry) -> bool {
297 self.restrictions.iter().all(|r| r.r_is_ok(router))
298 }
299}
300
301/// Position in circuit for weight calculation.
302///
303/// Different positions in a Tor circuit use different bandwidth weight
304/// multipliers from the consensus. This affects how relays are selected
305/// for each hop in the circuit.
306///
307/// # Weight Keys by Position
308///
309/// | Position | Weight Keys Used |
310/// |----------|------------------|
311/// | Guard | Wgg, Wgd |
312/// | Middle | Wmm, Wmg, Wme, Wmd |
313/// | Exit | Wee, Wed |
314///
315/// # See Also
316///
317/// - [`BwWeightedGenerator`] - Uses position for weight calculation
318#[derive(Debug, Clone, Copy, PartialEq, Eq)]
319pub enum Position {
320 /// Guard (entry) position - uses Wgg/Wgd weights.
321 Guard,
322 /// Middle position - uses Wmm/Wmg/Wme/Wmd weights.
323 Middle,
324 /// Exit position - uses Wee/Wed weights.
325 Exit,
326}
327
328impl Position {
329 fn weight_key_suffix(&self) -> char {
330 match self {
331 Position::Guard => 'g',
332 Position::Middle => 'm',
333 Position::Exit => 'e',
334 }
335 }
336}
337
338/// Bandwidth-weighted node generator.
339///
340/// Implements bandwidth-weighted random selection of relay nodes.
341/// Selection probability is proportional to each relay's bandwidth weight.
342///
343/// # Weight Calculation
344///
345/// The weight for each relay is calculated as:
346///
347/// ```text
348/// weight = measured_bandwidth × flag_weight_multiplier
349/// ```
350///
351/// Where `flag_weight_multiplier` depends on the relay's flags and position:
352///
353/// | Flags | Middle Position | Guard Position | Exit Position |
354/// |-------|-----------------|----------------|---------------|
355/// | Neither Guard nor Exit | Wmm | Wgm | Wem |
356/// | Guard only | Wmg | Wgg | Weg |
357/// | Exit only | Wme | Wge | Wee |
358/// | Guard + Exit | Wmd | Wgd | Wed |
359///
360/// # Selection Algorithm
361///
362/// 1. Filter routers through all restrictions
363/// 2. Calculate weighted bandwidth for each remaining router
364/// 3. Build cumulative weight distribution
365/// 4. Generate random value in [0, total_weight)
366/// 5. Select router where cumulative weight exceeds random value
367///
368/// # Example
369///
370/// ```rust,ignore
371/// use vanguards_rs::node_selection::{BwWeightedGenerator, FlagsRestriction, NodeRestrictionList, Position};
372///
373/// let restriction = FlagsRestriction::new(
374/// vec!["Fast".to_string(), "Stable".to_string(), "Valid".to_string()],
375/// vec!["Authority".to_string()],
376/// );
377/// let restrictions = NodeRestrictionList::new(vec![Box::new(restriction)]);
378///
379/// let generator = BwWeightedGenerator::new(routers, restrictions, weights, Position::Middle)?;
380/// let selected = generator.generate()?;
381/// println!("Selected relay: {}", selected.fingerprint);
382/// ```
383///
384/// # See Also
385///
386/// - [`Position`] - Circuit position affecting weight calculation
387/// - [`NodeRestrictionList`] - Filtering criteria
388/// - [`crate::error::Error::NoNodesRemain`] - Error when no nodes pass filters
389pub struct BwWeightedGenerator {
390 rstr_routers: Vec<RouterStatusEntry>,
391 node_weights: Vec<f64>,
392 weight_total: f64,
393 exit_total: f64,
394 position: Position,
395 bw_weights: HashMap<String, i64>,
396}
397
398impl BwWeightedGenerator {
399 /// Weight scale factor from consensus (typically 10000).
400 const WEIGHT_SCALE: f64 = 10000.0;
401
402 /// Creates a new bandwidth-weighted generator.
403 ///
404 /// # Arguments
405 ///
406 /// * `sorted_routers` - Routers sorted by measured bandwidth (descending)
407 /// * `restrictions` - Restrictions to filter routers
408 /// * `bw_weights` - Consensus bandwidth weights (Wmm, Wmg, Wme, Wmd, etc.)
409 /// * `position` - Circuit position for weight calculation
410 ///
411 /// # Errors
412 ///
413 /// Returns [`Error::NoNodesRemain`] if all routers are filtered out.
414 ///
415 /// # Example
416 ///
417 /// ```rust,ignore
418 /// let generator = BwWeightedGenerator::new(routers, restrictions, weights, Position::Middle)?;
419 /// ```
420 pub fn new(
421 sorted_routers: Vec<RouterStatusEntry>,
422 restrictions: NodeRestrictionList,
423 bw_weights: HashMap<String, i64>,
424 position: Position,
425 ) -> Result<Self> {
426 let rstr_routers: Vec<RouterStatusEntry> = sorted_routers
427 .into_iter()
428 .filter(|r| restrictions.r_is_ok(r))
429 .collect();
430
431 if rstr_routers.is_empty() {
432 return Err(Error::NoNodesRemain);
433 }
434
435 let mut generator = Self {
436 rstr_routers,
437 node_weights: Vec::new(),
438 weight_total: 0.0,
439 exit_total: 0.0,
440 position,
441 bw_weights,
442 };
443
444 generator.rebuild_weights();
445 Ok(generator)
446 }
447
448 /// Rebuilds the weight arrays after router list changes.
449 fn rebuild_weights(&mut self) {
450 self.node_weights.clear();
451 self.weight_total = 0.0;
452
453 for router in &self.rstr_routers {
454 let bw = router.measured.or(router.bandwidth).unwrap_or(0) as f64;
455 let weight = bw * self.flag_to_weight(router);
456 self.node_weights.push(weight);
457 self.weight_total += weight;
458 }
459 }
460
461 /// Calculates the weight multiplier based on router flags and position.
462 ///
463 /// Uses consensus bandwidth weights:
464 /// - Wmm: Middle-only relay (no Guard, no Exit)
465 /// - Wmg: Guard relay (no Exit)
466 /// - Wme: Exit relay (no Guard)
467 /// - Wmd: Guard+Exit relay
468 fn flag_to_weight(&self, router: &RouterStatusEntry) -> f64 {
469 let has_guard = router.flags.contains(&"Guard".to_string());
470 let has_exit = router.flags.contains(&"Exit".to_string());
471 let pos = self.position.weight_key_suffix();
472
473 let key = if has_guard && has_exit {
474 format!("W{}d", pos)
475 } else if has_exit {
476 format!("W{}e", pos)
477 } else if has_guard {
478 format!("W{}g", pos)
479 } else {
480 "Wmm".to_string()
481 };
482
483 self.bw_weights.get(&key).copied().unwrap_or(10000) as f64 / Self::WEIGHT_SCALE
484 }
485
486 /// Repairs exit node weights for rendezvous point selection.
487 ///
488 /// Exit nodes got their weights set based on middle position, but they can
489 /// still be used as rendezvous points in cannibalized circuits. This method
490 /// recalculates their weights using exit position weights and tracks a
491 /// separate `exit_total`.
492 ///
493 /// Note: We deliberately don't re-normalize `weight_total` since we don't
494 /// want to lower the upper bound of other nodes. But we do want a separate
495 /// `exit_total` for use with Exit nodes.
496 pub fn repair_exits(&mut self) {
497 let old_position = self.position;
498 self.position = Position::Exit;
499 self.exit_total = 0.0;
500
501 for (i, router) in self.rstr_routers.iter().enumerate() {
502 if router.flags.contains(&"Exit".to_string()) {
503 let bw = router.measured.or(router.bandwidth).unwrap_or(0) as f64;
504 let weight = bw * self.flag_to_weight(router);
505 self.node_weights[i] = weight;
506 self.exit_total += weight;
507 }
508 }
509
510 self.position = old_position;
511 }
512
513 /// Generates a randomly selected router using bandwidth-weighted selection.
514 ///
515 /// Selection probability is proportional to each router's bandwidth weight.
516 ///
517 /// # Returns
518 ///
519 /// A reference to the selected router.
520 ///
521 /// # Errors
522 ///
523 /// Returns [`Error::NoNodesRemain`] if the router list is empty or total weight is zero.
524 pub fn generate(&self) -> Result<&RouterStatusEntry> {
525 if self.rstr_routers.is_empty() || self.weight_total <= 0.0 {
526 return Err(Error::NoNodesRemain);
527 }
528
529 let mut rng = rand::thread_rng();
530 let choice_val = rng.gen_range(0.0..self.weight_total);
531 let mut cumulative = 0.0;
532
533 for (i, weight) in self.node_weights.iter().enumerate() {
534 cumulative += weight;
535 if cumulative > choice_val {
536 return Ok(&self.rstr_routers[i]);
537 }
538 }
539
540 Ok(self.rstr_routers.last().unwrap())
541 }
542
543 /// Returns the total weight of all routers.
544 pub fn weight_total(&self) -> f64 {
545 self.weight_total
546 }
547
548 /// Returns the total weight of exit-flagged routers.
549 pub fn exit_total(&self) -> f64 {
550 self.exit_total
551 }
552
553 /// Returns the number of routers after restrictions.
554 pub fn router_count(&self) -> usize {
555 self.rstr_routers.len()
556 }
557
558 /// Returns a reference to the filtered routers.
559 pub fn routers(&self) -> &[RouterStatusEntry] {
560 &self.rstr_routers
561 }
562
563 /// Returns a reference to the node weights.
564 pub fn node_weights(&self) -> &[f64] {
565 &self.node_weights
566 }
567}
568
569#[cfg(test)]
570mod tests {
571 use super::*;
572
573 #[test]
574 fn test_valid_fingerprints() {
575 assert!(is_valid_fingerprint(
576 "AABBCCDD00112233445566778899AABBCCDDEEFF"
577 ));
578 assert!(is_valid_fingerprint(
579 "aabbccdd00112233445566778899aabbccddeeff"
580 ));
581 assert!(is_valid_fingerprint(
582 "0123456789abcdefABCDEF0123456789abcdefAB"
583 ));
584 }
585
586 #[test]
587 fn test_invalid_fingerprints() {
588 assert!(!is_valid_fingerprint(""));
589 assert!(!is_valid_fingerprint("AABBCCDD"));
590 assert!(!is_valid_fingerprint(
591 "AABBCCDD00112233445566778899AABBCCDDEEFFGG"
592 ));
593 assert!(!is_valid_fingerprint(
594 "GGHHIIJJ00112233445566778899AABBCCDDEEFF"
595 ));
596 assert!(!is_valid_fingerprint(
597 "AABBCCDD00112233445566778899AABBCCDDEEF"
598 ));
599 assert!(!is_valid_fingerprint(
600 "AABBCCDD00112233445566778899AABBCCDDEEFFF"
601 ));
602 }
603
604 #[test]
605 fn test_valid_ip_addresses() {
606 assert!(is_valid_ip_or_network("192.168.1.1"));
607 assert!(is_valid_ip_or_network("10.0.0.0"));
608 assert!(is_valid_ip_or_network("255.255.255.255"));
609 assert!(is_valid_ip_or_network("0.0.0.0"));
610 assert!(is_valid_ip_or_network("::1"));
611 assert!(is_valid_ip_or_network("2001:db8::1"));
612 assert!(is_valid_ip_or_network("fe80::1"));
613 }
614
615 #[test]
616 fn test_valid_networks() {
617 assert!(is_valid_ip_or_network("192.168.1.0/24"));
618 assert!(is_valid_ip_or_network("10.0.0.0/8"));
619 assert!(is_valid_ip_or_network("0.0.0.0/0"));
620 assert!(is_valid_ip_or_network("2001:db8::/32"));
621 assert!(is_valid_ip_or_network("::/0"));
622 }
623
624 #[test]
625 fn test_invalid_ip_or_network() {
626 assert!(!is_valid_ip_or_network(""));
627 assert!(!is_valid_ip_or_network("not-an-ip"));
628 assert!(!is_valid_ip_or_network("192.168.1.256"));
629 assert!(!is_valid_ip_or_network("192.168.1.1/33"));
630 assert!(!is_valid_ip_or_network("192.168.1"));
631 assert!(!is_valid_ip_or_network("example.com"));
632 }
633
634 #[test]
635 fn test_valid_country_codes() {
636 assert!(is_valid_country_code("US"));
637 assert!(is_valid_country_code("us"));
638 assert!(is_valid_country_code("DE"));
639 assert!(is_valid_country_code("de"));
640 assert!(is_valid_country_code("GB"));
641 assert!(is_valid_country_code("JP"));
642 }
643
644 #[test]
645 fn test_invalid_country_codes() {
646 assert!(!is_valid_country_code(""));
647 assert!(!is_valid_country_code("U"));
648 assert!(!is_valid_country_code("USA"));
649 assert!(!is_valid_country_code("U1"));
650 assert!(!is_valid_country_code("12"));
651 assert!(!is_valid_country_code("U-"));
652 }
653
654 #[test]
655 fn test_flags_restriction() {
656 use chrono::Utc;
657 use stem_rs::descriptor::router_status::RouterStatusEntryType;
658
659 let mut router = RouterStatusEntry::new(
660 RouterStatusEntryType::V3,
661 "test".to_string(),
662 "A".repeat(40),
663 Utc::now(),
664 "192.0.2.1".parse().unwrap(),
665 9001,
666 );
667 router.flags = vec![
668 "Fast".to_string(),
669 "Stable".to_string(),
670 "Valid".to_string(),
671 ];
672
673 let restriction = FlagsRestriction::new(
674 vec!["Fast".to_string(), "Stable".to_string()],
675 vec!["Authority".to_string()],
676 );
677
678 assert!(restriction.r_is_ok(&router));
679
680 router.flags.push("Authority".to_string());
681 assert!(!restriction.r_is_ok(&router));
682
683 router.flags = vec!["Fast".to_string()];
684 assert!(!restriction.r_is_ok(&router));
685 }
686
687 #[test]
688 fn test_node_restriction_list() {
689 use chrono::Utc;
690 use stem_rs::descriptor::router_status::RouterStatusEntryType;
691
692 let mut router = RouterStatusEntry::new(
693 RouterStatusEntryType::V3,
694 "test".to_string(),
695 "A".repeat(40),
696 Utc::now(),
697 "192.0.2.1".parse().unwrap(),
698 9001,
699 );
700 router.flags = vec![
701 "Fast".to_string(),
702 "Stable".to_string(),
703 "Valid".to_string(),
704 ];
705
706 let restriction1 = FlagsRestriction::new(vec!["Fast".to_string()], vec![]);
707 let restriction2 = FlagsRestriction::new(vec!["Stable".to_string()], vec![]);
708
709 let list = NodeRestrictionList::new(vec![Box::new(restriction1), Box::new(restriction2)]);
710
711 assert!(list.r_is_ok(&router));
712
713 router.flags = vec!["Fast".to_string()];
714 assert!(!list.r_is_ok(&router));
715 }
716}