Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update NTP tests to use hostname #862

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions daq/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,10 @@ def _analyse_and_write_results(self):

# The device overall fails if any result is unexpected
if result_dict["result"] != required_result:
passes = False
if required_result == 'notfail' and result_dict["result"] != 'fail':
pass
else:
passes = False

if result_dict["result"] == 'gone':
gone = True
Expand Down Expand Up @@ -308,7 +311,7 @@ def _write_category_table(self):
total = 0

results = [[0, 0, 0] for _ in range(len(self._expected_headers))]
result = self._NO_REQUIRED # Overall category result
result = self._NO_REQUIRED # Overall category result is n/a if no tests

for test_name, result_dict in self._results.items():
test_info = self._get_test_info(test_name)
Expand All @@ -335,6 +338,9 @@ def _write_category_table(self):
# TODO remove when info tests are removed
if result_dict["result"] == 'info':
result_dict["result"] = 'pass'
elif (result_dict["result"] == 'skip' and
test_info['required'] == 'notfail'):
result = 'pass'
else:
result = "fail"
else:
Expand Down
19 changes: 13 additions & 6 deletions docs/device_report.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ Overall device result FAIL
|Base|2|FAIL|1/0/1|0/0/0|0/0/0|0/0/0|0/0/0|0/0/0|
|Connection|12|FAIL|3/5/4|0/0/0|0/0/0|0/0/0|0/0/0|0/0/0|
|Security|13|FAIL|2/4/4|0/0/0|0/0/1|0/0/0|0/2/0|0/0/0|
|NTP|2|PASS|2/0/0|0/0/0|0/0/0|0/0/0|0/0/0|0/0/0|
|NTP|3|PASS|2/0/1|0/0/0|0/0/0|0/0/0|0/0/0|0/0/0|
|DNS|1|SKIP|0/0/1|0/0/0|0/0/0|0/0/0|0/0/0|0/0/0|
|Communication|2|PASS|2/0/0|0/0/0|0/0/0|0/0/0|0/0/0|0/0/0|
|Protocol|2|FAIL|0/0/0|0/0/0|0/1/1|0/0/0|0/0/0|0/0/0|
Expand All @@ -64,7 +64,7 @@ Syntax: Pass / Fail / Skip

