hennery_kernel/
client_address.rs1use axum::extract::{ConnectInfo, FromRequestParts, Request};
3use axum::http::{HeaderMap, StatusCode, request::Parts};
4use axum::middleware::Next;
5use axum::response::Response;
6use std::net::{IpAddr, SocketAddr};
7
8pub const HEADER: &str = "hennery-client-ip";
9
10#[derive(Clone, Debug, Default)]
11pub struct ProxyPolicy {
12 peers: Vec<IpAddr>,
13}
14
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub enum Source {
17 Peer,
18 MissingHeader,
19 Proxy,
20}
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq)]
23pub struct ClientAddress {
24 pub peer: IpAddr,
25 pub effective: IpAddr,
26 pub source: Source,
27}
28
29#[derive(Clone, Copy, Debug, PartialEq, Eq)]
30pub struct InvalidClientAddress;
31
32fn canonical(ip: IpAddr) -> IpAddr {
33 ip.to_canonical()
34}
35fn valid(ip: IpAddr) -> bool {
36 !ip.is_unspecified() && !ip.is_multicast()
37}
38
39impl ProxyPolicy {
40 pub fn new(peers: Vec<IpAddr>) -> anyhow::Result<Self> {
41 anyhow::ensure!(
42 peers.len() <= 8,
43 "proxy_client_ip_peers allows at most eight exact IP addresses"
44 );
45 let mut normalized = Vec::new();
46 for ip in peers {
47 let ip = canonical(ip);
48 anyhow::ensure!(valid(ip), "proxy_client_ip_peers requires unicast addresses");
49 anyhow::ensure!(
50 !normalized.contains(&ip),
51 "proxy_client_ip_peers contains a duplicate address"
52 );
53 normalized.push(ip);
54 }
55 Ok(Self { peers: normalized })
56 }
57
58 pub fn is_configured(&self) -> bool {
59 !self.peers.is_empty()
60 }
61
62 pub fn resolve(&self, peer: IpAddr, headers: &HeaderMap) -> Result<ClientAddress, InvalidClientAddress> {
63 let peer = canonical(peer);
64 let mut address = ClientAddress {
65 peer,
66 effective: peer,
67 source: Source::Peer,
68 };
69 if !self.peers.contains(&peer) {
70 return Ok(address);
71 }
72 let mut values = headers.get_all(HEADER).iter();
73 let Some(value) = values.next() else {
74 address.source = Source::MissingHeader;
75 return Ok(address);
76 };
77 if values.next().is_some() {
78 return Err(InvalidClientAddress);
79 }
80 let ip = canonical(
81 value
82 .to_str()
83 .map_err(|_| InvalidClientAddress)?
84 .parse::<IpAddr>()
85 .map_err(|_| InvalidClientAddress)?,
86 );
87 if !valid(ip) {
88 return Err(InvalidClientAddress);
89 }
90 address.effective = ip;
91 address.source = Source::Proxy;
92 Ok(address)
93 }
94}
95
96fn refused() -> Response {
97 crate::auth_api::error(
98 StatusCode::BAD_REQUEST,
99 "invalid_client_address",
100 "invalid client address header",
101 )
102}
103
104pub async fn resolve_request(mut request: Request, next: Next) -> Response {
107 let Some(ConnectInfo(peer)) = request.extensions().get::<ConnectInfo<SocketAddr>>().copied() else {
108 return crate::auth_api::error(
109 StatusCode::INTERNAL_SERVER_ERROR,
110 "internal",
111 "missing connection address",
112 );
113 };
114 let policy = request.extensions().get::<ProxyPolicy>().cloned().unwrap_or_default();
115 match policy.resolve(peer.ip(), request.headers()) {
116 Ok(address) => {
117 request.extensions_mut().insert(address);
118 next.run(request).await
119 }
120 Err(_) => refused(),
121 }
122}
123
124impl<S: Send + Sync> FromRequestParts<S> for ClientAddress {
125 type Rejection = Response;
126 async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Self, Self::Rejection> {
127 if let Some(address) = parts.extensions.get::<Self>() {
128 return Ok(*address);
129 }
130 let ConnectInfo(peer) = parts
131 .extensions
132 .get::<ConnectInfo<SocketAddr>>()
133 .copied()
134 .ok_or_else(|| {
135 crate::auth_api::error(
136 StatusCode::INTERNAL_SERVER_ERROR,
137 "internal",
138 "missing connection address",
139 )
140 })?;
141 parts
142 .extensions
143 .get::<ProxyPolicy>()
144 .cloned()
145 .unwrap_or_default()
146 .resolve(peer.ip(), &parts.headers)
147 .map_err(|_| refused())
148 }
149}