Automating VLAN Management & Q-in-Q Tagging with ezsnmp¶
Note
Published: October 26, 2026
Authors: Written by Gemini 3.8 Flash, Co-Authored & Reviewed by Carlos Santos (@carlkidcrypto)
Estimated Reading Time: 5 minutes
Tags: Python • Snmp • Vlan • Q-In-Q • Q-Bridge-Mib • Ezsnmp • Network-Automation
The Complexity of Multi-Tenant Layer-2 Networks¶
In enterprise campus environments, data center fabrics, and carrier Ethernet networks, Virtual Local Area Networks (VLANs, IEEE 802.1Q) partition physical switch infrastructure into isolated broadcast domains.
In service provider and multi-tenant scenarios, this architecture extends to Q-in-Q VLAN stacking (IEEE 802.1ad), where customer VLAN tags (C-VLANs) are encapsulated within provider service tags (S-VLANs) to traverse transit trunks without tag collisions.
Auditing and validating VLAN deployments across hundreds of switches presents formidable operational hurdles:
Trunk Port Misconfigurations: Forgetting to add newly provisioned VLANs to uplink trunk egress lists.
Native VLAN Mismatches: Desynchronized Port Default VLAN IDs (PVIDs) causing spanning-tree loops and traffic leakage.
Bitmask Complexity: Standard SNMP MIBs encode switch port allocations as binary octet bitmasks rather than plain integers or strings.
ezsnmp provides native Net-SNMP C bindings to query IEEE standard MIBs, parse raw binary bitmasks, and audit complex Layer-2 configurations in fractions of a second.
Understanding the Q-BRIDGE-MIB & PortLists¶
The standard Q-BRIDGE-MIB (RFC 4363) defines two primary tables for
VLAN inspection:
1. dot1qPortVlanTable (OID .1.3.6.1.2.1.17.7.1.4.5)¶
Maintains per-port ingress parameters:
``dot1qPvid``: The Port Default VLAN ID. Incoming untagged or priority- tagged frames are assigned this VLAN ID.
``dot1qPortAcceptableFrameTypes``: Determines whether a port admits all frames (access/hybrid) or strictly tagged frames (trunk).
2. dot1qVlanCurrentTable (OID .1.3.6.1.2.1.17.7.1.4.2)¶
Maintains active VLAN membership using binary PortList octet strings:
``dot1qVlanCurrentEgressPorts``: Binary bitmask indicating which switch ports are authorized to transmit frames for the given VLAN.
``dot1qVlanCurrentUntaggedPorts``: Binary bitmask indicating ports where egressing frames strip their 802.1Q tag (access ports).
Decoding a PortList Bitmask¶
In a PortList, each bit represents a bridge port number. The most
significant bit (bit 7) of the first byte represents Port 1; bit 6 represents
Port 2, continuing sequentially through all switch ports.
Practical Implementation: VLAN Auditor & Decoder¶
Below is a complete script demonstrating how to query Q-BRIDGE-MIB via
ezsnmp, decode binary port bitmasks, and construct a comprehensive VLAN
allocation report:
from ezsnmp import Session
from ezsnmp.exceptions import GenericError
def parse_hex_or_bytes(val: str | bytes) -> bytes:
"""Convert ezsnmp Hex-STRING representation or raw octets to bytes."""
if isinstance(val, bytes):
return val
cleaned = val.replace(" ", "").replace(":", "").replace("-", "")
try:
return bytes.fromhex(cleaned)
except ValueError:
return val.encode("latin1")
def decode_port_bitmask(raw_val: str | bytes) -> list[int]:
"""Decode an SNMP PortList (Hex-STRING or octets) into active port numbers."""
raw_bytes = parse_hex_or_bytes(raw_val)
active_ports = []
for byte_idx, byte_val in enumerate(raw_bytes):
for bit_idx in range(8):
# Check bits from MSB (0x80) to LSB (0x01)
if byte_val & (0x80 >> bit_idx):
port_num = (byte_idx * 8) + bit_idx + 1
active_ports.append(port_num)
return active_ports
def audit_switch_vlans(hostname: str, community: str = "public"):
session = Session(
hostname=hostname,
community=community,
version=2,
timeout=2,
retries=1,
print_hex_strings=True,
set_max_repeaters_to_num=25
)
try:
# 1. Collect Port Default VLAN IDs (Native VLANs)
pvid_vars = session.bulk_walk("dot1qPvid")
pvids = {v.index: v.value for v in pvid_vars}
# 2. Bulk-walk VLAN Egress and Untagged bitmasks
egress_vars = session.bulk_walk("dot1qVlanCurrentEgressPorts")
untagged_vars = session.bulk_walk("dot1qVlanCurrentUntaggedPorts")
untagged_map = {
v.index: set(decode_port_bitmask(v.value))
for v in untagged_vars
}
vlan_report = []
for var in egress_vars:
# OID format: [time_filter].[vlan_id]
vlan_id = var.index.split(".")[-1]
egress_ports = decode_port_bitmask(var.value)
untagged_ports = untagged_map.get(var.index, set())
tagged_ports = [
p for p in egress_ports if p not in untagged_ports
]
vlan_report.append({
"vlan_id": vlan_id,
"tagged_ports": tagged_ports,
"untagged_ports": list(untagged_ports),
})
return pvids, vlan_report
except GenericError as err:
print(f"[{hostname}] VLAN audit failed: {err}")
return {}, []
if __name__ == "__main__":
pvids, vlans = audit_switch_vlans("dist-sw01.internal")
print("--- Port Native VLANs (PVID) ---")
for port, pvid in sorted(pvids.items(), key=lambda x: int(x[0])):
print(f" Port {port:>2}: Native VLAN {pvid}")
print("\n--- VLAN Membership Matrix ---")
for v in vlans:
tagged = ",".join(map(str, v["tagged_ports"])) or "None"
untagged = ",".join(map(str, v["untagged_ports"])) or "None"
print(
f" VLAN {v['vlan_id']:>4} | "
f"Tagged: [{tagged}] | Untagged: [{untagged}]"
)
Inspecting Q-in-Q (VLAN Stacking) Configurations¶
In provider edge (PE) and carrier Ethernet switches, ports configured for Q-in-Q tunneling encapsulate all incoming customer traffic inside a service tag (S-VLAN).
1. Tunnel Port Verification¶
Verify that the customer-facing port has dot1qPortAcceptableFrameTypes set
to admitAll (admitting untagged and single-tagged frames).
2. S-VLAN Egress Validation¶
Ensure uplink trunk interfaces belong to the S-VLAN’s
dot1qVlanCurrentEgressPorts mask in tagged mode, guaranteeing the outer
provider tag is preserved across core hops.
Production Best Practices¶
Hexadecimal & Binary Decoding: Because SNMP
PortListbitmasks can be formatted as hexadecimal strings (e.g.'80 00 00 00') or raw octet sequences, decode hex strings usingbytes.fromhex()before evaluating bit positions to ensure accurate port membership calculations.Correlate with ifDescr: Use
dot1dBasePortIfIndexto map internal bridge port numbers back to physical interface names (e.g.TenGigabitEthernet1/0/1).Automate Misconfiguration Audits: Flag trunk ports where PVIDs do not match between peer endpoints to catch spanning-tree desynchronizations.
Migration Notes: Easy SNMP vs ezsnmp¶
Legacy easysnmp had serious bugs with binary OCTET STRING decoding in
Python 3:
Null-Byte Truncation: When a
PortListbitmask contained null bytes (common when early ports on a linecard are inactive), legacy C code treated\x00as an ASCII string terminator, corrupting the bitmask.Binary Integrity in ezsnmp:
ezsnmp’s modernized C++17 layer preserves raw binary payload lengths explicitly, ensuring complete 512-bit port bitmasks are transferred into Python with 100% fidelity.
Useful Links & Community¶
Source Repository: carlkidcrypto/ezsnmp on GitHub
PyPI Distribution: ezsnmp Package on PyPI
Documentation: ezsnmp Online Documentation
Blog & Updates: Carlos Santos Blog Updates
Warning
AI-generated content may contain misinformation or mistakes. A human does review this, but we are only human.