statd: add support for ip forwarding operational state

This patch unlocks "routing interfaces" support in the ietf-routing yang
model.  An array of interface with IP forwarding enabled.

Note, because of #515 we skip IPv6 forwarding for now.  This will in the
near future be handled by a per-interface force_forwarding sysctl flag.

The 'show interface [ifname]' admin-exec command has been extended with
a Flags field for a quick overview of which interfaces have forwarding
currently enabled.

Fixes #647

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
Joachim Wiberg
2025-12-05 11:12:03 +01:00
parent 1cbc9a49a2
commit 024b51cc6a
4 changed files with 70 additions and 8 deletions
+36 -4
View File
@@ -98,6 +98,7 @@ def compress_interface_list(interfaces):
class Pad:
flags = 2
iface = 16
proto = 11
state = 12
@@ -803,6 +804,9 @@ class DhcpServer:
class Iface:
# Class variable to hold routing-enabled interfaces for the current display session
_routing_ifaces = set()
def __init__(self, data):
self.data = data
self.name = data.get('name', '')
@@ -915,13 +919,16 @@ class Iface:
return self.oper_status
def pr_name(self, pipe=""):
flag = "" if self.name in Iface._routing_ifaces else " "
print(f"{flag:<{Pad.flags}}", end="")
print(f"{pipe}{self.name:<{Pad.iface - len(pipe)}}", end="")
def pr_proto_ipv4(self, pipe=''):
for addr in self.ipv4_addr:
origin = f"({addr['origin']})" if addr.get('origin') else ""
row = f"{pipe:<{Pad.iface}}"
row = f"{'':<{Pad.flags}}"
row += f"{pipe:<{Pad.iface}}"
row += f"{'ipv4':<{Pad.proto}}"
row += f"{'':<{Pad.state}}{addr['ip']}/{addr['prefix-length']} {origin}"
print(row)
@@ -930,7 +937,8 @@ class Iface:
for addr in self.ipv6_addr:
origin = f"({addr['origin']})" if addr.get('origin') else ""
row = f"{pipe:<{Pad.iface}}"
row = f"{'':<{Pad.flags}}"
row += f"{pipe:<{Pad.iface}}"
row += f"{'ipv6':<{Pad.proto}}"
row += f"{'':<{Pad.state}}{addr['ip']}/{addr['prefix-length']} {origin}"
print(row)
@@ -938,7 +946,8 @@ class Iface:
def _pr_proto_common(self, name, phys_address, pipe=''):
row = ""
if len(pipe) > 0:
row = f"{pipe:<{Pad.iface}}"
row = f"{'':<{Pad.flags}}"
row += f"{pipe:<{Pad.iface}}"
row += f"{name:<{Pad.proto}}"
dec = Decore.green if self.oper() == "up" else Decore.red
@@ -1184,6 +1193,11 @@ class Iface:
parent.pr_proto_eth()
def pr_container(self):
# Add ⇅ flag for interfaces with IP forwarding enabled
flag = "" if self.name in Iface._routing_ifaces else " "
# Flag column is outside the gray background
print(f"{flag:<{Pad.flags}}", end="")
row = f"{self.name:<{Pad.iface}}"
row += f"{'container':<{Pad.proto}}"
row += f"{'':<{Pad.state}}"
@@ -1204,6 +1218,9 @@ class Iface:
if self.oper():
print(f"{'operational status':<{20}}: {self.oper(detail=True)}")
forwarding = "enabled" if self.name in Iface._routing_ifaces else "disabled"
print(f"{'ip forwarding':<{20}}: {forwarding}")
if self.lower_if:
print(f"{'lower-layer-if':<{20}}: {self.lower_if}")
@@ -1407,13 +1424,21 @@ def print_interface(iface):
def pr_interface_list(json):
hdr = (f"{'INTERFACE':<{Pad.iface}}"
hdr = (f"{'':<{Pad.flags}}"
f"{'INTERFACE':<{Pad.iface}}"
f"{'PROTOCOL':<{Pad.proto}}"
f"{'STATE':<{Pad.state}}"
f"{'DATA':<{Pad.data}}")
print(Decore.invert(hdr))
# Set class variable with routing-enabled interfaces (with IP forwarding)
if "ietf-routing:routing" in json:
routing_data = json["ietf-routing:routing"].get("interfaces", {})
Iface._routing_ifaces = set(routing_data.get("interface", []))
else:
Iface._routing_ifaces = set()
ifaces = sorted(json["ietf-interfaces:interfaces"]["interface"],
key=ifname_sort)
iface = find_iface(ifaces, "lo")
@@ -1472,6 +1497,13 @@ def pr_interface_list(json):
def show_interfaces(json, name):
# Set class variable with routing-enabled interfaces (with IP forwarding)
if "ietf-routing:routing" in json:
routing_data = json["ietf-routing:routing"].get("interfaces", {})
Iface._routing_ifaces = set(routing_data.get("interface", []))
else:
Iface._routing_ifaces = set()
if name:
if not json.get("ietf-interfaces:interfaces"):
print(f"No interface data found for \"{name}\"")
+28
View File
@@ -124,9 +124,37 @@ def add_protocol(routes, proto):
insert(routes, 'routes', out)
def get_routing_interfaces():
"""Get list of interfaces with IPv4 forwarding enabled"""
import json
# Get all interfaces
links_json = HOST.run(tuple(['ip', '-j', 'link', 'show']), default="[]")
links = json.loads(links_json)
routing_ifaces = []
for link in links:
ifname = link.get('ifname')
if not ifname:
continue
# Check if IPv4 forwarding is enabled
# Note: We only check IPv4 forwarding. IPv6 forwarding behaves differently
# and will be handled separately when Linux 6.17+ force_forwarding is available.
ipv4_fwd = HOST.run(tuple(['sysctl', '-n', f'net.ipv4.conf.{ifname}.forwarding']), default="0").strip()
if ipv4_fwd == "1":
routing_ifaces.append(ifname)
return routing_ifaces
def operational():
out = {
"ietf-routing:routing": {
"interfaces": {
"interface": get_routing_interfaces()
},
"ribs": {
"rib": [{
"name": "ipv4",