Deploying QinQ (Double VLAN 802.1ad) on Linux: Multi-Tenant Traffic Isolation and VLAN Space Expansion

Network tutorial - IT technology blog
Network tutorial - IT technology blog

When the number of tenants grows beyond a few dozen, the 4094-VLAN address space of 802.1Q starts to feel cramped. QinQ — also known as Double VLAN, 802.1ad, or 802.1Q-in-802.1Q — solves this by encapsulating an additional outer VLAN tag, expanding the total available VLAN combinations to roughly 16 million. This is a foundational technique in ISP infrastructure, cloud provider environments, and any setup requiring clean traffic isolation between multiple tenants over a single physical link.

Quick Start — Set Up QinQ in 5 Minutes

Let’s jump straight into a practical example. Assume you have an eth0 interface and need to create a QinQ interface with outer VLAN (S-VLAN) 100 and inner VLAN (C-VLAN) 10.

Step 1: Verify Kernel Support for 8021q

# Check and load the kernel module
modinfo 8021q
sudo modprobe 8021q
lsmod | grep 8021q

Step 2: Create the S-VLAN (outer tag — provider side)

# Create the outer VLAN interface with 802.1ad protocol
sudo ip link add link eth0 name eth0.100 type vlan proto 802.1ad id 100
sudo ip link set eth0.100 up

Step 3: Create the C-VLAN (inner tag — tenant side) stacked on the S-VLAN

# Create the inner VLAN stacked on top of the outer VLAN
sudo ip link add link eth0.100 name eth0.100.10 type vlan proto 802.1Q id 10
sudo ip link set eth0.100.10 up

# Assign an IP and verify
sudo ip addr add 192.168.10.1/24 dev eth0.100.10
ip addr show eth0.100.10

Step 4: Confirm QinQ Is Working

# View the nested interface structure
ip link show | grep -E 'eth0|vlan'

# Capture frames to confirm the double tag appears on the wire
sudo tcpdump -i eth0 -n -e 'vlan' 2>/dev/null | head -20

If you see output like 802.1Q vlan #100, p 0, 802.1Q vlan #10 in tcpdump, QinQ is working correctly.

Understanding QinQ from the Inside Out

Frame Structure vs. Standard 802.1Q

Standard 802.1Q Frame:
| Dst MAC | Src MAC | 0x8100 | PCP+DEI+VID (4B) | EtherType | Payload |

QinQ (802.1ad) Frame:
| Dst MAC | Src MAC | 0x88A8 | S-VID outer (4B) | 0x8100 | C-VID inner (4B) | EtherType | Payload |

EtherType 0x88A8 is the identifying marker for the 802.1ad outer tag. Switches and routers use this to distinguish QinQ frames from standard 802.1Q frames.

S-VLAN and C-VLAN — Who Manages What?

  • S-VLAN (Service VLAN / outer tag): Assigned and managed by the service provider. Each tenant receives a dedicated S-VLAN and cannot modify this value themselves.
  • C-VLAN (Customer VLAN / inner tag): Falls within the tenant’s own management scope. Tenant A and Tenant B can both use C-VLAN 10 without conflict — because their S-VLANs are already different.

The key insight: each tenant gets its own fully independent set of 4094 C-VLANs. Total capacity: 4094 S-VLANs × 4094 C-VLANs ≈ 16 million combinations.

Real-World Multi-Tenant Configuration

Scenario: 3 Tenants Sharing eth0

#!/bin/bash
# setup-qinq-tenants.sh

PHYS_IF="eth0"

# --- Tenant A: S-VLAN 100 ---
ip link add link $PHYS_IF name ${PHYS_IF}.100 type vlan proto 802.1ad id 100
ip link set ${PHYS_IF}.100 up

# C-VLAN 10 (management), C-VLAN 20 (production)
ip link add link ${PHYS_IF}.100 name ${PHYS_IF}.100.10 type vlan proto 802.1Q id 10
ip link add link ${PHYS_IF}.100 name ${PHYS_IF}.100.20 type vlan proto 802.1Q id 20
ip link set ${PHYS_IF}.100.10 up
ip link set ${PHYS_IF}.100.20 up
ip addr add 10.100.10.1/24 dev ${PHYS_IF}.100.10
ip addr add 10.100.20.1/24 dev ${PHYS_IF}.100.20

# --- Tenant B: S-VLAN 200 ---
ip link add link $PHYS_IF name ${PHYS_IF}.200 type vlan proto 802.1ad id 200
ip link set ${PHYS_IF}.200 up

