diff --git a/docs/source/config.rst b/docs/source/config.rst index 23bf6097..575a3139 100644 --- a/docs/source/config.rst +++ b/docs/source/config.rst @@ -92,7 +92,7 @@ At the top level of the network are ``nodes`` and ``links``. * ``acl`` (Router only): Define the ACL rules at each index of the ACL on the router. the possible options are: ``action`` (PERMIT or DENY), ``src_port``, ``dst_port``, ``protocol``, ``src_ip``, ``dst_ip``. Any options left blank default to none which usually means that it will apply across all options. For example leaving ``src_ip`` blank will apply the rule to all IP addresses. * ``services`` (computers and servers only): a list of services to install on the node. They must define a ``ref``, ``type``, and ``options`` that depend on which ``type`` was selected. * ``applications`` (computer and servers only): Similar to services. A list of application to install on the node. - * ``nics`` (computers and servers only): If the node has multiple networking devices, the second, third, fourth, etc... must be defined here with an ``ip_address`` and ``subnet_mask``. + * ``network_interfaces`` (computers and servers only): If the node has multiple networking devices, the second, third, fourth, etc... must be defined here with an ``ip_address`` and ``subnet_mask``. **links:** * ``ref``: unique identifier for this link diff --git a/docs/source/simulation_components/network/base_hardware.rst b/docs/source/simulation_components/network/base_hardware.rst index ae922105..01c68036 100644 --- a/docs/source/simulation_components/network/base_hardware.rst +++ b/docs/source/simulation_components/network/base_hardware.rst @@ -176,7 +176,7 @@ Network Interfaces A Node will typically have one or more NICs attached to it for network connectivity: -- **nics** - A dictionary containing the NIC instances attached to the Node. NICs can be added/removed. +- **network_interfaces** - A dictionary containing the NIC instances attached to the Node. NICs can be added/removed. ------------- Configuration diff --git a/src/primaite/config/_package_data/example_config.yaml b/src/primaite/config/_package_data/example_config.yaml index b777060f..db00bad5 100644 --- a/src/primaite/config/_package_data/example_config.yaml +++ b/src/primaite/config/_package_data/example_config.yaml @@ -659,7 +659,7 @@ simulation: subnet_mask: 255.255.255.0 default_gateway: 192.168.1.1 dns_server: 192.168.1.10 - nics: + network_interfaces: 2: # unfortunately this number is currently meaningless, they're just added in order and take up the next available slot ip_address: 192.168.10.110 subnet_mask: 255.255.255.0 diff --git a/src/primaite/config/_package_data/example_config_2_rl_agents.yaml b/src/primaite/config/_package_data/example_config_2_rl_agents.yaml index 6aa54487..3a6feb68 100644 --- a/src/primaite/config/_package_data/example_config_2_rl_agents.yaml +++ b/src/primaite/config/_package_data/example_config_2_rl_agents.yaml @@ -1070,7 +1070,7 @@ simulation: subnet_mask: 255.255.255.0 default_gateway: 192.168.1.1 dns_server: 192.168.1.10 - nics: + network_interfaces: 2: # unfortunately this number is currently meaningless, they're just added in order and take up the next available slot ip_address: 192.168.10.110 subnet_mask: 255.255.255.0 diff --git a/src/primaite/game/agent/actions.py b/src/primaite/game/agent/actions.py index 0c78f4c9..fe945678 100644 --- a/src/primaite/game/agent/actions.py +++ b/src/primaite/game/agent/actions.py @@ -555,7 +555,7 @@ class NetworkNICAbstractAction(AbstractAction): "network", "node", node_uuid, - "nic", + "network_interface", nic_uuid, self.verb, ] @@ -672,8 +672,8 @@ class ActionManager: self.ip_address_list = [] for node_uuid in self.node_uuids: node_obj = self.game.simulation.network.nodes[node_uuid] - nics = node_obj.nics - for nic_uuid, nic_obj in nics.items(): + network_interfaces = node_obj.network_interfaces + for nic_uuid, nic_obj in network_interfaces.items(): self.ip_address_list.append(nic_obj.ip_address) # action_args are settings which are applied to the action space as a whole. @@ -898,10 +898,10 @@ class ActionManager: """ node_uuid = self.get_node_uuid_by_idx(node_idx) node_obj = self.game.simulation.network.nodes[node_uuid] - nics = list(node_obj.nics.keys()) - if len(nics) <= nic_idx: + network_interfaces = list(node_obj.network_interfaces.keys()) + if len(network_interfaces) <= nic_idx: return None - return nics[nic_idx] + return network_interfaces[nic_idx] @classmethod def from_config(cls, game: "PrimaiteGame", cfg: Dict) -> "ActionManager": @@ -936,7 +936,7 @@ class ActionManager: node_ref = entry["node_ref"] nic_num = entry["nic_num"] node_obj = game.simulation.network.get_node_by_hostname(node_ref) - ip_address = node_obj.ethernet_port[nic_num].ip_address + ip_address = node_obj.network_interface[nic_num].ip_address ip_address_list.append(ip_address) obj = cls( diff --git a/src/primaite/game/agent/observations.py b/src/primaite/game/agent/observations.py index 1f99987b..8f1c739c 100644 --- a/src/primaite/game/agent/observations.py +++ b/src/primaite/game/agent/observations.py @@ -406,7 +406,7 @@ class NodeObservation(AbstractObservation): where: Optional[Tuple[str]] = None, services: List[ServiceObservation] = [], folders: List[FolderObservation] = [], - nics: List[NicObservation] = [], + network_interfaces: List[NicObservation] = [], logon_status: bool = False, num_services_per_node: int = 2, num_folders_per_node: int = 2, @@ -429,9 +429,9 @@ class NodeObservation(AbstractObservation): :type folders: Dict[int,str], optional :param max_folders: Max number of folders in this node's obs space, defaults to 2 :type max_folders: int, optional - :param nics: Mapping between position in observation space and NIC idx, defaults to {} - :type nics: Dict[int,str], optional - :param max_nics: Max number of NICS in this node's obs space, defaults to 5 + :param network_interfaces: Mapping between position in observation space and NIC idx, defaults to {} + :type network_interfaces: Dict[int,str], optional + :param max_nics: Max number of network interfaces in this node's obs space, defaults to 5 :type max_nics: int, optional """ super().__init__() @@ -456,11 +456,11 @@ class NodeObservation(AbstractObservation): msg = f"Too many folders in Node observation for node. Truncating service {truncated_folder.where[-1]}" _LOGGER.warning(msg) - self.nics: List[NicObservation] = nics - while len(self.nics) < num_nics_per_node: - self.nics.append(NicObservation()) - while len(self.nics) > num_nics_per_node: - truncated_nic = self.nics.pop() + self.network_interfaces: List[NicObservation] = network_interfaces + while len(self.network_interfaces) < num_nics_per_node: + self.network_interfaces.append(NicObservation()) + while len(self.network_interfaces) > num_nics_per_node: + truncated_nic = self.network_interfaces.pop() msg = f"Too many NICs in Node observation for node. Truncating service {truncated_nic.where[-1]}" _LOGGER.warning(msg) @@ -469,7 +469,7 @@ class NodeObservation(AbstractObservation): self.default_observation: Dict = { "SERVICES": {i + 1: s.default_observation for i, s in enumerate(self.services)}, "FOLDERS": {i + 1: f.default_observation for i, f in enumerate(self.folders)}, - "NICS": {i + 1: n.default_observation for i, n in enumerate(self.nics)}, + "NETWORK_INTERFACES": {i + 1: n.default_observation for i, n in enumerate(self.network_interfaces)}, "operating_status": 0, } if self.logon_status: @@ -494,7 +494,7 @@ class NodeObservation(AbstractObservation): obs["SERVICES"] = {i + 1: service.observe(state) for i, service in enumerate(self.services)} obs["FOLDERS"] = {i + 1: folder.observe(state) for i, folder in enumerate(self.folders)} obs["operating_status"] = node_state["operating_state"] - obs["NICS"] = {i + 1: nic.observe(state) for i, nic in enumerate(self.nics)} + obs["NETWORK_INTERFACES"] = {i + 1: network_interface.observe(state) for i, network_interface in enumerate(self.network_interfaces)} if self.logon_status: obs["logon_status"] = 0 @@ -508,7 +508,7 @@ class NodeObservation(AbstractObservation): "SERVICES": spaces.Dict({i + 1: service.space for i, service in enumerate(self.services)}), "FOLDERS": spaces.Dict({i + 1: folder.space for i, folder in enumerate(self.folders)}), "operating_status": spaces.Discrete(5), - "NICS": spaces.Dict({i + 1: nic.space for i, nic in enumerate(self.nics)}), + "NETWORK_INTERFACES": spaces.Dict({i + 1: network_interface.space for i, network_interface in enumerate(self.network_interfaces)}), } if self.logon_status: space_shape["logon_status"] = spaces.Discrete(3) @@ -564,13 +564,13 @@ class NodeObservation(AbstractObservation): ] # create some configs for the NIC observation in the format {"nic_num":1}, {"nic_num":2}, {"nic_num":3}, etc. nic_configs = [{"nic_num": i for i in range(num_nics_per_node)}] - nics = [NicObservation.from_config(config=c, game=game, parent_where=where) for c in nic_configs] + network_interfaces = [NicObservation.from_config(config=c, game=game, parent_where=where) for c in nic_configs] logon_status = config.get("logon_status", False) return cls( where=where, services=services, folders=folders, - nics=nics, + network_interfaces=network_interfaces, logon_status=logon_status, num_services_per_node=num_services_per_node, num_folders_per_node=num_folders_per_node, @@ -728,7 +728,7 @@ class AclObservation(AbstractObservation): node_ref = ip_map_config["node_ref"] nic_num = ip_map_config["nic_num"] node_obj = game.simulation.network.nodes[game.ref_map_nodes[node_ref]] - nic_obj = node_obj.ethernet_port[nic_num] + nic_obj = node_obj.network_interface[nic_num] node_ip_to_idx[nic_obj.ip_address] = ip_idx + 2 router_hostname = config["router_hostname"] diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index 89d71f38..60d201f6 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -11,11 +11,12 @@ from primaite.game.agent.interface import AbstractAgent, AgentSettings, ProxyAge from primaite.game.agent.observations import ObservationManager from primaite.game.agent.rewards import RewardFunction from primaite.session.io import SessionIO, SessionIOSettings -from primaite.simulator.network.hardware.base import NIC, NodeOperatingState -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 +from primaite.simulator.network.hardware.base import NodeOperatingState +from primaite.simulator.network.hardware.nodes.host.computer import Computer +from primaite.simulator.network.hardware.nodes.host.host_node import NIC +from primaite.simulator.network.hardware.nodes.network.router import Router +from primaite.simulator.network.hardware.nodes.host.server import Server +from primaite.simulator.network.hardware.nodes.network.switch import Switch from primaite.simulator.sim_container import Simulation from primaite.simulator.system.applications.database_client import DatabaseClient from primaite.simulator.system.applications.red_applications.data_manipulation_bot import DataManipulationBot @@ -305,8 +306,8 @@ class PrimaiteGame: if "options" in application_cfg: opt = application_cfg["options"] new_application.target_url = opt.get("target_url") - if "nics" in node_cfg: - for nic_num, nic_cfg in node_cfg["nics"].items(): + if "network_interfaces" in node_cfg: + for nic_num, nic_cfg in node_cfg["network_interfaces"].items(): new_node.connect_nic(NIC(ip_address=nic_cfg["ip_address"], subnet_mask=nic_cfg["subnet_mask"])) net.add_node(new_node) @@ -320,11 +321,11 @@ class PrimaiteGame: if isinstance(node_a, Switch): endpoint_a = node_a.switch_ports[link_cfg["endpoint_a_port"]] else: - endpoint_a = node_a.ethernet_port[link_cfg["endpoint_a_port"]] + endpoint_a = node_a.network_interface[link_cfg["endpoint_a_port"]] if isinstance(node_b, Switch): endpoint_b = node_b.switch_ports[link_cfg["endpoint_b_port"]] else: - endpoint_b = node_b.ethernet_port[link_cfg["endpoint_b_port"]] + endpoint_b = node_b.network_interface[link_cfg["endpoint_b_port"]] new_link = net.connect(endpoint_a=endpoint_a, endpoint_b=endpoint_b) game.ref_map_links[link_cfg["ref"]] = new_link.uuid diff --git a/src/primaite/notebooks/uc2_demo.ipynb b/src/primaite/notebooks/uc2_demo.ipynb index 679e8226..9d90963f 100644 --- a/src/primaite/notebooks/uc2_demo.ipynb +++ b/src/primaite/notebooks/uc2_demo.ipynb @@ -126,7 +126,7 @@ " - FILES\n", " - \n", " - health_status\n", - " - NICS\n", + " - NETWORK_INTERFACES\n", " - \n", " - nic_status\n", " - operating_status\n", @@ -180,7 +180,7 @@ "\n", "The ACL rules in the observation space appear in the same order that they do in the actual ACL. Though, only the first 10 rules are shown, there are default rules lower down that cannot be changed by the agent. The extra rules just allow the network to function normally, by allowing pings, ARP traffic, etc.\n", "\n", - "Most nodes have only 1 nic, so the observation for those is placed at NIC index 1 in the observation space. Only the security suite has 2 NICs, the second NIC in the observation space is the one that connects the security suite with swtich_2.\n", + "Most nodes have only 1 network_interface, so the observation for those is placed at NIC index 1 in the observation space. Only the security suite has 2 NICs, the second NIC in the observation space is the one that connects the security suite with swtich_2.\n", "\n", "The meaning of the services' operating_state is:\n", "|operating_state|label|\n", @@ -462,37 +462,37 @@ " 10: {'PROTOCOLS': {'ALL': 1}}},\n", " 'NODES': {1: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", " 'health_status': 0}},\n", - " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", + " 'NETWORK_INTERFACES': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", " 'SERVICES': {1: {'health_status': 0, 'operating_status': 1}},\n", " 'operating_status': 1},\n", " 2: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", " 'health_status': 0}},\n", - " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", + " 'NETWORK_INTERFACES': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", " 'SERVICES': {1: {'health_status': 0, 'operating_status': 1}},\n", " 'operating_status': 1},\n", " 3: {'FOLDERS': {1: {'FILES': {1: {'health_status': 1}},\n", " 'health_status': 1}},\n", - " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", + " 'NETWORK_INTERFACES': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", " 'operating_status': 1},\n", " 4: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", " 'health_status': 0}},\n", - " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", + " 'NETWORK_INTERFACES': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", " 'operating_status': 1},\n", " 5: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", " 'health_status': 0}},\n", - " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", + " 'NETWORK_INTERFACES': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", " 'operating_status': 1},\n", " 6: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", " 'health_status': 0}},\n", - " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", + " 'NETWORK_INTERFACES': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", " 'operating_status': 1},\n", " 7: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", " 'health_status': 0}},\n", - " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", + " 'NETWORK_INTERFACES': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", " 'operating_status': 1}}}\n" ] @@ -588,31 +588,31 @@ "output_type": "stream", "text": [ "{1: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", - " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", + " 'NETWORK_INTERFACES': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", " 'SERVICES': {1: {'health_status': 0, 'operating_status': 1}},\n", " 'operating_status': 1},\n", " 2: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", - " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", + " 'NETWORK_INTERFACES': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", " 'SERVICES': {1: {'health_status': 0, 'operating_status': 1}},\n", " 'operating_status': 1},\n", " 3: {'FOLDERS': {1: {'FILES': {1: {'health_status': 1}}, 'health_status': 1}},\n", - " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", + " 'NETWORK_INTERFACES': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", " 'operating_status': 1},\n", " 4: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", - " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", + " 'NETWORK_INTERFACES': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", " 'operating_status': 1},\n", " 5: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", - " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", + " 'NETWORK_INTERFACES': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", " 'operating_status': 1},\n", " 6: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", - " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", + " 'NETWORK_INTERFACES': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", " 'operating_status': 1},\n", " 7: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", - " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", + " 'NETWORK_INTERFACES': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", " 'operating_status': 1}}\n" ] @@ -639,31 +639,31 @@ "output_type": "stream", "text": [ "{1: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", - " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", + " 'NETWORK_INTERFACES': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", " 'SERVICES': {1: {'health_status': 0, 'operating_status': 1}},\n", " 'operating_status': 1},\n", " 2: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", - " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", + " 'NETWORK_INTERFACES': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", " 'SERVICES': {1: {'health_status': 3, 'operating_status': 1}},\n", " 'operating_status': 1},\n", " 3: {'FOLDERS': {1: {'FILES': {1: {'health_status': 2}}, 'health_status': 1}},\n", - " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", + " 'NETWORK_INTERFACES': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", " 'operating_status': 1},\n", " 4: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", - " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", + " 'NETWORK_INTERFACES': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", " 'operating_status': 1},\n", " 5: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", - " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", + " 'NETWORK_INTERFACES': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", " 'operating_status': 1},\n", " 6: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", - " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", + " 'NETWORK_INTERFACES': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", " 'operating_status': 1},\n", " 7: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", - " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", + " 'NETWORK_INTERFACES': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", " 'operating_status': 1}}\n" ] diff --git a/src/primaite/simulator/core.py b/src/primaite/simulator/core.py index 98a7e8db..964dac01 100644 --- a/src/primaite/simulator/core.py +++ b/src/primaite/simulator/core.py @@ -4,7 +4,7 @@ from abc import ABC, abstractmethod from typing import Callable, ClassVar, Dict, List, Optional, Union from uuid import uuid4 -from pydantic import BaseModel, ConfigDict +from pydantic import BaseModel, ConfigDict, Field from primaite import getLogger @@ -150,14 +150,12 @@ class SimComponent(BaseModel): model_config = ConfigDict(arbitrary_types_allowed=True, extra="allow") """Configure pydantic to allow arbitrary types and to let the instance have attributes not present in model.""" - uuid: str + uuid: str = Field(default_factory=lambda: str(uuid4())) """The component UUID.""" _original_state: Dict = {} def __init__(self, **kwargs): - if not kwargs.get("uuid"): - kwargs["uuid"] = str(uuid4()) super().__init__(**kwargs) self._request_manager: RequestManager = self._init_request_manager() self._parent: Optional["SimComponent"] = None diff --git a/src/primaite/simulator/network/container.py b/src/primaite/simulator/network/container.py index 8d8709d3..df793319 100644 --- a/src/primaite/simulator/network/container.py +++ b/src/primaite/simulator/network/container.py @@ -7,11 +7,11 @@ from prettytable import MARKDOWN, PrettyTable from primaite import getLogger from primaite.simulator.core import RequestManager, RequestType, SimComponent -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 +from primaite.simulator.network.hardware.base import Link, Node, WiredNetworkInterface +from primaite.simulator.network.hardware.nodes.host.computer import Computer +from primaite.simulator.network.hardware.nodes.network.router import Router +from primaite.simulator.network.hardware.nodes.host.server import Server +from primaite.simulator.network.hardware.nodes.network.switch import Switch from primaite.simulator.system.applications.application import Application from primaite.simulator.system.services.service import Service @@ -62,8 +62,8 @@ class Network(SimComponent): for node in self.nodes.values(): node.power_on() - for nic in node.nics.values(): - nic.enable() + for network_interface in node.network_interfaces.values(): + network_interface.enable() # Reset software for software in node.software_manager.software.values(): if isinstance(software, Service): @@ -148,7 +148,7 @@ class Network(SimComponent): table.title = "IP Addresses" for nodes in nodes_type_map.values(): for node in nodes: - for i, port in node.ethernet_port.items(): + for i, port in node.network_interface.items(): table.add_row([node.hostname, i, port.ip_address, port.subnet_mask, node.default_gateway]) print(table) @@ -209,8 +209,8 @@ class Network(SimComponent): node_b = link.endpoint_b._connected_node hostname_a = node_a.hostname if node_a else None hostname_b = node_b.hostname if node_b else None - port_a = link.endpoint_a._port_num_on_node - port_b = link.endpoint_b._port_num_on_node + port_a = link.endpoint_a.port_num + port_b = link.endpoint_b.port_num state["links"][uuid] = link.describe_state() state["links"][uuid]["hostname_a"] = hostname_a state["links"][uuid]["hostname_b"] = hostname_b @@ -272,7 +272,7 @@ class Network(SimComponent): self._node_request_manager.remove_request(name=node.uuid) def connect( - self, endpoint_a: Union[NIC, SwitchPort], endpoint_b: Union[NIC, SwitchPort], **kwargs + self, endpoint_a: Union[WiredNetworkInterface], endpoint_b: Union[WiredNetworkInterface], **kwargs ) -> Optional[Link]: """ Connect two endpoints on the network by creating a link between their NICs/SwitchPorts. @@ -280,9 +280,9 @@ class Network(SimComponent): .. note:: If the nodes owning the endpoints are not already in the network, they are automatically added. :param endpoint_a: The first endpoint to connect. - :type endpoint_a: Union[NIC, SwitchPort] + :type endpoint_a: WiredNetworkInterface :param endpoint_b: The second endpoint to connect. - :type endpoint_b: Union[NIC, SwitchPort] + :type endpoint_b: WiredNetworkInterface :raises RuntimeError: If any validation or runtime checks fail. """ node_a: Node = endpoint_a.parent diff --git a/src/primaite/simulator/network/creation.py b/src/primaite/simulator/network/creation.py index 48313a1f..370d85da 100644 --- a/src/primaite/simulator/network/creation.py +++ b/src/primaite/simulator/network/creation.py @@ -2,9 +2,9 @@ from ipaddress import IPv4Address from typing import Optional from primaite.simulator.network.container import Network -from primaite.simulator.network.hardware.nodes.computer import Computer -from primaite.simulator.network.hardware.nodes.router import ACLAction, Router -from primaite.simulator.network.hardware.nodes.switch import Switch +from primaite.simulator.network.hardware.nodes.host.computer import Computer +from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router +from primaite.simulator.network.hardware.nodes.network.switch import Switch from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port @@ -111,7 +111,7 @@ def create_office_lan( if num_of_switches > 1: network.connect(core_switch.switch_ports[core_switch_port], switch.switch_ports[24]) else: - network.connect(router.ethernet_ports[1], switch.switch_ports[24]) + network.connect(router.network_interface[1], switch.switch_ports[24]) # Add PCs to the LAN and connect them to switches for i in range(1, num_pcs + 1): @@ -127,7 +127,7 @@ def create_office_lan( core_switch_port += 1 network.connect(core_switch.switch_ports[core_switch_port], switch.switch_ports[24]) else: - network.connect(router.ethernet_ports[1], switch.switch_ports[24]) + network.connect(router.network_interface[1], switch.switch_ports[24]) # Create and add a PC to the network pc = Computer( @@ -142,7 +142,7 @@ def create_office_lan( # Connect the PC to the switch switch_port += 1 - network.connect(switch.switch_ports[switch_port], pc.ethernet_port[1]) + network.connect(switch.switch_ports[switch_port], pc.network_interface[1]) switch.switch_ports[switch_port].enable() return network diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 9edf7518..5299b3dd 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -2,11 +2,14 @@ from __future__ import annotations import re import secrets +from abc import abstractmethod, ABC from ipaddress import IPv4Address, IPv4Network from pathlib import Path -from typing import Any, Dict, List, Literal, Optional, Union +from typing import Any, Literal, Union +from typing import Dict, Optional from prettytable import MARKDOWN, PrettyTable +from pydantic import Field, BaseModel from primaite import getLogger from primaite.exceptions import NetworkError @@ -15,10 +18,7 @@ from primaite.simulator.core import RequestManager, RequestType, SimComponent from primaite.simulator.domain.account import Account from primaite.simulator.file_system.file_system import FileSystem from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState -from primaite.simulator.network.protocols.arp import ARPEntry, ARPPacket -from primaite.simulator.network.transmission.data_link_layer import EthernetHeader, Frame -from primaite.simulator.network.transmission.network_layer import IPPacket -from primaite.simulator.network.transmission.transport_layer import Port, TCPHeader +from primaite.simulator.network.transmission.data_link_layer import Frame from primaite.simulator.system.applications.application import Application from primaite.simulator.system.core.packet_capture import PacketCapture from primaite.simulator.system.core.session_manager import SessionManager @@ -26,6 +26,7 @@ from primaite.simulator.system.core.software_manager import SoftwareManager from primaite.simulator.system.core.sys_log import SysLog from primaite.simulator.system.processes.process import Process from primaite.simulator.system.services.service import Service +from primaite.utils.validators import IPV4Address _LOGGER = getLogger(__name__) @@ -34,14 +35,6 @@ def generate_mac_address(oui: Optional[str] = None) -> str: """ Generate a random MAC Address. - :Example: - - >>> generate_mac_address() - 'ef:7e:97:c8:a8:ce' - - >>> generate_mac_address(oui='aa:bb:cc') - 'aa:bb:cc:42:ba:41' - :param oui: The Organizationally Unique Identifier (OUI) portion of the MAC address. It should be a string with the first 3 bytes (24 bits) in the format "XX:XX:XX". :raises ValueError: If the 'oui' is not in the correct format (hexadecimal and 6 characters). @@ -55,111 +48,46 @@ def generate_mac_address(oui: Optional[str] = None) -> str: _LOGGER.error(msg) raise ValueError(msg) oui_bytes = [int(chunk, 16) for chunk in oui.split(":")] - mac = oui_bytes + random_bytes[len(oui_bytes) :] + mac = oui_bytes + random_bytes[len(oui_bytes):] else: mac = random_bytes return ":".join(f"{b:02x}" for b in mac) -class NIC(SimComponent): +class NetworkInterface(SimComponent, ABC): """ - Models a Network Interface Card (NIC) in a computer or network device. + A generic Network Interface in a Node on a Network. - :param ip_address: The IPv4 address assigned to the NIC. - :param subnet_mask: The subnet mask assigned to the NIC. - :param gateway: The default gateway IP address for forwarding network traffic to other networks. - :param mac_address: The MAC address of the NIC. Defaults to a randomly set MAC address. - :param speed: The speed of the NIC in Mbps (default is 100 Mbps). - :param mtu: The Maximum Transmission Unit (MTU) of the NIC in Bytes, representing the largest data packet size it - can handle without fragmentation (default is 1500 B). - :param wake_on_lan: Indicates if the NIC supports Wake-on-LAN functionality. - :param dns_servers: List of IP addresses of DNS servers used for name resolution. + This is a base class for specific types of network interfaces, providing common attributes and methods required + for network communication. It defines the fundamental properties that all network interfaces share, such as + MAC address, speed, MTU (maximum transmission unit), and the ability to enable or disable the interface. + + :ivar str mac_address: The MAC address of the network interface. Default to a randomly generated MAC address. + :ivar int speed: The speed of the interface in Mbps. Default is 100 Mbps. + :ivar int mtu: The Maximum Transmission Unit (MTU) of the interface in Bytes. Default is 1500 B. """ - ip_address: IPv4Address - "The IP address assigned to the NIC for communication on an IP-based network." - subnet_mask: IPv4Address - "The subnet mask assigned to the NIC." - mac_address: str - "The MAC address of the NIC. Defaults to a randomly set MAC address. Randomly generated upon creation." + mac_address: str = Field(default_factory=generate_mac_address) + "The MAC address of the interface." + speed: int = 100 - "The speed of the NIC in Mbps. Default is 100 Mbps." + "The speed of the interface in Mbps. Default is 100 Mbps." + mtu: int = 1500 - "The Maximum Transmission Unit (MTU) of the NIC in Bytes. Default is 1500 B" - wake_on_lan: bool = False - "Indicates if the NIC supports Wake-on-LAN functionality." - _connected_node: Optional[Node] = None - "The Node to which the NIC is connected." - _port_num_on_node: Optional[int] = None - "Which port number is assigned on this NIC" - _connected_link: Optional[Link] = None - "The Link to which the NIC is connected." + "The Maximum Transmission Unit (MTU) of the interface in Bytes. Default is 1500 B" + enabled: bool = False - "Indicates whether the NIC is enabled." + "Indicates whether the interface is enabled." + + _connected_node: Optional[Node] = None + "The Node to which the interface is connected." + + port_num: Optional[int] = None + "The port number assigned to this interface on the connected node." + pcap: Optional[PacketCapture] = None - - def __init__(self, **kwargs): - """ - NIC constructor. - - Performs some type conversion the calls ``super().__init__()``. Then performs some checking on the ip_address - and gateway just to check that it's all been configured correctly. - - :raises ValueError: When the ip_address and gateway are the same. And when the ip_address/subnet mask are a - network address. - """ - if not isinstance(kwargs["ip_address"], IPv4Address): - kwargs["ip_address"] = IPv4Address(kwargs["ip_address"]) - if "mac_address" not in kwargs: - kwargs["mac_address"] = generate_mac_address() - super().__init__(**kwargs) - - if self.ip_network.network_address == self.ip_address: - msg = ( - f"Failed to set IP address {self.ip_address} and subnet mask {self.subnet_mask} as it is a " - f"network address {self.ip_network.network_address}" - ) - _LOGGER.error(msg) - raise ValueError(msg) - - self.set_original_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 reset_component_for_episode(self, episode: int): - """Reset the original state of the SimComponent.""" - super().reset_component_for_episode(episode) - if episode and self.pcap: - self.pcap.current_episode = episode - self.pcap.setup_logger() - self.enable() - - 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 - """ - state = super().describe_state() - state.update( - { - "ip_address": str(self.ip_address), - "subnet_mask": str(self.subnet_mask), - "mac_address": self.mac_address, - "speed": self.speed, - "mtu": self.mtu, - "wake_on_lan": self.wake_on_lan, - "enabled": self.enabled, - } - ) - return state + "A PacketCapture instance for capturing and analysing packets passing through this interface." def _init_request_manager(self) -> RequestManager: rm = super()._init_request_manager() @@ -169,202 +97,11 @@ class NIC(SimComponent): return rm - @property - def ip_network(self) -> IPv4Network: - """ - Return the IPv4Network of the NIC. - - :return: The IPv4Network from the ip_address/subnet mask. - """ - return IPv4Network(f"{self.ip_address}/{self.subnet_mask}", strict=False) - - def enable(self): - """Attempt to enable the NIC.""" - if self.enabled: - return - if not self._connected_node: - _LOGGER.debug(f"NIC {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.error(f"NIC {self} cannot be enabled as the endpoint is not turned on") - return - if not self._connected_link: - _LOGGER.debug(f"NIC {self} cannot be enabled as it is not connected to a Link") - return - - self.enabled = True - self._connected_node.sys_log.info(f"NIC {self} enabled") - self.pcap = PacketCapture(hostname=self._connected_node.hostname, ip_address=self.ip_address) - if self._connected_link: - self._connected_link.endpoint_up() - - def disable(self): - """Disable the NIC.""" - if not self.enabled: - return - - self.enabled = False - if self._connected_node: - self._connected_node.sys_log.info(f"NIC {self} disabled") - else: - _LOGGER.debug(f"NIC {self} disabled") - if self._connected_link: - self._connected_link.endpoint_down() - - def connect_link(self, link: Link): - """ - Connect the NIC to a link. - - :param link: The link to which the NIC is connected. - :type link: :class:`~primaite.simulator.network.transmission.physical_layer.Link` - """ - if self._connected_link: - _LOGGER.error(f"Cannot connect Link to NIC ({self.mac_address}) as it already has a connection") - return - - if self._connected_link == link: - _LOGGER.error(f"Cannot connect Link to NIC ({self.mac_address}) as it is already connected") - return - - # TODO: Inform the Node that a link has been connected - self._connected_link = link - self.enable() - _LOGGER.debug(f"NIC {self} connected to Link {link}") - - def disconnect_link(self): - """Disconnect the NIC from the connected Link.""" - if self._connected_link.endpoint_a == self: - self._connected_link.endpoint_a = None - if self._connected_link.endpoint_b == self: - self._connected_link.endpoint_b = None - self._connected_link = None - - def add_dns_server(self, ip_address: IPv4Address): - """ - Add a DNS server IP address. - - :param ip_address: The IP address of the DNS server to be added. - :type ip_address: ipaddress.IPv4Address - """ - pass - - def remove_dns_server(self, ip_address: IPv4Address): - """ - Remove a DNS server IP Address. - - :param ip_address: The IP address of the DNS server to be removed. - :type ip_address: ipaddress.IPv4Address - """ - pass - - def send_frame(self, frame: Frame) -> bool: - """ - Send a network frame from the NIC to the connected link. - - :param frame: The network frame to be sent. - :type frame: :class:`~primaite.simulator.network.osi_layers.Frame` - """ - if self.enabled: - frame.set_sent_timestamp() - self.pcap.capture_outbound(frame) - self._connected_link.transmit_frame(sender_nic=self, frame=frame) - return True - # Cannot send Frame as the NIC is not enabled - return False - - def receive_frame(self, frame: Frame) -> bool: - """ - Receive a network frame from the connected link, processing it if the NIC is enabled. - - This method decrements the Time To Live (TTL) of the frame, captures it using PCAP (Packet Capture), and checks - if the frame is either a broadcast or destined for this NIC. If the frame is acceptable, it is passed to the - connected node. The method also handles the discarding of frames with TTL expired and logs this event. - - The frame's reception is based on various conditions: - - If the NIC is disabled, the frame is not processed. - - If the TTL of the frame reaches zero after decrement, it is discarded and logged. - - If the frame is a broadcast or its destination MAC/IP address matches this NIC's, it is accepted. - - All other frames are dropped and logged or printed to the console. - - :param frame: The network frame being received. This should be an instance of the Frame class. - :return: Returns True if the frame is processed and passed to the node, False otherwise. - """ - 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 - accept_frame = False - - # Check if it's a broadcast: - if frame.ethernet.dst_mac_addr == "ff:ff:ff:ff:ff:ff": - if frame.ip.dst_ip_address in {self.ip_address, self.ip_network.broadcast_address}: - accept_frame = True - else: - if frame.ethernet.dst_mac_addr == self.mac_address: - accept_frame = True - - if accept_frame: - self._connected_node.receive_frame(frame=frame, from_nic=self) - return True - return False - - def __str__(self) -> str: - return f"{self.mac_address}/{self.ip_address}" - - -class SwitchPort(SimComponent): - """ - Models a switch port in a network switch device. - - :param mac_address: The MAC address of the SwitchPort. Defaults to a randomly set MAC address. - :param speed: The speed of the SwitchPort in Mbps (default is 100 Mbps). - :param mtu: The Maximum Transmission Unit (MTU) of the SwitchPort in Bytes, representing the largest data packet - size it can handle without fragmentation (default is 1500 B). - """ - - port_num: int = 1 - mac_address: str - "The MAC address of the SwitchPort. Defaults to a randomly set MAC address." - speed: int = 100 - "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[Node] = None - "The Node to which the SwitchPort is connected." - _port_num_on_node: Optional[int] = None - "The port num on the connected node." - _connected_link: Optional[Link] = None - "The Link to which the SwitchPort is connected." - enabled: bool = False - "Indicates whether the SwitchPort is enabled." - pcap: Optional[PacketCapture] = None - - def __init__(self, **kwargs): - """The SwitchPort constructor.""" - if "mac_address" not in kwargs: - kwargs["mac_address"] = generate_mac_address() - super().__init__(**kwargs) - - self.set_original_state() - - def set_original_state(self): - """Sets the original state.""" - vals_to_include = {"port_num", "mac_address", "speed", "mtu", "enabled"} - self._original_state = self.model_dump(include=vals_to_include) - super().set_original_state() - 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 """ state = super().describe_state() state.update( @@ -377,58 +114,135 @@ class SwitchPort(SimComponent): ) return state + def reset_component_for_episode(self, episode: int): + """Reset the original state of the SimComponent.""" + super().reset_component_for_episode(episode) + if episode and self.pcap: + self.pcap.current_episode = episode + self.pcap.setup_logger() + self.enable() + + @abstractmethod def enable(self): - """Attempt to enable the SwitchPort.""" + """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 + + def __str__(self) -> str: + """ + String representation of the NIC. + + :return: A string combining the port number and the mac address + """ + return f"Port {self.port_num}: {self.mac_address}" + + +class WiredNetworkInterface(NetworkInterface, ABC): + """ + Represents a wired network interface in a network device. + + This abstract base class serves as a foundational blueprint for wired network interfaces, offering core + functionalities and enforcing the implementation of key operational methods such as enabling and disabling the + interface. It encapsulates common attributes and behaviors intrinsic to wired interfaces, including the + management of physical or logical connections to network links and the provision of methods for connecting to and + disconnecting from these links. + + Inherits from: + - NetworkInterface: Provides basic network interface properties and methods. + + + Subclasses of this class are expected to provide concrete implementations for the abstract methods defined here, + tailoring the functionality to the specific requirements of the wired interface types they represent. + """ + + _connected_link: Optional[Link] = None + "The network link to which the network interface is connected." + + def enable(self): + """Attempt to enable the network interface.""" if self.enabled: return if not self._connected_node: - _LOGGER.error(f"SwitchPort {self} cannot be enabled as it is not connected to a 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"SwitchPort {self} cannot be enabled as the endpoint is not turned 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"SwitchPort {self} enabled") - self.pcap = PacketCapture(hostname=self._connected_node.hostname, switch_port_number=self.port_num) + self._connected_node.sys_log.info(f"Network Interface {self} enabled") + self.pcap = PacketCapture(hostname=self._connected_node.hostname, interface_num=self.port_num) if self._connected_link: self._connected_link.endpoint_up() def disable(self): - """Disable the SwitchPort.""" + """Disable the network interface.""" if not self.enabled: return self.enabled = False if self._connected_node: - self._connected_node.sys_log.info(f"SwitchPort {self} disabled") + self._connected_node.sys_log.info(f"Network Interface {self} disabled") else: - _LOGGER.debug(f"SwitchPort {self} disabled") + _LOGGER.debug(f"Interface {self} disabled") if self._connected_link: self._connected_link.endpoint_down() def connect_link(self, link: Link): """ - Connect the SwitchPort to a link. + Connect this network interface to a specified link. - :param link: The link to which the SwitchPort is connected. + This method establishes a connection between the network interface and a network link if the network interface is not already + connected. If the network interface is already connected to a link, it logs an error and does not change the existing + connection. + + :param link: The Link instance to connect to this network interface. """ if self._connected_link: - _LOGGER.error(f"Cannot connect link to SwitchPort {self.mac_address} as it already has a connection") + _LOGGER.error(f"Cannot connect Link to network interface {self} as it already has a connection") return if self._connected_link == link: - _LOGGER.error(f"Cannot connect Link to SwitchPort {self.mac_address} as it is already connected") + _LOGGER.error(f"Cannot connect Link to network interface {self} as it is already connected") return - # TODO: Inform the Switch that a link has been connected self._connected_link = link - _LOGGER.debug(f"SwitchPort {self} connected to Link {link}") self.enable() def disconnect_link(self): - """Disconnect the SwitchPort from the connected Link.""" + """ + Disconnect the network interface from its connected Link, if any. + + This method removes the association between the network interface and its connected Link. It updates the connected Link's + endpoints to reflect the disconnection. + """ if self._connected_link.endpoint_a == self: self._connected_link.endpoint_a = None if self._connected_link.endpoint_b == self: @@ -437,38 +251,220 @@ class SwitchPort(SimComponent): def send_frame(self, frame: Frame) -> bool: """ - Send a network frame from the SwitchPort to the connected link. + Attempt to send a network frame through the connected Link. + + This method sends a frame if the NIC is enabled and connected to a link. It captures the frame using PCAP + (if available) and transmits it through the connected link. 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, False if the Network Interface is disabled or not connected to a link. """ if self.enabled: + frame.set_sent_timestamp() self.pcap.capture_outbound(frame) self._connected_link.transmit_frame(sender_nic=self, frame=frame) return True - # Cannot send Frame as the SwitchPort is not enabled + # Cannot send Frame as the NIC is not enabled return False + @abstractmethod def receive_frame(self, frame: Frame) -> bool: """ - Receive a network frame from the connected link if the SwitchPort is enabled. - - The Frame is passed to the Node. + 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. """ - 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 - self.pcap.capture_inbound(frame) - connected_node: Node = self._connected_node - connected_node.forward_frame(frame=frame, incoming_port=self) - return True - return False + pass - def __str__(self) -> str: - return f"{self.mac_address}" + +class Layer3Interface(BaseModel, ABC): + """ + Represents a Layer 3 (Network Layer) interface in a network device. + + This class serves as a base for network interfaces that operate at Layer 3 of the OSI model, providing IP + connectivity and subnetting capabilities. It's not meant to be instantiated directly but to be subclassed by + specific types of network interfaces that require IP addressing capabilities. + + :ivar IPV4Address ip_address: The IP address assigned to the interface. This address enables the interface to + participate in IP-based networking, allowing it to send and receive IP packets. + :ivar IPv4Address subnet_mask: The subnet mask assigned to the interface. This mask helps in determining the + network segment that the interface belongs to and is used in IP routing decisions. + """ + ip_address: IPV4Address + "The IP address assigned to the interface for communication on an IP-based network." + + subnet_mask: IPV4Address + "The subnet mask assigned to the interface, defining the network portion and the host portion of the IP 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. + """ + state = { + "ip_address": str(self.ip_address), + "subnet_mask": str(self.subnet_mask), + } + + return state + + @property + def ip_network(self) -> IPv4Network: + """ + Calculate and return the IPv4Network derived from the NIC's IP address and subnet mask. + + This property constructs an IPv4Network object which represents the whole network that the NIC's IP address + belongs to, based on its subnet mask. It's useful for determining the network range and broadcast address. + + :return: An IPv4Network instance representing the network of this NIC. + """ + return IPv4Network(f"{self.ip_address}/{self.subnet_mask}", strict=False) + + +class IPWiredNetworkInterface(WiredNetworkInterface, Layer3Interface, ABC): + """ + Represents an IP wired 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. This abstract class serves as a template for creating specific + wired network interfaces that support Internet Protocol (IP) functionalities. + + As this class is an amalgamation of its parent classes without additional attributes or methods, it is recommended + to refer to the documentation of `WiredNetworkInterface` and `Layer3Interface` for detailed information on the + supported operations and functionalities. + + The class inherits from: + - `WiredNetworkInterface`: Provides the functionalities and characteristics of a wired connection, such as + physical link establishment and data transmission over a cable. + - `Layer3Interface`: Enables network layer capabilities, including IP address assignment, routing, and + potentially, Layer 3 protocols like IPsec. + + As an abstract class, `IPWiredNetworkInterface` does not implement specific methods but mandates that any derived + class provides implementations for the functionalities of both `WiredNetworkInterface` and `Layer3Interface`. + This structure is ideal for representing network interfaces in devices that require wired connections and are + capable of IP routing and addressing, such as routers, switches, as well as end-host devices like computers and + servers. + + Derived classes should define specific behaviors and properties of an IP-capable wired network interface, + customizing it for their specific use cases. + """ + + 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)) + + return state + + def enable(self): + super().enable() + try: + self._connected_node.default_gateway_hello() + except AttributeError: + pass + + + @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 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): @@ -480,10 +476,10 @@ class Link(SimComponent): :param bandwidth: The bandwidth of the Link in Mbps (default is 100 Mbps). """ - endpoint_a: Union[NIC, SwitchPort] - "The first NIC or SwitchPort connected to the Link." - endpoint_b: Union[NIC, SwitchPort] - "The second NIC or SwitchPort connected to the Link." + endpoint_a: Union[WiredNetworkInterface] + "The first WiredNetworkInterface connected to the Link." + endpoint_b: Union[WiredNetworkInterface] + "The second WiredNetworkInterface connected to the Link." bandwidth: float = 100.0 "The bandwidth of the Link in Mbps (default is 100 Mbps)." current_load: float = 0.0 @@ -567,7 +563,7 @@ class Link(SimComponent): return True return False - def transmit_frame(self, sender_nic: Union[NIC, SwitchPort], frame: Frame) -> bool: + def transmit_frame(self, sender_nic: Union[WiredNetworkInterface], frame: Frame) -> bool: """ Send a network frame from one NIC or SwitchPort to another connected NIC or SwitchPort. @@ -599,6 +595,7 @@ class Link(SimComponent): def __str__(self) -> str: return f"{self.endpoint_a}<-->{self.endpoint_b}" + class Node(SimComponent): """ A basic Node class that represents a node on the network. @@ -612,14 +609,14 @@ class Node(SimComponent): hostname: str "The node hostname on the network." - default_gateway: Optional[IPv4Address] = None + default_gateway: Optional[IPV4Address] = None "The default gateway IP address for forwarding network traffic to other networks." operating_state: NodeOperatingState = NodeOperatingState.OFF "The hardware state of the node." - nics: Dict[str, NIC] = {} - "The NICs on the node." - ethernet_port: Dict[int, NIC] = {} - "The NICs on the node by port id." + network_interfaces: Dict[str, NetworkInterface] = {} + "The Network Interfaces on the node." + network_interface: Dict[int, NetworkInterface] = {} + "The Network Interfaces on the node by port id." dns_server: Optional[IPv4Address] = None "List of IP addresses of DNS servers used for name resolution." @@ -673,9 +670,6 @@ class Node(SimComponent): This method initializes the ARP cache, ICMP handler, session manager, and software manager if they are not provided. """ - if kwargs.get("default_gateway"): - if not isinstance(kwargs["default_gateway"], IPv4Address): - kwargs["default_gateway"] = IPv4Address(kwargs["default_gateway"]) if not kwargs.get("sys_log"): kwargs["sys_log"] = SysLog(kwargs["hostname"]) if not kwargs.get("session_manager"): @@ -698,6 +692,9 @@ class Node(SimComponent): self._install_system_software() self.set_original_state() + # def model_post_init(self, __context: Any) -> None: + # self._install_system_software() + # self.set_original_state() def set_original_state(self): """Sets the original state.""" @@ -706,8 +703,8 @@ class Node(SimComponent): self.file_system.set_original_state() - for nic in self.nics.values(): - nic.set_original_state() + for network_interface in self.network_interfaces.values(): + network_interface.set_original_state() vals_to_include = { "hostname", @@ -736,8 +733,8 @@ class Node(SimComponent): self.file_system.reset_component_for_episode(episode) # Reset all Nics - for nic in self.nics.values(): - nic.reset_component_for_episode(episode) + for network_interface in self.network_interfaces.values(): + network_interface.reset_component_for_episode(episode) for software in self.software_manager.software.values(): software.reset_component_for_episode(episode) @@ -754,7 +751,7 @@ class Node(SimComponent): self._service_request_manager = RequestManager() rm.add_request("service", RequestType(func=self._service_request_manager)) self._nic_request_manager = RequestManager() - rm.add_request("nic", RequestType(func=self._nic_request_manager)) + rm.add_request("network_interface", RequestType(func=self._nic_request_manager)) rm.add_request("file_system", RequestType(func=self.file_system._request_manager)) @@ -796,8 +793,8 @@ class Node(SimComponent): { "hostname": self.hostname, "operating_state": self.operating_state.value, - "NICs": {eth_num: nic.describe_state() for eth_num, nic in self.ethernet_port.items()}, - # "switch_ports": {uuid, sp for uuid, sp in self.switch_ports.items()}, + "NICs": {eth_num: network_interface.describe_state() for eth_num, network_interface in + self.network_interface.items()}, "file_system": self.file_system.describe_state(), "applications": {app.name: app.describe_state() for app in self.applications.values()}, "services": {svc.name: svc.describe_state() for svc in self.services.values()}, @@ -807,14 +804,12 @@ class Node(SimComponent): ) return state - def show(self, markdown: bool = False, component: Literal["NIC", "OPEN_PORTS"] = "NIC"): - """A multi-use .show function that accepts either NIC or OPEN_PORTS.""" - if component == "NIC": - self._show_nic(markdown) - elif component == "OPEN_PORTS": - self._show_open_ports(markdown) + def show(self, markdown: bool = False): + "Show function that calls both show NIC and show open ports." + self.show_nic(markdown) + self.show_open_ports(markdown) - def _show_open_ports(self, markdown: bool = False): + def show_open_ports(self, markdown: bool = False): """Prints a table of the open ports on the Node.""" table = PrettyTable(["Port", "Name"]) if markdown: @@ -825,21 +820,22 @@ class Node(SimComponent): table.add_row([port.value, port.name]) print(table) - def _show_nic(self, markdown: bool = False): + def show_nic(self, markdown: bool = False): """Prints a table of the NICs on the Node.""" - table = PrettyTable(["Port", "MAC Address", "Address", "Speed", "Status"]) + table = PrettyTable(["Port", "Type", "MAC Address", "Address", "Speed", "Status"]) if markdown: table.set_style(MARKDOWN) table.align = "l" table.title = f"{self.hostname} Network Interface Cards" - for port, nic in self.ethernet_port.items(): + for port, network_interface in self.network_interface.items(): table.add_row( [ port, - nic.mac_address, - f"{nic.ip_address}/{nic.ip_network.prefixlen}", - nic.speed, - "Enabled" if nic.enabled else "Disabled", + network_interface.__name__, + network_interface.mac_address, + f"{network_interface.ip_address}/{network_interface.ip_network.prefixlen}", + network_interface.speed, + "Enabled" if network_interface.enabled else "Disabled", ] ) print(table) @@ -864,9 +860,8 @@ class Node(SimComponent): if self.operating_state == NodeOperatingState.BOOTING: self.operating_state = NodeOperatingState.ON self.sys_log.info(f"{self.hostname}: Turned on") - for nic in self.nics.values(): - if nic._connected_link: - nic.enable() + for network_interface in self.network_interfaces.values(): + network_interface.enable() self._start_up_actions() @@ -975,23 +970,22 @@ class Node(SimComponent): if self.start_up_duration <= 0: self.operating_state = NodeOperatingState.ON self._start_up_actions() - self.sys_log.info("Turned on") - for nic in self.nics.values(): - if nic._connected_link: - nic.enable() + self.sys_log.info("Power on") + for network_interface in self.network_interfaces.values(): + network_interface.enable() def power_off(self): """Power off the Node, disabling its NICs if it is in the ON state.""" if self.operating_state == NodeOperatingState.ON: - for nic in self.nics.values(): - nic.disable() + for network_interface in self.network_interfaces.values(): + network_interface.disable() self.operating_state = NodeOperatingState.SHUTTING_DOWN self.shut_down_countdown = self.shut_down_duration if self.shut_down_duration <= 0: self._shut_down_actions() self.operating_state = NodeOperatingState.OFF - self.sys_log.info("Turned off") + self.sys_log.info("Power off") def reset(self): """ @@ -1000,59 +994,57 @@ class Node(SimComponent): Powers off the node and sets is_resetting to True. Applying more timesteps will eventually turn the node back on. """ - if not self.operating_state.ON: - self.sys_log.error(f"Cannot reset {self.hostname} - node is not turned on.") - else: + if self.operating_state.ON: self.is_resetting = True - self.sys_log.info(f"Resetting {self.hostname}...") + self.sys_log.info(f"Resetting") self.power_off() - def connect_nic(self, nic: NIC): + def connect_nic(self, network_interface: NetworkInterface): """ - Connect a NIC (Network Interface Card) to the node. + Connect a Network Interface to the node. - :param nic: The NIC to connect. + :param network_interface: The NIC to connect. :raise NetworkError: If the NIC is already connected. """ - if nic.uuid not in self.nics: - self.nics[nic.uuid] = nic - self.ethernet_port[len(self.nics)] = nic - nic._connected_node = self - nic._port_num_on_node = len(self.nics) - nic.parent = self - self.sys_log.info(f"Connected NIC {nic}") + if network_interface.uuid not in self.network_interfaces: + self.network_interfaces[network_interface.uuid] = network_interface + self.network_interface[len(self.network_interfaces)] = network_interface + network_interface._connected_node = self + network_interface.port_num = len(self.network_interfaces) + network_interface.parent = self + self.sys_log.info(f"Connected Network Interface {network_interface}") if self.operating_state == NodeOperatingState.ON: - nic.enable() - self._nic_request_manager.add_request(nic.uuid, RequestType(func=nic._request_manager)) + network_interface.enable() + self._nic_request_manager.add_request( + network_interface.uuid, RequestType(func=network_interface._request_manager) + ) else: - msg = f"Cannot connect NIC {nic} as it is already connected" + msg = f"Cannot connect NIC {network_interface} as it is already connected" self.sys_log.logger.error(msg) - _LOGGER.error(msg) raise NetworkError(msg) - def disconnect_nic(self, nic: Union[NIC, str]): + def disconnect_nic(self, network_interface: Union[NetworkInterface, str]): """ Disconnect a NIC (Network Interface Card) from the node. - :param nic: The NIC to Disconnect, or its UUID. + :param network_interface: The NIC to Disconnect, or its UUID. :raise NetworkError: If the NIC is not connected. """ - if isinstance(nic, str): - nic = self.nics.get(nic) - if nic or nic.uuid in self.nics: - for port, _nic in self.ethernet_port.items(): - if nic == _nic: - self.ethernet_port.pop(port) + if isinstance(network_interface, str): + network_interface = self.network_interfaces.get(network_interface) + if network_interface or network_interface.uuid in self.network_interfaces: + for port, _nic in self.network_interface.items(): + if network_interface == _nic: + self.network_interface.pop(port) break - self.nics.pop(nic.uuid) - nic.parent = None - nic.disable() - self.sys_log.info(f"Disconnected NIC {nic}") - self._nic_request_manager.remove_request(nic.uuid) + self.network_interfaces.pop(network_interface.uuid) + network_interface.parent = None + network_interface.disable() + self.sys_log.info(f"Disconnected Network Interface {network_interface}") + self._nic_request_manager.remove_request(network_interface.uuid) else: - msg = f"Cannot disconnect NIC {nic} as it is not connected" + msg = f"Cannot disconnect NIC {network_interface} as it is not connected" self.sys_log.logger.error(msg) - _LOGGER.error(msg) raise NetworkError(msg) def ping(self, target_ip_address: Union[IPv4Address, str], pings: int = 4) -> bool: @@ -1065,56 +1057,32 @@ class Node(SimComponent): """ if not isinstance(target_ip_address, IPv4Address): target_ip_address = IPv4Address(target_ip_address) - return self.software_manager.icmp.ping(target_ip_address) + if self.software_manager.icmp: + return self.software_manager.icmp.ping(target_ip_address, pings) + return False - def send_frame(self, frame: Frame): - """ - Send a Frame from the Node to the connected NIC. - - :param frame: The Frame to be sent. - """ - if self.operating_state == NodeOperatingState.ON: - nic: NIC = self._get_arp_cache_nic(frame.ip.dst_ip_address) - nic.send_frame(frame) - - def receive_frame(self, frame: Frame, from_nic: NIC): + @abstractmethod + def receive_frame(self, frame: Frame, from_network_interface: NetworkInterface): """ Receive a Frame from the connected NIC and process it. - Depending on the protocol, the frame is passed to the appropriate handler such as ARP or ICMP, or up to the - SessionManager if no code manager exists. + This is an abstract implementation of receive_frame with some very basic functionality (ARP population). All + Node subclasses should have their own implementation of receive_frame that first calls super().receive_frame( + ) before implementing its own internal receive_frame logic. :param frame: The Frame being received. - :param from_nic: The NIC that received the frame. + :param from_network_interface: The Network Interface that received the frame. """ if self.operating_state == NodeOperatingState.ON: if frame.ip: - if frame.ip.src_ip_address in self.software_manager.arp: + if self.software_manager.arp: self.software_manager.arp.add_arp_cache_entry( - ip_address=frame.ip.src_ip_address, mac_address=frame.ethernet.src_mac_addr, nic=from_nic + ip_address=frame.ip.src_ip_address, + mac_address=frame.ethernet.src_mac_addr, + network_interface=from_network_interface ) - - # Check if the destination port is open on the Node - dst_port = None - if frame.tcp: - dst_port = frame.tcp.dst_port - elif frame.udp: - dst_port = frame.udp.dst_port - - accept_frame = False - if frame.icmp or dst_port in self.software_manager.get_open_ports(): - # accept the frame as the port is open or if it's an ICMP frame - accept_frame = True - - # TODO: add internal node firewall check here? - - if accept_frame: - self.session_manager.receive_frame(frame, from_nic) - else: - # denied as port closed - self.sys_log.info(f"Ignoring frame for port {frame.tcp.dst_port.value} from {frame.ip.src_ip_address}") - # TODO: do we need to do anything more here? - pass + else: + return def install_service(self, service: Service) -> None: """ diff --git a/src/primaite/simulator/network/hardware/network_interface/__init__.py b/src/primaite/simulator/network/hardware/network_interface/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/primaite/simulator/network/hardware/network_interface/layer_3_interface.py b/src/primaite/simulator/network/hardware/network_interface/layer_3_interface.py new file mode 100644 index 00000000..fdfd3b26 --- /dev/null +++ b/src/primaite/simulator/network/hardware/network_interface/layer_3_interface.py @@ -0,0 +1,9 @@ +from abc import ABC +from ipaddress import IPv4Network +from typing import Dict + +from pydantic import BaseModel + +from primaite.utils.validators import IPV4Address + + diff --git a/src/primaite/simulator/network/hardware/network_interface/wired/__init__.py b/src/primaite/simulator/network/hardware/network_interface/wired/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/primaite/simulator/network/hardware/network_interface/wired/router_interface.py b/src/primaite/simulator/network/hardware/network_interface/wired/router_interface.py new file mode 100644 index 00000000..e69de29b diff --git a/src/primaite/simulator/network/hardware/network_interface/wireless/__init__.py b/src/primaite/simulator/network/hardware/network_interface/wireless/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/primaite/simulator/network/hardware/network_interface/wireless/wireless_access_point.py b/src/primaite/simulator/network/hardware/network_interface/wireless/wireless_access_point.py new file mode 100644 index 00000000..f94b7faa --- /dev/null +++ b/src/primaite/simulator/network/hardware/network_interface/wireless/wireless_access_point.py @@ -0,0 +1,84 @@ +from typing import Dict + +from primaite.simulator.network.hardware.base import WirelessNetworkInterface +from primaite.simulator.network.hardware.network_interface.layer_3_interface import Layer3Interface + +from primaite.simulator.network.transmission.data_link_layer import Frame + + +class WirelessAccessPoint(WirelessNetworkInterface, Layer3Interface): + """ + 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 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 WirelessNetworkInterface + state = WirelessNetworkInterface.describe_state(self) + + # Update the state with information from Layer3Interface + state.update(Layer3Interface.describe_state(self)) + + # Update the state with NIC-specific information + state.update( + { + "wake_on_lan": self.wake_on_lan, + } + ) + + return state + + def enable(self): + """Enable the interface.""" + pass + + def disable(self): + """Disable the interface.""" + pass + + 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 + + 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 + + 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}" \ No newline at end of file diff --git a/src/primaite/simulator/network/hardware/network_interface/wireless/wireless_nic.py b/src/primaite/simulator/network/hardware/network_interface/wireless/wireless_nic.py new file mode 100644 index 00000000..12172608 --- /dev/null +++ b/src/primaite/simulator/network/hardware/network_interface/wireless/wireless_nic.py @@ -0,0 +1,81 @@ +from typing import Dict + +from primaite.simulator.network.hardware.base import WirelessNetworkInterface +from primaite.simulator.network.hardware.network_interface.layer_3_interface import Layer3Interface + +from primaite.simulator.network.transmission.data_link_layer import Frame + + +class WirelessNIC(WirelessNetworkInterface, Layer3Interface): + """ + Represents a Wireless Network Interface Card (Wireless NIC) in a network device. + + This class encapsulates the functionalities and attributes of a wireless NIC, combining the characteristics of a + wireless network interface with Layer 3 features. It is capable of connecting to wireless networks, managing + wireless-specific properties such as signal strength and security protocols, and also handling IP-related + functionalities like IP addressing and subnetting. + + 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 participate + in IP-based networking. + + This class can be extended to include more advanced features or to tailor its behavior for specific types of + wireless networks or protocols. + """ + + 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 WirelessNetworkInterface + state = WirelessNetworkInterface.describe_state(self) + + # Update the state with information from Layer3Interface + state.update(Layer3Interface.describe_state(self)) + + # Update the state with NIC-specific information + state.update( + { + "wake_on_lan": self.wake_on_lan, + } + ) + + return state + + def enable(self): + """Enable the interface.""" + pass + + def disable(self): + """Disable the interface.""" + pass + + 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 + + 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 + + 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}" \ No newline at end of file diff --git a/src/primaite/simulator/network/hardware/nodes/host.py b/src/primaite/simulator/network/hardware/nodes/host.py deleted file mode 100644 index b0486538..00000000 --- a/src/primaite/simulator/network/hardware/nodes/host.py +++ /dev/null @@ -1,63 +0,0 @@ -from primaite.simulator.network.hardware.base import NIC, Node -from primaite.simulator.system.applications.web_browser import WebBrowser -from primaite.simulator.system.services.arp.host_arp import HostARP -from primaite.simulator.system.services.dns.dns_client import DNSClient -from primaite.simulator.system.services.ftp.ftp_client import FTPClient -from primaite.simulator.system.services.icmp.icmp import ICMP -from primaite.simulator.system.services.ntp.ntp_client import NTPClient - - -class Host(Node): - """ - A basic Host class. - - Example: - >>> pc_a = Host( - hostname="pc_a", - ip_address="192.168.1.10", - subnet_mask="255.255.255.0", - default_gateway="192.168.1.1" - ) - >>> pc_a.power_on() - - Instances of computer come 'pre-packaged' with the following: - - * Core Functionality: - * Packet Capture - * Sys Log - * Services: - * ARP Service - * ICMP Service - * DNS Client - * FTP Client - * NTP Client - * Applications: - * Web Browser - """ - - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.connect_nic(NIC(ip_address=kwargs["ip_address"], subnet_mask=kwargs["subnet_mask"])) - self._install_system_software() - - def _install_system_software(self): - """Install System Software - software that is usually provided with the OS.""" - # ARP Service - self.software_manager.install(HostARP) - - # ICMP Service - self.software_manager.install(ICMP) - - # DNS Client - self.software_manager.install(DNSClient) - - # FTP Client - self.software_manager.install(FTPClient) - - # NTP Client - self.software_manager.install(NTPClient) - - # Web Browser - self.software_manager.install(WebBrowser) - - super()._install_system_software() diff --git a/src/primaite/simulator/network/hardware/nodes/host/__init__.py b/src/primaite/simulator/network/hardware/nodes/host/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/primaite/simulator/network/hardware/nodes/computer.py b/src/primaite/simulator/network/hardware/nodes/host/computer.py similarity index 61% rename from src/primaite/simulator/network/hardware/nodes/computer.py rename to src/primaite/simulator/network/hardware/nodes/host/computer.py index 61d3e3ff..dc75df69 100644 --- a/src/primaite/simulator/network/hardware/nodes/computer.py +++ b/src/primaite/simulator/network/hardware/nodes/host/computer.py @@ -1,11 +1,7 @@ -from primaite.simulator.network.hardware.base import NIC, Node -from primaite.simulator.network.hardware.nodes.host import Host -from primaite.simulator.system.applications.web_browser import WebBrowser -from primaite.simulator.system.services.dns.dns_client import DNSClient -from primaite.simulator.system.services.ftp.ftp_client import FTPClient +from primaite.simulator.network.hardware.nodes.host.host_node import HostNode -class Computer(Host): +class Computer(HostNode): """ A basic Computer class. diff --git a/src/primaite/simulator/network/hardware/nodes/host/host_node.py b/src/primaite/simulator/network/hardware/nodes/host/host_node.py new file mode 100644 index 00000000..eefee304 --- /dev/null +++ b/src/primaite/simulator/network/hardware/nodes/host/host_node.py @@ -0,0 +1,354 @@ +from __future__ import annotations + +from typing import Dict +from typing import Optional + +from primaite import getLogger +from primaite.simulator.network.hardware.base import IPWiredNetworkInterface +from primaite.simulator.network.hardware.base import Node +from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState +from primaite.simulator.network.transmission.data_link_layer import Frame +from primaite.simulator.system.applications.web_browser import WebBrowser +from primaite.simulator.system.core.packet_capture import PacketCapture +from primaite.simulator.system.services.arp.arp import ARP, ARPPacket +from primaite.simulator.system.services.dns.dns_client import DNSClient +from primaite.simulator.system.services.ftp.ftp_client import FTPClient +from primaite.simulator.system.services.icmp.icmp import ICMP +from primaite.simulator.system.services.ntp.ntp_client import NTPClient +from primaite.utils.validators import IPV4Address + +_LOGGER = getLogger(__name__) + + +# Lives here due to pydantic circular dependency issue :( +class HostARP(ARP): + """ + The Host ARP Service. + + Extends the ARP service with functionalities specific to a host within the network. It provides mechanisms to + resolve and cache MAC addresses and NICs for given IP addresses, focusing on the host's perspective, including + handling the default gateway. + """ + + def get_default_gateway_mac_address(self) -> Optional[str]: + """ + Retrieves the MAC address of the default gateway from the ARP cache. + + :return: The MAC address of the default gateway if it exists in the ARP cache, otherwise None. + """ + if self.software_manager.node.default_gateway: + return self.get_arp_cache_mac_address(self.software_manager.node.default_gateway) + + def get_default_gateway_network_interface(self) -> Optional[NIC]: + """ + Retrieves the NIC associated with the default gateway from the ARP cache. + + :return: The NIC associated with the default gateway if it exists in the ARP cache, otherwise None. + """ + if self.software_manager.node.default_gateway: + return self.get_arp_cache_network_interface(self.software_manager.node.default_gateway) + + def _get_arp_cache_mac_address( + self, ip_address: IPV4Address, is_reattempt: bool = False, is_default_gateway_attempt: bool = False + ) -> Optional[str]: + """ + Internal method to retrieve the MAC address associated with an IP address from the ARP cache. + + :param ip_address: The IP address whose MAC address is to be retrieved. + :param is_reattempt: Indicates if this call is a reattempt after a failed initial attempt. + :param is_default_gateway_attempt: Indicates if this call is an attempt to get the default gateway's MAC address. + :return: The MAC address associated with the IP address if found, otherwise None. + """ + arp_entry = self.arp.get(ip_address) + + if arp_entry: + return arp_entry.mac_address + else: + if not is_reattempt: + self.send_arp_request(ip_address) + return self._get_arp_cache_mac_address( + ip_address=ip_address, is_reattempt=True, is_default_gateway_attempt=is_default_gateway_attempt + ) + else: + if self.software_manager.node.default_gateway: + if not is_default_gateway_attempt: + self.send_arp_request(self.software_manager.node.default_gateway) + return self._get_arp_cache_mac_address( + ip_address=self.software_manager.node.default_gateway, is_reattempt=True, + is_default_gateway_attempt=True + ) + return None + + def get_arp_cache_mac_address(self, ip_address: IPV4Address) -> Optional[str]: + """ + Retrieves the MAC address associated with an IP address from the ARP cache. + + :param ip_address: The IP address whose MAC address is to be retrieved. + :return: The MAC address associated with the IP address if found, otherwise None. + """ + return self._get_arp_cache_mac_address(ip_address) + + def _get_arp_cache_network_interface( + self, ip_address: IPV4Address, is_reattempt: bool = False, is_default_gateway_attempt: bool = False + ) -> Optional[NIC]: + """ + Internal method to retrieve the NIC associated with an IP address from the ARP cache. + + :param ip_address: The IP address whose NIC is to be retrieved. + :param is_reattempt: Indicates if this call is a reattempt after a failed initial attempt. + :param is_default_gateway_attempt: Indicates if this call is an attempt to get the NIC of the default gateway. + :return: The NIC associated with the IP address if found, otherwise None. + """ + arp_entry = self.arp.get(ip_address) + + if arp_entry: + return self.software_manager.node.network_interfaces[arp_entry.network_interface_uuid] + else: + if not is_reattempt: + self.send_arp_request(ip_address) + return self._get_arp_cache_network_interface( + ip_address=ip_address, is_reattempt=True, is_default_gateway_attempt=is_default_gateway_attempt + ) + else: + if self.software_manager.node.default_gateway: + if not is_default_gateway_attempt: + self.send_arp_request(self.software_manager.node.default_gateway) + return self._get_arp_cache_network_interface( + ip_address=self.software_manager.node.default_gateway, is_reattempt=True, + is_default_gateway_attempt=True + ) + return None + + def get_arp_cache_network_interface(self, ip_address: IPV4Address) -> Optional[NIC]: + """ + Retrieves the NIC associated with an IP address from the ARP cache. + + :param ip_address: The IP address whose NIC is to be retrieved. + :return: The NIC associated with the IP address if found, otherwise None. + """ + return self._get_arp_cache_network_interface(ip_address) + + def _process_arp_request(self, arp_packet: ARPPacket, from_network_interface: NIC): + """ + Processes an ARP request. + + Adds a new entry to the ARP cache if the target IP address matches the NIC's IP address and sends an ARP + reply back. + + :param arp_packet: The ARP packet containing the request. + :param from_network_interface: The NIC that received the ARP request. + """ + super()._process_arp_request(arp_packet, from_network_interface) + # Unmatched ARP Request + if arp_packet.target_ip_address != from_network_interface.ip_address: + self.sys_log.info( + f"Ignoring ARP request for {arp_packet.target_ip_address}. Current IP address is {from_network_interface.ip_address}" + ) + return + + # Matched ARP request + self.add_arp_cache_entry( + ip_address=arp_packet.sender_ip_address, mac_address=arp_packet.sender_mac_addr, + network_interface=from_network_interface + ) + arp_packet = arp_packet.generate_reply(from_network_interface.mac_address) + self.send_arp_reply(arp_packet) + + +class NIC(IPWiredNetworkInterface): + """ + Represents a Network Interface Card (NIC) in a Host Node. + + A NIC is a hardware component that provides a computer or other network device with the ability to connect to a + network. It operates at both Layer 2 (Data Link Layer) and Layer 3 (Network Layer) of the OSI model, meaning it + can interpret both MAC addresses and IP addresses. This class combines the functionalities of + WiredNetworkInterface and Layer3Interface, allowing the NIC to manage physical connections and network layer + addressing. + + Inherits from: + - WiredNetworkInterface: Provides properties and methods specific to wired connections, including methods to connect + and disconnect from network links and to manage the enabled/disabled state of the interface. + - Layer3Interface: Provides properties for Layer 3 network configuration, such as IP address and subnet mask. + """ + wake_on_lan: bool = False + "Indicates if the NIC supports Wake-on-LAN functionality." + + def __init__(self, **kwargs): + + super().__init__(**kwargs) + + 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 IPWiredNetworkInterface + state = super().describe_state() + + # Update the state with NIC-specific information + state.update( + { + "wake_on_lan": self.wake_on_lan, + } + ) + + 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 receive_frame(self, frame: Frame) -> bool: + """ + Attempt to receive and process a network frame from the connected Link. + + This method processes a frame if the NIC is enabled. It checks the frame's destination and TTL, captures the + frame using PCAP, and forwards it to the connected Node if valid. Returns True if the frame is processed, + False otherwise (e.g., if the NIC is disabled, or TTL expired). + + :param frame: The network frame being received. + :return: True if the frame is processed and passed to the node, False otherwise. + """ + if self.enabled: + frame.decrement_ttl() + if frame.ip and frame.ip.ttl < 1: + self._connected_node.sys_log.info(f"Frame discarded at {self} as TTL limit reached") + return False + frame.set_received_timestamp() + self.pcap.capture_inbound(frame) + # If this destination or is broadcast + accept_frame = False + + # Check if it's a broadcast: + if frame.ethernet.dst_mac_addr == "ff:ff:ff:ff:ff:ff": + if frame.ip.dst_ip_address in {self.ip_address, self.ip_network.broadcast_address}: + accept_frame = True + else: + if frame.ethernet.dst_mac_addr == self.mac_address: + accept_frame = True + + if accept_frame: + 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}" + + +class HostNode(Node): + """ + Represents a host node in the network. + + Extends the basic functionality of a Node with host-specific services and applications. A host node typically + represents an end-user device in the network, such as a Computer or a Server, and is capable of initiating and + responding to network communications. + + Example: + >>> pc_a = HostNode( + hostname="pc_a", + ip_address="192.168.1.10", + subnet_mask="255.255.255.0", + default_gateway="192.168.1.1" + ) + >>> pc_a.power_on() + + The host comes pre-installed with core functionalities and a suite of services and applications, making it ready + for various network operations and tasks. These include: + + Core Functionality: + ------------------- + + * Packet Capture: Monitors and logs network traffic. + * Sys Log: Logs system events and errors. + + Services: + --------- + + * ARP (Address Resolution Protocol) Service: Resolves IP addresses to MAC addresses. + * ICMP (Internet Control Message Protocol) Service: Handles ICMP operations, such as ping requests. + * DNS (Domain Name System) Client: Resolves domain names to IP addresses. + * FTP (File Transfer Protocol) Client: Enables file transfers between the host and FTP servers. + * NTP (Network Time Protocol) Client: Synchronizes the system clock with NTP servers. + + Applications: + ------------ + + * Web Browser: Provides web browsing capabilities. + """ + network_interfaces: Dict[str, NIC] = {} + "The Network Interfaces on the node." + network_interface: Dict[int, NIC] = {} + "The NICs on the node by port id." + + def __init__(self, ip_address: IPV4Address, subnet_mask: IPV4Address, **kwargs): + super().__init__(**kwargs) + self.connect_nic(NIC(ip_address=ip_address, subnet_mask=subnet_mask)) + + def _install_system_software(self): + """Install System Software - software that is usually provided with the OS.""" + # ARP Service + self.software_manager.install(HostARP) + + # ICMP Service + self.software_manager.install(ICMP) + + # DNS Client + self.software_manager.install(DNSClient) + + # FTP Client + self.software_manager.install(FTPClient) + + # NTP Client + self.software_manager.install(NTPClient) + + # Web Browser + self.software_manager.install(WebBrowser) + + super()._install_system_software() + + def default_gateway_hello(self): + if self.operating_state == NodeOperatingState.ON and self.default_gateway: + self.software_manager.arp.get_default_gateway_mac_address() + + def receive_frame(self, frame: Frame, from_network_interface: NIC): + """ + Receive a Frame from the connected NIC and process it. + + Depending on the protocol, the frame is passed to the appropriate handler such as ARP or ICMP, or up to the + SessionManager if no code manager exists. + + :param frame: The Frame being received. + :param from_network_interface: The NIC that received the frame. + """ + super().receive_frame(frame, from_network_interface) + + # Check if the destination port is open on the Node + dst_port = None + if frame.tcp: + dst_port = frame.tcp.dst_port + elif frame.udp: + dst_port = frame.udp.dst_port + + accept_frame = False + if frame.icmp or dst_port in self.software_manager.get_open_ports(): + # accept the frame as the port is open or if it's an ICMP frame + accept_frame = True + + # TODO: add internal node firewall check here? + + if accept_frame: + self.session_manager.receive_frame(frame, from_network_interface) + else: + # denied as port closed + self.sys_log.info(f"Ignoring frame for port {frame.tcp.dst_port.value} from {frame.ip.src_ip_address}") + # TODO: do we need to do anything more here? + pass diff --git a/src/primaite/simulator/network/hardware/nodes/server.py b/src/primaite/simulator/network/hardware/nodes/host/server.py similarity index 85% rename from src/primaite/simulator/network/hardware/nodes/server.py rename to src/primaite/simulator/network/hardware/nodes/host/server.py index 0a2c361f..148a277f 100644 --- a/src/primaite/simulator/network/hardware/nodes/server.py +++ b/src/primaite/simulator/network/hardware/nodes/host/server.py @@ -1,7 +1,7 @@ -from primaite.simulator.network.hardware.nodes.host import Host +from primaite.simulator.network.hardware.nodes.host.host_node import HostNode -class Server(Host): +class Server(HostNode): """ A basic Server class. @@ -28,4 +28,4 @@ class Server(Host): * Applications: * Web Browser """ - pass + diff --git a/src/primaite/simulator/network/hardware/nodes/network/__init__.py b/src/primaite/simulator/network/hardware/nodes/network/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/primaite/simulator/network/hardware/nodes/network/network_node.py b/src/primaite/simulator/network/hardware/nodes/network/network_node.py new file mode 100644 index 00000000..c7a2060b --- /dev/null +++ b/src/primaite/simulator/network/hardware/nodes/network/network_node.py @@ -0,0 +1,9 @@ +from primaite.simulator.network.hardware.base import Node, NetworkInterface +from primaite.simulator.network.transmission.data_link_layer import Frame + + +class NetworkNode(Node): + """""" + + def receive_frame(self, frame: Frame, from_network_interface: NetworkInterface): + pass diff --git a/src/primaite/simulator/network/hardware/nodes/router.py b/src/primaite/simulator/network/hardware/nodes/network/router.py similarity index 69% rename from src/primaite/simulator/network/hardware/nodes/router.py rename to src/primaite/simulator/network/hardware/nodes/network/router.py index 69717ae6..06464fd9 100644 --- a/src/primaite/simulator/network/hardware/nodes/router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/router.py @@ -1,19 +1,23 @@ from __future__ import annotations -import secrets from enum import Enum from ipaddress import IPv4Address, IPv4Network -from typing import Dict, List, Optional, Tuple, Union +from typing import Dict, Any +from typing import List, Optional, Tuple, Union from prettytable import MARKDOWN, PrettyTable from primaite.simulator.core import RequestManager, RequestType, SimComponent -from primaite.simulator.network.hardware.base import NIC, Node +from primaite.simulator.network.hardware.base import IPWiredNetworkInterface from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState -from primaite.simulator.network.transmission.data_link_layer import EthernetHeader, Frame -from primaite.simulator.network.transmission.network_layer import IPPacket, IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port, TCPHeader +from primaite.simulator.network.hardware.nodes.network.network_node import NetworkNode +from primaite.simulator.network.protocols.arp import ARPPacket +from primaite.simulator.network.transmission.data_link_layer import Frame +from primaite.simulator.network.transmission.network_layer import IPProtocol +from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.system.core.sys_log import SysLog +from primaite.simulator.system.services.arp.arp import ARP +from primaite.simulator.system.services.icmp.icmp import ICMP class ACLAction(Enum): @@ -197,14 +201,14 @@ class AccessControlList(SimComponent): return self._acl def add_rule( - self, - action: ACLAction, - protocol: Optional[IPProtocol] = None, - src_ip_address: Optional[Union[str, IPv4Address]] = None, - src_port: Optional[Port] = None, - dst_ip_address: Optional[Union[str, IPv4Address]] = None, - dst_port: Optional[Port] = None, - position: int = 0, + self, + action: ACLAction, + protocol: Optional[IPProtocol] = None, + src_ip_address: Optional[Union[str, IPv4Address]] = None, + src_port: Optional[Port] = None, + dst_ip_address: Optional[Union[str, IPv4Address]] = None, + dst_port: Optional[Port] = None, + position: int = 0, ) -> None: """ Add a new ACL rule. @@ -251,12 +255,12 @@ class AccessControlList(SimComponent): raise ValueError(f"Cannot remove ACL rule, position {position} is out of bounds.") def is_permitted( - self, - protocol: IPProtocol, - src_ip_address: Union[str, IPv4Address], - src_port: Optional[Port], - dst_ip_address: Union[str, IPv4Address], - dst_port: Optional[Port], + self, + protocol: IPProtocol, + src_ip_address: Union[str, IPv4Address], + src_port: Optional[Port], + dst_ip_address: 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. @@ -278,23 +282,23 @@ class AccessControlList(SimComponent): continue if ( - (rule.src_ip_address == src_ip_address or rule.src_ip_address is None) - and (rule.dst_ip_address == dst_ip_address or rule.dst_ip_address 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_address == src_ip_address or rule.src_ip_address is None) + and (rule.dst_ip_address == dst_ip_address or rule.dst_ip_address 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_address: Union[str, IPv4Address], - src_port: Port, - dst_ip_address: Union[str, IPv4Address], - dst_port: Port, + self, + protocol: IPProtocol, + src_ip_address: Union[str, IPv4Address], + src_port: Port, + dst_ip_address: Union[str, IPv4Address], + dst_port: Port, ) -> List[ACLRule]: """ Get the list of relevant rules for a packet with given properties. @@ -316,11 +320,11 @@ class AccessControlList(SimComponent): continue if ( - (rule.src_ip_address == src_ip_address or rule.src_ip_address is None) - or (rule.dst_ip_address == dst_ip_address or rule.dst_ip_address 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_address == src_ip_address or rule.src_ip_address is None) + or (rule.dst_ip_address == dst_ip_address or rule.dst_ip_address 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) @@ -437,11 +441,11 @@ class RouteTable(SimComponent): pass def add_route( - self, - address: Union[IPv4Address, str], - subnet_mask: Union[IPv4Address, str], - next_hop_ip_address: Union[IPv4Address, str], - metric: float = 0.0, + self, + address: Union[IPv4Address, str], + subnet_mask: Union[IPv4Address, str], + next_hop_ip_address: Union[IPv4Address, str], + metric: float = 0.0, ): """ Add a route to the routing table. @@ -528,7 +532,79 @@ class RouteTable(SimComponent): table.add_row([index, f"{route.address}/{network.prefixlen}", route.next_hop_ip_address, route.metric]) print(table) -class RouterNIC(NIC): + +class RouterARP(ARP): + """ + Inherits from ARPCache and adds router-specific ARP packet processing. + + :ivar SysLog sys_log: A system log for logging messages. + :ivar Router router: The router to which this ARP cache belongs. + """ + router: Optional[Router] = None + + def get_arp_cache_mac_address(self, ip_address: IPv4Address) -> Optional[str]: + arp_entry = self.arp.get(ip_address) + + if arp_entry: + return arp_entry.mac_address + return None + + def get_arp_cache_network_interface(self, ip_address: IPv4Address) -> Optional[RouterInterface]: + + arp_entry = self.arp.get(ip_address) + if arp_entry: + return self.software_manager.node.network_interfaces[arp_entry.network_interface_uuid] + for network_interface in self.router.network_interfaces.values(): + if ip_address in network_interface.ip_network: + return network_interface + return None + + def _process_arp_request(self, arp_packet: ARPPacket, from_network_interface: RouterInterface): + super()._process_arp_request(arp_packet, from_network_interface) + + # If the target IP matches one of the router's NICs + for network_interface in self.router.network_interfaces.values(): + if network_interface.enabled and network_interface.ip_address == arp_packet.target_ip_address: + arp_reply = arp_packet.generate_reply(from_network_interface.mac_address) + self.send_arp_reply(arp_reply) + return + + def _process_arp_reply(self, arp_packet: ARPPacket, from_network_interface: RouterInterface): + if arp_packet.target_ip_address == from_network_interface.ip_address: + super()._process_arp_reply(arp_packet, from_network_interface) + + def receive(self, payload: Any, session_id: str, **kwargs) -> bool: + """ + Processes received data, handling ARP packets. + + :param payload: The payload received. + :param session_id: The session ID associated with the received data. + :param kwargs: Additional keyword arguments. + :return: True if the payload was processed successfully, otherwise False. + """ + if not super().receive(payload, session_id, **kwargs): + return False + + arp_packet: ARPPacket = payload + from_network_interface: RouterInterface = kwargs["from_network_interface"] + + for network_interface in self.router.network_interfaces.values(): + # ARP frame is for this Router + if network_interface.ip_address == arp_packet.target_ip_address: + if payload.request: + self._process_arp_request(arp_packet=arp_packet, from_network_interface=from_network_interface) + else: + self._process_arp_reply(arp_packet=arp_packet, from_network_interface=from_network_interface) + return True + + # ARP frame is not for this router, pass back down to Router to continue routing + frame: Frame = kwargs["frame"] + self.router.process_frame(frame=frame, from_network_interface=from_network_interface) + + return True + + +class RouterNIC(IPWiredNetworkInterface): """ A Router-specific Network Interface Card (NIC) that extends the standard NIC functionality. @@ -561,7 +637,7 @@ class RouterNIC(NIC): 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_nic=self) + self._connected_node.receive_frame(frame=frame, from_network_interface=self) return True return False @@ -569,21 +645,63 @@ class RouterNIC(NIC): return f"{self.mac_address}/{self.ip_address}" -class Router(Node): +class RouterInterface(IPWiredNetworkInterface): + """ + Represents a Router Interface. + + Router interfaces are used to connect routers to networks. They can route packets across different networks, + hence have IP addressing information. + + Inherits from: + - WiredNetworkInterface: Provides properties and methods specific to wired connections. + - Layer3Interface: Provides Layer 3 properties like ip_address and subnet_mask. + """ + + 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}" + + +class Router(NetworkNode): """ A class to represent a network router node. :ivar str hostname: The name of the 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. + :ivar dict kwargs: Optional keyword arguments for SysLog, ACL, RouteTable, RouterARP, RouterICMP. """ num_ports: int - ethernet_ports: Dict[int, RouterNIC] = {} + network_interfaces: Dict[str, RouterInterface] = {} + "The Router Interfaces on the node." + network_interface: Dict[int, RouterInterface] = {} + "The Router Interfaceson the node by port id." acl: AccessControlList route_table: RouteTable - # arp: RouterARPCache - # icmp: RouterICMP def __init__(self, hostname: str, num_ports: int = 5, **kwargs): if not kwargs.get("sys_log"): @@ -592,23 +710,28 @@ class Router(Node): kwargs["acl"] = AccessControlList(sys_log=kwargs["sys_log"], implicit_action=ACLAction.DENY) if not kwargs.get("route_table"): kwargs["route_table"] = RouteTable(sys_log=kwargs["sys_log"]) - # if not kwargs.get("arp"): - # kwargs["arp"] = RouterARPCache(sys_log=kwargs.get("sys_log"), router=self) - # if not kwargs.get("icmp"): - # kwargs["icmp"] = RouterICMP(sys_log=kwargs.get("sys_log"), arp_cache=kwargs.get("arp"), router=self) super().__init__(hostname=hostname, num_ports=num_ports, **kwargs) - # TODO: Install RouterICMP - # TODO: Install RouterARP for i in range(1, self.num_ports + 1): - nic = RouterNIC(ip_address="127.0.0.1", subnet_mask="255.0.0.0", gateway="0.0.0.0") - self.connect_nic(nic) - self.ethernet_ports[i] = nic + network_interface = RouterInterface(ip_address="127.0.0.1", subnet_mask="255.0.0.0", gateway="0.0.0.0") + self.connect_nic(network_interface) + self.network_interface[i] = network_interface - self.arp.nics = self.nics - self.icmp.arp = self.arp + self._set_default_acl() self.set_original_state() + + def _install_system_software(self): + """Install System Software - software that is usually provided with the OS.""" + self.software_manager.install(ICMP) + self.software_manager.install(RouterARP) + arp: RouterARP = self.software_manager.arp # noqa + arp.router = self + + def _set_default_acl(self): + self.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.ARP, dst_port=Port.ARP, position=22) + self.acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol.ICMP, position=23) + def set_original_state(self): """Sets the original state.""" self.acl.set_original_state() @@ -619,11 +742,11 @@ class Router(Node): def reset_component_for_episode(self, episode: int): """Reset the original state of the SimComponent.""" - self.arp.clear() + self.software_manager.arp.clear() self.acl.reset_component_for_episode(episode) self.route_table.reset_component_for_episode(episode) - for i, nic in self.ethernet_ports.items(): - nic.reset_component_for_episode(episode) + for i, network_interface in self.network_interface.items(): + network_interface.reset_component_for_episode(episode) self.enable_port(i) super().reset_component_for_episode(episode) @@ -633,15 +756,15 @@ class Router(Node): rm.add_request("acl", RequestType(func=self.acl._request_manager)) return rm - def _get_port_of_nic(self, target_nic: NIC) -> Optional[int]: + def _get_port_of_nic(self, target_nic: RouterInterface) -> Optional[int]: """ Retrieve the port number for a given NIC. :param target_nic: Target network interface. :return: The port number if NIC is found, otherwise None. """ - for port, nic in self.ethernet_ports.items(): - if nic == target_nic: + for port, network_interface in self.network_interface.items(): + if network_interface == target_nic: return port def describe_state(self) -> Dict: @@ -655,83 +778,98 @@ class Router(Node): state["acl"] = self.acl.describe_state() return state - def process_frame(self, frame: Frame, from_nic: NIC, re_attempt: bool = False) -> None: + def process_frame(self, frame: Frame, from_network_interface: RouterInterface) -> None: """ Process a Frame. :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 from_network_interface: The source network interface. """ - # Check if src ip is on network of one of the NICs - nic = self.arp.get_arp_cache_nic(frame.ip.dst_ip_address) - target_mac = self.arp.get_arp_cache_mac_address(frame.ip.dst_ip_address) + # check if frame is addressed to this Router but has failed to be received by a service of application at the + # receive_frame stage + if frame.ip: + for network_interface in self.network_interfaces.values(): + if network_interface.ip_address == frame.ip.dst_ip_address: + self.sys_log.info(f"Dropping frame destined for this router on an port that isn't open.") + return - if re_attempt and not nic: + network_interface: RouterInterface = self.software_manager.arp.get_arp_cache_network_interface( + frame.ip.dst_ip_address + ) + target_mac = self.software_manager.arp.get_arp_cache_mac_address(frame.ip.dst_ip_address) + self.software_manager.arp.show() + + if not network_interface: self.sys_log.info(f"Destination {frame.ip.dst_ip_address} is unreachable") + # TODO: Send something back to src, is it some sort of ICMP? return - if not nic: - self.arp.send_arp_request( - frame.ip.dst_ip_address, ignore_networks=[frame.ip.src_ip_address, from_nic.ip_address] - ) - return self.process_frame(frame=frame, from_nic=from_nic, re_attempt=True) - - if not nic.enabled: - self.sys_log.info(f"Frame dropped as NIC {nic} is not enabled") + if not network_interface.enabled: + self.sys_log.info(f"Frame dropped as NIC {network_interface} is not enabled") + # TODO: Send something back to src, is it some sort of ICMP? return - if frame.ip.dst_ip_address in nic.ip_network: - from_port = self._get_port_of_nic(from_nic) - to_port = self._get_port_of_nic(nic) + if frame.ip.dst_ip_address in network_interface.ip_network: + from_port = self._get_port_of_nic(from_network_interface) + to_port = self._get_port_of_nic(network_interface) self.sys_log.info(f"Forwarding frame to internally from port {from_port} to port {to_port}") frame.decrement_ttl() if frame.ip and frame.ip.ttl < 1: self.sys_log.info("Frame discarded as TTL limit reached") + # TODO: Send something back to src, is it some sort of ICMP? return - frame.ethernet.src_mac_addr = nic.mac_address + frame.ethernet.src_mac_addr = network_interface.mac_address frame.ethernet.dst_mac_addr = target_mac - nic.send_frame(frame) + network_interface.send_frame(frame) return else: - self._route_frame(frame, from_nic) + self.route_frame(frame, from_network_interface) - def _route_frame(self, frame: Frame, from_nic: NIC, re_attempt: bool = False) -> None: + def route_frame(self, frame: Frame, from_network_interface: RouterInterface) -> None: route = self.route_table.find_best_route(frame.ip.dst_ip_address) if route: - nic = self.arp.get_arp_cache_nic(route.next_hop_ip_address) - target_mac = self.arp.get_arp_cache_mac_address(route.next_hop_ip_address) - if re_attempt and not nic: + network_interface = self.software_managerarp.get_arp_cache_network_interface(route.next_hop_ip_address) + target_mac = self.software_manager.arp.get_arp_cache_mac_address(route.next_hop_ip_address) + if not network_interface: self.sys_log.info(f"Destination {frame.ip.dst_ip_address} is unreachable") + # TODO: Send something back to src, is it some sort of ICMP? return - if not nic: - self.arp.send_arp_request(frame.ip.dst_ip_address, ignore_networks=[frame.ip.src_ip_address]) - return self.process_frame(frame=frame, from_nic=from_nic, re_attempt=True) - - if not nic.enabled: - self.sys_log.info(f"Frame dropped as NIC {nic} is not enabled") + if not network_interface.enabled: + self.sys_log.info(f"Frame dropped as NIC {network_interface} is not enabled") + # TODO: Send something back to src, is it some sort of ICMP? return - from_port = self._get_port_of_nic(from_nic) - to_port = self._get_port_of_nic(nic) + from_port = self._get_port_of_nic(from_network_interface) + to_port = self._get_port_of_nic(network_interface) self.sys_log.info(f"Routing frame to internally from port {from_port} to port {to_port}") frame.decrement_ttl() if frame.ip and frame.ip.ttl < 1: self.sys_log.info("Frame discarded as TTL limit reached") + # TODO: Send something back to src, is it some sort of ICMP? return - frame.ethernet.src_mac_addr = nic.mac_address + frame.ethernet.src_mac_addr = network_interface.mac_address frame.ethernet.dst_mac_addr = target_mac - nic.send_frame(frame) + network_interface.send_frame(frame) - def receive_frame(self, frame: Frame, from_nic: NIC): + def receive_frame(self, frame: Frame, from_network_interface: RouterInterface): """ - Receive a frame from a NIC and processes it based on its protocol. + Receive a frame from a RouterInterface and processes it based on its protocol. :param frame: The incoming frame. - :param from_nic: The network interface where the frame is coming from. + :param from_network_interface: The network interface where the frame is coming from. """ - process_frame = False + + if self.operating_state != NodeOperatingState.ON: + return + + if frame.ip and self.software_manager.arp: + 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 + ) + protocol = frame.ip.protocol src_ip_address = frame.ip.src_ip_address dst_ip_address = frame.ip.dst_ip_address @@ -754,21 +892,32 @@ class Router(Node): ) if not permitted: - at_port = self._get_port_of_nic(from_nic) + at_port = self._get_port_of_nic(from_network_interface) self.sys_log.info(f"Frame blocked at port {at_port} by rule {rule}") return - self.arp.add_arp_cache_entry(src_ip_address, frame.ethernet.src_mac_addr, from_nic) - if frame.ip.protocol == IPProtocol.ICMP: - self.icmp.process_icmp(frame=frame, from_nic=from_nic) + + self.software_manager.arp.add_arp_cache_entry( + ip_address=src_ip_address, mac_address=frame.ethernet.src_mac_addr, + network_interface=from_network_interface + ) + + # Check if the destination port is open on the Node + dst_port = None + if frame.tcp: + dst_port = frame.tcp.dst_port + elif frame.udp: + dst_port = frame.udp.dst_port + + send_to_session_manager = False + if ((frame.icmp and dst_ip_address == from_network_interface.ip_address) + or (dst_port in self.software_manager.get_open_ports())): + send_to_session_manager = True + + if send_to_session_manager: + # Port is open on this Router so pass Frame up to session manager first + self.session_manager.receive_frame(frame, from_network_interface) else: - if src_port == Port.ARP: - self.arp.process_arp_packet(from_nic=from_nic, frame=frame, route_table=self.route_table) - return - else: - # All other traffic - process_frame = True - if process_frame: - self.process_frame(frame, from_nic) + self.process_frame(frame, from_network_interface) def configure_port(self, port: int, ip_address: Union[IPv4Address, str], subnet_mask: Union[IPv4Address, str]): """ @@ -782,10 +931,12 @@ class Router(Node): ip_address = IPv4Address(ip_address) if not isinstance(subnet_mask, IPv4Address): subnet_mask = IPv4Address(subnet_mask) - nic = self.ethernet_ports[port] - nic.ip_address = ip_address - nic.subnet_mask = subnet_mask - self.sys_log.info(f"Configured port {port} with ip_address={ip_address}/{nic.ip_network.prefixlen}") + network_interface = self.network_interface[port] + network_interface.ip_address = ip_address + network_interface.subnet_mask = subnet_mask + self.sys_log.info( + f"Configured Network Interface {network_interface}" + ) self.set_original_state() def enable_port(self, port: int): @@ -794,9 +945,9 @@ class Router(Node): :param port: The port to enable. """ - nic = self.ethernet_ports.get(port) - if nic: - nic.enable() + network_interface = self.network_interface.get(port) + if network_interface: + network_interface.enable() def disable_port(self, port: int): """ @@ -804,9 +955,9 @@ class Router(Node): :param port: The port to disable. """ - nic = self.ethernet_ports.get(port) - if nic: - nic.disable() + network_interface = self.network_interface.get(port) + if network_interface: + network_interface.disable() def show(self, markdown: bool = False): """ @@ -820,14 +971,14 @@ class Router(Node): table.set_style(MARKDOWN) table.align = "l" table.title = f"{self.hostname} Ethernet Interfaces" - for port, nic in self.ethernet_ports.items(): + for port, network_interface in self.network_interface.items(): table.add_row( [ port, - nic.mac_address, - f"{nic.ip_address}/{nic.ip_network.prefixlen}", - nic.speed, - "Enabled" if nic.enabled else "Disabled", + network_interface.mac_address, + f"{network_interface.ip_address}/{network_interface.ip_network.prefixlen}", + network_interface.speed, + "Enabled" if network_interface.enabled else "Disabled", ] ) print(table) diff --git a/src/primaite/simulator/network/hardware/nodes/switch.py b/src/primaite/simulator/network/hardware/nodes/network/switch.py similarity index 56% rename from src/primaite/simulator/network/hardware/nodes/switch.py rename to src/primaite/simulator/network/hardware/nodes/network/switch.py index b394bae0..e7d5d616 100644 --- a/src/primaite/simulator/network/hardware/nodes/switch.py +++ b/src/primaite/simulator/network/hardware/nodes/network/switch.py @@ -1,16 +1,93 @@ -from typing import Dict +from __future__ import annotations +from typing import Dict, Optional from prettytable import MARKDOWN, PrettyTable from primaite import getLogger from primaite.exceptions import NetworkError -from primaite.simulator.network.hardware.base import Link, Node, SwitchPort +from primaite.simulator.network.hardware.base import WiredNetworkInterface, NetworkInterface, Link +from primaite.simulator.network.hardware.nodes.network.network_node import NetworkNode from primaite.simulator.network.transmission.data_link_layer import Frame _LOGGER = getLogger(__name__) -class Switch(Node): +class SwitchPort(WiredNetworkInterface): + """ + Represents a Switch Port. + + Switch ports connect devices within the same network. They operate at the data link layer (Layer 2) of the OSI model + and are responsible for receiving and forwarding frames based on MAC addresses. Despite operating at Layer 2, + they are an essential part of network infrastructure, enabling LAN segmentation, bandwidth management, and + the creation of VLANs. + + Inherits from: + - WiredNetworkInterface: Provides properties and methods specific to wired connections. + + Switch ports typically do not have IP addresses assigned to them as they function at Layer 2, but managed switches + can have management IP addresses for remote management and configuration purposes. + """ + _connected_node: Optional[Switch] = None + "The Switch to which the SwitchPort is connected." + + def set_original_state(self): + """Sets the original state.""" + vals_to_include = {"port_num", "mac_address", "speed", "mtu", "enabled"} + self._original_state = self.model_dump(include=vals_to_include) + super().set_original_state() + + 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 + """ + state = super().describe_state() + state.update( + { + "mac_address": self.mac_address, + "speed": self.speed, + "mtu": self.mtu, + "enabled": self.enabled, + } + ) + return state + + 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. + """ + if self.enabled: + self.pcap.capture_outbound(frame) + self._connected_link.transmit_frame(sender_nic=self, frame=frame) + return True + # Cannot send Frame as the SwitchPort is not enabled + return False + + 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 + self.pcap.capture_inbound(frame) + self._connected_node.receive_frame(frame=frame, from_network_interface=self) + return True + return False + + +class Switch(NetworkNode): """ A class representing a Layer 2 network switch. @@ -30,7 +107,7 @@ class Switch(Node): 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._port_num_on_node = port_num + port.port_num = port_num port.parent = self port.port_num = port_num @@ -78,16 +155,16 @@ class Switch(Node): 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): + def receive_frame(self, frame: Frame, from_network_interface: 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. + :param frame: The Frame being received. + :param from_network_interface: The SwitchPort that received the frame. """ src_mac = frame.ethernet.src_mac_addr dst_mac = frame.ethernet.dst_mac_addr - self._add_mac_table_entry(src_mac, incoming_port) + self._add_mac_table_entry(src_mac, from_network_interface) outgoing_port = self.mac_address_table.get(dst_mac) if outgoing_port and dst_mac.lower() != "ff:ff:ff:ff:ff:ff": @@ -95,7 +172,7 @@ class Switch(Node): 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.enabled and port != incoming_port: + if port.enabled and port != from_network_interface: port.send_frame(frame) def disconnect_link_from_port(self, link: Link, port_number: int): diff --git a/src/primaite/simulator/network/networks.py b/src/primaite/simulator/network/networks.py index 630846b3..1d47fdef 100644 --- a/src/primaite/simulator/network/networks.py +++ b/src/primaite/simulator/network/networks.py @@ -1,11 +1,12 @@ from ipaddress import IPv4Address from primaite.simulator.network.container import Network -from primaite.simulator.network.hardware.base import NIC, NodeOperatingState -from primaite.simulator.network.hardware.nodes.computer import Computer -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.hardware.base import NodeOperatingState +from primaite.simulator.network.hardware.nodes.host.computer import Computer +from primaite.simulator.network.hardware.nodes.host.host_node import NIC +from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router +from primaite.simulator.network.hardware.nodes.host.server import Server +from primaite.simulator.network.hardware.nodes.network.switch import Switch from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.system.applications.database_client import DatabaseClient @@ -40,13 +41,13 @@ def client_server_routed() -> Network: # Switch 1 switch_1 = Switch(hostname="switch_1", num_ports=6) switch_1.power_on() - network.connect(endpoint_a=router_1.ethernet_ports[1], endpoint_b=switch_1.switch_ports[6]) + network.connect(endpoint_a=router_1.network_interface[1], endpoint_b=switch_1.switch_ports[6]) router_1.enable_port(1) # Switch 2 switch_2 = Switch(hostname="switch_2", num_ports=6) switch_2.power_on() - network.connect(endpoint_a=router_1.ethernet_ports[2], endpoint_b=switch_2.switch_ports[6]) + network.connect(endpoint_a=router_1.network_interface[2], endpoint_b=switch_2.switch_ports[6]) router_1.enable_port(2) # Client 1 @@ -58,7 +59,7 @@ def client_server_routed() -> Network: operating_state=NodeOperatingState.ON, ) client_1.power_on() - network.connect(endpoint_b=client_1.ethernet_port[1], endpoint_a=switch_2.switch_ports[1]) + network.connect(endpoint_b=client_1.network_interface[1], endpoint_a=switch_2.switch_ports[1]) # Server 1 server_1 = Server( @@ -69,7 +70,7 @@ def client_server_routed() -> Network: operating_state=NodeOperatingState.ON, ) server_1.power_on() - network.connect(endpoint_b=server_1.ethernet_port[1], endpoint_a=switch_1.switch_ports[1]) + network.connect(endpoint_b=server_1.network_interface[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) @@ -126,13 +127,13 @@ def arcd_uc2_network() -> Network: # Switch 1 switch_1 = Switch(hostname="switch_1", num_ports=8, operating_state=NodeOperatingState.ON) switch_1.power_on() - network.connect(endpoint_a=router_1.ethernet_ports[1], endpoint_b=switch_1.switch_ports[8]) + network.connect(endpoint_a=router_1.network_interface[1], endpoint_b=switch_1.switch_ports[8]) router_1.enable_port(1) # Switch 2 switch_2 = Switch(hostname="switch_2", num_ports=8, operating_state=NodeOperatingState.ON) switch_2.power_on() - network.connect(endpoint_a=router_1.ethernet_ports[2], endpoint_b=switch_2.switch_ports[8]) + network.connect(endpoint_a=router_1.network_interface[2], endpoint_b=switch_2.switch_ports[8]) router_1.enable_port(2) # Client 1 @@ -145,7 +146,7 @@ def arcd_uc2_network() -> Network: operating_state=NodeOperatingState.ON, ) client_1.power_on() - network.connect(endpoint_b=client_1.ethernet_port[1], endpoint_a=switch_2.switch_ports[1]) + network.connect(endpoint_b=client_1.network_interface[1], endpoint_a=switch_2.switch_ports[1]) client_1.software_manager.install(DataManipulationBot) db_manipulation_bot: DataManipulationBot = client_1.software_manager.software.get("DataManipulationBot") db_manipulation_bot.configure( @@ -167,7 +168,7 @@ def arcd_uc2_network() -> Network: client_2.power_on() web_browser = client_2.software_manager.software.get("WebBrowser") web_browser.target_url = "http://arcd.com/users/" - network.connect(endpoint_b=client_2.ethernet_port[1], endpoint_a=switch_2.switch_ports[2]) + network.connect(endpoint_b=client_2.network_interface[1], endpoint_a=switch_2.switch_ports[2]) # Domain Controller domain_controller = Server( @@ -180,7 +181,7 @@ def arcd_uc2_network() -> Network: domain_controller.power_on() domain_controller.software_manager.install(DNSServer) - network.connect(endpoint_b=domain_controller.ethernet_port[1], endpoint_a=switch_1.switch_ports[1]) + network.connect(endpoint_b=domain_controller.network_interface[1], endpoint_a=switch_1.switch_ports[1]) # Database Server database_server = Server( @@ -192,7 +193,7 @@ def arcd_uc2_network() -> Network: operating_state=NodeOperatingState.ON, ) database_server.power_on() - network.connect(endpoint_b=database_server.ethernet_port[1], endpoint_a=switch_1.switch_ports[3]) + network.connect(endpoint_b=database_server.network_interface[1], endpoint_a=switch_1.switch_ports[3]) ddl = """ CREATE TABLE IF NOT EXISTS user ( @@ -270,7 +271,7 @@ def arcd_uc2_network() -> Network: database_client: DatabaseClient = web_server.software_manager.software.get("DatabaseClient") database_client.configure(server_ip_address=IPv4Address("192.168.1.14")) - network.connect(endpoint_b=web_server.ethernet_port[1], endpoint_a=switch_1.switch_ports[2]) + network.connect(endpoint_b=web_server.network_interface[1], endpoint_a=switch_1.switch_ports[2]) database_client.run() database_client.connect() @@ -291,7 +292,7 @@ def arcd_uc2_network() -> Network: ) backup_server.power_on() backup_server.software_manager.install(FTPServer) - network.connect(endpoint_b=backup_server.ethernet_port[1], endpoint_a=switch_1.switch_ports[4]) + network.connect(endpoint_b=backup_server.network_interface[1], endpoint_a=switch_1.switch_ports[4]) # Security Suite security_suite = Server( @@ -303,9 +304,9 @@ def arcd_uc2_network() -> Network: operating_state=NodeOperatingState.ON, ) security_suite.power_on() - network.connect(endpoint_b=security_suite.ethernet_port[1], endpoint_a=switch_1.switch_ports[7]) + network.connect(endpoint_b=security_suite.network_interface[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]) + network.connect(endpoint_b=security_suite.network_interface[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) diff --git a/src/primaite/simulator/network/protocols/arp.py b/src/primaite/simulator/network/protocols/arp.py index 7b3e4509..2e44884a 100644 --- a/src/primaite/simulator/network/protocols/arp.py +++ b/src/primaite/simulator/network/protocols/arp.py @@ -13,11 +13,12 @@ class ARPEntry(BaseModel): Represents an entry in the ARP cache. :param mac_address: The MAC address associated with the IP address. - :param nic: The NIC through which the NIC with the IP address is reachable. + :param network_interface_uuid: The UIId of the Network Interface through which the NIC with the IP address is + reachable. """ mac_address: str - nic_uuid: str + network_interface_uuid: str class ARPPacket(DataPacket): diff --git a/src/primaite/simulator/system/core/packet_capture.py b/src/primaite/simulator/system/core/packet_capture.py index d3a14d2a..5d34fd63 100644 --- a/src/primaite/simulator/system/core/packet_capture.py +++ b/src/primaite/simulator/system/core/packet_capture.py @@ -21,7 +21,7 @@ class PacketCapture: The PCAPs are logged to: //__pcap.log """ - def __init__(self, hostname: str, ip_address: Optional[str] = None, switch_port_number: Optional[int] = None): + def __init__(self, hostname: str, ip_address: Optional[str] = None, interface_num: Optional[int] = None): """ Initialize the PacketCapture process. @@ -32,8 +32,8 @@ class PacketCapture: "The hostname for which PCAP logs are being recorded." self.ip_address: str = ip_address "The IP address associated with the PCAP logs." - self.switch_port_number = switch_port_number - "The SwitchPort number." + self.interface_num = interface_num + "The interface num on the Node." self.inbound_logger = None self.outbound_logger = None @@ -81,8 +81,8 @@ class PacketCapture: """Get PCAP the logger name.""" if self.ip_address: return f"{self.hostname}_{self.ip_address}_{'outbound' if outbound else 'inbound'}_pcap" - if self.switch_port_number: - return f"{self.hostname}_port-{self.switch_port_number}_{'outbound' if outbound else 'inbound'}_pcap" + if self.interface_num: + return f"{self.hostname}_port-{self.interface_num}_{'outbound' if outbound else 'inbound'}_pcap" return f"{self.hostname}_{'outbound' if outbound else 'inbound'}_pcap" def _get_log_path(self, outbound: bool = False) -> Path: diff --git a/src/primaite/simulator/system/core/session_manager.py b/src/primaite/simulator/system/core/session_manager.py index 2120cde3..eafdac8e 100644 --- a/src/primaite/simulator/system/core/session_manager.py +++ b/src/primaite/simulator/system/core/session_manager.py @@ -13,7 +13,7 @@ from primaite.simulator.network.transmission.network_layer import IPPacket, IPPr from primaite.simulator.network.transmission.transport_layer import Port, TCPHeader, UDPHeader if TYPE_CHECKING: - from primaite.simulator.network.hardware.base import ARPCache, NIC + from primaite.simulator.network.hardware.base import NetworkInterface from primaite.simulator.system.core.software_manager import SoftwareManager from primaite.simulator.system.core.sys_log import SysLog @@ -84,8 +84,6 @@ class SessionManager: self.software_manager: SoftwareManager = None # Noqa self.node: Node = None # noqa - - def describe_state(self) -> Dict: """ Produce a dictionary describing the current state of this object. @@ -104,7 +102,7 @@ class SessionManager: @staticmethod def _get_session_key( - frame: Frame, inbound_frame: bool = True + frame: Frame, inbound_frame: bool = True ) -> Tuple[IPProtocol, IPv4Address, Optional[Port], Optional[Port]]: """ Extracts the session key from the given frame. @@ -142,19 +140,19 @@ class SessionManager: dst_port = None return protocol, with_ip_address, src_port, dst_port - def resolve_outbound_nic(self, dst_ip_address: IPv4Address) -> Optional[NIC]: - for nic in self.node.nics.values(): - if dst_ip_address in nic.ip_network and nic.enabled: - return nic - return self.software_manager.arp.get_default_gateway_nic() + def resolve_outbound_network_interface(self, dst_ip_address: IPv4Address) -> Optional['NetworkInterface']: + for network_interface in self.node.network_interfaces.values(): + if dst_ip_address in network_interface.ip_network and network_interface.enabled: + return network_interface + return self.software_manager.arp.get_default_gateway_network_interface() def resolve_outbound_transmission_details( - self, dst_ip_address: Optional[Union[IPv4Address, IPv4Network]] = None, session_id: Optional[str] = None - ) -> Tuple[Optional["NIC"], Optional[str], IPv4Address, Optional[IPProtocol], bool]: + self, dst_ip_address: Optional[Union[IPv4Address, IPv4Network]] = None, session_id: Optional[str] = None + ) -> Tuple[Optional['NetworkInterface'], Optional[str], IPv4Address, Optional[IPProtocol], bool]: if not isinstance(dst_ip_address, (IPv4Address, IPv4Network)): dst_ip_address = IPv4Address(dst_ip_address) is_broadcast = False - outbound_nic = None + outbound_network_interface = None dst_mac_address = None protocol = None @@ -172,36 +170,36 @@ class SessionManager: dst_ip_address = dst_ip_address.broadcast_address if dst_ip_address: # Find a suitable NIC for the broadcast - for nic in self.node.nics.values(): - if dst_ip_address in nic.ip_network and nic.enabled: + for network_interface in self.node.network_interfaces.values(): + if dst_ip_address in network_interface.ip_network and network_interface.enabled: dst_mac_address = "ff:ff:ff:ff:ff:ff" - outbound_nic = nic + outbound_network_interface = network_interface break else: # Resolve MAC address for unicast transmission use_default_gateway = True - for nic in self.node.nics.values(): - if dst_ip_address in nic.ip_network and nic.enabled: + for network_interface in self.node.network_interfaces.values(): + if dst_ip_address in network_interface.ip_network and network_interface.enabled: dst_mac_address = self.software_manager.arp.get_arp_cache_mac_address(dst_ip_address) break if dst_ip_address: use_default_gateway = False - outbound_nic = self.software_manager.arp.get_arp_cache_nic(dst_ip_address) + outbound_network_interface = self.software_manager.arp.get_arp_cache_network_interface(dst_ip_address) if use_default_gateway: dst_mac_address = self.software_manager.arp.get_default_gateway_mac_address() - outbound_nic = self.software_manager.arp.get_default_gateway_nic() - return outbound_nic, dst_mac_address, dst_ip_address, protocol, is_broadcast + outbound_network_interface = self.software_manager.arp.get_default_gateway_network_interface() + return outbound_network_interface, dst_mac_address, dst_ip_address, protocol, is_broadcast def receive_payload_from_software_manager( - self, - payload: Any, - dst_ip_address: Optional[Union[IPv4Address, IPv4Network]] = None, - dst_port: Optional[Port] = None, - session_id: Optional[str] = None, - ip_protocol: IPProtocol = IPProtocol.TCP, - icmp_packet: Optional[ICMPPacket] = None + self, + payload: Any, + dst_ip_address: Optional[Union[IPv4Address, IPv4Network]] = None, + dst_port: Optional[Port] = None, + session_id: Optional[str] = None, + ip_protocol: IPProtocol = IPProtocol.TCP, + icmp_packet: Optional[ICMPPacket] = None ) -> Union[Any, None]: """ Receive a payload from the SoftwareManager and send it to the appropriate NIC for transmission. @@ -222,19 +220,19 @@ class SessionManager: dst_mac_address = "ff:ff:ff:ff:ff:ff" else: dst_mac_address = payload.target_mac_addr - outbound_nic = self.resolve_outbound_nic(payload.target_ip_address) + outbound_network_interface = self.resolve_outbound_network_interface(payload.target_ip_address) is_broadcast = payload.request ip_protocol = IPProtocol.UDP else: vals = self.resolve_outbound_transmission_details( dst_ip_address=dst_ip_address, session_id=session_id ) - outbound_nic, dst_mac_address, dst_ip_address, protocol, is_broadcast = vals + outbound_network_interface, dst_mac_address, dst_ip_address, protocol, is_broadcast = vals if protocol: ip_protocol = protocol # Check if outbound NIC and destination MAC address are resolved - if not outbound_nic or not dst_mac_address: + if not outbound_network_interface or not dst_mac_address: return False tcp_header = None @@ -249,10 +247,18 @@ class SessionManager: src_port=dst_port, dst_port=dst_port, ) + # TODO: Only create IP packet if not ARP + # ip_packet = None + # if dst_port != Port.ARP: + # IPPacket( + # src_ip_address=outbound_network_interface.ip_address, + # dst_ip_address=dst_ip_address, + # protocol=ip_protocol + # ) # Construct the frame for transmission frame = Frame( - ethernet=EthernetHeader(src_mac_addr=outbound_nic.mac_address, dst_mac_addr=dst_mac_address), - ip=IPPacket(src_ip_address=outbound_nic.ip_address, dst_ip_address=dst_ip_address, protocol=ip_protocol), + ethernet=EthernetHeader(src_mac_addr=outbound_network_interface.mac_address, dst_mac_addr=dst_mac_address), + ip=IPPacket(src_ip_address=outbound_network_interface.ip_address, dst_ip_address=dst_ip_address, protocol=ip_protocol), tcp=tcp_header, udp=udp_header, icmp=icmp_packet, @@ -271,9 +277,9 @@ class SessionManager: self.sessions_by_uuid[session.uuid] = session # Send the frame through the NIC - return outbound_nic.send_frame(frame) + return outbound_network_interface.send_frame(frame) - def receive_frame(self, frame: Frame, from_nic: NIC): + def receive_frame(self, frame: Frame, from_network_interface: 'NetworkInterface'): """ Receive a Frame. @@ -302,7 +308,7 @@ class SessionManager: port=dst_port, protocol=frame.ip.protocol, session_id=session.uuid, - from_nic=from_nic, + from_network_interface=from_network_interface, frame=frame ) diff --git a/src/primaite/simulator/system/core/software_manager.py b/src/primaite/simulator/system/core/software_manager.py index 99dc5f38..53725c18 100644 --- a/src/primaite/simulator/system/core/software_manager.py +++ b/src/primaite/simulator/system/core/software_manager.py @@ -167,7 +167,7 @@ class SoftwareManager: ) def receive_payload_from_session_manager( - self, payload: Any, port: Port, protocol: IPProtocol, session_id: str, from_nic: "NIC", frame: Frame + self, payload: Any, port: Port, protocol: IPProtocol, session_id: str, from_network_interface: "NIC", frame: Frame ): """ Receive a payload from the SessionManager and forward it to the corresponding service or application. @@ -177,7 +177,7 @@ class SoftwareManager: """ receiver: Optional[Union[Service, Application]] = self.port_protocol_mapping.get((port, protocol), None) if receiver: - receiver.receive(payload=payload, session_id=session_id, from_nic=from_nic, frame=frame) + receiver.receive(payload=payload, session_id=session_id, from_network_interface=from_network_interface, frame=frame) else: self.sys_log.error(f"No service or application found for port {port} and protocol {protocol}") pass diff --git a/src/primaite/simulator/system/services/arp/arp.py b/src/primaite/simulator/system/services/arp/arp.py index c5b30d69..6a82432e 100644 --- a/src/primaite/simulator/system/services/arp/arp.py +++ b/src/primaite/simulator/system/services/arp/arp.py @@ -1,17 +1,16 @@ from __future__ import annotations from abc import abstractmethod -from ipaddress import IPv4Address from typing import Any, Dict, Optional, Union from prettytable import MARKDOWN, PrettyTable -from primaite.simulator.network.hardware.base import NIC +from primaite.simulator.network.hardware.base import NetworkInterface from primaite.simulator.network.protocols.arp import ARPEntry, ARPPacket -from primaite.simulator.network.transmission.data_link_layer import EthernetHeader, Frame -from primaite.simulator.network.transmission.network_layer import IPPacket, IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port, UDPHeader +from primaite.simulator.network.transmission.network_layer import IPProtocol +from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.system.services.service import Service +from primaite.utils.validators import IPV4Address class ARP(Service): @@ -21,7 +20,7 @@ class ARP(Service): Manages ARP for resolving network layer addresses into link layer addresses. It maintains an ARP cache, sends ARP requests and replies, and processes incoming ARP packets. """ - arp: Dict[IPv4Address, ARPEntry] = {} + arp: Dict[IPV4Address, ARPEntry] = {} def __init__(self, **kwargs): kwargs["name"] = "ARP" @@ -30,7 +29,7 @@ class ARP(Service): super().__init__(**kwargs) def describe_state(self) -> Dict: - pass + return super().describe_state() def show(self, markdown: bool = False): """ @@ -48,7 +47,7 @@ class ARP(Service): [ str(ip), arp.mac_address, - self.software_manager.node.nics[arp.nic_uuid].mac_address, + self.software_manager.node.network_interfaces[arp.network_interface_uuid].mac_address, ] ) print(table) @@ -57,7 +56,13 @@ class ARP(Service): """Clears the arp cache.""" self.arp.clear() - def add_arp_cache_entry(self, ip_address: IPv4Address, mac_address: str, nic: NIC, override: bool = False): + def add_arp_cache_entry( + self, + ip_address: IPV4Address, + mac_address: str, + network_interface: NetworkInterface, + override: bool = False + ): """ Add an ARP entry to the cache. @@ -66,20 +71,20 @@ class ARP(Service): :param ip_address: The IP address to be added to the cache. :param mac_address: The MAC address associated with the IP address. - :param nic: The NIC through which the NIC with the IP address is reachable. + :param network_interface: The NIC through which the NIC with the IP address is reachable. :param override: If True, an existing entry for the IP address will be overridden. Default is False. """ - for _nic in self.software_manager.node.nics.values(): - if _nic.ip_address == ip_address: + for _network_interface in self.software_manager.node.network_interfaces.values(): + if _network_interface.ip_address == ip_address: return if override or not self.arp.get(ip_address): - self.sys_log.info(f"Adding ARP cache entry for {mac_address}/{ip_address} via NIC {nic}") - arp_entry = ARPEntry(mac_address=mac_address, nic_uuid=nic.uuid) + self.sys_log.info(f"Adding ARP cache entry for {mac_address}/{ip_address} via NIC {network_interface}") + arp_entry = ARPEntry(mac_address=mac_address, network_interface_uuid=network_interface.uuid) self.arp[ip_address] = arp_entry @abstractmethod - def get_arp_cache_mac_address(self, ip_address: IPv4Address) -> Optional[str]: + def get_arp_cache_mac_address(self, ip_address: IPV4Address) -> Optional[str]: """ Retrieves the MAC address associated with a given IP address from the ARP cache. @@ -89,7 +94,7 @@ class ARP(Service): pass @abstractmethod - def get_arp_cache_nic(self, ip_address: IPv4Address) -> Optional[NIC]: + def get_arp_cache_network_interface(self, ip_address: IPV4Address) -> Optional[NetworkInterface]: """ Retrieves the NIC associated with a given IP address from the ARP cache. @@ -98,18 +103,20 @@ class ARP(Service): """ pass - def send_arp_request(self, target_ip_address: Union[IPv4Address, str]): + def send_arp_request(self, target_ip_address: Union[IPV4Address, str]): """ Sends an ARP request to resolve the MAC address of a target IP address. :param target_ip_address: The target IP address for which the MAC address is being requested. """ - outbound_nic = self.software_manager.session_manager.resolve_outbound_nic(target_ip_address) - if outbound_nic: - self.sys_log.info(f"Sending ARP request from NIC {outbound_nic} for ip {target_ip_address}") + outbound_network_interface = self.software_manager.session_manager.resolve_outbound_network_interface( + target_ip_address + ) + if outbound_network_interface: + self.sys_log.info(f"Sending ARP request from NIC {outbound_network_interface} for ip {target_ip_address}") arp_packet = ARPPacket( - sender_ip_address=outbound_nic.ip_address, - sender_mac_addr=outbound_nic.mac_address, + sender_ip_address=outbound_network_interface.ip_address, + sender_mac_addr=outbound_network_interface.mac_address, target_ip_address=target_ip_address, ) self.software_manager.session_manager.receive_payload_from_software_manager( @@ -125,11 +132,13 @@ class ARP(Service): Sends an ARP reply in response to an ARP request. :param arp_reply: The ARP packet containing the reply. - :param from_nic: The NIC from which the ARP reply is sent. + :param from_network_interface: The NIC from which the ARP reply is sent. """ - outbound_nic = self.software_manager.session_manager.resolve_outbound_nic(arp_reply.target_ip_address) - if outbound_nic: + outbound_network_interface = self.software_manager.session_manager.resolve_outbound_network_interface( + arp_reply.target_ip_address + ) + if outbound_network_interface: self.sys_log.info( f"Sending ARP reply from {arp_reply.sender_mac_addr}/{arp_reply.sender_ip_address} " f"to {arp_reply.target_ip_address}/{arp_reply.target_mac_addr} " @@ -147,31 +156,33 @@ class ARP(Service): @abstractmethod - def _process_arp_request(self, arp_packet: ARPPacket, from_nic: NIC): + def _process_arp_request(self, arp_packet: ARPPacket, from_network_interface: NIC): """ Processes an incoming ARP request. :param arp_packet: The ARP packet containing the request. - :param from_nic: The NIC that received the ARP request. + :param from_network_interface: The NIC that received the ARP request. """ self.sys_log.info( f"Received ARP request for {arp_packet.target_ip_address} from " f"{arp_packet.sender_mac_addr}/{arp_packet.sender_ip_address} " ) - def _process_arp_reply(self, arp_packet: ARPPacket, from_nic: NIC): + def _process_arp_reply(self, arp_packet: ARPPacket, from_network_interface: NIC): """ Processes an incoming ARP reply. :param arp_packet: The ARP packet containing the reply. - :param from_nic: The NIC that received the ARP reply. + :param from_network_interface: The NIC that received the ARP reply. """ self.sys_log.info( f"Received ARP response for {arp_packet.sender_ip_address} " - f"from {arp_packet.sender_mac_addr} via NIC {from_nic}" + f"from {arp_packet.sender_mac_addr} via Network Interface {from_network_interface}" ) self.add_arp_cache_entry( - ip_address=arp_packet.sender_ip_address, mac_address=arp_packet.sender_mac_addr, nic=from_nic + ip_address=arp_packet.sender_ip_address, + mac_address=arp_packet.sender_mac_addr, + network_interface=from_network_interface ) def receive(self, payload: Any, session_id: str, **kwargs) -> bool: @@ -183,15 +194,15 @@ class ARP(Service): :param kwargs: Additional keyword arguments. :return: True if the payload was processed successfully, otherwise False. """ - if not isinstance(payload, ARPPacket): - print("failied on payload check", type(payload)) + if not super().receive(payload, session_id, **kwargs): return False - from_nic = kwargs.get("from_nic") + from_network_interface = kwargs.get("from_network_interface") if payload.request: - self._process_arp_request(arp_packet=payload, from_nic=from_nic) + self._process_arp_request(arp_packet=payload, from_network_interface=from_network_interface) else: - self._process_arp_reply(arp_packet=payload, from_nic=from_nic) + self._process_arp_reply(arp_packet=payload, from_network_interface=from_network_interface) + return True def __contains__(self, item: Any) -> bool: """ diff --git a/src/primaite/simulator/system/services/arp/host_arp.py b/src/primaite/simulator/system/services/arp/host_arp.py deleted file mode 100644 index 4d6f7738..00000000 --- a/src/primaite/simulator/system/services/arp/host_arp.py +++ /dev/null @@ -1,95 +0,0 @@ -from __future__ import annotations - -from ipaddress import IPv4Address -from typing import Optional - -from primaite.simulator.network.hardware.base import NIC -from primaite.simulator.system.services.arp.arp import ARP, ARPPacket - - -class HostARP(ARP): - def get_default_gateway_mac_address(self) -> Optional[str]: - if self.software_manager.node.default_gateway: - return self.get_arp_cache_mac_address(self.software_manager.node.default_gateway) - - def get_default_gateway_nic(self) -> Optional[NIC]: - if self.software_manager.node.default_gateway: - return self.get_arp_cache_nic(self.software_manager.node.default_gateway) - - def _get_arp_cache_mac_address( - self, ip_address: IPv4Address, is_reattempt: bool = False, is_default_gateway_attempt: bool = False - ) -> Optional[str]: - arp_entry = self.arp.get(ip_address) - - if arp_entry: - return arp_entry.mac_address - else: - if not is_reattempt: - self.send_arp_request(ip_address) - return self._get_arp_cache_mac_address( - ip_address=ip_address, is_reattempt=True, is_default_gateway_attempt=is_default_gateway_attempt - ) - else: - if self.software_manager.node.default_gateway: - if not is_default_gateway_attempt: - self.send_arp_request(self.software_manager.node.default_gateway) - return self._get_arp_cache_mac_address( - ip_address=self.software_manager.node.default_gateway, is_reattempt=True, is_default_gateway_attempt=True - ) - return None - - def get_arp_cache_mac_address(self, ip_address: IPv4Address) -> Optional[str]: - """ - Get the MAC address associated with an IP address. - - :param ip_address: The IP address to look up in the cache. - :return: The MAC address associated with the IP address, or None if not found. - """ - return self._get_arp_cache_mac_address(ip_address) - - def _get_arp_cache_nic( - self, ip_address: IPv4Address, is_reattempt: bool = False, is_default_gateway_attempt: bool = False - ) -> Optional[NIC]: - arp_entry = self.arp.get(ip_address) - - if arp_entry: - return self.software_manager.node.nics[arp_entry.nic_uuid] - else: - if not is_reattempt: - self.send_arp_request(ip_address) - return self._get_arp_cache_nic( - ip_address=ip_address, is_reattempt=True, is_default_gateway_attempt=is_default_gateway_attempt - ) - else: - if self.software_manager.node.default_gateway: - if not is_default_gateway_attempt: - self.send_arp_request(self.software_manager.node.default_gateway) - return self._get_arp_cache_nic( - ip_address=self.software_manager.node.default_gateway, is_reattempt=True, is_default_gateway_attempt=True - ) - return None - - def get_arp_cache_nic(self, ip_address: IPv4Address) -> Optional[NIC]: - """ - Get the NIC associated with an IP address. - - :param ip_address: The IP address to look up in the cache. - :return: The NIC associated with the IP address, or None if not found. - """ - return self._get_arp_cache_nic(ip_address) - - def _process_arp_request(self, arp_packet: ARPPacket, from_nic: NIC): - super()._process_arp_request(arp_packet, from_nic) - # Unmatched ARP Request - if arp_packet.target_ip_address != from_nic.ip_address: - self.sys_log.info( - f"Ignoring ARP request for {arp_packet.target_ip_address}. Current IP address is {from_nic.ip_address}" - ) - return - - # Matched ARP request - self.add_arp_cache_entry( - ip_address=arp_packet.sender_ip_address, mac_address=arp_packet.sender_mac_addr, nic=from_nic - ) - arp_packet = arp_packet.generate_reply(from_nic.mac_address) - self.send_arp_reply(arp_packet) diff --git a/src/primaite/simulator/system/services/arp/router_arp.py b/src/primaite/simulator/system/services/arp/router_arp.py index 3c32b108..d9108910 100644 --- a/src/primaite/simulator/system/services/arp/router_arp.py +++ b/src/primaite/simulator/system/services/arp/router_arp.py @@ -1,98 +1,78 @@ -# class RouterARPCache(ARPCache): +# from ipaddress import IPv4Address +# from typing import Optional, Any +# +# from primaite.simulator.network.hardware.nodes.network.router import RouterInterface, Router +# from primaite.simulator.network.protocols.arp import ARPPacket +# from primaite.simulator.network.transmission.data_link_layer import Frame +# from primaite.simulator.system.services.arp.arp import ARP +# +# +# class RouterARP(ARP): # """ # Inherits from ARPCache and adds router-specific ARP packet processing. # # :ivar SysLog sys_log: A system log for logging messages. # :ivar Router router: The router to which this ARP cache belongs. # """ +# router: Router # -# def __init__(self, sys_log: SysLog, router: Router): -# super().__init__(sys_log) -# self.router: Router = router +# def get_arp_cache_mac_address(self, ip_address: IPv4Address) -> Optional[str]: +# arp_entry = self.arp.get(ip_address) # -# def process_arp_packet( -# self, from_nic: NIC, frame: Frame, route_table: RouteTable, is_reattempt: bool = False -# ) -> None: -# """ -# Processes a received ARP (Address Resolution Protocol) packet in a router-specific way. +# if arp_entry: +# return arp_entry.mac_address +# return None # -# This method is responsible for handling both ARP requests and responses. It processes ARP packets received on a -# Network Interface Card (NIC) and performs actions based on whether the packet is a request or a reply. This -# includes updating the ARP cache, forwarding ARP replies, sending ARP requests for unknown destinations, and -# handling packet TTL (Time To Live). +# def get_arp_cache_network_interface(self, ip_address: IPv4Address) -> Optional[RouterInterface]: +# arp_entry = self.arp.get(ip_address) +# if arp_entry: +# return self.software_manager.node.network_interfaces[arp_entry.network_interface_uuid] +# return None # -# The method first checks if the ARP packet is a request or a reply. For ARP replies, it updates the ARP cache -# and forwards the reply if necessary. For ARP requests, it checks if the target IP matches one of the router's -# NICs and sends an ARP reply if so. If the destination is not directly connected, it consults the routing table -# to find the best route and reattempts ARP request processing if needed. -# -# :param from_nic: The NIC that received the ARP packet. -# :param frame: The frame containing the ARP packet. -# :param route_table: The routing table of the router. -# :param is_reattempt: Flag to indicate if this is a reattempt of processing the ARP packet, defaults to False. -# """ -# arp_packet = frame.arp -# -# # ARP Reply -# if not arp_packet.request: -# if arp_packet.target_ip_address == from_nic.ip_address: -# # reply to the Router specifically -# self.sys_log.info( -# f"Received ARP response for {arp_packet.sender_ip_address} " -# f"from {arp_packet.sender_mac_addr} via NIC {from_nic}" -# ) -# self.add_arp_cache_entry( -# ip_address=arp_packet.sender_ip_address, -# mac_address=arp_packet.sender_mac_addr, -# nic=from_nic, -# ) -# return -# -# # # Reply for a connected requested -# # nic = self.get_arp_cache_nic(arp_packet.target_ip_address) -# # if nic: -# # self.sys_log.info( -# # f"Forwarding arp reply for {arp_packet.target_ip_address}, from {arp_packet.sender_ip_address}" -# # ) -# # arp_packet.sender_mac_addr = nic.mac_address -# # frame.decrement_ttl() -# # if frame.ip and frame.ip.ttl < 1: -# # self.sys_log.info("Frame discarded as TTL limit reached") -# # return -# # nic.send_frame(frame) -# # return -# -# # ARP Request -# self.sys_log.info( -# f"Received ARP request for {arp_packet.target_ip_address} from " -# f"{arp_packet.sender_mac_addr}/{arp_packet.sender_ip_address} " -# ) -# # Matched ARP request +# def _process_arp_request(self, arp_packet: ARPPacket, from_network_interface: RouterInterface): +# super()._process_arp_request(arp_packet, from_network_interface) # self.add_arp_cache_entry( -# ip_address=arp_packet.sender_ip_address, mac_address=arp_packet.sender_mac_addr, nic=from_nic +# ip_address=arp_packet.sender_ip_address, mac_address=arp_packet.sender_mac_addr, +# network_interface=from_network_interface # ) # # # If the target IP matches one of the router's NICs -# for nic in self.nics.values(): -# if nic.enabled and nic.ip_address == arp_packet.target_ip_address: -# arp_reply = arp_packet.generate_reply(from_nic.mac_address) -# self.send_arp_reply(arp_reply, from_nic) +# for network_interface in self.network_interfaces.values(): +# if network_interface.enabled and network_interface.ip_address == arp_packet.target_ip_address: +# arp_reply = arp_packet.generate_reply(from_network_interface.mac_address) +# self.send_arp_reply(arp_reply) # return # -# # # Check Route Table -# # route = route_table.find_best_route(arp_packet.target_ip_address) -# # if route and route != self.router.route_table.default_route: -# # nic = self.get_arp_cache_nic(route.next_hop_ip_address) -# # -# # if not nic: -# # if not is_reattempt: -# # self.send_arp_request(route.next_hop_ip_address, ignore_networks=[frame.ip.src_ip_address]) -# # return self.process_arp_packet(from_nic, frame, route_table, is_reattempt=True) -# # else: -# # self.sys_log.info("Ignoring ARP request as destination unavailable/No ARP entry found") -# # return -# # else: -# # arp_reply = arp_packet.generate_reply(from_nic.mac_address) -# # self.send_arp_reply(arp_reply, from_nic) -# # return +# def _process_arp_reply(self, arp_packet: ARPPacket, from_network_interface: RouterInterface): +# if arp_packet.target_ip_address == from_network_interface.ip_address: +# super()._process_arp_reply(arp_packet, from_network_interface) # +# def receive(self, payload: Any, session_id: str, **kwargs) -> bool: +# """ +# Processes received data, handling ARP packets. +# +# :param payload: The payload received. +# :param session_id: The session ID associated with the received data. +# :param kwargs: Additional keyword arguments. +# :return: True if the payload was processed successfully, otherwise False. +# """ +# if not super().receive(payload, session_id, **kwargs): +# return False +# +# arp_packet: ARPPacket = payload +# from_network_interface: RouterInterface = kwargs["from_network_interface"] +# +# for network_interface in self.network_interfaces.values(): +# # ARP frame is for this Router +# if network_interface.ip_address == arp_packet.target_ip_address: +# if payload.request: +# self._process_arp_request(arp_packet=arp_packet, from_network_interface=from_network_interface) +# else: +# self._process_arp_reply(arp_packet=arp_packet, from_network_interface=from_network_interface) +# return True +# +# # ARP frame is not for this router, pass back down to Router to continue routing +# frame: Frame = kwargs["frame"] +# self.router.process_frame(frame=frame, from_network_interface=from_network_interface) +# +# return True diff --git a/src/primaite/simulator/system/services/icmp/icmp.py b/src/primaite/simulator/system/services/icmp/icmp.py index 93582350..be943c28 100644 --- a/src/primaite/simulator/system/services/icmp/icmp.py +++ b/src/primaite/simulator/system/services/icmp/icmp.py @@ -3,7 +3,6 @@ from ipaddress import IPv4Address from typing import Dict, Any, Union, Optional, Tuple from primaite import getLogger -from primaite.simulator.network.hardware.base import NIC from primaite.simulator.network.protocols.icmp import ICMPPacket, ICMPType from primaite.simulator.network.transmission.data_link_layer import Frame from primaite.simulator.network.transmission.network_layer import IPProtocol @@ -53,7 +52,7 @@ class ICMP(Service): return False if target_ip_address.is_loopback: self.sys_log.info("Pinging loopback address") - return any(nic.enabled for nic in self.nics.values()) + return any(network_interface.enabled for network_interface in self.network_interfaces.values()) self.sys_log.info(f"Pinging {target_ip_address}:", to_terminal=True) sequence, identifier = 0, None while sequence < pings: @@ -88,9 +87,9 @@ class ICMP(Service): :param pings: The number of pings to send. Defaults to 4. :return: A tuple containing the next sequence number and the identifier. """ - nic = self.software_manager.session_manager.resolve_outbound_nic(target_ip_address) + network_interface = self.software_manager.session_manager.resolve_outbound_network_interface(target_ip_address) - if not nic: + if not network_interface: self.sys_log.error( "Cannot send ICMP echo request as there is no outbound NIC to use. Try configuring the default gateway." ) @@ -118,9 +117,11 @@ class ICMP(Service): """ self.sys_log.info(f"Received echo request from {frame.ip.src_ip_address}") - nic = self.software_manager.session_manager.resolve_outbound_nic(frame.ip.src_ip_address) + network_interface = self.software_manager.session_manager.resolve_outbound_network_interface( + frame.ip.src_ip_address + ) - if not nic: + if not network_interface: self.sys_log.error( "Cannot send ICMP echo reply as there is no outbound NIC to use. Try configuring the default gateway." ) diff --git a/src/primaite/simulator/system/services/icmp/router_icmp.py b/src/primaite/simulator/system/services/icmp/router_icmp.py index 1def00c4..5dcba3f1 100644 --- a/src/primaite/simulator/system/services/icmp/router_icmp.py +++ b/src/primaite/simulator/system/services/icmp/router_icmp.py @@ -16,30 +16,30 @@ # super().__init__(sys_log, arp_cache) # self.router = router # -# def process_icmp(self, frame: Frame, from_nic: NIC, is_reattempt: bool = False): +# def process_icmp(self, frame: Frame, from_network_interface: NIC, is_reattempt: bool = False): # """ # Process incoming ICMP frames based on ICMP type. # # :param frame: The incoming frame to process. -# :param from_nic: The network interface where the frame is coming from. +# :param from_network_interface: The network interface where the frame is coming from. # :param is_reattempt: Flag to indicate if the process is a reattempt. # """ # if frame.icmp.icmp_type == ICMPType.ECHO_REQUEST: # # determine if request is for router interface or whether it needs to be routed # -# for nic in self.router.nics.values(): -# if nic.ip_address == frame.ip.dst_ip_address: -# if nic.enabled: +# for network_interface in self.router.network_interfaces.values(): +# if network_interface.ip_address == frame.ip.dst_ip_address: +# if network_interface.enabled: # # reply to the request # if not is_reattempt: # self.sys_log.info(f"Received echo request from {frame.ip.src_ip_address}") # target_mac_address = self.arp.get_arp_cache_mac_address(frame.ip.src_ip_address) -# src_nic = self.arp.get_arp_cache_nic(frame.ip.src_ip_address) +# src_nic = self.arp.get_arp_cache_network_interface(frame.ip.src_ip_address) # tcp_header = TCPHeader(src_port=Port.ARP, dst_port=Port.ARP) # # # Network Layer # ip_packet = IPPacket( -# src_ip_address=nic.ip_address, +# src_ip_address=network_interface.ip_address, # dst_ip_address=frame.ip.src_ip_address, # protocol=IPProtocol.ICMP, # ) @@ -67,12 +67,12 @@ # return # # # Route the frame -# self.router.process_frame(frame, from_nic) +# self.router.process_frame(frame, from_network_interface) # # elif frame.icmp.icmp_type == ICMPType.ECHO_REPLY: -# for nic in self.router.nics.values(): -# if nic.ip_address == frame.ip.dst_ip_address: -# if nic.enabled: +# for network_interface in self.router.network_interfaces.values(): +# if network_interface.ip_address == frame.ip.dst_ip_address: +# if network_interface.enabled: # time = frame.transmission_duration() # time_str = f"{time}ms" if time > 0 else "<1ms" # self.sys_log.info( @@ -87,4 +87,4 @@ # # return # # Route the frame -# self.router.process_frame(frame, from_nic) +# self.router.process_frame(frame, from_network_interface) diff --git a/src/primaite/utils/validators.py b/src/primaite/utils/validators.py new file mode 100644 index 00000000..13cff653 --- /dev/null +++ b/src/primaite/utils/validators.py @@ -0,0 +1,40 @@ +from ipaddress import IPv4Address +from typing import Any, Final + +from pydantic import ( + BeforeValidator, +) +from typing_extensions import Annotated + + +def ipv4_validator(v: Any) -> IPv4Address: + """ + Validate the input and ensure it can be converted to an IPv4Address instance. + + This function takes an input `v`, and if it's not already an instance of IPv4Address, it tries to convert it to one. + If the conversion is successful, the IPv4Address instance is returned. This is useful for ensuring that any input + data is strictly in the format of an IPv4 address. + + :param v: The input value that needs to be validated or converted to IPv4Address. + :return: An instance of IPv4Address. + :raises ValueError: If `v` is not a valid IPv4 address and cannot be converted to an instance of IPv4Address. + """ + if isinstance(v, IPv4Address): + return v + + return IPv4Address(v) + + +# Define a custom type IPV4Address using the typing_extensions.Annotated. +# Annotated is used to attach metadata to type hints. In this case, it's used to associate the ipv4_validator +# with the IPv4Address type, ensuring that any usage of IPV4Address undergoes validation before assignment. +IPV4Address: Final[Annotated] = Annotated[IPv4Address, BeforeValidator(ipv4_validator)] +""" +IPv4Address with with pre-validation and auto-conversion from str using ipv4_validator. + +This type is essentially an IPv4Address from the standard library's ipaddress module, +but with added validation logic. If you use this custom type, the ipv4_validator function +will automatically check and convert the input value to an instance of IPv4Address before +any Pydantic model uses it. This ensures that any field marked with this type is not just +an IPv4Address in form, but also valid according to the rules defined in ipv4_validator. +""" diff --git a/tests/assets/configs/bad_primaite_session.yaml b/tests/assets/configs/bad_primaite_session.yaml index eddb2211..6861f915 100644 --- a/tests/assets/configs/bad_primaite_session.yaml +++ b/tests/assets/configs/bad_primaite_session.yaml @@ -633,7 +633,7 @@ simulation: subnet_mask: 255.255.255.0 default_gateway: 192.168.1.1 dns_server: 192.168.1.10 - nics: + network_interfaces: 2: # unfortunately this number is currently meaningless, they're just added in order and take up the next available slot ip_address: 192.168.10.110 subnet_mask: 255.255.255.0 diff --git a/tests/assets/configs/eval_only_primaite_session.yaml b/tests/assets/configs/eval_only_primaite_session.yaml index 8c273110..eb469ab8 100644 --- a/tests/assets/configs/eval_only_primaite_session.yaml +++ b/tests/assets/configs/eval_only_primaite_session.yaml @@ -637,7 +637,7 @@ simulation: subnet_mask: 255.255.255.0 default_gateway: 192.168.1.1 dns_server: 192.168.1.10 - nics: + network_interfaces: 2: # unfortunately this number is currently meaningless, they're just added in order and take up the next available slot ip_address: 192.168.10.110 subnet_mask: 255.255.255.0 diff --git a/tests/assets/configs/multi_agent_session.yaml b/tests/assets/configs/multi_agent_session.yaml index dda645c3..5c8ebffd 100644 --- a/tests/assets/configs/multi_agent_session.yaml +++ b/tests/assets/configs/multi_agent_session.yaml @@ -1092,7 +1092,7 @@ simulation: subnet_mask: 255.255.255.0 default_gateway: 192.168.1.1 dns_server: 192.168.1.10 - nics: + network_interfaces: 2: # unfortunately this number is currently meaningless, they're just added in order and take up the next available slot ip_address: 192.168.10.110 subnet_mask: 255.255.255.0 diff --git a/tests/assets/configs/test_primaite_session.yaml b/tests/assets/configs/test_primaite_session.yaml index e86d7f96..d9ca195f 100644 --- a/tests/assets/configs/test_primaite_session.yaml +++ b/tests/assets/configs/test_primaite_session.yaml @@ -642,7 +642,7 @@ simulation: subnet_mask: 255.255.255.0 default_gateway: 192.168.1.1 dns_server: 192.168.1.10 - nics: + network_interfaces: 2: # unfortunately this number is currently meaningless, they're just added in order and take up the next available slot ip_address: 192.168.10.110 subnet_mask: 255.255.255.0 diff --git a/tests/assets/configs/train_only_primaite_session.yaml b/tests/assets/configs/train_only_primaite_session.yaml index e960c1e9..2f76625f 100644 --- a/tests/assets/configs/train_only_primaite_session.yaml +++ b/tests/assets/configs/train_only_primaite_session.yaml @@ -643,7 +643,7 @@ simulation: subnet_mask: 255.255.255.0 default_gateway: 192.168.1.1 dns_server: 192.168.1.10 - nics: + network_interfaces: 2: # unfortunately this number is currently meaningless, they're just added in order and take up the next available slot ip_address: 192.168.10.110 subnet_mask: 255.255.255.0 diff --git a/tests/conftest.py b/tests/conftest.py index 8e458878..0043cad1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,17 +6,15 @@ import pytest import yaml from primaite import getLogger -from primaite.game.game import PrimaiteGame from primaite.session.session import PrimaiteSession # from primaite.environment.primaite_env import Primaite # from primaite.primaite_session import PrimaiteSession from primaite.simulator.network.container import Network -from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState -from primaite.simulator.network.hardware.nodes.computer import Computer -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.hardware.nodes.host.computer import Computer +from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router +from primaite.simulator.network.hardware.nodes.host.server import Server +from primaite.simulator.network.hardware.nodes.network.switch import Switch from primaite.simulator.network.networks import arcd_uc2_network from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port @@ -34,7 +32,7 @@ from primaite import PRIMAITE_PATHS # PrimAITE v3 stuff from primaite.simulator.file_system.file_system import FileSystem -from primaite.simulator.network.hardware.base import Link, Node +from primaite.simulator.network.hardware.base import Node class TestService(Service): @@ -157,7 +155,7 @@ def client_server() -> Tuple[Computer, Server]: server.power_on() # Connect Computer and Server - network.connect(computer.ethernet_port[1], server.ethernet_port[1]) + network.connect(computer.network_interface[1], server.network_interface[1]) # Should be linked assert next(iter(network.links.values())).is_up @@ -192,8 +190,8 @@ def client_switch_server() -> Tuple[Computer, Switch, Server]: switch = Switch(hostname="switch", start_up_duration=0) switch.power_on() - network.connect(endpoint_a=computer.ethernet_port[1], endpoint_b=switch.switch_ports[1]) - network.connect(endpoint_a=server.ethernet_port[1], endpoint_b=switch.switch_ports[2]) + network.connect(endpoint_a=computer.network_interface[1], endpoint_b=switch.switch_ports[1]) + network.connect(endpoint_a=server.network_interface[1], endpoint_b=switch.switch_ports[2]) assert all(link.is_up for link in network.links.values()) @@ -219,18 +217,33 @@ def example_network() -> Network: network = Network() # Router 1 - router_1 = Router(hostname="router_1", num_ports=5, operating_state=NodeOperatingState.ON) + router_1 = Router( + hostname="router_1", + start_up_duration=0 + ) + router_1.power_on() router_1.configure_port(port=1, ip_address="192.168.1.1", subnet_mask="255.255.255.0") router_1.configure_port(port=2, ip_address="192.168.10.1", subnet_mask="255.255.255.0") # Switch 1 - switch_1 = Switch(hostname="switch_1", num_ports=8, operating_state=NodeOperatingState.ON) - network.connect(endpoint_a=router_1.ethernet_ports[1], endpoint_b=switch_1.switch_ports[8]) + switch_1 = Switch( + hostname="switch_1", + num_ports=8, + start_up_duration=0 + ) + switch_1.power_on() + + network.connect(endpoint_a=router_1.network_interface[1], endpoint_b=switch_1.switch_ports[8]) router_1.enable_port(1) # Switch 2 - switch_2 = Switch(hostname="switch_2", num_ports=8, operating_state=NodeOperatingState.ON) - network.connect(endpoint_a=router_1.ethernet_ports[2], endpoint_b=switch_2.switch_ports[8]) + switch_2 = Switch( + hostname="switch_2", + num_ports=8, + start_up_duration=0 + ) + switch_2.power_on() + network.connect(endpoint_a=router_1.network_interface[2], endpoint_b=switch_2.switch_ports[8]) router_1.enable_port(2) # Client 1 @@ -239,9 +252,10 @@ def example_network() -> Network: ip_address="192.168.10.21", subnet_mask="255.255.255.0", default_gateway="192.168.10.1", - operating_state=NodeOperatingState.ON, + start_up_duration=0 ) - network.connect(endpoint_b=client_1.ethernet_port[1], endpoint_a=switch_2.switch_ports[1]) + client_1.power_on() + network.connect(endpoint_b=client_1.network_interface[1], endpoint_a=switch_2.switch_ports[1]) # Client 2 client_2 = Computer( @@ -249,32 +263,37 @@ def example_network() -> Network: ip_address="192.168.10.22", subnet_mask="255.255.255.0", default_gateway="192.168.10.1", - operating_state=NodeOperatingState.ON, + start_up_duration=0 ) - network.connect(endpoint_b=client_2.ethernet_port[1], endpoint_a=switch_2.switch_ports[2]) + client_2.power_on() + network.connect(endpoint_b=client_2.network_interface[1], endpoint_a=switch_2.switch_ports[2]) - # Domain Controller + # Server 1 server_1 = Server( hostname="server_1", ip_address="192.168.1.10", subnet_mask="255.255.255.0", default_gateway="192.168.1.1", - operating_state=NodeOperatingState.ON, + start_up_duration=0 ) + server_1.power_on() + network.connect(endpoint_b=server_1.network_interface[1], endpoint_a=switch_1.switch_ports[1]) - network.connect(endpoint_b=server_1.ethernet_port[1], endpoint_a=switch_1.switch_ports[1]) - - # Database Server + # DServer 2 server_2 = Server( hostname="server_2", ip_address="192.168.1.14", subnet_mask="255.255.255.0", default_gateway="192.168.1.1", - operating_state=NodeOperatingState.ON, + start_up_duration=0 ) - network.connect(endpoint_b=server_2.ethernet_port[1], endpoint_a=switch_1.switch_ports[2]) + server_2.power_on() + network.connect(endpoint_b=server_2.network_interface[1], endpoint_a=switch_1.switch_ports[2]) 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) + assert all(link.is_up for link in network.links.values()) + + return network diff --git a/tests/e2e_integration_tests/test_uc2_data_manipulation_scenario.py b/tests/e2e_integration_tests/test_uc2_data_manipulation_scenario.py index 992ed533..b68a887e 100644 --- a/tests/e2e_integration_tests/test_uc2_data_manipulation_scenario.py +++ b/tests/e2e_integration_tests/test_uc2_data_manipulation_scenario.py @@ -1,5 +1,5 @@ -from primaite.simulator.network.hardware.nodes.computer import Computer -from primaite.simulator.network.hardware.nodes.server import Server +from primaite.simulator.network.hardware.nodes.host.computer import Computer +from primaite.simulator.network.hardware.nodes.host.server import Server from primaite.simulator.system.applications.database_client import DatabaseClient from primaite.simulator.system.applications.red_applications.data_manipulation_bot import DataManipulationBot from primaite.simulator.system.services.database.database_service import DatabaseService diff --git a/tests/integration_tests/component_creation/test_action_integration.py b/tests/integration_tests/component_creation/test_action_integration.py index a2be923b..7d3945a6 100644 --- a/tests/integration_tests/component_creation/test_action_integration.py +++ b/tests/integration_tests/component_creation/test_action_integration.py @@ -1,9 +1,7 @@ -import pytest - from primaite.simulator.core import RequestType -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 +from primaite.simulator.network.hardware.nodes.host.computer import Computer +from primaite.simulator.network.hardware.nodes.host.server import Server +from primaite.simulator.network.hardware.nodes.network.switch import Switch from primaite.simulator.sim_container import Simulation from primaite.simulator.system.services.database.database_service import DatabaseService @@ -27,9 +25,9 @@ def test_passing_actions_down(monkeypatch) -> None: downloads_folder = pc1.file_system.create_folder("downloads") pc1.file_system.create_file("bermuda_triangle.png", folder_name="downloads") - sim.network.connect(pc1.ethernet_port[1], s1.switch_ports[1]) - sim.network.connect(pc2.ethernet_port[1], s1.switch_ports[2]) - sim.network.connect(s1.switch_ports[3], srv.ethernet_port[1]) + sim.network.connect(pc1.network_interface[1], s1.switch_ports[1]) + sim.network.connect(pc2.network_interface[1], s1.switch_ports[2]) + sim.network.connect(s1.switch_ports[3], srv.network_interface[1]) # call this method to make sure no errors occur. sim._request_manager.get_request_types_recursively() diff --git a/tests/integration_tests/game_layer/test_observations.py b/tests/integration_tests/game_layer/test_observations.py index 07f3d25c..d1301759 100644 --- a/tests/integration_tests/game_layer/test_observations.py +++ b/tests/integration_tests/game_layer/test_observations.py @@ -1,7 +1,7 @@ from gymnasium import spaces from primaite.game.agent.observations import FileObservation -from primaite.simulator.network.hardware.nodes.computer import Computer +from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.sim_container import Simulation diff --git a/tests/integration_tests/network/test_broadcast.py b/tests/integration_tests/network/test_broadcast.py index 5fb0917e..2dd9f7b8 100644 --- a/tests/integration_tests/network/test_broadcast.py +++ b/tests/integration_tests/network/test_broadcast.py @@ -4,9 +4,9 @@ from typing import Any, Dict, List, Tuple import pytest from primaite.simulator.network.container import Network -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 +from primaite.simulator.network.hardware.nodes.host.computer import Computer +from primaite.simulator.network.hardware.nodes.host.server import Server +from primaite.simulator.network.hardware.nodes.network.switch import Switch from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.system.applications.application import Application @@ -111,9 +111,9 @@ def broadcast_network() -> Network: switch_1 = Switch(hostname="switch_1", num_ports=6, start_up_duration=0) switch_1.power_on() - network.connect(endpoint_a=client_1.ethernet_port[1], endpoint_b=switch_1.switch_ports[1]) - network.connect(endpoint_a=client_2.ethernet_port[1], endpoint_b=switch_1.switch_ports[2]) - network.connect(endpoint_a=server_1.ethernet_port[1], endpoint_b=switch_1.switch_ports[3]) + network.connect(endpoint_a=client_1.network_interface[1], endpoint_b=switch_1.switch_ports[1]) + network.connect(endpoint_a=client_2.network_interface[1], endpoint_b=switch_1.switch_ports[2]) + network.connect(endpoint_a=server_1.network_interface[1], endpoint_b=switch_1.switch_ports[3]) return network diff --git a/tests/integration_tests/network/test_frame_transmission.py b/tests/integration_tests/network/test_frame_transmission.py index 527e4b4c..7beea643 100644 --- a/tests/integration_tests/network/test_frame_transmission.py +++ b/tests/integration_tests/network/test_frame_transmission.py @@ -1,7 +1,7 @@ from primaite.simulator.network.container import Network -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 +from primaite.simulator.network.hardware.nodes.host.computer import Computer +from primaite.simulator.network.hardware.nodes.host.server import Server +from primaite.simulator.network.hardware.nodes.network.switch import Switch @@ -30,8 +30,8 @@ def test_node_to_node_ping(): switch_1 = Switch(hostname="switch_1", start_up_duration=0) switch_1.power_on() - network.connect(endpoint_a=client_1.ethernet_port[1], endpoint_b=switch_1.switch_ports[1]) - network.connect(endpoint_a=server_1.ethernet_port[1], endpoint_b=switch_1.switch_ports[2]) + network.connect(endpoint_a=client_1.network_interface[1], endpoint_b=switch_1.switch_ports[1]) + network.connect(endpoint_a=server_1.network_interface[1], endpoint_b=switch_1.switch_ports[2]) assert client_1.ping("192.168.1.11") diff --git a/tests/integration_tests/network/test_network_creation.py b/tests/integration_tests/network/test_network_creation.py index 0af44dbb..d9792675 100644 --- a/tests/integration_tests/network/test_network_creation.py +++ b/tests/integration_tests/network/test_network_creation.py @@ -1,10 +1,7 @@ -import pytest - from primaite.simulator.network.container import Network -from primaite.simulator.network.hardware.base import NIC, Node -from primaite.simulator.network.hardware.nodes.computer import Computer -from primaite.simulator.network.hardware.nodes.server import Server -from primaite.simulator.network.networks import client_server_routed +from primaite.simulator.network.hardware.base import Node +from primaite.simulator.network.hardware.nodes.host.computer import Computer +from primaite.simulator.network.hardware.nodes.host.server import Server def test_network(example_network): @@ -14,16 +11,16 @@ def test_network(example_network): server_1: Server = network.get_node_by_hostname("server_1") server_2: Server = network.get_node_by_hostname("server_2") - assert client_1.ping(client_2.ethernet_port[1].ip_address) - assert client_2.ping(client_1.ethernet_port[1].ip_address) + assert client_1.ping(client_2.network_interface[1].ip_address) + assert client_2.ping(client_1.network_interface[1].ip_address) - assert server_1.ping(server_2.ethernet_port[1].ip_address) - assert server_2.ping(server_1.ethernet_port[1].ip_address) + assert server_1.ping(server_2.network_interface[1].ip_address) + assert server_2.ping(server_1.network_interface[1].ip_address) - assert client_1.ping(server_1.ethernet_port[1].ip_address) - assert client_2.ping(server_1.ethernet_port[1].ip_address) - assert client_1.ping(server_2.ethernet_port[1].ip_address) - assert client_2.ping(server_2.ethernet_port[1].ip_address) + assert client_1.ping(server_1.network_interface[1].ip_address) + assert client_2.ping(server_1.network_interface[1].ip_address) + assert client_1.ping(server_2.network_interface[1].ip_address) + assert client_2.ping(server_2.network_interface[1].ip_address) def test_adding_removing_nodes(): @@ -71,7 +68,7 @@ def test_connecting_nodes(): net.add_node(n1) net.add_node(n2) - net.connect(n1.nics[n1_nic.uuid], n2.nics[n2_nic.uuid], bandwidth=30) + net.connect(n1.network_interfaces[n1_nic.uuid], n2.network_interfaces[n2_nic.uuid], bandwidth=30) assert len(net.links) == 1 link = list(net.links.values())[0] @@ -89,7 +86,7 @@ def test_connecting_node_to_itself(): net.add_node(node) - net.connect(node.nics[nic1.uuid], node.nics[nic2.uuid], bandwidth=30) + net.connect(node.network_interfaces[nic1.uuid], node.network_interfaces[nic2.uuid], bandwidth=30) assert node in net assert nic1._connected_link is None @@ -110,7 +107,7 @@ def test_disconnecting_nodes(): n2.connect_nic(n2_nic) net.add_node(n2) - net.connect(n1.nics[n1_nic.uuid], n2.nics[n2_nic.uuid], bandwidth=30) + net.connect(n1.network_interfaces[n1_nic.uuid], n2.network_interfaces[n2_nic.uuid], bandwidth=30) assert len(net.links) == 1 link = list(net.links.values())[0] diff --git a/tests/integration_tests/network/test_routing.py b/tests/integration_tests/network/test_routing.py index 042debca..02524eab 100644 --- a/tests/integration_tests/network/test_routing.py +++ b/tests/integration_tests/network/test_routing.py @@ -1,12 +1,10 @@ -from ipaddress import IPv4Address from typing import Tuple import pytest from primaite.simulator.network.container import Network -from primaite.simulator.network.hardware.base import Link, NIC, Node, NodeOperatingState -from primaite.simulator.network.hardware.nodes.computer import Computer -from primaite.simulator.network.hardware.nodes.router import ACLAction, Router +from primaite.simulator.network.hardware.nodes.host.computer import Computer +from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.system.services.ntp.ntp_client import NTPClient @@ -14,28 +12,37 @@ from primaite.simulator.system.services.ntp.ntp_server import NTPServer @pytest.fixture(scope="function") -def pc_a_pc_b_router_1() -> Tuple[Node, Node, Router]: - pc_a = Node(hostname="pc_a", default_gateway="192.168.0.1", operating_state=NodeOperatingState.ON) - nic_a = NIC(ip_address="192.168.0.10", subnet_mask="255.255.255.0") - pc_a.connect_nic(nic_a) +def pc_a_pc_b_router_1() -> Tuple[Computer, Computer, Router]: + network = Network() + pc_a = Computer( + hostname="pc_a", + ip_address="192.168.0.10", + subnet_mask="255.255.255.0", + default_gateway="192.168.0.1", + start_up_duration=0 + ) + pc_a.power_on() - pc_b = Node(hostname="pc_b", default_gateway="192.168.1.1", operating_state=NodeOperatingState.ON) - nic_b = NIC(ip_address="192.168.1.10", subnet_mask="255.255.255.0") - pc_b.connect_nic(nic_b) + pc_b = Computer( + hostname="pc_b", + ip_address="192.168.1.10", + subnet_mask="255.255.255.0", + default_gateway="192.168.1.1", + start_up_duration=0 + ) + pc_b.power_on() - router_1 = Router(hostname="router_1", operating_state=NodeOperatingState.ON) + router_1 = Router(hostname="router_1", start_up_duration=0) + router_1.power_on() router_1.configure_port(1, "192.168.0.1", "255.255.255.0") router_1.configure_port(2, "192.168.1.1", "255.255.255.0") - Link(endpoint_a=nic_a, endpoint_b=router_1.ethernet_ports[1]) - Link(endpoint_a=nic_b, endpoint_b=router_1.ethernet_ports[2]) + network.connect(endpoint_a=pc_a.network_interface[1], endpoint_b=router_1.network_interface[1]) + network.connect(endpoint_a=pc_b.network_interface[1], endpoint_b=router_1.network_interface[2]) router_1.enable_port(1) router_1.enable_port(2) - 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) return pc_a, pc_b, router_1 @@ -61,7 +68,7 @@ def multi_hop_network() -> Network: # Configure the connection between PC A and Router 1 port 2 router_1.configure_port(2, "192.168.0.1", "255.255.255.0") - network.connect(pc_a.ethernet_port[1], router_1.ethernet_ports[2]) + network.connect(pc_a.network_interface[1], router_1.network_interface[2]) router_1.enable_port(2) # Configure Router 1 ACLs @@ -86,17 +93,15 @@ def multi_hop_network() -> Network: # Configure the connection between PC B and Router 2 port 2 router_2.configure_port(2, "192.168.2.1", "255.255.255.0") - network.connect(pc_b.ethernet_port[1], router_2.ethernet_ports[2]) + network.connect(pc_b.network_interface[1], router_2.network_interface[2]) router_2.enable_port(2) # Configure Router 2 ACLs - router_2.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.ARP, dst_port=Port.ARP, position=22) - router_2.acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol.ICMP, position=23) # Configure the connection between Router 1 port 1 and Router 2 port 1 router_2.configure_port(1, "192.168.1.2", "255.255.255.252") router_1.configure_port(1, "192.168.1.1", "255.255.255.252") - network.connect(router_1.ethernet_ports[1], router_2.ethernet_ports[1]) + network.connect(router_1.network_interface[1], router_2.network_interface[1]) router_1.enable_port(1) router_2.enable_port(1) return network @@ -117,14 +122,14 @@ def test_ping_other_router_port(pc_a_pc_b_router_1): def test_host_on_other_subnet(pc_a_pc_b_router_1): pc_a, pc_b, router_1 = pc_a_pc_b_router_1 - assert pc_a.ping("192.168.1.10") + assert pc_a.ping(pc_b.network_interface[1].ip_address) def test_no_route_no_ping(multi_hop_network): pc_a = multi_hop_network.get_node_by_hostname("pc_a") pc_b = multi_hop_network.get_node_by_hostname("pc_b") - assert not pc_a.ping(pc_b.ethernet_port[1].ip_address) + assert not pc_a.ping(pc_b.network_interface[1].ip_address) def test_with_routes_can_ping(multi_hop_network): @@ -144,7 +149,7 @@ def test_with_routes_can_ping(multi_hop_network): address="192.168.0.2", subnet_mask="255.255.255.0", next_hop_ip_address="192.168.1.1" ) - assert pc_a.ping(pc_b.ethernet_port[1].ip_address) + assert pc_a.ping(pc_b.network_interface[1].ip_address) def test_routing_services(multi_hop_network): @@ -159,7 +164,7 @@ def test_routing_services(multi_hop_network): pc_b.software_manager.install(NTPServer) pc_b.software_manager.software["NTPServer"].start() - ntp_client.configure(ntp_server_ip_address=pc_b.ethernet_port[1].ip_address) + ntp_client.configure(ntp_server_ip_address=pc_b.network_interface[1].ip_address) router_1: Router = multi_hop_network.get_node_by_hostname("router_1") # noqa router_2: Router = multi_hop_network.get_node_by_hostname("router_2") # noqa diff --git a/tests/integration_tests/network/test_switched_network.py b/tests/integration_tests/network/test_switched_network.py index 8a2bd0a2..98f36df6 100644 --- a/tests/integration_tests/network/test_switched_network.py +++ b/tests/integration_tests/network/test_switched_network.py @@ -1,12 +1,5 @@ -from primaite.simulator.network.container import Network -from primaite.simulator.network.hardware.base import Link, NodeOperatingState -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(client_switch_server): """Tests a node can ping another node via the switch.""" computer, switch, server = client_switch_server - assert computer.ping(server.ethernet_port[1].ip_address) + assert computer.ping(server.network_interface[1].ip_address) diff --git a/tests/integration_tests/system/red_applications/test_dos_bot_and_server.py b/tests/integration_tests/system/red_applications/test_dos_bot_and_server.py index fb768127..ecf2c5ae 100644 --- a/tests/integration_tests/system/red_applications/test_dos_bot_and_server.py +++ b/tests/integration_tests/system/red_applications/test_dos_bot_and_server.py @@ -4,9 +4,9 @@ from typing import Tuple import pytest from primaite.simulator.network.container import Network -from primaite.simulator.network.hardware.nodes.computer import Computer -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.host.computer import Computer +from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router +from primaite.simulator.network.hardware.nodes.host.server import Server from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.system.applications.application import ApplicationOperatingState from primaite.simulator.system.applications.database_client import DatabaseClient @@ -24,7 +24,7 @@ def dos_bot_and_db_server(client_server) -> Tuple[DoSBot, Computer, DatabaseServ dos_bot: DoSBot = computer.software_manager.software.get("DoSBot") dos_bot.configure( - target_ip_address=IPv4Address(server.nics.get(next(iter(server.nics))).ip_address), + target_ip_address=IPv4Address(server.network_interfaces.get(next(iter(server.network_interfaces))).ip_address), target_port=Port.POSTGRES_SERVER, ) @@ -54,7 +54,7 @@ def dos_bot_db_server_green_client(example_network) -> Network: dos_bot: DoSBot = client_1.software_manager.software.get("DoSBot") dos_bot.configure( - target_ip_address=IPv4Address(server.nics.get(next(iter(server.nics))).ip_address), + target_ip_address=IPv4Address(server.network_interfaces.get(next(iter(server.network_interfaces))).ip_address), target_port=Port.POSTGRES_SERVER, ) diff --git a/tests/integration_tests/system/test_application_on_node.py b/tests/integration_tests/system/test_application_on_node.py index 60497f22..143b2b04 100644 --- a/tests/integration_tests/system/test_application_on_node.py +++ b/tests/integration_tests/system/test_application_on_node.py @@ -3,7 +3,7 @@ from typing import Tuple import pytest from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState -from primaite.simulator.network.hardware.nodes.computer import Computer +from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.system.applications.application import Application, ApplicationOperatingState diff --git a/tests/integration_tests/system/test_database_on_node.py b/tests/integration_tests/system/test_database_on_node.py index daa125ca..df47d8ad 100644 --- a/tests/integration_tests/system/test_database_on_node.py +++ b/tests/integration_tests/system/test_database_on_node.py @@ -4,7 +4,7 @@ from typing import Tuple import pytest from primaite.simulator.network.hardware.base import Link, NIC, Node, NodeOperatingState -from primaite.simulator.network.hardware.nodes.server import Server +from primaite.simulator.network.hardware.nodes.host.server import Server from primaite.simulator.system.applications.database_client import DatabaseClient from primaite.simulator.system.services.database.database_service import DatabaseService from primaite.simulator.system.services.ftp.ftp_server import FTPServer diff --git a/tests/integration_tests/system/test_dns_client_server.py b/tests/integration_tests/system/test_dns_client_server.py index a54bf23f..18988043 100644 --- a/tests/integration_tests/system/test_dns_client_server.py +++ b/tests/integration_tests/system/test_dns_client_server.py @@ -4,8 +4,8 @@ from typing import Tuple import pytest from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState -from primaite.simulator.network.hardware.nodes.computer import Computer -from primaite.simulator.network.hardware.nodes.server import Server +from primaite.simulator.network.hardware.nodes.host.computer import Computer +from primaite.simulator.network.hardware.nodes.host.server import Server from primaite.simulator.system.services.dns.dns_client import DNSClient from primaite.simulator.system.services.dns.dns_server import DNSServer from primaite.simulator.system.services.service import ServiceOperatingState @@ -20,7 +20,7 @@ def dns_client_and_dns_server(client_server) -> Tuple[DNSClient, Computer, DNSSe dns_client: DNSClient = computer.software_manager.software.get("DNSClient") dns_client.start() # set server as DNS Server - dns_client.dns_server = IPv4Address(server.nics.get(next(iter(server.nics))).ip_address) + dns_client.dns_server = IPv4Address(server.network_interfaces.get(next(iter(server.network_interfaces))).ip_address) # Install DNS Server on server server.software_manager.install(DNSServer) @@ -28,7 +28,7 @@ def dns_client_and_dns_server(client_server) -> Tuple[DNSClient, Computer, DNSSe dns_server.start() # register arcd.com as a domain dns_server.dns_register( - domain_name="arcd.com", domain_ip_address=IPv4Address(server.nics.get(next(iter(server.nics))).ip_address) + domain_name="arcd.com", domain_ip_address=IPv4Address(server.network_interfaces.get(next(iter(server.network_interfaces))).ip_address) ) return dns_client, computer, dns_server, server diff --git a/tests/integration_tests/system/test_ftp_client_server.py b/tests/integration_tests/system/test_ftp_client_server.py index 1a6a8f41..6b46e302 100644 --- a/tests/integration_tests/system/test_ftp_client_server.py +++ b/tests/integration_tests/system/test_ftp_client_server.py @@ -1,10 +1,9 @@ -from ipaddress import IPv4Address from typing import Tuple import pytest -from primaite.simulator.network.hardware.nodes.computer import Computer -from primaite.simulator.network.hardware.nodes.server import Server +from primaite.simulator.network.hardware.nodes.host.computer import Computer +from primaite.simulator.network.hardware.nodes.host.server import Server from primaite.simulator.system.services.ftp.ftp_client import FTPClient from primaite.simulator.system.services.ftp.ftp_server import FTPServer from primaite.simulator.system.services.service import ServiceOperatingState @@ -44,7 +43,7 @@ def test_ftp_client_store_file_in_server(ftp_client_and_ftp_server): src_file_name="test_file.txt", dest_folder_name="client_1_backup", dest_file_name="test_file.txt", - dest_ip_address=server.nics.get(next(iter(server.nics))).ip_address, + dest_ip_address=server.network_interfaces.get(next(iter(server.network_interfaces))).ip_address, ) assert ftp_server.file_system.get_file(folder_name="client_1_backup", file_name="test_file.txt") @@ -67,7 +66,7 @@ def test_ftp_client_retrieve_file_from_server(ftp_client_and_ftp_server): src_file_name="test_file.txt", dest_folder_name="downloads", dest_file_name="test_file.txt", - dest_ip_address=server.nics.get(next(iter(server.nics))).ip_address, + dest_ip_address=server.network_interfaces.get(next(iter(server.network_interfaces))).ip_address, ) # client should have retrieved the file @@ -98,7 +97,7 @@ def test_ftp_client_tries_to_connect_to_offline_server(ftp_client_and_ftp_server src_file_name="test_file.txt", dest_folder_name="downloads", dest_file_name="test_file.txt", - dest_ip_address=server.nics.get(next(iter(server.nics))).ip_address, + dest_ip_address=server.network_interfaces.get(next(iter(server.network_interfaces))).ip_address, ) is False ) diff --git a/tests/integration_tests/system/test_ntp_client_server.py b/tests/integration_tests/system/test_ntp_client_server.py index b7839479..92133d50 100644 --- a/tests/integration_tests/system/test_ntp_client_server.py +++ b/tests/integration_tests/system/test_ntp_client_server.py @@ -4,10 +4,8 @@ from typing import Tuple import pytest -from primaite.simulator.network.container import Network -from primaite.simulator.network.hardware.nodes.computer import Computer -from primaite.simulator.network.hardware.nodes.server import Server -from primaite.simulator.network.protocols.ntp import NTPPacket +from primaite.simulator.network.hardware.nodes.host.computer import Computer +from primaite.simulator.network.hardware.nodes.host.server import Server from primaite.simulator.system.services.ntp.ntp_client import NTPClient from primaite.simulator.system.services.ntp.ntp_server import NTPServer from primaite.simulator.system.services.service import ServiceOperatingState diff --git a/tests/integration_tests/system/test_service_on_node.py b/tests/integration_tests/system/test_service_on_node.py index 9b0084bd..12fed578 100644 --- a/tests/integration_tests/system/test_service_on_node.py +++ b/tests/integration_tests/system/test_service_on_node.py @@ -3,8 +3,8 @@ from typing import Tuple import pytest from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState -from primaite.simulator.network.hardware.nodes.computer import Computer -from primaite.simulator.network.hardware.nodes.server import Server +from primaite.simulator.network.hardware.nodes.host.computer import Computer +from primaite.simulator.network.hardware.nodes.host.server import Server from primaite.simulator.system.services.service import Service, ServiceOperatingState diff --git a/tests/integration_tests/system/test_web_client_server.py b/tests/integration_tests/system/test_web_client_server.py index b3d2e891..c809f954 100644 --- a/tests/integration_tests/system/test_web_client_server.py +++ b/tests/integration_tests/system/test_web_client_server.py @@ -3,8 +3,8 @@ from typing import Tuple import pytest from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState -from primaite.simulator.network.hardware.nodes.computer import Computer -from primaite.simulator.network.hardware.nodes.server import Server +from primaite.simulator.network.hardware.nodes.host.computer import Computer +from primaite.simulator.network.hardware.nodes.host.server import Server from primaite.simulator.network.protocols.http import HttpStatusCode from primaite.simulator.system.applications.application import ApplicationOperatingState from primaite.simulator.system.applications.web_browser import WebBrowser @@ -26,7 +26,7 @@ def web_client_and_web_server(client_server) -> Tuple[WebBrowser, Computer, WebS computer.software_manager.install(DNSClient) dns_client: DNSClient = computer.software_manager.software.get("DNSClient") # set dns server - dns_client.dns_server = server.nics[next(iter(server.nics))].ip_address + dns_client.dns_server = server.network_interfaces[next(iter(server.network_interfaces))].ip_address # Install Web Server service on server server.software_manager.install(WebServer) @@ -37,7 +37,7 @@ def web_client_and_web_server(client_server) -> Tuple[WebBrowser, Computer, WebS server.software_manager.install(DNSServer) dns_server: DNSServer = server.software_manager.software.get("DNSServer") # register arcd.com to DNS - dns_server.dns_register(domain_name="arcd.com", domain_ip_address=server.nics[next(iter(server.nics))].ip_address) + dns_server.dns_register(domain_name="arcd.com", domain_ip_address=server.network_interfaces[next(iter(server.network_interfaces))].ip_address) return web_browser, computer, web_server_service, server @@ -46,7 +46,7 @@ def test_web_page_get_users_page_request_with_domain_name(web_client_and_web_ser """Test to see if the client can handle requests with domain names""" web_browser_app, computer, web_server_service, server = web_client_and_web_server - web_server_ip = server.nics.get(next(iter(server.nics))).ip_address + web_server_ip = server.network_interfaces.get(next(iter(server.network_interfaces))).ip_address web_browser_app.target_url = f"http://arcd.com/" assert web_browser_app.operating_state == ApplicationOperatingState.RUNNING @@ -61,7 +61,7 @@ def test_web_page_get_users_page_request_with_ip_address(web_client_and_web_serv """Test to see if the client can handle requests that use ip_address.""" web_browser_app, computer, web_server_service, server = web_client_and_web_server - web_server_ip = server.nics.get(next(iter(server.nics))).ip_address + web_server_ip = server.network_interfaces.get(next(iter(server.network_interfaces))).ip_address web_browser_app.target_url = f"http://{web_server_ip}/" assert web_browser_app.operating_state == ApplicationOperatingState.RUNNING @@ -76,7 +76,7 @@ def test_web_page_request_from_shut_down_server(web_client_and_web_server): """Test to see that the web server does not respond when the server is off.""" web_browser_app, computer, web_server_service, server = web_client_and_web_server - web_server_ip = server.nics.get(next(iter(server.nics))).ip_address + web_server_ip = server.network_interfaces.get(next(iter(server.network_interfaces))).ip_address web_browser_app.target_url = f"http://arcd.com/" assert web_browser_app.operating_state == ApplicationOperatingState.RUNNING diff --git a/tests/integration_tests/system/test_web_client_server_and_database.py b/tests/integration_tests/system/test_web_client_server_and_database.py index a4ef3d52..efb29f41 100644 --- a/tests/integration_tests/system/test_web_client_server_and_database.py +++ b/tests/integration_tests/system/test_web_client_server_and_database.py @@ -4,10 +4,9 @@ from typing import Tuple import pytest from primaite.simulator.network.hardware.base import Link -from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState -from primaite.simulator.network.hardware.nodes.computer import Computer -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.host.computer import Computer +from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router +from primaite.simulator.network.hardware.nodes.host.server import Server from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.system.applications.database_client import DatabaseClient from primaite.simulator.system.applications.web_browser import WebBrowser @@ -44,9 +43,9 @@ def web_client_web_server_database(example_network) -> Tuple[Computer, Server, S db_server = example_network.get_node_by_hostname("server_2") # Get the NICs - computer_nic = computer.nics[next(iter(computer.nics))] - server_nic = web_server.nics[next(iter(web_server.nics))] - db_server_nic = db_server.nics[next(iter(db_server.nics))] + computer_nic = computer.network_interfaces[next(iter(computer.network_interfaces))] + server_nic = web_server.network_interfaces[next(iter(web_server.network_interfaces))] + db_server_nic = db_server.network_interfaces[next(iter(db_server.network_interfaces))] # Connect Computer and Server link_computer_server = Link(endpoint_a=computer_nic, endpoint_b=server_nic) @@ -74,7 +73,7 @@ def web_client_web_server_database(example_network) -> Tuple[Computer, Server, S computer.software_manager.install(DNSClient) dns_client: DNSClient = computer.software_manager.software.get("DNSClient") # set dns server - dns_client.dns_server = web_server.nics[next(iter(web_server.nics))].ip_address + dns_client.dns_server = web_server.network_interfaces[next(iter(web_server.network_interfaces))].ip_address # Install Web Server service on web server web_server.software_manager.install(WebServer) @@ -86,7 +85,7 @@ def web_client_web_server_database(example_network) -> Tuple[Computer, Server, S dns_server: DNSServer = web_server.software_manager.software.get("DNSServer") # register arcd.com to DNS dns_server.dns_register( - domain_name="arcd.com", domain_ip_address=web_server.nics[next(iter(web_server.nics))].ip_address + domain_name="arcd.com", domain_ip_address=web_server.network_interfaces[next(iter(web_server.network_interfaces))].ip_address ) # Install DatabaseClient service on web server diff --git a/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_acl.py b/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_acl.py index 554cba38..428f370c 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_acl.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_acl.py @@ -1,6 +1,6 @@ from ipaddress import IPv4Address -from primaite.simulator.network.hardware.nodes.router import ACLAction, Router +from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port diff --git a/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_switch.py b/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_switch.py index d2d0e52c..a0f6619c 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_switch.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_switch.py @@ -1,7 +1,7 @@ import pytest from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState -from primaite.simulator.network.hardware.nodes.switch import Switch +from primaite.simulator.network.hardware.nodes.network.switch import Switch @pytest.fixture(scope="function") diff --git a/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_nic.py b/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_nic.py index 1bf2cdbb..90b54b78 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_nic.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_nic.py @@ -29,21 +29,21 @@ def test_invalid_oui_mac_address(): def test_nic_ip_address_type_conversion(): """Tests NIC IP and gateway address is converted to IPv4Address is originally a string.""" - nic = NIC( + network_interface = NIC( ip_address="192.168.1.2", subnet_mask="255.255.255.0", ) - assert isinstance(nic.ip_address, IPv4Address) + assert isinstance(network_interface.ip_address, IPv4Address) def test_nic_deserialize(): """Tests NIC serialization and deserialization.""" - nic = NIC( + network_interface = NIC( ip_address="192.168.1.2", subnet_mask="255.255.255.0", ) - nic_json = nic.model_dump_json() + nic_json = network_interface.model_dump_json() deserialized_nic = NIC.model_validate_json(nic_json) assert nic_json == deserialized_nic.model_dump_json() diff --git a/tests/unit_tests/_primaite/_simulator/_network/test_container.py b/tests/unit_tests/_primaite/_simulator/_network/test_container.py index 7667a59f..b56253fb 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/test_container.py +++ b/tests/unit_tests/_primaite/_simulator/_network/test_container.py @@ -5,9 +5,7 @@ import pytest from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.base import Link, Node from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState -from primaite.simulator.network.hardware.nodes.computer import Computer -from primaite.simulator.system.applications.database_client import DatabaseClient -from primaite.simulator.system.services.database.database_service import DatabaseService +from primaite.simulator.network.hardware.nodes.host.computer import Computer def filter_keys_nested_item(data, keys): diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_dos_bot.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_dos_bot.py index 71489171..eafa6359 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_dos_bot.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_dos_bot.py @@ -3,7 +3,7 @@ from ipaddress import IPv4Address import pytest from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState -from primaite.simulator.network.hardware.nodes.computer import Computer +from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.system.applications.application import ApplicationOperatingState from primaite.simulator.system.applications.red_applications.dos_bot import DoSAttackStage, DoSBot diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_database_client.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_database_client.py index 204b356f..6fec4555 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_database_client.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_database_client.py @@ -1,11 +1,11 @@ from ipaddress import IPv4Address -from typing import Tuple, Union +from typing import Tuple from uuid import uuid4 import pytest from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState -from primaite.simulator.network.hardware.nodes.computer import Computer +from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.system.applications.application import ApplicationOperatingState from primaite.simulator.system.applications.database_client import DatabaseClient diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_web_browser.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_web_browser.py index dc8f7419..9dc7a52e 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_web_browser.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_web_browser.py @@ -1,9 +1,7 @@ -from typing import Tuple - import pytest from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState -from primaite.simulator.network.hardware.nodes.computer import Computer +from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.protocols.http import HttpResponsePacket, HttpStatusCode from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_client.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_client.py index 2bcb512d..97c1cf4e 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_client.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_client.py @@ -4,7 +4,7 @@ import pytest from primaite.simulator.network.hardware.base import Node from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState -from primaite.simulator.network.hardware.nodes.computer import Computer +from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.protocols.dns import DNSPacket, DNSReply, DNSRequest from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_server.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_server.py index eb042c92..5f5fdcba 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_server.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_server.py @@ -4,7 +4,7 @@ import pytest from primaite.simulator.network.hardware.base import Node from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState -from primaite.simulator.network.hardware.nodes.server import Server +from primaite.simulator.network.hardware.nodes.host.server import Server from primaite.simulator.network.protocols.dns import DNSPacket, DNSRequest from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py index 941a465e..5d900fff 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py @@ -5,7 +5,7 @@ import pytest from primaite.simulator.file_system.file_system_item_abc import FileSystemItemHealthStatus from primaite.simulator.network.hardware.base import Node from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState -from primaite.simulator.network.hardware.nodes.computer import Computer +from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.protocols.ftp import FTPCommand, FTPPacket, FTPStatusCode from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_server.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_server.py index 137e74d0..a4fcdff7 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_server.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_server.py @@ -3,7 +3,7 @@ import pytest from primaite.simulator.file_system.file_system_item_abc import FileSystemItemHealthStatus from primaite.simulator.network.hardware.base import Node from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState -from primaite.simulator.network.hardware.nodes.server import Server +from primaite.simulator.network.hardware.nodes.host.server import Server from primaite.simulator.network.protocols.ftp import FTPCommand, FTPPacket, FTPStatusCode from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_web_server.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_web_server.py index 64277356..2e645435 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_web_server.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_web_server.py @@ -1,7 +1,7 @@ import pytest from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState -from primaite.simulator.network.hardware.nodes.server import Server +from primaite.simulator.network.hardware.nodes.host.server import Server from primaite.simulator.network.protocols.http import ( HttpRequestMethod, HttpRequestPacket,