Merge remote-tracking branch 'origin/dev' into feature/2317-refactor-reset

This commit is contained in:
Marek Wolan
2024-02-25 16:41:40 +00:00
124 changed files with 7441 additions and 3680 deletions

View File

@@ -113,6 +113,7 @@ stages:
testRunner: JUnit
testResultsFiles: 'junit/**.xml'
testRunTitle: 'Publish test results'
failTaskOnFailedTests: true
- publish: $(System.DefaultWorkingDirectory)/htmlcov/
# publish the html report - so we can debug the coverage if needed

View File

@@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed a bug where the red agent acted to early
- Fixed the order of service health state
- Fixed an issue where starting a node didn't start the services on it
- Added support for SQL INSERT command.
@@ -63,6 +64,25 @@ 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.
- **Firewall Node**: Introduced the `Firewall` class extending the functionality of the existing `Router` class. The `Firewall` class incorporates advanced features to scrutinize, direct, and filter traffic between various network zones, guided by predefined security rules and policies. Key functionalities include:
- Access Control Lists (ACLs) for traffic filtering based on IP addresses, protocols, and port numbers.
- Network zone segmentation for managing traffic across external, internal, and DMZ (De-Militarized Zone) networks.
- Interface configuration to establish connectivity and define network parameters for external, internal, and DMZ interfaces.
- Protocol and service management to oversee traffic and enforce security policies.
- Dynamic traffic processing and filtering to ensure network security and integrity.
- `AirSpace` class to simulate wireless communications, managing wireless interfaces and facilitating the transmission of frames within specified frequencies.
- `AirSpaceFrequency` enum for defining standard wireless frequencies, including 2.4 GHz and 5 GHz bands, to support realistic wireless network simulations.
- `WirelessRouter` class, extending the `Router` class, to incorporate wireless networking capabilities alongside traditional wired connections. This class allows the configuration of wireless access points with specific IP settings and operating frequencies.
### Changed
- Integrated the RouteTable into the Routers frame processing.
@@ -70,6 +90,10 @@ 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.
- **ACLRule Wildcard Masking**: Updated the `ACLRule` class to support IP ranges using wildcard masking. This enhancement allows for more flexible and granular control over traffic filtering, enabling the specification of broader or more specific IP address ranges in ACL rules.
### Removed
@@ -77,6 +101,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

View File

@@ -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

View File

@@ -17,9 +17,17 @@ 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/nodes/wireless_router
simulation_components/network/nodes/firewall
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

View File

@@ -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.

View File

@@ -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.

View 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.

View File

@@ -0,0 +1,432 @@
.. only:: comment
© Crown-owned copyright 2023, Defence Science and Technology Laboratory UK
########
Firewall
########
The ``firewall.py`` module is a cornerstone in network security within the PrimAITE simulation, designed to simulate
the functionalities of a firewall in monitoring, controlling, and securing network traffic.
Firewall Class
--------------
The ``Firewall`` class extends the ``Router`` class, incorporating advanced capabilities to scrutinise, direct,
and filter traffic between various network zones, guided by predefined security rules and policies.
Key Features
============
- **Access Control Lists (ACLs):** Employs ACLs to establish security rules for permitting or denying traffic
based on IP addresses, protocols, and port numbers, offering detailed oversight of network traffic.
- **Network Zone Segmentation:** Facilitates network division into distinct zones, including internal, external,
and DMZ (De-Militarized Zone), each governed by specific inbound and outbound traffic rules.
- **Interface Configuration:** Enables the configuration of network interfaces for connectivity to external,
internal, and DMZ networks, including setting up IP addressing and subnetting.
- **Protocol and Service Management:** Oversees and filters traffic across different protocols and services,
enforcing adherence to established security policies.
- **Dynamic Traffic Processing:** Actively processes incoming and outgoing traffic via relevant ACLs, determining
whether to forward or block based on the evaluation of rules.
- **Logging and Diagnostics:** Integrates with ``SysLog`` for detailed logging of firewall actions, supporting
security monitoring and incident investigation.
Operations
==========
- **Rule Definition and Management:** Permits the creation and administration of ACL rules for precise traffic
control, enabling the firewall to serve as an effective guard against unauthorised access.
- **Traffic Forwarding and Filtering:** Assesses network frames against ACL rules to allow or block traffic,
forwarding permitted traffic towards its destination whilst obstructing malicious or unauthorised requests.
- **Interface and Zone Configuration:** Provides mechanisms for configuring and managing network interfaces,
aligning with logical network architecture and security zoning requisites.
Configuring Interfaces
======================
To set up firewall interfaces, allocate IP addresses and subnet masks to the external, internal, and DMZ interfaces
using the respective configuration methods:
.. code-block:: python
firewall.configure_external_port(ip_address="10.0.0.1", subnet_mask="255.255.255.0")
firewall.configure_internal_port(ip_address="192.168.1.1", subnet_mask="255.255.255.0")
firewall.configure_dmz_port(ip_address="172.16.0.1", subnet_mask="255.255.255.0")
Firewall ACLs
=============
In the PrimAITE network simulation, six Access Control Lists (ACLs) are crucial for delineating and enforcing
comprehensive network security measures. These ACLs, designated as internal inbound, internal outbound, DMZ inbound,
DMZ outbound, external inbound, and external outbound, each serve a specific role in orchestrating the flow of data
through the network. They allow for meticulous control of traffic entering, exiting, and moving within the network,
ensuring robust protection against unauthorised access and potential cyber threats. By leveraging these ACLs both
individually and collectively, users can simulate a multi-layered security architecture.
Internal Inbound ACL
^^^^^^^^^^^^^^^^^^^^
This ACL controls incoming traffic from the external network and DMZ to the internal network. It's crucial for
preventing unauthorised access to internal resources. By filtering incoming requests, it ensures that only legitimate
and necessary traffic can enter the internal network, protecting sensitive data and systems.
Internal Outbound ACL
^^^^^^^^^^^^^^^^^^^^^
The internal outbound ACL manages traffic leaving the internal network to the external network or DMZ. It can restrict
internal users or systems from accessing potentially harmful external sites or services, mitigate data exfiltration
risks.
DMZ Inbound ACL
^^^^^^^^^^^^^^^
This ACL regulates access to services hosted in the DMZ from the external network and internal network. Since the DMZ
hosts public-facing services like web and email servers, the DMZ inbound ACL is pivotal in allowing necessary access
while blocking malicious or unauthorised attempts, thus serving as a first line of defence.
DMZ Outbound ACL
^^^^^^^^^^^^^^^^
The ACL controls traffic from DMZ to the external network and internal network. It's used to restrict the DMZ services
from initiating unauthorised connections, which is essential for preventing compromised DMZ services from being used
as launchpads for attacks or data exfiltration.
External Inbound ACL
^^^^^^^^^^^^^^^^^^^^
This ACL filters all incoming traffic from the external network towards the internal network or DMZ. It's instrumental
in blocking unwanted or potentially harmful external traffic, ensuring that only traffic conforming to the security
policies is allowed into the network. **This ACL should only be used when the rule applies to both internal and DMZ
networks.**
External Outbound ACL
^^^^^^^^^^^^^^^^^^^^^
This ACL governs traffic leaving the internal network or DMZ to the external network. It plays a critical role in data
loss prevention (DLP) by restricting the types of data and services that internal users and systems can access or
interact with on external networks. **This ACL should only be used when the rule applies to both internal and DMZ
networks.**
Using ACLs Together
^^^^^^^^^^^^^^^^^^^
When these ACLs are used in concert, they create a robust security matrix that controls traffic flow in all directions:
into the internal network, out of the internal network, into the DMZ, out of the DMZ, and between these networks and
the external world. For example, while the external inbound ACL might block all incoming SSH requests to protect both
the internal network and DMZ, the internal outbound ACL could allow SSH access to specific external servers for
management purposes. Simultaneously, the DMZ inbound ACL might permit HTTP and HTTPS traffic to specific servers to
provide access to web services while the DMZ outbound ACL ensures these servers cannot make unauthorised outbound
connections.
By effectively configuring and managing these ACLs, users can establish and experiment with detailed security policies
that are finely tuned to their simulated network's unique requirements and threat models, achieving granular oversight
over traffic flows. This not only enables secure simulated interactions and data exchanges within PrimAITE environments
but also fortifies the virtual network against unauthorised access and cyber threats, mirroring real-world network
security practices.
ACL Configuration Examples
==========================
The subsequent examples provide detailed illustrations on configuring ACL rules within PrimAITE's firewall setup,
addressing various scenarios that encompass external attempts to access resources not only within the internal network
but also within the DMZ. These examples reflect the firewall's specific port configurations and showcase the
versatility and control that ACLs offer in managing network traffic, ensuring that security policies are precisely
enforced. Each example highlights different aspects of ACL usage, from basic traffic filtering to more complex
scenarios involving specific service access and protection against external threats.
**Blocking External Traffic to Internal Network**
To prevent all external traffic from accessing the internal network, with exceptions for approved services:
.. code-block:: python
# Default rule to deny all external traffic to the internal network
firewall.internal_inbound_acl.add_rule(
action=ACLAction.DENY,
src_ip_address="0.0.0.0",
src_wildcard_mask="255.255.255.255",
dst_ip_address="192.168.1.0",
dst_wildcard_mask="0.0.0.255",
position=1
)
# Exception rule to allow HTTP traffic from external to internal network
firewall.internal_inbound_acl.add_rule(
action=ACLAction.PERMIT,
protocol=IPProtocol.TCP,
dst_port=Port.HTTP,
dst_ip_address="192.168.1.0",
dst_wildcard_mask="0.0.0.255",
position=2
)
**Allowing External Access to Specific Services in DMZ**
To enable external traffic to access specific services hosted within the DMZ:
.. code-block:: python
# Allow HTTP and HTTPS traffic to the DMZ
firewall.dmz_inbound_acl.add_rule(
action=ACLAction.PERMIT,
protocol=IPProtocol.TCP,
dst_port=Port.HTTP,
dst_ip_address="172.16.0.0",
dst_wildcard_mask="0.0.0.255",
position=3
)
firewall.dmz_inbound_acl.add_rule(
action=ACLAction.PERMIT,
protocol=IPProtocol.TCP,
dst_port=Port.HTTPS,
dst_ip_address="172.16.0.0",
dst_wildcard_mask="0.0.0.255",
position=4
)
**Edge Case - Permitting External SSH Access to a Specific Internal Server**
To permit SSH access from a designated external IP to a specific server within the internal network:
.. code-block:: python
# Allow SSH from a specific external IP to an internal server
firewall.internal_inbound_acl.add_rule(
action=ACLAction.PERMIT,
protocol=IPProtocol.TCP,
src_ip_address="10.0.0.2",
dst_port=Port.SSH,
dst_ip_address="192.168.1.10",
position=5
)
**Restricting Access to Internal Database Server**
To limit database server access to selected external IP addresses:
.. code-block:: python
# Allow PostgreSQL traffic from an authorized external IP to the internal DB server
firewall.internal_inbound_acl.add_rule(
action=ACLAction.PERMIT,
protocol=IPProtocol.TCP,
src_ip_address="10.0.0.3",
dst_port=Port.POSTGRES_SERVER,
dst_ip_address="192.168.1.20",
position=6
)
# Deny all other PostgreSQL traffic from external sources
firewall.internal_inbound_acl.add_rule(
action=ACLAction.DENY,
protocol=IPProtocol.TCP,
dst_port=Port.POSTGRES_SERVER,
dst_ip_address="192.168.1.0",
dst_wildcard_mask="0.0.0.255",
position=7
)
**Permitting DMZ Web Server Access while Blocking Specific Threats*
To authorize HTTP/HTTPS access to a DMZ-hosted web server, excluding known malicious IPs:
.. code-block:: python
# Deny access from a known malicious IP to any DMZ service
firewall.dmz_inbound_acl.add_rule(
action=ACLAction.DENY,
src_ip_address="10.0.0.4",
dst_ip_address="172.16.0.0",
dst_wildcard_mask="0.0.0.255",
position=8
)
# Allow HTTP/HTTPS traffic to the DMZ web server
firewall.dmz_inbound_acl.add_rule(
action=ACLAction.PERMIT,
protocol=IPProtocol.TCP,
dst_port=Port.HTTP,
dst_ip_address="172.16.0.2",
position=9
)
firewall.dmz_inbound_acl.add_rule(
action=ACLAction.PERMIT,
protocol=IPProtocol.TCP,
dst_port=Port.HTTPS,
dst_ip_address="172.16.0.2",
position=10
)
**Enabling Internal to DMZ Restricted Access**
To facilitate restricted access from the internal network to DMZ-hosted services:
.. code-block:: python
# Permit specific internal application server HTTPS access to a DMZ-hosted API
firewall.internal_outbound_acl.add_rule(
action=ACLAction.PERMIT,
protocol=IPProtocol.TCP,
src_ip_address="192.168.1.30", # Internal application server IP
dst_port=Port.HTTPS,
dst_ip_address="172.16.0.3", # DMZ API server IP
position=11
)
# Deny all other traffic from the internal network to the DMZ
firewall.internal_outbound_acl.add_rule(
action=ACLAction.DENY,
src_ip_address="192.168.1.0",
src_wildcard_mask="0.0.0.255",
dst_ip_address="172.16.0.0",
dst_wildcard_mask="0.0.0.255",
position=12
)
# Corresponding rule in DMZ inbound ACL to allow the traffic from the specific internal server
firewall.dmz_inbound_acl.add_rule(
action=ACLAction.PERMIT,
protocol=IPProtocol.TCP,
src_ip_address="192.168.1.30", # Ensuring this specific source is allowed
dst_port=Port.HTTPS,
dst_ip_address="172.16.0.3", # DMZ API server IP
position=13
)
# Deny all other internal traffic to the specific DMZ API server
firewall.dmz_inbound_acl.add_rule(
action=ACLAction.DENY,
src_ip_address="192.168.1.0",
src_wildcard_mask="0.0.0.255",
dst_port=Port.HTTPS,
dst_ip_address="172.16.0.3", # DMZ API server IP
position=14
)
**Blocking Unwanted External Access**
To block all SSH access attempts from the external network:
.. code-block:: python
# Deny all SSH traffic from any external source
firewall.external_inbound_acl.add_rule(
action=ACLAction.DENY,
protocol=IPProtocol.TCP,
dst_port=Port.SSH,
position=1
)
**Allowing Specific External Communication**
To allow the internal network to initiate HTTP connections to the external network:
.. code-block:: python
# Permit outgoing HTTP traffic from the internal network to any external destination
firewall.external_outbound_acl.add_rule(
action=ACLAction.PERMIT,
protocol=IPProtocol.TCP,
dst_port=Port.HTTP,
position=2
)
The examples above demonstrate the versatility and power of ACLs in crafting nuanced security policies. By combining
rules that specify permitted and denied traffic, both broadly and narrowly defined, administrators can construct
a firewall policy that safeguards network resources while ensuring necessary access is maintained.
Show Rules Function
===================
The show_rules function in the Firewall class displays the configurations of Access Control Lists (ACLs) within a
network firewall. It presents a comprehensive table detailing the rules that govern the filtering and management of
network traffic.
**Functionality:**
This function showcases each rule in an ACL, outlining its:
- **Index**: The rule's position within the ACL.
- **Action**: Specifies whether to permit or deny matching traffic.
- **Protocol**: The network protocol to which the rule applies.
- **Src IP and Dst IP**: Source and destination IP addresses.
- **Src Wildcard and Dst** Wildcard: Wildcard masks for source and destination IP ranges.
- **Src Port and Dst Port**: Source and destination ports.
- **Matched**: The number of times the rule has been matched by traffic.
Example Output:
.. code-block:: text
+---------------------------------------------------------------------------------------------------------------+
| firewall_1 - External Inbound Access Control List |
+-------+--------+----------+--------+--------------+-----------+--------+--------------+-----------+-----------+
| Index | Action | Protocol | Src IP | Src Wildcard | Src Port | Dst IP | Dst Wildcard | Dst Port | Matched |
+-------+--------+----------+--------+--------------+-----------+--------+--------------+-----------+-----------+
| 22 | PERMIT | ANY | ANY | ANY | 219 (ARP) | ANY | ANY | 219 (ARP) | 1 |
| 23 | PERMIT | ICMP | ANY | ANY | ANY | ANY | ANY | ANY | 0 |
| 24 | PERMIT | ANY | ANY | ANY | ANY | ANY | ANY | ANY | 2 |
+-------+--------+----------+--------+--------------+-----------+--------+--------------+-----------+-----------+
+---------------------------------------------------------------------------------------------------------------+
| firewall_1 - External Outbound Access Control List |
+-------+--------+----------+--------+--------------+-----------+--------+--------------+-----------+-----------+
| Index | Action | Protocol | Src IP | Src Wildcard | Src Port | Dst IP | Dst Wildcard | Dst Port | Matched |
+-------+--------+----------+--------+--------------+-----------+--------+--------------+-----------+-----------+
| 22 | PERMIT | ANY | ANY | ANY | 219 (ARP) | ANY | ANY | 219 (ARP) | 0 |
| 23 | PERMIT | ICMP | ANY | ANY | ANY | ANY | ANY | ANY | 0 |
| 24 | PERMIT | ANY | ANY | ANY | ANY | ANY | ANY | ANY | 2 |
+-------+--------+----------+--------+--------------+-----------+--------+--------------+-----------+-----------+
+---------------------------------------------------------------------------------------------------------------+
| firewall_1 - Internal Inbound Access Control List |
+-------+--------+----------+--------+--------------+-----------+--------+--------------+-----------+-----------+
| Index | Action | Protocol | Src IP | Src Wildcard | Src Port | Dst IP | Dst Wildcard | Dst Port | Matched |
+-------+--------+----------+--------+--------------+-----------+--------+--------------+-----------+-----------+
| 1 | PERMIT | ANY | ANY | ANY | 123 (NTP) | ANY | ANY | 123 (NTP) | 1 |
| 22 | PERMIT | ANY | ANY | ANY | 219 (ARP) | ANY | ANY | 219 (ARP) | 0 |
| 23 | PERMIT | ICMP | ANY | ANY | ANY | ANY | ANY | ANY | 0 |
| 24 | DENY | ANY | ANY | ANY | ANY | ANY | ANY | ANY | 0 |
+-------+--------+----------+--------+--------------+-----------+--------+--------------+-----------+-----------+
+---------------------------------------------------------------------------------------------------------------+
| firewall_1 - Internal Outbound Access Control List |
+-------+--------+----------+--------+--------------+-----------+--------+--------------+-----------+-----------+
| Index | Action | Protocol | Src IP | Src Wildcard | Src Port | Dst IP | Dst Wildcard | Dst Port | Matched |
+-------+--------+----------+--------+--------------+-----------+--------+--------------+-----------+-----------+
| 1 | PERMIT | ANY | ANY | ANY | 123 (NTP) | ANY | ANY | 123 (NTP) | 1 |
| 22 | PERMIT | ANY | ANY | ANY | 219 (ARP) | ANY | ANY | 219 (ARP) | 1 |
| 23 | PERMIT | ICMP | ANY | ANY | ANY | ANY | ANY | ANY | 0 |
| 24 | DENY | ANY | ANY | ANY | ANY | ANY | ANY | ANY | 0 |
+-------+--------+----------+--------+--------------+-----------+--------+--------------+-----------+-----------+
+---------------------------------------------------------------------------------------------------------------+
| firewall_1 - DMZ Inbound Access Control List |
+-------+--------+----------+--------+--------------+-----------+--------+--------------+-----------+-----------+
| Index | Action | Protocol | Src IP | Src Wildcard | Src Port | Dst IP | Dst Wildcard | Dst Port | Matched |
+-------+--------+----------+--------+--------------+-----------+--------+--------------+-----------+-----------+
| 1 | PERMIT | ANY | ANY | ANY | 123 (NTP) | ANY | ANY | 123 (NTP) | 1 |
| 22 | PERMIT | ANY | ANY | ANY | 219 (ARP) | ANY | ANY | 219 (ARP) | 0 |
| 23 | PERMIT | ICMP | ANY | ANY | ANY | ANY | ANY | ANY | 0 |
| 24 | DENY | ANY | ANY | ANY | ANY | ANY | ANY | ANY | 0 |
+-------+--------+----------+--------+--------------+-----------+--------+--------------+-----------+-----------+
+---------------------------------------------------------------------------------------------------------------+
| firewall_1 - DMZ Outbound Access Control List |
+-------+--------+----------+--------+--------------+-----------+--------+--------------+-----------+-----------+
| Index | Action | Protocol | Src IP | Src Wildcard | Src Port | Dst IP | Dst Wildcard | Dst Port | Matched |
+-------+--------+----------+--------+--------------+-----------+--------+--------------+-----------+-----------+
| 1 | PERMIT | ANY | ANY | ANY | 123 (NTP) | ANY | ANY | 123 (NTP) | 1 |
| 22 | PERMIT | ANY | ANY | ANY | 219 (ARP) | ANY | ANY | 219 (ARP) | 1 |
| 23 | PERMIT | ICMP | ANY | ANY | ANY | ANY | ANY | ANY | 0 |
| 24 | DENY | ANY | ANY | ANY | ANY | ANY | ANY | ANY | 0 |
+-------+--------+----------+--------+--------------+-----------+--------+--------------+-----------+-----------+
The ``firewall.py`` module within PrimAITE empowers users to accurately model and simulate the pivotal role of
firewalls in network security. It provides detailed command over traffic flow and enforces security policies to safeguard
networked assets.

