a (hacky, wip) multi-tenant oidc-terminating reverse proxy, written in anger on top of pingora
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}