Skip to content

Commit

Permalink
feat: Add the support for the optional route source parameter in nm p…
Browse files Browse the repository at this point in the history
…rovider

Enhancement: Add the optional route source parameter for the nm provider

Reason: 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.

Result: 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.

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

Signed-off-by: Wen Liang <[email protected]>
  • Loading branch information
liangwen12year committed Aug 5, 2024
1 parent 098e5e1 commit 8195188
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 4 deletions.
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -596,10 +596,11 @@ The IP configuration supports the following options:

Static route configuration can be specified via a list of routes given in the
`route` option. The default value is an empty list. Each route is a dictionary with
the following entries: `gateway`, `metric`, `network`, `prefix`, `table` and `type`.
`network` and `prefix` specify the destination network. `table` supports both the
numeric table and named table. In order to specify the named table, the users have to
ensure the named table is properly defined in `/etc/iproute2/rt_tables` or
the following entries: `gateway`, `metric`, `network`, `prefix`, `src`, `table` and
`type`. `network` and `prefix` specify the destination network. `src` specifies the
source IP address for a route. `table` supports both the numeric table and named
table. In order to specify the named table, the users have to ensure the named table
is properly defined in `/etc/iproute2/rt_tables` or
`/etc/iproute2/rt_tables.d/*.conf`. The optional `type` key supports the values
`blackhole`, `prohibit`, and `unreachable`.
See [man 8 ip-route](https://man7.org/linux/man-pages/man8/ip-route.8.html#DESCRIPTION)
Expand Down
4 changes: 4 additions & 0 deletions library/network_connections.py
Original file line number Diff line number Diff line change
Expand Up @@ -1263,6 +1263,10 @@ def connection_create(self, connections, idx, connection_current=None):
NM.IPRoute.set_attribute(
new_route, "table", Util.GLib().Variant.new_uint32(r["table"])
)
if r["src"]:
NM.IPRoute.set_attribute(
new_route, "src", Util.GLib().Variant.new_string(r["src"])
)

if r["family"] == socket.AF_INET:
s_ip4.add_route(new_route)
Expand Down
19 changes: 19 additions & 0 deletions module_utils/network_lsr/argument_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -680,6 +680,9 @@ def __init__(self, name, family=None, required=False):
enum_values=["blackhole", "prohibit", "unreachable"],
),
ArgValidatorRouteTable("table"),
ArgValidatorIP(
"src", family=family, default_value=None, plain_address=False
),
],
default_value=None,
)
Expand Down Expand Up @@ -716,6 +719,16 @@ def _validate_post(self, value, name, result):
elif not Util.addr_family_valid_prefix(family, prefix):
raise ValidationError(name, "invalid prefix %s in '%s'" % (prefix, value))

src = result["src"]
if src is not None:
if family != src["family"]:
raise ValidationError(
name,
"conflicting address family between network and src "
"address {0}".format(src["address"]),
)
result["src"] = src["address"]

return result


Expand Down Expand Up @@ -2627,6 +2640,12 @@ def _ipv6_is_not_configured(connection):
idx,
"type is not supported by initscripts",
)
if route["src"] is not None:
raise ValidationError.from_connection(
idx,
"configuring the route source is not supported by initscripts",
)

