1
//! Configuration logic for onion service reverse proxy.
2

            
3
use derive_deftly::Deftly;
4
use serde::{Deserialize, Serialize};
5
use std::{net::SocketAddr, ops::RangeInclusive, str::FromStr};
6
use tor_config::ConfigBuildError;
7
use tor_config::derive::prelude::*;
8
use tracing::warn;
9

            
10
/// Configuration for a reverse proxy running for one onion service.
11
#[derive(Clone, Debug, Deftly, Eq, PartialEq)]
12
#[derive_deftly(TorConfig)]
13
#[deftly(tor_config(no_default_trait, pre_build = "Self::validate"))]
14
pub struct ProxyConfig {
15
    /// A list of rules to apply to incoming requests.  If no rule
16
    /// matches, we take the DestroyCircuit action.
17
    #[deftly(tor_config(list(element(clone), listtype = "ProxyRuleList"), default = "vec![]"))]
18
    pub(crate) proxy_ports: Vec<ProxyRule>,
19
    //
20
    // TODO: Someday we may want to allow udp, resolve, etc.  If we do, it will
21
    // be via another option, rather than adding another subtype to ProxySource.
22
}
23

            
24
impl ProxyConfigBuilder {
25
    /// Run checks on this ProxyConfig to ensure that it's valid.
26
274
    fn validate(&self) -> Result<(), ConfigBuildError> {
27
        // Make sure that every proxy pattern is actually reachable.
28
274
        let mut covered = rangemap::RangeInclusiveSet::<u16>::new();
29
318
        for rule in self.proxy_ports.access_opt().iter().flatten() {
30
318
            let range = &rule.source.0;
31
318
            if covered.gaps(range).next().is_none() {
32
2
                return Err(ConfigBuildError::Invalid {
33
2
                    field: "proxy_ports".into(),
34
2
                    problem: format!("Port pattern {} is not reachable", rule.source),
35
2
                });
36
316
            }
37
316
            covered.insert(range.clone());
38
        }
39

            
40
        // Warn about proxy setups that are likely to be surprising.
41
272
        let mut any_forward = false;
42
312
        for rule in self.proxy_ports.access_opt().iter().flatten() {
43
312
            if let ProxyAction::Forward(_, target) = &rule.target {
44
62
                any_forward = true;
45
62
                if !target.is_sufficiently_private() {
46
                    // TODO: here and below, we might want to someday
47
                    // have a mechanism to suppress these warnings,
48
                    // or have them show up only when relevant.
49
                    // For now they are unconditional.
50
                    // See discussion at #1154.
51
                    warn!(
52
                        "Onion service target {} does not look like a private address. \
53
                         Do you really mean to send connections onto the public internet?",
54
                        target
55
                    );
56
62
                }
57
250
            }
58
        }
59

            
60
272
        if !any_forward {
61
210
            warn!("Onion service is not configured to accept any connections.");
62
62
        }
63

            
64
272
        Ok(())
65
274
    }
66
}
67

            
68
impl ProxyConfig {
69
    /// Find the configured action to use when receiving a request for a
70
    /// connection on a given port.
71
    pub(crate) fn resolve_port_for_begin(&self, port: u16) -> Option<&ProxyAction> {
72
        self.proxy_ports
73
            .iter()
74
            .find(|rule| rule.source.matches_port(port))
75
            .map(|rule| &rule.target)
76
    }
77
}
78

            
79
/// A single rule in a `ProxyConfig`.
80
///
81
/// Rules take the form of, "When this pattern matches, take this action."
82
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
83
// TODO: we might someday want to accept structs here as well, so that
84
// we can add per-rule fields if we need to.  We can make that an option if/when
85
// it comes up, however.
86
#[serde(from = "ProxyRuleAsTuple", into = "ProxyRuleAsTuple")]
87
pub struct ProxyRule {
88
    /// Any connections to a port matching this pattern match this rule.
89
    source: ProxyPattern,
90
    /// When this rule matches, we take this action.
91
    target: ProxyAction,
92
}
93

            
94
/// Helper type used to (de)serialize ProxyRule.
95
type ProxyRuleAsTuple = (ProxyPattern, ProxyAction);
96
impl From<ProxyRuleAsTuple> for ProxyRule {
97
283
    fn from(value: ProxyRuleAsTuple) -> Self {
98
283
        Self {
99
283
            source: value.0,
100
283
            target: value.1,
101
283
        }
102
283
    }
103
}
104
impl From<ProxyRule> for ProxyRuleAsTuple {
105
    fn from(value: ProxyRule) -> Self {
106
        (value.source, value.target)
107
    }
108
}
109
impl ProxyRule {
110
    /// Create a new ProxyRule mapping `source` to `target`.
111
41
    pub fn new(source: ProxyPattern, target: ProxyAction) -> Self {
112
41
        Self { source, target }
113
41
    }
114
}
115

            
116
/// A set of ports to use when checking how to handle a port.
117
#[derive(Clone, Debug, serde::Deserialize, serde_with::SerializeDisplay, Eq, PartialEq)]
118
#[serde(try_from = "ProxyPatternAsEnum")]
119
pub struct ProxyPattern(RangeInclusive<u16>);
120

            
121
/// Representation for a [`ProxyPattern`]. Used while deserializing.
122
#[derive(serde::Deserialize)]
123
#[serde(untagged)]
124
enum ProxyPatternAsEnum {
125
    /// Representation the [`ProxyPattern`] as an integer.
126
    Number(u16),
127
    /// Representation of the [`ProxyPattern`] as a string.
128
    String(String),
129
}
130

            
131
impl TryFrom<ProxyPatternAsEnum> for ProxyPattern {
132
    type Error = ProxyConfigError;
133

            
134
283
    fn try_from(value: ProxyPatternAsEnum) -> Result<Self, Self::Error> {
135
283
        match value {
136
2
            ProxyPatternAsEnum::Number(port) => Self::one_port(port),
137
281
            ProxyPatternAsEnum::String(s) => Self::from_str(&s),
138
        }
139
283
    }
140
}
141

            
142
impl FromStr for ProxyPattern {
143
    type Err = ProxyConfigError;
144

            
145
295
    fn from_str(s: &str) -> Result<Self, Self::Err> {
146
        use ProxyConfigError as PCE;
147
295
        if s == "*" {
148
193
            Ok(Self::all_ports())
149
102
        } else if let Some((left, right)) = s.split_once('-') {
150
20
            let left: u16 = left
151
20
                .parse()
152
20
                .map_err(|e| PCE::InvalidPort(left.to_string(), e))?;
153
20
            let right: u16 = right
154
20
                .parse()
155
21
                .map_err(|e| PCE::InvalidPort(right.to_string(), e))?;
156
18
            Self::port_range(left, right)
157
        } else {
158
83
            let port = s.parse().map_err(|e| PCE::InvalidPort(s.to_string(), e))?;
159
80
            Self::one_port(port)
160
        }
161
295
    }
162
}
163
impl std::fmt::Display for ProxyPattern {
164
8
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
165
8
        match self.0.clone().into_inner() {
166
8
            (start, end) if start == end => write!(f, "{}", start),
167
2
            (1, 65535) => write!(f, "*"),
168
4
            (start, end) => write!(f, "{}-{}", start, end),
169
        }
170
8
    }
