route rule: Add support of iif and action.

* Added `RouteRule.iif` for matching on incoming interface name.
* Added `RouteRule.action` for these actions:
    * `RouteRuleAction::Blackhole`
    * `RouteRuleAction::Unreachable`
    * `RouteRuleAction::Prohibit`

Bumped required of nispor to 1.2.9 for the fix of route rule actions.

Integration test cases included.

Signed-off-by: Gris Ge <fge@redhat.com>
This commit is contained in:
Gris Ge 2022-12-13 21:20:08 +08:00
parent c7d22d9f6d
commit 81c661c2f7
12 changed files with 235 additions and 36 deletions

View File

@ -16,7 +16,7 @@ edition = "2018"
path = "lib.rs"
[dependencies.nispor]
version = "1.2.8"
version = "1.2.9"
optional = true
[dependencies.ipnet]

View File

@ -158,4 +158,6 @@ pub use crate::policy::{
NetworkCaptureRules, NetworkPolicy, NetworkStateTemplate,
};
pub use crate::route::{RouteEntry, RouteState, Routes};
pub use crate::route_rule::{RouteRuleEntry, RouteRuleState, RouteRules};
pub use crate::route_rule::{
RouteRuleAction, RouteRuleEntry, RouteRuleState, RouteRules,
};

View File

