Skip to main content

srx/client/
policy.rs

1//! Transport policy: environment-aware selection of optimal transports.
2//!
3//! Different network environments favor different transports:
4//! - **Wifi**: low latency, prefer QUIC/UDP
5//! - **Cellular**: unreliable, prefer TCP/QUIC (retransmission built-in)
6//! - **Corporate**: restrictive firewalls, prefer HTTP/gRPC tunnels
7//! - **Unknown**: balanced default order
8
9use crate::transport::TransportKind;
10
11/// Network environment hint for transport selection tuning.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum NetworkEnvironment {
14    Wifi,
15    Cellular,
16    Corporate,
17    Unknown,
18}
19
20/// Per-environment transport preference (first = most preferred).
21fn preference_for(env: NetworkEnvironment) -> &'static [TransportKind] {
22    match env {
23        NetworkEnvironment::Wifi => &[
24            TransportKind::Quic,
25            TransportKind::Udp,
26            TransportKind::Tcp,
27            TransportKind::WebSocket,
28            TransportKind::Grpc,
29            TransportKind::Http2,
30        ],
31        NetworkEnvironment::Cellular => &[
32            TransportKind::Tcp,
33            TransportKind::Quic,
34            TransportKind::WebSocket,
35            TransportKind::Http2,
36            TransportKind::Grpc,
37            TransportKind::Udp,
38        ],
39        NetworkEnvironment::Corporate => &[
40            TransportKind::Http2,
41            TransportKind::Grpc,
42            TransportKind::WebSocket,
43            TransportKind::Tcp,
44            TransportKind::Quic,
45            TransportKind::Udp,
46        ],
47        NetworkEnvironment::Unknown => &[
48            TransportKind::Quic,
49            TransportKind::Tcp,
50            TransportKind::WebSocket,
51            TransportKind::Udp,
52            TransportKind::Grpc,
53            TransportKind::Http2,
54        ],
55    }
56}
57
58/// Selects the optimal set of transports based on network environment and
59/// optional health scores.
60pub struct TransportPolicy {
61    env: NetworkEnvironment,
62}
63
64impl TransportPolicy {
65    pub fn new(env: NetworkEnvironment) -> Self {
66        Self { env }
67    }
68
69    /// Current network environment.
70    pub fn environment(&self) -> NetworkEnvironment {
71        self.env
72    }
73
74    /// Update the detected network environment.
75    pub fn set_environment(&mut self, env: NetworkEnvironment) {
76        self.env = env;
77    }
78
79    /// Return `available` transports sorted by environment-specific preference;
80    /// unknown kinds appended at the end.
81    pub fn recommend(&self, available: &[TransportKind]) -> Vec<TransportKind> {
82        let pref = preference_for(self.env);
83        let mut out: Vec<TransportKind> = pref
84            .iter()
85            .copied()
86            .filter(|k| available.contains(k))
87            .collect();
88        for k in available {
89            if !out.contains(k) {
90                out.push(*k);
91            }
92        }
93        out
94    }
95
96    /// Return `available` transports sorted by health score (descending),
97    /// with ties broken by environment-specific preference.
98    ///
99    /// `scores` maps each transport to a health score (0.0 = dead, 1.0 = perfect).
100    /// Transports without a score entry are treated as score 0.5 (unknown).
101    pub fn recommend_with_health(
102        &self,
103        available: &[TransportKind],
104        scores: &[(TransportKind, f64)],
105    ) -> Vec<TransportKind> {
106        let pref = preference_for(self.env);
107
108        let pref_rank =
109            |k: &TransportKind| -> usize { pref.iter().position(|p| p == k).unwrap_or(pref.len()) };
110        let score_of = |k: &TransportKind| -> f64 {
111            scores
112                .iter()
113                .find(|(sk, _)| sk == k)
114                .map(|(_, s)| *s)
115                .unwrap_or(0.5)
116        };
117
118        let mut ranked: Vec<TransportKind> = available.to_vec();
119        ranked.sort_by(|a, b| {
120            let sa = score_of(a);
121            let sb = score_of(b);
122            // Higher score first; on tie, lower preference rank first.
123            sb.partial_cmp(&sa)
124                .unwrap_or(std::cmp::Ordering::Equal)
125                .then_with(|| pref_rank(a).cmp(&pref_rank(b)))
126        });
127        ranked
128    }
129}
130
131impl Default for TransportPolicy {
132    fn default() -> Self {
133        Self::new(NetworkEnvironment::Unknown)
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    #[test]
142    fn wifi_prefers_quic_udp() {
143        let p = TransportPolicy::new(NetworkEnvironment::Wifi);
144        let v = p.recommend(&[TransportKind::Tcp, TransportKind::Udp, TransportKind::Quic]);
145        assert_eq!(v[0], TransportKind::Quic);
146        assert_eq!(v[1], TransportKind::Udp);
147        assert_eq!(v[2], TransportKind::Tcp);
148    }
149
150    #[test]
151    fn cellular_prefers_tcp() {
152        let p = TransportPolicy::new(NetworkEnvironment::Cellular);
153        let v = p.recommend(&[TransportKind::Udp, TransportKind::Tcp, TransportKind::Quic]);
154        assert_eq!(v[0], TransportKind::Tcp);
155    }
156
157    #[test]
158    fn corporate_prefers_http_grpc() {
159        let p = TransportPolicy::new(NetworkEnvironment::Corporate);
160        let v = p.recommend(&[
161            TransportKind::Tcp,
162            TransportKind::Http2,
163            TransportKind::Grpc,
164        ]);
165        assert_eq!(v[0], TransportKind::Http2);
166        assert_eq!(v[1], TransportKind::Grpc);
167        assert_eq!(v[2], TransportKind::Tcp);
168    }
169
170    #[test]
171    fn unknown_matches_original_order() {
172        let p = TransportPolicy::new(NetworkEnvironment::Unknown);
173        let v = p.recommend(&[TransportKind::Udp, TransportKind::Tcp, TransportKind::Quic]);
174        assert_eq!(
175            v,
176            vec![TransportKind::Quic, TransportKind::Tcp, TransportKind::Udp]
177        );
178    }
179
180    #[test]
181    fn health_scores_override_preference() {
182        let p = TransportPolicy::new(NetworkEnvironment::Wifi);
183        // UDP has a higher health score than QUIC despite Wifi preferring QUIC.
184        let scores = vec![
185            (TransportKind::Quic, 0.3),
186            (TransportKind::Udp, 0.9),
187            (TransportKind::Tcp, 0.5),
188        ];
189        let v = p.recommend_with_health(
190            &[TransportKind::Quic, TransportKind::Udp, TransportKind::Tcp],
191            &scores,
192        );
193        assert_eq!(v[0], TransportKind::Udp);
194        assert_eq!(v[1], TransportKind::Tcp);
195        assert_eq!(v[2], TransportKind::Quic);
196    }
197
198    #[test]
199    fn equal_scores_break_tie_by_env_preference() {
200        let p = TransportPolicy::new(NetworkEnvironment::Wifi);
201        let scores = vec![(TransportKind::Quic, 0.8), (TransportKind::Udp, 0.8)];
202        let v = p.recommend_with_health(&[TransportKind::Udp, TransportKind::Quic], &scores);
203        // Wifi prefers QUIC over UDP.
204        assert_eq!(v[0], TransportKind::Quic);
205        assert_eq!(v[1], TransportKind::Udp);
206    }
207}