171
}
172

            
173
impl ProxyPattern {
174
    /// Return a pattern matching all ports.
175
202
    pub fn all_ports() -> Self {
176
202
        Self::check(1, 65535).expect("Somehow, 1-65535 was not a valid pattern")
177
202
    }
178
    /// Return a pattern matching a single port.
179
    ///
180
    /// Gives an error if the port is zero.
181
118
    pub fn one_port(port: u16) -> Result<Self, ProxyConfigError> {
182
118
        Self::check(port, port)
183
118
    }
184
    /// Return a pattern matching all ports between `low` and `high` inclusive.
185
    ///
186
    /// Gives an error unless `0 < low <= high`.
187
20
    pub fn port_range(low: u16, high: u16) -> Result<Self, ProxyConfigError> {
188
20
        Self::check(low, high)
189
20
    }
190

            
191
    /// Return true if this pattern includes `port`.
192
    pub(crate) fn matches_port(&self, port: u16) -> bool {
193
        self.0.contains(&port)
194
    }
195

            
196
    /// If start..=end is a valid pattern, wrap it as a ProxyPattern. Otherwise return
197
    /// an error.
198
340
    fn check(start: u16, end: u16) -> Result<ProxyPattern, ProxyConfigError> {
199
        use ProxyConfigError as PCE;
200
340
        match (start, end) {
201
            (_, 0) => Err(PCE::ZeroPort),
202
2
            (0, n) => Ok(Self(1..=n)),
203
338
            (low, high) if low > high => Err(PCE::EmptyPortRange),
204
336
            (low, high) => Ok(Self(low..=high)),
205
        }
206
340
    }
207
}
208

            
209
/// An action to take upon receiving an incoming request.
210
//
211
// The variant names (but not the payloads) are part of the metrics schema.
212
// When changing them, see `doc/dev/MetricsStrategy.md` re schema stability policy.
213
#[derive(
214
    Clone,
