diff --git a/backend/app.py b/backend/app.py index c4d43ec..1502137 100644 --- a/backend/app.py +++ b/backend/app.py @@ -13,7 +13,7 @@ # Import routers from routers.session import session as session_router -from routers.interfaces import ethernet, dummy, bonding, bridge, geneve, input, l2tpv3, loopback, macsec, openvpn, pppoe, pseudo_ethernet, sstpc, virtual_ethernet, vpp, vti, wireless +from routers.interfaces import ethernet, dummy, bonding, bridge, geneve, input, l2tpv3, loopback, macsec, openvpn, pppoe, pseudo_ethernet, sstpc, virtual_ethernet, vpp, vti, wireless, wwan from routers.firewall import groups from routers.firewall import ipv4 as firewall_ipv4 from routers.firewall import ipv6 as firewall_ipv6 @@ -297,6 +297,7 @@ async def get_permissions(request: Request) -> dict: app.include_router(vpp.router) app.include_router(vti.router) app.include_router(wireless.router) +app.include_router(wwan.router) app.include_router(groups.router) app.include_router(firewall_ipv4.router) app.include_router(firewall_ipv6.router) diff --git a/backend/routers/interfaces/wwan.py b/backend/routers/interfaces/wwan.py new file mode 100644 index 0000000..9773f2b --- /dev/null +++ b/backend/routers/interfaces/wwan.py @@ -0,0 +1,418 @@ +""" +WWAN Interface Configuration Endpoints + +All WWAN (Wireless WAN / cellular modem) interface endpoints for VyOS configuration. +Supports APN, authentication, DHCP/DHCPv6 options, IP/IPv6 settings, mirror, and redirect. +""" + +import inspect +import logging +from typing import Dict, List, Optional, Any + +from fastapi import APIRouter, HTTPException, Request +from pydantic import BaseModel, Field, ConfigDict +from starlette.concurrency import run_in_threadpool + +from fastapi_permissions import require_read_permission, require_write_permission +from rbac_permissions import FeatureGroup +from session_vyos_service import get_session_vyos_service + +logger = logging.getLogger(__name__) + +router = APIRouter(prefix="/vyos/wwan", tags=["wwan-interface"]) + + +# ============================================================================ +# Request / Response Models +# ============================================================================ + + +class BatchOperation(BaseModel): + op: str = Field(..., description="Operation name") + value: Optional[str] = Field(None, description="Operation value (if required)") + + +class BatchRequest(BaseModel): + interface: str = Field(..., description="WWAN interface name (e.g., wwan0)") + operations: List[BatchOperation] + + +class VyOSResponse(BaseModel): + success: bool + data: Optional[Dict[str, Any]] = None + error: Optional[str] = None + + +class Dhcpv6PdInterface(BaseModel): + interface: str + address: List[str] = Field(default_factory=list) + sla_id: Optional[str] = None + + +class DhcpPrefixDelegation(BaseModel): + id: str + length: Optional[str] = None + interfaces: List[Dhcpv6PdInterface] = Field(default_factory=list) + + +class WwanInterfaceConfig(BaseModel): + name: str + type: str = "wwan" + description: Optional[str] = None + disable: bool = False + disable_link_detect: bool = False + connect_on_demand: bool = False + mtu: Optional[str] = None + vrf: Optional[str] = None + addresses: List[str] = Field(default_factory=list) + redirect: Optional[str] = None + # APN / Authentication + apn: Optional[str] = None + auth_username: Optional[str] = None + auth_password: Optional[str] = None + # DHCP options + dhcp_client_id: Optional[str] = None + dhcp_default_route_distance: Optional[str] = None + dhcp_host_name: Optional[str] = None + dhcp_mtu: Optional[str] = None + dhcp_no_default_route: bool = False + dhcp_reject: List[str] = Field(default_factory=list) + dhcp_user_class: Optional[str] = None + dhcp_vendor_class_id: Optional[str] = None + # DHCPv6 options + dhcpv6_duid: Optional[str] = None + dhcpv6_no_release: bool = False + dhcpv6_no_request_dns: Optional[bool] = None + dhcpv6_no_request_domain_name: Optional[bool] = None + dhcpv6_parameters_only: bool = False + dhcpv6_rapid_commit: bool = False + dhcpv6_temporary: bool = False + dhcpv6_pd: List[DhcpPrefixDelegation] = Field(default_factory=list) + # Mirror + mirror_ingress: Optional[str] = None + mirror_egress: Optional[str] = None + # IP settings + ip_adjust_mss: Optional[str] = None + ip_arp_cache_timeout: Optional[str] = None + ip_disable_arp_filter: bool = False + ip_disable_forwarding: bool = False + ip_enable_arp_accept: bool = False + ip_enable_arp_announce: bool = False + ip_enable_arp_ignore: bool = False + ip_enable_directed_broadcast: bool = False + ip_enable_proxy_arp: bool = False + ip_proxy_arp_pvlan: bool = False + ip_source_validation: Optional[str] = None + # IPv6 settings + ipv6_accept_dad: Optional[str] = None + ipv6_address_autoconf: bool = False + ipv6_address_eui64: List[str] = Field(default_factory=list) + ipv6_address_no_default_link_local: bool = False + ipv6_address_interface_identifier: Optional[str] = None + ipv6_adjust_mss: Optional[str] = None + ipv6_base_reachable_time: Optional[str] = None + ipv6_disable_forwarding: bool = False + ipv6_dup_addr_detect_transmits: Optional[str] = None + ipv6_source_validation: Optional[str] = None + + model_config = ConfigDict(populate_by_name=True) + + +class WwanInterfacesConfigResponse(BaseModel): + interfaces: List[WwanInterfaceConfig] = Field(default_factory=list) + total: int = 0 + by_vrf: Dict[str, int] = Field(default_factory=dict) + + +# ============================================================================ +# Endpoints +# ============================================================================ + + +@router.get("/capabilities") +async def get_capabilities(request: Request) -> Dict[str, Any]: + """Return version-aware feature capabilities for WWAN interfaces.""" + await require_read_permission(request, FeatureGroup.INTERFACES) + service = get_session_vyos_service(request) + from vyos_builders.interfaces.wwan import WwanInterfaceBatchBuilder + builder = WwanInterfaceBatchBuilder(version=service.get_version()) + return builder.get_capabilities() + + +@router.get("/config", response_model=WwanInterfacesConfigResponse) +async def get_config(http_request: Request, refresh: bool = False) -> WwanInterfacesConfigResponse: + """Get all WWAN interface configurations from VyOS.""" + await require_read_permission(http_request, FeatureGroup.INTERFACES) + try: + service = get_session_vyos_service(http_request) + full_config = await run_in_threadpool(service.get_full_config, refresh) + raw_config = full_config.get("interfaces", {}).get("wwan", {}) + + from vyos_mappers.interfaces.wwan_versions import get_wwan_mapper + mapper = get_wwan_mapper(service.get_version()) + parsed = mapper.parse_interfaces_of_type(raw_config) + + interfaces = [] + for iface in parsed.get("interfaces", []): + pd_entries = [] + for pd in (iface.get("dhcpv6_pd") or []): + pd_ifaces = [ + Dhcpv6PdInterface( + interface=pi["interface"], + address=pi.get("address") or [], + sla_id=pi.get("sla_id"), + ) + for pi in (pd.get("interfaces") or []) + ] + pd_entries.append(DhcpPrefixDelegation( + id=pd["id"], + length=pd.get("length"), + interfaces=pd_ifaces, + )) + + interfaces.append(WwanInterfaceConfig( + name=iface["name"], + description=iface.get("description"), + disable=iface.get("disable", False), + disable_link_detect=iface.get("disable_link_detect", False), + connect_on_demand=iface.get("connect_on_demand", False), + mtu=iface.get("mtu"), + vrf=iface.get("vrf"), + addresses=iface.get("addresses") or [], + redirect=iface.get("redirect"), + apn=iface.get("apn"), + auth_username=iface.get("auth_username"), + auth_password=iface.get("auth_password"), + dhcp_client_id=iface.get("dhcp_client_id"), + dhcp_default_route_distance=iface.get("dhcp_default_route_distance"), + dhcp_host_name=iface.get("dhcp_host_name"), + dhcp_mtu=iface.get("dhcp_mtu"), + dhcp_no_default_route=iface.get("dhcp_no_default_route", False), + dhcp_reject=iface.get("dhcp_reject") or [], + dhcp_user_class=iface.get("dhcp_user_class"), + dhcp_vendor_class_id=iface.get("dhcp_vendor_class_id"), + dhcpv6_duid=iface.get("dhcpv6_duid"), + dhcpv6_no_release=iface.get("dhcpv6_no_release", False), + dhcpv6_no_request_dns=iface.get("dhcpv6_no_request_dns"), + dhcpv6_no_request_domain_name=iface.get("dhcpv6_no_request_domain_name"), + dhcpv6_parameters_only=iface.get("dhcpv6_parameters_only", False), + dhcpv6_rapid_commit=iface.get("dhcpv6_rapid_commit", False), + dhcpv6_temporary=iface.get("dhcpv6_temporary", False), + dhcpv6_pd=pd_entries, + mirror_ingress=iface.get("mirror_ingress"), + mirror_egress=iface.get("mirror_egress"), + ip_adjust_mss=iface.get("ip_adjust_mss"), + ip_arp_cache_timeout=iface.get("ip_arp_cache_timeout"), + ip_disable_arp_filter=iface.get("ip_disable_arp_filter", False), + ip_disable_forwarding=iface.get("ip_disable_forwarding", False), + ip_enable_arp_accept=iface.get("ip_enable_arp_accept", False), + ip_enable_arp_announce=iface.get("ip_enable_arp_announce", False), + ip_enable_arp_ignore=iface.get("ip_enable_arp_ignore", False), + ip_enable_directed_broadcast=iface.get("ip_enable_directed_broadcast", False), + ip_enable_proxy_arp=iface.get("ip_enable_proxy_arp", False), + ip_proxy_arp_pvlan=iface.get("ip_proxy_arp_pvlan", False), + ip_source_validation=iface.get("ip_source_validation"), + ipv6_accept_dad=iface.get("ipv6_accept_dad"), + ipv6_address_autoconf=iface.get("ipv6_address_autoconf", False), + ipv6_address_eui64=iface.get("ipv6_address_eui64") or [], + ipv6_address_no_default_link_local=iface.get("ipv6_address_no_default_link_local", False), + ipv6_address_interface_identifier=iface.get("ipv6_address_interface_identifier"), + ipv6_adjust_mss=iface.get("ipv6_adjust_mss"), + ipv6_base_reachable_time=iface.get("ipv6_base_reachable_time"), + ipv6_disable_forwarding=iface.get("ipv6_disable_forwarding", False), + ipv6_dup_addr_detect_transmits=iface.get("ipv6_dup_addr_detect_transmits"), + ipv6_source_validation=iface.get("ipv6_source_validation"), + )) + + return WwanInterfacesConfigResponse( + interfaces=interfaces, + total=parsed.get("total", 0), + by_vrf=parsed.get("by_vrf", {}), + ) + except Exception: + logger.exception("Unhandled error in get_config") + raise HTTPException(status_code=500, detail="Internal server error") + + +@router.post("/batch", response_model=VyOSResponse) +async def batch_configure(http_request: Request, request: BatchRequest) -> VyOSResponse: + """ + Configure a WWAN interface using batch operations. + + **Basic operations (all versions):** + | Operation | Value | Description | + |-----------|-------|-------------| + | `set_interface` | No | Create interface node | + | `delete_interface` | No | Delete entire interface | + | `set_description` | Yes | Set interface description | + | `delete_description` | No | Remove description | + | `set_address` | Yes | Add IP address (CIDR, dhcp, or dhcpv6) | + | `delete_address` | Yes | Remove a specific IP address | + | `delete_address_all` | No | Remove all addresses | + | `set_disable` | No | Administratively disable interface | + | `delete_disable` | No | Re-enable interface | + | `set_disable_link_detect` | No | Disable link state detection | + | `delete_disable_link_detect` | No | Enable link state detection | + | `set_connect_on_demand` | No | Connect when traffic is sent | + | `delete_connect_on_demand` | No | Always-on connection | + | `set_mtu` | Yes | Set MTU (68-1500) | + | `delete_mtu` | No | Reset MTU to default | + | `set_vrf` | Yes | Assign to VRF instance | + | `delete_vrf` | No | Remove VRF assignment | + | `set_redirect` | Yes | Redirect incoming packets to interface | + | `delete_redirect` | No | Remove redirect | + + **APN / Authentication:** + | `set_apn` | Yes | Set Access Point Name | + | `delete_apn` | No | Remove APN | + | `set_auth_username` | Yes | Set APN username | + | `delete_auth_username` | No | Remove username | + | `set_auth_password` | Yes | Set APN password | + | `delete_auth_password` | No | Remove password | + | `delete_authentication` | No | Remove all authentication config | + + **DHCP options:** + | `set_dhcp_client_id` | Yes | DHCP client identifier | + | `delete_dhcp_client_id` | No | Remove client identifier | + | `set_dhcp_default_route_distance` | Yes | Default route distance (1-255) | + | `delete_dhcp_default_route_distance` | No | Remove distance override | + | `set_dhcp_host_name` | Yes | Override hostname sent to DHCP server | + | `delete_dhcp_host_name` | No | Remove hostname override | + | `set_dhcp_mtu` | Yes | Override MTU via DHCP | + | `delete_dhcp_mtu` | No | Remove MTU override | + | `set_dhcp_no_default_route` | No | Do not install default route from DHCP | + | `delete_dhcp_no_default_route` | No | Allow default route from DHCP | + | `set_dhcp_reject` | Yes | Reject leases from IP/subnet | + | `delete_dhcp_reject` | Yes | Remove specific reject entry | + | `delete_dhcp_reject_all` | No | Remove all reject entries | + | `set_dhcp_user_class` | Yes | DHCP user class identifier | + | `delete_dhcp_user_class` | No | Remove user class | + | `set_dhcp_vendor_class_id` | Yes | DHCP vendor class identifier | + | `delete_dhcp_vendor_class_id` | No | Remove vendor class | + | `delete_dhcp_options` | No | Remove all DHCP options | + + **DHCPv6 options:** + | `set_dhcpv6_duid` | Yes | DHCPv6 DUID | + | `delete_dhcpv6_duid` | No | Remove DUID | + | `set_dhcpv6_no_release` | No | Do not send Release on client exit | + | `delete_dhcpv6_no_release` | No | Send Release on exit | + | `set_dhcpv6_parameters_only` | No | Request parameters only, no address | + | `delete_dhcpv6_parameters_only` | No | Request address and parameters | + | `set_dhcpv6_rapid_commit` | No | Enable rapid commit (2-message exchange) | + | `delete_dhcpv6_rapid_commit` | No | Disable rapid commit | + | `set_dhcpv6_temporary` | No | Request temporary address | + | `delete_dhcpv6_temporary` | No | Do not request temporary address | + | `set_dhcpv6_pd_instance` | Yes | Create prefix delegation instance (e.g. "0") | + | `delete_dhcpv6_pd_instance` | Yes | Delete prefix delegation instance | + | `delete_dhcpv6_pd_all` | No | Remove all prefix delegations | + | `delete_dhcpv6_options` | No | Remove all DHCPv6 options | + + **Mirror:** + | `set_mirror_ingress` | Yes | Mirror ingress traffic to destination interface | + | `delete_mirror_ingress` | No | Remove ingress mirror | + | `set_mirror_egress` | Yes | Mirror egress traffic to destination interface | + | `delete_mirror_egress` | No | Remove egress mirror | + + **IP settings:** + | `set_ip_adjust_mss` | Yes | Clamp TCP MSS to PMTU | + | `delete_ip_adjust_mss` | No | Remove MSS clamping | + | `set_ip_arp_cache_timeout` | Yes | ARP cache timeout (seconds) | + | `delete_ip_arp_cache_timeout` | No | Reset ARP cache timeout | + | `set_ip_disable_arp_filter` | No | Disable ARP filter | + | `delete_ip_disable_arp_filter` | No | Enable ARP filter | + | `set_ip_disable_forwarding` | No | Disable IPv4 forwarding | + | `delete_ip_disable_forwarding` | No | Enable IPv4 forwarding | + | `set_ip_enable_arp_accept` | No | Enable ARP accept | + | `delete_ip_enable_arp_accept` | No | Disable ARP accept | + | `set_ip_enable_arp_announce` | No | Enable ARP announce | + | `delete_ip_enable_arp_announce` | No | Disable ARP announce | + | `set_ip_enable_arp_ignore` | No | Enable ARP ignore | + | `delete_ip_enable_arp_ignore` | No | Disable ARP ignore | + | `set_ip_enable_directed_broadcast` | No | Enable directed broadcast | + | `delete_ip_enable_directed_broadcast` | No | Disable directed broadcast | + | `set_ip_enable_proxy_arp` | No | Enable proxy ARP | + | `delete_ip_enable_proxy_arp` | No | Disable proxy ARP | + | `set_ip_proxy_arp_pvlan` | No | Enable proxy ARP PVLAN | + | `delete_ip_proxy_arp_pvlan` | No | Disable proxy ARP PVLAN | + | `set_ip_source_validation` | Yes | Source validation (strict/loose/disable) | + | `delete_ip_source_validation` | No | Remove source validation | + + **IPv6 settings:** + | `set_ipv6_accept_dad` | Yes | DAD mode (0=disable, 1=enable, 2=enable+no-link-local-if-duplicate) | + | `delete_ipv6_accept_dad` | No | Reset DAD | + | `set_ipv6_address_autoconf` | No | Enable SLAAC | + | `delete_ipv6_address_autoconf` | No | Disable SLAAC | + | `set_ipv6_address_eui64` | Yes | EUI-64 prefix | + | `delete_ipv6_address_eui64` | Yes | Remove specific EUI-64 prefix | + | `delete_ipv6_address_eui64_all` | No | Remove all EUI-64 prefixes | + | `set_ipv6_address_no_default_link_local` | No | Disable default link-local address | + | `delete_ipv6_address_no_default_link_local` | No | Enable default link-local address | + | `set_ipv6_adjust_mss` | Yes | Clamp TCP MSS to PMTU (IPv6) | + | `delete_ipv6_adjust_mss` | No | Remove IPv6 MSS clamping | + | `set_ipv6_base_reachable_time` | Yes | Neighbor base reachable time | + | `delete_ipv6_base_reachable_time` | No | Reset base reachable time | + | `set_ipv6_disable_forwarding` | No | Disable IPv6 forwarding | + | `delete_ipv6_disable_forwarding` | No | Enable IPv6 forwarding | + | `set_ipv6_dup_addr_detect_transmits` | Yes | DAD transmit count | + | `delete_ipv6_dup_addr_detect_transmits` | No | Reset DAD transmit count | + | `set_ipv6_source_validation` | Yes | IPv6 source validation (strict/loose/disable) | + | `delete_ipv6_source_validation` | No | Remove IPv6 source validation | + + **VyOS 1.5 only:** + | `set_dhcpv6_no_request_dns` | No | Do not request DNS servers via DHCPv6 | + | `delete_dhcpv6_no_request_dns` | No | Request DNS servers via DHCPv6 | + | `set_dhcpv6_no_request_domain_name` | No | Do not request domain name via DHCPv6 | + | `delete_dhcpv6_no_request_domain_name` | No | Request domain name via DHCPv6 | + | `set_ipv6_address_interface_identifier` | Yes | SLAAC interface identifier (::h:h:h:h) | + | `delete_ipv6_address_interface_identifier` | No | Remove interface identifier | + """ + await require_write_permission(http_request, FeatureGroup.INTERFACES) + + try: + service = get_session_vyos_service(http_request) + from vyos_builders.interfaces.wwan import WwanInterfaceBatchBuilder + batch = WwanInterfaceBatchBuilder(version=service.get_version()) + + for op in request.operations: + if op.op in batch._INTERNAL_BUILDER_METHODS: + raise HTTPException( + status_code=400, + detail=f"Operation '{op.op}' is not a valid interface operation", + ) + + method = getattr(batch, op.op, None) + if method is None: + raise HTTPException( + status_code=400, + detail=f"Unsupported operation: {op.op}", + ) + + sig = inspect.signature(method) + params = [p for p in sig.parameters.keys() if p != "self"] + + if len(params) == 1: + method(request.interface) + elif len(params) == 2: + if op.value is None: + raise HTTPException( + status_code=400, + detail=f"Operation '{op.op}' requires a value", + ) + method(request.interface, op.value) + else: + raise HTTPException( + status_code=400, + detail=f"Operation '{op.op}' has unexpected signature", + ) + + response = service.execute_batch(batch) + return VyOSResponse( + success=response.status == 200, + data=response.result if isinstance(response.result, dict) else None, + error=response.error if response.error else None, + ) + except HTTPException: + raise + except Exception: + logger.exception("Unhandled error in batch_configure") + raise HTTPException(status_code=500, detail="Internal server error") diff --git a/backend/vyos_builders/interfaces/wwan.py b/backend/vyos_builders/interfaces/wwan.py new file mode 100644 index 0000000..d06254f --- /dev/null +++ b/backend/vyos_builders/interfaces/wwan.py @@ -0,0 +1,448 @@ +""" +WWAN Interface Batch Builder + +Provides all WWAN interface batch operations covering: +- Basic interface settings (address, description, disable, mtu, vrf) +- WWAN-specific: APN, authentication (username/password), connect-on-demand, disable-link-detect +- DHCP options (client-id, default-route-distance, host-name, mtu, no-default-route, reject, user-class, vendor-class-id) +- DHCPv6 options (duid, no-release, parameters-only, rapid-commit, temporary, pd prefix delegation) +- IP/IPv6 settings, mirror, redirect +- Version-specific: no-request-dns/no-request-domain-name, interface-identifier (v1.5) +""" + +from typing import List, Dict, Any +from vyos_mappers import CommandMapperRegistry + + +class WwanInterfaceBatchBuilder: + """Complete batch builder for WWAN interface operations.""" + + _INTERNAL_BUILDER_METHODS = frozenset({ + "add_set", "add_delete", "add_multiple_sets", "clear", + "get_operations", "operation_count", "is_empty", "get_capabilities", + }) + + def __init__(self, version: str): + self.version = version + self._operations: List[Dict[str, Any]] = [] + self.mappers = CommandMapperRegistry.get_all_mappers(version) + self.mapper_key = "interface_wwan" + + # ======================================================================== + # Core Batch Operations + # ======================================================================== + + def add_set(self, path: List[str]) -> "WwanInterfaceBatchBuilder": + if path: + self._operations.append({"op": "set", "path": path}) + return self + + def add_delete(self, path: List[str]) -> "WwanInterfaceBatchBuilder": + if path: + self._operations.append({"op": "delete", "path": path}) + return self + + def add_multiple_sets(self, paths: List[List[str]]) -> "WwanInterfaceBatchBuilder": + for path in paths: + self.add_set(path) + return self + + def clear(self) -> None: + self._operations = [] + + def get_operations(self) -> List[Dict[str, Any]]: + return self._operations.copy() + + def operation_count(self) -> int: + return len(self._operations) + + def is_empty(self) -> bool: + return len(self._operations) == 0 + + # ======================================================================== + # Capabilities + # ======================================================================== + + def get_capabilities(self) -> Dict[str, Any]: + is_v15 = "1.5" in self.version or "latest" in self.version + return { + "version": self.version, + "features": { + "address": {"supported": True, "description": "IPv4/IPv6 address or DHCP/DHCPv6"}, + "apn": {"supported": True, "description": "Access Point Name (APN) for cellular connection"}, + "authentication": {"supported": True, "description": "APN username and password"}, + "connect_on_demand": {"supported": True, "description": "Establish connection when traffic is sent"}, + "description": {"supported": True, "description": "Interface description"}, + "disable": {"supported": True, "description": "Administratively disable interface"}, + "disable_link_detect": {"supported": True, "description": "Disable link state change detection"}, + "mtu": {"supported": True, "description": "Maximum Transmission Unit (68-1500, default 1430)"}, + "vrf": {"supported": True, "description": "VRF instance assignment"}, + "redirect": {"supported": True, "description": "Redirect incoming packets to another interface"}, + "mirror": {"supported": True, "description": "Mirror ingress/egress traffic to another interface"}, + "dhcp_options": {"supported": True, "description": "DHCP client options (client-id, host-name, etc.)"}, + "dhcpv6_options": {"supported": True, "description": "DHCPv6 client options including prefix delegation"}, + "dhcpv6_pd": {"supported": True, "description": "DHCPv6 prefix delegation"}, + "ip_settings": {"supported": True, "description": "IPv4 ARP, forwarding, and MSS settings"}, + "ipv6_settings": {"supported": True, "description": "IPv6 DAD, EUI-64, forwarding, and MSS settings"}, + "dhcpv6_no_request_dns": {"supported": is_v15, "description": "Do not request DNS servers via DHCPv6 (VyOS 1.5+)"}, + "dhcpv6_no_request_domain_name": {"supported": is_v15, "description": "Do not request domain name via DHCPv6 (VyOS 1.5+)"}, + "ipv6_interface_identifier": {"supported": is_v15, "description": "SLAAC interface identifier override (VyOS 1.5+)"}, + }, + } + + # ======================================================================== + # Basic Interface Settings + # ======================================================================== + + def set_interface(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_set(self.mappers[self.mapper_key].get_interface(interface)) + + def delete_interface(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_delete(self.mappers[self.mapper_key].get_interface(interface)) + + def set_description(self, interface: str, description: str) -> "WwanInterfaceBatchBuilder": + return self.add_set(self.mappers[self.mapper_key].get_description(interface, description)) + + def delete_description(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_delete(self.mappers[self.mapper_key].get_description_path(interface)) + + def set_address(self, interface: str, address: str) -> "WwanInterfaceBatchBuilder": + return self.add_set(self.mappers[self.mapper_key].get_address(interface, address)) + + def delete_address(self, interface: str, address: str) -> "WwanInterfaceBatchBuilder": + return self.add_delete(self.mappers[self.mapper_key].get_address(interface, address)) + + def delete_address_all(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_delete(self.mappers[self.mapper_key].get_address_path(interface)) + + def set_disable(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_set(self.mappers[self.mapper_key].get_disable(interface)) + + def delete_disable(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_delete(self.mappers[self.mapper_key].get_disable(interface)) + + def set_disable_link_detect(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_set(self.mappers[self.mapper_key].get_disable_link_detect(interface)) + + def delete_disable_link_detect(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_delete(self.mappers[self.mapper_key].get_disable_link_detect(interface)) + + def set_connect_on_demand(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_set(self.mappers[self.mapper_key].get_connect_on_demand(interface)) + + def delete_connect_on_demand(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_delete(self.mappers[self.mapper_key].get_connect_on_demand(interface)) + + def set_mtu(self, interface: str, mtu: str) -> "WwanInterfaceBatchBuilder": + return self.add_set(self.mappers[self.mapper_key].get_mtu(interface, mtu)) + + def delete_mtu(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_delete(self.mappers[self.mapper_key].get_mtu_path(interface)) + + def set_vrf(self, interface: str, vrf: str) -> "WwanInterfaceBatchBuilder": + return self.add_set(self.mappers[self.mapper_key].get_vrf(interface, vrf)) + + def delete_vrf(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_delete(self.mappers[self.mapper_key].get_vrf_path(interface)) + + # ======================================================================== + # APN / Authentication + # ======================================================================== + + def set_apn(self, interface: str, apn: str) -> "WwanInterfaceBatchBuilder": + return self.add_set(self.mappers[self.mapper_key].get_apn(interface, apn)) + + def delete_apn(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_delete(self.mappers[self.mapper_key].get_apn_path(interface)) + + def set_auth_username(self, interface: str, username: str) -> "WwanInterfaceBatchBuilder": + return self.add_set(self.mappers[self.mapper_key].get_authentication_username(interface, username)) + + def delete_auth_username(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_delete(self.mappers[self.mapper_key].get_authentication_username_path(interface)) + + def set_auth_password(self, interface: str, password: str) -> "WwanInterfaceBatchBuilder": + return self.add_set(self.mappers[self.mapper_key].get_authentication_password(interface, password)) + + def delete_auth_password(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_delete(self.mappers[self.mapper_key].get_authentication_password_path(interface)) + + def delete_authentication(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_delete(self.mappers[self.mapper_key].get_authentication_path(interface)) + + # ======================================================================== + # DHCP Options + # ======================================================================== + + def set_dhcp_client_id(self, interface: str, client_id: str) -> "WwanInterfaceBatchBuilder": + return self.add_set(self.mappers[self.mapper_key].get_dhcp_client_id(interface, client_id)) + + def delete_dhcp_client_id(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_delete(self.mappers[self.mapper_key].get_dhcp_client_id_path(interface)) + + def set_dhcp_default_route_distance(self, interface: str, distance: str) -> "WwanInterfaceBatchBuilder": + return self.add_set(self.mappers[self.mapper_key].get_dhcp_default_route_distance(interface, distance)) + + def delete_dhcp_default_route_distance(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_delete(self.mappers[self.mapper_key].get_dhcp_default_route_distance_path(interface)) + + def set_dhcp_host_name(self, interface: str, hostname: str) -> "WwanInterfaceBatchBuilder": + return self.add_set(self.mappers[self.mapper_key].get_dhcp_host_name(interface, hostname)) + + def delete_dhcp_host_name(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_delete(self.mappers[self.mapper_key].get_dhcp_host_name_path(interface)) + + def set_dhcp_mtu(self, interface: str, mtu: str) -> "WwanInterfaceBatchBuilder": + return self.add_set(self.mappers[self.mapper_key].get_dhcp_mtu(interface, mtu)) + + def delete_dhcp_mtu(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_delete(self.mappers[self.mapper_key].get_dhcp_mtu_path(interface)) + + def set_dhcp_no_default_route(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_set(self.mappers[self.mapper_key].get_dhcp_no_default_route(interface)) + + def delete_dhcp_no_default_route(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_delete(self.mappers[self.mapper_key].get_dhcp_no_default_route(interface)) + + def set_dhcp_reject(self, interface: str, reject: str) -> "WwanInterfaceBatchBuilder": + return self.add_set(self.mappers[self.mapper_key].get_dhcp_reject(interface, reject)) + + def delete_dhcp_reject(self, interface: str, reject: str) -> "WwanInterfaceBatchBuilder": + return self.add_delete(self.mappers[self.mapper_key].get_dhcp_reject(interface, reject)) + + def delete_dhcp_reject_all(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_delete(self.mappers[self.mapper_key].get_dhcp_reject_path(interface)) + + def set_dhcp_user_class(self, interface: str, user_class: str) -> "WwanInterfaceBatchBuilder": + return self.add_set(self.mappers[self.mapper_key].get_dhcp_user_class(interface, user_class)) + + def delete_dhcp_user_class(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_delete(self.mappers[self.mapper_key].get_dhcp_user_class_path(interface)) + + def set_dhcp_vendor_class_id(self, interface: str, vendor_class_id: str) -> "WwanInterfaceBatchBuilder": + return self.add_set(self.mappers[self.mapper_key].get_dhcp_vendor_class_id(interface, vendor_class_id)) + + def delete_dhcp_vendor_class_id(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_delete(self.mappers[self.mapper_key].get_dhcp_vendor_class_id_path(interface)) + + def delete_dhcp_options(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_delete(self.mappers[self.mapper_key].get_dhcp_options_path(interface)) + + # ======================================================================== + # DHCPv6 Options + # ======================================================================== + + def set_dhcpv6_duid(self, interface: str, duid: str) -> "WwanInterfaceBatchBuilder": + return self.add_set(self.mappers[self.mapper_key].get_dhcpv6_duid(interface, duid)) + + def delete_dhcpv6_duid(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_delete(self.mappers[self.mapper_key].get_dhcpv6_duid_path(interface)) + + def set_dhcpv6_no_release(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_set(self.mappers[self.mapper_key].get_dhcpv6_no_release(interface)) + + def delete_dhcpv6_no_release(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_delete(self.mappers[self.mapper_key].get_dhcpv6_no_release(interface)) + + def set_dhcpv6_parameters_only(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_set(self.mappers[self.mapper_key].get_dhcpv6_parameters_only(interface)) + + def delete_dhcpv6_parameters_only(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_delete(self.mappers[self.mapper_key].get_dhcpv6_parameters_only(interface)) + + def set_dhcpv6_rapid_commit(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_set(self.mappers[self.mapper_key].get_dhcpv6_rapid_commit(interface)) + + def delete_dhcpv6_rapid_commit(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_delete(self.mappers[self.mapper_key].get_dhcpv6_rapid_commit(interface)) + + def set_dhcpv6_temporary(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_set(self.mappers[self.mapper_key].get_dhcpv6_temporary(interface)) + + def delete_dhcpv6_temporary(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_delete(self.mappers[self.mapper_key].get_dhcpv6_temporary(interface)) + + def set_dhcpv6_pd_instance(self, interface: str, instance: str) -> "WwanInterfaceBatchBuilder": + return self.add_set(self.mappers[self.mapper_key].get_dhcpv6_pd_instance(interface, instance)) + + def delete_dhcpv6_pd_instance(self, interface: str, instance: str) -> "WwanInterfaceBatchBuilder": + return self.add_delete(self.mappers[self.mapper_key].get_dhcpv6_pd_instance(interface, instance)) + + def delete_dhcpv6_pd_all(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_delete(self.mappers[self.mapper_key].get_dhcpv6_pd_path(interface)) + + def delete_dhcpv6_options(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_delete(self.mappers[self.mapper_key].get_dhcpv6_options_path(interface)) + + # VyOS 1.5 only + def set_dhcpv6_no_request_dns(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_set(self.mappers[self.mapper_key].get_dhcpv6_no_request_dns(interface)) + + def delete_dhcpv6_no_request_dns(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_delete(self.mappers[self.mapper_key].get_dhcpv6_no_request_dns(interface)) + + def set_dhcpv6_no_request_domain_name(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_set(self.mappers[self.mapper_key].get_dhcpv6_no_request_domain_name(interface)) + + def delete_dhcpv6_no_request_domain_name(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_delete(self.mappers[self.mapper_key].get_dhcpv6_no_request_domain_name(interface)) + + # ======================================================================== + # Mirror / Redirect + # ======================================================================== + + def set_redirect(self, interface: str, destination: str) -> "WwanInterfaceBatchBuilder": + return self.add_set(self.mappers[self.mapper_key].get_redirect(interface, destination)) + + def delete_redirect(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_delete(self.mappers[self.mapper_key].get_redirect_path(interface)) + + def set_mirror_ingress(self, interface: str, destination: str) -> "WwanInterfaceBatchBuilder": + return self.add_set(self.mappers[self.mapper_key].get_mirror_ingress(interface, destination)) + + def delete_mirror_ingress(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_delete(self.mappers[self.mapper_key].get_mirror_ingress_path(interface)) + + def set_mirror_egress(self, interface: str, destination: str) -> "WwanInterfaceBatchBuilder": + return self.add_set(self.mappers[self.mapper_key].get_mirror_egress(interface, destination)) + + def delete_mirror_egress(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_delete(self.mappers[self.mapper_key].get_mirror_egress_path(interface)) + + # ======================================================================== + # IP Settings + # ======================================================================== + + def set_ip_adjust_mss(self, interface: str, value: str) -> "WwanInterfaceBatchBuilder": + return self.add_set(self.mappers[self.mapper_key].get_ip_adjust_mss(interface, value)) + + def delete_ip_adjust_mss(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_delete(self.mappers[self.mapper_key].get_ip_adjust_mss_path(interface)) + + def set_ip_arp_cache_timeout(self, interface: str, timeout: str) -> "WwanInterfaceBatchBuilder": + return self.add_set(self.mappers[self.mapper_key].get_ip_arp_cache_timeout(interface, timeout)) + + def delete_ip_arp_cache_timeout(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_delete(self.mappers[self.mapper_key].get_ip_arp_cache_timeout_path(interface)) + + def set_ip_disable_arp_filter(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_set(self.mappers[self.mapper_key].get_ip_disable_arp_filter(interface)) + + def delete_ip_disable_arp_filter(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_delete(self.mappers[self.mapper_key].get_ip_disable_arp_filter(interface)) + + def set_ip_disable_forwarding(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_set(self.mappers[self.mapper_key].get_ip_disable_forwarding(interface)) + + def delete_ip_disable_forwarding(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_delete(self.mappers[self.mapper_key].get_ip_disable_forwarding(interface)) + + def set_ip_enable_arp_accept(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_set(self.mappers[self.mapper_key].get_ip_enable_arp_accept(interface)) + + def delete_ip_enable_arp_accept(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_delete(self.mappers[self.mapper_key].get_ip_enable_arp_accept(interface)) + + def set_ip_enable_arp_announce(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_set(self.mappers[self.mapper_key].get_ip_enable_arp_announce(interface)) + + def delete_ip_enable_arp_announce(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_delete(self.mappers[self.mapper_key].get_ip_enable_arp_announce(interface)) + + def set_ip_enable_arp_ignore(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_set(self.mappers[self.mapper_key].get_ip_enable_arp_ignore(interface)) + + def delete_ip_enable_arp_ignore(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_delete(self.mappers[self.mapper_key].get_ip_enable_arp_ignore(interface)) + + def set_ip_enable_directed_broadcast(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_set(self.mappers[self.mapper_key].get_ip_enable_directed_broadcast(interface)) + + def delete_ip_enable_directed_broadcast(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_delete(self.mappers[self.mapper_key].get_ip_enable_directed_broadcast(interface)) + + def set_ip_enable_proxy_arp(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_set(self.mappers[self.mapper_key].get_ip_enable_proxy_arp(interface)) + + def delete_ip_enable_proxy_arp(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_delete(self.mappers[self.mapper_key].get_ip_enable_proxy_arp(interface)) + + def set_ip_proxy_arp_pvlan(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_set(self.mappers[self.mapper_key].get_ip_proxy_arp_pvlan(interface)) + + def delete_ip_proxy_arp_pvlan(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_delete(self.mappers[self.mapper_key].get_ip_proxy_arp_pvlan(interface)) + + def set_ip_source_validation(self, interface: str, mode: str) -> "WwanInterfaceBatchBuilder": + return self.add_set(self.mappers[self.mapper_key].get_ip_source_validation(interface, mode)) + + def delete_ip_source_validation(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_delete(self.mappers[self.mapper_key].get_ip_source_validation_path(interface)) + + # ======================================================================== + # IPv6 Settings + # ======================================================================== + + def set_ipv6_accept_dad(self, interface: str, value: str) -> "WwanInterfaceBatchBuilder": + return self.add_set(self.mappers[self.mapper_key].get_ipv6_accept_dad(interface, value)) + + def delete_ipv6_accept_dad(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_delete(self.mappers[self.mapper_key].get_ipv6_accept_dad_path(interface)) + + def set_ipv6_address_autoconf(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_set(self.mappers[self.mapper_key].get_ipv6_address_autoconf(interface)) + + def delete_ipv6_address_autoconf(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_delete(self.mappers[self.mapper_key].get_ipv6_address_autoconf(interface)) + + def set_ipv6_address_eui64(self, interface: str, prefix: str) -> "WwanInterfaceBatchBuilder": + return self.add_set(self.mappers[self.mapper_key].get_ipv6_address_eui64(interface, prefix)) + + def delete_ipv6_address_eui64(self, interface: str, prefix: str) -> "WwanInterfaceBatchBuilder": + return self.add_delete(self.mappers[self.mapper_key].get_ipv6_address_eui64(interface, prefix)) + + def delete_ipv6_address_eui64_all(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_delete(self.mappers[self.mapper_key].get_ipv6_address_eui64_path(interface)) + + def set_ipv6_address_no_default_link_local(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_set(self.mappers[self.mapper_key].get_ipv6_address_no_default_link_local(interface)) + + def delete_ipv6_address_no_default_link_local(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_delete(self.mappers[self.mapper_key].get_ipv6_address_no_default_link_local(interface)) + + def set_ipv6_adjust_mss(self, interface: str, value: str) -> "WwanInterfaceBatchBuilder": + return self.add_set(self.mappers[self.mapper_key].get_ipv6_adjust_mss(interface, value)) + + def delete_ipv6_adjust_mss(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_delete(self.mappers[self.mapper_key].get_ipv6_adjust_mss_path(interface)) + + def set_ipv6_base_reachable_time(self, interface: str, value: str) -> "WwanInterfaceBatchBuilder": + return self.add_set(self.mappers[self.mapper_key].get_ipv6_base_reachable_time(interface, value)) + + def delete_ipv6_base_reachable_time(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_delete(self.mappers[self.mapper_key].get_ipv6_base_reachable_time_path(interface)) + + def set_ipv6_disable_forwarding(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_set(self.mappers[self.mapper_key].get_ipv6_disable_forwarding(interface)) + + def delete_ipv6_disable_forwarding(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_delete(self.mappers[self.mapper_key].get_ipv6_disable_forwarding(interface)) + + def set_ipv6_dup_addr_detect_transmits(self, interface: str, value: str) -> "WwanInterfaceBatchBuilder": + return self.add_set(self.mappers[self.mapper_key].get_ipv6_dup_addr_detect_transmits(interface, value)) + + def delete_ipv6_dup_addr_detect_transmits(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_delete(self.mappers[self.mapper_key].get_ipv6_dup_addr_detect_transmits_path(interface)) + + def set_ipv6_source_validation(self, interface: str, mode: str) -> "WwanInterfaceBatchBuilder": + return self.add_set(self.mappers[self.mapper_key].get_ipv6_source_validation(interface, mode)) + + def delete_ipv6_source_validation(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_delete(self.mappers[self.mapper_key].get_ipv6_source_validation_path(interface)) + + # VyOS 1.5 only + def set_ipv6_address_interface_identifier(self, interface: str, identifier: str) -> "WwanInterfaceBatchBuilder": + return self.add_set(self.mappers[self.mapper_key].get_ipv6_address_interface_identifier(interface, identifier)) + + def delete_ipv6_address_interface_identifier(self, interface: str) -> "WwanInterfaceBatchBuilder": + return self.add_delete(self.mappers[self.mapper_key].get_ipv6_address_interface_identifier_path(interface)) diff --git a/backend/vyos_mappers/__init__.py b/backend/vyos_mappers/__init__.py index 945a1ed..99b9ed5 100644 --- a/backend/vyos_mappers/__init__.py +++ b/backend/vyos_mappers/__init__.py @@ -24,6 +24,7 @@ from .interfaces.vpp_versions import get_vpp_mapper from .interfaces.vti_versions import get_vti_mapper from .interfaces.wireless_versions import get_wireless_mapper +from .interfaces.wwan_versions import get_wwan_mapper from .firewall import FirewallGroupsMapper, FirewallIPv4Mapper, FirewallIPv6Mapper, BridgeFirewallMapper, FlowtablesMapper, FirewallZonesMapper from .firewall.groups_versions import get_firewall_groups_mapper from .firewall.ipv4_versions import get_firewall_ipv4_mapper @@ -135,6 +136,8 @@ CommandMapperRegistry.register_feature("interface_vti", get_vti_mapper) # Wireless uses factory for version-specific mappers CommandMapperRegistry.register_feature("interface_wireless", get_wireless_mapper) +# WWAN uses factory for version-specific mappers +CommandMapperRegistry.register_feature("interface_wwan", get_wwan_mapper) # Firewall groups uses factory for version-specific mappers CommandMapperRegistry.register_feature("firewall_groups", get_firewall_groups_mapper) # Firewall IPv4 uses factory for version-specific mappers diff --git a/backend/vyos_mappers/interfaces/__init__.py b/backend/vyos_mappers/interfaces/__init__.py index 4c14cb5..1a76c6f 100644 --- a/backend/vyos_mappers/interfaces/__init__.py +++ b/backend/vyos_mappers/interfaces/__init__.py @@ -20,6 +20,7 @@ from .vpp import VppInterfaceMapper from .vti import VtiInterfaceMapper from .wireless import WirelessInterfaceMapper +from .wwan import WwanInterfaceMapper __all__ = [ "EthernetInterfaceMapper", @@ -38,4 +39,5 @@ "VppInterfaceMapper", "VtiInterfaceMapper", "WirelessInterfaceMapper", + "WwanInterfaceMapper", ] diff --git a/backend/vyos_mappers/interfaces/wwan.py b/backend/vyos_mappers/interfaces/wwan.py new file mode 100644 index 0000000..dadfc3b --- /dev/null +++ b/backend/vyos_mappers/interfaces/wwan.py @@ -0,0 +1,440 @@ +""" +WWAN (Wireless WAN) Interface Command Mapper + +Handles WWAN interface commands for VyOS `interfaces wwan wwanN`. +WWAN interfaces connect via cellular modems using APN configuration. +Provides both command path generation (for writes) and config parsing (for reads). +""" + +from typing import List, Dict, Any +from ..base import BaseFeatureMapper + + +class WwanInterfaceMapper(BaseFeatureMapper): + """WWAN interface mapper with all WWAN interface operations.""" + + def __init__(self, version: str): + super().__init__(version) + self.interface_type = "wwan" + + def _base(self, interface: str) -> List[str]: + return ["interfaces", "wwan", interface] + + # ======================================================================== + # Basic interface settings + # ======================================================================== + + def get_interface(self, interface: str) -> List[str]: + return self._base(interface) + + def get_description(self, interface: str, description: str) -> List[str]: + return self._base(interface) + ["description", description] + + def get_description_path(self, interface: str) -> List[str]: + return self._base(interface) + ["description"] + + def get_address(self, interface: str, address: str) -> List[str]: + return self._base(interface) + ["address", address] + + def get_address_path(self, interface: str) -> List[str]: + return self._base(interface) + ["address"] + + def get_disable(self, interface: str) -> List[str]: + return self._base(interface) + ["disable"] + + def get_disable_link_detect(self, interface: str) -> List[str]: + return self._base(interface) + ["disable-link-detect"] + + def get_connect_on_demand(self, interface: str) -> List[str]: + return self._base(interface) + ["connect-on-demand"] + + def get_mtu(self, interface: str, mtu: str) -> List[str]: + return self._base(interface) + ["mtu", mtu] + + def get_mtu_path(self, interface: str) -> List[str]: + return self._base(interface) + ["mtu"] + + def get_vrf(self, interface: str, vrf: str) -> List[str]: + return self._base(interface) + ["vrf", vrf] + + def get_vrf_path(self, interface: str) -> List[str]: + return self._base(interface) + ["vrf"] + + # ======================================================================== + # APN / Authentication + # ======================================================================== + + def get_apn(self, interface: str, apn: str) -> List[str]: + return self._base(interface) + ["apn", apn] + + def get_apn_path(self, interface: str) -> List[str]: + return self._base(interface) + ["apn"] + + def get_authentication_username(self, interface: str, username: str) -> List[str]: + return self._base(interface) + ["authentication", "username", username] + + def get_authentication_username_path(self, interface: str) -> List[str]: + return self._base(interface) + ["authentication", "username"] + + def get_authentication_password(self, interface: str, password: str) -> List[str]: + return self._base(interface) + ["authentication", "password", password] + + def get_authentication_password_path(self, interface: str) -> List[str]: + return self._base(interface) + ["authentication", "password"] + + def get_authentication_path(self, interface: str) -> List[str]: + return self._base(interface) + ["authentication"] + + # ======================================================================== + # DHCP options + # ======================================================================== + + def get_dhcp_options_path(self, interface: str) -> List[str]: + return self._base(interface) + ["dhcp-options"] + + def get_dhcp_client_id(self, interface: str, client_id: str) -> List[str]: + return self._base(interface) + ["dhcp-options", "client-id", client_id] + + def get_dhcp_client_id_path(self, interface: str) -> List[str]: + return self._base(interface) + ["dhcp-options", "client-id"] + + def get_dhcp_default_route_distance(self, interface: str, distance: str) -> List[str]: + return self._base(interface) + ["dhcp-options", "default-route-distance", distance] + + def get_dhcp_default_route_distance_path(self, interface: str) -> List[str]: + return self._base(interface) + ["dhcp-options", "default-route-distance"] + + def get_dhcp_host_name(self, interface: str, hostname: str) -> List[str]: + return self._base(interface) + ["dhcp-options", "host-name", hostname] + + def get_dhcp_host_name_path(self, interface: str) -> List[str]: + return self._base(interface) + ["dhcp-options", "host-name"] + + def get_dhcp_mtu(self, interface: str, mtu: str) -> List[str]: + return self._base(interface) + ["dhcp-options", "mtu", mtu] + + def get_dhcp_mtu_path(self, interface: str) -> List[str]: + return self._base(interface) + ["dhcp-options", "mtu"] + + def get_dhcp_no_default_route(self, interface: str) -> List[str]: + return self._base(interface) + ["dhcp-options", "no-default-route"] + + def get_dhcp_reject(self, interface: str, reject: str) -> List[str]: + return self._base(interface) + ["dhcp-options", "reject", reject] + + def get_dhcp_reject_path(self, interface: str) -> List[str]: + return self._base(interface) + ["dhcp-options", "reject"] + + def get_dhcp_user_class(self, interface: str, user_class: str) -> List[str]: + return self._base(interface) + ["dhcp-options", "user-class", user_class] + + def get_dhcp_user_class_path(self, interface: str) -> List[str]: + return self._base(interface) + ["dhcp-options", "user-class"] + + def get_dhcp_vendor_class_id(self, interface: str, vendor_class_id: str) -> List[str]: + return self._base(interface) + ["dhcp-options", "vendor-class-id", vendor_class_id] + + def get_dhcp_vendor_class_id_path(self, interface: str) -> List[str]: + return self._base(interface) + ["dhcp-options", "vendor-class-id"] + + # ======================================================================== + # DHCPv6 options + # ======================================================================== + + def get_dhcpv6_options_path(self, interface: str) -> List[str]: + return self._base(interface) + ["dhcpv6-options"] + + def get_dhcpv6_duid(self, interface: str, duid: str) -> List[str]: + return self._base(interface) + ["dhcpv6-options", "duid", duid] + + def get_dhcpv6_duid_path(self, interface: str) -> List[str]: + return self._base(interface) + ["dhcpv6-options", "duid"] + + def get_dhcpv6_no_release(self, interface: str) -> List[str]: + return self._base(interface) + ["dhcpv6-options", "no-release"] + + def get_dhcpv6_parameters_only(self, interface: str) -> List[str]: + return self._base(interface) + ["dhcpv6-options", "parameters-only"] + + def get_dhcpv6_rapid_commit(self, interface: str) -> List[str]: + return self._base(interface) + ["dhcpv6-options", "rapid-commit"] + + def get_dhcpv6_temporary(self, interface: str) -> List[str]: + return self._base(interface) + ["dhcpv6-options", "temporary"] + + def get_dhcpv6_pd_path(self, interface: str) -> List[str]: + return self._base(interface) + ["dhcpv6-options", "pd"] + + def get_dhcpv6_pd_instance(self, interface: str, instance: str) -> List[str]: + return self._base(interface) + ["dhcpv6-options", "pd", instance] + + def get_dhcpv6_pd_length(self, interface: str, instance: str, length: str) -> List[str]: + return self._base(interface) + ["dhcpv6-options", "pd", instance, "length", length] + + def get_dhcpv6_pd_length_path(self, interface: str, instance: str) -> List[str]: + return self._base(interface) + ["dhcpv6-options", "pd", instance, "length"] + + def get_dhcpv6_pd_interface(self, interface: str, instance: str, delegated_iface: str) -> List[str]: + return self._base(interface) + ["dhcpv6-options", "pd", instance, "interface", delegated_iface] + + def get_dhcpv6_pd_interface_path(self, interface: str, instance: str) -> List[str]: + return self._base(interface) + ["dhcpv6-options", "pd", instance, "interface"] + + def get_dhcpv6_pd_interface_address(self, interface: str, instance: str, delegated_iface: str, address: str) -> List[str]: + return self._base(interface) + ["dhcpv6-options", "pd", instance, "interface", delegated_iface, "address", address] + + def get_dhcpv6_pd_interface_sla_id(self, interface: str, instance: str, delegated_iface: str, sla_id: str) -> List[str]: + return self._base(interface) + ["dhcpv6-options", "pd", instance, "interface", delegated_iface, "sla-id", sla_id] + + # ======================================================================== + # Mirror / Redirect + # ======================================================================== + + def get_redirect(self, interface: str, destination: str) -> List[str]: + return self._base(interface) + ["redirect", destination] + + def get_redirect_path(self, interface: str) -> List[str]: + return self._base(interface) + ["redirect"] + + def get_mirror_ingress(self, interface: str, destination: str) -> List[str]: + return self._base(interface) + ["mirror", "ingress", destination] + + def get_mirror_ingress_path(self, interface: str) -> List[str]: + return self._base(interface) + ["mirror", "ingress"] + + def get_mirror_egress(self, interface: str, destination: str) -> List[str]: + return self._base(interface) + ["mirror", "egress", destination] + + def get_mirror_egress_path(self, interface: str) -> List[str]: + return self._base(interface) + ["mirror", "egress"] + + # ======================================================================== + # IP settings + # ======================================================================== + + def get_ip_adjust_mss(self, interface: str, value: str) -> List[str]: + return self._base(interface) + ["ip", "adjust-mss", value] + + def get_ip_adjust_mss_path(self, interface: str) -> List[str]: + return self._base(interface) + ["ip", "adjust-mss"] + + def get_ip_arp_cache_timeout(self, interface: str, timeout: str) -> List[str]: + return self._base(interface) + ["ip", "arp-cache-timeout", timeout] + + def get_ip_arp_cache_timeout_path(self, interface: str) -> List[str]: + return self._base(interface) + ["ip", "arp-cache-timeout"] + + def get_ip_disable_arp_filter(self, interface: str) -> List[str]: + return self._base(interface) + ["ip", "disable-arp-filter"] + + def get_ip_disable_forwarding(self, interface: str) -> List[str]: + return self._base(interface) + ["ip", "disable-forwarding"] + + def get_ip_enable_arp_accept(self, interface: str) -> List[str]: + return self._base(interface) + ["ip", "enable-arp-accept"] + + def get_ip_enable_arp_announce(self, interface: str) -> List[str]: + return self._base(interface) + ["ip", "enable-arp-announce"] + + def get_ip_enable_arp_ignore(self, interface: str) -> List[str]: + return self._base(interface) + ["ip", "enable-arp-ignore"] + + def get_ip_enable_directed_broadcast(self, interface: str) -> List[str]: + return self._base(interface) + ["ip", "enable-directed-broadcast"] + + def get_ip_enable_proxy_arp(self, interface: str) -> List[str]: + return self._base(interface) + ["ip", "enable-proxy-arp"] + + def get_ip_proxy_arp_pvlan(self, interface: str) -> List[str]: + return self._base(interface) + ["ip", "proxy-arp-pvlan"] + + def get_ip_source_validation(self, interface: str, mode: str) -> List[str]: + return self._base(interface) + ["ip", "source-validation", mode] + + def get_ip_source_validation_path(self, interface: str) -> List[str]: + return self._base(interface) + ["ip", "source-validation"] + + # ======================================================================== + # IPv6 settings + # ======================================================================== + + def get_ipv6_accept_dad(self, interface: str, value: str) -> List[str]: + return self._base(interface) + ["ipv6", "accept-dad", value] + + def get_ipv6_accept_dad_path(self, interface: str) -> List[str]: + return self._base(interface) + ["ipv6", "accept-dad"] + + def get_ipv6_address_autoconf(self, interface: str) -> List[str]: + return self._base(interface) + ["ipv6", "address", "autoconf"] + + def get_ipv6_address_eui64(self, interface: str, prefix: str) -> List[str]: + return self._base(interface) + ["ipv6", "address", "eui64", prefix] + + def get_ipv6_address_eui64_path(self, interface: str) -> List[str]: + return self._base(interface) + ["ipv6", "address", "eui64"] + + def get_ipv6_address_no_default_link_local(self, interface: str) -> List[str]: + return self._base(interface) + ["ipv6", "address", "no-default-link-local"] + + def get_ipv6_adjust_mss(self, interface: str, value: str) -> List[str]: + return self._base(interface) + ["ipv6", "adjust-mss", value] + + def get_ipv6_adjust_mss_path(self, interface: str) -> List[str]: + return self._base(interface) + ["ipv6", "adjust-mss"] + + def get_ipv6_base_reachable_time(self, interface: str, value: str) -> List[str]: + return self._base(interface) + ["ipv6", "base-reachable-time", value] + + def get_ipv6_base_reachable_time_path(self, interface: str) -> List[str]: + return self._base(interface) + ["ipv6", "base-reachable-time"] + + def get_ipv6_disable_forwarding(self, interface: str) -> List[str]: + return self._base(interface) + ["ipv6", "disable-forwarding"] + + def get_ipv6_dup_addr_detect_transmits(self, interface: str, value: str) -> List[str]: + return self._base(interface) + ["ipv6", "dup-addr-detect-transmits", value] + + def get_ipv6_dup_addr_detect_transmits_path(self, interface: str) -> List[str]: + return self._base(interface) + ["ipv6", "dup-addr-detect-transmits"] + + def get_ipv6_source_validation(self, interface: str, mode: str) -> List[str]: + return self._base(interface) + ["ipv6", "source-validation", mode] + + def get_ipv6_source_validation_path(self, interface: str) -> List[str]: + return self._base(interface) + ["ipv6", "source-validation"] + + # ======================================================================== + # Config Parsing (READ operations) + # ======================================================================== + + def parse_single_interface(self, name: str, config: Dict[str, Any]) -> Dict[str, Any]: + """Parse a single WWAN interface configuration from VyOS.""" + addresses = [] + if "address" in config: + addr = config["address"] + if isinstance(addr, list): + addresses = addr + elif isinstance(addr, str): + addresses = [addr] + + auth_config = config.get("authentication", {}) or {} + ip_config = config.get("ip", {}) or {} + ipv6_config = config.get("ipv6", {}) or {} + ipv6_addr_config = ipv6_config.get("address", {}) or {} + mirror_config = config.get("mirror", {}) or {} + dhcp_config = config.get("dhcp-options", {}) or {} + dhcpv6_config = config.get("dhcpv6-options", {}) or {} + + eui64 = ipv6_addr_config.get("eui64") + if isinstance(eui64, str): + eui64 = [eui64] + elif not isinstance(eui64, list): + eui64 = [] + + dhcp_reject = dhcp_config.get("reject") + if isinstance(dhcp_reject, str): + dhcp_reject = [dhcp_reject] + elif not isinstance(dhcp_reject, list): + dhcp_reject = [] + + dhcpv6_pd_raw = dhcpv6_config.get("pd", {}) or {} + dhcpv6_pd = [] + for pd_id, pd_conf in dhcpv6_pd_raw.items(): + if not isinstance(pd_conf, dict): + continue + pd_entry: Dict[str, Any] = {"id": pd_id, "length": pd_conf.get("length"), "interfaces": []} + for iface_name, iface_conf in (pd_conf.get("interface", {}) or {}).items(): + if not isinstance(iface_conf, dict): + continue + pd_iface = iface_conf.get("address") + if isinstance(pd_iface, str): + pd_iface = [pd_iface] + elif not isinstance(pd_iface, list): + pd_iface = [] + pd_entry["interfaces"].append({ + "interface": iface_name, + "address": pd_iface, + "sla_id": iface_conf.get("sla-id"), + }) + dhcpv6_pd.append(pd_entry) + + return { + "name": name, + "type": self.interface_type, + "addresses": addresses, + "description": config.get("description"), + "mtu": config.get("mtu"), + "disable": "disable" in config, + "disable_link_detect": "disable-link-detect" in config, + "connect_on_demand": "connect-on-demand" in config, + "vrf": config.get("vrf"), + "redirect": config.get("redirect"), + # APN / Authentication + "apn": config.get("apn"), + "auth_username": auth_config.get("username"), + "auth_password": auth_config.get("password"), + # Mirror + "mirror_ingress": mirror_config.get("ingress"), + "mirror_egress": mirror_config.get("egress"), + # DHCP options + "dhcp_client_id": dhcp_config.get("client-id"), + "dhcp_default_route_distance": dhcp_config.get("default-route-distance"), + "dhcp_host_name": dhcp_config.get("host-name"), + "dhcp_mtu": dhcp_config.get("mtu"), + "dhcp_no_default_route": "no-default-route" in dhcp_config, + "dhcp_reject": dhcp_reject, + "dhcp_user_class": dhcp_config.get("user-class"), + "dhcp_vendor_class_id": dhcp_config.get("vendor-class-id"), + # DHCPv6 options + "dhcpv6_duid": dhcpv6_config.get("duid"), + "dhcpv6_no_release": "no-release" in dhcpv6_config, + "dhcpv6_parameters_only": "parameters-only" in dhcpv6_config, + "dhcpv6_rapid_commit": "rapid-commit" in dhcpv6_config, + "dhcpv6_temporary": "temporary" in dhcpv6_config, + "dhcpv6_pd": dhcpv6_pd, + # IP settings + "ip_adjust_mss": ip_config.get("adjust-mss"), + "ip_arp_cache_timeout": ip_config.get("arp-cache-timeout"), + "ip_disable_arp_filter": "disable-arp-filter" in ip_config, + "ip_disable_forwarding": "disable-forwarding" in ip_config, + "ip_enable_arp_accept": "enable-arp-accept" in ip_config, + "ip_enable_arp_announce": "enable-arp-announce" in ip_config, + "ip_enable_arp_ignore": "enable-arp-ignore" in ip_config, + "ip_enable_directed_broadcast": "enable-directed-broadcast" in ip_config, + "ip_enable_proxy_arp": "enable-proxy-arp" in ip_config, + "ip_proxy_arp_pvlan": "proxy-arp-pvlan" in ip_config, + "ip_source_validation": ip_config.get("source-validation"), + # IPv6 settings + "ipv6_accept_dad": ipv6_config.get("accept-dad"), + "ipv6_address_autoconf": "autoconf" in ipv6_addr_config, + "ipv6_address_eui64": eui64, + "ipv6_address_no_default_link_local": "no-default-link-local" in ipv6_addr_config, + "ipv6_adjust_mss": ipv6_config.get("adjust-mss"), + "ipv6_base_reachable_time": ipv6_config.get("base-reachable-time"), + "ipv6_disable_forwarding": "disable-forwarding" in ipv6_config, + "ipv6_dup_addr_detect_transmits": ipv6_config.get("dup-addr-detect-transmits"), + "ipv6_source_validation": ipv6_config.get("source-validation"), + } + + def parse_interfaces_of_type(self, config: Dict[str, Any]) -> Dict[str, Any]: + """Parse all WWAN interfaces.""" + interfaces = [] + by_vrf: Dict[str, int] = {} + + for iface_name, iface_config in config.items(): + if not isinstance(iface_config, dict): + continue + + interface = self.parse_single_interface(iface_name, iface_config) + interfaces.append(interface) + + if interface.get("vrf"): + vrf = interface["vrf"] + by_vrf[vrf] = by_vrf.get(vrf, 0) + 1 + + return { + "interfaces": interfaces, + "total": len(interfaces), + "by_type": {self.interface_type: len(interfaces)}, + "by_vrf": by_vrf, + } diff --git a/backend/vyos_mappers/interfaces/wwan_versions/__init__.py b/backend/vyos_mappers/interfaces/wwan_versions/__init__.py new file mode 100644 index 0000000..6d714a4 --- /dev/null +++ b/backend/vyos_mappers/interfaces/wwan_versions/__init__.py @@ -0,0 +1,16 @@ +""" +WWAN Interface Version-Specific Mappers + +Factory function returns the appropriate mapper based on VyOS version. +""" + +from ..wwan import WwanInterfaceMapper +from .v1_4 import WwanMapper_v1_4 +from .v1_5 import WwanMapper_v1_5 + + +def get_wwan_mapper(version: str) -> WwanInterfaceMapper: + """Get version-specific WWAN interface mapper.""" + if "1.5" in version or "latest" in version: + return WwanMapper_v1_5(version) + return WwanMapper_v1_4(version) diff --git a/backend/vyos_mappers/interfaces/wwan_versions/v1_4.py b/backend/vyos_mappers/interfaces/wwan_versions/v1_4.py new file mode 100644 index 0000000..ab5bd60 --- /dev/null +++ b/backend/vyos_mappers/interfaces/wwan_versions/v1_4.py @@ -0,0 +1,17 @@ +""" +WWAN Interface Mapper - VyOS 1.4 + +VyOS 1.4 does NOT support: +- dhcpv6-options/no-request-dns +- dhcpv6-options/no-request-domain-name +- ipv6/address/interface-identifier (SLAAC interface identifier) +""" + +from ..wwan import WwanInterfaceMapper + + +class WwanMapper_v1_4(WwanInterfaceMapper): + """VyOS 1.4 WWAN interface mapper.""" + + def __init__(self, version: str): + super().__init__(version) diff --git a/backend/vyos_mappers/interfaces/wwan_versions/v1_5.py b/backend/vyos_mappers/interfaces/wwan_versions/v1_5.py new file mode 100644 index 0000000..3e6a339 --- /dev/null +++ b/backend/vyos_mappers/interfaces/wwan_versions/v1_5.py @@ -0,0 +1,39 @@ +""" +WWAN Interface Mapper - VyOS 1.5 + +VyOS 1.5 adds: +- dhcpv6-options/no-request-dns +- dhcpv6-options/no-request-domain-name +- ipv6/address/interface-identifier (SLAAC interface identifier) +""" + +from typing import List, Dict, Any +from ..wwan import WwanInterfaceMapper + + +class WwanMapper_v1_5(WwanInterfaceMapper): + """VyOS 1.5 WWAN interface mapper.""" + + def __init__(self, version: str): + super().__init__(version) + + def get_dhcpv6_no_request_dns(self, interface: str) -> List[str]: + return self._base(interface) + ["dhcpv6-options", "no-request-dns"] + + def get_dhcpv6_no_request_domain_name(self, interface: str) -> List[str]: + return self._base(interface) + ["dhcpv6-options", "no-request-domain-name"] + + def get_ipv6_address_interface_identifier(self, interface: str, identifier: str) -> List[str]: + return self._base(interface) + ["ipv6", "address", "interface-identifier", identifier] + + def get_ipv6_address_interface_identifier_path(self, interface: str) -> List[str]: + return self._base(interface) + ["ipv6", "address", "interface-identifier"] + + def parse_single_interface(self, name: str, config: Dict[str, Any]) -> Dict[str, Any]: + result = super().parse_single_interface(name, config) + dhcpv6_config = config.get("dhcpv6-options", {}) or {} + ipv6_addr_config = (config.get("ipv6", {}) or {}).get("address", {}) or {} + result["dhcpv6_no_request_dns"] = "no-request-dns" in dhcpv6_config + result["dhcpv6_no_request_domain_name"] = "no-request-domain-name" in dhcpv6_config + result["ipv6_address_interface_identifier"] = ipv6_addr_config.get("interface-identifier") + return result diff --git a/frontend/src/app/network/interfaces/page.tsx b/frontend/src/app/network/interfaces/page.tsx index 0c7770e..12facf6 100644 --- a/frontend/src/app/network/interfaces/page.tsx +++ b/frontend/src/app/network/interfaces/page.tsx @@ -16,7 +16,7 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; -import { Plus, RefreshCw, AlertCircle, Search, Cable, Pencil, Trash2, Network, ChevronRight, Shield, Boxes, Waypoints, Link2, GitMerge, Box, Layers, ArrowDownToLine, Repeat, Lock, ArrowLeftRight, Wifi } from "lucide-react"; +import { Plus, RefreshCw, AlertCircle, Search, Cable, Pencil, Trash2, Network, ChevronRight, Shield, Boxes, Waypoints, Link2, GitMerge, Box, Layers, ArrowDownToLine, Repeat, Lock, ArrowLeftRight, Wifi, Signal } from "lucide-react"; import { useState, useEffect } from "react"; import { cn } from "@/lib/utils"; import { ethernetService } from "@/lib/api/ethernet"; @@ -87,6 +87,10 @@ import { wirelessService, type WirelessInterface, type WirelessCapabilitiesRespo import { CreateWirelessModal } from "@/components/wireless/CreateWirelessModal"; import { EditWirelessModal } from "@/components/wireless/EditWirelessModal"; import { DeleteWirelessModal } from "@/components/wireless/DeleteWirelessModal"; +import { wwanService, type WwanInterface, type WwanCapabilities } from "@/lib/api/wwan"; +import { CreateWwanModal } from "@/components/wwan/CreateWwanModal"; +import { EditWwanModal } from "@/components/wwan/EditWwanModal"; +import { DeleteWwanModal } from "@/components/wwan/DeleteWwanModal"; import { CreateVxlanModal } from "@/components/vxlan/CreateVxlanModal"; import { EditVxlanModal } from "@/components/vxlan/EditVxlanModal"; import { DeleteVxlanModal } from "@/components/vxlan/DeleteVxlanModal"; @@ -100,7 +104,7 @@ import { LoadingSpinner } from "@/components/ui/loading-spinner"; import { usePermissions } from "@/hooks/usePermissions"; import { FeatureGroup } from "@/lib/api/user-management"; -type InterfaceType = "ethernet" | "vlan" | "wireguard" | "vxlan" | "tunnel" | "bonding" | "bridge" | "dummy" | "geneve" | "input" | "l2tpv3" | "loopback" | "macsec" | "pppoe" | "pseudo-ethernet" | "sstpc" | "virtual-ethernet" | "vpp" | "vti" | "wireless"; +type InterfaceType = "ethernet" | "vlan" | "wireguard" | "vxlan" | "tunnel" | "bonding" | "bridge" | "dummy" | "geneve" | "input" | "l2tpv3" | "loopback" | "macsec" | "pppoe" | "pseudo-ethernet" | "sstpc" | "virtual-ethernet" | "vpp" | "vti" | "wireless" | "wwan"; type VlanSubTab = "vif" | "vif-s" | "vif-c"; interface VLANWithParent extends VIFConfig { @@ -280,6 +284,15 @@ export default function InterfacesPage() { const [editingWireless, setEditingWireless] = useState(null); const [deletingWireless, setDeletingWireless] = useState(null); + // WWAN state + const [wwanInterfaces, setWwanInterfaces] = useState([]); + const [wwanCapabilities, setWwanCapabilities] = useState(null); + + // WWAN Modal states + const [isCreateWwanModalOpen, setIsCreateWwanModalOpen] = useState(false); + const [editingWwan, setEditingWwan] = useState(null); + const [deletingWwan, setDeletingWwan] = useState(null); + // VPP state const [vppBonding, setVppBonding] = useState([]); const [vppBridge, setVppBridge] = useState([]); @@ -308,7 +321,7 @@ export default function InterfacesPage() { const loadData = async () => { try { setError(null); - const [configData, capabilitiesData, wgData, vxlanData, vxlanCapData, tunnelData, tunnelCapData, dummyData, dummyCapData, geneveData, geneveCapData, inputData, inputCapData, l2tpv3Data, l2tpv3CapData, loopbackData, loopbackCapData, macsecData, macsecCapData, bondingData, bondingCapData, bridgeData, bridgeCapData, pppoeData, pppoeCapData, pseudoEthernetData, pseudoEthernetCapData, sstpcData, sstpcCapData, virtualEthernetData, virtualEthernetCapData, vppData, vppCapData, vtiData, vtiCapData, wirelessData, wirelessCapData] = await Promise.all([ + const [configData, capabilitiesData, wgData, vxlanData, vxlanCapData, tunnelData, tunnelCapData, dummyData, dummyCapData, geneveData, geneveCapData, inputData, inputCapData, l2tpv3Data, l2tpv3CapData, loopbackData, loopbackCapData, macsecData, macsecCapData, bondingData, bondingCapData, bridgeData, bridgeCapData, pppoeData, pppoeCapData, pseudoEthernetData, pseudoEthernetCapData, sstpcData, sstpcCapData, virtualEthernetData, virtualEthernetCapData, vppData, vppCapData, vtiData, vtiCapData, wirelessData, wirelessCapData, wwanData, wwanCapData] = await Promise.all([ ethernetService.getConfig(), ethernetService.getCapabilities(), wireguardService.getConfig(), @@ -346,6 +359,8 @@ export default function InterfacesPage() { vtiService.getCapabilities(), wirelessService.getConfig(), wirelessService.getCapabilities(), + wwanService.getConfig(), + wwanService.getCapabilities(), ]); setInterfaces(configData.interfaces); setCapabilities(capabilitiesData); @@ -392,6 +407,8 @@ export default function InterfacesPage() { setVtiCapabilities(vtiCapData); setWirelessInterfaces(wirelessData.interfaces); setWirelessCapabilities(wirelessCapData); + setWwanInterfaces(wwanData.interfaces); + setWwanCapabilities(wwanCapData); } catch (err) { setError(err instanceof Error ? err.message : "Failed to load interface data"); } finally { @@ -453,6 +470,7 @@ export default function InterfacesPage() { const totalVpp = vppBonding.length + vppBridge.length + vppGre.length + vppIpip.length + vppLoopback.length + vppVxlanIfaces.length + vppXconnect.length; const totalVti = vtiInterfaces.length; const totalWireless = wirelessInterfaces.length; + const totalWwan = wwanInterfaces.length; // Filter interfaces based on search const filteredInterfaces = interfaces.filter((iface) => { @@ -837,6 +855,18 @@ export default function InterfacesPage() { ); }); + const filteredWwan = wwanInterfaces.filter((iface) => { + if (searchQuery === "") return true; + const q = searchQuery.toLowerCase(); + return ( + iface.name.toLowerCase().includes(q) || + (iface.description || "").toLowerCase().includes(q) || + (iface.apn || "").toLowerCase().includes(q) || + iface.addresses?.some((addr) => addr.toLowerCase().includes(q)) || + (iface.vrf || "").toLowerCase().includes(q) + ); + }); + const filterVppIface = (items: T[]): T[] => { if (searchQuery === "") return items; const q = searchQuery.toLowerCase(); @@ -863,7 +893,7 @@ export default function InterfacesPage() {