View File

@@ -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.

View File

@@ -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.

View 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.

View 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.

View File

@@ -0,0 +1,193 @@
.. only:: comment
© Crown-owned copyright 2023, Defence Science and Technology Laboratory UK
######
Router
######
The ``WirelessRouter`` class extends the functionality of the standard ``Router`` class within PrimAITE,
integrating wireless networking capabilities. This class enables the simulation of a router that supports both wired
and wireless connections, allowing for a more comprehensive network simulation environment.
Overview
--------
The ``WirelessRouter`` class is designed to simulate the operations of a real-world wireless router, offering both
Ethernet and Wi-Fi connectivity. This includes managing wireless access points, configuring network interfaces for
different frequencies, and handling wireless frames transmission.
Features
--------
- **Dual Interface Support:** Supports both wired (Ethernet) and wireless network interfaces.
- **Wireless Access Point Configuration:** Allows configuring a wireless access point, including setting its IP
address, subnet mask, and operating frequency.
- **Frequency Management:** Utilises the ``AirSpaceFrequency`` enum to set the operating frequency of wireless
interfaces, supporting common Wi-Fi bands like 2.4 GHz and 5 GHz.
- **Seamless Wireless Communication:** Integrates with the ``AirSpace`` class to manage wireless transmissions across
different frequencies, ensuring that wireless communication is realistically simulated.
Usage
-----
To use the ``WirelessRouter`` class in a network simulation, instantiate it similarly to a regular router but with
additional steps to configure wireless settings:
.. code-block:: python
from primaite.simulator.network.hardware.nodes.network.wireless_router import WirelessRouter
from primaite.simulator.network.airspace import AirSpaceFrequency
# Instantiate the WirelessRouter
wireless_router = WirelessRouter(hostname="MyWirelessRouter")
# Configure a wired Ethernet interface
wireless_router.configure_port(port=2, ip_address="192.168.1.1", subnet_mask="255.255.255.0")
# Configure a wireless access point
wireless_router.configure_wireless_access_point(
port=1, ip_address="192.168.2.1",
subnet_mask="255.255.255.0",
frequency=AirSpaceFrequency.WIFI_2_4
)
Integration with AirSpace
-------------------------
The ``WirelessRouter`` class works closely with the ``AirSpace`` class to simulate the transmission of wireless frames.
Frames sent from wireless interfaces are transmitted across the simulated airspace, allowing for interactions with
other wireless devices within the same frequency band.
Example Scenario
----------------
This example sets up a network with two PCs (PC A and PC B), each connected to their own `WirelessRouter`
(Router 1 and Router 2). These routers are then wirelessly connected to each other, enabling communication between the
PCs through the routers over the airspace. Access Control Lists (ACLs) are configured on the routers to permit ARP and
ICMP traffic, ensuring basic network connectivity and ping functionality.
.. code-block:: python
from primaite.simulator.network.airspace import AIR_SPACE, AirSpaceFrequency
from primaite.simulator.network.container import Network
from primaite.simulator.network.hardware.nodes.host.computer import Computer
from primaite.simulator.network.hardware.nodes.network.router import ACLAction
from primaite.simulator.network.hardware.nodes.network.wireless_router import WirelessRouter
from primaite.simulator.network.transmission.network_layer import IPProtocol
from primaite.simulator.network.transmission.transport_layer import Port
network = Network()
# Configure PC A
pc_a = Computer(
hostname="pc_a",
ip_address="192.168.0.2",
subnet_mask="255.255.255.0",
default_gateway="192.168.0.1",
start_up_duration=0,
)
pc_a.power_on()
network.add_node(pc_a)
# Configure Router 1
router_1 = WirelessRouter(hostname="router_1", start_up_duration=0)
router_1.power_on()
network.add_node(router_1)
# Configure the connection between PC A and Router 1 port 2
router_1.configure_router_interface("192.168.0.1", "255.255.255.0")
network.connect(pc_a.network_interface[1], router_1.router_interface)
# Configure Router 1 ACLs
router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.ARP, dst_port=Port.ARP, position=22)
router_1.acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol.ICMP, position=23)
# Configure PC B
pc_b = Computer(
hostname="pc_b",
ip_address="192.168.2.2",
subnet_mask="255.255.255.0",
default_gateway="192.168.2.1",
start_up_duration=0,
)
pc_b.power_on()
network.add_node(pc_b)
# Configure Router 2
router_2 = WirelessRouter(hostname="router_2", start_up_duration=0)
router_2.power_on()
network.add_node(router_2)
# Configure the connection between PC B and Router 2 port 2
router_2.configure_router_interface("192.168.2.1", "255.255.255.0")
network.connect(pc_b.network_interface[1], router_2.router_interface)
# Configure the wireless connection between Router 1 and Router 2
router_1.configure_wireless_access_point(
port=1,
ip_address="192.168.1.1",
subnet_mask="255.255.255.0",
frequency=AirSpaceFrequency.WIFI_2_4
)
router_2.configure_wireless_access_point(
port=1,
ip_address="192.168.1.2",
subnet_mask="255.255.255.0",
frequency=AirSpaceFrequency.WIFI_2_4
)
# Configure routes for inter-router communication
router_1.route_table.add_route(
address="192.168.2.0", subnet_mask="255.255.255.0", next_hop_ip_address="192.168.1.2"
)
router_2.route_table.add_route(
address="192.168.0.0", subnet_mask="255.255.255.0", next_hop_ip_address="192.168.1.1"
)
# Test connectivity
print(pc_a.ping(pc_b.network_interface[1].ip_address))
print(pc_b.ping(pc_a.network_interface[1].ip_address))
This setup demonstrates the `WirelessRouter` class's capability to manage both wired and wireless connections within a
simulated network environment. By configuring the wireless access points and enabling the appropriate ACL rules, the
example facilitates basic network operations such as ARP resolution and ICMP pinging between devices across different
network segments.
Viewing Wireless Network Configuration
--------------------------------------
The `AirSpace.show()` function is an invaluable tool for inspecting the current wireless network configuration within
the PrimAITE environment. It presents a table summarising all wireless interfaces, including routers and access points,
that are active within the airspace. The table outlines each device's connected node name, MAC address, IP address,
subnet mask, operating frequency, and status, providing a comprehensive view of the wireless network topology.
Example Output
^^^^^^^^^^^^^^^
Below is an example output of the `AirSpace.show()` function, demonstrating the visibility it provides into the
wireless network:
.. code-block:: none
+----------------+-------------------+-------------+---------------+--------------+---------+
| Connected Node | MAC Address | IP Address | Subnet Mask | Frequency | Status |
+----------------+-------------------+-------------+---------------+--------------+---------+
| router_1 | 31:29:46:53:ed:f8 | 192.168.1.1 | 255.255.255.0 | WiFi 2.4 GHz | Enabled |
| router_2 | 34:c8:47:43:98:78 | 192.168.1.2 | 255.255.255.0 | WiFi 2.4 GHz | Enabled |
+----------------+-------------------+-------------+---------------+--------------+---------+
This table aids in verifying that wireless devices are correctly configured and operational. It also helps in
diagnosing connectivity issues by ensuring that devices are on the correct frequency and have the appropriate network
settings. The `Status` column, indicating whether a device is enabled or disabled, further assists in troubleshooting
by quickly identifying any devices that are not actively participating in the network.
Utilising the `AirSpace.show()` function is particularly beneficial in complex network simulations where multiple
wireless devices are in use. It provides a snapshot of the wireless landscape, facilitating the understanding of how
devices interact within the network and ensuring that configurations are aligned with the intended network architecture.
The addition of the ``WirelessRouter`` class enriches the PrimAITE simulation toolkit by enabling the simulation of
mixed wired and wireless network environments.

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

