Merge remote-tracking branch 'origin/dev' into feature/2258-dosbot-cannot-be-added-via-configuration-file
This commit is contained in:
17
CHANGELOG.md
17
CHANGELOG.md
@@ -62,6 +62,16 @@ SessionManager.
|
||||
- **Custom Layer-3 Processing**: The `RouterNIC` class includes custom handling for network frames, bypassing standard Node NIC's Layer 3 broadcast/unicast checks. This allows for more efficient routing behavior in network scenarios where router-specific frame processing is required.
|
||||
- **Enhanced Frame Reception**: The `receive_frame` method in `RouterNIC` is tailored to handle frames based on Layer 2 (Ethernet) checks, focusing on MAC address-based routing and broadcast frame acceptance.
|
||||
- **Subnet-Wide Broadcasting for Services and Applications**: Implemented the ability for services and applications to conduct broadcasts across an entire IPv4 subnet within the network simulation framework.
|
||||
- Introduced the `NetworkInterface` abstract class to provide a common interface for all network interfaces. Subclasses are divided into two main categories: `WiredNetworkInterface` and `WirelessNetworkInterface`, each serving as an abstract base class (ABC) for more specific interface types. Under `WiredNetworkInterface`, the subclasses `NIC` and `SwitchPort` were added. For wireless interfaces, `WirelessNIC` and `WirelessAccessPoint` are the subclasses under `WirelessNetworkInterface`.
|
||||
- Added `Layer3Interface` as an abstract base class for networking functionalities at layer 3, including IP addressing and routing capabilities. This class is inherited by `NIC`, `WirelessNIC`, and `WirelessAccessPoint` to provide them with layer 3 capabilities, facilitating their role in both wired and wireless networking contexts with IP-based communication.
|
||||
- Created the `ARP` and `ICMP` service classes to handle Address Resolution Protocol operations and Internet Control Message Protocol messages, respectively, with `RouterARP` and `RouterICMP` for router-specific implementations.
|
||||
- Created `HostNode` as a subclass of `Node`, extending its functionality with host-specific services and applications. This class is designed to represent end-user devices like computers or servers that can initiate and respond to network communications.
|
||||
- Introduced a new `IPV4Address` type in the Pydantic model for enhanced validation and auto-conversion of IPv4 addresses from strings using an `ipv4_validator`.
|
||||
- Comprehensive documentation for the Node and its network interfaces, detailing the operational workflow from frame reception to application-level processing.
|
||||
- Detailed descriptions of the Session Manager and Software Manager functionalities, including their roles in managing sessions, software services, and applications within the simulation.
|
||||
- Documentation for the Packet Capture (PCAP) service and SysLog functionality, highlighting their importance in logging network frames and system events, respectively.
|
||||
- Expanded documentation on network devices such as Routers, Switches, Computers, and Switch Nodes, explaining their specific processing logic and protocol support.
|
||||
|
||||
|
||||
### Changed
|
||||
- Integrated the RouteTable into the Routers frame processing.
|
||||
@@ -69,6 +79,9 @@ SessionManager.
|
||||
- **NIC Functionality Update**: Updated the Network Interface Card (`NIC`) functionality to support Layer 3 (L3) broadcasts.
|
||||
- **Layer 3 Broadcast Handling**: Enhanced the existing `NIC` classes to correctly process and handle Layer 3 broadcasts. This update allows devices using standard NICs to effectively participate in network activities that involve L3 broadcasting.
|
||||
- **Improved Frame Reception Logic**: The `receive_frame` method of the `NIC` class has been updated to include additional checks and handling for L3 broadcasts, ensuring proper frame processing in a wider range of network scenarios.
|
||||
- Standardised the way network interfaces are accessed across all `Node` subclasses (`HostNode`, `Router`, `Switch`) by maintaining a comprehensive `network_interface` attribute. This attribute captures all network interfaces by their port number, streamlining the management and interaction with network interfaces across different types of nodes.
|
||||
- Refactored all tests to utilise new `Node` subclasses (`Computer`, `Server`, `Router`, `Switch`) instead of creating generic `Node` instances and manually adding network interfaces. This change aligns test setups more closely with the intended use cases and hierarchies within the network simulation framework.
|
||||
- Updated all tests to employ the `Network()` class for managing nodes and their connections, ensuring a consistent and structured approach to setting up network topologies in testing scenarios.
|
||||
|
||||
|
||||
### Removed
|
||||
@@ -76,6 +89,10 @@ SessionManager.
|
||||
- Removed legacy training modules
|
||||
- Removed tests for legacy code
|
||||
|
||||
### Fixed
|
||||
- Addressed network transmission issues that previously allowed ARP requests to be incorrectly routed and repeated across different subnets. This fix ensures ARP requests are correctly managed and confined to their appropriate network segments.
|
||||
- Resolved problems in `Node` and its subclasses where the default gateway configuration was not properly utilized for communications across different subnets. This correction ensures that nodes effectively use their configured default gateways for outbound communications to other network segments, thereby enhancing the network's routing functionality and reliability.
|
||||
|
||||
|
||||
## [2.0.0] - 2023-07-26
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ At the top level of the network are ``nodes`` and ``links``.
|
||||
* ``acl`` (Router only): Define the ACL rules at each index of the ACL on the router. the possible options are: ``action`` (PERMIT or DENY), ``src_port``, ``dst_port``, ``protocol``, ``src_ip``, ``dst_ip``. Any options left blank default to none which usually means that it will apply across all options. For example leaving ``src_ip`` blank will apply the rule to all IP addresses.
|
||||
* ``services`` (computers and servers only): a list of services to install on the node. They must define a ``ref``, ``type``, and ``options`` that depend on which ``type`` was selected.
|
||||
* ``applications`` (computer and servers only): Similar to services. A list of application to install on the node.
|
||||
* ``nics`` (computers and servers only): If the node has multiple networking devices, the second, third, fourth, etc... must be defined here with an ``ip_address`` and ``subnet_mask``.
|
||||
* ``network_interfaces`` (computers and servers only): If the node has multiple networking devices, the second, third, fourth, etc... must be defined here with an ``ip_address`` and ``subnet_mask``.
|
||||
|
||||
**links:**
|
||||
* ``ref``: unique identifier for this link
|
||||
|
||||
@@ -17,9 +17,15 @@ Contents
|
||||
|
||||
simulation_structure
|
||||
simulation_components/network/base_hardware
|
||||
simulation_components/network/network_interfaces
|
||||
simulation_components/network/transport_to_data_link_layer
|
||||
simulation_components/network/router
|
||||
simulation_components/network/nodes/host_node
|
||||
simulation_components/network/nodes/network_node
|
||||
simulation_components/network/nodes/router
|
||||
simulation_components/network/switch
|
||||
simulation_components/network/network
|
||||
simulation_components/system/internal_frame_processing
|
||||
simulation_components/system/sys_log
|
||||
simulation_components/system/pcap
|
||||
simulation_components/system/session_and_software_manager
|
||||
simulation_components/system/software
|
||||
|
||||
@@ -6,719 +6,59 @@
|
||||
Base Hardware
|
||||
#############
|
||||
|
||||
The physical layer components are models of a NIC (Network Interface Card), SwitchPort, Node, Switch, and a Link.
|
||||
These components allow modelling of layer 1 (physical layer) in the OSI model and the nodes that connect to and
|
||||
transmit across layer 1.
|
||||
The ``base.py`` module in ``primaite.simulator.network.hardware`` provides foundational components, interfaces, and classes for
|
||||
modeling network hardware within PrimAITE simulations. It establishes core building blocks and abstractions that more
|
||||
complex, specialized hardware components inherit from and build upon.
|
||||
|
||||
===
|
||||
NIC
|
||||
===
|
||||
The key elements defined in ``base.py`` are:
|
||||
|
||||
The NIC class provides a realistic model of a Network Interface Card. The NIC acts as the interface between a Node and
|
||||
a Link, handling IP and MAC addressing, status, and sending/receiving frames.
|
||||
NetworkInterface
|
||||
================
|
||||
|
||||
----------
|
||||
Addressing
|
||||
----------
|
||||
- Abstract base class for network interfaces like NICs. Defines common attributes like MAC address, speed, MTU.
|
||||
- Requires subclasses to implement ``enable()``, ``disable()``, ``send_frame()`` and ``receive_frame()``.
|
||||
- Provides basic state description and request handling capabilities.
|
||||
|
||||
A NIC has both an IPv4 address and MAC address assigned:
|
||||
|
||||
- **ip_address** - The IPv4 address assigned to the NIC for communication on an IP network.
|
||||
- **subnet_mask** - The subnet mask that defines the network subnet.
|
||||
- **gateway** - The default gateway IP address for routing traffic beyond the local network.
|
||||
- **mac_address** - A unique MAC address assigned to the NIC by the manufacturer.
|
||||
|
||||
|
||||
------
|
||||
Status
|
||||
------
|
||||
|
||||
The status of the NIC is represented by:
|
||||
|
||||
- **enabled** - Indicates if the NIC is active/enabled or disabled/down. It must be enabled to send/receive frames.
|
||||
- **connected_node** - The Node instance the NIC is attached to.
|
||||
- **connected_link** - The Link instance the NIC is wired to.
|
||||
|
||||
|
||||
--------------
|
||||
Packet Capture
|
||||
--------------
|
||||
|
||||
- **pcap** - A PacketCapture instance attached to the NIC for capturing all frames sent and received. This allows packet
|
||||
capture and analysis.
|
||||
|
||||
------------------------
|
||||
Sending/Receiving Frames
|
||||
------------------------
|
||||
|
||||
The NIC can send and receive Frames to/from the connected Link:
|
||||
|
||||
- **send_frame()** - Sends a Frame through the NIC onto the attached Link.
|
||||
- **receive_frame()** - Receives a Frame from the attached Link and processes it.
|
||||
|
||||
This allows a NIC to handle sending, receiving, and forwarding of network traffic at layer 2 of the OSI model.
|
||||
The Frames contain network data encapsulated with various protocol headers.
|
||||
|
||||
-----------
|
||||
Basic Usage
|
||||
-----------
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
nic1 = NIC(
|
||||
ip_address="192.168.0.100",
|
||||
subnet_mask="255.255.255.0",
|
||||
gateway="192.168.0.1"
|
||||
)
|
||||
nic1.enable()
|
||||
frame = Frame(...)
|
||||
nic1.send_frame(frame)
|
||||
|
||||
==========
|
||||
SwitchPort
|
||||
==========
|
||||
|
||||
The SwitchPort models a port on a network switch. It has similar attributes and methods to NIC for addressing, status,
|
||||
packet capture, sending/receiving frames, etc.
|
||||
|
||||
Key attributes:
|
||||
|
||||
- **port_num**: The port number on the switch.
|
||||
- **connected_switch**: The switch to which this port belongs.
|
||||
|
||||
====
|
||||
Node
|
||||
====
|
||||
The Node class stands as a central component in ``base.py``, acting as the superclass for all network nodes within a
|
||||
PrimAITE simulation.
|
||||
|
||||
The Node class represents a base node that communicates on the Network.
|
||||
|
||||
Nodes take more than 1 time step to power on (3 time steps by default).
|
||||
To create a Node that is already powered on, the Node's operating state can be overriden.
|
||||
Otherwise, the node ``start_up_duration`` (and ``shut_down_duration``) can be set to 0 if
|
||||
the node will be powered off or on multiple times. This will still need ``power_on()`` to
|
||||
be called to turn the node on.
|
||||
|
||||
e.g.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
active_node = Node(hostname='server1', operating_state=NodeOperatingState.ON)
|
||||
# node is already on, no need to call power_on()
|
||||
|
||||
|
||||
instant_start_node = Node(hostname="client", start_up_duration=0, shut_down_duration=0)
|
||||
instant_start_node.power_on() # node will still need to be powered on
|
||||
|
||||
.. _Node Start up and Shut down:
|
||||
|
||||
---------------------------
|
||||
Node Start up and Shut down
|
||||
---------------------------
|
||||
|
||||
Nodes are powered on and off over multiple timesteps. By default, the node ``start_up_duration`` and ``shut_down_duration`` is 3 timesteps.
|
||||
|
||||
Example code where a node is turned on:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from primaite.simulator.network.hardware.base import Node
|
||||
from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState
|
||||
|
||||
node = Node(hostname="pc_a")
|
||||
|
||||
assert node.operating_state is NodeOperatingState.OFF # By default, node is instantiated in an OFF state
|
||||
|
||||
node.power_on() # power on the node
|
||||
|
||||
assert node.operating_state is NodeOperatingState.BOOTING # node is booting up
|
||||
|
||||
for i in range(node.start_up_duration + 1):
|
||||
# apply timestep until the node start up duration
|
||||
node.apply_timestep(timestep=i)
|
||||
|
||||
assert node.operating_state is NodeOperatingState.ON # node is in ON state
|
||||
|
||||
|
||||
If the node needs to be instantiated in an on state:
|
||||
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from primaite.simulator.network.hardware.base import Node
|
||||
from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState
|
||||
|
||||
node = Node(hostname="pc_a", operating_state=NodeOperatingState.ON)
|
||||
|
||||
assert node.operating_state is NodeOperatingState.ON # node is in ON state
|
||||
|
||||
Setting ``start_up_duration`` and/or ``shut_down_duration`` to ``0`` will allow for the ``power_on`` and ``power_off`` methods to be completed instantly without applying timesteps:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from primaite.simulator.network.hardware.base import Node
|
||||
from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState
|
||||
|
||||
node = Node(hostname="pc_a", start_up_duration=0, shut_down_duration=0)
|
||||
|
||||
assert node.operating_state is NodeOperatingState.OFF # node is in OFF state
|
||||
|
||||
node.power_on()
|
||||
|
||||
assert node.operating_state is NodeOperatingState.ON # node is in ON state
|
||||
|
||||
node.power_off()
|
||||
|
||||
assert node.operating_state is NodeOperatingState.OFF # node is in OFF state
|
||||
|
||||
------------------
|
||||
Network Interfaces
|
||||
------------------
|
||||
|
||||
A Node will typically have one or more NICs attached to it for network connectivity:
|
||||
|
||||
- **nics** - A dictionary containing the NIC instances attached to the Node. NICs can be added/removed.
|
||||
|
||||
-------------
|
||||
Configuration
|
||||
-------------
|
||||
|
||||
- **hostname** - Configured hostname of the Node.
|
||||
- **operating_state** - Current operating state like ON or OFF. The NICs will be enabled/disabled based on this.
|
||||
|
||||
----------------
|
||||
Network Services
|
||||
----------------
|
||||
|
||||
A Node runs various network services and components for handling traffic:
|
||||
|
||||
- **session_manager** - Handles establishing sessions to/from the Node.
|
||||
- **software_manager** - Manages software and applications on the Node.
|
||||
- **arp** - ARP cache for resolving IP addresses to MAC addresses.
|
||||
- **icmp** - ICMP service for responding to pings and echo requests.
|
||||
- **sys_log** - System log service for logging internal events and messages.
|
||||
|
||||
The SysLog provides a logging mechanism for the Node:
|
||||
|
||||
The SysLog records informational, warning, and error events that occur on the Node during simulation. This allows
|
||||
debugging and tracing program execution and network activity for each simulated Node. Other Node services like ARP and
|
||||
ICMP, along with custom Applications, services, and Processes will log to the SysLog.
|
||||
|
||||
-----------------
|
||||
Sending/Receiving
|
||||
-----------------
|
||||
|
||||
The Node handles sending and receiving Frames via its attached NICs:
|
||||
|
||||
- **send_frame()** - Sends a Frame to the network through one of the Node's NICs.
|
||||
- **receive_frame()** - Receives a Frame from the network through a NIC. The Node then processes it appropriately based
|
||||
on the protocols and payload.
|
||||
|
||||
-----------
|
||||
Basic Usage
|
||||
-----------
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
node1 = Node(hostname='server1')
|
||||
node1.operating_state = NodeOperatingState.ON
|
||||
|
||||
nic1 = NIC()
|
||||
node1.connect_nic(nic1)
|
||||
|
||||
Send a frame
|
||||
frame = Frame(...)
|
||||
node1.send_frame(frame)
|
||||
|
||||
The Node class brings together the NICs, configuration, and services to model a full network node that can send,
|
||||
receive, process, and forward traffic on a simulated network.
|
||||
|
||||
======
|
||||
Switch
|
||||
======
|
||||
|
||||
The Switch subclass models a network switch. It inherits from Node and acts at layer 2 of the OSI model to forward
|
||||
frames based on MAC addresses.
|
||||
|
||||
--------------------------
|
||||
Inherits Node Capabilities
|
||||
--------------------------
|
||||
|
||||
Since Switch subclasses Node, it inherits all capabilities from Node like:
|
||||
|
||||
- **Managing NICs**
|
||||
- **Running network services like ARP, ICMP**
|
||||
- **Sending and receiving frames**
|
||||
- **Maintaining system logs**
|
||||
|
||||
-----
|
||||
Ports
|
||||
-----
|
||||
|
||||
A Switch has multiple ports implemented using SwitchPort instances:
|
||||
|
||||
- **switch_ports** - A dictionary mapping port numbers to SwitchPort instances.
|
||||
- **num_ports** - The number of ports the Switch has.
|
||||
|
||||
----------
|
||||
Forwarding
|
||||
----------
|
||||
|
||||
A Switch forwards frames between ports based on the destination MAC:
|
||||
|
||||
- **dst_mac_table** - MAC address table that maps MACs to SwitchPorts.
|
||||
- **forward_frame()** - Forwards a frame out the port associated with the destination MAC.
|
||||
|
||||
When a frame is received on a SwitchPort:
|
||||
|
||||
1. The source MAC address is extracted from the frame.
|
||||
2. An entry is added to dst_mac_table that maps this source MAC to the SwitchPort it was received on.
|
||||
3. When a frame with that destination MAC is received in the future, it will be forwarded out this SwitchPort.
|
||||
|
||||
This allows the Switch to dynamically build up a mapping table between MAC addresses and SwitchPorts based on traffic
|
||||
received. If no entry exists for a destination MAC, it floods the frame out all ports.
|
||||
|
||||
====
|
||||
Link
|
||||
====
|
||||
|
||||
The Link class represents a physical link or connection between two network endpoints like NICs or SwitchPorts.
|
||||
|
||||
---------
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
A Link connects two endpoints:
|
||||
|
||||
- **endpoint_a** - The first endpoint, a NIC or SwitchPort.
|
||||
- **endpoint_b** - The second endpoint, a NIC or SwitchPort.
|
||||
|
||||
------------
|
||||
Transmission
|
||||
------------
|
||||
|
||||
Links transmit Frames between the endpoints:
|
||||
|
||||
- **transmit_frame()** - Sends a Frame from one endpoint to the other.
|
||||
|
||||
Uses bandwidth/load properties to determine if transmission is possible.
|
||||
|
||||
----------------
|
||||
Bandwidth & Load
|
||||
----------------
|
||||
|
||||
- **bandwidth** - The total capacity of the Link in Mbps.
|
||||
- **current_load** - The current bandwidth utilization of the Link in Mbps.
|
||||
|
||||
As Frames are sent over the Link, the load increases. The Link tracks if there is enough unused capacity to transmit a
|
||||
Frame based on its size and the current load.
|
||||
|
||||
------
|
||||
Status
|
||||
------
|
||||
|
||||
- **up** - Boolean indicating if the Link is currently up/active based on the endpoint status.
|
||||
- **endpoint_up()/down()** - Notifies the Link when an endpoint goes up or down.
|
||||
|
||||
This allows the Link to realistically model the connection and transmission characteristics between two endpoints.
|
||||
|
||||
=======================
|
||||
Putting it all Together
|
||||
=======================
|
||||
|
||||
We'll now demonstrate how the nodes, NICs, switches, and links connect in a network, including full code examples and
|
||||
syslog extracts to illustrate the step-by-step process.
|
||||
|
||||
To demonstrate successful network communication between nodes and switches, we'll model a standard network with four
|
||||
PC's and two switches.
|
||||
|
||||
|
||||
.. image:: ../../../_static/four_node_two_switch_network.png
|
||||
|
||||
-------------------
|
||||
Create Nodes & NICs
|
||||
-------------------
|
||||
|
||||
First, we'll create the four nodes, each with a single NIC.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from primaite.simulator.network.hardware.base import Node, NodeOperatingState, NIC
|
||||
|
||||
pc_a = Node(hostname="pc_a", operating_state=NodeOperatingState.ON)
|
||||
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_b = Node(hostname="pc_b", operating_state=NodeOperatingState.ON)
|
||||
nic_b = NIC(ip_address="192.168.0.11", subnet_mask="255.255.255.0", gateway="192.168.0.1")
|
||||
pc_b.connect_nic(nic_b)
|
||||
|
||||
pc_c = Node(hostname="pc_c", operating_state=NodeOperatingState.ON)
|
||||
nic_c = NIC(ip_address="192.168.0.12", subnet_mask="255.255.255.0", gateway="192.168.0.1")
|
||||
pc_c.connect_nic(nic_c)
|
||||
|
||||
pc_d = Node(hostname="pc_d", operating_state=NodeOperatingState.ON)
|
||||
nic_d = NIC(ip_address="192.168.0.13", subnet_mask="255.255.255.0", gateway="192.168.0.1")
|
||||
pc_d.connect_nic(nic_d)
|
||||
|
||||
Creating the four nodes results in:
|
||||
|
||||
**node_a NIC table**
|
||||
|
||||
+-------------------+--------------+---------------+-----------------+--------------+----------+
|
||||
| MAC Address | IP Address | Subnet Mask | Default Gateway | Speed (Mbps) | Status |
|
||||
+===================+==============+===============+=================+==============+==========+
|
||||
| 80:af:f2:f6:58:b7 | 102.169.0.10 | 255.255.255.0 | 192.168.0.1 | 100 | Disabled |
|
||||
+-------------------+--------------+---------------+-----------------+--------------+----------+
|
||||
|
||||
**node_a sys log**
|
||||
|
||||
.. code-block::
|
||||
|
||||
2023-08-08 15:50:08,355 INFO: Connected NIC 80:af:f2:f6:58:b7/192.168.0.10
|
||||
|
||||
**node_b NIC table**
|
||||
|
||||
+-------------------+--------------+---------------+-----------------+--------------+----------+
|
||||
| MAC Address | IP Address | Subnet Mask | Default Gateway | Speed (Mbps) | Status |
|
||||
+===================+==============+===============+=================+==============+==========+
|
||||
| 98:ad:eb:7c:dc:cb | 102.169.0.11 | 255.255.255.0 | 192.168.0.1 | 100 | Disabled |
|
||||
+-------------------+--------------+---------------+-----------------+--------------+----------+
|
||||
|
||||
**node_b sys log**
|
||||
|
||||
.. code-block::
|
||||
|
||||
2023-08-08 15:50:08,357 INFO: Connected NIC 98:ad:eb:7c:dc:cb/192.168.0.11
|
||||
|
||||
**node_c NIC table**
|
||||
|
||||
+-------------------+--------------+---------------+-----------------+--------------+----------+
|
||||
| MAC Address | IP Address | Subnet Mask | Default Gateway | Speed (Mbps) | Status |
|
||||
+===================+==============+===============+=================+==============+==========+
|
||||
| bc:72:82:5d:82:a4 | 102.169.0.12 | 255.255.255.0 | 192.168.0.1 | 100 | Disabled |
|
||||
+-------------------+--------------+---------------+-----------------+--------------+----------+
|
||||
|
||||
**node_c sys log**
|
||||
|
||||
.. code-block::
|
||||
|
||||
2023-08-08 15:50:08,358 INFO: Connected NIC bc:72:82:5d:82:a4/192.168.0.12
|
||||
|
||||
**node_d NIC table**
|
||||
|
||||
+-------------------+--------------+---------------+-----------------+--------------+----------+
|
||||
| MAC Address | IP Address | Subnet Mask | Default Gateway | Speed (Mbps) | Status |
|
||||
+===================+==============+===============+=================+==============+==========+
|
||||
| 84:20:7c:ec:a5:c6 | 102.169.0.13 | 255.255.255.0 | 192.168.0.1 | 100 | Disabled |
|
||||
+-------------------+--------------+---------------+-----------------+--------------+----------+
|
||||
|
||||
**node_d sys log**
|
||||
|
||||
.. code-block::
|
||||
|
||||
2023-08-08 15:50:08,359 INFO: Connected NIC 84:20:7c:ec:a5:c6/192.168.0.13
|
||||
|
||||
---------------
|
||||
Create Switches
|
||||
Node Attributes
|
||||
---------------
|
||||
|
||||
Next, we'll create two six-port switches:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
switch_1 = Switch(hostname="switch_1", num_ports=6, operating_state=NodeOperatingState.ON)
|
||||
|
||||
switch_2 = Switch(hostname="switch_2", num_ports=6, operating_state=NodeOperatingState.ON)
|
||||
|
||||
This produces:
|
||||
|
||||
**switch_1 MAC table**
|
||||
|
||||
+------+-------------------+--------------+----------+
|
||||
| Port | MAC Address | Speed (Mbps) | Status |
|
||||
+======+===================+==============+==========+
|
||||
| 1 | 9d:ac:59:a0:05:13 | 100 | Disabled |
|
||||
+------+-------------------+--------------+----------+
|
||||
| 2 | 45:f5:8e:b6:f5:d3 | 100 | Disabled |
|
||||
+------+-------------------+--------------+----------+
|
||||
| 3 | ef:f5:b9:28:cb:ae | 100 | Disabled |
|
||||
+------+-------------------+--------------+----------+
|
||||
| 4 | 88:76:0a:72:fc:14 | 100 | Disabled |
|
||||
+------+-------------------+--------------+----------+
|
||||
| 5 | 79:de:da:bd:e2:ba | 100 | Disabled |
|
||||
+------+-------------------+--------------+----------+
|
||||
| 6 | 91:d5:83:a0:02:f2 | 100 | Disabled |
|
||||
+------+-------------------+--------------+----------+
|
||||
|
||||
**switch_1 sys log**
|
||||
|
||||
.. code-block::
|
||||
|
||||
2023-08-08 15:50:08,373 INFO: Turned on
|
||||
|
||||
**switch_2 MAC table**
|
||||
|
||||
+------+-------------------+--------------+----------+
|
||||
| Port | MAC Address | Speed (Mbps) | Status |
|
||||
+======+===================+==============+==========+
|
||||
| 1 | aa:58:fa:66:d7:be | 100 | Disabled |
|
||||
+------+-------------------+--------------+----------+
|
||||
| 2 | 72:d2:1e:88:e9:45 | 100 | Disabled |
|
||||
+------+-------------------+--------------+----------+
|
||||
| 3 | 8a:fc:2a:56:d5:c5 | 100 | Disabled |
|
||||
+------+-------------------+--------------+----------+
|
||||
| 4 | fb:b5:9a:04:4a:49 | 100 | Disabled |
|
||||
+------+-------------------+--------------+----------+
|
||||
| 5 | 88:aa:48:d0:21:9e | 100 | Disabled |
|
||||
+------+-------------------+--------------+----------+
|
||||
| 6 | 96:77:39:d1:de:44 | 100 | Disabled |
|
||||
+------+-------------------+--------------+----------+
|
||||
|
||||
**switch_2 sys log**
|
||||
|
||||
.. code-block::
|
||||
|
||||
2023-08-08 15:50:08,374 INFO: Turned on
|
||||
|
||||
------------
|
||||
Create Links
|
||||
------------
|
||||
|
||||
Finally, we'll create the five links that connect the nodes and the switches:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
link_nic_a_switch_1 = Link(endpoint_a=nic_a, endpoint_b=switch_1.switch_ports[1])
|
||||
link_nic_b_switch_1 = Link(endpoint_a=nic_b, endpoint_b=switch_1.switch_ports[2])
|
||||
link_nic_c_switch_2 = Link(endpoint_a=nic_c, endpoint_b=switch_2.switch_ports[1])
|
||||
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]
|
||||
)
|
||||
|
||||
This produces:
|
||||
|
||||
**node_a NIC table**
|
||||
|
||||
+-------------------+--------------+---------------+-----------------+--------------+---------+
|
||||
| MAC Address | IP Address | Subnet Mask | Default Gateway | Speed (Mbps) | Status |
|
||||
+===================+==============+===============+=================+==============+=========+
|
||||
| 80:af:f2:f6:58:b7 | 102.169.0.10 | 255.255.255.0 | 192.168.0.1 | 100 | Enabled |
|
||||
+-------------------+--------------+---------------+-----------------+--------------+---------+
|
||||
|
||||
**node_a sys log**
|
||||
|
||||
.. code-block::
|
||||
|
||||
2023-08-08 15:50:08,355 INFO: Connected NIC 80:af:f2:f6:58:b7/192.168.0.10
|
||||
2023-08-08 15:50:08,355 INFO: Turned on
|
||||
2023-08-08 15:50:08,355 INFO: NIC 80:af:f2:f6:58:b7/192.168.0.10 enabled
|
||||
|
||||
**node_b NIC table**
|
||||
|
||||
+-------------------+--------------+---------------+-----------------+--------------+---------+
|
||||
| MAC Address | IP Address | Subnet Mask | Default Gateway | Speed (Mbps) | Status |
|
||||
+===================+==============+===============+=================+==============+=========+
|
||||
| 98:ad:eb:7c:dc:cb | 102.169.0.11 | 255.255.255.0 | 192.168.0.1 | 100 | Enabled |
|
||||
+-------------------+--------------+---------------+-----------------+--------------+---------+
|
||||
|
||||
**node_b sys log**
|
||||
|
||||
.. code-block::
|
||||
|
||||
2023-08-08 15:50:08,357 INFO: Connected NIC 98:ad:eb:7c:dc:cb/192.168.0.11
|
||||
2023-08-08 15:50:08,357 INFO: Turned on
|
||||
2023-08-08 15:50:08,357 INFO: NIC 98:ad:eb:7c:dc:cb/192.168.0.11 enabled
|
||||
|
||||
**node_c NIC table**
|
||||
|
||||
+-------------------+--------------+---------------+-----------------+--------------+---------+
|
||||
| MAC Address | IP Address | Subnet Mask | Default Gateway | Speed (Mbps) | Status |
|
||||
+===================+==============+===============+=================+==============+=========+
|
||||
| bc:72:82:5d:82:a4 | 102.169.0.12 | 255.255.255.0 | 192.168.0.1 | 100 | Enabled |
|
||||
+-------------------+--------------+---------------+-----------------+--------------+---------+
|
||||
|
||||
**node_c sys log**
|
||||
|
||||
.. code-block::
|
||||
|
||||
2023-08-08 15:50:08,358 INFO: Connected NIC bc:72:82:5d:82:a4/192.168.0.12
|
||||
2023-08-08 15:50:08,358 INFO: Turned on
|
||||
2023-08-08 15:50:08,358 INFO: NIC bc:72:82:5d:82:a4/192.168.0.12 enabled
|
||||
|
||||
**node_d NIC table**
|
||||
|
||||
+-------------------+--------------+---------------+-----------------+--------------+---------+
|
||||
| MAC Address | IP Address | Subnet Mask | Default Gateway | Speed (Mbps) | Status |
|
||||
+===================+==============+===============+=================+==============+=========+
|
||||
| 84:20:7c:ec:a5:c6 | 102.169.0.13 | 255.255.255.0 | 192.168.0.1 | 100 | Enabled |
|
||||
+-------------------+--------------+---------------+-----------------+--------------+---------+
|
||||
|
||||
**node_d sys log**
|
||||
|
||||
.. code-block::
|
||||
|
||||
2023-08-08 15:50:08,359 INFO: Connected NIC 84:20:7c:ec:a5:c6/192.168.0.13
|
||||
2023-08-08 15:50:08,360 INFO: Turned on
|
||||
2023-08-08 15:50:08,360 INFO: NIC 84:20:7c:ec:a5:c6/192.168.0.13 enabled
|
||||
|
||||
**switch_1 MAC table**
|
||||
|
||||
+------+-------------------+--------------+----------+
|
||||
| Port | MAC Address | Speed (Mbps) | Status |
|
||||
+======+===================+==============+==========+
|
||||
| 1 | 9d:ac:59:a0:05:13 | 100 | Enabled |
|
||||
+------+-------------------+--------------+----------+
|
||||
| 2 | 45:f5:8e:b6:f5:d3 | 100 | Enabled |
|
||||
+------+-------------------+--------------+----------+
|
||||
| 3 | ef:f5:b9:28:cb:ae | 100 | Disabled |
|
||||
+------+-------------------+--------------+----------+
|
||||
| 4 | 88:76:0a:72:fc:14 | 100 | Disabled |
|
||||
+------+-------------------+--------------+----------+
|
||||
| 5 | 79:de:da:bd:e2:ba | 100 | Disabled |
|
||||
+------+-------------------+--------------+----------+
|
||||
| 6 | 91:d5:83:a0:02:f2 | 100 | Enabled |
|
||||
+------+-------------------+--------------+----------+
|
||||
|
||||
|
||||
**switch_1 sys log**
|
||||
|
||||
.. code-block::
|
||||
|
||||
2023-08-08 15:50:08,373 INFO: Turned on
|
||||
2023-08-08 15:50:08,378 INFO: SwitchPort 9d:ac:59:a0:05:13 enabled
|
||||
2023-08-08 15:50:08,380 INFO: SwitchPort 45:f5:8e:b6:f5:d3 enabled
|
||||
2023-08-08 15:50:08,384 INFO: SwitchPort 91:d5:83:a0:02:f2 enabled
|
||||
|
||||
|
||||
**switch_2 MAC table**
|
||||
|
||||
+------+-------------------+--------------+----------+
|
||||
| Port | MAC Address | Speed (Mbps) | Status |
|
||||
+======+===================+==============+==========+
|
||||
| 1 | aa:58:fa:66:d7:be | 100 | Enabled |
|
||||
+------+-------------------+--------------+----------+
|
||||
| 2 | 72:d2:1e:88:e9:45 | 100 | Enabled |
|
||||
+------+-------------------+--------------+----------+
|
||||
| 3 | 8a:fc:2a:56:d5:c5 | 100 | Disabled |
|
||||
+------+-------------------+--------------+----------+
|
||||
| 4 | fb:b5:9a:04:4a:49 | 100 | Disabled |
|
||||
+------+-------------------+--------------+----------+
|
||||
| 5 | 88:aa:48:d0:21:9e | 100 | Disabled |
|
||||
+------+-------------------+--------------+----------+
|
||||
| 6 | 96:77:39:d1:de:44 | 100 | Enabled |
|
||||
+------+-------------------+--------------+----------+
|
||||
|
||||
|
||||
**switch_2 sys log**
|
||||
|
||||
.. code-block::
|
||||
|
||||
2023-08-08 15:50:08,374 INFO: Turned on
|
||||
2023-08-08 15:50:08,381 INFO: SwitchPort aa:58:fa:66:d7:be enabled
|
||||
2023-08-08 15:50:08,383 INFO: SwitchPort 72:d2:1e:88:e9:45 enabled
|
||||
2023-08-08 15:50:08,384 INFO: SwitchPort 96:77:39:d1:de:44 enabled
|
||||
|
||||
|
||||
------------
|
||||
Perform Ping
|
||||
------------
|
||||
|
||||
Now with the network setup and operational, we can perform a ping to confirm that communication between nodes over a
|
||||
switched network is possible. In the below example, we ping 192.168.0.13 (node_d) from node_a:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
pc_a.ping("192.168.0.13")
|
||||
|
||||
|
||||
This produces:
|
||||
|
||||
**node_a sys log**
|
||||
|
||||
.. code-block::
|
||||
|
||||
2023-08-08 15:50:08,355 INFO: Connected NIC 80:af:f2:f6:58:b7/192.168.0.10
|
||||
2023-08-08 15:50:08,355 INFO: Turned on
|
||||
2023-08-08 15:50:08,355 INFO: NIC 80:af:f2:f6:58:b7/192.168.0.10 enabled
|
||||
2023-08-08 15:50:08,406 INFO: Attempting to ping 192.168.0.13
|
||||
2023-08-08 15:50:08,406 INFO: No entry in ARP cache for 192.168.0.13
|
||||
2023-08-08 15:50:08,406 INFO: Sending ARP request from NIC 80:af:f2:f6:58:b7/192.168.0.10 for ip 192.168.0.13
|
||||
2023-08-08 15:50:08,413 INFO: Received ARP response for 192.168.0.13 from 84:20:7c:ec:a5:c6 via NIC 80:af:f2:f6:58:b7/192.168.0.10
|
||||
2023-08-08 15:50:08,413 INFO: Adding ARP cache entry for 84:20:7c:ec:a5:c6/192.168.0.13 via NIC 80:af:f2:f6:58:b7/192.168.0.10
|
||||
2023-08-08 15:50:08,415 INFO: Sending echo request to 192.168.0.13
|
||||
2023-08-08 15:50:08,417 INFO: Received echo reply from 192.168.0.13
|
||||
2023-08-08 15:50:08,419 INFO: Sending echo request to 192.168.0.13
|
||||
2023-08-08 15:50:08,421 INFO: Received echo reply from 192.168.0.13
|
||||
2023-08-08 15:50:08,422 INFO: Sending echo request to 192.168.0.13
|
||||
2023-08-08 15:50:08,424 INFO: Received echo reply from 192.168.0.13
|
||||
2023-08-08 15:50:08,425 INFO: Sending echo request to 192.168.0.13
|
||||
2023-08-08 15:50:08,427 INFO: Received echo reply from 192.168.0.13
|
||||
|
||||
|
||||
**node_b sys log**
|
||||
|
||||
.. code-block::
|
||||
|
||||
2023-08-08 15:50:08,357 INFO: Connected NIC 98:ad:eb:7c:dc:cb/192.168.0.11
|
||||
2023-08-08 15:50:08,357 INFO: Turned on
|
||||
2023-08-08 15:50:08,357 INFO: NIC 98:ad:eb:7c:dc:cb/192.168.0.11 enabled
|
||||
2023-08-08 15:50:08,410 INFO: Received ARP request for 192.168.0.13 from 80:af:f2:f6:58:b7/192.168.0.10
|
||||
2023-08-08 15:50:08,410 INFO: Ignoring ARP request for 192.168.0.13
|
||||
|
||||
|
||||
**node_c sys log**
|
||||
|
||||
.. code-block::
|
||||
|
||||
2023-08-08 15:50:08,358 INFO: Connected NIC bc:72:82:5d:82:a4/192.168.0.12
|
||||
2023-08-08 15:50:08,358 INFO: Turned on
|
||||
2023-08-08 15:50:08,358 INFO: NIC bc:72:82:5d:82:a4/192.168.0.12 enabled
|
||||
2023-08-08 15:50:08,411 INFO: Received ARP request for 192.168.0.13 from 80:af:f2:f6:58:b7/192.168.0.10
|
||||
2023-08-08 15:50:08,411 INFO: Ignoring ARP request for 192.168.0.13
|
||||
|
||||
|
||||
**node_d sys log**
|
||||
|
||||
.. code-block::
|
||||
|
||||
2023-08-08 15:50:08,359 INFO: Connected NIC 84:20:7c:ec:a5:c6/192.168.0.13
|
||||
2023-08-08 15:50:08,360 INFO: Turned on
|
||||
2023-08-08 15:50:08,360 INFO: NIC 84:20:7c:ec:a5:c6/192.168.0.13 enabled
|
||||
2023-08-08 15:50:08,412 INFO: Received ARP request for 192.168.0.13 from 80:af:f2:f6:58:b7/192.168.0.10
|
||||
2023-08-08 15:50:08,412 INFO: Adding ARP cache entry for 80:af:f2:f6:58:b7/192.168.0.10 via NIC 84:20:7c:ec:a5:c6/192.168.0.13
|
||||
2023-08-08 15:50:08,412 INFO: Sending ARP reply from 84:20:7c:ec:a5:c6/192.168.0.13 to 192.168.0.10/80:af:f2:f6:58:b7
|
||||
2023-08-08 15:50:08,416 INFO: Received echo request from 192.168.0.10
|
||||
2023-08-08 15:50:08,417 INFO: Sending echo reply to 192.168.0.10
|
||||
2023-08-08 15:50:08,420 INFO: Received echo request from 192.168.0.10
|
||||
2023-08-08 15:50:08,420 INFO: Sending echo reply to 192.168.0.10
|
||||
2023-08-08 15:50:08,423 INFO: Received echo request from 192.168.0.10
|
||||
2023-08-08 15:50:08,423 INFO: Sending echo reply to 192.168.0.10
|
||||
2023-08-08 15:50:08,426 INFO: Received echo request from 192.168.0.10
|
||||
2023-08-08 15:50:08,426 INFO: Sending echo reply to 192.168.0.10
|
||||
|
||||
|
||||
|
||||
**switch_1 sys log**
|
||||
|
||||
.. code-block::
|
||||
|
||||
2023-08-08 15:50:08,373 INFO: Turned on
|
||||
2023-08-08 15:50:08,378 INFO: SwitchPort 9d:ac:59:a0:05:13 enabled
|
||||
2023-08-08 15:50:08,380 INFO: SwitchPort 45:f5:8e:b6:f5:d3 enabled
|
||||
2023-08-08 15:50:08,384 INFO: SwitchPort 91:d5:83:a0:02:f2 enabled
|
||||
2023-08-08 15:50:08,409 INFO: Added MAC table entry: Port 1 -> 80:af:f2:f6:58:b7
|
||||
2023-08-08 15:50:08,413 INFO: Added MAC table entry: Port 6 -> 84:20:7c:ec:a5:c6
|
||||
|
||||
|
||||
|
||||
**switch_2 sys log**
|
||||
|
||||
.. code-block::
|
||||
|
||||
2023-08-08 15:50:08,374 INFO: Turned on
|
||||
2023-08-08 15:50:08,381 INFO: SwitchPort aa:58:fa:66:d7:be enabled
|
||||
2023-08-08 15:50:08,383 INFO: SwitchPort 72:d2:1e:88:e9:45 enabled
|
||||
2023-08-08 15:50:08,384 INFO: SwitchPort 96:77:39:d1:de:44 enabled
|
||||
2023-08-08 15:50:08,411 INFO: Added MAC table entry: Port 6 -> 80:af:f2:f6:58:b7
|
||||
2023-08-08 15:50:08,412 INFO: Added MAC table entry: Port 2 -> 84:20:7c:ec:a5:c6
|
||||
- **hostname**: The network hostname of the node.
|
||||
- **operating_state**: Indicates the current hardware state of the node.
|
||||
- **network_interfaces**: Maps interface names to NetworkInterface objects on the node.
|
||||
- **network_interface**: Maps port IDs to ``NetworkInterface`` objects on the node.
|
||||
- **dns_server**: Specifies DNS servers for domain name resolution.
|
||||
- **start_up_duration**: The time it takes for the node to become fully operational after being powered on.
|
||||
- **shut_down_duration**: The time required for the node to properly shut down.
|
||||
- **sys_log**: A system log for recording events related to the node.
|
||||
- **session_manager**: Manages user sessions within the node.
|
||||
- **software_manager**: Controls the installation and management of software and services on the node.
|
||||
|
||||
Node Behaviours/Functions
|
||||
-------------------------
|
||||
|
||||
|
||||
- **connect_nic()**: Connects a ``NetworkInterface`` to the node for network communication.
|
||||
- **disconnect_nic()**: Removes a ``NetworkInterface`` from the node.
|
||||
- **receive_frame()**: Handles the processing of incoming network frames.
|
||||
- **apply_timestep()**: Advances the state of the node according to the simulation timestep.
|
||||
- **power_on()**: Initiates the node, enabling all connected Network Interfaces and starting all Services and
|
||||
Applications, taking into account the `start_up_duration`.
|
||||
- **power_off()**: Stops the node's operations, adhering to the `shut_down_duration`.
|
||||
- **ping()**: Sends ICMP echo requests to a specified IP address to test connectivity.
|
||||
- **has_enabled_network_interface()**: Checks if the node has any network interfaces enabled, facilitating network
|
||||
communication.
|
||||
- **show()**: Provides a summary of the node's current state, including network interfaces, operational status, and
|
||||
other key attributes.
|
||||
|
||||
|
||||
The Node class handles installation of system software, network connectivity, frame processing, system logging, and
|
||||
power states. It establishes baseline functionality while allowing subclassing to model specific node types like hosts,
|
||||
routers, firewalls etc. The flexible architecture enables composing complex network topologies.
|
||||
|
||||
@@ -66,9 +66,9 @@ we'll use the following Network that has a client, server, two switches, and a r
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
network.connect(endpoint_a=router_1.ethernet_ports[1], endpoint_b=switch_1.switch_ports[6])
|
||||
network.connect(endpoint_a=router_1.network_interfaces[1], endpoint_b=switch_1.network_interface[6])
|
||||
router_1.enable_port(1)
|
||||
network.connect(endpoint_a=router_1.ethernet_ports[2], endpoint_b=switch_2.switch_ports[6])
|
||||
network.connect(endpoint_a=router_1.network_interfaces[2], endpoint_b=switch_2.network_interface[6])
|
||||
router_1.enable_port(2)
|
||||
|
||||
6. Create the Client and Server nodes.
|
||||
@@ -94,8 +94,8 @@ we'll use the following Network that has a client, server, two switches, and a r
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
network.connect(endpoint_a=switch_2.switch_ports[1], endpoint_b=client_1.ethernet_port[1])
|
||||
network.connect(endpoint_a=switch_1.switch_ports[1], endpoint_b=server_1.ethernet_port[1])
|
||||
network.connect(endpoint_a=switch_2.network_interface[1], endpoint_b=client_1.network_interface[1])
|
||||
network.connect(endpoint_a=switch_1.network_interface[1], endpoint_b=server_1.network_interface[1])
|
||||
|
||||
8. Add ACL rules on the Router to allow ARP and ICMP traffic.
|
||||
|
||||
|
||||
118
docs/source/simulation_components/network/network_interfaces.rst
Normal file
118
docs/source/simulation_components/network/network_interfaces.rst
Normal file
@@ -0,0 +1,118 @@
|
||||
.. only:: comment
|
||||
|
||||
© Crown-owned copyright 2023, Defence Science and Technology Laboratory UK
|
||||
|
||||
#################################
|
||||
Network Interface Hierarchy Model
|
||||
#################################
|
||||
|
||||
The network interface hierarchy model is designed to represent the various types of network interfaces and their
|
||||
functionalities within a networking system. This model is organised into five distinct layers, each serving a specific
|
||||
purpose in the abstraction, implementation, and utilisation of network interfaces. This hierarchical structure
|
||||
facilitates modular development, enhances maintainability, and supports scalability by clearly separating concerns and
|
||||
allowing for focused enhancements within each layer.
|
||||
|
||||
.. image:: primaite_network_interface_model.png
|
||||
|
||||
Layer Descriptions
|
||||
==================
|
||||
|
||||
#. **Base Layer**
|
||||
|
||||
* **Purpose:** Serves as the foundation of the hierarchy, defining the most abstract properties and behaviours common
|
||||
to all network interfaces.
|
||||
* **Content:** Contains the NetworkInterface class, which abstracts basic functionalities such as enabling/disabling
|
||||
the interface, sending, and receiving frames.
|
||||
* **Significance:** Ensures that core functionalities are universally available across all types of network
|
||||
interfaces, promoting code reuse and consistency.
|
||||
|
||||
#. **Connection Type Layer**
|
||||
|
||||
* **Purpose:** Differentiates network interfaces based on their physical connection type: wired or wireless.
|
||||
* **Content:** Includes ``WiredNetworkInterface`` and ``WirelessNetworkInterface`` classes, each tailoring the base
|
||||
functionalities to specific mediums.
|
||||
* **Significance:** Allows the development of medium-specific features (e.g., handling point-to-point links in
|
||||
wired devices) while maintaining a clear separation from IP-related functionalities.
|
||||
|
||||
#. **IP Layer**
|
||||
|
||||
* **Purpose:** Introduces Internet Protocol (IP) capabilities to network interfaces, enabling IP-based networking.
|
||||
* **Content:** Includes ``IPWiredNetworkInterface`` and ``IPWirelessNetworkInterface`` classes, extending connection
|
||||
type-specific classes with IP functionalities.
|
||||
* **Significance:** Facilitates the implementation of IP address assignment, subnetting, and other Layer 3
|
||||
networking features, crucial for modern networking applications.
|
||||
|
||||
#. **Interface Layer**
|
||||
|
||||
* **Purpose:** Defines concrete implementations of network interfaces for specific devices or roles within a network.
|
||||
* **Content:** Includes ``NIC``, ``RouterInterface``, ``SwitchPort``, ``WirelessNIC``, and ``WirelessAccessPoint``
|
||||
classes, each designed for a particular networking function or device.
|
||||
* **Significance:** This layer allows developers to directly utilise or extend pre-built interfaces tailored to
|
||||
specific networking tasks, enhancing development efficiency and clarity.
|
||||
|
||||
#. **Device Layer**
|
||||
|
||||
* **Purpose:** Maps the concrete interface implementations to their respective devices within a network,
|
||||
illustrating practical usage scenarios.
|
||||
* **Content:** Conceptually groups devices such as ``Computer``, ``Server``, ``Switch``, ``Router``, and ``Firewall``
|
||||
with the interfaces they utilise (e.g., ``Computer`` might use ``NIC`` or ``WirelessNIC``).
|
||||
* **Significance:** Provides a clear understanding of how various network interfaces are applied in real-world
|
||||
devices, aiding in system design and architecture planning.
|
||||
|
||||
|
||||
Network Interface Classes
|
||||
=========================
|
||||
|
||||
**NetworkInterface (Base Layer)**
|
||||
|
||||
Abstract base class defining core interface properties like MAC address, speed, MTU.
|
||||
Requires subclasses implement key methods like send/receive frames, enable/disable interface.
|
||||
Establishes universal network interface capabilities.
|
||||
|
||||
**WiredNetworkInterface (Connection Type Layer)**
|
||||
|
||||
- Extends NetworkInterface for wired connection interfaces.
|
||||
- Adds notions of physical/logical connectivity and link management.
|
||||
- Mandates subclasses implement wired-specific methods.
|
||||
|
||||
**WirelessNetworkInterface (Connection Type Layer)**
|
||||
|
||||
- Extends NetworkInterface for wireless interfaces.
|
||||
- Encapsulates wireless-specific behaviours like signal strength handling.
|
||||
- Requires wireless-specific methods in subclasses.
|
||||
|
||||
**Layer3Interface (IP Layer)**
|
||||
|
||||
- Introduces IP addressing abilities with ip_address and subnet_mask.
|
||||
- Validates address configuration.
|
||||
- Enables participation in IP networking.
|
||||
|
||||
**IPWiredNetworkInterface (IP Layer)**
|
||||
|
||||
- Merges Layer3Interface and WiredNetworkInterface.
|
||||
- Defines wired interfaces with IP capabilities.
|
||||
- Meant to be extended, doesn't implement methods.
|
||||
|
||||
**IPWirelessNetworkInterface (IP Layer)**
|
||||
|
||||
- Combines Layer3Interface and WirelessNetworkInterface.
|
||||
- Represents wireless interfaces with IP capabilities.
|
||||
- Intended to be extended and specialised.
|
||||
|
||||
**NIC (Interface Layer)**
|
||||
|
||||
- Concrete wired NIC implementation combining IPWiredNetworkInterface and Layer3Interface.
|
||||
- Provides network connectivity for host nodes.
|
||||
- Manages MAC and IP addressing, frame processing.
|
||||
|
||||
**WirelessNIC (Interface Layer)**
|
||||
|
||||
- Concrete wireless NIC implementation combining IPWirelessNetworkInterface and Layer3Interface.
|
||||
- Delivers wireless connectivity with IP for hosts.
|
||||
- Handles wireless transmission/reception.
|
||||
|
||||
**WirelessAccessPoint (Interface Layer)**
|
||||
|
||||
- Concrete wireless access point implementation using IPWirelessNetworkInterface and Layer3Interface.
|
||||
- Bridges wireless and wired networks.
|
||||
- Manages wireless network.
|
||||
@@ -0,0 +1,47 @@
|
||||
|
||||
#########
|
||||
Host Node
|
||||
#########
|
||||
|
||||
The ``host_node.py`` module is a core component of the PrimAITE project, aimed at simulating network host. It
|
||||
encapsulates the functionality necessary for modelling the behaviour, communication capabilities, and interactions of
|
||||
hosts in a networked environment.
|
||||
|
||||
|
||||
HostNode Class
|
||||
==============
|
||||
|
||||
The ``HostNode`` class acts as a foundational representation of a networked device or computer, capable of both
|
||||
initiating and responding to network communications.
|
||||
|
||||
**Attributes:**
|
||||
|
||||
- Manages IP addressing with support for IPv4.
|
||||
- Employs ``NIC`` or ``WirelessNIC`` (subclasses of``IPWiredNetworkInterface``) to simulate wired network connections.
|
||||
- Integrates with ``SysLog`` for logging operational events, aiding in debugging and monitoring the host node's
|
||||
behaviour.
|
||||
|
||||
**Key Methods:**
|
||||
|
||||
- Facilitates the sending and receiving of ``Frame`` objects to simulate data link layer communications.
|
||||
- Manages a variety of network services and applications, enhancing the simulation's realism and functionality.
|
||||
|
||||
Default Services and Applications
|
||||
=================================
|
||||
|
||||
Both the ``HostNode`` and its subclasses come equipped with a suite of default services and applications critical for
|
||||
fundamental network operations:
|
||||
|
||||
1. **ARP (Address Resolution Protocol):** The ``HostARP`` subclass enhances ARP functionality for host-specific
|
||||
operations.
|
||||
|
||||
2. **DNS (Domain Name System) Client:** Facilitates domain name resolution to IP addresses, enabling web navigation.
|
||||
|
||||
3. **FTP (File Transfer Protocol) Client:** Supports file transfers across the network.
|
||||
|
||||
4. **ICMP (Internet Control Message Protocol):** Utilised for network diagnostics and control, such as executing ping
|
||||
requests.
|
||||
|
||||
5. **NTP (Network Time Protocol) Client:** Synchronises the host's clock with network time servers.
|
||||
|
||||
6. **Web Browser:** A simulated application that allows the host to request and display web content.
|
||||
@@ -0,0 +1,41 @@
|
||||
.. only:: comment
|
||||
|
||||
© Crown-owned copyright 2023, Defence Science and Technology Laboratory UK
|
||||
|
||||
############
|
||||
Network Node
|
||||
############
|
||||
|
||||
|
||||
The ``network_node.py`` module within the PrimAITE project is pivotal for simulating network nodes like routers and
|
||||
switches, which are integral to network traffic management. This module establishes the framework for these devices,
|
||||
enabling them to receive and process network frames effectively.
|
||||
|
||||
Overview
|
||||
========
|
||||
|
||||
The module defines the ``NetworkNode`` class, an abstract base class that outlines essential behaviours for network
|
||||
devices tasked with handling network traffic. It is designed to be extended by more specific device simulations that
|
||||
implement these foundational capabilities.
|
||||
|
||||
NetworkNode Class
|
||||
=================
|
||||
|
||||
The ``NetworkNode`` class is at the heart of the module, providing an interface for network devices that participate
|
||||
in the transmission and routing of data within the simulated environment.
|
||||
|
||||
**Key Features:**
|
||||
|
||||
- **Frame Processing:** Central to the class is the ability to receive and process network frames, facilitating the
|
||||
simulation of data flow through network devices.
|
||||
|
||||
- **Abstract Methods:** Includes abstract methods such as ``receive_frame``, which subclasses must implement to specify
|
||||
how devices handle incoming traffic.
|
||||
|
||||
- **Extensibility:** Designed for extension, allowing for the creation of specific device simulations, such as router
|
||||
and switch classes, that embody unique behaviours and functionalities.
|
||||
|
||||
|
||||
The ``network_node.py`` module's abstract approach to defining network devices allows the PrimAITE project to simulate
|
||||
a wide range of network behaviours and scenarios comprehensively. By providing a common framework for all network
|
||||
nodes, it facilitates the development of a modular and scalable simulation environment.
|
||||
41
docs/source/simulation_components/network/nodes/router.rst
Normal file
41
docs/source/simulation_components/network/nodes/router.rst
Normal file
@@ -0,0 +1,41 @@
|
||||
.. only:: comment
|
||||
|
||||
© Crown-owned copyright 2023, Defence Science and Technology Laboratory UK
|
||||
|
||||
######
|
||||
Router
|
||||
######
|
||||
|
||||
The ``router.py`` module is a pivotal component of the PrimAITE, designed to simulate the complex functionalities of a
|
||||
router within a network simulation. Routers are essential for directing traffic between different network segments,
|
||||
and this module provides the tools necessary to model these devices' behaviour and capabilities accurately.
|
||||
|
||||
Router Class
|
||||
------------
|
||||
|
||||
The ``Router`` class embodies the core functionalities of a network router, extending the ``NetworkNode`` class to
|
||||
incorporate routing-specific behaviours.
|
||||
|
||||
**Key Features:**
|
||||
|
||||
- **IP Routing:** Supports dynamic handling of IP packets, including forwarding based on destination IP addresses and
|
||||
subnetting.
|
||||
- **Routing Table:** Maintains a routing table to determine the best path for forwarding packets.
|
||||
- **Protocol Support:** Implements support for key networking protocols, including ARP for address resolution and ICMP
|
||||
for diagnostic messages.
|
||||
- **Interface Management:** Manages multiple ``RouterInterface`` instances, enabling connections to different network
|
||||
segments.
|
||||
- **Network Interface Configuration:** Tools for configuring router interfaces, including setting IP addresses, subnet
|
||||
masks, and enabling/disabling interfaces.
|
||||
- **Logging and Monitoring:** Integrates with ``SysLog`` for logging operational events, aiding in debugging and
|
||||
monitoring router behaviour.
|
||||
|
||||
**Operations:**
|
||||
|
||||
- **Packet Forwarding:** Utilises the routing table to forward packets to their correct destination across
|
||||
interconnected networks.
|
||||
- **ARP Handling:** Responds to ARP requests for any IP addresses configured on its interfaces, facilitating
|
||||
communication within local networks.
|
||||
- **ICMP Processing:** Generates and processes ICMP packets, such as echo requests and replies, for network diagnostics.
|
||||
|
||||
The ``router.py`` module offers a comprehensive simulation of router functionalities. By providing detailed modelling of router operations, including packet forwarding, interface management, and protocol handling, PrimAITE enables the exploration of advanced network topologies and routing scenarios.
|
||||
29
docs/source/simulation_components/network/nodes/switch.rst
Normal file
29
docs/source/simulation_components/network/nodes/switch.rst
Normal file
@@ -0,0 +1,29 @@
|
||||
.. only:: comment
|
||||
|
||||
© Crown-owned copyright 2023, Defence Science and Technology Laboratory UK
|
||||
|
||||
######
|
||||
Switch
|
||||
######
|
||||
|
||||
The ``switch.py`` module is a crucial component of the PrimAITE, aimed at simulating network switches within a network simulation environment. Network switches play a vital role in managing data flow within local area networks (LANs) by forwarding frames based on MAC addresses. This module provides a comprehensive framework for modelling switch operations and behaviours.
|
||||
|
||||
Switch Class Overview
|
||||
---------------------
|
||||
|
||||
The module introduces the concept of switch ports through the ``SwitchPort`` class, which extends the functionality of ``WiredNetworkInterface`` to simulate the operation of switch ports in a network.
|
||||
|
||||
**Key Features:**
|
||||
|
||||
- **Data Link Layer Operation:** Operates at the data link layer (Layer 2) of the OSI model, handling the reception and forwarding of frames based on MAC addresses.
|
||||
- **Port Management:** Tools for configuring switch ports, including enabling/disabling ports, setting port speeds, and managing port security features.
|
||||
- **Logging and Monitoring:** Integrates with ``SysLog`` for logging operational events, aiding in debugging and
|
||||
monitoring switch behaviour.
|
||||
|
||||
Functionality and Implementation
|
||||
---------------------------------
|
||||
|
||||
- **MAC Address Learning:** Dynamically learns and associates MAC addresses with switch ports, enabling intelligent frame forwarding.
|
||||
- **Frame Forwarding:** Utilises the learned MAC address table to forward frames only to the specific port associated with the destination MAC address, minimising unnecessary network traffic.
|
||||
|
||||
The ``switch.py`` module offers a realistic and configurable representation of switch operations. By detailing the functionalities of the ``SwitchPort`` class, the module lays the foundation for simulating complex network topologies.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
@@ -1,73 +0,0 @@
|
||||
.. only:: comment
|
||||
|
||||
© Crown-owned copyright 2023, Defence Science and Technology Laboratory UK
|
||||
|
||||
.. _router:
|
||||
|
||||
Router Module
|
||||
=============
|
||||
|
||||
The router module contains classes for simulating the functions of a network router.
|
||||
|
||||
Router
|
||||
------
|
||||
|
||||
The Router class represents a multi-port network router that can receive, process, and route network packets between its ports and other Nodes
|
||||
|
||||
The router maintains internal state including:
|
||||
|
||||
- RouteTable - Routing table to lookup where to forward packets.
|
||||
- AccessControlList - Access control rules to block or allow packets.
|
||||
- ARP cache - MAC address lookups for connected devices.
|
||||
- ICMP handler - Handles ICMP requests to router interfaces.
|
||||
|
||||
The router receives incoming frames on enabled ports. It processes the frame headers and applies the following logic:
|
||||
|
||||
1. Checks the AccessControlList if the packet is permitted. If blocked, it is dropped.
|
||||
2. For permitted packets, routes the frame based on:
|
||||
- ARP cache lookups for destination MAC address.
|
||||
- ICMP echo requests handled directly.
|
||||
- RouteTable lookup to forward packet out other ports.
|
||||
3. Updates ARP cache as it learns new information about the Network.
|
||||
|
||||
|
||||
|
||||
RouteTable
|
||||
----------
|
||||
|
||||
The RouteTable holds RouteEntry objects representing routes. It finds the best route for a destination IP using a metric and the longest prefix match algorithm.
|
||||
|
||||
Routes can be added and looked up based on destination IP address. The RouteTable is used by the Router when forwarding packets between other Nodes.
|
||||
|
||||
AccessControlList
|
||||
-----------------
|
||||
|
||||
The AccessControlList defines Access Control Rules to block or allow packets. Packets are checked against the rules to determine if they are permitted to be forwarded.
|
||||
|
||||
Rules can be added to deny or permit traffic based on IP, port, and protocol. The ACL is checked by the Router when packets are received.
|
||||
|
||||
Packet Processing
|
||||
-----------------
|
||||
|
||||
-The Router supports the following protocols and packet types:
|
||||
|
||||
ARP
|
||||
^^^
|
||||
|
||||
- Handles both ARP requests and responses.
|
||||
- Updates ARP cache.
|
||||
- Proxies ARP replies for connected networks.
|
||||
- Routes ARP requests.
|
||||
|
||||
ICMP
|
||||
^^^^
|
||||
|
||||
- Responds to ICMP echo requests to Router interfaces.
|
||||
- Routes other ICMP messages based on routes.
|
||||
|
||||
TCP/UDP
|
||||
^^^^^^^
|
||||
|
||||
- Forwards packets based on routes like IP.
|
||||
- Applies ACL rules based on protocol, source/destination IP address, and source/destination port numbers.
|
||||
- Decrements TTL and drops expired TTL packets.
|
||||
@@ -52,7 +52,7 @@ Example
|
||||
default_gateway="192.168.10.1"
|
||||
operating_state=NodeOperatingState.ON # initialise the computer in an ON state
|
||||
)
|
||||
network.connect(endpoint_b=client_1.ethernet_port[1], endpoint_a=switch_2.switch_ports[1])
|
||||
network.connect(endpoint_b=client_1.network_interface[1], endpoint_a=switch_2.network_interface[1])
|
||||
client_1.software_manager.install(DataManipulationBot)
|
||||
data_manipulation_bot: DataManipulationBot = client_1.software_manager.software.get("DataManipulationBot")
|
||||
data_manipulation_bot.configure(server_ip_address=IPv4Address("192.168.1.14"), payload="DELETE")
|
||||
|
||||
@@ -98,7 +98,7 @@ Example peer to peer network
|
||||
subnet_mask="255.255.255.0",
|
||||
operating_state=NodeOperatingState.ON # initialise the server in an ON state
|
||||
)
|
||||
net.connect(pc1.ethernet_port[1], srv.ethernet_port[1])
|
||||
net.connect(pc1.network_interface[1], srv.network_interface[1])
|
||||
|
||||
Install the FTP Server
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 51 KiB |
51
docs/source/simulation_components/system/pcap.rst
Normal file
51
docs/source/simulation_components/system/pcap.rst
Normal file
@@ -0,0 +1,51 @@
|
||||
.. only:: comment
|
||||
|
||||
© Crown-owned copyright 2023, Defence Science and Technology Laboratory UK
|
||||
|
||||
PCAP
|
||||
====
|
||||
|
||||
The ``packet_capture.py`` module introduces a Packet Capture (PCAP) service within PrimAITE, designed to simulate
|
||||
packet capturing functionalities for the simulated network environment. This service enables the logging of network
|
||||
frames as JSON strings, providing valuable insights into the data flowing across the network.
|
||||
|
||||
Overview
|
||||
--------
|
||||
|
||||
Packet capture is a crucial tool in network analysis, troubleshooting, and monitoring, allowing for the examination of
|
||||
packets traversing the network. Within the context of the PrimAITE simulation, the PCAP service enhances the realism
|
||||
and depth of network simulations by offering detailed visibility into network communications. Notably, PCAP is created
|
||||
by default at the NetworkInterface level.
|
||||
|
||||
PacketCapture Class
|
||||
-------------------
|
||||
|
||||
The ``PacketCapture`` class represents the core of the PCAP service, facilitating the capture and logging of network
|
||||
frames for analysis.
|
||||
|
||||
**Features:**
|
||||
|
||||
- **Automatic Creation:** PCAP is automatically created at the NetworkInterface level, simplifying setup and integration.
|
||||
- **Inbound and Outbound Frame Capture:** Frames can be captured and logged separately for inbound and outbound
|
||||
traffic, offering granular insight into network communications.
|
||||
- **Logging Format:** Captures and logs frames as JSON strings, ensuring that the data is structured and easily
|
||||
interpretable.
|
||||
- **File Location:** PCAP logs are saved to a specified directory within the simulation output, organised by hostname
|
||||
and IP address to facilitate easy retrieval and analysis.
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
The PCAP service is seamlessly integrated within the simulation, automatically capturing and logging frames for both
|
||||
inbound and outbound traffic at the NetworkInterface level. This automatic functionality, combined with the ability
|
||||
to separate traffic directions, significantly enhances network analysis and troubleshooting capabilities.
|
||||
|
||||
This service is particularly useful for:
|
||||
|
||||
- **Network Analysis:** Detailed examination of packet flows and protocols within the simulated environment.
|
||||
- **Troubleshooting:** Identifying and resolving network issues by analysing packet transmissions and errors.
|
||||
- **Educational Purposes:** Teaching network principles and diagnostics through hands-on packet analysis.
|
||||
|
||||
The introduction of the ``packet_capture.py`` module significantly enhances the network simulation capabilities of
|
||||
PrimAITE. By providing a robust tool for packet capture and analysis, PrimAITE allows users to gain deeper insights
|
||||
into network operations, supporting a wide range of educational, developmental, and research activities.
|
||||
@@ -0,0 +1,90 @@
|
||||
.. only:: comment
|
||||
|
||||
© Crown-owned copyright 2023, Defence Science and Technology Laboratory UK
|
||||
|
||||
Session and Software Manager
|
||||
============================
|
||||
|
||||
The Software Manager and Session Manager are core components of the Node in PrimAITE. These managers orchestrate the
|
||||
flow of network frames through the Node, ensuring that frames are processed accurately and passed to the relevant
|
||||
services or applications.
|
||||
|
||||
The following flow diagram illustrates the journey of a network frame as it navigates through various components within
|
||||
the node. Starting from the network interface, the frame progresses to the node, then to the session manager, and
|
||||
subsequently to the software manager. From there, it may be directed to one of three potential software destinations:
|
||||
ARP, ICMP, or the Web Client. This pathway exemplifies the structured processing sequence designed to ensure that
|
||||
each frame reaches its intended target within the simulated environment.
|
||||
|
||||
.. image:: node_session_software_model_example.png
|
||||
|
||||
Session Manager
|
||||
---------------
|
||||
|
||||
The `SessionManager` acts as the intermediary between the Node's hardware-level interactions and higher-level software
|
||||
processes. It receives frames from the Node and determines the appropriate session or connection context for further
|
||||
processing.
|
||||
|
||||
**Key Responsibilities:**
|
||||
|
||||
- **Frame Handling:** Receives network frames and identifies the session context based on headers and session state.
|
||||
- **Protocol Management:** Supports various protocols (e.g., ARP, ICMP) by interpreting protocol-specific information
|
||||
within frames and facilitating their processing.
|
||||
- **Session Tracking:** Maintains a record of active sessions and manages their lifecycle, including creation,
|
||||
maintenance, and termination.
|
||||
|
||||
**Implementation Overview:**
|
||||
|
||||
- Utilises IP and transport layer information to route frames to the correct session.
|
||||
- Integrates closely with the `SoftwareManager` to ensure seamless transmission of session-specific data to the
|
||||
application layer.
|
||||
|
||||
Software Manager
|
||||
----------------
|
||||
|
||||
The `SoftwareManager` is responsible for the final step in the frame processing pipeline, handling the delivery of
|
||||
network frames to the appropriate software services or applications within the Node.
|
||||
|
||||
**Key Responsibilities:**
|
||||
|
||||
- **Application Routing:** Determines the target application or service for incoming frames based on protocol and port
|
||||
information.
|
||||
- **Software Management:** Oversees the registration, installation, and management of software services and
|
||||
applications, facilitating communication between network layers and application processes.
|
||||
- **Frame Dispatching:** Directs frames to their designated applications or services, enabling the processing of
|
||||
network communications at the application layer.
|
||||
- **Installation and Uninstallation:** Responsible for the installing and uninstalling of services and applications,
|
||||
managing the availability of software resources on the Node.
|
||||
|
||||
**Implementation Overview:**
|
||||
|
||||
- Maintains a registry of services and applications, keyed by protocol and port numbers, to efficiently route network
|
||||
traffic.
|
||||
- Interacts with the `FileSystem` and other core components to manage application state and data persistence,
|
||||
supporting complex software interactions within the simulated environment.
|
||||
|
||||
Integration and Workflow
|
||||
------------------------
|
||||
|
||||
1. **Initial Port Check:** Upon receiving a network frame at the hardware level, the Node first checks if the
|
||||
destination port and protocol match any software currently running, as managed by the `SoftwareManager`. This step
|
||||
determines if the port is open and if the frame's destination is actively listening for incoming traffic on the Node.
|
||||
2. **Frame Acceptance:** If the frame's destination port and protocol are open on the Node, indicating that there is
|
||||
software prepared to handle such traffic, the Node accepts the frame. This verification ensures that only relevant
|
||||
traffic is processed further, enhancing network security and efficiency.
|
||||
3. **Session Manager Processing:** Accepted frames are then passed to the `SessionManager`, which analyses the frames
|
||||
within the context of existing sessions or connections. The Session Manager performs protocol-specific handling,
|
||||
routing the frames based on session state and protocol requirements.
|
||||
4. **Software Manager Dispatch:** After session processing, frames are dispatched to the `SoftwareManager`, which
|
||||
routes them to the appropriate services or applications. The Software Manager identifies the target based on the
|
||||
frame's destination port and protocol, aligning with the initial port check.
|
||||
5. **Application Processing:** The relevant applications or services process the received frames, completing the
|
||||
communication pathway within the Node. This step involves the actual handling of frame data by the intended software,
|
||||
facilitating the intended network operations or communications.
|
||||
|
||||
|
||||
Together, the Software Manager and Session Manager form a critical part of the Node's architecture in the PrimAITE,
|
||||
facilitating a structured and efficient processing pipeline for network frames. This architecture enables the
|
||||
simulation of realistic network environments, where frames are accurately routed and processed, mirroring the
|
||||
complexities of real-world network communications. The addition of installation and uninstallation capabilities by
|
||||
the Software Manager further enhances the Node's functionality, allowing for dynamic software management within the
|
||||
simulated network.
|
||||
51
docs/source/simulation_components/system/sys_log.rst
Normal file
51
docs/source/simulation_components/system/sys_log.rst
Normal file
@@ -0,0 +1,51 @@
|
||||
.. only:: comment
|
||||
|
||||
© Crown-owned copyright 2023, Defence Science and Technology Laboratory UK
|
||||
|
||||
SysLog
|
||||
======
|
||||
|
||||
The ``sys_log.py`` module introduces a system logging (SysLog) service within PrimAITE, designed to facilitate the
|
||||
management and recording of system logs for nodes in the simulated network environment. This essential service tracks
|
||||
system events, assists in debugging, and aids network analysis by providing a structured and accessible log of
|
||||
activities.
|
||||
|
||||
Overview
|
||||
--------
|
||||
|
||||
System logging is vital in network management and diagnostics, offering a timestamped record of events within network
|
||||
devices. In the PrimAITE simulation context, the SysLog service automatically enables logging at the node level,
|
||||
enhancing the simulation's analysis and troubleshooting capabilities without manual configuration.
|
||||
|
||||
SysLog Class
|
||||
------------
|
||||
|
||||
**Features:**
|
||||
|
||||
- **Automatic Activation:** SysLog is enabled by default at the node level, ensuring comprehensive activity logging
|
||||
with no additional setup.
|
||||
- **Log Levels:** Supports various logging levels, including debug, info, error, etc., allowing for detailed
|
||||
categorisation and severity indication of log messages.
|
||||
- **Terminal Output:** Logs can be printed to the terminal by setting `to_terminal=True`, offering real-time monitoring
|
||||
and debugging capabilities.
|
||||
- **Logging Format:** Records system logs in standard text format for enhanced readability and interpretability.
|
||||
- **File Location:** Systematically saves logs to a designated directory within the simulation output, organised by
|
||||
hostname, facilitating log management and retrieval.
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
SysLog service is seamlessly integrated into the simulation, with automatic activation for each node and support for
|
||||
various logging levels. The addition of terminal output capabilities further enhances the utility of SysLog for
|
||||
real-time event monitoring and troubleshooting.
|
||||
|
||||
This service is invaluable for:
|
||||
|
||||
- **Event Tracking:** Documents key system events, configuration changes, and operational status updates.
|
||||
- **Debugging:** Aids in identifying and resolving simulated network issues by providing a comprehensive event history.
|
||||
- **Network Analysis:** Offers insights into network node behaviour and interactions.
|
||||
|
||||
|
||||
The ``sys_log.py`` module significantly enhances PrimAITE's network simulation capabilities. Providing a robust system
|
||||
logging tool, automatically enabled at the node level and featuring various log levels and terminal output options,
|
||||
PrimAITE enables users to conduct in-depth network simulations.
|
||||
@@ -84,7 +84,7 @@ Example peer to peer network
|
||||
srv = Server(hostname="srv", ip_address="192.168.1.10", subnet_mask="255.255.255.0")
|
||||
pc1.power_on()
|
||||
srv.power_on()
|
||||
net.connect(pc1.ethernet_port[1], srv.ethernet_port[1])
|
||||
net.connect(pc1.network_interface[1], srv.network_interface[1])
|
||||
|
||||
Install the Web Server
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
@@ -690,7 +690,7 @@ simulation:
|
||||
subnet_mask: 255.255.255.0
|
||||
default_gateway: 192.168.1.1
|
||||
dns_server: 192.168.1.10
|
||||
nics:
|
||||
network_interfaces:
|
||||
2: # unfortunately this number is currently meaningless, they're just added in order and take up the next available slot
|
||||
ip_address: 192.168.10.110
|
||||
subnet_mask: 255.255.255.0
|
||||
|
||||
@@ -1070,7 +1070,7 @@ simulation:
|
||||
subnet_mask: 255.255.255.0
|
||||
default_gateway: 192.168.1.1
|
||||
dns_server: 192.168.1.10
|
||||
nics:
|
||||
network_interfaces:
|
||||
2: # unfortunately this number is currently meaningless, they're just added in order and take up the next available slot
|
||||
ip_address: 192.168.10.110
|
||||
subnet_mask: 255.255.255.0
|
||||
|
||||
@@ -550,7 +550,7 @@ class NetworkNICAbstractAction(AbstractAction):
|
||||
nic_num = self.manager.get_nic_num_by_idx(node_idx=node_id, nic_idx=nic_id)
|
||||
if node_name is None or nic_num is None:
|
||||
return ["do_nothing"]
|
||||
return ["network", "node", node_name, "nic", nic_num, self.verb]
|
||||
return ["network", "node", node_name, "network_interface", nic_num, self.verb]
|
||||
|
||||
|
||||
class NetworkNICEnableAction(NetworkNICAbstractAction):
|
||||
@@ -723,8 +723,8 @@ class ActionManager:
|
||||
node_obj = self.game.simulation.network.get_node_by_hostname(node_name)
|
||||
if node_obj is None:
|
||||
continue
|
||||
nics = node_obj.nics
|
||||
for nic_uuid, nic_obj in nics.items():
|
||||
network_interfaces = node_obj.network_interfaces
|
||||
for nic_uuid, nic_obj in network_interfaces.items():
|
||||
self.ip_address_list.append(nic_obj.ip_address)
|
||||
|
||||
# action_args are settings which are applied to the action space as a whole.
|
||||
@@ -964,7 +964,7 @@ class ActionManager:
|
||||
node_name = entry["node_name"]
|
||||
nic_num = entry["nic_num"]
|
||||
node_obj = game.simulation.network.get_node_by_hostname(node_name)
|
||||
ip_address = node_obj.ethernet_port[nic_num].ip_address
|
||||
ip_address = node_obj.network_interface[nic_num].ip_address
|
||||
ip_address_list.append(ip_address)
|
||||
|
||||
obj = cls(
|
||||
|
||||
@@ -406,7 +406,7 @@ class NodeObservation(AbstractObservation):
|
||||
where: Optional[Tuple[str]] = None,
|
||||
services: List[ServiceObservation] = [],
|
||||
folders: List[FolderObservation] = [],
|
||||
nics: List[NicObservation] = [],
|
||||
network_interfaces: List[NicObservation] = [],
|
||||
logon_status: bool = False,
|
||||
num_services_per_node: int = 2,
|
||||
num_folders_per_node: int = 2,
|
||||
@@ -429,9 +429,9 @@ class NodeObservation(AbstractObservation):
|
||||
:type folders: Dict[int,str], optional
|
||||
:param max_folders: Max number of folders in this node's obs space, defaults to 2
|
||||
:type max_folders: int, optional
|
||||
:param nics: Mapping between position in observation space and NIC idx, defaults to {}
|
||||
:type nics: Dict[int,str], optional
|
||||
:param max_nics: Max number of NICS in this node's obs space, defaults to 5
|
||||
:param network_interfaces: Mapping between position in observation space and NIC idx, defaults to {}
|
||||
:type network_interfaces: Dict[int,str], optional
|
||||
:param max_nics: Max number of network interfaces in this node's obs space, defaults to 5
|
||||
:type max_nics: int, optional
|
||||
"""
|
||||
super().__init__()
|
||||
@@ -456,11 +456,11 @@ class NodeObservation(AbstractObservation):
|
||||
msg = f"Too many folders in Node observation for node. Truncating service {truncated_folder.where[-1]}"
|
||||
_LOGGER.warning(msg)
|
||||
|
||||
self.nics: List[NicObservation] = nics
|
||||
while len(self.nics) < num_nics_per_node:
|
||||
self.nics.append(NicObservation())
|
||||
while len(self.nics) > num_nics_per_node:
|
||||
truncated_nic = self.nics.pop()
|
||||
self.network_interfaces: List[NicObservation] = network_interfaces
|
||||
while len(self.network_interfaces) < num_nics_per_node:
|
||||
self.network_interfaces.append(NicObservation())
|
||||
while len(self.network_interfaces) > num_nics_per_node:
|
||||
truncated_nic = self.network_interfaces.pop()
|
||||
msg = f"Too many NICs in Node observation for node. Truncating service {truncated_nic.where[-1]}"
|
||||
_LOGGER.warning(msg)
|
||||
|
||||
@@ -469,7 +469,7 @@ class NodeObservation(AbstractObservation):
|
||||
self.default_observation: Dict = {
|
||||
"SERVICES": {i + 1: s.default_observation for i, s in enumerate(self.services)},
|
||||
"FOLDERS": {i + 1: f.default_observation for i, f in enumerate(self.folders)},
|
||||
"NICS": {i + 1: n.default_observation for i, n in enumerate(self.nics)},
|
||||
"NETWORK_INTERFACES": {i + 1: n.default_observation for i, n in enumerate(self.network_interfaces)},
|
||||
"operating_status": 0,
|
||||
}
|
||||
if self.logon_status:
|
||||
@@ -494,7 +494,9 @@ class NodeObservation(AbstractObservation):
|
||||
obs["SERVICES"] = {i + 1: service.observe(state) for i, service in enumerate(self.services)}
|
||||
obs["FOLDERS"] = {i + 1: folder.observe(state) for i, folder in enumerate(self.folders)}
|
||||
obs["operating_status"] = node_state["operating_state"]
|
||||
obs["NICS"] = {i + 1: nic.observe(state) for i, nic in enumerate(self.nics)}
|
||||
obs["NETWORK_INTERFACES"] = {
|
||||
i + 1: network_interface.observe(state) for i, network_interface in enumerate(self.network_interfaces)
|
||||
}
|
||||
|
||||
if self.logon_status:
|
||||
obs["logon_status"] = 0
|
||||
@@ -508,7 +510,9 @@ class NodeObservation(AbstractObservation):
|
||||
"SERVICES": spaces.Dict({i + 1: service.space for i, service in enumerate(self.services)}),
|
||||
"FOLDERS": spaces.Dict({i + 1: folder.space for i, folder in enumerate(self.folders)}),
|
||||
"operating_status": spaces.Discrete(5),
|
||||
"NICS": spaces.Dict({i + 1: nic.space for i, nic in enumerate(self.nics)}),
|
||||
"NETWORK_INTERFACES": spaces.Dict(
|
||||
{i + 1: network_interface.space for i, network_interface in enumerate(self.network_interfaces)}
|
||||
),
|
||||
}
|
||||
if self.logon_status:
|
||||
space_shape["logon_status"] = spaces.Discrete(3)
|
||||
@@ -564,13 +568,13 @@ class NodeObservation(AbstractObservation):
|
||||
]
|
||||
# create some configs for the NIC observation in the format {"nic_num":1}, {"nic_num":2}, {"nic_num":3}, etc.
|
||||
nic_configs = [{"nic_num": i for i in range(num_nics_per_node)}]
|
||||
nics = [NicObservation.from_config(config=c, game=game, parent_where=where) for c in nic_configs]
|
||||
network_interfaces = [NicObservation.from_config(config=c, game=game, parent_where=where) for c in nic_configs]
|
||||
logon_status = config.get("logon_status", False)
|
||||
return cls(
|
||||
where=where,
|
||||
services=services,
|
||||
folders=folders,
|
||||
nics=nics,
|
||||
network_interfaces=network_interfaces,
|
||||
logon_status=logon_status,
|
||||
num_services_per_node=num_services_per_node,
|
||||
num_folders_per_node=num_folders_per_node,
|
||||
@@ -728,7 +732,7 @@ class AclObservation(AbstractObservation):
|
||||
node_ref = ip_map_config["node_hostname"]
|
||||
nic_num = ip_map_config["nic_num"]
|
||||
node_obj = game.simulation.network.nodes[game.ref_map_nodes[node_ref]]
|
||||
nic_obj = node_obj.ethernet_port[nic_num]
|
||||
nic_obj = node_obj.network_interface[nic_num]
|
||||
node_ip_to_idx[nic_obj.ip_address] = ip_idx + 2
|
||||
|
||||
router_hostname = config["router_hostname"]
|
||||
|
||||
@@ -11,11 +11,12 @@ from primaite.game.agent.interface import AbstractAgent, AgentSettings, ProxyAge
|
||||
from primaite.game.agent.observations import ObservationManager
|
||||
from primaite.game.agent.rewards import RewardFunction
|
||||
from primaite.session.io import SessionIO, SessionIOSettings
|
||||
from primaite.simulator.network.hardware.base import NIC, NodeOperatingState
|
||||
from primaite.simulator.network.hardware.nodes.computer import Computer
|
||||
from primaite.simulator.network.hardware.nodes.router import Router
|
||||
from primaite.simulator.network.hardware.nodes.server import Server
|
||||
from primaite.simulator.network.hardware.nodes.switch import Switch
|
||||
from primaite.simulator.network.hardware.base import NodeOperatingState
|
||||
from primaite.simulator.network.hardware.nodes.host.computer import Computer
|
||||
from primaite.simulator.network.hardware.nodes.host.host_node import NIC
|
||||
from primaite.simulator.network.hardware.nodes.host.server import Server
|
||||
from primaite.simulator.network.hardware.nodes.network.router import Router
|
||||
from primaite.simulator.network.hardware.nodes.network.switch import Switch
|
||||
from primaite.simulator.network.transmission.transport_layer import Port
|
||||
from primaite.simulator.sim_container import Simulation
|
||||
from primaite.simulator.system.applications.database_client import DatabaseClient
|
||||
@@ -330,8 +331,8 @@ class PrimaiteGame:
|
||||
dos_intensity=float(opt.get("dos_intensity", "1.0")),
|
||||
max_sessions=int(opt.get("max_sessions", "1000")),
|
||||
)
|
||||
if "nics" in node_cfg:
|
||||
for nic_num, nic_cfg in node_cfg["nics"].items():
|
||||
if "network_interfaces" in node_cfg:
|
||||
for nic_num, nic_cfg in node_cfg["network_interfaces"].items():
|
||||
new_node.connect_nic(NIC(ip_address=nic_cfg["ip_address"], subnet_mask=nic_cfg["subnet_mask"]))
|
||||
|
||||
net.add_node(new_node)
|
||||
@@ -343,13 +344,13 @@ class PrimaiteGame:
|
||||
node_a = net.nodes[game.ref_map_nodes[link_cfg["endpoint_a_ref"]]]
|
||||
node_b = net.nodes[game.ref_map_nodes[link_cfg["endpoint_b_ref"]]]
|
||||
if isinstance(node_a, Switch):
|
||||
endpoint_a = node_a.switch_ports[link_cfg["endpoint_a_port"]]
|
||||
endpoint_a = node_a.network_interface[link_cfg["endpoint_a_port"]]
|
||||
else:
|
||||
endpoint_a = node_a.ethernet_port[link_cfg["endpoint_a_port"]]
|
||||
endpoint_a = node_a.network_interface[link_cfg["endpoint_a_port"]]
|
||||
if isinstance(node_b, Switch):
|
||||
endpoint_b = node_b.switch_ports[link_cfg["endpoint_b_port"]]
|
||||
endpoint_b = node_b.network_interface[link_cfg["endpoint_b_port"]]
|
||||
else:
|
||||
endpoint_b = node_b.ethernet_port[link_cfg["endpoint_b_port"]]
|
||||
endpoint_b = node_b.network_interface[link_cfg["endpoint_b_port"]]
|
||||
new_link = net.connect(endpoint_a=endpoint_a, endpoint_b=endpoint_b)
|
||||
game.ref_map_links[link_cfg["ref"]] = new_link.uuid
|
||||
|
||||
|
||||
@@ -126,7 +126,7 @@
|
||||
" - FILES\n",
|
||||
" - <file_id 1-1>\n",
|
||||
" - health_status\n",
|
||||
" - NICS\n",
|
||||
" - NETWORK_INTERFACES\n",
|
||||
" - <nic_id 1-2>\n",
|
||||
" - nic_status\n",
|
||||
" - operating_status\n",
|
||||
@@ -180,7 +180,7 @@
|
||||
"\n",
|
||||
"The ACL rules in the observation space appear in the same order that they do in the actual ACL. Though, only the first 10 rules are shown, there are default rules lower down that cannot be changed by the agent. The extra rules just allow the network to function normally, by allowing pings, ARP traffic, etc.\n",
|
||||
"\n",
|
||||
"Most nodes have only 1 nic, so the observation for those is placed at NIC index 1 in the observation space. Only the security suite has 2 NICs, the second NIC in the observation space is the one that connects the security suite with swtich_2.\n",
|
||||
"Most nodes have only 1 network_interface, so the observation for those is placed at NIC index 1 in the observation space. Only the security suite has 2 NICs, the second NIC in the observation space is the one that connects the security suite with swtich_2.\n",
|
||||
"\n",
|
||||
"The meaning of the services' operating_state is:\n",
|
||||
"|operating_state|label|\n",
|
||||
@@ -328,7 +328,9 @@
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%load_ext autoreload\n",
|
||||
@@ -338,7 +340,9 @@
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Imports\n",
|
||||
@@ -361,7 +365,9 @@
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# create the env\n",
|
||||
@@ -389,7 +395,9 @@
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"for step in range(32):\n",
|
||||
@@ -407,7 +415,9 @@
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"pprint(obs['NODES'])"
|
||||
@@ -423,7 +433,9 @@
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"obs, reward, terminated, truncated, info = env.step(9) # scan database file\n",
|
||||
@@ -449,7 +461,9 @@
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"obs, reward, terminated, truncated, info = env.step(13) # patch the database\n",
|
||||
@@ -474,7 +488,9 @@
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"obs, reward, terminated, truncated, info = env.step(0) # patch the database\n",
|
||||
@@ -495,7 +511,9 @@
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"env.step(13) # Patch the database\n",
|
||||
@@ -535,7 +553,7 @@
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "venv",
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
@@ -549,9 +567,9 @@
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.10.12"
|
||||
"version": "3.8.10"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 2
|
||||
"nbformat_minor": 4
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ from abc import ABC, abstractmethod
|
||||
from typing import Callable, ClassVar, Dict, List, Optional, Union
|
||||
from uuid import uuid4
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from primaite import getLogger
|
||||
|
||||
@@ -150,14 +150,12 @@ class SimComponent(BaseModel):
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True, extra="allow")
|
||||
"""Configure pydantic to allow arbitrary types and to let the instance have attributes not present in model."""
|
||||
|
||||
uuid: str
|
||||
uuid: str = Field(default_factory=lambda: str(uuid4()))
|
||||
"""The component UUID."""
|
||||
|
||||
_original_state: Dict = {}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
if not kwargs.get("uuid"):
|
||||
kwargs["uuid"] = str(uuid4())
|
||||
super().__init__(**kwargs)
|
||||
self._request_manager: RequestManager = self._init_request_manager()
|
||||
self._parent: Optional["SimComponent"] = None
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
import networkx as nx
|
||||
@@ -7,11 +7,11 @@ from prettytable import MARKDOWN, PrettyTable
|
||||
|
||||
from primaite import getLogger
|
||||
from primaite.simulator.core import RequestManager, RequestType, SimComponent
|
||||
from primaite.simulator.network.hardware.base import Link, NIC, Node, SwitchPort
|
||||
from primaite.simulator.network.hardware.nodes.computer import Computer
|
||||
from primaite.simulator.network.hardware.nodes.router import Router
|
||||
from primaite.simulator.network.hardware.nodes.server import Server
|
||||
from primaite.simulator.network.hardware.nodes.switch import Switch
|
||||
from primaite.simulator.network.hardware.base import Link, Node, WiredNetworkInterface
|
||||
from primaite.simulator.network.hardware.nodes.host.computer import Computer
|
||||
from primaite.simulator.network.hardware.nodes.host.server import Server
|
||||
from primaite.simulator.network.hardware.nodes.network.router import Router
|
||||
from primaite.simulator.network.hardware.nodes.network.switch import Switch
|
||||
from primaite.simulator.system.applications.application import Application
|
||||
from primaite.simulator.system.services.service import Service
|
||||
|
||||
@@ -62,8 +62,8 @@ class Network(SimComponent):
|
||||
for node in self.nodes.values():
|
||||
node.power_on()
|
||||
|
||||
for nic in node.nics.values():
|
||||
nic.enable()
|
||||
for network_interface in node.network_interfaces.values():
|
||||
network_interface.enable()
|
||||
# Reset software
|
||||
for software in node.software_manager.software.values():
|
||||
if isinstance(software, Service):
|
||||
@@ -148,8 +148,9 @@ class Network(SimComponent):
|
||||
table.title = "IP Addresses"
|
||||
for nodes in nodes_type_map.values():
|
||||
for node in nodes:
|
||||
for i, port in node.ethernet_port.items():
|
||||
table.add_row([node.hostname, i, port.ip_address, port.subnet_mask, node.default_gateway])
|
||||
for i, port in node.network_interface.items():
|
||||
if hasattr(port, "ip_address"):
|
||||
table.add_row([node.hostname, i, port.ip_address, port.subnet_mask, node.default_gateway])
|
||||
print(table)
|
||||
|
||||
if links:
|
||||
@@ -209,8 +210,8 @@ class Network(SimComponent):
|
||||
node_b = link.endpoint_b._connected_node
|
||||
hostname_a = node_a.hostname if node_a else None
|
||||
hostname_b = node_b.hostname if node_b else None
|
||||
port_a = link.endpoint_a._port_num_on_node
|
||||
port_b = link.endpoint_b._port_num_on_node
|
||||
port_a = link.endpoint_a.port_num
|
||||
port_b = link.endpoint_b.port_num
|
||||
state["links"][uuid] = link.describe_state()
|
||||
state["links"][uuid]["hostname_a"] = hostname_a
|
||||
state["links"][uuid]["hostname_b"] = hostname_b
|
||||
@@ -271,18 +272,16 @@ class Network(SimComponent):
|
||||
self._node_request_manager.remove_request(name=node.hostname)
|
||||
_LOGGER.info(f"Removed node {node.hostname} from network {self.uuid}")
|
||||
|
||||
def connect(
|
||||
self, endpoint_a: Union[NIC, SwitchPort], endpoint_b: Union[NIC, SwitchPort], **kwargs
|
||||
) -> Optional[Link]:
|
||||
def connect(self, endpoint_a: WiredNetworkInterface, endpoint_b: WiredNetworkInterface, **kwargs) -> Optional[Link]:
|
||||
"""
|
||||
Connect two endpoints on the network by creating a link between their NICs/SwitchPorts.
|
||||
|
||||
.. note:: If the nodes owning the endpoints are not already in the network, they are automatically added.
|
||||
|
||||
:param endpoint_a: The first endpoint to connect.
|
||||
:type endpoint_a: Union[NIC, SwitchPort]
|
||||
:type endpoint_a: WiredNetworkInterface
|
||||
:param endpoint_b: The second endpoint to connect.
|
||||
:type endpoint_b: Union[NIC, SwitchPort]
|
||||
:type endpoint_b: WiredNetworkInterface
|
||||
:raises RuntimeError: If any validation or runtime checks fail.
|
||||
"""
|
||||
node_a: Node = endpoint_a.parent
|
||||
|
||||
@@ -2,9 +2,9 @@ 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.hardware.nodes.host.computer import Computer
|
||||
from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router
|
||||
from primaite.simulator.network.hardware.nodes.network.switch import Switch
|
||||
from primaite.simulator.network.transmission.network_layer import IPProtocol
|
||||
from primaite.simulator.network.transmission.transport_layer import Port
|
||||
|
||||
@@ -109,9 +109,9 @@ def create_office_lan(
|
||||
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])
|
||||
network.connect(core_switch.network_interface[core_switch_port], switch.network_interface[24])
|
||||
else:
|
||||
network.connect(router.ethernet_ports[1], switch.switch_ports[24])
|
||||
network.connect(router.network_interface[1], switch.network_interface[24])
|
||||
|
||||
# Add PCs to the LAN and connect them to switches
|
||||
for i in range(1, num_pcs + 1):
|
||||
@@ -125,9 +125,9 @@ def create_office_lan(
|
||||
# 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])
|
||||
network.connect(core_switch.network_interface[core_switch_port], switch.network_interface[24])
|
||||
else:
|
||||
network.connect(router.ethernet_ports[1], switch.switch_ports[24])
|
||||
network.connect(router.network_interface[1], switch.network_interface[24])
|
||||
|
||||
# Create and add a PC to the network
|
||||
pc = Computer(
|
||||
@@ -142,7 +142,7 @@ def create_office_lan(
|
||||
|
||||
# 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()
|
||||
network.connect(switch.network_interface[switch_port], pc.network_interface[1])
|
||||
switch.network_interface[switch_port].enable()
|
||||
|
||||
return network
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,86 @@
|
||||
from typing import Dict
|
||||
|
||||
from primaite.simulator.network.hardware.base import (
|
||||
IPWirelessNetworkInterface,
|
||||
Layer3Interface,
|
||||
WirelessNetworkInterface,
|
||||
)
|
||||
from primaite.simulator.network.transmission.data_link_layer import Frame
|
||||
|
||||
|
||||
class WirelessAccessPoint(IPWirelessNetworkInterface):
|
||||
"""
|
||||
Represents a Wireless Access Point (AP) in a network.
|
||||
|
||||
This class models a Wireless Access Point, a device that allows wireless devices to connect to a wired network
|
||||
using Wi-Fi or other wireless standards. The Wireless Access Point bridges the wireless and wired segments of
|
||||
the network, allowing wireless devices to communicate with other devices on the network.
|
||||
|
||||
As an integral component of wireless networking, a Wireless Access Point provides functionalities for network
|
||||
management, signal broadcasting, security enforcement, and connection handling. It also possesses Layer 3
|
||||
capabilities such as IP addressing and subnetting, allowing for network segmentation and routing.
|
||||
|
||||
Inherits from:
|
||||
- WirelessNetworkInterface: Provides basic properties and methods specific to wireless interfaces.
|
||||
- Layer3Interface: Provides Layer 3 properties like ip_address and subnet_mask, enabling the device to manage
|
||||
network traffic and routing.
|
||||
|
||||
This class can be further specialised or extended to support specific features or standards related to wireless
|
||||
networking, such as different Wi-Fi versions, frequency bands, or advanced security protocols.
|
||||
"""
|
||||
|
||||
def describe_state(self) -> Dict:
|
||||
"""
|
||||
Produce a dictionary describing the current state of this object.
|
||||
|
||||
:return: Current state of this object and child objects.
|
||||
:rtype: Dict
|
||||
"""
|
||||
# Get the state from the WirelessNetworkInterface
|
||||
state = WirelessNetworkInterface.describe_state(self)
|
||||
|
||||
# Update the state with information from Layer3Interface
|
||||
state.update(Layer3Interface.describe_state(self))
|
||||
|
||||
# Update the state with NIC-specific information
|
||||
state.update(
|
||||
{
|
||||
"wake_on_lan": self.wake_on_lan,
|
||||
}
|
||||
)
|
||||
|
||||
return state
|
||||
|
||||
def enable(self):
|
||||
"""Enable the interface."""
|
||||
pass
|
||||
|
||||
def disable(self):
|
||||
"""Disable the interface."""
|
||||
pass
|
||||
|
||||
def send_frame(self, frame: Frame) -> bool:
|
||||
"""
|
||||
Attempts to send a network frame through the interface.
|
||||
|
||||
:param frame: The network frame to be sent.
|
||||
:return: A boolean indicating whether the frame was successfully sent.
|
||||
"""
|
||||
pass
|
||||
|
||||
def receive_frame(self, frame: Frame) -> bool:
|
||||
"""
|
||||
Receives a network frame on the interface.
|
||||
|
||||
:param frame: The network frame being received.
|
||||
:return: A boolean indicating whether the frame was successfully received.
|
||||
"""
|
||||
pass
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""
|
||||
String representation of the NIC.
|
||||
|
||||
:return: A string combining the port number, MAC address and IP address of the NIC.
|
||||
"""
|
||||
return f"Port {self.port_num}: {self.mac_address}/{self.ip_address}"
|
||||
@@ -0,0 +1,83 @@
|
||||
from typing import Dict
|
||||
|
||||
from primaite.simulator.network.hardware.base import (
|
||||
IPWirelessNetworkInterface,
|
||||
Layer3Interface,
|
||||
WirelessNetworkInterface,
|
||||
)
|
||||
from primaite.simulator.network.transmission.data_link_layer import Frame
|
||||
|
||||
|
||||
class WirelessNIC(IPWirelessNetworkInterface):
|
||||
"""
|
||||
Represents a Wireless Network Interface Card (Wireless NIC) in a network device.
|
||||
|
||||
This class encapsulates the functionalities and attributes of a wireless NIC, combining the characteristics of a
|
||||
wireless network interface with Layer 3 features. It is capable of connecting to wireless networks, managing
|
||||
wireless-specific properties such as signal strength and security protocols, and also handling IP-related
|
||||
functionalities like IP addressing and subnetting.
|
||||
|
||||
Inherits from:
|
||||
- WirelessNetworkInterface: Provides basic properties and methods specific to wireless interfaces.
|
||||
- Layer3Interface: Provides Layer 3 properties like ip_address and subnet_mask, enabling the device to participate
|
||||
in IP-based networking.
|
||||
|
||||
This class can be extended to include more advanced features or to tailor its behavior for specific types of
|
||||
wireless networks or protocols.
|
||||
"""
|
||||
|
||||
def describe_state(self) -> Dict:
|
||||
"""
|
||||
Produce a dictionary describing the current state of this object.
|
||||
|
||||
:return: Current state of this object and child objects.
|
||||
:rtype: Dict
|
||||
"""
|
||||
# Get the state from the WirelessNetworkInterface
|
||||
state = WirelessNetworkInterface.describe_state(self)
|
||||
|
||||
# Update the state with information from Layer3Interface
|
||||
state.update(Layer3Interface.describe_state(self))
|
||||
|
||||
# Update the state with NIC-specific information
|
||||
state.update(
|
||||
{
|
||||
"wake_on_lan": self.wake_on_lan,
|
||||
}
|
||||
)
|
||||
|
||||
return state
|
||||
|
||||
def enable(self):
|
||||
"""Enable the interface."""
|
||||
pass
|
||||
|
||||
def disable(self):
|
||||
"""Disable the interface."""
|
||||
pass
|
||||
|
||||
def send_frame(self, frame: Frame) -> bool:
|
||||
"""
|
||||
Attempts to send a network frame through the interface.
|
||||
|
||||
:param frame: The network frame to be sent.
|
||||
:return: A boolean indicating whether the frame was successfully sent.
|
||||
"""
|
||||
pass
|
||||
|
||||
def receive_frame(self, frame: Frame) -> bool:
|
||||
"""
|
||||
Receives a network frame on the interface.
|
||||
|
||||
:param frame: The network frame being received.
|
||||
:return: A boolean indicating whether the frame was successfully received.
|
||||
"""
|
||||
pass
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""
|
||||
String representation of the NIC.
|
||||
|
||||
:return: A string combining the port number, MAC address and IP address of the NIC.
|
||||
"""
|
||||
return f"Port {self.port_num}: {self.mac_address}/{self.ip_address}"
|
||||
@@ -1,59 +0,0 @@
|
||||
from primaite.simulator.network.hardware.base import NIC, Node
|
||||
from primaite.simulator.system.applications.web_browser import WebBrowser
|
||||
from primaite.simulator.system.services.dns.dns_client import DNSClient
|
||||
from primaite.simulator.system.services.ftp.ftp_client import FTPClient
|
||||
from primaite.simulator.system.services.ntp.ntp_client import NTPClient
|
||||
|
||||
|
||||
class Computer(Node):
|
||||
"""
|
||||
A basic Computer class.
|
||||
|
||||
Example:
|
||||
>>> pc_a = Computer(
|
||||
hostname="pc_a",
|
||||
ip_address="192.168.1.10",
|
||||
subnet_mask="255.255.255.0",
|
||||
default_gateway="192.168.1.1"
|
||||
)
|
||||
>>> pc_a.power_on()
|
||||
|
||||
Instances of computer come 'pre-packaged' with the following:
|
||||
|
||||
* Core Functionality:
|
||||
* ARP
|
||||
* ICMP
|
||||
* Packet Capture
|
||||
* Sys Log
|
||||
* Services:
|
||||
* DNS Client
|
||||
* FTP Client
|
||||
* LDAP Client
|
||||
* NTP Client
|
||||
* Applications:
|
||||
* Email Client
|
||||
* Web Browser
|
||||
* Processes:
|
||||
* Placeholder
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.connect_nic(NIC(ip_address=kwargs["ip_address"], subnet_mask=kwargs["subnet_mask"]))
|
||||
self._install_system_software()
|
||||
|
||||
def _install_system_software(self):
|
||||
"""Install System Software - software that is usually provided with the OS."""
|
||||
# DNS Client
|
||||
self.software_manager.install(DNSClient)
|
||||
|
||||
# FTP
|
||||
self.software_manager.install(FTPClient)
|
||||
|
||||
# NTP
|
||||
self.software_manager.install(NTPClient)
|
||||
|
||||
# Web Browser
|
||||
self.software_manager.install(WebBrowser)
|
||||
|
||||
super()._install_system_software()
|
||||
@@ -0,0 +1,32 @@
|
||||
from primaite.simulator.network.hardware.nodes.host.host_node import HostNode
|
||||
|
||||
|
||||
class Computer(HostNode):
|
||||
"""
|
||||
A basic Computer class.
|
||||
|
||||
Example:
|
||||
>>> pc_a = Computer(
|
||||
hostname="pc_a",
|
||||
ip_address="192.168.1.10",
|
||||
subnet_mask="255.255.255.0",
|
||||
default_gateway="192.168.1.1"
|
||||
)
|
||||
>>> pc_a.power_on()
|
||||
|
||||
Instances of computer come 'pre-packaged' with the following:
|
||||
|
||||
* Core Functionality:
|
||||
* Packet Capture
|
||||
* Sys Log
|
||||
* Services:
|
||||
* ARP Service
|
||||
* ICMP Service
|
||||
* DNS Client
|
||||
* FTP Client
|
||||
* NTP Client
|
||||
* Applications:
|
||||
* Web Browser
|
||||
"""
|
||||
|
||||
pass
|
||||
385
src/primaite/simulator/network/hardware/nodes/host/host_node.py
Normal file
385
src/primaite/simulator/network/hardware/nodes/host/host_node.py
Normal file
@@ -0,0 +1,385 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from ipaddress import IPv4Address
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from primaite import getLogger
|
||||
from primaite.simulator.network.hardware.base import IPWiredNetworkInterface, Link, Node
|
||||
from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState
|
||||
from primaite.simulator.network.transmission.data_link_layer import Frame
|
||||
from primaite.simulator.system.applications.web_browser import WebBrowser
|
||||
from primaite.simulator.system.services.arp.arp import ARP, ARPPacket
|
||||
from primaite.simulator.system.services.dns.dns_client import DNSClient
|
||||
from primaite.simulator.system.services.ftp.ftp_client import FTPClient
|
||||
from primaite.simulator.system.services.icmp.icmp import ICMP
|
||||
from primaite.simulator.system.services.ntp.ntp_client import NTPClient
|
||||
from primaite.utils.validators import IPV4Address
|
||||
|
||||
_LOGGER = getLogger(__name__)
|
||||
|
||||
|
||||
class HostARP(ARP):
|
||||
"""
|
||||
The Host ARP Service.
|
||||
|
||||
Extends the ARP service for host-specific functionalities within a network, focusing on resolving and caching
|
||||
MAC addresses and network interfaces (NICs) based on IP addresses, especially concerning the default gateway.
|
||||
|
||||
This specialized ARP service for hosts facilitates efficient network communication by managing ARP entries
|
||||
and handling ARP requests and replies with additional logic for default gateway processing.
|
||||
"""
|
||||
|
||||
def get_default_gateway_mac_address(self) -> Optional[str]:
|
||||
"""
|
||||
Retrieves the MAC address of the default gateway as known from the ARP cache.
|
||||
|
||||
:return: The MAC address of the default gateway if present in the ARP cache; otherwise, None.
|
||||
"""
|
||||
if self.software_manager.node.default_gateway:
|
||||
return self.get_arp_cache_mac_address(self.software_manager.node.default_gateway)
|
||||
|
||||
def get_default_gateway_network_interface(self) -> Optional[NIC]:
|
||||
"""
|
||||
Obtains the network interface card (NIC) associated with the default gateway from the ARP cache.
|
||||
|
||||
:return: The NIC associated with the default gateway if it exists in the ARP cache; otherwise, None.
|
||||
"""
|
||||
if self.software_manager.node.default_gateway and self.software_manager.node.has_enabled_network_interface:
|
||||
return self.get_arp_cache_network_interface(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]:
|
||||
"""
|
||||
Internal method to retrieve the MAC address associated with an IP address from the ARP cache.
|
||||
|
||||
:param ip_address: The IP address whose MAC address is to be retrieved.
|
||||
:param is_reattempt: Indicates if this call is a reattempt after a failed initial attempt.
|
||||
:param is_default_gateway_attempt: Indicates if this call is an attempt to get the default gateway's MAC
|
||||
address.
|
||||
:return: The MAC address associated with the IP address if found, otherwise None.
|
||||
"""
|
||||
arp_entry = self.arp.get(ip_address)
|
||||
|
||||
if arp_entry:
|
||||
return arp_entry.mac_address
|
||||
|
||||
if ip_address == self.software_manager.node.default_gateway:
|
||||
is_reattempt = True
|
||||
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]:
|
||||
"""
|
||||
Retrieves the MAC address associated with a given IP address from the ARP cache.
|
||||
|
||||
:param ip_address: The IP address for which the MAC address is sought.
|
||||
:return: The MAC address if available in the ARP cache; otherwise, None.
|
||||
"""
|
||||
return self._get_arp_cache_mac_address(ip_address)
|
||||
|
||||
def _get_arp_cache_network_interface(
|
||||
self, ip_address: IPV4Address, is_reattempt: bool = False, is_default_gateway_attempt: bool = False
|
||||
) -> Optional[NIC]:
|
||||
"""
|
||||
Internal method to retrieve the NIC associated with an IP address from the ARP cache.
|
||||
|
||||
:param ip_address: The IP address whose NIC is to be retrieved.
|
||||
:param is_reattempt: Indicates if this call is a reattempt after a failed initial attempt.
|
||||
:param is_default_gateway_attempt: Indicates if this call is an attempt to get the NIC of the default gateway.
|
||||
:return: The NIC associated with the IP address if found, otherwise None.
|
||||
"""
|
||||
arp_entry = self.arp.get(ip_address)
|
||||
|
||||
if arp_entry:
|
||||
return self.software_manager.node.network_interfaces[arp_entry.network_interface_uuid]
|
||||
else:
|
||||
if ip_address == self.software_manager.node.default_gateway:
|
||||
is_reattempt = True
|
||||
if not is_reattempt:
|
||||
self.send_arp_request(ip_address)
|
||||
return self._get_arp_cache_network_interface(
|
||||
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_network_interface(
|
||||
ip_address=self.software_manager.node.default_gateway,
|
||||
is_reattempt=True,
|
||||
is_default_gateway_attempt=True,
|
||||
)
|
||||
return None
|
||||
|
||||
def get_arp_cache_network_interface(self, ip_address: IPv4Address) -> Optional[NIC]:
|
||||
"""
|
||||
Retrieves the network interface card (NIC) associated with a given IP address from the ARP cache.
|
||||
|
||||
:param ip_address: The IP address for which the associated NIC is sought.
|
||||
:return: The NIC if available in the ARP cache; otherwise, None.
|
||||
"""
|
||||
return self._get_arp_cache_network_interface(ip_address)
|
||||
|
||||
def _process_arp_request(self, arp_packet: ARPPacket, from_network_interface: NIC):
|
||||
"""
|
||||
Processes an ARP request.
|
||||
|
||||
Adds a new entry to the ARP cache if the target IP address matches the NIC's IP address and sends an ARP
|
||||
reply back.
|
||||
|
||||
:param arp_packet: The ARP packet containing the request.
|
||||
:param from_network_interface: The NIC that received the ARP request.
|
||||
"""
|
||||
super()._process_arp_request(arp_packet, from_network_interface)
|
||||
# Unmatched ARP Request
|
||||
if arp_packet.target_ip_address != from_network_interface.ip_address:
|
||||
self.sys_log.info(
|
||||
f"Ignoring ARP request for {arp_packet.target_ip_address}. Current IP address is "
|
||||
f"{from_network_interface.ip_address}"
|
||||
)
|
||||
return
|
||||
|
||||
arp_packet = arp_packet.generate_reply(from_network_interface.mac_address)
|
||||
self.send_arp_reply(arp_packet)
|
||||
|
||||
|
||||
class NIC(IPWiredNetworkInterface):
|
||||
"""
|
||||
Represents a Network Interface Card (NIC) in a Host Node.
|
||||
|
||||
A NIC is a hardware component that provides a computer or other network device with the ability to connect to a
|
||||
network. It operates at both Layer 2 (Data Link Layer) and Layer 3 (Network Layer) of the OSI model, meaning it
|
||||
can interpret both MAC addresses and IP addresses. This class combines the functionalities of
|
||||
WiredNetworkInterface and Layer3Interface, allowing the NIC to manage physical connections and network layer
|
||||
addressing.
|
||||
|
||||
Inherits from:
|
||||
- WiredNetworkInterface: Provides properties and methods specific to wired connections, including methods to connect
|
||||
and disconnect from network links and to manage the enabled/disabled state of the interface.
|
||||
- Layer3Interface: Provides properties for Layer 3 network configuration, such as IP address and subnet mask.
|
||||
"""
|
||||
|
||||
_connected_link: Optional[Link] = None
|
||||
"The network link to which the network interface is connected."
|
||||
wake_on_lan: bool = False
|
||||
"Indicates if the NIC supports Wake-on-LAN functionality."
|
||||
|
||||
def model_post_init(self, __context: Any) -> None:
|
||||
"""
|
||||
Performs post-initialisation checks to ensure the model's IP configuration is valid.
|
||||
|
||||
This method is invoked after the initialisation of a network model object to validate its network settings,
|
||||
particularly to ensure that the assigned IP address is not a network address. This validation is crucial for
|
||||
maintaining the integrity of network simulations and avoiding configuration errors that could lead to
|
||||
unrealistic or incorrect behavior.
|
||||
|
||||
:param __context: Contextual information or parameters passed to the method, used for further initializing or
|
||||
validating the model post-creation.
|
||||
:raises ValueError: If the IP address is the same as the network address, indicating an incorrect configuration.
|
||||
"""
|
||||
if self.ip_network.network_address == self.ip_address:
|
||||
raise ValueError(f"{self.ip_address}/{self.subnet_mask} must not be a network address")
|
||||
|
||||
def describe_state(self) -> Dict:
|
||||
"""
|
||||
Produce a dictionary describing the current state of this object.
|
||||
|
||||
:return: Current state of this object and child objects.
|
||||
:rtype: Dict
|
||||
"""
|
||||
# Get the state from the IPWiredNetworkInterface
|
||||
state = super().describe_state()
|
||||
|
||||
# Update the state with NIC-specific information
|
||||
state.update(
|
||||
{
|
||||
"wake_on_lan": self.wake_on_lan,
|
||||
}
|
||||
)
|
||||
|
||||
return state
|
||||
|
||||
def set_original_state(self):
|
||||
"""Sets the original state."""
|
||||
vals_to_include = {"ip_address", "subnet_mask", "mac_address", "speed", "mtu", "wake_on_lan", "enabled"}
|
||||
self._original_state = self.model_dump(include=vals_to_include)
|
||||
|
||||
def receive_frame(self, frame: Frame) -> bool:
|
||||
"""
|
||||
Attempt to receive and process a network frame from the connected Link.
|
||||
|
||||
This method processes a frame if the NIC is enabled. It checks the frame's destination and TTL, captures the
|
||||
frame using PCAP, and forwards it to the connected Node if valid. Returns True if the frame is processed,
|
||||
False otherwise (e.g., if the NIC is disabled, or TTL expired).
|
||||
|
||||
:param frame: The network frame being received.
|
||||
:return: True if the frame is processed and passed to the node, False otherwise.
|
||||
"""
|
||||
if self.enabled:
|
||||
frame.decrement_ttl()
|
||||
if frame.ip and frame.ip.ttl < 1:
|
||||
self._connected_node.sys_log.info(f"Frame discarded at {self} as TTL limit reached")
|
||||
return False
|
||||
frame.set_received_timestamp()
|
||||
self.pcap.capture_inbound(frame)
|
||||
# If this destination or is broadcast
|
||||
accept_frame = False
|
||||
|
||||
# Check if it's a broadcast:
|
||||
if frame.ethernet.dst_mac_addr == "ff:ff:ff:ff:ff:ff":
|
||||
if frame.ip.dst_ip_address in {self.ip_address, self.ip_network.broadcast_address}:
|
||||
accept_frame = True
|
||||
else:
|
||||
if frame.ethernet.dst_mac_addr == self.mac_address:
|
||||
accept_frame = True
|
||||
|
||||
if accept_frame:
|
||||
self._connected_node.receive_frame(frame=frame, from_network_interface=self)
|
||||
return True
|
||||
return False
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""
|
||||
String representation of the NIC.
|
||||
|
||||
:return: A string combining the port number, MAC address and IP address of the NIC.
|
||||
"""
|
||||
return f"Port {self.port_num}: {self.mac_address}/{self.ip_address}"
|
||||
|
||||
|
||||
class HostNode(Node):
|
||||
"""
|
||||
Represents a host node in the network.
|
||||
|
||||
An end-user device within the network, such as a computer or server, equipped with the capability to initiate and
|
||||
respond to network communications.
|
||||
|
||||
A `HostNode` extends the base `Node` class by incorporating host-specific services and applications, thereby
|
||||
simulating the functionalities typically expected from a networked end-user device.
|
||||
|
||||
**Example**::
|
||||
|
||||
>>> pc_a = HostNode(
|
||||
... hostname="pc_a",
|
||||
... ip_address="192.168.1.10",
|
||||
... subnet_mask="255.255.255.0",
|
||||
... default_gateway="192.168.1.1"
|
||||
... )
|
||||
>>> pc_a.power_on()
|
||||
|
||||
The host node comes pre-equipped with a range of core functionalities, services, and applications necessary
|
||||
for engaging in various network operations and tasks.
|
||||
|
||||
Core Functionality:
|
||||
-------------------
|
||||
|
||||
* Packet Capture: Monitors and logs network traffic.
|
||||
* Sys Log: Logs system events and errors.
|
||||
|
||||
Services:
|
||||
---------
|
||||
|
||||
* ARP (Address Resolution Protocol) Service: Resolves IP addresses to MAC addresses.
|
||||
* ICMP (Internet Control Message Protocol) Service: Handles ICMP operations, such as ping requests.
|
||||
* DNS (Domain Name System) Client: Resolves domain names to IP addresses.
|
||||
* FTP (File Transfer Protocol) Client: Enables file transfers between the host and FTP servers.
|
||||
* NTP (Network Time Protocol) Client: Synchronizes the system clock with NTP servers.
|
||||
|
||||
Applications:
|
||||
------------
|
||||
|
||||
* Web Browser: Provides web browsing capabilities.
|
||||
"""
|
||||
|
||||
network_interfaces: Dict[str, NIC] = {}
|
||||
"The Network Interfaces on the node."
|
||||
network_interface: Dict[int, NIC] = {}
|
||||
"The NICs on the node by port id."
|
||||
|
||||
def __init__(self, ip_address: IPV4Address, subnet_mask: IPV4Address, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.connect_nic(NIC(ip_address=ip_address, subnet_mask=subnet_mask))
|
||||
|
||||
def _install_system_software(self):
|
||||
"""
|
||||
Installs the system software and network services typically found on an operating system.
|
||||
|
||||
This method equips the host with essential network services and applications, preparing it for various
|
||||
network-related tasks and operations.
|
||||
"""
|
||||
# ARP Service
|
||||
self.software_manager.install(HostARP)
|
||||
|
||||
# ICMP Service
|
||||
self.software_manager.install(ICMP)
|
||||
|
||||
# DNS Client
|
||||
self.software_manager.install(DNSClient)
|
||||
|
||||
# FTP Client
|
||||
self.software_manager.install(FTPClient)
|
||||
|
||||
# NTP Client
|
||||
self.software_manager.install(NTPClient)
|
||||
|
||||
# Web Browser
|
||||
self.software_manager.install(WebBrowser)
|
||||
|
||||
super()._install_system_software()
|
||||
|
||||
def default_gateway_hello(self):
|
||||
"""
|
||||
Sends a hello message to the default gateway to establish connectivity and resolve the gateway's MAC address.
|
||||
|
||||
This method is invoked to ensure the host node can communicate with its default gateway, primarily to confirm
|
||||
network connectivity and populate the ARP cache with the gateway's MAC address.
|
||||
"""
|
||||
if self.operating_state == NodeOperatingState.ON and self.default_gateway:
|
||||
self.software_manager.arp.get_default_gateway_mac_address()
|
||||
|
||||
def receive_frame(self, frame: Frame, from_network_interface: NIC):
|
||||
"""
|
||||
Receive a Frame from the connected NIC and process it.
|
||||
|
||||
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_network_interface: The NIC that received the frame.
|
||||
"""
|
||||
super().receive_frame(frame, from_network_interface)
|
||||
|
||||
# Check if the destination port is open on the Node
|
||||
dst_port = None
|
||||
if frame.tcp:
|
||||
dst_port = frame.tcp.dst_port
|
||||
elif frame.udp:
|
||||
dst_port = frame.udp.dst_port
|
||||
|
||||
accept_frame = False
|
||||
if frame.icmp or dst_port in self.software_manager.get_open_ports():
|
||||
# accept the frame as the port is open or if it's an ICMP frame
|
||||
accept_frame = True
|
||||
|
||||
# TODO: add internal node firewall check here?
|
||||
|
||||
if accept_frame:
|
||||
self.session_manager.receive_frame(frame, from_network_interface)
|
||||
else:
|
||||
self.sys_log.info(f"Ignoring frame from {frame.ip.src_ip_address}")
|
||||
# TODO: do we need to do anything more here?
|
||||
pass
|
||||
@@ -1,7 +1,7 @@
|
||||
from primaite.simulator.network.hardware.nodes.computer import Computer
|
||||
from primaite.simulator.network.hardware.nodes.host.host_node import HostNode
|
||||
|
||||
|
||||
class Server(Computer):
|
||||
class Server(HostNode):
|
||||
"""
|
||||
A basic Server class.
|
||||
|
||||
@@ -17,18 +17,14 @@ class Server(Computer):
|
||||
Instances of Server come 'pre-packaged' with the following:
|
||||
|
||||
* Core Functionality:
|
||||
* ARP
|
||||
* ICMP
|
||||
* Packet Capture
|
||||
* Sys Log
|
||||
* Services:
|
||||
* ARP Service
|
||||
* ICMP Service
|
||||
* DNS Client
|
||||
* FTP Client
|
||||
* LDAP Client
|
||||
* NTP Client
|
||||
* Applications:
|
||||
* Email Client
|
||||
* Web Browser
|
||||
* Processes:
|
||||
* Placeholder
|
||||
"""
|
||||
@@ -0,0 +1,30 @@
|
||||
from abc import abstractmethod
|
||||
|
||||
from primaite.simulator.network.hardware.base import NetworkInterface, Node
|
||||
from primaite.simulator.network.transmission.data_link_layer import Frame
|
||||
|
||||
|
||||
class NetworkNode(Node):
|
||||
"""
|
||||
Represents an abstract base class for a network node that can receive and process network frames.
|
||||
|
||||
This class provides a common interface for network nodes such as routers and switches, defining the essential
|
||||
behavior that allows these devices to handle incoming network traffic. Implementations of this class must
|
||||
provide functionality for receiving and processing frames received on their network interfaces.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def receive_frame(self, frame: Frame, from_network_interface: NetworkInterface):
|
||||
"""
|
||||
Abstract method that must be implemented by subclasses to define how to receive and process frames.
|
||||
|
||||
This method is called when a frame is received by a network interface belonging to this node. Subclasses
|
||||
should implement the logic to process the frame, including examining its contents, making forwarding decisions,
|
||||
or performing any necessary actions based on the frame's protocol and destination.
|
||||
|
||||
:param frame: The network frame that has been received.
|
||||
:type frame: Frame
|
||||
:param from_network_interface: The network interface on which the frame was received.
|
||||
:type from_network_interface: NetworkInterface
|
||||
"""
|
||||
pass
|
||||
@@ -3,17 +3,22 @@ from __future__ import annotations
|
||||
import secrets
|
||||
from enum import Enum
|
||||
from ipaddress import IPv4Address, IPv4Network
|
||||
from typing import Dict, List, Optional, Tuple, Union
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
|
||||
from prettytable import MARKDOWN, PrettyTable
|
||||
|
||||
from primaite.simulator.core import RequestManager, RequestType, SimComponent
|
||||
from primaite.simulator.network.hardware.base import ARPCache, ICMP, NIC, Node
|
||||
from primaite.simulator.network.hardware.base import IPWiredNetworkInterface
|
||||
from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState
|
||||
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.network.hardware.nodes.network.network_node import NetworkNode
|
||||
from primaite.simulator.network.protocols.arp import ARPPacket
|
||||
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
|
||||
from primaite.simulator.network.transmission.transport_layer import Port
|
||||
from primaite.simulator.system.core.sys_log import SysLog
|
||||
from primaite.simulator.system.services.arp.arp import ARP
|
||||
from primaite.simulator.system.services.icmp.icmp import ICMP
|
||||
|
||||
|
||||
class ACLAction(Enum):
|
||||
@@ -196,6 +201,15 @@ class AccessControlList(SimComponent):
|
||||
"""
|
||||
return self._acl
|
||||
|
||||
@property
|
||||
def num_rules(self) -> int:
|
||||
"""
|
||||
Get the number of rules in the ACL.
|
||||
|
||||
:return: The number of rules in the ACL.
|
||||
"""
|
||||
return len([rule for rule in self._acl if rule is not None])
|
||||
|
||||
def add_rule(
|
||||
self,
|
||||
action: ACLAction,
|
||||
@@ -538,222 +552,282 @@ class RouteTable(SimComponent):
|
||||
print(table)
|
||||
|
||||
|
||||
class RouterARPCache(ARPCache):
|
||||
class RouterARP(ARP):
|
||||
"""
|
||||
Inherits from ARPCache and adds router-specific ARP packet processing.
|
||||
Extends ARP functionality with router-specific ARP packet processing capabilities.
|
||||
|
||||
:ivar SysLog sys_log: A system log for logging messages.
|
||||
:ivar Router router: The router to which this ARP cache belongs.
|
||||
This class is designed to manage ARP requests and replies within a router, handling both the resolution of MAC
|
||||
addresses for IP addresses within the router's networks and the forwarding of ARP requests to other networks
|
||||
based on routing information.
|
||||
"""
|
||||
|
||||
def __init__(self, sys_log: SysLog, router: Router):
|
||||
super().__init__(sys_log)
|
||||
self.router: Router = router
|
||||
router: Optional[Router] = None
|
||||
|
||||
def process_arp_packet(
|
||||
self, from_nic: NIC, frame: Frame, route_table: RouteTable, is_reattempt: bool = False
|
||||
) -> None:
|
||||
def _get_arp_cache_mac_address(
|
||||
self, ip_address: IPv4Address, is_reattempt: bool = False, is_default_route_attempt: bool = False
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Processes a received ARP (Address Resolution Protocol) packet in a router-specific way.
|
||||
Attempts to retrieve the MAC address associated with the given IP address from the ARP cache.
|
||||
|
||||
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).
|
||||
If the address is not in the cache, an ARP request may be sent, and the method may reattempt the lookup.
|
||||
|
||||
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.
|
||||
:param ip_address: The IP address for which to find the corresponding MAC address.
|
||||
:type ip_address: IPv4Address
|
||||
:param is_reattempt: Indicates whether this call is a reattempt after a failed initial attempt to find the MAC
|
||||
address.
|
||||
:type is_reattempt: bool
|
||||
:param is_default_route_attempt: Indicates whether the attempt is being made to resolve the MAC address for the
|
||||
default route.
|
||||
:type is_default_route_attempt: bool
|
||||
:return: The MAC address associated with the given IP address, if found; otherwise, None.
|
||||
:rtype: Optional[str]
|
||||
"""
|
||||
arp_packet = frame.arp
|
||||
arp_entry = self.arp.get(ip_address)
|
||||
|
||||
# ARP Reply
|
||||
if not arp_packet.request:
|
||||
for nic in self.router.nics.values():
|
||||
if arp_packet.target_ip_address == 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
|
||||
if arp_entry:
|
||||
return arp_entry.mac_address
|
||||
|
||||
# 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}"
|
||||
if not is_reattempt:
|
||||
if self.router.ip_is_in_router_interface_subnet(ip_address):
|
||||
self.send_arp_request(ip_address)
|
||||
return self._get_arp_cache_mac_address(
|
||||
ip_address=ip_address, is_reattempt=True, is_default_route_attempt=is_default_route_attempt
|
||||
)
|
||||
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
|
||||
self.add_arp_cache_entry(
|
||||
ip_address=arp_packet.sender_ip_address, mac_address=arp_packet.sender_mac_addr, nic=from_nic
|
||||
)
|
||||
route = self.router.route_table.find_best_route(ip_address)
|
||||
if route and route != self.router.route_table.default_route:
|
||||
self.send_arp_request(route.next_hop_ip_address)
|
||||
return self._get_arp_cache_mac_address(
|
||||
ip_address=route.next_hop_ip_address,
|
||||
is_reattempt=True,
|
||||
is_default_route_attempt=is_default_route_attempt,
|
||||
)
|
||||
else:
|
||||
if self.router.route_table.default_route:
|
||||
if not is_default_route_attempt:
|
||||
self.send_arp_request(self.router.route_table.default_route.next_hop_ip_address)
|
||||
return self._get_arp_cache_mac_address(
|
||||
ip_address=self.router.route_table.default_route.next_hop_ip_address,
|
||||
is_reattempt=True,
|
||||
is_default_route_attempt=True,
|
||||
)
|
||||
return None
|
||||
|
||||
def get_arp_cache_mac_address(self, ip_address: IPv4Address) -> Optional[str]:
|
||||
"""
|
||||
Public interface to retrieve the MAC address associated with the given IP address from the ARP cache.
|
||||
|
||||
:param ip_address: The IP address for which to find the corresponding MAC address.
|
||||
:type ip_address: IPv4Address
|
||||
:return: The MAC address associated with the given IP address, if found; otherwise, None.
|
||||
:rtype: Optional[str]
|
||||
"""
|
||||
return self._get_arp_cache_mac_address(ip_address)
|
||||
|
||||
def _get_arp_cache_network_interface(
|
||||
self, ip_address: IPv4Address, is_reattempt: bool = False, is_default_route_attempt: bool = False
|
||||
) -> Optional[RouterInterface]:
|
||||
"""
|
||||
Attempts to retrieve the router interface associated with the given IP address.
|
||||
|
||||
If the address is not directly associated with a router interface, it may send an ARP request based on
|
||||
routing information.
|
||||
|
||||
:param ip_address: The IP address for which to find the corresponding router interface.
|
||||
:type ip_address: IPv4Address
|
||||
:param is_reattempt: Indicates whether this call is a reattempt after a failed initial attempt.
|
||||
:type is_reattempt: bool
|
||||
:param is_default_route_attempt: Indicates whether the attempt is being made for the default route's next-hop
|
||||
IP address.
|
||||
:type is_default_route_attempt: bool
|
||||
:return: The router interface associated with the given IP address, if applicable; otherwise, None.
|
||||
:rtype: Optional[RouterInterface]
|
||||
"""
|
||||
arp_entry = self.arp.get(ip_address)
|
||||
if arp_entry:
|
||||
return self.software_manager.node.network_interfaces[arp_entry.network_interface_uuid]
|
||||
|
||||
for network_interface in self.router.network_interfaces.values():
|
||||
if ip_address in network_interface.ip_network:
|
||||
return network_interface
|
||||
|
||||
if not is_reattempt:
|
||||
route = self.router.route_table.find_best_route(ip_address)
|
||||
if route and route != self.router.route_table.default_route:
|
||||
self.send_arp_request(route.next_hop_ip_address)
|
||||
return self._get_arp_cache_network_interface(
|
||||
ip_address=route.next_hop_ip_address,
|
||||
is_reattempt=True,
|
||||
is_default_route_attempt=is_default_route_attempt,
|
||||
)
|
||||
else:
|
||||
if self.router.route_table.default_route:
|
||||
if not is_default_route_attempt:
|
||||
self.send_arp_request(self.router.route_table.default_route.next_hop_ip_address)
|
||||
return self._get_arp_cache_network_interface(
|
||||
ip_address=self.router.route_table.default_route.next_hop_ip_address,
|
||||
is_reattempt=True,
|
||||
is_default_route_attempt=True,
|
||||
)
|
||||
return None
|
||||
|
||||
def get_arp_cache_network_interface(self, ip_address: IPv4Address) -> Optional[RouterInterface]:
|
||||
"""
|
||||
Public interface to retrieve the router interface associated with the given IP address.
|
||||
|
||||
:param ip_address: The IP address for which to find the corresponding router interface.
|
||||
:type ip_address: IPv4Address
|
||||
:return: The router interface associated with the given IP address, if found; otherwise, None.
|
||||
:rtype: Optional[RouterInterface]
|
||||
"""
|
||||
return self._get_arp_cache_network_interface(ip_address)
|
||||
|
||||
def _process_arp_request(self, arp_packet: ARPPacket, from_network_interface: RouterInterface):
|
||||
"""
|
||||
Processes an ARP request packet received on a router interface.
|
||||
|
||||
If the target IP address matches the interface's IP address, generates and sends an ARP reply.
|
||||
|
||||
:param arp_packet: The received ARP request packet.
|
||||
:type arp_packet: ARPPacket
|
||||
:param from_network_interface: The router interface on which the ARP request was received.
|
||||
:type from_network_interface: RouterInterface
|
||||
"""
|
||||
super()._process_arp_request(arp_packet, from_network_interface)
|
||||
|
||||
# If the target IP matches one of the router's NICs
|
||||
for nic in self.nics.values():
|
||||
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:
|
||||
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)
|
||||
if from_network_interface.enabled and from_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
|
||||
|
||||
def _process_arp_reply(self, arp_packet: ARPPacket, from_network_interface: RouterInterface):
|
||||
"""
|
||||
Processes an ARP reply packet received on a router interface. Updates the ARP cache with the new information.
|
||||
|
||||
:param arp_packet: The received ARP reply packet.
|
||||
:type arp_packet: ARPPacket
|
||||
:param from_network_interface: The router interface on which the ARP reply was received.
|
||||
:type from_network_interface: RouterInterface
|
||||
"""
|
||||
if arp_packet.target_ip_address == from_network_interface.ip_address:
|
||||
super()._process_arp_reply(arp_packet, from_network_interface)
|
||||
|
||||
|
||||
class RouterICMP(ICMP):
|
||||
"""
|
||||
A class to represent a router's Internet Control Message Protocol (ICMP) handler.
|
||||
The Router Internet Control Message Protocol (ICMP) service.
|
||||
|
||||
:param sys_log: System log for logging network events and errors.
|
||||
:type sys_log: SysLog
|
||||
:param arp_cache: The ARP cache for resolving MAC addresses.
|
||||
:type arp_cache: ARPCache
|
||||
:param router: The router to which this ICMP handler belongs.
|
||||
:type router: Router
|
||||
Extends the ICMP service to provide router-specific functionalities for processing ICMP packets. This class is
|
||||
responsible for handling ICMP operations such as echo requests and replies in the context of a router.
|
||||
|
||||
Inherits from:
|
||||
ICMP: Inherits core functionalities for handling ICMP operations, including the processing of echo requests
|
||||
and replies.
|
||||
"""
|
||||
|
||||
router: Router
|
||||
router: Optional[Router] = None
|
||||
|
||||
def __init__(self, sys_log: SysLog, arp_cache: ARPCache, router: Router):
|
||||
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_echo_request(self, frame: Frame, from_network_interface: RouterInterface):
|
||||
"""
|
||||
Process incoming ICMP frames based on ICMP type.
|
||||
Processes an ICMP echo request received by the service.
|
||||
|
||||
:param frame: The incoming frame to process.
|
||||
:param from_nic: The network interface where the frame is coming from.
|
||||
:param is_reattempt: Flag to indicate if the process is a reattempt.
|
||||
:param frame: The network frame containing the ICMP echo request.
|
||||
"""
|
||||
self.sys_log.info(f"Received echo request from {frame.ip.src_ip_address}")
|
||||
|
||||
network_interface = self.software_manager.session_manager.resolve_outbound_network_interface(
|
||||
frame.ip.src_ip_address
|
||||
)
|
||||
|
||||
if not network_interface:
|
||||
self.sys_log.error(
|
||||
"Cannot send ICMP echo reply as there is no outbound Network Interface to use. Try configuring the "
|
||||
"default gateway."
|
||||
)
|
||||
return
|
||||
|
||||
icmp_packet = ICMPPacket(
|
||||
icmp_type=ICMPType.ECHO_REPLY,
|
||||
icmp_code=0,
|
||||
identifier=frame.icmp.identifier,
|
||||
sequence=frame.icmp.sequence + 1,
|
||||
)
|
||||
payload = secrets.token_urlsafe(int(32 / 1.3)) # Standard ICMP 32 bytes size
|
||||
self.sys_log.info(f"Sending echo reply to {frame.ip.dst_ip_address}")
|
||||
|
||||
self.software_manager.session_manager.receive_payload_from_software_manager(
|
||||
payload=payload,
|
||||
dst_ip_address=frame.ip.src_ip_address,
|
||||
dst_port=self.port,
|
||||
ip_protocol=self.protocol,
|
||||
icmp_packet=icmp_packet,
|
||||
)
|
||||
|
||||
def receive(self, payload: Any, session_id: str, **kwargs) -> bool:
|
||||
"""
|
||||
Processes received data, specifically handling ICMP echo requests and replies.
|
||||
|
||||
This method determines the appropriate action based on the packet type and the destination IP address's
|
||||
association with the router interfaces.
|
||||
|
||||
Initially, it checks if the destination IP address of the ICMP packet corresponds to any router interface. If
|
||||
the packet is not destined for an enabled interface but still matches a router interface, it is redirected
|
||||
back to the router for further processing. This ensures proper handling of packets intended for the router
|
||||
itself or needing to be routed to other destinations.
|
||||
|
||||
:param payload: The payload received, expected to be an ICMP packet.
|
||||
:param session_id: The session ID associated with the received data.
|
||||
:param kwargs: Additional keyword arguments, including 'frame' (the received network frame) and
|
||||
'from_network_interface' (the router interface that received the frame).
|
||||
:return: True if the ICMP packet was processed successfully, False otherwise. False indicates either the packet
|
||||
was not ICMP, the destination IP does not correspond to an enabled router interface (and no further action
|
||||
was required), or the ICMP packet type is not handled by this method.
|
||||
"""
|
||||
frame: Frame = kwargs["frame"]
|
||||
from_network_interface = kwargs["from_network_interface"]
|
||||
|
||||
# Check for the presence of an ICMP payload in the frame.
|
||||
if not frame.icmp:
|
||||
return False
|
||||
|
||||
# If the frame's destination IP address corresponds to any router interface, not just enabled ones.
|
||||
if not self.router.ip_is_router_interface(frame.ip.dst_ip_address):
|
||||
# If the frame is not for this router, pass it back down to the Router for potential further routing.
|
||||
self.router.process_frame(frame=frame, from_network_interface=from_network_interface)
|
||||
return True
|
||||
|
||||
# Ensure the destination IP address corresponds to an enabled router interface.
|
||||
if not self.router.ip_is_router_interface(frame.ip.dst_ip_address, enabled_only=True):
|
||||
return False
|
||||
|
||||
# Process ICMP echo requests and replies.
|
||||
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:
|
||||
# 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)
|
||||
tcp_header = TCPHeader(src_port=Port.ARP, dst_port=Port.ARP)
|
||||
|
||||
# Network Layer
|
||||
ip_packet = IPPacket(
|
||||
src_ip_address=nic.ip_address,
|
||||
dst_ip_address=frame.ip.src_ip_address,
|
||||
protocol=IPProtocol.ICMP,
|
||||
)
|
||||
# Data Link Layer
|
||||
ethernet_header = EthernetHeader(
|
||||
src_mac_addr=src_nic.mac_address, dst_mac_addr=target_mac_address
|
||||
)
|
||||
icmp_reply_packet = ICMPPacket(
|
||||
icmp_type=ICMPType.ECHO_REPLY,
|
||||
icmp_code=0,
|
||||
identifier=frame.icmp.identifier,
|
||||
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,
|
||||
)
|
||||
self.sys_log.info(f"Sending echo reply to {frame.ip.dst_ip_address}")
|
||||
|
||||
src_nic.send_frame(frame)
|
||||
return
|
||||
|
||||
# Route the frame
|
||||
self.router.process_frame(frame, from_nic)
|
||||
|
||||
self._process_icmp_echo_request(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:
|
||||
time = frame.transmission_duration()
|
||||
time_str = f"{time}ms" if time > 0 else "<1ms"
|
||||
self.sys_log.info(
|
||||
f"Reply from {frame.ip.src_ip_address}: "
|
||||
f"bytes={len(frame.payload)}, "
|
||||
f"time={time_str}, "
|
||||
f"TTL={frame.ip.ttl}"
|
||||
)
|
||||
if not self.request_replies.get(frame.icmp.identifier):
|
||||
self.request_replies[frame.icmp.identifier] = 0
|
||||
self.request_replies[frame.icmp.identifier] += 1
|
||||
self._process_icmp_echo_reply(frame)
|
||||
|
||||
return
|
||||
# Route the frame
|
||||
self.router.process_frame(frame, from_nic)
|
||||
return True
|
||||
|
||||
|
||||
class RouterNIC(NIC):
|
||||
class RouterInterface(IPWiredNetworkInterface):
|
||||
"""
|
||||
A Router-specific Network Interface Card (NIC) that extends the standard NIC functionality.
|
||||
Represents a Router Interface.
|
||||
|
||||
This class overrides the standard Node NIC's Layer 3 (L3) broadcast/unicast checks. It is designed
|
||||
to handle network frames in a manner specific to routers, allowing them to efficiently process
|
||||
and route network traffic.
|
||||
Router interfaces are used to connect routers to networks. They can route packets across different networks,
|
||||
hence have IP addressing information.
|
||||
|
||||
Inherits from:
|
||||
- WiredNetworkInterface: Provides properties and methods specific to wired connections.
|
||||
- Layer3Interface: Provides Layer 3 properties like ip_address and subnet_mask.
|
||||
"""
|
||||
|
||||
def receive_frame(self, frame: Frame) -> bool:
|
||||
"""
|
||||
Receive and process a network frame from the connected link, provided the NIC is enabled.
|
||||
Receives a network frame on the interface.
|
||||
|
||||
This method is tailored for router behavior. It decrements the frame's Time To Live (TTL), checks for TTL
|
||||
expiration, and captures the frame using PCAP (Packet Capture). The frame is accepted if it is destined for
|
||||
this NIC's MAC address or is a broadcast frame.
|
||||
|
||||
Key Differences from Standard NIC:
|
||||
- Does not perform Layer 3 (IP-based) broadcast checks.
|
||||
- Only checks for Layer 2 (Ethernet) destination MAC address and broadcast frames.
|
||||
|
||||
:param frame: The network frame being received. This should be an instance of the Frame class.
|
||||
:return: Returns True if the frame is processed and passed to the connected node, False otherwise.
|
||||
:param frame: The network frame being received.
|
||||
:return: A boolean indicating whether the frame was successfully received.
|
||||
"""
|
||||
if self.enabled:
|
||||
frame.decrement_ttl()
|
||||
@@ -761,32 +835,46 @@ class RouterNIC(NIC):
|
||||
self._connected_node.sys_log.info("Frame discarded as TTL limit reached")
|
||||
return False
|
||||
frame.set_received_timestamp()
|
||||
self.pcap.capture(frame)
|
||||
self.pcap.capture_inbound(frame)
|
||||
# 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)
|
||||
self._connected_node.receive_frame(frame=frame, from_network_interface=self)
|
||||
return True
|
||||
return False
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.mac_address}/{self.ip_address}"
|
||||
"""
|
||||
String representation of the NIC.
|
||||
|
||||
:return: A string combining the port number, MAC address and IP address of the NIC.
|
||||
"""
|
||||
return f"Port {self.port_num}: {self.mac_address}/{self.ip_address}"
|
||||
|
||||
|
||||
class Router(Node):
|
||||
class Router(NetworkNode):
|
||||
"""
|
||||
A class to represent a network router node.
|
||||
Represents a network router, managing routing and forwarding of IP packets across network interfaces.
|
||||
|
||||
:ivar str hostname: The name of the router node.
|
||||
:ivar int num_ports: The number of ports in the router.
|
||||
:ivar dict kwargs: Optional keyword arguments for SysLog, ACL, RouteTable, RouterARPCache, RouterICMP.
|
||||
A router operates at the network layer and is responsible for receiving, processing, and forwarding data packets
|
||||
between computer networks. It examines the destination IP address of incoming packets and determines the best way
|
||||
to route them towards their destination.
|
||||
|
||||
The router integrates various network services and protocols to facilitate IP routing, including ARP (Address
|
||||
Resolution Protocol) and ICMP (Internet Control Message Protocol) for handling network diagnostics and errors.
|
||||
|
||||
:ivar str hostname: The name of the router, used for identification and logging.
|
||||
:ivar int num_ports: The number of physical or logical ports on the router.
|
||||
:ivar dict kwargs: Optional keyword arguments for initializing components like SysLog, ACL (Access Control List),
|
||||
RouteTable, RouterARP, and RouterICMP services.
|
||||
"""
|
||||
|
||||
num_ports: int
|
||||
ethernet_ports: Dict[int, RouterNIC] = {}
|
||||
network_interfaces: Dict[str, RouterInterface] = {}
|
||||
"The Router Interfaces on the node."
|
||||
network_interface: Dict[int, RouterInterface] = {}
|
||||
"The Router Interfaces on the node by port id."
|
||||
acl: AccessControlList
|
||||
route_table: RouteTable
|
||||
arp: RouterARPCache
|
||||
icmp: RouterICMP
|
||||
|
||||
def __init__(self, hostname: str, num_ports: int = 5, **kwargs):
|
||||
if not kwargs.get("sys_log"):
|
||||
@@ -795,23 +883,48 @@ class Router(Node):
|
||||
kwargs["acl"] = AccessControlList(sys_log=kwargs["sys_log"], implicit_action=ACLAction.DENY)
|
||||
if not kwargs.get("route_table"):
|
||||
kwargs["route_table"] = RouteTable(sys_log=kwargs["sys_log"])
|
||||
if not kwargs.get("arp"):
|
||||
kwargs["arp"] = RouterARPCache(sys_log=kwargs.get("sys_log"), router=self)
|
||||
if not kwargs.get("icmp"):
|
||||
kwargs["icmp"] = RouterICMP(sys_log=kwargs.get("sys_log"), arp_cache=kwargs.get("arp"), router=self)
|
||||
super().__init__(hostname=hostname, num_ports=num_ports, **kwargs)
|
||||
for i in range(1, self.num_ports + 1):
|
||||
nic = RouterNIC(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
|
||||
network_interface = RouterInterface(ip_address="127.0.0.1", subnet_mask="255.0.0.0", gateway="0.0.0.0")
|
||||
self.connect_nic(network_interface)
|
||||
self.network_interface[i] = network_interface
|
||||
|
||||
self.arp.nics = self.nics
|
||||
self.icmp.arp = self.arp
|
||||
self._set_default_acl()
|
||||
|
||||
self.set_original_state()
|
||||
|
||||
def _install_system_software(self):
|
||||
"""
|
||||
Installs essential system software and network services on the router.
|
||||
|
||||
This includes initializing and setting up RouterICMP for handling ICMP packets and RouterARP for address
|
||||
resolution within the network. These services are crucial for the router's operation, enabling it to manage
|
||||
network traffic efficiently.
|
||||
"""
|
||||
self.software_manager.install(RouterICMP)
|
||||
icmp: RouterICMP = self.software_manager.icmp # noqa
|
||||
icmp.router = self
|
||||
self.software_manager.install(RouterARP)
|
||||
arp: RouterARP = self.software_manager.arp # noqa
|
||||
arp.router = self
|
||||
|
||||
def _set_default_acl(self):
|
||||
"""
|
||||
Sets default access control rules for the router.
|
||||
|
||||
Initializes the router's ACL (Access Control List) with default rules, permitting essential protocols like ARP
|
||||
and ICMP, which are necessary for basic network operations and diagnostics.
|
||||
"""
|
||||
self.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.ARP, dst_port=Port.ARP, position=22)
|
||||
self.acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol.ICMP, position=23)
|
||||
|
||||
def set_original_state(self):
|
||||
"""Sets the original state."""
|
||||
"""
|
||||
Sets or resets the router to its original configuration state.
|
||||
|
||||
This method is called to initialize the router's state or to revert it to a known good configuration during
|
||||
network simulations or after configuration changes.
|
||||
"""
|
||||
self.acl.set_original_state()
|
||||
self.route_table.set_original_state()
|
||||
super().set_original_state()
|
||||
@@ -819,12 +932,19 @@ class Router(Node):
|
||||
self._original_state.update(self.model_dump(include=vals_to_include))
|
||||
|
||||
def reset_component_for_episode(self, episode: int):
|
||||
"""Reset the original state of the SimComponent."""
|
||||
self.arp.clear()
|
||||
"""
|
||||
Resets the router's components for a new network simulation episode.
|
||||
|
||||
Clears ARP cache, resets ACL and route table to their original states, and re-enables all network interfaces.
|
||||
This ensures that the router starts from a clean state for each simulation episode.
|
||||
|
||||
:param episode: The episode number for which the router is being reset.
|
||||
"""
|
||||
self.software_manager.arp.clear()
|
||||
self.acl.reset_component_for_episode(episode)
|
||||
self.route_table.reset_component_for_episode(episode)
|
||||
for i, nic in self.ethernet_ports.items():
|
||||
nic.reset_component_for_episode(episode)
|
||||
for i, network_interface in self.network_interface.items():
|
||||
network_interface.reset_component_for_episode(episode)
|
||||
self.enable_port(i)
|
||||
|
||||
super().reset_component_for_episode(episode)
|
||||
@@ -834,15 +954,47 @@ class Router(Node):
|
||||
rm.add_request("acl", RequestType(func=self.acl._request_manager))
|
||||
return rm
|
||||
|
||||
def _get_port_of_nic(self, target_nic: NIC) -> Optional[int]:
|
||||
def ip_is_router_interface(self, ip_address: IPv4Address, enabled_only: bool = False) -> bool:
|
||||
"""
|
||||
Retrieve the port number for a given NIC.
|
||||
Checks if a given IP address belongs to any of the router's interfaces.
|
||||
|
||||
:param target_nic: Target network interface.
|
||||
:return: The port number if NIC is found, otherwise None.
|
||||
:param ip_address: The IP address to check.
|
||||
:param enabled_only: If True, only considers enabled network interfaces.
|
||||
:return: True if the IP address is assigned to one of the router's interfaces; False otherwise.
|
||||
"""
|
||||
for port, nic in self.ethernet_ports.items():
|
||||
if nic == target_nic:
|
||||
for router_interface in self.network_interface.values():
|
||||
if router_interface.ip_address == ip_address:
|
||||
if enabled_only:
|
||||
return router_interface.enabled
|
||||
else:
|
||||
return True
|
||||
return False
|
||||
|
||||
def ip_is_in_router_interface_subnet(self, ip_address: IPv4Address, enabled_only: bool = False) -> bool:
|
||||
"""
|
||||
Determines if a given IP address falls within the subnet of any router interface.
|
||||
|
||||
:param ip_address: The IP address to check.
|
||||
:param enabled_only: If True, only considers enabled network interfaces.
|
||||
:return: True if the IP address is within the subnet of any router's interface; False otherwise.
|
||||
"""
|
||||
for router_interface in self.network_interface.values():
|
||||
if ip_address in router_interface.ip_network:
|
||||
if enabled_only:
|
||||
return router_interface.enabled
|
||||
else:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _get_port_of_nic(self, target_nic: RouterInterface) -> Optional[int]:
|
||||
"""
|
||||
Retrieves the port number associated with a given network interface controller (NIC).
|
||||
|
||||
:param target_nic: The NIC whose port number is being queried.
|
||||
:return: The port number if the NIC is found; otherwise, None.
|
||||
"""
|
||||
for port, network_interface in self.network_interface.items():
|
||||
if network_interface == target_nic:
|
||||
return port
|
||||
|
||||
def describe_state(self) -> Dict:
|
||||
@@ -856,83 +1008,19 @@ class Router(Node):
|
||||
state["acl"] = self.acl.describe_state()
|
||||
return state
|
||||
|
||||
def process_frame(self, frame: Frame, from_nic: NIC, re_attempt: bool = False) -> None:
|
||||
def receive_frame(self, frame: Frame, from_network_interface: RouterInterface):
|
||||
"""
|
||||
Process a Frame.
|
||||
Processes an incoming frame received on one of the router's interfaces.
|
||||
|
||||
:param frame: The frame to be routed.
|
||||
:param from_nic: The source network interface.
|
||||
:param re_attempt: Flag to indicate if the routing is a reattempt.
|
||||
Examines the frame's destination and protocol, applies ACL rules, and either forwards or drops the frame based
|
||||
on routing decisions and ACL permissions.
|
||||
|
||||
:param frame: The incoming frame to be processed.
|
||||
:param from_network_interface: The router interface on which the frame was received.
|
||||
"""
|
||||
# Check if src ip is on network of one of the NICs
|
||||
nic = self.arp.get_arp_cache_nic(frame.ip.dst_ip_address)
|
||||
target_mac = self.arp.get_arp_cache_mac_address(frame.ip.dst_ip_address)
|
||||
|
||||
if re_attempt and not nic:
|
||||
self.sys_log.info(f"Destination {frame.ip.dst_ip_address} is unreachable")
|
||||
if self.operating_state != NodeOperatingState.ON:
|
||||
return
|
||||
|
||||
if not nic:
|
||||
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:
|
||||
self.sys_log.info(f"Frame dropped as NIC {nic} is not enabled")
|
||||
return
|
||||
|
||||
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"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:
|
||||
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:
|
||||
self.sys_log.info(f"Frame dropped as NIC {nic} is not enabled")
|
||||
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):
|
||||
"""
|
||||
Receive a frame from a NIC and processes it based on its protocol.
|
||||
|
||||
:param frame: The incoming frame.
|
||||
:param from_nic: The network interface where the frame is coming from.
|
||||
"""
|
||||
process_frame = False
|
||||
protocol = frame.ip.protocol
|
||||
src_ip_address = frame.ip.src_ip_address
|
||||
dst_ip_address = frame.ip.dst_ip_address
|
||||
@@ -955,59 +1043,156 @@ class Router(Node):
|
||||
)
|
||||
|
||||
if not permitted:
|
||||
at_port = self._get_port_of_nic(from_nic)
|
||||
at_port = self._get_port_of_nic(from_network_interface)
|
||||
self.sys_log.info(f"Frame blocked at port {at_port} by rule {rule}")
|
||||
return
|
||||
if not self.arp.get_arp_cache_nic(src_ip_address):
|
||||
self.arp.add_arp_cache_entry(src_ip_address, frame.ethernet.src_mac_addr, from_nic)
|
||||
if frame.ip.protocol == IPProtocol.ICMP:
|
||||
self.icmp.process_icmp(frame=frame, from_nic=from_nic)
|
||||
|
||||
if frame.ip and self.software_manager.arp:
|
||||
self.software_manager.arp.add_arp_cache_entry(
|
||||
ip_address=frame.ip.src_ip_address,
|
||||
mac_address=frame.ethernet.src_mac_addr,
|
||||
network_interface=from_network_interface,
|
||||
)
|
||||
|
||||
send_to_session_manager = False
|
||||
if (frame.icmp and self.ip_is_router_interface(dst_ip_address)) or (
|
||||
dst_port in self.software_manager.get_open_ports()
|
||||
):
|
||||
send_to_session_manager = True
|
||||
|
||||
if send_to_session_manager:
|
||||
# Port is open on this Router so pass Frame up to session manager first
|
||||
self.session_manager.receive_frame(frame, from_network_interface)
|
||||
else:
|
||||
if src_port == Port.ARP:
|
||||
self.arp.process_arp_packet(from_nic=from_nic, frame=frame, route_table=self.route_table)
|
||||
else:
|
||||
# All other traffic
|
||||
process_frame = True
|
||||
if process_frame:
|
||||
self.process_frame(frame, from_nic)
|
||||
self.process_frame(frame, from_network_interface)
|
||||
|
||||
def process_frame(self, frame: Frame, from_network_interface: RouterInterface) -> None:
|
||||
"""
|
||||
Routes or forwards a frame based on the router's routing table and interface configurations.
|
||||
|
||||
This method is called if a frame is not directly addressed to the router or does not match any open service
|
||||
ports. It determines the next hop for the frame and forwards it accordingly.
|
||||
|
||||
:param frame: The frame to be routed or forwarded.
|
||||
:param from_network_interface: The network interface from which the frame originated.
|
||||
"""
|
||||
# check if frame is addressed to this Router but has failed to be received by a service of application at the
|
||||
# receive_frame stage
|
||||
if frame.ip:
|
||||
for network_interface in self.network_interfaces.values():
|
||||
if network_interface.ip_address == frame.ip.dst_ip_address:
|
||||
self.sys_log.info("Dropping frame destined for this router on a port that isn't open.")
|
||||
return
|
||||
|
||||
network_interface: RouterInterface = self.software_manager.arp.get_arp_cache_network_interface(
|
||||
frame.ip.dst_ip_address
|
||||
)
|
||||
target_mac = self.software_manager.arp.get_arp_cache_mac_address(frame.ip.dst_ip_address)
|
||||
|
||||
if not target_mac:
|
||||
self.sys_log.info(f"Frame dropped as ARP cannot be resolved for {frame.ip.dst_ip_address}")
|
||||
# TODO: Send something back to src, is it some sort of ICMP?
|
||||
return
|
||||
|
||||
if not network_interface:
|
||||
self.sys_log.info(f"Destination {frame.ip.dst_ip_address} is unreachable")
|
||||
# TODO: Send something back to src, is it some sort of ICMP?
|
||||
return
|
||||
|
||||
if not network_interface.enabled:
|
||||
self.sys_log.info(f"Frame dropped as NIC {network_interface} is not enabled")
|
||||
# TODO: Send something back to src, is it some sort of ICMP?
|
||||
return
|
||||
|
||||
if frame.ip.dst_ip_address in network_interface.ip_network:
|
||||
from_port = self._get_port_of_nic(from_network_interface)
|
||||
to_port = self._get_port_of_nic(network_interface)
|
||||
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")
|
||||
# TODO: Send something back to src, is it some sort of ICMP?
|
||||
return
|
||||
frame.ethernet.src_mac_addr = network_interface.mac_address
|
||||
frame.ethernet.dst_mac_addr = target_mac
|
||||
network_interface.send_frame(frame)
|
||||
return
|
||||
else:
|
||||
self.route_frame(frame, from_network_interface)
|
||||
|
||||
def route_frame(self, frame: Frame, from_network_interface: RouterInterface) -> None:
|
||||
"""
|
||||
Determines the best route for a frame and forwards it towards its destination.
|
||||
|
||||
Uses the router's routing table to find the best route for the frame's destination IP address and forwards the
|
||||
frame through the appropriate interface.
|
||||
|
||||
:param frame: The frame to be routed.
|
||||
:param from_network_interface: The source network interface.
|
||||
"""
|
||||
route = self.route_table.find_best_route(frame.ip.dst_ip_address)
|
||||
if route:
|
||||
network_interface = self.software_manager.arp.get_arp_cache_network_interface(route.next_hop_ip_address)
|
||||
target_mac = self.software_manager.arp.get_arp_cache_mac_address(route.next_hop_ip_address)
|
||||
if not network_interface:
|
||||
self.sys_log.info(f"Destination {frame.ip.dst_ip_address} is unreachable")
|
||||
# TODO: Send something back to src, is it some sort of ICMP?
|
||||
return
|
||||
|
||||
if not network_interface.enabled:
|
||||
self.sys_log.info(f"Frame dropped as NIC {network_interface} is not enabled")
|
||||
# TODO: Send something back to src, is it some sort of ICMP?
|
||||
return
|
||||
|
||||
from_port = self._get_port_of_nic(from_network_interface)
|
||||
to_port = self._get_port_of_nic(network_interface)
|
||||
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")
|
||||
# TODO: Send something back to src, is it some sort of ICMP?
|
||||
return
|
||||
frame.ethernet.src_mac_addr = network_interface.mac_address
|
||||
frame.ethernet.dst_mac_addr = target_mac
|
||||
network_interface.send_frame(frame)
|
||||
|
||||
def configure_port(self, port: int, ip_address: Union[IPv4Address, str], subnet_mask: Union[IPv4Address, str]):
|
||||
"""
|
||||
Configure the IP settings of a given port.
|
||||
Configures the IP settings for a specified router port.
|
||||
|
||||
:param port: The port to configure.
|
||||
:param ip_address: The IP address to set.
|
||||
:param subnet_mask: The subnet mask to set.
|
||||
:param port: The port number to configure.
|
||||
:param ip_address: The IP address to assign to the port.
|
||||
:param subnet_mask: The subnet mask for the port.
|
||||
"""
|
||||
if not isinstance(ip_address, IPv4Address):
|
||||
ip_address = IPv4Address(ip_address)
|
||||
if not isinstance(subnet_mask, IPv4Address):
|
||||
subnet_mask = IPv4Address(subnet_mask)
|
||||
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={ip_address}/{nic.ip_network.prefixlen}")
|
||||
network_interface = self.network_interface[port]
|
||||
network_interface.ip_address = ip_address
|
||||
network_interface.subnet_mask = subnet_mask
|
||||
self.sys_log.info(f"Configured Network Interface {network_interface}")
|
||||
self.set_original_state()
|
||||
|
||||
def enable_port(self, port: int):
|
||||
"""
|
||||
Enable a given port on the router.
|
||||
Enables a specified port on the router.
|
||||
|
||||
:param port: The port to enable.
|
||||
:param port: The port number to enable.
|
||||
"""
|
||||
nic = self.ethernet_ports.get(port)
|
||||
if nic:
|
||||
nic.enable()
|
||||
network_interface = self.network_interface.get(port)
|
||||
if network_interface:
|
||||
network_interface.enable()
|
||||
|
||||
def disable_port(self, port: int):
|
||||
"""
|
||||
Disable a given port on the router.
|
||||
Disables a specified port on the router.
|
||||
|
||||
:param port: The port to disable.
|
||||
:param port: The port number to disable.
|
||||
"""
|
||||
nic = self.ethernet_ports.get(port)
|
||||
if nic:
|
||||
nic.disable()
|
||||
network_interface = self.network_interface.get(port)
|
||||
if network_interface:
|
||||
network_interface.disable()
|
||||
|
||||
def show(self, markdown: bool = False):
|
||||
"""
|
||||
@@ -1021,14 +1206,14 @@ class Router(Node):
|
||||
table.set_style(MARKDOWN)
|
||||
table.align = "l"
|
||||
table.title = f"{self.hostname} Ethernet Interfaces"
|
||||
for port, nic in self.ethernet_ports.items():
|
||||
for port, network_interface in self.network_interface.items():
|
||||
table.add_row(
|
||||
[
|
||||
port,
|
||||
nic.mac_address,
|
||||
f"{nic.ip_address}/{nic.ip_network.prefixlen}",
|
||||
nic.speed,
|
||||
"Enabled" if nic.enabled else "Disabled",
|
||||
network_interface.mac_address,
|
||||
f"{network_interface.ip_address}/{network_interface.ip_network.prefixlen}",
|
||||
network_interface.speed,
|
||||
"Enabled" if network_interface.enabled else "Disabled",
|
||||
]
|
||||
)
|
||||
print(table)
|
||||
200
src/primaite/simulator/network/hardware/nodes/network/switch.py
Normal file
200
src/primaite/simulator/network/hardware/nodes/network/switch.py
Normal file
@@ -0,0 +1,200 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Dict, Optional
|
||||
|
||||
from prettytable import MARKDOWN, PrettyTable
|
||||
|
||||
from primaite import getLogger
|
||||
from primaite.exceptions import NetworkError
|
||||
from primaite.simulator.network.hardware.base import Link, WiredNetworkInterface
|
||||
from primaite.simulator.network.hardware.nodes.network.network_node import NetworkNode
|
||||
from primaite.simulator.network.transmission.data_link_layer import Frame
|
||||
|
||||
_LOGGER = getLogger(__name__)
|
||||
|
||||
|
||||
class SwitchPort(WiredNetworkInterface):
|
||||
"""
|
||||
Represents a Switch Port.
|
||||
|
||||
Switch ports connect devices within the same network. They operate at the data link layer (Layer 2) of the OSI model
|
||||
and are responsible for receiving and forwarding frames based on MAC addresses. Despite operating at Layer 2,
|
||||
they are an essential part of network infrastructure, enabling LAN segmentation, bandwidth management, and
|
||||
the creation of VLANs.
|
||||
|
||||
Inherits from:
|
||||
- WiredNetworkInterface: Provides properties and methods specific to wired connections.
|
||||
|
||||
Switch ports typically do not have IP addresses assigned to them as they function at Layer 2, but managed switches
|
||||
can have management IP addresses for remote management and configuration purposes.
|
||||
"""
|
||||
|
||||
_connected_node: Optional[Switch] = None
|
||||
"The Switch to which the SwitchPort is connected."
|
||||
|
||||
def set_original_state(self):
|
||||
"""Sets the original state."""
|
||||
vals_to_include = {"port_num", "mac_address", "speed", "mtu", "enabled"}
|
||||
self._original_state = self.model_dump(include=vals_to_include)
|
||||
super().set_original_state()
|
||||
|
||||
def describe_state(self) -> Dict:
|
||||
"""
|
||||
Produce a dictionary describing the current state of this object.
|
||||
|
||||
:return: Current state of this object and child objects.
|
||||
:rtype: Dict
|
||||
"""
|
||||
state = super().describe_state()
|
||||
state.update(
|
||||
{
|
||||
"mac_address": self.mac_address,
|
||||
"speed": self.speed,
|
||||
"mtu": self.mtu,
|
||||
"enabled": self.enabled,
|
||||
}
|
||||
)
|
||||
return state
|
||||
|
||||
def send_frame(self, frame: Frame) -> bool:
|
||||
"""
|
||||
Attempts to send a network frame through the interface.
|
||||
|
||||
:param frame: The network frame to be sent.
|
||||
:return: A boolean indicating whether the frame was successfully sent.
|
||||
"""
|
||||
if self.enabled:
|
||||
self.pcap.capture_outbound(frame)
|
||||
self._connected_link.transmit_frame(sender_nic=self, frame=frame)
|
||||
return True
|
||||
# Cannot send Frame as the SwitchPort is not enabled
|
||||
return False
|
||||
|
||||
def receive_frame(self, frame: Frame) -> bool:
|
||||
"""
|
||||
Receives a network frame on the interface.
|
||||
|
||||
:param frame: The network frame being received.
|
||||
:return: A boolean indicating whether the frame was successfully received.
|
||||
"""
|
||||
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_inbound(frame)
|
||||
self._connected_node.receive_frame(frame=frame, from_network_interface=self)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class Switch(NetworkNode):
|
||||
"""
|
||||
A class representing a Layer 2 network switch.
|
||||
|
||||
:ivar num_ports: The number of ports on the switch. Default is 24.
|
||||
"""
|
||||
|
||||
num_ports: int = 24
|
||||
"The number of ports on the switch."
|
||||
network_interfaces: Dict[str, SwitchPort] = {}
|
||||
"The SwitchPorts on the Switch."
|
||||
network_interface: Dict[int, SwitchPort] = {}
|
||||
"The SwitchPorts on the Switch by port id."
|
||||
mac_address_table: Dict[str, SwitchPort] = {}
|
||||
"A MAC address table mapping destination MAC addresses to corresponding SwitchPorts."
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
if not self.network_interface:
|
||||
self.network_interface = {i: SwitchPort() for i in range(1, self.num_ports + 1)}
|
||||
for port_num, port in self.network_interface.items():
|
||||
port._connected_node = self
|
||||
port.port_num = port_num
|
||||
port.parent = self
|
||||
port.port_num = port_num
|
||||
|
||||
def show(self, markdown: bool = False):
|
||||
"""
|
||||
Prints a table of the SwitchPorts on the Switch.
|
||||
|
||||
:param markdown: If True, outputs the table in markdown format. Default is False.
|
||||
"""
|
||||
table = PrettyTable(["Port", "MAC Address", "Speed", "Status"])
|
||||
if markdown:
|
||||
table.set_style(MARKDOWN)
|
||||
table.align = "l"
|
||||
table.title = f"{self.hostname} Switch Ports"
|
||||
for port_num, port in self.network_interface.items():
|
||||
table.add_row([port_num, port.mac_address, port.speed, "Enabled" if port.enabled else "Disabled"])
|
||||
print(table)
|
||||
|
||||
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["ports"] = {port_num: port.describe_state() for port_num, port in self.network_interface.items()}
|
||||
state["num_ports"] = self.num_ports # redundant?
|
||||
state["mac_address_table"] = {mac: port.port_num for mac, port in self.mac_address_table.items()}
|
||||
return state
|
||||
|
||||
def _add_mac_table_entry(self, mac_address: str, switch_port: SwitchPort):
|
||||
"""
|
||||
Private method to add an entry to the MAC address table.
|
||||
|
||||
:param mac_address: MAC address to be added.
|
||||
:param switch_port: Corresponding SwitchPort object.
|
||||
"""
|
||||
mac_table_port = self.mac_address_table.get(mac_address)
|
||||
if not mac_table_port:
|
||||
self.mac_address_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.mac_address_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 receive_frame(self, frame: Frame, from_network_interface: SwitchPort):
|
||||
"""
|
||||
Forward a frame to the appropriate port based on the destination MAC address.
|
||||
|
||||
:param frame: The Frame being received.
|
||||
:param from_network_interface: The SwitchPort that received the frame.
|
||||
"""
|
||||
src_mac = frame.ethernet.src_mac_addr
|
||||
dst_mac = frame.ethernet.dst_mac_addr
|
||||
self._add_mac_table_entry(src_mac, from_network_interface)
|
||||
|
||||
outgoing_port = self.mac_address_table.get(dst_mac)
|
||||
if outgoing_port and dst_mac.lower() != "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.network_interface.values():
|
||||
if port.enabled and port != from_network_interface:
|
||||
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.network_interface.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()
|
||||
@@ -1,120 +0,0 @@
|
||||
from typing import Dict
|
||||
|
||||
from prettytable import MARKDOWN, PrettyTable
|
||||
|
||||
from primaite import getLogger
|
||||
from primaite.exceptions import NetworkError
|
||||
from primaite.simulator.network.hardware.base import Link, Node, SwitchPort
|
||||
from primaite.simulator.network.transmission.data_link_layer import Frame
|
||||
|
||||
_LOGGER = getLogger(__name__)
|
||||
|
||||
|
||||
class Switch(Node):
|
||||
"""
|
||||
A class representing a Layer 2 network switch.
|
||||
|
||||
:ivar num_ports: The number of ports on the switch. Default is 24.
|
||||
"""
|
||||
|
||||
num_ports: int = 24
|
||||
"The number of ports on the switch."
|
||||
switch_ports: Dict[int, SwitchPort] = {}
|
||||
"The SwitchPorts on the switch."
|
||||
mac_address_table: Dict[str, SwitchPort] = {}
|
||||
"A MAC address table mapping destination MAC addresses to corresponding SwitchPorts."
|
||||
|
||||
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_on_node = port_num
|
||||
port.parent = self
|
||||
port.port_num = port_num
|
||||
|
||||
def show(self, markdown: bool = False):
|
||||
"""
|
||||
Prints a table of the SwitchPorts on the Switch.
|
||||
|
||||
:param markdown: If True, outputs the table in markdown format. Default is False.
|
||||
"""
|
||||
table = PrettyTable(["Port", "MAC Address", "Speed", "Status"])
|
||||
if markdown:
|
||||
table.set_style(MARKDOWN)
|
||||
table.align = "l"
|
||||
table.title = f"{self.hostname} Switch Ports"
|
||||
for port_num, port in self.switch_ports.items():
|
||||
table.add_row([port_num, port.mac_address, port.speed, "Enabled" if port.enabled else "Disabled"])
|
||||
print(table)
|
||||
|
||||
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["ports"] = {port_num: port.describe_state() for port_num, port in self.switch_ports.items()}
|
||||
state["num_ports"] = self.num_ports # redundant?
|
||||
state["mac_address_table"] = {mac: port.port_num for mac, port in self.mac_address_table.items()}
|
||||
return state
|
||||
|
||||
def _add_mac_table_entry(self, mac_address: str, switch_port: SwitchPort):
|
||||
"""
|
||||
Private method to add an entry to the MAC address table.
|
||||
|
||||
:param mac_address: MAC address to be added.
|
||||
:param switch_port: Corresponding SwitchPort object.
|
||||
"""
|
||||
mac_table_port = self.mac_address_table.get(mac_address)
|
||||
if not mac_table_port:
|
||||
self.mac_address_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.mac_address_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.mac_address_table.get(dst_mac)
|
||||
if outgoing_port and dst_mac.lower() != "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.enabled and 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()
|
||||
@@ -1,11 +1,11 @@
|
||||
from ipaddress import IPv4Address
|
||||
|
||||
from primaite.simulator.network.container import Network
|
||||
from primaite.simulator.network.hardware.base import NIC, NodeOperatingState
|
||||
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.server import Server
|
||||
from primaite.simulator.network.hardware.nodes.switch import Switch
|
||||
from primaite.simulator.network.hardware.nodes.host.computer import Computer
|
||||
from primaite.simulator.network.hardware.nodes.host.host_node import NIC
|
||||
from primaite.simulator.network.hardware.nodes.host.server import Server
|
||||
from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router
|
||||
from primaite.simulator.network.hardware.nodes.network.switch import Switch
|
||||
from primaite.simulator.network.transmission.network_layer import IPProtocol
|
||||
from primaite.simulator.network.transmission.transport_layer import Port
|
||||
from primaite.simulator.system.applications.database_client import DatabaseClient
|
||||
@@ -40,13 +40,13 @@ def client_server_routed() -> Network:
|
||||
# Switch 1
|
||||
switch_1 = Switch(hostname="switch_1", num_ports=6)
|
||||
switch_1.power_on()
|
||||
network.connect(endpoint_a=router_1.ethernet_ports[1], endpoint_b=switch_1.switch_ports[6])
|
||||
network.connect(endpoint_a=router_1.network_interface[1], endpoint_b=switch_1.network_interface[6])
|
||||
router_1.enable_port(1)
|
||||
|
||||
# Switch 2
|
||||
switch_2 = Switch(hostname="switch_2", num_ports=6)
|
||||
switch_2.power_on()
|
||||
network.connect(endpoint_a=router_1.ethernet_ports[2], endpoint_b=switch_2.switch_ports[6])
|
||||
network.connect(endpoint_a=router_1.network_interface[2], endpoint_b=switch_2.network_interface[6])
|
||||
router_1.enable_port(2)
|
||||
|
||||
# Client 1
|
||||
@@ -55,10 +55,10 @@ def client_server_routed() -> Network:
|
||||
ip_address="192.168.2.2",
|
||||
subnet_mask="255.255.255.0",
|
||||
default_gateway="192.168.2.1",
|
||||
operating_state=NodeOperatingState.ON,
|
||||
start_up_duration=0,
|
||||
)
|
||||
client_1.power_on()
|
||||
network.connect(endpoint_b=client_1.ethernet_port[1], endpoint_a=switch_2.switch_ports[1])
|
||||
network.connect(endpoint_b=client_1.network_interface[1], endpoint_a=switch_2.network_interface[1])
|
||||
|
||||
# Server 1
|
||||
server_1 = Server(
|
||||
@@ -66,10 +66,10 @@ def client_server_routed() -> Network:
|
||||
ip_address="192.168.1.2",
|
||||
subnet_mask="255.255.255.0",
|
||||
default_gateway="192.168.1.1",
|
||||
operating_state=NodeOperatingState.ON,
|
||||
start_up_duration=0,
|
||||
)
|
||||
server_1.power_on()
|
||||
network.connect(endpoint_b=server_1.ethernet_port[1], endpoint_a=switch_1.switch_ports[1])
|
||||
network.connect(endpoint_b=server_1.network_interface[1], endpoint_a=switch_1.network_interface[1])
|
||||
|
||||
router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.ARP, dst_port=Port.ARP, position=22)
|
||||
|
||||
@@ -118,21 +118,21 @@ def arcd_uc2_network() -> Network:
|
||||
network = Network()
|
||||
|
||||
# Router 1
|
||||
router_1 = Router(hostname="router_1", num_ports=5, operating_state=NodeOperatingState.ON)
|
||||
router_1 = Router(hostname="router_1", num_ports=5, start_up_duration=0)
|
||||
router_1.power_on()
|
||||
router_1.configure_port(port=1, ip_address="192.168.1.1", subnet_mask="255.255.255.0")
|
||||
router_1.configure_port(port=2, ip_address="192.168.10.1", subnet_mask="255.255.255.0")
|
||||
|
||||
# Switch 1
|
||||
switch_1 = Switch(hostname="switch_1", num_ports=8, operating_state=NodeOperatingState.ON)
|
||||
switch_1 = Switch(hostname="switch_1", num_ports=8, start_up_duration=0)
|
||||
switch_1.power_on()
|
||||
network.connect(endpoint_a=router_1.ethernet_ports[1], endpoint_b=switch_1.switch_ports[8])
|
||||
network.connect(endpoint_a=router_1.network_interface[1], endpoint_b=switch_1.network_interface[8])
|
||||
router_1.enable_port(1)
|
||||
|
||||
# Switch 2
|
||||
switch_2 = Switch(hostname="switch_2", num_ports=8, operating_state=NodeOperatingState.ON)
|
||||
switch_2 = Switch(hostname="switch_2", num_ports=8, start_up_duration=0)
|
||||
switch_2.power_on()
|
||||
network.connect(endpoint_a=router_1.ethernet_ports[2], endpoint_b=switch_2.switch_ports[8])
|
||||
network.connect(endpoint_a=router_1.network_interface[2], endpoint_b=switch_2.network_interface[8])
|
||||
router_1.enable_port(2)
|
||||
|
||||
# Client 1
|
||||
@@ -142,10 +142,10 @@ def arcd_uc2_network() -> Network:
|
||||
subnet_mask="255.255.255.0",
|
||||
default_gateway="192.168.10.1",
|
||||
dns_server=IPv4Address("192.168.1.10"),
|
||||
operating_state=NodeOperatingState.ON,
|
||||
start_up_duration=0,
|
||||
)
|
||||
client_1.power_on()
|
||||
network.connect(endpoint_b=client_1.ethernet_port[1], endpoint_a=switch_2.switch_ports[1])
|
||||
network.connect(endpoint_b=client_1.network_interface[1], endpoint_a=switch_2.network_interface[1])
|
||||
client_1.software_manager.install(DataManipulationBot)
|
||||
db_manipulation_bot: DataManipulationBot = client_1.software_manager.software.get("DataManipulationBot")
|
||||
db_manipulation_bot.configure(
|
||||
@@ -162,12 +162,12 @@ def arcd_uc2_network() -> Network:
|
||||
subnet_mask="255.255.255.0",
|
||||
default_gateway="192.168.10.1",
|
||||
dns_server=IPv4Address("192.168.1.10"),
|
||||
operating_state=NodeOperatingState.ON,
|
||||
start_up_duration=0,
|
||||
)
|
||||
client_2.power_on()
|
||||
web_browser = client_2.software_manager.software.get("WebBrowser")
|
||||
web_browser.target_url = "http://arcd.com/users/"
|
||||
network.connect(endpoint_b=client_2.ethernet_port[1], endpoint_a=switch_2.switch_ports[2])
|
||||
network.connect(endpoint_b=client_2.network_interface[1], endpoint_a=switch_2.network_interface[2])
|
||||
|
||||
# Domain Controller
|
||||
domain_controller = Server(
|
||||
@@ -175,12 +175,12 @@ def arcd_uc2_network() -> Network:
|
||||
ip_address="192.168.1.10",
|
||||
subnet_mask="255.255.255.0",
|
||||
default_gateway="192.168.1.1",
|
||||
operating_state=NodeOperatingState.ON,
|
||||
start_up_duration=0,
|
||||
)
|
||||
domain_controller.power_on()
|
||||
domain_controller.software_manager.install(DNSServer)
|
||||
|
||||
network.connect(endpoint_b=domain_controller.ethernet_port[1], endpoint_a=switch_1.switch_ports[1])
|
||||
network.connect(endpoint_b=domain_controller.network_interface[1], endpoint_a=switch_1.network_interface[1])
|
||||
|
||||
# Database Server
|
||||
database_server = Server(
|
||||
@@ -189,10 +189,10 @@ def arcd_uc2_network() -> Network:
|
||||
subnet_mask="255.255.255.0",
|
||||
default_gateway="192.168.1.1",
|
||||
dns_server=IPv4Address("192.168.1.10"),
|
||||
operating_state=NodeOperatingState.ON,
|
||||
start_up_duration=0,
|
||||
)
|
||||
database_server.power_on()
|
||||
network.connect(endpoint_b=database_server.ethernet_port[1], endpoint_a=switch_1.switch_ports[3])
|
||||
network.connect(endpoint_b=database_server.network_interface[1], endpoint_a=switch_1.network_interface[3])
|
||||
|
||||
ddl = """
|
||||
CREATE TABLE IF NOT EXISTS user (
|
||||
@@ -263,14 +263,14 @@ def arcd_uc2_network() -> Network:
|
||||
subnet_mask="255.255.255.0",
|
||||
default_gateway="192.168.1.1",
|
||||
dns_server=IPv4Address("192.168.1.10"),
|
||||
operating_state=NodeOperatingState.ON,
|
||||
start_up_duration=0,
|
||||
)
|
||||
web_server.power_on()
|
||||
web_server.software_manager.install(DatabaseClient)
|
||||
|
||||
database_client: DatabaseClient = web_server.software_manager.software.get("DatabaseClient")
|
||||
database_client.configure(server_ip_address=IPv4Address("192.168.1.14"))
|
||||
network.connect(endpoint_b=web_server.ethernet_port[1], endpoint_a=switch_1.switch_ports[2])
|
||||
network.connect(endpoint_b=web_server.network_interface[1], endpoint_a=switch_1.network_interface[2])
|
||||
database_client.run()
|
||||
database_client.connect()
|
||||
|
||||
@@ -278,7 +278,7 @@ def arcd_uc2_network() -> Network:
|
||||
|
||||
# register the web_server to a domain
|
||||
dns_server_service: DNSServer = domain_controller.software_manager.software.get("DNSServer") # noqa
|
||||
dns_server_service.dns_register("arcd.com", web_server.ip_address)
|
||||
dns_server_service.dns_register("arcd.com", web_server.network_interface[1].ip_address)
|
||||
|
||||
# Backup Server
|
||||
backup_server = Server(
|
||||
@@ -287,11 +287,11 @@ def arcd_uc2_network() -> Network:
|
||||
subnet_mask="255.255.255.0",
|
||||
default_gateway="192.168.1.1",
|
||||
dns_server=IPv4Address("192.168.1.10"),
|
||||
operating_state=NodeOperatingState.ON,
|
||||
start_up_duration=0,
|
||||
)
|
||||
backup_server.power_on()
|
||||
backup_server.software_manager.install(FTPServer)
|
||||
network.connect(endpoint_b=backup_server.ethernet_port[1], endpoint_a=switch_1.switch_ports[4])
|
||||
network.connect(endpoint_b=backup_server.network_interface[1], endpoint_a=switch_1.network_interface[4])
|
||||
|
||||
# Security Suite
|
||||
security_suite = Server(
|
||||
@@ -300,12 +300,12 @@ def arcd_uc2_network() -> Network:
|
||||
subnet_mask="255.255.255.0",
|
||||
default_gateway="192.168.1.1",
|
||||
dns_server=IPv4Address("192.168.1.10"),
|
||||
operating_state=NodeOperatingState.ON,
|
||||
start_up_duration=0,
|
||||
)
|
||||
security_suite.power_on()
|
||||
network.connect(endpoint_b=security_suite.ethernet_port[1], endpoint_a=switch_1.switch_ports[7])
|
||||
network.connect(endpoint_b=security_suite.network_interface[1], endpoint_a=switch_1.network_interface[7])
|
||||
security_suite.connect_nic(NIC(ip_address="192.168.10.110", subnet_mask="255.255.255.0"))
|
||||
network.connect(endpoint_b=security_suite.ethernet_port[2], endpoint_a=switch_2.switch_ports[7])
|
||||
network.connect(endpoint_b=security_suite.network_interface[2], endpoint_a=switch_2.network_interface[7])
|
||||
|
||||
router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.ARP, dst_port=Port.ARP, position=22)
|
||||
|
||||
|
||||
@@ -13,11 +13,12 @@ class ARPEntry(BaseModel):
|
||||
Represents an entry in the ARP 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_uuid: The UIId of the Network Interface through which the NIC with the IP address is
|
||||
reachable.
|
||||
"""
|
||||
|
||||
mac_address: str
|
||||
nic_uuid: str
|
||||
network_interface_uuid: str
|
||||
|
||||
|
||||
class ARPPacket(DataPacket):
|
||||
|
||||
114
src/primaite/simulator/network/protocols/icmp.py
Normal file
114
src/primaite/simulator/network/protocols/icmp.py
Normal file
@@ -0,0 +1,114 @@
|
||||
import secrets
|
||||
from enum import Enum
|
||||
from typing import Union
|
||||
|
||||
from pydantic import BaseModel, field_validator, validate_call
|
||||
from pydantic_core.core_schema import FieldValidationInfo
|
||||
|
||||
from primaite import getLogger
|
||||
|
||||
_LOGGER = getLogger(__name__)
|
||||
|
||||
|
||||
class ICMPType(Enum):
|
||||
"""Enumeration of common ICMP (Internet Control Message Protocol) types."""
|
||||
|
||||
ECHO_REPLY = 0
|
||||
"Echo Reply message."
|
||||
DESTINATION_UNREACHABLE = 3
|
||||
"Destination Unreachable."
|
||||
REDIRECT = 5
|
||||
"Redirect."
|
||||
ECHO_REQUEST = 8
|
||||
"Echo Request (ping)."
|
||||
ROUTER_ADVERTISEMENT = 9
|
||||
"Router Advertisement."
|
||||
ROUTER_SOLICITATION = 10
|
||||
"Router discovery/selection/solicitation."
|
||||
TIME_EXCEEDED = 11
|
||||
"Time Exceeded."
|
||||
TIMESTAMP_REQUEST = 13
|
||||
"Timestamp Request."
|
||||
TIMESTAMP_REPLY = 14
|
||||
"Timestamp Reply."
|
||||
|
||||
|
||||
@validate_call
|
||||
def get_icmp_type_code_description(icmp_type: ICMPType, icmp_code: int) -> Union[str, None]:
|
||||
"""
|
||||
Maps ICMPType and code pairings to their respective description.
|
||||
|
||||
:param icmp_type: An ICMPType.
|
||||
:param icmp_code: An icmp code.
|
||||
:return: The icmp type and code pairing description if it exists, otherwise returns None.
|
||||
"""
|
||||
icmp_code_descriptions = {
|
||||
ICMPType.ECHO_REPLY: {0: "Echo reply"},
|
||||
ICMPType.DESTINATION_UNREACHABLE: {
|
||||
0: "Destination network unreachable",
|
||||
1: "Destination host unreachable",
|
||||
2: "Destination protocol unreachable",
|
||||
3: "Destination port unreachable",
|
||||
4: "Fragmentation required",
|
||||
5: "Source route failed",
|
||||
6: "Destination network unknown",
|
||||
7: "Destination host unknown",
|
||||
8: "Source host isolated",
|
||||
9: "Network administratively prohibited",
|
||||
10: "Host administratively prohibited",
|
||||
11: "Network unreachable for ToS",
|
||||
12: "Host unreachable for ToS",
|
||||
13: "Communication administratively prohibited",
|
||||
14: "Host Precedence Violation",
|
||||
15: "Precedence cutoff in effect",
|
||||
},
|
||||
ICMPType.REDIRECT: {
|
||||
0: "Redirect Datagram for the Network",
|
||||
1: "Redirect Datagram for the Host",
|
||||
},
|
||||
ICMPType.ECHO_REQUEST: {0: "Echo request"},
|
||||
ICMPType.ROUTER_ADVERTISEMENT: {0: "Router Advertisement"},
|
||||
ICMPType.ROUTER_SOLICITATION: {0: "Router discovery/selection/solicitation"},
|
||||
ICMPType.TIME_EXCEEDED: {0: "TTL expired in transit", 1: "Fragment reassembly time exceeded"},
|
||||
ICMPType.TIMESTAMP_REQUEST: {0: "Timestamp Request"},
|
||||
ICMPType.TIMESTAMP_REPLY: {0: "Timestamp reply"},
|
||||
}
|
||||
return icmp_code_descriptions[icmp_type].get(icmp_code)
|
||||
|
||||
|
||||
class ICMPPacket(BaseModel):
|
||||
"""Models an ICMP Packet."""
|
||||
|
||||
icmp_type: ICMPType = ICMPType.ECHO_REQUEST
|
||||
"ICMP Type."
|
||||
icmp_code: int = 0
|
||||
"ICMP Code."
|
||||
identifier: int
|
||||
"ICMP identifier (16 bits randomly generated)."
|
||||
sequence: int = 0
|
||||
"ICMP message sequence number."
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
if not kwargs.get("identifier"):
|
||||
kwargs["identifier"] = secrets.randbits(16)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
@field_validator("icmp_code") # noqa
|
||||
@classmethod
|
||||
def _icmp_type_must_have_icmp_code(cls, v: int, info: FieldValidationInfo) -> int:
|
||||
"""Validates the icmp_type and icmp_code."""
|
||||
icmp_type = info.data["icmp_type"]
|
||||
if get_icmp_type_code_description(icmp_type, v):
|
||||
return v
|
||||
msg = f"No Matching ICMP code for type:{icmp_type.name}, code:{v}"
|
||||
_LOGGER.error(msg)
|
||||
raise ValueError(msg)
|
||||
|
||||
def code_description(self) -> str:
|
||||
"""The icmp_code description."""
|
||||
description = get_icmp_type_code_description(self.icmp_type, self.icmp_code)
|
||||
if description:
|
||||
return description
|
||||
msg = f"No Matching ICMP code for type:{self.icmp_type.name}, code:{self.icmp_code}"
|
||||
_LOGGER.error(msg)
|
||||
raise ValueError(msg)
|
||||
@@ -4,9 +4,9 @@ from typing import Any, Optional
|
||||
from pydantic import BaseModel
|
||||
|
||||
from primaite import getLogger
|
||||
from primaite.simulator.network.protocols.arp import ARPPacket
|
||||
from primaite.simulator.network.protocols.icmp import ICMPPacket
|
||||
from primaite.simulator.network.protocols.packet import DataPacket
|
||||
from primaite.simulator.network.transmission.network_layer import ICMPPacket, IPPacket, IPProtocol
|
||||
from primaite.simulator.network.transmission.network_layer import IPPacket, IPProtocol
|
||||
from primaite.simulator.network.transmission.primaite_layer import PrimaiteHeader
|
||||
from primaite.simulator.network.transmission.transport_layer import TCPHeader, UDPHeader
|
||||
from primaite.simulator.network.utils import convert_bytes_to_megabits
|
||||
@@ -73,7 +73,7 @@ class Frame(BaseModel):
|
||||
msg = "Cannot build a Frame using the TCP IP Protocol without a TCPHeader"
|
||||
_LOGGER.error(msg)
|
||||
raise ValueError(msg)
|
||||
if kwargs["ip"].protocol == IPProtocol.UDP and not kwargs.get("UDP"):
|
||||
if kwargs["ip"].protocol == IPProtocol.UDP and not kwargs.get("udp"):
|
||||
msg = "Cannot build a Frame using the UDP IP Protocol without a UDPHeader"
|
||||
_LOGGER.error(msg)
|
||||
raise ValueError(msg)
|
||||
@@ -95,8 +95,6 @@ class Frame(BaseModel):
|
||||
"UDP header."
|
||||
icmp: Optional[ICMPPacket] = None
|
||||
"ICMP header."
|
||||
arp: Optional[ARPPacket] = None
|
||||
"ARP packet."
|
||||
primaite: PrimaiteHeader
|
||||
"PrimAITE header."
|
||||
payload: Optional[Any] = None
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import secrets
|
||||
from enum import Enum
|
||||
from ipaddress import IPv4Address
|
||||
from typing import Union
|
||||
|
||||
from pydantic import BaseModel, field_validator, validate_call
|
||||
from pydantic_core.core_schema import FieldValidationInfo
|
||||
from pydantic import BaseModel
|
||||
|
||||
from primaite import getLogger
|
||||
|
||||
@@ -54,110 +51,6 @@ class Precedence(Enum):
|
||||
"Highest priority level, used for the most critical network control messages, such as routing protocol hellos."
|
||||
|
||||
|
||||
class ICMPType(Enum):
|
||||
"""Enumeration of common ICMP (Internet Control Message Protocol) types."""
|
||||
|
||||
ECHO_REPLY = 0
|
||||
"Echo Reply message."
|
||||
DESTINATION_UNREACHABLE = 3
|
||||
"Destination Unreachable."
|
||||
REDIRECT = 5
|
||||
"Redirect."
|
||||
ECHO_REQUEST = 8
|
||||
"Echo Request (ping)."
|
||||
ROUTER_ADVERTISEMENT = 10
|
||||
"Router Advertisement."
|
||||
ROUTER_SOLICITATION = 11
|
||||
"Router discovery/selection/solicitation."
|
||||
TIME_EXCEEDED = 11
|
||||
"Time Exceeded."
|
||||
TIMESTAMP_REQUEST = 13
|
||||
"Timestamp Request."
|
||||
TIMESTAMP_REPLY = 14
|
||||
"Timestamp Reply."
|
||||
|
||||
|
||||
@validate_call
|
||||
def get_icmp_type_code_description(icmp_type: ICMPType, icmp_code: int) -> Union[str, None]:
|
||||
"""
|
||||
Maps ICMPType and code pairings to their respective description.
|
||||
|
||||
:param icmp_type: An ICMPType.
|
||||
:param icmp_code: An icmp code.
|
||||
:return: The icmp type and code pairing description if it exists, otherwise returns None.
|
||||
"""
|
||||
icmp_code_descriptions = {
|
||||
ICMPType.ECHO_REPLY: {0: "Echo reply"},
|
||||
ICMPType.DESTINATION_UNREACHABLE: {
|
||||
0: "Destination network unreachable",
|
||||
1: "Destination host unreachable",
|
||||
2: "Destination protocol unreachable",
|
||||
3: "Destination port unreachable",
|
||||
4: "Fragmentation required",
|
||||
5: "Source route failed",
|
||||
6: "Destination network unknown",
|
||||
7: "Destination host unknown",
|
||||
8: "Source host isolated",
|
||||
9: "Network administratively prohibited",
|
||||
10: "Host administratively prohibited",
|
||||
11: "Network unreachable for ToS",
|
||||
12: "Host unreachable for ToS",
|
||||
13: "Communication administratively prohibited",
|
||||
14: "Host Precedence Violation",
|
||||
15: "Precedence cutoff in effect",
|
||||
},
|
||||
ICMPType.REDIRECT: {
|
||||
0: "Redirect Datagram for the Network",
|
||||
1: "Redirect Datagram for the Host",
|
||||
},
|
||||
ICMPType.ECHO_REQUEST: {0: "Echo request"},
|
||||
ICMPType.ROUTER_ADVERTISEMENT: {0: "Router Advertisement"},
|
||||
ICMPType.ROUTER_SOLICITATION: {0: "Router discovery/selection/solicitation"},
|
||||
ICMPType.TIME_EXCEEDED: {0: "TTL expired in transit", 1: "Fragment reassembly time exceeded"},
|
||||
ICMPType.TIMESTAMP_REQUEST: {0: "Timestamp Request"},
|
||||
ICMPType.TIMESTAMP_REPLY: {0: "Timestamp reply"},
|
||||
}
|
||||
return icmp_code_descriptions[icmp_type].get(icmp_code)
|
||||
|
||||
|
||||
class ICMPPacket(BaseModel):
|
||||
"""Models an ICMP Packet."""
|
||||
|
||||
icmp_type: ICMPType = ICMPType.ECHO_REQUEST
|
||||
"ICMP Type."
|
||||
icmp_code: int = 0
|
||||
"ICMP Code."
|
||||
identifier: int
|
||||
"ICMP identifier (16 bits randomly generated)."
|
||||
sequence: int = 0
|
||||
"ICMP message sequence number."
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
if not kwargs.get("identifier"):
|
||||
kwargs["identifier"] = secrets.randbits(16)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
@field_validator("icmp_code") # noqa
|
||||
@classmethod
|
||||
def _icmp_type_must_have_icmp_code(cls, v: int, info: FieldValidationInfo) -> int:
|
||||
"""Validates the icmp_type and icmp_code."""
|
||||
icmp_type = info.data["icmp_type"]
|
||||
if get_icmp_type_code_description(icmp_type, v):
|
||||
return v
|
||||
msg = f"No Matching ICMP code for type:{icmp_type.name}, code:{v}"
|
||||
_LOGGER.error(msg)
|
||||
raise ValueError(msg)
|
||||
|
||||
def code_description(self) -> str:
|
||||
"""The icmp_code description."""
|
||||
description = get_icmp_type_code_description(self.icmp_type, self.icmp_code)
|
||||
if description:
|
||||
return description
|
||||
msg = f"No Matching ICMP code for type:{self.icmp_type.name}, code:{self.icmp_code}"
|
||||
_LOGGER.error(msg)
|
||||
raise ValueError(msg)
|
||||
|
||||
|
||||
class IPPacket(BaseModel):
|
||||
"""
|
||||
Represents the IP layer of a network frame.
|
||||
@@ -190,10 +83,3 @@ class IPPacket(BaseModel):
|
||||
"Time to Live (TTL) for the packet."
|
||||
precedence: Precedence = Precedence.ROUTINE
|
||||
"Precedence level for Quality of Service (default is Precedence.ROUTINE)."
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
if not isinstance(kwargs["src_ip_address"], IPv4Address):
|
||||
kwargs["src_ip_address"] = IPv4Address(kwargs["src_ip_address"])
|
||||
if not isinstance(kwargs["dst_ip_address"], IPv4Address):
|
||||
kwargs["dst_ip_address"] = IPv4Address(kwargs["dst_ip_address"])
|
||||
super().__init__(**kwargs)
|
||||
|
||||
@@ -7,6 +7,8 @@ from pydantic import BaseModel
|
||||
class Port(Enum):
|
||||
"""Enumeration of common known TCP/UDP ports used by protocols for operation of network applications."""
|
||||
|
||||
NONE = 0
|
||||
"Place holder for a non-port."
|
||||
WOL = 9
|
||||
"Wake-on-Lan (WOL) - Used to turn or awaken a computer from sleep mode by a network message."
|
||||
FTP_DATA = 20
|
||||
|
||||
@@ -23,6 +23,7 @@ class DatabaseClient(Application):
|
||||
|
||||
server_ip_address: Optional[IPv4Address] = None
|
||||
server_password: Optional[str] = None
|
||||
connected: bool = False
|
||||
_query_success_tracker: Dict[str, bool] = {}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
@@ -73,9 +74,10 @@ class DatabaseClient(Application):
|
||||
if not connection_id:
|
||||
connection_id = str(uuid4())
|
||||
|
||||
return self._connect(
|
||||
self.connected = self._connect(
|
||||
server_ip_address=self.server_ip_address, password=self.server_password, connection_id=connection_id
|
||||
)
|
||||
return self.connected
|
||||
|
||||
def _connect(
|
||||
self,
|
||||
@@ -147,6 +149,7 @@ class DatabaseClient(Application):
|
||||
self.sys_log.info(
|
||||
f"{self.name}: DatabaseClient disconnected connection {connection_id} from {self.server_ip_address}"
|
||||
)
|
||||
self.connected = False
|
||||
|
||||
def _query(self, sql: str, query_id: str, connection_id: str, is_reattempt: bool = False) -> bool:
|
||||
"""
|
||||
|
||||
@@ -21,7 +21,7 @@ class PacketCapture:
|
||||
The PCAPs are logged to: <simulation output directory>/<hostname>/<hostname>_<ip address>_pcap.log
|
||||
"""
|
||||
|
||||
def __init__(self, hostname: str, ip_address: Optional[str] = None, switch_port_number: Optional[int] = None):
|
||||
def __init__(self, hostname: str, ip_address: Optional[str] = None, interface_num: Optional[int] = None):
|
||||
"""
|
||||
Initialize the PacketCapture process.
|
||||
|
||||
@@ -32,19 +32,20 @@ class PacketCapture:
|
||||
"The hostname for which PCAP logs are being recorded."
|
||||
self.ip_address: str = ip_address
|
||||
"The IP address associated with the PCAP logs."
|
||||
self.switch_port_number = switch_port_number
|
||||
"The SwitchPort number."
|
||||
self.interface_num = interface_num
|
||||
"The interface num on the Node."
|
||||
|
||||
self.inbound_logger = None
|
||||
self.outbound_logger = None
|
||||
|
||||
self.current_episode: int = 1
|
||||
|
||||
self.setup_logger()
|
||||
self.setup_logger(outbound=False)
|
||||
self.setup_logger(outbound=True)
|
||||
|
||||
def setup_logger(self):
|
||||
def setup_logger(self, outbound: bool = False):
|
||||
"""Set up the logger configuration."""
|
||||
if not SIM_OUTPUT.save_pcap_logs:
|
||||
return
|
||||
|
||||
log_path = self._get_log_path()
|
||||
log_path = self._get_log_path(outbound)
|
||||
|
||||
file_handler = logging.FileHandler(filename=log_path)
|
||||
file_handler.setLevel(60) # Custom log level > CRITICAL to prevent any unwanted standard DEBUG-CRITICAL logs
|
||||
@@ -52,11 +53,17 @@ class PacketCapture:
|
||||
log_format = "%(message)s"
|
||||
file_handler.setFormatter(logging.Formatter(log_format))
|
||||
|
||||
self.logger = logging.getLogger(self._logger_name)
|
||||
self.logger.setLevel(60) # Custom log level > CRITICAL to prevent any unwanted standard DEBUG-CRITICAL logs
|
||||
self.logger.addHandler(file_handler)
|
||||
if outbound:
|
||||
self.outbound_logger = logging.getLogger(self._get_logger_name(outbound))
|
||||
logger = self.outbound_logger
|
||||
else:
|
||||
self.inbound_logger = logging.getLogger(self._get_logger_name(outbound))
|
||||
logger = self.inbound_logger
|
||||
|
||||
self.logger.addFilter(_JSONFilter())
|
||||
logger.setLevel(60) # Custom log level > CRITICAL to prevent any unwanted standard DEBUG-CRITICAL logs
|
||||
logger.addHandler(file_handler)
|
||||
|
||||
logger.addFilter(_JSONFilter())
|
||||
|
||||
def read(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
@@ -70,27 +77,34 @@ class PacketCapture:
|
||||
frames.append(json.loads(line.rstrip()))
|
||||
return frames
|
||||
|
||||
@property
|
||||
def _logger_name(self) -> str:
|
||||
def _get_logger_name(self, outbound: bool = False) -> str:
|
||||
"""Get PCAP the logger name."""
|
||||
if self.ip_address:
|
||||
return f"{self.hostname}_{self.ip_address}_pcap"
|
||||
if self.switch_port_number:
|
||||
return f"{self.hostname}_port-{self.switch_port_number}_pcap"
|
||||
return f"{self.hostname}_pcap"
|
||||
return f"{self.hostname}_{self.ip_address}_{'outbound' if outbound else 'inbound'}_pcap"
|
||||
if self.interface_num:
|
||||
return f"{self.hostname}_port-{self.interface_num}_{'outbound' if outbound else 'inbound'}_pcap"
|
||||
return f"{self.hostname}_{'outbound' if outbound else 'inbound'}_pcap"
|
||||
|
||||
def _get_log_path(self) -> Path:
|
||||
def _get_log_path(self, outbound: bool = False) -> Path:
|
||||
"""Get the path for the log file."""
|
||||
root = SIM_OUTPUT.path / f"episode_{self.current_episode}" / self.hostname
|
||||
root.mkdir(exist_ok=True, parents=True)
|
||||
return root / f"{self._logger_name}.log"
|
||||
return root / f"{self._get_logger_name(outbound)}.log"
|
||||
|
||||
def capture(self, frame): # noqa - I'll have a circular import and cant use if TYPE_CHECKING ;(
|
||||
def capture_inbound(self, frame): # noqa - I'll have a circular import and cant use if TYPE_CHECKING ;(
|
||||
"""
|
||||
Capture a Frame and log it.
|
||||
Capture an inbound Frame and log it.
|
||||
|
||||
:param frame: The PCAP frame to capture.
|
||||
"""
|
||||
if SIM_OUTPUT.save_pcap_logs:
|
||||
msg = frame.model_dump_json()
|
||||
self.logger.log(level=60, msg=msg) # Log at custom log level > CRITICAL
|
||||
msg = frame.model_dump_json()
|
||||
self.inbound_logger.log(level=60, msg=msg) # Log at custom log level > CRITICAL
|
||||
|
||||
def capture_outbound(self, frame): # noqa - I'll have a circular import and cant use if TYPE_CHECKING ;(
|
||||
"""
|
||||
Capture an outbound Frame and log it.
|
||||
|
||||
:param frame: The PCAP frame to capture.
|
||||
"""
|
||||
msg = frame.model_dump_json()
|
||||
self.outbound_logger.log(level=60, msg=msg) # Log at custom log level > CRITICAL
|
||||
|
||||
@@ -6,12 +6,14 @@ from typing import Any, Dict, Optional, Tuple, TYPE_CHECKING, Union
|
||||
from prettytable import MARKDOWN, PrettyTable
|
||||
|
||||
from primaite.simulator.core import SimComponent
|
||||
from primaite.simulator.network.protocols.arp import ARPPacket
|
||||
from primaite.simulator.network.protocols.icmp import ICMPPacket
|
||||
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, TCPHeader
|
||||
from primaite.simulator.network.transmission.transport_layer import Port, TCPHeader, UDPHeader
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from primaite.simulator.network.hardware.base import ARPCache
|
||||
from primaite.simulator.network.hardware.base import NetworkInterface
|
||||
from primaite.simulator.system.core.software_manager import SoftwareManager
|
||||
from primaite.simulator.system.core.sys_log import SysLog
|
||||
|
||||
@@ -73,14 +75,14 @@ class SessionManager:
|
||||
:param arp_cache: A reference to the ARP cache component.
|
||||
"""
|
||||
|
||||
def __init__(self, sys_log: SysLog, arp_cache: "ARPCache"):
|
||||
def __init__(self, sys_log: SysLog):
|
||||
self.sessions_by_key: Dict[
|
||||
Tuple[IPProtocol, IPv4Address, IPv4Address, Optional[Port], Optional[Port]], Session
|
||||
] = {}
|
||||
self.sessions_by_uuid: Dict[str, Session] = {}
|
||||
self.sys_log: SysLog = sys_log
|
||||
self.software_manager: SoftwareManager = None # Noqa
|
||||
self.arp_cache: "ARPCache" = arp_cache
|
||||
self.node: Node = None # noqa
|
||||
|
||||
def describe_state(self) -> Dict:
|
||||
"""
|
||||
@@ -138,13 +140,130 @@ class SessionManager:
|
||||
dst_port = None
|
||||
return protocol, with_ip_address, src_port, dst_port
|
||||
|
||||
def resolve_outbound_network_interface(self, dst_ip_address: IPv4Address) -> Optional["NetworkInterface"]:
|
||||
"""
|
||||
Resolves the appropriate outbound network interface for a given destination IP address.
|
||||
|
||||
This method determines the most suitable network interface for sending a packet to the specified
|
||||
destination IP address. It considers only enabled network interfaces and checks if the destination
|
||||
IP address falls within the subnet of each interface. If no suitable local network interface is found,
|
||||
the method defaults to using the network interface associated with the default gateway.
|
||||
|
||||
The search process prioritises local network interfaces based on the IP network to which they belong.
|
||||
If the destination IP address does not match any local subnet, the method assumes that the destination
|
||||
is outside the local network and hence, routes the packet through the default gateway's network interface.
|
||||
|
||||
:param dst_ip_address: The destination IP address for which the outbound interface is to be resolved.
|
||||
:type dst_ip_address: IPv4Address
|
||||
:return: The network interface through which the packet should be sent to reach the destination IP address,
|
||||
or the default gateway's network interface if the destination is not within any local subnet.
|
||||
:rtype: Optional["NetworkInterface"]
|
||||
"""
|
||||
for network_interface in self.node.network_interfaces.values():
|
||||
if dst_ip_address in network_interface.ip_network and network_interface.enabled:
|
||||
return network_interface
|
||||
return self.software_manager.arp.get_default_gateway_network_interface()
|
||||
|
||||
def resolve_outbound_transmission_details(
|
||||
self,
|
||||
dst_ip_address: Optional[Union[IPv4Address, IPv4Network]] = None,
|
||||
src_port: Optional[Port] = None,
|
||||
dst_port: Optional[Port] = None,
|
||||
protocol: Optional[IPProtocol] = None,
|
||||
session_id: Optional[str] = None,
|
||||
) -> Tuple[
|
||||
Optional["NetworkInterface"],
|
||||
Optional[str],
|
||||
IPv4Address,
|
||||
Optional[Port],
|
||||
Optional[Port],
|
||||
Optional[IPProtocol],
|
||||
bool,
|
||||
]:
|
||||
"""
|
||||
Resolves the necessary details for outbound transmission based on the provided parameters.
|
||||
|
||||
This method determines whether the payload should be broadcast or unicast based on the destination IP address
|
||||
and resolves the outbound network interface and destination MAC address accordingly.
|
||||
|
||||
The method first checks if `session_id` is provided and uses the session details if available. For broadcast
|
||||
transmissions, it finds a suitable network interface and uses a broadcast MAC address. For unicast
|
||||
transmissions, it attempts to resolve the destination MAC address using ARP and finds the appropriate
|
||||
outbound network interface. If the destination IP address is outside the local network and no specific MAC
|
||||
address is resolved, it uses the default gateway for the transmission.
|
||||
|
||||
:param dst_ip_address: The destination IP address or network. If an IPv4Network is provided, the method
|
||||
treats the transmission as a broadcast to that network. Optional.
|
||||
:type dst_ip_address: Optional[Union[IPv4Address, IPv4Network]]
|
||||
:param src_port: The source port number for the transmission. Optional.
|
||||
:type src_port: Optional[Port]
|
||||
:param dst_port: The destination port number for the transmission. Optional.
|
||||
:type dst_port: Optional[Port]
|
||||
:param protocol: The IP protocol to be used for the transmission. Optional.
|
||||
:type protocol: Optional[IPProtocol]
|
||||
:param session_id: The session ID associated with the transmission. If provided, the session details override
|
||||
other parameters. Optional.
|
||||
:type session_id: Optional[str]
|
||||
:return: A tuple containing the resolved outbound network interface, destination MAC address, destination IP
|
||||
address, source port, destination port, protocol, and a boolean indicating whether the transmission is a
|
||||
broadcast.
|
||||
:rtype: Tuple[Optional["NetworkInterface"], Optional[str], IPv4Address, Optional[Port], Optional[Port],
|
||||
Optional[IPProtocol], bool]
|
||||
"""
|
||||
if dst_ip_address and not isinstance(dst_ip_address, (IPv4Address, IPv4Network)):
|
||||
dst_ip_address = IPv4Address(dst_ip_address)
|
||||
is_broadcast = False
|
||||
outbound_network_interface = None
|
||||
dst_mac_address = None
|
||||
|
||||
# Use session details if session_id is provided
|
||||
if session_id:
|
||||
session = self.sessions_by_uuid[session_id]
|
||||
|
||||
dst_ip_address = session.with_ip_address
|
||||
protocol = session.protocol
|
||||
src_port = session.src_port
|
||||
dst_port = session.dst_port
|
||||
|
||||
# Determine if the payload is for broadcast or unicast
|
||||
|
||||
# Handle broadcast transmission
|
||||
if isinstance(dst_ip_address, IPv4Network):
|
||||
is_broadcast = True
|
||||
dst_ip_address = dst_ip_address.broadcast_address
|
||||
if dst_ip_address:
|
||||
# Find a suitable NIC for the broadcast
|
||||
for network_interface in self.node.network_interfaces.values():
|
||||
if dst_ip_address in network_interface.ip_network and network_interface.enabled:
|
||||
dst_mac_address = "ff:ff:ff:ff:ff:ff"
|
||||
outbound_network_interface = network_interface
|
||||
break
|
||||
else:
|
||||
# Resolve MAC address for unicast transmission
|
||||
use_default_gateway = True
|
||||
for network_interface in self.node.network_interfaces.values():
|
||||
if dst_ip_address in network_interface.ip_network and network_interface.enabled:
|
||||
dst_mac_address = self.software_manager.arp.get_arp_cache_mac_address(dst_ip_address)
|
||||
break
|
||||
|
||||
if dst_mac_address:
|
||||
use_default_gateway = False
|
||||
outbound_network_interface = self.software_manager.arp.get_arp_cache_network_interface(dst_ip_address)
|
||||
|
||||
if use_default_gateway:
|
||||
dst_mac_address = self.software_manager.arp.get_default_gateway_mac_address()
|
||||
outbound_network_interface = self.software_manager.arp.get_default_gateway_network_interface()
|
||||
return outbound_network_interface, dst_mac_address, dst_ip_address, src_port, dst_port, protocol, is_broadcast
|
||||
|
||||
def receive_payload_from_software_manager(
|
||||
self,
|
||||
payload: Any,
|
||||
dst_ip_address: Optional[Union[IPv4Address, IPv4Network]] = None,
|
||||
src_port: Optional[Port] = None,
|
||||
dst_port: Optional[Port] = None,
|
||||
session_id: Optional[str] = None,
|
||||
is_reattempt: bool = False,
|
||||
ip_protocol: IPProtocol = IPProtocol.TCP,
|
||||
icmp_packet: Optional[ICMPPacket] = None,
|
||||
) -> Union[Any, None]:
|
||||
"""
|
||||
Receive a payload from the SoftwareManager and send it to the appropriate NIC for transmission.
|
||||
@@ -157,74 +276,82 @@ class SessionManager:
|
||||
:param dst_ip_address: The destination IP address or network for broadcast. Optional.
|
||||
:param dst_port: The destination port for the TCP packet. Optional.
|
||||
:param session_id: The Session ID from which the payload originates. Optional.
|
||||
:param is_reattempt: Flag to indicate if this is a reattempt after an ARP request. Default is False.
|
||||
:return: The outcome of sending the frame, or None if sending was unsuccessful.
|
||||
"""
|
||||
is_broadcast = False
|
||||
outbound_nic = None
|
||||
dst_mac_address = None
|
||||
|
||||
# Use session details if session_id is provided
|
||||
if session_id:
|
||||
session = self.sessions_by_uuid[session_id]
|
||||
dst_ip_address = session.with_ip_address
|
||||
dst_port = session.dst_port
|
||||
|
||||
# Determine if the payload is for broadcast or unicast
|
||||
|
||||
# Handle broadcast transmission
|
||||
if isinstance(dst_ip_address, IPv4Network):
|
||||
is_broadcast = True
|
||||
dst_ip_address = dst_ip_address.broadcast_address
|
||||
if dst_ip_address:
|
||||
# Find a suitable NIC for the broadcast
|
||||
for nic in self.arp_cache.nics.values():
|
||||
if dst_ip_address in nic.ip_network and nic.enabled:
|
||||
dst_mac_address = "ff:ff:ff:ff:ff:ff"
|
||||
outbound_nic = nic
|
||||
else:
|
||||
# Resolve MAC address for unicast transmission
|
||||
dst_mac_address = self.arp_cache.get_arp_cache_mac_address(dst_ip_address)
|
||||
|
||||
# Resolve outbound NIC for unicast transmission
|
||||
if dst_mac_address:
|
||||
outbound_nic = self.arp_cache.get_arp_cache_nic(dst_ip_address)
|
||||
|
||||
# If MAC address not found, initiate ARP request
|
||||
if isinstance(payload, ARPPacket):
|
||||
# ARP requests need to be handles differently
|
||||
if payload.request:
|
||||
dst_mac_address = "ff:ff:ff:ff:ff:ff"
|
||||
else:
|
||||
if not is_reattempt:
|
||||
self.arp_cache.send_arp_request(dst_ip_address)
|
||||
# Reattempt payload transmission after ARP request
|
||||
return self.receive_payload_from_software_manager(
|
||||
payload=payload,
|
||||
dst_ip_address=dst_ip_address,
|
||||
dst_port=dst_port,
|
||||
session_id=session_id,
|
||||
is_reattempt=True,
|
||||
)
|
||||
else:
|
||||
# Return None if reattempt fails
|
||||
return
|
||||
dst_mac_address = payload.target_mac_addr
|
||||
outbound_network_interface = self.resolve_outbound_network_interface(payload.target_ip_address)
|
||||
is_broadcast = payload.request
|
||||
ip_protocol = IPProtocol.UDP
|
||||
else:
|
||||
vals = self.resolve_outbound_transmission_details(
|
||||
dst_ip_address=dst_ip_address,
|
||||
src_port=src_port,
|
||||
dst_port=dst_port,
|
||||
protocol=ip_protocol,
|
||||
session_id=session_id,
|
||||
)
|
||||
(
|
||||
outbound_network_interface,
|
||||
dst_mac_address,
|
||||
dst_ip_address,
|
||||
src_port,
|
||||
dst_port,
|
||||
protocol,
|
||||
is_broadcast,
|
||||
) = vals
|
||||
if protocol:
|
||||
ip_protocol = protocol
|
||||
|
||||
# Check if outbound NIC and destination MAC address are resolved
|
||||
if not outbound_nic or not dst_mac_address:
|
||||
if not outbound_network_interface or not dst_mac_address:
|
||||
return False
|
||||
|
||||
# Construct the frame for transmission
|
||||
frame = Frame(
|
||||
ethernet=EthernetHeader(src_mac_addr=outbound_nic.mac_address, dst_mac_addr=dst_mac_address),
|
||||
ip=IPPacket(
|
||||
src_ip_address=outbound_nic.ip_address,
|
||||
dst_ip_address=dst_ip_address,
|
||||
),
|
||||
tcp=TCPHeader(
|
||||
if not (src_port or dst_port):
|
||||
raise ValueError(
|
||||
"Failed to resolve src or dst port. Have you sent the port from the service or application?"
|
||||
)
|
||||
|
||||
tcp_header = None
|
||||
udp_header = None
|
||||
if ip_protocol == IPProtocol.TCP:
|
||||
tcp_header = TCPHeader(
|
||||
src_port=dst_port,
|
||||
dst_port=dst_port,
|
||||
)
|
||||
elif ip_protocol == IPProtocol.UDP:
|
||||
udp_header = UDPHeader(
|
||||
src_port=dst_port,
|
||||
dst_port=dst_port,
|
||||
)
|
||||
# TODO: Only create IP packet if not ARP
|
||||
# ip_packet = None
|
||||
# if dst_port != Port.ARP:
|
||||
# IPPacket(
|
||||
# src_ip_address=outbound_network_interface.ip_address,
|
||||
# dst_ip_address=dst_ip_address,
|
||||
# protocol=ip_protocol
|
||||
# )
|
||||
# Construct the frame for transmission
|
||||
frame = Frame(
|
||||
ethernet=EthernetHeader(src_mac_addr=outbound_network_interface.mac_address, dst_mac_addr=dst_mac_address),
|
||||
ip=IPPacket(
|
||||
src_ip_address=outbound_network_interface.ip_address,
|
||||
dst_ip_address=dst_ip_address,
|
||||
protocol=ip_protocol,
|
||||
),
|
||||
tcp=tcp_header,
|
||||
udp=udp_header,
|
||||
icmp=icmp_packet,
|
||||
payload=payload,
|
||||
)
|
||||
|
||||
# Manage session for unicast transmission
|
||||
# TODO: Only create sessions for TCP
|
||||
if not (is_broadcast and session_id):
|
||||
session_key = self._get_session_key(frame, inbound_frame=False)
|
||||
session = self.sessions_by_key.get(session_key)
|
||||
@@ -235,9 +362,9 @@ class SessionManager:
|
||||
self.sessions_by_uuid[session.uuid] = session
|
||||
|
||||
# Send the frame through the NIC
|
||||
return outbound_nic.send_frame(frame)
|
||||
return outbound_network_interface.send_frame(frame)
|
||||
|
||||
def receive_frame(self, frame: Frame):
|
||||
def receive_frame(self, frame: Frame, from_network_interface: "NetworkInterface"):
|
||||
"""
|
||||
Receive a Frame.
|
||||
|
||||
@@ -246,6 +373,7 @@ class SessionManager:
|
||||
|
||||
:param frame: The frame being received.
|
||||
"""
|
||||
# TODO: Only create sessions for TCP
|
||||
session_key = self._get_session_key(frame, inbound_frame=True)
|
||||
session: Session = self.sessions_by_key.get(session_key)
|
||||
if not session:
|
||||
@@ -253,8 +381,20 @@ class SessionManager:
|
||||
session = Session.from_session_key(session_key)
|
||||
self.sessions_by_key[session_key] = session
|
||||
self.sessions_by_uuid[session.uuid] = session
|
||||
dst_port = None
|
||||
if frame.tcp:
|
||||
dst_port = frame.tcp.dst_port
|
||||
elif frame.udp:
|
||||
dst_port = frame.udp.dst_port
|
||||
elif frame.icmp:
|
||||
dst_port = Port.NONE
|
||||
self.software_manager.receive_payload_from_session_manager(
|
||||
payload=frame.payload, port=frame.tcp.dst_port, protocol=frame.ip.protocol, session_id=session.uuid
|
||||
payload=frame.payload,
|
||||
port=dst_port,
|
||||
protocol=frame.ip.protocol,
|
||||
session_id=session.uuid,
|
||||
from_network_interface=from_network_interface,
|
||||
frame=frame,
|
||||
)
|
||||
|
||||
def show(self, markdown: bool = False):
|
||||
|
||||
@@ -4,6 +4,7 @@ from typing import Any, Dict, List, Optional, Tuple, TYPE_CHECKING, Union
|
||||
from prettytable import MARKDOWN, PrettyTable
|
||||
|
||||
from primaite.simulator.file_system.file_system import FileSystem
|
||||
from primaite.simulator.network.transmission.data_link_layer import Frame
|
||||
from primaite.simulator.network.transmission.network_layer import IPProtocol
|
||||
from primaite.simulator.network.transmission.transport_layer import Port
|
||||
from primaite.simulator.system.applications.application import Application, ApplicationOperatingState
|
||||
@@ -14,7 +15,9 @@ from primaite.simulator.system.software import IOSoftware
|
||||
if TYPE_CHECKING:
|
||||
from primaite.simulator.system.core.session_manager import SessionManager
|
||||
from primaite.simulator.system.core.sys_log import SysLog
|
||||
from primaite.simulator.network.hardware.base import Node
|
||||
from primaite.simulator.network.hardware.base import Node, NIC
|
||||
from primaite.simulator.system.services.arp.arp import ARP
|
||||
from primaite.simulator.system.services.icmp.icmp import ICMP
|
||||
|
||||
from typing import Type, TypeVar
|
||||
|
||||
@@ -22,7 +25,14 @@ IOSoftwareClass = TypeVar("IOSoftwareClass", bound=IOSoftware)
|
||||
|
||||
|
||||
class SoftwareManager:
|
||||
"""A class that manages all running Services and Applications on a Node and facilitates their communication."""
|
||||
"""
|
||||
Manages all running services and applications on a network node and facilitates their communication.
|
||||
|
||||
This class is responsible for installing, uninstalling, and managing the operational state of various network
|
||||
services and applications. It acts as a bridge between the node's session manager and its software components,
|
||||
ensuring that incoming and outgoing network payloads are correctly routed to and from the appropriate services
|
||||
or applications.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -46,17 +56,26 @@ class SoftwareManager:
|
||||
self.file_system: FileSystem = file_system
|
||||
self.dns_server: Optional[IPv4Address] = dns_server
|
||||
|
||||
@property
|
||||
def arp(self) -> "ARP":
|
||||
"""Provides access to the ARP service instance, if installed."""
|
||||
return self.software.get("ARP") # noqa
|
||||
|
||||
@property
|
||||
def icmp(self) -> "ICMP":
|
||||
"""Provides access to the ICMP service instance, if installed."""
|
||||
return self.software.get("ICMP") # noqa
|
||||
|
||||
def get_open_ports(self) -> List[Port]:
|
||||
"""
|
||||
Get a list of open ports.
|
||||
|
||||
:return: A list of all open ports on the Node.
|
||||
"""
|
||||
open_ports = [Port.ARP]
|
||||
open_ports = []
|
||||
for software in self.port_protocol_mapping.values():
|
||||
if software.operating_state in {ApplicationOperatingState.RUNNING, ServiceOperatingState.RUNNING}:
|
||||
open_ports.append(software.port)
|
||||
open_ports.sort(key=lambda port: port.value)
|
||||
return open_ports
|
||||
|
||||
def install(self, software_class: Type[IOSoftwareClass]):
|
||||
@@ -132,6 +151,7 @@ class SoftwareManager:
|
||||
payload: Any,
|
||||
dest_ip_address: Optional[Union[IPv4Address, IPv4Network]] = None,
|
||||
dest_port: Optional[Port] = None,
|
||||
ip_protocol: IPProtocol = IPProtocol.TCP,
|
||||
session_id: Optional[str] = None,
|
||||
) -> bool:
|
||||
"""
|
||||
@@ -151,10 +171,19 @@ class SoftwareManager:
|
||||
payload=payload,
|
||||
dst_ip_address=dest_ip_address,
|
||||
dst_port=dest_port,
|
||||
ip_protocol=ip_protocol,
|
||||
session_id=session_id,
|
||||
)
|
||||
|
||||
def receive_payload_from_session_manager(self, payload: Any, port: Port, protocol: IPProtocol, session_id: str):
|
||||
def receive_payload_from_session_manager(
|
||||
self,
|
||||
payload: Any,
|
||||
port: Port,
|
||||
protocol: IPProtocol,
|
||||
session_id: str,
|
||||
from_network_interface: "NIC",
|
||||
frame: Frame,
|
||||
):
|
||||
"""
|
||||
Receive a payload from the SessionManager and forward it to the corresponding service or application.
|
||||
|
||||
@@ -163,7 +192,9 @@ class SoftwareManager:
|
||||
"""
|
||||
receiver: Optional[Union[Service, Application]] = self.port_protocol_mapping.get((port, protocol), None)
|
||||
if receiver:
|
||||
receiver.receive(payload=payload, session_id=session_id)
|
||||
receiver.receive(
|
||||
payload=payload, session_id=session_id, from_network_interface=from_network_interface, frame=frame
|
||||
)
|
||||
else:
|
||||
self.sys_log.error(f"No service or application found for port {port} and protocol {protocol}")
|
||||
pass
|
||||
@@ -174,7 +205,7 @@ class SoftwareManager:
|
||||
|
||||
:param markdown: If True, outputs the table in markdown format. Default is False.
|
||||
"""
|
||||
table = PrettyTable(["Name", "Type", "Operating State", "Health State", "Port"])
|
||||
table = PrettyTable(["Name", "Type", "Operating State", "Health State", "Port", "Protocol"])
|
||||
if markdown:
|
||||
table.set_style(MARKDOWN)
|
||||
table.align = "l"
|
||||
@@ -187,7 +218,8 @@ class SoftwareManager:
|
||||
software_type,
|
||||
software.operating_state.name,
|
||||
software.health_state_actual.name,
|
||||
software.port.value,
|
||||
software.port.value if software.port != Port.NONE else None,
|
||||
software.protocol.value,
|
||||
]
|
||||
)
|
||||
print(table)
|
||||
|
||||
@@ -88,47 +88,62 @@ class SysLog:
|
||||
root.mkdir(exist_ok=True, parents=True)
|
||||
return root / f"{self.hostname}_sys.log"
|
||||
|
||||
def debug(self, msg: str):
|
||||
def debug(self, msg: str, to_terminal: bool = False):
|
||||
"""
|
||||
Logs a message with the DEBUG level.
|
||||
|
||||
:param msg: The message to be logged.
|
||||
:param to_terminal: If True, prints to the terminal too.
|
||||
"""
|
||||
if SIM_OUTPUT.save_sys_logs:
|
||||
self.logger.debug(msg)
|
||||
if to_terminal:
|
||||
print(msg)
|
||||
|
||||
def info(self, msg: str):
|
||||
def info(self, msg: str, to_terminal: bool = False):
|
||||
"""
|
||||
Logs a message with the INFO level.
|
||||
|
||||
:param msg: The message to be logged.
|
||||
:param to_terminal: If True, prints to the terminal too.
|
||||
"""
|
||||
if SIM_OUTPUT.save_sys_logs:
|
||||
self.logger.info(msg)
|
||||
if to_terminal:
|
||||
print(msg)
|
||||
|
||||
def warning(self, msg: str):
|
||||
def warning(self, msg: str, to_terminal: bool = False):
|
||||
"""
|
||||
Logs a message with the WARNING level.
|
||||
|
||||
:param msg: The message to be logged.
|
||||
:param to_terminal: If True, prints to the terminal too.
|
||||
"""
|
||||
if SIM_OUTPUT.save_sys_logs:
|
||||
self.logger.warning(msg)
|
||||
if to_terminal:
|
||||
print(msg)
|
||||
|
||||
def error(self, msg: str):
|
||||
def error(self, msg: str, to_terminal: bool = False):
|
||||
"""
|
||||
Logs a message with the ERROR level.
|
||||
|
||||
:param msg: The message to be logged.
|
||||
:param to_terminal: If True, prints to the terminal too.
|
||||
"""
|
||||
if SIM_OUTPUT.save_sys_logs:
|
||||
self.logger.error(msg)
|
||||
if to_terminal:
|
||||
print(msg)
|
||||
|
||||
def critical(self, msg: str):
|
||||
def critical(self, msg: str, to_terminal: bool = False):
|
||||
"""
|
||||
Logs a message with the CRITICAL level.
|
||||
|
||||
:param msg: The message to be logged.
|
||||
:param to_terminal: If True, prints to the terminal too.
|
||||
"""
|
||||
if SIM_OUTPUT.save_sys_logs:
|
||||
self.logger.critical(msg)
|
||||
if to_terminal:
|
||||
print(msg)
|
||||
|
||||
233
src/primaite/simulator/system/services/arp/arp.py
Normal file
233
src/primaite/simulator/system/services/arp/arp.py
Normal file
@@ -0,0 +1,233 @@
|
||||
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 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.error(
|
||||
"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.error(
|
||||
"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
|
||||
194
src/primaite/simulator/system/services/icmp/icmp.py
Normal file
194
src/primaite/simulator/system/services/icmp/icmp.py
Normal file
@@ -0,0 +1,194 @@
|
||||
import secrets
|
||||
from ipaddress import IPv4Address
|
||||
from typing import Any, Dict, Optional, Tuple, Union
|
||||
|
||||
from primaite import getLogger
|
||||
from primaite.simulator.network.hardware.base import NetworkInterface
|
||||
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
|
||||
from primaite.simulator.network.transmission.transport_layer import Port
|
||||
from primaite.simulator.system.services.service import Service
|
||||
|
||||
_LOGGER = getLogger(__name__)
|
||||
|
||||
|
||||
class ICMP(Service):
|
||||
"""
|
||||
The Internet Control Message Protocol (ICMP) service.
|
||||
|
||||
Enables the sending and receiving of ICMP messages such as echo requests and replies. This is typically used for
|
||||
network diagnostics, notably the ping command.
|
||||
"""
|
||||
|
||||
request_replies: Dict = {}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
kwargs["name"] = "ICMP"
|
||||
kwargs["port"] = Port.NONE
|
||||
kwargs["protocol"] = IPProtocol.ICMP
|
||||
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.
|
||||
"""
|
||||
return super().describe_state()
|
||||
|
||||
def clear(self):
|
||||
"""
|
||||
Clears the ICMP request and reply tracker.
|
||||
|
||||
This is typically used to reset the state of the service, removing all tracked ICMP echo requests and their
|
||||
corresponding replies.
|
||||
"""
|
||||
self.request_replies.clear()
|
||||
|
||||
def ping(self, target_ip_address: Union[IPv4Address, str], pings: int = 4) -> bool:
|
||||
"""
|
||||
Pings a target IP address by sending an ICMP echo request and waiting for a reply.
|
||||
|
||||
:param target_ip_address: The IP address to be pinged.
|
||||
:param pings: The number of echo requests to send. Defaults to 4.
|
||||
:return: True if the ping was successful (i.e., if a reply was received for every request sent), otherwise
|
||||
False.
|
||||
"""
|
||||
if not self._can_perform_action():
|
||||
return False
|
||||
if target_ip_address.is_loopback:
|
||||
self.sys_log.info("Pinging loopback address")
|
||||
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:
|
||||
sequence, identifier = self._send_icmp_echo_request(target_ip_address, sequence, identifier, pings)
|
||||
request_replies = self.software_manager.icmp.request_replies.get(identifier)
|
||||
passed = request_replies == pings
|
||||
if request_replies:
|
||||
self.software_manager.icmp.request_replies.pop(identifier)
|
||||
else:
|
||||
request_replies = 0
|
||||
output = (
|
||||
f"Ping statistics for {target_ip_address}: "
|
||||
f"Packets: Sent = {pings}, "
|
||||
f"Received = {request_replies}, "
|
||||
f"Lost = {pings - request_replies} ({(pings - request_replies) / pings * 100}% loss)"
|
||||
)
|
||||
self.sys_log.info(output, to_terminal=True)
|
||||
|
||||
return passed
|
||||
|
||||
def _send_icmp_echo_request(
|
||||
self, target_ip_address: IPv4Address, sequence: int = 0, identifier: Optional[int] = None, pings: int = 4
|
||||
) -> Tuple[int, Union[int, None]]:
|
||||
"""
|
||||
Sends an ICMP echo request to a specified target IP address.
|
||||
|
||||
:param target_ip_address: The target IP address for the echo request.
|
||||
:param sequence: The sequence number of the echo request.
|
||||
:param identifier: The identifier for the ICMP packet. If None, a default identifier is used.
|
||||
:param pings: The number of pings to send. Defaults to 4.
|
||||
:return: A tuple containing the next sequence number and the identifier.
|
||||
"""
|
||||
network_interface = self.software_manager.session_manager.resolve_outbound_network_interface(target_ip_address)
|
||||
|
||||
if not network_interface:
|
||||
self.sys_log.error(
|
||||
"Cannot send ICMP echo request as there is no outbound Network Interface to use. Try configuring the "
|
||||
"default gateway."
|
||||
)
|
||||
return pings, None
|
||||
|
||||
sequence += 1
|
||||
|
||||
icmp_packet = ICMPPacket(identifier=identifier, sequence=sequence)
|
||||
payload = secrets.token_urlsafe(int(32 / 1.3)) # Standard ICMP 32 bytes size
|
||||
|
||||
self.software_manager.session_manager.receive_payload_from_software_manager(
|
||||
payload=payload,
|
||||
dst_ip_address=target_ip_address,
|
||||
dst_port=self.port,
|
||||
ip_protocol=self.protocol,
|
||||
icmp_packet=icmp_packet,
|
||||
)
|
||||
return sequence, icmp_packet.identifier
|
||||
|
||||
def _process_icmp_echo_request(self, frame: Frame, from_network_interface: NetworkInterface):
|
||||
"""
|
||||
Processes an ICMP echo request received by the service.
|
||||
|
||||
:param frame: The network frame containing the ICMP echo request.
|
||||
"""
|
||||
if frame.ip.dst_ip_address != from_network_interface.ip_address:
|
||||
return
|
||||
self.sys_log.info(f"Received echo request from {frame.ip.src_ip_address}")
|
||||
|
||||
network_interface = self.software_manager.session_manager.resolve_outbound_network_interface(
|
||||
frame.ip.src_ip_address
|
||||
)
|
||||
|
||||
if not network_interface:
|
||||
self.sys_log.error(
|
||||
"Cannot send ICMP echo reply as there is no outbound Network Interface to use. Try configuring the "
|
||||
"default gateway."
|
||||
)
|
||||
return
|
||||
|
||||
icmp_packet = ICMPPacket(
|
||||
icmp_type=ICMPType.ECHO_REPLY,
|
||||
icmp_code=0,
|
||||
identifier=frame.icmp.identifier,
|
||||
sequence=frame.icmp.sequence + 1,
|
||||
)
|
||||
payload = secrets.token_urlsafe(int(32 / 1.3)) # Standard ICMP 32 bytes size
|
||||
self.sys_log.info(f"Sending echo reply to {frame.ip.dst_ip_address}")
|
||||
|
||||
self.software_manager.session_manager.receive_payload_from_software_manager(
|
||||
payload=payload,
|
||||
dst_ip_address=frame.ip.src_ip_address,
|
||||
dst_port=self.port,
|
||||
ip_protocol=self.protocol,
|
||||
icmp_packet=icmp_packet,
|
||||
)
|
||||
|
||||
def _process_icmp_echo_reply(self, frame: Frame):
|
||||
"""
|
||||
Processes an ICMP echo reply received by the service, logging the reply details.
|
||||
|
||||
:param frame: The network frame containing the ICMP echo reply.
|
||||
"""
|
||||
time = frame.transmission_duration()
|
||||
time_str = f"{time}ms" if time > 0 else "<1ms"
|
||||
self.sys_log.info(
|
||||
f"Reply from {frame.ip.src_ip_address}: "
|
||||
f"bytes={len(frame.payload)}, "
|
||||
f"time={time_str}, "
|
||||
f"TTL={frame.ip.ttl}",
|
||||
to_terminal=True,
|
||||
)
|
||||
if not self.request_replies.get(frame.icmp.identifier):
|
||||
self.request_replies[frame.icmp.identifier] = 0
|
||||
self.request_replies[frame.icmp.identifier] += 1
|
||||
|
||||
def receive(self, payload: Any, session_id: str, **kwargs) -> bool:
|
||||
"""
|
||||
Processes received data, handling ICMP echo requests and replies.
|
||||
|
||||
: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.
|
||||
"""
|
||||
frame: Frame = kwargs["frame"]
|
||||
from_network_interface = kwargs["from_network_interface"]
|
||||
|
||||
if not frame.icmp:
|
||||
return False
|
||||
|
||||
if frame.icmp.icmp_type == ICMPType.ECHO_REQUEST:
|
||||
self._process_icmp_echo_request(frame, from_network_interface)
|
||||
elif frame.icmp.icmp_type == ICMPType.ECHO_REPLY:
|
||||
self._process_icmp_echo_reply(frame)
|
||||
return True
|
||||
90
src/primaite/simulator/system/services/icmp/router_icmp.py
Normal file
90
src/primaite/simulator/system/services/icmp/router_icmp.py
Normal file
@@ -0,0 +1,90 @@
|
||||
# class RouterICMP(ICMP):
|
||||
# """
|
||||
# A class to represent a router's Internet Control Message Protocol (ICMP) handler.
|
||||
#
|
||||
# :param sys_log: System log for logging network events and errors.
|
||||
# :type sys_log: SysLog
|
||||
# :param arp_cache: The ARP cache for resolving MAC addresses.
|
||||
# :type arp_cache: ARPCache
|
||||
# :param router: The router to which this ICMP handler belongs.
|
||||
# :type router: Router
|
||||
# """
|
||||
#
|
||||
# router: Router
|
||||
#
|
||||
# def __init__(self, sys_log: SysLog, arp_cache: ARPCache, router: Router):
|
||||
# super().__init__(sys_log, arp_cache)
|
||||
# self.router = router
|
||||
#
|
||||
# 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_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 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_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=network_interface.ip_address,
|
||||
# dst_ip_address=frame.ip.src_ip_address,
|
||||
# protocol=IPProtocol.ICMP,
|
||||
# )
|
||||
# # Data Link Layer
|
||||
# ethernet_header = EthernetHeader(
|
||||
# src_mac_addr=src_nic.mac_address, dst_mac_addr=target_mac_address
|
||||
# )
|
||||
# icmp_reply_packet = ICMPPacket(
|
||||
# icmp_type=ICMPType.ECHO_REPLY,
|
||||
# icmp_code=0,
|
||||
# identifier=frame.icmp.identifier,
|
||||
# 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,
|
||||
# )
|
||||
# self.sys_log.info(f"Sending echo reply to {frame.ip.dst_ip_address}")
|
||||
#
|
||||
# src_nic.send_frame(frame)
|
||||
# return
|
||||
#
|
||||
# # Route the frame
|
||||
# self.router.process_frame(frame, from_network_interface)
|
||||
#
|
||||
# elif frame.icmp.icmp_type == ICMPType.ECHO_REPLY:
|
||||
# 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(
|
||||
# f"Reply from {frame.ip.src_ip_address}: "
|
||||
# f"bytes={len(frame.payload)}, "
|
||||
# f"time={time_str}, "
|
||||
# f"TTL={frame.ip.ttl}"
|
||||
# )
|
||||
# if not self.request_replies.get(frame.icmp.identifier):
|
||||
# self.request_replies[frame.icmp.identifier] = 0
|
||||
# self.request_replies[frame.icmp.identifier] += 1
|
||||
#
|
||||
# return
|
||||
# # Route the frame
|
||||
# self.router.process_frame(frame, from_network_interface)
|
||||
@@ -108,9 +108,14 @@ class NTPClient(Service):
|
||||
|
||||
def request_time(self) -> None:
|
||||
"""Send request to ntp_server."""
|
||||
ntp_server_packet = NTPPacket()
|
||||
|
||||
self.send(payload=ntp_server_packet, dest_ip_address=self.ntp_server)
|
||||
if self.ntp_server:
|
||||
self.software_manager.session_manager.receive_payload_from_software_manager(
|
||||
payload=NTPPacket(),
|
||||
dst_ip_address=self.ntp_server,
|
||||
src_port=self.port,
|
||||
dst_port=self.port,
|
||||
ip_protocol=self.protocol,
|
||||
)
|
||||
|
||||
def apply_timestep(self, timestep: int) -> None:
|
||||
"""
|
||||
|
||||
@@ -69,5 +69,7 @@ class NTPServer(Service):
|
||||
time = datetime.now()
|
||||
payload = payload.generate_reply(time)
|
||||
# send reply
|
||||
self.send(payload, session_id)
|
||||
self.software_manager.session_manager.receive_payload_from_software_manager(
|
||||
payload=payload, src_port=self.port, dst_port=self.port, ip_protocol=self.protocol, session_id=session_id
|
||||
)
|
||||
return True
|
||||
|
||||
@@ -8,6 +8,7 @@ from typing import Any, Dict, Optional, Union
|
||||
from primaite.simulator.core import _LOGGER, RequestManager, RequestType, SimComponent
|
||||
from primaite.simulator.file_system.file_system import FileSystem, Folder
|
||||
from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState
|
||||
from primaite.simulator.network.transmission.network_layer import IPProtocol
|
||||
from primaite.simulator.network.transmission.transport_layer import Port
|
||||
from primaite.simulator.system.core.session_manager import Session
|
||||
from primaite.simulator.system.core.sys_log import SysLog
|
||||
@@ -242,6 +243,8 @@ class IOSoftware(Software):
|
||||
"Indicates if the software uses UDP protocol for communication. Default is True."
|
||||
port: Port
|
||||
"The port to which the software is connected."
|
||||
protocol: IPProtocol
|
||||
"The IP Protocol the Software operates on."
|
||||
_connections: Dict[str, Dict] = {}
|
||||
"Active connections."
|
||||
|
||||
@@ -353,6 +356,7 @@ class IOSoftware(Software):
|
||||
session_id: Optional[str] = None,
|
||||
dest_ip_address: Optional[Union[IPv4Address, IPv4Network]] = None,
|
||||
dest_port: Optional[Port] = None,
|
||||
ip_protocol: IPProtocol = IPProtocol.TCP,
|
||||
**kwargs,
|
||||
) -> bool:
|
||||
"""
|
||||
@@ -372,7 +376,11 @@ class IOSoftware(Software):
|
||||
return False
|
||||
|
||||
return self.software_manager.send_payload_to_session_manager(
|
||||
payload=payload, dest_ip_address=dest_ip_address, dest_port=dest_port, session_id=session_id
|
||||
payload=payload,
|
||||
dest_ip_address=dest_ip_address,
|
||||
dest_port=dest_port,
|
||||
ip_protocol=ip_protocol,
|
||||
session_id=session_id,
|
||||
)
|
||||
|
||||
@abstractmethod
|
||||
|
||||
38
src/primaite/utils/validators.py
Normal file
38
src/primaite/utils/validators.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from ipaddress import IPv4Address
|
||||
from typing import Any, Final
|
||||
|
||||
from pydantic import BeforeValidator
|
||||
from typing_extensions import Annotated
|
||||
|
||||
|
||||
def ipv4_validator(v: Any) -> IPv4Address:
|
||||
"""
|
||||
Validate the input and ensure it can be converted to an IPv4Address instance.
|
||||
|
||||
This function takes an input `v`, and if it's not already an instance of IPv4Address, it tries to convert it to one.
|
||||
If the conversion is successful, the IPv4Address instance is returned. This is useful for ensuring that any input
|
||||
data is strictly in the format of an IPv4 address.
|
||||
|
||||
:param v: The input value that needs to be validated or converted to IPv4Address.
|
||||
:return: An instance of IPv4Address.
|
||||
:raises ValueError: If `v` is not a valid IPv4 address and cannot be converted to an instance of IPv4Address.
|
||||
"""
|
||||
if isinstance(v, IPv4Address):
|
||||
return v
|
||||
|
||||
return IPv4Address(v)
|
||||
|
||||
|
||||
# Define a custom type IPV4Address using the typing_extensions.Annotated.
|
||||
# Annotated is used to attach metadata to type hints. In this case, it's used to associate the ipv4_validator
|
||||
# with the IPv4Address type, ensuring that any usage of IPV4Address undergoes validation before assignment.
|
||||
IPV4Address: Final[Annotated] = Annotated[IPv4Address, BeforeValidator(ipv4_validator)]
|
||||
"""
|
||||
IPv4Address with with IPv4Address with with pre-validation and auto-conversion from str using ipv4_validator..
|
||||
|
||||
This type is essentially an IPv4Address from the standard library's ipaddress module,
|
||||
but with added validation logic. If you use this custom type, the ipv4_validator function
|
||||
will automatically check and convert the input value to an instance of IPv4Address before
|
||||
any Pydantic model uses it. This ensures that any field marked with this type is not just
|
||||
an IPv4Address in form, but also valid according to the rules defined in ipv4_validator.
|
||||
"""
|
||||
@@ -629,7 +629,7 @@ simulation:
|
||||
subnet_mask: 255.255.255.0
|
||||
default_gateway: 192.168.1.1
|
||||
dns_server: 192.168.1.10
|
||||
nics:
|
||||
network_interfaces:
|
||||
2: # unfortunately this number is currently meaningless, they're just added in order and take up the next available slot
|
||||
ip_address: 192.168.10.110
|
||||
subnet_mask: 255.255.255.0
|
||||
|
||||
@@ -633,7 +633,7 @@ simulation:
|
||||
subnet_mask: 255.255.255.0
|
||||
default_gateway: 192.168.1.1
|
||||
dns_server: 192.168.1.10
|
||||
nics:
|
||||
network_interfaces:
|
||||
2: # unfortunately this number is currently meaningless, they're just added in order and take up the next available slot
|
||||
ip_address: 192.168.10.110
|
||||
subnet_mask: 255.255.255.0
|
||||
|
||||
@@ -1083,7 +1083,7 @@ simulation:
|
||||
subnet_mask: 255.255.255.0
|
||||
default_gateway: 192.168.1.1
|
||||
dns_server: 192.168.1.10
|
||||
nics:
|
||||
network_interfaces:
|
||||
2: # unfortunately this number is currently meaningless, they're just added in order and take up the next available slot
|
||||
ip_address: 192.168.10.110
|
||||
subnet_mask: 255.255.255.0
|
||||
|
||||
@@ -639,7 +639,7 @@ simulation:
|
||||
subnet_mask: 255.255.255.0
|
||||
default_gateway: 192.168.1.1
|
||||
dns_server: 192.168.1.10
|
||||
nics:
|
||||
network_interfaces:
|
||||
2: # unfortunately this number is currently meaningless, they're just added in order and take up the next available slot
|
||||
ip_address: 192.168.10.110
|
||||
subnet_mask: 255.255.255.0
|
||||
|
||||
@@ -640,7 +640,7 @@ simulation:
|
||||
subnet_mask: 255.255.255.0
|
||||
default_gateway: 192.168.1.1
|
||||
dns_server: 192.168.1.10
|
||||
nics:
|
||||
network_interfaces:
|
||||
2: # unfortunately this number is currently meaningless, they're just added in order and take up the next available slot
|
||||
ip_address: 192.168.10.110
|
||||
subnet_mask: 255.255.255.0
|
||||
|
||||
@@ -5,22 +5,19 @@ from typing import Any, Dict, Tuple, Union
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
from primaite import getLogger
|
||||
from primaite import getLogger, PRIMAITE_PATHS
|
||||
from primaite.game.agent.actions import ActionManager
|
||||
from primaite.game.agent.interface import AbstractAgent
|
||||
from primaite.game.agent.observations import ICSObservation, ObservationManager
|
||||
from primaite.game.agent.rewards import RewardFunction
|
||||
from primaite.game.game import PrimaiteGame
|
||||
from primaite.session.session import PrimaiteSession
|
||||
|
||||
# from primaite.environment.primaite_env import Primaite
|
||||
# from primaite.primaite_session import PrimaiteSession
|
||||
from primaite.simulator.file_system.file_system import FileSystem
|
||||
from primaite.simulator.network.container import Network
|
||||
from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState
|
||||
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.server import Server
|
||||
from primaite.simulator.network.hardware.nodes.switch import Switch
|
||||
from primaite.simulator.network.hardware.nodes.host.computer import Computer
|
||||
from primaite.simulator.network.hardware.nodes.host.server import Server
|
||||
from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router
|
||||
from primaite.simulator.network.hardware.nodes.network.switch import Switch
|
||||
from primaite.simulator.network.networks import arcd_uc2_network
|
||||
from primaite.simulator.network.transmission.network_layer import IPProtocol
|
||||
from primaite.simulator.network.transmission.transport_layer import Port
|
||||
@@ -39,12 +36,6 @@ ACTION_SPACE_NODE_ACTION_VALUES = 1
|
||||
|
||||
_LOGGER = getLogger(__name__)
|
||||
|
||||
from primaite import PRIMAITE_PATHS
|
||||
|
||||
# PrimAITE v3 stuff
|
||||
from primaite.simulator.file_system.file_system import FileSystem
|
||||
from primaite.simulator.network.hardware.base import Link, Node
|
||||
|
||||
|
||||
class TestService(Service):
|
||||
"""Test Service class"""
|
||||
@@ -106,7 +97,9 @@ def application_class():
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def file_system() -> FileSystem:
|
||||
return Node(hostname="fs_node").file_system
|
||||
computer = Computer(hostname="fs_node", ip_address="192.168.1.2", subnet_mask="255.255.255.0", start_up_duration=0)
|
||||
computer.power_on()
|
||||
return computer.file_system
|
||||
|
||||
|
||||
# PrimAITE v2 stuff
|
||||
@@ -143,31 +136,72 @@ def temp_primaite_session(request, monkeypatch) -> TempPrimaiteSession:
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def client_server() -> Tuple[Computer, Server]:
|
||||
network = Network()
|
||||
|
||||
# Create Computer
|
||||
computer: Computer = Computer(
|
||||
hostname="test_computer",
|
||||
ip_address="192.168.0.1",
|
||||
computer = Computer(
|
||||
hostname="computer",
|
||||
ip_address="192.168.1.2",
|
||||
subnet_mask="255.255.255.0",
|
||||
default_gateway="192.168.1.1",
|
||||
operating_state=NodeOperatingState.ON,
|
||||
start_up_duration=0,
|
||||
)
|
||||
computer.power_on()
|
||||
|
||||
# Create Server
|
||||
server = Server(
|
||||
hostname="server", ip_address="192.168.0.2", subnet_mask="255.255.255.0", operating_state=NodeOperatingState.ON
|
||||
hostname="server",
|
||||
ip_address="192.168.1.3",
|
||||
subnet_mask="255.255.255.0",
|
||||
default_gateway="192.168.1.1",
|
||||
start_up_duration=0,
|
||||
)
|
||||
server.power_on()
|
||||
|
||||
# Connect Computer and Server
|
||||
computer_nic = computer.nics[next(iter(computer.nics))]
|
||||
server_nic = server.nics[next(iter(server.nics))]
|
||||
link = Link(endpoint_a=computer_nic, endpoint_b=server_nic)
|
||||
network.connect(computer.network_interface[1], server.network_interface[1])
|
||||
|
||||
# Should be linked
|
||||
assert link.is_up
|
||||
assert next(iter(network.links.values())).is_up
|
||||
|
||||
return computer, server
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def client_switch_server() -> Tuple[Computer, Switch, Server]:
|
||||
network = Network()
|
||||
|
||||
# Create Computer
|
||||
computer = Computer(
|
||||
hostname="computer",
|
||||
ip_address="192.168.1.2",
|
||||
subnet_mask="255.255.255.0",
|
||||
default_gateway="192.168.1.1",
|
||||
start_up_duration=0,
|
||||
)
|
||||
computer.power_on()
|
||||
|
||||
# Create Server
|
||||
server = Server(
|
||||
hostname="server",
|
||||
ip_address="192.168.1.3",
|
||||
subnet_mask="255.255.255.0",
|
||||
default_gateway="192.168.1.1",
|
||||
start_up_duration=0,
|
||||
)
|
||||
server.power_on()
|
||||
|
||||
switch = Switch(hostname="switch", start_up_duration=0)
|
||||
switch.power_on()
|
||||
|
||||
network.connect(endpoint_a=computer.network_interface[1], endpoint_b=switch.network_interface[1])
|
||||
network.connect(endpoint_a=server.network_interface[1], endpoint_b=switch.network_interface[2])
|
||||
|
||||
assert all(link.is_up for link in network.links.values())
|
||||
|
||||
return computer, switch, server
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def example_network() -> Network:
|
||||
"""
|
||||
@@ -187,18 +221,22 @@ def example_network() -> Network:
|
||||
network = Network()
|
||||
|
||||
# Router 1
|
||||
router_1 = Router(hostname="router_1", num_ports=5, operating_state=NodeOperatingState.ON)
|
||||
router_1 = Router(hostname="router_1", start_up_duration=0)
|
||||
router_1.power_on()
|
||||
router_1.configure_port(port=1, ip_address="192.168.1.1", subnet_mask="255.255.255.0")
|
||||
router_1.configure_port(port=2, ip_address="192.168.10.1", subnet_mask="255.255.255.0")
|
||||
|
||||
# Switch 1
|
||||
switch_1 = Switch(hostname="switch_1", num_ports=8, operating_state=NodeOperatingState.ON)
|
||||
network.connect(endpoint_a=router_1.ethernet_ports[1], endpoint_b=switch_1.switch_ports[8])
|
||||
switch_1 = Switch(hostname="switch_1", num_ports=8, start_up_duration=0)
|
||||
switch_1.power_on()
|
||||
|
||||
network.connect(endpoint_a=router_1.network_interface[1], endpoint_b=switch_1.network_interface[8])
|
||||
router_1.enable_port(1)
|
||||
|
||||
# Switch 2
|
||||
switch_2 = Switch(hostname="switch_2", num_ports=8, operating_state=NodeOperatingState.ON)
|
||||
network.connect(endpoint_a=router_1.ethernet_ports[2], endpoint_b=switch_2.switch_ports[8])
|
||||
switch_2 = Switch(hostname="switch_2", num_ports=8, start_up_duration=0)
|
||||
switch_2.power_on()
|
||||
network.connect(endpoint_a=router_1.network_interface[2], endpoint_b=switch_2.network_interface[8])
|
||||
router_1.enable_port(2)
|
||||
|
||||
# Client 1
|
||||
@@ -207,9 +245,10 @@ def example_network() -> Network:
|
||||
ip_address="192.168.10.21",
|
||||
subnet_mask="255.255.255.0",
|
||||
default_gateway="192.168.10.1",
|
||||
operating_state=NodeOperatingState.ON,
|
||||
start_up_duration=0,
|
||||
)
|
||||
network.connect(endpoint_b=client_1.ethernet_port[1], endpoint_a=switch_2.switch_ports[1])
|
||||
client_1.power_on()
|
||||
network.connect(endpoint_b=client_1.network_interface[1], endpoint_a=switch_2.network_interface[1])
|
||||
|
||||
# Client 2
|
||||
client_2 = Computer(
|
||||
@@ -217,34 +256,38 @@ def example_network() -> Network:
|
||||
ip_address="192.168.10.22",
|
||||
subnet_mask="255.255.255.0",
|
||||
default_gateway="192.168.10.1",
|
||||
operating_state=NodeOperatingState.ON,
|
||||
start_up_duration=0,
|
||||
)
|
||||
network.connect(endpoint_b=client_2.ethernet_port[1], endpoint_a=switch_2.switch_ports[2])
|
||||
client_2.power_on()
|
||||
network.connect(endpoint_b=client_2.network_interface[1], endpoint_a=switch_2.network_interface[2])
|
||||
|
||||
# Domain Controller
|
||||
# Server 1
|
||||
server_1 = Server(
|
||||
hostname="server_1",
|
||||
ip_address="192.168.1.10",
|
||||
subnet_mask="255.255.255.0",
|
||||
default_gateway="192.168.1.1",
|
||||
operating_state=NodeOperatingState.ON,
|
||||
start_up_duration=0,
|
||||
)
|
||||
server_1.power_on()
|
||||
network.connect(endpoint_b=server_1.network_interface[1], endpoint_a=switch_1.network_interface[1])
|
||||
|
||||
network.connect(endpoint_b=server_1.ethernet_port[1], endpoint_a=switch_1.switch_ports[1])
|
||||
|
||||
# Database Server
|
||||
# DServer 2
|
||||
server_2 = Server(
|
||||
hostname="server_2",
|
||||
ip_address="192.168.1.14",
|
||||
subnet_mask="255.255.255.0",
|
||||
default_gateway="192.168.1.1",
|
||||
operating_state=NodeOperatingState.ON,
|
||||
start_up_duration=0,
|
||||
)
|
||||
network.connect(endpoint_b=server_2.ethernet_port[1], endpoint_a=switch_1.switch_ports[2])
|
||||
server_2.power_on()
|
||||
network.connect(endpoint_b=server_2.network_interface[1], endpoint_a=switch_1.network_interface[2])
|
||||
|
||||
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)
|
||||
|
||||
assert all(link.is_up for link in network.links.values())
|
||||
|
||||
return network
|
||||
|
||||
|
||||
@@ -283,19 +326,19 @@ def install_stuff_to_sim(sim: Simulation):
|
||||
|
||||
# 1: Set up network hardware
|
||||
# 1.1: Configure the router
|
||||
router = Router(hostname="router", num_ports=3, operating_state=NodeOperatingState.ON)
|
||||
router = Router(hostname="router", num_ports=3, start_up_duration=0)
|
||||
router.power_on()
|
||||
router.configure_port(port=1, ip_address="10.0.1.1", subnet_mask="255.255.255.0")
|
||||
router.configure_port(port=2, ip_address="10.0.2.1", subnet_mask="255.255.255.0")
|
||||
|
||||
# 1.2: Create and connect switches
|
||||
switch_1 = Switch(hostname="switch_1", num_ports=6, operating_state=NodeOperatingState.ON)
|
||||
switch_1 = Switch(hostname="switch_1", num_ports=6, start_up_duration=0)
|
||||
switch_1.power_on()
|
||||
network.connect(endpoint_a=router.ethernet_ports[1], endpoint_b=switch_1.switch_ports[6])
|
||||
network.connect(endpoint_a=router.network_interface[1], endpoint_b=switch_1.network_interface[6])
|
||||
router.enable_port(1)
|
||||
switch_2 = Switch(hostname="switch_2", num_ports=6, operating_state=NodeOperatingState.ON)
|
||||
switch_2 = Switch(hostname="switch_2", num_ports=6, start_up_duration=0)
|
||||
switch_2.power_on()
|
||||
network.connect(endpoint_a=router.ethernet_ports[2], endpoint_b=switch_2.switch_ports[6])
|
||||
network.connect(endpoint_a=router.network_interface[2], endpoint_b=switch_2.network_interface[6])
|
||||
router.enable_port(2)
|
||||
|
||||
# 1.3: Create and connect computer
|
||||
@@ -304,12 +347,12 @@ def install_stuff_to_sim(sim: Simulation):
|
||||
ip_address="10.0.1.2",
|
||||
subnet_mask="255.255.255.0",
|
||||
default_gateway="10.0.1.1",
|
||||
operating_state=NodeOperatingState.ON,
|
||||
start_up_duration=0,
|
||||
)
|
||||
client_1.power_on()
|
||||
network.connect(
|
||||
endpoint_a=client_1.ethernet_port[1],
|
||||
endpoint_b=switch_1.switch_ports[1],
|
||||
endpoint_a=client_1.network_interface[1],
|
||||
endpoint_b=switch_1.network_interface[1],
|
||||
)
|
||||
|
||||
# 1.4: Create and connect servers
|
||||
@@ -318,20 +361,20 @@ def install_stuff_to_sim(sim: Simulation):
|
||||
ip_address="10.0.2.2",
|
||||
subnet_mask="255.255.255.0",
|
||||
default_gateway="10.0.2.1",
|
||||
operating_state=NodeOperatingState.ON,
|
||||
start_up_duration=0,
|
||||
)
|
||||
server_1.power_on()
|
||||
network.connect(endpoint_a=server_1.ethernet_port[1], endpoint_b=switch_2.switch_ports[1])
|
||||
network.connect(endpoint_a=server_1.network_interface[1], endpoint_b=switch_2.network_interface[1])
|
||||
|
||||
server_2 = Server(
|
||||
hostname="server_2",
|
||||
ip_address="10.0.2.3",
|
||||
subnet_mask="255.255.255.0",
|
||||
default_gateway="10.0.2.1",
|
||||
operating_state=NodeOperatingState.ON,
|
||||
start_up_duration=0,
|
||||
)
|
||||
server_2.power_on()
|
||||
network.connect(endpoint_a=server_2.ethernet_port[1], endpoint_b=switch_2.switch_ports[2])
|
||||
network.connect(endpoint_a=server_2.network_interface[1], endpoint_b=switch_2.network_interface[2])
|
||||
|
||||
# 2: Configure base ACL
|
||||
router.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.ARP, dst_port=Port.ARP, position=22)
|
||||
@@ -342,12 +385,12 @@ def install_stuff_to_sim(sim: Simulation):
|
||||
# 3: Install server software
|
||||
server_1.software_manager.install(DNSServer)
|
||||
dns_service: DNSServer = server_1.software_manager.software.get("DNSServer") # noqa
|
||||
dns_service.dns_register("www.example.com", server_2.ip_address)
|
||||
dns_service.dns_register("www.example.com", server_2.network_interface[1].ip_address)
|
||||
server_2.software_manager.install(WebServer)
|
||||
|
||||
# 3.1: Ensure that the dns clients are configured correctly
|
||||
client_1.software_manager.software.get("DNSClient").dns_server = server_1.ethernet_port[1].ip_address
|
||||
server_2.software_manager.software.get("DNSClient").dns_server = server_1.ethernet_port[1].ip_address
|
||||
client_1.software_manager.software.get("DNSClient").dns_server = server_1.network_interface[1].ip_address
|
||||
server_2.software_manager.software.get("DNSClient").dns_server = server_1.network_interface[1].ip_address
|
||||
|
||||
# 4: Check that client came pre-installed with web browser and dns client
|
||||
assert isinstance(client_1.software_manager.software.get("WebBrowser"), WebBrowser)
|
||||
@@ -379,16 +422,16 @@ def install_stuff_to_sim(sim: Simulation):
|
||||
c: Computer = [node for node in sim.network.nodes.values() if node.hostname == "client_1"][0]
|
||||
assert c.software_manager.software.get("WebBrowser") is not None
|
||||
assert c.software_manager.software.get("DNSClient") is not None
|
||||
assert str(c.ethernet_port[1].ip_address) == "10.0.1.2"
|
||||
assert str(c.network_interface[1].ip_address) == "10.0.1.2"
|
||||
|
||||
# 5.3: Assert that server_1 is correctly configured
|
||||
s1: Server = [node for node in sim.network.nodes.values() if node.hostname == "server_1"][0]
|
||||
assert str(s1.ethernet_port[1].ip_address) == "10.0.2.2"
|
||||
assert str(s1.network_interface[1].ip_address) == "10.0.2.2"
|
||||
assert s1.software_manager.software.get("DNSServer") is not None
|
||||
|
||||
# 5.4: Assert that server_2 is correctly configured
|
||||
s2: Server = [node for node in sim.network.nodes.values() if node.hostname == "server_2"][0]
|
||||
assert str(s2.ethernet_port[1].ip_address) == "10.0.2.3"
|
||||
assert str(s2.network_interface[1].ip_address) == "10.0.2.3"
|
||||
assert s2.software_manager.software.get("WebServer") is not None
|
||||
|
||||
# 6: Return the simulation
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from primaite.simulator.network.hardware.nodes.computer import Computer
|
||||
from primaite.simulator.network.hardware.nodes.server import Server
|
||||
from primaite.simulator.network.hardware.nodes.host.computer import Computer
|
||||
from primaite.simulator.network.hardware.nodes.host.server import Server
|
||||
from primaite.simulator.system.applications.database_client import DatabaseClient
|
||||
from primaite.simulator.system.applications.red_applications.data_manipulation_bot import DataManipulationBot
|
||||
from primaite.simulator.system.services.database.database_service import DatabaseService
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import pytest
|
||||
|
||||
from primaite.simulator.core import RequestType
|
||||
from primaite.simulator.network.hardware.nodes.computer import Computer
|
||||
from primaite.simulator.network.hardware.nodes.server import Server
|
||||
from primaite.simulator.network.hardware.nodes.switch import Switch
|
||||
from primaite.simulator.network.hardware.nodes.host.computer import Computer
|
||||
from primaite.simulator.network.hardware.nodes.host.server import Server
|
||||
from primaite.simulator.network.hardware.nodes.network.switch import Switch
|
||||
from primaite.simulator.sim_container import Simulation
|
||||
from primaite.simulator.system.services.database.database_service import DatabaseService
|
||||
|
||||
@@ -27,9 +25,9 @@ def test_passing_actions_down(monkeypatch) -> None:
|
||||
downloads_folder = pc1.file_system.create_folder("downloads")
|
||||
pc1.file_system.create_file("bermuda_triangle.png", folder_name="downloads")
|
||||
|
||||
sim.network.connect(pc1.ethernet_port[1], s1.switch_ports[1])
|
||||
sim.network.connect(pc2.ethernet_port[1], s1.switch_ports[2])
|
||||
sim.network.connect(s1.switch_ports[3], srv.ethernet_port[1])
|
||||
sim.network.connect(pc1.network_interface[1], s1.network_interface[1])
|
||||
sim.network.connect(pc2.network_interface[1], s1.network_interface[2])
|
||||
sim.network.connect(s1.network_interface[3], srv.network_interface[1])
|
||||
|
||||
# call this method to make sure no errors occur.
|
||||
sim._request_manager.get_request_types_recursively()
|
||||
|
||||
@@ -21,10 +21,10 @@ from primaite.game.agent.rewards import RewardFunction
|
||||
from primaite.game.game import PrimaiteGame
|
||||
from primaite.simulator.file_system.file_system_item_abc import FileSystemItemHealthStatus
|
||||
from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState
|
||||
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.server import Server
|
||||
from primaite.simulator.network.hardware.nodes.switch import Switch
|
||||
from primaite.simulator.network.hardware.nodes.host.computer import Computer
|
||||
from primaite.simulator.network.hardware.nodes.host.server import Server
|
||||
from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router
|
||||
from primaite.simulator.network.hardware.nodes.network.switch import Switch
|
||||
from primaite.simulator.network.transmission.network_layer import IPProtocol
|
||||
from primaite.simulator.network.transmission.transport_layer import Port
|
||||
from primaite.simulator.sim_container import Simulation
|
||||
@@ -227,7 +227,7 @@ def test_network_nic_disable_integration(game_and_agent: Tuple[PrimaiteGame, Pro
|
||||
game.step()
|
||||
|
||||
# 3: Check that the NIC is disabled, and that client 1 cannot access example.com
|
||||
assert client_1.ethernet_port[1].enabled == False
|
||||
assert client_1.network_interface[1].enabled == False
|
||||
assert not browser.get_webpage()
|
||||
assert not client_1.ping("10.0.2.2")
|
||||
assert not client_1.ping("10.0.2.3")
|
||||
@@ -243,7 +243,7 @@ def test_network_nic_enable_integration(game_and_agent: Tuple[PrimaiteGame, Prox
|
||||
|
||||
# 1: Disable client_1 nic
|
||||
client_1 = game.simulation.network.get_node_by_hostname("client_1")
|
||||
client_1.ethernet_port[1].disable()
|
||||
client_1.network_interface[1].disable()
|
||||
assert not client_1.ping("10.0.2.2")
|
||||
|
||||
# 2: Use action to enable nic
|
||||
@@ -258,7 +258,7 @@ def test_network_nic_enable_integration(game_and_agent: Tuple[PrimaiteGame, Prox
|
||||
game.step()
|
||||
|
||||
# 3: Check that the NIC is enabled, and that client 1 can ping again
|
||||
assert client_1.ethernet_port[1].enabled == True
|
||||
assert client_1.network_interface[1].enabled == True
|
||||
assert client_1.ping("10.0.2.3")
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from gymnasium import spaces
|
||||
|
||||
from primaite.game.agent.observations import FileObservation
|
||||
from primaite.simulator.network.hardware.nodes.computer import Computer
|
||||
from primaite.simulator.network.hardware.nodes.host.computer import Computer
|
||||
from primaite.simulator.sim_container import Simulation
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from primaite.game.agent.rewards import RewardFunction, WebpageUnavailablePenalty
|
||||
from primaite.simulator.network.hardware.nodes.router import ACLAction, Router
|
||||
from primaite.game.agent.rewards import WebpageUnavailablePenalty
|
||||
from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router
|
||||
from primaite.simulator.network.transmission.network_layer import IPProtocol
|
||||
from primaite.simulator.network.transmission.transport_layer import Port
|
||||
from tests.conftest import ControlledAgent
|
||||
|
||||
@@ -4,9 +4,9 @@ from typing import Any, Dict, List, Tuple
|
||||
import pytest
|
||||
|
||||
from primaite.simulator.network.container import Network
|
||||
from primaite.simulator.network.hardware.nodes.computer import Computer
|
||||
from primaite.simulator.network.hardware.nodes.server import Server
|
||||
from primaite.simulator.network.hardware.nodes.switch import Switch
|
||||
from primaite.simulator.network.hardware.nodes.host.computer import Computer
|
||||
from primaite.simulator.network.hardware.nodes.host.server import Server
|
||||
from primaite.simulator.network.hardware.nodes.network.switch import Switch
|
||||
from primaite.simulator.network.transmission.network_layer import IPProtocol
|
||||
from primaite.simulator.network.transmission.transport_layer import Port
|
||||
from primaite.simulator.system.applications.application import Application
|
||||
@@ -37,11 +37,7 @@ class BroadcastService(Service):
|
||||
|
||||
def broadcast(self, ip_network: IPv4Network):
|
||||
# Send a broadcast payload to an entire IP network
|
||||
super().send(
|
||||
payload="broadcast",
|
||||
dest_ip_address=ip_network,
|
||||
dest_port=Port.HTTP,
|
||||
)
|
||||
super().send(payload="broadcast", dest_ip_address=ip_network, dest_port=Port.HTTP, ip_protocol=self.protocol)
|
||||
|
||||
|
||||
class BroadcastClient(Application):
|
||||
@@ -110,9 +106,9 @@ def broadcast_network() -> Network:
|
||||
switch_1 = Switch(hostname="switch_1", num_ports=6, start_up_duration=0)
|
||||
switch_1.power_on()
|
||||
|
||||
network.connect(endpoint_a=client_1.ethernet_port[1], endpoint_b=switch_1.switch_ports[1])
|
||||
network.connect(endpoint_a=client_2.ethernet_port[1], endpoint_b=switch_1.switch_ports[2])
|
||||
network.connect(endpoint_a=server_1.ethernet_port[1], endpoint_b=switch_1.switch_ports[3])
|
||||
network.connect(endpoint_a=client_1.network_interface[1], endpoint_b=switch_1.network_interface[1])
|
||||
network.connect(endpoint_a=client_2.network_interface[1], endpoint_b=switch_1.network_interface[2])
|
||||
network.connect(endpoint_a=server_1.network_interface[1], endpoint_b=switch_1.network_interface[3])
|
||||
|
||||
return network
|
||||
|
||||
|
||||
@@ -1,41 +1,62 @@
|
||||
from primaite.simulator.network.hardware.base import Link, NIC, Node, NodeOperatingState
|
||||
from primaite.simulator.network.container import Network
|
||||
from primaite.simulator.network.hardware.nodes.host.computer import Computer
|
||||
from primaite.simulator.network.hardware.nodes.host.host_node import NIC
|
||||
from primaite.simulator.network.hardware.nodes.host.server import Server
|
||||
from primaite.simulator.network.hardware.nodes.network.switch import Switch
|
||||
|
||||
|
||||
def test_node_to_node_ping():
|
||||
"""Tests two Nodes are able to ping each other."""
|
||||
node_a = Node(hostname="node_a", operating_state=NodeOperatingState.ON)
|
||||
nic_a = NIC(ip_address="192.168.0.10", subnet_mask="255.255.255.0", operating_state=NodeOperatingState.ON)
|
||||
node_a.connect_nic(nic_a)
|
||||
"""Tests two Computers are able to ping each other."""
|
||||
network = Network()
|
||||
|
||||
node_b = Node(hostname="node_b", operating_state=NodeOperatingState.ON)
|
||||
nic_b = NIC(ip_address="192.168.0.11", subnet_mask="255.255.255.0")
|
||||
node_b.connect_nic(nic_b)
|
||||
client_1 = Computer(
|
||||
hostname="client_1",
|
||||
ip_address="192.168.1.10",
|
||||
subnet_mask="255.255.255.0",
|
||||
default_gateway="192.168.1.1",
|
||||
start_up_duration=0,
|
||||
)
|
||||
client_1.power_on()
|
||||
|
||||
Link(endpoint_a=nic_a, endpoint_b=nic_b)
|
||||
server_1 = Server(
|
||||
hostname="server_1",
|
||||
ip_address="192.168.1.11",
|
||||
subnet_mask="255.255.255.0",
|
||||
default_gateway="192.168.1.1",
|
||||
start_up_duration=0,
|
||||
)
|
||||
server_1.power_on()
|
||||
|
||||
assert node_a.ping("192.168.0.11")
|
||||
switch_1 = Switch(hostname="switch_1", start_up_duration=0)
|
||||
switch_1.power_on()
|
||||
|
||||
network.connect(endpoint_a=client_1.network_interface[1], endpoint_b=switch_1.network_interface[1])
|
||||
network.connect(endpoint_a=server_1.network_interface[1], endpoint_b=switch_1.network_interface[2])
|
||||
|
||||
assert client_1.ping("192.168.1.11")
|
||||
|
||||
|
||||
def test_multi_nic():
|
||||
"""Tests that Nodes with multiple NICs can ping each other and the data go across the correct links."""
|
||||
node_a = Node(hostname="node_a", operating_state=NodeOperatingState.ON)
|
||||
nic_a = NIC(ip_address="192.168.0.10", subnet_mask="255.255.255.0")
|
||||
node_a.connect_nic(nic_a)
|
||||
"""Tests that Computers with multiple NICs can ping each other and the data go across the correct links."""
|
||||
network = Network()
|
||||
|
||||
node_b = Node(hostname="node_b", operating_state=NodeOperatingState.ON)
|
||||
nic_b1 = NIC(ip_address="192.168.0.11", subnet_mask="255.255.255.0")
|
||||
nic_b2 = NIC(ip_address="10.0.0.12", subnet_mask="255.0.0.0")
|
||||
node_b.connect_nic(nic_b1)
|
||||
node_b.connect_nic(nic_b2)
|
||||
node_a = Computer(hostname="node_a", ip_address="192.168.0.10", subnet_mask="255.255.255.0", start_up_duration=0)
|
||||
node_a.power_on()
|
||||
|
||||
node_c = Node(hostname="node_c", operating_state=NodeOperatingState.ON)
|
||||
nic_c = NIC(ip_address="10.0.0.13", subnet_mask="255.0.0.0")
|
||||
node_c.connect_nic(nic_c)
|
||||
node_b = Computer(hostname="node_b", ip_address="192.168.0.11", subnet_mask="255.255.255.0", start_up_duration=0)
|
||||
node_b.power_on()
|
||||
node_b.connect_nic(NIC(ip_address="10.0.0.12", subnet_mask="255.0.0.0"))
|
||||
|
||||
Link(endpoint_a=nic_a, endpoint_b=nic_b1)
|
||||
node_c = Computer(hostname="node_c", ip_address="10.0.0.13", subnet_mask="255.0.0.0", start_up_duration=0)
|
||||
node_c.power_on()
|
||||
|
||||
Link(endpoint_a=nic_b2, endpoint_b=nic_c)
|
||||
network.connect(node_a.network_interface[1], node_b.network_interface[1])
|
||||
network.connect(node_b.network_interface[2], node_c.network_interface[1])
|
||||
|
||||
node_a.ping("192.168.0.11")
|
||||
assert node_a.ping(node_b.network_interface[1].ip_address)
|
||||
|
||||
assert node_c.ping("10.0.0.12")
|
||||
assert node_c.ping(node_b.network_interface[2].ip_address)
|
||||
|
||||
assert not node_a.ping(node_b.network_interface[2].ip_address)
|
||||
|
||||
assert not node_a.ping(node_c.network_interface[1].ip_address)
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
from primaite.simulator.network.hardware.base import Link, NIC, Node, NodeOperatingState
|
||||
|
||||
|
||||
def test_link_up():
|
||||
"""Tests Nodes, NICs, and Links can all be connected and be in an enabled/up state."""
|
||||
node_a = Node(hostname="node_a", operating_state=NodeOperatingState.ON)
|
||||
nic_a = NIC(ip_address="192.168.0.10", subnet_mask="255.255.255.0")
|
||||
node_a.connect_nic(nic_a)
|
||||
|
||||
node_b = Node(hostname="node_b", operating_state=NodeOperatingState.ON)
|
||||
nic_b = NIC(ip_address="192.168.0.11", subnet_mask="255.255.255.0")
|
||||
node_b.connect_nic(nic_b)
|
||||
|
||||
link = Link(endpoint_a=nic_a, endpoint_b=nic_b)
|
||||
|
||||
assert nic_a.enabled
|
||||
assert nic_b.enabled
|
||||
assert link.is_up
|
||||
|
||||
|
||||
def test_ping_between_computer_and_server(client_server):
|
||||
computer, server = client_server
|
||||
|
||||
assert computer.ping(target_ip_address=server.nics[next(iter(server.nics))].ip_address)
|
||||
@@ -1,10 +1,7 @@
|
||||
import pytest
|
||||
|
||||
from primaite.simulator.network.container import Network
|
||||
from primaite.simulator.network.hardware.base import NIC, Node
|
||||
from primaite.simulator.network.hardware.nodes.computer import Computer
|
||||
from primaite.simulator.network.hardware.nodes.server import Server
|
||||
from primaite.simulator.network.networks import client_server_routed
|
||||
from primaite.simulator.network.hardware.nodes.host.computer import Computer
|
||||
from primaite.simulator.network.hardware.nodes.host.host_node import NIC
|
||||
from primaite.simulator.network.hardware.nodes.host.server import Server
|
||||
|
||||
|
||||
def test_network(example_network):
|
||||
@@ -14,22 +11,22 @@ def test_network(example_network):
|
||||
server_1: Server = network.get_node_by_hostname("server_1")
|
||||
server_2: Server = network.get_node_by_hostname("server_2")
|
||||
|
||||
assert client_1.ping(client_2.ethernet_port[1].ip_address)
|
||||
assert client_2.ping(client_1.ethernet_port[1].ip_address)
|
||||
assert client_1.ping(client_2.network_interface[1].ip_address)
|
||||
assert client_2.ping(client_1.network_interface[1].ip_address)
|
||||
|
||||
assert server_1.ping(server_2.ethernet_port[1].ip_address)
|
||||
assert server_2.ping(server_1.ethernet_port[1].ip_address)
|
||||
assert server_1.ping(server_2.network_interface[1].ip_address)
|
||||
assert server_2.ping(server_1.network_interface[1].ip_address)
|
||||
|
||||
assert client_1.ping(server_1.ethernet_port[1].ip_address)
|
||||
assert client_2.ping(server_1.ethernet_port[1].ip_address)
|
||||
assert client_1.ping(server_2.ethernet_port[1].ip_address)
|
||||
assert client_2.ping(server_2.ethernet_port[1].ip_address)
|
||||
assert client_1.ping(server_1.network_interface[1].ip_address)
|
||||
assert client_2.ping(server_1.network_interface[1].ip_address)
|
||||
assert client_1.ping(server_2.network_interface[1].ip_address)
|
||||
assert client_2.ping(server_2.network_interface[1].ip_address)
|
||||
|
||||
|
||||
def test_adding_removing_nodes():
|
||||
"""Check that we can create and add a node to a network."""
|
||||
net = Network()
|
||||
n1 = Node(hostname="computer")
|
||||
n1 = Computer(hostname="computer", ip_address="192.168.1.2", subnet_mask="255.255.255.0", start_up_duration=0)
|
||||
net.add_node(n1)
|
||||
assert n1.parent is net
|
||||
assert n1 in net
|
||||
@@ -42,7 +39,7 @@ def test_adding_removing_nodes():
|
||||
def test_readding_node():
|
||||
"""Check that warning is raised when readding a node."""
|
||||
net = Network()
|
||||
n1 = Node(hostname="computer")
|
||||
n1 = Computer(hostname="computer", ip_address="192.168.1.2", subnet_mask="255.255.255.0", start_up_duration=0)
|
||||
net.add_node(n1)
|
||||
net.add_node(n1)
|
||||
assert n1.parent is net
|
||||
@@ -52,7 +49,7 @@ def test_readding_node():
|
||||
def test_removing_nonexistent_node():
|
||||
"""Check that warning is raised when trying to remove a node that is not in the network."""
|
||||
net = Network()
|
||||
n1 = Node(hostname="computer")
|
||||
n1 = Computer(hostname="computer1", ip_address="192.168.1.1", subnet_mask="255.255.255.0", start_up_duration=0)
|
||||
net.remove_node(n1)
|
||||
assert n1.parent is None
|
||||
assert n1 not in net
|
||||
@@ -61,17 +58,13 @@ def test_removing_nonexistent_node():
|
||||
def test_connecting_nodes():
|
||||
"""Check that two nodes on the network can be connected."""
|
||||
net = Network()
|
||||
n1 = Node(hostname="computer")
|
||||
n1_nic = NIC(ip_address="120.30.0.1", gateway="192.168.0.1", subnet_mask="255.255.255.0")
|
||||
n1.connect_nic(n1_nic)
|
||||
n2 = Node(hostname="server")
|
||||
n2_nic = NIC(ip_address="120.30.0.2", gateway="192.168.0.1", subnet_mask="255.255.255.0")
|
||||
n2.connect_nic(n2_nic)
|
||||
n1 = Computer(hostname="computer1", ip_address="192.168.1.1", subnet_mask="255.255.255.0", start_up_duration=0)
|
||||
n2 = Computer(hostname="computer2", ip_address="192.168.1.2", subnet_mask="255.255.255.0", start_up_duration=0)
|
||||
|
||||
net.add_node(n1)
|
||||
net.add_node(n2)
|
||||
|
||||
net.connect(n1.nics[n1_nic.uuid], n2.nics[n2_nic.uuid], bandwidth=30)
|
||||
net.connect(n1.network_interface[1], n2.network_interface[1])
|
||||
|
||||
assert len(net.links) == 1
|
||||
link = list(net.links.values())[0]
|
||||
@@ -79,38 +72,29 @@ def test_connecting_nodes():
|
||||
assert link.parent is net
|
||||
|
||||
|
||||
def test_connecting_node_to_itself():
|
||||
def test_connecting_node_to_itself_fails():
|
||||
net = Network()
|
||||
node = Node(hostname="computer")
|
||||
nic1 = NIC(ip_address="120.30.0.1", gateway="192.168.0.1", subnet_mask="255.255.255.0")
|
||||
node.connect_nic(nic1)
|
||||
nic2 = NIC(ip_address="120.30.0.2", gateway="192.168.0.1", subnet_mask="255.255.255.0")
|
||||
node.connect_nic(nic2)
|
||||
node = Computer(hostname="node_b", ip_address="192.168.0.11", subnet_mask="255.255.255.0", start_up_duration=0)
|
||||
node.power_on()
|
||||
node.connect_nic(NIC(ip_address="10.0.0.12", subnet_mask="255.0.0.0"))
|
||||
|
||||
net.add_node(node)
|
||||
|
||||
net.connect(node.nics[nic1.uuid], node.nics[nic2.uuid], bandwidth=30)
|
||||
net.connect(node.network_interface[1], node.network_interface[2])
|
||||
|
||||
assert node in net
|
||||
assert nic1._connected_link is None
|
||||
assert nic2._connected_link is None
|
||||
assert node.network_interface[1]._connected_link is None
|
||||
assert node.network_interface[2]._connected_link is None
|
||||
assert len(net.links) == 0
|
||||
|
||||
|
||||
def test_disconnecting_nodes():
|
||||
net = Network()
|
||||
|
||||
n1 = Node(hostname="computer")
|
||||
n1_nic = NIC(ip_address="120.30.0.1", gateway="192.168.0.1", subnet_mask="255.255.255.0")
|
||||
n1.connect_nic(n1_nic)
|
||||
net.add_node(n1)
|
||||
n1 = Computer(hostname="computer1", ip_address="192.168.1.1", subnet_mask="255.255.255.0", start_up_duration=0)
|
||||
n2 = Computer(hostname="computer2", ip_address="192.168.1.2", subnet_mask="255.255.255.0", start_up_duration=0)
|
||||
|
||||
n2 = Node(hostname="server")
|
||||
n2_nic = NIC(ip_address="120.30.0.2", gateway="192.168.0.1", subnet_mask="255.255.255.0")
|
||||
n2.connect_nic(n2_nic)
|
||||
net.add_node(n2)
|
||||
|
||||
net.connect(n1.nics[n1_nic.uuid], n2.nics[n2_nic.uuid], bandwidth=30)
|
||||
net.connect(n1.network_interface[1], n2.network_interface[1])
|
||||
assert len(net.links) == 1
|
||||
|
||||
link = list(net.links.values())[0]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import pytest
|
||||
|
||||
from primaite.simulator.network.hardware.base import Link, NIC
|
||||
from primaite.simulator.network.hardware.base import Link
|
||||
from primaite.simulator.network.hardware.nodes.host.host_node import NIC
|
||||
|
||||
|
||||
def test_link_fails_with_same_nic():
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
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.hardware.nodes.host.computer import Computer
|
||||
from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router
|
||||
from primaite.simulator.network.transmission.network_layer import IPProtocol
|
||||
from primaite.simulator.network.transmission.transport_layer import Port
|
||||
from primaite.simulator.system.services.ntp.ntp_client import NTPClient
|
||||
@@ -14,28 +12,37 @@ from primaite.simulator.system.services.ntp.ntp_server import NTPServer
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def pc_a_pc_b_router_1() -> Tuple[Node, Node, Router]:
|
||||
pc_a = Node(hostname="pc_a", default_gateway="192.168.0.1", operating_state=NodeOperatingState.ON)
|
||||
nic_a = NIC(ip_address="192.168.0.10", subnet_mask="255.255.255.0")
|
||||
pc_a.connect_nic(nic_a)
|
||||
def pc_a_pc_b_router_1() -> Tuple[Computer, Computer, Router]:
|
||||
network = Network()
|
||||
pc_a = Computer(
|
||||
hostname="pc_a",
|
||||
ip_address="192.168.0.10",
|
||||
subnet_mask="255.255.255.0",
|
||||
default_gateway="192.168.0.1",
|
||||
start_up_duration=0,
|
||||
)
|
||||
pc_a.power_on()
|
||||
|
||||
pc_b = Node(hostname="pc_b", default_gateway="192.168.1.1", operating_state=NodeOperatingState.ON)
|
||||
nic_b = NIC(ip_address="192.168.1.10", subnet_mask="255.255.255.0")
|
||||
pc_b.connect_nic(nic_b)
|
||||
pc_b = Computer(
|
||||
hostname="pc_b",
|
||||
ip_address="192.168.1.10",
|
||||
subnet_mask="255.255.255.0",
|
||||
default_gateway="192.168.1.1",
|
||||
start_up_duration=0,
|
||||
)
|
||||
pc_b.power_on()
|
||||
|
||||
router_1 = Router(hostname="router_1", operating_state=NodeOperatingState.ON)
|
||||
router_1 = Router(hostname="router_1", start_up_duration=0)
|
||||
router_1.power_on()
|
||||
|
||||
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")
|
||||
|
||||
Link(endpoint_a=nic_a, endpoint_b=router_1.ethernet_ports[1])
|
||||
Link(endpoint_a=nic_b, endpoint_b=router_1.ethernet_ports[2])
|
||||
network.connect(endpoint_a=pc_a.network_interface[1], endpoint_b=router_1.network_interface[1])
|
||||
network.connect(endpoint_a=pc_b.network_interface[1], endpoint_b=router_1.network_interface[2])
|
||||
router_1.enable_port(1)
|
||||
router_1.enable_port(2)
|
||||
|
||||
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)
|
||||
return pc_a, pc_b, router_1
|
||||
|
||||
|
||||
@@ -61,7 +68,7 @@ def multi_hop_network() -> Network:
|
||||
|
||||
# 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])
|
||||
network.connect(pc_a.network_interface[1], router_1.network_interface[2])
|
||||
router_1.enable_port(2)
|
||||
|
||||
# Configure Router 1 ACLs
|
||||
@@ -86,17 +93,15 @@ def multi_hop_network() -> Network:
|
||||
|
||||
# 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])
|
||||
network.connect(pc_b.network_interface[1], router_2.network_interface[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])
|
||||
network.connect(router_1.network_interface[1], router_2.network_interface[1])
|
||||
router_1.enable_port(1)
|
||||
router_2.enable_port(1)
|
||||
return network
|
||||
@@ -117,14 +122,14 @@ def test_ping_other_router_port(pc_a_pc_b_router_1):
|
||||
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")
|
||||
assert pc_a.ping(pc_b.network_interface[1].ip_address)
|
||||
|
||||
|
||||
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)
|
||||
assert not pc_a.ping(pc_b.network_interface[1].ip_address)
|
||||
|
||||
|
||||
def test_with_routes_can_ping(multi_hop_network):
|
||||
@@ -144,7 +149,7 @@ def test_with_routes_can_ping(multi_hop_network):
|
||||
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)
|
||||
assert pc_a.ping(pc_b.network_interface[1].ip_address)
|
||||
|
||||
|
||||
def test_routing_services(multi_hop_network):
|
||||
@@ -159,7 +164,7 @@ def test_routing_services(multi_hop_network):
|
||||
pc_b.software_manager.install(NTPServer)
|
||||
pc_b.software_manager.software["NTPServer"].start()
|
||||
|
||||
ntp_client.configure(ntp_server_ip_address=pc_b.ethernet_port[1].ip_address)
|
||||
ntp_client.configure(ntp_server_ip_address=pc_b.network_interface[1].ip_address)
|
||||
|
||||
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
|
||||
|
||||
@@ -1,30 +1,5 @@
|
||||
from primaite.simulator.network.hardware.base import Link, NodeOperatingState
|
||||
from primaite.simulator.network.hardware.nodes.computer import Computer
|
||||
from primaite.simulator.network.hardware.nodes.server import Server
|
||||
from primaite.simulator.network.hardware.nodes.switch import Switch
|
||||
|
||||
|
||||
def test_switched_network():
|
||||
def test_switched_network(client_switch_server):
|
||||
"""Tests a node can ping another node via the switch."""
|
||||
client_1 = Computer(
|
||||
hostname="client_1",
|
||||
ip_address="192.168.1.10",
|
||||
subnet_mask="255.255.255.0",
|
||||
default_gateway="192.168.1.0",
|
||||
operating_state=NodeOperatingState.ON,
|
||||
)
|
||||
computer, switch, server = client_switch_server
|
||||
|
||||
server_1 = Server(
|
||||
hostname=" server_1",
|
||||
ip_address="192.168.1.11",
|
||||
subnet_mask="255.255.255.0",
|
||||
default_gateway="192.168.1.11",
|
||||
operating_state=NodeOperatingState.ON,
|
||||
)
|
||||
|
||||
switch_1 = Switch(hostname="switch_1", num_ports=6, operating_state=NodeOperatingState.ON)
|
||||
|
||||
Link(endpoint_a=client_1.ethernet_port[1], endpoint_b=switch_1.switch_ports[1])
|
||||
Link(endpoint_a=server_1.ethernet_port[1], endpoint_b=switch_1.switch_ports[2])
|
||||
|
||||
assert client_1.ping("192.168.1.11")
|
||||
assert computer.ping(server.network_interface[1].ip_address)
|
||||
|
||||
@@ -4,9 +4,9 @@ from typing import Tuple
|
||||
import pytest
|
||||
|
||||
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.server import Server
|
||||
from primaite.simulator.network.hardware.nodes.host.computer import Computer
|
||||
from primaite.simulator.network.hardware.nodes.host.server import Server
|
||||
from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router
|
||||
from primaite.simulator.network.transmission.transport_layer import Port
|
||||
from primaite.simulator.system.applications.application import ApplicationOperatingState
|
||||
from primaite.simulator.system.applications.database_client import DatabaseClient
|
||||
@@ -24,7 +24,7 @@ def dos_bot_and_db_server(client_server) -> Tuple[DoSBot, Computer, DatabaseServ
|
||||
|
||||
dos_bot: DoSBot = computer.software_manager.software.get("DoSBot")
|
||||
dos_bot.configure(
|
||||
target_ip_address=IPv4Address(server.nics.get(next(iter(server.nics))).ip_address),
|
||||
target_ip_address=IPv4Address(server.network_interface[1].ip_address),
|
||||
target_port=Port.POSTGRES_SERVER,
|
||||
)
|
||||
|
||||
@@ -54,7 +54,7 @@ def dos_bot_db_server_green_client(example_network) -> Network:
|
||||
|
||||
dos_bot: DoSBot = client_1.software_manager.software.get("DoSBot")
|
||||
dos_bot.configure(
|
||||
target_ip_address=IPv4Address(server.nics.get(next(iter(server.nics))).ip_address),
|
||||
target_ip_address=IPv4Address(server.network_interface[1].ip_address),
|
||||
target_port=Port.POSTGRES_SERVER,
|
||||
)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ from typing import Tuple
|
||||
import pytest
|
||||
|
||||
from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState
|
||||
from primaite.simulator.network.hardware.nodes.computer import Computer
|
||||
from primaite.simulator.network.hardware.nodes.host.computer import Computer
|
||||
from primaite.simulator.system.applications.application import Application, ApplicationOperatingState
|
||||
|
||||
|
||||
@@ -14,8 +14,10 @@ def populated_node(application_class) -> Tuple[Application, Computer]:
|
||||
ip_address="192.168.1.2",
|
||||
subnet_mask="255.255.255.0",
|
||||
default_gateway="192.168.1.1",
|
||||
operating_state=NodeOperatingState.ON,
|
||||
start_up_duration=0,
|
||||
shut_down_duration=0,
|
||||
)
|
||||
computer.power_on()
|
||||
computer.software_manager.install(application_class)
|
||||
|
||||
app = computer.software_manager.software.get("TestApplication")
|
||||
@@ -31,7 +33,8 @@ def test_application_on_offline_node(application_class):
|
||||
ip_address="192.168.1.2",
|
||||
subnet_mask="255.255.255.0",
|
||||
default_gateway="192.168.1.1",
|
||||
operating_state=NodeOperatingState.ON,
|
||||
start_up_duration=0,
|
||||
shut_down_duration=0,
|
||||
)
|
||||
computer.software_manager.install(application_class)
|
||||
|
||||
@@ -39,9 +42,6 @@ def test_application_on_offline_node(application_class):
|
||||
|
||||
computer.power_off()
|
||||
|
||||
for i in range(computer.shut_down_duration + 1):
|
||||
computer.apply_timestep(timestep=i)
|
||||
|
||||
assert computer.operating_state is NodeOperatingState.OFF
|
||||
assert app.operating_state is ApplicationOperatingState.CLOSED
|
||||
|
||||
@@ -58,9 +58,6 @@ def test_server_turns_off_application(populated_node):
|
||||
|
||||
computer.power_off()
|
||||
|
||||
for i in range(computer.shut_down_duration + 1):
|
||||
computer.apply_timestep(timestep=i)
|
||||
|
||||
assert computer.operating_state is NodeOperatingState.OFF
|
||||
assert app.operating_state is ApplicationOperatingState.CLOSED
|
||||
|
||||
@@ -74,9 +71,6 @@ def test_application_cannot_be_turned_on_when_computer_is_off(populated_node):
|
||||
|
||||
computer.power_off()
|
||||
|
||||
for i in range(computer.shut_down_duration + 1):
|
||||
computer.apply_timestep(timestep=i)
|
||||
|
||||
assert computer.operating_state is NodeOperatingState.OFF
|
||||
assert app.operating_state is ApplicationOperatingState.CLOSED
|
||||
|
||||
@@ -95,28 +89,20 @@ def test_computer_runs_applications(populated_node):
|
||||
|
||||
computer.power_off()
|
||||
|
||||
for i in range(computer.shut_down_duration + 1):
|
||||
computer.apply_timestep(timestep=i)
|
||||
|
||||
assert computer.operating_state is NodeOperatingState.OFF
|
||||
assert app.operating_state is ApplicationOperatingState.CLOSED
|
||||
|
||||
computer.power_on()
|
||||
|
||||
for i in range(computer.start_up_duration + 1):
|
||||
computer.apply_timestep(timestep=i)
|
||||
|
||||
assert computer.operating_state is NodeOperatingState.ON
|
||||
assert app.operating_state is ApplicationOperatingState.RUNNING
|
||||
|
||||
computer.power_off()
|
||||
for i in range(computer.start_up_duration + 1):
|
||||
computer.apply_timestep(timestep=i)
|
||||
|
||||
assert computer.operating_state is NodeOperatingState.OFF
|
||||
assert app.operating_state is ApplicationOperatingState.CLOSED
|
||||
|
||||
computer.power_on()
|
||||
for i in range(computer.start_up_duration + 1):
|
||||
computer.apply_timestep(timestep=i)
|
||||
|
||||
assert computer.operating_state is NodeOperatingState.ON
|
||||
assert app.operating_state is ApplicationOperatingState.RUNNING
|
||||
|
||||
@@ -3,8 +3,10 @@ from typing import Tuple
|
||||
|
||||
import pytest
|
||||
|
||||
from primaite.simulator.network.hardware.base import Link, NIC, Node, NodeOperatingState
|
||||
from primaite.simulator.network.hardware.nodes.server import Server
|
||||
from primaite.simulator.network.container import Network
|
||||
from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState
|
||||
from primaite.simulator.network.hardware.nodes.host.computer import Computer
|
||||
from primaite.simulator.network.hardware.nodes.host.server import Server
|
||||
from primaite.simulator.system.applications.database_client import DatabaseClient
|
||||
from primaite.simulator.system.services.database.database_service import DatabaseService
|
||||
from primaite.simulator.system.services.ftp.ftp_server import FTPServer
|
||||
@@ -12,17 +14,15 @@ from primaite.simulator.system.services.service import ServiceOperatingState
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def peer_to_peer() -> Tuple[Node, Node]:
|
||||
node_a = Node(hostname="node_a", operating_state=NodeOperatingState.ON)
|
||||
nic_a = NIC(ip_address="192.168.0.10", subnet_mask="255.255.255.0", operating_state=NodeOperatingState.ON)
|
||||
node_a.connect_nic(nic_a)
|
||||
def peer_to_peer() -> Tuple[Computer, Computer]:
|
||||
network = Network()
|
||||
node_a = Computer(hostname="node_a", ip_address="192.168.0.10", subnet_mask="255.255.255.0", start_up_duration=0)
|
||||
node_a.power_on()
|
||||
node_a.software_manager.get_open_ports()
|
||||
|
||||
node_b = Node(hostname="node_b", operating_state=NodeOperatingState.ON)
|
||||
nic_b = NIC(ip_address="192.168.0.11", subnet_mask="255.255.255.0")
|
||||
node_b.connect_nic(nic_b)
|
||||
|
||||
Link(endpoint_a=nic_a, endpoint_b=nic_b)
|
||||
node_b = Computer(hostname="node_b", ip_address="192.168.0.11", subnet_mask="255.255.255.0", start_up_duration=0)
|
||||
node_b.power_on()
|
||||
network.connect(node_a.network_interface[1], node_b.network_interface[1])
|
||||
|
||||
assert node_a.ping("192.168.0.11")
|
||||
|
||||
@@ -37,26 +37,11 @@ def peer_to_peer() -> Tuple[Node, Node]:
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def peer_to_peer_secure_db() -> Tuple[Node, Node]:
|
||||
node_a = Node(hostname="node_a", operating_state=NodeOperatingState.ON)
|
||||
nic_a = NIC(ip_address="192.168.0.10", subnet_mask="255.255.255.0", operating_state=NodeOperatingState.ON)
|
||||
node_a.connect_nic(nic_a)
|
||||
node_a.software_manager.get_open_ports()
|
||||
def peer_to_peer_secure_db(peer_to_peer) -> Tuple[Computer, Computer]:
|
||||
node_a, node_b = peer_to_peer
|
||||
|
||||
node_b = Node(hostname="node_b", operating_state=NodeOperatingState.ON)
|
||||
nic_b = NIC(ip_address="192.168.0.11", subnet_mask="255.255.255.0")
|
||||
node_b.connect_nic(nic_b)
|
||||
|
||||
Link(endpoint_a=nic_a, endpoint_b=nic_b)
|
||||
|
||||
assert node_a.ping("192.168.0.11")
|
||||
|
||||
node_a.software_manager.install(DatabaseClient)
|
||||
node_a.software_manager.software["DatabaseClient"].configure(server_ip_address=IPv4Address("192.168.0.11"))
|
||||
node_a.software_manager.software["DatabaseClient"].run()
|
||||
|
||||
node_b.software_manager.install(DatabaseService)
|
||||
database_service: DatabaseService = node_b.software_manager.software["DatabaseService"] # noqa
|
||||
database_service.stop()
|
||||
database_service.password = "12345"
|
||||
database_service.start()
|
||||
return node_a, node_b
|
||||
|
||||
@@ -4,8 +4,8 @@ from typing import Tuple
|
||||
import pytest
|
||||
|
||||
from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState
|
||||
from primaite.simulator.network.hardware.nodes.computer import Computer
|
||||
from primaite.simulator.network.hardware.nodes.server import Server
|
||||
from primaite.simulator.network.hardware.nodes.host.computer import Computer
|
||||
from primaite.simulator.network.hardware.nodes.host.server import Server
|
||||
from primaite.simulator.system.services.dns.dns_client import DNSClient
|
||||
from primaite.simulator.system.services.dns.dns_server import DNSServer
|
||||
from primaite.simulator.system.services.service import ServiceOperatingState
|
||||
@@ -20,7 +20,7 @@ def dns_client_and_dns_server(client_server) -> Tuple[DNSClient, Computer, DNSSe
|
||||
dns_client: DNSClient = computer.software_manager.software.get("DNSClient")
|
||||
dns_client.start()
|
||||
# set server as DNS Server
|
||||
dns_client.dns_server = IPv4Address(server.nics.get(next(iter(server.nics))).ip_address)
|
||||
dns_client.dns_server = IPv4Address(server.network_interfaces.get(next(iter(server.network_interfaces))).ip_address)
|
||||
|
||||
# Install DNS Server on server
|
||||
server.software_manager.install(DNSServer)
|
||||
@@ -28,7 +28,8 @@ def dns_client_and_dns_server(client_server) -> Tuple[DNSClient, Computer, DNSSe
|
||||
dns_server.start()
|
||||
# register arcd.com as a domain
|
||||
dns_server.dns_register(
|
||||
domain_name="arcd.com", domain_ip_address=IPv4Address(server.nics.get(next(iter(server.nics))).ip_address)
|
||||
domain_name="arcd.com",
|
||||
domain_ip_address=IPv4Address(server.network_interface[1].ip_address),
|
||||
)
|
||||
|
||||
return dns_client, computer, dns_server, server
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
from ipaddress import IPv4Address
|
||||
from typing import Tuple
|
||||
|
||||
import pytest
|
||||
|
||||
from primaite.simulator.network.hardware.nodes.computer import Computer
|
||||
from primaite.simulator.network.hardware.nodes.server import Server
|
||||
from primaite.simulator.network.hardware.nodes.host.computer import Computer
|
||||
from primaite.simulator.network.hardware.nodes.host.server import Server
|
||||
from primaite.simulator.system.services.ftp.ftp_client import FTPClient
|
||||
from primaite.simulator.system.services.ftp.ftp_server import FTPServer
|
||||
from primaite.simulator.system.services.service import ServiceOperatingState
|
||||
@@ -44,7 +43,7 @@ def test_ftp_client_store_file_in_server(ftp_client_and_ftp_server):
|
||||
src_file_name="test_file.txt",
|
||||
dest_folder_name="client_1_backup",
|
||||
dest_file_name="test_file.txt",
|
||||
dest_ip_address=server.nics.get(next(iter(server.nics))).ip_address,
|
||||
dest_ip_address=server.network_interfaces.get(next(iter(server.network_interfaces))).ip_address,
|
||||
)
|
||||
|
||||
assert ftp_server.file_system.get_file(folder_name="client_1_backup", file_name="test_file.txt")
|
||||
@@ -67,7 +66,7 @@ def test_ftp_client_retrieve_file_from_server(ftp_client_and_ftp_server):
|
||||
src_file_name="test_file.txt",
|
||||
dest_folder_name="downloads",
|
||||
dest_file_name="test_file.txt",
|
||||
dest_ip_address=server.nics.get(next(iter(server.nics))).ip_address,
|
||||
dest_ip_address=server.network_interfaces.get(next(iter(server.network_interfaces))).ip_address,
|
||||
)
|
||||
|
||||
# client should have retrieved the file
|
||||
@@ -98,7 +97,7 @@ def test_ftp_client_tries_to_connect_to_offline_server(ftp_client_and_ftp_server
|
||||
src_file_name="test_file.txt",
|
||||
dest_folder_name="downloads",
|
||||
dest_file_name="test_file.txt",
|
||||
dest_ip_address=server.nics.get(next(iter(server.nics))).ip_address,
|
||||
dest_ip_address=server.network_interfaces.get(next(iter(server.network_interfaces))).ip_address,
|
||||
)
|
||||
is False
|
||||
)
|
||||
|
||||
@@ -4,10 +4,8 @@ from typing import Tuple
|
||||
|
||||
import pytest
|
||||
|
||||
from primaite.simulator.network.container import Network
|
||||
from primaite.simulator.network.hardware.nodes.computer import Computer
|
||||
from primaite.simulator.network.hardware.nodes.server import Server
|
||||
from primaite.simulator.network.protocols.ntp import NTPPacket
|
||||
from primaite.simulator.network.hardware.nodes.host.computer import Computer
|
||||
from primaite.simulator.network.hardware.nodes.host.server import Server
|
||||
from primaite.simulator.system.services.ntp.ntp_client import NTPClient
|
||||
from primaite.simulator.system.services.ntp.ntp_server import NTPServer
|
||||
from primaite.simulator.system.services.service import ServiceOperatingState
|
||||
@@ -49,11 +47,11 @@ def test_ntp_client_server(create_ntp_network):
|
||||
|
||||
assert ntp_server.operating_state == ServiceOperatingState.RUNNING
|
||||
assert ntp_client.operating_state == ServiceOperatingState.RUNNING
|
||||
ntp_client.configure(ntp_server_ip_address=IPv4Address("192.168.0.2"))
|
||||
ntp_client.configure(ntp_server_ip_address=IPv4Address("192.168.1.3"))
|
||||
|
||||
assert ntp_client.time is None
|
||||
assert not ntp_client.time
|
||||
ntp_client.request_time()
|
||||
assert ntp_client.time is not None
|
||||
assert ntp_client.time
|
||||
first_time = ntp_client.time
|
||||
sleep(0.1)
|
||||
ntp_client.apply_timestep(1) # Check time advances
|
||||
@@ -70,7 +68,7 @@ def test_ntp_server_failure(create_ntp_network):
|
||||
|
||||
assert ntp_client.operating_state == ServiceOperatingState.RUNNING
|
||||
assert ntp_client.operating_state == ServiceOperatingState.RUNNING
|
||||
ntp_client.configure(ntp_server_ip_address=IPv4Address("192.168.0.2"))
|
||||
ntp_client.configure(ntp_server_ip_address=IPv4Address("192.168.1.3"))
|
||||
|
||||
# Turn off ntp server.
|
||||
ntp_server.stop()
|
||||
|
||||
@@ -3,8 +3,8 @@ from typing import Tuple
|
||||
import pytest
|
||||
|
||||
from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState
|
||||
from primaite.simulator.network.hardware.nodes.computer import Computer
|
||||
from primaite.simulator.network.hardware.nodes.server import Server
|
||||
from primaite.simulator.network.hardware.nodes.host.computer import Computer
|
||||
from primaite.simulator.network.hardware.nodes.host.server import Server
|
||||
from primaite.simulator.system.services.service import Service, ServiceOperatingState
|
||||
|
||||
|
||||
@@ -13,8 +13,13 @@ def populated_node(
|
||||
service_class,
|
||||
) -> Tuple[Server, Service]:
|
||||
server = Server(
|
||||
hostname="server", ip_address="192.168.0.1", subnet_mask="255.255.255.0", operating_state=NodeOperatingState.ON
|
||||
hostname="server",
|
||||
ip_address="192.168.0.1",
|
||||
subnet_mask="255.255.255.0",
|
||||
start_up_duration=0,
|
||||
shut_down_duration=0,
|
||||
)
|
||||
server.power_on()
|
||||
server.software_manager.install(service_class)
|
||||
|
||||
service = server.software_manager.software.get("TestService")
|
||||
@@ -30,17 +35,16 @@ def test_service_on_offline_node(service_class):
|
||||
ip_address="192.168.1.2",
|
||||
subnet_mask="255.255.255.0",
|
||||
default_gateway="192.168.1.1",
|
||||
operating_state=NodeOperatingState.ON,
|
||||
start_up_duration=0,
|
||||
shut_down_duration=0,
|
||||
)
|
||||
computer.power_on()
|
||||
computer.software_manager.install(service_class)
|
||||
|
||||
service: Service = computer.software_manager.software.get("TestService")
|
||||
|
||||
computer.power_off()
|
||||
|
||||
for i in range(computer.shut_down_duration + 1):
|
||||
computer.apply_timestep(timestep=i)
|
||||
|
||||
assert computer.operating_state is NodeOperatingState.OFF
|
||||
assert service.operating_state is ServiceOperatingState.STOPPED
|
||||
|
||||
@@ -66,9 +70,6 @@ def test_server_turns_off_service(populated_node):
|
||||
|
||||
server.power_off()
|
||||
|
||||
for i in range(server.shut_down_duration + 1):
|
||||
server.apply_timestep(timestep=i)
|
||||
|
||||
assert server.operating_state is NodeOperatingState.OFF
|
||||
assert service.operating_state is ServiceOperatingState.STOPPED
|
||||
|
||||
@@ -82,9 +83,6 @@ def test_service_cannot_be_turned_on_when_server_is_off(populated_node):
|
||||
|
||||
server.power_off()
|
||||
|
||||
for i in range(server.shut_down_duration + 1):
|
||||
server.apply_timestep(timestep=i)
|
||||
|
||||
assert server.operating_state is NodeOperatingState.OFF
|
||||
assert service.operating_state is ServiceOperatingState.STOPPED
|
||||
|
||||
@@ -103,28 +101,20 @@ def test_server_turns_on_service(populated_node):
|
||||
|
||||
server.power_off()
|
||||
|
||||
for i in range(server.shut_down_duration + 1):
|
||||
server.apply_timestep(timestep=i)
|
||||
|
||||
assert server.operating_state is NodeOperatingState.OFF
|
||||
assert service.operating_state is ServiceOperatingState.STOPPED
|
||||
|
||||
server.power_on()
|
||||
|
||||
for i in range(server.start_up_duration + 1):
|
||||
server.apply_timestep(timestep=i)
|
||||
|
||||
assert server.operating_state is NodeOperatingState.ON
|
||||
assert service.operating_state is ServiceOperatingState.RUNNING
|
||||
|
||||
server.power_off()
|
||||
for i in range(server.start_up_duration + 1):
|
||||
server.apply_timestep(timestep=i)
|
||||
|
||||
assert server.operating_state is NodeOperatingState.OFF
|
||||
assert service.operating_state is ServiceOperatingState.STOPPED
|
||||
|
||||
server.power_on()
|
||||
for i in range(server.start_up_duration + 1):
|
||||
server.apply_timestep(timestep=i)
|
||||
|
||||
assert server.operating_state is NodeOperatingState.ON
|
||||
assert service.operating_state is ServiceOperatingState.RUNNING
|
||||
|
||||
@@ -3,8 +3,8 @@ from typing import Tuple
|
||||
import pytest
|
||||
|
||||
from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState
|
||||
from primaite.simulator.network.hardware.nodes.computer import Computer
|
||||
from primaite.simulator.network.hardware.nodes.server import Server
|
||||
from primaite.simulator.network.hardware.nodes.host.computer import Computer
|
||||
from primaite.simulator.network.hardware.nodes.host.server import Server
|
||||
from primaite.simulator.network.protocols.http import HttpStatusCode
|
||||
from primaite.simulator.system.applications.application import ApplicationOperatingState
|
||||
from primaite.simulator.system.applications.web_browser import WebBrowser
|
||||
@@ -26,7 +26,7 @@ def web_client_and_web_server(client_server) -> Tuple[WebBrowser, Computer, WebS
|
||||
computer.software_manager.install(DNSClient)
|
||||
dns_client: DNSClient = computer.software_manager.software.get("DNSClient")
|
||||
# set dns server
|
||||
dns_client.dns_server = server.nics[next(iter(server.nics))].ip_address
|
||||
dns_client.dns_server = server.network_interfaces[next(iter(server.network_interfaces))].ip_address
|
||||
|
||||
# Install Web Server service on server
|
||||
server.software_manager.install(WebServer)
|
||||
@@ -37,7 +37,10 @@ def web_client_and_web_server(client_server) -> Tuple[WebBrowser, Computer, WebS
|
||||
server.software_manager.install(DNSServer)
|
||||
dns_server: DNSServer = server.software_manager.software.get("DNSServer")
|
||||
# register arcd.com to DNS
|
||||
dns_server.dns_register(domain_name="arcd.com", domain_ip_address=server.nics[next(iter(server.nics))].ip_address)
|
||||
dns_server.dns_register(
|
||||
domain_name="arcd.com",
|
||||
domain_ip_address=server.network_interfaces[next(iter(server.network_interfaces))].ip_address,
|
||||
)
|
||||
|
||||
return web_browser, computer, web_server_service, server
|
||||
|
||||
@@ -46,7 +49,7 @@ def test_web_page_get_users_page_request_with_domain_name(web_client_and_web_ser
|
||||
"""Test to see if the client can handle requests with domain names"""
|
||||
web_browser_app, computer, web_server_service, server = web_client_and_web_server
|
||||
|
||||
web_server_ip = server.nics.get(next(iter(server.nics))).ip_address
|
||||
web_server_ip = server.network_interfaces.get(next(iter(server.network_interfaces))).ip_address
|
||||
web_browser_app.target_url = f"http://arcd.com/"
|
||||
assert web_browser_app.operating_state == ApplicationOperatingState.RUNNING
|
||||
|
||||
@@ -61,7 +64,7 @@ def test_web_page_get_users_page_request_with_ip_address(web_client_and_web_serv
|
||||
"""Test to see if the client can handle requests that use ip_address."""
|
||||
web_browser_app, computer, web_server_service, server = web_client_and_web_server
|
||||
|
||||
web_server_ip = server.nics.get(next(iter(server.nics))).ip_address
|
||||
web_server_ip = server.network_interfaces.get(next(iter(server.network_interfaces))).ip_address
|
||||
web_browser_app.target_url = f"http://{web_server_ip}/"
|
||||
assert web_browser_app.operating_state == ApplicationOperatingState.RUNNING
|
||||
|
||||
@@ -76,7 +79,7 @@ def test_web_page_request_from_shut_down_server(web_client_and_web_server):
|
||||
"""Test to see that the web server does not respond when the server is off."""
|
||||
web_browser_app, computer, web_server_service, server = web_client_and_web_server
|
||||
|
||||
web_server_ip = server.nics.get(next(iter(server.nics))).ip_address
|
||||
web_server_ip = server.network_interfaces.get(next(iter(server.network_interfaces))).ip_address
|
||||
web_browser_app.target_url = f"http://arcd.com/"
|
||||
assert web_browser_app.operating_state == ApplicationOperatingState.RUNNING
|
||||
|
||||
|
||||
@@ -5,10 +5,9 @@ import pytest
|
||||
|
||||
from primaite.simulator.network.container import Network
|
||||
from primaite.simulator.network.hardware.base import Link
|
||||
from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState
|
||||
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.server import Server
|
||||
from primaite.simulator.network.hardware.nodes.host.computer import Computer
|
||||
from primaite.simulator.network.hardware.nodes.host.server import Server
|
||||
from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router
|
||||
from primaite.simulator.network.transmission.transport_layer import Port
|
||||
from primaite.simulator.system.applications.database_client import DatabaseClient
|
||||
from primaite.simulator.system.applications.web_browser import WebBrowser
|
||||
@@ -45,9 +44,9 @@ def web_client_web_server_database(example_network) -> Tuple[Network, Computer,
|
||||
db_server = example_network.get_node_by_hostname("server_2")
|
||||
|
||||
# Get the NICs
|
||||
computer_nic = computer.nics[next(iter(computer.nics))]
|
||||
server_nic = web_server.nics[next(iter(web_server.nics))]
|
||||
db_server_nic = db_server.nics[next(iter(db_server.nics))]
|
||||
computer_nic = computer.network_interfaces[next(iter(computer.network_interfaces))]
|
||||
server_nic = web_server.network_interfaces[next(iter(web_server.network_interfaces))]
|
||||
db_server_nic = db_server.network_interfaces[next(iter(db_server.network_interfaces))]
|
||||
|
||||
# Connect Computer and Server
|
||||
link_computer_server = Link(endpoint_a=computer_nic, endpoint_b=server_nic)
|
||||
@@ -75,7 +74,7 @@ def web_client_web_server_database(example_network) -> Tuple[Network, Computer,
|
||||
computer.software_manager.install(DNSClient)
|
||||
dns_client: DNSClient = computer.software_manager.software.get("DNSClient")
|
||||
# set dns server
|
||||
dns_client.dns_server = web_server.nics[next(iter(web_server.nics))].ip_address
|
||||
dns_client.dns_server = web_server.network_interfaces[next(iter(web_server.network_interfaces))].ip_address
|
||||
|
||||
# Install Web Server service on web server
|
||||
web_server.software_manager.install(WebServer)
|
||||
@@ -87,7 +86,8 @@ def web_client_web_server_database(example_network) -> Tuple[Network, Computer,
|
||||
dns_server: DNSServer = web_server.software_manager.software.get("DNSServer")
|
||||
# register arcd.com to DNS
|
||||
dns_server.dns_register(
|
||||
domain_name="arcd.com", domain_ip_address=web_server.nics[next(iter(web_server.nics))].ip_address
|
||||
domain_name="arcd.com",
|
||||
domain_ip_address=web_server.network_interfaces[next(iter(web_server.network_interfaces))].ip_address,
|
||||
)
|
||||
|
||||
# Install DatabaseClient service on web server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from ipaddress import IPv4Address
|
||||
|
||||
from primaite.simulator.network.hardware.nodes.router import ACLAction, Router
|
||||
from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router
|
||||
from primaite.simulator.network.transmission.network_layer import IPProtocol
|
||||
from primaite.simulator.network.transmission.transport_layer import Port
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import pytest
|
||||
|
||||
from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState
|
||||
from primaite.simulator.network.hardware.nodes.switch import Switch
|
||||
from primaite.simulator.network.hardware.nodes.network.switch import Switch
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def switch() -> Switch:
|
||||
switch: Switch = Switch(hostname="switch_1", num_ports=8, operating_state=NodeOperatingState.ON)
|
||||
switch: Switch = Switch(hostname="switch_1", num_ports=8, start_up_duration=0)
|
||||
switch.power_on()
|
||||
switch.show()
|
||||
return switch
|
||||
|
||||
|
||||
@@ -2,8 +2,10 @@ import re
|
||||
from ipaddress import IPv4Address
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from primaite.simulator.network.hardware.base import generate_mac_address, NIC
|
||||
from primaite.simulator.network.hardware.base import generate_mac_address
|
||||
from primaite.simulator.network.hardware.nodes.host.host_node import NIC
|
||||
|
||||
|
||||
def test_mac_address_generation():
|
||||
@@ -29,29 +31,26 @@ def test_invalid_oui_mac_address():
|
||||
|
||||
def test_nic_ip_address_type_conversion():
|
||||
"""Tests NIC IP and gateway address is converted to IPv4Address is originally a string."""
|
||||
nic = NIC(
|
||||
network_interface = NIC(
|
||||
ip_address="192.168.1.2",
|
||||
subnet_mask="255.255.255.0",
|
||||
)
|
||||
assert isinstance(nic.ip_address, IPv4Address)
|
||||
assert isinstance(network_interface.ip_address, IPv4Address)
|
||||
|
||||
|
||||
def test_nic_deserialize():
|
||||
"""Tests NIC serialization and deserialization."""
|
||||
nic = NIC(
|
||||
network_interface = NIC(
|
||||
ip_address="192.168.1.2",
|
||||
subnet_mask="255.255.255.0",
|
||||
)
|
||||
|
||||
nic_json = nic.model_dump_json()
|
||||
nic_json = network_interface.model_dump_json()
|
||||
deserialized_nic = NIC.model_validate_json(nic_json)
|
||||
assert nic_json == deserialized_nic.model_dump_json()
|
||||
|
||||
|
||||
def test_nic_ip_address_as_network_address_fails():
|
||||
"""Tests NIC creation fails if ip address and subnet mask are a network address."""
|
||||
with pytest.raises(ValueError):
|
||||
NIC(
|
||||
ip_address="192.168.0.0",
|
||||
subnet_mask="255.255.255.0",
|
||||
)
|
||||
with pytest.raises(ValidationError):
|
||||
NIC(ip_address="192.168.0.0", subnet_mask="255.255.255.0")
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import re
|
||||
from ipaddress import IPv4Address
|
||||
|
||||
import pytest
|
||||
|
||||
from primaite.simulator.network.hardware.base import Node
|
||||
|
||||
|
||||
def test_node_creation():
|
||||
node = Node(hostname="host_1")
|
||||
@@ -4,12 +4,13 @@ from primaite.simulator.file_system.file import File
|
||||
from primaite.simulator.file_system.file_system_item_abc import FileSystemItemHealthStatus
|
||||
from primaite.simulator.file_system.folder import Folder
|
||||
from primaite.simulator.network.hardware.base import Node, NodeOperatingState
|
||||
from primaite.simulator.network.hardware.nodes.host.computer import Computer
|
||||
from primaite.simulator.system.software import SoftwareHealthState
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def node() -> Node:
|
||||
return Node(hostname="test")
|
||||
return Computer(hostname="test", ip_address="192.168.1.2", subnet_mask="255.255.255.0")
|
||||
|
||||
|
||||
def test_node_startup(node):
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import pytest
|
||||
|
||||
from primaite.simulator.network.protocols.icmp import ICMPPacket
|
||||
from primaite.simulator.network.transmission.data_link_layer import EthernetHeader, Frame
|
||||
from primaite.simulator.network.transmission.network_layer import ICMPPacket, IPPacket, IPProtocol, Precedence
|
||||
from primaite.simulator.network.transmission.network_layer import IPPacket, IPProtocol, Precedence
|
||||
from primaite.simulator.network.transmission.primaite_layer import AgentSource, DataStatus
|
||||
from primaite.simulator.network.transmission.transport_layer import Port, TCPFlags, TCPHeader, UDPHeader
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import pytest
|
||||
|
||||
from primaite.simulator.network.transmission.network_layer import ICMPPacket, ICMPType
|
||||
from primaite.simulator.network.protocols.icmp import ICMPPacket, ICMPType
|
||||
|
||||
|
||||
def test_icmp_minimal_header_creation():
|
||||
|
||||
@@ -5,9 +5,7 @@ import pytest
|
||||
from primaite.simulator.network.container import Network
|
||||
from primaite.simulator.network.hardware.base import Link, Node
|
||||
from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState
|
||||
from primaite.simulator.network.hardware.nodes.computer import Computer
|
||||
from primaite.simulator.system.applications.database_client import DatabaseClient
|
||||
from primaite.simulator.system.services.database.database_service import DatabaseService
|
||||
from primaite.simulator.network.hardware.nodes.host.computer import Computer
|
||||
|
||||
|
||||
def filter_keys_nested_item(data, keys):
|
||||
@@ -110,7 +108,7 @@ def test_removing_node_that_does_not_exist(network):
|
||||
"""Node that does not exist on network should not affect existing nodes."""
|
||||
assert len(network.nodes) is 7
|
||||
|
||||
network.remove_node(Node(hostname="new_node"))
|
||||
network.remove_node(Computer(hostname="new_node", ip_address="192.168.1.2", subnet_mask="255.255.255.0"))
|
||||
assert len(network.nodes) is 7
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ from ipaddress import IPv4Address
|
||||
import pytest
|
||||
|
||||
from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState
|
||||
from primaite.simulator.network.hardware.nodes.computer import Computer
|
||||
from primaite.simulator.network.hardware.nodes.host.computer import Computer
|
||||
from primaite.simulator.network.transmission.transport_layer import Port
|
||||
from primaite.simulator.system.applications.application import ApplicationOperatingState
|
||||
from primaite.simulator.system.applications.red_applications.dos_bot import DoSAttackStage, DoSBot
|
||||
@@ -12,12 +12,9 @@ from primaite.simulator.system.applications.red_applications.dos_bot import DoSA
|
||||
@pytest.fixture(scope="function")
|
||||
def dos_bot() -> DoSBot:
|
||||
computer = Computer(
|
||||
hostname="compromised_pc",
|
||||
ip_address="192.168.0.1",
|
||||
subnet_mask="255.255.255.0",
|
||||
operating_state=NodeOperatingState.ON,
|
||||
hostname="compromised_pc", ip_address="192.168.0.1", subnet_mask="255.255.255.0", start_up_duration=0
|
||||
)
|
||||
|
||||
computer.power_on()
|
||||
computer.software_manager.install(DoSBot)
|
||||
|
||||
dos_bot: DoSBot = computer.software_manager.software.get("DoSBot")
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user