diff --git a/doc/netplan-yaml.md b/doc/netplan-yaml.md index b22755755..ea2bfa356 100644 --- a/doc/netplan-yaml.md +++ b/doc/netplan-yaml.md @@ -865,6 +865,14 @@ network: > Match on traffic going to the specified destination. + - **`iif`** (scalar) + + > 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 d6ab84902..23ae82ba7 100644 --- a/src/netplan.c +++ b/src/netplan.c @@ -641,6 +641,8 @@ 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_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 982d18cb9..d65ccb9e3 100644 --- a/src/networkd.c +++ b/src/networkd.c @@ -749,6 +749,11 @@ 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->oif) + g_string_append_printf(s, "OutgoingInterface=%s\n", r->oif); + 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..82df272f5 100644 --- a/src/nm.c +++ b/src/nm.c @@ -297,6 +297,10 @@ 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->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 64a9a809f..3d8e897dd 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_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); +} + STATIC gboolean handle_ip_rule_guint(NetplanParser* npp, yaml_node_t* node, const void* data, GError** error) { @@ -2295,6 +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_if}, ip_rule_offset(iif)}, + {"oif", YAML_SCALAR_NODE, {.generic=handle_ip_rule_if}, ip_rule_offset(oif)}, {NULL} }; @@ -2309,11 +2332,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 && !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 f8c1df3df..c386adf86 100644 --- a/src/types-internal.h +++ b/src/types-internal.h @@ -168,6 +168,11 @@ typedef struct { guint fwmark; /* type-of-service: between 0 and 255 */ guint tos; + + /* incoming interface */ + char* iif; + /* outgoing interface */ + char* oif; } NetplanIPRule; struct netplan_vxlan { diff --git a/src/types.c b/src/types.c index 7a1c20ed2..9a8d51887 100644 --- a/src/types.c +++ b/src/types.c @@ -87,6 +87,8 @@ free_ip_rules(void* ptr) NetplanIPRule* rule = 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 1b2852dce..08712ce4b 100644 --- a/src/util.c +++ b/src/util.c @@ -1210,7 +1210,9 @@ 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 && + g_strcmp0(entry->oif, rule->oif) == 0 ) return TRUE; } diff --git a/tests/generator/test_errors.py b/tests/generator/test_errors.py index d447c4920..1c86808e2 100644 --- a/tests/generator/test_errors.py +++ b/tests/generator/test_errors.py @@ -916,6 +916,30 @@ 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_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 2612d2e96..a157c3363 100644 --- a/tests/generator/test_routing.py +++ b/tests/generator/test_routing.py @@ -654,6 +654,56 @@ 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_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): @@ -1185,6 +1235,68 @@ 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 +'''}) + + 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 '''})