Skip to content

Commit

Permalink
route: Add support for route source
Browse files Browse the repository at this point in the history
In a scenario where you have a machine with multiple public IP
addresses, typically due to a multi-WAN setup, the src parameter in the
context of routes allows you to specify which source IP address should
be used when sending packets via a specific route. This is crucial when
you want to ensure that outbound traffic uses a specific IP address tied
to a particular network interface, especially when dealing with multiple
WAN connections.

Adding support for the src parameter in routes results in a more
powerful and flexible network configuration capability, especially
important in environments with multiple network interfaces or multiple
IP addresses, it provides better control over traffic routing.

The following is the example for specifying the route src in Nmstate:

```
---
interfaces:
  - name: eth1
    type: ethernet
    state: up
    ipv4:
      address:
        - ip: 192.0.2.251
          prefix-length: 24
        - ip: 192.0.2.252
          prefix-length: 24
      dhcp: false
      enabled: true
routes:
  config:
    - destination: 198.51.100.0/24
      source: 192.0.2.251
      next-hop-address: 192.0.2.1
      next-hop-interface: eth1
      table-id: 254
      metric: 150
```

Resolves: https://issues.redhat.com/browse/RHEL-56258

Signed-off-by: Wen Liang <[email protected]>
  • Loading branch information
liangwen12year authored and cathay4t committed Sep 12, 2024
1 parent 631c6a0 commit d4487de
Show file tree
Hide file tree
Showing 9 changed files with 78 additions and 2 deletions.
1 change: 1 addition & 0 deletions examples/eth1_add_route.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ interfaces:
routes:
config:
- destination: 198.51.100.0/24
source: 192.0.2.251
metric: 150
next-hop-address: 192.0.2.1
next-hop-interface: eth1
Expand Down
2 changes: 2 additions & 0 deletions rust/src/lib/nispor/route.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,10 +194,12 @@ fn np_route_to_nmstate(np_route: &nispor::Route) -> RouteEntry {
}
};