Interfaces

- {totalInterfaces + totalVlans + totalWireGuard + totalVxlan + totalTunnel + totalDummy + totalGeneve + totalInput + totalLoopback + totalMacsec + totalBonding + totalBridge + totalPppoe + totalPseudoEthernet + totalSstpc + totalVirtualEthernet + totalVti + totalWireless} total + {totalInterfaces + totalVlans + totalWireGuard + totalVxlan + totalTunnel + totalDummy + totalGeneve + totalInput + totalLoopback + totalMacsec + totalBonding + totalBridge + totalPppoe + totalPseudoEthernet + totalSstpc + totalVirtualEthernet + totalVti + totalWireless + totalWwan} total

- - {/* VLAN */} + {/* Bonding */} - {/* VXLAN */} + {/* Bridge */} - {/* Tunnel */} + {/* Dummy */} - {/* Dummy */} + {/* Ethernet */} )} + {/* Tunnel */} + + {/* Virtual Ethernet */} {canRead(FeatureGroup.INTERFACES) && ( + {/* VPP — only shown when capabilities confirm VyOS 1.5+ support */} {canRead(FeatureGroup.INTERFACES) && vppCapabilities?.supported && ( - {/* Wireless */} + {/* VXLAN */} - {/* Bonding */} + {/* WireGuard */} - {/* Bridge */} + {/* Wireless */} - {/* WireGuard */} + {/* WWAN */} + )} + + + + ) : ( + <> +
+ + + + Name + APN + Status + Addresses + VRF + + + + + {filteredWwan.map((iface) => ( + + {iface.name} + + {iface.apn ? ( + {iface.apn} + ) : } + + + {iface.disable ? ( + Disabled + ) : ( + Enabled + )} + + + {iface.addresses?.length ? ( +
+ {iface.addresses.slice(0, 2).map((addr, idx) => {addr})} + {iface.addresses.length > 2 && +{iface.addresses.length - 2}} +
+ ) : } +
+ + {iface.vrf ? ( + + {iface.vrf} + + ) : } + + +
+ {canWrite(FeatureGroup.INTERFACES) && ( + <> + + + + )} +
+
+
+ ))} +
+
+
+

+ Showing {filteredWwan.length} of {totalWwan} interface{totalWwan !== 1 ? "s" : ""} +

+ + ) ) : filteredWireGuard.length === 0 ? ( @@ -4116,6 +4274,33 @@ export default function InterfacesPage() { }} interfaceData={deletingWireless} /> + {/* WWAN Modals */} + i.name)} + /> + !open && setEditingWwan(null)} + onSuccess={() => { + setEditingWwan(null); + loadData(); + }} + capabilities={wwanCapabilities} + interfaceData={editingWwan} + /> + !open && setDeletingWwan(null)} + onSuccess={() => { + setDeletingWwan(null); + loadData(); + }} + interfaceData={deletingWwan} + /> {/* Bridge Modals */} void; + onSuccess: () => void; + capabilities: WwanCapabilities | null; + existingInterfaces: string[]; +} + +const MTU_PRESETS = ["1280", "1400", "1430", "1500"]; +const TIMEOUT_PRESETS = ["30", "60", "300", "600", "3600"]; +const DAD_PRESETS = ["0", "1", "2", "3"]; + +export function CreateWwanModal({ + open, + onOpenChange, + onSuccess, + capabilities, + existingInterfaces, +}: CreateWwanModalProps) { + // Connection + const [name, setName] = useState("wwan0"); + const [apn, setApn] = useState(""); + const [authUsername, setAuthUsername] = useState(""); + const [authPassword, setAuthPassword] = useState(""); + const [showPassword, setShowPassword] = useState(false); + const [connectOnDemand, setConnectOnDemand] = useState(false); + const [disableLinkDetect, setDisableLinkDetect] = useState(false); + const [disable, setDisable] = useState(false); + + // Basic + const [description, setDescription] = useState(""); + const [mtu, setMtu] = useState(""); + const [mtuIsCustom, setMtuIsCustom] = useState(false); + const [vrf, setVrf] = useState(""); + + // Addresses + const [addresses, setAddresses] = useState(""); + const [dhcpClientId, setDhcpClientId] = useState(""); + const [dhcpDefaultRouteDistance, setDhcpDefaultRouteDistance] = useState(""); + const [dhcpHostName, setDhcpHostName] = useState(""); + const [dhcpMtu, setDhcpMtu] = useState(""); + const [dhcpNoDefaultRoute, setDhcpNoDefaultRoute] = useState(false); + const [dhcpReject, setDhcpReject] = useState([]); + const [dhcpRejectInput, setDhcpRejectInput] = useState(""); + const [dhcpUserClass, setDhcpUserClass] = useState(""); + const [dhcpVendorClassId, setDhcpVendorClassId] = useState(""); + const [dhcpv6Duid, setDhcpv6Duid] = useState(""); + const [dhcpv6NoRelease, setDhcpv6NoRelease] = useState(false); + const [dhcpv6ParametersOnly, setDhcpv6ParametersOnly] = useState(false); + const [dhcpv6RapidCommit, setDhcpv6RapidCommit] = useState(false); + const [dhcpv6Temporary, setDhcpv6Temporary] = useState(false); + const [dhcpv6NoRequestDns, setDhcpv6NoRequestDns] = useState(false); + const [dhcpv6NoRequestDomainName, setDhcpv6NoRequestDomainName] = useState(false); + const [dhcpv6Pd, setDhcpv6Pd] = useState([]); + const [dhcpv6PdInput, setDhcpv6PdInput] = useState(""); + const [ipv6AddressEui64, setIpv6AddressEui64] = useState(""); + const [ipv6AddressAutoconf, setIpv6AddressAutoconf] = useState(false); + const [ipv6AddressNoDefaultLinkLocal, setIpv6AddressNoDefaultLinkLocal] = useState(false); + const [ipv6AddressInterfaceIdentifier, setIpv6AddressInterfaceIdentifier] = useState(""); + + // IP Settings + const [ipAdjustMss, setIpAdjustMss] = useState(""); + const [ipAdjustMssIsCustom, setIpAdjustMssIsCustom] = useState(false); + const [ipArpCacheTimeout, setIpArpCacheTimeout] = useState(""); + const [ipArpCacheTimeoutIsCustom, setIpArpCacheTimeoutIsCustom] = useState(false); + const [ipSourceValidation, setIpSourceValidation] = useState(""); + const [ipDisableArpFilter, setIpDisableArpFilter] = useState(false); + const [ipDisableForwarding, setIpDisableForwarding] = useState(false); + const [ipEnableArpAccept, setIpEnableArpAccept] = useState(false); + const [ipEnableArpAnnounce, setIpEnableArpAnnounce] = useState(false); + const [ipEnableArpIgnore, setIpEnableArpIgnore] = useState(false); + const [ipEnableDirectedBroadcast, setIpEnableDirectedBroadcast] = useState(false); + const [ipEnableProxyArp, setIpEnableProxyArp] = useState(false); + const [ipProxyArpPvlan, setIpProxyArpPvlan] = useState(false); + + // IPv6 Settings + const [ipv6AcceptDad, setIpv6AcceptDad] = useState(""); + const [ipv6AdjustMss, setIpv6AdjustMss] = useState(""); + const [ipv6AdjustMssIsCustom, setIpv6AdjustMssIsCustom] = useState(false); + const [ipv6BaseReachableTime, setIpv6BaseReachableTime] = useState(""); + const [ipv6BaseReachableTimeIsCustom, setIpv6BaseReachableTimeIsCustom] = useState(false); + const [ipv6DupAddrDetectTransmits, setIpv6DupAddrDetectTransmits] = useState(""); + const [dadIsCustom, setDadIsCustom] = useState(false); + const [ipv6SourceValidation, setIpv6SourceValidation] = useState(""); + const [ipv6DisableForwarding, setIpv6DisableForwarding] = useState(false); + + // Advanced + const [mirrorIngress, setMirrorIngress] = useState(""); + const [mirrorEgress, setMirrorEgress] = useState(""); + const [redirect, setRedirect] = useState(""); + + const [availableInterfaces, setAvailableInterfaces] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const supportsNoRequestDns = capabilities?.features?.dhcpv6_no_request_dns?.supported ?? false; + const supportsNoRequestDomainName = capabilities?.features?.dhcpv6_no_request_domain_name?.supported ?? false; + const supportsInterfaceIdentifier = capabilities?.features?.ipv6_interface_identifier?.supported ?? false; + + const mtuMode = mtuIsCustom ? "custom" : (!mtu ? "default" : MTU_PRESETS.includes(mtu) ? mtu : "custom"); + const ipAdjustMssMode = ipAdjustMssIsCustom ? "custom" : (!ipAdjustMss ? "none" : ipAdjustMss === "clamp-mss-to-pmtu" ? "clamp" : "custom"); + const ipArpCacheTimeoutMode = ipArpCacheTimeoutIsCustom ? "custom" : (!ipArpCacheTimeout ? "none" : TIMEOUT_PRESETS.includes(ipArpCacheTimeout) ? ipArpCacheTimeout : "custom"); + const ipv6AdjustMssMode = ipv6AdjustMssIsCustom ? "custom" : (!ipv6AdjustMss ? "none" : ipv6AdjustMss === "clamp-mss-to-pmtu" ? "clamp" : "custom"); + const ipv6BaseReachableTimeMode = ipv6BaseReachableTimeIsCustom ? "custom" : (!ipv6BaseReachableTime ? "none" : TIMEOUT_PRESETS.includes(ipv6BaseReachableTime) ? ipv6BaseReachableTime : "custom"); + const dadTransmitsMode = dadIsCustom ? "custom" : (!ipv6DupAddrDetectTransmits ? "default" : DAD_PRESETS.includes(ipv6DupAddrDetectTransmits) ? ipv6DupAddrDetectTransmits : "custom"); + + const getNextInterfaceName = (): string => { + let i = 0; + while (existingInterfaces.includes(`wwan${i}`)) i++; + return `wwan${i}`; + }; + + const resetForm = () => { + setName(getNextInterfaceName()); + setApn(""); setAuthUsername(""); setAuthPassword(""); setShowPassword(false); + setConnectOnDemand(false); setDisableLinkDetect(false); setDisable(false); + setDescription(""); setMtu(""); setMtuIsCustom(false); setVrf(""); + setAddresses(""); + setDhcpClientId(""); setDhcpDefaultRouteDistance(""); setDhcpHostName(""); setDhcpMtu(""); + setDhcpNoDefaultRoute(false); setDhcpReject([]); setDhcpRejectInput(""); + setDhcpUserClass(""); setDhcpVendorClassId(""); + setDhcpv6Duid(""); setDhcpv6NoRelease(false); setDhcpv6ParametersOnly(false); + setDhcpv6RapidCommit(false); setDhcpv6Temporary(false); + setDhcpv6NoRequestDns(false); setDhcpv6NoRequestDomainName(false); + setDhcpv6Pd([]); setDhcpv6PdInput(""); + setIpv6AddressEui64(""); setIpv6AddressAutoconf(false); + setIpv6AddressNoDefaultLinkLocal(false); setIpv6AddressInterfaceIdentifier(""); + setIpAdjustMss(""); setIpAdjustMssIsCustom(false); + setIpArpCacheTimeout(""); setIpArpCacheTimeoutIsCustom(false); + setIpSourceValidation(""); + setIpDisableArpFilter(false); setIpDisableForwarding(false); + setIpEnableArpAccept(false); setIpEnableArpAnnounce(false); + setIpEnableArpIgnore(false); setIpEnableDirectedBroadcast(false); + setIpEnableProxyArp(false); setIpProxyArpPvlan(false); + setIpv6AcceptDad(""); + setIpv6AdjustMss(""); setIpv6AdjustMssIsCustom(false); + setIpv6BaseReachableTime(""); setIpv6BaseReachableTimeIsCustom(false); + setIpv6DupAddrDetectTransmits(""); setDadIsCustom(false); + setIpv6SourceValidation(""); setIpv6DisableForwarding(false); + setMirrorIngress(""); setMirrorEgress(""); setRedirect(""); + setError(null); + }; + + useEffect(() => { + if (open) { + resetForm(); + showService.getAllInterfaces().then((res) => setAvailableInterfaces(res.interfaces)).catch(() => {}); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [open]); + + const validateForm = (): string | null => { + if (!name.trim()) return "Interface name is required"; + if (!/^wwan\d+$/.test(name)) return "Name must be wwan0, wwan1, wwan2, …"; + if (existingInterfaces.includes(name)) return `Interface ${name} already exists`; + return null; + }; + + const handleSubmit = async () => { + const validationError = validateForm(); + if (validationError) { setError(validationError); return; } + + setLoading(true); + setError(null); + + try { + const addrList = addresses.split(/[\n,]/).map((a) => a.trim()).filter(Boolean); + const eui64List = ipv6AddressEui64.split(/[\n,]/).map((a) => a.trim()).filter(Boolean); + + const config: Parameters[0] = { name }; + + if (apn.trim()) config.apn = apn.trim(); + if (authUsername.trim()) config.auth_username = authUsername.trim(); + if (authPassword.trim()) config.auth_password = authPassword.trim(); + if (connectOnDemand) config.connect_on_demand = true; + if (disable) config.disable = true; + if (disableLinkDetect) config.disable_link_detect = true; + if (description.trim()) config.description = description.trim(); + if (mtu) config.mtu = mtu; + if (vrf.trim()) config.vrf = vrf.trim(); + if (addrList.length > 0) config.addresses = addrList; + if (mirrorIngress.trim()) config.mirror_ingress = mirrorIngress.trim(); + if (mirrorEgress.trim()) config.mirror_egress = mirrorEgress.trim(); + if (redirect.trim()) config.redirect = redirect.trim(); + if (dhcpClientId.trim()) config.dhcp_client_id = dhcpClientId.trim(); + if (dhcpDefaultRouteDistance.trim()) config.dhcp_default_route_distance = dhcpDefaultRouteDistance.trim(); + if (dhcpHostName.trim()) config.dhcp_host_name = dhcpHostName.trim(); + if (dhcpMtu.trim()) config.dhcp_mtu = dhcpMtu.trim(); + if (dhcpNoDefaultRoute) config.dhcp_no_default_route = true; + if (dhcpReject.length > 0) config.dhcp_reject = dhcpReject; + if (dhcpUserClass.trim()) config.dhcp_user_class = dhcpUserClass.trim(); + if (dhcpVendorClassId.trim()) config.dhcp_vendor_class_id = dhcpVendorClassId.trim(); + if (dhcpv6Duid.trim()) config.dhcpv6_duid = dhcpv6Duid.trim(); + if (dhcpv6NoRelease) config.dhcpv6_no_release = true; + if (dhcpv6ParametersOnly) config.dhcpv6_parameters_only = true; + if (dhcpv6RapidCommit) config.dhcpv6_rapid_commit = true; + if (dhcpv6Temporary) config.dhcpv6_temporary = true; + if (supportsNoRequestDns && dhcpv6NoRequestDns) config.dhcpv6_no_request_dns = true; + if (supportsNoRequestDomainName && dhcpv6NoRequestDomainName) config.dhcpv6_no_request_domain_name = true; + if (dhcpv6Pd.length > 0) config.dhcpv6_pd = dhcpv6Pd; + if (ipAdjustMss) config.ip_adjust_mss = ipAdjustMss; + if (ipArpCacheTimeout) config.ip_arp_cache_timeout = ipArpCacheTimeout; + if (ipDisableArpFilter) config.ip_disable_arp_filter = true; + if (ipDisableForwarding) config.ip_disable_forwarding = true; + if (ipEnableArpAccept) config.ip_enable_arp_accept = true; + if (ipEnableArpAnnounce) config.ip_enable_arp_announce = true; + if (ipEnableArpIgnore) config.ip_enable_arp_ignore = true; + if (ipEnableDirectedBroadcast) config.ip_enable_directed_broadcast = true; + if (ipEnableProxyArp) config.ip_enable_proxy_arp = true; + if (ipProxyArpPvlan) config.ip_proxy_arp_pvlan = true; + if (ipSourceValidation) config.ip_source_validation = ipSourceValidation; + if (ipv6AcceptDad) config.ipv6_accept_dad = ipv6AcceptDad; + if (ipv6AddressAutoconf) config.ipv6_address_autoconf = true; + if (eui64List.length > 0) config.ipv6_address_eui64 = eui64List; + if (ipv6AddressNoDefaultLinkLocal) config.ipv6_address_no_default_link_local = true; + if (supportsInterfaceIdentifier && ipv6AddressInterfaceIdentifier.trim()) config.ipv6_address_interface_identifier = ipv6AddressInterfaceIdentifier.trim(); + if (ipv6AdjustMss) config.ipv6_adjust_mss = ipv6AdjustMss; + if (ipv6BaseReachableTime) config.ipv6_base_reachable_time = ipv6BaseReachableTime; + if (ipv6DisableForwarding) config.ipv6_disable_forwarding = true; + if (ipv6DupAddrDetectTransmits) config.ipv6_dup_addr_detect_transmits = ipv6DupAddrDetectTransmits; + if (ipv6SourceValidation) config.ipv6_source_validation = ipv6SourceValidation; + + const result = await wwanService.createInterface(config); + if (result.success) { + onOpenChange(false); + onSuccess(); + } else { + setError(result.error || "Failed to create WWAN interface"); + } + } catch (err) { + const msg = (err as ApiError).message; + setError(typeof msg === "string" ? msg : JSON.stringify(msg, null, 2)); + } finally { + setLoading(false); + } + }; + + return ( + + + + + + Create WWAN Interface + + + Create a new Wireless WAN (cellular modem) interface. + + + + + + Connection + Basic + Addresses + IP + IPv6 + Advanced + + + {/* Connection Tab */} + +
+ + setName(e.target.value)} + placeholder="wwan0" + /> +

Must match pattern: wwan0, wwan1, wwan2, …

+
+ +
+ + setApn(e.target.value)} + placeholder="internet" + /> +

Access Point Name provided by your carrier

+
+ +
+ + setAuthUsername(e.target.value)} + placeholder="Optional" + autoComplete="off" + /> +
+ +
+ +
+ setAuthPassword(e.target.value)} + placeholder="Optional" + autoComplete="new-password" + className="pr-10" + /> + +
+
+ +
+ setConnectOnDemand(c === true)} /> + +
+ +
+ setDisableLinkDetect(c === true)} /> + +
+ +
+ setDisable(c === true)} /> + +
+
+ + {/* Basic Tab */} + +
+ + setDescription(e.target.value)} + placeholder="Optional description" + /> +
+ +
+ + + {mtuMode === "custom" && ( + setMtu(e.target.value)} + placeholder="Enter MTU (68–1500)" + className="mt-2" + /> + )} +
+ +
+ + setVrf(e.target.value)} + placeholder="Optional VRF name" + /> +
+
+ + {/* Addresses Tab */} + +
+ +