net_route_info.py - RTNetlink Routing Table Query Module

# net_route_info.py

RTNetlink Routing Table Query Module with C Library via CFFI

Overview: A high-performance Python module for querying Linux routing tables using the RTNetlink protocol. This module provides comprehensive access to kernel routing information for both IPv4 and IPv6, including support for multiple routing tables, multipath routes, and detailed route metadata.

## Architecture & Design

### Design Philosophy

The module is designed around a hybrid architecture that combines the performance of C with the convenience of Python:

- C Core: Low-level RTNetlink communication implemented in C for maximum performance and direct kernel access
- CFFI Bridge: Uses CFFI's `ffi.verify()` for runtime compilation and seamless Python-C integration
- Python Interface: Clean, Pythonic API with type hints and context managers for safe resource management

### Architecture Layers

#### Layer 1: Kernel Interface

Direct communication with Linux kernel via RTNetlink sockets using `RTM_GETROUTE` messages

#### Layer 2: C Implementation

Handles socket management, message construction, response parsing, and attribute extraction

#### Layer 3: CFFI Bridge

Runtime compilation and type conversion between C structures and Python objects

#### Layer 4: Python API

Context manager interface, type conversion, and user-friendly data structures

### Data Flow

    ┌─────────────────────────────────────────────┐
    │  Python Application Code                   │
    └──────────────────┬──────────────────────────┘
                       │ get_routes()
                       ▼
    ┌─────────────────────────────────────────────┐
    │  RoutingTableQuery Class                   │
    │  - Socket management (context manager)     │
    │  - Type conversion (Python ↔ C)            │
    └──────────────────┬──────────────────────────┘
                       │ lib.nl_send_getroute()
                       ▼
    ┌─────────────────────────────────────────────┐
    │  C Library (via CFFI)                      │
    │  - nl_create_socket()                      │
    │  - nl_send_getroute()                      │
    │  - nl_receive_response()                   │
    │  - nl_parse_routes()                       │
    └──────────────────┬──────────────────────────┘
                       │ RTNetlink Protocol
                       ▼
    ┌─────────────────────────────────────────────┐
    │  Linux Kernel Routing Subsystem            │
    │  - Route lookup and filtering              │
    │  - RTM_NEWROUTE messages                   │
    └─────────────────────────────────────────────┘

## Key Features

#### ✓ Dual Stack Support

- Full IPv4 routing tables
- Full IPv6 routing tables
- Mixed query support

#### ✓ Multiple Tables

- Main routing table
- Local routing table
- Custom routing tables (0-255+)

#### ✓ Route Types

- UNICAST (normal routes)
- LOCAL (interface addresses)
- BROADCAST/MULTICAST
- UNREACHABLE/PROHIBIT/BLACKHOLE

#### ✓ Advanced Features

- Multipath routes (ECMP)
- Route metrics and preferences
- Routing protocols (DHCP, BIRD, etc.)
- Cache information

### Route Metadata Extracted

Category | Fields | Description
--- | --- | ---
Addressing | dst, src, gateway, prefsrc | Destination network, source address, next-hop gateway, preferred source
Interface | dev, dev_index | Output interface name and index
Classification | family, type, scope, protocol | IPv4/IPv6, route type, scope (link/host/universe), protocol that installed route
Routing Policy | table, metric, tos, flags | Routing table ID, preference/metric, type of service, route flags
Multipath | multipath[] | Array of nexthop objects for ECMP routes
Cache Info | cacheinfo{} | Cache statistics (clntref, last_use, expires, error, used)

## Requirements & Installation

### System Requirements

Platform: Linux 2.6+
This module requires Linux with RTNetlink support. It will not work on Windows, macOS, or other Unix systems.

### Python Requirements

Requirement | Version | Notes
--- | --- | ---
Python | ≥ 3.8 | Required for type hints and modern syntax
cffi | ≥ 1.0.0 | Required for C library integration
setuptools | Any version | Required for Python 3.12+ due to distutils removal

### Installation

    # Install dependencies
    pip install cffi setuptools

    # For Python 3.12+, setuptools is mandatory
    pip install setuptools  # if not already installed

⚠ Python 3.12+ Note: Python 3.12 removed the `distutils` module from the standard library. CFFI's `ffi.verify()` method requires setuptools on Python 3.12+ to provide the compilation infrastructure that distutils previously offered. The module will automatically check for setuptools availability at import time.

