Skip to main content

lab_proxy_core/
config.rs

1// Copyright 2026 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5//! Structured configuration and runtime limit bounds (Spec Section 22).
6//!
7//! Structured configuration can narrow (reduce) bounds such as enabled state,
8//! audit capacity, maximum operations, and maximum deadlines. It may never
9//! widen any bound beyond the baseline specification.
10
11use crate::access_policy::{AccessClass, Denial};
12use std::collections::BTreeMap;
13
14/// Baseline upper bounds enforced by the core proxy specification.
15/// Structured configuration can only narrow (reduce) these bounds.
16pub const BASELINE_AUDIT_CAPACITY: u32 = 1024;
17pub const BASELINE_MAX_SNAPSHOT_ITEMS: u32 = 64;
18pub const BASELINE_MAX_SEQUENCE_ITEMS: u32 = 64;
19pub const BASELINE_MAX_DELAY_NS: u64 = 1_000_000_000;
20pub const BASELINE_MAX_SEQUENCE_DURATION_NS: u64 = 1_000_000_000;
21pub const BASELINE_MAX_DEADLINE_NS: u64 = 1_000_000_000;
22
23/// Structured configuration values for the proxy driver.
24#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
25pub struct ProxyConfig {
26    /// Whether the proxy accepts sessions. If false, all sessions are rejected.
27    pub enabled: bool,
28    /// Capacity of the audit ring buffer. Can only narrow (<= 1024).
29    pub audit_capacity: u32,
30    /// Maximum items allowed in a single snapshot. Can only narrow (<= 64).
31    pub max_snapshot_items: u32,
32    /// Maximum items allowed in a single sequence. Can only narrow (<= 64).
33    pub max_sequence_items: u32,
34    /// Maximum delay allowed per sequence step in ns (<= 100ms).
35    pub max_delay_ns: u64,
36    /// Maximum sequence duration in ns (<= 1s).
37    pub max_sequence_duration_ns: u64,
38    /// Maximum deadline timeout allowed for waits and polls in ns (<= 1s).
39    pub max_deadline_ns: u64,
40    /// Maximum operations per second per session (0 = unlimited).
41    pub max_ops_per_second: u32,
42}
43
44impl Default for ProxyConfig {
45    fn default() -> Self {
46        Self {
47            enabled: true,
48            audit_capacity: BASELINE_AUDIT_CAPACITY,
49            max_snapshot_items: BASELINE_MAX_SNAPSHOT_ITEMS,
50            max_sequence_items: BASELINE_MAX_SEQUENCE_ITEMS,
51            max_delay_ns: BASELINE_MAX_DELAY_NS,
52            max_sequence_duration_ns: BASELINE_MAX_SEQUENCE_DURATION_NS,
53            max_deadline_ns: BASELINE_MAX_DEADLINE_NS,
54            max_ops_per_second: 0,
55        }
56    }
57}
58
59/// Why a runtime configuration failed narrowing validation against baseline.
60#[derive(Clone, Debug, PartialEq, Eq)]
61pub enum ConfigNarrowingError {
62    WidenedEnabledState,
63    WidenedAuditCapacity { configured: u32, baseline: u32 },
64    WidenedMaxSnapshotItems { configured: u32, baseline: u32 },
65    WidenedMaxSequenceItems { configured: u32, baseline: u32 },
66    WidenedMaxDelay { configured: u64, baseline: u64 },
67    WidenedMaxSequenceDuration { configured: u64, baseline: u64 },
68    WidenedMaxDeadline { configured: u64, baseline: u64 },
69    WidenedMaxOpsPerSecond { configured: u32, baseline: u32 },
70}
71
72impl std::fmt::Display for ConfigNarrowingError {
73    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74        match self {
75            Self::WidenedEnabledState => write!(f, "cannot enable proxy when baseline is disabled"),
76            Self::WidenedAuditCapacity { configured, baseline } => {
77                write!(f, "audit_capacity {configured} exceeds baseline {baseline}")
78            }
79            Self::WidenedMaxSnapshotItems { configured, baseline } => {
80                write!(f, "max_snapshot_items {configured} exceeds baseline {baseline}")
81            }
82            Self::WidenedMaxSequenceItems { configured, baseline } => {
83                write!(f, "max_sequence_items {configured} exceeds baseline {baseline}")
84            }
85            Self::WidenedMaxDelay { configured, baseline } => {
86                write!(f, "max_delay_ns {configured} exceeds baseline {baseline}")
87            }
88            Self::WidenedMaxSequenceDuration { configured, baseline } => {
89                write!(f, "max_sequence_duration_ns {configured} exceeds baseline {baseline}")
90            }
91            Self::WidenedMaxDeadline { configured, baseline } => {
92                write!(f, "max_deadline_ns {configured} exceeds baseline {baseline}")
93            }
94            Self::WidenedMaxOpsPerSecond { configured, baseline } => {
95                write!(f, "max_ops_per_second {configured} widens baseline limit {baseline}")
96            }
97        }
98    }
99}
100
101impl std::error::Error for ConfigNarrowingError {}
102
103impl ProxyConfig {
104    /// Validates that `runtime` narrows or preserves this baseline config.
105    pub fn narrow_with(&self, runtime: &Self) -> Result<Self, ConfigNarrowingError> {
106        if !self.enabled && runtime.enabled {
107            return Err(ConfigNarrowingError::WidenedEnabledState);
108        }
109        if runtime.audit_capacity > self.audit_capacity {
110            return Err(ConfigNarrowingError::WidenedAuditCapacity {
111                configured: runtime.audit_capacity,
112                baseline: self.audit_capacity,
113            });
114        }
115        if runtime.max_snapshot_items > self.max_snapshot_items {
116            return Err(ConfigNarrowingError::WidenedMaxSnapshotItems {
117                configured: runtime.max_snapshot_items,
118                baseline: self.max_snapshot_items,
119            });
120        }
121        if runtime.max_sequence_items > self.max_sequence_items {
122            return Err(ConfigNarrowingError::WidenedMaxSequenceItems {
123                configured: runtime.max_sequence_items,
124                baseline: self.max_sequence_items,
125            });
126        }
127        if runtime.max_delay_ns > self.max_delay_ns {
128            return Err(ConfigNarrowingError::WidenedMaxDelay {
129                configured: runtime.max_delay_ns,
130                baseline: self.max_delay_ns,
131            });
132        }
133        if runtime.max_sequence_duration_ns > self.max_sequence_duration_ns {
134            return Err(ConfigNarrowingError::WidenedMaxSequenceDuration {
135                configured: runtime.max_sequence_duration_ns,
136                baseline: self.max_sequence_duration_ns,
137            });
138        }
139        if runtime.max_deadline_ns > self.max_deadline_ns {
140            return Err(ConfigNarrowingError::WidenedMaxDeadline {
141                configured: runtime.max_deadline_ns,
142                baseline: self.max_deadline_ns,
143            });
144        }
145        if self.max_ops_per_second > 0
146            && (runtime.max_ops_per_second == 0
147                || runtime.max_ops_per_second > self.max_ops_per_second)
148        {
149            return Err(ConfigNarrowingError::WidenedMaxOpsPerSecond {
150                configured: runtime.max_ops_per_second,
151                baseline: self.max_ops_per_second,
152            });
153        }
154        Ok(*runtime)
155    }
156}
157
158/// Tracks access counts and timestamps to enforce access-class count/rate limits (Spec 12.1 step 9).
159#[derive(Clone, Debug, PartialEq, Eq)]
160pub struct RateLimiter {
161    max_ops_per_second: u32,
162    window_start_ns: i64,
163    ops_in_window: u32,
164}
165
166impl RateLimiter {
167    pub fn new(max_ops_per_second: u32) -> Self {
168        Self { max_ops_per_second, window_start_ns: 0, ops_in_window: 0 }
169    }
170
171    /// Checks if an operation is permitted at `now_ns`.
172    pub fn check_and_record(&mut self, now_ns: i64) -> Result<(), Denial> {
173        if self.max_ops_per_second == 0 {
174            return Ok(());
175        }
176        let window_duration_ns: i64 = 1_000_000_000;
177        if now_ns - self.window_start_ns >= window_duration_ns {
178            self.window_start_ns = now_ns;
179            self.ops_in_window = 1;
180            Ok(())
181        } else if self.ops_in_window < self.max_ops_per_second {
182            self.ops_in_window += 1;
183            Ok(())
184        } else {
185            Err(Denial::LimitExceeded)
186        }
187    }
188}
189
190/// Enforces access-class count, rate, and deadline limits (Spec 12.1 step 9).
191#[derive(Clone, Debug, PartialEq, Eq)]
192pub struct AccessLimitEnforcer {
193    global_rate_limiter: RateLimiter,
194    per_class_rate_limiters: BTreeMap<AccessClass, RateLimiter>,
195    max_deadline_ns: u64,
196}
197
198impl AccessLimitEnforcer {
199    pub fn new(max_ops_per_second: u32, max_deadline_ns: u64) -> Self {
200        Self {
201            global_rate_limiter: RateLimiter::new(max_ops_per_second),
202            per_class_rate_limiters: BTreeMap::new(),
203            max_deadline_ns,
204        }
205    }
206
207    pub fn set_class_rate_limit(&mut self, class: AccessClass, max_ops_per_second: u32) {
208        self.per_class_rate_limiters.insert(class, RateLimiter::new(max_ops_per_second));
209    }
210
211    /// Enforces access-class rate limit at `now_ns`.
212    pub fn check_access(&mut self, class: AccessClass, now_ns: i64) -> Result<(), Denial> {
213        self.global_rate_limiter.check_and_record(now_ns)?;
214        if let Some(limiter) = self.per_class_rate_limiters.get_mut(&class) {
215            limiter.check_and_record(now_ns)?;
216        }
217        Ok(())
218    }
219
220    /// Enforces maximum deadline limit.
221    pub fn check_deadline(&self, requested_timeout_ns: i64) -> Result<(), Denial> {
222        if requested_timeout_ns < 0 || (requested_timeout_ns as u64) > self.max_deadline_ns {
223            Err(Denial::LimitExceeded)
224        } else {
225            Ok(())
226        }
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233
234    #[test]
235    fn test_proxy_config_defaults() {
236        let config = ProxyConfig::default();
237        assert!(config.enabled);
238        assert_eq!(config.audit_capacity, BASELINE_AUDIT_CAPACITY);
239        assert_eq!(config.max_snapshot_items, BASELINE_MAX_SNAPSHOT_ITEMS);
240        assert_eq!(config.max_sequence_items, BASELINE_MAX_SEQUENCE_ITEMS);
241        assert_eq!(config.max_delay_ns, BASELINE_MAX_DELAY_NS);
242        assert_eq!(config.max_sequence_duration_ns, BASELINE_MAX_SEQUENCE_DURATION_NS);
243        assert_eq!(config.max_deadline_ns, BASELINE_MAX_DEADLINE_NS);
244        assert_eq!(config.max_ops_per_second, 0);
245    }
246
247    #[test]
248    fn test_narrow_with_accepts_valid_reductions() {
249        let baseline = ProxyConfig::default();
250        let reduced = ProxyConfig {
251            enabled: false,
252            audit_capacity: 512,
253            max_snapshot_items: 16,
254            max_sequence_items: 16,
255            max_delay_ns: 50_000_000,
256            max_sequence_duration_ns: 500_000_000,
257            max_deadline_ns: 500_000_000,
258            max_ops_per_second: 100,
259        };
260        let narrowed = baseline.narrow_with(&reduced).unwrap();
261        assert_eq!(narrowed, reduced);
262    }
263
264    #[test]
265    fn test_narrow_with_rejects_widening() {
266        let baseline = ProxyConfig { enabled: false, ..ProxyConfig::default() };
267        assert_eq!(
268            baseline.narrow_with(&ProxyConfig::default()),
269            Err(ConfigNarrowingError::WidenedEnabledState)
270        );
271
272        let baseline = ProxyConfig::default();
273        let mut widened = baseline;
274        widened.audit_capacity = 2048;
275        assert!(matches!(
276            baseline.narrow_with(&widened),
277            Err(ConfigNarrowingError::WidenedAuditCapacity { .. })
278        ));
279
280        let mut widened = baseline;
281        widened.max_snapshot_items = 128;
282        assert!(matches!(
283            baseline.narrow_with(&widened),
284            Err(ConfigNarrowingError::WidenedMaxSnapshotItems { .. })
285        ));
286
287        let mut widened = baseline;
288        widened.max_sequence_items = 128;
289        assert!(matches!(
290            baseline.narrow_with(&widened),
291            Err(ConfigNarrowingError::WidenedMaxSequenceItems { .. })
292        ));
293
294        let mut widened = baseline;
295        widened.max_delay_ns = 2_000_000_000;
296        assert!(matches!(
297            baseline.narrow_with(&widened),
298            Err(ConfigNarrowingError::WidenedMaxDelay { .. })
299        ));
300
301        let mut widened = baseline;
302        widened.max_sequence_duration_ns = 2_000_000_000;
303        assert!(matches!(
304            baseline.narrow_with(&widened),
305            Err(ConfigNarrowingError::WidenedMaxSequenceDuration { .. })
306        ));
307
308        let mut widened = baseline;
309        widened.max_deadline_ns = 2_000_000_000;
310        assert!(matches!(
311            baseline.narrow_with(&widened),
312            Err(ConfigNarrowingError::WidenedMaxDeadline { .. })
313        ));
314
315        let baseline = ProxyConfig { max_ops_per_second: 50, ..ProxyConfig::default() };
316        let mut widened = baseline;
317        widened.max_ops_per_second = 100;
318        assert!(matches!(
319            baseline.narrow_with(&widened),
320            Err(ConfigNarrowingError::WidenedMaxOpsPerSecond { .. })
321        ));
322    }
323
324    #[test]
325    fn test_rate_limiter() {
326        let mut limiter = RateLimiter::new(2);
327        let t0 = 1_000_000_000;
328        assert!(limiter.check_and_record(t0).is_ok());
329        assert!(limiter.check_and_record(t0 + 100_000_000).is_ok());
330        // 3rd operation in the same 1s window is rejected
331        assert_eq!(limiter.check_and_record(t0 + 200_000_000), Err(Denial::LimitExceeded));
332
333        // Next second window resets counter
334        let t1 = t0 + 1_000_000_001;
335        assert!(limiter.check_and_record(t1).is_ok());
336    }
337
338    #[test]
339    fn test_access_limit_enforcer_and_deadline() {
340        let mut enforcer = AccessLimitEnforcer::new(100, 1_000_000_000);
341        enforcer.set_class_rate_limit(AccessClass::Poll, 1);
342
343        assert!(enforcer.check_access(AccessClass::ReadOnce, 1_000_000).is_ok());
344        assert!(enforcer.check_access(AccessClass::Poll, 1_000_000).is_ok());
345        // Second poll in same second is rejected by class rate limit
346        assert_eq!(enforcer.check_access(AccessClass::Poll, 1_500_000), Err(Denial::LimitExceeded));
347        // But read_once is still allowed under global limit
348        assert!(enforcer.check_access(AccessClass::ReadOnce, 1_500_000).is_ok());
349
350        // Deadline check
351        assert!(enforcer.check_deadline(500_000_000).is_ok());
352        assert_eq!(enforcer.check_deadline(1_500_000_000), Err(Denial::LimitExceeded));
353        assert_eq!(enforcer.check_deadline(-1), Err(Denial::LimitExceeded));
354    }
355}