@ -1,6 +1,8 @@
// SPDX-License-Identifier: Apache-2.0
use log::warn;
use crate::{AddressFamily, RouteRuleEntry, RouteRules};
use crate::{AddressFamily, RouteRuleAction, RouteRuleEntry, RouteRules};
// Due to a bug in NetworkManager all route rules added using NetworkManager are
// using RTM_PROTOCOL UnSpec. Therefore, we need to support it until it is
@ -38,8 +40,21 @@ pub(crate) fn get_route_rules(
for np_rule in np_rules {
let mut rule = RouteRuleEntry::new();
// We only support route rules with 'table' action
if np_rule.action != nispor::RuleAction::Table {
continue;
match np_rule.action {
nispor::RuleAction::Table => (),
nispor::RuleAction::Blackhole => {
rule.action = Some(RouteRuleAction::Blackhole)
}
nispor::RuleAction::Unreachable => {
rule.action = Some(RouteRuleAction::Unreachable)
}
nispor::RuleAction::Prohibit => {
rule.action = Some(RouteRuleAction::Prohibit)
}
_ => {
log::debug!("Got unsupported route rule {:?}", np_rule);
continue;
}
}
// Filter out the routes with protocols that we do not support
if let Some(rule_protocol) = np_rule.protocol.as_ref() {
@ -47,6 +62,7 @@ pub(crate) fn get_route_rules(
continue;
}
}
rule.iif = np_rule.iif.clone();
rule.ip_to = np_rule.dst.clone();
rule.ip_from = np_rule.src.clone();
rule.table_id = np_rule.table;

View File

@ -55,7 +55,7 @@ pub use self::ovs::{
NmSettingOvsIface, NmSettingOvsPatch, NmSettingOvsPort,
};
pub use self::route::NmIpRoute;
pub use self::route_rule::NmIpRouteRule;
pub use self::route_rule::{NmIpRouteRule, NmIpRouteRuleAction};
pub use self::sriov::{NmSettingSriov, NmSettingSriovVf, NmSettingSriovVfVlan};
pub use self::user::NmSettingUser;
pub use self::veth::NmSettingVeth;

View File

@ -32,6 +32,8 @@ pub struct NmIpRouteRule {
pub table: Option<u32>,
pub fw_mark: Option<u32>,
pub fw_mask: Option<u32>,
pub iifname: Option<String>,
pub action: Option<NmIpRouteRuleAction>,
_other: DbusDictionary,
}
@ -48,6 +50,9 @@ impl TryFrom<DbusDictionary> for NmIpRouteRule {
table: _from_map!(v, "table", u32::try_from)?,
fw_mark: _from_map!(v, "fw_mark", u32::try_from)?,
fw_mask: _from_map!(v, "fw_mask", u32::try_from)?,
iifname: _from_map!(v, "iifname", String::try_from)?,
action: _from_map!(v, "action", u8::try_from)?
.map(NmIpRouteRuleAction::from),
_other: v,
})
}
@ -113,6 +118,18 @@ impl NmIpRouteRule {
zvariant::Value::new(zvariant::Value::new(v)),
)?;
}
if let Some(v) = &self.iifname {
ret.append(
zvariant::Value::new("iifname"),
zvariant::Value::new(zvariant::Value::new(v)),
)?;
}
if let Some(v) = &self.action {
ret.append(
zvariant::Value::new("action"),
zvariant::Value::new(zvariant::Value::new(u8::from(*v))),
)?;
}
for (key, value) in self._other.iter() {
ret.append(
@ -144,3 +161,41 @@ pub(crate) fn nm_ip_rules_to_value(
}
Ok(zvariant::Value::Array(rule_values))
}
const RTN_BLACKHOLE: u8 = 6;
const RTN_UNREACHABLE: u8 = 7;
const RTN_PROHIBIT: u8 = 8;
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub enum NmIpRouteRuleAction {
Blackhole,
Unreachable,
Prohibit,
Other(u8),
}
impl From<u8> for NmIpRouteRuleAction {
fn from(v: u8) -> Self {
match v {
RTN_BLACKHOLE => Self::Blackhole,
RTN_UNREACHABLE => Self::Unreachable,
RTN_PROHIBIT => Self::Prohibit,
_ => {
log::warn!("Unsupported IP route rule action {}", v);
Self::Other(v)
}
}
}
}
impl From<NmIpRouteRuleAction> for u8 {
fn from(v: NmIpRouteRuleAction) -> Self {
match v {
NmIpRouteRuleAction::Blackhole => RTN_BLACKHOLE,
NmIpRouteRuleAction::Unreachable => RTN_UNREACHABLE,
NmIpRouteRuleAction::Prohibit => RTN_PROHIBIT,
NmIpRouteRuleAction::Other(d) => d,
}
}
}

View File

@ -26,14 +26,15 @@ pub use self::active_connection::{
NmActiveConnection, NM_ACTIVATION_STATE_FLAG_EXTERNAL,
};
pub use self::connection::{
NmConnection, NmIpRoute, NmIpRouteRule, NmSetting8021X, NmSettingBond,
NmSettingBridge, NmSettingBridgePort, NmSettingBridgeVlanRange,
NmSettingConnection, NmSettingEthtool, NmSettingInfiniBand, NmSettingIp,
NmSettingIpMethod, NmSettingLoopback, NmSettingMacVlan, NmSettingOvsBridge,
NmSettingOvsDpdk, NmSettingOvsExtIds, NmSettingOvsIface, NmSettingOvsPatch,
NmSettingOvsPort, NmSettingSriov, NmSettingSriovVf, NmSettingSriovVfVlan,
NmSettingUser, NmSettingVeth, NmSettingVlan, NmSettingVrf, NmSettingVxlan,
NmSettingWired, NmSettingsConnectionFlag, NmVlanProtocol,
NmConnection, NmIpRoute, NmIpRouteRule, NmIpRouteRuleAction,
NmSetting8021X, NmSettingBond, NmSettingBridge, NmSettingBridgePort,
NmSettingBridgeVlanRange, NmSettingConnection, NmSettingEthtool,
NmSettingInfiniBand, NmSettingIp, NmSettingIpMethod, NmSettingLoopback,
NmSettingMacVlan, NmSettingOvsBridge, NmSettingOvsDpdk, NmSettingOvsExtIds,
NmSettingOvsIface, NmSettingOvsPatch, NmSettingOvsPort, NmSettingSriov,
NmSettingSriovVf, NmSettingSriovVfVlan, NmSettingUser, NmSettingVeth,
NmSettingVlan, NmSettingVrf, NmSettingVxlan, NmSettingWired,
NmSettingsConnectionFlag, NmVlanProtocol,
};
#[cfg(feature = "query_apply")]
pub use self::device::{NmDevice, NmDeviceState, NmDeviceStateReason};

View File

@ -64,6 +64,12 @@ pub(crate) fn gen_nm_ip_rules<'a>(
nm_rule.fw_mark = rule.fwmark;
nm_rule.fw_mask = rule.fwmask;
if let Some(iif) = rule.iif.as_ref() {
nm_rule.iifname = Some(iif.to_string());
}
if let Some(action) = rule.action.as_ref() {
nm_rule.action = Some(u8::from(*action).into());
}
ret.push(nm_rule);
}

