use arc_swap::ArcSwap;
use clap::Parser;
use ferryman_edge::{reload, serve, AppState, UpstreamClient};
use ferryman_edge_core::{
build_limiter, build_table, health_loop, spawn_gc, ConfigToml, JwtVerifier, Limiter,
ReloadingTls, SharedTable,
};
use hyper_util::client::legacy::connect::HttpConnector;
use hyper_util::client::legacy::Client;
use hyper_util::rt::TokioExecutor;
use metrics_exporter_prometheus::PrometheusBuilder;
use std::net::SocketAddr;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use tracing_subscriber::EnvFilter;
const LIMITER_GC_INTERVAL: Duration = Duration::from_secs(60);
#[derive(Parser, Debug)]
#[command(name = "ferryman-edge-server", about = "programmable mTLS L7 proxy")]
struct Args {
#[arg(long, env = "FERRYMAN_EDGE_CONFIG", default_value = "config.toml")]
config: PathBuf,
#[arg(long, env = "FERRYMAN_EDGE_BIND", default_value = "0.0.0.0:8443")]
bind: SocketAddr,
#[arg(
long,
env = "FERRYMAN_EDGE_METRICS_BIND",
default_value = "0.0.0.0:9090"
)]
metrics_bind: SocketAddr,
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
rustls::crypto::aws_lc_rs::default_provider()
.install_default()
.map_err(|_| anyhow::anyhow!("failed to install aws-lc-rs crypto provider"))?;
tracing_subscriber::fmt()
.with_env_filter(
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")),
)
.json()
.init();
let args = Args::parse();
let raw = std::fs::read_to_string(&args.config)?;
let cfg: ConfigToml = toml::from_str(&raw)?;
let interval = Duration::from_secs(cfg.health_interval_secs);
let table = build_table(&cfg)?;
let shared: SharedTable = Arc::new(ArcSwap::from_pointee(table));
let tls = ReloadingTls::new(
&cfg.tls.cert_path,
&cfg.tls.key_path,
&cfg.tls.client_ca_path,
)?;
let jwks_pem = std::fs::read(&cfg.jwt.jwks_path)?;
let mut verifier = JwtVerifier::new(&jwks_pem)?;
if let Some(iss) = &cfg.jwt.issuer {
verifier = verifier.with_issuer(iss);
}
if let Some(aud) = &cfg.jwt.audience {
verifier = verifier.with_audience(aud);
}
let jwt: Arc<JwtVerifier> = Arc::new(verifier);
let limiter: Option<Arc<Limiter>> = build_limiter(cfg.tenant_rps);
if let Some(l) = &limiter {
spawn_gc(l.clone(), LIMITER_GC_INTERVAL);
}
PrometheusBuilder::new()
.with_http_listener(args.metrics_bind)
.install()?;
tracing::info!(addr = %args.metrics_bind, "metrics listener bound");
tokio::spawn(health_loop(shared.clone(), interval));
reload::spawn_reload(args.config.clone(), shared.clone());
let client: UpstreamClient = Client::builder(TokioExecutor::new()).build(HttpConnector::new());
let state = Arc::new(AppState {
tls,
table: shared,
jwt,
limiter,
client,
});
let listener = tokio::net::TcpListener::bind(args.bind).await?;
tracing::info!(addr = %args.bind, "ferryman-edge-server listening (mTLS)");
serve(listener, state, shutdown_signal()).await;
Ok(())
}
async fn shutdown_signal() {
let mut sigterm = match tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
{
Ok(s) => s,
Err(e) => {
tracing::error!(?e, "failed to install SIGTERM handler");
return;
}
};
tokio::select! {
_ = sigterm.recv() => tracing::info!("SIGTERM received"),
_ = tokio::signal::ctrl_c() => tracing::info!("SIGINT received"),
}
}