Skip to content

Commit

Permalink
networkmanager: add support for "routing-policy"
Browse files Browse the repository at this point in the history
NetworkManager supports ip rules via the "routing-ruleX" field.  Let's
teach the networkmanager backend to emit "routing-ruleX" entries for
each "routing-policy" entry in the netplan yaml.

Update the tests for the new behavior and add additional tests to
exercise the new code paths.
  • Loading branch information
drafnel committed Nov 13, 2024
1 parent cf4fb22 commit cc5d82d
Show file tree
Hide file tree
Showing 5 changed files with 201 additions and 5 deletions.
3 changes: 3 additions & 0 deletions doc/netplan-yaml.md
Original file line number Diff line number Diff line change
Expand Up @@ -879,6 +879,9 @@ network:
> Specify a priority for the routing policy rule, to influence the order
> in which routing rules are processed. A higher number means lower
> priority: rules are processed in order by increasing priority number.
> Specifying an explicit, unique, priority for each routing policy rule
> is strongly recommended and is mandatory on the `NetworkManager`
> back-end.

- **`mark`** (scalar)

Expand Down
65 changes: 65 additions & 0 deletions src/nm.c
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,64 @@ write_routes_nm(const NetplanNetDefinition* def, GKeyFile *kf, gint family, GErr
j++;
}
}

return TRUE;
}

STATIC gboolean
write_ip_rules_nm(const NetplanNetDefinition* def, GKeyFile *kf, gint family, GError** error)
{
const gchar* group = NULL;
gchar* tmp_key = NULL;
GString* tmp_val = NULL;

if (family == AF_INET)
group = "ipv4";
else if (family == AF_INET6)
group = "ipv6";
g_assert(group != NULL);

if (def->ip_rules != NULL) {
for (unsigned i = 0, j = 1; i < def->ip_rules->len; ++i) {
const NetplanIPRule *cur_rule = g_array_index(def->ip_rules, NetplanIPRule*, i);

if (cur_rule->family != family)
continue;

/* NetworkManager requires that priority be specified. This is
* also in-line with the iproute2 guidance that "Each rule should
* have an explicitly set unique priority value"[1].
* [1]http://www.policyrouting.org/iproute2.doc.html#ss9.6.1 */
if (cur_rule->priority == NETPLAN_IP_RULE_PRIO_UNSPEC) {
g_set_error(error, NETPLAN_BACKEND_ERROR, NETPLAN_ERROR_UNSUPPORTED,
"ERROR: %s: The priority setting is mandatory for NetworkManager routing-policy\n", def->id);
return FALSE;
}

tmp_key = g_strdup_printf("routing-rule%u", j);
tmp_val = g_string_sized_new(200);

g_string_printf(tmp_val, "priority %u", cur_rule->priority);

if (cur_rule->from)
g_string_append_printf(tmp_val, " from %s", cur_rule->from);
if (cur_rule->to)
g_string_append_printf(tmp_val, " to %s", cur_rule->to);
if (cur_rule->tos != NETPLAN_IP_RULE_TOS_UNSPEC)
g_string_append_printf(tmp_val, " tos %u", cur_rule->tos);
if (cur_rule->fwmark != NETPLAN_IP_RULE_FW_MARK_UNSPEC)
g_string_append_printf(tmp_val, " fwmark %u", cur_rule->fwmark);
if (cur_rule->table != NETPLAN_ROUTE_TABLE_UNSPEC)
g_string_append_printf(tmp_val, " table %u", cur_rule->table);

g_key_file_set_string(kf, group, tmp_key, tmp_val->str);
g_free(tmp_key);
g_string_free(tmp_val, TRUE);

j++;
}
}

return TRUE;
}

Expand Down Expand Up @@ -716,6 +774,8 @@ write_nm_conf_access_point(const NetplanNetDefinition* def, const char* rootdir,
g_key_file_set_uint64(kf, "vrf", "table", def->vrf_table);
if (!write_routes_nm(def, kf, AF_INET, error) || !write_routes_nm(def, kf, AF_INET6, error))
return FALSE;
if (!write_ip_rules_nm(def, kf, AF_INET, error) || !write_ip_rules_nm(def, kf, AF_INET6, error))
return FALSE;
}

