Skip to content

linklayer.vlan: classify received frames per port, and set the VLAN policy of a port during the run - #1391

Merged
levy merged 4 commits into
masterfrom
topic/vlan-port-policy
Oct 28, 2026
Merged

levy merged 4 commits into
masterfrom
topic/vlan-port-policy

Conversation

@avarga

@avarga avarga commented Oct 27, 2026

Copy link
Copy Markdown
Member

A VLAN-aware INET bridge cannot put two untagged ports into two different VLANs. VlanPolicyLayer assigns VLANs only on egress, keyed by the outgoing port; an untagged frame crosses the bridge without a VlanInd, and the relay learns and looks it up in VLAN 0. So every untagged port of a bridge shares one flooding domain, and a trunk port cannot tag a frame with the VLAN of the access port it came from. IEEE 802.1Q-2018 clause 6.9.1 f classifies untagged and priority-tagged frames into the port VLAN id (PVID) of the ingress port; INET has no such classification, and the inboundMapper slot of VlanPolicyLayer has had no module to fill it. A second gap: the VLAN policy modules read their per-port maps once from object parameters and have no API, so a port created during the run (for example one port per 5G Ethernet PDU session in a UPF bridge, with the VLAN known only at session establishment) cannot get a VLAN policy.

After this change, VlanIndMapper, the ingress counterpart of VlanReqMapper, classifies received frames per port ({"-1" : n, "0" : n} is PVID classification), and all four policy modules (VlanReqMapper, VlanReqFilter, VlanIndFilter, VlanIndMapper) have C++ setters that replace or remove the map of one interface at run time, and forget a deleted interface. On the way, the three existing modules validate their map parameters at initialization instead of ignoring malformed entries or failing at the first packet, and keep their maps in C++ containers instead of looking every packet up in the parameter's cValueMap.

The inbound filter stays before the inbound mapper, so both filters of VlanPolicyLayer check the VLAN id as it is on the wire (an access port accepts [-1, 0] inbound and [-1] outbound). Classification of untagged frames needs the policy layer in BridgingLayer.vlanPolicy; the policy of Ieee8021qLayer sits above the tag checker and never sees untagged frames, and the NED documentation says so.

Risk: inboundMapper keeps its empty default and nothing in the tree calls the setters, so existing models keep their behavior. The one behavior change for existing users is that a malformed VLAN map, which used to be silently ignored or to fail late, now stops the simulation at initialization; no configuration in the tree has one. The relays still use VLAN 0, not the standard default PVID 1, for frames that no mapper classifies; changing that would move the fingerprints of every bridge with an 802.1Q layer and is left out of this pull request.

Commits

  1. linklayer.vlan: add: classify received frames per port with VlanIndMapper -- the new module and its documentation, with the module test VlanIndMapper_1 on a two-bridge access/trunk network. Moves behavior only where configured.
  2. linklayer.vlan: fix: VLAN policy modules reject malformed maps at initialization -- the checks, shared in VlanIdMaps, with the module tests VlanPolicyMapsInvalid_1 to _6. Moves behavior for malformed configurations only.
  3. linklayer.vlan: refactor: VLAN policy modules keep their maps in C++ containers -- prepares commit 4; no behavior change. Commit 2 exists so that this one is a true refactor: containers parsed at initialization cannot reproduce the old per-packet handling of malformed entries.
  4. linklayer.vlan: add: set the VLAN policy of a port during the run -- the setters and the deletion handling, with the module test VlanPolicyLayer_runtime_1.

Architectural surface: a new simple module VlanIndMapper (interfaceTableModule, mappedVlanIds), fitting the existing inboundMapper slot of VlanPolicyLayer. New public non-virtual C++ methods on the four policy modules (setInterfaceVlanIdMap(), removeInterfaceVlanIdMap(), setInterfaceAcceptedVlanIds(), removeInterfaceAcceptedVlanIds()), entered through Enter_Method; the four modules become cListeners of interfaceDeletedSignal on their interface table. The protected members holding the maps change type from cValueMap * to std::map containers. New free functions in inet/linklayer/vlan/VlanIdMaps.h. No message, packet-format or default change. No release notes yet.

Relation to #1390: the entries of an interface module that is deleted without InterfaceTable::deleteInterface() stay until #1390, which makes every deletion go through the interface table, is merged; the runtime test deletes through deleteInterface() and does not depend on it.

Evidence

Before the series was rebased onto the current master (7fda2e3), locally, debug:

  • Each commit built, and the VLAN module tests present at it passed (VlanIndMapper_1, VlanPolicyMapsInvalid_1 to _6, VlanPolicyLayer_runtime_1, and the relay tests *Relay*_vlan_1), run through opp_test.
  • The tplx, ~tNl and tyf fingerprints of the six configurations of examples/ethernet/vlan and of VlanIndMapper_1 were identical from commit 2 to commit 4, computed directly with inet_dbg, because the installed opp_repl rejected the inet.opp of that revision.
  • The full module suite at the tip, through opp_test: 759 of 767 passed; the other 8 (IEEE 802.11 tests) gave the same results on master.
  • Mutation checks: with the deletion handling of VlanReqFilter disabled, VlanPolicyLayer_runtime_1 fails; classifying b1 into VLAN 10 in VlanIndMapper_1 changes the frame counts.

Not run after the rebase onto the current master: the build, the module tests and the fingerprint suite; CI on this pull request covers them. Not run at all: the release-mode build and the store-based full fingerprint suite (left to CI).

…pper