|Expectation|pass|fail|skip|gone|
|---|---|---|---|---|
|Required Pass|10|1|10|8|
|Required Pass|10|1|11|8|
|Required Pass for PoE Devices|0|0|1|0|
|Required Pass for BACnet Devices|0|1|2|0|
|Required Pass for IoT Devices|0|0|1|0|
Expand Down Expand Up @@ -97,7 +97,8 @@ Syntax: Pass / Fail / Skip
|skip|dns.network.hostname_resolution|DNS|Required Pass|Device did not send any DNS requests|
|pass|dot1x.dot1x|Other|Other|Authentication for 9a:02:57:1e:8f:01 succeeded.|
|pass|ntp.network.ntp_support|NTP|Required Pass|Using NTPv4.|
|pass|ntp.network.ntp_update|NTP|Required Pass|Device clock synchronized.|
|pass|ntp.network.ntp_update_dhcp|NTP|Required Pass|Device clock synchronized.|
|skip|ntp.network.ntp_update_dns|NTP|Required Pass|Device not configured for NTP via DNS|
|skip|poe.switch.power|PoE|Required Pass for PoE Devices|No local IP has been set, check system config|
|fail|protocol.bacext.pic|Protocol|Required Pass for BACnet Devices|PICS file defined however a BACnet device was not found.|
|skip|protocol.bacext.version|Protocol|Required Pass for BACnet Devices|Bacnet device not found.|
Expand Down Expand Up @@ -557,11 +558,17 @@ Device supports NTP version 4.
--------------------
RESULT pass ntp.network.ntp_support Using NTPv4.
--------------------
ntp.network.ntp_update
ntp.network.ntp_update_dhcp
--------------------
Device synchronizes its time to the NTP server.
Device synchronizes its time to the NTP server using DHCP
--------------------
RESULT pass ntp.network.ntp_update Device clock synchronized.
RESULT pass ntp.network.ntp_update_dhcp Device clock synchronized.
--------------------
ntp.network.ntp_update_dns
--------------------
Device synchronizes its time to the NTP server using DNS
--------------------
RESULT skip ntp.network.ntp_update_dns Device not configured for NTP via DNS
--------------------
connection.network.mac_oui
--------------------
Expand Down
9 changes: 7 additions & 2 deletions resources/setups/common/tests_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,14 @@
"required": "pass",
"expected": "Required Pass"
},
"ntp.network.ntp_update": {
"ntp.network.ntp_update_dhcp": {
"category": "NTP",
"required": "pass",
"required": "notfail",
"expected": "Required Pass"
},
"ntp.network.ntp_update_dns": {
"category": "NTP",
"required": "notfail",
"expected": "Required Pass"
},
"communication.network.min_send": {
Expand Down
4 changes: 2 additions & 2 deletions subset/network/Dockerfile.test_network
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ RUN $AG update && $AG install openjdk-8-jre

RUN $AG update && $AG install openjdk-8-jdk git

RUN $AG update && $AG install python python-setuptools python-pip netcat
RUN $AG update && $AG install python3.8 python3-setuptools python3-pip netcat

RUN $AG update && $AG install curl

RUN pip install scapy
RUN python3.8 -m pip install scapy

COPY subset/network/ .

Expand Down
Empty file added subset/network/__init__.py
Empty file.
214 changes: 153 additions & 61 deletions subset/network/ntp_tests.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
from __future__ import absolute_import, division
from scapy.all import NTP, rdpcap
import sys
import os
import re
import json
import test_result
from scapy.all import NTP, rdpcap, DNS

arguments = sys.argv

test_request = str(arguments[1])
pcap_file = str(arguments[2])
device_address = str(arguments[3])

report_filename = 'ntp_tests.txt'
ignore = '%%'
summary_text = ''
result = 'fail'
dash_break_line = '--------------------\n'

description_ntp_support = 'Device supports NTP version 4.'
description_ntp_update = 'Device synchronizes its time to the NTP server.'
description_ntp_update_dhcp = 'Device synchronizes its time to the NTP server using DHCP'
description_ntp_update_dns = 'Device synchronizes its time to the NTP server using DNS'

NTP_VERSION_PASS = 4
LOCAL_PREFIX = '10.20.'
Expand All @@ -26,22 +27,29 @@
OFFSET_ALLOWANCE = 0.128
LEAP_ALARM = 3

IP_REGEX = r'(([0-9]{1,3}\.){3}[0-9]{1,3})'
NTP_SERVER_IP_SUFFIX = '.2'
NTP_SERVER_HOSTNAME = 'ntp.daqlocal'
MODULE_CONFIG_PATH = '/config/device/module_config.json'

TEST_DHCP = 'dhpc'
TEST_DNS = 'dns'

def write_report(string_to_append):
with open(report_filename, 'a+') as file_open:
file_open.write(string_to_append)


# Extracts the NTP version from the first client NTP packet
def ntp_client_version(capture):
""" Extracts the NTP version from the first client NTP packet """
client_packets = ntp_packets(capture, MODE_CLIENT)
if len(client_packets) == 0:
return None
return ntp_payload(client_packets[0]).version


# Filters the packets by type (NTP)
def ntp_packets(capture, mode=None):
""" Filters the packets by type (NTP) """
packets = []
for packet in capture:
if NTP in packet:
Expand All @@ -53,53 +61,84 @@ def ntp_packets(capture, mode=None):
return packets


# Extracts the NTP payload from a packet of type NTP
def ntp_configured_by_dns():
"""Checks module_config for parameter that NTP is configured using DNS

Parameter must be (bool) True, else will be considered false
"""
module_config = open(MODULE_CONFIG_PATH)
module_config = json.load(module_config)
try:
ntp_by_dns = (module_config['modules']['network']['ntp_dns'])
except KeyError:
ntp_by_dns = False

return ntp_by_dns is True


def ntp_payload(packet):
""" Extracts the NTP payload from a packet of type NTP """
ip = packet.payload
udp = ip.payload
ntp = udp.payload
return ntp


def test_ntp_support():
capture = rdpcap(pcap_file)
packets = ntp_packets(capture)
if len(packets) > 0:
version = ntp_client_version(packets)
if version is None:
add_summary("No NTP packets received.")
return 'skip'
if version == NTP_VERSION_PASS:
add_summary("Using NTPv" + str(NTP_VERSION_PASS) + ".")
return 'pass'
else:
add_summary("Not using NTPv" + str(NTP_VERSION_PASS) + ".")
return 'fail'
else:
add_summary("No NTP packets received.")
return 'skip'
def dns_requests_for_hostname(hostname, packet_capture):
"""Checks for DNS requests for a given hostname

Args:
packet_capture path to tcpdump packet capture file
hostname hostname to look for

Returns:
true/false if any matching DNS requests detected to hostname
"""
capture = rdpcap(packet_capture)
fqdn = hostname + '.'
for packet in capture:
if DNS in packet:
if packet.qd.qname.decode("utf8") == fqdn:
return True
return False


def ntp_server_from_ip(ip_address):
"""Returns the IP address of the NTP server provided by DAQ

Args:
ip_address: IP address of the device under test

Returns:
IP address of NTP server
"""
return re.sub(r'\.\d+$', NTP_SERVER_IP_SUFFIX, ip_address)


def check_ntp_synchronized(ntp_packets_array, ntp_server_ip):
""" Checks if NTP packets indicate a device is syncronized with the provided
IP address

Args:
packet_capture Array of scapy object of packet capture with NTP
packets from ntp_packets()
ntp_server_ip IP address of server to checK

Returns:
boolean true/false if synchronized with provided NTP server.
"""

def test_ntp_update():
capture = rdpcap(pcap_file)
packets = ntp_packets(capture)
if len(packets) < 2:
add_summary("Not enough NTP packets received.")
return 'skip'
# Check that DAQ NTP server has been used
using_local_server = False
local_ntp_packets = []
for packet in packets:
# Packet is to or from local NTP server
if ((packet.payload.dst.startswith(LOCAL_PREFIX) and
packet.payload.dst.endswith(NTP_SERVER_SUFFIX)) or
(packet.payload.src.startswith(LOCAL_PREFIX) and
packet.payload.src.endswith(NTP_SERVER_SUFFIX))):
using_local_server = True
using_given_server = False
for packet in ntp_packets_array:
# Packet is to or from NTP server
if (packet.payload.dst == ntp_server_ip or packet.payload.src == ntp_server_ip):
using_given_server = True
local_ntp_packets.append(packet)
if not using_local_server or len(local_ntp_packets) < 2:
add_summary("Device clock not synchronized with local NTP server.")
return 'fail'

if not using_given_server or len(local_ntp_packets) < 2:
return False

# Obtain the latest NTP poll
p1 = p2 = p3 = p4 = None
for i in range(len(local_ntp_packets)):
Expand All @@ -122,9 +161,10 @@ def test_ntp_update():
p3 = p4 = None
else:
p3 = local_ntp_packets[i]

if p1 is None or p2 is None:
add_summary("Device clock not synchronized with local NTP server.")
return 'fail'
return False

t1 = ntp_payload(p1).sent
t2 = ntp_payload(p1).time
t3 = ntp_payload(p2).sent
Expand All @@ -142,26 +182,78 @@ def test_ntp_update():

offset = abs((t2 - t1) + (t3 - t4))/2
if offset < OFFSET_ALLOWANCE and not ntp_payload(p1).leap == LEAP_ALARM:
add_summary("Device clock synchronized.")
return 'pass'
return True
else:
add_summary("Device clock not synchronized with local NTP server.")
return 'fail'
return False


def add_summary(text):
global summary_text
summary_text = summary_text + " " + text if summary_text else text
def test_ntp_support():
""" Tests support for NTPv4 """
capture = rdpcap(pcap_file)
packets = ntp_packets(capture)
test_ntp = test_result.test_result(name='ntp.network.ntp_support',
description=description_ntp_support)
if len(packets) > 0:
version = ntp_client_version(packets)
if version is None:
test_ntp.add_summary("No NTP packets received.")
test_ntp.result = test_result.SKIP
if version == NTP_VERSION_PASS:
test_ntp.add_summary("Using NTPv" + str(NTP_VERSION_PASS) + ".")
test_ntp.result = test_result.PASS
else:
test_ntp.add_summary("Not using NTPv" + str(NTP_VERSION_PASS) + ".")
test_ntp.result = test_result.FAIL
else:
test_ntp.add_summary("No NTP packets received.")
test_ntp.result = test_result.SKIP

test_ntp.write_results(report_filename)

write_report("{b}{t}\n{b}".format(b=dash_break_line, t=test_request))

def test_ntp_update():
"""Runs NTP Update Test for both DHCP and DNS"""
# Used to always print test output in the same order
ntp_tests = {}
ntp_tests[TEST_DHCP] = test_result.test_result(
name='ntp.network.ntp_update_dhcp',
description=description_ntp_update_dhcp)
ntp_tests[TEST_DNS] = test_result.test_result(
name='ntp.network.ntp_update_dns',
description=description_ntp_update_dns)

capture = rdpcap(pcap_file)
packets = ntp_packets(capture)

if len(packets) < 2:
for test in ntp_tests:
ntp_tests[test].add_summary("Not enough NTP packets received.")
ntp_tests[test].result = test_result.SKIP
else:
test_dns = ntp_configured_by_dns()
local_ntp_ip = ntp_server_from_ip(device_address)
device_sync_local_server = check_ntp_synchronized(packets, local_ntp_ip)

if test_dns:
active = TEST_DNS
ntp_tests[TEST_DHCP].add_summary("Device not configured for NTP via DHCP")
ntp_tests[TEST_DHCP].result = test_result.SKIP
else:
active = TEST_DHCP
ntp_tests[TEST_DNS].add_summary("Device not configured for NTP via DNS")
ntp_tests[TEST_DNS].result = test_result.SKIP

if device_sync_local_server:
ntp_tests[active].add_summary("Device clock synchronized.")
ntp_tests[active].result = test_result.PASS
else:
ntp_tests[active].add_summary("Device clock not synchronized with local NTP server.")
ntp_tests[active].result = test_result.FAIL

ntp_tests[TEST_DHCP].write_results(report_filename)
ntp_tests[TEST_DNS].write_results(report_filename)

if test_request == 'ntp.network.ntp_support':
write_report("{d}\n{b}".format(b=dash_break_line, d=description_ntp_support))
result = test_ntp_support()
test_ntp_support()
elif test_request == 'ntp.network.ntp_update':
write_report("{d}\n{b}".format(b=dash_break_line, d=description_ntp_update))
result = test_ntp_update()

write_report("RESULT {r} {t} {s}\n".format(r=result, t=test_request, s=summary_text.strip()))
test_ntp_update()
Loading