if (def->type == NETPLAN_DEF_TYPE_VETH && def->veth_peer_link) {
Expand Down Expand Up @@ -871,6 +931,8 @@ write_nm_conf_access_point(const NetplanNetDefinition* def, const char* rootdir,
write_search_domains(def, "ipv4", kf);
if (!write_routes_nm(def, kf, AF_INET, error))
return FALSE;
if (!write_ip_rules_nm(def, kf, AF_INET, error))
return FALSE;
}

if (!def->dhcp4_overrides.use_routes) {
Expand Down Expand Up @@ -917,6 +979,9 @@ write_nm_conf_access_point(const NetplanNetDefinition* def, const char* rootdir,
if (!write_routes_nm(def, kf, AF_INET6, error))
return FALSE;

if (!write_ip_rules_nm(def, kf, AF_INET6, error))
return FALSE;

if (!def->dhcp6_overrides.use_routes) {
g_key_file_set_boolean(kf, "ipv6", "ignore-auto-routes", TRUE);
g_key_file_set_boolean(kf, "ipv6", "never-default", TRUE);
Expand Down
116 changes: 116 additions & 0 deletions tests/generator/test_routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -1071,6 +1071,122 @@ def test_route_v6_default(self):
address1=2001:dead:beef::2/64
ip6-privacy=0
route1=::/0,2001:beef:beef::1
'''})

def test_ip_rule_missing_priority_fails_ipv4(self):
err = self.generate('''network:
version: 2
renderer: NetworkManager
ethernets:
engreen:
addresses: ["192.168.14.2/24"]
routing-policy:
- to: 10.10.10.0/24
table: 100
''', expect_fail=True)
self.assertIn("ERROR: engreen: The priority setting is mandatory for NetworkManager routing-policy", err)

def test_ip_rule_missing_priority_fails_ipv6(self):
err = self.generate('''network:
version: 2
renderer: NetworkManager
ethernets:
engreen:
addresses: ["2001:FFfe::1/64"]
routing-policy:
- to: 2001:FFfe::1/64
table: 100
''', expect_fail=True)
self.assertIn("ERROR: engreen: The priority setting is mandatory for NetworkManager routing-policy", err)

def test_ip_rule_table(self):
self.generate('''network:
version: 2
renderer: NetworkManager
ethernets:
engreen:
addresses: ["192.168.14.2/24"]
routing-policy:
- to: 10.10.10.0/24
table: 100
priority: 99
''')

self.assert_nm({'engreen': '''[connection]
id=netplan-engreen
type=ethernet
interface-name=engreen
[ethernet]
wake-on-lan=0
[ipv4]
method=manual
address1=192.168.14.2/24
routing-rule1=priority 99 to 10.10.10.0/24 table 100
[ipv6]
method=ignore
'''})

def test_ip_rule_fwmark(self):
self.generate('''network:
version: 2
renderer: NetworkManager
ethernets:
engreen:
addresses: ["192.168.14.2/24"]
routing-policy:
- from: 10.10.10.0/24
mark: 50
priority: 99
''')

self.assert_nm({'engreen': '''[connection]
id=netplan-engreen
type=ethernet
interface-name=engreen
[ethernet]
wake-on-lan=0
[ipv4]
method=manual
address1=192.168.14.2/24
routing-rule1=priority 99 from 10.10.10.0/24 fwmark 50
[ipv6]
method=ignore
'''})

def test_ip_rule_tos(self):
self.generate('''network:
version: 2
renderer: NetworkManager
ethernets:
engreen:
addresses: ["192.168.14.2/24"]
routing-policy:
- to: 10.10.10.0/24
type-of-service: 250
priority: 99
''')

self.assert_nm({'engreen': '''[connection]
id=netplan-engreen
type=ethernet
interface-name=engreen
[ethernet]
wake-on-lan=0
[ipv4]
method=manual
address1=192.168.14.2/24
routing-rule1=priority 99 to 10.10.10.0/24 tos 250
[ipv6]
method=ignore
'''})

def test_routes_mixed(self):
Expand Down
15 changes: 14 additions & 1 deletion tests/generator/test_vrfs.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ def test_vrf_set_table(self):
- to: default
via: 1.2.3.4
routing-policy:
- from: 2.3.4.5''')
- from: 2.3.4.5
priority: 99''')

self.assert_nm({'eth0': '''[connection]
id=netplan-eth0
Expand Down Expand Up @@ -66,6 +67,7 @@ def test_vrf_set_table(self):
[ipv4]
route1=0.0.0.0/0,1.2.3.4
route1_options=table=1005
routing-rule1=priority 99 from 2.3.4.5 table 1005
method=link-local
[ipv6]
Expand Down Expand Up @@ -224,3 +226,14 @@ def test_vrf_without_routes(self):
Table=1005
''',
'vrf1005.netdev': ND_VRF % ('vrf1005', 1005)})

def test_vrf_routing_policy_missing_priority_nm(self):
err = self.generate('''network:
version: 2
renderer: NetworkManager
vrfs:
vrf1005:
table: 1005
routing-policy:
- from: 2.3.4.5''', expect_fail=True)
self.assertIn("ERROR: vrf1005: The priority setting is mandatory for NetworkManager routing-policy", err)
7 changes: 3 additions & 4 deletions tests/integration/routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,7 @@ def test_vrf_basic(self):
via: 10.10.10.1
routing-policy:
- from: 10.10.10.42
priority: 99
''' % {'r': self.backend, 'ec': self.dev_e_client})
self.generate_and_settle([self.dev_e_client])
self.assert_iface_up(self.dev_e_client, ['inet 10.10.10.22', 'master vrf0']) # wokeignore:rule=master
Expand All @@ -332,10 +333,8 @@ def test_vrf_basic(self):
self.assertIn('11.11.11.0/24 via 10.10.10.2 dev {}'.format(self.dev_e_client), out)

# verify routing policy was setup correctly to the VRF's table
# 'routing-policy' is not supported on NetworkManager
if self.backend == 'networkd':
out = subprocess.check_output(['ip', 'rule', 'show'], text=True)
self.assertIn('from 10.10.10.42 lookup 1000', out)
out = subprocess.check_output(['ip', 'rule', 'show'], text=True)
self.assertIn('from 10.10.10.42 lookup 1000', out)

def test_route_advmss_v6(self):
self.setup_eth(None)
Expand Down

0 comments on commit cc5d82d

Please sign in to comment.