tor_hsrproxy/
config.rs

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