MDB Snapshotter - Developer Guide

## 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__*