From e07a7333f5365743c64cefd54bcfa97f58f11855 Mon Sep 17 00:00:00 2001 From: Romain FIHUE Date: Tue, 5 Aug 2025 20:05:13 +0200 Subject: [PATCH 1/8] routing-policy: Add a "type" scalar to routing-policy specification One can now manage the type of the rule (which is unicast by default). This allows to manage exotic - but used in the wild - rule like blackholes. --- doc/netplan-yaml.md | 5 +++++ src/netplan.c | 1 + src/parse.c | 26 ++++++++++++++++++++++++-- src/types-internal.h | 4 ++++ src/types.c | 1 + 5 files changed, 35 insertions(+), 2 deletions(-) diff --git a/doc/netplan-yaml.md b/doc/netplan-yaml.md index b22755755..4c26e4087 100644 --- a/doc/netplan-yaml.md +++ b/doc/netplan-yaml.md @@ -894,6 +894,11 @@ network: > Match this policy rule based on the type of service number applied to > the traffic. + - **`type`** (scalar) + + > The type of the rule. Valid options are `unicast` (default), + > `blackhole`, `unreachable`, `prohibit` and `nat`. + (yaml-auth)= ## Authentication diff --git a/src/netplan.c b/src/netplan.c index d6ab84902..2a785570a 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, "type", r->type); YAML_MAPPING_CLOSE(event, emitter); } YAML_SEQUENCE_CLOSE(event, emitter); diff --git a/src/parse.c b/src/parse.c index 64a9a809f..3ea14af9f 100644 --- a/src/parse.c +++ b/src/parse.c @@ -2072,6 +2072,24 @@ handle_ip_rule_tos(NetplanParser* npp, yaml_node_t* node, const void* data, GErr return ret; } +STATIC gboolean +handle_ip_rule_type(NetplanParser* npp, yaml_node_t* node, __unused const void* data, GError** error) +{ + NetplanIPRule* ip_rule = npp->current.ip_rule; + if (ip_rule->type) + g_free(ip_rule->type); + ip_rule->type = g_strdup(scalar(node)); + + if ( g_ascii_strcasecmp(ip_rule->type, "unicast") == 0 + || g_ascii_strcasecmp(ip_rule->type, "blackhole") == 0 + || g_ascii_strcasecmp(ip_rule->type, "unreachable") == 0 + || g_ascii_strcasecmp(ip_rule->type, "nat") == 0 + || g_ascii_strcasecmp(ip_rule->type, "prohibit") == 0) + return TRUE; + + return yaml_error(npp, node, error, "invalid rule type '%s'", ip_rule->type); +} + /**************************************************** * Grammar and handlers for network config "bridge_params" entry ****************************************************/ @@ -2295,6 +2313,9 @@ 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)}, + {"type", YAML_SCALAR_NODE, {.generic=handle_ip_rule_type}, ip_rule_offset(type)}, + {"iif", YAML_SCALAR_NODE, {.generic=handle_ip_rule_iif}, ip_rule_offset(iif)}, + {"oif", YAML_SCALAR_NODE, {.generic=handle_ip_rule_oif}, ip_rule_offset(oif)}, {NULL} }; @@ -2324,11 +2345,12 @@ handle_ip_rules(NetplanParser* npp, yaml_node_t* node, __unused const void* _, G npp->current.netdef->ip_rules = g_array_new(FALSE, FALSE, sizeof(NetplanIPRule*)); if (is_route_rule_present(npp->current.netdef, ip_rule)) { - g_debug("%s: rule (from: %s, to: %s, table: %d) has already been added", + g_debug("%s: rule (from: %s, to: %s, table: %d, type: %s) has already been added", npp->current.netdef->id, ip_rule->from, ip_rule->to, - ip_rule->table); + ip_rule->table, + ip_rule->type); ip_rule_clear(&ip_rule); npp->current.ip_rule = NULL; continue; diff --git a/src/types-internal.h b/src/types-internal.h index f8c1df3df..dabacbe71 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; + /* type of rule (eg. blackhole, prohibit, ...)*/ + char* type; + } NetplanIPRule; struct netplan_vxlan { @@ -287,6 +290,7 @@ struct netplan_state_iterator { #define NETPLAN_IP_RULE_PRIO_UNSPEC G_MAXUINT #define NETPLAN_IP_RULE_FW_MARK_UNSPEC 0 #define NETPLAN_IP_RULE_TOS_UNSPEC G_MAXUINT +#define NETPLAN_IP_RULE_TYPE_UNSPEC 0 #define NETPLAN_ADVMSS_UNSPEC 0 #if defined(UNITTESTS) diff --git a/src/types.c b/src/types.c index 7a1c20ed2..9d3709dd0 100644 --- a/src/types.c +++ b/src/types.c @@ -167,6 +167,7 @@ reset_ip_rule(NetplanIPRule* ip_rule) ip_rule->table = NETPLAN_ROUTE_TABLE_UNSPEC; ip_rule->tos = NETPLAN_IP_RULE_TOS_UNSPEC; ip_rule->fwmark = NETPLAN_IP_RULE_FW_MARK_UNSPEC; + ip_rule->type = NETPLAN_IP_RULE_TYPE_UNSPEC; } /* Reset a backend settings object. */ From 4cacb48cbb09ca09f9d425b20b9410aea50704e7 Mon Sep 17 00:00:00 2001 From: Romain FIHUE Date: Tue, 5 Aug 2025 20:05:41 +0200 Subject: [PATCH 2/8] nm: Generate rule type in NetworkManager configuration files --- src/nm.c | 3 ++- tests/generator/test_routing.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/nm.c b/src/nm.c index 583fd2d62..a599381f1 100644 --- a/src/nm.c +++ b/src/nm.c @@ -303,7 +303,8 @@ write_ip_rules_nm(const NetplanNetDefinition* def, GKeyFile *kf, gint family, GE 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); - + if (cur_rule->type) + g_string_append_printf(tmp_val, " type %s", cur_rule->type); g_key_file_set_string(kf, group, tmp_key, tmp_val->str); g_free(tmp_key); g_string_free(tmp_val, TRUE); diff --git a/tests/generator/test_routing.py b/tests/generator/test_routing.py index 2612d2e96..2a65d281c 100644 --- a/tests/generator/test_routing.py +++ b/tests/generator/test_routing.py @@ -1125,6 +1125,36 @@ def test_ip_rule_table(self): 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_type(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: blackhole + 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 type blackhole + [ipv6] method=ignore '''}) From ed0426775e2dac9591ec269fd818a5d2db82c788 Mon Sep 17 00:00:00 2001 From: Romain FIHUE Date: Tue, 5 Aug 2025 20:32:30 +0200 Subject: [PATCH 3/8] fix: Still set ip rule if specified to do so. It may be used later on in some cases like virtual IPs set by an external tool (eg. keepalived) --- src/nm.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/nm.c b/src/nm.c index a599381f1..951b6fca7 100644 --- a/src/nm.c +++ b/src/nm.c @@ -933,10 +933,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; } - + write_ip_rules_nm(def, kf, AF_INET, error); if (!def->dhcp4_overrides.use_routes) { g_key_file_set_boolean(kf, "ipv4", "ignore-auto-routes", TRUE); g_key_file_set_boolean(kf, "ipv4", "never-default", TRUE); From 13272b96225f20e6a43786d7c7e01f380ec42aee Mon Sep 17 00:00:00 2001 From: Romain FIHUE Date: Tue, 5 Aug 2025 22:02:51 +0200 Subject: [PATCH 4/8] routing-policy: Add "iif" and "oif" scalars to routing-policy specification One can now fine-tune IP rules using input or output interfaces. One use-case is to catch locally-generated trafic by setting 'iif' to 'lo' --- doc/netplan-yaml.md | 11 +++++++++++ src/netplan.c | 2 ++ src/parse.c | 25 +++++++++++++++++++++++++ src/types-internal.h | 7 ++++++- src/types.c | 2 ++ 5 files changed, 46 insertions(+), 1 deletion(-) diff --git a/doc/netplan-yaml.md b/doc/netplan-yaml.md index 4c26e4087..3094e81f1 100644 --- a/doc/netplan-yaml.md +++ b/doc/netplan-yaml.md @@ -899,6 +899,17 @@ network: > The type of the rule. Valid options are `unicast` (default), > `blackhole`, `unreachable`, `prohibit` and `nat`. + - **`iif`** (scalar) + + > select the incoming device to match. If the interface is loopback + > the rule only matches packets originating from this host. + + - **`oif`** (scalar) + + > Select the outgoing device to match. + > The outgoing interface is only available for packets originating + > from local sockets that are bound to a device. + (yaml-auth)= ## Authentication diff --git a/src/netplan.c b/src/netplan.c index 2a785570a..ea5425243 100644 --- a/src/netplan.c +++ b/src/netplan.c @@ -642,6 +642,8 @@ 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, "type", r->type); + 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/parse.c b/src/parse.c index 3ea14af9f..b30b3e72c 100644 --- a/src/parse.c +++ b/src/parse.c @@ -2090,6 +2090,31 @@ handle_ip_rule_type(NetplanParser* npp, yaml_node_t* node, __unused const void* return yaml_error(npp, node, error, "invalid rule type '%s'", ip_rule->type); } +STATIC gboolean +handle_ip_rule_iif(NetplanParser* npp, yaml_node_t* node, __unused const void* data, GError** error) +{ + NetplanIPRule* ip_rule = npp->current.ip_rule; + if (ip_rule->iif) + g_free(ip_rule->iif); + ip_rule->iif = g_strdup(scalar(node)); + if (strpbrk(ip_rule->iif, "*[]?")) + return yaml_error(npp, node, error, "Rule input interface '%s' must not use globbing", ip_rule->iif); + return TRUE; +} + +STATIC gboolean +handle_ip_rule_oif(NetplanParser* npp, yaml_node_t* node, __unused const void* data, GError** error) +{ + NetplanIPRule* ip_rule = npp->current.ip_rule; + if (ip_rule->oif) { + g_free(ip_rule->oif); + } + ip_rule->oif = g_strdup(scalar(node)); + if (strpbrk(ip_rule->oif, "*[]?")) + return yaml_error(npp, node, error, "Rule output interface '%s' must not use globbing", ip_rule->oif); + return TRUE; +} + /**************************************************** * Grammar and handlers for network config "bridge_params" entry ****************************************************/ diff --git a/src/types-internal.h b/src/types-internal.h index dabacbe71..f5b5c847a 100644 --- a/src/types-internal.h +++ b/src/types-internal.h @@ -170,7 +170,10 @@ typedef struct { guint tos; /* type of rule (eg. blackhole, prohibit, ...)*/ char* type; - + /* Input and/or Output interface string*/ + char* iif; + char* oif; + } NetplanIPRule; struct netplan_vxlan { @@ -291,6 +294,8 @@ struct netplan_state_iterator { #define NETPLAN_IP_RULE_FW_MARK_UNSPEC 0 #define NETPLAN_IP_RULE_TOS_UNSPEC G_MAXUINT #define NETPLAN_IP_RULE_TYPE_UNSPEC 0 +#define NETPLAN_IP_RULE_IIF_UNSPEC 0 +#define NETPLAN_IP_RULE_OIF_UNSPEC 0 #define NETPLAN_ADVMSS_UNSPEC 0 #if defined(UNITTESTS) diff --git a/src/types.c b/src/types.c index 9d3709dd0..241b518b3 100644 --- a/src/types.c +++ b/src/types.c @@ -168,6 +168,8 @@ reset_ip_rule(NetplanIPRule* ip_rule) ip_rule->tos = NETPLAN_IP_RULE_TOS_UNSPEC; ip_rule->fwmark = NETPLAN_IP_RULE_FW_MARK_UNSPEC; ip_rule->type = NETPLAN_IP_RULE_TYPE_UNSPEC; + ip_rule->iif = NETPLAN_IP_RULE_IIF_UNSPEC; + ip_rule->oif = NETPLAN_IP_RULE_OIF_UNSPEC; } /* Reset a backend settings object. */ From aef3f29f449599a83f0cf7314ae49ac1c34daf12 Mon Sep 17 00:00:00 2001 From: Romain FIHUE Date: Tue, 5 Aug 2025 22:03:40 +0200 Subject: [PATCH 5/8] nm: Generate rule iif and oif in NetworkManager configuration files --- src/nm.c | 4 +++ tests/generator/test_routing.py | 60 +++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/src/nm.c b/src/nm.c index 951b6fca7..148b62076 100644 --- a/src/nm.c +++ b/src/nm.c @@ -305,6 +305,10 @@ write_ip_rules_nm(const NetplanNetDefinition* def, GKeyFile *kf, gint family, GE g_string_append_printf(tmp_val, " table %u", cur_rule->table); if (cur_rule->type) g_string_append_printf(tmp_val, " type %s", cur_rule->type); + 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); g_key_file_set_string(kf, group, tmp_key, tmp_val->str); g_free(tmp_key); g_string_free(tmp_val, TRUE); diff --git a/tests/generator/test_routing.py b/tests/generator/test_routing.py index 2a65d281c..e39d52bb8 100644 --- a/tests/generator/test_routing.py +++ b/tests/generator/test_routing.py @@ -1155,6 +1155,66 @@ def test_ip_rule_type(self): address1=192.168.14.2/24 routing-rule1=priority 99 to 10.10.10.0/24 type blackhole +[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 + iif: engreen + 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 iif engreen + +[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 + priority: 99 + oif: engreen + ''') + + 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 engreen + [ipv6] method=ignore '''}) From 394b9fcd8f73c0556681f2979caadf4ac5e9c57e Mon Sep 17 00:00:00 2001 From: Romain FIHUE Date: Tue, 5 Aug 2025 22:34:39 +0200 Subject: [PATCH 6/8] nm: Don't set link-local method on ipv4 if we're not asked for --- src/nm.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/nm.c b/src/nm.c index 148b62076..2f991eca6 100644 --- a/src/nm.c +++ b/src/nm.c @@ -912,9 +912,12 @@ write_nm_conf_access_point(const NetplanNetDefinition* def, const char* rootdir, else if (def->type == NETPLAN_DEF_TYPE_TUNNEL) /* sit tunnels will not start in link-local apparently */ g_key_file_set_string(kf, "ipv4", "method", "disabled"); - else - /* Without any address, this is the only available mode */ + else if (def->linklocal.ipv4) + /* Without any address, set link-local addresses if configured */ g_key_file_set_string(kf, "ipv4", "method", "link-local"); + else + /* Without any address nor link-local we fall back to disabled mode */ + g_key_file_set_string(kf, "ipv4", "method", "disabled"); if (def->ip4_addresses) { for (unsigned i = 0; i < def->ip4_addresses->len; ++i) { From 06920ee536425fc5cdad9f09285892b608713fbc Mon Sep 17 00:00:00 2001 From: Romain FIHUE Date: Tue, 5 Aug 2025 23:07:02 +0200 Subject: [PATCH 7/8] routing-policy: Add an example with a blackhole rule and some iif statements --- examples/source_routing_with_blackhole.yaml | 29 +++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 examples/source_routing_with_blackhole.yaml diff --git a/examples/source_routing_with_blackhole.yaml b/examples/source_routing_with_blackhole.yaml new file mode 100644 index 000000000..ab51225e0 --- /dev/null +++ b/examples/source_routing_with_blackhole.yaml @@ -0,0 +1,29 @@ +network: + version: 2 + ethernets: + eno0: + addresses: + - 192.168.3.42/24 + dhcp4: no + routes: + - to: default + via: 192.168.3.1 + eno1: + addresses: + - 192.168.4.24/24 + eno2: + addresses: + - 192.168.5.78/24 + routes: + - to: default + via: 192.168.4.1 + table: 100 + routing-policy: + - from: 192.168.5.0/24 + iif: eno1 + table: 100 + preference: 100 + - from: 192.168.5.0/24 + iif: eno1 + type: blackhole + preference: 200 From 6efe5e4387f1b1f03820b718c68037d0281656f1 Mon Sep 17 00:00:00 2001 From: Romain FIHUE Date: Tue, 5 Aug 2025 23:21:57 +0200 Subject: [PATCH 8/8] networkd: Generate rule type, iif and oif in networkd configuration files --- src/networkd.c | 6 +++ tests/generator/test_routing.py | 69 +++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/src/networkd.c b/src/networkd.c index d0d820562..7b08c3f2d 100644 --- a/src/networkd.c +++ b/src/networkd.c @@ -758,6 +758,12 @@ write_ip_rule(NetplanIPRule* r, GString* s) g_string_append_printf(s, "FirewallMark=%d\n", r->fwmark); if (r->tos != NETPLAN_IP_RULE_TOS_UNSPEC) g_string_append_printf(s, "TypeOfService=%d\n", r->tos); + if (r->type) + g_string_append_printf(s, "Type=%s\n", r->type); + 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); } STATIC void diff --git a/tests/generator/test_routing.py b/tests/generator/test_routing.py index e39d52bb8..41bad7bb9 100644 --- a/tests/generator/test_routing.py +++ b/tests/generator/test_routing.py @@ -654,6 +654,75 @@ def test_ip_rule_tos(self): [RoutingPolicyRule] To=10.10.10.0/24 TypeOfService=250 +'''}) + + def test_ip_rule_type(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: ["192.168.14.2/24"] + routing-policy: + - to: 10.10.10.0/24 + type: blackhole + ''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=ipv6 +Address=192.168.14.2/24 + +[RoutingPolicyRule] +To=10.10.10.0/24 +Type=blackhole +'''}) + + 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 + iif: engreen + ''') + + 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=engreen +'''}) + + 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 + oif: engreen + ''') + + 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=engreen '''}) def test_use_routes(self):