### Permissions

⚠ Root/CAP_NET_ADMIN Required: Querying routing tables via RTNetlink requires either:

- Running as root: `sudo python3 net_route_info.py`
- CAP_NET_ADMIN capability: `setcap cap_net_admin+ep /usr/bin/python3` (not recommended for system Python)

## Usage Examples

### Basic Usage: Query All Routes

    from net_route_info import RoutingTableQuery

    # Using context manager (recommended)
    with RoutingTableQuery() as query:
        routes = query.get_routes()

        for route in routes:
            print(f"Destination: {route['dst']}")
            if 'gateway' in route:
                print(f"  via {route['gateway']}")
            if 'dev' in route:
                print(f"  dev {route['dev']}")
            print(f"  protocol: {route['protocol']}")
            print()

### IPv4-Only Routes

    with RoutingTableQuery() as query:
        ipv4_routes = query.get_routes(family='ipv4')

        # Filter for default route
        default_routes = [r for r in ipv4_routes if r['dst'] == '0.0.0.0/0']

        for route in default_routes:
            print(f"Default gateway: {route.get('gateway', 'N/A')}")

### IPv6-Only Routes

    with RoutingTableQuery() as query:
        ipv6_routes = query.get_routes(family='ipv6')

        # Filter for link-local routes
        link_local = [r for r in ipv6_routes
                      if r['dst'].startswith('fe80:')]

        print(f"Found {len(link_local)} link-local routes")

### Filtering by Routing Table

    with RoutingTableQuery() as query:
        all_routes = query.get_routes()

        # Main table routes only
        main_routes = [r for r in all_routes if r['table'] == 'MAIN']

        # Local table routes only
        local_routes = [r for r in all_routes if r['table'] == 'LOCAL']

        print(f"Main table: {len(main_routes)} routes")
        print(f"Local table: {len(local_routes)} routes")

### Analyzing Multipath Routes

    with RoutingTableQuery() as query:
        routes = query.get_routes()

        # Find ECMP routes
        multipath_routes = [r for r in routes if 'multipath' in r]

        for route in multipath_routes:
            print(f"ECMP route to {route['dst']}:")
            for nexthop in route['multipath']:
                print(f"  - via {nexthop.get('gateway', 'direct')}")
                print(f"    dev {nexthop.get('dev', nexthop['dev_index'])}")
                print(f"    weight {nexthop['weight']}")

### Disabling Unknown Attribute Tracking

    # By default, unknown RTA_* attributes are tracked
    # Disable for slightly better performance
    with RoutingTableQuery(capture_unknown_attrs=False) as query:
        routes = query.get_routes()
        # 'unknown_rta_attrs' field will not be present

### JSON Export

    import json

    with RoutingTableQuery() as query:
        routes = query.get_routes()

        # Pretty-print JSON
        print(json.dumps(routes, indent=2))

        # Save to file
        with open('routes.json', 'w') as f:
            json.dump(routes, f, indent=2)

## API Reference

### RoutingTableQuery Class

class RoutingTableQuery(capture_unknown_attrs: bool = True)

Main class for querying routing tables. Implements context manager protocol for safe socket management.

#### Constructor Parameters

Parameter | Type | Default | Description
--- | --- | --- | ---
`capture_unknown_attrs` | bool | True | Whether to track unknown RTA_* attributes in route entries

#### Methods

get_routes(family: Optional[str] = None) → List[Dict[str, Any]]

Query routing table entries with optional address family filtering.

Parameter | Type | Valid Values | Description
--- | --- | --- | ---
`family` | Optional[str] | 'ipv4', 'ipv6', None | Address family filter. None returns both IPv4 and IPv6

Returns: List of dictionaries, each representing a route entry with complete metadata.

Raises:

- `ValueError` - Invalid family parameter
- `RuntimeError` - Socket creation, send, receive, or parse failure
- `PermissionError` - Insufficient privileges (needs root/CAP_NET_ADMIN)

#### Context Manager Protocol

    # Automatically manages socket lifecycle
    with RoutingTableQuery() as query:
        routes = query.get_routes()
        # Socket is automatically closed on exit

    # Manual socket management (not recommended)
    query = RoutingTableQuery()
    query.__enter__()  # Creates socket
    try:
        routes = query.get_routes()
    finally:
        query.__exit__(None, None, None)  # Closes socket

