Introduction
mdbsnapshotter.py is a comprehensive tool for querying and inspecting Linux bridge multicast database (MDB) entries. It provides detailed information about IGMP (IPv4) and MLD (IPv6) snooping on bridge devices, including group memberships, router ports, and forwarding states.
What is Multicast Database (MDB)?
The Multicast Database (MDB) is a kernel data structure that tracks multicast group memberships on bridge ports. When IGMP/MLD snooping is enabled on a bridge, the kernel monitors multicast group subscriptions and only forwards multicast traffic to ports with interested listeners, reducing unnecessary network traffic.
Key Features
- Complete MDB Inspection: Query all multicast groups across all bridges or filter by specific bridge
- IPv4 & IPv6 Support: Full IGMP and MLD snooping entry support
- L2 Multicast: MAC-based multicast group tracking
- Router Port Detection: Identify and track multicast router ports
- State Tracking: Distinguish between TEMPORARY (learned) and PERMANENT (static) entries
- Timer Information: View expiration timers for dynamic entries
- VLAN Awareness: Support for VLAN-tagged multicast forwarding
- JSON Export: Machine-readable output for automation
- Human-Readable Summary: Formatted output with interface name resolution
- Unknown Attribute Tracking: Forward compatibility with future kernel versions
Use Cases
- Network Monitoring: Track multicast group subscriptions in real-time
- Troubleshooting: Diagnose IGMP/MLD snooping issues
- Automation: Integrate with network management systems via JSON output
- Performance Analysis: Identify multicast traffic patterns
- Security Auditing: Monitor unauthorized multicast subscriptions
Installation
Requirements
- Python 3.8 or higher
- Linux kernel 3.10+ with MDB support
- Root/sudo access for netlink operations
- GCC compiler for CFFI compilation
Python Dependencies
pip install cffi setuptools
Quick Start
# Make the script executable
chmod +x mdbsnapshotter.py
# Run with root privileges
sudo python3 mdbsnapshotter.py --summary
Usage Examples
Basic Commands
sudo python3 mdbsnapshotter.py
sudo python3 mdbsnapshotter.py --summary
sudo python3 mdbsnapshotter.py --summary --bridge br0
sudo python3 mdbsnapshotter.py --summary --ipv4
sudo python3 mdbsnapshotter.py --summary --ipv6
sudo python3 mdbsnapshotter.py --summary --routers
sudo python3 mdbsnapshotter.py --debug --summary
sudo python3 mdbsnapshotter.py > mdb_snapshot.json
Command-Line Options
| Option | Description |
|---|---|
--summary |
Display human-readable summary instead of JSON |
--bridge NAME |
Filter by specific bridge interface name |
--ipv4 |
Show only IPv4 multicast groups (IGMP) |
--ipv6 |
Show only IPv6 multicast groups (MLD) |
--routers |
Show only router ports |
--debug |
Enable debug output for troubleshooting |
--no-unknown-attrs |
Disable unknown attribute tracking |
Understanding the Output
MDB Entry States
| State | Value | Description |
|---|---|---|
| TEMPORARY | 0 | Dynamically learned via IGMP/MLD, will expire |
| PERMANENT | 0x80 | Statically configured, never expires |
MDB Entry Flags
| Flag | Value | Description |
|---|---|---|
| OFFLOAD | 0x01 | Entry offloaded to hardware |
| FAST_LEAVE | 0x02 | Fast leave enabled for this entry |
| STAR_EXCL | 0x04 | Exclude mode for source-specific multicast |
| BLOCKED | 0x08 | Traffic blocked for this entry |
Router Port Types
| Type | Value | Description |
|---|---|---|
| DISABLED | 0 | Not a router port |
| TEMP_QUERY | 1 | Temporary router port (learned from query) |
| PERMANENT | 2 | Permanently configured router port |
| TEMPORARY | 3 | Temporary router port |
Architecture
System Design
The tool uses a hybrid C/Python architecture for optimal performance and ease of use:
- C Core: Low-level netlink communication and parsing compiled via CFFI
- Python Wrapper: High-level API, formatting, and output generation
- CFFI Bridge: Seamless integration between C and Python with minimal overhead
Data Flow
- Python creates
MDBQuerycontext manager - C code opens netlink socket (
AF_NETLINK, NETLINK_ROUTE) - Send
RTM_GETMDBrequest withNLM_F_DUMPflag - Kernel responds with
RTM_NEWMDBmessages (type 86) - C code parses netlink attributes (MDBA_MDB, MDBA_ROUTER)
- Extract
br_mdb_entrystructures and extended attributes - Convert to Python dictionaries with decoded fields
- Python formats output (JSON or human-readable)
Netlink Message Structure
RTM_NEWMDB (type 86)
├── br_port_msg (family=AF_BRIDGE, ifindex)
└── Attributes (rtattr chain)
├── MDBA_MDB (1)
│ └── MDBA_MDB_ENTRY (1)
│ └── MDBA_MDB_ENTRY_INFO (1)
│ ├── br_mdb_entry (base structure)
│ └── Extended attributes
│ ├── MDBA_MDB_EATTR_TIMER (1)
│ ├── MDBA_MDB_EATTR_GROUP_MODE (3)
│ ├── MDBA_MDB_EATTR_PROTO (6)
│ └── MDBA_MDB_EATTR_RTPROT (5)
└── MDBA_ROUTER (2)
├── MDBA_ROUTER_PORT (1)
└── MDBA_ROUTER_PATTR (2)
├── MDBA_ROUTER_PATTR_TIMER (1)
├── MDBA_ROUTER_PATTR_TYPE (2)
├── MDBA_ROUTER_PATTR_INET_TIMER (3)
├── MDBA_ROUTER_PATTR_INET6_TIMER (4)
└── MDBA_ROUTER_PATTR_VID (5)
Developer Guide
Code Structure
Main Components
- C_SOURCE: Embedded C code for netlink operations
- FFI Definitions: C structure and function declarations
- MDBQuery Class: Python context manager for safe socket handling
- Helper Functions: Attribute decoding and name mapping
- CLI Interface: Argument parsing and output formatting
Adding New Features
1. Adding New Attribute Parsing
To parse a new MDBA attribute:
// In C_SOURCE, add to mdb_entry_t structure:
typedef struct {
// ... existing fields ...
unsigned char new_field;
int has_new_field;
} mdb_entry_t;
// In parse_mdb_entry_attrs():
case MDBA_MDB_EATTR_NEW_ATTR:
if (RTA_PAYLOAD(entry_rta) >= sizeof(unsigned char)) {
entry->new_field = *(unsigned char*)RTA_DATA(entry_rta);
entry->has_new_field = 1;
}
break;
# In Python, update parsing:
if entry.has_new_field:
entry_info['new_field'] = entry.new_field
2. Adding New Output Format
# Add new argument
parser.add_argument('--csv', action='store_true',
help='Output in CSV format')
# Add formatting function
def format_as_csv(mdb_data):
import csv
import sys
writer = csv.writer(sys.stdout)
writer.writerow(['Bridge', 'Group', 'Port', 'State'])
for entry in mdb_data['entries']:
writer.writerow([
entry.get('ifindex'),
entry.get('group'),
entry.get('port_ifindex'),
entry.get('state_name')
])
# Use it
if args.csv:
format_as_csv(mdb_data)
3. Adding Filtering Options
# Add filter argument
parser.add_argument('--state', choices=['temp', 'permanent'],
help='Filter by entry state')
# Apply filter
if args.state:
filter_state = 'TEMPORARY' if args.state == 'temp' else 'PERMANENT'
entries = [e for e in entries
if e.get('state_name') == filter_state]
Debugging
CFFI Compilation Issues
If CFFI fails to compile, check:
- Kernel headers installed:
apt-get install linux-headers-$(uname -r) - GCC compiler available:
apt-get install gcc - CFFI cache cleared:
rm -rf __pycache__/_cffi__* - Python version:
python3 --version(must be 3.8+)
No Entries Returned
If the tool returns 0 entries:
- Verify entries exist:
bridge mdb show - Check kernel version:
uname -r(need 3.10+) - Enable debug mode:
--debug - Verify root access:
sudorequired - Check for netlink permission errors in dmesg
Unknown Attributes
The tool tracks unknown attributes for forward compatibility:
sudo python3 mdbsnapshotter.py | jq '.entries[].unknown_entry_attrs'
If you see unknown attributes, check:
- Kernel version for new features
- Linux kernel documentation for new MDBA_* constants
- Update C_SOURCE with new attribute definitions
Testing
Unit Tests
import unittest
from mdbsnapshotter import MDBQuery, decode_unknown_attrs
class TestMDBQuery(unittest.TestCase):
def test_decode_unknown_attrs(self):
attrs = [1, 2, 0x8001] # 0x8001 is nested
decoded = decode_unknown_attrs(attrs, 'MDBA')
self.assertEqual(len(decoded), 3)
self.assertTrue(decoded[2]['nested'])
def test_mdb_query_requires_root(self):
# Should fail without root
with self.assertRaises(RuntimeError):
with MDBQuery() as mdb:
pass
if __name__ == '__main__':
unittest.main()
Integration Tests
#!/bin/bash
# Create test bridge
ip link add br-test type bridge
ip link set br-test up
# Enable multicast snooping
echo 1 > /sys/class/net/br-test/bridge/multicast_snooping
# Test query
python3 mdbsnapshotter.py --bridge br-test --summary
# Cleanup
ip link del br-test
Performance Optimization
Large Networks
For bridges with thousands of entries:
- Use
--bridgefilter to query specific bridges - Use
--ipv4or--ipv6to filter by family - Pipe JSON output to
jqfor efficient filtering - Consider batch processing with scripts
Memory Usage
The C code uses a dynamic buffer that grows as needed. Initial size is 64KB, doubles when full. For very large MDB tables, you may want to adjust:
// In nl_recv_response()
buf->capacity = 131072; // Start with 128KB instead of 64KB
API Reference
Python API
MDBQuery Class
class MDBQuery:
"""Query multicast database information using RTNETLINK protocol.
Args:
capture_unknown_attrs (bool): Track unknown attributes for
forward compatibility. Default: True
Usage:
with MDBQuery() as mdb:
data = mdb.get_mdb(bridge_ifindex=0)
"""
def __init__(self, capture_unknown_attrs: bool = True)
def get_mdb(self, bridge_ifindex: int = 0) -> Dict[str, Any]:
"""Query MDB entries and router ports.
Args:
bridge_ifindex: Bridge interface index (0 for all bridges)
Returns:
Dictionary with 'entries' and 'routers' lists
Raises:
RuntimeError: If socket creation or query fails
"""
Helper Functions
def decode_unknown_attrs(attr_list: List[int],
attr_type: str = 'MDBA') -> List[Dict[str, Any]]:
"""Decode unknown attribute numbers into human-readable information.
Args:
attr_list: List of attribute numbers
attr_type: Attribute type ('MDBA' or 'MDBA_MDB_EATTR')
Returns:
List of dictionaries with attribute information:
- number: Raw attribute number
- base_number: Number without nested flag
- nested: Boolean indicating if nested
- name: Human-readable name
"""
C API
Core Functions
int nl_create_socket(void);
// Returns: Socket FD or -1 on error
void nl_close_socket(int sock);
// Closes netlink socket
int nl_send_getmdb(int sock, unsigned int* seq_out, int ifindex);
// Returns: Bytes sent or -1 on error
response_buffer_t* nl_recv_response(int sock, unsigned int expected_seq);
// Returns: Response buffer or NULL on error
int nl_parse_mdb_entries(response_buffer_t* buf,
mdb_entry_t** entries, int* count);
// Returns: 0 on success, -1 on error
int nl_parse_mdb_routers(response_buffer_t* buf,
mdb_router_t** routers, int* count);
// Returns: 0 on success, -1 on error
void nl_free_mdb_entries(mdb_entry_t* entries);
void nl_free_mdb_routers(mdb_router_t* routers);
void nl_free_response(response_buffer_t* buf);
Data Structures
typedef struct {
int ifindex; // Bridge interface index
unsigned char state; // Entry state (0=temp, 0x80=permanent)
unsigned char flags; // Entry flags (offload, fast_leave, etc)
unsigned short vid; // VLAN ID
int has_vid; // VLAN present flag
unsigned char addr[16]; // Group address (IPv4/IPv6)
int addr_len; // Address length (4 or 16)
unsigned short addr_proto;// ETH_P_IP (0x0800) or ETH_P_IPV6 (0x86DD)
int has_addr; // Address present flag
int port_ifindex; // Port interface index
int has_port; // Port present flag
unsigned int timer; // Timer in centiseconds
int has_timer; // Timer present flag
unsigned char group_mode;// INCLUDE(1) or EXCLUDE(2)
int has_group_mode; // Group mode present flag
unsigned char proto; // Protocol (IGMP/MLD version)
int has_proto; // Protocol present flag
unsigned char rtprot; // Routing protocol
int has_rtprot; // Routing protocol present flag
unsigned short unknown_mdba_attrs[64]; // Unknown attributes
int unknown_mdba_attrs_count;
unsigned short unknown_entry_attrs[64];
int unknown_entry_attrs_count;
} mdb_entry_t;
JSON Output Schema
{
"entries": [
{
"ifindex": 3, // Bridge interface index
"state": 0, // 0=TEMPORARY, 0x80=PERMANENT
"state_name": "TEMPORARY", // Human-readable state
"flags": 0, // Flags bitmask
"flags_name": "NONE", // Human-readable flags
"port_ifindex": 5, // Port interface index
"vid": 100, // VLAN ID (optional)
"group": "ff02::1:ff5a:d02b", // Multicast group address
"family": "ipv6", // ipv4, ipv6, or l2
"addr_proto": 34525, // ETH_P_* value
"timer": 25987, // Timer in centiseconds
"timer_sec": 259.87, // Timer in seconds
"group_mode": "EXCLUDE", // INCLUDE or EXCLUDE (optional)
"proto": 2, // Protocol version (optional)
"rtprot": 0 // Routing protocol (optional)
}
],
"routers": [
{
"ifindex": 3, // Bridge interface index
"port_ifindex": 5, // Port interface index
"type": "TEMPORARY", // Router port type
"type_value": 3, // Numeric type
"timer": 25987, // Timer in centiseconds (optional)
"timer_sec": 259.87, // Timer in seconds (optional)
"inet_timer": 25987, // IPv4 timer (optional)
"inet_timer_sec": 259.87, // IPv4 timer in seconds (optional)
"inet6_timer": 25987, // IPv6 timer (optional)
"inet6_timer_sec": 259.87, // IPv6 timer in seconds (optional)
"vid": 100 // VLAN ID (optional)
}
]
}
with MDBQuery() as mdb) to ensure proper socket cleanup, even if exceptions occur.
Troubleshooting
Common Issues
Permission Denied
RuntimeError: Failed to create netlink socket
Solution: Run with sudo or as root:
sudo python3 mdbsnapshotter.py
CFFI Compilation Failed
error: command 'gcc' failed with exit status 1
Solutions:
- Install build tools:
apt-get install build-essential python3-dev - Install kernel headers:
apt-get install linux-headers-$(uname -r) - For Python 3.12+:
pip install setuptools
No Entries Found
Verification steps:
- Check if entries exist:
bridge mdb show - Verify multicast snooping enabled:
cat /sys/class/net/BRIDGE/bridge/multicast_snooping - Check kernel support:
uname -r(need 3.10+) - Clear CFFI cache:
rm -rf __pycache__/_cffi__* - Enable debug mode:
--debug
Wrong Message Type
DEBUG: nlmsg_type = 86 (RTM_NEWMDB=84, ...)
Solution: This is normal. The tool handles both message types (84 and 86) automatically. Clear the CFFI cache to recompile:
rm -rf __pycache__/_cffi__*