Before this commit, a VLAN-aware bridge could not put two untagged ports into two different VLANs. VlanPolicyLayer assigned VLANs only on egress, keyed by the outgoing port, so the relay learned and looked up every untagged frame in VLAN 0. All untagged ports of a bridge shared one flooding domain, and a trunk port could not tag a frame with the VLAN of the access port it came from. IEEE 802.1Q-2018 clause 6.9.1 f classifies an untagged or priority-tagged frame into the port VLAN id (PVID) of its ingress port.

After this commit, VlanIndMapper, the ingress counterpart of VlanReqMapper, can fill the inboundMapper slot of VlanPolicyLayer, which had no module in INET. Per incoming interface, it maps the received VLAN id (-1 untagged, 0 priority-tagged) to the VLAN id of the VlanInd; {"-1" : n, "0" : n} on a port is PVID classification with PVID n. The relay then learns and looks the frame up in VLAN n, and the outbound mapper and filter of each port tag, untag or drop it as they do a tagged frame.

The inbound filter still runs before the mapper, so both filters check the VLAN id as it is on the wire. Classifying untagged frames needs the policy layer in the vlanPolicy slot of BridgingLayer, because the policy of Ieee8021qLayer sits above the tag checker and never sees an untagged frame; the NED documentation states both. inboundMapper keeps its empty default, so no existing model changes behavior.

The module test VlanIndMapper_1 joins two bridges by a trunk, with hosts of VLAN 10 and VLAN 20 that share MAC addresses across the VLANs; one host sends priority-tagged frames. Every receiver gets its data, both VLANs are tagged on the trunk, and the frame count of every host equals that of the same hosts on two separate pairs of plain switches.

Change: src.linklayer.vlan | behavior.add | test whatsnew | vlan-port-policy
…tialization

Before this commit, VlanReqMapper, VlanReqFilter and VlanIndFilter read their map parameters only when a packet arrived, so a malformed entry was ignored or failed late. With mappedVlanIds = {"eth0" : {"010" : -1}}, the key never matched, frames of VLAN 10 were not untagged on eth0, and an outbound filter of [-1] dropped them all without saying why. {"eth0" : {"-1" : 5000}} put 5000 into the VlanReq, and the tag inserter wrote it into the 12-bit VID field. An entry of the wrong type raised an error only when the first packet reached its interface, so a port without traffic hid it.

After this commit, each of the three modules checks its parameter at initialization and throws an error that names the interface and the entry: per interface a list (filters) or a map (mapper), mapper keys in decimal form, and VLAN ids that are integers from -1 to 4094. The checks are shared in VlanIdMaps. The lookups do not change, so every well-formed configuration behaves as before.

The module tests VlanPolicyMapsInvalid_1 to _6 each run the network of VlanIndMapper_1 with one malformed parameter and expect its error message. On the parent, the configurations of _2, _3, _5 and _6 run to the end without an error, and those of _1 and _4 fail only when the first packet reaches the entry.

Change: src.linklayer.vlan | behavior.change.fix | test whatsnew | vlan-port-policy
…containers

VlanReqMapper, VlanReqFilter and VlanIndFilter looked every packet up in the cValueMap of their parameter, with string keys built per packet, and VlanIndMapper had a parser of its own. The next commit lets a module change the map of one interface during the run, which needs maps that the module owns: writing into the object of a parameter would bypass the parameter system.

After this commit, the four modules parse their parameter at initialization into std::map containers keyed by interface name, holding sets of accepted VLAN ids (filters) or maps from VLAN id to VLAN id (mappers). The shared parseAcceptedVlanIds() and parseMappedVlanIds() of VlanIdMaps take over the checks of the previous commit with the same error messages, and findInterfaceEntry() keeps the lookup order: the interface name, then "*", then no entry. The WATCHes of the parameter objects of the three older modules are removed; they showed a parameter that the inspector shows anyway.

The behavior does not change: the VLAN module tests pass unchanged, and the fingerprints of examples/ethernet/vlan stay the same.

Change: src.linklayer.vlan | refactor | - | vlan-port-policy
Before this commit, the VLAN policy modules took their maps only from their object parameters, keyed by interface name, and no API changed them, so a port created during the run could not get a VLAN policy. A bridge that creates one port per session, with the VLAN of the session known only when the port appears, could not use them. The entries of a deleted interface also stayed in the maps.

After this commit, VlanReqMapper and VlanIndMapper have setInterfaceVlanIdMap() and removeInterfaceVlanIdMap(), and VlanReqFilter and VlanIndFilter have setInterfaceAcceptedVlanIds() and removeInterfaceAcceptedVlanIds(). Each takes a NetworkInterface of the module's interface table, enters through Enter_Method, checks the VLAN ids as the parameter parsing does (VlanIndMapper does not map to VLAN id 0), and replaces or removes the entry of the interface name, which is the entry the static configuration fills. Each module also subscribes to interfaceDeletedSignal on its interface table, which emits it for its own interfaces only, and erases the entry of a deleted interface. The maps can now change during the run, so they are shown by a WATCH.

The interface table emits interfaceDeletedSignal in deleteInterface(); the entries of an interface module deleted in another way stay.

The module test VlanPolicyLayer_runtime_1 runs the network of VlanIndMapper_1 with no static entry for the port of b1 on bridge1. The first ARP request of b1 is dropped; at 0.5 s a test module makes the port an access port of VLAN 20 through the four setters, after which b2 receives all data of b1 and the VLAN 10 hosts receive none of it. At 2.5 s the test module deletes the interface, and none of the four modules holds an entry for it afterwards.

Change: src.linklayer.vlan | behavior.add | test whatsnew | vlan-port-policy
@levy
levy merged commit 57bb252 into master Oct 28, 2026
17 checks passed
@avarga
avarga deleted the topic/vlan-port-policy branch October 28, 2026 09:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants