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.

Table of Contents

Architecture & Design

Design Philosophy

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

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:

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:

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:

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_DST1Destination network selector
FRA_SRC2Source network selector
FRA_IIFNAME3Input interface name
FRA_GOTO4Target rule priority for GOTO action
FRA_PRIORITY6Rule priority/preference
FRA_FWMARK10Firewall mark value
FRA_TABLE15Routing table ID (>255)
FRA_FWMASK16Firewall mark mask
FRA_OIFNAME17Output interface name

Rule Actions

Action Value Description
FR_ACT_TO_TBL1Lookup route in specified routing table
FR_ACT_GOTO2Jump to another rule at specified priority
FR_ACT_NOP3No operation (skip to next rule)
FR_ACT_BLACKHOLE6Silently discard packet
FR_ACT_UNREACHABLE7Send ICMP unreachable error
FR_ACT_PROHIBIT8Send ICMP administratively prohibited error

Default System Rules

Linux typically starts with three default rules:

Limitations

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