Skip to main content

hns_browser_loopback_proxy/chromium/
config.rs

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    /// Constructs one checked session while preserving the existing
30    /// URL-safe, unpadded platform token representation.
31    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    /// Exact nonzero bytes shared with the canonical browser runtime.
44    pub const fn as_bytes(&self) -> &[u8; SESSION_ID_RANDOM_BYTES] {
45        &self.bytes
46    }
47
48    /// Explicitly exposes the opaque token for platform boundary serialization.
49    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/// Determines which browser requests one proxy generation may route.
91///
92/// `DaneBrowser` is the Chromium release mode: every canonical DNS host
93/// reaches the same Rust request boundary for dual-root resolution,
94/// DNSSEC/TLSA discovery, and origin trust selection. `ScopedHns` remains as a
95/// narrow internal substrate for host-scope unit and integration coverage.
96#[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/// Immutable configuration for one proxy generation.
298///
299/// Authentication is intentionally absent: `RunningProxy` must generate a mandatory fresh
300/// [`crate::auth::ProxyAuthorization`] when it starts.
301#[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    /// Creates a Chromium-style generation which admits canonical HNS and
326    /// ICANN DNS names to the native gateway while rejecting special-use,
327    /// search, and IP-literal targets.
328    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}