215
    Debug,
216
    Default,
217
    serde_with::DeserializeFromStr,
218
    serde_with::SerializeDisplay,
219
    Eq,
220
    PartialEq,
221
    strum::EnumDiscriminants,
222
)]
223
#[strum_discriminants(derive(Hash, strum::EnumIter))] //
224
#[strum_discriminants(derive(strum::IntoStaticStr), strum(serialize_all = "snake_case"))]
225
#[strum_discriminants(vis(pub(crate)))]
226
#[non_exhaustive]
227
pub enum ProxyAction {
228
    /// Close the circuit immediately with an error.
229
    #[default]
230
    DestroyCircuit,
231
    /// Accept the client's request and forward it, via some encapsulation method,
232
    /// to some target address.
233
    Forward(Encapsulation, TargetAddr),
234
    /// Close the stream immediately with an error.
235
    RejectStream,
236
    /// Ignore the stream request.
237
    IgnoreStream,
238
}
239

            
240
/// The address to which we forward an accepted connection.
241
#[derive(Clone, Debug, Eq, PartialEq)]
242
#[non_exhaustive]
243
pub enum TargetAddr {
244
    /// An address that we can reach over the internet.
245
    Inet(SocketAddr),
246
    /* TODO (#1246): Put this back.
247
    /// An address of a local unix domain socket.
248
    Unix(PathBuf),
249
    */
250
}
251

            
252
impl TargetAddr {
253
    /// Return true if this target is sufficiently private that we can be
254
    /// reasonably sure that the user has not misconfigured their onion service
255
    /// to relay traffic onto the public network.
256
62
    fn is_sufficiently_private(&self) -> bool {
257
        use std::net::IpAddr;
258
62
        match self {
259
            /* TODO(#1246) */
260
            // TargetAddr::Unix(_) => true,
261

            
262
            // NOTE: We may want to relax these rules in the future!
263
            // NOTE: Contrast this with is_local in arti_client::address,
264
            // which has a different purpose. Also see #1159.
265
            // The purpose of _this_ test is to make sure that the address is
266
            // one that will _probably_ not go over the public internet.
267
62
            TargetAddr::Inet(sa) => match sa.ip() {
268
62
                IpAddr::V4(ip) => ip.is_loopback() || ip.is_unspecified() || ip.is_private(),
269
                IpAddr::V6(ip) => ip.is_loopback() || ip.is_unspecified(),
270
            },
271
        }
272
62
    }
273
}
274

            
275
impl FromStr for TargetAddr {
276
    type Err = ProxyConfigError;
277

            
278
81
    fn from_str(s: &str) -> Result<Self, Self::Err> {
279
        use ProxyConfigError as PCE;
280

            
281
        /// Return true if 's' looks like an attempted IPv4 or IPv6 socketaddr.
282
69
        fn looks_like_attempted_addr(s: &str) -> bool {
283
86
            s.starts_with(|c: char| c.is_ascii_digit())
284
4
                || s.strip_prefix('[')
285
5
                    .map(|rhs| rhs.starts_with(|c: char| c.is_ascii_hexdigit() || c == ':'))
286
4
                    .unwrap_or(false)
287
69
        }
288
        /* TODO (#1246): Put this back
289
        if let Some(path) = s.strip_prefix("unix:") {
290
            Ok(Self::Unix(PathBuf::from(path)))
291
        } else
292
        */
293
81
        if let Some(addr) = s.strip_prefix("inet:") {
294
16
            Ok(Self::Inet(addr.parse().map_err(|e| {
295
8
                PCE::InvalidTargetAddr(addr.to_string(), e)
296
12
            })?))
297
69
        } else if looks_like_attempted_addr(s) {
298
            // We check 'looks_like_attempted_addr' before parsing this.
299
            Ok(Self::Inet(
300
67
                s.parse()
301
70
                    .map_err(|e| PCE::InvalidTargetAddr(s.to_string(), e))?,
302
            ))
303
        } else {
304
2
            Err(PCE::UnrecognizedTargetType(s.to_string()))
305
        }
306
81
    }
307
}
308

            
309
impl std::fmt::Display for TargetAddr {
310
4
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
311
4
        match self {
312
4
            TargetAddr::Inet(a) => write!(f, "inet:{}", a),
313
            // TODO (#1246): Put this back.
314
            // TargetAddr::Unix(p) => write!(f, "unix:{}", p.display()),
315
        }
316
4
    }
