diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b495c09..2f2918aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,12 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] + + ### Added -- Network Hardware - Added base hardware module with NIC, SwitchPort, Node, Switch, and Link. Nodes and Switches have +- Network Hardware - Added base hardware module with NIC, SwitchPort, Node, and Link. Nodes have fundamental services like ARP, ICMP, and PCAP running them by default. - Network Transmission - Modelled OSI Model layers 1 through to 5 with various classes for creating network frames and transmitting them from a Service/Application, down through the layers, over the wire, and back up through the layers to a Service/Application another machine. +- Introduced `Router` and `Switch` classes to manage networking routes more effectively. + - Added `ACLRule` and `RouteTableEntry` classes as part of the `Router`. +- New `.show()` methods in all network component classes to inspect the state in either plain text or markdown formats. +- Added `Computer` and `Server` class to better differentiate types of network nodes. +- Integrated a new Use Case 2 network into the system. +- New unit tests to verify routing between different subnets using `.ping()`. - system - Added the core structure of Application, Services, and Components. Also added a SoftwareManager and SessionManager. - Permission System - each action can define criteria that will be used to permit or deny agent actions. diff --git a/MANIFEST.in b/MANIFEST.in index 51ae4ddf..2ac7b306 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,3 @@ include src/primaite/setup/_package_data/primaite_config.yaml include src/primaite/config/_package_data/*.yaml +include src/primaite/simulator/_package_data/*.ipynb diff --git a/docs/source/simulation_components/network/network.rst b/docs/source/simulation_components/network/network.rst index e5614980..f4d64b16 100644 --- a/docs/source/simulation_components/network/network.rst +++ b/docs/source/simulation_components/network/network.rst @@ -30,10 +30,11 @@ we'll use the following Network that has a client, server, two switches, and a r .. code-block:: python from primaite.simulator.network.container import Network - from primaite.simulator.network.hardware.base import Switch, NIC + from primaite.simulator.network.hardware.base import NIC from primaite.simulator.network.hardware.nodes.computer import Computer from primaite.simulator.network.hardware.nodes.router import Router, ACLAction from primaite.simulator.network.hardware.nodes.server import Server + from primaite.simulator.network.hardware.nodes.switch import Switch from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port diff --git a/docs/source/simulation_components/network/switch.rst b/docs/source/simulation_components/network/switch.rst deleted file mode 100644 index 4b3b24bc..00000000 --- a/docs/source/simulation_components/network/switch.rst +++ /dev/null @@ -1,8 +0,0 @@ -.. only:: comment - - © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK - -.. _about: - -Switch -====== \ No newline at end of file diff --git a/src/primaite/notebooks/__init__.py b/src/primaite/notebooks/__init__.py deleted file mode 100644 index bc1dcfcd..00000000 --- a/src/primaite/notebooks/__init__.py +++ /dev/null @@ -1,34 +0,0 @@ -# © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK -"""Contains default jupyter notebooks which demonstrate PrimAITE functionality.""" - -import importlib.util -import os -import subprocess -import sys -from logging import Logger - -from primaite import getLogger, PRIMAITE_PATHS - -_LOGGER: Logger = getLogger(__name__) - - -def start_jupyter_session() -> None: - """ - Starts a new Jupyter notebook session in the app notebooks directory. - - Currently only works on Windows OS. - - .. todo:: Figure out how to get this working for Linux and MacOS too. - """ - if importlib.util.find_spec("jupyter") is not None: - jupyter_cmd = "python3 -m jupyter lab" - if sys.platform == "win32": - jupyter_cmd = "jupyter lab" - - working_dir = os.getcwd() - os.chdir(PRIMAITE_PATHS.user_notebooks_path) - subprocess.Popen(jupyter_cmd) - os.chdir(working_dir) - else: - # Jupyter is not installed - _LOGGER.error("Cannot start jupyter lab as it is not installed") diff --git a/src/primaite/setup/reset_demo_notebooks.py b/src/primaite/setup/reset_demo_notebooks.py index 1f96c90f..a4ee4c4d 100644 --- a/src/primaite/setup/reset_demo_notebooks.py +++ b/src/primaite/setup/reset_demo_notebooks.py @@ -1,35 +1,46 @@ # © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK import filecmp -import os import shutil from logging import Logger from pathlib import Path -import pkg_resources - from primaite import getLogger, PRIMAITE_PATHS _LOGGER: Logger = getLogger(__name__) +def should_copy_file(src: Path, dest: Path, overwrite_existing: bool) -> bool: + """ + Determine if the file should be copied. + + :param src: The source file Path. + :param dest: The destination file Path. + :param overwrite_existing: A bool to toggle replacing existing edited files on or off. + :return: True if file should be copied, otherwise False. + """ + if not dest.is_file(): + return True + + if overwrite_existing and not filecmp.cmp(src, dest): + return True + + return False + + def run(overwrite_existing: bool = True) -> None: """ - Resets the demo jupyter notebooks in the users app notebooks directory. + Resets the demo Jupyter notebooks in the user's app notebooks directory. :param overwrite_existing: A bool to toggle replacing existing edited notebooks on or off. """ - notebooks_package_data_root = pkg_resources.resource_filename("primaite", "notebooks/_package_data") - for subdir, dirs, files in os.walk(notebooks_package_data_root): - for file in files: - fp = os.path.join(subdir, file) - path_split = os.path.relpath(fp, notebooks_package_data_root).split(os.sep) - target_fp = PRIMAITE_PATHS.user_notebooks_path / Path(*path_split) - target_fp.parent.mkdir(exist_ok=True, parents=True) - copy_file = not target_fp.is_file() + primaite_root = Path(__file__).parent.parent + example_notebooks_user_dir = PRIMAITE_PATHS.user_notebooks_path / "example_notebooks" + example_notebooks_user_dir.mkdir(exist_ok=True, parents=True) - if overwrite_existing and not copy_file: - copy_file = (not filecmp.cmp(fp, target_fp)) and (".ipynb_checkpoints" not in str(target_fp)) + for src_fp in primaite_root.glob("**/*.ipynb"): + dst_fp = example_notebooks_user_dir / src_fp.name - if copy_file: - shutil.copy2(fp, target_fp) - _LOGGER.info(f"Reset example notebook: {target_fp}") + if should_copy_file(src_fp, dst_fp, overwrite_existing): + print(dst_fp) + shutil.copy2(src_fp, dst_fp) + _LOGGER.info(f"Reset example notebook: {dst_fp}") diff --git a/src/primaite/notebooks/create-simulation.ipynb b/src/primaite/simulator/_package_data/create-simulation_demo.ipynb similarity index 100% rename from src/primaite/notebooks/create-simulation.ipynb rename to src/primaite/simulator/_package_data/create-simulation_demo.ipynb diff --git a/src/primaite/simulator/_package_data/network_simulator_demo.ipynb b/src/primaite/simulator/_package_data/network_simulator_demo.ipynb new file mode 100644 index 00000000..252f31fa --- /dev/null +++ b/src/primaite/simulator/_package_data/network_simulator_demo.ipynb @@ -0,0 +1,688 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "03b2013a-b7d1-47ee-b08c-8dab83833720", + "metadata": {}, + "source": [ + "# PrimAITE Router Simulation Demo\n", + "\n", + "This demo uses the ARCD Use Case 2 Network (seen below) to demonstrate the capabilities of the Network simulator in PrimAITE." + ] + }, + { + "cell_type": "raw", + "id": "c8bb5698-e746-4e90-9c2f-efe962acdfa0", + "metadata": {}, + "source": [ + " +------------+\n", + " | domain_ |\n", + " +------------+ controller |\n", + " | | |\n", + " | +------------+\n", + " |\n", + " |\n", + "+------------+ | +------------+\n", + "| | | | |\n", + "| client_1 +---------+ | +---------+ web_server |\n", + "| | | | | | |\n", + "+------------+ | | | +------------+\n", + " +--+---------+ +------------+ +------+--+--+\n", + " | | | | | |\n", + " | switch_2 +------+ router_1 +------+ switch_1 |\n", + " | | | | | |\n", + " +--+------+--+ +------------+ +--+---+--+--+\n", + "+------------+ | | | | | +------------+\n", + "| | | | | | | | database |\n", + "| client_2 +---------+ | | | +---------+ _server |\n", + "| | | | | | |\n", + "+------------+ | | | +------------+\n", + " | +------------+ | |\n", + " | | security | | |\n", + " +---------+ _suite +---------+ | +------------+\n", + " | | | | backup_ |\n", + " +------------+ +------------+ server |\n", + " | |\n", + " +------------+" + ] + }, + { + "cell_type": "markdown", + "id": "415d487c-6457-497d-85d6-99439b3541e7", + "metadata": {}, + "source": [ + "## The Network\n", + "First let's create our network. The network comes 'pre-packaged' with PrimAITE in the `primaite.simulator.network.networks` module.\n", + "\n", + "> ℹ️ You'll see a bunch of logs associated with parts of the Network that aern't an 'electronic' device on the Network and thus don't have a stsrem to log to. Soon these logs are going to be pushed to a Network Logger so we're not clogging up the PrimAITE application logs." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "de57ac8c-5b28-4847-a759-2ceaf5593329", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "from primaite.simulator.network.networks import arcd_uc2_network" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a1e2e4df-67c0-4584-ab27-47e2c7c7fcd2", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "network = arcd_uc2_network()" + ] + }, + { + "cell_type": "markdown", + "id": "fb052c56-e9ca-4093-9115-d0c440b5ff53", + "metadata": {}, + "source": [ + "Most of the Network components have a `.show()` function that prints a table of information about that object. We can view the Nodes and Links on the Network by calling `network.show()`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cc199741-ef2e-47f5-b2f0-e20049ccf40f", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "network.show()" + ] + }, + { + "cell_type": "markdown", + "id": "76d2b7e9-280b-4741-a8b3-a84bed219fac", + "metadata": { + "tags": [] + }, + "source": [ + "## Nodes\n", + "\n", + "Now let's inspect some of the nodes. We can directly access a node on the Network by calling .`get_node_by_hostname`. Like Network, a Node, along with some core services like ARP, have a `.show()` method." + ] + }, + { + "cell_type": "markdown", + "id": "84113002-843e-4cab-b899-667b50f25f6b", + "metadata": {}, + "source": [ + "### Router Nodes\n", + "\n", + "First we'll inspect the Router node and some of it's core services." + ] + }, + { + "cell_type": "markdown", + "id": "bf63a178-eee5-4669-bf64-13aea7ecf6cb", + "metadata": {}, + "source": [ + "Calling `router.show()` displays the Ethernet interfaces on the Router. If you need a table in markdown format, pass `markdown=True`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e76d1854-961e-438c-b40f-77fd9c3abe38", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "network.get_node_by_hostname(\"router_1\").show()" + ] + }, + { + "cell_type": "markdown", + "id": "e000540c-687c-4254-870c-1d814603bdbf", + "metadata": {}, + "source": [ + "Calling `router.arp.show()` displays the Router ARP Cache." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "92de8b42-92d7-4934-9c12-50bf724c9eb2", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "network.get_node_by_hostname(\"router_1\").arp.show()" + ] + }, + { + "cell_type": "markdown", + "id": "a9ff7ee8-9482-44de-9039-b684866bdc82", + "metadata": {}, + "source": [ + "Calling `router.acl.show()` displays the Access Control List." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5922282a-d22b-4e55-9176-f3f3654c849f", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "network.get_node_by_hostname(\"router_1\").acl.show()" + ] + }, + { + "cell_type": "markdown", + "id": "71c87884-f793-4c9f-b004-5b0df86cf585", + "metadata": {}, + "source": [ + "Calling `router.router_table.show()` displays the static routes the Router provides." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "327203be-f475-4727-82a1-e992d3b70ed8", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "network.get_node_by_hostname(\"router_1\").route_table.show()" + ] + }, + { + "cell_type": "markdown", + "id": "eef561a8-3d39-4c8b-bbc8-e8b10b8ed25f", + "metadata": {}, + "source": [ + "Calling `router.sys_log.show()` displays the Router system log. By default, only the last 10 log entries are displayed, this can be changed by passing `last_n=`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3d0aa004-b10c-445f-aaab-340e0e716c74", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "network.get_node_by_hostname(\"router_1\").sys_log.show(last_n=10)" + ] + }, + { + "cell_type": "markdown", + "id": "25630c90-c54e-4b5d-8bf4-ad1b0722e126", + "metadata": {}, + "source": [ + "### Switch Nodes\n", + "\n", + "Next we'll inspect the Switch node and some of it's core services." + ] + }, + { + "cell_type": "markdown", + "id": "4879394d-2981-40de-a229-e19b09a34e6e", + "metadata": {}, + "source": [ + "Calling `switch.show()` displays the Switch orts on the Switch." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e7fd439b-5442-4e9d-9e7d-86dacb77f458", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "network.get_node_by_hostname(\"switch_1\").show()" + ] + }, + { + "cell_type": "markdown", + "id": "beb8dbd6-7250-4ac9-9fa2-d2a9c0e5fd19", + "metadata": { + "tags": [] + }, + "source": [ + "Calling `switch.arp.show()` displays the Switch ARP Cache." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d06e1310-4a77-4315-a59f-cb1b49ca2352", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "network.get_node_by_hostname(\"switch_1\").arp.show()" + ] + }, + { + "cell_type": "markdown", + "id": "fda75ac3-8123-4234-8f36-86547891d8df", + "metadata": {}, + "source": [ + "Calling `switch.sys_log.show()` displays the Switch system log. By default, only the last 10 log entries are displayed, this can be changed by passing `last_n=`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a0d984b7-a7c1-4bbd-aa5a-9d3caecb08dc", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "network.get_node_by_hostname(\"switch_1\").sys_log.show()" + ] + }, + { + "cell_type": "markdown", + "id": "2f1d99ad-db4f-4baf-8a35-e1d95f269586", + "metadata": {}, + "source": [ + "### Computer/Server Nodes\n", + "\n", + "Finally, we'll inspect a Computer or Server Node and some of its core services." + ] + }, + { + "cell_type": "markdown", + "id": "c9e2251a-1b47-46e5-840f-7fec3e39c5aa", + "metadata": { + "tags": [] + }, + "source": [ + "Calling `computer.show()` displays the NICs on the Computer/Server." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "656c37f6-b145-42af-9714-8d2886d0eff8", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "network.get_node_by_hostname(\"security_suite\").show()" + ] + }, + { + "cell_type": "markdown", + "id": "f1097a49-a3da-4d79-a06d-ae8af452918f", + "metadata": {}, + "source": [ + "Calling `computer.arp.show()` displays the Computer/Server ARP Cache." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "66b267d6-2308-486a-b9aa-cb8d3bcf0753", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "network.get_node_by_hostname(\"security_suite\").arp.show()" + ] + }, + { + "cell_type": "markdown", + "id": "0d1fcad8-5b1a-4d8b-a49f-aa54a95fcaf0", + "metadata": {}, + "source": [ + "Calling `switch.sys_log.show()` displays the Computer/Server system log. By default, only the last 10 log entries are displayed, this can be changed by passing `last_n=`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1b5debe8-ef1b-445d-8fa9-6a45568f21f3", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "network.get_node_by_hostname(\"security_suite\").sys_log.show()" + ] + }, + { + "cell_type": "markdown", + "id": "fcfa1773-798c-4ada-9318-c3ad928217da", + "metadata": {}, + "source": [ + "## Basic Network Comms Check\n", + "\n", + "We can perform a good old ping to check that Nodes are able to communicate with each other." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "495b7de4-b6ce-41a6-9114-f74752ab4491", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "network.show(nodes=False, links=False)" + ] + }, + { + "cell_type": "markdown", + "id": "3e13922a-217f-4f4e-99b6-57a07613cade", + "metadata": {}, + "source": [ + "We'll first ping client_1's default gateway." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a38abb71-994e-49e8-8f51-e9a550e95b99", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "network.get_node_by_hostname(\"client_1\").ping(\"192.168.10.1\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8388e1e9-30e3-4534-8e5a-c6e9144149d2", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "network.get_node_by_hostname(\"client_1\").sys_log.show(15)" + ] + }, + { + "cell_type": "markdown", + "id": "02c76d5c-d954-49db-912d-cb9c52f46375", + "metadata": {}, + "source": [ + "Next, we'll ping the interface of the 192.168.1.0/24 Network on the Router (port 1)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ff8e976a-c16b-470c-8923-325713a30d6c", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "network.get_node_by_hostname(\"client_1\").ping(\"192.168.1.1\")" + ] + }, + { + "cell_type": "markdown", + "id": "80280404-a5ab-452f-8a02-771a0d7496b1", + "metadata": {}, + "source": [ + "And finally, we'll ping the web server." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c4163f8d-6a72-410c-9f5c-4f881b7de45e", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "network.get_node_by_hostname(\"client_1\").ping(\"192.168.1.12\")" + ] + }, + { + "cell_type": "markdown", + "id": "1194c045-ba77-4427-be30-ed7b5b224850", + "metadata": {}, + "source": [ + "To confirm that the ping was received and processed by the web_server, we can view the sys log" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e79a523a-5780-45b6-8798-c434e0e522bd", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "network.get_node_by_hostname(\"web_server\").sys_log.show()" + ] + }, + { + "cell_type": "markdown", + "id": "5928f6dd-1006-45e3-99f3-8f311a875faa", + "metadata": {}, + "source": [ + "## Advanced Network Usage\n", + "\n", + "We can now use the Network to perform some more advaced things." + ] + }, + { + "cell_type": "markdown", + "id": "5e023ef3-7d18-4006-96ee-042a06a481fc", + "metadata": {}, + "source": [ + "Let's attempt to prevent client_2 from being able to ping the web server. First, we'll confirm that it can ping the server first..." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "603cf913-e261-49da-a7dd-85e1bb6dec56", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "network.get_node_by_hostname(\"client_2\").ping(\"192.168.1.12\")" + ] + }, + { + "cell_type": "markdown", + "id": "5cf962a4-20e6-44ae-9748-7fc5267ae111", + "metadata": {}, + "source": [ + "If we look at the client_2 sys log we can see that the four ICMP echo requests were sent and four ICMP each replies were received:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e047de00-3de4-4823-b26a-2c8d64c7a663", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "network.get_node_by_hostname(\"client_2\").sys_log.show()" + ] + }, + { + "cell_type": "markdown", + "id": "bdc4741d-6e3e-4aec-a69c-c2e9653bd02c", + "metadata": {}, + "source": [ + "Now we'll add an ACL to block ICMP from 192.168.10.22" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6db355ae-b99a-441b-a2c4-4ffe78f46bff", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "from primaite.simulator.network.transmission.network_layer import IPProtocol\n", + "from primaite.simulator.network.transmission.transport_layer import Port\n", + "from primaite.simulator.network.hardware.nodes.router import ACLAction\n", + "network.get_node_by_hostname(\"router_1\").acl.add_rule(\n", + " action=ACLAction.DENY,\n", + " protocol=IPProtocol.ICMP,\n", + " src_ip=\"192.168.10.22\",\n", + " position=1\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a345e000-8842-4827-af96-adc0fbe390fb", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "network.get_node_by_hostname(\"router_1\").acl.show()" + ] + }, + { + "cell_type": "markdown", + "id": "3a5bfd9f-04cb-493e-a86c-cd268563a262", + "metadata": {}, + "source": [ + "Now we attempt (and fail) to ping the web server" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a4f4ff31-590f-40fb-b13d-efaa8c2720b6", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "network.get_node_by_hostname(\"client_2\").ping(\"192.168.1.12\")" + ] + }, + { + "cell_type": "markdown", + "id": "83e56497-097b-45cb-964e-b15c72547b38", + "metadata": {}, + "source": [ + "We can check that the ping was actually sent by client_2 by viewing the sys log" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f62b8a4e-fd3b-4059-b108-3d4a0b18f2a0", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "network.get_node_by_hostname(\"client_2\").sys_log.show()" + ] + }, + { + "cell_type": "markdown", + "id": "c7040311-a879-4620-86a0-55d0774156e5", + "metadata": {}, + "source": [ + "We can check the router sys log to see why the traffic was blocked" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7e53d776-99da-4d2c-a2a7-bd7ce27bff4c", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "network.get_node_by_hostname(\"router_1\").sys_log.show()" + ] + }, + { + "cell_type": "markdown", + "id": "aba0bc7d-da57-477b-b34a-3688b5aab2c6", + "metadata": {}, + "source": [ + "Now a final check to ensure that client_1 can still ping the web_server." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d542734b-7582-4af7-8254-bda3de50d091", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "network.get_node_by_hostname(\"client_1\").ping(\"192.168.1.12\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d78e9fe3-02c6-4792-944f-5622e26e0412", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "network.get_node_by_hostname(\"client_1\").sys_log.show()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/src/primaite/simulator/network/container.py b/src/primaite/simulator/network/container.py index ccb9ce77..239c98a7 100644 --- a/src/primaite/simulator/network/container.py +++ b/src/primaite/simulator/network/container.py @@ -1,16 +1,17 @@ -from typing import Any, Dict, Union, Optional, List +from typing import Any, Dict, List, Optional, Union import matplotlib.pyplot as plt import networkx as nx from networkx import MultiGraph -from prettytable import PrettyTable, MARKDOWN +from prettytable import MARKDOWN, PrettyTable from primaite import getLogger from primaite.simulator.core import Action, ActionManager, AllowAllValidator, SimComponent -from primaite.simulator.network.hardware.base import Link, NIC, Node, SwitchPort, Switch +from primaite.simulator.network.hardware.base import Link, NIC, Node, SwitchPort from primaite.simulator.network.hardware.nodes.computer import Computer from primaite.simulator.network.hardware.nodes.router import Router from primaite.simulator.network.hardware.nodes.server import Server +from primaite.simulator.network.hardware.nodes.switch import Switch _LOGGER = getLogger(__name__) @@ -30,7 +31,7 @@ class Network(SimComponent): links: Dict[str, Link] = {} def __init__(self, **kwargs): - """" + """ Initialise the network. Constructs the network and sets up its initial state including @@ -84,14 +85,14 @@ class Network(SimComponent): "Router": self.routers, "Switch": self.switches, "Server": self.servers, - "Computer": self.computers + "Computer": self.computers, } if nodes: table = PrettyTable(["Node", "Type", "Operating State"]) if markdown: table.set_style(MARKDOWN) table.align = "l" - table.title = f"Nodes" + table.title = "Nodes" for node_type, nodes in nodes_type_map.items(): for node in nodes: table.add_row([node.hostname, node_type, node.operating_state.name]) @@ -102,7 +103,7 @@ class Network(SimComponent): if markdown: table.set_style(MARKDOWN) table.align = "l" - table.title = f"IP Addresses" + table.title = "IP Addresses" for nodes in nodes_type_map.values(): for node in nodes: for i, port in node.ethernet_port.items(): @@ -114,7 +115,7 @@ class Network(SimComponent): if markdown: table.set_style(MARKDOWN) table.align = "l" - table.title = f"Links" + table.title = "Links" links = list(self.links.values()) for nodes in nodes_type_map.values(): for node in nodes: @@ -126,7 +127,7 @@ class Network(SimComponent): link.endpoint_b.parent.hostname, link.is_up, link.bandwidth, - link.current_load_percent + link.current_load_percent, ] ) links.remove(link) @@ -207,9 +208,7 @@ class Network(SimComponent): node.parent = None _LOGGER.info(f"Removed node {node.uuid} from network {self.uuid}") - def connect( - self, endpoint_a: Union[NIC, SwitchPort], endpoint_b: Union[NIC, SwitchPort], **kwargs - ) -> None: + def connect(self, endpoint_a: Union[NIC, SwitchPort], endpoint_b: Union[NIC, SwitchPort], **kwargs) -> None: """ Connect two endpoints on the network by creating a link between their NICs/SwitchPorts. diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 674020ee..1193f3ef 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -1,13 +1,12 @@ from __future__ import annotations -import random import re import secrets from enum import Enum from ipaddress import IPv4Address, IPv4Network -from typing import Dict, List, Optional, Tuple, Union +from typing import Any, Dict, List, Optional, Tuple, Union -from prettytable import PrettyTable, MARKDOWN +from prettytable import MARKDOWN, PrettyTable from primaite import getLogger from primaite.exceptions import NetworkError @@ -289,7 +288,7 @@ class SwitchPort(SimComponent): "The speed of the SwitchPort in Mbps. Default is 100 Mbps." mtu: int = 1500 "The Maximum Transmission Unit (MTU) of the SwitchPort in Bytes. Default is 1500 B" - connected_node: Optional[Switch] = None + connected_node: Optional[Node] = None "The Node to which the SwitchPort is connected." connected_link: Optional[Link] = None "The Link to which the SwitchPort is connected." @@ -715,7 +714,7 @@ class ARPCache: arp_packet = arp_packet.generate_reply(from_nic.mac_address) self.send_arp_reply(arp_packet, from_nic) - def __contains__(self, item) -> bool: + def __contains__(self, item: Any) -> bool: return item in self.arp @@ -765,7 +764,7 @@ class ICMP: identifier=frame.icmp.identifier, sequence=frame.icmp.sequence + 1, ) - payload = secrets.token_urlsafe(int(32/1.3)) # Standard ICMP 32 bytes size + payload = secrets.token_urlsafe(int(32 / 1.3)) # Standard ICMP 32 bytes size frame = Frame( ethernet=ethernet_header, ip=ip_packet, tcp=tcp_header, icmp=icmp_reply_packet, payload=payload ) @@ -829,7 +828,7 @@ class ICMP: # Data Link Layer ethernet_header = EthernetHeader(src_mac_addr=src_nic.mac_address, dst_mac_addr=target_mac_address) icmp_packet = ICMPPacket(identifier=identifier, sequence=sequence) - payload = secrets.token_urlsafe(int(32/1.3)) # Standard ICMP 32 bytes size + payload = secrets.token_urlsafe(int(32 / 1.3)) # Standard ICMP 32 bytes size frame = Frame(ethernet=ethernet_header, ip=ip_packet, tcp=tcp_header, icmp=icmp_packet, payload=payload) nic.send_frame(frame) return sequence, icmp_packet.identifier @@ -1049,7 +1048,8 @@ class Node(SimComponent): f"Ping statistics for {target_ip_address}: " f"Packets: Sent = {pings}, " f"Received = {request_replies}, " - f"Lost = {pings-request_replies} ({(pings-request_replies)/pings*100}% loss)") + f"Lost = {pings-request_replies} ({(pings-request_replies)/pings*100}% loss)" + ) return passed return False @@ -1084,102 +1084,3 @@ class Node(SimComponent): pass elif frame.ip.protocol == IPProtocol.ICMP: self.icmp.process_icmp(frame=frame, from_nic=from_nic) - - -class Switch(Node): - """A class representing a Layer 2 network switch.""" - - num_ports: int = 24 - "The number of ports on the switch." - switch_ports: Dict[int, SwitchPort] = {} - "The SwitchPorts on the switch." - mac_address_table: Dict[str, SwitchPort] = {} - "A MAC address table mapping destination MAC addresses to corresponding SwitchPorts." - - def __init__(self, **kwargs): - super().__init__(**kwargs) - if not self.switch_ports: - self.switch_ports = {i: SwitchPort() for i in range(1, self.num_ports + 1)} - for port_num, port in self.switch_ports.items(): - port.connected_node = self - port.parent = self - port.port_num = port_num - - def show(self, markdown: bool = False): - """Prints a table of the SwitchPorts on the Switch.""" - table = PrettyTable(["Port", "MAC Address", "Speed", "Status"]) - if markdown: - table.set_style(MARKDOWN) - table.align = "l" - table.title = f"{self.hostname} Switch Ports" - for port_num, port in self.switch_ports.items(): - table.add_row([port_num, port.mac_address, port.speed, "Enabled" if port.enabled else "Disabled"]) - print(table) - - def describe_state(self) -> Dict: - """ - Produce a dictionary describing the current state of this object. - - Please see :py:meth:`primaite.simulator.core.SimComponent.describe_state` for a more detailed explanation. - - :return: Current state of this object and child objects. - :rtype: Dict - """ - return { - "uuid": self.uuid, - "num_ports": self.num_ports, # redundant? - "ports": {port_num: port.describe_state() for port_num, port in self.switch_ports.items()}, - "mac_address_table": {mac: port for mac, port in self.mac_address_table.items()}, - } - - def _add_mac_table_entry(self, mac_address: str, switch_port: SwitchPort): - mac_table_port = self.mac_address_table.get(mac_address) - if not mac_table_port: - self.mac_address_table[mac_address] = switch_port - self.sys_log.info(f"Added MAC table entry: Port {switch_port.port_num} -> {mac_address}") - else: - if mac_table_port != switch_port: - self.mac_address_table.pop(mac_address) - self.sys_log.info(f"Removed MAC table entry: Port {mac_table_port.port_num} -> {mac_address}") - self._add_mac_table_entry(mac_address, switch_port) - - def forward_frame(self, frame: Frame, incoming_port: SwitchPort): - """ - Forward a frame to the appropriate port based on the destination MAC address. - - :param frame: The Frame to be forwarded. - :param incoming_port: The port number from which the frame was received. - """ - src_mac = frame.ethernet.src_mac_addr - dst_mac = frame.ethernet.dst_mac_addr - self._add_mac_table_entry(src_mac, incoming_port) - - outgoing_port = self.mac_address_table.get(dst_mac) - if outgoing_port or dst_mac != "ff:ff:ff:ff:ff:ff": - outgoing_port.send_frame(frame) - else: - # If the destination MAC is not in the table, flood to all ports except incoming - for port in self.switch_ports.values(): - if port != incoming_port: - port.send_frame(frame) - - def disconnect_link_from_port(self, link: Link, port_number: int): - """ - Disconnect a given link from the specified port number on the switch. - - :param link: The Link object to be disconnected. - :param port_number: The port number on the switch from where the link should be disconnected. - :raise NetworkError: When an invalid port number is provided or the link does not match the connection. - """ - port = self.switch_ports.get(port_number) - if port is None: - msg = f"Invalid port number {port_number} on the switch" - _LOGGER.error(msg) - raise NetworkError(msg) - - if port.connected_link != link: - msg = f"The link does not match the connection at port number {port_number}" - _LOGGER.error(msg) - raise NetworkError(msg) - - port.disconnect_link() diff --git a/src/primaite/simulator/network/hardware/nodes/computer.py b/src/primaite/simulator/network/hardware/nodes/computer.py index 110ad385..2a2e8524 100644 --- a/src/primaite/simulator/network/hardware/nodes/computer.py +++ b/src/primaite/simulator/network/hardware/nodes/computer.py @@ -1,6 +1,6 @@ from ipaddress import IPv4Address -from primaite.simulator.network.hardware.base import Node, NIC +from primaite.simulator.network.hardware.base import NIC, Node class Computer(Node): diff --git a/src/primaite/simulator/network/hardware/nodes/router.py b/src/primaite/simulator/network/hardware/nodes/router.py index b507143b..26ba01ae 100644 --- a/src/primaite/simulator/network/hardware/nodes/router.py +++ b/src/primaite/simulator/network/hardware/nodes/router.py @@ -5,7 +5,7 @@ from enum import Enum from ipaddress import IPv4Address, IPv4Network from typing import Dict, List, Optional, Tuple, Union -from prettytable import PrettyTable, MARKDOWN +from prettytable import MARKDOWN, PrettyTable from primaite.simulator.core import SimComponent from primaite.simulator.network.hardware.base import ARPCache, ICMP, NIC, Node @@ -71,6 +71,7 @@ class AccessControlList(SimComponent): :ivar int max_acl_rules: Maximum number of ACL rules that can be added. Default is 25. :ivar List[Optional[ACLRule]] _acl: A list containing the ACL rules. """ + sys_log: SysLog implicit_action: ACLAction implicit_rule: ACLRule @@ -105,14 +106,14 @@ class AccessControlList(SimComponent): return self._acl def add_rule( - self, - action: ACLAction, - protocol: Optional[IPProtocol] = None, - src_ip: Optional[Union[str, IPv4Address]] = None, - src_port: Optional[Port] = None, - dst_ip: Optional[Union[str, IPv4Address]] = None, - dst_port: Optional[Port] = None, - position: int = 0, + self, + action: ACLAction, + protocol: Optional[IPProtocol] = None, + src_ip: Optional[Union[str, IPv4Address]] = None, + src_port: Optional[Port] = None, + dst_ip: Optional[Union[str, IPv4Address]] = None, + dst_port: Optional[Port] = None, + position: int = 0, ) -> None: """ Add a new ACL rule. @@ -150,12 +151,12 @@ class AccessControlList(SimComponent): raise ValueError(f"Position {position} is out of bounds.") def is_permitted( - self, - protocol: IPProtocol, - src_ip: Union[str, IPv4Address], - src_port: Optional[Port], - dst_ip: Union[str, IPv4Address], - dst_port: Optional[Port], + self, + protocol: IPProtocol, + src_ip: Union[str, IPv4Address], + src_port: Optional[Port], + dst_ip: Union[str, IPv4Address], + dst_port: Optional[Port], ) -> Tuple[bool, Optional[Union[str, ACLRule]]]: """ Check if a packet with the given properties is permitted through the ACL. @@ -177,23 +178,23 @@ class AccessControlList(SimComponent): continue if ( - (rule.src_ip == src_ip or rule.src_ip is None) - and (rule.dst_ip == dst_ip or rule.dst_ip is None) - and (rule.protocol == protocol or rule.protocol is None) - and (rule.src_port == src_port or rule.src_port is None) - and (rule.dst_port == dst_port or rule.dst_port is None) + (rule.src_ip == src_ip or rule.src_ip is None) + and (rule.dst_ip == dst_ip or rule.dst_ip is None) + and (rule.protocol == protocol or rule.protocol is None) + and (rule.src_port == src_port or rule.src_port is None) + and (rule.dst_port == dst_port or rule.dst_port is None) ): return rule.action == ACLAction.PERMIT, rule return self.implicit_action == ACLAction.PERMIT, f"Implicit {self.implicit_action.name}" def get_relevant_rules( - self, - protocol: IPProtocol, - src_ip: Union[str, IPv4Address], - src_port: Port, - dst_ip: Union[str, IPv4Address], - dst_port: Port, + self, + protocol: IPProtocol, + src_ip: Union[str, IPv4Address], + src_port: Port, + dst_ip: Union[str, IPv4Address], + dst_port: Port, ) -> List[ACLRule]: """ Get the list of relevant rules for a packet with given properties. @@ -215,11 +216,11 @@ class AccessControlList(SimComponent): continue if ( - (rule.src_ip == src_ip or rule.src_ip is None) - or (rule.dst_ip == dst_ip or rule.dst_ip is None) - or (rule.protocol == protocol or rule.protocol is None) - or (rule.src_port == src_port or rule.src_port is None) - or (rule.dst_port == dst_port or rule.dst_port is None) + (rule.src_ip == src_ip or rule.src_ip is None) + or (rule.dst_ip == dst_ip or rule.dst_ip is None) + or (rule.protocol == protocol or rule.protocol is None) + or (rule.src_port == src_port or rule.src_port is None) + or (rule.dst_port == dst_port or rule.dst_port is None) ): relevant_rules.append(rule) @@ -326,11 +327,11 @@ class RouteTable(SimComponent): pass def add_route( - self, - address: Union[IPv4Address, str], - subnet_mask: Union[IPv4Address, str], - next_hop: Union[IPv4Address, str], - metric: float = 0.0, + self, + address: Union[IPv4Address, str], + subnet_mask: Union[IPv4Address, str], + next_hop: Union[IPv4Address, str], + metric: float = 0.0, ): """ Add a route to the routing table. @@ -397,6 +398,7 @@ class RouterARPCache(ARPCache): :ivar SysLog sys_log: A system log for logging messages. :ivar Router router: The router to which this ARP cache belongs. """ + def __init__(self, sys_log: SysLog, router: Router): super().__init__(sys_log) self.router: Router = router @@ -416,7 +418,8 @@ class RouterARPCache(ARPCache): if arp_packet.target_ip == nic.ip_address: # reply to the Router specifically self.sys_log.info( - f"Received ARP response for {arp_packet.sender_ip} from {arp_packet.sender_mac_addr} via NIC {from_nic}" + f"Received ARP response for {arp_packet.sender_ip} " + f"from {arp_packet.sender_mac_addr} via NIC {from_nic}" ) self.add_arp_cache_entry( ip_address=arp_packet.sender_ip, @@ -462,6 +465,7 @@ class RouterICMP(ICMP): :param router: The router to which this ICMP handler belongs. :type router: Router """ + router: Router def __init__(self, sys_log: SysLog, arp_cache: ARPCache, router: Router): @@ -492,16 +496,22 @@ class RouterICMP(ICMP): # Network Layer ip_packet = IPPacket(src_ip=nic.ip_address, dst_ip=frame.ip.src_ip, protocol=IPProtocol.ICMP) # Data Link Layer - ethernet_header = EthernetHeader(src_mac_addr=src_nic.mac_address, dst_mac_addr=target_mac_address) + ethernet_header = EthernetHeader( + src_mac_addr=src_nic.mac_address, dst_mac_addr=target_mac_address + ) icmp_reply_packet = ICMPPacket( icmp_type=ICMPType.ECHO_REPLY, icmp_code=0, identifier=frame.icmp.identifier, sequence=frame.icmp.sequence + 1, ) - payload = secrets.token_urlsafe(int(32/1.3)) # Standard ICMP 32 bytes size + payload = secrets.token_urlsafe(int(32 / 1.3)) # Standard ICMP 32 bytes size frame = Frame( - ethernet=ethernet_header, ip=ip_packet, tcp=tcp_header, icmp=icmp_reply_packet, payload=payload + ethernet=ethernet_header, + ip=ip_packet, + tcp=tcp_header, + icmp=icmp_reply_packet, + payload=payload, ) self.sys_log.info(f"Sending echo reply to {frame.ip.dst_ip}") @@ -540,6 +550,7 @@ class Router(Node): :ivar int num_ports: The number of ports in the router. :ivar dict kwargs: Optional keyword arguments for SysLog, ACL, RouteTable, RouterARPCache, RouterICMP. """ + num_ports: int ethernet_ports: Dict[int, NIC] = {} acl: AccessControlList @@ -588,12 +599,12 @@ class Router(Node): def route_frame(self, frame: Frame, from_nic: NIC, re_attempt: bool = False) -> None: """ - Route a given frame from a source NIC to its destination. + Route a given frame from a source NIC to its destination. - :param frame: The frame to be routed. - :param from_nic: The source network interface. - :param re_attempt: Flag to indicate if the routing is a reattempt. - """ + :param frame: The frame to be routed. + :param from_nic: The source network interface. + :param re_attempt: Flag to indicate if the routing is a reattempt. + """ # Check if src ip is on network of one of the NICs nic = self.arp.get_arp_cache_nic(frame.ip.dst_ip) target_mac = self.arp.get_arp_cache_mac_address(frame.ip.dst_ip) diff --git a/src/primaite/simulator/network/hardware/nodes/server.py b/src/primaite/simulator/network/hardware/nodes/server.py index a3e6f2d7..b72cc71c 100644 --- a/src/primaite/simulator/network/hardware/nodes/server.py +++ b/src/primaite/simulator/network/hardware/nodes/server.py @@ -1,6 +1,3 @@ -from ipaddress import IPv4Address - -from primaite.simulator.network.hardware.base import Node, NIC from primaite.simulator.network.hardware.nodes.computer import Computer diff --git a/src/primaite/simulator/network/hardware/nodes/switch.py b/src/primaite/simulator/network/hardware/nodes/switch.py new file mode 100644 index 00000000..b7cc1242 --- /dev/null +++ b/src/primaite/simulator/network/hardware/nodes/switch.py @@ -0,0 +1,121 @@ +from typing import Dict + +from prettytable import MARKDOWN, PrettyTable + +from primaite import getLogger +from primaite.exceptions import NetworkError +from primaite.links.link import Link +from primaite.simulator.network.hardware.base import Node, SwitchPort +from primaite.simulator.network.transmission.data_link_layer import Frame + +_LOGGER = getLogger(__name__) + + +class Switch(Node): + """ + A class representing a Layer 2 network switch. + + :ivar num_ports: The number of ports on the switch. Default is 24. + """ + + num_ports: int = 24 + "The number of ports on the switch." + switch_ports: Dict[int, SwitchPort] = {} + "The SwitchPorts on the switch." + mac_address_table: Dict[str, SwitchPort] = {} + "A MAC address table mapping destination MAC addresses to corresponding SwitchPorts." + + def __init__(self, **kwargs): + super().__init__(**kwargs) + if not self.switch_ports: + self.switch_ports = {i: SwitchPort() for i in range(1, self.num_ports + 1)} + for port_num, port in self.switch_ports.items(): + port.connected_node = self + port.parent = self + port.port_num = port_num + + def show(self, markdown: bool = False): + """ + Prints a table of the SwitchPorts on the Switch. + + :param markdown: If True, outputs the table in markdown format. Default is False. + """ + table = PrettyTable(["Port", "MAC Address", "Speed", "Status"]) + if markdown: + table.set_style(MARKDOWN) + table.align = "l" + table.title = f"{self.hostname} Switch Ports" + for port_num, port in self.switch_ports.items(): + table.add_row([port_num, port.mac_address, port.speed, "Enabled" if port.enabled else "Disabled"]) + print(table) + + def describe_state(self) -> Dict: + """ + Produce a dictionary describing the current state of this object. + + :return: Current state of this object and child objects. + """ + return { + "uuid": self.uuid, + "num_ports": self.num_ports, # redundant? + "ports": {port_num: port.describe_state() for port_num, port in self.switch_ports.items()}, + "mac_address_table": {mac: port for mac, port in self.mac_address_table.items()}, + } + + def _add_mac_table_entry(self, mac_address: str, switch_port: SwitchPort): + """ + Private method to add an entry to the MAC address table. + + :param mac_address: MAC address to be added. + :param switch_port: Corresponding SwitchPort object. + """ + mac_table_port = self.mac_address_table.get(mac_address) + if not mac_table_port: + self.mac_address_table[mac_address] = switch_port + self.sys_log.info(f"Added MAC table entry: Port {switch_port.port_num} -> {mac_address}") + else: + if mac_table_port != switch_port: + self.mac_address_table.pop(mac_address) + self.sys_log.info(f"Removed MAC table entry: Port {mac_table_port.port_num} -> {mac_address}") + self._add_mac_table_entry(mac_address, switch_port) + + def forward_frame(self, frame: Frame, incoming_port: SwitchPort): + """ + Forward a frame to the appropriate port based on the destination MAC address. + + :param frame: The Frame to be forwarded. + :param incoming_port: The port number from which the frame was received. + """ + src_mac = frame.ethernet.src_mac_addr + dst_mac = frame.ethernet.dst_mac_addr + self._add_mac_table_entry(src_mac, incoming_port) + + outgoing_port = self.mac_address_table.get(dst_mac) + if outgoing_port or dst_mac != "ff:ff:ff:ff:ff:ff": + outgoing_port.send_frame(frame) + else: + # If the destination MAC is not in the table, flood to all ports except incoming + for port in self.switch_ports.values(): + if port != incoming_port: + port.send_frame(frame) + + def disconnect_link_from_port(self, link: Link, port_number: int): + """ + Disconnect a given link from the specified port number on the switch. + + :param link: The Link object to be disconnected. + :param port_number: The port number on the switch from where the link should be disconnected. + :raise NetworkError: When an invalid port number is provided or the link does not match the connection. + """ + port = self.switch_ports.get(port_number) + if port is None: + msg = f"Invalid port number {port_number} on the switch" + _LOGGER.error(msg) + raise NetworkError(msg) + + if port.connected_link != link: + msg = f"The link does not match the connection at port number {port_number}" + _LOGGER.error(msg) + raise NetworkError(msg) + + port.disconnect_link() diff --git a/src/primaite/simulator/network/networks.py b/src/primaite/simulator/network/networks.py index 28e58ca4..6a50fe3f 100644 --- a/src/primaite/simulator/network/networks.py +++ b/src/primaite/simulator/network/networks.py @@ -1,8 +1,9 @@ from primaite.simulator.network.container import Network -from primaite.simulator.network.hardware.base import Switch, NIC +from primaite.simulator.network.hardware.base import NIC from primaite.simulator.network.hardware.nodes.computer import Computer -from primaite.simulator.network.hardware.nodes.router import Router, ACLAction +from primaite.simulator.network.hardware.nodes.router import ACLAction, Router from primaite.simulator.network.hardware.nodes.server import Server +from primaite.simulator.network.hardware.nodes.switch import Switch from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port @@ -42,36 +43,21 @@ def client_server_routed() -> Network: # Client 1 client_1 = Computer( - hostname="client_1", - ip_address="192.168.2.2", - subnet_mask="255.255.255.0", - default_gateway="192.168.2.1" + hostname="client_1", ip_address="192.168.2.2", subnet_mask="255.255.255.0", default_gateway="192.168.2.1" ) client_1.power_on() network.connect(endpoint_b=client_1.ethernet_port[1], endpoint_a=switch_2.switch_ports[1]) # Server 1 server_1 = Server( - hostname="server_1", - ip_address="192.168.1.2", - subnet_mask="255.255.255.0", - default_gateway="192.168.1.1" + hostname="server_1", ip_address="192.168.1.2", subnet_mask="255.255.255.0", default_gateway="192.168.1.1" ) server_1.power_on() network.connect(endpoint_b=server_1.ethernet_port[1], endpoint_a=switch_1.switch_ports[1]) - 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, src_port=Port.ARP, dst_port=Port.ARP, position=22) - router_1.acl.add_rule( - action=ACLAction.PERMIT, - protocol=IPProtocol.ICMP, - position=23 - ) + router_1.acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol.ICMP, position=23) return network @@ -135,20 +121,14 @@ def arcd_uc2_network() -> Network: # Client 1 client_1 = Computer( - hostname="client_1", - ip_address="192.168.10.21", - subnet_mask="255.255.255.0", - default_gateway="192.168.10.1" + hostname="client_1", ip_address="192.168.10.21", subnet_mask="255.255.255.0", default_gateway="192.168.10.1" ) client_1.power_on() network.connect(endpoint_b=client_1.ethernet_port[1], endpoint_a=switch_2.switch_ports[1]) # Client 2 client_2 = Computer( - hostname="client_2", - ip_address="192.168.10.22", - subnet_mask="255.255.255.0", - default_gateway="192.168.10.1" + hostname="client_2", ip_address="192.168.10.22", subnet_mask="255.255.255.0", default_gateway="192.168.10.1" ) client_2.power_on() network.connect(endpoint_b=client_2.ethernet_port[1], endpoint_a=switch_2.switch_ports[2]) @@ -158,17 +138,14 @@ def arcd_uc2_network() -> Network: hostname="domain_controller", ip_address="192.168.1.10", subnet_mask="255.255.255.0", - default_gateway="192.168.1.1" + default_gateway="192.168.1.1", ) domain_controller.power_on() network.connect(endpoint_b=domain_controller.ethernet_port[1], endpoint_a=switch_1.switch_ports[1]) # Web Server web_server = Server( - hostname="web_server", - ip_address="192.168.1.12", - subnet_mask="255.255.255.0", - default_gateway="192.168.1.1" + hostname="web_server", ip_address="192.168.1.12", subnet_mask="255.255.255.0", default_gateway="192.168.1.1" ) web_server.power_on() network.connect(endpoint_b=web_server.ethernet_port[1], endpoint_a=switch_1.switch_ports[2]) @@ -178,17 +155,14 @@ def arcd_uc2_network() -> Network: hostname="database_server", ip_address="192.168.1.14", subnet_mask="255.255.255.0", - default_gateway="192.168.1.1" + default_gateway="192.168.1.1", ) database_server.power_on() network.connect(endpoint_b=database_server.ethernet_port[1], endpoint_a=switch_1.switch_ports[3]) # Backup Server backup_server = Server( - hostname="backup_server", - ip_address="192.168.1.16", - subnet_mask="255.255.255.0", - default_gateway="192.168.1.1" + hostname="backup_server", ip_address="192.168.1.16", subnet_mask="255.255.255.0", default_gateway="192.168.1.1" ) backup_server.power_on() network.connect(endpoint_b=backup_server.ethernet_port[1], endpoint_a=switch_1.switch_ports[4]) @@ -198,24 +172,15 @@ def arcd_uc2_network() -> Network: hostname="security_suite", ip_address="192.168.1.110", subnet_mask="255.255.255.0", - default_gateway="192.168.1.1" + default_gateway="192.168.1.1", ) security_suite.power_on() network.connect(endpoint_b=security_suite.ethernet_port[1], endpoint_a=switch_1.switch_ports[7]) security_suite.connect_nic(NIC(ip_address="192.168.10.110", subnet_mask="255.255.255.0")) network.connect(endpoint_b=security_suite.ethernet_port[2], endpoint_a=switch_2.switch_ports[7]) - 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, src_port=Port.ARP, dst_port=Port.ARP, position=22) - router_1.acl.add_rule( - action=ACLAction.PERMIT, - protocol=IPProtocol.ICMP, - position=23 - ) + router_1.acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol.ICMP, position=23) return network diff --git a/src/primaite/simulator/system/core/sys_log.py b/src/primaite/simulator/system/core/sys_log.py index 5a7bbbfe..e07c28aa 100644 --- a/src/primaite/simulator/system/core/sys_log.py +++ b/src/primaite/simulator/system/core/sys_log.py @@ -1,7 +1,7 @@ import logging from pathlib import Path -from prettytable import PrettyTable, MARKDOWN +from prettytable import MARKDOWN, PrettyTable from primaite.simulator import TEMP_SIM_OUTPUT @@ -55,6 +55,14 @@ class SysLog: self.logger.addFilter(_NotJSONFilter()) def show(self, last_n: int = 10, markdown: bool = False): + """ + Print the Node Sys Log as a table. + + Generate and print PrettyTable instance that shows the Nodes Sys Log, with columns Timestamp, Level, + and Massage. + + :param markdown: Use Markdown style in table output. Defaults to False. + """ table = PrettyTable(["Timestamp", "Level", "Message"]) if markdown: table.set_style(MARKDOWN) diff --git a/tests/integration_tests/network/test_frame_transmission.py b/tests/integration_tests/network/test_frame_transmission.py index 34b76060..85717b25 100644 --- a/tests/integration_tests/network/test_frame_transmission.py +++ b/tests/integration_tests/network/test_frame_transmission.py @@ -1,4 +1,4 @@ -from primaite.simulator.network.hardware.base import Link, NIC, Node, Switch +from primaite.simulator.network.hardware.base import Link, NIC, Node def test_node_to_node_ping(): @@ -20,7 +20,6 @@ def test_node_to_node_ping(): def test_multi_nic(): """Tests that Nodes with multiple NICs can ping each other and the data go across the correct links.""" - # TODO Add actual checks. Manual check performed for now. node_a = Node(hostname="node_a") nic_a = NIC(ip_address="192.168.0.10", subnet_mask="255.255.255.0") node_a.connect_nic(nic_a) @@ -45,41 +44,3 @@ def test_multi_nic(): node_a.ping("192.168.0.11") assert node_c.ping("10.0.0.12") - - -def test_switched_network(): - """Tests a larges network of Nodes and Switches with one node pinging another.""" - # TODO Add actual checks. Manual check performed for now. - pc_a = Node(hostname="pc_a") - nic_a = NIC(ip_address="192.168.0.10", subnet_mask="255.255.255.0") - pc_a.connect_nic(nic_a) - pc_a.power_on() - - pc_b = Node(hostname="pc_b") - nic_b = NIC(ip_address="192.168.0.11", subnet_mask="255.255.255.0") - pc_b.connect_nic(nic_b) - pc_b.power_on() - - pc_c = Node(hostname="pc_c") - nic_c = NIC(ip_address="192.168.0.12", subnet_mask="255.255.255.0") - pc_c.connect_nic(nic_c) - pc_c.power_on() - - pc_d = Node(hostname="pc_d") - nic_d = NIC(ip_address="192.168.0.13", subnet_mask="255.255.255.0") - pc_d.connect_nic(nic_d) - pc_d.power_on() - - switch_1 = Switch(hostname="switch_1", num_ports=6) - switch_1.power_on() - - switch_2 = Switch(hostname="switch_2", num_ports=6) - switch_2.power_on() - - link_nic_a_switch_1 = Link(endpoint_a=nic_a, endpoint_b=switch_1.switch_ports[1]) - link_nic_b_switch_1 = Link(endpoint_a=nic_b, endpoint_b=switch_1.switch_ports[2]) - link_nic_c_switch_2 = Link(endpoint_a=nic_c, endpoint_b=switch_2.switch_ports[1]) - link_nic_d_switch_2 = Link(endpoint_a=nic_d, endpoint_b=switch_2.switch_ports[2]) - link_switch_1_switch_2 = Link(endpoint_a=switch_1.switch_ports[6], endpoint_b=switch_2.switch_ports[6]) - - assert pc_a.ping("192.168.0.13") diff --git a/tests/integration_tests/network/test_nic_link_connection.py b/tests/integration_tests/network/test_nic_link_connection.py index f051d026..228099c6 100644 --- a/tests/integration_tests/network/test_nic_link_connection.py +++ b/tests/integration_tests/network/test_nic_link_connection.py @@ -6,8 +6,5 @@ from primaite.simulator.network.hardware.base import Link, NIC def test_link_fails_with_same_nic(): """Tests Link creation fails with endpoint_a and endpoint_b are the same NIC.""" with pytest.raises(ValueError): - nic_a = NIC( - ip_address="192.168.1.2", - subnet_mask="255.255.255.0" - ) + nic_a = NIC(ip_address="192.168.1.2", subnet_mask="255.255.255.0") Link(endpoint_a=nic_a, endpoint_b=nic_a) diff --git a/tests/integration_tests/network/test_switched_network.py b/tests/integration_tests/network/test_switched_network.py new file mode 100644 index 00000000..dc7742f4 --- /dev/null +++ b/tests/integration_tests/network/test_switched_network.py @@ -0,0 +1,25 @@ +from primaite.simulator.network.hardware.base import Link +from primaite.simulator.network.hardware.nodes.computer import Computer +from primaite.simulator.network.hardware.nodes.server import Server +from primaite.simulator.network.hardware.nodes.switch import Switch + + +def test_switched_network(): + """Tests a node can ping another node via the switch.""" + client_1 = Computer( + hostname="client_1", ip_address="192.168.1.10", subnet_mask="255.255.255.0", default_gateway="192.168.1.0" + ) + client_1.power_on() + + server_1 = Server( + hostname=" server_1", ip_address="192.168.1.11", subnet_mask="255.255.255.0", default_gateway="192.168.1.11" + ) + server_1.power_on() + + switch_1 = Switch(hostname="switch_1", num_ports=6) + switch_1.power_on() + + Link(endpoint_a=client_1.ethernet_port[1], endpoint_b=switch_1.switch_ports[1]) + Link(endpoint_a=server_1.ethernet_port[1], endpoint_b=switch_1.switch_ports[2]) + + assert client_1.ping("192.168.1.11") diff --git a/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_router.py b/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_acl.py similarity index 84% rename from tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_router.py rename to tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_acl.py index 48d0fc06..99736421 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_router.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_acl.py @@ -1,12 +1,13 @@ from ipaddress import IPv4Address -from primaite.simulator.network.hardware.nodes.router import AccessControlList, ACLAction, ACLRule +from primaite.simulator.network.hardware.nodes.router import ACLAction, Router from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port def test_add_rule(): - acl = AccessControlList() + router = Router("Router") + acl = router.acl acl.add_rule( action=ACLAction.PERMIT, protocol=IPProtocol.TCP, @@ -25,7 +26,8 @@ def test_add_rule(): def test_remove_rule(): - acl = AccessControlList() + router = Router("Router") + acl = router.acl acl.add_rule( action=ACLAction.PERMIT, protocol=IPProtocol.TCP, @@ -40,7 +42,8 @@ def test_remove_rule(): def test_rules(): - acl = AccessControlList() + router = Router("Router") + acl = router.acl acl.add_rule( action=ACLAction.PERMIT, protocol=IPProtocol.TCP, @@ -59,24 +62,27 @@ def test_rules(): dst_port=Port(80), position=2, ) - assert acl.is_permitted( + is_permitted, rule = acl.is_permitted( protocol=IPProtocol.TCP, src_ip=IPv4Address("192.168.1.1"), src_port=Port(8080), dst_ip=IPv4Address("192.168.1.2"), dst_port=Port(80), ) - assert not acl.is_permitted( + assert is_permitted + is_permitted, rule = acl.is_permitted( protocol=IPProtocol.TCP, src_ip=IPv4Address("192.168.1.3"), src_port=Port(8080), dst_ip=IPv4Address("192.168.1.4"), dst_port=Port(80), ) + assert not is_permitted def test_default_rule(): - acl = AccessControlList() + router = Router("Router") + acl = router.acl acl.add_rule( action=ACLAction.PERMIT, protocol=IPProtocol.TCP, @@ -95,10 +101,11 @@ def test_default_rule(): dst_port=Port(80), position=2, ) - assert not acl.is_permitted( + is_permitted, rule = acl.is_permitted( protocol=IPProtocol.UDP, src_ip=IPv4Address("192.168.1.5"), src_port=Port(8080), dst_ip=IPv4Address("192.168.1.12"), dst_port=Port(80), ) + assert not is_permitted