RTNetlink Routing Table Query Module with C Library via CFFI
The module is designed around a hybrid architecture that combines the performance of C with the convenience of Python:
ffi.verify() for runtime compilation and seamless Python-C integrationDirect communication with Linux kernel via RTNetlink sockets using RTM_GETROUTE messages
Handles socket management, message construction, response parsing, and 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_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 │
└─────────────────────────────────────────────┘
| 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) |
| 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. The module will automatically check for setuptools availability at import time.
sudo python3 net_route_info.pysetcap cap_net_admin+ep /usr/bin/python3 (not recommended for system Python)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()
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')}")
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")
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")
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']}")
# 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
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)
Main class for querying routing tables. Implements context manager protocol for safe socket management.
| Parameter | Type | Default | Description |
|---|---|---|---|
capture_unknown_attrs |
bool | True | Whether to track unknown RTA_* attributes in route entries |
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 parameterRuntimeError - Socket creation, send, receive, or parse failurePermissionError - Insufficient privileges (needs root/CAP_NET_ADMIN)# 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
Each route entry is returned as a dictionary with the following structure:
| 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) |
| 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 |
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 |
{
"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
}
The module can be run directly as a script with various options:
# 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
| 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 |
======================================================================
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]
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.
NETLINK_ROUTE socketRTM_GETROUTE request with optional family filterRTM_NEWROUTE messages
The module uses CFFI's ffi.verify() mode, which compiles C code at runtime during first import:
__pycache__ for subsequent runsmalloc() and free()
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.
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.
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 |
| 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 |
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+)