317
}
318

            
319
/// The method by which we encapsulate a forwarded request.
320
///
321
/// (Right now, only `Simple` is supported, but we may later support
322
/// "HTTP CONNECT", "HAProxy", or others.)
323
#[derive(Clone, Debug, Default, Eq, PartialEq)]
324
#[non_exhaustive]
325
pub enum Encapsulation {
326
    /// Handle a request by opening a local socket to the target address and
327
    /// forwarding the contents verbatim.
328
    ///
329
    /// This does not transmit any information about the circuit origin of the request;
330
    /// only the local port will distinguish one request from another.
331
    #[default]
332
    Simple,
333
}
334

            
335
impl FromStr for ProxyAction {
336
    type Err = ProxyConfigError;
337

            
338
313
    fn from_str(s: &str) -> Result<Self, Self::Err> {
339
313
        if s == "destroy" {
340
206
            Ok(Self::DestroyCircuit)
341
107
        } else if s == "reject" {
342
9
            Ok(Self::RejectStream)
343
98
        } else if s == "ignore" {
344
17
            Ok(Self::IgnoreStream)
345
81
        } else if let Some(addr) = s.strip_prefix("simple:") {
346
            Ok(Self::Forward(Encapsulation::Simple, addr.parse()?))
347
        } else {
348
81
            Ok(Self::Forward(Encapsulation::Simple, s.parse()?))
349
        }
350
313
    }
351
}
352

            
353
impl std::fmt::Display for ProxyAction {
354
10
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
355
4
        match self {
356
2
            ProxyAction::DestroyCircuit => write!(f, "destroy"),
357
4
            ProxyAction::Forward(Encapsulation::Simple, addr) => write!(f, "simple:{}", addr),
358
2
            ProxyAction::RejectStream => write!(f, "reject"),
359
2
            ProxyAction::IgnoreStream => write!(f, "ignore"),
360
        }
361
10
    }