@@ -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.

View File

@@ -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")

View File

@@ -17,7 +17,7 @@ Key capabilities
- Creates a database file in the ``Node`` 's ``FileSystem`` upon creation.
- Handles connecting clients by maintaining a dictionary of connections mapped to session IDs.
- Authenticates connections using a configurable password.
- Simulates ``SELECT`` and ``DELETE`` SQL queries.
- Simulates ``SELECT``, ``DELETE`` and ``INSERT`` SQL queries.
- Returns query results and status codes back to clients.
- Leverages the Service base class for install/uninstall, status tracking, etc.

View File

@@ -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

View File

@@ -22,7 +22,7 @@ Key capabilities
Usage
^^^^^
- Install on a Node via the ``SoftwareManager`` to start the database service.
- Service runs on TCP port 123 by default.
- Service runs on UDP port 123 by default.
Implementation
^^^^^^^^^^^^^^
@@ -44,7 +44,7 @@ Usage
^^^^^
- Install on a Node via the ``SoftwareManager`` to start the database service.
- Service runs on TCP port 123 by default.
- Service runs on UDP port 123 by default.
Implementation
^^^^^^^^^^^^^^

View 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.

View File

@@ -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.

View 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.

View File

@@ -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
^^^^^^^^^^^^^^^^^^^^^^

View File

@@ -693,7 +693,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

View File

@@ -1073,7 +1073,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

View File

@@ -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(

View File

@@ -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"]

View File

@@ -68,6 +68,8 @@ class DummyReward(AbstractReward):
:param config: dict of options for the reward component's constructor. Should be empty.
:type config: dict
:return: The reward component.
:rtype: DummyReward
"""
return cls()
@@ -230,7 +232,12 @@ class WebpageUnavailablePenalty(AbstractReward):
@classmethod
def from_config(cls, config: dict) -> AbstractReward:
"""Build the reward component object from config."""
"""
Build the reward component object from config.
:param config: Configuration dictionary.
:type config: Dict
"""
node_hostname = config.get("node_hostname")
return cls(node_hostname=node_hostname)

View File

@@ -11,14 +11,17 @@ 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
from primaite.simulator.system.applications.red_applications.data_manipulation_bot import DataManipulationBot
from primaite.simulator.system.applications.red_applications.dos_bot import DoSBot
from primaite.simulator.system.applications.web_browser import WebBrowser
from primaite.simulator.system.services.database.database_service import DatabaseService
from primaite.simulator.system.services.dns.dns_client import DNSClient
@@ -31,6 +34,24 @@ from primaite.simulator.system.services.web_server.web_server import WebServer
_LOGGER = getLogger(__name__)
APPLICATION_TYPES_MAPPING = {
"WebBrowser": WebBrowser,
"DatabaseClient": DatabaseClient,
"DataManipulationBot": DataManipulationBot,
"DoSBot": DoSBot,
}
SERVICE_TYPES_MAPPING = {
"DNSClient": DNSClient,
"DNSServer": DNSServer,
"DatabaseService": DatabaseService,
"WebServer": WebServer,
"FTPClient": FTPClient,
"FTPServer": FTPServer,
"NTPClient": NTPClient,
"NTPServer": NTPServer,
}
class PrimaiteGameOptions(BaseModel):
"""
@@ -231,54 +252,48 @@ class PrimaiteGame:
new_service = None
service_ref = service_cfg["ref"]
service_type = service_cfg["type"]
service_types_mapping = {
"DNSClient": DNSClient, # key is equal to the 'name' attr of the service class itself.
"DNSServer": DNSServer,
"DatabaseClient": DatabaseClient,
"DatabaseService": DatabaseService,
"WebServer": WebServer,
"FTPClient": FTPClient,
"FTPServer": FTPServer,
"NTPClient": NTPClient,
"NTPServer": NTPServer,
}
if service_type in service_types_mapping:
if service_type in SERVICE_TYPES_MAPPING:
_LOGGER.debug(f"installing {service_type} on node {new_node.hostname}")
new_node.software_manager.install(service_types_mapping[service_type])
new_node.software_manager.install(SERVICE_TYPES_MAPPING[service_type])
new_service = new_node.software_manager.software[service_type]
game.ref_map_services[service_ref] = new_service.uuid
else:
_LOGGER.warning(f"service type not found {service_type}")
# service-dependent options
if service_type == "DatabaseClient":
if service_type == "DNSClient":
if "options" in service_cfg:
opt = service_cfg["options"]
if "db_server_ip" in opt:
new_service.configure(server_ip_address=IPv4Address(opt["db_server_ip"]))
if "dns_server" in opt:
new_service.dns_server = IPv4Address(opt["dns_server"])
if service_type == "DNSServer":
if "options" in service_cfg:
opt = service_cfg["options"]
if "domain_mapping" in opt:
for domain, ip in opt["domain_mapping"].items():
new_service.dns_register(domain, ip)
new_service.dns_register(domain, IPv4Address(ip))
if service_type == "DatabaseService":
if "options" in service_cfg:
opt = service_cfg["options"]
if "backup_server_ip" in opt:
new_service.configure_backup(backup_server=IPv4Address(opt["backup_server_ip"]))
new_service.configure_backup(backup_server=IPv4Address(opt.get("backup_server_ip")))
new_service.start()
if service_type == "FTPServer":
if "options" in service_cfg:
opt = service_cfg["options"]
new_service.server_password = opt.get("server_password")
new_service.start()
if service_type == "NTPClient":
if "options" in service_cfg:
opt = service_cfg["options"]
new_service.ntp_server = IPv4Address(opt.get("ntp_server_ip"))
new_service.start()
if "applications" in node_cfg:
for application_cfg in node_cfg["applications"]:
new_application = None
application_ref = application_cfg["ref"]
application_type = application_cfg["type"]
application_types_mapping = {
"WebBrowser": WebBrowser,
"DataManipulationBot": DataManipulationBot,
}
if application_type in application_types_mapping:
new_node.software_manager.install(application_types_mapping[application_type])
if application_type in APPLICATION_TYPES_MAPPING:
new_node.software_manager.install(APPLICATION_TYPES_MAPPING[application_type])
new_application = new_node.software_manager.software[application_type]
game.ref_map_applications[application_ref] = new_application.uuid
else:
@@ -294,12 +309,32 @@ class PrimaiteGame:
port_scan_p_of_success=float(opt.get("port_scan_p_of_success", "0.1")),
data_manipulation_p_of_success=float(opt.get("data_manipulation_p_of_success", "0.1")),
)
elif application_type == "DatabaseClient":
if "options" in application_cfg:
opt = application_cfg["options"]
new_application.configure(
server_ip_address=IPv4Address(opt.get("db_server_ip")),
server_password=opt.get("server_password"),
)
elif application_type == "WebBrowser":
if "options" in application_cfg:
opt = application_cfg["options"]
new_application.target_url = opt.get("target_url")
if "nics" in node_cfg:
for nic_num, nic_cfg in node_cfg["nics"].items():
elif application_type == "DoSBot":
if "options" in application_cfg:
opt = application_cfg["options"]
new_application.configure(
target_ip_address=IPv4Address(opt.get("target_ip_address")),
target_port=Port(opt.get("target_port", Port.POSTGRES_SERVER.value)),
payload=opt.get("payload"),
repeat=bool(opt.get("repeat")),
port_scan_p_of_success=float(opt.get("port_scan_p_of_success", "0.1")),
dos_intensity=float(opt.get("dos_intensity", "1.0")),
max_sessions=int(opt.get("max_sessions", "1000")),
)
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)
@@ -311,13 +346,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

View File

