# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from __future__ import annotations from abc import abstractmethod from typing import Any, Dict, Optional, Union from prettytable import MARKDOWN, PrettyTable from primaite.simulator.network.hardware.base import NetworkInterface from primaite.simulator.network.protocols.arp import ARPEntry, ARPPacket 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): """ The ARP (Address Resolution Protocol) 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): kwargs["name"] = "ARP" kwargs["port"] = Port.ARP kwargs["protocol"] = IPProtocol.UDP 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): """ Prints the current state of the ARP cache in a table format. :param markdown: If True, format the output as Markdown. Otherwise, use plain text. """ table = PrettyTable(["IP Address", "MAC Address", "Via"]) if markdown: table.set_style(MARKDOWN) table.align = "l" table.title = f"{self.sys_log.hostname} ARP Cache" for ip, arp in self.arp.items(): table.add_row( [ str(ip), arp.mac_address, self.software_manager.node.network_interfaces[arp.network_interface_uuid].mac_address, ] ) print(table) def clear(self): """Clears the arp cache.""" self.arp.clear() def get_default_gateway_network_interface(self) -> Optional[NetworkInterface]: """Not used at the parent ARP level. Should return None when there is no override by child class.""" return None 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. If an entry for the given IP address already exists, the entry is only updated if the `override` parameter is set to True. :param ip_address: The IP address to be added to the cache. :param mac_address: The MAC address associated with the IP address. :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 _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 {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]: """ Retrieves the MAC address associated with a given IP address from the ARP cache. :param ip_address: The IP address to look up. :return: The associated MAC address, if found. Otherwise, returns None. """ pass @abstractmethod 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. :param ip_address: The IP address to look up. :return: The associated NIC, if found. Otherwise, returns None. """ pass 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. """ if target_ip_address in self.arp: return use_default_gateway = True for network_interface in self.software_manager.node.network_interfaces.values(): if target_ip_address in network_interface.ip_network: use_default_gateway = False break if use_default_gateway: if self.software_manager.node.default_gateway: target_ip_address = self.software_manager.node.default_gateway else: return 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_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( payload=arp_packet, dst_ip_address=target_ip_address, dst_port=self.port, ip_protocol=self.protocol ) else: self.sys_log.warning( "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): """ Sends an ARP reply in response to an ARP request. :param arp_reply: The ARP packet containing the reply. """ 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} " ) self.software_manager.session_manager.receive_payload_from_software_manager( payload=arp_reply, dst_ip_address=arp_reply.target_ip_address, dst_port=self.port, ip_protocol=self.protocol, ) else: self.sys_log.warning( "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): """ Processes an incoming ARP request. :param arp_packet: The ARP packet containing the 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_network_interface: NetworkInterface): """ Processes an incoming ARP reply. :param arp_packet: The ARP packet containing the 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 Network Interface {from_network_interface}" ) self.add_arp_cache_entry( 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: """ 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 from_network_interface = kwargs["from_network_interface"] if payload.request: self._process_arp_request(arp_packet=payload, from_network_interface=from_network_interface) else: self._process_arp_reply(arp_packet=payload, from_network_interface=from_network_interface) return True def __contains__(self, item: Any) -> bool: """ Checks if an item is in the ARP cache. :param item: The item to check. :return: True if the item is in the cache, otherwise False. """ return item in self.arp