🌉 MDB Snapshotter

RTNetlink Multicast Database Query Tool for Bridge IGMP/MLD Snooping

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
Note: This tool uses the RTNetlink protocol directly via CFFI-compiled C code for maximum performance and reliability. It requires root privileges to access the netlink socket.

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
Python 3.12+ Users: The setuptools package is required for CFFI compilation on Python 3.12 and later.

Quick Start

# Make the script executable
chmod +x mdbsnapshotter.py

# Run with root privileges
sudo python3 mdbsnapshotter.py --summary

Usage Examples

Basic Commands

View all multicast groups (JSON output):
sudo python3 mdbsnapshotter.py
Human-readable summary:
sudo python3 mdbsnapshotter.py --summary
Filter by specific bridge:
sudo python3 mdbsnapshotter.py --summary --bridge br0
Show only IPv4 multicast groups:
sudo python3 mdbsnapshotter.py --summary --ipv4
Show only IPv6 multicast groups:
sudo python3 mdbsnapshotter.py --summary --ipv6
Show only router ports:
sudo python3 mdbsnapshotter.py --summary --routers
Debug mode for troubleshooting:
sudo python3 mdbsnapshotter.py --debug --summary
Export to JSON file:
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

  1. Python creates MDBQuery context manager
  2. C code opens netlink socket (AF_NETLINK, NETLINK_ROUTE)
  3. Send RTM_GETMDB request with NLM_F_DUMP flag
  4. Kernel responds with RTM_NEWMDB messages (type 86)
  5. C code parses netlink attributes (MDBA_MDB, MDBA_ROUTER)
  6. Extract br_mdb_entry structures and extended attributes
  7. Convert to Python dictionaries with decoded fields
  8. 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)
Kernel Compatibility: The tool handles message type variations across kernel versions (RTM_NEWMDB can be 84 or 86) and gracefully tracks unknown attributes for forward compatibility.

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:

  1. Verify entries exist: bridge mdb show
  2. Check kernel version: uname -r (need 3.10+)
  3. Enable debug mode: --debug
  4. Verify root access: sudo required
  5. 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 --bridge filter to query specific bridges
  • Use --ipv4 or --ipv6 to filter by family
  • Pipe JSON output to jq for 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)
    }
  ]
}
Best Practice: Always use the context manager pattern (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:

  1. Check if entries exist: bridge mdb show
  2. Verify multicast snooping enabled: cat /sys/class/net/BRIDGE/bridge/multicast_snooping
  3. Check kernel support: uname -r (need 3.10+)
  4. Clear CFFI cache: rm -rf __pycache__/_cffi__*
  5. 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__*