diff --git a/docs/source/simulation.rst b/docs/source/simulation.rst index 56761517..c703b299 100644 --- a/docs/source/simulation.rst +++ b/docs/source/simulation.rst @@ -22,6 +22,7 @@ Contents simulation_components/network/nodes/host_node simulation_components/network/nodes/network_node simulation_components/network/nodes/router + simulation_components/network/nodes/wireless_router simulation_components/network/nodes/firewall simulation_components/network/switch simulation_components/network/network diff --git a/docs/source/simulation_components/network/nodes/wireless_router.rst b/docs/source/simulation_components/network/nodes/wireless_router.rst new file mode 100644 index 00000000..75cbe0f7 --- /dev/null +++ b/docs/source/simulation_components/network/nodes/wireless_router.rst @@ -0,0 +1,193 @@ +.. only:: comment + + © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK + +###### +Router +###### + +The ``WirelessRouter`` class extends the functionality of the standard ``Router`` class within PrimAITE, +integrating wireless networking capabilities. This class enables the simulation of a router that supports both wired +and wireless connections, allowing for a more comprehensive network simulation environment. + +Overview +-------- + +The ``WirelessRouter`` class is designed to simulate the operations of a real-world wireless router, offering both +Ethernet and Wi-Fi connectivity. This includes managing wireless access points, configuring network interfaces for +different frequencies, and handling wireless frames transmission. + +Features +-------- + +- **Dual Interface Support:** Supports both wired (Ethernet) and wireless network interfaces. +- **Wireless Access Point Configuration:** Allows configuring a wireless access point, including setting its IP + address, subnet mask, and operating frequency. +- **Frequency Management:** Utilises the ``AirSpaceFrequency`` enum to set the operating frequency of wireless + interfaces, supporting common Wi-Fi bands like 2.4 GHz and 5 GHz. +- **Seamless Wireless Communication:** Integrates with the ``AirSpace`` class to manage wireless transmissions across + different frequencies, ensuring that wireless communication is realistically simulated. + +Usage +----- + +To use the ``WirelessRouter`` class in a network simulation, instantiate it similarly to a regular router but with +additional steps to configure wireless settings: + +.. code-block:: python + + from primaite.simulator.network.hardware.nodes.network.wireless_router import WirelessRouter + from primaite.simulator.network.airspace import AirSpaceFrequency + + # Instantiate the WirelessRouter + wireless_router = WirelessRouter(hostname="MyWirelessRouter") + + # Configure a wired Ethernet interface + wireless_router.configure_port(port=2, ip_address="192.168.1.1", subnet_mask="255.255.255.0") + + # Configure a wireless access point + wireless_router.configure_wireless_access_point( + port=1, ip_address="192.168.2.1", + subnet_mask="255.255.255.0", + frequency=AirSpaceFrequency.WIFI_2_4 + ) + + + +Integration with AirSpace +------------------------- + +The ``WirelessRouter`` class works closely with the ``AirSpace`` class to simulate the transmission of wireless frames. +Frames sent from wireless interfaces are transmitted across the simulated airspace, allowing for interactions with +other wireless devices within the same frequency band. + +Example Scenario +---------------- + +This example sets up a network with two PCs (PC A and PC B), each connected to their own `WirelessRouter` +(Router 1 and Router 2). These routers are then wirelessly connected to each other, enabling communication between the +PCs through the routers over the airspace. Access Control Lists (ACLs) are configured on the routers to permit ARP and +ICMP traffic, ensuring basic network connectivity and ping functionality. + +.. code-block:: python + + from primaite.simulator.network.airspace import AIR_SPACE, AirSpaceFrequency + from primaite.simulator.network.container import Network + from primaite.simulator.network.hardware.nodes.host.computer import Computer + from primaite.simulator.network.hardware.nodes.network.router import ACLAction + from primaite.simulator.network.hardware.nodes.network.wireless_router import WirelessRouter + from primaite.simulator.network.transmission.network_layer import IPProtocol + from primaite.simulator.network.transmission.transport_layer import Port + + network = Network() + + # Configure PC A + pc_a = Computer( + hostname="pc_a", + ip_address="192.168.0.2", + subnet_mask="255.255.255.0", + default_gateway="192.168.0.1", + start_up_duration=0, + ) + pc_a.power_on() + network.add_node(pc_a) + + # Configure Router 1 + router_1 = WirelessRouter(hostname="router_1", start_up_duration=0) + router_1.power_on() + network.add_node(router_1) + + # Configure the connection between PC A and Router 1 port 2 + router_1.configure_router_interface("192.168.0.1", "255.255.255.0") + network.connect(pc_a.network_interface[1], router_1.router_interface) + + # Configure Router 1 ACLs + router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.ARP, dst_port=Port.ARP, position=22) + router_1.acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol.ICMP, position=23) + + # Configure PC B + pc_b = Computer( + hostname="pc_b", + ip_address="192.168.2.2", + subnet_mask="255.255.255.0", + default_gateway="192.168.2.1", + start_up_duration=0, + ) + pc_b.power_on() + network.add_node(pc_b) + + # Configure Router 2 + router_2 = WirelessRouter(hostname="router_2", start_up_duration=0) + router_2.power_on() + network.add_node(router_2) + + # Configure the connection between PC B and Router 2 port 2 + router_2.configure_router_interface("192.168.2.1", "255.255.255.0") + network.connect(pc_b.network_interface[1], router_2.router_interface) + + # Configure the wireless connection between Router 1 and Router 2 + router_1.configure_wireless_access_point( + port=1, + ip_address="192.168.1.1", + subnet_mask="255.255.255.0", + frequency=AirSpaceFrequency.WIFI_2_4 + ) + router_2.configure_wireless_access_point( + port=1, + ip_address="192.168.1.2", + subnet_mask="255.255.255.0", + frequency=AirSpaceFrequency.WIFI_2_4 + ) + + # Configure routes for inter-router communication + router_1.route_table.add_route( + address="192.168.2.0", subnet_mask="255.255.255.0", next_hop_ip_address="192.168.1.2" + ) + + router_2.route_table.add_route( + address="192.168.0.0", subnet_mask="255.255.255.0", next_hop_ip_address="192.168.1.1" + ) + + # Test connectivity + print(pc_a.ping(pc_b.network_interface[1].ip_address)) + print(pc_b.ping(pc_a.network_interface[1].ip_address)) + +This setup demonstrates the `WirelessRouter` class's capability to manage both wired and wireless connections within a +simulated network environment. By configuring the wireless access points and enabling the appropriate ACL rules, the +example facilitates basic network operations such as ARP resolution and ICMP pinging between devices across different +network segments. + +Viewing Wireless Network Configuration +-------------------------------------- + +The `AirSpace.show()` function is an invaluable tool for inspecting the current wireless network configuration within +the PrimAITE environment. It presents a table summarising all wireless interfaces, including routers and access points, +that are active within the airspace. The table outlines each device's connected node name, MAC address, IP address, +subnet mask, operating frequency, and status, providing a comprehensive view of the wireless network topology. + +Example Output +^^^^^^^^^^^^^^^ + +Below is an example output of the `AirSpace.show()` function, demonstrating the visibility it provides into the +wireless network: + +.. code-block:: none + + +----------------+-------------------+-------------+---------------+--------------+---------+ + | Connected Node | MAC Address | IP Address | Subnet Mask | Frequency | Status | + +----------------+-------------------+-------------+---------------+--------------+---------+ + | router_1 | 31:29:46:53:ed:f8 | 192.168.1.1 | 255.255.255.0 | WiFi 2.4 GHz | Enabled | + | router_2 | 34:c8:47:43:98:78 | 192.168.1.2 | 255.255.255.0 | WiFi 2.4 GHz | Enabled | + +----------------+-------------------+-------------+---------------+--------------+---------+ + +This table aids in verifying that wireless devices are correctly configured and operational. It also helps in +diagnosing connectivity issues by ensuring that devices are on the correct frequency and have the appropriate network +settings. The `Status` column, indicating whether a device is enabled or disabled, further assists in troubleshooting +by quickly identifying any devices that are not actively participating in the network. + +Utilising the `AirSpace.show()` function is particularly beneficial in complex network simulations where multiple +wireless devices are in use. It provides a snapshot of the wireless landscape, facilitating the understanding of how +devices interact within the network and ensuring that configurations are aligned with the intended network architecture. + +The addition of the ``WirelessRouter`` class enriches the PrimAITE simulation toolkit by enabling the simulation of +mixed wired and wireless network environments. diff --git a/src/primaite/simulator/network/hardware/nodes/network/firewall.py b/src/primaite/simulator/network/hardware/nodes/network/firewall.py index bccfeab1..22effa2a 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/firewall.py +++ b/src/primaite/simulator/network/hardware/nodes/network/firewall.py @@ -122,6 +122,8 @@ class Firewall(Router): "internal_outbound_acl", "dmz_inbound_acl", "dmz_outbound_acl", + "external_inbound_acl", + "external_outbound_acl", } self._original_state.update(self.model_dump(include=vals_to_include)) @@ -142,6 +144,8 @@ class Firewall(Router): "internal_outbound_acl": self.internal_outbound_acl.describe_state(), "dmz_inbound_acl": self.dmz_inbound_acl.describe_state(), "dmz_outbound_acl": self.dmz_outbound_acl.describe_state(), + "external_inbound_acl": self.external_inbound_acl.describe_state(), + "external_outbound_acl": self.external_outbound_acl.describe_state(), } ) @@ -263,12 +267,11 @@ class Firewall(Router): if not permitted: self.sys_log.info(f"Frame blocked at interface {from_network_interface} by rule {rule}") return - else: - self.software_manager.arp.add_arp_cache_entry( - ip_address=frame.ip.src_ip_address, - mac_address=frame.ethernet.src_mac_addr, - network_interface=from_network_interface, - ) + self.software_manager.arp.add_arp_cache_entry( + ip_address=frame.ip.src_ip_address, + mac_address=frame.ethernet.src_mac_addr, + network_interface=from_network_interface, + ) if self.check_send_frame_to_session_manager(frame): # Port is open on this Router so pass Frame up to session manager first @@ -332,12 +335,11 @@ class Firewall(Router): if not permitted: self.sys_log.info(f"Frame blocked at interface {from_network_interface} by rule {rule}") return - else: - self.software_manager.arp.add_arp_cache_entry( - ip_address=frame.ip.src_ip_address, - mac_address=frame.ethernet.src_mac_addr, - network_interface=from_network_interface, - ) + self.software_manager.arp.add_arp_cache_entry( + ip_address=frame.ip.src_ip_address, + mac_address=frame.ethernet.src_mac_addr, + network_interface=from_network_interface, + ) if self.check_send_frame_to_session_manager(frame): # Port is open on this Router so pass Frame up to session manager first @@ -387,12 +389,11 @@ class Firewall(Router): if not permitted: self.sys_log.info(f"Frame blocked at interface {from_network_interface} by rule {rule}") return - else: - self.software_manager.arp.add_arp_cache_entry( - ip_address=frame.ip.src_ip_address, - mac_address=frame.ethernet.src_mac_addr, - network_interface=from_network_interface, - ) + self.software_manager.arp.add_arp_cache_entry( + ip_address=frame.ip.src_ip_address, + mac_address=frame.ethernet.src_mac_addr, + network_interface=from_network_interface, + ) if self.check_send_frame_to_session_manager(frame): # Port is open on this Router so pass Frame up to session manager first diff --git a/src/primaite/simulator/network/hardware/nodes/network/router.py b/src/primaite/simulator/network/hardware/nodes/network/router.py index 0d5b3d76..bb7b8a83 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/router.py @@ -35,9 +35,9 @@ def ip_matches_masked_range(ip_to_check: IPV4Address, base_ip: IPV4Address, wild by the wildcard mask. If the resulting masked IP addresses are equal, it means the IP address to check falls within the range defined by the base IP and wildcard mask. - :param IPv4Address ip_to_check: The IP address to be checked. - :param IPv4Address base_ip: The base IP address defining the start of the range. - :param IPv4Address wildcard_mask: The wildcard mask specifying which bits to ignore. + :param IPV4Address ip_to_check: The IP address to be checked. + :param IPV4Address base_ip: The base IP address defining the start of the range. + :param IPV4Address wildcard_mask: The wildcard mask specifying which bits to ignore. :return: A boolean value indicating whether the IP address matches the masked range. :rtype: bool diff --git a/tests/integration_tests/network/test_wireless_router.py b/tests/integration_tests/network/test_wireless_router.py new file mode 100644 index 00000000..1b55c1b6 --- /dev/null +++ b/tests/integration_tests/network/test_wireless_router.py @@ -0,0 +1,134 @@ +import pytest + +from primaite.simulator.network.airspace import AIR_SPACE, AirSpaceFrequency +from primaite.simulator.network.container import Network +from primaite.simulator.network.hardware.nodes.host.computer import Computer +from primaite.simulator.network.hardware.nodes.network.router import ACLAction +from primaite.simulator.network.hardware.nodes.network.wireless_router import WirelessRouter +from primaite.simulator.network.transmission.network_layer import IPProtocol +from primaite.simulator.network.transmission.transport_layer import Port + + +@pytest.fixture(scope="function") +def setup_network(): + network = Network() + + # Configure PC A + pc_a = Computer( + hostname="pc_a", + ip_address="192.168.0.2", + subnet_mask="255.255.255.0", + default_gateway="192.168.0.1", + start_up_duration=0, + ) + pc_a.power_on() + network.add_node(pc_a) + + # Configure Router 1 + router_1 = WirelessRouter(hostname="router_1", start_up_duration=0) + router_1.power_on() + network.add_node(router_1) + + # Configure the connection between PC A and Router 1 port 2 + router_1.configure_router_interface("192.168.0.1", "255.255.255.0") + network.connect(pc_a.network_interface[1], router_1.network_interface[2]) + + # Configure Router 1 ACLs + router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.ARP, dst_port=Port.ARP, position=22) + router_1.acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol.ICMP, position=23) + + # Configure PC B + pc_b = Computer( + hostname="pc_b", + ip_address="192.168.2.2", + subnet_mask="255.255.255.0", + default_gateway="192.168.2.1", + start_up_duration=0, + ) + pc_b.power_on() + network.add_node(pc_b) + + # Configure Router 2 + router_2 = WirelessRouter(hostname="router_2", start_up_duration=0) + router_2.power_on() + network.add_node(router_2) + + # Configure the connection between PC B and Router 2 port 2 + router_2.configure_router_interface("192.168.2.1", "255.255.255.0") + network.connect(pc_b.network_interface[1], router_2.network_interface[2]) + + # Configure Router 2 ACLs + + # Configure the wireless connection between Router 1 port 1 and Router 2 port 1 + router_1.configure_wireless_access_point("192.168.1.1", "255.255.255.0") + router_2.configure_wireless_access_point("192.168.1.2", "255.255.255.0") + + AIR_SPACE.show() + + router_1.route_table.add_route( + address="192.168.2.0", subnet_mask="255.255.255.0", next_hop_ip_address="192.168.1.2" + ) + + # Configure Route from Router 2 to PC A subnet + router_2.route_table.add_route( + address="192.168.0.2", subnet_mask="255.255.255.0", next_hop_ip_address="192.168.1.1" + ) + + # Configure PC C + pc_c = Computer( + hostname="pc_c", + ip_address="192.168.3.2", + subnet_mask="255.255.255.0", + default_gateway="192.168.3.1", + start_up_duration=0, + ) + pc_c.power_on() + network.add_node(pc_c) + + # Configure Router 3 + router_3 = WirelessRouter(hostname="router_3", start_up_duration=0) + router_3.power_on() + network.add_node(router_3) + + # Configure the connection between PC C and Router 3 port 2 + router_3.configure_router_interface("192.168.3.1", "255.255.255.0") + network.connect(pc_c.network_interface[1], router_3.network_interface[2]) + + # Configure the wireless connection between Router 2 port 1 and Router 3 port 1 + router_3.configure_wireless_access_point("192.168.1.3", "255.255.255.0") + + # Configure Route from Router 1 to PC C subnet + router_1.route_table.add_route( + address="192.168.3.0", subnet_mask="255.255.255.0", next_hop_ip_address="192.168.1.3" + ) + + # Configure Route from Router 2 to PC C subnet + router_2.route_table.add_route( + address="192.168.3.0", subnet_mask="255.255.255.0", next_hop_ip_address="192.168.1.3" + ) + + # Configure Route from Router 3 to PC A and PC B subnets + router_3.route_table.add_route( + address="192.168.0.0", subnet_mask="255.255.255.0", next_hop_ip_address="192.168.1.1" + ) + router_3.route_table.add_route( + address="192.168.2.0", subnet_mask="255.255.255.0", next_hop_ip_address="192.168.1.2" + ) + + return pc_a, pc_b, pc_c, router_1, router_2, router_3 + + +def test_ping_default_gateways(setup_network): + pc_a, pc_b, pc_c, router_1, router_2, router_3 = setup_network + # Check if each PC can ping its default gateway + assert pc_a.ping(pc_a.default_gateway), "PC A should ping its default gateway successfully." + assert pc_b.ping(pc_b.default_gateway), "PC B should ping its default gateway successfully." + assert pc_c.ping(pc_c.default_gateway), "PC C should ping its default gateway successfully." + + +def test_cross_router_connectivity_pre_frequency_change(setup_network): + pc_a, pc_b, pc_c, router_1, router_2, router_3 = setup_network + # Ensure that PCs can ping across routers before any frequency change + assert pc_a.ping(pc_b.network_interface[1].ip_address), "PC A should ping PC B across routers successfully." + assert pc_a.ping(pc_c.network_interface[1].ip_address), "PC A should ping PC C across routers successfully." + assert pc_b.ping(pc_c.network_interface[1].ip_address), "PC B should ping PC C across routers successfully."