From 8de110980c6e758a814836bc1b758c1680747d41 Mon Sep 17 00:00:00 2001 From: Dominik Pataky Date: Sun, 31 Mar 2019 21:23:24 +0200 Subject: [PATCH] Add tests for the collector (main.py). --- README.md | 17 ++++++- tests.py | 144 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 159 insertions(+), 2 deletions(-) create mode 100644 tests.py diff --git a/README.md b/README.md index 547458e..6422886 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,23 @@ Version 9 is the first NetFlow version using templates. Templates make dynamically sized and configured NetFlow data flowsets possible, which makes the collector's job harder. -Copyright 2017, 2018 Dominik Pataky - +Copyright 2017-2019 Dominik Pataky Licensed under MIT License. See LICENSE. +## Running tests +The file `tests.py` contains some tests based on real softflowd export packets. +To create the test packets try the following: + + 1. Run tcpdump/Wireshark on your interface + 2. Produce some sample flows, e.g. surf the web and refresh your mail client. + 3. Save the pcap file to disk. + 4. Run tcpdump/Wireshark again on an interface. + 4. Run softflowd with the `-r ` flag. softflowd reads the captured traffic, produces the flows and exports them. Use the interface you are capturing packets on to send the exports. + 5. Examine the captured traffic. Use Wireshark and set the `CFLOW` "decode as" dissector on the export packets (e.g. based on the port). The `data` fields should then be shown correctly as Netflow payload. + 6. Extract this payload as hex stream. Anonymize the IP addresses with a hex editor if necessary. A recommended hex editor is [bless](https://github.com/afrantzis/bless). + +The collector is run in a background thread. The difference in transmission speed from the exporting client can lead to different results, possibly caused by race conditions during the usage of the JSON output file. + ## Using the collector and analyzer In this repo you also find `main.py` and `analyze_json.py`. diff --git a/tests.py b/tests.py new file mode 100644 index 0000000..025f8a7 --- /dev/null +++ b/tests.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 + +""" +This file contains tests for the softflowd UDP collector saved in main.py +The test packets (defined below as hex streams) were extracted from a "real" softflowd export +based on a sample PCAP capture file. They consist of one export with the templates and three without. + +Two tests are defined, one slow, one fast. During some runs exceptions occured which might hint +to race conditions during reading and writing to the JSON output file. +For now, both tests run successfully. + +Copyright 2017-2019 Dominik Pataky +Licensed under MIT License. See LICENSE. +""" + +import ipaddress +import json +import logging +from pprint import pprint +import random +import socket +import socketserver +import subprocess +import tempfile +from time import sleep +import threading +import unittest + +from main import SoftflowUDPHandler + + +logging.getLogger().setLevel(logging.DEBUG) + +# The flowset with 2 templates and 8 flows +template_packet = '0009000a000000035c9f55980000000100000000000000400400000e00080004000c000400150004001600040001000400020004000a0004000e000400070002000b00020004000100060001003c000100050001000000400800000e001b0010001c001000150004001600040001000400020004000a0004000e000400070002000b00020004000100060001003c000100050001040001447f0000017f000001fb3c1aaafb3c18fd000190100000004b00000000000000000050942c061b04007f0000017f000001fb3c1aaafb3c18fd00000f94000000360000000000000000942c0050061f04007f0000017f000001fb3c1cfcfb3c1a9b0000d3fc0000002a000000000000000000509434061b04007f0000017f000001fb3c1cfcfb3c1a9b00000a490000001e000000000000000094340050061f04007f0000017f000001fb3bb82cfb3ba48b000002960000000300000000000000000050942a061904007f0000017f000001fb3bb82cfb3ba48b00000068000000020000000000000000942a0050061104007f0000017f000001fb3c1900fb3c18fe0000004c0000000100000000000000000035b3c9110004007f0000017f000001fb3c1900fb3c18fe0000003c000000010000000000000000b3c9003511000400' + +# Three packets without templates, each with 12 flows, anonymized +packets = [ + '0009000c000000035c9f55980000000200000000040001e47f0000017f000001fb3c1a17fb3c19fd000001480000000200000000000000000035ea82110004007f0000017f000001fb3c1a17fb3c19fd0000007a000000020000000000000000ea820035110004007f0000017f000001fb3c1a17fb3c19fd000000f80000000200000000000000000035c6e2110004007f0000017f000001fb3c1a17fb3c19fd0000007a000000020000000000000000c6e20035110004007f0000017f000001fb3c1a9efb3c1a9c0000004c0000000100000000000000000035adc1110004007f0000017f000001fb3c1a9efb3c1a9c0000003c000000010000000000000000adc10035110004007f0000017f000001fb3c1b74fb3c1b720000004c0000000100000000000000000035d0b3110004007f0000017f000001fb3c1b74fb3c1b720000003c000000010000000000000000d0b30035110004007f0000017f000001fb3c2f59fb3c1b7100001a350000000a000000000000000000509436061b04007f0000017f000001fb3c2f59fb3c1b710000038a0000000a000000000000000094360050061b04007f0000017f000001fb3c913bfb3c91380000004c0000000100000000000000000035e262110004007f0000017f000001fb3c913bfb3c91380000003c000000010000000000000000e262003511000400', + '0009000c000000035c9f55980000000300000000040001e47f0000017f000001fb3ca523fb3c913b0000030700000005000000000000000000509438061b04007f0000017f000001fb3ca523fb3c913b000002a200000005000000000000000094380050061b04007f0000017f000001fb3f7fe1fb3dbc970002d52800000097000000000000000001bb8730061b04007f0000017f000001fb3f7fe1fb3dbc970000146c000000520000000000000000873001bb061f04007f0000017f000001fb3d066ffb3d066c0000004c0000000100000000000000000035e5bd110004007f0000017f000001fb3d066ffb3d066c0000003c000000010000000000000000e5bd0035110004007f0000017f000001fb3d1a61fb3d066b000003060000000500000000000000000050943a061b04007f0000017f000001fb3d1a61fb3d066b000002a2000000050000000000000000943a0050061b04007f0000017f000001fb3fed00fb3f002c0000344000000016000000000000000001bbae50061f04007f0000017f000001fb3fed00fb3f002c00000a47000000120000000000000000ae5001bb061b04007f0000017f000001fb402f17fb402a750003524c000000a5000000000000000001bbc48c061b04007f0000017f000001fb402f17fb402a75000020a60000007e0000000000000000c48c01bb061f0400', + '0009000c000000035c9f55980000000400000000040001e47f0000017f000001fb3d7ba2fb3d7ba00000004c0000000100000000000000000035a399110004007f0000017f000001fb3d7ba2fb3d7ba00000003c000000010000000000000000a3990035110004007f0000017f000001fb3d8f85fb3d7b9f000003070000000500000000000000000050943c061b04007f0000017f000001fb3d8f85fb3d7b9f000002a2000000050000000000000000943c0050061b04007f0000017f000001fb3d9165fb3d7f6d0000c97b0000002a000000000000000001bbae48061b04007f0000017f000001fb3d9165fb3d7f6d000007f40000001a0000000000000000ae4801bb061b04007f0000017f000001fb3dbc96fb3dbc7e0000011e0000000200000000000000000035bd4f110004007f0000017f000001fb3dbc96fb3dbc7e0000008e000000020000000000000000bd4f0035110004007f0000017f000001fb3ddbb3fb3c1a180000bfee0000002f00000000000000000050ae56061b04007f0000017f000001fb3ddbb3fb3c1a1800000982000000270000000000000000ae560050061b04007f0000017f000001fb3ddbb3fb3c1a180000130e0000001200000000000000000050e820061b04007f0000017f000001fb3ddbb3fb3c1a180000059c000000140000000000000000e8200050061b0400' +] + + +class ThreadedUDPServer(socketserver.ThreadingMixIn, socketserver.UDPServer): + pass + +class TestSoftflowExport(unittest.TestCase): + CONNECTION = ('127.0.0.1', 1337) + COUNT_PACKETS_TO_TEST = 5 + SLEEP_TIME = 0.3 + RUN_ANALYZER = False + + def setUp(self): + logging.debug("Creating temporary JSON output file.") + self.temp_output_file = tempfile.NamedTemporaryFile(prefix="softflowd_") + + # FIXME: templates are saved between test runs, because they are stored with the class + # Maybe the templates should be stored with an instance? + logging.debug("Resetting SoftflowUDPHandler templates.") + SoftflowUDPHandler.templates = {} + + logging.debug("Setting temporary file {} as output for SoftflowUDPHandler".format(self.temp_output_file.name)) + SoftflowUDPHandler.set_output_file(self.temp_output_file.name) + + logging.debug("Writing empty dict to output file.") + with open(self.temp_output_file.name, "w") as fh: + json.dump({}, fh) + + logging.debug("Creating and running the Softflow collector in another thread.") + self.server = ThreadedUDPServer(self.CONNECTION, SoftflowUDPHandler) + self.server_thread = threading.Thread(target=self.server.serve_forever) + self.server_thread.daemon = True + self.server_thread.start() + + logging.debug("Creating UDP socket for client packets.") + self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + + def tearDown(self): + logging.debug("Running tear down procedure.") + self.server.shutdown() + self.server.server_close() + self.server_thread.join() + self.sock.close() + self.temp_output_file.close() + + def _test_export(self): + logging.info("Running UDP client sending raw hex packets with flows.") + + # Get a random index on which the template is sent + template_idx = random.randint(1, self.COUNT_PACKETS_TO_TEST - 1) # 1 for enhanced testing, -1 because randint + + # Save the order of lengths for later check + lens = [] + + for idx in range(self.COUNT_PACKETS_TO_TEST): + # Choose a random packet payload + p = random.choice(packets) + + logging.info("Sending packet {}.".format(idx)) + self.sock.sendto(bytes.fromhex(p), self.CONNECTION) + lens.append(12) + sleep(self.SLEEP_TIME) + + # Randomly inject the template packet + if idx == template_idx: + logging.info("Sending template packet.") + self.sock.sendto(bytes.fromhex(template_packet), self.CONNECTION) + lens.append(8) + sleep(self.SLEEP_TIME) + + with open(self.temp_output_file.name, "r") as fh: + exported = json.load(fh) + + # We got four exports + logging.info("Testing the existence of all exports, including the ones with formerly unknown templates: {} of {}".format( + len(exported.keys()), self.COUNT_PACKETS_TO_TEST + 1)) + self.assertEqual(len(exported.keys()), self.COUNT_PACKETS_TO_TEST + 1) # +1 including the template packet + + # Test lengths of exports + logging.info("Testing the correct lengths of all exports.") + for idx, val in enumerate(exported.values()): + self.assertEqual(len(val), lens[idx]) + + if self.RUN_ANALYZER: + logging.info("Running analyze_json.py") + analyzer = subprocess.run(['python3', 'analyze_json.py', self.temp_output_file.name], stdout=subprocess.PIPE) + for line in analyzer.stdout.split(b"\n"): + print(line.decode()) + + def test_slow(self): + logging.info("Running slow test") + self.SLEEP_TIME = 0.5 + self.COUNT_PACKETS_TO_TEST = 3 + self._test_export() + + def test_fast(self): + logging.info("Running fast test") + self.SLEEP_TIME = 0.1 + self.COUNT_PACKETS_TO_TEST = 30 + self._test_export() + +if __name__ == '__main__': + unittest.main()