From 36b80cee72b98974d01241055a33e4a30094a9dc Mon Sep 17 00:00:00 2001 From: Emanuele Altomare Date: Thu, 20 Feb 2025 17:51:03 +0000 Subject: [PATCH 1/2] add 'iif' key to routing-policy entry It's now possible to specify the incoming interface for a routing policy entry. Example: network: version: 2 ethernets: if0: {} eth0: dhcp4: true routing-policy: - table: 101 iif: if0 Signed-off-by: Emanuele Altomare --- doc/netplan-yaml.md | 4 +++ src/netplan.c | 1 + src/networkd.c | 3 ++ src/nm.c | 2 ++ src/parse.c | 27 ++++++++++++++-- src/types-internal.h | 3 ++ src/types.c | 1 + src/util.c | 3 +- tests/generator/test_errors.py | 12 +++++++ tests/generator/test_routing.py | 56 +++++++++++++++++++++++++++++++++ 10 files changed, 109 insertions(+), 3 deletions(-) diff --git a/doc/netplan-yaml.md b/doc/netplan-yaml.md index b22755755..41a520a6f 100644 --- a/doc/netplan-yaml.md +++ b/doc/netplan-yaml.md @@ -865,6 +865,10 @@ network: > Match on traffic going to the specified destination. + - **`iif`** (scalar) + + > Set an incoming interface to match traffic for this policy rule. + - **`table`** (scalar) > The table number to match for the route. In some scenarios, it may be diff --git a/src/netplan.c b/src/netplan.c index d6ab84902..1fda2488a 100644 --- a/src/netplan.c +++ b/src/netplan.c @@ -641,6 +641,7 @@ write_routes(yaml_event_t* event, yaml_emitter_t* emitter, const NetplanNetDefin YAML_UINT_DEFAULT(def, event, emitter, "mark", r->fwmark, NETPLAN_IP_RULE_FW_MARK_UNSPEC); YAML_STRING(def, event, emitter, "from", r->from); YAML_STRING(def, event, emitter, "to", r->to); + YAML_STRING(def, event, emitter, "iif", r->iif); YAML_MAPPING_CLOSE(event, emitter); } YAML_SEQUENCE_CLOSE(event, emitter); diff --git a/src/networkd.c b/src/networkd.c index 982d18cb9..71683e6f2 100644 --- a/src/networkd.c +++ b/src/networkd.c @@ -749,6 +749,9 @@ write_ip_rule(NetplanIPRule* r, GString* s) g_string_append_printf(s, "From=%s\n", r->from); if (r->to) g_string_append_printf(s, "To=%s\n", r->to); + if (r->iif) + g_string_append_printf(s, "IncomingInterface=%s\n", r->iif); + if (r->table != NETPLAN_ROUTE_TABLE_UNSPEC) g_string_append_printf(s, "Table=%d\n", r->table); diff --git a/src/nm.c b/src/nm.c index 583fd2d62..8751f0b53 100644 --- a/src/nm.c +++ b/src/nm.c @@ -297,6 +297,8 @@ write_ip_rules_nm(const NetplanNetDefinition* def, GKeyFile *kf, gint family, GE 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->iif) + g_string_append_printf(tmp_val, " iif %s", cur_rule->iif); 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) diff --git a/src/parse.c b/src/parse.c index 64a9a809f..ba51500b2 100644 --- a/src/parse.c +++ b/src/parse.c @@ -386,6 +386,21 @@ handle_generic_str(NetplanParser* npp, yaml_node_t* node, void* entryptr, const return TRUE; } +/** + * Handler for setting a string ID field from a scalar node, inside a given struct + * @entryptr: pointer to the beginning of the to-be-modified data structure + * @data: offset into entryptr struct where the const char* field to write is + * located + */ +STATIC gboolean +handle_generic_id(NetplanParser* npp, yaml_node_t* node, void* entryptr, const void* data, GError** error) +{ + if (!assert_valid_id(npp, node, error)) + return FALSE; + + return handle_generic_str(npp, node, entryptr, data, error); +} + STATIC gboolean handle_special_macaddress_option(NetplanParser* npp, yaml_node_t* node, void* entryptr, const void* data, GError** error) { @@ -2048,6 +2063,12 @@ handle_ip_rule_ip(NetplanParser* npp, yaml_node_t* node, const void* data, GErro return TRUE; } +STATIC gboolean +handle_ip_rule_iif(NetplanParser* npp, yaml_node_t* node, const void* data, GError** error) +{ + return handle_generic_id(npp, node, npp->current.ip_rule, (void *) data, error); +} + STATIC gboolean handle_ip_rule_guint(NetplanParser* npp, yaml_node_t* node, const void* data, GError** error) { @@ -2295,6 +2316,7 @@ static const mapping_entry_handler ip_rules_handlers[] = { {"table", YAML_SCALAR_NODE, {.generic=handle_ip_rule_guint}, ip_rule_offset(table)}, {"to", YAML_SCALAR_NODE, {.generic=handle_ip_rule_ip}, ip_rule_offset(to)}, {"type-of-service", YAML_SCALAR_NODE, {.generic=handle_ip_rule_tos}, ip_rule_offset(tos)}, + {"iif", YAML_SCALAR_NODE, {.generic=handle_ip_rule_iif}, ip_rule_offset(iif)}, {NULL} }; @@ -2309,11 +2331,12 @@ handle_ip_rules(NetplanParser* npp, yaml_node_t* node, __unused const void* _, G reset_ip_rule(ip_rule); npp->current.ip_rule = ip_rule; + ret = process_mapping(npp, entry, NULL, ip_rules_handlers, NULL, error); npp->current.ip_rule = NULL; - if (ret && !ip_rule->from && !ip_rule->to) - ret = yaml_error(npp, node, error, "IP routing policy must include either a 'from' or 'to' IP"); + if (ret && !ip_rule->from && !ip_rule->to && !ip_rule->iif) + ret = yaml_error(npp, node, error, "IP routing policy must include at least one of the following fields: 'from', 'to', 'iif'"); if (!ret) { ip_rule_clear(&ip_rule); diff --git a/src/types-internal.h b/src/types-internal.h index f8c1df3df..f4db42c9b 100644 --- a/src/types-internal.h +++ b/src/types-internal.h @@ -168,6 +168,9 @@ typedef struct { guint fwmark; /* type-of-service: between 0 and 255 */ guint tos; + + /* incoming interface */ + char* iif; } NetplanIPRule; struct netplan_vxlan { diff --git a/src/types.c b/src/types.c index 7a1c20ed2..b69e13629 100644 --- a/src/types.c +++ b/src/types.c @@ -87,6 +87,7 @@ free_ip_rules(void* ptr) NetplanIPRule* rule = ptr; g_free(rule->to); g_free(rule->from); + g_free(rule->iif); g_free(rule); } diff --git a/src/util.c b/src/util.c index 1b2852dce..423c1e40a 100644 --- a/src/util.c +++ b/src/util.c @@ -1210,7 +1210,8 @@ is_route_rule_present(const NetplanNetDefinition* netdef, const NetplanIPRule* r entry->table == rule->table && entry->priority == rule->priority && entry->fwmark == rule->fwmark && - entry->tos == rule->tos + entry->tos == rule->tos && + g_strcmp0(entry->iif, rule->iif) == 0 ) return TRUE; } diff --git a/tests/generator/test_errors.py b/tests/generator/test_errors.py index d447c4920..1b8446da4 100644 --- a/tests/generator/test_errors.py +++ b/tests/generator/test_errors.py @@ -916,6 +916,18 @@ def test_device_ip_rule_invalid_address(self): - 192.168.14.2/24 - 2001:FFfe::1/64''', expect_fail=True) + def test_device_ip_rule_invalid_iif(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + routing-policy: + - from: 10.10.10.0/24 + iif: not valid iface name + addresses: + - 192.168.14.2/24 + - 2001:FFfe::1/64''', expect_fail=True) + def test_invalid_dhcp_identifier(self): self.generate('''network: version: 2 diff --git a/tests/generator/test_routing.py b/tests/generator/test_routing.py index 2612d2e96..be3a6cb1c 100644 --- a/tests/generator/test_routing.py +++ b/tests/generator/test_routing.py @@ -654,6 +654,31 @@ def test_ip_rule_tos(self): [RoutingPolicyRule] To=10.10.10.0/24 TypeOfService=250 +'''}) + + def test_ip_rule_iif(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: ["192.168.14.2/24"] + routing-policy: + - to: 10.10.10.0/24 + table: 100 + iif: if0 + ''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=ipv6 +Address=192.168.14.2/24 + +[RoutingPolicyRule] +To=10.10.10.0/24 +IncomingInterface=if0 +Table=100 '''}) def test_use_routes(self): @@ -1185,6 +1210,37 @@ def test_ip_rule_tos(self): address1=192.168.14.2/24 routing-rule1=priority 99 to 10.10.10.0/24 tos 250 +[ipv6] +method=ignore +'''}) + + def test_ip_rule_iif(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 + iif: if0 + ''') + + 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 iif if0 table 100 + [ipv6] method=ignore '''}) From 8d0ed9bb9792a9199ec8823494d9e412235f7165 Mon Sep 17 00:00:00 2001 From: Emanuele Altomare Date: Tue, 25 Feb 2025 21:45:26 +0100 Subject: [PATCH 2/2] add 'oif' key to routing-policy entry It's now possible to specify the outgoing interface for a routing policy entry. Example: network: version: 2 ethernets: if0: {} eth0: dhcp4: true routing-policy: - table: 101 oif: if0 Signed-off-by: Emanuele Altomare --- doc/netplan-yaml.md | 4 +++ src/netplan.c | 1 + src/networkd.c | 2 ++ src/nm.c | 2 ++ src/parse.c | 9 +++--- src/types-internal.h | 2 ++ src/types.c | 1 + src/util.c | 3 +- tests/generator/test_errors.py | 12 +++++++ tests/generator/test_routing.py | 56 +++++++++++++++++++++++++++++++++ 10 files changed, 87 insertions(+), 5 deletions(-) diff --git a/doc/netplan-yaml.md b/doc/netplan-yaml.md index 41a520a6f..ea2bfa356 100644 --- a/doc/netplan-yaml.md +++ b/doc/netplan-yaml.md @@ -869,6 +869,10 @@ network: > Set an incoming interface to match traffic for this policy rule. + - **`oif`** (scalar) + + > Set an outgoing interface to match traffic for this policy rule. + - **`table`** (scalar) > The table number to match for the route. In some scenarios, it may be diff --git a/src/netplan.c b/src/netplan.c index 1fda2488a..23ae82ba7 100644 --- a/src/netplan.c +++ b/src/netplan.c @@ -642,6 +642,7 @@ write_routes(yaml_event_t* event, yaml_emitter_t* emitter, const NetplanNetDefin YAML_STRING(def, event, emitter, "from", r->from); YAML_STRING(def, event, emitter, "to", r->to); YAML_STRING(def, event, emitter, "iif", r->iif); + YAML_STRING(def, event, emitter, "oif", r->oif); YAML_MAPPING_CLOSE(event, emitter); } YAML_SEQUENCE_CLOSE(event, emitter); diff --git a/src/networkd.c b/src/networkd.c index 71683e6f2..d65ccb9e3 100644 --- a/src/networkd.c +++ b/src/networkd.c @@ -751,6 +751,8 @@ write_ip_rule(NetplanIPRule* r, GString* s) g_string_append_printf(s, "To=%s\n", r->to); if (r->iif) g_string_append_printf(s, "IncomingInterface=%s\n", r->iif); + if (r->oif) + g_string_append_printf(s, "OutgoingInterface=%s\n", r->oif); if (r->table != NETPLAN_ROUTE_TABLE_UNSPEC) diff --git a/src/nm.c b/src/nm.c index 8751f0b53..82df272f5 100644 --- a/src/nm.c +++ b/src/nm.c @@ -299,6 +299,8 @@ write_ip_rules_nm(const NetplanNetDefinition* def, GKeyFile *kf, gint family, GE g_string_append_printf(tmp_val, " to %s", cur_rule->to); if (cur_rule->iif) g_string_append_printf(tmp_val, " iif %s", cur_rule->iif); + if (cur_rule->oif) + g_string_append_printf(tmp_val, " oif %s", cur_rule->oif); 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) diff --git a/src/parse.c b/src/parse.c index ba51500b2..3d8e897dd 100644 --- a/src/parse.c +++ b/src/parse.c @@ -2064,7 +2064,7 @@ handle_ip_rule_ip(NetplanParser* npp, yaml_node_t* node, const void* data, GErro } STATIC gboolean -handle_ip_rule_iif(NetplanParser* npp, yaml_node_t* node, const void* data, GError** error) +handle_ip_rule_if(NetplanParser* npp, yaml_node_t* node, const void* data, GError** error) { return handle_generic_id(npp, node, npp->current.ip_rule, (void *) data, error); } @@ -2316,7 +2316,8 @@ static const mapping_entry_handler ip_rules_handlers[] = { {"table", YAML_SCALAR_NODE, {.generic=handle_ip_rule_guint}, ip_rule_offset(table)}, {"to", YAML_SCALAR_NODE, {.generic=handle_ip_rule_ip}, ip_rule_offset(to)}, {"type-of-service", YAML_SCALAR_NODE, {.generic=handle_ip_rule_tos}, ip_rule_offset(tos)}, - {"iif", YAML_SCALAR_NODE, {.generic=handle_ip_rule_iif}, ip_rule_offset(iif)}, + {"iif", YAML_SCALAR_NODE, {.generic=handle_ip_rule_if}, ip_rule_offset(iif)}, + {"oif", YAML_SCALAR_NODE, {.generic=handle_ip_rule_if}, ip_rule_offset(oif)}, {NULL} }; @@ -2335,8 +2336,8 @@ handle_ip_rules(NetplanParser* npp, yaml_node_t* node, __unused const void* _, G ret = process_mapping(npp, entry, NULL, ip_rules_handlers, NULL, error); npp->current.ip_rule = NULL; - if (ret && !ip_rule->from && !ip_rule->to && !ip_rule->iif) - ret = yaml_error(npp, node, error, "IP routing policy must include at least one of the following fields: 'from', 'to', 'iif'"); + if (ret && !ip_rule->from && !ip_rule->to && !ip_rule->iif && !ip_rule->oif) + ret = yaml_error(npp, node, error, "IP routing policy must include at least one of the following fields: 'from', 'to', 'iif', 'oif'"); if (!ret) { ip_rule_clear(&ip_rule); diff --git a/src/types-internal.h b/src/types-internal.h index f4db42c9b..c386adf86 100644 --- a/src/types-internal.h +++ b/src/types-internal.h @@ -171,6 +171,8 @@ typedef struct { /* incoming interface */ char* iif; + /* outgoing interface */ + char* oif; } NetplanIPRule; struct netplan_vxlan { diff --git a/src/types.c b/src/types.c index b69e13629..9a8d51887 100644 --- a/src/types.c +++ b/src/types.c @@ -88,6 +88,7 @@ free_ip_rules(void* ptr) g_free(rule->to); g_free(rule->from); g_free(rule->iif); + g_free(rule->oif); g_free(rule); } diff --git a/src/util.c b/src/util.c index 423c1e40a..08712ce4b 100644 --- a/src/util.c +++ b/src/util.c @@ -1211,7 +1211,8 @@ is_route_rule_present(const NetplanNetDefinition* netdef, const NetplanIPRule* r entry->priority == rule->priority && entry->fwmark == rule->fwmark && entry->tos == rule->tos && - g_strcmp0(entry->iif, rule->iif) == 0 + g_strcmp0(entry->iif, rule->iif) == 0 && + g_strcmp0(entry->oif, rule->oif) == 0 ) return TRUE; } diff --git a/tests/generator/test_errors.py b/tests/generator/test_errors.py index 1b8446da4..1c86808e2 100644 --- a/tests/generator/test_errors.py +++ b/tests/generator/test_errors.py @@ -928,6 +928,18 @@ def test_device_ip_rule_invalid_iif(self): - 192.168.14.2/24 - 2001:FFfe::1/64''', expect_fail=True) + def test_device_ip_rule_invalid_oif(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + routing-policy: + - from: 10.10.10.0/24 + oif: not valid iface name + addresses: + - 192.168.14.2/24 + - 2001:FFfe::1/64''', expect_fail=True) + def test_invalid_dhcp_identifier(self): self.generate('''network: version: 2 diff --git a/tests/generator/test_routing.py b/tests/generator/test_routing.py index be3a6cb1c..a157c3363 100644 --- a/tests/generator/test_routing.py +++ b/tests/generator/test_routing.py @@ -679,6 +679,31 @@ def test_ip_rule_iif(self): To=10.10.10.0/24 IncomingInterface=if0 Table=100 +'''}) + + def test_ip_rule_oif(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: ["192.168.14.2/24"] + routing-policy: + - to: 10.10.10.0/24 + table: 100 + oif: if0 + ''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=ipv6 +Address=192.168.14.2/24 + +[RoutingPolicyRule] +To=10.10.10.0/24 +OutgoingInterface=if0 +Table=100 '''}) def test_use_routes(self): @@ -1241,6 +1266,37 @@ def test_ip_rule_iif(self): address1=192.168.14.2/24 routing-rule1=priority 99 to 10.10.10.0/24 iif if0 table 100 +[ipv6] +method=ignore +'''}) + + def test_ip_rule_oif(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 + oif: if0 + ''') + + 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 oif if0 table 100 + [ipv6] method=ignore '''})