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

            
3
use derive_builder::Builder;
4
use derive_deftly::Deftly;
5
use serde::{Deserialize, Serialize};
6
use std::{net::SocketAddr, ops::RangeInclusive, str::FromStr};
7
use tracing::warn;
8
//use tor_config::derive_deftly_template_Flattenable;
9
use tor_config::{define_list_builder_accessors, define_list_builder_helper, ConfigBuildError};
10

            
11
/// Configuration for a reverse proxy running for one onion service.
12
70
#[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))]
16
pub 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

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

            
42
        // Warn about proxy setups that are likely to be surprising.
43
90
        let mut any_forward = false;
44
130
        for rule in self.proxy_ports.access_opt().iter().flatten() {
45
130
            if let ProxyAction::Forward(_, target) = &rule.target {
46
62
                any_forward = true;
47
62
                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
62
                }
59
68
            }
60
        }
61

            
62
90
        if !any_forward {
63
28
            warn!("Onion service is not configured to accept any connections.");
64
62
        }
65

            
66
90
        Ok(())
67
92
    }
68
}
69

            
70
define_list_builder_accessors! {
71
   struct ProxyConfigBuilder {
72
       pub proxy_ports: [ProxyRule],
73
   }
74
}
75

            
76
/// Helper to define builder for ProxyConfig.
77
type ProxyRuleList = Vec<ProxyRule>;
78

            
79
define_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
130
   item_build: |value| Ok(value.clone());
87
}
88

            
89
impl 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")]
108
pub 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.
116
type ProxyRuleAsTuple = (ProxyPattern, ProxyAction);
117
impl From<ProxyRuleAsTuple> for ProxyRule {
118
101
    fn from(value: ProxyRuleAsTuple) -> Self {
119
101
        Self {
120
101
            source: value.0,
121
101
            target: value.1,
122
101
        }
123
101
    }
124
}
125
impl From<ProxyRule> for ProxyRuleAsTuple {
126
    fn from(value: ProxyRule) -> Self {
127
        (value.source, value.target)
128
    }
129
}
130
impl ProxyRule {
131
    /// Create a new ProxyRule mapping `source` to `target`.
132
41
    pub fn new(source: ProxyPattern, target: ProxyAction) -> Self {
133
41
        Self { source, target }
134
41
    }
135
}
136

            
137
/// A set of ports to use when checking how to handle a port.
138
70
#[derive(Clone, Debug, serde::Deserialize, serde_with::SerializeDisplay, Eq, PartialEq)]
139
#[serde(try_from = "ProxyPatternAsEnum")]
140
pub struct ProxyPattern(RangeInclusive<u16>);
141

            
142
/// Representation for a [`ProxyPattern`]. Used while deserializing.
143
#[derive(serde::Deserialize)]
144
#[serde(untagged)]
145
enum ProxyPatternAsEnum {
146
    /// Representation the [`ProxyPattern`] as an integer.
147
    Number(u16),
148
    /// Representation of the [`ProxyPattern`] as a string.
149
    String(String),
150
}
151

            
152
impl TryFrom<ProxyPatternAsEnum> for ProxyPattern {
153
    type Error = ProxyConfigError;
154

            
155
101
    fn try_from(value: ProxyPatternAsEnum) -> Result<Self, Self::Error> {
156
101
        match value {
157
2
            ProxyPatternAsEnum::Number(port) => Self::one_port(port),
158
99
            ProxyPatternAsEnum::String(s) => Self::from_str(&s),
159
        }
160
101
    }
161
}
162

            
163
impl FromStr for ProxyPattern {
164
    type Err = ProxyConfigError;
165

            
166
113
    fn from_str(s: &str) -> Result<Self, Self::Err> {
167
        use ProxyConfigError as PCE;
168
113
        if s == "*" {
169
11
            Ok(Self::all_ports())
170
102
        } else if let Some((left, right)) = s.split_once('-') {
171
20
            let left: u16 = left
172
20
                .parse()
173
20
                .map_err(|e| PCE::InvalidPort(left.to_string(), e))?;
174
20
            let right: u16 = right
175
20
                .parse()
176
21
                .map_err(|e| PCE::InvalidPort(right.to_string(), e))?;
177
18
            Self::port_range(left, right)
178
        } else {
179
83
            let port = s.parse().map_err(|e| PCE::InvalidPort(s.to_string(), e))?;
180
80
            Self::one_port(port)
181
        }
182
113
    }
183
}
184
impl std::fmt::Display for ProxyPattern {
185
8
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
186
8
        match self.0.clone().into_inner() {
187
8
            (start, end) if start == end => write!(f, "{}", start),
188
2
            (1, 65535) => write!(f, "*"),
189
4
            (start, end) => write!(f, "{}-{}", start, end),
190
        }
191
8
    }
