1use futures::future::BoxFuture;
20use std::collections::HashMap;
21use std::fmt;
22use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
23use std::sync::{Arc, Mutex};
24use std::time::{Duration, Instant};
25
26#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
27pub enum ActionKind {
28 Navigate,
29 Reload,
30 History,
31 Click,
32 DoubleClick,
33 Hover,
34 Drag,
35 Wheel,
36 Press,
37 TypeText,
38 Fill,
39 Select,
40 Upload,
41 Eval,
42}
43
44#[derive(Clone, Debug)]
47pub struct AdmissionRequest {
48 pub session_id: String,
49 pub page_id: String,
50 pub action: ActionKind,
51 pub url: Option<String>,
53 pub target: Option<String>,
55 pub detail: Option<String>,
57}
58
59#[derive(Clone, Debug, Eq, PartialEq)]
60pub enum Decision {
61 Allow,
62 Deny(String),
63}
64
65pub type AdmissionHook =
66 Arc<dyn Fn(AdmissionRequest) -> BoxFuture<'static, Decision> + Send + Sync>;
67
68#[derive(Clone, Copy, Debug, Eq, PartialEq)]
70pub enum AddressClass {
71 Public,
72 Loopback,
73 LinkLocal,
74 Private,
75 Unspecified,
76 Multicast,
77}
78
79#[derive(Clone, Debug)]
81pub struct NetworkRequest {
82 pub url: String,
83 pub scheme: String,
84 pub host: String,
85 pub port: Option<u16>,
86 pub class: AddressClass,
87 pub resource_type: String,
89}
90
91pub type AllowCallback = Arc<dyn Fn(&NetworkRequest) -> bool + Send + Sync>;
92
93type ResolvedCache = Arc<Mutex<HashMap<String, (Instant, Vec<IpAddr>)>>>;
95
96#[derive(Clone, Default)]
97pub struct NetworkPolicy {
98 pub allow_hosts: Vec<String>,
100 pub allow_request: Option<AllowCallback>,
102 pub unrestricted: bool,
104 pub resolver: Option<Resolver>,
106 resolved: ResolvedCache,
109 approved: Arc<Mutex<HashMap<String, Vec<IpAddr>>>>,
111}
112
113pub type Resolver = Arc<dyn Fn(&str) -> Vec<IpAddr> + Send + Sync>;
114
115impl fmt::Debug for NetworkPolicy {
116 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
117 f.debug_struct("NetworkPolicy")
118 .field("allow_hosts", &self.allow_hosts)
119 .field("allow_request", &self.allow_request.is_some())
120 .field("unrestricted", &self.unrestricted)
121 .finish()
122 }
123}
124
125#[derive(Clone, Debug, Eq, PartialEq)]
126pub struct BlockedRequest {
127 pub url: String,
128 pub reason: String,
129}
130
131pub fn classify_ip(ip: IpAddr) -> AddressClass {
132 match ip {
133 IpAddr::V4(v4) => classify_v4(v4),
134 IpAddr::V6(v6) => {
135 if let Some(v4) = v6.to_ipv4_mapped() {
136 return classify_v4(v4);
137 }
138 let seg = v6.segments();
139 if v6.is_loopback() {
140 AddressClass::Loopback
141 } else if v6.is_unspecified() {
142 AddressClass::Unspecified
143 } else if v6.is_multicast() {
144 AddressClass::Multicast
145 } else if seg[0] & 0xffc0 == 0xfe80 {
146 AddressClass::LinkLocal
147 } else if seg[0] & 0xfe00 == 0xfc00 || is_nat64_private(v6) {
148 AddressClass::Private
149 } else {
150 AddressClass::Public
151 }
152 }
153 }
154}
155
156fn is_nat64_private(v6: Ipv6Addr) -> bool {
158 let s = v6.segments();
159 if s[..6] == [0x64, 0xff9b, 0, 0, 0, 0] {
160 let v4 = Ipv4Addr::new((s[6] >> 8) as u8, s[6] as u8, (s[7] >> 8) as u8, s[7] as u8);
161 return classify_v4(v4) != AddressClass::Public;
162 }
163 false
164}
165
166fn classify_v4(ip: Ipv4Addr) -> AddressClass {
167 let o = ip.octets();
168 if ip.is_loopback() {
169 AddressClass::Loopback
170 } else if ip.is_unspecified() || o[0] == 0 {
171 AddressClass::Unspecified
172 } else if ip.is_link_local() {
173 AddressClass::LinkLocal
174 } else if ip.is_private() || (o[0] == 100 && (64..128).contains(&o[1])) {
175 AddressClass::Private
176 } else if ip.is_multicast() || ip.is_broadcast() || o[0] >= 240 {
177 AddressClass::Multicast
178 } else {
179 AddressClass::Public
180 }
181}
182
183impl NetworkPolicy {
184 pub fn unrestricted() -> Self {
186 Self {
187 unrestricted: true,
188 ..Self::default()
189 }
190 }
191
192 pub fn allow_host(mut self, host: impl Into<String>) -> Self {
193 self.allow_hosts.push(host.into().to_ascii_lowercase());
194 self
195 }
196
197 pub fn with_resolver(
198 mut self,
199 f: impl Fn(&str) -> Vec<IpAddr> + Send + Sync + 'static,
200 ) -> Self {
201 self.resolver = Some(Arc::new(f));
202 self
203 }
204
205 pub fn allow_with(
206 mut self,
207 f: impl Fn(&NetworkRequest) -> bool + Send + Sync + 'static,
208 ) -> Self {
209 self.allow_request = Some(Arc::new(f));
210 self
211 }
212
213 pub async fn check(&self, url: &str, resource_type: &str) -> std::result::Result<(), String> {
215 if self.unrestricted {
216 return Ok(());
217 }
218 let parsed = url::Url::parse(url).map_err(|e| format!("unparseable url: {e}"))?;
219 let scheme = parsed.scheme().to_ascii_lowercase();
220 if matches!(scheme.as_str(), "about" | "data" | "blob") {
222 if scheme == "about" && parsed.path() != "blank" {
223 return self.decide(
224 url,
225 &scheme,
226 "",
227 None,
228 AddressClass::Loopback,
229 resource_type,
230 "about: page",
231 );
232 }
233 return Ok(());
234 }
235 if !matches!(scheme.as_str(), "http" | "https" | "ws" | "wss") {
236 let reason = format!("scheme '{scheme}' is blocked");
237 return self.decide(
238 url,
239 &scheme,
240 "",
241 None,
242 AddressClass::Loopback,
243 resource_type,
244 &reason,
245 );
246 }
247 let host = parsed
248 .host_str()
249 .unwrap_or("")
250 .trim_matches(['[', ']'])
251 .to_ascii_lowercase();
252 if host.is_empty() {
253 return Err("request has no host".into());
254 }
255 let (class, ips) = self.host_class(&host).await;
256 if class == AddressClass::Public {
257 return Ok(());
258 }
259 let reason = format!("host '{host}' is {class:?}");
260 self.decide(
261 url,
262 &scheme,
263 &host,
264 parsed.port_or_known_default(),
265 class,
266 resource_type,
267 &reason,
268 )?;
269 self.approved.lock().unwrap().insert(host, ips);
270 Ok(())
271 }
272
273 pub(crate) async fn pin(&self, host: &str) -> std::result::Result<Vec<IpAddr>, String> {
278 let host = host.trim_matches(['[', ']']).to_ascii_lowercase();
279 let (_, ips) = self.host_class(&host).await;
280 if ips.is_empty() {
281 return Err(format!("host '{host}' did not resolve"));
282 }
283 let approved = self
284 .approved
285 .lock()
286 .unwrap()
287 .get(&host)
288 .cloned()
289 .unwrap_or_default();
290 let listed = self.allow_hosts.contains(&host);
291 let ok: Vec<IpAddr> = ips
292 .iter()
293 .copied()
294 .filter(|ip| {
295 classify_ip(*ip) == AddressClass::Public || listed || approved.contains(ip)
296 })
297 .collect();
298 if ok.len() == ips.len() {
299 Ok(ok)
300 } else {
301 Err(format!("host '{host}' resolves to a blocked address"))
302 }
303 }
304
305 #[allow(clippy::too_many_arguments)]
306 fn decide(
307 &self,
308 url: &str,
309 scheme: &str,
310 host: &str,
311 port: Option<u16>,
312 class: AddressClass,
313 resource_type: &str,
314 reason: &str,
315 ) -> std::result::Result<(), String> {
316 if !host.is_empty() && self.allow_hosts.iter().any(|h| h == host) {
317 return Ok(());
318 }
319 if let Some(cb) = &self.allow_request {
320 let req = NetworkRequest {
321 url: url.to_string(),
322 scheme: scheme.to_string(),
323 host: host.to_string(),
324 port,
325 class,
326 resource_type: resource_type.to_string(),
327 };
328 if cb(&req) {
329 return Ok(());
330 }
331 }
332 Err(reason.to_string())
333 }
334
335 async fn host_class(&self, host: &str) -> (AddressClass, Vec<IpAddr>) {
337 if let Ok(ip) = host.parse::<IpAddr>() {
338 return (classify_ip(ip), vec![ip]);
339 }
340 if host == "localhost" || host.ends_with(".localhost") {
341 return (
342 AddressClass::Loopback,
343 vec![IpAddr::V4(Ipv4Addr::LOCALHOST)],
344 );
345 }
346 let addrs = self.resolve(host).await;
347 let class = addrs
348 .iter()
349 .copied()
350 .map(classify_ip)
351 .find(|c| *c != AddressClass::Public)
352 .unwrap_or(AddressClass::Public);
353 (class, addrs)
354 }
355
356 async fn resolve(&self, host: &str) -> Vec<IpAddr> {
357 const TTL: Duration = Duration::from_secs(30);
358 if let Some((at, v)) = self.resolved.lock().unwrap().get(host) {
359 if at.elapsed() < TTL {
360 return v.clone();
361 }
362 }
363 let found: Vec<IpAddr> = if let Some(r) = &self.resolver {
364 r(host)
365 } else {
366 match tokio::time::timeout(Duration::from_secs(3), tokio::net::lookup_host((host, 0)))
367 .await
368 {
369 Ok(Ok(it)) => it.map(|a| a.ip()).collect(),
370 _ => Vec::new(), }
372 };
373 self.resolved
374 .lock()
375 .unwrap()
376 .insert(host.to_string(), (Instant::now(), found.clone()));
377 found
378 }
379}
380
381#[derive(Clone, Debug)]
383pub enum BrowserEvent {
384 Started {
385 session_id: String,
386 pid: Option<u32>,
387 },
388 Stopped {
389 session_id: String,
390 },
391 Denied {
393 session_id: String,
394 what: String,
395 reason: String,
396 },
397}
398
399pub type EventSink = Arc<dyn Fn(&BrowserEvent) + Send + Sync>;
400
401pub(crate) struct Guard {
403 pub session_id: String,
404 pub admission: Option<AdmissionHook>,
405 pub network: NetworkPolicy,
406 pub events: Option<EventSink>,
407}
408
409impl Guard {
410 pub fn emit(&self, e: BrowserEvent) {
411 if let Some(s) = &self.events {
412 s(&e);
413 }
414 }
415
416 pub async fn admit(&self, req: AdmissionRequest) -> crate::error::Result<()> {
417 let what = format!("{:?}", req.action);
418 if let Some(hook) = &self.admission {
419 if let Decision::Deny(reason) = hook(req).await {
420 self.emit(BrowserEvent::Denied {
421 session_id: self.session_id.clone(),
422 what,
423 reason: reason.clone(),
424 });
425 return Err(crate::error::BrowserError::Denied(reason));
426 }
427 }
428 Ok(())
429 }
430
431 pub async fn check_url(&self, url: &str, kind: &str) -> crate::error::Result<()> {
432 match self.network.check(url, kind).await {
433 Ok(()) => Ok(()),
434 Err(reason) => {
435 self.emit(BrowserEvent::Denied {
436 session_id: self.session_id.clone(),
437 what: kind.into(),
438 reason: reason.clone(),
439 });
440 Err(crate::error::BrowserError::Denied(format!(
441 "{url}: {reason}"
442 )))
443 }
444 }
445 }
446}