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}