362
}
363

            
364
/// An error encountered while parsing or applying a proxy configuration.
365
#[derive(Debug, Clone, thiserror::Error)]
366
#[non_exhaustive]
367
pub enum ProxyConfigError {
368
    /// We encountered a proxy target with an unrecognized type keyword.
369
    #[error("Could not parse onion service target type {0:?}")]
370
    UnrecognizedTargetType(String),
371

            
372
    /// A socket address could not be parsed to be invalid.
373
    #[error("Could not parse onion service target address {0:?}")]
374
    InvalidTargetAddr(String, #[source] std::net::AddrParseError),
375

            
376
    /// A socket rule had an source port that couldn't be parsed as a `u16`.
377
    #[error("Could not parse onion service source port {0:?}")]
378
    InvalidPort(String, #[source] std::num::ParseIntError),
379

            
380
    /// A socket rule had a zero source port.
381
    #[error("Zero is not a valid port.")]
382
    ZeroPort,
383

            
384
    /// A socket rule specified an empty port range.
385
    #[error("Port range is empty.")]
386
    EmptyPortRange,
387
}
388

            
389
#[cfg(test)]
390
mod test {
391
    // @@ begin test lint list maintained by maint/add_warning @@
392
    #![allow(clippy::bool_assert_comparison)]
393
    #![allow(clippy::clone_on_copy)]
394
    #![allow(clippy::dbg_macro)]
395
    #![allow(clippy::mixed_attributes_style)]
396
    #![allow(clippy::print_stderr)]
397
    #![allow(clippy::print_stdout)]
398
    #![allow(clippy::single_char_pattern)]
399
    #![allow(clippy::unwrap_used)]
400
    #![allow(clippy::unchecked_time_subtraction)]
401
    #![allow(clippy::useless_vec)]
402
    #![allow(clippy::needless_pass_by_value)]
403
    #![allow(clippy::string_slice)] // See arti#2571
404
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
405
    use super::*;
406

            
407
    #[test]
408
    fn pattern_ok() {
409
        use ProxyPattern as P;
410
        assert_eq!(P::from_str("*").unwrap(), P(1..=65535));
411
        assert_eq!(P::from_str("100").unwrap(), P(100..=100));
412
        assert_eq!(P::from_str("100-200").unwrap(), P(100..=200));
413
        assert_eq!(P::from_str("0-200").unwrap(), P(1..=200));
414
    }
415

            
416
    #[test]
417
    fn pattern_display() {
418
        use ProxyPattern as P;
419
        assert_eq!(P::all_ports().to_string(), "*");
420
        assert_eq!(P::one_port(100).unwrap().to_string(), "100");
421
        assert_eq!(P::port_range(100, 200).unwrap().to_string(), "100-200");
422
    }
423

            
424
    #[test]
425
    fn pattern_err() {
426
        use ProxyConfigError as PCE;
427
        use ProxyPattern as P;
428
        assert!(matches!(P::from_str("fred"), Err(PCE::InvalidPort(_, _))));
429
        assert!(matches!(
430
            P::from_str("100-fred"),
431
            Err(PCE::InvalidPort(_, _))
432
        ));
433
        assert!(matches!(P::from_str("100-42"), Err(PCE::EmptyPortRange)));
434
    }
435

            
436
    #[test]
437
    fn target_ok() {
438
        use Encapsulation::Simple;
439
        use ProxyAction as T;
440
        use TargetAddr as A;
441
        assert!(matches!(T::from_str("reject"), Ok(T::RejectStream)));
442
        assert!(matches!(T::from_str("ignore"), Ok(T::IgnoreStream)));
443
        assert!(matches!(T::from_str("destroy"), Ok(T::DestroyCircuit)));
444
        let sa: SocketAddr = "192.168.1.1:50".parse().unwrap();
445
        assert!(
446
            matches!(T::from_str("192.168.1.1:50"), Ok(T::Forward(Simple, A::Inet(a))) if a == sa)
447
        );
448
        assert!(
449
            matches!(T::from_str("inet:192.168.1.1:50"), Ok(T::Forward(Simple, A::Inet(a))) if a == sa)
450
        );
451
        let sa: SocketAddr = "[::1]:999".parse().unwrap();
452
        assert!(matches!(T::from_str("[::1]:999"), Ok(T::Forward(Simple, A::Inet(a))) if a == sa));
453
        assert!(
454
            matches!(T::from_str("inet:[::1]:999"), Ok(T::Forward(Simple, A::Inet(a))) if a == sa)
455
        );
456
        /* TODO (#1246)
457
        let pb = PathBuf::from("/var/run/hs/socket");
458
        assert!(
459
            matches!(T::from_str("unix:/var/run/hs/socket"), Ok(T::Forward(Simple, A::Unix(p))) if p == pb)
460
        );
461
        */
462
    }
463

            
464
    #[test]
465
    fn target_display() {
466
        use Encapsulation::Simple;
467
        use ProxyAction as T;
468
        use TargetAddr as A;
469

            
470
        assert_eq!(T::RejectStream.to_string(), "reject");
471
        assert_eq!(T::IgnoreStream.to_string(), "ignore");
472
        assert_eq!(T::DestroyCircuit.to_string(), "destroy");
473
        assert_eq!(
474
            T::Forward(Simple, A::Inet("192.168.1.1:50".parse().unwrap())).to_string(),
475
            "simple:inet:192.168.1.1:50"
476
        );
477
        assert_eq!(
478
            T::Forward(Simple, A::Inet("[::1]:999".parse().unwrap())).to_string(),
479
            "simple:inet:[::1]:999"
480
        );
481
        /* TODO (#1246)
482
        assert_eq!(
483
            T::Forward(Simple, A::Unix("/var/run/hs/socket".into())).to_string(),
484
            "simple:unix:/var/run/hs/socket"
485
        );
486
        */
487
    }
488

            
489
    #[test]
490
    fn target_err() {
491
        use ProxyAction as T;
492
        use ProxyConfigError as PCE;
493

            
494
        assert!(matches!(
495
            T::from_str("sdakljf"),
496
            Err(PCE::UnrecognizedTargetType(_))
497
        ));
498

            
499
        assert!(matches!(
500
            T::from_str("inet:hello"),
501
            Err(PCE::InvalidTargetAddr(_, _))
502
        ));
503
        assert!(matches!(
504
            T::from_str("inet:wwww.example.com:80"),
505
            Err(PCE::InvalidTargetAddr(_, _))
506
        ));
507

            
508
        assert!(matches!(
509
            T::from_str("127.1:80"),
510
            Err(PCE::InvalidTargetAddr(_, _))
511
        ));
512
        assert!(matches!(
513
            T::from_str("inet:127.1:80"),
514
            Err(PCE::InvalidTargetAddr(_, _))
515
        ));
516
        assert!(matches!(
517
            T::from_str("127.1:80"),
518
            Err(PCE::InvalidTargetAddr(_, _))
519
        ));
520
        assert!(matches!(
521
            T::from_str("inet:2130706433:80"),
522
            Err(PCE::InvalidTargetAddr(_, _))
523
        ));
524

            
525
        assert!(matches!(
526
            T::from_str("128.256.cats.and.dogs"),
527
            Err(PCE::InvalidTargetAddr(_, _))
528
        ));
529
    }
530

            
531
    #[test]
532
    fn deserialize() {
533
        use Encapsulation::Simple;
534
        use TargetAddr as A;
535
        let ex = r#"{
536
            "proxy_ports": [
537
                [ "443", "127.0.0.1:11443" ],
538
                [ "80", "ignore" ],
539
                [ "*", "destroy" ]
540
            ]
541
        }"#;
542
        let bld: ProxyConfigBuilder = serde_json::from_str(ex).unwrap();
543
        let cfg = bld.build().unwrap();
544
        assert_eq!(cfg.proxy_ports.len(), 3);
545
        assert_eq!(cfg.proxy_ports[0].source.0, 443..=443);
546
        assert_eq!(cfg.proxy_ports[1].source.0, 80..=80);
547
        assert_eq!(cfg.proxy_ports[2].source.0, 1..=65535);
548

            
549
        assert_eq!(
550
            cfg.proxy_ports[0].target,
551
            ProxyAction::Forward(Simple, A::Inet("127.0.0.1:11443".parse().unwrap()))
552
        );
553
        assert_eq!(cfg.proxy_ports[1].target, ProxyAction::IgnoreStream);
554
        assert_eq!(cfg.proxy_ports[2].target, ProxyAction::DestroyCircuit);
555
    }
556

            
557
    #[test]
558
    fn validation_fail() {
559
        // this should fail; the third pattern isn't reachable.
560
        let ex = r#"{
561
            "proxy_ports": [
562
                [ "2-300", "127.0.0.1:11443" ],
563
                [ "301-999", "ignore" ],
564
                [ "30-310", "destroy" ]
565
            ]
566
        }"#;
567
        let bld: ProxyConfigBuilder = serde_json::from_str(ex).unwrap();
568
        match bld.build() {
569
            Err(ConfigBuildError::Invalid { field, problem }) => {
570
                assert_eq!(field, "proxy_ports");
571
                assert_eq!(problem, "Port pattern 30-310 is not reachable");
572
            }
573
            other => panic!("Expected an Invalid error; got {other:?}"),
574
        }
575

            
576
        // This should work; the third pattern is not completely covered.
577
        let ex = r#"{
578
            "proxy_ports": [
579
                [ "2-300", "127.0.0.1:11443" ],
580
                [ "302-999", "ignore" ],
581
                [ "30-310", "destroy" ]
582
            ]
583
        }"#;
584
        let bld: ProxyConfigBuilder = serde_json::from_str(ex).unwrap();
585
        assert!(bld.build().is_ok());
586
    }
