RTNetlink Routing Rule (Policy Routing) Query Module with C Library via CFFI
The module follows the same proven architecture as net_route_info.py, combining C performance with Python convenience:
RTM_GETRULEDirect communication with Linux kernel via RTNetlink sockets using RTM_GETRULE messages
Handles socket management, message construction, response parsing, and FRA_* attribute extraction
Runtime compilation and type conversion between C structures and Python objects
Context manager interface, type conversion, and user-friendly data structures
┌─────────────────────────────────────────────┐
│ 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 (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.
Routing rules form a prioritized list that the kernel traverses when making routing decisions. Each rule contains:
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)
└─────────────────────────┘
Route traffic from different source IPs through different gateways
Route some traffic through VPN, rest through normal gateway
Distribute traffic across multiple ISP connections based on source
Use firewall marks to route specific applications through specific paths
| 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 |
| 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 |
# Install dependencies
pip install cffi setuptools
# For Python 3.12+, setuptools is mandatory
pip install setuptools # if not already installed
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.
sudo python3 net_rule_info.pyfrom 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()
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']}")
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")
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']}")
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}")
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")
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}")
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)
Main class for querying routing rules. Implements context manager protocol for safe socket management.
| Parameter | Type | Default | Description |
|---|---|---|---|
capture_unknown_attrs |
bool | True | Whether to track unknown FRA_* attributes in rule entries |
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 parameterRuntimeError - Socket creation, send, receive, or parse failurePermissionError - Insufficient privileges (needs root/CAP_NET_ADMIN)# Automatically manages socket lifecycle
with RoutingRuleQuery() as query:
rules = query.get_rules()
# Socket is automatically closed on exit
Each rule entry is returned as a dictionary with the following structure:
| 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) |
| 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 |
{
"family": "ipv4",
"action": "TO_TBL",
"table": "LOCAL",
"priority": 0,
"dst_len": 0,
"src_len": 0,
"tos": 0,
"flags": 0
}
{
"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
}
{
"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
}
# 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
| 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 |
======================================================================
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
The module communicates with the Linux kernel using the RTNetlink protocol's RTM_GETRULE message
to query the routing policy database (RPDB).
NETLINK_ROUTE socketRTM_GETRULE request with optional family filterRTM_NEWRULE messages| 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 |
| 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 |
| 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 |
Linux typically starts with three default rules:
ip rule for that)| 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) |
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']}")
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