1use std::net::{Ipv4Addr, SocketAddrV4};
2use std::time::Duration;
3
4use base64::Engine as _;
5use base64::engine::general_purpose::URL_SAFE_NO_PAD;
6use ring::rand::{SecureRandom, SystemRandom};
7use thiserror::Error;
8
9use crate::LocalCertificateAuthority;
10use crate::host::HostScope;
11
12const SESSION_ID_RANDOM_BYTES: usize = 16;
13
14#[derive(Clone, Eq, Hash, PartialEq)]
15pub struct ProxySessionId {
16 bytes: [u8; SESSION_ID_RANDOM_BYTES],
17 token: String,
18}
19
20impl ProxySessionId {
21 pub fn generate() -> Result<Self, SessionIdGenerationError> {
22 let mut bytes = [0_u8; SESSION_ID_RANDOM_BYTES];
23 SystemRandom::new()
24 .fill(&mut bytes)
25 .map_err(|_| SessionIdGenerationError)?;
26 Self::from_bytes(bytes)
27 }
28
29 pub fn from_bytes(
32 bytes: [u8; SESSION_ID_RANDOM_BYTES],
33 ) -> Result<Self, SessionIdGenerationError> {
34 if bytes == [0; SESSION_ID_RANDOM_BYTES] {
35 return Err(SessionIdGenerationError);
36 }
37 Ok(Self {
38 token: URL_SAFE_NO_PAD.encode(bytes),
39 bytes,
40 })
41 }
42
43 pub const fn as_bytes(&self) -> &[u8; SESSION_ID_RANDOM_BYTES] {
45 &self.bytes
46 }
47
48 pub fn as_str(&self) -> &str {
50 &self.token
51 }
52}
53
54impl std::fmt::Debug for ProxySessionId {
55 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56 formatter.write_str("ProxySessionId([REDACTED])")
57 }
58}
59
60#[derive(Clone, Debug, Eq, Hash, PartialEq)]
61pub struct ProxyInstanceId {
62 session: ProxySessionId,
63 generation: u64,
64}
65
66impl ProxyInstanceId {
67 pub fn new(session: ProxySessionId, generation: u64) -> Self {
68 Self {
69 session,
70 generation,
71 }
72 }
73
74 pub fn session(&self) -> &ProxySessionId {
75 &self.session
76 }
77
78 pub fn generation(&self) -> u64 {
79 self.generation
80 }
81}
82
83#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
84#[non_exhaustive]
85pub enum LoopbackBind {
86 #[default]
87 Ipv4,
88}
89
90#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
97pub enum ProxyRoutingMode {
98 #[default]
99 ScopedHns,
100 DaneBrowser,
101}
102
103impl LoopbackBind {
104 pub const fn socket_addr(self) -> SocketAddrV4 {
105 match self {
106 Self::Ipv4 => SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0),
107 }
108 }
109}
110
111#[derive(Clone, Copy, Debug, Eq, PartialEq)]
112pub struct ProxyLimits {
113 max_header_bytes: usize,
114 max_body_bytes: u64,
115 max_active_clients: usize,
116 max_requests_per_window: usize,
117 max_requests_per_host_per_window: usize,
118 max_tracked_hosts: usize,
119 rate_window: Duration,
120}
121
122impl ProxyLimits {
123 pub const DEFAULT_MAX_HEADER_BYTES: usize = 64 * 1024;
124 pub const DEFAULT_MAX_BODY_BYTES: u64 = 1024 * 1024;
125 pub const DEFAULT_MAX_ACTIVE_CLIENTS: usize = 128;
126 pub const DEFAULT_MAX_REQUESTS_PER_WINDOW: usize = 2_048;
127 pub const DEFAULT_MAX_REQUESTS_PER_HOST_PER_WINDOW: usize = 1_024;
128 pub const DEFAULT_MAX_TRACKED_HOSTS: usize = 256;
129 pub const DEFAULT_RATE_WINDOW: Duration = Duration::from_secs(10);
130
131 #[allow(clippy::too_many_arguments)]
132 pub fn new(
133 max_header_bytes: usize,
134 max_body_bytes: u64,
135 max_active_clients: usize,
136 max_requests_per_window: usize,
137 max_requests_per_host_per_window: usize,
138 max_tracked_hosts: usize,
139 rate_window: Duration,
140 ) -> Result<Self, ProxyLimitsError> {
141 require_nonzero(max_header_bytes, "max_header_bytes")?;
142 require_at_most(
143 max_header_bytes,
144 Self::DEFAULT_MAX_HEADER_BYTES,
145 "max_header_bytes",
146 )?;
147 if max_body_bytes == 0 {
148 return Err(ProxyLimitsError::Zero("max_body_bytes"));
149 }
150 if max_body_bytes > Self::DEFAULT_MAX_BODY_BYTES {
151 return Err(ProxyLimitsError::ExceedsMaximum("max_body_bytes"));
152 }
153 require_nonzero(max_active_clients, "max_active_clients")?;
154 require_at_most(
155 max_active_clients,
156 Self::DEFAULT_MAX_ACTIVE_CLIENTS,
157 "max_active_clients",
158 )?;
159 require_nonzero(max_requests_per_window, "max_requests_per_window")?;
160 require_at_most(
161 max_requests_per_window,
162 Self::DEFAULT_MAX_REQUESTS_PER_WINDOW,
163 "max_requests_per_window",
164 )?;
165 require_nonzero(
166 max_requests_per_host_per_window,
167 "max_requests_per_host_per_window",
168 )?;
169 require_at_most(
170 max_requests_per_host_per_window,
171 Self::DEFAULT_MAX_REQUESTS_PER_HOST_PER_WINDOW,
172 "max_requests_per_host_per_window",
173 )?;
174 require_nonzero(max_tracked_hosts, "max_tracked_hosts")?;
175 require_at_most(
176 max_tracked_hosts,
177 Self::DEFAULT_MAX_TRACKED_HOSTS,
178 "max_tracked_hosts",
179 )?;
180 if rate_window.is_zero() {
181 return Err(ProxyLimitsError::Zero("rate_window"));
182 }
183 if rate_window > Self::DEFAULT_RATE_WINDOW {
184 return Err(ProxyLimitsError::ExceedsMaximum("rate_window"));
185 }
186 if max_requests_per_host_per_window > max_requests_per_window {
187 return Err(ProxyLimitsError::PerHostExceedsGlobal);
188 }
189
190 Ok(Self {
191 max_header_bytes,
192 max_body_bytes,
193 max_active_clients,
194 max_requests_per_window,
195 max_requests_per_host_per_window,
196 max_tracked_hosts,
197 rate_window,
198 })
199 }
200
201 pub const fn max_header_bytes(self) -> usize {
202 self.max_header_bytes
203 }
204
205 pub const fn max_body_bytes(self) -> u64 {
206 self.max_body_bytes
207 }
208
209 pub const fn max_active_clients(self) -> usize {
210 self.max_active_clients
211 }
212
213 pub const fn max_requests_per_window(self) -> usize {
214 self.max_requests_per_window
215 }
216
217 pub const fn max_requests_per_host_per_window(self) -> usize {
218 self.max_requests_per_host_per_window
219 }
220
221 pub const fn max_tracked_hosts(self) -> usize {
222 self.max_tracked_hosts
223 }
224
225 pub const fn rate_window(self) -> Duration {
226 self.rate_window
227 }
228}
229
230impl Default for ProxyLimits {
231 fn default() -> Self {
232 Self {
233 max_header_bytes: Self::DEFAULT_MAX_HEADER_BYTES,
234 max_body_bytes: Self::DEFAULT_MAX_BODY_BYTES,
235 max_active_clients: Self::DEFAULT_MAX_ACTIVE_CLIENTS,
236 max_requests_per_window: Self::DEFAULT_MAX_REQUESTS_PER_WINDOW,
237 max_requests_per_host_per_window: Self::DEFAULT_MAX_REQUESTS_PER_HOST_PER_WINDOW,
238 max_tracked_hosts: Self::DEFAULT_MAX_TRACKED_HOSTS,
239 rate_window: Self::DEFAULT_RATE_WINDOW,
240 }
241 }
242}
243
244#[derive(Clone, Copy, Debug, Eq, PartialEq)]
245pub struct ProxyTimeouts {
246 request_header_timeout: Duration,
247 socket_timeout: Duration,
248}
249
250impl ProxyTimeouts {
251 pub const DEFAULT_REQUEST_HEADER_TIMEOUT: Duration = Duration::from_secs(5);
252 pub const DEFAULT_SOCKET_TIMEOUT: Duration = Duration::from_secs(30);
253
254 pub fn new(
255 request_header_timeout: Duration,
256 socket_timeout: Duration,
257 ) -> Result<Self, ProxyTimeoutsError> {
258 if request_header_timeout.is_zero() {
259 return Err(ProxyTimeoutsError::Zero("request_header_timeout"));
260 }
261 if socket_timeout.is_zero() {
262 return Err(ProxyTimeoutsError::Zero("socket_timeout"));
263 }
264 if request_header_timeout > socket_timeout {
265 return Err(ProxyTimeoutsError::HeaderExceedsSocket);
266 }
267 if request_header_timeout > Self::DEFAULT_REQUEST_HEADER_TIMEOUT {
268 return Err(ProxyTimeoutsError::ExceedsMaximum("request_header_timeout"));
269 }
270 if socket_timeout > Self::DEFAULT_SOCKET_TIMEOUT {
271 return Err(ProxyTimeoutsError::ExceedsMaximum("socket_timeout"));
272 }
273 Ok(Self {
274 request_header_timeout,
275 socket_timeout,
276 })
277 }
278
279 pub const fn request_header_timeout(self) -> Duration {
280 self.request_header_timeout
281 }
282
283 pub const fn socket_timeout(self) -> Duration {
284 self.socket_timeout
285 }
286}
287
288impl Default for ProxyTimeouts {
289 fn default() -> Self {
290 Self {
291 request_header_timeout: Self::DEFAULT_REQUEST_HEADER_TIMEOUT,
292 socket_timeout: Self::DEFAULT_SOCKET_TIMEOUT,
293 }
294 }
295}
296
297#[derive(Clone, Debug, Eq, PartialEq)]
302pub struct ProxyConfig {
303 instance: ProxyInstanceId,
304 hns_scope: Option<HostScope>,
305 local_certificate_authority: Option<LocalCertificateAuthority>,
306 limits: ProxyLimits,
307 timeouts: ProxyTimeouts,
308 bind: LoopbackBind,
309 routing_mode: ProxyRoutingMode,
310}
311
312impl ProxyConfig {
313 pub fn new(instance: ProxyInstanceId, scope: HostScope) -> Self {
314 Self {
315 instance,
316 hns_scope: Some(scope),
317 local_certificate_authority: None,
318 limits: ProxyLimits::default(),
319 timeouts: ProxyTimeouts::default(),
320 bind: LoopbackBind::default(),
321 routing_mode: ProxyRoutingMode::default(),
322 }
323 }
324
325 pub fn dane_browser(instance: ProxyInstanceId) -> Self {
329 Self {
330 instance,
331 hns_scope: None,
332 local_certificate_authority: None,
333 limits: ProxyLimits::default(),
334 timeouts: ProxyTimeouts::default(),
335 bind: LoopbackBind::default(),
336 routing_mode: ProxyRoutingMode::DaneBrowser,
337 }
338 }
339
340 pub fn with_controls(
341 instance: ProxyInstanceId,
342 scope: HostScope,
343 limits: ProxyLimits,
344 timeouts: ProxyTimeouts,
345 bind: LoopbackBind,
346 ) -> Self {
347 Self {
348 instance,
349 hns_scope: Some(scope),
350 local_certificate_authority: None,
351 limits,
352 timeouts,
353 bind,
354 routing_mode: ProxyRoutingMode::default(),
355 }
356 }
357
358 pub fn instance(&self) -> &ProxyInstanceId {
359 &self.instance
360 }
361
362 pub fn with_local_certificate_authority(
363 mut self,
364 certificate_authority: LocalCertificateAuthority,
365 ) -> Self {
366 self.local_certificate_authority = Some(certificate_authority);
367 self
368 }
369
370 pub fn local_certificate_authority(&self) -> Option<&LocalCertificateAuthority> {
371 self.local_certificate_authority.as_ref()
372 }
373
374 pub fn hns_scope(&self) -> Option<&HostScope> {
375 self.hns_scope.as_ref()
376 }
377
378 pub const fn limits(&self) -> ProxyLimits {
379 self.limits
380 }
381
382 pub const fn timeouts(&self) -> ProxyTimeouts {
383 self.timeouts
384 }
385
386 pub const fn bind(&self) -> LoopbackBind {
387 self.bind
388 }
389
390 pub const fn routing_mode(&self) -> ProxyRoutingMode {
391 self.routing_mode
392 }
393}
394
395#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
396#[error("the operating-system random number generator failed")]
397pub struct SessionIdGenerationError;
398
399#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
400pub enum ProxyLimitsError {
401 #[error("{0} must be nonzero")]
402 Zero(&'static str),
403 #[error("{0} exceeds the hard proxy maximum")]
404 ExceedsMaximum(&'static str),
405 #[error("the per-host request limit cannot exceed the global request limit")]
406 PerHostExceedsGlobal,
407}
408
409#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
410pub enum ProxyTimeoutsError {
411 #[error("{0} must be nonzero")]
412 Zero(&'static str),
413 #[error("{0} exceeds the hard proxy maximum")]
414 ExceedsMaximum(&'static str),
415 #[error("the request-header timeout cannot exceed the socket timeout")]
416 HeaderExceedsSocket,
417}
418
419fn require_nonzero(value: usize, name: &'static str) -> Result<(), ProxyLimitsError> {
420 if value == 0 {
421 Err(ProxyLimitsError::Zero(name))
422 } else {
423 Ok(())
424 }
425}
426
427fn require_at_most(
428 value: usize,
429 maximum: usize,
430 name: &'static str,
431) -> Result<(), ProxyLimitsError> {
432 if value > maximum {
433 Err(ProxyLimitsError::ExceedsMaximum(name))
434 } else {
435 Ok(())
436 }
437}
438
439#[cfg(test)]
440mod tests {
441 use super::*;
442
443 #[test]
444 fn session_ids_are_fresh_and_debug_redacted() {
445 let first = ProxySessionId::generate().unwrap();
446 let second = ProxySessionId::generate().unwrap();
447
448 assert_ne!(first, second);
449 assert_ne!(first.as_bytes(), &[0; SESSION_ID_RANDOM_BYTES]);
450 assert!(!first.as_str().is_empty());
451 assert!(!format!("{first:?}").contains(first.as_str()));
452 }
453
454 #[test]
455 fn checked_session_bytes_retain_the_frozen_base64url_token() {
456 let bytes = [0xabu8; SESSION_ID_RANDOM_BYTES];
457 let session = ProxySessionId::from_bytes(bytes).unwrap();
458
459 assert_eq!(session.as_bytes(), &bytes);
460 assert_eq!(session.as_str(), "q6urq6urq6urq6urq6urqw");
461 assert_eq!(
462 ProxySessionId::from_bytes([0; SESSION_ID_RANDOM_BYTES]),
463 Err(SessionIdGenerationError)
464 );
465 assert!(!format!("{session:?}").contains(session.as_str()));
466 }
467
468 #[test]
469 fn instance_debug_preserves_generation_but_redacts_session() {
470 let instance = ProxyInstanceId::new(ProxySessionId::generate().unwrap(), 7);
471 let debug = format!("{instance:?}");
472
473 assert!(debug.contains("generation: 7"));
474 assert!(!debug.contains(instance.session().as_str()));
475 }
476
477 #[test]
478 fn bind_is_always_ipv4_loopback_and_allows_ephemeral_port() {
479 assert_eq!(
480 LoopbackBind::Ipv4.socket_addr(),
481 SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0)
482 );
483 }
484
485 #[test]
486 fn default_limits_match_the_android_parity_envelope() {
487 let limits = ProxyLimits::default();
488
489 assert_eq!(limits.max_header_bytes(), 64 * 1024);
490 assert_eq!(limits.max_body_bytes(), 1024 * 1024);
491 assert_eq!(limits.max_active_clients(), 128);
492 assert_eq!(limits.max_requests_per_window(), 2_048);
493 assert_eq!(limits.max_requests_per_host_per_window(), 1_024);
494 assert_eq!(limits.max_tracked_hosts(), 256);
495 assert_eq!(limits.rate_window(), Duration::from_secs(10));
496 }
497
498 #[test]
499 fn custom_limits_reject_zero_and_incoherent_values() {
500 assert_eq!(
501 ProxyLimits::new(0, 1, 1, 1, 1, 1, Duration::from_secs(1)),
502 Err(ProxyLimitsError::Zero("max_header_bytes"))
503 );
504 assert_eq!(
505 ProxyLimits::new(1, 1, 1, 3, 4, 1, Duration::from_secs(1)),
506 Err(ProxyLimitsError::PerHostExceedsGlobal)
507 );
508 assert_eq!(
509 ProxyLimits::new(1, 1, 1, 1, 1, 1, Duration::ZERO),
510 Err(ProxyLimitsError::Zero("rate_window"))
511 );
512 assert_eq!(
513 ProxyLimits::new(
514 ProxyLimits::DEFAULT_MAX_HEADER_BYTES + 1,
515 1,
516 1,
517 1,
518 1,
519 1,
520 Duration::from_secs(1),
521 ),
522 Err(ProxyLimitsError::ExceedsMaximum("max_header_bytes"))
523 );
524 }
525
526 #[test]
527 fn timeouts_match_parity_defaults_and_reject_invalid_ordering() {
528 let timeouts = ProxyTimeouts::default();
529 assert_eq!(timeouts.request_header_timeout(), Duration::from_secs(5));
530 assert_eq!(timeouts.socket_timeout(), Duration::from_secs(30));
531 assert_eq!(
532 ProxyTimeouts::new(Duration::from_secs(31), Duration::from_secs(30)),
533 Err(ProxyTimeoutsError::HeaderExceedsSocket)
534 );
535 assert_eq!(
536 ProxyTimeouts::new(Duration::from_secs(5), Duration::from_secs(31)),
537 Err(ProxyTimeoutsError::ExceedsMaximum("socket_timeout"))
538 );
539 }
540
541 #[test]
542 fn proxy_config_is_immutable_and_contains_no_authentication_bypass() {
543 let instance = ProxyInstanceId::new(ProxySessionId::generate().unwrap(), 3);
544 let scope = HostScope::new("welcome").unwrap();
545 let config = ProxyConfig::new(instance.clone(), scope.clone());
546
547 assert_eq!(config.instance(), &instance);
548 assert_eq!(config.hns_scope(), Some(&scope));
549 assert!(config.local_certificate_authority().is_none());
550 assert_eq!(config.routing_mode(), ProxyRoutingMode::ScopedHns);
551 assert_eq!(config.bind(), LoopbackBind::Ipv4);
552 assert_eq!(config.limits(), ProxyLimits::default());
553 assert_eq!(config.timeouts(), ProxyTimeouts::default());
554 assert!(!format!("{config:?}").contains(scope.root().as_str()));
555
556 let dane_browser = ProxyConfig::dane_browser(instance);
557 assert_eq!(dane_browser.hns_scope(), None);
558 assert_eq!(dane_browser.routing_mode(), ProxyRoutingMode::DaneBrowser);
559 }
560}