# C-VLAN 10 — same ID as Tenant A but completely independent
ip link add link ${PHYS_IF}.200 name ${PHYS_IF}.200.10 type vlan proto 802.1Q id 10
ip link set ${PHYS_IF}.200.10 up
ip addr add 10.200.10.1/24 dev ${PHYS_IF}.200.10

# --- Tenant C: S-VLAN 300 ---
ip link add link $PHYS_IF name ${PHYS_IF}.300 type vlan proto 802.1ad id 300
ip link set ${PHYS_IF}.300 up
ip link add link ${PHYS_IF}.300 name ${PHYS_IF}.300.10 type vlan proto 802.1Q id 10
ip link set ${PHYS_IF}.300.10 up
ip addr add 10.300.10.1/24 dev ${PHYS_IF}.300.10

echo "QinQ interfaces ready for 3 tenants"
ip link show type vlan

Persisting Configuration Across Reboots with systemd-networkd

Configuration applied directly with ip link is lost after a reboot. For persistent configuration, create .netdev and .network files:

# /etc/systemd/network/10-svlan100.netdev
[NetDev]
Name=eth0.100
Kind=vlan

[VLAN]
Protocol=802.1ad
Id=100
# /etc/systemd/network/20-cvlan100-10.netdev
[NetDev]
Name=eth0.100.10
Kind=vlan

[VLAN]
Protocol=802.1q
Id=10
# /etc/systemd/network/20-cvlan100-10.network
[Match]
Name=eth0.100.10

[Network]
Address=10.100.10.1/24
sudo systemctl restart systemd-networkd
networkctl status eth0.100.10

Practical Tips from Operational Experience

MTU — The Most Common QinQ Pitfall

Each VLAN tag adds 4 bytes to the header. QinQ with 2 tags means 8 bytes of overhead. If the physical interface MTU stays at 1500, large frames will be silently dropped or fragmented without any clear warning — making this extremely difficult to diagnose.

# Increase MTU on the physical interface to accommodate the double tag
sudo ip link set eth0 mtu 1508   # 1500 + 4 (outer tag) + 4 (inner tag)

# Verify current MTU on the interfaces
ip link show eth0 | grep mtu
ip link show eth0.100.10 | grep mtu

Debugging an Intermittent Packet Loss Issue

The hardest network issue I ever debugged was intermittent packet loss that only appeared during peak hours — everything was perfectly fine in the early morning and late evening. My initial assumption was bandwidth saturation, but the metrics showed plenty of headroom.

After several days of direct wire captures with tcpdump, I finally tracked it down: an MTU mismatch on the QinQ interface. Large frames from SSH sessions and file transfers were being dropped. During peak hours, many concurrent connections were sending near-maximum-size frames, which exposed the problem — during off-peak hours, nobody was hitting that threshold.

How to pinpoint the exact MTU threshold:

# The -M do flag enables Don't Fragment — dropped frames will immediately report an error instead of failing silently
ping -M do -s 1472 192.168.10.2   # 1472 payload + 28 (IP+ICMP header) = 1500 bytes
ping -M do -s 1464 192.168.10.2   # Decrease gradually to find the threshold
ping -M do -s 1492 192.168.10.2

# If you receive "Frag needed" or a timeout → MTU is too small
# Find the largest value that doesn't get dropped — that's the actual path MTU

Checking Whether Your NIC Supports 802.1ad Hardware Offload

Not all NICs support hardware offload for 802.1ad. If yours doesn’t, the kernel will still handle it in software, but throughput will take a hit under high traffic.

# Check offload features
ethtool -k eth0 | grep -E 'vlan|tx-vlan|rx-vlan'

# Check whether the NIC supports rx-vlan-filter
ethtool -k eth0 | grep vlan-filter

Cleaning Up Interfaces When No Longer Needed

# Remove in order from inner to outer (inner first, then outer)
sudo ip link del eth0.100.10
sudo ip link del eth0.100.20
sudo ip link del eth0.100

# Remove all VLAN interfaces at once
for iface in $(ip link show type vlan | grep -oP '^\d+: \K[\w.]+'); do
  ip link del "$iface" 2>/dev/null
done

QinQ is a powerful tool, but it demands discipline in MTU management and careful tracking of nested interface structures. Once you understand the S-VLAN → C-VLAN flow, you can scale multi-tenant infrastructure without adding hardware or reconfiguring your entire network.

Share: