blob: 08cb62dd7514b0ef694d3f3e90886104742e9e65 [file] [log] [blame]
// 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.
mod matchers;
use std::collections::{BTreeMap, HashMap};
use assert_matches::assert_matches;
use fidl_fuchsia_net_filter as fnet_filter;
use fidl_fuchsia_net_filter_ext as fnet_filter_ext;
use net_types::ip::{GenericOverIp, Ip, Ipv4, Ipv6};
use packet_formats::ip::IpExt;
use crate::bindings::filter::{
controller::{
InstalledIpRoutine, InstalledNatRoutine, IpRoutineType, Namespace, NatRoutineType, Routine,
Rule,
},
CommitError,
};
use matchers::{ConversionResult, IpVersionMismatchError, TryConvertToCoreState as _};
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Ord, Eq)]
struct RoutinePriority {
priority: i32,
installation_order: usize,
}
type CoreRule<I> =
netstack3_core::filter::Rule<I, fnet_filter::DeviceClass, fnet_filter_ext::RuleId>;
type CoreRoutine<I> =
netstack3_core::filter::Routine<I, fnet_filter::DeviceClass, fnet_filter_ext::RuleId>;
type CoreUninstalledRoutine<I> = netstack3_core::filter::UninstalledRoutine<
I,
fnet_filter::DeviceClass,
fnet_filter_ext::RuleId,
>;
type CoreHook<I> =
netstack3_core::filter::Hook<I, fnet_filter::DeviceClass, fnet_filter_ext::RuleId>;
type CoreRoutines<I> =
netstack3_core::filter::Routines<I, fnet_filter::DeviceClass, fnet_filter_ext::RuleId>;
#[derive(Clone, Debug, Default, GenericOverIp)]
#[generic_over_ip(I, Ip)]
struct IpRoutines<I: IpExt> {
ingress: BTreeMap<RoutinePriority, CoreRoutine<I>>,
local_ingress: BTreeMap<RoutinePriority, CoreRoutine<I>>,
forwarding: BTreeMap<RoutinePriority, CoreRoutine<I>>,
local_egress: BTreeMap<RoutinePriority, CoreRoutine<I>>,
egress: BTreeMap<RoutinePriority, CoreRoutine<I>>,
}
#[derive(Clone, Debug, Default, GenericOverIp)]
#[generic_over_ip(I, Ip)]
struct NatRoutines<I: IpExt> {
ingress: BTreeMap<RoutinePriority, CoreRoutine<I>>,
local_ingress: BTreeMap<RoutinePriority, CoreRoutine<I>>,
local_egress: BTreeMap<RoutinePriority, CoreRoutine<I>>,
egress: BTreeMap<RoutinePriority, CoreRoutine<I>>,
}
/// This is state that has almost entirely been converted to
/// [`netstack3_core::filter::Routines`], but which retains some auxiliary
/// information to make it easy to merge with other controllers' state. In
/// particular, it stores routines as BTreeMaps keyed by the routine's priority,
/// which allows routines from other controllers to be merged into a single set
/// of IP and NAT hooks.
#[derive(Clone, Debug, Default, GenericOverIp)]
#[generic_over_ip(I, Ip)]
pub(super) struct State<I: IpExt> {
ip_routines: IpRoutines<I>,
nat_routines: NatRoutines<I>,
}
// The requirement or lack thereof that a particular resource's IP version (for
// example, whether an address matcher is IPv4 or IPv6) must match the version
// of the state that it is being added to.
//
// For a namespace with a specific IP domain, this is `IpVersionMustMatchState`;
// for a namespace with an IP-agnostic domain, this is
// `IpVersionCanDifferFromState`.
#[derive(Clone, Copy)]
enum IpVersionStrictness {
IpVersionMustMatchState,
IpVersionCanDifferFromState,
}
impl<I: IpExt> State<I> {
pub fn merge(&mut self, other: &Self) {
fn merge_hook<I: IpExt>(
dst: &mut BTreeMap<RoutinePriority, CoreRoutine<I>>,
src: &BTreeMap<RoutinePriority, CoreRoutine<I>>,
) {
for (priority, routine) in src {
assert_matches!(dst.insert(priority.clone(), routine.clone()), None);
}
}
let State { ip_routines, nat_routines } = self;
merge_hook(&mut ip_routines.ingress, &other.ip_routines.ingress);
merge_hook(&mut ip_routines.local_ingress, &other.ip_routines.local_ingress);
merge_hook(&mut ip_routines.forwarding, &other.ip_routines.forwarding);
merge_hook(&mut ip_routines.local_egress, &other.ip_routines.local_egress);
merge_hook(&mut ip_routines.egress, &other.ip_routines.egress);
merge_hook(&mut nat_routines.ingress, &other.nat_routines.ingress);
merge_hook(&mut nat_routines.local_ingress, &other.nat_routines.local_ingress);
merge_hook(&mut nat_routines.local_egress, &other.nat_routines.local_egress);
merge_hook(&mut nat_routines.egress, &other.nat_routines.egress);
}
fn add_namespace(
&mut self,
namespace: &fnet_filter_ext::NamespaceId,
installed_routines: Vec<InstalledRoutine>,
uninstalled_routines: HashMap<String, UninstalledRoutine>,
ip_version_strictness: IpVersionStrictness,
) -> Result<(), CommitError> {
// We recursively convert all uninstalled routines to their `netstack3_core`
// equivalents up front, so that later, when converting *installed* routines, we
// can expect all jump targets (which are always uninstalled routines) to have
// already been "resolved", or fully converted to their `netstack3_core`
// representation.
let uninstalled_routines = CoreUninstalledRoutines::convert_routines(
uninstalled_routines,
namespace,
ip_version_strictness,
)?;
for InstalledRoutine { name, routine_type, rules } in installed_routines {
let (hook, routine_priority, routine_type) = match routine_type {
InstalledRoutineType::Ip(InstalledIpRoutine {
hook,
priority,
installation_order,
}) => {
let hook = match hook {
fnet_filter_ext::IpHook::Ingress => &mut self.ip_routines.ingress,
fnet_filter_ext::IpHook::LocalIngress => {
&mut self.ip_routines.local_ingress
}
fnet_filter_ext::IpHook::Forwarding => &mut self.ip_routines.forwarding,
fnet_filter_ext::IpHook::LocalEgress => &mut self.ip_routines.local_egress,
fnet_filter_ext::IpHook::Egress => &mut self.ip_routines.egress,
};
(hook, RoutinePriority { priority, installation_order }, RoutineType::Ip)
}
InstalledRoutineType::Nat(InstalledNatRoutine {
hook,
priority,
installation_order,
}) => {
let hook = match hook {
fnet_filter_ext::NatHook::Ingress => &mut self.nat_routines.ingress,
fnet_filter_ext::NatHook::LocalIngress => {
&mut self.nat_routines.local_ingress
}
fnet_filter_ext::NatHook::LocalEgress => {
&mut self.nat_routines.local_egress
}
fnet_filter_ext::NatHook::Egress => &mut self.nat_routines.egress,
};
(hook, RoutinePriority { priority, installation_order }, RoutineType::Nat)
}
};
let rules = convert_rules(
namespace,
&name,
rules,
ip_version_strictness,
/* resolve_jump_target */
|name, rule_id| {
uninstalled_routines.expect_routine_resolved(&name, routine_type, rule_id)
},
)?;
assert_matches!(hook.insert(routine_priority, CoreRoutine { rules }), None);
}
Ok(())
}
}
impl<I: IpExt> From<State<I>> for CoreRoutines<I> {
fn from(state: State<I>) -> Self {
fn core_hook<I: IpExt>(routines: BTreeMap<RoutinePriority, CoreRoutine<I>>) -> CoreHook<I> {
CoreHook { routines: routines.into_values().collect() }
}
let State { ip_routines, nat_routines } = state;
CoreRoutines {
ip: netstack3_core::filter::IpRoutines {
ingress: core_hook(ip_routines.ingress),
local_ingress: core_hook(ip_routines.local_ingress),
forwarding: core_hook(ip_routines.forwarding),
local_egress: core_hook(ip_routines.local_egress),
egress: core_hook(ip_routines.egress),
},
nat: netstack3_core::filter::NatRoutines {
ingress: core_hook(nat_routines.ingress),
local_ingress: core_hook(nat_routines.local_ingress),
local_egress: core_hook(nat_routines.local_egress),
egress: core_hook(nat_routines.egress),
},
}
}
}
fn convert_rules<I, F>(
namespace: &fnet_filter_ext::NamespaceId,
routine: &str,
rules: BTreeMap<u32, Rule>,
ip_version_strictness: IpVersionStrictness,
mut resolve_jump_target: F,
) -> Result<Vec<CoreRule<I>>, CommitError>
where
I: IpExt,
F: FnMut(String, fnet_filter_ext::RuleId) -> Result<CoreUninstalledRoutine<I>, CommitError>,
{
rules
.into_iter()
.filter_map(|(index, Rule { matchers, action })| {
let rule_id = fnet_filter_ext::RuleId {
routine: fnet_filter_ext::RoutineId {
namespace: namespace.to_owned(),
name: routine.to_owned(),
},
index,
};
let matcher = match matchers.try_convert(ip_version_strictness) {
Ok(ConversionResult::State(matcher)) => matcher,
Ok(ConversionResult::Omit) => return None,
Err(IpVersionMismatchError) => {
return Some(Err(CommitError::RuleWithInvalidMatcher(rule_id)))
}
};
let action = match action {
fnet_filter_ext::Action::Accept => netstack3_core::filter::Action::Accept,
fnet_filter_ext::Action::Drop => netstack3_core::filter::Action::Drop,
fnet_filter_ext::Action::Return => netstack3_core::filter::Action::Return,
fnet_filter_ext::Action::Jump(name) => {
let target = match resolve_jump_target(name, rule_id.clone()) {
Ok(target) => target,
Err(e) => return Some(Err(e)),
};
netstack3_core::filter::Action::Jump(target)
}
};
Some(Ok(CoreRule { matcher, action, validation_info: rule_id }))
})
.collect::<Result<Vec<_>, _>>()
}
/// This is a collection of uninstalled routines that have been converted to the
/// equivalent [`netstack3_core::filter::UninstalledRoutine`] type, but which
/// organizes them by type (IP vs. NAT) and keys them by routine name, so that
/// as further installed routines are converted to Core state, jump actions can
/// reuse these routines that have already been converted.
///
/// Note that because [`netstack3_core::filter::UninstalledRoutine`] is a
/// reference-counted pointer to the underlying routine, this type can safely be
/// dropped once all other state has been converted; actions that jump to other
/// routines will hold a reference to their jump target, which will keep that
/// [`netstack3_core::filter::UninstalledRoutine`] alive.
#[derive(Default)]
struct CoreUninstalledRoutines<I: IpExt> {
ip: HashMap<String, CoreUninstalledRoutine<I>>,
nat: HashMap<String, CoreUninstalledRoutine<I>>,
routine_types: HashMap<String, RoutineType>,
}
impl<I: IpExt> CoreUninstalledRoutines<I> {
/// Converts all uninstalled routines to their `netstack3_core` equivalents up
/// front, and checks for cycles in each routine graph by keeping track of which
/// routines we've seen thus far, and returning an error if we ever encounter
/// the same routine twice.
fn convert_routines(
mut uninstalled_routines: HashMap<String, UninstalledRoutine>,
namespace: &fnet_filter_ext::NamespaceId,
ip_version_strictness: IpVersionStrictness,
) -> Result<Self, CommitError> {
// As uninstalled routines are converted, they are removed from
// `uninstalled_routines` and added to `core_uninstalled`.
let mut core_uninstalled = CoreUninstalledRoutines::default();
while !uninstalled_routines.is_empty() {
let name = uninstalled_routines.keys().next().expect("should not be empty").clone();
let _ = core_uninstalled.convert_routine(
&namespace,
&name,
&mut uninstalled_routines,
ip_version_strictness,
)?;
}
Ok(core_uninstalled)
}
/// Converts the uninstalled routine identified by `name` to its
/// `netstack3_core` equivalent, which involves also recursively converting any
/// routines referenced in `Jump` actions in the routine. The converted routine
/// is stored in either `self.nat` or `self.ip` depending on its type, and its
/// type is stored in `self.routine_types`.
fn convert_routine(
&mut self,
namespace: &fnet_filter_ext::NamespaceId,
name: &str,
uninstalled_routines: &mut HashMap<String, UninstalledRoutine>,
ip_version_strictness: IpVersionStrictness,
) -> Result<CoreUninstalledRoutine<I>, CommitError> {
// Because we remove a routine from `uninstalled_routines` whenever we begin
// converting it to its Core representation, failing to remove a routine
// here means that we have encountered it twice in the same callstack, which
// can only happen if there is a cycle in the routine graph formed by `Jump`
// actions.
//
// (NB: by this point, `Jump` targets have already been validated to refer
// to existing uninstalled routines.)
let UninstalledRoutine { routine_type, rules, id } =
uninstalled_routines.remove(name).ok_or_else(|| {
CommitError::CyclicalRoutineGraph(fnet_filter_ext::RoutineId {
namespace: namespace.to_owned(),
name: name.to_owned(),
})
})?;
let rules = convert_rules(
&namespace,
name,
rules,
ip_version_strictness,
// To resolve the target routine of a `Jump` action, either use the existing
// converted routine if it's already been converted, or recursively convert the
// target routine before continuing.
|name, rule_id| {
if self.routine_types.contains_key(&name) {
self.expect_routine_resolved(&name, routine_type, rule_id)
} else {
self.convert_routine(
namespace,
&name,
uninstalled_routines,
ip_version_strictness,
)
}
},
)?;
// Insert the resulting converted routine in the `core_uninstalled` state.
let target = CoreUninstalledRoutine::new(rules, id);
let uninstalled = match routine_type {
RoutineType::Ip => &mut self.ip,
RoutineType::Nat => &mut self.nat,
};
assert_matches!(
uninstalled.insert(name.to_owned(), target.clone()),
None,
"this routine should not have been converted unless there is a cycle, and we \
detect cycles"
);
assert_matches!(self.routine_types.insert(name.to_owned(), routine_type), None);
Ok(target)
}
fn expect_routine_resolved(
&self,
name: &str,
calling_routine_type: RoutineType,
rule_id: fnet_filter_ext::RuleId,
) -> Result<CoreUninstalledRoutine<I>, CommitError> {
let Self { ip, nat, routine_types } = self;
// Rules can only jump to target routines that are the same type as the calling
// routine (e.g. NAT to NAT).
let target_routine_type = routine_types.get(name).expect("target should be resolved");
if *target_routine_type != calling_routine_type {
return Err(CommitError::RuleWithInvalidAction(rule_id));
}
let uninstalled = match calling_routine_type {
RoutineType::Ip => &ip,
RoutineType::Nat => &nat,
};
Ok(uninstalled.get(name).expect("uninstalled routine should already be converted").clone())
}
}
#[derive(Clone)]
enum InstalledRoutineType {
Ip(InstalledIpRoutine),
Nat(InstalledNatRoutine),
}
#[derive(Clone)]
struct InstalledRoutine {
name: String,
routine_type: InstalledRoutineType,
rules: BTreeMap<u32, Rule>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum RoutineType {
Ip,
Nat,
}
#[derive(Debug, Clone)]
struct UninstalledRoutine {
routine_type: RoutineType,
rules: BTreeMap<u32, Rule>,
id: usize,
}
/// Converts a controller's state to the equivalent `netstack3_core` state.
///
/// Because Core state is parameterized on IP version, this requires splitting
/// the state in each of the controller's namespaces into v4-specific and v6-
/// specific state.
pub(super) fn convert_to_core(
namespaces: HashMap<fnet_filter_ext::NamespaceId, Namespace>,
) -> Result<(State<Ipv4>, State<Ipv6>), CommitError> {
let (mut v4, mut v6) = (State::<Ipv4>::default(), State::<Ipv6>::default());
for (id, namespace) in namespaces {
let Namespace { domain, routines } = namespace;
let (installed, uninstalled) = routines.into_iter().fold(
(Vec::new(), HashMap::new()),
|(mut installed, mut uninstalled), (name, routine)| {
let Routine { routine_type, rules } = routine;
match routine_type {
super::controller::RoutineType::Ip(IpRoutineType::Uninstalled(id)) => {
assert_matches!(
uninstalled.insert(
name,
UninstalledRoutine { routine_type: RoutineType::Ip, rules, id },
),
None
);
}
super::controller::RoutineType::Nat(NatRoutineType::Uninstalled(id)) => {
assert_matches!(
uninstalled.insert(
name,
UninstalledRoutine { routine_type: RoutineType::Nat, rules, id },
),
None
);
}
super::controller::RoutineType::Ip(IpRoutineType::Installed(installation)) => {
installed.push(InstalledRoutine {
name,
routine_type: InstalledRoutineType::Ip(installation),
rules,
})
}
super::controller::RoutineType::Nat(NatRoutineType::Installed(
installation,
)) => installed.push(InstalledRoutine {
name,
routine_type: InstalledRoutineType::Nat(installation),
rules,
}),
}
(installed, uninstalled)
},
);
match domain {
fnet_filter_ext::Domain::Ipv4 => {
v4.add_namespace(
&id,
installed,
uninstalled,
// Because this is an IPv4-specific domain, any rule that includes an
// IP-specific matcher must also be IPv4-specific.
IpVersionStrictness::IpVersionMustMatchState,
)?;
}
fnet_filter_ext::Domain::Ipv6 => {
v6.add_namespace(
&id,
installed,
uninstalled,
// Because this is an IPv6-specific domain, any rule that includes an
// IP-specific matcher must also be IPv6-specific.
IpVersionStrictness::IpVersionMustMatchState,
)?;
}
fnet_filter_ext::Domain::AllIp => {
// Because this is an IP-agnostic domain, rules can be IPv4- or IPv6-specific,
// but IP-version-specific rules will be omitted from the version of the state
// that does not match the rule's version. For example, a rule with an IPv4
// address matcher will not be installed in Core's IPv6 filtering state.
v4.add_namespace(
&id,
installed.clone(),
uninstalled.clone(),
IpVersionStrictness::IpVersionCanDifferFromState,
)?;
v6.add_namespace(
&id,
installed,
uninstalled,
IpVersionStrictness::IpVersionCanDifferFromState,
)?;
}
}
}
Ok((v4, v6))
}