···193193194194Set `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.
195195196196-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.
196196+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.
197197198198The 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:
199199
···11use std::fmt;
22-use std::net::{AddrParseError, IpAddr, SocketAddr};
22+use std::net::SocketAddr;
33use std::path::{Path, PathBuf};
44use std::sync::OnceLock;
55use std::time::Duration;
···77use base64::Engine;
88use confique::Config;
99use knot_runtime::HttpLimits;
1010-use knot_types::{AccountDid, AdmissionPolicy, AppviewEndpoint};
1010+use knot_types::{
1111+ AccountDid, AdmissionPolicy, AppviewEndpoint, ProxyNetError, TrustedProxies, comma_separated,
1212+};
1113use url::Url;
12141315#[derive(Debug, Config)]
···300302 pub fork_fetch_timeout_ms: u64,
301303302304 /// When the knot runs behind a trusted reverse proxy that terminates TLS,
303303- /// set this to the header the proxy appends the client address to, for
304304- /// example x-forwarded-for. The rightmost entry is used. Leave unset when
305305- /// the knot is directly exposed so the socket peer address is used. Only set
306306- /// this when a trusted proxy overwrites or appends the header, since a client
307307- /// can forge it otherwise.
305305+ /// set this to the header the proxy appends the client address to,
306306+ /// for example x-forwarded-for.
307307+ /// The knot will read the chain right -> left
308308+ /// and take the first entry that `trusted_proxies` doesn't cover.
309309+ /// Leave unset when the knot is directly exposed so the socket peer address is used.
310310+ /// Only set this when a trusted proxy overwrites or appends the header,
311311+ /// since a client can forge it otherwise.
308312 #[config(env = "KNOT_XRPC_TRUSTED_PROXY_HEADER")]
309313 pub trusted_proxy_header: Option<String>,
310314311311- /// IP addresses whose `trusted_proxy_header` the knot honors,
312312- /// without a port,
315315+ /// Addresses whose `trusted_proxy_header` the knot honors,
316316+ /// each a bare IP without a port or a CIDR block such as 173.245.48.0/20,
313317 /// for ex the loopback address of a reverse proxy on the same host.
314314- /// The knot rate-limits a request from any other address
315315- /// by its own socket address and ignores the header.
316316- /// Leave empty to honor the header from every peer,
317317- /// which is safe *only* if nothing but the proxy can reach this knot.
318318+ /// The knot will rate-limit a request from any other address
319319+ /// by its own socket address and ignore the header.
320320+ /// These same addresses are hops the knot will iterate over when it reads
321321+ /// the header, so list every proxy you control in the path.
322322+ /// A proxy that the knot doesn't know about becomes the entry it keys on,
323323+ /// and everyone that proxy serves will then share one rate-limit bucket.
324324+ /// The knot will read the last 32 entries of the chain, at most.
325325+ /// When the list covers all 32, the knot
326326+ /// will rate-limit by the address the request connected from.
327327+ /// Leave empty to honor the header from every peer and take its rightmost
328328+ /// entry, which is safe *only* while every route to this knot passes
329329+ /// through the proxy.
318330 #[config(
319331 env = "KNOT_XRPC_TRUSTED_PROXIES",
320320- parse_env = parse_trusted_proxies,
332332+ parse_env = comma_separated,
321333 default = []
322334 )]
323323- pub trusted_proxies: Vec<IpAddr>,
335335+ pub trusted_proxies: Vec<String>,
324336325337 #[config(env = "KNOT_XRPC_EVENTS_REPLAY_BUFFER", default = 4096)]
326338 pub events_replay_buffer: u32,
···439451 .collect()
440452}
441453442442-fn parse_trusted_proxies(raw: &str) -> Result<Vec<IpAddr>, AddrParseError> {
443443- raw.split(',')
444444- .map(str::trim)
445445- .filter(|item| !item.is_empty())
446446- .map(str::parse)
447447- .collect()
448448-}
449449-450454impl KnotConfig {
451455 pub fn object_format(&self) -> Option<knot_types::ObjectFormat> {
452456 knot_types::ObjectFormat::from_capability(&self.git.object_format)
453457 }
454458459459+ pub fn trusted_proxies(&self) -> Result<TrustedProxies, ProxyNetError> {
460460+ TrustedProxies::parse(self.xrpc.trusted_proxies.iter().map(String::as_str))
461461+ }
462462+455463 pub fn tls_enabled(&self) -> bool {
456464 self.static_cert_enabled() || self.tls.acme_enabled
457465 }
···834842 self.xrpc.trusted_proxy_header.is_some() || self.xrpc.trusted_proxies.is_empty(),
835843 "xrpc.trusted_proxies needs xrpc.trusted_proxy_header, the header the knot honors from those addresses",
836844 ),
845845+ self.trusted_proxies()
846846+ .err()
847847+ .map(|error| format!("xrpc.trusted_proxies: {error}")),
837848 self.acl
838849 .legacy_admin_secret_env
839850 .as_deref()
···15761587 ),
15771588 (
15781589 "trusted_proxies_without_the_header_the_knot_honors",
15791579- |config| config.xrpc.trusted_proxies = vec!["127.0.0.1".parse().unwrap()],
15901590+ |config| config.xrpc.trusted_proxies = vec!["127.0.0.1".to_owned()],
15801591 "needs xrpc.trusted_proxy_header",
15811592 ),
15821593 ];
···16481659 }
1649166016501661 #[test]
16511651- fn trusted_proxies_parse_from_comma_separated_env() {
16521652- assert_eq!(
16531653- parse_trusted_proxies("127.0.0.1, ::1").unwrap(),
16541654- vec![
16551655- "127.0.0.1".parse::<IpAddr>().unwrap(),
16561656- "::1".parse::<IpAddr>().unwrap()
16571657- ]
16581658- );
16591659- assert!(parse_trusted_proxies("").unwrap().is_empty());
16601660- assert!(
16611661- parse_trusted_proxies("127.0.0.1:5555").is_err(),
16621662- "xrpc.trusted_proxies takes bare IP addresses, so a port must fail to parse"
16631663- );
16621662+ fn a_trusted_proxy_entry_takes_an_address_or_a_cidr_block() {
16631663+ let listing = |entries: &[&str]| {
16641664+ let mut config = sample();
16651665+ config.xrpc.trusted_proxy_header = Some("x-forwarded-for".to_owned());
16661666+ config.xrpc.trusted_proxies = entries.iter().map(|&e| e.to_owned()).collect();
16671667+ config
16681668+ };
16691669+ let config = listing(&["127.0.0.1", "173.245.48.0/20", "2400:cb00::/32"]);
16701670+ assert!(config.validate().is_ok());
16711671+ let proxies = config.trusted_proxies().unwrap();
16721672+ assert!(proxies.contains("173.245.48.7".parse().unwrap()));
16731673+ assert!(proxies.contains("2400:cb00::1".parse().unwrap()));
16741674+16751675+ [
16761676+ (
16771677+ "127.0.0.1:5555",
16781678+ "127.0.0.1:5555",
16791679+ "xrpc.trusted_proxies takes a bare address or a CIDR block, so the failure must quote the rejected entry",
16801680+ ),
16811681+ (
16821682+ " ",
16831683+ "blank entry",
16841684+ "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",
16851685+ ),
16861686+ ]
16871687+ .iter()
16881688+ .for_each(|&(entry, quoted, why)| {
16891689+ let report = listing(&[entry]).validate().unwrap_err().to_string();
16901690+ assert!(report.contains(quoted), "{why}: {report}");
16911691+ });
16641692 }
1665169316661694 #[test]
···2020pub use hex::{decode_hex, lowercase_hex};
21212222mod net;
2323-pub use net::{PeerKey, ProxyTrust, TrustedProxies};
2323+pub use net::{PeerKey, ProxyNetError, ProxyTrust, TrustedProxies, comma_separated};
24242525pub use jacquard_common::CowStr;
2626pub use jacquard_common::DefaultStr;
···264264#fork_fetch_timeout_ms = 600000
265265266266# When the knot runs behind a trusted reverse proxy that terminates TLS,
267267-# set this to the header the proxy appends the client address to, for
268268-# example x-forwarded-for. The rightmost entry is used. Leave unset when
269269-# the knot is directly exposed so the socket peer address is used. Only set
270270-# this when a trusted proxy overwrites or appends the header, since a client
271271-# can forge it otherwise.
267267+# set this to the header the proxy appends the client address to,
268268+# for example x-forwarded-for.
269269+# The knot will read the chain right -> left
270270+# and take the first entry that `trusted_proxies` doesn't cover.
271271+# Leave unset when the knot is directly exposed so the socket peer address is used.
272272+# Only set this when a trusted proxy overwrites or appends the header,
273273+# since a client can forge it otherwise.
272274#
273275# Can also be specified via environment variable `KNOT_XRPC_TRUSTED_PROXY_HEADER`.
274276#trusted_proxy_header =
275277276276-# IP addresses whose `trusted_proxy_header` the knot honors,
277277-# without a port,
278278+# Addresses whose `trusted_proxy_header` the knot honors,
279279+# each a bare IP without a port or a CIDR block such as 173.245.48.0/20,
278280# for ex the loopback address of a reverse proxy on the same host.
279279-# The knot rate-limits a request from any other address
280280-# by its own socket address and ignores the header.
281281-# Leave empty to honor the header from every peer,
282282-# which is safe *only* if nothing but the proxy can reach this knot.
281281+# The knot will rate-limit a request from any other address
282282+# by its own socket address and ignore the header.
283283+# These same addresses are hops the knot will iterate over when it reads
284284+# the header, so list every proxy you control in the path.
285285+# A proxy that the knot doesn't know about becomes the entry it keys on,
286286+# and everyone that proxy serves will then share one rate-limit bucket.
287287+# The knot will read the last 32 entries of the chain, at most.
288288+# When the list covers all 32, the knot
289289+# will rate-limit by the address the request connected from.
290290+# Leave empty to honor the header from every peer and take its rightmost
291291+# entry, which is safe *only* while every route to this knot passes
292292+# through the proxy.
283293#
284294# Can also be specified via environment variable `KNOT_XRPC_TRUSTED_PROXIES`.
285295#