flowprobe: L2 RX with L3/L4 records fails for 802.1Q tagged traffic
Summary
flowprobe does not correctly collect L3/L4 fields when it is attached as an L2 RX feature and receives 802.1Q-tagged IPv4 traffic.
With the following configuration:
set ipfix exporter collector 10.224.5.196 port 4739 src 10.255.255.190 template-interval 20 path-mtu 1450
flowprobe params record l3 l4 active 120 passive 300
flowprobe feature add-del Eth1_AB1 l2 rx
untagged IPv4 traffic is classified as FLOW_VARIANT_L2_IP4, but VLAN-tagged IPv4 traffic is classified only as FLOW_VARIANT_L2.
As a result, IPFIX records contain empty L3/L4 fields:
and the collector receives IPFIX templates but no useful flow data.
Environment
VPP v26.06-release
built 2026-06-24
Traffic is received through a DPDK interface connected to a SPAN/TAP monitoring port.
The issue was reproduced with normal IEEE 802.1Q tagged IPv4/TCP traffic.
Configuration
set ipfix exporter collector 10.224.5.196 port 4739 src 10.255.255.190 template-interval 20 path-mtu 1450
flowprobe params record l3 l4 active 120 passive 300
flowprobe feature add-del Eth1_AB1 l2 rx
show flowprobe output:
vppctl show flowprobe params
l3 l4 active: 120 passive: 300
vppctl show flowprobe feature
Eth1_AB1 l2 rx
Observed behavior
VPP correctly decodes the packet at dpdk-input:
dpdk-input
Eth1_AB1 rx queue 0
length 1518
IP4: 6c:b3:11:4d:19:de -> 5c:5e:ab:4b:da:00 802.1q vlan 2196
TCP: 10.224.240.107 -> 10.144.149.3
TCP: 80 -> 39510
However, immediately afterwards flowprobe reports:
flowprobe-input-l2
FLOWPROBE[L2]: rx_sw_if_index 2, tx_sw_if_index -1,
timestamp 0, size 0
-> 00:00:00:00:00:00
The flow table contains empty entries:
rx 2/-1 00:00:00:00:00:00 00:00:00:00:00:00
0.0.0.0 -> 0.0.0.0 0 0 0
Example:
vppctl show flowprobe table
Dumping IPFIX table
rx 2/-1 00:00:00:00:00:00 00:00:00:00:00:00 0.0.0.0 -> 0.0.0.0 0 0 0
rx 2/-1 00:00:00:00:00:00 00:00:00:00:00:00 0.0.0.0 -> 0.0.0.0 0 0 0
Expected behavior
For:
Ethernet
└─ 802.1Q
└─ IPv4
└─ TCP/UDP
with:
flowprobe params record l3 l4
flowprobe feature add-del <interface> l2 rx
flowprobe should select:
and export normal L3/L4 information:
10.224.240.107 -> 10.144.149.3
protocol TCP
src-port 80
dst-port 39510
Actual behavior
For an 802.1Q-tagged IPv4 packet:
is selected instead.
The resulting flow contains:
0.0.0.0 -> 0.0.0.0
protocol 0
src-port 0
dst-port 0
Root cause
The issue appears to be caused by the order in which the Ethernet type is evaluated in:
src/plugins/flowprobe/node.c
flowprobe_node_fn() reads the EtherType directly from the outer Ethernet header:
ethernet_header_t *eh0 = vlib_buffer_get_current (b0);
u16 ethertype0 = clib_net_to_host_u16 (eh0->type);
flowprobe_get_variant (
which,
fm->context[which].flags,
ethertype0);
For an untagged IPv4 packet:
so flowprobe_get_variant() correctly selects:
For an IEEE 802.1Q-tagged packet, the outer EtherType is:
so flowprobe_get_variant() selects:
instead.
The inner EtherType is parsed only later inside add_to_flow_record_state():
if (ethertype == ETHERNET_TYPE_VLAN)
{
ethernet_vlan_header_tv_t *ethv =
(ethernet_vlan_header_tv_t *) (&(eth->type));
while (clib_net_to_host_u16 (ethv->type) == ETHERNET_TYPE_VLAN)
{
ethv++;
l3_hdr_offset += sizeof (ethernet_vlan_header_tv_t);
}
k.ethertype = ethertype =
clib_net_to_host_u16 ((ethv)->type);
}
At that point the inner EtherType is correctly identified as IPv4, but the flow variant has already been selected as FLOW_VARIANT_L2.
Earlier in the same function:
collect_ip4 =
which == FLOW_VARIANT_L2_IP4 ||
which == FLOW_VARIANT_IP4;
therefore evaluates to false.
As a result, IPv4 addresses, IP protocol and TCP/UDP ports are not copied into the flow key.
Workaround
A workaround without modifying VPP is to let ethernet-input process the VLAN first and then attach flowprobe to the IPv4 feature arc.
Example:
create sub-interfaces Eth1_AB1 9999 default
set interface state Eth1_AB1.9999 up
flowprobe feature add-del Eth1_AB1 ip4 rx
flowprobe feature add-del Eth1_AB1.9999 ip4 rx
For mirrored/SPAN traffic another issue appears.
SPAN preserves the destination MAC address of the original packet. This MAC normally does not belong to the VPP monitoring interface.
The packet is therefore rejected by VPP with:
ethernet-input: l3 mac mismatch
NIC promiscuous mode alone is not sufficient because VPP performs its own L3 MAC validation after packet reception.
The mirrored destination MAC can be added as a secondary MAC:
set interface promiscuous on Eth1_AB1
set interface secondary-mac-address Eth1_AB1 5c:5e:ab:4b:da:00 add
With this configuration the packet reaches the IPv4 feature arc and:
flowprobe feature add-del Eth1_AB1.9999 ip4 rx
correctly generates L3/L4 IPFIX records.
This workaround works, but it is not ideal for passive monitoring because destination MAC addresses may vary.
Proposed fix
The flow variant should be selected using the inner EtherType after stripping VLAN headers, rather than using the outer Ethernet EtherType.
For:
Ethernet
EtherType 0x8100
VLAN 2196
EtherType 0x0800
IPv4
flowprobe should use:
when deciding between:
FLOW_VARIANT_L2
FLOW_VARIANT_L2_IP4
FLOW_VARIANT_L2_IP6
Proposed patch
diff --git a/src/plugins/flowprobe/node.c b/src/plugins/flowprobe/node.c
--- a/src/plugins/flowprobe/node.c
+++ b/src/plugins/flowprobe/node.c
@@ -121,14 +121,31 @@
static inline flowprobe_variant_t
flowprobe_get_variant (flowprobe_variant_t which,
- flowprobe_record_t flags, u16 ethertype)
+ flowprobe_record_t flags, ethernet_header_t *eth)
{
if (which == FLOW_VARIANT_L2
&& (flags & FLOW_RECORD_L3 || flags & FLOW_RECORD_L4))
- return ethertype == ETHERNET_TYPE_IP6 ? FLOW_VARIANT_L2_IP6 : ethertype ==
- ETHERNET_TYPE_IP4 ? FLOW_VARIANT_L2_IP4 : FLOW_VARIANT_L2;
+ {
+ u16 ethertype = clib_net_to_host_u16 (eth->type);
+
+ /*
+ * Resolve the inner EtherType before selecting the flow variant.
+ * add_to_flow_record_state() already performs equivalent VLAN
+ * processing when extracting L3/L4 fields.
+ */
+ if (ethertype == ETHERNET_TYPE_VLAN)
+ {
+ ethernet_vlan_header_tv_t *ethv =
+ (ethernet_vlan_header_tv_t *) (ð->type);
+
+ while (clib_net_to_host_u16 (ethv->type) ==
+ ETHERNET_TYPE_VLAN)
+ ethv++;
+
+ ethertype = clib_net_to_host_u16 (ethv->type);
+ }
+
+ return ethertype == ETHERNET_TYPE_IP6 ? FLOW_VARIANT_L2_IP6 :
+ ethertype == ETHERNET_TYPE_IP4 ? FLOW_VARIANT_L2_IP4 :
+ FLOW_VARIANT_L2;
+ }
+
return which;
}
@@ -746,23 +763,19 @@ flowprobe_node_fn (...)
len0 = vlib_buffer_length_in_chain (vm, b0);
ethernet_header_t *eh0 = vlib_buffer_get_current (b0);
- u16 ethertype0 = clib_net_to_host_u16 (eh0->type);
if (PREDICT_TRUE ((b0->flags & VNET_BUFFER_F_FLOW_REPORT) == 0))
add_to_flow_record_state (
vm, node, fm, b0, timestamp, len0,
flowprobe_get_variant (which, fm->context[which].flags,
- ethertype0),
+ eh0),
direction, 0);
len1 = vlib_buffer_length_in_chain (vm, b1);
ethernet_header_t *eh1 = vlib_buffer_get_current (b1);
- u16 ethertype1 = clib_net_to_host_u16 (eh1->type);
if (PREDICT_TRUE ((b1->flags & VNET_BUFFER_F_FLOW_REPORT) == 0))
add_to_flow_record_state (
vm, node, fm, b1, timestamp, len1,
flowprobe_get_variant (which, fm->context[which].flags,
- ethertype1),
+ eh1),
direction, 0);
@@ -790,8 +803,7 @@ flowprobe_node_fn (...)
len0 = vlib_buffer_length_in_chain (vm, b0);
ethernet_header_t *eh0 = vlib_buffer_get_current (b0);
- u16 ethertype0 = clib_net_to_host_u16 (eh0->type);
if (PREDICT_TRUE ((b0->flags & VNET_BUFFER_F_FLOW_REPORT) == 0))
{
@@ -801,8 +813,7 @@ flowprobe_node_fn (...)
add_to_flow_record_state (
vm, node, fm, b0, timestamp, len0,
flowprobe_get_variant (which, fm->context[which].flags,
- ethertype0),
+ eh0),
direction, t);
}
This intentionally follows the same VLAN parsing logic that already exists in add_to_flow_record_state().
Possible cleaner implementation
Instead of duplicating VLAN parsing logic in two places, a cleaner long-term implementation would introduce a helper that returns:
- inner EtherType;
- L3 header offset.
The same helper could then be used by:
flowprobe_get_variant();
add_to_flow_record_state().
For example:
static_always_inline u16
flowprobe_get_inner_ethertype (ethernet_header_t *eth, u16 *l3_offset);
This would avoid maintaining two separate VLAN parsing implementations.
It could also simplify support for additional encapsulations such as IEEE 802.1ad.
Suggested regression tests
The flowprobe tests should cover at least:
Ethernet / IPv4 / TCP
Ethernet / IPv4 / UDP
Ethernet / 802.1Q / IPv4 / TCP
Ethernet / 802.1Q / IPv4 / UDP
Ethernet / 802.1Q / IPv6 / TCP
Ethernet / 802.1Q / IPv6 / UDP
It would also be useful to test stacked VLAN tags:
Ethernet / 802.1Q / 802.1Q / IPv4 / TCP
because the current add_to_flow_record_state() implementation already attempts to process multiple VLAN headers.
For:
flowprobe params record l3 l4
tagged and untagged packets should produce equivalent L3/L4 flow keys.
Impact
This issue affects passive monitoring deployments where VPP flowprobe is attached at L2, including:
- SPAN/mirror ports;
- optical TAP traffic;
- trunk monitoring;
- passive IPFIX probes;
- packet-broker outputs carrying multiple VLANs.
In such deployments it is undesirable to create a VPP subinterface for every monitored VLAN simply to allow flowprobe to extract IPv4/IPv6 and TCP/UDP fields.
The L2 flowprobe node already receives the complete Ethernet frame, and add_to_flow_record_state() already contains VLAN parsing logic.
Therefore resolving the inner EtherType before selecting the flow variant appears to be the appropriate fix.
flowprobe: L2 RX with L3/L4 records fails for 802.1Q tagged traffic
Summary
flowprobedoes not correctly collect L3/L4 fields when it is attached as an L2 RX feature and receives 802.1Q-tagged IPv4 traffic.With the following configuration:
untagged IPv4 traffic is classified as
FLOW_VARIANT_L2_IP4, but VLAN-tagged IPv4 traffic is classified only asFLOW_VARIANT_L2.As a result, IPFIX records contain empty L3/L4 fields:
and the collector receives IPFIX templates but no useful flow data.
Environment
Traffic is received through a DPDK interface connected to a SPAN/TAP monitoring port.
The issue was reproduced with normal IEEE 802.1Q tagged IPv4/TCP traffic.
Configuration
show flowprobeoutput:Observed behavior
VPP correctly decodes the packet at
dpdk-input:However, immediately afterwards
flowprobereports:The flow table contains empty entries:
Example:
Expected behavior
For:
with:
flowprobeshould select:and export normal L3/L4 information:
Actual behavior
For an 802.1Q-tagged IPv4 packet:
is selected instead.
The resulting flow contains:
Root cause
The issue appears to be caused by the order in which the Ethernet type is evaluated in:
flowprobe_node_fn()reads the EtherType directly from the outer Ethernet header:For an untagged IPv4 packet:
so
flowprobe_get_variant()correctly selects:For an IEEE 802.1Q-tagged packet, the outer EtherType is:
so
flowprobe_get_variant()selects:instead.
The inner EtherType is parsed only later inside
add_to_flow_record_state():At that point the inner EtherType is correctly identified as IPv4, but the flow variant has already been selected as
FLOW_VARIANT_L2.Earlier in the same function:
therefore evaluates to false.
As a result, IPv4 addresses, IP protocol and TCP/UDP ports are not copied into the flow key.
Workaround
A workaround without modifying VPP is to let
ethernet-inputprocess the VLAN first and then attach flowprobe to the IPv4 feature arc.Example:
For mirrored/SPAN traffic another issue appears.
SPAN preserves the destination MAC address of the original packet. This MAC normally does not belong to the VPP monitoring interface.
The packet is therefore rejected by VPP with:
NIC promiscuous mode alone is not sufficient because VPP performs its own L3 MAC validation after packet reception.
The mirrored destination MAC can be added as a secondary MAC:
With this configuration the packet reaches the IPv4 feature arc and:
correctly generates L3/L4 IPFIX records.
This workaround works, but it is not ideal for passive monitoring because destination MAC addresses may vary.
Proposed fix
The flow variant should be selected using the inner EtherType after stripping VLAN headers, rather than using the outer Ethernet EtherType.
For:
flowprobe should use:
when deciding between:
Proposed patch
This intentionally follows the same VLAN parsing logic that already exists in
add_to_flow_record_state().Possible cleaner implementation
Instead of duplicating VLAN parsing logic in two places, a cleaner long-term implementation would introduce a helper that returns:
The same helper could then be used by:
flowprobe_get_variant();add_to_flow_record_state().For example:
This would avoid maintaining two separate VLAN parsing implementations.
It could also simplify support for additional encapsulations such as IEEE 802.1ad.
Suggested regression tests
The flowprobe tests should cover at least:
It would also be useful to test stacked VLAN tags:
because the current
add_to_flow_record_state()implementation already attempts to process multiple VLAN headers.For:
tagged and untagged packets should produce equivalent L3/L4 flow keys.
Impact
This issue affects passive monitoring deployments where VPP flowprobe is attached at L2, including:
In such deployments it is undesirable to create a VPP subinterface for every monitored VLAN simply to allow flowprobe to extract IPv4/IPv6 and TCP/UDP fields.
The L2 flowprobe node already receives the complete Ethernet frame, and
add_to_flow_record_state()already contains VLAN parsing logic.Therefore resolving the inner EtherType before selecting the flow variant appears to be the appropriate fix.