View File

@ -271,6 +271,12 @@ pub struct RouteRuleEntry {
)]
/// Select the fwmask value to match
pub fwmask: Option<u32>,
/// Actions for matching packages.
#[serde(skip_serializing_if = "Option::is_none")]
pub action: Option<RouteRuleAction>,
/// Incoming interface.
#[serde(skip_serializing_if = "Option::is_none")]
pub iif: Option<String>,
}
impl RouteRuleEntry {
@ -329,7 +335,9 @@ impl RouteRuleEntry {
if self.fwmark.is_none() && self.fwmask.is_some() {
let e = NmstateError::new(
ErrorKind::InvalidArgument,
format!("fwmask is present but fwmark is not defined or is zero {self:?}"
format!(
"fwmask is present but fwmark is \
not defined or is zero {self:?}"
),
);
log::error!("{}", e);
@ -393,12 +401,15 @@ impl RouteRuleEntry {
if self.fwmask.is_some() && self.fwmask != other.fwmask {
return false;
}
if self.action.is_some() && self.action != other.action {
return false;
}
true
}
// Return tuple of (no_absent, is_ipv4, table_id, ip_from,
// ip_to, priority, fwmark, fwmask)
fn sort_key(&self) -> (bool, bool, u32, &str, &str, i64, u32, u32) {
// ip_to, priority, fwmark, fwmask, action)
fn sort_key(&self) -> (bool, bool, u32, &str, &str, i64, u32, u32, u8) {
(
!matches!(self.state, Some(RouteRuleState::Absent)),
{
@ -424,6 +435,7 @@ impl RouteRuleEntry {
.unwrap_or(RouteRuleEntry::USE_DEFAULT_PRIORITY),
self.fwmark.unwrap_or(0),
self.fwmask.unwrap_or(0),
self.action.map(u8::from).unwrap_or(0),
)
}
@ -530,3 +542,41 @@ fn flat_absent_rule(
}
ret
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
#[serde(deny_unknown_fields)]
pub enum RouteRuleAction {
Blackhole,
Unreachable,
Prohibit,
}
impl std::fmt::Display for RouteRuleAction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Blackhole => "blackhole",
Self::Unreachable => "unreachable",
Self::Prohibit => "prohibit",
}
)
}
}
const FR_ACT_BLACKHOLE: u8 = 6;
const FR_ACT_UNREACHABLE: u8 = 7;
const FR_ACT_PROHIBIT: u8 = 8;
impl From<RouteRuleAction> for u8 {
fn from(v: RouteRuleAction) -> u8 {
match v {
RouteRuleAction::Blackhole => FR_ACT_BLACKHOLE,
RouteRuleAction::Unreachable => FR_ACT_UNREACHABLE,
RouteRuleAction::Prohibit => FR_ACT_PROHIBIT,
}
}
}

View File

@ -167,6 +167,7 @@ fn gen_rule_entry(
priority: Some(priority),
fwmark: Some(fwmark),
fwmask: Some(fwmask),
..Default::default()
}
}

View File

@ -55,6 +55,11 @@ class RouteRule:
FAMILY = "family"
FAMILY_IPV4 = "ipv4"
FAMILY_IPV6 = "ipv6"
IIF = "iif"
ACTION = "action"
ACTION_BLACKHOLE = "blackhole"
ACTION_UNREACHABLE = "unreachable"
ACTION_PROHIBIT = "prohibit"
class DNS:

View File

@ -1073,6 +1073,8 @@ def _check_ip_rules(rules):
rule.get(RouteRule.FWMARK),
rule.get(RouteRule.FWMASK),
rule.get(RouteRule.FAMILY),
rule.get(RouteRule.IIF),
rule.get(RouteRule.ACTION),
)
@ -1343,3 +1345,67 @@ def test_do_not_show_bgp_route(static_route_with_additional_bgp_route):
for route in routes:
assert route[Route.DESTINATION] != BGP_ROUTE_DST_V4
assert route[Route.DESTINATION] != BGP_ROUTE_DST_V6
@pytest.mark.tier1
def test_route_rule_iif(route_rule_test_env):
desired_rules = [
{
RouteRule.IIF: "eth1",
RouteRule.ROUTE_TABLE: IPV4_ROUTE_TABLE_ID1,
RouteRule.IP_FROM: IPV4_TEST_NET1,
},
{
RouteRule.IIF: "eth1",
RouteRule.ROUTE_TABLE: IPV6_ROUTE_TABLE_ID1,
RouteRule.IP_FROM: IPV6_TEST_NET1,
},
]
libnmstate.apply({RouteRule.KEY: {RouteRule.CONFIG: desired_rules}})
_check_ip_rules(desired_rules)
@pytest.mark.tier1
def test_route_rule_action(route_rule_test_env):
desired_rules = [
{
RouteRule.PRIORITY: 10000,
RouteRule.IIF: "eth1",
RouteRule.IP_FROM: "192.0.2.1/32",
RouteRule.ACTION: RouteRule.ACTION_BLACKHOLE,
},
{
RouteRule.PRIORITY: 10001,
RouteRule.IIF: "eth1",
RouteRule.IP_FROM: "192.0.2.2/32",
RouteRule.ACTION: RouteRule.ACTION_UNREACHABLE,
},
{
RouteRule.PRIORITY: 10002,
RouteRule.IIF: "eth1",
RouteRule.IP_FROM: "192.0.2.3/32",
RouteRule.ACTION: RouteRule.ACTION_PROHIBIT,
},
{
RouteRule.PRIORITY: 20000,
RouteRule.IIF: "eth1",
RouteRule.IP_FROM: "2001:db8:1::1/128",
RouteRule.ACTION: RouteRule.ACTION_BLACKHOLE,
},
{
RouteRule.PRIORITY: 20001,
RouteRule.IIF: "eth1",
RouteRule.IP_FROM: "2001:db8:1::2/128",
RouteRule.ACTION: RouteRule.ACTION_UNREACHABLE,
},
{
RouteRule.PRIORITY: 20002,
RouteRule.IIF: "eth1",
RouteRule.IP_FROM: "2001:db8:1::3/128",
RouteRule.ACTION: RouteRule.ACTION_PROHIBIT,
},
]
libnmstate.apply({RouteRule.KEY: {RouteRule.CONFIG: desired_rules}})
_check_ip_rules(desired_rules)

View File

@ -1,21 +1,4 @@
#
# Copyright (c) 2019-2020 Red Hat, Inc.
#
# This file is part of nmstate
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 2.1 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: LGPL-2.1-or-later
import json
import logging
@ -25,7 +8,15 @@ from . import cmdlib
def ip_rule_exist_in_os(
ip_from, ip_to, priority, table, fwmark, fwmask, family
ip_from,
ip_to,
priority,
table,
fwmark,
fwmask,
family,
iif=None,
action=None,
):
expected_rule = locals()
logging.debug("Checking ip rule for {}".format(expected_rule))
@ -76,6 +67,12 @@ def ip_rule_exist_in_os(
if fwmask is not None and rule["fwmask"] != hex(fwmask):
found = False
continue
if iif is not None and rule["iif"] != iif:
found = False
continue
if action is not None and rule["action"] != action:
found = False
continue
if found:
break
if not found: