1
//! Configuration logic for onion service reverse proxy.
2
use derive_deftly::Deftly;
3
use serde::{Deserialize, Serialize};
4
use std::{net::SocketAddr, ops::RangeInclusive, str::FromStr, sync::Arc};
5
use tor_config::ConfigBuildError;
6
use tor_config::derive::prelude::*;
7
use tracing::warn;
8

            
9
#[cfg(unix)]
10
use 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"))]
15
pub 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

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

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

            
61
209
        if !any_forward {
62
147
            warn!("Onion service is not configured to accept any connections.");
63
62
        }
64

            
65
209
        Ok(())
66
211
    }
67
}
68

            
69
impl 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")]
88
pub 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.
96
type ProxyRuleAsTuple = (ProxyPattern, ProxyAction);
97
impl From<ProxyRuleAsTuple> for ProxyRule {
98
220
    fn from(value: ProxyRuleAsTuple) -> Self {
99
220
        Self {
100
220
            source: value.0,
101
220
            target: value.1,
102
220
        }
103
220
    }
104
}
105
impl From<ProxyRule> for ProxyRuleAsTuple {
106
    fn from(value: ProxyRule) -> Self {
107
        (value.source, value.target)
108
    }
109
}
110
impl ProxyRule {
111
    /// Create a new ProxyRule mapping `source` to `target`.
112
41
    pub fn new(source: ProxyPattern, target: ProxyAction) -> Self {
113
41
        Self { source, target }
114
41
    }
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")]
120
pub struct ProxyPattern(RangeInclusive<u16>);
121

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

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

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

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

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

            
174
impl ProxyPattern {
175
    /// Return a pattern matching all ports.
176
139
    pub fn all_ports() -> Self {
177
139
        Self::check(1, 65535).expect("Somehow, 1-65535 was not a valid pattern")
178
139
    }
179
    /// Return a pattern matching a single port.
180
    ///
181
    /// Gives an error if the port is zero.
182
118
    pub fn one_port(port: u16) -> Result<Self, ProxyConfigError> {
183
118
        Self::check(port, port)
184
118
    }
185
    /// Return a pattern matching all ports between `low` and `high` inclusive.
186
    ///
187
    /// Gives an error unless `0 < low <= high`.
188
20
    pub fn port_range(low: u16, high: u16) -> Result<Self, ProxyConfigError> {
189
20
        Self::check(low, high)
190
20
    }
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
277
    fn check(start: u16, end: u16) -> Result<ProxyPattern, ProxyConfigError> {
200
        use ProxyConfigError as PCE;
201
277
        match (start, end) {
202
            (_, 0) => Err(PCE::ZeroPort),
203
2
            (0, n) => Ok(Self(1..=n)),
204
275
            (low, high) if low > high => Err(PCE::EmptyPortRange),
205
273
            (low, high) => Ok(Self(low..=high)),
206
        }
207
277
    }
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]
228
pub 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]
244
pub 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

            
258
impl PartialEq for TargetAddr {
259
    /// Implement equality for TargetAddr,
260
    /// which we can't automatically derived because UnixSocketAddr does not implement Eq.
261
11
    fn eq(&self, other: &Self) -> bool {
262
11
        match (self, other) {
263
11
            (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
11
    }
269
}
270

            
271
impl Eq for TargetAddr {}
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
            // Unix Socket is private, because it's local
281
            #[cfg(unix)]
282
            TargetAddr::Unix(_) => true,
283

            
284
62
            TargetAddr::Inet(sa) => match sa.ip() {
285
62
                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
62
    }
290
}
291

            
292
impl FromStr for TargetAddr {
293
    type Err = ProxyConfigError;
294

            
295
81
    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
69
        fn looks_like_attempted_addr(s: &str) -> bool {
300
86
            s.starts_with(|c: char| c.is_ascii_digit())
301
4
                || s.strip_prefix('[')
302
5
                    .map(|rhs| rhs.starts_with(|c: char| c.is_ascii_hexdigit() || c == ':'))
303
4
                    .unwrap_or(false)
304
69
        }
305

            
306
        #[cfg(unix)]
307
81
        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
81
        }
315
81
        if let Some(addr) = s.strip_prefix("inet:") {
316
16
            Ok(Self::Inet(addr.parse().map_err(|e| {
317
8
                PCE::InvalidTargetAddr(addr.to_string(), e)
318
12
            })?))
319
69
        } else if looks_like_attempted_addr(s) {
320
            // We check 'looks_like_attempted_addr' before parsing this.
321
            Ok(Self::Inet(
322
67
                s.parse()
323
70
                    .map_err(|e| PCE::InvalidTargetAddr(s.to_string(), e))?,
324
            ))
325
        } else {
326
2
            Err(PCE::UnrecognizedTargetType(s.to_string()))
327
        }
328
81
    }
329
}
330

            
331
impl std::fmt::Display for TargetAddr {
332
    #![allow(clippy::disallowed_methods)]
333
4
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
334
4
        match self {
335
4
            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
4
    }
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]
351
pub 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

            
361
impl FromStr for ProxyAction {
362
    type Err = ProxyConfigError;
363

            
364
250
    fn from_str(s: &str) -> Result<Self, Self::Err> {
365
250
        if s == "destroy" {
366
143
            Ok(Self::DestroyCircuit)
367
107
        } else if s == "reject" {
368
9
            Ok(Self::RejectStream)
369
98
        } else if s == "ignore" {
370
17
            Ok(Self::IgnoreStream)
371
81
        } else if let Some(addr) = s.strip_prefix("simple:") {
372
            Ok(Self::Forward(Encapsulation::Simple, addr.parse()?))
373
        } else {
374
81
            Ok(Self::Forward(Encapsulation::Simple, s.parse()?))
375
        }
376
250
    }
377
}
378

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

            
390
/// An error encountered while parsing or applying a proxy configuration.
391
#[derive(Debug, Clone, thiserror::Error)]
392
#[non_exhaustive]
393
pub 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)]
427
mod 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#"
629
proxy_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
}