forge
ExploreLog in

Tahinli/forge

public
0 watching
★ 0
4.7 KiB
1//! Client IP for throttling.
2//!
3//! The TCP peer is the client unless it is listed in `config.trusted_proxies`. A trusted peer's
4//! `X-Forwarded-For` is read right to left, skipping trusted proxies, and the first untrusted
5//! address is the client. Any unusable header falls back to the peer. IPv6 is bucketed by /64, so
6//! one client holding a /64 cannot multiply its attempt budget.
7
8use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
9
10use ipnet::IpNet;
11use topcoat::context::{Cx, app_context};
12use topcoat::router::request::{headers, remote_addr};
13
14use crate::state::AppState;
15
16const FORWARDED_FOR: &str = "x-forwarded-for";
17
18/// The client IP for this request, bucketed for throttling. Unix-socket requests have no peer and
19/// map to the unspecified address.
20pub fn client_ip(cx: &Cx) -> IpAddr {
21 let trusted = &app_context::<AppState>(cx).config.trusted_proxies;
22 let peer = remote_addr(cx).map(|addr| addr.ip());
23 let forwarded = headers(cx)
24 .get_all(FORWARDED_FOR)
25 .iter()
26 .filter_map(|value| value.to_str().ok())
27 .collect::<Vec<_>>()
28 .join(",");
29 resolve(peer, &forwarded, trusted)
30}
31
32/// Pure form of [`client_ip`]: `forwarded` is the combined `X-Forwarded-For` value, or empty.
33pub fn resolve(peer: Option<IpAddr>, forwarded: &str, trusted: &[IpNet]) -> IpAddr {
34 let Some(peer) = peer.map(normalize) else {
35 return bucket(IpAddr::V4(Ipv4Addr::UNSPECIFIED));
36 };
37 if !is_trusted(peer, trusted) {
38 return bucket(peer);
39 }
40 for entry in forwarded.rsplit(',') {
41 let Ok(ip) = entry.trim().parse::<IpAddr>() else {
42 return bucket(peer);
43 };
44 let ip = normalize(ip);
45 if !is_trusted(ip, trusted) {
46 return bucket(ip);
47 }
48 }
49 bucket(peer)
50}
51
52fn is_trusted(ip: IpAddr, trusted: &[IpNet]) -> bool {
53 trusted.iter().any(|net| net.contains(&ip))
54}
55
56/// IPv4-mapped IPv6 addresses are reported as IPv4.
57fn normalize(ip: IpAddr) -> IpAddr {
58 match ip {
59 IpAddr::V6(v6) => v6.to_ipv4_mapped().map_or(IpAddr::V6(v6), IpAddr::V4),
60 IpAddr::V4(_) => ip,
61 }
62}
63
64/// Clears the host part of IPv6 addresses so every address in a /64 shares one bucket.
65fn bucket(ip: IpAddr) -> IpAddr {
66 match ip {
67 IpAddr::V4(_) => ip,
68 IpAddr::V6(v6) => {
69 let mut octets = v6.octets();
70 octets[8..].fill(0);
71 IpAddr::V6(Ipv6Addr::from(octets))
72 }
73 }
74}
75
76#[cfg(test)]
77mod tests {
78 use super::*;
79
80 fn ip(text: &str) -> IpAddr {
81 text.parse().unwrap()
82 }
83
84 fn net(text: &str) -> IpNet {
85 text.parse().unwrap()
86 }
87
88 #[test]
89 fn forwarded_for_is_ignored_from_untrusted_peer() {
90 let trusted = [net("10.0.0.0/8")];
91 let got = resolve(Some(ip("203.0.113.9")), "198.51.100.7", &trusted);
92 assert_eq!(got, ip("203.0.113.9"));
93 }
94
95 #[test]
96 fn forwarded_for_is_honored_from_trusted_peer() {
97 let trusted = [net("127.0.0.1/32")];
98 let got = resolve(Some(ip("127.0.0.1")), "198.51.100.7", &trusted);
99 assert_eq!(got, ip("198.51.100.7"));
100 }
101
102 #[test]
103 fn right_most_untrusted_entry_wins() {
104 let trusted = [net("127.0.0.1/32"), net("10.0.0.0/8")];
105 // The client may prepend anything; the proxy appends the address it saw.
106 let got = resolve(
107 Some(ip("127.0.0.1")),
108 "1.2.3.4, 198.51.100.7, 10.0.0.5",
109 &trusted,
110 );
111 assert_eq!(got, ip("198.51.100.7"));
112 }
113
114 #[test]
115 fn unusable_header_falls_back_to_peer() {
116 let trusted = [net("127.0.0.1/32")];
117 assert_eq!(
118 resolve(Some(ip("127.0.0.1")), "", &trusted),
119 ip("127.0.0.1")
120 );
121 assert_eq!(
122 resolve(Some(ip("127.0.0.1")), "garbage", &trusted),
123 ip("127.0.0.1")
124 );
125 assert_eq!(
126 resolve(Some(ip("127.0.0.1")), "127.0.0.1", &trusted),
127 ip("127.0.0.1")
128 );
129 }
130
131 #[test]
132 fn ipv6_is_bucketed_by_slash_64() {
133 let trusted: [IpNet; 0] = [];
134 let a = resolve(Some(ip("2001:db8:1:2:aaaa::1")), "", &trusted);
135 let b = resolve(Some(ip("2001:db8:1:2:bbbb::9")), "", &trusted);
136 let c = resolve(Some(ip("2001:db8:1:3::1")), "", &trusted);
137 assert_eq!(a, b);
138 assert_ne!(a, c);
139 assert_eq!(a, ip("2001:db8:1:2::"));
140 }
141
142 #[test]
143 fn ipv4_mapped_peer_is_reported_as_ipv4() {
144 let trusted: [IpNet; 0] = [];
145 assert_eq!(
146 resolve(Some(ip("::ffff:192.0.2.5")), "", &trusted),
147 ip("192.0.2.5")
148 );
149 }
150
151 #[test]
152 fn unknown_peer_maps_to_unspecified() {
153 let trusted: [IpNet; 0] = [];
154 assert_eq!(resolve(None, "1.2.3.4", &trusted), ip("0.0.0.0"));
155 }
156}