Skip to main content

tor_hsrproxy/
config.rs

1//! Configuration logic for onion service reverse proxy.
2use derive_deftly::Deftly;
3use serde::{Deserialize, Serialize};
4use std::{net::SocketAddr, ops::RangeInclusive, str::FromStr, sync::Arc};
5use tor_config::ConfigBuildError;
6use tor_config::derive::prelude::*;
7use tracing::warn;
8
9#[cfg(unix)]
10use std::os::unix::net::SocketAddr as UnixSocketAddr;
11/// Configuration for a reverse proxy running for one onion service.
12#[derive(Clone, Debug, Deftly, Eq, PartialEq)]
13#[derive_deftly(TorConfig)]
14#[deftly(tor_config(no_default_trait, pre_build = "Self::validate"))]
15pub struct ProxyConfig {
16    /// A list of rules to apply to incoming requests.  If no rule
17    /// matches, we take the DestroyCircuit action.
18    #[deftly(tor_config(list(element(clone), listtype = "ProxyRuleList"), default = "vec![]"))]
19    pub(crate) proxy_ports: Vec<ProxyRule>,
20    //
21    // TODO: Someday we may want to allow udp, resolve, etc.  If we do, it will
22    // be via another option, rather than adding another subtype to ProxySource.
23}
24
25impl ProxyConfigBuilder {
26    /// Run checks on this ProxyConfig to ensure that it's valid.
27    fn validate(&self) -> Result<(), ConfigBuildError> {
28        // Make sure that every proxy pattern is actually reachable.
29        let mut covered = rangemap::RangeInclusiveSet::<u16>::new();
30        for rule in self.proxy_ports.access_opt().iter().flatten() {
31            let range = &rule.source.0;
32            if covered.gaps(range).next().is_none() {
33                return Err(ConfigBuildError::Invalid {
34                    field: "proxy_ports".into(),
35                    problem: format!("Port pattern {} is not reachable", rule.source),
36                });
37            }
38            covered.insert(range.clone());
39        }
40
41        // Warn about proxy setups that are likely to be surprising.
42        let mut any_forward = false;
43        for rule in self.proxy_ports.access_opt().iter().flatten() {
44            if let ProxyAction::Forward(_, target) = &rule.target {
45                any_forward = true;
46                if !target.is_sufficiently_private() {
47                    // TODO: here and below, we might want to someday
48                    // have a mechanism to suppress these warnings,
49                    // or have them show up only when relevant.
50                    // For now they are unconditional.
51                    // See discussion at #1154.
52                    warn!(
53                        "Onion service target {} does not look like a private address. \
54                         Do you really mean to send connections onto the public internet?",
55                        target
56                    );
57                }
58            }
59        }
60
61        if !any_forward {
62            warn!("Onion service is not configured to accept any connections.");
63        }
64
65        Ok(())
66    }
67}
68
69impl ProxyConfig {
70    /// Find the configured action to use when receiving a request for a
71    /// connection on a given port.
72    pub(crate) fn resolve_port_for_begin(&self, port: u16) -> Option<&ProxyAction> {
73        self.proxy_ports
74            .iter()
75            .find(|rule| rule.source.matches_port(port))
76            .map(|rule| &rule.target)
77    }
78}
79
80/// A single rule in a `ProxyConfig`.
81///
82/// Rules take the form of, "When this pattern matches, take this action."
83#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
84// TODO: we might someday want to accept structs here as well, so that
85// we can add per-rule fields if we need to.  We can make that an option if/when
86// it comes up, however.
87#[serde(from = "ProxyRuleAsTuple", into = "ProxyRuleAsTuple")]
88pub struct ProxyRule {
89    /// Any connections to a port matching this pattern match this rule.
90    source: ProxyPattern,
91    /// When this rule matches, we take this action.
92    target: ProxyAction,
93}
94
95/// Helper type used to (de)serialize ProxyRule.
96type ProxyRuleAsTuple = (ProxyPattern, ProxyAction);
97impl From<ProxyRuleAsTuple> for ProxyRule {
98    fn from(value: ProxyRuleAsTuple) -> Self {
99        Self {
100            source: value.0,
101            target: value.1,
102        }
103    }
104}
105impl From<ProxyRule> for ProxyRuleAsTuple {
106    fn from(value: ProxyRule) -> Self {
107        (value.source, value.target)
108    }
109}
110impl ProxyRule {
111    /// Create a new ProxyRule mapping `source` to `target`.
112    pub fn new(source: ProxyPattern, target: ProxyAction) -> Self {
113        Self { source, target }
114    }
115}
116
117/// A set of ports to use when checking how to handle a port.
118#[derive(Clone, Debug, serde::Deserialize, serde_with::SerializeDisplay, Eq, PartialEq)]
119#[serde(try_from = "ProxyPatternAsEnum")]
120pub struct ProxyPattern(RangeInclusive<u16>);
121
122/// Representation for a [`ProxyPattern`]. Used while deserializing.
123#[derive(serde::Deserialize)]
124#[serde(untagged)]
125enum ProxyPatternAsEnum {
126    /// Representation the [`ProxyPattern`] as an integer.
127    Number(u16),
128    /// Representation of the [`ProxyPattern`] as a string.
129    String(String),
130}
131
132impl TryFrom<ProxyPatternAsEnum> for ProxyPattern {
133    type Error = ProxyConfigError;
134
135    fn try_from(value: ProxyPatternAsEnum) -> Result<Self, Self::Error> {
136        match value {
137            ProxyPatternAsEnum::Number(port) => Self::one_port(port),
138            ProxyPatternAsEnum::String(s) => Self::from_str(&s),
139        }
140    }
141}
142
143impl FromStr for ProxyPattern {
144    type Err = ProxyConfigError;
145
146    fn from_str(s: &str) -> Result<Self, Self::Err> {
147        use ProxyConfigError as PCE;
148        if s == "*" {
149            Ok(Self::all_ports())
150        } else if let Some((left, right)) = s.split_once('-') {
151            let left: u16 = left
152                .parse()
153                .map_err(|e| PCE::InvalidPort(left.to_string(), e))?;
154            let right: u16 = right
155                .parse()
156                .map_err(|e| PCE::InvalidPort(right.to_string(), e))?;
157            Self::port_range(left, right)
158        } else {
159            let port = s.parse().map_err(|e| PCE::InvalidPort(s.to_string(), e))?;
160            Self::one_port(port)
161        }
162    }
163}
164impl std::fmt::Display for ProxyPattern {
165    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
166        match self.0.clone().into_inner() {
167            (start, end) if start == end => write!(f, "{}", start),
168            (1, 65535) => write!(f, "*"),
169            (start, end) => write!(f, "{}-{}", start, end),
170        }
171    }
172}
173
174impl ProxyPattern {
175    /// Return a pattern matching all ports.
176    pub fn all_ports() -> Self {
177        Self::check(1, 65535).expect("Somehow, 1-65535 was not a valid pattern")
178    }
179    /// Return a pattern matching a single port.
180    ///
181    /// Gives an error if the port is zero.
182    pub fn one_port(port: u16) -> Result<Self, ProxyConfigError> {
183        Self::check(port, port)
184    }
185    /// Return a pattern matching all ports between `low` and `high` inclusive.
186    ///
187    /// Gives an error unless `0 < low <= high`.
188    pub fn port_range(low: u16, high: u16) -> Result<Self, ProxyConfigError> {
189        Self::check(low, high)
190    }
191
192    /// Return true if this pattern includes `port`.
193    pub(crate) fn matches_port(&self, port: u16) -> bool {
194        self.0.contains(&port)
195    }
196
197    /// If start..=end is a valid pattern, wrap it as a ProxyPattern. Otherwise return
198    /// an error.
199    fn check(start: u16, end: u16) -> Result<ProxyPattern, ProxyConfigError> {
200        use ProxyConfigError as PCE;
201        match (start, end) {
202            (_, 0) => Err(PCE::ZeroPort),
203            (0, n) => Ok(Self(1..=n)),
204            (low, high) if low > high => Err(PCE::EmptyPortRange),
205            (low, high) => Ok(Self(low..=high)),
206        }
207    }
208}
209
210/// An action to take upon receiving an incoming request.
211//
212// The variant names (but not the payloads) are part of the metrics schema.
213// When changing them, see `doc/dev/MetricsStrategy.md` re schema stability policy.
214#[derive(
215    Clone,
216    Debug,
217    Default,
218    serde_with::DeserializeFromStr,
219    serde_with::SerializeDisplay,
220    Eq,
221    PartialEq,
222    strum::EnumDiscriminants,
223)]
224#[strum_discriminants(derive(Hash, strum::EnumIter))] //
225#[strum_discriminants(derive(strum::IntoStaticStr), strum(serialize_all = "snake_case"))]
226#[strum_discriminants(vis(pub(crate)))]
227#[non_exhaustive]
228pub enum ProxyAction {
229    /// Close the circuit immediately with an error.
230    #[default]
231    DestroyCircuit,
232    /// Accept the client's request and forward it, via some encapsulation method,
233    /// to some target address.
234    Forward(Encapsulation, TargetAddr),
235    /// Close the stream immediately with an error.
236    RejectStream,
237    /// Ignore the stream request.
238    IgnoreStream,
239}
240
241/// The address to which we forward an accepted connection.
242#[derive(Clone, Debug)]
243#[non_exhaustive]
244pub enum TargetAddr {
245    /// An address that we can reach over the internet.
246    Inet(SocketAddr),
247    /// And address for Unix Socket
248    /// (Only supported on Unix platforms, std::os::unix::net::SocketAddr
249    /// and ignored on non-Unix platforms Void::void).
250    ///
251    // TODO: we need more tests for Unix Socket support
252    // I will open a issue or just finish it in this MR
253    // (UnixTests)
254    #[cfg(unix)]
255    Unix(UnixSocketAddr),
256}
257
258impl PartialEq for TargetAddr {
259    /// Implement equality for TargetAddr,
260    /// which we can't automatically derived because UnixSocketAddr does not implement Eq.
261    fn eq(&self, other: &Self) -> bool {
262        match (self, other) {
263            (TargetAddr::Inet(a), TargetAddr::Inet(b)) => a == b,
264            #[cfg(unix)]
265            (TargetAddr::Unix(a), TargetAddr::Unix(b)) => a.as_pathname() == b.as_pathname(),
266            _ => false,
267        }
268    }
269}
270
271impl Eq for TargetAddr {}
272
273impl TargetAddr {
274    /// Return true if this target is sufficiently private that we can be
275    /// reasonably sure that the user has not misconfigured their onion service
276    /// to relay traffic onto the public network.
277    fn is_sufficiently_private(&self) -> bool {
278        use std::net::IpAddr;
279        match self {
280            // Unix Socket is private, because it's local
281            #[cfg(unix)]
282            TargetAddr::Unix(_) => true,
283
284            TargetAddr::Inet(sa) => match sa.ip() {
285                IpAddr::V4(ip) => ip.is_loopback() || ip.is_unspecified() || ip.is_private(),
286                IpAddr::V6(ip) => ip.is_loopback() || ip.is_unspecified(),
287            },
288        }
289    }
290}
291
292impl FromStr for TargetAddr {
293    type Err = ProxyConfigError;
294
295    fn from_str(s: &str) -> Result<Self, Self::Err> {
296        use ProxyConfigError as PCE;
297
298        /// Return true if 's' looks like an attempted IPv4 or IPv6 socketaddr.
299        fn looks_like_attempted_addr(s: &str) -> bool {
300            s.starts_with(|c: char| c.is_ascii_digit())
301                || s.strip_prefix('[')
302                    .map(|rhs| rhs.starts_with(|c: char| c.is_ascii_hexdigit() || c == ':'))
303                    .unwrap_or(false)
304        }
305
306        #[cfg(unix)]
307        if let Some(path) = s.strip_prefix("unix:") {
308            return Ok(Self::Unix(UnixSocketAddr::from_pathname(path).map_err(
309                |e| ProxyConfigError::InvalidUnixAddr {
310                    path: s.to_string(),
311                    source_error: Arc::new(e),
312                },
313            )?));
314        }
315        if let Some(addr) = s.strip_prefix("inet:") {
316            Ok(Self::Inet(addr.parse().map_err(|e| {
317                PCE::InvalidTargetAddr(addr.to_string(), e)
318            })?))
319        } else if looks_like_attempted_addr(s) {
320            // We check 'looks_like_attempted_addr' before parsing this.
321            Ok(Self::Inet(
322                s.parse()
323                    .map_err(|e| PCE::InvalidTargetAddr(s.to_string(), e))?,
324            ))
325        } else {
326            Err(PCE::UnrecognizedTargetType(s.to_string()))
327        }
328    }
329}
330
331impl std::fmt::Display for TargetAddr {
332    #![allow(clippy::disallowed_methods)]
333    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
334        match self {
335            TargetAddr::Inet(a) => write!(f, "inet:{}", a),
336            #[cfg(unix)]
337            TargetAddr::Unix(p) => match p.as_pathname() {
338                Some(path) => write!(f, "unix:{}", path.display()),
339                None => write!(f, "unix:<unnamed>"),
340            },
341        }
342    }
343}
344
345/// The method by which we encapsulate a forwarded request.
346///
347/// (Right now, only `Simple` is supported, but we may later support
348/// "HTTP CONNECT", "HAProxy", or others.)
349#[derive(Clone, Debug, Default, Eq, PartialEq)]
350#[non_exhaustive]
351pub enum Encapsulation {
352    /// Handle a request by opening a local socket to the target address and
353    /// forwarding the contents verbatim.
354    ///
355    /// This does not transmit any information about the circuit origin of the request;
356    /// only the local port will distinguish one request from another.
357    #[default]
358    Simple,
359}
360
361impl FromStr for ProxyAction {
362    type Err = ProxyConfigError;
363
364    fn from_str(s: &str) -> Result<Self, Self::Err> {
365        if s == "destroy" {
366            Ok(Self::DestroyCircuit)
367        } else if s == "reject" {
368            Ok(Self::RejectStream)
369        } else if s == "ignore" {
370            Ok(Self::IgnoreStream)
371        } else if let Some(addr) = s.strip_prefix("simple:") {
372            Ok(Self::Forward(Encapsulation::Simple, addr.parse()?))
373        } else {
374            Ok(Self::Forward(Encapsulation::Simple, s.parse()?))
375        }
376    }
377}
378
379impl std::fmt::Display for ProxyAction {
380    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
381        match self {
382            ProxyAction::DestroyCircuit => write!(f, "destroy"),
383            ProxyAction::Forward(Encapsulation::Simple, addr) => write!(f, "simple:{}", addr),
384            ProxyAction::RejectStream => write!(f, "reject"),
385            ProxyAction::IgnoreStream => write!(f, "ignore"),
386        }
387    }
388}
389
390/// An error encountered while parsing or applying a proxy configuration.
391#[derive(Debug, Clone, thiserror::Error)]
392#[non_exhaustive]
393pub enum ProxyConfigError {
394    /// We encountered a proxy target with an unrecognized type keyword.
395    #[error("Could not parse onion service target type {0:?}")]
396    UnrecognizedTargetType(String),
397
398    /// A socket address could not be parsed to be invalid.
399    #[error("Could not parse onion service target address {0:?}")]
400    InvalidTargetAddr(String, #[source] std::net::AddrParseError),
401
402    /// A unix socket address could not be parsed to be invalid.
403    /// Only supported on Unix platforms.
404    #[error("Invalid unix socket address:'{path}': '{source_error}'")]
405    InvalidUnixAddr {
406        /// The path that was attempted to be parsed as a unix socket address.
407        path: String,
408        #[source]
409        /// The error that was encountered while parsing the unix socket address.
410        source_error: Arc<std::io::Error>,
411    },
412
413    /// A socket rule had an source port that couldn't be parsed as a `u16`.
414    #[error("Could not parse onion service source port {0:?}")]
415    InvalidPort(String, #[source] std::num::ParseIntError),
416
417    /// A socket rule had a zero source port.
418    #[error("Zero is not a valid port.")]
419    ZeroPort,
420
421    /// A socket rule specified an empty port range.
422    #[error("Port range is empty.")]
423    EmptyPortRange,
424}
425
426#[cfg(test)]
427mod test {
428    // @@ begin test lint list maintained by maint/add_warning @@
429    #![allow(clippy::bool_assert_comparison)]
430    #![allow(clippy::clone_on_copy)]
431    #![allow(clippy::dbg_macro)]
432    #![allow(clippy::mixed_attributes_style)]
433    #![allow(clippy::print_stderr)]
434    #![allow(clippy::print_stdout)]
435    #![allow(clippy::single_char_pattern)]
436    #![allow(clippy::unwrap_used)]
437    #![allow(clippy::unchecked_time_subtraction)]
438    #![allow(clippy::useless_vec)]
439    #![allow(clippy::needless_pass_by_value)]
440    #![allow(clippy::string_slice)] // See arti#2571
441    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
442    use super::*;
443
444    #[test]
445    fn pattern_ok() {
446        use ProxyPattern as P;
447        assert_eq!(P::from_str("*").unwrap(), P(1..=65535));
448        assert_eq!(P::from_str("100").unwrap(), P(100..=100));
449        assert_eq!(P::from_str("100-200").unwrap(), P(100..=200));
450        assert_eq!(P::from_str("0-200").unwrap(), P(1..=200));
451    }
452
453    #[test]
454    fn pattern_display() {
455        use ProxyPattern as P;
456        assert_eq!(P::all_ports().to_string(), "*");
457        assert_eq!(P::one_port(100).unwrap().to_string(), "100");
458        assert_eq!(P::port_range(100, 200).unwrap().to_string(), "100-200");
459    }
460
461    #[test]
462    fn pattern_err() {
463        use ProxyConfigError as PCE;
464        use ProxyPattern as P;
465        assert!(matches!(P::from_str("fred"), Err(PCE::InvalidPort(_, _))));
466        assert!(matches!(
467            P::from_str("100-fred"),
468            Err(PCE::InvalidPort(_, _))
469        ));
470        assert!(matches!(P::from_str("100-42"), Err(PCE::EmptyPortRange)));
471    }
472
473    #[test]
474    fn target_ok() {
475        use Encapsulation::Simple;
476        use ProxyAction as T;
477        use TargetAddr as A;
478        assert!(matches!(T::from_str("reject"), Ok(T::RejectStream)));
479        assert!(matches!(T::from_str("ignore"), Ok(T::IgnoreStream)));
480        assert!(matches!(T::from_str("destroy"), Ok(T::DestroyCircuit)));
481        let sa: SocketAddr = "192.168.1.1:50".parse().unwrap();
482        assert!(
483            matches!(T::from_str("192.168.1.1:50"), Ok(T::Forward(Simple, A::Inet(a))) if a == sa)
484        );
485        assert!(
486            matches!(T::from_str("inet:192.168.1.1:50"), Ok(T::Forward(Simple, A::Inet(a))) if a == sa)
487        );
488        let sa: SocketAddr = "[::1]:999".parse().unwrap();
489        assert!(matches!(T::from_str("[::1]:999"), Ok(T::Forward(Simple, A::Inet(a))) if a == sa));
490        assert!(
491            matches!(T::from_str("inet:[::1]:999"), Ok(T::Forward(Simple, A::Inet(a))) if a == sa)
492        );
493        /* TODO (#1246)
494        let pb = PathBuf::from("/var/run/hs/socket");
495        assert!(
496            matches!(T::from_str("unix:/var/run/hs/socket"), Ok(T::Forward(Simple, A::Unix(p))) if p == pb)
497        );
498        */
499    }
500
501    #[test]
502    fn target_display() {
503        use Encapsulation::Simple;
504        use ProxyAction as T;
505        use TargetAddr as A;
506
507        assert_eq!(T::RejectStream.to_string(), "reject");
508        assert_eq!(T::IgnoreStream.to_string(), "ignore");
509        assert_eq!(T::DestroyCircuit.to_string(), "destroy");
510        assert_eq!(
511            T::Forward(Simple, A::Inet("192.168.1.1:50".parse().unwrap())).to_string(),
512            "simple:inet:192.168.1.1:50"
513        );
514        assert_eq!(
515            T::Forward(Simple, A::Inet("[::1]:999".parse().unwrap())).to_string(),
516            "simple:inet:[::1]:999"
517        );
518        /* TODO (#1246)
519        assert_eq!(
520            T::Forward(Simple, A::Unix("/var/run/hs/socket".into())).to_string(),
521            "simple:unix:/var/run/hs/socket"
522        );
523        */
524    }
525
526    #[test]
527    fn target_err() {
528        use ProxyAction as T;
529        use ProxyConfigError as PCE;
530
531        assert!(matches!(
532            T::from_str("sdakljf"),
533            Err(PCE::UnrecognizedTargetType(_))
534        ));
535
536        assert!(matches!(
537            T::from_str("inet:hello"),
538            Err(PCE::InvalidTargetAddr(_, _))
539        ));
540        assert!(matches!(
541            T::from_str("inet:wwww.example.com:80"),
542            Err(PCE::InvalidTargetAddr(_, _))
543        ));
544
545        assert!(matches!(
546            T::from_str("127.1:80"),
547            Err(PCE::InvalidTargetAddr(_, _))
548        ));
549        assert!(matches!(
550            T::from_str("inet:127.1:80"),
551            Err(PCE::InvalidTargetAddr(_, _))
552        ));
553        assert!(matches!(
554            T::from_str("127.1:80"),
555            Err(PCE::InvalidTargetAddr(_, _))
556        ));
557        assert!(matches!(
558            T::from_str("inet:2130706433:80"),
559            Err(PCE::InvalidTargetAddr(_, _))
560        ));
561
562        assert!(matches!(
563            T::from_str("128.256.cats.and.dogs"),
564            Err(PCE::InvalidTargetAddr(_, _))
565        ));
566    }
567
568    #[test]
569    fn deserialize() {
570        use Encapsulation::Simple;
571        use TargetAddr as A;
572        let ex = r#"{
573            "proxy_ports": [
574                [ "443", "127.0.0.1:11443" ],
575                [ "80", "ignore" ],
576                [ "*", "destroy" ]
577            ]
578        }"#;
579        let bld: ProxyConfigBuilder = serde_json::from_str(ex).unwrap();
580        let cfg = bld.build().unwrap();
581        assert_eq!(cfg.proxy_ports.len(), 3);
582        assert_eq!(cfg.proxy_ports[0].source.0, 443..=443);
583        assert_eq!(cfg.proxy_ports[1].source.0, 80..=80);
584        assert_eq!(cfg.proxy_ports[2].source.0, 1..=65535);
585
586        assert_eq!(
587            cfg.proxy_ports[0].target,
588            ProxyAction::Forward(Simple, A::Inet("127.0.0.1:11443".parse().unwrap()))
589        );
590        assert_eq!(cfg.proxy_ports[1].target, ProxyAction::IgnoreStream);
591        assert_eq!(cfg.proxy_ports[2].target, ProxyAction::DestroyCircuit);
592    }
593
594    #[test]
595    fn validation_fail() {
596        // this should fail; the third pattern isn't reachable.
597        let ex = r#"{
598            "proxy_ports": [
599                [ "2-300", "127.0.0.1:11443" ],
600                [ "301-999", "ignore" ],
601                [ "30-310", "destroy" ]
602            ]
603        }"#;
604        let bld: ProxyConfigBuilder = serde_json::from_str(ex).unwrap();
605        match bld.build() {
606            Err(ConfigBuildError::Invalid { field, problem }) => {
607                assert_eq!(field, "proxy_ports");
608                assert_eq!(problem, "Port pattern 30-310 is not reachable");
609            }
610            other => panic!("Expected an Invalid error; got {other:?}"),
611        }
612
613        // This should work; the third pattern is not completely covered.
614        let ex = r#"{
615            "proxy_ports": [
616                [ "2-300", "127.0.0.1:11443" ],
617                [ "302-999", "ignore" ],
618                [ "30-310", "destroy" ]
619            ]
620        }"#;
621        let bld: ProxyConfigBuilder = serde_json::from_str(ex).unwrap();
622        assert!(bld.build().is_ok());
623    }
624
625    #[test]
626    fn demo() {
627        let b: ProxyConfigBuilder = toml::de::from_str(
628            r#"
629proxy_ports = [
630    [ 80, "127.0.0.1:10080"],
631    ["22", "destroy"],
632    ["265", "ignore"],
633    # ["1-1024", "unix:/var/run/allium-cepa/socket"], # TODO (#1246))
634]
635"#,
636        )
637        .unwrap();
638        let c = b.build().unwrap();
639        assert_eq!(c.proxy_ports.len(), 3);
640        assert_eq!(
641            c.proxy_ports[0],
642            ProxyRule::new(
643                ProxyPattern::one_port(80).unwrap(),
644                ProxyAction::Forward(
645                    Encapsulation::Simple,
646                    TargetAddr::Inet("127.0.0.1:10080".parse().unwrap())
647                )
648            )
649        );
650        assert_eq!(
651            c.proxy_ports[1],
652            ProxyRule::new(
653                ProxyPattern::one_port(22).unwrap(),
654                ProxyAction::DestroyCircuit
655            )
656        );
657        assert_eq!(
658            c.proxy_ports[2],
659            ProxyRule::new(
660                ProxyPattern::one_port(265).unwrap(),
661                ProxyAction::IgnoreStream
662            )
663        );
664        /* TODO (#1246)
665        assert_eq!(
666            c.proxy_ports[3],
667            ProxyRule::new(
668                ProxyPattern::port_range(1, 1024).unwrap(),
669                ProxyAction::Forward(
670                    Encapsulation::Simple,
671                    TargetAddr::Unix("/var/run/allium-cepa/socket".into())
672                )
673            )
674        );
675        */
676    }
677}