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}