@@ -127,7 +127,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",
@@ -181,7 +181,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",
@@ -314,7 +314,8 @@
"The blue agent's reward is calculated using two measures:\n",
"1. Whether the database file is in a good state (+1 for good, -1 for corrupted, 0 for any other state)\n",
"2. Whether each green agents' most recent webpage request was successful (+1 for a `200` return code, -1 for a `404` return code and 0 otherwise).\n",
"The file status reward and the two green-agent-related reward are averaged to get a total step reward.\n"
"\n",
"The file status reward and the two green-agent-related rewards are averaged to get a total step reward.\n"
]
},
{
@@ -334,7 +335,9 @@
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"%load_ext autoreload\n",
@@ -344,7 +347,9 @@
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"# Imports\n",
@@ -367,7 +372,9 @@
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"# create the env\n",
@@ -415,7 +422,9 @@
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"for step in range(35):\n",
@@ -433,7 +442,9 @@
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"pprint(obs['NODES'])"
@@ -449,7 +460,9 @@
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"obs, reward, terminated, truncated, info = env.step(9) # scan database file\n",
@@ -475,7 +488,9 @@
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"obs, reward, terminated, truncated, info = env.step(13) # patch the database\n",
@@ -500,7 +515,9 @@
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"obs, reward, terminated, truncated, info = env.step(0) # patch the database\n",
@@ -523,7 +540,9 @@
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"env.step(13) # Patch the database\n",
@@ -573,7 +592,7 @@
],
"metadata": {
"kernelspec": {
"display_name": "venv",
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
@@ -587,9 +606,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
}

View File

@@ -12,8 +12,8 @@ class _SimOutput:
self._path: Path = (
_PRIMAITE_ROOT.parent.parent / "simulation_output" / datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
)
self.save_pcap_logs: bool = False
self.save_sys_logs: bool = False
self.save_pcap_logs: bool = True
self.save_sys_logs: bool = True
@property
def path(self) -> Path:

View File

@@ -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,12 +150,10 @@ 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."""
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

View File

@@ -0,0 +1,307 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from enum import Enum
from typing import Any, Dict, Final, List, Optional
from prettytable import PrettyTable
from primaite import getLogger
from primaite.simulator.network.hardware.base import Layer3Interface, NetworkInterface, WiredNetworkInterface
from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState
from primaite.simulator.network.transmission.data_link_layer import Frame
from primaite.simulator.system.core.packet_capture import PacketCapture
_LOGGER = getLogger(__name__)
__all__ = ["AIR_SPACE", "AirSpaceFrequency", "WirelessNetworkInterface", "IPWirelessNetworkInterface"]
class AirSpace:
"""Represents a wireless airspace, managing wireless network interfaces and handling wireless transmission."""
def __init__(self):
self._wireless_interfaces: Dict[str, WirelessNetworkInterface] = {}
self._wireless_interfaces_by_frequency: Dict[AirSpaceFrequency, List[WirelessNetworkInterface]] = {}
def show(self, frequency: Optional[AirSpaceFrequency] = None):
"""
Displays a summary of wireless interfaces in the airspace, optionally filtered by a specific frequency.
:param frequency: The frequency band to filter devices by. If None, devices for all frequencies are shown.
"""
table = PrettyTable()
table.field_names = ["Connected Node", "MAC Address", "IP Address", "Subnet Mask", "Frequency", "Status"]
# If a specific frequency is provided, filter by it; otherwise, use all frequencies.
frequencies_to_show = [frequency] if frequency else self._wireless_interfaces_by_frequency.keys()
for freq in frequencies_to_show:
interfaces = self._wireless_interfaces_by_frequency.get(freq, [])
for interface in interfaces:
status = "Enabled" if interface.enabled else "Disabled"
table.add_row(
[
interface._connected_node.hostname, # noqa
interface.mac_address,
interface.ip_address if hasattr(interface, "ip_address") else None,
interface.subnet_mask if hasattr(interface, "subnet_mask") else None,
str(freq),
status,
]
)
print(table)
def add_wireless_interface(self, wireless_interface: WirelessNetworkInterface):
"""
Adds a wireless network interface to the airspace if it's not already present.
:param wireless_interface: The wireless network interface to be added.
"""
if wireless_interface.mac_address not in self._wireless_interfaces:
self._wireless_interfaces[wireless_interface.mac_address] = wireless_interface
if wireless_interface.frequency not in self._wireless_interfaces_by_frequency:
self._wireless_interfaces_by_frequency[wireless_interface.frequency] = []
self._wireless_interfaces_by_frequency[wireless_interface.frequency].append(wireless_interface)
def remove_wireless_interface(self, wireless_interface: WirelessNetworkInterface):
"""
Removes a wireless network interface from the airspace if it's present.
:param wireless_interface: The wireless network interface to be removed.
"""
if wireless_interface.mac_address in self._wireless_interfaces:
self._wireless_interfaces.pop(wireless_interface.mac_address)
self._wireless_interfaces_by_frequency[wireless_interface.frequency].remove(wireless_interface)
def clear(self):
"""
Clears all wireless network interfaces and their frequency associations from the airspace.
After calling this method, the airspace will contain no wireless network interfaces, and transmissions cannot
occur until new interfaces are added again.
"""
self._wireless_interfaces.clear()
self._wireless_interfaces_by_frequency.clear()
def transmit(self, frame: Frame, sender_network_interface: WirelessNetworkInterface):
"""
Transmits a frame to all enabled wireless network interfaces on a specific frequency within the airspace.
This ensures that a wireless interface does not receive its own transmission.
:param frame: The frame to be transmitted.
:param sender_network_interface: The wireless network interface sending the frame. This interface will be
excluded from the list of receivers to prevent it from receiving its own transmission.
"""
for wireless_interface in self._wireless_interfaces_by_frequency.get(sender_network_interface.frequency, []):
if wireless_interface != sender_network_interface and wireless_interface.enabled:
wireless_interface.receive_frame(frame)
AIR_SPACE: Final[AirSpace] = AirSpace()
"""
A singleton instance of the AirSpace class, representing the global wireless airspace.
This instance acts as the central management point for all wireless communications within the simulated network
environment. By default, there is only one airspace in the simulation, making this variable a singleton that
manages the registration, removal, and transmission of wireless frames across all wireless network interfaces configured
in the simulation. It ensures that wireless frames are appropriately transmitted to and received by wireless
interfaces based on their operational status and frequency band.
"""
class AirSpaceFrequency(Enum):
"""Enumeration representing the operating frequencies for wireless communications."""
WIFI_2_4 = 2.4e9
"""WiFi 2.4 GHz. Known for its extensive range and ability to penetrate solid objects effectively."""
WIFI_5 = 5e9
"""WiFi 5 GHz. Known for its higher data transmission speeds and reduced interference from other devices."""
def __str__(self) -> str:
if self == AirSpaceFrequency.WIFI_2_4:
return "WiFi 2.4 GHz"
elif self == AirSpaceFrequency.WIFI_5:
return "WiFi 5 GHz"
else:
return "Unknown Frequency"
class WirelessNetworkInterface(NetworkInterface, ABC):
"""
Represents a wireless network interface in a network device.
This abstract base class models wireless network interfaces, encapsulating properties and behaviors specific to
wireless connectivity. It provides a framework for managing wireless connections, including signal strength,
security protocols, and other wireless-specific attributes and methods.
Wireless network interfaces differ from wired ones in their medium of communication, relying on radio frequencies
for data transmission and reception. This class serves as a base for more specific types of wireless network
interfaces, such as Wi-Fi adapters or radio network interfaces, ensuring that essential wireless functionality is
defined and standardised.
Inherits from:
- NetworkInterface: Provides basic network interface properties and methods.
As an abstract base class, it requires subclasses to implement specific methods related to wireless communication
and may define additional properties and methods specific to wireless technology.
"""
frequency: AirSpaceFrequency = AirSpaceFrequency.WIFI_2_4
def enable(self):
"""Attempt to enable the network interface."""
if self.enabled:
return
if not self._connected_node:
_LOGGER.error(f"Interface {self} cannot be enabled as it is not connected to a Node")
return
if self._connected_node.operating_state != NodeOperatingState.ON:
self._connected_node.sys_log.info(
f"Interface {self} cannot be enabled as the connected Node is not powered on"
)
return
self.enabled = True
self._connected_node.sys_log.info(f"Network Interface {self} enabled")
self.pcap = PacketCapture(hostname=self._connected_node.hostname, interface_num=self.port_num)
AIR_SPACE.add_wireless_interface(self)
def disable(self):
"""Disable the network interface."""
if not self.enabled:
return
self.enabled = False
if self._connected_node:
self._connected_node.sys_log.info(f"Network Interface {self} disabled")
else:
_LOGGER.debug(f"Interface {self} disabled")
AIR_SPACE.remove_wireless_interface(self)
def send_frame(self, frame: Frame) -> bool:
"""
Attempts to send a network frame over the airspace.
This method sends a frame if the network interface is enabled and connected to a wireless airspace. It captures
the frame using PCAP (if available) and transmits it through the airspace. Returns True if the frame is
successfully sent, False otherwise (e.g., if the network interface is disabled).
:param frame: The network frame to be sent.
:return: True if the frame is sent successfully, False if the network interface is disabled.
"""
if self.enabled:
frame.set_sent_timestamp()
self.pcap.capture_outbound(frame)
AIR_SPACE.transmit(frame, self)
return True
# Cannot send Frame as the network interface is not enabled
return False
@abstractmethod
def receive_frame(self, frame: Frame) -> bool:
"""
Receives a network frame on the network interface.
:param frame: The network frame being received.
:return: A boolean indicating whether the frame was successfully received.
"""
pass
class IPWirelessNetworkInterface(WirelessNetworkInterface, Layer3Interface, ABC):
"""
Represents an IP wireless network interface.
This interface operates at both the data link layer (Layer 2) and the network layer (Layer 3) of the OSI model,
specifically tailored for IP-based communication over wireless connections. This abstract class provides a
template for creating specific wireless network interfaces that support Internet Protocol (IP) functionalities.
As this class is a combination of its parent classes without additional attributes or methods, please refer to
the documentation of `WirelessNetworkInterface` and `Layer3Interface` for more details on the supported operations
and functionalities.
The class inherits from:
- `WirelessNetworkInterface`: Providing the functionalities and characteristics of a wireless connection, such as
managing wireless signal transmission, reception, and associated wireless protocols.
- `Layer3Interface`: Enabling network layer capabilities, including IP address assignment, routing, and
potentially, Layer 3 protocols like IPsec.
As an abstract class, `IPWirelessNetworkInterface` does not implement specific methods but ensures that any derived
class provides implementations for the functionalities of both `WirelessNetworkInterface` and `Layer3Interface`.
This setup is ideal for representing network interfaces in devices that require wireless connections and are capable
of IP routing and addressing, such as wireless routers, access points, and wireless end-host devices like
smartphones and laptops.
This class should be extended by concrete classes that define specific behaviors and properties of an IP-capable
wireless network interface.
"""
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 WiredNetworkInterface
state = WiredNetworkInterface.describe_state(self)
# Update the state with information from Layer3Interface
state.update(Layer3Interface.describe_state(self))
state["frequency"] = self.frequency
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 enable(self):
"""
Enables this wired network interface and attempts to send a "hello" message to the default gateway.
This method activates the network interface, making it operational for network communications. After enabling,
it tries to initiate a default gateway "hello" process, typically to establish initial connectivity and resolve
the default gateway's MAC address. This step is crucial for ensuring the interface can successfully send data
to and receive data from the network.
The method safely handles cases where the connected node might not have a default gateway set or the
`default_gateway_hello` method is not defined, ignoring such errors to proceed without interruption.
"""
super().enable()
try:
self._connected_node.default_gateway_hello()
except AttributeError:
pass
@abstractmethod
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

View File

@@ -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
@@ -55,8 +55,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):
@@ -141,8 +141,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:
@@ -202,8 +203,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
@@ -264,18 +265,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

View File

@@ -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

View File

@@ -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}"

View File

@@ -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}"

View File

@@ -1,55 +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
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)
# Web Browser
self.software_manager.install(WebBrowser)
super()._install_system_software()

View File

@@ -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

View 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

View File

@@ -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
"""

View File

@@ -0,0 +1,493 @@
from typing import Dict, Final, Optional, Union
from prettytable import MARKDOWN, PrettyTable
from pydantic import validate_call
from primaite.simulator.network.hardware.nodes.network.router import (
AccessControlList,
ACLAction,
Router,
RouterInterface,
)
from primaite.simulator.network.transmission.data_link_layer import Frame
from primaite.simulator.system.core.sys_log import SysLog
from primaite.utils.validators import IPV4Address
EXTERNAL_PORT_ID: Final[int] = 1
"""The Firewall port ID of the external port."""
INTERNAL_PORT_ID: Final[int] = 2
"""The Firewall port ID of the internal port."""
DMZ_PORT_ID: Final[int] = 3
"""The Firewall port ID of the DMZ port."""
class Firewall(Router):
"""
A Firewall class that extends the functionality of a Router.
The Firewall class acts as a network security system that monitors and controls incoming and outgoing
network traffic based on predetermined security rules. It is an intermediary between internal and external
networks (including DMZ - De-Militarized Zone), ensuring that all inbound and outbound traffic complies with
the security policies.
The Firewall employs Access Control Lists (ACLs) to filter traffic. Both the internal and DMZ ports have both
inbound and outbound ACLs that determine what traffic is allowed to pass.
In addition to the security functions, the Firewall can also perform some routing functions similar to a Router,
forwarding packets between its interfaces based on the destination IP address.
Usage:
To utilise the Firewall class, instantiate it with a hostname and optionally specify sys_log for logging.
Configure the internal, external, and DMZ ports with IP addresses and subnet masks. Define ACL rules to
permit or deny traffic based on your security policies. The Firewall will process frames based on these
rules, determining whether to allow or block traffic at each network interface.
Example:
>>> from primaite.simulator.network.transmission.network_layer import IPProtocol
>>> from primaite.simulator.network.transmission.transport_layer import Port
>>> firewall = Firewall(hostname="Firewall1")
>>> firewall.configure_internal_port(ip_address="192.168.1.1", subnet_mask="255.255.255.0")
>>> firewall.configure_external_port(ip_address="10.0.0.1", subnet_mask="255.255.255.0")
>>> firewall.configure_dmz_port(ip_address="172.16.0.1", subnet_mask="255.255.255.0")
>>> # Permit HTTP traffic to the DMZ
>>> firewall.dmz_inbound_acl.add_rule(
... action=ACLAction.PERMIT,
... protocol=IPProtocol.TCP,
... dst_port=Port.HTTP,
... src_ip_address="0.0.0.0",
... src_wildcard_mask="0.0.0.0",
... dst_ip_address="172.16.0.0",
... dst_wildcard_mask="0.0.0.255"
... )
:ivar str hostname: The Firewall hostname.
"""
internal_inbound_acl: Optional[AccessControlList] = None
"""Access Control List for managing entering the internal network."""
internal_outbound_acl: Optional[AccessControlList] = None
"""Access Control List for managing traffic leaving the internal network."""
dmz_inbound_acl: Optional[AccessControlList] = None
"""Access Control List for managing traffic entering the DMZ."""
dmz_outbound_acl: Optional[AccessControlList] = None
"""Access Control List for managing traffic leaving the DMZ."""
external_inbound_acl: Optional[AccessControlList] = None
"""Access Control List for managing traffic entering from an external network."""
external_outbound_acl: Optional[AccessControlList] = None
"""Access Control List for managing traffic leaving towards an external network."""
def __init__(self, hostname: str, **kwargs):
if not kwargs.get("sys_log"):
kwargs["sys_log"] = SysLog(hostname)
super().__init__(hostname=hostname, num_ports=3, **kwargs)
# Initialise ACLs for internal and dmz interfaces with a default DENY policy
self.internal_inbound_acl = AccessControlList(
sys_log=kwargs["sys_log"], implicit_action=ACLAction.DENY, name=f"{hostname} - Internal Inbound"
)
self.internal_outbound_acl = AccessControlList(
sys_log=kwargs["sys_log"], implicit_action=ACLAction.DENY, name=f"{hostname} - Internal Outbound"
)
self.dmz_inbound_acl = AccessControlList(
sys_log=kwargs["sys_log"], implicit_action=ACLAction.DENY, name=f"{hostname} - DMZ Inbound"
)
self.dmz_outbound_acl = AccessControlList(
sys_log=kwargs["sys_log"], implicit_action=ACLAction.DENY, name=f"{hostname} - DMZ Outbound"
)
# external ACLs should have a default PERMIT policy
self.external_inbound_acl = AccessControlList(
sys_log=kwargs["sys_log"], implicit_action=ACLAction.PERMIT, name=f"{hostname} - External Inbound"
)
self.external_outbound_acl = AccessControlList(
sys_log=kwargs["sys_log"], implicit_action=ACLAction.PERMIT, name=f"{hostname} - External Outbound"
)
self.set_original_state()
def set_original_state(self):
"""Set the original state for the Firewall."""
super().set_original_state()
vals_to_include = {
"internal_port",
"external_port",
"dmz_port",
"internal_inbound_acl",
"internal_outbound_acl",
"dmz_inbound_acl",
"dmz_outbound_acl",
"external_inbound_acl",
"external_outbound_acl",
}
self._original_state.update(self.model_dump(include=vals_to_include))
def describe_state(self) -> Dict:
"""
Describes the current state of the Firewall.
:return: A dictionary representing the current state.
"""
state = super().describe_state()
state.update(
{
"internal_port": self.internal_port.describe_state(),
"external_port": self.external_port.describe_state(),
"dmz_port": self.dmz_port.describe_state(),
"internal_inbound_acl": self.internal_inbound_acl.describe_state(),
"internal_outbound_acl": self.internal_outbound_acl.describe_state(),
"dmz_inbound_acl": self.dmz_inbound_acl.describe_state(),
"dmz_outbound_acl": self.dmz_outbound_acl.describe_state(),
"external_inbound_acl": self.external_inbound_acl.describe_state(),
"external_outbound_acl": self.external_outbound_acl.describe_state(),
}
)
return state
def show(self, markdown: bool = False):
"""
Displays the current configuration of the firewall's network interfaces in a table format.
The table includes information about each port (External, Internal, DMZ), their MAC addresses, IP
configurations, link speeds, and operational status. The output can be formatted as Markdown if specified.
:param markdown: If True, formats the output table in Markdown style. Useful for documentation or reporting
purposes within Markdown-compatible platforms.
"""
table = PrettyTable(["Port", "MAC Address", "Address", "Speed", "Status"])
if markdown:
table.set_style(MARKDOWN)
table.align = "l"
table.title = f"{self.hostname} Network Interfaces"
ports = {"External": self.external_port, "Internal": self.internal_port, "DMZ": self.dmz_port}
for port, network_interface in ports.items():
table.add_row(
[
port,
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)
def show_rules(self, external: bool = True, internal: bool = True, dmz: bool = True, markdown: bool = False):
"""
Prints the configured ACL rules for each specified network zone of the firewall.
This method allows selective viewing of ACL rules applied to external, internal, and DMZ interfaces, providing
a clear overview of the firewall's current traffic filtering policies. Each section can be independently
toggled.
:param external: If True, shows ACL rules for external interfaces.
:param internal: If True, shows ACL rules for internal interfaces.
:param dmz: If True, shows ACL rules for DMZ interfaces.
:param markdown: If True, formats the output in Markdown, enhancing readability in Markdown-compatible viewers.
"""
print(f"{self.hostname} Firewall Rules")
print()
if external:
self.external_inbound_acl.show(markdown)
print()
self.external_outbound_acl.show(markdown)
print()
if internal:
self.internal_inbound_acl.show(markdown)
print()
self.internal_outbound_acl.show(markdown)
print()
if dmz:
self.dmz_inbound_acl.show(markdown)
print()
self.dmz_outbound_acl.show(markdown)
print()
def receive_frame(self, frame: Frame, from_network_interface: RouterInterface):
"""
Receive a frame and process it.
Acts as the primary entry point for all network frames arriving at the Firewall, determining the flow of
traffic based on the source network interface controller (NIC) and applying the appropriate Access Control
List (ACL) rules.
This method categorizes the incoming traffic into three main pathways based on the source NIC: external inbound,
internal outbound, and DMZ (De-Militarized Zone) outbound. It plays a crucial role in enforcing the firewall's
security policies by directing each frame to the corresponding processing method that evaluates it against
specific ACL rules.
Based on the originating NIC:
- Frames from the external port are processed as external inbound traffic, potentially destined for either the
DMZ or the internal network.
- Frames from the internal port are treated as internal outbound traffic, aimed at reaching the external
network or a service within the DMZ.
- Frames from the DMZ port are handled as DMZ outbound traffic, with potential destinations including the
internal network or the external network.
:param frame: The network frame to be processed.
:param from_network_interface: The network interface controller from which the frame is coming. Used to
determine the direction of the traffic (inbound or outbound) and the zone (external, internal,
DMZ) it belongs to.
"""
# If the frame comes from the external port, it's considered as external inbound traffic
if from_network_interface == self.external_port:
self._process_external_inbound_frame(frame, from_network_interface)
return
# If the frame comes from the internal port, it's considered as internal outbound traffic
elif from_network_interface == self.internal_port:
self._process_internal_outbound_frame(frame, from_network_interface)
return
# If the frame comes from the DMZ port, it's considered as DMZ outbound traffic
elif from_network_interface == self.dmz_port:
self._process_dmz_outbound_frame(frame, from_network_interface)
return
def _process_external_inbound_frame(self, frame: Frame, from_network_interface: RouterInterface) -> None:
"""
Process frames arriving from the external network.
Determines the path for frames based on their destination IP addresses and ACL rules for the external inbound
interface. Frames destined for the DMZ or internal network are forwarded accordingly, if allowed by the ACL.
If a frame is permitted by the ACL, it is either passed to the session manager (if applicable) or forwarded to
the appropriate network zone (DMZ/internal). Denied frames are logged and dropped.
:param frame: The frame to be processed, containing network layer and transport layer information.
:param from_network_interface: The interface on the firewall through which the frame was received.
"""
# check if External Inbound ACL Rules permit frame
permitted, rule = self.external_inbound_acl.is_permitted(frame)
if not permitted:
self.sys_log.info(f"Frame blocked at interface {from_network_interface} by rule {rule}")
return
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,
)
if self.check_send_frame_to_session_manager(frame):
# 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 the destination IP is within the DMZ network, process the frame as DMZ inbound
if frame.ip.dst_ip_address in self.dmz_port.ip_network:
self._process_dmz_inbound_frame(frame, from_network_interface)
else:
# Otherwise, process the frame as internal inbound
self._process_internal_inbound_frame(frame, from_network_interface)
def _process_external_outbound_frame(self, frame: Frame, from_network_interface: RouterInterface) -> None:
"""
Process frames that are outbound towards the external network.
:param frame: The frame to be processed.
:param from_network_interface: The network interface controller from which the frame is coming.
:param re_attempt: Indicates if the processing is a re-attempt, defaults to False.
"""
# check if External Outbound ACL Rules permit frame
permitted, rule = self.external_outbound_acl.is_permitted(frame=frame)
if not permitted:
self.sys_log.info(f"Frame blocked at interface {from_network_interface} by rule {rule}")
return
self.process_frame(frame=frame, from_network_interface=from_network_interface)
def _process_internal_inbound_frame(self, frame: Frame, from_network_interface: RouterInterface) -> None:
"""
Process frames that are inbound towards the internal LAN.
This method is responsible for handling frames coming from either the external network or the DMZ towards
the internal LAN. It checks the frames against the internal inbound ACL to decide whether to allow or deny
the traffic, and take appropriate actions.
:param frame: The frame to be processed.
:param from_network_interface: The network interface controller from which the frame is coming.
:param re_attempt: Indicates if the processing is a re-attempt, defaults to False.
"""
# check if Internal Inbound ACL Rules permit frame
permitted, rule = self.internal_inbound_acl.is_permitted(frame=frame)
if not permitted:
self.sys_log.info(f"Frame blocked at interface {from_network_interface} by rule {rule}")
return
self.process_frame(frame=frame, from_network_interface=from_network_interface)
def _process_internal_outbound_frame(self, frame: Frame, from_network_interface: RouterInterface) -> None:
"""
Process frames that are outbound from the internal network.
This method handles frames that are leaving the internal network. Depending on the destination IP address,
the frame may be forwarded to the DMZ or to the external network.
:param frame: The frame to be processed.
:param from_network_interface: The network interface controller from which the frame is coming.
:param re_attempt: Indicates if the processing is a re-attempt, defaults to False.
"""
permitted, rule = self.internal_outbound_acl.is_permitted(frame)
if not permitted:
self.sys_log.info(f"Frame blocked at interface {from_network_interface} by rule {rule}")
return
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,
)
if self.check_send_frame_to_session_manager(frame):
# 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 the destination IP is within the DMZ network, process the frame as DMZ inbound
if frame.ip.dst_ip_address in self.dmz_port.ip_network:
self._process_dmz_inbound_frame(frame, from_network_interface)
else:
# If the destination IP is not within the DMZ network, process the frame as external outbound
self._process_external_outbound_frame(frame, from_network_interface)
def _process_dmz_inbound_frame(self, frame: Frame, from_network_interface: RouterInterface) -> None:
"""
Process frames that are inbound from the DMZ.
This method is responsible for handling frames coming from either the external network or the internal LAN
towards the DMZ. It checks the frames against the DMZ inbound ACL to decide whether to allow or deny the
traffic, and take appropriate actions.
:param frame: The frame to be processed.
:param from_network_interface: The network interface controller from which the frame is coming.
:param re_attempt: Indicates if the processing is a re-attempt, defaults to False.
"""
# check if DMZ Inbound ACL Rules permit frame
permitted, rule = self.dmz_inbound_acl.is_permitted(frame=frame)
if not permitted:
self.sys_log.info(f"Frame blocked at interface {from_network_interface} by rule {rule}")
return
self.process_frame(frame=frame, from_network_interface=from_network_interface)
def _process_dmz_outbound_frame(self, frame: Frame, from_network_interface: RouterInterface) -> None:
"""
Process frames that are outbound from the DMZ.
This method handles frames originating from the DMZ and determines their appropriate path based on the
destination IP address. It involves checking the DMZ outbound ACL, consulting the ARP cache and the routing
table to find the correct outbound NIC, and then forwarding the frame to either the internal network or the
external network.
:param frame: The frame to be processed.
:param from_network_interface: The network interface controller from which the frame is coming.
:param re_attempt: Indicates if the processing is a re-attempt, defaults to False.
"""
permitted, rule = self.dmz_outbound_acl.is_permitted(frame)
if not permitted:
self.sys_log.info(f"Frame blocked at interface {from_network_interface} by rule {rule}")
return
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,
)
if self.check_send_frame_to_session_manager(frame):
# Port is open on this Router so pass Frame up to session manager first
self.session_manager.receive_frame(frame, from_network_interface)
else:
# Attempt to get the outbound NIC from the ARP cache using the destination IP address
outbound_nic = self.software_manager.arp.get_arp_cache_network_interface(frame.ip.dst_ip_address)
# If outbound NIC is not found in the ARP cache, consult the routing table to find the best route
if not outbound_nic:
route = self.route_table.find_best_route(frame.ip.dst_ip_address)
if route:
# If a route is found, get the corresponding outbound NIC from the ARP cache using the next-hop IP
# address
outbound_nic = self.software_manager.arp.get_arp_cache_network_interface(route.next_hop_ip_address)
# If an outbound NIC is determined
if outbound_nic:
if outbound_nic == self.external_port:
# If the outbound NIC is the external port, check the frame against the DMZ outbound ACL and
# process it as an external outbound frame
self._process_external_outbound_frame(frame, from_network_interface)
return
elif outbound_nic == self.internal_port:
# If the outbound NIC is the internal port, check the frame against the DMZ outbound ACL and
# process it as an internal inbound frame
self._process_internal_inbound_frame(frame, from_network_interface)
return
# TODO: What to do here? Destination unreachable? Send ICMP back?
return
@property
def external_port(self) -> RouterInterface:
"""
The external port of the firewall.
:return: The external port connecting the firewall to the external network.
"""
return self.network_interface[EXTERNAL_PORT_ID]
@validate_call()
def configure_external_port(self, ip_address: Union[IPV4Address, str], subnet_mask: Union[IPV4Address, str]):
"""
Configure the external port with an IP address and a subnet mask.
Enables the port once configured.
:param ip_address: The IP address to assign to the external port.
:param subnet_mask: The subnet mask to assign to the external port.
"""
# Configure the external port with the specified IP address and subnet mask
self.configure_port(EXTERNAL_PORT_ID, ip_address, subnet_mask)
self.external_port.enable()
@property
def internal_port(self) -> RouterInterface:
"""
The internal port of the firewall.
:return: The external port connecting the firewall to the internal LAN.
"""
return self.network_interface[INTERNAL_PORT_ID]
@validate_call()
def configure_internal_port(self, ip_address: Union[IPV4Address, str], subnet_mask: Union[IPV4Address, str]):
"""
Configure the internal port with an IP address and a subnet mask.
Enables the port once configured.
:param ip_address: The IP address to assign to the internal port.
:param subnet_mask: The subnet mask to assign to the internal port.
"""
self.configure_port(INTERNAL_PORT_ID, ip_address, subnet_mask)
self.internal_port.enable()
@property
def dmz_port(self) -> RouterInterface:
"""
The DMZ port of the firewall.
:return: The external port connecting the firewall to the DMZ.
"""
return self.network_interface[DMZ_PORT_ID]
@validate_call()
def configure_dmz_port(self, ip_address: Union[IPV4Address, str], subnet_mask: Union[IPV4Address, str]):
"""
Configure the DMZ port with an IP address and a subnet mask.
Enables the port once configured.
:param ip_address: The IP address to assign to the DMZ port.
:param subnet_mask: The subnet mask to assign to the DMZ port.
"""
self.configure_port(DMZ_PORT_ID, ip_address, subnet_mask)
self.dmz_port.enable()

