use std::io;
use std::net::{IpAddr, SocketAddr, TcpStream, ToSocketAddrs as _};
use std::time::Duration;
use openmoat_core::{CompiledPolicy, Decision, Verdict};
use crate::decide::{self, Forbidden, RULE_ADDRESS, RULE_UPSTREAM};
pub trait Resolve: Send + Sync {
fn resolve(&self, host: &str, port: u16) -> io::Result<Vec<SocketAddr>>;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct SystemResolver;
impl Resolve for SystemResolver {
fn resolve(&self, host: &str, port: u16) -> io::Result<Vec<SocketAddr>> {
Ok((host, port).to_socket_addrs()?.collect())
}
}
#[derive(Debug)]
pub enum Refusal {
Address(SocketAddr, Forbidden),
Unreachable(String),
}
impl Refusal {
#[must_use]
pub fn decision(&self, host: &str) -> Decision {
match self {
Self::Address(addr, why) => Decision::single(
Verdict::Deny,
RULE_ADDRESS,
format!("{host} resolves to {}, {}", addr.ip(), why.describe()),
),
Self::Unreachable(why) => Decision::single(Verdict::Deny, RULE_UPSTREAM, why.clone()),
}
}
}
pub fn connect(
resolver: &dyn Resolve,
host: &str,
port: u16,
addresses: &CompiledPolicy<'_>,
loopback_ok: &[SocketAddr],
timeout: Duration,
) -> Result<TcpStream, Refusal> {
let addrs = match host.parse::<IpAddr>() {
Ok(ip) => vec![SocketAddr::new(ip, port)],
Err(_) => resolver
.resolve(host, port)
.map_err(|e| Refusal::Unreachable(format!("cannot resolve {host}: {e}")))?,
};
if addrs.is_empty() {
return Err(Refusal::Unreachable(format!("{host} has no addresses")));
}
for addr in &addrs {
match decide::forbidden(addr.ip()) {
Some(Forbidden::Loopback) if loopback_ok.contains(addr) => {}
Some(Forbidden::Private(_)) if decide::named(addresses, addr.ip()) => {}
Some(why) => return Err(Refusal::Address(*addr, why)),
None => {}
}
}
let mut last = None;
for addr in &addrs {
match TcpStream::connect_timeout(addr, timeout) {
Ok(stream) => return Ok(stream),
Err(e) => last = Some(e),
}
}
Err(Refusal::Unreachable(match last {
Some(e) => format!("cannot connect to {host}:{port}: {e}"),
None => format!("cannot connect to {host}:{port}"),
}))
}