structured_proxy/service.rs
1//! The proxy as one tower service: native gRPC to the upstream, everything
2//! else to the proxy's routes, and what no route answers to a fallback.
3
4use std::convert::Infallible;
5use std::future::Future;
6use std::net::SocketAddr;
7use std::pin::Pin;
8use std::sync::Arc;
9use std::task::{Context, Poll};
10
11use crate::cors::{Cors, CorsFuture, CorsLayer};
12use axum::extract::connect_info::ConnectInfo;
13use axum::response::IntoResponse;
14use axum::routing::future::RouteFuture;
15use bytes::Bytes;
16use pin_project_lite::pin_project;
17use rustls::pki_types::CertificateDer;
18use tonic::transport::server::{Connected, TcpConnectInfo};
19use tower::{Layer, Service, ServiceExt};
20
21use crate::client_address::ClientAddressLayer;
22use crate::guard::{BoxedService, Class, GrpcRejections, Guards};
23use crate::transcode::{Chooser, Direct};
24
25/// tonic's `TlsConnectInfo<TcpConnectInfo>`, the record its TLS server puts on
26/// a request and `Request::peer_certs` reads. tonic exports the name only with
27/// a TLS backend feature, so it is reached through the stream it describes.
28pub type TlsConnectInfo =
29 <tokio_rustls::server::TlsStream<tokio::net::TcpStream> as Connected>::ConnectInfo;
30
31use crate::upstream::{
32 grpc_protocol, is_grpc_web_preflight, BoxError, GrpcProtocol, PassThrough, Upstream,
33};
34
35/// The proxy as a tower service, built by
36/// [`ProxyServer::service`](crate::ProxyServer::service).
37///
38/// A request with a gRPC or gRPC-Web content type goes to the upstream as it
39/// arrived, so one listener carries REST and native gRPC. gRPC-Web is the
40/// upstream's to translate (tonic-web's `GrpcWebLayer` around its services),
41/// or the proxy's with `grpc_web.translate` for an upstream that speaks only
42/// gRPC. A gRPC-Web
43/// answer gets the proxy's CORS policy (`cors.grpc_web`), since the proxy
44/// answers the browser's preflight for it. Every other request goes to the
45/// proxy's routes (transcoded RPCs, health, metrics, OpenAPI, OIDC,
46/// forward-auth, extra routes) behind the proxy's middleware. A request no
47/// route matches is answered `404`, or handed to the service set with
48/// [`with_fallback`](Self::with_fallback). Native gRPC and the fallback pass
49/// only the guards whose scope names them (`grpc`, `fallback`); a guard's
50/// rejection of a gRPC call is a gRPC status.
51///
52/// Serve it with [`serve`](crate::serve()) or
53/// [`serve_with`](crate::serve_with) (TLS, a connection limit), or hand it to
54/// any server that takes a tower service of `http` types: a Unix socket, an
55/// existing hyper or axum server. Native gRPC needs HTTP/2 on that server
56/// (ALPN `h2` next to `http/1.1` behind TLS). Such a server tells the proxy
57/// which connection a request came on with
58/// [`for_connection`](Self::for_connection).
59///
60/// # Examples
61///
62/// ```
63/// use structured_proxy::ProxyServer;
64///
65/// # fn build() -> anyhow::Result<()> {
66/// // The embedder's own tonic services, called in process.
67/// let grpc = tonic::service::Routes::default();
68/// let service = ProxyServer::from_yaml_str("service:\n name: demo\n")?.service(grpc)?;
69/// # let _ = service;
70/// # Ok(())
71/// # }
72/// # build().unwrap();
73/// ```
74#[derive(Clone)]
75pub struct ProxyService<U> {
76 /// Shared by every clone: a server clones the service for each request
77 /// and only one path serves it.
78 shared: Arc<Shared<U>>,
79 /// The connection the requests arrive on, set per connection by the
80 /// server.
81 connection: Option<ConnectionInfo>,
82}
83
84/// What every request of a [`ProxyService`] may take a path through.
85#[derive(Clone)]
86struct Shared<U> {
87 upstream: U,
88 /// Where the requests that are not gRPC go.
89 routing: Routing,
90 /// Binary and text gRPC-Web to the upstream under the CORS policy, each
91 /// built once; `None` when the upstream owns CORS for gRPC-Web.
92 grpc_web: Option<GrpcWebCors<U>>,
93 /// The gRPC paths behind guards or through gRPC-Web translation; a
94 /// protocol that needs neither passes through with nothing in between.
95 boxed: BoxedGrpc,
96 /// The guards, for the fallback an embedder sets later.
97 guards: Arc<Guards>,
98}
99
100impl<U> std::fmt::Debug for ProxyService<U> {
101 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102 f.debug_struct("ProxyService")
103 .field("boxed_grpc", &self.shared.boxed.grpc.is_some())
104 .field("boxed_grpc_web", &self.shared.boxed.web.is_some())
105 .field("connection", &self.connection)
106 .finish_non_exhaustive()
107 }
108}
109
110/// How the requests that are not gRPC reach the proxy's routes.
111#[derive(Clone)]
112pub(crate) enum Routing {
113 /// The transcoded routes are matched by the proxy and served past any
114 /// router; the other routes by theirs.
115 Direct {
116 direct: Direct,
117 others: axum::Router,
118 },
119 /// Every route by one router, which `chooser` (with transcoded routes)
120 /// chooses a binding after; `served` is the router with it.
121 Routed {
122 router: axum::Router,
123 chooser: Option<Chooser>,
124 served: axum::Router,
125 },
126}
127
128impl Routing {
129 /// Every route by `router`, choosing among its transcoded bindings with
130 /// `chooser`.
131 pub(crate) fn routed(router: axum::Router, chooser: Option<Chooser>) -> Self {
132 let served = match &chooser {
133 Some(chooser) => chooser.clone().layer(router.clone()),
134 None => router.clone(),
135 };
136 Self::Routed {
137 router,
138 chooser,
139 served,
140 }
141 }
142
143 /// These routes, with `fallback` answering what none does.
144 fn with_fallback(self, fallback: BoxedService) -> Self {
145 match self {
146 Self::Direct { direct, others } => Self::Direct {
147 direct,
148 others: others.fallback_service(fallback),
149 },
150 Self::Routed {
151 router, chooser, ..
152 } => Self::routed(
153 router.fallback_service(fallback.clone()),
154 chooser.map(|chooser| chooser.with_fallback(fallback)),
155 ),
156 }
157 }
158}
159
160/// The gRPC paths that need more than a pass-through (guards, gRPC-Web
161/// translation), each protocol's stack built once; `None` for a protocol that
162/// passes through as it is.
163#[derive(Clone, Default)]
164struct BoxedGrpc {
165 grpc: Option<BoxedService>,
166 web: Option<BoxedService>,
167 web_text: Option<BoxedService>,
168}
169
170/// Hands a gRPC-Web call, translated to gRPC by tonic-web, to an upstream
171/// that speaks only gRPC.
172#[derive(Clone, Debug)]
173struct Translated<U> {
174 upstream: U,
175}
176
177impl<U: Upstream> Service<http::Request<tonic::body::Body>> for Translated<U> {
178 type Response = http::Response<axum::body::Body>;
179 type Error = Infallible;
180 type Future = PassThrough<U>;
181
182 #[inline]
183 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
184 Poll::Ready(Ok(()))
185 }
186
187 fn call(&mut self, request: http::Request<tonic::body::Body>) -> Self::Future {
188 let (mut parts, body) = request.into_parts();
189 // A browser's call arrives over HTTP/1.1; as gRPC it is an HTTP/2
190 // request (gRPC PROTOCOL-HTTP2).
191 parts.version = http::Version::HTTP_2;
192 // tonic-web reports the size of the base64 text body for the decoded
193 // one, and an HTTP/2 client would announce that as its length; the
194 // gRPC body goes without one.
195 let body = tonic::body::Body::new(Unsized { body });
196 PassThrough::new(
197 self.upstream.clone(),
198 http::Request::from_parts(parts, body),
199 GrpcProtocol::Grpc,
200 )
201 }
202}
203
204pin_project! {
205 /// `body` without its size hint.
206 struct Unsized {
207 #[pin]
208 body: tonic::body::Body,
209 }
210}
211
212impl http_body::Body for Unsized {
213 type Data = Bytes;
214 type Error = tonic::Status;
215
216 #[inline]
217 fn poll_frame(
218 self: Pin<&mut Self>,
219 cx: &mut Context<'_>,
220 ) -> Poll<Option<Result<http_body::Frame<Bytes>, tonic::Status>>> {
221 self.project().body.poll_frame(cx)
222 }
223
224 #[inline]
225 fn is_end_stream(&self) -> bool {
226 self.body.is_end_stream()
227 }
228}
229
230/// gRPC-Web pass-through, one CORS-wrapped path per encoding.
231#[derive(Clone, Debug)]
232struct GrpcWebCors<U> {
233 web: Cors<Forward<U>>,
234 web_text: Cors<Forward<U>>,
235}
236
237/// Hands a request to the upstream in a known protocol, as a tower service so
238/// a layer can wrap it.
239#[derive(Clone, Debug)]
240struct Forward<U> {
241 upstream: U,
242 protocol: GrpcProtocol,
243}
244
245impl<U: Upstream> Service<http::Request<tonic::body::Body>> for Forward<U> {
246 type Response = http::Response<axum::body::Body>;
247 type Error = Infallible;
248 type Future = PassThrough<U>;
249
250 #[inline]
251 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
252 // The upstream's readiness is waited for per request, on its clone.
253 Poll::Ready(Ok(()))
254 }
255
256 fn call(&mut self, request: http::Request<tonic::body::Body>) -> Self::Future {
257 PassThrough::new(self.upstream.clone(), request, self.protocol)
258 }
259}
260
261/// The guarded path's end: the guards run on axum's request type.
262impl<U: Upstream> Service<axum::extract::Request> for Forward<U> {
263 type Response = http::Response<axum::body::Body>;
264 type Error = Infallible;
265 type Future = PassThrough<U>;
266
267 #[inline]
268 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
269 Poll::Ready(Ok(()))
270 }
271
272 fn call(&mut self, request: axum::extract::Request) -> Self::Future {
273 let request = request.map(tonic::body::Body::new);
274 PassThrough::new(self.upstream.clone(), request, self.protocol)
275 }
276}
277
278/// The connection requests arrive on, in the form a tonic server records it:
279/// the TCP ends, and behind TLS the client's certificate chain.
280///
281/// Built from what [`Connected::connect_info`] returns for a
282/// `tokio::net::TcpStream` (`ConnectionInfo::from`) or a
283/// `tokio_rustls::server::TlsStream<TcpStream>` ([`ConnectionInfo::tls`]), so a
284/// tonic handler behind the proxy reads it as it would behind tonic's own
285/// server: `Request::remote_addr`, and `Request::peer_certs` for mTLS.
286///
287/// [`Connected::connect_info`]: tonic::transport::server::Connected::connect_info
288#[derive(Clone, Debug)]
289pub struct ConnectionInfo {
290 tcp: TcpConnectInfo,
291 tls: Option<TlsConnectInfo>,
292}
293
294impl ConnectionInfo {
295 /// The client's address.
296 pub fn remote_addr(&self) -> Option<SocketAddr> {
297 self.tcp.remote_addr
298 }
299
300 /// The certificate chain the client presented over TLS, if it did.
301 pub fn peer_certs(&self) -> Option<Arc<Vec<CertificateDer<'static>>>> {
302 self.tls.as_ref().and_then(|tls| tls.peer_certs())
303 }
304
305 /// Put the connection where tonic reads it: [`TcpConnectInfo`] for
306 /// `Request::remote_addr` and, behind TLS, [`TlsConnectInfo`] for
307 /// `Request::peer_certs`.
308 pub(crate) fn into_tonic_extensions(self, extensions: &mut http::Extensions) {
309 extensions.insert(self.tcp);
310 if let Some(tls) = self.tls {
311 extensions.insert(tls);
312 }
313 }
314}
315
316impl From<TcpConnectInfo> for ConnectionInfo {
317 fn from(tcp: TcpConnectInfo) -> Self {
318 Self { tcp, tls: None }
319 }
320}
321
322impl ConnectionInfo {
323 /// A TLS connection, from what `connect_info` reports for a
324 /// `tokio_rustls::server::TlsStream<TcpStream>`: the TCP ends and the
325 /// client's certificate chain.
326 pub fn tls(tls: TlsConnectInfo) -> Self {
327 Self {
328 tcp: tls.get_ref().clone(),
329 tls: Some(tls),
330 }
331 }
332}
333
334impl<U: Upstream> ProxyService<U> {
335 /// The service over `upstream` and the routes of `routing`; gRPC-Web
336 /// answers carry `grpc_web_cors` when set, and gRPC-Web calls are
337 /// translated to gRPC when `translate_grpc_web`.
338 pub(crate) fn new(
339 upstream: U,
340 routing: Routing,
341 grpc_web_cors: Option<CorsLayer>,
342 guards: Arc<Guards>,
343 translate_grpc_web: bool,
344 ) -> Self {
345 let forward = |protocol| Forward {
346 upstream: upstream.clone(),
347 protocol,
348 };
349 let guard_grpc = guards.cover(Class::Grpc);
350 let stack = |protocol: GrpcProtocol| {
351 let web = protocol != GrpcProtocol::Grpc;
352 let mut service = if web && translate_grpc_web {
353 let translator = tonic_web::GrpcWebLayer::new().layer(Translated {
354 upstream: upstream.clone(),
355 });
356 BoxedService::new(ServiceExt::<axum::extract::Request>::map_response(
357 translator,
358 |response| response.map(axum::body::Body::new),
359 ))
360 } else {
361 BoxedService::new(forward(protocol))
362 };
363 if guard_grpc {
364 service = BoxedService::new(GrpcRejections {
365 inner: guards.service(service, Class::Grpc),
366 protocol,
367 });
368 }
369 // CORS outermost, so a rejected gRPC-Web call still carries it.
370 match &grpc_web_cors {
371 Some(cors) if web => BoxedService::new(cors.layer(service)),
372 _ => service,
373 }
374 };
375 let boxed_web = guard_grpc || translate_grpc_web;
376 let boxed = BoxedGrpc {
377 grpc: guard_grpc.then(|| stack(GrpcProtocol::Grpc)),
378 web: boxed_web.then(|| stack(GrpcProtocol::Web)),
379 web_text: boxed_web.then(|| stack(GrpcProtocol::WebText)),
380 };
381 // The pass-through of gRPC-Web, and the answer to its preflights.
382 let grpc_web = grpc_web_cors.map(|cors| GrpcWebCors {
383 web: cors.layer(forward(GrpcProtocol::Web)),
384 web_text: cors.layer(forward(GrpcProtocol::WebText)),
385 });
386 Self {
387 shared: Arc::new(Shared {
388 upstream,
389 routing,
390 grpc_web,
391 boxed,
392 guards,
393 }),
394 connection: None,
395 }
396 }
397
398 /// Hand the requests no route matches to `fallback` instead of answering
399 /// `404`: an embedder's own REST routes, a static site, anything that is a
400 /// tower service. Only the guards whose scope names `fallback` traffic see
401 /// them; CORS and tracing are the fallback's own.
402 ///
403 /// A URL whose path a transcoded template's route matches, but which no
404 /// binding answers (its field template or custom verb does not fit), is
405 /// the fallback's too. A request whose path a route answers but not with
406 /// its method stays with the proxy (`405`), as do every gRPC request and
407 /// every browser preflight for a gRPC-Web call, which follows the call it
408 /// announces.
409 #[must_use]
410 pub fn with_fallback<F>(mut self, fallback: F) -> Self
411 where
412 F: Service<axum::extract::Request, Error = Infallible> + Clone + Send + Sync + 'static,
413 F::Response: IntoResponse,
414 F::Future: Send + 'static,
415 {
416 let shared = Arc::make_mut(&mut self.shared);
417 let fallback = BoxedService::new(fallback.map_response(IntoResponse::into_response));
418 let guarded = if shared.guards.cover(Class::Fallback) {
419 shared.guards.service(fallback, Class::Fallback)
420 } else {
421 fallback
422 };
423 // The routes' own layers do not reach a fallback set after them, so
424 // it gets the client-address resolution of its own, around its guards.
425 // A URL no transcoded binding answers goes to the other routes, which
426 // hand what they do not answer to the same fallback.
427 let resolve = ClientAddressLayer::with(shared.guards.client_address.clone());
428 let fallback = BoxedService::new(resolve.layer(guarded));
429 shared.routing = shared.routing.clone().with_fallback(fallback);
430 self
431 }
432
433 /// This service for the requests of one connection: the proxy's
434 /// middleware sees its client's address (rate limits by IP, the auth
435 /// decider), and a tonic upstream in process reads it with
436 /// `Request::remote_addr`, and its TLS client certificates with
437 /// `Request::peer_certs`, for native and transcoded calls alike.
438 ///
439 /// A server of your own calls it once per accepted connection, with what
440 /// tonic's [`Connected`] trait
441 /// reports for the stream. [`serve_with`](crate::serve_with) does this
442 /// itself.
443 ///
444 /// # Examples
445 ///
446 /// ```no_run
447 /// use structured_proxy::ProxyServer;
448 /// use tonic::transport::server::Connected;
449 ///
450 /// # async fn run() -> anyhow::Result<()> {
451 /// let proxy = ProxyServer::from_yaml_str("service:\n name: demo\n")?
452 /// .service(tonic::service::Routes::default())?;
453 /// let listener = tokio::net::TcpListener::bind("0.0.0.0:8443").await?;
454 /// let (tcp, _) = listener.accept().await?;
455 /// // After a TLS handshake, the `TlsStream` reports the client's certificates too.
456 /// let service = proxy.for_connection(tcp.connect_info());
457 /// # let _ = service;
458 /// # Ok(())
459 /// # }
460 /// ```
461 ///
462 /// Without it, a request's peer comes from the `ConnectInfo` an outer
463 /// axum server recorded, when there is one.
464 #[must_use]
465 pub fn for_connection(&self, connection: impl Into<ConnectionInfo>) -> Self {
466 Self {
467 shared: self.shared.clone(),
468 connection: Some(connection.into()),
469 }
470 }
471}
472
473/// The connection `request` came on: `connection`, the one given to
474/// [`ProxyService::for_connection`], else the peer an outer axum server
475/// recorded as `ConnectInfo`, so the upstream sees the same client the proxy's
476/// middleware does. A free function, so the caller keeps its other fields.
477fn connection_of<B>(
478 connection: Option<&ConnectionInfo>,
479 request: &http::Request<B>,
480) -> Option<ConnectionInfo> {
481 if let Some(connection) = connection {
482 return Some(connection.clone());
483 }
484 let ConnectInfo(remote) = request.extensions().get::<ConnectInfo<SocketAddr>>()?;
485 Some(ConnectionInfo::from(TcpConnectInfo {
486 local_addr: None,
487 remote_addr: Some(*remote),
488 }))
489}
490
491impl<U, B> Service<http::Request<B>> for ProxyService<U>
492where
493 U: Upstream,
494 B: http_body::Body<Data = Bytes> + Send + 'static,
495 B::Error: Into<BoxError>,
496{
497 type Response = http::Response<axum::body::Body>;
498 type Error = Infallible;
499 type Future = ResponseFuture<U>;
500
501 #[inline]
502 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
503 // Readiness of the upstream is waited for per request, on its clone.
504 Poll::Ready(Ok(()))
505 }
506
507 fn call(&mut self, mut request: http::Request<B>) -> Self::Future {
508 // A gRPC-Web preflight goes where the call it announces goes, so both
509 // get one CORS policy: the proxy's, or the upstream's when it owns
510 // CORS. A fallback in between would answer it with neither.
511 let call = grpc_protocol(request.headers());
512 let protocol = call.or_else(|| {
513 is_grpc_web_preflight(request.method(), request.headers()).then_some(GrpcProtocol::Web)
514 });
515 // Only the path the request takes is cloned for it.
516 let shared = &*self.shared;
517 // A preflight is not a call: it skips the guards and the translation.
518 let boxed = match call {
519 Some(GrpcProtocol::Grpc) => shared.boxed.grpc.as_ref(),
520 Some(GrpcProtocol::Web) => shared.boxed.web.as_ref(),
521 Some(GrpcProtocol::WebText) => shared.boxed.web_text.as_ref(),
522 None => None,
523 };
524 let inner = if let Some(protocol) = protocol {
525 // Whatever goes the upstream's way, guarded or not: its client
526 // address is resolved and its forwarding headers rewritten here,
527 // once, for the guards and the upstream alike. The call carries
528 // its connection the way tonic's server hands it to a handler.
529 let connection = connection_of(self.connection.as_ref(), &request);
530 let peer = connection.as_ref().and_then(ConnectionInfo::remote_addr);
531 shared.guards.client_address.apply(&mut request, peer);
532 if let Some(connection) = connection {
533 connection.into_tonic_extensions(request.extensions_mut());
534 }
535 match boxed {
536 // Every service on these paths is ready at once: guards and
537 // the translator are middleware, and `Forward` waits for the
538 // upstream per request.
539 Some(service) => Inner::Boxed {
540 future: service.clone().call(request.map(axum::body::Body::new)),
541 },
542 None => {
543 let request = request.map(tonic::body::Body::new);
544 // A browser only speaks gRPC-Web, and the routes answered
545 // its preflight: its call gets the same CORS policy.
546 let cors = match (&shared.grpc_web, protocol) {
547 (Some(cors), GrpcProtocol::Web) => Some(&cors.web),
548 (Some(cors), GrpcProtocol::WebText) => Some(&cors.web_text),
549 _ => None,
550 };
551 match cors {
552 // `Forward` is always ready, and so is CORS around it.
553 Some(cors) => Inner::GrpcWeb {
554 future: cors.clone().call(request),
555 },
556 None => Inner::Grpc {
557 call: PassThrough::new(shared.upstream.clone(), request, protocol),
558 },
559 }
560 }
561 }
562 } else {
563 // The middleware reads the peer as axum's `ConnectInfo`; the
564 // transcoder passes the whole connection on to the upstream.
565 if let Some(connection) = &self.connection {
566 let extensions = request.extensions_mut();
567 if let Some(remote) = connection.remote_addr() {
568 extensions.insert(ConnectInfo(remote));
569 }
570 extensions.insert(connection.clone());
571 } else if let Some(connection) = connection_of(None, &request) {
572 request.extensions_mut().insert(connection);
573 }
574 // A router and the transcoded routes' layers are always ready.
575 // A router is a reference count.
576 match &shared.routing {
577 Routing::Direct { direct, others } => {
578 if direct.takes(&mut request) {
579 Inner::Boxed {
580 future: direct
581 .service()
582 .clone()
583 .call(request.map(axum::body::Body::new)),
584 }
585 } else {
586 Inner::Routes {
587 future: others.clone().call(request),
588 }
589 }
590 }
591 Routing::Routed { served, .. } => Inner::Routes {
592 future: served.clone().call(request),
593 },
594 }
595 };
596 ResponseFuture { inner }
597 }
598}
599
600pin_project! {
601 /// The response future of [`ProxyService`].
602 pub struct ResponseFuture<U: Upstream> {
603 #[pin]
604 inner: Inner<U>,
605 }
606}
607
608pin_project! {
609 #[project = InnerProj]
610 enum Inner<U: Upstream> {
611 Routes {
612 #[pin]
613 future: RouteFuture<Infallible>,
614 },
615 Grpc {
616 #[pin]
617 call: PassThrough<U>,
618 },
619 GrpcWeb {
620 #[pin]
621 future: CorsFuture<PassThrough<U>, axum::body::Body>,
622 },
623 Boxed {
624 #[pin]
625 future: <BoxedService as Service<axum::extract::Request>>::Future,
626 },
627 }
628}
629
630impl<U: Upstream> Future for ResponseFuture<U> {
631 type Output = Result<http::Response<axum::body::Body>, Infallible>;
632
633 #[inline]
634 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
635 match self.project().inner.project() {
636 InnerProj::Routes { future } => future.poll(cx),
637 InnerProj::Grpc { call } => call.poll(cx),
638 InnerProj::GrpcWeb { future } => future.poll(cx),
639 InnerProj::Boxed { future } => future.poll(cx),
640 }
641 }
642}
643
644#[cfg(test)]
645mod tests;