Skip to content

Commit 2a657c2

Browse files
slyonp-strusiewiczsurmacki-mobica
authored andcommitted
bridges: implement vlans & port-vlans options for NM backend
NM:bridge:vlan: enable vlan-filtering on bridge, if vlans are set
1 parent 0965bc9 commit 2a657c2

File tree

4 files changed

+319
-0
lines changed

4 files changed

+319
-0
lines changed

src/networkd.c

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,11 @@ write_bridge_params_networkd(GString* s, const NetplanNetDefinition* def)
215215
if (def->bridge_params.max_age)
216216
g_string_append_printf(params, "MaxAgeSec=%s\n", def->bridge_params.max_age);
217217
g_string_append_printf(params, "STP=%s\n", def->bridge_params.stp ? "true" : "false");
218+
if (def->bridge_params.vlans) {
219+
// TODO: research and implement bridge vlans for networkd
220+
g_fprintf(stderr, "ERROR: %s: networkd does not support bridge vlans\n", def->id);
221+
exit(1);
222+
}
218223

219224
g_string_append_printf(s, "\n[Bridge]\n%s", params->str);
220225

@@ -982,6 +987,11 @@ _netplan_netdef_write_network_file(
982987
g_string_append_printf(network, "Learning=%s\n", def->bridge_learning ? "true" : "false");
983988
if (def->bridge_neigh_suppress != NETPLAN_TRISTATE_UNSET)
984989
g_string_append_printf(network, "NeighborSuppression=%s\n", def->bridge_neigh_suppress ? "true" : "false");
990+
if (def->bridge_params.port_vlans) {
991+
// TODO: research and implement bridge port-vlans for networkd
992+
g_fprintf(stderr, "ERROR: %s: networkd does not support bridge port-vlans\n", def->id);
993+
exit(1);
994+
}
985995

986996
}
987997
if (def->bond && def->backend != NETPLAN_BACKEND_OVS) {

src/nm.c

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,25 @@ wifi_band_str(const NetplanWifiBand band)
145145
}
146146
}
147147

148+
/**
149+
* Return NM bridge vlan string.
150+
*/
151+
static const char*
152+
bridge_vlan_str(const NetplanBridgeVlan* vlan)
153+
{
154+
GString* s = NULL;
155+
s = g_string_sized_new(20);
156+
157+
g_string_append_printf(s, "%u", vlan->vid);
158+
if (vlan->vid_to)
159+
g_string_append_printf(s, "-%u", vlan->vid_to);
160+
if (vlan->pvid)
161+
g_string_append(s, " pvid");
162+
if (vlan->untagged)
163+
g_string_append(s, " untagged");
164+
return s->str;
165+
}
166+
148167
/**
149168
* Return NM addr-gen-mode string.
150169
*/
@@ -388,6 +407,18 @@ write_bridge_params_nm(const NetplanNetDefinition* def, GKeyFile *kf)
388407
if (def->bridge_params.max_age)
389408
g_key_file_set_string(kf, "bridge", "max-age", def->bridge_params.max_age);
390409
g_key_file_set_boolean(kf, "bridge", "stp", def->bridge_params.stp);
410+
if (def->bridge_params.vlans) {
411+
g_string_append(params, "vlan-filtering=true\n");
412+
g_string_append(params, "vlans=");
413+
for (unsigned i = 0; i < def->bridge_params.vlans->len; ++i) {
414+
if (i > 0)
415+
g_string_append(params, ", ");
416+
g_string_append_printf(params, "%s", bridge_vlan_str(
417+
g_array_index(def->bridge_params.vlans,
418+
NetplanBridgeVlan*, i)));
419+
}
420+
g_string_append(params, "\n");
421+
}
391422
}
392423
}
393424