View File

@@ -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

File diff suppressed because it is too large Load Diff

View 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()

View File

@@ -0,0 +1,211 @@
from typing import Any, Dict, Union
from pydantic import validate_call
from primaite.simulator.network.airspace import AirSpaceFrequency, IPWirelessNetworkInterface
from primaite.simulator.network.hardware.nodes.network.router import Router, RouterInterface
from primaite.simulator.network.transmission.data_link_layer import Frame
from primaite.utils.validators import IPV4Address
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 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
"""
return super().describe_state()
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
frame.set_received_timestamp()
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_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} ({self.frequency})"
class WirelessRouter(Router):
"""
A WirelessRouter class that extends the functionality of a standard Router to include wireless capabilities.
This class represents a network device that performs routing functions similar to a traditional router but also
includes the functionality of a wireless access point. This allows the WirelessRouter to not only direct traffic
between wired networks but also to manage and facilitate wireless network connections.
A WirelessRouter is instantiated and configured with both wired and wireless interfaces. The wired interfaces are
managed similarly to those in a standard Router, while the wireless interfaces require additional configuration
specific to wireless settings, such as setting the frequency band (e.g., 2.4 GHz or 5 GHz for Wi-Fi).
The WirelessRouter facilitates creating a network environment where devices can be interconnected via both
Ethernet (wired) and Wi-Fi (wireless), making it an essential component for simulating more complex and realistic
network topologies within PrimAITE.
Example:
>>> wireless_router = WirelessRouter(hostname="wireless_router_1")
>>> wireless_router.configure_router_interface(
... ip_address="192.168.1.1",
... subnet_mask="255.255.255.0"
... )
>>> wireless_router.configure_wireless_access_point(
... ip_address="10.10.10.1",
... subnet_mask="255.255.255.0"
... frequency=AirSpaceFrequency.WIFI_2_4
... )
"""
network_interfaces: Dict[str, Union[RouterInterface, WirelessAccessPoint]] = {}
network_interface: Dict[int, Union[RouterInterface, WirelessAccessPoint]] = {}
def __init__(self, hostname: str, **kwargs):
super().__init__(hostname=hostname, num_ports=0, **kwargs)
self.connect_nic(WirelessAccessPoint(ip_address="127.0.0.1", subnet_mask="255.0.0.0", gateway="0.0.0.0"))
self.connect_nic(RouterInterface(ip_address="127.0.0.1", subnet_mask="255.0.0.0", gateway="0.0.0.0"))
self.set_original_state()
@property
def wireless_access_point(self) -> WirelessAccessPoint:
"""
Retrieves the wireless access point interface associated with this wireless router.
This property provides direct access to the WirelessAccessPoint interface of the router, facilitating wireless
communications. Specifically, it returns the interface configured on port 1, dedicated to establishing and
managing wireless network connections. This interface is essential for enabling wireless connectivity,
allowing devices within connect to the network wirelessly.
:return: The WirelessAccessPoint instance representing the wireless connection interface on port 1 of the
wireless router.
"""
return self.network_interface[1]
@validate_call()
def configure_wireless_access_point(
self,
ip_address: IPV4Address,
subnet_mask: IPV4Address,
frequency: AirSpaceFrequency = AirSpaceFrequency.WIFI_2_4,
):
"""
Configures a wireless access point (WAP).
Sets its IP address, subnet mask, and operating frequency. This method ensures the wireless access point is
properly set up to manage wireless communication over the specified frequency band.
The method first disables the WAP to safely apply configuration changes. After configuring the IP settings,
it sets the WAP to operate on the specified frequency band and then re-enables the WAP for operation.
:param ip_address: The IP address to be assigned to the wireless access point.
:param subnet_mask: The subnet mask associated with the IP address
:param frequency: The operating frequency of the wireless access point, defined by the AirSpaceFrequency
enum. This determines the frequency band (e.g., 2.4 GHz or 5 GHz) the access point will use for wireless
communication. Default is AirSpaceFrequency.WIFI_2_4.
"""
self.wireless_access_point.disable() # Temporarily disable the WAP for reconfiguration
network_interface = self.network_interface[1]
network_interface.ip_address = ip_address
network_interface.subnet_mask = subnet_mask
self.sys_log.info(f"Configured WAP {network_interface}")
self.set_original_state()
self.wireless_access_point.frequency = frequency # Set operating frequency
self.wireless_access_point.enable() # Re-enable the WAP with new settings
@property
def router_interface(self) -> RouterInterface:
"""
Retrieves the router interface associated with this wireless router.
This property provides access to the router interface configured for wired connections. It specifically
returns the interface configured on port 2, which is reserved for wired LAN/WAN connections.
:return: The RouterInterface instance representing the wired LAN/WAN connection on port 2 of the wireless
router.
"""
return self.network_interface[2]
@validate_call()
def configure_router_interface(
self,
ip_address: IPV4Address,
subnet_mask: IPV4Address,
):
"""
Configures a router interface.
Sets its IP address and subnet mask.
The method first disables the router interface to safely apply configuration changes. After configuring the IP
settings, it re-enables the router interface for operation.
:param ip_address: The IP address to be assigned to the router interface.
:param subnet_mask: The subnet mask associated with the IP address
"""
self.router_interface.disable() # Temporarily disable the router interface for reconfiguration
super().configure_port(port=2, ip_address=ip_address, subnet_mask=subnet_mask) # Set IP configuration
self.router_interface.enable() # Re-enable the router interface with new settings
def configure_port(self, port: int, ip_address: Union[IPV4Address, str], subnet_mask: Union[IPV4Address, str]):
"""Not Implemented."""
raise NotImplementedError(
"Please use the 'configure_wireless_access_point' and 'configure_router_interface' functions."
)

File diff suppressed because it is too large Load Diff

View File

@@ -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()

View File

@@ -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)

View File

@@ -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):

View 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)

View File

@@ -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

View File

@@ -1,12 +1,9 @@
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
from primaite.utils.validators import IPV4Address
_LOGGER = getLogger(__name__)
@@ -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.
@@ -180,9 +73,9 @@ class IPPacket(BaseModel):
... )
"""
src_ip_address: IPv4Address
src_ip_address: IPV4Address
"Source IP address."
dst_ip_address: IPv4Address
dst_ip_address: IPV4Address
"Destination IP address."
protocol: IPProtocol = IPProtocol.TCP
"IPProtocol."
@@ -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)

