blob: 6360ecdb85ac8da0186a47117e1f9bd5cab40bdf [file] [log] [blame]
// Copyright 2018 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.
extern crate network_manager_core_interface as interface;
use std::collections::HashMap;
use std::collections::HashSet;
use std::fs;
use std::io;
use std::os::unix::io::AsRawFd;
use std::path;
use std::sync::Arc;
use anyhow::Context as _;
use fuchsia_async::DurationExt;
use fuchsia_component::client::connect_to_service;
use fuchsia_syslog::fx_log_info;
use fuchsia_zircon::DurationNum;
use futures::lock::Mutex;
use futures::{future::try_join, TryFutureExt, TryStreamExt};
use io_util::{open_directory_in_namespace, OPEN_RIGHT_READABLE};
use serde::Deserialize;
mod matchers;
#[derive(Debug, Deserialize)]
pub struct DnsConfig {
pub servers: Vec<std::net::IpAddr>,
}
#[derive(Debug, Deserialize)]
pub struct FilterConfig {
pub rules: Vec<String>,
pub nat_rules: Vec<String>,
pub rdr_rules: Vec<String>,
}
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
enum InterfaceType {
UNKNOWN(String),
ETHERNET,
WLAN,
}
#[derive(Debug, Deserialize)]
struct Config {
pub dns_config: DnsConfig,
#[serde(deserialize_with = "matchers::InterfaceSpec::parse_as_tuples")]
pub rules: Vec<matchers::InterfaceSpec>,
pub filter_config: FilterConfig,
pub filter_enabled_interface_types: Vec<String>,
}
impl Config {
pub fn load<P: AsRef<path::Path>>(path: P) -> Result<Self, anyhow::Error> {
let path = path.as_ref();
let file = fs::File::open(path)
.with_context(|| format!("could not open the config file {}", path.display()))?;
let config = serde_json::from_reader(io::BufReader::new(file))
.with_context(|| format!("could not deserialize the config file {}", path.display()))?;
Ok(config)
}
}
// We use Compare-And-Swap (CAS) protocol to update filter rules. $get_rules returns the current
// generation number. $update_rules will send it with new rules to make sure we are updating the
// intended generation. If the generation number doesn't match, $update_rules will return a
// GenerationMismatch error, then we have to restart from $get_rules.
const FILTER_CAS_RETRY_MAX: i32 = 3;
const FILTER_CAS_RETRY_INTERVAL_MILLIS: i64 = 500;
macro_rules! cas_filter_rules {
($filter:ident, $get_rules:ident, $update_rules:ident, $rules:expr) => {
for retry in 0..FILTER_CAS_RETRY_MAX {
let (_, generation, status) = $filter.$get_rules().await?;
if status != fidl_fuchsia_net_filter::Status::Ok {
let () =
Err(anyhow::format_err!("{} failed: {:?}", stringify!($get_rules), status))?;
}
let status = $filter
.$update_rules(&mut $rules.iter_mut(), generation)
.await
.context("error getting response")?;
match status {
fidl_fuchsia_net_filter::Status::Ok => {
break;
}
fidl_fuchsia_net_filter::Status::ErrGenerationMismatch
if retry < FILTER_CAS_RETRY_MAX - 1 =>
{
fuchsia_async::Timer::new(
FILTER_CAS_RETRY_INTERVAL_MILLIS.millis().after_now(),
)
.await;
}
_ => {
let () = Err(anyhow::format_err!(
"{} failed: {:?}",
stringify!($update_rules),
status
))?;
}
}
}
};
}
impl From<String> for InterfaceType {
fn from(s: String) -> InterfaceType {
match s.as_ref() {
"ethernet" => InterfaceType::ETHERNET,
"wlan" => InterfaceType::WLAN,
_ => InterfaceType::UNKNOWN(s),
}
}
}
fn should_enable_filter(
filter_enabled_interface_types: &HashSet<InterfaceType>,
features: &fidl_fuchsia_hardware_ethernet_ext::EthernetFeatures,
) -> bool {
if features.contains(fidl_fuchsia_hardware_ethernet_ext::EthernetFeatures::LOOPBACK) {
false
} else if features.contains(fidl_fuchsia_hardware_ethernet_ext::EthernetFeatures::WLAN) {
filter_enabled_interface_types.contains(&InterfaceType::WLAN)
} else {
filter_enabled_interface_types.contains(&InterfaceType::ETHERNET)
}
}
#[fuchsia_async::run_singlethreaded]
async fn main() -> Result<(), anyhow::Error> {
fuchsia_syslog::init_with_tags(&["netcfg"])?;
fx_log_info!("Started");
let Config {
dns_config: DnsConfig { servers },
rules: default_config_rules,
filter_config,
filter_enabled_interface_types,
} = Config::load("/config/data/default.json")?;
let parse_result: HashSet<InterfaceType> =
filter_enabled_interface_types.into_iter().map(Into::into).collect();
if parse_result.iter().any(|interface_type| match interface_type {
&InterfaceType::UNKNOWN(_) => true,
_ => false,
}) {
return Err(anyhow::format_err!(
"failed to parse filter_enabled_interface_types: {:?}",
parse_result
));
};
let filter_enabled_interface_types = parse_result;
let mut persisted_interface_config =
interface::FileBackedConfig::load(&"/data/net_interfaces.cfg.json")?;
let stack = connect_to_service::<fidl_fuchsia_net_stack::StackMarker>()
.context("could not connect to stack")?;
let netstack = connect_to_service::<fidl_fuchsia_netstack::NetstackMarker>()
.context("could not connect to netstack")?;
let lookup_admin = connect_to_service::<fidl_fuchsia_net_name::LookupAdminMarker>()
.context("could not connect to lookup admin")?;
let filter = connect_to_service::<fidl_fuchsia_net_filter::FilterMarker>()
.context("could not connect to filter")?;
let filter_setup = async {
if !filter_config.rules.is_empty() {
let mut rules = netfilter::parser::parse_str_to_rules(&filter_config.rules.join(""))
.map_err(|e| anyhow::format_err!("could not parse filter rules: {:?}", e))?;
cas_filter_rules!(filter, get_rules, update_rules, rules);
}
if !filter_config.nat_rules.is_empty() {
let mut nat_rules =
netfilter::parser::parse_str_to_nat_rules(&filter_config.nat_rules.join(""))
.map_err(|e| anyhow::format_err!("could not parse nat rules: {:?}", e))?;
cas_filter_rules!(filter, get_nat_rules, update_nat_rules, nat_rules);
}
if !filter_config.rdr_rules.is_empty() {
let mut rdr_rules =
netfilter::parser::parse_str_to_rdr_rules(&filter_config.rdr_rules.join(""))
.map_err(|e| anyhow::format_err!("could not parse nat rules: {:?}", e))?;
cas_filter_rules!(filter, get_rdr_rules, update_rdr_rules, rdr_rules);
}
Ok::<(), anyhow::Error>(())
};
let mut servers = servers
.into_iter()
.map(fidl_fuchsia_net_ext::IpAddress)
.map(Into::into)
.collect::<Vec<fidl_fuchsia_net::IpAddress>>();
let () = lookup_admin
.set_default_dns_servers(&mut servers.iter_mut())
.await
.map_err(anyhow::Error::new)
.and_then(|r| {
r.map_err(|status| {
anyhow::anyhow!(
"Failed to set DNS servers: {:?}",
fuchsia_zircon::Status::from_raw(status)
)
})
})
.context("could not set name servers")?;
// Interface metrics are used to sort the route table. An interface with a
// lower metric is favored over one with a higher metric.
// For now favor WLAN over Ethernet.
const INTF_METRIC_WLAN: u32 = 90;
const INTF_METRIC_ETH: u32 = 100;
const ETHDIR: &str = "/dev/class/ethernet";
let mut interface_ids = HashMap::new();
// TODO(chunyingw): Add the loopback interfaces through netcfg
// Remove the following hardcoded nic id 1.
interface_ids.insert("lo".to_owned(), 1);
let interface_ids = Arc::new(Mutex::new(interface_ids));
let dir_proxy = open_directory_in_namespace(ETHDIR, OPEN_RIGHT_READABLE)?;
let ethernet_device = async {
let mut watcher = fuchsia_vfs_watcher::Watcher::new(dir_proxy)
.await
.with_context(|| format!("could not watch {}", ETHDIR))?;
while let Some(fuchsia_vfs_watcher::WatchMessage { event, filename }) =
watcher.try_next().await.with_context(|| format!("watching {}", ETHDIR))?
{
match event {
fuchsia_vfs_watcher::WatchEvent::ADD_FILE
| fuchsia_vfs_watcher::WatchEvent::EXISTING => {
let filepath = path::Path::new(ETHDIR).join(filename);
let device = fs::File::open(&filepath)
.with_context(|| format!("could not open {}", filepath.display()))?;
let topological_path =
fdio::device_get_topo_path(&device).with_context(|| {
format!("fdio::device_get_topo_path({})", filepath.display())
})?;
let device = device.as_raw_fd();
let mut client = 0;
// Safe because we're passing a valid fd.
let () = fuchsia_zircon::Status::ok(unsafe {
fdio::fdio_sys::fdio_get_service_handle(device, &mut client)
})
.with_context(|| {
format!(
"fuchsia_zircon::sys::fdio_get_service_handle({})",
filepath.display()
)
})?;
// Safe because we checked the return status above.
let client = fuchsia_zircon::Channel::from(unsafe {
fuchsia_zircon::Handle::from_raw(client)
});
let device = fidl::endpoints::ClientEnd::<
fidl_fuchsia_hardware_ethernet::DeviceMarker,
>::new(client)
.into_proxy()?;
if let Ok(device_info) = device.get_info().await {
let device_info: fidl_fuchsia_hardware_ethernet_ext::EthernetInfo =
device_info.into();
if device_info.features.is_physical() {
let client = device
.into_channel()
.map_err(
|fidl_fuchsia_hardware_ethernet::DeviceProxy { .. }| {
anyhow::format_err!(
"failed to convert device proxy into channel",
)
},
)?
.into_zx_channel();
let name = persisted_interface_config.get_stable_name(
&topological_path, /* TODO(tamird): we can probably do
* better with std::borrow::Cow. */
device_info.mac,
device_info.features.contains(
fidl_fuchsia_hardware_ethernet_ext::EthernetFeatures::WLAN,
),
)?;
// Hardcode the interface metric. Eventually this should
// be part of the config file.
let metric: u32 = match device_info.features.contains(
fidl_fuchsia_hardware_ethernet_ext::EthernetFeatures::WLAN,
) {
true => INTF_METRIC_WLAN,
false => INTF_METRIC_ETH,
};
let mut derived_interface_config = matchers::config_for_device(
&device_info,
name.to_string(),
&topological_path,
metric,
&default_config_rules,
&filepath,
);
let nic_id = netstack
.add_ethernet_device(
&topological_path,
&mut derived_interface_config,
fidl::endpoints::ClientEnd::<
fidl_fuchsia_hardware_ethernet::DeviceMarker,
>::new(client),
)
.await
.with_context(|| {
format!(
"fidl_netstack::Netstack::add_ethernet_device({})",
filepath.display()
)
})?;
if should_enable_filter(
&filter_enabled_interface_types,
&device_info.features,
) {
fx_log_info!("enable filter for nic {}", nic_id);
let result = stack
.enable_packet_filter(nic_id as u64)
.await
.context("couldn't call enable_packet_filter")?;
let () = result.map_err(|e| {
anyhow::format_err!("failed to enable packet filter: {:?}", e)
})?;
} else {
fx_log_info!("disable filter for nic {}", nic_id);
let result = stack
.disable_packet_filter(nic_id as u64)
.await
.context("couldn't call disable_packet_filter")?;
let () = result.map_err(|e| {
anyhow::format_err!("failed to disable packet filter: {:?}", e)
})?;
};
match derived_interface_config.ip_address_config {
fidl_fuchsia_netstack::IpAddressConfig::Dhcp(_) => {
let (dhcp_client, server_end) =
fidl::endpoints::create_proxy::<
fidl_fuchsia_net_dhcp::ClientMarker,
>()
.context("dhcp client: failed to create fidl endpoints")?;
netstack
.get_dhcp_client(nic_id, server_end)
.await
.context("failed to call netstack.get_dhcp_client")?
.map_err(fuchsia_zircon::Status::from_raw)
.context("failed to get dhcp client")?;
dhcp_client
.start()
.map_ok(|result| match result {
Ok(()) => fidl_fuchsia_netstack::NetErr {
status: fidl_fuchsia_netstack::Status::Ok,
message: "".to_string(),
},
Err(status) => fidl_fuchsia_netstack::NetErr {
status: fidl_fuchsia_netstack::Status::UnknownError,
message: fuchsia_zircon::Status::from_raw(status)
.to_string(),
},
})
.await
.context("failed to start dhcp client")?
}
fidl_fuchsia_netstack::IpAddressConfig::StaticIp(
fidl_fuchsia_net::Subnet { addr: mut address, prefix_len },
) => {
netstack
.set_interface_address(
nic_id as u32,
&mut address,
prefix_len,
)
.await?
}
};
let () = netstack.set_interface_status(nic_id as u32, true)?;
// TODO(chunyingw): when netcfg switches to stack.add_ethernet_interface,
// remove casting nic_id to u64.
interface_ids
.lock()
.await
.insert(derived_interface_config.name, nic_id as u64);
}
}
}
fuchsia_vfs_watcher::WatchEvent::IDLE
| fuchsia_vfs_watcher::WatchEvent::REMOVE_FILE => {}
event => {
let () = Err(anyhow::format_err!("unknown WatchEvent {:?}", event))?;
}
}
}
Ok(())
};
let ((), ()) = try_join(filter_setup, ethernet_device).await?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use matchers::{ConfigOption, InterfaceMatcher, InterfaceSpec};
impl Config {
pub fn load_str(s: &str) -> Result<Self, anyhow::Error> {
let config = serde_json::from_str(s)
.with_context(|| format!("could not deserialize the config data {}", s))?;
Ok(config)
}
}
#[test]
fn test_config() {
let config_str = r#"
{
"dns_config": {
"servers": ["8.8.8.8"]
},
"rules": [
[ ["all", "all"], ["ip_address", "dhcp"] ]
],
"filter_config": {
"rules": [],
"nat_rules": [],
"rdr_rules": []
},
"filter_enabled_interface_types": ["wlan"]
}
"#;
let Config {
dns_config: DnsConfig { servers },
rules: default_config_rules,
filter_config,
filter_enabled_interface_types,
} = Config::load_str(config_str).unwrap();
assert_eq!(vec!["8.8.8.8".parse::<std::net::IpAddr>().unwrap()], servers);
assert_eq!(
vec![InterfaceSpec {
matcher: InterfaceMatcher::All,
config: ConfigOption::IpConfig(fidl_fuchsia_netstack_ext::IpAddressConfig::Dhcp),
}],
default_config_rules
);
let FilterConfig { rules, nat_rules, rdr_rules } = filter_config;
assert_eq!(Vec::<String>::new(), rules);
assert_eq!(Vec::<String>::new(), nat_rules);
assert_eq!(Vec::<String>::new(), rdr_rules);
assert_eq!(vec!["wlan"], filter_enabled_interface_types);
}
#[test]
fn test_to_interface_type() {
assert_eq!(InterfaceType::ETHERNET, "ethernet".to_string().into());
assert_eq!(InterfaceType::WLAN, "wlan".to_string().into());
assert_eq!(InterfaceType::UNKNOWN("bluetooth".to_string()), "bluetooth".to_string().into());
assert_eq!(InterfaceType::UNKNOWN("Ethernet".to_string()), "Ethernet".to_string().into());
assert_eq!(InterfaceType::UNKNOWN("Wlan".to_string()), "Wlan".to_string().into());
}
#[test]
fn test_should_enable_filter() {
let types_empty: HashSet<InterfaceType> = [].iter().cloned().collect();
let types_ethernet: HashSet<InterfaceType> =
[InterfaceType::ETHERNET].iter().cloned().collect();
let types_wlan: HashSet<InterfaceType> = [InterfaceType::WLAN].iter().cloned().collect();
let types_ethernet_wlan: HashSet<InterfaceType> =
[InterfaceType::ETHERNET, InterfaceType::WLAN].iter().cloned().collect();
let features_wlan = fidl_fuchsia_hardware_ethernet_ext::EthernetFeatures::WLAN;
let features_loopback = fidl_fuchsia_hardware_ethernet_ext::EthernetFeatures::LOOPBACK;
let features_synthetic = fidl_fuchsia_hardware_ethernet_ext::EthernetFeatures::SYNTHETIC;
let features_empty = fidl_fuchsia_hardware_ethernet_ext::EthernetFeatures::empty();
assert_eq!(should_enable_filter(&types_empty, &features_empty), false);
assert_eq!(should_enable_filter(&types_empty, &features_wlan), false);
assert_eq!(should_enable_filter(&types_empty, &features_synthetic), false);
assert_eq!(should_enable_filter(&types_empty, &features_loopback), false);
assert_eq!(should_enable_filter(&types_ethernet, &features_empty), true);
assert_eq!(should_enable_filter(&types_ethernet, &features_wlan), false);
assert_eq!(should_enable_filter(&types_ethernet, &features_synthetic), true);
assert_eq!(should_enable_filter(&types_ethernet, &features_loopback), false);
assert_eq!(should_enable_filter(&types_wlan, &features_empty), false);
assert_eq!(should_enable_filter(&types_wlan, &features_wlan), true);
assert_eq!(should_enable_filter(&types_wlan, &features_synthetic), false);
assert_eq!(should_enable_filter(&types_wlan, &features_loopback), false);
assert_eq!(should_enable_filter(&types_ethernet_wlan, &features_empty), true);
assert_eq!(should_enable_filter(&types_ethernet_wlan, &features_wlan), true);
assert_eq!(should_enable_filter(&types_ethernet_wlan, &features_synthetic), true);
assert_eq!(should_enable_filter(&types_ethernet_wlan, &features_loopback), false);
}
}