@@ -824,6 +855,17 @@ write_nm_conf_access_point(const NetplanNetDefinition* def, const char* rootdir,
824855
g_key_file_set_uint64(kf, "bridge-port", "priority", def->bridge_params.port_priority);
825856
if (def->bridge_hairpin != NETPLAN_TRISTATE_UNSET)
826857
g_key_file_set_boolean(kf, "bridge-port", "hairpin-mode", def->bridge_hairpin);
858+
if (def->bridge_params.port_vlans) {
859+
g_string_append(s, "vlans=");
860+
for (unsigned i = 0; i < def->bridge_params.port_vlans->len; ++i) {
861+
if (i > 0)
862+
g_string_append(s, ", ");
863+
g_string_append_printf(s, "%s", bridge_vlan_str(
864+
g_array_index(def->bridge_params.port_vlans,
865+
NetplanBridgeVlan*, i)));
866+
}
867+
g_string_append(s, "\n");
868+
}
827869
}
828870
if (def->bond) {
829871
g_key_file_set_string(kf, "connection", "slave-type", "bond"); /* wokeignore:rule=slave */

src/parse.c

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2167,6 +2167,108 @@ handle_bridge_port_priority(NetplanParser* npp, yaml_node_t* node, const char* k
21672167
return TRUE;
21682168
}
21692169

2170+
static gboolean
2171+
handle_generic_vlans(yaml_document_t* doc, yaml_node_t* node, GArray** entryptr, const void* data, GError** error)
2172+
{
2173+
static regex_t re;
2174+
static gboolean re_inited = FALSE;
2175+
2176+
if (!re_inited) {
2177+
g_assert(regcomp(&re, "^([0-9]+)(-([0-9]+))?( (pvid))?( (untagged))?$", REG_EXTENDED) == 0);
2178+
re_inited = TRUE;
2179+
}
2180+
2181+
for (yaml_node_item_t *i = node->data.sequence.items.start; i < node->data.sequence.items.top; i++) {
2182+
g_autofree char* vlan = NULL;
2183+
yaml_node_t *entry = yaml_document_get_node(doc, *i);
2184+
assert_type(entry, YAML_SCALAR_NODE);
2185+
2186+
vlan = g_strdup(scalar(entry));
2187+
2188+
size_t maxGroups = 7+1;
2189+
regmatch_t groups[maxGroups];
2190+
/* does it match the vlans= definition? */
2191+
if (regexec(&re, vlan, maxGroups, groups, 0) == 0) {
2192+
NetplanBridgeVlan* data = g_new0(NetplanBridgeVlan, 1);
2193+
for (unsigned g = 1; g < maxGroups; g = g+2) {
2194+
if (groups[g].rm_so == (size_t)-1)
2195+
continue; // Invalid group
2196+
2197+
char cursorCopy[strlen(vlan) + 1];
2198+
strcpy(cursorCopy, vlan);
2199+
cursorCopy[groups[g].rm_eo] = 0;
2200+
guint v = 0;
2201+
switch (g) {
2202+
case 1:
2203+
v = g_ascii_strtoull(cursorCopy + groups[g].rm_so, NULL, 10);
2204+
if (v < 1 || v > 4094)
2205+
return yaml_error(node, error, "malformed vlan vid '%u', must be in range [1..4094]", v);
2206+
data->vid = v;
2207+
break;
2208+
case 3:
2209+
v = g_ascii_strtoull(cursorCopy + groups[g].rm_so, NULL, 10);
2210+
if (v < 1 || v > 4094)
2211+
return yaml_error(node, error, "malformed vlan vid '%u', must be in range [1..4094]", v);
2212+
else if (v <= data->vid)
2213+
return yaml_error(node, error, "malformed vlan vid range '%s': %u > %u!", scalar(entry), data->vid, v);
2214+
data->vid_to = v;
2215+
break;
2216+
case 5:
2217+
data->pvid = TRUE;
2218+
break;
2219+
case 7:
2220+
data->untagged = TRUE;
2221+
break;
2222+
default: g_assert_not_reached(); // LCOV_EXCL_LINE
2223+
}
2224+
}
2225+
if (!*entryptr)
2226+
*entryptr = g_array_new(FALSE, FALSE, sizeof(NetplanBridgeVlan*));
2227+
g_array_append_val(*entryptr, data);
2228+
continue;
2229+
}
2230+
2231+
return yaml_error(node, error, "malformed vlan '%s', must be: $vid [pvid] [untagged] [, $vid [pvid] [untagged]]", scalar(entry));
2232+
}
2233+
2234+
return TRUE;
2235+
}
2236+
2237+
static gboolean
2238+
handle_bridge_vlans(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error)
2239+
{
2240+
return handle_generic_vlans(doc, node, &(cur_netdef->bridge_params.vlans), data, error);
2241+
}
2242+
2243+
static gboolean
2244+
handle_bridge_port_vlans(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error)
2245+
{
2246+
for (yaml_node_pair_t* entry = node->data.mapping.pairs.start; entry < node->data.mapping.pairs.top; entry++) {
2247+
yaml_node_t* key, *value;
2248+
NetplanNetDefinition *component;
2249+
GArray** ref_ptr;
2250+
2251+
key = yaml_document_get_node(doc, entry->key);
2252+
assert_type(key, YAML_SCALAR_NODE);
2253+
value = yaml_document_get_node(doc, entry->value);
2254+
assert_type(value, YAML_SEQUENCE_NODE);
2255+
2256+
component = g_hash_table_lookup(netdefs, scalar(key));
2257+
if (!component) {
2258+
add_missing_node(key);
2259+
} else {
2260+
ref_ptr = &(component->bridge_params.port_vlans);
2261+
if (*ref_ptr)
2262+
return yaml_error(node, error, "%s: interface '%s' already has port vlans",
2263+
cur_netdef->id, scalar(key));
2264+
2265+
if (!handle_generic_vlans(doc, value, ref_ptr, data, error))
2266+
return FALSE;
2267+
}
2268+
}
2269+
return TRUE;
2270+
}
2271+
21702272
static const mapping_entry_handler bridge_params_handlers[] = {
21712273
{"ageing-time", YAML_SCALAR_NODE, {.generic=handle_netdef_str}, netdef_offset(bridge_params.ageing_time)},
21722274
{"aging-time", YAML_SCALAR_NODE, {.generic=handle_netdef_str}, netdef_offset(bridge_params.ageing_time)},
@@ -2175,8 +2277,10 @@ static const mapping_entry_handler bridge_params_handlers[] = {
21752277
{"max-age", YAML_SCALAR_NODE, {.generic=handle_netdef_str}, netdef_offset(bridge_params.max_age)},
21762278
{"path-cost", YAML_MAPPING_NODE, {.map={.custom=handle_bridge_path_cost}}, netdef_offset(bridge_params.path_cost)},
21772279
{"port-priority", YAML_MAPPING_NODE, {.map={.custom=handle_bridge_port_priority}}, netdef_offset(bridge_params.port_priority)},
2280+
{"port-vlans", YAML_MAPPING_NODE, handle_bridge_port_vlans},
21782281
{"priority", YAML_SCALAR_NODE, {.generic=handle_netdef_guint}, netdef_offset(bridge_params.priority)},
21792282
{"stp", YAML_SCALAR_NODE, {.generic=handle_netdef_bool}, netdef_offset(bridge_params.stp)},
2283+
{"vlans", YAML_SEQUENCE_NODE, handle_bridge_vlans},
21802284
{NULL}
21812285
};
21822286

tests/generator/test_bridges.py

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,77 @@ def test_bridge_stp(self):
610610
stp: no
611611
dhcp4: true''')
612612

613+
def test_bridge_vlans(self):
614+
self.generate('''network:
615+
version: 2
616+
renderer: NetworkManager
617+
ethernets:
618+
eno1: {}
619+
switchport: {}
620+
bridges:
621+
br0:
622+
interfaces: [eno1, switchport]
623+
parameters:
624+
vlans: [1-100 pvid untagged, 42 untagged, 13, 1 pvid, 2-100 pvid untagged]
625+
port-vlans:
626+
eno1: [99-999 pvid untagged, 1 untagged, 42 pvid]
627+
switchport: [4000-4094, 1 pvid, 13 untagged]''')
628+
629+
self.assert_nm({'br0': '''[connection]
630+
id=netplan-br0
631+
type=bridge
632+
interface-name=br0
633+
634+
[bridge]
635+
stp=true
636+
vlan-filtering=true
637+
vlans=1-100 pvid untagged, 42 untagged, 13, 1 pvid, 2-100 pvid untagged
638+
639+
[ipv4]
640+
method=link-local
641+
642+
[ipv6]
643+
method=ignore
644+
''',
645+
'eno1': '''[connection]
646+
id=netplan-eno1
647+
type=ethernet
648+
interface-name=eno1
649+
slave-type=bridge
650+
master=br0
651+
652+
[bridge-port]
653+
vlans=99-999 pvid untagged, 1 untagged, 42 pvid
654+
655+
[ethernet]
656+
wake-on-lan=0
657+
658+
[ipv4]
659+
method=link-local
660+
661+
[ipv6]
662+
method=ignore
663+
''',
664+
'switchport': '''[connection]
665+
id=netplan-switchport
666+
type=ethernet
667+
interface-name=switchport
668+
slave-type=bridge
669+
master=br0
670+
671+
[bridge-port]
672+
vlans=4000-4094, 1 pvid, 13 untagged
673+
674+
[ethernet]
675+
wake-on-lan=0
676+
677+
[ipv4]
678+
method=link-local
679+
680+
[ipv6]
681+
method=ignore
682+
'''})
683+
613684

614685
class TestConfigErrors(TestBase):
615686

@@ -724,3 +795,95 @@ def test_bridge_invalid_port_prio(self):
724795
port-priority:
725796
eno1: 257
726797
dhcp4: true''', expect_fail=True)
798+
799+
def test_bridge_no_vlan(self):
800+
err = self.generate('''network:
801+
version: 2
802+
bridges:
803+
br0:
804+
parameters:
805+
vlans: [99-999 pvid untagged, 1 untagged, 42 pvid]''', expect_fail=True)
806+
self.assertIn("ERROR: br0: networkd does not support bridge vlans", err)
807+
808+
def test_bridge_no_port_vlan(self):
809+
err = self.generate('''network:
810+
version: 2
811+
ethernets:
812+
eno1: {}
813+
bridges:
814+
br0:
815+
interfaces: [eno1]
816+
parameters:
817+
port-vlans:
818+
eno1: [99-999 pvid untagged, 1 untagged, 42 pvid]''', expect_fail=True)
819+
self.assertIn("ERROR: eno1: networkd does not support bridge port-vlans", err)
820+
821+
def test_bridge_invalid_vlan(self):
822+
err = self.generate('''network:
823+
version: 2
824+
bridges:
825+
br0:
826+
parameters:
827+
vlans: [1 unmapped INVALID]''', expect_fail=True)
828+
self.assertIn("Error in network definition: malformed vlan '1 unmapped INVALID', must be: $vid [pvid] [untagged] \
829+
[, $vid [pvid] [untagged]]", err)
830+
831+
def test_bridge_invalid_vlan_vid(self):
832+
err = self.generate('''network:
833+
version: 2
834+
bridges:
835+
br0:
836+
parameters:
837+
vlans: [0]''', expect_fail=True)
838+
self.assertIn("Error in network definition: malformed vlan vid '0', must be in range [1..4094]", err)
839+
840+
def test_bridge_invalid_port_vlan_vid_to(self):
841+
err = self.generate('''network:
842+
version: 2
843+
ethernets:
844+
eno1: {}
845+
bridges:
846+
br0:
847+
interfaces: [eno1]
848+
parameters:
849+
port-vlans:
850+
eno1: [1-4095]''', expect_fail=True)
851+
self.assertIn("Error in network definition: malformed vlan vid '4095', must be in range [1..4094]", err)
852+
853+
def test_bridge_port_vlan_already_defined(self):
854+
err = self.generate('''network:
855+
version: 2
856+
ethernets:
857+
eno1: {}
858+
bridges:
859+
br0:
860+
interfaces: [eno1]
861+
parameters:
862+
port-vlans:
863+
eno1: [1]
864+
eno1: [1]''', expect_fail=True)
865+
self.assertIn("Error in network definition: br0: interface 'eno1' already has port vlans", err)
866+
867+
def test_bridge_invalid_vlan_vid_range(self):
868+
err = self.generate('''network:
869+
version: 2
870+
bridges:
871+
br0:
872+
parameters:
873+
vlans: [100-1]''', expect_fail=True)
874+
self.assertIn("Error in network definition: malformed vlan vid range '100-1': 100 > 1!", err)
875+
876+
def test_bridge_port_vlan_add_missing_node(self):
877+
err = self.generate('''network:
878+
version: 2
879+
ethernets:
880+
eno1:
881+
match:
882+
name: eth0
883+
bridges:
884+
br0:
885+
interfaces: [eno1]
886+
parameters:
887+
port-vlans:
888+
eth0: [1]''', expect_fail=True)
889+
self.assertIn("Error in network definition: br0: interface 'eth0' is not defined", err)

0 commit comments

Comments
 (0)