View File

@@ -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

View File

@@ -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):
@@ -59,9 +60,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,
@@ -133,6 +135,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:
"""

View File

@@ -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

View File

@@ -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):

View File

@@ -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)

View File

@@ -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)

View 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

View File

@@ -170,7 +170,7 @@ class DatabaseService(Service):
}
def _process_sql(
self, query: Literal["SELECT", "DELETE"], query_id: str, connection_id: Optional[str] = None
self, query: Literal["SELECT", "DELETE", "INSERT"], query_id: str, connection_id: Optional[str] = None
) -> Dict[str, Union[int, List[Any]]]:
"""
Executes the given SQL query and returns the result.
@@ -178,6 +178,7 @@ class DatabaseService(Service):
Possible queries:
- SELECT : returns the data
- DELETE : deletes the data
- INSERT : inserts the data
:param query: The SQL query to be executed.
:return: Dictionary containing status code and data fetched.
@@ -201,9 +202,27 @@ class DatabaseService(Service):
return {"status_code": 404, "data": False}
elif query == "DELETE":
self.db_file.health_status = FileSystemItemHealthStatus.COMPROMISED
return {"status_code": 200, "type": "sql", "data": False, "uuid": query_id, "connection_id": connection_id}
return {
"status_code": 200,
"type": "sql",
"data": False,
"uuid": query_id,
"connection_id": connection_id,
}
elif query == "INSERT":
if self.health_state_actual == SoftwareHealthState.GOOD:
return {
"status_code": 200,
"type": "sql",
"data": False,
"uuid": query_id,
"connection_id": connection_id,
}
else:
return {"status_code": 404, "data": False}
else:
# Invalid query
self.sys_log.info(f"{self.name}: Invalid {query}")
return {"status_code": 500, "data": False}
def describe_state(self) -> Dict:

View 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

View 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)

View File

@@ -21,7 +21,7 @@ class NTPClient(Service):
def __init__(self, **kwargs):
kwargs["name"] = "NTPClient"
kwargs["port"] = Port.NTP
kwargs["protocol"] = IPProtocol.TCP
kwargs["protocol"] = IPProtocol.UDP
super().__init__(**kwargs)
self.start()
@@ -99,9 +99,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:
"""

View File

@@ -16,7 +16,7 @@ class NTPServer(Service):
def __init__(self, **kwargs):
kwargs["name"] = "NTPServer"
kwargs["port"] = Port.NTP
kwargs["protocol"] = IPProtocol.TCP
kwargs["protocol"] = IPProtocol.UDP
super().__init__(**kwargs)
self.start()
@@ -59,5 +59,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

View File

@@ -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
@@ -229,6 +230,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."
@@ -334,6 +337,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:
"""
@@ -353,7 +357,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

