Skip to main content

netcfg/
filter.rs

1// Copyright 2024 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
5use std::collections::hash_map::Entry;
6use std::collections::{HashMap, HashSet};
7use std::num::NonZeroU64;
8
9use fidl_fuchsia_net_filter as fnet_filter;
10use fidl_fuchsia_net_filter_deprecated as fnet_filter_deprecated;
11use fidl_fuchsia_net_filter_ext::{
12    self as fnet_filter_ext, Action, Change, CommitError, Domain, InstalledIpRoutine,
13    InstalledNatRoutine, IpHook, Matchers, Namespace, NamespaceId, NatHook, PushChangesError,
14    Resource, ResourceId, Routine, RoutineId, RoutineType, Rule, RuleId,
15};
16use fidl_fuchsia_net_interfaces_ext as fnet_interfaces_ext;
17use fidl_fuchsia_net_masquerade as fnet_masquerade;
18use fidl_fuchsia_net_matchers_ext as fnet_matchers_ext;
19use fuchsia_async::DurationExt as _;
20
21use anyhow::{Context as _, bail};
22use log::{error, info, warn};
23
24use crate::{FilterConfig, InterfaceId, InterfaceType, exit_with_fidl_error};
25
26/// An error observed on the `fuchsia.net.filter` API.
27#[derive(Debug)]
28pub(crate) enum FilterError {
29    Push(PushChangesError),
30    Commit(CommitError),
31}
32
33// A container to dispatch filtering functions depending on the
34// filtering API present.
35pub(crate) enum FilterControl {
36    Deprecated(fnet_filter_deprecated::FilterProxy),
37    Current(Box<FilterState>),
38}
39
40impl FilterControl {
41    // Determine whether to use the fuchsia.net.filter.deprecated API or the
42    // fuchsia.net.filter API. When the deprecated API is present and active,
43    // we should use it.
44    pub(super) async fn new(
45        deprecated_proxy: Option<fnet_filter_deprecated::FilterProxy>,
46        current_proxy: Option<fnet_filter::ControlProxy>,
47    ) -> Result<Self, anyhow::Error> {
48        if let Some(proxy) = deprecated_proxy {
49            if probe_for_presence(&proxy).await {
50                return Ok(FilterControl::Deprecated(proxy));
51            }
52        }
53
54        if let Some(proxy) = current_proxy {
55            let controller_id = fnet_filter_ext::ControllerId(String::from("netcfg"));
56            let filter = FilterControl::Current(Box::new(FilterState {
57                controller: fnet_filter_ext::Controller::new(&proxy, &controller_id)
58                    .await
59                    .context("could not create controller from filter proxy")?,
60                uninstalled_ip_routines: filter_routines(false /* installed */),
61                installed_ip_routines: filter_routines(true /* installed */),
62                current_installed_rule_index: 0,
63                masquerade: MasqueradeState {
64                    routine_id: masquerade_routine(),
65                    next_rule_index: 0,
66                },
67            }));
68            return Ok(filter);
69        }
70
71        Err(anyhow::anyhow!("no filtering proxy available!"))
72    }
73
74    /// Updates the initial network filter configuration using either
75    /// fuchsia.net.filter.deprecated or fuchsia.net.filter.
76    pub(super) async fn update_filters(
77        &mut self,
78        config: FilterConfig,
79        filter_enabled_state: &FilterEnabledState,
80    ) -> Result<(), anyhow::Error> {
81        match self {
82            FilterControl::Deprecated(proxy) => update_filters_deprecated(proxy, config).await,
83            FilterControl::Current(state) => {
84                state.update_filters_current(config, filter_enabled_state).await
85            }
86        }
87    }
88}
89
90// Filtering state for Masquerade NAT on the current `fuchsia.net.filter` API.
91struct MasqueradeState {
92    // The routine that holds all masquerade rules.
93    routine_id: RoutineId,
94    // The index to use for the next masquerade rule.
95    //
96    // Note: By using a simple counter, we don't re-use indices that were once
97    // used but are now available. The upside to this approach is that all
98    // filtering config has a stable order: older filtering config will always
99    // have a lower index (and therefore a higher priority) than newer filtering
100    // config. On the other hand, we do run the risk of overflowing the index
101    // if Netcfg were to add/remove u32::MAX filtering rules. That should only
102    // happen under pathological circumstances, and thus is a non-concern.
103    next_rule_index: u32,
104}
105
106// Filtering state on the current `fuchsia.net.filter` API.
107pub(super) struct FilterState {
108    controller: fnet_filter_ext::Controller,
109    uninstalled_ip_routines: netfilter::parser::FilterRoutines,
110    installed_ip_routines: netfilter::parser::FilterRoutines,
111    masquerade: MasqueradeState,
112    current_installed_rule_index: u32,
113    // TODO(https://fxbug.dev/331469354): Add NAT routines when this
114    // functionality has been added to fuchsia.net.filter.
115}
116
117impl FilterState {
118    // Commit the initial filter state using fuchsia.net.filter.
119    async fn update_filters_current(
120        &mut self,
121        config: FilterConfig,
122        filter_enabled_state: &FilterEnabledState,
123    ) -> Result<(), anyhow::Error> {
124        let FilterState {
125            controller,
126            uninstalled_ip_routines,
127            installed_ip_routines,
128            current_installed_rule_index,
129            masquerade,
130        } = self;
131        let (changes, num_installed_rules) = generate_initial_filter_changes(
132            uninstalled_ip_routines,
133            installed_ip_routines,
134            &masquerade.routine_id,
135            config,
136            &filter_enabled_state.interface_types,
137            *current_installed_rule_index,
138        )?;
139
140        for batch in changes.chunks(usize::from(fnet_filter::MAX_BATCH_SIZE)) {
141            controller
142                .push_changes(batch.to_vec())
143                .await
144                .context("failed to push changes to filter controller")?;
145        }
146
147        controller.commit().await.context("failed to commit changes to filter controller")?;
148        info!("initial filter configuration has been committed successfully");
149        *current_installed_rule_index =
150            current_installed_rule_index.wrapping_add(num_installed_rules);
151        Ok(())
152    }
153}
154
155// Netcfg's `FilterRoutines` to maintain the same namespace
156// and routine for each of the filter `Rule`s across installed
157// and uninstalled routines.
158fn filter_routines(installed: bool) -> netfilter::parser::FilterRoutines {
159    let suffix = if !installed { "_uninstalled" } else { "" };
160    netfilter::parser::FilterRoutines {
161        local_ingress: Some(RoutineId {
162            namespace: namespace_id(),
163            name: format!("local_ingress{suffix}"),
164        }),
165        local_egress: Some(RoutineId {
166            namespace: namespace_id(),
167            name: format!("local_egress{suffix}"),
168        }),
169    }
170}
171
172// Netcfg's masquerade NAT `RoutineId`.
173//
174// Masquerade NAT rules are always installed at the EGRESS hook.
175fn masquerade_routine() -> RoutineId {
176    RoutineId { namespace: namespace_id(), name: format!("egress_masquerade") }
177}
178
179fn namespace_id() -> NamespaceId {
180    NamespaceId(String::from("netcfg"))
181}
182
183pub(super) async fn probe_for_presence(filter: &fnet_filter_deprecated::FilterProxy) -> bool {
184    match filter.check_presence().await {
185        Ok(()) => true,
186        Err(fidl::Error::ClientChannelClosed { .. }) => false,
187        Err(e) => panic!("unexpected error while probing: {e}"),
188    }
189}
190
191// Create a set of `fnet_filter_ext::Change`s that, when used with
192// `fnet_filter_ext::Controller`, will establish the initial filtering
193// state for the `netcfg` namespace.
194fn generate_initial_filter_changes(
195    uninstalled_ip_routines: &netfilter::parser::FilterRoutines,
196    installed_ip_routines: &netfilter::parser::FilterRoutines,
197    masquerade_routine: &RoutineId,
198    config: FilterConfig,
199    filter_enabled_interface_types: &HashSet<InterfaceType>,
200    current_installed_rule_index: u32,
201) -> Result<(Vec<Change>, u32), anyhow::Error> {
202    let namespace = Change::Create(Resource::Namespace(Namespace {
203        id: NamespaceId(String::from("netcfg")),
204        domain: Domain::AllIp,
205    }));
206    let mut changes = vec![namespace];
207
208    // Create uninstalled `Routine`s that the installed `Routine`s can use to
209    // `Jump` to `Rule`s. There must be a separate uninstalled `Routine` for
210    // each `IpHook` so that there are not issues with a `Rule` containing
211    // a matcher that is not allowed in the installed `Routine`'s hook.
212    // E.g., A `Rule` in an installed ingress hook `Routine` that `Jump`s to a
213    // `Routine` with a `Rule` that specifies an out_interface matcher is not
214    // permitted.
215    let netfilter::parser::FilterRoutines { local_ingress, local_egress } = uninstalled_ip_routines;
216    let uninstalled_local_ingress =
217        local_ingress.clone().map(|id| Routine { id, routine_type: RoutineType::Ip(None) });
218    let uninstalled_local_egress =
219        local_egress.clone().map(|id| Routine { id, routine_type: RoutineType::Ip(None) });
220
221    // Push installed routines so that netcfg can install `Jump` rules
222    // at interface installation time that are rooted in these routines.
223    fn installed_routine_from_id(id: RoutineId, hook: IpHook) -> Routine {
224        Routine {
225            id: id,
226            routine_type: RoutineType::Ip(Some(InstalledIpRoutine { hook, priority: 0i32 })),
227        }
228    }
229    let netfilter::parser::FilterRoutines { local_ingress, local_egress } = installed_ip_routines;
230    let local_ingress =
231        local_ingress.clone().map(|id| installed_routine_from_id(id, IpHook::LocalIngress));
232    let local_egress =
233        local_egress.clone().map(|id| installed_routine_from_id(id, IpHook::LocalEgress));
234
235    let masquerade = Routine {
236        id: masquerade_routine.clone(),
237        routine_type: RoutineType::Nat(Some(InstalledNatRoutine {
238            hook: NatHook::Egress,
239            priority: 0i32,
240        })),
241    };
242
243    let routine_changes = [
244        uninstalled_local_ingress,
245        local_ingress,
246        uninstalled_local_egress,
247        local_egress,
248        Some(masquerade),
249    ]
250    .into_iter()
251    .filter_map(|routine| routine)
252    .map(|routine| Change::Create(Resource::Routine(routine)));
253    changes.extend(routine_changes);
254
255    // TODO(https://fxbug.dev/331469354): Handle NAT and NAT RDR rules when supported
256    // by netfilter and filtering library
257    let FilterConfig { rules, nat_rules: _, rdr_rules: _ } = config;
258    if !rules.is_empty() {
259        // Only insert the rules from the config into the uninstalled routine.
260        // The rules inserted in the installed routines will be intended for
261        // redirection to the uninstalled routines.
262        let rules =
263            netfilter::parser::parse_str_to_rules(&rules.join(""), &uninstalled_ip_routines)
264                .context("error parsing filter rules")?;
265        let rule_changes = rules.into_iter().map(|rule| Change::Create(Resource::Rule(rule)));
266        changes.extend(rule_changes);
267    }
268
269    // TODO(https://fxbug.dev/530218539): Add PortClass filtering for
270    // interfaces other than LoWPAN once NS2/filter.deprecated is removed.
271    let mut num_installed_rules = 0;
272    if filter_enabled_interface_types.contains(&InterfaceType::Lowpan) {
273        let lowpan_rules = generate_static_port_class_filter_rules(
274            uninstalled_ip_routines,
275            installed_ip_routines,
276            fnet_interfaces_ext::PortClass::Lowpan,
277            current_installed_rule_index.wrapping_add(num_installed_rules),
278        );
279        changes.extend(lowpan_rules.into_iter().map(|rule| Change::Create(Resource::Rule(rule))));
280        num_installed_rules += 1;
281    }
282
283    Ok((changes, num_installed_rules))
284}
285
286// Create a list of `fnet_filter_ext::Rule`s that, when used with
287// `fnet_filter_ext::Controller`, will `Jump` on each available
288// `IpHook` to the corresponding uninstalled routine for
289// that `IpHook`.
290fn generate_updated_filter_rules(
291    uninstalled_ip_routines: &netfilter::parser::FilterRoutines,
292    installed_ip_routines: &netfilter::parser::FilterRoutines,
293    interface_id: InterfaceId,
294    current_installed_rule_index: u32,
295) -> Vec<Rule> {
296    let netfilter::parser::FilterRoutines {
297        local_ingress: uninstalled_local_ingress,
298        local_egress: uninstalled_local_egress,
299    } = uninstalled_ip_routines;
300    let netfilter::parser::FilterRoutines { local_ingress, local_egress } = installed_ip_routines;
301
302    // Use the same rule index for all rules created for the
303    // interface. It is assumed that all `Rule`s across the
304    // `FilterRoutines` will inserted in tandem.
305    let local_ingress_rule = local_ingress.clone().map(|routine_id| {
306        create_interface_matching_jump_rule(
307            routine_id,
308            current_installed_rule_index,
309            interface_id,
310            IpHook::LocalIngress,
311            &uninstalled_local_ingress
312                .as_ref()
313                .expect("there should be a corresponding uninstalled routine for local ingress")
314                .name,
315        )
316    });
317    let local_egress_rule = local_egress.clone().map(|routine_id| {
318        create_interface_matching_jump_rule(
319            routine_id,
320            current_installed_rule_index,
321            interface_id,
322            IpHook::LocalEgress,
323            &uninstalled_local_egress
324                .as_ref()
325                .expect("there should be a corresponding uninstalled routine for local egress")
326                .name,
327        )
328    });
329
330    [local_ingress_rule, local_egress_rule].into_iter().flatten().collect()
331}
332
333fn create_jump_rule(
334    routine_id: RoutineId,
335    index: u32,
336    interface: fnet_matchers_ext::Interface,
337    hook: IpHook,
338    target_routine_name: &str,
339) -> Rule {
340    let (in_interface, out_interface) = match hook {
341        IpHook::LocalIngress | IpHook::Ingress => (Some(interface), None),
342        IpHook::LocalEgress | IpHook::Egress => (None, Some(interface)),
343        IpHook::Forwarding => (Some(interface.clone()), Some(interface)),
344    };
345
346    Rule {
347        id: RuleId { routine: routine_id, index },
348        matchers: Matchers { in_interface, out_interface, ..Default::default() },
349        action: Action::Jump(target_routine_name.to_string()),
350    }
351}
352
353fn create_interface_matching_jump_rule(
354    routine_id: RoutineId,
355    index: u32,
356    interface_id: InterfaceId,
357    hook: IpHook,
358    target_routine_name: &str,
359) -> Rule {
360    create_jump_rule(
361        routine_id,
362        index,
363        fnet_matchers_ext::Interface::Id(interface_id.into()),
364        hook,
365        target_routine_name,
366    )
367}
368
369/// Generates static filter rules (jump rules) for a given `PortClass` (specifically Lowpan) to
370/// redirect traffic to uninstalled routines.
371fn generate_static_port_class_filter_rules(
372    uninstalled_ip_routines: &netfilter::parser::FilterRoutines,
373    installed_ip_routines: &netfilter::parser::FilterRoutines,
374    port_class: fnet_interfaces_ext::PortClass,
375    current_installed_rule_index: u32,
376) -> Vec<Rule> {
377    let netfilter::parser::FilterRoutines {
378        local_ingress: uninstalled_local_ingress,
379        local_egress: uninstalled_local_egress,
380    } = uninstalled_ip_routines;
381    let netfilter::parser::FilterRoutines { local_ingress, local_egress } = installed_ip_routines;
382
383    let local_ingress_rule = local_ingress.clone().map(|routine_id| {
384        create_port_class_matching_jump_rule(
385            routine_id,
386            current_installed_rule_index,
387            port_class,
388            IpHook::LocalIngress,
389            &uninstalled_local_ingress
390                .as_ref()
391                .expect("there should be a corresponding uninstalled routine for local ingress")
392                .name,
393        )
394    });
395    let local_egress_rule = local_egress.clone().map(|routine_id| {
396        create_port_class_matching_jump_rule(
397            routine_id,
398            current_installed_rule_index,
399            port_class,
400            IpHook::LocalEgress,
401            &uninstalled_local_egress
402                .as_ref()
403                .expect("there should be a corresponding uninstalled routine for local egress")
404                .name,
405        )
406    });
407
408    [local_ingress_rule, local_egress_rule].into_iter().flatten().collect()
409}
410
411/// Helper to create a single rule matching a `PortClass` on input/output interfaces depending on
412/// the hook.
413fn create_port_class_matching_jump_rule(
414    routine_id: RoutineId,
415    index: u32,
416    port_class: fnet_interfaces_ext::PortClass,
417    hook: IpHook,
418    target_routine_name: &str,
419) -> Rule {
420    create_jump_rule(
421        routine_id,
422        index,
423        fnet_matchers_ext::Interface::PortClass(port_class),
424        hook,
425        target_routine_name,
426    )
427}
428
429// We use Compare-And-Swap (CAS) protocol to update filter rules. $get_rules returns the current
430// generation number. $update_rules will send it with new rules to make sure we are updating the
431// intended generation. If the generation number doesn't match, $update_rules will return a
432// GenerationMismatch error, then we have to restart from $get_rules.
433
434pub(crate) const FILTER_CAS_RETRY_MAX: i32 = 3;
435pub(crate) const FILTER_CAS_RETRY_INTERVAL_MILLIS: i64 = 500;
436
437macro_rules! cas_filter_rules {
438    ($filter:expr, $get_rules:ident, $update_rules:ident, $rules:expr, $error_type:ident) => {
439        for retry in 0..FILTER_CAS_RETRY_MAX {
440            let (_rules, generation) =
441                $filter.$get_rules().await.unwrap_or_else(|err| exit_with_fidl_error(err));
442
443            match $filter
444                .$update_rules(&$rules, generation)
445                .await
446                .unwrap_or_else(|err| exit_with_fidl_error(err))
447            {
448                Ok(()) => {
449                    break;
450                }
451                Err(fnet_filter_deprecated::$error_type::GenerationMismatch)
452                    if retry < FILTER_CAS_RETRY_MAX - 1 =>
453                {
454                    fuchsia_async::Timer::new(
455                        zx::MonotonicDuration::from_millis(FILTER_CAS_RETRY_INTERVAL_MILLIS)
456                            .after_now(),
457                    )
458                    .await;
459                }
460                Err(e) => {
461                    bail!("{} failed: {:?}", stringify!($update_rules), e);
462                }
463            }
464        }
465    };
466}
467
468// This is a placeholder macro while some update operations are not supported.
469macro_rules! no_update_filter_rules {
470    ($filter:expr, $get_rules:ident, $update_rules:ident, $rules:expr, $error_type:ident) => {
471        let (_rules, generation) =
472            $filter.$get_rules().await.unwrap_or_else(|err| exit_with_fidl_error(err));
473
474        match $filter
475            .$update_rules(&$rules, generation)
476            .await
477            .unwrap_or_else(|err| exit_with_fidl_error(err))
478        {
479            Ok(()) => {}
480            Err(fnet_filter_deprecated::$error_type::NotSupported) => {
481                error!("{} not supported", stringify!($update_rules));
482            }
483        }
484    };
485}
486
487async fn update_filters_deprecated(
488    filter: &mut fnet_filter_deprecated::FilterProxy,
489    config: FilterConfig,
490) -> Result<(), anyhow::Error> {
491    let FilterConfig { rules, nat_rules, rdr_rules } = config;
492
493    if !rules.is_empty() {
494        let rules = netfilter::parser_deprecated::parse_str_to_rules(&rules.join(""))
495            .context("error parsing filter rules")?;
496        cas_filter_rules!(filter, get_rules, update_rules, rules, FilterUpdateRulesError);
497    }
498
499    if !nat_rules.is_empty() {
500        let nat_rules = netfilter::parser_deprecated::parse_str_to_nat_rules(&nat_rules.join(""))
501            .context("error parsing NAT rules")?;
502        cas_filter_rules!(
503            filter,
504            get_nat_rules,
505            update_nat_rules,
506            nat_rules,
507            FilterUpdateNatRulesError
508        );
509    }
510
511    if !rdr_rules.is_empty() {
512        let rdr_rules = netfilter::parser_deprecated::parse_str_to_rdr_rules(&rdr_rules.join(""))
513            .context("error parsing RDR rules")?;
514        // TODO(https://fxbug.dev/42147284): Change this to cas_filter_rules once
515        // update is supported.
516        no_update_filter_rules!(
517            filter,
518            get_rdr_rules,
519            update_rdr_rules,
520            rdr_rules,
521            FilterUpdateRdrRulesError
522        );
523    }
524
525    Ok(())
526}
527
528#[derive(Debug)]
529struct MasqueradeCounter(NonZeroU64);
530
531impl MasqueradeCounter {
532    fn new() -> Self {
533        Self(NonZeroU64::new(1).unwrap())
534    }
535
536    fn increment(&mut self) {
537        *self = Self(self.0.checked_add(1).expect("integer_overflow on u64"));
538    }
539
540    fn decrement(&self) -> Option<Self> {
541        NonZeroU64::new(self.0.get() - 1).map(Self)
542    }
543}
544
545#[derive(Debug, Default)]
546pub(super) struct FilterEnabledState {
547    interface_types: HashSet<InterfaceType>,
548    // A map of interface ID to the number of active masquerade configurations
549    // applied on that interface.
550    masquerade_enabled_interface_ids: HashMap<InterfaceId, MasqueradeCounter>,
551    // Indexed by interface id and stores `RuleId`s inserted for that interface.
552    // All rules for an interface should be removed upon interface removal.
553    // Vec will always be empty when using filter.deprecated.
554    //
555    // Note: Masquerade rules are not held here. Filtering on an interface can
556    // only be disabled when there are no masquerade configurations remaining
557    // (i.e. absence of an `InterfaceId` from `masquerade_enabled_interface_ids`
558    // is proof that there are no installed Masquerade Rules on the interface).
559    currently_enabled_interfaces: HashMap<InterfaceId, Vec<RuleId>>,
560}
561
562impl FilterEnabledState {
563    pub(super) fn new(interface_types: HashSet<InterfaceType>) -> Self {
564        Self { interface_types, ..Default::default() }
565    }
566
567    /// Updates the filter state for the provided `interface_id` using either
568    /// fuchsia.net.filter.deprecated or fuchsia.net.filter.
569    ///
570    /// `interface_type`: The type of the given interface. If the type cannot be
571    /// determined, this will be None, and `FilterEnabledState::interface_types`
572    /// will be ignored.
573    pub(super) async fn maybe_update(
574        &mut self,
575        interface_type: Option<InterfaceType>,
576        interface_id: InterfaceId,
577        filter: &mut FilterControl,
578    ) -> Result<(), anyhow::Error> {
579        match filter {
580            FilterControl::Deprecated(proxy) => self
581                .maybe_update_deprecated(interface_type, interface_id, proxy)
582                .await
583                .map_err(|e| anyhow::anyhow!("{e:?}")),
584            FilterControl::Current(filter_state) => self
585                .maybe_update_current(interface_type, interface_id, filter_state)
586                .await
587                .map_err(|e| anyhow::anyhow!("{e:?}")),
588        }
589    }
590
591    /// Clears tracking and masquerade counts for a removed interface.
592    /// Netstack automatically destroys the rules on interface removal.
593    pub(super) fn remove_interface(&mut self, interface_id: InterfaceId) {
594        let _removed_rules: Option<Vec<RuleId>> =
595            self.currently_enabled_interfaces.remove(&interface_id);
596        let _removed_count: Option<MasqueradeCounter> =
597            self.masquerade_enabled_interface_ids.remove(&interface_id);
598    }
599
600    pub(super) async fn maybe_update_deprecated<
601        Filter: fnet_filter_deprecated::FilterProxyInterface,
602    >(
603        &mut self,
604        interface_type: Option<InterfaceType>,
605        interface_id: InterfaceId,
606        filter: &Filter,
607    ) -> Result<(), fnet_filter_deprecated::EnableDisableInterfaceError> {
608        let should_be_enabled = self.should_enable(interface_type, interface_id);
609        let is_enabled = self.currently_enabled_interfaces.entry(interface_id);
610
611        match (should_be_enabled, is_enabled) {
612            (true, Entry::Vacant(entry)) => {
613                if let Err(e) = filter
614                    .enable_interface(interface_id.get())
615                    .await
616                    .unwrap_or_else(|err| exit_with_fidl_error(err))
617                {
618                    warn!("failed to enable interface {interface_id}: {e:?}");
619                    return Err(e);
620                }
621                let _ = entry.insert(vec![]);
622            }
623            (false, Entry::Occupied(entry)) => {
624                if let Err(e) = filter
625                    .disable_interface(interface_id.get())
626                    .await
627                    .unwrap_or_else(|err| exit_with_fidl_error(err))
628                {
629                    warn!("failed to disable interface {interface_id}: {e:?}");
630                    return Err(e);
631                }
632                let _ = entry.remove();
633            }
634            (true, Entry::Occupied(_)) | (false, Entry::Vacant(_)) => {
635                // Do nothing. The interface's current state aligns with
636                // whether it is present in the map.
637            }
638        }
639        Ok(())
640    }
641
642    pub(super) async fn maybe_update_current(
643        &mut self,
644        interface_type: Option<InterfaceType>,
645        interface_id: InterfaceId,
646        filter: &mut FilterState,
647    ) -> Result<(), FilterError> {
648        let should_be_enabled = self.should_enable(interface_type, interface_id);
649        let is_enabled = self.currently_enabled_interfaces.entry(interface_id);
650
651        match (should_be_enabled, is_enabled) {
652            (true, Entry::Vacant(entry)) => {
653                let FilterState {
654                    controller,
655                    uninstalled_ip_routines,
656                    installed_ip_routines,
657                    current_installed_rule_index,
658                    masquerade: _,
659                } = filter;
660                let rules = generate_updated_filter_rules(
661                    uninstalled_ip_routines,
662                    installed_ip_routines,
663                    interface_id,
664                    *current_installed_rule_index,
665                );
666
667                if !rules.is_empty() {
668                    let rule_changes = rules
669                        .clone()
670                        .into_iter()
671                        .map(|rule| Change::Create(Resource::Rule(rule)))
672                        .collect();
673                    controller.push_changes(rule_changes).await.map_err(FilterError::Push)?;
674                    controller.commit().await.map_err(FilterError::Commit)?;
675                    info!(
676                        "new filter rules for iface with id {interface_id:?} \
677                                have been committed successfully"
678                    );
679                    // Increment the current rule index only on success since
680                    // `commit` will either apply changes in entirety, or none
681                    // at all.
682                    *current_installed_rule_index = current_installed_rule_index.wrapping_add(1);
683                }
684
685                // Get the `RuleId`s from the inserted `Rule`s so that they can be
686                // removed if the interface is disabled.
687                let rule_ids: Vec<_> = rules.into_iter().map(|rule| rule.id).collect();
688                let _ = entry.insert(rule_ids);
689            }
690            (false, Entry::Occupied(entry)) => {
691                let FilterState { controller, .. } = filter;
692                let rule_changes: Vec<_> = entry
693                    .remove()
694                    .into_iter()
695                    .map(|rule_id| Change::Remove(ResourceId::Rule(rule_id)))
696                    .collect();
697
698                if !rule_changes.is_empty() {
699                    controller.push_changes(rule_changes).await.map_err(FilterError::Push)?;
700                    controller.commit().await.map_err(FilterError::Commit)?;
701                    info!(
702                        "removal of filter rules for iface with id {interface_id:?} \
703                                have been committed successfully"
704                    );
705                }
706            }
707            (true, Entry::Occupied(_)) | (false, Entry::Vacant(_)) => {
708                // Do nothing. The interface's current state aligns with
709                // whether it is present in the map.
710            }
711        }
712
713        Ok(())
714    }
715
716    /// Determines whether a given `interface_id` should be enabled.
717    ///
718    /// `interface_type`: The type of the given interface. If the type cannot be
719    /// determined, this will be None, and `FilterEnabledState::interface_types`
720    /// will be ignored.
721    fn should_enable(
722        &self,
723        interface_type: Option<InterfaceType>,
724        interface_id: InterfaceId,
725    ) -> bool {
726        interface_type
727            .as_ref()
728            .map(|ty| match ty {
729                InterfaceType::WlanClient
730                | InterfaceType::Ethernet
731                | InterfaceType::Blackhole
732                | InterfaceType::Lowpan => self.interface_types.contains(ty),
733                // An AP device can be filtered by specifying AP or WLAN.
734                InterfaceType::WlanAp => {
735                    self.interface_types.contains(ty)
736                        | self.interface_types.contains(&InterfaceType::WlanClient)
737                }
738            })
739            .unwrap_or(false)
740            || self.masquerade_enabled_interface_ids.contains_key(&interface_id)
741    }
742
743    pub(crate) fn increment_masquerade_count_on_interface(&mut self, interface_id: InterfaceId) {
744        match self.masquerade_enabled_interface_ids.entry(interface_id) {
745            Entry::Vacant(entry) => {
746                let _new_count = entry.insert(MasqueradeCounter::new());
747            }
748            Entry::Occupied(mut entry) => entry.get_mut().increment(),
749        }
750    }
751
752    pub(crate) fn decrement_masquerade_count_on_interface(&mut self, interface_id: InterfaceId) {
753        match self.masquerade_enabled_interface_ids.entry(interface_id) {
754            Entry::Vacant(_) => panic!(
755                "asked to decrement the masquerade count for a non-configured interface: {}",
756                interface_id
757            ),
758            Entry::Occupied(mut entry) => match entry.get().decrement() {
759                // Subtraction made the count 0; remove it.
760                None => {
761                    let _old_count = entry.remove();
762                }
763                Some(count) => {
764                    let _old_count = entry.insert(count);
765                }
766            },
767        }
768    }
769}
770
771/// Repeatedly attempts to update the NAT rules using `fuchsia.net.filter.deprecated`.
772///
773/// The update will be attempted up to `FILTER_CAS_RETRY_MAX` times.
774async fn update_nat_rules_deprecated(
775    filter: &mut fnet_filter_deprecated::FilterProxy,
776    update_fn: impl Fn(&mut Vec<fnet_filter_deprecated::Nat>) -> Result<(), fnet_masquerade::Error>,
777) -> Result<(), fnet_masquerade::Error> {
778    for _ in 0..FILTER_CAS_RETRY_MAX {
779        let (mut rules, generation) =
780            filter.get_nat_rules().await.expect("call to GetNatRules failed");
781        update_fn(&mut rules)?;
782
783        match filter
784            .update_nat_rules(&rules, generation)
785            .await
786            .expect("call to UpdateNatRules failed")
787        {
788            Ok(()) => return Ok(()),
789            Err(fnet_filter_deprecated::FilterUpdateNatRulesError::GenerationMismatch) => {
790                // We need to try again.
791                fuchsia_async::Timer::new(
792                    zx::MonotonicDuration::from_millis(
793                        crate::filter::FILTER_CAS_RETRY_INTERVAL_MILLIS,
794                    )
795                    .after_now(),
796                )
797                .await;
798            }
799            Err(fnet_filter_deprecated::FilterUpdateNatRulesError::BadRule) => {
800                // This can sometimes be triggered if the NIC is deleted before
801                // the call to `update_nat_rules`.
802                error!(
803                    "Generated Nat rule was invalid. Perhaps the requested \
804                     NIC has been removed {rules:?}"
805                );
806                // There is no point in retrying in this case.
807                return Err(fnet_masquerade::Error::BadRule);
808            }
809        }
810    }
811
812    error!("Failed to update Nat rules");
813    Err(fnet_masquerade::Error::RetryExceeded)
814}
815
816// Attempts to add a new masquerade NAT rule using `fuchsia.net.filter.deprecated`.
817pub(crate) async fn add_masquerade_rule_deprecated(
818    filter: &mut fnet_filter_deprecated::FilterProxy,
819    rule: fnet_filter_deprecated::Nat,
820) -> Result<(), fnet_masquerade::Error> {
821    update_nat_rules_deprecated(filter, |rules| {
822        if rules.iter().any(|existing_rule| existing_rule == &rule) {
823            Err(fnet_masquerade::Error::AlreadyExists)
824        } else {
825            rules.push(rule.clone());
826            Ok(())
827        }
828    })
829    .await
830}
831
832// Attempts to remove an existing masquerade NAT rule using `fuchsia.net.filter.deprecated`.
833pub(crate) async fn remove_masquerade_rule_deprecated(
834    filter: &mut fnet_filter_deprecated::FilterProxy,
835    rule: fnet_filter_deprecated::Nat,
836) -> Result<(), fnet_masquerade::Error> {
837    update_nat_rules_deprecated(filter, |rules| {
838        rules.retain(|existing_rule| existing_rule != &rule);
839        Ok(())
840    })
841    .await
842}
843
844// Attempts to add a new masquerade NAT rule using `fuchsia.net.filter`.
845pub(crate) async fn add_masquerade_rule_current(
846    filter: &mut FilterState,
847    matchers: Matchers,
848) -> Result<RuleId, FilterError> {
849    let MasqueradeState { routine_id, next_rule_index } = &mut filter.masquerade;
850    let rule_id = RuleId { routine: routine_id.clone(), index: *next_rule_index };
851    let rule_changes = vec![Change::Create(Resource::Rule(Rule {
852        id: rule_id.clone(),
853        matchers: matchers,
854        action: Action::Masquerade { src_port: None },
855    }))];
856    filter.controller.push_changes(rule_changes).await.map_err(FilterError::Push)?;
857    filter.controller.commit().await.map_err(FilterError::Commit)?;
858    *next_rule_index += 1;
859    Ok(rule_id)
860}
861
862// Attempts to remove an existing masquerade NAT rule using `fuchsia.net.filter`.
863pub(crate) async fn remove_masquerade_rule_current(
864    filter: &mut FilterState,
865    rule: &RuleId,
866) -> Result<(), FilterError> {
867    let rule_changes = vec![Change::Remove(ResourceId::Rule(rule.clone()))];
868    filter.controller.push_changes(rule_changes).await.map_err(FilterError::Push)?;
869    filter.controller.commit().await.map_err(FilterError::Commit)
870}
871
872#[cfg(test)]
873mod tests {
874    use test_case::test_case;
875
876    use crate::DeviceClass;
877    use crate::interface::DeviceInfoRef;
878
879    use super::*;
880
881    const INTERFACE_ID: InterfaceId = InterfaceId::new(1).unwrap();
882    const LOCAL_INGRESS: &str = "local_ingress";
883    const UNINSTALLED_LOCAL_INGRESS: &str = "local_ingress_uninstalled";
884    const LOCAL_EGRESS: &str = "local_egress";
885    const UNINSTALLED_LOCAL_EGRESS: &str = "local_egress_uninstalled";
886    const MASQUERADE: &str = "egress_masquerade";
887
888    fn get_foundational_changes() -> Vec<Change> {
889        let mut changes = vec![Change::Create(Resource::Namespace(Namespace {
890            id: namespace_id(),
891            domain: Domain::AllIp,
892        }))];
893
894        let local_ingress = (LOCAL_INGRESS, UNINSTALLED_LOCAL_INGRESS, IpHook::LocalIngress);
895        let local_egress = (LOCAL_EGRESS, UNINSTALLED_LOCAL_EGRESS, IpHook::LocalEgress);
896
897        let routine_changes = vec![local_ingress, local_egress]
898            .into_iter()
899            .map(|(installed_name, uninstalled_name, hook)| {
900                vec![
901                    Routine {
902                        id: RoutineId {
903                            namespace: namespace_id(),
904                            name: String::from(uninstalled_name),
905                        },
906                        routine_type: RoutineType::Ip(None),
907                    },
908                    Routine {
909                        id: RoutineId {
910                            namespace: namespace_id(),
911                            name: String::from(installed_name),
912                        },
913                        routine_type: RoutineType::Ip(Some(InstalledIpRoutine {
914                            hook,
915                            priority: 0i32,
916                        })),
917                    },
918                ]
919            })
920            .flatten()
921            .chain([Routine {
922                id: RoutineId { namespace: namespace_id(), name: String::from(MASQUERADE) },
923                routine_type: RoutineType::Nat(Some(InstalledNatRoutine {
924                    hook: NatHook::Egress,
925                    priority: 0i32,
926                })),
927            }])
928            .map(|routine| Change::Create(Resource::Routine(routine)));
929        changes.extend(routine_changes);
930
931        changes
932    }
933
934    fn create_rule(routine: RoutineId, index: u32, action: Action) -> Rule {
935        Rule { id: RuleId { routine, index }, matchers: Matchers::default(), action }
936    }
937
938    fn create_routine_id(name: &str) -> RoutineId {
939        RoutineId { namespace: namespace_id(), name: String::from(name) }
940    }
941
942    fn create_filter_routines(
943        namespace: NamespaceId,
944        local_ingress: &str,
945        local_egress: &str,
946    ) -> netfilter::parser::FilterRoutines {
947        netfilter::parser::FilterRoutines {
948            local_ingress: Some(RoutineId {
949                namespace: namespace.clone(),
950                name: local_ingress.to_owned(),
951            }),
952            local_egress: Some(RoutineId { namespace, name: local_egress.to_owned() }),
953        }
954    }
955
956    // This test only checks for `Ok` cases. The only possible failures for the function under
957    // test are related to Rule parsing, which the netfilter library already tests.
958    #[test_case(vec![], vec![]; "no_rules")]
959    #[test_case(
960        vec!["pass in;"],
961        vec![create_rule(
962                create_routine_id(UNINSTALLED_LOCAL_INGRESS),
963                0,
964                Action::Accept,
965            )]; "ingress_accept")]
966    #[test_case(
967        vec!["drop out;"],
968        vec![create_rule(
969                create_routine_id(UNINSTALLED_LOCAL_EGRESS),
970                0,
971                Action::Drop,
972            )]; "egress_drop")]
973    #[test_case(
974        vec!["pass in; drop out;"],
975        vec![create_rule(
976                create_routine_id(UNINSTALLED_LOCAL_INGRESS),
977                0,
978                Action::Accept),
979            create_rule(
980                create_routine_id(UNINSTALLED_LOCAL_EGRESS),
981                1,
982                Action::Drop,
983            )]; "ingress_accept_egress_drop")]
984    fn test_initial_filter_changes(rules_input: Vec<&str>, expected_rules: Vec<Rule>) {
985        let namespace = namespace_id();
986        let installed_filter_routines =
987            create_filter_routines(namespace.clone(), LOCAL_INGRESS, LOCAL_EGRESS);
988        let uninstalled_filter_routines =
989            create_filter_routines(namespace, UNINSTALLED_LOCAL_INGRESS, UNINSTALLED_LOCAL_EGRESS);
990
991        let (changes, _num_installed_rules) = generate_initial_filter_changes(
992            &uninstalled_filter_routines,
993            &installed_filter_routines,
994            &masquerade_routine(),
995            FilterConfig {
996                rules: rules_input.into_iter().map(|rule| rule.to_owned()).collect(),
997                nat_rules: vec![],
998                rdr_rules: vec![],
999            },
1000            &HashSet::new(),
1001            0,
1002        )
1003        .expect("rules should be formatted correctly");
1004
1005        let mut expected_changes = get_foundational_changes();
1006        let expected_rule_changes =
1007            expected_rules.into_iter().map(|rule| Change::Create(Resource::Rule(rule)));
1008        expected_changes.extend(expected_rule_changes);
1009
1010        assert_eq!(changes, expected_changes);
1011    }
1012
1013    #[test]
1014    fn test_generate_updated_filter_rules() {
1015        let namespace = namespace_id();
1016        let installed_filter_routines =
1017            create_filter_routines(namespace.clone(), LOCAL_INGRESS, LOCAL_EGRESS);
1018        let uninstalled_filter_routines =
1019            create_filter_routines(namespace, UNINSTALLED_LOCAL_INGRESS, UNINSTALLED_LOCAL_EGRESS);
1020
1021        let rules = generate_updated_filter_rules(
1022            &uninstalled_filter_routines,
1023            &installed_filter_routines,
1024            INTERFACE_ID,
1025            0,
1026        );
1027
1028        let local_ingress = (
1029            installed_filter_routines.local_ingress.unwrap(),
1030            uninstalled_filter_routines.local_ingress.unwrap().name,
1031            IpHook::LocalIngress,
1032        );
1033        let local_egress = (
1034            installed_filter_routines.local_egress.unwrap(),
1035            uninstalled_filter_routines.local_egress.unwrap().name,
1036            IpHook::LocalEgress,
1037        );
1038        let expected_rules: Vec<_> = vec![local_ingress, local_egress]
1039            .into_iter()
1040            .map(|(installed_routine, uninstalled_routine_name, hook)| {
1041                create_interface_matching_jump_rule(
1042                    installed_routine,
1043                    0,
1044                    INTERFACE_ID,
1045                    hook,
1046                    &uninstalled_routine_name,
1047                )
1048            })
1049            .collect();
1050
1051        assert_eq!(rules, expected_rules);
1052    }
1053
1054    #[test]
1055    fn test_generate_static_port_class_filter_rules() {
1056        let namespace = namespace_id();
1057        let installed_filter_routines =
1058            create_filter_routines(namespace.clone(), LOCAL_INGRESS, LOCAL_EGRESS);
1059        let uninstalled_filter_routines =
1060            create_filter_routines(namespace, UNINSTALLED_LOCAL_INGRESS, UNINSTALLED_LOCAL_EGRESS);
1061
1062        let rules = generate_static_port_class_filter_rules(
1063            &uninstalled_filter_routines,
1064            &installed_filter_routines,
1065            fnet_interfaces_ext::PortClass::Lowpan,
1066            0,
1067        );
1068
1069        let local_ingress = (
1070            installed_filter_routines.local_ingress.unwrap(),
1071            uninstalled_filter_routines.local_ingress.unwrap().name,
1072            IpHook::LocalIngress,
1073        );
1074        let local_egress = (
1075            installed_filter_routines.local_egress.unwrap(),
1076            uninstalled_filter_routines.local_egress.unwrap().name,
1077            IpHook::LocalEgress,
1078        );
1079        let expected_rules: Vec<_> = vec![local_ingress, local_egress]
1080            .into_iter()
1081            .map(|(installed_routine, uninstalled_routine_name, hook)| {
1082                create_port_class_matching_jump_rule(
1083                    installed_routine,
1084                    0,
1085                    fnet_interfaces_ext::PortClass::Lowpan,
1086                    hook,
1087                    &uninstalled_routine_name,
1088                )
1089            })
1090            .collect();
1091
1092        assert_eq!(rules, expected_rules);
1093    }
1094
1095    #[test]
1096    fn test_initial_filter_changes_with_lowpan() {
1097        let namespace = namespace_id();
1098        let installed_filter_routines =
1099            create_filter_routines(namespace.clone(), LOCAL_INGRESS, LOCAL_EGRESS);
1100        let uninstalled_filter_routines =
1101            create_filter_routines(namespace, UNINSTALLED_LOCAL_INGRESS, UNINSTALLED_LOCAL_EGRESS);
1102
1103        let (changes, num_installed_rules) = generate_initial_filter_changes(
1104            &uninstalled_filter_routines,
1105            &installed_filter_routines,
1106            &masquerade_routine(),
1107            FilterConfig { rules: vec![], nat_rules: vec![], rdr_rules: vec![] },
1108            &[InterfaceType::Lowpan].into_iter().collect(),
1109            0,
1110        )
1111        .expect("rules should be formatted correctly");
1112
1113        let mut expected_changes = get_foundational_changes();
1114
1115        let lowpan_rules = generate_static_port_class_filter_rules(
1116            &uninstalled_filter_routines,
1117            &installed_filter_routines,
1118            fnet_interfaces_ext::PortClass::Lowpan,
1119            0,
1120        );
1121        expected_changes
1122            .extend(lowpan_rules.into_iter().map(|rule| Change::Create(Resource::Rule(rule))));
1123
1124        assert_eq!(changes, expected_changes);
1125        assert_eq!(num_installed_rules, 1);
1126    }
1127
1128    #[test]
1129    fn test_should_enable_filter() {
1130        let types_empty: HashSet<InterfaceType> = [].iter().cloned().collect();
1131        let types_ethernet: HashSet<InterfaceType> =
1132            [InterfaceType::Ethernet].iter().cloned().collect();
1133        let types_wlan: HashSet<InterfaceType> =
1134            [InterfaceType::WlanClient].iter().cloned().collect();
1135        let types_ap: HashSet<InterfaceType> = [InterfaceType::WlanAp].iter().cloned().collect();
1136
1137        let id = InterfaceId::new(10).unwrap();
1138
1139        let make_info = |device_class| DeviceInfoRef {
1140            device_class,
1141            mac: &fidl_fuchsia_net_ext::MacAddress { octets: [0x1, 0x1, 0x1, 0x1, 0x1, 0x1] },
1142            topological_path: "",
1143        };
1144
1145        let wlan_info = make_info(DeviceClass::WlanClient);
1146        let wlan_ap_info = make_info(DeviceClass::WlanAp);
1147        let ethernet_info = make_info(DeviceClass::Ethernet);
1148
1149        let mut fes = FilterEnabledState::new(types_empty.clone());
1150        assert_eq!(fes.should_enable(Some(wlan_info.interface_type()), id), false);
1151        assert_eq!(fes.should_enable(Some(wlan_ap_info.interface_type()), id), false);
1152        assert_eq!(fes.should_enable(Some(ethernet_info.interface_type()), id), false);
1153
1154        fes.increment_masquerade_count_on_interface(id);
1155        assert_eq!(fes.should_enable(Some(ethernet_info.interface_type()), id), true);
1156
1157        let mut fes = FilterEnabledState::new(types_ethernet);
1158        assert_eq!(fes.should_enable(Some(wlan_info.interface_type()), id), false);
1159        assert_eq!(fes.should_enable(Some(wlan_ap_info.interface_type()), id), false);
1160        assert_eq!(fes.should_enable(Some(ethernet_info.interface_type()), id), true);
1161
1162        fes.increment_masquerade_count_on_interface(id);
1163        assert_eq!(fes.should_enable(Some(wlan_info.interface_type()), id), true);
1164
1165        let mut fes = FilterEnabledState::new(types_wlan);
1166        assert_eq!(fes.should_enable(Some(wlan_info.interface_type()), id), true);
1167        assert_eq!(fes.should_enable(Some(wlan_ap_info.interface_type()), id), true);
1168        assert_eq!(fes.should_enable(Some(ethernet_info.interface_type()), id), false);
1169
1170        fes.increment_masquerade_count_on_interface(id);
1171        assert_eq!(fes.should_enable(Some(ethernet_info.interface_type()), id), true);
1172
1173        let mut fes = FilterEnabledState::new(types_ap);
1174        assert_eq!(fes.should_enable(Some(wlan_info.interface_type()), id), false);
1175        assert_eq!(fes.should_enable(Some(wlan_ap_info.interface_type()), id), true);
1176        assert_eq!(fes.should_enable(Some(ethernet_info.interface_type()), id), false);
1177
1178        fes.increment_masquerade_count_on_interface(id);
1179        assert_eq!(fes.should_enable(Some(wlan_info.interface_type()), id), true);
1180        assert_eq!(fes.should_enable(Some(ethernet_info.interface_type()), id), true);
1181
1182        // Verify that the count can be decremented while keeping filtering enabled.
1183        let mut fes = FilterEnabledState::new(types_empty);
1184        for _ in 0..3 {
1185            fes.increment_masquerade_count_on_interface(id);
1186        }
1187        for expect_enabled in [true, true, false] {
1188            fes.decrement_masquerade_count_on_interface(id);
1189            assert_eq!(fes.should_enable(Some(wlan_info.interface_type()), id), expect_enabled);
1190            assert_eq!(fes.should_enable(Some(wlan_ap_info.interface_type()), id), expect_enabled);
1191            assert_eq!(fes.should_enable(Some(ethernet_info.interface_type()), id), expect_enabled);
1192        }
1193    }
1194
1195    #[fuchsia::test]
1196    async fn test_update_filters_current_large_batch() {
1197        use futures::StreamExt as _;
1198
1199        let (control_client, control_server) =
1200            fidl::endpoints::create_endpoints::<fnet_filter::ControlMarker>();
1201        let client_fut = FilterControl::new(None, Some(control_client.into_proxy()));
1202        let mut control_stream = control_server.into_stream();
1203        let control_server_fut = async move {
1204            match control_stream
1205                .next()
1206                .await
1207                .expect("stream shouldn't close")
1208                .expect("stream shouldn't have an error")
1209            {
1210                fnet_filter::ControlRequest::OpenController { id, request, control_handle: _ } => {
1211                    let (request_stream, control_handle) = request.into_stream_and_control_handle();
1212                    control_handle.send_on_id_assigned(id.as_str()).expect("failed to respond");
1213                    request_stream
1214                }
1215                _ => panic!("unexpected request"),
1216            }
1217        };
1218        let (filter_control, mut server_request_stream) =
1219            futures::join!(client_fut, control_server_fut);
1220        let mut filter_control = filter_control.expect("failed to create filter control");
1221
1222        let config = FilterConfig {
1223            rules: std::iter::repeat("pass in;".to_string()).take(50).collect(),
1224            nat_rules: vec![],
1225            rdr_rules: vec![],
1226        };
1227
1228        let server_fut = async move {
1229            let mut push_changes_count = 0;
1230            while let Some(req) = server_request_stream.next().await {
1231                match req.expect("stream shouldn't have an error") {
1232                    fnet_filter::NamespaceControllerRequest::PushChanges { changes, responder } => {
1233                        assert!(
1234                            changes.len() <= usize::from(fnet_filter::MAX_BATCH_SIZE),
1235                            "batch size {} exceeds MAX_BATCH_SIZE",
1236                            changes.len()
1237                        );
1238                        push_changes_count += 1;
1239                        responder
1240                            .send(fnet_filter::ChangeValidationResult::Ok(fnet_filter::Empty))
1241                            .expect("failed to respond");
1242                    }
1243                    fnet_filter::NamespaceControllerRequest::Commit { payload: _, responder } => {
1244                        responder
1245                            .send(fnet_filter::CommitResult::Ok(fnet_filter::Empty))
1246                            .expect("failed to respond");
1247                        break;
1248                    }
1249                    _ => panic!("unexpected request"),
1250                }
1251            }
1252            push_changes_count
1253        };
1254
1255        let filter_enabled_state = FilterEnabledState::default();
1256        let (client_res, push_changes_count) = futures::join!(
1257            filter_control.update_filters(config, &filter_enabled_state),
1258            server_fut
1259        );
1260
1261        client_res.expect("update_filters should succeed");
1262        assert_eq!(push_changes_count, 2);
1263    }
1264}