netcfg/
interface.rs

1// Copyright 2018 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 either::Either;
6use serde::{Deserialize, Deserializer};
7use std::collections::{HashMap, HashSet};
8use std::sync::atomic::{AtomicU32, Ordering};
9
10use fidl_fuchsia_net_interfaces_admin as fnet_interfaces_admin;
11
12use crate::DeviceClass;
13
14const INTERFACE_PREFIX_WLAN: &str = "wlan";
15const INTERFACE_PREFIX_ETHERNET: &str = "eth";
16const INTERFACE_PREFIX_AP: &str = "ap";
17const INTERFACE_PREFIX_BLACKHOLE: &str = "blackhole";
18
19// Interfaces with different InterfaceNamingIdentifiers are expected to have
20// different names.
21//
22// If two interfaces have the same MAC, they are expected to produce a different
23// name in two possible ways: 1) have a different port class, or 2) use the
24// NormalizedMac naming scheme (which avoids conflicts via retries).
25#[derive(PartialEq, Eq, Debug, Clone, Hash)]
26pub(crate) struct InterfaceNamingIdentifier {
27    pub(crate) mac: fidl_fuchsia_net_ext::MacAddress,
28    pub(crate) topological_path: String,
29}
30
31pub(crate) fn generate_identifier(
32    mac_address: &fidl_fuchsia_net_ext::MacAddress,
33    topological_path: &str,
34) -> InterfaceNamingIdentifier {
35    InterfaceNamingIdentifier { mac: *mac_address, topological_path: topological_path.to_string() }
36}
37
38// Get the NormalizedMac using the last octet of the MAC address. The offset
39// modifies the last_byte in an attempt to avoid naming conflicts.
40// For example, a MAC of `[0x1, 0x1, 0x1, 0x1, 0x1, 0x9]` with offset 0
41// becomes `9`.
42fn get_mac_identifier_from_octets(
43    octets: &[u8; 6],
44    interface_type: crate::InterfaceType,
45    offset: u8,
46) -> Result<u8, anyhow::Error> {
47    if offset == u8::MAX {
48        return Err(anyhow::format_err!(
49            "could not find unique identifier for mac={:?}, interface_type={:?}",
50            octets,
51            interface_type
52        ));
53    }
54
55    let last_byte = octets[octets.len() - 1];
56    let (identifier, _) = last_byte.overflowing_add(offset);
57    Ok(identifier)
58}
59
60// Get the normalized bus path for a topological path.
61// For example, a PCI device at `02:00.1` becomes `02001`.
62// At the time of writing, typical topological paths appear similar to:
63//
64// PCI:
65// "/dev/sys/platform/pt/PCI0/bus/02:00.0/02:00.0/e1000/ethernet"
66//
67// USB over PCI:
68// "/dev/sys/platform/pt/PCI0/bus/00:14.0/00:14.0/xhci/usb/007/ifc-000/<snip>/wlan/wlan-ethernet/ethernet"
69// 00:14:0 following "/PCI0/bus/" represents BDF (Bus Device Function)
70//
71// USB over DWC:
72// "/dev/sys/platform/05:00:18/usb-phy-composite/aml_usb_phy/dwc2/dwc2_phy/dwc2/usb-peripheral/function-000/cdc-eth-function/netdevice-migration/network-device"
73// 05:00:18 following "platform" represents
74// vid(vendor id):pid(product id):did(device id) and are defined in each board file
75//
76// SDIO
77// "/dev/sys/platform/05:00:6/aml-sd-emmc/sdio/broadcom-wlanphy/wlanphy"
78// 05:00:6 following "platform" represents
79// vid(vendor id):pid(product id):did(device id) and are defined in each board file
80//
81// Ethernet Jack for VIM2
82// "/dev/sys/platform/04:02:7/aml-ethernet/Designware-MAC/ethernet"
83//
84// VirtIo
85// "/dev/sys/platform/pt/PC00/bus/00:1e.0/00_1e_0/virtio-net/network-device"
86//
87// Since there is no real standard for topological paths, when no bus path can be found,
88// the function attempts to return one that is unlikely to conflict with any existing path
89// by assuming a bus path of ff:ff:ff, and decrementing from there. This permits
90// generating unique, well-formed names in cases where a matching path component can't be
91// found, while also being relatively recognizable as exceptional.
92fn get_normalized_bus_path_for_topo_path(topological_path: &str) -> String {
93    static PATH_UNIQ_MARKER: AtomicU32 = AtomicU32::new(0xffffff);
94    topological_path
95        .split("/")
96        .find(|pc| {
97            pc.len() >= 7 && pc.chars().all(|c| c.is_digit(16) || c == ':' || c == '.' || c == '_')
98        })
99        .and_then(|s| {
100            Some(s.replace(&[':', '.', '_'], "").trim_end_matches(|c| c == '0').to_string())
101        })
102        .unwrap_or_else(|| format!("{:01$x}", PATH_UNIQ_MARKER.fetch_sub(1, Ordering::SeqCst), 6))
103}
104
105#[derive(Debug)]
106pub struct InterfaceNamingConfig {
107    naming_rules: Vec<NamingRule>,
108    interfaces: HashMap<InterfaceNamingIdentifier, String>,
109}
110
111impl InterfaceNamingConfig {
112    pub(crate) fn from_naming_rules(naming_rules: Vec<NamingRule>) -> InterfaceNamingConfig {
113        InterfaceNamingConfig { naming_rules, interfaces: HashMap::new() }
114    }
115
116    /// Returns a stable interface name for the specified interface.
117    pub(crate) fn generate_stable_name(
118        &mut self,
119        topological_path: &str,
120        mac: &fidl_fuchsia_net_ext::MacAddress,
121        device_class: DeviceClass,
122    ) -> Result<(&str, InterfaceNamingIdentifier), NameGenerationError> {
123        let interface_naming_id = generate_identifier(mac, topological_path);
124        let info = DeviceInfoRef { topological_path, mac, device_class };
125
126        // Interfaces that are named using the NormalizedMac naming rule are
127        // named to avoid MAC address final octet collisions. When a device
128        // with the same identifier is re-installed, re-attempt name generation
129        // since the MAC identifiers used may have changed.
130        match self.interfaces.remove(&interface_naming_id) {
131            Some(name) => log::info!(
132                "{name} already existed for this identifier\
133            {interface_naming_id:?}. inserting a new one."
134            ),
135            None => {
136                // This interface naming id will have a new entry
137            }
138        }
139
140        let generated_name = self.generate_name(&info)?;
141        if let Some(name) =
142            self.interfaces.insert(interface_naming_id.clone(), generated_name.clone())
143        {
144            log::error!(
145                "{name} was unexpectedly found for {interface_naming_id:?} \
146            when inserting a new name"
147            );
148        }
149
150        // Need to grab a reference to appease the borrow checker.
151        let generated_name = match self.interfaces.get(&interface_naming_id) {
152            Some(name) => Ok(name),
153            None => Err(NameGenerationError::GenerationError(anyhow::format_err!(
154                "expected to see name {generated_name} present since it was just added"
155            ))),
156        }?;
157
158        Ok((generated_name, interface_naming_id))
159    }
160
161    fn generate_name(&self, info: &DeviceInfoRef<'_>) -> Result<String, NameGenerationError> {
162        generate_name_from_naming_rules(&self.naming_rules, &self.interfaces, &info)
163    }
164}
165
166/// An error observed when generating a new name.
167#[derive(Debug)]
168pub enum NameGenerationError {
169    GenerationError(anyhow::Error),
170}
171
172#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Deserialize)]
173#[serde(deny_unknown_fields, rename_all = "lowercase")]
174pub enum BusType {
175    PCI,
176    SDIO,
177    USB,
178    Unknown,
179    VirtIo,
180}
181
182impl BusType {
183    // Retrieve the list of composition rules that comprise the default name
184    // for the interface based on BusType.
185    // Example names for the following default rules:
186    // * USB device: "ethx5"
187    // * PCI/SDIO device: "wlans5009"
188    fn get_default_name_composition_rules(&self) -> Vec<NameCompositionRule> {
189        match *self {
190            BusType::USB | BusType::Unknown => vec![
191                NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::DeviceClass },
192                NameCompositionRule::Static { value: String::from("x") },
193                NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::NormalizedMac },
194            ],
195            BusType::PCI | BusType::SDIO | BusType::VirtIo => vec![
196                NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::DeviceClass },
197                NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::BusType },
198                NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::BusPath },
199            ],
200        }
201    }
202}
203
204impl std::fmt::Display for BusType {
205    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
206        let name = match *self {
207            Self::PCI => "p",
208            Self::SDIO => "s",
209            Self::USB => "u",
210            Self::Unknown => "unk",
211            Self::VirtIo => "v",
212        };
213        write!(f, "{}", name)
214    }
215}
216
217// Extract the `BusType` for a device given the topological path.
218fn get_bus_type_for_topological_path(topological_path: &str) -> BusType {
219    let p = topological_path;
220
221    if p.contains("/PCI0") {
222        // A USB bus will require a bridge over a PCI controller, so a
223        // topological path for a USB bus should contain strings to represent
224        // PCI and USB.
225        if p.contains("/usb/") {
226            return BusType::USB;
227        }
228        return BusType::PCI;
229    } else if p.contains("/usb-peripheral/") {
230        // On VIM3 targets, the USB bus does not require a bridge over a PCI
231        // controller, so the bus path represents the USB type with a
232        // different string.
233        return BusType::USB;
234    } else if p.contains("/sdio/") {
235        return BusType::SDIO;
236    } else if p.contains("/virtio-net/") {
237        return BusType::VirtIo;
238    }
239
240    BusType::Unknown
241}
242
243fn deserialize_glob_pattern<'de, D>(deserializer: D) -> Result<glob::Pattern, D::Error>
244where
245    D: Deserializer<'de>,
246{
247    let buf = String::deserialize(deserializer)?;
248    glob::Pattern::new(&buf).map_err(serde::de::Error::custom)
249}
250
251/// The matching rules available for a `NamingRule`.
252#[derive(Debug, Deserialize, Eq, Hash, PartialEq)]
253#[serde(deny_unknown_fields, rename_all = "snake_case")]
254pub enum MatchingRule {
255    BusTypes(Vec<BusType>),
256    // TODO(https://fxbug.dev/42085144): Use a lightweight regex crate with the basic
257    // regex features to allow for more configurations than glob.
258    #[serde(deserialize_with = "deserialize_glob_pattern")]
259    TopologicalPath(glob::Pattern),
260    DeviceClasses(Vec<DeviceClass>),
261    // Signals whether this rule should match any interface.
262    Any(bool),
263}
264
265/// The matching rules available for a `ProvisoningRule`.
266#[derive(Debug, Deserialize, Eq, Hash, PartialEq)]
267#[serde(untagged)]
268pub enum ProvisioningMatchingRule {
269    // TODO(github.com/serde-rs/serde/issues/912): Use `other` once it supports
270    // deserializing into non-unit variants. `untagged` can only be applied
271    // to the entire enum, so `interface_name` is used as a field to ensure
272    // stability across configuration matching rules.
273    InterfaceName {
274        #[serde(rename = "interface_name", deserialize_with = "deserialize_glob_pattern")]
275        pattern: glob::Pattern,
276    },
277    Common(MatchingRule),
278}
279
280impl MatchingRule {
281    fn does_interface_match(&self, info: &DeviceInfoRef<'_>) -> Result<bool, anyhow::Error> {
282        match &self {
283            MatchingRule::BusTypes(type_list) => {
284                // Match the interface if the interface under comparison
285                // matches any of the types included in the list.
286                let bus_type = get_bus_type_for_topological_path(info.topological_path);
287                Ok(type_list.contains(&bus_type))
288            }
289            MatchingRule::TopologicalPath(pattern) => {
290                // Match the interface if the provided pattern finds any
291                // matches in the interface under comparison's
292                // topological path.
293                Ok(pattern.matches(info.topological_path))
294            }
295            MatchingRule::DeviceClasses(class_list) => {
296                // Match the interface if the interface under comparison
297                // matches any of the types included in the list.
298                Ok(class_list.contains(&info.device_class))
299            }
300            MatchingRule::Any(matches_any_interface) => Ok(*matches_any_interface),
301        }
302    }
303}
304
305impl ProvisioningMatchingRule {
306    fn does_interface_match(
307        &self,
308        info: &DeviceInfoRef<'_>,
309        interface_name: &str,
310    ) -> Result<bool, anyhow::Error> {
311        match &self {
312            ProvisioningMatchingRule::InterfaceName { pattern } => {
313                // Match the interface if the provided pattern finds any
314                // matches in the interface under comparison's name.
315                Ok(pattern.matches(interface_name))
316            }
317            ProvisioningMatchingRule::Common(matching_rule) => {
318                // Handle the other `MatchingRule`s the same as the naming
319                // policy matchers.
320                matching_rule.does_interface_match(info)
321            }
322        }
323    }
324}
325
326// TODO(https://fxbug.dev/42084785): Create dynamic naming rules
327// A naming rule that uses device information to produce a component of
328// the interface's name.
329#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, Deserialize)]
330#[serde(deny_unknown_fields, rename_all = "snake_case")]
331pub enum DynamicNameCompositionRule {
332    BusPath,
333    BusType,
334    DeviceClass,
335    // A unique value seeded by the final octet of the interface's MAC address.
336    NormalizedMac,
337}
338
339impl DynamicNameCompositionRule {
340    // `true` when a rule can be re-tried to produce a different name.
341    fn supports_retry(&self) -> bool {
342        match *self {
343            DynamicNameCompositionRule::BusPath
344            | DynamicNameCompositionRule::BusType
345            | DynamicNameCompositionRule::DeviceClass => false,
346            DynamicNameCompositionRule::NormalizedMac => true,
347        }
348    }
349
350    fn get_name(&self, info: &DeviceInfoRef<'_>, attempt_num: u8) -> Result<String, anyhow::Error> {
351        Ok(match *self {
352            DynamicNameCompositionRule::BusPath => {
353                get_normalized_bus_path_for_topo_path(info.topological_path)
354            }
355            DynamicNameCompositionRule::BusType => {
356                get_bus_type_for_topological_path(info.topological_path).to_string()
357            }
358            DynamicNameCompositionRule::DeviceClass => match info.device_class.into() {
359                crate::InterfaceType::WlanClient => INTERFACE_PREFIX_WLAN,
360                crate::InterfaceType::Ethernet => INTERFACE_PREFIX_ETHERNET,
361                crate::InterfaceType::WlanAp => INTERFACE_PREFIX_AP,
362                crate::InterfaceType::Blackhole => INTERFACE_PREFIX_BLACKHOLE,
363            }
364            .to_string(),
365            DynamicNameCompositionRule::NormalizedMac => {
366                let fidl_fuchsia_net_ext::MacAddress { octets } = info.mac;
367                let mac_identifier =
368                    get_mac_identifier_from_octets(octets, info.device_class.into(), attempt_num)?;
369                format!("{mac_identifier:x}")
370            }
371        })
372    }
373}
374
375// A rule that dictates a component of an interface's name. An interface's name
376// is determined by extracting the name of each rule, in order, and
377// concatenating the results.
378#[derive(Clone, Debug, Deserialize, PartialEq)]
379#[serde(deny_unknown_fields, rename_all = "lowercase", tag = "type")]
380pub enum NameCompositionRule {
381    Static { value: String },
382    Dynamic { rule: DynamicNameCompositionRule },
383    // The default name composition rules based on the device's BusType.
384    // Defined in `BusType::get_default_name_composition_rules`.
385    Default,
386}
387
388/// A rule that dictates how interfaces that align with the property matching
389/// rules should be named.
390#[derive(Debug, Deserialize, PartialEq)]
391#[serde(deny_unknown_fields, rename_all = "lowercase")]
392pub struct NamingRule {
393    /// A set of rules to check against an interface's properties. All rules
394    /// must apply for the naming scheme to take effect.
395    pub matchers: HashSet<MatchingRule>,
396    /// The rules to apply to the interface to produce the interface's name.
397    pub naming_scheme: Vec<NameCompositionRule>,
398}
399
400impl NamingRule {
401    // An interface's name is determined by extracting the name of each rule,
402    // in order, and concatenating the results. Returns an error if the
403    // interface name cannot be generated.
404    fn generate_name(
405        &self,
406        interfaces: &HashMap<InterfaceNamingIdentifier, String>,
407        info: &DeviceInfoRef<'_>,
408    ) -> Result<String, NameGenerationError> {
409        // When a bus type cannot be found for a path, use the USB
410        // default naming policy which uses a MAC address.
411        let bus_type = get_bus_type_for_topological_path(&info.topological_path);
412
413        // Expand any `Default` rules into the `Static` and `Dynamic` rules in a single vector.
414        // If this was being consumed once, we could avoid the call to `collect`. However, since we
415        // want to use it twice, we need to convert it to a form where the items can be itererated
416        // over without consuming them.
417        let expanded_rules = self
418            .naming_scheme
419            .iter()
420            .map(|rule| {
421                if let NameCompositionRule::Default = rule {
422                    Either::Right(bus_type.get_default_name_composition_rules().into_iter())
423                } else {
424                    Either::Left(std::iter::once(rule.clone()))
425                }
426            })
427            .flatten()
428            .collect::<Vec<_>>();
429
430        // Determine whether any rules present support retrying for a unique name.
431        let should_reattempt_on_conflict = expanded_rules.iter().any(|rule| {
432            if let NameCompositionRule::Dynamic { rule } = rule {
433                rule.supports_retry()
434            } else {
435                false
436            }
437        });
438
439        let mut attempt_num = 0u8;
440        loop {
441            let name = expanded_rules
442                .iter()
443                .map(|rule| match rule {
444                    NameCompositionRule::Static { value } => Ok(value.clone()),
445                    // Dynamic rules require the knowledge of `DeviceInfo` properties.
446                    NameCompositionRule::Dynamic { rule } => rule
447                        .get_name(info, attempt_num)
448                        .map_err(NameGenerationError::GenerationError),
449                    NameCompositionRule::Default => {
450                        unreachable!(
451                            "Default naming rules should have been pre-expanded. \
452                             Nested default rules are not supported."
453                        );
454                    }
455                })
456                .collect::<Result<String, NameGenerationError>>()?;
457
458            if interfaces.values().any(|existing_name| existing_name == &name) {
459                if should_reattempt_on_conflict {
460                    attempt_num += 1;
461                    // Try to generate another name with the modified attempt number.
462                    continue;
463                }
464
465                log::warn!(
466                    "name ({name}) already used for an interface installed by netcfg. \
467                 using name since it is possible that the interface using this name is no \
468                 longer active"
469                );
470            }
471            return Ok(name);
472        }
473    }
474
475    // An interface must align with all specified `MatchingRule`s.
476    fn does_interface_match(&self, info: &DeviceInfoRef<'_>) -> bool {
477        self.matchers.iter().all(|rule| rule.does_interface_match(info).unwrap_or_default())
478    }
479}
480
481// Find the first `NamingRule` that matches the device and attempt to
482// construct a name from the provided `NameCompositionRule`s.
483fn generate_name_from_naming_rules(
484    naming_rules: &[NamingRule],
485    interfaces: &HashMap<InterfaceNamingIdentifier, String>,
486    info: &DeviceInfoRef<'_>,
487) -> Result<String, NameGenerationError> {
488    // TODO(https://fxbug.dev/42086002): Consider adding an option to the rules to allow
489    // fallback rules when name generation fails.
490    // Use the first naming rule that matches the interface to enforce consistent
491    // interface names, even if there are other matching rules.
492    let fallback_rule = fallback_naming_rule();
493    let first_matching_rule =
494        naming_rules.iter().find(|rule| rule.does_interface_match(&info)).unwrap_or(
495            // When there are no `NamingRule`s that match the device,
496            // use a fallback rule that has the Default naming scheme.
497            &fallback_rule,
498        );
499
500    first_matching_rule.generate_name(interfaces, &info)
501}
502
503// Matches any device and uses the default naming rule.
504fn fallback_naming_rule() -> NamingRule {
505    NamingRule {
506        matchers: HashSet::from([MatchingRule::Any(true)]),
507        naming_scheme: vec![NameCompositionRule::Default],
508    }
509}
510
511/// The provision action to take if the matchers are satisfied.
512#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Default)]
513#[serde(deny_unknown_fields, rename_all = "lowercase")]
514pub struct ProvisioningAction {
515    /// The type of the provisioning.
516    pub provisioning: ProvisioningType,
517    /// Where the netstack managed routes should be installed.
518    pub netstack_managed_routes_designation: Option<NetstackManagedRoutesDesignation>,
519}
520
521/// Whether the interface should be provisioned locally by netcfg, or
522/// delegated. Provisioning is the set of events that occurs after
523/// interface enumeration, such as starting a DHCP client and assigning
524/// an IP to the interface. Provisioning actions work to support
525/// Internet connectivity.
526#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Default)]
527#[serde(deny_unknown_fields, rename_all = "lowercase")]
528pub enum ProvisioningType {
529    /// Netcfg will provision the interface
530    #[default]
531    Local,
532    /// Netcfg will not provision the interface. The provisioning
533    /// of the interface will occur elsewhere
534    Delegated,
535}
536
537/// Where the netstack managed routes should be stored.
538///
539/// Mirrors [`fnet_interfaces_admin::NetstackManagedRoutesDesignation`].
540#[derive(Copy, Clone, Debug, Deserialize, PartialEq)]
541#[serde(deny_unknown_fields, rename_all = "snake_case")]
542pub enum NetstackManagedRoutesDesignation {
543    Main,
544    InterfaceLocal,
545}
546
547impl From<NetstackManagedRoutesDesignation>
548    for fnet_interfaces_admin::NetstackManagedRoutesDesignation
549{
550    fn from(value: NetstackManagedRoutesDesignation) -> Self {
551        match value {
552            NetstackManagedRoutesDesignation::Main => Self::Main(fnet_interfaces_admin::Empty),
553            NetstackManagedRoutesDesignation::InterfaceLocal => {
554                Self::InterfaceLocal(fnet_interfaces_admin::Empty)
555            }
556        }
557    }
558}
559
560/// A rule that dictates how interfaces that align with the property matching
561/// rules should be provisioned.
562#[derive(Debug, Deserialize, PartialEq)]
563#[serde(deny_unknown_fields, rename_all = "lowercase")]
564pub struct ProvisioningRule {
565    /// A set of rules to check against an interface's properties. All rules
566    /// must apply for the provisioning action to take effect.
567    pub matchers: HashSet<ProvisioningMatchingRule>,
568    /// The provisioning policy that netcfg applies to a matching
569    /// interface.
570    #[serde(flatten)]
571    pub action: ProvisioningAction,
572}
573
574// A ref version of `devices::DeviceInfo` to avoid the need to clone data
575// unnecessarily. Devices without MAC are not supported yet, see
576// `add_new_device` in `lib.rs`. This makes mac into a required field for
577// ease of use.
578pub(super) struct DeviceInfoRef<'a> {
579    pub(super) device_class: DeviceClass,
580    pub(super) mac: &'a fidl_fuchsia_net_ext::MacAddress,
581    pub(super) topological_path: &'a str,
582}
583
584impl<'a> DeviceInfoRef<'a> {
585    pub(super) fn interface_type(&self) -> crate::InterfaceType {
586        let DeviceInfoRef { device_class, mac: _, topological_path: _ } = self;
587        (*device_class).into()
588    }
589
590    pub(super) fn is_wlan_ap(&self) -> bool {
591        let DeviceInfoRef { device_class, mac: _, topological_path: _ } = self;
592        match device_class {
593            DeviceClass::WlanAp => true,
594            DeviceClass::WlanClient
595            | DeviceClass::Virtual
596            | DeviceClass::Ethernet
597            | DeviceClass::Bridge
598            | DeviceClass::Ppp
599            | DeviceClass::Lowpan
600            | DeviceClass::Blackhole => false,
601        }
602    }
603}
604
605impl ProvisioningRule {
606    // An interface must align with all specified `MatchingRule`s.
607    fn does_interface_match(&self, info: &DeviceInfoRef<'_>, interface_name: &str) -> bool {
608        self.matchers
609            .iter()
610            .all(|rule| rule.does_interface_match(info, interface_name).unwrap_or_default())
611    }
612}
613
614// Find the first `ProvisioningRule` that matches the device and get
615// the associated `ProvisioningAction`. By default, use Local provisioning
616// so that Netcfg will provision interfaces unless configuration
617// indicates otherwise.
618pub(crate) fn find_provisioning_action_from_provisioning_rules(
619    provisioning_rules: &[ProvisioningRule],
620    info: &DeviceInfoRef<'_>,
621    interface_name: &str,
622) -> ProvisioningAction {
623    provisioning_rules
624        .iter()
625        .find_map(|rule| {
626            if rule.does_interface_match(&info, &interface_name) { Some(rule.action) } else { None }
627        })
628        .unwrap_or_default()
629}
630
631#[cfg(test)]
632mod tests {
633    use super::*;
634    use assert_matches::assert_matches;
635    use test_case::test_case;
636
637    // This is a lossy conversion between `InterfaceType` and `DeviceClass`
638    // that allows tests to use a `devices::DeviceInfo` struct instead of
639    // handling the fields individually.
640    fn device_class_from_interface_type(ty: crate::InterfaceType) -> DeviceClass {
641        match ty {
642            crate::InterfaceType::Ethernet => DeviceClass::Ethernet,
643            crate::InterfaceType::WlanClient => DeviceClass::WlanClient,
644            crate::InterfaceType::WlanAp => DeviceClass::WlanAp,
645            crate::InterfaceType::Blackhole => DeviceClass::Blackhole,
646        }
647    }
648
649    // usb interfaces
650    #[test_case(
651        "/dev/sys/platform/pt/PCI0/bus/00:14.0/00:14.0/xhci/usb/004/004/ifc-000/ax88179/ethernet",
652        [0x01, 0x01, 0x01, 0x01, 0x01, 0x01],
653        crate::InterfaceType::WlanClient,
654        "wlanx1";
655        "usb_wlan"
656    )]
657    #[test_case(
658        "/dev/sys/platform/pt/PCI0/bus/00:15.0/00:15.0/xhci/usb/004/004/ifc-000/ax88179/ethernet",
659        [0x02, 0x02, 0x02, 0x02, 0x02, 0x02],
660        crate::InterfaceType::Ethernet,
661        "ethx2";
662        "usb_eth"
663    )]
664    // pci interfaces
665    #[test_case(
666        "/dev/sys/platform/pt/PCI0/bus/00:14.0/00:14.0/ethernet",
667        [0x03, 0x03, 0x03, 0x03, 0x03, 0x03],
668        crate::InterfaceType::WlanClient,
669        "wlanp0014";
670        "pci_wlan"
671    )]
672    #[test_case(
673        "/dev/sys/platform/pt/PCI0/bus/00:15.0/00:14.0/ethernet",
674        [0x04, 0x04, 0x04, 0x04, 0x04, 0x04],
675        crate::InterfaceType::Ethernet,
676        "ethp0015";
677        "pci_eth"
678    )]
679    // platform interfaces (ethernet jack and sdio devices)
680    #[test_case(
681        "/dev/sys/platform/05:00:6/aml-sd-emmc/sdio/broadcom-wlanphy/wlanphy",
682        [0x05, 0x05, 0x05, 0x05, 0x05, 0x05],
683        crate::InterfaceType::WlanClient,
684        "wlans05006";
685        "platform_wlan"
686    )]
687    #[test_case(
688        "/dev/sys/platform/04:02:7/aml-ethernet/Designware-MAC/ethernet",
689        [0x07, 0x07, 0x07, 0x07, 0x07, 0x07],
690        crate::InterfaceType::Ethernet,
691        "ethx7";
692        "platform_eth"
693    )]
694    // unknown interfaces
695    #[test_case(
696        "/dev/sys/unknown",
697        [0x08, 0x08, 0x08, 0x08, 0x08, 0x08],
698        crate::InterfaceType::WlanClient,
699        "wlanx8";
700        "unknown_wlan1"
701    )]
702    #[test_case(
703        "unknown",
704        [0x09, 0x09, 0x09, 0x09, 0x09, 0x09],
705        crate::InterfaceType::WlanClient,
706        "wlanx9";
707        "unknown_wlan2"
708    )]
709    #[test_case(
710        "unknown",
711        [0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a],
712        crate::InterfaceType::WlanAp,
713        "apxa";
714        "unknown_ap"
715    )]
716    #[test_case(
717        "/dev/sys/platform/pt/PC00/bus/00:1e.0/00_1e_0/virtio-net/network-device",
718        [0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b],
719        crate::InterfaceType::Ethernet,
720        "ethv001e";
721        "virtio_attached_ethernet"
722    )]
723    // NB: name generation for blackhole interfaces is never expected to be invoked.
724    #[test_case(
725        "/dev/sys/platform/pt/PCI0/bus/00:15.0/00:15.0/xhci/usb/004/004/ifc-000/ax88179/ethernet",
726        [0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c],
727        crate::InterfaceType::Blackhole,
728        "blackholexc";
729        "usb_blackhole")]
730    fn test_generate_name(
731        topological_path: &'static str,
732        mac: [u8; 6],
733        interface_type: crate::InterfaceType,
734        want_name: &'static str,
735    ) {
736        let interface_naming_config = InterfaceNamingConfig::from_naming_rules(vec![]);
737        let name = interface_naming_config
738            .generate_name(&DeviceInfoRef {
739                device_class: device_class_from_interface_type(interface_type),
740                mac: &fidl_fuchsia_net_ext::MacAddress { octets: mac },
741                topological_path,
742            })
743            .expect("failed to generate the name");
744        assert_eq!(name, want_name);
745    }
746
747    struct StableNameTestCase {
748        topological_path: &'static str,
749        mac: [u8; 6],
750        interface_type: crate::InterfaceType,
751        want_name: &'static str,
752        expected_size: usize,
753    }
754
755    // Base case. Interface should be added to config.
756    #[test_case([StableNameTestCase {
757        topological_path: "/dev/sys/platform/pt/PCI0/bus/00:14.0_/00:14.0/ethernet",
758        mac: [0x01, 0x01, 0x01, 0x01, 0x01, 0x01],
759        interface_type: crate::InterfaceType::WlanClient,
760        want_name: "wlanp0014",
761        expected_size: 1 }];
762        "single_interface"
763    )]
764    // Test case that shares the same topo path and different MAC, but same
765    // last octet. Expect to see second interface added with different name.
766    #[test_case([StableNameTestCase {
767        topological_path: "/dev/sys/platform/pt/PCI0/bus/00:14.0_/00:14.0/ethernet",
768        mac: [0x01, 0x01, 0x01, 0x01, 0x01, 0x01],
769        interface_type: crate::InterfaceType::WlanClient,
770        want_name: "wlanp0014",
771        expected_size: 1}, StableNameTestCase {
772        topological_path: "/dev/sys/platform/pt/PCI0/bus/00:14.0_/00:14.0/ethernet",
773        mac: [0xFE, 0x01, 0x01, 0x01, 0x01, 0x01],
774        interface_type: crate::InterfaceType::WlanAp,
775        want_name: "app0014",
776        expected_size: 2 }];
777        "two_interfaces_same_topo_path_different_mac"
778    )]
779    #[test_case([StableNameTestCase {
780        topological_path: "/dev/sys/platform/pt/PCI0/bus/00:14.0_/00:14.0/ethernet",
781        mac: [0x01, 0x01, 0x01, 0x01, 0x01, 0x01],
782        interface_type: crate::InterfaceType::WlanClient,
783        want_name: "wlanp0014",
784        expected_size: 1}, StableNameTestCase {
785        topological_path: "/dev/sys/platform/pt/PCI0/bus/01:00.0/01:00.0/iwlwifi-wlan-softmac/wlan-ethernet/ethernet",
786        mac: [0xFE, 0x01, 0x01, 0x01, 0x01, 0x01],
787        interface_type: crate::InterfaceType::Ethernet,
788        want_name: "ethp01",
789        expected_size: 2 }];
790        "two_distinct_interfaces"
791    )]
792    // Test case that labels iwilwifi as ethernet, then changes the device
793    // class to wlan. The test should detect that the device class doesn't
794    // match the interface name, and overwrite with the new interface name
795    // that does match.
796    #[test_case([StableNameTestCase {
797        topological_path: "/dev/sys/platform/pt/PCI0/bus/01:00.0/01:00.0/iwlwifi-wlan-softmac/wlan-ethernet/ethernet",
798        mac: [0x01, 0x01, 0x01, 0x01, 0x01, 0x01],
799        interface_type: crate::InterfaceType::Ethernet,
800        want_name: "ethp01",
801        expected_size: 1 }, StableNameTestCase {
802        topological_path: "/dev/sys/platform/pt/PCI0/bus/01:00.0/01:00.0/iwlwifi-wlan-softmac/wlan-ethernet/ethernet",
803        mac: [0x01, 0x01, 0x01, 0x01, 0x01, 0x01],
804        interface_type: crate::InterfaceType::WlanClient,
805        want_name: "wlanp01",
806        expected_size: 1 }];
807        "two_interfaces_different_device_class"
808    )]
809    fn test_generate_stable_name(test_cases: impl IntoIterator<Item = StableNameTestCase>) {
810        let mut interface_naming_config = InterfaceNamingConfig::from_naming_rules(vec![]);
811
812        // query an existing interface with the same topo path and a different mac address
813        for (
814            _i,
815            StableNameTestCase { topological_path, mac, interface_type, want_name, expected_size },
816        ) in test_cases.into_iter().enumerate()
817        {
818            let (name, _identifier) = interface_naming_config
819                .generate_stable_name(
820                    topological_path,
821                    &fidl_fuchsia_net_ext::MacAddress { octets: mac },
822                    device_class_from_interface_type(interface_type),
823                )
824                .expect("failed to get the interface name");
825            assert_eq!(name, want_name);
826            // Ensure the number of interfaces we expect are present.
827            assert_eq!(interface_naming_config.interfaces.len(), expected_size);
828        }
829    }
830
831    #[test]
832    fn test_get_usb_255() {
833        let topo_usb = "/dev/pci-00:14.0-fidl/xhci/usb/004/004/ifc-000/ax88179/ethernet";
834
835        // test cases for 256 usb interfaces
836        let mut config = InterfaceNamingConfig::from_naming_rules(vec![]);
837        for n in 0u8..255u8 {
838            let octets = [n, 0x01, 0x01, 0x01, 0x01, 00];
839
840            let interface_naming_id =
841                generate_identifier(&fidl_fuchsia_net_ext::MacAddress { octets }, topo_usb);
842
843            let name = config
844                .generate_name(&DeviceInfoRef {
845                    device_class: device_class_from_interface_type(
846                        crate::InterfaceType::WlanClient,
847                    ),
848                    mac: &fidl_fuchsia_net_ext::MacAddress { octets },
849                    topological_path: topo_usb,
850                })
851                .expect("failed to generate the name");
852            assert_eq!(name, format!("{}{:x}", "wlanx", n));
853            assert_matches!(config.interfaces.insert(interface_naming_id, name), None);
854        }
855
856        let octets = [0x00, 0x00, 0x01, 0x01, 0x01, 00];
857        assert!(
858            config
859                .generate_name(&DeviceInfoRef {
860                    device_class: device_class_from_interface_type(
861                        crate::InterfaceType::WlanClient
862                    ),
863                    mac: &fidl_fuchsia_net_ext::MacAddress { octets },
864                    topological_path: topo_usb
865                },)
866                .is_err()
867        );
868    }
869
870    #[test]
871    fn test_get_usb_255_with_naming_rule() {
872        let topo_usb = "/dev/pci-00:14.0-fidl/xhci/usb/004/004/ifc-000/ax88179/ethernet";
873
874        let naming_rule = NamingRule {
875            matchers: HashSet::new(),
876            naming_scheme: vec![
877                NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::NormalizedMac },
878                NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::NormalizedMac },
879            ],
880        };
881
882        // test cases for 256 usb interfaces
883        let mut config = InterfaceNamingConfig::from_naming_rules(vec![naming_rule]);
884        for n in 0u8..255u8 {
885            let octets = [n, 0x01, 0x01, 0x01, 0x01, 00];
886            let interface_naming_id =
887                generate_identifier(&fidl_fuchsia_net_ext::MacAddress { octets }, topo_usb);
888
889            let info = DeviceInfoRef {
890                device_class: DeviceClass::Ethernet,
891                mac: &fidl_fuchsia_net_ext::MacAddress { octets },
892                topological_path: topo_usb,
893            };
894
895            let name = config.generate_name(&info).expect("failed to generate the name");
896            // With only NormalizedMac as a NameCompositionRule, the name
897            // should simply be the NormalizedMac itself.
898            assert_eq!(name, format!("{n:x}{n:x}"));
899
900            assert_matches!(config.interfaces.insert(interface_naming_id, name), None);
901        }
902
903        let octets = [0x00, 0x00, 0x01, 0x01, 0x01, 00];
904        assert!(
905            config
906                .generate_name(&DeviceInfoRef {
907                    device_class: DeviceClass::Ethernet,
908                    mac: &fidl_fuchsia_net_ext::MacAddress { octets },
909                    topological_path: topo_usb
910                })
911                .is_err()
912        );
913    }
914
915    // Arbitrary values for devices::DeviceInfo for cases where DeviceInfo has
916    // no impact on the test.
917    fn default_device_info() -> DeviceInfoRef<'static> {
918        DeviceInfoRef {
919            device_class: DeviceClass::Ethernet,
920            mac: &fidl_fuchsia_net_ext::MacAddress { octets: [0x1, 0x1, 0x1, 0x1, 0x1, 0x1] },
921            topological_path: "",
922        }
923    }
924
925    #[test_case(
926        "/dev/sys/platform/pt/PCI0/bus/00:14.0_/00:14.0/ethernet",
927        vec![BusType::PCI],
928        BusType::PCI,
929        true,
930        "0014";
931        "pci_match"
932    )]
933    #[test_case(
934        "/dev/sys/platform/pt/PCI0/bus/00:14.0_/00:14.0/ethernet",
935        vec![BusType::USB, BusType::SDIO],
936        BusType::PCI,
937        false,
938        "0014";
939        "pci_no_match"
940    )]
941    #[test_case(
942        "/dev/sys/platform/pt/PCI0/bus/00:14.0/00:14.0/xhci/usb/004/004/ifc-000/ax88179/ethernet",
943        vec![BusType::USB],
944        BusType::USB,
945        true,
946        "0014";
947        "pci_usb_match"
948    )]
949    #[test_case(
950        "/dev/sys/platform/05:00:18/usb-phy-composite/aml_usb_phy/dwc2/dwc2_phy/dwc2/usb-peripheral/function-000/cdc-eth-function/netdevice-migration/network-device",
951        vec![BusType::USB],
952        BusType::USB,
953        true,
954        "050018";
955        "dwc_usb_match"
956    )]
957    // Same topological path as the case for USB, but with
958    // non-matching bus types. Ensure that even though PCI is
959    // present in the topological path, it does not match a PCI
960    // controller.
961    #[test_case(
962        "/dev/sys/platform/pt/PCI0/bus/00:14.0/00:14.0/xhci/usb/004/004/ifc-000/ax88179/ethernet",
963        vec![BusType::PCI, BusType::SDIO],
964        BusType::USB,
965        false,
966        "0014";
967        "usb_no_match"
968    )]
969    #[test_case(
970        "/dev/sys/platform/05:00:6/aml-sd-emmc/sdio/broadcom-wlanphy/wlanphy",
971        vec![BusType::SDIO],
972        BusType::SDIO,
973        true,
974        "05006";
975        "sdio_match"
976    )]
977    #[test_case(
978        "/dev/sys/platform/pt/PC00/bus/00:1e.0/00_1e_0/virtio-net/network-device",
979        vec![BusType::VirtIo],
980        BusType::VirtIo,
981        true,
982        "001e";
983        "virtio_match_alternate_location"
984    )]
985    #[test_case(
986        "/dev/sys/platform/pt/PC00/bus/<malformed>/00_1e_0/virtio-net/network-device",
987        vec![BusType::VirtIo],
988        BusType::VirtIo,
989        true,
990        "001e";
991        "virtio_matches_underscore_path"
992    )]
993    #[test_case(
994        "/dev/sys/platform/pt/PC00/bus/00:1e.1/00_1e_1/virtio-net/network-device",
995        vec![BusType::VirtIo],
996        BusType::VirtIo,
997        true,
998        "001e1";
999        "virtio_match_alternate_no_trim"
1000    )]
1001    #[test_case(
1002        "/dev/sys/platform/pt/PC00/bus/<unrecognized_bus_path>/network-device",
1003        vec![BusType::Unknown],
1004        BusType::Unknown,
1005        true,
1006        "ffffff";
1007        "unknown_bus_match_unrecognized"
1008    )]
1009    fn test_interface_matching_and_naming_by_bus_properties(
1010        topological_path: &'static str,
1011        bus_types: Vec<BusType>,
1012        expected_bus_type: BusType,
1013        want_match: bool,
1014        want_name: &'static str,
1015    ) {
1016        let device_info = DeviceInfoRef {
1017            topological_path: topological_path,
1018            // `device_class` and `mac` have no effect on `BusType`
1019            // matching, so we use arbitrary values.
1020            ..default_device_info()
1021        };
1022
1023        // Verify the `BusType` determined from the device's
1024        // topological path.
1025        let bus_type = get_bus_type_for_topological_path(&device_info.topological_path);
1026        assert_eq!(bus_type, expected_bus_type);
1027
1028        // Create a matching rule for the provided `BusType` list.
1029        let matching_rule = MatchingRule::BusTypes(bus_types);
1030        let does_interface_match = matching_rule.does_interface_match(&device_info).unwrap();
1031        assert_eq!(does_interface_match, want_match);
1032
1033        let name = get_normalized_bus_path_for_topo_path(&device_info.topological_path);
1034        assert_eq!(name, want_name);
1035
1036        // Ensure that calling again will decrement this. It's unfortunate to need to encode this
1037        // in the test itself, but each test runs separately, so we can't rely on static storage
1038        // between test invocations.
1039        if want_name == "ffffff" {
1040            let name = get_normalized_bus_path_for_topo_path(&device_info.topological_path);
1041            assert_eq!(name, "fffffe");
1042        }
1043    }
1044
1045    // Glob matches the number pattern of XX:XX in the path.
1046    #[test_case(
1047        "/dev/sys/platform/pt/PCI0/bus/00:14.0_/00:14.0/ethernet",
1048        r"*[0-9][0-9]:[0-9][0-9]*",
1049        true;
1050        "pattern_matches"
1051    )]
1052    #[test_case("pattern/will/match/anything", r"*", true; "pattern_matches_any")]
1053    // Glob checks for '00' after the colon but it will not find it.
1054    #[test_case(
1055        "/dev/sys/platform/pt/PCI0/bus/00:14.0_/00:14.0/ethernet",
1056        r"*[0-9][0-9]:00*",
1057        false;
1058        "no_matches"
1059    )]
1060    fn test_interface_matching_by_topological_path(
1061        topological_path: &'static str,
1062        glob_str: &'static str,
1063        want_match: bool,
1064    ) {
1065        let device_info = DeviceInfoRef {
1066            topological_path,
1067            // `device_class` and `mac` have no effect on `TopologicalPath`
1068            // matching, so we use arbitrary values.
1069            ..default_device_info()
1070        };
1071
1072        // Create a matching rule for the provided glob expression.
1073        let matching_rule = MatchingRule::TopologicalPath(glob::Pattern::new(glob_str).unwrap());
1074        let does_interface_match = matching_rule.does_interface_match(&device_info).unwrap();
1075        assert_eq!(does_interface_match, want_match);
1076    }
1077
1078    // Glob matches the default naming by MAC address.
1079    #[test_case(
1080        "ethx5",
1081        r"ethx[0-9]*",
1082        true;
1083        "pattern_matches"
1084    )]
1085    #[test_case("arbitraryname", r"*", true; "pattern_matches_any")]
1086    // Glob matches default naming by SDIO + bus path.
1087    #[test_case(
1088        "wlans1002",
1089        r"eths[0-9][0-9][0-9][0-9]*",
1090        false;
1091        "no_matches"
1092    )]
1093    fn test_interface_matching_by_interface_name(
1094        interface_name: &'static str,
1095        glob_str: &'static str,
1096        want_match: bool,
1097    ) {
1098        // Create a matching rule for the provided glob expression.
1099        let provisioning_matching_rule = ProvisioningMatchingRule::InterfaceName {
1100            pattern: glob::Pattern::new(glob_str).unwrap(),
1101        };
1102        let does_interface_match = provisioning_matching_rule
1103            .does_interface_match(&default_device_info(), interface_name)
1104            .unwrap();
1105        assert_eq!(does_interface_match, want_match);
1106    }
1107
1108    #[test_case(
1109        DeviceClass::Ethernet,
1110        vec![DeviceClass::Ethernet],
1111        true;
1112        "eth_match"
1113    )]
1114    #[test_case(
1115        DeviceClass::Ethernet,
1116        vec![DeviceClass::WlanClient, DeviceClass::WlanAp],
1117        false;
1118        "eth_no_match"
1119    )]
1120    #[test_case(
1121        DeviceClass::WlanClient,
1122        vec![DeviceClass::WlanClient],
1123        true;
1124        "wlan_match"
1125    )]
1126    #[test_case(
1127        DeviceClass::WlanClient,
1128        vec![DeviceClass::Ethernet, DeviceClass::WlanAp],
1129        false;
1130        "wlan_no_match"
1131    )]
1132    #[test_case(
1133        DeviceClass::WlanAp,
1134        vec![DeviceClass::WlanAp],
1135        true;
1136        "ap_match"
1137    )]
1138    #[test_case(
1139        DeviceClass::WlanAp,
1140        vec![DeviceClass::Ethernet, DeviceClass::WlanClient],
1141        false;
1142        "ap_no_match"
1143    )]
1144    fn test_interface_matching_by_device_class(
1145        device_class: DeviceClass,
1146        device_classes: Vec<DeviceClass>,
1147        want_match: bool,
1148    ) {
1149        let device_info = DeviceInfoRef { device_class, ..default_device_info() };
1150
1151        // Create a matching rule for the provided `DeviceClass` list.
1152        let matching_rule = MatchingRule::DeviceClasses(device_classes);
1153        let does_interface_match = matching_rule.does_interface_match(&device_info).unwrap();
1154        assert_eq!(does_interface_match, want_match);
1155    }
1156
1157    // The device information should not have any impact on whether the
1158    // interface matches, but we use Ethernet and Wlan as base cases
1159    // to ensure that all interfaces are accepted or all interfaces
1160    // are rejected.
1161    #[test_case(
1162        DeviceClass::Ethernet,
1163        "/dev/pci-00:15.0-fidl/xhci/usb/004/004/ifc-000/ax88179/ethernet"
1164    )]
1165    #[test_case(DeviceClass::WlanClient, "/dev/pci-00:14.0/ethernet")]
1166    fn test_interface_matching_by_any_matching_rule(
1167        device_class: DeviceClass,
1168        topological_path: &'static str,
1169    ) {
1170        let device_info = DeviceInfoRef {
1171            device_class,
1172            mac: &fidl_fuchsia_net_ext::MacAddress { octets: [0x1, 0x1, 0x1, 0x1, 0x1, 0x1] },
1173            topological_path,
1174        };
1175
1176        // Create a matching rule that should match any interface.
1177        let matching_rule = MatchingRule::Any(true);
1178        let does_interface_match = matching_rule.does_interface_match(&device_info).unwrap();
1179        assert!(does_interface_match);
1180
1181        // Create a matching rule that should reject any interface.
1182        let matching_rule = MatchingRule::Any(false);
1183        let does_interface_match = matching_rule.does_interface_match(&device_info).unwrap();
1184        assert!(!does_interface_match);
1185    }
1186
1187    #[test_case(
1188        DeviceInfoRef { device_class: DeviceClass::Ethernet, ..default_device_info() },
1189        vec![MatchingRule::DeviceClasses(vec![DeviceClass::WlanClient])],
1190        false;
1191        "false_single_rule"
1192    )]
1193    #[test_case(
1194        DeviceInfoRef { device_class: DeviceClass::Ethernet, ..default_device_info() },
1195        vec![MatchingRule::DeviceClasses(vec![DeviceClass::WlanClient]), MatchingRule::Any(true)],
1196        false;
1197        "false_one_rule_of_multiple"
1198    )]
1199    #[test_case(
1200        DeviceInfoRef { device_class: DeviceClass::Ethernet, ..default_device_info() },
1201        vec![MatchingRule::Any(true)],
1202        true;
1203        "true_single_rule"
1204    )]
1205    #[test_case(
1206        DeviceInfoRef { device_class: DeviceClass::Ethernet, ..default_device_info() },
1207        vec![MatchingRule::DeviceClasses(vec![DeviceClass::Ethernet]), MatchingRule::Any(true)],
1208        true;
1209        "true_multiple_rules"
1210    )]
1211    fn test_does_interface_match(
1212        info: DeviceInfoRef<'_>,
1213        matching_rules: Vec<MatchingRule>,
1214        want_match: bool,
1215    ) {
1216        let naming_rule =
1217            NamingRule { matchers: HashSet::from_iter(matching_rules), naming_scheme: Vec::new() };
1218        assert_eq!(naming_rule.does_interface_match(&info), want_match);
1219    }
1220
1221    #[test_case(
1222        DeviceInfoRef { device_class: DeviceClass::Ethernet, ..default_device_info() },
1223        "",
1224        vec![
1225            ProvisioningMatchingRule::Common(
1226                MatchingRule::DeviceClasses(vec![DeviceClass::WlanClient])
1227            )
1228        ],
1229        false;
1230        "false_single_rule"
1231    )]
1232    #[test_case(
1233        DeviceInfoRef { device_class: DeviceClass::WlanClient, ..default_device_info() },
1234        "wlanx5009",
1235        vec![
1236            ProvisioningMatchingRule::InterfaceName {
1237                pattern: glob::Pattern::new("ethx*").unwrap()
1238            },
1239            ProvisioningMatchingRule::Common(MatchingRule::Any(true))
1240        ],
1241        false;
1242        "false_one_rule_of_multiple"
1243    )]
1244    #[test_case(
1245        DeviceInfoRef { device_class: DeviceClass::Ethernet, ..default_device_info() },
1246        "",
1247        vec![ProvisioningMatchingRule::Common(MatchingRule::Any(true))],
1248        true;
1249        "true_single_rule"
1250    )]
1251    #[test_case(
1252        DeviceInfoRef { device_class: DeviceClass::Ethernet, ..default_device_info() },
1253        "wlanx5009",
1254        vec![
1255            ProvisioningMatchingRule::Common(
1256                MatchingRule::DeviceClasses(vec![DeviceClass::Ethernet])
1257            ),
1258            ProvisioningMatchingRule::InterfaceName {
1259                pattern: glob::Pattern::new("wlanx*").unwrap()
1260            }
1261        ],
1262        true;
1263        "true_multiple_rules"
1264    )]
1265    fn test_does_interface_match_provisioning_rule(
1266        info: DeviceInfoRef<'_>,
1267        interface_name: &str,
1268        matching_rules: Vec<ProvisioningMatchingRule>,
1269        want_match: bool,
1270    ) {
1271        let provisioning_rule = ProvisioningRule {
1272            matchers: HashSet::from_iter(matching_rules),
1273            action: ProvisioningAction {
1274                provisioning: ProvisioningType::Local,
1275                ..Default::default()
1276            },
1277        };
1278        assert_eq!(provisioning_rule.does_interface_match(&info, interface_name), want_match);
1279    }
1280
1281    #[test_case(
1282        vec![NameCompositionRule::Static { value: String::from("x") }],
1283        default_device_info(),
1284        "x";
1285        "single_static"
1286    )]
1287    #[test_case(
1288        vec![
1289            NameCompositionRule::Static { value: String::from("eth") },
1290            NameCompositionRule::Static { value: String::from("x") },
1291            NameCompositionRule::Static { value: String::from("100") },
1292        ],
1293        default_device_info(),
1294        "ethx100";
1295        "multiple_static"
1296    )]
1297    #[test_case(
1298        vec![NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::NormalizedMac }],
1299        DeviceInfoRef {
1300            mac: &fidl_fuchsia_net_ext::MacAddress { octets: [0x1, 0x1, 0x1, 0x1, 0x1, 0x1] },
1301            ..default_device_info()
1302        },
1303        "1";
1304        "normalized_mac"
1305    )]
1306    #[test_case(
1307        vec![
1308            NameCompositionRule::Static { value: String::from("eth") },
1309            NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::NormalizedMac },
1310        ],
1311        DeviceInfoRef {
1312            mac: &fidl_fuchsia_net_ext::MacAddress { octets: [0x1, 0x1, 0x1, 0x1, 0x1, 0x9] },
1313            ..default_device_info()
1314        },
1315        "eth9";
1316        "normalized_mac_with_static"
1317    )]
1318    #[test_case(
1319        vec![NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::DeviceClass }],
1320        DeviceInfoRef { device_class: DeviceClass::Ethernet, ..default_device_info() },
1321        "eth";
1322        "eth_device_class"
1323    )]
1324    #[test_case(
1325        vec![NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::DeviceClass }],
1326        DeviceInfoRef { device_class: DeviceClass::WlanClient, ..default_device_info() },
1327        "wlan";
1328        "wlan_device_class"
1329    )]
1330    #[test_case(
1331        vec![
1332            NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::DeviceClass },
1333            NameCompositionRule::Static { value: String::from("x") },
1334        ],
1335        DeviceInfoRef { device_class: DeviceClass::Ethernet, ..default_device_info() },
1336        "ethx";
1337        "device_class_with_static"
1338    )]
1339    #[test_case(
1340        vec![
1341            NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::DeviceClass },
1342            NameCompositionRule::Static { value: String::from("x") },
1343            NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::NormalizedMac },
1344        ],
1345        DeviceInfoRef {
1346            device_class: DeviceClass::WlanClient,
1347            mac: &fidl_fuchsia_net_ext::MacAddress { octets: [0x1, 0x1, 0x1, 0x1, 0x1, 0x8] },
1348            ..default_device_info()
1349        },
1350        "wlanx8";
1351        "device_class_with_static_with_normalized_mac"
1352    )]
1353    #[test_case(
1354        vec![
1355            NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::DeviceClass },
1356            NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::BusType },
1357            NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::BusPath },
1358        ],
1359        DeviceInfoRef {
1360            device_class: DeviceClass::Ethernet,
1361            topological_path: "/dev/sys/platform/pt/PCI0/bus/00:14.0_/00:14.0/ethernet",
1362            ..default_device_info()
1363        },
1364        "ethp0014";
1365        "device_class_with_pci_bus_type_with_bus_path"
1366    )]
1367    #[test_case(
1368        vec![
1369            NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::DeviceClass },
1370            NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::BusType },
1371            NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::BusPath },
1372        ],
1373        DeviceInfoRef {
1374            device_class: DeviceClass::Ethernet,
1375            topological_path: "/dev/sys/platform/pt/PCI0/bus/00:14.0/00:14.0/xhci/usb/004/004/ifc-000/ax88179/ethernet",
1376            ..default_device_info()
1377        },
1378        "ethu0014";
1379        "device_class_with_pci_usb_bus_type_with_bus_path"
1380    )]
1381    #[test_case(
1382        vec![
1383            NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::DeviceClass },
1384            NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::BusType },
1385            NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::BusPath },
1386        ],
1387        DeviceInfoRef {
1388            device_class: DeviceClass::Ethernet,
1389            topological_path: "/dev/sys/platform/05:00:18/usb-phy-composite/aml_usb_phy/dwc2/dwc2_phy/dwc2/usb-peripheral/function-000/cdc-eth-function/netdevice-migration/network-device",
1390            ..default_device_info()
1391        },
1392        "ethu050018";
1393        "device_class_with_dwc_usb_bus_type_with_bus_path"
1394    )]
1395    #[test_case(
1396        vec![NameCompositionRule::Default],
1397        DeviceInfoRef {
1398            device_class: DeviceClass::Ethernet,
1399            topological_path: "/dev/sys/platform/pt/PCI0/bus/00:14.0/00:14.0/xhci/usb/004/004/ifc-000/ax88179/ethernet",
1400            mac: &fidl_fuchsia_net_ext::MacAddress { octets: [0x1, 0x1, 0x1, 0x1, 0x1, 0x2] },
1401        },
1402        "ethx2";
1403        "default_usb_pci"
1404    )]
1405    #[test_case(
1406        vec![NameCompositionRule::Default],
1407        DeviceInfoRef {
1408            device_class: DeviceClass::Ethernet,
1409            topological_path: "/dev/sys/platform/05:00:18/usb-phy-composite/aml_usb_phy/dwc2/dwc2_phy/dwc2/usb-peripheral/function-000/cdc-eth-function/netdevice-migration/network-device",
1410            mac: &fidl_fuchsia_net_ext::MacAddress { octets: [0x1, 0x1, 0x1, 0x1, 0x1, 0x3] },
1411        },
1412        "ethx3";
1413        "default_usb_dwc"
1414    )]
1415    #[test_case(
1416        vec![NameCompositionRule::Default],
1417        DeviceInfoRef {
1418            device_class: DeviceClass::Ethernet,
1419            topological_path: "/dev/sys/platform/05:00:6/aml-sd-emmc/sdio/broadcom-wlanphy/wlanphy",
1420            ..default_device_info()
1421        },
1422        "eths05006";
1423        "default_sdio"
1424    )]
1425    fn test_naming_rules(
1426        composition_rules: Vec<NameCompositionRule>,
1427        info: DeviceInfoRef<'_>,
1428        expected_name: &'static str,
1429    ) {
1430        let naming_rule = NamingRule { matchers: HashSet::new(), naming_scheme: composition_rules };
1431
1432        let name = naming_rule.generate_name(&HashMap::new(), &info);
1433        assert_eq!(name.unwrap(), expected_name.to_owned());
1434    }
1435
1436    #[test]
1437    fn test_generate_name_from_naming_rule_interface_name_exists_no_reattempt() {
1438        let topo_usb = "/dev/pci-00:14.0-fidl/xhci/usb/004/004/ifc-000/ax88179/ethernet";
1439
1440        let shared_interface_name = "x".to_owned();
1441        let mut interfaces = HashMap::new();
1442        assert_matches!(
1443            interfaces.insert(
1444                InterfaceNamingIdentifier {
1445                    mac: fidl_fuchsia_net_ext::MacAddress {
1446                        octets: [0x1, 0x1, 0x1, 0x1, 0x1, 0x1]
1447                    },
1448                    topological_path: topo_usb.to_string()
1449                },
1450                shared_interface_name.clone(),
1451            ),
1452            None
1453        );
1454
1455        let naming_rule = NamingRule {
1456            matchers: HashSet::new(),
1457            naming_scheme: vec![NameCompositionRule::Static {
1458                value: shared_interface_name.clone(),
1459            }],
1460        };
1461
1462        let name = naming_rule.generate_name(&interfaces, &default_device_info()).unwrap();
1463        assert_eq!(name, shared_interface_name);
1464    }
1465
1466    // This test is different from `test_get_usb_255_with_naming_rule` as this
1467    // test increments the last byte, ensuring that the offset is reset prior
1468    // to each name being generated.
1469    #[test]
1470    fn test_generate_name_from_naming_rule_many_unique_macs() {
1471        let topo_usb = "/dev/pci-00:14.0-fidl/xhci/usb/004/004/ifc-000/ax88179/ethernet";
1472
1473        let naming_rule = NamingRule {
1474            matchers: HashSet::new(),
1475            naming_scheme: vec![NameCompositionRule::Dynamic {
1476                rule: DynamicNameCompositionRule::NormalizedMac,
1477            }],
1478        };
1479
1480        // test cases for 256 usb interfaces
1481        let mut interfaces = HashMap::new();
1482
1483        for n in 0u8..255u8 {
1484            let octets = [0x01, 0x01, 0x01, 0x01, 0x01, n];
1485            let interface_naming_id =
1486                generate_identifier(&fidl_fuchsia_net_ext::MacAddress { octets }, topo_usb);
1487            let info = DeviceInfoRef {
1488                device_class: DeviceClass::Ethernet,
1489                mac: &fidl_fuchsia_net_ext::MacAddress { octets },
1490                topological_path: topo_usb,
1491            };
1492
1493            let name =
1494                naming_rule.generate_name(&interfaces, &info).expect("failed to generate the name");
1495            assert_eq!(name, format!("{n:x}"));
1496
1497            assert_matches!(interfaces.insert(interface_naming_id, name.clone()), None);
1498        }
1499    }
1500
1501    #[test_case(true, "x"; "matches_first_rule")]
1502    #[test_case(false, "ethx1"; "fallback_default")]
1503    fn test_generate_name_from_naming_rules(match_first_rule: bool, expected_name: &'static str) {
1504        // Use an Ethernet device that is determined to have a USB bus type
1505        // from the topological path.
1506        let info = DeviceInfoRef {
1507            device_class: DeviceClass::Ethernet,
1508            mac: &fidl_fuchsia_net_ext::MacAddress { octets: [0x1, 0x1, 0x1, 0x1, 0x1, 0x1] },
1509            topological_path: "/dev/sys/platform/pt/PCI0/bus/00:14.0/00:14.0/xhci/usb/004/004/ifc-000/ax88179/ethernet",
1510        };
1511        let name = generate_name_from_naming_rules(
1512            &[
1513                NamingRule {
1514                    matchers: HashSet::from([MatchingRule::Any(match_first_rule)]),
1515                    naming_scheme: vec![NameCompositionRule::Static { value: String::from("x") }],
1516                },
1517                // Include an arbitrary rule that matches no interface
1518                // to ensure that it has no impact on the test.
1519                NamingRule {
1520                    matchers: HashSet::from([MatchingRule::Any(false)]),
1521                    naming_scheme: vec![NameCompositionRule::Static { value: String::from("y") }],
1522                },
1523            ],
1524            &HashMap::new(),
1525            &info,
1526        )
1527        .unwrap();
1528        assert_eq!(name, expected_name.to_owned());
1529    }
1530
1531    #[test_case(true, ProvisioningType::Delegated; "matches_first_rule")]
1532    #[test_case(false, ProvisioningType::Local; "fallback_default")]
1533    fn test_find_provisioning_action_from_provisioning_rules(
1534        match_first_rule: bool,
1535        expected: ProvisioningType,
1536    ) {
1537        let provisioning_action = find_provisioning_action_from_provisioning_rules(
1538            &[ProvisioningRule {
1539                matchers: HashSet::from([ProvisioningMatchingRule::Common(MatchingRule::Any(
1540                    match_first_rule,
1541                ))]),
1542                action: ProvisioningAction {
1543                    provisioning: ProvisioningType::Delegated,
1544                    ..Default::default()
1545                },
1546            }],
1547            &DeviceInfoRef {
1548                device_class: DeviceClass::WlanClient,
1549                mac: &fidl_fuchsia_net_ext::MacAddress { octets: [0x1, 0x1, 0x1, 0x1, 0x1, 0x1] },
1550                topological_path: "",
1551            },
1552            "wlans5009",
1553        );
1554        assert_eq!(provisioning_action.provisioning, expected);
1555    }
1556}