diff --git a/CHANGELOG.md b/CHANGELOG.md index a18e4d2f..cc52a197 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -77,6 +77,10 @@ SessionManager. - Interface configuration to establish connectivity and define network parameters for external, internal, and DMZ interfaces. - Protocol and service management to oversee traffic and enforce security policies. - Dynamic traffic processing and filtering to ensure network security and integrity. +- `AirSpace` class to simulate wireless communications, managing wireless interfaces and facilitating the transmission of frames within specified frequencies. +- `AirSpaceFrequency` enum for defining standard wireless frequencies, including 2.4 GHz and 5 GHz bands, to support realistic wireless network simulations. +- `WirelessRouter` class, extending the `Router` class, to incorporate wireless networking capabilities alongside traditional wired connections. This class allows the configuration of wireless access points with specific IP settings and operating frequencies. + ### Changed - Integrated the RouteTable into the Routers frame processing. diff --git a/src/primaite/simulator/network/airspace.py b/src/primaite/simulator/network/airspace.py new file mode 100644 index 00000000..56cd1cc7 --- /dev/null +++ b/src/primaite/simulator/network/airspace.py @@ -0,0 +1,308 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from enum import Enum +from typing import Any, Dict, Final, List, Optional + +from prettytable import PrettyTable + +from primaite import getLogger +from primaite.simulator.network.hardware.base import Layer3Interface, NetworkInterface, WiredNetworkInterface +from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState +from primaite.simulator.network.transmission.data_link_layer import Frame +from primaite.simulator.system.core.packet_capture import PacketCapture + +_LOGGER = getLogger(__name__) + +__all__ = ["AIR_SPACE", "AirSpaceFrequency", "WirelessNetworkInterface", "IPWirelessNetworkInterface"] + + +class AirSpace: + """Represents a wireless airspace, managing wireless network interfaces and handling wireless transmission.""" + + def __init__(self): + self._wireless_interfaces: Dict[str, WirelessNetworkInterface] = {} + self._wireless_interfaces_by_frequency: Dict[AirSpaceFrequency, List[WirelessNetworkInterface]] = {} + + def show(self, frequency: Optional[AirSpaceFrequency] = None): + """ + Displays a summary of wireless interfaces in the airspace, optionally filtered by a specific frequency. + + :param frequency: The frequency band to filter devices by. If None, devices for all frequencies are shown. + """ + table = PrettyTable() + table.field_names = ["Connected Node", "MAC Address", "IP Address", "Subnet Mask", "Frequency", "Status"] + + # If a specific frequency is provided, filter by it; otherwise, use all frequencies. + frequencies_to_show = [frequency] if frequency else self._wireless_interfaces_by_frequency.keys() + + for freq in frequencies_to_show: + interfaces = self._wireless_interfaces_by_frequency.get(freq, []) + for interface in interfaces: + status = "Enabled" if interface.enabled else "Disabled" + table.add_row( + [ + interface._connected_node.hostname, # noqa + interface.mac_address, + interface.ip_address if hasattr(interface, "ip_address") else None, + interface.subnet_mask if hasattr(interface, "subnet_mask") else None, + str(freq), + status, + ] + ) + + print(table) + + def add_wireless_interface(self, wireless_interface: WirelessNetworkInterface): + """ + Adds a wireless network interface to the airspace if it's not already present. + + :param wireless_interface: The wireless network interface to be added. + """ + if wireless_interface.mac_address not in self._wireless_interfaces: + self._wireless_interfaces[wireless_interface.mac_address] = wireless_interface + if wireless_interface.frequency not in self._wireless_interfaces_by_frequency: + self._wireless_interfaces_by_frequency[wireless_interface.frequency] = [] + self._wireless_interfaces_by_frequency[wireless_interface.frequency].append(wireless_interface) + + def remove_wireless_interface(self, wireless_interface: WirelessNetworkInterface): + """ + Removes a wireless network interface from the airspace if it's present. + + :param wireless_interface: The wireless network interface to be removed. + """ + if wireless_interface.mac_address in self._wireless_interfaces: + self._wireless_interfaces.pop(wireless_interface.mac_address) + self._wireless_interfaces_by_frequency[wireless_interface.frequency].remove(wireless_interface) + + def clear(self): + """ + Clears all wireless network interfaces and their frequency associations from the airspace. + + After calling this method, the airspace will contain no wireless network interfaces, and transmissions cannot + occur until new interfaces are added again. + """ + self._wireless_interfaces.clear() + self._wireless_interfaces_by_frequency.clear() + + def transmit(self, frame: Frame, sender_network_interface: WirelessNetworkInterface): + """ + Transmits a frame to all enabled wireless network interfaces on a specific frequency within the airspace. + + This ensures that a wireless interface does not receive its own transmission. + + :param frame: The frame to be transmitted. + :param sender_network_interface: The wireless network interface sending the frame. This interface will be + excluded from the list of receivers to prevent it from receiving its own transmission. + """ + for wireless_interface in self._wireless_interfaces_by_frequency.get(sender_network_interface.frequency, []): + if wireless_interface != sender_network_interface and wireless_interface.enabled: + wireless_interface.receive_frame(frame) + + +AIR_SPACE: Final[AirSpace] = AirSpace() +""" +A singleton instance of the AirSpace class, representing the global wireless airspace. + +This instance acts as the central management point for all wireless communications within the simulated network +environment. By default, there is only one airspace in the simulation, making this variable a singleton that +manages the registration, removal, and transmission of wireless frames across all wireless network interfaces configured +in the simulation. It ensures that wireless frames are appropriately transmitted to and received by wireless +interfaces based on their operational status and frequency band. +""" + + +class AirSpaceFrequency(Enum): + """Enumeration representing the operating frequencies for wireless communications.""" + + WIFI_2_4 = 2.4e9 + """WiFi 2.4 GHz. Known for its extensive range and ability to penetrate solid objects effectively.""" + WIFI_5 = 5e9 + """WiFi 5 GHz. Known for its higher data transmission speeds and reduced interference from other devices.""" + + def __str__(self) -> str: + if self == AirSpaceFrequency.WIFI_2_4: + return "WiFi 2.4 GHz" + elif self == AirSpaceFrequency.WIFI_5: + return "WiFi 5 GHz" + else: + return "Unknown Frequency" + + +class WirelessNetworkInterface(NetworkInterface, ABC): + """ + Represents a wireless network interface in a network device. + + This abstract base class models wireless network interfaces, encapsulating properties and behaviors specific to + wireless connectivity. It provides a framework for managing wireless connections, including signal strength, + security protocols, and other wireless-specific attributes and methods. + + Wireless network interfaces differ from wired ones in their medium of communication, relying on radio frequencies + for data transmission and reception. This class serves as a base for more specific types of wireless network + interfaces, such as Wi-Fi adapters or radio network interfaces, ensuring that essential wireless functionality is + defined and standardised. + + Inherits from: + - NetworkInterface: Provides basic network interface properties and methods. + + As an abstract base class, it requires subclasses to implement specific methods related to wireless communication + and may define additional properties and methods specific to wireless technology. + """ + + frequency: AirSpaceFrequency = AirSpaceFrequency.WIFI_2_4 + + def enable(self): + """Attempt to enable the network interface.""" + if self.enabled: + return + + if not self._connected_node: + _LOGGER.error(f"Interface {self} cannot be enabled as it is not connected to a Node") + return + + if self._connected_node.operating_state != NodeOperatingState.ON: + self._connected_node.sys_log.info( + f"Interface {self} cannot be enabled as the connected Node is not powered on" + ) + return + + self.enabled = True + self._connected_node.sys_log.info(f"Network Interface {self} enabled") + self.pcap = PacketCapture(hostname=self._connected_node.hostname, interface_num=self.port_num) + AIR_SPACE.add_wireless_interface(self) + + def disable(self): + """Disable the network interface.""" + if not self.enabled: + return + self.enabled = False + if self._connected_node: + self._connected_node.sys_log.info(f"Network Interface {self} disabled") + else: + _LOGGER.debug(f"Interface {self} disabled") + AIR_SPACE.remove_wireless_interface(self) + + def send_frame(self, frame: Frame) -> bool: + """ + Attempts to send a network frame over the airspace. + + This method sends a frame if the network interface is enabled and connected to a wireless airspace. It captures + the frame using PCAP (if available) and transmits it through the airspace. Returns True if the frame is + successfully sent, False otherwise (e.g., if the network interface is disabled). + + :param frame: The network frame to be sent. + :return: True if the frame is sent successfully, False if the network interface is disabled. + """ + if self.enabled: + frame.set_sent_timestamp() + self.pcap.capture_outbound(frame) + AIR_SPACE.transmit(frame, self) + return True + # Cannot send Frame as the network interface is not enabled + return False + + @abstractmethod + def receive_frame(self, frame: Frame) -> bool: + """ + Receives a network frame on the network interface. + + :param frame: The network frame being received. + :return: A boolean indicating whether the frame was successfully received. + """ + pass + + +class IPWirelessNetworkInterface(WirelessNetworkInterface, Layer3Interface, ABC): + """ + Represents an IP wireless network interface. + + This interface operates at both the data link layer (Layer 2) and the network layer (Layer 3) of the OSI model, + specifically tailored for IP-based communication over wireless connections. This abstract class provides a + template for creating specific wireless network interfaces that support Internet Protocol (IP) functionalities. + + As this class is a combination of its parent classes without additional attributes or methods, please refer to + the documentation of `WirelessNetworkInterface` and `Layer3Interface` for more details on the supported operations + and functionalities. + + The class inherits from: + - `WirelessNetworkInterface`: Providing the functionalities and characteristics of a wireless connection, such as + managing wireless signal transmission, reception, and associated wireless protocols. + - `Layer3Interface`: Enabling network layer capabilities, including IP address assignment, routing, and + potentially, Layer 3 protocols like IPsec. + + As an abstract class, `IPWirelessNetworkInterface` does not implement specific methods but ensures that any derived + class provides implementations for the functionalities of both `WirelessNetworkInterface` and `Layer3Interface`. + This setup is ideal for representing network interfaces in devices that require wireless connections and are capable + of IP routing and addressing, such as wireless routers, access points, and wireless end-host devices like + smartphones and laptops. + + This class should be extended by concrete classes that define specific behaviors and properties of an IP-capable + wireless network interface. + """ + + def model_post_init(self, __context: Any) -> None: + """ + Performs post-initialisation checks to ensure the model's IP configuration is valid. + + This method is invoked after the initialisation of a network model object to validate its network settings, + particularly to ensure that the assigned IP address is not a network address. This validation is crucial for + maintaining the integrity of network simulations and avoiding configuration errors that could lead to + unrealistic or incorrect behavior. + + :param __context: Contextual information or parameters passed to the method, used for further initializing or + validating the model post-creation. + :raises ValueError: If the IP address is the same as the network address, indicating an incorrect configuration. + """ + if self.ip_network.network_address == self.ip_address: + raise ValueError(f"{self.ip_address}/{self.subnet_mask} must not be a network address") + + def describe_state(self) -> Dict: + """ + Produce a dictionary describing the current state of this object. + + :return: Current state of this object and child objects. + :rtype: Dict + """ + # Get the state from the WiredNetworkInterface + state = WiredNetworkInterface.describe_state(self) + + # Update the state with information from Layer3Interface + state.update(Layer3Interface.describe_state(self)) + + state["frequency"] = self.frequency + + return state + + def set_original_state(self): + """Sets the original state.""" + vals_to_include = {"ip_address", "subnet_mask", "mac_address", "speed", "mtu", "wake_on_lan", "enabled"} + self._original_state = self.model_dump(include=vals_to_include) + + def enable(self): + """ + Enables this wired network interface and attempts to send a "hello" message to the default gateway. + + This method activates the network interface, making it operational for network communications. After enabling, + it tries to initiate a default gateway "hello" process, typically to establish initial connectivity and resolve + the default gateway's MAC address. This step is crucial for ensuring the interface can successfully send data + to and receive data from the network. + + The method safely handles cases where the connected node might not have a default gateway set or the + `default_gateway_hello` method is not defined, ignoring such errors to proceed without interruption. + """ + super().enable() + try: + pass + self._connected_node.default_gateway_hello() + except AttributeError: + pass + + @abstractmethod + def receive_frame(self, frame: Frame) -> bool: + """ + Receives a network frame on the interface. + + :param frame: The network frame being received. + :return: A boolean indicating whether the frame was successfully received. + """ + pass diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 55640121..7354725a 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -420,86 +420,6 @@ class IPWiredNetworkInterface(WiredNetworkInterface, Layer3Interface, ABC): pass -class WirelessNetworkInterface(NetworkInterface, ABC): - """ - Represents a wireless network interface in a network device. - - This abstract base class models wireless network interfaces, encapsulating properties and behaviors specific to - wireless connectivity. It provides a framework for managing wireless connections, including signal strength, - security protocols, and other wireless-specific attributes and methods. - - Wireless network interfaces differ from wired ones in their medium of communication, relying on radio frequencies - for data transmission and reception. This class serves as a base for more specific types of wireless interfaces, - such as Wi-Fi adapters or radio network interfaces, ensuring that essential wireless functionality is defined - and standardised. - - Inherits from: - - NetworkInterface: Provides basic network interface properties and methods. - - As an abstract base class, it requires subclasses to implement specific methods related to wireless communication - and may define additional properties and methods specific to wireless technology. - """ - - -class IPWirelessNetworkInterface(WiredNetworkInterface, Layer3Interface, ABC): - """ - Represents an IP wireless network interface. - - This interface operates at both the data link layer (Layer 2) and the network layer (Layer 3) of the OSI model, - specifically tailored for IP-based communication over wireless connections. This abstract class provides a - template for creating specific wireless network interfaces that support Internet Protocol (IP) functionalities. - - As this class is a combination of its parent classes without additional attributes or methods, please refer to - the documentation of `WirelessNetworkInterface` and `Layer3Interface` for more details on the supported operations - and functionalities. - - The class inherits from: - - `WirelessNetworkInterface`: Providing the functionalities and characteristics of a wireless connection, such as - managing wireless signal transmission, reception, and associated wireless protocols. - - `Layer3Interface`: Enabling network layer capabilities, including IP address assignment, routing, and - potentially, Layer 3 protocols like IPsec. - - As an abstract class, `IPWirelessNetworkInterface` does not implement specific methods but ensures that any derived - class provides implementations for the functionalities of both `WirelessNetworkInterface` and `Layer3Interface`. - This setup is ideal for representing network interfaces in devices that require wireless connections and are capable - of IP routing and addressing, such as wireless routers, access points, and wireless end-host devices like - smartphones and laptops. - - This class should be extended by concrete classes that define specific behaviors and properties of an IP-capable - wireless network interface. - """ - - @abstractmethod - def enable(self): - """Enable the interface.""" - pass - - @abstractmethod - def disable(self): - """Disable the interface.""" - pass - - @abstractmethod - def send_frame(self, frame: Frame) -> bool: - """ - Attempts to send a network frame through the interface. - - :param frame: The network frame to be sent. - :return: A boolean indicating whether the frame was successfully sent. - """ - pass - - @abstractmethod - def receive_frame(self, frame: Frame) -> bool: - """ - Receives a network frame on the interface. - - :param frame: The network frame being received. - :return: A boolean indicating whether the frame was successfully received. - """ - pass - - class Link(SimComponent): """ Represents a network link between NIC<-->NIC, NIC<-->SwitchPort, or SwitchPort<-->SwitchPort. diff --git a/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py b/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py new file mode 100644 index 00000000..3a797031 --- /dev/null +++ b/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py @@ -0,0 +1,217 @@ +from typing import Any, Dict, Union + +from pydantic import validate_call + +from primaite.simulator.network.airspace import AirSpaceFrequency, IPWirelessNetworkInterface +from primaite.simulator.network.hardware.nodes.network.router import Router, RouterInterface +from primaite.simulator.network.transmission.data_link_layer import Frame +from primaite.utils.validators import IPV4Address + + +class WirelessAccessPoint(IPWirelessNetworkInterface): + """ + Represents a Wireless Access Point (AP) in a network. + + This class models a Wireless Access Point, a device that allows wireless devices to connect to a wired network + using Wi-Fi or other wireless standards. The Wireless Access Point bridges the wireless and wired segments of + the network, allowing wireless devices to communicate with other devices on the network. + + As an integral component of wireless networking, a Wireless Access Point provides functionalities for network + management, signal broadcasting, security enforcement, and connection handling. It also possesses Layer 3 + capabilities such as IP addressing and subnetting, allowing for network segmentation and routing. + + Inherits from: + - WirelessNetworkInterface: Provides basic properties and methods specific to wireless interfaces. + - Layer3Interface: Provides Layer 3 properties like ip_address and subnet_mask, enabling the device to manage + network traffic and routing. + + This class can be further specialised or extended to support specific features or standards related to wireless + networking, such as different Wi-Fi versions, frequency bands, or advanced security protocols. + """ + + def model_post_init(self, __context: Any) -> None: + """ + Performs post-initialisation checks to ensure the model's IP configuration is valid. + + This method is invoked after the initialisation of a network model object to validate its network settings, + particularly to ensure that the assigned IP address is not a network address. This validation is crucial for + maintaining the integrity of network simulations and avoiding configuration errors that could lead to + unrealistic or incorrect behavior. + + :param __context: Contextual information or parameters passed to the method, used for further initializing or + validating the model post-creation. + :raises ValueError: If the IP address is the same as the network address, indicating an incorrect configuration. + """ + if self.ip_network.network_address == self.ip_address: + raise ValueError(f"{self.ip_address}/{self.subnet_mask} must not be a network address") + + def describe_state(self) -> Dict: + """ + Produce a dictionary describing the current state of this object. + + :return: Current state of this object and child objects. + :rtype: Dict + """ + return super().describe_state() + + def receive_frame(self, frame: Frame) -> bool: + """ + Receives a network frame on the interface. + + :param frame: The network frame being received. + :return: A boolean indicating whether the frame was successfully received. + """ + if self.enabled: + frame.decrement_ttl() + if frame.ip and frame.ip.ttl < 1: + self._connected_node.sys_log.info("Frame discarded as TTL limit reached") + return False + frame.set_received_timestamp() + self.pcap.capture_inbound(frame) + # If this destination or is broadcast + if frame.ethernet.dst_mac_addr == self.mac_address or frame.ethernet.dst_mac_addr == "ff:ff:ff:ff:ff:ff": + self._connected_node.receive_frame(frame=frame, from_network_interface=self) + return True + return False + + def __str__(self) -> str: + """ + String representation of the NIC. + + :return: A string combining the port number, MAC address and IP address of the NIC. + """ + return f"Port {self.port_num}: {self.mac_address}/{self.ip_address} ({self.frequency})" + + +class WirelessRouter(Router): + """ + A WirelessRouter class that extends the functionality of a standard Router to include wireless capabilities. + + This class represents a network device that performs routing functions similar to a traditional router but also + includes the functionality of a wireless access point. This allows the WirelessRouter to not only direct traffic + between wired networks but also to manage and facilitate wireless network connections. + + A WirelessRouter is instantiated and configured with both wired and wireless interfaces. The wired interfaces are + managed similarly to those in a standard Router, while the wireless interfaces require additional configuration + specific to wireless settings, such as setting the frequency band (e.g., 2.4 GHz or 5 GHz for Wi-Fi). + + The WirelessRouter facilitates creating a network environment where devices can be interconnected via both + Ethernet (wired) and Wi-Fi (wireless), making it an essential component for simulating more complex and realistic + network topologies within PrimAITE. + + Example: + >>> wireless_router = WirelessRouter(hostname="wireless_router_1") + >>> wireless_router.configure_router_interface( + ... ip_address="192.168.1.1", + ... subnet_mask="255.255.255.0" + ... ) + >>> wireless_router.configure_wireless_access_point( + ... ip_address="10.10.10.1", + ... subnet_mask="255.255.255.0" + ... frequency=AirSpaceFrequency.WIFI_2_4 + ... ) + """ + + network_interfaces: Dict[str, Union[RouterInterface, WirelessAccessPoint]] = {} + network_interface: Dict[int, Union[RouterInterface, WirelessAccessPoint]] = {} + + def __init__(self, hostname: str, **kwargs): + super().__init__(hostname=hostname, num_ports=0, **kwargs) + + wap = WirelessAccessPoint(ip_address="127.0.0.1", subnet_mask="255.0.0.0", gateway="0.0.0.0") + wap.port_num = 1 + self.connect_nic(wap) + self.network_interface[1] = wap + + router_interface = RouterInterface(ip_address="127.0.0.1", subnet_mask="255.0.0.0", gateway="0.0.0.0") + router_interface.port_num = 2 + self.connect_nic(router_interface) + self.network_interface[2] = router_interface + + self.set_original_state() + + @property + def wireless_access_point(self) -> WirelessAccessPoint: + """ + Retrieves the wireless access point interface associated with this wireless router. + + This property provides direct access to the WirelessAccessPoint interface of the router, facilitating wireless + communications. Specifically, it returns the interface configured on port 1, dedicated to establishing and + managing wireless network connections. This interface is essential for enabling wireless connectivity, + allowing devices within connect to the network wirelessly. + + :return: The WirelessAccessPoint instance representing the wireless connection interface on port 1 of the + wireless router. + """ + return self.network_interface[1] + + @validate_call() + def configure_wireless_access_point( + self, + ip_address: IPV4Address, + subnet_mask: IPV4Address, + frequency: AirSpaceFrequency = AirSpaceFrequency.WIFI_2_4, + ): + """ + Configures a wireless access point (WAP). + + Sets its IP address, subnet mask, and operating frequency. This method ensures the wireless access point is + properly set up to manage wireless communication over the specified frequency band. + + The method first disables the WAP to safely apply configuration changes. After configuring the IP settings, + it sets the WAP to operate on the specified frequency band and then re-enables the WAP for operation. + + :param ip_address: The IP address to be assigned to the wireless access point. + :param subnet_mask: The subnet mask associated with the IP address + :param frequency: The operating frequency of the wireless access point, defined by the AirSpaceFrequency + enum. This determines the frequency band (e.g., 2.4 GHz or 5 GHz) the access point will use for wireless + communication. Default is AirSpaceFrequency.WIFI_2_4. + """ + self.wireless_access_point.disable() # Temporarily disable the WAP for reconfiguration + network_interface = self.network_interface[1] + network_interface.ip_address = ip_address + network_interface.subnet_mask = subnet_mask + self.sys_log.info(f"Configured WAP {network_interface}") + self.set_original_state() + self.wireless_access_point.frequency = frequency # Set operating frequency + self.wireless_access_point.enable() # Re-enable the WAP with new settings + + @property + def router_interface(self) -> RouterInterface: + """ + Retrieves the router interface associated with this wireless router. + + This property provides access to the router interface configured for wired connections. It specifically + returns the interface configured on port 2, which is reserved for wired LAN/WAN connections. + + :return: The RouterInterface instance representing the wired LAN/WAN connection on port 2 of the wireless + router. + """ + return self.network_interface[2] + + @validate_call() + def configure_router_interface( + self, + ip_address: IPV4Address, + subnet_mask: IPV4Address, + ): + """ + Configures a router interface. + + Sets its IP address and subnet mask. + + The method first disables the router interface to safely apply configuration changes. After configuring the IP + settings, it re-enables the router interface for operation. + + :param ip_address: The IP address to be assigned to the router interface. + :param subnet_mask: The subnet mask associated with the IP address + """ + self.router_interface.disable() # Temporarily disable the router interface for reconfiguration + super().configure_port(port=2, ip_address=ip_address, subnet_mask=subnet_mask) # Set IP configuration + self.router_interface.enable() # Re-enable the router interface with new settings + + def configure_port(self, port: int, ip_address: Union[IPV4Address, str], subnet_mask: Union[IPV4Address, str]): + """Not Implemented.""" + raise NotImplementedError( + "Please use the 'configure_wireless_access_point' and 'configure_router_interface' functions." + )