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