route_rule: support from all to all routing policy

Add support to `from all to all` routing policy. In static config
generator it will add `from 0.0.0.0/0` or `from ::/0` to the keyfile.

A new parameter `family` have been introduced to the route-rule config
field. This parameter is used to specify the address family. If `family`
is not specified it will fail on validation, if not specified from
Nispor it will assume IPv4 as it was done in the past.

Example:

```yaml
route-rules:
  config:
    - route-table: 254
      priority: 100
      family: ipv4
```

Integration test cases added.

Fixes #1417

Signed-off-by: Fernando Fernandez Mancera <ffmancera@riseup.net>
This commit is contained in:
Fernando Fernandez Mancera 2022-11-07 13:26:39 +01:00 committed by Fernando Fernández Mancera
parent 84e0b3e176
commit c53dc56824
16 changed files with 275 additions and 38 deletions

View File

@ -73,8 +73,8 @@ pub struct BaseInterface {
pub ipv6: Option<InterfaceIpv6>,
#[serde(skip_serializing_if = "Option::is_none")]
/// Interface wide MPTCP flags.
/// Nmstate will apply these flags to all valid IP addresses(both static and
/// dynamic).
/// Nmstate will apply these flags to all valid IP addresses(both static
/// and dynamic).
pub mptcp: Option<MptcpConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
// None here mean no change, empty string mean detach from controller.

View File

@ -964,7 +964,7 @@ fn default_allow_extra_address() -> bool {
true
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum AddressFamily {

View File

@ -1,21 +1,69 @@
use crate::{RouteRuleEntry, RouteRules};
use log::warn;
pub(crate) fn get_route_rules(np_rules: &[nispor::RouteRule]) -> RouteRules {
use crate::{AddressFamily, 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
// fixed.
const SUPPORTED_STATIC_ROUTE_PROTOCOL: [nispor::RouteProtocol; 3] = [
nispor::RouteProtocol::Boot,
nispor::RouteProtocol::Static,
nispor::RouteProtocol::UnSpec,
];
const SUPPORTED_ROUTE_PROTOCOL: [nispor::RouteProtocol; 8] = [
nispor::RouteProtocol::Boot,
nispor::RouteProtocol::Static,
nispor::RouteProtocol::Ra,
nispor::RouteProtocol::Dhcp,
nispor::RouteProtocol::Mrouted,
nispor::RouteProtocol::KeepAlived,
nispor::RouteProtocol::Babel,
nispor::RouteProtocol::UnSpec,
];
pub(crate) fn get_route_rules(
np_rules: &[nispor::RouteRule],
running_config_only: bool,
) -> RouteRules {
let mut ret = RouteRules::new();
let mut rules = Vec::new();
let protocols = if running_config_only {
SUPPORTED_STATIC_ROUTE_PROTOCOL.as_slice()
} else {
SUPPORTED_ROUTE_PROTOCOL.as_slice()
};
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;
}
// Filter out the routes with protocols that we do not support
if let Some(rule_protocol) = np_rule.protocol.as_ref() {
if !protocols.contains(rule_protocol) {
continue;
}
}
rule.ip_to = np_rule.dst.clone();
rule.ip_from = np_rule.src.clone();
rule.table_id = np_rule.table;
rule.priority = np_rule.priority.map(i64::from);
rule.fwmark = np_rule.fw_mark;
rule.fwmask = np_rule.fw_mask;
rule.family = match np_rule.address_family {
nispor::AddressFamily::IPv4 => Some(AddressFamily::IPv4),
nispor::AddressFamily::IPv6 => Some(AddressFamily::IPv6),
_ => {
warn!(
"Unsupported route rule family {:?}",
np_rule.address_family
);
None
}
};
rules.push(rule);
}
ret.config = Some(rules);

View File

@ -123,7 +123,7 @@ pub(crate) fn nispor_retrieve(
}
set_controller_type(&mut net_state.interfaces);
net_state.routes = get_routes(running_config_only);
net_state.rules = get_route_rules(&np_state.rules);
net_state.rules = get_route_rules(&np_state.rules, running_config_only);
Ok(net_state)
}

View File

@ -1,10 +1,12 @@
// SPDX-License-Identifier: Apache-2.0
use log::warn;
use std::collections::HashMap;
use super::super::NmIpRouteRule;
const DEFAULT_ROUTE_TABLE: u32 = 254;
const AF_INET: i32 = 2;
impl NmIpRouteRule {
pub(crate) fn to_keyfile(&self) -> HashMap<String, String> {
@ -23,10 +25,25 @@ impl NmIpRouteRule {
keys.push(to_str);
}
let from_str = match (self.from.as_ref(), self.from_len.as_ref()) {
(Some(f), Some(f_len)) => format!("from {f}/{f_len}"),
(Some(f), None) => format!("from {f}"),
_ => "".to_string(),
let mut from_str =
match (self.from.as_ref(), self.from_len.as_ref()) {
(Some(f), Some(f_len)) => format!("from {f}/{f_len}"),
(Some(f), None) => format!("from {f}"),
_ => "".to_string(),
};
from_str = if self.from.is_none() && self.to.is_none() {
if let Some(family) = self.family {
if family == AF_INET {
"from 0.0.0.0/0".to_string()
} else {
"from ::/0".to_string()
}
} else {
warn!("Neither from, to or family specified on route rule. Assuming IPv4.");
"from 0.0.0.0/0".to_string()
}
} else {
from_str
};
if !from_str.is_empty() {
keys.push(from_str);

View File

@ -6,18 +6,18 @@ use super::{
};
use crate::nm::nm_dbus::{NmConnection, NmSettingIp, NmSettingIpMethod};
use crate::{
BaseInterface, Dhcpv4ClientId, Dhcpv6Duid, ErrorKind, Interface,
InterfaceIpv4, InterfaceIpv6, Ipv6AddrGenMode, NmstateError, RouteEntry,
RouteRuleEntry, WaitIp,
AddressFamily, BaseInterface, Dhcpv4ClientId, Dhcpv6Duid, ErrorKind,
Interface, InterfaceIpv4, InterfaceIpv6, Ipv6AddrGenMode, NmstateError,
RouteEntry, RouteRuleEntry, WaitIp,
};
const ADDR_GEN_MODE_EUI64: i32 = 0;
const ADDR_GEN_MODE_STABLE_PRIVACY: i32 = 1;
fn gen_nm_ipv4_setting(
fn gen_nm_ipv4_setting<'a>(
iface_ip: Option<&InterfaceIpv4>,
routes: Option<&[RouteEntry]>,
rules: Option<&[RouteRuleEntry]>,
rules: impl std::iter::Iterator<Item = &'a RouteRuleEntry>,
nm_conn: &mut NmConnection,
) -> Result<(), NmstateError> {
let iface_ip = match iface_ip {
@ -85,9 +85,7 @@ fn gen_nm_ipv4_setting(
// Clean up static routes if ip is disabled
nm_setting.routes = Vec::new();
}
if let Some(rules) = rules {
nm_setting.route_rules = gen_nm_ip_rules(rules, false)?;
}
nm_setting.route_rules = gen_nm_ip_rules(rules, false)?;
if let Some(dns) = &iface_ip.dns {
apply_nm_dns_setting(&mut nm_setting, dns);
}
@ -95,10 +93,10 @@ fn gen_nm_ipv4_setting(
Ok(())
}
fn gen_nm_ipv6_setting(
fn gen_nm_ipv6_setting<'a>(
iface_ip: Option<&InterfaceIpv6>,
routes: Option<&[RouteEntry]>,
rules: Option<&[RouteRuleEntry]>,
rules: impl std::iter::Iterator<Item = &'a RouteRuleEntry>,
nm_conn: &mut NmConnection,
) -> Result<(), NmstateError> {
let iface_ip = match iface_ip {
@ -179,9 +177,7 @@ fn gen_nm_ipv6_setting(
nm_setting.routes = gen_nm_ip_routes(routes, true)?;
}
}
if let Some(rules) = rules {
nm_setting.route_rules = gen_nm_ip_rules(rules, true)?;
}
nm_setting.route_rules = gen_nm_ip_rules(rules, true)?;
if let Some(dns) = &iface_ip.dns {
apply_nm_dns_setting(&mut nm_setting, dns);
}
@ -197,8 +193,24 @@ pub(crate) fn gen_nm_ip_setting(
) -> Result<(), NmstateError> {
let base_iface = iface.base_iface();
if base_iface.can_have_ip() {
gen_nm_ipv4_setting(base_iface.ipv4.as_ref(), routes, rules, nm_conn)?;
gen_nm_ipv6_setting(base_iface.ipv6.as_ref(), routes, rules, nm_conn)?;
gen_nm_ipv4_setting(
base_iface.ipv4.as_ref(),
routes,
rules
.unwrap_or_default()
.iter()
.filter(|r| r.family == Some(AddressFamily::IPv4)),
nm_conn,
)?;
gen_nm_ipv6_setting(
base_iface.ipv6.as_ref(),
routes,
rules
.unwrap_or_default()
.iter()
.filter(|r| r.family == Some(AddressFamily::IPv6)),
nm_conn,
)?;
apply_nmstate_wait_ip(base_iface, nm_conn);
} else {
nm_conn.ipv4 = None;

View File

@ -4,7 +4,10 @@ use std::convert::TryFrom;
use super::super::nm_dbus::NmIpRouteRule;
use crate::{ip::is_ipv6_addr, InterfaceIpAddr, NmstateError, RouteRuleEntry};
use crate::{
ip::is_ipv6_addr, ip::AddressFamily, InterfaceIpAddr, NmstateError,
RouteRuleEntry,
};
// NM require route rule priority been set explicitly, use 30,000 when
// desire state instruct to use USE_DEFAULT_PRIORITY
@ -13,14 +16,19 @@ const ROUTE_RULE_DEFAULT_PRIORIRY: u32 = 30000;
const AF_INET6: i32 = 10;
const AF_INET: i32 = 2;
pub(crate) fn gen_nm_ip_rules(
rules: &[RouteRuleEntry],
pub(crate) fn gen_nm_ip_rules<'a>(
rules: impl std::iter::Iterator<Item = &'a RouteRuleEntry>,
is_ipv6: bool,
) -> Result<Vec<NmIpRouteRule>, NmstateError> {
let mut ret = Vec::new();
for rule in rules {
let mut nm_rule = NmIpRouteRule::default();
nm_rule.family = Some(if is_ipv6 { AF_INET6 } else { AF_INET });
if let Some(family) = rule.family {
if is_ipv6 != matches!(family, AddressFamily::IPv6) {
continue;
}
}
if let Some(addr) = rule.ip_from.as_deref() {
match (is_ipv6, is_ipv6_addr(addr)) {
(true, true) | (false, false) => {

View File

@ -4,7 +4,7 @@ use std::convert::TryFrom;
use serde::{Deserialize, Serialize};
use crate::{
ip::{is_ipv6_addr, sanitize_ip_network},
ip::{is_ipv6_addr, sanitize_ip_network, AddressFamily},
ErrorKind, InterfaceIpAddr, NmstateError,
};
@ -222,8 +222,11 @@ impl Default for RouteRuleState {
#[non_exhaustive]
#[serde(deny_unknown_fields)]
pub struct RouteRuleEntry {
/// Indicate the address family of the route rule.
#[serde(skip_serializing_if = "Option::is_none")]
pub family: Option<AddressFamily>,
/// Indicate this is normal route rule or absent route rule.
#[serde(skip_serializing_if = "Option::is_none")]
pub state: Option<RouteRuleState>,
#[serde(skip_serializing_if = "Option::is_none")]
/// Source prefix to match.
@ -280,20 +283,50 @@ impl RouteRuleEntry {
Self::default()
}
// * Neither ip_from nor ip_to should be defined
pub(crate) fn validate(&self) -> Result<(), NmstateError> {
if self.ip_from.is_none() && self.ip_to.is_none() {
if self.ip_from.is_none()
&& self.ip_to.is_none()
&& self.family.is_none()
{
let e = NmstateError::new(
ErrorKind::InvalidArgument,
format!(
"Neither ip-from or ip-to is defined in route rule {:?}",
"Neither ip-from, ip-to nor family is defined {:?}",
self
),
);
log::error!("{}", e);
return Err(e);
} else if let Some(family) = self.family {
if let Some(ip_from) = self.ip_from.as_ref() {
if is_ipv6_addr(ip_from.as_str())
!= matches!(family, AddressFamily::IPv6)
{
let e = NmstateError::new(
ErrorKind::InvalidArgument,
format!("The ip-from format mismatches with the family set {:?}",
self
),
);
log::error!("{}", e);
return Err(e);
}
}
if let Some(ip_to) = self.ip_to.as_ref() {
if is_ipv6_addr(ip_to.as_str())
!= matches!(family, AddressFamily::IPv6)
{
let e = NmstateError::new(
ErrorKind::InvalidArgument,
format!("The ip-to format mismatches with the family set {:?}",
self
),
);
log::error!("{}", e);
return Err(e);
}
}
}
if self.fwmark.is_none() && self.fwmask.is_some() {
let e = NmstateError::new(
ErrorKind::InvalidArgument,
@ -375,9 +408,11 @@ impl RouteRuleEntry {
!is_ipv6_addr(ip_from.as_str())
} else if let Some(ip_to) = self.ip_to.as_ref() {
!is_ipv6_addr(ip_to.as_str())
} else if let Some(family) = self.family.as_ref() {
*family == AddressFamily::IPv4
} else {
log::warn!(
"Neither ip-from nor ip-to \
"Neither ip-from, ip-to nor family \
is defined, treating it a IPv4 route rule"
);
true
@ -397,6 +432,12 @@ impl RouteRuleEntry {
pub(crate) fn sanitize(&mut self) -> Result<(), NmstateError> {
if let Some(ip) = self.ip_from.as_ref() {
let new_ip = sanitize_ip_network(ip)?;
if self.family.is_none() {
match is_ipv6_addr(new_ip.as_str()) {
true => self.family = Some(AddressFamily::IPv6),
false => self.family = Some(AddressFamily::IPv4),
};
}
if ip != &new_ip {
log::warn!("Route rule ip-from {} sanitized to {}", ip, new_ip);
self.ip_from = Some(new_ip);
@ -404,6 +445,12 @@ impl RouteRuleEntry {
}
if let Some(ip) = self.ip_to.as_ref() {
let new_ip = sanitize_ip_network(ip)?;
if self.family.is_none() {
match is_ipv6_addr(new_ip.as_str()) {
true => self.family = Some(AddressFamily::IPv6),
false => self.family = Some(AddressFamily::IPv4),
};
}
if ip != &new_ip {
log::warn!("Route rule ip-to {} sanitized to {}", ip, new_ip);
self.ip_to = Some(new_ip);

View File

@ -159,6 +159,7 @@ fn gen_rule_entry(
fwmask: u32,
) -> RouteRuleEntry {
RouteRuleEntry {
family: None,
state: None,
ip_from: Some(ip_from.to_string()),
ip_to: Some(ip_to.to_string()),
@ -391,3 +392,33 @@ ip-from: 2001:db8:2:0000::ffff
assert_eq!(rule.ip_to.unwrap(), "2001:db8:1::2/128");
assert_eq!(rule.ip_from.unwrap(), "2001:db8:2::ffff/128");
}
#[test]
fn test_route_rule_validate_ipv6_family_ip_from() {
let rule: RouteRuleEntry = serde_yaml::from_str(
r#"
ip-from: 2001:db8:b::/64
priority: 30000
route-table: 200
family: ipv6
"#,
)
.unwrap();
rule.validate().unwrap();
}
#[test]
fn test_route_rule_validate_ipv4_family_ip_from() {
let rule: RouteRuleEntry = serde_yaml::from_str(
r#"
ip-from: 192.168.2.0/24
priority: 30000
route-table: 200
family: ipv4
"#,
)
.unwrap();
rule.validate().unwrap();
}

View File

@ -68,6 +68,9 @@ class RouteRule:
STATE_ABSENT = "absent"
FWMARK = "fwmark"
FWMASK = "fwmask"
FAMILY = "family"
FAMILY_IPV4 = "ipv4"
FAMILY_IPV6 = "ipv6"
class DNS:

View File

@ -160,6 +160,7 @@ def test_add_remove_route_rule(eth1_up):
rule.get(RouteRule.ROUTE_TABLE),
rule.get(RouteRule.FWMARK),
rule.get(RouteRule.FWMASK),
rule.get(RouteRule.FAMILY),
)

View File

@ -118,6 +118,7 @@ route-rules:
rule.get(RouteRule.ROUTE_TABLE),
rule.get(RouteRule.FWMARK),
rule.get(RouteRule.FWMASK),
rule.get(RouteRule.FAMILY),
)

View File

@ -310,9 +310,11 @@ def eth1_with_static_route_and_rule(eth1_up):
- ip-from: 2001:db8:b::/64
priority: 30000
route-table: 200
family: ipv6
- ip-from: 192.168.3.2/32
priority: 30001
route-table: 200
family: ipv4
interfaces:
- name: eth1
type: ethernet

View File

@ -939,6 +939,7 @@ def test_add_route_rule_to_ovs_interface_dhcp_auto_route_table(
route_rule.get(RouteRule.ROUTE_TABLE),
route_rule.get(RouteRule.FWMARK),
route_rule.get(RouteRule.FWMASK),
route_rule.get(RouteRule.FAMILY),
)

View File

@ -670,7 +670,7 @@ def route_rule_test_env(eth1_static_gateway_dns):
@pytest.mark.tier1
def test_route_rule_add_without_from_or_to(route_rule_test_env):
def test_route_rule_add_without_from_or_to_or_family(route_rule_test_env):
state = route_rule_test_env
state[RouteRule.KEY] = {
RouteRule.CONFIG: [
@ -1002,6 +1002,67 @@ def test_route_rule_fwmark_with_fwmask(route_rule_test_env):
_check_ip_rules(rules)
@pytest.mark.tier1
def test_route_rule_from_all_to_all(route_rule_test_env):
state = route_rule_test_env
rules = [
{
RouteRule.ROUTE_TABLE: IPV6_ROUTE_TABLE_ID1,
RouteRule.PRIORITY: 100,
RouteRule.FAMILY: RouteRule.FAMILY_IPV6,
},
{
RouteRule.ROUTE_TABLE: IPV4_ROUTE_TABLE_ID1,
RouteRule.PRIORITY: 100,
RouteRule.FAMILY: RouteRule.FAMILY_IPV4,
},
]
state[RouteRule.KEY] = {RouteRule.CONFIG: rules}
libnmstate.apply(state)
_check_ip_rules(rules)
@pytest.mark.tier1
def test_route_rule_from_all_to_all_ipv4(route_rule_test_env):
state = route_rule_test_env
rules = [
{
RouteRule.ROUTE_TABLE: IPV4_ROUTE_TABLE_ID1,
RouteRule.PRIORITY: 100,
RouteRule.FAMILY: RouteRule.FAMILY_IPV4,
},
]
state[RouteRule.KEY] = {RouteRule.CONFIG: rules}
libnmstate.apply(state)
_check_ip_rules(rules)
rules[0][RouteRule.FAMILY] = RouteRule.FAMILY_IPV6
with pytest.raises(AssertionError):
assert _check_ip_rules(rules)
@pytest.mark.tier1
def test_route_rule_from_all_to_all_ipv6(route_rule_test_env):
state = route_rule_test_env
rules = [
{
RouteRule.ROUTE_TABLE: IPV6_ROUTE_TABLE_ID1,
RouteRule.PRIORITY: 100,
RouteRule.FAMILY: RouteRule.FAMILY_IPV6,
},
]
state[RouteRule.KEY] = {RouteRule.CONFIG: rules}
libnmstate.apply(state)
_check_ip_rules(rules)
rules[0][RouteRule.FAMILY] = RouteRule.FAMILY_IPV4
with pytest.raises(AssertionError):
assert _check_ip_rules(rules)
def _check_ip_rules(rules):
for rule in rules:
iprule.ip_rule_exist_in_os(
@ -1011,6 +1072,7 @@ def _check_ip_rules(rules):
rule.get(RouteRule.ROUTE_TABLE),
rule.get(RouteRule.FWMARK),
rule.get(RouteRule.FWMASK),
rule.get(RouteRule.FAMILY),
)

View File

@ -24,12 +24,16 @@ from libnmstate import iplib
from . import cmdlib
def ip_rule_exist_in_os(ip_from, ip_to, priority, table, fwmark, fwmask):
def ip_rule_exist_in_os(
ip_from, ip_to, priority, table, fwmark, fwmask, family
):
expected_rule = locals()
logging.debug("Checking ip rule for {}".format(expected_rule))
cmds = ["ip"]
if (ip_from and iplib.is_ipv6_address(ip_from)) or (
ip_to and iplib.is_ipv6_address(ip_to)
if (
(ip_from and iplib.is_ipv6_address(ip_from))
or (ip_to and iplib.is_ipv6_address(ip_to))
or (family and family == "ipv6")
):
cmds.append("-6")
if ip_from and "/" not in ip_from: