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    /// The name did not resolve, or no address accepted the connection.
39    Unreachable(String),
40}
41
42impl Refusal {
43    /// The decision recorded for this refusal.
44    #[must_use]
45    pub fn decision(&self, host: &str) -> Decision {
46        match self {
47            Self::Address(addr, why) => Decision::single(
48                Verdict::Deny,
49                RULE_ADDRESS,
50                format!("{host} resolves to {}, {}", addr.ip(), why.describe()),
51            ),
52            Self::Unreachable(why) => Decision::single(Verdict::Deny, RULE_UPSTREAM, why.clone()),
53        }
54    }
55}
56
57/// Resolve `host`, refuse if any address is forbidden, then connect to the
58/// first address that answers. A private address passes only when
59/// `addresses` (compiled from [`decide::address_policy`]) names it.
60/// `loopback_ok` lists loopback addresses that may be reached anyway (tests);
61/// nothing exempts link-local or metadata.
62pub fn connect(
63    resolver: &dyn Resolve,
64    host: &str,
65    port: u16,
66    addresses: &CompiledPolicy<'_>,
67    loopback_ok: &[SocketAddr],
68    timeout: Duration,
69) -> Result<TcpStream, Refusal> {
70    let addrs = match host.parse::<IpAddr>() {
71        Ok(ip) => vec![SocketAddr::new(ip, port)],
72        Err(_) => resolver
73            .resolve(host, port)
74            .map_err(|e| Refusal::Unreachable(format!("cannot resolve {host}: {e}")))?,
75    };
76    if addrs.is_empty() {
77        return Err(Refusal::Unreachable(format!("{host} has no addresses")));
78    }
79    // One bad address refuses the name: an allowed name that also points at
80    // a metadata service is an attack, not a fallback.
81    for addr in &addrs {
82        match decide::forbidden(addr.ip()) {
83            Some(Forbidden::Loopback) if loopback_ok.contains(addr) => {}
84            Some(Forbidden::Private(_)) if decide::named(addresses, addr.ip()) => {}
85            Some(why) => return Err(Refusal::Address(*addr, why)),
86            None => {}
87        }
88    }
89    let mut last = None;
90    for addr in &addrs {
91        match TcpStream::connect_timeout(addr, timeout) {
92            Ok(stream) => return Ok(stream),
93            Err(e) => last = Some(e),
94        }
95    }
96    Err(Refusal::Unreachable(match last {
97        Some(e) => format!("cannot connect to {host}:{port}: {e}"),
98        None => format!("cannot connect to {host}:{port}"),
99    }))
100}