#![cfg(target_os = "macos")]
use std::io;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, TcpListener, TcpStream, ToSocketAddrs};
use std::time::Duration;
use anyhow::{bail, Context, Result};
use crate::config::NetMode;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PinnedConnection {
host: String,
port: u16,
addr: SocketAddr,
}
impl PinnedConnection {
pub fn connect(&self, timeout: Option<Duration>) -> io::Result<TcpStream> {
if self.port == 0 || self.addr.port() != self.port {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"pinned connection port does not match its SocketAddr",
));
}
if normalize_host(&self.host).is_err() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"invalid pinned host",
));
}
validate_public_addr(self.addr.ip())
.map_err(|error| io::Error::new(io::ErrorKind::PermissionDenied, error.to_string()))?;
let stream = match timeout {
Some(timeout) => TcpStream::connect_timeout(&self.addr, timeout)?,
None => TcpStream::connect(self.addr)?,
};
stream.set_nodelay(true)?;
Ok(stream)
}
pub fn host(&self) -> &str {
&self.host
}
pub const fn port(&self) -> u16 {
self.port
}
pub const fn addr(&self) -> SocketAddr {
self.addr
}
}
#[derive(Clone, Debug)]
pub struct BrokerPolicy {
mode: NetMode,
bind: SocketAddr,
}
impl BrokerPolicy {
pub fn new(mode: NetMode, bind: SocketAddr) -> Result<Self> {
if !is_loopback(bind.ip()) {
bail!("local broker must bind to loopback, not {}", bind.ip());
}
if matches!(&mode, &NetMode::Off) {
bail!("a broker policy cannot be constructed for --net=off");
}
Ok(Self { mode, bind })
}
pub fn authorize(&self, host: &str, port: u16, addr: SocketAddr) -> Result<PinnedConnection> {
let host = normalize_host(host)?;
if port == 0 || addr.port() != port {
bail!("pinned endpoint port does not match the requested port");
}
validate_public_addr(addr.ip())?;
match &self.mode {
NetMode::Off => bail!("network policy is off"),
NetMode::Allowlist(domains) => {
if !domains.iter().any(|domain| host_matches(&host, domain)) {
bail!("host {host:?} is not in the broker allowlist");
}
}
NetMode::Strict(rules) => {
if !rules
.iter()
.any(|rule| rule.port == port && host_matches(&host, &rule.domain))
{
bail!("host {host:?}:{port} is not in the strict broker policy");
}
}
NetMode::Ask => {}
}
Ok(PinnedConnection { host, port, addr })
}
pub fn resolve(&self, host: &str, port: u16) -> Result<Vec<PinnedConnection>> {
let normalized = normalize_host(host)?;
if port == 0 {
bail!("port 0 is not a connectable broker target");
}
let mut addresses = (normalized.as_str(), port)
.to_socket_addrs()
.with_context(|| format!("resolve {normalized}:{port} in broker"))?
.collect::<Vec<_>>();
addresses.sort();
addresses.dedup();
if addresses.is_empty() {
bail!("resolver returned no addresses for {normalized}:{port}");
}
if addresses.len() > 1024 {
bail!("resolver returned too many addresses for {normalized}:{port}");
}
let mut pinned = Vec::with_capacity(addresses.len());
for addr in addresses {
pinned.push(self.authorize(&normalized, port, addr)?);
}
Ok(pinned)
}
pub fn mode(&self) -> &NetMode {
&self.mode
}
pub const fn bind_addr(&self) -> SocketAddr {
self.bind
}
}
pub struct LocalBroker {
listener: TcpListener,
policy: BrokerPolicy,
}
impl std::fmt::Debug for LocalBroker {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("LocalBroker")
.field("local_addr", &self.listener.local_addr().ok())
.field("policy", &self.policy)
.field("tls", &"passthrough; no MITM")
.finish()
}
}
impl LocalBroker {
pub fn bind(policy: BrokerPolicy) -> Result<Self> {
let listener = TcpListener::bind(policy.bind)
.with_context(|| format!("bind local broker at {}", policy.bind))?;
let bound = listener.local_addr()?;
if !is_loopback(bound.ip()) {
bail!("OS returned a non-loopback broker address {bound}");
}
Ok(Self { listener, policy })
}
pub fn local_addr(&self) -> io::Result<SocketAddr> {
self.listener.local_addr()
}
pub fn accept(&self) -> io::Result<(TcpStream, SocketAddr)> {
self.listener.accept()
}
pub fn policy(&self) -> &BrokerPolicy {
&self.policy
}
pub const fn tls_mode() -> &'static str {
"TLS pass-through only; no certificate interception or MITM"
}
}
pub fn validate_public_addr(addr: IpAddr) -> Result<()> {
if is_restricted_addr(addr) {
bail!("refusing private, loopback, metadata, documentation, or NAT64 address {addr}");
}
Ok(())
}
fn normalize_host(host: &str) -> Result<String> {
if host.is_empty() || host.contains('\0') || host.chars().any(char::is_whitespace) {
bail!("host is empty, contains NUL, or contains whitespace");
}
let host = host.trim_end_matches('.').to_ascii_lowercase();
if host.is_empty() || host.contains('@') || host.contains('/') || host.contains('\\') {
bail!("host contains unsupported URL/userinfo syntax");
}
if host.len() > 253 || host.parse::<IpAddr>().is_ok() || !host.is_ascii() {
bail!("host must be an ASCII DNS name, not an IP literal");
}
for label in host.split('.') {
if label.is_empty()
|| label.len() > 63
|| label.starts_with('-')
|| label.ends_with('-')
|| !label
.bytes()
.all(|character| character.is_ascii_alphanumeric() || character == b'-')
{
bail!("host contains an invalid DNS label");
}
}
Ok(host)
}
fn host_matches(host: &str, configured: &str) -> bool {
let Ok(configured) = normalize_host(configured) else {
return false;
};
host == configured || host.ends_with(&format!(".{configured}"))
}
fn is_loopback(addr: IpAddr) -> bool {
match addr {
IpAddr::V4(value) => value.is_loopback(),
IpAddr::V6(value) => value.is_loopback(),
}
}
fn is_restricted_addr(addr: IpAddr) -> bool {
match addr {
IpAddr::V4(value) => is_restricted_v4(value),
IpAddr::V6(value) => is_restricted_v6(value),
}
}
fn is_restricted_v4(value: Ipv4Addr) -> bool {
let octets = value.octets();
let first = octets[0];
let second = octets[1];
let third = octets[2];
let fourth = octets[3];
value.is_unspecified()
|| value.is_loopback()
|| value.is_private()
|| value.is_link_local()
|| value.is_multicast()
|| value.is_broadcast()
|| (first == 0)
|| (first == 100 && (64..=127).contains(&second)) || (first == 169 && second == 254) || (first == 192 && second == 0 && third == 0) || (first == 192 && second == 0 && third == 2) || (first == 198 && second == 18) || (first == 198 && second == 19)
|| (first == 198 && second == 51 && third == 100) || (first == 203 && second == 0 && third == 113) || (first == 192 && second == 88 && third == 99) || (first == 169 && second == 254 && third == 169 && fourth == 254)
}
fn is_restricted_v6(value: Ipv6Addr) -> bool {
let segments = value.segments();
let mapped_v4 = if segments[0] == 0
&& segments[1] == 0
&& segments[2] == 0
&& segments[3] == 0
&& segments[4] == 0
&& segments[5] == 0xffff
{
Some(Ipv4Addr::new(
(segments[6] >> 8) as u8,
segments[6] as u8,
(segments[7] >> 8) as u8,
segments[7] as u8,
))
} else if segments[0] == 0
&& segments[1] == 0
&& segments[2] == 0
&& segments[3] == 0
&& segments[4] == 0
&& segments[5] == 0
{
Some(Ipv4Addr::new(
(segments[6] >> 8) as u8,
segments[6] as u8,
(segments[7] >> 8) as u8,
segments[7] as u8,
))
} else {
None
};
value.is_unspecified()
|| value.is_loopback()
|| value.is_multicast()
|| (segments[0] & 0xfe00) == 0xfc00 || (segments[0] & 0xffc0) == 0xfe80 || segments[0] == 0x2001 && segments[1] == 0x0db8 || (segments[0] == 0x0064 && segments[1] == 0xff9b) || (segments[0] == 0x0064 && segments[1] == 0xff9b && segments[2] == 1)
|| mapped_v4.is_some_and(is_restricted_v4)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn private_metadata_and_nat64_are_rejected() {
for address in [
IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)),
IpAddr::V4(Ipv4Addr::new(169, 254, 169, 254)),
IpAddr::V6("fd00::1".parse().unwrap()),
IpAddr::V6("64:ff9b::c000:0201".parse().unwrap()),
] {
assert!(validate_public_addr(address).is_err(), "{address}");
}
}
#[test]
fn policy_pins_the_supplied_socket_address() {
let policy = BrokerPolicy::new(
NetMode::Strict(vec![crate::config::NetRule {
domain: "example.com".into(),
port: 443,
}]),
"127.0.0.1:0".parse().unwrap(),
)
.unwrap();
let pinned = policy
.authorize("example.com", 443, "93.184.216.34:443".parse().unwrap())
.unwrap();
assert_eq!(pinned.addr(), "93.184.216.34:443".parse().unwrap());
assert_eq!(
LocalBroker::tls_mode(),
"TLS pass-through only; no certificate interception or MITM"
);
}
}