a (hacky, wip) multi-tenant oidc-terminating reverse proxy, written in anger on top of pingora
at main 167 lines 6.2 kB view raw
1use std::collections::HashMap; 2 3use color_eyre::eyre::Context as _; 4use pingora::lb::{self, Backend}; 5use pingora::listeners::tls::{BundleCert, CertAndKey, TlsSettings}; 6use pingora::prelude::*; 7use pingora::utils::tls::CertKey; 8 9use self::backend_selection::SelectionChoice; 10use self::gateway::{AuthGateway, BackendData, DomainInfo, oidc}; 11 12mod backend_selection; 13mod config; 14mod cookies; 15mod gateway; 16mod httputil; 17mod oauth; 18 19/// constructed load balancer, with [backend info][`BackendInfo`] to be passed to [`AuthGateway`] 20type BalancerInfo = ( 21 Vec<pingora::services::background::GenBackgroundService<LoadBalancer<SelectionChoice>>>, 22 HashMap<String, DomainInfo>, 23); 24 25/// construct the load balancer and initialize the [backend info][`BackendInfo`] for the 26/// [`AuthGateway`] 27fn balancer(domains: &HashMap<String, config::format::Domain>) -> color_eyre::Result<BalancerInfo> { 28 use lb::{self, discovery}; 29 use pingora::protocols::l4::socket::SocketAddr; 30 31 let mut balancers = HashMap::with_capacity(domains.len()); 32 let mut svcs = Vec::with_capacity(domains.len()); 33 for (name, domain) in domains { 34 let backends = domain 35 .https 36 .iter() 37 .map(|backend| { 38 let mut ext = lb::Extensions::new(); 39 let ca = backend.ca_path.as_ref().map(|path| { 40 // yikes pingora does not make this easy to construct 41 CertKey::new( 42 vec![std::fs::read(path).expect("unable to read ca path")], 43 vec![], 44 ) 45 .into_certs() 46 }); 47 ext.insert(BackendData::Tls { 48 skip_verifying_cert: backend.skip_verifying_certs, 49 ca, 50 }); 51 let mut backend = Backend::new_with_weight( 52 &backend.addr, 53 backend.weight.map(|w| w as usize).unwrap_or(1), 54 ) 55 .context("parsing addr of https socket backend")?; 56 backend.ext = ext; 57 Ok(backend) 58 }) 59 .chain(domain.http.iter().map(|backend| { 60 let mut ext = lb::Extensions::new(); 61 ext.insert(BackendData::HttpOnly); 62 let mut backend = Backend::new_with_weight( 63 &backend.addr, 64 backend.weight.map(|w| w as usize).unwrap_or(1), 65 ) 66 .context("parsing addr of http socket backend")?; 67 backend.ext = ext; 68 Ok(backend) 69 })) 70 .chain(domain.uds.iter().map(|backend| { 71 let mut ext = lb::Extensions::new(); 72 ext.insert(BackendData::HttpOverUds); 73 Ok(Backend { 74 addr: SocketAddr::Unix( 75 std::os::unix::net::SocketAddr::from_pathname(&backend.path) 76 .context("turning uds path into socketaddr")?, 77 ), 78 weight: backend.weight.map(|w| w as usize).unwrap_or(1), 79 ext, 80 }) 81 })) 82 .collect::<color_eyre::Result<_>>() 83 .context("constucting backends for domain")?; 84 let backends = lb::Backends::new(discovery::Static::new(backends)); 85 let balancer = LoadBalancer::from_backends(backends); 86 let svc = background_service("health checking", balancer); 87 88 let info = DomainInfo { 89 balancer: svc.task(), 90 tls_mode: config::format::domain::TlsMode::try_from(domain.tls_mode) 91 .context("invalid tls mode")?, 92 sni_name: domain 93 .tls 94 .as_ref() 95 .and_then(|d| d.sni.as_ref()) 96 .unwrap_or(name) 97 .clone(), 98 oidc: domain 99 .oidc_auth 100 .clone() 101 .map(|config| oidc::Info::from_config(config, name.clone())) 102 .transpose()?, 103 headers: domain.manage_headers.clone().unwrap_or_default(), 104 }; 105 106 balancers.insert(name.clone(), info); 107 svcs.push(svc) 108 } 109 110 Ok((svcs, balancers)) 111} 112 113fn main() -> color_eyre::Result<()> { 114 use color_eyre::eyre::eyre; 115 116 tracing_subscriber::fmt().init(); 117 color_eyre::install()?; 118 rustls::crypto::aws_lc_rs::default_provider() 119 .install_default() 120 .expect("unable to install crypto provider"); 121 122 let opts = Opt::parse_args(); 123 124 let config = config::load(opts.conf.as_ref().ok_or_else(|| { 125 eyre!("no config file specified, refusing to do anything (try `-c FILE`?)") 126 })?)?; 127 128 let pingora_config = match config.pingora.as_ref().map(Into::into) { 129 Some(conf) => conf, 130 None => pingora::server::configuration::ServerConf::new_with_opt_override(&opts) 131 .ok_or_else(|| { 132 eyre!("could not create a base pingora config, and none was specified") 133 })?, 134 }; 135 let mut server = Server::new_with_opt_and_conf(opts, pingora_config); 136 server.bootstrap(); 137 138 let (balancer_svcs, balancers) = 139 balancer(&config.domains).context("setting up load balancing")?; 140 let mut gateway = http_proxy_service(&server.configuration, AuthGateway { domains: balancers }); 141 for binding in config.bind_to_tcp { 142 let certs = config 143 .domains 144 .iter() 145 .filter_map(|(name, domain)| { 146 domain.tls.as_ref().map(|tls| BundleCert { 147 sni: tls.sni.as_ref().unwrap_or(name).clone(), 148 cert_path: tls.cert_path.clone(), 149 key_path: tls.key_path.clone(), 150 }) 151 }) 152 .collect(); 153 gateway.endpoints().add_tls_with_settings( 154 &binding.addr, 155 None, 156 TlsSettings::intermediate_custom(CertAndKey::Bundle(certs)) 157 .context("setting up tls")?, 158 ); 159 } 160 161 balancer_svcs 162 .into_iter() 163 .for_each(|svc| server.add_service(svc)); 164 server.add_service(gateway); 165 166 server.run_forever(); 167}