Package: async-packet-test
Asynchronous Packet TestingThis library aims to allow the creation of simple assertions and tests to be
run against packets thatmaybe receivedin the future. It's original
intent was to provide a black box testing framework for the P4.org Behavioural
Model, with no dependencies on any special P4 libraries, but I think it has some
utility outside of this context.RunningBecause scapy isusuallyrequired to be run as root (or at least with network
capture capabilities) the tests must also be run as root.Example ScriptIf installing into a virtual environment, running the example would involve:python3 -m venv .venv
source .venv/bin/activate
pip install .
sudo .venv/bin/python ./example.pyA safer alternative may be to create the virtual environment with
copies of the python binaries from the host (rather than symlinks, as is the
default behaviour), and then setting capabilities on them usingsetcap.Unit TestsThe unit tests require the use of adummyinterface. To that end, a script
has been fashioned which, usingpoetry:Installs and sources the virtual environment;Installs the packet test library;Usingsudo, inserts thedummykernel module and creates an interface;Usingsudo, and preserving the poetry environment, runs the tests;Usingsudo, removes thedummyinterface and exits.A Simple ExampleThe following example creates a test for observing a single packet on thelo(loopback) interface.importpytestfromscapy.allimportEther,Dot1Q,TCP,IP,sendpfromasync_packet_test.contextimportmake_pytest_contextfromasync_packet_test.predicatesimportsaw_vlan_tagiface='lo'context=make_pytest_context()test_packet=Ether()/Dot1Q(vlan=102)/IP()/TCP()deftest_saw_vlan_102(context):result=context.expect(iface,saw_vlan_tag(102))sendp(test_packet,iface=iface)assertresult# You could use the `did_not_see_vlan_tag` predicate, however, this demonstrates# a negated assertion. It also demonstrates the usage of a timeout. Otherwise# the test would be sat waiting for a packet it will never see (until the# default timeout is reached)deftest_did_not_see_vlan_103(context):result=context.expect(iface,saw_vlan_tag(103),timeout=0.5)sendp(test_packet,iface=iface)assertnotresultpytestThe test context file includes apytestfixture, yielding aTestContext.
Therefore, the test context and predicates can be used to write unit tests.The unit tests intest/test_async_packet_test.pyshould be fairly easy to grok.Test PredicatesThe core purpose of this repository is testing switch functionality, which
involve the use of predicates which are applied against future packets,
I suppose asynchronously.Predicates have been named to read naturally in English (even if it isn't my
strong suit) and read well in the context of the test. Even though they are
technically classes, in spite ofPEP, they are written in the underscore
style. This is because they are used in a context where they could be perceived
as functions, and that lower case anything is easier to read and above all, in
my opinion (outside testing an outcome), unit tests should be comprehensible.
[[ citation needed ]]Because they are testing something thatmayhappen in the future, they cannot
be evaluated immediately, and therefore their use may not be as obvious as other
testing frameworks.As an example, we might want to test if a specific port saw a packet with a
particular MAC address.def test_saw_mac_address(context):
    dst_mac = 'ab:ab:ab:ab:ab:ab'
    iface = 'veth0'
    future = context.expect(iface, saw_dst_mac(dst_mac))
    assert(future.result() == True)It may be obvious, but what this is saying that we want to test that at some
point in the future thatveth0will see the supplied MAC adress. Thefuturefrom theexpectcall represents the outcome of the test. Consequently, when
retrieving the result, this call will block until the test completes.Writing PredicatesThis is relatively easy. The predicates are objects that test incoming packets,
and are able to carry state from one evaluation from the next. The base test is
as follows:class async_packet_test:
    def on_packet(self, pkt):
        pass

    def stop_condition(self, pkt) -> bool:
        pass

    def on_finish(self, timed_out) -> bool:
        passon_packetreceives avery packet. It is there for updating state.stop_conditionis the to notify the test manager that the test has
completedon_finishreports the result back to the manager, and ultimately the
future given back to the user.Carrying on with our previous examplesaw_dst_mac, the predicate is
constructed as follows:class saw_src_mac(async_packet_test):
    def __init__(self, mac):
        self._mac = mac

    def stop_condition(self, pkt):
        return pkt.haslayer(Ether) and pkt[Ether].src == self._mac

    def on_finish(self, timed_out):
        return not timed_outThis is straightforward as no state is needed between packets; we only need to
test each individual packet for the supplied MAC address. As soon as that MAC is
seen, the test will terminate andon_finishwill be called. Ultimately, the
only thing that needs to be returned is whether the test timed out, if it didn't
time out, the stop condition will never have returnedTrue.on_finishcould have been written by default to test whether or not it timed
out however I wasn't sure whether that was reasonable behaviour or not.Another simple example is testing the count of packets received.This may be difficult to guarantee as ports may receive packets for things
like SSDP, multicast DNS, or any number of packets that may be sent to a port
by the OS as a part of it's normal operationclass packet_count_was(async_packet_test):
    def __init__(self, count):
        self._expected_count = count
        self._received_count = 0

    def on_packet(self, pkt):
        self._received_count += 1

    def on_finish(self, timed_out):
        return self._received_count == self._expected_countAs each packet is received, a counter will be incremented. At the end of the
time out period, the count will be compared with the expected result.The stop condition cannot be used for this purpose as it is called before the
more generalon_packetfunction.It is important to note differences in between behaviour. This test expects a
specific number of packets, if the total count is off, it will return False.
However, it could equally be written so that it terminates as soon as the number
of packets are counted, that is to say, we care about the minimum number of
packets received, and not the total. This could be written as follows:class min_packet_count_was(async_packet_test):
    def __init__(self, count):
        self._expected_count = count
        self._received_count = 0

    def stop_condition(self, pkt):
        self._received_count += 1
        if self._received_count == self._expected_count:
            return True
        return False