Skip to content

Flowprobe: L2 RX with L3/L4 records fails for 802.1Q tagged traffic #3733

Description

@maxbur89

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:

0.0.0.0 -> 0.0.0.0 0 0 0

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:

FLOW_VARIANT_L2_IP4

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:

FLOW_VARIANT_L2

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:

EtherType = 0x0800

so flowprobe_get_variant() correctly selects:

FLOW_VARIANT_L2_IP4

For an IEEE 802.1Q-tagged packet, the outer EtherType is:

EtherType = 0x8100

so flowprobe_get_variant() selects:

FLOW_VARIANT_L2

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:

0x0800

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 *) (&eth->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:

  1. flowprobe_get_variant();
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions