net_rule_info.py - RTNetlink Routing Rule Query Module

# net_rule_info.py

RTNetlink Routing Rule (Policy Routing) Query Module with C Library via CFFI

Overview: A high-performance Python module for querying Linux routing policy rules using the RTNetlink protocol. This module provides comprehensive access to the kernel's routing rule database (policy routing) for both IPv4 and IPv6, including support for priority-based rule ordering, source/destination selectors, firewall mark matching, and interface-based routing decisions.

## Architecture & Design

### Design Philosophy

The module follows the same proven architecture as `net_route_info.py`, combining C performance with Python convenience:

- C Core: Low-level RTNetlink communication for querying routing rules via `RTM_GETRULE`
- CFFI Bridge: Runtime compilation and seamless integration between C and Python
- Python Interface: Clean, type-hinted API with context managers for resource safety

### Architecture Layers

#### Layer 1: Kernel Interface

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

#### Layer 2: C Implementation

Handles socket management, message construction, response parsing, and FRA_* 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_rules()
                       ▼
    ┌─────────────────────────────────────────────┐
    │  RoutingRuleQuery Class                    │
    │  - Socket management (context manager)     │
    │  - Type conversion (Python ↔ C)            │
    └──────────────────┬──────────────────────────┘
                       │ lib.nl_send_getrule()
                       ▼
    ┌─────────────────────────────────────────────┐
    │  C Library (via CFFI)                      │
    │  - nl_create_socket()                      │
    │  - nl_send_getrule()                       │
    │  - nl_receive_response()                   │
    │  - nl_parse_rules()                        │
    └──────────────────┬──────────────────────────┘
                       │ RTNetlink Protocol
                       ▼
    ┌─────────────────────────────────────────────┐
    │  Linux Kernel Routing Rule Subsystem       │
    │  - Rule lookup and filtering              │
    │  - RTM_NEWRULE messages                    │
    └─────────────────────────────────────────────┘

## Policy Routing Overview

### What is Policy Routing?

Policy routing (also called source routing or rule-based routing) allows Linux to make routing decisions based on criteria beyond just the destination address. Traditional routing only considers where a packet is going; policy routing also considers where it came from, which interface it arrived on, packet marks, and other attributes.

### How Routing Rules Work

Routing rules form a prioritized list that the kernel traverses when making routing decisions. Each rule contains:

- Selectors: Criteria to match packets (source, destination, interface, fwmark, etc.)
- Action: What to do when matched (lookup table, goto, blackhole, etc.)
- Priority: Rules are evaluated in priority order (lower numbers first)

### Rule Processing Flow

    Packet arrives
        │
        ▼
    ┌─────────────────────────┐
    │ Rule 0: from all        │──→ Lookup in local table
    │         lookup local    │    (Usually 255.255.255.255, 127.0.0.0/8)
    └───────────┬─────────────┘
                │ Not matched or no route
                ▼
    ┌─────────────────────────┐
    │ Rule 32766: from all    │──→ Lookup in main table
    │         lookup main     │    (Normal routing table)
    └───────────┬─────────────┘
                │ Not matched or no route
                ▼
    ┌─────────────────────────┐
    │ Rule 32767: from all    │──→ Lookup in default table
    │         lookup default  │    (Usually empty)
    └─────────────────────────┘

### Common Use Cases

#### Multi-Homing

Route traffic from different source IPs through different gateways

#### VPN Split-Tunneling

Route some traffic through VPN, rest through normal gateway

#### Load Balancing

Distribute traffic across multiple ISP connections based on source

#### Traffic Engineering

Use firewall marks to route specific applications through specific paths

## Key Features

#### ✓ Dual Stack Support

- Full IPv4 rule queries
- Full IPv6 rule queries
- Mixed query support

#### ✓ Rule Selectors

- Source address/network
- Destination address/network
- Input interface (iif)
- Output interface (oif)
- Firewall mark (fwmark/fwmask)
- Type of Service (TOS)

#### ✓ Rule Actions

- TO_TBL (lookup table)
- GOTO (jump to rule)
- BLACKHOLE (silent drop)
- UNREACHABLE (ICMP error)
- PROHIBIT (ICMP prohibited)
- NOP (no operation)

#### ✓ Advanced Features

- Priority-based ordering
- Multiple routing tables
- Unknown attribute tracking
- Full metadata extraction

### Rule Metadata Extracted

Category | Fields | Description
--- | --- | ---
Classification | family, action, table, priority | IPv4/IPv6, rule action, target routing table, evaluation priority
Selectors | src, dst, iif, oif, fwmark, tos | Source network, destination network, interfaces, firewall mark, type of service
Routing Decision | table, table_id, goto | Which routing table to use or which rule to jump to
Matching Details | src_len, dst_len, fwmask, flags | Prefix lengths, firewall mark mask, rule flags

## 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.

### Permissions

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

- Running as root: `sudo python3 net_rule_info.py`
- CAP_NET_ADMIN capability (not recommended for system Python)

## Usage Examples

### Basic Usage: Query All Rules

    from net_rule_info import RoutingRuleQuery

    # Using context manager (recommended)
    with RoutingRuleQuery() as query:
        rules = query.get_rules()

        # Sort by priority
        sorted_rules = sorted(rules, key=lambda r: r.get('priority', 0))

        for rule in sorted_rules:
            print(f"Priority {rule.get('priority', 'N/A')}: ", end='')
            print(f"{rule['action']} ", end='')

            if 'src' in rule:
                print(f"from {rule['src']} ", end='')

            if rule['action'] == 'TO_TBL':
                print(f"lookup {rule['table']}")
            else:
                print()

### IPv4-Only Rules

    with RoutingRuleQuery() as query:
        ipv4_rules = query.get_rules(family='ipv4')

        print(f"Found {len(ipv4_rules)} IPv4 routing rules")

        for rule in ipv4_rules:
            if 'fwmark' in rule:
                print(f"Rule with fwmark {rule['fwmark']}: {rule['action']}")

### IPv6-Only Rules

    with RoutingRuleQuery() as query:
        ipv6_rules = query.get_rules(family='ipv6')

        # Filter for specific source networks
        ula_rules = [r for r in ipv6_rules
                     if 'src' in r and r['src'].startswith('fd')]

        print(f"Found {len(ula_rules)} rules for ULA addresses")

### Finding Interface-Specific Rules

    with RoutingRuleQuery() as query:
        rules = query.get_rules()

        # Find rules with input interface
        iif_rules = [r for r in rules if 'iif' in r]

        for rule in iif_rules:
            print(f"Traffic from {rule['iif']} → {rule['action']}")

### Analyzing Firewall Mark Rules

    with RoutingRuleQuery() as query:
        rules = query.get_rules()

        # Find rules using fwmark
        fwmark_rules = [r for r in rules if 'fwmark' in r]

        for rule in fwmark_rules:
            mark = rule['fwmark']
            mask = rule.get('fwmask', 0xffffffff)
            table = rule.get('table', 'unknown')

            print(f"Fwmark 0x{mark:x}/0x{mask:x} → table {table}")

### Finding Custom Routing Tables

    with RoutingRuleQuery() as query:
        rules = query.get_rules()

        # Find rules pointing to custom tables (not MAIN or LOCAL)
        custom_tables = {}

        for rule in rules:
            table = rule['table']
            if table not in ['MAIN', 'LOCAL', 'DEFAULT', 'UNSPEC']:
                if table not in custom_tables:
                    custom_tables[table] = []
                custom_tables[table].append(rule)

        print(f"Found {len(custom_tables)} custom routing tables:")
        for table, table_rules in custom_tables.items():
            print(f"  Table {table}: {len(table_rules)} rules")

### Source-Based Routing Analysis

    with RoutingRuleQuery() as query:
        rules = query.get_rules()

        # Find source-based routing rules
        src_rules = [r for r in rules if 'src' in r]

        print("Source-based routing rules:")
        for rule in src_rules:
            src = rule['src']
            action = rule['action']
            priority = rule.get('priority', 'N/A')

            if action == 'TO_TBL':
                print(f"  [{priority}] {src} → table {rule['table']}")
            else:
                print(f"  [{priority}] {src} → {action}")

### JSON Export

    import json

    with RoutingRuleQuery() as query:
        rules = query.get_rules()

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

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

## API Reference

### RoutingRuleQuery Class

class RoutingRuleQuery(capture_unknown_attrs: bool = True)

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

#### Constructor Parameters

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

#### Methods

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

Query routing rule 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 rule 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 RoutingRuleQuery() as query:
        rules = query.get_rules()
        # Socket is automatically closed on exit

## Rule Information Structure

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

### Standard Fields

Field | Type | Always Present | Description
--- | --- | --- | ---
`family` | str | ✓ | 'ipv4' or 'ipv6'
`action` | str | ✓ | Rule action: TO_TBL, GOTO, NOP, BLACKHOLE, UNREACHABLE, PROHIBIT
`table` | str/int | ✓ | Routing table: MAIN, LOCAL, DEFAULT, or numeric ID
`dst_len` | int | ✓ | Destination prefix length (0-32 for IPv4, 0-128 for IPv6)
`src_len` | int | ✓ | Source prefix length (0-32 for IPv4, 0-128 for IPv6)
`tos` | int | ✓ | Type of Service value (0 if not used)
`flags` | int | ✓ | Rule flags (bitfield)

### Optional Fields

Field | Type | Description
--- | --- | ---
`priority` | int | Rule priority (lower = evaluated first). Default system rules: 0, 32766, 32767
`src` | str | Source address/network in CIDR notation (e.g., '192.168.1.0/24')
`dst` | str | Destination address/network in CIDR notation
`iif` | str | Input interface name (e.g., 'eth0', 'wlan0')
`oif` | str | Output interface name
`fwmark` | int | Firewall mark value to match (set by iptables/nftables)
`fwmask` | int | Firewall mark mask (which bits to check)
`table_id` | int | Numeric routing table ID (for tables > 255)
`goto` | int | Target rule priority for GOTO action
`unknown_fra_attrs` | List[int] | List of unknown FRA_* attribute numbers (if capture_unknown_attrs=True)
`unknown_fra_attrs_decoded` | List[Dict] | Decoded information about unknown attributes

### Example Rule Entries

#### Default System Rule (Local Table)

    {
      "family": "ipv4",
      "action": "TO_TBL",
      "table": "LOCAL",
      "priority": 0,
      "dst_len": 0,
      "src_len": 0,
      "tos": 0,
      "flags": 0
    }

#### Source-Based Routing Rule

    {
      "family": "ipv4",
      "action": "TO_TBL",
      "table": 100,
      "priority": 100,
      "src": "192.168.2.0/24",
      "src_len": 24,
      "dst_len": 0,
      "tos": 0,
      "flags": 0,
      "table_id": 100
    }

#### Firewall Mark Rule

    {
      "family": "ipv4",
      "action": "TO_TBL",
      "table": 200,
      "priority": 200,
      "fwmark": 1,
      "fwmask": 255,
      "dst_len": 0,
      "src_len": 0,
      "tos": 0,
      "flags": 0,
      "table_id": 200
    }

## Command-Line Interface

### Basic Usage

    # Full JSON output of all rules
    sudo python3 net_rule_info.py

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

    # IPv4 rules only
    sudo python3 net_rule_info.py --ipv4

    # IPv6 rules only
    sudo python3 net_rule_info.py --ipv6

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

    # Combine options
    sudo python3 net_rule_info.py --ipv4 --summary

### Command-Line Options

Option | Description
--- | ---
`--summary` | Display human-readable summary instead of JSON
`--ipv4` | Show only IPv4 rules
`--ipv6` | Show only IPv6 rules
`--no-unknown-attrs` | Disable tracking of unknown FRA_* attributes

### Example Output (--summary mode)

    ======================================================================
    ROUTING RULE QUERY
    ======================================================================

    Total rules: 5

    Rule entries:

      0: from all lookup LOCAL
      100: from 192.168.2.0/24 lookup 100
      200: from all fwmark 0x1/0xff lookup 200
      32766: from all lookup MAIN
      32767: from all lookup DEFAULT

## Technical Details

### RTNetlink Protocol

