blob: d0dc24432b6c5fa1b11229c1844074c38250ccb3 [file] [log] [blame] [edit]
// Copyright 2024 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//! This file contains an implementation for storing network information in a file system.
//!
//! Each file within `/sys/fs/fuchsia_network_monitor_fs` represents a network and its properties.
use crate::NetworkManager;
use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
use starnix_core::task::{CurrentTask, Kernel};
use starnix_core::vfs::fs_args::parse;
use starnix_core::vfs::pseudo::simple_file::{BytesFile, BytesFileOps};
use starnix_core::vfs::{
CacheMode, FileOps, FileSystem, FileSystemHandle, FileSystemOps, FileSystemOptions, FsNode,
FsNodeHandle, FsNodeInfo, FsNodeOps, FsStr, MemoryDirectoryFile,
};
use starnix_logging::{log_error, log_warn};
use starnix_sync::{FileOpsCore, LockEqualOrBefore, Locked, Unlocked};
use starnix_types::vfs::default_statfs;
use starnix_uapi::auth::FsCred;
use starnix_uapi::device_type::DeviceType;
use starnix_uapi::errors::Errno;
use starnix_uapi::file_mode::FileMode;
use starnix_uapi::open_flags::OpenFlags;
use starnix_uapi::{errno, error, statfs};
use std::borrow::Cow;
use std::net::{Ipv4Addr, Ipv6Addr};
use std::sync::Arc;
use fidl_fuchsia_net as fnet;
use fidl_fuchsia_net_policy_socketproxy::{self as fnp_socketproxy, ProtoProperties};
const DEFAULT_NETWORK_FILE_NAME: &str = "default";
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
pub(crate) struct NetworkMessage {
pub(crate) netid: u32,
pub(crate) mark: u32,
pub(crate) handle: u64,
#[serde(with = "addr_list")]
pub(crate) dnsv4: Vec<Ipv4Addr>,
#[serde(with = "addr_list")]
pub(crate) dnsv6: Vec<Ipv6Addr>,
#[serde(flatten)]
pub(crate) versioned_properties: VersionedProperties,
}
#[derive(Clone, Copy, Debug, Deserialize_repr, PartialEq, Serialize_repr)]
#[repr(u8)]
pub(crate) enum TransportType {
Cellular = 0,
Wifi = 1,
Bluetooth = 2,
Ethernet = 3,
Vpn = 4,
WifiAware = 5,
Lowpan = 6,
}
#[derive(Clone, Copy, Debug, Deserialize_repr, PartialEq, Serialize_repr)]
#[repr(u8)]
pub(crate) enum NetworkCapability {
Mms = 0,
Supl = 1,
Dun = 2,
Fota = 3,
Ims = 4,
Cbs = 5,
WifiP2p = 6,
Ia = 7,
Rcs = 8,
Xcap = 9,
Eims = 10,
NotMetered = 11,
Internet = 12,
NotRestricted = 13,
Trusted = 14,
NotVpn = 15,
Validated = 16,
CaptivePortal = 17,
NotRoaming = 18,
Foreground = 19,
NotCongested = 20,
NotSuspended = 21,
OemPaid = 22,
Mcs = 23,
PartialConnectivity = 24,
TemporarilyNotMetered = 25,
OemPrivate = 26,
VehicleInternal = 27,
NotVcnManaged = 28,
Enterprise = 29,
Vsim = 30,
Bip = 31,
HeadUnit = 32,
Mmtel = 33,
PrioritizeLatency = 34,
PrioritizeBandwidth = 35,
LocalNetwork = 36,
NotBandwidthConstrained = 37,
PrioritizeUnifiedCommunications = 38,
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
#[serde(tag = "version")]
pub(crate) enum VersionedProperties {
#[default]
V1,
V2 {
#[serde(with = "transport_list")]
transports: Vec<TransportType>,
#[serde(with = "capability_list")]
capabilities: Vec<NetworkCapability>,
name: String,
// Whether or not there is a v4/v6 address assigned for
// the underlying link properties.
addrv4: bool,
addrv6: bool,
// Whether or not there is a v4/v6 default route for
// the underlying link properties.
defaultv4: bool,
defaultv6: bool,
},
}
pub struct FuchsiaNetworkMonitorFs;
impl FuchsiaNetworkMonitorFs {
pub fn new_fs<L>(
locked: &mut Locked<L>,
kernel: &Kernel,
options: FileSystemOptions,
) -> Result<FileSystemHandle, Errno>
where
L: LockEqualOrBefore<FileOpsCore>,
{
let fs = FileSystem::new(
locked,
kernel,
CacheMode::Permanent,
FuchsiaNetworkMonitorFs,
options,
)?;
let root_ino = fs.allocate_ino();
fs.create_root(root_ino, NetworkDirectoryNode::new());
Ok(fs)
}
}
const FUCHSIA_NETWORK_MONITOR_FS_NAME: &[u8; 26] = b"fuchsia_network_monitor_fs";
const FUCHSIA_NETWORK_MONITOR_FS_MAGIC: u32 = u32::from_be_bytes(*b"nmfs");
impl FileSystemOps for FuchsiaNetworkMonitorFs {
fn statfs(
&self,
_locked: &mut Locked<FileOpsCore>,
_fs: &FileSystem,
_current_task: &CurrentTask,
) -> Result<statfs, Errno> {
Ok(default_statfs(FUCHSIA_NETWORK_MONITOR_FS_MAGIC))
}
fn name(&self) -> &'static FsStr {
FUCHSIA_NETWORK_MONITOR_FS_NAME.into()
}
}
pub fn fuchsia_network_monitor_fs(
locked: &mut Locked<Unlocked>,
current_task: &CurrentTask,
options: FileSystemOptions,
) -> Result<FileSystemHandle, Errno> {
struct FuchsiaNetworkMonitorFsHandle(FileSystemHandle);
let kernel = current_task.kernel();
Ok(kernel
.expando
.get_or_try_init(|| {
FuchsiaNetworkMonitorFs::new_fs(locked, kernel, options)
.map(|fs| FuchsiaNetworkMonitorFsHandle(fs))
})?
.0
.clone())
}
// Get the NetworkManager from the Kernel's Expando.
//
// Returns the NetworkManager when available, or EPERM when the NetworkManager
// is absent.
fn try_acquire_network_manager(current_task: &CurrentTask) -> Result<Arc<NetworkManager>, Errno> {
let kernel = current_task.kernel();
kernel.expando.peek::<NetworkManager>().ok_or_else(|| errno!(EPERM))
}
pub struct NetworkDirectoryNode;
impl NetworkDirectoryNode {
pub fn new() -> Self {
Self
}
}
impl FsNodeOps for NetworkDirectoryNode {
fn create_file_ops(
&self,
_locked: &mut Locked<FileOpsCore>,
_node: &FsNode,
_current_task: &CurrentTask,
_flags: OpenFlags,
) -> Result<Box<dyn FileOps>, Errno> {
Ok(Box::new(MemoryDirectoryFile::new()))
}
fn mkdir(
&self,
_locked: &mut Locked<FileOpsCore>,
_node: &FsNode,
_current_task: &CurrentTask,
_name: &FsStr,
_mode: FileMode,
_owner: FsCred,
) -> Result<FsNodeHandle, Errno> {
error!(EPERM)
}
fn mknod(
&self,
_locked: &mut Locked<FileOpsCore>,
node: &FsNode,
current_task: &CurrentTask,
name: &FsStr,
mode: FileMode,
_dev: DeviceType,
_owner: FsCred,
) -> Result<FsNodeHandle, Errno> {
if !mode.is_reg() {
return error!(EACCES);
}
let ops: Box<dyn FsNodeOps> = if name == DEFAULT_NETWORK_FILE_NAME {
// The node with DEFAULT_NETWORK_FILE_NAME is special and can
// only be written to with network ids.
Box::new(DefaultNetworkIdFile::new_node())
} else {
let id: u32 = parse(name).map_err(|_| errno!(EINVAL))?;
// Insert a new network entry, but don't populate any fields.
let network_manager = try_acquire_network_manager(current_task)?;
// This call should only occur on the first node with this name,
// so this call isn't expected to fail.
network_manager.add_empty_network(id)?;
Box::new(NetworkFile::new_node(id))
};
let child = node.fs().create_node_and_allocate_node_id(
ops,
FsNodeInfo::new(mode, current_task.current_fscred()),
);
Ok(child)
}
fn unlink(
&self,
_locked: &mut Locked<FileOpsCore>,
_node: &FsNode,
current_task: &CurrentTask,
name: &FsStr,
_child: &FsNodeHandle,
) -> Result<(), Errno> {
let network_manager = try_acquire_network_manager(current_task)?;
// Note: direct equality comparisons are easier using FsStr
// than using a match block.
if name == DEFAULT_NETWORK_FILE_NAME {
// Reset the default network when the associated
// network file with the same id is unlinked.
network_manager.set_default_network_id(None);
} else {
let id: u32 = parse(name)?;
network_manager.remove_network(id)?;
}
Ok(())
}
fn create_symlink(
&self,
_locked: &mut Locked<FileOpsCore>,
_node: &FsNode,
_current_task: &CurrentTask,
_name: &FsStr,
_target: &FsStr,
_owner: FsCred,
) -> Result<FsNodeHandle, Errno> {
error!(EPERM)
}
}
pub struct NetworkFile {
network_id: u32,
}
impl NetworkFile {
pub fn new_node(network_id: u32) -> impl FsNodeOps {
BytesFile::new_node(Self { network_id })
}
}
impl BytesFileOps for NetworkFile {
fn write(&self, current_task: &CurrentTask, data: Vec<u8>) -> Result<(), Errno> {
let network: NetworkMessage = serde_json::from_slice(&data).map_err(|e| {
log_error!("failed to deserialize network message: {}", e);
errno!(EINVAL)
})?;
let new_netid = network.netid;
// The network id must be the same as the id listed in the JSON.
if new_netid != self.network_id {
return error!(EINVAL);
}
let network_manager = try_acquire_network_manager(current_task)?;
match network_manager.get_network(&new_netid) {
None | Some(None) => {
network_manager.add_network(network)?;
}
Some(Some(_old_network)) => {
network_manager.update_network(network)?;
}
}
Ok(())
}
fn read(&self, current_task: &CurrentTask) -> Result<Cow<'_, [u8]>, Errno> {
let network_manager = try_acquire_network_manager(current_task)?;
// Verify whether the network exists before reading.
if let None = network_manager.get_network(&self.network_id) {
return error!(ENOENT);
}
Ok(network_manager.get_network_by_id_as_bytes(self.network_id).to_vec().into())
}
}
pub struct DefaultNetworkIdFile {}
impl DefaultNetworkIdFile {
pub fn new_node() -> impl FsNodeOps {
BytesFile::new_node(Self {})
}
}
impl BytesFileOps for DefaultNetworkIdFile {
fn write(&self, current_task: &CurrentTask, data: Vec<u8>) -> Result<(), Errno> {
let id_string = std::str::from_utf8(&data).map_err(|_| errno!(EINVAL))?;
let id: u32 = id_string.parse().map_err(|_| errno!(EINVAL))?;
{
let network_manager = try_acquire_network_manager(current_task)?;
match network_manager.get_network(&id) {
// A network with the provided id must already
// exist to become the default network.
Some(Some(_)) => {
network_manager.set_default_network_id(Some(id));
}
// The network properties must be provided for
// a network before it can become the default.
Some(None) | None => return error!(ENOENT),
};
}
Ok(())
}
fn read(&self, current_task: &CurrentTask) -> Result<Cow<'_, [u8]>, Errno> {
let network_manager = try_acquire_network_manager(current_task)?;
if let None = network_manager.get_default_network_id() {
return error!(ENOENT);
}
Ok(network_manager.get_default_id_as_bytes().to_vec().into())
}
}
impl From<&NetworkMessage> for fnp_socketproxy::Network {
fn from(message: &NetworkMessage) -> Self {
let (transports, capabilities, name, addrv4, addrv6, defaultv4, defaultv6) =
match &message.versioned_properties {
VersionedProperties::V1 => (None, None, None, None, None, None, None),
VersionedProperties::V2 {
transports,
capabilities,
name,
addrv4,
addrv6,
defaultv4,
defaultv6,
} => (
Some(transports),
Some(capabilities),
Some(name),
Some(addrv4),
Some(addrv6),
Some(defaultv4),
Some(defaultv6),
),
};
Self {
network_id: Some(message.netid),
info: Some(fnp_socketproxy::NetworkInfo::Starnix(
fnp_socketproxy::StarnixNetworkInfo {
mark: Some(message.mark),
handle: Some(message.handle),
..Default::default()
},
)),
dns_servers: Some(fnp_socketproxy::NetworkDnsServers {
v4: Some(
message
.dnsv4
.clone()
.into_iter()
.map(|a| fnet::Ipv4Address { addr: a.octets() })
.collect::<Vec<_>>(),
),
v6: Some(
message
.dnsv6
.clone()
.into_iter()
.map(|a| fnet::Ipv6Address { addr: a.octets() })
.collect::<Vec<_>>(),
),
..Default::default()
}),
network_type: transports
.map(|transport_list| consolidate_transport_types_to_network_type(transport_list)),
capabilities: capabilities.map(|capability_list| convert_capabilities(capability_list)),
connectivity: capabilities
.map(|capability_list| convert_connectivity_state(capability_list)),
name: name.cloned(),
has_address: Some(proto_properties_from_v4_v6(addrv4.copied(), addrv6.copied())),
has_default_route: Some(proto_properties_from_v4_v6(
defaultv4.copied(),
defaultv6.copied(),
)),
..Default::default()
}
}
}
fn proto_properties_from_v4_v6(v4: Option<bool>, v6: Option<bool>) -> ProtoProperties {
let mut properties = ProtoProperties::empty();
if Some(true) == v4 {
properties |= ProtoProperties::V4;
}
if Some(true) == v6 {
properties |= ProtoProperties::V6;
}
properties
}
// Given a list of `TransportType`, convert it to a single `NetworkType`. Only a single
// transport is expected, so take the first one if multiple are present. If none are
// present, Unknown is used as a fallback.
fn consolidate_transport_types_to_network_type(
value: &Vec<TransportType>,
) -> fnp_socketproxy::NetworkType {
value
.iter()
.filter_map(|tt| maybe_network_type_from_transport_type(*tt))
.next()
.unwrap_or(fnp_socketproxy::NetworkType::Unknown)
}
fn maybe_network_type_from_transport_type(
value: TransportType,
) -> Option<fnp_socketproxy::NetworkType> {
match value {
TransportType::Ethernet => Some(fnp_socketproxy::NetworkType::Ethernet),
TransportType::Wifi => Some(fnp_socketproxy::NetworkType::Wifi),
TransportType::Bluetooth => Some(fnp_socketproxy::NetworkType::Bluetooth),
TransportType::Cellular => Some(fnp_socketproxy::NetworkType::Cellular),
TransportType::Vpn | TransportType::WifiAware | TransportType::Lowpan => {
log_warn!("Known NetworkType {value:?} is not currently supported");
None
}
}
}
// Currently, we only check for the following capabilities: NotMetered, NotCongested,
// NotBandwidthConstrained. If we wish to have more knowledge of the offered capabilities,
// we can consider adding fields to the fnp_socketproxy::NetworkRegistry FIDL API.
fn convert_capabilities(value: &Vec<NetworkCapability>) -> Vec<fnp_socketproxy::NetworkCapability> {
value
.iter()
.filter_map(|c| match c {
NetworkCapability::NotMetered => Some(fnp_socketproxy::NetworkCapability::UNMETERED),
NetworkCapability::NotCongested => {
Some(fnp_socketproxy::NetworkCapability::UNCONGESTED)
}
NetworkCapability::NotBandwidthConstrained => {
Some(fnp_socketproxy::NetworkCapability::NOT_BANDWIDTH_CONSTRAINED)
}
_ => None,
})
.collect::<Vec<_>>()
}
fn convert_connectivity_state(
value: &Vec<NetworkCapability>,
) -> fnp_socketproxy::ConnectivityState {
// Validated and Internet capabilities indicate that the network has been
// validated to have internet connectivity (HTTP and DNS).
if value.contains(&NetworkCapability::Validated) && value.contains(&NetworkCapability::Internet)
{
return fnp_socketproxy::ConnectivityState::FullConnectivity;
}
if value.contains(&NetworkCapability::PartialConnectivity) {
return fnp_socketproxy::ConnectivityState::PartialConnectivity;
}
if value.contains(&NetworkCapability::LocalNetwork) {
return fnp_socketproxy::ConnectivityState::LocalConnectivity;
}
return fnp_socketproxy::ConnectivityState::NoConnectivity;
}
mod addr_list {
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
pub fn serialize<S, Addr>(addr: &Vec<Addr>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
Addr: std::fmt::Display,
{
let strings = addr.iter().map(|x| x.to_string()).collect::<Vec<_>>();
strings.serialize(serializer)
}
pub fn deserialize<'de, D, Addr>(deserializer: D) -> Result<Vec<Addr>, D::Error>
where
D: Deserializer<'de>,
Addr: std::str::FromStr,
Addr::Err: std::fmt::Display,
{
let s = Vec::<String>::deserialize(deserializer)?;
s.into_iter().map(|s| s.parse().map_err(de::Error::custom)).collect()
}
}
macro_rules! tolerant_repr_serde_impl {
($module_name:ident, $NewType:path) => {
mod $module_name {
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use starnix_logging::log_error;
pub fn serialize<S>(items: &Vec<$NewType>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let nums = items.iter().map(|item| *item as u8).collect::<Vec<_>>();
nums.serialize(serializer)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<$NewType>, D::Error>
where
D: Deserializer<'de>,
{
let vals = Vec::<serde_json::Value>::deserialize(deserializer)?;
Ok(vals
.into_iter()
.filter_map(|val| match serde_json::from_value::<$NewType>(val.clone()) {
Ok(s) => Some(s),
Err(_) => {
log_error!("unknown value: {}", val);
None
}
})
.collect())
}
}
};
}
tolerant_repr_serde_impl!(transport_list, crate::TransportType);
tolerant_repr_serde_impl!(capability_list, crate::NetworkCapability);
#[cfg(test)]
mod tests {
use super::*;
use net_declare::{std_ip_v4, std_ip_v6};
use test_case::test_case;
#[::fuchsia::test]
fn network_message_serde() {
let network = NetworkMessage {
netid: 123,
mark: 456,
handle: 789,
dnsv4: vec![std_ip_v4!("192.168.0.1")],
dnsv6: vec![std_ip_v6!("2001:db8::1")],
versioned_properties: VersionedProperties::V1,
};
serde_helper(network);
}
#[::fuchsia::test]
fn network_message_serde_version2() {
let network = NetworkMessage {
netid: 123,
mark: 456,
handle: 789,
dnsv4: vec![std_ip_v4!("192.168.0.1")],
dnsv6: vec![std_ip_v6!("2001:db8::1")],
versioned_properties: VersionedProperties::V2 {
transports: vec![
TransportType::Cellular,
TransportType::Wifi,
TransportType::Bluetooth,
TransportType::Ethernet,
TransportType::Vpn,
TransportType::WifiAware,
TransportType::Lowpan,
],
capabilities: vec![
NetworkCapability::Trusted,
NetworkCapability::Internet,
NetworkCapability::Validated,
],
name: "test01".to_string(),
addrv4: false,
addrv6: true,
defaultv4: false,
defaultv6: true,
},
};
serde_helper(network);
}
#[::fuchsia::test]
fn network_message_serde_unknown_transports_and_capabilities() {
let json = r#"{
"netid": 123,
"mark": 456,
"handle": 789,
"dnsv4": ["192.168.0.1"],
"dnsv6": ["2001:db8::1"],
"version": "V2",
"transports": [1, 99, 3],
"capabilities": [11, 99],
"name": "test01",
"addrv4": false,
"addrv6": false,
"defaultv4": false,
"defaultv6": false
}"#;
let expected = NetworkMessage {
netid: 123,
mark: 456,
handle: 789,
dnsv4: vec![std_ip_v4!("192.168.0.1")],
dnsv6: vec![std_ip_v6!("2001:db8::1")],
versioned_properties: VersionedProperties::V2 {
transports: vec![TransportType::Wifi, TransportType::Ethernet],
capabilities: vec![NetworkCapability::NotMetered],
name: "test01".to_string(),
addrv4: false,
addrv6: false,
defaultv4: false,
defaultv6: false,
},
};
let deserialized: NetworkMessage = serde_json::from_str(json).unwrap();
assert_eq!(expected, deserialized);
}
// Ensure that a provided NetworkMessage can be serialized and
// deserialized into the original form.
fn serde_helper(network: NetworkMessage) {
let serialized = serde_json::to_string(&network).unwrap_or_else(|_| "{}".to_string());
let deserialized = serde_json::from_str(&serialized).unwrap();
assert_eq!(network, deserialized);
}
#[test_case(vec![TransportType::Cellular], fnp_socketproxy::NetworkType::Cellular;
"cellular")]
#[test_case(vec![TransportType::Wifi], fnp_socketproxy::NetworkType::Wifi;
"wifi")]
#[test_case(vec![TransportType::Bluetooth], fnp_socketproxy::NetworkType::Bluetooth;
"bluetooth")]
#[test_case(vec![TransportType::Ethernet], fnp_socketproxy::NetworkType::Ethernet;
"ethernet")]
#[test_case(
vec![
TransportType::Wifi,
TransportType::Cellular
],
fnp_socketproxy::NetworkType::Wifi; "first_transport_prioritized")]
#[test_case(vec![], fnp_socketproxy::NetworkType::Unknown; "empty_fallback")]
#[test_case(
vec![
TransportType::Lowpan,
TransportType::WifiAware,
TransportType::Vpn
],
fnp_socketproxy::NetworkType::Unknown; "none_applicable")]
#[::fuchsia::test]
fn test_consolidate_transport_types_to_network_type(
transports: Vec<TransportType>,
expected: fnp_socketproxy::NetworkType,
) {
assert_eq!(consolidate_transport_types_to_network_type(&transports), expected);
}
#[test_case(
vec![
NetworkCapability::NotMetered,
NetworkCapability::NotCongested,
NetworkCapability::NotBandwidthConstrained,
],
vec![
fnp_socketproxy::NetworkCapability::UNMETERED,
fnp_socketproxy::NetworkCapability::UNCONGESTED,
fnp_socketproxy::NetworkCapability::NOT_BANDWIDTH_CONSTRAINED,
];
"all_capabilities"
)]
#[test_case(
vec![NetworkCapability::NotMetered],
vec![fnp_socketproxy::NetworkCapability::UNMETERED];
"one_capability"
)]
#[test_case(
vec![],
vec![];
"no_capabilities"
)]
#[test_case(
vec![
NetworkCapability::NotMetered,
NetworkCapability::Trusted, // This should be ignored.
],
vec![fnp_socketproxy::NetworkCapability::UNMETERED];
"ignore_unrelated"
)]
#[::fuchsia::test]
fn test_convert_capabilities(
capabilities: Vec<NetworkCapability>,
expected: Vec<fnp_socketproxy::NetworkCapability>,
) {
let mut result = convert_capabilities(&capabilities);
result.sort();
let mut expected_sorted = expected;
expected_sorted.sort();
assert_eq!(result, expected_sorted);
}
#[test_case(
vec![
NetworkCapability::Validated,
NetworkCapability::Internet,
],
fnp_socketproxy::ConnectivityState::FullConnectivity;
"full_connectivity"
)]
#[test_case(
vec![
NetworkCapability::Validated,
NetworkCapability::Internet,
NetworkCapability::LocalNetwork,
],
fnp_socketproxy::ConnectivityState::FullConnectivity;
"full_connectivity_prioritized"
)]
#[test_case(
vec![NetworkCapability::PartialConnectivity],
fnp_socketproxy::ConnectivityState::PartialConnectivity;
"partial_connectivity"
)]
#[test_case(
vec![NetworkCapability::LocalNetwork],
fnp_socketproxy::ConnectivityState::LocalConnectivity;
"local_connectivity"
)]
#[test_case(
vec![],
fnp_socketproxy::ConnectivityState::NoConnectivity;
"no_connectivity"
)]
#[test_case(
vec![NetworkCapability::Trusted], // This should be ignored.
fnp_socketproxy::ConnectivityState::NoConnectivity;
"ignore_unrelated"
)]
#[::fuchsia::test]
fn test_convert_connectivity_state(
capabilities: Vec<NetworkCapability>,
expected: fnp_socketproxy::ConnectivityState,
) {
assert_eq!(convert_connectivity_state(&capabilities), expected);
}
}