## Route Information Structure

Each route entry is returned as a dictionary with the following structure:

### Standard Fields

Field | Type | Always Present | Description
--- | --- | --- | ---
`family` | str | ✓ | 'ipv4' or 'ipv6'
`type` | str | ✓ | Route type: UNICAST, LOCAL, BROADCAST, MULTICAST, UNREACHABLE, PROHIBIT, BLACKHOLE, etc.
`protocol` | str | ✓ | Routing protocol: KERNEL, BOOT, STATIC, DHCP, BIRD, ZEBRA, BABEL, BGP, etc.
`scope` | str | ✓ | Route scope: UNIVERSE, SITE, LINK, HOST, NOWHERE
`table` | str/int | ✓ | Routing table: MAIN, LOCAL, DEFAULT, or numeric ID
`dst` | str | ✓ | Destination network in CIDR notation (e.g., '192.168.1.0/24', '0.0.0.0/0', 'fe80::/64')
`dst_len` | int | ✓ | Destination prefix length (0-32 for IPv4, 0-128 for IPv6)
`flags` | int | ✓ | Route flags (bitfield)

### Optional Fields

Field | Type | Description
--- | --- | ---
`gateway` | str | Next-hop gateway IP address
`dev` | str | Output interface name (e.g., 'eth0', 'wlan0')
`dev_index` | int | Output interface index
`metric` | int | Route priority/preference (lower = preferred)
`src` | str | Source address/network for policy routing (CIDR notation)
`prefsrc` | str | Preferred source address for outgoing packets
`table_id` | int | Numeric routing table ID (for tables > 255)
`multipath` | List[Dict] | Array of nexthop objects for ECMP routes
`cacheinfo` | Dict | Cache statistics (clntref, last_use, expires, error, used)
`unknown_rta_attrs` | List[int] | List of unknown RTA_* attribute numbers (if capture_unknown_attrs=True)
`unknown_rta_attrs_decoded` | List[Dict] | Decoded information about unknown attributes

### Multipath Nexthop Structure

When `multipath` field is present, each nexthop has the following structure:

Field | Type | Description
--- | --- | ---
`dev_index` | int | Output interface index
`dev` | str | Output interface name (if resolvable)
`gateway` | str | Next-hop gateway IP (optional)
`weight` | int | Relative weight for load balancing
`flags` | int | Nexthop flags

### Example Route Entry

    {
      "family": "ipv4",
      "type": "UNICAST",
      "protocol": "DHCP",
      "scope": "UNIVERSE",
      "table": "MAIN",
      "dst": "0.0.0.0/0",
      "dst_len": 0,
      "gateway": "192.168.1.1",
      "dev": "eth0",
      "dev_index": 2,
      "metric": 100,
      "prefsrc": "192.168.1.100",
      "flags": 0
    }

## Command-Line Interface

The module can be run directly as a script with various options:

### Basic Usage

    # Full JSON output of all routes
    sudo python3 net_route_info.py

    # Human-readable summary
    sudo python3 net_route_info.py --summary

    # IPv4 routes only
    sudo python3 net_route_info.py --ipv4

    # IPv6 routes only
    sudo python3 net_route_info.py --ipv6

    # Filter by routing table
    sudo python3 net_route_info.py --table main
    sudo python3 net_route_info.py --table local

    # Disable unknown attribute tracking
    sudo python3 net_route_info.py --no-unknown-attrs

    # Combine options
    sudo python3 net_route_info.py --ipv4 --table main --summary

### Command-Line Options

Option | Description
--- | ---
`--summary` | Display human-readable summary instead of JSON
`--ipv4` | Show only IPv4 routes
`--ipv6` | Show only IPv6 routes
`--table TABLE` | Filter by routing table name (main, local, etc.)
`--no-unknown-attrs` | Disable tracking of unknown RTA_* attributes

### Example Output (--summary mode)

    ======================================================================
    ROUTING TABLE QUERY
    ======================================================================

    Total routes: 15

    Route entries:

      0.0.0.0/0                                via 192.168.1.1          dev eth0       metric 100 [DHCP]
      192.168.1.0/24                           dev eth0       [KERNEL]
      192.168.1.100/32                         dev eth0       [KERNEL]
      fe80::/64                                dev eth0       [KERNEL]
      ::1/128                                  dev lo         [KERNEL]