if connection["ip"]["routing_rule"]:
if mode == self.VALIDATE_ONE_MODE_INITSCRIPTS:
raise ValidationError.from_connection(
Expand Down
18 changes: 18 additions & 0 deletions tests/playbooks/tests_route_table.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@
gateway: 198.51.100.6
metric: 4
table: 30200
- network: 192.0.2.64
prefix: 26
gateway: 198.51.100.8
metric: 50
table: 30200
src: 198.51.100.3

- name: Get the routes from the route table 30200
command: ip route show table 30200
Expand All @@ -65,6 +71,9 @@
that:
- route_table_30200.stdout is search("198.51.100.64/26 via
198.51.100.6 dev ethtest0 proto static metric 4")
- route_table_30200.stdout is search("192.0.2.64/26 via
198.51.100.8 dev ethtest0 proto static src 198.51.100.3
metric 50")
msg: "the route table 30200 does not exist or does not contain the
specified route"

Expand Down Expand Up @@ -111,6 +120,12 @@
gateway: 198.51.100.6
metric: 4
table: custom
- network: 192.0.2.64
prefix: 26
gateway: 198.51.100.8
metric: 50
table: custom
src: 198.51.100.3

- name: Get the routes from the named route table 'custom'
command: ip route show table custom
Expand All @@ -126,6 +141,9 @@
198.51.100.1 dev ethtest0 proto static metric 2")
- route_table_custom.stdout is search("198.51.100.64/26 via
198.51.100.6 dev ethtest0 proto static metric 4")
- route_table_custom.stdout is search("192.0.2.64/26 via
198.51.100.8 dev ethtest0 proto static src 198.51.100.3
metric 50")
msg: "the named route table 'custom' does not exist or does not contain
the specified route"

Expand Down
38 changes: 38 additions & 0 deletions tests/unit/test_network_connections.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ def assert_nm_connection_routes_expected(self, connection, route_list_expected):
"metric": int(r.get_metric()),
"type": r.get_attribute("type"),
"table": r.get_attribute("table"),
"src": r.get_attribute("src"),
}
for r in route_list_new
]
Expand Down Expand Up @@ -295,6 +296,12 @@ def do_connections_validate_nm(self, input_connections, **kwargs):
"table",
Util.GLib().Variant.new_uint32(r["table"]),
)
if r["src"]:
NM.IPRoute.set_attribute(
new_route,
"src",
Util.GLib().Variant.new_uint32(r["src"]),
)
if r["family"] == socket.AF_INET:
s4.add_route(new_route)
else:
Expand Down Expand Up @@ -1144,6 +1151,7 @@ def test_routes(self):
"metric": -1,
"type": None,
"table": None,
"src": None,
}
],
"routing_rule": [],
Expand Down Expand Up @@ -1485,6 +1493,7 @@ def test_vlan(self):
"metric": -1,
"type": None,
"table": None,
"src": None,
}
],
"routing_rule": [],
Expand Down Expand Up @@ -1635,6 +1644,7 @@ def test_macvlan(self):
"metric": -1,
"type": None,
"table": None,
"src": None,
}
],
"routing_rule": [],
Expand Down Expand Up @@ -1698,6 +1708,7 @@ def test_macvlan(self):
"metric": -1,
"type": None,
"table": None,
"src": None,
}
],
"routing_rule": [],
Expand Down Expand Up @@ -2661,6 +2672,7 @@ def test_route_metric_prefix(self):
"metric": 545,
"type": None,
"table": None,
"src": None,
},
{
"family": socket.AF_INET,
Expand All @@ -2670,6 +2682,7 @@ def test_route_metric_prefix(self):
"metric": -1,
"type": None,
"table": None,
"src": None,
},
],
"routing_rule": [],
Expand Down Expand Up @@ -2767,6 +2780,7 @@ def test_route_v6(self):
"metric": 545,
"type": None,
"table": None,
"src": None,
},
{
"family": socket.AF_INET,
Expand All @@ -2776,6 +2790,7 @@ def test_route_v6(self):
"metric": -1,
"type": None,
"table": None,
"src": None,
},
{
"family": socket.AF_INET6,
Expand All @@ -2785,6 +2800,7 @@ def test_route_v6(self):
"metric": -1,
"type": None,
"table": None,
"src": None,
},
],
"routing_rule": [],
Expand Down Expand Up @@ -2923,6 +2939,7 @@ def test_route_without_interface_name(self):
"metric": 545,
"type": None,
"table": None,
"src": None,
},
{
"family": socket.AF_INET,
Expand All @@ -2932,6 +2949,7 @@ def test_route_without_interface_name(self):
"metric": -1,
"type": None,
"table": None,
"src": None,
},
{
"family": socket.AF_INET6,
Expand All @@ -2941,6 +2959,7 @@ def test_route_without_interface_name(self):
"metric": -1,
"type": None,
"table": None,
"src": None,
},
],
"routing_rule": [],
Expand Down Expand Up @@ -5001,6 +5020,25 @@ def test_type_route_with_gateway(self):
self.test_connections,
)

def test_route_with_source_address(self):
"""
Test setting the route with src address specified
"""
self.test_connections[0]["ip"]["route"][0]["src"] = "2001:db8::2"
self.assertRaisesRegex(
ValidationError,
"conflicting address family between network and src "
"address {0}".format(
self.test_connections[0]["ip"]["route"][0]["src"],
),
self.validator.validate,
self.test_connections,
)

self.test_connections[0]["ip"]["route"][0]["src"] = "198.51.100.3"
result = self.validator.validate(self.test_connections)
self.assertEqual(result[0]["ip"]["route"][0]["src"], "198.51.100.3")


class TestValidatorRoutingRules(Python26CompatTestCase):
def setUp(self):
Expand Down

0 comments on commit 8195188

Please sign in to comment.