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