Skip to main content

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/usb-cdc-netdev/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"
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                // Lowpan interfaces are not managed or installed by Netcfg.
362                crate::InterfaceType::Lowpan => {
363                    unreachable!("unexpected to install lowpan interface")
364                }
365                crate::InterfaceType::WlanAp => INTERFACE_PREFIX_AP,
366                crate::InterfaceType::Blackhole => INTERFACE_PREFIX_BLACKHOLE,
367            }
368            .to_string(),
369            DynamicNameCompositionRule::NormalizedMac => {
370                let fidl_fuchsia_net_ext::MacAddress { octets } = info.mac;
371                let mac_identifier =
372                    get_mac_identifier_from_octets(octets, info.device_class.into(), attempt_num)?;
373                format!("{mac_identifier:x}")
374            }
375        })
376    }
377}
378
379// A rule that dictates a component of an interface's name. An interface's name
380// is determined by extracting the name of each rule, in order, and
381// concatenating the results.
382#[derive(Clone, Debug, Deserialize, PartialEq)]
383#[serde(deny_unknown_fields, rename_all = "lowercase", tag = "type")]
384pub enum NameCompositionRule {
385    Static { value: String },
386    Dynamic { rule: DynamicNameCompositionRule },
387    // The default name composition rules based on the device's BusType.
388    // Defined in `BusType::get_default_name_composition_rules`.
389    Default,
390}
391
392/// A rule that dictates how interfaces that align with the property matching
393/// rules should be named.
394#[derive(Debug, Deserialize, PartialEq)]
395#[serde(deny_unknown_fields, rename_all = "lowercase")]
396pub struct NamingRule {
397    /// A set of rules to check against an interface's properties. All rules
398    /// must apply for the naming scheme to take effect.
399    pub matchers: HashSet<MatchingRule>,
400    /// The rules to apply to the interface to produce the interface's name.
401    pub naming_scheme: Vec<NameCompositionRule>,
402}
403
404impl NamingRule {
405    // An interface's name is determined by extracting the name of each rule,
406    // in order, and concatenating the results. Returns an error if the
407    // interface name cannot be generated.
408    fn generate_name(
409        &self,
410        interfaces: &HashMap<InterfaceNamingIdentifier, String>,
411        info: &DeviceInfoRef<'_>,
412    ) -> Result<String, NameGenerationError> {
413        // When a bus type cannot be found for a path, use the USB
414        // default naming policy which uses a MAC address.
415        let bus_type = get_bus_type_for_topological_path(&info.topological_path);
416
417        // Expand any `Default` rules into the `Static` and `Dynamic` rules in a single vector.
418        // If this was being consumed once, we could avoid the call to `collect`. However, since we
419        // want to use it twice, we need to convert it to a form where the items can be itererated
420        // over without consuming them.
421        let expanded_rules = self
422            .naming_scheme
423            .iter()
424            .map(|rule| {
425                if let NameCompositionRule::Default = rule {
426                    Either::Right(bus_type.get_default_name_composition_rules().into_iter())
427                } else {
428                    Either::Left(std::iter::once(rule.clone()))
429                }
430            })
431            .flatten()
432            .collect::<Vec<_>>();
433
434        // Determine whether any rules present support retrying for a unique name.
435        let should_reattempt_on_conflict = expanded_rules.iter().any(|rule| {
436            if let NameCompositionRule::Dynamic { rule } = rule {
437                rule.supports_retry()
438            } else {
439                false
440            }
441        });
442
443        let mut attempt_num = 0u8;
444        loop {
445            let name = expanded_rules
446                .iter()
447                .map(|rule| match rule {
448                    NameCompositionRule::Static { value } => Ok(value.clone()),
449                    // Dynamic rules require the knowledge of `DeviceInfo` properties.
450                    NameCompositionRule::Dynamic { rule } => rule
451                        .get_name(info, attempt_num)
452                        .map_err(NameGenerationError::GenerationError),
453                    NameCompositionRule::Default => {
454                        unreachable!(
455                            "Default naming rules should have been pre-expanded. \
456                             Nested default rules are not supported."
457                        );
458                    }
459                })
460                .collect::<Result<String, NameGenerationError>>()?;
461
462            if interfaces.values().any(|existing_name| existing_name == &name) {
463                if should_reattempt_on_conflict {
464                    attempt_num += 1;
465                    // Try to generate another name with the modified attempt number.
466                    continue;
467                }
468
469                log::warn!(
470                    "name ({name}) already used for an interface installed by netcfg. \
471                 using name since it is possible that the interface using this name is no \
472                 longer active"
473                );
474            }
475            return Ok(name);
476        }
477    }
478
479    // An interface must align with all specified `MatchingRule`s.
480    fn does_interface_match(&self, info: &DeviceInfoRef<'_>) -> bool {
481        self.matchers.iter().all(|rule| rule.does_interface_match(info).unwrap_or_default())
482    }
483}
484
485// Find the first `NamingRule` that matches the device and attempt to
486// construct a name from the provided `NameCompositionRule`s.
487fn generate_name_from_naming_rules(
488    naming_rules: &[NamingRule],
489    interfaces: &HashMap<InterfaceNamingIdentifier, String>,
490    info: &DeviceInfoRef<'_>,
491) -> Result<String, NameGenerationError> {
492    // TODO(https://fxbug.dev/42086002): Consider adding an option to the rules to allow
493    // fallback rules when name generation fails.
494    // Use the first naming rule that matches the interface to enforce consistent
495    // interface names, even if there are other matching rules.
496    let fallback_rule = fallback_naming_rule();
497    let first_matching_rule =
498        naming_rules.iter().find(|rule| rule.does_interface_match(&info)).unwrap_or(
499            // When there are no `NamingRule`s that match the device,
500            // use a fallback rule that has the Default naming scheme.
501            &fallback_rule,
502        );
503
504    first_matching_rule.generate_name(interfaces, &info)
505}
506
507// Matches any device and uses the default naming rule.
508fn fallback_naming_rule() -> NamingRule {
509    NamingRule {
510        matchers: HashSet::from([MatchingRule::Any(true)]),
511        naming_scheme: vec![NameCompositionRule::Default],
512    }
513}
514
515/// The provision action to take if the matchers are satisfied.
516#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Default)]
517#[serde(deny_unknown_fields, rename_all = "lowercase")]
518pub struct ProvisioningAction {
519    /// The type of the provisioning.
520    pub provisioning: ProvisioningType,
521    /// Where the netstack managed routes should be installed.
522    pub netstack_managed_routes_designation: Option<NetstackManagedRoutesDesignation>,
523}
524
525/// Whether the interface should be provisioned locally by netcfg, or
526/// delegated. Provisioning is the set of events that occurs after
527/// interface enumeration, such as starting a DHCP client and assigning
528/// an IP to the interface. Provisioning actions work to support
529/// Internet connectivity.
530#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Default)]
531#[serde(deny_unknown_fields, rename_all = "lowercase")]
532pub enum ProvisioningType {
533    /// Netcfg will provision the interface
534    #[default]
535    Local,
536    /// Netcfg will not provision the interface. The provisioning
537    /// of the interface will occur elsewhere
538    Delegated,
539}
540
541impl ProvisioningType {
542    /// Whether the interface should be tracked in the network registry.
543    ///
544    /// Locally provisioned interfaces are tracked in the network registry because their
545    /// lifetime is managed by netcfg. Delegated interfaces are not tracked in the network
546    /// registry because their presence is communicated by the network-socket-proxy service.
547    pub fn track_in_network_registry(&self) -> bool {
548        match self {
549            ProvisioningType::Local => true,
550            ProvisioningType::Delegated => false,
551        }
552    }
553}
554
555/// Where the netstack managed routes should be stored.
556///
557/// Mirrors [`fnet_interfaces_admin::NetstackManagedRoutesDesignation`].
558#[derive(Copy, Clone, Debug, Deserialize, PartialEq)]
559#[serde(deny_unknown_fields, rename_all = "snake_case")]
560pub enum NetstackManagedRoutesDesignation {
561    Main,
562    InterfaceLocal,
563}
564
565impl From<NetstackManagedRoutesDesignation>
566    for fnet_interfaces_admin::NetstackManagedRoutesDesignation
567{
568    fn from(value: NetstackManagedRoutesDesignation) -> Self {
569        match value {
570            NetstackManagedRoutesDesignation::Main => Self::Main(fnet_interfaces_admin::Empty),
571            NetstackManagedRoutesDesignation::InterfaceLocal => {
572                Self::InterfaceLocal(fnet_interfaces_admin::Empty)
573            }
574        }
575    }
576}
577
578/// A rule that dictates how interfaces that align with the property matching
579/// rules should be provisioned.
580#[derive(Debug, Deserialize, PartialEq)]
581#[serde(deny_unknown_fields, rename_all = "lowercase")]
582pub struct ProvisioningRule {
583    /// A set of rules to check against an interface's properties. All rules
584    /// must apply for the provisioning action to take effect.
585    pub matchers: HashSet<ProvisioningMatchingRule>,
586    /// The provisioning policy that netcfg applies to a matching
587    /// interface.
588    #[serde(flatten)]
589    pub action: ProvisioningAction,
590}
591
592// A ref version of `devices::DeviceInfo` to avoid the need to clone data
593// unnecessarily. Devices without MAC are not supported yet, see
594// `add_new_device` in `lib.rs`. This makes mac into a required field for
595// ease of use.
596pub(super) struct DeviceInfoRef<'a> {
597    pub(super) device_class: DeviceClass,
598    pub(super) mac: &'a fidl_fuchsia_net_ext::MacAddress,
599    pub(super) topological_path: &'a str,
600}
601
602impl<'a> DeviceInfoRef<'a> {
603    pub(super) fn interface_type(&self) -> crate::InterfaceType {
604        let DeviceInfoRef { device_class, mac: _, topological_path: _ } = self;
605        (*device_class).into()
606    }
607
608    pub(super) fn is_wlan_ap(&self) -> bool {
609        let DeviceInfoRef { device_class, mac: _, topological_path: _ } = self;
610        match device_class {
611            DeviceClass::WlanAp => true,
612            DeviceClass::WlanClient
613            | DeviceClass::Virtual
614            | DeviceClass::Ethernet
615            | DeviceClass::Bridge
616            | DeviceClass::Ppp
617            | DeviceClass::Lowpan
618            | DeviceClass::Blackhole => false,
619        }
620    }
621}
622
623impl ProvisioningRule {
624    // An interface must align with all specified `MatchingRule`s.
625    fn does_interface_match(&self, info: &DeviceInfoRef<'_>, interface_name: &str) -> bool {
626        self.matchers
627            .iter()
628            .all(|rule| rule.does_interface_match(info, interface_name).unwrap_or_default())
629    }
630}
631
632// Find the first `ProvisioningRule` that matches the device and get
633// the associated `ProvisioningAction`. By default, use Local provisioning
634// so that Netcfg will provision interfaces unless configuration
635// indicates otherwise.
636pub(crate) fn find_provisioning_action_from_provisioning_rules(
637    provisioning_rules: &[ProvisioningRule],
638    info: &DeviceInfoRef<'_>,
639    interface_name: &str,
640) -> ProvisioningAction {
641    provisioning_rules
642        .iter()
643        .find_map(|rule| {
644            if rule.does_interface_match(&info, &interface_name) { Some(rule.action) } else { None }
645        })
646        .unwrap_or_default()
647}
648
649#[cfg(test)]
650mod tests {
651    use super::*;
652    use assert_matches::assert_matches;
653    use test_case::test_case;
654
655    // This is a lossy conversion between `InterfaceType` and `DeviceClass`
656    // that allows tests to use a `devices::DeviceInfo` struct instead of
657    // handling the fields individually.
658    fn device_class_from_interface_type(ty: crate::InterfaceType) -> DeviceClass {
659        match ty {
660            crate::InterfaceType::Ethernet => DeviceClass::Ethernet,
661            crate::InterfaceType::Lowpan => DeviceClass::Lowpan,
662            crate::InterfaceType::WlanClient => DeviceClass::WlanClient,
663            crate::InterfaceType::WlanAp => DeviceClass::WlanAp,
664            crate::InterfaceType::Blackhole => DeviceClass::Blackhole,
665        }
666    }
667
668    // usb interfaces
669    #[test_case(
670        "/dev/sys/platform/pt/PCI0/bus/00:14.0/00:14.0/xhci/usb/004/004/ifc-000/ax88179/ethernet",
671        [0x01, 0x01, 0x01, 0x01, 0x01, 0x01],
672        crate::InterfaceType::WlanClient,
673        "wlanx1";
674        "usb_wlan"
675    )]
676    #[test_case(
677        "/dev/sys/platform/pt/PCI0/bus/00:15.0/00:15.0/xhci/usb/004/004/ifc-000/ax88179/ethernet",
678        [0x02, 0x02, 0x02, 0x02, 0x02, 0x02],
679        crate::InterfaceType::Ethernet,
680        "ethx2";
681        "usb_eth"
682    )]
683    // pci interfaces
684    #[test_case(
685        "/dev/sys/platform/pt/PCI0/bus/00:14.0/00:14.0/ethernet",
686        [0x03, 0x03, 0x03, 0x03, 0x03, 0x03],
687        crate::InterfaceType::WlanClient,
688        "wlanp0014";
689        "pci_wlan"
690    )]
691    #[test_case(
692        "/dev/sys/platform/pt/PCI0/bus/00:15.0/00:14.0/ethernet",
693        [0x04, 0x04, 0x04, 0x04, 0x04, 0x04],
694        crate::InterfaceType::Ethernet,
695        "ethp0015";
696        "pci_eth"
697    )]
698    // platform interfaces (ethernet jack and sdio devices)
699    #[test_case(
700        "/dev/sys/platform/05:00:6/aml-sd-emmc/sdio/broadcom-wlanphy",
701        [0x05, 0x05, 0x05, 0x05, 0x05, 0x05],
702        crate::InterfaceType::WlanClient,
703        "wlans05006";
704        "platform_wlan"
705    )]
706    #[test_case(
707        "/dev/sys/platform/04:02:7/aml-ethernet/Designware-MAC/ethernet",
708        [0x07, 0x07, 0x07, 0x07, 0x07, 0x07],
709        crate::InterfaceType::Ethernet,
710        "ethx7";
711        "platform_eth"
712    )]
713    // unknown interfaces
714    #[test_case(
715        "/dev/sys/unknown",
716        [0x08, 0x08, 0x08, 0x08, 0x08, 0x08],
717        crate::InterfaceType::WlanClient,
718        "wlanx8";
719        "unknown_wlan1"
720    )]
721    #[test_case(
722        "unknown",
723        [0x09, 0x09, 0x09, 0x09, 0x09, 0x09],
724        crate::InterfaceType::WlanClient,
725        "wlanx9";
726        "unknown_wlan2"
727    )]
728    #[test_case(
729        "unknown",
730        [0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a],
731        crate::InterfaceType::WlanAp,
732        "apxa";
733        "unknown_ap"
734    )]
735    #[test_case(
736        "/dev/sys/platform/pt/PC00/bus/00:1e.0/00_1e_0/virtio-net/network-device",
737        [0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b],
738        crate::InterfaceType::Ethernet,
739        "ethv001e";
740        "virtio_attached_ethernet"
741    )]
742    // NB: name generation for blackhole interfaces is never expected to be invoked.
743    #[test_case(
744        "/dev/sys/platform/pt/PCI0/bus/00:15.0/00:15.0/xhci/usb/004/004/ifc-000/ax88179/ethernet",
745        [0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c],
746        crate::InterfaceType::Blackhole,
747        "blackholexc";
748        "usb_blackhole")]
749    fn test_generate_name(
750        topological_path: &'static str,
751        mac: [u8; 6],
752        interface_type: crate::InterfaceType,
753        want_name: &'static str,
754    ) {
755        let interface_naming_config = InterfaceNamingConfig::from_naming_rules(vec![]);
756        let name = interface_naming_config
757            .generate_name(&DeviceInfoRef {
758                device_class: device_class_from_interface_type(interface_type),
759                mac: &fidl_fuchsia_net_ext::MacAddress { octets: mac },
760                topological_path,
761            })
762            .expect("failed to generate the name");
763        assert_eq!(name, want_name);
764    }
765
766    struct StableNameTestCase {
767        topological_path: &'static str,
768        mac: [u8; 6],
769        interface_type: crate::InterfaceType,
770        want_name: &'static str,
771        expected_size: usize,
772    }
773
774    // Base case. Interface should be added to config.
775    #[test_case([StableNameTestCase {
776        topological_path: "/dev/sys/platform/pt/PCI0/bus/00:14.0_/00:14.0/ethernet",
777        mac: [0x01, 0x01, 0x01, 0x01, 0x01, 0x01],
778        interface_type: crate::InterfaceType::WlanClient,
779        want_name: "wlanp0014",
780        expected_size: 1 }];
781        "single_interface"
782    )]
783    // Test case that shares the same topo path and different MAC, but same
784    // last octet. Expect to see second interface added with different name.
785    #[test_case([StableNameTestCase {
786        topological_path: "/dev/sys/platform/pt/PCI0/bus/00:14.0_/00:14.0/ethernet",
787        mac: [0x01, 0x01, 0x01, 0x01, 0x01, 0x01],
788        interface_type: crate::InterfaceType::WlanClient,
789        want_name: "wlanp0014",
790        expected_size: 1}, StableNameTestCase {
791        topological_path: "/dev/sys/platform/pt/PCI0/bus/00:14.0_/00:14.0/ethernet",
792        mac: [0xFE, 0x01, 0x01, 0x01, 0x01, 0x01],
793        interface_type: crate::InterfaceType::WlanAp,
794        want_name: "app0014",
795        expected_size: 2 }];
796        "two_interfaces_same_topo_path_different_mac"
797    )]
798    #[test_case([StableNameTestCase {
799        topological_path: "/dev/sys/platform/pt/PCI0/bus/00:14.0_/00:14.0/ethernet",
800        mac: [0x01, 0x01, 0x01, 0x01, 0x01, 0x01],
801        interface_type: crate::InterfaceType::WlanClient,
802        want_name: "wlanp0014",
803        expected_size: 1}, StableNameTestCase {
804        topological_path: "/dev/sys/platform/pt/PCI0/bus/01:00.0/01:00.0/iwlwifi-wlan-softmac/wlan-ethernet/ethernet",
805        mac: [0xFE, 0x01, 0x01, 0x01, 0x01, 0x01],
806        interface_type: crate::InterfaceType::Ethernet,
807        want_name: "ethp01",
808        expected_size: 2 }];
809        "two_distinct_interfaces"
810    )]
811    // Test case that labels iwilwifi as ethernet, then changes the device
812    // class to wlan. The test should detect that the device class doesn't
813    // match the interface name, and overwrite with the new interface name
814    // that does match.
815    #[test_case([StableNameTestCase {
816        topological_path: "/dev/sys/platform/pt/PCI0/bus/01:00.0/01:00.0/iwlwifi-wlan-softmac/wlan-ethernet/ethernet",
817        mac: [0x01, 0x01, 0x01, 0x01, 0x01, 0x01],
818        interface_type: crate::InterfaceType::Ethernet,
819        want_name: "ethp01",
820        expected_size: 1 }, StableNameTestCase {
821        topological_path: "/dev/sys/platform/pt/PCI0/bus/01:00.0/01:00.0/iwlwifi-wlan-softmac/wlan-ethernet/ethernet",
822        mac: [0x01, 0x01, 0x01, 0x01, 0x01, 0x01],
823        interface_type: crate::InterfaceType::WlanClient,
824        want_name: "wlanp01",
825        expected_size: 1 }];
826        "two_interfaces_different_device_class"
827    )]
828    fn test_generate_stable_name(test_cases: impl IntoIterator<Item = StableNameTestCase>) {
829        let mut interface_naming_config = InterfaceNamingConfig::from_naming_rules(vec![]);
830
831        // query an existing interface with the same topo path and a different mac address
832        for (
833            _i,
834            StableNameTestCase { topological_path, mac, interface_type, want_name, expected_size },
835        ) in test_cases.into_iter().enumerate()
836        {
837            let (name, _identifier) = interface_naming_config
838                .generate_stable_name(
839                    topological_path,
840                    &fidl_fuchsia_net_ext::MacAddress { octets: mac },
841                    device_class_from_interface_type(interface_type),
842                )
843                .expect("failed to get the interface name");
844            assert_eq!(name, want_name);
845            // Ensure the number of interfaces we expect are present.
846            assert_eq!(interface_naming_config.interfaces.len(), expected_size);
847        }
848    }
849
850    #[test]
851    fn test_get_usb_255() {
852        let topo_usb = "/dev/pci-00:14.0-fidl/xhci/usb/004/004/ifc-000/ax88179/ethernet";
853
854        // test cases for 256 usb interfaces
855        let mut config = InterfaceNamingConfig::from_naming_rules(vec![]);
856        for n in 0u8..255u8 {
857            let octets = [n, 0x01, 0x01, 0x01, 0x01, 00];
858
859            let interface_naming_id =
860                generate_identifier(&fidl_fuchsia_net_ext::MacAddress { octets }, topo_usb);
861
862            let name = config
863                .generate_name(&DeviceInfoRef {
864                    device_class: device_class_from_interface_type(
865                        crate::InterfaceType::WlanClient,
866                    ),
867                    mac: &fidl_fuchsia_net_ext::MacAddress { octets },
868                    topological_path: topo_usb,
869                })
870                .expect("failed to generate the name");
871            assert_eq!(name, format!("{}{:x}", "wlanx", n));
872            assert_matches!(config.interfaces.insert(interface_naming_id, name), None);
873        }
874
875        let octets = [0x00, 0x00, 0x01, 0x01, 0x01, 00];
876        assert!(
877            config
878                .generate_name(&DeviceInfoRef {
879                    device_class: device_class_from_interface_type(
880                        crate::InterfaceType::WlanClient
881                    ),
882                    mac: &fidl_fuchsia_net_ext::MacAddress { octets },
883                    topological_path: topo_usb
884                },)
885                .is_err()
886        );
887    }
888
889    #[test]
890    fn test_get_usb_255_with_naming_rule() {
891        let topo_usb = "/dev/pci-00:14.0-fidl/xhci/usb/004/004/ifc-000/ax88179/ethernet";
892
893        let naming_rule = NamingRule {
894            matchers: HashSet::new(),
895            naming_scheme: vec![
896                NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::NormalizedMac },
897                NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::NormalizedMac },
898            ],
899        };
900
901        // test cases for 256 usb interfaces
902        let mut config = InterfaceNamingConfig::from_naming_rules(vec![naming_rule]);
903        for n in 0u8..255u8 {
904            let octets = [n, 0x01, 0x01, 0x01, 0x01, 00];
905            let interface_naming_id =
906                generate_identifier(&fidl_fuchsia_net_ext::MacAddress { octets }, topo_usb);
907
908            let info = DeviceInfoRef {
909                device_class: DeviceClass::Ethernet,
910                mac: &fidl_fuchsia_net_ext::MacAddress { octets },
911                topological_path: topo_usb,
912            };
913
914            let name = config.generate_name(&info).expect("failed to generate the name");
915            // With only NormalizedMac as a NameCompositionRule, the name
916            // should simply be the NormalizedMac itself.
917            assert_eq!(name, format!("{n:x}{n:x}"));
918
919            assert_matches!(config.interfaces.insert(interface_naming_id, name), None);
920        }
921
922        let octets = [0x00, 0x00, 0x01, 0x01, 0x01, 00];
923        assert!(
924            config
925                .generate_name(&DeviceInfoRef {
926                    device_class: DeviceClass::Ethernet,
927                    mac: &fidl_fuchsia_net_ext::MacAddress { octets },
928                    topological_path: topo_usb
929                })
930                .is_err()
931        );
932    }
933
934    // Arbitrary values for devices::DeviceInfo for cases where DeviceInfo has
935    // no impact on the test.
936    fn default_device_info() -> DeviceInfoRef<'static> {
937        DeviceInfoRef {
938            device_class: DeviceClass::Ethernet,
939            mac: &fidl_fuchsia_net_ext::MacAddress { octets: [0x1, 0x1, 0x1, 0x1, 0x1, 0x1] },
940            topological_path: "",
941        }
942    }
943
944    #[test_case(
945        "/dev/sys/platform/pt/PCI0/bus/00:14.0_/00:14.0/ethernet",
946        vec![BusType::PCI],
947        BusType::PCI,
948        true,
949        "0014";
950        "pci_match"
951    )]
952    #[test_case(
953        "/dev/sys/platform/pt/PCI0/bus/00:14.0_/00:14.0/ethernet",
954        vec![BusType::USB, BusType::SDIO],
955        BusType::PCI,
956        false,
957        "0014";
958        "pci_no_match"
959    )]
960    #[test_case(
961        "/dev/sys/platform/pt/PCI0/bus/00:14.0/00:14.0/xhci/usb/004/004/ifc-000/ax88179/ethernet",
962        vec![BusType::USB],
963        BusType::USB,
964        true,
965        "0014";
966        "pci_usb_match"
967    )]
968    #[test_case(
969        "/dev/sys/platform/05:00:18/usb-phy-composite/aml_usb_phy/dwc2/dwc2_phy/dwc2/usb-peripheral/function-000/usb-cdc-netdev/network-device",
970        vec![BusType::USB],
971        BusType::USB,
972        true,
973        "050018";
974        "dwc_usb_match"
975    )]
976    // Same topological path as the case for USB, but with
977    // non-matching bus types. Ensure that even though PCI is
978    // present in the topological path, it does not match a PCI
979    // controller.
980    #[test_case(
981        "/dev/sys/platform/pt/PCI0/bus/00:14.0/00:14.0/xhci/usb/004/004/ifc-000/ax88179/ethernet",
982        vec![BusType::PCI, BusType::SDIO],
983        BusType::USB,
984        false,
985        "0014";
986        "usb_no_match"
987    )]
988    #[test_case(
989        "/dev/sys/platform/05:00:6/aml-sd-emmc/sdio/broadcom-wlanphy",
990        vec![BusType::SDIO],
991        BusType::SDIO,
992        true,
993        "05006";
994        "sdio_match"
995    )]
996    #[test_case(
997        "/dev/sys/platform/pt/PC00/bus/00:1e.0/00_1e_0/virtio-net/network-device",
998        vec![BusType::VirtIo],
999        BusType::VirtIo,
1000        true,
1001        "001e";
1002        "virtio_match_alternate_location"
1003    )]
1004    #[test_case(
1005        "/dev/sys/platform/pt/PC00/bus/<malformed>/00_1e_0/virtio-net/network-device",
1006        vec![BusType::VirtIo],
1007        BusType::VirtIo,
1008        true,
1009        "001e";
1010        "virtio_matches_underscore_path"
1011    )]
1012    #[test_case(
1013        "/dev/sys/platform/pt/PC00/bus/00:1e.1/00_1e_1/virtio-net/network-device",
1014        vec![BusType::VirtIo],
1015        BusType::VirtIo,
1016        true,
1017        "001e1";
1018        "virtio_match_alternate_no_trim"
1019    )]
1020    #[test_case(
1021        "/dev/sys/platform/pt/PC00/bus/<unrecognized_bus_path>/network-device",
1022        vec![BusType::Unknown],
1023        BusType::Unknown,
1024        true,
1025        "ffffff";
1026        "unknown_bus_match_unrecognized"
1027    )]
1028    fn test_interface_matching_and_naming_by_bus_properties(
1029        topological_path: &'static str,
1030        bus_types: Vec<BusType>,
1031        expected_bus_type: BusType,
1032        want_match: bool,
1033        want_name: &'static str,
1034    ) {
1035        let device_info = DeviceInfoRef {
1036            topological_path: topological_path,
1037            // `device_class` and `mac` have no effect on `BusType`
1038            // matching, so we use arbitrary values.
1039            ..default_device_info()
1040        };
1041
1042        // Verify the `BusType` determined from the device's
1043        // topological path.
1044        let bus_type = get_bus_type_for_topological_path(&device_info.topological_path);
1045        assert_eq!(bus_type, expected_bus_type);
1046
1047        // Create a matching rule for the provided `BusType` list.
1048        let matching_rule = MatchingRule::BusTypes(bus_types);
1049        let does_interface_match = matching_rule.does_interface_match(&device_info).unwrap();
1050        assert_eq!(does_interface_match, want_match);
1051
1052        let name = get_normalized_bus_path_for_topo_path(&device_info.topological_path);
1053        assert_eq!(name, want_name);
1054
1055        // Ensure that calling again will decrement this. It's unfortunate to need to encode this
1056        // in the test itself, but each test runs separately, so we can't rely on static storage
1057        // between test invocations.
1058        if want_name == "ffffff" {
1059            let name = get_normalized_bus_path_for_topo_path(&device_info.topological_path);
1060            assert_eq!(name, "fffffe");
1061        }
1062    }
1063
1064    // Glob matches the number pattern of XX:XX in the path.
1065    #[test_case(
1066        "/dev/sys/platform/pt/PCI0/bus/00:14.0_/00:14.0/ethernet",
1067        r"*[0-9][0-9]:[0-9][0-9]*",
1068        true;
1069        "pattern_matches"
1070    )]
1071    #[test_case("pattern/will/match/anything", r"*", true; "pattern_matches_any")]
1072    // Glob checks for '00' after the colon but it will not find it.
1073    #[test_case(
1074        "/dev/sys/platform/pt/PCI0/bus/00:14.0_/00:14.0/ethernet",
1075        r"*[0-9][0-9]:00*",
1076        false;
1077        "no_matches"
1078    )]
1079    fn test_interface_matching_by_topological_path(
1080        topological_path: &'static str,
1081        glob_str: &'static str,
1082        want_match: bool,
1083    ) {
1084        let device_info = DeviceInfoRef {
1085            topological_path,
1086            // `device_class` and `mac` have no effect on `TopologicalPath`
1087            // matching, so we use arbitrary values.
1088            ..default_device_info()
1089        };
1090
1091        // Create a matching rule for the provided glob expression.
1092        let matching_rule = MatchingRule::TopologicalPath(glob::Pattern::new(glob_str).unwrap());
1093        let does_interface_match = matching_rule.does_interface_match(&device_info).unwrap();
1094        assert_eq!(does_interface_match, want_match);
1095    }
1096
1097    // Glob matches the default naming by MAC address.
1098    #[test_case(
1099        "ethx5",
1100        r"ethx[0-9]*",
1101        true;
1102        "pattern_matches"
1103    )]
1104    #[test_case("arbitraryname", r"*", true; "pattern_matches_any")]
1105    // Glob matches default naming by SDIO + bus path.
1106    #[test_case(
1107        "wlans1002",
1108        r"eths[0-9][0-9][0-9][0-9]*",
1109        false;
1110        "no_matches"
1111    )]
1112    fn test_interface_matching_by_interface_name(
1113        interface_name: &'static str,
1114        glob_str: &'static str,
1115        want_match: bool,
1116    ) {
1117        // Create a matching rule for the provided glob expression.
1118        let provisioning_matching_rule = ProvisioningMatchingRule::InterfaceName {
1119            pattern: glob::Pattern::new(glob_str).unwrap(),
1120        };
1121        let does_interface_match = provisioning_matching_rule
1122            .does_interface_match(&default_device_info(), interface_name)
1123            .unwrap();
1124        assert_eq!(does_interface_match, want_match);
1125    }
1126
1127    #[test_case(
1128        DeviceClass::Ethernet,
1129        vec![DeviceClass::Ethernet],
1130        true;
1131        "eth_match"
1132    )]
1133    #[test_case(
1134        DeviceClass::Ethernet,
1135        vec![DeviceClass::WlanClient, DeviceClass::WlanAp],
1136        false;
1137        "eth_no_match"
1138    )]
1139    #[test_case(
1140        DeviceClass::WlanClient,
1141        vec![DeviceClass::WlanClient],
1142        true;
1143        "wlan_match"
1144    )]
1145    #[test_case(
1146        DeviceClass::WlanClient,
1147        vec![DeviceClass::Ethernet, DeviceClass::WlanAp],
1148        false;
1149        "wlan_no_match"
1150    )]
1151    #[test_case(
1152        DeviceClass::WlanAp,
1153        vec![DeviceClass::WlanAp],
1154        true;
1155        "ap_match"
1156    )]
1157    #[test_case(
1158        DeviceClass::WlanAp,
1159        vec![DeviceClass::Ethernet, DeviceClass::WlanClient],
1160        false;
1161        "ap_no_match"
1162    )]
1163    fn test_interface_matching_by_device_class(
1164        device_class: DeviceClass,
1165        device_classes: Vec<DeviceClass>,
1166        want_match: bool,
1167    ) {
1168        let device_info = DeviceInfoRef { device_class, ..default_device_info() };
1169
1170        // Create a matching rule for the provided `DeviceClass` list.
1171        let matching_rule = MatchingRule::DeviceClasses(device_classes);
1172        let does_interface_match = matching_rule.does_interface_match(&device_info).unwrap();
1173        assert_eq!(does_interface_match, want_match);
1174    }
1175
1176    // The device information should not have any impact on whether the
1177    // interface matches, but we use Ethernet and Wlan as base cases
1178    // to ensure that all interfaces are accepted or all interfaces
1179    // are rejected.
1180    #[test_case(
1181        DeviceClass::Ethernet,
1182        "/dev/pci-00:15.0-fidl/xhci/usb/004/004/ifc-000/ax88179/ethernet"
1183    )]
1184    #[test_case(DeviceClass::WlanClient, "/dev/pci-00:14.0/ethernet")]
1185    fn test_interface_matching_by_any_matching_rule(
1186        device_class: DeviceClass,
1187        topological_path: &'static str,
1188    ) {
1189        let device_info = DeviceInfoRef {
1190            device_class,
1191            mac: &fidl_fuchsia_net_ext::MacAddress { octets: [0x1, 0x1, 0x1, 0x1, 0x1, 0x1] },
1192            topological_path,
1193        };
1194
1195        // Create a matching rule that should match any interface.
1196        let matching_rule = MatchingRule::Any(true);
1197        let does_interface_match = matching_rule.does_interface_match(&device_info).unwrap();
1198        assert!(does_interface_match);
1199
1200        // Create a matching rule that should reject any interface.
1201        let matching_rule = MatchingRule::Any(false);
1202        let does_interface_match = matching_rule.does_interface_match(&device_info).unwrap();
1203        assert!(!does_interface_match);
1204    }
1205
1206    #[test_case(
1207        DeviceInfoRef { device_class: DeviceClass::Ethernet, ..default_device_info() },
1208        vec![MatchingRule::DeviceClasses(vec![DeviceClass::WlanClient])],
1209        false;
1210        "false_single_rule"
1211    )]
1212    #[test_case(
1213        DeviceInfoRef { device_class: DeviceClass::Ethernet, ..default_device_info() },
1214        vec![MatchingRule::DeviceClasses(vec![DeviceClass::WlanClient]), MatchingRule::Any(true)],
1215        false;
1216        "false_one_rule_of_multiple"
1217    )]
1218    #[test_case(
1219        DeviceInfoRef { device_class: DeviceClass::Ethernet, ..default_device_info() },
1220        vec![MatchingRule::Any(true)],
1221        true;
1222        "true_single_rule"
1223    )]
1224    #[test_case(
1225        DeviceInfoRef { device_class: DeviceClass::Ethernet, ..default_device_info() },
1226        vec![MatchingRule::DeviceClasses(vec![DeviceClass::Ethernet]), MatchingRule::Any(true)],
1227        true;
1228        "true_multiple_rules"
1229    )]
1230    fn test_does_interface_match(
1231        info: DeviceInfoRef<'_>,
1232        matching_rules: Vec<MatchingRule>,
1233        want_match: bool,
1234    ) {
1235        let naming_rule =
1236            NamingRule { matchers: HashSet::from_iter(matching_rules), naming_scheme: Vec::new() };
1237        assert_eq!(naming_rule.does_interface_match(&info), want_match);
1238    }
1239
1240    #[test_case(
1241        DeviceInfoRef { device_class: DeviceClass::Ethernet, ..default_device_info() },
1242        "",
1243        vec![
1244            ProvisioningMatchingRule::Common(
1245                MatchingRule::DeviceClasses(vec![DeviceClass::WlanClient])
1246            )
1247        ],
1248        false;
1249        "false_single_rule"
1250    )]
1251    #[test_case(
1252        DeviceInfoRef { device_class: DeviceClass::WlanClient, ..default_device_info() },
1253        "wlanx5009",
1254        vec![
1255            ProvisioningMatchingRule::InterfaceName {
1256                pattern: glob::Pattern::new("ethx*").unwrap()
1257            },
1258            ProvisioningMatchingRule::Common(MatchingRule::Any(true))
1259        ],
1260        false;
1261        "false_one_rule_of_multiple"
1262    )]
1263    #[test_case(
1264        DeviceInfoRef { device_class: DeviceClass::Ethernet, ..default_device_info() },
1265        "",
1266        vec![ProvisioningMatchingRule::Common(MatchingRule::Any(true))],
1267        true;
1268        "true_single_rule"
1269    )]
1270    #[test_case(
1271        DeviceInfoRef { device_class: DeviceClass::Ethernet, ..default_device_info() },
1272        "wlanx5009",
1273        vec![
1274            ProvisioningMatchingRule::Common(
1275                MatchingRule::DeviceClasses(vec![DeviceClass::Ethernet])
1276            ),
1277            ProvisioningMatchingRule::InterfaceName {
1278                pattern: glob::Pattern::new("wlanx*").unwrap()
1279            }
1280        ],
1281        true;
1282        "true_multiple_rules"
1283    )]
1284    fn test_does_interface_match_provisioning_rule(
1285        info: DeviceInfoRef<'_>,
1286        interface_name: &str,
1287        matching_rules: Vec<ProvisioningMatchingRule>,
1288        want_match: bool,
1289    ) {
1290        let provisioning_rule = ProvisioningRule {
1291            matchers: HashSet::from_iter(matching_rules),
1292            action: ProvisioningAction {
1293                provisioning: ProvisioningType::Local,
1294                ..Default::default()
1295            },
1296        };
1297        assert_eq!(provisioning_rule.does_interface_match(&info, interface_name), want_match);
1298    }
1299
1300    #[test_case(
1301        vec![NameCompositionRule::Static { value: String::from("x") }],
1302        default_device_info(),
1303        "x";
1304        "single_static"
1305    )]
1306    #[test_case(
1307        vec![
1308            NameCompositionRule::Static { value: String::from("eth") },
1309            NameCompositionRule::Static { value: String::from("x") },
1310            NameCompositionRule::Static { value: String::from("100") },
1311        ],
1312        default_device_info(),
1313        "ethx100";
1314        "multiple_static"
1315    )]
1316    #[test_case(
1317        vec![NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::NormalizedMac }],
1318        DeviceInfoRef {
1319            mac: &fidl_fuchsia_net_ext::MacAddress { octets: [0x1, 0x1, 0x1, 0x1, 0x1, 0x1] },
1320            ..default_device_info()
1321        },
1322        "1";
1323        "normalized_mac"
1324    )]
1325    #[test_case(
1326        vec![
1327            NameCompositionRule::Static { value: String::from("eth") },
1328            NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::NormalizedMac },
1329        ],
1330        DeviceInfoRef {
1331            mac: &fidl_fuchsia_net_ext::MacAddress { octets: [0x1, 0x1, 0x1, 0x1, 0x1, 0x9] },
1332            ..default_device_info()
1333        },
1334        "eth9";
1335        "normalized_mac_with_static"
1336    )]
1337    #[test_case(
1338        vec![NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::DeviceClass }],
1339        DeviceInfoRef { device_class: DeviceClass::Ethernet, ..default_device_info() },
1340        "eth";
1341        "eth_device_class"
1342    )]
1343    #[test_case(
1344        vec![NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::DeviceClass }],
1345        DeviceInfoRef { device_class: DeviceClass::WlanClient, ..default_device_info() },
1346        "wlan";
1347        "wlan_device_class"
1348    )]
1349    #[test_case(
1350        vec![
1351            NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::DeviceClass },
1352            NameCompositionRule::Static { value: String::from("x") },
1353        ],
1354        DeviceInfoRef { device_class: DeviceClass::Ethernet, ..default_device_info() },
1355        "ethx";
1356        "device_class_with_static"
1357    )]
1358    #[test_case(
1359        vec![
1360            NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::DeviceClass },
1361            NameCompositionRule::Static { value: String::from("x") },
1362            NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::NormalizedMac },
1363        ],
1364        DeviceInfoRef {
1365            device_class: DeviceClass::WlanClient,
1366            mac: &fidl_fuchsia_net_ext::MacAddress { octets: [0x1, 0x1, 0x1, 0x1, 0x1, 0x8] },
1367            ..default_device_info()
1368        },
1369        "wlanx8";
1370        "device_class_with_static_with_normalized_mac"
1371    )]
1372    #[test_case(
1373        vec![
1374            NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::DeviceClass },
1375            NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::BusType },
1376            NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::BusPath },
1377        ],
1378        DeviceInfoRef {
1379            device_class: DeviceClass::Ethernet,
1380            topological_path: "/dev/sys/platform/pt/PCI0/bus/00:14.0_/00:14.0/ethernet",
1381            ..default_device_info()
1382        },
1383        "ethp0014";
1384        "device_class_with_pci_bus_type_with_bus_path"
1385    )]
1386    #[test_case(
1387        vec![
1388            NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::DeviceClass },
1389            NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::BusType },
1390            NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::BusPath },
1391        ],
1392        DeviceInfoRef {
1393            device_class: DeviceClass::Ethernet,
1394            topological_path: "/dev/sys/platform/pt/PCI0/bus/00:14.0/00:14.0/xhci/usb/004/004/ifc-000/ax88179/ethernet",
1395            ..default_device_info()
1396        },
1397        "ethu0014";
1398        "device_class_with_pci_usb_bus_type_with_bus_path"
1399    )]
1400    #[test_case(
1401        vec![
1402            NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::DeviceClass },
1403            NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::BusType },
1404            NameCompositionRule::Dynamic { rule: DynamicNameCompositionRule::BusPath },
1405        ],
1406        DeviceInfoRef {
1407            device_class: DeviceClass::Ethernet,
1408            topological_path: "/dev/sys/platform/05:00:18/usb-phy-composite/aml_usb_phy/dwc2/dwc2_phy/dwc2/usb-peripheral/function-000/usb-cdc-netdev/network-device",
1409            ..default_device_info()
1410        },
1411        "ethu050018";
1412        "device_class_with_dwc_usb_bus_type_with_bus_path"
1413    )]
1414    #[test_case(
1415        vec![NameCompositionRule::Default],
1416        DeviceInfoRef {
1417            device_class: DeviceClass::Ethernet,
1418            topological_path: "/dev/sys/platform/pt/PCI0/bus/00:14.0/00:14.0/xhci/usb/004/004/ifc-000/ax88179/ethernet",
1419            mac: &fidl_fuchsia_net_ext::MacAddress { octets: [0x1, 0x1, 0x1, 0x1, 0x1, 0x2] },
1420        },
1421        "ethx2";
1422        "default_usb_pci"
1423    )]
1424    #[test_case(
1425        vec![NameCompositionRule::Default],
1426        DeviceInfoRef {
1427            device_class: DeviceClass::Ethernet,
1428            topological_path: "/dev/sys/platform/05:00:18/usb-phy-composite/aml_usb_phy/dwc2/dwc2_phy/dwc2/usb-peripheral/function-000/usb-cdc-netdev/network-device",
1429            mac: &fidl_fuchsia_net_ext::MacAddress { octets: [0x1, 0x1, 0x1, 0x1, 0x1, 0x3] },
1430        },
1431        "ethx3";
1432        "default_usb_dwc"
1433    )]
1434    #[test_case(
1435        vec![NameCompositionRule::Default],
1436        DeviceInfoRef {
1437            device_class: DeviceClass::Ethernet,
1438            topological_path: "/dev/sys/platform/05:00:6/aml-sd-emmc/sdio/broadcom-wlanphy",
1439            ..default_device_info()
1440        },
1441        "eths05006";
1442        "default_sdio"
1443    )]
1444    fn test_naming_rules(
1445        composition_rules: Vec<NameCompositionRule>,
1446        info: DeviceInfoRef<'_>,
1447        expected_name: &'static str,
1448    ) {
1449        let naming_rule = NamingRule { matchers: HashSet::new(), naming_scheme: composition_rules };
1450
1451        let name = naming_rule.generate_name(&HashMap::new(), &info);
1452        assert_eq!(name.unwrap(), expected_name.to_owned());
1453    }
1454
1455    #[test]
1456    fn test_generate_name_from_naming_rule_interface_name_exists_no_reattempt() {
1457        let topo_usb = "/dev/pci-00:14.0-fidl/xhci/usb/004/004/ifc-000/ax88179/ethernet";
1458
1459        let shared_interface_name = "x".to_owned();
1460        let mut interfaces = HashMap::new();
1461        assert_matches!(
1462            interfaces.insert(
1463                InterfaceNamingIdentifier {
1464                    mac: fidl_fuchsia_net_ext::MacAddress {
1465                        octets: [0x1, 0x1, 0x1, 0x1, 0x1, 0x1]
1466                    },
1467                    topological_path: topo_usb.to_string()
1468                },
1469                shared_interface_name.clone(),
1470            ),
1471            None
1472        );
1473
1474        let naming_rule = NamingRule {
1475            matchers: HashSet::new(),
1476            naming_scheme: vec![NameCompositionRule::Static {
1477                value: shared_interface_name.clone(),
1478            }],
1479        };
1480
1481        let name = naming_rule.generate_name(&interfaces, &default_device_info()).unwrap();
1482        assert_eq!(name, shared_interface_name);
1483    }
1484
1485    // This test is different from `test_get_usb_255_with_naming_rule` as this
1486    // test increments the last byte, ensuring that the offset is reset prior
1487    // to each name being generated.
1488    #[test]
1489    fn test_generate_name_from_naming_rule_many_unique_macs() {
1490        let topo_usb = "/dev/pci-00:14.0-fidl/xhci/usb/004/004/ifc-000/ax88179/ethernet";
1491
1492        let naming_rule = NamingRule {
1493            matchers: HashSet::new(),
1494            naming_scheme: vec![NameCompositionRule::Dynamic {
1495                rule: DynamicNameCompositionRule::NormalizedMac,
1496            }],
1497        };
1498
1499        // test cases for 256 usb interfaces
1500        let mut interfaces = HashMap::new();
1501
1502        for n in 0u8..255u8 {
1503            let octets = [0x01, 0x01, 0x01, 0x01, 0x01, n];
1504            let interface_naming_id =
1505                generate_identifier(&fidl_fuchsia_net_ext::MacAddress { octets }, topo_usb);
1506            let info = DeviceInfoRef {
1507                device_class: DeviceClass::Ethernet,
1508                mac: &fidl_fuchsia_net_ext::MacAddress { octets },
1509                topological_path: topo_usb,
1510            };
1511
1512            let name =
1513                naming_rule.generate_name(&interfaces, &info).expect("failed to generate the name");
1514            assert_eq!(name, format!("{n:x}"));
1515
1516            assert_matches!(interfaces.insert(interface_naming_id, name.clone()), None);
1517        }
1518    }
1519
1520    #[test_case(true, "x"; "matches_first_rule")]
1521    #[test_case(false, "ethx1"; "fallback_default")]
1522    fn test_generate_name_from_naming_rules(match_first_rule: bool, expected_name: &'static str) {
1523        // Use an Ethernet device that is determined to have a USB bus type
1524        // from the topological path.
1525        let info = DeviceInfoRef {
1526            device_class: DeviceClass::Ethernet,
1527            mac: &fidl_fuchsia_net_ext::MacAddress { octets: [0x1, 0x1, 0x1, 0x1, 0x1, 0x1] },
1528            topological_path: "/dev/sys/platform/pt/PCI0/bus/00:14.0/00:14.0/xhci/usb/004/004/ifc-000/ax88179/ethernet",
1529        };
1530        let name = generate_name_from_naming_rules(
1531            &[
1532                NamingRule {
1533                    matchers: HashSet::from([MatchingRule::Any(match_first_rule)]),
1534                    naming_scheme: vec![NameCompositionRule::Static { value: String::from("x") }],
1535                },
1536                // Include an arbitrary rule that matches no interface
1537                // to ensure that it has no impact on the test.
1538                NamingRule {
1539                    matchers: HashSet::from([MatchingRule::Any(false)]),
1540                    naming_scheme: vec![NameCompositionRule::Static { value: String::from("y") }],
1541                },
1542            ],
1543            &HashMap::new(),
1544            &info,
1545        )
1546        .unwrap();
1547        assert_eq!(name, expected_name.to_owned());
1548    }
1549
1550    #[test_case(true, ProvisioningType::Delegated; "matches_first_rule")]
1551    #[test_case(false, ProvisioningType::Local; "fallback_default")]
1552    fn test_find_provisioning_action_from_provisioning_rules(
1553        match_first_rule: bool,
1554        expected: ProvisioningType,
1555    ) {
1556        let provisioning_action = find_provisioning_action_from_provisioning_rules(
1557            &[ProvisioningRule {
1558                matchers: HashSet::from([ProvisioningMatchingRule::Common(MatchingRule::Any(
1559                    match_first_rule,
1560                ))]),
1561                action: ProvisioningAction {
1562                    provisioning: ProvisioningType::Delegated,
1563                    ..Default::default()
1564                },
1565            }],
1566            &DeviceInfoRef {
1567                device_class: DeviceClass::WlanClient,
1568                mac: &fidl_fuchsia_net_ext::MacAddress { octets: [0x1, 0x1, 0x1, 0x1, 0x1, 0x1] },
1569                topological_path: "",
1570            },
1571            "wlans5009",
1572        );
1573        assert_eq!(provisioning_action.provisioning, expected);
1574    }
1575}