## Technical Details

### RTNetlink Protocol

The module communicates with the Linux kernel using the RTNetlink protocol, part of the Netlink socket family. RTNetlink provides a mechanism for querying and modifying routing tables, IP addresses, and network interfaces.

#### Message Flow

1. Socket Creation: Opens a `NETLINK_ROUTE` socket
2. Request Construction: Builds `RTM_GETROUTE` request with optional family filter
3. Kernel Query: Sends request to kernel and waits for response
4. Response Processing: Parses `RTM_NEWROUTE` messages
5. Attribute Extraction: Extracts route attributes (RTA_DST, RTA_GATEWAY, etc.)
6. Data Conversion: Converts C structures to Python dictionaries

### CFFI Integration

The module uses CFFI's `ffi.verify()` mode, which compiles C code at runtime during first import:

- C code is compiled into a shared library on first import
- Cached in `__pycache__` for subsequent runs
- Provides near-native performance with Python convenience
- Type-safe interface through CFFI's type system

Performance Note: First import compiles C code (~1-2 seconds). Subsequent imports use cached library (~0.1 seconds). Route queries execute in microseconds after socket setup.

### Memory Management

- C Side: Manual memory management with explicit `malloc()` and `free()`
- Python Side: Automatic garbage collection of Python objects
- Bridge: CFFI handles memory lifecycle for data crossing Python-C boundary
- Safety: Context manager ensures socket cleanup even on exceptions

### Interface Name Resolution

The module uses the C library function `if_indextoname()` to convert interface indices to names. This is wrapped in the custom `nl_get_ifname()` function that also tries reading from `/sys/class/net/` as a fallback.

### Supported Route Attributes (RTA_*)

The module explicitly handles these RTNetlink route attributes:

Attribute | Value | Description
--- | --- | ---
RTA_DST | 1 | Destination network
RTA_SRC | 2 | Source network (policy routing)
RTA_OIF | 4 | Output interface index
RTA_GATEWAY | 5 | Next-hop gateway
RTA_PRIORITY | 6 | Route metric/preference
RTA_PREFSRC | 7 | Preferred source address
RTA_MULTIPATH | 9 | Multipath nexthops (ECMP)
RTA_CACHEINFO | 12 | Cache statistics
RTA_TABLE | 15 | Routing table ID (>255)

Unknown attributes are tracked if `capture_unknown_attrs=True`.

### Limitations

- Linux Only: Requires Linux kernel with RTNetlink (2.6+)
- Root Required: Needs CAP_NET_ADMIN capability or root privileges
- Read-Only: Query operations only; cannot modify routes
- Snapshot: Returns point-in-time snapshot; no real-time monitoring
- First Import Delay: 1-2 second compilation delay on first import

### Error Handling

The module raises clear exceptions for common error conditions:

Exception | Cause | Solution
--- | --- | ---
RuntimeError (Python version) | Python < 3.8 | Upgrade to Python 3.8+
RuntimeError (setuptools) | Python 3.12+ without setuptools | Install setuptools: `pip install setuptools`
RuntimeError (socket) | Failed to create netlink socket | Check kernel support and permissions
RuntimeError (send/receive) | Communication failure with kernel | Check system logs; possible kernel issue
PermissionError | Insufficient privileges | Run with sudo or add CAP_NET_ADMIN capability
ValueError | Invalid family parameter | Use 'ipv4', 'ipv6', or None

### Comparison with Alternatives

Approach | Pros | Cons
--- | --- | ---
net_route_info.py | Direct kernel access, complete metadata, high performance, type-safe | Compilation required, Linux-only, requires privileges
`ip route show` | No coding required, human-readable | Text parsing needed, limited programmatic use, incomplete metadata
pyroute2 | Pure Python, comprehensive netlink support | Slower, more complex API, larger dependency
/proc/net/route | No privileges, simple text format | IPv4 only, incomplete information, deprecated interface

## Conclusion

net_route_info.py provides a high-performance, type-safe interface to Linux routing tables through direct kernel communication via RTNetlink. Its hybrid C/Python architecture delivers native performance while maintaining Python's ease of use. The module is ideal for network monitoring tools, diagnostic utilities, SDN controllers, and any application requiring programmatic access to routing information.

Module: net_route_info.py

Documentation Generated: 2025

Python Version: 3.8+

Platform: Linux (2.6+)