stem_rs/
exit_policy.rs

1//! Exit policy parsing and evaluation for Tor relays.
2//!
3//! This module provides types for parsing and evaluating Tor exit policies,
4//! which determine what traffic a relay will allow to exit the Tor network.
5//! Exit policies are a fundamental part of Tor's design, allowing relay
6//! operators to control which destinations their relay will connect to.
7//!
8//! # Overview
9//!
10//! Exit policies consist of ordered rules that are evaluated in sequence.
11//! Each rule either accepts or rejects traffic to specific addresses and ports.
12//! The first matching rule determines whether traffic is allowed.
13//!
14//! # Policy Types
15//!
16//! This module provides three main types:
17//!
18//! - [`ExitPolicy`]: A complete exit policy consisting of multiple rules
19//! - [`ExitPolicyRule`]: A single accept/reject rule with address and port specifications
20//! - [`MicroExitPolicy`]: A compact policy format used in microdescriptors
21//!
22//! # Rule Format
23//!
24//! Exit policy rules follow the format defined in the Tor directory specification:
25//!
26//! ```text
27//! accept|reject[6] addrspec:portspec
28//! ```
29//!
30//! Where:
31//! - `accept` or `reject` determines if matching traffic is allowed
32//! - `accept6` or `reject6` are IPv6-specific variants
33//! - `addrspec` is an address specification (wildcard, IPv4, or IPv6 with optional CIDR mask)
34//! - `portspec` is a port specification (wildcard, single port, or port range)
35//!
36//! # Address Specifications
37//!
38//! Address specifications can be:
39//! - `*` - matches any address (IPv4 or IPv6)
40//! - `*4` - matches any IPv4 address
41//! - `*6` - matches any IPv6 address
42//! - IPv4 address: `192.168.1.1`
43//! - IPv4 with CIDR: `10.0.0.0/8`
44//! - IPv4 with mask: `192.168.0.0/255.255.0.0`
45//! - IPv6 address: `[::1]`
46//! - IPv6 with CIDR: `[2001:db8::]/32`
47//!
48//! # Port Specifications
49//!
50//! Port specifications can be:
51//! - `*` - matches any port (1-65535)
52//! - Single port: `80`
53//! - Port range: `80-443`
54//!
55//! # Example
56//!
57//! ```rust
58//! use stem_rs::exit_policy::{ExitPolicy, MicroExitPolicy};
59//! use std::net::IpAddr;
60//!
61//! // Parse a full exit policy
62//! let policy = ExitPolicy::parse("accept *:80, accept *:443, reject *:*").unwrap();
63//!
64//! // Check if traffic can exit to a destination
65//! let addr: IpAddr = "192.168.1.1".parse().unwrap();
66//! assert!(policy.can_exit_to(addr, 80));   // HTTP allowed
67//! assert!(policy.can_exit_to(addr, 443));  // HTTPS allowed
68//! assert!(!policy.can_exit_to(addr, 22));  // SSH blocked
69//!
70//! // Get a summary of the policy
71//! assert_eq!(policy.summary(), "accept 80, 443");
72//!
73//! // Parse a microdescriptor policy (port-only)
74//! let micro = MicroExitPolicy::parse("accept 80,443").unwrap();
75//! assert!(micro.can_exit_to(80));
76//! assert!(!micro.can_exit_to(22));
77//! ```
78//!
79//! # Private and Default Rules
80//!
81//! Exit policies may contain special rule sequences:
82//!
83//! - **Private rules**: Rules expanded from the `private` keyword that block
84//!   traffic to private/internal IP ranges (10.0.0.0/8, 192.168.0.0/16, etc.)
85//! - **Default rules**: The standard suffix appended by Tor that blocks
86//!   commonly abused ports (SMTP, NetBIOS, etc.)
87//!
88//! Use [`ExitPolicy::has_private`] and [`ExitPolicy::has_default`] to check
89//! for these, and [`ExitPolicy::strip_private`] and [`ExitPolicy::strip_default`]
90//! to remove them.
91//!
92//! # See Also
93//!
94//! - [`crate::descriptor::ServerDescriptor`] - Contains relay exit policies
95//! - [`crate::descriptor::Microdescriptor`] - Contains micro exit policies
96//! - [Tor Directory Specification](https://spec.torproject.org/dir-spec) - Formal policy format
97
98use crate::Error;
99use std::fmt;
100use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
101use std::str::FromStr;
102
103/// The type of address in an exit policy rule.
104///
105/// This enum categorizes the address specification in an exit policy rule,
106/// determining how address matching is performed.
107///
108/// # Variants
109///
110/// - [`Wildcard`](AddressType::Wildcard): Matches any address (IPv4 or IPv6)
111/// - [`IPv4`](AddressType::IPv4): Matches only IPv4 addresses
112/// - [`IPv6`](AddressType::IPv6): Matches only IPv6 addresses
113///
114/// # Example
115///
116/// ```rust
117/// use stem_rs::exit_policy::{ExitPolicyRule, AddressType};
118///
119/// let rule = ExitPolicyRule::parse("accept *:80").unwrap();
120/// assert_eq!(rule.get_address_type(), AddressType::Wildcard);
121///
122/// let rule = ExitPolicyRule::parse("accept 192.168.1.1:80").unwrap();
123/// assert_eq!(rule.get_address_type(), AddressType::IPv4);
124///
125/// let rule = ExitPolicyRule::parse("accept [::1]:80").unwrap();
126/// assert_eq!(rule.get_address_type(), AddressType::IPv6);
127/// ```
128#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
129pub enum AddressType {
130    /// Matches any address, both IPv4 and IPv6.
131    ///
132    /// This is used when the address specification is `*` without
133    /// a version suffix.
134    Wildcard,
135    /// Matches only IPv4 addresses.
136    ///
137    /// This includes explicit IPv4 addresses, CIDR ranges, and the `*4` wildcard.
138    IPv4,
139    /// Matches only IPv6 addresses.
140    ///
141    /// This includes explicit IPv6 addresses (in brackets), CIDR ranges,
142    /// the `*6` wildcard, and rules using `accept6`/`reject6`.
143    IPv6,
144}
145
146/// A range of TCP/UDP ports.
147///
148/// Represents a contiguous range of ports from `min` to `max` (inclusive).
149/// Used in exit policies to specify which ports a rule applies to.
150///
151/// # Invariants
152///
153/// - `min` must be less than or equal to `max`
154/// - Valid port values are 0-65535, though port 0 is typically not used
155///
156/// # Example
157///
158/// ```rust
159/// use stem_rs::exit_policy::PortRange;
160///
161/// // Create a range for common web ports
162/// let web_ports = PortRange::new(80, 443).unwrap();
163/// assert!(web_ports.contains(80));
164/// assert!(web_ports.contains(443));
165/// assert!(web_ports.contains(200));
166/// assert!(!web_ports.contains(22));
167///
168/// // Create a single port
169/// let ssh = PortRange::single(22);
170/// assert!(ssh.contains(22));
171/// assert!(!ssh.contains(23));
172///
173/// // Create a wildcard range (all ports)
174/// let all = PortRange::all();
175/// assert!(all.is_wildcard());
176/// assert!(all.contains(1));
177/// assert!(all.contains(65535));
178/// ```
179#[derive(Debug, Clone, PartialEq, Eq)]
180pub struct PortRange {
181    /// The minimum port number (inclusive).
182    pub min: u16,
183    /// The maximum port number (inclusive).
184    pub max: u16,
185}
186
187impl PortRange {
188    /// Creates a new port range from minimum to maximum port.
189    ///
190    /// # Arguments
191    ///
192    /// * `min` - The minimum port number (inclusive)
193    /// * `max` - The maximum port number (inclusive)
194    ///
195    /// # Errors
196    ///
197    /// Returns [`Error::Parse`] if `min` is greater than `max`.
198    ///
199    /// # Example
200    ///
201    /// ```rust
202    /// use stem_rs::exit_policy::PortRange;
203    ///
204    /// let range = PortRange::new(80, 443).unwrap();
205    /// assert!(range.contains(200));
206    ///
207    /// // Invalid range (min > max)
208    /// assert!(PortRange::new(443, 80).is_err());
209    /// ```
210    pub fn new(min: u16, max: u16) -> Result<Self, Error> {
211        if min > max {
212            return Err(Error::Parse {
213                location: "port range".to_string(),
214                reason: format!("min port {} greater than max port {}", min, max),
215            });
216        }
217        Ok(Self { min, max })
218    }
219
220    /// Creates a port range containing only a single port.
221    ///
222    /// # Arguments
223    ///
224    /// * `port` - The single port number
225    ///
226    /// # Example
227    ///
228    /// ```rust
229    /// use stem_rs::exit_policy::PortRange;
230    ///
231    /// let ssh = PortRange::single(22);
232    /// assert!(ssh.contains(22));
233    /// assert!(!ssh.contains(23));
234    /// assert_eq!(ssh.to_string(), "22");
235    /// ```
236    pub fn single(port: u16) -> Self {
237        Self {
238            min: port,
239            max: port,
240        }
241    }
242
243    /// Creates a port range covering all valid ports (1-65535).
244    ///
245    /// This is equivalent to the `*` wildcard in exit policy rules.
246    ///
247    /// # Example
248    ///
249    /// ```rust
250    /// use stem_rs::exit_policy::PortRange;
251    ///
252    /// let all = PortRange::all();
253    /// assert!(all.is_wildcard());
254    /// assert!(all.contains(1));
255    /// assert!(all.contains(65535));
256    /// assert_eq!(all.to_string(), "*");
257    /// ```
258    pub fn all() -> Self {
259        Self { min: 1, max: 65535 }
260    }
261
262    /// Checks if a port is within this range.
263    ///
264    /// # Arguments
265    ///
266    /// * `port` - The port number to check
267    ///
268    /// # Returns
269    ///
270    /// `true` if `port` is between `min` and `max` (inclusive), `false` otherwise.
271    ///
272    /// # Example
273    ///
274    /// ```rust
275    /// use stem_rs::exit_policy::PortRange;
276    ///
277    /// let range = PortRange::new(80, 443).unwrap();
278    /// assert!(range.contains(80));   // min boundary
279    /// assert!(range.contains(443));  // max boundary
280    /// assert!(range.contains(200));  // middle
281    /// assert!(!range.contains(79));  // below min
282    /// assert!(!range.contains(444)); // above max
283    /// ```
284    pub fn contains(&self, port: u16) -> bool {
285        port >= self.min && port <= self.max
286    }
287
288    /// Checks if this range covers all ports (is a wildcard).
289    ///
290    /// A range is considered a wildcard if it covers ports 1-65535.
291    /// Port 0 is excluded as it's not a valid destination port.
292    ///
293    /// # Returns
294    ///
295    /// `true` if this range matches any port, `false` otherwise.
296    ///
297    /// # Example
298    ///
299    /// ```rust
300    /// use stem_rs::exit_policy::PortRange;
301    ///
302    /// assert!(PortRange::all().is_wildcard());
303    /// assert!(PortRange::new(1, 65535).unwrap().is_wildcard());
304    /// assert!(!PortRange::new(80, 443).unwrap().is_wildcard());
305    /// ```
306    pub fn is_wildcard(&self) -> bool {
307        self.min <= 1 && self.max == 65535
308    }
309}
310
311impl fmt::Display for PortRange {
312    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
313        if self.is_wildcard() {
314            write!(f, "*")
315        } else if self.min == self.max {
316            write!(f, "{}", self.min)
317        } else {
318            write!(f, "{}-{}", self.min, self.max)
319        }
320    }
321}
322
323/// A single rule in an exit policy.
324///
325/// Each rule specifies whether to accept or reject traffic to a particular
326/// address and port combination. Rules are evaluated in order, and the first
327/// matching rule determines whether traffic is allowed.
328///
329/// # Rule Format
330///
331/// Rules follow the Tor exit policy format:
332///
333/// ```text
334/// accept|reject[6] addrspec:portspec
335/// ```
336///
337/// # Matching Semantics
338///
339/// A rule matches a destination if:
340/// 1. The destination address matches the rule's address specification
341///    (considering CIDR masks and address family)
342/// 2. The destination port is within the rule's port range
343///
344/// # Address Family Matching
345///
346/// - Wildcard (`*`) rules match both IPv4 and IPv6 addresses
347/// - IPv4-specific rules (`*4`, explicit IPv4) only match IPv4 addresses
348/// - IPv6-specific rules (`*6`, explicit IPv6, `accept6`/`reject6`) only match IPv6
349///
350/// # Example
351///
352/// ```rust
353/// use stem_rs::exit_policy::ExitPolicyRule;
354/// use std::net::IpAddr;
355///
356/// // Parse a rule that accepts HTTP traffic
357/// let rule = ExitPolicyRule::parse("accept *:80").unwrap();
358/// assert!(rule.is_accept);
359/// assert!(rule.is_address_wildcard());
360/// assert!(!rule.is_port_wildcard());
361///
362/// // Check if the rule matches a destination
363/// let addr: IpAddr = "192.168.1.1".parse().unwrap();
364/// assert!(rule.is_match(Some(addr), Some(80)));
365/// assert!(!rule.is_match(Some(addr), Some(443)));
366///
367/// // Parse a CIDR rule
368/// let rule = ExitPolicyRule::parse("reject 10.0.0.0/8:*").unwrap();
369/// assert!(!rule.is_accept);
370/// assert_eq!(rule.get_masked_bits(), Some(8));
371/// ```
372///
373/// # See Also
374///
375/// - [`ExitPolicy`]: A collection of rules forming a complete policy
376/// - [`MicroExitPolicy`]: A compact port-only policy format
377#[derive(Debug, Clone, PartialEq, Eq)]
378pub struct ExitPolicyRule {
379    /// Whether this rule accepts (`true`) or rejects (`false`) matching traffic.
380    pub is_accept: bool,
381    address: Option<IpAddr>,
382    mask_bits: Option<u8>,
383    address_type: AddressType,
384    /// The minimum port number this rule applies to (inclusive).
385    pub min_port: u16,
386    /// The maximum port number this rule applies to (inclusive).
387    pub max_port: u16,
388    is_default: bool,
389    is_private: bool,
390}
391
392impl ExitPolicyRule {
393    /// Parses an exit policy rule from a string.
394    ///
395    /// The rule must follow the Tor exit policy format:
396    ///
397    /// ```text
398    /// accept|reject[6] addrspec:portspec
399    /// ```
400    ///
401    /// # Arguments
402    ///
403    /// * `rule` - The rule string to parse
404    ///
405    /// # Supported Formats
406    ///
407    /// ## Actions
408    /// - `accept` - Allow matching traffic
409    /// - `reject` - Block matching traffic
410    /// - `accept6` - Allow matching IPv6 traffic only
411    /// - `reject6` - Block matching IPv6 traffic only
412    ///
413    /// ## Address Specifications
414    /// - `*` - Any address (IPv4 or IPv6)
415    /// - `*4` - Any IPv4 address
416    /// - `*6` - Any IPv6 address
417    /// - `A.B.C.D` - Specific IPv4 address
418    /// - `A.B.C.D/N` - IPv4 CIDR notation (N = 0-32)
419    /// - `A.B.C.D/M.M.M.M` - IPv4 with subnet mask
420    /// - `[IPv6]` - Specific IPv6 address
421    /// - `[IPv6]/N` - IPv6 CIDR notation (N = 0-128)
422    ///
423    /// ## Port Specifications
424    /// - `*` - Any port (1-65535)
425    /// - `N` - Single port
426    /// - `N-M` - Port range (inclusive)
427    ///
428    /// # Errors
429    ///
430    /// Returns [`Error::Parse`] if:
431    /// - The rule doesn't start with `accept` or `reject`
432    /// - The address specification is invalid
433    /// - The port specification is invalid
434    /// - The CIDR mask is out of range
435    /// - The port range is invalid (min > max)
436    ///
437    /// # Example
438    ///
439    /// ```rust
440    /// use stem_rs::exit_policy::ExitPolicyRule;
441    ///
442    /// // Various valid rule formats
443    /// let rule = ExitPolicyRule::parse("accept *:80").unwrap();
444    /// let rule = ExitPolicyRule::parse("reject 10.0.0.0/8:*").unwrap();
445    /// let rule = ExitPolicyRule::parse("accept 192.168.0.0/255.255.0.0:80-443").unwrap();
446    /// let rule = ExitPolicyRule::parse("accept6 [::1]:22").unwrap();
447    /// let rule = ExitPolicyRule::parse("reject [2001:db8::]/32:*").unwrap();
448    ///
449    /// // Invalid rules
450    /// assert!(ExitPolicyRule::parse("allow *:80").is_err());  // Invalid action
451    /// assert!(ExitPolicyRule::parse("accept *").is_err());    // Missing port
452    /// assert!(ExitPolicyRule::parse("accept *:443-80").is_err()); // Invalid range
453    /// ```
454    pub fn parse(rule: &str) -> Result<Self, Error> {
455        let rule = rule.trim();
456        let (is_accept, exitpattern) = if let Some(rest) = rule.strip_prefix("accept6 ") {
457            (true, rest.trim())
458        } else if let Some(rest) = rule.strip_prefix("reject6 ") {
459            (false, rest.trim())
460        } else if let Some(rest) = rule.strip_prefix("accept ") {
461            (true, rest.trim())
462        } else if let Some(rest) = rule.strip_prefix("reject ") {
463            (false, rest.trim())
464        } else {
465            return Err(Error::Parse {
466                location: rule.to_string(),
467                reason: "rule must start with accept/reject".to_string(),
468            });
469        };
470
471        let is_ipv6_only = rule.starts_with("accept6") || rule.starts_with("reject6");
472        let (addrspec, portspec) = Self::split_addr_port(exitpattern)?;
473        let (address, mask_bits, address_type) = Self::parse_addrspec(addrspec, is_ipv6_only)?;
474        let (min_port, max_port) = Self::parse_portspec(portspec)?;
475
476        Ok(Self {
477            is_accept,
478            address,
479            mask_bits,
480            address_type,
481            min_port,
482            max_port,
483            is_default: false,
484            is_private: false,
485        })
486    }
487
488    /// Splits an exit pattern into address and port specifications.
489    ///
490    /// Handles both IPv4 and IPv6 address formats. IPv6 addresses are enclosed
491    /// in brackets, so the colon separator must be found after the closing bracket.
492    ///
493    /// # Arguments
494    ///
495    /// * `exitpattern` - The exit pattern string (e.g., "192.168.1.1:80" or "[::1]:80")
496    ///
497    /// # Returns
498    ///
499    /// A tuple of (addrspec, portspec) on success.
500    ///
501    /// # Errors
502    ///
503    /// Returns [`Error::Parse`] if the pattern is malformed.
504    fn split_addr_port(exitpattern: &str) -> Result<(&str, &str), Error> {
505        if exitpattern.contains('[') {
506            if let Some(bracket_end) = exitpattern.find(']') {
507                let after_bracket = &exitpattern[bracket_end + 1..];
508                if let Some(colon_pos) = after_bracket.find(':') {
509                    let addrspec = &exitpattern[..bracket_end + 1 + colon_pos];
510                    let portspec = &after_bracket[colon_pos + 1..];
511                    return Ok((addrspec, portspec));
512                }
513            }
514            return Err(Error::Parse {
515                location: exitpattern.to_string(),
516                reason: "malformed IPv6 address".to_string(),
517            });
518        }
519
520        if let Some(colon_pos) = exitpattern.rfind(':') {
521            let addrspec = &exitpattern[..colon_pos];
522            let portspec = &exitpattern[colon_pos + 1..];
523            Ok((addrspec, portspec))
524        } else {
525            Err(Error::Parse {
526                location: exitpattern.to_string(),
527                reason: "exitpattern must be addrspec:portspec".to_string(),
528            })
529        }
530    }
531
532    /// Parses an address specification into its components.
533    ///
534    /// Handles wildcards (`*`, `*4`, `*6`), IPv4 addresses with optional CIDR
535    /// or subnet mask notation, and IPv6 addresses with optional CIDR notation.
536    ///
537    /// # Arguments
538    ///
539    /// * `addrspec` - The address specification string
540    /// * `is_ipv6_only` - Whether this is from an `accept6`/`reject6` rule
541    ///
542    /// # Returns
543    ///
544    /// A tuple of (address, mask_bits, address_type) on success.
545    /// - `address` is `None` for wildcards
546    /// - `mask_bits` is `None` for wildcards, otherwise the CIDR prefix length
547    /// - `address_type` indicates the type of address
548    ///
549    /// # Errors
550    ///
551    /// Returns [`Error::Parse`] if the address specification is invalid.
552    fn parse_addrspec(
553        addrspec: &str,
554        is_ipv6_only: bool,
555    ) -> Result<(Option<IpAddr>, Option<u8>, AddressType), Error> {
556        let addrspec = if addrspec == "*4" {
557            "0.0.0.0/0"
558        } else if addrspec == "*6" || (addrspec == "*" && is_ipv6_only) {
559            "[::]/0"
560        } else {
561            addrspec
562        };
563
564        if addrspec == "*" {
565            return Ok((None, None, AddressType::Wildcard));
566        }
567
568        let (addr_part, mask_part) = if let Some(slash_pos) = addrspec.rfind('/') {
569            (&addrspec[..slash_pos], Some(&addrspec[slash_pos + 1..]))
570        } else {
571            (addrspec, None)
572        };
573
574        if addr_part.starts_with('[') && addr_part.ends_with(']') {
575            let ipv6_str = &addr_part[1..addr_part.len() - 1];
576            let ipv6: Ipv6Addr = ipv6_str.parse().map_err(|_| Error::Parse {
577                location: addr_part.to_string(),
578                reason: "invalid IPv6 address".to_string(),
579            })?;
580
581            let mask_bits = match mask_part {
582                Some(m) => {
583                    let bits: u8 = m.parse().map_err(|_| Error::Parse {
584                        location: m.to_string(),
585                        reason: "invalid mask bits".to_string(),
586                    })?;
587                    if bits > 128 {
588                        return Err(Error::Parse {
589                            location: m.to_string(),
590                            reason: "IPv6 mask must be 0-128".to_string(),
591                        });
592                    }
593                    bits
594                }
595                None => 128,
596            };
597
598            return Ok((Some(IpAddr::V6(ipv6)), Some(mask_bits), AddressType::IPv6));
599        }
600
601        if let Ok(ipv4) = addr_part.parse::<Ipv4Addr>() {
602            let mask_bits = match mask_part {
603                Some(m) => {
604                    if let Ok(bits) = m.parse::<u8>() {
605                        if bits > 32 {
606                            return Err(Error::Parse {
607                                location: m.to_string(),
608                                reason: "IPv4 mask must be 0-32".to_string(),
609                            });
610                        }
611                        bits
612                    } else if let Ok(mask_addr) = m.parse::<Ipv4Addr>() {
613                        Self::ipv4_mask_to_bits(mask_addr)?
614                    } else {
615                        return Err(Error::Parse {
616                            location: m.to_string(),
617                            reason: "invalid mask".to_string(),
618                        });
619                    }
620                }
621                None => 32,
622            };
623
624            return Ok((Some(IpAddr::V4(ipv4)), Some(mask_bits), AddressType::IPv4));
625        }
626
627        Err(Error::Parse {
628            location: addrspec.to_string(),
629            reason: "not a valid address".to_string(),
630        })
631    }
632
633    /// Converts an IPv4 subnet mask to CIDR bit count.
634    ///
635    /// Takes a subnet mask in dotted-quad notation (e.g., `255.255.0.0`) and
636    /// converts it to the equivalent CIDR prefix length (e.g., `16`).
637    ///
638    /// # Arguments
639    ///
640    /// * `mask` - The subnet mask as an IPv4 address
641    ///
642    /// # Returns
643    ///
644    /// The number of leading 1-bits in the mask (0-32).
645    ///
646    /// # Errors
647    ///
648    /// Returns [`Error::Parse`] if the mask is not a valid subnet mask
649    /// (i.e., not a contiguous sequence of 1-bits followed by 0-bits).
650    /// For example, `255.255.0.255` is invalid.
651    fn ipv4_mask_to_bits(mask: Ipv4Addr) -> Result<u8, Error> {
652        let mask_u32 = u32::from_be_bytes(mask.octets());
653        if mask_u32 == 0 {
654            return Ok(0);
655        }
656        let leading_ones = mask_u32.leading_ones();
657        let expected = if leading_ones == 32 {
658            u32::MAX
659        } else {
660            !((1u32 << (32 - leading_ones)) - 1)
661        };
662        if mask_u32 != expected {
663            return Err(Error::Parse {
664                location: mask.to_string(),
665                reason: "mask cannot be represented as bit count".to_string(),
666            });
667        }
668        Ok(leading_ones as u8)
669    }
670
671    /// Parses a port specification into min and max port values.
672    ///
673    /// Handles wildcards (`*`), single ports, and port ranges.
674    ///
675    /// # Arguments
676    ///
677    /// * `portspec` - The port specification string (e.g., "*", "80", "80-443")
678    ///
679    /// # Returns
680    ///
681    /// A tuple of (min_port, max_port) on success.
682    /// For single ports, min and max are the same.
683    /// For wildcards, returns (1, 65535).
684    ///
685    /// # Errors
686    ///
687    /// Returns [`Error::Parse`] if:
688    /// - The port number is not a valid u16
689    /// - The port range has min > max
690    fn parse_portspec(portspec: &str) -> Result<(u16, u16), Error> {
691        if portspec == "*" {
692            return Ok((1, 65535));
693        }
694
695        if let Some(dash_pos) = portspec.find('-') {
696            let min_str = &portspec[..dash_pos];
697            let max_str = &portspec[dash_pos + 1..];
698            let min: u16 = min_str.parse().map_err(|_| Error::Parse {
699                location: portspec.to_string(),
700                reason: "invalid min port".to_string(),
701            })?;
702            let max: u16 = max_str.parse().map_err(|_| Error::Parse {
703                location: portspec.to_string(),
704                reason: "invalid max port".to_string(),
705            })?;
706            if min > max {
707                return Err(Error::Parse {
708                    location: portspec.to_string(),
709                    reason: "min port greater than max port".to_string(),
710                });
711            }
712            return Ok((min, max));
713        }
714
715        let port: u16 = portspec.parse().map_err(|_| Error::Parse {
716            location: portspec.to_string(),
717            reason: "invalid port".to_string(),
718        })?;
719        Ok((port, port))
720    }
721
722    /// Checks if this rule matches a given destination.
723    ///
724    /// A rule matches if both the address and port match the rule's specifications.
725    /// If either the address or port is `None`, the rule performs a "fuzzy" match
726    /// where the missing component is considered to potentially match.
727    ///
728    /// This is equivalent to calling [`is_match_strict`](Self::is_match_strict)
729    /// with `strict = false`.
730    ///
731    /// # Arguments
732    ///
733    /// * `address` - The destination IP address, or `None` to match any address
734    /// * `port` - The destination port, or `None` to match any port
735    ///
736    /// # Returns
737    ///
738    /// `true` if the rule matches the destination, `false` otherwise.
739    ///
740    /// # Matching Rules
741    ///
742    /// - If the rule has a wildcard address, any address matches
743    /// - If the rule has a specific address/CIDR, only addresses in that range match
744    /// - IPv4 rules don't match IPv6 addresses and vice versa
745    /// - If the rule has a wildcard port, any port matches
746    /// - If the rule has a specific port range, only ports in that range match
747    ///
748    /// # Example
749    ///
750    /// ```rust
751    /// use stem_rs::exit_policy::ExitPolicyRule;
752    /// use std::net::IpAddr;
753    ///
754    /// let rule = ExitPolicyRule::parse("accept 10.0.0.0/8:80-443").unwrap();
755    ///
756    /// // Exact match
757    /// let addr: IpAddr = "10.1.2.3".parse().unwrap();
758    /// assert!(rule.is_match(Some(addr), Some(80)));
759    /// assert!(rule.is_match(Some(addr), Some(443)));
760    /// assert!(!rule.is_match(Some(addr), Some(22)));
761    ///
762    /// // Address outside CIDR range
763    /// let addr: IpAddr = "192.168.1.1".parse().unwrap();
764    /// assert!(!rule.is_match(Some(addr), Some(80)));
765    ///
766    /// // Fuzzy match (None means "any")
767    /// assert!(rule.is_match(None, Some(80)));  // Any address, port 80
768    /// ```
769    pub fn is_match(&self, address: Option<IpAddr>, port: Option<u16>) -> bool {
770        self.is_match_strict(address, port, false)
771    }
772
773    /// Checks if this rule matches a destination with strict mode option.
774    ///
775    /// Similar to [`is_match`](Self::is_match), but with control over how
776    /// fuzzy matches (when address or port is `None`) are handled.
777    ///
778    /// # Arguments
779    ///
780    /// * `address` - The destination IP address, or `None` to match any address
781    /// * `port` - The destination port, or `None` to match any port
782    /// * `strict` - Controls fuzzy match behavior:
783    ///   - `false`: Fuzzy matches return `true` for accept rules, `false` for reject
784    ///   - `true`: Fuzzy matches return `false` for accept rules, `true` for reject
785    ///
786    /// # Strict Mode Semantics
787    ///
788    /// When `strict = true`, the question becomes "does this rule match ALL
789    /// possible values for the missing component?" rather than "does this rule
790    /// match ANY possible value?"
791    ///
792    /// This is useful for determining if traffic can definitely exit (strict=true)
793    /// versus if traffic might be able to exit (strict=false).
794    ///
795    /// # Example
796    ///
797    /// ```rust
798    /// use stem_rs::exit_policy::ExitPolicyRule;
799    ///
800    /// let accept_rule = ExitPolicyRule::parse("accept 10.0.0.0/8:80").unwrap();
801    ///
802    /// // Non-strict: "can ANY address on port 80 match?"
803    /// assert!(accept_rule.is_match_strict(None, Some(80), false));
804    ///
805    /// // Strict: "do ALL addresses on port 80 match?"
806    /// assert!(!accept_rule.is_match_strict(None, Some(80), true));
807    /// ```
808    pub fn is_match_strict(
809        &self,
810        address: Option<IpAddr>,
811        port: Option<u16>,
812        strict: bool,
813    ) -> bool {
814        let mut fuzzy_match = false;
815
816        if !self.is_address_wildcard() {
817            match address {
818                None => fuzzy_match = true,
819                Some(addr) => {
820                    if let Some(rule_addr) = &self.address {
821                        if !Self::same_address_family(&addr, rule_addr) {
822                            return false;
823                        }
824                    }
825                    if !self.address_in_network(addr) {
826                        return false;
827                    }
828                }
829            }
830        }
831
832        if !self.is_port_wildcard() {
833            match port {
834                None => fuzzy_match = true,
835                Some(p) => {
836                    if p < self.min_port || p > self.max_port {
837                        return false;
838                    }
839                }
840            }
841        }
842
843        if fuzzy_match {
844            strict != self.is_accept
845        } else {
846            true
847        }
848    }
849
850    /// Checks if two IP addresses are in the same address family.
851    ///
852    /// Returns `true` if both addresses are IPv4 or both are IPv6.
853    ///
854    /// # Arguments
855    ///
856    /// * `a` - First IP address
857    /// * `b` - Second IP address
858    ///
859    /// # Returns
860    ///
861    /// `true` if both addresses are the same family, `false` otherwise.
862    fn same_address_family(a: &IpAddr, b: &IpAddr) -> bool {
863        matches!(
864            (a, b),
865            (IpAddr::V4(_), IpAddr::V4(_)) | (IpAddr::V6(_), IpAddr::V6(_))
866        )
867    }
868
869    /// Checks if an address falls within this rule's network range.
870    ///
871    /// Applies the rule's subnet mask to both the rule's address and the
872    /// test address, then compares the masked values.
873    ///
874    /// # Arguments
875    ///
876    /// * `addr` - The IP address to check
877    ///
878    /// # Returns
879    ///
880    /// `true` if the address is within the rule's network, `false` otherwise.
881    /// Returns `true` if the rule has no address (wildcard) or no mask.
882    fn address_in_network(&self, addr: IpAddr) -> bool {
883        let Some(rule_addr) = &self.address else {
884            return true;
885        };
886        let Some(mask_bits) = self.mask_bits else {
887            return true;
888        };
889
890        match (addr, rule_addr) {
891            (IpAddr::V4(a), IpAddr::V4(r)) => {
892                let a_u32 = u32::from_be_bytes(a.octets());
893                let r_u32 = u32::from_be_bytes(r.octets());
894                let mask = if mask_bits == 0 {
895                    0
896                } else {
897                    !((1u32 << (32 - mask_bits)) - 1)
898                };
899                (a_u32 & mask) == (r_u32 & mask)
900            }
901            (IpAddr::V6(a), IpAddr::V6(r)) => {
902                let a_u128 = u128::from_be_bytes(a.octets());
903                let r_u128 = u128::from_be_bytes(r.octets());
904                let mask = if mask_bits == 0 {
905                    0
906                } else {
907                    !((1u128 << (128 - mask_bits)) - 1)
908                };
909                (a_u128 & mask) == (r_u128 & mask)
910            }
911            _ => false,
912        }
913    }
914
915    /// Checks if this rule matches any address (is an address wildcard).
916    ///
917    /// A rule is an address wildcard if its address specification is `*`,
918    /// which matches both IPv4 and IPv6 addresses.
919    ///
920    /// Note that `*4` and `*6` are NOT considered wildcards by this method,
921    /// as they only match one address family.
922    ///
923    /// # Returns
924    ///
925    /// `true` if the rule matches any address, `false` otherwise.
926    ///
927    /// # Example
928    ///
929    /// ```rust
930    /// use stem_rs::exit_policy::ExitPolicyRule;
931    ///
932    /// assert!(ExitPolicyRule::parse("accept *:80").unwrap().is_address_wildcard());
933    /// assert!(!ExitPolicyRule::parse("accept *4:80").unwrap().is_address_wildcard());
934    /// assert!(!ExitPolicyRule::parse("accept *6:80").unwrap().is_address_wildcard());
935    /// assert!(!ExitPolicyRule::parse("accept 10.0.0.0/8:80").unwrap().is_address_wildcard());
936    /// ```
937    pub fn is_address_wildcard(&self) -> bool {
938        self.address_type == AddressType::Wildcard
939    }
940
941    /// Checks if this rule matches any port (is a port wildcard).
942    ///
943    /// A rule is a port wildcard if its port specification covers all valid
944    /// ports (1-65535), typically written as `*` in the rule string.
945    ///
946    /// # Returns
947    ///
948    /// `true` if the rule matches any port, `false` otherwise.
949    ///
950    /// # Example
951    ///
952    /// ```rust
953    /// use stem_rs::exit_policy::ExitPolicyRule;
954    ///
955    /// assert!(ExitPolicyRule::parse("accept *:*").unwrap().is_port_wildcard());
956    /// assert!(ExitPolicyRule::parse("accept *:1-65535").unwrap().is_port_wildcard());
957    /// assert!(!ExitPolicyRule::parse("accept *:80").unwrap().is_port_wildcard());
958    /// assert!(!ExitPolicyRule::parse("accept *:80-443").unwrap().is_port_wildcard());
959    /// ```
960    pub fn is_port_wildcard(&self) -> bool {
961        self.min_port <= 1 && self.max_port == 65535
962    }
963
964    /// Returns the address type of this rule.
965    ///
966    /// # Returns
967    ///
968    /// The [`AddressType`] indicating whether this rule matches:
969    /// - [`AddressType::Wildcard`]: Any address (IPv4 or IPv6)
970    /// - [`AddressType::IPv4`]: Only IPv4 addresses
971    /// - [`AddressType::IPv6`]: Only IPv6 addresses
972    ///
973    /// # Example
974    ///
975    /// ```rust
976    /// use stem_rs::exit_policy::{ExitPolicyRule, AddressType};
977    ///
978    /// let rule = ExitPolicyRule::parse("accept *:80").unwrap();
979    /// assert_eq!(rule.get_address_type(), AddressType::Wildcard);
980    ///
981    /// let rule = ExitPolicyRule::parse("accept 192.168.1.1:80").unwrap();
982    /// assert_eq!(rule.get_address_type(), AddressType::IPv4);
983    ///
984    /// let rule = ExitPolicyRule::parse("accept [::1]:80").unwrap();
985    /// assert_eq!(rule.get_address_type(), AddressType::IPv6);
986    /// ```
987    pub fn get_address_type(&self) -> AddressType {
988        self.address_type
989    }
990
991    /// Returns the subnet mask as an IP address.
992    ///
993    /// For IPv4 rules, returns the mask in dotted-quad notation (e.g., `255.255.0.0`).
994    /// For IPv6 rules, returns the mask as an IPv6 address.
995    /// For wildcard rules, returns `None`.
996    ///
997    /// # Returns
998    ///
999    /// The subnet mask as an [`IpAddr`], or `None` for wildcard rules.
1000    ///
1001    /// # Example
1002    ///
1003    /// ```rust
1004    /// use stem_rs::exit_policy::ExitPolicyRule;
1005    /// use std::net::IpAddr;
1006    ///
1007    /// let rule = ExitPolicyRule::parse("accept 192.168.0.0/16:*").unwrap();
1008    /// assert_eq!(rule.get_mask(), Some("255.255.0.0".parse::<IpAddr>().unwrap()));
1009    ///
1010    /// let rule = ExitPolicyRule::parse("accept 10.0.0.0/8:*").unwrap();
1011    /// assert_eq!(rule.get_mask(), Some("255.0.0.0".parse::<IpAddr>().unwrap()));
1012    ///
1013    /// let rule = ExitPolicyRule::parse("accept *:80").unwrap();
1014    /// assert_eq!(rule.get_mask(), None);
1015    /// ```
1016    pub fn get_mask(&self) -> Option<IpAddr> {
1017        let bits = self.mask_bits?;
1018        match self.address_type {
1019            AddressType::Wildcard => None,
1020            AddressType::IPv4 => {
1021                let mask = if bits == 0 {
1022                    0u32
1023                } else {
1024                    !((1u32 << (32 - bits)) - 1)
1025                };
1026                Some(IpAddr::V4(Ipv4Addr::from(mask)))
1027            }
1028            AddressType::IPv6 => {
1029                let mask = if bits == 0 {
1030                    0u128
1031                } else {
1032                    !((1u128 << (128 - bits)) - 1)
1033                };
1034                Some(IpAddr::V6(Ipv6Addr::from(mask)))
1035            }
1036        }
1037    }
1038
1039    /// Returns the number of bits in the subnet mask.
1040    ///
1041    /// For CIDR notation like `10.0.0.0/8`, this returns `8`.
1042    /// For specific addresses without a mask, returns the full mask (32 for IPv4, 128 for IPv6).
1043    /// For wildcard rules, returns `None`.
1044    ///
1045    /// # Returns
1046    ///
1047    /// The number of mask bits, or `None` for wildcard rules.
1048    ///
1049    /// # Example
1050    ///
1051    /// ```rust
1052    /// use stem_rs::exit_policy::ExitPolicyRule;
1053    ///
1054    /// let rule = ExitPolicyRule::parse("accept 10.0.0.0/8:*").unwrap();
1055    /// assert_eq!(rule.get_masked_bits(), Some(8));
1056    ///
1057    /// let rule = ExitPolicyRule::parse("accept 192.168.1.1:80").unwrap();
1058    /// assert_eq!(rule.get_masked_bits(), Some(32));  // Full mask for specific address
1059    ///
1060    /// let rule = ExitPolicyRule::parse("accept *:80").unwrap();
1061    /// assert_eq!(rule.get_masked_bits(), None);
1062    /// ```
1063    pub fn get_masked_bits(&self) -> Option<u8> {
1064        self.mask_bits
1065    }
1066
1067    /// Checks if this rule is part of Tor's default exit policy suffix.
1068    ///
1069    /// Tor appends a default policy suffix that blocks commonly abused ports
1070    /// (SMTP, NetBIOS, etc.) and then accepts all other traffic. This method
1071    /// returns `true` if this rule was identified as part of that suffix.
1072    ///
1073    /// # Returns
1074    ///
1075    /// `true` if this rule is part of the default policy suffix, `false` otherwise.
1076    ///
1077    /// # See Also
1078    ///
1079    /// - [`ExitPolicy::has_default`]: Check if a policy contains default rules
1080    /// - [`ExitPolicy::strip_default`]: Remove default rules from a policy
1081    pub fn is_default(&self) -> bool {
1082        self.is_default
1083    }
1084
1085    /// Checks if this rule was expanded from the `private` keyword.
1086    ///
1087    /// The `private` keyword in Tor exit policies expands to rules blocking
1088    /// traffic to private/internal IP ranges (10.0.0.0/8, 192.168.0.0/16,
1089    /// 127.0.0.0/8, etc.). This method returns `true` if this rule was
1090    /// identified as part of that expansion.
1091    ///
1092    /// # Returns
1093    ///
1094    /// `true` if this rule was expanded from `private`, `false` otherwise.
1095    ///
1096    /// # See Also
1097    ///
1098    /// - [`ExitPolicy::has_private`]: Check if a policy contains private rules
1099    /// - [`ExitPolicy::strip_private`]: Remove private rules from a policy
1100    pub fn is_private(&self) -> bool {
1101        self.is_private
1102    }
1103
1104    /// Returns the IP address this rule applies to.
1105    ///
1106    /// For rules with a specific address or CIDR range, returns the base address.
1107    /// For wildcard rules, returns `None`.
1108    ///
1109    /// # Returns
1110    ///
1111    /// The IP address, or `None` for wildcard rules.
1112    ///
1113    /// # Example
1114    ///
1115    /// ```rust
1116    /// use stem_rs::exit_policy::ExitPolicyRule;
1117    /// use std::net::IpAddr;
1118    ///
1119    /// let rule = ExitPolicyRule::parse("accept 192.168.1.1:80").unwrap();
1120    /// assert_eq!(rule.address(), Some("192.168.1.1".parse::<IpAddr>().unwrap()));
1121    ///
1122    /// let rule = ExitPolicyRule::parse("accept 10.0.0.0/8:*").unwrap();
1123    /// assert_eq!(rule.address(), Some("10.0.0.0".parse::<IpAddr>().unwrap()));
1124    ///
1125    /// let rule = ExitPolicyRule::parse("accept *:80").unwrap();
1126    /// assert_eq!(rule.address(), None);
1127    /// ```
1128    pub fn address(&self) -> Option<IpAddr> {
1129        self.address
1130    }
1131
1132    /// Sets whether this rule is part of Tor's default exiffix.
1133    ///
1134    /// This is called internally during policy construction when detecting
1135    /// the default policy suffix.
1136    ///
1137    /// # Arguments
1138    ///
1139    /// * `is_default` - Whether this rule is part of the default suffix
1140    fn set_default(&mut self, is_default: bool) {
1141        self.is_default = is_default;
1142    }
1143
1144    /// Sets whether this rule was expanded from the `private` keyword.
1145    ///
1146    /// This is called internally during policy construction when detecting
1147    /// private address rules.
1148    ///
1149    /// # Arguments
1150    ///
1151    /// * `is_private` - Whether this rule was expanded from `private`
1152    fn set_private(&mut self, is_private: bool) {
1153        self.is_private = is_private;
1154    }
1155}
1156
1157impl fmt::Display for ExitPolicyRule {
1158    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1159        let action = if self.is_accept { "accept" } else { "reject" };
1160        write!(f, "{} ", action)?;
1161
1162        match (&self.address, self.address_type) {
1163            (None, AddressType::Wildcard) => write!(f, "*")?,
1164            (Some(IpAddr::V4(addr)), _) => {
1165                write!(f, "{}", addr)?;
1166                if let Some(bits) = self.mask_bits {
1167                    if bits != 32 {
1168                        write!(f, "/{}", bits)?;
1169                    }
1170                }
1171            }
1172            (Some(IpAddr::V6(addr)), _) => {
1173                write!(f, "[{}]", addr)?;
1174                if let Some(bits) = self.mask_bits {
1175                    if bits != 128 {
1176                        write!(f, "/{}", bits)?;
1177                    }
1178                }
1179            }
1180            _ => write!(f, "*")?,
1181        }
1182
1183        write!(f, ":")?;
1184
1185        if self.is_port_wildcard() {
1186            write!(f, "*")
1187        } else if self.min_port == self.max_port {
1188            write!(f, "{}", self.min_port)
1189        } else {
1190            write!(f, "{}-{}", self.min_port, self.max_port)
1191        }
1192    }
1193}
1194
1195impl FromStr for ExitPolicyRule {
1196    type Err = Error;
1197
1198    fn from_str(s: &str) -> Result<Self, Self::Err> {
1199        Self::parse(s)
1200    }
1201}
1202
1203const PRIVATE_ADDRESSES: &[&str] = &[
1204    "0.0.0.0/8",
1205    "169.254.0.0/16",
1206    "127.0.0.0/8",
1207    "192.168.0.0/16",
1208    "10.0.0.0/8",
1209    "172.16.0.0/12",
1210];
1211
1212const DEFAULT_POLICY_RULES: &[&str] = &[
1213    "reject *:25",
1214    "reject *:119",
1215    "reject *:135-139",
1216    "reject *:445",
1217    "reject *:563",
1218    "reject *:1214",
1219    "reject *:4661-4666",
1220    "reject *:6346-6429",
1221    "reject *:6699",
1222    "reject *:6881-6999",
1223    "accept *:*",
1224];
1225
1226/// A complete exit policy consisting of multiple rules.
1227///
1228/// An exit policy is an ordered list of [`ExitPolicyRule`]s that determine
1229/// whether a Tor relay will allow traffic to exit to a given destination.
1230/// Rules are evaluated in order, and the first matching rule determines
1231/// whether traffic is allowed.
1232///
1233/// # Rule Evaluation
1234///
1235/// When checking if traffic can exit to a destination:
1236/// 1. Each rule is checked in order
1237/// 2. The first rule that matches the destination determines the result
1238/// 3. If no rule matches, the default is to allow traffic
1239///
1240/// # Special Rule Sequences
1241///
1242/// Exit policies may contain special rule sequences that are automatically
1243/// detected:
1244///
1245/// - **Private rules**: Rules blocking traffic to private/internal IP ranges
1246///   (expanded from the `private` keyword in torrc)
1247/// - **Default rules**: Tor's standard suffix blocking commonly abused ports
1248///
1249/// # Example
1250///
1251/// ```rust
1252/// use stem_rs::exit_policy::ExitPolicy;
1253/// use std::net::IpAddr;
1254///
1255/// // Parse a policy that allows only web traffic
1256/// let policy = ExitPolicy::parse("accept *:80, accept *:443, reject *:*").unwrap();
1257///
1258/// let addr: IpAddr = "192.168.1.1".parse().unwrap();
1259/// assert!(policy.can_exit_to(addr, 80));   // HTTP allowed
1260/// assert!(policy.can_exit_to(addr, 443));  // HTTPS allowed
1261/// assert!(!policy.can_exit_to(addr, 22));  // SSH blocked
1262///
1263/// // Get a summary
1264/// assert_eq!(policy.summary(), "accept 80, 443");
1265///
1266/// // Check if any exiting is allowed
1267/// assert!(policy.is_exiting_allowed());
1268///
1269/// // A reject-all policy
1270/// let reject_all = ExitPolicy::parse("reject *:*").unwrap();
1271/// assert!(!reject_all.is_exiting_allowed());
1272/// ```
1273///
1274/// # See Also
1275///
1276/// - [`ExitPolicyRule`]: Individual rules that make up a policy
1277/// - [`MicroExitPolicy`]: Compact port-only policy format
1278#[derive(Debug, Clone, PartialEq, Eq)]
1279pub struct ExitPolicy {
1280    rules: Vec<ExitPolicyRule>,
1281    is_allowed_default: bool,
1282}
1283
1284impl ExitPolicy {
1285    /// Creates a new exit policy from a vector of rules.
1286    ///
1287    /// The rules are automatically analyzed to detect private and default
1288    /// rule sequences.
1289    ///
1290    /// # Arguments
1291    ///
1292    /// * `rules` - The rules that make up this policy
1293    ///
1294    /// # Example
1295    ///
1296    /// ```rust
1297    /// use stem_rs::exit_policy::{ExitPolicy, ExitPolicyRule};
1298    ///
1299    /// let rules = vec![
1300    ///     ExitPolicyRule::parse("accept *:80").unwrap(),
1301    ///     ExitPolicyRule::parse("reject *:*").unwrap(),
1302    /// ];
1303    /// let policy = ExitPolicy::new(rules);
1304    /// assert_eq!(policy.len(), 2);
1305    /// ```
1306    pub fn new(rules: Vec<ExitPolicyRule>) -> Self {
1307        let mut policy = Self {
1308            rules,
1309            is_allowed_default: true,
1310        };
1311        policy.flag_private_rules();
1312        policy.flag_default_rules();
1313        policy
1314    }
1315
1316    /// Parses an exit policy from a string.
1317    ///
1318    /// The string can contain multiple rules separated by commas or newlines.
1319    /// Rules after a catch-all rule (`accept *:*` or `reject *:*`) are ignored.
1320    ///
1321    /// # Arguments
1322    ///
1323    /// * `content` - The policy string to parse
1324    ///
1325    /// # Errors
1326    ///
1327    /// Returns [`Error::Parse`] if any rule in the policy is invalid.
1328    ///
1329    /// # Example
1330    ///
1331    /// ```rust
1332    /// use stem_rs::exit_policy::ExitPolicy;
1333    ///
1334    /// // Comma-separated rules
1335    /// let policy = ExitPolicy::parse("accept *:80, accept *:443, reject *:*").unwrap();
1336    /// assert_eq!(policy.len(), 3);
1337    ///
1338    /// // Newline-separated rules
1339    /// let policy = ExitPolicy::parse("accept *:80\naccept *:443\nreject *:*").unwrap();
1340    /// assert_eq!(policy.len(), 3);
1341    ///
1342    /// // Rules after catch-all are ignored
1343    /// let policy = ExitPolicy::parse("reject *:*, accept *:80").unwrap();
1344    /// assert_eq!(policy.len(), 1);  // Only the reject rule
1345    /// ```
1346    pub fn parse(content: &str) -> Result<Self, Error> {
1347        let mut rules = Vec::new();
1348        let delimiter = if content.contains('\n') { '\n' } else { ',' };
1349        for rule_str in content.split(delimiter) {
1350            let rule_str = rule_str.trim();
1351            if rule_str.is_empty() {
1352                continue;
1353            }
1354            let rule = ExitPolicyRule::parse(rule_str)?;
1355            let is_catch_all = rule.is_address_wildcard() && rule.is_port_wildcard();
1356            rules.push(rule);
1357            if is_catch_all {
1358                break;
1359            }
1360        }
1361        Ok(Self::new(rules))
1362    }
1363
1364    /// Creates an exit policy from a slice of rule strings.
1365    ///
1366    /// Similar to [`parse`](Self::parse), but takes individual rule strings
1367    /// instead of a single concatenated string.
1368    ///
1369    /// # Arguments
1370    ///
1371    /// * `rules` - Slice of rule strings
1372    ///
1373    /// # Errors
1374    ///
1375    /// Returns [`Error::Parse`] if any rule is invalid.
1376    ///
1377    /// # Example
1378    ///
1379    /// ```rust
1380    /// use stem_rs::exit_policy::ExitPolicy;
1381    ///
1382    /// let policy = ExitPolicy::from_rules(&[
1383    ///     "accept *:80",
1384    ///     "accept *:443",
1385    ///     "reject *:*",
1386    /// ]).unwrap();
1387    /// assert_eq!(policy.len(), 3);
1388    /// ```
1389    pub fn from_rules<S: AsRef<str>>(rules: &[S]) -> Result<Self, Error> {
1390        let mut parsed_rules = Vec::new();
1391        for rule_str in rules {
1392            let rule_str = rule_str.as_ref().trim();
1393            if rule_str.is_empty() {
1394                continue;
1395            }
1396            let rule = ExitPolicyRule::parse(rule_str)?;
1397            let is_catch_all = rule.is_address_wildcard() && rule.is_port_wildcard();
1398            parsed_rules.push(rule);
1399            if is_catch_all {
1400                break;
1401            }
1402        }
1403        Ok(Self::new(parsed_rules))
1404    }
1405
1406    /// Detects and flags rules that were expanded from the `private` keyword.
1407    ///
1408    /// Scans the policy for sequences of rulesatch the private address
1409    /// ranges (0.0.0.0/8, 169.254.0.0/16, 127.0.0.0/8, 192.168.0.0/16,
1410    /// 10.0.0.0/8, 172.16.0.0/12) with the same port range and action.
1411    ///
1412    /// When found, marks those rules as private via [`ExitPolicyRule::set_private`].
1413    /// Also marks the following rule if it appears to be the relay's public IP.
1414    fn flag_private_rules(&mut self) {
1415        if self.rules.len() < PRIVATE_ADDRESSES.len() {
1416            return;
1417        }
1418
1419        for start_idx in 0..=self.rules.len() - PRIVATE_ADDRESSES.len() {
1420            let mut is_match = true;
1421            let first_rule = &self.rules[start_idx];
1422            let min_port = first_rule.min_port;
1423            let max_port = first_rule.max_port;
1424            let is_accept = first_rule.is_accept;
1425
1426            for (i, private_addr) in PRIVATE_ADDRESSES.iter().enumerate() {
1427                let rule = &self.rules[start_idx + i];
1428                if rule.min_port != min_port
1429                    || rule.max_port != max_port
1430                    || rule.is_accept != is_accept
1431                {
1432                    is_match = false;
1433                    break;
1434                }
1435
1436                let expected = format!(
1437                    "{} {}:{}",
1438                    if is_accept { "accept" } else { "reject" },
1439                    private_addr,
1440                    if min_port == max_port {
1441                        format!("{}", min_port)
1442                    } else if min_port <= 1 && max_port == 65535 {
1443                        "*".to_string()
1444                    } else {
1445                        format!("{}-{}", min_port, max_port)
1446                    }
1447                );
1448
1449                if let Ok(expected_rule) = ExitPolicyRule::parse(&expected) {
1450                    if rule.address != expected_rule.address
1451                        || rule.mask_bits != expected_rule.mask_bits
1452                    {
1453                        is_match = false;
1454                        break;
1455                    }
1456                } else {
1457                    is_match = false;
1458                    break;
1459                }
1460            }
1461
1462            if is_match {
1463                for i in 0..PRIVATE_ADDRESSES.len() {
1464                    self.rules[start_idx + i].set_private(true);
1465                }
1466
1467                let next_idx = start_idx + PRIVATE_ADDRESSES.len();
1468                if next_idx < self.rules.len() {
1469                    let next_rule = &self.rules[next_idx];
1470                    if !next_rule.is_address_wildcard()
1471                        && next_rule.min_port == min_port
1472                        && next_rule.max_port == max_port
1473                        && next_rule.is_accept == is_accept
1474                    {
1475                        self.rules[next_idx].set_private(true);
1476                    }
1477                }
1478            }
1479        }
1480    }
1481
1482    /// Detects and flags rules that match Tor's default exit policy suffix.
1483    ///
1484    /// Checks if the policy ends with the standard default suffix that Tor
1485    /// appends to exit policies. The default suffix blocks commonly abused
1486    /// ports (SMTP port 25, NNTP port 119, NetBIOS ports 135-139, etc.)
1487    /// and then accepts all other traffic.
1488    ///
1489    /// When found, marks those rules as default via [`ExitPolicyRule::set_default`].
1490    fn flag_default_rules(&mut self) {
1491        if self.rules.len() < DEFAULT_POLICY_RULES.len() {
1492            return;
1493        }
1494
1495        let start_idx = self.rules.len() - DEFAULT_POLICY_RULES.len();
1496        let mut is_match = true;
1497
1498        for (i, default_rule_str) in DEFAULT_POLICY_RULES.iter().enumerate() {
1499            if let Ok(default_rule) = ExitPolicyRule::parse(default_rule_str) {
1500                let rule = &self.rules[start_idx + i];
1501                if rule.is_accept != default_rule.is_accept
1502                    || rule.address != default_rule.address
1503                    || rule.mask_bits != default_rule.mask_bits
1504                    || rule.min_port != default_rule.min_port
1505                    || rule.max_port != default_rule.max_port
1506                {
1507                    is_match = false;
1508                    break;
1509                }
1510            } else {
1511                is_match = false;
1512                break;
1513            }
1514        }
1515
1516        if is_match {
1517            for i in 0..DEFAULT_POLICY_RULES.len() {
1518                self.rules[start_idx + i].set_default(true);
1519            }
1520        }
1521    }
1522
1523    /// Checks if traffic can exit to a specific destination.
1524    ///
1525    /// Evaluates the policy rules in order and returns whether traffic to
1526    /// the given address and port is allowed.
1527    ///
1528    /// # Arguments
1529    ///
1530    /// * `address` - The destination IP address
1531    /// * `port` - The destination port
1532    ///
1533    /// # Returns
1534    ///
1535    /// `true` if traffic to this destination is allowed, `false` otherwise.
1536    ///
1537    /// # Example
1538    ///
1539    /// ```rust
1540    /// use stem_rs::exit_policy::ExitPolicy;
1541    /// use std::net::IpAddr;
1542    ///
1543    /// let policy = ExitPolicy::parse("accept *:80, accept *:443, reject *:*").unwrap();
1544    /// let addr: IpAddr = "192.168.1.1".parse().unwrap();
1545    ///
1546    /// assert!(policy.can_exit_to(addr, 80));   // HTTP allowed
1547    /// assert!(policy.can_exit_to(addr, 443));  // HTTPS allowed
1548    /// assert!(!policy.can_exit_to(addr, 22));  // SSH blocked
1549    /// ```
1550    pub fn can_exit_to(&self, address: IpAddr, port: u16) -> bool {
1551        self.can_exit_to_optional(Some(address), Some(port), false)
1552    }
1553
1554    /// Checks if traffic can exit to a destination with optional parameters.
1555    ///
1556    /// Similar to [`can_exit_to`](Self::can_exit_to), but allows omitting
1557    /// the address or port to check if traffic to ANY matching destination
1558    /// is allowed.
1559    ///
1560    /// # Arguments
1561    ///
1562    /// * `address` - The destination IP address, or `None` for any address
1563    /// * `port` - The destination port, or `None` for any port
1564    /// * `strict` - If `true`, checks if ALL matching destinations are allowed;
1565    ///   if `false`, checks if ANY matching destination is allowed
1566    ///
1567    /// # Returns
1568    ///
1569    /// `true` if traffic is allowed according to the strict mode, `false` otherwise.
1570    ///
1571    /// # Example
1572    ///
1573    /// ```rust
1574    /// use stem_rs::exit_policy::ExitPolicy;
1575    ///
1576    /// let policy = ExitPolicy::parse("reject 10.0.0.0/8:80, accept *:*").unwrap();
1577    ///
1578    /// // Non-strict: Can ANY address exit on port 80?
1579    /// assert!(policy.can_exit_to_optional(None, Some(80), false));
1580    ///
1581    /// // Strict: Can ALL addresses exit on port 80?
1582    /// assert!(!policy.can_exit_to_optional(None, Some(80), true));
1583    /// ```
1584    pub fn can_exit_to_optional(
1585        &self,
1586        address: Option<IpAddr>,
1587        port: Option<u16>,
1588        strict: bool,
1589    ) -> bool {
1590        if !self.is_exiting_allowed() {
1591            return false;
1592        }
1593
1594        for rule in &self.rules {
1595            if rule.is_match_strict(address, port, strict) {
1596                return rule.is_accept;
1597            }
1598        }
1599
1600        self.is_allowed_default
1601    }
1602
1603    /// Checks if this policy allows any exiting at all.
1604    ///
1605    /// Returns `false` if the policy effectively blocks all traffic
1606    /// (e.g., `reject *:*` with no prior accept rules).
1607    ///
1608    /// # Returns
1609    ///
1610    /// `true` if at least some traffic can exit, `false` if all traffic is blocked.
1611    ///
1612    /// # Example
1613    ///
1614    /// ```rust
1615    /// use stem_rs::exit_policy::ExitPolicy;
1616    ///
1617    /// let policy = ExitPolicy::parse("accept *:80, reject *:*").unwrap();
1618    /// assert!(policy.is_exiting_allowed());
1619    ///
1620    /// let policy = ExitPolicy::parse("reject *:*").unwrap();
1621    /// assert!(!policy.is_exiting_allowed());
1622    ///
1623    /// // Empty policy allows by default
1624    /// let policy = ExitPolicy::parse("").unwrap();
1625    /// assert!(policy.is_exiting_allowed());
1626    /// ```
1627    pub fn is_exiting_allowed(&self) -> bool {
1628        let mut rejected_ports = std::collections::HashSet::new();
1629
1630        for rule in &self.rules {
1631            if rule.is_accept {
1632                for port in rule.min_port..=rule.max_port {
1633                    if !rejected_ports.contains(&port) {
1634                        return true;
1635                    }
1636                }
1637            } else if rule.is_address_wildcard() {
1638                if rule.is_port_wildcard() {
1639                    return false;
1640                }
1641                for port in rule.min_port..=rule.max_port {
1642                    rejected_ports.insert(port);
1643                }
1644            }
1645        }
1646
1647        self.is_allowed_default
1648    }
1649
1650    /// Returns a short summary of the policy, similar to a microdescriptor.
1651    ///
1652    /// The summary shows which ports are accepted or rejected, ignoring
1653    /// address-specific rules. This is useful for quickly understanding
1654    /// what a relay allows.
1655    ///
1656    /// # Returns
1657    ///
1658    /// A string like `"accept 80, 443"` or `"reject 1-1024"`.
1659    ///
1660    /// # Example
1661    ///
1662    /// ```rust
1663    /// use stem_rs::exit_policy::ExitPolicy;
1664    ///
1665    /// let policy = ExitPolicy::parse("accept *:80, accept *:443, reject *:*").unwrap();
1666    /// assert_eq!(policy.summary(), "accept 80, 443");
1667    ///
1668    /// let policy = ExitPolicy::parse("accept *:443, reject *:1-1024, accept *:*").unwrap();
1669    /// assert_eq!(policy.summary(), "reject 1-442, 444-1024");
1670    /// ```
1671    pub fn summary(&self) -> String {
1672        let mut is_whitelist = !self.is_allowed_default;
1673
1674        for rule in &self.rules {
1675            if rule.is_address_wildcard() && rule.is_port_wildcard() {
1676                is_whitelist = !rule.is_accept;
1677                break;
1678            }
1679        }
1680
1681        let mut display_ports = Vec::new();
1682        let mut skip_ports = std::collections::HashSet::new();
1683
1684        for rule in &self.rules {
1685            if !rule.is_address_wildcard() {
1686                continue;
1687            }
1688            if rule.is_port_wildcard() {
1689                break;
1690            }
1691
1692            for port in rule.min_port..=rule.max_port {
1693                if skip_ports.contains(&port) {
1694                    continue;
1695                }
1696                if rule.is_accept == is_whitelist {
1697                    display_ports.push(port);
1698                }
1699                skip_ports.insert(port);
1700            }
1701        }
1702
1703        let display_ranges = if display_ports.is_empty() {
1704            is_whitelist = !is_whitelist;
1705            vec!["1-65535".to_string()]
1706        } else {
1707            display_ports.sort();
1708            Self::ports_to_ranges(&display_ports)
1709        };
1710
1711        let prefix = if is_whitelist { "accept " } else { "reject " };
1712        format!("{}{}", prefix, display_ranges.join(", "))
1713    }
1714
1715    /// Converts a sorted list of ports into a list of range strings.
1716    ///
1717    /// Groups consecutive ports into ranges for compact display.
1718    /// For example, `[80, 81, 82, 443]` becomes `["80-82", "443"]`.
1719    ///
1720    /// # Arguments
1721    ///
1722    /// * `ports` - A sorted slice of port numbers
1723    ///
1724    /// # Returns
1725    ///
1726    /// A vector of range strings (e.g., "80", "80-443").
1727    fn ports_to_ranges(ports: &[u16]) -> Vec<String> {
1728        if ports.is_empty() {
1729            return vec![];
1730        }
1731
1732        let mut ranges = Vec::new();
1733        let mut range_start = ports[0];
1734        let mut range_end = ports[0];
1735
1736        for &port in &ports[1..] {
1737            if port == range_end + 1 {
1738                range_end = port;
1739            } else {
1740                if range_start == range_end {
1741                    ranges.push(format!("{}", range_start));
1742                } else {
1743                    ranges.push(format!("{}-{}", range_start, range_end));
1744                }
1745                range_start = port;
1746                range_end = port;
1747            }
1748        }
1749
1750        if range_start == range_end {
1751            ranges.push(format!("{}", range_start));
1752        } else {
1753            ranges.push(format!("{}-{}", range_start, range_end));
1754        }
1755
1756        ranges
1757    }
1758
1759    /// Checks if this policy contains rules expanded from the `private` keyword.
1760    ///
1761    /// The `private` keyword in Tor exit policies expands to rules blocking
1762    /// traffic to private/internal IP ranges. This method detects if such
1763    /// rules are present.
1764    ///
1765    /// # Returns
1766    ///
1767    /// `true` if the policy contains private rules, `false` otherwise.
1768    ///
1769    /// # See Also
1770    ///
1771    /// - [`strip_private`](Self::strip_private): Remove private rules
1772    /// - [`ExitPolicyRule::is_private`]: Check individual rules
1773    pub fn has_private(&self) -> bool {
1774        self.rules.iter().any(|r| r.is_private())
1775    }
1776
1777    /// Returns a copy of this policy without private rules.
1778    ///
1779    /// Creates a new policy with all rules expanded from the `private`
1780    /// keyword removed.
1781    ///
1782    /// # Returns
1783    ///
1784    /// A new [`ExitPolicy`] without private rules.
1785    ///
1786    /// # Example
1787    ///
1788    /// ```rust
1789    /// use stem_rs::exit_policy::ExitPolicy;
1790    ///
1791    /// let policy = ExitPolicy::parse("accept *:80, reject *:*").unwrap();
1792    /// let stripped = policy.strip_private();
1793    /// assert!(!stripped.has_private());
1794    /// ```
1795    pub fn strip_private(&self) -> Self {
1796        let rules: Vec<_> = self
1797            .rules
1798            .iter()
1799            .filter(|r| !r.is_private())
1800            .cloned()
1801            .collect();
1802        Self {
1803            rules,
1804            is_allowed_default: self.is_allowed_default,
1805        }
1806    }
1807
1808    /// Checks if this policy contains Tor's default exit policy suffix.
1809    ///
1810    /// Tor appends a default suffix to exit policies that blocks commonly
1811    /// abused ports (SMTP, NetBIOS, etc.) and then accepts all other traffic.
1812    ///
1813    /// # Returns
1814    ///
1815    /// `true` if the policy ends with the default suffix, `false` otherwise.
1816    ///
1817    /// # See Also
1818    ///
1819    /// - [`strip_default`](Self::strip_default): Remove default rules
1820    /// - [`ExitPolicyRule::is_default`]: Check individual rules
1821    pub fn has_default(&self) -> bool {
1822        self.rules.iter().any(|r| r.is_default())
1823    }
1824
1825    /// Returns a copy of this policy without the default suffix.
1826    ///
1827    /// Creates a new policy with Tor's default exit policy suffix removed.
1828    ///
1829    /// # Returns
1830    ///
1831    /// A new [`ExitPolicy`] without default rules.
1832    ///
1833    /// # Example
1834    ///
1835    /// ```rust
1836    /// use stem_rs::exit_policy::ExitPolicy;
1837    ///
1838    /// let policy = ExitPolicy::parse("accept *:80, reject *:*").unwrap();
1839    /// let stripped = policy.strip_default();
1840    /// assert!(!stripped.has_default());
1841    /// ```
1842    pub fn strip_default(&self) -> Self {
1843        let rules: Vec<_> = self
1844            .rules
1845            .iter()
1846            .filter(|r| !r.is_default())
1847            .cloned()
1848            .collect();
1849        Self {
1850            rules,
1851            is_allowed_default: self.is_allowed_default,
1852        }
1853    }
1854
1855    /// Returns an iterator over the rules in this policy.
1856    ///
1857    /// Rules are yielded in evaluation order (first rule first).
1858    ///
1859    /// # Example
1860    ///
1861    /// ```rust
1862    /// use stem_rs::exit_policy::ExitPolicy;
1863    ///
1864    /// let policy = ExitPolicy::parse("accept *:80, reject *:*").unwrap();
1865    /// for rule in policy.iter() {
1866    ///     println!("{}", rule);
1867    /// }
1868    /// ```
1869    pub fn iter(&self) -> impl Iterator<Item = &ExitPolicyRule> {
1870        self.rules.iter()
1871    }
1872
1873    /// Returns a slice of all rules in this policy.
1874    ///
1875    /// # Example
1876    ///
1877    /// ```rust
1878    /// use stem_rs::exit_policy::ExitPolicy;
1879    ///
1880    /// let policy = ExitPolicy::parse("accept *:80, reject *:*").unwrap();
1881    /// let rules = policy.rules();
1882    /// assert_eq!(rules.len(), 2);
1883    /// ```
1884    pub fn rules(&self) -> &[ExitPolicyRule] {
1885        &self.rules
1886    }
1887
1888    /// Returns the number of rules in this policy.
1889    ///
1890    /// # Example
1891    ///
1892    /// ```rust
1893    /// use stem_rs::exit_policy::ExitPolicy;
1894    ///
1895    /// let policy = ExitPolicy::parse("accept *:80, reject *:*").unwrap();
1896    /// assert_eq!(policy.len(), 2);
1897    /// ```
1898    pub fn len(&self) -> usize {
1899        self.rules.len()
1900    }
1901
1902    /// Checks if this policy has no rules.
1903    ///
1904    /// An empty policy allows all traffic by default.
1905    ///
1906    /// # Example
1907    ///
1908    /// ```rust
1909    /// use stem_rs::exit_policy::ExitPolicy;
1910    ///
1911    /// let policy = ExitPolicy::parse("").unwrap();
1912    /// assert!(policy.is_empty());
1913    ///
1914    /// let policy = ExitPolicy::parse("accept *:80").unwrap();
1915    /// assert!(!policy.is_empty());
1916    /// ```
1917    pub fn is_empty(&self) -> bool {
1918        self.rules.is_empty()
1919    }
1920}
1921
1922impl fmt::Display for ExitPolicy {
1923    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1924        let rules: Vec<String> = self.rules.iter().map(|r| r.to_string()).collect();
1925        write!(f, "{}", rules.join(", "))
1926    }
1927}
1928
1929impl FromStr for ExitPolicy {
1930    type Err = Error;
1931
1932    fn from_str(s: &str) -> Result<Self, Self::Err> {
1933        Self::parse(s)
1934    }
1935}
1936
1937/// A compact exit policy used in microdescriptors.
1938///
1939/// Microdescriptor exit policies are a simplified form of exit policy that
1940/// only specify ports, not addresses. They are used in Tor's microdescriptor
1941/// format to provide a compact representation of a relay's exit policy.
1942///
1943/// # Format
1944///
1945/// Micro exit policies have the format:
1946///
1947/// ```text
1948/// accept|reject port[,port...]
1949/// ```
1950///
1951/// Where each port can be a single port or a range (e.g., `80-443`).
1952///
1953/// # Matching Semantics
1954///
1955/// - `accept` policies allow traffic to the listed ports
1956/// - `reject` policies block traffic to the listed ports (allowing all others)
1957///
1958/// Since micro policies don't include address information, clients can only
1959/// guess whether a relay will accept their traffic. If the guess is wrong,
1960/// the relay will return an end-reason-exit-policy error.
1961///
1962/// # Example
1963///
1964/// ```rust
1965/// use stem_rs::exit_policy::MicroExitPolicy;
1966///
1967/// // Accept only web ports
1968/// let policy = MicroExitPolicy::parse("accept 80,443").unwrap();
1969/// assert!(policy.can_exit_to(80));
1970/// assert!(policy.can_exit_to(443));
1971/// assert!(!policy.can_exit_to(22));
1972///
1973/// // Reject privileged ports
1974/// let policy = MicroExitPolicy::parse("reject 1-1024").unwrap();
1975/// assert!(!policy.can_exit_to(80));
1976/// assert!(policy.can_exit_to(8080));
1977/// ```
1978///
1979/// # See Also
1980///
1981/// - [`ExitPolicy`]: Full exit policy with address support
1982/// - [`crate::descriptor::Microdescriptor`]: Contains micro exit policies
1983#[derive(Debug, Clone, PartialEq, Eq)]
1984pub struct MicroExitPolicy {
1985    /// Whether this policy accepts (`true`) or rejects (`false`) the listed ports.
1986    pub is_accept: bool,
1987    /// The port ranges this policy applies to.
1988    pub ports: Vec<PortRange>,
1989}
1990
1991impl MicroExitPolicy {
1992    /// Parses a micro exit policy from a string.
1993    ///
1994    /// The string must follow the microdescriptor policy format:
1995    ///
1996    /// ```text
1997    /// accept|reject port[,port...]
1998    /// ```
1999    ///
2000    /// # Arguments
2001    ///
2002    /// * `content` - The policy string to parse
2003    ///
2004    /// # Supported Formats
2005    ///
2006    /// - Single port: `accept 80`
2007    /// - Multiple ports: `accept 80,443`
2008    /// - Port range: `reject 1-1024`
2009    /// - Mixed: `accept 80,443,8080-8090`
2010    /// - Wildcard: `accept *` (all ports)
2011    ///
2012    /// # Errors
2013    ///
2014    /// Returns [`Error::Parse`] if:
2015    /// - The policy doesn't start with `accept` or `reject`
2016    /// - A port number is invalid
2017    /// - A port range is invalid (min > max)
2018    ///
2019    /// # Example
2020    ///
2021    /// ```rust
2022    /// use stem_rs::exit_policy::MicroExitPolicy;
2023    ///
2024    /// let policy = MicroExitPolicy::parse("accept 80,443").unwrap();
2025    /// assert!(policy.is_accept);
2026    /// assert_eq!(policy.ports.len(), 2);
2027    ///
2028    /// let policy = MicroExitPolicy::parse("reject 1-1024").unwrap();
2029    /// assert!(!policy.is_accept);
2030    ///
2031    /// // Invalid policies
2032    /// assert!(MicroExitPolicy::parse("allow 80").is_err());
2033    /// assert!(MicroExitPolicy::parse("80,443").is_err());
2034    /// ```
2035    pub fn parse(content: &str) -> Result<Self, Error> {
2036        let content = content.trim();
2037        let (is_accept, port_list) = if let Some(rest) = content.strip_prefix("accept ") {
2038            (true, rest.trim())
2039        } else if let Some(rest) = content.strip_prefix("reject ") {
2040            (false, rest.trim())
2041        } else {
2042            return Err(Error::Parse {
2043                location: content.to_string(),
2044                reason: "microdescriptor policy must start with accept/reject".to_string(),
2045            });
2046        };
2047
2048        let mut ports = Vec::new();
2049        for port_entry in port_list.split(',') {
2050            let port_entry = port_entry.trim();
2051            if port_entry.is_empty() {
2052                continue;
2053            }
2054
2055            let range = if port_entry == "*" {
2056                PortRange::new(1, 65535)?
2057            } else if let Some(dash_pos) = port_entry.find('-') {
2058                let min_str = &port_entry[..dash_pos];
2059                let max_str = &port_entry[dash_pos + 1..];
2060                let min: u16 = min_str.parse().map_err(|_| Error::Parse {
2061                    location: port_entry.to_string(),
2062                    reason: "invalid min port".to_string(),
2063                })?;
2064                let max: u16 = max_str.parse().map_err(|_| Error::Parse {
2065                    location: port_entry.to_string(),
2066                    reason: "invalid max port".to_string(),
2067                })?;
2068                PortRange::new(min, max)?
2069            } else {
2070                let port: u16 = port_entry.parse().map_err(|_| Error::Parse {
2071                    location: port_entry.to_string(),
2072                    reason: "invalid port".to_string(),
2073                })?;
2074                PortRange::single(port)
2075            };
2076            ports.push(range);
2077        }
2078
2079        Ok(Self { is_accept, ports })
2080    }
2081
2082    /// Checks if traffic can exit to a specific port.
2083    ///
2084    /// For `accept` policies, returns `true` if the port is in the list.
2085    /// For `reject` policies, returns `true` if the port is NOT in the list.
2086    ///
2087    /// # Arguments
2088    ///
2089    /// * `port` - The destination port to check
2090    ///
2091    /// # Returns
2092    ///
2093    /// `true` if traffic to this port is allowed, `false` otherwise.
2094    ///
2095    /// # Example
2096    ///
2097    /// ```rust
2098    /// use stem_rs::exit_policy::MicroExitPolicy;
2099    ///
2100    /// // Accept policy: only listed ports are allowed
2101    /// let policy = MicroExitPolicy::parse("accept 80,443").unwrap();
2102    /// assert!(policy.can_exit_to(80));
2103    /// assert!(policy.can_exit_to(443));
2104    /// assert!(!policy.can_exit_to(22));
2105    ///
2106    /// // Reject policy: listed ports are blocked, others allowed
2107    /// let policy = MicroExitPolicy::parse("reject 1-1024").unwrap();
2108    /// assert!(!policy.can_exit_to(80));
2109    /// assert!(!policy.can_exit_to(443));
2110    /// assert!(policy.can_exit_to(8080));
2111    /// ```
2112    pub fn can_exit_to(&self, port: u16) -> bool {
2113        let matches = self.ports.iter().any(|r| r.contains(port));
2114        if self.is_accept {
2115            matches
2116        } else {
2117            !matches
2118        }
2119    }
2120}
2121
2122impl fmt::Display for MicroExitPolicy {
2123    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2124        let action = if self.is_accept { "accept" } else { "reject" };
2125        let ports: Vec<String> = self.ports.iter().map(|r| r.to_string()).collect();
2126        write!(f, "{} {}", action, ports.join(","))
2127    }
2128}
2129
2130impl FromStr for MicroExitPolicy {
2131    type Err = Error;
2132
2133    fn from_str(s: &str) -> Result<Self, Self::Err> {
2134        Self::parse(s)
2135    }
2136}
2137
2138#[cfg(test)]
2139mod tests {
2140    use super::*;
2141
2142    #[test]
2143    fn test_parse_accept_all() {
2144        let rule = ExitPolicyRule::parse("accept *:*").unwrap();
2145        assert!(rule.is_accept);
2146        assert!(rule.is_address_wildcard());
2147        assert!(rule.is_port_wildcard());
2148    }
2149
2150    #[test]
2151    fn test_parse_reject_all() {
2152        let rule = ExitPolicyRule::parse("reject *:*").unwrap();
2153        assert!(!rule.is_accept);
2154        assert!(rule.is_address_wildcard());
2155        assert!(rule.is_port_wildcard());
2156    }
2157
2158    #[test]
2159    fn test_parse_specific_port() {
2160        let rule = ExitPolicyRule::parse("accept *:80").unwrap();
2161        assert!(rule.is_accept);
2162        assert!(rule.is_address_wildcard());
2163        assert_eq!(rule.min_port, 80);
2164        assert_eq!(rule.max_port, 80);
2165    }
2166
2167    #[test]
2168    fn test_parse_port_range() {
2169        let rule = ExitPolicyRule::parse("reject *:80-443").unwrap();
2170        assert!(!rule.is_accept);
2171        assert_eq!(rule.min_port, 80);
2172        assert_eq!(rule.max_port, 443);
2173    }
2174
2175    #[test]
2176    fn test_parse_ipv4_address() {
2177        let rule = ExitPolicyRule::parse("accept 192.168.1.1:80").unwrap();
2178        assert!(rule.is_accept);
2179        assert_eq!(
2180            rule.address(),
2181            Some(IpAddr::V4("192.168.1.1".parse().unwrap()))
2182        );
2183        assert_eq!(rule.get_masked_bits(), Some(32));
2184    }
2185
2186    #[test]
2187    fn test_parse_ipv4_cidr() {
2188        let rule = ExitPolicyRule::parse("reject 10.0.0.0/8:*").unwrap();
2189        assert!(!rule.is_accept);
2190        assert_eq!(
2191            rule.address(),
2192            Some(IpAddr::V4("10.0.0.0".parse().unwrap()))
2193        );
2194        assert_eq!(rule.get_masked_bits(), Some(8));
2195    }
2196
2197    #[test]
2198    fn test_parse_ipv6_address() {
2199        let rule = ExitPolicyRule::parse("accept [::1]:80").unwrap();
2200        assert!(rule.is_accept);
2201        assert!(matches!(rule.address(), Some(IpAddr::V6(_))));
2202        assert_eq!(rule.get_masked_bits(), Some(128));
2203    }
2204
2205    #[test]
2206    fn test_parse_ipv6_cidr() {
2207        let rule = ExitPolicyRule::parse("reject [2001:db8::]/32:*").unwrap();
2208        assert!(!rule.is_accept);
2209        assert_eq!(rule.get_masked_bits(), Some(32));
2210    }
2211
2212    #[test]
2213    fn test_rule_match_wildcard() {
2214        let rule = ExitPolicyRule::parse("accept *:80").unwrap();
2215        assert!(rule.is_match(Some("192.168.1.1".parse().unwrap()), Some(80)));
2216        assert!(rule.is_match(Some("10.0.0.1".parse().unwrap()), Some(80)));
2217        assert!(!rule.is_match(Some("192.168.1.1".parse().unwrap()), Some(443)));
2218    }
2219
2220    #[test]
2221    fn test_rule_match_cidr() {
2222        let rule = ExitPolicyRule::parse("accept 10.0.0.0/8:*").unwrap();
2223        assert!(rule.is_match(Some("10.0.0.1".parse().unwrap()), Some(80)));
2224        assert!(rule.is_match(Some("10.255.255.255".parse().unwrap()), Some(80)));
2225        assert!(!rule.is_match(Some("192.168.1.1".parse().unwrap()), Some(80)));
2226    }
2227
2228    #[test]
2229    fn test_rule_match_port_range() {
2230        let rule = ExitPolicyRule::parse("accept *:80-443").unwrap();
2231        assert!(rule.is_match(Some("192.168.1.1".parse().unwrap()), Some(80)));
2232        assert!(rule.is_match(Some("192.168.1.1".parse().unwrap()), Some(443)));
2233        assert!(rule.is_match(Some("192.168.1.1".parse().unwrap()), Some(200)));
2234        assert!(!rule.is_match(Some("192.168.1.1".parse().unwrap()), Some(79)));
2235        assert!(!rule.is_match(Some("192.168.1.1".parse().unwrap()), Some(444)));
2236    }
2237
2238    #[test]
2239    fn test_policy_can_exit_to() {
2240        let policy = ExitPolicy::parse("accept *:80, accept *:443, reject *:*").unwrap();
2241        assert!(policy.can_exit_to("192.168.1.1".parse().unwrap(), 80));
2242        assert!(policy.can_exit_to("192.168.1.1".parse().unwrap(), 443));
2243        assert!(!policy.can_exit_to("192.168.1.1".parse().unwrap(), 22));
2244    }
2245
2246    #[test]
2247    fn test_policy_is_exiting_allowed() {
2248        let policy = ExitPolicy::parse("accept *:80, reject *:*").unwrap();
2249        assert!(policy.is_exiting_allowed());
2250
2251        let policy = ExitPolicy::parse("reject *:*").unwrap();
2252        assert!(!policy.is_exiting_allowed());
2253    }
2254
2255    #[test]
2256    fn test_policy_summary() {
2257        let policy = ExitPolicy::parse("accept *:80, accept *:443, reject *:*").unwrap();
2258        assert_eq!(policy.summary(), "accept 80, 443");
2259
2260        let policy = ExitPolicy::parse("accept *:443, reject *:1-1024, accept *:*").unwrap();
2261        assert_eq!(policy.summary(), "reject 1-442, 444-1024");
2262    }
2263
2264    #[test]
2265    fn test_micro_exit_policy_parse() {
2266        let policy = MicroExitPolicy::parse("accept 80,443").unwrap();
2267        assert!(policy.is_accept);
2268        assert_eq!(policy.ports.len(), 2);
2269    }
2270
2271    #[test]
2272    fn test_micro_exit_policy_can_exit_to() {
2273        let policy = MicroExitPolicy::parse("accept 80,443").unwrap();
2274        assert!(policy.can_exit_to(80));
2275        assert!(policy.can_exit_to(443));
2276        assert!(!policy.can_exit_to(22));
2277
2278        let policy = MicroExitPolicy::parse("reject 1-1024").unwrap();
2279        assert!(!policy.can_exit_to(80));
2280        assert!(policy.can_exit_to(8080));
2281    }
2282
2283    #[test]
2284    fn test_rule_display() {
2285        let rule = ExitPolicyRule::parse("accept *:80").unwrap();
2286        assert_eq!(rule.to_string(), "accept *:80");
2287
2288        let rule = ExitPolicyRule::parse("reject 10.0.0.0/8:*").unwrap();
2289        assert_eq!(rule.to_string(), "reject 10.0.0.0/8:*");
2290
2291        let rule = ExitPolicyRule::parse("accept *:80-443").unwrap();
2292        assert_eq!(rule.to_string(), "accept *:80-443");
2293    }
2294
2295    #[test]
2296    fn test_policy_display() {
2297        let policy = ExitPolicy::parse("accept *:80, reject *:*").unwrap();
2298        assert_eq!(policy.to_string(), "accept *:80, reject *:*");
2299    }
2300
2301    #[test]
2302    fn test_micro_policy_display() {
2303        let policy = MicroExitPolicy::parse("accept 80,443").unwrap();
2304        assert_eq!(policy.to_string(), "accept 80,443");
2305    }
2306
2307    #[test]
2308    fn test_ipv4_mask_notation() {
2309        let rule = ExitPolicyRule::parse("accept 192.168.0.0/255.255.0.0:*").unwrap();
2310        assert_eq!(rule.get_masked_bits(), Some(16));
2311    }
2312
2313    #[test]
2314    fn test_address_type() {
2315        let rule = ExitPolicyRule::parse("accept *:*").unwrap();
2316        assert_eq!(rule.get_address_type(), AddressType::Wildcard);
2317
2318        let rule = ExitPolicyRule::parse("accept 192.168.1.1:*").unwrap();
2319        assert_eq!(rule.get_address_type(), AddressType::IPv4);
2320
2321        let rule = ExitPolicyRule::parse("accept [::1]:*").unwrap();
2322        assert_eq!(rule.get_address_type(), AddressType::IPv6);
2323    }
2324
2325    #[test]
2326    fn test_get_mask() {
2327        let rule = ExitPolicyRule::parse("accept 192.168.0.0/16:*").unwrap();
2328        assert_eq!(
2329            rule.get_mask(),
2330            Some(IpAddr::V4("255.255.0.0".parse().unwrap()))
2331        );
2332
2333        let rule = ExitPolicyRule::parse("accept 10.0.0.0/8:*").unwrap();
2334        assert_eq!(
2335            rule.get_mask(),
2336            Some(IpAddr::V4("255.0.0.0".parse().unwrap()))
2337        );
2338    }
2339
2340    #[test]
2341    fn test_policy_from_rules() {
2342        let policy = ExitPolicy::from_rules(&["accept *:80", "reject *:*"]).unwrap();
2343        assert_eq!(policy.len(), 2);
2344        assert!(policy.can_exit_to("192.168.1.1".parse().unwrap(), 80));
2345        assert!(!policy.can_exit_to("192.168.1.1".parse().unwrap(), 443));
2346    }
2347
2348    #[test]
2349    fn test_invalid_rule_no_action() {
2350        assert!(ExitPolicyRule::parse("*:80").is_err());
2351    }
2352
2353    #[test]
2354    fn test_invalid_rule_no_port() {
2355        assert!(ExitPolicyRule::parse("accept *").is_err());
2356    }
2357
2358    #[test]
2359    fn test_invalid_rule_bad_port_range() {
2360        assert!(ExitPolicyRule::parse("accept *:443-80").is_err());
2361    }
2362
2363    #[test]
2364    fn test_invalid_rule_bad_mask() {
2365        assert!(ExitPolicyRule::parse("accept 192.168.0.0/33:*").is_err());
2366    }
2367
2368    #[test]
2369    fn test_cidr_matching_edge_cases() {
2370        let rule = ExitPolicyRule::parse("accept 192.168.0.0/24:*").unwrap();
2371        assert!(rule.is_match(Some("192.168.0.0".parse().unwrap()), Some(80)));
2372        assert!(rule.is_match(Some("192.168.0.255".parse().unwrap()), Some(80)));
2373        assert!(!rule.is_match(Some("192.168.1.0".parse().unwrap()), Some(80)));
2374    }
2375
2376    #[test]
2377    fn test_ipv6_cidr_matching() {
2378        let rule = ExitPolicyRule::parse("accept [2001:db8::]/32:*").unwrap();
2379        assert!(rule.is_match(Some("2001:db8::1".parse().unwrap()), Some(80)));
2380        assert!(rule.is_match(Some("2001:db8:ffff::1".parse().unwrap()), Some(80)));
2381        assert!(!rule.is_match(Some("2001:db9::1".parse().unwrap()), Some(80)));
2382    }
2383
2384    #[test]
2385    fn test_policy_iter() {
2386        let policy = ExitPolicy::parse("accept *:80, accept *:443, reject *:*").unwrap();
2387        let rules: Vec<_> = policy.iter().collect();
2388        assert_eq!(rules.len(), 3);
2389    }
2390
2391    #[test]
2392    fn test_port_range_struct() {
2393        let range = PortRange::new(80, 443).unwrap();
2394        assert!(range.contains(80));
2395        assert!(range.contains(443));
2396        assert!(range.contains(200));
2397        assert!(!range.contains(79));
2398        assert!(!range.contains(444));
2399
2400        let single = PortRange::single(80);
2401        assert!(single.contains(80));
2402        assert!(!single.contains(81));
2403
2404        let all = PortRange::all();
2405        assert!(all.is_wildcard());
2406        assert!(all.contains(1));
2407        assert!(all.contains(65535));
2408    }
2409
2410    #[test]
2411    fn test_port_range_invalid() {
2412        assert!(PortRange::new(443, 80).is_err());
2413    }
2414
2415    #[test]
2416    fn test_accept6_reject6() {
2417        let rule = ExitPolicyRule::parse("accept6 [::]/0:*").unwrap();
2418        assert!(rule.is_accept);
2419        assert_eq!(rule.get_address_type(), AddressType::IPv6);
2420
2421        let rule = ExitPolicyRule::parse("reject6 [::]/0:*").unwrap();
2422        assert!(!rule.is_accept);
2423    }
2424
2425    #[test]
2426    fn test_star4_star6_wildcards() {
2427        let rule = ExitPolicyRule::parse("accept *4:*").unwrap();
2428        assert_eq!(rule.get_address_type(), AddressType::IPv4);
2429        assert_eq!(rule.get_masked_bits(), Some(0));
2430
2431        let rule = ExitPolicyRule::parse("accept *6:*").unwrap();
2432        assert_eq!(rule.get_address_type(), AddressType::IPv6);
2433        assert_eq!(rule.get_masked_bits(), Some(0));
2434    }
2435}
2436
2437#[cfg(test)]
2438mod stem_tests {
2439    use super::*;
2440
2441    #[test]
2442    fn test_example() {
2443        let policy =
2444            ExitPolicy::from_rules(&["accept *:80", "accept *:443", "reject *:*"]).unwrap();
2445        assert_eq!(policy.to_string(), "accept *:80, accept *:443, reject *:*");
2446        assert_eq!(policy.summary(), "accept 80, 443");
2447        assert!(policy.can_exit_to("75.119.206.243".parse().unwrap(), 80));
2448
2449        let policy = MicroExitPolicy::parse("accept 80,443").unwrap();
2450        assert!(policy.can_exit_to(80));
2451    }
2452
2453    #[test]
2454    fn test_constructor_truncates_after_catch_all() {
2455        let policy = ExitPolicy::from_rules(&[
2456            "accept *:80",
2457            "accept *:443",
2458            "reject *:*",
2459            "accept *:20-50",
2460        ])
2461        .unwrap();
2462        assert_eq!(policy.len(), 3);
2463    }
2464
2465    #[test]
2466    fn test_can_exit_to_various_ports() {
2467        let policy =
2468            ExitPolicy::from_rules(&["accept *:80", "accept *:443", "reject *:*"]).unwrap();
2469
2470        for port in 1..100u16 {
2471            let ip: IpAddr = format!("{}.{}.{}.{}", port / 2, port / 2, port / 2, port / 2)
2472                .parse()
2473                .unwrap_or("0.0.0.0".parse().unwrap());
2474            let expected = port == 80 || port == 443;
2475            assert_eq!(
2476                expected,
2477                policy.can_exit_to(ip, port),
2478                "port {} expected {}",
2479                port,
2480                expected
2481            );
2482        }
2483    }
2484
2485    #[test]
2486    fn test_can_exit_to_strictness() {
2487        let policy = ExitPolicy::from_rules(&["reject 1.0.0.0/8:80", "accept *:*"]).unwrap();
2488        assert!(!policy.can_exit_to_optional(None, Some(80), true));
2489        assert!(policy.can_exit_to_optional(None, Some(80), false));
2490
2491        let policy = ExitPolicy::from_rules(&["accept 1.0.0.0/8:80", "reject *:*"]).unwrap();
2492        assert!(!policy.can_exit_to_optional(None, Some(80), true));
2493        assert!(policy.can_exit_to_optional(None, Some(80), false));
2494    }
2495
2496    #[test]
2497    fn test_is_exiting_allowed_various() {
2498        let test_cases: Vec<(&[&str], bool)> = vec![
2499            (&[], true),
2500            (&["accept *:*"], true),
2501            (&["reject *:*"], false),
2502            (&["accept *:80", "reject *:*"], true),
2503            (&["reject *:80", "accept *:80", "reject *:*"], false),
2504            (&["reject *:50-90", "accept *:80", "reject *:*"], false),
2505            (
2506                &["reject *:2-65535", "accept *:80-65535", "reject *:*"],
2507                false,
2508            ),
2509            (
2510                &["reject *:2-65535", "accept 127.0.0.0:1", "reject *:*"],
2511                true,
2512            ),
2513            (&["reject 127.0.0.1:*", "accept *:80", "reject *:*"], true),
2514        ];
2515
2516        for (rules, expected) in test_cases {
2517            let policy = ExitPolicy::from_rules(rules).unwrap();
2518            assert_eq!(
2519                expected,
2520                policy.is_exiting_allowed(),
2521                "rules {:?} expected {}",
2522                rules,
2523                expected
2524            );
2525        }
2526    }
2527
2528    #[test]
2529    fn test_summary_large_ranges() {
2530        let policy =
2531            ExitPolicy::from_rules(&["reject *:80-65535", "accept *:1-65533", "reject *:*"])
2532                .unwrap();
2533        assert_eq!(policy.summary(), "accept 1-79");
2534    }
2535
2536    #[test]
2537    fn test_non_private_non_default_policy() {
2538        let policy =
2539            ExitPolicy::from_rules(&["reject *:80-65535", "accept *:1-65533", "reject *:*"])
2540                .unwrap();
2541
2542        for rule in policy.iter() {
2543            assert!(!rule.is_private());
2544            assert!(!rule.is_default());
2545        }
2546
2547        assert!(!policy.has_private());
2548        assert!(!policy.has_default());
2549    }
2550
2551    #[test]
2552    fn test_str_whitespace_handling() {
2553        let policy = ExitPolicy::from_rules(&["  accept *:80\n", "\taccept *:443"]).unwrap();
2554        assert_eq!(policy.to_string(), "accept *:80, accept *:443");
2555    }
2556
2557    #[test]
2558    fn test_str_mask_conversion() {
2559        let policy =
2560            ExitPolicy::from_rules(&["reject 0.0.0.0/255.255.255.0:*", "accept *:*"]).unwrap();
2561        assert_eq!(policy.to_string(), "reject 0.0.0.0/24:*, accept *:*");
2562    }
2563
2564    #[test]
2565    fn test_microdescriptor_parsing_valid() {
2566        assert!(MicroExitPolicy::parse("accept 80").is_ok());
2567        assert!(MicroExitPolicy::parse("accept 80,443").is_ok());
2568    }
2569
2570    #[test]
2571    fn test_microdescriptor_parsing_invalid() {
2572        assert!(MicroExitPolicy::parse("").is_err());
2573        assert!(MicroExitPolicy::parse("accept").is_err());
2574        assert!(MicroExitPolicy::parse("accept ").is_err());
2575        assert!(MicroExitPolicy::parse("80,443").is_err());
2576        assert!(MicroExitPolicy::parse("bar 80,443").is_err());
2577    }
2578
2579    #[test]
2580    fn test_microdescriptor_attributes() {
2581        let policy = MicroExitPolicy::parse("accept 443").unwrap();
2582        assert!(policy.is_accept);
2583
2584        let policy = MicroExitPolicy::parse("accept 80,443").unwrap();
2585        assert!(policy.is_accept);
2586
2587        let policy = MicroExitPolicy::parse("reject 1-1024").unwrap();
2588        assert!(!policy.is_accept);
2589    }
2590
2591    #[test]
2592    fn test_microdescriptor_can_exit_to_various() {
2593        let test_cases: Vec<(&str, Vec<(u16, bool)>)> = vec![
2594            ("accept 443", vec![(442, false), (443, true), (444, false)]),
2595            ("reject 443", vec![(442, true), (443, false), (444, true)]),
2596            ("accept 80,443", vec![(80, true), (443, true), (10, false)]),
2597            (
2598                "reject 1-1024",
2599                vec![(1, false), (1024, false), (1025, true)],
2600            ),
2601        ];
2602
2603        for (policy_str, checks) in test_cases {
2604            let policy = MicroExitPolicy::parse(policy_str).unwrap();
2605            for (port, expected) in checks {
2606                assert_eq!(
2607                    expected,
2608                    policy.can_exit_to(port),
2609                    "policy {} port {} expected {}",
2610                    policy_str,
2611                    port,
2612                    expected
2613                );
2614            }
2615        }
2616    }
2617
2618    #[test]
2619    fn test_accept_or_reject() {
2620        assert!(ExitPolicyRule::parse("accept *:*").unwrap().is_accept);
2621        assert!(!ExitPolicyRule::parse("reject *:*").unwrap().is_accept);
2622    }
2623
2624    #[test]
2625    fn test_invalid_rule_formats() {
2626        let invalid_inputs = [
2627            "accept",
2628            "reject",
2629            "acceptt *:*",
2630            "rejectt *:*",
2631            "blarg *:*",
2632            " *:*",
2633            "*:*",
2634            "",
2635        ];
2636
2637        for input in invalid_inputs {
2638            assert!(
2639                ExitPolicyRule::parse(input).is_err(),
2640                "expected error for: {}",
2641                input
2642            );
2643        }
2644    }
2645
2646    #[test]
2647    fn test_with_multiple_spaces() {
2648        let rule = ExitPolicyRule::parse("accept    *:80").unwrap();
2649        assert_eq!(rule.to_string(), "accept *:80");
2650
2651        let policy = MicroExitPolicy::parse("accept      80,443").unwrap();
2652        assert!(policy.can_exit_to(80));
2653    }
2654
2655    #[test]
2656    fn test_str_unchanged() {
2657        let test_inputs = [
2658            "accept *:*",
2659            "reject *:*",
2660            "accept *:80",
2661            "accept *:80-443",
2662            "accept 127.0.0.1:80",
2663            "accept 87.0.0.1/24:80",
2664        ];
2665
2666        for input in test_inputs {
2667            let rule = ExitPolicyRule::parse(input).unwrap();
2668            assert_eq!(input, rule.to_string(), "input: {}", input);
2669        }
2670    }
2671
2672    #[test]
2673    fn test_str_changed() {
2674        let test_cases = [
2675            ("accept 10.0.0.1/32:80", "accept 10.0.0.1:80"),
2676            (
2677                "accept 192.168.0.1/255.255.255.0:80",
2678                "accept 192.168.0.1/24:80",
2679            ),
2680        ];
2681
2682        for (input, expected) in test_cases {
2683            let rule = ExitPolicyRule::parse(input).unwrap();
2684            assert_eq!(expected, rule.to_string(), "input: {}", input);
2685        }
2686    }
2687
2688    #[test]
2689    fn test_valid_wildcard() {
2690        let test_cases: Vec<(&str, bool, bool)> = vec![
2691            ("reject *:*", true, true),
2692            ("reject *:80", true, false),
2693            ("accept 192.168.0.1:*", false, true),
2694            ("accept 192.168.0.1:80", false, false),
2695            ("reject *4:*", false, true),
2696            ("reject *6:*", false, true),
2697            ("reject 127.0.0.1/0:*", false, true),
2698            ("reject 127.0.0.1/16:*", false, true),
2699            ("reject 127.0.0.1/32:*", false, true),
2700            ("accept 192.168.0.1:0-65535", false, true),
2701            ("accept 192.168.0.1:1-65535", false, true),
2702            ("accept 192.168.0.1:2-65535", false, false),
2703            ("accept 192.168.0.1:1-65534", false, false),
2704        ];
2705
2706        for (rule_str, is_addr_wildcard, is_port_wildcard) in test_cases {
2707            let rule = ExitPolicyRule::parse(rule_str).unwrap();
2708            assert_eq!(
2709                is_addr_wildcard,
2710                rule.is_address_wildcard(),
2711                "{} address wildcard",
2712                rule_str
2713            );
2714            assert_eq!(
2715                is_port_wildcard,
2716                rule.is_port_wildcard(),
2717                "{} port wildcard",
2718                rule_str
2719            );
2720        }
2721    }
2722
2723    #[test]
2724    fn test_invalid_wildcard() {
2725        let invalid_inputs = [
2726            "reject */16:*",
2727            "reject 127.0.0.1/*:*",
2728            "reject *:0-*",
2729            "reject *:*-15",
2730        ];
2731
2732        for input in invalid_inputs {
2733            assert!(
2734                ExitPolicyRule::parse(input).is_err(),
2735                "expected error for: {}",
2736                input
2737            );
2738        }
2739    }
2740
2741    #[test]
2742    fn test_wildcard_attributes() {
2743        let rule = ExitPolicyRule::parse("reject *:*").unwrap();
2744        assert_eq!(AddressType::Wildcard, rule.get_address_type());
2745        assert!(rule.address().is_none());
2746        assert!(rule.get_mask().is_none());
2747        assert!(rule.get_masked_bits().is_none());
2748        assert_eq!(1, rule.min_port);
2749        assert_eq!(65535, rule.max_port);
2750    }
2751
2752    #[test]
2753    fn test_valid_ipv4_addresses() {
2754        let test_cases: Vec<(&str, &str, &str, u8)> = vec![
2755            ("0.0.0.0", "0.0.0.0", "255.255.255.255", 32),
2756            ("127.0.0.1/32", "127.0.0.1", "255.255.255.255", 32),
2757            ("192.168.0.50/24", "192.168.0.50", "255.255.255.0", 24),
2758            ("255.255.255.255/0", "255.255.255.255", "0.0.0.0", 0),
2759        ];
2760
2761        for (rule_addr, address, mask, masked_bits) in test_cases {
2762            let rule = ExitPolicyRule::parse(&format!("accept {}:*", rule_addr)).unwrap();
2763            assert_eq!(AddressType::IPv4, rule.get_address_type());
2764            assert_eq!(
2765                Some(address.parse::<IpAddr>().unwrap()),
2766                rule.address(),
2767                "address for {}",
2768                rule_addr
2769            );
2770            assert_eq!(
2771                Some(mask.parse::<IpAddr>().unwrap()),
2772                rule.get_mask(),
2773                "mask for {}",
2774                rule_addr
2775            );
2776            assert_eq!(
2777                Some(masked_bits),
2778                rule.get_masked_bits(),
2779                "bits for {}",
2780                rule_addr
2781            );
2782        }
2783    }
2784
2785    #[test]
2786    fn test_invalid_ipv4_addresses() {
2787        let invalid_inputs = [
2788            "256.0.0.0",
2789            "0.0.0",
2790            "0.0.0.",
2791            "0.0.0.a",
2792            "127.0.0.1/-1",
2793            "127.0.0.1/33",
2794        ];
2795
2796        for addr in invalid_inputs {
2797            assert!(
2798                ExitPolicyRule::parse(&format!("accept {}:*", addr)).is_err(),
2799                "expected error for: {}",
2800                addr
2801            );
2802        }
2803    }
2804
2805    #[test]
2806    fn test_valid_ipv6_addresses() {
2807        let rule = ExitPolicyRule::parse("accept [fe80::0202:b3ff:fe1e:8329]:*").unwrap();
2808        assert_eq!(AddressType::IPv6, rule.get_address_type());
2809        assert_eq!(Some(128), rule.get_masked_bits());
2810
2811        let rule = ExitPolicyRule::parse("accept [::]:*").unwrap();
2812        assert_eq!(AddressType::IPv6, rule.get_address_type());
2813        assert_eq!(Some(128), rule.get_masked_bits());
2814
2815        let rule = ExitPolicyRule::parse("accept [::]/0:*").unwrap();
2816        assert_eq!(Some(0), rule.get_masked_bits());
2817    }
2818
2819    #[test]
2820    fn test_invalid_ipv6_addresses() {
2821        let invalid_inputs = [
2822            "fe80::0202:b3ff:fe1e:8329",
2823            "[fe80::0202:b3ff:fe1e:8329",
2824            "fe80::0202:b3ff:fe1e:8329]",
2825            "[fe80::0202:b3ff:fe1e:8329]/-1",
2826            "[fe80::0202:b3ff:fe1e:8329]/129",
2827        ];
2828
2829        for addr in invalid_inputs {
2830            assert!(
2831                ExitPolicyRule::parse(&format!("accept {}:*", addr)).is_err(),
2832                "expected error for: {}",
2833                addr
2834            );
2835        }
2836    }
2837
2838    #[test]
2839    fn test_valid_ports() {
2840        let test_cases: Vec<(&str, u16, u16)> = vec![
2841            ("0", 0, 0),
2842            ("1", 1, 1),
2843            ("80", 80, 80),
2844            ("80-443", 80, 443),
2845        ];
2846
2847        for (port_str, min_port, max_port) in test_cases {
2848            let rule = ExitPolicyRule::parse(&format!("accept 127.0.0.1:{}", port_str)).unwrap();
2849            assert_eq!(min_port, rule.min_port, "min_port for {}", port_str);
2850            assert_eq!(max_port, rule.max_port, "max_port for {}", port_str);
2851        }
2852    }
2853
2854    #[test]
2855    fn test_invalid_ports() {
2856        let invalid_inputs = ["65536", "a", "5-3", "5-", "-3"];
2857
2858        for port in invalid_inputs {
2859            assert!(
2860                ExitPolicyRule::parse(&format!("accept 127.0.0.1:{}", port)).is_err(),
2861                "expected error for port: {}",
2862                port
2863            );
2864        }
2865    }
2866
2867    #[test]
2868    fn test_is_match_wildcard_rule() {
2869        let rule = ExitPolicyRule::parse("reject *:*").unwrap();
2870        assert!(rule.is_match(Some("192.168.0.1".parse().unwrap()), Some(80)));
2871        assert!(rule.is_match(Some("0.0.0.0".parse().unwrap()), Some(80)));
2872        assert!(rule.is_match(Some("255.255.255.255".parse().unwrap()), Some(80)));
2873        assert!(rule.is_match(Some("fe80::0202:b3ff:fe1e:8329".parse().unwrap()), Some(80)));
2874        assert!(rule.is_match(Some("192.168.0.1".parse().unwrap()), None));
2875        assert!(rule.is_match(None, Some(80)));
2876        assert!(rule.is_match(None, None));
2877    }
2878
2879    #[test]
2880    fn test_is_match_ipv4_specific() {
2881        let rule = ExitPolicyRule::parse("reject 192.168.0.50:*").unwrap();
2882        assert!(rule.is_match(Some("192.168.0.50".parse().unwrap()), Some(80)));
2883        assert!(!rule.is_match(Some("192.168.0.51".parse().unwrap()), Some(80)));
2884        assert!(!rule.is_match(Some("192.168.0.49".parse().unwrap()), Some(80)));
2885        assert!(rule.is_match(Some("192.168.0.50".parse().unwrap()), None));
2886    }
2887
2888    #[test]
2889    fn test_is_match_ipv4_cidr() {
2890        let rule = ExitPolicyRule::parse("reject 0.0.0.0/24:*").unwrap();
2891        assert!(rule.is_match(Some("0.0.0.0".parse().unwrap()), Some(80)));
2892        assert!(rule.is_match(Some("0.0.0.1".parse().unwrap()), Some(80)));
2893        assert!(rule.is_match(Some("0.0.0.255".parse().unwrap()), Some(80)));
2894        assert!(!rule.is_match(Some("0.0.1.0".parse().unwrap()), Some(80)));
2895        assert!(!rule.is_match(Some("0.1.0.0".parse().unwrap()), Some(80)));
2896        assert!(!rule.is_match(Some("1.0.0.0".parse().unwrap()), Some(80)));
2897    }
2898
2899    #[test]
2900    fn test_is_match_ipv6_specific() {
2901        let rule = ExitPolicyRule::parse("reject [fe80::0202:b3ff:fe1e:8329]:*").unwrap();
2902        assert!(rule.is_match(Some("fe80::0202:b3ff:fe1e:8329".parse().unwrap()), Some(80)));
2903        assert!(!rule.is_match(Some("fe80::0202:b3ff:fe1e:8330".parse().unwrap()), Some(80)));
2904        assert!(!rule.is_match(Some("fe80::0202:b3ff:fe1e:8328".parse().unwrap()), Some(80)));
2905    }
2906
2907    #[test]
2908    fn test_is_match_ipv6_cidr() {
2909        let rule = ExitPolicyRule::parse("reject [fe80::0202:b3ff:fe1e:8329]/112:*").unwrap();
2910        assert!(rule.is_match(Some("fe80::0202:b3ff:fe1e:8329".parse().unwrap()), Some(80)));
2911        assert!(rule.is_match(Some("fe80::0202:b3ff:fe1e:0000".parse().unwrap()), Some(80)));
2912        assert!(rule.is_match(Some("fe80::0202:b3ff:fe1e:ffff".parse().unwrap()), Some(80)));
2913        assert!(!rule.is_match(Some("fe80::0202:b3ff:fe1f:8329".parse().unwrap()), Some(80)));
2914    }
2915
2916    #[test]
2917    fn test_is_match_port_specific() {
2918        let rule = ExitPolicyRule::parse("reject *:80").unwrap();
2919        assert!(rule.is_match(Some("192.168.0.50".parse().unwrap()), Some(80)));
2920        assert!(!rule.is_match(Some("192.168.0.50".parse().unwrap()), Some(81)));
2921        assert!(!rule.is_match(Some("192.168.0.50".parse().unwrap()), Some(79)));
2922        assert!(rule.is_match(None, Some(80)));
2923    }
2924
2925    #[test]
2926    fn test_is_match_port_range() {
2927        let rule = ExitPolicyRule::parse("reject *:80-85").unwrap();
2928        assert!(!rule.is_match(Some("192.168.0.50".parse().unwrap()), Some(79)));
2929        assert!(rule.is_match(Some("192.168.0.50".parse().unwrap()), Some(80)));
2930        assert!(rule.is_match(Some("192.168.0.50".parse().unwrap()), Some(83)));
2931        assert!(rule.is_match(Some("192.168.0.50".parse().unwrap()), Some(85)));
2932        assert!(!rule.is_match(Some("192.168.0.50".parse().unwrap()), Some(86)));
2933        assert!(rule.is_match(None, Some(83)));
2934    }
2935
2936    #[test]
2937    fn test_ipv4_ipv6_address_family_mismatch() {
2938        let rule = ExitPolicyRule::parse("reject *4:*").unwrap();
2939        assert!(rule.is_match(Some("192.168.0.1".parse().unwrap()), Some(80)));
2940        assert!(!rule.is_match(Some("fe80::0202:b3ff:fe1e:8329".parse().unwrap()), Some(80)));
2941
2942        let rule = ExitPolicyRule::parse("reject *6:*").unwrap();
2943        assert!(!rule.is_match(Some("192.168.0.1".parse().unwrap()), Some(80)));
2944        assert!(rule.is_match(Some("fe80::0202:b3ff:fe1e:8329".parse().unwrap()), Some(80)));
2945    }
2946}
2947
2948#[cfg(test)]
2949mod proptests {
2950    use super::*;
2951    use proptest::prelude::*;
2952
2953    fn ipv4_addr() -> impl Strategy<Value = Ipv4Addr> {
2954        (any::<u8>(), any::<u8>(), any::<u8>(), any::<u8>())
2955            .prop_map(|(a, b, c, d)| Ipv4Addr::new(a, b, c, d))
2956    }
2957
2958    fn ipv6_addr() -> impl Strategy<Value = Ipv6Addr> {
2959        (
2960            any::<u16>(),
2961            any::<u16>(),
2962            any::<u16>(),
2963            any::<u16>(),
2964            any::<u16>(),
2965            any::<u16>(),
2966            any::<u16>(),
2967            any::<u16>(),
2968        )
2969            .prop_map(|(a, b, c, d, e, f, g, h)| Ipv6Addr::new(a, b, c, d, e, f, g, h))
2970    }
2971
2972    fn valid_port() -> impl Strategy<Value = u16> {
2973        1..=65535u16
2974    }
2975
2976    fn valid_port_range() -> impl Strategy<Value = (u16, u16)> {
2977        (1..=65535u16).prop_flat_map(|min| (Just(min), min..=65535u16))
2978    }
2979
2980    fn cidr_mask_ipv4() -> impl Strategy<Value = u8> {
2981        0..=32u8
2982    }
2983
2984    fn cidr_mask_ipv6() -> impl Strategy<Value = u8> {
2985        0..=128u8
2986    }
2987
2988    proptest! {
2989        #![proptest_config(ProptestConfig::with_cases(100))]
2990
2991        #[test]
2992        fn prop_exit_policy_evaluation_consistency(
2993            addr in ipv4_addr(),
2994            port in valid_port()
2995        ) {
2996            let policy = ExitPolicy::from_rules(&["accept *:80", "accept *:443", "reject *:*"]).unwrap();
2997            let result1 = policy.can_exit_to(IpAddr::V4(addr), port);
2998            let result2 = policy.can_exit_to(IpAddr::V4(addr), port);
2999            prop_assert_eq!(result1, result2, "evaluation should be consistent");
3000
3001            let expected = port == 80 || port == 443;
3002            prop_assert_eq!(expected, result1, "port {} should be {}", port, expected);
3003        }
3004
3005        #[test]
3006        fn prop_exit_policy_cidr_matching_ipv4(
3007            network_base in 0..=255u8,
3008            mask_bits in cidr_mask_ipv4(),
3009            test_addr in ipv4_addr(),
3010            port in valid_port()
3011        ) {
3012            let network = Ipv4Addr::new(network_base, 0, 0, 0);
3013            let rule_str = format!("accept {}/{}:*", network, mask_bits);
3014            let rule = ExitPolicyRule::parse(&rule_str).unwrap();
3015
3016            let network_u32 = u32::from_be_bytes(network.octets());
3017            let test_u32 = u32::from_be_bytes(test_addr.octets());
3018            let mask = if mask_bits == 0 { 0 } else { !((1u32 << (32 - mask_bits)) - 1) };
3019
3020            let should_match = (network_u32 & mask) == (test_u32 & mask);
3021            let does_match = rule.is_match(Some(IpAddr::V4(test_addr)), Some(port));
3022
3023            prop_assert_eq!(should_match, does_match,
3024                "CIDR {}/{} test {} expected {} got {}",
3025                network, mask_bits, test_addr, should_match, does_match);
3026        }
3027
3028        #[test]
3029        fn prop_exit_policy_cidr_matching_ipv6(
3030            mask_bits in cidr_mask_ipv6(),
3031            test_addr in ipv6_addr(),
3032            port in valid_port()
3033        ) {
3034            let network = Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 0);
3035            let rule_str = format!("accept [{}]/{}:*", network, mask_bits);
3036            let rule = ExitPolicyRule::parse(&rule_str).unwrap();
3037
3038            let network_u128 = u128::from_be_bytes(network.octets());
3039            let test_u128 = u128::from_be_bytes(test_addr.octets());
3040            let mask = if mask_bits == 0 { 0 } else { !((1u128 << (128 - mask_bits)) - 1) };
3041
3042            let should_match = (network_u128 & mask) == (test_u128 & mask);
3043            let does_match = rule.is_match(Some(IpAddr::V6(test_addr)), Some(port));
3044
3045            prop_assert_eq!(should_match, does_match,
3046                "CIDR [{}]/{} test {} expected {} got {}",
3047                network, mask_bits, test_addr, should_match, does_match);
3048        }
3049
3050        #[test]
3051        fn prop_exit_policy_port_range_matching(
3052            (min_port, max_port) in valid_port_range(),
3053            test_port in valid_port(),
3054            addr in ipv4_addr()
3055        ) {
3056            let rule_str = format!("accept *:{}-{}", min_port, max_port);
3057            let rule = ExitPolicyRule::parse(&rule_str).unwrap();
3058
3059            let should_match = test_port >= min_port && test_port <= max_port;
3060            let does_match = rule.is_match(Some(IpAddr::V4(addr)), Some(test_port));
3061
3062            prop_assert_eq!(should_match, does_match,
3063                "port range {}-{} test {} expected {} got {}",
3064                min_port, max_port, test_port, should_match, does_match);
3065        }
3066
3067        #[test]
3068        fn prop_policy_first_match_semantics(
3069            addr in ipv4_addr(),
3070            port in valid_port()
3071        ) {
3072            let policy = ExitPolicy::from_rules(&[
3073                "reject 10.0.0.0/8:*",
3074                "accept *:80",
3075                "reject *:*"
3076            ]).unwrap();
3077
3078            let is_10_network = addr.octets()[0] == 10;
3079            let result = policy.can_exit_to(IpAddr::V4(addr), port);
3080
3081            if is_10_network {
3082                prop_assert!(!result, "10.x.x.x should be rejected");
3083            } else if port == 80 {
3084                prop_assert!(result, "port 80 should be accepted for non-10.x.x.x");
3085            } else {
3086                prop_assert!(!result, "other ports should be rejected for non-10.x.x.x");
3087            }
3088        }
3089
3090        #[test]
3091        fn prop_micro_policy_port_matching(
3092            port1 in valid_port(),
3093            port2 in valid_port(),
3094            test_port in valid_port()
3095        ) {
3096            let (min, max) = if port1 <= port2 { (port1, port2) } else { (port2, port1) };
3097            let policy_str = format!("accept {}-{}", min, max);
3098            let policy = MicroExitPolicy::parse(&policy_str).unwrap();
3099
3100            let should_match = test_port >= min && test_port <= max;
3101            let does_match = policy.can_exit_to(test_port);
3102
3103            prop_assert_eq!(should_match, does_match,
3104                "micro policy {}-{} test {} expected {} got {}",
3105                min, max, test_port, should_match, does_match);
3106        }
3107
3108        #[test]
3109        fn prop_port_range_contains(
3110            (min, max) in valid_port_range(),
3111            test_port in valid_port()
3112        ) {
3113            let range = PortRange::new(min, max).unwrap();
3114            let should_contain = test_port >= min && test_port <= max;
3115            prop_assert_eq!(should_contain, range.contains(test_port));
3116        }
3117
3118        #[test]
3119        fn prop_address_type_ipv4(addr in ipv4_addr(), port in valid_port()) {
3120            let rule_str = format!("accept {}:*", addr);
3121            let rule = ExitPolicyRule::parse(&rule_str).unwrap();
3122            prop_assert_eq!(AddressType::IPv4, rule.get_address_type());
3123            prop_assert!(rule.is_match(Some(IpAddr::V4(addr)), Some(port)));
3124        }
3125
3126        #[test]
3127        fn prop_address_type_ipv6(addr in ipv6_addr(), port in valid_port()) {
3128            let rule_str = format!("accept [{}]:*", addr);
3129            let rule = ExitPolicyRule::parse(&rule_str).unwrap();
3130            prop_assert_eq!(AddressType::IPv6, rule.get_address_type());
3131            prop_assert!(rule.is_match(Some(IpAddr::V6(addr)), Some(port)));
3132        }
3133    }
3134}