This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

knot2/xrpc: read forwarded chain past every listed proxy

Lewis: May this revision serve well! <did:plc:3fwecdnvtcscjnrx2p4n7alz>

author did:plc:3fwecdnvtcscjnrx2p4n7a… date (Jul 31, 2026, 3:48 PM +0300) commit 5422d7ca parent 5fa53a05 change-id xsrsuvvw
+188 -199
+1
Cargo.lock
··· 4917 4917 "serde", 4918 4918 "serde_json", 4919 4919 "thiserror 2.0.18", 4920 + "trusted-proxies", 4920 4921 "url", 4921 4922 ] 4922 4923
+1 -1
knot2/README.md
··· 193 193 194 194 Set `xrpc.trusted_proxy_header = "x-forwarded-for"` when doing this, otherwise every client looks like it comes from the proxy and the ratelimiter wil treat them as one very busy mister. Only set it behind a proxy the operator controls, since a direct client can like, invent that header. 195 195 196 - Add `xrpc.trusted_proxies = ["fd00:1::4", "10.89.0.4"]` for example, one entry per address that the proxy connects from, so the knot honors that header from the proxy alone & ratelimits anyone else by the address they connected from. 196 + Add `xrpc.trusted_proxies = ["fd00:1::4", "10.89.0.4"]` for example, one entry per address that the proxy connects from, so the knot honors that header from the proxy alone & ratelimits anyone else by the address they connected from. A CIDR block will work too, `["173.245.48.0/20"]` covers a whole provider's edge. If several proxies you control are in the path, list them all. Mister knot will read the chain right -> left, iterate over every entry the list covers, and take the first entry it doesn't. It will read 32 entries at most, and the knot will ratelimit by the address the request connected from when the list covers all 32. 197 197 198 198 The knot can also terminate TLS itself (and that's the only way to get its HTTP3 support) because a plain TCP frontend can't proxy QUIC. Using a certificate the operator already manages: 199 199
+65 -37
knot2/crates/knot-config/src/lib.rs
··· 1 1 use std::fmt; 2 - use std::net::{AddrParseError, IpAddr, SocketAddr}; 2 + use std::net::SocketAddr; 3 3 use std::path::{Path, PathBuf}; 4 4 use std::sync::OnceLock; 5 5 use std::time::Duration; ··· 7 7 use base64::Engine; 8 8 use confique::Config; 9 9 use knot_runtime::HttpLimits; 10 - use knot_types::{AccountDid, AdmissionPolicy, AppviewEndpoint}; 10 + use knot_types::{ 11 + AccountDid, AdmissionPolicy, AppviewEndpoint, ProxyNetError, TrustedProxies, comma_separated, 12 + }; 11 13 use url::Url; 12 14 13 15 #[derive(Debug, Config)] ··· 300 302 pub fork_fetch_timeout_ms: u64, 301 303 302 304 /// When the knot runs behind a trusted reverse proxy that terminates TLS, 303 - /// set this to the header the proxy appends the client address to, for 304 - /// example x-forwarded-for. The rightmost entry is used. Leave unset when 305 - /// the knot is directly exposed so the socket peer address is used. Only set 306 - /// this when a trusted proxy overwrites or appends the header, since a client 307 - /// can forge it otherwise. 305 + /// set this to the header the proxy appends the client address to, 306 + /// for example x-forwarded-for. 307 + /// The knot will read the chain right -> left 308 + /// and take the first entry that `trusted_proxies` doesn't cover. 309 + /// Leave unset when the knot is directly exposed so the socket peer address is used. 310 + /// Only set this when a trusted proxy overwrites or appends the header, 311 + /// since a client can forge it otherwise. 308 312 #[config(env = "KNOT_XRPC_TRUSTED_PROXY_HEADER")] 309 313 pub trusted_proxy_header: Option<String>, 310 314 311 - /// IP addresses whose `trusted_proxy_header` the knot honors, 312 - /// without a port, 315 + /// Addresses whose `trusted_proxy_header` the knot honors, 316 + /// each a bare IP without a port or a CIDR block such as 173.245.48.0/20, 313 317 /// for ex the loopback address of a reverse proxy on the same host. 314 - /// The knot rate-limits a request from any other address 315 - /// by its own socket address and ignores the header. 316 - /// Leave empty to honor the header from every peer, 317 - /// which is safe *only* if nothing but the proxy can reach this knot. 318 + /// The knot will rate-limit a request from any other address 319 + /// by its own socket address and ignore the header. 320 + /// These same addresses are hops the knot will iterate over when it reads 321 + /// the header, so list every proxy you control in the path. 322 + /// A proxy that the knot doesn't know about becomes the entry it keys on, 323 + /// and everyone that proxy serves will then share one rate-limit bucket. 324 + /// The knot will read the last 32 entries of the chain, at most. 325 + /// When the list covers all 32, the knot 326 + /// will rate-limit by the address the request connected from. 327 + /// Leave empty to honor the header from every peer and take its rightmost 328 + /// entry, which is safe *only* while every route to this knot passes 329 + /// through the proxy. 318 330 #[config( 319 331 env = "KNOT_XRPC_TRUSTED_PROXIES", 320 - parse_env = parse_trusted_proxies, 332 + parse_env = comma_separated, 321 333 default = [] 322 334 )] 323 - pub trusted_proxies: Vec<IpAddr>, 335 + pub trusted_proxies: Vec<String>, 324 336 325 337 #[config(env = "KNOT_XRPC_EVENTS_REPLAY_BUFFER", default = 4096)] 326 338 pub events_replay_buffer: u32, ··· 439 451 .collect() 440 452 } 441 453 442 - fn parse_trusted_proxies(raw: &str) -> Result<Vec<IpAddr>, AddrParseError> { 443 - raw.split(',') 444 - .map(str::trim) 445 - .filter(|item| !item.is_empty()) 446 - .map(str::parse) 447 - .collect() 448 - } 449 - 450 454 impl KnotConfig { 451 455 pub fn object_format(&self) -> Option<knot_types::ObjectFormat> { 452 456 knot_types::ObjectFormat::from_capability(&self.git.object_format) 453 457 } 454 458 459 + pub fn trusted_proxies(&self) -> Result<TrustedProxies, ProxyNetError> { 460 + TrustedProxies::parse(self.xrpc.trusted_proxies.iter().map(String::as_str)) 461 + } 462 + 455 463 pub fn tls_enabled(&self) -> bool { 456 464 self.static_cert_enabled() || self.tls.acme_enabled 457 465 } ··· 834 842 self.xrpc.trusted_proxy_header.is_some() || self.xrpc.trusted_proxies.is_empty(), 835 843 "xrpc.trusted_proxies needs xrpc.trusted_proxy_header, the header the knot honors from those addresses", 836 844 ), 845 + self.trusted_proxies() 846 + .err() 847 + .map(|error| format!("xrpc.trusted_proxies: {error}")), 837 848 self.acl 838 849 .legacy_admin_secret_env 839 850 .as_deref() ··· 1576 1587 ), 1577 1588 ( 1578 1589 "trusted_proxies_without_the_header_the_knot_honors", 1579 - |config| config.xrpc.trusted_proxies = vec!["127.0.0.1".parse().unwrap()], 1590 + |config| config.xrpc.trusted_proxies = vec!["127.0.0.1".to_owned()], 1580 1591 "needs xrpc.trusted_proxy_header", 1581 1592 ), 1582 1593 ]; ··· 1648 1659 } 1649 1660 1650 1661 #[test] 1651 - fn trusted_proxies_parse_from_comma_separated_env() { 1652 - assert_eq!( 1653 - parse_trusted_proxies("127.0.0.1, ::1").unwrap(), 1654 - vec![ 1655 - "127.0.0.1".parse::<IpAddr>().unwrap(), 1656 - "::1".parse::<IpAddr>().unwrap() 1657 - ] 1658 - ); 1659 - assert!(parse_trusted_proxies("").unwrap().is_empty()); 1660 - assert!( 1661 - parse_trusted_proxies("127.0.0.1:5555").is_err(), 1662 - "xrpc.trusted_proxies takes bare IP addresses, so a port must fail to parse" 1663 - ); 1662 + fn a_trusted_proxy_entry_takes_an_address_or_a_cidr_block() { 1663 + let listing = |entries: &[&str]| { 1664 + let mut config = sample(); 1665 + config.xrpc.trusted_proxy_header = Some("x-forwarded-for".to_owned()); 1666 + config.xrpc.trusted_proxies = entries.iter().map(|&e| e.to_owned()).collect(); 1667 + config 1668 + }; 1669 + let config = listing(&["127.0.0.1", "173.245.48.0/20", "2400:cb00::/32"]); 1670 + assert!(config.validate().is_ok()); 1671 + let proxies = config.trusted_proxies().unwrap(); 1672 + assert!(proxies.contains("173.245.48.7".parse().unwrap())); 1673 + assert!(proxies.contains("2400:cb00::1".parse().unwrap())); 1674 + 1675 + [ 1676 + ( 1677 + "127.0.0.1:5555", 1678 + "127.0.0.1:5555", 1679 + "xrpc.trusted_proxies takes a bare address or a CIDR block, so the failure must quote the rejected entry", 1680 + ), 1681 + ( 1682 + " ", 1683 + "blank entry", 1684 + "parse refuses a blank in the file instead of reading a list the operator filled in as empty, because the knot honors the header from every peer while the list is empty. `comma_separated` discards the same blank from the env var, since it can't tell that blank from the gap a trailing separator leaves", 1685 + ), 1686 + ] 1687 + .iter() 1688 + .for_each(|&(entry, quoted, why)| { 1689 + let report = listing(&[entry]).validate().unwrap_err().to_string(); 1690 + assert!(report.contains(quoted), "{why}: {report}"); 1691 + }); 1664 1692 } 1665 1693 1666 1694 #[test]
+1 -1
knot2/crates/knot-edge/src/robustness.rs
··· 316 316 fn trusting_loopback() -> ProxyTrust { 317 317 ProxyTrust::new( 318 318 Some(forwarded_for()), 319 - knot_types::TrustedProxies::new(["127.0.0.1".parse::<IpAddr>().unwrap()]), 319 + knot_types::TrustedProxies::parse(["127.0.0.1"]).unwrap(), 320 320 ) 321 321 } 322 322
+1 -3
knot2/crates/knot-server/src/main.rs
··· 290 290 .map(|header| axum::http::HeaderName::from_bytes(header.as_bytes())) 291 291 .transpose() 292 292 .context("xrpc.trusted_proxy_header isn't a valid HTTP header name")?; 293 - let trusted_proxies = 294 - knot_types::TrustedProxies::new(config.xrpc.trusted_proxies.iter().copied()); 295 - let proxy_trust = knot_types::ProxyTrust::new(trusted_proxy_header, trusted_proxies); 293 + let proxy_trust = knot_types::ProxyTrust::new(trusted_proxy_header, config.trusted_proxies()?); 296 294 if proxy_trust.trusts_any_peer() && !http_addr.ip().is_loopback() { 297 295 tracing::warn!( 298 296 bind = %http_addr,
+1
knot2/crates/knot-types/Cargo.toml
··· 12 12 serde = { workspace = true } 13 13 serde_json = { workspace = true } 14 14 thiserror = { workspace = true } 15 + trusted-proxies = { workspace = true } 15 16 url = { workspace = true } 16 17 17 18 [dev-dependencies]
+1 -1
knot2/crates/knot-types/src/lib.rs
··· 20 20 pub use hex::{decode_hex, lowercase_hex}; 21 21 22 22 mod net; 23 - pub use net::{PeerKey, ProxyTrust, TrustedProxies}; 23 + pub use net::{PeerKey, ProxyNetError, ProxyTrust, TrustedProxies, comma_separated}; 24 24 25 25 pub use jacquard_common::CowStr; 26 26 pub use jacquard_common::DefaultStr;
+96 -145
knot2/crates/knot-types/src/net.rs
··· 1 - use std::collections::BTreeSet; 2 1 use std::net::IpAddr; 3 2 4 3 use http::{HeaderMap, HeaderName}; 5 - 6 - #[derive(Debug, Clone, Default, PartialEq, Eq)] 7 - pub struct TrustedProxies(BTreeSet<IpAddr>); 8 - 9 - impl TrustedProxies { 10 - pub fn new(addresses: impl IntoIterator<Item = IpAddr>) -> Self { 11 - Self( 12 - addresses 13 - .into_iter() 14 - .map(|peer| peer.to_canonical()) 15 - .collect(), 16 - ) 17 - } 18 - 19 - pub fn trusts(&self, peer: Option<IpAddr>) -> bool { 20 - match peer { 21 - _ if self.0.is_empty() => true, 22 - Some(peer) => self.0.contains(&peer.to_canonical()), 23 - None => false, 24 - } 25 - } 26 - } 4 + pub use trusted_proxies::{ProxyNetError, TrustedProxies, comma_separated}; 27 5 28 6 #[derive(Debug, Clone, Copy, PartialEq, Eq)] 29 7 pub enum PeerKey { ··· 63 41 } 64 42 65 43 pub fn trusts_any_peer(&self) -> bool { 66 - self.header.is_some() && self.proxies.trusts(None) 44 + self.header.is_some() && self.proxies.is_empty() 45 + } 46 + 47 + fn trusts(&self, socket: Option<IpAddr>) -> bool { 48 + match socket { 49 + _ if self.proxies.is_empty() => true, 50 + Some(socket) => self.proxies.contains(socket), 51 + None => false, 52 + } 67 53 } 68 54 69 55 pub fn peer_key(&self, headers: &HeaderMap, socket: Option<IpAddr>) -> PeerKey { ··· 90 76 fn relayed_peer(&self, headers: &HeaderMap, socket: Option<IpAddr>) -> Option<IpAddr> { 91 77 self.header 92 78 .as_ref() 93 - .filter(|_| self.proxies.trusts(socket)) 94 - .and_then(|header| forwarded_peer(headers, header)) 79 + .filter(|_| self.trusts(socket)) 80 + .and_then(|header| { 81 + self.proxies.rightmost_untrusted( 82 + headers 83 + .get_all(header) 84 + .iter() 85 + .filter_map(|value| value.to_str().ok()) 86 + .flat_map(|value| value.split(',')), 87 + ) 88 + }) 95 89 } 96 90 97 91 fn ignores_header_from(&self, headers: &HeaderMap, socket: IpAddr) -> bool { 98 92 self.header 99 93 .as_ref() 100 94 .is_some_and(|header| headers.contains_key(header)) 101 - && !self.proxies.trusts(Some(socket)) 95 + && !self.trusts(Some(socket)) 102 96 } 103 - } 104 - 105 - fn forwarded_peer(headers: &HeaderMap, header: &HeaderName) -> Option<IpAddr> { 106 - headers 107 - .get(header) 108 - .and_then(|value| value.to_str().ok()) 109 - .and_then(|value| value.rsplit(',').next()) 110 - .map(str::trim) 111 - .and_then(|candidate| candidate.parse::<IpAddr>().ok()) 112 97 } 113 98 114 99 #[cfg(test)] ··· 133 118 value.parse().unwrap() 134 119 } 135 120 121 + fn trusting<'a>(entries: impl IntoIterator<Item = &'a str>) -> TrustedProxies { 122 + TrustedProxies::parse(entries).unwrap() 123 + } 124 + 125 + fn relaying<'a>(entries: impl IntoIterator<Item = &'a str>) -> ProxyTrust { 126 + ProxyTrust::new(Some(forwarded_for()), trusting(entries)) 127 + } 128 + 136 129 #[test] 137 - fn forwarded_peer_takes_the_rightmost_parseable_entry() { 130 + fn the_knot_reads_the_rightmost_entry_from_any_peer_with_an_empty_allowlist() { 131 + let anyone = ProxyTrust::new(Some(forwarded_for()), TrustedProxies::default()); 138 132 [ 139 - (Some("203.0.113.7, 198.51.100.4"), Some("198.51.100.4")), 140 - (Some(" 192.0.2.1 "), Some("192.0.2.1")), 141 - (Some("not-an-ip"), None), 142 - (None, None), 133 + (Some("203.0.113.7, 198.51.100.4"), "198.51.100.4"), 134 + (Some(" 192.0.2.1 "), "192.0.2.1"), 135 + (Some("not-an-ip"), "192.0.2.9"), 136 + (None, "192.0.2.9"), 143 137 ] 144 138 .iter() 145 139 .for_each(|&(header, expected)| { 146 140 assert_eq!( 147 - forwarded_peer(&headers(header), &forwarded_for()), 148 - expected.map(ip), 141 + anyone.client_peer_of(&headers(header), ip("192.0.2.9")), 142 + ip(expected), 149 143 "{header:?}" 150 144 ); 151 145 }); 152 146 } 153 147 154 148 #[test] 155 - fn an_empty_allowlist_trusts_every_peer() { 156 - let anyone = TrustedProxies::default(); 157 - assert!(anyone.trusts(Some(ip("203.0.113.7")))); 158 - assert!(anyone.trusts(None)); 149 + fn the_knot_joins_every_line_of_a_repeated_header_into_one_chain() { 150 + let mut map = HeaderMap::new(); 151 + map.append(forwarded_for(), "203.0.113.7".parse().unwrap()); 152 + map.append(forwarded_for(), "198.51.100.4".parse().unwrap()); 153 + assert_eq!( 154 + relaying(["127.0.0.1"]).client_peer(&map, Some(ip("127.0.0.1"))), 155 + Some(ip("198.51.100.4")), 156 + "a proxy that appends a second header line puts the address we want in the last line" 157 + ); 159 158 } 160 159 161 160 #[test] 162 161 fn the_header_applies_only_to_a_peer_on_the_allowlist() { 163 - let proxy = ip("127.0.0.1"); 164 - let forged = headers(Some("198.51.100.4")); 165 - let trust = ProxyTrust::new(Some(forwarded_for()), TrustedProxies::new([proxy])); 166 - let peer = |socket| trust.client_peer(&forged, Some(socket)); 167 - 162 + let relayed = headers(Some("198.51.100.4")); 163 + [ 164 + (&["127.0.0.1"][..], "127.0.0.1", "198.51.100.4", 165 + "a request relayed by the listed proxy is limited by the address the proxy recorded"), 166 + (&["127.0.0.1"], "203.0.113.7", "203.0.113.7", 167 + "a client reaching the knot directly forged the header and must answer for its socket"), 168 + (&["127.0.0.1", "::1"], "::1", "198.51.100.4", 169 + "a second listed entry relays as readily as the first"), 170 + (&["127.0.0.1"], "::ffff:127.0.0.1", "198.51.100.4", 171 + "binding [::] turns an IPv4 proxy into ::ffff:127.0.0.1 and the allowlist must still match it"), 172 + (&["::ffff:127.0.0.1"], "127.0.0.1", "198.51.100.4", 173 + "an operator who writes the mapped form must match a plain IPv4 peer too"), 174 + (&["127.0.0.1"], "::1", "::1", 175 + "that peer answers for the socket it connected from, since the IPv6 loopback is a different address from the IPv4 loopback"), 176 + (&["127.0.0.1"], "::ffff:203.0.113.7", "203.0.113.7", 177 + "an ignored header still keys the peer on its socket, canonical so the warning and the bucket agree"), 178 + ] 179 + .iter() 180 + .for_each(|&(listed, socket, expected, why)| { 181 + assert_eq!( 182 + relaying(listed.iter().copied()).client_peer(&relayed, Some(ip(socket))), 183 + Some(ip(expected)), 184 + "{why}: {listed:?} saw {socket}" 185 + ); 186 + }); 168 187 assert_eq!( 169 - peer(proxy), 170 - Some(ip("198.51.100.4")), 171 - "a request relayed by the listed proxy is limited by the address the proxy recorded" 172 - ); 173 - assert_eq!( 174 - peer(ip("203.0.113.7")), 175 - Some(ip("203.0.113.7")), 176 - "a client reaching the knot directly forged the header and must answer for its socket" 188 + relaying(["127.0.0.1"]).client_peer( 189 + &headers(Some("203.0.113.7, not-an-ip")), 190 + Some(ip("127.0.0.1")) 191 + ), 192 + Some(ip("127.0.0.1")), 193 + "the knot keys on the listed proxy's own socket when it can't read past the chain" 177 194 ); 178 195 } 179 196 180 197 #[test] 181 198 fn a_caller_with_a_socket_address_gets_the_same_answer_without_an_option() { 182 - let listed = TrustedProxies::new([ip("127.0.0.1")]); 199 + let listed = trusting(["127.0.0.1"]); 183 200 [ 184 201 (ProxyTrust::default(), Some("198.51.100.4")), 185 202 ( ··· 207 224 } 208 225 209 226 #[test] 210 - fn a_listed_ipv4_proxy_still_matches_the_v4_mapped_address_a_dual_stack_listener_reports() { 211 - let mapped = ip("::ffff:127.0.0.1"); 212 - assert!( 213 - TrustedProxies::new([ip("127.0.0.1")]).trusts(Some(mapped)), 214 - "binding [::] turns an IPv4 proxy into ::ffff:127.0.0.1 and the allowlist must still match it" 215 - ); 216 - assert!( 217 - TrustedProxies::new([mapped]).trusts(Some(ip("127.0.0.1"))), 218 - "an operator who writes the mapped form must match a plain IPv4 peer too" 219 - ); 220 - assert!( 221 - !TrustedProxies::new([ip("127.0.0.1")]).trusts(Some(ip("::1"))), 222 - "the IPv6 loopback is a different address from the IPv4 one" 223 - ); 224 - } 225 - 226 - #[test] 227 - fn client_peer_falls_back_to_the_socket_whenever_no_header_applies() { 227 + fn client_peer_falls_back_to_the_socket_whenever_the_header_doesnt_apply() { 228 228 let socket = ip("203.0.113.7"); 229 229 [ 230 230 (None, Some("198.51.100.4")), ··· 239 239 Some(socket), 240 240 "{header_name:?} with {header_value:?}" 241 241 ); 242 + assert_eq!( 243 + trust.client_peer(&headers(header_value), Some(ip("::ffff:203.0.113.7"))), 244 + Some(socket), 245 + "a v4-mapped socket and the plain v4 address are one client, so they share a key" 246 + ); 242 247 }); 243 248 } 244 249 245 250 #[test] 246 - fn one_address_gets_one_bucket_however_the_listener_spelled_it() { 247 - let trust = ProxyTrust::default(); 248 - assert_eq!( 249 - trust.client_peer(&headers(None), Some(ip("::ffff:203.0.113.7"))), 250 - trust.client_peer(&headers(None), Some(ip("203.0.113.7"))), 251 - "a v4-mapped socket and the plain v4 address are one client, so they share a key" 252 - ); 253 - } 254 - 255 - #[test] 256 - fn client_peer_reports_no_peer_when_an_allowlist_leaves_it_with_neither_source() { 257 - let trust = ProxyTrust::new( 258 - Some(forwarded_for()), 259 - TrustedProxies::new([ip("127.0.0.1")]), 260 - ); 261 - assert_eq!( 262 - trust.client_peer(&headers(Some("198.51.100.4")), None), 263 - None, 264 - "with no socket to check against the allowlist there is no client to key on" 265 - ); 266 - } 267 - 268 - #[test] 269 251 fn the_peer_key_separates_an_ignored_header_from_a_request_that_never_sent_one() { 270 - let listed = ProxyTrust::new( 271 - Some(forwarded_for()), 272 - TrustedProxies::new([ip("127.0.0.1")]), 273 - ); 252 + let listed = ProxyTrust::new(Some(forwarded_for()), trusting(["127.0.0.1"])); 274 253 assert_eq!( 275 254 listed.peer_key(&headers(Some("198.51.100.4")), Some(ip("203.0.113.7"))), 276 255 PeerKey::SocketWithIgnoredHeader(ip("203.0.113.7")), 277 - "an unlisted peer sent the header, which is the address an operator has to see" 256 + "an operator has to see the address of an unlisted peer that sent the header" 278 257 ); 279 258 assert_eq!( 280 259 listed.peer_key(&headers(None), Some(ip("203.0.113.7"))), 281 260 PeerKey::Socket(ip("203.0.113.7")), 282 - "a request without the header says nothing about the allowlist" 261 + "the allowlist stays untested when a request arrives without the header" 283 262 ); 284 263 assert_eq!( 285 264 listed.peer_key(&headers(Some("198.51.100.4")), Some(ip("127.0.0.1"))), ··· 290 269 listed.peer_key(&headers(Some("198.51.100.4")), None), 291 270 PeerKey::Unidentified 292 271 ); 272 + assert_eq!( 273 + listed.client_peer(&headers(Some("198.51.100.4")), None), 274 + None, 275 + "the knot won't key on a client until it has a socket to check against the allowlist" 276 + ); 293 277 } 294 278 295 279 #[test] 296 - fn only_an_ignored_header_reports_an_address_to_warn_about() { 280 + fn only_an_ignored_header_has_an_address_to_warn_about() { 297 281 assert_eq!( 298 282 PeerKey::SocketWithIgnoredHeader(ip("203.0.113.7")).ignored_header(), 299 283 Some(ip("203.0.113.7")) ··· 308 292 assert_eq!( 309 293 key.ignored_header(), 310 294 None, 311 - "{key:?} is not a misconfigured allowlist" 295 + "{key:?} isn't a misconfigured allowlist" 312 296 ); 313 297 }); 314 298 } 315 299 316 300 #[test] 317 - fn an_ignored_header_still_keys_the_peer_on_its_socket() { 318 - let listed = ProxyTrust::new( 319 - Some(forwarded_for()), 320 - TrustedProxies::new([ip("127.0.0.1")]), 321 - ); 322 - let forged = headers(Some("198.51.100.4")); 323 - assert_eq!( 324 - listed.client_peer(&forged, Some(ip("203.0.113.7"))), 325 - Some(ip("203.0.113.7")) 326 - ); 327 - assert_eq!( 328 - listed.client_peer_of(&forged, ip("::ffff:203.0.113.7")), 329 - ip("203.0.113.7"), 330 - "the reported address stays canonical so the warning and the bucket agree" 331 - ); 332 - } 333 - 334 - #[test] 335 - fn a_populated_allowlist_trusts_only_the_addresses_it_lists() { 336 - let proxies = TrustedProxies::new([ip("127.0.0.1"), ip("::1")]); 337 - assert!(proxies.trusts(Some(ip("127.0.0.1")))); 338 - assert!(proxies.trusts(Some(ip("::1")))); 339 - assert!( 340 - !proxies.trusts(Some(ip("203.0.113.7"))), 341 - "a client reaching the knot directly would pick its own rate-limit bucket" 342 - ); 343 - assert!( 344 - !proxies.trusts(None), 345 - "a peer of None has no address to match against the list" 346 - ); 347 - } 348 - 349 - #[test] 350 - fn only_a_header_without_an_allowlist_trusts_any_peer() { 351 - let listed = TrustedProxies::new([ip("127.0.0.1")]); 301 + fn only_a_header_without_an_allowlist_makes_the_knot_trust_any_peer() { 302 + let listed = trusting(["127.0.0.1"]); 352 303 assert!( 353 304 ProxyTrust::new(Some(forwarded_for()), TrustedProxies::default()).trusts_any_peer() 354 305 );
+21 -11
knot2/example.toml
··· 264 264 #fork_fetch_timeout_ms = 600000 265 265 266 266 # When the knot runs behind a trusted reverse proxy that terminates TLS, 267 - # set this to the header the proxy appends the client address to, for 268 - # example x-forwarded-for. The rightmost entry is used. Leave unset when 269 - # the knot is directly exposed so the socket peer address is used. Only set 270 - # this when a trusted proxy overwrites or appends the header, since a client 271 - # can forge it otherwise. 267 + # set this to the header the proxy appends the client address to, 268 + # for example x-forwarded-for. 269 + # The knot will read the chain right -> left 270 + # and take the first entry that `trusted_proxies` doesn't cover. 271 + # Leave unset when the knot is directly exposed so the socket peer address is used. 272 + # Only set this when a trusted proxy overwrites or appends the header, 273 + # since a client can forge it otherwise. 272 274 # 273 275 # Can also be specified via environment variable `KNOT_XRPC_TRUSTED_PROXY_HEADER`. 274 276 #trusted_proxy_header = 275 277 276 - # IP addresses whose `trusted_proxy_header` the knot honors, 277 - # without a port, 278 + # Addresses whose `trusted_proxy_header` the knot honors, 279 + # each a bare IP without a port or a CIDR block such as 173.245.48.0/20, 278 280 # for ex the loopback address of a reverse proxy on the same host. 279 - # The knot rate-limits a request from any other address 280 - # by its own socket address and ignores the header. 281 - # Leave empty to honor the header from every peer, 282 - # which is safe *only* if nothing but the proxy can reach this knot. 281 + # The knot will rate-limit a request from any other address 282 + # by its own socket address and ignore the header. 283 + # These same addresses are hops the knot will iterate over when it reads 284 + # the header, so list every proxy you control in the path. 285 + # A proxy that the knot doesn't know about becomes the entry it keys on, 286 + # and everyone that proxy serves will then share one rate-limit bucket. 287 + # The knot will read the last 32 entries of the chain, at most. 288 + # When the list covers all 32, the knot 289 + # will rate-limit by the address the request connected from. 290 + # Leave empty to honor the header from every peer and take its rightmost 291 + # entry, which is safe *only* while every route to this knot passes 292 + # through the proxy. 283 293 # 284 294 # Can also be specified via environment variable `KNOT_XRPC_TRUSTED_PROXIES`. 285 295 #