from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel, Field from typing import List, Optional from pyroute2 import IPRoute, NDB router = APIRouter() # Globals for lazy initialization ip: IPRoute | None = None ndb: NDB | None = None # ------------------------------ # Pydantic models # ------------------------------ class InterfaceAddress(BaseModel): """ Represents an IP address assigned to a network interface. """ family: str = Field(..., description="IP family: 'ipv4' or 'ipv6'.") address: str = Field(..., description="The IP address assigned to the interface.") prefixlen: int = Field(..., description="Subnet prefix length (e.g., 24 for 255.255.255.0).") class InterfaceInfo(BaseModel): """ Represents a network interface with all its properties. """ ifindex: int = Field(..., description="Interface index (unique identifier assigned by the kernel).") name: str = Field(..., description="Interface name (e.g., 'eth0', 'enp38s0').") state: str = Field(..., description="Operational state (e.g., 'UP', 'DOWN', 'UNKNOWN').") mac: Optional[str] = Field(None, description="MAC address of the interface, if applicable.") mtu: int = Field(..., description="Maximum Transmission Unit for the interface.") flags: List[str] = Field(..., description="List of interface flags (e.g., ['BROADCAST', 'MULTICAST']).") addresses: List[InterfaceAddress] = Field(..., description="List of IP addresses assigned to the interface.") class RouteInfo(BaseModel): """ Represents a single routing table entry. """ dst: Optional[str] = Field( None, description="Destination network in CIDR notation (e.g., '192.168.1.0/24'). None means default route." ) gateway: Optional[str] = Field( None, description="Next-hop gateway IP address for this route. None if the route is directly connected." ) prefsrc: Optional[str] = Field( None, description="Preferred source IP to use when sending packets via this route." ) oif: Optional[int] = Field( None, description="Output interface index (ifindex) for this route. Can be used to look up the interface name." ) ifname: Optional[str] = Field( None, description="Name of the interface corresponding to `oif` (e.g., 'eth0')." ) table: int = Field( ..., description="Routing table ID (e.g., 254 = main, 255 = local)." ) proto: Optional[int] = Field( None, description="Protocol of the route (numeric Linux codes, e.g., 2=kernel, 16=static)." ) scope: Optional[int] = Field( None, description="Scope of the route: 0=global, 253=link, 254=host, 255=nowhere." ) type: Optional[int] = Field( None, description="Type of the route (numeric code): 1=unicast, 2=local, 3=broadcast, 5=multicast." ) class BridgeInterfaceInfo(BaseModel): """ Represents a network interface which is a member of an bridge. """ ifindex: int = Field(..., description="Interface index of a bridge member") ifname: str = Field(..., description="Interface name of a bridge member") state: Optional[str] = Field(None, description="Operational state of the interface") mtu: Optional[int] = Field(None, description="MTU of the interface") class BridgeInfo(BaseModel): """ Represents a network bridge interface with all its properties. """ ifindex: int = Field(..., description="Interface index of the bridge") ifname: str = Field(..., description="Bridge interface name") state: Optional[str] = Field(None, description="Operational state of the bridge") mtu: Optional[int] = Field(None, description="MTU of the bridge") stp_state: Optional[int] = Field(None, description="STP (Spanning Tree Protocol) state of the bridge") members: List[BridgeInterfaceInfo] = Field(default_factory=list, description="List of member interfaces of the bridge") class BridgeCreateRequest(BaseModel): name: str interfaces: List[str] class BridgeRemoveRequest(BaseModel): name: str # ------------------------------ # Lazy Init Functions # ------------------------------ def init_network_api(): global ip, ndb if ip is None: ip = IPRoute() if ndb is None: ndb = NDB() def shutdown_network_api(): global ip, ndb if ip: ip.close() ip = None if ndb: ndb.close() ndb = None def get_iproute(): if ip is None: init_network_api() return ip def get_ndb(): if ndb is None: init_network_api() return ndb # ------------------------------ # Utility functions # ------------------------------ def parse_addresses(addrs): res = [] for a in addrs: family = "ipv4" if a.get("family") == 2 else "ipv6" res.append( InterfaceAddress( family=family, address=a.get("address"), prefixlen=a.get("prefixlen"), ) ) return res def parse_flags(flags_int: int) -> list[str]: """ Converts the integer flags from pyroute2 to human-readable list of strings. """ flags_map = { 0x1: "UP", 0x2: "BROADCAST", 0x4: "DEBUG", 0x8: "LOOPBACK", 0x10: "POINTOPOINT", 0x20: "NOTRAILERS", 0x40: "RUNNING", 0x80: "NOARP", 0x100: "PROMISC", 0x200: "ALLMULTI", 0x400: "MASTER", 0x800: "SLAVE", 0x1000: "MULTICAST", 0x2000: "PORTSEL", 0x4000: "AUTOMEDIA", 0x8000: "DYNAMIC", 0x10000: "LOWER_UP", 0x20000: "DORMANT", 0x40000: "ECHO", } result = [] for bit, name in flags_map.items(): if flags_int & bit: result.append(name) return result def iface_index(name: str, ip: IPRoute) -> int: idx = ip.link_lookup(ifname=name) if not idx: raise HTTPException(status_code=404, detail=f"Interface {name} not found") return idx[0] def bridge_exists(name: str, ip: IPRoute) -> bool: return bool(ip.link_lookup(ifname=name)) # ------------------------------ # Endpoints # ------------------------------ @router.get("/interfaces", response_model=List[InterfaceInfo]) def get_interfaces(ip: IPRoute = Depends(get_iproute)): result = [] links = ip.get_links() addresses = ip.get_addr() addr_map = {} for a in addresses: ifindex = a.get("index") addr_map.setdefault(ifindex, []).append(a) for link in links: attrs = dict(link["attrs"]) ifindex = link["index"] addrs = addr_map.get(ifindex, []) result.append( InterfaceInfo( ifindex=ifindex, name=attrs.get("IFLA_IFNAME"), state=attrs.get("IFLA_OPERSTATE", "unknown"), mac=attrs.get("IFLA_ADDRESS"), mtu=attrs.get("IFLA_MTU"), flags=parse_flags(link.get("flags", 0)), addresses=parse_addresses(addrs), ) ) return result @router.get("/routes", response_model=List[RouteInfo]) def get_routes(ip: IPRoute = Depends(get_iproute)): routes = [] for r in ip.get_routes(): attrs = dict(r["attrs"]) dst = attrs.get("RTA_DST") gateway = attrs.get("RTA_GATEWAY") prefsrc = attrs.get("RTA_PREFSRC") oif = r.get("oif") ifname = None if oif is not None: # translate ifindex → name link = ip.get_links(oif)[0] ifname = dict(link["attrs"]).get("IFLA_IFNAME") routes.append( RouteInfo( dst=f"{dst}/{r.get('dst_len')}" if dst else None, gateway=gateway, prefsrc=prefsrc, oif=oif, ifname=ifname, table=r.get("table", 254), proto=r.get("proto"), scope=r.get("scope"), type=r.get("type"), ) ) return routes @router.get("/links", response_model=List[InterfaceInfo]) def get_raw_links(ip: IPRoute = Depends(get_iproute)): """ Returns all interfaces in a clean Pydantic format. This is similar to /interfaces but avoids additional processing if needed. """ result = [] links = ip.get_links() addresses = ip.get_addr() # group addresses by interface index addr_map = {} for a in addresses: ifindex = a.get("index") addr_map.setdefault(ifindex, []).append(a) for link in links: attrs = dict(link.get("attrs", [])) # convert list of tuples to dict ifindex = link["index"] addrs = addr_map.get(ifindex, []) result.append( InterfaceInfo( ifindex=ifindex, name=attrs.get("IFLA_IFNAME", "unknown"), state=attrs.get("IFLA_OPERSTATE", "unknown"), mac=attrs.get("IFLA_ADDRESS"), mtu=attrs.get("IFLA_MTU", 0), flags=[], # latest pyroute2 removed ifi_flags, leave empty addresses=parse_addresses(addrs), ) ) return result @router.get("/bridges", response_model=List[BridgeInfo]) def get_bridges(): """ Get all bridge interfaces on the system, including their member interfaces. Returns detailed information: - Bridge index, name, state, MTU - STP state - Member interfaces with index, name, state, and MTU """ bridges_list: List[BridgeInfo] = [] with NDB() as ndb: for br in ndb.interfaces: # Only bridges if getattr(br, "kind", None) == "bridge": members: List[BridgeInterfaceInfo] = [] # Find member interfaces for iface in ndb.interfaces: if getattr(iface, "master", None) == br.index: members.append( BridgeInterfaceInfo( ifindex=iface.index, ifname=iface.ifname, state=getattr(iface, "operstate", None), mtu=getattr(iface, "mtu", None) ) ) bridges_list.append( BridgeInfo( ifindex=br.index, ifname=br.ifname, state=getattr(br, "operstate", None), mtu=getattr(br, "mtu", None), stp_state=getattr(br, "stp_state", None), members=members ) ) return bridges_list @router.get("/full-state") def full_state( ip: IPRoute = Depends(get_iproute), ): """ Returns the full network state: - Interfaces with IP addresses and flags - Routes - Bridges with member interfaces """ return { "interfaces": get_interfaces(ip), "routes": get_routes(ip), "bridges": get_bridges(), # uses NDB internally } @router.post("/create") def create_bridge(req: BridgeCreateRequest, ip: IPRoute = Depends(get_iproute)): if bridge_exists(req.name, ip): raise HTTPException(400, f"Bridge {req.name} already exists") # Bridge erzeugen ip.link("add", ifname=req.name, kind="bridge") br_idx = iface_index(req.name, ip) # Bridge konfigurieren # TODO Parameter anpassen (STP, etc.) ip.link("set", index=br_idx, kind="bridge", br_stp_state=0) ip.link("set", index=br_idx, state="up") # Interfaces hinzufügen + aktivieren for iface in req.interfaces: idx = iface_index(iface, ip) # interface hochfahren ip.link("set", index=idx, state="down") # optional - sicherer ip.link("set", index=idx, state="up") # interface in die bridge hängen ip.link("set", index=idx, master=br_idx) return { "status": "ok", "bridge": req.name, "interfaces": req.interfaces } @router.post("/remove") def remove_bridge(req: BridgeRemoveRequest, ip: IPRoute = Depends(get_iproute)): if not bridge_exists(req.name, ip): raise HTTPException(404, f"Bridge {req.name} not found") br_idx = iface_index(req.name, ip) # Bridge runterfahren ip.link("set", index=br_idx, state="down") # Bridge löschen ip.link("del", index=br_idx) return { "status": "ok", "deleted": req.name }