192
}
193

            
194
impl ProxyPattern {
195
    /// Return a pattern matching all ports.
196
20
    pub fn all_ports() -> Self {
197
20
        Self::check(1, 65535).expect("Somehow, 1-65535 was not a valid pattern")
198
20
    }
199
    /// Return a pattern matching a single port.
200
    ///
201
    /// Gives an error if the port is zero.
202
118
    pub fn one_port(port: u16) -> Result<Self, ProxyConfigError> {
203
118
        Self::check(port, port)
204
118
    }
205
    /// Return a pattern matching all ports between `low` and `high` inclusive.
206
    ///
207
    /// Gives an error unless `0 < low <= high`.
208
20
    pub fn port_range(low: u16, high: u16) -> Result<Self, ProxyConfigError> {
209
20
        Self::check(low, high)
210
20
    }
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
158
    fn check(start: u16, end: u16) -> Result<ProxyPattern, ProxyConfigError> {
220
        use ProxyConfigError as PCE;
221
158
        match (start, end) {
222
            (_, 0) => Err(PCE::ZeroPort),
223
2
            (0, n) => Ok(Self(1..=n)),
224
156
            (low, high) if low > high => Err(PCE::EmptyPortRange),
225
154
            (low, high) => Ok(Self(low..=high)),
226
        }
227
158
    }
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
70
    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]
248
pub 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]
264
pub 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

            
273
impl 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
62
    fn is_sufficiently_private(&self) -> bool {
278
        use std::net::IpAddr;
279
62
        match self {
280
62
            /* TODO(#1246) */
281
62
            // TargetAddr::Unix(_) => true,
282
62

            
283
62
            // NOTE: We may want to relax these rules in the future!
284
62
            // NOTE: Contrast this with is_local in arti_client::address,
285
62
            // which has a different purpose. Also see #1159.
286
62
            // The purpose of _this_ test is to make sure that the address is
287
62
            // one that will _probably_ not go over the public internet.
288
62
            TargetAddr::Inet(sa) => match sa.ip() {
289
62
                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
62
    }
294
}
295

            
296
impl FromStr for TargetAddr {
297
    type Err = ProxyConfigError;
298

            
299
81
    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
69
        fn looks_like_attempted_addr(s: &str) -> bool {
304
86
            s.starts_with(|c: char| c.is_ascii_digit())
305
4
                || s.strip_prefix('[')
306
5
                    .map(|rhs| rhs.starts_with(|c: char| c.is_ascii_hexdigit() || c == ':'))
307
4
                    .unwrap_or(false)
308
69
        }
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
81
        if let Some(addr) = s.strip_prefix("inet:") {
315
16
            Ok(Self::Inet(addr.parse().map_err(|e| {
316
8
                PCE::InvalidTargetAddr(addr.to_string(), e)
317
16
            })?))
318
69
        } else if looks_like_attempted_addr(s) {
319
            // We check 'looks_like_attempted_addr' before parsing this.
320
            Ok(Self::Inet(
321
67
                s.parse()
322
70
                    .map_err(|e| PCE::InvalidTargetAddr(s.to_string(), e))?,
323
            ))
324
        } else {
325
2
            Err(PCE::UnrecognizedTargetType(s.to_string()))
326
        }
327
81
    }
328
}
329

            
330
impl std::fmt::Display for TargetAddr {
331
4
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
332
4
        match self {
333
4
            TargetAddr::Inet(a) => write!(f, "inet:{}", a),
334
4
            // TODO (#1246): Put this back.
335
4
            // TargetAddr::Unix(p) => write!(f, "unix:{}", p.display()),
336
4
        }
337
4
    }
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]
346
pub 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

            
356
impl FromStr for ProxyAction {
357
    type Err = ProxyConfigError;
358

            
359
131
    fn from_str(s: &str) -> Result<Self, Self::Err> {
360
131
        if s == "destroy" {
361
24
            Ok(Self::DestroyCircuit)
362
107
        } else if s == "reject" {
363
9
            Ok(Self::RejectStream)
364
98
        } else if s == "ignore" {
365
17
            Ok(Self::IgnoreStream)
366
81
        } else if let Some(addr) = s.strip_prefix("simple:") {
367
            Ok(Self::Forward(Encapsulation::Simple, addr.parse()?))
368
        } else {
369
81
            Ok(Self::Forward(Encapsulation::Simple, s.parse()?))
370
        }
371
131
    }
372
}
373

            
374
impl std::fmt::Display for ProxyAction {
375
10
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
376
10
        match self {
377
2
            ProxyAction::DestroyCircuit => write!(f, "destroy"),
378
4
            ProxyAction::Forward(Encapsulation::Simple, addr) => write!(f, "simple:{}", addr),
379
2
            ProxyAction::RejectStream => write!(f, "reject"),
380
2
            ProxyAction::IgnoreStream => write!(f, "ignore"),
381
        }
382
10
    }
383
}
384

            
385
/// An error encountered while parsing or applying a proxy configuration.
386
#[derive(Debug, Clone, thiserror::Error)]
387
#[non_exhaustive]
388
pub 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)]
411
mod 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#"
612
proxy_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
}