Skip to main content

openmoat_proxy/
upstream.rs

1//! Resolving and connecting to the destination.
2//!
3//! The proxy resolves names itself and checks every address it gets back
4//! before connecting to one of exactly those addresses, so the check and the
5//! connection cannot disagree (no second lookup an attacker could race).
6
7use std::io;
8use std::net::{IpAddr, SocketAddr, TcpStream, ToSocketAddrs as _};
9use std::time::Duration;
10
11use openmoat_core::{CompiledPolicy, Decision, Verdict};
12
13use crate::decide::{self, Forbidden, RULE_ADDRESS, RULE_UPSTREAM};
14
15/// Name resolution, injectable so tests can map names without a DNS server.
16pub trait Resolve: Send + Sync {
17    /// Every address the DNS name `host` resolves to, with `port` attached.
18    /// IP literals never reach the resolver.
19    fn resolve(&self, host: &str, port: u16) -> io::Result<Vec<SocketAddr>>;
20}
21
22/// The operating system's resolver (`getaddrinfo` and its equivalents).
23#[derive(Debug, Clone, Copy, Default)]
24pub struct SystemResolver;
25
26impl Resolve for SystemResolver {
27    fn resolve(&self, host: &str, port: u16) -> io::Result<Vec<SocketAddr>> {
28        Ok((host, port).to_socket_addrs()?.collect())
29    }
30}
31
32/// Why no connection was made.
33#[derive(Debug)]
34#[non_exhaustive]
35pub enum Refusal {
36    /// A resolved address the proxy never connects to.
37    Address(SocketAddr, Forbidden),
38    /// A resolved address a policy deny rule matches, with that rule's decision.
39    Denied(SocketAddr, Decision),
40    /// The name did not resolve, or no address accepted the connection.
41    Unreachable(String),
42}
43
44impl Refusal {
45    /// The decision recorded for this refusal.
46    #[must_use]
47    pub fn decision(&self, host: &str) -> Decision {
48        match self {
49            Self::Address(addr, why) => Decision::single(
50                Verdict::Deny,
51                RULE_ADDRESS,
52                format!("{host} resolves to {}, {}", addr.ip(), why.describe()),
53            ),
54            Self::Denied(addr, denied) => {
55                let rules = denied
56                    .rules
57                    .iter()
58                    .zip(&denied.reasons)
59                    .map(|(rule, reason)| format!("`{rule}` ({reason})"))
60                    .collect::<Vec<_>>()
61                    .join(", ");
62                Decision::single(
63                    Verdict::Deny,
64                    RULE_ADDRESS,
65                    format!(
66                        "{host} resolves to {}, refused by deny rule {rules}",
67                        addr.ip()
68                    ),
69                )
70            }
71            Self::Unreachable(why) => Decision::single(Verdict::Deny, RULE_UPSTREAM, why.clone()),
72        }
73    }
74}
75
76/// Resolve `host`, refuse if any address is forbidden or matched by a deny
77/// rule of `addresses` (compiled from [`decide::address_policy`]), then
78/// connect to the first address that answers. A private address passes only
79/// when `addresses` names it.
80/// `loopback_ok` lists loopback addresses that may be reached anyway (tests);
81/// nothing exempts link-local or metadata.
82pub fn connect(
83    resolver: &dyn Resolve,
84    host: &str,
85    port: u16,
86    addresses: &CompiledPolicy<'_>,
87    loopback_ok: &[SocketAddr],
88    timeout: Duration,
89) -> Result<TcpStream, Refusal> {
90    let addrs = match host.parse::<IpAddr>() {
91        Ok(ip) => vec![SocketAddr::new(ip, port)],
92        Err(_) => resolver
93            .resolve(host, port)
94            .map_err(|e| Refusal::Unreachable(format!("cannot resolve {host}: {e}")))?,
95    };
96    if addrs.is_empty() {
97        return Err(Refusal::Unreachable(format!("{host} has no addresses")));
98    }
99    // One bad address refuses the name: an allowed name that also points at
100    // a metadata service is an attack, not a fallback.
101    for addr in &addrs {
102        match decide::forbidden(addr.ip()) {
103            Some(Forbidden::Loopback) if loopback_ok.contains(addr) => {}
104            Some(Forbidden::Private(_)) if decide::named(addresses, addr.ip()) => {}
105            Some(why) => return Err(Refusal::Address(*addr, why)),
106            None => {}
107        }
108        // The host decision matched only the name; a deny naming one of its
109        // addresses still holds.
110        if let Some(denied) = decide::denied(addresses, addr.ip()) {
111            return Err(Refusal::Denied(*addr, denied));
112        }
113    }
114    let mut last = None;
115    for addr in &addrs {
116        match TcpStream::connect_timeout(addr, timeout) {
117            Ok(stream) => return Ok(stream),
118            Err(e) => last = Some(e),
119        }
120    }
121    Err(Refusal::Unreachable(match last {
122        Some(e) => format!("cannot connect to {host}:{port}: {e}"),
123        None => format!("cannot connect to {host}:{port}"),
124    }))
125}