View 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.
"""

View File

@@ -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

View File

@@ -0,0 +1,148 @@
training_config:
rl_framework: SB3
rl_algorithm: PPO
seed: 333
n_learn_episodes: 1
n_eval_episodes: 5
max_steps_per_episode: 128
deterministic_eval: false
n_agents: 1
agent_references:
- defender
io_settings:
save_checkpoints: true
checkpoint_interval: 5
save_step_metadata: false
save_pcap_logs: true
save_sys_logs: true
game:
max_episode_length: 256
ports:
- ARP
- DNS
- HTTP
- POSTGRES_SERVER
protocols:
- ICMP
- TCP
- UDP
agents:
- ref: client_2_green_user
team: GREEN
type: GreenWebBrowsingAgent
observation_space:
type: UC2GreenObservation
action_space:
action_list:
- type: DONOTHING
- type: NODE_APPLICATION_EXECUTE
options:
nodes:
- node_name: client_2
applications:
- application_name: WebBrowser
max_folders_per_node: 1
max_files_per_folder: 1
max_services_per_node: 1
max_applications_per_node: 1
reward_function:
reward_components:
- type: DUMMY
agent_settings:
start_settings:
start_step: 5
frequency: 4
variance: 3
simulation:
network:
nodes:
- ref: switch_1
type: switch
hostname: switch_1
num_ports: 8
- ref: client_1
type: computer
hostname: client_1
ip_address: 192.168.10.21
subnet_mask: 255.255.255.0
default_gateway: 192.168.10.1
dns_server: 192.168.1.10
applications:
- ref: client_1_web_browser
type: WebBrowser
options:
target_url: http://arcd.com/users/
- ref: client_1_database_client
type: DatabaseClient
options:
db_server_ip: 192.168.1.10
server_password: arcd
- ref: data_manipulation_bot
type: DataManipulationBot
options:
port_scan_p_of_success: 0.8
data_manipulation_p_of_success: 0.8
payload: "DELETE"
server_ip: 192.168.1.21
server_password: arcd
- ref: dos_bot
type: DoSBot
options:
target_ip_address: 192.168.10.21
payload: SPOOF DATA
port_scan_p_of_success: 0.8
services:
- ref: client_1_dns_client
type: DNSClient
options:
dns_server: 192.168.1.10
- ref: client_1_dns_server
type: DNSServer
options:
domain_mapping:
arcd.com: 192.168.1.10
- ref: client_1_database_service
type: DatabaseService
options:
backup_server_ip: 192.168.1.10
- ref: client_1_web_service
type: WebServer
- ref: client_1_ftp_server
type: FTPServer
options:
server_password: arcd
- ref: client_1_ntp_client
type: NTPClient
options:
ntp_server_ip: 192.168.1.10
- ref: client_1_ntp_server
type: NTPServer
- ref: client_2
type: computer
hostname: client_2
ip_address: 192.168.10.22
subnet_mask: 255.255.255.0
default_gateway: 192.168.10.1
dns_server: 192.168.1.10
# pre installed services and applications
links:
- ref: switch_1___client_1
endpoint_a_ref: switch_1
endpoint_a_port: 1
endpoint_b_ref: client_1
endpoint_b_port: 1
- ref: switch_1___client_2
endpoint_a_ref: switch_1
endpoint_a_port: 2
endpoint_b_ref: client_2
endpoint_b_port: 1

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -0,0 +1,220 @@
from ipaddress import IPv4Address
from pathlib import Path
from typing import Union
import yaml
from primaite.config.load import example_config_path
from primaite.game.agent.data_manipulation_bot import DataManipulationAgent
from primaite.game.agent.interface import ProxyAgent, RandomAgent
from primaite.game.game import APPLICATION_TYPES_MAPPING, PrimaiteGame, SERVICE_TYPES_MAPPING
from primaite.simulator.network.container import Network
from primaite.simulator.network.hardware.nodes.host.computer import Computer
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.applications.red_applications.dos_bot import DoSBot
from primaite.simulator.system.applications.web_browser import WebBrowser
from primaite.simulator.system.services.database.database_service import DatabaseService
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.ftp.ftp_client import FTPClient
from primaite.simulator.system.services.ftp.ftp_server import FTPServer
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.web_server.web_server import WebServer
from tests import TEST_ASSETS_ROOT
BASIC_CONFIG = TEST_ASSETS_ROOT / "configs/basic_switched_network.yaml"
def load_config(config_path: Union[str, Path]) -> PrimaiteGame:
"""Returns a PrimaiteGame object which loads the contents of a given yaml path."""
with open(config_path, "r") as f:
cfg = yaml.safe_load(f)
return PrimaiteGame.from_config(cfg)
def test_example_config():
"""Test that the example config can be parsed properly."""
game = load_config(example_config_path())
assert len(game.agents) == 4 # red, blue and 2 green agents
# green agent 1
assert game.agents[0].agent_name == "client_2_green_user"
assert isinstance(game.agents[0], RandomAgent)
# green agent 2
assert game.agents[1].agent_name == "client_1_green_user"
assert isinstance(game.agents[1], RandomAgent)
# red agent
assert game.agents[2].agent_name == "client_1_data_manipulation_red_bot"
assert isinstance(game.agents[2], DataManipulationAgent)
# blue agent
assert game.agents[3].agent_name == "defender"
assert isinstance(game.agents[3], ProxyAgent)
network: Network = game.simulation.network
assert len(network.nodes) == 10 # 10 nodes in example network
assert len(network.routers) == 1 # 1 router in network
assert len(network.switches) == 2 # 2 switches in network
assert len(network.servers) == 5 # 5 servers in network
def test_node_software_install():
"""Test that software can be installed on a node."""
game = load_config(BASIC_CONFIG)
client_1: Computer = game.simulation.network.get_node_by_hostname("client_1")
client_2: Computer = game.simulation.network.get_node_by_hostname("client_2")
system_software = {DNSClient, FTPClient, NTPClient, WebBrowser}
# check that system software is installed on client 1
for software in system_software:
assert client_1.software_manager.software.get(software.__name__) is not None
# check that system software is installed on client 2
for software in system_software:
assert client_2.software_manager.software.get(software.__name__) is not None
# check that applications have been installed on client 1
for applications in APPLICATION_TYPES_MAPPING:
assert client_1.software_manager.software.get(applications) is not None
# check that services have been installed on client 1
for service in SERVICE_TYPES_MAPPING:
assert client_1.software_manager.software.get(service) is not None
def test_web_browser_install():
"""Test that the web browser can be configured via config."""
game = load_config(BASIC_CONFIG)
client_1: Computer = game.simulation.network.get_node_by_hostname("client_1")
web_browser: WebBrowser = client_1.software_manager.software.get("WebBrowser")
assert web_browser.target_url == "http://arcd.com/users/"
def test_database_client_install():
"""Test that the Database Client service can be configured via config."""
game = load_config(BASIC_CONFIG)
client_1: Computer = game.simulation.network.get_node_by_hostname("client_1")
database_client: DatabaseClient = client_1.software_manager.software.get("DatabaseClient")
assert database_client.server_ip_address == IPv4Address("192.168.1.10")
assert database_client.server_password == "arcd"
def test_data_manipulation_bot_install():
"""Test that the data manipulation bot can be configured via config."""
game = load_config(BASIC_CONFIG)
client_1: Computer = game.simulation.network.get_node_by_hostname("client_1")
data_manipulation_bot: DataManipulationBot = client_1.software_manager.software.get("DataManipulationBot")
assert data_manipulation_bot.server_ip_address == IPv4Address("192.168.1.21")
assert data_manipulation_bot.payload == "DELETE"
assert data_manipulation_bot.data_manipulation_p_of_success == 0.8
assert data_manipulation_bot.port_scan_p_of_success == 0.8
assert data_manipulation_bot.server_password == "arcd"
def test_dos_bot_install():
"""Test that the denial of service bot can be configured via config."""
game = load_config(BASIC_CONFIG)
client_1: Computer = game.simulation.network.get_node_by_hostname("client_1")
dos_bot: DoSBot = client_1.software_manager.software.get("DoSBot")
assert dos_bot.target_ip_address == IPv4Address("192.168.10.21")
assert dos_bot.payload == "SPOOF DATA"
assert dos_bot.port_scan_p_of_success == 0.8
assert dos_bot.dos_intensity == 1.0 # default
assert dos_bot.max_sessions == 1000 # default
assert dos_bot.repeat is False # default
def test_dns_client_install():
"""Test that the DNS Client service can be configured via config."""
game = load_config(BASIC_CONFIG)
client_1: Computer = game.simulation.network.get_node_by_hostname("client_1")
dns_client: DNSClient = client_1.software_manager.software.get("DNSClient")
assert dns_client.dns_server == IPv4Address("192.168.1.10")
def test_dns_server_install():
"""Test that the DNS Client service can be configured via config."""
game = load_config(BASIC_CONFIG)
client_1: Computer = game.simulation.network.get_node_by_hostname("client_1")
dns_server: DNSServer = client_1.software_manager.software.get("DNSServer")
assert dns_server.dns_lookup("arcd.com") == IPv4Address("192.168.1.10")
def test_database_service_install():
"""Test that the Database Service can be configured via config."""
game = load_config(BASIC_CONFIG)
client_1: Computer = game.simulation.network.get_node_by_hostname("client_1")
database_service: DatabaseService = client_1.software_manager.software.get("DatabaseService")
assert database_service.backup_server_ip == IPv4Address("192.168.1.10")
def test_web_server_install():
"""Test that the Web Server Service can be configured via config."""
game = load_config(BASIC_CONFIG)
client_1: Computer = game.simulation.network.get_node_by_hostname("client_1")
web_server_service: WebServer = client_1.software_manager.software.get("WebServer")
# config should have also installed database client - web server service should be able to retrieve this
assert web_server_service.software_manager.software.get("DatabaseClient") is not None
def test_ftp_client_install():
"""Test that the FTP Client Service can be configured via config."""
game = load_config(BASIC_CONFIG)
client_1: Computer = game.simulation.network.get_node_by_hostname("client_1")
ftp_client_service: FTPClient = client_1.software_manager.software.get("FTPClient")
assert ftp_client_service is not None
def test_ftp_server_install():
"""Test that the FTP Server Service can be configured via config."""
game = load_config(BASIC_CONFIG)
client_1: Computer = game.simulation.network.get_node_by_hostname("client_1")
ftp_server_service: FTPServer = client_1.software_manager.software.get("FTPServer")
assert ftp_server_service is not None
assert ftp_server_service.server_password == "arcd"
def test_ntp_client_install():
"""Test that the NTP Client Service can be configured via config."""
game = load_config(BASIC_CONFIG)
client_1: Computer = game.simulation.network.get_node_by_hostname("client_1")
ntp_client_service: NTPClient = client_1.software_manager.software.get("NTPClient")
assert ntp_client_service is not None
assert ntp_client_service.ntp_server == IPv4Address("192.168.1.10")
def test_ntp_server_install():
"""Test that the NTP Server Service can be configured via config."""
game = load_config(BASIC_CONFIG)
client_1: Computer = game.simulation.network.get_node_by_hostname("client_1")
ntp_server_service: NTPServer = client_1.software_manager.software.get("NTPServer")
assert ntp_server_service is not None

View File

@@ -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")

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,280 @@
from ipaddress import IPv4Address
import pytest
from primaite.simulator.network.container import Network
from primaite.simulator.network.hardware.nodes.host.computer import Computer
from primaite.simulator.network.hardware.nodes.network.firewall import Firewall
from primaite.simulator.network.hardware.nodes.network.router import ACLAction
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
from primaite.simulator.system.services.ntp.ntp_server import NTPServer
@pytest.fixture(scope="function")
def dmz_external_internal_network() -> Network:
"""
Fixture for setting up a simulated network with a firewall, external node, internal node, and DMZ node. This
configuration is designed to test firewall rules and their impact on traffic between these network segments.
-------------- -------------- --------------
| external |---------| firewall |---------| internal |
-------------- -------------- --------------
|
|
---------
| DMZ |
---------
The network is set up as follows:
- An external node simulates an entity outside the organization's network.
- An internal node represents a device within the organization's LAN.
- A DMZ (Demilitarized Zone) node acts as a server or service exposed to external traffic.
- A firewall node controls traffic between these nodes based on ACL (Access Control List) rules.
The firewall is configured to allow ICMP and ARP traffic across all interfaces to ensure basic connectivity
for the tests. Specific tests will modify ACL rules to test various traffic filtering scenarios.
:return: A `Network` instance with the described nodes and configurations.
"""
network = Network()
firewall_node: Firewall = Firewall(hostname="firewall_1", start_up_duration=0)
firewall_node.power_on()
# configure firewall ports
firewall_node.configure_external_port(
ip_address=IPv4Address("192.168.10.1"), subnet_mask=IPv4Address("255.255.255.0")
)
firewall_node.configure_dmz_port(ip_address=IPv4Address("192.168.1.1"), subnet_mask=IPv4Address("255.255.255.0"))
firewall_node.configure_internal_port(
ip_address=IPv4Address("192.168.0.1"), subnet_mask=IPv4Address("255.255.255.0")
)
# Allow ICMP
firewall_node.internal_inbound_acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol.ICMP, position=23)
firewall_node.internal_outbound_acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol.ICMP, position=23)
firewall_node.external_inbound_acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol.ICMP, position=23)
firewall_node.external_outbound_acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol.ICMP, position=23)
firewall_node.dmz_inbound_acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol.ICMP, position=23)
firewall_node.dmz_outbound_acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol.ICMP, position=23)
# Allow ARP
firewall_node.internal_inbound_acl.add_rule(
action=ACLAction.PERMIT, src_port=Port.ARP, dst_port=Port.ARP, position=22
)
firewall_node.internal_outbound_acl.add_rule(
action=ACLAction.PERMIT, src_port=Port.ARP, dst_port=Port.ARP, position=22
)
firewall_node.external_inbound_acl.add_rule(
action=ACLAction.PERMIT, src_port=Port.ARP, dst_port=Port.ARP, position=22
)
firewall_node.external_outbound_acl.add_rule(
action=ACLAction.PERMIT, src_port=Port.ARP, dst_port=Port.ARP, position=22
)
firewall_node.dmz_inbound_acl.add_rule(action=ACLAction.PERMIT, src_port=Port.ARP, dst_port=Port.ARP, position=22)
firewall_node.dmz_outbound_acl.add_rule(action=ACLAction.PERMIT, src_port=Port.ARP, dst_port=Port.ARP, position=22)
# external node
external_node = Computer(
hostname="external_node",
ip_address="192.168.10.2",
subnet_mask="255.255.255.0",
default_gateway="192.168.10.1",
start_up_duration=0,
)
external_node.power_on()
external_node.software_manager.install(NTPServer)
ntp_service: NTPServer = external_node.software_manager.software["NTPServer"]
ntp_service.start()
# connect external node to firewall node
network.connect(endpoint_b=external_node.network_interface[1], endpoint_a=firewall_node.external_port)
# internal node
internal_node = Computer(
hostname="internal_node",
ip_address="192.168.0.2",
subnet_mask="255.255.255.0",
default_gateway="192.168.0.1",
start_up_duration=0,
)
internal_node.power_on()
internal_node.software_manager.install(NTPClient)
internal_ntp_client: NTPClient = internal_node.software_manager.software["NTPClient"]
internal_ntp_client.configure(external_node.network_interface[1].ip_address)
internal_ntp_client.start()
# connect external node to firewall node
network.connect(endpoint_b=internal_node.network_interface[1], endpoint_a=firewall_node.internal_port)
# dmz node
dmz_node = Computer(
hostname="dmz_node",
ip_address="192.168.1.2",
subnet_mask="255.255.255.0",
default_gateway="192.168.1.1",
start_up_duration=0,
)
dmz_node.power_on()
dmz_ntp_client: NTPClient = dmz_node.software_manager.software["NTPClient"]
dmz_ntp_client.configure(external_node.network_interface[1].ip_address)
dmz_ntp_client.start()
# connect external node to firewall node
network.connect(endpoint_b=dmz_node.network_interface[1], endpoint_a=firewall_node.dmz_port)
return network
def test_firewall_can_ping_nodes(dmz_external_internal_network):
"""
Tests the firewall's ability to ping the external, internal, and DMZ nodes in the network.
Verifies that the firewall has connectivity to all nodes within the network by performing a ping operation.
Successful pings indicate proper network setup and basic ICMP traffic passage through the firewall.
"""
firewall = dmz_external_internal_network.get_node_by_hostname("firewall_1")
# ping from the firewall
assert firewall.ping("192.168.0.2") # firewall to internal
assert firewall.ping("192.168.1.2") # firewall to dmz
assert firewall.ping("192.168.10.2") # firewall to external
def test_nodes_can_ping_default_gateway(dmz_external_internal_network):
"""
Checks if the external, internal, and DMZ nodes can ping their respective default gateways.
This test confirms that each node is correctly configured with a route to its default gateway and that the
firewall permits ICMP traffic for these basic connectivity checks.
"""
external_node = dmz_external_internal_network.get_node_by_hostname("external_node")
internal_node = dmz_external_internal_network.get_node_by_hostname("internal_node")
dmz_node = dmz_external_internal_network.get_node_by_hostname("dmz_node")
assert internal_node.ping(internal_node.default_gateway) # default gateway internal
assert dmz_node.ping(dmz_node.default_gateway) # default gateway dmz
assert external_node.ping(external_node.default_gateway) # default gateway external
def test_nodes_can_ping_default_gateway_on_another_subnet(dmz_external_internal_network):
"""
Verifies that nodes can ping default gateways located in a different subnet, facilitated by the firewall.
This test assesses the routing and firewall ACL configurations that allow ICMP traffic between different
network segments, ensuring that nodes can reach default gateways outside their local subnet.
"""
external_node = dmz_external_internal_network.get_node_by_hostname("external_node")
internal_node = dmz_external_internal_network.get_node_by_hostname("internal_node")
dmz_node = dmz_external_internal_network.get_node_by_hostname("dmz_node")
assert internal_node.ping(external_node.default_gateway) # internal node to external default gateway
assert internal_node.ping(dmz_node.default_gateway) # internal node to dmz default gateway
assert dmz_node.ping(internal_node.default_gateway) # dmz node to internal default gateway
assert dmz_node.ping(external_node.default_gateway) # dmz node to external default gateway
assert external_node.ping(external_node.default_gateway) # external node to internal default gateway
assert external_node.ping(dmz_node.default_gateway) # external node to dmz default gateway
def test_nodes_can_ping_each_other(dmz_external_internal_network):
"""
Evaluates the ability of each node (external, internal, DMZ) to ping the other nodes within the network.
This comprehensive connectivity test checks if the firewall's current ACL configuration allows for inter-node
communication via ICMP pings, highlighting the effectiveness of the firewall rules in place.
"""
external_node = dmz_external_internal_network.get_node_by_hostname("external_node")
internal_node = dmz_external_internal_network.get_node_by_hostname("internal_node")
dmz_node = dmz_external_internal_network.get_node_by_hostname("dmz_node")
# test that nodes can ping each other
assert internal_node.ping(external_node.network_interface[1].ip_address)
assert internal_node.ping(dmz_node.network_interface[1].ip_address)
assert external_node.ping(internal_node.network_interface[1].ip_address)
assert external_node.ping(dmz_node.network_interface[1].ip_address)
assert dmz_node.ping(internal_node.network_interface[1].ip_address)
assert dmz_node.ping(external_node.network_interface[1].ip_address)
def test_service_blocked(dmz_external_internal_network):
"""
Tests the firewall's default blocking stance on NTP service requests from internal and DMZ nodes.
Initially, without specific ACL rules to allow NTP traffic, this test confirms that NTP clients on both the
internal and DMZ nodes are unable to update their time, demonstrating the firewall's effective blocking of
unspecified services.
"""
firewall = dmz_external_internal_network.get_node_by_hostname("firewall_1")
internal_node = dmz_external_internal_network.get_node_by_hostname("internal_node")
dmz_node = dmz_external_internal_network.get_node_by_hostname("dmz_node")
internal_ntp_client: NTPClient = internal_node.software_manager.software["NTPClient"]
dmz_ntp_client: NTPClient = dmz_node.software_manager.software["NTPClient"]
assert not internal_ntp_client.time
internal_ntp_client.request_time()
assert not internal_ntp_client.time
assert not dmz_ntp_client.time
dmz_ntp_client.request_time()
assert not dmz_ntp_client.time
firewall.show_rules()
def test_service_allowed_with_rule(dmz_external_internal_network):
"""
Tests that NTP service requests are allowed through the firewall based on ACL rules.
This test verifies the functionality of the firewall in a network scenario where both an internal node and
a node in the DMZ attempt to access NTP services. Initially, no NTP traffic is allowed. The test then
configures ACL rules on the firewall to permit NTP traffic and checks if the NTP clients on the internal
node and DMZ node can successfully request and receive time updates.
Procedure:
1. Assert that the internal node's NTP client initially has no time information due to ACL restrictions.
2. Add ACL rules to the firewall to permit outbound and inbound NTP traffic from the internal network.
3. Trigger an NTP time request from the internal node and assert that it successfully receives time
information.
4. Assert that the DMZ node's NTP client initially has no time information.
5. Add ACL rules to the firewall to permit outbound and inbound NTP traffic from the DMZ.
6. Trigger an NTP time request from the DMZ node and assert that it successfully receives time information.
Asserts:
- The internal node's NTP client has no time information before ACL rules are applied.
- The internal node's NTP client successfully receives time information after the appropriate ACL rules
are applied.
- The DMZ node's NTP client has no time information before ACL rules are applied for the DMZ.
- The DMZ node's NTP client successfully receives time information after the appropriate ACL rules for
the DMZ are applied.
"""
firewall = dmz_external_internal_network.get_node_by_hostname("firewall_1")
internal_node = dmz_external_internal_network.get_node_by_hostname("internal_node")
dmz_node = dmz_external_internal_network.get_node_by_hostname("dmz_node")
internal_ntp_client: NTPClient = internal_node.software_manager.software["NTPClient"]
dmz_ntp_client: NTPClient = dmz_node.software_manager.software["NTPClient"]
assert not internal_ntp_client.time
firewall.internal_outbound_acl.add_rule(action=ACLAction.PERMIT, src_port=Port.NTP, dst_port=Port.NTP, position=1)
firewall.internal_inbound_acl.add_rule(action=ACLAction.PERMIT, src_port=Port.NTP, dst_port=Port.NTP, position=1)
internal_ntp_client.request_time()
assert internal_ntp_client.time
assert not dmz_ntp_client.time
firewall.dmz_outbound_acl.add_rule(action=ACLAction.PERMIT, src_port=Port.NTP, dst_port=Port.NTP, position=1)
firewall.dmz_inbound_acl.add_rule(action=ACLAction.PERMIT, src_port=Port.NTP, dst_port=Port.NTP, position=1)
dmz_ntp_client.request_time()
assert dmz_ntp_client.time
firewall.show_rules()

View File

@@ -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)

View File

@@ -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)

View File

@@ -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]

View File

@@ -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():

View File

@@ -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

View File

@@ -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)

View File

@@ -0,0 +1,87 @@
import pytest
from primaite.simulator.network.airspace import AIR_SPACE, AirSpaceFrequency
from primaite.simulator.network.container import Network
from primaite.simulator.network.hardware.nodes.host.computer import Computer
from primaite.simulator.network.hardware.nodes.network.router import ACLAction
from primaite.simulator.network.hardware.nodes.network.wireless_router import WirelessRouter
from primaite.simulator.network.transmission.network_layer import IPProtocol
from primaite.simulator.network.transmission.transport_layer import Port
@pytest.fixture(scope="function")
def setup_network():
network = Network()
# Configure PC A
pc_a = Computer(
hostname="pc_a",
ip_address="192.168.0.2",
subnet_mask="255.255.255.0",
default_gateway="192.168.0.1",
start_up_duration=0,
)
pc_a.power_on()
network.add_node(pc_a)
# Configure Router 1
router_1 = WirelessRouter(hostname="router_1", start_up_duration=0)
router_1.power_on()
network.add_node(router_1)
# Configure the connection between PC A and Router 1 port 2
router_1.configure_router_interface("192.168.0.1", "255.255.255.0")
network.connect(pc_a.network_interface[1], router_1.network_interface[2])
# Configure Router 1 ACLs
router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.ARP, dst_port=Port.ARP, position=22)
router_1.acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol.ICMP, position=23)
# Configure PC B
pc_b = Computer(
hostname="pc_b",
ip_address="192.168.2.2",
subnet_mask="255.255.255.0",
default_gateway="192.168.2.1",
start_up_duration=0,
)
pc_b.power_on()
network.add_node(pc_b)
# Configure Router 2
router_2 = WirelessRouter(hostname="router_2", start_up_duration=0)
router_2.power_on()
network.add_node(router_2)
# Configure the connection between PC B and Router 2 port 2
router_2.configure_router_interface("192.168.2.1", "255.255.255.0")
network.connect(pc_b.network_interface[1], router_2.network_interface[2])
# Configure Router 2 ACLs
# Configure the wireless connection between Router 1 port 1 and Router 2 port 1
router_1.configure_wireless_access_point("192.168.1.1", "255.255.255.0")
router_2.configure_wireless_access_point("192.168.1.2", "255.255.255.0")
AIR_SPACE.show()
router_1.route_table.add_route(
address="192.168.2.0", subnet_mask="255.255.255.0", next_hop_ip_address="192.168.1.2"
)
# Configure Route from Router 2 to PC A subnet
router_2.route_table.add_route(
address="192.168.0.2", subnet_mask="255.255.255.0", next_hop_ip_address="192.168.1.1"
)
return pc_a, pc_b, router_1, router_2
def test_cross_router_connectivity(setup_network):
pc_a, pc_b, router_1, router_2 = setup_network
# Ensure that PCs can ping across routers before any frequency change
assert pc_a.ping(pc_a.default_gateway), "PC A should ping its default gateway successfully."
assert pc_b.ping(pc_b.default_gateway), "PC B should ping its default gateway successfully."
assert pc_a.ping(pc_b.network_interface[1].ip_address), "PC A should ping PC B across routers successfully."
assert pc_b.ping(pc_a.network_interface[1].ip_address), "PC B should ping PC A across routers successfully."

View File

@@ -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,
)

View File

@@ -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

Some files were not shown because too many files have changed in this diff Show More