From ade5f133d0491398e090cebe8213778183fb3e5b Mon Sep 17 00:00:00 2001 From: Chris McCarthy Date: Fri, 22 Dec 2023 10:31:11 +0000 Subject: [PATCH] #2139 - Implemented routing --- CHANGELOG.md | 4 + src/primaite/simulator/network/creation.py | 148 ++++++++++++++++++ .../simulator/network/hardware/base.py | 26 +-- .../network/hardware/nodes/router.py | 148 ++++++++++++++---- .../network/hardware/nodes/switch.py | 4 +- .../integration_tests/network/test_routing.py | 93 +++++++++++ 6 files changed, 381 insertions(+), 42 deletions(-) create mode 100644 src/primaite/simulator/network/creation.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 541a39d5..96634b28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,10 @@ SessionManager. - Fixed an issue where the services were still able to run even though the node the service is installed on is turned off - NTP Services: `NTPClient` and `NTPServer` +### Changed +- Integrated the RouteTable into the Routers frame processing. +- Frames are now dropped when their TTL reaches 0 + ### Removed - Removed legacy simulation modules: `acl`, `common`, `environment`, `links`, `nodes`, `pol` - Removed legacy training modules diff --git a/src/primaite/simulator/network/creation.py b/src/primaite/simulator/network/creation.py new file mode 100644 index 00000000..48313a1f --- /dev/null +++ b/src/primaite/simulator/network/creation.py @@ -0,0 +1,148 @@ +from ipaddress import IPv4Address +from typing import Optional + +from primaite.simulator.network.container import Network +from primaite.simulator.network.hardware.nodes.computer import Computer +from primaite.simulator.network.hardware.nodes.router import ACLAction, Router +from primaite.simulator.network.hardware.nodes.switch import Switch +from primaite.simulator.network.transmission.network_layer import IPProtocol +from primaite.simulator.network.transmission.transport_layer import Port + + +def num_of_switches_required(num_nodes: int, max_switch_ports: int = 24) -> int: + """ + Calculate the minimum number of network switches required to connect a given number of nodes. + + Each switch is assumed to have one port reserved for connecting to a router, reducing the effective + number of ports available for PCs. The function calculates the total number of switches needed + to accommodate all nodes under this constraint. + + :param num_nodes: The total number of nodes that need to be connected in the network. + :param max_switch_ports: The maximum number of ports available on each switch. Defaults to 24. + + :return: The minimum number of switches required to connect all PCs. + + Example: + >>> num_of_switches_required(5) + 1 + >>> num_of_switches_required(24,24) + 2 + >>> num_of_switches_required(48,24) + 3 + >>> num_of_switches_required(25,10) + 3 + """ + # Reduce the effective number of switch ports by 1 to leave space for the router + effective_switch_ports = max_switch_ports - 1 + + # Calculate the number of fully utilised switches and any additional switch for remaining PCs + full_switches = num_nodes // effective_switch_ports + extra_pcs = num_nodes % effective_switch_ports + + # Return the total number of switches required + return full_switches + (1 if extra_pcs > 0 else 0) + + +def create_office_lan( + lan_name: str, + subnet_base: int, + pcs_ip_block_start: int, + num_pcs: int, + network: Optional[Network] = None, + include_router: bool = True, +) -> Network: + """ + Creates a 2-Tier or 3-Tier office local area network (LAN). + + The LAN is configured with a specified number of personal computers (PCs), optionally including a router, + and multiple edge switches to connect them. A core switch is added only if more than one edge switch is required. + The network topology involves edge switches connected either directly to the router in a 2-Tier setup or + to a core switch in a 3-Tier setup. If a router is included, it is connected to the core switch (if present) + and configured with basic access control list (ACL) rules. PCs are distributed across the edge switches. + + + :param str lan_name: The name to be assigned to the LAN. + :param int subnet_base: The subnet base number to be used in the IP addresses. + :param int pcs_ip_block_start: The starting block for assigning IP addresses to PCs. + :param int num_pcs: The number of PCs to be added to the LAN. + :param Optional[Network] network: The network to which the LAN components will be added. If None, a new network is + created. + :param bool include_router: Flag to determine if a router should be included in the LAN. Defaults to True. + :return: The network object with the LAN components added. + :raises ValueError: If pcs_ip_block_start is less than or equal to the number of required switches. + """ + # Initialise the network if not provided + if not network: + network = Network() + + # Calculate the required number of switches + num_of_switches = num_of_switches_required(num_nodes=num_pcs) + effective_switch_ports = 23 # One port less for router connection + if pcs_ip_block_start <= num_of_switches: + raise ValueError(f"pcs_ip_block_start must be greater than the number of required switches {num_of_switches}") + + # Create a core switch if more than one edge switch is needed + if num_of_switches > 1: + core_switch = Switch(hostname=f"switch_core_{lan_name}", start_up_duration=0) + core_switch.power_on() + network.add_node(core_switch) + core_switch_port = 1 + + # Initialise the default gateway to None + default_gateway = None + + # Optionally include a router in the LAN + if include_router: + default_gateway = IPv4Address(f"192.168.{subnet_base}.1") + router = Router(hostname=f"router_{lan_name}", start_up_duration=0) + router.power_on() + router.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.ARP, dst_port=Port.ARP, position=22) + router.acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol.ICMP, position=23) + network.add_node(router) + router.configure_port(port=1, ip_address=default_gateway, subnet_mask="255.255.255.0") + router.enable_port(1) + + # Initialise the first edge switch and connect to the router or core switch + switch_port = 0 + switch_n = 1 + switch = Switch(hostname=f"switch_edge_{switch_n}_{lan_name}", start_up_duration=0) + switch.power_on() + network.add_node(switch) + if num_of_switches > 1: + network.connect(core_switch.switch_ports[core_switch_port], switch.switch_ports[24]) + else: + network.connect(router.ethernet_ports[1], switch.switch_ports[24]) + + # Add PCs to the LAN and connect them to switches + for i in range(1, num_pcs + 1): + # Add a new edge switch if the current one is full + if switch_port == effective_switch_ports: + switch_n += 1 + switch_port = 0 + switch = Switch(hostname=f"switch_edge_{switch_n}_{lan_name}", start_up_duration=0) + switch.power_on() + network.add_node(switch) + # Connect the new switch to the router or core switch + if num_of_switches > 1: + core_switch_port += 1 + network.connect(core_switch.switch_ports[core_switch_port], switch.switch_ports[24]) + else: + network.connect(router.ethernet_ports[1], switch.switch_ports[24]) + + # Create and add a PC to the network + pc = Computer( + hostname=f"pc_{i}_{lan_name}", + ip_address=f"192.168.{subnet_base}.{i+pcs_ip_block_start-1}", + subnet_mask="255.255.255.0", + default_gateway=default_gateway, + start_up_duration=0, + ) + pc.power_on() + network.add_node(pc) + + # Connect the PC to the switch + switch_port += 1 + network.connect(switch.switch_ports[switch_port], pc.ethernet_port[1]) + switch.switch_ports[switch_port].enable() + + return network diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index ad3d73aa..c27378a8 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -4,7 +4,7 @@ import re import secrets from ipaddress import IPv4Address, IPv4Network from pathlib import Path -from typing import Any, Dict, Literal, Optional, Tuple, Union +from typing import Any, Dict, List, Literal, Optional, Tuple, Union from prettytable import MARKDOWN, PrettyTable @@ -282,6 +282,9 @@ class NIC(SimComponent): """ if self.enabled: frame.decrement_ttl() + if frame.ip and frame.ip.ttl < 1: + self._connected_node.sys_log.info("Frame discarded as TTL limit reached") + return False frame.set_received_timestamp() self.pcap.capture(frame) # If this destination or is broadcast @@ -436,6 +439,9 @@ class SwitchPort(SimComponent): """ if self.enabled: frame.decrement_ttl() + if frame.ip and frame.ip.ttl < 1: + self._connected_node.sys_log.info("Frame discarded as TTL limit reached") + return False self.pcap.capture(frame) connected_node: Node = self._connected_node connected_node.forward_frame(frame=frame, incoming_port=self) @@ -671,7 +677,9 @@ class ARPCache: """Clear the entire ARP cache, removing all stored entries.""" self.arp.clear() - def send_arp_request(self, target_ip_address: Union[IPv4Address, str]): + def send_arp_request( + self, target_ip_address: Union[IPv4Address, str], ignore_networks: Optional[List[IPv4Address]] = None + ): """ Perform a standard ARP request for a given target IP address. @@ -681,7 +689,12 @@ class ARPCache: :param target_ip_address: The target IP address to send an ARP request for. """ for nic in self.nics.values(): - if nic.enabled: + use_nic = True + if ignore_networks: + for ipv4 in ignore_networks: + if ipv4 in nic.ip_network: + use_nic = False + if nic.enabled and use_nic: self.sys_log.info(f"Sending ARP request from NIC {nic} for ip {target_ip_address}") tcp_header = TCPHeader(src_port=Port.ARP, dst_port=Port.ARP) @@ -806,7 +819,6 @@ class ICMP: self.arp.send_arp_request(frame.ip.src_ip_address) self.process_icmp(frame=frame, from_nic=from_nic, is_reattempt=True) return - tcp_header = TCPHeader(src_port=Port.ARP, dst_port=Port.ARP) # Network Layer ip_packet = IPPacket( @@ -821,9 +833,7 @@ class ICMP: sequence=frame.icmp.sequence + 1, ) payload = secrets.token_urlsafe(int(32 / 1.3)) # Standard ICMP 32 bytes size - frame = Frame( - ethernet=ethernet_header, ip=ip_packet, tcp=tcp_header, icmp=icmp_reply_packet, payload=payload - ) + frame = Frame(ethernet=ethernet_header, ip=ip_packet, icmp=icmp_reply_packet, payload=payload) self.sys_log.info(f"Sending echo reply to {frame.ip.dst_ip_address}") src_nic.send_frame(frame) @@ -1447,7 +1457,6 @@ class Node(SimComponent): service.parent = self service.install() # Perform any additional setup, such as creating files for this service on the node. self.sys_log.info(f"Installed service {service.name}") - _LOGGER.info(f"Added service {service.uuid} to node {self.uuid}") self._service_request_manager.add_request(service.uuid, RequestType(func=service._request_manager)) def uninstall_service(self, service: Service) -> None: @@ -1480,7 +1489,6 @@ class Node(SimComponent): self.applications[application.uuid] = application application.parent = self self.sys_log.info(f"Installed application {application.name}") - _LOGGER.info(f"Added application {application.uuid} to node {self.uuid}") self._application_request_manager.add_request(application.uuid, RequestType(func=application._request_manager)) def uninstall_application(self, application: Application) -> None: diff --git a/src/primaite/simulator/network/hardware/nodes/router.py b/src/primaite/simulator/network/hardware/nodes/router.py index 0234934d..1e3d8022 100644 --- a/src/primaite/simulator/network/hardware/nodes/router.py +++ b/src/primaite/simulator/network/hardware/nodes/router.py @@ -324,11 +324,10 @@ class RouteEntry(SimComponent): """ Represents a single entry in a routing table. - Attributes: - address (IPv4Address): The destination IP address or network address. - subnet_mask (IPv4Address): The subnet mask for the network. - next_hop_ip_address (IPv4Address): The next hop IP address to which packets should be forwarded. - metric (int): The cost metric for this route. Default is 0.0. + :ivar address: The destination IP address or network address. + :ivar subnet_mask: The subnet mask for the network. + :ivar next_hop_ip_address: The next hop IP address to which packets should be forwarded. + :ivar metric: The cost metric for this route. Default is 0.0. Example: >>> entry = RouteEntry( @@ -348,12 +347,6 @@ class RouteEntry(SimComponent): metric: float = 0.0 "The cost metric for this route. Default is 0.0." - def __init__(self, **kwargs): - for key in {"address", "subnet_mask", "next_hop_ip_address"}: - if not isinstance(kwargs[key], IPv4Address): - kwargs[key] = IPv4Address(kwargs[key]) - super().__init__(**kwargs) - def set_original_state(self): """Sets the original state.""" vals_to_include = {"address", "subnet_mask", "next_hop_ip_address", "metric"} @@ -388,6 +381,7 @@ class RouteTable(SimComponent): """ routes: List[RouteEntry] = [] + default_route: Optional[RouteEntry] = None sys_log: SysLog def set_original_state(self): @@ -433,12 +427,35 @@ class RouteTable(SimComponent): ) self.routes.append(route) + def set_default_route_next_hop_ip_address(self, ip_address: IPv4Address): + """ + Sets the next-hop IP address for the default route in a routing table. + + This method checks if a default route (0.0.0.0/0) exists in the routing table. If it does not exist, + the method creates a new default route with the specified next-hop IP address. If a default route already + exists, it updates the next-hop IP address of the existing default route. After setting the next-hop + IP address, the method logs this action. + + :param ip_address: The next-hop IP address to be set for the default route. + """ + if not self.default_route: + self.default_route = RouteEntry( + ip_address=IPv4Address("0.0.0.0"), + subnet_mask=IPv4Address("0.0.0.0"), + next_hop_ip_address=ip_address, + ) + else: + self.default_route.next_hop_ip_address = ip_address + self.sys_log.info(f"Default configured to use {ip_address} as the next-hop") + def find_best_route(self, destination_ip: Union[str, IPv4Address]) -> Optional[RouteEntry]: """ Find the best route for a given destination IP. This method uses the Longest Prefix Match algorithm and considers metrics to find the best route. + If no dedicated route exists but a default route does, then the default route is returned as a last resort. + :param destination_ip: The destination IP to find the route for. :return: The best matching RouteEntry, or None if no route matches. """ @@ -458,6 +475,9 @@ class RouteTable(SimComponent): longest_prefix = prefix_len lowest_metric = route.metric + if not best_route and self.default_route: + best_route = self.default_route + return best_route def show(self, markdown: bool = False): @@ -489,12 +509,26 @@ class RouterARPCache(ARPCache): super().__init__(sys_log) self.router: Router = router - def process_arp_packet(self, from_nic: NIC, frame: Frame): + def process_arp_packet( + self, from_nic: NIC, frame: Frame, route_table: RouteTable, is_reattempt: bool = False + ) -> None: """ - Overridden method to process a received ARP packet in a router-specific way. + Processes a received ARP (Address Resolution Protocol) packet in a router-specific way. + + 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). + + 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 original ARP frame. + :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 @@ -522,7 +556,11 @@ class RouterARPCache(ARPCache): ) 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( @@ -533,16 +571,32 @@ class RouterARPCache(ARPCache): 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, from_nic) # 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: + if arp_packet.target_ip_address in nic.ip_network: + # 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) return + # Check Route Table + route = route_table.find_best_route(arp_packet.target_ip_address) + if 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: + pass + # TODO: destination unavailable/No ARP netry found + else: + arp_reply = arp_packet.generate_reply(from_nic.mac_address) + self.send_arp_reply(arp_reply, from_nic) + return + class RouterICMP(ICMP): """ @@ -613,7 +667,7 @@ class RouterICMP(ICMP): return # Route the frame - self.router.route_frame(frame, from_nic) + self.router.process_frame(frame, from_nic) elif frame.icmp.icmp_type == ICMPType.ECHO_REPLY: for nic in self.router.nics.values(): @@ -633,7 +687,7 @@ class RouterICMP(ICMP): return # Route the frame - self.router.route_frame(frame, from_nic) + self.router.process_frame(frame, from_nic) class Router(Node): @@ -720,9 +774,9 @@ class Router(Node): state["acl"] = (self.acl.describe_state(),) return state - def route_frame(self, frame: Frame, from_nic: NIC, re_attempt: bool = False) -> None: + def process_frame(self, frame: Frame, from_nic: NIC, re_attempt: bool = False) -> None: """ - Route a given frame from a source NIC to its destination. + Process a Frame. :param frame: The frame to be routed. :param from_nic: The source network interface. @@ -737,8 +791,10 @@ class Router(Node): return if not nic: - self.arp.send_arp_request(frame.ip.dst_ip_address) - return self.route_frame(frame=frame, from_nic=from_nic, re_attempt=True) + self.arp.send_arp_request( + frame.ip.dst_ip_address, ignore_networks=[frame.ip.src_ip_address, from_nic.ip_address] + ) + return self.process_frame(frame=frame, from_nic=from_nic, re_attempt=True) if not nic.enabled: # TODO: Add sys_log here @@ -747,15 +803,45 @@ class Router(Node): if frame.ip.dst_ip_address in nic.ip_network: from_port = self._get_port_of_nic(from_nic) to_port = self._get_port_of_nic(nic) - self.sys_log.info(f"Routing frame to internally from port {from_port} to port {to_port}") + self.sys_log.info(f"Forwarding frame to internally from port {from_port} to port {to_port}") frame.decrement_ttl() + if frame.ip and frame.ip.ttl < 1: + self.sys_log.info("Frame discarded as TTL limit reached") + return frame.ethernet.src_mac_addr = nic.mac_address frame.ethernet.dst_mac_addr = target_mac nic.send_frame(frame) return else: - pass - # TODO: Deal with routing from route tables + self._route_frame(frame, from_nic) + + def _route_frame(self, frame: Frame, from_nic: NIC, re_attempt: bool = False) -> None: + route = self.route_table.find_best_route(frame.ip.dst_ip_address) + if route: + nic = self.arp.get_arp_cache_nic(route.next_hop_ip_address) + target_mac = self.arp.get_arp_cache_mac_address(route.next_hop_ip_address) + if re_attempt and not nic: + self.sys_log.info(f"Destination {frame.ip.dst_ip_address} is unreachable") + return + + if not nic: + self.arp.send_arp_request(frame.ip.dst_ip_address, ignore_networks=[frame.ip.src_ip_address]) + return self.process_frame(frame=frame, from_nic=from_nic, re_attempt=True) + + if not nic.enabled: + # TODO: Add sys_log here + return + + from_port = self._get_port_of_nic(from_nic) + to_port = self._get_port_of_nic(nic) + self.sys_log.info(f"Routing frame to internally from port {from_port} to port {to_port}") + frame.decrement_ttl() + if frame.ip and frame.ip.ttl < 1: + self.sys_log.info("Frame discarded as TTL limit reached") + return + frame.ethernet.src_mac_addr = nic.mac_address + frame.ethernet.dst_mac_addr = target_mac + nic.send_frame(frame) def receive_frame(self, frame: Frame, from_nic: NIC): """ @@ -764,7 +850,7 @@ class Router(Node): :param frame: The incoming frame. :param from_nic: The network interface where the frame is coming from. """ - route_frame = False + process_frame = False protocol = frame.ip.protocol src_ip_address = frame.ip.src_ip_address dst_ip_address = frame.ip.dst_ip_address @@ -796,12 +882,12 @@ class Router(Node): self.icmp.process_icmp(frame=frame, from_nic=from_nic) else: if src_port == Port.ARP: - self.arp.process_arp_packet(from_nic=from_nic, frame=frame) + self.arp.process_arp_packet(from_nic=from_nic, frame=frame, route_table=self.route_table) else: # All other traffic - route_frame = True - if route_frame: - self.route_frame(frame, from_nic) + process_frame = True + if process_frame: + self.process_frame(frame, from_nic) def configure_port(self, port: int, ip_address: Union[IPv4Address, str], subnet_mask: Union[IPv4Address, str]): """ diff --git a/src/primaite/simulator/network/hardware/nodes/switch.py b/src/primaite/simulator/network/hardware/nodes/switch.py index fffae6e2..ead857f2 100644 --- a/src/primaite/simulator/network/hardware/nodes/switch.py +++ b/src/primaite/simulator/network/hardware/nodes/switch.py @@ -90,12 +90,12 @@ class Switch(Node): self._add_mac_table_entry(src_mac, incoming_port) outgoing_port = self.mac_address_table.get(dst_mac) - if outgoing_port or dst_mac != "ff:ff:ff:ff:ff:ff": + if outgoing_port and 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: + if port.enabled and port != incoming_port: port.send_frame(frame) def disconnect_link_from_port(self, link: Link, port_number: int): diff --git a/tests/integration_tests/network/test_routing.py b/tests/integration_tests/network/test_routing.py index 6053c457..3f636eae 100644 --- a/tests/integration_tests/network/test_routing.py +++ b/tests/integration_tests/network/test_routing.py @@ -1,8 +1,11 @@ +from ipaddress import IPv4Address from typing import Tuple import pytest +from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.base import Link, NIC, Node, NodeOperatingState +from primaite.simulator.network.hardware.nodes.computer import Computer from primaite.simulator.network.hardware.nodes.router import ACLAction, Router from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port @@ -34,6 +37,69 @@ def pc_a_pc_b_router_1() -> Tuple[Node, Node, Router]: return pc_a, pc_b, router_1 +@pytest.fixture(scope="function") +def multi_hop_network() -> Network: + network = Network() + + # Configure PC A + pc_a = Computer( + hostname="pc_a", + ip_address="192.168.0.2", + subnet_mask="255.255.255.0", + default_gateway="192.168.0.1", + start_up_duration=0, + ) + pc_a.power_on() + network.add_node(pc_a) + + # Configure Router 1 + router_1 = Router(hostname="router_1", start_up_duration=0) + router_1.power_on() + network.add_node(router_1) + + # Configure the connection between PC A and Router 1 port 2 + router_1.configure_port(2, "192.168.0.1", "255.255.255.0") + network.connect(pc_a.ethernet_port[1], router_1.ethernet_ports[2]) + router_1.enable_port(2) + + # Configure Router 1 ACLs + router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.ARP, dst_port=Port.ARP, position=22) + router_1.acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol.ICMP, position=23) + + # Configure PC B + pc_b = Computer( + hostname="pc_b", + ip_address="192.168.2.2", + subnet_mask="255.255.255.0", + default_gateway="192.168.2.1", + start_up_duration=0, + ) + pc_b.power_on() + network.add_node(pc_b) + + # Configure Router 2 + router_2 = Router(hostname="router_2", start_up_duration=0) + router_2.power_on() + network.add_node(router_2) + + # Configure the connection between PC B and Router 2 port 2 + router_2.configure_port(2, "192.168.2.1", "255.255.255.0") + network.connect(pc_b.ethernet_port[1], router_2.ethernet_ports[2]) + router_2.enable_port(2) + + # Configure Router 2 ACLs + router_2.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.ARP, dst_port=Port.ARP, position=22) + router_2.acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol.ICMP, position=23) + + # Configure the connection between Router 1 port 1 and Router 2 port 1 + router_2.configure_port(1, "192.168.1.2", "255.255.255.252") + router_1.configure_port(1, "192.168.1.1", "255.255.255.252") + network.connect(router_1.ethernet_ports[1], router_2.ethernet_ports[1]) + router_1.enable_port(1) + router_2.enable_port(1) + return network + + def test_ping_default_gateway(pc_a_pc_b_router_1): pc_a, pc_b, router_1 = pc_a_pc_b_router_1 @@ -50,3 +116,30 @@ def test_host_on_other_subnet(pc_a_pc_b_router_1): pc_a, pc_b, router_1 = pc_a_pc_b_router_1 assert pc_a.ping("192.168.1.10") + + +def test_no_route_no_ping(multi_hop_network): + pc_a = multi_hop_network.get_node_by_hostname("pc_a") + pc_b = multi_hop_network.get_node_by_hostname("pc_b") + + assert not pc_a.ping(pc_b.ethernet_port[1].ip_address) + + +def test_with_routes_can_ping(multi_hop_network): + pc_a = multi_hop_network.get_node_by_hostname("pc_a") + pc_b = multi_hop_network.get_node_by_hostname("pc_b") + + router_1: Router = multi_hop_network.get_node_by_hostname("router_1") # noqa + router_2: Router = multi_hop_network.get_node_by_hostname("router_2") # noqa + + # Configure Route from Router 1 to PC B subnet + router_1.route_table.add_route( + address="192.168.2.0", subnet_mask="255.255.255.0", next_hop_ip_address="192.168.1.2" + ) + + # Configure Route from Router 2 to PC A subnet + router_2.route_table.add_route( + address="192.168.0.2", subnet_mask="255.255.255.0", next_hop_ip_address="192.168.1.1" + ) + + assert pc_a.ping(pc_b.ethernet_port[1].ip_address)