diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 28e7693a..c64b9b67 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -173,6 +173,9 @@ class NIC(SimComponent): if self.connected_node.operating_state != NodeOperatingState.ON: self.connected_node.sys_log.error(f"NIC {self} cannot be enabled as the endpoint is not turned on") return + if not self.connected_link: + _LOGGER.error(f"NIC {self} cannot be enabled as it is not connected to a Link") + return self.enabled = True self.connected_node.sys_log.info(f"NIC {self} enabled") @@ -210,6 +213,7 @@ class NIC(SimComponent): # TODO: Inform the Node that a link has been connected self.connected_link = link + self.enable() _LOGGER.info(f"NIC {self} connected to Link {link}") def disconnect_link(self): @@ -266,8 +270,10 @@ class NIC(SimComponent): frame.decrement_ttl() frame.set_received_timestamp() self.pcap.capture(frame) - self.connected_node.receive_frame(frame=frame, from_nic=self) - return True + # If this destination or is broadcast + if frame.ethernet.dst_mac_addr == self.mac_address or frame.ethernet.dst_mac_addr == "ff:ff:ff:ff:ff:ff": + self.connected_node.receive_frame(frame=frame, from_nic=self) + return True return False def __str__(self) -> str: @@ -688,7 +694,6 @@ class ARPCache: frame = Frame(ethernet=ethernet_header, ip=ip_packet, tcp=tcp_header, arp=arp_packet) from_nic.send_frame(frame) - class ICMP: """ The ICMP (Internet Control Message Protocol) class. @@ -705,6 +710,8 @@ class ICMP: """ self.sys_log: SysLog = sys_log self.arp: ARPCache = arp_cache + self.request_replies = {} + def process_icmp(self, frame: Frame): """ @@ -733,6 +740,9 @@ class ICMP: 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}") + if not self.request_replies.get(frame.icmp.identifier): + self.request_replies[frame.icmp.identifier] = 0 + self.request_replies[frame.icmp.identifier] += 1 def ping( self, target_ip_address: IPv4Address, sequence: int = 0, identifier: Optional[int] = None @@ -875,7 +885,7 @@ class Node(SimComponent): return state def show(self): - """Prints a table of the NICs on the Node..""" + """Prints a table of the NICs on the Node.""" from prettytable import PrettyTable table = PrettyTable(["MAC Address", "Address", "Default Gateway", "Speed", "Status"]) @@ -898,7 +908,8 @@ class Node(SimComponent): self.operating_state = NodeOperatingState.ON self.sys_log.info("Turned on") for nic in self.nics.values(): - nic.enable() + if nic.connected_link: + nic.enable() def power_off(self): """Power off the Node, disabling its NICs if it is in the ON state.""" @@ -961,7 +972,9 @@ class Node(SimComponent): sequence, identifier = 0, None while sequence < pings: sequence, identifier = self.icmp.ping(target_ip_address, sequence, identifier) - return True + passed = self.icmp.request_replies[identifier] == pings + self.icmp.request_replies.pop(identifier) + return passed self.sys_log.info("Ping failed as the node is turned off") return False diff --git a/src/primaite/simulator/network/hardware/nodes/router.py b/src/primaite/simulator/network/hardware/nodes/router.py new file mode 100644 index 00000000..c5620b88 --- /dev/null +++ b/src/primaite/simulator/network/hardware/nodes/router.py @@ -0,0 +1,86 @@ +from enum import Enum +from ipaddress import IPv4Address +from typing import Dict, List, Union + +from primaite.simulator.core import SimComponent +from primaite.simulator.network.hardware.base import Node, NIC +from prettytable import PrettyTable + +from primaite.simulator.network.transmission.network_layer import IPProtocol +from primaite.simulator.network.transmission.transport_layer import Port + + +class ACLAction(Enum): + DENY = 0 + PERMIT = 1 + + +class ACLRule(SimComponent): + action: ACLAction + protocol: IPProtocol + src_ip: IPv4Address + src_wildcard: IPv4Address = IPv4Address("0.0.0.0") + src_port: Port + dst_ip: IPv4Address + dst_port: Port + + +class RouteTableEntry(SimComponent): + pass + + +class Router(Node): + num_ports: int + ethernet_ports: Dict[int, NIC] = {} + acl: List = [] + route_table: Dict = {} + + def __init__(self, hostname: str, num_ports: int = 5, **kwargs): + super().__init__(hostname=hostname, num_ports=num_ports, **kwargs) + + for i in range(1, self.num_ports + 1): + nic = NIC(ip_address="127.0.0.1", subnet_mask="255.0.0.0", gateway="0.0.0.0") + self.connect_nic(nic) + self.ethernet_ports[i] = nic + + def describe_state(self) -> Dict: + pass + + def configure_port( + self, + port: int, + ip_address: Union[IPv4Address, str], + subnet_mask: str + ): + if not isinstance(ip_address, IPv4Address): + ip_address = IPv4Address(ip_address) + nic = self.ethernet_ports[port] + nic.ip_address = ip_address + nic.subnet_mask = subnet_mask + self.sys_log.info(f"Configured port {port} with {ip_address=} {subnet_mask=}") + + def enable_port(self, port: int): + nic = self.ethernet_ports.get(port) + if nic: + nic.enable() + + def disable_port(self, port: int): + nic = self.ethernet_ports.get(port) + if nic: + nic.disable() + + def show(self): + """Prints a table of the NICs on the Node.""" + table = PrettyTable(["Port", "MAC Address", "Address", "Speed", "Status"]) + + for port, nic in self.ethernet_ports.items(): + table.add_row( + [ + port, + nic.mac_address, + f"{nic.ip_address}/{nic.ip_network.prefixlen}", + nic.speed, + "Enabled" if nic.enabled else "Disabled", + ] + ) + print(table) diff --git a/tests/integration_tests/network/test_frame_transmission.py b/tests/integration_tests/network/test_frame_transmission.py index 3840c302..d3d6541a 100644 --- a/tests/integration_tests/network/test_frame_transmission.py +++ b/tests/integration_tests/network/test_frame_transmission.py @@ -45,7 +45,7 @@ def test_multi_nic(): node_a.ping("192.168.0.11") - node_c.ping("10.0.0.12") + assert node_c.ping("10.0.0.12") def test_switched_network(): @@ -83,4 +83,4 @@ def test_switched_network(): link_nic_d_switch_2 = Link(endpoint_a=nic_d, endpoint_b=switch_2.switch_ports[2]) link_switch_1_switch_2 = Link(endpoint_a=switch_1.switch_ports[6], endpoint_b=switch_2.switch_ports[6]) - pc_a.ping("192.168.0.13") + assert pc_a.ping("192.168.0.13") diff --git a/tests/integration_tests/network/test_routing.py b/tests/integration_tests/network/test_routing.py new file mode 100644 index 00000000..cca48c0d --- /dev/null +++ b/tests/integration_tests/network/test_routing.py @@ -0,0 +1,27 @@ +from primaite.simulator.network.hardware.base import Node, NIC, Link +from primaite.simulator.network.hardware.nodes.router import Router + + +def test_ping_fails_with_no_route(): + """Tests a larges network of Nodes and Switches with one node pinging another.""" + pc_a = Node(hostname="pc_a") + nic_a = NIC(ip_address="192.168.0.10", subnet_mask="255.255.255.0", gateway="192.168.0.1") + pc_a.connect_nic(nic_a) + pc_a.power_on() + + pc_b = Node(hostname="pc_b") + nic_b = NIC(ip_address="192.168.1.10", subnet_mask="255.255.255.0", gateway="192.168.1.1") + pc_b.connect_nic(nic_b) + pc_b.power_on() + + router_1 = Router(hostname="router_1") + router_1.configure_port(1, "192.168.0.1", "255.255.255.0") + router_1.configure_port(2, "192.168.1.1", "255.255.255.0") + + router_1.power_on() + router_1.show() + + link_nic_a_router_1 = Link(endpoint_a=nic_a, endpoint_b=router_1.ethernet_ports[1]) + link_nic_b_router_1 = Link(endpoint_a=nic_b, endpoint_b=router_1.ethernet_ports[2]) + router_1.power_on() + #assert pc_a.ping("192.168.1.10") \ No newline at end of file