#2248 - Big refactor of base with all Network Interface subclasses created to allow for proper management of ports on devices as it was starting to get messy with the Router. Some routing tests still need fixing as ARP doesn't seem to be working properly

This commit is contained in:
Chris McCarthy
2024-02-05 08:44:10 +00:00
parent a0253ce6c4
commit 7bbfd564fb
74 changed files with 1806 additions and 1192 deletions

View File

@@ -1,17 +1,16 @@
from __future__ import annotations
from abc import abstractmethod
from ipaddress import IPv4Address
from typing import Any, Dict, Optional, Union
from prettytable import MARKDOWN, PrettyTable
from primaite.simulator.network.hardware.base import NIC
from primaite.simulator.network.hardware.base import NetworkInterface
from primaite.simulator.network.protocols.arp import ARPEntry, ARPPacket
from primaite.simulator.network.transmission.data_link_layer import EthernetHeader, Frame
from primaite.simulator.network.transmission.network_layer import IPPacket, IPProtocol
from primaite.simulator.network.transmission.transport_layer import Port, UDPHeader
from primaite.simulator.network.transmission.network_layer import IPProtocol
from primaite.simulator.network.transmission.transport_layer import Port
from primaite.simulator.system.services.service import Service
from primaite.utils.validators import IPV4Address
class ARP(Service):
@@ -21,7 +20,7 @@ class ARP(Service):
Manages ARP for resolving network layer addresses into link layer addresses. It maintains an ARP cache,
sends ARP requests and replies, and processes incoming ARP packets.
"""
arp: Dict[IPv4Address, ARPEntry] = {}
arp: Dict[IPV4Address, ARPEntry] = {}
def __init__(self, **kwargs):
kwargs["name"] = "ARP"
@@ -30,7 +29,7 @@ class ARP(Service):
super().__init__(**kwargs)
def describe_state(self) -> Dict:
pass
return super().describe_state()
def show(self, markdown: bool = False):
"""
@@ -48,7 +47,7 @@ class ARP(Service):
[
str(ip),
arp.mac_address,
self.software_manager.node.nics[arp.nic_uuid].mac_address,
self.software_manager.node.network_interfaces[arp.network_interface_uuid].mac_address,
]
)
print(table)
@@ -57,7 +56,13 @@ class ARP(Service):
"""Clears the arp cache."""
self.arp.clear()
def add_arp_cache_entry(self, ip_address: IPv4Address, mac_address: str, nic: NIC, override: bool = False):
def add_arp_cache_entry(
self,
ip_address: IPV4Address,
mac_address: str,
network_interface: NetworkInterface,
override: bool = False
):
"""
Add an ARP entry to the cache.
@@ -66,20 +71,20 @@ class ARP(Service):
:param ip_address: The IP address to be added to the cache.
:param mac_address: The MAC address associated with the IP address.
:param nic: The NIC through which the NIC with the IP address is reachable.
:param network_interface: The NIC through which the NIC with the IP address is reachable.
:param override: If True, an existing entry for the IP address will be overridden. Default is False.
"""
for _nic in self.software_manager.node.nics.values():
if _nic.ip_address == ip_address:
for _network_interface in self.software_manager.node.network_interfaces.values():
if _network_interface.ip_address == ip_address:
return
if override or not self.arp.get(ip_address):
self.sys_log.info(f"Adding ARP cache entry for {mac_address}/{ip_address} via NIC {nic}")
arp_entry = ARPEntry(mac_address=mac_address, nic_uuid=nic.uuid)
self.sys_log.info(f"Adding ARP cache entry for {mac_address}/{ip_address} via NIC {network_interface}")
arp_entry = ARPEntry(mac_address=mac_address, network_interface_uuid=network_interface.uuid)
self.arp[ip_address] = arp_entry
@abstractmethod
def get_arp_cache_mac_address(self, ip_address: IPv4Address) -> Optional[str]:
def get_arp_cache_mac_address(self, ip_address: IPV4Address) -> Optional[str]:
"""
Retrieves the MAC address associated with a given IP address from the ARP cache.
@@ -89,7 +94,7 @@ class ARP(Service):
pass
@abstractmethod
def get_arp_cache_nic(self, ip_address: IPv4Address) -> Optional[NIC]:
def get_arp_cache_network_interface(self, ip_address: IPV4Address) -> Optional[NetworkInterface]:
"""
Retrieves the NIC associated with a given IP address from the ARP cache.
@@ -98,18 +103,20 @@ class ARP(Service):
"""
pass
def send_arp_request(self, target_ip_address: Union[IPv4Address, str]):
def send_arp_request(self, target_ip_address: Union[IPV4Address, str]):
"""
Sends an ARP request to resolve the MAC address of a target IP address.
:param target_ip_address: The target IP address for which the MAC address is being requested.
"""
outbound_nic = self.software_manager.session_manager.resolve_outbound_nic(target_ip_address)
if outbound_nic:
self.sys_log.info(f"Sending ARP request from NIC {outbound_nic} for ip {target_ip_address}")
outbound_network_interface = self.software_manager.session_manager.resolve_outbound_network_interface(
target_ip_address
)
if outbound_network_interface:
self.sys_log.info(f"Sending ARP request from NIC {outbound_network_interface} for ip {target_ip_address}")
arp_packet = ARPPacket(
sender_ip_address=outbound_nic.ip_address,
sender_mac_addr=outbound_nic.mac_address,
sender_ip_address=outbound_network_interface.ip_address,
sender_mac_addr=outbound_network_interface.mac_address,
target_ip_address=target_ip_address,
)
self.software_manager.session_manager.receive_payload_from_software_manager(
@@ -125,11 +132,13 @@ class ARP(Service):
Sends an ARP reply in response to an ARP request.
:param arp_reply: The ARP packet containing the reply.
:param from_nic: The NIC from which the ARP reply is sent.
:param from_network_interface: The NIC from which the ARP reply is sent.
"""
outbound_nic = self.software_manager.session_manager.resolve_outbound_nic(arp_reply.target_ip_address)
if outbound_nic:
outbound_network_interface = self.software_manager.session_manager.resolve_outbound_network_interface(
arp_reply.target_ip_address
)
if outbound_network_interface:
self.sys_log.info(
f"Sending ARP reply from {arp_reply.sender_mac_addr}/{arp_reply.sender_ip_address} "
f"to {arp_reply.target_ip_address}/{arp_reply.target_mac_addr} "
@@ -147,31 +156,33 @@ class ARP(Service):
@abstractmethod
def _process_arp_request(self, arp_packet: ARPPacket, from_nic: NIC):
def _process_arp_request(self, arp_packet: ARPPacket, from_network_interface: NIC):
"""
Processes an incoming ARP request.
:param arp_packet: The ARP packet containing the request.
:param from_nic: The NIC that received the ARP request.
:param from_network_interface: The NIC that received the ARP request.
"""
self.sys_log.info(
f"Received ARP request for {arp_packet.target_ip_address} from "
f"{arp_packet.sender_mac_addr}/{arp_packet.sender_ip_address} "
)
def _process_arp_reply(self, arp_packet: ARPPacket, from_nic: NIC):
def _process_arp_reply(self, arp_packet: ARPPacket, from_network_interface: NIC):
"""
Processes an incoming ARP reply.
:param arp_packet: The ARP packet containing the reply.
:param from_nic: The NIC that received the ARP reply.
:param from_network_interface: The NIC that received the ARP reply.
"""
self.sys_log.info(
f"Received ARP response for {arp_packet.sender_ip_address} "
f"from {arp_packet.sender_mac_addr} via NIC {from_nic}"
f"from {arp_packet.sender_mac_addr} via Network Interface {from_network_interface}"
)
self.add_arp_cache_entry(
ip_address=arp_packet.sender_ip_address, mac_address=arp_packet.sender_mac_addr, nic=from_nic
ip_address=arp_packet.sender_ip_address,
mac_address=arp_packet.sender_mac_addr,
network_interface=from_network_interface
)
def receive(self, payload: Any, session_id: str, **kwargs) -> bool:
@@ -183,15 +194,15 @@ class ARP(Service):
:param kwargs: Additional keyword arguments.
:return: True if the payload was processed successfully, otherwise False.
"""
if not isinstance(payload, ARPPacket):
print("failied on payload check", type(payload))
if not super().receive(payload, session_id, **kwargs):
return False
from_nic = kwargs.get("from_nic")
from_network_interface = kwargs.get("from_network_interface")
if payload.request:
self._process_arp_request(arp_packet=payload, from_nic=from_nic)
self._process_arp_request(arp_packet=payload, from_network_interface=from_network_interface)
else:
self._process_arp_reply(arp_packet=payload, from_nic=from_nic)
self._process_arp_reply(arp_packet=payload, from_network_interface=from_network_interface)
return True
def __contains__(self, item: Any) -> bool:
"""

View File

@@ -1,95 +0,0 @@
from __future__ import annotations
from ipaddress import IPv4Address
from typing import Optional
from primaite.simulator.network.hardware.base import NIC
from primaite.simulator.system.services.arp.arp import ARP, ARPPacket
class HostARP(ARP):
def get_default_gateway_mac_address(self) -> Optional[str]:
if self.software_manager.node.default_gateway:
return self.get_arp_cache_mac_address(self.software_manager.node.default_gateway)
def get_default_gateway_nic(self) -> Optional[NIC]:
if self.software_manager.node.default_gateway:
return self.get_arp_cache_nic(self.software_manager.node.default_gateway)
def _get_arp_cache_mac_address(
self, ip_address: IPv4Address, is_reattempt: bool = False, is_default_gateway_attempt: bool = False
) -> Optional[str]:
arp_entry = self.arp.get(ip_address)
if arp_entry:
return arp_entry.mac_address
else:
if not is_reattempt:
self.send_arp_request(ip_address)
return self._get_arp_cache_mac_address(
ip_address=ip_address, is_reattempt=True, is_default_gateway_attempt=is_default_gateway_attempt
)
else:
if self.software_manager.node.default_gateway:
if not is_default_gateway_attempt:
self.send_arp_request(self.software_manager.node.default_gateway)
return self._get_arp_cache_mac_address(
ip_address=self.software_manager.node.default_gateway, is_reattempt=True, is_default_gateway_attempt=True
)
return None
def get_arp_cache_mac_address(self, ip_address: IPv4Address) -> Optional[str]:
"""
Get the MAC address associated with an IP address.
:param ip_address: The IP address to look up in the cache.
:return: The MAC address associated with the IP address, or None if not found.
"""
return self._get_arp_cache_mac_address(ip_address)
def _get_arp_cache_nic(
self, ip_address: IPv4Address, is_reattempt: bool = False, is_default_gateway_attempt: bool = False
) -> Optional[NIC]:
arp_entry = self.arp.get(ip_address)
if arp_entry:
return self.software_manager.node.nics[arp_entry.nic_uuid]
else:
if not is_reattempt:
self.send_arp_request(ip_address)
return self._get_arp_cache_nic(
ip_address=ip_address, is_reattempt=True, is_default_gateway_attempt=is_default_gateway_attempt
)
else:
if self.software_manager.node.default_gateway:
if not is_default_gateway_attempt:
self.send_arp_request(self.software_manager.node.default_gateway)
return self._get_arp_cache_nic(
ip_address=self.software_manager.node.default_gateway, is_reattempt=True, is_default_gateway_attempt=True
)
return None
def get_arp_cache_nic(self, ip_address: IPv4Address) -> Optional[NIC]:
"""
Get the NIC associated with an IP address.
:param ip_address: The IP address to look up in the cache.
:return: The NIC associated with the IP address, or None if not found.
"""
return self._get_arp_cache_nic(ip_address)
def _process_arp_request(self, arp_packet: ARPPacket, from_nic: NIC):
super()._process_arp_request(arp_packet, from_nic)
# Unmatched ARP Request
if arp_packet.target_ip_address != from_nic.ip_address:
self.sys_log.info(
f"Ignoring ARP request for {arp_packet.target_ip_address}. Current IP address is {from_nic.ip_address}"
)
return
# Matched ARP request
self.add_arp_cache_entry(
ip_address=arp_packet.sender_ip_address, mac_address=arp_packet.sender_mac_addr, nic=from_nic
)
arp_packet = arp_packet.generate_reply(from_nic.mac_address)
self.send_arp_reply(arp_packet)

View File

@@ -1,98 +1,78 @@
# class RouterARPCache(ARPCache):
# from ipaddress import IPv4Address
# from typing import Optional, Any
#
# from primaite.simulator.network.hardware.nodes.network.router import RouterInterface, Router
# from primaite.simulator.network.protocols.arp import ARPPacket
# from primaite.simulator.network.transmission.data_link_layer import Frame
# from primaite.simulator.system.services.arp.arp import ARP
#
#
# class RouterARP(ARP):
# """
# Inherits from ARPCache and adds router-specific ARP packet processing.
#
# :ivar SysLog sys_log: A system log for logging messages.
# :ivar Router router: The router to which this ARP cache belongs.
# """
# router: Router
#
# def __init__(self, sys_log: SysLog, router: Router):
# super().__init__(sys_log)
# self.router: Router = router
# def get_arp_cache_mac_address(self, ip_address: IPv4Address) -> Optional[str]:
# arp_entry = self.arp.get(ip_address)
#
# def process_arp_packet(
# self, from_nic: NIC, frame: Frame, route_table: RouteTable, is_reattempt: bool = False
# ) -> None:
# """
# Processes a received ARP (Address Resolution Protocol) packet in a router-specific way.
# if arp_entry:
# return arp_entry.mac_address
# return None
#
# This method is responsible for handling both ARP requests and responses. It processes ARP packets received on a
# Network Interface Card (NIC) and performs actions based on whether the packet is a request or a reply. This
# includes updating the ARP cache, forwarding ARP replies, sending ARP requests for unknown destinations, and
# handling packet TTL (Time To Live).
# def get_arp_cache_network_interface(self, ip_address: IPv4Address) -> Optional[RouterInterface]:
# arp_entry = self.arp.get(ip_address)
# if arp_entry:
# return self.software_manager.node.network_interfaces[arp_entry.network_interface_uuid]
# return None
#
# The method first checks if the ARP packet is a request or a reply. For ARP replies, it updates the ARP cache
# and forwards the reply if necessary. For ARP requests, it checks if the target IP matches one of the router's
# NICs and sends an ARP reply if so. If the destination is not directly connected, it consults the routing table
# to find the best route and reattempts ARP request processing if needed.
#
# :param from_nic: The NIC that received the ARP packet.
# :param frame: The frame containing the ARP packet.
# :param route_table: The routing table of the router.
# :param is_reattempt: Flag to indicate if this is a reattempt of processing the ARP packet, defaults to False.
# """
# arp_packet = frame.arp
#
# # ARP Reply
# if not arp_packet.request:
# if arp_packet.target_ip_address == from_nic.ip_address:
# # reply to the Router specifically
# self.sys_log.info(
# f"Received ARP response for {arp_packet.sender_ip_address} "
# f"from {arp_packet.sender_mac_addr} via NIC {from_nic}"
# )
# self.add_arp_cache_entry(
# ip_address=arp_packet.sender_ip_address,
# mac_address=arp_packet.sender_mac_addr,
# nic=from_nic,
# )
# return
#
# # # Reply for a connected requested
# # nic = self.get_arp_cache_nic(arp_packet.target_ip_address)
# # if nic:
# # self.sys_log.info(
# # f"Forwarding arp reply for {arp_packet.target_ip_address}, from {arp_packet.sender_ip_address}"
# # )
# # arp_packet.sender_mac_addr = nic.mac_address
# # frame.decrement_ttl()
# # if frame.ip and frame.ip.ttl < 1:
# # self.sys_log.info("Frame discarded as TTL limit reached")
# # return
# # nic.send_frame(frame)
# # return
#
# # ARP Request
# self.sys_log.info(
# f"Received ARP request for {arp_packet.target_ip_address} from "
# f"{arp_packet.sender_mac_addr}/{arp_packet.sender_ip_address} "
# )
# # Matched ARP request
# def _process_arp_request(self, arp_packet: ARPPacket, from_network_interface: RouterInterface):
# super()._process_arp_request(arp_packet, from_network_interface)
# self.add_arp_cache_entry(
# ip_address=arp_packet.sender_ip_address, mac_address=arp_packet.sender_mac_addr, nic=from_nic
# ip_address=arp_packet.sender_ip_address, mac_address=arp_packet.sender_mac_addr,
# network_interface=from_network_interface
# )
#
# # If the target IP matches one of the router's NICs
# for nic in self.nics.values():
# if nic.enabled and nic.ip_address == arp_packet.target_ip_address:
# arp_reply = arp_packet.generate_reply(from_nic.mac_address)
# self.send_arp_reply(arp_reply, from_nic)
# for network_interface in self.network_interfaces.values():
# if network_interface.enabled and network_interface.ip_address == arp_packet.target_ip_address:
# arp_reply = arp_packet.generate_reply(from_network_interface.mac_address)
# self.send_arp_reply(arp_reply)
# return
#
# # # Check Route Table
# # route = route_table.find_best_route(arp_packet.target_ip_address)
# # if route and route != self.router.route_table.default_route:
# # nic = self.get_arp_cache_nic(route.next_hop_ip_address)
# #
# # if not nic:
# # if not is_reattempt:
# # self.send_arp_request(route.next_hop_ip_address, ignore_networks=[frame.ip.src_ip_address])
# # return self.process_arp_packet(from_nic, frame, route_table, is_reattempt=True)
# # else:
# # self.sys_log.info("Ignoring ARP request as destination unavailable/No ARP entry found")
# # return
# # else:
# # arp_reply = arp_packet.generate_reply(from_nic.mac_address)
# # self.send_arp_reply(arp_reply, from_nic)
# # return
# def _process_arp_reply(self, arp_packet: ARPPacket, from_network_interface: RouterInterface):
# if arp_packet.target_ip_address == from_network_interface.ip_address:
# super()._process_arp_reply(arp_packet, from_network_interface)
#
# def receive(self, payload: Any, session_id: str, **kwargs) -> bool:
# """
# Processes received data, handling ARP packets.
#
# :param payload: The payload received.
# :param session_id: The session ID associated with the received data.
# :param kwargs: Additional keyword arguments.
# :return: True if the payload was processed successfully, otherwise False.
# """
# if not super().receive(payload, session_id, **kwargs):
# return False
#
# arp_packet: ARPPacket = payload
# from_network_interface: RouterInterface = kwargs["from_network_interface"]
#
# for network_interface in self.network_interfaces.values():
# # ARP frame is for this Router
# if network_interface.ip_address == arp_packet.target_ip_address:
# if payload.request:
# self._process_arp_request(arp_packet=arp_packet, from_network_interface=from_network_interface)
# else:
# self._process_arp_reply(arp_packet=arp_packet, from_network_interface=from_network_interface)
# return True
#
# # ARP frame is not for this router, pass back down to Router to continue routing
# frame: Frame = kwargs["frame"]
# self.router.process_frame(frame=frame, from_network_interface=from_network_interface)
#
# return True

View File

@@ -3,7 +3,6 @@ from ipaddress import IPv4Address
from typing import Dict, Any, Union, Optional, Tuple
from primaite import getLogger
from primaite.simulator.network.hardware.base import NIC
from primaite.simulator.network.protocols.icmp import ICMPPacket, ICMPType
from primaite.simulator.network.transmission.data_link_layer import Frame
from primaite.simulator.network.transmission.network_layer import IPProtocol
@@ -53,7 +52,7 @@ class ICMP(Service):
return False
if target_ip_address.is_loopback:
self.sys_log.info("Pinging loopback address")
return any(nic.enabled for nic in self.nics.values())
return any(network_interface.enabled for network_interface in self.network_interfaces.values())
self.sys_log.info(f"Pinging {target_ip_address}:", to_terminal=True)
sequence, identifier = 0, None
while sequence < pings:
@@ -88,9 +87,9 @@ class ICMP(Service):
:param pings: The number of pings to send. Defaults to 4.
:return: A tuple containing the next sequence number and the identifier.
"""
nic = self.software_manager.session_manager.resolve_outbound_nic(target_ip_address)
network_interface = self.software_manager.session_manager.resolve_outbound_network_interface(target_ip_address)
if not nic:
if not network_interface:
self.sys_log.error(
"Cannot send ICMP echo request as there is no outbound NIC to use. Try configuring the default gateway."
)
@@ -118,9 +117,11 @@ class ICMP(Service):
"""
self.sys_log.info(f"Received echo request from {frame.ip.src_ip_address}")
nic = self.software_manager.session_manager.resolve_outbound_nic(frame.ip.src_ip_address)
network_interface = self.software_manager.session_manager.resolve_outbound_network_interface(
frame.ip.src_ip_address
)
if not nic:
if not network_interface:
self.sys_log.error(
"Cannot send ICMP echo reply as there is no outbound NIC to use. Try configuring the default gateway."
)

View File

@@ -16,30 +16,30 @@
# super().__init__(sys_log, arp_cache)
# self.router = router
#
# def process_icmp(self, frame: Frame, from_nic: NIC, is_reattempt: bool = False):
# def process_icmp(self, frame: Frame, from_network_interface: NIC, is_reattempt: bool = False):
# """
# Process incoming ICMP frames based on ICMP type.
#
# :param frame: The incoming frame to process.
# :param from_nic: The network interface where the frame is coming from.
# :param from_network_interface: The network interface where the frame is coming from.
# :param is_reattempt: Flag to indicate if the process is a reattempt.
# """
# if frame.icmp.icmp_type == ICMPType.ECHO_REQUEST:
# # determine if request is for router interface or whether it needs to be routed
#
# for nic in self.router.nics.values():
# if nic.ip_address == frame.ip.dst_ip_address:
# if nic.enabled:
# for network_interface in self.router.network_interfaces.values():
# if network_interface.ip_address == frame.ip.dst_ip_address:
# if network_interface.enabled:
# # reply to the request
# if not is_reattempt:
# self.sys_log.info(f"Received echo request from {frame.ip.src_ip_address}")
# target_mac_address = self.arp.get_arp_cache_mac_address(frame.ip.src_ip_address)
# src_nic = self.arp.get_arp_cache_nic(frame.ip.src_ip_address)
# src_nic = self.arp.get_arp_cache_network_interface(frame.ip.src_ip_address)
# tcp_header = TCPHeader(src_port=Port.ARP, dst_port=Port.ARP)
#
# # Network Layer
# ip_packet = IPPacket(
# src_ip_address=nic.ip_address,
# src_ip_address=network_interface.ip_address,
# dst_ip_address=frame.ip.src_ip_address,
# protocol=IPProtocol.ICMP,
# )
@@ -67,12 +67,12 @@
# return
#
# # Route the frame
# self.router.process_frame(frame, from_nic)
# self.router.process_frame(frame, from_network_interface)
#
# elif frame.icmp.icmp_type == ICMPType.ECHO_REPLY:
# for nic in self.router.nics.values():
# if nic.ip_address == frame.ip.dst_ip_address:
# if nic.enabled:
# for network_interface in self.router.network_interfaces.values():
# if network_interface.ip_address == frame.ip.dst_ip_address:
# if network_interface.enabled:
# time = frame.transmission_duration()
# time_str = f"{time}ms" if time > 0 else "<1ms"
# self.sys_log.info(
@@ -87,4 +87,4 @@
#
# return
# # Route the frame
# self.router.process_frame(frame, from_nic)
# self.router.process_frame(frame, from_network_interface)