#2248 - Final run over all the docstrings after running pre-commit. All tests now working. Updated CHANGELOG.md.
This commit is contained in:
@@ -494,7 +494,9 @@ 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["NETWORK_INTERFACES"] = {i + 1: network_interface.observe(state) for i, network_interface in enumerate(self.network_interfaces)}
|
||||
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 +510,9 @@ 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),
|
||||
"NETWORK_INTERFACES": spaces.Dict({i + 1: network_interface.space for i, network_interface in enumerate(self.network_interfaces)}),
|
||||
"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)
|
||||
|
||||
@@ -14,8 +14,8 @@ from primaite.session.io import SessionIO, SessionIOSettings
|
||||
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.router import Router
|
||||
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
|
||||
|
||||
@@ -9,8 +9,8 @@ from primaite import getLogger
|
||||
from primaite.simulator.core import RequestManager, RequestType, SimComponent
|
||||
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.router import Router
|
||||
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
|
||||
|
||||
@@ -2,14 +2,13 @@ from __future__ import annotations
|
||||
|
||||
import re
|
||||
import secrets
|
||||
from abc import abstractmethod, ABC
|
||||
from abc import ABC, abstractmethod
|
||||
from ipaddress import IPv4Address, IPv4Network
|
||||
from pathlib import Path
|
||||
from typing import Any, Union
|
||||
from typing import Dict, Optional
|
||||
from typing import Any, Dict, Optional, Union
|
||||
|
||||
from prettytable import MARKDOWN, PrettyTable
|
||||
from pydantic import Field, BaseModel
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from primaite import getLogger
|
||||
from primaite.exceptions import NetworkError
|
||||
@@ -48,7 +47,7 @@ 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
|
||||
|
||||
@@ -198,9 +197,7 @@ class WiredNetworkInterface(NetworkInterface, ABC):
|
||||
return
|
||||
|
||||
if not self._connected_link:
|
||||
self._connected_node.sys_log.info(
|
||||
f"Interface {self} cannot be enabled as there is no Link connected."
|
||||
)
|
||||
self._connected_node.sys_log.info(f"Interface {self} cannot be enabled as there is no Link connected.")
|
||||
return
|
||||
|
||||
self.enabled = True
|
||||
@@ -225,9 +222,9 @@ class WiredNetworkInterface(NetworkInterface, ABC):
|
||||
"""
|
||||
Connect this network interface to a specified link.
|
||||
|
||||
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.
|
||||
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.
|
||||
"""
|
||||
@@ -246,8 +243,8 @@ class WiredNetworkInterface(NetworkInterface, ABC):
|
||||
"""
|
||||
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.
|
||||
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
|
||||
@@ -298,6 +295,7 @@ class Layer3Interface(BaseModel, ABC):
|
||||
: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."
|
||||
|
||||
@@ -357,10 +355,23 @@ class IPWiredNetworkInterface(WiredNetworkInterface, Layer3Interface, ABC):
|
||||
Derived classes should define specific behaviors and properties of an IP-capable wired network interface,
|
||||
customizing it for their specific use cases.
|
||||
"""
|
||||
|
||||
_connected_link: Optional[Link] = None
|
||||
"The network link to which the network interface is connected."
|
||||
|
||||
def model_post_init(self, __context: Any) -> None:
|
||||
"""
|
||||
Performs post-initialisation checks to ensure the model's IP configuration is valid.
|
||||
|
||||
This method is invoked after the initialisation of a network model object to validate its network settings,
|
||||
particularly to ensure that the assigned IP address is not a network address. This validation is crucial for
|
||||
maintaining the integrity of network simulations and avoiding configuration errors that could lead to
|
||||
unrealistic or incorrect behavior.
|
||||
|
||||
:param __context: Contextual information or parameters passed to the method, used for further initializing or
|
||||
validating the model post-creation.
|
||||
:raises ValueError: If the IP address is the same as the network address, indicating an incorrect configuration.
|
||||
"""
|
||||
if self.ip_network.network_address == self.ip_address:
|
||||
raise ValueError(f"{self.ip_address}/{self.subnet_mask} must not be a network address")
|
||||
|
||||
@@ -380,6 +391,17 @@ class IPWiredNetworkInterface(WiredNetworkInterface, Layer3Interface, ABC):
|
||||
return state
|
||||
|
||||
def enable(self):
|
||||
"""
|
||||
Enables this wired network interface and attempts to send a "hello" message to the default gateway.
|
||||
|
||||
This method activates the network interface, making it operational for network communications. After enabling,
|
||||
it tries to initiate a default gateway "hello" process, typically to establish initial connectivity and resolve
|
||||
the default gateway's MAC address. This step is crucial for ensuring the interface can successfully send data
|
||||
to and receive data from the network.
|
||||
|
||||
The method safely handles cases where the connected node might not have a default gateway set or the
|
||||
`default_gateway_hello` method is not defined, ignoring such errors to proceed without interruption.
|
||||
"""
|
||||
super().enable()
|
||||
try:
|
||||
pass
|
||||
@@ -440,8 +462,8 @@ class IPWirelessNetworkInterface(WiredNetworkInterface, Layer3Interface, ABC):
|
||||
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.
|
||||
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.
|
||||
@@ -804,8 +826,10 @@ class Node(SimComponent):
|
||||
{
|
||||
"hostname": self.hostname,
|
||||
"operating_state": self.operating_state.value,
|
||||
"NICs": {eth_num: network_interface.describe_state() for eth_num, network_interface in
|
||||
self.network_interface.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()},
|
||||
@@ -816,7 +840,7 @@ class Node(SimComponent):
|
||||
return state
|
||||
|
||||
def show(self, markdown: bool = False):
|
||||
"Show function that calls both show NIC and show open ports."
|
||||
"""Show function that calls both show NIC and show open ports."""
|
||||
self.show_nic(markdown)
|
||||
self.show_open_ports(markdown)
|
||||
|
||||
@@ -833,6 +857,14 @@ class Node(SimComponent):
|
||||
|
||||
@property
|
||||
def has_enabled_network_interface(self) -> bool:
|
||||
"""
|
||||
Checks if the node has at least one enabled network interface.
|
||||
|
||||
Iterates through all network interfaces associated with the node to determine if at least one is enabled. This
|
||||
property is essential for determining the node's ability to communicate within the network.
|
||||
|
||||
:return: True if there is at least one enabled network interface; otherwise, False.
|
||||
"""
|
||||
for network_interface in self.network_interfaces.values():
|
||||
if network_interface.enabled:
|
||||
return True
|
||||
@@ -1014,7 +1046,7 @@ class Node(SimComponent):
|
||||
"""
|
||||
if self.operating_state.ON:
|
||||
self.is_resetting = True
|
||||
self.sys_log.info(f"Resetting")
|
||||
self.sys_log.info("Resetting")
|
||||
self.power_off()
|
||||
|
||||
def connect_nic(self, network_interface: NetworkInterface):
|
||||
@@ -1097,7 +1129,7 @@ class Node(SimComponent):
|
||||
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
|
||||
network_interface=from_network_interface,
|
||||
)
|
||||
else:
|
||||
return
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
from abc import ABC
|
||||
from ipaddress import IPv4Network
|
||||
from typing import Dict
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from primaite.utils.validators import IPV4Address
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ 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
|
||||
|
||||
|
||||
@@ -81,4 +80,4 @@ class WirelessAccessPoint(WirelessNetworkInterface, Layer3Interface):
|
||||
|
||||
: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}"
|
||||
return f"Port {self.port_num}: {self.mac_address}/{self.ip_address}"
|
||||
|
||||
@@ -2,7 +2,6 @@ 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
|
||||
|
||||
|
||||
@@ -78,4 +77,4 @@ class WirelessNIC(WirelessNetworkInterface, Layer3Interface):
|
||||
|
||||
: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}"
|
||||
return f"Port {self.port_num}: {self.mac_address}/{self.ip_address}"
|
||||
|
||||
@@ -28,5 +28,5 @@ class Computer(HostNode):
|
||||
* Applications:
|
||||
* Web Browser
|
||||
"""
|
||||
pass
|
||||
|
||||
pass
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Dict, Any
|
||||
from typing import Optional
|
||||
from ipaddress import IPv4Address
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from primaite import getLogger
|
||||
from primaite.simulator.network.hardware.base import IPWiredNetworkInterface, Link
|
||||
from primaite.simulator.network.hardware.base import Node
|
||||
from primaite.simulator.network.hardware.base import IPWiredNetworkInterface, Link, 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
|
||||
@@ -20,43 +18,45 @@ 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.
|
||||
Extends the ARP service for host-specific functionalities within a network, focusing on resolving and caching
|
||||
MAC addresses and network interfaces (NICs) based on IP addresses, especially concerning the default gateway.
|
||||
|
||||
This specialized ARP service for hosts facilitates efficient network communication by managing ARP entries
|
||||
and handling ARP requests and replies with additional logic for default gateway processing.
|
||||
"""
|
||||
|
||||
def get_default_gateway_mac_address(self) -> Optional[str]:
|
||||
"""
|
||||
Retrieves the MAC address of the default gateway from the ARP cache.
|
||||
Retrieves the MAC address of the default gateway as known from the ARP cache.
|
||||
|
||||
:return: The MAC address of the default gateway if it exists in the ARP cache, otherwise None.
|
||||
:return: The MAC address of the default gateway if present 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.
|
||||
Obtains the network interface card (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.
|
||||
:return: The NIC associated with the default gateway if it exists in the ARP cache; otherwise, None.
|
||||
"""
|
||||
if self.software_manager.node.default_gateway and self.software_manager.node.has_enabled_network_interface:
|
||||
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
|
||||
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.
|
||||
: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)
|
||||
@@ -76,22 +76,23 @@ class HostARP(ARP):
|
||||
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
|
||||
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]:
|
||||
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.
|
||||
Retrieves the MAC address associated with a given 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.
|
||||
"""
|
||||
:param ip_address: The IP address for which the MAC address is sought.
|
||||
:return: The MAC address if available in the ARP cache; 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
|
||||
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.
|
||||
@@ -118,17 +119,18 @@ class HostARP(ARP):
|
||||
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
|
||||
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]:
|
||||
def get_arp_cache_network_interface(self, ip_address: IPv4Address) -> Optional[NIC]:
|
||||
"""
|
||||
Retrieves the NIC associated with an IP address from the ARP cache.
|
||||
Retrieves the network interface card (NIC) associated with a given 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.
|
||||
:param ip_address: The IP address for which the associated NIC is sought.
|
||||
:return: The NIC if available in the ARP cache; otherwise, None.
|
||||
"""
|
||||
return self._get_arp_cache_network_interface(ip_address)
|
||||
|
||||
@@ -146,15 +148,17 @@ class HostARP(ARP):
|
||||
# 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}"
|
||||
f"Ignoring ARP request for {arp_packet.target_ip_address}. Current IP address is "
|
||||
f"{from_network_interface.ip_address}"
|
||||
)
|
||||
return
|
||||
|
||||
# Matched ARP request
|
||||
# TODO: try taking this out
|
||||
self.add_arp_cache_entry(
|
||||
ip_address=arp_packet.sender_ip_address, mac_address=arp_packet.sender_mac_addr,
|
||||
network_interface=from_network_interface
|
||||
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)
|
||||
@@ -175,12 +179,25 @@ class NIC(IPWiredNetworkInterface):
|
||||
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.
|
||||
"""
|
||||
|
||||
_connected_link: Optional[Link] = None
|
||||
"The network link to which the network interface is connected."
|
||||
wake_on_lan: bool = False
|
||||
"Indicates if the NIC supports Wake-on-LAN functionality."
|
||||
|
||||
def model_post_init(self, __context: Any) -> None:
|
||||
"""
|
||||
Performs post-initialisation checks to ensure the model's IP configuration is valid.
|
||||
|
||||
This method is invoked after the initialisation of a network model object to validate its network settings,
|
||||
particularly to ensure that the assigned IP address is not a network address. This validation is crucial for
|
||||
maintaining the integrity of network simulations and avoiding configuration errors that could lead to
|
||||
unrealistic or incorrect behavior.
|
||||
|
||||
:param __context: Contextual information or parameters passed to the method, used for further initializing or
|
||||
validating the model post-creation.
|
||||
:raises ValueError: If the IP address is the same as the network address, indicating an incorrect configuration.
|
||||
"""
|
||||
if self.ip_network.network_address == self.ip_address:
|
||||
raise ValueError(f"{self.ip_address}/{self.subnet_mask} must not be a network address")
|
||||
|
||||
@@ -255,21 +272,24 @@ 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.
|
||||
An end-user device within the network, such as a computer or server, equipped with the capability to initiate and
|
||||
respond to network communications.
|
||||
|
||||
A `HostNode` extends the base `Node` class by incorporating host-specific services and applications, thereby
|
||||
simulating the functionalities typically expected from a networked end-user device.
|
||||
|
||||
**Example**::
|
||||
|
||||
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"
|
||||
)
|
||||
... 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:
|
||||
The host node comes pre-equipped with a range of core functionalities, services, and applications necessary
|
||||
for engaging in various network operations and tasks.
|
||||
|
||||
Core Functionality:
|
||||
-------------------
|
||||
@@ -291,6 +311,7 @@ class HostNode(Node):
|
||||
|
||||
* Web Browser: Provides web browsing capabilities.
|
||||
"""
|
||||
|
||||
network_interfaces: Dict[str, NIC] = {}
|
||||
"The Network Interfaces on the node."
|
||||
network_interface: Dict[int, NIC] = {}
|
||||
@@ -301,7 +322,12 @@ class HostNode(Node):
|
||||
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."""
|
||||
"""
|
||||
Installs the system software and network services typically found on an operating system.
|
||||
|
||||
This method equips the host with essential network services and applications, preparing it for various
|
||||
network-related tasks and operations.
|
||||
"""
|
||||
# ARP Service
|
||||
self.software_manager.install(HostARP)
|
||||
|
||||
@@ -323,6 +349,12 @@ class HostNode(Node):
|
||||
super()._install_system_software()
|
||||
|
||||
def default_gateway_hello(self):
|
||||
"""
|
||||
Sends a hello message to the default gateway to establish connectivity and resolve the gateway's MAC address.
|
||||
|
||||
This method is invoked to ensure the host node can communicate with its default gateway, primarily to confirm
|
||||
network connectivity and populate the ARP cache with the gateway's MAC address.
|
||||
"""
|
||||
if self.operating_state == NodeOperatingState.ON and self.default_gateway:
|
||||
self.software_manager.arp.get_default_gateway_mac_address()
|
||||
|
||||
|
||||
@@ -28,4 +28,3 @@ class Server(HostNode):
|
||||
* Applications:
|
||||
* Web Browser
|
||||
"""
|
||||
|
||||
|
||||
@@ -1,9 +1,30 @@
|
||||
from primaite.simulator.network.hardware.base import Node, NetworkInterface
|
||||
from abc import abstractmethod
|
||||
|
||||
from primaite.simulator.network.hardware.base import NetworkInterface, Node
|
||||
from primaite.simulator.network.transmission.data_link_layer import Frame
|
||||
|
||||
|
||||
class NetworkNode(Node):
|
||||
""""""
|
||||
"""
|
||||
Represents an abstract base class for a network node that can receive and process network frames.
|
||||
|
||||
This class provides a common interface for network nodes such as routers and switches, defining the essential
|
||||
behavior that allows these devices to handle incoming network traffic. Implementations of this class must
|
||||
provide functionality for receiving and processing frames received on their network interfaces.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def receive_frame(self, frame: Frame, from_network_interface: NetworkInterface):
|
||||
"""
|
||||
Abstract method that must be implemented by subclasses to define how to receive and process frames.
|
||||
|
||||
This method is called when a frame is received by a network interface belonging to this node. Subclasses
|
||||
should implement the logic to process the frame, including examining its contents, making forwarding decisions,
|
||||
or performing any necessary actions based on the frame's protocol and destination.
|
||||
|
||||
:param frame: The network frame that has been received.
|
||||
:type frame: Frame
|
||||
:param from_network_interface: The network interface on which the frame was received.
|
||||
:type from_network_interface: NetworkInterface
|
||||
"""
|
||||
pass
|
||||
|
||||
@@ -3,25 +3,22 @@ from __future__ import annotations
|
||||
import secrets
|
||||
from enum import Enum
|
||||
from ipaddress import IPv4Address, IPv4Network
|
||||
from typing import Dict, Any
|
||||
from typing import List, Optional, Tuple, Union
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
|
||||
from prettytable import MARKDOWN, PrettyTable
|
||||
from pydantic import ValidationError
|
||||
|
||||
from primaite.simulator.core import RequestManager, RequestType, SimComponent
|
||||
from primaite.simulator.network.hardware.base import IPWiredNetworkInterface
|
||||
from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState
|
||||
from primaite.simulator.network.hardware.nodes.network.network_node import NetworkNode
|
||||
from primaite.simulator.network.protocols.arp import ARPPacket
|
||||
from primaite.simulator.network.protocols.icmp import ICMPType, ICMPPacket
|
||||
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
|
||||
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
|
||||
from primaite.utils.validators import IPV4Address
|
||||
|
||||
|
||||
class ACLAction(Enum):
|
||||
@@ -205,14 +202,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.
|
||||
@@ -259,12 +256,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.
|
||||
@@ -286,23 +283,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.
|
||||
@@ -324,11 +321,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)
|
||||
|
||||
@@ -445,11 +442,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.
|
||||
@@ -539,16 +536,34 @@ class RouteTable(SimComponent):
|
||||
|
||||
class RouterARP(ARP):
|
||||
"""
|
||||
Inherits from ARPCache and adds router-specific ARP packet processing.
|
||||
Extends ARP functionality with router-specific ARP packet processing capabilities.
|
||||
|
||||
:ivar SysLog sys_log: A system log for logging messages.
|
||||
:ivar Router router: The router to which this ARP cache belongs.
|
||||
This class is designed to manage ARP requests and replies within a router, handling both the resolution of MAC
|
||||
addresses for IP addresses within the router's networks and the forwarding of ARP requests to other networks
|
||||
based on routing information.
|
||||
"""
|
||||
|
||||
router: Optional[Router] = None
|
||||
|
||||
def _get_arp_cache_mac_address(
|
||||
self, ip_address: IPV4Address, is_reattempt: bool = False, is_default_route_attempt: bool = False
|
||||
self, ip_address: IPv4Address, is_reattempt: bool = False, is_default_route_attempt: bool = False
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Attempts to retrieve the MAC address associated with the given IP address from the ARP cache.
|
||||
|
||||
If the address is not in the cache, an ARP request may be sent, and the method may reattempt the lookup.
|
||||
|
||||
:param ip_address: The IP address for which to find the corresponding MAC address.
|
||||
:type ip_address: IPv4Address
|
||||
:param is_reattempt: Indicates whether this call is a reattempt after a failed initial attempt to find the MAC
|
||||
address.
|
||||
:type is_reattempt: bool
|
||||
:param is_default_route_attempt: Indicates whether the attempt is being made to resolve the MAC address for the
|
||||
default route.
|
||||
:type is_default_route_attempt: bool
|
||||
:return: The MAC address associated with the given IP address, if found; otherwise, None.
|
||||
:rtype: Optional[str]
|
||||
"""
|
||||
arp_entry = self.arp.get(ip_address)
|
||||
|
||||
if arp_entry:
|
||||
@@ -558,9 +573,7 @@ class RouterARP(ARP):
|
||||
if self.router.ip_is_in_router_interface_subnet(ip_address):
|
||||
self.send_arp_request(ip_address)
|
||||
return self._get_arp_cache_mac_address(
|
||||
ip_address=ip_address,
|
||||
is_reattempt=True,
|
||||
is_default_route_attempt=is_default_route_attempt
|
||||
ip_address=ip_address, is_reattempt=True, is_default_route_attempt=is_default_route_attempt
|
||||
)
|
||||
|
||||
route = self.router.route_table.find_best_route(ip_address)
|
||||
@@ -569,7 +582,7 @@ class RouterARP(ARP):
|
||||
return self._get_arp_cache_mac_address(
|
||||
ip_address=route.next_hop_ip_address,
|
||||
is_reattempt=True,
|
||||
is_default_route_attempt=is_default_route_attempt
|
||||
is_default_route_attempt=is_default_route_attempt,
|
||||
)
|
||||
else:
|
||||
if self.router.route_table.default_route:
|
||||
@@ -578,16 +591,40 @@ class RouterARP(ARP):
|
||||
return self._get_arp_cache_mac_address(
|
||||
ip_address=self.router.route_table.default_route.next_hop_ip_address,
|
||||
is_reattempt=True,
|
||||
is_default_route_attempt=True
|
||||
is_default_route_attempt=True,
|
||||
)
|
||||
return None
|
||||
|
||||
def get_arp_cache_mac_address(self, ip_address: IPv4Address) -> Optional[str]:
|
||||
"""
|
||||
Public interface to retrieve the MAC address associated with the given IP address from the ARP cache.
|
||||
|
||||
:param ip_address: The IP address for which to find the corresponding MAC address.
|
||||
:type ip_address: IPv4Address
|
||||
:return: The MAC address associated with the given IP address, if found; otherwise, None.
|
||||
:rtype: Optional[str]
|
||||
"""
|
||||
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_route_attempt: bool = False
|
||||
self, ip_address: IPv4Address, is_reattempt: bool = False, is_default_route_attempt: bool = False
|
||||
) -> Optional[RouterInterface]:
|
||||
"""
|
||||
Attempts to retrieve the router interface associated with the given IP address.
|
||||
|
||||
If the address is not directly associated with a router interface, it may send an ARP request based on
|
||||
routing information.
|
||||
|
||||
:param ip_address: The IP address for which to find the corresponding router interface.
|
||||
:type ip_address: IPv4Address
|
||||
:param is_reattempt: Indicates whether this call is a reattempt after a failed initial attempt.
|
||||
:type is_reattempt: bool
|
||||
:param is_default_route_attempt: Indicates whether the attempt is being made for the default route's next-hop
|
||||
IP address.
|
||||
:type is_default_route_attempt: bool
|
||||
:return: The router interface associated with the given IP address, if applicable; otherwise, None.
|
||||
:rtype: Optional[RouterInterface]
|
||||
"""
|
||||
arp_entry = self.arp.get(ip_address)
|
||||
if arp_entry:
|
||||
return self.software_manager.node.network_interfaces[arp_entry.network_interface_uuid]
|
||||
@@ -603,7 +640,7 @@ class RouterARP(ARP):
|
||||
return self._get_arp_cache_network_interface(
|
||||
ip_address=route.next_hop_ip_address,
|
||||
is_reattempt=True,
|
||||
is_default_route_attempt=is_default_route_attempt
|
||||
is_default_route_attempt=is_default_route_attempt,
|
||||
)
|
||||
else:
|
||||
if self.router.route_table.default_route:
|
||||
@@ -612,17 +649,32 @@ class RouterARP(ARP):
|
||||
return self._get_arp_cache_network_interface(
|
||||
ip_address=self.router.route_table.default_route.next_hop_ip_address,
|
||||
is_reattempt=True,
|
||||
is_default_route_attempt=True
|
||||
is_default_route_attempt=True,
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
|
||||
def get_arp_cache_network_interface(self, ip_address: IPv4Address) -> Optional[RouterInterface]:
|
||||
"""
|
||||
Public interface to retrieve the router interface associated with the given IP address.
|
||||
|
||||
return self._get_arp_cache_network_interface(ip_address)
|
||||
:param ip_address: The IP address for which to find the corresponding router interface.
|
||||
:type ip_address: IPv4Address
|
||||
:return: The router interface associated with the given IP address, if found; otherwise, None.
|
||||
:rtype: Optional[RouterInterface]
|
||||
"""
|
||||
return self._get_arp_cache_network_interface(ip_address)
|
||||
|
||||
def _process_arp_request(self, arp_packet: ARPPacket, from_network_interface: RouterInterface):
|
||||
"""
|
||||
Processes an ARP request packet received on a router interface.
|
||||
|
||||
If the target IP address matches the interface's IP address, generates and sends an ARP reply.
|
||||
|
||||
:param arp_packet: The received ARP request packet.
|
||||
:type arp_packet: ARPPacket
|
||||
:param from_network_interface: The router interface on which the ARP request was received.
|
||||
:type from_network_interface: RouterInterface
|
||||
"""
|
||||
super()._process_arp_request(arp_packet, from_network_interface)
|
||||
|
||||
# If the target IP matches one of the router's NICs
|
||||
@@ -632,6 +684,14 @@ class RouterARP(ARP):
|
||||
return
|
||||
|
||||
def _process_arp_reply(self, arp_packet: ARPPacket, from_network_interface: RouterInterface):
|
||||
"""
|
||||
Processes an ARP reply packet received on a router interface. Updates the ARP cache with the new information.
|
||||
|
||||
:param arp_packet: The received ARP reply packet.
|
||||
:type arp_packet: ARPPacket
|
||||
:param from_network_interface: The router interface on which the ARP reply was received.
|
||||
:type from_network_interface: RouterInterface
|
||||
"""
|
||||
if arp_packet.target_ip_address == from_network_interface.ip_address:
|
||||
super()._process_arp_reply(arp_packet, from_network_interface)
|
||||
|
||||
@@ -650,7 +710,7 @@ class RouterICMP(ICMP):
|
||||
|
||||
router: Optional[Router] = None
|
||||
|
||||
def _process_icmp_echo_request(self, frame: Frame, from_network_interface):
|
||||
def _process_icmp_echo_request(self, frame: Frame, from_network_interface: RouterInterface):
|
||||
"""
|
||||
Processes an ICMP echo request received by the service.
|
||||
|
||||
@@ -664,7 +724,8 @@ class RouterICMP(ICMP):
|
||||
|
||||
if not network_interface:
|
||||
self.sys_log.error(
|
||||
"Cannot send ICMP echo reply as there is no outbound Network Interface to use. Try configuring the default gateway."
|
||||
"Cannot send ICMP echo reply as there is no outbound Network Interface to use. Try configuring the "
|
||||
"default gateway."
|
||||
)
|
||||
return
|
||||
|
||||
@@ -682,7 +743,7 @@ class RouterICMP(ICMP):
|
||||
dst_ip_address=frame.ip.src_ip_address,
|
||||
dst_port=self.port,
|
||||
ip_protocol=self.protocol,
|
||||
icmp_packet=icmp_packet
|
||||
icmp_packet=icmp_packet,
|
||||
)
|
||||
|
||||
def receive(self, payload: Any, session_id: str, **kwargs) -> bool:
|
||||
@@ -815,11 +876,19 @@ class RouterInterface(IPWiredNetworkInterface):
|
||||
|
||||
class Router(NetworkNode):
|
||||
"""
|
||||
A class to represent a network router node.
|
||||
Represents a network router, managing routing and forwarding of IP packets across network interfaces.
|
||||
|
||||
: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, RouterARP, RouterICMP.
|
||||
A router operates at the network layer and is responsible for receiving, processing, and forwarding data packets
|
||||
between computer networks. It examines the destination IP address of incoming packets and determines the best way
|
||||
to route them towards their destination.
|
||||
|
||||
The router integrates various network services and protocols to facilitate IP routing, including ARP (Address
|
||||
Resolution Protocol) and ICMP (Internet Control Message Protocol) for handling network diagnostics and errors.
|
||||
|
||||
:ivar str hostname: The name of the router, used for identification and logging.
|
||||
:ivar int num_ports: The number of physical or logical ports on the router.
|
||||
:ivar dict kwargs: Optional keyword arguments for initializing components like SysLog, ACL (Access Control List),
|
||||
RouteTable, RouterARP, and RouterICMP services.
|
||||
"""
|
||||
|
||||
num_ports: int
|
||||
@@ -848,7 +917,13 @@ class Router(NetworkNode):
|
||||
self.set_original_state()
|
||||
|
||||
def _install_system_software(self):
|
||||
"""Install System Software - software that is usually provided with the OS."""
|
||||
"""
|
||||
Installs essential system software and network services on the router.
|
||||
|
||||
This includes initializing and setting up RouterICMP for handling ICMP packets and RouterARP for address
|
||||
resolution within the network. These services are crucial for the router's operation, enabling it to manage
|
||||
network traffic efficiently.
|
||||
"""
|
||||
self.software_manager.install(RouterICMP)
|
||||
icmp: RouterICMP = self.software_manager.icmp # noqa
|
||||
icmp.router = self
|
||||
@@ -857,11 +932,22 @@ class Router(NetworkNode):
|
||||
arp.router = self
|
||||
|
||||
def _set_default_acl(self):
|
||||
"""
|
||||
Sets default access control rules for the router.
|
||||
|
||||
Initializes the router's ACL (Access Control List) with default rules, permitting essential protocols like ARP
|
||||
and ICMP, which are necessary for basic network operations and diagnostics.
|
||||
"""
|
||||
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."""
|
||||
"""
|
||||
Sets or resets the router to its original configuration state.
|
||||
|
||||
This method is called to initialize the router's state or to revert it to a known good configuration during
|
||||
network simulations or after configuration changes.
|
||||
"""
|
||||
self.acl.set_original_state()
|
||||
self.route_table.set_original_state()
|
||||
super().set_original_state()
|
||||
@@ -869,7 +955,14 @@ class Router(NetworkNode):
|
||||
self._original_state.update(self.model_dump(include=vals_to_include))
|
||||
|
||||
def reset_component_for_episode(self, episode: int):
|
||||
"""Reset the original state of the SimComponent."""
|
||||
"""
|
||||
Resets the router's components for a new network simulation episode.
|
||||
|
||||
Clears ARP cache, resets ACL and route table to their original states, and re-enables all network interfaces.
|
||||
This ensures that the router starts from a clean state for each simulation episode.
|
||||
|
||||
:param episode: The episode number for which the router is being reset.
|
||||
"""
|
||||
self.software_manager.arp.clear()
|
||||
self.acl.reset_component_for_episode(episode)
|
||||
self.route_table.reset_component_for_episode(episode)
|
||||
@@ -884,7 +977,14 @@ class Router(NetworkNode):
|
||||
rm.add_request("acl", RequestType(func=self.acl._request_manager))
|
||||
return rm
|
||||
|
||||
def ip_is_router_interface(self, ip_address: IPV4Address, enabled_only: bool = False) -> bool:
|
||||
def ip_is_router_interface(self, ip_address: IPv4Address, enabled_only: bool = False) -> bool:
|
||||
"""
|
||||
Checks if a given IP address belongs to any of the router's interfaces.
|
||||
|
||||
:param ip_address: The IP address to check.
|
||||
:param enabled_only: If True, only considers enabled network interfaces.
|
||||
:return: True if the IP address is assigned to one of the router's interfaces; False otherwise.
|
||||
"""
|
||||
for router_interface in self.network_interface.values():
|
||||
if router_interface.ip_address == ip_address:
|
||||
if enabled_only:
|
||||
@@ -893,7 +993,14 @@ class Router(NetworkNode):
|
||||
return True
|
||||
return False
|
||||
|
||||
def ip_is_in_router_interface_subnet(self, ip_address: IPV4Address, enabled_only: bool = False) -> bool:
|
||||
def ip_is_in_router_interface_subnet(self, ip_address: IPv4Address, enabled_only: bool = False) -> bool:
|
||||
"""
|
||||
Determines if a given IP address falls within the subnet of any router interface.
|
||||
|
||||
:param ip_address: The IP address to check.
|
||||
:param enabled_only: If True, only considers enabled network interfaces.
|
||||
:return: True if the IP address is within the subnet of any router's interface; False otherwise.
|
||||
"""
|
||||
for router_interface in self.network_interface.values():
|
||||
if ip_address in router_interface.ip_network:
|
||||
if enabled_only:
|
||||
@@ -904,10 +1011,10 @@ class Router(NetworkNode):
|
||||
|
||||
def _get_port_of_nic(self, target_nic: RouterInterface) -> Optional[int]:
|
||||
"""
|
||||
Retrieve the port number for a given NIC.
|
||||
Retrieves the port number associated with a given network interface controller (NIC).
|
||||
|
||||
:param target_nic: Target network interface.
|
||||
:return: The port number if NIC is found, otherwise None.
|
||||
:param target_nic: The NIC whose port number is being queried.
|
||||
:return: The port number if the NIC is found; otherwise, None.
|
||||
"""
|
||||
for port, network_interface in self.network_interface.items():
|
||||
if network_interface == target_nic:
|
||||
@@ -926,12 +1033,14 @@ class Router(NetworkNode):
|
||||
|
||||
def receive_frame(self, frame: Frame, from_network_interface: RouterInterface):
|
||||
"""
|
||||
Receive a frame from a RouterInterface and processes it based on its protocol.
|
||||
Processes an incoming frame received on one of the router's interfaces.
|
||||
|
||||
:param frame: The incoming frame.
|
||||
:param from_network_interface: The network interface where the frame is coming from.
|
||||
Examines the frame's destination and protocol, applies ACL rules, and either forwards or drops the frame based
|
||||
on routing decisions and ACL permissions.
|
||||
|
||||
:param frame: The incoming frame to be processed.
|
||||
:param from_network_interface: The router interface on which the frame was received.
|
||||
"""
|
||||
|
||||
if self.operating_state != NodeOperatingState.ON:
|
||||
return
|
||||
|
||||
@@ -965,12 +1074,13 @@ class Router(NetworkNode):
|
||||
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
|
||||
network_interface=from_network_interface,
|
||||
)
|
||||
|
||||
send_to_session_manager = False
|
||||
if ((frame.icmp and self.ip_is_router_interface(dst_ip_address))
|
||||
or (dst_port in self.software_manager.get_open_ports())):
|
||||
if (frame.icmp and self.ip_is_router_interface(dst_ip_address)) or (
|
||||
dst_port in self.software_manager.get_open_ports()
|
||||
):
|
||||
send_to_session_manager = True
|
||||
|
||||
if send_to_session_manager:
|
||||
@@ -981,17 +1091,20 @@ class Router(NetworkNode):
|
||||
|
||||
def process_frame(self, frame: Frame, from_network_interface: RouterInterface) -> None:
|
||||
"""
|
||||
Process a Frame.
|
||||
Routes or forwards a frame based on the router's routing table and interface configurations.
|
||||
|
||||
:param frame: The frame to be routed.
|
||||
:param from_network_interface: The source network interface.
|
||||
This method is called if a frame is not directly addressed to the router or does not match any open service
|
||||
ports. It determines the next hop for the frame and forwards it accordingly.
|
||||
|
||||
:param frame: The frame to be routed or forwarded.
|
||||
:param from_network_interface: The network interface from which the frame originated.
|
||||
"""
|
||||
# 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 a port that isn't open.")
|
||||
self.sys_log.info("Dropping frame destined for this router on a port that isn't open.")
|
||||
return
|
||||
|
||||
network_interface: RouterInterface = self.software_manager.arp.get_arp_cache_network_interface(
|
||||
@@ -1031,6 +1144,15 @@ class Router(NetworkNode):
|
||||
self.route_frame(frame, from_network_interface)
|
||||
|
||||
def route_frame(self, frame: Frame, from_network_interface: RouterInterface) -> None:
|
||||
"""
|
||||
Determines the best route for a frame and forwards it towards its destination.
|
||||
|
||||
Uses the router's routing table to find the best route for the frame's destination IP address and forwards the
|
||||
frame through the appropriate interface.
|
||||
|
||||
:param frame: The frame to be routed.
|
||||
:param from_network_interface: The source network interface.
|
||||
"""
|
||||
route = self.route_table.find_best_route(frame.ip.dst_ip_address)
|
||||
if route:
|
||||
network_interface = self.software_manager.arp.get_arp_cache_network_interface(route.next_hop_ip_address)
|
||||
@@ -1059,11 +1181,11 @@ class Router(NetworkNode):
|
||||
|
||||
def configure_port(self, port: int, ip_address: Union[IPv4Address, str], subnet_mask: Union[IPv4Address, str]):
|
||||
"""
|
||||
Configure the IP settings of a given port.
|
||||
Configures the IP settings for a specified router port.
|
||||
|
||||
:param port: The port to configure.
|
||||
:param ip_address: The IP address to set.
|
||||
:param subnet_mask: The subnet mask to set.
|
||||
:param port: The port number to configure.
|
||||
:param ip_address: The IP address to assign to the port.
|
||||
:param subnet_mask: The subnet mask for the port.
|
||||
"""
|
||||
if not isinstance(ip_address, IPv4Address):
|
||||
ip_address = IPv4Address(ip_address)
|
||||
@@ -1072,16 +1194,14 @@ class Router(NetworkNode):
|
||||
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.sys_log.info(f"Configured Network Interface {network_interface}")
|
||||
self.set_original_state()
|
||||
|
||||
def enable_port(self, port: int):
|
||||
"""
|
||||
Enable a given port on the router.
|
||||
Enables a specified port on the router.
|
||||
|
||||
:param port: The port to enable.
|
||||
:param port: The port number to enable.
|
||||
"""
|
||||
network_interface = self.network_interface.get(port)
|
||||
if network_interface:
|
||||
@@ -1089,9 +1209,9 @@ class Router(NetworkNode):
|
||||
|
||||
def disable_port(self, port: int):
|
||||
"""
|
||||
Disable a given port on the router.
|
||||
Disables a specified port on the router.
|
||||
|
||||
:param port: The port to disable.
|
||||
:param port: The port number to disable.
|
||||
"""
|
||||
network_interface = self.network_interface.get(port)
|
||||
if network_interface:
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
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 WiredNetworkInterface, NetworkInterface, Link
|
||||
from primaite.simulator.network.hardware.base import Link, WiredNetworkInterface
|
||||
from primaite.simulator.network.hardware.nodes.network.network_node import NetworkNode
|
||||
from primaite.simulator.network.transmission.data_link_layer import Frame
|
||||
|
||||
@@ -27,6 +28,7 @@ class SwitchPort(WiredNetworkInterface):
|
||||
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."
|
||||
|
||||
@@ -40,7 +42,6 @@ class SwitchPort(WiredNetworkInterface):
|
||||
"""
|
||||
Produce a dictionary describing the current state of this object.
|
||||
|
||||
|
||||
:return: Current state of this object and child objects.
|
||||
:rtype: Dict
|
||||
"""
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
from ipaddress import IPv4Address
|
||||
|
||||
from primaite.simulator.network.container import Network
|
||||
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.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
|
||||
@@ -56,7 +55,7 @@ def client_server_routed() -> Network:
|
||||
ip_address="192.168.2.2",
|
||||
subnet_mask="255.255.255.0",
|
||||
default_gateway="192.168.2.1",
|
||||
start_up_duration=0
|
||||
start_up_duration=0,
|
||||
)
|
||||
client_1.power_on()
|
||||
network.connect(endpoint_b=client_1.network_interface[1], endpoint_a=switch_2.network_interface[1])
|
||||
@@ -67,7 +66,7 @@ def client_server_routed() -> Network:
|
||||
ip_address="192.168.1.2",
|
||||
subnet_mask="255.255.255.0",
|
||||
default_gateway="192.168.1.1",
|
||||
start_up_duration=0
|
||||
start_up_duration=0,
|
||||
)
|
||||
server_1.power_on()
|
||||
network.connect(endpoint_b=server_1.network_interface[1], endpoint_a=switch_1.network_interface[1])
|
||||
@@ -143,7 +142,7 @@ def arcd_uc2_network() -> Network:
|
||||
subnet_mask="255.255.255.0",
|
||||
default_gateway="192.168.10.1",
|
||||
dns_server=IPv4Address("192.168.1.10"),
|
||||
start_up_duration=0
|
||||
start_up_duration=0,
|
||||
)
|
||||
client_1.power_on()
|
||||
network.connect(endpoint_b=client_1.network_interface[1], endpoint_a=switch_2.network_interface[1])
|
||||
@@ -163,7 +162,7 @@ def arcd_uc2_network() -> Network:
|
||||
subnet_mask="255.255.255.0",
|
||||
default_gateway="192.168.10.1",
|
||||
dns_server=IPv4Address("192.168.1.10"),
|
||||
start_up_duration=0
|
||||
start_up_duration=0,
|
||||
)
|
||||
client_2.power_on()
|
||||
web_browser = client_2.software_manager.software.get("WebBrowser")
|
||||
@@ -176,7 +175,7 @@ def arcd_uc2_network() -> Network:
|
||||
ip_address="192.168.1.10",
|
||||
subnet_mask="255.255.255.0",
|
||||
default_gateway="192.168.1.1",
|
||||
start_up_duration=0
|
||||
start_up_duration=0,
|
||||
)
|
||||
domain_controller.power_on()
|
||||
domain_controller.software_manager.install(DNSServer)
|
||||
@@ -190,7 +189,7 @@ def arcd_uc2_network() -> Network:
|
||||
subnet_mask="255.255.255.0",
|
||||
default_gateway="192.168.1.1",
|
||||
dns_server=IPv4Address("192.168.1.10"),
|
||||
start_up_duration=0
|
||||
start_up_duration=0,
|
||||
)
|
||||
database_server.power_on()
|
||||
network.connect(endpoint_b=database_server.network_interface[1], endpoint_a=switch_1.network_interface[3])
|
||||
@@ -264,7 +263,7 @@ def arcd_uc2_network() -> Network:
|
||||
subnet_mask="255.255.255.0",
|
||||
default_gateway="192.168.1.1",
|
||||
dns_server=IPv4Address("192.168.1.10"),
|
||||
start_up_duration=0
|
||||
start_up_duration=0,
|
||||
)
|
||||
web_server.power_on()
|
||||
web_server.software_manager.install(DatabaseClient)
|
||||
@@ -288,7 +287,7 @@ def arcd_uc2_network() -> Network:
|
||||
subnet_mask="255.255.255.0",
|
||||
default_gateway="192.168.1.1",
|
||||
dns_server=IPv4Address("192.168.1.10"),
|
||||
start_up_duration=0
|
||||
start_up_duration=0,
|
||||
)
|
||||
backup_server.power_on()
|
||||
backup_server.software_manager.install(FTPServer)
|
||||
@@ -301,7 +300,7 @@ def arcd_uc2_network() -> Network:
|
||||
subnet_mask="255.255.255.0",
|
||||
default_gateway="192.168.1.1",
|
||||
dns_server=IPv4Address("192.168.1.10"),
|
||||
start_up_duration=0
|
||||
start_up_duration=0,
|
||||
)
|
||||
security_suite.power_on()
|
||||
network.connect(endpoint_b=security_suite.network_interface[1], endpoint_a=switch_1.network_interface[7])
|
||||
|
||||
@@ -111,4 +111,4 @@ class ICMPPacket(BaseModel):
|
||||
return description
|
||||
msg = f"No Matching ICMP code for type:{self.icmp_type.name}, code:{self.icmp_code}"
|
||||
_LOGGER.error(msg)
|
||||
raise ValueError(msg)
|
||||
raise ValueError(msg)
|
||||
|
||||
@@ -4,7 +4,6 @@ from typing import Any, Optional
|
||||
from pydantic import BaseModel
|
||||
|
||||
from primaite import getLogger
|
||||
from primaite.simulator.network.protocols.arp import ARPPacket
|
||||
from primaite.simulator.network.protocols.icmp import ICMPPacket
|
||||
from primaite.simulator.network.protocols.packet import DataPacket
|
||||
from primaite.simulator.network.transmission.network_layer import IPPacket, IPProtocol
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import secrets
|
||||
from enum import Enum
|
||||
from ipaddress import IPv4Address, IPv4Network
|
||||
from typing import Union
|
||||
from ipaddress import IPv4Address
|
||||
|
||||
from pydantic import BaseModel, field_validator, validate_call
|
||||
from pydantic_core.core_schema import FieldValidationInfo
|
||||
from pydantic import BaseModel
|
||||
|
||||
from primaite import getLogger
|
||||
|
||||
|
||||
@@ -108,4 +108,3 @@ class PacketCapture:
|
||||
"""
|
||||
msg = frame.model_dump_json()
|
||||
self.outbound_logger.log(level=60, msg=msg) # Log at custom log level > CRITICAL
|
||||
|
||||
|
||||
@@ -102,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.
|
||||
@@ -140,27 +140,76 @@ class SessionManager:
|
||||
dst_port = None
|
||||
return protocol, with_ip_address, src_port, dst_port
|
||||
|
||||
def resolve_outbound_network_interface(self, dst_ip_address: IPv4Address) -> Optional['NetworkInterface']:
|
||||
def resolve_outbound_network_interface(self, dst_ip_address: IPv4Address) -> Optional["NetworkInterface"]:
|
||||
"""
|
||||
Resolves the appropriate outbound network interface for a given destination IP address.
|
||||
|
||||
This method determines the most suitable network interface for sending a packet to the specified
|
||||
destination IP address. It considers only enabled network interfaces and checks if the destination
|
||||
IP address falls within the subnet of each interface. If no suitable local network interface is found,
|
||||
the method defaults to using the network interface associated with the default gateway.
|
||||
|
||||
The search process prioritises local network interfaces based on the IP network to which they belong.
|
||||
If the destination IP address does not match any local subnet, the method assumes that the destination
|
||||
is outside the local network and hence, routes the packet through the default gateway's network interface.
|
||||
|
||||
:param dst_ip_address: The destination IP address for which the outbound interface is to be resolved.
|
||||
:type dst_ip_address: IPv4Address
|
||||
:return: The network interface through which the packet should be sent to reach the destination IP address,
|
||||
or the default gateway's network interface if the destination is not within any local subnet.
|
||||
:rtype: 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,
|
||||
src_port: Optional[Port] = None,
|
||||
dst_port: Optional[Port] = None,
|
||||
protocol: Optional[IPProtocol] = None,
|
||||
session_id: Optional[str] = None
|
||||
self,
|
||||
dst_ip_address: Optional[Union[IPv4Address, IPv4Network]] = None,
|
||||
src_port: Optional[Port] = None,
|
||||
dst_port: Optional[Port] = None,
|
||||
protocol: Optional[IPProtocol] = None,
|
||||
session_id: Optional[str] = None,
|
||||
) -> Tuple[
|
||||
Optional['NetworkInterface'],
|
||||
Optional[str], IPv4Address,
|
||||
Optional["NetworkInterface"],
|
||||
Optional[str],
|
||||
IPv4Address,
|
||||
Optional[Port],
|
||||
Optional[Port],
|
||||
Optional[IPProtocol],
|
||||
bool
|
||||
bool,
|
||||
]:
|
||||
"""
|
||||
Resolves the necessary details for outbound transmission based on the provided parameters.
|
||||
|
||||
This method determines whether the payload should be broadcast or unicast based on the destination IP address
|
||||
and resolves the outbound network interface and destination MAC address accordingly.
|
||||
|
||||
The method first checks if `session_id` is provided and uses the session details if available. For broadcast
|
||||
transmissions, it finds a suitable network interface and uses a broadcast MAC address. For unicast
|
||||
transmissions, it attempts to resolve the destination MAC address using ARP and finds the appropriate
|
||||
outbound network interface. If the destination IP address is outside the local network and no specific MAC
|
||||
address is resolved, it uses the default gateway for the transmission.
|
||||
|
||||
:param dst_ip_address: The destination IP address or network. If an IPv4Network is provided, the method
|
||||
treats the transmission as a broadcast to that network. Optional.
|
||||
:type dst_ip_address: Optional[Union[IPv4Address, IPv4Network]]
|
||||
:param src_port: The source port number for the transmission. Optional.
|
||||
:type src_port: Optional[Port]
|
||||
:param dst_port: The destination port number for the transmission. Optional.
|
||||
:type dst_port: Optional[Port]
|
||||
:param protocol: The IP protocol to be used for the transmission. Optional.
|
||||
:type protocol: Optional[IPProtocol]
|
||||
:param session_id: The session ID associated with the transmission. If provided, the session details override
|
||||
other parameters. Optional.
|
||||
:type session_id: Optional[str]
|
||||
:return: A tuple containing the resolved outbound network interface, destination MAC address, destination IP
|
||||
address, source port, destination port, protocol, and a boolean indicating whether the transmission is a
|
||||
broadcast.
|
||||
:rtype: Tuple[Optional["NetworkInterface"], Optional[str], IPv4Address, Optional[Port], Optional[Port],
|
||||
Optional[IPProtocol], bool]
|
||||
"""
|
||||
if dst_ip_address and not isinstance(dst_ip_address, (IPv4Address, IPv4Network)):
|
||||
dst_ip_address = IPv4Address(dst_ip_address)
|
||||
is_broadcast = False
|
||||
@@ -207,14 +256,14 @@ class SessionManager:
|
||||
return outbound_network_interface, dst_mac_address, dst_ip_address, src_port, dst_port, protocol, is_broadcast
|
||||
|
||||
def receive_payload_from_software_manager(
|
||||
self,
|
||||
payload: Any,
|
||||
dst_ip_address: Optional[Union[IPv4Address, IPv4Network]] = None,
|
||||
src_port: Optional[Port] = 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,
|
||||
src_port: Optional[Port] = 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.
|
||||
@@ -239,15 +288,22 @@ class SessionManager:
|
||||
is_broadcast = payload.request
|
||||
ip_protocol = IPProtocol.UDP
|
||||
else:
|
||||
|
||||
vals = self.resolve_outbound_transmission_details(
|
||||
dst_ip_address=dst_ip_address,
|
||||
src_port=src_port,
|
||||
dst_port=dst_port,
|
||||
protocol=ip_protocol,
|
||||
session_id=session_id
|
||||
session_id=session_id,
|
||||
)
|
||||
outbound_network_interface, dst_mac_address, dst_ip_address, src_port, dst_port, protocol, is_broadcast = vals
|
||||
(
|
||||
outbound_network_interface,
|
||||
dst_mac_address,
|
||||
dst_ip_address,
|
||||
src_port,
|
||||
dst_port,
|
||||
protocol,
|
||||
is_broadcast,
|
||||
) = vals
|
||||
if protocol:
|
||||
ip_protocol = protocol
|
||||
|
||||
@@ -257,7 +313,7 @@ class SessionManager:
|
||||
|
||||
if not (src_port or dst_port):
|
||||
raise ValueError(
|
||||
f"Failed to resolve src or dst port. Have you sent the port from the service or application?"
|
||||
"Failed to resolve src or dst port. Have you sent the port from the service or application?"
|
||||
)
|
||||
|
||||
tcp_header = None
|
||||
@@ -283,7 +339,11 @@ class SessionManager:
|
||||
# Construct the frame for transmission
|
||||
frame = Frame(
|
||||
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),
|
||||
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,
|
||||
@@ -304,7 +364,7 @@ class SessionManager:
|
||||
# Send the frame through the NIC
|
||||
return outbound_network_interface.send_frame(frame)
|
||||
|
||||
def receive_frame(self, frame: Frame, from_network_interface: 'NetworkInterface'):
|
||||
def receive_frame(self, frame: Frame, from_network_interface: "NetworkInterface"):
|
||||
"""
|
||||
Receive a Frame.
|
||||
|
||||
@@ -334,7 +394,7 @@ class SessionManager:
|
||||
protocol=frame.ip.protocol,
|
||||
session_id=session.uuid,
|
||||
from_network_interface=from_network_interface,
|
||||
frame=frame
|
||||
frame=frame,
|
||||
)
|
||||
|
||||
def show(self, markdown: bool = False):
|
||||
|
||||
@@ -25,7 +25,14 @@ IOSoftwareClass = TypeVar("IOSoftwareClass", bound=IOSoftware)
|
||||
|
||||
|
||||
class SoftwareManager:
|
||||
"""A class that manages all running Services and Applications on a Node and facilitates their communication."""
|
||||
"""
|
||||
Manages all running services and applications on a network node and facilitates their communication.
|
||||
|
||||
This class is responsible for installing, uninstalling, and managing the operational state of various network
|
||||
services and applications. It acts as a bridge between the node's session manager and its software components,
|
||||
ensuring that incoming and outgoing network payloads are correctly routed to and from the appropriate services
|
||||
or applications.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -50,11 +57,13 @@ class SoftwareManager:
|
||||
self.dns_server: Optional[IPv4Address] = dns_server
|
||||
|
||||
@property
|
||||
def arp(self) -> 'ARP':
|
||||
def arp(self) -> "ARP":
|
||||
"""Provides access to the ARP service instance, if installed."""
|
||||
return self.software.get("ARP") # noqa
|
||||
|
||||
@property
|
||||
def icmp(self) -> 'ICMP':
|
||||
def icmp(self) -> "ICMP":
|
||||
"""Provides access to the ICMP service instance, if installed."""
|
||||
return self.software.get("ICMP") # noqa
|
||||
|
||||
def get_open_ports(self) -> List[Port]:
|
||||
@@ -167,7 +176,13 @@ class SoftwareManager:
|
||||
)
|
||||
|
||||
def receive_payload_from_session_manager(
|
||||
self, payload: Any, port: Port, protocol: IPProtocol, session_id: str, from_network_interface: "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 +192,9 @@ 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_network_interface=from_network_interface, 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
|
||||
@@ -202,7 +219,7 @@ class SoftwareManager:
|
||||
software.operating_state.name,
|
||||
software.health_state_actual.name,
|
||||
software.port.value if software.port != Port.NONE else None,
|
||||
software.protocol.value
|
||||
software.protocol.value,
|
||||
]
|
||||
)
|
||||
print(table)
|
||||
|
||||
@@ -20,6 +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] = {}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
@@ -29,6 +30,14 @@ class ARP(Service):
|
||||
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.
|
||||
"""
|
||||
state = super().describe_state()
|
||||
state.update({str(ip): arp_entry.mac_address for ip, arp_entry in self.arp.items()})
|
||||
|
||||
return super().describe_state()
|
||||
|
||||
def show(self, markdown: bool = False):
|
||||
@@ -57,11 +66,7 @@ class ARP(Service):
|
||||
self.arp.clear()
|
||||
|
||||
def add_arp_cache_entry(
|
||||
self,
|
||||
ip_address: IPV4Address,
|
||||
mac_address: str,
|
||||
network_interface: NetworkInterface,
|
||||
override: bool = False
|
||||
self, ip_address: IPV4Address, mac_address: str, network_interface: NetworkInterface, override: bool = False
|
||||
):
|
||||
"""
|
||||
Add an ARP entry to the cache.
|
||||
@@ -139,7 +144,8 @@ class ARP(Service):
|
||||
)
|
||||
else:
|
||||
self.sys_log.error(
|
||||
"Cannot send ARP request as there is no outbound Network Interface to use. Try configuring the default gateway."
|
||||
"Cannot send ARP request as there is no outbound Network Interface to use. Try configuring the default "
|
||||
"gateway."
|
||||
)
|
||||
|
||||
def send_arp_reply(self, arp_reply: ARPPacket):
|
||||
@@ -147,12 +153,10 @@ class ARP(Service):
|
||||
Sends an ARP reply in response to an ARP request.
|
||||
|
||||
:param arp_reply: The ARP packet containing the reply.
|
||||
:param from_network_interface: The NIC from which the ARP reply is sent.
|
||||
"""
|
||||
|
||||
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} "
|
||||
@@ -162,14 +166,14 @@ class ARP(Service):
|
||||
payload=arp_reply,
|
||||
dst_ip_address=arp_reply.target_ip_address,
|
||||
dst_port=self.port,
|
||||
ip_protocol=self.protocol
|
||||
ip_protocol=self.protocol,
|
||||
)
|
||||
else:
|
||||
self.sys_log.error(
|
||||
"Cannot send ARP reply as there is no outbound Network Interface to use. Try configuring the default gateway."
|
||||
"Cannot send ARP reply as there is no outbound Network Interface to use. Try configuring the default "
|
||||
"gateway."
|
||||
)
|
||||
|
||||
|
||||
@abstractmethod
|
||||
def _process_arp_request(self, arp_packet: ARPPacket, from_network_interface: NetworkInterface):
|
||||
"""
|
||||
@@ -197,7 +201,7 @@ class ARP(Service):
|
||||
self.add_arp_cache_entry(
|
||||
ip_address=arp_packet.sender_ip_address,
|
||||
mac_address=arp_packet.sender_mac_addr,
|
||||
network_interface=from_network_interface
|
||||
network_interface=from_network_interface,
|
||||
)
|
||||
|
||||
def receive(self, payload: Any, session_id: str, **kwargs) -> bool:
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import secrets
|
||||
from ipaddress import IPv4Address
|
||||
from typing import Dict, Any, Union, Optional, Tuple
|
||||
from typing import Any, Dict, Optional, Tuple, Union
|
||||
|
||||
from primaite import getLogger
|
||||
from primaite.simulator.network.hardware.base import NetworkInterface
|
||||
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
|
||||
@@ -19,6 +20,7 @@ class ICMP(Service):
|
||||
Enables the sending and receiving of ICMP messages such as echo requests and replies. This is typically used for
|
||||
network diagnostics, notably the ping command.
|
||||
"""
|
||||
|
||||
request_replies: Dict = {}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
@@ -28,7 +30,12 @@ class ICMP(Service):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def describe_state(self) -> Dict:
|
||||
pass
|
||||
"""
|
||||
Produce a dictionary describing the current state of this object.
|
||||
|
||||
:return: Current state of this object and child objects.
|
||||
"""
|
||||
return super().describe_state()
|
||||
|
||||
def clear(self):
|
||||
"""
|
||||
@@ -56,9 +63,7 @@ class ICMP(Service):
|
||||
self.sys_log.info(f"Pinging {target_ip_address}:", to_terminal=True)
|
||||
sequence, identifier = 0, None
|
||||
while sequence < pings:
|
||||
sequence, identifier = self._send_icmp_echo_request(
|
||||
target_ip_address, sequence, identifier, pings
|
||||
)
|
||||
sequence, identifier = self._send_icmp_echo_request(target_ip_address, sequence, identifier, pings)
|
||||
request_replies = self.software_manager.icmp.request_replies.get(identifier)
|
||||
passed = request_replies == pings
|
||||
if request_replies:
|
||||
@@ -76,7 +81,7 @@ class ICMP(Service):
|
||||
return passed
|
||||
|
||||
def _send_icmp_echo_request(
|
||||
self, target_ip_address: IPv4Address, sequence: int = 0, identifier: Optional[int] = None, pings: int = 4
|
||||
self, target_ip_address: IPv4Address, sequence: int = 0, identifier: Optional[int] = None, pings: int = 4
|
||||
) -> Tuple[int, Union[int, None]]:
|
||||
"""
|
||||
Sends an ICMP echo request to a specified target IP address.
|
||||
@@ -91,7 +96,8 @@ class ICMP(Service):
|
||||
|
||||
if not network_interface:
|
||||
self.sys_log.error(
|
||||
"Cannot send ICMP echo request as there is no outbound Network Interface to use. Try configuring the default gateway."
|
||||
"Cannot send ICMP echo request as there is no outbound Network Interface to use. Try configuring the "
|
||||
"default gateway."
|
||||
)
|
||||
return pings, None
|
||||
|
||||
@@ -105,11 +111,11 @@ class ICMP(Service):
|
||||
dst_ip_address=target_ip_address,
|
||||
dst_port=self.port,
|
||||
ip_protocol=self.protocol,
|
||||
icmp_packet=icmp_packet
|
||||
icmp_packet=icmp_packet,
|
||||
)
|
||||
return sequence, icmp_packet.identifier
|
||||
|
||||
def _process_icmp_echo_request(self, frame: Frame, from_network_interface):
|
||||
def _process_icmp_echo_request(self, frame: Frame, from_network_interface: NetworkInterface):
|
||||
"""
|
||||
Processes an ICMP echo request received by the service.
|
||||
|
||||
@@ -121,11 +127,12 @@ class ICMP(Service):
|
||||
|
||||
network_interface = self.software_manager.session_manager.resolve_outbound_network_interface(
|
||||
frame.ip.src_ip_address
|
||||
)
|
||||
)
|
||||
|
||||
if not network_interface:
|
||||
self.sys_log.error(
|
||||
"Cannot send ICMP echo reply as there is no outbound Network Interface to use. Try configuring the default gateway."
|
||||
"Cannot send ICMP echo reply as there is no outbound Network Interface to use. Try configuring the "
|
||||
"default gateway."
|
||||
)
|
||||
return
|
||||
|
||||
@@ -143,7 +150,7 @@ class ICMP(Service):
|
||||
dst_ip_address=frame.ip.src_ip_address,
|
||||
dst_port=self.port,
|
||||
ip_protocol=self.protocol,
|
||||
icmp_packet=icmp_packet
|
||||
icmp_packet=icmp_packet,
|
||||
)
|
||||
|
||||
def _process_icmp_echo_reply(self, frame: Frame):
|
||||
@@ -159,7 +166,7 @@ class ICMP(Service):
|
||||
f"bytes={len(frame.payload)}, "
|
||||
f"time={time_str}, "
|
||||
f"TTL={frame.ip.ttl}",
|
||||
to_terminal=True
|
||||
to_terminal=True,
|
||||
)
|
||||
if not self.request_replies.get(frame.icmp.identifier):
|
||||
self.request_replies[frame.icmp.identifier] = 0
|
||||
|
||||
@@ -70,10 +70,6 @@ class NTPServer(Service):
|
||||
payload = payload.generate_reply(time)
|
||||
# send reply
|
||||
self.software_manager.session_manager.receive_payload_from_software_manager(
|
||||
payload=payload,
|
||||
src_port=self.port,
|
||||
dst_port=self.port,
|
||||
ip_protocol=self.protocol,
|
||||
session_id=session_id
|
||||
payload=payload, src_port=self.port, dst_port=self.port, ip_protocol=self.protocol, session_id=session_id
|
||||
)
|
||||
return True
|
||||
|
||||
@@ -380,7 +380,7 @@ class IOSoftware(Software):
|
||||
dest_ip_address=dest_ip_address,
|
||||
dest_port=dest_port,
|
||||
ip_protocol=ip_protocol,
|
||||
session_id=session_id
|
||||
session_id=session_id,
|
||||
)
|
||||
|
||||
@abstractmethod
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
from ipaddress import IPv4Address
|
||||
from typing import Any, Final
|
||||
|
||||
from pydantic import (
|
||||
BeforeValidator,
|
||||
)
|
||||
from pydantic import BeforeValidator
|
||||
from typing_extensions import Annotated
|
||||
|
||||
|
||||
@@ -30,7 +28,7 @@ def ipv4_validator(v: Any) -> IPv4Address:
|
||||
# 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.
|
||||
IPv4Address with with 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
|
||||
|
||||
Reference in New Issue
Block a user