use std::io;
use std::net::{Ipv4Addr, SocketAddrV4};
use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
use anyhow::{Context, Result, ensure};
use arcbox_constants::paths::guest;
use arcbox_local_ca::LeafMinter;
use tokio::net::{TcpListener, TcpStream};
use tokio_rustls::TlsAcceptor;
use tokio_util::sync::CancellationToken;
use super::HttpPorts;
use super::http_port::HTTPS_PORT;
pub(super) const PROXY_PORT: u16 = 61443;
const TIMEOUT: Duration = Duration::from_secs(10);
const ACCEPT_BACKOFF: Duration = Duration::from_millis(100);
pub(super) struct Https {
listener: TcpListener,
acceptor: TlsAcceptor,
}
impl Https {
pub(super) async fn bind() -> Result<Self> {
let dir = Path::new(guest::MOUNT).join(guest::TLS);
let minter = LeafMinter::load(&dir)
.with_context(|| format!("loading the local CA from {}", dir.display()))?;
let mut config = minter.into_server_config()?;
config.alpn_protocols = vec![b"http/1.1".to_vec()];
let listener = TcpListener::bind((Ipv4Addr::UNSPECIFIED, PROXY_PORT))
.await
.with_context(|| format!("binding port {PROXY_PORT}"))?;
Ok(Self {
listener,
acceptor: TlsAcceptor::from(Arc::new(config)),
})
}
pub(super) async fn serve(self, ports: HttpPorts, cancel: CancellationToken) {
loop {
let (client, peer) = tokio::select! {
() = cancel.cancelled() => return,
accepted = self.listener.accept() => match accepted {
Ok(accepted) => accepted,
Err(e) => {
tracing::warn!(error = %e, "container domain HTTPS accept failed");
tokio::time::sleep(ACCEPT_BACKOFF).await;
continue;
}
},
};
let acceptor = self.acceptor.clone();
let ports = ports.clone();
tokio::spawn(async move {
if let Err(e) = relay(client, &acceptor, &ports).await {
tracing::debug!(%peer, error = %format!("{e:#}"), "container domain HTTPS connection ended");
}
});
}
}
}
async fn relay(client: TcpStream, acceptor: &TlsAcceptor, ports: &HttpPorts) -> Result<()> {
let dialled = original_destination(&client).context("reading the dialled address")?;
ensure!(
dialled.port() == HTTPS_PORT,
"{dialled} is no container's HTTPS port"
);
let container = *dialled.ip();
let http_port = ports
.get(container)
.with_context(|| format!("{container} serves no HTTP port"))?;
let mut tls = tokio::time::timeout(TIMEOUT, acceptor.accept(client))
.await
.context("TLS handshake timed out")?
.context("TLS handshake")?;
let mut upstream = tokio::time::timeout(TIMEOUT, TcpStream::connect((container, http_port)))
.await
.context("connecting to the container timed out")?
.with_context(|| format!("connecting to {container}:{http_port}"))?;
tokio::io::copy_bidirectional(&mut tls, &mut upstream).await?;
Ok(())
}
#[cfg(target_os = "linux")]
fn original_destination(stream: &TcpStream) -> io::Result<SocketAddrV4> {
use nix::sys::socket::{getsockopt, sockopt::OriginalDst};
let addr = getsockopt(stream, OriginalDst)?;
Ok(SocketAddrV4::new(
Ipv4Addr::from(u32::from_be(addr.sin_addr.s_addr)),
u16::from_be(addr.sin_port),
))
}
#[cfg(not(target_os = "linux"))]
fn original_destination(_: &TcpStream) -> io::Result<SocketAddrV4> {
Err(io::ErrorKind::Unsupported.into())
}