#1706 - Tidies up the sysLog ARPCache, and ICMP classes and integrated them into the Node. Tidied up the base implementation of SoftwareManager and SessionManager. Tidies up the public API for Services and Applications. Added the SwitchPort and Switch classes. Added a basic test in test_frame_transmission.py that tests sending a frame from one node to another across a multi-switch network.

This commit is contained in:
Chris McCarthy
2023-08-07 19:33:52 +01:00
parent 04f1cb0dc6
commit 139d739732
15 changed files with 846 additions and 216 deletions

View File

@@ -13,8 +13,10 @@ 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 ICMPPacket, ICMPType, IPPacket, IPProtocol
from primaite.simulator.network.transmission.transport_layer import Port, TCPHeader
from primaite.simulator.system.packet_capture import PacketCapture
from primaite.simulator.system.sys_log import SysLog
from primaite.simulator.system.core.packet_capture import PacketCapture
from primaite.simulator.system.core.session_manager import SessionManager
from primaite.simulator.system.core.software_manager import SoftwareManager
from primaite.simulator.system.core.sys_log import SysLog
_LOGGER = getLogger(__name__)
@@ -103,8 +105,8 @@ class NIC(SimComponent):
kwargs["ip_address"] = IPv4Address(kwargs["ip_address"])
if not isinstance(kwargs["gateway"], IPv4Address):
kwargs["gateway"] = IPv4Address(kwargs["gateway"])
if "mac_address" not in kwargs:
kwargs["mac_address"] = generate_mac_address()
if "mac_address" not in kwargs:
kwargs["mac_address"] = generate_mac_address()
super().__init__(**kwargs)
if self.ip_address == self.gateway:
@@ -163,9 +165,9 @@ class NIC(SimComponent):
"""
if not self.connected_link:
if self.connected_link != link:
_LOGGER.info(f"NIC {self} connected to Link")
# TODO: Inform the Node that a link has been connected
self.connected_link = link
_LOGGER.info(f"NIC {self} connected to Link {link}")
else:
_LOGGER.warning(f"Cannot connect link to NIC ({self.mac_address}) as it is already connected")
else:
@@ -254,23 +256,155 @@ class NIC(SimComponent):
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[Switch] = None
"The Node to which the SwitchPort is connected."
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)
def enable(self):
"""Attempt to enable the SwitchPort."""
if not self.enabled:
if self.connected_node:
if self.connected_node.operating_state == NodeOperatingState.ON:
self.enabled = True
_LOGGER.info(f"SwitchPort {self} enabled")
self.pcap = PacketCapture(hostname=self.connected_node.hostname)
if self.connected_link:
self.connected_link.endpoint_up()
else:
_LOGGER.info(f"SwitchPort {self} cannot be enabled as the endpoint is not turned on")
else:
msg = f"SwitchPort {self} cannot be enabled as it is not connected to a Node"
_LOGGER.error(msg)
raise NetworkError(msg)
def disable(self):
"""Disable the SwitchPort."""
if self.enabled:
self.enabled = False
_LOGGER.info(f"SwitchPort {self} disabled")
if self.connected_link:
self.connected_link.endpoint_down()
def connect_link(self, link: Link):
"""
Connect the SwitchPort to a link.
:param link: The link to which the SwitchPort is connected.
:raise NetworkError: When an attempt to connect a Link is made while the SwitchPort has a connected Link.
"""
if not self.connected_link:
if self.connected_link != link:
# TODO: Inform the Switch that a link has been connected
self.connected_link = link
_LOGGER.info(f"SwitchPort {self} connected to Link {link}")
self.enable()
else:
_LOGGER.warning(f"Cannot connect link to SwitchPort ({self.mac_address}) as it is already connected")
else:
msg = f"Cannot connect link to SwitchPort ({self.mac_address}) as it already has a connection"
_LOGGER.error(msg)
raise NetworkError(msg)
def disconnect_link(self):
"""Disconnect the SwitchPort 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 send_frame(self, frame: Frame) -> bool:
"""
Send a network frame from the SwitchPort to the connected link.
:param frame: The network frame to be sent.
"""
if self.enabled:
self.pcap.capture(frame)
self.connected_link.transmit_frame(sender_nic=self, frame=frame)
return True
else:
# Cannot send Frame as the SwitchPort is not enabled
return False
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.
:param frame: The network frame being received.
"""
if self.enabled:
frame.decrement_ttl()
self.pcap.capture(frame)
self.connected_node.forward_frame(frame=frame, incoming_port=self)
return True
else:
return False
def describe_state(self) -> Dict:
"""
Get the current state of the SwitchPort as a dict.
:return: A dict containing the current state of the SwitchPort.
"""
pass
def apply_action(self, action: str):
"""
Apply an action to the SwitchPort.
:param action: The action to be applied.
:type action: str
"""
pass
def __str__(self) -> str:
return f"{self.mac_address}"
class Link(SimComponent):
"""
Represents a network link between two network interface cards (NICs).
Represents a network link between NIC<-->, NIC<-->SwitchPort, or SwitchPort<-->SwitchPort.
:param endpoint_a: The first NIC connected to the Link.
:type endpoint_a: NIC
:param endpoint_b: The second NIC connected to the Link.
:type endpoint_b: NIC
:param endpoint_a: The first NIC or SwitchPort connected to the Link.
:param endpoint_b: The second NIC or SwitchPort connected to the Link.
:param bandwidth: The bandwidth of the Link in Mbps (default is 100 Mbps).
:type bandwidth: int
"""
endpoint_a: NIC
"The first NIC connected to the Link."
endpoint_b: NIC
"The second NIC connected to the Link."
bandwidth: int = 100
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."
bandwidth: float = 100.0
"The bandwidth of the Link in Mbps (default is 100 Mbps)."
current_load: float = 0.0
"The current load on the link in Mbps."
@@ -284,7 +418,7 @@ class Link(SimComponent):
:raises ValueError: If endpoint_a and endpoint_b are the same NIC.
"""
if kwargs["endpoint_a"] == kwargs["endpoint_b"]:
msg = "endpoint_a and endpoint_b cannot be the same NIC"
msg = "endpoint_a and endpoint_b cannot be the same NIC or SwitchPort"
_LOGGER.error(msg)
raise ValueError(msg)
super().__init__(**kwargs)
@@ -292,6 +426,11 @@ class Link(SimComponent):
self.endpoint_b.connect_link(self)
self.endpoint_up()
@property
def current_load_percent(self) -> str:
"""Get the current load formatted as a percentage string."""
return f"{self.current_load / self.bandwidth:.5f}%"
def endpoint_up(self):
"""Let the Link know and endpoint has been brought up."""
if self.up:
@@ -318,25 +457,30 @@ class Link(SimComponent):
return self.current_load + frame_size_Mbits <= self.bandwidth
return False
def transmit_frame(self, sender_nic: NIC, frame: Frame) -> bool:
def transmit_frame(self, sender_nic: Union[NIC, SwitchPort], frame: Frame) -> bool:
"""
Send a network frame from one NIC to another connected NIC.
Send a network frame from one NIC or SwitchPort to another connected NIC or SwitchPort.
:param sender_nic: The NIC sending the frame.
:param sender_nic: The NIC or SwitchPort sending the frame.
:param frame: The network frame to be sent.
:return: True if the Frame can be sent, otherwise False.
"""
if self._can_transmit(frame):
receiver_nic = self.endpoint_a
if receiver_nic == sender_nic:
receiver_nic = self.endpoint_b
receiver = self.endpoint_a
if receiver == sender_nic:
receiver = self.endpoint_b
frame_size = frame.size_Mbits
sent = receiver_nic.receive_frame(frame)
sent = receiver.receive_frame(frame)
if sent:
# Frame transmitted successfully
# Load the frame size on the link
self.current_load += frame_size
_LOGGER.info(f"Added {frame_size:.3f} Mbits to {self}, current load {self.current_load:.3f} Mbits")
(
_LOGGER.info(
f"Added {frame_size:.3f} Mbits to {self}, current load {self.current_load:.3f} Mbits "
f"({self.current_load_percent})"
)
)
return True
# Received NIC disabled, reply
@@ -345,7 +489,7 @@ class Link(SimComponent):
_LOGGER.info(f"Cannot transmit frame as {self} is at capacity")
return False
def reset_component_for_episode(self):
def reset_component_for_episode(self, episode: int):
"""
Link reset function.
@@ -356,7 +500,7 @@ class Link(SimComponent):
def describe_state(self) -> Dict:
"""
Get the current state of the Libk as a dict.
Get the current state of the Link as a dict.
:return: A dict containing the current state of the Link.
"""
@@ -375,108 +519,23 @@ class Link(SimComponent):
return f"{self.endpoint_a}<-->{self.endpoint_b}"
class NodeOperatingState(Enum):
"""Enumeration of Node Operating States."""
OFF = 0
"The node is powered off."
ON = 1
"The node is powered on."
SHUTTING_DOWN = 2
"The node is in the process of shutting down."
BOOTING = 3
"The node is in the process of booting up."
class Node(SimComponent):
class ARPCache:
"""
A basic Node class.
The ARPCache (Address Resolution Protocol) class.
:param hostname: The node hostname on the network.
:param operating_state: The node operating state.
Responsible for maintaining a mapping between IP addresses and MAC addresses (ARP cache) for the network. It
provides methods for looking up, adding, and removing entries, and for processing ARPPackets.
"""
hostname: str
"The node hostname on the network."
operating_state: NodeOperatingState = NodeOperatingState.OFF
"The hardware state of the node."
nics: Dict[str, NIC] = {}
"The NICs on the node."
accounts: Dict = {}
"All accounts on the node."
applications: Dict = {}
"All applications on the node."
services: Dict = {}
"All services on the node."
processes: Dict = {}
"All processes on the node."
file_system: Any = None
"The nodes file system."
arp_cache: Dict[IPv4Address, ARPEntry] = {}
"The ARP cache."
sys_log: Optional[SysLog] = None
revealed_to_red: bool = False
"Informs whether the node has been revealed to a red agent."
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.sys_log = SysLog(self.hostname)
def turn_on(self):
"""Turn on the Node."""
if self.operating_state == NodeOperatingState.OFF:
self.operating_state = NodeOperatingState.ON
self.sys_log.info("Turned on")
for nic in self.nics.values():
nic.enable()
def turn_off(self):
"""Turn off the Node."""
if self.operating_state == NodeOperatingState.ON:
for nic in self.nics.values():
nic.disable()
self.operating_state = NodeOperatingState.OFF
self.sys_log.info("Turned off")
def connect_nic(self, nic: NIC):
def __init__(self, sys_log: "SysLog"):
"""
Connect a NIC.
Initialize an ARP (Address Resolution Protocol) cache.
:param nic: The NIC to connect.
:raise NetworkError: If the NIC is already connected.
:param sys_log: The nodes sys log.
"""
if nic.uuid not in self.nics:
self.nics[nic.uuid] = nic
nic.connected_node = self
self.sys_log.info(f"Connected NIC {nic}")
if self.operating_state == NodeOperatingState.ON:
nic.enable()
else:
msg = f"Cannot connect NIC {nic} 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]):
"""
Disconnect a NIC.
:param nic: The NIC to Disconnect.
: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:
self.nics.pop(nic.uuid)
nic.disable()
self.sys_log.info(f"Disconnected NIC {nic}")
else:
msg = f"Cannot disconnect NIC {nic} as it is not connected"
self.sys_log.logger.error(msg)
_LOGGER.error(msg)
raise NetworkError(msg)
self.sys_log: "SysLog" = sys_log
self.arp: Dict[IPv4Address, ARPEntry] = {}
self.nics: Dict[str, "NIC"] = {}
def _add_arp_cache_entry(self, ip_address: IPv4Address, mac_address: str, nic: NIC):
"""
@@ -488,7 +547,7 @@ class Node(SimComponent):
"""
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.arp_cache[ip_address] = arp_entry
self.arp[ip_address] = arp_entry
def _remove_arp_cache_entry(self, ip_address: IPv4Address):
"""
@@ -496,37 +555,44 @@ class Node(SimComponent):
:param ip_address: The IP address to be removed from the cache.
"""
if ip_address in self.arp_cache:
del self.arp_cache[ip_address]
if ip_address in self.arp:
del self.arp[ip_address]
def _get_arp_cache_mac_address(self, ip_address: IPv4Address) -> Optional[str]:
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.
"""
arp_entry = self.arp_cache.get(ip_address)
arp_entry = self.arp.get(ip_address)
if arp_entry:
return arp_entry.mac_address
def _get_arp_cache_nic(self, ip_address: IPv4Address) -> Optional[NIC]:
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.
"""
arp_entry = self.arp_cache.get(ip_address)
arp_entry = self.arp.get(ip_address)
if arp_entry:
return self.nics[arp_entry.nic_uuid]
def _clear_arp_cache(self):
"""Clear the entire ARP cache."""
self.arp_cache.clear()
def clear_arp_cache(self):
"""Clear the entire ARP cache, removing all stored entries."""
self.arp.clear()
def _send_arp_request(self, target_ip_address: Union[IPv4Address, str]):
"""Perform a standard ARP request for a given target IP address."""
def send_arp_request(self, target_ip_address: Union[IPv4Address, str]):
"""
Perform a standard ARP request for a given target IP address.
Broadcasts the request through all enabled NICs to determine the MAC address corresponding to the target IP
address.
:param target_ip_address: The target IP address to send an ARP request for.
"""
for nic in self.nics.values():
if nic.enabled:
self.sys_log.info(f"Sending ARP request from NIC {nic} for ip {target_ip_address}")
@@ -547,12 +613,13 @@ class Node(SimComponent):
def process_arp_packet(self, from_nic: NIC, arp_packet: ARPPacket):
"""
Process an ARP packet.
Process a received ARP packet, handling both ARP requests and responses.
# TODO: This will become a service that sits on the Node.
If an ARP request is received for the local IP, a response is sent back.
If an ARP response is received, the ARP cache is updated with the new entry.
:param from_nic: The NIC the arp packet was received at.
:param arp_packet:The ARP packet to process.
:param from_nic: The NIC that received the ARP packet.
:param arp_packet: The ARP packet to be processed.
"""
if arp_packet.request:
self.sys_log.info(
@@ -581,7 +648,7 @@ class Node(SimComponent):
src_mac_addr=arp_packet.sender_mac_addr, dst_mac_addr=arp_packet.target_mac_addr
)
frame = Frame(ethernet=ethernet_header, ip=ip_packet, tcp=tcp_header, arp=arp_packet)
self.send_frame(frame)
from_nic.send_frame(frame)
else:
self.sys_log.info(f"Ignoring ARP request for {arp_packet.target_ip}")
else:
@@ -592,18 +659,34 @@ class Node(SimComponent):
ip_address=arp_packet.sender_ip, mac_address=arp_packet.sender_mac_addr, nic=from_nic
)
class ICMP:
"""
The ICMP (Internet Control Message Protocol) class.
Provides functionalities for managing and handling ICMP packets, including echo requests and replies.
"""
def __init__(self, sys_log: SysLog, arp_cache: ARPCache):
"""
Initialize the ICMP (Internet Control Message Protocol) service.
:param sys_log: The system log to store system messages and information.
:param arp_cache: The ARP cache for resolving IP to MAC address mappings.
"""
self.sys_log: SysLog = sys_log
self.arp: ARPCache = arp_cache
def process_icmp(self, frame: Frame):
"""
Process an ICMP packet.
Process an ICMP packet, including handling echo requests and replies.
# TODO: This will become a service that sits on the Node.
:param frame: The Frame containing the icmp packet to process.
:param frame: The Frame containing the ICMP packet to process.
"""
if frame.icmp.icmp_type == ICMPType.ECHO_REQUEST:
self.sys_log.info(f"Received echo request from {frame.ip.src_ip}")
target_mac_address = self._get_arp_cache_mac_address(frame.ip.src_ip)
src_nic = self._get_arp_cache_nic(frame.ip.src_ip)
target_mac_address = self.arp.get_arp_cache_mac_address(frame.ip.src_ip)
src_nic = self.arp.get_arp_cache_nic(frame.ip.src_ip)
tcp_header = TCPHeader(src_port=Port.ARP, dst_port=Port.ARP)
# Network Layer
@@ -617,19 +700,28 @@ class Node(SimComponent):
sequence=frame.icmp.sequence + 1,
)
frame = Frame(ethernet=ethernet_header, ip=ip_packet, tcp=tcp_header, icmp=icmp_reply_packet)
self.sys_log.info(f"Sending echo reply to {frame.ip.src_ip}")
self.sys_log.info(f"Sending echo reply to {frame.ip.dst_ip}")
src_nic.send_frame(frame)
elif frame.icmp.icmp_type == ICMPType.ECHO_REPLY:
self.sys_log.info(f"Received echo reply from {frame.ip.src_ip}")
def _ping(
def ping(
self, target_ip_address: IPv4Address, sequence: int = 0, identifier: Optional[int] = None
) -> Tuple[int, Union[int, None]]:
nic = self._get_arp_cache_nic(target_ip_address)
"""
Send an ICMP echo request (ping) to a target IP address and manage the sequence and identifier.
:param target_ip_address: The target IP address to send the ping.
:param sequence: The sequence number of the echo request. Defaults to 0.
:param identifier: An optional identifier for the ICMP packet. If None, a default will be used.
:return: A tuple containing the next sequence number and the identifier, or (0, None) if the target IP address
was not found in the ARP cache.
"""
nic = self.arp.get_arp_cache_nic(target_ip_address)
if nic:
sequence += 1
target_mac_address = self._get_arp_cache_mac_address(target_ip_address)
src_nic = self._get_arp_cache_nic(target_ip_address)
target_mac_address = self.arp.get_arp_cache_mac_address(target_ip_address)
src_nic = self.arp.get_arp_cache_nic(target_ip_address)
tcp_header = TCPHeader(src_port=Port.ARP, dst_port=Port.ARP)
# Network Layer
@@ -647,17 +739,143 @@ class Node(SimComponent):
return sequence, icmp_packet.identifier
else:
self.sys_log.info(f"No entry in ARP cache for {target_ip_address}")
self._send_arp_request(target_ip_address)
self.arp.send_arp_request(target_ip_address)
return 0, None
class NodeOperatingState(Enum):
"""Enumeration of Node Operating States."""
OFF = 0
"The node is powered off."
ON = 1
"The node is powered on."
SHUTTING_DOWN = 2
"The node is in the process of shutting down."
BOOTING = 3
"The node is in the process of booting up."
class Node(SimComponent):
"""
A basic Node class that represents a node on the network.
This class manages the state of the node, including the NICs (Network Interface Cards), accounts, applications,
services, processes, file system, and various managers like ARP, ICMP, SessionManager, and SoftwareManager.
:param hostname: The node hostname on the network.
:param operating_state: The node operating state, either ON or OFF.
"""
hostname: str
"The node hostname on the network."
operating_state: NodeOperatingState = NodeOperatingState.OFF
"The hardware state of the node."
nics: Dict[str, NIC] = {}
"The NICs on the node."
accounts: Dict = {}
"All accounts on the node."
applications: Dict = {}
"All applications on the node."
services: Dict = {}
"All services on the node."
processes: Dict = {}
"All processes on the node."
file_system: Any = None
"The nodes file system."
sys_log: SysLog
arp: ARPCache
icmp: ICMP
session_manager: SessionManager
software_manager: SoftwareManager
revealed_to_red: bool = False
"Informs whether the node has been revealed to a red agent."
def __init__(self, **kwargs):
"""
Initialize the Node with various components and managers.
This method initializes the ARP cache, ICMP handler, session manager, and software manager if they are not
provided.
"""
if not kwargs.get("sys_log"):
kwargs["sys_log"] = SysLog(kwargs["hostname"])
if not kwargs.get("arp_cache"):
kwargs["arp"] = ARPCache(sys_log=kwargs.get("sys_log"))
if not kwargs.get("icmp"):
kwargs["icmp"] = ICMP(sys_log=kwargs.get("sys_log"), arp_cache=kwargs.get("arp"))
if not kwargs.get("session_manager"):
kwargs["session_manager"] = SessionManager(sys_log=kwargs.get("sys_log"), arp_cache=kwargs.get("arp"))
if not kwargs.get("software_manager"):
kwargs["software_manager"] = SoftwareManager(
sys_log=kwargs.get("sys_log"), session_manager=kwargs.get("session_manager")
)
super().__init__(**kwargs)
self.arp.nics = self.nics
def turn_on(self):
"""Turn on the Node, enabling its NICs if it is in the OFF state."""
if self.operating_state == NodeOperatingState.OFF:
self.operating_state = NodeOperatingState.ON
self.sys_log.info("Turned on")
for nic in self.nics.values():
nic.enable()
def turn_off(self):
"""Turn 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()
self.operating_state = NodeOperatingState.OFF
self.sys_log.info("Turned off")
def connect_nic(self, nic: NIC):
"""
Connect a NIC (Network Interface Card) to the node.
:param nic: The NIC to connect.
:raise NetworkError: If the NIC is already connected.
"""
if nic.uuid not in self.nics:
self.nics[nic.uuid] = nic
nic.connected_node = self
self.sys_log.info(f"Connected NIC {nic}")
if self.operating_state == NodeOperatingState.ON:
nic.enable()
else:
msg = f"Cannot connect NIC {nic} 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]):
"""
Disconnect a NIC (Network Interface Card) from the node.
:param nic: 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:
self.nics.pop(nic.uuid)
nic.disable()
self.sys_log.info(f"Disconnected NIC {nic}")
else:
msg = f"Cannot disconnect NIC {nic} 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:
"""
Ping an IP address.
Performs a standard ICMP echo request/response four times.
Ping an IP address, performing a standard ICMP echo request/response.
:param target_ip_address: The target IP address to ping.
:return: True if successful, otherwise False.
:param pings: The number of pings to attempt, default is 4.
:return: True if the ping is successful, otherwise False.
"""
if not isinstance(target_ip_address, IPv4Address):
target_ip_address = IPv4Address(target_ip_address)
@@ -665,7 +883,7 @@ class Node(SimComponent):
self.sys_log.info(f"Attempting to ping {target_ip_address}")
sequence, identifier = 0, None
while sequence < pings:
sequence, identifier = self._ping(target_ip_address, sequence, identifier)
sequence, identifier = self.icmp.ping(target_ip_address, sequence, identifier)
return True
self.sys_log.info("Ping failed as the node is turned off")
return False
@@ -681,20 +899,101 @@ class Node(SimComponent):
def receive_frame(self, frame: Frame, from_nic: NIC):
"""
Receive a Frame from the connected NIC.
Receive a Frame from the connected NIC and process it.
The Frame is passed to up to the SessionManager.
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_nic: The NIC that received the frame.
"""
if frame.ip.protocol == IPProtocol.TCP:
if frame.tcp.src_port == Port.ARP:
self.process_arp_packet(from_nic=from_nic, arp_packet=frame.arp)
self.arp.process_arp_packet(from_nic=from_nic, arp_packet=frame.arp)
elif frame.ip.protocol == IPProtocol.UDP:
pass
elif frame.ip.protocol == IPProtocol.ICMP:
self.process_icmp(frame=frame)
self.icmp.process_icmp(frame=frame)
def describe_state(self) -> Dict:
"""Describe the state of a Node."""
"""
Describe the state of the Node.
:return: A dictionary representing the state of the node.
"""
pass
class Switch(Node):
"""A class representing a Layer 2 network switch."""
num_ports: int = 24
"The number of ports on the switch."
switch_ports: Dict[int, SwitchPort] = {}
"The SwitchPorts on the switch."
dst_mac_table: Dict[str, SwitchPort] = {}
"A MAC address table mapping destination MAC addresses to corresponding SwitchPorts."
def describe_state(self) -> Dict:
"""TODO."""
pass
def __init__(self, **kwargs):
super().__init__(**kwargs)
if not self.switch_ports:
self.switch_ports = {i: SwitchPort() for i in range(1, self.num_ports + 1)}
for port_num, port in self.switch_ports.items():
port.connected_node = self
port.port_num = port_num
def _add_mac_table_entry(self, mac_address: str, switch_port: SwitchPort):
mac_table_port = self.dst_mac_table.get(mac_address)
if not mac_table_port:
self.dst_mac_table[mac_address] = switch_port
self.sys_log.info(f"Added MAC table entry: Port {switch_port.port_num} -> {mac_address}")
else:
if mac_table_port != switch_port:
self.dst_mac_table.pop(mac_address)
self.sys_log.info(f"Removed MAC table entry: Port {mac_table_port.port_num} -> {mac_address}")
self._add_mac_table_entry(mac_address, switch_port)
def forward_frame(self, frame: Frame, incoming_port: SwitchPort):
"""
Forward a frame to the appropriate port based on the destination MAC address.
:param frame: The Frame to be forwarded.
:param incoming_port: The port number from which the frame was received.
"""
src_mac = frame.ethernet.src_mac_addr
dst_mac = frame.ethernet.dst_mac_addr
self._add_mac_table_entry(src_mac, incoming_port)
outgoing_port = self.dst_mac_table.get(dst_mac)
if outgoing_port or dst_mac != "ff:ff:ff:ff:ff:ff":
outgoing_port.send_frame(frame)
else:
# If the destination MAC is not in the table, flood to all ports except incoming
for port in self.switch_ports.values():
if port != incoming_port:
port.send_frame(frame)
def disconnect_link_from_port(self, link: Link, port_number: int):
"""
Disconnect a given link from the specified port number on the switch.
:param link: The Link object to be disconnected.
:param port_number: The port number on the switch from where the link should be disconnected.
:raise NetworkError: When an invalid port number is provided or the link does not match the connection.
"""
port = self.switch_ports.get(port_number)
if port is None:
msg = f"Invalid port number {port_number} on the switch"
_LOGGER.error(msg)
raise NetworkError(msg)
if port.connected_link != link:
msg = f"The link does not match the connection at port number {port_number}"
_LOGGER.error(msg)
raise NetworkError(msg)
port.disconnect_link()