#1752 - Moved dns_server ip address from the NIC to the Node. Updated the arcd_uc2_network so that clients and servers have a dns server. Added sys_log entries for DNSServer and DNSClient. MAde the DNSServer always rend a reply, but for the resolved IP address to be empty if it cannot be resolved.
This commit is contained in:
@@ -3,10 +3,10 @@
|
|||||||
© Crown-owned copyright 2023, Defence Science and Technology Laboratory UK
|
© Crown-owned copyright 2023, Defence Science and Technology Laboratory UK
|
||||||
|
|
||||||
DNS Client Server
|
DNS Client Server
|
||||||
======================
|
=================
|
||||||
|
|
||||||
DNS Server
|
DNS Server
|
||||||
----------------
|
----------
|
||||||
Also known as a DNS Resolver, the ``DNSServer`` provides a DNS Server simulation by extending the base Service class.
|
Also known as a DNS Resolver, the ``DNSServer`` provides a DNS Server simulation by extending the base Service class.
|
||||||
|
|
||||||
Key capabilities
|
Key capabilities
|
||||||
@@ -29,7 +29,7 @@ Implementation
|
|||||||
- Extends Service class for integration with ``SoftwareManager``.
|
- Extends Service class for integration with ``SoftwareManager``.
|
||||||
|
|
||||||
DNS Client
|
DNS Client
|
||||||
---------------
|
----------
|
||||||
|
|
||||||
The DNSClient provides a client interface for connecting to the ``DNSServer``.
|
The DNSClient provides a client interface for connecting to the ``DNSServer``.
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ Usage
|
|||||||
|
|
||||||
- Install on a Node via the ``SoftwareManager`` to start the database service.
|
- Install on a Node via the ``SoftwareManager`` to start the database service.
|
||||||
- Service runs on TCP port 53 by default. (TODO: TCP for now, should be UDP in future)
|
- Service runs on TCP port 53 by default. (TODO: TCP for now, should be UDP in future)
|
||||||
- Execute domain name checks with ``check_domain_exists``, providing a ``DNSServer`` ``IPv4Address``.
|
- Execute domain name checks with ``check_domain_exists``.
|
||||||
- ``DNSClient`` will automatically add the IP Address of the domain into its cache
|
- ``DNSClient`` will automatically add the IP Address of the domain into its cache
|
||||||
|
|
||||||
Implementation
|
Implementation
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import secrets
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from ipaddress import IPv4Address, IPv4Network
|
from ipaddress import IPv4Address, IPv4Network
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Literal, Optional, Tuple, Union
|
from typing import Any, Dict, Literal, Optional, Tuple, Union
|
||||||
|
|
||||||
from prettytable import MARKDOWN, PrettyTable
|
from prettytable import MARKDOWN, PrettyTable
|
||||||
|
|
||||||
@@ -89,8 +89,6 @@ class NIC(SimComponent):
|
|||||||
"The Maximum Transmission Unit (MTU) of the NIC in Bytes. Default is 1500 B"
|
"The Maximum Transmission Unit (MTU) of the NIC in Bytes. Default is 1500 B"
|
||||||
wake_on_lan: bool = False
|
wake_on_lan: bool = False
|
||||||
"Indicates if the NIC supports Wake-on-LAN functionality."
|
"Indicates if the NIC supports Wake-on-LAN functionality."
|
||||||
dns_servers: List[IPv4Address] = []
|
|
||||||
"List of IP addresses of DNS servers used for name resolution."
|
|
||||||
connected_node: Optional[Node] = None
|
connected_node: Optional[Node] = None
|
||||||
"The Node to which the NIC is connected."
|
"The Node to which the NIC is connected."
|
||||||
connected_link: Optional[Link] = None
|
connected_link: Optional[Link] = None
|
||||||
@@ -882,6 +880,8 @@ class Node(SimComponent):
|
|||||||
"The NICs on the node."
|
"The NICs on the node."
|
||||||
ethernet_port: Dict[int, NIC] = {}
|
ethernet_port: Dict[int, NIC] = {}
|
||||||
"The NICs on the node by port id."
|
"The NICs on the node by port id."
|
||||||
|
dns_server: Optional[IPv4Address] = None
|
||||||
|
"List of IP addresses of DNS servers used for name resolution."
|
||||||
|
|
||||||
accounts: Dict[str, Account] = {}
|
accounts: Dict[str, Account] = {}
|
||||||
"All accounts on the node."
|
"All accounts on the node."
|
||||||
@@ -931,6 +931,7 @@ class Node(SimComponent):
|
|||||||
sys_log=kwargs.get("sys_log"),
|
sys_log=kwargs.get("sys_log"),
|
||||||
session_manager=kwargs.get("session_manager"),
|
session_manager=kwargs.get("session_manager"),
|
||||||
file_system=kwargs.get("file_system"),
|
file_system=kwargs.get("file_system"),
|
||||||
|
dns_server=kwargs.get("dns_server"),
|
||||||
)
|
)
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self.arp.nics = self.nics
|
self.arp.nics = self.nics
|
||||||
|
|||||||
@@ -128,7 +128,11 @@ def arcd_uc2_network() -> Network:
|
|||||||
|
|
||||||
# Client 1
|
# Client 1
|
||||||
client_1 = Computer(
|
client_1 = Computer(
|
||||||
hostname="client_1", ip_address="192.168.10.21", subnet_mask="255.255.255.0", default_gateway="192.168.10.1"
|
hostname="client_1",
|
||||||
|
ip_address="192.168.10.21",
|
||||||
|
subnet_mask="255.255.255.0",
|
||||||
|
default_gateway="192.168.10.1",
|
||||||
|
dns_server=IPv4Address("192.168.1.10"),
|
||||||
)
|
)
|
||||||
client_1.power_on()
|
client_1.power_on()
|
||||||
client_1.software_manager.install(DNSClient)
|
client_1.software_manager.install(DNSClient)
|
||||||
@@ -141,7 +145,11 @@ def arcd_uc2_network() -> Network:
|
|||||||
|
|
||||||
# Client 2
|
# Client 2
|
||||||
client_2 = Computer(
|
client_2 = Computer(
|
||||||
hostname="client_2", ip_address="192.168.10.22", subnet_mask="255.255.255.0", default_gateway="192.168.10.1"
|
hostname="client_2",
|
||||||
|
ip_address="192.168.10.22",
|
||||||
|
subnet_mask="255.255.255.0",
|
||||||
|
default_gateway="192.168.10.1",
|
||||||
|
dns_server=IPv4Address("192.168.1.10"),
|
||||||
)
|
)
|
||||||
client_2.power_on()
|
client_2.power_on()
|
||||||
client_2.software_manager.install(DNSClient)
|
client_2.software_manager.install(DNSClient)
|
||||||
@@ -167,6 +175,7 @@ def arcd_uc2_network() -> Network:
|
|||||||
ip_address="192.168.1.14",
|
ip_address="192.168.1.14",
|
||||||
subnet_mask="255.255.255.0",
|
subnet_mask="255.255.255.0",
|
||||||
default_gateway="192.168.1.1",
|
default_gateway="192.168.1.1",
|
||||||
|
dns_server=IPv4Address("192.168.1.10"),
|
||||||
)
|
)
|
||||||
database_server.power_on()
|
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.ethernet_port[1], endpoint_a=switch_1.switch_ports[3])
|
||||||
@@ -206,7 +215,11 @@ def arcd_uc2_network() -> Network:
|
|||||||
|
|
||||||
# Web Server
|
# Web Server
|
||||||
web_server = Server(
|
web_server = Server(
|
||||||
hostname="web_server", ip_address="192.168.1.12", subnet_mask="255.255.255.0", default_gateway="192.168.1.1"
|
hostname="web_server",
|
||||||
|
ip_address="192.168.1.12",
|
||||||
|
subnet_mask="255.255.255.0",
|
||||||
|
default_gateway="192.168.1.1",
|
||||||
|
dns_server=IPv4Address("192.168.1.10"),
|
||||||
)
|
)
|
||||||
web_server.power_on()
|
web_server.power_on()
|
||||||
web_server.software_manager.install(DatabaseClient)
|
web_server.software_manager.install(DatabaseClient)
|
||||||
@@ -224,7 +237,11 @@ def arcd_uc2_network() -> Network:
|
|||||||
|
|
||||||
# Backup Server
|
# Backup Server
|
||||||
backup_server = Server(
|
backup_server = Server(
|
||||||
hostname="backup_server", ip_address="192.168.1.16", subnet_mask="255.255.255.0", default_gateway="192.168.1.1"
|
hostname="backup_server",
|
||||||
|
ip_address="192.168.1.16",
|
||||||
|
subnet_mask="255.255.255.0",
|
||||||
|
default_gateway="192.168.1.1",
|
||||||
|
dns_server=IPv4Address("192.168.1.10"),
|
||||||
)
|
)
|
||||||
backup_server.power_on()
|
backup_server.power_on()
|
||||||
network.connect(endpoint_b=backup_server.ethernet_port[1], endpoint_a=switch_1.switch_ports[4])
|
network.connect(endpoint_b=backup_server.ethernet_port[1], endpoint_a=switch_1.switch_ports[4])
|
||||||
@@ -235,6 +252,7 @@ def arcd_uc2_network() -> Network:
|
|||||||
ip_address="192.168.1.110",
|
ip_address="192.168.1.110",
|
||||||
subnet_mask="255.255.255.0",
|
subnet_mask="255.255.255.0",
|
||||||
default_gateway="192.168.1.1",
|
default_gateway="192.168.1.1",
|
||||||
|
dns_server=IPv4Address("192.168.1.10"),
|
||||||
)
|
)
|
||||||
security_suite.power_on()
|
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.ethernet_port[1], endpoint_a=switch_1.switch_ports[7])
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class DNSReply(BaseModel):
|
|||||||
:param domain_name_ip_address: IP Address of the Domain Name requested.
|
:param domain_name_ip_address: IP Address of the Domain Name requested.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
domain_name_ip_address: IPv4Address
|
domain_name_ip_address: Optional[IPv4Address] = None
|
||||||
"IP Address of the Domain Name requested."
|
"IP Address of the Domain Name requested."
|
||||||
|
|
||||||
|
|
||||||
@@ -56,7 +56,6 @@ class DNSPacket(BaseModel):
|
|||||||
:param domain_ip_address: The IP address that was being sought after from the original target domain name.
|
:param domain_ip_address: The IP address that was being sought after from the original target domain name.
|
||||||
:return: A new instance of DNSPacket.
|
:return: A new instance of DNSPacket.
|
||||||
"""
|
"""
|
||||||
if domain_ip_address is not None:
|
self.dns_reply = DNSReply(domain_name_ip_address=domain_ip_address)
|
||||||
self.dns_reply = DNSReply(domain_name_ip_address=IPv4Address(domain_ip_address))
|
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|||||||
@@ -23,7 +23,13 @@ IOSoftwareClass = TypeVar("IOSoftwareClass", bound=IOSoftware)
|
|||||||
class SoftwareManager:
|
class SoftwareManager:
|
||||||
"""A class that manages all running Services and Applications on a Node and facilitates their communication."""
|
"""A class that manages all running Services and Applications on a Node and facilitates their communication."""
|
||||||
|
|
||||||
def __init__(self, session_manager: "SessionManager", sys_log: SysLog, file_system: FileSystem):
|
def __init__(
|
||||||
|
self,
|
||||||
|
session_manager: "SessionManager",
|
||||||
|
sys_log: SysLog,
|
||||||
|
file_system: FileSystem,
|
||||||
|
dns_server: Optional[IPv4Address],
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Initialize a new instance of SoftwareManager.
|
Initialize a new instance of SoftwareManager.
|
||||||
|
|
||||||
@@ -35,6 +41,7 @@ class SoftwareManager:
|
|||||||
self.port_protocol_mapping: Dict[Tuple[Port, IPProtocol], Union[Service, Application]] = {}
|
self.port_protocol_mapping: Dict[Tuple[Port, IPProtocol], Union[Service, Application]] = {}
|
||||||
self.sys_log: SysLog = sys_log
|
self.sys_log: SysLog = sys_log
|
||||||
self.file_system: FileSystem = file_system
|
self.file_system: FileSystem = file_system
|
||||||
|
self.dns_server: Optional[IPv4Address] = dns_server
|
||||||
|
|
||||||
def get_open_ports(self) -> List[Port]:
|
def get_open_ports(self) -> List[Port]:
|
||||||
"""
|
"""
|
||||||
@@ -58,7 +65,9 @@ class SoftwareManager:
|
|||||||
if software_class in self._software_class_to_name_map:
|
if software_class in self._software_class_to_name_map:
|
||||||
self.sys_log.info(f"Cannot install {software_class} as it is already installed")
|
self.sys_log.info(f"Cannot install {software_class} as it is already installed")
|
||||||
return
|
return
|
||||||
software = software_class(software_manager=self, sys_log=self.sys_log, file_system=self.file_system)
|
software = software_class(
|
||||||
|
software_manager=self, sys_log=self.sys_log, file_system=self.file_system, dns_server=self.dns_server
|
||||||
|
)
|
||||||
if isinstance(software, Application):
|
if isinstance(software, Application):
|
||||||
software.install()
|
software.install()
|
||||||
software.software_manager = self
|
software.software_manager = self
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ class DNSClient(Service):
|
|||||||
|
|
||||||
dns_cache: Dict[str, IPv4Address] = {}
|
dns_cache: Dict[str, IPv4Address] = {}
|
||||||
"A dict of known mappings between domain/URLs names and IPv4 addresses."
|
"A dict of known mappings between domain/URLs names and IPv4 addresses."
|
||||||
|
dns_server: Optional[IPv4Address] = None
|
||||||
|
"The DNS Server the client sends requests to."
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
kwargs["name"] = "DNSClient"
|
kwargs["name"] = "DNSClient"
|
||||||
@@ -60,16 +62,12 @@ class DNSClient(Service):
|
|||||||
def check_domain_exists(
|
def check_domain_exists(
|
||||||
self,
|
self,
|
||||||
target_domain: str,
|
target_domain: str,
|
||||||
dest_ip_address: Optional[IPv4Address] = None,
|
|
||||||
dest_port: Optional[Port] = Port.DNS,
|
|
||||||
session_id: Optional[str] = None,
|
session_id: Optional[str] = None,
|
||||||
is_reattempt: bool = False,
|
is_reattempt: bool = False,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Function to check if domain name exists.
|
"""Function to check if domain name exists.
|
||||||
|
|
||||||
:param: target_domain: The domain requested for an IP address.
|
:param: target_domain: The domain requested for an IP address.
|
||||||
:param: dest_ip_address: The ip address of the DNS Server used for domain lookup.
|
|
||||||
:param: dest_port: The port on the DNS Server which accepts domain lookup requests. Default is Port.DNS.
|
|
||||||
:param: session_id: The Session ID the payload is to originate from. Optional.
|
:param: session_id: The Session ID the payload is to originate from. Optional.
|
||||||
:param: is_reattempt: Checks if the request has been reattempted. Default is False.
|
:param: is_reattempt: Checks if the request has been reattempted. Default is False.
|
||||||
"""
|
"""
|
||||||
@@ -78,30 +76,28 @@ class DNSClient(Service):
|
|||||||
|
|
||||||
# check if the domain is already in the DNS cache
|
# check if the domain is already in the DNS cache
|
||||||
if target_domain in self.dns_cache:
|
if target_domain in self.dns_cache:
|
||||||
|
self.sys_log.info(
|
||||||
|
f"DNS Client: Domain lookup for {target_domain} successful, resolves to {self.dns_cache[target_domain]}"
|
||||||
|
)
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
# return False if already reattempted
|
# return False if already reattempted
|
||||||
if is_reattempt:
|
if is_reattempt:
|
||||||
|
self.sys_log.info(f"DNS Client: Domain lookup for {target_domain} failed")
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
# send a request to check if domain name exists in the DNS Server
|
# send a request to check if domain name exists in the DNS Server
|
||||||
software_manager: SoftwareManager = self.software_manager
|
software_manager: SoftwareManager = self.software_manager
|
||||||
software_manager.send_payload_to_session_manager(
|
software_manager.send_payload_to_session_manager(
|
||||||
payload=payload,
|
payload=payload, dest_ip_address=self.dns_server, dest_port=Port.DNS
|
||||||
dest_ip_address=dest_ip_address,
|
|
||||||
dest_port=dest_port,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# check if the domain has been added to cache
|
# recursively re-call the function passing is_reattempt=True
|
||||||
if self.dns_cache.get(target_domain, None) is None:
|
return self.check_domain_exists(
|
||||||
# call function again
|
target_domain=target_domain,
|
||||||
return self.check_domain_exists(
|
session_id=session_id,
|
||||||
target_domain=target_domain,
|
is_reattempt=True,
|
||||||
dest_ip_address=dest_ip_address,
|
)
|
||||||
dest_port=dest_port,
|
|
||||||
session_id=session_id,
|
|
||||||
is_reattempt=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
def send(
|
def send(
|
||||||
self,
|
self,
|
||||||
@@ -125,6 +121,7 @@ class DNSClient(Service):
|
|||||||
# create DNS request packet
|
# create DNS request packet
|
||||||
software_manager: SoftwareManager = self.software_manager
|
software_manager: SoftwareManager = self.software_manager
|
||||||
software_manager.send_payload_to_session_manager(payload=payload, session_id=session_id)
|
software_manager.send_payload_to_session_manager(payload=payload, session_id=session_id)
|
||||||
|
return True
|
||||||
|
|
||||||
def receive(
|
def receive(
|
||||||
self,
|
self,
|
||||||
@@ -150,7 +147,8 @@ class DNSClient(Service):
|
|||||||
payload: DNSPacket = payload
|
payload: DNSPacket = payload
|
||||||
if payload.dns_reply is not None:
|
if payload.dns_reply is not None:
|
||||||
# add the IP address to the client cache
|
# add the IP address to the client cache
|
||||||
self.dns_cache[payload.dns_request.domain_name_request] = payload.dns_reply.domain_name_ip_address
|
if payload.dns_reply.domain_name_ip_address:
|
||||||
return True
|
self.dns_cache[payload.dns_request.domain_name_request] = payload.dns_reply.domain_name_ip_address
|
||||||
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -47,10 +47,7 @@ class DNSServer(Service):
|
|||||||
:param target_domain: The single domain name requested by a DNS client.
|
:param target_domain: The single domain name requested by a DNS client.
|
||||||
:return ip_address: The IP address of that domain name or None.
|
:return ip_address: The IP address of that domain name or None.
|
||||||
"""
|
"""
|
||||||
if target_domain in self.dns_table:
|
return self.dns_table.get(target_domain)
|
||||||
return self.dns_table[target_domain]
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def dns_register(self, domain_name: str, domain_ip_address: IPv4Address):
|
def dns_register(self, domain_name: str, domain_ip_address: IPv4Address):
|
||||||
"""
|
"""
|
||||||
@@ -97,11 +94,19 @@ class DNSServer(Service):
|
|||||||
# cast payload into a DNS packet
|
# cast payload into a DNS packet
|
||||||
payload: DNSPacket = payload
|
payload: DNSPacket = payload
|
||||||
if payload.dns_request is not None:
|
if payload.dns_request is not None:
|
||||||
|
self.sys_log.info(
|
||||||
|
f"DNS Server: Received domain lookup request for {payload.dns_request.domain_name_request} "
|
||||||
|
f"from session {session_id}"
|
||||||
|
)
|
||||||
# generate a reply with the correct DNS IP address
|
# generate a reply with the correct DNS IP address
|
||||||
payload = payload.generate_reply(self.dns_lookup(payload.dns_request.domain_name_request))
|
payload = payload.generate_reply(self.dns_lookup(payload.dns_request.domain_name_request))
|
||||||
|
self.sys_log.info(
|
||||||
|
f"DNS Server: Responding to domain lookup request for {payload.dns_request.domain_name_request} "
|
||||||
|
f"with ip address: {payload.dns_reply.domain_name_ip_address}"
|
||||||
|
)
|
||||||
# send reply
|
# send reply
|
||||||
self.send(payload, session_id)
|
self.send(payload, session_id)
|
||||||
return payload.dns_reply is not None
|
return payload.dns_reply.domain_name_ip_address is not None
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|||||||
@@ -20,11 +20,9 @@ def test_dns_client_server(uc2_network):
|
|||||||
dns_server.show()
|
dns_server.show()
|
||||||
|
|
||||||
# fake domain should not be added to dns cache
|
# fake domain should not be added to dns cache
|
||||||
dns_client.check_domain_exists(
|
assert not dns_client.check_domain_exists(target_domain="fake-domain.com")
|
||||||
target_domain="fake-domain.com", dest_ip_address=IPv4Address(domain_controller.ip_address)
|
|
||||||
)
|
|
||||||
assert dns_client.dns_cache.get("fake-domain.com", None) is None
|
assert dns_client.dns_cache.get("fake-domain.com", None) is None
|
||||||
|
|
||||||
# arcd.com is registered in dns server and should be saved to cache
|
# arcd.com is registered in dns server and should be saved to cache
|
||||||
dns_client.check_domain_exists(target_domain="arcd.com", dest_ip_address=IPv4Address(domain_controller.ip_address))
|
assert dns_client.check_domain_exists(target_domain="arcd.com")
|
||||||
assert dns_client.dns_cache.get("arcd.com", None) is not None
|
assert dns_client.dns_cache.get("arcd.com", None) is not None
|
||||||
|
|||||||
Reference in New Issue
Block a user