Skip to content
16 changes: 16 additions & 0 deletions doc/netplan-yaml.md
Original file line number Diff line number Diff line change
Expand Up @@ -894,6 +894,22 @@ 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`.

- **`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

Expand Down
29 changes: 29 additions & 0 deletions examples/source_routing_with_blackhole.yaml
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions src/netplan.c
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,9 @@ 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_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);
Expand Down
6 changes: 6 additions & 0 deletions src/networkd.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 12 additions & 6 deletions src/nm.c
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,12 @@ 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);
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);
Expand Down Expand Up @@ -907,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) {
Expand All @@ -932,10 +940,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);
Expand Down
51 changes: 49 additions & 2 deletions src/parse.c
Original file line number Diff line number Diff line change
Expand Up @@ -2072,6 +2072,49 @@ 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);
}

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
****************************************************/
Expand Down Expand Up @@ -2295,6 +2338,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}
};

Expand Down Expand Up @@ -2324,11 +2370,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;
Expand Down
9 changes: 9 additions & 0 deletions src/types-internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,12 @@ typedef struct {
guint fwmark;
/* type-of-service: between 0 and 255 */
guint tos;
/* type of rule (eg. blackhole, prohibit, ...)*/
char* type;
/* Input and/or Output interface string*/
char* iif;
char* oif;

} NetplanIPRule;

struct netplan_vxlan {
Expand Down Expand Up @@ -287,6 +293,9 @@ 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_IP_RULE_IIF_UNSPEC 0
#define NETPLAN_IP_RULE_OIF_UNSPEC 0
#define NETPLAN_ADVMSS_UNSPEC 0

#if defined(UNITTESTS)
Expand Down
3 changes: 3 additions & 0 deletions src/types.c
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,9 @@ 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;
ip_rule->iif = NETPLAN_IP_RULE_IIF_UNSPEC;
ip_rule->oif = NETPLAN_IP_RULE_OIF_UNSPEC;
}

/* Reset a backend settings object. */
Expand Down
159 changes: 159 additions & 0 deletions tests/generator/test_routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -1125,6 +1194,96 @@ 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
'''})

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
'''})
Expand Down
Loading