1use std::collections::HashSet;
6
7use fidl_fuchsia_net_filter as fnet_filter;
8use fidl_fuchsia_net_filter_ext::{
9 self as fnet_filter_ext, Action, Change, CommitError, Domain, InstalledIpRoutine,
10 InstalledNatRoutine, IpHook, Matchers, Namespace, NamespaceId, NatHook, PushChangesError,
11 Resource, ResourceId, Routine, RoutineId, RoutineType, Rule, RuleId,
12};
13use fidl_fuchsia_net_interfaces_ext as fnet_interfaces_ext;
14use fidl_fuchsia_net_matchers_ext as fnet_matchers_ext;
15
16use anyhow::Context as _;
17use log::info;
18
19use crate::{FilterConfig, InterfaceType};
20
21#[derive(Debug)]
23pub(crate) enum FilterError {
24 Push(PushChangesError),
25 Commit(CommitError),
26}
27
28pub(crate) struct FilterControl {
30 controller: fnet_filter_ext::Controller,
31 masquerade: MasqueradeState,
32 }
35
36impl FilterControl {
37 pub(super) async fn new(proxy: fnet_filter::ControlProxy) -> Result<Self, anyhow::Error> {
38 let controller_id = fnet_filter_ext::ControllerId(String::from("netcfg"));
39 Ok(Self {
40 controller: fnet_filter_ext::Controller::new(&proxy, &controller_id)
41 .await
42 .context("could not create controller from filter proxy")?,
43 masquerade: MasqueradeState { routine_id: masquerade_routine(), next_rule_index: 0 },
44 })
45 }
46
47 pub(super) async fn update_filters(
50 &mut self,
51 config: FilterConfig,
52 filter_enabled_interface_types: &HashSet<InterfaceType>,
53 ) -> Result<(), anyhow::Error> {
54 let Self { controller, masquerade } = self;
55 let uninstalled_ip_routines = filter_routines(false );
56 let installed_ip_routines = filter_routines(true );
57 let changes = generate_initial_filter_changes(
58 &uninstalled_ip_routines,
59 &installed_ip_routines,
60 &masquerade.routine_id,
61 config,
62 filter_enabled_interface_types,
63 )?;
64
65 for batch in changes.chunks(usize::from(fnet_filter::MAX_BATCH_SIZE)) {
66 controller
67 .push_changes(batch.to_vec())
68 .await
69 .context("failed to push changes to filter controller")?;
70 }
71
72 controller.commit().await.context("failed to commit changes to filter controller")?;
73 info!("initial filter configuration has been committed successfully");
74 Ok(())
75 }
76}
77
78struct MasqueradeState {
80 routine_id: RoutineId,
82 next_rule_index: u32,
92}
93
94fn filter_routines(installed: bool) -> netfilter::parser::FilterRoutines {
98 let suffix = if !installed { "_uninstalled" } else { "" };
99 netfilter::parser::FilterRoutines {
100 local_ingress: Some(RoutineId {
101 namespace: namespace_id(),
102 name: format!("local_ingress{suffix}"),
103 }),
104 local_egress: Some(RoutineId {
105 namespace: namespace_id(),
106 name: format!("local_egress{suffix}"),
107 }),
108 }
109}
110
111fn masquerade_routine() -> RoutineId {
115 RoutineId { namespace: namespace_id(), name: format!("egress_masquerade") }
116}
117
118fn namespace_id() -> NamespaceId {
119 NamespaceId(String::from("netcfg"))
120}
121
122fn get_enabled_port_classes(
123 interface_types: &HashSet<InterfaceType>,
124) -> HashSet<fnet_interfaces_ext::PortClass> {
125 let mut port_classes = HashSet::new();
126 for interface_type in interface_types {
127 port_classes.extend(interface_type.port_classes());
128 if *interface_type == InterfaceType::WlanClient {
130 let _replaced: bool = port_classes.insert(fnet_interfaces_ext::PortClass::WlanAp);
131 }
132 }
133 return port_classes;
134}
135
136fn generate_initial_filter_changes(
140 uninstalled_ip_routines: &netfilter::parser::FilterRoutines,
141 installed_ip_routines: &netfilter::parser::FilterRoutines,
142 masquerade_routine: &RoutineId,
143 config: FilterConfig,
144 filter_enabled_interface_types: &HashSet<InterfaceType>,
145) -> Result<Vec<Change>, anyhow::Error> {
146 let mut changes = Vec::new();
147 let namespace = Namespace { id: namespace_id(), domain: Domain::AllIp };
148 changes.push(Change::Create(Resource::Namespace(namespace)));
149
150 let netfilter::parser::FilterRoutines { local_ingress, local_egress } = uninstalled_ip_routines;
159 let uninstalled_local_ingress =
160 local_ingress.clone().map(|id| Routine { id, routine_type: RoutineType::Ip(None) });
161 let uninstalled_local_egress =
162 local_egress.clone().map(|id| Routine { id, routine_type: RoutineType::Ip(None) });
163
164 fn installed_routine_from_id(id: RoutineId, hook: IpHook) -> Routine {
167 Routine {
168 id: id,
169 routine_type: RoutineType::Ip(Some(InstalledIpRoutine { hook, priority: 0i32 })),
170 }
171 }
172 let netfilter::parser::FilterRoutines { local_ingress, local_egress } = installed_ip_routines;
173 let local_ingress =
174 local_ingress.clone().map(|id| installed_routine_from_id(id, IpHook::LocalIngress));
175 let local_egress =
176 local_egress.clone().map(|id| installed_routine_from_id(id, IpHook::LocalEgress));
177
178 let masquerade = Routine {
179 id: masquerade_routine.clone(),
180 routine_type: RoutineType::Nat(Some(InstalledNatRoutine {
181 hook: NatHook::Egress,
182 priority: 0i32,
183 })),
184 };
185
186 let routine_changes = [
187 uninstalled_local_ingress,
188 local_ingress,
189 uninstalled_local_egress,
190 local_egress,
191 Some(masquerade),
192 ]
193 .into_iter()
194 .filter_map(|routine| routine)
195 .map(|routine| Change::Create(Resource::Routine(routine)));
196 changes.extend(routine_changes);
197
198 let FilterConfig { rules, nat_rules: _, rdr_rules: _ } = config;
201 if !rules.is_empty() {
202 let rules =
206 netfilter::parser::parse_str_to_rules(&rules.join(""), &uninstalled_ip_routines)
207 .context("error parsing filter rules")?;
208 let rule_changes = rules.into_iter().map(|rule| Change::Create(Resource::Rule(rule)));
209 changes.extend(rule_changes);
210 }
211
212 for (i, port_class) in
213 get_enabled_port_classes(filter_enabled_interface_types).into_iter().enumerate()
214 {
215 let port_class_rules = generate_static_port_class_filter_rules(
216 uninstalled_ip_routines,
217 installed_ip_routines,
218 port_class,
219 u32::try_from(i).expect("rule index overflowed u32"),
220 );
221 changes
222 .extend(port_class_rules.into_iter().map(|rule| Change::Create(Resource::Rule(rule))));
223 }
224
225 Ok(changes)
226}
227
228fn create_jump_rule(
229 routine_id: RoutineId,
230 index: u32,
231 interface: fnet_matchers_ext::Interface,
232 hook: IpHook,
233 target_routine_name: &str,
234) -> Rule {
235 let (in_interface, out_interface) = match hook {
236 IpHook::LocalIngress | IpHook::Ingress => (Some(interface), None),
237 IpHook::LocalEgress | IpHook::Egress => (None, Some(interface)),
238 IpHook::Forwarding => (Some(interface.clone()), Some(interface)),
239 };
240
241 Rule {
242 id: RuleId { routine: routine_id, index },
243 matchers: Matchers { in_interface, out_interface, ..Default::default() },
244 action: Action::Jump(target_routine_name.to_string()),
245 }
246}
247
248fn generate_static_port_class_filter_rules(
251 uninstalled_ip_routines: &netfilter::parser::FilterRoutines,
252 installed_ip_routines: &netfilter::parser::FilterRoutines,
253 port_class: fnet_interfaces_ext::PortClass,
254 current_installed_rule_index: u32,
255) -> Vec<Rule> {
256 let netfilter::parser::FilterRoutines {
257 local_ingress: uninstalled_local_ingress,
258 local_egress: uninstalled_local_egress,
259 } = uninstalled_ip_routines;
260 let netfilter::parser::FilterRoutines { local_ingress, local_egress } = installed_ip_routines;
261
262 let local_ingress_rule = local_ingress.clone().map(|routine_id| {
263 create_port_class_matching_jump_rule(
264 routine_id,
265 current_installed_rule_index,
266 port_class,
267 IpHook::LocalIngress,
268 &uninstalled_local_ingress
269 .as_ref()
270 .expect("there should be a corresponding uninstalled routine for local ingress")
271 .name,
272 )
273 });
274 let local_egress_rule = local_egress.clone().map(|routine_id| {
275 create_port_class_matching_jump_rule(
276 routine_id,
277 current_installed_rule_index,
278 port_class,
279 IpHook::LocalEgress,
280 &uninstalled_local_egress
281 .as_ref()
282 .expect("there should be a corresponding uninstalled routine for local egress")
283 .name,
284 )
285 });
286
287 [local_ingress_rule, local_egress_rule].into_iter().flatten().collect()
288}
289
290fn create_port_class_matching_jump_rule(
293 routine_id: RoutineId,
294 index: u32,
295 port_class: fnet_interfaces_ext::PortClass,
296 hook: IpHook,
297 target_routine_name: &str,
298) -> Rule {
299 create_jump_rule(
300 routine_id,
301 index,
302 fnet_matchers_ext::Interface::PortClass(port_class),
303 hook,
304 target_routine_name,
305 )
306}
307
308pub(crate) async fn add_masquerade_rule(
310 filter: &mut FilterControl,
311 matchers: Matchers,
312) -> Result<RuleId, FilterError> {
313 let MasqueradeState { routine_id, next_rule_index } = &mut filter.masquerade;
314 let rule_id = RuleId { routine: routine_id.clone(), index: *next_rule_index };
315 let rule_changes = vec![Change::Create(Resource::Rule(Rule {
316 id: rule_id.clone(),
317 matchers: matchers,
318 action: Action::Masquerade { src_port: None },
319 }))];
320 filter.controller.push_changes(rule_changes).await.map_err(FilterError::Push)?;
321 filter.controller.commit().await.map_err(FilterError::Commit)?;
322 *next_rule_index += 1;
323 Ok(rule_id)
324}
325
326pub(crate) async fn remove_masquerade_rule(
328 filter: &mut FilterControl,
329 rule: &RuleId,
330) -> Result<(), FilterError> {
331 let rule_changes = vec![Change::Remove(ResourceId::Rule(rule.clone()))];
332 filter.controller.push_changes(rule_changes).await.map_err(FilterError::Push)?;
333 filter.controller.commit().await.map_err(FilterError::Commit)
334}
335
336#[cfg(test)]
337mod tests {
338 use futures::StreamExt as _;
339 use test_case::test_case;
340
341 use super::*;
342
343 const LOCAL_INGRESS: &str = "local_ingress";
344 const UNINSTALLED_LOCAL_INGRESS: &str = "local_ingress_uninstalled";
345 const LOCAL_EGRESS: &str = "local_egress";
346 const UNINSTALLED_LOCAL_EGRESS: &str = "local_egress_uninstalled";
347 const MASQUERADE: &str = "egress_masquerade";
348
349 fn get_foundational_changes() -> Vec<Change> {
350 let mut changes = vec![Change::Create(Resource::Namespace(Namespace {
351 id: namespace_id(),
352 domain: Domain::AllIp,
353 }))];
354
355 let local_ingress = (LOCAL_INGRESS, UNINSTALLED_LOCAL_INGRESS, IpHook::LocalIngress);
356 let local_egress = (LOCAL_EGRESS, UNINSTALLED_LOCAL_EGRESS, IpHook::LocalEgress);
357
358 let routine_changes = vec![local_ingress, local_egress]
359 .into_iter()
360 .map(|(installed_name, uninstalled_name, hook)| {
361 vec![
362 Routine {
363 id: RoutineId {
364 namespace: namespace_id(),
365 name: String::from(uninstalled_name),
366 },
367 routine_type: RoutineType::Ip(None),
368 },
369 Routine {
370 id: RoutineId {
371 namespace: namespace_id(),
372 name: String::from(installed_name),
373 },
374 routine_type: RoutineType::Ip(Some(InstalledIpRoutine {
375 hook,
376 priority: 0i32,
377 })),
378 },
379 ]
380 })
381 .flatten()
382 .chain([Routine {
383 id: RoutineId { namespace: namespace_id(), name: String::from(MASQUERADE) },
384 routine_type: RoutineType::Nat(Some(InstalledNatRoutine {
385 hook: NatHook::Egress,
386 priority: 0i32,
387 })),
388 }])
389 .map(|routine| Change::Create(Resource::Routine(routine)));
390 changes.extend(routine_changes);
391
392 changes
393 }
394
395 fn create_rule(routine: RoutineId, index: u32, action: Action) -> Rule {
396 Rule { id: RuleId { routine, index }, matchers: Matchers::default(), action }
397 }
398
399 fn create_routine_id(name: &str) -> RoutineId {
400 RoutineId { namespace: namespace_id(), name: String::from(name) }
401 }
402
403 fn create_filter_routines(
404 namespace: NamespaceId,
405 local_ingress: &str,
406 local_egress: &str,
407 ) -> netfilter::parser::FilterRoutines {
408 netfilter::parser::FilterRoutines {
409 local_ingress: Some(RoutineId {
410 namespace: namespace.clone(),
411 name: local_ingress.to_owned(),
412 }),
413 local_egress: Some(RoutineId { namespace, name: local_egress.to_owned() }),
414 }
415 }
416
417 #[test_case(vec![], vec![]; "no_rules")]
420 #[test_case(
421 vec!["pass in;"],
422 vec![create_rule(
423 create_routine_id(UNINSTALLED_LOCAL_INGRESS),
424 0,
425 Action::Accept,
426 )]; "ingress_accept")]
427 #[test_case(
428 vec!["drop out;"],
429 vec![create_rule(
430 create_routine_id(UNINSTALLED_LOCAL_EGRESS),
431 0,
432 Action::Drop,
433 )]; "egress_drop")]
434 #[test_case(
435 vec!["pass in; drop out;"],
436 vec![create_rule(
437 create_routine_id(UNINSTALLED_LOCAL_INGRESS),
438 0,
439 Action::Accept),
440 create_rule(
441 create_routine_id(UNINSTALLED_LOCAL_EGRESS),
442 1,
443 Action::Drop,
444 )]; "ingress_accept_egress_drop")]
445 fn test_initial_filter_changes(rules_input: Vec<&str>, expected_rules: Vec<Rule>) {
446 let namespace = namespace_id();
447 let installed_filter_routines =
448 create_filter_routines(namespace.clone(), LOCAL_INGRESS, LOCAL_EGRESS);
449 let uninstalled_filter_routines =
450 create_filter_routines(namespace, UNINSTALLED_LOCAL_INGRESS, UNINSTALLED_LOCAL_EGRESS);
451
452 let changes = generate_initial_filter_changes(
453 &uninstalled_filter_routines,
454 &installed_filter_routines,
455 &masquerade_routine(),
456 FilterConfig {
457 rules: rules_input.into_iter().map(|rule| rule.to_owned()).collect(),
458 nat_rules: vec![],
459 rdr_rules: vec![],
460 },
461 &HashSet::new(),
462 )
463 .expect("rules should be formatted correctly");
464
465 let mut expected_changes = get_foundational_changes();
466 let expected_rule_changes =
467 expected_rules.into_iter().map(|rule| Change::Create(Resource::Rule(rule)));
468 expected_changes.extend(expected_rule_changes);
469
470 assert_eq!(changes, expected_changes);
471 }
472
473 #[test]
474 fn test_generate_static_port_class_filter_rules() {
475 let namespace = namespace_id();
476 let installed_filter_routines =
477 create_filter_routines(namespace.clone(), LOCAL_INGRESS, LOCAL_EGRESS);
478 let uninstalled_filter_routines =
479 create_filter_routines(namespace, UNINSTALLED_LOCAL_INGRESS, UNINSTALLED_LOCAL_EGRESS);
480
481 let rules = generate_static_port_class_filter_rules(
482 &uninstalled_filter_routines,
483 &installed_filter_routines,
484 fnet_interfaces_ext::PortClass::Lowpan,
485 0,
486 );
487
488 let local_ingress = (
489 installed_filter_routines.local_ingress.unwrap(),
490 uninstalled_filter_routines.local_ingress.unwrap().name,
491 IpHook::LocalIngress,
492 );
493 let local_egress = (
494 installed_filter_routines.local_egress.unwrap(),
495 uninstalled_filter_routines.local_egress.unwrap().name,
496 IpHook::LocalEgress,
497 );
498 let expected_rules: Vec<_> = vec![local_ingress, local_egress]
499 .into_iter()
500 .map(|(installed_routine, uninstalled_routine_name, hook)| {
501 create_port_class_matching_jump_rule(
502 installed_routine,
503 0,
504 fnet_interfaces_ext::PortClass::Lowpan,
505 hook,
506 &uninstalled_routine_name,
507 )
508 })
509 .collect();
510
511 assert_eq!(rules, expected_rules);
512 }
513
514 #[test]
515 fn test_initial_filter_changes_with_lowpan() {
516 let namespace = namespace_id();
517 let installed_filter_routines =
518 create_filter_routines(namespace.clone(), LOCAL_INGRESS, LOCAL_EGRESS);
519 let uninstalled_filter_routines =
520 create_filter_routines(namespace, UNINSTALLED_LOCAL_INGRESS, UNINSTALLED_LOCAL_EGRESS);
521
522 let changes = generate_initial_filter_changes(
523 &uninstalled_filter_routines,
524 &installed_filter_routines,
525 &masquerade_routine(),
526 FilterConfig { rules: vec![], nat_rules: vec![], rdr_rules: vec![] },
527 &[InterfaceType::Lowpan].into(),
528 )
529 .expect("rules should be formatted correctly");
530
531 let mut expected_changes = get_foundational_changes();
532 expected_changes.extend(
533 generate_static_port_class_filter_rules(
534 &uninstalled_filter_routines,
535 &installed_filter_routines,
536 fnet_interfaces_ext::PortClass::Lowpan,
537 0,
538 )
539 .into_iter()
540 .map(|rule| Change::Create(Resource::Rule(rule))),
541 );
542
543 assert_eq!(changes, expected_changes);
544 }
545
546 #[test_case(
547 &[],
548 &[];
549 "empty"
550 )]
551 #[test_case(
552 &[InterfaceType::Lowpan],
553 &[fnet_interfaces_ext::PortClass::Lowpan];
554 "lowpan"
555 )]
556 #[test_case(
557 &[InterfaceType::Ethernet],
558 &[
559 fnet_interfaces_ext::PortClass::Virtual,
560 fnet_interfaces_ext::PortClass::Ethernet,
561 fnet_interfaces_ext::PortClass::Ppp,
562 fnet_interfaces_ext::PortClass::Bridge,
563 ];
564 "ethernet"
565 )]
566 #[test_case(
567 &[InterfaceType::WlanClient],
568 &[
569 fnet_interfaces_ext::PortClass::WlanClient,
570 fnet_interfaces_ext::PortClass::WlanAp,
571 ];
572 "wlan_client_enables_wlan_client_and_ap"
573 )]
574 #[test_case(
575 &[InterfaceType::WlanAp],
576 &[fnet_interfaces_ext::PortClass::WlanAp];
577 "wlan_ap"
578 )]
579 #[test_case(
580 &[InterfaceType::WlanClient, InterfaceType::WlanAp],
581 &[
582 fnet_interfaces_ext::PortClass::WlanClient,
583 fnet_interfaces_ext::PortClass::WlanAp,
584 ];
585 "wlan_client_and_ap_deduplicated"
586 )]
587 #[test_case(
588 &[InterfaceType::Blackhole],
589 &[fnet_interfaces_ext::PortClass::Blackhole];
590 "blackhole"
591 )]
592 fn test_get_enabled_port_classes(
593 interface_types: &[InterfaceType],
594 expected_port_classes: &[fnet_interfaces_ext::PortClass],
595 ) {
596 let enabled: HashSet<_> =
597 get_enabled_port_classes(&interface_types.iter().copied().collect());
598 let expected: HashSet<_> = expected_port_classes.iter().copied().collect();
599 assert_eq!(enabled, expected);
600 }
601
602 #[fuchsia::test]
603 async fn test_update_filters_large_batch() {
604 let (control_client, control_server) =
605 fidl::endpoints::create_endpoints::<fnet_filter::ControlMarker>();
606 let client_fut = FilterControl::new(control_client.into_proxy());
607 let mut control_stream = control_server.into_stream();
608 let control_server_fut = async move {
609 match control_stream
610 .next()
611 .await
612 .expect("stream shouldn't close")
613 .expect("stream shouldn't have an error")
614 {
615 fnet_filter::ControlRequest::OpenController { id, request, control_handle: _ } => {
616 let (request_stream, control_handle) = request.into_stream_and_control_handle();
617 control_handle.send_on_id_assigned(id.as_str()).expect("failed to respond");
618 request_stream
619 }
620 _ => panic!("unexpected request"),
621 }
622 };
623 let (filter_control, mut server_request_stream) =
624 futures::join!(client_fut, control_server_fut);
625 let mut filter_control = filter_control.expect("failed to create filter control");
626
627 let config = FilterConfig {
628 rules: std::iter::repeat("pass in;".to_string()).take(50).collect(),
629 nat_rules: vec![],
630 rdr_rules: vec![],
631 };
632
633 let server_fut = async move {
634 let mut push_changes_count = 0;
635 while let Some(req) = server_request_stream.next().await {
636 match req.expect("stream shouldn't have an error") {
637 fnet_filter::NamespaceControllerRequest::PushChanges { changes, responder } => {
638 assert!(
639 changes.len() <= usize::from(fnet_filter::MAX_BATCH_SIZE),
640 "batch size {} exceeds MAX_BATCH_SIZE",
641 changes.len()
642 );
643 push_changes_count += 1;
644 responder
645 .send(fnet_filter::ChangeValidationResult::Ok(fnet_filter::Empty))
646 .expect("failed to respond");
647 }
648 fnet_filter::NamespaceControllerRequest::Commit { payload: _, responder } => {
649 responder
650 .send(fnet_filter::CommitResult::Ok(fnet_filter::Empty))
651 .expect("failed to respond");
652 break;
653 }
654 _ => panic!("unexpected request"),
655 }
656 }
657 push_changes_count
658 };
659
660 let filter_enabled_interface_types = HashSet::new();
661 let (client_res, push_changes_count) = futures::join!(
662 filter_control.update_filters(config, &filter_enabled_interface_types),
663 server_fut
664 );
665
666 client_res.expect("update_filters should succeed");
667 assert_eq!(push_changes_count, 2);
668 }
669}