The module communicates with the Linux kernel using the RTNetlink protocol's `RTM_GETRULE` message to query the routing policy database (RPDB).

#### Message Flow

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

### Differences from Route Queries

Aspect | Route Queries (RTM_GETROUTE) | Rule Queries (RTM_GETRULE)
--- | --- | ---
Message Type | RTM_GETROUTE / RTM_NEWROUTE | RTM_GETRULE / RTM_NEWRULE
Attributes | RTA_* (Route Attributes) | FRA_* (Forwarding Rule Attributes)
Primary Use | Determine next-hop for destination | Determine which routing table to use
Key Fields | dst, gateway, dev, metric | priority, src, dst, table, action

### Supported Rule Attributes (FRA_*)

Attribute | Value | Description
--- | --- | ---
FRA_DST | 1 | Destination network selector
FRA_SRC | 2 | Source network selector
FRA_IIFNAME | 3 | Input interface name
FRA_GOTO | 4 | Target rule priority for GOTO action
FRA_PRIORITY | 6 | Rule priority/preference
FRA_FWMARK | 10 | Firewall mark value
FRA_TABLE | 15 | Routing table ID (>255)
FRA_FWMASK | 16 | Firewall mark mask
FRA_OIFNAME | 17 | Output interface name

### Rule Actions

Action | Value | Description
--- | --- | ---
FR_ACT_TO_TBL | 1 | Lookup route in specified routing table
FR_ACT_GOTO | 2 | Jump to another rule at specified priority
FR_ACT_NOP | 3 | No operation (skip to next rule)
FR_ACT_BLACKHOLE | 6 | Silently discard packet
FR_ACT_UNREACHABLE | 7 | Send ICMP unreachable error
FR_ACT_PROHIBIT | 8 | Send ICMP administratively prohibited error

### Default System Rules

Linux typically starts with three default rules:

- Priority 0: Lookup in LOCAL table (broadcasts, local addresses)
- Priority 32766: Lookup in MAIN table (normal routing)
- Priority 32767: Lookup in DEFAULT table (usually empty)

### 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 rules (use `ip rule` for that)
- Snapshot: Returns point-in-time snapshot; no real-time monitoring
- First Import Delay: 1-2 second compilation delay on first import

### Comparison with ip rule command

Aspect | net_rule_info.py | ip rule show
--- | --- | ---
Output Format | Structured JSON/Python dicts | Human-readable text
Programmatic Use | Easy (native Python objects) | Requires text parsing
Performance | High (direct kernel access) | Good (external process)
Metadata | Complete (all FRA_* attributes) | Limited to displayed fields
Setup | Requires CFFI compilation | No setup (standard tool)

### Integration with net_route_info.py

Use both modules together for complete routing analysis:

    from net_rule_info import RoutingRuleQuery
    from net_route_info import RoutingTableQuery

    # Get rules
    with RoutingRuleQuery() as rule_query:
        rules = rule_query.get_rules()

    # For each custom routing table found in rules
    with RoutingTableQuery() as route_query:
        all_routes = route_query.get_routes()

        # Group routes by table
        routes_by_table = {}
        for route in all_routes:
            table = route['table']
            if table not in routes_by_table:
                routes_by_table[table] = []
            routes_by_table[table].append(route)

        # Display rules and their associated routes
        for rule in sorted(rules, key=lambda r: r.get('priority', 0)):
            print(f"\nRule {rule.get('priority', '?')}: {rule['action']}")

            if rule['action'] == 'TO_TBL':
                table = rule['table']
                table_routes = routes_by_table.get(table, [])
                print(f"  Routes in table {table}:")
                for route in table_routes[:5]:  # Show first 5
                    print(f"    {route['dst']}")

## Conclusion

net_rule_info.py provides direct, high-performance access to Linux's routing policy database through RTNetlink. It enables programmatic analysis of policy routing configurations, making it ideal for network monitoring tools, SDN controllers, VPN management, and advanced routing diagnostics. Use it alongside net_route_info.py for complete visibility into Linux routing behavior.

Module: net_rule_info.py

Documentation Generated: 2025

Python Version: 3.8+

Platform: Linux (2.6+)

Related: net_route_info.py