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