let source = np_route.prefered_src.as_ref().map(|src| src.to_string());
let mut route_entry = RouteEntry::new();
route_entry.destination = destination;
route_entry.next_hop_iface = np_route.oif.as_ref().cloned();
route_entry.next_hop_addr = next_hop_addr;
route_entry.source = source;
route_entry.metric = np_route.metric.map(i64::from);
route_entry.table_id = Some(np_route.table);
// according to `man ip-route`, cwnd is useless without the lock flag, so
Expand Down
8 changes: 8 additions & 0 deletions rust/src/lib/nm/nm_dbus/connection/route.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pub struct NmIpRoute {
pub dest: Option<String>,
pub prefix: Option<u32>,
pub next_hop: Option<String>,
pub src: Option<String>,
pub table: Option<u32>,
pub metric: Option<u32>,
pub weight: Option<u32>,
Expand All @@ -35,6 +36,7 @@ impl TryFrom<DbusDictionary> for NmIpRoute {
dest: _from_map!(v, "dest", String::try_from)?,
prefix: _from_map!(v, "prefix", u32::try_from)?,
next_hop: _from_map!(v, "next-hop", String::try_from)?,
src: _from_map!(v, "src", String::try_from)?,
table: _from_map!(v, "table", u32::try_from)?,
metric: _from_map!(v, "metric", u32::try_from)?,
weight,
Expand Down Expand Up @@ -70,6 +72,12 @@ impl NmIpRoute {
zvariant::Value::new(zvariant::Value::new(v)),
)?;
}
if let Some(v) = &self.src {
ret.append(
zvariant::Value::new("src"),
zvariant::Value::new(zvariant::Value::new(v)),
)?;
}
if let Some(v) = &self.table {
ret.append(
zvariant::Value::new("table"),
Expand Down
3 changes: 3 additions & 0 deletions rust/src/lib/nm/nm_dbus/gen_conf/route.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ impl NmIpRoute {
if let Some(lock_cwnd) = self.lock_cwnd {
write!(opt_string, ",lock-cwnd={}", lock_cwnd).ok();
}
if let Some(src) = self.src.as_ref() {
write!(opt_string, ",src={}", src).ok();
}
ret.insert("options".to_string(), opt_string);
}
ret
Expand Down
1 change: 1 addition & 0 deletions rust/src/lib/nm/settings/route.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ pub(crate) fn gen_nm_ip_routes(
None => None,
};
nm_route.next_hop = route.next_hop_addr.as_ref().cloned();
nm_route.src = route.source.as_ref().cloned();
if let Some(weight) = route.weight {
nm_route.weight = Some(weight as u32);
}
Expand Down
34 changes: 32 additions & 2 deletions rust/src/lib/route.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,10 @@ pub struct RouteEntry {
/// Congestion window clamp
#[serde(skip_serializing_if = "Option::is_none")]
pub cwnd: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
/// Route source defines which IP address should be used as the source
/// for packets routed via a specific route
pub source: Option<String>,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
Expand Down Expand Up @@ -271,13 +275,16 @@ impl RouteEntry {
if self.cwnd.is_some() && self.cwnd != other.cwnd {
return false;
}
if self.source.as_ref().is_some() && self.source != other.source {
return false;
}
true
}

// Return tuple of (no_absent, is_ipv4, table_id, next_hop_iface,
// destination, next_hop_addr, weight, cwnd)
// destination, next_hop_addr, source, weight, cwnd)
// Metric is ignored
fn sort_key(&self) -> (bool, bool, u32, &str, &str, &str, u16, u32) {
fn sort_key(&self) -> (bool, bool, u32, &str, &str, &str, &str, u16, u32) {
(
!matches!(self.state, Some(RouteState::Absent)),
!self
Expand All @@ -291,6 +298,7 @@ impl RouteEntry {
.unwrap_or(LOOPBACK_IFACE_NAME),
self.destination.as_deref().unwrap_or(""),
self.next_hop_addr.as_deref().unwrap_or(""),
self.source.as_deref().unwrap_or(""),
self.weight.unwrap_or_default(),
self.cwnd.unwrap_or_default(),
)
Expand Down Expand Up @@ -323,6 +331,25 @@ impl RouteEntry {
self.next_hop_addr = Some(new_via);
}
}
if let Some(src) = self.source.as_ref() {
let new_src = format!(
"{}",
src.parse::<std::net::IpAddr>().map_err(|e| {
NmstateError::new(
ErrorKind::InvalidArgument,
format!("Failed to parse IP address '{}': {}", src, e),
)
})?
);
if src != &new_src {
log::info!(
"Route source address {} sanitized to {}",
src,
new_src
);
self.source = Some(new_src);
}
}
if let Some(weight) = self.weight {
if !(1..=256).contains(&weight) {
return Err(NmstateError::new(
Expand Down Expand Up @@ -410,6 +437,9 @@ impl std::fmt::Display for RouteEntry {
if let Some(v) = self.next_hop_addr.as_ref() {
props.push(format!("next-hop-address: {v}"));
}
if let Some(v) = self.source.as_ref() {
props.push(format!("source: {v}"));
}
if let Some(v) = self.metric.as_ref() {
props.push(format!("metric: {v}"));
}
Expand Down
5 changes: 5 additions & 0 deletions rust/src/lib/unit_tests/route.rs
Original file line number Diff line number Diff line change
Expand Up @@ -260,12 +260,17 @@ fn test_route_sanitize_ipv6_host_not_compact() {
r#"
destination: "2001:db8:1:0000:000::1"
next-hop-address: "2001:db8:a:0000:000::1"
source: "2001:0db8:85a3:0000:0000:8a2e:0370:7001"
"#,
)
.unwrap();
route.sanitize().unwrap();
assert_eq!(route.destination, Some("2001:db8:1::1/128".to_string()));
assert_eq!(route.next_hop_addr, Some("2001:db8:a::1".to_string()));
assert_eq!(
route.source,
Some("2001:db8:85a3::8a2e:370:7001".to_string())
);
}

#[test]
Expand Down
1 change: 1 addition & 0 deletions rust/src/python/libnmstate/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class Route:
DESTINATION = "destination"
NEXT_HOP_INTERFACE = "next-hop-interface"
NEXT_HOP_ADDRESS = "next-hop-address"
SOURCE = "source"
METRIC = "metric"
WEIGHT = "weight"
ROUTETYPE = "route-type"
Expand Down
25 changes: 25 additions & 0 deletions tests/integration/route_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,31 @@ def test_add_gateway(eth1_up):
assert_routes(routes, cur_state)


def test_add_static_route_with_route_src(eth1_up):
routes = [
{
Route.DESTINATION: IPV4_TEST_NET1,
Route.SOURCE: IPV4_ADDRESS1,
Route.NEXT_HOP_INTERFACE: "eth1",
Route.NEXT_HOP_ADDRESS: "192.0.2.1",
},
{
Route.DESTINATION: IPV6_TEST_NET1,
Route.SOURCE: IPV6_ADDRESS1,
Route.NEXT_HOP_INTERFACE: "eth1",
Route.NEXT_HOP_ADDRESS: IPV6_GATEWAY1,
},
]
libnmstate.apply(
{
Interface.KEY: [ETH1_INTERFACE_STATE],
Route.KEY: {Route.CONFIG: routes},
}
)
cur_state = libnmstate.show()
assert_routes(routes, cur_state)


def test_add_route_without_metric(eth1_up):
routes = _get_ipv4_test_routes() + _get_ipv6_test_routes()
for route in routes:
Expand Down

0 comments on commit d4487de

Please sign in to comment.