Skip to main content

tor_hsrproxy/
config.rs

1//! Configuration logic for onion service reverse proxy.
2
3use derive_deftly::Deftly;
4use serde::{Deserialize, Serialize};
5use std::{net::SocketAddr, ops::RangeInclusive, str::FromStr};
6use tor_config::ConfigBuildError;
7use tor_config::derive::prelude::*;
8use 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"))]
14pub 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
24impl ProxyConfigBuilder {
25    /// Run checks on this ProxyConfig to ensure that it's valid.
26    fn validate(&self) -> Result<(), ConfigBuildError> {
27        // Make sure that every proxy pattern is actually reachable.
28        let mut covered = rangemap::RangeInclusiveSet::<u16>::new();
29        for rule in self.proxy_ports.access_opt().iter().flatten() {
30            let range = &rule.source.0;
31            if covered.gaps(range).next().is_none() {
32                return Err(ConfigBuildError::Invalid {
33                    field: "proxy_ports".into(),
34                    problem: format!("Port pattern {} is not reachable", rule.source),
35                });
36            }
37            covered.insert(range.clone());
38        }
39
40        // Warn about proxy setups that are likely to be surprising.
41        let mut any_forward = false;
42        for rule in self.proxy_ports.access_opt().iter().flatten() {
43            if let ProxyAction::Forward(_, target) = &rule.target {
44                any_forward = true;
45                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                }
57            }
58        }
59
60        if !any_forward {
61            warn!("Onion service is not configured to accept any connections.");
62        }
63
64        Ok(())
65    }
66}
67
68impl 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")]
87pub 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.
95type ProxyRuleAsTuple = (ProxyPattern, ProxyAction);
96impl From<ProxyRuleAsTuple> for ProxyRule {
97    fn from(value: ProxyRuleAsTuple) -> Self {
98        Self {
99            source: value.0,
100            target: value.1,
101        }
102    }
103}
104impl From<ProxyRule> for ProxyRuleAsTuple {
105    fn from(value: ProxyRule) -> Self {
106        (value.source, value.target)
107    }
108}
109impl ProxyRule {
110    /// Create a new ProxyRule mapping `source` to `target`.
111    pub fn new(source: ProxyPattern, target: ProxyAction) -> Self {
112        Self { source, target }
113    }
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")]
119pub struct ProxyPattern(RangeInclusive<u16>);
120
121/// Representation for a [`ProxyPattern`]. Used while deserializing.
122#[derive(serde::Deserialize)]
123#[serde(untagged)]
124enum ProxyPatternAsEnum {
125    /// Representation the [`ProxyPattern`] as an integer.
126    Number(u16),
127    /// Representation of the [`ProxyPattern`] as a string.
128    String(String),
129}
130
131impl TryFrom<ProxyPatternAsEnum> for ProxyPattern {
132    type Error = ProxyConfigError;
133
134    fn try_from(value: ProxyPatternAsEnum) -> Result<Self, Self::Error> {
135        match value {
136            ProxyPatternAsEnum::Number(port) => Self::one_port(port),
137            ProxyPatternAsEnum::String(s) => Self::from_str(&s),
138        }
139    }
140}
141
142impl FromStr for ProxyPattern {
143    type Err = ProxyConfigError;
144
145    fn from_str(s: &str) -> Result<Self, Self::Err> {
146        use ProxyConfigError as PCE;
147        if s == "*" {
148            Ok(Self::all_ports())
149        } else if let Some((left, right)) = s.split_once('-') {
150            let left: u16 = left
151                .parse()
152                .map_err(|e| PCE::InvalidPort(left.to_string(), e))?;
153            let right: u16 = right
154                .parse()
155                .map_err(|e| PCE::InvalidPort(right.to_string(), e))?;
156            Self::port_range(left, right)
157        } else {
158            let port = s.parse().map_err(|e| PCE::InvalidPort(s.to_string(), e))?;
159            Self::one_port(port)
160        }
161    }
162}
163impl std::fmt::Display for ProxyPattern {
164    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
165        match self.0.clone().into_inner() {
166            (start, end) if start == end => write!(f, "{}", start),
167            (1, 65535) => write!(f, "*"),
168            (start, end) => write!(f, "{}-{}", start, end),
169        }
170    }
171}
172
173impl ProxyPattern {
174    /// Return a pattern matching all ports.
175    pub fn all_ports() -> Self {
176        Self::check(1, 65535).expect("Somehow, 1-65535 was not a valid pattern")
177    }
178    /// Return a pattern matching a single port.
179    ///
180    /// Gives an error if the port is zero.
181    pub fn one_port(port: u16) -> Result<Self, ProxyConfigError> {
182        Self::check(port, port)
183    }
184    /// Return a pattern matching all ports between `low` and `high` inclusive.
185    ///
186    /// Gives an error unless `0 < low <= high`.
187    pub fn port_range(low: u16, high: u16) -> Result<Self, ProxyConfigError> {
188        Self::check(low, high)
189    }
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    fn check(start: u16, end: u16) -> Result<ProxyPattern, ProxyConfigError> {
199        use ProxyConfigError as PCE;
200        match (start, end) {
201            (_, 0) => Err(PCE::ZeroPort),
202            (0, n) => Ok(Self(1..=n)),
203            (low, high) if low > high => Err(PCE::EmptyPortRange),
204            (low, high) => Ok(Self(low..=high)),
205        }
206    }
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]
227pub 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]
243pub 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
252impl 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    fn is_sufficiently_private(&self) -> bool {
257        use std::net::IpAddr;
258        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            TargetAddr::Inet(sa) => match sa.ip() {
268                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    }
273}
274
275impl FromStr for TargetAddr {
276    type Err = ProxyConfigError;
277
278    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        fn looks_like_attempted_addr(s: &str) -> bool {
283            s.starts_with(|c: char| c.is_ascii_digit())
284                || s.strip_prefix('[')
285                    .map(|rhs| rhs.starts_with(|c: char| c.is_ascii_hexdigit() || c == ':'))
286                    .unwrap_or(false)
287        }
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        if let Some(addr) = s.strip_prefix("inet:") {
294            Ok(Self::Inet(addr.parse().map_err(|e| {
295                PCE::InvalidTargetAddr(addr.to_string(), e)
296            })?))
297        } else if looks_like_attempted_addr(s) {
298            // We check 'looks_like_attempted_addr' before parsing this.
299            Ok(Self::Inet(
300                s.parse()
301                    .map_err(|e| PCE::InvalidTargetAddr(s.to_string(), e))?,
302            ))
303        } else {
304            Err(PCE::UnrecognizedTargetType(s.to_string()))
305        }
306    }
307}
308
309impl std::fmt::Display for TargetAddr {
310    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
311        match self {
312            TargetAddr::Inet(a) => write!(f, "inet:{}", a),
313            // TODO (#1246): Put this back.
314            // TargetAddr::Unix(p) => write!(f, "unix:{}", p.display()),
315        }
316    }
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]
325pub 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
335impl FromStr for ProxyAction {
336    type Err = ProxyConfigError;
337
338    fn from_str(s: &str) -> Result<Self, Self::Err> {
339        if s == "destroy" {
340            Ok(Self::DestroyCircuit)
341        } else if s == "reject" {
342            Ok(Self::RejectStream)
343        } else if s == "ignore" {
344            Ok(Self::IgnoreStream)
345        } else if let Some(addr) = s.strip_prefix("simple:") {
346            Ok(Self::Forward(Encapsulation::Simple, addr.parse()?))
347        } else {
348            Ok(Self::Forward(Encapsulation::Simple, s.parse()?))
349        }
350    }
351}
352
353impl std::fmt::Display for ProxyAction {
354    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
355        match self {
356            ProxyAction::DestroyCircuit => write!(f, "destroy"),
357            ProxyAction::Forward(Encapsulation::Simple, addr) => write!(f, "simple:{}", addr),
358            ProxyAction::RejectStream => write!(f, "reject"),
359            ProxyAction::IgnoreStream => write!(f, "ignore"),
360        }
361    }
362}
363
364/// An error encountered while parsing or applying a proxy configuration.
365#[derive(Debug, Clone, thiserror::Error)]
366#[non_exhaustive]
367pub 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)]
390mod 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    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
404    use super::*;
405
406    #[test]
407    fn pattern_ok() {
408        use ProxyPattern as P;
409        assert_eq!(P::from_str("*").unwrap(), P(1..=65535));
410        assert_eq!(P::from_str("100").unwrap(), P(100..=100));
411        assert_eq!(P::from_str("100-200").unwrap(), P(100..=200));
412        assert_eq!(P::from_str("0-200").unwrap(), P(1..=200));
413    }
414
415    #[test]
416    fn pattern_display() {
417        use ProxyPattern as P;
418        assert_eq!(P::all_ports().to_string(), "*");
419        assert_eq!(P::one_port(100).unwrap().to_string(), "100");
420        assert_eq!(P::port_range(100, 200).unwrap().to_string(), "100-200");
421    }
422
423    #[test]
424    fn pattern_err() {
425        use ProxyConfigError as PCE;
426        use ProxyPattern as P;
427        assert!(matches!(P::from_str("fred"), Err(PCE::InvalidPort(_, _))));
428        assert!(matches!(
429            P::from_str("100-fred"),
430            Err(PCE::InvalidPort(_, _))
431        ));
432        assert!(matches!(P::from_str("100-42"), Err(PCE::EmptyPortRange)));
433    }
434
435    #[test]
436    fn target_ok() {
437        use Encapsulation::Simple;
438        use ProxyAction as T;
439        use TargetAddr as A;
440        assert!(matches!(T::from_str("reject"), Ok(T::RejectStream)));
441        assert!(matches!(T::from_str("ignore"), Ok(T::IgnoreStream)));
442        assert!(matches!(T::from_str("destroy"), Ok(T::DestroyCircuit)));
443        let sa: SocketAddr = "192.168.1.1:50".parse().unwrap();
444        assert!(
445            matches!(T::from_str("192.168.1.1:50"), Ok(T::Forward(Simple, A::Inet(a))) if a == sa)
446        );
447        assert!(
448            matches!(T::from_str("inet:192.168.1.1:50"), Ok(T::Forward(Simple, A::Inet(a))) if a == sa)
449        );
450        let sa: SocketAddr = "[::1]:999".parse().unwrap();
451        assert!(matches!(T::from_str("[::1]:999"), Ok(T::Forward(Simple, A::Inet(a))) if a == sa));
452        assert!(
453            matches!(T::from_str("inet:[::1]:999"), Ok(T::Forward(Simple, A::Inet(a))) if a == sa)
454        );
455        /* TODO (#1246)
456        let pb = PathBuf::from("/var/run/hs/socket");
457        assert!(
458            matches!(T::from_str("unix:/var/run/hs/socket"), Ok(T::Forward(Simple, A::Unix(p))) if p == pb)
459        );
460        */
461    }
462
463    #[test]
464    fn target_display() {
465        use Encapsulation::Simple;
466        use ProxyAction as T;
467        use TargetAddr as A;
468
469        assert_eq!(T::RejectStream.to_string(), "reject");
470        assert_eq!(T::IgnoreStream.to_string(), "ignore");
471        assert_eq!(T::DestroyCircuit.to_string(), "destroy");
472        assert_eq!(
473            T::Forward(Simple, A::Inet("192.168.1.1:50".parse().unwrap())).to_string(),
474            "simple:inet:192.168.1.1:50"
475        );
476        assert_eq!(
477            T::Forward(Simple, A::Inet("[::1]:999".parse().unwrap())).to_string(),
478            "simple:inet:[::1]:999"
479        );
480        /* TODO (#1246)
481        assert_eq!(
482            T::Forward(Simple, A::Unix("/var/run/hs/socket".into())).to_string(),
483            "simple:unix:/var/run/hs/socket"
484        );
485        */
486    }
487
488    #[test]
489    fn target_err() {
490        use ProxyAction as T;
491        use ProxyConfigError as PCE;
492
493        assert!(matches!(
494            T::from_str("sdakljf"),
495            Err(PCE::UnrecognizedTargetType(_))
496        ));
497
498        assert!(matches!(
499            T::from_str("inet:hello"),
500            Err(PCE::InvalidTargetAddr(_, _))
501        ));
502        assert!(matches!(
503            T::from_str("inet:wwww.example.com:80"),
504            Err(PCE::InvalidTargetAddr(_, _))
505        ));
506
507        assert!(matches!(
508            T::from_str("127.1:80"),
509            Err(PCE::InvalidTargetAddr(_, _))
510        ));
511        assert!(matches!(
512            T::from_str("inet:127.1:80"),
513            Err(PCE::InvalidTargetAddr(_, _))
514        ));
515        assert!(matches!(
516            T::from_str("127.1:80"),
517            Err(PCE::InvalidTargetAddr(_, _))
518        ));
519        assert!(matches!(
520            T::from_str("inet:2130706433:80"),
521            Err(PCE::InvalidTargetAddr(_, _))
522        ));
523
524        assert!(matches!(
525            T::from_str("128.256.cats.and.dogs"),
526            Err(PCE::InvalidTargetAddr(_, _))
527        ));
528    }
529
530    #[test]
531    fn deserialize() {
532        use Encapsulation::Simple;
533        use TargetAddr as A;
534        let ex = r#"{
535            "proxy_ports": [
536                [ "443", "127.0.0.1:11443" ],
537                [ "80", "ignore" ],
538                [ "*", "destroy" ]
539            ]
540        }"#;
541        let bld: ProxyConfigBuilder = serde_json::from_str(ex).unwrap();
542        let cfg = bld.build().unwrap();
543        assert_eq!(cfg.proxy_ports.len(), 3);
544        assert_eq!(cfg.proxy_ports[0].source.0, 443..=443);
545        assert_eq!(cfg.proxy_ports[1].source.0, 80..=80);
546        assert_eq!(cfg.proxy_ports[2].source.0, 1..=65535);
547
548        assert_eq!(
549            cfg.proxy_ports[0].target,
550            ProxyAction::Forward(Simple, A::Inet("127.0.0.1:11443".parse().unwrap()))
551        );
552        assert_eq!(cfg.proxy_ports[1].target, ProxyAction::IgnoreStream);
553        assert_eq!(cfg.proxy_ports[2].target, ProxyAction::DestroyCircuit);
554    }
555
556    #[test]
557    fn validation_fail() {
558        // this should fail; the third pattern isn't reachable.
559        let ex = r#"{
560            "proxy_ports": [
561                [ "2-300", "127.0.0.1:11443" ],
562                [ "301-999", "ignore" ],
563                [ "30-310", "destroy" ]
564            ]
565        }"#;
566        let bld: ProxyConfigBuilder = serde_json::from_str(ex).unwrap();
567        match bld.build() {
568            Err(ConfigBuildError::Invalid { field, problem }) => {
569                assert_eq!(field, "proxy_ports");
570                assert_eq!(problem, "Port pattern 30-310 is not reachable");
571            }
572            other => panic!("Expected an Invalid error; got {other:?}"),
573        }
574
575        // This should work; the third pattern is not completely covered.
576        let ex = r#"{
577            "proxy_ports": [
578                [ "2-300", "127.0.0.1:11443" ],
579                [ "302-999", "ignore" ],
580                [ "30-310", "destroy" ]
581            ]
582        }"#;
583        let bld: ProxyConfigBuilder = serde_json::from_str(ex).unwrap();
584        assert!(bld.build().is_ok());
585    }
586
587    #[test]
588    fn demo() {
589        let b: ProxyConfigBuilder = toml::de::from_str(
590            r#"
591proxy_ports = [
592    [ 80, "127.0.0.1:10080"],
593    ["22", "destroy"],
594    ["265", "ignore"],
595    # ["1-1024", "unix:/var/run/allium-cepa/socket"], # TODO (#1246))
596]
597"#,
598        )
599        .unwrap();
600        let c = b.build().unwrap();
601        assert_eq!(c.proxy_ports.len(), 3);
602        assert_eq!(
603            c.proxy_ports[0],
604            ProxyRule::new(
605                ProxyPattern::one_port(80).unwrap(),
606                ProxyAction::Forward(
607                    Encapsulation::Simple,
608                    TargetAddr::Inet("127.0.0.1:10080".parse().unwrap())
609                )
610            )
611        );
612        assert_eq!(
613            c.proxy_ports[1],
614            ProxyRule::new(
615                ProxyPattern::one_port(22).unwrap(),
616                ProxyAction::DestroyCircuit
617            )
618        );
619        assert_eq!(
620            c.proxy_ports[2],
621            ProxyRule::new(
622                ProxyPattern::one_port(265).unwrap(),
623                ProxyAction::IgnoreStream
624            )
625        );
626        /* TODO (#1246)
627        assert_eq!(
628            c.proxy_ports[3],
629            ProxyRule::new(
630                ProxyPattern::port_range(1, 1024).unwrap(),
631                ProxyAction::Forward(
632                    Encapsulation::Simple,
633                    TargetAddr::Unix("/var/run/allium-cepa/socket".into())
634                )
635            )
636        );
637        */
638    }
639}