587

            
588
    #[test]
589
    fn demo() {
590
        let b: ProxyConfigBuilder = toml::de::from_str(
591
            r#"
592
proxy_ports = [
593
    [ 80, "127.0.0.1:10080"],
594
    ["22", "destroy"],
595
    ["265", "ignore"],
596
    # ["1-1024", "unix:/var/run/allium-cepa/socket"], # TODO (#1246))
597
]
598
"#,
599
        )
600
        .unwrap();
601
        let c = b.build().unwrap();
602
        assert_eq!(c.proxy_ports.len(), 3);
603
        assert_eq!(
604
            c.proxy_ports[0],
605
            ProxyRule::new(
606
                ProxyPattern::one_port(80).unwrap(),
607
                ProxyAction::Forward(
608
                    Encapsulation::Simple,
609
                    TargetAddr::Inet("127.0.0.1:10080".parse().unwrap())
610
                )
611
            )
612
        );
613
        assert_eq!(
614
            c.proxy_ports[1],
615
            ProxyRule::new(
616
                ProxyPattern::one_port(22).unwrap(),
617
                ProxyAction::DestroyCircuit
618
            )
619
        );
620
        assert_eq!(
621
            c.proxy_ports[2],
622
            ProxyRule::new(
623
                ProxyPattern::one_port(265).unwrap(),
624
                ProxyAction::IgnoreStream
625
            )
626
        );
627
        /* TODO (#1246)
628
        assert_eq!(
629
            c.proxy_ports[3],
630
            ProxyRule::new(
631
                ProxyPattern::port_range(1, 1024).unwrap(),
632
                ProxyAction::Forward(
633
                    Encapsulation::Simple,
634
                    TargetAddr::Unix("/var/run/allium-cepa/socket".into())
635
                )
636
            )
637
        );
638
        */
639
    }
640
}